Files
Rotimi BestandRotimi Best 7605395f44 fix(dashboard): respect defaultMode light and track=false for color mode (#742)
* fix(dashboard): respect defaultMode light and track=false for color mode

ModeWatcher defaults userPrefersMode to system and enables OS tracking
before components mount, so prefers-color-scheme won on first paint even
with defaultMode="light" and track={false}.

- Add a blocking init script in app.html that defaults empty storage to
  light before any JS bundle runs
- Disable ModeWatcher head injection to avoid duplicate init scripts
- Keep track={false} on ModeWatcher and sync system tracking only when
  userPrefersMode is explicitly system
- Update the sidebar theme toggle to enable OS tracking only when the
  user selects System

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(dashboard): ignore stale system color mode until user opts in

mode-watcher leaves mode-watcher-mode as system for anyone who never
picked a theme. ModeWatcher onMount then calls setMode('system'), which
overrides defaultMode="light" and track={false}.

Track an explicit sidebar choice separately and normalize stale system
values to light during the app.html init script and on mount.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(dashboard): bootstrap color mode before mode-watcher module init

Even with cleared localStorage, mode-watcher's module-level derivedMode
could apply the OS color-scheme before ModeWatcher mounted. Stop relying
on ModeWatcher for document classes and bootstrap light mode from
hooks.client.ts immediately after the app.html script runs.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* refactor(dashboard): collapse color-mode workaround onto mode-watcher

The scaffolding (color-mode.svelte.ts state module, color-mode-document.ts,
init-color-mode.client.ts) existed only to work around disableHeadScriptInjection,
which had turned off mode-watcher's own setInitialMode script — the very script
that seeds localStorage before the module's eager "system" write and prevents FOUC.

Let ModeWatcher inject that script again (defaultMode="light") and drive theme
application, tracking, and Sonner theming. Keep only the app.html bootstrap for
the one thing mode-watcher can't do: treat a returning user's stale "system" as
light unless they explicitly chose it. The theme toggle now calls mode-watcher's
setMode directly and marks the choice explicit.

* fix(dashboard): run color-mode bootstrap as external script to satisfy CSP

The inline app.html bootstrap and mode-watcher's own setInitialMode script were
both blocked by the nonce-based CSP (script-src has no 'unsafe-inline'), so
localStorage was never seeded before mode-watcher's eager "system" write — every
fresh load followed the OS and came up dark.

Move the seed + FOUC logic to a same-origin static file (static/theme-init.js),
which 'self' allows without a nonce, and load it as a parser-blocking script in
app.html so it still runs before the app bundle. Disable ModeWatcher's inline
head-script injection since it is CSP-blocked and now redundant, and paint the
root element with var(--background) so the pre-hydration shell never flashes
white in dark mode.

Verified with Playwright across fresh/explicit/stale-system cases in light and
dark OS settings; no theme-related CSP violations remain.

---------

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-07-12 22:07:36 +01:00

42 lines
1.4 KiB
JavaScript

// Runs before the app bundle to set the color mode and prevent a flash of the
// wrong theme. Lives as an external same-origin file (not inline in app.html)
// because the app's CSP has a nonce and no 'unsafe-inline', which blocks inline
// scripts — an external 'self' script is allowed. Seeding localStorage here also
// beats mode-watcher's eager "system" default write when the module loads.
//
// Keep the stale-"system" rule in sync with resolveStoredColorMode in
// src/lib/utils/functions/color-mode.ts.
(function () {
try {
var storageKey = 'mode-watcher-mode';
var explicitKey = 'mode-watcher-explicit';
var stored = localStorage.getItem(storageKey);
var isExplicit = localStorage.getItem(explicitKey) === 'true';
var mode = 'light';
if (stored === 'light' || stored === 'dark') {
mode = stored;
} else if (stored === 'system' && isExplicit) {
mode = 'system';
}
var isLight =
mode === 'light' ||
(mode === 'system' && window.matchMedia('(prefers-color-scheme: light)').matches);
var root = document.documentElement;
if (isLight) {
root.classList.remove('dark');
root.style.colorScheme = 'light';
} else {
root.classList.add('dark');
root.style.colorScheme = 'dark';
}
localStorage.setItem(storageKey, mode);
} catch (error) {
// Ignore storage / matchMedia errors in restricted environments.
}
})();