973480d812
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>
104 lines
4.8 KiB
Markdown
104 lines
4.8 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 `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.
|