diff --git a/design/README.md b/design/README.md index 7bfc812..5e7c7af 100644 --- a/design/README.md +++ b/design/README.md @@ -40,6 +40,11 @@ The HTML mock includes two chrome variants — **A (single-row)** and **B (two-r - **Right:** game-folder button + kebab menu. - 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 + status dot, with the full path moved to the tooltip. Two states: + - **Set & valid:** label `Game folder`, green status dot, tooltip = full configured path. + - **Not set / invalid path:** label `Set game folder`, red status dot, 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 @@ -77,7 +82,7 @@ The default screen. A grid of game cards over a dark, gradient-tinted background - **Right zone (col 3, flex space-between):** - **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`. - - **Game directory button** (pinned right of itself, before the kebab) — 36 px button, max-width 360 px. Folder icon, "Game directory" label (600 weight `--t-1`), then the current path in `ui-monospace` 11.5 px `--t-3` truncated with leading ellipsis when long (e.g. `…s/Desktop/eti_games_AFTER_LAN_2025`). + - **Game-folder button** (pinned right of itself, before the kebab) — see "Game-folder button" below. - **Kebab menu** (`⋮`, pinned far-right) — 36×36 button with same surface. Menu items: `Settings` (opens Settings dialog), `Refresh library`, separator, `Unpack logs`, `About SoftLAN`. **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. @@ -211,6 +216,37 @@ Persisted settings (write through to local storage / Tauri config): --- +## Game-folder button + +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. + +Two visual states, driven by whether `settings.gameFolder` resolves to an accessible directory: + +| State | Trigger | Label | Status dot | Border | Tooltip | +|---|---|---|---|---|---| +| **Set & valid** | path is configured and exists on disk | `Game folder` | green (`--ok` `#22c55e`) | 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` | red (`--danger` `#ef4444`) | tinted red (`color-mix(in srgb, var(--danger) 35%, var(--bd-1))`) | `Please select a game folder` | + +"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: + +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). +3. **Status dot** — `8×8` px circle, `border-radius: 999px`, `margin-left: 2px`. Background `var(--ok)` (set) or `var(--danger)` (unset). Glow via `box-shadow: 0 0 6px color-mix(in srgb, 70%, transparent)`. The dot is **inline** (a flex sibling next to the label), not corner-pinned over the icon — because the button is now wider than tall, a corner badge would feel misplaced. + +**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". + +**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. + +**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 dot + short label communicate the *state* of the configuration, which is what users actually need to glance at. + +**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. + +**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. + +--- + ## Game card The unit element of the library grid. @@ -389,7 +425,7 @@ 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 directory" button** → open native folder picker via Tauri; on selection, rescan library. +- **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 "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. diff --git a/design/design_reference/SoftLAN Launcher.html b/design/design_reference/SoftLAN Launcher.html index b2ef484..b4a3ab9 100644 --- a/design/design_reference/SoftLAN Launcher.html +++ b/design/design_reference/SoftLAN Launcher.html @@ -37,7 +37,8 @@ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "aspect": "square", "bg": "gradient", "username": "d", - "language": "en" + "language": "en", + "gameFolderSet": true }/*EDITMODE-END*/; const ACCENTS = ['#3b82f6', '#22d3ee', '#a855f7', '#22c55e', '#f59e0b', '#ef4444']; @@ -113,6 +114,10 @@ function App() { setTweak('aspect', v)}/> + + + setTweak('gameFolderSet', v)}/> ); diff --git a/design/design_reference/components.jsx b/design/design_reference/components.jsx index 4abd6de..cef2f9e 100644 --- a/design/design_reference/components.jsx +++ b/design/design_reference/components.jsx @@ -354,12 +354,18 @@ function StorageMeter({ accent, compact = false }) { // Directory button (shows path) // ──────────────────────────────────────────────────────────────────── function DirectoryButton({ path }) { - const short = path.length > 36 ? '…' + path.slice(-34) : path; + const isSet = !!(path && path.trim()); + const label = isSet ? 'Game folder' : 'Set game folder'; + const tooltip = isSet ? path : 'Please select a game folder'; return ( - ); } diff --git a/design/design_reference/styles.css b/design/design_reference/styles.css index b6826ae..9b352de 100644 --- a/design/design_reference/styles.css +++ b/design/design_reference/styles.css @@ -359,29 +359,41 @@ /* ─── Directory button ─── */ .dirbtn { + position: relative; display: inline-flex; align-items: center; gap: 8px; - height: 36px; padding: 0 12px; + height: 36px; padding: 0 14px 0 12px; background: var(--bg-2); border: 1px solid var(--bd-1); border-radius: 8px; - color: var(--t-2); - font: inherit; font-size: 12.5px; + color: var(--t-1); + font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; - max-width: 360px; - transition: border-color .15s, color .15s; - flex-shrink: 1; - min-width: 0; -} -.dirbtn:hover { border-color: var(--bd-2); color: var(--t-1); } -.dirbtn-label { color: var(--t-1); font-weight: 600; flex-shrink: 0; } -.dirbtn-path { - color: var(--t-3); - font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; - font-size: 11.5px; + transition: border-color .15s, color .15s, background .15s; + flex-shrink: 0; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; +} +.dirbtn:hover { border-color: var(--bd-2); } +.dirbtn-label { line-height: 1; } +.dirbtn-status-dot { + width: 8px; height: 8px; + margin-left: 2px; + border-radius: 999px; + flex-shrink: 0; +} +.dirbtn-set .dirbtn-status-dot { + background: var(--ok); + box-shadow: 0 0 6px color-mix(in srgb, var(--ok) 70%, transparent); +} +.dirbtn-unset .dirbtn-status-dot { + background: var(--danger); + box-shadow: 0 0 6px color-mix(in srgb, var(--danger) 70%, transparent); +} +.dirbtn-unset { + border-color: color-mix(in srgb, var(--danger) 35%, var(--bd-1)); +} +.dirbtn-unset:hover { + border-color: color-mix(in srgb, var(--danger) 55%, var(--bd-2)); + background: color-mix(in srgb, var(--danger) 8%, var(--bg-2)); } /* ─── Kebab menu ─── */