@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,509 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy, createEventDispatcher } from 'svelte';
3
+ import { setCssVar, removeCssVar, CSS_VAR_CHANGE_EVENT } from '../lib/cssVarSync';
4
+ import type { CssVarRef } from '../lib/editorTypes';
5
+ import {
6
+ editorState,
7
+ setComponentAlias,
8
+ clearComponentAlias,
9
+ setComponentAliasLinked,
10
+ clearComponentAliasLinked,
11
+ isComponentPropertyLinked,
12
+ unlinkComponentProperty,
13
+ relinkComponentProperty,
14
+ getComponentPropertySiblings,
15
+ } from '../lib/editorStore';
16
+ import UILinkToggle from './UILinkToggle.svelte';
17
+ import UIRelinkConfirmPopover from './UIRelinkConfirmPopover.svelte';
18
+ import { keepInViewport } from './keepInViewport';
19
+
20
+ const dispatch = createEventDispatcher<{
21
+ change: void;
22
+ reset: void;
23
+ open: void;
24
+ close: void;
25
+ 'var-change': void;
26
+ }>();
27
+
28
+ export let variable: string;
29
+ /** When set, writes persist through the editor store under this component. */
30
+ export let component: string | undefined = undefined;
31
+ /** When true, render a link toggle that lets the user share this value across all sibling variants. */
32
+ export let canBeLinked: boolean = false;
33
+ /** Minimum width of the dropdown panel. */
34
+ export let dropdownMinWidth: string = '14rem';
35
+ /** Max width of the dropdown panel (useful for grids). */
36
+ export let dropdownMaxWidth: string = '';
37
+ /** When true, the default dropdown header (variable name + reset) is omitted. */
38
+ export let hideDefaultHeader: boolean = false;
39
+ /** When true, the trigger becomes non-interactive and visually dimmed. */
40
+ export let disabled: boolean = false;
41
+ /** When true, the trigger opens normally but the dropdown's selection area is
42
+ * dimmed and non-interactive. The lock toggle in the header stays active so
43
+ * the user can re-engage editing by re-linking. Used by the linked block to
44
+ * make the row openable even when currently unshared. */
45
+ export let selectionsLocked: boolean = false;
46
+
47
+ let open = false;
48
+ let container: HTMLElement;
49
+ let relinkOpen = false;
50
+ let relinkCandidates: { variable: string; alias: string }[] = [];
51
+
52
+ $: isLinkedFromData = canBeLinked && component && $editorState
53
+ ? isComponentPropertyLinked(component, variable)
54
+ : false;
55
+ $: isLinkedDisplay = canBeLinked && !!component && isLinkedFromData;
56
+ $: peerCount = canBeLinked && component && $editorState
57
+ ? getComponentPropertySiblings(component, variable).length
58
+ : 0;
59
+ $: hasSiblings = peerCount >= 2;
60
+ $: showLinkToggle = canBeLinked && !!component && hasSiblings;
61
+
62
+ /** Persist a semantic CSS-var reference (or clear it when null). */
63
+ export function writeOverride(semanticName: string | null): void {
64
+ if (component) {
65
+ const useLinked = isLinkedDisplay;
66
+ if (semanticName) {
67
+ // Mirror splitAliasesAndConfig: a `--…` reference becomes a token
68
+ // (rendered as `var(name)`); anything else (color-mix expressions,
69
+ // `transparent`, gradient tokens already wrapped) is a literal whose
70
+ // value is emitted as-is. Storing complex CSS as a token would render
71
+ // `var(color-mix(...))`, which is invalid and breaks the preview.
72
+ const ref: CssVarRef = semanticName.startsWith('--')
73
+ ? { kind: 'token', name: semanticName }
74
+ : { kind: 'literal', value: semanticName };
75
+ if (useLinked) setComponentAliasLinked(component, variable, ref);
76
+ else setComponentAlias(component, variable, ref);
77
+ } else {
78
+ if (useLinked) clearComponentAliasLinked(component, variable);
79
+ else clearComponentAlias(component, variable);
80
+ }
81
+ return;
82
+ }
83
+ if (semanticName) {
84
+ setCssVar(variable, semanticName.startsWith('--') ? `var(${semanticName})` : semanticName);
85
+ } else {
86
+ removeCssVar(variable);
87
+ }
88
+ }
89
+
90
+ export function close() {
91
+ if (!open) return;
92
+ open = false;
93
+ dispatch('close');
94
+ }
95
+
96
+ function toggle() {
97
+ if (disabled) return;
98
+ open = !open;
99
+ dispatch(open ? 'open' : 'close');
100
+ }
101
+
102
+ $: if (disabled && open) close();
103
+
104
+ function toggleLinked() {
105
+ if (!canBeLinked || !component) return;
106
+ if (isLinkedDisplay) {
107
+ unlinkComponentProperty(component, variable);
108
+ dispatch('change');
109
+ return;
110
+ }
111
+ const slice = $editorState.components[component];
112
+ if (!slice) return;
113
+ const siblings = getComponentPropertySiblings(component, variable);
114
+ if (siblings.length < 2) return;
115
+
116
+ // Re-linking this property: figure out the value to adopt by inspecting the
117
+ // siblings *currently linked* (those not in `unlinked`, excluding the target).
118
+ // The target itself is rejoining, so its own value isn't a candidate unless
119
+ // every other sibling has also detached.
120
+ const linkedSiblings = siblings.filter(
121
+ (v) => v !== variable && !slice.unlinked?.includes(v),
122
+ );
123
+
124
+ if (linkedSiblings.length === 0) {
125
+ // No remaining linked siblings — promote this property's current value.
126
+ const currentValue = slice.aliases[variable];
127
+ if (currentValue) {
128
+ setComponentAliasLinked(component, variable, currentValue);
129
+ dispatch('change');
130
+ }
131
+ return;
132
+ }
133
+
134
+ const candidates = linkedSiblings.map((v) => {
135
+ const ref = slice.aliases[v];
136
+ const alias = ref?.kind === 'token' ? ref.name : '';
137
+ return { variable: v, alias };
138
+ });
139
+ const definedCandidates = candidates.filter((c) => c.alias);
140
+ const distinctValues = new Set(definedCandidates.map((c) => c.alias));
141
+
142
+ if (distinctValues.size <= 1) {
143
+ // ≤1 explicit alias among peers: adopt it; if none of them have one,
144
+ // promote this property's current value so the lock takes effect even
145
+ // when peers are still at their declared defaults. If no alias exists
146
+ // anywhere in the group, rejoin as pure metadata (the group is linked
147
+ // at its upstream default and there's nothing to write).
148
+ const adoptRef = definedCandidates.length > 0
149
+ ? slice.aliases[definedCandidates[0].variable]
150
+ : slice.aliases[variable];
151
+ if (adoptRef) setComponentAliasLinked(component, variable, adoptRef);
152
+ else relinkComponentProperty(component, variable);
153
+ dispatch('change');
154
+ return;
155
+ }
156
+
157
+ relinkCandidates = candidates;
158
+ relinkOpen = true;
159
+ }
160
+
161
+ function handleRelinkConfirm(e: CustomEvent<{ alias: string }>) {
162
+ if (!component) return;
163
+ setComponentAliasLinked(component, variable, { kind: 'token', name: e.detail.alias });
164
+ dispatch('change');
165
+ relinkOpen = false;
166
+ }
167
+
168
+ function handleRelinkCancel() {
169
+ relinkOpen = false;
170
+ }
171
+
172
+ function handleReset() {
173
+ // Order matters: notify children of reset first so they can clear local
174
+ // state, THEN clear the override. writeOverride fires CSS_VAR_CHANGE_EVENT
175
+ // synchronously, which triggers `var-change` on children — that's where
176
+ // each selector should re-derive its display state from the new default.
177
+ //
178
+ // Linked properties: writeOverride(null) routes through
179
+ // `clearComponentAliasLinked`, which clears the shared override on every
180
+ // linked peer. That's the natural meaning of "clear" on a linked group —
181
+ // peers all return to the upstream default together, the link stays.
182
+ // Per-peer resets while preserving the link are impossible by definition
183
+ // (linked = peers share one value); a "reset just this one" intent is
184
+ // really "unlink, then reset," which the user does in two visible steps.
185
+ dispatch('reset');
186
+ writeOverride(null);
187
+ close();
188
+ dispatch('change');
189
+ }
190
+
191
+ function handleClickOutside(e: MouseEvent) {
192
+ if (container && !container.contains(e.target as Node)) {
193
+ close();
194
+ relinkOpen = false;
195
+ }
196
+ }
197
+
198
+ function handleVarChange(e: Event) {
199
+ const detail = (e as CustomEvent<{ name: string }>).detail;
200
+ if (detail?.name === variable) dispatch('var-change');
201
+ }
202
+
203
+ onMount(() => {
204
+ document.addEventListener('click', handleClickOutside, true);
205
+ document.addEventListener(CSS_VAR_CHANGE_EVENT, handleVarChange);
206
+ });
207
+
208
+ onDestroy(() => {
209
+ document.removeEventListener('click', handleClickOutside, true);
210
+ document.removeEventListener(CSS_VAR_CHANGE_EVENT, handleVarChange);
211
+ });
212
+ </script>
213
+
214
+ <div class="ui-token-selector" class:disabled bind:this={container}>
215
+ <div class="ui-ts-trigger-wrap">
216
+ <button
217
+ class="ui-ts-trigger"
218
+ class:linked={isLinkedDisplay}
219
+ class:unlinked={showLinkToggle && !isLinkedDisplay}
220
+ on:click={toggle}
221
+ {disabled}
222
+ >
223
+ <div class="ui-ts-content">
224
+ {#if $$slots['trigger-preview']}
225
+ <div class="ui-ts-preview">
226
+ <slot name="trigger-preview" />
227
+ </div>
228
+ {/if}
229
+ <div class="ui-ts-text">
230
+ <slot name="trigger-text">
231
+ {#if $$slots['trigger-title']}
232
+ <span class="ui-ts-category"><slot name="trigger-title" /></span>
233
+ {/if}
234
+ </slot>
235
+ </div>
236
+ </div>
237
+ <i class="fas fa-chevron-down ui-ts-chevron" class:open></i>
238
+ </button>
239
+
240
+ {#if relinkOpen && component}
241
+ <UIRelinkConfirmPopover
242
+ candidates={relinkCandidates}
243
+ initialVariable={variable}
244
+ prefixToStrip={`--${component}-`}
245
+ on:confirm={handleRelinkConfirm}
246
+ on:cancel={handleRelinkCancel}
247
+ />
248
+ {/if}
249
+
250
+ {#if open}
251
+ <div
252
+ class="ui-ts-dropdown"
253
+ style="min-width: {dropdownMinWidth};{dropdownMaxWidth ? ` max-width: ${dropdownMaxWidth};` : ''}"
254
+ use:keepInViewport
255
+ >
256
+ {#if !hideDefaultHeader}
257
+ <slot name="header">
258
+ <div class="ui-ts-header">
259
+ {#if showLinkToggle}
260
+ <UILinkToggle linked={isLinkedDisplay} on:toggle={toggleLinked} />
261
+ {/if}
262
+ <button
263
+ type="button"
264
+ class="ui-ts-reset"
265
+ on:click={handleReset}
266
+ disabled={selectionsLocked}
267
+ title={selectionsLocked ? 'Unlock to reset' : 'Reset to default'}
268
+ >
269
+ <i class="fas fa-undo" aria-hidden="true"></i>
270
+ <span>Reset</span>
271
+ </button>
272
+ </div>
273
+ </slot>
274
+ {/if}
275
+ <div class="ui-ts-selections" class:locked={selectionsLocked}>
276
+ <slot name="subheader" />
277
+ <slot {close} {handleReset} />
278
+ </div>
279
+ </div>
280
+ {/if}
281
+ </div>
282
+
283
+ {#if $$slots['trigger-meta']}
284
+ <span class="ui-ts-meta-text"><slot name="trigger-meta" /></span>
285
+ {/if}
286
+ </div>
287
+
288
+ <style>
289
+ /* Subgrid spanning the parent's trigger + meta columns. */
290
+ .ui-token-selector {
291
+ display: grid;
292
+ grid-template-columns: subgrid;
293
+ grid-column: span 2;
294
+ align-items: stretch;
295
+ column-gap: var(--ui-space-8);
296
+ }
297
+
298
+ .ui-ts-trigger-wrap {
299
+ position: relative;
300
+ min-width: 0;
301
+ justify-self: stretch;
302
+ }
303
+
304
+ .ui-token-selector.disabled {
305
+ opacity: 0.4;
306
+ }
307
+
308
+ .ui-token-selector.disabled .ui-ts-trigger {
309
+ cursor: not-allowed;
310
+ }
311
+
312
+ .ui-token-selector.disabled .ui-ts-trigger:hover {
313
+ border-color: var(--ui-border-default);
314
+ background: var(--ui-surface-low);
315
+ }
316
+
317
+ .ui-ts-trigger {
318
+ display: flex;
319
+ align-items: center;
320
+ gap: var(--ui-space-6);
321
+ padding: var(--ui-space-2) var(--ui-space-8);
322
+ background: var(--ui-surface-low);
323
+ border: 1px solid var(--ui-border-default);
324
+ border-radius: var(--ui-radius-md);
325
+ cursor: pointer;
326
+ transition: all var(--ui-transition-fast);
327
+ min-height: 1.75rem;
328
+ width: 100%;
329
+ }
330
+
331
+ .ui-ts-content {
332
+ display: flex;
333
+ flex: 1;
334
+ min-width: 0;
335
+ align-items: center;
336
+ justify-content: flex-start;
337
+ gap: var(--ui-space-6);
338
+ overflow: hidden;
339
+ align-self: stretch;
340
+ }
341
+
342
+ .ui-ts-text {
343
+ display: flex;
344
+ flex-direction: column;
345
+ gap: 1px;
346
+ flex: 0 1 auto;
347
+ text-align: left;
348
+ align-items: flex-start;
349
+ min-width: 0;
350
+ }
351
+
352
+ .ui-ts-text:has(.ui-ts-category:empty) {
353
+ display: none;
354
+ }
355
+
356
+ .ui-ts-category {
357
+ font-size: var(--ui-font-size-sm);
358
+ color: var(--ui-text-primary);
359
+ font-weight: var(--ui-font-weight-medium);
360
+ text-align: left;
361
+ width: 100%;
362
+ white-space: nowrap;
363
+ overflow: hidden;
364
+ text-overflow: ellipsis;
365
+ }
366
+
367
+ .ui-ts-trigger:hover {
368
+ border-color: var(--ui-border-strong);
369
+ background: var(--ui-surface-high);
370
+ }
371
+
372
+ /* Link-state pop-bar. The wrap already has `position: relative` (so the
373
+ dropdown can anchor). The bar is a `::before` anchored at the wrap's
374
+ left edge — same column position regardless of state, so a stack of
375
+ mixed linked/unlinked rows shares one continuous link-state column.
376
+ Linked rows: bar is teal, full size, sitting flush against the trigger.
377
+ Unlinked rows: bar shrinks to a smaller amber tick and the trigger is
378
+ indented (`padding-left` on the wrap) so the bar sits clear of it. */
379
+ .ui-ts-trigger-wrap {
380
+ transition: padding-left 320ms cubic-bezier(0.5, 1.6, 0.5, 1);
381
+ }
382
+ .ui-ts-trigger-wrap:has(> .ui-ts-trigger.linked, > .ui-ts-trigger.unlinked)::before {
383
+ content: "";
384
+ position: absolute;
385
+ top: 50%;
386
+ left: 0;
387
+ width: 4px;
388
+ height: 1.75rem;
389
+ border-radius: var(--ui-radius-md) 0 0 var(--ui-radius-md);
390
+ background: var(--ui-text-primary);
391
+ transform: translateY(-50%);
392
+ pointer-events: none;
393
+ transition:
394
+ width 220ms cubic-bezier(0.4, 0, 0.2, 1),
395
+ height 220ms cubic-bezier(0.4, 0, 0.2, 1),
396
+ border-radius 220ms cubic-bezier(0.4, 0, 0.2, 1),
397
+ background-color 220ms cubic-bezier(0.4, 0, 0.2, 1);
398
+ }
399
+ .ui-ts-trigger-wrap:has(> .ui-ts-trigger.unlinked) {
400
+ padding-left: 0.5rem;
401
+ }
402
+ .ui-ts-trigger-wrap:has(> .ui-ts-trigger.unlinked)::before {
403
+ background: var(--ui-link-broken);
404
+ width: 2px;
405
+ height: 0.875rem;
406
+ border-radius: 1px;
407
+ }
408
+ /* Keep the dropdown's left edge aligned with the (indented) trigger rather
409
+ than the column anchor, so an open dropdown doesn't bleed left of its
410
+ associated control. */
411
+ .ui-ts-trigger-wrap:has(> .ui-ts-trigger.unlinked) > .ui-ts-dropdown {
412
+ left: 0.5rem;
413
+ }
414
+ @media (prefers-reduced-motion: reduce) {
415
+ .ui-ts-trigger-wrap,
416
+ .ui-ts-trigger-wrap::before { transition: none; }
417
+ }
418
+
419
+ .ui-ts-preview {
420
+ flex: 1;
421
+ align-self: stretch;
422
+ flex-shrink: 0;
423
+ display: flex;
424
+ align-items: center;
425
+ justify-content: center;
426
+ }
427
+
428
+ .ui-ts-preview:empty {
429
+ display: none;
430
+ }
431
+
432
+ .ui-ts-meta-text {
433
+ align-self: center;
434
+ color: var(--ui-text-tertiary);
435
+ font-family: var(--ui-font-mono);
436
+ font-size: var(--ui-font-size-sm);
437
+ overflow: hidden;
438
+ text-overflow: ellipsis;
439
+ white-space: nowrap;
440
+ min-width: 0;
441
+ }
442
+
443
+ .ui-ts-chevron {
444
+ font-size: 0.625rem;
445
+ color: var(--ui-text-secondary);
446
+ transition: transform var(--ui-transition-fast);
447
+ }
448
+
449
+ .ui-ts-chevron.open {
450
+ transform: rotate(180deg);
451
+ }
452
+
453
+ .ui-ts-dropdown {
454
+ position: absolute;
455
+ top: calc(100% + var(--ui-space-4));
456
+ left: 0;
457
+ background: var(--ui-surface-higher);
458
+ border: 1px solid var(--ui-border-medium);
459
+ border-radius: var(--ui-radius-md);
460
+ box-shadow: var(--ui-shadow-lg);
461
+ z-index: 10;
462
+ }
463
+
464
+ .ui-ts-header {
465
+ display: flex;
466
+ align-items: center;
467
+ justify-content: flex-end;
468
+ gap: var(--ui-space-6);
469
+ padding: var(--ui-space-6) var(--ui-space-8);
470
+ border-bottom: 1px solid var(--ui-border-faint);
471
+ }
472
+
473
+ .ui-ts-reset {
474
+ display: inline-flex;
475
+ align-items: center;
476
+ gap: var(--ui-space-6);
477
+ height: 1.5rem;
478
+ padding: var(--ui-space-2) var(--ui-space-8);
479
+ background: none;
480
+ border: 1px solid var(--ui-border-default);
481
+ border-radius: var(--ui-radius-sm);
482
+ color: var(--ui-text-secondary);
483
+ font-family: inherit;
484
+ font-size: var(--ui-font-size-sm);
485
+ cursor: pointer;
486
+ flex-shrink: 0;
487
+ transition: all var(--ui-transition-fast);
488
+ }
489
+
490
+ .ui-ts-reset i {
491
+ font-size: 0.625rem;
492
+ }
493
+
494
+ .ui-ts-reset:hover:not(:disabled) {
495
+ background: var(--ui-hover);
496
+ border-color: var(--ui-border-strong);
497
+ color: var(--ui-text-primary);
498
+ }
499
+
500
+ .ui-ts-reset:disabled {
501
+ opacity: 0.4;
502
+ cursor: not-allowed;
503
+ }
504
+
505
+ .ui-ts-selections.locked {
506
+ opacity: 0.4;
507
+ pointer-events: none;
508
+ }
509
+ </style>
@@ -0,0 +1,145 @@
1
+ <script lang="ts" generics="T extends { key: string; label?: string; value?: string }">
2
+ import { createEventDispatcher } from 'svelte';
3
+ import { resolveAliasChain } from '../lib/tokenRegistry';
4
+ import UITokenSelector from './UITokenSelector.svelte';
5
+ import UIOptionList from './UIOptionList.svelte';
6
+ import UIOptionItem from './UIOptionItem.svelte';
7
+
8
+ const dispatch = createEventDispatcher<{
9
+ change: void;
10
+ }>();
11
+
12
+ export let variable: string;
13
+ export let component: string | undefined = undefined;
14
+ export let canBeLinked: boolean = false;
15
+ export let disabled: boolean = false;
16
+ export let selectionsLocked: boolean = false;
17
+ export let dropdownMinWidth: string = '12rem';
18
+ export let dropdownMaxWidth: string = '';
19
+ /** Forwarded to UIOptionList — when set, options render in a linked-column grid. */
20
+ export let dropdownGridColumns: string = '';
21
+ /** CSS var prefix that, joined with an option `key`, forms the target var (e.g. `--font-weight-`). */
22
+ export let varPrefix: string;
23
+ /** Selectable options. Each must have a unique `key`. */
24
+ export let options: ReadonlyArray<T>;
25
+
26
+ let selector: UITokenSelector;
27
+ let chosenKey: string | null = null;
28
+ let currentValue: string = '';
29
+
30
+ $: validKeys = new Set(options.map((o) => o.key));
31
+ $: refMatcher = (() => {
32
+ const escaped = varPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
33
+ return new RegExp(`var\\((${escaped}[^)\\s]+)\\)`);
34
+ })();
35
+
36
+ function parseRef(value: string): string | null {
37
+ const m = value.match(refMatcher);
38
+ if (!m) return null;
39
+ const key = m[1].slice(varPrefix.length);
40
+ return validKeys.has(key) ? key : null;
41
+ }
42
+
43
+ function readResolved() {
44
+ currentValue = getComputedStyle(document.documentElement).getPropertyValue(variable).trim();
45
+ }
46
+
47
+ function initFromCurrent() {
48
+ readResolved();
49
+ const raw = document.documentElement.style.getPropertyValue(variable).trim();
50
+ if (raw) {
51
+ const key = parseRef(raw);
52
+ if (key) {
53
+ chosenKey = key;
54
+ return;
55
+ }
56
+ }
57
+ for (const alias of resolveAliasChain(variable)) {
58
+ const key = parseRef(`var(${alias})`);
59
+ if (key) {
60
+ chosenKey = key;
61
+ return;
62
+ }
63
+ }
64
+ // Last-ditch fallback: match the resolved value against an option's `value`
65
+ // (lets us highlight a variant even when the default is a literal, not an alias).
66
+ const matchedByValue = options.find((o) => o.value !== undefined && o.value === currentValue);
67
+ chosenKey = matchedByValue ? matchedByValue.key : null;
68
+ }
69
+
70
+ function handleReset() {
71
+ chosenKey = null;
72
+ readResolved();
73
+ dispatch('change');
74
+ }
75
+
76
+ function selectKey(key: string, close: () => void) {
77
+ const target = `${varPrefix}${key}`;
78
+ if (target === variable) {
79
+ selector.writeOverride(null);
80
+ chosenKey = null;
81
+ } else {
82
+ selector.writeOverride(target);
83
+ chosenKey = key;
84
+ }
85
+ readResolved();
86
+ close();
87
+ dispatch('change');
88
+ }
89
+
90
+ // Re-derive `chosenKey` when the bound `variable` changes (e.g. when a
91
+ // VariantGroup tabs view reuses the same selector across states). Without
92
+ // this, prop swaps leave the trigger label stale.
93
+ let lastSeenVariable: string | null = null;
94
+ $: if (variable !== lastSeenVariable) {
95
+ lastSeenVariable = variable;
96
+ initFromCurrent();
97
+ }
98
+
99
+ $: activeOption = (options.find((o) => o.key === chosenKey) ?? null) as T | null;
100
+ </script>
101
+
102
+ <UITokenSelector
103
+ bind:this={selector}
104
+ {variable}
105
+ {component}
106
+ {canBeLinked}
107
+ {disabled}
108
+ {selectionsLocked}
109
+ {dropdownMinWidth}
110
+ {dropdownMaxWidth}
111
+ on:reset={handleReset}
112
+ on:var-change={initFromCurrent}
113
+ >
114
+ <svelte:fragment slot="trigger-title">
115
+ <slot name="trigger-title" {activeOption}>{activeOption?.label ?? ''}</slot>
116
+ </svelte:fragment>
117
+ <svelte:fragment slot="trigger-meta">
118
+ <slot name="trigger-meta" {currentValue} {activeOption}>{currentValue || '—'}</slot>
119
+ </svelte:fragment>
120
+
121
+ <svelte:fragment let:close>
122
+ <UIOptionList gridColumns={dropdownGridColumns}>
123
+ {#each options as opt (opt.key)}
124
+ <slot
125
+ name="option"
126
+ {opt}
127
+ active={chosenKey === opt.key}
128
+ select={() => selectKey(opt.key, close)}
129
+ >
130
+ {#if opt.value !== undefined}
131
+ <UIOptionItem active={chosenKey === opt.key} on:click={() => selectKey(opt.key, close)}>
132
+ <svelte:fragment slot="label">{opt.label ?? ''}</svelte:fragment>
133
+ <svelte:fragment slot="meta">{opt.value}</svelte:fragment>
134
+ </UIOptionItem>
135
+ {:else}
136
+ <UIOptionItem active={chosenKey === opt.key} on:click={() => selectKey(opt.key, close)}>
137
+ <svelte:fragment slot="label">{opt.label ?? ''}</svelte:fragment>
138
+ </UIOptionItem>
139
+ {/if}
140
+ </slot>
141
+ {/each}
142
+ <slot name="extras" {close} />
143
+ </UIOptionList>
144
+ </svelte:fragment>
145
+ </UITokenSelector>