Add light mode toggle to Studio shell chrome (1/3) - #506
Conversation
First of a 3-part light-mode rollout (split for reviewability). Adds the theme system and applies it to the app shell: header (with the new Sun/Moon toggle), sidebar nav, status bar, preview panel toolbar, command palette, and domain panel tab bar. Dark remains the default appearance; panels/inputs get their light-mode pass in the follow-up PRs in this stack. - New `lib/theme.svelte.ts`: reactive theme state, defaults to (and live-follows) `prefers-color-scheme` until the user explicitly toggles, then persists to localStorage. - `main.ts` binds the theme to the mount root before Svelte mounts, so the first paint is never wrong-themed. - Tailwind v4 class-based `dark:` variant (`@custom-variant dark`) in `app.css`, kept separate from the framework's own `[data-theme]` (which only governs the dogfooded `--sf-*` tokens). - Includes a couple of small hardening/accessibility fixes surfaced by review bots on the original combined PR: aria-label/aria-pressed on the toggle, light-mode backgrounds for the save-state error/saved chips, a feature-detect fallback for the deprecated MediaQueryList.addListener API, and a missing dark: pairing on a command-palette badge.
|
Warning Review limit reached
Next review available in: 49 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoAdd light/dark theme toggle for Studio shell chrome
AI Description
Diagram
High-Level Assessment
Files changed (10)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
11 rules 1. No scheme listener teardown
|
| if (typeof matchMedia !== "undefined") { | ||
| const mql = matchMedia("(prefers-color-scheme: dark)"); | ||
| const onChange = (e: MediaQueryListEvent) => { | ||
| if (!followSystem) return; | ||
| themeState.value = e.matches ? "dark" : "light"; | ||
| applyToRoot(); | ||
| }; | ||
| // Safari < 14 only exposes the older addListener/removeListener pair. | ||
| if (typeof mql.addEventListener === "function") { | ||
| mql.addEventListener("change", onChange); | ||
| } else if (typeof mql.addListener === "function") { | ||
| mql.addListener(onChange); | ||
| } |
There was a problem hiding this comment.
2. No scheme listener teardown 🐞 Bug ☼ Reliability
The theme module registers a matchMedia('(prefers-color-scheme: dark)') change listener at module
load and never removes it, so HMR or repeated module evaluation can accumulate duplicate listeners.
This can cause redundant theme updates and minor memory growth in dev/remount scenarios.
Agent Prompt
## Issue description
A `matchMedia` change listener is attached at module scope and is never removed. In production this typically lives for the page lifetime, but under HMR or repeated module evaluation it can create duplicate listeners.
## Issue Context
The callback is guarded by `followSystem`, but the listener remains registered regardless, and there is no exported teardown API.
## Fix Focus Areas
- configurator/src/lib/theme.svelte.ts[61-74]
- configurator/src/main.ts[19-30]
## Suggested fix
Option A (simple guard):
- Add a module-level boolean like `let listenerBound = false;` and skip binding if already bound.
Option B (proper teardown):
- Store `mql` + handler references, export `disposeThemeListener()` that calls `removeEventListener('change', handler)` (and `removeListener` fallback).
- In dev/HMR, call disposal from an HMR hook (or if you ever add an explicit unmount path, call it there).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Triaged both findings from the last review pass — no code changes for either:
Generated by Claude Code |
First of a 3-part light-mode rollout for the SLASHED Studio configurator, split from #500 for reviewability. Stack: 1/3 (this PR) → #TBD (2/3) → #TBD (3/3).
Summary
Adds a light/dark theme toggle to the Studio chrome and applies it to the app shell. Dark remains the default appearance until the user toggles or their OS is already light; panels/inputs get their light-mode pass in the two follow-up PRs in this stack (currently unstyled for light mode until those land — expected/acceptable interim state since dark stays the default).
configurator/src/lib/theme.svelte.ts: reactive theme state that defaults to (and live-follows)prefers-color-schemeuntil the user explicitly toggles, then persists the choice tolocalStorage.main.tsbinds the theme to the mount root before Svelte mounts, so the first paint is never wrong-themed (no flash).dark:variant (@custom-variant dark) added inapp.css, kept independent of the framework's own[data-theme](which only governs the dogfooded--sf-*tokens loaded bymain.ts).StudioHeader.svelte.App.svelte,StudioHeader,SidebarNav,StatusBar,PreviewPanel,CommandPalette,DomainPanel.Review-bot fixes folded in
Several bots reviewed the original combined PR (#500) before this split. The following real findings are fixed here:
StudioHeader: theme toggle now hasaria-label/aria-pressed; save-state error/saved chips get a light-mode background (were dark-only, producing unreadable text in light mode).theme.svelte.ts: feature-detectsMediaQueryList.addEventListenerwith a fallback to the deprecatedaddListener(Safari < 14), so an unusual environment can't throw at module-import time.CommandPalette: a domain badge was missing itsdark:text-color pairing.StatusBar: the domain label had a contrast bug in light mode (text-slate-300onbg-slate-100), fixed to match the file's existing muted-text convention.Not actioned (raised, considered, deliberately skipped): a suggestion to scope the
dark:variant to a component-specific attribute instead of a.darkclass, to guard against a coincidental.darkclass from an unrelated embedding host. The bot's own alternatives analysis recommended keeping the current approach; flagging here for visibility rather than adding speculative complexity.Testing
npx svelte-check— 0 errorsnpx vitest run— 113/113 passingGenerated by Claude Code