Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/mapgen-studio/src/app/StudioProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useEffect } from "react";

import { Toaster, TooltipProvider } from "../components/ui";
import { useThemePreference } from "../ui/hooks";

Expand All @@ -15,6 +17,15 @@ import { StudioShell } from "./StudioShell";
*/
export function StudioProviders() {
const { preference, isLightMode, cyclePreference } = useThemePreference();

// Runtime half of the single-`.dark`-class strategy: the index.html bootstrap
// owns the pre-paint initial state (no flash); this effect owns every change
// after hydration so the theme toggle actually re-themes the chrome
// (theme-class-sync spec — without it the cycle only relabels the button).
useEffect(() => {
document.documentElement.classList.toggle("dark", !isLightMode);
}, [isLightMode]);

return (
<TooltipProvider delayDuration={300}>
<StudioShell
Expand Down
25 changes: 20 additions & 5 deletions docs/projects/mapgen-studio-redesign/00-GOAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,23 @@ contract in [`pass-2-design-fixes.md`](pass-2-design-fixes.md). Five OpenSpec
slices stacked on `design/a11y-fixes` (Graphite-only, never submitted); each slice
closes only with visual proof from the running app:

- [ ] **C1** `mapgen-studio-layout-geometry` — `design/layout-geometry`
- [ ] **C2** `mapgen-studio-form-hierarchy` — `design/form-hierarchy`
- [ ] **C3** `mapgen-studio-run-console` — `design/run-console`
- [ ] **C4** `mapgen-studio-explore-toolbar` — `design/explore-toolbar`
- [ ] **C5** `mapgen-studio-first-run-visibility` — `design/first-run-visibility`
- [x] **C1** `mapgen-studio-layout-geometry` — `design/layout-geometry` (340px dock,
content-driven header reserve, header→footer docks, scroll-edge fade)
- [x] **C2** `mapgen-studio-form-hierarchy` — `design/form-hierarchy` (foreground
labels vs muted prose; rawErrors-gated alert regions — 40 phantom alerts → 0)
- [x] **C3** `mapgen-studio-run-console` — `design/run-console` (single Run CTA in
the footer console; full-width Save & Deploy; "Balanced" not "Bal")
- [x] **C4** `mapgen-studio-explore-toolbar` — `design/explore-toolbar` (Render/Space
as inset segmented controls)
- [x] **C5** `mapgen-studio-first-run-visibility` — `design/first-run-visibility`
(diagnosis: not reproducible at tip; fit/selection fallbacks verified; rAF-starvation
commit backstop added to vizStore)
- [x] **C6** `mapgen-studio-theme-class-sync` — `design/theme-class-sync` (found by
the visual gate: nothing wrote `.dark` after boot; theme toggle now re-themes live)

**Pass-2 closure (2026-06-11):** all six slices implemented + visually verified on
:5173 (dark + light screenshots, DOM measurements, fresh-state first-run proof).
Gates green at tip: tsc, build + worker-bundle, 144 tests, six strict OpenSpec
validations. Review wave: no P1/P2; two P3s repaired in-stack (scroll-fade padding;
a foreign `codex/sieve-engine-reference` docs commit `gt move`d out of the stack
base). Stack remains UNSUBMITTED per the standing rule.
37 changes: 37 additions & 0 deletions openspec/changes/mapgen-studio-theme-class-sync/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Why

Found during Pass-2 final verification (2026-06-11, live app): cycling the theme
control changes the button label (Auto → Light → Dark) and the deck.gl scene's
`lightMode` input, but the chrome never re-themes — `<html>` keeps whatever
class the `index.html` no-flash bootstrap set at boot. The theming-finish slice
deleted the legacy runtime theme plumbing in favor of the single `.dark` class,
but no runtime writer of that class was ever added: the only `classList.add("dark")`
calls in the app are the pre-paint bootstrap. The in-app theme toggle is
therefore functionally broken; light mode is reachable only by reloading with a
light OS preference and no stored override.

## Target Authority Refs

- `apps/mapgen-studio/.interface-design/system.md` (§Theming mechanism: single
`.dark` class on `<html>`, "one theme switch writes the class")
- `docs/projects/mapgen-studio-redesign/pass-2-design-fixes.md` (verification
contract — found by the visual gate)

## What Changes

- `StudioProviders` (the component that owns theme-preference wiring) gains the
missing effect: it syncs `document.documentElement.classList` `.dark` to the
resolved preference (`isLightMode`) on change. The bootstrap still owns the
pre-paint initial state; React owns every change after hydration.

## Out Of Scope / Parity Guarantees

- No change to the preference cycle, storage key (`theme-preference`), bootstrap
script, tokens, or deck.gl `lightMode` input.

## Verification Gates

- `bun run openspec -- validate mapgen-studio-theme-class-sync --strict`
- tsc + mapgen-studio vitest project green
- Visual on :5173: cycle the theme control → chrome re-themes live (dark and
light screenshots); reload preserves the chosen theme without flash.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## ADDED Requirements

### Requirement: The Theme Toggle Re-Themes The Chrome At Runtime

The resolved theme preference SHALL be synced to the `.dark` class on `<html>`
at runtime: cycling the theme control re-themes the chrome immediately, with the
`index.html` bootstrap owning only the pre-paint initial state.

#### Scenario: Cycling to light re-themes live
- **WHEN** the user cycles the theme control to an effective light preference
- **THEN** `<html>` loses the `dark` class without a reload and the chrome renders the light tokens

#### Scenario: Cycling to dark re-themes live
- **WHEN** the user cycles the theme control to an effective dark preference
- **THEN** `<html>` gains the `dark` class without a reload and the chrome renders the dark tokens

#### Scenario: Reload preserves the stored choice without flash
- **WHEN** the page reloads after the user chose a theme
- **THEN** the bootstrap applies the stored preference pre-paint and the runtime sync agrees with it
11 changes: 11 additions & 0 deletions openspec/changes/mapgen-studio-theme-class-sync/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 1. Runtime class sync

- [x] 1.1 `StudioProviders.tsx`: effect syncing `document.documentElement.classList`
`.dark` to `!isLightMode` on change (bootstrap keeps pre-paint ownership).

## 2. Verification

- [x] 2.1 `bun run openspec -- validate mapgen-studio-theme-class-sync --strict`
- [x] 2.2 tsc + mapgen-studio vitest project green
- [x] 2.3 Visual on :5173: cycle theme → live light + dark screenshots; reload
keeps the chosen theme.