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

+26 -3
View File
@@ -6,10 +6,19 @@ import { requireStaff } from "@/lib/admin/guard";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings";
import { logStaffActivity } from "@/lib/services/staff-activity";
import { PRESETS, THEME_COLOR_KEYS } from "@/lib/theme-presets";
import { FONTS, PRESETS, THEME_COLOR_KEYS } from "@/lib/theme-presets";
// Only hex/keyword colour values are accepted (matches ThemeVars' sanitiser).
const COLOR_RE = /^[#a-zA-Z0-9(),.\s%-]+$/;
// Extra colour settings beyond the preset palette (buttons + links).
const EXTRA_COLOR_KEYS = [
"button_secondary_color",
"button_danger_color",
"link_color",
"link_hover_color",
];
const HEADING_KEYS = ["size_heading_h1", "size_heading_h2", "size_heading_h3"];
const CUSTOM_CSS_MAX = 20000;
async function writeSetting(key: string, value: string): Promise<void> {
await prisma.websiteSetting.upsert({
@@ -23,18 +32,32 @@ export async function saveTheme(formData: FormData): Promise<void> {
const staff = await requireStaff();
try {
for (const key of THEME_COLOR_KEYS) {
for (const key of [...THEME_COLOR_KEYS, ...EXTRA_COLOR_KEYS]) {
const raw = String(formData.get(key) ?? "").trim();
if (raw && COLOR_RE.test(raw)) await writeSetting(key, raw);
}
const radius = String(formData.get("border_radius") ?? "").trim();
if (/^\d{1,3}$/.test(radius)) await writeSetting("border_radius", radius);
// Typography
const font = String(formData.get("font_family") ?? "").trim();
if (font in FONTS) await writeSetting("font_family", font);
for (const key of HEADING_KEYS) {
const v = String(formData.get(key) ?? "").trim();
if (/^\d{1,3}$/.test(v)) await writeSetting(key, v);
}
// Raw custom CSS (staff-trusted; length-capped, ThemeVars injects it as-is).
if (formData.has("custom_css")) {
const cssRaw = String(formData.get("custom_css") ?? "").slice(0, CUSTOM_CSS_MAX);
await writeSetting("custom_css", cssRaw);
}
siteSettings.reload();
await logStaffActivity({
staffId: staff.id,
action: "theme_update",
description: "Updated theme colours",
description: "Updated theme settings",
});
revalidatePath("/", "layout");
} catch {