@motion-proto/live-tokens 0.6.2 → 0.7.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 (212) hide show
  1. package/README.md +14 -13
  2. package/dist-plugin/index.cjs +147 -136
  3. package/dist-plugin/index.d.cts +1 -1
  4. package/dist-plugin/index.d.ts +1 -1
  5. package/dist-plugin/index.js +145 -135
  6. package/package.json +25 -40
  7. package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
  8. package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
  9. package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
  10. package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +3 -3
  11. package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
  12. package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
  13. package/src/editor/component-editor/ImageEditor.svelte +30 -0
  14. package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
  15. package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
  16. package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +64 -37
  17. package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
  18. package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
  19. package/src/{component-editor → editor/component-editor}/SectionDividerEditor.svelte +57 -84
  20. package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
  21. package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +16 -20
  22. package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
  23. package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
  24. package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
  25. package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
  26. package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +2 -2
  27. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
  28. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +144 -416
  29. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
  30. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
  31. package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
  32. package/src/{component-editor → editor/component-editor}/scaffolding/DividerEditor.svelte +1 -1
  33. package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
  34. package/src/{component-editor → editor/component-editor}/scaffolding/GradientCard.svelte +6 -6
  35. package/src/{component-editor → editor/component-editor}/scaffolding/LinkageChart.svelte +6 -6
  36. package/src/{component-editor → editor/component-editor}/scaffolding/LinkedBlock.svelte +6 -11
  37. package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
  38. package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
  39. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +72 -0
  40. package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
  41. package/src/editor/component-editor/scaffolding/StateBlock.svelte +257 -0
  42. package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +9 -7
  43. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +644 -0
  44. package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
  45. package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
  46. package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +14 -0
  47. package/src/{lib → editor/core/components}/componentConfigService.ts +2 -2
  48. package/src/{lib → editor/core/components}/componentPersist.ts +5 -5
  49. package/src/editor/core/flashStatus.ts +30 -0
  50. package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
  51. package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
  52. package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
  53. package/src/editor/core/manifests/manifestService.ts +116 -0
  54. package/src/{lib → editor/core/palettes}/paletteDerivation.ts +2 -2
  55. package/src/{lib → editor/core/palettes}/tokenRegistry.ts +5 -5
  56. package/src/editor/core/productionPulse.ts +37 -0
  57. package/src/{lib → editor/core/routing}/router.ts +1 -1
  58. package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
  59. package/src/{lib → editor/core/store}/editorCore.ts +24 -8
  60. package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
  61. package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
  62. package/src/{lib → editor/core/store}/editorStore.ts +17 -17
  63. package/src/{lib → editor/core/store}/editorTypes.ts +1 -1
  64. package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
  65. package/src/{lib → editor/core/themes}/slices/components.ts +2 -2
  66. package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
  67. package/src/{lib → editor/core/themes}/slices/gradients.ts +2 -2
  68. package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
  69. package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
  70. package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
  71. package/src/{lib → editor/core/themes}/themeInit.ts +6 -6
  72. package/src/{lib → editor/core/themes}/themeService.ts +6 -6
  73. package/src/{lib → editor/core/themes}/themeTypes.ts +11 -7
  74. package/src/editor/index.ts +69 -0
  75. package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +79 -125
  76. package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
  77. package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
  78. package/src/{pages → editor/pages}/Editor.svelte +4 -4
  79. package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
  80. package/src/{styles → editor/styles}/ui-editor.css +41 -21
  81. package/src/{styles → editor/styles}/ui-form-controls.css +8 -8
  82. package/src/{ui → editor/ui}/BezierCurveEditor.svelte +8 -8
  83. package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
  84. package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +8 -6
  85. package/src/editor/ui/FileLoadList.svelte +350 -0
  86. package/src/editor/ui/FilePill.svelte +80 -0
  87. package/src/{ui → editor/ui}/FontStackEditor.svelte +7 -7
  88. package/src/{ui → editor/ui}/GradientEditor.svelte +11 -11
  89. package/src/{ui → editor/ui}/GradientStopPicker.svelte +1 -1
  90. package/src/editor/ui/ManifestFileManager.svelte +371 -0
  91. package/src/{ui → editor/ui}/PaletteEditor.svelte +132 -598
  92. package/src/{ui → editor/ui}/ProjectFontsSection.svelte +102 -144
  93. package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
  94. package/src/{ui → editor/ui}/TextTab.svelte +3 -3
  95. package/src/{ui → editor/ui}/ThemeFileManager.svelte +286 -519
  96. package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
  97. package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -6
  98. package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +1 -1
  99. package/src/editor/ui/UIInfoPopover.svelte +244 -0
  100. package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
  101. package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
  102. package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
  103. package/src/{ui → editor/ui}/UIPaletteSelector.svelte +26 -26
  104. package/src/editor/ui/UIPillButton.svelte +138 -0
  105. package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
  106. package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
  107. package/src/editor/ui/UISquareButton.svelte +172 -0
  108. package/src/{ui → editor/ui}/UITokenSelector.svelte +10 -10
  109. package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
  110. package/src/{ui → editor/ui}/VariablesTab.svelte +31 -8
  111. package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
  112. package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +13 -13
  113. package/src/{ui → editor/ui}/palette/PaletteBase.svelte +8 -5
  114. package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
  115. package/src/editor/ui/palette/paletteMath.ts +275 -0
  116. package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -17
  117. package/src/{ui → editor/ui}/sections/GradientsSection.svelte +7 -7
  118. package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +17 -17
  119. package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +22 -22
  120. package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
  121. package/src/{components → system/components}/Badge.svelte +0 -36
  122. package/src/{components → system/components}/Card.svelte +8 -62
  123. package/src/{components → system/components}/CornerBadge.svelte +8 -24
  124. package/src/{components → system/components}/Dialog.svelte +1 -1
  125. package/src/system/components/FloatingTokenTags.css +256 -0
  126. package/src/system/components/FloatingTokenTags.svelte +592 -0
  127. package/src/{components → system/components}/InlineEditActions.svelte +6 -4
  128. package/src/system/components/MenuSelect.svelte +229 -0
  129. package/src/{components → system/components}/ProgressBar.svelte +29 -11
  130. package/src/{components → system/components}/SegmentedControl.svelte +49 -43
  131. package/src/{components → system/components}/TabBar.svelte +81 -65
  132. package/src/{components → system/components}/Table.svelte +17 -3
  133. package/src/{components → system/components}/Tooltip.svelte +6 -4
  134. package/src/system/styles/CONVENTIONS.md +178 -0
  135. package/src/{styles → system/styles}/fonts.css +6 -3
  136. package/src/{styles → system/styles}/tokens.css +149 -29
  137. package/src/component-editor/ImageEditor.svelte +0 -74
  138. package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
  139. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
  140. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
  141. package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
  142. package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
  143. package/src/data/google-fonts.json +0 -75
  144. package/src/lib/index.ts +0 -68
  145. package/src/lib/presetService.ts +0 -214
  146. package/src/lib/productionPulse.ts +0 -32
  147. package/src/ui/PresetFileManager.svelte +0 -1116
  148. package/src/ui/UnsavedComponentsDialog.svelte +0 -315
  149. /package/src/{styles → app}/site.css +0 -0
  150. /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
  151. /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
  152. /package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +0 -0
  153. /package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +0 -0
  154. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
  155. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
  156. /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
  157. /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
  158. /package/src/{lib → editor/core/components}/componentConfigKeys.ts +0 -0
  159. /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
  160. /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
  161. /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
  162. /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
  163. /package/src/{lib → editor/core/storage}/storage.ts +0 -0
  164. /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
  165. /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
  166. /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
  167. /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
  168. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
  169. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
  170. /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
  171. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
  172. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
  173. /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
  174. /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
  175. /package/src/{lib → editor/core/themes}/migrations/index.ts +0 -0
  176. /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
  177. /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
  178. /package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -0
  179. /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
  180. /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
  181. /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
  182. /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
  183. /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
  184. /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
  185. /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
  186. /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
  187. /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
  188. /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
  189. /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
  190. /package/src/{ui → editor/ui}/index.ts +0 -0
  191. /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
  192. /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
  193. /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
  194. /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
  195. /package/src/{ui → editor/ui}/variantScales.ts +0 -0
  196. /package/src/{assets → system/assets}/newspaper.webp +0 -0
  197. /package/src/{assets → system/assets}/offering.webp +0 -0
  198. /package/src/{components → system/components}/Button.svelte +0 -0
  199. /package/src/{components → system/components}/Callout.svelte +0 -0
  200. /package/src/{components → system/components}/CollapsibleSection.svelte +0 -0
  201. /package/src/{components → system/components}/Image.svelte +0 -0
  202. /package/src/{components → system/components}/Notification.svelte +0 -0
  203. /package/src/{components → system/components}/RadioButton.svelte +0 -0
  204. /package/src/{components → system/components}/SectionDivider.svelte +0 -0
  205. /package/src/{components → system/components}/types.ts +0 -0
  206. /package/src/{styles → system/styles}/_padding.scss +0 -0
  207. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  208. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  209. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  210. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  211. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  212. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
@@ -0,0 +1,644 @@
1
+ <script lang="ts">
2
+ import { writable } from 'svelte/store';
3
+ import type { Snippet } from 'svelte';
4
+ import TokenLayout from './TokenLayout.svelte';
5
+ import StateBlock from './StateBlock.svelte';
6
+ import CopyFromMenu from './CopyFromMenu.svelte';
7
+ import ShadowBackdrop from './ShadowBackdrop.svelte';
8
+ import ShadowBackdropControls from './ShadowBackdropControls.svelte';
9
+ import { mutate } from '../../core/store/editorStore';
10
+ import { getDeclaredValue } from '../../core/palettes/tokenRegistry';
11
+ import type { CssVarRef } from '../../core/store/editorTypes';
12
+ import { getEditorContext } from './editorContext';
13
+ import type { Token, TypeGroupConfig } from './types';
14
+ import type { Sibling } from './siblings';
15
+
16
+ interface Props {
17
+ name: string;
18
+ title: string;
19
+ tokens?: Token[];
20
+ states?: Record<string, Token[]> | null;
21
+ typeGroups?: Record<string, TypeGroupConfig[]>;
22
+ /** When set, overrides are read from and cleared through the editor store. */
23
+ component?: string | undefined;
24
+ siblings?: Sibling[];
25
+ columns?: number;
26
+ /** Defaults to "Element" because most strips mix structural parts with states. */
27
+ selectorLabel?: string;
28
+ onchange?: () => void;
29
+ children?: Snippet<[{ activeState: string }]>;
30
+ stateActions?: Snippet<[string]>;
31
+ previewActions?: Snippet;
32
+ compositeControls?: Snippet<[string]>;
33
+ /** Each child should be a `.property-row` to match the token grid above. */
34
+ extraPropertyRows?: Snippet<[string]>;
35
+ /** Extra sections appended below the Background controls inside the canvas
36
+ toolbar. Use `.canvas-toolbar-eyebrow` for section headings to match
37
+ Background. Lets per-instance display knobs (anchor, alignment, etc.)
38
+ live with the canvas rather than in a separate config block above. */
39
+ canvasToolbarExtras?: Snippet;
40
+ /** Skip the default centered, padded stage when the editor brings its own backdrop. */
41
+ unboxedPreview?: boolean;
42
+ backdropPadding?: string;
43
+ backdropModes?: ReadonlyArray<'default' | 'image' | 'color'>;
44
+ }
45
+
46
+ let {
47
+ name,
48
+ title,
49
+ tokens = [],
50
+ states = null,
51
+ typeGroups = {},
52
+ component = undefined,
53
+ siblings = [],
54
+ columns = 1,
55
+ selectorLabel = 'Element',
56
+ onchange,
57
+ children,
58
+ stateActions,
59
+ previewActions,
60
+ compositeControls,
61
+ extraPropertyRows,
62
+ canvasToolbarExtras,
63
+ unboxedPreview = false,
64
+ backdropPadding,
65
+ backdropModes,
66
+ }: Props = $props();
67
+
68
+ let bgMode: 'default' | 'image' | 'color' = $state('default');
69
+ let bgVar = $derived(`--backdrop-${component ?? name}-surface`);
70
+
71
+ const editorCtx = getEditorContext();
72
+ const linkedOrderStore = editorCtx?.linkedOrder ?? writable<Map<string, number> | null>(null);
73
+ const focusedVariantStore = editorCtx?.focusedVariant ?? writable<string | null>(null);
74
+ const focusedStateStore = editorCtx?.focusedState ?? writable<string | null>(null);
75
+ const variantsStore = editorCtx?.variants ?? writable<{ value: string; label: string }[]>([]);
76
+ const preserveColorFamilyStore = editorCtx?.preserveColorFamily ?? writable(false);
77
+ let linkedOrder = $derived($linkedOrderStore ?? undefined);
78
+
79
+ let activeTab: string = $state('');
80
+
81
+ const TYPE_PROPS = ['colorVariable', 'familyVariable', 'sizeVariable', 'weightVariable', 'lineHeightVariable', 'outlineWidthVariable', 'outlineColorVariable'] as const;
82
+ // Carry per-side derived vars so split padding fully transfers; no-op when absent.
83
+ const PADDING_SIDES = ['top', 'right', 'bottom', 'left'] as const;
84
+
85
+ /** A token whose label contains "color" describes a color property. The
86
+ copy-from "Preserve color families" toggle skips these so the destination
87
+ keeps its existing palette family (e.g. button-primary stays on `brand`)
88
+ while still picking up shape/typography from the source variant. */
89
+ function isColorToken(t: Token): boolean {
90
+ return t.label.toLowerCase().includes('color');
91
+ }
92
+
93
+ /** Parse the right-hand side of a CSS declaration (e.g. `var(--surface-accent-lowest)`
94
+ or `#ff0000`) into a CssVarRef. Returns null for empty/unparseable input.
95
+ Used by copy-from when the source has no explicit override — we resolve
96
+ its declared default so the destination visually matches the source
97
+ instead of falling back to its own family's default. */
98
+ function declaredToRef(declared: string | null): CssVarRef | null {
99
+ if (!declared) return null;
100
+ const m = declared.match(/^\s*var\((--[a-z0-9-]+)\)\s*$/i);
101
+ if (m) return { kind: 'token', name: m[1] };
102
+ return { kind: 'literal', value: declared };
103
+ }
104
+
105
+ /** Extract a transparency percentage from a `color-mix(in srgb, var(--X) N%, transparent)`
106
+ wrapper, optionally itself wrapped in `var(...)`. Returns null if no
107
+ such wrapper is present. */
108
+ const ALPHA_RE = /color-mix\s*\(\s*in\s+srgb\s*,\s*var\(--[a-z0-9-]+\)\s+([\d.]+%)\s*,\s*transparent\s*\)/i;
109
+ function extractAlpha(value: string): string | null {
110
+ const m = value.match(ALPHA_RE);
111
+ return m ? m[1] : null;
112
+ }
113
+
114
+ /** Extract the inner var() base reference, whether the value is a bare
115
+ `var(--X)`, or wrapped by a transparency color-mix, or both. */
116
+ function extractBaseToken(value: string): string | null {
117
+ const mix = value.match(/color-mix\s*\(\s*in\s+srgb\s*,\s*var\((--[a-z0-9-]+)\)/i);
118
+ if (mix) return mix[1];
119
+ const bare = value.match(/var\((--[a-z0-9-]+)\)/i);
120
+ return bare ? bare[1] : null;
121
+ }
122
+
123
+ /** TypeGroup props whose values are color tokens; preserve-color-families
124
+ skips these in the typeGroups copy loop. */
125
+ const COLOR_TYPE_PROPS = new Set(['colorVariable', 'outlineColorVariable']);
126
+
127
+ function pickCopySource(toState: string, fromVariant: string, fromState: string) {
128
+ const preserveColorFamily = $preserveColorFamilyStore;
129
+ if (!component || !states) return;
130
+ const isSelfVariant = fromVariant === name;
131
+ const sibling = isSelfVariant ? null : siblings.find((s) => s.name === fromVariant);
132
+ if (!isSelfVariant && !sibling) return;
133
+ const srcTokens = (isSelfVariant ? states[fromState] : sibling!.states[fromState]) ?? [];
134
+ const dstTokens = states[toState] ?? [];
135
+ const srcTypeGroups = (isSelfVariant ? typeGroups[fromState] : sibling!.typeGroups?.[fromState]) ?? [];
136
+ const dstTypeGroups = typeGroups[toState] ?? [];
137
+
138
+ mutate(`copy ${fromVariant}/${fromState} → ${name}/${toState}`, (s) => {
139
+ const slice = s.components[component!] ?? (s.components[component!] = { activeFile: 'default', aliases: {}, config: {} });
140
+ const dstVarsTouched: string[] = [];
141
+ /** Resolve a variable's effective value as a CSS string: the override if
142
+ set, otherwise its declared default. Returns null if neither exists. */
143
+ const effectiveValue = (varName: string): string | null => {
144
+ const ref = slice.aliases[varName];
145
+ if (ref) return ref.kind === 'token' ? `var(${ref.name})` : ref.value;
146
+ return getDeclaredValue(varName);
147
+ };
148
+
149
+ const apply = (srcVar: string, dstVar: string) => {
150
+ if (srcVar === dstVar) return;
151
+ if (srcVar in slice.aliases) {
152
+ slice.aliases[dstVar] = slice.aliases[srcVar];
153
+ } else {
154
+ // Src is at its declared default. Materialize that default as dst's
155
+ // override so dst visually picks up src's value — otherwise dst
156
+ // falls back to its OWN family default and the copy is a visual no-op.
157
+ const ref = declaredToRef(getDeclaredValue(srcVar));
158
+ if (ref) slice.aliases[dstVar] = ref;
159
+ else delete slice.aliases[dstVar];
160
+ }
161
+ dstVarsTouched.push(dstVar);
162
+ };
163
+
164
+ /** Preserve-color-families variant of apply: copy src's transparency
165
+ wrapper (if any) over dst's existing base color so dst keeps its
166
+ palette family but picks up src's alpha. Source with no alpha = no-op
167
+ (leaves dst's color untouched). */
168
+ const applyColorPreserve = (srcVar: string, dstVar: string) => {
169
+ const srcVal = effectiveValue(srcVar);
170
+ if (!srcVal) return;
171
+ const alpha = extractAlpha(srcVal);
172
+ if (!alpha) return;
173
+ const dstVal = effectiveValue(dstVar);
174
+ const dstBase = dstVal ? extractBaseToken(dstVal) : null;
175
+ if (!dstBase) return;
176
+ slice.aliases[dstVar] = {
177
+ kind: 'literal',
178
+ value: `color-mix(in srgb, var(${dstBase}) ${alpha}, transparent)`,
179
+ };
180
+ dstVarsTouched.push(dstVar);
181
+ };
182
+ const minLen = Math.min(srcTokens.length, dstTokens.length);
183
+ for (let i = 0; i < minLen; i++) {
184
+ const srcVar = srcTokens[i].variable;
185
+ const dstVar = dstTokens[i].variable;
186
+ if (preserveColorFamily && isColorToken(srcTokens[i])) {
187
+ applyColorPreserve(srcVar, dstVar);
188
+ continue;
189
+ }
190
+ apply(srcVar, dstVar);
191
+ if (srcTokens[i].splittable !== false) {
192
+ for (const side of PADDING_SIDES) apply(`${srcVar}-${side}`, `${dstVar}-${side}`);
193
+ }
194
+ }
195
+ const minTypeGroups = Math.min(srcTypeGroups.length, dstTypeGroups.length);
196
+ for (let g = 0; g < minTypeGroups; g++) {
197
+ const srcType = srcTypeGroups[g];
198
+ const dstType = dstTypeGroups[g];
199
+ for (const prop of TYPE_PROPS) {
200
+ const srcVar = srcType[prop];
201
+ const dstVar = dstType[prop];
202
+ if (!srcVar || !dstVar) continue;
203
+ if (preserveColorFamily && COLOR_TYPE_PROPS.has(prop)) {
204
+ applyColorPreserve(srcVar, dstVar);
205
+ continue;
206
+ }
207
+ apply(srcVar, dstVar);
208
+ }
209
+ }
210
+ // Copy intent "make this state match" clears stale unlinked markers on touched vars.
211
+ if (slice.unlinked && slice.unlinked.length > 0) {
212
+ const touched = new Set(dstVarsTouched);
213
+ const remaining = slice.unlinked.filter((v) => !touched.has(v));
214
+ if (remaining.length === 0) delete slice.unlinked;
215
+ else slice.unlinked = remaining;
216
+ }
217
+ });
218
+ }
219
+
220
+ let copySources = $derived.by(() => {
221
+ const fromSiblings = siblings.map((s) => ({
222
+ name: s.name,
223
+ label: s.label,
224
+ states: Object.keys(s.states),
225
+ }));
226
+ const ownStates = states ? Object.keys(states) : [];
227
+ if (ownStates.length >= 2) {
228
+ return [{ name, label: title, states: ownStates }, ...fromSiblings];
229
+ }
230
+ return fromSiblings;
231
+ });
232
+
233
+ let stateNames = $derived(states ? Object.keys(states) : []);
234
+ let tabsStripVisible = $derived(stateNames.length >= 2);
235
+ $effect(() => {
236
+ if (stateNames.length > 0 && !stateNames.includes(activeTab)) {
237
+ activeTab = stateNames[0];
238
+ }
239
+ });
240
+ // Adopt cross-group state hint from chart row clicks.
241
+ $effect(() => {
242
+ if ($focusedStateStore && stateNames.includes($focusedStateStore) && activeTab !== $focusedStateStore) {
243
+ activeTab = $focusedStateStore;
244
+ }
245
+ });
246
+
247
+ let inFocusMode = $derived(siblings.length > 0);
248
+ let amIFocused = $derived($focusedVariantStore === name);
249
+ let shouldRender = $derived(!inFocusMode || amIFocused);
250
+ let showVariantTabs = $derived(inFocusMode && $variantsStore.length >= 2);
251
+ // Mirror state-tab clicks to the shared store so linked-block + chart track them.
252
+ $effect(() => {
253
+ if (amIFocused && activeTab && stateNames.includes(activeTab) && $focusedStateStore !== activeTab) {
254
+ focusedStateStore.set(activeTab);
255
+ }
256
+ });
257
+
258
+ </script>
259
+
260
+ {#if shouldRender}
261
+ <div class="editor-section-card demo-section variant-group">
262
+ {#if states}
263
+ <div class="tabs-preview">
264
+ <div class="preview-header">
265
+ <span class="editor-section-title">Preview</span>
266
+ {#if showVariantTabs}
267
+ <div class="variant-tabs" role="tablist">
268
+ {#each $variantsStore as opt (opt.value)}
269
+ <button
270
+ type="button"
271
+ class="variant-tab-btn"
272
+ class:active={opt.value === $focusedVariantStore}
273
+ role="tab"
274
+ aria-selected={opt.value === $focusedVariantStore}
275
+ onclick={() => focusedVariantStore.set(opt.value)}
276
+ >{opt.label}</button>
277
+ {/each}
278
+ </div>
279
+ {/if}
280
+ {#if previewActions}
281
+ <div class="preview-actions">
282
+ {@render previewActions?.()}
283
+ </div>
284
+ {/if}
285
+ </div>
286
+ {#if unboxedPreview}
287
+ {@render children?.({ activeState: activeTab })}
288
+ {:else}
289
+ <ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
290
+ {#snippet controls()}
291
+ <div class="canvas-toolbar">
292
+ <span class="canvas-toolbar-eyebrow">Background</span>
293
+ <ShadowBackdropControls bind:mode={bgMode} colorVariable={bgVar} modes={backdropModes ?? ['default', 'image', 'color']} />
294
+ {@render canvasToolbarExtras?.()}
295
+ </div>
296
+ {/snippet}
297
+ {@render children?.({ activeState: activeTab })}
298
+ </ShadowBackdrop>
299
+ {/if}
300
+ </div>
301
+
302
+ {#if tabsStripVisible}
303
+ <div class="tabs-states-block">
304
+ <span class="editor-subsection-title">{selectorLabel}</span>
305
+ <div class="tabs-selectors">
306
+ <div class="state-tabs" role="tablist">
307
+ {#each stateNames as s}
308
+ <button
309
+ type="button"
310
+ class="state-tab-btn"
311
+ class:active={activeTab === s}
312
+ role="tab"
313
+ aria-selected={activeTab === s}
314
+ onclick={() => { activeTab = s; focusedStateStore.set(s); }}
315
+ >{s}</button>
316
+ {/each}
317
+ </div>
318
+ {#if activeTab}
319
+ {@render stateActions?.(activeTab)}
320
+ {/if}
321
+ </div>
322
+ </div>
323
+ {/if}
324
+
325
+ {#if activeTab && states[activeTab]}
326
+ {@const stateName = activeTab}
327
+ {@render compositeControls?.(stateName)}
328
+ <div class="properties-header">
329
+ <span class="editor-subsection-title properties-title">Properties</span>
330
+ {#if !tabsStripVisible && stateActions}
331
+ {@render stateActions(activeTab)}
332
+ {/if}
333
+ {#if copySources.length > 0}
334
+ <CopyFromMenu
335
+ toState={activeTab}
336
+ variantName={name}
337
+ {copySources}
338
+ onselect={(d) => pickCopySource(activeTab, d.fromVariant, d.fromState)}
339
+ />
340
+ {/if}
341
+ </div>
342
+ <StateBlock
343
+ tokens={states[stateName]}
344
+ typeGroups={typeGroups[stateName] ?? []}
345
+ {component}
346
+ {linkedOrder}
347
+ {columns}
348
+ {onchange}
349
+ />
350
+ {#if extraPropertyRows}
351
+ <div class="extra-property-rows">
352
+ {@render extraPropertyRows(stateName)}
353
+ </div>
354
+ {/if}
355
+ {/if}
356
+ {:else}
357
+ {#if unboxedPreview}
358
+ {@render children?.({ activeState: '' })}
359
+ {:else}
360
+ <ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
361
+ {@render children?.({ activeState: '' })}
362
+ </ShadowBackdrop>
363
+ {/if}
364
+ <TokenLayout
365
+ title={name}
366
+ tokens={tokens}
367
+ {component}
368
+ {linkedOrder}
369
+ {onchange}
370
+ />
371
+ {/if}
372
+
373
+ </div>
374
+ {/if}
375
+
376
+ <style>
377
+ /* Card chrome lives on .editor-section-card in ui-editor.css. */
378
+ .variant-group {
379
+ gap: var(--ui-space-12);
380
+ }
381
+
382
+ .tabs-preview {
383
+ display: flex;
384
+ flex-direction: column;
385
+ gap: var(--ui-space-20);
386
+ }
387
+
388
+ .preview-header {
389
+ display: flex;
390
+ align-items: center;
391
+ gap: var(--ui-space-24);
392
+ flex-wrap: wrap;
393
+ }
394
+
395
+ .preview-actions {
396
+ margin-left: auto;
397
+ display: inline-flex;
398
+ align-items: center;
399
+ gap: var(--ui-space-8);
400
+ }
401
+
402
+ /* Sits in the backdrop's right-rail column (ShadowBackdrop owns the two-column
403
+ split). Own border + raised surface so it reads as a distinct panel inside
404
+ the backdrop, regardless of which backdrop mode is active. */
405
+ .canvas-toolbar {
406
+ width: 11rem;
407
+ height: 100%;
408
+ display: flex;
409
+ flex-direction: column;
410
+ gap: var(--ui-space-12);
411
+ padding: var(--ui-space-10) var(--ui-space-12);
412
+ background: var(--ui-surface-low);
413
+ border: 1px solid var(--ui-border-low);
414
+ border-radius: var(--ui-radius-md);
415
+ color: var(--ui-text-primary);
416
+ box-sizing: border-box;
417
+ }
418
+
419
+ .canvas-toolbar :global(.canvas-toolbar-eyebrow) {
420
+ font-size: var(--ui-font-size-xs);
421
+ font-weight: var(--ui-font-weight-medium);
422
+ color: var(--ui-text-tertiary);
423
+ }
424
+
425
+ /* Divider lives *between* sections, not under each eyebrow. First eyebrow
426
+ has no preceding sibling and stays flush. */
427
+ .canvas-toolbar > :global(* + .canvas-toolbar-eyebrow) {
428
+ padding-top: var(--ui-space-12);
429
+ border-top: 1px solid var(--ui-border-low);
430
+ }
431
+
432
+ /* Explicit separator for label-less sections in canvasToolbarExtras. */
433
+ .canvas-toolbar :global(.canvas-toolbar-divider) {
434
+ margin: 0;
435
+ border: 0;
436
+ border-top: 1px solid var(--ui-border-low);
437
+ }
438
+
439
+ /* Native <select> styled to match the property-row trigger chrome
440
+ (UITokenSelector) so toolbar selects don't read as a separate visual
441
+ vocabulary from the rest of the editor. */
442
+ .canvas-toolbar :global(.canvas-toolbar-select) {
443
+ width: 100%;
444
+ appearance: none;
445
+ -webkit-appearance: none;
446
+ padding: 0 var(--ui-space-24) 0 var(--ui-space-8);
447
+ min-height: 1.75rem;
448
+ background-color: var(--ui-surface-low);
449
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23999' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M3 5l3 3 3-3'/%3E%3C/svg%3E");
450
+ background-repeat: no-repeat;
451
+ background-position: right var(--ui-space-8) center;
452
+ border: 1px solid var(--ui-border);
453
+ border-radius: var(--ui-radius-md);
454
+ color: var(--ui-text-primary);
455
+ font-family: var(--ui-font-sans);
456
+ font-size: var(--ui-font-size-sm);
457
+ cursor: pointer;
458
+ transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
459
+ }
460
+ .canvas-toolbar :global(.canvas-toolbar-select:hover) {
461
+ background-color: var(--ui-surface-high);
462
+ border-color: var(--ui-border-higher);
463
+ }
464
+ .canvas-toolbar :global(.canvas-toolbar-select:focus-visible) {
465
+ outline: 2px solid var(--ui-highlight);
466
+ outline-offset: 2px;
467
+ }
468
+
469
+ /* Native <input> styled to match the toolbar's select chrome. */
470
+ .canvas-toolbar :global(.canvas-toolbar-input) {
471
+ width: 100%;
472
+ padding: 0 var(--ui-space-8);
473
+ min-height: 1.75rem;
474
+ background: var(--ui-surface-low);
475
+ border: 1px solid var(--ui-border);
476
+ border-radius: var(--ui-radius-md);
477
+ color: var(--ui-text-primary);
478
+ font-family: var(--ui-font-sans);
479
+ font-size: var(--ui-font-size-sm);
480
+ transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
481
+ }
482
+ .canvas-toolbar :global(.canvas-toolbar-input:hover) {
483
+ border-color: var(--ui-border-higher);
484
+ }
485
+ .canvas-toolbar :global(.canvas-toolbar-input:focus-visible) {
486
+ outline: 2px solid var(--ui-highlight);
487
+ outline-offset: 2px;
488
+ }
489
+
490
+ /* Pill each option row so Default/Image match the swatch trigger's weight.
491
+ One step below the toolbar's --ui-surface-low so option chrome stays visible
492
+ against the panel. */
493
+ .canvas-toolbar :global(.backdrop-option) {
494
+ padding: 0 var(--ui-space-8);
495
+ background: var(--ui-surface-lowest);
496
+ border: 1px solid var(--ui-border-low);
497
+ border-radius: var(--ui-radius-md);
498
+ color: var(--ui-text-secondary);
499
+ }
500
+ .canvas-toolbar :global(.backdrop-option.checked) {
501
+ background: var(--ui-surface-high);
502
+ border-color: var(--ui-border);
503
+ color: var(--ui-text-primary);
504
+ }
505
+
506
+ /* Drop the swatch trigger's frame to avoid a double-border inside the option pill. */
507
+ .canvas-toolbar :global(.backdrop-option .ui-ts-trigger) {
508
+ background: transparent;
509
+ border-color: transparent;
510
+ padding-left: 0;
511
+ padding-right: 0;
512
+ }
513
+ .canvas-toolbar :global(.backdrop-option .ui-ts-trigger:hover) {
514
+ background: var(--ui-hover);
515
+ border-color: transparent;
516
+ }
517
+
518
+ .canvas-toolbar :global(.ui-ts-meta-text) {
519
+ color: var(--ui-text-tertiary);
520
+ }
521
+
522
+ /* Anchor dropdown to the trigger's right edge; the toolbar sits flush against
523
+ the editor's right gutter, so a left-anchored dropdown would clip. */
524
+ .canvas-toolbar :global(.ui-ts-dropdown) {
525
+ left: auto;
526
+ right: 0;
527
+ }
528
+
529
+ .variant-tabs {
530
+ display: inline-flex;
531
+ flex-wrap: wrap;
532
+ gap: var(--ui-space-4);
533
+ padding: var(--ui-space-4);
534
+ background: var(--ui-surface-lowest);
535
+ border: 1px solid var(--ui-border-low);
536
+ border-radius: var(--ui-radius-md);
537
+ }
538
+
539
+ .variant-tab-btn {
540
+ padding: var(--ui-space-6) var(--ui-space-12);
541
+ background: none;
542
+ border: none;
543
+ border-radius: var(--ui-radius-sm);
544
+ color: var(--ui-text-secondary);
545
+ font-size: var(--ui-font-size-md);
546
+ font-weight: var(--ui-font-weight-semibold);
547
+ text-transform: capitalize;
548
+ cursor: pointer;
549
+ transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
550
+ }
551
+
552
+ .variant-tab-btn:hover:not(.active) {
553
+ color: var(--ui-text-primary);
554
+ background: var(--ui-hover);
555
+ }
556
+
557
+ .variant-tab-btn.active {
558
+ color: var(--ui-text-primary);
559
+ background: var(--ui-surface-high);
560
+ box-shadow: 0 0 0 1px var(--ui-border);
561
+ }
562
+
563
+ .tabs-states-block {
564
+ display: flex;
565
+ flex-direction: column;
566
+ gap: var(--ui-space-8);
567
+ }
568
+
569
+ .tabs-selectors {
570
+ display: flex;
571
+ align-items: center;
572
+ gap: var(--ui-space-12);
573
+ }
574
+
575
+ .state-tabs {
576
+ display: inline-flex;
577
+ flex-wrap: wrap;
578
+ gap: var(--ui-space-4);
579
+ padding: var(--ui-space-4);
580
+ background: var(--ui-surface-lowest);
581
+ border: 1px solid var(--ui-border-low);
582
+ border-radius: var(--ui-radius-md);
583
+ }
584
+
585
+ .state-tab-btn {
586
+ padding: var(--ui-space-6) var(--ui-space-12);
587
+ background: none;
588
+ border: none;
589
+ border-radius: var(--ui-radius-sm);
590
+ color: var(--ui-text-secondary);
591
+ font-size: var(--ui-font-size-sm);
592
+ font-weight: var(--ui-font-weight-medium);
593
+ text-transform: capitalize;
594
+ cursor: pointer;
595
+ transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
596
+ }
597
+
598
+ .state-tab-btn:hover:not(.active) {
599
+ color: var(--ui-text-primary);
600
+ background: var(--ui-hover);
601
+ }
602
+
603
+ .state-tab-btn.active {
604
+ color: var(--ui-text-primary);
605
+ background: var(--ui-surface-high);
606
+ box-shadow: 0 0 0 1px var(--ui-border);
607
+ }
608
+
609
+ /* Direct child of .variant-group; needs its own margin since flex gap doesn't apply across siblings. */
610
+ .properties-header {
611
+ display: flex;
612
+ align-items: center;
613
+ gap: var(--ui-space-16);
614
+ margin-top: var(--ui-space-8);
615
+ margin-bottom: var(--ui-space-8);
616
+ }
617
+
618
+ /* Tighten the title's line-height so its line-box matches its visual glyph
619
+ height — otherwise the inherited body line-height makes the heading sit
620
+ visually above the smaller CopyFromMenu trigger even with align-items:center. */
621
+ .properties-header .properties-title {
622
+ line-height: 1;
623
+ }
624
+
625
+ .extra-property-rows {
626
+ display: flex;
627
+ flex-direction: column;
628
+ gap: var(--ui-space-6);
629
+ }
630
+
631
+ .extra-property-rows :global(.property-row) {
632
+ display: grid;
633
+ grid-template-columns: minmax(8rem, max-content) 1fr;
634
+ column-gap: var(--ui-space-16);
635
+ align-items: center;
636
+ min-height: 1.75rem;
637
+ }
638
+
639
+ .extra-property-rows :global(.property-row > .property-label) {
640
+ font-size: var(--ui-font-size-sm);
641
+ color: var(--ui-text-secondary);
642
+ }
643
+
644
+ </style>
@@ -3,37 +3,47 @@ import { writable, type Writable, type Readable } from 'svelte/store';
3
3
 
4
4
  const KEY = Symbol('editor-context');
5
5
 
6
+ export type VariantOption = { value: string; label: string };
7
+
6
8
  export type EditorContext = {
7
- /** Per-variable rank used by TokenLayout to align linked rows; null when no linked block. */
9
+ /** Per-variable rank for TokenLayout row alignment; null when no linked block. */
8
10
  linkedOrder: Readable<Map<string, number> | null>;
9
- /** Variant currently focused in the preview when multiple sibling variants exist. */
11
+ /** Focused preview variant when siblings exist. */
10
12
  focusedVariant: Writable<string | null>;
11
- /** Cross-group hint for which state tab to activate. VariantGroups whose `stateNames`
12
- contain the value adopt it as their `activeTab`; others ignore it. Used to forward
13
- LinkageChart row clicks to the state tab strip when the chart spans states. */
13
+ /** Variants for this editor; empty when single-variant. Focused VariantGroup renders the tab strip itself. */
14
+ variants: Readable<VariantOption[]>;
15
+ /** Cross-group state-tab hint; VariantGroups whose stateNames contain it adopt as activeTab. */
14
16
  focusedState: Writable<string | null>;
15
- /** Variable currently hovered in either the per-state Properties grid or the
16
- Linked-properties block. Bidirectional cue: a hover in one surface lights up
17
- the matching row in the other so the user can see the linkage at a glance. */
17
+ /** Hovered variable in Properties grid or Linked block; bidirectional highlight. */
18
18
  hoveredLinkedVariable: Writable<string | null>;
19
+ /** Copy-from "Preserve color families" toggle, scoped to this editor. Lives
20
+ on context (not local to CopyFromMenu) so it survives VariantGroup remounts
21
+ when the user changes which variant is focused. */
22
+ preserveColorFamily: Writable<boolean>;
19
23
  };
20
24
 
21
- /** Internal mutable handle used by ComponentEditorBase. */
25
+ /** Mutable handle for ComponentEditorBase. */
22
26
  export type EditorContextInternal = EditorContext & {
23
27
  _linkedOrder: Writable<Map<string, number> | null>;
28
+ _variants: Writable<VariantOption[]>;
24
29
  };
25
30
 
26
31
  export function createEditorContext(): EditorContextInternal {
27
32
  const _linkedOrder = writable<Map<string, number> | null>(null);
33
+ const _variants = writable<VariantOption[]>([]);
28
34
  const focusedVariant = writable<string | null>(null);
29
35
  const focusedState = writable<string | null>(null);
30
36
  const hoveredLinkedVariable = writable<string | null>(null);
37
+ const preserveColorFamily = writable<boolean>(false);
31
38
  const ctx: EditorContextInternal = {
32
39
  linkedOrder: _linkedOrder,
33
40
  focusedVariant,
41
+ variants: _variants,
34
42
  focusedState,
35
43
  hoveredLinkedVariable,
44
+ preserveColorFamily,
36
45
  _linkedOrder,
46
+ _variants,
37
47
  };
38
48
  setContext(KEY, ctx);
39
49
  return ctx;