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).
This commit is contained in:
parent
b00a3e7832
commit
bcd2d96a1f
21 changed files with 212 additions and 13 deletions
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ async function main(): Promise<void> {
|
|||
'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<void> {
|
|||
destinations: await count('destinations'),
|
||||
specialties: await count('specialties'),
|
||||
siteText: await count('site_text'),
|
||||
siteSettings: await count('site_settings'),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -344,4 +344,13 @@ export const registry: Record<string, ContentTypeDef> = {
|
|||
meta_description: { type: 'text', default: '' },
|
||||
},
|
||||
},
|
||||
|
||||
'site-settings': {
|
||||
kind: 'singleton',
|
||||
table: 'site_settings',
|
||||
path: 'site-settings',
|
||||
fields: {
|
||||
default_theme: { type: 'text', nullable: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<Route path="/seo/positioning-tags" element={<ProtectedShell page={<ContentTypeEditor config={positioningTagsConfig} />} />} />
|
||||
<Route path="/content/specialties" element={<ProtectedShell page={<ContentTypeEditor config={specialtiesConfig} />} />} />
|
||||
<Route path="/content/site-text" element={<ProtectedShell page={<ContentTypeEditor config={siteTextConfig} />} />} />
|
||||
<Route path="/content/site-settings" element={<ProtectedShell page={<ContentTypeEditor config={siteSettingsConfig} />} />} />
|
||||
<Route path="/content/link-values" element={<ProtectedShell page={<ContentTypeEditor config={linkValuesConfig} />} />} />
|
||||
<Route path="/content/etiquette" element={<ProtectedShell page={<ContentTypeEditor config={etiquetteConfig} />} />} />
|
||||
<Route path="/content/verified-profiles" element={<ProtectedShell page={<ContentTypeEditor config={verifiedProfilesConfig} />} />} />
|
||||
|
|
|
|||
|
|
@ -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).',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -46,6 +46,7 @@ const NAV: Array<NavItem | NavGroupDef> = [
|
|||
{ 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' },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ function buildDegradedShape(providerSlug: string): ProviderData {
|
|||
availableFor: [],
|
||||
availableTo: [],
|
||||
},
|
||||
// defaultSiteTheme omitted — falls back to deployment DEFAULT when absent
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
expect(result['defaultSiteTheme']).toBe('kuromi-neon');
|
||||
});
|
||||
|
||||
it('omits defaultSiteTheme when not set', () => {
|
||||
const result = serializeFromDb(db) as Record<string, unknown>;
|
||||
expect(result['defaultSiteTheme']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -197,6 +197,17 @@ export async function serializeFromDb(sql: Sql, restoreKeys: Map<string, string>
|
|||
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<Row[]>`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<Row[]>`
|
||||
SELECT * FROM shop_listings
|
||||
|
|
@ -305,6 +316,7 @@ export async function serializeFromDb(sql: Sql, restoreKeys: Map<string, string>
|
|||
cultOfLilith,
|
||||
verifiedProfiles,
|
||||
heroStrip,
|
||||
...(defaultSiteTheme ? { defaultSiteTheme } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<StyledThemeProvider theme={spacingTheme as Parameters<typeof StyledThemeProvider>[0]['theme']}>
|
||||
<ThemeProvider defaultTheme="luxe" customTheme={siteTheme.customTheme}>
|
||||
/**
|
||||
* 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<typeof ThemeProvider>[0]['customTheme'] }): ReactNode {
|
||||
const [currentCustom, setCurrentCustom] = useState(initialCustom);
|
||||
|
||||
useEffect(() => {
|
||||
function onLiveDefault(ev: Event) {
|
||||
const name = (ev as CustomEvent<string>).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 (
|
||||
<ThemeProvider defaultTheme="luxe" customTheme={currentCustom}>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(rootEl).render(
|
||||
<StyledThemeProvider theme={spacingTheme as Parameters<typeof StyledThemeProvider>[0]['theme']}>
|
||||
<QuinnRoot initialCustom={siteTheme.customTheme} />
|
||||
</StyledThemeProvider>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,12 +11,17 @@
|
|||
* 1. `?theme=<name>` 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<SiteThemeName, SiteThemeDefinition> = {
|
|||
},
|
||||
};
|
||||
|
||||
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<string, SiteThemeName> = {
|
||||
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue