feat: add scoped theme builder system with per-route, per-module, and multi-site support

- Add theme_scopes and theme_scope_values database tables for scoped themes
- Implement theme resolver engine with inheritance: global > site > module > route
- Add module detection for 20+ routes (shop, guilds, radio, news, etc.)
- Create admin UI at /admin/theme-builder with scope tree and color editor
- Add ScopedThemeVars component for injecting scoped CSS via data-attributes
- Add ThemeScopeDetector client component for runtime module/route detection
- Add site-resolver for multi-site domain detection
- Add /api/themes/export endpoint (JSON, CSS, variables formats)
- Add /api/themes/export/embed.js for external integration widget
- Add server actions for full CRUD on scopes and theme values
- Add admin nav link and EN/NL translations
This commit is contained in:
openhands committed 2026-08-31 17:15:42 +02:00
1 parent b57697b2e0
commit a98b194386
19 files changed
+1843

No files matched your search

+42
View File
@@ -2583,3 +2583,45 @@ export const YoutubePlaylists = mysqlTable(
),
],
);
// ─── Theme Builder: scoped theme system ──────────────────────────────────────
export const ThemeScope = mysqlTable("theme_scopes", {
id: bigint("id", { mode: "bigint", unsigned: true })
.autoincrement()
.primaryKey()
.notNull(),
name: varchar("name", { length: 100 }).notNull(),
type: mysqlEnum("type", ["global", "site", "module", "route"]).notNull(),
parentId: bigint("parent_id", { mode: "bigint", unsigned: true }),
siteDomain: varchar("site_domain", { length: 255 }),
routePath: varchar("route_path", { length: 255 }),
moduleId: varchar("module_id", { length: 100 }),
isActive: boolean("is_active").notNull().default(true),
sortOrder: int("sort_order").notNull().default(0),
createdAt: datetime("created_at").notNull(),
updatedAt: datetime("updated_at").notNull(),
});
export const ThemeScopeValue = mysqlTable(
"theme_scope_values",
{
id: bigint("id", { mode: "bigint", unsigned: true })
.autoincrement()
.primaryKey()
.notNull(),
scopeId: bigint("scope_id", { mode: "bigint", unsigned: true }).notNull(),
settingKey: varchar("setting_key", { length: 100 }).notNull(),
settingVal: varchar("setting_val", { length: 255 }).notNull(),
mode: mysqlEnum("mode", ["light", "dark"]).notNull().default("light"),
createdAt: datetime("created_at").notNull(),
updatedAt: datetime("updated_at").notNull(),
},
(t) => [
uniqueIndex("theme_scope_values_unique").on(
t.scopeId,
t.settingKey,
t.mode,
),
],
);