@motion-proto/live-tokens 0.1.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +168 -21
  2. package/dist-plugin/index.cjs +823 -336
  3. package/dist-plugin/index.d.cts +9 -7
  4. package/dist-plugin/index.d.ts +9 -7
  5. package/dist-plugin/index.js +822 -335
  6. package/package.json +46 -20
  7. package/src/assets/newspaper.webp +0 -0
  8. package/src/assets/offering.webp +0 -0
  9. package/src/component-editor/BadgeEditor.svelte +170 -0
  10. package/src/component-editor/CalloutEditor.svelte +103 -0
  11. package/src/component-editor/CardEditor.svelte +184 -0
  12. package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
  13. package/src/component-editor/CornerBadgeEditor.svelte +207 -0
  14. package/src/component-editor/DialogEditor.svelte +172 -0
  15. package/src/component-editor/ImageEditor.svelte +72 -0
  16. package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
  17. package/src/component-editor/NotificationEditor.svelte +160 -0
  18. package/src/component-editor/ProgressBarEditor.svelte +124 -0
  19. package/src/component-editor/RadioButtonEditor.svelte +140 -0
  20. package/src/component-editor/SectionDividerEditor.svelte +263 -0
  21. package/src/component-editor/SegmentedControlEditor.svelte +154 -0
  22. package/src/component-editor/StandardButtonsEditor.svelte +178 -0
  23. package/src/component-editor/TabBarEditor.svelte +137 -0
  24. package/src/component-editor/TableEditor.svelte +128 -0
  25. package/src/component-editor/TooltipEditor.svelte +122 -0
  26. package/src/component-editor/editorTokens.test.ts +93 -0
  27. package/src/component-editor/groupKeySlots.test.ts +67 -0
  28. package/src/component-editor/groupKeySnapshot.test.ts +52 -0
  29. package/src/component-editor/index.ts +5 -0
  30. package/src/component-editor/registry.ts +246 -0
  31. package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
  32. package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
  33. package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
  34. package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
  35. package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
  36. package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
  37. package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
  38. package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
  39. package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
  40. package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
  41. package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
  42. package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
  43. package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
  44. package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
  45. package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
  46. package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
  47. package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
  48. package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
  49. package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
  50. package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
  51. package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
  52. package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
  53. package/src/component-editor/scaffolding/componentSources.ts +9 -0
  54. package/src/component-editor/scaffolding/defaultSections.ts +16 -0
  55. package/src/component-editor/scaffolding/editorContext.ts +44 -0
  56. package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
  57. package/src/component-editor/scaffolding/siblings.ts +33 -0
  58. package/src/component-editor/scaffolding/types.ts +39 -0
  59. package/src/components/Badge.svelte +231 -42
  60. package/src/components/Button.svelte +324 -124
  61. package/src/components/Callout.svelte +145 -0
  62. package/src/components/Card.svelte +123 -25
  63. package/src/components/CollapsibleSection.svelte +213 -35
  64. package/src/components/CornerBadge.svelte +224 -0
  65. package/src/components/Dialog.svelte +137 -114
  66. package/src/components/Image.svelte +43 -0
  67. package/src/components/InlineEditActions.svelte +74 -14
  68. package/src/components/Notification.svelte +184 -163
  69. package/src/components/ProgressBar.svelte +216 -22
  70. package/src/components/RadioButton.svelte +110 -40
  71. package/src/components/SectionDivider.svelte +428 -74
  72. package/src/components/SegmentedControl.svelte +203 -0
  73. package/src/components/TabBar.svelte +146 -21
  74. package/src/components/Table.svelte +102 -0
  75. package/src/components/Tooltip.svelte +45 -19
  76. package/src/components/types.ts +51 -0
  77. package/src/data/google-fonts.json +75 -0
  78. package/src/lib/ColumnsOverlay.svelte +20 -7
  79. package/src/lib/LiveEditorOverlay.svelte +257 -78
  80. package/src/lib/columnsOverlay.ts +21 -17
  81. package/src/lib/componentConfig.test.ts +204 -0
  82. package/src/lib/componentConfigKeys.ts +19 -0
  83. package/src/lib/componentConfigService.ts +88 -0
  84. package/src/lib/copyPopover.ts +30 -0
  85. package/src/lib/cssVarSync.ts +59 -7
  86. package/src/lib/editorConfigStore.ts +0 -10
  87. package/src/lib/editorCore.ts +402 -0
  88. package/src/lib/editorKeybindings.ts +52 -0
  89. package/src/lib/editorPersistence.ts +106 -0
  90. package/src/lib/editorRenderer.ts +74 -0
  91. package/src/lib/editorStore.test.ts +328 -0
  92. package/src/lib/editorStore.ts +412 -0
  93. package/src/lib/editorTypes.ts +100 -0
  94. package/src/lib/editorViewStore.ts +55 -0
  95. package/src/lib/files/versionedFileResource.ts +140 -0
  96. package/src/lib/fontLoader.ts +130 -0
  97. package/src/lib/fontMigration.ts +140 -0
  98. package/src/lib/fontParse.ts +168 -0
  99. package/src/lib/index.ts +48 -30
  100. package/src/lib/lazyConfig.test.ts +54 -0
  101. package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
  102. package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
  103. package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
  104. package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
  105. package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
  106. package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
  107. package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
  108. package/src/lib/migrations/index.ts +93 -0
  109. package/src/lib/migrations/migrations.test.ts +341 -0
  110. package/src/lib/navLinkTypes.ts +1 -0
  111. package/src/lib/overlayState.ts +3 -0
  112. package/src/lib/paletteDerivation.ts +300 -0
  113. package/src/lib/parentRouteStore.ts +42 -0
  114. package/src/lib/parsers/globalRootBlock.ts +32 -0
  115. package/src/lib/presetService.ts +94 -0
  116. package/src/lib/router.ts +42 -10
  117. package/src/lib/scrollSection.ts +45 -0
  118. package/src/lib/slices/columns.ts +59 -0
  119. package/src/lib/slices/components.ts +362 -0
  120. package/src/lib/slices/domainVars.ts +15 -0
  121. package/src/lib/slices/fonts.ts +30 -0
  122. package/src/lib/slices/gradients.ts +153 -0
  123. package/src/lib/slices/overlays.ts +132 -0
  124. package/src/lib/slices/palettes.ts +26 -0
  125. package/src/lib/slices/shadows.ts +123 -0
  126. package/src/lib/storage.ts +88 -0
  127. package/src/lib/themeInit.ts +74 -0
  128. package/src/lib/themeService.ts +101 -0
  129. package/src/lib/themeTypes.ts +146 -0
  130. package/src/lib/tokenRegistry.ts +148 -0
  131. package/src/pages/ComponentEditorPage.svelte +384 -0
  132. package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
  133. package/src/pages/Editor.svelte +98 -0
  134. package/src/pages/Editor.svelte.d.ts +2 -0
  135. package/src/pages/EditorShell.svelte +348 -0
  136. package/src/styles/_padding.scss +34 -0
  137. package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
  138. package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
  139. package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
  140. package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
  141. package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
  142. package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
  143. package/src/styles/fonts.css +22 -10
  144. package/src/styles/form-controls.css +14 -16
  145. package/src/styles/tokens.css +1322 -0
  146. package/src/styles/ui-editor.css +126 -0
  147. package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
  148. package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
  149. package/src/ui/EditorViewSwitcher.svelte +180 -0
  150. package/src/ui/FontStackEditor.svelte +360 -0
  151. package/src/ui/GradientEditor.svelte +461 -0
  152. package/src/ui/GradientStopPicker.svelte +74 -0
  153. package/src/ui/PaletteEditor.svelte +1590 -0
  154. package/src/ui/PaletteEditor.test.ts +108 -0
  155. package/src/ui/PresetFileManager.svelte +567 -0
  156. package/src/ui/ProjectFontsSection.svelte +645 -0
  157. package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
  158. package/src/{showcase → ui}/TextTab.svelte +27 -27
  159. package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
  160. package/src/ui/Toggle.svelte +108 -0
  161. package/src/ui/UICopyPopover.svelte +78 -0
  162. package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
  163. package/src/ui/UIFontFamilySelector.svelte +309 -0
  164. package/src/ui/UIFontSizeSelector.svelte +165 -0
  165. package/src/ui/UIFontWeightSelector.svelte +52 -0
  166. package/src/ui/UILineHeightSelector.svelte +47 -0
  167. package/src/ui/UILinkToggle.svelte +60 -0
  168. package/src/ui/UIOptionItem.svelte +74 -0
  169. package/src/ui/UIOptionList.svelte +27 -0
  170. package/src/ui/UIPaddingSelector.svelte +661 -0
  171. package/src/ui/UIPaletteSelector.svelte +1084 -0
  172. package/src/ui/UIRadio.svelte +72 -0
  173. package/src/ui/UIRadioGroup.svelte +59 -0
  174. package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
  175. package/src/ui/UITokenSelector.svelte +509 -0
  176. package/src/ui/UIVariantSelector.svelte +145 -0
  177. package/src/ui/VariablesTab.svelte +252 -0
  178. package/src/ui/index.ts +31 -0
  179. package/src/ui/keepInViewport.ts +84 -0
  180. package/src/ui/palette/GradientStopEditor.svelte +482 -0
  181. package/src/ui/palette/OverridesPanel.svelte +526 -0
  182. package/src/ui/palette/PaletteBase.svelte +165 -0
  183. package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
  184. package/src/ui/palette/paletteEditorState.ts +89 -0
  185. package/src/ui/sections/ColumnsSection.svelte +273 -0
  186. package/src/ui/sections/GradientsSection.svelte +147 -0
  187. package/src/ui/sections/OverlaysSection.svelte +670 -0
  188. package/src/ui/sections/ShadowsSection.svelte +1250 -0
  189. package/src/ui/sections/TokenScaleTable.svelte +332 -0
  190. package/src/ui/sections/tokenScales.ts +81 -0
  191. package/src/ui/variantScales.ts +108 -0
  192. package/src/components/DetailNav.svelte +0 -78
  193. package/src/components/Toggle.svelte +0 -86
  194. package/src/lib/tokenInit.ts +0 -29
  195. package/src/lib/tokenService.ts +0 -144
  196. package/src/lib/tokenTypes.ts +0 -45
  197. package/src/pages/Admin.svelte +0 -100
  198. package/src/pages/ShowcasePage.svelte +0 -144
  199. package/src/showcase/BackupBrowser.svelte +0 -617
  200. package/src/showcase/ComponentsTab.svelte +0 -105
  201. package/src/showcase/PaletteEditor.svelte +0 -2579
  202. package/src/showcase/PaletteSelector.svelte +0 -627
  203. package/src/showcase/TokenMap.svelte +0 -54
  204. package/src/showcase/VariablesTab.svelte +0 -2655
  205. package/src/showcase/VisualsTab.svelte +0 -231
  206. package/src/showcase/demos/BadgeDemo.svelte +0 -56
  207. package/src/showcase/demos/CardDemo.svelte +0 -50
  208. package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
  209. package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
  210. package/src/showcase/demos/DialogDemo.svelte +0 -42
  211. package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
  212. package/src/showcase/demos/NotificationDemo.svelte +0 -147
  213. package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
  214. package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
  215. package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
  216. package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
  217. package/src/showcase/demos/TabBarDemo.svelte +0 -58
  218. package/src/showcase/demos/TooltipDemo.svelte +0 -52
  219. package/src/showcase/editor.css +0 -93
  220. package/src/showcase/index.ts +0 -17
  221. package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
  222. package/src/styles/fonts/Domine/OFL.txt +0 -97
  223. package/src/styles/fonts/Domine/README.txt +0 -66
  224. /package/src/{showcase → ui}/curveEngine.ts +0 -0
@@ -0,0 +1,461 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Visual gradient editor. Stops are draggable diamond handles below a live
4
+ * ribbon; only the selected stop exposes its position + color controls (the
5
+ * old list of every stop is replaced by this single-row pattern, mirroring
6
+ * GradientCard.svelte). Stops can be added/removed with a minimum of two.
7
+ */
8
+ import { tick, onMount, createEventDispatcher } from 'svelte';
9
+ import {
10
+ editorState,
11
+ setGradient,
12
+ setGradientType,
13
+ setGradientAngle,
14
+ setGradientStop,
15
+ addGradientStop,
16
+ removeGradientStop,
17
+ } from '../lib/editorStore';
18
+ import type { GradientType, GradientTokenStop } from '../lib/editorTypes';
19
+ import GradientStopPicker from './GradientStopPicker.svelte';
20
+ import AngleDial from '../component-editor/scaffolding/AngleDial.svelte';
21
+
22
+ export let variable: string;
23
+
24
+ const dispatch = createEventDispatcher<{ save: void; cancel: void }>();
25
+
26
+ /** Deep snapshot of the gradient at editor open, used to restore on Cancel. */
27
+ let snapshot: { type: GradientType; angle: number; stops: GradientTokenStop[] } | null = null;
28
+ onMount(() => {
29
+ const g = $editorState.gradients.tokens.find((t) => t.variable === variable);
30
+ if (g) {
31
+ snapshot = { type: g.type, angle: g.angle, stops: g.stops.map((s) => ({ ...s })) };
32
+ }
33
+ });
34
+
35
+ function save() { dispatch('save'); }
36
+ function cancel() {
37
+ if (snapshot) setGradient(variable, snapshot);
38
+ dispatch('cancel');
39
+ }
40
+
41
+ $: gradient = $editorState.gradients.tokens.find((t) => t.variable === variable);
42
+ $: stopCount = gradient?.stops.length ?? 0;
43
+
44
+ let selected = 0;
45
+ // Keep `selected` in range as stops are added/removed.
46
+ $: if (selected >= stopCount) selected = Math.max(0, stopCount - 1);
47
+
48
+ function setType(type: GradientType) {
49
+ setGradientType(variable, type);
50
+ }
51
+
52
+ function onAngleChange(e: CustomEvent<{ value: number }>) {
53
+ setGradientAngle(variable, e.detail.value);
54
+ }
55
+
56
+ function setPosition(i: number, pct: number) {
57
+ const clamped = Math.max(0, Math.min(100, Math.round(pct * 10) / 10));
58
+ setGradientStop(variable, i, { position: clamped });
59
+ }
60
+
61
+ function onPositionInput(e: Event) {
62
+ const v = parseFloat((e.target as HTMLInputElement).value);
63
+ if (Number.isFinite(v)) setPosition(selected, v);
64
+ }
65
+
66
+ function handleStopChange(i: number, e: CustomEvent<{ color: string; opacity: number }>) {
67
+ setGradientStop(variable, i, { color: e.detail.color, opacity: e.detail.opacity });
68
+ }
69
+
70
+ /** Insert a stop at the given percentage, inheriting color/opacity from the
71
+ * given source stop. Selects the newly inserted stop once the store sort
72
+ * settles. */
73
+ async function insertStopAt(pct: number, source: GradientTokenStop) {
74
+ const clamped = Math.max(0, Math.min(100, Math.round(pct * 10) / 10));
75
+ addGradientStop(variable, {
76
+ position: clamped,
77
+ color: source.color,
78
+ opacity: source.opacity ?? 100,
79
+ });
80
+ await tick();
81
+ const after = $editorState.gradients.tokens.find((t) => t.variable === variable);
82
+ if (after) {
83
+ const idx = after.stops.findIndex((s) => s.position === clamped && s.color === source.color);
84
+ if (idx >= 0) selected = idx;
85
+ }
86
+ }
87
+
88
+ function addStop() {
89
+ if (!gradient) return;
90
+ const stops = gradient.stops;
91
+ // Insert after the currently-selected stop, midway to its right neighbour
92
+ // (or to 100% if it's the last stop).
93
+ const anchor = stops[selected] ?? stops[stops.length - 1];
94
+ const next = stops[selected + 1];
95
+ const newPos = next
96
+ ? (anchor.position + next.position) / 2
97
+ : (anchor.position + 100) / 2;
98
+ insertStopAt(newPos, anchor);
99
+ }
100
+
101
+ /** Click on the ribbon body inserts a stop at that position, picking up the
102
+ * color/opacity of the closest existing stop so the new handle starts from
103
+ * a sensible color rather than a default. */
104
+ function onRibbonClick(e: MouseEvent) {
105
+ if (!gradient || e.button !== 0) return;
106
+ const rect = barEl.getBoundingClientRect();
107
+ const pct = ((e.clientX - rect.left) / rect.width) * 100;
108
+ const nearest = gradient.stops.reduce(
109
+ (best, s) => (Math.abs(s.position - pct) < Math.abs(best.position - pct) ? s : best),
110
+ gradient.stops[0],
111
+ );
112
+ insertStopAt(pct, nearest);
113
+ }
114
+
115
+ function removeSelected() {
116
+ if (!gradient || gradient.stops.length <= 2) return;
117
+ removeGradientStop(variable, selected);
118
+ if (selected > 0) selected -= 1;
119
+ }
120
+
121
+ // ── Ribbon handle drag ─────────────────────────────────────────────────
122
+ let barEl: HTMLDivElement;
123
+ let dragIndex: number | null = null;
124
+
125
+ function pctFromEvent(e: PointerEvent): number {
126
+ const rect = barEl.getBoundingClientRect();
127
+ const x = e.clientX - rect.left;
128
+ return (x / rect.width) * 100;
129
+ }
130
+
131
+ function onHandleDown(e: PointerEvent, i: number) {
132
+ selected = i;
133
+ dragIndex = i;
134
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
135
+ setPosition(i, pctFromEvent(e));
136
+ }
137
+ function onHandleMove(e: PointerEvent) {
138
+ if (dragIndex === null) return;
139
+ setPosition(dragIndex, pctFromEvent(e));
140
+ }
141
+ function onHandleUp(e: PointerEvent) {
142
+ if (dragIndex === null) return;
143
+ (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
144
+ dragIndex = null;
145
+ }
146
+
147
+ // Stop colors rendered into the diamonds: token refs become var(...).
148
+ $: stopSwatches = (gradient?.stops ?? []).map((s) => {
149
+ const base = s.color.startsWith('--') ? `var(${s.color})` : s.color;
150
+ const op = s.opacity ?? 100;
151
+ return op >= 100 ? base : `color-mix(in srgb, ${base} ${Math.round(op)}%, transparent)`;
152
+ });
153
+ </script>
154
+
155
+ {#if gradient}
156
+ <div class="gradient-editor">
157
+ <div class="ribbon-wrap">
158
+ <div
159
+ class="ribbon"
160
+ bind:this={barEl}
161
+ style="background: var({variable});"
162
+ on:click={onRibbonClick}
163
+ role="button"
164
+ tabindex="-1"
165
+ aria-label="Click to add a gradient stop"
166
+ ></div>
167
+ <div class="handles">
168
+ {#each gradient.stops as stop, i (i)}
169
+ <button
170
+ type="button"
171
+ class="handle"
172
+ class:selected={selected === i}
173
+ class:dragging={dragIndex === i}
174
+ style="left: {stop.position}%; --stop-color: {stopSwatches[i]};"
175
+ on:pointerdown={(e) => onHandleDown(e, i)}
176
+ on:pointermove={onHandleMove}
177
+ on:pointerup={onHandleUp}
178
+ on:pointercancel={onHandleUp}
179
+ title={`Stop ${i + 1} (${stop.position}%)`}
180
+ aria-label={`Gradient stop ${i + 1}`}
181
+ >
182
+ <span class="handle-diamond"></span>
183
+ </button>
184
+ {/each}
185
+ </div>
186
+ </div>
187
+
188
+ <div class="controls-row">
189
+ <div class="type-toggle" role="radiogroup">
190
+ <button
191
+ type="button"
192
+ class:active={gradient.type === 'linear'}
193
+ on:click={() => setType('linear')}
194
+ >Linear</button>
195
+ <button
196
+ type="button"
197
+ class:active={gradient.type === 'radial'}
198
+ on:click={() => setType('radial')}
199
+ >Radial</button>
200
+ </div>
201
+ {#if gradient.type === 'linear'}
202
+ <div class="angle-slot">
203
+ <AngleDial value={gradient.angle} on:change={onAngleChange} />
204
+ </div>
205
+ {/if}
206
+ <div class="spacer"></div>
207
+ <button
208
+ type="button"
209
+ class="ghost-btn"
210
+ on:click={addStop}
211
+ title="Add stop"
212
+ >
213
+ <i class="fas fa-plus"></i> Add stop
214
+ </button>
215
+ <button
216
+ type="button"
217
+ class="ghost-btn"
218
+ on:click={removeSelected}
219
+ disabled={gradient.stops.length <= 2}
220
+ title={gradient.stops.length <= 2 ? 'Gradient needs at least two stops' : 'Remove selected stop'}
221
+ >
222
+ <i class="fas fa-times"></i> Remove
223
+ </button>
224
+ </div>
225
+
226
+ {#if gradient.stops[selected]}
227
+ <div class="stop-edit-row">
228
+ <span class="row-label">Stop {selected + 1}</span>
229
+ <label class="pos-input">
230
+ <input
231
+ type="number"
232
+ min="0"
233
+ max="100"
234
+ step="0.1"
235
+ value={gradient.stops[selected].position}
236
+ on:change={onPositionInput}
237
+ />
238
+ <span class="suffix">%</span>
239
+ </label>
240
+ <div class="picker-slot">
241
+ <GradientStopPicker
242
+ stopId={`${variable}-${selected}`}
243
+ color={gradient.stops[selected].color}
244
+ opacity={gradient.stops[selected].opacity ?? 100}
245
+ on:change={(e) => handleStopChange(selected, e)}
246
+ />
247
+ </div>
248
+ </div>
249
+ {/if}
250
+
251
+ <div class="footer-row">
252
+ <button type="button" class="ghost-btn" on:click={cancel}>Cancel</button>
253
+ <button type="button" class="primary-btn" on:click={save}>Save</button>
254
+ </div>
255
+ </div>
256
+ {/if}
257
+
258
+ <style>
259
+ .gradient-editor {
260
+ display: flex;
261
+ flex-direction: column;
262
+ gap: var(--ui-space-12);
263
+ width: 100%;
264
+ min-width: 0;
265
+ }
266
+
267
+ .ribbon-wrap {
268
+ display: flex;
269
+ flex-direction: column;
270
+ gap: var(--ui-space-8);
271
+ }
272
+
273
+ .ribbon {
274
+ position: relative;
275
+ height: 3rem;
276
+ border-radius: var(--ui-radius-md);
277
+ border: 1px solid var(--ui-border-faint);
278
+ cursor: copy;
279
+ }
280
+
281
+ .handles {
282
+ position: relative;
283
+ height: 1.25rem;
284
+ }
285
+
286
+ /* Button is a generous (1.25rem) transparent hit target; the visible marker
287
+ lives in `.handle-diamond` inside. Mirrors GradientCard. */
288
+ .handle {
289
+ position: absolute;
290
+ top: 0;
291
+ width: 1.25rem;
292
+ height: 1.25rem;
293
+ margin-left: -0.625rem;
294
+ padding: 0;
295
+ background: transparent;
296
+ border: none;
297
+ cursor: ew-resize;
298
+ touch-action: none;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: center;
302
+ }
303
+
304
+ .handle-diamond {
305
+ width: 0.7rem;
306
+ height: 0.7rem;
307
+ background: var(--stop-color, var(--ui-surface-high));
308
+ border: 1px solid var(--ui-border-default);
309
+ transform: rotate(45deg);
310
+ border-radius: 1px;
311
+ transition: border-color var(--ui-transition-fast), box-shadow var(--ui-transition-fast);
312
+ }
313
+
314
+ .handle:hover .handle-diamond {
315
+ border-color: var(--ui-text-secondary);
316
+ }
317
+
318
+ .handle.selected .handle-diamond {
319
+ border-color: var(--ui-text-primary);
320
+ box-shadow: 0 0 0 1px var(--ui-text-primary);
321
+ }
322
+
323
+ .handle.dragging {
324
+ z-index: 2;
325
+ }
326
+ .handle.dragging .handle-diamond {
327
+ border-color: var(--ui-text-primary);
328
+ box-shadow: 0 0 0 2px var(--ui-text-primary);
329
+ }
330
+
331
+ .controls-row {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: var(--ui-space-12);
335
+ flex-wrap: wrap;
336
+ }
337
+
338
+ .spacer { flex: 1 1 auto; }
339
+
340
+ .type-toggle {
341
+ display: inline-flex;
342
+ border: 1px solid var(--ui-border-faint);
343
+ border-radius: var(--ui-radius-md);
344
+ overflow: hidden;
345
+ }
346
+
347
+ .type-toggle button {
348
+ padding: var(--ui-space-4) var(--ui-space-12);
349
+ background: transparent;
350
+ border: none;
351
+ color: var(--ui-text-secondary);
352
+ font-size: var(--ui-font-size-sm);
353
+ cursor: pointer;
354
+ font-family: inherit;
355
+ }
356
+
357
+ .type-toggle button.active {
358
+ background: var(--ui-surface-high);
359
+ color: var(--ui-text-primary);
360
+ }
361
+
362
+ .type-toggle button + button {
363
+ border-left: 1px solid var(--ui-border-faint);
364
+ }
365
+
366
+ .ghost-btn {
367
+ display: inline-flex;
368
+ align-items: center;
369
+ gap: var(--ui-space-6);
370
+ padding: var(--ui-space-4) var(--ui-space-10);
371
+ background: var(--ui-surface-low);
372
+ border: 1px solid var(--ui-border-faint);
373
+ border-radius: var(--ui-radius-sm);
374
+ color: var(--ui-text-secondary);
375
+ font-size: var(--ui-font-size-sm);
376
+ cursor: pointer;
377
+ font-family: inherit;
378
+ }
379
+
380
+ .ghost-btn:hover:not(:disabled) {
381
+ color: var(--ui-text-primary);
382
+ border-color: var(--ui-border-default);
383
+ }
384
+
385
+ .ghost-btn:disabled {
386
+ opacity: 0.4;
387
+ cursor: not-allowed;
388
+ }
389
+
390
+ .stop-edit-row {
391
+ display: flex;
392
+ flex-wrap: wrap;
393
+ align-items: center;
394
+ gap: var(--ui-space-12);
395
+ }
396
+
397
+ .row-label {
398
+ font-size: var(--ui-font-size-xs);
399
+ color: var(--ui-text-secondary);
400
+ white-space: nowrap;
401
+ }
402
+
403
+ .pos-input {
404
+ display: inline-flex;
405
+ align-items: center;
406
+ gap: var(--ui-space-4);
407
+ font-size: var(--ui-font-size-xs);
408
+ color: var(--ui-text-secondary);
409
+ }
410
+
411
+ .pos-input input {
412
+ width: 4.5rem;
413
+ padding: var(--ui-space-2) var(--ui-space-6);
414
+ background: var(--ui-surface-lowest);
415
+ border: 1px solid var(--ui-border-faint);
416
+ border-radius: var(--ui-radius-sm);
417
+ color: var(--ui-text-primary);
418
+ font-family: var(--ui-font-mono);
419
+ font-size: var(--ui-font-size-sm);
420
+ text-align: right;
421
+ }
422
+
423
+ .pos-input input::-webkit-outer-spin-button,
424
+ .pos-input input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
425
+
426
+ .suffix {
427
+ font-size: var(--ui-font-size-xs);
428
+ color: var(--ui-text-tertiary);
429
+ }
430
+
431
+ .picker-slot {
432
+ flex: 1 1 12rem;
433
+ min-width: 8rem;
434
+ }
435
+
436
+ .footer-row {
437
+ display: flex;
438
+ justify-content: flex-end;
439
+ gap: var(--ui-space-8);
440
+ padding-top: var(--ui-space-4);
441
+ }
442
+
443
+ .primary-btn {
444
+ display: inline-flex;
445
+ align-items: center;
446
+ gap: var(--ui-space-6);
447
+ padding: var(--ui-space-4) var(--ui-space-12);
448
+ background: var(--ui-surface-high);
449
+ border: 1px solid var(--ui-border-medium);
450
+ border-radius: var(--ui-radius-sm);
451
+ color: var(--ui-text-primary);
452
+ font-size: var(--ui-font-size-sm);
453
+ cursor: pointer;
454
+ font-family: inherit;
455
+ }
456
+
457
+ .primary-btn:hover {
458
+ background: var(--ui-surface-higher);
459
+ border-color: var(--ui-border-strong);
460
+ }
461
+ </style>
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Color/opacity picker for a single gradient stop. Reuses UIPaletteSelector's
4
+ * dropdown UI by mounting it against a per-stop "scratch" CSS variable; writes
5
+ * to that scratch var get parsed back out and forwarded as a structured update
6
+ * to gradient state, so we don't have to refactor UIPaletteSelector itself.
7
+ */
8
+ import { onDestroy, createEventDispatcher } from 'svelte';
9
+ import UIPaletteSelector from './UIPaletteSelector.svelte';
10
+ import { setCssVar, removeCssVar } from '../lib/cssVarSync';
11
+
12
+ export let stopId: string; // unique key (e.g. gradient-var + stop index)
13
+ export let color: string; // token name like '--color-brand-500'
14
+ export let opacity: number = 100; // 0–100
15
+
16
+ const dispatch = createEventDispatcher<{ change: { color: string; opacity: number } }>();
17
+
18
+ /** Scratch var the embedded picker reads/writes; isolated per stop. */
19
+ const scratchVar = `--__grad-stop-${stopId}`;
20
+
21
+ function buildScratchValue(c: string, o: number): string {
22
+ const base = c.startsWith('--') ? `var(${c})` : c;
23
+ if (o >= 100) return base;
24
+ return `color-mix(in srgb, ${base} ${Math.round(o)}%, transparent)`;
25
+ }
26
+
27
+ /** Parse a scratch var write back into structured stop fields. */
28
+ function parseScratch(raw: string): { color: string; opacity: number } | null {
29
+ const trimmed = raw.trim();
30
+ if (!trimmed) return null;
31
+ const mixMatch = trimmed.match(/^color-mix\(in srgb,\s*var\((--[a-z0-9-]+)\)\s+(\d+(?:\.\d+)?)%,\s*transparent\)$/i);
32
+ if (mixMatch) {
33
+ return { color: mixMatch[1], opacity: parseFloat(mixMatch[2]) };
34
+ }
35
+ const varMatch = trimmed.match(/^var\((--[a-z0-9-]+)\)$/);
36
+ if (varMatch) {
37
+ return { color: varMatch[1], opacity: 100 };
38
+ }
39
+ return null;
40
+ }
41
+
42
+ // Seed the scratch var synchronously during script init so UIPaletteSelector
43
+ // (which mounts before its parent's onMount) reads the current stop value.
44
+ if (typeof document !== 'undefined') {
45
+ setCssVar(scratchVar, buildScratchValue(color, opacity));
46
+ }
47
+
48
+ function handleChange() {
49
+ const raw = document.documentElement.style.getPropertyValue(scratchVar);
50
+ const parsed = parseScratch(raw);
51
+ if (!parsed) return;
52
+ if (parsed.color === color && parsed.opacity === opacity) return;
53
+ dispatch('change', parsed);
54
+ }
55
+
56
+ onDestroy(() => {
57
+ removeCssVar(scratchVar);
58
+ });
59
+
60
+ // When external state updates the stop (undo/redo, sibling-stop edits),
61
+ // refresh the scratch so the picker reflects current values.
62
+ let lastSynced = `${color}|${opacity}`;
63
+ $: {
64
+ const sig = `${color}|${opacity}`;
65
+ if (sig !== lastSynced) {
66
+ lastSynced = sig;
67
+ if (typeof document !== 'undefined') {
68
+ setCssVar(scratchVar, buildScratchValue(color, opacity));
69
+ }
70
+ }
71
+ }
72
+ </script>
73
+
74
+ <UIPaletteSelector variable={scratchVar} on:change={handleChange} />