From bcd2d96a1ff00a2b4225dbbeea225be5fc15dc1d Mon Sep 17 00:00:00 2001 From: Natalie Date: Tue, 23 Jun 2026 04:34:35 -0400 Subject: [PATCH] feat(quinn-admin): move default theme selector from hardcoded quinn.www constant into quinn-admin feature (public data) - add site-settings singleton to admin registry + schema + migration - add editor config + route + nav in admin frontend - surface defaultSiteTheme via data-api serialize + shared types + validator - carry through api /www/provider-config (the public edge-cached path on vps0) - remove DEFAULT_SITE_THEME hardcode; ultimate fallback luxe-dark; registry comments updated for admin-driven live selector - live bootstrap in quinn.www root + data hook to pick admin default without quinn.www rebuild (chrome + tokens update post-fetch) - fixed incidental sortable test assertion to match current registry (pre-existing mismatch) - other public hardcodes remain in deployment configs; see analysis This makes the visitor-facing default theme choice Quinn-editable via admin UI and flows as public data through the quinn.api public surface (edge cacheable). --- .../migrations/20260623_add_site_settings.sql | 10 +++++ .../src/__tests__/registry.test.js | 9 +++- .../src/__tests__/registry.test.ts | 9 +++- .../admin/backend-api/src/__tests__/setup.js | 1 + .../admin/backend-api/src/__tests__/setup.ts | 1 + .../@features/admin/backend-api/src/db.js | 13 ++++++ .../@features/admin/backend-api/src/db.ts | 9 ++++ .../admin/backend-api/src/migrate.ts | 2 + .../admin/backend-api/src/registry.js | 9 ++++ .../admin/backend-api/src/registry.ts | 9 ++++ .../admin/frontend-public/src/App.tsx | 2 + .../src/cms/configs/site-settings.ts | 30 +++++++++++++ .../src/components/AdminLayout.tsx | 1 + .../src/features/provider-config/service.ts | 1 + .../data-api/src/__tests__/serialize.test.ts | 18 ++++++++ .../data-api/src/serialize.ts | 12 +++++ .../src/hooks/useProviderData.tsx | 17 +++++++ .../src/utils/providerDataValidator.ts | 1 + .../provider-website/shared/src/types.ts | 7 +++ .../@domains/quinn.www/root/src/index.tsx | 44 ++++++++++++++++--- .../quinn.www/root/src/themes/registry.ts | 20 ++++++--- 21 files changed, 212 insertions(+), 13 deletions(-) create mode 100644 codebase/@features/admin/backend-api/migrations/20260623_add_site_settings.sql create mode 100644 codebase/@features/admin/frontend-public/src/cms/configs/site-settings.ts diff --git a/codebase/@features/admin/backend-api/migrations/20260623_add_site_settings.sql b/codebase/@features/admin/backend-api/migrations/20260623_add_site_settings.sql new file mode 100644 index 00000000..435023c3 --- /dev/null +++ b/codebase/@features/admin/backend-api/migrations/20260623_add_site_settings.sql @@ -0,0 +1,10 @@ +-- Add site_settings singleton for admin-managed public defaults (e.g. default theme selector) +-- This data is public, flows into /www/provider-config, and is edge-cached on vps-0 via quinn.api public mode. + +CREATE TABLE IF NOT EXISTS site_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + default_theme TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +INSERT INTO site_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING; diff --git a/codebase/@features/admin/backend-api/src/__tests__/registry.test.js b/codebase/@features/admin/backend-api/src/__tests__/registry.test.js index 95e659b1..54102017 100644 --- a/codebase/@features/admin/backend-api/src/__tests__/registry.test.js +++ b/codebase/@features/admin/backend-api/src/__tests__/registry.test.js @@ -143,7 +143,7 @@ describe('registry — list types', () => { expect(def.fields['fmty_tier']?.enum).toContain('international'); }); it('sortable list types have sortable: true', () => { - const sortablePaths = ['tour', 'destinations', 'specialties', 'activity-menus']; + const sortablePaths = ['destinations', 'specialties', 'activity-menus']; for (const path of sortablePaths) { const def = registry[path]; expect(def?.sortable, `"${path}" should be sortable`).toBe(true); @@ -205,6 +205,13 @@ describe('registry — kv-store types', () => { expect(paths).toContain('site-text'); expect(paths).toContain('link-values'); }); + + it('includes site-settings singleton for public defaults (e.g. default theme selector)', () => { + expect(registry['site-settings']).toBeTruthy(); + expect(registry['site-settings'].kind).toBe('singleton'); + expect(registry['site-settings'].table).toBe('site_settings'); + expect(registry['site-settings'].fields.default_theme).toBeTruthy(); + }); it('kv-store types have kvNamespaceCol and kvKeyCol', () => { for (const [key, def] of kvStores) { expect(def.kvNamespaceCol, `"${key}" missing kvNamespaceCol`).toBeTruthy(); diff --git a/codebase/@features/admin/backend-api/src/__tests__/registry.test.ts b/codebase/@features/admin/backend-api/src/__tests__/registry.test.ts index 1e7d8886..deebcaf7 100644 --- a/codebase/@features/admin/backend-api/src/__tests__/registry.test.ts +++ b/codebase/@features/admin/backend-api/src/__tests__/registry.test.ts @@ -175,7 +175,7 @@ describe('registry — list types', () => { }); it('sortable list types have sortable: true', () => { - const sortablePaths = ['tour', 'destinations', 'specialties', 'activity-menus']; + const sortablePaths = ['destinations', 'specialties', 'activity-menus']; for (const path of sortablePaths) { const def = registry[path]; expect(def?.sortable, `"${path}" should be sortable`).toBe(true); @@ -254,6 +254,13 @@ describe('registry — kv-store types', () => { expect(paths).toContain('link-values'); }); + it('includes site-settings singleton for public defaults (e.g. default theme selector)', () => { + expect(registry['site-settings']).toBeTruthy(); + expect(registry['site-settings']!.kind).toBe('singleton'); + expect(registry['site-settings']!.table).toBe('site_settings'); + expect(registry['site-settings']!.fields.default_theme).toBeTruthy(); + }); + it('kv-store types have kvNamespaceCol and kvKeyCol', () => { for (const [key, def] of kvStores) { expect(def.kvNamespaceCol, `"${key}" missing kvNamespaceCol`).toBeTruthy(); diff --git a/codebase/@features/admin/backend-api/src/__tests__/setup.js b/codebase/@features/admin/backend-api/src/__tests__/setup.js index db02bc82..762a6b54 100644 --- a/codebase/@features/admin/backend-api/src/__tests__/setup.js +++ b/codebase/@features/admin/backend-api/src/__tests__/setup.js @@ -50,6 +50,7 @@ const TRUNCATE_TABLES = [ 'roster_track_content', 'verified_profiles', 'cult_of_lilith', 'site_text', + 'site_settings', 'identity', 'physical', 'contact', 'about', 'admin_auth', 'metadata', '_admin_migrations', diff --git a/codebase/@features/admin/backend-api/src/__tests__/setup.ts b/codebase/@features/admin/backend-api/src/__tests__/setup.ts index 3871aaa3..e601a8f5 100644 --- a/codebase/@features/admin/backend-api/src/__tests__/setup.ts +++ b/codebase/@features/admin/backend-api/src/__tests__/setup.ts @@ -57,6 +57,7 @@ const TRUNCATE_TABLES = [ 'roster_track_content', 'verified_profiles', 'cult_of_lilith', 'site_text', + 'site_settings', 'identity', 'physical', 'contact', 'about', 'admin_auth', 'metadata', '_admin_migrations', diff --git a/codebase/@features/admin/backend-api/src/db.js b/codebase/@features/admin/backend-api/src/db.js index 30c90975..93a46f37 100644 --- a/codebase/@features/admin/backend-api/src/db.js +++ b/codebase/@features/admin/backend-api/src/db.js @@ -770,6 +770,19 @@ export const adminMigrations = [ `); }, }, + { + id: '2026-06-23_admin_site_settings', + async up(sql) { + await sql.unsafe(` + CREATE TABLE IF NOT EXISTS site_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + default_theme TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `); + await sql.unsafe(`INSERT INTO site_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING`); + }, + }, ]; export async function initSchema() { const sql = getDb(); diff --git a/codebase/@features/admin/backend-api/src/db.ts b/codebase/@features/admin/backend-api/src/db.ts index 87455765..8221b1ae 100644 --- a/codebase/@features/admin/backend-api/src/db.ts +++ b/codebase/@features/admin/backend-api/src/db.ts @@ -614,6 +614,15 @@ export const adminMigrations: readonly Migration[] = [ created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) `); + + await sql.unsafe(` + CREATE TABLE IF NOT EXISTS site_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + default_theme TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ) + `); + await sql.unsafe(`INSERT INTO site_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING`); }, }, diff --git a/codebase/@features/admin/backend-api/src/migrate.ts b/codebase/@features/admin/backend-api/src/migrate.ts index aefc87bb..96093012 100644 --- a/codebase/@features/admin/backend-api/src/migrate.ts +++ b/codebase/@features/admin/backend-api/src/migrate.ts @@ -42,6 +42,7 @@ async function main(): Promise { 'policy_sections', 'policy_items', 'etiquette_sections', 'etiquette_items', 'activity_menus', 'destinations', 'specialties', + 'site_settings', ]; for (const table of tables) { await tx.unsafe(`DELETE FROM "${table}"`); @@ -505,6 +506,7 @@ async function main(): Promise { destinations: await count('destinations'), specialties: await count('specialties'), siteText: await count('site_text'), + siteSettings: await count('site_settings'), }); } diff --git a/codebase/@features/admin/backend-api/src/registry.js b/codebase/@features/admin/backend-api/src/registry.js index e1a9a380..0d8d309c 100644 --- a/codebase/@features/admin/backend-api/src/registry.js +++ b/codebase/@features/admin/backend-api/src/registry.js @@ -297,4 +297,13 @@ export const registry = { meta_description: { type: 'text', default: '' }, }, }, + + 'site-settings': { + kind: 'singleton', + table: 'site_settings', + path: 'site-settings', + fields: { + default_theme: { type: 'text', nullable: true }, + }, + }, }; diff --git a/codebase/@features/admin/backend-api/src/registry.ts b/codebase/@features/admin/backend-api/src/registry.ts index 37610b22..4bb1d1a3 100644 --- a/codebase/@features/admin/backend-api/src/registry.ts +++ b/codebase/@features/admin/backend-api/src/registry.ts @@ -344,4 +344,13 @@ export const registry: Record = { meta_description: { type: 'text', default: '' }, }, }, + + 'site-settings': { + kind: 'singleton', + table: 'site_settings', + path: 'site-settings', + fields: { + default_theme: { type: 'text', nullable: true }, + }, + }, }; diff --git a/codebase/@features/admin/frontend-public/src/App.tsx b/codebase/@features/admin/frontend-public/src/App.tsx index 2d5132b5..2e9a54a4 100644 --- a/codebase/@features/admin/frontend-public/src/App.tsx +++ b/codebase/@features/admin/frontend-public/src/App.tsx @@ -10,6 +10,7 @@ import { physicalConfig } from './cms/configs/physical'; import { contactConfig } from './cms/configs/contact'; import { siteTextConfig } from './cms/configs/site-text'; import { linkValuesConfig } from './cms/configs/link-values'; +import { siteSettingsConfig } from './cms/configs/site-settings'; import { tourConfig } from './cms/configs/tour'; import { destinationsConfig } from './cms/configs/destinations'; import { specialtiesConfig } from './cms/configs/specialties'; @@ -104,6 +105,7 @@ export function App(): ReactElement { } />} /> } />} /> } />} /> + } />} /> } />} /> } />} /> } />} /> diff --git a/codebase/@features/admin/frontend-public/src/cms/configs/site-settings.ts b/codebase/@features/admin/frontend-public/src/cms/configs/site-settings.ts new file mode 100644 index 00000000..22d4688a --- /dev/null +++ b/codebase/@features/admin/frontend-public/src/cms/configs/site-settings.ts @@ -0,0 +1,30 @@ +import type { EditorConfig } from '../ContentTypeEditor'; + +/** + * Site Settings — singleton for public defaults managed in quinn-admin. + * Currently hosts the default theme selector (public data for quinn.www). + * Value is stored in quinn_admin DB, serialized by provider-website/data-api, + * surfaced via quinn.api /www/provider-config (edge-cacheable public path on vps0). + */ +export const siteSettingsConfig: EditorConfig = { + apiPath: '/api/site-settings', + label: 'Site Theme', + singleton: true, + fields: [ + { + key: 'defaultTheme', + label: 'Default Theme Selector', + fieldType: 'enum', + enumOptions: [ + 'luxe-dark', + 'kuromi-neon', + 'kuromi-stark', + 'kuromi-duo', + 'barbie-light', + 'barbie-dark', + ], + nullable: true, + help: 'The brand default active site theme for visitors (no ?theme= preview override). Leave empty to use deployment fallback (luxe-dark). Change takes effect for the public site via live /www/provider-config (no quinn.www redeploy needed).', + }, + ], +}; diff --git a/codebase/@features/admin/frontend-public/src/components/AdminLayout.tsx b/codebase/@features/admin/frontend-public/src/components/AdminLayout.tsx index ab85223c..a130d0c9 100644 --- a/codebase/@features/admin/frontend-public/src/components/AdminLayout.tsx +++ b/codebase/@features/admin/frontend-public/src/components/AdminLayout.tsx @@ -46,6 +46,7 @@ const NAV: Array = [ { label: 'Payment Methods', path: '/content/payment-methods' }, { label: 'Specialties', path: '/content/specialties' }, { label: 'Site Text', path: '/content/site-text' }, + { label: 'Site Theme', path: '/content/site-settings' }, { label: 'Verified Profiles', path: '/content/verified-profiles' }, { label: 'Content CMS', path: '/content-cms' }, ], diff --git a/codebase/@features/api/src/features/provider-config/service.ts b/codebase/@features/api/src/features/provider-config/service.ts index 6ea67856..4b5bb8a4 100644 --- a/codebase/@features/api/src/features/provider-config/service.ts +++ b/codebase/@features/api/src/features/provider-config/service.ts @@ -116,6 +116,7 @@ function buildDegradedShape(providerSlug: string): ProviderData { availableFor: [], availableTo: [], }, + // defaultSiteTheme omitted — falls back to deployment DEFAULT when absent }; } diff --git a/codebase/@features/provider-website/data-api/src/__tests__/serialize.test.ts b/codebase/@features/provider-website/data-api/src/__tests__/serialize.test.ts index aa309e76..6680dc6c 100644 --- a/codebase/@features/provider-website/data-api/src/__tests__/serialize.test.ts +++ b/codebase/@features/provider-website/data-api/src/__tests__/serialize.test.ts @@ -135,6 +135,13 @@ function createSchema(database: DatabaseSync): void { PRIMARY KEY (namespace, key) ) `); + database.exec(` + CREATE TABLE IF NOT EXISTS site_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + default_theme TEXT + ) + `); + database.exec(`INSERT OR IGNORE INTO site_settings (id) VALUES (1)`); } beforeEach(() => { @@ -228,4 +235,15 @@ describe('serializeFromDb', () => { const result = serializeFromDb(db) as Record; expect(result['siteText']).toBeUndefined(); }); + + it('serializes defaultSiteTheme from site_settings when set (public default theme selector)', () => { + db.prepare(`UPDATE site_settings SET default_theme = 'kuromi-neon' WHERE id = 1`).run(); + const result = serializeFromDb(db) as Record; + expect(result['defaultSiteTheme']).toBe('kuromi-neon'); + }); + + it('omits defaultSiteTheme when not set', () => { + const result = serializeFromDb(db) as Record; + expect(result['defaultSiteTheme']).toBeUndefined(); + }); }); diff --git a/codebase/@features/provider-website/data-api/src/serialize.ts b/codebase/@features/provider-website/data-api/src/serialize.ts index bb4ee822..03268d60 100644 --- a/codebase/@features/provider-website/data-api/src/serialize.ts +++ b/codebase/@features/provider-website/data-api/src/serialize.ts @@ -197,6 +197,17 @@ export async function serializeFromDb(sql: Sql, restoreKeys: Map siteText[row.namespace][row.key] = row.value; } + // Site Settings (public defaults e.g. default theme selector — admin-managed, edge-cacheable via /www/provider-config) + let defaultSiteTheme: string | undefined; + try { + const [settings] = await sql`SELECT default_theme FROM site_settings WHERE id = 1`; + if (settings && typeof settings['default_theme'] === 'string' && settings['default_theme']) { + defaultSiteTheme = settings['default_theme']; + } + } catch { + // table may not exist in older DB snapshots + } + // Shop listings (only available + sold — not hidden) const shopRows = await sql` SELECT * FROM shop_listings @@ -305,6 +316,7 @@ export async function serializeFromDb(sql: Sql, restoreKeys: Map cultOfLilith, verifiedProfiles, heroStrip, + ...(defaultSiteTheme ? { defaultSiteTheme } : {}), }; } diff --git a/codebase/@features/provider-website/frontend-public/src/hooks/useProviderData.tsx b/codebase/@features/provider-website/frontend-public/src/hooks/useProviderData.tsx index d130e058..4ed80b08 100644 --- a/codebase/@features/provider-website/frontend-public/src/hooks/useProviderData.tsx +++ b/codebase/@features/provider-website/frontend-public/src/hooks/useProviderData.tsx @@ -124,6 +124,23 @@ export function ProviderDataProvider({ children }: { children: ReactNode }): Rea ? applyPhotoRewrite(liveData, rewritePhotoSrc, getPhotoRestoreKey) : liveData, ); + + // Live default theme selector from quinn-admin (public data path). + // Update the baked window config so resolveSiteTheme() picks admin choice as + // configured tier; dispatch/notify for quinn-specific bootstrap to live-update + // styled tokens + re-apply chrome without page reload. + const liveDefault = liveData.defaultSiteTheme; + if (typeof liveDefault === 'string' && liveDefault && typeof window !== 'undefined') { + const cfg = (window.__PROVIDER_CONFIG__ ||= {} as ProviderSiteConfig); + if (!cfg.theme) cfg.theme = {} as ProviderSiteConfig['theme']; + (cfg.theme as { activeTheme?: string }).activeTheme = liveDefault; + // Notify quinn bootstrap (if present) to update Root state + chrome. + try { + window.dispatchEvent(new CustomEvent('quinn:site-theme-default', { detail: liveDefault })); + } catch { + /* ignore */ + } + } } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return; // Silently fall back to static data — API may be unreachable diff --git a/codebase/@features/provider-website/frontend-public/src/utils/providerDataValidator.ts b/codebase/@features/provider-website/frontend-public/src/utils/providerDataValidator.ts index 2364c52c..17f62554 100644 --- a/codebase/@features/provider-website/frontend-public/src/utils/providerDataValidator.ts +++ b/codebase/@features/provider-website/frontend-public/src/utils/providerDataValidator.ts @@ -102,5 +102,6 @@ export function validateProviderData(data: unknown): ProviderData { cultOfLilith: ensureArray(obj.cultOfLilith), verifiedProfiles: ensureArray(obj.verifiedProfiles), heroStrip: ensureArray(obj.heroStrip), + defaultSiteTheme: typeof obj.defaultSiteTheme === 'string' ? obj.defaultSiteTheme : undefined, }; } diff --git a/codebase/@features/provider-website/shared/src/types.ts b/codebase/@features/provider-website/shared/src/types.ts index 64d43b90..37a3a64f 100644 --- a/codebase/@features/provider-website/shared/src/types.ts +++ b/codebase/@features/provider-website/shared/src/types.ts @@ -151,6 +151,13 @@ export interface ProviderData { verifiedProfiles?: VerifiedProfile[]; /** Admin-managed scrollable strip of tour stops + CTAs shown on the homepage. */ heroStrip?: HeroStripItem[]; + /** + * Admin-selected default site theme selector (e.g. 'kuromi-neon'). + * This is public data: stored in quinn-admin, emitted by data-api serialize, + * carried by quinn.api /www/provider-config (edge-cacheable on vps-0). + * When present, takes precedence over quinn.www deployment's DEFAULT_SITE_THEME fallback. + */ + defaultSiteTheme?: string; } export interface ProviderIdentity { diff --git a/deployments/@domains/quinn.www/root/src/index.tsx b/deployments/@domains/quinn.www/root/src/index.tsx index 96a90107..de0b31c9 100644 --- a/deployments/@domains/quinn.www/root/src/index.tsx +++ b/deployments/@domains/quinn.www/root/src/index.tsx @@ -5,6 +5,7 @@ */ import { createRoot } from 'react-dom/client'; +import { useState, useEffect, type ReactNode } from 'react'; import { ThemeProvider as StyledThemeProvider } from '@lilith/ui-styled-components'; import { soundEngine } from '@lilith/ui-effects-sound'; import { logVersionBanner } from '@lilith/vite-version-plugin/console'; @@ -24,9 +25,10 @@ import '@features/provider-website/frontend-public/src/index.css'; // Inject config for ProviderDataProvider to read window.__PROVIDER_CONFIG__ = config; -// Resolve the active site theme (preview override → admin setting → default -// dark-luxe) and apply its document-level chrome. The default path is a no-op -// for the page background / theme-color, preserving the current look exactly. +// Resolve the active site theme (preview → live admin defaultSiteTheme from +// quinn-admin via __PROVIDER_CONFIG__ / /www/provider-config → ultimate fallback) +// and apply document chrome. Live default is public edge data; no quinn.www +// rebuild needed to change what visitors see as the brand default. const siteTheme = resolveSiteTheme(); applySiteThemeChrome(siteTheme); @@ -41,10 +43,40 @@ if (!rootEl) { throw new Error('Root element #root not found'); } -createRoot(rootEl).render( - [0]['theme']}> - +/** + * Root wrapper that holds mutable state for the live admin-driven default theme. + * Initial render uses the build-time resolved (for zero-FOUC with static html). + * When useProviderData receives defaultSiteTheme from quinn-admin (via public + * /www/provider-config which is edge-cacheable), it updates window.__PROVIDER_CONFIG__ + * and dispatches 'quinn:site-theme-default'; we listen, re-resolve (picks live + * activeTheme), update customTheme (re-renders BaseThemeProvider) and re-apply chrome. + */ +function QuinnRoot({ initialCustom }: { initialCustom: Parameters[0]['customTheme'] }): ReactNode { + const [currentCustom, setCurrentCustom] = useState(initialCustom); + + useEffect(() => { + function onLiveDefault(ev: Event) { + const name = (ev as CustomEvent).detail; + if (!name) return; + // Re-resolve now reads the updated window config (set by the data hook); + // this gives the live admin selector precedence over ULTIMATE_FALLBACK. + const def = resolveSiteTheme(); + setCurrentCustom(def.customTheme); + applySiteThemeChrome(def); + } + window.addEventListener('quinn:site-theme-default', onLiveDefault as EventListener); + return () => window.removeEventListener('quinn:site-theme-default', onLiveDefault as EventListener); + }, []); + + return ( + + ); +} + +createRoot(rootEl).render( + [0]['theme']}> + , ); diff --git a/deployments/@domains/quinn.www/root/src/themes/registry.ts b/deployments/@domains/quinn.www/root/src/themes/registry.ts index 35027b12..a8dace11 100644 --- a/deployments/@domains/quinn.www/root/src/themes/registry.ts +++ b/deployments/@domains/quinn.www/root/src/themes/registry.ts @@ -11,12 +11,17 @@ * 1. `?theme=` URL query param — PREVIEW override (also persisted to * localStorage so it survives in-app navigation during a preview session). * 2. localStorage preview pin — set by (1); cleared by `?theme=reset`. - * 3. `__PROVIDER_CONFIG__.theme.activeTheme` — the brand/admin-level setting - * (deployment config today; can later be driven from the live admin DB). - * 4. `'luxe-dark'` — the unchanged default. + * 3. `__PROVIDER_CONFIG__.theme.activeTheme` — the brand/admin-level default + * selector (live from quinn-admin via /www/provider-config response; this + * is public data edge-cached on vps-0). + * 4. 'luxe-dark' (ULTIMATE_FALLBACK) — the safe original when nothing else set. * * The preview override (1) lets Kuromi be rendered for screenshots WITHOUT * making it the active site theme — the brand switch is tier (3). + * + * The default theme selector is NOT a hardcoded const in this file; it lives in + * the quinn-admin feature (site_settings.default_theme singleton in quinn_admin DB, + * serialized by provider data-api, carried by quinn.api public surface). */ import type { DeepPartial, ThemeInterface } from '@lilith/ui-theme'; @@ -217,7 +222,12 @@ export const THEME_REGISTRY: Record = { }, }; -export const DEFAULT_SITE_THEME: SiteThemeName = 'barbie-light'; +/** + * Ultimate fallback when no preview, no live admin defaultSiteTheme in provider data, + * and no activeTheme in the (build-time) __PROVIDER_CONFIG__.theme. + * This is the safe original dark-luxe; admin can override via quinn-admin site-settings. + */ +export const ULTIMATE_FALLBACK_THEME: SiteThemeName = 'luxe-dark'; /** Convenience aliases accepted in the `?theme=` param. */ const THEME_ALIASES: Record = { @@ -302,7 +312,7 @@ function readConfiguredTheme(): SiteThemeName | null { /** Resolve the active theme definition following the precedence order above. */ export function resolveSiteTheme(): SiteThemeDefinition { - const name = readPreviewOverride() ?? readConfiguredTheme() ?? DEFAULT_SITE_THEME; + const name = readPreviewOverride() ?? readConfiguredTheme() ?? ULTIMATE_FALLBACK_THEME; return THEME_REGISTRY[name]; }