@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,661 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy, createEventDispatcher } from 'svelte';
3
+ import { fade, fly, slide } from 'svelte/transition';
4
+ import { cubicOut, cubicIn, cubicInOut } from 'svelte/easing';
5
+ import UITokenSelector from './UITokenSelector.svelte';
6
+ import UIOptionList from './UIOptionList.svelte';
7
+ import UIOptionItem from './UIOptionItem.svelte';
8
+ import { setCssVar, removeCssVar, CSS_VAR_CHANGE_EVENT } from '../lib/cssVarSync';
9
+ import {
10
+ editorState,
11
+ setComponentAlias,
12
+ clearComponentAlias,
13
+ getComponentPropertySiblings,
14
+ isComponentPropertyLinked,
15
+ setComponentAliasLinked,
16
+ unlinkComponentProperty,
17
+ relinkComponentProperty,
18
+ } from '../lib/editorStore';
19
+ import UIRelinkConfirmPopover from './UIRelinkConfirmPopover.svelte';
20
+ import UILinkToggle from './UILinkToggle.svelte';
21
+
22
+ const dispatch = createEventDispatcher();
23
+
24
+ export let variable: string;
25
+ export let component: string | undefined = undefined;
26
+ export let canBeLinked: boolean = false;
27
+ export let disabled: boolean = false;
28
+ export let selectionsLocked: boolean = false;
29
+ /** When 'sides', renders the per-side rows alongside the link/merge header.
30
+ The header occupies cols 2-3 of the parent token-row (sharing row 1 with
31
+ TokenLayout's .token-label) and the side rows fill cols 1-3 of row 2. */
32
+ export let mode: 'single' | 'sides' = 'single';
33
+ /** When false, hide the split-to-sides affordance (e.g. for non-box spacing like gap). */
34
+ export let splittable: boolean = true;
35
+
36
+ type Side = 'top' | 'right' | 'bottom' | 'left';
37
+ const SIDES: readonly Side[] = ['top', 'right', 'bottom', 'left'];
38
+
39
+ /** Honor prefers-reduced-motion: skip the orchestrated split↔merge entry/exit
40
+ when the OS asks for less motion. Read once at module mount. The split
41
+ transition has two phases — old-block fades, then new-block expands; the
42
+ cumulative time is large enough that ignoring this preference would be
43
+ annoying for users who set it. */
44
+ const reduceMotion = typeof window !== 'undefined'
45
+ && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
46
+ const t = (ms: number) => (reduceMotion ? 0 : ms);
47
+
48
+ function sideVar(s: Side): string {
49
+ return `${variable}-${s}`;
50
+ }
51
+
52
+ const options = [
53
+ { key: '0', label: 'None', size: '0' },
54
+ { key: '2', label: '2XS', size: '0.125rem' },
55
+ { key: '4', label: 'XS', size: '0.25rem' },
56
+ { key: '6', label: 'Small', size: '0.375rem' },
57
+ { key: '8', label: 'Medium', size: '0.5rem' },
58
+ { key: '10', label: 'Large', size: '0.625rem' },
59
+ { key: '12', label: 'XL', size: '0.75rem' },
60
+ { key: '16', label: '2XL', size: '1rem' },
61
+ { key: '20', label: '3XL', size: '1.25rem' },
62
+ { key: '24', label: '4XL', size: '1.5rem' },
63
+ { key: '32', label: '5XL', size: '2rem' },
64
+ { key: '48', label: '6XL', size: '3rem' },
65
+ ];
66
+
67
+ function tokenForKey(key: string): string {
68
+ return `--space-${key}`;
69
+ }
70
+
71
+ function readAlias(v: string): string {
72
+ if (component) {
73
+ const ref = $editorState.components[component]?.aliases?.[v];
74
+ return ref?.kind === 'token' ? ref.name : '';
75
+ }
76
+ const inline = document.documentElement.style.getPropertyValue(v).trim();
77
+ const m = inline.match(/var\((--space-[a-z0-9]+)\)/);
78
+ return m ? m[1] : '';
79
+ }
80
+
81
+ function parseKey(value: string): string | null {
82
+ if (!value) return null;
83
+ const m = value.match(/^--space-([a-z0-9]+)$/);
84
+ if (!m) return null;
85
+ return options.find((o) => o.key === m[1]) ? m[1] : null;
86
+ }
87
+
88
+ function readResolved(v: string): string {
89
+ return getComputedStyle(document.documentElement).getPropertyValue(v).trim();
90
+ }
91
+
92
+ function writeAlias(v: string, semantic: string | null) {
93
+ if (component) {
94
+ if (semantic) setComponentAlias(component, v, { kind: 'token', name: semantic });
95
+ else clearComponentAlias(component, v);
96
+ return;
97
+ }
98
+ if (semantic) setCssVar(v, `var(${semantic})`);
99
+ else removeCssVar(v);
100
+ }
101
+
102
+ /** Honor the parent token's linkage on every write the padding selector
103
+ * performs — for the parent var itself and for any per-side child var. When
104
+ * the parent is linked, mirror the write across every linked peer's
105
+ * matching var (parent → peer; parent-side → peer-side); otherwise fall
106
+ * through to the local write. The split topology is "shared" because the
107
+ * parent's groupKey is — peers stay in lockstep on single↔split toggles
108
+ * and per-side picks alike. */
109
+ function writeAliasLinked(targetVar: string, semantic: string | null) {
110
+ if (!component) {
111
+ writeAlias(targetVar, semantic);
112
+ return;
113
+ }
114
+ if (!isComponentPropertyLinked(component, variable)) {
115
+ writeAlias(targetVar, semantic);
116
+ return;
117
+ }
118
+ const peers = getComponentPropertySiblings(component, variable);
119
+ if (peers.length < 2) {
120
+ writeAlias(targetVar, semantic);
121
+ return;
122
+ }
123
+ const slice = $editorState.components[component];
124
+ const unlinked = slice?.unlinked ?? [];
125
+ const suffix = targetVar.slice(variable.length);
126
+ for (const peer of peers) {
127
+ if (unlinked.includes(peer)) continue;
128
+ const peerVar = `${peer}${suffix}`;
129
+ if (semantic) setComponentAlias(component, peerVar, { kind: 'token', name: semantic });
130
+ else clearComponentAlias(component, peerVar);
131
+ }
132
+ }
133
+
134
+ let chosenKey: string | null = null;
135
+ let resolvedSize = '';
136
+ let sideKeys: Record<Side, string | null> = { top: null, right: null, bottom: null, left: null };
137
+ let sideResolved: Record<Side, string> = { top: '', right: '', bottom: '', left: '' };
138
+
139
+ function refreshFromState() {
140
+ chosenKey = parseKey(readAlias(variable));
141
+ resolvedSize = readResolved(variable);
142
+ const nextKeys: Record<Side, string | null> = { top: null, right: null, bottom: null, left: null };
143
+ const nextResolved: Record<Side, string> = { top: '', right: '', bottom: '', left: '' };
144
+ for (const s of SIDES) {
145
+ nextKeys[s] = parseKey(readAlias(sideVar(s)));
146
+ nextResolved[s] = readResolved(sideVar(s));
147
+ }
148
+ sideKeys = nextKeys;
149
+ sideResolved = nextResolved;
150
+ }
151
+
152
+ function selectSingle(key: string, close: () => void) {
153
+ writeAliasLinked(variable, tokenForKey(key));
154
+ close();
155
+ dispatch('change');
156
+ }
157
+
158
+ function selectSide(s: Side, key: string, close: () => void) {
159
+ writeAliasLinked(sideVar(s), tokenForKey(key));
160
+ close();
161
+ dispatch('change');
162
+ }
163
+
164
+ function handleResetAll() {
165
+ for (const s of SIDES) writeAliasLinked(sideVar(s), null);
166
+ writeAliasLinked(variable, null);
167
+ dispatch('change');
168
+ }
169
+
170
+ function handleResetSide(s: Side) {
171
+ writeAliasLinked(sideVar(s), null);
172
+ dispatch('change');
173
+ }
174
+
175
+ function splitToSides() {
176
+ if (disabled) return;
177
+ const seed = readAlias(variable) || tokenForKey('4');
178
+ for (const s of SIDES) writeAliasLinked(sideVar(s), seed);
179
+ dispatch('change');
180
+ }
181
+
182
+ function mergeToSingle() {
183
+ if (disabled) return;
184
+ const seed = readAlias(sideVar('top')) || readAlias(variable);
185
+ if (seed && !readAlias(variable)) writeAliasLinked(variable, seed);
186
+ for (const s of SIDES) writeAliasLinked(sideVar(s), null);
187
+ dispatch('change');
188
+ }
189
+
190
+ function handleVarChange() {
191
+ refreshFromState();
192
+ }
193
+
194
+ onMount(() => {
195
+ refreshFromState();
196
+ document.addEventListener(CSS_VAR_CHANGE_EVENT, handleVarChange);
197
+ });
198
+
199
+ onDestroy(() => {
200
+ document.removeEventListener(CSS_VAR_CHANGE_EVENT, handleVarChange);
201
+ });
202
+
203
+ // Track `variable` alongside `$editorState` so a VariantGroup tabs view that
204
+ // reuses this selector across states refreshes when the bound prop swaps.
205
+ $: { variable; if ($editorState) refreshFromState(); }
206
+
207
+ $: activeKey = chosenKey ?? (options.find((o) => o.size === resolvedSize)?.key ?? null);
208
+ $: activeLabel = options.find((o) => o.key === activeKey)?.label ?? '';
209
+ $: sideActiveKey = {
210
+ top: sideKeys.top ?? (options.find((o) => o.size === sideResolved.top)?.key ?? null),
211
+ right: sideKeys.right ?? (options.find((o) => o.size === sideResolved.right)?.key ?? null),
212
+ bottom: sideKeys.bottom ?? (options.find((o) => o.size === sideResolved.bottom)?.key ?? null),
213
+ left: sideKeys.left ?? (options.find((o) => o.size === sideResolved.left)?.key ?? null),
214
+ } as Record<Side, string | null>;
215
+ $: sideLabels = {
216
+ top: options.find((o) => o.key === sideActiveKey.top)?.label ?? '',
217
+ right: options.find((o) => o.key === sideActiveKey.right)?.label ?? '',
218
+ bottom: options.find((o) => o.key === sideActiveKey.bottom)?.label ?? '',
219
+ left: options.find((o) => o.key === sideActiveKey.left)?.label ?? '',
220
+ } as Record<Side, string>;
221
+
222
+ // Linkage state for the parent token. The split fieldset is a single
223
+ // linkage unit — peers either share the parent (and their per-side state
224
+ // mirrors via `writeAliasLinked`) or one peer has detached. Mirrors the
225
+ // pop-bar/lock-toggle vocabulary used by `UITokenSelector` so the visual
226
+ // and interaction language is the same at both scales.
227
+ $: hasParentSiblings = canBeLinked && component && $editorState
228
+ ? getComponentPropertySiblings(component, variable).length >= 2
229
+ : false;
230
+ $: showLinkUI = canBeLinked && !!component && hasParentSiblings;
231
+ /** $editorState is referenced directly so this re-evaluates on every store
232
+ * update. isComponentPropertyLinked reads `get(store)` internally — Svelte
233
+ * can't see that as a dependency. Without the explicit reference, an
234
+ * unlink mutation (which leaves siblings count and showLinkUI unchanged)
235
+ * would not re-run this expression: Svelte's $$invalidate skips marking
236
+ * a boolean dirty when the new value equals the old, so the dirty bit
237
+ * never propagates to isLinkedParent. */
238
+ $: isLinkedParent = !!$editorState
239
+ && showLinkUI
240
+ && !!component
241
+ && isComponentPropertyLinked(component, variable);
242
+
243
+ let relinkOpen = false;
244
+ let relinkCandidates: { variable: string; alias: string }[] = [];
245
+
246
+ /** Adopt one peer's entire padding block (parent + four sides) onto every
247
+ * currently-linked peer. Padding is treated as a single shared unit — when
248
+ * peers diverge in split topology (one in single mode, another in split),
249
+ * relinking must converge them, not just mirror the parent var. The source
250
+ * peer's split-or-single state, plus all four side aliases, become the
251
+ * group's canonical state. */
252
+ function adoptBlockFromPeer(sourcePeer: string) {
253
+ if (!component) return;
254
+ const slice = $editorState.components[component];
255
+ if (!slice) return;
256
+
257
+ const sourceParent = slice.aliases[sourcePeer];
258
+ const sourceSides: Record<Side, ReturnType<typeof readSideAlias>> = {
259
+ top: readSideAlias(sourcePeer, 'top'),
260
+ right: readSideAlias(sourcePeer, 'right'),
261
+ bottom: readSideAlias(sourcePeer, 'bottom'),
262
+ left: readSideAlias(sourcePeer, 'left'),
263
+ };
264
+
265
+ if (sourceParent) setComponentAliasLinked(component, variable, sourceParent);
266
+ else relinkComponentProperty(component, variable);
267
+
268
+ // After re-engaging linkage, fan the source's per-side state to every
269
+ // peer not currently opted out. `setComponentAliasLinked` already removed
270
+ // `variable` from `unlinked`; re-read so we don't skip self.
271
+ const next = $editorState.components[component];
272
+ if (!next) return;
273
+ const peers = getComponentPropertySiblings(component, variable);
274
+ const unlinked = new Set(next.unlinked ?? []);
275
+ for (const peer of peers) {
276
+ if (unlinked.has(peer)) continue;
277
+ for (const side of SIDES) {
278
+ const peerSide = `${peer}-${side}`;
279
+ const ref = sourceSides[side];
280
+ if (ref) setComponentAlias(component, peerSide, ref);
281
+ else clearComponentAlias(component, peerSide);
282
+ }
283
+ }
284
+ }
285
+
286
+ function readSideAlias(peer: string, side: Side) {
287
+ if (!component) return undefined;
288
+ const slice = $editorState.components[component];
289
+ return slice?.aliases[`${peer}-${side}`];
290
+ }
291
+
292
+ function toggleLinkPaddingGroup() {
293
+ if (!showLinkUI || !component) return;
294
+ if (isLinkedParent) {
295
+ unlinkComponentProperty(component, variable);
296
+ dispatch('change');
297
+ return;
298
+ }
299
+ const slice = $editorState.components[component];
300
+ if (!slice) return;
301
+ const peers = getComponentPropertySiblings(component, variable);
302
+ if (peers.length < 2) return;
303
+ const linkedPeers = peers.filter(
304
+ (v) => v !== variable && !slice.unlinked?.includes(v),
305
+ );
306
+ if (linkedPeers.length === 0) {
307
+ const currentValue = slice.aliases[variable];
308
+ if (currentValue) {
309
+ adoptBlockFromPeer(variable);
310
+ dispatch('change');
311
+ }
312
+ return;
313
+ }
314
+ const candidates = linkedPeers.map((v) => {
315
+ const ref = slice.aliases[v];
316
+ const alias = ref?.kind === 'token' ? ref.name : '';
317
+ return { variable: v, alias };
318
+ });
319
+ const definedCandidates = candidates.filter((c) => c.alias);
320
+ const distinctValues = new Set(definedCandidates.map((c) => c.alias));
321
+ if (distinctValues.size <= 1) {
322
+ const sourcePeer = definedCandidates.length > 0
323
+ ? definedCandidates[0].variable
324
+ : variable;
325
+ adoptBlockFromPeer(sourcePeer);
326
+ dispatch('change');
327
+ return;
328
+ }
329
+ relinkCandidates = candidates;
330
+ relinkOpen = true;
331
+ }
332
+
333
+ function handleRelinkConfirm(e: CustomEvent<{ alias: string }>) {
334
+ if (!component) return;
335
+ // Find the peer whose parent alias matches the chosen one — its full
336
+ // block (split state + side values) becomes canonical.
337
+ const sourcePeer = relinkCandidates.find((c) => c.alias === e.detail.alias)?.variable ?? variable;
338
+ adoptBlockFromPeer(sourcePeer);
339
+ dispatch('change');
340
+ relinkOpen = false;
341
+ }
342
+
343
+ function handleRelinkCancel() {
344
+ relinkOpen = false;
345
+ }
346
+ </script>
347
+
348
+ {#if mode === 'sides'}
349
+ <!--
350
+ Sides mode renders into the parent token-row subgrid, NOT inside a
351
+ self-contained fieldset. TokenLayout puts the .token-label "padding" in
352
+ col 1; we fill cols 2-3 of the same row with the link/merge header, then
353
+ drop the four side rows onto row 2 spanning cols 1-3 (still subgridded so
354
+ each side's dropdown lands in the same trigger column as the surrounding
355
+ properties — border-width, corner-radius, etc.).
356
+
357
+ Both .padding-header and .padding-sides-block carry explicit grid-row so
358
+ auto-placement during the transition can't push them onto extra rows when
359
+ the leaving .padding-single-row briefly co-occupies cols 2-3 row 1; with
360
+ the explicit row anchors the OLD and NEW header content cross-fade in
361
+ the same cell instead of stacking.
362
+ -->
363
+ <div
364
+ class="padding-header"
365
+ in:fade|local={{ duration: t(220), delay: t(200) }}
366
+ out:fade|local={{ duration: t(180), easing: cubicIn }}
367
+ >
368
+ {#if showLinkUI}
369
+ <div class="link-toggle-wrap">
370
+ <UILinkToggle linked={isLinkedParent} on:toggle={toggleLinkPaddingGroup} />
371
+ {#if relinkOpen && component}
372
+ <UIRelinkConfirmPopover
373
+ candidates={relinkCandidates}
374
+ initialVariable={variable}
375
+ prefixToStrip={`--${component}-`}
376
+ on:confirm={handleRelinkConfirm}
377
+ on:cancel={handleRelinkCancel}
378
+ />
379
+ {/if}
380
+ </div>
381
+ {/if}
382
+ <button
383
+ type="button"
384
+ class="merge-btn"
385
+ on:click={mergeToSingle}
386
+ title="Use the same value for all sides"
387
+ disabled={disabled || selectionsLocked}
388
+ >
389
+ <i class="fas fa-square" aria-hidden="true"></i>
390
+ <span>Merge</span>
391
+ </button>
392
+ </div>
393
+ <div
394
+ class="padding-sides-block"
395
+ class:linked={showLinkUI && isLinkedParent}
396
+ class:unlinked={showLinkUI && !isLinkedParent}
397
+ in:slide|local={{ duration: t(360), delay: t(200), easing: cubicOut }}
398
+ out:slide|local={{ duration: t(320), easing: cubicInOut }}
399
+ >
400
+ {#each SIDES as s}
401
+ <span class="side-label">{s}</span>
402
+ <UITokenSelector
403
+ variable={sideVar(s)}
404
+ {component}
405
+ canBeLinked={false}
406
+ {disabled}
407
+ {selectionsLocked}
408
+ on:reset={() => handleResetSide(s)}
409
+ on:var-change={handleVarChange}
410
+ >
411
+ <svelte:fragment slot="trigger-title">{sideLabels[s] || '—'}</svelte:fragment>
412
+ <svelte:fragment slot="trigger-meta">{sideResolved[s] || '—'}</svelte:fragment>
413
+
414
+ <svelte:fragment let:close>
415
+ <UIOptionList>
416
+ {#each options as opt}
417
+ <UIOptionItem
418
+ active={sideActiveKey[s] === opt.key}
419
+ on:click={() => selectSide(s, opt.key, close)}
420
+ >
421
+ <svelte:fragment slot="label">{opt.label}</svelte:fragment>
422
+ <svelte:fragment slot="meta">{opt.size}</svelte:fragment>
423
+ </UIOptionItem>
424
+ {/each}
425
+ </UIOptionList>
426
+ </svelte:fragment>
427
+ </UITokenSelector>
428
+ {/each}
429
+ </div>
430
+ {:else}
431
+ <!--
432
+ Parent fade carries the row's overall opacity (and through it, the
433
+ nested UITokenSelector trigger's apparent opacity, since opacity
434
+ cascades through the stacking context). The split-button's `fly`
435
+ sets `opacity: 1` to disable fly's built-in fade so we don't
436
+ double-fade it: the parent already handles opacity, fly handles
437
+ only the right-ward translate. Result: as the user clicks split,
438
+ the trigger fades in place while the grid icon glides to the right
439
+ and disappears in lockstep.
440
+ -->
441
+ <div
442
+ class="padding-single-row"
443
+ class:disabled
444
+ in:fade|local={{ duration: t(220), delay: t(320) }}
445
+ out:fade|local={{ duration: t(200), easing: cubicIn }}
446
+ >
447
+ <UITokenSelector
448
+ {variable}
449
+ {component}
450
+ {canBeLinked}
451
+ {disabled}
452
+ {selectionsLocked}
453
+ on:reset={handleResetAll}
454
+ on:var-change={handleVarChange}
455
+ >
456
+ <svelte:fragment slot="trigger-title">{activeLabel || '—'}</svelte:fragment>
457
+
458
+ <svelte:fragment let:close>
459
+ <UIOptionList>
460
+ {#each options as opt}
461
+ <UIOptionItem
462
+ active={activeKey === opt.key}
463
+ on:click={() => selectSingle(opt.key, close)}
464
+ >
465
+ <svelte:fragment slot="label">{opt.label}</svelte:fragment>
466
+ <svelte:fragment slot="meta">{opt.size}</svelte:fragment>
467
+ </UIOptionItem>
468
+ {/each}
469
+ </UIOptionList>
470
+ </svelte:fragment>
471
+ </UITokenSelector>
472
+ {#if splittable}
473
+ <button
474
+ type="button"
475
+ class="split-btn"
476
+ on:click={splitToSides}
477
+ title="Set each side independently"
478
+ disabled={disabled || selectionsLocked}
479
+ in:fly|local={{ x: 24, opacity: 1, duration: t(220), delay: t(320), easing: cubicOut }}
480
+ out:fly|local={{ x: 24, opacity: 1, duration: t(200), easing: cubicIn }}
481
+ >
482
+ <i class="fas fa-border-all" aria-hidden="true"></i>
483
+ </button>
484
+ {/if}
485
+ </div>
486
+ {/if}
487
+
488
+ <style>
489
+ /* Single-mode padding row: trigger + split-toggle, sitting side-by-side.
490
+ The row spans grid columns from the trigger slot onward —
491
+ `--padding-row-start` defaults to column 2 (matches the per-variant
492
+ `[label][trigger][value]` grid) and is overridden to 1 by callers whose
493
+ grid skips the label column (the linked-block layout). The inner
494
+ UITokenSelector switches from its default subgrid to inline-flex so it
495
+ occupies natural width inside this flex row instead of stealing both
496
+ parent columns via `grid-column: span 2`. */
497
+ .padding-single-row {
498
+ display: flex;
499
+ align-items: center;
500
+ gap: var(--ui-space-6);
501
+ grid-column: var(--padding-row-start, 2) / -1;
502
+ /* Pin to row 1 so during a sides→single transition the leaving header
503
+ and the entering single row co-occupy the same cell instead of
504
+ stacking on adjacent grid rows. */
505
+ grid-row: 1;
506
+ min-width: 0;
507
+ }
508
+ .padding-single-row :global(.ui-token-selector) {
509
+ display: inline-flex;
510
+ align-items: center;
511
+ gap: var(--ui-space-8);
512
+ grid-column: auto;
513
+ flex: 0 0 auto;
514
+ }
515
+ /* Pin the trigger to the same width as the surrounding selectors' trigger
516
+ column (`--token-selector-w` on the parent .token-grid, 8rem default).
517
+ Without this, a short token label like "XS" collapses the inline-flex
518
+ trigger to natural width and breaks the column alignment with the
519
+ border-width/corner-radius/etc. rows above and below. */
520
+ .padding-single-row :global(.ui-ts-trigger-wrap) {
521
+ min-width: var(--token-selector-w, 8rem);
522
+ }
523
+
524
+ .split-btn {
525
+ display: inline-flex;
526
+ align-items: center;
527
+ justify-content: center;
528
+ width: 1.75rem;
529
+ height: 1.75rem;
530
+ padding: 0;
531
+ background: var(--ui-surface-low);
532
+ border: 1px solid var(--ui-border-default);
533
+ border-radius: var(--ui-radius-sm);
534
+ color: var(--ui-text-secondary);
535
+ font-size: var(--ui-font-size-sm);
536
+ cursor: pointer;
537
+ transition: all var(--ui-transition-fast);
538
+ flex-shrink: 0;
539
+ }
540
+
541
+ .split-btn:hover:not(:disabled) {
542
+ border-color: var(--ui-border-strong);
543
+ background: var(--ui-surface-high);
544
+ color: var(--ui-text-primary);
545
+ }
546
+
547
+ .split-btn:disabled {
548
+ cursor: not-allowed;
549
+ }
550
+
551
+ /* Header row: link-toggle + Merge button, sharing row 1 with the parent's
552
+ .token-label "padding". Cols 2-3 of the parent subgrid by default; the
553
+ left col stays free for the label TokenLayout already rendered. The
554
+ --padding-row-start hook lets a host (the linked-block, which renders
555
+ the property name as an h4 instead of an in-row label) re-anchor the
556
+ header to col 1 so it spans the full row width. */
557
+ .padding-header {
558
+ grid-column: var(--padding-row-start, 2) / -1;
559
+ grid-row: 1;
560
+ display: flex;
561
+ justify-content: flex-start;
562
+ align-items: center;
563
+ gap: var(--ui-space-6);
564
+ min-width: 0;
565
+ }
566
+
567
+ .link-toggle-wrap {
568
+ position: relative;
569
+ }
570
+
571
+ .merge-btn {
572
+ display: inline-flex;
573
+ align-items: center;
574
+ gap: var(--ui-space-6);
575
+ padding: var(--ui-space-4) var(--ui-space-8);
576
+ height: 1.5rem;
577
+ background: none;
578
+ border: 1px solid var(--ui-border-subtle);
579
+ border-radius: var(--ui-radius-sm);
580
+ color: var(--ui-text-muted);
581
+ font-family: inherit;
582
+ font-size: var(--ui-font-size-sm);
583
+ cursor: pointer;
584
+ transition: all var(--ui-transition-fast);
585
+ }
586
+
587
+ .merge-btn:hover:not(:disabled) {
588
+ background: var(--ui-hover);
589
+ color: var(--ui-text-primary);
590
+ border-color: var(--ui-border-default);
591
+ }
592
+
593
+ .merge-btn:disabled {
594
+ cursor: not-allowed;
595
+ }
596
+
597
+ /* Sides block: spans all three parent columns on its own row, then
598
+ subgrids back to those same columns so each side's label, dropdown, and
599
+ resolved value land in the same tracks as the surrounding properties.
600
+ Position: relative anchors the link-state pop-bar (::before) at the
601
+ left edge of col 1; side labels carry padding-left so they clear the
602
+ bar and read as visually indented under "padding". */
603
+ .padding-sides-block {
604
+ grid-column: 1 / -1;
605
+ grid-row: 2;
606
+ display: grid;
607
+ grid-template-columns: subgrid;
608
+ row-gap: var(--ui-space-6);
609
+ align-items: center;
610
+ margin-top: var(--ui-space-6);
611
+ position: relative;
612
+ }
613
+
614
+ /* Group-scale link-state pop-bar. The bar runs the full height of the
615
+ sides block in both states — the four sides are conceptually one
616
+ property, so the indicator never breaks into a tick. Linked = thick
617
+ teal stripe; unlinked = thinner amber stripe, drifted slightly left
618
+ to mirror the dropdown's "trigger pulls away from indicator" cue. */
619
+ .padding-sides-block.linked::before,
620
+ .padding-sides-block.unlinked::before {
621
+ content: "";
622
+ position: absolute;
623
+ top: 50%;
624
+ left: 0;
625
+ height: 100%;
626
+ transform: translateY(-50%);
627
+ pointer-events: none;
628
+ background: var(--ui-text-primary);
629
+ border-radius: var(--ui-radius-md);
630
+ transition:
631
+ width 220ms cubic-bezier(0.4, 0, 0.2, 1),
632
+ border-radius 220ms cubic-bezier(0.4, 0, 0.2, 1),
633
+ background-color 220ms cubic-bezier(0.4, 0, 0.2, 1),
634
+ left 220ms cubic-bezier(0.4, 0, 0.2, 1);
635
+ }
636
+ .padding-sides-block.linked::before {
637
+ width: 4px;
638
+ }
639
+ .padding-sides-block.unlinked::before {
640
+ background: var(--ui-link-broken);
641
+ width: 2px;
642
+ border-radius: 1px;
643
+ left: -0.25rem;
644
+ }
645
+ @media (prefers-reduced-motion: reduce) {
646
+ .padding-sides-block::before { transition: none; }
647
+ }
648
+
649
+ .side-label {
650
+ grid-column: 1;
651
+ font-size: var(--ui-font-size-sm);
652
+ color: var(--ui-text-secondary);
653
+ text-align: left;
654
+ line-height: 1;
655
+ /* Reserve gutter for the link-state bar so labels don't jostle when the
656
+ indicator changes shape. Indents the side names slightly under
657
+ "padding" — matching the sketch where top/right/bottom/left sit one
658
+ step in from the parent label. */
659
+ padding-left: 0.75rem;
660
+ }
661
+ </style>