e308009a08
Document and mock the redesigned downloading state for the launcher. The reference now replaces the action button slot with a dedicated progress primitive, covers both card and detail-modal layouts, and records the sizing, number formatting, container-query fallback, and sample-data expectations that implementation work should follow. This commit keeps the design package separate from application code so the next UI/backend changes can be reviewed against a stable reference. Test Plan: - git diff --cached --check Refs: local design reference update
487 lines
31 KiB
Markdown
487 lines
31 KiB
Markdown
# 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 `<deck>` / 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: <bold value>` 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 <strong>N</strong> 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: `<sq> 78 GB installed <sq> 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 `<h2>` — 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 <swatch-color>` 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 <x>% <y>%, <accent>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 <c2 + alpha>, 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 <color>`.
|
||
- **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
|
||
downloading — progress see "Download progress" below — the button slot is replaced with a live progress component
|
||
```
|
||
|
||
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).
|
||
|
||
---
|
||
|
||
## Download progress (state === 'downloading')
|
||
|
||
When a game is actively downloading, the **action-button slot is replaced** by an inline progress component. The component is its own visual primitive (`DownloadProgress` in `components.jsx`); it is NOT a button with a `<progress>` child. Two layouts share the same primitive:
|
||
|
||
### Shared visuals
|
||
|
||
- Container: `border-radius: 7px` (card) / `9px` (modal), `1px solid color-mix(in srgb, var(--accent) 45%, var(--bd-2))`, faint accent halo via `box-shadow`. `container-type: inline-size` (we use container queries for graceful fallback, see below).
|
||
- **Progress fill** (`.dl-fill`): absolutely positioned, `width: <pct>%`, animated via `transition: width 480ms cubic-bezier(.4,0,.2,1)`. Background is a vertical gradient of `color-mix(in srgb, var(--accent) 38–26%, transparent)`. Right edge gets a 1px accent rule + accent glow.
|
||
- **Live shimmer** on top of the fill: `repeating-linear-gradient(115deg, transparent 0 14px, rgba(255,255,255,0.05) 14px 22px)` panned via `animation: dl-stripe 1.4s linear infinite`, `mix-blend-mode: screen`. Subtle — it reads as "live" without being distracting.
|
||
- **Pulse dot** (`.dl-pulse`): 7px accent dot with an outward-pulsing `box-shadow` ring (1.4s ease-out infinite). Visual cue that the network transfer is active.
|
||
- **Tabular numerics** on all values (`font-variant-numeric: tabular-nums`) so the percentage and speed don't jitter as digits roll over.
|
||
|
||
### Card layout (`.dl-md`, replaces the 32px action button)
|
||
|
||
A single row. Two values, separated by `justify-content: space-between`:
|
||
|
||
- **Left:** `<pulse> <pct>%` — 12px / 600, `var(--t-1)`. `%` glyph at 0.55 opacity. e.g. `• 32%`.
|
||
- **Right:** `<speed>` — 11px / 500, `var(--t-2)`. Short format: `49 MB/s` (no decimals at card scale).
|
||
|
||
Heights match the action button per density: 30px compact / 32px normal / 34px large. Padding `0 10px` (9 compact / 12 large). Font sizes scale similarly (see `styles.css`).
|
||
|
||
**Container-query graceful degradation** — this is the important part, it has to fit every aspect/density combo:
|
||
|
||
```css
|
||
@container (max-width: 132px) { .dl-md .dl-speed { display: none; } .dl-md-row { justify-content: center; gap: 6px; } }
|
||
@container (max-width: 96px) { .dl-md .dl-pulse { display: none; } }
|
||
```
|
||
|
||
At 132 px and below, the speed disappears and the percentage centres. At 96 px and below, the pulse dot also drops, leaving just the percentage. This is what guarantees `compact` density + `box` aspect (the narrowest combination) still reads cleanly.
|
||
|
||
The state chip in the cover corner still says "Downloading" — we are deliberately NOT repeating that label inside the progress bar.
|
||
|
||
### Detail-overlay layout (`.dl-lg`, replaces the 44px modal action button)
|
||
|
||
Fixed 56px height. CSS-grid with three columns and two rows:
|
||
|
||
```
|
||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||
grid-template-areas:
|
||
"primary pct cancel"
|
||
"secondary pct cancel";
|
||
```
|
||
|
||
- **Primary row** (`.dl-lg-primary`, top-left) — pulse dot + the uppercase live label `DOWNLOADING` in `color-mix(in srgb, var(--accent) 80%, white)`, 13px / 600, `letter-spacing: 0.02em`. This is the only place the word "Downloading" appears in the component.
|
||
- **Secondary row** (`.dl-lg-secondary`, bottom-left) — the live stats. 12px, three groups separated by `·` (0.45 opacity):
|
||
1. `<strong>11.4 GB</strong> / 35 GB` (`var(--t-1)` strong + `var(--t-2)` rest)
|
||
2. `47.6 MB/s` (`var(--t-1)`)
|
||
3. `8 min left` (`var(--t-2)`)
|
||
- **pct column** — large percentage, 20px / 700, `letter-spacing: -0.01em`, `var(--t-1)`. `%` glyph at 12px / 600 / 0.55 opacity.
|
||
- **cancel column** — 28×28 square, `1px solid var(--bd-2)`, `border-radius: 6px`, X icon. Hover: bg `rgba(239,68,68,0.12)`, border `rgba(239,68,68,0.40)`, text `#fca5a5`. Cancelling reverts the game to its prior state (`local` if any data was kept, `none` otherwise) — dev decides the underlying behavior.
|
||
|
||
**Graceful degradation in narrow modals:**
|
||
|
||
```css
|
||
@container (max-width: 380px) { .dl-lg-secondary .dl-eta, .dl-lg-secondary .dl-sep-eta { display: none; } }
|
||
```
|
||
|
||
The ETA drops first if the modal narrows; bytes + speed stay (they're the actionable numbers). The pct/cancel column never collapses.
|
||
|
||
### Number formatting
|
||
|
||
All helpers live in `data.jsx`:
|
||
|
||
- `fmtSpeed(mbps)` — `49.4 MB/s` below 100, `MM MB/s` (rounded) at/above 100. Used in `.dl-lg`.
|
||
- `fmtSpeedShort(mbps)` — always rounded: `49 MB/s`. Used in `.dl-md` so the card stays compact.
|
||
- `fmtBytes(gb)` — `<1 GB → MB rounded`, `<10 GB → up to 2 decimals` (trailing zeros stripped: `2.35 GB`, `2.3 GB`, `2 GB`), `≥10 GB → 1 decimal max` (`11.4 GB`, `35 GB`).
|
||
- `fmtEta(seconds)` — `< 60s → "N s"`, `< 60min → "N min"`, else `"H h M min"`.
|
||
|
||
Keep these formats; they're tuned so the secondary row never wraps at normal modal width.
|
||
|
||
### Data shape
|
||
|
||
The `Game` type gains a `downloading` state plus two transient fields:
|
||
|
||
```ts
|
||
type Game = {
|
||
// … existing fields …
|
||
state: 'installed' | 'local' | 'downloading' | 'none';
|
||
progress?: number; // 0–1, only when state === 'downloading'
|
||
speed?: number; // current throughput in MB/s
|
||
};
|
||
```
|
||
|
||
In the real app, `progress` and `speed` come from the download worker (Tauri command emitting events). The mock's `useLiveDownload(game)` hook (in `components.jsx`) is just a placeholder — 600ms `setInterval` advancing `progress` proportional to `speed`, with `speed` smoothed via a low-pass filter and small random drift so the number doesn't look fake. Replace with a `useEffect` that subscribes to your real progress events; the rendering layer needs nothing else.
|
||
|
||
Filter changes:
|
||
- `Local` filter includes `installed` + `local` + `downloading` (in-flight downloads belong on the Local tab — you're managing them).
|
||
- Sort by `state` orders `installed < local < downloading < none`.
|
||
|
||
### State chip
|
||
|
||
Add a fourth entry to `STATE_META`:
|
||
|
||
```js
|
||
downloading: { label: 'Downloading', dot: 'var(--accent)' }
|
||
```
|
||
|
||
Dot uses the live accent so it visually ties to the progress fill.
|
||
|
||
---
|
||
|
||
## 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' | 'downloading' | 'none';
|
||
progress?: number; // 0–1 — present only when state === 'downloading'
|
||
speed?: number; // MB/s — present only when state === 'downloading'
|
||
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 <color>, 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
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap">
|
||
```
|
||
|
||
---
|
||
|
||
## File reference
|
||
|
||
```
|
||
design_reference/
|
||
├── SoftLAN Launcher.html ← entry; wires React + Babel, mounts <App>
|
||
├── 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 ← <Launcher> 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** — designed. See "Download progress" section above. The action-button slot is swapped for a live `DownloadProgress` component (card + modal variants with container-query fallback for narrow tiles). Wire it to your real progress events; the rendering layer is dev-ready.
|
||
- **Keyboard arrow nav** — arrow keys should move focus between cards in the grid; not implemented in the mock but mentioned as a goal.
|