import * as React from "react"; import { useEffect, useMemo, useRef } from "react"; export type ThemeProps = { headerFont?: string; bodyFont?: string; primaryColor?: string; primaryForegroundColor?: string; secondaryColor?: string; accentColor?: string; bgColor?: string; fgColor?: string; mutedColor?: string; mutedForegroundColor?: string; borderColor?: string; radius?: "none" | "sm" | "md" | "lg" | "xl"; buttonRadius?: "none" | "sm" | "md" | "lg" | "xl"; shadow?: "none" | "sm" | "md" | "lg" | "xl"; maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full"; }; const radiusMap: Record, string> = { none: "0px", sm: "0.25rem", md: "0.5rem", lg: "0.75rem", xl: "1rem", }; const buttonRadiusMap: Record, string> = { none: "0px", sm: "0.25rem", md: "0.5rem", lg: "0.75rem", xl: "1rem", }; const shadowMap: Record, string> = { none: "0 0 #0000", sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)", md: "0 4px 6px -1px rgb(0 0 0 / 0.10), 0 2px 4px -2px rgb(0 0 0 / 0.10)", lg: "0 10px 15px -3px rgb(0 0 0 / 0.10), 0 4px 6px -4px rgb(0 0 0 / 0.10)", xl: "0 20px 25px -5px rgb(0 0 0 / 0.10), 0 8px 10px -6px rgb(0 0 0 / 0.10)", }; function googleFontsHref(headerFont?: string, bodyFont?: string): string | null { const fonts = [headerFont, bodyFont].filter( (f): f is string => !!f && f !== "system-ui" ); if (fonts.length === 0) return null; const families = Array.from(new Set(fonts)) .map((f) => `family=${encodeURIComponent(f)}:wght@400;500;600;700`) .join("&"); return `https://fonts.googleapis.com/css2?${families}&display=swap`; } export function ThemeProvider({ headerFont, bodyFont, primaryColor, primaryForegroundColor, secondaryColor, accentColor, bgColor, fgColor, mutedColor, mutedForegroundColor, borderColor, radius, buttonRadius, shadow, children, }: ThemeProps & { children?: React.ReactNode }) { // Recompute CSS-variable map only when a relevant prop changes. const cssVars = useMemo>(() => { const vars: Record = {}; if (primaryColor) vars["--primary"] = primaryColor; if (primaryForegroundColor) vars["--primary-foreground"] = primaryForegroundColor; if (secondaryColor) vars["--secondary"] = secondaryColor; if (accentColor) vars["--accent"] = accentColor; if (bgColor) vars["--background"] = bgColor; if (fgColor) vars["--foreground"] = fgColor; if (mutedColor) vars["--muted"] = mutedColor; if (mutedForegroundColor) vars["--muted-foreground"] = mutedForegroundColor; if (borderColor) vars["--border"] = borderColor; if (radius) vars["--radius"] = radiusMap[radius]; if (buttonRadius) vars["--button-radius"] = buttonRadiusMap[buttonRadius]; if (shadow) vars["--shadow"] = shadowMap[shadow]; if (headerFont) vars["--font-header"] = `"${headerFont}", system-ui, sans-serif`; if (bodyFont) vars["--font-body"] = `"${bodyFont}", system-ui, sans-serif`; return vars; }, [ headerFont, bodyFont, primaryColor, primaryForegroundColor, secondaryColor, accentColor, bgColor, fgColor, mutedColor, mutedForegroundColor, borderColor, radius, buttonRadius, shadow, ]); // Imperatively push every CSS var onto :root inside the host document // (which is the iframe's document for the editor preview, and the page // for the published render). This guarantees descendants pick up // updates even if React's style-prop diffing missed something or the // base-layer rules need access via :root inheritance. const rootRef = useRef(null); useEffect(() => { const doc = rootRef.current?.ownerDocument ?? (typeof document !== "undefined" ? document : null); if (!doc) return; const target = doc.documentElement; const previous: Record = {}; for (const [key, value] of Object.entries(cssVars)) { previous[key] = target.style.getPropertyValue(key); target.style.setProperty(key, value); } return () => { // Restore prior values so unmount doesn't leak our overrides. for (const [key, value] of Object.entries(previous)) { if (value) target.style.setProperty(key, value); else target.style.removeProperty(key); } }; }, [cssVars]); const fontsHref = useMemo( () => googleFontsHref(headerFont, bodyFont), [headerFont, bodyFont], ); // Plain CSS rules — applied directly, no Tailwind CDN runtime needed. // Tailwind preflight resets h1..h6 to font-family: inherit, which would // make headings pick up the body font. We override that here using the // `--font-header` CSS var ThemeProvider sets per-page. const css = ` body, p, span, a, li, button, input, textarea, select { font-family: var(--font-body), system-ui, -apple-system, sans-serif; } h1, h2, h3, h4, h5, h6 { font-family: var(--font-header), system-ui, -apple-system, sans-serif; } `; // Tailwind theme directives — only useful if/when the CDN compiles // font-heading / font-body utilities. The plain CSS above handles the // common case so headers always render with the right font. const tailwindCss = ` @theme { --font-family-heading: var(--font-header), system-ui, -apple-system, sans-serif; --font-family-body: var(--font-body), system-ui, -apple-system, sans-serif; } `; return ( <> {fontsHref ? : null}