@motion-proto/live-tokens 0.1.1 → 0.3.2

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 (224) hide show
  1. package/README.md +168 -21
  2. package/dist-plugin/index.cjs +823 -336
  3. package/dist-plugin/index.d.cts +9 -7
  4. package/dist-plugin/index.d.ts +9 -7
  5. package/dist-plugin/index.js +822 -335
  6. package/package.json +46 -20
  7. package/src/assets/newspaper.webp +0 -0
  8. package/src/assets/offering.webp +0 -0
  9. package/src/component-editor/BadgeEditor.svelte +170 -0
  10. package/src/component-editor/CalloutEditor.svelte +103 -0
  11. package/src/component-editor/CardEditor.svelte +184 -0
  12. package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
  13. package/src/component-editor/CornerBadgeEditor.svelte +207 -0
  14. package/src/component-editor/DialogEditor.svelte +172 -0
  15. package/src/component-editor/ImageEditor.svelte +72 -0
  16. package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
  17. package/src/component-editor/NotificationEditor.svelte +160 -0
  18. package/src/component-editor/ProgressBarEditor.svelte +124 -0
  19. package/src/component-editor/RadioButtonEditor.svelte +140 -0
  20. package/src/component-editor/SectionDividerEditor.svelte +263 -0
  21. package/src/component-editor/SegmentedControlEditor.svelte +154 -0
  22. package/src/component-editor/StandardButtonsEditor.svelte +178 -0
  23. package/src/component-editor/TabBarEditor.svelte +137 -0
  24. package/src/component-editor/TableEditor.svelte +128 -0
  25. package/src/component-editor/TooltipEditor.svelte +122 -0
  26. package/src/component-editor/editorTokens.test.ts +93 -0
  27. package/src/component-editor/groupKeySlots.test.ts +67 -0
  28. package/src/component-editor/groupKeySnapshot.test.ts +52 -0
  29. package/src/component-editor/index.ts +5 -0
  30. package/src/component-editor/registry.ts +246 -0
  31. package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
  32. package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
  33. package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
  34. package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
  35. package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
  36. package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
  37. package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
  38. package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
  39. package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
  40. package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
  41. package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
  42. package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
  43. package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
  44. package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
  45. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
  46. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
  47. package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
  48. package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
  49. package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
  50. package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
  51. package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
  52. package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
  53. package/src/component-editor/scaffolding/componentSources.ts +9 -0
  54. package/src/component-editor/scaffolding/defaultSections.ts +16 -0
  55. package/src/component-editor/scaffolding/editorContext.ts +44 -0
  56. package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
  57. package/src/component-editor/scaffolding/siblings.ts +33 -0
  58. package/src/component-editor/scaffolding/types.ts +39 -0
  59. package/src/components/Badge.svelte +231 -42
  60. package/src/components/Button.svelte +324 -124
  61. package/src/components/Callout.svelte +145 -0
  62. package/src/components/Card.svelte +123 -25
  63. package/src/components/CollapsibleSection.svelte +213 -35
  64. package/src/components/CornerBadge.svelte +224 -0
  65. package/src/components/Dialog.svelte +137 -114
  66. package/src/components/Image.svelte +43 -0
  67. package/src/components/InlineEditActions.svelte +74 -14
  68. package/src/components/Notification.svelte +184 -163
  69. package/src/components/ProgressBar.svelte +216 -22
  70. package/src/components/RadioButton.svelte +110 -40
  71. package/src/components/SectionDivider.svelte +428 -74
  72. package/src/components/SegmentedControl.svelte +203 -0
  73. package/src/components/TabBar.svelte +146 -21
  74. package/src/components/Table.svelte +102 -0
  75. package/src/components/Tooltip.svelte +45 -19
  76. package/src/components/types.ts +51 -0
  77. package/src/data/google-fonts.json +75 -0
  78. package/src/lib/ColumnsOverlay.svelte +20 -7
  79. package/src/lib/LiveEditorOverlay.svelte +257 -78
  80. package/src/lib/columnsOverlay.ts +21 -17
  81. package/src/lib/componentConfig.test.ts +204 -0
  82. package/src/lib/componentConfigKeys.ts +19 -0
  83. package/src/lib/componentConfigService.ts +88 -0
  84. package/src/lib/copyPopover.ts +30 -0
  85. package/src/lib/cssVarSync.ts +59 -7
  86. package/src/lib/editorConfigStore.ts +0 -10
  87. package/src/lib/editorCore.ts +402 -0
  88. package/src/lib/editorKeybindings.ts +52 -0
  89. package/src/lib/editorPersistence.ts +106 -0
  90. package/src/lib/editorRenderer.ts +74 -0
  91. package/src/lib/editorStore.test.ts +328 -0
  92. package/src/lib/editorStore.ts +412 -0
  93. package/src/lib/editorTypes.ts +100 -0
  94. package/src/lib/editorViewStore.ts +55 -0
  95. package/src/lib/files/versionedFileResource.ts +140 -0
  96. package/src/lib/fontLoader.ts +130 -0
  97. package/src/lib/fontMigration.ts +140 -0
  98. package/src/lib/fontParse.ts +168 -0
  99. package/src/lib/index.ts +48 -30
  100. package/src/lib/lazyConfig.test.ts +54 -0
  101. package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
  102. package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
  103. package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
  104. package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
  105. package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
  106. package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
  107. package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
  108. package/src/lib/migrations/index.ts +93 -0
  109. package/src/lib/migrations/migrations.test.ts +341 -0
  110. package/src/lib/navLinkTypes.ts +1 -0
  111. package/src/lib/overlayState.ts +3 -0
  112. package/src/lib/paletteDerivation.ts +300 -0
  113. package/src/lib/parentRouteStore.ts +42 -0
  114. package/src/lib/parsers/globalRootBlock.ts +32 -0
  115. package/src/lib/presetService.ts +94 -0
  116. package/src/lib/router.ts +42 -10
  117. package/src/lib/scrollSection.ts +45 -0
  118. package/src/lib/slices/columns.ts +59 -0
  119. package/src/lib/slices/components.ts +362 -0
  120. package/src/lib/slices/domainVars.ts +15 -0
  121. package/src/lib/slices/fonts.ts +30 -0
  122. package/src/lib/slices/gradients.ts +153 -0
  123. package/src/lib/slices/overlays.ts +132 -0
  124. package/src/lib/slices/palettes.ts +26 -0
  125. package/src/lib/slices/shadows.ts +123 -0
  126. package/src/lib/storage.ts +88 -0
  127. package/src/lib/themeInit.ts +74 -0
  128. package/src/lib/themeService.ts +101 -0
  129. package/src/lib/themeTypes.ts +146 -0
  130. package/src/lib/tokenRegistry.ts +148 -0
  131. package/src/pages/ComponentEditorPage.svelte +384 -0
  132. package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
  133. package/src/pages/Editor.svelte +98 -0
  134. package/src/pages/Editor.svelte.d.ts +2 -0
  135. package/src/pages/EditorShell.svelte +348 -0
  136. package/src/styles/_padding.scss +34 -0
  137. package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  138. package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  139. package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  140. package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  141. package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  142. package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
  143. package/src/styles/fonts.css +22 -10
  144. package/src/styles/form-controls.css +14 -16
  145. package/src/styles/tokens.css +1322 -0
  146. package/src/styles/ui-editor.css +126 -0
  147. package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
  148. package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
  149. package/src/ui/EditorViewSwitcher.svelte +180 -0
  150. package/src/ui/FontStackEditor.svelte +360 -0
  151. package/src/ui/GradientEditor.svelte +461 -0
  152. package/src/ui/GradientStopPicker.svelte +74 -0
  153. package/src/ui/PaletteEditor.svelte +1590 -0
  154. package/src/ui/PaletteEditor.test.ts +108 -0
  155. package/src/ui/PresetFileManager.svelte +567 -0
  156. package/src/ui/ProjectFontsSection.svelte +645 -0
  157. package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
  158. package/src/{showcase → ui}/TextTab.svelte +27 -27
  159. package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
  160. package/src/ui/Toggle.svelte +108 -0
  161. package/src/ui/UICopyPopover.svelte +78 -0
  162. package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
  163. package/src/ui/UIFontFamilySelector.svelte +309 -0
  164. package/src/ui/UIFontSizeSelector.svelte +165 -0
  165. package/src/ui/UIFontWeightSelector.svelte +52 -0
  166. package/src/ui/UILineHeightSelector.svelte +47 -0
  167. package/src/ui/UILinkToggle.svelte +60 -0
  168. package/src/ui/UIOptionItem.svelte +74 -0
  169. package/src/ui/UIOptionList.svelte +27 -0
  170. package/src/ui/UIPaddingSelector.svelte +661 -0
  171. package/src/ui/UIPaletteSelector.svelte +1084 -0
  172. package/src/ui/UIRadio.svelte +72 -0
  173. package/src/ui/UIRadioGroup.svelte +59 -0
  174. package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
  175. package/src/ui/UITokenSelector.svelte +509 -0
  176. package/src/ui/UIVariantSelector.svelte +145 -0
  177. package/src/ui/VariablesTab.svelte +252 -0
  178. package/src/ui/index.ts +31 -0
  179. package/src/ui/keepInViewport.ts +84 -0
  180. package/src/ui/palette/GradientStopEditor.svelte +482 -0
  181. package/src/ui/palette/OverridesPanel.svelte +526 -0
  182. package/src/ui/palette/PaletteBase.svelte +165 -0
  183. package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
  184. package/src/ui/palette/paletteEditorState.ts +89 -0
  185. package/src/ui/sections/ColumnsSection.svelte +273 -0
  186. package/src/ui/sections/GradientsSection.svelte +147 -0
  187. package/src/ui/sections/OverlaysSection.svelte +670 -0
  188. package/src/ui/sections/ShadowsSection.svelte +1250 -0
  189. package/src/ui/sections/TokenScaleTable.svelte +332 -0
  190. package/src/ui/sections/tokenScales.ts +81 -0
  191. package/src/ui/variantScales.ts +108 -0
  192. package/src/components/DetailNav.svelte +0 -78
  193. package/src/components/Toggle.svelte +0 -86
  194. package/src/lib/tokenInit.ts +0 -29
  195. package/src/lib/tokenService.ts +0 -144
  196. package/src/lib/tokenTypes.ts +0 -45
  197. package/src/pages/Admin.svelte +0 -100
  198. package/src/pages/ShowcasePage.svelte +0 -144
  199. package/src/showcase/BackupBrowser.svelte +0 -617
  200. package/src/showcase/ComponentsTab.svelte +0 -105
  201. package/src/showcase/PaletteEditor.svelte +0 -2579
  202. package/src/showcase/PaletteSelector.svelte +0 -627
  203. package/src/showcase/TokenMap.svelte +0 -54
  204. package/src/showcase/VariablesTab.svelte +0 -2655
  205. package/src/showcase/VisualsTab.svelte +0 -231
  206. package/src/showcase/demos/BadgeDemo.svelte +0 -56
  207. package/src/showcase/demos/CardDemo.svelte +0 -50
  208. package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
  209. package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
  210. package/src/showcase/demos/DialogDemo.svelte +0 -42
  211. package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
  212. package/src/showcase/demos/NotificationDemo.svelte +0 -147
  213. package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
  214. package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
  215. package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
  216. package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
  217. package/src/showcase/demos/TabBarDemo.svelte +0 -58
  218. package/src/showcase/demos/TooltipDemo.svelte +0 -52
  219. package/src/showcase/editor.css +0 -93
  220. package/src/showcase/index.ts +0 -17
  221. package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
  222. package/src/styles/fonts/Domine/OFL.txt +0 -97
  223. package/src/styles/fonts/Domine/README.txt +0 -66
  224. /package/src/{showcase → ui}/curveEngine.ts +0 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Central editor state store — the single mutation funnel.
3
+ *
4
+ * All editor state (palettes, fonts, shadows, overlays, columns, ad-hoc CSS
5
+ * vars) lives in one `EditorState` tree. Every change must go through
6
+ * `mutate` (or a transaction). History is captured automatically; the
7
+ * renderer subscribes and writes derived CSS vars to :root via `cssVarSync`
8
+ * (which fans out to the parent document for the overlay iframe).
9
+ *
10
+ * This module is now a barrel: it re-exports the public API from
11
+ * - `editorCore` (history machine + transaction/session primitives)
12
+ * - `slices/*` (per-domain factories + actions)
13
+ * - `editorRenderer` (DOM subscriber)
14
+ * - `editorPersistence` (debounced localStorage)
15
+ *
16
+ * Component-config + theme migrations live in `./migrations` (Wave 4).
17
+ * `loadComponentActive` / `seedComponentsFromApi` / `loadFromFile` / `toTheme`
18
+ * orchestrate across slices and stay here.
19
+ */
20
+
21
+ import type { CssVarRef, EditorState } from './editorTypes';
22
+ import type { Theme } from './themeTypes';
23
+ import { KNOWN_COMPONENT_CONFIG_KEYS } from './componentConfigKeys';
24
+ import {
25
+ CURRENT_THEME_SCHEMA_VERSION,
26
+ CURRENT_COMPONENT_SCHEMA_VERSION,
27
+ runMigrations,
28
+ } from './migrations';
29
+ import { renamePrimaryPaletteKey } from './migrations/2026-05-13-primary-to-brand';
30
+ import { __resetRendererCacheForTests, installRenderer } from './editorRenderer';
31
+ import {
32
+ store,
33
+ mutate,
34
+ setEmptyStateFactory as setCoreEmptyStateFactory,
35
+ setPersistHook,
36
+ resetHistoryForLoad,
37
+ __resetCoreForTests,
38
+ } from './editorCore';
39
+ import {
40
+ schedulePersist,
41
+ setEmptyStateFactory as setPersistenceEmptyStateFactory,
42
+ ensureHydrated,
43
+ initializeEditorStore,
44
+ } from './editorPersistence';
45
+ import {
46
+ DEFAULT_COLUMNS,
47
+ columnsEqualsDefault,
48
+ columnsToVars,
49
+ loadColumnsFromVars,
50
+ } from './slices/columns';
51
+ import {
52
+ loadOverlaysFromVars,
53
+ makeDefaultOverlaysState,
54
+ overlaysEqualsDefault,
55
+ overlaysToVars,
56
+ } from './slices/overlays';
57
+ import {
58
+ loadShadowsFromVars,
59
+ shadowsToVars,
60
+ } from './slices/shadows';
61
+ import { makeDefaultGradients } from './slices/gradients';
62
+ import {
63
+ componentBaseline,
64
+ loadComponentsFromVars,
65
+ notifyComponentSavedChanged,
66
+ setSavedComponentBaseline,
67
+ __resetComponentsForTests,
68
+ } from './slices/components';
69
+
70
+ function emptyState(): EditorState {
71
+ return {
72
+ palettes: {},
73
+ fonts: { sources: [], stacks: [] },
74
+ shadows: {
75
+ globals: {
76
+ angle: 90, opacityMin: 0.15, opacityMax: 0.15, opacityLocked: true,
77
+ distanceMin: 1, distanceMax: 25,
78
+ blurMin: 2, blurMax: 50, blurLocked: false,
79
+ sizeMin: 0, sizeMax: 0, sizeLocked: true,
80
+ hue: 0, saturation: 0, lightness: 0,
81
+ },
82
+ tokens: [],
83
+ overrides: {},
84
+ },
85
+ overlays: makeDefaultOverlaysState(),
86
+ columns: { ...DEFAULT_COLUMNS },
87
+ components: {},
88
+ gradients: { tokens: makeDefaultGradients() },
89
+ cssVars: {},
90
+ };
91
+ }
92
+
93
+ // ── editorCore re-exports ─────────────────────────────────────────────────
94
+
95
+ export {
96
+ editorState,
97
+ mutate,
98
+ transaction,
99
+ beginScope,
100
+ commitScope,
101
+ cancelScope,
102
+ beginSliderGesture,
103
+ undo,
104
+ redo,
105
+ canUndo,
106
+ canRedo,
107
+ dirty,
108
+ markSaved,
109
+ __getHistoryLengths,
110
+ __getPastAt,
111
+ } from './editorCore';
112
+ export type { Scope } from './editorCore';
113
+
114
+ // Wire the factory in both editorCore and editorPersistence. Resetting the
115
+ // store lands a proper EditorState before any helper below runs `mutate`.
116
+ setCoreEmptyStateFactory(emptyState);
117
+ setPersistenceEmptyStateFactory(emptyState);
118
+ store.set(emptyState());
119
+
120
+ // ── Slice re-exports ──────────────────────────────────────────────────────
121
+
122
+ export {
123
+ DOMAIN_VAR_NAMES,
124
+ } from './slices/domainVars';
125
+
126
+ export {
127
+ columnsToVars,
128
+ columnsEqualsDefault,
129
+ COLUMN_VAR_NAMES,
130
+ DEFAULT_COLUMNS,
131
+ parseColumnVars,
132
+ } from './slices/columns';
133
+
134
+ export {
135
+ overlaysToVars,
136
+ overlaysEqualsDefault,
137
+ OVERLAY_VAR_NAMES,
138
+ applyOverlayVarsToState,
139
+ makeDefaultOverlaysState,
140
+ overlayTokenToRgba,
141
+ parseRgba,
142
+ RGBA_RE,
143
+ HEX_RE,
144
+ } from './slices/overlays';
145
+
146
+ export {
147
+ shadowsToVars,
148
+ applyShadowVarsToState,
149
+ SHADOW_VAR_NAMES,
150
+ SCALE_SHADOW_VARIABLES,
151
+ computeShadowXY,
152
+ shadowTokenCss,
153
+ defaultShadowOverride,
154
+ parseShadowCss,
155
+ seedShadowsFromDom,
156
+ } from './slices/shadows';
157
+
158
+ export {
159
+ gradientsToVars,
160
+ makeDefaultGradients,
161
+ setGradient,
162
+ setGradientType,
163
+ setGradientAngle,
164
+ setGradientStop,
165
+ addGradientStop,
166
+ removeGradientStop,
167
+ addGradientToken,
168
+ removeGradientToken,
169
+ } from './slices/gradients';
170
+
171
+ export {
172
+ componentsToVars,
173
+ getComponentOwnedVarNames,
174
+ componentDirty,
175
+ setComponentAlias,
176
+ clearComponentAlias,
177
+ setComponentConfig,
178
+ clearComponentConfig,
179
+ registerComponentSchema,
180
+ getComponentPropertySiblings,
181
+ isComponentPropertyLinked,
182
+ setComponentAliasLinked,
183
+ clearComponentAliasLinked,
184
+ unlinkComponentProperty,
185
+ relinkComponentProperty,
186
+ markComponentSaved,
187
+ } from './slices/components';
188
+
189
+ export {
190
+ setFontSources,
191
+ setFontStacks,
192
+ seedFontsFromTheme,
193
+ } from './slices/fonts';
194
+
195
+ export {
196
+ setPaletteConfig,
197
+ seedPalettesFromTheme,
198
+ } from './slices/palettes';
199
+
200
+ // ── Component-config load orchestration ───────────────────────────────────
201
+ //
202
+ // Component-config migrations now live in `./migrations` (Wave 4): the
203
+ // migration runner pipes `aliases` through any registered transforms whose
204
+ // `fromVersion >= file.schemaVersion`, then `splitAliasesAndConfig` routes
205
+ // literal-valued knobs (per `KNOWN_COMPONENT_CONFIG_KEYS`) into the config
206
+ // bucket and wraps the remainder as `CssVarRef` discriminated unions.
207
+
208
+ function migrateComponentAliases(
209
+ component: string,
210
+ aliases: Record<string, string>,
211
+ fileVersion: number,
212
+ ): Record<string, string> {
213
+ return runMigrations('component-config', fileVersion, aliases, { component });
214
+ }
215
+
216
+ /**
217
+ * Disk-shape → in-memory split. Routes legacy single-bucket aliases that
218
+ * carry literal-valued knobs (per `KNOWN_COMPONENT_CONFIG_KEYS`) into the
219
+ * config bucket, and wraps the remainder as `CssVarRef` discriminated unions.
220
+ */
221
+ function splitAliasesAndConfig(
222
+ rawAliases: Record<string, string>,
223
+ rawConfig: Record<string, unknown> | undefined,
224
+ ): { aliases: Record<string, CssVarRef>; config: Record<string, unknown> } {
225
+ const aliases: Record<string, CssVarRef> = {};
226
+ const config: Record<string, unknown> = { ...(rawConfig ?? {}) };
227
+ for (const [key, value] of Object.entries(rawAliases)) {
228
+ if (KNOWN_COMPONENT_CONFIG_KEYS.has(key)) {
229
+ if (config[key] === undefined) config[key] = value;
230
+ continue;
231
+ }
232
+ aliases[key] = value.startsWith('--')
233
+ ? { kind: 'token', name: value }
234
+ : { kind: 'literal', value };
235
+ }
236
+ return { aliases, config };
237
+ }
238
+
239
+ /**
240
+ * Replace a component's slice with a loaded config file's contents. Uses
241
+ * `mutate()` so the load is one undoable entry; updates the dirty baseline
242
+ * so the post-load state reads clean for this component.
243
+ *
244
+ * `schemaVersion` is the stamp on the loaded file (0 for legacy files
245
+ * with no stamp). The runner applies any migrations between that and
246
+ * `CURRENT_COMPONENT_SCHEMA_VERSION` before the slice is stored — in-memory
247
+ * state is always at the current version.
248
+ */
249
+ export function loadComponentActive(
250
+ component: string,
251
+ activeFile: string,
252
+ aliases: Record<string, string>,
253
+ config?: Record<string, unknown>,
254
+ schemaVersion: number = 0,
255
+ ): void {
256
+ const migrated = migrateComponentAliases(component, aliases, schemaVersion);
257
+ const split = splitAliasesAndConfig(migrated, config);
258
+ mutate(`load ${component}/${activeFile}`, (s) => {
259
+ s.components[component] = { activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
260
+ });
261
+ setSavedComponentBaseline(component, componentBaseline(split));
262
+ notifyComponentSavedChanged();
263
+ }
264
+
265
+ export interface ComponentSeed {
266
+ activeFile: string;
267
+ aliases: Record<string, string>;
268
+ config?: Record<string, unknown>;
269
+ schemaVersion?: number;
270
+ }
271
+
272
+ /**
273
+ * Boot-path hydration from the server's /api/component-configs fetch. No
274
+ * history entry, no dirty flag — components are clean relative to disk.
275
+ *
276
+ * Each seed may carry a `schemaVersion`; absent entries are treated as 0
277
+ * and migrated up to current.
278
+ */
279
+ export function seedComponentsFromApi(
280
+ configs: Record<string, ComponentSeed>,
281
+ ): void {
282
+ store.update((s) => {
283
+ s.components = {};
284
+ 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);
287
+ s.components[comp] = { activeFile: cfg.activeFile, aliases: { ...split.aliases }, config: { ...split.config } };
288
+ setSavedComponentBaseline(comp, componentBaseline(split));
289
+ }
290
+ return s;
291
+ });
292
+ notifyComponentSavedChanged();
293
+ schedulePersist();
294
+ }
295
+
296
+ // ── Theme load / save ──────────────────────────────────────────────────────
297
+
298
+ /**
299
+ * Per-domain loader: routes a freshly-loaded theme's `cssVariables` bag
300
+ * into typed state on `next`, then removes the routed entries so the
301
+ * remainder lands as the catch-all `cssVars` bag. Each loader owns its
302
+ * domain's parser + name list; this table is the only place editorStore
303
+ * needs to know about the per-slice loading contract.
304
+ */
305
+ type DomainLoader = (next: EditorState, rawVars: Record<string, string>) => void;
306
+
307
+ const domainLoaders: Record<string, DomainLoader> = {
308
+ columns: loadColumnsFromVars,
309
+ overlays: loadOverlaysFromVars,
310
+ shadows: loadShadowsFromVars,
311
+ components: loadComponentsFromVars,
312
+ };
313
+
314
+ /**
315
+ * Replace state with a loaded theme. Clears history and marks saved —
316
+ * "open a different document" semantics. Undo cannot cross a theme load.
317
+ *
318
+ * Reads `theme.schemaVersion` (absent = 0) and runs theme migrations up to
319
+ * `CURRENT_THEME_SCHEMA_VERSION` before splitting the bag into domains.
320
+ */
321
+ export function loadFromFile(theme: Theme): void {
322
+ const next = emptyState();
323
+ next.palettes = renamePrimaryPaletteKey(structuredClone(theme.editorConfigs ?? {}));
324
+ next.fonts.sources = structuredClone(theme.fontSources ?? []);
325
+ next.fonts.stacks = structuredClone(theme.fontStacks ?? []);
326
+ const rawVars = runMigrations(
327
+ 'theme',
328
+ theme.schemaVersion ?? 0,
329
+ theme.cssVariables ?? {},
330
+ );
331
+ // Route domain-owned entries out of the catch-all bag into typed state.
332
+ // Order doesn't matter — each loader claims a disjoint set of var names
333
+ // (or in components' case, vars that wouldn't have been in the theme to
334
+ // begin with).
335
+ for (const load of Object.values(domainLoaders)) load(next, rawVars);
336
+ next.cssVars = rawVars;
337
+ resetHistoryForLoad();
338
+ store.set(next);
339
+ schedulePersist();
340
+ }
341
+
342
+ /**
343
+ * Serialize current state for saving. Domains with their own typed state
344
+ * (columns, overlays, shadows) fold derived vars into `cssVariables` only
345
+ * when they diverge from defaults; the catch-all `cssVars` bag carries
346
+ * everything not yet migrated to a typed domain.
347
+ *
348
+ * Stamps the file with `CURRENT_THEME_SCHEMA_VERSION` so future loads can
349
+ * skip migrations the file is already past.
350
+ */
351
+ export function toTheme(state: EditorState, meta: { name: string }): Theme {
352
+ const now = new Date().toISOString();
353
+ const cssVariables: Record<string, string> = { ...state.cssVars };
354
+ if (!columnsEqualsDefault(state.columns)) {
355
+ Object.assign(cssVariables, columnsToVars(state.columns));
356
+ }
357
+ if (!overlaysEqualsDefault(state.overlays)) {
358
+ Object.assign(cssVariables, overlaysToVars(state.overlays));
359
+ }
360
+ if (state.shadows.tokens.length > 0) {
361
+ Object.assign(cssVariables, shadowsToVars(state.shadows));
362
+ }
363
+ return {
364
+ name: meta.name,
365
+ createdAt: now,
366
+ updatedAt: now,
367
+ editorConfigs: state.palettes,
368
+ cssVariables,
369
+ fontSources: state.fonts.sources,
370
+ fontStacks: state.fonts.stacks,
371
+ schemaVersion: CURRENT_THEME_SCHEMA_VERSION,
372
+ };
373
+ }
374
+
375
+ // ── Persistence ────────────────────────────────────────────────────────────
376
+ //
377
+ // `schedulePersist` + `hydrate` + the eager-hydrate gate live in
378
+ // `./editorPersistence`. The barrel re-exports `initializeEditorStore` for
379
+ // API parity.
380
+
381
+ export { initializeEditorStore };
382
+ /** Idempotent host hook — call once during boot. Alias for `initializeEditorStore`
383
+ * matching the `module.init()` convention used by `cssVarSync` / `router` /
384
+ * `columnsOverlay` so `main.ts` can call them uniformly. */
385
+ export const init = initializeEditorStore;
386
+
387
+ // ── Test-only reset ────────────────────────────────────────────────────────
388
+
389
+ /**
390
+ * Test-only: clear all history + transient session/transaction state and
391
+ * reset the store to `emptyState()`. Not exported from the public barrel.
392
+ */
393
+ export function __resetForTests(): void {
394
+ __resetCoreForTests();
395
+ __resetRendererCacheForTests();
396
+ __resetComponentsForTests();
397
+ }
398
+
399
+ // ── Wiring ─────────────────────────────────────────────────────────────────
400
+ //
401
+ // `setPersistHook(schedulePersist)` lets editorCore's mutate/undo/redo
402
+ // debounce-persist through the same path. `ensureHydrated()` runs the
403
+ // eager localStorage load (pre-Svelte-mount so children see persisted
404
+ // state in onMount). `installRenderer()` runs the `store.subscribe(...)`
405
+ // wiring legacy `import '.../editorStore'` paths depended on; it must run
406
+ // *after* this module's exports are initialized — the renderer imports
407
+ // `editorState` back from editorCore, and calling it earlier would TDZ
408
+ // because of the circular import.
409
+ setPersistHook(schedulePersist);
410
+ ensureHydrated();
411
+ export { deriveCssVars } from './editorRenderer';
412
+ installRenderer();
@@ -0,0 +1,100 @@
1
+ import type { PaletteConfig, FontSource, FontStack } from './themeTypes';
2
+
3
+ export interface ShadowGlobals {
4
+ angle: number;
5
+ opacityMin: number; opacityMax: number; opacityLocked: boolean;
6
+ distanceMin: number; distanceMax: number;
7
+ blurMin: number; blurMax: number; blurLocked: boolean;
8
+ sizeMin: number; sizeMax: number; sizeLocked: boolean;
9
+ hue: number; saturation: number; lightness: number;
10
+ }
11
+
12
+ export interface ShadowToken {
13
+ variable: string;
14
+ x: number; y: number; blur: number; spread: number;
15
+ opacity: number; hue: number; saturation: number; lightness: number;
16
+ angle: number; distance: number;
17
+ }
18
+
19
+ export interface ShadowOverrideFlags {
20
+ angle: boolean; opacity: boolean; color: boolean;
21
+ distance: boolean; blur: boolean; size: boolean;
22
+ }
23
+
24
+ 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;
37
+ }
38
+
39
+ export interface ColumnsState {
40
+ count: number;
41
+ maxWidth: number;
42
+ gutter: number;
43
+ margin: number;
44
+ }
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';
58
+
59
+ export interface GradientTokenStop {
60
+ /** 0–100 percentage along the gradient axis. */
61
+ position: number;
62
+ /** CSS variable name the stop resolves through (e.g. '--color-brand-500'). */
63
+ color: string;
64
+ /** 0–100 alpha applied to the stop's color. Defaults to 100 (fully opaque). */
65
+ opacity?: number;
66
+ }
67
+
68
+ export interface GradientToken {
69
+ /** Output CSS variable, e.g. '--gradient-1'. */
70
+ variable: string;
71
+ type: GradientType;
72
+ /** Degrees, applies to linear only. */
73
+ angle: number;
74
+ stops: GradientTokenStop[];
75
+ }
76
+
77
+ /**
78
+ * Single source of truth for everything a saved token file depends on, plus
79
+ * the domain state currently scattered across VariablesTab local `let` fields.
80
+ * View state (tab selection, dialog flags, drag payloads, editing drafts)
81
+ * stays out of this tree.
82
+ */
83
+ export interface EditorState {
84
+ palettes: Record<string, PaletteConfig>;
85
+ fonts: { sources: FontSource[]; stacks: FontStack[] };
86
+ shadows: {
87
+ globals: ShadowGlobals;
88
+ tokens: ShadowToken[];
89
+ overrides: Record<string, ShadowOverrideFlags>;
90
+ };
91
+ overlays: {
92
+ tokens: OverlayToken[];
93
+ hoverTokens: OverlayToken[];
94
+ globals: OverlayGlobals;
95
+ };
96
+ columns: ColumnsState;
97
+ components: Record<string, ComponentSlice>;
98
+ gradients: { tokens: GradientToken[] };
99
+ cssVars: Record<string, string>;
100
+ }
@@ -0,0 +1,55 @@
1
+ import { writable } from 'svelte/store';
2
+
3
+ export type EditorView = 'tokens' | 'components';
4
+ export type SidebarCondensed = boolean | 'auto';
5
+
6
+ const VIEW_KEY = 'lt.editorView';
7
+ const CONDENSED_KEY = 'lt.sidebarCondensed';
8
+
9
+ function readView(): EditorView {
10
+ try {
11
+ const v = localStorage.getItem(VIEW_KEY);
12
+ if (v === 'components' || v === 'tokens') return v;
13
+ } catch {}
14
+ return 'tokens';
15
+ }
16
+
17
+ function readCondensed(): SidebarCondensed {
18
+ try {
19
+ const v = localStorage.getItem(CONDENSED_KEY);
20
+ if (v === 'true') return true;
21
+ if (v === 'false') return false;
22
+ } catch {}
23
+ return 'auto';
24
+ }
25
+
26
+ export const editorView = writable<EditorView>(readView());
27
+ export const sidebarCondensed = writable<SidebarCondensed>(readCondensed());
28
+ export const selectedComponent = writable<string>('button');
29
+
30
+ editorView.subscribe((v) => {
31
+ try { localStorage.setItem(VIEW_KEY, v); } catch {}
32
+ });
33
+
34
+ sidebarCondensed.subscribe((v) => {
35
+ try { localStorage.setItem(CONDENSED_KEY, v === 'auto' ? 'auto' : String(v)); } catch {}
36
+ });
37
+
38
+ // Cross-window sync: parent and overlay iframe share localStorage but each have
39
+ // their own module state. `storage` events fire in the *other* window when one
40
+ // writes, so we mirror the new value into this window's store.
41
+ if (typeof window !== 'undefined') {
42
+ window.addEventListener('storage', (e) => {
43
+ if (e.key === VIEW_KEY) {
44
+ if (e.newValue === 'tokens' || e.newValue === 'components') editorView.set(e.newValue);
45
+ } else if (e.key === CONDENSED_KEY) {
46
+ if (e.newValue === 'true') sidebarCondensed.set(true);
47
+ else if (e.newValue === 'false') sidebarCondensed.set(false);
48
+ else if (e.newValue === 'auto') sidebarCondensed.set('auto');
49
+ }
50
+ });
51
+ }
52
+
53
+ export function setEditorView(v: EditorView) {
54
+ editorView.set(v);
55
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Client-side REST helpers for any "versioned file resource" — a class of
3
+ * editable artifact whose lifecycle mirrors the theme files: list / load /
4
+ * save / delete + an active pointer + a production pointer.
5
+ *
6
+ * `themeService.ts` and `componentConfigService.ts` previously reimplemented
7
+ * the same fetch shape. They now both consume `versionedFileResource(...)`
8
+ * with their resource-specific URL.
9
+ *
10
+ * Pure URL construction + fetch — no DOM, no filesystem, safe for any browser
11
+ * or test harness that has a `fetch` global.
12
+ */
13
+
14
+ export interface VersionedFileResourceClientOptions {
15
+ /** REST endpoint root, e.g. `/api/themes` or `/api/component-configs/button`. */
16
+ baseUrl: string;
17
+ }
18
+
19
+ export interface VersionedFileResourceClient<TItem, TMeta, TProductionInfo> {
20
+ list(): Promise<{ files: TMeta[]; activeFile?: string; productionFile?: string }>;
21
+ load(fileName: string): Promise<TItem>;
22
+ save(fileName: string, data: TItem): Promise<void>;
23
+ remove(fileName: string): Promise<void>;
24
+ getActive(): Promise<TItem | null>;
25
+ setActive(fileName: string): Promise<void>;
26
+ getProductionInfo(): Promise<TProductionInfo>;
27
+ setProduction(fileName: string): Promise<TProductionInfo & { ok: boolean }>;
28
+ }
29
+
30
+ /**
31
+ * Build a CRUD client bound to a single resource root. All routes follow the
32
+ * same shape:
33
+ * GET {base} → list
34
+ * GET {base}/:name → load
35
+ * PUT {base}/:name → save
36
+ * DELETE {base}/:name → delete
37
+ * GET {base}/active → load active
38
+ * PUT {base}/active {name} → set active
39
+ * GET {base}/production → production info
40
+ * PUT {base}/production {name} → set production
41
+ *
42
+ * Generic over the payload types so theme & component-config can share the
43
+ * same plumbing while keeping their own response/info shapes.
44
+ */
45
+ export function versionedFileResource<TItem, TMeta, TProductionInfo>(
46
+ opts: VersionedFileResourceClientOptions,
47
+ ): VersionedFileResourceClient<TItem, TMeta, TProductionInfo> {
48
+ const { baseUrl } = opts;
49
+
50
+ async function readJsonError(res: Response, fallback: string): Promise<string> {
51
+ const err = await res.json().catch(() => ({ error: fallback }));
52
+ return err.error || fallback;
53
+ }
54
+
55
+ async function list(): Promise<{ files: TMeta[]; activeFile?: string; productionFile?: string }> {
56
+ const res = await fetch(baseUrl);
57
+ if (!res.ok) throw new Error(`Failed to list ${baseUrl}`);
58
+ return res.json();
59
+ }
60
+
61
+ async function load(fileName: string): Promise<TItem> {
62
+ const res = await fetch(`${baseUrl}/${encodeURIComponent(fileName)}`);
63
+ if (!res.ok) throw new Error(`Failed to load: ${fileName}`);
64
+ return res.json();
65
+ }
66
+
67
+ async function save(fileName: string, data: TItem): Promise<void> {
68
+ const res = await fetch(`${baseUrl}/${encodeURIComponent(fileName)}`, {
69
+ method: 'PUT',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify(data),
72
+ });
73
+ if (!res.ok) {
74
+ throw new Error(await readJsonError(res, 'Save failed'));
75
+ }
76
+ }
77
+
78
+ async function remove(fileName: string): Promise<void> {
79
+ const res = await fetch(`${baseUrl}/${encodeURIComponent(fileName)}`, {
80
+ method: 'DELETE',
81
+ });
82
+ if (!res.ok) {
83
+ throw new Error(await readJsonError(res, 'Delete failed'));
84
+ }
85
+ }
86
+
87
+ async function getActive(): Promise<TItem | null> {
88
+ try {
89
+ const res = await fetch(`${baseUrl}/active`);
90
+ if (!res.ok) return null;
91
+ return res.json();
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ async function setActive(fileName: string): Promise<void> {
98
+ const res = await fetch(`${baseUrl}/active`, {
99
+ method: 'PUT',
100
+ headers: { 'Content-Type': 'application/json' },
101
+ body: JSON.stringify({ name: fileName }),
102
+ });
103
+ if (!res.ok) {
104
+ throw new Error(await readJsonError(res, 'Set active failed'));
105
+ }
106
+ }
107
+
108
+ async function getProductionInfo(): Promise<TProductionInfo> {
109
+ const res = await fetch(`${baseUrl}/production`);
110
+ if (!res.ok) throw new Error('Failed to get production info');
111
+ return res.json();
112
+ }
113
+
114
+ async function setProduction(fileName: string): Promise<TProductionInfo & { ok: boolean }> {
115
+ const res = await fetch(`${baseUrl}/production`, {
116
+ method: 'PUT',
117
+ headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify({ name: fileName }),
119
+ });
120
+ if (!res.ok) {
121
+ throw new Error(await readJsonError(res, 'Set production failed'));
122
+ }
123
+ return res.json();
124
+ }
125
+
126
+ return { list, load, save, remove, getActive, setActive, getProductionInfo, setProduction };
127
+ }
128
+
129
+ /** Sanitize a display name to a safe file name. Pure — no DOM, no fs. */
130
+ export function sanitizeFileName(name: string): string {
131
+ return (
132
+ name
133
+ .toLowerCase()
134
+ .trim()
135
+ .replace(/\s+/g, '-')
136
+ .replace(/[^a-z0-9\-_]/g, '')
137
+ .replace(/-+/g, '-')
138
+ .replace(/^-|-$/g, '') || 'unnamed'
139
+ );
140
+ }