@soulcraft/theme 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ /**
2
+ * @module stores/theme.svelte
3
+ * @description Product-aware Svelte 5 theme store for all Soulcraft products.
4
+ *
5
+ * Manages theme selection, persistence, and CSS custom property injection across
6
+ * the 4-level cascade (catalog → kit → custom → user). Supports an optional
7
+ * separate editor theme for Workshop's syntax highlighting use case.
8
+ *
9
+ * Product modes:
10
+ * - `'workshop'` — emits --theme-* vars + Workshop backward-compat aliases (--bg-dark, etc.)
11
+ * Also supports `editorThemeId` for separate Monaco/Shiki themes.
12
+ * - `'venue'` — client-side (rare; Venue normally uses SSR injection from hooks.server.ts).
13
+ * Emits --theme-* vars + Venue brand aliases (--brand-primary, etc.).
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // Workshop (src/stores/themeStore.svelte.ts):
18
+ * import { createThemeStore } from '@soulcraft/theme/stores/theme.svelte';
19
+ * export const themeStore = createThemeStore({ product: 'workshop' });
20
+ * ```
21
+ */
22
+
23
+ import { CATALOG_BY_ID, getThemesGrouped } from '../catalog.js';
24
+ import { buildThemeCss, buildWorkshopAliasCss, buildBrandAliasCss, buildWorkshopDerivedCss } from '../css.js';
25
+ import { oklchToHex } from '../oklch.js';
26
+ import type { ThemeDefinition, ThemeName } from '../types.js';
27
+
28
+ /** Browser detection without SvelteKit's $app/environment dependency. */
29
+ const isBrowser = typeof window !== 'undefined';
30
+
31
+ /** Product types supported by the ThemeStore. */
32
+ export type ThemeProduct = 'workshop' | 'venue' | 'skillvill' | 'portal';
33
+
34
+ /** Options for creating a ThemeStore instance. */
35
+ export interface ThemeStoreOptions {
36
+ /** Which product context this store serves. Determines which CSS aliases are emitted. */
37
+ product: ThemeProduct;
38
+ /** localStorage key for persisting the selected theme. @default 'appTheme' */
39
+ storageKey?: string;
40
+ /** ID of the default catalog theme. @default 'soulcraft-dark' */
41
+ defaultThemeId?: ThemeName;
42
+ }
43
+
44
+ /**
45
+ * Product-aware theme store using Svelte 5 runes.
46
+ *
47
+ * Manages the current catalog theme (plus an optional separate editor theme for
48
+ * Workshop), persists selection to localStorage, and applies `--theme-*` CSS custom
49
+ * properties to `document.documentElement` on every theme change.
50
+ */
51
+ class ThemeStore {
52
+ /** Currently active application theme ID. */
53
+ currentThemeId = $state<ThemeName>('soulcraft-dark');
54
+ /**
55
+ * Optional separate editor theme ID.
56
+ * When set, Monaco and Shiki use this instead of `currentThemeId`.
57
+ * Workshop-specific; other products leave this null.
58
+ */
59
+ editorThemeId = $state<ThemeName | null>(null);
60
+
61
+ /** Convenience alias matching Workshop's legacy `currentTheme` property name. */
62
+ get currentTheme(): ThemeName { return this.currentThemeId; }
63
+ /** Convenience alias matching Workshop's legacy `editorTheme` property name. */
64
+ get editorTheme(): ThemeName | null { return this.editorThemeId; }
65
+
66
+ constructor(private options: ThemeStoreOptions) {
67
+ if (!isBrowser) return;
68
+
69
+ const key = options.storageKey ?? 'appTheme';
70
+ const saved = localStorage.getItem(key);
71
+ if (saved && CATALOG_BY_ID.has(saved)) {
72
+ this.currentThemeId = saved as ThemeName;
73
+ this.applyTheme(this.currentThemeId);
74
+ } else {
75
+ const fallback = options.defaultThemeId ?? 'soulcraft-dark';
76
+ this.currentThemeId = fallback;
77
+ this.applyTheme(fallback);
78
+ }
79
+
80
+ const savedEditor = localStorage.getItem(`${key}:editor`);
81
+ if (savedEditor && CATALOG_BY_ID.has(savedEditor)) {
82
+ this.editorThemeId = savedEditor as ThemeName;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check whether a string is a valid catalog theme ID.
88
+ *
89
+ * @param id - The string to validate.
90
+ * @returns True if `id` is a known catalog theme ID.
91
+ */
92
+ isValidTheme(id: string): boolean {
93
+ return CATALOG_BY_ID.has(id);
94
+ }
95
+
96
+ /**
97
+ * Get the ThemeDefinition for a given theme ID.
98
+ *
99
+ * @param id - A catalog theme ID.
100
+ * @returns The ThemeDefinition, or null if not found.
101
+ */
102
+ getTheme(id: string): ThemeDefinition | null {
103
+ return CATALOG_BY_ID.get(id) ?? null;
104
+ }
105
+
106
+ /**
107
+ * Get the effective editor theme ID.
108
+ * Returns `editorThemeId` if set, otherwise falls back to `currentThemeId`.
109
+ *
110
+ * @returns The theme ID to use for Monaco/Shiki rendering.
111
+ */
112
+ getEffectiveEditorTheme(): ThemeName {
113
+ return this.editorThemeId ?? this.currentThemeId;
114
+ }
115
+
116
+ /**
117
+ * Set the application theme and persist to localStorage.
118
+ *
119
+ * @param id - A valid catalog theme ID.
120
+ */
121
+ setTheme(id: ThemeName): void {
122
+ this.currentThemeId = id;
123
+ this.applyTheme(id);
124
+
125
+ if (isBrowser) {
126
+ const key = this.options.storageKey ?? 'appTheme';
127
+ localStorage.setItem(key, id);
128
+ window.dispatchEvent(new CustomEvent('themeChanged', {
129
+ detail: { appTheme: id, editorTheme: this.getEffectiveEditorTheme() }
130
+ }));
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Set the editor theme override and persist to localStorage.
136
+ * Pass null to clear the override and use the app theme.
137
+ *
138
+ * @param id - A valid catalog theme ID, or null to clear.
139
+ */
140
+ setEditorTheme(id: ThemeName | null): void {
141
+ this.editorThemeId = id;
142
+
143
+ if (isBrowser) {
144
+ const key = `${this.options.storageKey ?? 'appTheme'}:editor`;
145
+ if (id) {
146
+ localStorage.setItem(key, id);
147
+ } else {
148
+ localStorage.removeItem(key);
149
+ }
150
+ window.dispatchEvent(new CustomEvent('themeChanged', {
151
+ detail: { appTheme: this.currentThemeId, editorTheme: this.getEffectiveEditorTheme() }
152
+ }));
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Return all available theme IDs in catalog order.
158
+ *
159
+ * @returns Array of all 23 theme IDs.
160
+ */
161
+ getAvailableThemes(): ThemeName[] {
162
+ return Array.from(CATALOG_BY_ID.keys()) as ThemeName[];
163
+ }
164
+
165
+ /**
166
+ * Return the ThemeDefinition for the current active theme.
167
+ *
168
+ * @returns The current ThemeDefinition.
169
+ */
170
+ getCurrentTheme(): ThemeDefinition {
171
+ return CATALOG_BY_ID.get(this.currentThemeId)!;
172
+ }
173
+
174
+ /**
175
+ * Get themes grouped by category for rendering the picker UI.
176
+ * Equivalent to Workshop's legacy `getThemesByCategory()`.
177
+ *
178
+ * @returns Object with `soulcraft`, `dark`, and `light` arrays.
179
+ */
180
+ getThemesByCategory(): Record<string, ThemeDefinition[]> {
181
+ return getThemesGrouped();
182
+ }
183
+
184
+ // ─── Internal CSS injection ─────────────────────────────────────────────
185
+
186
+ /**
187
+ * Apply a theme by setting CSS custom properties on `document.documentElement`.
188
+ *
189
+ * Sets `--theme-*` variables plus product-specific backward-compat aliases.
190
+ * Also updates `document.body` background and color for immediate visual feedback.
191
+ *
192
+ * @param id - The catalog theme ID to apply.
193
+ */
194
+ applyTheme(id: ThemeName): void {
195
+ if (!isBrowser) return;
196
+ const theme = CATALOG_BY_ID.get(id);
197
+ if (!theme) return;
198
+
199
+ const root = document.documentElement;
200
+ const body = document.body;
201
+ const { colors } = theme;
202
+
203
+ // ── Core --theme-* vars ───────────────────────────────────────────────
204
+ root.style.setProperty('--theme-bg-base', colors.bgBase);
205
+ root.style.setProperty('--theme-bg-surface', colors.bgSurface);
206
+ root.style.setProperty('--theme-bg-elevated', colors.bgElevated);
207
+ root.style.setProperty('--theme-text-primary', colors.textPrimary);
208
+ root.style.setProperty('--theme-text-secondary', colors.textSecondary);
209
+ root.style.setProperty('--theme-primary', colors.primary);
210
+ root.style.setProperty('--theme-primary-light', colors.primaryLight);
211
+ root.style.setProperty('--theme-primary-dark', colors.primaryDark);
212
+ root.style.setProperty('--theme-accent', colors.accent);
213
+ root.style.setProperty('--theme-accent-light', colors.accentLight);
214
+ root.style.setProperty('--theme-glass', colors.glass);
215
+ root.style.setProperty('--theme-glass-border', colors.glassBorder);
216
+ root.style.setProperty('--theme-success', colors.success);
217
+ root.style.setProperty('--theme-warning', colors.warning);
218
+ root.style.setProperty('--theme-error', colors.error);
219
+ root.style.setProperty('--theme-font-display', theme.fonts.displayFont);
220
+ root.style.setProperty('--theme-font-body', theme.fonts.bodyFont);
221
+
222
+ // ── Product-specific backward-compat aliases ──────────────────────────
223
+ if (this.options.product === 'workshop') {
224
+ // Legacy Workshop vars — --bg-dark, --primary, --glass-border, etc.
225
+ root.style.setProperty('--bg-dark', colors.bgBase);
226
+ root.style.setProperty('--bg-medium', colors.bgSurface);
227
+ root.style.setProperty('--bg-light', colors.bgElevated);
228
+ root.style.setProperty('--text-primary', colors.textPrimary);
229
+ root.style.setProperty('--text-secondary', colors.textSecondary);
230
+ root.style.setProperty('--primary', colors.primary);
231
+ root.style.setProperty('--primary-light', colors.primaryLight);
232
+ root.style.setProperty('--primary-dark', colors.primaryDark);
233
+ root.style.setProperty('--accent', colors.accent);
234
+ root.style.setProperty('--accent-light', colors.accentLight);
235
+ root.style.setProperty('--glass', colors.glass);
236
+ root.style.setProperty('--glass-border', colors.glassBorder);
237
+ root.style.setProperty('--success', colors.success);
238
+ root.style.setProperty('--warning', colors.warning);
239
+ root.style.setProperty('--error', colors.error);
240
+ // Semantic surface aliases
241
+ root.style.setProperty('--surface-primary', colors.bgBase);
242
+ root.style.setProperty('--surface-secondary', colors.bgSurface);
243
+ root.style.setProperty('--surface-tertiary', colors.bgElevated);
244
+ root.style.setProperty('--surface-hover', colors.bgElevated);
245
+ root.style.setProperty('--border-subtle', colors.glassBorder);
246
+ root.style.setProperty('--accent-muted', colors.primaryLight);
247
+ root.style.setProperty('--accent-subtle', colors.primary.replace(/\)$/, ' / 0.13)').replace('oklch(', 'oklch('));
248
+ root.style.setProperty('--accent-hover', colors.primaryDark);
249
+ root.style.setProperty('--text-muted', colors.textSecondary);
250
+ root.style.setProperty('--success-subtle', colors.success.replace(/\)$/, ' / 0.13)').replace('oklch(', 'oklch('));
251
+ } else if (this.options.product === 'venue') {
252
+ // Legacy Venue brand vars
253
+ root.style.setProperty('--brand-primary', colors.primary);
254
+ root.style.setProperty('--brand-background', colors.bgBase);
255
+ root.style.setProperty('--brand-accent', colors.accent);
256
+ root.style.setProperty('--brand-text', colors.textPrimary);
257
+ root.style.setProperty('--brand-font-display', theme.fonts.displayFont);
258
+ root.style.setProperty('--brand-font-body', theme.fonts.bodyFont);
259
+ }
260
+
261
+ // ── Immediate body visual feedback ────────────────────────────────────
262
+ if (body) {
263
+ body.style.background = `linear-gradient(135deg, ${colors.bgBase} 0%, ${colors.bgSurface} 50%, ${colors.bgBase} 100%)`;
264
+ body.style.color = colors.textPrimary;
265
+ }
266
+
267
+ // Force CSS recalculation by toggling a timestamp var
268
+ root.style.setProperty('--theme-timestamp', Date.now().toString());
269
+ }
270
+
271
+ /**
272
+ * Get the oklch colors as a hex color map for Shiki/Monaco token registration.
273
+ *
274
+ * Shiki requires hex color strings in its VS Code theme format. This method
275
+ * converts the current theme's oklch colors to hex for that bridging use case.
276
+ *
277
+ * @param id - Theme ID to convert. Defaults to current theme.
278
+ * @returns Object mapping semantic color names to hex strings.
279
+ */
280
+ getHexColorsForShiki(id?: ThemeName): Record<string, string> | null {
281
+ const theme = CATALOG_BY_ID.get(id ?? this.currentThemeId);
282
+ if (!theme) return null;
283
+ const { colors } = theme;
284
+ return {
285
+ bgBase: oklchToHex(colors.bgBase),
286
+ bgElevated: oklchToHex(colors.bgElevated),
287
+ textPrimary: oklchToHex(colors.textPrimary),
288
+ textSecondary: oklchToHex(colors.textSecondary),
289
+ primary: oklchToHex(colors.primary),
290
+ primaryLight: oklchToHex(colors.primaryLight),
291
+ accent: oklchToHex(colors.accent),
292
+ accentLight: oklchToHex(colors.accentLight),
293
+ success: oklchToHex(colors.success),
294
+ warning: oklchToHex(colors.warning),
295
+ error: oklchToHex(colors.error),
296
+ };
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Create a product-aware ThemeStore instance.
302
+ *
303
+ * @param options - Store configuration including product type and optional keys.
304
+ * @returns A reactive Svelte 5 ThemeStore instance.
305
+ *
306
+ * @example
307
+ * ```ts
308
+ * // Workshop:
309
+ * export const themeStore = createThemeStore({ product: 'workshop' });
310
+ *
311
+ * // Venue client-side (rare):
312
+ * export const themeStore = createThemeStore({ product: 'venue' });
313
+ * ```
314
+ */
315
+ export function createThemeStore(options: ThemeStoreOptions): ThemeStore {
316
+ return new ThemeStore(options);
317
+ }
318
+
319
+ export type { ThemeStore };
320
+ export type { ThemeDefinition, ThemeName };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @soulcraft/theme — Shared Tailwind CSS 4 design tokens.
3
+ *
4
+ * Defines the @theme block that bridges runtime CSS custom properties
5
+ * (--theme-*) to Tailwind utility classes (bg-theme-base, text-theme-text, etc.).
6
+ *
7
+ * The --theme-* variables are set at runtime by:
8
+ * - ThemeStore.applyTheme() (client-side, Workshop)
9
+ * - hooks.server.ts themeInjector (SSR, Venue)
10
+ *
11
+ * Import this file in app.css alongside Tailwind:
12
+ * @import '@soulcraft/theme/tailwind/tokens.css';
13
+ * @import 'tailwindcss';
14
+ *
15
+ * This generates Tailwind utilities:
16
+ * bg-theme-base bg-theme-surface bg-theme-elevated
17
+ * text-theme-text text-theme-text-muted
18
+ * bg-theme-primary bg-theme-primary-light bg-theme-primary-dark
19
+ * bg-theme-accent bg-theme-accent-light
20
+ * bg-theme-glass border-theme-glass-border
21
+ * text-theme-success text-theme-warning text-theme-error
22
+ * font-display font-body font-mono
23
+ */
24
+
25
+ @theme {
26
+ /* ── Background scale ───────────────────────────────────────────────────── */
27
+ --color-theme-base: var(--theme-bg-base);
28
+ --color-theme-surface: var(--theme-bg-surface);
29
+ --color-theme-elevated: var(--theme-bg-elevated);
30
+
31
+ /* ── Text scale ─────────────────────────────────────────────────────────── */
32
+ --color-theme-text: var(--theme-text-primary);
33
+ --color-theme-text-muted: var(--theme-text-secondary);
34
+
35
+ /* ── Brand: primary ─────────────────────────────────────────────────────── */
36
+ --color-theme-primary: var(--theme-primary);
37
+ --color-theme-primary-light: var(--theme-primary-light);
38
+ --color-theme-primary-dark: var(--theme-primary-dark);
39
+
40
+ /* ── Brand: accent ──────────────────────────────────────────────────────── */
41
+ --color-theme-accent: var(--theme-accent);
42
+ --color-theme-accent-light: var(--theme-accent-light);
43
+
44
+ /* ── Glass / overlay ────────────────────────────────────────────────────── */
45
+ --color-theme-glass: var(--theme-glass);
46
+ --color-theme-glass-border: var(--theme-glass-border);
47
+
48
+ /* ── Semantic status (same across all themes) ───────────────────────────── */
49
+ --color-theme-success: var(--theme-success);
50
+ --color-theme-warning: var(--theme-warning);
51
+ --color-theme-error: var(--theme-error);
52
+
53
+ /* ── Typography ─────────────────────────────────────────────────────────── */
54
+ --font-display: var(--theme-font-display, system-ui), serif;
55
+ --font-body: var(--theme-font-body, system-ui), ui-sans-serif, system-ui, sans-serif;
56
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
57
+ }
package/src/types.ts ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * @module types
3
+ * @description Core TypeScript types for the @soulcraft/theme design system.
4
+ *
5
+ * Provides the unified ThemeColors interface that reconciles Workshop's 15-property
6
+ * hex/rgba palette with Venue's 6-property oklch VenueKitTheme. All color values
7
+ * are oklch strings for perceptually uniform palette operations.
8
+ *
9
+ * The type system has three levels of abstraction:
10
+ * - `ThemeSeed` — minimal 2-6 properties; kit authors write this
11
+ * - `ThemeColors` — the full 15-property palette; derived from a seed or defined directly
12
+ * - `ThemeDefinition` — colors + fonts + metadata; what the catalog stores
13
+ */
14
+
15
+ /** An OKLCH color string as produced by {@link formatOklch} or CSS Color 4. */
16
+ export type OklchColor = string;
17
+
18
+ /**
19
+ * Theme category for grouping in the ThemePicker UI.
20
+ * Matches Workshop's existing getThemesByCategory() groupings.
21
+ */
22
+ export type ThemeCategory = 'soulcraft' | 'dark' | 'light';
23
+
24
+ /**
25
+ * The full 15-property unified color palette.
26
+ *
27
+ * Reconciles Workshop's `ThemeColors` (bgDark/bgMedium/bgLight naming) with
28
+ * Venue's `VenueKitTheme` (background/primary/accent naming). All values are
29
+ * OKLCH strings compatible with CSS Color 4 and Tailwind CSS 4.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const colors: ThemeColors = {
34
+ * bgBase: 'oklch(0.052 0.023 261.6)',
35
+ * bgSurface: 'oklch(0.092 0.031 261.4)',
36
+ * bgElevated: 'oklch(0.143 0.022 251.7)',
37
+ * textPrimary: 'oklch(0.951 0.010 251.1)',
38
+ * textSecondary: 'oklch(0.659 0.020 261.4)',
39
+ * primary: 'oklch(0.624 0.082 181.4)',
40
+ * primaryLight: 'oklch(0.742 0.094 181.4)',
41
+ * primaryDark: 'oklch(0.316 0.053 211.0)',
42
+ * accent: 'oklch(0.656 0.127 39.8)',
43
+ * accentLight: 'oklch(0.755 0.107 55.4)',
44
+ * glass: 'oklch(1 0 0 / 0.05)',
45
+ * glassBorder: 'oklch(1 0 0 / 0.10)',
46
+ * success: 'oklch(0.698 0.158 145.3)',
47
+ * warning: 'oklch(0.762 0.161 76.8)',
48
+ * error: 'oklch(0.628 0.200 25.6)',
49
+ * }
50
+ * ```
51
+ */
52
+ export interface ThemeColors {
53
+ /** Deepest background. Workshop: bgDark. Venue: background. Tailwind: bg-theme-base. */
54
+ bgBase: OklchColor;
55
+ /** Mid-level surface. Workshop: bgMedium. Tailwind: bg-theme-surface. */
56
+ bgSurface: OklchColor;
57
+ /** Raised card/panel surface. Workshop: bgLight. Tailwind: bg-theme-elevated. */
58
+ bgElevated: OklchColor;
59
+
60
+ /** Primary text. Workshop: textPrimary. Venue: text. Tailwind: text-theme-text. */
61
+ textPrimary: OklchColor;
62
+ /** Secondary/muted text. Workshop: textSecondary. Tailwind: text-theme-text-muted. */
63
+ textSecondary: OklchColor;
64
+
65
+ /** Brand primary color. Workshop: primary. Venue: primary. Tailwind: bg-theme-primary. */
66
+ primary: OklchColor;
67
+ /** Lighter primary shade. Workshop: primaryLight. Tailwind: bg-theme-primary-light. */
68
+ primaryLight: OklchColor;
69
+ /** Darker primary shade. Workshop: primaryDark. Tailwind: bg-theme-primary-dark. */
70
+ primaryDark: OklchColor;
71
+
72
+ /** Accent/secondary brand color. Workshop: accent. Venue: accent. Tailwind: bg-theme-accent. */
73
+ accent: OklchColor;
74
+ /** Lighter accent shade. Workshop: accentLight. Tailwind: bg-theme-accent-light. */
75
+ accentLight: OklchColor;
76
+
77
+ /** Glass background for overlays. Workshop: glass. Tailwind: bg-theme-glass. */
78
+ glass: OklchColor;
79
+ /** Glass border for overlays. Workshop: glassBorder. Tailwind: border-theme-glass-border. */
80
+ glassBorder: OklchColor;
81
+
82
+ /** Success/positive semantic color. Tailwind: text-theme-success. */
83
+ success: OklchColor;
84
+ /** Warning/caution semantic color. Tailwind: text-theme-warning. */
85
+ warning: OklchColor;
86
+ /** Error/destructive semantic color. Tailwind: text-theme-error. */
87
+ error: OklchColor;
88
+ }
89
+
90
+ /**
91
+ * Font configuration for display headings and body text.
92
+ * Font names are Google Font names or CSS system font stack keywords.
93
+ */
94
+ export interface ThemeFonts {
95
+ /**
96
+ * Google Font name or system font keyword for headings.
97
+ * @example 'Fraunces' | 'system-ui' | 'Inter'
98
+ */
99
+ displayFont: string;
100
+ /**
101
+ * Google Font name or system font keyword for body text.
102
+ * @example 'Inter' | 'system-ui'
103
+ */
104
+ bodyFont: string;
105
+ }
106
+
107
+ /**
108
+ * Metadata describing a theme's identity and rendering mode.
109
+ */
110
+ export interface ThemeMeta {
111
+ /** Stable kebab-case identifier. @example 'soulcraft-dark' */
112
+ id: string;
113
+ /** Human-readable display name. @example 'Soulcraft Dark' */
114
+ name: string;
115
+ /** Category for grouping in the picker UI. */
116
+ category: ThemeCategory;
117
+ /** Whether this is a dark-mode theme (used for semantic status defaults). */
118
+ isDark: boolean;
119
+ }
120
+
121
+ /**
122
+ * A complete, fully-resolved theme: metadata + all 15 colors + fonts.
123
+ * This is what the catalog stores and what `resolveTheme()` returns.
124
+ */
125
+ export interface ThemeDefinition {
126
+ /** Theme identity and rendering metadata. */
127
+ meta: ThemeMeta;
128
+ /** The full 15-property color palette (all OKLCH). */
129
+ colors: ThemeColors;
130
+ /** Display and body fonts for this theme. */
131
+ fonts: ThemeFonts;
132
+ }
133
+
134
+ /**
135
+ * Minimal input for kit authors to define a brand theme.
136
+ *
137
+ * Only `primary` and `bgBase` are required. All other properties are auto-derived
138
+ * by `deriveFullPalette()` using oklch arithmetic — surface levels from bgBase lightness,
139
+ * text from bgBase contrast, light/dark primary shades from hue, etc.
140
+ *
141
+ * Maps to `VenueKitTheme` in kit-schema. Kit authors specify this in `kit.json`
142
+ * under `venue.theme`.
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * const seed: ThemeSeed = {
147
+ * primary: 'oklch(0.72 0.15 25)', // Rose brand color
148
+ * bgBase: 'oklch(0.96 0.02 80)', // Warm cream background
149
+ * accent: 'oklch(0.80 0.12 75)', // Amber accent
150
+ * displayFont: 'Fraunces',
151
+ * bodyFont: 'Inter',
152
+ * };
153
+ * ```
154
+ */
155
+ export interface ThemeSeed {
156
+ /** Primary brand color. Required. */
157
+ primary: OklchColor;
158
+ /** Deepest background color. Required. */
159
+ bgBase: OklchColor;
160
+ /** Accent color. Derived from primary hue+150 if absent. */
161
+ accent?: OklchColor;
162
+ /** Primary text color. Derived from bgBase contrast if absent. */
163
+ textPrimary?: OklchColor;
164
+ /** Display/heading Google Font name. @default 'system-ui' */
165
+ displayFont?: string;
166
+ /** Body Google Font name. @default 'Inter' */
167
+ bodyFont?: string;
168
+ }
169
+
170
+ /**
171
+ * Input to the 4-level theme cascade resolver.
172
+ *
173
+ * Priority order (highest wins):
174
+ * 1. `catalogId` — base catalog theme
175
+ * 2. `kitSeed` — kit brand colors merged on top
176
+ * 3. `customOverrides` — admin-edited color/font overrides
177
+ * 4. `userThemeId` — end-user picks a catalog theme (overrides all if present)
178
+ */
179
+ export interface ThemeCascadeInput {
180
+ /** Base catalog theme ID. Defaults to 'soulcraft-dark'. */
181
+ catalogId?: string;
182
+ /** Kit brand seed — derives a full palette and merges on top of the base. */
183
+ kitSeed?: ThemeSeed;
184
+ /** Admin-level overrides for specific colors or fonts. */
185
+ customOverrides?: Partial<ThemeColors & ThemeFonts>;
186
+ /** End-user theme preference. If set and valid, used instead of the cascade result. */
187
+ userThemeId?: string;
188
+ }
189
+
190
+ /**
191
+ * All 23 catalog theme IDs shipped with the package.
192
+ * Exported for type-safe theme name references in Workshop and other products.
193
+ */
194
+ export type ThemeName =
195
+ | 'soulcraft-dark'
196
+ | 'soulcraft-light'
197
+ | 'tokyo-night'
198
+ | 'catppuccin-mocha'
199
+ | 'gruvbox-dark'
200
+ | 'material-theme-darker'
201
+ | 'material-theme-palenight'
202
+ | 'ayu-dark'
203
+ | 'synthwave-84'
204
+ | 'one-dark-pro'
205
+ | 'dracula'
206
+ | 'nord'
207
+ | 'monokai'
208
+ | 'night-owl'
209
+ | 'solarized-dark'
210
+ | 'github-dark'
211
+ | 'github-dark-dimmed'
212
+ | 'catppuccin-latte'
213
+ | 'gruvbox-light'
214
+ | 'solarized-light'
215
+ | 'github-light'
216
+ | 'one-light'
217
+ | 'material-theme-lighter';