import { useContext, useEffect } from 'react'; import { deepEqual } from '@wener/utils'; import { z } from 'zod'; import { useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; import { getPrefersColorSchema } from '../../utils/getPrefersColorSchema'; import { getSupportedThemes } from './getSupportedThemes'; import { DaisyThemeStoreContext, type DaisyThemeStoreState } from './useTheme'; type Updator = (update: Partial | ((state: T) => void)) => void; export namespace DaisyTheme { const Context = DaisyThemeStoreContext; type RootProps = {}; export const Root = () => { return; }; export function useThemeStore() { return useContext(Context); } export function useThemeState(): [DaisyThemeStoreState, Updator] { let store = useThemeStore(); const state = useStore( store, useShallow((s) => s), ); return [ state, (update) => { store.setState(update); }, ]; } export const Sidecar = () => { const store = useContext(Context); useEffect(() => { const storage = globalThis.localStorage; if (!storage && typeof window === 'undefined') { return; } const StateSchema = z.object({ light: z.string().optional(), dark: z.string().optional(), theme: z.string().optional(), custom: z.record(z.string(), z.any()).optional(), }); const closer: Array<() => void> = []; const loadState = (s: string | null) => { if (!s) { return; } try { const loaded = StateSchema.parse(JSON.parse(s)); store.setState(loaded, true); } catch (e) { console.warn('Failed to parse theme state', e); } }; const storageKey = 'THEME_STATE'; // load previous loadState(storage.getItem(storageKey)); // watch storage change { const handleStorageChange = (e: StorageEvent) => { if (e.key !== storageKey) { return; } loadState(e.newValue); }; window.addEventListener('storage', handleStorageChange); closer.push(() => { window.removeEventListener('storage', handleStorageChange); }); } // system { store.setState((s) => { s.system = getPrefersColorSchema(); }); closer.push( watchPrefersColorSchema(() => { store.setState((s) => { s.system = getPrefersColorSchema(); }); }), ); } // persist { let last = StateSchema.parse(store.getState()); const unsub = store.subscribe((s) => { const current = StateSchema.parse(s); if (deepEqual(last, current)) { return; } storage.setItem(storageKey, JSON.stringify(current)); last = current; }); // closer.push(unsub); } // active { // const element = document.documentElement; store.subscribe((s) => { let active = getActiveTheme(s); let schema = getThemeSchema(active); if (s.active === active && s.schema === schema) { return; } store.setState({ active, schema }); const el = globalThis.document?.documentElement; // setElementThemeAttribute(active, element); el?.setAttribute('data-theme', active); el?.setAttribute('data-color-mode', schema); }); } return () => { closer.forEach((c) => c()); }; }, [store]); /* data-color-mode="dark" */ return null; }; export function useThemeSchema(): 'light' | 'dark' { let store = useThemeStore(); return useStore(store, ({ theme }) => { return getThemeSchema(theme); }); } } function getThemeSchema(theme: string | undefined) { switch (theme) { case 'system': return getPrefersColorSchema(); case 'dark': case 'light': return theme; default: return getSupportedThemes().find((v) => v.value === theme)?.schema ?? 'light'; } } function watchPrefersColorSchema(cb: (light: boolean) => void) { const listener = (event: MediaQueryListEvent) => { cb(event.matches); }; const target = window.matchMedia('(prefers-color-scheme: light)'); cb(target.matches); target.addEventListener('change', listener); return () => { target.removeEventListener('change', listener); }; } function getActiveTheme(s: { theme?: string; light?: string; dark?: string; system?: string }) { let theme = s.theme || 'system'; if (theme === 'system') { theme = s.system || 'light'; switch (theme) { case 'dark': return s.dark || theme; case 'light': return s.light || theme; } } return theme || 'light'; }