Extend the theme editor: typography, button/link colours, custom CSS, more presets

Grew /admin/theme from colours-only to a full theme editor, all applied
live via website_settings + ThemeVars:
- Typography: body font (10 web-safe + Google options; Google fonts load
  via an injected <link>) and H1/H2/H3 sizes (globals.css now reads
  --size-heading-* vars).
- Buttons & links: secondary/danger button colours + link/link-hover.
- Custom CSS: a raw textarea injected after the theme variables (staff-
  trusted), for anything the controls don't cover.
- Presets: 6 → 13 (added Galaxy, Royal, Cyberpunk, Neon, Coffee, Arctic,
  Christmas). ThemeVars now injects all the new vars + the font link.

Verified live (prod, amx_test): saved font=mono / H1=44px / custom CSS →
the public home reflected --font-family "Courier New", --size-heading-h1
44px and the injected rule; reverted the test settings. tsc 0,
vitest 49/49, next build 0.
This commit is contained in:
Simo committed 2026-06-28 21:14:10 +02:00
1 parent 0cd4d06ff6
commit 5a4b6f27e9
5 files changed
+322 -52

No files matched your search

+49 -4
View File
@@ -1,7 +1,9 @@
import { siteSettings } from "@/lib/services/site-settings";
import { FONTS } from "@/lib/theme-presets";
// 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.
// 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 [
@@ -16,8 +18,17 @@ export async function ThemeVars() {
accent,
buttonColor,
buttonText,
buttonSecondary,
buttonDanger,
linkColor,
linkHover,
borderColor,
borderRadius,
fontKey,
h1,
h2,
h3,
customCss,
] = await Promise.all([
g("color_primary", "#f59e0b"),
g("color_background", "#f8fafc"),
@@ -30,12 +41,24 @@ export async function ThemeVars() {
g("color_accent", "#10b981"),
g("button_primary_color", "#f59e0b"),
g("button_text_color", "#1e293b"),
g("button_secondary_color", "#22c55e"),
g("button_danger_color", "#ef4444"),
g("link_color", "#eeb425"),
g("link_hover_color", "#cf9d15"),
g("border_color", "#eeb425"),
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 font = FONTS[fontKey ?? "nunito"] ?? FONTS.nunito;
const css = `:root{
--color-primary:${safe(primary, "#f59e0b")};
@@ -49,10 +72,32 @@ export async function ThemeVars() {
--color-accent:${safe(accent, "#10b981")};
--button-color:${safe(buttonColor, "#f59e0b")};
--button-text-color:${safe(buttonText, "#1e293b")};
--button-secondary-color:${safe(buttonSecondary, "#22c55e")};
--button-danger-color:${safe(buttonDanger, "#ef4444")};
--link-color:${safe(linkColor, "#eeb425")};
--link-hover-color:${safe(linkHover, "#cf9d15")};
--border-color:${safe(borderColor, "#eeb425")};
--border-radius:${/^\d+$/.test(borderRadius ?? "") ? borderRadius : "12"}px;
--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;
}`;
// biome-ignore lint/security/noDangerouslySetInnerHtml: server-built CSS from sanitized admin settings
return <style dangerouslySetInnerHTML={{ __html: css }} />;
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}
</>
);
}