UNPKG

theme-o-rama

Version:

A TypeScript library for dynamic theme management in react + shadcn + tailwind applications

168 lines (167 loc) 6.73 kB
"use client"; import { jsx as _jsx } from "react/jsx-runtime"; import { createContext, useContext, useEffect, useRef, useState } from "react"; import colorTheme from "./color.json" with { type: "json" }; import darkTheme from "./dark.json" with { type: "json" }; import { applyTheme } from "./index.js"; import lightTheme from "./light.json" with { type: "json" }; import { ThemeLoader } from "./theme-loader.js"; // Browser detection for SSR compatibility const isBrowser = typeof window !== "undefined"; const ThemeContext = createContext(undefined); export function ThemeProvider({ children, discoverThemes = async () => [], imageResolver, defaultTheme = "light", onThemeChange, }) { const [currentTheme, setCurrentTheme] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isSettingTheme, setIsSettingTheme] = useState(false); const [error, setError] = useState(null); // Use refs for stable instances that don't need to trigger re-renders const themeLoader = useRef(new ThemeLoader()).current; // Store callbacks and functions in refs to avoid re-running effects when they change const onThemeChangeRef = useRef(onThemeChange); const imageResolverRef = useRef(imageResolver); const discoverThemesRef = useRef(discoverThemes); useEffect(() => { onThemeChangeRef.current = onThemeChange; imageResolverRef.current = imageResolver; discoverThemesRef.current = discoverThemes; }, [onThemeChange, imageResolver, discoverThemes]); const setTheme = async (themeName) => { if (isSettingTheme) return; // Prevent concurrent calls setIsSettingTheme(true); try { const theme = themeLoader.getTheme(themeName); setCurrentTheme(theme); if (isBrowser) { applyTheme(theme, document.documentElement); } // Notify app of theme change (app handles storage) if (onThemeChangeRef.current) { onThemeChangeRef.current(themeName); } setError(null); // Clear any previous errors } catch (err) { console.error("Error setting theme:", err); setError("Failed to set theme"); } finally { setIsSettingTheme(false); } }; const setCustomTheme = async (themeJson) => { if (isSettingTheme) return false; // Prevent concurrent calls setIsSettingTheme(true); try { if (themeJson) { const theme = await themeLoader.loadThemeFromJson(themeJson, imageResolver); if (theme) { setCurrentTheme(theme); if (isBrowser) { applyTheme(theme, document.documentElement); } // Notify app of theme change (app handles storage) if (onThemeChangeRef.current) { onThemeChangeRef.current(theme.name); } setError(null); // Clear any previous errors return true; } } } catch (err) { console.error("Error setting custom theme:", err); setError("Failed to load custom theme"); return false; } finally { setIsSettingTheme(false); } setError("Invalid theme JSON"); return false; }; const reloadThemes = async () => { try { setIsLoading(true); setError(null); themeLoader.clearCache(); // Always load built-in themes await themeLoader.loadTheme(lightTheme); await themeLoader.loadTheme(darkTheme); await themeLoader.loadTheme(colorTheme); // Load additional themes if discovery function provided if (discoverThemesRef.current) { const appThemes = await discoverThemesRef.current(); await themeLoader.loadThemes(appThemes, imageResolverRef.current); } const theme = themeLoader.getTheme(defaultTheme); setCurrentTheme(theme); if (isBrowser) { applyTheme(theme, document.documentElement); } } catch (err) { console.error("Error reloading themes:", err); setError("Failed to reload themes"); setCurrentTheme(null); } finally { setIsLoading(false); } }; useEffect(() => { const initializeThemes = async () => { try { setIsLoading(true); setError(null); // Always load built-in themes await themeLoader.loadTheme(lightTheme); await themeLoader.loadTheme(darkTheme); await themeLoader.loadTheme(colorTheme); // Load additional themes if discovery function provided if (discoverThemesRef.current) { const appThemes = await discoverThemesRef.current(); await themeLoader.loadThemes(appThemes, imageResolverRef.current); } // Set initial theme after loading (use defaultTheme prop) const initialTheme = themeLoader.getTheme(defaultTheme); setCurrentTheme(initialTheme); if (isBrowser) { applyTheme(initialTheme, document.documentElement); } } catch (err) { console.error("Error loading themes:", err); setError("Failed to load themes"); // Don't set a fallback theme - let CSS defaults handle it setCurrentTheme(null); } finally { setIsLoading(false); } }; initializeThemes(); // Only re-run when defaultTheme changes, not when functions change }, [defaultTheme]); const initializeTheme = async (theme) => { return await themeLoader.initializeTheme(theme, imageResolverRef.current); }; return (_jsx(ThemeContext.Provider, { value: { currentTheme, setTheme, setCustomTheme, availableThemes: themeLoader.getThemes(), isLoading, error, reloadThemes, initializeTheme, }, children: children })); } export function useTheme() { const context = useContext(ThemeContext); if (context === undefined) { throw new Error("useTheme must be used within a ThemeProvider"); } return context; }