import { asc, eq } from "drizzle-orm"; import { db, ThemeScope, ThemeScopeValue } from "@/lib/db"; import { THEME_COLOR_KEYS, type ThemeColorKey } from "@/lib/theme-presets"; // ─── Scoped CSS injection ──────────────────────────────────────────────────── // This component works alongside ThemeVars. ThemeVars handles the global theme; // this component adds scoped CSS blocks for module/route overrides using // data-attribute selectors. type ThemeScopeType = "global" | "site" | "module" | "route"; interface ScopeRow { id: number; type: ThemeScopeType; siteDomain: string | null; routePath: string | null; moduleId: string | null; } interface ScopeValues { light: Partial>; dark: Partial>; } async function loadScopedThemes(): Promise< Array<{ scope: ScopeRow; values: ScopeValues }> > { const rows = await db .select() .from(ThemeScope) .where(eq(ThemeScope.isActive, true)) .orderBy(asc(ThemeScope.sortOrder), asc(ThemeScope.id)); const scopes: ScopeRow[] = rows .filter((r) => r.type !== "global") .map((r) => ({ id: Number(r.id), type: r.type as ThemeScopeType, siteDomain: r.siteDomain, routePath: r.routePath, moduleId: r.moduleId, })); if (scopes.length === 0) return []; const allValues = await db.select().from(ThemeScopeValue); const valuesByScope = new Map(); for (const row of allValues) { const scopeId = Number(row.scopeId); const key = row.settingKey as ThemeColorKey; if (!valuesByScope.has(scopeId)) { valuesByScope.set(scopeId, { light: {}, dark: {} }); } const sv = valuesByScope.get(scopeId); if (!sv) continue; if (row.mode === "light") sv.light[key] = row.settingVal; else sv.dark[key] = row.settingVal; } return scopes .filter((s) => valuesByScope.has(s.id)) .map((scope) => ({ scope, values: valuesByScope.get(scope.id) ?? { light: {}, dark: {} }, })); } function escapeAttr(v: string): string { return v.replace(/"/g, """).replace(//g, ">"); } function buildScopedCssBlock( scope: ScopeRow, values: ScopeValues, ): string | null { let selector: string; switch (scope.type) { case "site": if (!scope.siteDomain) return null; selector = `[data-theme-site="${escapeAttr(scope.siteDomain)}"]`; break; case "module": if (!scope.moduleId) return null; selector = `[data-theme-module="${escapeAttr(scope.moduleId)}"]`; break; case "route": if (!scope.routePath) return null; selector = `[data-theme-route="${escapeAttr(scope.routePath)}"]`; break; default: return null; } const lightVars = THEME_COLOR_KEYS.filter((k) => values.light[k]) .map((k) => `--${k}:${values.light[k]};`) .join(""); const darkVars = THEME_COLOR_KEYS.filter((k) => values.dark[k]) .map((k) => `--${k}:${values.dark[k]};`) .join(""); if (!lightVars && !darkVars) return null; let css = ""; if (lightVars) css += `${selector}{${lightVars}}`; if (darkVars) css += `html.dark ${selector}{${darkVars}}`; return css; } /** * ScopedThemeVars injects CSS blocks for non-global theme scopes. * Place this after in your layout. */ export async function ScopedThemeVars({ nonce }: { nonce?: string }) { const scoped = await loadScopedThemes(); if (scoped.length === 0) return null; const blocks: string[] = []; for (const { scope, values } of scoped) { const css = buildScopedCssBlock(scope, values); if (css) blocks.push(css); } if (blocks.length === 0) return null; return (