Files
galaxy-game/ui/docs/design-system.md
T
Ilia Denisov 4a23c357e5
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Waiting to run
feat(ui): F8-05 — game-mode chrome cleanup + inspector compact rows (#48)
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>
2026-05-27 13:38:42 +02:00

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.