Add named custom theme presets (save/load/rename/delete) in admin theme editor
Local Build and Deploy / deploy (push) Successful in 58s

This commit is contained in:
openhands committed 2026-07-13 20:42:40 +02:00
1 parent 01b70c2369
commit e6d7f2280b
4 files changed
+321 -2

No files matched your search

+83 -2
View File
@@ -1,8 +1,10 @@
import { getTranslations } from "next-intl/server";
import { applyPreset, saveTheme } from "@/actions/admin-theme";
import { applyCustomTheme, applyPreset, deleteCustomTheme, saveCustomTheme, saveTheme } from "@/actions/admin-theme";
import { siteSettings } from "@/lib/services/site-settings";
import { FONTS, PRESETS } from "@/lib/theme-presets";
import { listCustomThemes } from "@/lib/theme-custom-store";
import { ColorField } from "./color-field";
import { RenameTheme } from "@/components/admin/theme/rename-theme";
export const dynamic = "force-dynamic";
export const metadata = { title: "Theme" };
@@ -106,7 +108,7 @@ function colorFields(
export default async function AdminTheme({
searchParams,
}: {
searchParams: Promise<{ saved?: string; preset?: string }>;
searchParams: Promise<{ saved?: string; preset?: string; savedTheme?: string; theme?: string; renamed?: string; deletedTheme?: string }>;
}) {
const t = await getTranslations("pages.admin.theme");
@@ -134,6 +136,7 @@ export default async function AdminTheme({
),
),
) as Record<string, string>;
const customThemes = await listCustomThemes();
const radius = (await siteSettings.get("border_radius", "12")) ?? "12";
const fontKey = (await siteSettings.get("font_family", "nunito")) ?? "nunito";
const headings = Object.fromEntries(
@@ -163,6 +166,20 @@ export default async function AdminTheme({
{t("presetApplied", { preset: sp.preset })}
</div>
) : null}
{sp.savedTheme ? (
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">Theme saved as preset.</div>
) : null}
{sp.theme ? (
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">
Applied theme “{sp.theme}”.
</div>
) : null}
{sp.renamed ? (
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">Theme renamed.</div>
) : null}
{sp.deletedTheme ? (
<div className="mb-4 p-3 rounded-lg theme-status-success text-xs">Theme deleted.</div>
) : null}
<section className="mt-6">
<h2 className="admin-section-title">{t("presetsTitle")}</h2>
@@ -193,6 +210,70 @@ export default async function AdminTheme({
</div>
</section>
<section className="mt-6">
<h2 className="admin-section-title">Your saved themes</h2>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
Save the current configuration as a named preset, then load it anytime. Perfect for keeping multiple brand
themes ready to switch.
</p>
<form action={saveCustomTheme} className="flex flex-wrap items-center gap-2 mb-4">
<input
type="text"
name="name"
placeholder="Theme name (e.g. Summer Gold)"
className="px-3 py-2 rounded-lg border bg-[var(--admin-canvas)] text-[var(--admin-text)] text-sm w-64"
style={{ borderColor: "var(--admin-border)" }}
/>
<button type="submit" className="btn btn-primary">
Save current theme
</button>
</form>
{customThemes.length === 0 ? (
<p className="text-xs text-[var(--admin-text-muted)]">No saved themes yet — save one above.</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{customThemes.map((theme) => (
<div key={theme.id} className="admin-card p-3 flex flex-col gap-2">
<div className="flex items-center gap-1">
{["color_primary", "color_accent", "color_surface", "color_text"].map((k) => (
<span
key={k}
style={{
width: 20,
height: 20,
borderRadius: 4,
// eslint-disable-next-line security/detect-object-injection -- k from fixed theme color key list
background: theme.settings[k] ?? "#64748b",
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.2)",
}}
/>
))}
</div>
<div className="flex items-center justify-between gap-2">
<span className="font-semibold text-[var(--admin-text)] truncate">{theme.name}</span>
<RenameTheme id={theme.id} name={theme.name} />
</div>
<div className="flex gap-2">
<form action={applyCustomTheme}>
<input type="hidden" name="id" value={theme.id} />
<button type="submit" className="btn btn-sm btn-outline">
Load
</button>
</form>
<form action={deleteCustomTheme}>
<input type="hidden" name="id" value={theme.id} />
<button type="submit" className="btn btn-sm btn-danger">
Delete
</button>
</form>
</div>
</div>
))}
</div>
)}
</section>
<form action={saveTheme}>
<h2 className="mt-6 text-lg font-extrabold">☀ Light mode</h2>
<p className="text-xs text-[var(--admin-text-muted)] mb-3 mt-1">