import { createContext, useCallback, useContext, useMemo, useEffect, useState, type PropsWithChildren, type CSSProperties, } from "react"; import { UhuruAlertProvider, UhuruAlertViewport, type UhuruAlertConfig, } from "./components/feedback/alert-system"; import { UhuruBackground, type UhuruBackgroundConfig } from "./background"; import { defaultAccentPresets, flattenColorConfig, type UhuruAccentPresetMap } from "./accent-presets"; export type UhuruTheme = "light" | "dark"; export type UhuruDensity = "compact" | "comfortable"; export type UhuruRadiusStyle = "tight" | "balanced" | "soft"; export type UhuruVariant = "default" | "glass"; export type UhuruColorPalette = Partial<{ actionPrimary: string; actionPrimaryHover: string; accentSolid: string; accentStrong: string; accentSubtle: string; bgBase: string; bgMuted: string; bgSubtle: string; borderAccent: string; borderDefault: string; borderDisabled: string; borderStrong: string; borderSubtle: string; errorBg: string; errorBorder: string; errorText: string; infoBg: string; infoBorder: string; infoText: string; successBg: string; successBorder: string; successText: string; surfaceDefault: string; surfaceInverse: string; surfaceMuted: string; surfaceOverlay: string; textDisabled: string; textInverse: string; textPrimary: string; textSecondary: string; textTertiary: string; warningBg: string; warningBorder: string; warningText: string; }>; export type UhuruColorConfig = UhuruColorPalette & { dark?: UhuruColorPalette; light?: UhuruColorPalette; }; export type UhuruProviderProps = PropsWithChildren<{ theme?: UhuruTheme; defaultTheme?: UhuruTheme; density?: UhuruDensity; defaultDensity?: UhuruDensity; colors?: UhuruColorConfig; presetAccent?: string | null; defaultPresetAccent?: string | null; accentPresets?: UhuruAccentPresetMap; radiusStyle?: UhuruRadiusStyle; defaultRadiusStyle?: UhuruRadiusStyle; variant?: UhuruVariant; defaultVariant?: UhuruVariant; className?: string; alertConfig?: UhuruAlertConfig; background?: UhuruBackgroundConfig; onThemeChange?: (theme: UhuruTheme) => void; onDensityChange?: (density: UhuruDensity) => void; onPresetAccentChange?: (presetAccent: string | null) => void; onRadiusStyleChange?: (radiusStyle: UhuruRadiusStyle) => void; }>; export type UhuruThemeContextValue = { colors: UhuruColorConfig; density: UhuruDensity; isDark: boolean; presetAccent: string | null; radiusStyle: UhuruRadiusStyle; setVariant: (variant: UhuruVariant) => void; setDensity: (density: UhuruDensity) => void; setPresetAccent: (presetAccent: string | null) => void; setRadiusStyle: (radiusStyle: UhuruRadiusStyle) => void; setTheme: (theme: UhuruTheme) => void; theme: UhuruTheme; toggleTheme: () => void; variant: UhuruVariant; }; const UhuruThemeContext = createContext(null); export function UhuruProvider({ children, theme, defaultTheme = "light", density, defaultDensity = "compact", colors, presetAccent, defaultPresetAccent = "brown", accentPresets, alertConfig, background, radiusStyle, defaultRadiusStyle = "balanced", variant, defaultVariant = "default", className, onThemeChange, onDensityChange, onPresetAccentChange, onRadiusStyleChange, }: UhuruProviderProps) { const [internalTheme, setInternalTheme] = useState(defaultTheme); const [internalDensity, setInternalDensity] = useState(defaultDensity); const [internalPresetAccent, setInternalPresetAccent] = useState(defaultPresetAccent); const [internalRadiusStyle, setInternalRadiusStyle] = useState(defaultRadiusStyle); const [internalVariant, setInternalVariant] = useState(defaultVariant); const resolvedTheme = theme ?? internalTheme; const resolvedDensity = density ?? internalDensity; const resolvedPresetAccent = presetAccent ?? internalPresetAccent; const resolvedRadiusStyle = radiusStyle ?? internalRadiusStyle; const resolvedVariant = variant ?? internalVariant; const resolvedAccentPresets: UhuruAccentPresetMap = { ...defaultAccentPresets, ...accentPresets, }; const resolvedColors = { ...flattenColorConfig(resolvedPresetAccent ? resolvedAccentPresets[resolvedPresetAccent] : undefined, resolvedTheme), ...flattenColorConfig(colors, resolvedTheme), }; useEffect(() => { if (typeof document === "undefined") { return; } const root = document.documentElement; const previousBackground = root.style.backgroundColor; const previousOverscroll = root.style.overscrollBehaviorY; const backgroundColor = resolvedColors.bgBase ?? (resolvedTheme === "dark" ? "#141210" : "#f7f7f5"); root.style.backgroundColor = backgroundColor; root.style.overscrollBehaviorY = "none"; return () => { root.style.backgroundColor = previousBackground; root.style.overscrollBehaviorY = previousOverscroll; }; }, [resolvedColors.bgBase, resolvedTheme]); const colorStyle = Object.entries(resolvedColors).reduce>( (style, [key, value]) => { if (key === "light" || key === "dark" || typeof value !== "string") { return style; } const variable = key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`); style[`--uhuru-${variable}`] = value; return style; }, {}, ) as CSSProperties; const setTheme = useCallback( (nextTheme: UhuruTheme) => { if (theme === undefined) { setInternalTheme(nextTheme); } onThemeChange?.(nextTheme); }, [onThemeChange, theme], ); const setDensity = useCallback( (nextDensity: UhuruDensity) => { if (density === undefined) { setInternalDensity(nextDensity); } onDensityChange?.(nextDensity); }, [density, onDensityChange], ); const setPresetAccent = useCallback( (nextPresetAccent: string | null) => { if (presetAccent === undefined) { setInternalPresetAccent(nextPresetAccent); } onPresetAccentChange?.(nextPresetAccent); }, [onPresetAccentChange, presetAccent], ); const setRadiusStyle = useCallback( (nextRadiusStyle: UhuruRadiusStyle) => { if (radiusStyle === undefined) { setInternalRadiusStyle(nextRadiusStyle); } onRadiusStyleChange?.(nextRadiusStyle); }, [onRadiusStyleChange, radiusStyle], ); const toggleTheme = useCallback(() => { setTheme(resolvedTheme === "light" ? "dark" : "light"); }, [resolvedTheme, setTheme]); const setVariant = useCallback( (nextVariant: UhuruVariant) => { if (variant === undefined) { setInternalVariant(nextVariant); } }, [variant], ); const value = useMemo( () => ({ colors: resolvedColors, density: resolvedDensity, isDark: resolvedTheme === "dark", presetAccent: resolvedPresetAccent, radiusStyle: resolvedRadiusStyle, setVariant, setDensity, setPresetAccent, setRadiusStyle, setTheme, theme: resolvedTheme, toggleTheme, variant: resolvedVariant, }), [ resolvedColors, resolvedDensity, resolvedPresetAccent, resolvedRadiusStyle, resolvedTheme, resolvedVariant, setDensity, setPresetAccent, setRadiusStyle, setTheme, setVariant, toggleTheme, ], ); return (
{children}
); } export function useUhuruTheme() { const context = useContext(UhuruThemeContext); if (!context) { throw new Error("useUhuruTheme must be used within UhuruProvider."); } return context; }