Files
galaxy-game/ui/docs/design-system.md
T
Ilia Denisov f6e4a4f6bd
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Successful in 2m45s
feat(ui): map canvas follows light/dark theme; fix invisible gear control
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>
2026-05-24 08:49:37 +02:00

6.3 KiB

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 — 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 — 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; 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 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 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 — 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.