@motion-proto/live-tokens 0.7.1 → 0.9.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 (96) hide show
  1. package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
  2. package/README.md +34 -0
  3. package/dist-plugin/index.cjs +707 -90
  4. package/dist-plugin/index.d.cts +1 -0
  5. package/dist-plugin/index.d.ts +1 -0
  6. package/dist-plugin/index.js +707 -90
  7. package/package.json +6 -2
  8. package/src/app/site.css +1 -1
  9. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +34 -27
  10. package/src/editor/component-editor/DialogEditor.svelte +4 -4
  11. package/src/editor/component-editor/NotificationEditor.svelte +3 -1
  12. package/src/editor/component-editor/SectionDividerEditor.svelte +439 -112
  13. package/src/editor/component-editor/StandardButtonsEditor.svelte +13 -1
  14. package/src/editor/component-editor/editors.d.ts +10 -0
  15. package/src/editor/component-editor/index.ts +16 -1
  16. package/src/editor/component-editor/registry.ts +103 -26
  17. package/src/editor/component-editor/scaffolding/AngleDial.svelte +52 -13
  18. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
  19. package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
  20. package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +0 -1
  21. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  22. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +15 -2
  23. package/src/editor/component-editor/scaffolding/StateBlock.svelte +103 -15
  24. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +9 -6
  25. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +13 -1
  26. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +239 -25
  27. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -0
  28. package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
  29. package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
  30. package/src/editor/component-editor/scaffolding/types.ts +11 -0
  31. package/src/editor/core/components/componentConfigKeys.ts +22 -3
  32. package/src/editor/core/components/componentConfigService.ts +2 -2
  33. package/src/editor/core/components/componentPersist.ts +7 -5
  34. package/src/editor/core/manifests/manifestService.ts +58 -3
  35. package/src/editor/core/palettes/familySwap.ts +99 -0
  36. package/src/editor/core/palettes/paletteDerivation.ts +69 -0
  37. package/src/editor/core/palettes/tokenRegistry.ts +4 -1
  38. package/src/editor/core/store/editorStore.ts +206 -12
  39. package/src/editor/core/store/editorTypes.ts +55 -12
  40. package/src/editor/core/store/gradientSource.ts +192 -0
  41. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  42. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  43. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  44. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  45. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  46. package/src/editor/core/themes/migrations/index.ts +10 -0
  47. package/src/editor/core/themes/slices/components.ts +27 -4
  48. package/src/editor/core/themes/slices/gradients.ts +88 -13
  49. package/src/editor/core/themes/themeInit.ts +2 -2
  50. package/src/editor/core/themes/themeTypes.ts +56 -1
  51. package/src/editor/index.ts +10 -1
  52. package/src/editor/overlay/ColumnsOverlay.svelte +0 -1
  53. package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
  54. package/src/editor/pages/ComponentEditorPage.svelte +53 -3
  55. package/src/editor/pages/EditorShell.svelte +53 -3
  56. package/src/editor/styles/ui-editor.css +1 -0
  57. package/src/editor/styles/ui-form-controls.css +19 -20
  58. package/src/editor/ui/BezierCurveEditor.svelte +114 -63
  59. package/src/editor/ui/EditorViewSwitcher.svelte +0 -1
  60. package/src/editor/ui/FileLoadList.svelte +22 -5
  61. package/src/editor/ui/FontStackEditor.svelte +214 -76
  62. package/src/editor/ui/GradientEditor.svelte +435 -215
  63. package/src/editor/ui/GradientStopPicker.svelte +11 -3
  64. package/src/editor/ui/ManifestFileManager.svelte +71 -4
  65. package/src/editor/ui/PaletteEditor.svelte +52 -79
  66. package/src/editor/ui/ProjectFontsSection.svelte +328 -293
  67. package/src/editor/ui/ThemeFileManager.svelte +0 -4
  68. package/src/editor/ui/UIFontFamilySelector.svelte +0 -1
  69. package/src/editor/ui/UIFontSizeSelector.svelte +3 -0
  70. package/src/editor/ui/UIInfoPopover.svelte +0 -1
  71. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  72. package/src/editor/ui/UIPaletteSelector.svelte +31 -4
  73. package/src/editor/ui/UIPillButton.svelte +33 -3
  74. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  75. package/src/editor/ui/UITokenSelector.svelte +4 -1
  76. package/src/editor/ui/VariablesTab.svelte +41 -35
  77. package/src/editor/ui/palette/OverridesPanel.svelte +14 -37
  78. package/src/editor/ui/palette/PaletteBase.svelte +3 -3
  79. package/src/editor/ui/sections/ColumnsSection.svelte +1 -2
  80. package/src/editor/ui/sections/GradientsSection.svelte +1 -1
  81. package/src/editor/ui/sections/OverlaysSection.svelte +1 -1
  82. package/src/editor/ui/sections/ShadowsSection.svelte +1 -1
  83. package/src/system/components/Button.svelte +2 -2
  84. package/src/system/components/Card.svelte +29 -1
  85. package/src/system/components/CollapsibleSection.svelte +25 -2
  86. package/src/system/components/Dialog.svelte +24 -4
  87. package/src/system/components/FloatingTokenTags.css +43 -24
  88. package/src/system/components/FloatingTokenTags.svelte +88 -137
  89. package/src/system/components/Notification.svelte +8 -1
  90. package/src/system/components/SectionDivider.svelte +532 -381
  91. package/src/system/styles/CONVENTIONS.md +1 -1
  92. package/src/system/styles/fonts.css +3 -16
  93. package/src/system/styles/tokens.css +356 -1199
  94. package/src/system/styles/tokens.generated.css +544 -0
  95. package/src/editor/component-editor/scaffolding/DividerEditor.svelte +0 -94
  96. package/src/editor/component-editor/scaffolding/GradientCard.svelte +0 -296
@@ -4,112 +4,115 @@
4
4
 
5
5
  export const component = 'sectiondivider';
6
6
 
7
- type Variant = 'canvas' | 'neutral' | 'alternate' | 'primary' | 'accent' | 'special';
8
- const variants: { key: Variant; title: string }[] = [
9
- { key: 'primary', title: 'Primary' },
10
- { key: 'accent', title: 'Accent' },
11
- { key: 'neutral', title: 'Neutral' },
12
- { key: 'special', title: 'Special' },
13
- { key: 'canvas', title: 'Canvas' },
14
- { key: 'alternate', title: 'Alternate' },
7
+ /** Variant axis = full presets. Each variant owns its size/typography +
8
+ * colors/background, AND its intrinsic display properties (align,
9
+ * hairline, eyebrow + description visibility). */
10
+ type Variant = 'lg' | 'md' | 'sm';
11
+ const variants: { key: Variant; title: string; family: string }[] = [
12
+ { key: 'lg', title: 'Large', family: 'canvas' },
13
+ { key: 'md', title: 'Medium', family: 'canvas' },
14
+ { key: 'sm', title: 'Small', family: 'canvas' },
15
15
  ];
16
16
 
17
- /** Frame tokens for a variant. Gradient tokens are owned by GradientCard, and
18
- * outline thickness/color are rendered nested under the title TypeEditor;
19
- * both are emitted only for `allTokens` (reset/registry) — not for the
20
- * per-state token grid. */
21
- function frameTokens(v: Variant): Token[] {
17
+ function containerTokens(v: Variant): Token[] {
22
18
  return [
23
- // Padding is routed through an internal `--_divider-padding` aggregator
24
- // per variant split mode would require per-side forwarding in every
25
- // variant rule. Until that lands, present single-value only.
26
- { label: 'padding', canBeLinked: true, groupKey: 'padding', variable: `--sectiondivider-${v}-padding`, splittable: false },
27
- { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--sectiondivider-${v}-radius` },
28
- { label: 'border color', canBeLinked: true, groupKey: 'border', variable: `--sectiondivider-${v}-border` },
29
- { label: 'border width', canBeLinked: true, groupKey: 'border-width', variable: `--sectiondivider-${v}-border-width` },
30
- { label: 'drop shadow', canBeLinked: true, groupKey: 'shadow', variable: `--sectiondivider-${v}-shadow` },
19
+ { label: 'corner radius', canBeLinked: true, groupKey: 'radius', variable: `--sectiondivider-${v}-radius`, element: 'Container' },
20
+ { label: 'drop shadow', canBeLinked: true, groupKey: 'shadow', variable: `--sectiondivider-${v}-shadow`, element: 'Container' },
21
+ { label: 'padding', canBeLinked: true, groupKey: 'container-padding', variable: `--sectiondivider-${v}-padding`, element: 'Container' },
22
+ { label: 'border width', canBeLinked: true, groupKey: 'border-width', variable: `--sectiondivider-${v}-border-width`, element: 'Container' },
23
+ { label: 'border color', canBeLinked: true, groupKey: 'border', variable: `--sectiondivider-${v}-border`, element: 'Container' },
24
+ ];
25
+ }
26
+ function typePaddingTokens(v: Variant): Token[] {
27
+ return [
28
+ { label: 'padding', canBeLinked: true, groupKey: 'title-padding', variable: `--sectiondivider-${v}-title-padding`, element: 'title' },
29
+ { label: 'padding', canBeLinked: true, groupKey: 'description-padding', variable: `--sectiondivider-${v}-description-padding`, element: 'description' },
30
+ { label: 'padding', canBeLinked: true, groupKey: 'eyebrow-padding', variable: `--sectiondivider-${v}-eyebrow-padding`, element: 'eyebrow' },
31
+ ];
32
+ }
33
+ function hairlineTokens(v: Variant): Token[] {
34
+ return [
35
+ { label: 'hairline color', canBeLinked: true, groupKey: 'hairline-color', variable: `--sectiondivider-${v}-hairline-color`, element: 'hairline' },
36
+ { label: 'hairline thickness', canBeLinked: true, groupKey: 'hairline-thickness', variable: `--sectiondivider-${v}-hairline-thickness`, element: 'hairline' },
31
37
  ];
32
38
  }
33
- /** Title outline tokens — rendered inside the title TypeEditor (not the
34
- * property grid) but registered here so reset/linkage still see them. */
35
39
  function titleOutlineTokens(v: Variant): Token[] {
36
40
  return [
37
- { label: 'outline thickness', canBeLinked: true, groupKey: 'title-border-width', variable: `--sectiondivider-${v}-title-border-width` },
38
- { label: 'outline color', canBeLinked: true, groupKey: 'title-stroke-color', variable: `--sectiondivider-${v}-title-stroke-color` },
41
+ { label: 'outline thickness', canBeLinked: true, groupKey: 'title-outline-width', variable: `--sectiondivider-${v}-title-outline-width`, element: 'title' },
42
+ { label: 'outline color', canBeLinked: true, groupKey: 'title-outline-color', variable: `--sectiondivider-${v}-title-outline-color`, element: 'title' },
39
43
  ];
40
44
  }
41
- function gradientTokens(v: Variant): Token[] {
45
+ function backgroundTokens(v: Variant): Token[] {
42
46
  return [
43
- { label: 'gradient angle', groupKey: 'angle', variable: `--sectiondivider-${v}-gradient-angle` },
44
- { label: 'gradient stop 1 color', groupKey: 'color', variable: `--sectiondivider-${v}-gradient-stop-1-color` },
45
- { label: 'gradient stop 1 position', groupKey: 'position', variable: `--sectiondivider-${v}-gradient-stop-1-position` },
46
- { label: 'gradient stop 2 color', groupKey: 'color', variable: `--sectiondivider-${v}-gradient-stop-2-color` },
47
- { label: 'gradient stop 2 position', groupKey: 'position', variable: `--sectiondivider-${v}-gradient-stop-2-position` },
48
- { label: 'gradient stop 3 color', groupKey: 'color', variable: `--sectiondivider-${v}-gradient-stop-3-color` },
49
- { label: 'gradient stop 3 position', groupKey: 'position', variable: `--sectiondivider-${v}-gradient-stop-3-position` },
47
+ { label: 'background', groupKey: 'background', variable: `--sectiondivider-${v}-background`, kind: 'gradient', family: variants.find((x) => x.key === v)!.family },
50
48
  ];
51
49
  }
52
50
  function variantTokens(v: Variant): Token[] {
53
- return [...frameTokens(v), ...titleOutlineTokens(v), ...gradientTokens(v)];
51
+ return [...containerTokens(v), ...hairlineTokens(v), ...titleOutlineTokens(v), ...backgroundTokens(v), ...typePaddingTokens(v)];
54
52
  }
55
-
56
- /** Token list passed to VariantGroup.states + sibling builder so Copy-from
57
- * iterates ALL tokens (frame + outline + gradient), not just the visible
58
- * frame. Outline + gradient are flagged `hidden` so the property grid still
59
- * shows only frame tokens — outline renders inside the title TypeEditor,
60
- * gradient renders in GradientCard. Mismatch between this list and the
61
- * sibling list would mis-align positional copy. */
62
53
  function stateTokens(v: Variant): Token[] {
63
54
  return [
64
- ...frameTokens(v),
55
+ ...containerTokens(v),
56
+ ...hairlineTokens(v),
65
57
  ...titleOutlineTokens(v).map((t) => ({ ...t, hidden: true })),
66
- ...gradientTokens(v).map((t) => ({ ...t, hidden: true })),
58
+ ...backgroundTokens(v).map((t) => ({ ...t, hidden: true })),
59
+ ...typePaddingTokens(v),
67
60
  ];
68
61
  }
69
62
 
70
- /** Two type groups per variant: title and description. The TypeEditor
71
- * fieldset visually groups each color with its font-shape props. The
72
- * title fieldset also nests the SVG-text outline width + color so those
73
- * controls sit with the typography that drives them. */
74
63
  function variantTypeGroups(v: Variant): TypeGroupConfig[] {
75
64
  return [
76
65
  {
77
- legend: 'title',
66
+ legend: '',
67
+ element: 'title',
78
68
  colorVariable: `--sectiondivider-${v}-title`,
79
69
  familyVariable: `--sectiondivider-${v}-title-font-family`,
80
70
  sizeVariable: `--sectiondivider-${v}-title-font-size`,
81
71
  weightVariable: `--sectiondivider-${v}-title-font-weight`,
82
72
  lineHeightVariable: `--sectiondivider-${v}-title-line-height`,
83
- outlineWidthVariable: `--sectiondivider-${v}-title-border-width`,
84
- outlineColorVariable: `--sectiondivider-${v}-title-stroke-color`,
73
+ letterSpacingVariable: `--sectiondivider-${v}-title-letter-spacing`,
74
+ outlineWidthVariable: `--sectiondivider-${v}-title-outline-width`,
75
+ outlineColorVariable: `--sectiondivider-${v}-title-outline-color`,
85
76
  },
86
77
  {
87
- legend: 'description',
78
+ legend: '',
79
+ element: 'description',
88
80
  colorVariable: `--sectiondivider-${v}-description`,
89
81
  familyVariable: `--sectiondivider-${v}-description-font-family`,
90
82
  sizeVariable: `--sectiondivider-${v}-description-font-size`,
91
83
  weightVariable: `--sectiondivider-${v}-description-font-weight`,
92
84
  lineHeightVariable: `--sectiondivider-${v}-description-line-height`,
93
85
  },
86
+ {
87
+ legend: '',
88
+ element: 'eyebrow',
89
+ colorVariable: `--sectiondivider-${v}-eyebrow`,
90
+ familyVariable: `--sectiondivider-${v}-eyebrow-font-family`,
91
+ sizeVariable: `--sectiondivider-${v}-eyebrow-font-size`,
92
+ weightVariable: `--sectiondivider-${v}-eyebrow-font-weight`,
93
+ letterSpacingVariable: `--sectiondivider-${v}-eyebrow-letter-spacing`,
94
+ },
94
95
  ];
95
96
  }
96
97
 
97
- /** Token registry entries for the type-group properties. Colors are emitted
98
- * with canBeLinked so the LinkedBlock keeps the cross-variant linkage they
99
- * ship with (title/description colors share the same theme defaults across
100
- * every variant). */
101
98
  function variantTypeGroupTokens(v: Variant): Token[] {
102
99
  return [
103
100
  { label: 'title color', canBeLinked: true, groupKey: 'title-color', variable: `--sectiondivider-${v}-title` },
104
101
  { label: 'title font family', canBeLinked: true, groupKey: 'title-font-family', variable: `--sectiondivider-${v}-title-font-family` },
105
- { label: 'title font size', canBeLinked: true, groupKey: 'title-font-size', variable: `--sectiondivider-${v}-title-font-size` },
106
102
  { label: 'title font weight', canBeLinked: true, groupKey: 'title-font-weight', variable: `--sectiondivider-${v}-title-font-weight` },
103
+ { label: 'title font size', variable: `--sectiondivider-${v}-title-font-size` },
107
104
  { label: 'title line height', canBeLinked: true, groupKey: 'title-line-height', variable: `--sectiondivider-${v}-title-line-height` },
105
+ { label: 'title letter spacing', canBeLinked: true, groupKey: 'title-letter-spacing', variable: `--sectiondivider-${v}-title-letter-spacing` },
108
106
  { label: 'description color', canBeLinked: true, groupKey: 'description-color', variable: `--sectiondivider-${v}-description` },
109
107
  { label: 'description font family', canBeLinked: true, groupKey: 'description-font-family', variable: `--sectiondivider-${v}-description-font-family` },
110
- { label: 'description font size', canBeLinked: true, groupKey: 'description-font-size', variable: `--sectiondivider-${v}-description-font-size` },
111
108
  { label: 'description font weight', canBeLinked: true, groupKey: 'description-font-weight', variable: `--sectiondivider-${v}-description-font-weight` },
109
+ { label: 'description font size', variable: `--sectiondivider-${v}-description-font-size` },
112
110
  { label: 'description line height', canBeLinked: true, groupKey: 'description-line-height', variable: `--sectiondivider-${v}-description-line-height` },
111
+ { label: 'eyebrow color', canBeLinked: true, groupKey: 'eyebrow-color', variable: `--sectiondivider-${v}-eyebrow` },
112
+ { label: 'eyebrow font family', canBeLinked: true, groupKey: 'eyebrow-font-family', variable: `--sectiondivider-${v}-eyebrow-font-family` },
113
+ { label: 'eyebrow font weight', canBeLinked: true, groupKey: 'eyebrow-font-weight', variable: `--sectiondivider-${v}-eyebrow-font-weight` },
114
+ { label: 'eyebrow font size', variable: `--sectiondivider-${v}-eyebrow-font-size` },
115
+ { label: 'eyebrow letter spacing', canBeLinked: true, groupKey: 'eyebrow-letter-spacing', variable: `--sectiondivider-${v}-eyebrow-letter-spacing` },
113
116
  ];
114
117
  }
115
118
 
@@ -118,14 +121,15 @@
118
121
  ...variantTypeGroupTokens(v.key),
119
122
  ]);
120
123
 
121
- // Linked block contexts: every linkable token registers under its variant name.
122
- // Gradient stops are intentionally absent — they're variant-defining and never link.
123
124
  const LINKED_GROUP_KEYS = [
124
- 'padding', 'radius',
125
- 'border', 'border-width', 'shadow',
126
- 'title-border-width', 'title-stroke-color',
127
- 'title-color', 'title-font-family', 'title-font-size', 'title-font-weight', 'title-line-height',
128
- 'description-color', 'description-font-family', 'description-font-size', 'description-font-weight', 'description-line-height',
125
+ 'container-padding', 'radius', 'border', 'border-width', 'shadow',
126
+ 'hairline-color', 'hairline-thickness',
127
+ 'title-outline-width', 'title-outline-color',
128
+ 'title-color', 'description-color', 'eyebrow-color',
129
+ 'title-padding', 'description-padding', 'eyebrow-padding',
130
+ 'title-font-family', 'title-font-weight', 'title-line-height', 'title-letter-spacing',
131
+ 'description-font-family', 'description-font-weight', 'description-line-height',
132
+ 'eyebrow-font-family', 'eyebrow-font-weight', 'eyebrow-letter-spacing',
129
133
  ] as const;
130
134
  const linkableContexts = new Map<string, string>(
131
135
  variants.flatMap((v) =>
@@ -136,28 +140,212 @@
136
140
  );
137
141
 
138
142
  const variantOptions = variants.map((v) => ({ value: v.key, label: v.title }));
143
+
144
+ type Align = 'start' | 'center';
145
+ type HairlinePosition =
146
+ | 'above-label'
147
+ | 'through-label'
148
+ | 'below-label'
149
+ | 'above-description'
150
+ | 'through-description'
151
+ | 'below-description';
139
152
  </script>
140
153
 
141
154
  <script lang="ts">
142
155
  import SectionDivider from '../../system/components/SectionDivider.svelte';
143
156
  import VariantGroup from './scaffolding/VariantGroup.svelte';
144
157
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
145
- import GradientCard from './scaffolding/GradientCard.svelte';
146
- import { editorState } from '../core/store/editorStore';
158
+ import GradientEditor from '../ui/GradientEditor.svelte';
159
+ import { editorState, mutate, setComponentAlias, setComponentConfig } from '../core/store/editorStore';
160
+ import { componentGradientSource } from '../core/store/gradientSource';
147
161
  import { computeLinkedBlock, withLinkedDisabled } from './scaffolding/linkedBlock';
162
+ import { KNOWN_FAMILIES, swapTokenFamily } from '../core/palettes/familySwap';
163
+
164
+ // Variants list above carries a default family ('canvas') used as the starting
165
+ // value for new presets. Live family is per-variant config so the user can
166
+ // swap a divider's color world without touching the variants array.
167
+ const FAMILY_OPTIONS: { value: string; label: string }[] = KNOWN_FAMILIES.map((f) => ({
168
+ value: f,
169
+ label: f.charAt(0).toUpperCase() + f.slice(1),
170
+ }));
171
+
172
+ const SAMPLE_TITLE: Record<Variant, string> = {
173
+ lg: 'Large Section',
174
+ md: 'Medium Section',
175
+ sm: 'Small Section',
176
+ };
177
+ const SAMPLE_EYEBROW = 'Section Eyebrow';
178
+ const SAMPLE_DESCRIPTION = 'This text is meant to provide additional context or meaning.';
179
+
180
+ // Intrinsic per-variant properties live in the aliases bucket as literal
181
+ // CssVarRefs, so they cascade to `:root` via cssVarSync and reach every
182
+ // live consumer instance. `color-family` is editor metadata (drives the
183
+ // family-swap rewrite, not a runtime CSS value) and stays in the config
184
+ // bucket.
185
+ let aliases = $derived(($editorState.components[component]?.aliases ?? {}) as Record<string, import('../core/store/editorTypes').CssVarRef>);
186
+ let cfg = $derived(($editorState.components[component]?.config ?? {}) as Record<string, unknown>);
148
187
 
149
- let testTitle = $state('Section Title');
150
- let showDescription = $state(true);
151
- let descriptionText = $state('This text is meant to provide additional context or meaning.');
188
+ function readLiteral(key: string): string | undefined {
189
+ const ref = aliases[key];
190
+ if (!ref || ref.kind !== 'literal') return undefined;
191
+ return ref.value;
192
+ }
193
+
194
+ function getAlign(v: Variant): Align {
195
+ return readLiteral(`--sectiondivider-${v}-align`) === 'start' ? 'start' : 'center';
196
+ }
197
+ function getColorFamily(v: Variant): string {
198
+ const raw = cfg[`--sectiondivider-${v}-color-family`];
199
+ if (typeof raw === 'string' && (KNOWN_FAMILIES as readonly string[]).includes(raw)) return raw;
200
+ return variants.find((x) => x.key === v)!.family;
201
+ }
202
+ /** Active hairline position OR `'none'` (= hidden). */
203
+ function getHairlineValue(v: Variant): HairlinePosition | 'none' {
204
+ const raw = readLiteral(`--sectiondivider-${v}-hairline`);
205
+ if (raw === undefined || raw === 'none') return 'none';
206
+ // 'above-description' renders identically to 'below-label' — coerce on read
207
+ // so the dropdown's option list (which omits 'above-description') stays valid.
208
+ if (raw === 'above-description') return 'below-label';
209
+ const positions: HairlinePosition[] = ['above-label', 'through-label', 'below-label', 'through-description', 'below-description'];
210
+ return (positions as string[]).includes(raw) ? (raw as HairlinePosition) : 'none';
211
+ }
212
+ function getShowHairline(v: Variant): boolean {
213
+ return getHairlineValue(v) !== 'none';
214
+ }
215
+ /** Position bound to the position select. Falls back to 'above-label' when
216
+ * hairline is hidden, so the dropdown displays a sensible value while
217
+ * toggled off. */
218
+ function getHairlinePosition(v: Variant): HairlinePosition {
219
+ const val = getHairlineValue(v);
220
+ return val === 'none' ? 'above-label' : val;
221
+ }
222
+ function getShowEyebrow(v: Variant): boolean {
223
+ return readLiteral(`--sectiondivider-${v}-eyebrow-display`) === 'block';
224
+ }
225
+ function getEyebrowUppercase(v: Variant): boolean {
226
+ return readLiteral(`--sectiondivider-${v}-eyebrow-text-transform`) === 'uppercase';
227
+ }
228
+ function getShowDescription(v: Variant): boolean {
229
+ const raw = readLiteral(`--sectiondivider-${v}-description-display`);
230
+ // Default: shown (flex) when unset. Matches the :root default and the
231
+ // legacy `getShowDescription` semantics for files that never set the key.
232
+ if (raw === undefined) return true;
233
+ return raw === 'flex';
234
+ }
235
+ /** Write an intrinsic to the aliases bucket as a literal so it cascades
236
+ * through cssVarSync to `:root` on both the editor iframe and host page. */
237
+ function setIntrinsic(v: Variant, prop: string, value: string) {
238
+ setComponentAlias(component, `--sectiondivider-${v}-${prop}`, { kind: 'literal', value });
239
+ }
240
+ function setCfg(v: Variant, prop: string, value: string) {
241
+ setComponentConfig(component, `--sectiondivider-${v}-${prop}`, value);
242
+ }
152
243
 
153
244
  let linked = $derived(computeLinkedBlock(component, linkableContexts, allTokens, $editorState));
154
- // Pass the full stateTokens list (with outline/gradient hidden) so positional
155
- // Copy-from covers every variable. TokenLayout filters hidden tokens from the
156
- // grid render path, so the user still sees only frame tokens.
157
245
  let visibleVariantTokens = $derived((v: Variant) => withLinkedDisabled(stateTokens(v), linked.varSet));
246
+
247
+ const gradientSources = Object.fromEntries(
248
+ variants.map((v) => [v.key, componentGradientSource(component, `--sectiondivider-${v.key}-background`)]),
249
+ ) as Record<Variant, ReturnType<typeof componentGradientSource>>;
250
+
251
+ // Family swap on color-family change: for every alias under this variant
252
+ // that currently references `oldFamily`, rewrite the reference to
253
+ // `newFamily`. Aliases targeting other families are left alone so an
254
+ // intentionally-cross-family border doesn't get swept along. Gradient stops
255
+ // flagged `monochrome: false` are off-palette overrides — those skip the
256
+ // rewrite so the user's deliberate non-family stop survives the swap.
257
+ function remapFamily(v: Variant, oldFamily: string, newFamily: string) {
258
+ if (oldFamily === newFamily) return;
259
+ const prefix = `--sectiondivider-${v}-`;
260
+ mutate(`color-family remap ${v} ${oldFamily}->${newFamily}`, (s) => {
261
+ const slice = s.components[component];
262
+ if (!slice) return;
263
+ for (const [key, ref] of Object.entries(slice.aliases)) {
264
+ if (!key.startsWith(prefix)) continue;
265
+ if (ref.kind === 'token') {
266
+ const swapped = swapTokenFamily(ref.name, oldFamily, newFamily);
267
+ if (swapped !== ref.name) {
268
+ slice.aliases[key] = { kind: 'token', name: swapped };
269
+ }
270
+ } else if (ref.kind === 'gradient') {
271
+ let changed = false;
272
+ const stops = ref.value.stops.map((stop) => {
273
+ if (stop.monochrome === false) return stop;
274
+ const swapped = swapTokenFamily(stop.color, oldFamily, newFamily);
275
+ if (swapped === stop.color) return stop;
276
+ changed = true;
277
+ return { ...stop, color: swapped };
278
+ });
279
+ if (changed) {
280
+ slice.aliases[key] = {
281
+ kind: 'gradient',
282
+ value: {
283
+ type: ref.value.type,
284
+ angle: ref.value.angle,
285
+ ...(ref.value.radius !== undefined ? { radius: ref.value.radius } : {}),
286
+ ...(ref.value.centerX !== undefined ? { centerX: ref.value.centerX } : {}),
287
+ ...(ref.value.aspectX !== undefined ? { aspectX: ref.value.aspectX } : {}),
288
+ ...(ref.value.aspectY !== undefined ? { aspectY: ref.value.aspectY } : {}),
289
+ stops,
290
+ },
291
+ };
292
+ }
293
+ }
294
+ }
295
+ });
296
+ }
297
+
298
+ // "None" on the background segmented control clears the container outright:
299
+ // the gradient flips to `type: 'none'` (handled inside GradientEditor) and
300
+ // we follow through here by clearing the border color too — a convenience
301
+ // so the user doesn't have to chase the residual outline. The user can
302
+ // re-set the border independently afterwards; changing border alone never
303
+ // bounces the background back to solid.
304
+ function clearContainerBorder(v: Variant) {
305
+ setComponentAlias(component, `--sectiondivider-${v}-border`, { kind: 'literal', value: 'transparent' });
306
+ }
307
+
308
+ function hairlineOptions(v: Variant): { value: HairlinePosition; label: string }[] {
309
+ const base: { value: HairlinePosition; label: string }[] = [
310
+ { value: 'above-label', label: 'Above title' },
311
+ { value: 'through-label', label: 'Through title' },
312
+ { value: 'below-label', label: 'Below title' },
313
+ ];
314
+ if (!getShowDescription(v)) return base;
315
+ return [
316
+ ...base,
317
+ { value: 'through-description', label: 'Through description' },
318
+ { value: 'below-description', label: 'Below description' },
319
+ ];
320
+ }
321
+
322
+ // Hairline show + position are now one var. Toggle off → 'none'; toggle on
323
+ // → restore the position currently displayed in the dropdown (or
324
+ // 'above-label' default). Description-targeted hairlines are suppressed by
325
+ // a container style query when description-display is 'none', so no
326
+ // editor-side snap is needed at runtime.
327
+ function elementTogglesFor(v: Variant) {
328
+ return {
329
+ description: {
330
+ checked: getShowDescription(v),
331
+ label: 'Show description',
332
+ onchange: (c: boolean) => setIntrinsic(v, 'description-display', c ? 'flex' : 'none'),
333
+ },
334
+ eyebrow: {
335
+ checked: getShowEyebrow(v),
336
+ label: 'Show eyebrow',
337
+ onchange: (c: boolean) => setIntrinsic(v, 'eyebrow-display', c ? 'block' : 'none'),
338
+ },
339
+ hairline: {
340
+ checked: getShowHairline(v),
341
+ label: 'Show',
342
+ onchange: (c: boolean) => setIntrinsic(v, 'hairline', c ? getHairlinePosition(v) : 'none'),
343
+ },
344
+ };
345
+ }
158
346
  </script>
159
347
 
160
- <ComponentEditorBase {component} title="Section Divider" description="Full-width section banner with display font and palette variants." tokens={allTokens} {linked} variants={variantOptions}>
348
+ <ComponentEditorBase {component} title="Section Divider" description="Full-width section banner. Each variant (lg/md/sm) is a full preset: its own size, typography, colors, AND intrinsic properties (alignment, hairline, eyebrow/description visibility)." tokens={allTokens} {linked} variants={variantOptions}>
161
349
  {#each variants as v}
162
350
  <VariantGroup
163
351
  name={v.key}
@@ -171,68 +359,207 @@
171
359
  (sv) => ({ [sv]: stateTokens(sv) }),
172
360
  (sv) => ({ [sv]: variantTypeGroups(sv) }),
173
361
  )}
174
- backdropPadding="32px"
362
+ backdropPadding="20px"
363
+ elementToggles={elementTogglesFor(v.key)}
364
+ elementOrder={['Container', 'title', 'description', 'eyebrow', 'hairline']}
175
365
  >
176
- {#snippet canvasToolbarExtras()}
177
- <hr class="canvas-toolbar-divider" />
178
- <label class="toolbar-field">
179
- <span>Test title</span>
180
- <input type="text" class="canvas-toolbar-input" bind:value={testTitle} placeholder="Section Title" />
181
- </label>
182
- <label class="toolbar-check">
183
- <input type="checkbox" bind:checked={showDescription} />
184
- <span>Show description</span>
185
- </label>
186
- <label class="toolbar-field">
187
- <span>Description text</span>
188
- <input type="text" class="canvas-toolbar-input" bind:value={descriptionText} placeholder="Description text" />
189
- </label>
190
- {/snippet}
191
366
  <div class="section-divider-stage">
192
367
  <SectionDivider
193
- title={testTitle || v.title}
368
+ title={SAMPLE_TITLE[v.key]}
194
369
  variant={v.key}
195
- description={showDescription ? descriptionText : undefined}
370
+ description={SAMPLE_DESCRIPTION}
371
+ eyebrow={SAMPLE_EYEBROW}
196
372
  />
197
373
  </div>
198
374
  {#snippet compositeControls(_stateName)}
199
- <span class="gradient-section-label">Gradient</span>
200
- <GradientCard {component} prefix={`--sectiondivider-${v.key}`} />
375
+ <div class="gradient-bg-section">
376
+ <GradientEditor
377
+ sectionLabel="Background"
378
+ source={gradientSources[v.key]}
379
+ stopIdPrefix={`sectiondivider-${v.key}`}
380
+ familyFilter={getColorFamily(v.key)}
381
+ showNone
382
+ onNone={() => clearContainerBorder(v.key)}
383
+ />
384
+ </div>
385
+ {/snippet}
386
+ {#snippet extraPropertyRowsTop(_stateName)}
387
+ <div class="property-row sd-intrinsic-row">
388
+ <span class="property-label">alignment</span>
389
+ <select
390
+ class="sd-intrinsic-select"
391
+ value={getAlign(v.key)}
392
+ onchange={(e) => setIntrinsic(v.key, 'align', (e.currentTarget as HTMLSelectElement).value)}
393
+ >
394
+ <option value="center">Center</option>
395
+ <option value="start">Start</option>
396
+ </select>
397
+ </div>
398
+ <div class="property-row sd-intrinsic-row">
399
+ <span class="property-label">color family</span>
400
+ <select
401
+ class="sd-intrinsic-select"
402
+ value={getColorFamily(v.key)}
403
+ onchange={(e) => {
404
+ const next = (e.currentTarget as HTMLSelectElement).value;
405
+ const prev = getColorFamily(v.key);
406
+ setCfg(v.key, 'color-family', next);
407
+ remapFamily(v.key, prev, next);
408
+ }}
409
+ >
410
+ {#each FAMILY_OPTIONS as opt (opt.value)}
411
+ <option value={opt.value}>{opt.label}</option>
412
+ {/each}
413
+ </select>
414
+ </div>
415
+ {/snippet}
416
+ {#snippet elementExtras(elementName)}
417
+ {#if elementName === 'eyebrow'}
418
+ <label class="sd-element-check">
419
+ <input
420
+ type="checkbox"
421
+ checked={getEyebrowUppercase(v.key)}
422
+ onchange={(e) => setIntrinsic(v.key, 'eyebrow-text-transform', (e.currentTarget as HTMLInputElement).checked ? 'uppercase' : 'none')}
423
+ />
424
+ <span>All caps</span>
425
+ </label>
426
+ {/if}
427
+ {#if elementName === 'hairline'}
428
+ <div class="property-row sd-intrinsic-row sd-hairline-position-row">
429
+ <span class="property-label">position</span>
430
+ <select
431
+ class="sd-intrinsic-select"
432
+ value={getHairlinePosition(v.key)}
433
+ onchange={(e) => setIntrinsic(v.key, 'hairline', (e.currentTarget as HTMLSelectElement).value)}
434
+ >
435
+ {#each hairlineOptions(v.key) as opt (opt.value)}
436
+ <option value={opt.value}>{opt.label}</option>
437
+ {/each}
438
+ </select>
439
+ </div>
440
+ {/if}
201
441
  {/snippet}
202
442
  </VariantGroup>
203
443
  {/each}
204
444
  </ComponentEditorBase>
205
445
 
206
446
  <style>
207
- .toolbar-check {
447
+ .section-divider-stage {
448
+ width: 100%;
449
+ min-width: 32rem;
450
+ }
451
+ /* The shipped SectionDivider carries a 24px block margin so it breathes
452
+ inside a real page. In the editor preview the backdrop already supplies
453
+ framing space, so collapse the margin here to keep the stage tight. */
454
+ .section-divider-stage :global(.section-divider) {
455
+ margin-block: 0;
456
+ }
457
+
458
+
459
+ /* Mirror ShadowBackdrop's preview/controls grid so the ribbon's left/right
460
+ edges land on the preview-stage edges above AND the radial pad lands in
461
+ the same column as the canvas-toolbar above.
462
+ - col 1 (1fr) = preview-stage area; its content sits at left = 1.5rem
463
+ and right edge = parent right - 11rem - 48px
464
+ - col 2 (11rem) = canvas-toolbar area; left edge = parent right - 11rem - 8px
465
+ - gap = 40px (32px backdrop-content right padding + 8px controls-cell left padding)
466
+ - parent padding-right = 8px (matches the controls-cell's right inset)
467
+ For non-radial gradients the editor occupies col 1 only; col 2 stays
468
+ empty. For radial (has-pad), the editor uses subgrid so its inner ribbon
469
+ and pad align with the outer columns. */
470
+ .gradient-bg-section {
471
+ display: grid;
472
+ grid-template-columns: minmax(0, 1fr) 11rem;
473
+ column-gap: 40px;
474
+ padding-left: 1.5rem;
475
+ padding-right: 8px;
476
+ margin-top: var(--ui-space-16);
477
+ box-sizing: border-box;
478
+ }
479
+ .gradient-bg-section > :global(.gradient-editor) {
480
+ grid-column: 1;
481
+ }
482
+ .gradient-bg-section > :global(.gradient-editor.has-pad) {
483
+ grid-column: 1 / -1;
484
+ grid-template-columns: subgrid;
485
+ column-gap: 40px;
486
+ }
487
+ @container variant-group (max-width: 32rem) {
488
+ .gradient-bg-section {
489
+ grid-template-columns: minmax(0, 1fr);
490
+ padding-right: 32px;
491
+ }
492
+ /* Collapsed: parent is 1-col. Drop subgrid and let the editor fall back to
493
+ its native 2-col (ribbon | pad) layout within the single column. */
494
+ .gradient-bg-section > :global(.gradient-editor.has-pad) {
495
+ grid-column: 1;
496
+ grid-template-columns: minmax(0, 1fr) max-content;
497
+ column-gap: var(--ui-space-16);
498
+ }
499
+ }
500
+
501
+ /* Intrinsic-property rows. Sit in the Properties section under the token
502
+ grid (via VariantGroup.extraPropertyRows). The grid columns are inherited
503
+ from the parent so labels line up with token labels above. */
504
+ /* Inline boolean control inside an element-section (e.g. "All caps" under
505
+ the eyebrow heading). Sits flush-left so it reads as a section property
506
+ rather than aligning with the token grid below. */
507
+ :global(.sd-element-check) {
208
508
  display: inline-flex;
209
509
  align-items: center;
210
510
  gap: var(--ui-space-6);
211
511
  font-size: var(--ui-font-size-sm);
212
- color: rgba(255, 255, 255, 0.78);
512
+ color: var(--ui-text-secondary);
213
513
  cursor: pointer;
514
+ user-select: none;
515
+ margin-bottom: var(--ui-space-4);
214
516
  }
517
+ :global(.sd-element-check:hover) { color: var(--ui-text-primary); }
518
+ :global(.sd-element-check input) { margin: 0; cursor: pointer; }
215
519
 
216
- .toolbar-field {
217
- display: flex;
218
- flex-direction: column;
219
- gap: var(--ui-space-4);
220
- font-size: var(--ui-font-size-xs);
221
- color: rgba(255, 255, 255, 0.6);
520
+ /* Property-row inside an element-section (e.g. hairline position) — the
521
+ extra-property-rows grid lives in VariantGroup and only applies above
522
+ the token list. Mirror its column layout here so the position row's
523
+ label/select reads consistent with the alignment + color family rows. */
524
+ :global(.sd-hairline-position-row.property-row) {
525
+ display: grid;
526
+ grid-template-columns: minmax(8rem, max-content) 1fr;
527
+ column-gap: var(--ui-space-16);
528
+ align-items: center;
529
+ min-height: 1.75rem;
530
+ margin-bottom: var(--ui-space-4);
222
531
  }
223
-
224
- /* Floor the preview width so the divider always reads as a banner, even
225
- when the canvas gets cramped (e.g. on narrower editor viewports). */
226
- .section-divider-stage {
227
- width: 100%;
228
- min-width: 32rem;
532
+ :global(.sd-hairline-position-row .property-label) {
533
+ font-size: var(--ui-font-size-sm);
534
+ color: var(--ui-text-secondary);
229
535
  }
230
536
 
231
- .gradient-section-label {
232
- display: block;
233
- margin-top: var(--ui-space-8);
234
- font-size: var(--ui-font-size-md);
235
- font-weight: 500;
537
+ :global(.sd-intrinsic-select) {
538
+ appearance: none;
539
+ -webkit-appearance: none;
540
+ justify-self: start;
541
+ width: max-content;
542
+ min-width: 8rem;
543
+ padding: 0 var(--ui-space-24) 0 var(--ui-space-8);
544
+ min-height: 1.75rem;
545
+ background-color: var(--ui-surface-lowest);
546
+ 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");
547
+ background-repeat: no-repeat;
548
+ background-position: right var(--ui-space-8) center;
549
+ border: 1px solid var(--ui-border-low);
550
+ border-radius: var(--ui-radius-sm);
236
551
  color: var(--ui-text-primary);
552
+ font-family: var(--ui-font-sans);
553
+ font-size: var(--ui-font-size-sm);
554
+ cursor: pointer;
555
+ }
556
+ :global(.sd-intrinsic-select:hover) {
557
+ background-color: var(--ui-surface-low);
558
+ border-color: var(--ui-border);
237
559
  }
560
+ :global(.sd-intrinsic-select:focus-visible) {
561
+ outline: 2px solid var(--ui-highlight);
562
+ outline-offset: 2px;
563
+ }
564
+
238
565
  </style>