{"version":3,"file":"ThemeProvider.cjs","names":[],"sources":["../../src/theme/ThemeProvider.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useLatestRef } from \"@/hooks/use-latest-ref\";\nimport { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface ThemeContextValue {\n    /** Raw user preference (light / dark / system). */\n    theme: ThemeMode;\n    /** Effective theme actually applied to the DOM (light or dark). */\n    resolvedTheme: ResolvedTheme;\n    /** Update the preference. Persisted to localStorage when `storageKey` is set. */\n    setTheme: (next: ThemeMode) => void;\n    /** Convenience: flip light ↔ dark. When in `system` mode, switches to the opposite of the current resolved theme. */\n    toggle: () => void;\n}\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null);\n\nexport interface ThemeProviderProps {\n    children: ReactNode;\n    /** Initial preference when nothing is stored. Default: `\"system\"`. */\n    defaultTheme?: ThemeMode;\n    /** localStorage key used to persist the preference. Pass `null` to disable persistence. Default: `\"tempest-theme\"`. */\n    storageKey?: string | null;\n    /**\n     * Element that receives the theme attribute(s). Defaults to\n     * `document.documentElement`. Override when scoping the theme to a subtree.\n     */\n    target?: () => HTMLElement | null;\n    /**\n     * Attribute name(s) written on the target with the resolved theme\n     * (`\"light\"` / `\"dark\"`). Default: `\"data-tempest-theme\"`.\n     *\n     * Pass an array to mirror the theme onto more than one attribute — handy\n     * when the SDK components read `data-tempest-theme` but the host app's own\n     * CSS keys off a different attribute (e.g. `[\"data-tempest-theme\",\n     * \"data-theme\"]`). Avoids a separate sync effect in the consumer.\n     */\n    attribute?: string | string[];\n    /**\n     * When set, keeps `<meta name=\"theme-color\">` in sync with the resolved\n     * theme — `content` becomes `themeColor.dark` in dark mode and\n     * `themeColor.light` in light mode. The meta tag must already exist in the\n     * document `<head>`. No-op when omitted.\n     */\n    themeColor?: { light: string; dark: string };\n}\n\nfunction resolve(mode: ThemeMode): ResolvedTheme {\n    if (mode === \"dark\" || mode === \"light\") return mode;\n    if (typeof window === \"undefined\") return \"light\";\n    return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * Write the resolved theme onto every configured attribute and, when a\n * `themeColor` map is provided, sync the `<meta name=\"theme-color\">` tag.\n */\nfunction applyResolved(\n    element: HTMLElement,\n    resolved: ResolvedTheme,\n    attribute: string | string[],\n    themeColor?: { light: string; dark: string },\n): void {\n    const attrs = Array.isArray(attribute) ? attribute : [attribute];\n    for (const attr of attrs) element.setAttribute(attr, resolved);\n    if (themeColor && typeof document !== \"undefined\") {\n        const meta = document.querySelector<HTMLMetaElement>('meta[name=\"theme-color\"]');\n        if (meta) meta.content = themeColor[resolved];\n    }\n}\n\nfunction readStored(storageKey: string | null): ThemeMode | null {\n    if (!storageKey) return null;\n    const value = storage.getRaw(storageKey);\n    if (value === \"light\" || value === \"dark\" || value === \"system\") return value;\n    return null;\n}\n\n/**\n * Wire dark/light theming. Writes a data attribute on a target element (the\n * `<html>` element by default) and exposes the current preference via\n * {@link useTheme}.\n *\n * Pair with `themeInitScript()` in the HTML head to prevent the flash of\n * incorrect theme on first paint.\n *\n * @tempest-limits empty-catch — the theme is applied to the DOM before it is\n * persisted, so a `localStorage` write refused by quota or private mode leaves the\n * user with the theme they just picked and only forfeits it on the next load.\n * Throwing from the setter would break the switch that already worked.\n */\nexport function ThemeProvider({\n    children,\n    defaultTheme = \"system\",\n    storageKey = \"tempest-theme\",\n    target,\n    attribute = \"data-tempest-theme\",\n    themeColor,\n}: ThemeProviderProps) {\n    const [theme, setThemeState] = useState<ThemeMode>(\n        () => readStored(storageKey) ?? defaultTheme,\n    );\n    const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(() => resolve(theme));\n\n    const targetRef = useLatestRef(target);\n\n    const attributeKey = Array.isArray(attribute) ? attribute.join(\",\") : attribute;\n    const themeColorRef = useLatestRef(themeColor);\n\n    useEffect(() => {\n        const element = targetRef.current?.() ?? document.documentElement;\n        if (!element) return;\n        const next = resolve(theme);\n        applyResolved(element, next, attribute, themeColorRef.current);\n        setResolvedTheme(next);\n        // attributeKey is the stable string form of `attribute`.\n        // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [theme, attributeKey]);\n\n    useEffect(() => {\n        if (theme !== \"system\" || typeof window === \"undefined\") return;\n        const list = window.matchMedia(\"(prefers-color-scheme: dark)\");\n        const handler = (): void => {\n            const element = targetRef.current?.() ?? document.documentElement;\n            if (!element) return;\n            const next: ResolvedTheme = list.matches ? \"dark\" : \"light\";\n            applyResolved(element, next, attribute, themeColorRef.current);\n            setResolvedTheme(next);\n        };\n        list.addEventListener(\"change\", handler);\n        return () => list.removeEventListener(\"change\", handler);\n        // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [theme, attributeKey]);\n\n    const setTheme = useCallback(\n        (next: ThemeMode) => {\n            setThemeState(next);\n            if (storageKey) storage.setRaw(storageKey, next);\n        },\n        [storageKey],\n    );\n\n    const toggle = useCallback(() => {\n        setTheme(resolvedTheme === \"dark\" ? \"light\" : \"dark\");\n    }, [resolvedTheme, setTheme]);\n\n    const value = useMemo<ThemeContextValue>(\n        () => ({ theme, resolvedTheme, setTheme, toggle }),\n        [theme, resolvedTheme, setTheme, toggle],\n    );\n\n    return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\n}\n\n/**\n * Read and mutate the current theme. Must be used inside a {@link ThemeProvider}.\n */\nexport function useTheme(): ThemeContextValue {\n    const ctx = useContext(ThemeContext);\n    if (!ctx) throw new Error(\"useTheme must be used inside a <ThemeProvider>\");\n    return ctx;\n}\n"],"mappings":"uIAiBA,IAAM,GAAA,EAAe,EAAA,cAAA,CAAwC,IAAI,EAgCjE,SAAS,EAAQ,EAAgC,CAG7C,OAFI,IAAS,QAAU,IAAS,QAAgB,EAC5C,OAAO,OAAW,IAAoB,QACnC,OAAO,WAAW,8BAA8B,CAAC,CAAC,QAAU,OAAS,OAChF,CAMA,SAAS,EACL,EACA,EACA,EACA,EACI,CACJ,IAAM,EAAQ,MAAM,QAAQ,CAAS,EAAI,EAAY,CAAC,CAAS,EAC/D,IAAK,IAAM,KAAQ,EAAO,EAAQ,aAAa,EAAM,CAAQ,EAC7D,GAAI,GAAc,OAAO,SAAa,IAAa,CAC/C,IAAM,EAAO,SAAS,cAA+B,0BAA0B,EAC3E,IAAM,EAAK,QAAU,EAAW,GACxC,CACJ,CAEA,SAAS,EAAW,EAA6C,CAC7D,GAAI,CAAC,EAAY,OAAO,KACxB,IAAM,EAAQ,EAAA,QAAQ,OAAO,CAAU,EAEvC,OADI,IAAU,SAAW,IAAU,QAAU,IAAU,SAAiB,EACjE,IACX,CAeA,SAAgB,EAAc,CAC1B,WACA,eAAe,SACf,aAAa,gBACb,SACA,YAAY,qBACZ,cACmB,CACnB,GAAM,CAAC,EAAO,IAAA,EAAiB,EAAA,SAAA,KACrB,EAAW,CAAU,GAAK,CACpC,EACM,CAAC,EAAe,IAAA,EAAoB,EAAA,SAAA,KAA8B,EAAQ,CAAK,CAAC,EAEhF,EAAY,EAAA,aAAa,CAAM,EAE/B,EAAe,MAAM,QAAQ,CAAS,EAAI,EAAU,KAAK,GAAG,EAAI,EAChE,EAAgB,EAAA,aAAa,CAAU,GAE7C,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAU,EAAU,UAAU,GAAK,SAAS,gBAClD,GAAI,CAAC,EAAS,OACd,IAAM,EAAO,EAAQ,CAAK,EAC1B,EAAc,EAAS,EAAM,EAAW,EAAc,OAAO,EAC7D,EAAiB,CAAI,CAGzB,EAAG,CAAC,EAAO,CAAY,CAAC,GAExB,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,IAAU,UAAY,OAAO,OAAW,IAAa,OACzD,IAAM,EAAO,OAAO,WAAW,8BAA8B,EACvD,MAAsB,CACxB,IAAM,EAAU,EAAU,UAAU,GAAK,SAAS,gBAClD,GAAI,CAAC,EAAS,OACd,IAAM,EAAsB,EAAK,QAAU,OAAS,QACpD,EAAc,EAAS,EAAM,EAAW,EAAc,OAAO,EAC7D,EAAiB,CAAI,CACzB,EAEA,OADA,EAAK,iBAAiB,SAAU,CAAO,MAC1B,EAAK,oBAAoB,SAAU,CAAO,CAE3D,EAAG,CAAC,EAAO,CAAY,CAAC,EAExB,IAAM,GAAA,EAAW,EAAA,YAAA,CACZ,GAAoB,CACjB,EAAc,CAAI,EACd,GAAY,EAAA,QAAQ,OAAO,EAAY,CAAI,CACnD,EACA,CAAC,CAAU,CACf,EAEM,GAAA,EAAS,EAAA,YAAA,KAAkB,CAC7B,EAAS,IAAkB,OAAS,QAAU,MAAM,CACxD,EAAG,CAAC,EAAe,CAAQ,CAAC,EAEtB,GAAA,EAAQ,EAAA,QAAA,MACH,CAAE,QAAO,gBAAe,WAAU,QAAO,GAChD,CAAC,EAAO,EAAe,EAAU,CAAM,CAC3C,EAEA,OAAO,EAAA,EAAA,IAAA,CAAC,EAAa,SAAd,CAA8B,QAAQ,UAAgC,CAAA,CACjF,CAKA,SAAgB,GAA8B,CAC1C,IAAM,GAAA,EAAM,EAAA,WAAA,CAAW,CAAY,EACnC,GAAI,CAAC,EAAK,MAAU,MAAM,gDAAgD,EAC1E,OAAO,CACX"}