implement dark mode with instant theme switching
This commit is contained in:
parent
5eefcc0bc7
commit
2806d73f72
4 changed files with 98 additions and 3 deletions
11
src/app.html
11
src/app.html
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
69
src/lib/stores/theme.svelte.ts
Normal file
69
src/lib/stores/theme.svelte.ts
Normal 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()
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue