@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.
- package/package.json +60 -0
- package/src/catalog.ts +313 -0
- package/src/components/ColorInput.svelte +150 -0
- package/src/components/ColorInput.svelte.d.ts +14 -0
- package/src/components/FontSizeControl.svelte +138 -0
- package/src/components/FontSizeControl.svelte.d.ts +16 -0
- package/src/components/ThemeCustomizer.svelte +359 -0
- package/src/components/ThemeCustomizer.svelte.d.ts +14 -0
- package/src/components/ThemePicker.svelte +129 -0
- package/src/components/ThemePicker.svelte.d.ts +13 -0
- package/src/components/ThemeSwatch.svelte +136 -0
- package/src/components/ThemeSwatch.svelte.d.ts +14 -0
- package/src/css.ts +324 -0
- package/src/derive.ts +110 -0
- package/src/index.ts +63 -0
- package/src/oklch.ts +299 -0
- package/src/resolve.ts +103 -0
- package/src/stores/font-size.svelte.ts +158 -0
- package/src/stores/theme.svelte.ts +320 -0
- package/src/tailwind/tokens.css +57 -0
- package/src/types.ts +217 -0
- package/tests/catalog.test.ts +199 -0
- package/tests/css.test.ts +250 -0
- package/tests/oklch.test.ts +256 -0
- package/tests/resolve.test.ts +192 -0
- package/tsconfig.base.json +21 -0
- package/tsconfig.json +4 -0
- package/vitest.config.ts +14 -0
|
@@ -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';
|