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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
* 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 <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
parent
bce0f582ce
commit
7605395f44
@@ -1,21 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { ModeWatcher, setMode, systemPrefersMode, userPrefersMode } from '@cio/ui/base/dark-mode';
|
||||
|
||||
import { readStoredColorMode } from '$lib/utils/functions/color-mode';
|
||||
|
||||
$effect(() => {
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storedPreference = readStoredColorMode();
|
||||
systemPrefersMode.tracking(storedPreference === 'system');
|
||||
|
||||
if (storedPreference !== 'system' && userPrefersMode.current !== storedPreference) {
|
||||
setMode(storedPreference);
|
||||
}
|
||||
});
|
||||
import { ModeWatcher } from '@cio/ui/base/dark-mode';
|
||||
</script>
|
||||
|
||||
<ModeWatcher defaultMode="light" track={false} />
|
||||
<!-- theme-init.js (loaded from app.html) handles the pre-paint seed + FOUC. Skip
|
||||
mode-watcher's own head-script injection: it is inline and blocked by our CSP. -->
|
||||
<ModeWatcher defaultMode="light" disableHeadScriptInjection={true} />
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { MonitorIcon, SunIcon, MoonIcon } from '@lucide/svelte';
|
||||
import { userPrefersMode, setMode } from '@cio/ui/base/dark-mode';
|
||||
import { setMode, userPrefersMode } from '@cio/ui/base/dark-mode';
|
||||
|
||||
type ThemeMode = 'light' | 'dark' | 'system';
|
||||
import { markColorModeExplicit, type ColorModePreference } from '$lib/utils/functions/color-mode';
|
||||
|
||||
const themes: { mode: ThemeMode; icon: typeof MonitorIcon }[] = [
|
||||
const themes: { mode: ColorModePreference; icon: typeof MonitorIcon }[] = [
|
||||
{ mode: 'light', icon: SunIcon },
|
||||
{ mode: 'dark', icon: MoonIcon },
|
||||
{ mode: 'system', icon: MonitorIcon }
|
||||
];
|
||||
|
||||
const activeMode = $derived(userPrefersMode.current ?? 'system');
|
||||
const activeMode = $derived(userPrefersMode.current ?? 'light');
|
||||
|
||||
const handleThemeChange = (newMode: ThemeMode) => {
|
||||
if (newMode === 'light') {
|
||||
setMode('light');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMode === 'dark') {
|
||||
setMode('dark');
|
||||
return;
|
||||
}
|
||||
|
||||
setMode('system');
|
||||
const handleThemeChange = (newMode: ColorModePreference) => {
|
||||
markColorModeExplicit();
|
||||
setMode(newMode);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user