From c4daf47628abe447fbe4ca883f23ae400edbadbd Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 27 Feb 2026 12:21:44 +0300 Subject: [PATCH] feat(ThemeManager): create ThemeManager that uses persistent storage to store preferred user theme --- .../store/ThemeManager/ThemeManager.svelte.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts diff --git a/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts b/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts new file mode 100644 index 0000000..d551017 --- /dev/null +++ b/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts @@ -0,0 +1,99 @@ +import { createPersistentStore } from '$shared/lib'; + +type Theme = 'light' | 'dark'; +type ThemeSource = 'system' | 'user'; + +class ThemeManager { + // Private reactive state + #theme = $state('light'); + #source = $state('system'); + #mediaQuery: MediaQueryList | null = null; + #store = createPersistentStore('glyphdiff:theme', null); + #systemChangeHandler = this.#onSystemChange.bind(this); + + constructor() { + // Derive initial values from stored preference or OS + const stored = this.#store.value; + if (stored === 'dark' || stored === 'light') { + this.#theme = stored; + this.#source = 'user'; + } else { + this.#theme = this.#getSystemTheme(); + this.#source = 'system'; + } + } + + get value(): Theme { + return this.#theme; + } + + get source(): ThemeSource { + return this.#source; + } + + get isDark(): boolean { + return this.#theme === 'dark'; + } + + get isUserControlled(): boolean { + return this.#source === 'user'; + } + + /** Call once in root onMount */ + init(): void { + this.#applyToDom(this.#theme); + this.#mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + this.#mediaQuery.addEventListener('change', this.#systemChangeHandler); + } + + /** Call in root onDestroy */ + destroy(): void { + this.#mediaQuery?.removeEventListener('change', this.#systemChangeHandler); + this.#mediaQuery = null; + } + + setTheme(theme: Theme): void { + this.#source = 'user'; + this.#theme = theme; + this.#store.value = theme; + this.#applyToDom(theme); + } + + toggle(): void { + this.setTheme(this.value === 'dark' ? 'light' : 'dark'); + } + + /** Hand control back to OS */ + resetToSystem(): void { + this.#store.clear(); + this.#theme = this.#getSystemTheme(); + this.#source = 'system'; + this.#applyToDom(this.#theme); + } + + // ------------------------- + // Private helpers + // ------------------------- + + #getSystemTheme(): Theme { + if (typeof window === 'undefined') { + return 'light'; + } + + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + #applyToDom(theme: Theme): void { + document.documentElement.classList.toggle('dark', theme === 'dark'); + } + + #onSystemChange(e: MediaQueryListEvent): void { + if (this.#source === 'system') { + this.#theme = e.matches ? 'dark' : 'light'; + this.#applyToDom(this.#theme); + } + } +} + +// Export a singleton — one instance for the whole app +export const themeManager = new ThemeManager();