@motion-proto/live-tokens 0.9.0 → 0.11.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 (65) hide show
  1. package/README.md +50 -29
  2. package/dist-plugin/index.cjs +177 -125
  3. package/dist-plugin/index.d.cts +3 -2
  4. package/dist-plugin/index.d.ts +3 -2
  5. package/dist-plugin/index.js +177 -125
  6. package/package.json +4 -1
  7. package/src/editor/component-editor/BadgeEditor.svelte +44 -42
  8. package/src/editor/component-editor/ButtonEditor.svelte +224 -0
  9. package/src/editor/component-editor/CardEditor.svelte +2 -0
  10. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
  11. package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
  12. package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
  13. package/src/editor/component-editor/InputEditor.svelte +272 -0
  14. package/src/editor/component-editor/NotificationEditor.svelte +44 -65
  15. package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
  16. package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
  17. package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
  18. package/src/editor/component-editor/registry.ts +35 -2
  19. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
  20. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +10 -1
  21. package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
  22. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
  23. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
  24. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
  25. package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
  26. package/src/editor/component-editor/scaffolding/types.ts +2 -1
  27. package/src/editor/core/components/componentConfigService.ts +7 -6
  28. package/src/editor/core/manifests/manifestService.ts +5 -4
  29. package/src/editor/core/storage/apiBase.ts +15 -0
  30. package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
  31. package/src/editor/core/store/editorRenderer.ts +1 -4
  32. package/src/editor/core/store/editorStore.ts +5 -9
  33. package/src/editor/core/store/editorTypes.ts +6 -13
  34. package/src/editor/core/themes/migrations/2026-05-13-primary-to-brand.ts +2 -29
  35. package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
  36. package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
  37. package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
  38. package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
  39. package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
  40. package/src/editor/core/themes/migrations/2026-05-26-drop-overlay-extra-stops.ts +46 -0
  41. package/src/editor/core/themes/migrations/index.ts +16 -0
  42. package/src/editor/core/themes/slices/overlays.ts +44 -75
  43. package/src/editor/core/themes/themeInit.ts +3 -2
  44. package/src/editor/core/themes/themeService.ts +3 -2
  45. package/src/editor/ui/SurfacesTab.svelte +3 -7
  46. package/src/editor/ui/UIEasingSelector.svelte +240 -0
  47. package/src/editor/ui/UIPaddingSelector.svelte +2 -2
  48. package/src/editor/ui/UIPaletteSelector.svelte +151 -36
  49. package/src/editor/ui/{UIRelinkConfirmPopover.svelte → UIRelinkConfirmDialog.svelte} +107 -75
  50. package/src/editor/ui/UITokenSelector.svelte +15 -2
  51. package/src/editor/ui/sections/OverlaysSection.svelte +107 -540
  52. package/src/editor/ui/variantScales.ts +34 -0
  53. package/src/system/components/Button.svelte +34 -85
  54. package/src/system/components/Card.svelte +2 -1
  55. package/src/system/components/CollapsibleSection.svelte +1 -48
  56. package/src/system/components/CornerBadge.svelte +72 -138
  57. package/src/system/components/ImageLightbox.svelte +578 -0
  58. package/src/system/components/Input.svelte +387 -0
  59. package/src/system/components/ProgressBar.svelte +62 -258
  60. package/src/system/components/SegmentedControl.svelte +81 -15
  61. package/src/system/components/SideNavigation.svelte +777 -0
  62. package/src/system/components/TabBar.svelte +1 -1
  63. package/src/system/styles/tokens.css +48 -5
  64. package/src/system/styles/tokens.generated.css +33 -185
  65. package/src/editor/component-editor/StandardButtonsEditor.svelte +0 -190
@@ -28,6 +28,7 @@
28
28
  import { CURRENT_COMPONENT_SCHEMA_VERSION } from '../../core/themes/migrations';
29
29
  import type { CssVarRef } from '../../core/store/editorTypes';
30
30
  import { safeFetch } from '../../core/storage/storage';
31
+ import { API_BASE } from '../../core/storage/apiBase';
31
32
  import { flashStatus } from '../../core/flashStatus';
32
33
  import ComponentFileMenu from './ComponentFileMenu.svelte';
33
34
  import SaveAsDialog from './SaveAsDialog.svelte';
@@ -91,7 +92,7 @@
91
92
  const data = await safeFetch<{
92
93
  files: ComponentConfigMeta[];
93
94
  activeFile: string;
94
- }>(`/api/component-configs/${encodeURIComponent(component)}`);
95
+ }>(`${API_BASE}/component-configs/${encodeURIComponent(component)}`);
95
96
  if (!data) return;
96
97
  files = data.files;
97
98
  activeFileName = data.activeFile;
@@ -103,7 +104,7 @@
103
104
  // Preserve existing productionInfo on transient fetch failure rather than
104
105
  // clobbering it to null — same behaviour as the previous empty catch.
105
106
  const info = await safeFetch<ComponentProductionInfo>(
106
- `/api/component-configs/${encodeURIComponent(component)}/production`,
107
+ `${API_BASE}/component-configs/${encodeURIComponent(component)}/production`,
107
108
  );
108
109
  if (info) productionInfo = info;
109
110
  }
@@ -53,7 +53,6 @@
53
53
  min-height: 12rem;
54
54
  border-radius: var(--ui-radius-md);
55
55
  box-sizing: border-box;
56
- overflow: hidden;
57
56
  }
58
57
 
59
58
  .shadow-backdrop.with-controls {
@@ -61,12 +60,22 @@
61
60
  grid-template-areas: "preview controls";
62
61
  }
63
62
 
63
+ /* Clip slotted children (e.g. Dialog's full-bleed overlay in padding=0 mode)
64
+ to the backdrop's rounded corners here rather than on the wrapper, so the
65
+ controls cell's dropdowns can escape vertically. The wrapper's image/color
66
+ background is still clipped naturally by its own border-radius. */
64
67
  .shadow-backdrop-content {
65
68
  display: grid;
66
69
  align-items: center;
67
70
  justify-items: start;
68
71
  min-width: 0;
69
72
  grid-area: preview;
73
+ border-radius: var(--ui-radius-md);
74
+ overflow: hidden;
75
+ }
76
+
77
+ .shadow-backdrop.with-controls .shadow-backdrop-content {
78
+ border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
70
79
  }
71
80
 
72
81
  .shadow-backdrop-controls {
@@ -250,19 +250,18 @@
250
250
  padding-top: calc(var(--ui-font-size-xs) + var(--ui-space-4));
251
251
  }
252
252
 
253
- /* Element-grouped mode: subsections fan out across available width, each
254
- labeled by the element it targets (e.g. Frame / Header / Body). Three
255
- columns at typical editor widths, dropping to two then one as the panel
256
- narrows the auto-fit + minmax does the responsive work without media
257
- queries. `align-items: start` keeps columns of different heights aligned
258
- to their top edge instead of stretching the shorter ones.
253
+ /* Element-grouped mode: subsections sit flush against their content width
254
+ with a 1rem gap between them, wrapping to a new row when the panel is too
255
+ narrow to fit them side-by-side. Flex (not grid 1fr) so wide viewports
256
+ don't spread the columns apart sections cluster left and consume only
257
+ as much width as their controls need.
259
258
  Within a section the two-col split (typography fieldsets + property grid)
260
259
  still applies when the section has both. */
261
260
  .state-controls.element-grouped {
262
- display: grid;
263
- grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
264
- gap: var(--ui-space-20) var(--ui-space-32);
265
- align-items: start;
261
+ display: flex;
262
+ flex-wrap: wrap;
263
+ gap: var(--ui-space-16);
264
+ align-items: flex-start;
266
265
  }
267
266
 
268
267
  /* Each element section stacks typography fieldset(s) above the property
@@ -7,7 +7,9 @@
7
7
  import UIFontSizeSelector from '../../ui/UIFontSizeSelector.svelte';
8
8
  import UILineHeightSelector from '../../ui/UILineHeightSelector.svelte';
9
9
  import UIPaddingSelector from '../../ui/UIPaddingSelector.svelte';
10
- import { BLUR, BORDER_WIDTH, DOT_SIZE, RADIUS, SHADOW, DIVIDER_HEIGHT } from '../../ui/variantScales';
10
+ import UILetterSpacingSelector from '../../ui/UILetterSpacingSelector.svelte';
11
+ import UIEasingSelector from '../../ui/UIEasingSelector.svelte';
12
+ import { BLUR, BORDER_WIDTH, DOT_SIZE, DURATION, RADIUS, SHADOW, DIVIDER_HEIGHT, DIVIDER_INSET } from '../../ui/variantScales';
11
13
  import {
12
14
  editorState,
13
15
  getComponentPropertySiblings,
@@ -26,6 +28,7 @@
26
28
  | 'radius'
27
29
  | 'divider-width'
28
30
  | 'divider-height'
31
+ | 'divider-inset'
29
32
  | 'dot-size'
30
33
  | 'blur'
31
34
  | 'shadow'
@@ -33,10 +36,13 @@
33
36
  | 'font-weight'
34
37
  | 'font-size'
35
38
  | 'line-height'
39
+ | 'letter-spacing'
36
40
  | 'padding'
37
41
  | 'padding-split'
38
42
  | 'gap'
39
- | 'extras';
43
+ | 'duration'
44
+ | 'easing'
45
+ | 'text-color';
40
46
 
41
47
  type Entry = { kind: Kind; token: Token };
42
48
 
@@ -75,44 +81,61 @@
75
81
  onchange,
76
82
  }: Props = $props();
77
83
 
78
- /** Suffix/prefix patterns mapped to kinds — single source of truth used by `categorize`.
79
- Order matters: `text` must run before `border`/`surface` because `--text-*` would
80
- otherwise match `surface` checks if any pattern overlapped. */
84
+ /** Suffix/prefix patterns mapped to kinds — single source of truth used by `rawKind`.
85
+ Order matters: `-text` must run before `-border`/`-surface` because `--text-*`
86
+ would otherwise match `surface`/`border` if any pattern overlapped. Variables
87
+ that don't match any pattern fall through to `text-color` (renders as a palette
88
+ picker). Tokens with unconventional suffixes should be renamed. */
81
89
  const KIND_PATTERNS: Array<{ kind: Kind; matches: (v: string) => boolean }> = [
82
- { kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
83
- { kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
84
- { kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') },
85
- { kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
86
- { kind: 'extras', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
87
- { kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
88
- { kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
90
+ { kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
91
+ { kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
92
+ { kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') },
93
+ { kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
94
+ { kind: 'letter-spacing', matches: (v) => v.endsWith('-letter-spacing') },
95
+ { kind: 'text-color', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
96
+ { kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
97
+ { kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
89
98
  { kind: 'divider-height', matches: (v) => v.endsWith('-divider-height') || v.endsWith('-track-height') },
90
- { kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
91
- { kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
92
- { kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
93
- { kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
94
- { kind: 'gap', matches: (v) => v.endsWith('-gap') },
95
- { kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.endsWith('-accent-width') || v.endsWith('-hairline-thickness') || v.startsWith('--border-width-') },
96
- { kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
97
- { kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
99
+ { kind: 'divider-inset', matches: (v) => v.endsWith('-divider-inset') },
100
+ { kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
101
+ { kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
102
+ { kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
103
+ { kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
104
+ { kind: 'gap', matches: (v) => v.endsWith('-gap') },
105
+ { kind: 'duration', matches: (v) => v.endsWith('-duration') || v.startsWith('--duration-') },
106
+ { kind: 'easing', matches: (v) => v.endsWith('-easing') || v.startsWith('--ease-') },
107
+ { kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.endsWith('-accent-width') || v.endsWith('-hairline-thickness') || v.startsWith('--border-width-') },
108
+ { kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
109
+ { kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
98
110
  ];
99
111
 
112
+ function rawKind(variable: string): Kind {
113
+ for (const { kind, matches } of KIND_PATTERNS) {
114
+ if (matches(variable)) return kind;
115
+ }
116
+ return 'text-color';
117
+ }
118
+
100
119
  /** Fixed internal order for tokens within a layout. `padding-split` co-orders with `padding`. */
101
120
  const baseKindOrder: Kind[] = [
102
121
  'font-family',
103
122
  'font-weight',
104
123
  'font-size',
105
124
  'line-height',
125
+ 'letter-spacing',
106
126
  'divider-width',
107
127
  'divider-height',
128
+ 'divider-inset',
108
129
  'dot-size',
109
130
  'radius',
110
131
  'padding',
111
132
  'padding-split',
112
133
  'gap',
134
+ 'duration',
135
+ 'easing',
113
136
  'blur',
114
137
  'shadow',
115
- 'extras',
138
+ 'text-color',
116
139
  'surface',
117
140
  'border-width',
118
141
  'border',
@@ -121,13 +144,6 @@
121
144
  baseKindOrder.map((k, i) => [k, i]),
122
145
  ) as Record<Kind, number>;
123
146
 
124
- function rawKind(v: string): Kind {
125
- for (const { kind, matches } of KIND_PATTERNS) {
126
- if (matches(v)) return kind;
127
- }
128
- return 'extras';
129
- }
130
-
131
147
  /** A padding token is "split" when its per-side variables exist for this component. */
132
148
  function paddingIsSplit(varName: string, comp: string | undefined, state: typeof $editorState): boolean {
133
149
  const sides = ['top', 'right', 'bottom', 'left'];
@@ -148,8 +164,8 @@
148
164
  }
149
165
 
150
166
  /** For sibling/grouping checks we want the canonical kind, not the split-vs-single distinction. */
151
- function groupingKind(v: string): Kind {
152
- return rawKind(v);
167
+ function groupingKind(variable: string): Kind {
168
+ return rawKind(variable);
153
169
  }
154
170
 
155
171
  /** Selector registry: one entry per kind. `extra` props (e.g. UIPaddingSelector's
@@ -167,9 +183,11 @@
167
183
  'font-weight': { component: UIFontWeightSelector },
168
184
  'font-size': { component: UIFontSizeSelector },
169
185
  'line-height': { component: UILineHeightSelector },
186
+ 'letter-spacing': { component: UILetterSpacingSelector },
170
187
  'border-width': { component: UIVariantSelector, extra: () => ({ ...BORDER_WIDTH }) },
171
188
  'divider-width': { component: UIVariantSelector, extra: () => ({ ...BORDER_WIDTH }) },
172
189
  'divider-height': { component: UIVariantSelector, extra: () => ({ ...DIVIDER_HEIGHT }) },
190
+ 'divider-inset': { component: UIVariantSelector, extra: () => ({ ...DIVIDER_INSET }) },
173
191
  'dot-size': { component: UIVariantSelector, extra: () => ({ ...DOT_SIZE }) },
174
192
  'radius': { component: UIVariantSelector, extra: () => ({ ...RADIUS }) },
175
193
  'padding': { component: UIPaddingSelector, extra: (t) => ({ mode: 'single', splittable: t.splittable !== false }) },
@@ -184,31 +202,37 @@
184
202
  extra: () => ({ mode: 'sides' }),
185
203
  },
186
204
  'gap': { component: UIPaddingSelector, extra: () => ({ mode: 'single', splittable: false }) },
205
+ 'duration': { component: UIVariantSelector, extra: () => ({ ...DURATION }) },
206
+ 'easing': { component: UIEasingSelector },
187
207
  'blur': { component: UIVariantSelector, extra: () => ({ ...BLUR }) },
188
208
  'shadow': { component: UIVariantSelector, extra: () => ({ ...SHADOW }) },
189
209
  'surface': { component: UIPaletteSelector },
190
210
  'border': { component: UIPaletteSelector },
191
- 'extras': { component: UIPaletteSelector },
211
+ 'text-color': { component: UIPaletteSelector },
192
212
  };
193
213
 
194
- /** Multi-col rank: same as `orderRank` but with `extras` (text-color-like) hoisted
195
- between `line-height` and `border-width` so typography reads as one logical
196
- block in column flow. Single-col mode keeps `orderRank` (linked-first sort
197
- already segregates extras to the bottom). */
214
+ /** Multi-col rank: same as `orderRank` but with `text-color` hoisted between
215
+ `line-height` and `divider-width` so typography reads as one logical block
216
+ in column flow. Single-col mode keeps `orderRank` (linked-first sort
217
+ already segregates text-color to the bottom). */
198
218
  const multiColRank: Record<Kind, number> = (() => {
199
219
  const reordered: Kind[] = [
200
220
  'font-family',
201
221
  'font-weight',
202
222
  'font-size',
203
223
  'line-height',
204
- 'extras',
224
+ 'letter-spacing',
225
+ 'text-color',
205
226
  'divider-width',
206
227
  'divider-height',
228
+ 'divider-inset',
207
229
  'dot-size',
208
230
  'radius',
209
231
  'padding',
210
232
  'padding-split',
211
233
  'gap',
234
+ 'duration',
235
+ 'easing',
212
236
  'blur',
213
237
  'shadow',
214
238
  'surface',
@@ -582,10 +582,47 @@
582
582
  }
583
583
 
584
584
  .preview-actions {
585
- margin-left: auto;
585
+ display: inline-flex;
586
+ align-items: center;
587
+ gap: var(--ui-space-12);
588
+ }
589
+
590
+ /* Labels inside previewActions read at the UI body size, matching the
591
+ variant tab strip they sit beside. Greyscale only. */
592
+ .preview-actions :global(label) {
586
593
  display: inline-flex;
587
594
  align-items: center;
588
595
  gap: var(--ui-space-8);
596
+ font-size: var(--ui-font-size-md);
597
+ color: var(--ui-text-secondary);
598
+ }
599
+
600
+ /* Native <select> styled to match the property-row trigger chrome so
601
+ toolbar selects share one visual vocabulary with property dropdowns. */
602
+ .preview-actions :global(select) {
603
+ appearance: none;
604
+ -webkit-appearance: none;
605
+ padding: 0 var(--ui-space-24) 0 var(--ui-space-10);
606
+ min-height: 1.75rem;
607
+ background-color: var(--ui-surface-low);
608
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M3 5l3 3 3-3'/%3E%3C/svg%3E");
609
+ background-repeat: no-repeat;
610
+ background-position: right var(--ui-space-8) center;
611
+ border: 1px solid var(--ui-border);
612
+ border-radius: var(--ui-radius-md);
613
+ color: var(--ui-text-primary);
614
+ font-family: var(--ui-font-sans);
615
+ font-size: var(--ui-font-size-md);
616
+ cursor: pointer;
617
+ transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
618
+ }
619
+ .preview-actions :global(select:hover) {
620
+ background-color: var(--ui-surface-high);
621
+ border-color: var(--ui-border-higher);
622
+ }
623
+ .preview-actions :global(select:focus-visible) {
624
+ outline: 2px solid var(--ui-highlight);
625
+ outline-offset: 2px;
589
626
  }
590
627
 
591
628
  /* Sits in the backdrop's right-rail column (ShadowBackdrop owns the two-column
@@ -31,7 +31,7 @@ export type BuildTypeGroupTokensOptions = {
31
31
  out of the linked block while still appearing in the editor's full token surface
32
32
  (used by the reset-button and the design-token resolution test).
33
33
 
34
- Mirrors the `flatMap`/loop pattern in StandardButtonsEditor and RadioButtonEditor so
34
+ Mirrors the `flatMap`/loop pattern in ButtonEditor and RadioButtonEditor so
35
35
  editors don't have to hand-list 16+ near-identical Token entries. */
36
36
  export function buildTypeGroupTokens(
37
37
  typeGroups: Record<string, TypeGroupConfig[]>,
@@ -13,8 +13,8 @@ export type Sibling = {
13
13
 
14
14
  `variantStates(v)` and `variantTypeGroups(v)` return the same shape the
15
15
  parent VariantGroup gets for its own `states` / `typeGroups` props — a map
16
- keyed by state name. For single-state-per-variant editors (Badge, Notification,
17
- ProgressBar), wrap the single-state builders inline:
16
+ keyed by state name. For single-state-per-variant editors (Badge,
17
+ Notification), wrap the single-state builders inline:
18
18
  `(v) => ({ [v]: variantTokens(v) })`. */
19
19
  export function buildSiblings<V extends string>(
20
20
  variants: readonly V[],
@@ -27,7 +27,8 @@ export type Token = {
27
27
  /** Hint to the editor that this token's alias is a structured payload
28
28
  (currently only `kind: 'gradient'`). Drives Copy-from's per-kind
29
29
  branch — gradient aliases need family-swap of in-family stop colors
30
- rather than a verbatim ref copy. */
30
+ rather than a verbatim ref copy. Distinct from `picker`: `kind`
31
+ marks the value's data shape; `picker` selects the editor control. */
31
32
  kind?: 'gradient';
32
33
  /** Color-family slug for this token's owning variant (e.g. `brand`,
33
34
  `accent`). Set on gradient-kind tokens so Copy-from's family-swap
@@ -1,12 +1,13 @@
1
1
  import type { AliasDiskValue, ComponentConfig, ComponentConfigMeta } from '../themes/themeTypes';
2
2
  import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
3
+ import { API_BASE } from '../storage/apiBase';
3
4
 
4
5
  /**
5
6
  * REST client for per-component config files. Parallel to `themeService.ts`
6
- * but scoped to `/api/component-configs/*`. Each component (button, card, …)
7
- * has its own lifecycle: default.json (generated from the `.svelte` source),
8
- * plus user-authored named configs, each with its own active / production
9
- * pointer.
7
+ * 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.
10
11
  *
11
12
  * Both this and `themeService` consume `versionedFileResource(...)`. Adding a
12
13
  * third file-managed resource — per the user's "mirror theme-file lifecycle
@@ -34,7 +35,7 @@ export interface ComponentConfigList {
34
35
  }
35
36
 
36
37
  export async function listComponents(): Promise<ComponentSummary[]> {
37
- const res = await fetch('/api/component-configs');
38
+ const res = await fetch(`${API_BASE}/component-configs`);
38
39
  if (!res.ok) throw new Error('Failed to list components');
39
40
  const data = await res.json();
40
41
  return data.components;
@@ -42,7 +43,7 @@ export async function listComponents(): Promise<ComponentSummary[]> {
42
43
 
43
44
  function resourceFor(component: string) {
44
45
  return versionedFileResource<ComponentConfig, ComponentConfigMeta, ComponentProductionInfo>({
45
- baseUrl: `/api/component-configs/${encodeURIComponent(component)}`,
46
+ baseUrl: `${API_BASE}/component-configs/${encodeURIComponent(component)}`,
46
47
  });
47
48
  }
48
49
 
@@ -1,5 +1,6 @@
1
1
  import type { Manifest, ManifestMeta, ManifestBundle, Theme, ComponentConfig } from '../themes/themeTypes';
2
2
  import { versionedFileResource } from '../storage/files/versionedFileResourceClient';
3
+ import { API_BASE } from '../storage/apiBase';
3
4
  import { listComponents } from '../components/componentConfigService';
4
5
  import { getActiveTheme } from '../themes/themeService';
5
6
 
@@ -13,7 +14,7 @@ import { getActiveTheme } from '../themes/themeService';
13
14
  */
14
15
 
15
16
  const manifestsResource = versionedFileResource<Manifest, ManifestMeta, never>({
16
- baseUrl: '/api/manifests',
17
+ baseUrl: `${API_BASE}/manifests`,
17
18
  });
18
19
 
19
20
  export const listManifests = async (): Promise<ManifestMeta[]> => {
@@ -47,7 +48,7 @@ export interface ApplyManifestResult {
47
48
  * world" action.
48
49
  */
49
50
  export async function applyManifest(fileName: string): Promise<ApplyManifestResult> {
50
- const res = await fetch(`/api/manifests/${encodeURIComponent(fileName)}/apply`, {
51
+ const res = await fetch(`${API_BASE}/manifests/${encodeURIComponent(fileName)}/apply`, {
51
52
  method: 'PUT',
52
53
  });
53
54
  if (!res.ok) {
@@ -132,7 +133,7 @@ export interface ImportManifestResult {
132
133
  * See temp/manifest-robustness-plan.md §11.
133
134
  */
134
135
  export async function exportManifest(fileName: string): Promise<void> {
135
- const res = await fetch(`/api/manifests/${encodeURIComponent(fileName)}/export`);
136
+ const res = await fetch(`${API_BASE}/manifests/${encodeURIComponent(fileName)}/export`);
136
137
  if (!res.ok) {
137
138
  const err = await res.json().catch(() => ({ error: 'Export failed' }));
138
139
  throw new Error(err.error || 'Export failed');
@@ -158,7 +159,7 @@ export async function exportManifest(fileName: string): Promise<void> {
158
159
  * the rename map so the UI can surface what got renamed.
159
160
  */
160
161
  export async function importManifest(bundle: ManifestBundle): Promise<ImportManifestResult> {
161
- const res = await fetch('/api/manifests/import', {
162
+ const res = await fetch(`${API_BASE}/manifests/import`, {
162
163
  method: 'POST',
163
164
  headers: { 'Content-Type': 'application/json' },
164
165
  body: JSON.stringify(bundle),
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resolved API base path for the editor's REST client. Injected by the
3
+ * `themeFileApi` vite plugin via `config().define` so the client and server
4
+ * always agree on the route prefix, even when a consumer overrides `apiBase`.
5
+ *
6
+ * Fallback (no plugin running) matches the plugin's default. The strings must
7
+ * stay in sync — there is no single source of truth across the build-time /
8
+ * runtime boundary, but the cost of one constant in two files is small.
9
+ */
10
+ declare const __LIVE_TOKENS_API_BASE__: string | undefined;
11
+
12
+ export const API_BASE: string =
13
+ typeof __LIVE_TOKENS_API_BASE__ !== 'undefined' && __LIVE_TOKENS_API_BASE__
14
+ ? __LIVE_TOKENS_API_BASE__
15
+ : '/api/live-tokens';
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  export interface VersionedFileResourceClientOptions {
15
- /** REST endpoint root, e.g. `/api/themes` or `/api/component-configs/button`. */
15
+ /** REST endpoint root, e.g. `${API_BASE}/themes` or `${API_BASE}/component-configs/button`. */
16
16
  baseUrl: string;
17
17
  }
18
18
 
@@ -19,7 +19,6 @@ import {
19
19
  columnsToVars,
20
20
  columnsEqualsDefault,
21
21
  overlaysToVars,
22
- overlaysEqualsDefault,
23
22
  shadowsToVars,
24
23
  gradientsToVars,
25
24
  componentsToVars,
@@ -30,9 +29,7 @@ export function deriveCssVars(state: EditorState): Record<string, string> {
30
29
  if (!columnsEqualsDefault(state.columns)) {
31
30
  Object.assign(out, columnsToVars(state.columns));
32
31
  }
33
- if (!overlaysEqualsDefault(state.overlays)) {
34
- Object.assign(out, overlaysToVars(state.overlays));
35
- }
32
+ Object.assign(out, overlaysToVars(state.overlays));
36
33
  if (state.shadows.tokens.length > 0) {
37
34
  Object.assign(out, shadowsToVars(state.shadows));
38
35
  }
@@ -51,7 +51,6 @@ import {
51
51
  import {
52
52
  loadOverlaysFromVars,
53
53
  makeDefaultOverlaysState,
54
- overlaysEqualsDefault,
55
54
  overlaysToVars,
56
55
  } from '../themes/slices/overlays';
57
56
  import {
@@ -133,14 +132,13 @@ export {
133
132
 
134
133
  export {
135
134
  overlaysToVars,
136
- overlaysEqualsDefault,
137
135
  OVERLAY_VAR_NAMES,
138
136
  applyOverlayVarsToState,
139
137
  makeDefaultOverlaysState,
140
- overlayTokenToRgba,
141
- parseRgba,
142
- RGBA_RE,
143
- HEX_RE,
138
+ makeDefaultOverlayTokens,
139
+ makeDefaultHoverTokens,
140
+ overlayTokenToCss,
141
+ parseOverlayCss,
144
142
  } from '../themes/slices/overlays';
145
143
 
146
144
  export {
@@ -548,9 +546,7 @@ export function toTheme(state: EditorState, meta: { name: string }): Theme {
548
546
  if (!columnsEqualsDefault(state.columns)) {
549
547
  Object.assign(cssVariables, columnsToVars(state.columns));
550
548
  }
551
- if (!overlaysEqualsDefault(state.overlays)) {
552
- Object.assign(cssVariables, overlaysToVars(state.overlays));
553
- }
549
+ Object.assign(cssVariables, overlaysToVars(state.overlays));
554
550
  if (state.shadows.tokens.length > 0) {
555
551
  Object.assign(cssVariables, shadowsToVars(state.shadows));
556
552
  }
@@ -21,19 +21,13 @@ export interface ShadowOverrideFlags {
21
21
  distance: boolean; blur: boolean; size: boolean;
22
22
  }
23
23
 
24
+ /** Overlay stop: an aliased color token + an opacity. Emits as
25
+ * `color-mix(in srgb, var(<alias>) <opacity%>, transparent)`. */
24
26
  export interface OverlayToken {
25
- variable: string; label: string;
26
- r: number; g: number; b: number; opacity: number;
27
- }
28
-
29
- export interface OverlayChannelGlobals {
30
- hue: number; saturation: number; lightness: number;
31
- opacityMin: number; opacityMax: number;
32
- }
33
-
34
- export interface OverlayGlobals {
35
- overlay: OverlayChannelGlobals;
36
- hover: OverlayChannelGlobals;
27
+ variable: string;
28
+ label: string;
29
+ alias: string;
30
+ opacity: number;
37
31
  }
38
32
 
39
33
  export interface ColumnsState {
@@ -134,7 +128,6 @@ export interface EditorState {
134
128
  overlays: {
135
129
  tokens: OverlayToken[];
136
130
  hoverTokens: OverlayToken[];
137
- globals: OverlayGlobals;
138
131
  };
139
132
  columns: ColumnsState;
140
133
  components: Record<string, ComponentSlice>;
@@ -1,26 +1,6 @@
1
1
  import type { Migration } from './index';
2
2
 
3
- /**
4
- * 2026-05-13: rename the `primary` color family to `brand`.
5
- *
6
- * The word "primary" was overloaded — both a color-family slot (alongside
7
- * info/success/danger/etc.) AND the top of the neutral text-emphasis ramp
8
- * (`--text-primary` / `--text-secondary` / `--text-tertiary`). Tokens like
9
- * `--text-primary-color` only existed because `--text-primary` was already
10
- * taken by neutral. Renaming the family to `brand` dissolves the collision:
11
- *
12
- * --color-primary-{step} → --color-brand-{step}
13
- * --surface-primary[-step] → --surface-brand[-step]
14
- * --border-primary[-step] → --border-brand[-step]
15
- * --text-primary-color → --text-brand
16
- * --text-primary-{step} → --text-brand-{step} (step in secondary|tertiary|muted|disabled)
17
- *
18
- * The neutral text ramp (`--text-primary`, `--text-secondary`, …) is **left
19
- * untouched** — it's a different namespace that just happens to share the
20
- * word. The match list below enumerates the brand-family names explicitly so
21
- * a substring-on-`primary` mistake can't clobber neutral text or unrelated
22
- * tokens like `--button-primary-*` (component variant, not family).
23
- */
3
+ // Match list is explicit so the neutral `--text-primary` ramp (different namespace) isn't clobbered.
24
4
 
25
5
  const PALETTE_STEPS = ['100','200','300','400','500','600','700','800','850','900','950'] as const;
26
6
  const SURFACE_SUFFIXES = ['lowest','lower','low','high','higher','highest'] as const;
@@ -43,7 +23,6 @@ function renameToken(name: string): string {
43
23
  return RENAME_MAP[name] ?? name;
44
24
  }
45
25
 
46
- /** Rewrite keys only — used for theme cssVariables (values are hex). */
47
26
  function renameKeys(rawVars: Record<string, string>): Record<string, string> {
48
27
  const out: Record<string, string> = {};
49
28
  for (const [k, v] of Object.entries(rawVars)) {
@@ -52,7 +31,6 @@ function renameKeys(rawVars: Record<string, string>): Record<string, string> {
52
31
  return out;
53
32
  }
54
33
 
55
- /** Rewrite both keys and values — used for component-config aliases. */
56
34
  function renameKeysAndValues(rawVars: Record<string, string>): Record<string, string> {
57
35
  const out: Record<string, string> = {};
58
36
  for (const [k, v] of Object.entries(rawVars)) {
@@ -77,12 +55,7 @@ export const componentMigration_2026_05_13_primaryToBrand: Migration = {
77
55
  apply: renameKeysAndValues,
78
56
  };
79
57
 
80
- /**
81
- * Helper for loadFromFile: rename `theme.editorConfigs.Primary` → `Brand`.
82
- * Lives here (with the rest of the rename) instead of in the migration
83
- * framework because the framework's contract is `Record<string, string>` —
84
- * editorConfigs is a structured palette-config map and can't pass through it.
85
- */
58
+ // Lives outside the migration framework because its contract is Record<string, string>; editorConfigs is structured.
86
59
  export function renamePrimaryPaletteKey<T>(editorConfigs: Record<string, T>): Record<string, T> {
87
60
  if (!('Primary' in editorConfigs) || 'Brand' in editorConfigs) return editorConfigs;
88
61
  const { Primary, ...rest } = editorConfigs;
@@ -0,0 +1,28 @@
1
+ import type { Migration } from './index';
2
+
3
+ /**
4
+ * 2026-05-24: drop the `active` header state from collapsiblesection.
5
+ *
6
+ * The state only paid rent when the component was used as a nav link (an
7
+ * externally-supplied `active={true}` paired with `href`). No consumer was
8
+ * using it, so the runtime now omits both the prop and the `&.active` CSS
9
+ * branch. Strip the matching saved aliases from every variant.
10
+ */
11
+ function dropActiveState(rawVars: Record<string, string>, meta: { component?: string }): Record<string, string> {
12
+ if (meta.component !== 'collapsiblesection') return rawVars;
13
+ const re = /^--collapsiblesection-(chromeless|divider|container)-active-/;
14
+ const out: Record<string, string> = {};
15
+ for (const [key, value] of Object.entries(rawVars)) {
16
+ if (re.test(key)) continue;
17
+ out[key] = value;
18
+ }
19
+ return out;
20
+ }
21
+
22
+ export const componentMigration_2026_05_24_collapsiblesectionDropActiveState: Migration = {
23
+ id: '2026-05-24-collapsiblesection-drop-active-state',
24
+ fromVersion: 14,
25
+ toVersion: 15,
26
+ appliesTo: 'component-config',
27
+ apply: dropActiveState,
28
+ };