docs(design): center search in a 3-zone top bar

The single-row top bar was a flat flex row, which made the search field
drift left or right depending on how wide the surrounding clusters were.
Rework it as a 3-column CSS grid (left zone, search, right zone) so the
search input lands at the geometric center of the window regardless of
the side-zone widths.

Mechanics:

- `.topbar-single` becomes `display: grid` with
  `grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr)` and a 16 px
  column gap. The middle (auto) column holds only the search, which is
  capped at `flex: 0 1 360px` so it cannot push into the side columns.
- The left and right zones are flex containers with
  `justify-content: space-between`, so brand pins far-left while filter
  pills hug the search, and sort hugs the search while the kebab pins
  far-right. The filter pills are now grouped semantically with the
  search (they scope it) instead of floating next to the brand.
- A `@container launcher (max-width: 1100px)` rule collapses the layout
  back to a single nowrap flex row at narrow widths — the geometric
  centering stops reading at narrow widths and would otherwise force
  awkward truncation, so we abandon it rather than fight it.
- The `.launcher` root opts into container queries via
  `container-type: inline-size; container-name: launcher`.

`launcher.jsx` now wraps the existing children in `.topbar-left`,
`.topbar-center`, `.topbar-right` (plus a `.topbar-left-trail` /
`.topbar-right-lead` for the inner space-between alignment), but each
control component is otherwise untouched.

The README's "Top bar" section is rewritten to spec the new layout, and
a new "Changes since v2" section calls it out at the top of the
handoff. The game-directory button line is left as-is in this commit
and addressed separately.

Test Plan

- Open `design_reference/SoftLAN Launcher.html` in a static server and
  inspect variant A at full width: the search input's horizontal center
  should match the window's horizontal center, regardless of accent /
  density choices in the Tweaks panel.
- Shrink the launcher artboard below 1100 px and confirm the row
  collapses to a single left-to-right strip with no overlap.
This commit is contained in:
2026-05-21 18:00:55 +02:00
parent 8d96d99160
commit 6e28f736e8
3 changed files with 108 additions and 28 deletions
+30 -12
View File
@@ -32,6 +32,15 @@ The HTML mock includes two chrome variants — **A (single-row)** and **B (two-r
--- ---
## 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.
- 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.
## Changes since v1 ## Changes since v1
- **Settings → Profile section** added at the top of the dialog with two new persisted preferences: **Username** (text input) and **Language** (segmented `English` / `Deutsch`). See "Settings dialog" below for shape + persistence keys. - **Settings → Profile section** added at the top of the dialog with two new persisted preferences: **Username** (text input) and **Language** (segmented `English` / `Deutsch`). See "Settings dialog" below for shape + persistence keys.
@@ -48,21 +57,30 @@ The default screen. A grid of game cards over a dark, gradient-tinted background
**Layout (top-to-bottom):** **Layout (top-to-bottom):**
1. **Top bar** — single row, sticky, full width, 64px tall, semi-transparent dark with backdrop-blur. Background `rgba(10,14,19,0.65)` + `backdrop-filter: blur(20px) saturate(140%)`. Border-bottom `1px solid rgba(255,255,255,0.06)`. Contents, left-to-right with 18px gap and 24px horizontal padding: 1. **Top bar** — single row, sticky, full width, 64px tall, semi-transparent dark with backdrop-blur. Background `rgba(10,14,19,0.65)` + `backdrop-filter: blur(20px) saturate(140%)`. Border-bottom `1px solid rgba(255,255,255,0.06)`. Padding `14px 24px`. **Layout:** a 3-column CSS grid — `grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr)` with `column-gap: 16px` — putting the search field in the middle (auto-sized) column so it sits at the **geometric center of the window** regardless of how wide the side groups are. The side columns are each `display: flex; justify-content: space-between` so their contents pin to the outer edge on one end and hug the search on the other.
- **Brand** — 28×28px rounded square in `--accent` (default `#3b82f6`) with the letter "S" in Bebas Neue 20px white. Next to it, the wordmark "SoftLAN" in 15px / 700 weight `--t-1` `#e6edf3`. - **Left zone (col 1, flex space-between):**
- **Segmented filter pills** — pill-shaped container (`background var(--bg-2) #131b25`, `1px solid rgba(255,255,255,0.06)`, `border-radius: 999px`, `padding: 4px`). Three buttons: - **Brand** (pinned far-left) — 28×28 px rounded square in `--accent` (default `#3b82f6`) with the letter "S" in Bebas Neue 20 px white. Next to it, the wordmark "SoftLAN" in 15 px / 700 weight `--t-1` `#e6edf3`.
- `All Games` · count chip - **Segmented filter pills** (pinned right, hugging the search field) — pill-shaped container (`background var(--bg-2) #131b25`, `1px solid rgba(255,255,255,0.06)`, `border-radius: 999px`, `padding: 4px`). Three buttons:
- `Local` · count chip - `All Games` · count chip
- `Installed` · count chip - `Local` · count chip
- `Installed` · count chip
Active button has an animated pill thumb (background `var(--accent)`, transitions `left` and `width` with `cubic-bezier(.4,1.2,.5,1)` over 220ms), text becomes white, count-chip background goes `rgba(0,0,0,0.25)`. Inactive: text `var(--t-2) #9aa6b4`, count-chip background `rgba(255,255,255,0.08)`. Active button has an animated pill thumb (background `var(--accent)`, transitions `left` and `width` with `cubic-bezier(.4,1.2,.5,1)` over 220 ms), text becomes white, count-chip background goes `rgba(0,0,0,0.25)`. Inactive: text `var(--t-2) #9aa6b4`, count-chip background `rgba(255,255,255,0.08)`.
`Local` = installed *or* downloaded-but-not-yet-installed. `Installed` = installed only. `All Games` = everything available on the network. `Local` = installed *or* downloaded-but-not-yet-installed. `Installed` = installed only. `All Games` = everything available on the network.
- **Search field** — 36px tall, min-width 320px (flex 0 1 380px). `background var(--bg-2)`, `1px solid var(--bd-1)`, `border-radius: 8px`, padding `0 12px`. Has a 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 becomes `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.
- **Sort menu** — 36px button, same surface style as search. Label `Sort: <bold value>` plus 13px sort-bars icon and 11px chevron. Click reveals dropdown menu below. Options: `Name (AZ)`, `Size (largest)`, `Recently Played`, `Status`. The filter is grouped semantically with the search — it scopes what the user is searching, so it belongs at the search field's left shoulder.
- **Game directory button** — 36px button, max-width 360px. Folder icon, "Game directory" label (600 weight `--t-1`), then the current path in `ui-monospace` 11.5px `--t-3` truncated with leading ellipsis when long (e.g. `…s/Desktop/eti_games_AFTER_LAN_2025`).
- **Kebab menu** (`⋮`) — 36×36 button with same surface. Menu items: `Settings` (opens Settings dialog — see below), `Refresh library`, separator, `Unpack logs`, `About SoftLAN`. - **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.
- **Right zone (col 3, flex space-between):**
- **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 (AZ)`, `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`).
- **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.
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)`).
+20 -10
View File
@@ -28,7 +28,7 @@ function Launcher({
initialOpenGame = null, initialOpenGame = null,
initialSettingsOpen = false, initialSettingsOpen = false,
}) { }) {
const { density, aspect, accent, bg } = tweaks; const { density, aspect, accent, bg, gameFolderSet } = 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);
@@ -51,15 +51,25 @@ function Launcher({
style={{ '--accent': accent }}> style={{ '--accent': accent }}>
{variant === 'single' ? ( {variant === 'single' ? (
<header className="topbar topbar-single"> <header className="topbar topbar-single">
<div className="brand"> <div className="topbar-left">
<div className="brand-mark" style={{ background: accent }}>S</div> <div className="brand">
<div className="brand-name">SoftLAN</div> <div className="brand-mark" style={{ background: accent }}>S</div>
<div className="brand-name">SoftLAN</div>
</div>
<div className="topbar-left-trail">
<SegmentedFilters value={filter} onChange={setFilter} counts={counts} accent={accent}/>
</div>
</div>
<div className="topbar-center">
<SearchField value={query} onChange={setQuery} accent={accent} wide/>
</div>
<div className="topbar-right">
<div className="topbar-right-lead">
<SortMenu value={sort} onChange={setSort} accent={accent}/>
</div>
<DirectoryButton path={gameFolderSet === false ? null : DIR_PATH}/>
<KebabMenu items={menuItems}/>
</div> </div>
<SegmentedFilters value={filter} onChange={setFilter} counts={counts} accent={accent}/>
<SearchField value={query} onChange={setQuery} accent={accent} wide/>
<SortMenu value={sort} onChange={setSort} accent={accent}/>
<DirectoryButton path={DIR_PATH}/>
<KebabMenu items={menuItems}/>
</header> </header>
) : ( ) : (
<header className="topbar topbar-two"> <header className="topbar topbar-two">
@@ -68,7 +78,7 @@ 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={DIR_PATH}/> <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}/>
+56 -4
View File
@@ -41,6 +41,8 @@
color: var(--t-1); color: var(--t-1);
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: 13px; font-size: 13px;
container-type: inline-size;
container-name: launcher;
line-height: 1.4; line-height: 1.4;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@@ -77,13 +79,63 @@
border-bottom: 1px solid var(--bd-1); border-bottom: 1px solid var(--bd-1);
} }
/* Variant 1: single row */ /* Variant 1: single row — three visual zones with search at geometric center */
.topbar-single { .topbar-single {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
column-gap: 16px;
padding: 14px 24px;
}
.topbar-single .topbar-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 18px; justify-content: space-between;
padding: 14px 24px; gap: 16px;
flex-wrap: nowrap; min-width: 0;
}
.topbar-single .topbar-left-trail {
display: flex;
align-items: center;
min-width: 0;
}
.topbar-single .topbar-center {
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
}
.topbar-single .topbar-center .search { flex: 0 1 360px; min-width: 0; }
.topbar-single .topbar-right {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
}
.topbar-single .topbar-right-lead {
display: flex;
align-items: center;
min-width: 0;
}
/* When the launcher gets narrow, the three-zone centering breaks down —
collapse to a single left-to-right flowing row. */
@container launcher (max-width: 1100px) {
.topbar-single {
display: flex;
flex-wrap: nowrap;
gap: 16px;
}
.topbar-single .topbar-left,
.topbar-single .topbar-center,
.topbar-single .topbar-right {
justify-content: flex-start;
flex: 0 0 auto;
gap: 12px;
}
.topbar-single .topbar-center { flex: 1 1 200px; }
.topbar-single .topbar-center .search { flex: 1 1 auto; }
} }
/* Variant 2: two row */ /* Variant 2: two row */