From ba11825b91ac2523a8491ed6cc22cb5e0fda3a1c Mon Sep 17 00:00:00 2001 From: Andrew Zamojc Date: Mon, 15 Jun 2026 19:19:37 -0400 Subject: [PATCH] fix(theme): add overrideTheme to DarkMode and force light on login DarkMode now accepts an `overrideTheme` prop ('light' | 'dark') that supersedes the user's dark-mode preference and writes it to body[data-theme]. The (login) layout mounts so login-page components stop resolving dark-mode colors against a hardcoded light surface. Also fixes a leaked store subscription in the darkMode action. DT-3930 --- src/lib/utilities/dark-mode/dark-mode.svelte | 8 ++- src/lib/utilities/dark-mode/dark-mode.test.ts | 59 +++++++++++++++++-- src/lib/utilities/dark-mode/dark-mode.ts | 33 ++++++++--- src/lib/utilities/dark-mode/index.ts | 2 +- src/routes/(login)/+layout.svelte | 3 + 5 files changed, 91 insertions(+), 14 deletions(-) diff --git a/src/lib/utilities/dark-mode/dark-mode.svelte b/src/lib/utilities/dark-mode/dark-mode.svelte index 5b3ce499e3..c851c600a6 100644 --- a/src/lib/utilities/dark-mode/dark-mode.svelte +++ b/src/lib/utilities/dark-mode/dark-mode.svelte @@ -1,10 +1,14 @@ - + {@render children?.()} diff --git a/src/lib/utilities/dark-mode/dark-mode.test.ts b/src/lib/utilities/dark-mode/dark-mode.test.ts index 951baf8a52..48b81a449f 100644 --- a/src/lib/utilities/dark-mode/dark-mode.test.ts +++ b/src/lib/utilities/dark-mode/dark-mode.test.ts @@ -65,22 +65,73 @@ describe('dark-mode utilities', () => { }); describe('darkMode', () => { - it('should set data-theme to "dark" when dark mode is enabled', async () => { + it('should set data-theme to "dark" when dark mode is enabled', () => { const node = document.createElement('div'); useDarkModePreference.set(true); darkMode(node); - await new Promise((resolve) => setTimeout(resolve, 0)); expect(node.dataset.theme).toBe('dark'); }); - it('should set data-theme to "light" when dark mode is disabled', async () => { + it('should set data-theme to "light" when dark mode is disabled', () => { const node = document.createElement('div'); useDarkModePreference.set(false); darkMode(node); - await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(node.dataset.theme).toBe('light'); + }); + + it('should force "light" via overrideTheme even when dark mode is enabled', () => { + const node = document.createElement('div'); + useDarkModePreference.set(true); + + darkMode(node, 'light'); + + expect(node.dataset.theme).toBe('light'); + }); + + it('should force "dark" via overrideTheme even when dark mode is disabled', () => { + const node = document.createElement('div'); + useDarkModePreference.set(false); + + darkMode(node, 'dark'); + + expect(node.dataset.theme).toBe('dark'); + }); + + it('should re-apply the theme when the override changes via update', () => { + const node = document.createElement('div'); + useDarkModePreference.set(true); + + const action = darkMode(node, 'light'); + expect(node.dataset.theme).toBe('light'); + + action.update('dark'); + expect(node.dataset.theme).toBe('dark'); + }); + + it('should fall back to the store theme when the override is cleared', () => { + const node = document.createElement('div'); + useDarkModePreference.set(true); + + const action = darkMode(node, 'light'); + expect(node.dataset.theme).toBe('light'); + + action.update(undefined); + expect(node.dataset.theme).toBe('dark'); + }); + + it('should stop updating data-theme after destroy', () => { + const node = document.createElement('div'); + useDarkModePreference.set(false); + + const action = darkMode(node); + expect(node.dataset.theme).toBe('light'); + + action.destroy(); + useDarkModePreference.set(true); expect(node.dataset.theme).toBe('light'); }); diff --git a/src/lib/utilities/dark-mode/dark-mode.ts b/src/lib/utilities/dark-mode/dark-mode.ts index 710335fcc8..4b5637276d 100644 --- a/src/lib/utilities/dark-mode/dark-mode.ts +++ b/src/lib/utilities/dark-mode/dark-mode.ts @@ -3,6 +3,7 @@ import { derived } from 'svelte/store'; import { persistStore } from '$lib/stores/persist-store'; export type DarkModePreference = boolean | 'system'; +export type ThemeOverride = 'light' | 'dark'; export const useDarkModePreference = persistStore( 'dark mode', @@ -25,12 +26,30 @@ export const useDarkMode = derived(useDarkModePreference, prefersDarkMode); export const getNextDarkModePreference = (value: DarkModePreference) => value == 'system' ? true : value == true ? false : 'system'; -export const darkMode = (node: HTMLElement) => { - useDarkMode.subscribe((value) => { - if (value) { - node.dataset.theme = 'dark'; - } else { - node.dataset.theme = 'light'; - } +const applyTheme = ( + node: HTMLElement, + prefersDark: boolean, + overrideTheme?: ThemeOverride, +) => { + node.dataset.theme = overrideTheme ?? (prefersDark ? 'dark' : 'light'); +}; + +export const darkMode = (node: HTMLElement, overrideTheme?: ThemeOverride) => { + let override = overrideTheme; + let prefersDark = false; + + const unsubscribe = useDarkMode.subscribe((value) => { + prefersDark = value; + applyTheme(node, prefersDark, override); }); + + return { + update(newOverride?: ThemeOverride) { + override = newOverride; + applyTheme(node, prefersDark, override); + }, + destroy() { + unsubscribe(); + }, + }; }; diff --git a/src/lib/utilities/dark-mode/index.ts b/src/lib/utilities/dark-mode/index.ts index 49e5192a96..2e48089ad4 100644 --- a/src/lib/utilities/dark-mode/index.ts +++ b/src/lib/utilities/dark-mode/index.ts @@ -7,4 +7,4 @@ export { getNextDarkModePreference, } from './dark-mode'; -export type { DarkModePreference } from './dark-mode'; +export type { DarkModePreference, ThemeOverride } from './dark-mode'; diff --git a/src/routes/(login)/+layout.svelte b/src/routes/(login)/+layout.svelte index 2b4a90763d..2b33c58c69 100644 --- a/src/routes/(login)/+layout.svelte +++ b/src/routes/(login)/+layout.svelte @@ -1,6 +1,8 @@ + {@render children()}