@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
@@ -1,26 +1,30 @@
1
1
  import { writable } from 'svelte/store';
2
2
  import { storageKey } from './editorConfig';
3
+ import { quietGet, quietSet } from './storage';
3
4
 
4
- const STORAGE_KEY = storageKey('columns-visible');
5
-
6
- function load(): boolean {
7
- try {
8
- return localStorage.getItem(STORAGE_KEY) === '1';
9
- } catch {
10
- return false;
11
- }
5
+ function getStorageKey(): string {
6
+ return storageKey('columns-visible');
12
7
  }
13
8
 
14
- export const columnsVisible = writable<boolean>(load());
15
-
16
- columnsVisible.subscribe((v) => {
17
- try {
18
- localStorage.setItem(STORAGE_KEY, v ? '1' : '0');
19
- } catch {
20
- // ignore quota errors
21
- }
22
- });
9
+ export const columnsVisible = writable<boolean>(false);
23
10
 
24
11
  export function toggleColumns() {
25
12
  columnsVisible.update((v) => !v);
26
13
  }
14
+
15
+ let initialised = false;
16
+
17
+ /**
18
+ * Idempotent host hook — call once during boot to hydrate the column-visibility
19
+ * store from localStorage and start persisting future writes. Importing the
20
+ * module no longer subscribes-then-persists, so SSR / test harnesses can pull
21
+ * `columnsVisible` without DOM/storage access.
22
+ */
23
+ export function init(): void {
24
+ if (initialised) return;
25
+ initialised = true;
26
+ columnsVisible.set(quietGet(getStorageKey()) === '1');
27
+ columnsVisible.subscribe((v) => {
28
+ quietSet(getStorageKey(), v ? '1' : '0');
29
+ });
30
+ }
@@ -0,0 +1,204 @@
1
+ // @vitest-environment happy-dom
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import { get } from 'svelte/store';
4
+ import {
5
+ editorState,
6
+ componentDirty,
7
+ setComponentAlias,
8
+ clearComponentAlias,
9
+ setComponentConfig,
10
+ clearComponentConfig,
11
+ loadComponentActive,
12
+ markComponentSaved,
13
+ seedComponentsFromApi,
14
+ undo,
15
+ redo,
16
+ __resetForTests,
17
+ } from './editorStore';
18
+
19
+ const tokenRef = (name: string) => ({ kind: 'token' as const, name });
20
+
21
+ beforeEach(() => {
22
+ __resetForTests();
23
+ });
24
+
25
+ describe('component aliases — editor-state round trip', () => {
26
+ it('setComponentAlias → undo restores previous state; redo reapplies', () => {
27
+ setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-success-high'));
28
+ expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
29
+
30
+ setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
31
+ expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-error-high'));
32
+
33
+ undo();
34
+ expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
35
+
36
+ redo();
37
+ expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-error-high'));
38
+ });
39
+
40
+ it('clearComponentAlias removes the entry and is undoable', () => {
41
+ setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-success-high'));
42
+ clearComponentAlias('button', '--button-primary-surface');
43
+ expect(get(editorState).components.button.aliases['--button-primary-surface']).toBeUndefined();
44
+
45
+ undo();
46
+ expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
47
+ });
48
+
49
+ it('setComponentAlias implicitly registers the slice with activeFile "default"', () => {
50
+ setComponentAlias('card', '--card-radius', tokenRef('--radius-lg'));
51
+ expect(get(editorState).components.card.activeFile).toBe('default');
52
+ expect(get(editorState).components.card.aliases['--card-radius']).toEqual(tokenRef('--radius-lg'));
53
+ expect(get(editorState).components.card.config).toEqual({});
54
+ });
55
+ });
56
+
57
+ describe('component config — literal-valued knobs', () => {
58
+ it('setComponentConfig stores the value and is undoable', () => {
59
+ setComponentConfig('dialog', '--dialog-confirm-variant', 'danger');
60
+ expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('danger');
61
+
62
+ setComponentConfig('dialog', '--dialog-confirm-variant', 'warning');
63
+ expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('warning');
64
+
65
+ undo();
66
+ expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('danger');
67
+ });
68
+
69
+ it('clearComponentConfig removes the entry', () => {
70
+ setComponentConfig('dialog', '--dialog-confirm-variant', 'danger');
71
+ clearComponentConfig('dialog', '--dialog-confirm-variant');
72
+ expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBeUndefined();
73
+ });
74
+
75
+ it('setComponentConfig implicitly registers the slice with empty aliases', () => {
76
+ setComponentConfig('dialog', '--dialog-cancel-variant', 'outline');
77
+ expect(get(editorState).components.dialog.activeFile).toBe('default');
78
+ expect(get(editorState).components.dialog.aliases).toEqual({});
79
+ expect(get(editorState).components.dialog.config['--dialog-cancel-variant']).toBe('outline');
80
+ });
81
+ });
82
+
83
+ describe('componentDirty — per-component scoping', () => {
84
+ it('marks a component dirty only after its aliases diverge from the saved baseline', () => {
85
+ loadComponentActive('button', 'default', { '--button-primary-surface': '--surface-success-high' });
86
+ expect(get(componentDirty).button).toBe(false);
87
+
88
+ setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
89
+ expect(get(componentDirty).button).toBe(true);
90
+
91
+ markComponentSaved('button');
92
+ expect(get(componentDirty).button).toBe(false);
93
+ });
94
+
95
+ it('marks a component dirty when config diverges from the saved baseline', () => {
96
+ loadComponentActive('dialog', 'default', {}, { '--dialog-confirm-variant': 'primary' });
97
+ expect(get(componentDirty).dialog).toBe(false);
98
+
99
+ setComponentConfig('dialog', '--dialog-confirm-variant', 'danger');
100
+ expect(get(componentDirty).dialog).toBe(true);
101
+
102
+ markComponentSaved('dialog');
103
+ expect(get(componentDirty).dialog).toBe(false);
104
+ });
105
+
106
+ it('editing one component does not dirty another', () => {
107
+ loadComponentActive('button', 'default', { '--button-primary-surface': '--surface-success-high' });
108
+ loadComponentActive('card', 'default', { '--card-radius': '--radius-md' });
109
+ expect(get(componentDirty).button).toBe(false);
110
+ expect(get(componentDirty).card).toBe(false);
111
+
112
+ setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
113
+ expect(get(componentDirty).button).toBe(true);
114
+ expect(get(componentDirty).card).toBe(false);
115
+ });
116
+
117
+ it('undo after a saved state marks dirty again', () => {
118
+ loadComponentActive('button', 'default', { '--button-primary-surface': '--surface-success-high' });
119
+ setComponentAlias('button', '--button-primary-surface', tokenRef('--surface-error-high'));
120
+ markComponentSaved('button');
121
+ expect(get(componentDirty).button).toBe(false);
122
+
123
+ undo();
124
+ expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
125
+ expect(get(componentDirty).button).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe('loadComponentActive — split-on-load migration', () => {
130
+ it('routes legacy config keys from single-bucket aliases into the config bucket', () => {
131
+ loadComponentActive('dialog', 'default', {
132
+ '--dialog-surface': '--surface-neutral-low',
133
+ '--dialog-confirm-variant': 'danger',
134
+ });
135
+ const slice = get(editorState).components.dialog;
136
+ expect(slice.aliases['--dialog-surface']).toEqual(tokenRef('--surface-neutral-low'));
137
+ expect(slice.aliases['--dialog-confirm-variant']).toBeUndefined();
138
+ expect(slice.config['--dialog-confirm-variant']).toBe('danger');
139
+ });
140
+
141
+ it('keeps CSS-var-valued aliases (e.g. --button-shimmer → --shimmer-on) in the aliases bucket', () => {
142
+ loadComponentActive('button', 'default', {
143
+ '--button-primary-surface': '--surface-success-high',
144
+ '--button-shimmer': '--shimmer-on',
145
+ });
146
+ const slice = get(editorState).components.button;
147
+ expect(slice.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
148
+ expect(slice.aliases['--button-shimmer']).toEqual(tokenRef('--shimmer-on'));
149
+ expect(slice.config['--button-shimmer']).toBeUndefined();
150
+ });
151
+
152
+ it('classifies literal alias values as kind "literal"', () => {
153
+ loadComponentActive('myComp', 'default', {
154
+ '--my-comp-token': '--some-token',
155
+ '--my-comp-color': 'rebeccapurple',
156
+ });
157
+ const slice = get(editorState).components.myComp;
158
+ expect(slice.aliases['--my-comp-token']).toEqual(tokenRef('--some-token'));
159
+ expect(slice.aliases['--my-comp-color']).toEqual({ kind: 'literal', value: 'rebeccapurple' });
160
+ });
161
+
162
+ it('explicit config field wins over legacy alias-bucketed value', () => {
163
+ loadComponentActive(
164
+ 'dialog',
165
+ 'default',
166
+ { '--dialog-confirm-variant': 'primary' },
167
+ { '--dialog-confirm-variant': 'danger' },
168
+ );
169
+ expect(get(editorState).components.dialog.config['--dialog-confirm-variant']).toBe('danger');
170
+ });
171
+ });
172
+
173
+ describe('seedComponentsFromApi — boot-time hydration', () => {
174
+ it('populates state and establishes the clean baseline', () => {
175
+ seedComponentsFromApi({
176
+ button: { activeFile: 'myConfig', aliases: { '--button-primary-surface': '--surface-success-high' } },
177
+ });
178
+ expect(get(editorState).components.button.activeFile).toBe('myConfig');
179
+ expect(get(editorState).components.button.aliases['--button-primary-surface']).toEqual(tokenRef('--surface-success-high'));
180
+ expect(get(componentDirty).button).toBe(false);
181
+ });
182
+
183
+ it('routes config keys when seeding from legacy single-bucket API payload', () => {
184
+ seedComponentsFromApi({
185
+ dialog: {
186
+ activeFile: 'default',
187
+ aliases: { '--dialog-confirm-variant': 'danger', '--dialog-shadow': '--shadow-2xl' },
188
+ },
189
+ });
190
+ const slice = get(editorState).components.dialog;
191
+ expect(slice.aliases['--dialog-shadow']).toEqual(tokenRef('--shadow-2xl'));
192
+ expect(slice.aliases['--dialog-confirm-variant']).toBeUndefined();
193
+ expect(slice.config['--dialog-confirm-variant']).toBe('danger');
194
+ });
195
+
196
+ it('replaces the full components slice', () => {
197
+ setComponentAlias('card', '--card-radius', tokenRef('--radius-md'));
198
+ seedComponentsFromApi({
199
+ button: { activeFile: 'default', aliases: {} },
200
+ });
201
+ expect(get(editorState).components.card).toBeUndefined();
202
+ expect(get(editorState).components.button).toBeDefined();
203
+ });
204
+ });
@@ -0,0 +1,19 @@
1
+ // TODO(M3 schemaVersion / Wave 4): replace this enumerated list with a
2
+ // per-component schema declaration (e.g. registerComponentSchema(...,
3
+ // { kind: 'config' })). For now it's a flat set used by the on-load
4
+ // migration that splits legacy single-bucket aliases into the new
5
+ // {aliases, config} shape.
6
+ //
7
+ // What goes here: literal-valued knobs that don't translate to CSS vars
8
+ // (e.g. Dialog's confirm/cancel variant string is consumed by Dialog.svelte
9
+ // via `$editorState`, not via CSS cascade).
10
+ //
11
+ // What does NOT go here: aliases whose values are themselves CSS-var refs
12
+ // — even if the value space is constrained (e.g. `--button-shimmer` →
13
+ // `--shimmer-on` | `--shimmer-off`). Those are still aliases and must flow
14
+ // through `componentsToVars` so SCSS that does `var(--button-shimmer)`
15
+ // keeps resolving.
16
+ export const KNOWN_COMPONENT_CONFIG_KEYS: ReadonlySet<string> = new Set([
17
+ '--dialog-confirm-variant',
18
+ '--dialog-cancel-variant',
19
+ ]);
@@ -0,0 +1,88 @@
1
+ import type { ComponentConfig, ComponentConfigMeta } from './themeTypes';
2
+ import { versionedFileResource } from './files/versionedFileResource';
3
+
4
+ /**
5
+ * 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.
10
+ *
11
+ * Both this and `themeService` consume `versionedFileResource(...)`. Adding a
12
+ * third file-managed resource — per the user's "mirror theme-file lifecycle
13
+ * for new editor artifacts" invariant — is one helper call here, not a third
14
+ * round of copy-paste CRUD.
15
+ */
16
+
17
+ export interface ComponentSummary {
18
+ name: string;
19
+ activeFile: string;
20
+ productionFile: string;
21
+ }
22
+
23
+ export interface ComponentProductionInfo {
24
+ fileName: string;
25
+ name: string;
26
+ aliases: Record<string, string>;
27
+ }
28
+
29
+ export interface ComponentConfigList {
30
+ component: string;
31
+ files: ComponentConfigMeta[];
32
+ activeFile: string;
33
+ productionFile: string;
34
+ }
35
+
36
+ export async function listComponents(): Promise<ComponentSummary[]> {
37
+ const res = await fetch('/api/component-configs');
38
+ if (!res.ok) throw new Error('Failed to list components');
39
+ const data = await res.json();
40
+ return data.components;
41
+ }
42
+
43
+ function resourceFor(component: string) {
44
+ return versionedFileResource<ComponentConfig, ComponentConfigMeta, ComponentProductionInfo>({
45
+ baseUrl: `/api/component-configs/${encodeURIComponent(component)}`,
46
+ });
47
+ }
48
+
49
+ export async function listComponentConfigs(component: string): Promise<ComponentConfigList> {
50
+ const data = await resourceFor(component).list();
51
+ return {
52
+ component,
53
+ files: data.files,
54
+ activeFile: data.activeFile ?? 'default',
55
+ productionFile: data.productionFile ?? 'default',
56
+ };
57
+ }
58
+
59
+ export const loadComponentConfig = (
60
+ component: string,
61
+ fileName: string,
62
+ ): Promise<ComponentConfig> => resourceFor(component).load(fileName);
63
+
64
+ export const saveComponentConfig = (
65
+ component: string,
66
+ fileName: string,
67
+ data: ComponentConfig,
68
+ ): Promise<void> => resourceFor(component).save(fileName, data);
69
+
70
+ export const deleteComponentConfig = (component: string, fileName: string): Promise<void> =>
71
+ resourceFor(component).remove(fileName);
72
+
73
+ export const getActiveComponentConfig = (component: string): Promise<ComponentConfig | null> =>
74
+ resourceFor(component).getActive();
75
+
76
+ export const setActiveComponentFile = (component: string, fileName: string): Promise<void> =>
77
+ resourceFor(component).setActive(fileName);
78
+
79
+ export const getComponentProductionInfo = (component: string): Promise<ComponentProductionInfo> =>
80
+ resourceFor(component).getProductionInfo();
81
+
82
+ export async function setComponentProductionFile(
83
+ component: string,
84
+ fileName: string,
85
+ ): Promise<{ ok: boolean; productionFile: string }> {
86
+ const data = await resourceFor(component).setProduction(fileName);
87
+ return { ok: data.ok, productionFile: data.fileName };
88
+ }
@@ -0,0 +1,30 @@
1
+ import { writable } from 'svelte/store';
2
+
3
+ export type CopyPopoverState = {
4
+ visible: boolean;
5
+ value: string;
6
+ /** Viewport-relative anchor rect for the trigger element. */
7
+ anchor: { top: number; left: number; width: number; height: number } | null;
8
+ /** Monotonic id so consecutive copies of the same value re-trigger the timer. */
9
+ nonce: number;
10
+ };
11
+
12
+ const initial: CopyPopoverState = { visible: false, value: '', anchor: null, nonce: 0 };
13
+
14
+ export const copyPopover = writable<CopyPopoverState>(initial);
15
+
16
+ let hideTimer: ReturnType<typeof setTimeout> | null = null;
17
+
18
+ export function showCopyPopover(value: string, target: EventTarget | null, durationMs = 1500) {
19
+ const el = target instanceof Element ? target : null;
20
+ const rect = el?.getBoundingClientRect() ?? null;
21
+ const anchor = rect ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height } : null;
22
+
23
+ copyPopover.update((s) => ({ visible: true, value, anchor, nonce: s.nonce + 1 }));
24
+
25
+ if (hideTimer) clearTimeout(hideTimer);
26
+ hideTimer = setTimeout(() => {
27
+ copyPopover.update((s) => ({ ...s, visible: false }));
28
+ hideTimer = null;
29
+ }, durationMs);
30
+ }
@@ -4,14 +4,24 @@
4
4
  * Writes to document.documentElement and — when running inside a same-origin
5
5
  * iframe (the live-preview overlay) — also writes to
6
6
  * window.parent.document.documentElement. This lets the overlay editor at
7
- * /admin drive the host site's :root in real time without any message-passing
7
+ * /editor drive the host site's :root in real time without any message-passing
8
8
  * infrastructure.
9
9
  *
10
- * When the editor runs standalone at /admin (not inside the overlay iframe),
10
+ * When the editor runs standalone at /editor (not inside the overlay iframe),
11
11
  * parentRoot is null and every call is a plain single-root write.
12
+ *
13
+ * Roots are resolved lazily — `init()` (or any setter call) populates them on
14
+ * first use so importing this module does not touch the DOM. This keeps the
15
+ * library importable from SSR / test harnesses and decouples consumers from
16
+ * module-load ordering.
12
17
  */
13
18
 
19
+ let selfRoot: HTMLElement | null = null;
20
+ let parentRoot: HTMLElement | null = null;
21
+ let resolved = false;
22
+
14
23
  function resolveParentRoot(): HTMLElement | null {
24
+ if (typeof window === 'undefined') return null;
15
25
  try {
16
26
  if (window.parent !== window && window.parent?.document) {
17
27
  return window.parent.document.documentElement;
@@ -22,17 +32,56 @@ function resolveParentRoot(): HTMLElement | null {
22
32
  return null;
23
33
  }
24
34
 
25
- const selfRoot: HTMLElement = document.documentElement;
26
- const parentRoot: HTMLElement | null = resolveParentRoot();
35
+ function ensureResolved(): void {
36
+ if (resolved) return;
37
+ resolved = true;
38
+ selfRoot = typeof document !== 'undefined' ? document.documentElement : null;
39
+ parentRoot = resolveParentRoot();
40
+ }
41
+
42
+ /**
43
+ * Idempotent host hook — call once during boot to eagerly resolve the self
44
+ * and parent document roots. Optional in practice (any setter call resolves
45
+ * lazily), but explicit init makes ordering legible.
46
+ */
47
+ export function init(): void {
48
+ ensureResolved();
49
+ }
50
+
51
+ /**
52
+ * Return the self and parent document heads as a tuple, with parent omitted
53
+ * when not in an iframe. Consumers that need to mirror node injection (not
54
+ * just style properties) can iterate this list.
55
+ */
56
+ export function getSyncedDocuments(): Document[] {
57
+ ensureResolved();
58
+ if (typeof document === 'undefined') return [];
59
+ const docs: Document[] = [document];
60
+ if (parentRoot && parentRoot.ownerDocument && parentRoot.ownerDocument !== document) {
61
+ docs.push(parentRoot.ownerDocument);
62
+ }
63
+ return docs;
64
+ }
65
+
66
+ export const CSS_VAR_CHANGE_EVENT = 'cssvar:change';
67
+
68
+ function notifyChange(name: string): void {
69
+ if (typeof document === 'undefined') return;
70
+ document.dispatchEvent(new CustomEvent(CSS_VAR_CHANGE_EVENT, { detail: { name } }));
71
+ }
27
72
 
28
73
  export function setCssVar(name: string, value: string): void {
29
- selfRoot.style.setProperty(name, value);
74
+ ensureResolved();
75
+ selfRoot?.style.setProperty(name, value);
30
76
  parentRoot?.style.setProperty(name, value);
77
+ notifyChange(name);
31
78
  }
32
79
 
33
80
  export function removeCssVar(name: string): void {
34
- selfRoot.style.removeProperty(name);
81
+ ensureResolved();
82
+ selfRoot?.style.removeProperty(name);
35
83
  parentRoot?.style.removeProperty(name);
84
+ notifyChange(name);
36
85
  }
37
86
 
38
87
  /** Apply a map of CSS variables to :root (and the parent :root when in an iframe). */
@@ -44,7 +93,8 @@ export function applyCssVariables(variables: Record<string, string>): void {
44
93
 
45
94
  /** Remove all inline CSS custom properties from :root on both self and parent. */
46
95
  export function clearAllCssVarOverrides(): void {
47
- clearRoot(selfRoot);
96
+ ensureResolved();
97
+ if (selfRoot) clearRoot(selfRoot);
48
98
  if (parentRoot) clearRoot(parentRoot);
49
99
  }
50
100
 
@@ -60,6 +110,8 @@ function clearRoot(el: HTMLElement): void {
60
110
 
61
111
  /** Scrape all inline CSS custom properties currently on self :root. */
62
112
  export function scrapeCssVariables(): Record<string, string> {
113
+ ensureResolved();
114
+ if (!selfRoot) return {};
63
115
  const style = selfRoot.style;
64
116
  const variables: Record<string, string> = {};
65
117
  for (let i = 0; i < style.length; i++) {
@@ -1,14 +1,4 @@
1
1
  import { writable } from 'svelte/store';
2
- import type { PaletteConfig } from './tokenTypes';
3
-
4
- /** Each PaletteEditor pushes its config here keyed by label. Used when saving. */
5
- export const editorConfigs = writable<Record<string, PaletteConfig>>({});
6
-
7
- /** Set by the load flow. Each PaletteEditor watches for its label and applies the config. */
8
- export const loadedConfigs = writable<Record<string, PaletteConfig> | null>(null);
9
-
10
- /** True when editorConfigs were populated from a token file load or deliberate user edit, not just component defaults. */
11
- export const configsLoadedFromFile = writable<boolean>(false);
12
2
 
13
3
  /** The file name of the currently active token file. */
14
4
  export const activeFileName = writable<string>('default');