"use client"; import { createContext, useContext, useState, useEffect, startTransition, type ReactNode } from "react"; import { resolveIncomingThemeMode, resolveStoredTheme, type ResolvedTheme } from "./theme-sync"; export type Theme = "light" | "dark" | "system"; interface ThemeContextType { theme: Theme; setTheme: (t: Theme) => void; resolved: ResolvedTheme; isEmbedded: boolean; } function getPrefersDark() { if (typeof window === "undefined") { return true; } return window.matchMedia("(prefers-color-scheme: dark)").matches; } function getIsEmbedded() { if (typeof window === "undefined") { return false; } try { return window.self !== window.top; } catch { return true; } } function getParentResolvedTheme(): ResolvedTheme | null { if (!getIsEmbedded()) { return null; } try { const parentRoot = window.parent.document.documentElement; const parentBody = window.parent.document.body; if (parentBody?.getAttribute("theme-mode") === "dark" || parentRoot.classList.contains("dark")) { return "dark"; } return "light"; } catch { return null; } } const ThemeContext = createContext({ theme: "system", setTheme: () => {}, resolved: "dark", isEmbedded: false, }); export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setThemeState] = useState("system"); const [isEmbedded, setIsEmbedded] = useState(getIsEmbedded); const [parentResolved, setParentResolved] = useState(getParentResolvedTheme); const [resolved, setResolved] = useState( () => getParentResolvedTheme() ?? resolveStoredTheme("system", getPrefersDark()), ); useEffect(() => { const saved = localStorage.getItem("theme") as Theme | null; if (saved && ["light", "dark", "system"].includes(saved)) { startTransition(() => { setThemeState(saved); }); } }, []); useEffect(() => { const nextIsEmbedded = getIsEmbedded(); const nextParentResolved = getParentResolvedTheme(); startTransition(() => { setIsEmbedded(nextIsEmbedded); setParentResolved((current) => current ?? nextParentResolved); }); }, []); useEffect(() => { if (!isEmbedded) { return; } const handleMessage = (event: MessageEvent) => { if (event.source !== window.parent) { return; } const nextResolved = resolveIncomingThemeMode( event.data?.themeMode, window.matchMedia("(prefers-color-scheme: dark)").matches, ); if (nextResolved) { setParentResolved(nextResolved); } }; window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); }, [isEmbedded]); useEffect(() => { const mq = window.matchMedia("(prefers-color-scheme: dark)"); const root = document.documentElement; const applyResolved = (nextResolved: ResolvedTheme) => { setResolved(nextResolved); root.classList.remove("light", "dark"); root.classList.add(nextResolved); root.setAttribute("data-theme", nextResolved); }; applyResolved(parentResolved ?? resolveStoredTheme(theme, mq.matches)); if (parentResolved !== null || theme !== "system") { return; } const handleSystemThemeChange = (event: MediaQueryListEvent) => { applyResolved(resolveStoredTheme(theme, event.matches)); }; mq.addEventListener("change", handleSystemThemeChange); return () => mq.removeEventListener("change", handleSystemThemeChange); }, [parentResolved, theme]); const setTheme = (t: Theme) => { setThemeState(t); localStorage.setItem("theme", t); }; return ( {children} ); } export function useTheme() { return useContext(ThemeContext); }