From 2806d73f722278bf2089e4f0f4fdcf51546eb820 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 20 Dec 2025 21:13:26 -0800 Subject: [PATCH] implement dark mode with instant theme switching --- src/app.html | 11 ++++ src/lib/components/UserSettingsModal.svelte | 12 +++- src/lib/stores/theme.svelte.ts | 69 +++++++++++++++++++++ src/routes/+layout.svelte | 9 +++ 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/lib/stores/theme.svelte.ts diff --git a/src/app.html b/src/app.html index 50bd0b52..33afd594 100644 --- a/src/app.html +++ b/src/app.html @@ -3,6 +3,17 @@ + %sveltekit.head% diff --git a/src/lib/components/UserSettingsModal.svelte b/src/lib/components/UserSettingsModal.svelte index ade05d42..01063f2f 100644 --- a/src/lib/components/UserSettingsModal.svelte +++ b/src/lib/components/UserSettingsModal.svelte @@ -14,6 +14,7 @@ import { createQuery } from '@tanstack/svelte-query' import { crewQueries } from '$lib/api/queries/crew.queries' import { userAdapter } from '$lib/api/adapters/user.adapter' + import { themeStore, type ThemePreference } from '$lib/stores/theme.svelte' interface Props { open: boolean @@ -190,12 +191,17 @@ body: JSON.stringify(updatedUser) }) - // If language, theme, or bahamut mode changed, we need a full page reload - if (originalLanguage !== language || originalTheme !== theme || user.bahamut !== bahamut) { + // Apply theme change immediately without reload + if (originalTheme !== theme) { + themeStore.setTheme(theme as ThemePreference) + } + + // If language or bahamut mode changed, we need a full page reload + if (originalLanguage !== language || user.bahamut !== bahamut) { await invalidateAll() window.location.reload() } else { - // For other changes (element, picture, gender), invalidate to refresh layout data + // For other changes (element, picture, gender, theme), invalidate to refresh layout data await invalidateAll() } diff --git a/src/lib/stores/theme.svelte.ts b/src/lib/stores/theme.svelte.ts new file mode 100644 index 00000000..69bfa3ea --- /dev/null +++ b/src/lib/stores/theme.svelte.ts @@ -0,0 +1,69 @@ +export type ThemePreference = 'system' | 'light' | 'dark' +export type ResolvedTheme = 'light' | 'dark' + +class ThemeStore { + #preference = $state('system') + #resolved = $state('light') + #initialized = false + #mediaQuery: MediaQueryList | null = null + + get preference(): ThemePreference { + return this.#preference + } + + get resolved(): ResolvedTheme { + return this.#resolved + } + + /** + * Initialize the theme store with a preference (typically from user cookie/session) + * This should be called once on app startup from the root layout + */ + init(initial: ThemePreference = 'system') { + if (this.#initialized || typeof window === 'undefined') return + this.#initialized = true + + this.#preference = initial + this.#resolved = this.#resolveTheme(initial) + this.#applyTheme(this.#resolved) + + // Listen for system preference changes + this.#mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + this.#mediaQuery.addEventListener('change', this.#handleSystemChange) + } + + /** + * Set the theme preference and apply it immediately + */ + setTheme(pref: ThemePreference) { + this.#preference = pref + this.#resolved = this.#resolveTheme(pref) + this.#applyTheme(this.#resolved) + } + + #resolveTheme(pref: ThemePreference): ResolvedTheme { + if (pref === 'system') { + return this.#getSystemTheme() + } + return pref + } + + #getSystemTheme(): ResolvedTheme { + if (typeof window === 'undefined') return 'light' + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + } + + #applyTheme(theme: ResolvedTheme) { + if (typeof document === 'undefined') return + document.documentElement.setAttribute('data-theme', theme) + } + + #handleSystemChange = (e: MediaQueryListEvent) => { + if (this.#preference === 'system') { + this.#resolved = e.matches ? 'dark' : 'light' + this.#applyTheme(this.#resolved) + } + } +} + +export const themeStore = new ThemeStore() diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4d205a3a..d8416c34 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,12 +6,21 @@ import { browser } from '$app/environment' import { QueryClientProvider } from '@tanstack/svelte-query' import { Toaster } from 'svelte-sonner' + import { themeStore, type ThemePreference } from '$lib/stores/theme.svelte' import type { LayoutData } from './$types' const { data, children } = $props<{ data: LayoutData & { [key: string]: any } children: () => any }>() + + // Initialize theme from user cookie preference + $effect(() => { + if (browser) { + const userTheme = (data.currentUser?.theme as ThemePreference) ?? 'system' + themeStore.init(userTheme) + } + })