@motion-proto/live-tokens 0.6.2 → 0.8.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 (232) hide show
  1. package/README.md +14 -13
  2. package/dist-plugin/index.cjs +854 -226
  3. package/dist-plugin/index.d.cts +2 -1
  4. package/dist-plugin/index.d.ts +2 -1
  5. package/dist-plugin/index.js +852 -225
  6. package/package.json +26 -40
  7. package/src/{styles → app}/site.css +1 -1
  8. package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
  9. package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
  10. package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
  11. package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +37 -30
  12. package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
  13. package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
  14. package/src/editor/component-editor/ImageEditor.svelte +30 -0
  15. package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
  16. package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
  17. package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +67 -38
  18. package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
  19. package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
  20. package/src/editor/component-editor/SectionDividerEditor.svelte +565 -0
  21. package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
  22. package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +29 -21
  23. package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
  24. package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
  25. package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
  26. package/src/editor/component-editor/editors.d.ts +10 -0
  27. package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
  28. package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +54 -15
  29. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
  30. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +151 -424
  31. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
  32. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
  33. package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
  34. package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
  35. package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
  36. package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -12
  37. package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
  38. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  39. package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
  40. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +85 -0
  41. package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
  42. package/src/editor/component-editor/scaffolding/StateBlock.svelte +345 -0
  43. package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +17 -12
  44. package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +13 -1
  45. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +858 -0
  46. package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +1 -0
  47. package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
  48. package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
  49. package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +25 -0
  50. package/src/{lib → editor/core/components}/componentConfigKeys.ts +8 -0
  51. package/src/{lib → editor/core/components}/componentConfigService.ts +3 -3
  52. package/src/{lib → editor/core/components}/componentPersist.ts +11 -9
  53. package/src/editor/core/flashStatus.ts +30 -0
  54. package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
  55. package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
  56. package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
  57. package/src/editor/core/manifests/manifestService.ts +171 -0
  58. package/src/editor/core/palettes/familySwap.ts +99 -0
  59. package/src/{lib → editor/core/palettes}/paletteDerivation.ts +71 -2
  60. package/src/{lib → editor/core/palettes}/tokenRegistry.ts +9 -6
  61. package/src/editor/core/productionPulse.ts +37 -0
  62. package/src/{lib → editor/core/routing}/router.ts +1 -1
  63. package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
  64. package/src/{lib → editor/core/store}/editorCore.ts +24 -8
  65. package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
  66. package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
  67. package/src/{lib → editor/core/store}/editorStore.ts +222 -28
  68. package/src/{lib → editor/core/store}/editorTypes.ts +56 -13
  69. package/src/editor/core/store/gradientSource.ts +192 -0
  70. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  71. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  72. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  73. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  74. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  75. package/src/{lib → editor/core/themes}/migrations/index.ts +10 -0
  76. package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
  77. package/src/{lib → editor/core/themes}/slices/components.ts +20 -6
  78. package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
  79. package/src/{lib → editor/core/themes}/slices/gradients.ts +89 -14
  80. package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
  81. package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
  82. package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
  83. package/src/{lib → editor/core/themes}/themeInit.ts +8 -8
  84. package/src/{lib → editor/core/themes}/themeService.ts +6 -6
  85. package/src/{lib → editor/core/themes}/themeTypes.ts +67 -8
  86. package/src/editor/index.ts +69 -0
  87. package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -1
  88. package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +80 -129
  89. package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
  90. package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
  91. package/src/{pages → editor/pages}/Editor.svelte +4 -4
  92. package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
  93. package/src/{styles → editor/styles}/ui-editor.css +43 -22
  94. package/src/{styles → editor/styles}/ui-form-controls.css +23 -24
  95. package/src/{ui → editor/ui}/BezierCurveEditor.svelte +119 -68
  96. package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
  97. package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +7 -6
  98. package/src/editor/ui/FileLoadList.svelte +367 -0
  99. package/src/editor/ui/FilePill.svelte +80 -0
  100. package/src/editor/ui/FontStackEditor.svelte +499 -0
  101. package/src/editor/ui/GradientEditor.svelte +690 -0
  102. package/src/{ui → editor/ui}/GradientStopPicker.svelte +12 -4
  103. package/src/editor/ui/ManifestFileManager.svelte +438 -0
  104. package/src/{ui → editor/ui}/PaletteEditor.svelte +180 -673
  105. package/src/editor/ui/ProjectFontsSection.svelte +638 -0
  106. package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
  107. package/src/{ui → editor/ui}/TextTab.svelte +3 -3
  108. package/src/editor/ui/ThemeFileManager.svelte +783 -0
  109. package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
  110. package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -7
  111. package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +4 -1
  112. package/src/editor/ui/UIInfoPopover.svelte +243 -0
  113. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  114. package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
  115. package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
  116. package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
  117. package/src/{ui → editor/ui}/UIPaletteSelector.svelte +57 -30
  118. package/src/editor/ui/UIPillButton.svelte +168 -0
  119. package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
  120. package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
  121. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  122. package/src/editor/ui/UISquareButton.svelte +172 -0
  123. package/src/{ui → editor/ui}/UITokenSelector.svelte +14 -11
  124. package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
  125. package/src/{ui → editor/ui}/VariablesTab.svelte +46 -17
  126. package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
  127. package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +24 -47
  128. package/src/{ui → editor/ui}/palette/PaletteBase.svelte +11 -8
  129. package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
  130. package/src/editor/ui/palette/paletteMath.ts +275 -0
  131. package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -18
  132. package/src/{ui → editor/ui}/sections/GradientsSection.svelte +8 -8
  133. package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +18 -18
  134. package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +23 -23
  135. package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
  136. package/src/{components → system/components}/Badge.svelte +0 -36
  137. package/src/{components → system/components}/Button.svelte +2 -2
  138. package/src/{components → system/components}/Card.svelte +34 -60
  139. package/src/{components → system/components}/CollapsibleSection.svelte +25 -2
  140. package/src/{components → system/components}/CornerBadge.svelte +8 -24
  141. package/src/{components → system/components}/Dialog.svelte +1 -1
  142. package/src/system/components/FloatingTokenTags.css +275 -0
  143. package/src/system/components/FloatingTokenTags.svelte +543 -0
  144. package/src/{components → system/components}/InlineEditActions.svelte +6 -4
  145. package/src/system/components/MenuSelect.svelte +229 -0
  146. package/src/{components → system/components}/Notification.svelte +8 -1
  147. package/src/{components → system/components}/ProgressBar.svelte +29 -11
  148. package/src/system/components/SectionDivider.svelte +560 -0
  149. package/src/{components → system/components}/SegmentedControl.svelte +49 -43
  150. package/src/{components → system/components}/TabBar.svelte +81 -65
  151. package/src/{components → system/components}/Table.svelte +17 -3
  152. package/src/{components → system/components}/Tooltip.svelte +6 -4
  153. package/src/system/styles/CONVENTIONS.md +178 -0
  154. package/src/system/styles/fonts.css +20 -0
  155. package/src/system/styles/tokens.css +601 -0
  156. package/src/system/styles/tokens.generated.css +544 -0
  157. package/src/component-editor/ImageEditor.svelte +0 -74
  158. package/src/component-editor/SectionDividerEditor.svelte +0 -265
  159. package/src/component-editor/scaffolding/DividerEditor.svelte +0 -94
  160. package/src/component-editor/scaffolding/GradientCard.svelte +0 -296
  161. package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
  162. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
  163. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
  164. package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
  165. package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
  166. package/src/components/SectionDivider.svelte +0 -483
  167. package/src/data/google-fonts.json +0 -75
  168. package/src/lib/index.ts +0 -68
  169. package/src/lib/presetService.ts +0 -214
  170. package/src/lib/productionPulse.ts +0 -32
  171. package/src/styles/fonts.css +0 -30
  172. package/src/styles/tokens.css +0 -1324
  173. package/src/ui/FontStackEditor.svelte +0 -361
  174. package/src/ui/GradientEditor.svelte +0 -470
  175. package/src/ui/PresetFileManager.svelte +0 -1116
  176. package/src/ui/ProjectFontsSection.svelte +0 -645
  177. package/src/ui/ThemeFileManager.svelte +0 -1020
  178. package/src/ui/UnsavedComponentsDialog.svelte +0 -315
  179. /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
  180. /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
  181. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
  182. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
  183. /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
  184. /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
  185. /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
  186. /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
  187. /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
  188. /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
  189. /package/src/{lib → editor/core/storage}/storage.ts +0 -0
  190. /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
  191. /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
  192. /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
  193. /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
  194. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
  195. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
  196. /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
  197. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
  198. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
  199. /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
  200. /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
  201. /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
  202. /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
  203. /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
  204. /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
  205. /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
  206. /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
  207. /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
  208. /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
  209. /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
  210. /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
  211. /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
  212. /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
  213. /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
  214. /package/src/{ui → editor/ui}/index.ts +0 -0
  215. /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
  216. /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
  217. /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
  218. /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
  219. /package/src/{ui → editor/ui}/variantScales.ts +0 -0
  220. /package/src/{assets → system/assets}/newspaper.webp +0 -0
  221. /package/src/{assets → system/assets}/offering.webp +0 -0
  222. /package/src/{components → system/components}/Callout.svelte +0 -0
  223. /package/src/{components → system/components}/Image.svelte +0 -0
  224. /package/src/{components → system/components}/RadioButton.svelte +0 -0
  225. /package/src/{components → system/components}/types.ts +0 -0
  226. /package/src/{styles → system/styles}/_padding.scss +0 -0
  227. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  228. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  229. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  230. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  231. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  232. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
@@ -0,0 +1,37 @@
1
+ import { writable } from 'svelte/store';
2
+ import type { ProductionInfo } from './themes/themeService';
3
+ import type { ManifestMeta } from './themes/themeTypes';
4
+
5
+ /**
6
+ * Monotonic counter that ticks every time a production pointer flips —
7
+ * theme production or a component's production. UI surfaces that need to
8
+ * react to a sibling Adopt subscribe to this so they refresh without
9
+ * per-pair wiring.
10
+ *
11
+ * Bumpers: `ThemeFileManager.handleApplyToProduction`,
12
+ * `ComponentFileManager.handleUpdateProduction`. Anyone setting
13
+ * `_production.json` should bump.
14
+ */
15
+ export const productionRevision = writable(0);
16
+
17
+ export function bumpProductionRevision(): void {
18
+ productionRevision.update((n) => n + 1);
19
+ }
20
+
21
+ /**
22
+ * Cached production-state stores. The Theme and Manifest file managers live in
23
+ * the sidebar footer, swapping in/out of the DOM as the user toggles between
24
+ * the tokens and components views. Keeping the last-known production state in
25
+ * module-level Svelte stores means a remount renders the correct Adopt-button
26
+ * state on the first frame instead of flashing through "not in sync" while a
27
+ * fresh fetch resolves.
28
+ */
29
+ export const themeProductionInfo = writable<ProductionInfo | null>(null);
30
+
31
+ /**
32
+ * Last-known active manifest meta. Bumped by ManifestFileManager whenever the
33
+ * active manifest changes (load, save, save-as) and whenever a theme or
34
+ * component Adopt completes (the server patches the active manifest as a
35
+ * side-effect, so consumers re-read it on `productionRevision` ticks).
36
+ */
37
+ export const activeManifest = writable<ManifestMeta | null>(null);
@@ -1,5 +1,5 @@
1
1
  import { writable } from 'svelte/store';
2
- import { storageKey } from './editorConfig';
2
+ import { storageKey } from '../store/editorConfig';
3
3
 
4
4
  function prevKey(): string {
5
5
  return storageKey('prev-route');
@@ -118,7 +118,14 @@ export function versionedFileResource<TItem, TMeta, TProductionInfo>(
118
118
  body: JSON.stringify({ name: fileName }),
119
119
  });
120
120
  if (!res.ok) {
121
- throw new Error(await readJsonError(res, 'Set production failed'));
121
+ const body = await res.json().catch(() => ({}));
122
+ const err = new Error(body.error || 'Set production failed') as Error & {
123
+ status?: number;
124
+ code?: string;
125
+ };
126
+ err.status = res.status;
127
+ if (body.code) err.code = body.code;
128
+ throw err;
122
129
  }
123
130
  return res.json();
124
131
  }
@@ -260,21 +260,37 @@ export function mutate(label: string, fn: (draft: EditorState) => void): void {
260
260
  if (import.meta.env.DEV && !label) {
261
261
  console.warn('[editorStore] mutate() called without a label');
262
262
  }
263
+ // Svelte 5's `$derived` uses strict `===` for equality (see
264
+ // node_modules/svelte/src/internal/client/reactivity/deriveds.js → equality.js).
265
+ // If we mutated `s` in place and emitted the same top-level reference, any
266
+ // intermediate `$derived($editorState.someSlice.someObj)` in a component
267
+ // would return the same object reference and short-circuit the whole
268
+ // downstream chain — swatches and previews would freeze even while the
269
+ // renderer's direct `editorState.subscribe(...)` (which uses `safe_not_equal`,
270
+ // always-different for objects) continued to write CSS vars to :root.
271
+ //
272
+ // Cloning the draft before mutation guarantees every nested reference is
273
+ // fresh, so `$derived` chains everywhere propagate correctly. The previous
274
+ // live state (`current`) becomes the immutable history snapshot — no second
275
+ // structuredClone needed.
263
276
  if (transactionScope) {
264
- // Inside a non-clipping scope: don't push individually; just mark the
265
- // scope dirty and apply. The scope's commit pushes one collapsed entry.
266
277
  transactionScope.changed = true;
267
278
  if (clippingScope) clippingScope.changed = true;
268
- store.update((s) => { fn(s); return s; });
279
+ store.update((s) => {
280
+ const next = structuredClone(s);
281
+ fn(next);
282
+ return next;
283
+ });
269
284
  return;
270
285
  }
271
- // No transaction scope: each mutate is its own history entry. Inside a
272
- // clipping scope this still pushes per-mutate entries (so undo within the
273
- // scope walks them back), and the scope's commit will collapse them.
274
286
  if (clippingScope) clippingScope.changed = true;
275
287
  const current = get(store);
276
- pushPast(structuredClone(current));
277
- store.update((s) => { fn(s); return s; });
288
+ pushPast(current);
289
+ store.update((s) => {
290
+ const next = structuredClone(s);
291
+ fn(next);
292
+ return next;
293
+ });
278
294
  bumpTick();
279
295
  persistHook();
280
296
  }
@@ -17,9 +17,9 @@ import { get } from 'svelte/store';
17
17
  import type { EditorState } from './editorTypes';
18
18
  import { storageKey } from './editorConfig';
19
19
  import { store } from './editorCore';
20
- import { quietGet, quietSet } from './storage';
21
- import { makeDefaultGradients } from './slices/gradients';
22
- import { seedShadowsFromDom } from './slices/shadows';
20
+ import { quietGet, quietSet } from '../storage/storage';
21
+ import { makeDefaultGradients } from '../themes/slices/gradients';
22
+ import { seedShadowsFromDom } from '../themes/slices/shadows';
23
23
 
24
24
  // Resolve the persist key lazily (per-call) so library consumers that invoke
25
25
  // `configureEditor({storagePrefix})` before the first store write get the
@@ -12,8 +12,8 @@
12
12
  */
13
13
 
14
14
  import type { EditorState } from './editorTypes';
15
- import { setCssVar, removeCssVar } from './cssVarSync';
16
- import { palettesToVars } from './paletteDerivation';
15
+ import { setCssVar, removeCssVar } from '../cssVarSync';
16
+ import { palettesToVars } from '../palettes/paletteDerivation';
17
17
  import {
18
18
  editorState,
19
19
  columnsToVars,
@@ -18,15 +18,15 @@
18
18
  * orchestrate across slices and stay here.
19
19
  */
20
20
 
21
- import type { CssVarRef, EditorState } from './editorTypes';
22
- import type { Theme } from './themeTypes';
23
- import { KNOWN_COMPONENT_CONFIG_KEYS } from './componentConfigKeys';
21
+ import type { CssVarRef, EditorState, GradientAliasValue } from './editorTypes';
22
+ import type { AliasDiskValue, Theme } from '../themes/themeTypes';
23
+ import { KNOWN_COMPONENT_CONFIG_KEYS } from '../components/componentConfigKeys';
24
24
  import {
25
25
  CURRENT_THEME_SCHEMA_VERSION,
26
26
  CURRENT_COMPONENT_SCHEMA_VERSION,
27
27
  runMigrations,
28
- } from './migrations';
29
- import { renamePrimaryPaletteKey } from './migrations/2026-05-13-primary-to-brand';
28
+ } from '../themes/migrations';
29
+ import { renamePrimaryPaletteKey } from '../themes/migrations/2026-05-13-primary-to-brand';
30
30
  import { __resetRendererCacheForTests, installRenderer } from './editorRenderer';
31
31
  import {
32
32
  store,
@@ -47,25 +47,25 @@ import {
47
47
  columnsEqualsDefault,
48
48
  columnsToVars,
49
49
  loadColumnsFromVars,
50
- } from './slices/columns';
50
+ } from '../themes/slices/columns';
51
51
  import {
52
52
  loadOverlaysFromVars,
53
53
  makeDefaultOverlaysState,
54
54
  overlaysEqualsDefault,
55
55
  overlaysToVars,
56
- } from './slices/overlays';
56
+ } from '../themes/slices/overlays';
57
57
  import {
58
58
  loadShadowsFromVars,
59
59
  shadowsToVars,
60
- } from './slices/shadows';
61
- import { makeDefaultGradients } from './slices/gradients';
60
+ } from '../themes/slices/shadows';
61
+ import { makeDefaultGradients } from '../themes/slices/gradients';
62
62
  import {
63
63
  componentBaseline,
64
64
  loadComponentsFromVars,
65
65
  notifyComponentSavedChanged,
66
66
  setSavedComponentBaseline,
67
67
  __resetComponentsForTests,
68
- } from './slices/components';
68
+ } from '../themes/slices/components';
69
69
 
70
70
  function emptyState(): EditorState {
71
71
  return {
@@ -121,7 +121,7 @@ store.set(emptyState());
121
121
 
122
122
  export {
123
123
  DOMAIN_VAR_NAMES,
124
- } from './slices/domainVars';
124
+ } from '../themes/slices/domainVars';
125
125
 
126
126
  export {
127
127
  columnsToVars,
@@ -129,7 +129,7 @@ export {
129
129
  COLUMN_VAR_NAMES,
130
130
  DEFAULT_COLUMNS,
131
131
  parseColumnVars,
132
- } from './slices/columns';
132
+ } from '../themes/slices/columns';
133
133
 
134
134
  export {
135
135
  overlaysToVars,
@@ -141,7 +141,7 @@ export {
141
141
  parseRgba,
142
142
  RGBA_RE,
143
143
  HEX_RE,
144
- } from './slices/overlays';
144
+ } from '../themes/slices/overlays';
145
145
 
146
146
  export {
147
147
  shadowsToVars,
@@ -153,7 +153,7 @@ export {
153
153
  defaultShadowOverride,
154
154
  parseShadowCss,
155
155
  seedShadowsFromDom,
156
- } from './slices/shadows';
156
+ } from '../themes/slices/shadows';
157
157
 
158
158
  export {
159
159
  gradientsToVars,
@@ -166,7 +166,7 @@ export {
166
166
  removeGradientStop,
167
167
  addGradientToken,
168
168
  removeGradientToken,
169
- } from './slices/gradients';
169
+ } from '../themes/slices/gradients';
170
170
 
171
171
  export {
172
172
  componentsToVars,
@@ -184,18 +184,18 @@ export {
184
184
  unlinkComponentProperty,
185
185
  relinkComponentProperty,
186
186
  markComponentSaved,
187
- } from './slices/components';
187
+ } from '../themes/slices/components';
188
188
 
189
189
  export {
190
190
  setFontSources,
191
191
  setFontStacks,
192
192
  seedFontsFromTheme,
193
- } from './slices/fonts';
193
+ } from '../themes/slices/fonts';
194
194
 
195
195
  export {
196
196
  setPaletteConfig,
197
197
  seedPalettesFromTheme,
198
- } from './slices/palettes';
198
+ } from '../themes/slices/palettes';
199
199
 
200
200
  // ── Component-config load orchestration ───────────────────────────────────
201
201
  //
@@ -205,12 +205,200 @@ export {
205
205
  // literal-valued knobs (per `KNOWN_COMPONENT_CONFIG_KEYS`) into the config
206
206
  // bucket and wraps the remainder as `CssVarRef` discriminated unions.
207
207
 
208
+ /**
209
+ * Migrate a disk-shape alias bag through the registered migration runner.
210
+ *
211
+ * The runner is string-typed by contract (most migrations only do
212
+ * key/value string rewrites). Object-valued entries — currently just
213
+ * inline `{ kind: 'gradient', value: ... }` payloads — pass through
214
+ * untouched: we split the bag into string/object subsets, run the runner
215
+ * on the string subset, then merge.
216
+ *
217
+ * For sectiondivider's legacy 7-flat-token gradients we run a synthesis
218
+ * step BEFORE the migration runner that would strip those tokens. The
219
+ * synthesized gradient object lands in the object subset and survives
220
+ * the stripping pass.
221
+ */
208
222
  function migrateComponentAliases(
209
223
  component: string,
210
- aliases: Record<string, string>,
224
+ aliasesIn: Record<string, AliasDiskValue>,
225
+ rawConfig: Record<string, unknown> | undefined,
211
226
  fileVersion: number,
212
- ): Record<string, string> {
213
- return runMigrations('component-config', fileVersion, aliases, { component });
227
+ ): { aliases: Record<string, AliasDiskValue>; config: Record<string, unknown> } {
228
+ const stringPart: Record<string, string> = {};
229
+ const objectPart: Record<string, AliasDiskValue> = {};
230
+ for (const [k, v] of Object.entries(aliasesIn)) {
231
+ if (typeof v === 'string') stringPart[k] = v;
232
+ else objectPart[k] = v;
233
+ }
234
+ // String-valued config entries join the migration input so a migration can
235
+ // rename or relocate them. After migration the bag flows through
236
+ // `splitAliasesAndConfig` which routes KNOWN keys back to config and the
237
+ // rest to aliases — this is what lets a migration *move* a key from the
238
+ // config bucket to the aliases bucket by also removing it from KNOWN.
239
+ // Config wins over an aliases-bucket collision on the same key: the legacy
240
+ // single-bucket → split-bucket move preserved "explicit config field beats
241
+ // legacy alias-bucketed value", and that contract still holds.
242
+ // Non-string config values (rare; reserved for future structured payloads)
243
+ // pass through untouched.
244
+ const configOut: Record<string, unknown> = {};
245
+ for (const [k, v] of Object.entries(rawConfig ?? {})) {
246
+ if (typeof v === 'string') stringPart[k] = v;
247
+ else configOut[k] = v;
248
+ }
249
+ if (component === 'sectiondivider') {
250
+ synthesizeSectionDividerGradients(stringPart, objectPart);
251
+ }
252
+ const migratedString = runMigrations('component-config', fileVersion, stringPart, { component });
253
+ const migratedObject = component === 'sectiondivider'
254
+ ? renameSectionDividerObjectSlots(objectPart)
255
+ : objectPart;
256
+ return {
257
+ aliases: { ...migratedString, ...migratedObject },
258
+ config: configOut,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * v8→v9 companion to `componentMigration_2026_05_20_sectiondividerSlimVariants`:
264
+ * the structured background payload is reshaped from six color families →
265
+ * three size variants (lg/md/sm). Canvas's gradient is taken as the canonical
266
+ * seed and fanned across all three new variants; other families' gradients
267
+ * are dropped. Idempotent — keys already at the final shape pass through.
268
+ *
269
+ * Sources accepted (in priority order, first-found wins for canvas's slot):
270
+ * --sectiondivider-canvas-background (current/in-progress shape)
271
+ * --sectiondivider-canvas-gradient (v7-era shape)
272
+ * --sectiondivider-color-canvas-background (intermediate shape that landed
273
+ * briefly during the color-prop pivot)
274
+ *
275
+ * The migration runner only sees string keys, so this object-shape rename
276
+ * has to live alongside the runner call.
277
+ */
278
+ const FAMILIES = ['canvas', 'neutral', 'alternate', 'primary', 'accent', 'special'] as const;
279
+ const SIZE_VARIANTS = ['lg', 'md', 'sm'] as const;
280
+ function renameSectionDividerObjectSlots(
281
+ objectPart: Record<string, AliasDiskValue>,
282
+ ): Record<string, AliasDiskValue> {
283
+ // Find canvas's gradient under any historical key shape.
284
+ const canvasSourceKeys = [
285
+ '--sectiondivider-canvas-background',
286
+ '--sectiondivider-canvas-gradient',
287
+ '--sectiondivider-color-canvas-background',
288
+ ];
289
+ let canvasGradient: AliasDiskValue | undefined;
290
+ for (const key of canvasSourceKeys) {
291
+ if (objectPart[key] !== undefined) {
292
+ canvasGradient = objectPart[key];
293
+ break;
294
+ }
295
+ }
296
+
297
+ // If we found a canvas gradient (any era), fan it into the three new
298
+ // per-variant background slots and drop every other family's gradient.
299
+ // If no canvas gradient is found, pass through non-family keys verbatim
300
+ // so non-migration data (theme tokens, other components) isn't touched.
301
+ const out: Record<string, AliasDiskValue> = {};
302
+ for (const [key, value] of Object.entries(objectPart)) {
303
+ // Skip any sectiondivider per-family gradient; the canvas fan-out emits
304
+ // the canonical set below.
305
+ let isFamilyGradient = false;
306
+ for (const f of FAMILIES) {
307
+ if (
308
+ key === `--sectiondivider-${f}-background`
309
+ || key === `--sectiondivider-${f}-gradient`
310
+ || key === `--sectiondivider-color-${f}-background`
311
+ ) {
312
+ isFamilyGradient = true;
313
+ break;
314
+ }
315
+ }
316
+ // Pass through anything that's already at the final lg/md/sm shape so
317
+ // re-running the migration is idempotent.
318
+ if (isFamilyGradient) continue;
319
+ out[key] = value;
320
+ }
321
+ if (canvasGradient !== undefined) {
322
+ for (const v of SIZE_VARIANTS) {
323
+ out[`--sectiondivider-${v}-background`] = canvasGradient;
324
+ }
325
+ }
326
+ return out;
327
+ }
328
+
329
+ /** SectionDivider variants that ship gradient aliases. */
330
+ const SECTIONDIVIDER_VARIANTS = ['canvas', 'neutral', 'alternate', 'primary', 'accent', 'special'] as const;
331
+
332
+ /**
333
+ * Collapse legacy 7-flat-token gradients (`{angle, stop-{1,2,3}-{color,position}}`)
334
+ * for each sectiondivider variant into one structured gradient alias at
335
+ * `--sectiondivider-{v}-gradient`. The flat tokens stay in `stringPart`
336
+ * for the migration runner to strip; the structured payload joins
337
+ * `objectPart` so the merge produces the new shape.
338
+ *
339
+ * Skips variants that already declare a gradient alias (object form) so
340
+ * round-tripping doesn't overwrite user edits.
341
+ */
342
+ function synthesizeSectionDividerGradients(
343
+ stringPart: Record<string, string>,
344
+ objectPart: Record<string, AliasDiskValue>,
345
+ ): void {
346
+ const GRADIENT_ANGLE_TOKENS: Record<string, number> = {
347
+ '--gradient-angle-horizontal': 90,
348
+ '--gradient-angle-vertical': 180,
349
+ '--gradient-angle-diagonal': 135,
350
+ '--gradient-angle-counter-diagonal': 45,
351
+ };
352
+ const GRADIENT_STOP_POSITIONS: Record<string, number> = {
353
+ '--gradient-stop-start': 0,
354
+ '--gradient-stop-mid': 50,
355
+ '--gradient-stop-end': 100,
356
+ };
357
+ const parseAngle = (raw: string | undefined): number => {
358
+ if (!raw) return 135;
359
+ if (raw.startsWith('--')) return GRADIENT_ANGLE_TOKENS[raw] ?? 135;
360
+ const m = raw.match(/^(-?\d+(?:\.\d+)?)\s*deg$/i);
361
+ return m ? parseFloat(m[1]) : 135;
362
+ };
363
+ const parsePosition = (raw: string | undefined, fallback: number): number => {
364
+ if (!raw) return fallback;
365
+ if (raw.startsWith('--')) return GRADIENT_STOP_POSITIONS[raw] ?? fallback;
366
+ const m = raw.match(/^(-?\d+(?:\.\d+)?)\s*%$/);
367
+ return m ? parseFloat(m[1]) : fallback;
368
+ };
369
+ const parseStopColor = (raw: string | undefined): { color: string; opacity?: number } | null => {
370
+ if (!raw) return null;
371
+ if (raw.startsWith('--')) return { color: raw };
372
+ const mix = raw.match(/^color-mix\(\s*in\s+srgb\s*,\s*var\((--[a-z0-9-]+)\)\s+([\d.]+)%\s*,\s*transparent\s*\)$/i);
373
+ if (mix) return { color: mix[1], opacity: parseFloat(mix[2]) };
374
+ return null;
375
+ };
376
+ for (const v of SECTIONDIVIDER_VARIANTS) {
377
+ const slot = `--sectiondivider-${v}-gradient`;
378
+ if (slot in objectPart) continue;
379
+ const angleKey = `${slot}-angle`;
380
+ const has1 = stringPart[`${slot}-stop-1-color`] !== undefined;
381
+ const has2 = stringPart[`${slot}-stop-2-color`] !== undefined;
382
+ const has3 = stringPart[`${slot}-stop-3-color`] !== undefined;
383
+ if (!has1 && !has2 && !has3) continue;
384
+ const stops: { position: number; color: string; opacity?: number }[] = [];
385
+ const positionDefaults = [0, 50, 100];
386
+ for (let i = 1; i <= 3; i++) {
387
+ const c = parseStopColor(stringPart[`${slot}-stop-${i}-color`]);
388
+ if (!c) continue;
389
+ const pos = parsePosition(stringPart[`${slot}-stop-${i}-position`], positionDefaults[i - 1]);
390
+ stops.push({ position: pos, color: c.color, ...(c.opacity !== undefined ? { opacity: c.opacity } : {}) });
391
+ }
392
+ if (stops.length < 2) continue;
393
+ objectPart[slot] = {
394
+ kind: 'gradient',
395
+ value: {
396
+ type: 'linear',
397
+ angle: parseAngle(stringPart[angleKey]),
398
+ stops,
399
+ },
400
+ };
401
+ }
214
402
  }
215
403
 
216
404
  /**
@@ -219,12 +407,18 @@ function migrateComponentAliases(
219
407
  * config bucket, and wraps the remainder as `CssVarRef` discriminated unions.
220
408
  */
221
409
  function splitAliasesAndConfig(
222
- rawAliases: Record<string, string>,
410
+ rawAliases: Record<string, AliasDiskValue>,
223
411
  rawConfig: Record<string, unknown> | undefined,
224
412
  ): { aliases: Record<string, CssVarRef>; config: Record<string, unknown> } {
225
413
  const aliases: Record<string, CssVarRef> = {};
226
414
  const config: Record<string, unknown> = { ...(rawConfig ?? {}) };
227
415
  for (const [key, value] of Object.entries(rawAliases)) {
416
+ if (typeof value !== 'string') {
417
+ if (value.kind === 'gradient') {
418
+ aliases[key] = { kind: 'gradient', value: value.value as GradientAliasValue };
419
+ }
420
+ continue;
421
+ }
228
422
  if (KNOWN_COMPONENT_CONFIG_KEYS.has(key)) {
229
423
  if (config[key] === undefined) config[key] = value;
230
424
  continue;
@@ -249,12 +443,12 @@ function splitAliasesAndConfig(
249
443
  export function loadComponentActive(
250
444
  component: string,
251
445
  activeFile: string,
252
- aliases: Record<string, string>,
446
+ aliases: Record<string, AliasDiskValue>,
253
447
  config?: Record<string, unknown>,
254
448
  schemaVersion: number = 0,
255
449
  ): void {
256
- const migrated = migrateComponentAliases(component, aliases, schemaVersion);
257
- const split = splitAliasesAndConfig(migrated, config);
450
+ const migrated = migrateComponentAliases(component, aliases, config, schemaVersion);
451
+ const split = splitAliasesAndConfig(migrated.aliases, migrated.config);
258
452
  mutate(`load ${component}/${activeFile}`, (s) => {
259
453
  s.components[component] = { activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
260
454
  });
@@ -264,7 +458,7 @@ export function loadComponentActive(
264
458
 
265
459
  export interface ComponentSeed {
266
460
  activeFile: string;
267
- aliases: Record<string, string>;
461
+ aliases: Record<string, AliasDiskValue>;
268
462
  config?: Record<string, unknown>;
269
463
  schemaVersion?: number;
270
464
  }
@@ -282,8 +476,8 @@ export function seedComponentsFromApi(
282
476
  store.update((s) => {
283
477
  s.components = {};
284
478
  for (const [comp, cfg] of Object.entries(configs)) {
285
- const migrated = migrateComponentAliases(comp, cfg.aliases, cfg.schemaVersion ?? 0);
286
- const split = splitAliasesAndConfig(migrated, cfg.config);
479
+ const migrated = migrateComponentAliases(comp, cfg.aliases, cfg.config, cfg.schemaVersion ?? 0);
480
+ const split = splitAliasesAndConfig(migrated.aliases, migrated.config);
287
481
  s.components[comp] = { activeFile: cfg.activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
288
482
  setSavedComponentBaseline(comp, componentBaseline(split));
289
483
  }
@@ -1,4 +1,4 @@
1
- import type { PaletteConfig, FontSource, FontStack } from './themeTypes';
1
+ import type { PaletteConfig, FontSource, FontStack } from '../themes/themeTypes';
2
2
 
3
3
  export interface ShadowGlobals {
4
4
  angle: number;
@@ -43,24 +43,25 @@ export interface ColumnsState {
43
43
  margin: number;
44
44
  }
45
45
 
46
- export type CssVarRef =
47
- | { kind: 'token'; name: string }
48
- | { kind: 'literal'; value: string };
49
-
50
- export interface ComponentSlice {
51
- activeFile: string;
52
- aliases: Record<string, CssVarRef>;
53
- config: Record<string, unknown>;
54
- unlinked?: string[];
55
- }
56
-
57
- export type GradientType = 'linear' | 'radial';
46
+ /** Gradient render mode.
47
+ * - `linear` / `radial`: real gradients with N stops + angle (linear) or radius (radial).
48
+ * - `solid`: collapses to the first stop's color. Angle/radius/extra stops carried in
49
+ * the payload but ignored by the renderer; toggling back restores the prior shape.
50
+ * - `none`: transparent. Same carry-forward semantics — payload retained for
51
+ * round-trip when the user toggles back to a real gradient. */
52
+ export type GradientType = 'linear' | 'radial' | 'solid' | 'none';
58
53
 
59
54
  export interface GradientTokenStop {
60
55
  /** 0–100 percentage along the gradient axis. */
61
56
  position: number;
62
57
  /** CSS variable name the stop resolves through (e.g. '--color-brand-500'). */
63
58
  color: string;
59
+ /** When `false`, this stop is an explicit off-palette override: the stop
60
+ * picker opens to any color (not family-filtered), and the stop is skipped
61
+ * by family-swap rewrites so its color survives a variant family change.
62
+ * Defaults to true (follow family) on read; absence is the same as `true`.
63
+ * Only meaningful for gradients rendered inside a family-aware context. */
64
+ monochrome?: boolean;
64
65
  /** 0–100 alpha applied to the stop's color. Defaults to 100 (fully opaque). */
65
66
  opacity?: number;
66
67
  }
@@ -71,9 +72,51 @@ export interface GradientToken {
71
72
  type: GradientType;
72
73
  /** Degrees, applies to linear only. */
73
74
  angle: number;
75
+ /** Pixel radius for radial gradients. When absent or zero, the renderer
76
+ * emits CSS's default ellipse/farthest-corner shape. */
77
+ radius?: number;
78
+ /** Horizontal center position for radial gradients, 0–100. Defaults to 50. */
79
+ centerX?: number;
80
+ /** Horizontal stretch factor for the radial ellipse (1–8). Defaults to 1.
81
+ * With aspectY, the rendered semi-axes are `radius * aspect*`, so both
82
+ * shape (ratio) and size are encoded together. Both = 1 keeps the legacy
83
+ * `circle` render path verbatim. */
84
+ aspectX?: number;
85
+ /** Vertical stretch factor for the radial ellipse (1–8). Defaults to 1. */
86
+ aspectY?: number;
74
87
  stops: GradientTokenStop[];
75
88
  }
76
89
 
90
+ /** Structured gradient payload carried inline on a component alias.
91
+ * Mirrors GradientToken minus `variable` (the alias key itself is the
92
+ * binding). Used when a component owns a per-instance gradient that
93
+ * doesn't share the theme-level `--gradient-N` library. */
94
+ export interface GradientAliasValue {
95
+ type: GradientType;
96
+ angle: number;
97
+ /** See GradientToken.radius. */
98
+ radius?: number;
99
+ /** See GradientToken.centerX. */
100
+ centerX?: number;
101
+ /** See GradientToken.aspectX. */
102
+ aspectX?: number;
103
+ /** See GradientToken.aspectY. */
104
+ aspectY?: number;
105
+ stops: GradientTokenStop[];
106
+ }
107
+
108
+ export type CssVarRef =
109
+ | { kind: 'token'; name: string }
110
+ | { kind: 'literal'; value: string }
111
+ | { kind: 'gradient'; value: GradientAliasValue };
112
+
113
+ export interface ComponentSlice {
114
+ activeFile: string;
115
+ aliases: Record<string, CssVarRef>;
116
+ config: Record<string, unknown>;
117
+ unlinked?: string[];
118
+ }
119
+
77
120
  /**
78
121
  * Single source of truth for everything a saved token file depends on, plus
79
122
  * the domain state currently scattered across VariablesTab local `let` fields.