Repository Public
1import { create } from 'zustand';23export type ThemePref = 'light' | 'dark' | 'system';4export type Theme = 'light' | 'dark';5export const THEME_KEY = 'cr.theme';6export const THEME_COLOR: Record<Theme, string> = { light: '#F7F6F3', dark: '#080B10' };78const systemDark = () => typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches;9export const resolveTheme = (pref: ThemePref): Theme => (pref === 'dark' || (pref === 'system' && systemDark()) ? 'dark' : 'light');10function readPref(): ThemePref {11 try {12 const v = typeof window === 'undefined' ? null : localStorage.getItem(THEME_KEY);13 return v === 'dark' || v === 'system' ? v : 'light';14 } catch {15 return 'light';16 }17}1819interface ThemeState {20 pref: ThemePref;21 theme: Theme;22 /** Apply a preference: <html data-theme>, theme-color meta, localStorage; cross-fades unless told not to. */23 setPref: (pref: ThemePref, opts?: { animate?: boolean }) => void;24}25function applyDom(pref: ThemePref, theme: Theme, animate: boolean): void {26 const root = document.documentElement;27 if (animate && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {28 root.classList.add('theme-fade');29 window.setTimeout(() => root.classList.remove('theme-fade'), 320);30 }31 root.setAttribute('data-theme', theme);32 root.setAttribute('data-theme-pref', pref);33 document.querySelector('meta[name="theme-color"]')?.setAttribute('content', THEME_COLOR[theme]);34}3536export const useTheme = create<ThemeState>((set, get) => {37 const pref = readPref();38 if (typeof window !== 'undefined') {39 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {40 if (get().pref !== 'system') return;41 const theme = resolveTheme('system');42 applyDom('system', theme, true);43 set({ theme });44 });45 }46 return {47 pref,48 theme: typeof window === 'undefined' ? 'light' : resolveTheme(pref),49 setPref: (next, opts) => {50 const theme = resolveTheme(next);51 try {52 localStorage.setItem(THEME_KEY, next);53 } catch {54 /* storage unavailable */55 }56 applyDom(next, theme, opts?.animate ?? true);57 set({ pref: next, theme });58 },59 };60});61