Local Build and Deploy / deploy (push) Successful in 58s
- Add 6 new admin color DB keys (admin_canvas, admin_surface, admin_text, admin_text_muted, admin_border, admin_sidebar_bg) that override the derived admin palette - Extract adminPaletteCss() from themePaletteCss() for reuse - Generate admin CSS variables in both :root and html.dark with overrides - Persist admin color settings via saveTheme action - Add Admin panel (HK) section to /admin/theme with color pickers
240 lines
9.6 KiB
TypeScript
240 lines
9.6 KiB
TypeScript
import { siteSettings } from "@/lib/services/site-settings";
|
|
import { adminPaletteCss, themePaletteCss } from "@/lib/theme-css";
|
|
import { FONTS, PRESETS, THEME_COLOR_KEYS, type ThemePalette } from "@/lib/theme-presets";
|
|
import { deriveAdminPalette, 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,
|
|
adminCanvas,
|
|
adminSurface,
|
|
adminText,
|
|
adminTextMuted,
|
|
adminBorder,
|
|
adminSidebarBg,
|
|
] = 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", ""),
|
|
g("admin_canvas", ""),
|
|
g("admin_surface", ""),
|
|
g("admin_text", ""),
|
|
g("admin_text_muted", ""),
|
|
g("admin_border", ""),
|
|
g("admin_sidebar_bg", ""),
|
|
]);
|
|
|
|
// 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 adminOverrides = {
|
|
...(adminCanvas ? { canvas: safe(adminCanvas, "") } : {}),
|
|
...(adminSurface ? { surface: safe(adminSurface, "") } : {}),
|
|
...(adminText ? { text: safe(adminText, "") } : {}),
|
|
...(adminTextMuted ? { muted: safe(adminTextMuted, "") } : {}),
|
|
...(adminBorder ? { border: safe(adminBorder, "") } : {}),
|
|
...(adminSidebarBg ? { sidebar: safe(adminSidebarBg, "") } : {}),
|
|
};
|
|
|
|
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, adminOverrides);
|
|
|
|
const lightAdmin = deriveAdminPalette({
|
|
color_background: safeBackground,
|
|
color_surface: safeSurface,
|
|
color_text: safe(text, "#0f172a"),
|
|
color_text_muted: safe(textMuted, "#64748b"),
|
|
color_primary: safe(primary, "#f59e0b"),
|
|
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"),
|
|
} as ThemePalette, adminOverrides);
|
|
|
|
const css = `:root{${adminPaletteCss(lightAdmin)}
|
|
--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 ? <link rel="stylesheet" href={googleHref} /> : null}
|
|
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: server-built CSS from sanitized admin settings */}
|
|
<style dangerouslySetInnerHTML={{ __html: css }} />
|
|
{customCss && customCss.trim() ? (
|
|
// Staff-authored custom CSS (housekeeping → Theme). Trusted input.
|
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: staff-authored theme CSS
|
|
<style dangerouslySetInnerHTML={{ __html: customCss }} />
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|