@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,690 @@
1
+ <script lang="ts">
2
+ import { run } from 'svelte/legacy';
3
+
4
+ // Visual gradient editor: draggable stop diamonds on a live ribbon.
5
+ // Bound to a GradientSource (theme via `variable`, or component via `source`).
6
+ import { tick, onMount } from 'svelte';
7
+ import { get } from 'svelte/store';
8
+ import type { GradientType, GradientTokenStop } from '../core/store/editorTypes';
9
+ import {
10
+ themeGradientSource,
11
+ snapshotGradient,
12
+ type GradientSource,
13
+ type GradientSourceSnapshot,
14
+ } from '../core/store/gradientSource';
15
+ import GradientStopPicker from './GradientStopPicker.svelte';
16
+ import AngleDial from '../component-editor/scaffolding/AngleDial.svelte';
17
+ import RadialShapePad from '../component-editor/scaffolding/RadialShapePad.svelte';
18
+ import UISegmentedControl from './UISegmentedControl.svelte';
19
+ import UIPillButton from './UIPillButton.svelte';
20
+ import { snapTokenToFamily } from '../core/palettes/familySwap';
21
+
22
+ interface Props {
23
+ /** Theme-gradient mode: variable name (e.g. `--gradient-1`). */
24
+ variable?: string;
25
+ /** Component-gradient mode: source adapter. Wins over `variable`. */
26
+ source?: GradientSource;
27
+ /** Header label above the ribbon; turns the editor into a 2-col grid. */
28
+ sectionLabel?: string;
29
+ /** Stable id for per-stop picker scratch vars. */
30
+ stopIdPrefix?: string;
31
+ /** Greys out tokens outside this family prefix in the stop picker. */
32
+ familyFilter?: string | null;
33
+ /** Show the "None" segment so the user can clear the fill outright. */
34
+ showNone?: boolean;
35
+ /** Called when the user picks "None" so the parent can zero ancillary tokens. */
36
+ onNone?: () => void;
37
+ onsave?: () => void;
38
+ oncancel?: () => void;
39
+ }
40
+
41
+ let {
42
+ variable,
43
+ source,
44
+ sectionLabel,
45
+ stopIdPrefix,
46
+ familyFilter = null,
47
+ showNone = false,
48
+ onNone,
49
+ onsave,
50
+ oncancel,
51
+ }: Props = $props();
52
+
53
+ // Captured once: callers remount when the target gradient changes.
54
+ // svelte-ignore state_referenced_locally
55
+ const gradientSource: GradientSource = source ?? themeGradientSource(variable!);
56
+ // Local const so Svelte 5's `$<store>` auto-subscription works.
57
+ const gradientSourceCurrent = gradientSource.current;
58
+ // svelte-ignore state_referenced_locally
59
+ const stopKeyPrefix: string = stopIdPrefix ?? variable ?? 'gradient-edit';
60
+
61
+ // Snapshot at open, restored on Cancel.
62
+ let snapshot: GradientSourceSnapshot | null = null;
63
+ onMount(() => {
64
+ snapshot = snapshotGradient(gradientSource);
65
+ });
66
+
67
+ function save() { onsave?.(); }
68
+ function cancel() {
69
+ if (snapshot) gradientSource.setAll(snapshot);
70
+ oncancel?.();
71
+ }
72
+
73
+ let gradient = $derived($gradientSourceCurrent);
74
+ let stopCount = $derived(gradient?.stops.length ?? 0);
75
+
76
+ let selected = $state(0);
77
+ // Keep `selected` in range as stops are added/removed.
78
+ run(() => {
79
+ if (selected >= stopCount) selected = Math.max(0, stopCount - 1);
80
+ });
81
+
82
+ function setType(type: GradientType) {
83
+ gradientSource.setType(type);
84
+ }
85
+
86
+ function onAngleChange(detail: { value: number }) {
87
+ gradientSource.setAngle(detail.value);
88
+ }
89
+
90
+ function onAspectChange(detail: { x: number; y: number }) {
91
+ gradientSource.setAspect(detail);
92
+ }
93
+
94
+ function setPosition(i: number, pct: number) {
95
+ const clamped = Math.max(0, Math.min(100, Math.round(pct * 10) / 10));
96
+ gradientSource.setStop(i, { position: clamped });
97
+ }
98
+
99
+ function onPositionInput(e: Event) {
100
+ const v = parseFloat((e.target as HTMLInputElement).value);
101
+ if (Number.isFinite(v)) setPosition(selected, v);
102
+ }
103
+
104
+ function handleStopChange(i: number, payload: { color: string; opacity: number }) {
105
+ // Picking a real color while `none` promotes to `solid`; `transparent` keeps `none`.
106
+ if (gradient?.type === 'none' && payload.color !== 'transparent') {
107
+ gradientSource.setType('solid');
108
+ }
109
+ gradientSource.setStop(i, { color: payload.color, opacity: payload.opacity });
110
+ }
111
+
112
+ // Mono on: snap to family. Mono off: mark off-palette so family swaps skip it.
113
+ function handleMonoToggle(i: number, mono: boolean) {
114
+ if (mono && familyFilter) {
115
+ const stop = gradient?.stops[i];
116
+ if (stop) {
117
+ const snapped = snapTokenToFamily(stop.color, familyFilter);
118
+ gradientSource.setStop(i, { monochrome: true, color: snapped });
119
+ return;
120
+ }
121
+ }
122
+ gradientSource.setStop(i, { monochrome: mono });
123
+ }
124
+
125
+ function stopValueLabel(stop: GradientTokenStop): string {
126
+ const op = stop.opacity ?? 100;
127
+ const base = stop.color.startsWith('--') ? stop.color.slice(2) : stop.color;
128
+ return op < 100 ? `${base} (${Math.round(op)}%)` : base;
129
+ }
130
+
131
+ // Inserts at pct inheriting from source stop, then selects after the sort settles.
132
+ async function insertStopAt(pct: number, sourceStop: GradientTokenStop) {
133
+ const clamped = Math.max(0, Math.min(100, Math.round(pct * 10) / 10));
134
+ gradientSource.addStop({
135
+ position: clamped,
136
+ color: sourceStop.color,
137
+ opacity: sourceStop.opacity ?? 100,
138
+ });
139
+ await tick();
140
+ const after = get(gradientSource.current);
141
+ if (after) {
142
+ const idx = after.stops.findIndex((s) => s.position === clamped && s.color === sourceStop.color);
143
+ if (idx >= 0) selected = idx;
144
+ }
145
+ }
146
+
147
+ function addStop() {
148
+ if (!gradient) return;
149
+ const stops = gradient.stops;
150
+ // Midway to the right neighbour, or to 100% if last.
151
+ const anchor = stops[selected] ?? stops[stops.length - 1];
152
+ const next = stops[selected + 1];
153
+ const newPos = next
154
+ ? (anchor.position + next.position) / 2
155
+ : (anchor.position + 100) / 2;
156
+ insertStopAt(newPos, anchor);
157
+ }
158
+
159
+ // Inserts a stop at click position, inheriting from the nearest existing stop.
160
+ function onRibbonClick(e: MouseEvent) {
161
+ if (!gradient || e.button !== 0) return;
162
+ const rect = barEl!.getBoundingClientRect();
163
+ const x = e.clientX - rect.left;
164
+ let pct: number;
165
+ if (gradient.type === 'radial') {
166
+ // Radial: both halves map to the same distance from center.
167
+ const half = rect.width / 2;
168
+ pct = (Math.abs(x - half) / half) * 100;
169
+ } else {
170
+ pct = (x / rect.width) * 100;
171
+ }
172
+ const nearest = gradient.stops.reduce(
173
+ (best, s) => (Math.abs(s.position - pct) < Math.abs(best.position - pct) ? s : best),
174
+ gradient.stops[0],
175
+ );
176
+ insertStopAt(pct, nearest);
177
+ }
178
+
179
+ function removeSelected() {
180
+ if (!gradient || gradient.stops.length <= 2) return;
181
+ gradientSource.removeStop(selected);
182
+ if (selected > 0) selected -= 1;
183
+ }
184
+
185
+ // Ribbon handle drag
186
+ let barEl: HTMLDivElement | undefined = $state();
187
+ let dragIndex: number | null = $state(null);
188
+ // Drag origin side on radial ribbon: lets pointer x map symmetrically to stop position.
189
+ let dragSide: 'left' | 'right' | null = $state(null);
190
+
191
+ function pctFromEvent(e: PointerEvent): number {
192
+ const rect = barEl!.getBoundingClientRect();
193
+ const x = e.clientX - rect.left;
194
+ if (gradient?.type === 'radial') {
195
+ const half = rect.width / 2;
196
+ return dragSide === 'left'
197
+ ? ((half - x) / half) * 100
198
+ : ((x - half) / half) * 100;
199
+ }
200
+ return (x / rect.width) * 100;
201
+ }
202
+
203
+ function onHandleDown(e: PointerEvent, i: number, side: 'left' | 'right' | null = null) {
204
+ selected = i;
205
+ dragIndex = i;
206
+ dragSide = side;
207
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
208
+ setPosition(i, pctFromEvent(e));
209
+ }
210
+ function onHandleMove(e: PointerEvent) {
211
+ if (dragIndex === null) return;
212
+ setPosition(dragIndex, pctFromEvent(e));
213
+ }
214
+ function onHandleUp(e: PointerEvent) {
215
+ if (dragIndex === null) return;
216
+ (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
217
+ dragIndex = null;
218
+ dragSide = null;
219
+ }
220
+
221
+ // Synthesise ribbon background from the snapshot so it renders before the
222
+ // CSS var has been pushed to :root. Radial mirrors stops across 50%.
223
+ function stopColorCss(s: GradientTokenStop): string {
224
+ const base = s.color.startsWith('--') ? `var(${s.color})` : s.color;
225
+ const op = s.opacity ?? 100;
226
+ return op >= 100 ? base : `color-mix(in srgb, ${base} ${Math.round(op)}%, transparent)`;
227
+ }
228
+
229
+ let ribbonBg = $derived.by(() => {
230
+ if (!gradient) return 'transparent';
231
+ if (gradient.type === 'none') return 'transparent';
232
+ if (gradient.type === 'solid') {
233
+ const first = gradient.stops[0];
234
+ return first ? stopColorCss(first) : 'transparent';
235
+ }
236
+ const sorted = gradient.stops.slice().sort((a, b) => a.position - b.position);
237
+ if (gradient.type === 'radial') {
238
+ const leftStops = sorted.slice().reverse().map((s) => `${stopColorCss(s)} ${50 - s.position / 2}%`);
239
+ const rightStops = sorted.map((s) => `${stopColorCss(s)} ${50 + s.position / 2}%`);
240
+ return `linear-gradient(90deg, ${[...leftStops, ...rightStops].join(', ')})`;
241
+ }
242
+ const stopsCss = sorted.map((s) => `${stopColorCss(s)} ${s.position}%`).join(', ');
243
+ return `linear-gradient(90deg, ${stopsCss})`;
244
+ });
245
+
246
+ // Flat (solid/none) = single-stop passive UI. Linear/radial show full chrome.
247
+ let isFlat = $derived(gradient?.type === 'solid' || gradient?.type === 'none');
248
+ let isNone = $derived(gradient?.type === 'none');
249
+ let isRadial = $derived(gradient?.type === 'radial');
250
+ let isLinear = $derived(gradient?.type === 'linear');
251
+ // Right column carries the radial pad or angle dial.
252
+ let hasAside = $derived(isRadial || isLinear);
253
+
254
+ let stopSwatches = $derived((gradient?.stops ?? []).map((s) => {
255
+ const base = s.color.startsWith('--') ? `var(${s.color})` : s.color;
256
+ const op = s.opacity ?? 100;
257
+ return op >= 100 ? base : `color-mix(in srgb, ${base} ${Math.round(op)}%, transparent)`;
258
+ }));
259
+
260
+ type TypeChoice = 'none' | 'solid' | 'linear' | 'radial';
261
+ let typeOptions = $derived(
262
+ [
263
+ ...(showNone ? [{ value: 'none' as TypeChoice, label: 'None' }] : []),
264
+ { value: 'solid' as TypeChoice, label: 'Solid' },
265
+ { value: 'linear' as TypeChoice, label: 'Linear' },
266
+ { value: 'radial' as TypeChoice, label: 'Radial' },
267
+ ],
268
+ );
269
+
270
+ function onTypeSelect(next: TypeChoice) {
271
+ setType(next as GradientType);
272
+ if (next === 'none') onNone?.();
273
+ }
274
+ </script>
275
+
276
+ {#if gradient}
277
+ <div class="gradient-editor" class:has-pad={hasAside}>
278
+ {#if sectionLabel || hasAside}
279
+ <div class="editor-header editor-section-left">
280
+ <span class="editor-section-label">{sectionLabel ?? ''}</span>
281
+ {#if !isFlat}
282
+ <div class="stop-actions">
283
+ <UIPillButton
284
+ variant="secondary"
285
+ size="compact"
286
+ icon="fa-plus"
287
+ title="Add stop"
288
+ onclick={addStop}
289
+ >Add stop</UIPillButton>
290
+ <UIPillButton
291
+ variant="secondary"
292
+ size="compact"
293
+ icon="fa-times"
294
+ title={gradient.stops.length <= 2 ? 'Gradient needs at least two stops' : 'Remove selected stop'}
295
+ disabled={gradient.stops.length <= 2}
296
+ onclick={removeSelected}
297
+ >Remove stop</UIPillButton>
298
+ </div>
299
+ {/if}
300
+ </div>
301
+ {#if isRadial}
302
+ <span class="editor-section-label editor-section-right">Gradient shape</span>
303
+ {:else if isLinear}
304
+ <span class="editor-section-label editor-section-right">Gradient angle</span>
305
+ {/if}
306
+ {/if}
307
+ <div class="ribbon-stack">
308
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
309
+ <div
310
+ class="ribbon"
311
+ class:solid={isFlat}
312
+ class:none={isNone}
313
+ class:radial={isRadial}
314
+ bind:this={barEl}
315
+ style="background: {ribbonBg};"
316
+ onclick={isFlat ? undefined : onRibbonClick}
317
+ role={isFlat ? 'presentation' : 'button'}
318
+ tabindex="-1"
319
+ aria-label={isNone ? 'No fill' : isFlat ? 'Solid color preview' : isRadial ? 'Click the right side to add a gradient stop' : 'Click to add a gradient stop'}
320
+ >
321
+ {#if isRadial}<span class="center-divider" aria-hidden="true"></span>{/if}
322
+ </div>
323
+ {#if !isFlat}
324
+ <div class="handles" class:radial={isRadial}>
325
+ {#if isRadial}
326
+ {#each gradient.stops as stop, i (`mirror-${i}`)}
327
+ <button
328
+ type="button"
329
+ class="handle"
330
+ class:selected={selected === i}
331
+ class:dragging={dragIndex === i && dragSide === 'left'}
332
+ style="left: {50 - stop.position / 2}%; --stop-color: {stopSwatches[i]};"
333
+ onpointerdown={(e) => onHandleDown(e, i, 'left')}
334
+ onpointermove={onHandleMove}
335
+ onpointerup={onHandleUp}
336
+ onpointercancel={onHandleUp}
337
+ title={`Stop ${i + 1} (${stop.position}%) — linked pair`}
338
+ aria-label={`Gradient stop ${i + 1}, mirrored side`}
339
+ >
340
+ <span class="handle-diamond"></span>
341
+ </button>
342
+ {/each}
343
+ {/if}
344
+ {#each gradient.stops as stop, i (i)}
345
+ <button
346
+ type="button"
347
+ class="handle"
348
+ class:selected={selected === i}
349
+ class:dragging={dragIndex === i && dragSide !== 'left'}
350
+ style="left: {isRadial ? 50 + stop.position / 2 : stop.position}%; --stop-color: {stopSwatches[i]};"
351
+ onpointerdown={(e) => onHandleDown(e, i, isRadial ? 'right' : null)}
352
+ onpointermove={onHandleMove}
353
+ onpointerup={onHandleUp}
354
+ onpointercancel={onHandleUp}
355
+ title={`Stop ${i + 1} (${stop.position}%)`}
356
+ aria-label={`Gradient stop ${i + 1}`}
357
+ >
358
+ <span class="handle-diamond"></span>
359
+ </button>
360
+ {/each}
361
+ </div>
362
+ {/if}
363
+ </div>
364
+ {#if gradient.type === 'radial'}
365
+ <div class="ribbon-pad">
366
+ <RadialShapePad
367
+ x={gradient.aspectX ?? 1}
368
+ y={gradient.aspectY ?? 1}
369
+ onchange={onAspectChange}
370
+ />
371
+ </div>
372
+ {:else if gradient.type === 'linear'}
373
+ <div class="ribbon-pad ribbon-pad-linear">
374
+ <AngleDial value={gradient.angle} size={64} orientation="vertical" label="" onchange={onAngleChange} />
375
+ </div>
376
+ {/if}
377
+
378
+ <div class="lower-row">
379
+ <UISegmentedControl
380
+ value={gradient.type as TypeChoice}
381
+ options={typeOptions}
382
+ ariaLabel="Gradient fill type"
383
+ onchange={onTypeSelect}
384
+ />
385
+ {#if gradient.stops[selected]}
386
+ {@const stop = gradient.stops[selected]}
387
+ {@const stopMono = stop.monochrome !== false}
388
+ <div class="stop-edit-row">
389
+ <span class="row-label">{isFlat ? 'Color' : `Stop ${selected + 1}`}</span>
390
+ {#if !isFlat}
391
+ <label class="pos-input">
392
+ <input
393
+ type="number"
394
+ min="0"
395
+ max="100"
396
+ step="0.1"
397
+ value={stop.position}
398
+ onchange={onPositionInput}
399
+ />
400
+ <span class="suffix">%</span>
401
+ </label>
402
+ {/if}
403
+ <div class="picker-column">
404
+ <div class="picker-slot">
405
+ <GradientStopPicker
406
+ stopId={`${stopKeyPrefix}-${selected}`}
407
+ color={stop.color}
408
+ opacity={stop.opacity ?? 100}
409
+ familyFilter={stopMono ? familyFilter : null}
410
+ onchange={(payload) => handleStopChange(selected, payload)}
411
+ />
412
+ </div>
413
+ {#if familyFilter !== null}
414
+ <label class="stop-mono-check">
415
+ <input
416
+ type="checkbox"
417
+ checked={stopMono}
418
+ onchange={(e) => handleMonoToggle(selected, (e.currentTarget as HTMLInputElement).checked)}
419
+ />
420
+ <span>Monochrome</span>
421
+ </label>
422
+ {/if}
423
+ </div>
424
+ <span class="stop-value-text" title={stopValueLabel(stop)}>{stopValueLabel(stop)}</span>
425
+ </div>
426
+ {/if}
427
+ </div>
428
+
429
+ {#if onsave || oncancel}
430
+ <div class="footer-row">
431
+ <UIPillButton variant="secondary" size="compact" onclick={cancel}>Cancel</UIPillButton>
432
+ <UIPillButton variant="primary" size="compact" onclick={save}>Save</UIPillButton>
433
+ </div>
434
+ {/if}
435
+ </div>
436
+ {/if}
437
+
438
+ <style>
439
+ /* Header labels share grid tracks with the ribbon + pad below them. */
440
+ .gradient-editor {
441
+ display: grid;
442
+ grid-template-columns: minmax(0, 1fr);
443
+ row-gap: var(--ui-space-12);
444
+ width: 100%;
445
+ min-width: 0;
446
+ }
447
+ .gradient-editor.has-pad {
448
+ grid-template-columns: minmax(0, 1fr) max-content;
449
+ column-gap: var(--ui-space-16);
450
+ }
451
+
452
+ .editor-section-label {
453
+ font-size: var(--ui-font-size-md);
454
+ font-weight: 500;
455
+ color: var(--ui-text-primary);
456
+ line-height: 1;
457
+ }
458
+ .editor-section-left { grid-column: 1; }
459
+ .editor-section-right { grid-column: 2; }
460
+
461
+ /* Header doubles as a toolbar: label left, Add/Remove flush right. */
462
+ .editor-header {
463
+ display: flex;
464
+ align-items: center;
465
+ justify-content: space-between;
466
+ gap: var(--ui-space-12);
467
+ min-width: 0;
468
+ }
469
+
470
+ .ribbon-stack {
471
+ grid-column: 1;
472
+ display: flex;
473
+ flex-direction: column;
474
+ gap: var(--ui-space-8);
475
+ min-width: 0;
476
+ }
477
+
478
+ /* min-height reserves the radial pad's full height so swapping types doesn't shift the lower row. */
479
+ .ribbon-pad {
480
+ grid-column: 2;
481
+ align-self: start;
482
+ min-height: 94px;
483
+ }
484
+ .ribbon-pad-linear {
485
+ display: flex;
486
+ justify-content: center;
487
+ }
488
+
489
+ .ribbon {
490
+ position: relative;
491
+ height: 3rem;
492
+ border-radius: var(--ui-radius-md);
493
+ border: 1px solid var(--ui-border-low);
494
+ cursor: copy;
495
+ }
496
+
497
+ /* Flat ribbon: passive swatch, no click affordance. */
498
+ .ribbon.solid {
499
+ cursor: default;
500
+ }
501
+
502
+ /* Radial: left half is a mirror, so suppress the copy cursor across the whole ribbon. */
503
+ .ribbon.radial {
504
+ cursor: default;
505
+ }
506
+
507
+ /* Marks the radial center (position 0). */
508
+ .center-divider {
509
+ position: absolute;
510
+ top: 0;
511
+ bottom: 0;
512
+ left: 50%;
513
+ width: 1px;
514
+ background: var(--ui-border);
515
+ pointer-events: none;
516
+ transform: translateX(-0.5px);
517
+ }
518
+
519
+
520
+ .handles {
521
+ position: relative;
522
+ height: 1.25rem;
523
+ }
524
+
525
+ /* 1.25rem hit target; visible marker is the inner .handle-diamond. */
526
+ .handle {
527
+ position: absolute;
528
+ top: 0;
529
+ width: 1.25rem;
530
+ height: 1.25rem;
531
+ margin-left: -0.625rem;
532
+ padding: 0;
533
+ background: transparent;
534
+ border: none;
535
+ cursor: ew-resize;
536
+ touch-action: none;
537
+ display: flex;
538
+ align-items: center;
539
+ justify-content: center;
540
+ }
541
+
542
+ .handle-diamond {
543
+ width: 0.7rem;
544
+ height: 0.7rem;
545
+ background: var(--stop-color, var(--ui-surface-high));
546
+ border: 1px solid var(--ui-border);
547
+ transform: rotate(45deg);
548
+ border-radius: 1px;
549
+ transition: border-color var(--ui-transition-fast), box-shadow var(--ui-transition-fast);
550
+ }
551
+
552
+ .handle:hover .handle-diamond {
553
+ border-color: var(--ui-text-secondary);
554
+ }
555
+
556
+ .handle.selected .handle-diamond {
557
+ border-color: var(--ui-text-primary);
558
+ box-shadow: 0 0 0 1px var(--ui-text-primary);
559
+ }
560
+
561
+ .handle.dragging {
562
+ z-index: 2;
563
+ }
564
+ .handle.dragging .handle-diamond {
565
+ border-color: var(--ui-text-primary);
566
+ box-shadow: 0 0 0 2px var(--ui-text-primary);
567
+ }
568
+
569
+
570
+ /* Per-gradient controls under the ribbon: type selector + stop edit row. */
571
+ .lower-row {
572
+ grid-column: 1 / -1;
573
+ display: flex;
574
+ align-items: flex-start;
575
+ gap: var(--ui-space-12);
576
+ flex-wrap: wrap;
577
+ }
578
+
579
+ .stop-actions {
580
+ display: inline-flex;
581
+ align-items: center;
582
+ gap: var(--ui-space-6);
583
+ flex: 0 0 auto;
584
+ }
585
+
586
+ /* Row 1: label / percent / picker / slug. Row 2: Monochrome under picker. */
587
+ .stop-edit-row {
588
+ display: flex;
589
+ flex-wrap: wrap;
590
+ align-items: flex-start;
591
+ gap: var(--ui-space-12);
592
+ flex: 1 1 18rem;
593
+ min-width: 0;
594
+ }
595
+ .stop-edit-row > .row-label,
596
+ .stop-edit-row > .pos-input,
597
+ .stop-edit-row > .stop-value-text {
598
+ line-height: 1.75rem;
599
+ }
600
+
601
+ .picker-column {
602
+ display: flex;
603
+ flex-direction: column;
604
+ align-items: flex-start;
605
+ gap: var(--ui-space-6);
606
+ flex: 0 0 auto;
607
+ }
608
+
609
+ /* Hide picker's built-in meta; we render the slug ourselves. */
610
+ .stop-edit-row :global(.ui-ts-meta-text) {
611
+ display: none;
612
+ }
613
+
614
+ /* Stop slug, mirrors UITokenSelector meta typography. */
615
+ .stop-value-text {
616
+ flex: 1 1 0;
617
+ min-width: 0;
618
+ color: var(--ui-text-tertiary);
619
+ font-family: var(--ui-font-mono);
620
+ font-size: var(--ui-font-size-sm);
621
+ overflow: hidden;
622
+ text-overflow: ellipsis;
623
+ white-space: nowrap;
624
+ }
625
+
626
+ .stop-mono-check {
627
+ display: inline-flex;
628
+ align-items: center;
629
+ gap: var(--ui-space-6);
630
+ font-size: var(--ui-font-size-sm);
631
+ color: var(--ui-text-secondary);
632
+ cursor: pointer;
633
+ user-select: none;
634
+ flex: 0 0 auto;
635
+ }
636
+ .stop-mono-check:hover { color: var(--ui-text-primary); }
637
+ .stop-mono-check input { margin: 0; cursor: pointer; }
638
+
639
+ /* Peers the section label; 1.5rem left-pad aligns with the ribbon's content edge. */
640
+ .row-label {
641
+ font-size: var(--ui-font-size-md);
642
+ font-weight: 500;
643
+ color: var(--ui-text-primary);
644
+ line-height: 1;
645
+ padding-left: 1.5rem;
646
+ white-space: nowrap;
647
+ }
648
+
649
+ .pos-input {
650
+ display: inline-flex;
651
+ align-items: center;
652
+ gap: var(--ui-space-4);
653
+ font-size: var(--ui-font-size-xs);
654
+ color: var(--ui-text-secondary);
655
+ }
656
+
657
+ .pos-input input {
658
+ width: 2.25rem;
659
+ padding: var(--ui-space-2) var(--ui-space-6);
660
+ background: var(--ui-surface-lowest);
661
+ border: 1px solid var(--ui-border-low);
662
+ border-radius: var(--ui-radius-sm);
663
+ color: var(--ui-text-primary);
664
+ font-family: var(--ui-font-mono);
665
+ font-size: var(--ui-font-size-sm);
666
+ text-align: right;
667
+ }
668
+
669
+ .pos-input input::-webkit-outer-spin-button,
670
+ .pos-input input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
671
+
672
+ .suffix {
673
+ font-size: var(--ui-font-size-xs);
674
+ color: var(--ui-text-tertiary);
675
+ }
676
+
677
+ /* 8rem matches the property-row token selector width (see TokenLayout). */
678
+ .picker-slot {
679
+ flex: 0 0 auto;
680
+ width: 8rem;
681
+ }
682
+
683
+ .footer-row {
684
+ grid-column: 1 / -1;
685
+ display: flex;
686
+ justify-content: flex-end;
687
+ gap: var(--ui-space-8);
688
+ padding-top: var(--ui-space-4);
689
+ }
690
+ </style>