Files
galaxy-game/ui/docs/design-system.md
T
Ilia Denisov e193f3ca88
Tests · UI / test (push) Has been cancelled
Tests · UI / test (pull_request) Failing after 2m7s
feat(ui): default theme to system (follow OS light/dark)
Light has been signed off, so the theme store's default choice is now
`system` (it was `dark` during the incremental migration). This matches
the app.html pre-paint guard, which already resolved an unset choice via
prefers-color-scheme — removing the brief boot-time mismatch where the
store re-pinned dark. Users still pin light/dark via the account-menu
picker. Updates the store default + its test and the design-system /
finalize-plan docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 08:36:17 +02:00

121 lines
5.7 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 account menu (`account-menu.svelte`) exposes the picker. The
default is `system` (it follows the OS preference); `light` / `dark`
pin a theme.
The `app.html` guard and the store deliberately duplicate the
resolution logic (one runs before modules load, the other after) — keep
them in sync.
## 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 keep a fixed palette. The battle scene
(`battle-player/battle-scene.svelte`) is a self-contained SVG
visualisation — like the WebGL map canvas — and stays dark in both
themes; its only themed neighbours are the surrounding chrome
(`battle-viewer.svelte`). Re-theming a viz surface for light is a
dedicated design task, not a token swap.
- 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 battle-scene data-viz palette, the overlay scrims, and the
directional / deliberate drop shadows.
The default theme is **`system`** — it follows the OS light/dark
preference; users can pin light or dark via the account-menu picker.