design docs: no green/red light on Game folder
This commit is contained in:
+13
-13
@@ -40,9 +40,9 @@ The HTML mock includes two chrome variants — **A (single-row)** and **B (two-r
|
|||||||
- **Right:** game-folder button + kebab menu.
|
- **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.
|
- 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 + status dot, with the full path moved to the tooltip. Two states:
|
- **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`, green status dot, tooltip = full configured path.
|
- **Set & valid:** label `Game folder`, default border, 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.
|
- **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.
|
- Click behavior unchanged — opens the native folder picker via Tauri.
|
||||||
- See "Game-folder button" below for the full spec.
|
- See "Game-folder button" below for the full spec.
|
||||||
|
|
||||||
@@ -80,10 +80,11 @@ The default screen. A grid of game cards over a dark, gradient-tinted background
|
|||||||
- **Center zone (col 2, search alone):**
|
- **Center zone (col 2, search alone):**
|
||||||
- **Search field** — 36 px tall, `flex: 0 1 360px` (caps at 360 px wide so it can't elbow into the side zones). `background var(--bg-2)`, `1px solid var(--bd-1)`, `border-radius: 8px`, padding `0 12px`. Leading magnifying-glass icon (14×14, `currentColor`) and a trailing "/" kbd hint (`background rgba(255,255,255,0.06)`, `border-radius: 4px`, font `11px ui-monospace`). On focus: border `color-mix(in srgb, var(--accent) 60%, var(--bd-2))`, background `var(--bg-1)`, ring `box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent)`. The `/` key shortcut should focus the search.
|
- **Search field** — 36 px tall, `flex: 0 1 360px` (caps at 360 px wide so it can't elbow into the side zones). `background var(--bg-2)`, `1px solid var(--bd-1)`, `border-radius: 8px`, padding `0 12px`. Leading magnifying-glass icon (14×14, `currentColor`) and a trailing "/" kbd hint (`background rgba(255,255,255,0.06)`, `border-radius: 4px`, font `11px ui-monospace`). On focus: border `color-mix(in srgb, var(--accent) 60%, var(--bd-2))`, background `var(--bg-1)`, ring `box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent)`. The `/` key shortcut should focus the search.
|
||||||
|
|
||||||
- **Right zone (col 3, flex space-between):**
|
- **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`.
|
- **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** (pinned right of itself, before the kebab) — see "Game-folder button" below.
|
- **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. Menu items: `Settings` (opens Settings dialog), `Refresh library`, separator, `Unpack logs`, `About SoftLAN`.
|
- **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 → 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.
|
||||||
|
|
||||||
@@ -222,10 +223,10 @@ The top-bar control that exposes the user's currently-configured game folder (th
|
|||||||
|
|
||||||
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 | Status dot | Border | Tooltip |
|
| State | Trigger | Label | Border | Tooltip |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| **Set & valid** | path is configured and exists on disk | `Game folder` | green (`--ok` `#22c55e`) | default `--bd-1` | full configured path |
|
| **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` | red (`--danger` `#ef4444`) | 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 | `Set game folder` | 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.
|
"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.
|
||||||
|
|
||||||
@@ -233,13 +234,12 @@ Two visual states, driven by whether `settings.gameFolder` resolves to an access
|
|||||||
|
|
||||||
1. **Folder icon** — `Icon.folder` from `components.jsx`, 14×14, `currentColor`.
|
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).
|
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, <dot-color> 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".
|
**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: <path>"` when set, `"Set game folder"` when unset — so screen readers don't have to interpret the colored dot.
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**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 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.
|
||||||
|
|
||||||
|
|||||||
@@ -365,7 +365,6 @@ function DirectoryButton({ path }) {
|
|||||||
>
|
>
|
||||||
<Icon.folder/>
|
<Icon.folder/>
|
||||||
<span className="dirbtn-label">{label}</span>
|
<span className="dirbtn-label">{label}</span>
|
||||||
<span className="dirbtn-status-dot" aria-hidden="true"/>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,8 +67,10 @@ function Launcher({
|
|||||||
<div className="topbar-right-lead">
|
<div className="topbar-right-lead">
|
||||||
<SortMenu value={sort} onChange={setSort} accent={accent}/>
|
<SortMenu value={sort} onChange={setSort} accent={accent}/>
|
||||||
</div>
|
</div>
|
||||||
<DirectoryButton path={gameFolderSet === false ? null : DIR_PATH}/>
|
<div className="topbar-right-trail">
|
||||||
<KebabMenu items={menuItems}/>
|
<DirectoryButton path={gameFolderSet === false ? null : DIR_PATH}/>
|
||||||
|
<KebabMenu items={menuItems}/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -118,6 +118,12 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.topbar-single .topbar-right-trail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* When the launcher gets narrow, the three-zone centering breaks down —
|
/* When the launcher gets narrow, the three-zone centering breaks down —
|
||||||
collapse to a single left-to-right flowing row. */
|
collapse to a single left-to-right flowing row. */
|
||||||
@@ -129,7 +135,8 @@
|
|||||||
}
|
}
|
||||||
.topbar-single .topbar-left,
|
.topbar-single .topbar-left,
|
||||||
.topbar-single .topbar-center,
|
.topbar-single .topbar-center,
|
||||||
.topbar-single .topbar-right {
|
.topbar-single .topbar-right,
|
||||||
|
.topbar-single .topbar-right-trail {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -374,20 +381,6 @@
|
|||||||
}
|
}
|
||||||
.dirbtn:hover { border-color: var(--bd-2); }
|
.dirbtn:hover { border-color: var(--bd-2); }
|
||||||
.dirbtn-label { line-height: 1; }
|
.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 {
|
.dirbtn-unset {
|
||||||
border-color: color-mix(in srgb, var(--danger) 35%, var(--bd-1));
|
border-color: color-mix(in srgb, var(--danger) 35%, var(--bd-1));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user