feat(ui): design tokens + light/dark theming, migrate in-game chrome (F1a)
Tests · UI / test (push) Successful in 2m4s

Introduce the shared design-token system under
ui/frontend/src/lib/theme/: tokens.css (dark default + light palette,
plus spacing/radii/typography scales), base.css global baseline
(document background, text, token focus ring, selection), and
theme.svelte.ts (system/light/dark choice, persisted to localStorage,
applied via data-theme on <html>). A pre-paint guard in app.html
resolves the theme before the app boots to avoid a flash, and the theme
picker is wired into the previously-disabled account-menu stub.

Migrate the always-visible in-game chrome to the tokens (header, account
menu, sidebar, tab-bar, bottom-tabs, shell background): dark renders as
before, light comes for free. The default stays dark during the
incremental migration; the remaining view bodies migrate in F1b.

Docs: ui/docs/design-system.md (+ index entry). Test: tests/theme.test.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-22 07:02:13 +02:00
parent 44ed0a90eb
commit 973480d812
16 changed files with 560 additions and 53 deletions
+3
View File
@@ -9,6 +9,9 @@ lives in [`../PLAN.md`](../PLAN.md), the active web finalization in
## Foundation & platform
- [design-system.md](design-system.md) — the design tokens (colour /
spacing / typography), the light/dark theming mechanism, and the
component migration conventions.
- [navigation.md](navigation.md) — routes, the sidebar tabs, and the
state-preservation rules across view/tab switches.
- [storage.md](storage.md) — the `KeyStore` and `Cache` abstractions and
+103
View File
@@ -0,0 +1,103 @@
# 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 `dark`; `system` follows the OS.
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.
- Spacing-scale adoption is gradual — colour tokens are the priority;
existing one-off paddings are migrated opportunistically, not churned
en masse.
## Migration status
The token system and the always-visible in-game chrome (header, account
menu, sidebar frame, tab bar, bottom tabs, shell background) reference
tokens and switch cleanly between light and dark. The view bodies
(calculator, inspectors, tables, lobby, auth, map overlays, battle,
mail) are migrated incrementally; until a view is migrated its
hard-coded colours stay dark in both themes. F1 is complete when no
literal theme colours remain in component `<style>` blocks.