4a23c357e5
Drains six F8 polish items (parent #43) in one feature: а) Chrome cleanup - п.6 — remove the AccountMenu (settings/sessions/theme/language/logout ∼ rudimentary in-game) and replace it with a single icon-button light/dark theme toggle. The toggle flips an in-memory `theme.override`; game-shell unmount calls `theme.clearOverride()` so the lobby (and any re-entry) re-projects the persisted lobby choice. - п.8 — remove the wrap-scrolling radio from the map gear popover. The per-game `wrapMode` store and the renderer's no-wrap path stay in place for a future engine-side topology feature; only the UI surface is dropped (wrap is a server-side concept, not a per-session UI affordance). б) Inspector compact rows (single idiom: select + ✓ apply / ✗ cancel, or contextual edit/remove/add) - п.13 — planet name is now click-to-edit: clicking the name opens an inline `<input>` + ✓ confirm icon; Escape cancels; the explicit Rename action button and Cancel button are gone. - п.14 — production becomes one row: primary `<select>` picks industry/materials/research/ship, conditional secondary `<select>` picks the target (tech / science / ship class) for research and ship contexts. Apply is gated until row state differs from the planet's current effective production; auto-submit-on-click is replaced by the apply-gate. - п.16 — cargo routes collapse to one row: a single dropdown (COL/CAP/MAT/EMP plus a placeholder that absorbs the old section title) and contextual action buttons (add / edit + remove) to the right. After a successful pick or remove the dropdown stays on the type the user just acted on. - п.32 — stationed ship groups hoist the race column into a dropdown above the table. The dropdown seeds with the player's own race when local groups are stationed here, otherwise the first race alphabetically; rendered only when more than one race is in orbit. The race column is dropped in both single- and multi-race modes — the dropdown's value already names the active race. Tests: unit and Playwright e2e updated for every changed test-id and flow; new coverage added for `theme.override`, the in-game toggle, the apply-gate behaviour, and the stationed-race dropdown. i18n keys for the removed menu items, the wrap radios, the cargo title, and the explicit `rename.cancel` are dropped from both locales; new `game.shell.theme_toggle.*`, `production.main/target.*`, `production.apply/cancel`, `cargo.placeholder`, and `ship_groups.race_filter.aria` keys land. Docs synced: `docs/FUNCTIONAL.md` §6.7 + `docs/FUNCTIONAL_ru.md` mirror drop the torus / no-wrap radio mention; `ui/docs/design-system.md` documents the lobby-owned persisted picker + the in-game ephemeral override channel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
142 lines
7.2 KiB
Markdown
142 lines
7.2 KiB
Markdown
# Design system — tokens & theming
|
|
|
|
The client's visual language lives in a single set of CSS custom
|
|
properties (design tokens). Components reference the tokens
|
|
(`var(--color-…)`, `var(--space-…)`) instead of literal hex/px, so a
|
|
palette change is a one-file edit and the light and dark themes stay in
|
|
lockstep.
|
|
|
|
## Where the tokens live
|
|
|
|
- [`src/lib/theme/tokens.css`](../frontend/src/lib/theme/tokens.css) —
|
|
every token. Theme-independent scales (spacing, radii, typography) sit
|
|
in `:root`; the colour/shadow palette is defined twice, once for dark
|
|
(`:root, :root[data-theme="dark"]`) and once for light
|
|
(`:root[data-theme="light"]`).
|
|
- [`src/lib/theme/base.css`](../frontend/src/lib/theme/base.css) — a
|
|
small global baseline (document background, default text/typography, a
|
|
token-driven focus ring, selection colour). Everything else stays in
|
|
component-scoped `<style>`.
|
|
- Both are imported once, in the root
|
|
[`+layout.svelte`](../frontend/src/routes/+layout.svelte); a plain CSS
|
|
import is global (Svelte only scopes `<style>` blocks).
|
|
|
|
## Token reference
|
|
|
|
### Colour (theme-dependent)
|
|
|
|
| Token | Role |
|
|
|---|---|
|
|
| `--color-bg` | Application backdrop, header, bottom-tab bar. |
|
|
| `--color-surface` | Panels (sidebar, cards). |
|
|
| `--color-surface-raised` | Form controls and raised cards. |
|
|
| `--color-surface-overlay` | Floating surfaces (menus, drawers, popovers). |
|
|
| `--color-surface-hover` | Hover state on interactive surfaces. |
|
|
| `--color-border-subtle` | Structural chrome edges. |
|
|
| `--color-border` | Default control borders. |
|
|
| `--color-border-strong` | Emphasis / focus borders. |
|
|
| `--color-text` | Primary text. |
|
|
| `--color-text-muted` | Secondary text. |
|
|
| `--color-text-faint` | Tertiary text, placeholders. |
|
|
| `--color-accent` (`-hover`, `-active`, `-contrast`, `-subtle`) | Brand periwinkle: links, active states, fills, tints. `-contrast` is the text colour on an accent fill. |
|
|
| `--color-danger` (`-contrast`, `-subtle`) | Errors, destructive actions. |
|
|
| `--color-success` (`-subtle`) | Success / positive. |
|
|
| `--color-warning` (`-subtle`) | Warnings. |
|
|
| `--color-focus` | Focus-ring colour (aliases `--color-accent`). |
|
|
| `--shadow-sm` / `--shadow-md` / `--shadow-lg` | Elevation. |
|
|
|
|
`*-subtle` colours are translucent (`rgba`) so they tint whatever sits
|
|
behind them; `*-contrast` colours are opaque text colours meant to sit
|
|
on top of the matching solid fill.
|
|
|
|
### Scales (theme-independent)
|
|
|
|
- Spacing: `--space-1` (0.25rem) … `--space-6` (2rem), `--space-8` (3rem).
|
|
- Radii: `--radius-sm` 4px, `--radius-md` 6px, `--radius-lg` 10px,
|
|
`--radius-pill`.
|
|
- Typography: `--font-sans`, `--font-mono`; sizes `--text-xs` …
|
|
`--text-xl`; `--leading-tight` / `--leading-normal`; weights
|
|
`--weight-normal` / `--weight-medium` / `--weight-semibold`.
|
|
|
|
## Theming (light / dark / system)
|
|
|
|
The resolved theme is written to `data-theme` on `<html>`, which selects
|
|
the colour block in `tokens.css`.
|
|
|
|
- A pre-paint guard in [`app.html`](../frontend/src/app.html) reads the
|
|
stored choice from `localStorage["galaxy-theme"]` and sets `data-theme`
|
|
before the app boots, so first paint never flashes.
|
|
- [`src/lib/theme/theme.svelte.ts`](../frontend/src/lib/theme/theme.svelte.ts)
|
|
is the runtime store: `theme.choice` (`system` | `light` | `dark`),
|
|
`theme.resolved` (`light` | `dark`), and `theme.setChoice(…)`. It
|
|
persists the choice, applies `data-theme`, and — while the choice is
|
|
`system` — follows OS theme changes via `matchMedia`.
|
|
- The persisted picker lives in the lobby profile screen
|
|
([`screens/profile-screen.svelte`](../frontend/src/lib/screens/profile-screen.svelte)) —
|
|
the in-game header is intentionally light on chrome and only carries
|
|
the volatile light/dark toggle described below. The default is
|
|
`system` (it follows the OS preference); `light` / `dark` pin a theme.
|
|
- On top of the persisted choice the store carries an ephemeral
|
|
`theme.override` (`null` | `light` | `dark`). `setOverride(…)`
|
|
short-circuits `resolved` so the in-game toggle
|
|
([`header/game-mode-theme-toggle.svelte`](../frontend/src/lib/header/game-mode-theme-toggle.svelte))
|
|
can flip the document theme without touching the lobby preference.
|
|
The override lives in memory only; the game shell calls
|
|
`theme.clearOverride()` on unmount, so leaving the game and re-entering
|
|
it re-projects the persisted choice from lobby.
|
|
|
|
The `app.html` guard and the store deliberately duplicate the
|
|
resolution logic (one runs before modules load, the other after) — keep
|
|
them in sync. The ephemeral override is intentionally absent from the
|
|
pre-paint guard: it cannot survive a reload, only an in-tab session.
|
|
|
|
## Conventions
|
|
|
|
- No literal theme colours in component `<style>`. Use a token; if none
|
|
fits, add one to `tokens.css` rather than hard-coding.
|
|
- Map by role, not by hex: a form-control background is
|
|
`--color-surface-raised` even if its old value happened to match a
|
|
hover colour.
|
|
- Directional one-off drop shadows that are not part of the elevation
|
|
scale may stay as literal `rgba(0, 0, 0, …)` (they read acceptably in
|
|
both themes); reach for `--shadow-*` for standard elevation.
|
|
- Overlay scrims — a translucent layer dimming the app behind a modal,
|
|
or darkening a map/WebGL canvas so floating chrome stays readable —
|
|
stay literal `rgba(…)`. They sit over arbitrary content, not a themed
|
|
surface, so a surface token would be wrong; there is no `--color-scrim`
|
|
until a third caller justifies one.
|
|
- Data-visualisation surfaces define their palette in code, not via CSS
|
|
tokens, because they paint to a canvas / SVG instead of themed DOM.
|
|
The WebGL map canvas ships two palettes — `DARK_THEME` and
|
|
`LIGHT_THEME` in [`src/map/world.ts`](../frontend/src/map/world.ts) —
|
|
and follows the resolved app theme like the rest of the chrome:
|
|
`active-view/map.svelte` selects the palette from `theme.resolved` and
|
|
remounts the renderer on a theme flip (Pixi bakes the background and
|
|
every primitive colour at build time, so a live re-tint is not
|
|
possible; the remount preserves the camera). The battle scene
|
|
(`battle-player/battle-scene.svelte`) is a self-contained SVG
|
|
visualisation that still keeps a single fixed dark palette in both
|
|
themes — re-theming it for light is a separate design task — and its
|
|
only themed neighbours are the surrounding chrome
|
|
(`battle-viewer.svelte`).
|
|
- Spacing-scale adoption is gradual — colour tokens are the priority;
|
|
existing one-off paddings are migrated opportunistically, not churned
|
|
en masse.
|
|
|
|
## Migration status
|
|
|
|
All component `<style>` blocks reference the tokens — the chrome
|
|
(header, account menu, sidebar, tabs, shell) and every view body
|
|
(calculator, inspectors, tables, report, lobby, auth, map overlays,
|
|
battle, mail, toasts). The whole app switches coherently between light
|
|
and dark from a single token change.
|
|
|
|
The only remaining literal colours are the documented exceptions above:
|
|
the canvas data-viz palettes (the theme-aware map palette and the fixed
|
|
battle-scene palette, both defined in code rather than as tokens), the
|
|
overlay scrims, and the directional / deliberate drop shadows.
|
|
|
|
The default theme is **`system`** — it follows the OS light/dark
|
|
preference; users pin light or dark via the lobby profile screen, and
|
|
flip the in-game appearance volatilely through the header theme toggle.
|