# Handoff: SoftLAN Launcher redesign A modern, gamer-friendly redesign of the SoftLAN local-network game launcher, replacing the current basic UI with a Steam-inspired dark layout that keeps high usability while adding cover art, state-coded actions, a game-detail overlay, and an in-app Settings dialog. --- ## About the design files The files in `design_reference/` are **design references created in HTML/React via Babel-in-the-browser** — prototypes built to communicate the intended look, layout, and behavior. They are **not production code to copy directly**. The target codebase is a **Tauri + React** desktop app. The task is to **recreate these designs inside that codebase**, using its existing patterns (component conventions, state management, routing, IPC to Rust for filesystem / process work). Use the design files for: - Exact pixel/spacing/color/typography values - Component composition and interactions - Copy and microcopy - Animation easings/durations But: - Don't ship the Babel-in-browser setup or import the .jsx files as-is - Don't keep the `` / design-canvas wrapping — that's only for presenting variants - Don't ship the Tweaks panel — it's superseded by the in-app **Settings dialog** (see "Screens" below) - Re-implement using whatever the codebase uses (Vite + plain JSX, CSS modules / styled-components / tailwind, etc.) ## Fidelity **High-fidelity.** Final colors, typography, spacing, and interactions are decided. Pixel-fidelity to the mock is the goal — recreate exactly, using the codebase's libraries/patterns. Only deviate where the codebase has its own dictate (e.g. an existing button primitive that's near-identical). ## Layout variants The HTML mock includes two chrome variants — **A (single-row)** and **B (two-row)** — to choose from. **The user selected A as the primary direction.** Implement A. Variant B is left in the reference for context only. --- ## Screens / views ### 1. Main library (variant A — primary) The default screen. A grid of game cards over a dark, gradient-tinted background. **Layout (top-to-bottom):** 1. **Top bar** — single row, sticky, full width, 64px tall, semi-transparent dark with backdrop-blur. Background `rgba(10,14,19,0.65)` + `backdrop-filter: blur(20px) saturate(140%)`. Border-bottom `1px solid rgba(255,255,255,0.06)`. Contents, left-to-right with 18px gap and 24px horizontal padding: - **Brand** — 28×28px rounded square in `--accent` (default `#3b82f6`) with the letter "S" in Bebas Neue 20px white. Next to it, the wordmark "SoftLAN" in 15px / 700 weight `--t-1` `#e6edf3`. - **Segmented filter pills** — pill-shaped container (`background var(--bg-2) #131b25`, `1px solid rgba(255,255,255,0.06)`, `border-radius: 999px`, `padding: 4px`). Three buttons: - `All Games` · count chip - `Local` · count chip - `Installed` · count chip Active button has an animated pill thumb (background `var(--accent)`, transitions `left` and `width` with `cubic-bezier(.4,1.2,.5,1)` over 220ms), text becomes white, count-chip background goes `rgba(0,0,0,0.25)`. Inactive: text `var(--t-2) #9aa6b4`, count-chip background `rgba(255,255,255,0.08)`. `Local` = installed *or* downloaded-but-not-yet-installed. `Installed` = installed only. `All Games` = everything available on the network. - **Search field** — 36px tall, min-width 320px (flex 0 1 380px). `background var(--bg-2)`, `1px solid var(--bd-1)`, `border-radius: 8px`, padding `0 12px`. Has a leading magnifying-glass icon (14×14, `currentColor`) and a trailing "/" kbd hint (`background rgba(255,255,255,0.06)`, `border-radius: 4px`, font `11px ui-monospace`). On focus: border becomes `color-mix(in srgb, var(--accent) 60%, var(--bd-2))`, background `var(--bg-1)`, ring `box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent)`. The "/" key shortcut should focus the search. - **Sort menu** — 36px button, same surface style as search. Label `Sort: ` plus 13px sort-bars icon and 11px chevron. Click reveals dropdown menu below. Options: `Name (A–Z)`, `Size (largest)`, `Recently Played`, `Status`. - **Game directory button** — 36px button, max-width 360px. Folder icon, "Game directory" label (600 weight `--t-1`), then the current path in `ui-monospace` 11.5px `--t-3` truncated with leading ellipsis when long (e.g. `…s/Desktop/eti_games_AFTER_LAN_2025`). - **Kebab menu** (`⋮`) — 36×36 button with same surface. Menu items: `Settings` (opens Settings dialog — see below), `Refresh library`, separator, `Unpack logs`, `About SoftLAN`. 2. **Results bar** — 18px top padding inside the scroll wrapper, 24px horizontal. Flex row with space-between: - Left: `Showing N of M games` in 12.5px `var(--t-2)` (strong is `var(--t-1)`). - Right: compact **storage meter** — 200px min-width, 4px-tall horizontal bar with two stacked segments (`installed` and `local`), plus a 11px text row underneath: ` 78 GB installed 41 GB local 384 GB free`. Squares are 8×8px rounded 2px, colored `var(--accent)` and `color-mix(var(--accent), 55%)`. 3. **Grid** — CSS grid with `repeat(auto-fill, minmax(188px, 1fr))` at default density, 16px gap, 24px horizontal padding, 32px bottom padding. Scrolls vertically. - Density: `compact` → min 148, gap 12. `normal` → min 188, gap 16. `large` → min 244, gap 20. **Game card** (see "Game card" below for full anatomy). --- ### 2. Game detail overlay Opens when the user **clicks anywhere on a game card except the action button**. Modal over a scrim. Closes on scrim click, Esc key, or the close button. Should also work via keyboard nav (Enter on focused card). **Scrim:** absolutely positioned over the launcher, `inset: 0`, `z-index: 100`, `background: rgba(4,7,11,0.7)`, `backdrop-filter: blur(8px)`, fade-in 180ms. Padding 32px, content centered. **Modal panel:** `min(880px, 100%)` wide, `background: linear-gradient(180deg, var(--bg-2) 0%, var(--bg-1) 100%)`, `1px solid var(--bd-2)`, `border-radius: 14px`, drop shadow `0 30px 80px -10px rgba(0,0,0,0.7)`. Scales in from 0.96 with 250ms `cubic-bezier(.3,1.3,.4,1)`. **Modal structure (top-to-bottom):** 1. **Hero banner** — `aspect-ratio: 16/7`. Full-bleed cover art rendered as a banner (same gradient + accent treatment as the small cards, scaled up). Bottom-fade gradient `linear-gradient(180deg, transparent 40%, var(--bg-2) 100%)` so text reads. - **State chip** in the top-left of the hero (same chip style as on cards — see Game Card). - **Close button** top-right: 32×32 square, `background rgba(8,12,16,0.7)`, `1px solid var(--bd-2)`, `border-radius: 8px`, `backdrop-filter: blur(8px)`, X icon. - **Title overlay** in bottom-left at `left: 28px, right: 28px, bottom: 22px`: - Tags row — small uppercase pills (`background rgba(8,12,16,0.6)`, `1px solid var(--bd-2)`, `border-radius: 4px`, `padding: 3px 8px`, `font 11px / 600 / 0.04em letter-spacing`) - **Title** as `

` — system sans 32px / 700 / -0.015em, white, text-shadow `0 4px 24px rgba(0,0,0,0.6)`. **Not Bebas Neue** here — this is normal UI typography, not stylized cover art. 2. **Body** — 22px top, 26px bottom, 28px horizontal: - **Meta grid** — 4-column CSS grid, 12px gap. Each cell: `padding 10px 12px`, `background rgba(255,255,255,0.025)`, `1px solid var(--bd-1)`, `border-radius: 8px`. Cells (in order): `Size` (e.g. 8.2 GB), `Players` (icon + range), `Version` (mono, e.g. 2018.04.12), `Status` (Installed / Local / Not downloaded). - **Description** — 14px / 1.55 line-height, `var(--t-2)`, `text-wrap: pretty`, `max-width: 64ch`. - **Actions row** — flex row, 10px gap, 4px top padding: - Primary action button (44px tall, see "Action button" below — Play / Install / Download depending on state) - If `state === 'installed'`: ghost-button **Uninstall** — 44px, `background rgba(255,255,255,0.04)`, `1px solid var(--bd-2)`, `border-radius: 8px`, text `#f87171`, trash icon. On hover: bg `rgba(239,68,68,0.10)`, border `rgba(239,68,68,0.40)`, text `#fca5a5`. - If `state === 'local'`: ghost-button **Delete from disk** (same danger styling). - Spacer (`flex: 1`). - Ghost-button **View files** (neutral) — opens system file manager at the game folder. --- ### 3. Settings dialog Opens when the user clicks **Settings** from the kebab menu. Same modal-scrim treatment as the game-detail modal, but the panel is narrower (`min(640px, 100%)`) and styled as a list of preferences. **Structure:** ``` ┌─────────────────────────────────────────┐ │ Settings [×] │ ← head: 22 28 18, 1px bottom border ├─────────────────────────────────────────┤ │ │ │ APPEARANCE │ ← section title: 10.5px / 700 / 0.12em / uppercase / --t-3 │ │ │ Accent color │ ← row label: 14px / 600 / --t-1 │ Used for primary actions and highlights │ ← row hint: 12px / --t-3 │ ⬤⬤⬤⬤⬤⬤ │ ← 6 swatches, right-aligned │ │ │ Background │ │ Backdrop behind the library │ │ [Flat │Gradient│Animat]│ ← segmented radio │ │ │ LIBRARY │ │ │ │ Grid density │ │ How tightly cards are packed │ │ [Compact│Normal│Large]│ │ │ │ Cover aspect │ │ Shape of the cover art on each card │ │ [Box-art│Square│Banner│ │ │ ├─────────────────────────────────────────┤ │ [Done] │ ← foot: 14 22 18, 1px top border └─────────────────────────────────────────┘ ``` **Sections** are separated by 26px gap (column flex). Rows within a section: 14px gap. Each **row** is flex row with space-between (24px gap): - Left (`settings-row-info`): label (14px / 600 / `--t-1`) + hint (3px-top, 12px / `--t-3`) - Right (`settings-row-control`): the control **Color swatch picker:** flex row of 8px-gapped buttons. Each swatch is 32×32, `border-radius: 9px`, no border. Inside, a 100% × 100% rounded-8 colored dot with inset shadow `0 0 0 1px rgba(255,255,255,0.08)`. Hover: dot scales 1.06. **Active**: dot has ring `box-shadow: 0 0 0 2px var(--bg-2), 0 0 0 4px ` and shows a centered white check icon with drop-shadow `0 1px 2px rgba(0,0,0,0.5)`. Six accent options: Blue `#3b82f6`, Cyan `#22d3ee`, Violet `#a855f7`, Green `#22c55e`, Amber `#f59e0b`, Red `#ef4444`. **Segmented radio:** inline-flex with `background var(--bg-3) #1a2330`, `1px solid var(--bd-1)`, `border-radius: 8px`, `padding: 3px`. Each button: 30px tall, `padding: 0 14px`, `border-radius: 6px`, `font 12.5px / 600`. Inactive: `color var(--t-2)`. Active: `background var(--accent)`, `color white`, inset top shadow `0 1px 0 rgba(255,255,255,0.18)`. **Done button:** filled button in `--accent`, 36px tall, 13.5px / 600. Closes the dialog. Persisted settings (write through to local storage / Tauri config): - `accent`: one of the six hex values above. Default `#3b82f6`. - `bg`: `flat` | `gradient` | `animated`. Default `gradient`. - `density`: `compact` | `normal` | `large`. Default `normal`. - `aspect`: `box` | `square` | `banner`. Default `box`. --- ## Game card The unit element of the library grid. **Container:** flex column. `background: linear-gradient(180deg, var(--bg-2) 0%, var(--bg-1) 100%)`, `1px solid var(--bd-1)`, `border-radius: 10px`, `overflow: hidden`. Cursor pointer. **Hover/focus state:** - `transform: translateY(-2px)` (180ms `cubic-bezier(.4,1.2,.5,1)`) - `border-color: color-mix(in srgb, var(--accent) 45%, var(--bd-2))` - Box-shadow `0 14px 30px -16px color-mix(var(--accent), 50%, black), 0 0 0 1px color-mix(var(--accent), 30%, transparent)` - Cover inner image scales to 1.03 (350ms cubic-bezier) - Focus-visible: same lift + 2px solid accent outline ### Anatomy (top to bottom) 1. **Cover wrap** — `width: 100%`, `aspect-ratio: 2/3` (box) / `1/1` (square) / `16/9` (banner). `position: relative`, `overflow: hidden`, fallback bg `var(--bg-3)`. 2. **Cover** (inside cover-wrap, `position: absolute; inset: 0`): - **Base gradient** — diagonal (`linear-gradient(<110-170deg>, c1, c2)` — angle hashed from game id for variety). Per-game color pair from the game's `cover` metadata. - **Radial accent blob** — `radial-gradient(ellipse at % %, 38, transparent 55%)`. x/y also hashed from id. - **Grain / scanline** — two `repeating-linear-gradient` overlays at 1px intervals, `mix-blend-mode: overlay`, opacity 0.7. - **Decorative SVG mark** — preserveAspectRatio bottom-right, draws a triangle and dot in the accent color at 12% opacity. Variation via id hash. - **Title** absolutely positioned at bottom-left, padding `14px`. Font `Bebas Neue` (free Google Font, fallback `Oswald, Impact, "Arial Narrow Bold", sans-serif`), 400 weight, uppercase, `letter-spacing: 0.018em`, `line-height: 1.02`, white, text-shadow `0 4px 16px , 0 1px 0 rgba(0,0,0,0.3)`. Size scales by title length: 26px for ≤14 chars, 21px for ≤20, 17px for ≤26, 15px for longer (box aspect; see `components.jsx → GameCover` for square/banner variants). - **Vignette** — `linear-gradient(180deg, transparent 30%, rgba(0,0,0,0.62) 100%)` over the whole cover, painted *after* the title (so the dark gradient is behind the title visually — title is z-index 2). - **State chip** in top-right: pill with backdrop-blur, `background rgba(8,12,16,0.78)`, `1px solid rgba(255,255,255,0.08)`, `border-radius: 999px`, `padding: 4px 9px`, font `10.5px / 600`. A 6×6 colored dot (green `#22c55e` for installed, amber `#f59e0b` for local; hidden for "not downloaded") + label. Dot has glow `box-shadow: 0 0 8px `. - **Multiplayer badge** in top-left: same pill style but slightly lighter background (`rgba(8,12,16,0.65)`). Tiny "users" icon + player range (e.g. `2–32`). Always visible — every LAN game is multiplayer. 3. **Card body** — `padding: 11px 12px 12px`, flex column, 8px gap: - **Title** — game's full (mixed-case) title in 13.5px / 600 / `--t-1`, single line, ellipsis on overflow. - **Meta line** — 11.5px tabular-nums, `--t-3`: size · genre. Dot separator at 50% opacity. - **Action button** (full width) — primary action depending on state, see below. ### Action button A single button per card with the *primary action for the current state*. Color-coded as the main affordance for state at a glance. ``` state label button style ───────────── ────────── ──────────────────────────────────────────── not downloaded Download neutral: bg rgba(255,255,255,0.08), 1px var(--bd-2), text var(--t-1) local Install bg var(--accent), text white, inset top hl installed Play bg linear-gradient(180deg, #2bd07f 0%, #1aa460 100%), text white, inset top hl ``` Common sizing: 32px tall (card) or 44px tall (modal). `border-radius: 7px` (card) / 8px (modal). `font 12.5px / 600` (card) / `14px / 600` (modal). 6px gap between icon and label. Icons: filled play triangle, download arrow, install arrow-onto-line (all 12×12). Hover: `filter: brightness(1.12)`. Active: `transform: scale(0.98)`. **Uninstall / Delete-from-disk** are NOT on the card — only in the detail overlay (as ghost-danger buttons). --- ## Filter controls — variant B (not used, kept for reference) The two-row chrome has a different filter style — **underlined tabs with counts**, like browser tabs: - Buttons: no background, `padding: 10px 14px 12px`, font `13.5px / 600`. Color `--t-2` inactive, `--t-1` active. - Count chip after label: 11.5px / 600, `padding 1px 7px`, rounded pill. Inactive bg `rgba(255,255,255,0.06)`, text `--t-3`. Active bg `rgba(255,255,255,0.10)`, text `--t-1`. - Active tab has a 2px underline at the bottom (`left: 12px, right: 12px`) in `--accent`, animated in via opacity + scaleX (220ms cubic-bezier). Implement only if you decide variant A doesn't work after building. --- ## Interactions & behavior - **Click game card** (anywhere except the action button) → open detail overlay. - **Click action button on card** → trigger the state-appropriate action without opening the overlay. `e.stopPropagation()` on the button. - **Press / (slash)** → focus the search input. - **Type in search** → live-filter the visible grid by title or tag (case-insensitive substring). - **Click filter tab / segmented pill** → change filter. - **Click sort button** → opens dropdown; click an option → re-sort grid; clicking outside the menu closes it. - **Hover game card** → lift + accent border glow + cover image scale 1.03. - **Click "Game directory" button** → open native folder picker via Tauri; on selection, rescan library. - **Click "Settings"** in kebab → open Settings dialog. Changes apply live and persist immediately (no Apply button — Done just closes). - **Click "Unpack logs"** in kebab → opens a logs viewer (separate window or modal — out of scope for this design). - **Click "Refresh library"** in kebab → re-runs the library scan. - **Esc** → closes any open modal (detail overlay, Settings). ### Transitions / animations - Card hover: `180ms cubic-bezier(.4,1.2,.5,1)` on transform/border, `350ms cubic-bezier(.4,1.2,.5,1)` on cover scale. - Modal fade-in: scrim `opacity 0 → 1` over 180ms ease; modal `transform: scale(.96) translateY(8px) → scale(1) translateY(0)` and opacity over 250ms `cubic-bezier(.3,1.3,.4,1)`. - Segmented filter thumb: `220ms cubic-bezier(.4,1.2,.5,1)` on `left` and `width`. - Underline tab indicator (variant B): `200ms` on opacity, `250ms cubic-bezier(.4,1.2,.5,1)` on `transform: scaleX`. - Animated background option: subtle 18s ease-in-out infinite alternate background-position shift on two accent-tinted radial gradients. --- ## State management Recommend Zustand or a single React context for global launcher state; Tauri commands for filesystem and process operations. **Library state** (rebuilt on `refresh`): ```ts type Game = { id: string; title: string; size: number; // GB version: string; // "YYYY.MM.DD" desc: string; state: 'installed' | 'local' | 'none'; players: string; // e.g. "2–32" tags: string[]; cover: { c1: string; c2: string; accent: string; mood?: string }; }; ``` **UI state:** ```ts type LauncherUI = { filter: 'all' | 'local' | 'installed'; sort: 'az' | 'size' | 'recent' | 'state'; query: string; openGameId: string | null; settingsOpen: boolean; }; ``` **Persisted settings:** see Settings dialog section. Persist via Tauri's plugin-store or a local JSON file in app data dir. **Storage figures:** computed by summing game sizes per state, plus free-space query via Tauri. --- ## Design tokens ### Color | token | value | usage | |---|---|---| | `--bg-0` | `#0a0e13` | launcher background | | `--bg-1` | `#0f151c` | card bottom gradient stop | | `--bg-2` | `#131b25` | top bar / card top / search bg | | `--bg-3` | `#1a2330` | settings segmented bg / cover fallback | | `--bg-4` | `#232f3e` | (reserved) | | `--bd-1` | `rgba(255,255,255,0.06)` | subtle border | | `--bd-2` | `rgba(255,255,255,0.10)` | stronger border | | `--bd-3` | `rgba(255,255,255,0.16)` | scrollbar thumb | | `--t-1` | `#e6edf3` | primary text | | `--t-2` | `#9aa6b4` | secondary text | | `--t-3` | `#6b7785` | muted text / metadata | | `--t-4` | `#4a5663` | (reserved) | | `--ok` | `#22c55e` | "installed" dot | | `--warn` | `#f59e0b` | "local" dot | | `--danger` | `#ef4444` | destructive actions | | `--accent` | user-selected, default `#3b82f6` | primary actions, focus rings, brand mark | ### Typography - **UI font** — system sans stack: `-apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI", system-ui, sans-serif` - **Cover-art display font** — `"Bebas Neue"` (Google Fonts, weight 400) with fallback `"Oswald", Impact, "Arial Narrow Bold", sans-serif` - **Monospace** — `ui-monospace, "SF Mono", Menlo, Consolas, monospace` (used for: directory path, version field in detail overlay) Sizing reference: - Brand wordmark: 15 / 700 - Modal title: 32 / 700 / -0.015em - Card title: 13.5 / 600 - Filter pill label: 12.5 / 600 - Settings row label: 14 / 600 - Section title (settings): 10.5 / 700 / 0.12em uppercase - Meta (size · genre, etc.): 11.5 / tabular-nums / `--t-3` - Hint text: 12 / `--t-3` ### Spacing & radii - Card radius: 10px - Modal radius: 14px - Pill/control radius: 8px (search, sort, dir button), 999px (filter segmented), 7px (action button) - Common gaps: 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 28 - Card body padding: 11 12 12 ### Shadows - Card hover: `0 14px 30px -16px color-mix(var(--accent), 50%, black), 0 0 0 1px color-mix(var(--accent), 30%, transparent)` - Modal: `0 30px 80px -10px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.04)` - Brand mark: `0 6px 20px -6px color-mix(var(--accent), 60%, black), inset 0 1px 0 rgba(255,255,255,0.22)` - Action button (filled): `0 6px 16px -8px , inset 0 1px 0 rgba(255,255,255,0.22)` --- ## Assets Cover art in the design files is **stylized placeholder art** — generated entirely from the game's metadata (color pair + accent color + id hash for angle/blob position) plus the title typeset in Bebas Neue. There are no real game cover image assets in this design. In the production app, the launcher should ideally use real cover-art when available (fetch from IGDB / Steam / local game folder) and fall back to the placeholder generator for games without art. The placeholder generator is in `design_reference/components.jsx → GameCover`. The icon set (search, play, install, download, folder, kebab, sort, users, close, check, chevron, trash) is in `design_reference/components.jsx → Icon`. They are 12-14px inline SVGs using `currentColor`. Reuse as-is or substitute with the codebase's existing icon library at the same visual weight. Fonts to load: ```html ``` --- ## File reference ``` design_reference/ ├── SoftLAN Launcher.html ← entry; wires React + Babel, mounts ├── styles.css ← all visual styles (CSS custom props + components) ├── data.jsx ← mock GAMES array + filter/sort helpers + STORAGE mock ├── components.jsx ← Icon, GameCover, StateChip, ActionButton, GameCard, │ SegmentedFilters, UnderlineFilters, SearchField, │ SortMenu, StorageMeter, DirectoryButton, KebabMenu, │ GameDetailModal, SettingsDialog └── launcher.jsx ← component composing chrome + grid + modals ``` To preview the design in a browser: 1. Open `SoftLAN Launcher.html` in a static-server (e.g. `python -m http.server` from the folder). 2. You'll see a design canvas with all variants (A, B, C, D, E) side-by-side. Click an artboard's expand button to view it full-screen. 3. The "Tweaks" floating panel in the bottom-right is dev-only — it lets you live-change accent / density / aspect / background. In the production app these live in the Settings dialog (variant E). --- ## Out of scope / open questions for the developer - **Unpack logs viewer** — referenced from kebab menu but not designed. Surface it as a separate window or a slide-in panel, dev's choice. - **Empty state** — when filter returns 0 games (e.g. nothing installed yet). Show a centered message with a CTA to install the first game. - **Error state on action** — if a Download / Install fails, show inline error on the affected card (red border + retry button), and a toast. - **Progress state** — when a game is actively downloading or installing, the action button should become a progress bar with a cancel affordance. Not designed; recommend: replace the button with a progress bar of the same dimensions, percentage text on the left, cancel "×" on the right. - **Keyboard arrow nav** — arrow keys should move focus between cards in the grid; not implemented in the mock but mentioned as a goal.