feat: extend theme builder with block visibility, layout, effects, media, and custom CSS tabs
CI / check (push) Failing after 29s
CI / release (push) Skipped
CI / deploy (push) Skipped

- Add theme-blocks.ts registry with 24 themeable blocks across 4 categories
- Extend resolver to resolve blocks, layout, effects, media, and custom CSS per scope
- Add data-theme-block attributes to all site layout blocks
- Extend ScopedThemeVars to generate CSS for block visibility, layout vars, effect vars, media vars, and custom CSS
- Rewrite admin UI with 6 tabs: Colors, Blocks, Layout, Effects, Media, Custom
- Extend server actions to save/load all new setting types
- No database migration needed - uses existing theme_scope_values table with prefixed keys
This commit is contained in:
openhands committed 2026-08-31 17:44:29 +02:00
1 parent 3efd6c90f0
commit adf0658916
7 files changed
+1334 -75

No files matched your search

+156
View File
@@ -8,6 +8,13 @@ import { requirePermission } from "@/lib/admin/guard";
import { db, ThemeScope, ThemeScopeValue } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import { logStaffActivity } from "@/lib/services/staff-activity";
import {
BLOCK_KEY_PREFIX,
EFFECT_KEYS,
LAYOUT_KEYS,
MEDIA_KEYS,
THEME_BLOCKS,
} from "@/lib/theme-blocks";
import { allModuleIds } from "@/lib/theme-modules";
import { THEME_COLOR_KEYS } from "@/lib/theme-presets";
import { getAllScopes, getScopeValues } from "@/lib/theme-resolver";
@@ -143,6 +150,7 @@ export async function saveScopeValues(formData: FormData): Promise<void> {
if (!scopeId) redirect("/admin/theme-builder");
try {
// Save color values (light + dark)
for (const mode of ["light", "dark"] as const) {
for (const key of THEME_COLOR_KEYS) {
const fieldKey = `${key}_${mode}`;
@@ -165,6 +173,154 @@ export async function saveScopeValues(formData: FormData): Promise<void> {
}
}
// Save block visibility
for (const block of THEME_BLOCKS) {
const key = `${BLOCK_KEY_PREFIX}${block.id}`;
const val = formData.get(key);
const visible = val === "on" || val === "true" ? "true" : "false";
// Only save if different from default
if (visible !== String(block.defaultVisible)) {
await db
.insert(ThemeScopeValue)
.values({
scopeId: BigInt(scopeId),
settingKey: key,
settingVal: visible,
mode: "light",
createdAt: new Date(),
updatedAt: new Date(),
})
.onDuplicateKeyUpdate({
set: { settingVal: visible, updatedAt: new Date() },
});
} else {
// Remove if set back to default
await db
.delete(ThemeScopeValue)
.where(
and(
sql`${ThemeScopeValue.scopeId} = ${scopeId}`,
eq(ThemeScopeValue.settingKey, key),
),
);
}
}
// Save layout settings
for (const key of LAYOUT_KEYS) {
const raw = String(formData.get(key) ?? "").trim();
if (!raw) continue;
await db
.insert(ThemeScopeValue)
.values({
scopeId: BigInt(scopeId),
settingKey: key,
settingVal: raw,
mode: "light",
createdAt: new Date(),
updatedAt: new Date(),
})
.onDuplicateKeyUpdate({
set: { settingVal: raw, updatedAt: new Date() },
});
}
// Save effect settings
for (const key of EFFECT_KEYS) {
const raw = String(formData.get(key) ?? "").trim();
if (!raw) continue;
await db
.insert(ThemeScopeValue)
.values({
scopeId: BigInt(scopeId),
settingKey: key,
settingVal: raw,
mode: "light",
createdAt: new Date(),
updatedAt: new Date(),
})
.onDuplicateKeyUpdate({
set: { settingVal: raw, updatedAt: new Date() },
});
}
// Save media settings
for (const key of MEDIA_KEYS) {
const raw = String(formData.get(key) ?? "").trim();
if (!raw) continue;
await db
.insert(ThemeScopeValue)
.values({
scopeId: BigInt(scopeId),
settingKey: key,
settingVal: raw,
mode: "light",
createdAt: new Date(),
updatedAt: new Date(),
})
.onDuplicateKeyUpdate({
set: { settingVal: raw, updatedAt: new Date() },
});
}
// Save custom CSS
const customCss = String(formData.get("custom:css") ?? "").trim();
if (customCss) {
await db
.insert(ThemeScopeValue)
.values({
scopeId: BigInt(scopeId),
settingKey: "custom:css",
settingVal: customCss,
mode: "light",
createdAt: new Date(),
updatedAt: new Date(),
})
.onDuplicateKeyUpdate({
set: { settingVal: customCss, updatedAt: new Date() },
});
} else {
await db
.delete(ThemeScopeValue)
.where(
and(
sql`${ThemeScopeValue.scopeId} = ${scopeId}`,
eq(ThemeScopeValue.settingKey, "custom:css"),
),
);
}
// Save custom HTML
const customHtml = String(formData.get("custom:html") ?? "").trim();
if (customHtml) {
await db
.insert(ThemeScopeValue)
.values({
scopeId: BigInt(scopeId),
settingKey: "custom:html",
settingVal: customHtml,
mode: "light",
createdAt: new Date(),
updatedAt: new Date(),
})
.onDuplicateKeyUpdate({
set: { settingVal: customHtml, updatedAt: new Date() },
});
} else {
await db
.delete(ThemeScopeValue)
.where(
and(
sql`${ThemeScopeValue.scopeId} = ${scopeId}`,
eq(ThemeScopeValue.settingKey, "custom:html"),
),
);
}
const staff = await requirePermission(PERMS.SETTINGS_EDIT);
await logStaffActivity({
staffId: staff.id,