diff --git a/apps/dashboard/src/app.css b/apps/dashboard/src/app.css
index d01b8942..a76b25ed 100644
--- a/apps/dashboard/src/app.css
+++ b/apps/dashboard/src/app.css
@@ -67,6 +67,9 @@ html {
--scrollbarBG: #fff;
--thumbBG: #ccc;
scroll-behavior: smooth;
+ /* Paint the root with the theme background so the pre-hydration shell
+ matches the mode set by app.html and never flashes white in dark mode. */
+ background-color: var(--background);
}
#calendar .grid {
diff --git a/apps/dashboard/src/app.html b/apps/dashboard/src/app.html
index 58aee939..120e2833 100644
--- a/apps/dashboard/src/app.html
+++ b/apps/dashboard/src/app.html
@@ -7,6 +7,10 @@
+
+
+
%sveltekit.head%
diff --git a/apps/dashboard/src/lib/features/app/app-mode-watcher.svelte b/apps/dashboard/src/lib/features/app/app-mode-watcher.svelte
index ad7d10a4..cded7b0e 100644
--- a/apps/dashboard/src/lib/features/app/app-mode-watcher.svelte
+++ b/apps/dashboard/src/lib/features/app/app-mode-watcher.svelte
@@ -1,21 +1,7 @@
-
+
+
diff --git a/apps/dashboard/src/lib/features/ui/sidebar/footer/theme-toggle.svelte b/apps/dashboard/src/lib/features/ui/sidebar/footer/theme-toggle.svelte
index 070b6448..11dc8a3f 100644
--- a/apps/dashboard/src/lib/features/ui/sidebar/footer/theme-toggle.svelte
+++ b/apps/dashboard/src/lib/features/ui/sidebar/footer/theme-toggle.svelte
@@ -1,30 +1,21 @@
diff --git a/apps/dashboard/src/lib/utils/functions/color-mode.test.ts b/apps/dashboard/src/lib/utils/functions/color-mode.test.ts
index 343168b7..cbd8d6bc 100644
--- a/apps/dashboard/src/lib/utils/functions/color-mode.test.ts
+++ b/apps/dashboard/src/lib/utils/functions/color-mode.test.ts
@@ -1,6 +1,12 @@
import { describe, expect, it } from 'vitest';
-import { MODE_STORAGE_KEY, readStoredColorMode } from './color-mode';
+import {
+ MODE_EXPLICIT_STORAGE_KEY,
+ MODE_STORAGE_KEY,
+ hasExplicitColorModePreference,
+ markColorModeExplicit,
+ resolveStoredColorMode
+} from './color-mode';
function createMemoryStorage(initial: Record = {}): Storage {
const store = new Map(Object.entries(initial));
@@ -27,14 +33,43 @@ function createMemoryStorage(initial: Record = {}): Storage {
};
}
-describe('readStoredColorMode', () => {
+describe('resolveStoredColorMode', () => {
it('defaults to light when nothing is stored', () => {
- expect(readStoredColorMode(createMemoryStorage())).toBe('light');
+ expect(resolveStoredColorMode(createMemoryStorage())).toBe('light');
});
- it('returns the stored sidebar preference', () => {
- const storage = createMemoryStorage({ [MODE_STORAGE_KEY]: 'dark' });
+ it('returns stored light or dark preferences', () => {
+ const lightStorage = createMemoryStorage({ [MODE_STORAGE_KEY]: 'light' });
+ const darkStorage = createMemoryStorage({ [MODE_STORAGE_KEY]: 'dark' });
- expect(readStoredColorMode(storage)).toBe('dark');
+ expect(resolveStoredColorMode(lightStorage)).toBe('light');
+ expect(resolveStoredColorMode(darkStorage)).toBe('dark');
+ });
+
+ it('treats stale system values as light until the user explicitly chooses system', () => {
+ const staleSystemStorage = createMemoryStorage({ [MODE_STORAGE_KEY]: 'system' });
+
+ expect(resolveStoredColorMode(staleSystemStorage)).toBe('light');
+ });
+
+ it('honors system when the user explicitly chose it', () => {
+ const explicitSystemStorage = createMemoryStorage({
+ [MODE_STORAGE_KEY]: 'system',
+ [MODE_EXPLICIT_STORAGE_KEY]: 'true'
+ });
+
+ expect(resolveStoredColorMode(explicitSystemStorage)).toBe('system');
+ });
+});
+
+describe('markColorModeExplicit', () => {
+ it('marks the preference as user-selected', () => {
+ const storage = createMemoryStorage();
+
+ expect(hasExplicitColorModePreference(storage)).toBe(false);
+
+ markColorModeExplicit(storage);
+
+ expect(hasExplicitColorModePreference(storage)).toBe(true);
});
});
diff --git a/apps/dashboard/src/lib/utils/functions/color-mode.ts b/apps/dashboard/src/lib/utils/functions/color-mode.ts
index e2b6b09c..7d0902f0 100644
--- a/apps/dashboard/src/lib/utils/functions/color-mode.ts
+++ b/apps/dashboard/src/lib/utils/functions/color-mode.ts
@@ -1,14 +1,38 @@
export type ColorModePreference = 'light' | 'dark' | 'system';
export const MODE_STORAGE_KEY = 'mode-watcher-mode';
+export const MODE_EXPLICIT_STORAGE_KEY = 'mode-watcher-explicit';
export function isColorModePreference(value: string | null | undefined): value is ColorModePreference {
return value === 'light' || value === 'dark' || value === 'system';
}
-/** Read the sidebar theme preference from mode-watcher storage. */
-export function readStoredColorMode(storage: Pick = localStorage): ColorModePreference {
+export function hasExplicitColorModePreference(storage: Pick = localStorage): boolean {
+ return storage.getItem(MODE_EXPLICIT_STORAGE_KEY) === 'true';
+}
+
+export function markColorModeExplicit(storage: Pick = localStorage): void {
+ storage.setItem(MODE_EXPLICIT_STORAGE_KEY, 'true');
+}
+
+/**
+ * Resolve the stored preference to apply on load.
+ *
+ * mode-watcher used to default to "system" and persist it eagerly, so returning
+ * users still carry a stored "system" they never chose. Only honor "system"
+ * when the user explicitly picked it in the theme toggle; otherwise use light.
+ * This mirrors the inline bootstrap script in `app.html`.
+ */
+export function resolveStoredColorMode(storage: Pick = localStorage): ColorModePreference {
const stored = storage.getItem(MODE_STORAGE_KEY);
- return isColorModePreference(stored) ? stored : 'light';
+ if (!isColorModePreference(stored)) {
+ return 'light';
+ }
+
+ if (stored === 'system' && !hasExplicitColorModePreference(storage)) {
+ return 'light';
+ }
+
+ return stored;
}
diff --git a/apps/dashboard/static/theme-init.js b/apps/dashboard/static/theme-init.js
new file mode 100644
index 00000000..ce48f30b
--- /dev/null
+++ b/apps/dashboard/static/theme-init.js
@@ -0,0 +1,41 @@
+// 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.
+ }
+})();