@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,137 @@
1
+ <script context="module" lang="ts">
2
+ import { buildTypeGroupColorTokens } from './scaffolding/buildTypeGroupTokens';
3
+ import type { Token, TypeGroupConfig } from './scaffolding/types';
4
+
5
+ export const component = 'tabbar';
6
+
7
+ // The tab object — four states (default/hover/active/disabled) of the same tab button.
8
+ const tabStateNames = ['default', 'hover', 'active', 'disabled'] as const;
9
+ type TabState = typeof tabStateNames[number];
10
+ function tabStateTokens(s: TabState): Token[] {
11
+ return [
12
+ { label: 'icon size', canBeLinked: true, groupKey: 'icon-size', variable: `--tabbar-${s}-icon-size` },
13
+ { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--tabbar-${s}-padding` },
14
+ { label: 'surface color', groupKey: 'surface', variable: `--tabbar-${s}-surface` },
15
+ { label: 'top radius', canBeLinked: true, groupKey: 'tab-top-radius', variable: `--tabbar-${s}-tab-top-radius` },
16
+ { label: 'bottom radius', canBeLinked: true, groupKey: 'tab-bottom-radius', variable: `--tabbar-${s}-tab-bottom-radius` },
17
+ { label: 'border color', canBeLinked: true, groupKey: 'tab-border-color', variable: `--tabbar-${s}-tab-border-color` },
18
+ { label: 'border width', canBeLinked: true, groupKey: 'tab-border-width', variable: `--tabbar-${s}-tab-border-width` },
19
+ ];
20
+ }
21
+ function tabStateTypeGroups(s: TabState): TypeGroupConfig[] {
22
+ return [{
23
+ legend: 'tab text',
24
+ colorVariable: `--tabbar-${s}-text`,
25
+ familyVariable: `--tabbar-${s}-text-font-family`,
26
+ sizeVariable: `--tabbar-${s}-text-font-size`,
27
+ weightVariable: `--tabbar-${s}-text-font-weight`,
28
+ lineHeightVariable: `--tabbar-${s}-text-line-height`,
29
+ }];
30
+ }
31
+
32
+ // One VariantGroup with the bar exposed as a state alongside the three tab
33
+ // states (mirrors SegmentedControl's "control bar" + per-option states layout).
34
+ const states: Record<string, Token[]> = {
35
+ bar: [
36
+ { label: 'divider color', groupKey: 'bar-divider', variable: '--tabbar-bar-divider' },
37
+ { label: 'divider thickness', groupKey: 'bar-divider-thickness', variable: '--tabbar-bar-divider-thickness' },
38
+ { label: 'indicator thickness', groupKey: 'bar-indicator-thickness', variable: '--tabbar-bar-indicator-thickness' },
39
+ { label: 'space above', groupKey: 'bar-top-margin', variable: '--tabbar-bar-top-margin' },
40
+ { label: 'space below tabs', groupKey: 'bar-bottom-padding', variable: '--tabbar-bar-bottom-padding' },
41
+ { label: 'space under divider', groupKey: 'bar-bottom-margin', variable: '--tabbar-bar-bottom-margin' },
42
+ { label: 'tab gap', groupKey: 'tab-gap', variable: '--tabbar-tab-gap' },
43
+ ],
44
+ ...Object.fromEntries(tabStateNames.map((s) => [`${s} tab`, tabStateTokens(s)])),
45
+ };
46
+ states['active tab'] = [
47
+ ...states['active tab'],
48
+ { label: 'indicator color', groupKey: 'indicator-color', variable: '--tabbar-active-border' },
49
+ ];
50
+ const typeGroups: Record<string, TypeGroupConfig[]> = Object.fromEntries(
51
+ tabStateNames.map((s) => [`${s} tab`, tabStateTypeGroups(s)]),
52
+ );
53
+ const tabTypeGroupTokens: Token[] = tabStateNames.flatMap((s) => [
54
+ { label: 'font family', canBeLinked: true, groupKey: 'font-family', variable: `--tabbar-${s}-text-font-family` },
55
+ { label: 'font size', canBeLinked: true, groupKey: 'font-size', variable: `--tabbar-${s}-text-font-size` },
56
+ { label: 'font weight', canBeLinked: true, groupKey: 'font-weight', variable: `--tabbar-${s}-text-font-weight` },
57
+ { label: 'line height', canBeLinked: true, groupKey: 'line-height', variable: `--tabbar-${s}-text-line-height` },
58
+ ]);
59
+ export const allTokens: Token[] = [
60
+ ...Object.values(states).flat(),
61
+ ...buildTypeGroupColorTokens(typeGroups),
62
+ ...tabTypeGroupTokens,
63
+ ];
64
+
65
+ // Linking: shape props across tab states (same tab object).
66
+ const linkableContexts = new Map<string, string>([
67
+ [`--tabbar-active-border`, `active tab`] as const,
68
+ ...tabStateNames.flatMap((s) => [
69
+ [`--tabbar-${s}-tab-border-color`, `${s} tab`] as const,
70
+ [`--tabbar-${s}-tab-border-width`, `${s} tab`] as const,
71
+ [`--tabbar-${s}-tab-top-radius`, `${s} tab`] as const,
72
+ [`--tabbar-${s}-tab-bottom-radius`, `${s} tab`] as const,
73
+ [`--tabbar-${s}-padding`, `${s} tab`] as const,
74
+ [`--tabbar-${s}-icon-size`, `${s} tab`] as const,
75
+ [`--tabbar-${s}-text-font-family`, `${s} tab`] as const,
76
+ [`--tabbar-${s}-text-font-size`, `${s} tab`] as const,
77
+ [`--tabbar-${s}-text-font-weight`, `${s} tab`] as const,
78
+ [`--tabbar-${s}-text-line-height`, `${s} tab`] as const,
79
+ ]),
80
+ ]);
81
+ </script>
82
+
83
+ <script lang="ts">
84
+ import TabBar from '../components/TabBar.svelte';
85
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
86
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
87
+ import { editorState } from '../lib/editorStore';
88
+ import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
89
+
90
+ let selectedDemoTab = 'overview';
91
+ const demoTabs = [
92
+ { id: 'overview', label: 'Overview', icon: 'fas fa-home' },
93
+ { id: 'details', label: 'Details', icon: 'fas fa-info-circle' },
94
+ { id: 'settings', label: 'Settings', icon: 'fas fa-cog' },
95
+ { id: 'disabled', label: 'Disabled', icon: 'fas fa-ban', disabled: true },
96
+ ];
97
+
98
+ $: linked = computeLinkedBlock(component, linkableContexts, allTokens, $editorState);
99
+
100
+ $: visibleStates = Object.fromEntries(
101
+ Object.entries(states).map(([name, list]) => [name, withLinkedDisabled(list, linked.varSet)]),
102
+ ) as Record<string, Token[]>;
103
+ </script>
104
+
105
+ <ComponentEditorBase {component} title="Tab Bar" description="Tab navigation with icon support and disabled state. Import from <code>components/TabBar.svelte</code>" tokens={allTokens} {linked}>
106
+ <VariantGroup
107
+ name="tabbar"
108
+ title="Tab Bar"
109
+ states={visibleStates}
110
+ {typeGroups}
111
+ {component}
112
+ let:activeState
113
+ >
114
+ {@const forceClass = activeState === 'hover tab' ? 'force-hover' : ''}
115
+ <TabBar tabs={demoTabs} selectedTab={selectedDemoTab} class={forceClass} on:tabChange={(e) => (selectedDemoTab = e.detail)} />
116
+ <div class="tab-content-demo">
117
+ {#if selectedDemoTab === 'overview'}
118
+ <p style="margin: 0;">Overview tab content</p>
119
+ {:else if selectedDemoTab === 'details'}
120
+ <p style="margin: 0;">Details tab content</p>
121
+ {:else if selectedDemoTab === 'settings'}
122
+ <p style="margin: 0;">Settings tab content</p>
123
+ {/if}
124
+ </div>
125
+ </VariantGroup>
126
+ </ComponentEditorBase>
127
+
128
+ <style>
129
+ .tab-content-demo {
130
+ padding: var(--space-16);
131
+ color: var(--ui-text-secondary);
132
+ background: var(--ui-surface-low);
133
+ border: 1px solid var(--ui-border-faint);
134
+ border-top: none;
135
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
136
+ }
137
+ </style>
@@ -0,0 +1,128 @@
1
+ <script context="module" lang="ts">
2
+ import type { Token, TypeGroupConfig } from './scaffolding/types';
3
+
4
+ export const component = 'table';
5
+
6
+ // Border colors and widths are linkable across wrapper/header/row/column so
7
+ // a user can lock "all lines on the table" to the same swatch + weight with
8
+ // one move, then break out individual surfaces when needed. Every other
9
+ // groupKey is slot-unique so header/cell/stripe stay independent — header bg
10
+ // vs zebra stripe, header pad vs cell pad, header text vs cell text all
11
+ // serve different visual roles. (Sharing a groupKey would silently declare
12
+ // them as siblings without surfacing the link in the LinkedBlock.)
13
+ const states: Record<string, Token[]> = {
14
+ wrapper: [
15
+ { label: 'border color', canBeLinked: true, groupKey: 'border', variable: '--table-default-border' },
16
+ { label: 'border width', canBeLinked: true, groupKey: 'width', variable: '--table-default-border-width' },
17
+ { label: 'corner radius', groupKey: 'radius', variable: '--table-default-radius' },
18
+ { label: 'table shadow', groupKey: 'shadow', variable: '--table-default-shadow' },
19
+ ],
20
+ header: [
21
+ { label: 'surface color', groupKey: 'header-surface', variable: '--table-default-header-surface' },
22
+ { label: 'border color', canBeLinked: true, groupKey: 'border', variable: '--table-default-header-border' },
23
+ { label: 'border width', canBeLinked: true, groupKey: 'width', variable: '--table-default-header-border-width' },
24
+ { label: 'padding', groupKey: 'header-padding', variable: '--table-default-header-padding' },
25
+ ],
26
+ cell: [
27
+ { label: 'padding', groupKey: 'cell-padding', variable: '--table-default-cell-padding' },
28
+ ],
29
+ row: [
30
+ { label: 'divider color', canBeLinked: true, groupKey: 'border', variable: '--table-default-row-divider' },
31
+ { label: 'divider width', canBeLinked: true, groupKey: 'width', variable: '--table-default-row-divider-width' },
32
+ { label: 'stripe surface', groupKey: 'row-stripe-surface', variable: '--table-default-row-stripe-surface' },
33
+ ],
34
+ column: [
35
+ { label: 'divider color', canBeLinked: true, groupKey: 'border', variable: '--table-default-column-divider' },
36
+ { label: 'divider width', canBeLinked: true, groupKey: 'width', variable: '--table-default-column-divider-width' },
37
+ ],
38
+ };
39
+
40
+ // State name is the context label so the LinkageChart rows read as
41
+ // wrapper/header/row/column for each shared groupKey.
42
+ const linkableContexts = new Map<string, string>(
43
+ Object.entries(states).flatMap(([state, tokens]) =>
44
+ tokens
45
+ .filter((t) => t.canBeLinked)
46
+ .map((t) => [t.variable, state] as [string, string]),
47
+ ),
48
+ );
49
+
50
+ const typeGroups: Record<string, TypeGroupConfig[]> = {
51
+ header: [{
52
+ legend: 'header text',
53
+ colorVariable: '--table-default-header-text',
54
+ familyVariable: '--table-default-header-font-family',
55
+ sizeVariable: '--table-default-header-font-size',
56
+ weightVariable: '--table-default-header-font-weight',
57
+ lineHeightVariable: '--table-default-header-line-height',
58
+ }],
59
+ cell: [{
60
+ legend: 'cell text',
61
+ colorVariable: '--table-default-cell-text',
62
+ familyVariable: '--table-default-cell-font-family',
63
+ sizeVariable: '--table-default-cell-font-size',
64
+ weightVariable: '--table-default-cell-font-weight',
65
+ lineHeightVariable: '--table-default-cell-line-height',
66
+ }],
67
+ };
68
+ // Slot-unique groupKeys keep header text and cell text independent. The
69
+ // generic `buildTypeGroupColorTokens` helper isn't used here because it
70
+ // derives groupKey from the variable's last-dash suffix, which collapses
71
+ // both `--table-...-header-text` and `--table-...-cell-text` onto a shared
72
+ // `text` groupKey — phantom-linking the two slots.
73
+ const typeGroupColorTokens: Token[] = [
74
+ { label: 'color', groupKey: 'header-text', variable: '--table-default-header-text' },
75
+ { label: 'color', groupKey: 'cell-text', variable: '--table-default-cell-text' },
76
+ ];
77
+ const typeGroupTokens: Token[] = [
78
+ { label: 'font family', groupKey: 'header-font-family', variable: '--table-default-header-font-family' },
79
+ { label: 'font size', groupKey: 'header-font-size', variable: '--table-default-header-font-size' },
80
+ { label: 'font weight', groupKey: 'header-font-weight', variable: '--table-default-header-font-weight' },
81
+ { label: 'line height', groupKey: 'header-line-height', variable: '--table-default-header-line-height' },
82
+ { label: 'font family', groupKey: 'cell-font-family', variable: '--table-default-cell-font-family' },
83
+ { label: 'font size', groupKey: 'cell-font-size', variable: '--table-default-cell-font-size' },
84
+ { label: 'font weight', groupKey: 'cell-font-weight', variable: '--table-default-cell-font-weight' },
85
+ { label: 'line height', groupKey: 'cell-line-height', variable: '--table-default-cell-line-height' },
86
+ ];
87
+
88
+ export const allTokens: Token[] = [
89
+ ...Object.values(states).flat(),
90
+ ...typeGroupColorTokens,
91
+ ...typeGroupTokens,
92
+ ];
93
+ </script>
94
+
95
+ <script lang="ts">
96
+ import Table from '../components/Table.svelte';
97
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
98
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
99
+ import { editorState } from '../lib/editorStore';
100
+ import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
101
+
102
+ $: linked = computeLinkedBlock(component, linkableContexts, allTokens, $editorState);
103
+ $: visibleStates = Object.fromEntries(
104
+ Object.entries(states).map(([state, tokens]) => [state, withLinkedDisabled(tokens, linked.varSet)]),
105
+ ) as Record<string, Token[]>;
106
+ </script>
107
+
108
+ <ComponentEditorBase {component} title="Table" description="Styled wrapper around <code>&lt;table&gt;</code> with horizontal scroll on narrow viewports. Import from <code>components/Table.svelte</code>" tokens={allTokens} {linked}>
109
+ <VariantGroup name="table" title="Table" states={visibleStates} {typeGroups} {component}>
110
+ <Table>
111
+ <table>
112
+ <thead>
113
+ <tr>
114
+ <th>Resource</th>
115
+ <th>Yield</th>
116
+ <th>Tier</th>
117
+ </tr>
118
+ </thead>
119
+ <tbody>
120
+ <tr><td>Farmland</td><td>2 Food</td><td>I</td></tr>
121
+ <tr><td>Quarry</td><td>1 Stone</td><td>II</td></tr>
122
+ <tr><td>Mine</td><td>1 Ore</td><td>II</td></tr>
123
+ <tr><td>Forest</td><td>2 Lumber</td><td>I</td></tr>
124
+ </tbody>
125
+ </table>
126
+ </Table>
127
+ </VariantGroup>
128
+ </ComponentEditorBase>
@@ -0,0 +1,122 @@
1
+ <script context="module" lang="ts">
2
+ import { buildTypeGroupColorTokens } from './scaffolding/buildTypeGroupTokens';
3
+ import type { Token, TypeGroupConfig } from './scaffolding/types';
4
+
5
+ export const component = 'tooltip';
6
+
7
+ // Tooltip is a single object — surface/border/padding/radius/shadow live together.
8
+ const states: Record<string, Token[]> = {
9
+ tooltip: [
10
+ { label: 'surface color', variable: '--tooltip-surface' },
11
+ { label: 'border color', variable: '--tooltip-border' },
12
+ { label: 'border width', groupKey: 'width', variable: '--tooltip-border-width' },
13
+ { label: 'corner radius', variable: '--tooltip-radius' },
14
+ { label: 'padding', variable: '--tooltip-padding' },
15
+ { label: 'tooltip shadow', variable: '--tooltip-shadow' },
16
+ ],
17
+ };
18
+
19
+ const typeGroups: Record<string, TypeGroupConfig[]> = {
20
+ tooltip: [{
21
+ legend: 'tooltip text',
22
+ colorVariable: '--tooltip-text',
23
+ familyVariable: '--tooltip-text-font-family',
24
+ sizeVariable: '--tooltip-text-font-size',
25
+ weightVariable: '--tooltip-text-font-weight',
26
+ lineHeightVariable: '--tooltip-text-line-height',
27
+ }],
28
+ };
29
+ const typeGroupTokens: Token[] = [
30
+ { label: 'font family', groupKey: 'family', variable: '--tooltip-text-font-family' },
31
+ { label: 'font size', groupKey: 'size', variable: '--tooltip-text-font-size' },
32
+ { label: 'font weight', groupKey: 'weight', variable: '--tooltip-text-font-weight' },
33
+ { label: 'line height', groupKey: 'height', variable: '--tooltip-text-line-height' },
34
+ ];
35
+ export const allTokens: Token[] = [
36
+ ...Object.values(states).flat(),
37
+ ...buildTypeGroupColorTokens(typeGroups),
38
+ ...typeGroupTokens,
39
+ ];
40
+ </script>
41
+
42
+ <script lang="ts">
43
+ import { onMount } from 'svelte';
44
+ import Tooltip from '../components/Tooltip.svelte';
45
+ import VariantGroup from './scaffolding/VariantGroup.svelte';
46
+ import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
47
+ import ShadowBackdrop from './scaffolding/ShadowBackdrop.svelte';
48
+ import UIPaletteSelector from '../ui/UIPaletteSelector.svelte';
49
+ import { setCssVar } from '../lib/cssVarSync';
50
+
51
+ const bgVar = '--backdrop-tooltip-surface';
52
+ const hintText = 'Helpful Hint';
53
+
54
+ onMount(() => {
55
+ if (!document.documentElement.style.getPropertyValue(bgVar)) {
56
+ setCssVar(bgVar, 'var(--surface-canvas)');
57
+ }
58
+ });
59
+ </script>
60
+
61
+ <ComponentEditorBase {component} title="Tooltip" description="Hover tooltip with configurable position. Import from <code>components/Tooltip.svelte</code>" tokens={allTokens}>
62
+ <svelte:fragment slot="config">
63
+ <label class="backdrop-config">
64
+ <span>Sample background</span>
65
+ <div class="picker-slot">
66
+ <UIPaletteSelector variable={bgVar} />
67
+ </div>
68
+ </label>
69
+ </svelte:fragment>
70
+ <VariantGroup
71
+ name="tooltip"
72
+ title="Tooltip"
73
+ {states}
74
+ {typeGroups}
75
+ {component}
76
+ >
77
+ <ShadowBackdrop mode="color" colorVariable={bgVar}>
78
+ <div class="tooltip-demo-row">
79
+ <Tooltip text={hintText} open>
80
+ <span class="tooltip-demo-target">Helpful Hint</span>
81
+ </Tooltip>
82
+ <Tooltip text={hintText} position="bottom">
83
+ <span class="tooltip-demo-target">Hover me</span>
84
+ </Tooltip>
85
+ </div>
86
+ </ShadowBackdrop>
87
+ </VariantGroup>
88
+ </ComponentEditorBase>
89
+
90
+ <style>
91
+ .tooltip-demo-row {
92
+ display: flex;
93
+ gap: var(--space-48);
94
+ align-items: flex-start;
95
+ justify-content: center;
96
+ padding: var(--space-48) 0;
97
+ }
98
+
99
+ .tooltip-demo-target {
100
+ display: inline-block;
101
+ padding: var(--space-8) var(--space-16);
102
+ color: var(--ui-text-secondary);
103
+ font-size: var(--font-size-sm);
104
+ border: 1px dashed var(--ui-border-subtle);
105
+ border-radius: var(--radius-sm);
106
+ background: transparent;
107
+ }
108
+
109
+ .backdrop-config {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: var(--ui-space-8);
113
+ }
114
+
115
+ .picker-slot {
116
+ min-width: 8rem;
117
+ }
118
+
119
+ .picker-slot :global(.ui-token-selector) {
120
+ width: 100%;
121
+ }
122
+ </style>
@@ -0,0 +1,93 @@
1
+ // @vitest-environment node
2
+ import { describe, it, expect } from 'vitest';
3
+ import { readFileSync, readdirSync } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { buildTokenRegistry, extractGlobalRootBody } from '../lib/tokenRegistry';
7
+ import { componentRegistryEntries } from './registry';
8
+
9
+ const TEST_DIR = dirname(fileURLToPath(import.meta.url));
10
+ const TOKENS_CSS = join(TEST_DIR, '..', 'styles', 'tokens.css');
11
+ const COMPONENTS_DIR = join(TEST_DIR, '..', 'components');
12
+
13
+ // Vitest's CSS plugin swallows `?raw` imports for .css files, so we build the
14
+ // registry from an fs-loaded snapshot rather than going through the default
15
+ // module export. Layer-2 tokens now live in each component's <style> block
16
+ // under `:global(:root)`; merge those bodies with tokens.css to match the
17
+ // runtime registry.
18
+ const tokensSource = readFileSync(TOKENS_CSS, 'utf8');
19
+ const componentTokenCss = readdirSync(COMPONENTS_DIR)
20
+ .filter((f) => f.endsWith('.svelte'))
21
+ .map((f) => extractGlobalRootBody(readFileSync(join(COMPONENTS_DIR, f), 'utf8')))
22
+ .join('\n');
23
+ const registry = buildTokenRegistry(tokensSource + '\n' + componentTokenCss);
24
+
25
+ /** Test whether a variable name fits a layer-1 design-token naming pattern. */
26
+ function isLayer1TokenName(name: string): boolean {
27
+ // Color ramps: --color-<palette>-<step> OR semantic color primitives: --color-<name>
28
+ if (/^--color-[a-z]+(-\d{3})?$/.test(name)) return true;
29
+ if (/^--surface-[a-z]+(-[a-z]+)?$/.test(name)) return true;
30
+ if (/^--border-[a-z]+(-[a-z]+)?$/.test(name)) return true;
31
+ if (/^--text-[a-z]+(-[a-z]+)?$/.test(name)) return true;
32
+ if (/^--(radius|space|font|line-height|shadow|ring|transition|overlay|hover|page-bg|border-width|gradient|icon-size|blur|dot-size)(-[a-z0-9]+)*$/.test(name)) return true;
33
+ return false;
34
+ }
35
+
36
+ // Drives both describe blocks: one entry per (component, variable) pair pulled
37
+ // from each editor's exported `allTokens`. This is the authoritative surface —
38
+ // includes template-literal-built tokens (Button, SegmentedControl, etc.) and
39
+ // typography colorVariables that the previous source-regex approach silently
40
+ // missed. See registry.ts for how each editor exports its full token list via
41
+ // `<script context="module">`.
42
+ const editorTokenCases: Array<{ editor: string; variable: string }> = [];
43
+ for (const entry of componentRegistryEntries) {
44
+ for (const t of entry.schema) {
45
+ // `hidden: true` tokens are optional override slots (e.g. split-padding's
46
+ // per-side overrides used as `var(--x, fallback)` by the themed-padding
47
+ // mixin) — they don't need a default declaration in tokens.css.
48
+ if ((t as { hidden?: boolean }).hidden) continue;
49
+ editorTokenCases.push({ editor: entry.id, variable: t.variable });
50
+ }
51
+ }
52
+
53
+ describe('design-token architecture', () => {
54
+ it('has editor schemas to inspect', () => {
55
+ expect(editorTokenCases.length).toBeGreaterThan(0);
56
+ });
57
+
58
+ describe('every editor-exposed variable resolves to a design token', () => {
59
+ for (const { editor, variable } of editorTokenCases) {
60
+ it(`${editor}: ${variable} resolves via alias chain to a layer-1 token`, () => {
61
+ const chain = registry.resolveAliasChain(variable);
62
+ const terminal = chain[chain.length - 1];
63
+ expect(
64
+ isLayer1TokenName(terminal),
65
+ `${variable} → [${chain.join(' → ')}]; terminal "${terminal}" is not a design-token name. ` +
66
+ `Either rename the target or add a var() alias declaration in tokens.css.`,
67
+ ).toBe(true);
68
+ });
69
+ }
70
+ });
71
+
72
+ // Every variable an editor binds must be declared in tokens.css (or a
73
+ // component <style> :global(:root) block) as a var() alias — not a literal,
74
+ // not a raw primitive. Component properties bind to component-scoped Layer-2
75
+ // aliases so editing one component never rebinds a shared primitive used
76
+ // elsewhere.
77
+ describe('every editor follows the component-token pattern', () => {
78
+ for (const { editor, variable } of editorTokenCases) {
79
+ it(`${editor}: ${variable} is a layer-2 component token (declared as var() alias)`, () => {
80
+ const declared = registry.getDeclaredValue(variable);
81
+ expect(
82
+ declared,
83
+ `${variable} is referenced by ${editor} but not declared in tokens.css`,
84
+ ).not.toBeNull();
85
+ expect(
86
+ declared!.startsWith('var('),
87
+ `${variable} should be a component-scoped alias declared as var(--primitive); got "${declared}". ` +
88
+ `Component properties must not rebind shared primitives directly.`,
89
+ ).toBe(true);
90
+ });
91
+ }
92
+ });
93
+ });
@@ -0,0 +1,67 @@
1
+ // @vitest-environment node
2
+ //
3
+ // Slot-collision invariant for editor schemas.
4
+ //
5
+ // A groupKey declares a sibling set: tokens that share a groupKey are linked
6
+ // peers (`getComponentPropertySiblings`, `isComponentPropertyLinked`,
7
+ // `setComponentAliasLinked`). The codebase convention (src/styles/CONVENTIONS.md)
8
+ // requires shared groupKeys to cross only a VARIANT axis — never a slot/part
9
+ // axis. Crossing slots produces "phantom siblings": the schema reports header
10
+ // and cell as linked peers, but the linked block can't surface them because
11
+ // the editor's `linkableContexts` (correctly) treats them as independent.
12
+ //
13
+ // `registerComponentSchema` already warns at runtime when a typography
14
+ // groupKey covers multiple slots (e.g. one groupKey holding both
15
+ // `--card-...-title-font-family` and `--card-...-body-font-family`). This
16
+ // test asserts the invariant instead of relying on a console warning that
17
+ // nothing fails on.
18
+ //
19
+ // Caught the TableEditor regression where `header` and `cell` typography
20
+ // shared `family`/`size`/`weight`/`height` groupKeys (and `surface`/`padding`
21
+ // shared groupKeys across header/stripe and header/cell respectively). See
22
+ // the commit that introduced this test for context.
23
+
24
+ import { describe, it, expect } from 'vitest';
25
+ import { componentRegistryEntries } from './registry';
26
+
27
+ const TYPOGRAPHY_PROP_SUFFIXES = ['font-family', 'font-size', 'font-weight', 'line-height'] as const;
28
+
29
+ /** Mirror of `typographySlotOf` in `lib/slices/components.ts`. */
30
+ function typographySlotOf(varName: string): string | null {
31
+ for (const suffix of TYPOGRAPHY_PROP_SUFFIXES) {
32
+ if (!varName.endsWith('-' + suffix)) continue;
33
+ const head = varName.slice(0, -(suffix.length + 1));
34
+ const lastDash = head.lastIndexOf('-');
35
+ if (lastDash < 0) return null;
36
+ return head.slice(lastDash + 1);
37
+ }
38
+ return null;
39
+ }
40
+
41
+ describe('editor groupKey slot invariant', () => {
42
+ for (const entry of componentRegistryEntries) {
43
+ it(`${entry.id}: no typography groupKey straddles multiple slots`, () => {
44
+ const byKey = new Map<string, string[]>();
45
+ for (const t of entry.schema as Array<{ variable: string; groupKey?: string }>) {
46
+ if (!t.groupKey) continue;
47
+ const list = byKey.get(t.groupKey) ?? [];
48
+ list.push(t.variable);
49
+ byKey.set(t.groupKey, list);
50
+ }
51
+ const violations: string[] = [];
52
+ for (const [gk, vars] of byKey) {
53
+ const slots = new Set<string>();
54
+ for (const v of vars) {
55
+ const slot = typographySlotOf(v);
56
+ if (slot) slots.add(slot);
57
+ }
58
+ if (slots.size > 1) {
59
+ violations.push(
60
+ `groupKey "${gk}" spans typography slots {${[...slots].join(', ')}}: ${vars.join(', ')}`,
61
+ );
62
+ }
63
+ }
64
+ expect(violations, violations.join('\n')).toEqual([]);
65
+ });
66
+ }
67
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Locks the per-component sibling-group topology in place during the migration
3
+ * away from `getGroupKey`'s last-dash fallback.
4
+ *
5
+ * Workflow:
6
+ * 1. Snapshot current behavior: `UPDATE_SNAPSHOT=1 pnpm test groupKeySnapshot`
7
+ * writes `temp/groupkey-snapshot.json` (groupKey → variables, per component).
8
+ * 2. Migrate: add explicit `groupKey` to every token that previously relied on
9
+ * the legacy fallback; delete the fallback branch in `getGroupKey`.
10
+ * 3. Re-run this test (no env var) — must produce the same JSON.
11
+ *
12
+ * Once the migration is complete and verified, delete this file.
13
+ */
14
+ import { describe, it, expect } from 'vitest';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { componentRegistryEntries } from './registry';
18
+
19
+ const SNAPSHOT_PATH = path.resolve(__dirname, '../../temp/groupkey-snapshot.json');
20
+
21
+ /** Mirrors the *post-migration* getGroupKey logic — schema only, no fallback. */
22
+ function effectiveGroupKey(_component: string, _variable: string, explicit: string | undefined): string | null {
23
+ return explicit ?? null;
24
+ }
25
+
26
+ function computeSnapshot(): Record<string, Record<string, string[]>> {
27
+ const out: Record<string, Record<string, string[]>> = {};
28
+ for (const entry of componentRegistryEntries) {
29
+ const groups: Record<string, string[]> = {};
30
+ for (const t of entry.schema as Array<{ variable: string; groupKey?: string }>) {
31
+ const gk = effectiveGroupKey(entry.id, t.variable, t.groupKey);
32
+ if (!gk) continue;
33
+ (groups[gk] ||= []).push(t.variable);
34
+ }
35
+ for (const k of Object.keys(groups)) groups[k].sort();
36
+ out[entry.id] = Object.fromEntries(Object.entries(groups).sort(([a], [b]) => a.localeCompare(b)));
37
+ }
38
+ return out;
39
+ }
40
+
41
+ describe('groupKey topology snapshot', () => {
42
+ it('matches the committed snapshot', () => {
43
+ const computed = computeSnapshot();
44
+ if (process.env.UPDATE_SNAPSHOT === '1') {
45
+ fs.mkdirSync(path.dirname(SNAPSHOT_PATH), { recursive: true });
46
+ fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(computed, null, 2) + '\n');
47
+ return;
48
+ }
49
+ const expected = JSON.parse(fs.readFileSync(SNAPSHOT_PATH, 'utf-8'));
50
+ expect(computed).toEqual(expected);
51
+ });
52
+ });
@@ -0,0 +1,5 @@
1
+ export { default as ComponentsTab } from './scaffolding/ComponentsTab.svelte';
2
+ export type { ComponentSection } from './scaffolding/componentSectionType';
3
+ export { defaultSections } from './scaffolding/defaultSections';
4
+
5
+ export { default as TokenLayout } from './scaffolding/TokenLayout.svelte';