@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
package/src/css.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module css
|
|
3
|
+
* @description CSS custom property builders for theme injection.
|
|
4
|
+
*
|
|
5
|
+
* Generates CSS strings for `--theme-*` custom properties (the new unified vars),
|
|
6
|
+
* plus backward-compatible alias strings for both Workshop and Venue.
|
|
7
|
+
*
|
|
8
|
+
* **Workshop aliases** — maps `--bg-dark`, `--primary`, etc. to `--theme-*` vars.
|
|
9
|
+
* **Venue aliases** — maps `--brand-primary`, `--brand-background`, etc. to `--theme-*` vars.
|
|
10
|
+
*
|
|
11
|
+
* Also provides `buildFontUrl()` (ported from Venue's `theme.ts`), the Google Fonts
|
|
12
|
+
* URL builder with special handling for Fraunces's optical-size + WONK axes.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* // Server-side injection (Venue hooks.server.ts):
|
|
17
|
+
* const css = buildThemeCss(theme) + buildBrandAliasCss();
|
|
18
|
+
* // Inject into <html style="...">
|
|
19
|
+
*
|
|
20
|
+
* // Client-side injection (Workshop ThemeStore.applyTheme):
|
|
21
|
+
* const css = buildThemeCss(theme) + buildWorkshopAliasCss();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { withAlpha } from './oklch.js';
|
|
26
|
+
import type { ThemeDefinition } from './types.js';
|
|
27
|
+
|
|
28
|
+
// ─── Core --theme-* properties ───────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a CSS custom property string for the unified `--theme-*` variables.
|
|
32
|
+
*
|
|
33
|
+
* Suitable for injection as `document.documentElement.style.cssText +=` (client),
|
|
34
|
+
* or as an inline `style="..."` attribute on `<html>` (SSR).
|
|
35
|
+
*
|
|
36
|
+
* @param theme - The fully-resolved theme definition.
|
|
37
|
+
* @returns Semicolon-separated `--theme-*:value` declarations with no whitespace.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* buildThemeCss(theme);
|
|
42
|
+
* // '--theme-bg-base:oklch(0.052 0.023 261.6);--theme-bg-surface:oklch(0.092 0.031 261.4);...'
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function buildThemeCss(theme: ThemeDefinition): string {
|
|
46
|
+
const { colors, fonts } = theme;
|
|
47
|
+
return [
|
|
48
|
+
`--theme-bg-base:${colors.bgBase}`,
|
|
49
|
+
`--theme-bg-surface:${colors.bgSurface}`,
|
|
50
|
+
`--theme-bg-elevated:${colors.bgElevated}`,
|
|
51
|
+
`--theme-text-primary:${colors.textPrimary}`,
|
|
52
|
+
`--theme-text-secondary:${colors.textSecondary}`,
|
|
53
|
+
`--theme-primary:${colors.primary}`,
|
|
54
|
+
`--theme-primary-light:${colors.primaryLight}`,
|
|
55
|
+
`--theme-primary-dark:${colors.primaryDark}`,
|
|
56
|
+
`--theme-accent:${colors.accent}`,
|
|
57
|
+
`--theme-accent-light:${colors.accentLight}`,
|
|
58
|
+
`--theme-glass:${colors.glass}`,
|
|
59
|
+
`--theme-glass-border:${colors.glassBorder}`,
|
|
60
|
+
`--theme-success:${colors.success}`,
|
|
61
|
+
`--theme-warning:${colors.warning}`,
|
|
62
|
+
`--theme-error:${colors.error}`,
|
|
63
|
+
`--theme-font-display:${fonts.displayFont}`,
|
|
64
|
+
`--theme-font-body:${fonts.bodyFont}`,
|
|
65
|
+
].join(';');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Workshop backward-compat aliases ────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build CSS alias declarations mapping Workshop's legacy `--bg-dark`, `--primary`,
|
|
72
|
+
* etc. variables to the new `--theme-*` variables.
|
|
73
|
+
*
|
|
74
|
+
* Inject this alongside `buildThemeCss()` to maintain backward compatibility with
|
|
75
|
+
* all existing Workshop CSS that uses `var(--bg-dark)`, `var(--primary)`, etc.
|
|
76
|
+
*
|
|
77
|
+
* This is a static string (no theme parameter needed) because the aliases always
|
|
78
|
+
* reference `var(--theme-*)` rather than hardcoded values.
|
|
79
|
+
*
|
|
80
|
+
* @returns A CSS string of backward-compat Workshop variable aliases.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```css
|
|
84
|
+
* // Injected on :root, allows all existing CSS to continue working:
|
|
85
|
+
* --bg-dark: var(--theme-bg-base);
|
|
86
|
+
* --bg-medium: var(--theme-bg-surface);
|
|
87
|
+
* --primary: var(--theme-primary);
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function buildWorkshopAliasCss(): string {
|
|
91
|
+
return [
|
|
92
|
+
// Core palette aliases
|
|
93
|
+
'--bg-dark:var(--theme-bg-base)',
|
|
94
|
+
'--bg-medium:var(--theme-bg-surface)',
|
|
95
|
+
'--bg-light:var(--theme-bg-elevated)',
|
|
96
|
+
'--text-primary:var(--theme-text-primary)',
|
|
97
|
+
'--text-secondary:var(--theme-text-secondary)',
|
|
98
|
+
'--primary:var(--theme-primary)',
|
|
99
|
+
'--primary-light:var(--theme-primary-light)',
|
|
100
|
+
'--primary-dark:var(--theme-primary-dark)',
|
|
101
|
+
'--accent:var(--theme-accent)',
|
|
102
|
+
'--accent-light:var(--theme-accent-light)',
|
|
103
|
+
'--glass:var(--theme-glass)',
|
|
104
|
+
'--glass-border:var(--theme-glass-border)',
|
|
105
|
+
'--success:var(--theme-success)',
|
|
106
|
+
'--warning:var(--theme-warning)',
|
|
107
|
+
'--error:var(--theme-error)',
|
|
108
|
+
// Semantic surface aliases (used in components)
|
|
109
|
+
'--surface-primary:var(--theme-bg-base)',
|
|
110
|
+
'--surface-secondary:var(--theme-bg-surface)',
|
|
111
|
+
'--surface-tertiary:var(--theme-bg-elevated)',
|
|
112
|
+
'--surface-hover:var(--theme-bg-elevated)',
|
|
113
|
+
'--border-subtle:var(--theme-glass-border)',
|
|
114
|
+
'--accent-muted:var(--theme-primary-light)',
|
|
115
|
+
'--text-muted:var(--theme-text-secondary)',
|
|
116
|
+
].join(';');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Venue backward-compat aliases ───────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build CSS alias declarations mapping Venue's legacy `--brand-primary`,
|
|
123
|
+
* `--brand-background`, etc. to the new `--theme-*` variables.
|
|
124
|
+
*
|
|
125
|
+
* Inject this alongside `buildThemeCss()` in Venue's `hooks.server.ts` to maintain
|
|
126
|
+
* backward compatibility with all existing Venue CSS using `var(--brand-primary)` etc.
|
|
127
|
+
*
|
|
128
|
+
* @returns A CSS string of backward-compat Venue brand variable aliases.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* const fullCss = buildThemeCss(theme) + ';' + buildBrandAliasCss();
|
|
133
|
+
* // Inject as <html style="..."> in hooks.server.ts
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export function buildBrandAliasCss(): string {
|
|
137
|
+
return [
|
|
138
|
+
'--brand-primary:var(--theme-primary)',
|
|
139
|
+
'--brand-background:var(--theme-bg-base)',
|
|
140
|
+
'--brand-accent:var(--theme-accent)',
|
|
141
|
+
'--brand-text:var(--theme-text-primary)',
|
|
142
|
+
'--brand-font-display:var(--theme-font-display)',
|
|
143
|
+
'--brand-font-body:var(--theme-font-body)',
|
|
144
|
+
].join(';');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Derived workshop vars requiring color manipulation ────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build CSS declarations for Workshop's derived semi-transparent variables.
|
|
151
|
+
*
|
|
152
|
+
* These can't be expressed as pure `var()` aliases because they require
|
|
153
|
+
* computing a color with modified alpha from the theme colors.
|
|
154
|
+
*
|
|
155
|
+
* @param theme - The fully-resolved theme definition.
|
|
156
|
+
* @returns A CSS string with semi-transparent derived variables.
|
|
157
|
+
*/
|
|
158
|
+
export function buildWorkshopDerivedCss(theme: ThemeDefinition): string {
|
|
159
|
+
const { colors } = theme;
|
|
160
|
+
return [
|
|
161
|
+
`--accent-subtle:${withAlpha(colors.primary, 0.13)}`,
|
|
162
|
+
`--success-subtle:${withAlpha(colors.success, 0.13)}`,
|
|
163
|
+
].join(';');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Google Fonts URL builder ─────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build a Google Fonts URL for the display and body fonts in a theme.
|
|
170
|
+
*
|
|
171
|
+
* Returns null when both fonts are system fonts and no external loading is needed.
|
|
172
|
+
* Handles Fraunces's special optical-size (opsz) and WONK variable axes.
|
|
173
|
+
* The returned URL is suitable for a `<link rel="stylesheet">` tag.
|
|
174
|
+
*
|
|
175
|
+
* Ported from Venue's `apps/web/src/lib/server/theme.ts`.
|
|
176
|
+
*
|
|
177
|
+
* @param theme - The resolved theme containing font names.
|
|
178
|
+
* @returns A Google Fonts CSS2 URL, or null if no external fonts are needed.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* buildFontUrl(theme);
|
|
183
|
+
* // 'https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,...&family=Inter:wght@300..800&display=swap'
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
export function buildFontUrl(theme: ThemeDefinition): string | null {
|
|
187
|
+
const SYSTEM_FONTS = new Set(['system-ui', 'ui-sans-serif', 'ui-serif', 'ui-monospace']);
|
|
188
|
+
const { displayFont, bodyFont } = theme.fonts;
|
|
189
|
+
const fontParts: string[] = [];
|
|
190
|
+
|
|
191
|
+
if (!SYSTEM_FONTS.has(displayFont)) {
|
|
192
|
+
if (displayFont === 'Fraunces') {
|
|
193
|
+
// Fraunces needs optical sizing + WONK axis for the display aesthetic
|
|
194
|
+
fontParts.push('family=Fraunces:ital,opsz,wght,WONK@0,9..144,100..900,0..1;1,9..144,100..900,0..1');
|
|
195
|
+
} else {
|
|
196
|
+
fontParts.push(`family=${encodeURIComponent(displayFont)}:wght@300..800`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!SYSTEM_FONTS.has(bodyFont) && bodyFont !== displayFont) {
|
|
201
|
+
fontParts.push(`family=${encodeURIComponent(bodyFont)}:wght@300..800`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (fontParts.length === 0) return null;
|
|
205
|
+
return `https://fonts.googleapis.com/css2?${fontParts.join('&')}&display=swap`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Generate a complete `<style>` block for HTML document export.
|
|
210
|
+
*
|
|
211
|
+
* Creates a self-contained CSS style block with all theme variables resolved
|
|
212
|
+
* to their actual oklch values (not `var()` references). Suitable for embedding
|
|
213
|
+
* in exported HTML/PDF/EPUB files that won't have the Workshop runtime.
|
|
214
|
+
*
|
|
215
|
+
* Replaces Workshop's `getExportStylesForTheme()` from `themeStyles.ts`.
|
|
216
|
+
*
|
|
217
|
+
* @param theme - The fully-resolved theme to embed.
|
|
218
|
+
* @returns An HTML `<style>...</style>` string with complete document styles.
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* const styles = buildExportStyles(resolveTheme({ userThemeId: 'tokyo-night' }));
|
|
223
|
+
* // Embed in generated HTML export
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export function buildExportStyles(theme: ThemeDefinition): string {
|
|
227
|
+
const { colors } = theme;
|
|
228
|
+
const isDark = theme.meta.isDark;
|
|
229
|
+
|
|
230
|
+
return `<style>
|
|
231
|
+
:root {
|
|
232
|
+
--bg: ${colors.bgBase};
|
|
233
|
+
--bg-medium: ${colors.bgSurface};
|
|
234
|
+
--bg-light: ${colors.bgElevated};
|
|
235
|
+
--text: ${colors.textPrimary};
|
|
236
|
+
--text-muted: ${colors.textSecondary};
|
|
237
|
+
--primary: ${colors.primary};
|
|
238
|
+
--primary-light: ${colors.primaryLight};
|
|
239
|
+
--primary-dark: ${colors.primaryDark};
|
|
240
|
+
--accent: ${colors.accent};
|
|
241
|
+
--accent-light: ${colors.accentLight};
|
|
242
|
+
--border: ${colors.glassBorder};
|
|
243
|
+
--code-bg: ${isDark ? 'oklch(0 0 0 / 0.3)' : 'oklch(0 0 0 / 0.05)'};
|
|
244
|
+
--success: ${colors.success};
|
|
245
|
+
--warning: ${colors.warning};
|
|
246
|
+
--error: ${colors.error};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
* { box-sizing: border-box; }
|
|
250
|
+
|
|
251
|
+
body {
|
|
252
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
253
|
+
line-height: 1.7;
|
|
254
|
+
color: var(--text);
|
|
255
|
+
background: var(--bg);
|
|
256
|
+
margin: 0;
|
|
257
|
+
padding: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
main {
|
|
261
|
+
max-width: 800px;
|
|
262
|
+
margin: 0 auto;
|
|
263
|
+
padding: 40px 20px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.document-header {
|
|
267
|
+
margin-bottom: 40px;
|
|
268
|
+
padding-bottom: 20px;
|
|
269
|
+
border-bottom: 2px solid var(--primary);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.document-title { font-size: 2.5em; color: var(--primary-light); margin: 0 0 0.5em; }
|
|
273
|
+
.document-author { font-size: 1.1em; color: var(--text-muted); margin: 0; }
|
|
274
|
+
|
|
275
|
+
.toc { margin: 30px 0; padding: 20px; background: var(--code-bg); border-radius: 8px; }
|
|
276
|
+
.toc h2 { margin-top: 0; font-size: 1.2em; color: var(--text-muted); }
|
|
277
|
+
.toc ul { list-style: none; padding-left: 0; }
|
|
278
|
+
.toc li { margin: 8px 0; }
|
|
279
|
+
.toc a { color: var(--primary); text-decoration: none; }
|
|
280
|
+
|
|
281
|
+
.chapter { margin: 3em 0; }
|
|
282
|
+
.chapter-title { font-size: 1.8em; color: var(--primary); border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
|
|
283
|
+
.chapter-break { border: none; border-top: 2px dashed var(--border); margin: 3em 0; }
|
|
284
|
+
|
|
285
|
+
h1, h2, h3, h4, h5, h6 { color: var(--primary-light); margin: 1.5em 0 0.5em; }
|
|
286
|
+
h2 { font-size: 1.5em; }
|
|
287
|
+
h3 { font-size: 1.25em; color: var(--accent); }
|
|
288
|
+
|
|
289
|
+
p { margin: 1em 0; }
|
|
290
|
+
a { color: var(--primary); }
|
|
291
|
+
strong { color: var(--primary-light); }
|
|
292
|
+
em { color: var(--accent); }
|
|
293
|
+
|
|
294
|
+
code { background: var(--code-bg); padding: 2px 6px; border-radius: 3px; font-family: 'JetBrains Mono', monospace; }
|
|
295
|
+
pre { background: var(--code-bg); padding: 16px; border-radius: 8px; overflow-x: auto; }
|
|
296
|
+
pre code { background: none; padding: 0; }
|
|
297
|
+
|
|
298
|
+
blockquote { border-left: 4px solid var(--primary); margin: 1em 0; padding-left: 16px; color: var(--text-muted); font-style: italic; }
|
|
299
|
+
|
|
300
|
+
ul, ol { padding-left: 24px; }
|
|
301
|
+
|
|
302
|
+
table { width: 100%; border-collapse: collapse; margin: 1em 0; }
|
|
303
|
+
th, td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; }
|
|
304
|
+
th { background: var(--code-bg); font-weight: 600; }
|
|
305
|
+
|
|
306
|
+
hr { border: none; border-top: 2px solid var(--border); margin: 2em 0; }
|
|
307
|
+
|
|
308
|
+
figure { margin: 1.5em 0; text-align: center; }
|
|
309
|
+
figure img { max-width: 100%; border-radius: 8px; }
|
|
310
|
+
|
|
311
|
+
.video-embed { position: relative; width: 100%; aspect-ratio: 16/9; margin: 1.5em 0; }
|
|
312
|
+
.video-embed iframe, .video-embed video { width: 100%; height: 100%; border-radius: 8px; }
|
|
313
|
+
|
|
314
|
+
.callout { padding: 16px; margin: 1em 0; border-radius: 8px; border-left: 4px solid; }
|
|
315
|
+
.callout-info { background: ${isDark ? 'oklch(0.624 0.082 181.4 / 0.15)' : 'oklch(0.624 0.082 181.4 / 0.10)'}; border-color: var(--primary); }
|
|
316
|
+
.callout-warning { background: ${isDark ? 'oklch(0.755 0.107 55.4 / 0.15)' : 'oklch(0.755 0.107 55.4 / 0.10)'}; border-color: var(--accent); }
|
|
317
|
+
|
|
318
|
+
@media print {
|
|
319
|
+
main { max-width: none; padding: 0; }
|
|
320
|
+
.chapter { page-break-before: always; }
|
|
321
|
+
.toc { page-break-after: always; }
|
|
322
|
+
}
|
|
323
|
+
</style>`;
|
|
324
|
+
}
|
package/src/derive.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module derive
|
|
3
|
+
* @description Auto-derive a full 15-property ThemeColors palette from a minimal ThemeSeed.
|
|
4
|
+
*
|
|
5
|
+
* Kit authors specify 2–6 properties; this module generates the remaining 9–13
|
|
6
|
+
* using oklch arithmetic. All derivation rules preserve perceptual uniformity:
|
|
7
|
+
* - Surface levels are derived by adding/subtracting lightness from bgBase
|
|
8
|
+
* - Text contrast is derived from bgBase luminance
|
|
9
|
+
* - Primary light/dark shades are derived by ±12% lightness
|
|
10
|
+
* - Accent defaults to primary hue +150° (split-complementary)
|
|
11
|
+
* - Status colors are fixed semantic values (same across all kit themes)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const colors = deriveFullPalette({
|
|
16
|
+
* primary: 'oklch(0.72 0.15 25)',
|
|
17
|
+
* bgBase: 'oklch(0.96 0.02 80)',
|
|
18
|
+
* });
|
|
19
|
+
* // → Full ThemeColors with all 15 properties
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { parseOklch, formatOklch, adjustL, rotateH, withAlpha } from './oklch.js';
|
|
24
|
+
import type { ThemeColors, ThemeSeed } from './types.js';
|
|
25
|
+
|
|
26
|
+
// Fixed semantic status colors (same across all themes — consistent meaning)
|
|
27
|
+
const SUCCESS = 'oklch(0.752 0.158 145.3)';
|
|
28
|
+
const WARNING = 'oklch(0.762 0.161 76.8)';
|
|
29
|
+
const ERROR = 'oklch(0.628 0.200 25.6)';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Derive a full 15-property ThemeColors palette from a minimal ThemeSeed.
|
|
33
|
+
*
|
|
34
|
+
* Only `primary` and `bgBase` are required. All other properties have sensible
|
|
35
|
+
* defaults derived via oklch arithmetic. If any optional seed property is
|
|
36
|
+
* provided, it takes precedence over the derived value.
|
|
37
|
+
*
|
|
38
|
+
* @param seed - Minimal 2–6 property theme specification from a kit author.
|
|
39
|
+
* @returns A fully-resolved ThemeColors with all 15 properties set.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const colors = deriveFullPalette({
|
|
44
|
+
* primary: 'oklch(0.72 0.15 25)', // Rose
|
|
45
|
+
* bgBase: 'oklch(0.96 0.02 80)', // Warm cream
|
|
46
|
+
* accent: 'oklch(0.80 0.12 75)', // Amber (explicit, not derived)
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function deriveFullPalette(seed: ThemeSeed): ThemeColors {
|
|
51
|
+
const primaryParsed = parseOklch(seed.primary);
|
|
52
|
+
const bgBaseParsed = parseOklch(seed.bgBase);
|
|
53
|
+
|
|
54
|
+
if (!primaryParsed || !bgBaseParsed) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`deriveFullPalette: invalid oklch in seed. primary="${seed.primary}" bgBase="${seed.bgBase}"`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const [, , primaryH] = primaryParsed;
|
|
61
|
+
const [bgL] = bgBaseParsed;
|
|
62
|
+
|
|
63
|
+
// ── Is this a dark or light theme? ───────────────────────────────────────
|
|
64
|
+
// Lightness below 0.35 is considered dark; above 0.65 is light.
|
|
65
|
+
const isDark = bgL < 0.5;
|
|
66
|
+
|
|
67
|
+
// ── Background scale ─────────────────────────────────────────────────────
|
|
68
|
+
// Darker themes surface up; lighter themes surface down (invert direction)
|
|
69
|
+
const bgSurface = adjustL(seed.bgBase, isDark ? +0.06 : -0.06);
|
|
70
|
+
const bgElevated = adjustL(seed.bgBase, isDark ? +0.12 : -0.10);
|
|
71
|
+
|
|
72
|
+
// ── Text ─────────────────────────────────────────────────────────────────
|
|
73
|
+
const textPrimary: string = seed.textPrimary ??
|
|
74
|
+
(isDark ? 'oklch(0.940 0.008 264.0)' : 'oklch(0.230 0.020 264.0)');
|
|
75
|
+
|
|
76
|
+
const textSecondary = isDark
|
|
77
|
+
? adjustL(textPrimary, -0.28)
|
|
78
|
+
: adjustL(textPrimary, +0.28);
|
|
79
|
+
|
|
80
|
+
// ── Primary shades ────────────────────────────────────────────────────────
|
|
81
|
+
const primaryLight = adjustL(seed.primary, +0.12);
|
|
82
|
+
const primaryDark = adjustL(seed.primary, -0.12);
|
|
83
|
+
|
|
84
|
+
// ── Accent ───────────────────────────────────────────────────────────────
|
|
85
|
+
// Default: rotate primary hue by 150° (split-complementary) with similar lightness
|
|
86
|
+
const accent: string = seed.accent ?? rotateH(seed.primary, 150);
|
|
87
|
+
const accentLight = adjustL(accent, +0.10);
|
|
88
|
+
|
|
89
|
+
// ── Glass overlays ───────────────────────────────────────────────────────
|
|
90
|
+
const glass = isDark ? 'oklch(1 0 0 / 0.05)' : 'oklch(0 0 0 / 0.02)';
|
|
91
|
+
const glassBorder = isDark ? 'oklch(1 0 0 / 0.12)' : 'oklch(0 0 0 / 0.10)';
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
bgBase: seed.bgBase,
|
|
95
|
+
bgSurface,
|
|
96
|
+
bgElevated,
|
|
97
|
+
textPrimary,
|
|
98
|
+
textSecondary,
|
|
99
|
+
primary: seed.primary,
|
|
100
|
+
primaryLight,
|
|
101
|
+
primaryDark,
|
|
102
|
+
accent,
|
|
103
|
+
accentLight,
|
|
104
|
+
glass,
|
|
105
|
+
glassBorder,
|
|
106
|
+
success: SUCCESS,
|
|
107
|
+
warning: WARNING,
|
|
108
|
+
error: ERROR,
|
|
109
|
+
};
|
|
110
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @soulcraft/theme
|
|
3
|
+
* @description Public API barrel for the @soulcraft/theme design system package.
|
|
4
|
+
*
|
|
5
|
+
* Re-exports all public types, utilities, and constants. Import from the root
|
|
6
|
+
* entry point for utility functions and types; import component sub-paths directly
|
|
7
|
+
* for Svelte components (e.g. `@soulcraft/theme/components/ThemePicker`).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { CATALOG_BY_ID, resolveTheme, buildThemeCss } from '@soulcraft/theme';
|
|
12
|
+
* import { createThemeStore } from '@soulcraft/theme/stores/theme.svelte';
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
17
|
+
export type {
|
|
18
|
+
OklchColor,
|
|
19
|
+
ThemeCategory,
|
|
20
|
+
ThemeColors,
|
|
21
|
+
ThemeFonts,
|
|
22
|
+
ThemeMeta,
|
|
23
|
+
ThemeDefinition,
|
|
24
|
+
ThemeSeed,
|
|
25
|
+
ThemeCascadeInput,
|
|
26
|
+
ThemeName,
|
|
27
|
+
} from './types.js';
|
|
28
|
+
|
|
29
|
+
// ── OKLCH utilities ───────────────────────────────────────────────────────────
|
|
30
|
+
export {
|
|
31
|
+
formatOklch,
|
|
32
|
+
parseOklch,
|
|
33
|
+
hexToOklch,
|
|
34
|
+
rgbaToOklch,
|
|
35
|
+
oklchToHex,
|
|
36
|
+
withAlpha,
|
|
37
|
+
adjustL,
|
|
38
|
+
rotateH,
|
|
39
|
+
} from './oklch.js';
|
|
40
|
+
|
|
41
|
+
// ── Theme catalog ─────────────────────────────────────────────────────────────
|
|
42
|
+
export {
|
|
43
|
+
THEME_CATALOG,
|
|
44
|
+
CATALOG_BY_ID,
|
|
45
|
+
getThemesByCategory,
|
|
46
|
+
getThemesGrouped,
|
|
47
|
+
} from './catalog.js';
|
|
48
|
+
|
|
49
|
+
// ── Palette derivation ────────────────────────────────────────────────────────
|
|
50
|
+
export { deriveFullPalette } from './derive.js';
|
|
51
|
+
|
|
52
|
+
// ── Cascade resolution ────────────────────────────────────────────────────────
|
|
53
|
+
export { resolveTheme } from './resolve.js';
|
|
54
|
+
|
|
55
|
+
// ── CSS builders ──────────────────────────────────────────────────────────────
|
|
56
|
+
export {
|
|
57
|
+
buildThemeCss,
|
|
58
|
+
buildWorkshopAliasCss,
|
|
59
|
+
buildBrandAliasCss,
|
|
60
|
+
buildWorkshopDerivedCss,
|
|
61
|
+
buildFontUrl,
|
|
62
|
+
buildExportStyles,
|
|
63
|
+
} from './css.js';
|