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
This commit is contained in:
5 files changed
+148
-38
No files matched your search
+42
-29
@@ -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
|
## 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):
|
- **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.
|
- **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.
|
- **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.
|
- 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.
|
- 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
|
## 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):**
|
- **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.
|
- **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.
|
- **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.
|
||||||
- **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`.
|
|
||||||
|
|
||||||
**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:
|
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)`).
|
- 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 │
|
│ LIBRARY │
|
||||||
│ │
|
│ │
|
||||||
|
│ Game folder │
|
||||||
|
│ Parent directory where games are │
|
||||||
|
│ downloaded and installed │
|
||||||
|
│ [📁 /home/pfs/…/eti_games [Change…]] │ ← folder field (340×36)
|
||||||
|
│ │
|
||||||
│ Grid density │
|
│ Grid density │
|
||||||
│ How tightly cards are packed │
|
│ How tightly cards are packed │
|
||||||
│ [Compact│Normal│Large]│
|
│ [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)`.
|
- **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.
|
- **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)`.
|
**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`.
|
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`.
|
- `bg`: `flat` | `gradient` | `animated`. Default `gradient`.
|
||||||
- `density`: `compact` | `normal` | `large`. Default `normal`.
|
- `density`: `compact` | `normal` | `large`. Default `normal`.
|
||||||
- `aspect`: `box` | `square` | `banner`. Default `box`.
|
- `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:
|
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 |
|
| **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 | `Set game folder` | tinted red (`color-mix(in srgb, var(--danger) 35%, var(--bd-1))`) | `Please select a game folder` |
|
| **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.
|
"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`.
|
1. **Folder icon** — `Icon.folder` from `components.jsx`, 14×14, `var(--t-3)` (set state) or `#f87171` (unset state).
|
||||||
2. **Label** — 12.5 px / 600, `var(--t-1)`, `white-space: nowrap`, `line-height: 1`. Text content depends on state (see table above).
|
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 filter tab / segmented pill** → change filter.
|
- **Click filter tab / segmented pill** → change filter.
|
||||||
- **Click sort button** → opens dropdown; click an option → re-sort grid; clicking outside the menu closes it.
|
- **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.
|
- **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 "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 "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.
|
- **Click "Refresh library"** in kebab → re-runs the library scan.
|
||||||
- **Esc** → closes any open modal (detail overlay, Settings).
|
- **Esc** → closes any open modal (detail overlay, Settings).
|
||||||
@@ -480,12 +492,13 @@ type LauncherUI = {
|
|||||||
**Persisted settings** (mirror of Settings dialog state):
|
**Persisted settings** (mirror of Settings dialog state):
|
||||||
```ts
|
```ts
|
||||||
type LauncherSettings = {
|
type LauncherSettings = {
|
||||||
username: string; // new
|
username: string;
|
||||||
language: 'en' | 'de'; // new
|
language: 'en' | 'de';
|
||||||
accent: string; // hex from the curated 6-color palette
|
accent: string; // hex from the curated 6-color palette
|
||||||
bg: 'flat' | 'gradient' | 'animated';
|
bg: 'flat' | 'gradient' | 'animated';
|
||||||
density: 'compact' | 'normal' | 'large';
|
density: 'compact' | 'normal' | 'large';
|
||||||
aspect: 'box' | 'square' | 'banner';
|
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
|
├── data.jsx ← mock GAMES array + filter/sort helpers + STORAGE mock
|
||||||
├── components.jsx ← Icon, GameCover, StateChip, ActionButton, GameCard,
|
├── components.jsx ← Icon, GameCover, StateChip, ActionButton, GameCard,
|
||||||
│ SegmentedFilters, UnderlineFilters, SearchField,
|
│ SegmentedFilters, UnderlineFilters, SearchField,
|
||||||
│ SortMenu, StorageMeter, DirectoryButton, KebabMenu,
|
│ SortMenu, StorageMeter, KebabMenu,
|
||||||
│ GameDetailModal, SettingsDialog
|
│ GameDetailModal, SettingsDialog (incl. GameFolderField)
|
||||||
└── launcher.jsx ← <Launcher> component composing chrome + grid + modals
|
└── launcher.jsx ← <Launcher> 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**
|
- **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**
|
- **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
|
- **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
|
|||||||
"density": "normal",
|
"density": "normal",
|
||||||
"aspect": "square",
|
"aspect": "square",
|
||||||
"bg": "gradient",
|
"bg": "gradient",
|
||||||
"username": "d",
|
"username": "ddidderr",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"gameFolderSet": true
|
"gameFolder": "\/some\/folder\/to\/games"
|
||||||
}/*EDITMODE-END*/;
|
}/*EDITMODE-END*/;
|
||||||
|
|
||||||
const ACCENTS = ['#3b82f6', '#22d3ee', '#a855f7', '#22c55e', '#f59e0b', '#ef4444'];
|
const ACCENTS = ['#3b82f6', '#22d3ee', '#a855f7', '#22c55e', '#f59e0b', '#ef4444'];
|
||||||
@@ -116,8 +116,8 @@ function App() {
|
|||||||
onChange={(v) => setTweak('aspect', v)}/>
|
onChange={(v) => setTweak('aspect', v)}/>
|
||||||
|
|
||||||
<TweakSection label="Library"/>
|
<TweakSection label="Library"/>
|
||||||
<TweakToggle label="Game folder set" value={t.gameFolderSet}
|
<TweakText label="Game folder" value={t.gameFolder}
|
||||||
onChange={(v) => setTweak('gameFolderSet', v)}/>
|
onChange={(v) => setTweak('gameFolder', v)}/>
|
||||||
</TweaksPanel>
|
</TweaksPanel>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className={`folder-field ${isSet ? 'is-set' : 'is-unset'}`}
|
||||||
|
style={{ '--accent': accent }}>
|
||||||
|
<span className="folder-field-icon" aria-hidden="true"><Icon.folder/></span>
|
||||||
|
<div className="folder-field-path" title={isSet ? value : 'No folder selected'}>
|
||||||
|
{isSet
|
||||||
|
? <bdi>{value}</bdi>
|
||||||
|
: <span className="folder-field-empty">Not set</span>}
|
||||||
|
</div>
|
||||||
|
<button type="button" className="folder-field-btn" onClick={handleChange}>
|
||||||
|
{isSet ? 'Change\u2026' : 'Choose\u2026'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ColorSwatchPicker({ value, options, onChange }) {
|
function ColorSwatchPicker({ value, options, onChange }) {
|
||||||
return (
|
return (
|
||||||
<div className="swatch-row">
|
<div className="swatch-row">
|
||||||
@@ -582,6 +607,11 @@ function SettingsDialog({ settings, onChange, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<div className="settings-section-title">Library</div>
|
<div className="settings-section-title">Library</div>
|
||||||
|
<SettingsRow label="Game folder" hint="Parent directory where games are downloaded and installed">
|
||||||
|
<GameFolderField value={settings.gameFolder}
|
||||||
|
onChange={(v) => onChange('gameFolder', v)}
|
||||||
|
accent={settings.accent}/>
|
||||||
|
</SettingsRow>
|
||||||
<SettingsRow label="Grid density" hint="How tightly cards are packed">
|
<SettingsRow label="Grid density" hint="How tightly cards are packed">
|
||||||
<SegmentedRadio value={settings.density}
|
<SegmentedRadio value={settings.density}
|
||||||
options={SETTING_OPTIONS.density}
|
options={SETTING_OPTIONS.density}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// launcher.jsx — composes top bar + grid into a complete launcher screen
|
// launcher.jsx — composes top bar + grid into a complete launcher screen
|
||||||
// Comes in two chrome variants: 'single' (one-row) and 'two' (two-row).
|
// Comes in two chrome variants: 'single' (one-row) and 'two' (two-row).
|
||||||
|
|
||||||
const DIR_PATH = '/home/pfs/Desktop/eti_games_AFTER_LAN_2025';
|
|
||||||
|
|
||||||
function applyFilterAndSort(games, filter, sort, query) {
|
function applyFilterAndSort(games, filter, sort, query) {
|
||||||
let g = filterGames(games, filter);
|
let g = filterGames(games, filter);
|
||||||
if (query.trim()) {
|
if (query.trim()) {
|
||||||
@@ -28,7 +26,7 @@ function Launcher({
|
|||||||
initialOpenGame = null,
|
initialOpenGame = null,
|
||||||
initialSettingsOpen = false,
|
initialSettingsOpen = false,
|
||||||
}) {
|
}) {
|
||||||
const { density, aspect, accent, bg, gameFolderSet } = tweaks;
|
const { density, aspect, accent, bg } = tweaks;
|
||||||
const [filter, setFilter] = useState(initialFilter);
|
const [filter, setFilter] = useState(initialFilter);
|
||||||
const [sort, setSort] = useState(initialSort);
|
const [sort, setSort] = useState(initialSort);
|
||||||
const [query, setQuery] = useState(initialQuery);
|
const [query, setQuery] = useState(initialQuery);
|
||||||
@@ -68,7 +66,6 @@ function Launcher({
|
|||||||
<SortMenu value={sort} onChange={setSort} accent={accent}/>
|
<SortMenu value={sort} onChange={setSort} accent={accent}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="topbar-right-trail">
|
<div className="topbar-right-trail">
|
||||||
<DirectoryButton path={gameFolderSet === false ? null : DIR_PATH}/>
|
|
||||||
<KebabMenu items={menuItems}/>
|
<KebabMenu items={menuItems}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +77,6 @@ function Launcher({
|
|||||||
<div className="brand-mark" style={{ background: accent }}>S</div>
|
<div className="brand-mark" style={{ background: accent }}>S</div>
|
||||||
<div className="brand-name">SoftLAN <span className="brand-name-soft">Launcher</span></div>
|
<div className="brand-name">SoftLAN <span className="brand-name-soft">Launcher</span></div>
|
||||||
</div>
|
</div>
|
||||||
<DirectoryButton path={gameFolderSet === false ? null : DIR_PATH}/>
|
|
||||||
<div className="topbar-row1-right">
|
<div className="topbar-row1-right">
|
||||||
<StorageMeter accent={accent}/>
|
<StorageMeter accent={accent}/>
|
||||||
<KebabMenu items={menuItems}/>
|
<KebabMenu items={menuItems}/>
|
||||||
|
|||||||
@@ -1240,3 +1240,74 @@
|
|||||||
color: var(--t-3);
|
color: var(--t-3);
|
||||||
font-weight: 500;
|
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);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user