Move the design contract for choosing the library game folder out of the top
bar and into Settings > Library. The launcher chrome now reserves the right
edge for the kebab/menu controls, while Settings owns the required folder path
and its set/unset states.
The reference mock now persists `gameFolder: string | null` instead of a
boolean, adds an exercisable GameFolderField, and includes matching CSS so the
prototype reflects the documented interaction. This is still design/reference
only; no runtime Tauri settings code changes here.
Test Plan:
- git diff --cached --check
Refs: none
@@ -32,19 +32,20 @@ The HTML mock includes two chrome variants — **A (single-row)** and **B (two-r
---
## Changes since v3
- **Game-folder button removed from the top bar.** Setting the games directory is a one-time action — it doesn't deserve permanent real estate in the chrome. The button is gone from both top-bar variants, freeing the right zone for the kebab menu alone (variant A) / the storage meter + kebab pair (variant B).
- **Game folder moved into Settings → Library.** Now a row inside the Settings dialog, styled like the other Library rows. Two visual states (set / not-set) carry over from the old button — see "Settings dialog → Library → Game folder" below.
- **Persisted setting renamed.** `gameFolderSet: boolean` → `gameFolder: string | null`. The actual path is now persisted, not just a "is it configured?" flag. Default is `null` (unset on first run; user must pick a folder before the library scans).
## Changes since v2
- **Top bar layout reorganized.** The single-row top bar is now structured as three visual zones (still one row on wide windows):
- **Left:** brand mark + wordmark.
- **Center (semantically the "search cluster"):** segmented filter pills · search field · sort menu. The **search field is positioned at the geometric center of the window** — filter pills sit immediately to its left, sort menu immediately to its right.
- **Right:** game-folder button + kebab menu.
- **Right:** kebab menu (game-folder configuration has moved into Settings — see v3 changes).
- Below ~1100 px of launcher width (container query), the three zones collapse into a single left-to-right flowing row (no wrap, no centering). Implement via container query on the launcher root; viewport media query is acceptable if your codebase doesn't use container queries yet.
- See "Top bar (variant A)" below for the full spec and rationale.
- **Game-directory button redesigned.** No more inline path display. The button is now an icon + short label, with the full path moved to the tooltip. Two states:
- **Not set / invalid path:** label `Set game folder`, subtle red border, slightly red-tinted hover, tooltip = `Please select a game folder`. "Invalid" (a path is stored but doesn't exist on disk) is treated identically to "not set" — no separate warning state for now.
- Click behavior unchanged — opens the native folder picker via Tauri.
- See "Game-folder button" below for the full spec.
## Changes since v1
@@ -82,11 +83,9 @@ The default screen. A grid of game cards over a dark, gradient-tinted background
- **Right zone (col 3, flex space-between with two sub-groups):**
- **Sort menu** (pinned left, hugging search) — 36 px button, same surface style as search. Label `Sort: <bold value>` plus 13 px sort-bars icon and 11 px chevron. Click reveals dropdown menu below. Options: `Name (A–Z)`, `Size (largest)`, `Recently Played`, `Status`. This is the only thing on the *left* side of the right zone — it's part of the search cluster, so it hugs the search.
- **Game-folder button + kebab menu** (grouped together, pinned far-right) — 12 px gap between the two. They both belong to the "app-level" controls (settings/identity/menus), so they live as a tight pair on the right edge.
- **Game-folder button** — see "Game-folder button" below.
- **Kebab menu** (`⋮`) — 36×36 button with same surface as search. Menu items: `Settings` (opens Settings dialog), `Refresh library`, separator, `Unpack logs`, `About SoftLAN`.
- **Kebab menu** (`⋮`, pinned far-right) — 36×36 button with same surface as search. Menu items: `Settings` (opens Settings dialog), `Refresh library`, separator, `Unpack logs`, `About SoftLAN`. This is the only "app-level" control left in the top bar; the game-folder picker has moved into Settings.
**Narrow-window fallback** (container width < 1100 px): the grid is replaced by a single `display: flex; flex-wrap: nowrap; gap: 16px` row. All items align left-to-right in source order (brand → filter → search → sort → game-folder → kebab). The search field becomes `flex: 1 1 auto` so it absorbs remaining slack. The geometric centering is abandoned at narrow widths because there isn't enough horizontal slack for it to read cleanly. Implement via container query (`@container launcher (max-width: 1100px)`) on the launcher root; a viewport media query is an acceptable fallback if you're not using container queries yet.
**Narrow-window fallback** (container width < 1100 px): the grid is replaced by a single `display: flex; flex-wrap: nowrap; gap: 16px` row. All items align left-to-right in source order (brand → filter → search → sort → kebab). The search field becomes `flex: 1 1 auto` so it absorbs remaining slack. The geometric centering is abandoned at narrow widths because there isn't enough horizontal slack for it to read cleanly. Implement via container query (`@container launcher (max-width: 1100px)`) on the launcher root; a viewport media query is an acceptable fallback if you're not using container queries yet.
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)`).
@@ -176,6 +175,11 @@ Opens when the user clicks **Settings** from the kebab menu. Same modal-scrim tr
│ │
│ LIBRARY │
│ │
│ Game folder │
│ Parent directory where games are │
│ downloaded and installed │
│ [📁 /home/pfs/…/eti_games [Change…]] │ ← folder field (340×36)
│ │
│ Grid density │
│ How tightly cards are packed │
│ [Compact│Normal│Large]│
@@ -199,6 +203,10 @@ Opens when the user clicks **Settings** from the kebab menu. Same modal-scrim tr
- **Username** — `<input type="text">` wrapped in a styled container: 220px wide, 36px tall, `background var(--bg-3)`, `1px solid var(--bd-1)`, `border-radius: 8px`, `padding: 0 12px`. Input itself is transparent/borderless, `font 13.5px / 600`, color `--t-1`, placeholder `"Enter a username"` in `--t-3` / 500. `maxLength={24}`, `spellCheck={false}`. On focus the container gets `background var(--bg-2)`, border `var(--accent)`, and an accent focus ring `box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent)`.
- **Language** — same segmented-radio control as Background / Density / Cover aspect, with two options: `English` (value `'en'`) and `Deutsch` (value `'de'`). Active option gets the accent fill, same as the other segmented radios.
**Library section.** Three rows: **Game folder** (new in v3 — moved out of the top bar), **Grid density**, **Cover aspect**.
- **Game folder** — see "Game-folder field" below. The first row in the section because it's the only setting users *must* configure for the launcher to work; density and aspect are pure preference.
**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`.
@@ -214,36 +222,40 @@ Persisted settings (write through to local storage / Tauri config):
-`gameFolder`: `string | null`. Absolute path to the parent directory where games are downloaded and installed. Default `null` (unset on first run). See "Game-folder field" below.
---
## Game-folder button
## Game-folder field
The top-bar control that exposes the user's currently-configured game folder (the parent directory under which all per-game subfolders live). Sits in the right zone of the top bar, between the sort menu and the kebab menu. Click anywhere on it → native folder picker via Tauri; on selection, rescan library.
A settings row inside the **Library** section of the Settings dialog. Exposes the user's currently-configured game folder (the parent directory under which all per-game subfolders live).
**Why it lives in Settings now:** users set this once at install time and basically never touch it again. A permanent top-bar button burned high-attention chrome on a control nobody used after day one. Settings is where one-time configuration belongs.
Two visual states, driven by whether `settings.gameFolder` resolves to an accessible directory:
| **Set & valid** | path is configured and exists on disk | `Game folder` | default `--bd-1` | full configured path |
| **Not set / invalid** | path is `null`/empty, or path is set but the directory no longer exists | `Set game folder` | tinted red (`color-mix(in srgb, var(--danger) 35%, var(--bd-1))`) | `Please select a game folder` |
| **Set & valid** | path is configured and exists on disk | full path in mono, truncated head-first | default `--bd-1` | `Change…` (neutral pill) |
| **Not set / invalid** | path is `null`/empty, or path is set but the directory no longer exists | `Not set` in red | tinted red (`color-mix(in srgb, var(--danger) 35%, var(--bd-1))`) + faint red bg tint | `Choose…` (accent-filled pill) |
"Invalid" is intentionally collapsed into the same visual state as "not set" — the user's job is identical (open the picker and pick a folder), so we don't differentiate. If we later need a distinct "missing" state (e.g. to show the *last known* path so the user can re-attach an external drive), introduce a third state then; for now, keep it simple.
**Anatomy:**`inline-flex`, `height: 36px`, `padding: 0 14px 0 12px`, `gap: 8px`. `background: var(--bg-2)`, `border-radius: 8px`. Children, left to right:
**Anatomy:**`inline-flex`,`width: 340px`,`height: 36px`, `padding: 0 4px 0 12px`, `gap: 8px`. `background: var(--bg-3)`, `border-radius: 8px`. Children, left to right:
1.**Folder icon** — `Icon.folder` from `components.jsx`, 14×14, `currentColor`.
2.**Label** — 12.5 px / 600, `var(--t-1)`, `white-space: nowrap`, `line-height: 1`. Text content depends on state (see table above).
1.**Folder icon** — `Icon.folder` from `components.jsx`, 14×14, `var(--t-3)` (set state) or `#f87171` (unset state).
2.**Path display** — `flex: 1`, mono `12px / ui-monospace`, `--t-1`, single line, `overflow: hidden; text-overflow: ellipsis`. **`direction: rtl` + `unicode-bidi: plaintext`** so truncation happens from the head and the leaf folder (the part the user actually cares about) stays visible. When unset: shows the word `Not set` in 12.5 px / 600 / `#f87171` instead.
3.**Action button** — 28 px tall pill, `border-radius: 6px`, `padding: 0 12px`, `font 12.5px / 600`. Set state: neutral `rgba(255,255,255,0.06)` bg, label `Change…`. Unset state: `var(--accent)` fill at 85% alpha, white text, label `Choose…` (so the call-to-action reads stronger when the path needs picking). Click → native folder picker via Tauri; on selection, write through to `settings.gameFolder` and rescan library.
**Hover:** border darkens to `--bd-2` (set state) or to `color-mix(in srgb, var(--danger) 55%, var(--bd-2))`with a faint `color-mix(in srgb, var(--danger) 8%, var(--bg-2))` background tint (unset state) so the bad-state hover reads as "this is the thing you need to fix".
**Hover:** border darkens to `--bd-2` (set state) or to `color-mix(in srgb, var(--danger) 55%, var(--bd-2))`(unset state). The inner button has its own hover (background opacity bumps).
**Accessibility:** the `aria-label` carries the full state in words — `"Game folder: <path>"` when set, `"Set game folder"` when unset — so screen readers don't have to interpret the colored dot.
**Accessibility:** the path itself is selectable text inside the field; the action button carries `aria-label="Change game folder"` / `"Choose game folder"`. The full path is also exposed via `title` on the path-display element so it's reachable on hover when truncated.
**Why no inline path anymore?**The previous design squeezed the full path into the button as truncated monospace (`…/eti_games_AFTER_LAN_2025`). It rarely showed the meaningful part of the path on real-world configurations, ate horizontal space, and competed with the actual primary controls (filter / search / sort) for the top bar's attention budget. The information is still one mouseover away in the tooltip; on the surface, the label communicates whether configuration is needed — which is what users actually need to glance at.
**Why no inline path on the previous top-bar button anymore?**Original design squeezed the full path into a top-bar button as truncated mono. It rarely showed the meaningful part of the path on real-world configurations, ate horizontal space, and competed with the actual primary controls (filter / search / sort) for the top bar's attention budget. In the new home (Settings), the field has all the width it needs to show a useful prefix of the path while still keeping the leaf visible — and it's only on screen when the user is actively reconfiguring.
**Data:** the component takes a `path: string | null`prop. `null` (or empty/whitespace string) renders the unset state; any non-empty string renders the set state. In production, derive `path` from your settings store; if you want to additionally validate existence, do the `fs.metadata` check in the store / a hook and pass `null` when the directory is missing.
**Data:** the component takes `value: string | null`and an `onChange(next: string)` callback. `null` (or empty/whitespace string) renders the unset state; any non-empty string renders the set state. The `onChange` callback should fire only on successful picker confirmation (not on cancel). In production, derive `value` from your settings store; if you want to additionally validate existence, do the `fs.metadata` check in the store / a hook and pass `null` when the directory is missing.
**Dev preview:** the prototype's Tweaks panel exposes a `Game folder set` toggle (under the *Library* section) that flips the`gameFolderSet` flag wired into the Launcher. This is dev-only — in the real app the state comes from the settings store, **not** from a user-facing toggle. Don't ship it.
**Dev preview:** the prototype's Tweaks panel exposes a `Game folder`**text field** (under the *Library* section) that writes directly to`t.gameFolder`. Type any string to simulate the set state; clear it to simulate the unset state. This is dev-only — in the real app the value comes from the settings store via the picker, **not** from a free-form text input. Don't ship the Tweaks panel.
---
@@ -425,8 +437,8 @@ Implement only if you decide variant A doesn't work after building.
- **Click "Game folder" button** → open native folder picker via Tauri; on selection, rescan library. The button itself indicates whether a valid folder is currently configured (green dot + `Game folder`) or not (red dot + `Set game folder`) — see "Game-folder button" above.
- **Click "Settings"** in kebab → open Settings dialog. Changes apply live and persist immediately (no Apply button — Done just closes).
- **Click "Change…" / "Choose…" in the Settings → Library → Game folder row** → open native folder picker via Tauri; on selection, write to `settings.gameFolder` and rescan library. The field indicates whether a valid folder is currently configured (mono path + neutral `Change…`) or not (red `Not set` + accent-filled `Choose…`) — see "Game-folder field" above.
- **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).
@@ -480,12 +492,13 @@ type LauncherUI = {
**Persisted settings** (mirror of Settings dialog state):
```ts
typeLauncherSettings={
username: string;// new
language:'en'|'de';// new
username: string;
language:'en'|'de';
accent: string;// hex from the curated 6-color palette
bg:'flat'|'gradient'|'animated';
density:'compact'|'normal'|'large';
aspect:'box'|'square'|'banner';
gameFolder: string|null;// v3: moved out of top bar, persists actual path
@@ -590,7 +603,7 @@ To preview the design in a browser:
- **D** — detail overlay for a downloaded-but-not-installed game (CoD 4) → shows **Install + Delete from disk**
- **E** — detail overlay for a downloading game (AvP) → shows the live progress component + **Cancel**
- **F** — Settings dialog open, with the new **Profile** section at the top
3. The "Tweaks" floating panel in the bottom-right is dev-only — it lets you live-change every persisted setting (username / language / accent / background / density / aspect). In the production app these live in the Settings dialog.
3. The "Tweaks" floating panel in the bottom-right is dev-only — it lets you live-change every persisted setting (username / language / accent / background / density / aspect / game folder). In the production app these all live in the Settings dialog.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.