/** * Theme Context + Provider (React integration) * ============================================= * Wraps the registry in a React context so that the entire component tree * re-renders when the active theme changes. */ import React, { createContext, useContext, useEffect, useSyncExternalStore, useCallback, useMemo, } from "react"; import type { ThemePackage, ComponentKey, LayoutKey, PageKey } from "./types"; import { subscribe, getActiveTheme, getActiveThemeId, getRegisteredThemes, activateTheme, deactivateTheme, reapplyActiveTheme, resolveComponent, resolveLayout, resolvePage, getSavedMode, saveMode, getEffectiveMode, registerTheme, unregisterTheme, type ThemeMode, } from "./registry"; // ─── context value ─────────────────────────────────────────────────────────── interface ThemeContextValue { /** Currently active theme package (null = default). */ activeTheme: ThemePackage | null; activeThemeId: string | null; /** All registered themes. */ themes: ThemePackage[]; /** Current mode preference. */ mode: ThemeMode; /** Resolved effective mode (never "system"). */ effectiveMode: "light" | "dark"; /** Switch the active theme. */ switchTheme: (id: string) => void; /** Reset to no custom theme (use defaults). */ resetTheme: () => void; /** Change mode preference. */ setMode: (mode: ThemeMode) => void; /** Register a new theme at runtime. */ register: (pkg: ThemePackage) => void; /** Unregister a theme by id. */ unregister: (id: string) => void; /** Resolve a component (returns themed override or fallback). */ component:

(key: ComponentKey, fallback: React.ComponentType

) => React.ComponentType

; /** Resolve a layout. */ layout: (key: LayoutKey, fallback: React.ComponentType<{ children: React.ReactNode }>) => React.ComponentType<{ children: React.ReactNode }>; /** Resolve a page. */ page:

(key: PageKey, fallback: React.ComponentType

) => React.ComponentType

; } const ThemeContext = createContext(null); // ─── snapshot for useSyncExternalStore ──────────────────────────────────────── // We use a monotonic counter to create new snapshot references when the // registry notifies. let snapshotCounter = 0; function getSnapshot() { return snapshotCounter; } const originalSubscribe = (onStoreChange: () => void) => { const unsub = subscribe(() => { snapshotCounter++; onStoreChange(); }); return unsub; }; // ─── provider ──────────────────────────────────────────────────────────────── interface ThemeProviderProps { children: React.ReactNode; } export const ThemeProvider: React.FC = ({ children }) => { // Re-render whenever register changes useSyncExternalStore(originalSubscribe, getSnapshot); // Listen for system mode changes useEffect(() => { const mq = window.matchMedia("(prefers-color-scheme: dark)"); const handler = () => reapplyActiveTheme(); mq.addEventListener("change", handler); return () => mq.removeEventListener("change", handler); }, []); const switchTheme = useCallback((id: string) => activateTheme(id), []); const resetTheme = useCallback(() => { deactivateTheme(); reapplyActiveTheme(); localStorage.removeItem("flvx:active-theme"); }, []); const setMode = useCallback((m: ThemeMode) => { saveMode(m); reapplyActiveTheme(); }, []); const register = useCallback((pkg: ThemePackage) => registerTheme(pkg), []); const unregister = useCallback((id: string) => unregisterTheme(id), []); const value = useMemo(() => ({ activeTheme: getActiveTheme(), activeThemeId: getActiveThemeId(), themes: getRegisteredThemes(), mode: getSavedMode(), effectiveMode: getEffectiveMode(), switchTheme, resetTheme, setMode, register, unregister, component: resolveComponent, layout: resolveLayout, page: resolvePage, // eslint-disable-next-line react-hooks/exhaustive-deps }), [snapshotCounter, switchTheme, resetTheme, setMode, register, unregister]); return ( {children} ); }; // ─── hooks ─────────────────────────────────────────────────────────────────── /** Access the full theme context. */ export function useThemeContext(): ThemeContextValue { const ctx = useContext(ThemeContext); if (!ctx) throw new Error("useThemeContext must be used within "); return ctx; } /** * Convenience: resolve a single themed component. * * ```tsx * import { useThemedComponent } from "@/themes/context"; * import { Button as DefaultButton } from "@/shadcn-bridge/heroui/button"; * * function MyPage() { * const Button = useThemedComponent("Button", DefaultButton); * return ; * } * ``` */ export function useThemedComponent

( key: ComponentKey, fallback: React.ComponentType

, ): React.ComponentType

{ const ctx = useThemeContext(); return ctx.component(key, fallback); } /** Convenience: resolve a themed layout. */ export function useThemedLayout( key: LayoutKey, fallback: React.ComponentType<{ children: React.ReactNode }>, ): React.ComponentType<{ children: React.ReactNode }> { const ctx = useThemeContext(); return ctx.layout(key, fallback); } /** Convenience: resolve a themed page. */ export function useThemedPage

( key: PageKey, fallback: React.ComponentType

, ): React.ComponentType

{ const ctx = useThemeContext(); return ctx.page(key, fallback); }