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>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<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%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
import { createQuery } from '@tanstack/svelte-query'
|
import { createQuery } from '@tanstack/svelte-query'
|
||||||
import { crewQueries } from '$lib/api/queries/crew.queries'
|
import { crewQueries } from '$lib/api/queries/crew.queries'
|
||||||
import { userAdapter } from '$lib/api/adapters/user.adapter'
|
import { userAdapter } from '$lib/api/adapters/user.adapter'
|
||||||
|
import { themeStore, type ThemePreference } from '$lib/stores/theme.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|
@ -190,12 +191,17 @@
|
||||||
body: JSON.stringify(updatedUser)
|
body: JSON.stringify(updatedUser)
|
||||||
})
|
})
|
||||||
|
|
||||||
// If language, theme, or bahamut mode changed, we need a full page reload
|
// Apply theme change immediately without reload
|
||||||
if (originalLanguage !== language || originalTheme !== theme || user.bahamut !== bahamut) {
|
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()
|
await invalidateAll()
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} else {
|
} 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()
|
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 { browser } from '$app/environment'
|
||||||
import { QueryClientProvider } from '@tanstack/svelte-query'
|
import { QueryClientProvider } from '@tanstack/svelte-query'
|
||||||
import { Toaster } from 'svelte-sonner'
|
import { Toaster } from 'svelte-sonner'
|
||||||
|
import { themeStore, type ThemePreference } from '$lib/stores/theme.svelte'
|
||||||
import type { LayoutData } from './$types'
|
import type { LayoutData } from './$types'
|
||||||
|
|
||||||
const { data, children } = $props<{
|
const { data, children } = $props<{
|
||||||
data: LayoutData & { [key: string]: any }
|
data: LayoutData & { [key: string]: any }
|
||||||
children: () => any
|
children: () => any
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// Initialize theme from user cookie preference
|
||||||
|
$effect(() => {
|
||||||
|
if (browser) {
|
||||||
|
const userTheme = (data.currentUser?.theme as ThemePreference) ?? 'system'
|
||||||
|
themeStore.init(userTheme)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue