|
|
@@ -1,4 +1,11 @@
|
|
|
-import { createContext, useContext, useEffect, useState, useMemo } from 'react'
|
|
|
+import {
|
|
|
+ createContext,
|
|
|
+ useCallback,
|
|
|
+ useContext,
|
|
|
+ useEffect,
|
|
|
+ useMemo,
|
|
|
+ useState,
|
|
|
+} from 'react'
|
|
|
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
|
|
|
|
|
|
type Theme = 'dark' | 'light' | 'system'
|
|
|
@@ -7,6 +14,7 @@ type ResolvedTheme = Exclude<Theme, 'system'>
|
|
|
const DEFAULT_THEME = 'system'
|
|
|
const THEME_COOKIE_NAME = 'vite-ui-theme'
|
|
|
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
|
|
|
+const THEMES = new Set<Theme>(['dark', 'light', 'system'])
|
|
|
|
|
|
type ThemeProviderProps = {
|
|
|
children: React.ReactNode
|
|
|
@@ -32,66 +40,76 @@ const initialState: ThemeProviderState = {
|
|
|
|
|
|
const ThemeContext = createContext<ThemeProviderState>(initialState)
|
|
|
|
|
|
+function getSystemTheme(): ResolvedTheme {
|
|
|
+ if (typeof window === 'undefined') return 'light'
|
|
|
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
|
+ ? 'dark'
|
|
|
+ : 'light'
|
|
|
+}
|
|
|
+
|
|
|
+function resolveTheme(theme: Theme): ResolvedTheme {
|
|
|
+ return theme === 'system' ? getSystemTheme() : theme
|
|
|
+}
|
|
|
+
|
|
|
+function getStoredTheme(storageKey: string, fallback: Theme): Theme {
|
|
|
+ const storedTheme = getCookie(storageKey) as Theme | undefined
|
|
|
+ return storedTheme && THEMES.has(storedTheme) ? storedTheme : fallback
|
|
|
+}
|
|
|
+
|
|
|
export function ThemeProvider({
|
|
|
children,
|
|
|
defaultTheme = DEFAULT_THEME,
|
|
|
storageKey = THEME_COOKIE_NAME,
|
|
|
...props
|
|
|
}: ThemeProviderProps) {
|
|
|
- const [theme, _setTheme] = useState<Theme>(
|
|
|
- () => (getCookie(storageKey) as Theme) || defaultTheme
|
|
|
+ const [theme, _setTheme] = useState<Theme>(() =>
|
|
|
+ getStoredTheme(storageKey, defaultTheme)
|
|
|
+ )
|
|
|
+ const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() =>
|
|
|
+ resolveTheme(getStoredTheme(storageKey, defaultTheme))
|
|
|
)
|
|
|
-
|
|
|
- // Optimized: Memoize the resolved theme calculation to prevent unnecessary re-computations
|
|
|
- const resolvedTheme = useMemo((): ResolvedTheme => {
|
|
|
- if (theme === 'system') {
|
|
|
- return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
|
- ? 'dark'
|
|
|
- : 'light'
|
|
|
- }
|
|
|
- return theme as ResolvedTheme
|
|
|
- }, [theme])
|
|
|
|
|
|
useEffect(() => {
|
|
|
const root = window.document.documentElement
|
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
|
|
|
|
- const applyTheme = (currentResolvedTheme: ResolvedTheme) => {
|
|
|
- root.classList.remove('light', 'dark') // Remove existing theme classes
|
|
|
- root.classList.add(currentResolvedTheme) // Add the new theme class
|
|
|
- }
|
|
|
-
|
|
|
- const handleChange = () => {
|
|
|
- if (theme === 'system') {
|
|
|
- const systemTheme = mediaQuery.matches ? 'dark' : 'light'
|
|
|
- applyTheme(systemTheme)
|
|
|
- }
|
|
|
+ const applyTheme = () => {
|
|
|
+ const nextResolvedTheme = theme === 'system' ? getSystemTheme() : theme
|
|
|
+ root.classList.remove('light', 'dark')
|
|
|
+ root.classList.add(nextResolvedTheme)
|
|
|
+ setResolvedTheme(nextResolvedTheme)
|
|
|
}
|
|
|
|
|
|
- applyTheme(resolvedTheme)
|
|
|
+ applyTheme()
|
|
|
|
|
|
- mediaQuery.addEventListener('change', handleChange)
|
|
|
+ mediaQuery.addEventListener('change', applyTheme)
|
|
|
|
|
|
- return () => mediaQuery.removeEventListener('change', handleChange)
|
|
|
- }, [theme, resolvedTheme])
|
|
|
+ return () => mediaQuery.removeEventListener('change', applyTheme)
|
|
|
+ }, [theme])
|
|
|
|
|
|
- const setTheme = (theme: Theme) => {
|
|
|
- setCookie(storageKey, theme, THEME_COOKIE_MAX_AGE)
|
|
|
- _setTheme(theme)
|
|
|
- }
|
|
|
+ const setTheme = useCallback(
|
|
|
+ (theme: Theme) => {
|
|
|
+ setCookie(storageKey, theme, THEME_COOKIE_MAX_AGE)
|
|
|
+ _setTheme(theme)
|
|
|
+ },
|
|
|
+ [storageKey]
|
|
|
+ )
|
|
|
|
|
|
- const resetTheme = () => {
|
|
|
+ const resetTheme = useCallback(() => {
|
|
|
removeCookie(storageKey)
|
|
|
- _setTheme(DEFAULT_THEME)
|
|
|
- }
|
|
|
-
|
|
|
- const contextValue = {
|
|
|
- defaultTheme,
|
|
|
- resolvedTheme,
|
|
|
- resetTheme,
|
|
|
- theme,
|
|
|
- setTheme,
|
|
|
- }
|
|
|
+ _setTheme(defaultTheme)
|
|
|
+ }, [defaultTheme, storageKey])
|
|
|
+
|
|
|
+ const contextValue = useMemo(
|
|
|
+ () => ({
|
|
|
+ defaultTheme,
|
|
|
+ resolvedTheme,
|
|
|
+ resetTheme,
|
|
|
+ theme,
|
|
|
+ setTheme,
|
|
|
+ }),
|
|
|
+ [defaultTheme, resolvedTheme, resetTheme, theme, setTheme]
|
|
|
+ )
|
|
|
|
|
|
return (
|
|
|
<ThemeContext value={contextValue} {...props}>
|