@dryui/theme-wizard 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.
Files changed (47) hide show
  1. package/dist/actions.d.ts +4 -0
  2. package/dist/actions.js +9 -0
  3. package/dist/components/AlphaSlider.svelte +13 -0
  4. package/dist/components/AlphaSlider.svelte.d.ts +9 -0
  5. package/dist/components/ContrastBadge.svelte +22 -0
  6. package/dist/components/ContrastBadge.svelte.d.ts +8 -0
  7. package/dist/components/HsbPicker.svelte +304 -0
  8. package/dist/components/HsbPicker.svelte.d.ts +9 -0
  9. package/dist/components/StepIndicator.svelte +87 -0
  10. package/dist/components/StepIndicator.svelte.d.ts +7 -0
  11. package/dist/components/TokenPreview.svelte +55 -0
  12. package/dist/components/TokenPreview.svelte.d.ts +8 -0
  13. package/dist/components/WizardShell.svelte +140 -0
  14. package/dist/components/WizardShell.svelte.d.ts +15 -0
  15. package/dist/engine/derivation.d.ts +282 -0
  16. package/dist/engine/derivation.js +1445 -0
  17. package/dist/engine/derivation.test.d.ts +1 -0
  18. package/dist/engine/derivation.test.js +956 -0
  19. package/dist/engine/export-css.d.ts +32 -0
  20. package/dist/engine/export-css.js +90 -0
  21. package/dist/engine/export-css.test.d.ts +1 -0
  22. package/dist/engine/export-css.test.js +78 -0
  23. package/dist/engine/index.d.ts +10 -0
  24. package/dist/engine/index.js +6 -0
  25. package/dist/engine/palette.d.ts +16 -0
  26. package/dist/engine/palette.js +44 -0
  27. package/dist/engine/presets.d.ts +6 -0
  28. package/dist/engine/presets.js +34 -0
  29. package/dist/engine/url-codec.d.ts +53 -0
  30. package/dist/engine/url-codec.js +243 -0
  31. package/dist/engine/url-codec.test.d.ts +1 -0
  32. package/dist/engine/url-codec.test.js +137 -0
  33. package/dist/index.d.ts +14 -0
  34. package/dist/index.js +17 -0
  35. package/dist/state.svelte.d.ts +104 -0
  36. package/dist/state.svelte.js +555 -0
  37. package/dist/steps/BrandColor.svelte +218 -0
  38. package/dist/steps/BrandColor.svelte.d.ts +6 -0
  39. package/dist/steps/Personality.svelte +319 -0
  40. package/dist/steps/Personality.svelte.d.ts +3 -0
  41. package/dist/steps/PreviewExport.svelte +113 -0
  42. package/dist/steps/PreviewExport.svelte.d.ts +9 -0
  43. package/dist/steps/Shape.svelte +121 -0
  44. package/dist/steps/Shape.svelte.d.ts +18 -0
  45. package/dist/steps/Typography.svelte +115 -0
  46. package/dist/steps/Typography.svelte.d.ts +18 -0
  47. package/package.json +56 -0
@@ -0,0 +1,555 @@
1
+ import { untrack } from 'svelte';
2
+ import { generateTheme } from './engine/derivation.js';
3
+ import { PRESETS } from './engine/presets.js';
4
+ function fmtRem(value) {
5
+ return `${parseFloat(value.toFixed(4))}rem`;
6
+ }
7
+ const STORAGE_KEY = 'dryui-theme-wizard';
8
+ const DEFAULTS = {
9
+ currentStep: 1,
10
+ personality: 'structured',
11
+ brandHsb: { h: 230, s: 65, b: 85 },
12
+ neutralMode: 'monochromatic',
13
+ statusHues: { error: 0, warning: 40, success: 145, info: 210 },
14
+ darkBgOverrides: {},
15
+ fastTrack: false,
16
+ typography: {
17
+ fontPreset: 'System',
18
+ scale: 'default'
19
+ },
20
+ shape: {
21
+ radiusPreset: 'soft',
22
+ radiusScale: 1,
23
+ density: 'default'
24
+ },
25
+ shadows: {
26
+ preset: 'elevated',
27
+ intensity: 1,
28
+ tintBrand: true
29
+ }
30
+ };
31
+ function loadPersistedState() {
32
+ if (typeof sessionStorage === 'undefined')
33
+ return DEFAULTS;
34
+ try {
35
+ const raw = sessionStorage.getItem(STORAGE_KEY);
36
+ if (!raw)
37
+ return DEFAULTS;
38
+ const saved = JSON.parse(raw);
39
+ return { ...DEFAULTS, ...saved };
40
+ }
41
+ catch {
42
+ return DEFAULTS;
43
+ }
44
+ }
45
+ let persistTimer;
46
+ function persistState() {
47
+ if (typeof sessionStorage === 'undefined')
48
+ return;
49
+ clearTimeout(persistTimer);
50
+ persistTimer = setTimeout(() => {
51
+ untrack(() => {
52
+ try {
53
+ const { personality, brandHsb, neutralMode, statusHues, darkBgOverrides, typography, shape, shadows } = wizardState;
54
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
55
+ personality,
56
+ brandHsb,
57
+ neutralMode,
58
+ statusHues,
59
+ darkBgOverrides,
60
+ typography,
61
+ shape,
62
+ shadows
63
+ }));
64
+ }
65
+ catch {
66
+ // storage full or unavailable — ignore
67
+ }
68
+ });
69
+ }, 300);
70
+ }
71
+ const initial = loadPersistedState();
72
+ export const wizardState = $state({
73
+ currentStep: initial.currentStep,
74
+ personality: initial.personality,
75
+ brandHsb: initial.brandHsb,
76
+ neutralMode: initial.neutralMode,
77
+ statusHues: initial.statusHues,
78
+ darkBgOverrides: initial.darkBgOverrides,
79
+ fastTrack: initial.fastTrack,
80
+ typography: initial.typography,
81
+ shape: initial.shape,
82
+ shadows: initial.shadows
83
+ });
84
+ // ─── Derived theme ────────────────────────────────────────────────────────────
85
+ const derivedTheme = $derived.by(() => {
86
+ return {
87
+ value: generateTheme(wizardState.brandHsb, {
88
+ neutralMode: wizardState.neutralMode,
89
+ statusHues: wizardState.statusHues,
90
+ darkBg: wizardState.darkBgOverrides
91
+ })
92
+ };
93
+ });
94
+ export function getDerivedTheme() {
95
+ return derivedTheme.value;
96
+ }
97
+ // ─── Exported functions ───────────────────────────────────────────────────────
98
+ /** Update the brand color (h: 0-360, s/b: 0-100). */
99
+ export function setBrandHsb(h, s, b) {
100
+ wizardState.brandHsb = { h, s, b };
101
+ persistState();
102
+ }
103
+ /** Update a single status tone's hue. */
104
+ export function setStatusHue(tone, hue) {
105
+ wizardState.statusHues[tone] = hue;
106
+ persistState();
107
+ }
108
+ /** Update the neutral palette mode. */
109
+ export function setNeutralMode(mode) {
110
+ wizardState.neutralMode = mode;
111
+ persistState();
112
+ }
113
+ /** Set the personality (chrome level) and apply cross-step defaults. */
114
+ function applyPersonalityDefaults(p) {
115
+ wizardState.personality = p;
116
+ // Cross-step defaults
117
+ const PERSONALITY_SHADOW = {
118
+ minimal: 'flat',
119
+ clean: 'flat',
120
+ structured: 'elevated',
121
+ rich: 'deep'
122
+ };
123
+ const PERSONALITY_RADIUS = {
124
+ minimal: 'soft',
125
+ clean: 'soft',
126
+ structured: 'soft',
127
+ rich: 'rounded'
128
+ };
129
+ wizardState.shadows.preset = PERSONALITY_SHADOW[p];
130
+ wizardState.shape.radiusPreset = PERSONALITY_RADIUS[p];
131
+ }
132
+ /** Set the personality (chrome level) and apply cross-step defaults. */
133
+ export function setPersonality(p) {
134
+ applyPersonalityDefaults(p);
135
+ persistState();
136
+ }
137
+ /** Override a dark background level. */
138
+ export function setDarkBg(level, value) {
139
+ wizardState.darkBgOverrides[level] = value;
140
+ persistState();
141
+ }
142
+ /** Navigate to a specific step (1–5). */
143
+ export function setStep(n) {
144
+ wizardState.currentStep = Math.max(1, Math.min(5, n));
145
+ }
146
+ /** Advance to the next step. */
147
+ export function goNextStep() {
148
+ setStep(wizardState.currentStep + 1);
149
+ }
150
+ /** Go back to the previous step. */
151
+ export function goPrevStep() {
152
+ setStep(wizardState.currentStep - 1);
153
+ }
154
+ /** Enable fast-track mode and jump to the final step. */
155
+ export function activateFastTrack() {
156
+ wizardState.fastTrack = true;
157
+ setStep(5);
158
+ }
159
+ /** Apply a named preset from the PRESETS array. */
160
+ export function applyPreset(name) {
161
+ const preset = PRESETS.find((p) => p.name.toLowerCase() === name.toLowerCase());
162
+ if (!preset) {
163
+ throw new Error(`Unknown preset: "${name}"`);
164
+ }
165
+ wizardState.brandHsb = { ...preset.brandInput };
166
+ persistState();
167
+ }
168
+ /**
169
+ * Return a CSS custom property string for live preview injection.
170
+ *
171
+ * @param mode - 'light' | 'dark'
172
+ * @returns e.g. `--dry-color-brand: hsl(230, 75%, 60%); ...`
173
+ */
174
+ export function getStyleString(mode) {
175
+ return Object.entries(getAllTokens(mode))
176
+ .map(([name, value]) => `${name}: ${value}`)
177
+ .join('; ');
178
+ }
179
+ /** Update the font preset name. */
180
+ export function setFontPreset(preset) {
181
+ wizardState.typography.fontPreset = preset;
182
+ persistState();
183
+ }
184
+ /** Update the type scale. */
185
+ export function setTypeScale(scale) {
186
+ wizardState.typography.scale = scale;
187
+ persistState();
188
+ }
189
+ /** Reset all wizard state to defaults. */
190
+ export function resetToDefaults() {
191
+ wizardState.currentStep = 1;
192
+ wizardState.personality = 'structured';
193
+ wizardState.brandHsb = { h: 230, s: 65, b: 85 };
194
+ wizardState.neutralMode = 'monochromatic';
195
+ wizardState.statusHues = { error: 0, warning: 40, success: 145, info: 210 };
196
+ wizardState.darkBgOverrides = {};
197
+ wizardState.fastTrack = false;
198
+ wizardState.typography = { fontPreset: 'System', scale: 'default' };
199
+ wizardState.shape = { radiusPreset: 'soft', radiusScale: 1, density: 'default' };
200
+ wizardState.shadows = { preset: 'elevated', intensity: 1, tintBrand: true };
201
+ if (typeof sessionStorage !== 'undefined') {
202
+ sessionStorage.removeItem(STORAGE_KEY);
203
+ }
204
+ }
205
+ /** Apply a full wizard recipe, using personality defaults before explicit overrides. */
206
+ export function applyRecipe(recipe) {
207
+ wizardState.currentStep = DEFAULTS.currentStep;
208
+ wizardState.brandHsb = { ...recipe.brand };
209
+ wizardState.neutralMode = recipe.neutralMode ?? DEFAULTS.neutralMode;
210
+ wizardState.statusHues = { ...DEFAULTS.statusHues, ...recipe.statusHues };
211
+ wizardState.darkBgOverrides = { ...DEFAULTS.darkBgOverrides };
212
+ wizardState.fastTrack = false;
213
+ wizardState.typography = recipe.typography
214
+ ? { ...recipe.typography }
215
+ : { ...DEFAULTS.typography };
216
+ wizardState.shape = { ...DEFAULTS.shape };
217
+ wizardState.shadows = { ...DEFAULTS.shadows };
218
+ applyPersonalityDefaults(recipe.personality ?? DEFAULTS.personality);
219
+ if (recipe.shape) {
220
+ wizardState.shape = { ...recipe.shape };
221
+ }
222
+ if (recipe.shadows) {
223
+ wizardState.shadows = { ...recipe.shadows };
224
+ }
225
+ persistState();
226
+ }
227
+ // ─── Shape & spacing ─────────────────────────────────────────────────────────
228
+ export function setRadiusPreset(preset) {
229
+ wizardState.shape.radiusPreset = preset;
230
+ wizardState.shape.radiusScale = 1;
231
+ persistState();
232
+ }
233
+ export function setRadiusScale(scale) {
234
+ wizardState.shape.radiusScale = scale;
235
+ persistState();
236
+ }
237
+ export function setDensity(density) {
238
+ wizardState.shape.density = density;
239
+ persistState();
240
+ }
241
+ export const RADIUS_PRESETS = {
242
+ sharp: { sm: 0, md: 2, lg: 2, xl: 4, '2xl': 4, full: 9999 },
243
+ soft: { sm: 4, md: 8, lg: 8, xl: 12, '2xl': 16, full: 9999 },
244
+ rounded: { sm: 8, md: 12, lg: 16, xl: 20, '2xl': 24, full: 9999 },
245
+ pill: { sm: 9999, md: 9999, lg: 16, xl: 20, '2xl': 24, full: 9999 }
246
+ };
247
+ const DENSITY_FACTORS = {
248
+ compact: 0.85,
249
+ default: 1,
250
+ spacious: 1.15
251
+ };
252
+ // ─── Shadows & elevation ────────────────────────────────────────────────────
253
+ export function setShadowPreset(preset) {
254
+ wizardState.shadows.preset = preset;
255
+ persistState();
256
+ }
257
+ export function setShadowIntensity(intensity) {
258
+ wizardState.shadows.intensity = intensity;
259
+ persistState();
260
+ }
261
+ export function setShadowTint(tint) {
262
+ wizardState.shadows.tintBrand = tint;
263
+ persistState();
264
+ }
265
+ const SHADOW_DEFS = {
266
+ flat: {
267
+ raised: [],
268
+ overlay: []
269
+ },
270
+ subtle: {
271
+ raised: [
272
+ { y: 1, blur: 3, lightA: 0.06, darkA: 0.5 },
273
+ { y: 1, blur: 2, lightA: 0.04, darkA: 0.35 }
274
+ ],
275
+ overlay: [
276
+ { y: 4, blur: 16, lightA: 0.1, darkA: 0.6 },
277
+ { y: 2, blur: 6, lightA: 0.06, darkA: 0.4 }
278
+ ]
279
+ },
280
+ elevated: {
281
+ raised: [
282
+ { y: 2, blur: 6, lightA: 0.1, darkA: 0.65 },
283
+ { y: 1, blur: 3, lightA: 0.07, darkA: 0.45 }
284
+ ],
285
+ overlay: [
286
+ { y: 8, blur: 28, lightA: 0.14, darkA: 0.7 },
287
+ { y: 3, blur: 10, lightA: 0.1, darkA: 0.5 }
288
+ ]
289
+ },
290
+ deep: {
291
+ raised: [
292
+ { y: 4, blur: 14, lightA: 0.16, darkA: 0.8 },
293
+ { y: 2, blur: 5, lightA: 0.1, darkA: 0.55 }
294
+ ],
295
+ overlay: [
296
+ { y: 16, blur: 48, lightA: 0.22, darkA: 0.85 },
297
+ { y: 6, blur: 18, lightA: 0.14, darkA: 0.6 }
298
+ ]
299
+ }
300
+ };
301
+ function buildShadowValue(layers, mode, hue, sat, lightness, intensity) {
302
+ if (layers.length === 0)
303
+ return 'none';
304
+ return layers
305
+ .map((l) => {
306
+ const a = (mode === 'light' ? l.lightA : l.darkA) * intensity;
307
+ return `0 ${l.y}px ${l.blur}px hsla(${hue}, ${sat}%, ${lightness}%, ${Math.min(a, 1).toFixed(3)})`;
308
+ })
309
+ .join(', ');
310
+ }
311
+ export function getShadowTokens(shadowPreset = wizardState.shadows.preset, shadowIntensity = wizardState.shadows.intensity, tintBrand = wizardState.shadows.tintBrand, brandHue = wizardState.brandHsb.h) {
312
+ const H = Math.round(brandHue);
313
+ const hue = tintBrand ? H : 0;
314
+ const def = SHADOW_DEFS[shadowPreset];
315
+ return {
316
+ light: {
317
+ '--dry-shadow-raised': buildShadowValue(def.raised, 'light', hue, 20, 20, shadowIntensity),
318
+ '--dry-shadow-overlay': buildShadowValue(def.overlay, 'light', hue, 20, 20, shadowIntensity)
319
+ },
320
+ dark: {
321
+ '--dry-shadow-raised': buildShadowValue(def.raised, 'dark', hue, 15, 2, shadowIntensity),
322
+ '--dry-shadow-overlay': buildShadowValue(def.overlay, 'dark', hue, 15, 2, shadowIntensity)
323
+ }
324
+ };
325
+ }
326
+ const SPACE_SCALE = [
327
+ ['0_5', 0.125],
328
+ ['1', 0.25],
329
+ ['1_5', 0.375],
330
+ ['2', 0.5],
331
+ ['2_5', 0.625],
332
+ ['3', 0.75],
333
+ ['3_5', 0.875],
334
+ ['4', 1],
335
+ ['5', 1.25],
336
+ ['6', 1.5],
337
+ ['7', 1.75],
338
+ ['8', 2],
339
+ ['9', 2.25],
340
+ ['10', 2.5],
341
+ ['11', 2.75],
342
+ ['12', 3],
343
+ ['14', 3.5],
344
+ ['16', 4],
345
+ ['20', 5],
346
+ ['24', 6],
347
+ ['32', 8]
348
+ ];
349
+ export function getShapeTokens(radiusPreset = wizardState.shape.radiusPreset, radiusScale = wizardState.shape.radiusScale, densityPreset = wizardState.shape.density) {
350
+ const preset = RADIUS_PRESETS[radiusPreset];
351
+ const scale = radiusScale;
352
+ const density = DENSITY_FACTORS[densityPreset];
353
+ const tokens = {};
354
+ for (const [key, base] of Object.entries(preset)) {
355
+ let px;
356
+ if (base >= 9999) {
357
+ px = scale >= 1 ? 9999 : Math.round(24 * scale);
358
+ }
359
+ else {
360
+ px = Math.round(base * scale);
361
+ }
362
+ tokens[`--dry-radius-${key}`] = `${px}px`;
363
+ }
364
+ for (const [key, rem] of SPACE_SCALE) {
365
+ tokens[`--dry-space-${key}`] = fmtRem(rem * density);
366
+ }
367
+ return tokens;
368
+ }
369
+ // ─── Personality / chrome ───────────────────────────────────────────────────
370
+ const PERSONALITY_TOKENS = {
371
+ minimal: {
372
+ // Surface role
373
+ '--dry-surface-bg': 'transparent',
374
+ '--dry-surface-border': 'transparent',
375
+ '--dry-surface-shadow': 'none',
376
+ '--dry-surface-radius': '0',
377
+ '--dry-surface-padding': 'var(--dry-space-4)',
378
+ // Chrome role
379
+ '--dry-chrome-bg': 'transparent',
380
+ '--dry-chrome-border': 'transparent',
381
+ '--dry-chrome-shadow': 'none',
382
+ // Overlay role
383
+ '--dry-overlay-bg': 'var(--dry-color-bg-overlay)',
384
+ '--dry-overlay-border': 'var(--dry-color-stroke-weak)',
385
+ '--dry-overlay-shadow': 'var(--dry-shadow-sm)',
386
+ '--dry-overlay-radius': 'var(--dry-radius-lg)',
387
+ // Control role
388
+ '--dry-control-bg': 'transparent',
389
+ '--dry-control-border': 'var(--dry-color-stroke-weak)',
390
+ '--dry-control-radius': 'var(--dry-radius-sm)',
391
+ // Page role
392
+ '--dry-page-bg': 'transparent',
393
+ '--dry-page-border': 'transparent',
394
+ '--dry-page-shadow': 'none',
395
+ '--dry-page-radius': '0'
396
+ },
397
+ clean: {
398
+ // Surface role
399
+ '--dry-surface-bg': 'var(--dry-color-bg-raised)',
400
+ '--dry-surface-border': 'transparent',
401
+ '--dry-surface-shadow': 'none',
402
+ '--dry-surface-radius': 'var(--dry-radius-lg)',
403
+ '--dry-surface-padding': 'var(--dry-space-6)',
404
+ // Chrome role
405
+ '--dry-chrome-bg': 'transparent',
406
+ '--dry-chrome-border': 'var(--dry-color-stroke-weak)',
407
+ '--dry-chrome-shadow': 'none',
408
+ // Overlay role
409
+ '--dry-overlay-bg': 'var(--dry-color-bg-overlay)',
410
+ '--dry-overlay-border': 'var(--dry-color-stroke-weak)',
411
+ '--dry-overlay-shadow': 'var(--dry-shadow-md)',
412
+ '--dry-overlay-radius': 'var(--dry-radius-lg)',
413
+ // Control role
414
+ '--dry-control-bg': 'var(--dry-color-bg-raised)',
415
+ '--dry-control-border': 'var(--dry-color-stroke-strong)',
416
+ '--dry-control-radius': 'var(--dry-radius-md)',
417
+ // Page role
418
+ '--dry-page-bg': 'transparent',
419
+ '--dry-page-border': 'transparent',
420
+ '--dry-page-shadow': 'none',
421
+ '--dry-page-radius': 'var(--dry-radius-lg)'
422
+ },
423
+ structured: {
424
+ // Surface role
425
+ '--dry-surface-bg': 'var(--dry-color-bg-raised)',
426
+ '--dry-surface-border': 'var(--dry-color-stroke-weak)',
427
+ '--dry-surface-shadow': 'var(--dry-shadow-raised)',
428
+ '--dry-surface-radius': 'var(--dry-radius-xl)',
429
+ '--dry-surface-padding': 'var(--dry-space-8)',
430
+ // Chrome role
431
+ '--dry-chrome-bg': 'var(--dry-color-bg-raised)',
432
+ '--dry-chrome-border': 'var(--dry-color-stroke-weak)',
433
+ '--dry-chrome-shadow': 'var(--dry-shadow-sm)',
434
+ // Overlay role
435
+ '--dry-overlay-bg': 'var(--dry-color-bg-overlay)',
436
+ '--dry-overlay-border': 'var(--dry-color-stroke-weak)',
437
+ '--dry-overlay-shadow': 'var(--dry-shadow-lg)',
438
+ '--dry-overlay-radius': 'var(--dry-radius-xl)',
439
+ // Control role
440
+ '--dry-control-bg': 'var(--dry-color-bg-raised)',
441
+ '--dry-control-border': 'var(--dry-color-stroke-strong)',
442
+ '--dry-control-radius': 'var(--dry-radius-md)',
443
+ // Page role
444
+ '--dry-page-bg': 'var(--dry-color-bg-overlay)',
445
+ '--dry-page-border': 'var(--dry-color-stroke-weak)',
446
+ '--dry-page-shadow': 'var(--dry-shadow-sm)',
447
+ '--dry-page-radius': 'var(--dry-radius-xl)'
448
+ },
449
+ rich: {
450
+ // Surface role
451
+ '--dry-surface-bg': 'var(--dry-color-bg-overlay)',
452
+ '--dry-surface-border': 'var(--dry-color-stroke-weak)',
453
+ '--dry-surface-shadow': 'var(--dry-shadow-overlay)',
454
+ '--dry-surface-radius': 'var(--dry-radius-2xl)',
455
+ '--dry-surface-padding': 'var(--dry-space-10)',
456
+ // Chrome role
457
+ '--dry-chrome-bg': 'var(--dry-color-bg-overlay)',
458
+ '--dry-chrome-border': 'var(--dry-color-stroke-weak)',
459
+ '--dry-chrome-shadow': 'var(--dry-shadow-overlay)',
460
+ // Overlay role
461
+ '--dry-overlay-bg': 'var(--dry-color-bg-overlay)',
462
+ '--dry-overlay-border': 'var(--dry-color-stroke-weak)',
463
+ '--dry-overlay-shadow': 'var(--dry-shadow-overlay)',
464
+ '--dry-overlay-radius': 'var(--dry-radius-2xl)',
465
+ // Control role
466
+ '--dry-control-bg': 'var(--dry-color-bg-raised)',
467
+ '--dry-control-border': 'var(--dry-color-stroke-strong)',
468
+ '--dry-control-radius': 'var(--dry-radius-lg)',
469
+ // Page role
470
+ '--dry-page-bg': 'var(--dry-color-bg-raised)',
471
+ '--dry-page-border': 'var(--dry-color-stroke-weak)',
472
+ '--dry-page-shadow': 'var(--dry-shadow-overlay)',
473
+ '--dry-page-radius': 'var(--dry-radius-2xl)'
474
+ }
475
+ };
476
+ export function getPersonalityTokens() {
477
+ return PERSONALITY_TOKENS[wizardState.personality];
478
+ }
479
+ // ─── Typography tokens ──────────────────────────────────────────────────────
480
+ export const FONT_STACKS = {
481
+ System: 'ui-sans-serif, system-ui, -apple-system, sans-serif',
482
+ Humanist: 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", sans-serif',
483
+ Geometric: 'Avenir, Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif',
484
+ Classical: 'Optima, Candara, "Noto Sans", source-sans-pro, sans-serif',
485
+ Serif: 'Charter, "Bitstream Charter", "Sitka Text", Cambria, serif',
486
+ Mono: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace'
487
+ };
488
+ const TYPE_BASE = {
489
+ display: { size: 3.5, leading: 4 },
490
+ 'heading-1': { size: 2.5, leading: 3 },
491
+ 'heading-2': { size: 2, leading: 2.5 },
492
+ 'heading-3': { size: 1.5, leading: 2 },
493
+ 'heading-4': { size: 1.25, leading: 1.75 },
494
+ small: { size: 1, leading: 1.5 },
495
+ tiny: { size: 0.875, leading: 1.25 }
496
+ };
497
+ const TYPE_SCALE_FACTORS = {
498
+ compact: 0.9,
499
+ default: 1,
500
+ spacious: 1.1
501
+ };
502
+ export function getTypographyTokens(fontPreset = wizardState.typography.fontPreset, scale = wizardState.typography.scale) {
503
+ const factor = TYPE_SCALE_FACTORS[scale];
504
+ const tokens = {
505
+ '--dry-font-sans': FONT_STACKS[fontPreset]
506
+ };
507
+ for (const [name, { size, leading }] of Object.entries(TYPE_BASE)) {
508
+ tokens[`--dry-type-${name}-size`] = fmtRem(size * factor);
509
+ tokens[`--dry-type-${name}-leading`] = fmtRem(leading * factor);
510
+ }
511
+ return tokens;
512
+ }
513
+ /** Return all tokens (color + shape + shadow + personality + typography) merged for a given mode. */
514
+ export function getAllTokens(mode = 'light') {
515
+ const m = mode;
516
+ const theme = derivedTheme.value;
517
+ const colorTokens = m === 'dark' ? theme.dark : theme.light;
518
+ const shapeTokens = getShapeTokens();
519
+ const shadows = getShadowTokens();
520
+ const shadowTokens = shadows[m];
521
+ const personalityTokens = getPersonalityTokens();
522
+ const typographyTokens = getTypographyTokens();
523
+ return { ...colorTokens, ...shapeTokens, ...shadowTokens, ...personalityTokens, ...typographyTokens };
524
+ }
525
+ /** Return only tokens that the user has changed from defaults. */
526
+ export function getOverrideTokens(mode = 'light') {
527
+ const all = getAllTokens(mode);
528
+ const m = mode;
529
+ const base = getDefaultAllTokens(m);
530
+ const overrides = {};
531
+ for (const k in all) {
532
+ if (all[k] !== base[k]) {
533
+ overrides[k] = all[k];
534
+ }
535
+ }
536
+ return overrides;
537
+ }
538
+ /** getAllTokens computed with DEFAULTS — cached per mode. */
539
+ function getDefaultAllTokens(mode) {
540
+ return (defaultAllTokensCache[mode] ??= computeDefaultAllTokens(mode));
541
+ }
542
+ const defaultAllTokensCache = {};
543
+ function computeDefaultAllTokens(mode) {
544
+ const theme = generateTheme(DEFAULTS.brandHsb, {
545
+ neutralMode: DEFAULTS.neutralMode,
546
+ statusHues: DEFAULTS.statusHues,
547
+ darkBg: DEFAULTS.darkBgOverrides
548
+ });
549
+ const colorTokens = mode === 'dark' ? theme.dark : theme.light;
550
+ const shapeTokens = getShapeTokens(DEFAULTS.shape.radiusPreset, DEFAULTS.shape.radiusScale, DEFAULTS.shape.density);
551
+ const shadowTokens = getShadowTokens(DEFAULTS.shadows.preset, DEFAULTS.shadows.intensity, DEFAULTS.shadows.tintBrand, DEFAULTS.brandHsb.h)[mode];
552
+ const personalityTokens = PERSONALITY_TOKENS[DEFAULTS.personality];
553
+ const typographyTokens = getTypographyTokens(DEFAULTS.typography.fontPreset, DEFAULTS.typography.scale);
554
+ return { ...colorTokens, ...shapeTokens, ...shadowTokens, ...personalityTokens, ...typographyTokens };
555
+ }