@motion-proto/live-tokens 0.7.1 → 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.
- package/dist-plugin/index.cjs +707 -90
- package/dist-plugin/index.d.cts +1 -0
- package/dist-plugin/index.d.ts +1 -0
- package/dist-plugin/index.js +707 -90
- package/package.json +2 -1
- package/src/app/site.css +1 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +34 -27
- package/src/editor/component-editor/DialogEditor.svelte +4 -4
- package/src/editor/component-editor/NotificationEditor.svelte +3 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +439 -112
- package/src/editor/component-editor/StandardButtonsEditor.svelte +13 -1
- package/src/editor/component-editor/editors.d.ts +10 -0
- package/src/editor/component-editor/scaffolding/AngleDial.svelte +52 -13
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
- package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +0 -1
- package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +15 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +103 -15
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +9 -6
- package/src/editor/component-editor/scaffolding/TypeEditor.svelte +13 -1
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +239 -25
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -0
- package/src/editor/component-editor/scaffolding/types.ts +11 -0
- package/src/editor/core/components/componentConfigKeys.ts +8 -0
- package/src/editor/core/components/componentConfigService.ts +2 -2
- package/src/editor/core/components/componentPersist.ts +7 -5
- package/src/editor/core/manifests/manifestService.ts +58 -3
- package/src/editor/core/palettes/familySwap.ts +99 -0
- package/src/editor/core/palettes/paletteDerivation.ts +69 -0
- package/src/editor/core/palettes/tokenRegistry.ts +4 -1
- package/src/editor/core/store/editorStore.ts +206 -12
- package/src/editor/core/store/editorTypes.ts +55 -12
- package/src/editor/core/store/gradientSource.ts +192 -0
- package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
- package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
- package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
- package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
- package/src/editor/core/themes/migrations/index.ts +10 -0
- package/src/editor/core/themes/slices/components.ts +18 -4
- package/src/editor/core/themes/slices/gradients.ts +88 -13
- package/src/editor/core/themes/themeInit.ts +2 -2
- package/src/editor/core/themes/themeTypes.ts +56 -1
- package/src/editor/overlay/ColumnsOverlay.svelte +0 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
- package/src/editor/styles/ui-editor.css +1 -0
- package/src/editor/styles/ui-form-controls.css +19 -20
- package/src/editor/ui/BezierCurveEditor.svelte +114 -63
- package/src/editor/ui/EditorViewSwitcher.svelte +0 -1
- package/src/editor/ui/FileLoadList.svelte +22 -5
- package/src/editor/ui/FontStackEditor.svelte +214 -76
- package/src/editor/ui/GradientEditor.svelte +435 -215
- package/src/editor/ui/GradientStopPicker.svelte +11 -3
- package/src/editor/ui/ManifestFileManager.svelte +71 -4
- package/src/editor/ui/PaletteEditor.svelte +52 -79
- package/src/editor/ui/ProjectFontsSection.svelte +328 -293
- package/src/editor/ui/ThemeFileManager.svelte +0 -4
- package/src/editor/ui/UIFontFamilySelector.svelte +0 -1
- package/src/editor/ui/UIFontSizeSelector.svelte +3 -0
- package/src/editor/ui/UIInfoPopover.svelte +0 -1
- package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
- package/src/editor/ui/UIPaletteSelector.svelte +31 -4
- package/src/editor/ui/UIPillButton.svelte +33 -3
- package/src/editor/ui/UISegmentedControl.svelte +114 -0
- package/src/editor/ui/UITokenSelector.svelte +4 -1
- package/src/editor/ui/VariablesTab.svelte +41 -35
- package/src/editor/ui/palette/OverridesPanel.svelte +14 -37
- package/src/editor/ui/palette/PaletteBase.svelte +3 -3
- package/src/editor/ui/sections/ColumnsSection.svelte +1 -2
- package/src/editor/ui/sections/GradientsSection.svelte +1 -1
- package/src/editor/ui/sections/OverlaysSection.svelte +1 -1
- package/src/editor/ui/sections/ShadowsSection.svelte +1 -1
- package/src/system/components/Button.svelte +2 -2
- package/src/system/components/Card.svelte +29 -1
- package/src/system/components/CollapsibleSection.svelte +25 -2
- package/src/system/components/FloatingTokenTags.css +43 -24
- package/src/system/components/FloatingTokenTags.svelte +88 -137
- package/src/system/components/Notification.svelte +8 -1
- package/src/system/components/SectionDivider.svelte +456 -379
- package/src/system/styles/CONVENTIONS.md +1 -1
- package/src/system/styles/fonts.css +3 -16
- package/src/system/styles/tokens.css +356 -1199
- package/src/system/styles/tokens.generated.css +544 -0
- package/src/editor/component-editor/scaffolding/DividerEditor.svelte +0 -94
- package/src/editor/component-editor/scaffolding/GradientCard.svelte +0 -296
|
@@ -1,50 +1,76 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { run } from 'svelte/legacy';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* ribbon; only the selected stop exposes its position + color controls (the
|
|
7
|
-
* old list of every stop is replaced by this single-row pattern, mirroring
|
|
8
|
-
* GradientCard.svelte). Stops can be added/removed with a minimum of two.
|
|
9
|
-
*/
|
|
4
|
+
// Visual gradient editor: draggable stop diamonds on a live ribbon.
|
|
5
|
+
// Bound to a GradientSource (theme via `variable`, or component via `source`).
|
|
10
6
|
import { tick, onMount } from 'svelte';
|
|
11
|
-
import {
|
|
12
|
-
editorState,
|
|
13
|
-
setGradient,
|
|
14
|
-
setGradientType,
|
|
15
|
-
setGradientAngle,
|
|
16
|
-
setGradientStop,
|
|
17
|
-
addGradientStop,
|
|
18
|
-
removeGradientStop,
|
|
19
|
-
} from '../core/store/editorStore';
|
|
7
|
+
import { get } from 'svelte/store';
|
|
20
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';
|
|
21
15
|
import GradientStopPicker from './GradientStopPicker.svelte';
|
|
22
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';
|
|
23
21
|
|
|
24
22
|
interface Props {
|
|
25
|
-
|
|
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;
|
|
26
37
|
onsave?: () => void;
|
|
27
38
|
oncancel?: () => void;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
let {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
34
63
|
onMount(() => {
|
|
35
|
-
|
|
36
|
-
if (g) {
|
|
37
|
-
snapshot = { type: g.type, angle: g.angle, stops: g.stops.map((s) => ({ ...s })) };
|
|
38
|
-
}
|
|
64
|
+
snapshot = snapshotGradient(gradientSource);
|
|
39
65
|
});
|
|
40
66
|
|
|
41
67
|
function save() { onsave?.(); }
|
|
42
68
|
function cancel() {
|
|
43
|
-
if (snapshot)
|
|
69
|
+
if (snapshot) gradientSource.setAll(snapshot);
|
|
44
70
|
oncancel?.();
|
|
45
71
|
}
|
|
46
72
|
|
|
47
|
-
let gradient = $derived($
|
|
73
|
+
let gradient = $derived($gradientSourceCurrent);
|
|
48
74
|
let stopCount = $derived(gradient?.stops.length ?? 0);
|
|
49
75
|
|
|
50
76
|
let selected = $state(0);
|
|
@@ -54,16 +80,20 @@
|
|
|
54
80
|
});
|
|
55
81
|
|
|
56
82
|
function setType(type: GradientType) {
|
|
57
|
-
|
|
83
|
+
gradientSource.setType(type);
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
function onAngleChange(detail: { value: number }) {
|
|
61
|
-
|
|
87
|
+
gradientSource.setAngle(detail.value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function onAspectChange(detail: { x: number; y: number }) {
|
|
91
|
+
gradientSource.setAspect(detail);
|
|
62
92
|
}
|
|
63
93
|
|
|
64
94
|
function setPosition(i: number, pct: number) {
|
|
65
95
|
const clamped = Math.max(0, Math.min(100, Math.round(pct * 10) / 10));
|
|
66
|
-
|
|
96
|
+
gradientSource.setStop(i, { position: clamped });
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
function onPositionInput(e: Event) {
|
|
@@ -72,23 +102,44 @@
|
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
function handleStopChange(i: number, payload: { color: string; opacity: number }) {
|
|
75
|
-
|
|
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 });
|
|
76
123
|
}
|
|
77
124
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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) {
|
|
82
133
|
const clamped = Math.max(0, Math.min(100, Math.round(pct * 10) / 10));
|
|
83
|
-
|
|
134
|
+
gradientSource.addStop({
|
|
84
135
|
position: clamped,
|
|
85
|
-
color:
|
|
86
|
-
opacity:
|
|
136
|
+
color: sourceStop.color,
|
|
137
|
+
opacity: sourceStop.opacity ?? 100,
|
|
87
138
|
});
|
|
88
139
|
await tick();
|
|
89
|
-
const after =
|
|
140
|
+
const after = get(gradientSource.current);
|
|
90
141
|
if (after) {
|
|
91
|
-
const idx = after.stops.findIndex((s) => s.position === clamped && s.color ===
|
|
142
|
+
const idx = after.stops.findIndex((s) => s.position === clamped && s.color === sourceStop.color);
|
|
92
143
|
if (idx >= 0) selected = idx;
|
|
93
144
|
}
|
|
94
145
|
}
|
|
@@ -96,8 +147,7 @@
|
|
|
96
147
|
function addStop() {
|
|
97
148
|
if (!gradient) return;
|
|
98
149
|
const stops = gradient.stops;
|
|
99
|
-
//
|
|
100
|
-
// (or to 100% if it's the last stop).
|
|
150
|
+
// Midway to the right neighbour, or to 100% if last.
|
|
101
151
|
const anchor = stops[selected] ?? stops[stops.length - 1];
|
|
102
152
|
const next = stops[selected + 1];
|
|
103
153
|
const newPos = next
|
|
@@ -106,13 +156,19 @@
|
|
|
106
156
|
insertStopAt(newPos, anchor);
|
|
107
157
|
}
|
|
108
158
|
|
|
109
|
-
|
|
110
|
-
* color/opacity of the closest existing stop so the new handle starts from
|
|
111
|
-
* a sensible color rather than a default. */
|
|
159
|
+
// Inserts a stop at click position, inheriting from the nearest existing stop.
|
|
112
160
|
function onRibbonClick(e: MouseEvent) {
|
|
113
161
|
if (!gradient || e.button !== 0) return;
|
|
114
162
|
const rect = barEl!.getBoundingClientRect();
|
|
115
|
-
const
|
|
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
|
+
}
|
|
116
172
|
const nearest = gradient.stops.reduce(
|
|
117
173
|
(best, s) => (Math.abs(s.position - pct) < Math.abs(best.position - pct) ? s : best),
|
|
118
174
|
gradient.stops[0],
|
|
@@ -122,23 +178,32 @@
|
|
|
122
178
|
|
|
123
179
|
function removeSelected() {
|
|
124
180
|
if (!gradient || gradient.stops.length <= 2) return;
|
|
125
|
-
|
|
181
|
+
gradientSource.removeStop(selected);
|
|
126
182
|
if (selected > 0) selected -= 1;
|
|
127
183
|
}
|
|
128
184
|
|
|
129
|
-
//
|
|
185
|
+
// Ribbon handle drag
|
|
130
186
|
let barEl: HTMLDivElement | undefined = $state();
|
|
131
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);
|
|
132
190
|
|
|
133
191
|
function pctFromEvent(e: PointerEvent): number {
|
|
134
192
|
const rect = barEl!.getBoundingClientRect();
|
|
135
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
|
+
}
|
|
136
200
|
return (x / rect.width) * 100;
|
|
137
201
|
}
|
|
138
202
|
|
|
139
|
-
function onHandleDown(e: PointerEvent, i: number) {
|
|
203
|
+
function onHandleDown(e: PointerEvent, i: number, side: 'left' | 'right' | null = null) {
|
|
140
204
|
selected = i;
|
|
141
205
|
dragIndex = i;
|
|
206
|
+
dragSide = side;
|
|
142
207
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
143
208
|
setPosition(i, pctFromEvent(e));
|
|
144
209
|
}
|
|
@@ -150,133 +215,275 @@
|
|
|
150
215
|
if (dragIndex === null) return;
|
|
151
216
|
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
|
152
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)`;
|
|
153
227
|
}
|
|
154
228
|
|
|
155
|
-
|
|
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
|
+
|
|
156
254
|
let stopSwatches = $derived((gradient?.stops ?? []).map((s) => {
|
|
157
255
|
const base = s.color.startsWith('--') ? `var(${s.color})` : s.color;
|
|
158
256
|
const op = s.opacity ?? 100;
|
|
159
257
|
return op >= 100 ? base : `color-mix(in srgb, ${base} ${Math.round(op)}%, transparent)`;
|
|
160
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
|
+
}
|
|
161
274
|
</script>
|
|
162
275
|
|
|
163
276
|
{#if gradient}
|
|
164
|
-
<div class="gradient-editor">
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
onpointercancel={onHandleUp}
|
|
188
|
-
title={`Stop ${i + 1} (${stop.position}%)`}
|
|
189
|
-
aria-label={`Gradient stop ${i + 1}`}
|
|
190
|
-
>
|
|
191
|
-
<span class="handle-diamond"></span>
|
|
192
|
-
</button>
|
|
193
|
-
{/each}
|
|
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}
|
|
194
300
|
</div>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
class:
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
/>
|
|
209
371
|
</div>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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>
|
|
213
425
|
</div>
|
|
214
426
|
{/if}
|
|
215
|
-
<div class="spacer"></div>
|
|
216
|
-
<button
|
|
217
|
-
type="button"
|
|
218
|
-
class="ghost-btn"
|
|
219
|
-
onclick={addStop}
|
|
220
|
-
title="Add stop"
|
|
221
|
-
>
|
|
222
|
-
<i class="fas fa-plus"></i> Add stop
|
|
223
|
-
</button>
|
|
224
|
-
<button
|
|
225
|
-
type="button"
|
|
226
|
-
class="ghost-btn"
|
|
227
|
-
onclick={removeSelected}
|
|
228
|
-
disabled={gradient.stops.length <= 2}
|
|
229
|
-
title={gradient.stops.length <= 2 ? 'Gradient needs at least two stops' : 'Remove selected stop'}
|
|
230
|
-
>
|
|
231
|
-
<i class="fas fa-times"></i> Remove
|
|
232
|
-
</button>
|
|
233
427
|
</div>
|
|
234
428
|
|
|
235
|
-
{#if
|
|
236
|
-
<div class="
|
|
237
|
-
<
|
|
238
|
-
<
|
|
239
|
-
<input
|
|
240
|
-
type="number"
|
|
241
|
-
min="0"
|
|
242
|
-
max="100"
|
|
243
|
-
step="0.1"
|
|
244
|
-
value={gradient.stops[selected].position}
|
|
245
|
-
onchange={onPositionInput}
|
|
246
|
-
/>
|
|
247
|
-
<span class="suffix">%</span>
|
|
248
|
-
</label>
|
|
249
|
-
<div class="picker-slot">
|
|
250
|
-
<GradientStopPicker
|
|
251
|
-
stopId={`${variable}-${selected}`}
|
|
252
|
-
color={gradient.stops[selected].color}
|
|
253
|
-
opacity={gradient.stops[selected].opacity ?? 100}
|
|
254
|
-
onchange={(payload) => handleStopChange(selected, payload)}
|
|
255
|
-
/>
|
|
256
|
-
</div>
|
|
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>
|
|
257
433
|
</div>
|
|
258
434
|
{/if}
|
|
259
|
-
|
|
260
|
-
<div class="footer-row">
|
|
261
|
-
<button type="button" class="ghost-btn" onclick={cancel}>Cancel</button>
|
|
262
|
-
<button type="button" class="primary-btn" onclick={save}>Save</button>
|
|
263
|
-
</div>
|
|
264
435
|
</div>
|
|
265
436
|
{/if}
|
|
266
437
|
|
|
267
438
|
<style>
|
|
439
|
+
/* Header labels share grid tracks with the ribbon + pad below them. */
|
|
268
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 {
|
|
269
463
|
display: flex;
|
|
270
|
-
|
|
464
|
+
align-items: center;
|
|
465
|
+
justify-content: space-between;
|
|
271
466
|
gap: var(--ui-space-12);
|
|
272
|
-
width: 100%;
|
|
273
467
|
min-width: 0;
|
|
274
468
|
}
|
|
275
469
|
|
|
276
|
-
.ribbon-
|
|
470
|
+
.ribbon-stack {
|
|
471
|
+
grid-column: 1;
|
|
277
472
|
display: flex;
|
|
278
473
|
flex-direction: column;
|
|
279
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;
|
|
280
487
|
}
|
|
281
488
|
|
|
282
489
|
.ribbon {
|
|
@@ -287,13 +494,35 @@
|
|
|
287
494
|
cursor: copy;
|
|
288
495
|
}
|
|
289
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
|
+
|
|
290
520
|
.handles {
|
|
291
521
|
position: relative;
|
|
292
522
|
height: 1.25rem;
|
|
293
523
|
}
|
|
294
524
|
|
|
295
|
-
/*
|
|
296
|
-
lives in `.handle-diamond` inside. Mirrors GradientCard. */
|
|
525
|
+
/* 1.25rem hit target; visible marker is the inner .handle-diamond. */
|
|
297
526
|
.handle {
|
|
298
527
|
position: absolute;
|
|
299
528
|
top: 0;
|
|
@@ -337,75 +566,83 @@
|
|
|
337
566
|
box-shadow: 0 0 0 2px var(--ui-text-primary);
|
|
338
567
|
}
|
|
339
568
|
|
|
340
|
-
|
|
569
|
+
|
|
570
|
+
/* Per-gradient controls under the ribbon: type selector + stop edit row. */
|
|
571
|
+
.lower-row {
|
|
572
|
+
grid-column: 1 / -1;
|
|
341
573
|
display: flex;
|
|
342
|
-
align-items:
|
|
574
|
+
align-items: flex-start;
|
|
343
575
|
gap: var(--ui-space-12);
|
|
344
576
|
flex-wrap: wrap;
|
|
345
577
|
}
|
|
346
578
|
|
|
347
|
-
.
|
|
348
|
-
|
|
349
|
-
.type-toggle {
|
|
579
|
+
.stop-actions {
|
|
350
580
|
display: inline-flex;
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
.type-toggle button {
|
|
357
|
-
padding: var(--ui-space-4) var(--ui-space-12);
|
|
358
|
-
background: transparent;
|
|
359
|
-
border: none;
|
|
360
|
-
color: var(--ui-text-secondary);
|
|
361
|
-
font-size: var(--ui-font-size-sm);
|
|
362
|
-
cursor: pointer;
|
|
363
|
-
font-family: inherit;
|
|
581
|
+
align-items: center;
|
|
582
|
+
gap: var(--ui-space-6);
|
|
583
|
+
flex: 0 0 auto;
|
|
364
584
|
}
|
|
365
585
|
|
|
366
|
-
.
|
|
367
|
-
|
|
368
|
-
|
|
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;
|
|
369
594
|
}
|
|
370
|
-
|
|
371
|
-
.
|
|
372
|
-
|
|
595
|
+
.stop-edit-row > .row-label,
|
|
596
|
+
.stop-edit-row > .pos-input,
|
|
597
|
+
.stop-edit-row > .stop-value-text {
|
|
598
|
+
line-height: 1.75rem;
|
|
373
599
|
}
|
|
374
600
|
|
|
375
|
-
.
|
|
376
|
-
display:
|
|
377
|
-
|
|
601
|
+
.picker-column {
|
|
602
|
+
display: flex;
|
|
603
|
+
flex-direction: column;
|
|
604
|
+
align-items: flex-start;
|
|
378
605
|
gap: var(--ui-space-6);
|
|
379
|
-
|
|
380
|
-
background: var(--ui-surface-low);
|
|
381
|
-
border: 1px solid var(--ui-border-low);
|
|
382
|
-
border-radius: var(--ui-radius-sm);
|
|
383
|
-
color: var(--ui-text-secondary);
|
|
384
|
-
font-size: var(--ui-font-size-sm);
|
|
385
|
-
cursor: pointer;
|
|
386
|
-
font-family: inherit;
|
|
606
|
+
flex: 0 0 auto;
|
|
387
607
|
}
|
|
388
608
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
609
|
+
/* Hide picker's built-in meta; we render the slug ourselves. */
|
|
610
|
+
.stop-edit-row :global(.ui-ts-meta-text) {
|
|
611
|
+
display: none;
|
|
392
612
|
}
|
|
393
613
|
|
|
394
|
-
.
|
|
395
|
-
|
|
396
|
-
|
|
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;
|
|
397
624
|
}
|
|
398
625
|
|
|
399
|
-
.stop-
|
|
400
|
-
display: flex;
|
|
401
|
-
flex-wrap: wrap;
|
|
626
|
+
.stop-mono-check {
|
|
627
|
+
display: inline-flex;
|
|
402
628
|
align-items: center;
|
|
403
|
-
gap: var(--ui-space-
|
|
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;
|
|
404
635
|
}
|
|
636
|
+
.stop-mono-check:hover { color: var(--ui-text-primary); }
|
|
637
|
+
.stop-mono-check input { margin: 0; cursor: pointer; }
|
|
405
638
|
|
|
639
|
+
/* Peers the section label; 1.5rem left-pad aligns with the ribbon's content edge. */
|
|
406
640
|
.row-label {
|
|
407
|
-
font-size: var(--ui-font-size-
|
|
408
|
-
|
|
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;
|
|
409
646
|
white-space: nowrap;
|
|
410
647
|
}
|
|
411
648
|
|
|
@@ -418,7 +655,7 @@
|
|
|
418
655
|
}
|
|
419
656
|
|
|
420
657
|
.pos-input input {
|
|
421
|
-
width:
|
|
658
|
+
width: 2.25rem;
|
|
422
659
|
padding: var(--ui-space-2) var(--ui-space-6);
|
|
423
660
|
background: var(--ui-surface-lowest);
|
|
424
661
|
border: 1px solid var(--ui-border-low);
|
|
@@ -437,34 +674,17 @@
|
|
|
437
674
|
color: var(--ui-text-tertiary);
|
|
438
675
|
}
|
|
439
676
|
|
|
677
|
+
/* 8rem matches the property-row token selector width (see TokenLayout). */
|
|
440
678
|
.picker-slot {
|
|
441
|
-
flex:
|
|
442
|
-
|
|
679
|
+
flex: 0 0 auto;
|
|
680
|
+
width: 8rem;
|
|
443
681
|
}
|
|
444
682
|
|
|
445
683
|
.footer-row {
|
|
684
|
+
grid-column: 1 / -1;
|
|
446
685
|
display: flex;
|
|
447
686
|
justify-content: flex-end;
|
|
448
687
|
gap: var(--ui-space-8);
|
|
449
688
|
padding-top: var(--ui-space-4);
|
|
450
689
|
}
|
|
451
|
-
|
|
452
|
-
.primary-btn {
|
|
453
|
-
display: inline-flex;
|
|
454
|
-
align-items: center;
|
|
455
|
-
gap: var(--ui-space-6);
|
|
456
|
-
padding: var(--ui-space-4) var(--ui-space-12);
|
|
457
|
-
background: var(--ui-surface-high);
|
|
458
|
-
border: 1px solid var(--ui-border-high);
|
|
459
|
-
border-radius: var(--ui-radius-sm);
|
|
460
|
-
color: var(--ui-text-primary);
|
|
461
|
-
font-size: var(--ui-font-size-sm);
|
|
462
|
-
cursor: pointer;
|
|
463
|
-
font-family: inherit;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
.primary-btn:hover {
|
|
467
|
-
background: var(--ui-surface-higher);
|
|
468
|
-
border-color: var(--ui-border-higher);
|
|
469
|
-
}
|
|
470
690
|
</style>
|