Acceptance Criteria
ThemeTokensConfig.modes field accepts a record of mode name → PartialTokenContract- Build pipeline emits per-mode stylesheets scoped to
[data-theme="<mode>"] and to the @media (prefers-color-scheme: <mode>) { :root:not([data-theme]) } block for matching system preference - Authors only specify changed tokens in a mode overlay — unspecified tokens inherit from base via the CSS variable cascade
- Lumina's existing
dark.css migrated to modes.dark config form; resulting stylesheet renders identically (visual regression check) - Generated stylesheet output order is deterministic so diffs in CI stay clean
- Mode overlay validation rejects keys not present in
TokenContract with clear errors - Documentation note explaining how to add a custom mode (e.g. high-contrast) — single config snippet, no parallel CSS file required
Approach
Reshape the existing dark-mode CSS into config form. The migration is mechanical for Lumina:
- Read
packages/lumina/tokens/dark.css. - For each
--rf-*: value; declaration, mirror it into modes.dark.<path> in the new config. - Delete the raw CSS file once the generated stylesheet matches.
The generated CSS preserves the existing two-selector pattern (explicit [data-theme="dark"] for user-toggled mode, media query for system preference). That's the SSR-friendly pattern and matches what SPEC-052's cascade resolution will emit.
Authors authoring custom themes specify only the deltas:
{
"theme": {
"modes": {
"dark": {
"color": { "primary": "#a78bfa", "text": "#f1f5f9" }
},
"high-contrast": {
"color": { "border": "#000000", "text": "#000000" }
}
}
}
}
Dependencies
- WORK-185 —
PartialTokenContract shape defined. - WORK-187 — base stylesheet generation infrastructure to extend.
References
- SPEC-048 — "Modes are partials over the base, not parallel contracts" design principle
packages/lumina/tokens/dark.css — file being migrated and eventually removed