f6e4a4f6bd
The map view now selects a DARK_THEME or LIGHT_THEME palette from the resolved app theme and threads it through every primitive builder, so the canvas, planets, ship groups, cargo routes, battle/bombing markers, fog, reach + selection rings, pending-Send tracks, and the pick overlay all switch with the rest of the chrome. A theme flip remounts the renderer preserving the camera — Pixi bakes the background at init and every primitive bakes its colour at build, so a live re-tint is not possible on the same instance. This also fixes the reported bug: the gear-popover trigger and the loading overlay hardcoded a dark navy background, so in light theme the gear was invisible (dark icon on dark chip) until hover flipped it to a white chip. Both now use the --color-surface-overlay token and read correctly in both themes. The light palette mirrors the dark one role-for-role, darkened / saturated for contrast on a light background while keeping the incoming, battle, and bombing accents vivid. The values are a first pass meant to be refined during the F8 manual-QA loop. Removes the now-dead "Phase 35" references from the code and lifts the map-recoloring prohibition from the design-system / renderer docs; the battle scene stays a fixed-palette data-viz surface. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
130 lines
6.3 KiB
Markdown
130 lines
6.3 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 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 can pin light or dark via the account-menu picker.
|