From 19ae1938f621b3a3efeb0ffbf098120bd487a7f3 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 20:12:17 +0200 Subject: [PATCH] docs(design): move game folder selection into settings 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 --- design/README.md | 71 +++++++++++-------- design/design_reference/SoftLAN Launcher.html | 8 +-- design/design_reference/components.jsx | 30 ++++++++ design/design_reference/launcher.jsx | 6 +- design/design_reference/styles.css | 71 +++++++++++++++++++ 5 files changed, 148 insertions(+), 38 deletions(-) diff --git a/design/README.md b/design/README.md index 8983845..90d6dbf 100644 --- a/design/README.md +++ b/design/README.md @@ -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: - - **Set & valid:** label `Game folder`, default border, tooltip = full configured path. - - **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: ` 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 N 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** — `` 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 ` 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): - `bg`: `flat` | `gradient` | `animated`. Default `gradient`. - `density`: `compact` | `normal` | `large`. Default `normal`. - `aspect`: `box` | `square` | `banner`. Default `box`. +- `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: -| State | Trigger | Label | Border | Tooltip | +| State | Trigger | Path display | Border | Button label | |---|---|---|---|---| -| **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: "` 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 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 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 type LauncherSettings = { - 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 }; ``` @@ -577,8 +590,8 @@ design_reference/ ├── 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 +│ SortMenu, StorageMeter, KebabMenu, +│ GameDetailModal, SettingsDialog (incl. GameFolderField) └── launcher.jsx ← component composing chrome + grid + modals ``` @@ -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. --- diff --git a/design/design_reference/SoftLAN Launcher.html b/design/design_reference/SoftLAN Launcher.html index b4a3ab9..2848d30 100644 --- a/design/design_reference/SoftLAN Launcher.html +++ b/design/design_reference/SoftLAN Launcher.html @@ -36,9 +36,9 @@ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "density": "normal", "aspect": "square", "bg": "gradient", - "username": "d", + "username": "ddidderr", "language": "en", - "gameFolderSet": true + "gameFolder": "\/some\/folder\/to\/games" }/*EDITMODE-END*/; const ACCENTS = ['#3b82f6', '#22d3ee', '#a855f7', '#22c55e', '#f59e0b', '#ef4444']; @@ -116,8 +116,8 @@ function App() { onChange={(v) => setTweak('aspect', v)}/> - setTweak('gameFolderSet', v)}/> + setTweak('gameFolder', v)}/> ); diff --git a/design/design_reference/components.jsx b/design/design_reference/components.jsx index 19c642b..abf91cb 100644 --- a/design/design_reference/components.jsx +++ b/design/design_reference/components.jsx @@ -524,6 +524,31 @@ function SegmentedRadio({ value, options, onChange, accent }) { ); } +function GameFolderField({ value, onChange, accent }) { + const isSet = !!(value && value.trim()); + const handleChange = () => { + // In production: open native folder picker via Tauri. + // For the prototype, prompt for a path so the field is exercisable. + const next = window.prompt('Game folder path (leave empty to clear)', value || ''); + if (next == null) return; + onChange(next.trim()); + }; + return ( +
+ +
+ {isSet + ? {value} + : Not set} +
+ +
+ ); +} + function ColorSwatchPicker({ value, options, onChange }) { return (
@@ -582,6 +607,11 @@ function SettingsDialog({ settings, onChange, onClose }) {
Library
+ + onChange('gameFolder', v)} + accent={settings.accent}/> +
-
@@ -80,7 +77,6 @@ function Launcher({
S
SoftLAN Launcher
-
diff --git a/design/design_reference/styles.css b/design/design_reference/styles.css index 1a7db49..acb93d2 100644 --- a/design/design_reference/styles.css +++ b/design/design_reference/styles.css @@ -1240,3 +1240,74 @@ color: var(--t-3); font-weight: 500; } + +/* ─── Settings: game-folder field ─── */ +.folder-field { + display: inline-flex; + align-items: center; + gap: 8px; + width: 340px; + height: 36px; + padding: 0 4px 0 12px; + background: var(--bg-3); + border: 1px solid var(--bd-1); + border-radius: 8px; + transition: border-color .15s, background .15s; +} +.folder-field:hover { border-color: var(--bd-2); } +.folder-field.is-unset { + border-color: color-mix(in srgb, var(--danger) 35%, var(--bd-1)); + background: color-mix(in srgb, var(--danger) 6%, var(--bg-3)); +} +.folder-field.is-unset:hover { + border-color: color-mix(in srgb, var(--danger) 55%, var(--bd-2)); +} +.folder-field-icon { + display: inline-flex; + color: var(--t-3); + flex-shrink: 0; +} +.folder-field.is-unset .folder-field-icon { color: #f87171; } +.folder-field-path { + flex: 1; + min-width: 0; + font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + font-size: 12px; + color: var(--t-1); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + direction: rtl; /* truncate from the head so the leaf folder stays visible */ + text-align: left; + unicode-bidi: plaintext; /* keep character order intact */ +} +.folder-field-empty { + font-family: var(--font-ui); + font-size: 12.5px; + font-weight: 600; + color: #f87171; + letter-spacing: 0.1px; +} +.folder-field-btn { + flex-shrink: 0; + height: 28px; + padding: 0 12px; + border: 0; + border-radius: 6px; + background: rgba(255,255,255,0.06); + color: var(--t-1); + font: inherit; + font-size: 12.5px; + font-weight: 600; + letter-spacing: 0.1px; + cursor: pointer; + transition: background .15s, color .15s; +} +.folder-field-btn:hover { background: rgba(255,255,255,0.12); } +.folder-field.is-unset .folder-field-btn { + background: color-mix(in srgb, var(--accent, #3b82f6) 85%, transparent); + color: #fff; +} +.folder-field.is-unset .folder-field-btn:hover { + background: var(--accent, #3b82f6); +}