'use client' import type { Dispatch, PropsWithChildren, ScriptHTMLAttributes, SetStateAction } from 'react' import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { useServerInsertedHTML } from 'next/navigation' type SystemTheme = 'dark' | 'light' type DataAttribute = `data-${string}` type Attribute = DataAttribute | 'class' type ThemeAttribute = Attribute | Attribute[] type ValueObject = Record interface ScriptProps extends ScriptHTMLAttributes { [dataAttribute: DataAttribute]: unknown } export interface UseThemeProps { themes: string[] forcedTheme?: string setTheme: Dispatch> theme?: string resolvedTheme?: string systemTheme?: SystemTheme } export interface ThemeProviderProps extends PropsWithChildren { themes?: string[] forcedTheme?: string enableSystem?: boolean disableTransitionOnChange?: boolean enableColorScheme?: boolean storageKey?: string defaultTheme?: string attribute?: ThemeAttribute value?: ValueObject nonce?: string scriptProps?: ScriptProps } const DEFAULT_THEMES = ['light', 'dark'] const COLOR_SCHEME_THEMES = ['light', 'dark'] const SYSTEM_THEME_MEDIA = '(prefers-color-scheme: dark)' const fallbackThemeContext: UseThemeProps = { setTheme: () => {}, themes: [] } const ThemeContext = createContext(undefined) export function useTheme() { return useContext(ThemeContext) ?? fallbackThemeContext } export function ThemeProvider(props: ThemeProviderProps) { const context = useContext(ThemeContext) if (context) { return <>{props.children} } return } function ThemeProviderInner({ attribute = 'data-theme', children, defaultTheme, disableTransitionOnChange = false, enableColorScheme = true, enableSystem = true, forcedTheme, nonce, scriptProps, storageKey = 'theme', themes = DEFAULT_THEMES, value }: ThemeProviderProps) { const resolvedDefaultTheme = defaultTheme ?? (enableSystem ? 'system' : 'light') const [theme, setThemeState] = useState( resolvedDefaultTheme ) const [systemTheme, setSystemTheme] = useState() const hasSyncedTheme = useRef(false) const hasInsertedThemeScript = useRef(false) const themeRef = useRef(resolvedDefaultTheme) const applyThemeWithConfig = useCallback( (nextTheme?: string) => { applyTheme(nextTheme, { attribute, defaultTheme: resolvedDefaultTheme, disableTransitionOnChange, enableColorScheme, enableSystem, nonce, themes, value }) }, [ attribute, disableTransitionOnChange, enableColorScheme, enableSystem, nonce, resolvedDefaultTheme, themes, value ] ) const setTheme = useCallback>>( nextThemeOrUpdater => { const nextTheme = typeof nextThemeOrUpdater === 'function' ? nextThemeOrUpdater(themeRef.current ?? resolvedDefaultTheme) : nextThemeOrUpdater themeRef.current = nextTheme setThemeState(nextTheme) try { window.localStorage.setItem(storageKey, nextTheme) } catch {} if (hasSyncedTheme.current) { applyThemeWithConfig(forcedTheme ?? nextTheme) } }, [applyThemeWithConfig, forcedTheme, resolvedDefaultTheme, storageKey] ) useServerInsertedHTML(() => { if (hasInsertedThemeScript.current) { return null } hasInsertedThemeScript.current = true const scriptNonce = nonce ?? scriptProps?.nonce return (