docs(design): specify download progress treatment

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
This commit is contained in:
2026-05-20 23:09:46 +02:00
parent 51216b7281
commit e308009a08
5 changed files with 438 additions and 18 deletions
+106 -2
View File
@@ -196,6 +196,7 @@ 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).
@@ -206,6 +207,107 @@ Hover: `filter: brightness(1.12)`. Active: `transform: scale(0.98)`.
---
## 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) 3826%, 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; // 01, 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:
@@ -255,7 +357,9 @@ type Game = {
size: number; // GB
version: string; // "YYYY.MM.DD"
desc: string;
state: 'installed' | 'local' | 'none';
state: 'installed' | 'local' | 'downloading' | 'none';
progress?: number; // 01 — present only when state === 'downloading'
speed?: number; // MB/s — present only when state === 'downloading'
players: string; // e.g. "232"
tags: string[];
cover: { c1: string; c2: string; accent: string; mood?: string };
@@ -378,5 +482,5 @@ To preview the design in a browser:
- **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.
- **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.