@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,226 @@
1
+ import { get } from 'svelte/store';
2
+ import {
3
+ isComponentPropertyLinked,
4
+ getComponentPropertySiblings,
5
+ editorState,
6
+ } from '../../lib/editorStore';
7
+ import type { CssVarRef } from '../../lib/editorTypes';
8
+ import type { Token } from './types';
9
+
10
+ function aliasKey(ref: CssVarRef | undefined): string {
11
+ if (!ref) return '';
12
+ return ref.kind === 'token' ? `t:${ref.name}` : `v:${ref.value}`;
13
+ }
14
+
15
+ const TYPOGRAPHY_PROP_SUFFIXES = ['font-family', 'font-size', 'font-weight', 'line-height'] as const;
16
+
17
+ /** Derive a slot-aware label from a token's groupKey when the groupKey carries
18
+ * a slot prefix (e.g. `title-font-family` → "title font family"). Bare
19
+ * typography groupKeys (`font-family`) and non-typography groupKeys keep the
20
+ * token's original label.
21
+ *
22
+ * Disambiguates rows in the linked block when one bucket holds multiple
23
+ * same-shape link groups (e.g. Notification's title vs. text typography),
24
+ * without coupling the label to the per-variant view. */
25
+ function deriveLinkedLabel(label: string, groupKey: string | undefined): string {
26
+ if (!groupKey) return label;
27
+ for (const prop of TYPOGRAPHY_PROP_SUFFIXES) {
28
+ if (groupKey === prop) return label;
29
+ if (groupKey.endsWith('-' + prop)) {
30
+ const slot = groupKey.slice(0, groupKey.length - prop.length - 1);
31
+ return `${slot} ${label}`;
32
+ }
33
+ }
34
+ return label;
35
+ }
36
+
37
+ /** `Token` enriched by the linked-block computation. The base fields are inherited from
38
+ `Token`; the extra commentary on `mergeVariables` here is linked-block-specific. */
39
+ export interface LinkedToken extends Token {
40
+ /** Other groupKey lead variables whose current alias matches this row's. The row writes
41
+ the same alias to each of these (and their siblings) so the merged display stays in sync. */
42
+ mergeVariables?: string[];
43
+ }
44
+
45
+ export type LinkedGroup = {
46
+ token: LinkedToken;
47
+ /** Full set of contexts participating in this group (linked + broken), ordered by the
48
+ caller's `linkableContexts` insertion order so the LinkageChart row order matches the
49
+ variant tab strip. `brokenContexts` is a subset; the difference is currently linked. */
50
+ contexts: string[];
51
+ /** Subset of `contexts` whose alias has been overridden out of the linked group.
52
+ Renders as broken cells in the LinkageChart so the historical relationship stays visible. */
53
+ brokenContexts: string[];
54
+ variables: string[];
55
+ linked: boolean;
56
+ /** Reverse lookup from chart context label (e.g. "primary default") to the sibling variable
57
+ that backs that cell. Used by LinkedBlock to swap the row's displayed variable when the
58
+ user focuses a different variant/state, so the value and pop-bar reflect the cell the
59
+ user is looking at — not the group's first-seen representative. */
60
+ contextToVariable: Map<string, string>;
61
+ };
62
+
63
+ export type LinkedBlockResult = {
64
+ groups: LinkedGroup[];
65
+ varSet: Set<string>;
66
+ contextsByVar: Record<string, string[]>;
67
+ linkedOrder: Map<string, number>;
68
+ };
69
+
70
+ /**
71
+ * Compute linked-block groups for the given component.
72
+ * Each entry in `linkableContexts` maps a representative variable to a context label.
73
+ * A group is formed when ≥2 sibling variables (same component, same groupKey) exist.
74
+ *
75
+ * Reads editor state internally via `getComponentPropertySiblings` (which `get`s the store).
76
+ * Pass `$editorState` as the final argument so the call site subscribes and Svelte re-runs
77
+ * the reactive statement when state changes. The argument itself is ignored:
78
+ *
79
+ * $: linked = computeLinkedBlock(component, linkableContexts, allTokens, $editorState);
80
+ */
81
+ export function computeLinkedBlock(
82
+ component: string,
83
+ linkableContexts: Map<string, string>,
84
+ allTokens: LinkedToken[],
85
+ // Reactivity hook — see JSDoc. The runtime state is read via `get(store)` internally;
86
+ // this parameter exists only so callers can pass `$editorState` to create the subscription.
87
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
88
+ _stateForReactivity?: unknown,
89
+ ): LinkedBlockResult {
90
+ const groups: LinkedGroup[] = [];
91
+ const varSet = new Set<string>();
92
+ const seen = new Set<string>();
93
+ const seenGroupKeys = new Set<string>();
94
+ const tokensByVar = new Map(allTokens.map((t) => [t.variable, t]));
95
+
96
+ // Topology peers per groupKey (everything the editor declared as part of one sharing set).
97
+ // Used to mirror writes from a row's lead onto sibling variables that aren't yet in the
98
+ // slice, so a single user change propagates to the full declared topology.
99
+ const topologyByGroupKey = new Map<string, string[]>();
100
+ for (const [variable] of linkableContexts) {
101
+ const gk = tokensByVar.get(variable)?.groupKey;
102
+ if (!gk) continue;
103
+ const list = topologyByGroupKey.get(gk) ?? [];
104
+ list.push(variable);
105
+ topologyByGroupKey.set(gk, list);
106
+ }
107
+
108
+ for (const [variable] of linkableContexts) {
109
+ if (seen.has(variable)) continue;
110
+ const rep = tokensByVar.get(variable);
111
+ if (!rep) continue;
112
+ if (rep.groupKey && seenGroupKeys.has(rep.groupKey)) continue;
113
+ const siblings = getComponentPropertySiblings(component, variable);
114
+ if (siblings.length < 2) continue;
115
+ for (const s of siblings) seen.add(s);
116
+ if (rep.groupKey) {
117
+ seenGroupKeys.add(rep.groupKey);
118
+ // Mark every other declared peer in linkableContexts as seen so they don't spawn dupe groups
119
+ // when the slice covers only a subset of the declared topology.
120
+ for (const peer of topologyByGroupKey.get(rep.groupKey) ?? []) seen.add(peer);
121
+ }
122
+
123
+ const propertyLinked = isComponentPropertyLinked(component, variable);
124
+ const declaredPeers = rep.groupKey ? topologyByGroupKey.get(rep.groupKey) ?? [] : [];
125
+ const mergePeers = declaredPeers.filter((v) => v !== variable && !siblings.includes(v));
126
+
127
+ // Partition the *currently linked* siblings by alias. Explicitly unlinked
128
+ // siblings (those in `slice.unlinked`) are tracked separately and always
129
+ // render as broken in the chart — even when their alias happens to match
130
+ // the canonical value — because they've opted out of sharing.
131
+ const slice = get(editorState).components[component];
132
+ const aliases = slice?.aliases ?? {};
133
+ const unlinkedSet = new Set(slice?.unlinked ?? []);
134
+ const buckets = new Map<string, string[]>();
135
+ for (const s of siblings) {
136
+ if (unlinkedSet.has(s)) continue;
137
+ const k = aliasKey(aliases[s]);
138
+ const arr = buckets.get(k) ?? [];
139
+ arr.push(s);
140
+ buckets.set(k, arr);
141
+ }
142
+ // Canonical = representative's bucket (if it's linked). Promoted to a more
143
+ // populous *explicit-alias* bucket when one exists. The empty-key bucket
144
+ // (peers with no saved alias) only wins canonical when the representative
145
+ // itself has no saved alias — otherwise an unsaved peer would silently
146
+ // outvote a peer that the user actively set.
147
+ let canonicalKey = unlinkedSet.has(variable) ? '' : aliasKey(aliases[variable]);
148
+ let canonicalSize = buckets.get(canonicalKey)?.length ?? 0;
149
+ for (const [k, arr] of buckets) {
150
+ if (k === '' && canonicalKey !== '') continue;
151
+ if (arr.length > canonicalSize) {
152
+ canonicalKey = k;
153
+ canonicalSize = arr.length;
154
+ }
155
+ }
156
+ const linkedSiblings = buckets.get(canonicalKey) ?? [];
157
+ const divergedSiblings: string[] = [];
158
+ for (const [k, arr] of buckets) {
159
+ if (k !== canonicalKey) divergedSiblings.push(...arr);
160
+ }
161
+ const explicitlyUnlinked = siblings.filter((s) => unlinkedSet.has(s));
162
+
163
+ // Build context lists in canonical (linkableContexts insertion) order so the
164
+ // LinkageChart row order matches the variant tab strip the editor declared.
165
+ // `contextToVariable` records which sibling backs each cell, so LinkedBlock
166
+ // can swap the row's displayed variable to follow focusedVariant/focusedState.
167
+ const linkedSet = new Set(linkedSiblings);
168
+ const brokenVarSet = new Set([...divergedSiblings, ...mergePeers, ...explicitlyUnlinked]);
169
+ const siblingSet = new Set(siblings);
170
+ const seenAll = new Set<string>();
171
+ const seenBroken = new Set<string>();
172
+ const ctxs: string[] = [];
173
+ const brokenContexts: string[] = [];
174
+ const contextToVariable = new Map<string, string>();
175
+ for (const [peer, ctx] of linkableContexts) {
176
+ if (rep.groupKey && tokensByVar.get(peer)?.groupKey !== rep.groupKey) continue;
177
+ if (siblingSet.has(peer) && !contextToVariable.has(ctx)) {
178
+ contextToVariable.set(ctx, peer);
179
+ }
180
+ const isLinked = linkedSet.has(peer);
181
+ const isBroken = brokenVarSet.has(peer);
182
+ if (!isLinked && !isBroken) continue;
183
+ if (!seenAll.has(ctx)) {
184
+ seenAll.add(ctx);
185
+ ctxs.push(ctx);
186
+ }
187
+ if (isBroken && !seenBroken.has(ctx)) {
188
+ seenBroken.add(ctx);
189
+ brokenContexts.push(ctx);
190
+ }
191
+ }
192
+ const tok: LinkedToken = {
193
+ ...rep,
194
+ label: deriveLinkedLabel(rep.label, rep.groupKey),
195
+ canBeLinked: true,
196
+ ...(mergePeers.length ? { mergeVariables: mergePeers } : {}),
197
+ };
198
+ groups.push({ token: tok, contexts: ctxs, brokenContexts, variables: siblings, linked: propertyLinked, contextToVariable });
199
+ // Per-variant rows for linked siblings stay directly editable — writes fan
200
+ // out through the linked write path (`setComponentAliasLinked`) because
201
+ // `isLinkedDisplay` resolves true. The link topology is dev-declared, so
202
+ // we don't gate edits behind a "go use the linked block" detour; the
203
+ // linked-block row is the topology view, not the only edit point.
204
+ }
205
+
206
+ const contextsByVar = Object.fromEntries(groups.map((g) => [g.token.variable, g.contexts]));
207
+ const linkedOrder = new Map<string, number>(
208
+ groups.flatMap((g, i) => [
209
+ [g.token.variable, i] as const,
210
+ ...g.variables.map((v) => [v, i] as const),
211
+ ]),
212
+ );
213
+
214
+ return { groups, varSet, contextsByVar, linkedOrder };
215
+ }
216
+
217
+ /** No-op since the linked-block became a peer to per-variant editing rather
218
+ * than the single canonical edit point. Per-variant rows for linked properties
219
+ * stay fully interactive — writes route through `setComponentAliasLinked`
220
+ * when `isLinkedDisplay` resolves true, fanning out to all linked siblings.
221
+ *
222
+ * Kept as an identity passthrough so existing editor call sites compile
223
+ * without churn; remove the calls (and this export) when consolidating. */
224
+ export function withLinkedDisabled<T extends { variable: string }>(tokens: T[], _linked: Set<string>): T[] {
225
+ return tokens;
226
+ }
@@ -0,0 +1,33 @@
1
+ import type { Token, TypeGroupConfig } from './types';
2
+
3
+ export type Sibling = {
4
+ name: string;
5
+ label: string;
6
+ states: Record<string, Token[]>;
7
+ typeGroups?: Record<string, TypeGroupConfig[]>;
8
+ };
9
+
10
+ /** Build the `siblings` list a VariantGroup needs for its "Copy from" menu.
11
+ Given the full variant list and per-variant state-map builders, returns
12
+ every variant *except* `toVariant` shaped as a Sibling.
13
+
14
+ `variantStates(v)` and `variantTypeGroups(v)` return the same shape the
15
+ parent VariantGroup gets for its own `states` / `typeGroups` props — a map
16
+ keyed by state name. For single-state-per-variant editors (Badge, Notification,
17
+ ProgressBar), wrap the single-state builders inline:
18
+ `(v) => ({ [v]: variantTokens(v) })`. */
19
+ export function buildSiblings<V extends string>(
20
+ variants: readonly V[],
21
+ toVariant: V,
22
+ variantStates: (v: V) => Record<string, Token[]>,
23
+ variantTypeGroups?: (v: V) => Record<string, TypeGroupConfig[]>,
24
+ ): Sibling[] {
25
+ return variants
26
+ .filter((v) => v !== toVariant)
27
+ .map((v) => ({
28
+ name: v,
29
+ label: v.charAt(0).toUpperCase() + v.slice(1),
30
+ states: variantStates(v),
31
+ typeGroups: variantTypeGroups ? variantTypeGroups(v) : undefined,
32
+ }));
33
+ }
@@ -0,0 +1,39 @@
1
+ /** Editor token: a single CSS custom property the user can theme. */
2
+ export type Token = {
3
+ label: string;
4
+ variable: string;
5
+ /** When true, this token participates in the linked block when ≥2 variants agree on its value. */
6
+ canBeLinked?: boolean;
7
+ /** Used by the editor store to identify cross-variant counterparts (e.g. all `border-width` tokens link). */
8
+ groupKey?: string;
9
+ /** Token row is rendered in a disabled state (still visible). */
10
+ disabled?: boolean;
11
+ /** Token row is omitted entirely. */
12
+ hidden?: boolean;
13
+ /** When the linked block collapses several same-label same-value rows into one,
14
+ the surviving row carries the other groupKey leads here so writes co-propagate. */
15
+ mergeVariables?: string[];
16
+ };
17
+
18
+ /** Editor type-group: a fieldset containing a coordinated set of typography tokens
19
+ (text color + font-family/size/weight/line-height) for a piece of content
20
+ (e.g. a card title, notification body). Optional outline rows let
21
+ text-with-stroke groups (e.g. SectionDivider title) keep their stroke
22
+ width/color visually nested with the typography that drives them. */
23
+ export type TypeGroupConfig = {
24
+ legend?: string;
25
+ colorVariable: string;
26
+ colorLabel?: string;
27
+ familyVariable?: string;
28
+ familyLabel?: string;
29
+ sizeVariable?: string;
30
+ sizeLabel?: string;
31
+ weightVariable?: string;
32
+ weightLabel?: string;
33
+ lineHeightVariable?: string;
34
+ lineHeightLabel?: string;
35
+ outlineWidthVariable?: string;
36
+ outlineWidthLabel?: string;
37
+ outlineColorVariable?: string;
38
+ outlineColorLabel?: string;
39
+ };
@@ -1,10 +1,41 @@
1
+ <script context="module" lang="ts">
2
+ export const badgeVariants = [
3
+ 'primary',
4
+ 'accent',
5
+ 'neutral',
6
+ 'alternate',
7
+ 'canvas',
8
+ 'special',
9
+ 'success',
10
+ 'warning',
11
+ 'danger',
12
+ 'info',
13
+ ] as const;
14
+ export type BadgeVariant = typeof badgeVariants[number];
15
+ </script>
16
+
1
17
  <script lang="ts">
2
- export let variant: 'info' | 'accent' | 'trait' | 'hero' = 'info';
18
+ export let variant: BadgeVariant = 'info';
3
19
  export let size: 'default' | 'small' = 'default';
4
20
  export let icon: string | undefined = undefined;
21
+ /** When true, badge is absolutely positioned. Requires the parent to be position: relative. */
22
+ export let floating: boolean = false;
23
+ /** Corner of the positioned parent to attach to when floating. */
24
+ export let anchor: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'bottom-right';
25
+ /** CSS length to offset from the anchor edges. Defaults to --space-12 (or --space-8 when size='small'). */
26
+ export let offset: string | undefined = undefined;
27
+ /** When true, badge sits flush in the anchor corner: zero offset and squared corners. */
28
+ export let flush: boolean = false;
5
29
  </script>
6
30
 
7
- <span class="badge badge-{variant}" class:badge-small={size === 'small'}>
31
+ <span
32
+ class="badge badge-{variant}"
33
+ class:badge-small={size === 'small'}
34
+ class:badge-floating={floating}
35
+ class:badge-flush={flush}
36
+ data-anchor={floating ? anchor : undefined}
37
+ style={floating && offset && !flush ? `--badge-offset: ${offset};` : undefined}
38
+ >
8
39
  {#if icon}
9
40
  <span class="icon"><i class={icon}></i></span>
10
41
  {:else}
@@ -13,24 +44,185 @@
13
44
  <slot />
14
45
  </span>
15
46
 
16
- <style>
47
+ <style lang="scss">
48
+ @use '../styles/padding' as *;
49
+
50
+ $variants: info, accent, primary, success, warning, danger, neutral, special, alternate, canvas;
51
+
52
+ // Per-variant token block kept flat (not collapsed via SCSS @each) so the
53
+ // Layer-2 token-discovery parser (`extractGlobalRootBody` in
54
+ // src/lib/parsers/globalRootBlock.ts) can read the .svelte source verbatim;
55
+ // @each interpolation would make the parser see zero tokens for Badge,
56
+ // even though the rendered DOM would be identical. See parallel comment in
57
+ // Notification.svelte. `--text-primary` is the neutral primary text (with
58
+ // -secondary/-tertiary scale); `--text-brand` is the brand-family text.
59
+ :global(:root) {
60
+ /* Primary */
61
+ --badge-primary-surface: var(--surface-brand);
62
+ --badge-primary-text: var(--text-brand);
63
+ --badge-primary-border: var(--border-brand);
64
+ --badge-primary-text-font-family: var(--font-sans);
65
+ --badge-primary-text-font-size: var(--font-size-sm);
66
+ --badge-primary-text-font-weight: var(--font-weight-light);
67
+ --badge-primary-text-line-height: var(--line-height-tight);
68
+ --badge-primary-border-width: var(--border-width-1);
69
+ --badge-primary-radius: var(--radius-full);
70
+ --badge-primary-padding: var(--space-6);
71
+ --badge-primary-shadow: var(--shadow-none);
72
+ --badge-primary-blur: var(--blur-none);
73
+ --badge-primary-icon-size: var(--icon-size-sm);
74
+
75
+ /* Accent */
76
+ --badge-accent-surface: var(--surface-accent);
77
+ --badge-accent-text: var(--text-accent);
78
+ --badge-accent-border: var(--border-accent);
79
+ --badge-accent-text-font-family: var(--font-sans);
80
+ --badge-accent-text-font-size: var(--font-size-sm);
81
+ --badge-accent-text-font-weight: var(--font-weight-light);
82
+ --badge-accent-text-line-height: var(--line-height-tight);
83
+ --badge-accent-border-width: var(--border-width-1);
84
+ --badge-accent-radius: var(--radius-full);
85
+ --badge-accent-padding: var(--space-6);
86
+ --badge-accent-shadow: var(--shadow-none);
87
+ --badge-accent-blur: var(--blur-none);
88
+ --badge-accent-icon-size: var(--icon-size-sm);
89
+
90
+ /* Neutral */
91
+ --badge-neutral-surface: var(--surface-neutral);
92
+ --badge-neutral-text: var(--text-primary);
93
+ --badge-neutral-border: var(--border-neutral);
94
+ --badge-neutral-text-font-family: var(--font-sans);
95
+ --badge-neutral-text-font-size: var(--font-size-sm);
96
+ --badge-neutral-text-font-weight: var(--font-weight-light);
97
+ --badge-neutral-text-line-height: var(--line-height-tight);
98
+ --badge-neutral-border-width: var(--border-width-1);
99
+ --badge-neutral-radius: var(--radius-full);
100
+ --badge-neutral-padding: var(--space-6);
101
+ --badge-neutral-shadow: var(--shadow-none);
102
+ --badge-neutral-blur: var(--blur-none);
103
+ --badge-neutral-icon-size: var(--icon-size-sm);
104
+
105
+ /* Alternate */
106
+ --badge-alternate-surface: var(--surface-alternate);
107
+ --badge-alternate-text: var(--text-alternate);
108
+ --badge-alternate-border: var(--border-alternate);
109
+ --badge-alternate-text-font-family: var(--font-sans);
110
+ --badge-alternate-text-font-size: var(--font-size-sm);
111
+ --badge-alternate-text-font-weight: var(--font-weight-light);
112
+ --badge-alternate-text-line-height: var(--line-height-tight);
113
+ --badge-alternate-border-width: var(--border-width-1);
114
+ --badge-alternate-radius: var(--radius-full);
115
+ --badge-alternate-padding: var(--space-6);
116
+ --badge-alternate-shadow: var(--shadow-none);
117
+ --badge-alternate-blur: var(--blur-none);
118
+ --badge-alternate-icon-size: var(--icon-size-sm);
119
+
120
+ /* Canvas */
121
+ --badge-canvas-surface: var(--surface-canvas);
122
+ --badge-canvas-text: var(--text-canvas);
123
+ --badge-canvas-border: var(--border-canvas);
124
+ --badge-canvas-text-font-family: var(--font-sans);
125
+ --badge-canvas-text-font-size: var(--font-size-sm);
126
+ --badge-canvas-text-font-weight: var(--font-weight-light);
127
+ --badge-canvas-text-line-height: var(--line-height-tight);
128
+ --badge-canvas-border-width: var(--border-width-1);
129
+ --badge-canvas-radius: var(--radius-full);
130
+ --badge-canvas-padding: var(--space-6);
131
+ --badge-canvas-shadow: var(--shadow-none);
132
+ --badge-canvas-blur: var(--blur-none);
133
+ --badge-canvas-icon-size: var(--icon-size-sm);
134
+
135
+ /* Special */
136
+ --badge-special-surface: var(--surface-special);
137
+ --badge-special-text: var(--text-special);
138
+ --badge-special-border: var(--border-special);
139
+ --badge-special-text-font-family: var(--font-sans);
140
+ --badge-special-text-font-size: var(--font-size-sm);
141
+ --badge-special-text-font-weight: var(--font-weight-light);
142
+ --badge-special-text-line-height: var(--line-height-tight);
143
+ --badge-special-border-width: var(--border-width-1);
144
+ --badge-special-radius: var(--radius-full);
145
+ --badge-special-padding: var(--space-6);
146
+ --badge-special-shadow: var(--shadow-none);
147
+ --badge-special-blur: var(--blur-none);
148
+ --badge-special-icon-size: var(--icon-size-sm);
149
+
150
+ /* Success */
151
+ --badge-success-surface: var(--surface-success);
152
+ --badge-success-text: var(--text-success);
153
+ --badge-success-border: var(--border-success);
154
+ --badge-success-text-font-family: var(--font-sans);
155
+ --badge-success-text-font-size: var(--font-size-sm);
156
+ --badge-success-text-font-weight: var(--font-weight-light);
157
+ --badge-success-text-line-height: var(--line-height-tight);
158
+ --badge-success-border-width: var(--border-width-1);
159
+ --badge-success-radius: var(--radius-full);
160
+ --badge-success-padding: var(--space-6);
161
+ --badge-success-shadow: var(--shadow-none);
162
+ --badge-success-blur: var(--blur-none);
163
+ --badge-success-icon-size: var(--icon-size-sm);
164
+
165
+ /* Warning */
166
+ --badge-warning-surface: var(--surface-warning);
167
+ --badge-warning-text: var(--text-warning);
168
+ --badge-warning-border: var(--border-warning);
169
+ --badge-warning-text-font-family: var(--font-sans);
170
+ --badge-warning-text-font-size: var(--font-size-sm);
171
+ --badge-warning-text-font-weight: var(--font-weight-light);
172
+ --badge-warning-text-line-height: var(--line-height-tight);
173
+ --badge-warning-border-width: var(--border-width-1);
174
+ --badge-warning-radius: var(--radius-full);
175
+ --badge-warning-padding: var(--space-6);
176
+ --badge-warning-shadow: var(--shadow-none);
177
+ --badge-warning-blur: var(--blur-none);
178
+ --badge-warning-icon-size: var(--icon-size-sm);
179
+
180
+ /* Danger */
181
+ --badge-danger-surface: var(--surface-danger);
182
+ --badge-danger-text: var(--text-danger);
183
+ --badge-danger-border: var(--border-danger);
184
+ --badge-danger-text-font-family: var(--font-sans);
185
+ --badge-danger-text-font-size: var(--font-size-sm);
186
+ --badge-danger-text-font-weight: var(--font-weight-light);
187
+ --badge-danger-text-line-height: var(--line-height-tight);
188
+ --badge-danger-border-width: var(--border-width-1);
189
+ --badge-danger-radius: var(--radius-full);
190
+ --badge-danger-padding: var(--space-6);
191
+ --badge-danger-shadow: var(--shadow-none);
192
+ --badge-danger-blur: var(--blur-none);
193
+ --badge-danger-icon-size: var(--icon-size-sm);
194
+
195
+ /* Info */
196
+ --badge-info-surface: var(--surface-info);
197
+ --badge-info-text: var(--text-info);
198
+ --badge-info-border: var(--border-info);
199
+ --badge-info-text-font-family: var(--font-sans);
200
+ --badge-info-text-font-size: var(--font-size-sm);
201
+ --badge-info-text-font-weight: var(--font-weight-light);
202
+ --badge-info-text-line-height: var(--line-height-tight);
203
+ --badge-info-border-width: var(--border-width-1);
204
+ --badge-info-radius: var(--radius-full);
205
+ --badge-info-padding: var(--space-6);
206
+ --badge-info-shadow: var(--shadow-none);
207
+ --badge-info-blur: var(--blur-none);
208
+ --badge-info-icon-size: var(--icon-size-sm);
209
+ }
210
+
17
211
  .badge {
212
+ --badge-offset: var(--space-12);
18
213
  display: inline-flex;
19
214
  align-items: center;
20
215
  gap: var(--space-6);
21
- font-size: var(--font-sm);
22
- line-height: 1;
23
- padding: var(--space-6) var(--space-12);
24
- border-radius: var(--radius-full);
25
- font-family: var(--font-sans);
26
- font-weight: var(--font-weight-medium);
216
+ line-height: var(--line-height-tight);
27
217
  white-space: nowrap;
218
+ backdrop-filter: blur(var(--badge-blur, 0));
219
+ -webkit-backdrop-filter: blur(var(--badge-blur, 0));
28
220
  }
29
221
 
30
222
  .icon {
31
223
  display: inline-flex;
32
224
  align-items: center;
33
- font-size: 1em;
225
+ font-size: var(--badge-icon-size, 1em);
34
226
  }
35
227
 
36
228
  .icon:empty {
@@ -38,45 +230,42 @@
38
230
  }
39
231
 
40
232
  .badge-small {
41
- font-size: var(--font-xs);
42
- padding: var(--space-6) var(--space-12);
233
+ --badge-offset: var(--space-8);
43
234
  gap: var(--space-4);
44
235
  }
45
236
 
46
- /* Info (default) */
47
- .badge-info {
48
- color: var(--text-primary);
49
- background: var(--surface-neutral-higher);
50
- border: 1px solid var(--border-neutral-default);
51
- box-shadow: var(--shadow-sm);
52
- }
53
-
54
-
55
- /* Accent */
56
- .badge-accent {
57
- color: var(--color-accent-300);
58
- background: var(--surface-neutral-higher);
59
- border: 1px solid var(--border-accent);
60
- box-shadow: var(--shadow-sm);
61
- text-transform: capitalize;
237
+ .badge-floating {
238
+ position: absolute;
239
+ /* Floating badges are decorative overlays over other content — let clicks
240
+ pass through to whatever owns the underlying surface (links, buttons). */
241
+ pointer-events: none;
62
242
  }
63
243
 
244
+ .badge-floating[data-anchor='top-left'] { top: var(--badge-offset); left: var(--badge-offset); }
245
+ .badge-floating[data-anchor='top-right'] { top: var(--badge-offset); right: var(--badge-offset); }
246
+ .badge-floating[data-anchor='bottom-left'] { bottom: var(--badge-offset); left: var(--badge-offset); }
247
+ .badge-floating[data-anchor='bottom-right'] { bottom: var(--badge-offset); right: var(--badge-offset); }
64
248
 
65
- /* Hero */
66
- .badge-hero {
67
- color: #fff;
68
- background: rgba(0, 0, 0, 0.35);
69
- backdrop-filter: blur(8px);
70
- border: 1px solid rgba(255, 255, 255, 0.12);
71
- font-weight: var(--font-weight-semibold);
72
- letter-spacing: 0.02em;
249
+ @each $v in $variants {
250
+ .badge-#{$v} {
251
+ --badge-icon-size: var(--badge-#{$v}-icon-size);
252
+ --badge-blur: var(--badge-#{$v}-blur);
253
+ color: var(--badge-#{$v}-text);
254
+ background: var(--badge-#{$v}-surface);
255
+ border: var(--badge-#{$v}-border-width) solid var(--badge-#{$v}-border);
256
+ border-radius: var(--badge-#{$v}-radius);
257
+ @include themed-padding(--badge-#{$v}-padding, $h: 2);
258
+ font-family: var(--badge-#{$v}-text-font-family);
259
+ font-size: var(--badge-#{$v}-text-font-size);
260
+ font-weight: var(--badge-#{$v}-text-font-weight);
261
+ line-height: var(--badge-#{$v}-text-line-height);
262
+ box-shadow: var(--badge-#{$v}-shadow);
263
+ }
73
264
  }
74
265
 
75
- /* Trait */
76
- .badge-trait {
77
- color: var(--text-primary);
78
- background: var(--surface-primary-high);
79
- border: 1px solid var(--border-primary-strong);
80
- text-transform: capitalize;
266
+ /* Flush wins over per-variant radius via source order — declared after @each. */
267
+ .badge-flush {
268
+ --badge-offset: 0;
269
+ border-radius: 0;
81
270
  }
82
271
  </style>