implement dark mode with instant theme switching

This commit is contained in:
Justin Edmund 2025-12-20 21:13:26 -08:00
parent 5eefcc0bc7
commit 2806d73f72
4 changed files with 98 additions and 3 deletions

View file

@ -3,6 +3,17 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function () {
try {
var c = document.cookie.split('; ').find(function (c) { return c.startsWith('user=') })
var u = c ? JSON.parse(decodeURIComponent(c.split('=')[1])) : null
var p = (u && u.theme) || 'system'
var d = p === 'dark' || (p === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
document.documentElement.setAttribute('data-theme', d ? 'dark' : 'light')
} catch (e) {}
})()
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View file

@ -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()
}

View file

@ -0,0 +1,69 @@
export type ThemePreference = 'system' | 'light' | 'dark'
export type ResolvedTheme = 'light' | 'dark'
class ThemeStore {
#preference = $state<ThemePreference>('system')
#resolved = $state<ResolvedTheme>('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()

View file

@ -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)
}
})
</script>
<svelte:head>