@volter/editor-sdk 0.5.57
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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +19 -0
- package/package.json +90 -0
- package/src/account.ts +210 -0
- package/src/chrome.ts +83 -0
- package/src/client.ts +1547 -0
- package/src/commands.ts +66 -0
- package/src/contributions.ts +985 -0
- package/src/document-probe.ts +237 -0
- package/src/editor-view.ts +220 -0
- package/src/extension.ts +40 -0
- package/src/generations.ts +178 -0
- package/src/host.ts +1167 -0
- package/src/http-transport.browser.ts +14 -0
- package/src/http-transport.node.ts +19 -0
- package/src/index.ts +128 -0
- package/src/layout-arrangements.ts +5 -0
- package/src/layouts.tsx +108 -0
- package/src/looks.ts +14 -0
- package/src/project/output-roots.ts +73 -0
- package/src/project/tab-census.ts +149 -0
- package/src/project-tool-catalog.ts +96 -0
- package/src/selection.tsx +108 -0
- package/src/services.ts +18 -0
- package/src/session/build-report.ts +19 -0
- package/src/session/collaboration-types.ts +262 -0
- package/src/session/command-table.ts +333 -0
- package/src/session/discovery.ts +90 -0
- package/src/session/editor-brand.ts +73 -0
- package/src/session/editor-compatibility.ts +248 -0
- package/src/session/editor-control-lifecycle.ts +68 -0
- package/src/session/editor-control-protocol.ts +5 -0
- package/src/session/entrypoint-selection-readers.ts +66 -0
- package/src/session/entrypoint-selection-source.ts +120 -0
- package/src/session/game-css-scope.ts +30 -0
- package/src/session/product-create.ts +24 -0
- package/src/session/product-locator.ts +389 -0
- package/src/session/project-module-url.ts +245 -0
- package/src/session/registry-format.ts +203 -0
- package/src/session/relative-path-guard.ts +56 -0
- package/src/session/source-glob.ts +15 -0
- package/src/session/tool-contribution-convention.ts +116 -0
- package/src/session/workbench-locator.ts +650 -0
- package/src/session.ts +41 -0
- package/src/share.ts +160 -0
- package/src/tools/errors.ts +91 -0
- package/src/tools/provider-execution.ts +70 -0
- package/src/tools/registry.ts +341 -0
- package/src/tools/types.ts +159 -0
- package/src/transport.ts +97 -0
- package/src/types.ts +1581 -0
- package/src/views.ts +164 -0
- package/src/widgets/design-system.ts +93 -0
- package/src/widgets/editor-appearance.ts +149 -0
- package/src/widgets/editor-material.ts +83 -0
- package/src/widgets/icon-set-registry.ts +105 -0
- package/src/widgets/index.ts +71 -0
- package/src/widgets/inspector-widgets/AlignmentGrid.tsx +182 -0
- package/src/widgets/inspector-widgets/AssetSlotPicker.tsx +123 -0
- package/src/widgets/inspector-widgets/BorderEditor.tsx +309 -0
- package/src/widgets/inspector-widgets/ColorPicker.tsx +549 -0
- package/src/widgets/inspector-widgets/CurveEditor.tsx +359 -0
- package/src/widgets/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/widgets/inspector-widgets/FontPicker.tsx +191 -0
- package/src/widgets/inspector-widgets/GradientEditor.tsx +623 -0
- package/src/widgets/inspector-widgets/ScrubbableInput.tsx +180 -0
- package/src/widgets/inspector-widgets/ShadowEditor.tsx +319 -0
- package/src/widgets/inspector-widgets/color-utils.ts +201 -0
- package/src/widgets/inspector-widgets/curve-utils.ts +212 -0
- package/src/widgets/inspector-widgets/index.ts +24 -0
- package/src/widgets/inspector-widgets/shared.tsx +140 -0
- package/src/widgets/interactive-edit-scope.ts +33 -0
- package/src/widgets/patterns/Dialog.tsx +129 -0
- package/src/widgets/patterns/Fields.tsx +44 -0
- package/src/widgets/patterns/List.tsx +25 -0
- package/src/widgets/patterns/StateSurface.tsx +40 -0
- package/src/widgets/patterns/Surfaces.tsx +122 -0
- package/src/widgets/patterns/Tabs.tsx +80 -0
- package/src/widgets/patterns/Toolbar.tsx +72 -0
- package/src/widgets/patterns/Tree.tsx +72 -0
- package/src/widgets/primitives/AnchoredMenu.tsx +260 -0
- package/src/widgets/primitives/Button.tsx +62 -0
- package/src/widgets/primitives/ColorInput.tsx +78 -0
- package/src/widgets/primitives/DraftTextInput.tsx +63 -0
- package/src/widgets/primitives/EditorIcon.tsx +157 -0
- package/src/widgets/primitives/FormControls.tsx +88 -0
- package/src/widgets/primitives/HoverPreview.tsx +96 -0
- package/src/widgets/primitives/JsonInput.tsx +113 -0
- package/src/widgets/primitives/Layout.tsx +100 -0
- package/src/widgets/primitives/Menu.tsx +140 -0
- package/src/widgets/primitives/NumberInput.tsx +169 -0
- package/src/widgets/primitives/Panel.tsx +80 -0
- package/src/widgets/primitives/SectionHeader.tsx +77 -0
- package/src/widgets/primitives/Text.tsx +54 -0
- package/src/widgets/primitives/ThemeRootPortal.tsx +52 -0
- package/src/widgets/primitives/Tooltip.tsx +204 -0
- package/src/widgets/primitives/Vec3Input.tsx +70 -0
- package/src/widgets/primitives/banner-tones.ts +32 -0
- package/src/widgets/primitives/clamp-to-viewport.ts +44 -0
- package/src/widgets/primitives/editor-icons.ts +245 -0
- package/src/widgets/primitives/panel-header-styles.ts +42 -0
- package/src/widgets/theme.ts +2633 -0
- package/src/widgets/z-index.ts +25 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { Button, EditorPopover, TextInput } from '../design-system';
|
|
2
|
+
/**
|
|
3
|
+
* ColorPicker — HSV area + hue/alpha sliders, hex↔rgb↔hsl format cycle,
|
|
4
|
+
* eyedropper, recent colors (spec 27 §5 C1). Ported from
|
|
5
|
+
* `visual-edit/inspector.tsx`'s `ColorPicker` (:1043-1421). The reference
|
|
6
|
+
* threaded `onCommit`/`onClose`/an imperative `onActivateEyedropper` callback
|
|
7
|
+
* that the HOST app wired to its own canvas-hit-test eyedropper mode; this
|
|
8
|
+
* port instead feature-detects the native `EyeDropper` API directly (per the
|
|
9
|
+
* spec's explicit ask) and falls back to a caller-supplied
|
|
10
|
+
* `eyedropperFallback` (wired at the C2 step to
|
|
11
|
+
* `ui-source/inspect.ts:effectiveColorFromChain` over whatever background
|
|
12
|
+
* chain the host resolves at the click point — this pure widget has no DOM
|
|
13
|
+
* reach of its own, Rule zero). `value`/`onChange`/`onChangeEnd` are the
|
|
14
|
+
* kit-wide contract (`shared.tsx`); `onClose` is optional (a bare popover
|
|
15
|
+
* caller can omit it and just unmount the picker on outside click itself).
|
|
16
|
+
*
|
|
17
|
+
* D3.d (spec 27 §6) completes that fallback: `onEyedropperPick`, when given,
|
|
18
|
+
* is preferred over the synchronous `eyedropperFallback` and begins an
|
|
19
|
+
* INTERACTIVE canvas-pick session (`../../authoring/eyedropper-session.ts`) —
|
|
20
|
+
* this widget hands the session ITS OWN `applySampledColor` callback and
|
|
21
|
+
* returns immediately; the canvas (`RootSelectionOverlay`) shows a
|
|
22
|
+
* cursor-following swatch and resolves the session on click/Escape, which
|
|
23
|
+
* invokes the callback this widget passed in (or not at all, on cancel).
|
|
24
|
+
* `eyedropperFallback` stays as the synchronous, no-canvas-context fallback
|
|
25
|
+
* (e.g. a widget gallery/story with neither native EyeDropper nor a host that
|
|
26
|
+
* wires the session).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
30
|
+
import {
|
|
31
|
+
type ColorFormat,
|
|
32
|
+
colorToString,
|
|
33
|
+
hexToRgb,
|
|
34
|
+
hslToRgb,
|
|
35
|
+
hsvToRgb,
|
|
36
|
+
parseAlpha,
|
|
37
|
+
rgbToHsv,
|
|
38
|
+
toHex,
|
|
39
|
+
} from './color-utils';
|
|
40
|
+
import { fireChange, fireEnd, THEME } from './shared';
|
|
41
|
+
|
|
42
|
+
const PICKER_W = 200;
|
|
43
|
+
const PICKER_H = 150;
|
|
44
|
+
const HUE_H = 14;
|
|
45
|
+
|
|
46
|
+
export interface ColorPickerProps {
|
|
47
|
+
value: string;
|
|
48
|
+
onChange: (color: string) => void;
|
|
49
|
+
onChangeEnd?: ((color: string) => void) | undefined;
|
|
50
|
+
onClose?: (() => void) | undefined;
|
|
51
|
+
recentColors?: string[] | undefined;
|
|
52
|
+
onAddRecentColor?: ((color: string) => void) | undefined;
|
|
53
|
+
/** Feature-detected native browser eyedropper, with a caller-supplied
|
|
54
|
+
* fallback (see file doc comment) for browsers without the API. */
|
|
55
|
+
eyedropperFallback?: (() => string | null) | undefined;
|
|
56
|
+
/** D3.d — preferred over `eyedropperFallback` when given: begins an
|
|
57
|
+
* interactive canvas-pick session instead of reading one static value (see
|
|
58
|
+
* file doc comment). */
|
|
59
|
+
onEyedropperPick?: ((apply: (color: string | null) => void) => void) | undefined;
|
|
60
|
+
disabled?: boolean | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ColorPicker({
|
|
64
|
+
value,
|
|
65
|
+
onChange,
|
|
66
|
+
onChangeEnd,
|
|
67
|
+
onClose,
|
|
68
|
+
recentColors,
|
|
69
|
+
onAddRecentColor,
|
|
70
|
+
eyedropperFallback,
|
|
71
|
+
onEyedropperPick,
|
|
72
|
+
disabled,
|
|
73
|
+
}: ColorPickerProps): React.ReactElement {
|
|
74
|
+
const initialHex = toHex(value);
|
|
75
|
+
const initialRgb = hexToRgb(initialHex);
|
|
76
|
+
const [hsv, setHsv] = useState<[number, number, number]>(() => rgbToHsv(...initialRgb));
|
|
77
|
+
const [alpha, setAlpha] = useState(() => parseAlpha(value));
|
|
78
|
+
const [format, setFormat] = useState<ColorFormat>('hex');
|
|
79
|
+
const [colorInput, setColorInput] = useState(initialHex);
|
|
80
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
81
|
+
const satCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
82
|
+
const hueCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
83
|
+
const alphaCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
84
|
+
const draggingRef = useRef<'sat' | 'hue' | 'alpha' | null>(null);
|
|
85
|
+
|
|
86
|
+
const currentColor = useCallback(
|
|
87
|
+
(h = hsv, a = alpha, f = format): string => {
|
|
88
|
+
const [r, g, b] = hsvToRgb(h[0], h[1], h[2]);
|
|
89
|
+
return colorToString(r, g, b, a, f);
|
|
90
|
+
},
|
|
91
|
+
[hsv, alpha, format],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
setColorInput(currentColor());
|
|
96
|
+
}, [currentColor]);
|
|
97
|
+
|
|
98
|
+
// Saturation/value canvas.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const canvas = satCanvasRef.current;
|
|
101
|
+
const ctx = canvas?.getContext('2d');
|
|
102
|
+
if (!canvas || !ctx) return;
|
|
103
|
+
const [hr, hg, hb] = hsvToRgb(hsv[0], 1, 1);
|
|
104
|
+
const gradH = ctx.createLinearGradient(0, 0, PICKER_W, 0);
|
|
105
|
+
gradH.addColorStop(0, '#ffffff');
|
|
106
|
+
gradH.addColorStop(1, `rgb(${hr},${hg},${hb})`);
|
|
107
|
+
ctx.fillStyle = gradH;
|
|
108
|
+
ctx.fillRect(0, 0, PICKER_W, PICKER_H);
|
|
109
|
+
const gradV = ctx.createLinearGradient(0, 0, 0, PICKER_H);
|
|
110
|
+
gradV.addColorStop(0, 'rgba(0,0,0,0)');
|
|
111
|
+
gradV.addColorStop(1, 'rgba(0,0,0,1)');
|
|
112
|
+
ctx.fillStyle = gradV;
|
|
113
|
+
ctx.fillRect(0, 0, PICKER_W, PICKER_H);
|
|
114
|
+
}, [hsv[0]]);
|
|
115
|
+
|
|
116
|
+
// Hue slider.
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const canvas = hueCanvasRef.current;
|
|
119
|
+
const ctx = canvas?.getContext('2d');
|
|
120
|
+
if (!canvas || !ctx) return;
|
|
121
|
+
const grad = ctx.createLinearGradient(0, 0, PICKER_W, 0);
|
|
122
|
+
for (let i = 0; i <= 6; i++) {
|
|
123
|
+
const [r, g, b] = hsvToRgb(i / 6, 1, 1);
|
|
124
|
+
grad.addColorStop(i / 6, `rgb(${r},${g},${b})`);
|
|
125
|
+
}
|
|
126
|
+
ctx.fillStyle = grad;
|
|
127
|
+
ctx.fillRect(0, 0, PICKER_W, HUE_H);
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
// Alpha slider.
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const canvas = alphaCanvasRef.current;
|
|
133
|
+
const ctx = canvas?.getContext('2d');
|
|
134
|
+
if (!canvas || !ctx) return;
|
|
135
|
+
const size = 4;
|
|
136
|
+
for (let y = 0; y < HUE_H; y += size) {
|
|
137
|
+
for (let x = 0; x < PICKER_W; x += size) {
|
|
138
|
+
ctx.fillStyle = (x / size + y / size) % 2 === 0 ? '#ffffff' : '#cccccc';
|
|
139
|
+
ctx.fillRect(x, y, size, size);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const [r, g, b] = hsvToRgb(hsv[0], hsv[1], hsv[2]);
|
|
143
|
+
const grad = ctx.createLinearGradient(0, 0, PICKER_W, 0);
|
|
144
|
+
grad.addColorStop(0, `rgba(${r},${g},${b},0)`);
|
|
145
|
+
grad.addColorStop(1, `rgba(${r},${g},${b},1)`);
|
|
146
|
+
ctx.fillStyle = grad;
|
|
147
|
+
ctx.fillRect(0, 0, PICKER_W, HUE_H);
|
|
148
|
+
}, [hsv]);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!onClose) return;
|
|
152
|
+
function handleKey(e: KeyboardEvent): void {
|
|
153
|
+
if (e.key === 'Escape') onClose?.();
|
|
154
|
+
}
|
|
155
|
+
function handleClick(e: MouseEvent): void {
|
|
156
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) onClose?.();
|
|
157
|
+
}
|
|
158
|
+
document.addEventListener('keydown', handleKey);
|
|
159
|
+
document.addEventListener('mousedown', handleClick, true);
|
|
160
|
+
return () => {
|
|
161
|
+
document.removeEventListener('keydown', handleKey);
|
|
162
|
+
document.removeEventListener('mousedown', handleClick, true);
|
|
163
|
+
};
|
|
164
|
+
}, [onClose]);
|
|
165
|
+
|
|
166
|
+
/** sat/hue/alpha canvases all resolve `{hsv?, alpha?}` from a point the
|
|
167
|
+
* SAME way (ratio-of-rect, sat also inverts Y) — one function both
|
|
168
|
+
* `beginDrag` (mousedown, `e.currentTarget`) and the module-level
|
|
169
|
+
* `mousemove`/`mouseup` listener (no `currentTarget`, refs instead) call,
|
|
170
|
+
* keeping each of THOSE under biome's cognitive-complexity ceiling. */
|
|
171
|
+
const applyDragPoint = useCallback(
|
|
172
|
+
(
|
|
173
|
+
which: 'sat' | 'hue' | 'alpha',
|
|
174
|
+
canvas: HTMLCanvasElement | null,
|
|
175
|
+
clientX: number,
|
|
176
|
+
clientY: number,
|
|
177
|
+
): void => {
|
|
178
|
+
const rect = canvas?.getBoundingClientRect();
|
|
179
|
+
if (!rect) return;
|
|
180
|
+
if (which === 'sat') {
|
|
181
|
+
const next: [number, number, number] = [
|
|
182
|
+
hsv[0],
|
|
183
|
+
ratio(clientX, rect.left, rect.width),
|
|
184
|
+
invRatio(clientY, rect.top, rect.height),
|
|
185
|
+
];
|
|
186
|
+
setHsv(next);
|
|
187
|
+
fireChange({ onChange }, currentColor(next));
|
|
188
|
+
} else if (which === 'hue') {
|
|
189
|
+
const next: [number, number, number] = [
|
|
190
|
+
ratio(clientX, rect.left, rect.width),
|
|
191
|
+
hsv[1],
|
|
192
|
+
hsv[2],
|
|
193
|
+
];
|
|
194
|
+
setHsv(next);
|
|
195
|
+
fireChange({ onChange }, currentColor(next));
|
|
196
|
+
} else {
|
|
197
|
+
const a = Math.round(ratio(clientX, rect.left, rect.width) * 100) / 100;
|
|
198
|
+
setAlpha(a);
|
|
199
|
+
fireChange({ onChange }, currentColor(hsv, a));
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
[hsv, currentColor, onChange],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
function handleMove(e: MouseEvent): void {
|
|
207
|
+
const which = draggingRef.current;
|
|
208
|
+
if (!which) return;
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
const canvas =
|
|
211
|
+
which === 'sat'
|
|
212
|
+
? satCanvasRef.current
|
|
213
|
+
: which === 'hue'
|
|
214
|
+
? hueCanvasRef.current
|
|
215
|
+
: alphaCanvasRef.current;
|
|
216
|
+
applyDragPoint(which, canvas, e.clientX, e.clientY);
|
|
217
|
+
}
|
|
218
|
+
function handleUp(): void {
|
|
219
|
+
if (draggingRef.current) {
|
|
220
|
+
draggingRef.current = null;
|
|
221
|
+
const c = currentColor();
|
|
222
|
+
fireEnd({ onChange, onChangeEnd }, c);
|
|
223
|
+
onAddRecentColor?.(c);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
window.addEventListener('mousemove', handleMove);
|
|
227
|
+
window.addEventListener('mouseup', handleUp);
|
|
228
|
+
return () => {
|
|
229
|
+
window.removeEventListener('mousemove', handleMove);
|
|
230
|
+
window.removeEventListener('mouseup', handleUp);
|
|
231
|
+
};
|
|
232
|
+
}, [applyDragPoint, currentColor, onChange, onChangeEnd, onAddRecentColor]);
|
|
233
|
+
|
|
234
|
+
const beginDrag = (
|
|
235
|
+
which: 'sat' | 'hue' | 'alpha',
|
|
236
|
+
e: React.MouseEvent<HTMLCanvasElement>,
|
|
237
|
+
): void => {
|
|
238
|
+
if (disabled) return;
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
draggingRef.current = which;
|
|
241
|
+
applyDragPoint(which, e.currentTarget, e.clientX, e.clientY);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handleColorInputSubmit = (): void => {
|
|
245
|
+
const val = colorInput.trim();
|
|
246
|
+
const hex = parseHexInput(val);
|
|
247
|
+
const parsed = hex
|
|
248
|
+
? { r: hex[0], g: hex[1], b: hex[2], a: 1 }
|
|
249
|
+
: (parseRgbaInput(val) ?? parseHslaInput(val));
|
|
250
|
+
if (!parsed) return;
|
|
251
|
+
setHsv(rgbToHsv(parsed.r, parsed.g, parsed.b));
|
|
252
|
+
setAlpha(parsed.a);
|
|
253
|
+
const c = hex ? val : colorToString(parsed.r, parsed.g, parsed.b, parsed.a, format);
|
|
254
|
+
fireChange({ onChange }, c);
|
|
255
|
+
fireEnd({ onChange, onChangeEnd }, c);
|
|
256
|
+
onAddRecentColor?.(c);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const cycleFormat = (): void => {
|
|
260
|
+
setFormat((f) => (f === 'hex' ? 'rgb' : f === 'rgb' ? 'hsl' : 'hex'));
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const canEyedrop = typeof (globalThis as { EyeDropper?: unknown }).EyeDropper === 'function';
|
|
264
|
+
|
|
265
|
+
const applySampledColor = (sampled: string): void => {
|
|
266
|
+
const h = toHex(sampled);
|
|
267
|
+
setHsv(rgbToHsv(...hexToRgb(h)));
|
|
268
|
+
setAlpha(parseAlpha(sampled));
|
|
269
|
+
fireChange({ onChange }, sampled);
|
|
270
|
+
fireEnd({ onChange, onChangeEnd }, sampled);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const handleEyedropper = async (): Promise<void> => {
|
|
274
|
+
if (canEyedrop) {
|
|
275
|
+
try {
|
|
276
|
+
const ED = (
|
|
277
|
+
globalThis as unknown as {
|
|
278
|
+
EyeDropper: new () => { open: () => Promise<{ sRGBHex: string }> };
|
|
279
|
+
}
|
|
280
|
+
).EyeDropper;
|
|
281
|
+
const res = await new ED().open();
|
|
282
|
+
applySampledColor(res.sRGBHex);
|
|
283
|
+
} catch {
|
|
284
|
+
/* cancelled */
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// D3.d — no native EyeDropper: prefer the interactive canvas-pick session
|
|
289
|
+
// when the host wired one; a canvas-less caller (gallery/story) falls
|
|
290
|
+
// back to the synchronous, single-value `eyedropperFallback`.
|
|
291
|
+
if (onEyedropperPick) {
|
|
292
|
+
onEyedropperPick((color) => {
|
|
293
|
+
if (color) applySampledColor(color);
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const fallback = eyedropperFallback?.();
|
|
298
|
+
if (fallback) applySampledColor(fallback);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const [r, g, b] = hsvToRgb(hsv[0], hsv[1], hsv[2]);
|
|
302
|
+
const previewBg = `rgba(${r},${g},${b},${alpha})`;
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<EditorPopover
|
|
306
|
+
ref={containerRef}
|
|
307
|
+
data-testid="inspector-color-picker"
|
|
308
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
309
|
+
style={{
|
|
310
|
+
padding: 10,
|
|
311
|
+
width: PICKER_W + 16,
|
|
312
|
+
boxSizing: 'border-box',
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
<canvas
|
|
316
|
+
ref={satCanvasRef}
|
|
317
|
+
data-testid="color-picker-sv"
|
|
318
|
+
width={PICKER_W}
|
|
319
|
+
height={PICKER_H}
|
|
320
|
+
onMouseDown={(e) => beginDrag('sat', e)}
|
|
321
|
+
style={{ cursor: 'crosshair', borderRadius: THEME.radiusSmall, display: 'block' }}
|
|
322
|
+
/>
|
|
323
|
+
<canvas
|
|
324
|
+
ref={hueCanvasRef}
|
|
325
|
+
data-testid="color-picker-hue"
|
|
326
|
+
width={PICKER_W}
|
|
327
|
+
height={HUE_H}
|
|
328
|
+
onMouseDown={(e) => beginDrag('hue', e)}
|
|
329
|
+
style={{
|
|
330
|
+
cursor: 'pointer',
|
|
331
|
+
borderRadius: THEME.radiusSmall,
|
|
332
|
+
display: 'block',
|
|
333
|
+
marginTop: 6,
|
|
334
|
+
}}
|
|
335
|
+
/>
|
|
336
|
+
<canvas
|
|
337
|
+
ref={alphaCanvasRef}
|
|
338
|
+
data-testid="color-picker-alpha"
|
|
339
|
+
width={PICKER_W}
|
|
340
|
+
height={HUE_H}
|
|
341
|
+
onMouseDown={(e) => beginDrag('alpha', e)}
|
|
342
|
+
style={{
|
|
343
|
+
cursor: 'pointer',
|
|
344
|
+
borderRadius: THEME.radiusSmall,
|
|
345
|
+
display: 'block',
|
|
346
|
+
marginTop: 6,
|
|
347
|
+
}}
|
|
348
|
+
/>
|
|
349
|
+
<div style={{ display: 'flex', gap: 6, alignItems: 'center', marginTop: 8 }}>
|
|
350
|
+
<div
|
|
351
|
+
style={{
|
|
352
|
+
width: 22,
|
|
353
|
+
height: 22,
|
|
354
|
+
borderRadius: THEME.radiusSmall,
|
|
355
|
+
flexShrink: 0,
|
|
356
|
+
border: `1px solid ${THEME.border}`,
|
|
357
|
+
background: previewBg,
|
|
358
|
+
}}
|
|
359
|
+
/>
|
|
360
|
+
<TextInput
|
|
361
|
+
type="text"
|
|
362
|
+
data-testid="color-picker-text"
|
|
363
|
+
value={colorInput}
|
|
364
|
+
onChange={(e) => setColorInput(e.target.value)}
|
|
365
|
+
onKeyDown={(e) => {
|
|
366
|
+
if (e.key === 'Enter') handleColorInputSubmit();
|
|
367
|
+
}}
|
|
368
|
+
onBlur={handleColorInputSubmit}
|
|
369
|
+
data-density="compact"
|
|
370
|
+
data-monospace="true"
|
|
371
|
+
style={{ flex: 1 }}
|
|
372
|
+
/>
|
|
373
|
+
<Button
|
|
374
|
+
type="button"
|
|
375
|
+
variant="secondary"
|
|
376
|
+
size="compact"
|
|
377
|
+
data-testid="color-format-toggle"
|
|
378
|
+
onClick={cycleFormat}
|
|
379
|
+
title="Cycle color format"
|
|
380
|
+
style={{ flexShrink: 0 }}
|
|
381
|
+
>
|
|
382
|
+
{format.toUpperCase()}
|
|
383
|
+
</Button>
|
|
384
|
+
{(canEyedrop || eyedropperFallback || onEyedropperPick) && (
|
|
385
|
+
<Button
|
|
386
|
+
type="button"
|
|
387
|
+
variant="secondary"
|
|
388
|
+
size="compact"
|
|
389
|
+
data-testid="color-picker-eyedropper"
|
|
390
|
+
onClick={handleEyedropper}
|
|
391
|
+
title="Eyedropper"
|
|
392
|
+
style={{ flexShrink: 0 }}
|
|
393
|
+
>
|
|
394
|
+
⌖
|
|
395
|
+
</Button>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
{recentColors && recentColors.length > 0 && (
|
|
399
|
+
<div
|
|
400
|
+
style={{
|
|
401
|
+
display: 'flex',
|
|
402
|
+
flexWrap: 'wrap',
|
|
403
|
+
gap: 3,
|
|
404
|
+
marginTop: 8,
|
|
405
|
+
paddingTop: 6,
|
|
406
|
+
borderTop: `1px solid ${THEME.border}`,
|
|
407
|
+
}}
|
|
408
|
+
>
|
|
409
|
+
{recentColors.map((rc, i) => (
|
|
410
|
+
<div
|
|
411
|
+
key={`${rc}-${i}`}
|
|
412
|
+
data-testid="color-picker-recent"
|
|
413
|
+
onClick={() => {
|
|
414
|
+
const h = toHex(rc);
|
|
415
|
+
setHsv(rgbToHsv(...hexToRgb(h)));
|
|
416
|
+
setAlpha(parseAlpha(rc));
|
|
417
|
+
fireChange({ onChange }, rc);
|
|
418
|
+
fireEnd({ onChange, onChangeEnd }, rc);
|
|
419
|
+
}}
|
|
420
|
+
title={rc}
|
|
421
|
+
style={{
|
|
422
|
+
width: 16,
|
|
423
|
+
height: 16,
|
|
424
|
+
borderRadius: THEME.radiusSmall,
|
|
425
|
+
cursor: 'pointer',
|
|
426
|
+
background: rc,
|
|
427
|
+
border: `1px solid ${THEME.border}`,
|
|
428
|
+
}}
|
|
429
|
+
/>
|
|
430
|
+
))}
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
</EditorPopover>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** A compact swatch that opens a `ColorPicker` popover — the row-style usage
|
|
438
|
+
* most inspector sections want (visual-edit's `ColorRow`/`StrokeColorSwatch`,
|
|
439
|
+
* :1425-1512 — folded into one configurable component here since both were
|
|
440
|
+
* the identical swatch+popover shell around different labels). */
|
|
441
|
+
export interface ColorSwatchProps extends ColorPickerProps {
|
|
442
|
+
label?: string | undefined;
|
|
443
|
+
testId?: string | undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function ColorSwatch({
|
|
447
|
+
label,
|
|
448
|
+
value,
|
|
449
|
+
onChange,
|
|
450
|
+
onChangeEnd,
|
|
451
|
+
recentColors,
|
|
452
|
+
onAddRecentColor,
|
|
453
|
+
eyedropperFallback,
|
|
454
|
+
onEyedropperPick,
|
|
455
|
+
disabled,
|
|
456
|
+
testId,
|
|
457
|
+
}: ColorSwatchProps): React.ReactElement {
|
|
458
|
+
const [open, setOpen] = useState(false);
|
|
459
|
+
const hex = toHex(value);
|
|
460
|
+
return (
|
|
461
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, position: 'relative' }}>
|
|
462
|
+
<div
|
|
463
|
+
data-testid={testId ? `${testId}-swatch` : 'color-swatch'}
|
|
464
|
+
onClick={() => {
|
|
465
|
+
if (!disabled) setOpen((v) => !v);
|
|
466
|
+
}}
|
|
467
|
+
style={{
|
|
468
|
+
width: 20,
|
|
469
|
+
height: 20,
|
|
470
|
+
borderRadius: THEME.radiusSmall,
|
|
471
|
+
flexShrink: 0,
|
|
472
|
+
background: hex,
|
|
473
|
+
border: `1px solid ${THEME.border}`,
|
|
474
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
475
|
+
}}
|
|
476
|
+
/>
|
|
477
|
+
{label && (
|
|
478
|
+
<span style={{ fontSize: 10, color: THEME.textMuted, flexShrink: 0 }}>{label}</span>
|
|
479
|
+
)}
|
|
480
|
+
{open && !disabled && (
|
|
481
|
+
<div
|
|
482
|
+
style={{
|
|
483
|
+
position: 'absolute',
|
|
484
|
+
zIndex: 'var(--vgai-z-dropdown)',
|
|
485
|
+
top: '100%',
|
|
486
|
+
left: 0,
|
|
487
|
+
marginTop: 4,
|
|
488
|
+
}}
|
|
489
|
+
>
|
|
490
|
+
<ColorPicker
|
|
491
|
+
value={value}
|
|
492
|
+
onChange={onChange}
|
|
493
|
+
onChangeEnd={onChangeEnd}
|
|
494
|
+
onClose={() => setOpen(false)}
|
|
495
|
+
recentColors={recentColors}
|
|
496
|
+
onAddRecentColor={onAddRecentColor}
|
|
497
|
+
eyedropperFallback={eyedropperFallback}
|
|
498
|
+
onEyedropperPick={onEyedropperPick}
|
|
499
|
+
/>
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function clamp01(n: number): number {
|
|
507
|
+
return Math.max(0, Math.min(1, n));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** `clientPos` as a 0..1 fraction of `[start, start+size]`. */
|
|
511
|
+
function ratio(clientPos: number, start: number, size: number): number {
|
|
512
|
+
return clamp01(size ? (clientPos - start) / size : 0);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Same as `ratio`, but Y-inverted — canvas top is `v=1` for the sat/value area. */
|
|
516
|
+
function invRatio(clientPos: number, start: number, size: number): number {
|
|
517
|
+
return clamp01(size ? 1 - (clientPos - start) / size : 0);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** `"#rrggbb"` -> its RGB triple, or `null` if `val` isn't a bare 6-digit hex. */
|
|
521
|
+
function parseHexInput(val: string): [number, number, number] | null {
|
|
522
|
+
return /^#[0-9a-fA-F]{6}$/.test(val) ? hexToRgb(val) : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** `"rgb(a)(r, g, b[, a])"` -> `{r,g,b,a}`, or `null` if it doesn't match. */
|
|
526
|
+
function parseRgbaInput(val: string): { r: number; g: number; b: number; a: number } | null {
|
|
527
|
+
const m = val.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
528
|
+
if (!m) return null;
|
|
529
|
+
const [, rs, gs, bs, as] = m;
|
|
530
|
+
return {
|
|
531
|
+
r: Number.parseInt(rs ?? '0', 10),
|
|
532
|
+
g: Number.parseInt(gs ?? '0', 10),
|
|
533
|
+
b: Number.parseInt(bs ?? '0', 10),
|
|
534
|
+
a: as ? Number.parseFloat(as) : 1,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** `"hsl(a)(h, s%, l%[, a])"` -> `{r,g,b,a}` (converted to RGB), or `null`. */
|
|
539
|
+
function parseHslaInput(val: string): { r: number; g: number; b: number; a: number } | null {
|
|
540
|
+
const m = val.match(/hsla?\((\d+),\s*(\d+)%?,\s*(\d+)%?(?:,\s*([\d.]+))?\)/);
|
|
541
|
+
if (!m) return null;
|
|
542
|
+
const [, hs, ss, ls, as] = m;
|
|
543
|
+
const [r, g, b] = hslToRgb(
|
|
544
|
+
Number.parseInt(hs ?? '0', 10) / 360,
|
|
545
|
+
Number.parseInt(ss ?? '0', 10) / 100,
|
|
546
|
+
Number.parseInt(ls ?? '0', 10) / 100,
|
|
547
|
+
);
|
|
548
|
+
return { r, g, b, a: as ? Number.parseFloat(as) : 1 };
|
|
549
|
+
}
|