@motion-proto/live-tokens 0.47.0 → 0.48.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 (124) hide show
  1. package/.claude/skills/live-tokens-adjust-shape-space/SKILL.md +68 -0
  2. package/.claude/skills/live-tokens-create-component/SKILL.md +2 -2
  3. package/.claude/skills/live-tokens-generate-theme/SKILL.md +152 -0
  4. package/CHANGELOG.md +292 -0
  5. package/README.md +37 -26
  6. package/bin/adjust.mjs +254 -0
  7. package/bin/cli.mjs +94 -7
  8. package/bin/generate-theme.mjs +251 -0
  9. package/bin/migrate.mjs +95 -6
  10. package/dist-plugin/adjust/index.cjs +259 -0
  11. package/dist-plugin/adjust/index.d.cts +45 -0
  12. package/dist-plugin/adjust/index.d.ts +45 -0
  13. package/dist-plugin/adjust/index.js +176 -0
  14. package/dist-plugin/{chunk-H4TRUINI.js → chunk-44RSTAII.js} +0 -48
  15. package/dist-plugin/chunk-6OZFXIQI.js +316 -0
  16. package/dist-plugin/chunk-76TFDTJO.js +556 -0
  17. package/dist-plugin/chunk-D3ZVKOR4.js +52 -0
  18. package/dist-plugin/dataPaths-DBN0RPuT.d.cts +54 -0
  19. package/dist-plugin/dataPaths-DBN0RPuT.d.ts +54 -0
  20. package/dist-plugin/generateColorsAndType/index.cjs +1781 -0
  21. package/dist-plugin/generateColorsAndType/index.d.cts +81 -0
  22. package/dist-plugin/generateColorsAndType/index.d.ts +81 -0
  23. package/dist-plugin/generateColorsAndType/index.js +1210 -0
  24. package/dist-plugin/index.cjs +901 -533
  25. package/dist-plugin/index.d.cts +3 -2
  26. package/dist-plugin/index.d.ts +3 -2
  27. package/dist-plugin/index.js +658 -1082
  28. package/dist-plugin/migrateData/index.cjs +728 -0
  29. package/dist-plugin/migrateData/index.d.cts +60 -0
  30. package/dist-plugin/migrateData/index.d.ts +60 -0
  31. package/dist-plugin/migrateData/index.js +348 -0
  32. package/dist-plugin/themeTypes-DMHZOnUn.d.cts +211 -0
  33. package/dist-plugin/themeTypes-DMHZOnUn.d.ts +211 -0
  34. package/dist-plugin/tokensCssMigrations/index.cjs +2 -1
  35. package/dist-plugin/tokensCssMigrations/index.d.cts +3 -15
  36. package/dist-plugin/tokensCssMigrations/index.d.ts +3 -15
  37. package/dist-plugin/tokensCssMigrations/index.js +4 -2
  38. package/package.json +18 -4
  39. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +183 -158
  40. package/src/editor/component-editor/scaffolding/ComponentFileMenu.svelte +0 -4
  41. package/src/editor/component-editor/scaffolding/SaveAsDialog.svelte +4 -4
  42. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +2 -59
  43. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +1 -1
  44. package/src/editor/core/components/adjustAliases.ts +180 -0
  45. package/src/editor/core/components/aliasKinds.ts +73 -0
  46. package/src/editor/core/components/componentConfigService.ts +26 -38
  47. package/src/editor/core/flashStatus.ts +1 -1
  48. package/src/editor/core/fonts/fontMigration.ts +13 -13
  49. package/src/editor/core/fonts/fontPairing.ts +35 -0
  50. package/src/editor/core/palettes/paletteDerivation.ts +128 -16
  51. package/src/editor/core/preview/lookPreview.ts +141 -0
  52. package/src/editor/core/productionPulse.ts +34 -21
  53. package/src/editor/core/storage/files/versionedFileResourceClient.ts +18 -45
  54. package/src/editor/core/store/editorConfigStore.ts +2 -2
  55. package/src/editor/core/store/editorPersistence.ts +0 -1
  56. package/src/editor/core/store/editorStore.ts +114 -51
  57. package/src/editor/core/store/editorTypes.ts +0 -1
  58. package/src/editor/core/store/gradientSource.ts +2 -2
  59. package/src/editor/core/themes/colorsAndTypeService.ts +95 -0
  60. package/src/editor/core/themes/generateColorsAndType.ts +433 -0
  61. package/src/editor/core/themes/loadRows.ts +64 -0
  62. package/src/editor/core/themes/lookSummary.ts +75 -0
  63. package/src/editor/core/themes/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +3 -3
  64. package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -2
  65. package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +3 -3
  66. package/src/editor/core/themes/migrations/2026-08-13-drop-legacy-shape-space-keys.ts +38 -0
  67. package/src/editor/core/themes/migrations/2026-08-15-line-height-scale-rename.ts +57 -0
  68. package/src/editor/core/themes/migrations/index.ts +19 -11
  69. package/src/editor/core/themes/slices/components.ts +4 -4
  70. package/src/editor/core/themes/slices/fonts.ts +1 -1
  71. package/src/editor/core/themes/slices/gradients.ts +22 -0
  72. package/src/editor/core/themes/slices/palettes.ts +2 -2
  73. package/src/editor/core/themes/themeInit.ts +21 -29
  74. package/src/editor/core/themes/themeService.ts +218 -78
  75. package/src/editor/core/themes/themeTypes.ts +94 -51
  76. package/src/editor/docs/Docs.svelte +1 -0
  77. package/src/editor/docs/chapters.ts +1 -0
  78. package/src/editor/docs/content/01-overview.md +1 -1
  79. package/src/editor/docs/content/editing-tokens.md +6 -6
  80. package/src/editor/docs/content/getting-started.md +9 -7
  81. package/src/editor/docs/content/themes-workflow.md +74 -34
  82. package/src/editor/docs/content/where-themes-live.md +61 -0
  83. package/src/editor/docs/content.generated.ts +5 -4
  84. package/src/editor/index.ts +27 -27
  85. package/src/editor/pages/ComponentEditorPage.svelte +2 -2
  86. package/src/editor/pages/EditorShell.svelte +4 -30
  87. package/src/editor/ui/BezierCurveEditor.svelte +2 -2
  88. package/src/editor/ui/ColorEditPanel.svelte +1 -1
  89. package/src/editor/ui/FileLoadList.svelte +67 -7
  90. package/src/editor/ui/GradientEditor.svelte +3 -3
  91. package/src/editor/ui/PaletteEditor.svelte +26 -12
  92. package/src/editor/ui/ThemePanel.svelte +1067 -0
  93. package/src/editor/ui/UIDialog.svelte +11 -9
  94. package/src/editor/ui/UIPillButton.svelte +7 -2
  95. package/src/editor/ui/colors/ColorWheel.svelte +34 -1
  96. package/src/editor/ui/curveEngine.ts +48 -0
  97. package/src/editor/ui/index.ts +4 -2
  98. package/src/editor/ui/palette/PaletteBase.svelte +75 -51
  99. package/src/editor/ui/palette/PaletteJumpButton.svelte +3 -8
  100. package/src/live-tokens/data/colors-and-type/autumn.json +2490 -0
  101. package/src/live-tokens/data/{themes → colors-and-type}/default.json +816 -293
  102. package/src/live-tokens/data/colors-and-type/halloween.json +2529 -0
  103. package/src/live-tokens/data/colors-and-type/midnight-study.json +2536 -0
  104. package/src/live-tokens/data/colors-and-type/ocean.json +2489 -0
  105. package/src/live-tokens/data/colors-and-type/royal-velvet.json +2520 -0
  106. package/src/live-tokens/data/colors-and-type/spring-meadow.json +2476 -0
  107. package/src/live-tokens/data/colors-and-type/sunset.json +2528 -0
  108. package/src/live-tokens/data/themes/autumn.json +3918 -0
  109. package/src/live-tokens/data/themes/halloween.json +3992 -0
  110. package/src/live-tokens/data/themes/midnight-study.json +3932 -0
  111. package/src/live-tokens/data/themes/ocean.json +3917 -0
  112. package/src/live-tokens/data/themes/royal-velvet.json +3983 -0
  113. package/src/live-tokens/data/themes/spring-meadow.json +3904 -0
  114. package/src/live-tokens/data/themes/sunset.json +3931 -0
  115. package/src/live-tokens/data/tokens.generated.css +106 -117
  116. package/src/system/styles/CONVENTIONS.md +1 -1
  117. package/src/system/styles/fonts.css +1 -1
  118. package/template/README.md +7 -5
  119. package/template/_gitignore +0 -6
  120. package/src/editor/core/components/componentPersist.ts +0 -62
  121. package/src/editor/core/manifests/manifestService.ts +0 -172
  122. package/src/editor/ui/ManifestFileManager.svelte +0 -446
  123. package/src/editor/ui/ThemeFileManager.svelte +0 -785
  124. package/src/live-tokens/data/manifests/default.json +0 -35
@@ -16,34 +16,13 @@
16
16
  setComponentAliasLinked,
17
17
  clearComponentAliasLinked,
18
18
  } from '../../core/store/editorStore';
19
+ import { rawKind, type TokenKind } from '../../core/components/aliasKinds';
19
20
  import { getEditorContext } from './editorContext';
20
21
  import type { Token } from './types';
21
22
 
22
23
  /** Selector kind. `padding-split` is `padding` whose per-side variables exist;
23
24
  it renders the four-sided field group instead of the single-value row. */
24
- type Kind =
25
- | 'surface'
26
- | 'border'
27
- | 'border-width'
28
- | 'radius'
29
- | 'divider-width'
30
- | 'divider-height'
31
- | 'divider-inset'
32
- | 'dot-size'
33
- | 'blur'
34
- | 'scale'
35
- | 'shadow'
36
- | 'font-family'
37
- | 'font-weight'
38
- | 'font-size'
39
- | 'line-height'
40
- | 'letter-spacing'
41
- | 'padding'
42
- | 'padding-split'
43
- | 'gap'
44
- | 'duration'
45
- | 'easing'
46
- | 'text-color';
25
+ type Kind = TokenKind | 'padding-split';
47
26
 
48
27
  type Entry = { kind: Kind; token: Token };
49
28
 
@@ -82,42 +61,6 @@
82
61
  onchange,
83
62
  }: Props = $props();
84
63
 
85
- /** Suffix/prefix patterns mapped to kinds — single source of truth used by `rawKind`.
86
- Order matters: `-text` must run before `-border`/`-surface` because `--text-*`
87
- would otherwise match `surface`/`border` if any pattern overlapped. Variables
88
- that don't match any pattern fall through to `text-color` (renders as a palette
89
- picker). Tokens with unconventional suffixes should be renamed. */
90
- const KIND_PATTERNS: Array<{ kind: Kind; matches: (v: string) => boolean }> = [
91
- { kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
92
- { kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
93
- { kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') || v.endsWith('-thumb-size') },
94
- { kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
95
- { kind: 'letter-spacing', matches: (v) => v.endsWith('-letter-spacing') },
96
- { kind: 'text-color', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
97
- { kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
98
- { kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
99
- { kind: 'divider-height', matches: (v) => v.endsWith('-divider-height') || v.endsWith('-track-height') },
100
- { kind: 'divider-inset', matches: (v) => v.endsWith('-divider-inset') },
101
- { kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
102
- { kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
103
- { kind: 'scale', matches: (v) => v.endsWith('-scale') || v.startsWith('--scale-') },
104
- { kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
105
- { kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
106
- { kind: 'gap', matches: (v) => v.endsWith('-gap') },
107
- { kind: 'duration', matches: (v) => v.endsWith('-duration') || v.startsWith('--duration-') },
108
- { kind: 'easing', matches: (v) => v.endsWith('-easing') || v.startsWith('--ease-') },
109
- { kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.endsWith('-accent-width') || v.endsWith('-hairline-thickness') || v.startsWith('--border-width-') },
110
- { kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
111
- { kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
112
- ];
113
-
114
- function rawKind(variable: string): Kind {
115
- for (const { kind, matches } of KIND_PATTERNS) {
116
- if (matches(variable)) return kind;
117
- }
118
- return 'text-color';
119
- }
120
-
121
64
  /** Fixed internal order for tokens within a layout. `padding-split` co-orders with `padding`. */
122
65
  const baseKindOrder: Kind[] = [
123
66
  'font-family',
@@ -169,7 +169,7 @@
169
169
  const dstTypeGroups = typeGroups[toState] ?? [];
170
170
 
171
171
  mutate(`copy ${fromVariant}/${fromState} → ${name}/${toState}`, (s) => {
172
- const slice = s.components[component!] ?? (s.components[component!] = { activeFile: 'default', aliases: {}, config: {} });
172
+ const slice = s.components[component!] ?? (s.components[component!] = { aliases: {}, config: {} });
173
173
  const dstVarsTouched: string[] = [];
174
174
  /** Resolve a variable's effective value as a CSS string: the override if
175
175
  set, otherwise its declared default. Returns null if neither exists.
@@ -0,0 +1,180 @@
1
+ import { matchesKind } from './aliasKinds';
2
+ import type { AliasDiskValue, ComponentConfig } from '../themes/themeTypes';
3
+
4
+ export type AdjustKind = 'radius' | 'padding' | 'gap' | 'border-width';
5
+
6
+ export interface AdjustOp {
7
+ /** Component id; omitted applies the op to every config. */
8
+ target?: string;
9
+ kind: AdjustKind;
10
+ set?: string;
11
+ shift?: number;
12
+ /** Radius shifts only: admits `--radius-full` as the ladder's top rung. */
13
+ full?: boolean;
14
+ }
15
+
16
+ export type SkipReason = 'raw-value' | 'off-ladder' | 'clamped' | 'pill-preserved';
17
+
18
+ export interface AliasChange {
19
+ variable: string;
20
+ from: string;
21
+ to: string;
22
+ }
23
+
24
+ export interface AliasSkip {
25
+ variable: string;
26
+ value: AliasDiskValue;
27
+ reason: SkipReason;
28
+ }
29
+
30
+ export interface ComponentReport {
31
+ component: string;
32
+ changes: AliasChange[];
33
+ skips: AliasSkip[];
34
+ }
35
+
36
+ export interface AdjustReport {
37
+ components: ComponentReport[];
38
+ }
39
+
40
+ export interface AdjustResult {
41
+ configs: Record<string, ComponentConfig>;
42
+ report: AdjustReport;
43
+ }
44
+
45
+ const RADIUS_FULL = '--radius-full';
46
+
47
+ const RADIUS_RUNGS = [
48
+ '--radius-none', '--radius-sm', '--radius-md', '--radius-lg', '--radius-xl',
49
+ '--radius-2xl', '--radius-3xl', '--radius-4xl',
50
+ ];
51
+
52
+ /** The editor picker's 12-step subset (`UIPaddingSelector`), not the full
53
+ `--space-*` scale: every value this engine writes must stay re-editable
54
+ by hand in the picker. */
55
+ const SPACE_RUNGS = [
56
+ '--space-0', '--space-2', '--space-4', '--space-6', '--space-8', '--space-10',
57
+ '--space-12', '--space-16', '--space-20', '--space-24', '--space-32', '--space-48',
58
+ ];
59
+
60
+ const SPACE_FAMILY = [...SPACE_RUNGS, '--space-40', '--space-64', '--space-96', '--space-128'];
61
+
62
+ const BORDER_WIDTH_RUNGS = [
63
+ '--border-width-0', '--border-width-1', '--border-width-2', '--border-width-3',
64
+ '--border-width-4', '--border-width-5', '--border-width-6', '--border-width-8',
65
+ '--border-width-10', '--border-width-12', '--border-width-16', '--border-width-20',
66
+ '--border-width-24',
67
+ ];
68
+
69
+ const LADDERS: Record<AdjustKind, { rungs: string[]; family: string[] }> = {
70
+ radius: { rungs: RADIUS_RUNGS, family: [...RADIUS_RUNGS, RADIUS_FULL] },
71
+ padding: { rungs: SPACE_RUNGS, family: SPACE_FAMILY },
72
+ gap: { rungs: SPACE_RUNGS, family: SPACE_FAMILY },
73
+ 'border-width': { rungs: BORDER_WIDTH_RUNGS, family: BORDER_WIDTH_RUNGS },
74
+ };
75
+
76
+ const TOKEN_NAME = /^--[a-z0-9-]+$/;
77
+
78
+ type Outcome = { from: string; to: string } | { skip: SkipReason };
79
+
80
+ function rungsFor(op: AdjustOp): string[] {
81
+ return op.kind === 'radius' && op.full ? [...RADIUS_RUNGS, RADIUS_FULL] : LADDERS[op.kind].rungs;
82
+ }
83
+
84
+ function spaceStep(token: string): number {
85
+ return Number(token.slice('--space-'.length));
86
+ }
87
+
88
+ /** `<=` sends an exact tie (`--space-40`, midway between 32 and 48) upward. */
89
+ function nearestSpaceRung(token: string, rungs: string[]): number {
90
+ const step = spaceStep(token);
91
+ let best = 0;
92
+ for (let i = 1; i < rungs.length; i++) {
93
+ if (Math.abs(spaceStep(rungs[i]) - step) <= Math.abs(spaceStep(rungs[best]) - step)) best = i;
94
+ }
95
+ return best;
96
+ }
97
+
98
+ function resolve(op: AdjustOp, value: AliasDiskValue): Outcome {
99
+ if (typeof value !== 'string' || !TOKEN_NAME.test(value)) return { skip: 'raw-value' };
100
+ if (!LADDERS[op.kind].family.includes(value)) return { skip: 'off-ladder' };
101
+ if (op.set !== undefined) return { from: value, to: op.set };
102
+
103
+ const rungs = rungsFor(op);
104
+ let index = rungs.indexOf(value);
105
+ if (index < 0) {
106
+ if (value === RADIUS_FULL) return { skip: 'pill-preserved' };
107
+ index = nearestSpaceRung(value, rungs);
108
+ }
109
+
110
+ const wanted = index + op.shift!;
111
+ const landed = Math.min(Math.max(wanted, 0), rungs.length - 1);
112
+ if (landed !== wanted && rungs[landed] === value) return { skip: 'clamped' };
113
+ return { from: value, to: rungs[landed] };
114
+ }
115
+
116
+ function validateOp(op: AdjustOp, configs: Record<string, ComponentConfig>): void {
117
+ const ladder = LADDERS[op.kind];
118
+ if (!ladder) throw new Error(`Unknown kind "${op.kind}" (expected ${Object.keys(LADDERS).join(', ')})`);
119
+ if (op.target !== undefined && !(op.target in configs)) {
120
+ throw new Error(`Unknown target component "${op.target}"`);
121
+ }
122
+
123
+ const hasSet = op.set !== undefined;
124
+ const hasShift = op.shift !== undefined;
125
+ if (hasSet === hasShift) throw new Error(`A ${op.kind} op needs exactly one of "set" or "shift"`);
126
+ const settable = op.kind === 'radius' ? ladder.family : ladder.rungs;
127
+ if (hasSet && !settable.includes(op.set!)) {
128
+ throw new Error(`"${op.set}" is not on the ${op.kind} ladder`);
129
+ }
130
+ if (hasShift && !Number.isInteger(op.shift)) {
131
+ throw new Error(`"shift" must be a whole number of steps, got ${op.shift}`);
132
+ }
133
+ if (op.full !== undefined && !(op.kind === 'radius' && hasShift)) {
134
+ throw new Error('"full" applies to radius shifts only');
135
+ }
136
+ }
137
+
138
+ export function adjustAliases(
139
+ configs: Record<string, ComponentConfig>,
140
+ ops: readonly AdjustOp[],
141
+ now: string,
142
+ ): AdjustResult {
143
+ for (const op of ops) validateOp(op, configs);
144
+
145
+ const next = { ...configs };
146
+ const reports = new Map<string, ComponentReport>();
147
+ const reportFor = (component: string): ComponentReport => {
148
+ let report = reports.get(component);
149
+ if (!report) {
150
+ report = { component, changes: [], skips: [] };
151
+ reports.set(component, report);
152
+ }
153
+ return report;
154
+ };
155
+
156
+ for (const op of ops) {
157
+ for (const component of op.target ? [op.target] : Object.keys(next)) {
158
+ const config = next[component];
159
+ const aliases = { ...config.aliases };
160
+ let changed = false;
161
+
162
+ for (const [variable, value] of Object.entries(config.aliases)) {
163
+ if (!matchesKind(variable, op.kind)) continue;
164
+ const outcome = resolve(op, value);
165
+ if ('skip' in outcome) {
166
+ reportFor(component).skips.push({ variable, value, reason: outcome.skip });
167
+ continue;
168
+ }
169
+ if (outcome.to === outcome.from) continue;
170
+ aliases[variable] = outcome.to;
171
+ reportFor(component).changes.push({ variable, from: outcome.from, to: outcome.to });
172
+ changed = true;
173
+ }
174
+
175
+ if (changed) next[component] = { ...config, aliases, updatedAt: now };
176
+ }
177
+ }
178
+
179
+ return { configs: next, report: { components: [...reports.values()] } };
180
+ }
@@ -0,0 +1,73 @@
1
+ export type TokenKind =
2
+ | 'surface'
3
+ | 'border'
4
+ | 'border-width'
5
+ | 'radius'
6
+ | 'divider-width'
7
+ | 'divider-height'
8
+ | 'divider-inset'
9
+ | 'dot-size'
10
+ | 'blur'
11
+ | 'scale'
12
+ | 'shadow'
13
+ | 'font-family'
14
+ | 'font-weight'
15
+ | 'font-size'
16
+ | 'line-height'
17
+ | 'letter-spacing'
18
+ | 'padding'
19
+ | 'gap'
20
+ | 'duration'
21
+ | 'easing'
22
+ | 'text-color';
23
+
24
+ /** Suffix/prefix patterns mapped to kinds — the one source of truth for the
25
+ editor's selector layout and the `adjust` CLI, so the two cannot drift.
26
+ Order matters: `-text` must run before `-border`/`-surface` because `--text-*`
27
+ would otherwise match `surface`/`border` if any pattern overlapped. Variables
28
+ that don't match any pattern fall through to `text-color` (renders as a palette
29
+ picker). Tokens with unconventional suffixes should be renamed. */
30
+ export const KIND_PATTERNS: ReadonlyArray<{ kind: TokenKind; matches: (v: string) => boolean }> = [
31
+ { kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
32
+ { kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
33
+ { kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') || v.endsWith('-thumb-size') },
34
+ { kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
35
+ { kind: 'letter-spacing', matches: (v) => v.endsWith('-letter-spacing') },
36
+ { kind: 'text-color', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
37
+ { kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
38
+ { kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
39
+ { kind: 'divider-height', matches: (v) => v.endsWith('-divider-height') || v.endsWith('-track-height') },
40
+ { kind: 'divider-inset', matches: (v) => v.endsWith('-divider-inset') },
41
+ { kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
42
+ { kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
43
+ { kind: 'scale', matches: (v) => v.endsWith('-scale') || v.startsWith('--scale-') },
44
+ { kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
45
+ { kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
46
+ { kind: 'gap', matches: (v) => v.endsWith('-gap') },
47
+ { kind: 'duration', matches: (v) => v.endsWith('-duration') || v.startsWith('--duration-') },
48
+ { kind: 'easing', matches: (v) => v.endsWith('-easing') || v.startsWith('--ease-') },
49
+ { kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.endsWith('-accent-width') || v.endsWith('-hairline-thickness') || v.startsWith('--border-width-') },
50
+ { kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
51
+ { kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
52
+ ];
53
+
54
+ export function rawKind(variable: string): TokenKind {
55
+ for (const { kind, matches } of KIND_PATTERNS) {
56
+ if (matches(variable)) return kind;
57
+ }
58
+ return 'text-color';
59
+ }
60
+
61
+ const MATCHER_BY_KIND = Object.fromEntries(
62
+ KIND_PATTERNS.map((p) => [p.kind, p.matches]),
63
+ ) as Record<TokenKind, (v: string) => boolean>;
64
+
65
+ const SIDE_SUFFIXES = ['-top', '-right', '-bottom', '-left'];
66
+
67
+ /** Side-aware kind test. Per-side paddings (`--card-default-body-padding-top`)
68
+ are written by UIPaddingSelector and never declared as editor tokens, so
69
+ `rawKind` never meets one; the CLI does, and they belong with their parent. */
70
+ export function matchesKind(variable: string, kind: TokenKind): boolean {
71
+ const side = SIDE_SUFFIXES.find((s) => variable.endsWith(s));
72
+ return MATCHER_BY_KIND[kind](side ? variable.slice(0, -side.length) : variable);
73
+ }
@@ -1,37 +1,25 @@
1
- import type { AliasDiskValue, ComponentConfig, ComponentConfigMeta } from '../themes/themeTypes';
1
+ import type { ComponentConfig, ComponentConfigMeta, LiveSource } from '../themes/themeTypes';
2
2
  import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
3
3
  import { API_BASE } from '../storage/apiBase';
4
+ import { liveMovedSinceBake } from '../productionPulse';
4
5
 
5
6
  /**
6
- * REST client for per-component config files. Parallel to `themeService.ts`
7
+ * REST client for per-component config files. Parallel to `colorsAndTypeService.ts`
7
8
  * but scoped to `${API_BASE}/component-configs/*`. Each component (button,
8
- * card, …) has its own lifecycle: default.json (generated from the `.svelte`
9
- * source), plus user-authored named configs, each with its own active /
10
- * production pointer.
11
- *
12
- * Both this and `themeService` consume `versionedFileResource(...)`. Adding a
13
- * third file-managed resource — per the user's "mirror theme-file lifecycle
14
- * for new editor artifacts" invariant — is one helper call here, not a third
15
- * round of copy-paste CRUD.
9
+ * card, …) has a `default.json` generated from its `.svelte` source, a reserved
10
+ * `_working.json` buffer holding whatever the editor is running, and any number
11
+ * of user-saved presets.
16
12
  */
17
13
 
18
14
  export interface ComponentSummary {
19
15
  name: string;
20
- activeFile: string;
21
- productionFile: string;
22
- }
23
-
24
- export interface ComponentProductionInfo {
25
- fileName: string;
26
- name: string;
27
- aliases: Record<string, AliasDiskValue>;
16
+ /** Where the component's live config resolves from. */
17
+ source: LiveSource;
28
18
  }
29
19
 
30
20
  export interface ComponentConfigList {
31
21
  component: string;
32
22
  files: ComponentConfigMeta[];
33
- activeFile: string;
34
- productionFile: string;
35
23
  }
36
24
 
37
25
  export async function listComponents(): Promise<ComponentSummary[]> {
@@ -42,19 +30,14 @@ export async function listComponents(): Promise<ComponentSummary[]> {
42
30
  }
43
31
 
44
32
  function resourceFor(component: string) {
45
- return versionedFileResource<ComponentConfig, ComponentConfigMeta, ComponentProductionInfo>({
33
+ return versionedFileResource<ComponentConfig, ComponentConfigMeta>({
46
34
  baseUrl: `${API_BASE}/component-configs/${encodeURIComponent(component)}`,
47
35
  });
48
36
  }
49
37
 
50
38
  export async function listComponentConfigs(component: string): Promise<ComponentConfigList> {
51
39
  const data = await resourceFor(component).list();
52
- return {
53
- component,
54
- files: data.files,
55
- activeFile: data.activeFile ?? 'default',
56
- productionFile: data.productionFile ?? 'default',
57
- };
40
+ return { component, files: data.files };
58
41
  }
59
42
 
60
43
  export const loadComponentConfig = (
@@ -71,19 +54,24 @@ export const saveComponentConfig = (
71
54
  export const deleteComponentConfig = (component: string, fileName: string): Promise<void> =>
72
55
  resourceFor(component).remove(fileName);
73
56
 
57
+ /** The config the component is running: the buffer, else the open theme's
58
+ * copy, else the component default. `_source` says which. */
74
59
  export const getActiveComponentConfig = (component: string): Promise<ComponentConfig | null> =>
75
60
  resourceFor(component).getActive();
76
61
 
77
- export const setActiveComponentFile = (component: string, fileName: string): Promise<void> =>
78
- resourceFor(component).setActive(fileName);
79
-
80
- export const getComponentProductionInfo = (component: string): Promise<ComponentProductionInfo> =>
81
- resourceFor(component).getProductionInfo();
82
-
83
- export async function setComponentProductionFile(
62
+ /** Write the component's unsaved buffer. */
63
+ export async function writeWorkingComponentConfig(
84
64
  component: string,
85
- fileName: string,
86
- ): Promise<{ ok: boolean; productionFile: string }> {
87
- const data = await resourceFor(component).setProduction(fileName);
88
- return { ok: data.ok, productionFile: data.fileName };
65
+ data: ComponentConfig,
66
+ ): Promise<void> {
67
+ const res = await fetch(`${API_BASE}/component-configs/${encodeURIComponent(component)}/working`, {
68
+ method: 'PUT',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify(data),
71
+ });
72
+ if (!res.ok) {
73
+ const err = await res.json().catch(() => ({ error: 'Write failed' }));
74
+ throw new Error(err.error || 'Write failed');
75
+ }
76
+ liveMovedSinceBake.set(true);
89
77
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Centralised "flash a transient status, then revert to idle" helper for the
3
- * file managers (theme / component / manifest). Each pulse — saved, error,
3
+ * file managers (colors and type / component / theme). Each pulse — saved, error,
4
4
  * adopted — shares the same timing so the UI stays in sync.
5
5
  */
6
6
 
@@ -1,4 +1,4 @@
1
- import type { FontFamily, FontSource, FontStack, Theme } from '../themes/themeTypes';
1
+ import type { FontFamily, FontSource, FontStack, ColorsAndType } from '../themes/themeTypes';
2
2
 
3
3
  // Google Fonts CDN URLs for the default font families. We used to ship local
4
4
  // woff2 files and emit @font-face blocks pointing at them, but Vite's url()
@@ -30,7 +30,7 @@ function fam(sourceId: string, name: string, cssName?: string, weights?: number[
30
30
  }
31
31
 
32
32
  /**
33
- * Build the default fontSources. Used when a theme has no fontSources yet.
33
+ * Build the default fontSources. Used when a file has no fontSources yet.
34
34
  */
35
35
  export function defaultFontSources(): FontSource[] {
36
36
  const typekitId = 'src_typekit_jes8oow';
@@ -143,29 +143,29 @@ function migrateLegacyLocalFonts(src: FontSource): boolean {
143
143
  }
144
144
 
145
145
  /**
146
- * Ensure the loaded Theme has fontSources and fontStacks. Mutates in place
147
- * only when missing; safe to call on already-migrated themes. Also strips any
146
+ * Ensure the loaded colors and type have fontSources and fontStacks. Mutates in place
147
+ * only when missing; safe to call on already-migrated files. Also strips any
148
148
  * stale --font-* entries from cssVariables since those are now derived, and
149
149
  * migrates legacy local-font sources to Google Fonts URL sources.
150
150
  */
151
- export function migrateThemeFonts(theme: Theme): { migrated: boolean } {
151
+ export function migrateColorsAndTypeFonts(colorsAndType: ColorsAndType): { migrated: boolean } {
152
152
  let migrated = false;
153
- if (!theme.fontSources || theme.fontSources.length === 0) {
154
- theme.fontSources = defaultFontSources();
153
+ if (!colorsAndType.fontSources || colorsAndType.fontSources.length === 0) {
154
+ colorsAndType.fontSources = defaultFontSources();
155
155
  migrated = true;
156
156
  } else {
157
- for (const src of theme.fontSources) {
157
+ for (const src of colorsAndType.fontSources) {
158
158
  if (migrateLegacyLocalFonts(src)) migrated = true;
159
159
  }
160
160
  }
161
- if (!theme.fontStacks || theme.fontStacks.length === 0) {
162
- theme.fontStacks = defaultFontStacks(theme.fontSources);
161
+ if (!colorsAndType.fontStacks || colorsAndType.fontStacks.length === 0) {
162
+ colorsAndType.fontStacks = defaultFontStacks(colorsAndType.fontSources);
163
163
  migrated = true;
164
164
  }
165
- if (theme.cssVariables) {
165
+ if (colorsAndType.cssVariables) {
166
166
  for (const key of ['--font-display', '--font-sans', '--font-serif', '--font-mono']) {
167
- if (key in theme.cssVariables) {
168
- delete theme.cssVariables[key];
167
+ if (key in colorsAndType.cssVariables) {
168
+ delete colorsAndType.cssVariables[key];
169
169
  migrated = true;
170
170
  }
171
171
  }
@@ -0,0 +1,35 @@
1
+ import type { FontFamily, FontSource, FontStack, FontStackVariable } from '../themes/themeTypes';
2
+
3
+ /**
4
+ * The two faces a theme is recognised by, for the read-only identity line:
5
+ * headings then body, named as the user named them rather than as a CSS
6
+ * cascade. The first slot that resolves is the face the page actually shows.
7
+ */
8
+
9
+ const PAIRING: FontStackVariable[] = ['--font-display', '--font-sans'];
10
+
11
+ function slotLabel(
12
+ stack: FontStack | undefined,
13
+ familyById: Map<string, FontFamily>,
14
+ ): string | null {
15
+ for (const slot of stack?.slots ?? []) {
16
+ if (slot.kind === 'project') {
17
+ const family = familyById.get(slot.familyId);
18
+ if (family) return family.name;
19
+ continue;
20
+ }
21
+ if (slot.kind === 'system') return 'System';
22
+ return slot.value;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export function fontPairingLabel(stacks: FontStack[], sources: FontSource[]): string {
28
+ const familyById = new Map<string, FontFamily>();
29
+ for (const source of sources) {
30
+ for (const family of source.families) familyById.set(family.id, family);
31
+ }
32
+ return PAIRING.map((variable) => slotLabel(stacks.find((s) => s.variable === variable), familyById))
33
+ .filter((label): label is string => label !== null)
34
+ .join(' / ');
35
+ }