@motion-proto/live-tokens 0.1.1 → 0.3.1

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 +160 -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,56 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import UIRadioGroup from '../../ui/UIRadioGroup.svelte';
4
+ import UIPaletteSelector from '../../ui/UIPaletteSelector.svelte';
5
+ import { setCssVar } from '../../lib/cssVarSync';
6
+
7
+ type Mode = 'image' | 'color';
8
+ export let mode: Mode = 'image';
9
+ /** Editor-scoped CSS var the picker writes to (must end with `-surface` to allow gradients). */
10
+ export let colorVariable: string;
11
+
12
+ const options: ReadonlyArray<{ value: Mode; label: string }> = [
13
+ { value: 'image', label: 'Image' },
14
+ { value: 'color', label: 'Color' },
15
+ ];
16
+
17
+ // Editor-only backdrop vars aren't persisted to disk; seed surface-canvas as
18
+ // the default selection on first mount so the picker reads as surface-canvas
19
+ // and the color-mode preview matches.
20
+ onMount(() => {
21
+ if (!document.documentElement.style.getPropertyValue(colorVariable)) {
22
+ setCssVar(colorVariable, 'var(--surface-canvas)');
23
+ }
24
+ });
25
+ </script>
26
+
27
+ <label class="backdrop-config">
28
+ <span>Sample background</span>
29
+ <div class="backdrop-row">
30
+ <UIRadioGroup
31
+ bind:value={mode}
32
+ name="shadow-backdrop-mode-{Math.random().toString(36).slice(2, 8)}"
33
+ {options}
34
+ />
35
+ <div class="picker-slot">
36
+ <UIPaletteSelector variable={colorVariable} disabled={mode !== 'color'} />
37
+ </div>
38
+ </div>
39
+ </label>
40
+
41
+ <style>
42
+ .backdrop-row {
43
+ display: inline-flex;
44
+ align-items: center;
45
+ gap: var(--ui-space-16);
46
+ padding: var(--ui-space-4) var(--ui-space-8);
47
+ border: 1px solid var(--ui-border-faint);
48
+ border-radius: var(--ui-radius-sm);
49
+ }
50
+ .picker-slot {
51
+ min-width: 8rem;
52
+ }
53
+ .picker-slot :global(.ui-token-selector) {
54
+ width: 100%;
55
+ }
56
+ </style>
@@ -0,0 +1,115 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Shared inner block rendered inside a single state in VariantGroup.
4
+ *
5
+ * Both the tabs branch and the list branch of VariantGroup render the same
6
+ * `<TypeEditor>` (when a state has type groups) followed by `<TokenLayout>`,
7
+ * differing only in the surrounding chrome (preview placement, toggles,
8
+ * tab strip). This component owns the duplicated inner block so a per-state
9
+ * control change happens in exactly one place.
10
+ */
11
+ import TokenLayout from './TokenLayout.svelte';
12
+ import TypeEditor from './TypeEditor.svelte';
13
+ import type { Token, TypeGroupConfig } from './types';
14
+
15
+ /** Tokens for this state, fed to `<TokenLayout>`. */
16
+ export let tokens: Token[];
17
+ /** Type groups for this state; rendered as a row of `<TypeEditor>` blocks. */
18
+ export let typeGroups: TypeGroupConfig[] = [];
19
+ /** Forwarded to TypeEditor and TokenLayout so writes persist through the editor store. */
20
+ export let component: string | undefined = undefined;
21
+ /** Per-variable rank passed through to TokenLayout for linked-block alignment. */
22
+ export let linkedOrder: Map<string, number> | undefined = undefined;
23
+ /** Render the token grid with N visual columns. >1 spreads a long property
24
+ list horizontally; only meaningful for state-blocks without typeGroups
25
+ (the two-col flex layout already partitions screen real estate when
26
+ typeGroups are present). */
27
+ export let columns: number = 1;
28
+
29
+ $: hasTypeGroups = typeGroups.length > 0;
30
+ </script>
31
+
32
+ <div class="state-controls" class:two-col={hasTypeGroups}>
33
+ {#if hasTypeGroups}
34
+ <div class="state-type-groups">
35
+ {#each typeGroups as tg}
36
+ <TypeEditor
37
+ legend={tg.legend ?? 'type'}
38
+ colorVariable={tg.colorVariable}
39
+ colorLabel={tg.colorLabel ?? 'text color'}
40
+ familyVariable={tg.familyVariable}
41
+ familyLabel={tg.familyLabel ?? 'font family'}
42
+ sizeVariable={tg.sizeVariable}
43
+ sizeLabel={tg.sizeLabel ?? 'font size'}
44
+ weightVariable={tg.weightVariable}
45
+ weightLabel={tg.weightLabel ?? 'font weight'}
46
+ lineHeightVariable={tg.lineHeightVariable}
47
+ lineHeightLabel={tg.lineHeightLabel ?? 'line height'}
48
+ outlineWidthVariable={tg.outlineWidthVariable}
49
+ outlineWidthLabel={tg.outlineWidthLabel ?? 'outline thickness'}
50
+ outlineColorVariable={tg.outlineColorVariable}
51
+ outlineColorLabel={tg.outlineColorLabel ?? 'outline color'}
52
+ {component}
53
+ on:change
54
+ />
55
+ {/each}
56
+ </div>
57
+ {/if}
58
+ <TokenLayout
59
+ title=""
60
+ {tokens}
61
+ {component}
62
+ {linkedOrder}
63
+ {columns}
64
+ on:change
65
+ />
66
+ </div>
67
+
68
+ <style>
69
+ .state-controls {
70
+ display: grid;
71
+ grid-template-columns: 1fr;
72
+ gap: var(--ui-space-12);
73
+ align-items: start;
74
+ margin-top: var(--ui-space-4);
75
+ }
76
+
77
+ .state-controls.two-col {
78
+ display: flex;
79
+ flex-wrap: wrap;
80
+ gap: var(--ui-space-16) var(--ui-space-16);
81
+ align-items: flex-start;
82
+ justify-content: flex-start;
83
+ }
84
+
85
+ .state-type-groups {
86
+ display: flex;
87
+ flex-direction: row;
88
+ flex-wrap: wrap;
89
+ gap: var(--ui-space-16);
90
+ align-items: flex-start;
91
+ }
92
+
93
+ /* Inside a state's two-col layout the fieldset frame is redundant with the
94
+ surrounding state card. Flatten the border/padding but keep the legend so
95
+ each block ("title", "body text", …) is identifiable. */
96
+ .state-controls.two-col .state-type-groups :global(.fieldset-wrapper) {
97
+ border: none;
98
+ padding: 0;
99
+ }
100
+
101
+ .state-controls.two-col .state-type-groups :global(.fieldset-wrapper.active) {
102
+ outline: none;
103
+ }
104
+
105
+ .state-controls.two-col .state-type-groups :global(.fieldset-legend) {
106
+ padding: 0 var(--ui-space-4) var(--ui-space-4);
107
+ }
108
+
109
+ /* The general-properties column has no legend of its own; pad it down by
110
+ one legend-line so its first row aligns with the first row of the
111
+ adjacent type-group. */
112
+ .state-controls.two-col > :global(.token-group) {
113
+ padding-top: calc(var(--ui-font-size-xs) + var(--ui-space-4));
114
+ }
115
+ </style>
@@ -0,0 +1,511 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher, type ComponentType } from 'svelte';
3
+ import UIPaletteSelector from '../../ui/UIPaletteSelector.svelte';
4
+ import UIVariantSelector from '../../ui/UIVariantSelector.svelte';
5
+ import UIFontFamilySelector from '../../ui/UIFontFamilySelector.svelte';
6
+ import UIFontWeightSelector from '../../ui/UIFontWeightSelector.svelte';
7
+ import UIFontSizeSelector from '../../ui/UIFontSizeSelector.svelte';
8
+ import UILineHeightSelector from '../../ui/UILineHeightSelector.svelte';
9
+ import UIPaddingSelector from '../../ui/UIPaddingSelector.svelte';
10
+ import { BLUR, BORDER_WIDTH, DOT_SIZE, RADIUS, SHADOW, DIVIDER_HEIGHT } from '../../ui/variantScales';
11
+ import {
12
+ editorState,
13
+ getComponentPropertySiblings,
14
+ setComponentAliasLinked,
15
+ clearComponentAliasLinked,
16
+ } from '../../lib/editorStore';
17
+ import { getEditorContext } from './editorContext';
18
+ import type { Token } from './types';
19
+
20
+ /** Selector kind. `padding-split` is `padding` whose per-side variables exist;
21
+ it renders the four-sided field group instead of the single-value row. */
22
+ type Kind =
23
+ | 'surface'
24
+ | 'border'
25
+ | 'border-width'
26
+ | 'radius'
27
+ | 'divider-width'
28
+ | 'divider-height'
29
+ | 'dot-size'
30
+ | 'blur'
31
+ | 'shadow'
32
+ | 'font-family'
33
+ | 'font-weight'
34
+ | 'font-size'
35
+ | 'line-height'
36
+ | 'padding'
37
+ | 'padding-split'
38
+ | 'gap'
39
+ | 'extras';
40
+
41
+ type Entry = { kind: Kind; token: Token };
42
+
43
+ export let title: string = '';
44
+ export let tokens: Token[];
45
+ /** Forwarded to each selector; when set, writes persist through the editor store. */
46
+ export let component: string | undefined = undefined;
47
+ /** Optional context labels per variable (shown below the selector). */
48
+ export let contexts: Record<string, string[]> = {};
49
+ /** Per-variable rank that overrides kind rank when sorting; lets linked tokens align with the top linked row. */
50
+ export let linkedOrder: Map<string, number> | undefined = undefined;
51
+ /** Set true on the linked-block instance so dimmed variant rows can scroll/flash to the matching anchor. */
52
+ export let isLinkedBlock: boolean = false;
53
+ /** Number of visual columns. >1 switches to a column-major grid (grid-auto-flow: column)
54
+ so the consumer can spread a long property list across the available width. In
55
+ multi-col mode, the linked-first sort + zone divider are dropped — kind-grouped flow
56
+ reads more naturally when columns themselves carry the visual grouping. */
57
+ export let columns: number = 1;
58
+
59
+ /** Suffix/prefix patterns mapped to kinds — single source of truth used by `categorize`.
60
+ Order matters: `text` must run before `border`/`surface` because `--text-*` would
61
+ otherwise match `surface` checks if any pattern overlapped. */
62
+ const KIND_PATTERNS: Array<{ kind: Kind; matches: (v: string) => boolean }> = [
63
+ { kind: 'font-family', matches: (v) => v.endsWith('-font-family') },
64
+ { kind: 'font-weight', matches: (v) => v.endsWith('-font-weight') },
65
+ { kind: 'font-size', matches: (v) => v.endsWith('-font-size') || v.endsWith('-icon-size') },
66
+ { kind: 'line-height', matches: (v) => v.endsWith('-line-height') },
67
+ { kind: 'extras', matches: (v) => v.endsWith('-text') || v.startsWith('--text-') },
68
+ { kind: 'radius', matches: (v) => v.endsWith('-radius') || v.startsWith('--radius-') },
69
+ { kind: 'divider-width', matches: (v) => v.endsWith('-divider-width') || v.endsWith('-divider-thickness') },
70
+ { kind: 'divider-height', matches: (v) => v.endsWith('-divider-height') || v.endsWith('-track-height') },
71
+ { kind: 'dot-size', matches: (v) => v.endsWith('-dot-size') },
72
+ { kind: 'blur', matches: (v) => v.endsWith('-blur') || v.startsWith('--blur-') },
73
+ { kind: 'shadow', matches: (v) => v.endsWith('-shadow') || v.startsWith('--shadow-') },
74
+ { kind: 'padding', matches: (v) => v.endsWith('-padding') || v.endsWith('-margin') },
75
+ { kind: 'gap', matches: (v) => v.endsWith('-gap') },
76
+ { kind: 'border-width', matches: (v) => v.endsWith('-border-width') || v.startsWith('--border-width-') },
77
+ { kind: 'border', matches: (v) => v.endsWith('-border') || v.startsWith('--border-') },
78
+ { kind: 'surface', matches: (v) => v.endsWith('-surface') || v.startsWith('--surface-') },
79
+ ];
80
+
81
+ /** Fixed internal order for tokens within a layout. `padding-split` co-orders with `padding`. */
82
+ const baseKindOrder: Kind[] = [
83
+ 'font-family',
84
+ 'font-weight',
85
+ 'font-size',
86
+ 'line-height',
87
+ 'divider-width',
88
+ 'divider-height',
89
+ 'dot-size',
90
+ 'radius',
91
+ 'padding',
92
+ 'padding-split',
93
+ 'gap',
94
+ 'blur',
95
+ 'shadow',
96
+ 'extras',
97
+ 'surface',
98
+ 'border-width',
99
+ 'border',
100
+ ];
101
+ const orderRank: Record<Kind, number> = Object.fromEntries(
102
+ baseKindOrder.map((k, i) => [k, i]),
103
+ ) as Record<Kind, number>;
104
+
105
+ function rawKind(v: string): Kind {
106
+ for (const { kind, matches } of KIND_PATTERNS) {
107
+ if (matches(v)) return kind;
108
+ }
109
+ return 'extras';
110
+ }
111
+
112
+ /** A padding token is "split" when its per-side variables exist for this component. */
113
+ function paddingIsSplit(varName: string, comp: string | undefined, state: typeof $editorState): boolean {
114
+ const sides = ['top', 'right', 'bottom', 'left'];
115
+ if (comp) {
116
+ const slice = state.components[comp];
117
+ if (!slice) return false;
118
+ return sides.some((s) => `${varName}-${s}` in slice.aliases);
119
+ }
120
+ return sides.some((s) => !!document.documentElement.style.getPropertyValue(`${varName}-${s}`).trim());
121
+ }
122
+
123
+ function categorize(v: string, comp: string | undefined, state: typeof $editorState): Kind {
124
+ const k = rawKind(v);
125
+ if (k === 'padding' && paddingIsSplit(v, comp, state)) return 'padding-split';
126
+ return k;
127
+ }
128
+
129
+ /** For sibling/grouping checks we want the canonical kind, not the split-vs-single distinction. */
130
+ function groupingKind(v: string): Kind {
131
+ return rawKind(v);
132
+ }
133
+
134
+ /** Selector registry: one entry per kind. `extra` props (e.g. UIPaddingSelector's
135
+ `mode`/`splittable`/`rowLabel`) are forwarded alongside the linked props. */
136
+ type SelectorEntry = {
137
+ component: ComponentType;
138
+ extra?: (token: Token) => Record<string, unknown>;
139
+ /** When true, the row is rendered as a self-contained block (spans all grid columns,
140
+ no .token-row wrapper, no contexts strip). Currently only `padding-split`. */
141
+ standalone?: boolean;
142
+ };
143
+
144
+ const SELECTOR_REGISTRY: Record<Kind, SelectorEntry> = {
145
+ 'font-family': { component: UIFontFamilySelector },
146
+ 'font-weight': { component: UIFontWeightSelector },
147
+ 'font-size': { component: UIFontSizeSelector },
148
+ 'line-height': { component: UILineHeightSelector },
149
+ 'border-width': { component: UIVariantSelector, extra: () => ({ ...BORDER_WIDTH }) },
150
+ 'divider-width': { component: UIVariantSelector, extra: () => ({ ...BORDER_WIDTH }) },
151
+ 'divider-height': { component: UIVariantSelector, extra: () => ({ ...DIVIDER_HEIGHT }) },
152
+ 'dot-size': { component: UIVariantSelector, extra: () => ({ ...DOT_SIZE }) },
153
+ 'radius': { component: UIVariantSelector, extra: () => ({ ...RADIUS }) },
154
+ 'padding': { component: UIPaddingSelector, extra: () => ({ mode: 'single' }) },
155
+ /* padding-split is NOT standalone: TokenLayout renders the .token-label
156
+ (e.g. "padding") in col 1 and the wrapper provides the [label][trigger][value]
157
+ subgrid. UIPaddingSelector's sides template fills cols 2-3 of row 1 with
158
+ the link/merge header and cols 1-3 of row 2 with the side rows, so the
159
+ four side dropdowns align with the same trigger and value columns as
160
+ every other property in the panel. */
161
+ 'padding-split': {
162
+ component: UIPaddingSelector,
163
+ extra: () => ({ mode: 'sides' }),
164
+ },
165
+ 'gap': { component: UIPaddingSelector, extra: () => ({ mode: 'single', splittable: false }) },
166
+ 'blur': { component: UIVariantSelector, extra: () => ({ ...BLUR }) },
167
+ 'shadow': { component: UIVariantSelector, extra: () => ({ ...SHADOW }) },
168
+ 'surface': { component: UIPaletteSelector },
169
+ 'border': { component: UIPaletteSelector },
170
+ 'extras': { component: UIPaletteSelector },
171
+ };
172
+
173
+ /** Multi-col rank: same as `orderRank` but with `extras` (text-color-like) hoisted
174
+ between `line-height` and `border-width` so typography reads as one logical
175
+ block in column flow. Single-col mode keeps `orderRank` (linked-first sort
176
+ already segregates extras to the bottom). */
177
+ const multiColRank: Record<Kind, number> = (() => {
178
+ const reordered: Kind[] = [
179
+ 'font-family',
180
+ 'font-weight',
181
+ 'font-size',
182
+ 'line-height',
183
+ 'extras',
184
+ 'divider-width',
185
+ 'divider-height',
186
+ 'dot-size',
187
+ 'radius',
188
+ 'padding',
189
+ 'padding-split',
190
+ 'gap',
191
+ 'blur',
192
+ 'shadow',
193
+ 'surface',
194
+ 'border-width',
195
+ 'border',
196
+ ];
197
+ return Object.fromEntries(reordered.map((k, i) => [k, i])) as Record<Kind, number>;
198
+ })();
199
+
200
+ function buildEntries(list: Token[], order: Map<string, number> | undefined, linked: Set<Kind>, comp: string | undefined, state: typeof $editorState, multiCol: boolean): Entry[] {
201
+ const indexed = list.map((token, i) => ({ e: { kind: categorize(token.variable, comp, state), token }, i }));
202
+ const rank = multiCol ? multiColRank : orderRank;
203
+ indexed.sort((a, b) => {
204
+ if (!multiCol) {
205
+ const aLinked = linked.has(a.e.kind) ? 0 : 1;
206
+ const bLinked = linked.has(b.e.kind) ? 0 : 1;
207
+ if (aLinked !== bLinked) return aLinked - bLinked;
208
+ }
209
+ const rankDiff = rank[a.e.kind] - rank[b.e.kind];
210
+ if (rankDiff !== 0) return rankDiff;
211
+ const aKey = order?.get(a.e.token.variable);
212
+ const bKey = order?.get(b.e.token.variable);
213
+ if (aKey !== undefined && bKey !== undefined) return aKey - bKey;
214
+ if (aKey !== undefined) return -1;
215
+ if (bKey !== undefined) return 1;
216
+ return a.i - b.i;
217
+ });
218
+ return indexed.map((x) => x.e);
219
+ }
220
+
221
+ /** Kinds that currently have at least one variable with ≥2 siblings in the component.
222
+ Returns canonical kinds (so a `padding` linked-set covers `padding-split` rows too). */
223
+ function computeLinkedKinds(comp: string | undefined, state: typeof $editorState): Set<Kind> {
224
+ const set = new Set<Kind>();
225
+ if (!comp) return set;
226
+ const slice = state.components[comp];
227
+ if (!slice) return set;
228
+ for (const varName of Object.keys(slice.aliases)) {
229
+ if (getComponentPropertySiblings(comp, varName).length >= 2) {
230
+ const k = groupingKind(varName);
231
+ set.add(k);
232
+ if (k === 'padding') set.add('padding-split');
233
+ }
234
+ }
235
+ return set;
236
+ }
237
+
238
+ const dispatch = createEventDispatcher();
239
+
240
+ /** When a row collapses several groupKey leads into one display, mirror the lead's
241
+ new alias onto each peer (and its siblings) so the merged display stays in sync. */
242
+ function handleRowChange(token: Token) {
243
+ if (token.mergeVariables?.length && component) {
244
+ const slice = $editorState.components[component];
245
+ const leadAlias = slice?.aliases[token.variable];
246
+ for (const peer of token.mergeVariables) {
247
+ if (leadAlias) setComponentAliasLinked(component, peer, leadAlias);
248
+ else clearComponentAliasLinked(component, peer);
249
+ }
250
+ }
251
+ dispatch('change');
252
+ }
253
+
254
+ /** Bidirectional hover cue with the Linked-properties block. The upper grid
255
+ (isLinkedBlock=false) drives and reads the shared store; the inner grid
256
+ inside each linked card (isLinkedBlock=true) sits out — its parent card
257
+ already provides the visual cue, and re-emitting from the inner row would
258
+ flicker hover state when the cursor crossed sub-elements. */
259
+ const editorCtx = getEditorContext();
260
+ const hoveredLinkedVariable = editorCtx?.hoveredLinkedVariable;
261
+ function setHover(variable: string | null) {
262
+ if (isLinkedBlock) return;
263
+ hoveredLinkedVariable?.set(variable);
264
+ }
265
+
266
+ $: hoveredVar = !isLinkedBlock && hoveredLinkedVariable ? $hoveredLinkedVariable : null;
267
+ $: isMultiCol = columns > 1;
268
+ $: linkedKinds = computeLinkedKinds(component, $editorState);
269
+ $: entries = buildEntries(tokens.filter((t) => !t.hidden), linkedOrder, linkedKinds, component, $editorState, isMultiCol);
270
+ /** Index of the first independent (non-linked) entry; -1 when there are no linked entries or no boundary.
271
+ Suppressed in multi-col mode (no divider — column structure is the grouping). */
272
+ $: firstIndependentIdx = (() => {
273
+ if (isMultiCol) return -1;
274
+ const idx = entries.findIndex((e) => !linkedKinds.has(e.kind));
275
+ if (idx <= 0) return -1;
276
+ return idx;
277
+ })();
278
+ /** Rows per column for column-major flow; ceil so the last column may be short. */
279
+ $: rowsPerCol = isMultiCol ? Math.max(1, Math.ceil(entries.length / columns)) : entries.length;
280
+
281
+ </script>
282
+
283
+ <div class="token-group">
284
+ {#if title}
285
+ <span class="token-group-title">{title}</span>
286
+ {/if}
287
+ <div
288
+ class="token-grid"
289
+ class:multi-col={isMultiCol}
290
+ style:--columns={columns}
291
+ style:--rows-per-col={rowsPerCol}
292
+ >
293
+ {#each entries as entry, i}
294
+ {@const token = entry.token}
295
+ {@const dis = token.disabled ?? false}
296
+ {@const ctxs = contexts[token.variable]}
297
+ {@const lockedSelections = dis}
298
+ {@const sel = SELECTOR_REGISTRY[entry.kind]}
299
+ {@const sharedProps = {
300
+ variable: token.variable,
301
+ component,
302
+ canBeLinked: token.canBeLinked ?? false,
303
+ selectionsLocked: lockedSelections,
304
+ }}
305
+ {@const extra = sel.extra ? sel.extra(token) : {}}
306
+ {@const isNonFirstSet = isMultiCol && Math.floor(i / rowsPerCol) > 0}
307
+ {#if i === firstIndependentIdx}
308
+ <div class="zone-divider" aria-hidden="true"></div>
309
+ {/if}
310
+ <!--
311
+ Same wrapper for standalone and row-chrome modes so the inner
312
+ <svelte:component> stays at one template position across a kind
313
+ change (e.g. padding ↔ padding-split). If we branched on
314
+ sel.standalone with the component inside each branch, Svelte
315
+ would treat them as different mount points and remount the
316
+ selector — which kills any |local transition the selector runs
317
+ on internal prop changes (mode in UIPaddingSelector). Class
318
+ toggling alone keeps the instance alive; the label and contexts
319
+ come and go around it.
320
+ -->
321
+ {@const isLinkedRow = linkedKinds.has(entry.kind)}
322
+ {@const isHovered = !isLinkedBlock && isLinkedRow && hoveredVar === token.variable}
323
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
324
+ <div
325
+ class="token-entry"
326
+ class:token-row={!sel.standalone}
327
+ class:has-contexts={!sel.standalone && !!ctxs?.length}
328
+ class:linked-hovered={isHovered}
329
+ class:non-first-set={isNonFirstSet}
330
+ on:mouseenter={isLinkedBlock || !isLinkedRow ? undefined : () => setHover(token.variable)}
331
+ on:mouseleave={isLinkedBlock || !isLinkedRow ? undefined : () => setHover(null)}
332
+ >
333
+ {#if !sel.standalone}
334
+ <span class="token-label">{token.label}</span>
335
+ {/if}
336
+ <svelte:component
337
+ this={sel.component}
338
+ {...sharedProps}
339
+ {...extra}
340
+ on:change={() => handleRowChange(token)}
341
+ />
342
+ {#if !sel.standalone && ctxs?.length}
343
+ <div class="token-contexts">
344
+ {#each ctxs as ctx}
345
+ <span class="token-context">{ctx}</span>
346
+ {/each}
347
+ </div>
348
+ {/if}
349
+ </div>
350
+ {/each}
351
+ </div>
352
+ </div>
353
+
354
+ <style>
355
+ .token-group {
356
+ display: flex;
357
+ flex-direction: column;
358
+ gap: var(--ui-space-6);
359
+ }
360
+
361
+ .token-group-title {
362
+ font-size: var(--ui-font-size-xs);
363
+ font-weight: var(--ui-font-weight-semibold);
364
+ color: var(--ui-text-tertiary);
365
+ font-family: var(--ui-font-mono);
366
+ }
367
+
368
+ .token-grid {
369
+ --token-selector-w: 8rem;
370
+ --columns: 1;
371
+ display: grid;
372
+ grid-template-columns: repeat(var(--columns), max-content var(--token-selector-w) 1fr);
373
+ column-gap: var(--ui-space-10);
374
+ row-gap: var(--ui-space-6);
375
+ align-items: center;
376
+ padding: var(--ui-space-4) var(--ui-space-12);
377
+ min-width: 0;
378
+ }
379
+ /* Multi-col mode: column-major flow with explicit row count so items fill
380
+ column 1 top-to-bottom before spilling into column 2, etc. The value track
381
+ drops to `auto` so column-sets pack at natural width instead of letting
382
+ `1fr` value cells eat the panel and shove sets to opposite edges. The
383
+ inter-set gap is applied as `padding-left` on the lead column of every
384
+ non-first set — see `.non-first-set > .token-label`. Selector width and
385
+ gap are intentionally tighter than single-col so the layout stays viable
386
+ inside the docked overlay's iframe (~624px container at default width). */
387
+ .token-grid.multi-col {
388
+ --token-selector-w: 7rem;
389
+ grid-template-columns: repeat(var(--columns), max-content var(--token-selector-w) auto);
390
+ grid-template-rows: repeat(var(--rows-per-col), auto);
391
+ grid-auto-flow: column;
392
+ column-gap: var(--ui-space-8);
393
+ justify-content: start;
394
+ }
395
+ .token-grid.multi-col .token-entry.token-row.non-first-set > .token-label {
396
+ padding-left: var(--ui-space-20);
397
+ }
398
+
399
+ @container (max-width: 480px) {
400
+ .token-grid { --token-selector-w: 6rem; }
401
+ }
402
+
403
+ /* Narrow multi-col: shrink selector + inter-set gap further before giving
404
+ up the second column. Targets the overlay's typical docked width range. */
405
+ @container (max-width: 640px) {
406
+ .token-grid.multi-col {
407
+ --token-selector-w: 6rem;
408
+ column-gap: var(--ui-space-6);
409
+ }
410
+ .token-grid.multi-col .token-entry.token-row.non-first-set > .token-label {
411
+ padding-left: var(--ui-space-12);
412
+ }
413
+ }
414
+
415
+ /* Drop to one column only when even the tightened layout can't fit two
416
+ sets. Single col uses row flow (default), so unsetting the column-flow +
417
+ row template restores the original layout. The value track returns to
418
+ `1fr` so the lone column fills the panel like single-col mode, and the
419
+ inter-set padding is suppressed so wrapped "set 2" rows don't sit
420
+ indented. */
421
+ @container (max-width: 520px) {
422
+ .token-grid.multi-col {
423
+ --columns: 1;
424
+ grid-template-columns: max-content var(--token-selector-w) 1fr;
425
+ grid-template-rows: none;
426
+ grid-auto-flow: row;
427
+ column-gap: var(--ui-space-10);
428
+ }
429
+ .token-grid.multi-col .token-entry.token-row.non-first-set > .token-label {
430
+ padding-left: 0;
431
+ }
432
+ }
433
+
434
+ @container (max-width: 380px) {
435
+ .token-grid {
436
+ grid-template-columns: max-content 1fr;
437
+ column-gap: var(--ui-space-6);
438
+ }
439
+ .token-grid :global(.ui-token-selector) { grid-column: 2; }
440
+ }
441
+
442
+ .zone-divider {
443
+ grid-column: 1 / -1;
444
+ height: 1.75rem;
445
+ }
446
+
447
+ /* The wrapper exists in both standalone and row-chrome modes to keep
448
+ the inner <svelte:component> at one template position across kind
449
+ changes (so its internal |local transitions can fire on prop
450
+ updates instead of being short-circuited by a remount).
451
+ - Standalone (no .token-row): `display: contents` makes the
452
+ wrapper transparent to the parent grid, letting the standalone
453
+ child (e.g. UIPaddingSelector's split fieldset) place itself
454
+ directly against `.token-grid`'s columns just like it did when
455
+ it was rendered without a wrapper.
456
+ - Row chrome (.token-row): grid + subgrid + alignment, the
457
+ original .token-row layout for [label][trigger][value]. */
458
+ .token-entry {
459
+ display: contents;
460
+ }
461
+ .token-entry.token-row {
462
+ display: grid;
463
+ grid-template-columns: subgrid;
464
+ /* Span one [label][selector][value] column-set. Equivalent to `1 / -1`
465
+ in single-col mode (3 sub-cols total) and lands the row in one
466
+ column-set in multi-col mode (3*N sub-cols total). */
467
+ grid-column: span 3;
468
+ align-items: center;
469
+ row-gap: var(--ui-space-2);
470
+ border-radius: var(--ui-radius-sm);
471
+ transition: background var(--ui-transition-fast), box-shadow var(--ui-transition-fast);
472
+ min-width: 0;
473
+ }
474
+ /* Bidirectional hover cue with the Linked-properties block. Background
475
+ lift extends the row beyond its three columns so the cue reads as a
476
+ full-width band; box-shadow adds the same horizontal padding the grid's
477
+ own padding provides without pushing the row's inner layout. */
478
+ .token-entry.token-row.linked-hovered {
479
+ background: var(--ui-hover-lowest);
480
+ box-shadow: 0 0 0 var(--ui-space-6) var(--ui-hover-lowest);
481
+ }
482
+ /* Suppress the trigger's own surface fill while the row is in the linked-
483
+ hover state so the row's hover band reads as one continuous strip
484
+ instead of being broken by the trigger's brighter chip. `:not(:hover)`
485
+ keeps the trigger's direct hover style intact when the cursor lands on
486
+ it specifically. */
487
+ .token-entry.token-row.linked-hovered :global(.ui-ts-trigger:not(:hover)) {
488
+ background: transparent;
489
+ }
490
+
491
+ .token-label {
492
+ grid-column: 1;
493
+ font-size: var(--ui-font-size-sm);
494
+ color: var(--ui-text-secondary);
495
+ text-align: left;
496
+ line-height: 1;
497
+ }
498
+
499
+ .token-contexts {
500
+ grid-column: 2 / -1;
501
+ display: flex;
502
+ flex-direction: column;
503
+ gap: 1px;
504
+ }
505
+
506
+ .token-context {
507
+ font-size: var(--ui-font-size-xs);
508
+ color: var(--ui-text-tertiary);
509
+ white-space: nowrap;
510
+ }
511
+ </style>