"use client" import * as React from "react" export type ColorScheme = "light" | "dark" | "system" export type ResolvedColorScheme = "light" | "dark" const DEFAULT_STORAGE_KEY = "exxat-ds:color-scheme" type ColorSchemeContextValue = { theme?: ColorScheme setTheme: (theme: ColorScheme) => void resolvedTheme?: ResolvedColorScheme systemTheme?: ResolvedColorScheme themes: ColorScheme[] forcedTheme?: ColorScheme } const ColorSchemeContext = React.createContext(null) function readSystemScheme(): ResolvedColorScheme { if (typeof window === "undefined") return "light" return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" } function resolveScheme(theme: ColorScheme, systemTheme: ResolvedColorScheme): ResolvedColorScheme { return theme === "system" ? systemTheme : theme } function readStoredTheme(storageKey: string, fallback: ColorScheme): ColorScheme { if (typeof window === "undefined") return fallback try { const stored = localStorage.getItem(storageKey) if (stored === "light" || stored === "dark" || stored === "system") return stored } catch { /* ignore */ } return fallback } function applyDocumentScheme( resolved: ResolvedColorScheme, { attribute, disableTransitionOnChange, }: { attribute: "class"; disableTransitionOnChange: boolean }, ) { const root = document.documentElement if (disableTransitionOnChange) { const style = document.createElement("style") style.appendChild(document.createTextNode("*,*::before,*::after{transition:none!important}")) document.head.appendChild(style) window.setTimeout(() => style.remove(), 0) } if (attribute === "class") { root.classList.toggle("dark", resolved === "dark") } } export type ThemeProviderProps = { children: React.ReactNode attribute?: "class" defaultTheme?: ColorScheme enableSystem?: boolean disableTransitionOnChange?: boolean storageKey?: string forcedTheme?: ColorScheme themes?: ColorScheme[] } export function ThemeProvider({ children, attribute = "class", defaultTheme = "system", enableSystem = true, disableTransitionOnChange = false, storageKey = DEFAULT_STORAGE_KEY, forcedTheme, themes = enableSystem ? ["light", "dark", "system"] : ["light", "dark"], }: ThemeProviderProps) { const [theme, setThemeState] = React.useState(() => readStoredTheme(storageKey, defaultTheme), ) const [systemTheme, setSystemTheme] = React.useState(() => readSystemScheme()) const mounted = React.useSyncExternalStore( () => () => {}, () => true, () => false, ) React.useEffect(() => { const media = window.matchMedia("(prefers-color-scheme: dark)") const onChange = () => setSystemTheme(media.matches ? "dark" : "light") media.addEventListener("change", onChange) return () => media.removeEventListener("change", onChange) }, []) const effectiveTheme = forcedTheme ?? theme const resolvedTheme = resolveScheme(effectiveTheme, systemTheme) const setTheme = React.useCallback( (next: ColorScheme) => { if (!themes.includes(next)) return setThemeState(next) try { localStorage.setItem(storageKey, next) } catch { /* ignore */ } }, [storageKey, themes], ) React.useLayoutEffect(() => { if (!mounted) return applyDocumentScheme(resolvedTheme, { attribute, disableTransitionOnChange }) }, [mounted, resolvedTheme, attribute, disableTransitionOnChange]) const value = React.useMemo( () => ({ theme: effectiveTheme, setTheme, resolvedTheme: mounted ? resolvedTheme : undefined, systemTheme: mounted ? systemTheme : undefined, themes, forcedTheme, }), [effectiveTheme, setTheme, mounted, resolvedTheme, systemTheme, themes, forcedTheme], ) return {children} } export function useTheme(): ColorSchemeContextValue { const ctx = React.useContext(ColorSchemeContext) if (!ctx) { throw new Error("useTheme must be used within ThemeProvider") } return ctx }