@motion-proto/live-tokens 0.6.2 → 0.8.0

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 (232) hide show
  1. package/README.md +14 -13
  2. package/dist-plugin/index.cjs +854 -226
  3. package/dist-plugin/index.d.cts +2 -1
  4. package/dist-plugin/index.d.ts +2 -1
  5. package/dist-plugin/index.js +852 -225
  6. package/package.json +26 -40
  7. package/src/{styles → app}/site.css +1 -1
  8. package/src/{component-editor → editor/component-editor}/BadgeEditor.svelte +8 -82
  9. package/src/{component-editor → editor/component-editor}/CalloutEditor.svelte +4 -4
  10. package/src/{component-editor → editor/component-editor}/CardEditor.svelte +28 -76
  11. package/src/{component-editor → editor/component-editor}/CollapsibleSectionEditor.svelte +37 -30
  12. package/src/{component-editor → editor/component-editor}/CornerBadgeEditor.svelte +31 -93
  13. package/src/{component-editor → editor/component-editor}/DialogEditor.svelte +60 -57
  14. package/src/editor/component-editor/ImageEditor.svelte +30 -0
  15. package/src/{component-editor → editor/component-editor}/InlineEditActionsEditor.svelte +6 -4
  16. package/src/editor/component-editor/MenuSelectEditor.svelte +160 -0
  17. package/src/{component-editor → editor/component-editor}/NotificationEditor.svelte +67 -38
  18. package/src/{component-editor → editor/component-editor}/ProgressBarEditor.svelte +5 -4
  19. package/src/{component-editor → editor/component-editor}/RadioButtonEditor.svelte +3 -3
  20. package/src/editor/component-editor/SectionDividerEditor.svelte +565 -0
  21. package/src/{component-editor → editor/component-editor}/SegmentedControlEditor.svelte +2 -2
  22. package/src/{component-editor → editor/component-editor}/StandardButtonsEditor.svelte +29 -21
  23. package/src/{component-editor → editor/component-editor}/TabBarEditor.svelte +9 -14
  24. package/src/{component-editor → editor/component-editor}/TableEditor.svelte +9 -18
  25. package/src/{component-editor → editor/component-editor}/TooltipEditor.svelte +11 -47
  26. package/src/editor/component-editor/editors.d.ts +10 -0
  27. package/src/{component-editor → editor/component-editor}/registry.ts +28 -18
  28. package/src/{component-editor → editor/component-editor}/scaffolding/AngleDial.svelte +54 -15
  29. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentEditorBase.svelte +3 -51
  30. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileManager.svelte +151 -424
  31. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentFileMenu.svelte +18 -170
  32. package/src/{component-editor → editor/component-editor}/scaffolding/ComponentsTab.svelte +2 -2
  33. package/src/{component-editor → editor/component-editor}/scaffolding/CopyFromMenu.svelte +44 -4
  34. package/src/{component-editor → editor/component-editor}/scaffolding/FieldsetWrapper.svelte +1 -1
  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 -12
  37. package/src/editor/component-editor/scaffolding/NonStylableConfig.svelte +38 -0
  38. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  39. package/src/{component-editor → editor/component-editor}/scaffolding/SaveAsDialog.svelte +66 -12
  40. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +85 -0
  41. package/src/editor/component-editor/scaffolding/ShadowBackdropControls.svelte +132 -0
  42. package/src/editor/component-editor/scaffolding/StateBlock.svelte +345 -0
  43. package/src/{component-editor → editor/component-editor}/scaffolding/TokenLayout.svelte +17 -12
  44. package/src/{component-editor → editor/component-editor}/scaffolding/TypeEditor.svelte +13 -1
  45. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +858 -0
  46. package/src/{component-editor → editor/component-editor}/scaffolding/buildTypeGroupTokens.ts +1 -0
  47. package/src/{component-editor → editor/component-editor}/scaffolding/editorContext.ts +19 -9
  48. package/src/{component-editor → editor/component-editor}/scaffolding/linkedBlock.ts +2 -2
  49. package/src/{component-editor → editor/component-editor}/scaffolding/types.ts +25 -0
  50. package/src/{lib → editor/core/components}/componentConfigKeys.ts +8 -0
  51. package/src/{lib → editor/core/components}/componentConfigService.ts +3 -3
  52. package/src/{lib → editor/core/components}/componentPersist.ts +11 -9
  53. package/src/editor/core/flashStatus.ts +30 -0
  54. package/src/{lib → editor/core/fonts}/fontLoader.ts +2 -2
  55. package/src/{lib → editor/core/fonts}/fontMigration.ts +4 -4
  56. package/src/{lib → editor/core/fonts}/fontParse.ts +1 -1
  57. package/src/editor/core/manifests/manifestService.ts +171 -0
  58. package/src/editor/core/palettes/familySwap.ts +99 -0
  59. package/src/{lib → editor/core/palettes}/paletteDerivation.ts +71 -2
  60. package/src/{lib → editor/core/palettes}/tokenRegistry.ts +9 -6
  61. package/src/editor/core/productionPulse.ts +37 -0
  62. package/src/{lib → editor/core/routing}/router.ts +1 -1
  63. package/src/{lib/files/versionedFileResource.ts → editor/core/storage/files/versionedFileResourceClient.ts} +8 -1
  64. package/src/{lib → editor/core/store}/editorCore.ts +24 -8
  65. package/src/{lib → editor/core/store}/editorPersistence.ts +3 -3
  66. package/src/{lib → editor/core/store}/editorRenderer.ts +2 -2
  67. package/src/{lib → editor/core/store}/editorStore.ts +222 -28
  68. package/src/{lib → editor/core/store}/editorTypes.ts +56 -13
  69. package/src/editor/core/store/gradientSource.ts +192 -0
  70. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  71. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  72. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  73. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  74. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  75. package/src/{lib → editor/core/themes}/migrations/index.ts +10 -0
  76. package/src/{lib → editor/core/themes}/slices/columns.ts +2 -2
  77. package/src/{lib → editor/core/themes}/slices/components.ts +20 -6
  78. package/src/{lib → editor/core/themes}/slices/fonts.ts +1 -1
  79. package/src/{lib → editor/core/themes}/slices/gradients.ts +89 -14
  80. package/src/{lib → editor/core/themes}/slices/overlays.ts +1 -1
  81. package/src/{lib → editor/core/themes}/slices/palettes.ts +1 -1
  82. package/src/{lib → editor/core/themes}/slices/shadows.ts +3 -3
  83. package/src/{lib → editor/core/themes}/themeInit.ts +8 -8
  84. package/src/{lib → editor/core/themes}/themeService.ts +6 -6
  85. package/src/{lib → editor/core/themes}/themeTypes.ts +67 -8
  86. package/src/editor/index.ts +69 -0
  87. package/src/{lib → editor/overlay}/ColumnsOverlay.svelte +0 -1
  88. package/src/{lib → editor/overlay}/LiveEditorOverlay.svelte +80 -129
  89. package/src/{lib → editor/overlay}/columnsOverlay.ts +2 -2
  90. package/src/{pages → editor/pages}/ComponentEditorPage.svelte +12 -12
  91. package/src/{pages → editor/pages}/Editor.svelte +4 -4
  92. package/src/{pages → editor/pages}/EditorShell.svelte +18 -36
  93. package/src/{styles → editor/styles}/ui-editor.css +43 -22
  94. package/src/{styles → editor/styles}/ui-form-controls.css +23 -24
  95. package/src/{ui → editor/ui}/BezierCurveEditor.svelte +119 -68
  96. package/src/{ui → editor/ui}/ColorEditPanel.svelte +13 -13
  97. package/src/{ui → editor/ui}/EditorViewSwitcher.svelte +7 -6
  98. package/src/editor/ui/FileLoadList.svelte +367 -0
  99. package/src/editor/ui/FilePill.svelte +80 -0
  100. package/src/editor/ui/FontStackEditor.svelte +499 -0
  101. package/src/editor/ui/GradientEditor.svelte +690 -0
  102. package/src/{ui → editor/ui}/GradientStopPicker.svelte +12 -4
  103. package/src/editor/ui/ManifestFileManager.svelte +438 -0
  104. package/src/{ui → editor/ui}/PaletteEditor.svelte +180 -673
  105. package/src/editor/ui/ProjectFontsSection.svelte +638 -0
  106. package/src/{ui → editor/ui}/SurfacesTab.svelte +3 -3
  107. package/src/{ui → editor/ui}/TextTab.svelte +3 -3
  108. package/src/editor/ui/ThemeFileManager.svelte +783 -0
  109. package/src/{ui → editor/ui}/UICopyPopover.svelte +4 -4
  110. package/src/{ui → editor/ui}/UIFontFamilySelector.svelte +6 -7
  111. package/src/{ui → editor/ui}/UIFontSizeSelector.svelte +4 -1
  112. package/src/editor/ui/UIInfoPopover.svelte +243 -0
  113. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  114. package/src/{ui → editor/ui}/UILineHeightSelector.svelte +5 -5
  115. package/src/{ui → editor/ui}/UILinkToggle.svelte +2 -2
  116. package/src/{ui → editor/ui}/UIPaddingSelector.svelte +6 -6
  117. package/src/{ui → editor/ui}/UIPaletteSelector.svelte +57 -30
  118. package/src/editor/ui/UIPillButton.svelte +168 -0
  119. package/src/{ui → editor/ui}/UIRadio.svelte +2 -2
  120. package/src/{ui → editor/ui}/UIRelinkConfirmPopover.svelte +4 -4
  121. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  122. package/src/editor/ui/UISquareButton.svelte +172 -0
  123. package/src/{ui → editor/ui}/UITokenSelector.svelte +14 -11
  124. package/src/{ui → editor/ui}/UIVariantSelector.svelte +1 -1
  125. package/src/{ui → editor/ui}/VariablesTab.svelte +46 -17
  126. package/src/{ui → editor/ui}/palette/GradientStopEditor.svelte +13 -13
  127. package/src/{ui → editor/ui}/palette/OverridesPanel.svelte +24 -47
  128. package/src/{ui → editor/ui}/palette/PaletteBase.svelte +11 -8
  129. package/src/{ui → editor/ui}/palette/paletteEditorState.ts +1 -1
  130. package/src/editor/ui/palette/paletteMath.ts +275 -0
  131. package/src/{ui → editor/ui}/sections/ColumnsSection.svelte +137 -18
  132. package/src/{ui → editor/ui}/sections/GradientsSection.svelte +8 -8
  133. package/src/{ui → editor/ui}/sections/OverlaysSection.svelte +18 -18
  134. package/src/{ui → editor/ui}/sections/ShadowsSection.svelte +23 -23
  135. package/src/{ui → editor/ui}/sections/TokenScaleTable.svelte +3 -3
  136. package/src/{components → system/components}/Badge.svelte +0 -36
  137. package/src/{components → system/components}/Button.svelte +2 -2
  138. package/src/{components → system/components}/Card.svelte +34 -60
  139. package/src/{components → system/components}/CollapsibleSection.svelte +25 -2
  140. package/src/{components → system/components}/CornerBadge.svelte +8 -24
  141. package/src/{components → system/components}/Dialog.svelte +1 -1
  142. package/src/system/components/FloatingTokenTags.css +275 -0
  143. package/src/system/components/FloatingTokenTags.svelte +543 -0
  144. package/src/{components → system/components}/InlineEditActions.svelte +6 -4
  145. package/src/system/components/MenuSelect.svelte +229 -0
  146. package/src/{components → system/components}/Notification.svelte +8 -1
  147. package/src/{components → system/components}/ProgressBar.svelte +29 -11
  148. package/src/system/components/SectionDivider.svelte +560 -0
  149. package/src/{components → system/components}/SegmentedControl.svelte +49 -43
  150. package/src/{components → system/components}/TabBar.svelte +81 -65
  151. package/src/{components → system/components}/Table.svelte +17 -3
  152. package/src/{components → system/components}/Tooltip.svelte +6 -4
  153. package/src/system/styles/CONVENTIONS.md +178 -0
  154. package/src/system/styles/fonts.css +20 -0
  155. package/src/system/styles/tokens.css +601 -0
  156. package/src/system/styles/tokens.generated.css +544 -0
  157. package/src/component-editor/ImageEditor.svelte +0 -74
  158. package/src/component-editor/SectionDividerEditor.svelte +0 -265
  159. package/src/component-editor/scaffolding/DividerEditor.svelte +0 -94
  160. package/src/component-editor/scaffolding/GradientCard.svelte +0 -296
  161. package/src/component-editor/scaffolding/NonStylableConfig.svelte +0 -62
  162. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +0 -37
  163. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +0 -61
  164. package/src/component-editor/scaffolding/StateBlock.svelte +0 -132
  165. package/src/component-editor/scaffolding/VariantGroup.svelte +0 -310
  166. package/src/components/SectionDivider.svelte +0 -483
  167. package/src/data/google-fonts.json +0 -75
  168. package/src/lib/index.ts +0 -68
  169. package/src/lib/presetService.ts +0 -214
  170. package/src/lib/productionPulse.ts +0 -32
  171. package/src/styles/fonts.css +0 -30
  172. package/src/styles/tokens.css +0 -1324
  173. package/src/ui/FontStackEditor.svelte +0 -361
  174. package/src/ui/GradientEditor.svelte +0 -470
  175. package/src/ui/PresetFileManager.svelte +0 -1116
  176. package/src/ui/ProjectFontsSection.svelte +0 -645
  177. package/src/ui/ThemeFileManager.svelte +0 -1020
  178. package/src/ui/UnsavedComponentsDialog.svelte +0 -315
  179. /package/src/{component-editor → editor/component-editor}/index.ts +0 -0
  180. /package/src/{component-editor → editor/component-editor}/scaffolding/DemoHeader.svelte +0 -0
  181. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSectionType.ts +0 -0
  182. /package/src/{component-editor → editor/component-editor}/scaffolding/componentSources.ts +0 -0
  183. /package/src/{component-editor → editor/component-editor}/scaffolding/defaultSections.ts +0 -0
  184. /package/src/{component-editor → editor/component-editor}/scaffolding/siblings.ts +0 -0
  185. /package/src/{lib → editor/core}/cssVarSync.ts +0 -0
  186. /package/src/{lib → editor/core/palettes}/oklch.ts +0 -0
  187. /package/src/{lib → editor/core/routing}/navLinkTypes.ts +0 -0
  188. /package/src/{lib → editor/core/routing}/parentRouteStore.ts +0 -0
  189. /package/src/{lib → editor/core/storage}/storage.ts +0 -0
  190. /package/src/{lib → editor/core/store}/editorConfig.ts +0 -0
  191. /package/src/{lib → editor/core/store}/editorConfigStore.ts +0 -0
  192. /package/src/{lib → editor/core/store}/editorKeybindings.ts +0 -0
  193. /package/src/{lib → editor/core/store}/editorViewStore.ts +0 -0
  194. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +0 -0
  195. /package/src/{lib → editor/core/themes}/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +0 -0
  196. /package/src/{lib → editor/core/themes}/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +0 -0
  197. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +0 -0
  198. /package/src/{lib → editor/core/themes}/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +0 -0
  199. /package/src/{lib → editor/core/themes}/migrations/2026-05-10-sectiondivider-gradient-stops.ts +0 -0
  200. /package/src/{lib → editor/core/themes}/migrations/2026-05-13-primary-to-brand.ts +0 -0
  201. /package/src/{lib → editor/core/themes}/parsers/globalRootBlock.ts +0 -0
  202. /package/src/{lib → editor/core/themes}/slices/domainVars.ts +0 -0
  203. /package/src/{lib → editor/overlay}/overlayState.ts +0 -0
  204. /package/src/{pages → editor/pages}/ComponentEditorPage.svelte.d.ts +0 -0
  205. /package/src/{pages → editor/pages}/Editor.svelte.d.ts +0 -0
  206. /package/src/{ui → editor/ui}/Toggle.svelte +0 -0
  207. /package/src/{ui → editor/ui}/UIDialog.svelte +0 -0
  208. /package/src/{ui → editor/ui}/UIFontWeightSelector.svelte +0 -0
  209. /package/src/{ui → editor/ui}/UIOptionItem.svelte +0 -0
  210. /package/src/{ui → editor/ui}/UIOptionList.svelte +0 -0
  211. /package/src/{ui → editor/ui}/UIRadioGroup.svelte +0 -0
  212. /package/src/{lib → editor/ui}/copyPopover.ts +0 -0
  213. /package/src/{ui → editor/ui}/curveEngine.ts +0 -0
  214. /package/src/{ui → editor/ui}/index.ts +0 -0
  215. /package/src/{ui → editor/ui}/keepInViewport.ts +0 -0
  216. /package/src/{ui → editor/ui}/palette/ScaleCurveEditor.svelte +0 -0
  217. /package/src/{lib → editor/ui}/scrollSection.ts +0 -0
  218. /package/src/{ui → editor/ui}/sections/tokenScales.ts +0 -0
  219. /package/src/{ui → editor/ui}/variantScales.ts +0 -0
  220. /package/src/{assets → system/assets}/newspaper.webp +0 -0
  221. /package/src/{assets → system/assets}/offering.webp +0 -0
  222. /package/src/{components → system/components}/Callout.svelte +0 -0
  223. /package/src/{components → system/components}/Image.svelte +0 -0
  224. /package/src/{components → system/components}/RadioButton.svelte +0 -0
  225. /package/src/{components → system/components}/types.ts +0 -0
  226. /package/src/{styles → system/styles}/_padding.scss +0 -0
  227. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  228. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  229. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  230. /package/src/{styles → system/styles}/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  231. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  232. /package/src/{styles → system/styles}/fonts/Manrope/Manrope-latin.woff2 +0 -0
@@ -0,0 +1,858 @@
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`. Rendered above the token grid
34
+ so per-variant display knobs (alignment, hairline, etc.) lead the list
35
+ before the typography and token rows. */
36
+ extraPropertyRowsTop?: Snippet<[string]>;
37
+ /** Each child should be a `.property-row` to match the token grid above. */
38
+ extraPropertyRows?: Snippet<[string]>;
39
+ /** Extra sections appended below the Background controls inside the canvas
40
+ toolbar. Use `.canvas-toolbar-eyebrow` for section headings to match
41
+ Background. Lets per-instance display knobs (anchor, alignment, etc.)
42
+ live with the canvas rather than in a separate config block above. */
43
+ canvasToolbarExtras?: Snippet;
44
+ /** Per-element Show toggles, forwarded to StateBlock. Keyed by element name. */
45
+ elementToggles?: Record<string, { checked: boolean; label?: string; onchange: (checked: boolean) => void }>;
46
+ /** Explicit order for element-grouped sections, forwarded to StateBlock. */
47
+ elementOrder?: string[];
48
+ /** Per-element extras snippet, forwarded to StateBlock. Receives the
49
+ element name and renders between the section heading and its content. */
50
+ elementExtras?: Snippet<[string]>;
51
+ /** Skip the default centered, padded stage when the editor brings its own backdrop. */
52
+ unboxedPreview?: boolean;
53
+ backdropPadding?: string;
54
+ backdropModes?: ReadonlyArray<'default' | 'image' | 'color'>;
55
+ }
56
+
57
+ let {
58
+ name,
59
+ title,
60
+ tokens = [],
61
+ states = null,
62
+ typeGroups = {},
63
+ component = undefined,
64
+ siblings = [],
65
+ columns = 1,
66
+ selectorLabel = 'Element',
67
+ onchange,
68
+ children,
69
+ stateActions,
70
+ previewActions,
71
+ compositeControls,
72
+ extraPropertyRowsTop,
73
+ extraPropertyRows,
74
+ canvasToolbarExtras,
75
+ elementToggles,
76
+ elementOrder,
77
+ elementExtras,
78
+ unboxedPreview = false,
79
+ backdropPadding,
80
+ backdropModes,
81
+ }: Props = $props();
82
+
83
+ let bgMode: 'default' | 'image' | 'color' = $state('default');
84
+ let bgVar = $derived(`--backdrop-${component ?? name}-surface`);
85
+
86
+ const editorCtx = getEditorContext();
87
+ const linkedOrderStore = editorCtx?.linkedOrder ?? writable<Map<string, number> | null>(null);
88
+ const focusedVariantStore = editorCtx?.focusedVariant ?? writable<string | null>(null);
89
+ const focusedStateStore = editorCtx?.focusedState ?? writable<string | null>(null);
90
+ const variantsStore = editorCtx?.variants ?? writable<{ value: string; label: string }[]>([]);
91
+ const preserveColorFamilyStore = editorCtx?.preserveColorFamily ?? writable(false);
92
+ let linkedOrder = $derived($linkedOrderStore ?? undefined);
93
+
94
+ let activeTab: string = $state('');
95
+
96
+ const TYPE_PROPS = ['colorVariable', 'familyVariable', 'sizeVariable', 'weightVariable', 'lineHeightVariable', 'letterSpacingVariable', 'outlineWidthVariable', 'outlineColorVariable'] as const;
97
+ // Carry per-side derived vars so split padding fully transfers; no-op when absent.
98
+ const PADDING_SIDES = ['top', 'right', 'bottom', 'left'] as const;
99
+
100
+ /** A token whose label contains "color" describes a color property. The
101
+ copy-from "Preserve color families" toggle skips these so the destination
102
+ keeps its existing palette family (e.g. button-primary stays on `brand`)
103
+ while still picking up shape/typography from the source variant. */
104
+ function isColorToken(t: Token): boolean {
105
+ return t.label.toLowerCase().includes('color');
106
+ }
107
+
108
+ /** Parse the right-hand side of a CSS declaration (e.g. `var(--surface-accent-lowest)`
109
+ or `#ff0000`) into a CssVarRef. Returns null for empty/unparseable input.
110
+ Used by copy-from when the source has no explicit override — we resolve
111
+ its declared default so the destination visually matches the source
112
+ instead of falling back to its own family's default. */
113
+ function declaredToRef(declared: string | null): CssVarRef | null {
114
+ if (!declared) return null;
115
+ const m = declared.match(/^\s*var\((--[a-z0-9-]+)\)\s*$/i);
116
+ if (m) return { kind: 'token', name: m[1] };
117
+ return { kind: 'literal', value: declared };
118
+ }
119
+
120
+ /** Extract a transparency percentage from a `color-mix(in srgb, var(--X) N%, transparent)`
121
+ wrapper, optionally itself wrapped in `var(...)`. Returns null if no
122
+ such wrapper is present. */
123
+ const ALPHA_RE = /color-mix\s*\(\s*in\s+srgb\s*,\s*var\(--[a-z0-9-]+\)\s+([\d.]+%)\s*,\s*transparent\s*\)/i;
124
+ function extractAlpha(value: string): string | null {
125
+ const m = value.match(ALPHA_RE);
126
+ return m ? m[1] : null;
127
+ }
128
+
129
+ /** Extract the inner var() base reference, whether the value is a bare
130
+ `var(--X)`, or wrapped by a transparency color-mix, or both. */
131
+ function extractBaseToken(value: string): string | null {
132
+ const mix = value.match(/color-mix\s*\(\s*in\s+srgb\s*,\s*var\((--[a-z0-9-]+)\)/i);
133
+ if (mix) return mix[1];
134
+ const bare = value.match(/var\((--[a-z0-9-]+)\)/i);
135
+ return bare ? bare[1] : null;
136
+ }
137
+
138
+ /** TypeGroup props whose values are color tokens; preserve-color-families
139
+ skips these in the typeGroups copy loop. */
140
+ const COLOR_TYPE_PROPS = new Set(['colorVariable', 'outlineColorVariable']);
141
+
142
+ /** True iff `colorRef` is a CSS-var token whose slug contains `family`
143
+ as a hyphen-delimited segment (e.g. `--surface-canvas-low` ∋ `canvas`). */
144
+ function stopMatchesFamily(colorRef: string, family: string): boolean {
145
+ if (!colorRef.startsWith('--')) return false;
146
+ return colorRef.slice(2).split('-').includes(family);
147
+ }
148
+
149
+ /** Replace the first `from` segment in a CSS-var token slug with `to`.
150
+ Used to swap stop colors across variant families on gradient copy
151
+ (e.g. `--surface-success-high` + (success → accent) → `--surface-accent-high`). */
152
+ function swapFamilyInToken(colorRef: string, from: string, to: string): string {
153
+ if (!colorRef.startsWith('--')) return colorRef;
154
+ const parts = colorRef.slice(2).split('-');
155
+ const idx = parts.indexOf(from);
156
+ if (idx < 0) return colorRef;
157
+ parts[idx] = to;
158
+ return '--' + parts.join('-');
159
+ }
160
+
161
+ function pickCopySource(toState: string, fromVariant: string, fromState: string) {
162
+ const preserveColorFamily = $preserveColorFamilyStore;
163
+ if (!component || !states) return;
164
+ const isSelfVariant = fromVariant === name;
165
+ const sibling = isSelfVariant ? null : siblings.find((s) => s.name === fromVariant);
166
+ if (!isSelfVariant && !sibling) return;
167
+ const srcTokens = (isSelfVariant ? states[fromState] : sibling!.states[fromState]) ?? [];
168
+ const dstTokens = states[toState] ?? [];
169
+ const srcTypeGroups = (isSelfVariant ? typeGroups[fromState] : sibling!.typeGroups?.[fromState]) ?? [];
170
+ const dstTypeGroups = typeGroups[toState] ?? [];
171
+
172
+ mutate(`copy ${fromVariant}/${fromState} → ${name}/${toState}`, (s) => {
173
+ const slice = s.components[component!] ?? (s.components[component!] = { activeFile: 'default', aliases: {}, config: {} });
174
+ const dstVarsTouched: string[] = [];
175
+ /** Resolve a variable's effective value as a CSS string: the override if
176
+ set, otherwise its declared default. Returns null if neither exists.
177
+ Gradient refs short-circuit to null — the copy path for gradients
178
+ uses `applyGradient`, not the alpha-extract pipeline. */
179
+ const effectiveValue = (varName: string): string | null => {
180
+ const ref = slice.aliases[varName];
181
+ if (!ref) return getDeclaredValue(varName);
182
+ if (ref.kind === 'token') return `var(${ref.name})`;
183
+ if (ref.kind === 'literal') return ref.value;
184
+ return null;
185
+ };
186
+
187
+ const apply = (srcVar: string, dstVar: string) => {
188
+ if (srcVar === dstVar) return;
189
+ if (srcVar in slice.aliases) {
190
+ slice.aliases[dstVar] = slice.aliases[srcVar];
191
+ } else {
192
+ // Src is at its declared default. Materialize that default as dst's
193
+ // override so dst visually picks up src's value — otherwise dst
194
+ // falls back to its OWN family default and the copy is a visual no-op.
195
+ const ref = declaredToRef(getDeclaredValue(srcVar));
196
+ if (ref) slice.aliases[dstVar] = ref;
197
+ else delete slice.aliases[dstVar];
198
+ }
199
+ dstVarsTouched.push(dstVar);
200
+ };
201
+
202
+ /** Preserve-color-families variant of apply: copy src's transparency
203
+ wrapper (if any) over dst's existing base color so dst keeps its
204
+ palette family but picks up src's alpha. Source with no alpha = no-op
205
+ (leaves dst's color untouched). */
206
+ const applyColorPreserve = (srcVar: string, dstVar: string) => {
207
+ const srcVal = effectiveValue(srcVar);
208
+ if (!srcVal) return;
209
+ const alpha = extractAlpha(srcVal);
210
+ if (!alpha) return;
211
+ const dstVal = effectiveValue(dstVar);
212
+ const dstBase = dstVal ? extractBaseToken(dstVal) : null;
213
+ if (!dstBase) return;
214
+ slice.aliases[dstVar] = {
215
+ kind: 'literal',
216
+ value: `color-mix(in srgb, var(${dstBase}) ${alpha}, transparent)`,
217
+ };
218
+ dstVarsTouched.push(dstVar);
219
+ };
220
+ /** Copy a structured gradient ref. Stops carry source's positions +
221
+ opacities verbatim; out-of-family stop colors carry verbatim too.
222
+ With preserveColorFamily on, in-family stop colors swap to the
223
+ destination family — see plan §5 for the worked example. */
224
+ const applyGradient = (
225
+ srcVar: string,
226
+ dstVar: string,
227
+ srcFamily: string | undefined,
228
+ dstFamily: string | undefined,
229
+ ) => {
230
+ const srcRef = slice.aliases[srcVar];
231
+ if (!srcRef || srcRef.kind !== 'gradient') {
232
+ // Source has no override — clearing dst returns it to its own CSS
233
+ // default (which is dst's family by design), preserving the
234
+ // "destination keeps its family palette" invariant.
235
+ delete slice.aliases[dstVar];
236
+ dstVarsTouched.push(dstVar);
237
+ return;
238
+ }
239
+ const swapFamilies = preserveColorFamily
240
+ && !!srcFamily && !!dstFamily
241
+ && srcFamily !== dstFamily;
242
+ const newStops = srcRef.value.stops.map((s) => {
243
+ if (swapFamilies && stopMatchesFamily(s.color, srcFamily!)) {
244
+ return { ...s, color: swapFamilyInToken(s.color, srcFamily!, dstFamily!) };
245
+ }
246
+ return { ...s };
247
+ });
248
+ slice.aliases[dstVar] = {
249
+ kind: 'gradient',
250
+ value: { type: srcRef.value.type, angle: srcRef.value.angle, stops: newStops },
251
+ };
252
+ dstVarsTouched.push(dstVar);
253
+ };
254
+
255
+ const minLen = Math.min(srcTokens.length, dstTokens.length);
256
+ for (let i = 0; i < minLen; i++) {
257
+ const srcVar = srcTokens[i].variable;
258
+ const dstVar = dstTokens[i].variable;
259
+ if (srcTokens[i].kind === 'gradient' || dstTokens[i].kind === 'gradient') {
260
+ applyGradient(srcVar, dstVar, srcTokens[i].family, dstTokens[i].family);
261
+ continue;
262
+ }
263
+ if (preserveColorFamily && isColorToken(srcTokens[i])) {
264
+ applyColorPreserve(srcVar, dstVar);
265
+ continue;
266
+ }
267
+ apply(srcVar, dstVar);
268
+ if (srcTokens[i].splittable !== false) {
269
+ for (const side of PADDING_SIDES) apply(`${srcVar}-${side}`, `${dstVar}-${side}`);
270
+ }
271
+ }
272
+ const minTypeGroups = Math.min(srcTypeGroups.length, dstTypeGroups.length);
273
+ for (let g = 0; g < minTypeGroups; g++) {
274
+ const srcType = srcTypeGroups[g];
275
+ const dstType = dstTypeGroups[g];
276
+ for (const prop of TYPE_PROPS) {
277
+ const srcVar = srcType[prop];
278
+ const dstVar = dstType[prop];
279
+ if (!srcVar || !dstVar) continue;
280
+ if (preserveColorFamily && COLOR_TYPE_PROPS.has(prop)) {
281
+ applyColorPreserve(srcVar, dstVar);
282
+ continue;
283
+ }
284
+ apply(srcVar, dstVar);
285
+ }
286
+ }
287
+ // Copy intent "make this state match" clears stale unlinked markers on touched vars.
288
+ if (slice.unlinked && slice.unlinked.length > 0) {
289
+ const touched = new Set(dstVarsTouched);
290
+ const remaining = slice.unlinked.filter((v) => !touched.has(v));
291
+ if (remaining.length === 0) delete slice.unlinked;
292
+ else slice.unlinked = remaining;
293
+ }
294
+ });
295
+ }
296
+
297
+ let copySources = $derived.by(() => {
298
+ const fromSiblings = siblings.map((s) => ({
299
+ name: s.name,
300
+ label: s.label,
301
+ states: Object.keys(s.states),
302
+ }));
303
+ const ownStates = states ? Object.keys(states) : [];
304
+ if (ownStates.length >= 2) {
305
+ return [{ name, label: title, states: ownStates }, ...fromSiblings];
306
+ }
307
+ return fromSiblings;
308
+ });
309
+
310
+ let stateNames = $derived(states ? Object.keys(states) : []);
311
+ /** Key-name convention: when ANY state name contains " / ", the strip switches
312
+ to two-tier rendering — top row is parts (unique left-hand sides), bottom
313
+ row is the active part's states (right-hand sides). Parts with no slash
314
+ have no sub-states and skip the bottom row when active. */
315
+ const PART_SEP = ' / ';
316
+ let isHierarchical = $derived(stateNames.some((n) => n.includes(PART_SEP)));
317
+ /** Ordered list of unique parts, preserving the order they first appear in `states`. */
318
+ let parts = $derived.by(() => {
319
+ const seen: string[] = [];
320
+ for (const n of stateNames) {
321
+ const part = n.includes(PART_SEP) ? n.split(PART_SEP)[0] : n;
322
+ if (!seen.includes(part)) seen.push(part);
323
+ }
324
+ return seen;
325
+ });
326
+ /** Sub-states of the currently active part (only meaningful in hierarchical mode). */
327
+ let activePart = $derived(activeTab.includes(PART_SEP) ? activeTab.split(PART_SEP)[0] : activeTab);
328
+ let activeSubState = $derived(activeTab.includes(PART_SEP) ? activeTab.split(PART_SEP)[1] : '');
329
+ let partSubStates = $derived(
330
+ isHierarchical
331
+ ? stateNames.filter((n) => n.startsWith(activePart + PART_SEP)).map((n) => n.split(PART_SEP)[1])
332
+ : [],
333
+ );
334
+ let tabsStripVisible = $derived(stateNames.length >= 2);
335
+ let subStripVisible = $derived(isHierarchical && partSubStates.length >= 2);
336
+
337
+ /** Switch parts. If the new part has sub-states, jump to its first one; otherwise
338
+ activate the part itself. Keeps activeTab as a single canonical key so the
339
+ downstream property lookup, copy-from menus, and focusedStateStore stay simple. */
340
+ function selectPart(part: string) {
341
+ const firstSub = stateNames.find((n) => n.startsWith(part + PART_SEP));
342
+ activeTab = firstSub ?? part;
343
+ focusedStateStore.set(activeTab);
344
+ }
345
+ function selectSubState(sub: string) {
346
+ activeTab = `${activePart}${PART_SEP}${sub}`;
347
+ focusedStateStore.set(activeTab);
348
+ }
349
+
350
+ $effect(() => {
351
+ if (stateNames.length > 0 && !stateNames.includes(activeTab)) {
352
+ activeTab = stateNames[0];
353
+ }
354
+ });
355
+ // Adopt cross-group state hint from chart row clicks.
356
+ $effect(() => {
357
+ if ($focusedStateStore && stateNames.includes($focusedStateStore) && activeTab !== $focusedStateStore) {
358
+ activeTab = $focusedStateStore;
359
+ }
360
+ });
361
+
362
+ let inFocusMode = $derived(siblings.length > 0);
363
+ let amIFocused = $derived($focusedVariantStore === name);
364
+ let shouldRender = $derived(!inFocusMode || amIFocused);
365
+ let showVariantTabs = $derived(inFocusMode && $variantsStore.length >= 2);
366
+ // Mirror state-tab clicks to the shared store so linked-block + chart track them.
367
+ $effect(() => {
368
+ if (amIFocused && activeTab && stateNames.includes(activeTab) && $focusedStateStore !== activeTab) {
369
+ focusedStateStore.set(activeTab);
370
+ }
371
+ });
372
+
373
+ </script>
374
+
375
+ {#if shouldRender}
376
+ <div class="editor-section-card demo-section variant-group">
377
+ {#if states}
378
+ <div class="tabs-preview">
379
+ <div class="preview-header">
380
+ <span class="editor-section-title">Preview</span>
381
+ {#if showVariantTabs}
382
+ <div class="variant-tabs" role="tablist">
383
+ {#each $variantsStore as opt (opt.value)}
384
+ <button
385
+ type="button"
386
+ class="variant-tab-btn"
387
+ class:active={opt.value === $focusedVariantStore}
388
+ role="tab"
389
+ aria-selected={opt.value === $focusedVariantStore}
390
+ onclick={() => focusedVariantStore.set(opt.value)}
391
+ >{opt.label}</button>
392
+ {/each}
393
+ </div>
394
+ {/if}
395
+ {#if previewActions}
396
+ <div class="preview-actions">
397
+ {@render previewActions?.()}
398
+ </div>
399
+ {/if}
400
+ </div>
401
+ {#if unboxedPreview}
402
+ {@render children?.({ activeState: activeTab })}
403
+ {:else}
404
+ <ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
405
+ {#snippet controls()}
406
+ <div class="canvas-toolbar">
407
+ <span class="canvas-toolbar-eyebrow">Background</span>
408
+ <ShadowBackdropControls bind:mode={bgMode} colorVariable={bgVar} modes={backdropModes ?? ['default', 'image', 'color']} />
409
+ {@render canvasToolbarExtras?.()}
410
+ </div>
411
+ {/snippet}
412
+ {@render children?.({ activeState: activeTab })}
413
+ </ShadowBackdrop>
414
+ {/if}
415
+
416
+ {#if tabsStripVisible}
417
+ <div class="tabs-states-block">
418
+ <span class="editor-subsection-title">{selectorLabel}</span>
419
+ <div class="tabs-selectors">
420
+ {#if isHierarchical}
421
+ <div class="state-tabs" role="tablist">
422
+ {#each parts as p}
423
+ <button
424
+ type="button"
425
+ class="state-tab-btn"
426
+ class:active={activePart === p}
427
+ role="tab"
428
+ aria-selected={activePart === p}
429
+ onclick={() => selectPart(p)}
430
+ >{p}</button>
431
+ {/each}
432
+ </div>
433
+ {:else}
434
+ <div class="state-tabs" role="tablist">
435
+ {#each stateNames as s}
436
+ <button
437
+ type="button"
438
+ class="state-tab-btn"
439
+ class:active={activeTab === s}
440
+ role="tab"
441
+ aria-selected={activeTab === s}
442
+ onclick={() => { activeTab = s; focusedStateStore.set(s); }}
443
+ >{s}</button>
444
+ {/each}
445
+ </div>
446
+ {/if}
447
+ {#if activeTab}
448
+ {@render stateActions?.(activeTab)}
449
+ {/if}
450
+ </div>
451
+ {#if subStripVisible}
452
+ <div class="tabs-selectors substrip">
453
+ <span class="editor-subsection-title state-eyebrow">State</span>
454
+ <div class="state-tabs" role="tablist">
455
+ {#each partSubStates as s}
456
+ <button
457
+ type="button"
458
+ class="state-tab-btn"
459
+ class:active={activeSubState === s}
460
+ role="tab"
461
+ aria-selected={activeSubState === s}
462
+ onclick={() => selectSubState(s)}
463
+ >{s}</button>
464
+ {/each}
465
+ </div>
466
+ </div>
467
+ {/if}
468
+ </div>
469
+ {/if}
470
+ </div>
471
+
472
+ {#if activeTab && states[activeTab]}
473
+ {@const stateName = activeTab}
474
+ {@render compositeControls?.(stateName)}
475
+ <div class="properties-header">
476
+ <span class="editor-subsection-title properties-title">Properties</span>
477
+ {#if !tabsStripVisible && stateActions}
478
+ {@render stateActions(activeTab)}
479
+ {/if}
480
+ {#if copySources.length > 0}
481
+ <CopyFromMenu
482
+ toState={activeTab}
483
+ variantName={name}
484
+ {copySources}
485
+ onselect={(d) => pickCopySource(activeTab, d.fromVariant, d.fromState)}
486
+ />
487
+ {/if}
488
+ </div>
489
+ {#if extraPropertyRowsTop}
490
+ <div class="extra-property-rows extra-property-rows--top">
491
+ {@render extraPropertyRowsTop(stateName)}
492
+ </div>
493
+ {/if}
494
+ <StateBlock
495
+ tokens={states[stateName]}
496
+ typeGroups={typeGroups[stateName] ?? []}
497
+ {component}
498
+ {linkedOrder}
499
+ {columns}
500
+ {elementToggles}
501
+ {elementOrder}
502
+ {elementExtras}
503
+ {onchange}
504
+ />
505
+ {#if extraPropertyRows}
506
+ <div class="extra-property-rows">
507
+ {@render extraPropertyRows(stateName)}
508
+ </div>
509
+ {/if}
510
+ {/if}
511
+ {:else}
512
+ {#if unboxedPreview}
513
+ {@render children?.({ activeState: '' })}
514
+ {:else}
515
+ <ShadowBackdrop mode={bgMode} colorVariable={bgVar} padding={backdropPadding}>
516
+ {@render children?.({ activeState: '' })}
517
+ </ShadowBackdrop>
518
+ {/if}
519
+ <TokenLayout
520
+ title={name}
521
+ tokens={tokens}
522
+ {component}
523
+ {linkedOrder}
524
+ {onchange}
525
+ />
526
+ {/if}
527
+
528
+ </div>
529
+ {/if}
530
+
531
+ <style>
532
+ /* Card chrome lives on .editor-section-card in ui-editor.css. */
533
+ .variant-group {
534
+ gap: var(--ui-space-12);
535
+ container-type: inline-size;
536
+ container-name: variant-group;
537
+ }
538
+
539
+ /* Pin the preview + state-tab strip to the top of the page scroll so
540
+ property edits stay visually connected to the preview without scrolling.
541
+ The card background extends through the sticky band so the property grid
542
+ scrolls cleanly behind it. The element-grouped property layout (see
543
+ StateBlock) fans out horizontally, which keeps the property section
544
+ short enough that the sticky preview rarely steals usable space. */
545
+ .tabs-preview {
546
+ position: sticky;
547
+ top: 0;
548
+ z-index: 2;
549
+ display: flex;
550
+ flex-direction: column;
551
+ gap: var(--ui-space-20);
552
+ background: var(--ui-surface-low);
553
+ /* Bleed the background up through the card's top padding so content
554
+ scrolling behind doesn't peek between the viewport edge and the
555
+ pinned preview. The matching negative margin restores flow position.
556
+ Border-radius hugs the card's rounded top corners so the unpinned
557
+ state still reads as one continuous panel. */
558
+ margin: calc(-1 * var(--ui-space-20)) calc(-1 * var(--ui-space-20)) 0;
559
+ padding: var(--ui-space-20) var(--ui-space-20) 0;
560
+ border-radius: var(--ui-radius-md) var(--ui-radius-md) 0 0;
561
+ }
562
+
563
+ /* Soft fade at the bottom of the sticky band so property rows scrolling
564
+ up don't sharply cut against the pinned preview. Greyscale (no accent).
565
+ Sits below the preview body, above scrolling content. */
566
+ .tabs-preview::after {
567
+ content: '';
568
+ position: absolute;
569
+ left: 0;
570
+ right: 0;
571
+ bottom: calc(-1 * var(--ui-space-12));
572
+ height: var(--ui-space-12);
573
+ background: linear-gradient(to bottom, var(--ui-surface-low), transparent);
574
+ pointer-events: none;
575
+ }
576
+
577
+ .preview-header {
578
+ display: flex;
579
+ align-items: center;
580
+ gap: var(--ui-space-24);
581
+ flex-wrap: wrap;
582
+ }
583
+
584
+ .preview-actions {
585
+ margin-left: auto;
586
+ display: inline-flex;
587
+ align-items: center;
588
+ gap: var(--ui-space-8);
589
+ }
590
+
591
+ /* Sits in the backdrop's right-rail column (ShadowBackdrop owns the two-column
592
+ split). Own border + raised surface so it reads as a distinct panel inside
593
+ the backdrop, regardless of which backdrop mode is active. */
594
+ .canvas-toolbar {
595
+ width: 11rem;
596
+ height: 100%;
597
+ display: flex;
598
+ flex-direction: column;
599
+ gap: var(--ui-space-12);
600
+ padding: var(--ui-space-10) var(--ui-space-12);
601
+ background: var(--ui-surface-low);
602
+ border: 1px solid var(--ui-border-low);
603
+ border-radius: var(--ui-radius-md);
604
+ color: var(--ui-text-primary);
605
+ box-sizing: border-box;
606
+ }
607
+
608
+ @container variant-group (max-width: 32rem) {
609
+ .canvas-toolbar {
610
+ width: 100%;
611
+ height: auto;
612
+ }
613
+ }
614
+
615
+ .canvas-toolbar :global(.canvas-toolbar-eyebrow) {
616
+ font-size: var(--ui-font-size-xs);
617
+ font-weight: var(--ui-font-weight-medium);
618
+ color: var(--ui-text-tertiary);
619
+ }
620
+
621
+ /* Divider lives *between* sections, not under each eyebrow. First eyebrow
622
+ has no preceding sibling and stays flush. */
623
+ .canvas-toolbar > :global(* + .canvas-toolbar-eyebrow) {
624
+ padding-top: var(--ui-space-12);
625
+ border-top: 1px solid var(--ui-border-low);
626
+ }
627
+
628
+ /* Explicit separator for label-less sections in canvasToolbarExtras. */
629
+ .canvas-toolbar :global(.canvas-toolbar-divider) {
630
+ margin: 0;
631
+ border: 0;
632
+ border-top: 1px solid var(--ui-border-low);
633
+ }
634
+
635
+ /* Native <select> styled to match the property-row trigger chrome
636
+ (UITokenSelector) so toolbar selects don't read as a separate visual
637
+ vocabulary from the rest of the editor. */
638
+ .canvas-toolbar :global(.canvas-toolbar-select) {
639
+ width: 100%;
640
+ box-sizing: border-box;
641
+ appearance: none;
642
+ -webkit-appearance: none;
643
+ padding: 0 var(--ui-space-24) 0 var(--ui-space-8);
644
+ min-height: 1.75rem;
645
+ background-color: var(--ui-surface-low);
646
+ 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");
647
+ background-repeat: no-repeat;
648
+ background-position: right var(--ui-space-8) center;
649
+ border: 1px solid var(--ui-border);
650
+ border-radius: var(--ui-radius-md);
651
+ color: var(--ui-text-primary);
652
+ font-family: var(--ui-font-sans);
653
+ font-size: var(--ui-font-size-sm);
654
+ cursor: pointer;
655
+ transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
656
+ }
657
+ .canvas-toolbar :global(.canvas-toolbar-select:hover) {
658
+ background-color: var(--ui-surface-high);
659
+ border-color: var(--ui-border-higher);
660
+ }
661
+ .canvas-toolbar :global(.canvas-toolbar-select:focus-visible) {
662
+ outline: 2px solid var(--ui-highlight);
663
+ outline-offset: 2px;
664
+ }
665
+
666
+ /* Native <input> styled to match the toolbar's select chrome. Long values
667
+ ellipsize when blurred so the input doesn't grow or scroll-bleed past
668
+ the toolbar's 11rem column; focus restores native caret-driven scroll. */
669
+ .canvas-toolbar :global(.canvas-toolbar-input) {
670
+ width: 100%;
671
+ min-width: 0;
672
+ max-width: 100%;
673
+ box-sizing: border-box;
674
+ padding: 0 var(--ui-space-8);
675
+ min-height: 1.75rem;
676
+ background: var(--ui-surface-low);
677
+ border: 1px solid var(--ui-border);
678
+ border-radius: var(--ui-radius-md);
679
+ color: var(--ui-text-primary);
680
+ font-family: var(--ui-font-sans);
681
+ font-size: var(--ui-font-size-sm);
682
+ text-overflow: ellipsis;
683
+ transition: background-color var(--ui-transition-fast), border-color var(--ui-transition-fast);
684
+ }
685
+ .canvas-toolbar :global(.canvas-toolbar-input:hover) {
686
+ border-color: var(--ui-border-higher);
687
+ }
688
+ .canvas-toolbar :global(.canvas-toolbar-input:focus-visible) {
689
+ outline: 2px solid var(--ui-highlight);
690
+ outline-offset: 2px;
691
+ }
692
+
693
+ /* Pill each option row so Default/Image match the swatch trigger's weight.
694
+ One step below the toolbar's --ui-surface-low so option chrome stays visible
695
+ against the panel. */
696
+ .canvas-toolbar :global(.backdrop-option) {
697
+ padding: 0 var(--ui-space-8);
698
+ background: var(--ui-surface-lowest);
699
+ border: 1px solid var(--ui-border-low);
700
+ border-radius: var(--ui-radius-md);
701
+ color: var(--ui-text-secondary);
702
+ }
703
+ .canvas-toolbar :global(.backdrop-option.checked) {
704
+ background: var(--ui-surface-high);
705
+ border-color: var(--ui-border);
706
+ color: var(--ui-text-primary);
707
+ }
708
+
709
+ /* Drop the swatch trigger's frame to avoid a double-border inside the option pill. */
710
+ .canvas-toolbar :global(.backdrop-option .ui-ts-trigger) {
711
+ background: transparent;
712
+ border-color: transparent;
713
+ padding-left: 0;
714
+ padding-right: 0;
715
+ }
716
+ .canvas-toolbar :global(.backdrop-option .ui-ts-trigger:hover) {
717
+ background: var(--ui-hover);
718
+ border-color: transparent;
719
+ }
720
+
721
+ .canvas-toolbar :global(.ui-ts-meta-text) {
722
+ color: var(--ui-text-tertiary);
723
+ }
724
+
725
+ /* Anchor dropdown to the trigger's right edge; the toolbar sits flush against
726
+ the editor's right gutter, so a left-anchored dropdown would clip. */
727
+ .canvas-toolbar :global(.ui-ts-dropdown) {
728
+ left: auto;
729
+ right: 0;
730
+ }
731
+
732
+ .variant-tabs {
733
+ display: inline-flex;
734
+ flex-wrap: wrap;
735
+ gap: var(--ui-space-4);
736
+ padding: var(--ui-space-4);
737
+ background: var(--ui-surface-lowest);
738
+ border: 1px solid var(--ui-border-low);
739
+ border-radius: var(--ui-radius-md);
740
+ }
741
+
742
+ .variant-tab-btn {
743
+ padding: var(--ui-space-6) var(--ui-space-12);
744
+ background: none;
745
+ border: none;
746
+ border-radius: var(--ui-radius-sm);
747
+ color: var(--ui-text-secondary);
748
+ font-size: var(--ui-font-size-md);
749
+ font-weight: var(--ui-font-weight-semibold);
750
+ text-transform: capitalize;
751
+ cursor: pointer;
752
+ transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
753
+ }
754
+
755
+ .variant-tab-btn:hover:not(.active) {
756
+ color: var(--ui-text-primary);
757
+ background: var(--ui-hover);
758
+ }
759
+
760
+ .variant-tab-btn.active {
761
+ color: var(--ui-text-primary);
762
+ background: var(--ui-surface-high);
763
+ box-shadow: 0 0 0 1px var(--ui-border);
764
+ }
765
+
766
+ .tabs-states-block {
767
+ display: flex;
768
+ flex-direction: column;
769
+ gap: var(--ui-space-8);
770
+ }
771
+
772
+ .tabs-selectors {
773
+ display: flex;
774
+ align-items: center;
775
+ gap: var(--ui-space-12);
776
+ }
777
+
778
+ /* Sub-strip sits flush under the parts strip when a part has interaction states.
779
+ The eyebrow label distinguishes it from the parts row above. */
780
+ .tabs-selectors.substrip {
781
+ margin-top: var(--ui-space-6);
782
+ }
783
+ .tabs-selectors.substrip .state-eyebrow {
784
+ color: var(--ui-text-tertiary);
785
+ font-size: var(--ui-font-size-xs);
786
+ min-width: 2.5rem;
787
+ }
788
+
789
+ .state-tabs {
790
+ display: inline-flex;
791
+ flex-wrap: wrap;
792
+ gap: var(--ui-space-4);
793
+ padding: var(--ui-space-4);
794
+ background: var(--ui-surface-lowest);
795
+ border: 1px solid var(--ui-border-low);
796
+ border-radius: var(--ui-radius-md);
797
+ }
798
+
799
+ .state-tab-btn {
800
+ padding: var(--ui-space-6) var(--ui-space-12);
801
+ background: none;
802
+ border: none;
803
+ border-radius: var(--ui-radius-sm);
804
+ color: var(--ui-text-secondary);
805
+ font-size: var(--ui-font-size-sm);
806
+ font-weight: var(--ui-font-weight-medium);
807
+ text-transform: capitalize;
808
+ cursor: pointer;
809
+ transition: color var(--ui-transition-fast), background var(--ui-transition-fast);
810
+ }
811
+
812
+ .state-tab-btn:hover:not(.active) {
813
+ color: var(--ui-text-primary);
814
+ background: var(--ui-hover);
815
+ }
816
+
817
+ .state-tab-btn.active {
818
+ color: var(--ui-text-primary);
819
+ background: var(--ui-surface-high);
820
+ box-shadow: 0 0 0 1px var(--ui-border);
821
+ }
822
+
823
+ /* Direct child of .variant-group; needs its own margin since flex gap doesn't apply across siblings. */
824
+ .properties-header {
825
+ display: flex;
826
+ align-items: center;
827
+ gap: var(--ui-space-16);
828
+ margin-top: var(--ui-space-8);
829
+ margin-bottom: var(--ui-space-8);
830
+ }
831
+
832
+ /* Tighten the title's line-height so its line-box matches its visual glyph
833
+ height — otherwise the inherited body line-height makes the heading sit
834
+ visually above the smaller CopyFromMenu trigger even with align-items:center. */
835
+ .properties-header .properties-title {
836
+ line-height: 1;
837
+ }
838
+
839
+ .extra-property-rows {
840
+ display: flex;
841
+ flex-direction: column;
842
+ gap: var(--ui-space-6);
843
+ }
844
+
845
+ .extra-property-rows :global(.property-row) {
846
+ display: grid;
847
+ grid-template-columns: minmax(8rem, max-content) 1fr;
848
+ column-gap: var(--ui-space-16);
849
+ align-items: center;
850
+ min-height: 1.75rem;
851
+ }
852
+
853
+ .extra-property-rows :global(.property-row > .property-label) {
854
+ font-size: var(--ui-font-size-sm);
855
+ color: var(--ui-text-secondary);
856
+ }
857
+
858
+ </style>