import { siteSettings } from "@/lib/services/site-settings"; import { themePaletteCss } from "@/lib/theme-css"; import { FONTS, PRESETS, THEME_COLOR_KEYS, type ThemePalette } from "@/lib/theme-presets"; import { derivePublicForegrounds, readableColor } from "@/lib/theme-contrast"; // Injects the DB-driven CSS custom properties into :root, exactly like // AtomCMS's app.blade.php. Falls back to the atom defaults when no DB. Covers // colours, typography, buttons, links + an optional raw custom-CSS block. export async function ThemeVars() { const g = (k: string, d: string) => siteSettings.get(k, d); const [ primary, background, surface, dropdown, navbar, navbarText, text, textMuted, accent, success, warning, error, info, buttonColor, buttonText, buttonSecondary, buttonSecondaryText, buttonDanger, buttonDangerText, buttonOutline, buttonOutlineText, linkColor, linkHover, borderColor, gradientFrom, gradientTo, borderRadius, fontKey, h1, h2, h3, customCss, ] = await Promise.all([ g("color_primary", "#f59e0b"), g("color_background", "#f8fafc"), g("color_surface", "#ffffff"), g("color_dropdown", "#ffffff"), g("color_navbar", "#ffffff"), g("color_navbar_text", "#1e293b"), g("color_text", "#0f172a"), g("color_text_muted", "#64748b"), g("color_accent", "#10b981"), g("color_success", "#16a34a"), g("color_warning", "#eab308"), g("color_error", "#ef4444"), g("color_info", "#0ea5e9"), g("button_primary_color", "#f59e0b"), g("button_text_color", "#1e293b"), g("button_secondary_color", "#22c55e"), g("button_secondary_text_color", "#ffffff"), g("button_danger_color", "#ef4444"), g("button_danger_text_color", "#ffffff"), g("button_outline_color", "#eeb425"), g("button_outline_text_color", "#1a1a2e"), g("link_color", "#eeb425"), g("link_hover_color", "#cf9d15"), g("border_color", "#eeb425"), g("gradient_from", "#f59e0b"), g("gradient_to", "#10b981"), g("border_radius", "12"), g("font_family", "nunito"), g("size_heading_h1", "30"), g("size_heading_h2", "22"), g("size_heading_h3", "18"), g("custom_css", ""), ]); // Only hex/keyword color values are admin-controlled — sanitize defensively. const safe = (v: string | null, d: string) => (v && /^[#a-zA-Z0-9(),.\s%-]+$/.test(v) ? v : d); const px = (v: string | null, d: string) => (/^\d{1,3}$/.test(v ?? "") ? (v as string) : d); const safeBackground = safe(background, "#f8fafc"); const safeSurface = safe(surface, "#ffffff"); const publicBackgrounds = [safeBackground, safeSurface]; const readableText = readableColor(safe(text, "#0f172a"), publicBackgrounds); const readableMuted = readableColor(safe(textMuted, "#64748b"), publicBackgrounds); const readablePrimary = readableColor(safe(primary, "#f59e0b"), publicBackgrounds); const readableAccent = readableColor(safe(accent, "#10b981"), publicBackgrounds); const readableLink = readableColor(safe(linkColor, "#eeb425"), publicBackgrounds); const readableLinkHover = readableColor(safe(linkHover, "#cf9d15"), publicBackgrounds); const readableButtonText = readableColor( safe(buttonText, "#1e293b"), [safe(buttonColor, "#f59e0b")], ); const readableSecondaryText = readableColor( safe(buttonSecondaryText, "#ffffff"), [safe(buttonSecondary, "#22c55e")], ); const readableDangerText = readableColor( safe(buttonDangerText, "#ffffff"), [safe(buttonDanger, "#ef4444")], ); const readableNavbarText = readableColor( safe(navbarText, "#1e293b"), [safe(navbar, "#ffffff")], ); const readablePrimaryForeground = readableColor( safe(buttonText, "#1e293b"), [safe(primary, "#f59e0b")], ); const readableAccentForeground = readableColor("#ffffff", [safe(accent, "#10b981")]); const semantic = derivePublicForegrounds({ color_background: safeBackground, color_surface: safeSurface, color_dropdown: safe(dropdown, "#ffffff"), color_navbar: safe(navbar, "#ffffff"), color_navbar_text: safe(navbarText, "#1e293b"), color_text: safe(text, "#0f172a"), color_text_muted: safe(textMuted, "#64748b"), color_primary: safe(primary, "#f59e0b"), color_accent: safe(accent, "#10b981"), button_primary_color: safe(buttonColor, "#f59e0b"), button_text_color: safe(buttonText, "#1e293b"), button_secondary_color: safe(buttonSecondary, "#22c55e"), button_secondary_text_color: safe(buttonSecondaryText, "#ffffff"), button_danger_color: safe(buttonDanger, "#ef4444"), button_danger_text_color: safe(buttonDangerText, "#ffffff"), button_outline_color: safe(buttonOutline, "#eeb425"), button_outline_text_color: safe(buttonOutlineText, "#1a1a2e"), link_color: safe(linkColor, "#eeb425"), link_hover_color: safe(linkHover, "#cf9d15"), }); const font = FONTS[fontKey ?? "nunito"] ?? FONTS.nunito; const presetName = (await g("theme_preset", "Atom (golden)")) ?? "Atom (golden)"; const preset = PRESETS[presetName] ?? PRESETS["Atom (golden)"]; const darkPalette = Object.fromEntries( await Promise.all( THEME_COLOR_KEYS.map(async (key) => [ key, safe(await g(`${key}_dark`, preset.dark[key]), preset.dark[key]), ] as const), ), ) as ThemePalette; const darkCss = themePaletteCss("html.dark", darkPalette); const css = `:root{ --color-primary:${safe(primary, "#f59e0b")}; --color-background:${safeBackground}; --color-surface:${safeSurface}; --color-dropdown:${safe(dropdown, "#ffffff")}; --color-navbar:${safe(navbar, "#ffffff")}; --color-navbar-text:${safe(navbarText, "#1e293b")}; --color-text:${safe(text, "#0f172a")}; --color-text-muted:${safe(textMuted, "#64748b")}; --color-accent:${safe(accent, "#10b981")}; --color-success:${safe(success, "#16a34a")}; --color-warning:${safe(warning, "#eab308")}; --color-error:${safe(error, "#ef4444")}; --color-info:${safe(info, "#0ea5e9")}; --button-color:${safe(buttonColor, "#f59e0b")}; --button-text-color:${safe(buttonText, "#1e293b")}; --button-secondary-color:${safe(buttonSecondary, "#22c55e")}; --button-secondary-text-color:${safe(buttonSecondaryText, "#ffffff")}; --button-danger-color:${safe(buttonDanger, "#ef4444")}; --button-danger-text-color:${safe(buttonDangerText, "#ffffff")}; --button-outline-color:${safe(buttonOutline, "#eeb425")}; --button-outline-text-color:${safe(buttonOutlineText, "#1a1a2e")}; --link-color:${safe(linkColor, "#eeb425")}; --link-hover-color:${safe(linkHover, "#cf9d15")}; --border-color:${safe(borderColor, "#eeb425")}; --gradient-from:${safe(gradientFrom, "#f59e0b")}; --gradient-to:${safe(gradientTo, "#10b981")}; --color-text-readable:${readableText}; --color-text-muted-readable:${readableMuted}; --color-primary-readable:${readablePrimary}; --color-accent-readable:${readableAccent}; --color-primary-foreground-readable:${readablePrimaryForeground}; --color-accent-foreground-readable:${readableAccentForeground}; --color-navbar-text-readable:${readableNavbarText}; --button-text-color-readable:${readableButtonText}; --button-secondary-text-color-readable:${readableSecondaryText}; --button-danger-text-color-readable:${readableDangerText}; --link-color-readable:${readableLink}; --link-hover-color-readable:${readableLinkHover}; --color-surface-text-readable:${semantic.byName.surface}; --color-dropdown-text-readable:${semantic.byName.dropdown}; --button-outline-text-color-readable:${semantic.byName["button-outline"]}; --color-pill-text-readable:${semantic.byName.pill}; --color-badge-text-readable:${semantic.byName.badge}; --color-overlay-text-readable:${semantic.byName.overlay}; --border-radius:${px(borderRadius, "12")}px; --font-family:${font.stack}; --size-heading-h1:${px(h1, "30")}px; --size-heading-h2:${px(h2, "22")}px; --size-heading-h3:${px(h3, "18")}px; }${darkCss}`; const googleHref = font.google ? `https://fonts.googleapis.com/css2?family=${font.google}&display=swap` : null; return ( <> {googleHref ? : null} {/* biome-ignore lint/security/noDangerouslySetInnerHtml: server-built CSS from sanitized admin settings */}