Merge pull request 'feat(ui): visual design system — tokens + light/dark theming (F1)' (#26) from feature/ui-finalize-f1-tokens into development
Deploy · Dev / deploy (push) Successful in 37s
Tests · UI / test (push) Successful in 2m16s

This commit was merged in pull request #26.
This commit is contained in:
2026-05-22 05:37:11 +00:00
73 changed files with 1068 additions and 543 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
+120
View File
@@ -0,0 +1,120 @@
# 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.
- 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 **dark** while light coherence is being verified
across the migrated views; once the owner signs off on light, the
default can flip to `system`.
+20
View File
@@ -11,6 +11,26 @@
margin: 0;
}
</style>
<script>
// Pre-paint theme guard: resolve the stored theme choice before
// the app boots so first paint matches and never flashes. Mirrors
// $lib/theme/theme.svelte.ts; keep the two in sync.
(function () {
try {
var c = localStorage.getItem("galaxy-theme");
var resolved =
c === "light" || c === "dark"
? c
: window.matchMedia &&
window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
document.documentElement.dataset.theme = resolved;
} catch (e) {
document.documentElement.dataset.theme = "dark";
}
})();
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
@@ -180,16 +180,16 @@ viewer keeps its prop-driven contract.
overflow: hidden;
box-sizing: border-box;
font-family: system-ui, sans-serif;
color: #d6dcf2;
color: var(--color-text);
}
.status {
margin: 2rem auto;
max-width: 880px;
color: #93a0d0;
color: var(--color-text-muted);
font-size: 0.95rem;
text-align: center;
}
.status.error {
color: #e08585;
color: var(--color-danger);
}
</style>
@@ -353,7 +353,7 @@ fractions is a Phase 21 decision documented in
.hint,
.not-found {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.85rem;
}
.form {
@@ -369,32 +369,32 @@ fractions is a Phase 21 decision documented in
gap: 0.6rem;
}
.row span {
color: #aab;
color: var(--color-text-muted);
font-size: 0.85rem;
}
.row input {
font: inherit;
padding: 0.3rem 0.5rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
}
.row input[aria-invalid="true"] {
border-color: #d97a7a;
border-color: var(--color-danger);
}
.sum {
margin: 0;
font-size: 0.85rem;
color: #aab;
color: var(--color-text-muted);
}
.sum[data-sum-ok="false"] {
color: #d97a7a;
color: var(--color-danger);
}
.error {
margin: 0;
font-size: 0.8rem;
color: #d97a7a;
color: var(--color-danger);
}
.fields {
margin: 0;
@@ -408,7 +408,7 @@ fractions is a Phase 21 decision documented in
display: contents;
}
.field dt {
color: #aab;
color: var(--color-text-muted);
font-size: 0.85rem;
}
.field dd {
@@ -425,24 +425,24 @@ fractions is a Phase 21 decision documented in
font-size: 0.9rem;
padding: 0.3rem 0.7rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.actions button:not(:disabled):hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.actions button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.actions .delete {
color: #d97a7a;
color: var(--color-danger);
}
.actions .delete:not(:disabled):hover {
border-color: #d97a7a;
color: #f0a0a0;
border-color: var(--color-danger);
color: var(--color-danger);
}
</style>
+9 -9
View File
@@ -135,9 +135,9 @@ pane, system-item pane, compose form) live under
.compose-btn {
font: inherit;
padding: 0.35rem 0.75rem;
border: 1px solid #444;
background: #1a1a1a;
color: #fff;
border: 1px solid var(--color-border);
background: var(--color-surface-raised);
color: var(--color-text);
border-radius: 4px;
cursor: pointer;
}
@@ -146,10 +146,10 @@ pane, system-item pane, compose form) live under
cursor: not-allowed;
}
.status {
color: #888;
color: var(--color-text-muted);
}
.status.error {
color: #c62828;
color: var(--color-danger);
}
.panes {
display: grid;
@@ -159,10 +159,10 @@ pane, system-item pane, compose form) live under
}
.list-pane,
.detail-pane {
border: 1px solid #2a2a2a;
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
padding: 0.75rem;
background: #111;
background: var(--color-surface);
overflow: hidden;
}
.list-pane {
@@ -174,9 +174,9 @@ pane, system-item pane, compose form) live under
font: inherit;
margin-bottom: 0.5rem;
padding: 0.25rem 0.5rem;
border: 1px solid #444;
border: 1px solid var(--color-border);
background: transparent;
color: #fff;
color: var(--color-text);
border-radius: 4px;
cursor: pointer;
}
@@ -160,8 +160,8 @@ surfaces the resulting 403 inline.
flex-direction: column;
gap: 0.5rem;
padding: 1rem 1.25rem;
background: #161616;
border: 1px solid #2a2a2a;
background: var(--color-surface-overlay);
border: 1px solid var(--color-border-subtle);
border-radius: 8px;
min-width: min(420px, 90vw);
max-width: min(560px, 95vw);
@@ -188,15 +188,15 @@ surfaces the resulting 403 inline.
flex-direction: column;
gap: 0.2rem;
font-size: 0.85rem;
color: #ccc;
color: var(--color-text-muted);
}
input,
textarea,
select {
font: inherit;
padding: 0.4rem 0.5rem;
border: 1px solid #444;
background: #111;
border: 1px solid var(--color-border);
background: var(--color-surface);
color: inherit;
border-radius: 4px;
}
@@ -209,22 +209,23 @@ surfaces the resulting 403 inline.
footer button {
font: inherit;
padding: 0.35rem 0.75rem;
border: 1px solid #444;
background: #1a1a1a;
color: #fff;
border: 1px solid var(--color-border);
background: var(--color-surface-raised);
color: var(--color-text);
border-radius: 4px;
cursor: pointer;
}
footer button[type="submit"] {
background: #2a4d7d;
border-color: #2a4d7d;
background: var(--color-accent);
border-color: var(--color-accent);
color: var(--color-accent-contrast);
}
footer button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error {
color: #c62828;
color: var(--color-danger);
font-size: 0.85rem;
margin: 0;
}
@@ -82,7 +82,7 @@ available for incoming rows that the caller has read.
.title {
margin: 0;
font-size: 1rem;
color: #b3a14c;
color: var(--color-warning);
}
.subject {
font-weight: 700;
@@ -96,7 +96,7 @@ available for incoming rows that the caller has read.
align-self: flex-start;
font: inherit;
padding: 0.2rem 0.5rem;
border: 1px solid #444;
border: 1px solid var(--color-border);
background: transparent;
color: inherit;
border-radius: 4px;
@@ -95,14 +95,14 @@ here.
cursor: pointer;
}
.row.active .row-btn {
border-color: #555;
background: #1c1c1c;
border-color: var(--color-border-strong);
background: var(--color-surface-raised);
}
.row.has-unread .title {
font-weight: 700;
}
.row.standalone .title {
color: #b3a14c;
color: var(--color-warning);
}
.title {
grid-column: 1 / span 1;
@@ -116,12 +116,12 @@ here.
text-align: center;
font-size: 0.75rem;
border-radius: 999px;
background: #2a4d7d;
color: #fff;
background: var(--color-accent);
color: var(--color-accent-contrast);
}
.snippet {
grid-column: 1 / span 2;
color: #999;
color: var(--color-text-muted);
font-size: 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
@@ -186,17 +186,17 @@ sits at the bottom of the pane.
.message {
padding: 0.5rem 0.75rem;
border-radius: 6px;
background: #1c1c1c;
border: 1px solid #2a2a2a;
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
}
.message.outgoing {
background: #15252e;
background: var(--color-accent-subtle);
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #999;
color: var(--color-text-muted);
margin-bottom: 0.25rem;
}
.subject {
@@ -212,7 +212,7 @@ sits at the bottom of the pane.
margin-right: 0.5rem;
font: inherit;
padding: 0.2rem 0.5rem;
border: 1px solid #444;
border: 1px solid var(--color-border);
background: transparent;
color: inherit;
border-radius: 4px;
@@ -227,8 +227,8 @@ sits at the bottom of the pane.
.reply textarea {
font: inherit;
padding: 0.5rem;
border: 1px solid #444;
background: #111;
border: 1px solid var(--color-border);
background: var(--color-surface);
color: inherit;
border-radius: 4px;
resize: vertical;
@@ -237,9 +237,9 @@ sits at the bottom of the pane.
align-self: flex-end;
font: inherit;
padding: 0.35rem 0.75rem;
border: 1px solid #444;
background: #1a1a1a;
color: #fff;
border: 1px solid var(--color-border);
background: var(--color-surface-raised);
color: var(--color-text);
border-radius: 4px;
cursor: pointer;
}
@@ -248,7 +248,7 @@ sits at the bottom of the pane.
cursor: not-allowed;
}
.error {
color: #c62828;
color: var(--color-danger);
font-size: 0.85rem;
margin: 0;
}
@@ -238,13 +238,13 @@ bottom-tabs bar.
font-size: 1.4rem;
padding: 0.25rem 0.5rem;
background: rgba(20, 24, 42, 0.85);
color: #e8eaf6;
border: 1px solid #2a3150;
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 6px;
cursor: pointer;
}
.trigger:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
.surface {
position: absolute;
@@ -254,11 +254,11 @@ bottom-tabs bar.
display: flex;
flex-direction: column;
gap: 0.5rem;
background: #14182a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-surface-overlay);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 6px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
box-shadow: var(--shadow-lg);
padding: 0.5rem;
z-index: 50;
}
@@ -274,7 +274,7 @@ bottom-tabs bar.
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #aab;
color: var(--color-text-muted);
padding: 0 0 0.15rem 0;
}
label {
@@ -287,11 +287,11 @@ bottom-tabs bar.
cursor: pointer;
}
label:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
input[type="checkbox"],
input[type="radio"] {
accent-color: #6dd2ff;
accent-color: var(--color-accent);
}
.wrap-row {
display: flex;
@@ -301,7 +301,7 @@ bottom-tabs bar.
font-size: 0.9rem;
}
.wrap-label {
color: #aab;
color: var(--color-text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
+6 -6
View File
@@ -726,7 +726,7 @@ preference the store already manages.
min-height: 0;
position: relative;
overflow: hidden;
background: #0a0e1a;
background: var(--color-bg);
}
canvas {
display: block;
@@ -740,8 +740,8 @@ preference the store already manages.
transform: translateX(-50%);
padding: 0.4rem 0.9rem;
background: rgba(20, 24, 42, 0.85);
color: #e8eaf6;
border: 1px solid #2a3150;
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 6px;
z-index: 10;
font-family: system-ui, sans-serif;
@@ -749,8 +749,8 @@ preference the store already manages.
margin: 0;
}
.overlay.error {
background: #4a1820;
border-color: #6d2530;
color: #ffb4b4;
background: var(--color-danger-subtle);
border-color: var(--color-danger);
color: var(--color-danger);
}
</style>
@@ -173,7 +173,7 @@ TOC and the body iterate the same data.
.report-view > :global(.report-toc) {
position: sticky;
top: 0;
background: #0a0e1a;
background: var(--color-bg);
padding: 0.5rem 0;
z-index: 5;
}
@@ -128,15 +128,15 @@ The active section is computed by the orchestrator
font-size: 0.85rem;
text-align: left;
padding: 0.4rem 0.6rem;
background: #11172a;
color: #cfd7ff;
border: 1px solid #2a3150;
background: var(--color-surface);
color: var(--color-accent);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.back-to-map:hover {
background: #1a2240;
color: #e8eaf6;
background: var(--color-surface-hover);
color: var(--color-text);
}
.desktop {
display: block;
@@ -152,7 +152,7 @@ The active section is computed by the orchestrator
.desktop a {
display: block;
padding: 0.3rem 0.6rem;
color: #aab;
color: var(--color-text-muted);
text-decoration: none;
font-size: 0.85rem;
line-height: 1.3;
@@ -160,13 +160,13 @@ The active section is computed by the orchestrator
border-radius: 0 3px 3px 0;
}
.desktop a:hover {
color: #e8eaf6;
background: #11172a;
color: var(--color-text);
background: var(--color-surface);
}
.desktop a.active {
color: #e8eaf6;
background: #11172a;
border-left-color: #4a6cf7;
color: var(--color-text);
background: var(--color-surface);
border-left-color: var(--color-accent);
}
.mobile {
display: none;
@@ -175,9 +175,9 @@ The active section is computed by the orchestrator
width: 100%;
font: inherit;
padding: 0.4rem 0.5rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
}
.visually-hidden {
@@ -68,11 +68,11 @@ class when the group lands and a battle roster forms.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -85,15 +85,15 @@ class when the group lands and a battle roster forms.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -61,11 +61,11 @@ decision log called out.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.ids {
@@ -83,18 +83,18 @@ decision log called out.
gap: 0.6rem;
}
.label {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.7rem;
}
.uuid {
color: #cfd7ff;
color: var(--color-accent);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
text-decoration: underline;
text-underline-offset: 2px;
}
.uuid:hover {
color: #ffffff;
color: var(--color-text);
}
</style>
@@ -94,11 +94,11 @@ Decoder sorts by `planetNumber` already.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -111,19 +111,19 @@ Decoder sorts by `planetNumber` already.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
.wiped td {
color: #c97a7a;
color: var(--color-danger);
}
.wiped-badge {
display: inline-block;
@@ -131,9 +131,9 @@ Decoder sorts by `planetNumber` already.
font-size: 0.7rem;
letter-spacing: 0.06em;
text-transform: uppercase;
background: #4a1010;
color: #ffcaca;
border: 1px solid #8a3030;
background: var(--color-danger-subtle);
color: var(--color-danger);
border: 1px solid var(--color-danger);
border-radius: 3px;
}
</style>
@@ -83,11 +83,11 @@ has many routes.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -100,15 +100,15 @@ has many routes.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -85,11 +85,11 @@ as the local planets table plus an `owner` column.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -102,15 +102,15 @@ as the local planets table plus an `owner` column.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -97,18 +97,18 @@ unit even when the section spans many races.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.race-header {
margin: 0.75rem 0 0.3rem;
font-size: 0.85rem;
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -121,15 +121,15 @@ unit even when the section spans many races.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -99,18 +99,18 @@ incoming groups.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.race-header {
margin: 0.75rem 0 0.3rem;
font-size: 0.85rem;
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -123,15 +123,15 @@ incoming groups.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -77,11 +77,11 @@ to groups the player doesn't own.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -94,15 +94,15 @@ to groups the player doesn't own.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -48,11 +48,11 @@ section is never empty as long as the report has loaded.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.kv {
@@ -63,14 +63,14 @@ section is never empty as long as the report has loaded.
font-size: 0.9rem;
}
.kv dt {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.kv dd {
margin: 0;
color: #e8eaf6;
color: var(--color-text);
font-variant-numeric: tabular-nums;
}
</style>
@@ -70,11 +70,11 @@ in orbit has neither); empty cells in those columns are normal.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -87,15 +87,15 @@ in orbit has neither); empty cells in those columns are normal.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -83,11 +83,11 @@ column set (matches `ReportPlanet` shape).
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -100,15 +100,15 @@ column set (matches `ReportPlanet` shape).
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -64,11 +64,11 @@ table).
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -81,15 +81,15 @@ table).
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -67,11 +67,11 @@ drafts immediately, matching the ship-class designer's behaviour.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -84,15 +84,15 @@ drafts immediately, matching the ship-class designer's behaviour.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -91,11 +91,11 @@ shown together with `load` when carrying.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -108,19 +108,19 @@ shown together with `load` when carrying.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
.uuid {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: #cfd7ff;
color: var(--color-accent);
}
</style>
@@ -92,11 +92,11 @@ highlight so the user can locate themselves quickly.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -109,30 +109,30 @@ highlight so the user can locate themselves quickly.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
.local td {
background: #11203d;
background: var(--color-accent-subtle);
}
.extinct td {
color: #889;
color: var(--color-text-faint);
}
.marker {
margin-left: 0.4rem;
font-size: 0.75rem;
color: #aab;
color: var(--color-text-muted);
}
.extinct-marker {
color: #c97a7a;
color: var(--color-danger);
letter-spacing: 0.08em;
}
</style>
@@ -73,11 +73,11 @@ reads `#17 (Castle)` rather than just `#17`.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -90,15 +90,15 @@ reads `#17 (Castle)` rather than just `#17`.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -57,11 +57,11 @@ radar that doesn't even resolve to a planet.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -74,15 +74,15 @@ radar that doesn't even resolve to a planet.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -74,11 +74,11 @@ are intentionally omitted.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -91,15 +91,15 @@ are intentionally omitted.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -59,11 +59,11 @@ else is known.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -76,15 +76,15 @@ else is known.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
</style>
@@ -78,12 +78,12 @@ explanatory text on the races table.
.grid-section h2 {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: #e8eaf6;
color: var(--color-text);
}
.grid-section h3 {
margin: 1rem 0 0.4rem;
font-size: 0.85rem;
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
@@ -95,19 +95,19 @@ explanatory text on the races table.
font-size: 0.9rem;
}
.kv dt {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.kv dd {
margin: 0;
color: #e8eaf6;
color: var(--color-text);
font-variant-numeric: tabular-nums;
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -119,10 +119,10 @@ explanatory text on the races table.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
@@ -345,21 +345,21 @@ data fetching is performed here — the layout is responsible.
align-items: baseline;
}
.summary-label {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.summary-value {
color: #e8eaf6;
color: var(--color-text);
font-variant-numeric: tabular-nums;
}
.vote-picker select {
font: inherit;
padding: 0.2rem 0.4rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
}
.vote-picker select:disabled {
@@ -368,7 +368,7 @@ data fetching is performed here — the layout is responsible.
}
.note {
margin: 0;
color: #889;
color: var(--color-text-muted);
font-size: 0.8rem;
line-height: 1.35;
}
@@ -381,16 +381,16 @@ data fetching is performed here — the layout is responsible.
.filter {
font: inherit;
padding: 0.3rem 0.5rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
flex: 1 1 12rem;
min-width: 8rem;
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -402,17 +402,17 @@ data fetching is performed here — the layout is responsible.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.9rem;
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
.sort {
font: inherit;
@@ -441,22 +441,22 @@ data fetching is performed here — the layout is responsible.
letter-spacing: 0.05em;
padding: 0.2rem 0.55rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.stance-button:hover {
color: #e8eaf6;
color: var(--color-text);
}
.stance-button.war.active {
background: #4a1010;
color: #ffcaca;
border-color: #8a3030;
background: var(--color-danger-subtle);
color: var(--color-danger);
border-color: var(--color-danger);
}
.stance-button.peace.active {
background: #103a1a;
color: #c8f2cf;
border-color: #2f7a45;
background: var(--color-success-subtle);
color: var(--color-success);
border-color: var(--color-success);
}
</style>
@@ -250,9 +250,9 @@ data fetching is performed here — the layout is responsible.
.filter {
font: inherit;
padding: 0.3rem 0.5rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
flex: 1 1 12rem;
min-width: 8rem;
@@ -262,18 +262,18 @@ data fetching is performed here — the layout is responsible.
font-size: 0.9rem;
padding: 0.3rem 0.7rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.new:hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -285,11 +285,11 @@ data fetching is performed here — the layout is responsible.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.9rem;
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
@@ -298,7 +298,7 @@ data fetching is performed here — the layout is responsible.
cursor: pointer;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
.sort {
font: inherit;
@@ -322,12 +322,12 @@ data fetching is performed here — the layout is responsible.
font-size: 0.85rem;
padding: 0.15rem 0.5rem;
background: transparent;
color: #d97a7a;
border: 1px solid #2a3150;
color: var(--color-danger);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.delete:hover {
border-color: #d97a7a;
border-color: var(--color-danger);
}
</style>
@@ -241,9 +241,9 @@ data fetching is performed here — the layout is responsible.
.filter {
font: inherit;
padding: 0.3rem 0.5rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
flex: 1 1 12rem;
min-width: 8rem;
@@ -253,18 +253,18 @@ data fetching is performed here — the layout is responsible.
font-size: 0.9rem;
padding: 0.3rem 0.7rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.new:hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.status {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.grid {
@@ -276,11 +276,11 @@ data fetching is performed here — the layout is responsible.
.grid td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
font-size: 0.9rem;
}
.grid th {
color: #aab;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
font-size: 0.75rem;
@@ -289,7 +289,7 @@ data fetching is performed here — the layout is responsible.
cursor: pointer;
}
.grid tbody tr:hover {
background: #11172a;
background: var(--color-surface);
}
.sort {
font: inherit;
@@ -313,12 +313,12 @@ data fetching is performed here — the layout is responsible.
font-size: 0.85rem;
padding: 0.15rem 0.5rem;
background: transparent;
color: #d97a7a;
border: 1px solid #2a3150;
color: var(--color-danger);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.delete:hover {
border-color: #d97a7a;
border-color: var(--color-danger);
}
</style>
+1 -1
View File
@@ -53,6 +53,6 @@ e2e specs (`game-shell.spec.ts`, `view-menu`) keep matching.
}
.active-view p {
margin: 0;
color: #555;
color: var(--color-text-muted);
}
</style>
@@ -238,7 +238,7 @@ matching `pkg/model/report/battle.go` and it plays back.
min-height: 0;
margin: 0 auto;
padding: 0.75rem 1rem;
color: #d6dcf2;
color: var(--color-text);
font-family: inherit;
box-sizing: border-box;
}
@@ -263,19 +263,19 @@ matching `pkg/model/report/battle.go` and it plays back.
}
.back-btn {
appearance: none;
background: #1f2748;
color: #d6dcf2;
border: 1px solid #2c3568;
background: var(--color-surface-raised);
color: var(--color-text);
border: 1px solid var(--color-border);
padding: 0.3rem 0.6rem;
border-radius: 3px;
cursor: pointer;
font-size: 0.8rem;
}
.back-btn:hover {
background: #2a3463;
background: var(--color-surface-hover);
}
.progress {
color: #93a0d0;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
font-size: 0.85rem;
flex: 0 0 auto;
@@ -283,8 +283,8 @@ matching `pkg/model/report/battle.go` and it plays back.
text-align: right;
}
.scene {
background: #0a0d1a;
border: 1px solid #1e264a;
background: var(--color-bg);
border: 1px solid var(--color-border-subtle);
border-radius: 4px;
overflow: hidden;
flex: 1 1 auto;
@@ -294,7 +294,7 @@ matching `pkg/model/report/battle.go` and it plays back.
width: 100%;
margin: 0;
flex: 0 0 auto;
accent-color: #6d7bb5;
accent-color: var(--color-accent);
}
.log {
flex: 0 1 auto;
@@ -306,7 +306,7 @@ matching `pkg/model/report/battle.go` and it plays back.
}
.log h3 {
margin: 0 0 0.3rem;
color: #93a0d0;
color: var(--color-text-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
@@ -318,12 +318,12 @@ matching `pkg/model/report/battle.go` and it plays back.
padding: 0;
font-size: 0.85rem;
overflow-y: auto;
color: #c6cdf0;
color: var(--color-text-muted);
flex: 1 1 auto;
min-height: 0;
}
.log li {
border-bottom: 1px solid #1c2240;
border-bottom: 1px solid var(--color-border-subtle);
}
.log-row-btn {
display: block;
@@ -338,11 +338,11 @@ matching `pkg/model/report/battle.go` and it plays back.
}
.log-row-btn:hover,
.log-row-btn:focus-visible {
background: #131a36;
background: var(--color-surface-hover);
}
.log li[data-current="true"] .log-row-btn {
color: #ffe27a;
color: var(--color-accent);
font-weight: 600;
background: #1a2240;
background: var(--color-accent-subtle);
}
</style>
@@ -119,8 +119,8 @@ nowhere to go and step-forward when the timeline is at its end.
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.75rem;
background: #131934;
border: 1px solid #1e264a;
background: var(--color-surface);
border: 1px solid var(--color-border-subtle);
border-radius: 4px;
}
.spacer {
@@ -128,9 +128,9 @@ nowhere to go and step-forward when the timeline is at its end.
}
button {
appearance: none;
background: #1f2748;
color: #d6dcf2;
border: 1px solid #2c3568;
background: var(--color-surface-raised);
color: var(--color-text);
border: 1px solid var(--color-border);
padding: 0.35rem 0.7rem;
border-radius: 3px;
cursor: pointer;
@@ -139,7 +139,7 @@ nowhere to go and step-forward when the timeline is at its end.
min-width: 2.5rem;
}
button:hover:not(:disabled) {
background: #2a3463;
background: var(--color-surface-hover);
}
button:disabled {
opacity: 0.4;
@@ -150,6 +150,6 @@ nowhere to go and step-forward when the timeline is at its end.
font-variant-numeric: tabular-nums;
}
.log-toggle.active {
background: #2a3463;
background: var(--color-surface-hover);
}
</style>
@@ -172,13 +172,13 @@ calculator math — so the ship-group upgrade flow can reuse it later.
gap: 0.35rem;
}
.col-head {
color: #8890b0;
color: var(--color-text-muted);
font-size: 0.7rem;
text-align: center;
text-transform: lowercase;
}
.label {
color: #aab;
color: var(--color-text-muted);
font-size: 0.8rem;
}
input {
@@ -187,19 +187,19 @@ calculator math — so the ship-group upgrade flow can reuse it later.
width: 100%;
min-width: 0;
padding: 0.2rem 0.35rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
font-variant-numeric: tabular-nums;
}
input[data-computed="true"],
input[readonly] {
color: #9fb0ff;
background: #11162a;
color: var(--color-accent);
background: var(--color-surface-raised);
}
input[aria-invalid="true"] {
border-color: #d97a7a;
border-color: var(--color-danger);
}
.tech-cell {
display: flex;
+44 -18
View File
@@ -7,8 +7,20 @@ Sessions and Theme) take over.
-->
<script lang="ts">
import { onMount } from "svelte";
import { i18n, SUPPORTED_LOCALES, type Locale } from "$lib/i18n/index.svelte";
import {
i18n,
SUPPORTED_LOCALES,
type Locale,
type TranslationKey,
} from "$lib/i18n/index.svelte";
import { session } from "$lib/session-store.svelte";
import { theme, type ThemeChoice } from "$lib/theme/theme.svelte";
const THEME_CHOICES: ReadonlyArray<{ id: ThemeChoice; key: TranslationKey }> = [
{ id: "system", key: "game.shell.menu.theme_system" },
{ id: "light", key: "game.shell.menu.theme_light" },
{ id: "dark", key: "game.shell.menu.theme_dark" },
];
let open = $state(false);
let rootEl: HTMLDivElement | null = $state(null);
@@ -27,6 +39,11 @@ Sessions and Theme) take over.
i18n.setLocale(value);
}
function selectTheme(event: Event): void {
const value = (event.target as HTMLSelectElement).value as ThemeChoice;
theme.setChoice(value);
}
function onKeyDown(event: KeyboardEvent): void {
if (event.key === "Escape" && open) {
open = false;
@@ -69,10 +86,19 @@ Sessions and Theme) take over.
<button type="button" role="menuitem" data-testid="account-menu-sessions" disabled>
{i18n.t("game.shell.menu.sessions")}
</button>
<button type="button" role="menuitem" data-testid="account-menu-theme" disabled>
{i18n.t("game.shell.menu.theme")}
</button>
<label class="locale" data-testid="account-menu-language">
<label class="field" data-testid="account-menu-theme">
<span>{i18n.t("game.shell.menu.theme")}</span>
<select
data-testid="account-menu-theme-select"
value={theme.choice}
onchange={selectTheme}
>
{#each THEME_CHOICES as entry (entry.id)}
<option value={entry.id}>{i18n.t(entry.key)}</option>
{/each}
</select>
</label>
<label class="field" data-testid="account-menu-language">
<span>{i18n.t("game.shell.menu.language")}</span>
<select
data-testid="account-menu-language-select"
@@ -106,12 +132,12 @@ Sessions and Theme) take over.
padding: 0.25rem 0.6rem;
background: transparent;
color: inherit;
border: 1px solid #2a3150;
border-radius: 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
}
.trigger:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
.surface {
position: absolute;
@@ -120,10 +146,10 @@ Sessions and Theme) take over.
min-width: 12rem;
display: flex;
flex-direction: column;
background: #14182a;
border: 1px solid #2a3150;
border-radius: 6px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
background: var(--color-surface-overlay);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
z-index: 50;
}
.surface > button,
@@ -137,23 +163,23 @@ Sessions and Theme) take over.
cursor: pointer;
}
.surface > button:hover:not(:disabled) {
background: #1c2238;
background: var(--color-surface-hover);
}
.surface > button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.locale {
.field {
display: flex;
align-items: center;
gap: 0.5rem;
}
.locale select {
.field select {
font: inherit;
background: #1c2238;
background: var(--color-surface-raised);
color: inherit;
border: 1px solid #2a3150;
border-radius: 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: 0.15rem 0.35rem;
}
</style>
+7 -7
View File
@@ -84,10 +84,10 @@ absent until Phase 24 wires push-event state.
align-items: center;
gap: 1rem;
padding: 0.5rem 0.75rem;
background: #0a0e1a;
color: #e8eaf6;
border-bottom: 1px solid #20253a;
font-family: system-ui, sans-serif;
background: var(--color-bg);
color: var(--color-text);
border-bottom: 1px solid var(--color-border-subtle);
font-family: var(--font-sans);
}
.left,
.right {
@@ -108,13 +108,13 @@ absent until Phase 24 wires push-event state.
padding: 0.25rem 0.6rem;
background: transparent;
color: inherit;
border: 1px solid #2a3150;
border-radius: 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
display: none;
}
.sidebar-toggle:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
@media (min-width: 768px) and (max-width: 1023.98px) {
.sidebar-toggle {
@@ -51,9 +51,9 @@ budget.
align-items: center;
gap: 0.75rem;
padding: 0.4rem 0.9rem;
background: #2a2438;
color: #efe9c8;
border-bottom: 1px solid #45375a;
background: var(--color-surface);
color: var(--color-warning);
border-bottom: 1px solid var(--color-border);
font-family: system-ui, sans-serif;
font-size: 0.9rem;
}
@@ -69,12 +69,12 @@ budget.
padding: 0.25rem 0.65rem;
background: transparent;
color: inherit;
border: 1px solid #6c5a8a;
border: 1px solid var(--color-border-strong);
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
}
.return:hover {
background: #3a3050;
background: var(--color-surface-hover);
}
</style>
@@ -177,14 +177,14 @@ Selecting a row calls `gameState.viewTurn(N)`; the row that matches
padding: 0.25rem 0.55rem;
background: transparent;
color: inherit;
border: 1px solid #2a3150;
border: 1px solid var(--color-border);
border-radius: 4px;
cursor: pointer;
line-height: 1;
}
.step:hover:not(:disabled),
.trigger:hover:not(:disabled) {
background: #1c2238;
background: var(--color-surface-hover);
}
.step:disabled,
.trigger:disabled {
@@ -204,10 +204,10 @@ Selecting a row calls `gameState.viewTurn(N)`; the row that matches
overflow-y: auto;
display: flex;
flex-direction: column;
background: #14182a;
border: 1px solid #2a3150;
background: var(--color-surface-overlay);
border: 1px solid var(--color-border);
border-radius: 6px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
box-shadow: var(--shadow-lg);
z-index: 50;
}
.row {
@@ -224,11 +224,11 @@ Selecting a row calls `gameState.viewTurn(N)`; the row that matches
cursor: pointer;
}
.row:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
.row.viewed {
font-weight: 600;
background: #1a2040;
background: var(--color-surface-raised);
}
.label {
flex: 1;
@@ -243,8 +243,8 @@ Selecting a row calls `gameState.viewTurn(N)`; the row that matches
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.05rem 0.4rem;
background: #2a3150;
color: #d8def0;
background: var(--color-border);
color: var(--color-text);
border-radius: 999px;
}
@media (max-width: 767.98px) {
+10 -10
View File
@@ -157,12 +157,12 @@ polishes microcopy.
padding: 0.25rem 0.6rem;
background: transparent;
color: inherit;
border: 1px solid #2a3150;
border: 1px solid var(--color-border);
border-radius: 4px;
cursor: pointer;
}
.trigger:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
.icon-dropdown {
display: inline;
@@ -185,10 +185,10 @@ polishes microcopy.
min-width: 14rem;
display: flex;
flex-direction: column;
background: #14182a;
border: 1px solid #2a3150;
background: var(--color-surface-overlay);
border: 1px solid var(--color-border);
border-radius: 6px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
box-shadow: var(--shadow-lg);
z-index: 50;
}
.surface > button,
@@ -213,12 +213,12 @@ polishes microcopy.
text-align: center;
font-size: 0.75rem;
border-radius: 999px;
background: #2a4d7d;
color: #fff;
background: var(--color-accent);
color: var(--color-accent-contrast);
}
.surface > button:hover,
.surface > details > summary:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
.surface > details > summary {
list-style: none;
@@ -238,7 +238,7 @@ polishes microcopy.
display: flex;
flex-direction: column;
padding-left: 0.5rem;
border-left: 1px solid #2a3150;
border-left: 1px solid var(--color-border);
margin: 0 0.5rem 0.25rem;
}
.sub > button {
@@ -251,7 +251,7 @@ polishes microcopy.
cursor: pointer;
}
.sub > button:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
@media (max-width: 767.98px) {
.surface {
+3
View File
@@ -100,6 +100,9 @@ const en = {
"game.shell.menu.settings": "settings",
"game.shell.menu.sessions": "sessions",
"game.shell.menu.theme": "theme",
"game.shell.menu.theme_system": "system",
"game.shell.menu.theme_light": "light",
"game.shell.menu.theme_dark": "dark",
"game.shell.menu.language": "language",
"game.shell.menu.logout": "logout",
"game.shell.coming_soon": "coming soon",
+3
View File
@@ -101,6 +101,9 @@ const ru: Record<keyof typeof en, string> = {
"game.shell.menu.settings": "настройки",
"game.shell.menu.sessions": "сессии",
"game.shell.menu.theme": "тема",
"game.shell.menu.theme_system": "системная",
"game.shell.menu.theme_light": "светлая",
"game.shell.menu.theme_dark": "тёмная",
"game.shell.menu.language": "язык",
"game.shell.menu.logout": "выйти",
"game.shell.coming_soon": "скоро будет",
@@ -98,9 +98,9 @@ dismiss from the IA section §6 land in Phase 35 polish.
bottom: 3.25rem;
max-height: calc(100vh - 6rem);
overflow-y: auto;
background: #14182a;
color: #e8eaf6;
border-top: 1px solid #2a3150;
background: var(--color-surface-overlay);
color: var(--color-text);
border-top: 1px solid var(--color-border);
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
z-index: 40;
}
@@ -113,13 +113,13 @@ dismiss from the IA section §6 land in Phase 35 polish.
font-size: 1rem;
padding: 0.2rem 0.55rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.close:hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
</style>
+17 -17
View File
@@ -344,7 +344,7 @@ field with five buttons.
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #aab;
color: var(--color-text-muted);
}
.name {
margin: 0;
@@ -361,7 +361,7 @@ field with five buttons.
display: contents;
}
.field dt {
color: #aab;
color: var(--color-text-muted);
font-size: 0.85rem;
}
.field dd {
@@ -371,7 +371,7 @@ field with five buttons.
}
.hint {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.85rem;
}
.action {
@@ -381,14 +381,14 @@ field with five buttons.
font-size: 0.85rem;
padding: 0.2rem 0.55rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.action:hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.rename {
display: flex;
@@ -397,23 +397,23 @@ field with five buttons.
}
.rename-label {
font-size: 0.85rem;
color: #aab;
color: var(--color-text-muted);
}
.rename-input {
font: inherit;
padding: 0.3rem 0.5rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
}
.rename-input[aria-invalid="true"] {
border-color: #d97a7a;
border-color: var(--color-danger);
}
.rename-error {
margin: 0;
font-size: 0.8rem;
color: #d97a7a;
color: var(--color-danger);
}
.rename-actions {
display: flex;
@@ -425,15 +425,15 @@ field with five buttons.
font-size: 0.85rem;
padding: 0.25rem 0.65rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.rename-confirm:not(:disabled):hover,
.rename-cancel:hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.rename-confirm:disabled {
cursor: not-allowed;
@@ -262,7 +262,7 @@ The component is purposely deferential to the existing infrastructure:
.title {
margin: 0;
font-size: 0.85rem;
color: #aab;
color: var(--color-text-muted);
font-weight: 500;
}
.slots {
@@ -276,7 +276,7 @@ The component is purposely deferential to the existing infrastructure:
display: contents;
}
.slot-label {
color: #aab;
color: var(--color-text-muted);
font-size: 0.85rem;
align-self: center;
}
@@ -290,25 +290,25 @@ The component is purposely deferential to the existing infrastructure:
font-variant-numeric: tabular-nums;
}
.empty {
color: #888;
color: var(--color-text-muted);
font-style: italic;
}
.destination {
color: #e8eaf6;
color: var(--color-text);
}
.action {
font: inherit;
font-size: 0.8rem;
padding: 0.15rem 0.5rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.action:not(:disabled):hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.action:disabled {
cursor: not-allowed;
@@ -320,19 +320,19 @@ The component is purposely deferential to the existing infrastructure:
align-items: center;
flex-wrap: wrap;
padding: 0.3rem 0.5rem;
background: rgba(255, 224, 130, 0.1);
border: 1px solid #ffe082;
background: var(--color-warning-subtle);
border: 1px solid var(--color-warning);
border-radius: 4px;
}
.pick-message {
color: #ffe082;
color: var(--color-warning);
font-size: 0.85rem;
flex: 1;
}
.no-destinations {
margin: 0;
font-size: 0.8rem;
color: #888;
color: var(--color-text-muted);
font-style: italic;
}
</style>
@@ -322,7 +322,7 @@ they carry more user intent).
.title {
margin: 0;
font-size: 0.85rem;
color: #aab;
color: var(--color-text-muted);
font-weight: 500;
}
.row {
@@ -339,21 +339,21 @@ they carry more user intent).
font-size: 0.85rem;
padding: 0.25rem 0.55rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.seg:not(:disabled):hover,
.sub-seg:not(:disabled):hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.seg.active,
.sub-seg.active {
color: #e8eaf6;
border-color: #6d8cff;
background: rgba(109, 140, 255, 0.15);
color: var(--color-text);
border-color: var(--color-accent);
background: var(--color-accent-subtle);
}
.seg:disabled,
.sub-seg:disabled {
@@ -363,7 +363,7 @@ they carry more user intent).
.empty {
margin: 0;
font-size: 0.8rem;
color: #888;
color: var(--color-text-muted);
font-style: italic;
}
</style>
@@ -162,7 +162,7 @@ ship-groups table view with an additional `(planet, race)` filter.
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #aab;
color: var(--color-text-muted);
}
.rows {
list-style: none;
@@ -195,17 +195,17 @@ ship-groups table view with an additional `(planet, race)` filter.
cursor: pointer;
}
.select:hover {
border-color: #2a3150;
background: #0d1224;
border-color: var(--color-border);
background: var(--color-surface-hover);
}
.race {
font-weight: 600;
}
.class {
color: #cdd;
color: var(--color-text-muted);
}
.count,
.mass {
color: #aab;
color: var(--color-text-muted);
}
</style>
@@ -90,9 +90,9 @@ mounted by the in-game shell layout only while the active tool is
bottom: 3.25rem;
max-height: calc(100vh - 6rem);
overflow-y: auto;
background: #14182a;
color: #e8eaf6;
border-top: 1px solid #2a3150;
background: var(--color-surface-overlay);
color: var(--color-text);
border-top: 1px solid var(--color-border);
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
z-index: 40;
}
@@ -105,13 +105,13 @@ mounted by the in-game shell layout only while the active tool is
font-size: 1rem;
padding: 0.2rem 0.55rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.close:hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
</style>
@@ -267,7 +267,7 @@ variant — for Phase 19 the inspector is intentionally read-only.
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #aab;
color: var(--color-text-muted);
}
.name {
margin: 0;
@@ -284,7 +284,7 @@ variant — for Phase 19 the inspector is intentionally read-only.
display: contents;
}
.field dt {
color: #aab;
color: var(--color-text-muted);
font-size: 0.85rem;
}
.field dd {
@@ -294,7 +294,7 @@ variant — for Phase 19 the inspector is intentionally read-only.
}
.hint {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.85rem;
}
</style>
@@ -1151,14 +1151,14 @@ modernize cost preview backed by `core.blockUpgradeCost`.
font-size: 0.85rem;
padding: 0.2rem 0.55rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.action:not(:disabled):hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.action:disabled {
cursor: not-allowed;
@@ -1169,25 +1169,25 @@ modernize cost preview backed by `core.blockUpgradeCost`.
flex-direction: column;
gap: 0.4rem;
padding: 0.5rem;
border: 1px solid #2a3150;
border: 1px solid var(--color-border);
border-radius: 3px;
background: #0d1224;
background: var(--color-surface-raised);
}
.form label {
display: flex;
flex-direction: column;
gap: 0.2rem;
font-size: 0.85rem;
color: #aab;
color: var(--color-text-muted);
}
.form input[type="number"],
.form input[type="text"],
.form select {
font: inherit;
padding: 0.25rem 0.4rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
}
.form .destination-readonly {
@@ -1199,7 +1199,7 @@ modernize cost preview backed by `core.blockUpgradeCost`.
font-size: 0.85rem;
}
.form .destination-readonly .label {
color: #aab;
color: var(--color-text-muted);
}
.form-actions {
display: flex;
@@ -1211,14 +1211,14 @@ modernize cost preview backed by `core.blockUpgradeCost`.
font-size: 0.85rem;
padding: 0.25rem 0.65rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.form-actions button.primary {
color: #e8eaf6;
border-color: #4f6dd9;
color: var(--color-text);
border-color: var(--color-accent);
}
.form-actions button:disabled {
cursor: not-allowed;
@@ -1227,25 +1227,25 @@ modernize cost preview backed by `core.blockUpgradeCost`.
.preview {
margin: 0;
font-size: 0.85rem;
color: #aab;
color: var(--color-text-muted);
}
.warning {
margin: 0;
font-size: 0.85rem;
color: #d9a07a;
color: var(--color-warning);
}
.locked {
margin: 0;
padding: 0.4rem 0.55rem;
font-size: 0.85rem;
color: #aab;
background: #14182a;
border: 1px solid #2a3150;
color: var(--color-text-muted);
background: var(--color-surface-overlay);
border: 1px solid var(--color-border);
border-radius: 3px;
}
.hint {
margin: 0;
font-size: 0.8rem;
color: #888;
color: var(--color-text-muted);
}
</style>
+14 -14
View File
@@ -205,10 +205,10 @@ destinations beats the duplication.
display: flex;
justify-content: space-around;
align-items: stretch;
background: #0a0e1a;
color: #e8eaf6;
border-top: 1px solid #20253a;
font-family: system-ui, sans-serif;
background: var(--color-bg);
color: var(--color-text);
border-top: 1px solid var(--color-border-subtle);
font-family: var(--font-sans);
}
.tabs button {
flex: 1;
@@ -220,13 +220,13 @@ destinations beats the duplication.
font: inherit;
font-size: 0.75rem;
background: transparent;
color: #aab;
color: var(--color-text-muted);
border: 0;
cursor: pointer;
}
.tabs button.active {
color: #e8eaf6;
background: #1c2238;
color: var(--color-text);
background: var(--color-surface-hover);
}
.tabs .icon {
font-size: 1.25rem;
@@ -240,12 +240,12 @@ destinations beats the duplication.
overflow-y: auto;
display: flex;
flex-direction: column;
background: #14182a;
color: #e8eaf6;
border-top: 1px solid #2a3150;
background: var(--color-surface-overlay);
color: var(--color-text);
border-top: 1px solid var(--color-border);
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
z-index: 50;
font-family: system-ui, sans-serif;
font-family: var(--font-sans);
}
.drawer > button,
.drawer > details > summary {
@@ -259,7 +259,7 @@ destinations beats the duplication.
}
.drawer > button:hover,
.drawer > details > summary:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
.drawer > details > summary {
list-style: none;
@@ -279,7 +279,7 @@ destinations beats the duplication.
display: flex;
flex-direction: column;
padding-left: 0.5rem;
border-left: 1px solid #2a3150;
border-left: 1px solid var(--color-border);
margin: 0 0.5rem 0.25rem;
}
.sub > button {
@@ -292,6 +292,6 @@ destinations beats the duplication.
cursor: pointer;
}
.sub > button:hover {
background: #1c2238;
background: var(--color-surface-hover);
}
</style>
@@ -690,15 +690,15 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
font-size: 0.8rem;
padding: 0.25rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.modes button.active {
color: #e8eaf6;
border-color: #6d8cff;
background: #11162a;
color: var(--color-text);
border-color: var(--color-accent);
background: var(--color-surface-raised);
}
.namebar {
display: flex;
@@ -710,13 +710,13 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
font: inherit;
font-size: 0.85rem;
padding: 0.25rem 0.4rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
}
.name[aria-invalid="true"] {
border-color: #d97a7a;
border-color: var(--color-danger);
}
.create,
.delete {
@@ -724,21 +724,21 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
font-size: 0.8rem;
padding: 0.25rem 0.55rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.create:not(:disabled):hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.create:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.delete {
color: #d97a7a;
color: var(--color-danger);
align-self: flex-start;
}
.load {
@@ -755,23 +755,23 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
font-size: 0.75rem;
padding: 0.15rem 0.4rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.seg button.active {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.custom-load {
width: 4rem;
font: inherit;
font-size: 0.8rem;
padding: 0.15rem 0.3rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
}
.results,
@@ -787,12 +787,12 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
gap: 0.35rem;
}
.col-head {
color: #8890b0;
color: var(--color-text-muted);
font-size: 0.7rem;
text-align: center;
}
.label {
color: #aab;
color: var(--color-text-muted);
font-size: 0.8rem;
}
.cell {
@@ -812,20 +812,20 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
font: inherit;
font-size: 0.8rem;
padding: 0.15rem 0.3rem;
background: #0a0e1a;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 3px;
font-variant-numeric: tabular-nums;
text-align: right;
}
.cell.locked input {
color: #9fb0ff;
border-color: #6d8cff;
color: var(--color-accent);
border-color: var(--color-accent);
}
.cell.infeasible input {
border-color: #d97a7a;
color: #f0a0a0;
border-color: var(--color-danger);
color: var(--color-danger);
}
.lock {
flex: none;
@@ -846,7 +846,7 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
opacity: 0.2;
}
.planet {
border-top: 1px solid #20253a;
border-top: 1px solid var(--color-border-subtle);
padding-top: 0.5rem;
display: flex;
flex-direction: column;
@@ -854,13 +854,13 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
}
.hint {
margin: 0;
color: #888;
color: var(--color-text-muted);
font-size: 0.8rem;
}
.planet-name {
margin: 0;
font-size: 0.8rem;
color: #cdd3f0;
color: var(--color-text);
}
.planet-stats {
margin: 0;
@@ -873,7 +873,7 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
display: contents;
}
.planet-stats dt {
color: #aab;
color: var(--color-text-muted);
font-size: 0.8rem;
}
.planet-stats dd {
@@ -884,11 +884,11 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
}
.rrow.total .label {
grid-column: 1 / 3;
color: #cdd3f0;
color: var(--color-text);
white-space: nowrap;
}
input[aria-invalid="true"] {
border-color: #d97a7a;
border-color: var(--color-danger);
}
.seg button:disabled {
opacity: 0.4;
@@ -897,6 +897,6 @@ long-lived planning tool. `ensureGame` resets it when the game changes.
.full-capacity {
font-variant-numeric: tabular-nums;
font-size: 0.8rem;
color: #9fb0ff;
color: var(--color-accent);
}
</style>
@@ -157,6 +157,6 @@ from the Phase 10 stub.
.tool > p {
margin: 0;
padding: 0 1rem 1rem;
color: #888;
color: var(--color-text-muted);
}
</style>
+37 -37
View File
@@ -264,7 +264,7 @@ Tests exercise the tab through `__galaxyDebug.seedOrderDraft`
}
.empty {
margin: 0;
color: #888;
color: var(--color-text-muted);
}
.commands {
list-style: none;
@@ -280,12 +280,12 @@ Tests exercise the tab through `__galaxyDebug.seedOrderDraft`
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
background: #14182a;
border: 1px solid #20253a;
background: var(--color-surface-overlay);
border: 1px solid var(--color-border-subtle);
border-radius: 4px;
}
.index {
color: #aab;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
}
.label {
@@ -300,28 +300,28 @@ Tests exercise the tab through `__galaxyDebug.seedOrderDraft`
letter-spacing: 0.04em;
padding: 0.1rem 0.4rem;
border-radius: 999px;
border: 1px solid #2a3150;
color: #aab;
border: 1px solid var(--color-border);
color: var(--color-text-muted);
}
.status-applied {
color: #8be9a3;
border-color: #2f6d3f;
color: var(--color-success);
border-color: var(--color-success);
}
.status-rejected {
color: #d97a7a;
border-color: #6d2f2f;
color: var(--color-danger);
border-color: var(--color-danger);
}
.status-invalid {
color: #d6b86c;
border-color: #6d562f;
color: var(--color-warning);
border-color: var(--color-warning);
}
.status-submitting {
color: #6d8cff;
border-color: #2f3f6d;
color: var(--color-accent);
border-color: var(--color-border);
}
.status-conflict {
color: #d99a4b;
border-color: #6d4a2f;
color: var(--color-warning);
border-color: var(--color-warning);
}
.banner {
margin: 0 0 0.5rem;
@@ -331,28 +331,28 @@ Tests exercise the tab through `__galaxyDebug.seedOrderDraft`
line-height: 1.3;
}
.banner-conflict {
color: #f1bf78;
background: #2a1f10;
border: 1px solid #6d4a2f;
color: var(--color-warning);
background: var(--color-warning-subtle);
border: 1px solid var(--color-warning);
}
.banner-paused {
color: #d4d4d4;
background: #1a1f2a;
border: 1px solid #2f3f55;
color: var(--color-text-muted);
background: var(--color-surface);
border: 1px solid var(--color-border);
}
.delete {
font: inherit;
font-size: 0.85rem;
padding: 0.2rem 0.55rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.delete:hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
.sync {
display: flex;
@@ -360,38 +360,38 @@ Tests exercise the tab through `__galaxyDebug.seedOrderDraft`
justify-content: space-between;
gap: 0.5rem;
font-size: 0.8rem;
color: #aab;
color: var(--color-text-muted);
}
.sync-error {
color: #d97a7a;
color: var(--color-danger);
}
.sync-synced {
color: #8be9a3;
color: var(--color-success);
}
.sync-syncing {
color: #6d8cff;
color: var(--color-accent);
}
.sync-offline {
color: #b9a566;
color: var(--color-warning);
}
.sync-conflict {
color: #d99a4b;
color: var(--color-warning);
}
.sync-paused {
color: #d4d4d4;
color: var(--color-text-muted);
}
.sync-retry {
font: inherit;
font-size: 0.8rem;
padding: 0.15rem 0.5rem;
background: transparent;
color: #aab;
border: 1px solid #2a3150;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: 3px;
cursor: pointer;
}
.sync-retry:hover {
color: #e8eaf6;
border-color: #6d8cff;
color: var(--color-text);
border-color: var(--color-accent);
}
</style>
+4 -4
View File
@@ -108,9 +108,9 @@ through the binding without extra plumbing.
flex-direction: column;
width: 18rem;
min-width: 18rem;
background: #0e1322;
color: #e8eaf6;
border-left: 1px solid #20253a;
background: var(--color-surface);
color: var(--color-text);
border-left: 1px solid var(--color-border-subtle);
}
.head {
display: flex;
@@ -130,7 +130,7 @@ through the binding without extra plumbing.
cursor: pointer;
}
.close:hover {
color: #6d8cff;
color: var(--color-accent);
}
.content {
flex: 1;
+6 -6
View File
@@ -51,24 +51,24 @@ flips it on.
display: flex;
gap: 0.25rem;
padding: 0.5rem 0.5rem 0;
border-bottom: 1px solid #20253a;
font-family: system-ui, sans-serif;
border-bottom: 1px solid var(--color-border-subtle);
font-family: var(--font-sans);
}
.tab-bar button {
font: inherit;
font-size: 0.9rem;
padding: 0.4rem 0.75rem;
background: transparent;
color: #aab;
color: var(--color-text-muted);
border: 0;
border-bottom: 2px solid transparent;
cursor: pointer;
}
.tab-bar button.active {
color: #e8eaf6;
border-bottom-color: #6d8cff;
color: var(--color-text);
border-bottom-color: var(--color-accent);
}
.tab-bar button:hover:not(.active) {
color: #e8eaf6;
color: var(--color-text);
}
</style>
+29
View File
@@ -0,0 +1,29 @@
/*
* Global element baseline, layered on top of the design tokens. Kept
* deliberately small: it sets the document background, default text
* colour/typography, a token-driven focus ring, and selection colour.
* Component-scoped styles still own everything else.
*
* The focus-ring rule uses :where() so its specificity is zero and any
* component that defines its own focus treatment wins without !important.
*/
body {
margin: 0;
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--leading-normal);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:where(*):focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
::selection {
background: var(--color-accent-subtle);
}
+91
View File
@@ -0,0 +1,91 @@
/**
* Theme selection store.
*
* Holds the user's theme choice (system / light / dark), resolves it to a
* concrete light-or-dark theme, persists the choice to `localStorage`,
* and writes the resolved theme to `data-theme` on the document root so
* the token overrides in `tokens.css` take effect.
*
* First paint is handled by the inline guard in `app.html`, which sets
* `data-theme` from the same `localStorage` key before the app boots;
* this store mirrors that logic and takes over once mounted, including
* reacting to OS theme changes while the choice is `system`.
*/
/** A user's theme preference; `system` follows the OS setting. */
export type ThemeChoice = "system" | "light" | "dark";
/** A concrete theme actually applied to the document. */
export type ResolvedTheme = "light" | "dark";
/** `localStorage` key shared with the pre-paint guard in `app.html`. */
export const THEME_STORAGE_KEY = "galaxy-theme";
const SYSTEM_LIGHT_QUERY = "(prefers-color-scheme: light)";
function readStoredChoice(): ThemeChoice {
if (typeof localStorage === "undefined") return "dark";
const value = localStorage.getItem(THEME_STORAGE_KEY);
return value === "light" || value === "dark" || value === "system"
? value
: "dark";
}
function systemTheme(): ResolvedTheme {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
return "dark";
}
return window.matchMedia(SYSTEM_LIGHT_QUERY).matches ? "light" : "dark";
}
/**
* Reactive theme store. `choice` is the persisted preference and
* `resolved` is the concrete theme after applying `system`.
*/
class ThemeStore {
#choice = $state<ThemeChoice>(readStoredChoice());
#system = $state<ResolvedTheme>(systemTheme());
constructor() {
if (
typeof window !== "undefined" &&
typeof window.matchMedia === "function"
) {
window
.matchMedia(SYSTEM_LIGHT_QUERY)
.addEventListener("change", (event: MediaQueryListEvent) => {
this.#system = event.matches ? "light" : "dark";
if (this.#choice === "system") this.#apply();
});
}
this.#apply();
}
/** The persisted user preference. */
get choice(): ThemeChoice {
return this.#choice;
}
/** The concrete theme currently applied to the document. */
get resolved(): ResolvedTheme {
return this.#choice === "system" ? this.#system : this.#choice;
}
/** Persist a new preference and apply it to the document. */
setChoice(choice: ThemeChoice): void {
this.#choice = choice;
if (typeof localStorage !== "undefined") {
localStorage.setItem(THEME_STORAGE_KEY, choice);
}
this.#apply();
}
#apply(): void {
if (typeof document !== "undefined") {
document.documentElement.dataset.theme = this.resolved;
}
}
}
/** The application-wide theme store singleton. */
export const theme = new ThemeStore();
+139
View File
@@ -0,0 +1,139 @@
/*
* Galaxy UI design tokens.
*
* The single source of every theme value in the client. Components must
* reference these custom properties (`var(--color-)`, `var(--space-)`)
* instead of literal hex/px so a palette change is a one-file edit and
* the light/dark themes stay in sync.
*
* Structure:
* :root theme-independent scales (space, radii,
* typography) identical in every theme.
* :root, [data-theme=dark] the dark palette (also the default when no
* theme is set, so first paint is never bare).
* [data-theme=light] the light palette overrides.
*
* The resolved theme is written to `data-theme` on <html> by the
* pre-paint guard in `app.html` and thereafter by
* `$lib/theme/theme.svelte.ts`. See `ui/docs/design-system.md`.
*/
:root {
/* Spacing scale (4px base). */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--space-6: 2rem;
--space-8: 3rem;
/* Corner radii. */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 10px;
--radius-pill: 999px;
/* Typography. */
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas,
monospace;
--text-xs: 0.75rem;
--text-sm: 0.8125rem;
--text-base: 0.875rem;
--text-md: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.375rem;
--leading-tight: 1.25;
--leading-normal: 1.5;
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
}
/*
* Dark palette the project's native look and the default. Listed under
* both :root and [data-theme=dark] so a document with no data-theme yet
* (e.g. the pre-paint instant) still renders dark rather than unstyled.
*/
:root,
:root[data-theme="dark"] {
color-scheme: dark;
--color-bg: #0a0e1a;
--color-surface: #0e1322;
--color-surface-raised: #161d33;
--color-surface-overlay: #161b2e;
--color-surface-hover: #1c2238;
--color-border-subtle: #20253a;
--color-border: #2a3150;
--color-border-strong: #3a4470;
--color-text: #e8eaf6;
--color-text-muted: #9aa4c6;
--color-text-faint: #6b7396;
--color-accent: #6d8cff;
--color-accent-hover: #88a0ff;
--color-accent-active: #5a78f0;
--color-accent-contrast: #0a0e1a;
--color-accent-subtle: rgba(109, 140, 255, 0.14);
--color-danger: #e07a7a;
--color-danger-contrast: #0a0e1a;
--color-danger-subtle: rgba(224, 122, 122, 0.14);
--color-success: #5fb98c;
--color-success-subtle: rgba(95, 185, 140, 0.14);
--color-warning: #e0b15a;
--color-warning-subtle: rgba(224, 177, 90, 0.14);
--color-focus: var(--color-accent);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45);
}
/* Light palette. */
:root[data-theme="light"] {
color-scheme: light;
--color-bg: #f3f5fb;
--color-surface: #ffffff;
--color-surface-raised: #f7f9fd;
--color-surface-overlay: #ffffff;
--color-surface-hover: #eaeef8;
--color-border-subtle: #e6eaf3;
--color-border: #d3d9e8;
--color-border-strong: #aeb8d4;
--color-text: #1a2138;
--color-text-muted: #59617e;
--color-text-faint: #8b93ad;
--color-accent: #4a63d8;
--color-accent-hover: #3a52c8;
--color-accent-active: #2f46b5;
--color-accent-contrast: #ffffff;
--color-accent-subtle: rgba(74, 99, 216, 0.1);
--color-danger: #c84d4d;
--color-danger-contrast: #ffffff;
--color-danger-subtle: rgba(200, 77, 77, 0.1);
--color-success: #2f8f63;
--color-success-subtle: rgba(47, 143, 99, 0.12);
--color-warning: #b07d24;
--color-warning-subtle: rgba(176, 125, 36, 0.14);
--color-focus: var(--color-accent);
--shadow-sm: 0 1px 2px rgba(20, 28, 51, 0.08);
--shadow-md: 0 4px 12px rgba(20, 28, 51, 0.1);
--shadow-lg: 0 8px 24px rgba(20, 28, 51, 0.14);
}
+6 -6
View File
@@ -69,9 +69,9 @@ active so the surrounding shell layout is not perturbed.
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.85rem;
background: #1a2034;
color: #e8eaf6;
border: 1px solid #2c3354;
background: var(--color-surface-overlay);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
font-size: 0.9rem;
@@ -82,7 +82,7 @@ active so the surrounding shell layout is not perturbed.
}
.toast-action {
background: transparent;
color: #8ab4f8;
color: var(--color-accent);
border: none;
font-weight: 600;
cursor: pointer;
@@ -96,7 +96,7 @@ active so the surrounding shell layout is not perturbed.
}
.toast-close {
background: transparent;
color: #94a3b8;
color: var(--color-text-muted);
border: none;
font-size: 1.1rem;
line-height: 1;
@@ -104,6 +104,6 @@ active so the surrounding shell layout is not perturbed.
padding: 0 0.25rem;
}
.toast-close:hover {
color: #e8eaf6;
color: var(--color-text);
}
</style>
+4 -2
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import "$lib/theme/tokens.css";
import "$lib/theme/base.css";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/state";
@@ -99,7 +101,7 @@
<style>
.status {
padding: 2rem;
font-family: system-ui, sans-serif;
padding: var(--space-6);
font-family: var(--font-sans);
}
</style>
@@ -168,15 +168,15 @@
height: 100vh;
margin: 0;
font-family: system-ui, sans-serif;
color: #e8eaf6;
background: #0a0e1a;
color: var(--color-text);
background: var(--color-bg);
}
header {
padding: 0.5rem 1rem;
display: flex;
gap: 1rem;
align-items: center;
border-bottom: 1px solid #20253a;
border-bottom: 1px solid var(--color-border-subtle);
}
h1 {
margin: 0;
@@ -190,15 +190,15 @@
}
button {
padding: 0.25rem 0.75rem;
background: #1c2238;
color: #e8eaf6;
border: 1px solid #2a3150;
background: var(--color-surface-hover);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 4px;
cursor: pointer;
font: inherit;
}
button:hover {
background: #232b48;
background: var(--color-surface-hover);
}
.canvas-wrap {
flex: 1;
@@ -212,7 +212,7 @@
}
.error {
padding: 0.5rem 1rem;
background: #4a1820;
color: #ffb4b4;
background: var(--color-danger-subtle);
color: var(--color-danger);
}
</style>
@@ -594,8 +594,8 @@ fresh.
display: flex;
flex-direction: column;
min-height: 100vh;
background: #0a0e1a;
color: #e8eaf6;
background: var(--color-bg);
color: var(--color-text);
}
.body {
flex: 1;
+8 -8
View File
@@ -526,9 +526,9 @@
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem;
border: 1px solid #ccc;
border: 1px solid var(--color-border);
border-radius: 0.4rem;
background: #fafafa;
background: var(--color-surface-raised);
text-align: left;
font: inherit;
cursor: pointer;
@@ -537,8 +537,8 @@
button.card:disabled {
cursor: not-allowed;
color: #777;
background: #f0f0f0;
color: var(--color-text-faint);
background: var(--color-surface);
}
li.card {
@@ -546,7 +546,7 @@
}
.meta {
color: #555;
color: var(--color-text-muted);
font-size: 0.9rem;
}
@@ -554,7 +554,7 @@
align-self: flex-start;
padding: 0.1rem 0.5rem;
border-radius: 999px;
background: #e7e7e7;
background: var(--color-surface-raised);
font-size: 0.8rem;
}
@@ -587,9 +587,9 @@
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
border: 1px dashed #888;
border: 1px dashed var(--color-text-muted);
border-radius: 0.4rem;
background: #f7f7f7;
background: var(--color-surface-raised);
cursor: pointer;
font: inherit;
}
@@ -263,7 +263,7 @@
}
details {
border: 1px solid #ddd;
border: 1px solid var(--color-border);
border-radius: 0.4rem;
padding: 0.5rem 0.75rem;
display: flex;
@@ -287,6 +287,6 @@
}
small {
color: #666;
color: var(--color-text-muted);
}
</style>
+1 -1
View File
@@ -351,6 +351,6 @@
[role="alert"] {
margin-top: 1rem;
color: #b00020;
color: var(--color-danger);
}
</style>
+88
View File
@@ -0,0 +1,88 @@
import "@testing-library/jest-dom/vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// The store is a module singleton constructed on first import: it reads
// localStorage and `matchMedia` in its constructor. Each test therefore
// stubs `matchMedia` and resets the module registry, then imports a
// freshly-constructed store via `freshStore`.
const STORAGE_KEY = "galaxy-theme";
function stubMatchMedia(prefersLight: boolean): void {
Object.defineProperty(window, "matchMedia", {
writable: true,
configurable: true,
value: (query: string) => ({
matches: query.includes("light") ? prefersLight : false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
});
}
async function freshStore(
prefersLight = false,
): Promise<typeof import("../src/lib/theme/theme.svelte")> {
stubMatchMedia(prefersLight);
vi.resetModules();
return import("../src/lib/theme/theme.svelte");
}
describe("theme store", () => {
beforeEach(() => {
localStorage.clear();
delete document.documentElement.dataset.theme;
});
afterEach(() => {
vi.restoreAllMocks();
});
it("defaults to dark and applies it to the document", async () => {
const { theme } = await freshStore();
expect(theme.choice).toBe("dark");
expect(theme.resolved).toBe("dark");
expect(document.documentElement.dataset.theme).toBe("dark");
});
it("persists an explicit choice and writes data-theme", async () => {
const { theme, THEME_STORAGE_KEY } = await freshStore();
expect(THEME_STORAGE_KEY).toBe(STORAGE_KEY);
theme.setChoice("light");
expect(theme.choice).toBe("light");
expect(theme.resolved).toBe("light");
expect(document.documentElement.dataset.theme).toBe("light");
expect(localStorage.getItem(STORAGE_KEY)).toBe("light");
theme.setChoice("dark");
expect(theme.resolved).toBe("dark");
expect(document.documentElement.dataset.theme).toBe("dark");
expect(localStorage.getItem(STORAGE_KEY)).toBe("dark");
});
it("reads the stored choice on construction", async () => {
localStorage.setItem(STORAGE_KEY, "light");
const { theme } = await freshStore();
expect(theme.choice).toBe("light");
expect(theme.resolved).toBe("light");
});
it("resolves system to the OS preference", async () => {
const { theme } = await freshStore(true);
theme.setChoice("system");
expect(theme.choice).toBe("system");
expect(theme.resolved).toBe("light");
expect(document.documentElement.dataset.theme).toBe("light");
});
it("falls back to dark for an unrecognised stored value", async () => {
localStorage.setItem(STORAGE_KEY, "neon");
const { theme } = await freshStore();
expect(theme.choice).toBe("dark");
});
});