@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.
- package/README.md +168 -21
- package/dist-plugin/index.cjs +823 -336
- package/dist-plugin/index.d.cts +9 -7
- package/dist-plugin/index.d.ts +9 -7
- package/dist-plugin/index.js +822 -335
- package/package.json +46 -20
- package/src/assets/newspaper.webp +0 -0
- package/src/assets/offering.webp +0 -0
- package/src/component-editor/BadgeEditor.svelte +170 -0
- package/src/component-editor/CalloutEditor.svelte +103 -0
- package/src/component-editor/CardEditor.svelte +184 -0
- package/src/component-editor/CollapsibleSectionEditor.svelte +167 -0
- package/src/component-editor/CornerBadgeEditor.svelte +207 -0
- package/src/component-editor/DialogEditor.svelte +172 -0
- package/src/component-editor/ImageEditor.svelte +72 -0
- package/src/component-editor/InlineEditActionsEditor.svelte +83 -0
- package/src/component-editor/NotificationEditor.svelte +160 -0
- package/src/component-editor/ProgressBarEditor.svelte +124 -0
- package/src/component-editor/RadioButtonEditor.svelte +140 -0
- package/src/component-editor/SectionDividerEditor.svelte +263 -0
- package/src/component-editor/SegmentedControlEditor.svelte +154 -0
- package/src/component-editor/StandardButtonsEditor.svelte +178 -0
- package/src/component-editor/TabBarEditor.svelte +137 -0
- package/src/component-editor/TableEditor.svelte +128 -0
- package/src/component-editor/TooltipEditor.svelte +122 -0
- package/src/component-editor/editorTokens.test.ts +93 -0
- package/src/component-editor/groupKeySlots.test.ts +67 -0
- package/src/component-editor/groupKeySnapshot.test.ts +52 -0
- package/src/component-editor/index.ts +5 -0
- package/src/component-editor/registry.ts +246 -0
- package/src/component-editor/scaffolding/AngleDial.svelte +185 -0
- package/src/component-editor/scaffolding/ComponentEditorBase.svelte +96 -0
- package/src/component-editor/scaffolding/ComponentFileManager.svelte +682 -0
- package/src/component-editor/scaffolding/ComponentFileMenu.svelte +312 -0
- package/src/component-editor/scaffolding/ComponentsTab.svelte +69 -0
- package/src/component-editor/scaffolding/CopyFromMenu.svelte +246 -0
- package/src/component-editor/scaffolding/DemoHeader.svelte +21 -0
- package/src/component-editor/scaffolding/DividerEditor.svelte +81 -0
- package/src/component-editor/scaffolding/FieldsetWrapper.svelte +46 -0
- package/src/component-editor/scaffolding/GradientCard.svelte +291 -0
- package/src/component-editor/scaffolding/LinkageChart.svelte +297 -0
- package/src/component-editor/scaffolding/LinkedBlock.svelte +418 -0
- package/src/component-editor/scaffolding/NonStylableConfig.svelte +57 -0
- package/src/component-editor/scaffolding/SaveAsDialog.svelte +177 -0
- package/src/component-editor/scaffolding/ShadowBackdrop.svelte +25 -0
- package/src/component-editor/scaffolding/ShadowBackdropControls.svelte +56 -0
- package/src/component-editor/scaffolding/StateBlock.svelte +115 -0
- package/src/component-editor/scaffolding/TokenLayout.svelte +511 -0
- package/src/component-editor/scaffolding/TypeEditor.svelte +82 -0
- package/src/component-editor/scaffolding/VariantGroup.svelte +277 -0
- package/src/component-editor/scaffolding/buildTypeGroupTokens.ts +97 -0
- package/src/component-editor/scaffolding/componentSectionType.ts +8 -0
- package/src/component-editor/scaffolding/componentSources.ts +9 -0
- package/src/component-editor/scaffolding/defaultSections.ts +16 -0
- package/src/component-editor/scaffolding/editorContext.ts +44 -0
- package/src/component-editor/scaffolding/linkedBlock.ts +226 -0
- package/src/component-editor/scaffolding/siblings.ts +33 -0
- package/src/component-editor/scaffolding/types.ts +39 -0
- package/src/components/Badge.svelte +231 -42
- package/src/components/Button.svelte +324 -124
- package/src/components/Callout.svelte +145 -0
- package/src/components/Card.svelte +123 -25
- package/src/components/CollapsibleSection.svelte +213 -35
- package/src/components/CornerBadge.svelte +224 -0
- package/src/components/Dialog.svelte +137 -114
- package/src/components/Image.svelte +43 -0
- package/src/components/InlineEditActions.svelte +74 -14
- package/src/components/Notification.svelte +184 -163
- package/src/components/ProgressBar.svelte +216 -22
- package/src/components/RadioButton.svelte +110 -40
- package/src/components/SectionDivider.svelte +428 -74
- package/src/components/SegmentedControl.svelte +203 -0
- package/src/components/TabBar.svelte +146 -21
- package/src/components/Table.svelte +102 -0
- package/src/components/Tooltip.svelte +45 -19
- package/src/components/types.ts +51 -0
- package/src/data/google-fonts.json +75 -0
- package/src/lib/ColumnsOverlay.svelte +20 -7
- package/src/lib/LiveEditorOverlay.svelte +257 -78
- package/src/lib/columnsOverlay.ts +21 -17
- package/src/lib/componentConfig.test.ts +204 -0
- package/src/lib/componentConfigKeys.ts +19 -0
- package/src/lib/componentConfigService.ts +88 -0
- package/src/lib/copyPopover.ts +30 -0
- package/src/lib/cssVarSync.ts +59 -7
- package/src/lib/editorConfigStore.ts +0 -10
- package/src/lib/editorCore.ts +402 -0
- package/src/lib/editorKeybindings.ts +52 -0
- package/src/lib/editorPersistence.ts +106 -0
- package/src/lib/editorRenderer.ts +74 -0
- package/src/lib/editorStore.test.ts +328 -0
- package/src/lib/editorStore.ts +412 -0
- package/src/lib/editorTypes.ts +100 -0
- package/src/lib/editorViewStore.ts +55 -0
- package/src/lib/files/versionedFileResource.ts +140 -0
- package/src/lib/fontLoader.ts +130 -0
- package/src/lib/fontMigration.ts +140 -0
- package/src/lib/fontParse.ts +168 -0
- package/src/lib/index.ts +48 -30
- package/src/lib/lazyConfig.test.ts +54 -0
- package/src/lib/migrations/2026-04-24-component-prefix-and-suffix-renames.ts +64 -0
- package/src/lib/migrations/2026-04-24-legacy-keys-and-bg-to-canvas.ts +71 -0
- package/src/lib/migrations/2026-04-27-segmentedcontrol-disabled-flatten.ts +43 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-frame-and-cleanup.ts +68 -0
- package/src/lib/migrations/2026-05-08-collapsiblesection-variant-namespace.ts +35 -0
- package/src/lib/migrations/2026-05-10-sectiondivider-gradient-stops.ts +50 -0
- package/src/lib/migrations/2026-05-13-primary-to-brand.ts +90 -0
- package/src/lib/migrations/index.ts +93 -0
- package/src/lib/migrations/migrations.test.ts +341 -0
- package/src/lib/navLinkTypes.ts +1 -0
- package/src/lib/overlayState.ts +3 -0
- package/src/lib/paletteDerivation.ts +300 -0
- package/src/lib/parentRouteStore.ts +42 -0
- package/src/lib/parsers/globalRootBlock.ts +32 -0
- package/src/lib/presetService.ts +94 -0
- package/src/lib/router.ts +42 -10
- package/src/lib/scrollSection.ts +45 -0
- package/src/lib/slices/columns.ts +59 -0
- package/src/lib/slices/components.ts +362 -0
- package/src/lib/slices/domainVars.ts +15 -0
- package/src/lib/slices/fonts.ts +30 -0
- package/src/lib/slices/gradients.ts +153 -0
- package/src/lib/slices/overlays.ts +132 -0
- package/src/lib/slices/palettes.ts +26 -0
- package/src/lib/slices/shadows.ts +123 -0
- package/src/lib/storage.ts +88 -0
- package/src/lib/themeInit.ts +74 -0
- package/src/lib/themeService.ts +101 -0
- package/src/lib/themeTypes.ts +146 -0
- package/src/lib/tokenRegistry.ts +148 -0
- package/src/pages/ComponentEditorPage.svelte +384 -0
- package/src/pages/ComponentEditorPage.svelte.d.ts +2 -0
- package/src/pages/Editor.svelte +98 -0
- package/src/pages/Editor.svelte.d.ts +2 -0
- package/src/pages/EditorShell.svelte +348 -0
- package/src/styles/_padding.scss +34 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-italic-latin.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Fraunces/Fraunces-roman-latin.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin-ext.woff2 +0 -0
- package/src/styles/fonts/Manrope/Manrope-latin.woff2 +0 -0
- package/src/styles/fonts.css +22 -10
- package/src/styles/form-controls.css +14 -16
- package/src/styles/tokens.css +1322 -0
- package/src/styles/ui-editor.css +126 -0
- package/src/{showcase → ui}/BezierCurveEditor.svelte +14 -14
- package/src/{showcase → ui}/ColorEditPanel.svelte +42 -36
- package/src/ui/EditorViewSwitcher.svelte +180 -0
- package/src/ui/FontStackEditor.svelte +360 -0
- package/src/ui/GradientEditor.svelte +461 -0
- package/src/ui/GradientStopPicker.svelte +74 -0
- package/src/ui/PaletteEditor.svelte +1590 -0
- package/src/ui/PaletteEditor.test.ts +108 -0
- package/src/ui/PresetFileManager.svelte +567 -0
- package/src/ui/ProjectFontsSection.svelte +645 -0
- package/src/{showcase → ui}/SurfacesTab.svelte +39 -39
- package/src/{showcase → ui}/TextTab.svelte +27 -27
- package/src/{showcase/TokenFileManager.svelte → ui/ThemeFileManager.svelte} +196 -112
- package/src/ui/Toggle.svelte +108 -0
- package/src/ui/UICopyPopover.svelte +78 -0
- package/src/{showcase/EditorDialog.svelte → ui/UIDialog.svelte} +66 -25
- package/src/ui/UIFontFamilySelector.svelte +309 -0
- package/src/ui/UIFontSizeSelector.svelte +165 -0
- package/src/ui/UIFontWeightSelector.svelte +52 -0
- package/src/ui/UILineHeightSelector.svelte +47 -0
- package/src/ui/UILinkToggle.svelte +60 -0
- package/src/ui/UIOptionItem.svelte +74 -0
- package/src/ui/UIOptionList.svelte +27 -0
- package/src/ui/UIPaddingSelector.svelte +661 -0
- package/src/ui/UIPaletteSelector.svelte +1084 -0
- package/src/ui/UIRadio.svelte +72 -0
- package/src/ui/UIRadioGroup.svelte +59 -0
- package/src/ui/UIRelinkConfirmPopover.svelte +235 -0
- package/src/ui/UITokenSelector.svelte +509 -0
- package/src/ui/UIVariantSelector.svelte +145 -0
- package/src/ui/VariablesTab.svelte +252 -0
- package/src/ui/index.ts +31 -0
- package/src/ui/keepInViewport.ts +84 -0
- package/src/ui/palette/GradientStopEditor.svelte +482 -0
- package/src/ui/palette/OverridesPanel.svelte +526 -0
- package/src/ui/palette/PaletteBase.svelte +165 -0
- package/src/ui/palette/ScaleCurveEditor.svelte +38 -0
- package/src/ui/palette/paletteEditorState.ts +89 -0
- package/src/ui/sections/ColumnsSection.svelte +273 -0
- package/src/ui/sections/GradientsSection.svelte +147 -0
- package/src/ui/sections/OverlaysSection.svelte +670 -0
- package/src/ui/sections/ShadowsSection.svelte +1250 -0
- package/src/ui/sections/TokenScaleTable.svelte +332 -0
- package/src/ui/sections/tokenScales.ts +81 -0
- package/src/ui/variantScales.ts +108 -0
- package/src/components/DetailNav.svelte +0 -78
- package/src/components/Toggle.svelte +0 -86
- package/src/lib/tokenInit.ts +0 -29
- package/src/lib/tokenService.ts +0 -144
- package/src/lib/tokenTypes.ts +0 -45
- package/src/pages/Admin.svelte +0 -100
- package/src/pages/ShowcasePage.svelte +0 -144
- package/src/showcase/BackupBrowser.svelte +0 -617
- package/src/showcase/ComponentsTab.svelte +0 -105
- package/src/showcase/PaletteEditor.svelte +0 -2579
- package/src/showcase/PaletteSelector.svelte +0 -627
- package/src/showcase/TokenMap.svelte +0 -54
- package/src/showcase/VariablesTab.svelte +0 -2655
- package/src/showcase/VisualsTab.svelte +0 -231
- package/src/showcase/demos/BadgeDemo.svelte +0 -56
- package/src/showcase/demos/CardDemo.svelte +0 -50
- package/src/showcase/demos/ChoiceButtonsDemo.svelte +0 -192
- package/src/showcase/demos/CollapsibleSectionDemo.svelte +0 -54
- package/src/showcase/demos/DialogDemo.svelte +0 -42
- package/src/showcase/demos/InlineEditActionsDemo.svelte +0 -25
- package/src/showcase/demos/NotificationDemo.svelte +0 -147
- package/src/showcase/demos/ProgressBarDemo.svelte +0 -54
- package/src/showcase/demos/RadioButtonDemo.svelte +0 -56
- package/src/showcase/demos/SectionDividerDemo.svelte +0 -77
- package/src/showcase/demos/StandardButtonsDemo.svelte +0 -455
- package/src/showcase/demos/TabBarDemo.svelte +0 -58
- package/src/showcase/demos/TooltipDemo.svelte +0 -52
- package/src/showcase/editor.css +0 -93
- package/src/showcase/index.ts +0 -17
- package/src/styles/fonts/Domine/Domine-VariableFont_wght.ttf +0 -0
- package/src/styles/fonts/Domine/OFL.txt +0 -97
- package/src/styles/fonts/Domine/README.txt +0 -66
- /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} />
|