@vivix-labs/magic-edit 0.0.0-stage → 1.0.0-beta

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.
@@ -0,0 +1,2163 @@
1
+ // packages/magic-edit/src/client.tsx
2
+ import { useEffect as useEffect3, useEffectEvent, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState2 } from "react";
3
+
4
+ // packages/ui-system/dist/button.js
5
+ import { jsx as _jsx2, jsxs as _jsxs } from "react/jsx-runtime";
6
+
7
+ // packages/ui-system/dist/icon.js
8
+ import { jsx as _jsx } from "react/jsx-runtime";
9
+ function Icon({ name, size }) {
10
+ return _jsx("span", { "aria-hidden": "true", className: "vx-icon", "data-icon": name, style: size === void 0 ? void 0 : { "--vx-icon-size": `${size}px` } });
11
+ }
12
+
13
+ // packages/ui-system/dist/button-variants.js
14
+ var buttonVariants = {
15
+ "text-filled-sm": { size: 32, appearance: "filled", shape: "text", width: 215 },
16
+ "text-filled-leading-sm": { size: 32, appearance: "filled", shape: "text" },
17
+ "text-ghost-sm": { size: 32, appearance: "ghost", shape: "text" },
18
+ "text-on-content-sm": { size: 32, appearance: "on-content", shape: "text" },
19
+ "text-tonal-sm": { size: 32, appearance: "tonal", shape: "text" },
20
+ // The medium recipes are shared by all dialog footer actions.
21
+ "text-filled-md": { size: 40, appearance: "filled", shape: "text" },
22
+ "text-tonal-md": { size: 40, appearance: "tonal", shape: "text" },
23
+ "text-ghost-md": { size: 40, appearance: "ghost", shape: "text" },
24
+ "icon-ghost-md": { size: 40, appearance: "ghost", shape: "icon" },
25
+ "text-filled-lg": { size: 44, appearance: "filled", shape: "text", width: 412 },
26
+ "text-tonal-lg": { size: 44, appearance: "tonal", shape: "text", width: 412 },
27
+ "text-outline-lg": {
28
+ size: 44,
29
+ appearance: "outline",
30
+ shape: "text",
31
+ width: 412
32
+ },
33
+ "pill-filled": { size: 44, appearance: "filled", shape: "pill" },
34
+ "pill-plain": { size: 48, appearance: "plain", shape: "pill" },
35
+ "pill-small": { size: 32, appearance: "plain", shape: "pill" },
36
+ "icon-outline-sm": { size: 32, appearance: "outline", shape: "icon" },
37
+ "icon-on-content": { size: 24, appearance: "on-content", shape: "icon" },
38
+ "icon-outline-lg": { size: 48, appearance: "outline", shape: "icon" },
39
+ // Compact bare action: reuse the transparent/default and surface-hover ghost treatment.
40
+ "icon-bare-xs": { size: 24, appearance: "ghost", shape: "icon" },
41
+ "icon-ghost-sm": { size: 32, appearance: "ghost", shape: "icon" },
42
+ "icon-ghost-lg": { size: 48, appearance: "ghost", shape: "icon" },
43
+ "icon-circle-sm": { size: 32, appearance: "filled", shape: "circle" },
44
+ "icon-circle-lg": { size: 44, appearance: "filled", shape: "circle" }
45
+ };
46
+
47
+ // packages/ui-system/dist/button.js
48
+ function ButtonFrame({ label, variant, loading = false, width, children, disabled, type = "button", style, className, ...props }) {
49
+ const spec = buttonVariants[variant];
50
+ const buttonStyle = "width" in spec ? { ...style, "--vx-button-width": `${spec.width}px` } : style;
51
+ return _jsxs("button", { ...props, type, className: className ? `vx-button ${className}` : "vx-button", "data-variant": variant, "data-appearance": spec.appearance, "data-shape": spec.shape, "data-size": spec.size, "data-width": width, "data-state": loading ? "loading" : disabled ? "disabled" : "default", style: buttonStyle, disabled: disabled || loading, "aria-label": label, "aria-busy": loading || void 0, children: [_jsx2("span", { className: "vx-button-content", children }), loading && _jsx2("span", { className: "vx-button-loading", "aria-hidden": "true" })] });
52
+ }
53
+ function Button({ variant = "text-filled-sm", leadingIcon, trailingIcon, ...props }) {
54
+ return _jsxs(ButtonFrame, { ...props, variant, children: [leadingIcon && _jsx2(Icon, { name: leadingIcon }), _jsx2("span", { className: "vx-button-label", children: props.label }), trailingIcon && _jsx2("span", { className: "vx-button-trailing", children: _jsx2(Icon, { name: trailingIcon }) })] });
55
+ }
56
+ function IconButton({ icon, variant = "icon-ghost-sm", ...props }) {
57
+ return _jsx2(ButtonFrame, { ...props, variant, children: _jsx2(Icon, { name: icon }) });
58
+ }
59
+
60
+ // packages/ui-system/dist/text.js
61
+ import { jsx as _jsx3 } from "react/jsx-runtime";
62
+ function Divider({ orientation = "horizontal" }) {
63
+ return _jsx3("span", { className: "vx-divider", "data-orientation": orientation, "aria-hidden": "true" });
64
+ }
65
+
66
+ // packages/ui-system/dist/annotation-toolbar.js
67
+ import { jsx as _jsx4, Fragment as _Fragment, jsxs as _jsxs2 } from "react/jsx-runtime";
68
+ import { useEffect, useRef } from "react";
69
+ var tools = [
70
+ { id: "select", label: "Select element" },
71
+ { id: "area", label: "Rectangle" },
72
+ { id: "draw", label: "Draw" }
73
+ ];
74
+ function AnnotationToolbar({ active, tool, actions, onActiveChange, onToolChange }) {
75
+ const toolbar = useRef(null);
76
+ const focusAfterChange = useRef(false);
77
+ useEffect(() => {
78
+ if (!active && !focusAfterChange.current)
79
+ return;
80
+ focusAfterChange.current = false;
81
+ toolbar.current?.querySelector(active ? '[aria-pressed="true"]' : "button")?.focus();
82
+ }, [active]);
83
+ function toggle(next) {
84
+ focusAfterChange.current = true;
85
+ onActiveChange(next);
86
+ }
87
+ return _jsx4("div", { ref: toolbar, className: "vx-annotation-tools", role: active ? "toolbar" : void 0, "aria-label": active ? "Annotation tools" : void 0, onKeyDown: (event) => {
88
+ if (event.key === "Escape" && active) {
89
+ event.preventDefault();
90
+ event.stopPropagation();
91
+ toggle(false);
92
+ }
93
+ }, children: active ? _jsxs2(_Fragment, { children: [tools.map(({ id, label }, index) => _jsx4(IconButton, { icon: id, label, "aria-pressed": tool === id, tabIndex: tool === id ? 0 : -1, onClick: () => onToolChange(id), onKeyDown: (event) => {
94
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
95
+ return;
96
+ event.preventDefault();
97
+ const step = event.key === "ArrowLeft" ? -1 : 1;
98
+ const next = (index + step + tools.length) % tools.length;
99
+ toolbar.current?.querySelectorAll("[aria-pressed]")[next]?.focus();
100
+ } }, id)), actions, _jsx4(Divider, { orientation: "vertical" }), _jsx4(IconButton, { icon: "close", label: "Close annotation tools", onClick: () => toggle(false) })] }) : _jsx4(Button, { label: "Magic Edit", leadingIcon: "magic", "aria-keyshortcuts": "C", variant: "pill-filled", onClick: () => toggle(true) }) });
101
+ }
102
+
103
+ // packages/magic-edit/src/activation.ts
104
+ var lensMap;
105
+ function radialLensMap() {
106
+ if (lensMap) return lensMap;
107
+ const size = 128;
108
+ const canvas = document.createElement("canvas");
109
+ canvas.width = canvas.height = size;
110
+ const context = canvas.getContext("2d");
111
+ if (!context) return;
112
+ const pixels = context.createImageData(size, size);
113
+ for (let y = 0; y < size; y++) {
114
+ for (let x = 0; x < size; x++) {
115
+ const dx = 2 * (x + 0.5) / size - 1;
116
+ const dy = 2 * (y + 0.5) / size - 1;
117
+ const distance = Math.hypot(dx, dy);
118
+ const crest = (distance - 0.78) / 0.09;
119
+ const slope = crest * Math.exp((1 - crest * crest) / 2) / distance;
120
+ const index = (y * size + x) * 4;
121
+ pixels.data[index] = 128 + 126 * dx * slope;
122
+ pixels.data[index + 1] = 128 + 126 * dy * slope;
123
+ pixels.data[index + 3] = 255;
124
+ }
125
+ }
126
+ context.putImageData(pixels, 0, 0);
127
+ lensMap = canvas.toDataURL();
128
+ return lensMap;
129
+ }
130
+ function mountLens(layer) {
131
+ if (!CSS.supports("backdrop-filter", 'url("#lens")')) return;
132
+ const map = radialLensMap();
133
+ if (!map) return;
134
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
135
+ const id = `vx-magic-edit-lens-${crypto.randomUUID()}`;
136
+ svg.innerHTML = `<defs><filter id="${id}" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
137
+ <feFlood flood-color="#808080" result="flat" />
138
+ <feImage preserveAspectRatio="none" />
139
+ <feComposite in2="flat" operator="over" result="lens" />
140
+ <feDisplacementMap in="SourceGraphic" in2="lens" scale="17.28" xChannelSelector="R" yChannelSelector="G" />
141
+ <feColorMatrix values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="red" />
142
+ <feDisplacementMap in="SourceGraphic" in2="lens" scale="16" xChannelSelector="R" yChannelSelector="G" />
143
+ <feColorMatrix values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" />
144
+ <feBlend in2="red" mode="screen" result="rg" />
145
+ <feDisplacementMap in="SourceGraphic" in2="lens" scale="14.72" xChannelSelector="R" yChannelSelector="G" />
146
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" />
147
+ <feBlend in2="rg" mode="screen" />
148
+ </filter></defs>`;
149
+ svg.setAttribute("width", "0");
150
+ svg.setAttribute("height", "0");
151
+ const image = svg.querySelector("feImage");
152
+ image.setAttribute("href", map);
153
+ layer.append(svg);
154
+ layer.style.backdropFilter = `url("#${id}")`;
155
+ return image;
156
+ }
157
+ function playMagicEditWave(root, bounds, button) {
158
+ const motion = matchMedia("(prefers-reduced-motion: reduce)");
159
+ const left = Math.max(0, bounds.left);
160
+ const top = Math.max(0, bounds.top);
161
+ const width = Math.min(innerWidth, bounds.right) - left;
162
+ const height = Math.min(innerHeight, bounds.bottom) - top;
163
+ if (motion.matches || width <= 0 || height <= 0 || !root.isConnected) return;
164
+ const x = Math.max(0, Math.min(width, button.left + button.width / 2 - left));
165
+ const y = Math.max(0, Math.min(height, button.top + button.height / 2 - top));
166
+ const reach = Math.hypot(Math.max(x, width - x), Math.max(y, height - y));
167
+ const layer = document.createElement("div");
168
+ layer.className = "magic-edit-wave";
169
+ layer.setAttribute("aria-hidden", "true");
170
+ Object.assign(layer.style, {
171
+ left: `${left}px`,
172
+ top: `${top}px`,
173
+ width: `${width}px`,
174
+ height: `${height}px`
175
+ });
176
+ layer.style.setProperty("--vx-wave-x", `${x}px`);
177
+ layer.style.setProperty("--vx-wave-y", `${y}px`);
178
+ let lens = mountLens(layer);
179
+ root.prepend(layer);
180
+ const playback = new AbortController();
181
+ let frame = 0;
182
+ let previous = performance.now();
183
+ let elapsed = 0;
184
+ let previousFrameWasSlow = false;
185
+ function tick(now) {
186
+ const delta = Math.max(0, now - previous);
187
+ previous = now;
188
+ if (lens && previousFrameWasSlow && delta > 50) {
189
+ layer.style.backdropFilter = "none";
190
+ layer.replaceChildren();
191
+ lens = void 0;
192
+ }
193
+ previousFrameWasSlow = delta > 50;
194
+ elapsed += Math.min(delta, 50);
195
+ const progress = Math.min(1, elapsed / 900);
196
+ if (progress >= 1) {
197
+ playback.abort();
198
+ return;
199
+ }
200
+ const radius = Math.max(1, reach / 0.58 * (1 - (1 - progress) ** 1.7));
201
+ layer.style.setProperty("--vx-wave-radius", `${radius}px`);
202
+ layer.style.setProperty("--vx-wave-opacity", String(1 - progress ** 2));
203
+ if (lens) {
204
+ lens.setAttribute("x", String(x - radius));
205
+ lens.setAttribute("y", String(y - radius));
206
+ lens.setAttribute("width", String(radius * 2));
207
+ lens.setAttribute("height", String(radius * 2));
208
+ }
209
+ frame = requestAnimationFrame(tick);
210
+ }
211
+ function stop() {
212
+ playback.abort();
213
+ }
214
+ playback.signal.addEventListener(
215
+ "abort",
216
+ () => {
217
+ cancelAnimationFrame(frame);
218
+ layer.remove();
219
+ },
220
+ { once: true }
221
+ );
222
+ motion.addEventListener("change", stop, { signal: playback.signal });
223
+ window.addEventListener("resize", stop, { signal: playback.signal });
224
+ tick(previous);
225
+ return playback;
226
+ }
227
+
228
+ // packages/magic-edit/src/annotation.tsx
229
+ import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState } from "react";
230
+
231
+ // packages/magic-edit/src/shared.ts
232
+ var maxRegions = 12;
233
+ var maxStrokePoints = 256;
234
+ function formatSourceTime(milliseconds) {
235
+ const totalSeconds = Math.floor(milliseconds / 1e3);
236
+ const minutes = Math.floor(totalSeconds / 60);
237
+ const seconds = totalSeconds % 60;
238
+ const fraction = milliseconds % 1e3;
239
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${String(fraction).padStart(3, "0")}`;
240
+ }
241
+ function formatTargetTime({ sourceTimeMs, endSourceTimeMs }) {
242
+ if (sourceTimeMs === void 0) return void 0;
243
+ const start = formatSourceTime(sourceTimeMs);
244
+ if (endSourceTimeMs === void 0 || endSourceTimeMs === sourceTimeMs) return start;
245
+ return `${start}\u2013${formatSourceTime(endSourceTimeMs)}`;
246
+ }
247
+ function handoffText(record) {
248
+ const { target } = record;
249
+ const time = formatTargetTime(target);
250
+ const regions = record.regions.map((region, index) => {
251
+ const name = region.kind === "frame" ? "whole image" : region.kind;
252
+ return `${String(index + 1).padStart(2, "0")}: ${name}`;
253
+ });
254
+ return [
255
+ `Magic Edit \xB7 ${target.label}`,
256
+ record.projectId ? `Project: ${record.projectId}` : "Workspace: Studio Home",
257
+ ...Object.entries(target.context ?? {}).map(([key, value]) => `${key}: ${value}`),
258
+ target.revision !== void 0 && `Revision: ${target.revision}`,
259
+ target.visibleText && `Visible text: ${target.visibleText}`,
260
+ target.ref && `Asset version: ${target.ref.versionId} (${target.ref.storeId}/${target.ref.objectId})`,
261
+ time && `Source time: ${time}`,
262
+ `Captured image: ${record.frame.width} \xD7 ${record.frame.height}`,
263
+ `Marked regions: ${regions.join(", ")}. See the matching PNG.`,
264
+ `
265
+ ${record.instruction}`
266
+ ].filter((line) => Boolean(line)).join("\n");
267
+ }
268
+
269
+ // packages/magic-edit/src/viewport.ts
270
+ function embeddedDocument(frame) {
271
+ try {
272
+ if (!frame?.isConnected || !frame.contentWindow) return;
273
+ const expected = new URL(frame.src);
274
+ const current = new URL(frame.contentWindow.location.href);
275
+ if (current.origin !== window.location.origin || current.origin !== expected.origin) return;
276
+ const project = expected.searchParams.get("project");
277
+ if (current.pathname === expected.pathname && (project === null || current.searchParams.get("project") === project))
278
+ return frame.contentDocument ?? void 0;
279
+ } catch {
280
+ }
281
+ }
282
+ function elementNode(target) {
283
+ return target && target.nodeType === Node.ELEMENT_NODE ? target : void 0;
284
+ }
285
+ function clippedBounds(element, local) {
286
+ const view = element.ownerDocument.defaultView;
287
+ let left = Math.max(0, local.left);
288
+ let top = Math.max(0, local.top);
289
+ let right = Math.min(view?.innerWidth ?? 0, local.right);
290
+ let bottom = Math.min(view?.innerHeight ?? 0, local.bottom);
291
+ for (let parent = element.parentElement; parent && view; parent = parent.parentElement) {
292
+ const { overflowX, overflowY } = view.getComputedStyle(parent);
293
+ if (overflowX === "visible" && overflowY === "visible") continue;
294
+ const rect = parent.getBoundingClientRect();
295
+ if (overflowX !== "visible") {
296
+ left = Math.max(left, rect.left);
297
+ right = Math.min(right, rect.right);
298
+ }
299
+ if (overflowY !== "visible") {
300
+ top = Math.max(top, rect.top);
301
+ bottom = Math.min(bottom, rect.bottom);
302
+ }
303
+ }
304
+ if (right - left >= 1 && bottom - top >= 1) {
305
+ return new DOMRect(left, top, right - left, bottom - top);
306
+ }
307
+ }
308
+ function screenBounds(element, local, frame, clip = false) {
309
+ if (!element.isConnected || local.width < 1 || local.height < 1) return;
310
+ const inHome = element.ownerDocument === document;
311
+ if (inHome && !clip) return local;
312
+ const sourceFrame = inHome ? void 0 : frame;
313
+ if (!inHome && (!sourceFrame || embeddedDocument(sourceFrame) !== element.ownerDocument)) return;
314
+ const bounds = clip ? clippedBounds(element, local) : local;
315
+ if (!bounds || !sourceFrame) return bounds;
316
+ if (!sourceFrame.offsetWidth || !sourceFrame.offsetHeight) return;
317
+ const outer = sourceFrame.getBoundingClientRect();
318
+ const scaleX = outer.width / sourceFrame.offsetWidth;
319
+ const scaleY = outer.height / sourceFrame.offsetHeight;
320
+ const projected = new DOMRect(
321
+ outer.left + (sourceFrame.clientLeft + bounds.left) * scaleX,
322
+ outer.top + (sourceFrame.clientTop + bounds.top) * scaleY,
323
+ bounds.width * scaleX,
324
+ bounds.height * scaleY
325
+ );
326
+ return clip ? clippedBounds(sourceFrame, projected) : projected;
327
+ }
328
+
329
+ // packages/magic-edit/src/annotation.tsx
330
+ import { jsx, jsxs } from "react/jsx-runtime";
331
+ function pointInBounds(event, bounds) {
332
+ const clamp = (value) => Math.max(0, Math.min(1, value));
333
+ return {
334
+ x: clamp((event.clientX - bounds.left) / bounds.width),
335
+ y: clamp((event.clientY - bounds.top) / bounds.height)
336
+ };
337
+ }
338
+ function startRegion(tool, point) {
339
+ return tool === "area" ? { kind: "rectangle", start: point, end: point } : { kind: "pen", points: [point] };
340
+ }
341
+ function extendRegion(region, point) {
342
+ if (region.kind === "rectangle") return { ...region, end: point };
343
+ if (region.kind === "frame") return region;
344
+ const last = region.points.at(-1);
345
+ if (last && Math.hypot(point.x - last.x, point.y - last.y) < 2e-3) return region;
346
+ const points = region.points.length < maxStrokePoints ? region.points : region.points.slice(0, -1);
347
+ return { ...region, points: [...points, point] };
348
+ }
349
+ function validRegion(region) {
350
+ return region.kind !== "rectangle" || Math.abs(region.start.x - region.end.x) >= 5e-3 && Math.abs(region.start.y - region.end.y) >= 5e-3;
351
+ }
352
+ function paintRegions(canvas, image, regions, showImage = true) {
353
+ const context = canvas.getContext("2d");
354
+ if (!context) throw new Error("The browser could not draw the marked image.");
355
+ const { width, height } = canvas;
356
+ context.clearRect(0, 0, width, height);
357
+ if (showImage) context.drawImage(image, 0, 0, width, height);
358
+ const line = Math.max(2, Math.round(Math.max(width, height) / 230));
359
+ const badge = Math.min(width / 3, height / 3, Math.max(20, line * 6));
360
+ context.strokeStyle = showImage ? "#c3ff75" : "#2ba86a";
361
+ context.lineWidth = line;
362
+ context.lineJoin = "round";
363
+ context.lineCap = "round";
364
+ context.font = `bold ${Math.max(8, badge * 0.55)}px system-ui, sans-serif`;
365
+ context.textAlign = "center";
366
+ context.textBaseline = "middle";
367
+ regions.forEach((region, index) => {
368
+ let x = line;
369
+ let y = line;
370
+ if (region.kind === "frame") {
371
+ if (!showImage) {
372
+ context.fillStyle = "rgb(43 168 106 / 20%)";
373
+ context.fillRect(0, 0, width, height);
374
+ }
375
+ context.strokeRect(line, line, Math.max(1, width - line * 2), Math.max(1, height - line * 2));
376
+ } else if (region.kind === "rectangle") {
377
+ x = Math.min(region.start.x, region.end.x) * width;
378
+ y = Math.min(region.start.y, region.end.y) * height;
379
+ context.strokeRect(
380
+ x,
381
+ y,
382
+ Math.abs(region.start.x - region.end.x) * width,
383
+ Math.abs(region.start.y - region.end.y) * height
384
+ );
385
+ } else {
386
+ const first = region.points[0];
387
+ if (!first) return;
388
+ x = first.x * width;
389
+ y = first.y * height;
390
+ context.beginPath();
391
+ context.moveTo(x, y);
392
+ if (region.points.length === 1) context.lineTo(x + 0.01, y);
393
+ else
394
+ region.points.slice(1).forEach((point) => context.lineTo(point.x * width, point.y * height));
395
+ context.stroke();
396
+ }
397
+ const badgeX = Math.min(width - badge, Math.max(0, x));
398
+ const badgeY = Math.min(height - badge, Math.max(0, y - badge));
399
+ context.fillStyle = "#c3ff75";
400
+ context.fillRect(badgeX, badgeY, badge, badge);
401
+ context.fillStyle = "#17210f";
402
+ context.fillText(String(index + 1).padStart(2, "0"), badgeX + badge / 2, badgeY + badge / 2);
403
+ });
404
+ }
405
+ async function markedPng(image, regions) {
406
+ const canvas = document.createElement("canvas");
407
+ canvas.width = image.width;
408
+ canvas.height = image.height;
409
+ paintRegions(canvas, image, regions);
410
+ return new Promise((resolve, reject) => {
411
+ canvas.toBlob(
412
+ (blob) => blob ? resolve(blob) : reject(new Error("The browser could not create the marked PNG.")),
413
+ "image/png"
414
+ );
415
+ });
416
+ }
417
+ function pictureBounds(element, width, height) {
418
+ const bounds = element.getBoundingClientRect();
419
+ if (!element.isConnected || bounds.width < 1 || bounds.height < 1) return;
420
+ const fit = element.ownerDocument.defaultView?.getComputedStyle(element).objectFit;
421
+ if (fit === "fill") return bounds;
422
+ if (fit !== "contain" || width < 1 || height < 1) return;
423
+ const scale = Math.min(bounds.width / width, bounds.height / height);
424
+ return new DOMRect(
425
+ bounds.x + (bounds.width - width * scale) / 2,
426
+ bounds.y + (bounds.height - height * scale) / 2,
427
+ width * scale,
428
+ height * scale
429
+ );
430
+ }
431
+ function frameBounds({ element, target, image }, frame) {
432
+ const bounds = target.kind === "media" ? pictureBounds(element, image.width, image.height) : element.getBoundingClientRect();
433
+ if (!bounds) return;
434
+ const full = screenBounds(element, bounds, frame);
435
+ const visible = screenBounds(element, bounds, frame, true);
436
+ if (full && visible) return { full, visible };
437
+ }
438
+ function useCaptureBounds(capture, frame) {
439
+ const [measurement, setMeasurement] = useState();
440
+ useLayoutEffect(() => {
441
+ if (!capture) return;
442
+ const owner = capture.element.ownerDocument;
443
+ const update = () => setMeasurement({ capture, bounds: frameBounds(capture, frame) });
444
+ update();
445
+ const observer = new ResizeObserver(update);
446
+ observer.observe(capture.element);
447
+ if (frame) observer.observe(frame);
448
+ document.addEventListener("scroll", update, true);
449
+ if (owner !== document) owner.addEventListener("scroll", update, true);
450
+ window.addEventListener("resize", update);
451
+ return () => {
452
+ observer.disconnect();
453
+ document.removeEventListener("scroll", update, true);
454
+ if (owner !== document) owner.removeEventListener("scroll", update, true);
455
+ window.removeEventListener("resize", update);
456
+ };
457
+ }, [capture, frame]);
458
+ if (measurement?.capture === capture) return measurement?.bounds;
459
+ }
460
+ function AnnotationEditor({
461
+ capture,
462
+ regions,
463
+ onChange,
464
+ tool,
465
+ bounds,
466
+ pagePreview
467
+ }) {
468
+ const { image } = capture;
469
+ const [preview, setPreview] = useState();
470
+ const drag = useRef2(void 0);
471
+ const canvas = useRef2(null);
472
+ const onTarget = Boolean(bounds);
473
+ const activeMark = preview ?? pagePreview;
474
+ const full = bounds?.full;
475
+ const visible = bounds?.visible;
476
+ useEffect2(() => {
477
+ if (canvas.current)
478
+ paintRegions(
479
+ canvas.current,
480
+ image,
481
+ activeMark ? [...regions, activeMark] : regions,
482
+ !onTarget
483
+ );
484
+ }, [image, activeMark, regions, onTarget]);
485
+ function endDrag() {
486
+ drag.current = void 0;
487
+ setPreview(void 0);
488
+ }
489
+ const canAdd = regions.length < maxRegions;
490
+ const canMarkFrame = canAdd && !regions.some(({ kind }) => kind === "frame");
491
+ function addFrame() {
492
+ if (canMarkFrame) onChange([...regions, { kind: "frame" }]);
493
+ }
494
+ return /* @__PURE__ */ jsxs("div", { className: "magic-edit-annotation", children: [
495
+ /* @__PURE__ */ jsx(
496
+ "canvas",
497
+ {
498
+ ref: canvas,
499
+ width: image.width,
500
+ height: image.height,
501
+ "aria-label": "Numbered annotations",
502
+ "data-tool": tool,
503
+ "data-on-target": bounds ? "true" : void 0,
504
+ style: full && visible && {
505
+ left: full.left,
506
+ top: full.top,
507
+ width: full.width,
508
+ height: full.height,
509
+ clipPath: `inset(${visible.top - full.top}px ${full.right - visible.right}px ${full.bottom - visible.bottom}px ${visible.left - full.left}px)`
510
+ },
511
+ onPointerDown: (event) => {
512
+ if (!canAdd || !event.isPrimary || event.button !== 0 || drag.current) return;
513
+ if (tool === "select") {
514
+ addFrame();
515
+ return;
516
+ }
517
+ const point = pointInBounds(event, event.currentTarget.getBoundingClientRect());
518
+ const region = startRegion(tool, point);
519
+ event.currentTarget.setPointerCapture(event.pointerId);
520
+ drag.current = { pointerId: event.pointerId, region };
521
+ setPreview(region);
522
+ },
523
+ onPointerMove: (event) => {
524
+ const pending = drag.current;
525
+ if (pending?.pointerId !== event.pointerId) return;
526
+ pending.region = extendRegion(
527
+ pending.region,
528
+ pointInBounds(event, event.currentTarget.getBoundingClientRect())
529
+ );
530
+ setPreview(pending.region);
531
+ },
532
+ onPointerUp: (event) => {
533
+ const pending = drag.current;
534
+ if (pending?.pointerId !== event.pointerId) return;
535
+ const region = extendRegion(
536
+ pending.region,
537
+ pointInBounds(event, event.currentTarget.getBoundingClientRect())
538
+ );
539
+ endDrag();
540
+ if (canAdd && validRegion(region)) onChange([...regions, region]);
541
+ },
542
+ onPointerCancel: endDrag,
543
+ onLostPointerCapture: endDrag
544
+ }
545
+ ),
546
+ /* @__PURE__ */ jsxs("div", { className: "magic-edit-actions", children: [
547
+ /* @__PURE__ */ jsx(
548
+ Button,
549
+ {
550
+ label: "Mark whole target",
551
+ variant: "text-tonal-sm",
552
+ width: "hug",
553
+ onClick: addFrame,
554
+ disabled: !canMarkFrame
555
+ }
556
+ ),
557
+ /* @__PURE__ */ jsx(
558
+ Button,
559
+ {
560
+ label: "Undo mark",
561
+ variant: "text-ghost-sm",
562
+ width: "hug",
563
+ onClick: () => onChange(regions.slice(0, -1)),
564
+ disabled: regions.length === 0
565
+ }
566
+ ),
567
+ /* @__PURE__ */ jsxs("span", { "aria-live": "polite", children: [
568
+ regions.length === 1 ? "1 mark" : `${regions.length} marks`,
569
+ !canAdd && " (limit reached)"
570
+ ] })
571
+ ] })
572
+ ] });
573
+ }
574
+
575
+ // packages/magic-edit/src/bridge.ts
576
+ var sourceRequest = "vivix:magic-edit-source";
577
+ function readMagicEditSource(owner) {
578
+ let source;
579
+ const detail = (value) => {
580
+ source = value;
581
+ };
582
+ owner.dispatchEvent(new CustomEvent(sourceRequest, { detail }));
583
+ return source;
584
+ }
585
+
586
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/util.js
587
+ function resolveUrl(url, baseUrl) {
588
+ if (url.match(/^[a-z]+:\/\//i)) {
589
+ return url;
590
+ }
591
+ if (url.match(/^\/\//)) {
592
+ return window.location.protocol + url;
593
+ }
594
+ if (url.match(/^[a-z]+:/i)) {
595
+ return url;
596
+ }
597
+ const doc = document.implementation.createHTMLDocument();
598
+ const base = doc.createElement("base");
599
+ const a = doc.createElement("a");
600
+ doc.head.appendChild(base);
601
+ doc.body.appendChild(a);
602
+ if (baseUrl) {
603
+ base.href = baseUrl;
604
+ }
605
+ a.href = url;
606
+ return a.href;
607
+ }
608
+ var uuid = /* @__PURE__ */ (() => {
609
+ let counter = 0;
610
+ const random = () => (
611
+ // eslint-disable-next-line no-bitwise
612
+ `0000${(Math.random() * 36 ** 4 << 0).toString(36)}`.slice(-4)
613
+ );
614
+ return () => {
615
+ counter += 1;
616
+ return `u${random()}${counter}`;
617
+ };
618
+ })();
619
+ function toArray(arrayLike) {
620
+ const arr = [];
621
+ for (let i = 0, l = arrayLike.length; i < l; i++) {
622
+ arr.push(arrayLike[i]);
623
+ }
624
+ return arr;
625
+ }
626
+ var styleProps = null;
627
+ function getStyleProperties(options = {}) {
628
+ if (styleProps) {
629
+ return styleProps;
630
+ }
631
+ if (options.includeStyleProperties) {
632
+ styleProps = options.includeStyleProperties;
633
+ return styleProps;
634
+ }
635
+ styleProps = toArray(window.getComputedStyle(document.documentElement));
636
+ return styleProps;
637
+ }
638
+ function px(node, styleProperty) {
639
+ const win = node.ownerDocument.defaultView || window;
640
+ const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
641
+ return val ? parseFloat(val.replace("px", "")) : 0;
642
+ }
643
+ function getNodeWidth(node) {
644
+ const leftBorder = px(node, "border-left-width");
645
+ const rightBorder = px(node, "border-right-width");
646
+ return node.clientWidth + leftBorder + rightBorder;
647
+ }
648
+ function getNodeHeight(node) {
649
+ const topBorder = px(node, "border-top-width");
650
+ const bottomBorder = px(node, "border-bottom-width");
651
+ return node.clientHeight + topBorder + bottomBorder;
652
+ }
653
+ function getImageSize(targetNode, options = {}) {
654
+ const width = options.width || getNodeWidth(targetNode);
655
+ const height = options.height || getNodeHeight(targetNode);
656
+ return { width, height };
657
+ }
658
+ function getPixelRatio() {
659
+ let ratio;
660
+ let FINAL_PROCESS;
661
+ try {
662
+ FINAL_PROCESS = process;
663
+ } catch (e) {
664
+ }
665
+ const val = FINAL_PROCESS && FINAL_PROCESS.env ? FINAL_PROCESS.env.devicePixelRatio : null;
666
+ if (val) {
667
+ ratio = parseInt(val, 10);
668
+ if (Number.isNaN(ratio)) {
669
+ ratio = 1;
670
+ }
671
+ }
672
+ return ratio || window.devicePixelRatio || 1;
673
+ }
674
+ var canvasDimensionLimit = 16384;
675
+ function checkCanvasDimensions(canvas) {
676
+ if (canvas.width > canvasDimensionLimit || canvas.height > canvasDimensionLimit) {
677
+ if (canvas.width > canvasDimensionLimit && canvas.height > canvasDimensionLimit) {
678
+ if (canvas.width > canvas.height) {
679
+ canvas.height *= canvasDimensionLimit / canvas.width;
680
+ canvas.width = canvasDimensionLimit;
681
+ } else {
682
+ canvas.width *= canvasDimensionLimit / canvas.height;
683
+ canvas.height = canvasDimensionLimit;
684
+ }
685
+ } else if (canvas.width > canvasDimensionLimit) {
686
+ canvas.height *= canvasDimensionLimit / canvas.width;
687
+ canvas.width = canvasDimensionLimit;
688
+ } else {
689
+ canvas.width *= canvasDimensionLimit / canvas.height;
690
+ canvas.height = canvasDimensionLimit;
691
+ }
692
+ }
693
+ }
694
+ function createImage(url) {
695
+ return new Promise((resolve, reject) => {
696
+ const img = new Image();
697
+ img.onload = () => {
698
+ img.decode().then(() => {
699
+ requestAnimationFrame(() => resolve(img));
700
+ });
701
+ };
702
+ img.onerror = reject;
703
+ img.crossOrigin = "anonymous";
704
+ img.decoding = "async";
705
+ img.src = url;
706
+ });
707
+ }
708
+ async function svgToDataURL(svg) {
709
+ return Promise.resolve().then(() => new XMLSerializer().serializeToString(svg)).then(encodeURIComponent).then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
710
+ }
711
+ async function nodeToDataURL(node, width, height) {
712
+ const xmlns = "http://www.w3.org/2000/svg";
713
+ const svg = document.createElementNS(xmlns, "svg");
714
+ const foreignObject = document.createElementNS(xmlns, "foreignObject");
715
+ svg.setAttribute("width", `${width}`);
716
+ svg.setAttribute("height", `${height}`);
717
+ svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
718
+ foreignObject.setAttribute("width", "100%");
719
+ foreignObject.setAttribute("height", "100%");
720
+ foreignObject.setAttribute("x", "0");
721
+ foreignObject.setAttribute("y", "0");
722
+ foreignObject.setAttribute("externalResourcesRequired", "true");
723
+ svg.appendChild(foreignObject);
724
+ foreignObject.appendChild(node);
725
+ return svgToDataURL(svg);
726
+ }
727
+ var isInstanceOfElement = (node, instance) => {
728
+ if (node instanceof instance)
729
+ return true;
730
+ const nodePrototype = Object.getPrototypeOf(node);
731
+ if (nodePrototype === null)
732
+ return false;
733
+ return nodePrototype.constructor.name === instance.name || isInstanceOfElement(nodePrototype, instance);
734
+ };
735
+
736
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/clone-pseudos.js
737
+ function formatCSSText(style) {
738
+ const content = style.getPropertyValue("content");
739
+ return `${style.cssText} content: '${content.replace(/'|"/g, "")}';`;
740
+ }
741
+ function formatCSSProperties(style, options) {
742
+ return getStyleProperties(options).map((name) => {
743
+ const value = style.getPropertyValue(name);
744
+ const priority = style.getPropertyPriority(name);
745
+ return `${name}: ${value}${priority ? " !important" : ""};`;
746
+ }).join(" ");
747
+ }
748
+ function getPseudoElementStyle(className, pseudo, style, options) {
749
+ const selector = `.${className}:${pseudo}`;
750
+ const cssText = style.cssText ? formatCSSText(style) : formatCSSProperties(style, options);
751
+ return document.createTextNode(`${selector}{${cssText}}`);
752
+ }
753
+ function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
754
+ const style = window.getComputedStyle(nativeNode, pseudo);
755
+ const content = style.getPropertyValue("content");
756
+ if (content === "" || content === "none") {
757
+ return;
758
+ }
759
+ const className = uuid();
760
+ try {
761
+ clonedNode.className = `${clonedNode.className} ${className}`;
762
+ } catch (err) {
763
+ return;
764
+ }
765
+ const styleElement = document.createElement("style");
766
+ styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
767
+ clonedNode.appendChild(styleElement);
768
+ }
769
+ function clonePseudoElements(nativeNode, clonedNode, options) {
770
+ clonePseudoElement(nativeNode, clonedNode, ":before", options);
771
+ clonePseudoElement(nativeNode, clonedNode, ":after", options);
772
+ }
773
+
774
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/mimes.js
775
+ var WOFF = "application/font-woff";
776
+ var JPEG = "image/jpeg";
777
+ var mimes = {
778
+ woff: WOFF,
779
+ woff2: WOFF,
780
+ ttf: "application/font-truetype",
781
+ eot: "application/vnd.ms-fontobject",
782
+ png: "image/png",
783
+ jpg: JPEG,
784
+ jpeg: JPEG,
785
+ gif: "image/gif",
786
+ tiff: "image/tiff",
787
+ svg: "image/svg+xml",
788
+ webp: "image/webp"
789
+ };
790
+ function getExtension(url) {
791
+ const match = /\.([^./]*?)$/g.exec(url);
792
+ return match ? match[1] : "";
793
+ }
794
+ function getMimeType(url) {
795
+ const extension = getExtension(url).toLowerCase();
796
+ return mimes[extension] || "";
797
+ }
798
+
799
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/dataurl.js
800
+ function getContentFromDataUrl(dataURL) {
801
+ return dataURL.split(/,/)[1];
802
+ }
803
+ function isDataUrl(url) {
804
+ return url.search(/^(data:)/) !== -1;
805
+ }
806
+ function makeDataUrl(content, mimeType) {
807
+ return `data:${mimeType};base64,${content}`;
808
+ }
809
+ async function fetchAsDataURL(url, init, process2) {
810
+ const res = await fetch(url, init);
811
+ if (res.status === 404) {
812
+ throw new Error(`Resource "${res.url}" not found`);
813
+ }
814
+ const blob = await res.blob();
815
+ return new Promise((resolve, reject) => {
816
+ const reader = new FileReader();
817
+ reader.onerror = reject;
818
+ reader.onloadend = () => {
819
+ try {
820
+ resolve(process2({ res, result: reader.result }));
821
+ } catch (error) {
822
+ reject(error);
823
+ }
824
+ };
825
+ reader.readAsDataURL(blob);
826
+ });
827
+ }
828
+ var cache = {};
829
+ function getCacheKey(url, contentType, includeQueryParams) {
830
+ let key = url.replace(/\?.*/, "");
831
+ if (includeQueryParams) {
832
+ key = url;
833
+ }
834
+ if (/ttf|otf|eot|woff2?/i.test(key)) {
835
+ key = key.replace(/.*\//, "");
836
+ }
837
+ return contentType ? `[${contentType}]${key}` : key;
838
+ }
839
+ async function resourceToDataURL(resourceUrl, contentType, options) {
840
+ const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
841
+ if (cache[cacheKey] != null) {
842
+ return cache[cacheKey];
843
+ }
844
+ if (options.cacheBust) {
845
+ resourceUrl += (/\?/.test(resourceUrl) ? "&" : "?") + (/* @__PURE__ */ new Date()).getTime();
846
+ }
847
+ let dataURL;
848
+ try {
849
+ const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
850
+ if (!contentType) {
851
+ contentType = res.headers.get("Content-Type") || "";
852
+ }
853
+ return getContentFromDataUrl(result);
854
+ });
855
+ dataURL = makeDataUrl(content, contentType);
856
+ } catch (error) {
857
+ dataURL = options.imagePlaceholder || "";
858
+ let msg = `Failed to fetch resource: ${resourceUrl}`;
859
+ if (error) {
860
+ msg = typeof error === "string" ? error : error.message;
861
+ }
862
+ if (msg) {
863
+ console.warn(msg);
864
+ }
865
+ }
866
+ cache[cacheKey] = dataURL;
867
+ return dataURL;
868
+ }
869
+
870
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/clone-node.js
871
+ async function cloneCanvasElement(canvas) {
872
+ const dataURL = canvas.toDataURL();
873
+ if (dataURL === "data:,") {
874
+ return canvas.cloneNode(false);
875
+ }
876
+ return createImage(dataURL);
877
+ }
878
+ async function cloneVideoElement(video, options) {
879
+ if (video.currentSrc) {
880
+ const canvas = document.createElement("canvas");
881
+ const ctx = canvas.getContext("2d");
882
+ canvas.width = video.clientWidth;
883
+ canvas.height = video.clientHeight;
884
+ ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
885
+ const dataURL2 = canvas.toDataURL();
886
+ return createImage(dataURL2);
887
+ }
888
+ const poster = video.poster;
889
+ const contentType = getMimeType(poster);
890
+ const dataURL = await resourceToDataURL(poster, contentType, options);
891
+ return createImage(dataURL);
892
+ }
893
+ async function cloneIFrameElement(iframe, options) {
894
+ var _a;
895
+ try {
896
+ if ((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body) {
897
+ return await cloneNode(iframe.contentDocument.body, options, true);
898
+ }
899
+ } catch (_b) {
900
+ }
901
+ return iframe.cloneNode(false);
902
+ }
903
+ async function cloneSingleNode(node, options) {
904
+ if (isInstanceOfElement(node, HTMLCanvasElement)) {
905
+ return cloneCanvasElement(node);
906
+ }
907
+ if (isInstanceOfElement(node, HTMLVideoElement)) {
908
+ return cloneVideoElement(node, options);
909
+ }
910
+ if (isInstanceOfElement(node, HTMLIFrameElement)) {
911
+ return cloneIFrameElement(node, options);
912
+ }
913
+ return node.cloneNode(isSVGElement(node));
914
+ }
915
+ var isSlotElement = (node) => node.tagName != null && node.tagName.toUpperCase() === "SLOT";
916
+ var isSVGElement = (node) => node.tagName != null && node.tagName.toUpperCase() === "SVG";
917
+ async function cloneChildren(nativeNode, clonedNode, options) {
918
+ var _a, _b;
919
+ if (isSVGElement(clonedNode)) {
920
+ return clonedNode;
921
+ }
922
+ let children = [];
923
+ if (isSlotElement(nativeNode) && nativeNode.assignedNodes) {
924
+ children = toArray(nativeNode.assignedNodes());
925
+ } else if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && ((_a = nativeNode.contentDocument) === null || _a === void 0 ? void 0 : _a.body)) {
926
+ children = toArray(nativeNode.contentDocument.body.childNodes);
927
+ } else {
928
+ children = toArray(((_b = nativeNode.shadowRoot) !== null && _b !== void 0 ? _b : nativeNode).childNodes);
929
+ }
930
+ if (children.length === 0 || isInstanceOfElement(nativeNode, HTMLVideoElement)) {
931
+ return clonedNode;
932
+ }
933
+ await children.reduce((deferred, child) => deferred.then(() => cloneNode(child, options)).then((clonedChild) => {
934
+ if (clonedChild) {
935
+ clonedNode.appendChild(clonedChild);
936
+ }
937
+ }), Promise.resolve());
938
+ return clonedNode;
939
+ }
940
+ function cloneCSSStyle(nativeNode, clonedNode, options) {
941
+ const targetStyle = clonedNode.style;
942
+ if (!targetStyle) {
943
+ return;
944
+ }
945
+ const sourceStyle = window.getComputedStyle(nativeNode);
946
+ if (sourceStyle.cssText) {
947
+ targetStyle.cssText = sourceStyle.cssText;
948
+ targetStyle.transformOrigin = sourceStyle.transformOrigin;
949
+ } else {
950
+ getStyleProperties(options).forEach((name) => {
951
+ let value = sourceStyle.getPropertyValue(name);
952
+ if (name === "font-size" && value.endsWith("px")) {
953
+ const reducedFont = Math.floor(parseFloat(value.substring(0, value.length - 2))) - 0.1;
954
+ value = `${reducedFont}px`;
955
+ }
956
+ if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && name === "display" && value === "inline") {
957
+ value = "block";
958
+ }
959
+ if (name === "d" && clonedNode.getAttribute("d")) {
960
+ value = `path(${clonedNode.getAttribute("d")})`;
961
+ }
962
+ targetStyle.setProperty(name, value, sourceStyle.getPropertyPriority(name));
963
+ });
964
+ }
965
+ }
966
+ function cloneInputValue(nativeNode, clonedNode) {
967
+ if (isInstanceOfElement(nativeNode, HTMLTextAreaElement)) {
968
+ clonedNode.innerHTML = nativeNode.value;
969
+ }
970
+ if (isInstanceOfElement(nativeNode, HTMLInputElement)) {
971
+ clonedNode.setAttribute("value", nativeNode.value);
972
+ }
973
+ }
974
+ function cloneSelectValue(nativeNode, clonedNode) {
975
+ if (isInstanceOfElement(nativeNode, HTMLSelectElement)) {
976
+ const clonedSelect = clonedNode;
977
+ const selectedOption = Array.from(clonedSelect.children).find((child) => nativeNode.value === child.getAttribute("value"));
978
+ if (selectedOption) {
979
+ selectedOption.setAttribute("selected", "");
980
+ }
981
+ }
982
+ }
983
+ function decorate(nativeNode, clonedNode, options) {
984
+ if (isInstanceOfElement(clonedNode, Element)) {
985
+ cloneCSSStyle(nativeNode, clonedNode, options);
986
+ clonePseudoElements(nativeNode, clonedNode, options);
987
+ cloneInputValue(nativeNode, clonedNode);
988
+ cloneSelectValue(nativeNode, clonedNode);
989
+ }
990
+ return clonedNode;
991
+ }
992
+ async function ensureSVGSymbols(clone, options) {
993
+ const uses = clone.querySelectorAll ? clone.querySelectorAll("use") : [];
994
+ if (uses.length === 0) {
995
+ return clone;
996
+ }
997
+ const processedDefs = {};
998
+ for (let i = 0; i < uses.length; i++) {
999
+ const use = uses[i];
1000
+ const id = use.getAttribute("xlink:href");
1001
+ if (id) {
1002
+ const exist = clone.querySelector(id);
1003
+ const definition = document.querySelector(id);
1004
+ if (!exist && definition && !processedDefs[id]) {
1005
+ processedDefs[id] = await cloneNode(definition, options, true);
1006
+ }
1007
+ }
1008
+ }
1009
+ const nodes = Object.values(processedDefs);
1010
+ if (nodes.length) {
1011
+ const ns = "http://www.w3.org/1999/xhtml";
1012
+ const svg = document.createElementNS(ns, "svg");
1013
+ svg.setAttribute("xmlns", ns);
1014
+ svg.style.position = "absolute";
1015
+ svg.style.width = "0";
1016
+ svg.style.height = "0";
1017
+ svg.style.overflow = "hidden";
1018
+ svg.style.display = "none";
1019
+ const defs = document.createElementNS(ns, "defs");
1020
+ svg.appendChild(defs);
1021
+ for (let i = 0; i < nodes.length; i++) {
1022
+ defs.appendChild(nodes[i]);
1023
+ }
1024
+ clone.appendChild(svg);
1025
+ }
1026
+ return clone;
1027
+ }
1028
+ async function cloneNode(node, options, isRoot) {
1029
+ if (!isRoot && options.filter && !options.filter(node)) {
1030
+ return null;
1031
+ }
1032
+ return Promise.resolve(node).then((clonedNode) => cloneSingleNode(clonedNode, options)).then((clonedNode) => cloneChildren(node, clonedNode, options)).then((clonedNode) => decorate(node, clonedNode, options)).then((clonedNode) => ensureSVGSymbols(clonedNode, options));
1033
+ }
1034
+
1035
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/embed-resources.js
1036
+ var URL_REGEX = /url\((['"]?)([^'"]+?)\1\)/g;
1037
+ var URL_WITH_FORMAT_REGEX = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g;
1038
+ var FONT_SRC_REGEX = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;
1039
+ function toRegex(url) {
1040
+ const escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1");
1041
+ return new RegExp(`(url\\(['"]?)(${escaped})(['"]?\\))`, "g");
1042
+ }
1043
+ function parseURLs(cssText) {
1044
+ const urls = [];
1045
+ cssText.replace(URL_REGEX, (raw, quotation, url) => {
1046
+ urls.push(url);
1047
+ return raw;
1048
+ });
1049
+ return urls.filter((url) => !isDataUrl(url));
1050
+ }
1051
+ async function embed(cssText, resourceURL, baseURL, options, getContentFromUrl) {
1052
+ try {
1053
+ const resolvedURL = baseURL ? resolveUrl(resourceURL, baseURL) : resourceURL;
1054
+ const contentType = getMimeType(resourceURL);
1055
+ let dataURL;
1056
+ if (getContentFromUrl) {
1057
+ const content = await getContentFromUrl(resolvedURL);
1058
+ dataURL = makeDataUrl(content, contentType);
1059
+ } else {
1060
+ dataURL = await resourceToDataURL(resolvedURL, contentType, options);
1061
+ }
1062
+ return cssText.replace(toRegex(resourceURL), `$1${dataURL}$3`);
1063
+ } catch (error) {
1064
+ }
1065
+ return cssText;
1066
+ }
1067
+ function filterPreferredFontFormat(str, { preferredFontFormat }) {
1068
+ return !preferredFontFormat ? str : str.replace(FONT_SRC_REGEX, (match) => {
1069
+ while (true) {
1070
+ const [src, , format] = URL_WITH_FORMAT_REGEX.exec(match) || [];
1071
+ if (!format) {
1072
+ return "";
1073
+ }
1074
+ if (format === preferredFontFormat) {
1075
+ return `src: ${src};`;
1076
+ }
1077
+ }
1078
+ });
1079
+ }
1080
+ function shouldEmbed(url) {
1081
+ return url.search(URL_REGEX) !== -1;
1082
+ }
1083
+ async function embedResources(cssText, baseUrl, options) {
1084
+ if (!shouldEmbed(cssText)) {
1085
+ return cssText;
1086
+ }
1087
+ const filteredCSSText = filterPreferredFontFormat(cssText, options);
1088
+ const urls = parseURLs(filteredCSSText);
1089
+ return urls.reduce((deferred, url) => deferred.then((css) => embed(css, url, baseUrl, options)), Promise.resolve(filteredCSSText));
1090
+ }
1091
+
1092
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/embed-images.js
1093
+ async function embedProp(propName, node, options) {
1094
+ var _a;
1095
+ const propValue = (_a = node.style) === null || _a === void 0 ? void 0 : _a.getPropertyValue(propName);
1096
+ if (propValue) {
1097
+ const cssString = await embedResources(propValue, null, options);
1098
+ node.style.setProperty(propName, cssString, node.style.getPropertyPriority(propName));
1099
+ return true;
1100
+ }
1101
+ return false;
1102
+ }
1103
+ async function embedBackground(clonedNode, options) {
1104
+ ;
1105
+ await embedProp("background", clonedNode, options) || await embedProp("background-image", clonedNode, options);
1106
+ await embedProp("mask", clonedNode, options) || await embedProp("-webkit-mask", clonedNode, options) || await embedProp("mask-image", clonedNode, options) || await embedProp("-webkit-mask-image", clonedNode, options);
1107
+ }
1108
+ async function embedImageNode(clonedNode, options) {
1109
+ const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement);
1110
+ if (!(isImageElement && !isDataUrl(clonedNode.src)) && !(isInstanceOfElement(clonedNode, SVGImageElement) && !isDataUrl(clonedNode.href.baseVal))) {
1111
+ return;
1112
+ }
1113
+ const url = isImageElement ? clonedNode.src : clonedNode.href.baseVal;
1114
+ const dataURL = await resourceToDataURL(url, getMimeType(url), options);
1115
+ await new Promise((resolve, reject) => {
1116
+ clonedNode.onload = resolve;
1117
+ clonedNode.onerror = options.onImageErrorHandler ? (...attributes) => {
1118
+ try {
1119
+ resolve(options.onImageErrorHandler(...attributes));
1120
+ } catch (error) {
1121
+ reject(error);
1122
+ }
1123
+ } : reject;
1124
+ const image = clonedNode;
1125
+ if (image.decode) {
1126
+ image.decode = resolve;
1127
+ }
1128
+ if (image.loading === "lazy") {
1129
+ image.loading = "eager";
1130
+ }
1131
+ if (isImageElement) {
1132
+ clonedNode.srcset = "";
1133
+ clonedNode.src = dataURL;
1134
+ } else {
1135
+ clonedNode.href.baseVal = dataURL;
1136
+ }
1137
+ });
1138
+ }
1139
+ async function embedChildren(clonedNode, options) {
1140
+ const children = toArray(clonedNode.childNodes);
1141
+ const deferreds = children.map((child) => embedImages(child, options));
1142
+ await Promise.all(deferreds).then(() => clonedNode);
1143
+ }
1144
+ async function embedImages(clonedNode, options) {
1145
+ if (isInstanceOfElement(clonedNode, Element)) {
1146
+ await embedBackground(clonedNode, options);
1147
+ await embedImageNode(clonedNode, options);
1148
+ await embedChildren(clonedNode, options);
1149
+ }
1150
+ }
1151
+
1152
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/apply-style.js
1153
+ function applyStyle(node, options) {
1154
+ const { style } = node;
1155
+ if (options.backgroundColor) {
1156
+ style.backgroundColor = options.backgroundColor;
1157
+ }
1158
+ if (options.width) {
1159
+ style.width = `${options.width}px`;
1160
+ }
1161
+ if (options.height) {
1162
+ style.height = `${options.height}px`;
1163
+ }
1164
+ const manual = options.style;
1165
+ if (manual != null) {
1166
+ Object.keys(manual).forEach((key) => {
1167
+ style[key] = manual[key];
1168
+ });
1169
+ }
1170
+ return node;
1171
+ }
1172
+
1173
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/embed-webfonts.js
1174
+ var cssFetchCache = {};
1175
+ async function fetchCSS(url) {
1176
+ let cache2 = cssFetchCache[url];
1177
+ if (cache2 != null) {
1178
+ return cache2;
1179
+ }
1180
+ const res = await fetch(url);
1181
+ const cssText = await res.text();
1182
+ cache2 = { url, cssText };
1183
+ cssFetchCache[url] = cache2;
1184
+ return cache2;
1185
+ }
1186
+ async function embedFonts(data, options) {
1187
+ let cssText = data.cssText;
1188
+ const regexUrl = /url\(["']?([^"')]+)["']?\)/g;
1189
+ const fontLocs = cssText.match(/url\([^)]+\)/g) || [];
1190
+ const loadFonts = fontLocs.map(async (loc) => {
1191
+ let url = loc.replace(regexUrl, "$1");
1192
+ if (!url.startsWith("https://")) {
1193
+ url = new URL(url, data.url).href;
1194
+ }
1195
+ return fetchAsDataURL(url, options.fetchRequestInit, ({ result }) => {
1196
+ cssText = cssText.replace(loc, `url(${result})`);
1197
+ return [loc, result];
1198
+ });
1199
+ });
1200
+ return Promise.all(loadFonts).then(() => cssText);
1201
+ }
1202
+ function parseCSS(source) {
1203
+ if (source == null) {
1204
+ return [];
1205
+ }
1206
+ const result = [];
1207
+ const commentsRegex = /(\/\*[\s\S]*?\*\/)/gi;
1208
+ let cssText = source.replace(commentsRegex, "");
1209
+ const keyframesRegex = new RegExp("((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})", "gi");
1210
+ while (true) {
1211
+ const matches = keyframesRegex.exec(cssText);
1212
+ if (matches === null) {
1213
+ break;
1214
+ }
1215
+ result.push(matches[0]);
1216
+ }
1217
+ cssText = cssText.replace(keyframesRegex, "");
1218
+ const importRegex = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi;
1219
+ const combinedCSSRegex = "((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})";
1220
+ const unifiedRegex = new RegExp(combinedCSSRegex, "gi");
1221
+ while (true) {
1222
+ let matches = importRegex.exec(cssText);
1223
+ if (matches === null) {
1224
+ matches = unifiedRegex.exec(cssText);
1225
+ if (matches === null) {
1226
+ break;
1227
+ } else {
1228
+ importRegex.lastIndex = unifiedRegex.lastIndex;
1229
+ }
1230
+ } else {
1231
+ unifiedRegex.lastIndex = importRegex.lastIndex;
1232
+ }
1233
+ result.push(matches[0]);
1234
+ }
1235
+ return result;
1236
+ }
1237
+ async function getCSSRules(styleSheets, options) {
1238
+ const ret = [];
1239
+ const deferreds = [];
1240
+ styleSheets.forEach((sheet) => {
1241
+ if ("cssRules" in sheet) {
1242
+ try {
1243
+ toArray(sheet.cssRules || []).forEach((item, index) => {
1244
+ if (item.type === CSSRule.IMPORT_RULE) {
1245
+ let importIndex = index + 1;
1246
+ const url = item.href;
1247
+ const deferred = fetchCSS(url).then((metadata) => embedFonts(metadata, options)).then((cssText) => parseCSS(cssText).forEach((rule) => {
1248
+ try {
1249
+ sheet.insertRule(rule, rule.startsWith("@import") ? importIndex += 1 : sheet.cssRules.length);
1250
+ } catch (error) {
1251
+ console.error("Error inserting rule from remote css", {
1252
+ rule,
1253
+ error
1254
+ });
1255
+ }
1256
+ })).catch((e) => {
1257
+ console.error("Error loading remote css", e.toString());
1258
+ });
1259
+ deferreds.push(deferred);
1260
+ }
1261
+ });
1262
+ } catch (e) {
1263
+ const inline = styleSheets.find((a) => a.href == null) || document.styleSheets[0];
1264
+ if (sheet.href != null) {
1265
+ deferreds.push(fetchCSS(sheet.href).then((metadata) => embedFonts(metadata, options)).then((cssText) => parseCSS(cssText).forEach((rule) => {
1266
+ inline.insertRule(rule, inline.cssRules.length);
1267
+ })).catch((err) => {
1268
+ console.error("Error loading remote stylesheet", err);
1269
+ }));
1270
+ }
1271
+ console.error("Error inlining remote css file", e);
1272
+ }
1273
+ }
1274
+ });
1275
+ return Promise.all(deferreds).then(() => {
1276
+ styleSheets.forEach((sheet) => {
1277
+ if ("cssRules" in sheet) {
1278
+ try {
1279
+ toArray(sheet.cssRules || []).forEach((item) => {
1280
+ ret.push(item);
1281
+ });
1282
+ } catch (e) {
1283
+ console.error(`Error while reading CSS rules from ${sheet.href}`, e);
1284
+ }
1285
+ }
1286
+ });
1287
+ return ret;
1288
+ });
1289
+ }
1290
+ function getWebFontRules(cssRules) {
1291
+ return cssRules.filter((rule) => rule.type === CSSRule.FONT_FACE_RULE).filter((rule) => shouldEmbed(rule.style.getPropertyValue("src")));
1292
+ }
1293
+ async function parseWebFontRules(node, options) {
1294
+ if (node.ownerDocument == null) {
1295
+ throw new Error("Provided element is not within a Document");
1296
+ }
1297
+ const styleSheets = toArray(node.ownerDocument.styleSheets);
1298
+ const cssRules = await getCSSRules(styleSheets, options);
1299
+ return getWebFontRules(cssRules);
1300
+ }
1301
+ function normalizeFontFamily(font) {
1302
+ return font.trim().replace(/["']/g, "");
1303
+ }
1304
+ function getUsedFonts(node) {
1305
+ const fonts = /* @__PURE__ */ new Set();
1306
+ function traverse(node2) {
1307
+ const fontFamily = node2.style.fontFamily || getComputedStyle(node2).fontFamily;
1308
+ fontFamily.split(",").forEach((font) => {
1309
+ fonts.add(normalizeFontFamily(font));
1310
+ });
1311
+ Array.from(node2.children).forEach((child) => {
1312
+ if (child instanceof HTMLElement) {
1313
+ traverse(child);
1314
+ }
1315
+ });
1316
+ }
1317
+ traverse(node);
1318
+ return fonts;
1319
+ }
1320
+ async function getWebFontCSS(node, options) {
1321
+ const rules = await parseWebFontRules(node, options);
1322
+ const usedFonts = getUsedFonts(node);
1323
+ const cssTexts = await Promise.all(rules.filter((rule) => usedFonts.has(normalizeFontFamily(rule.style.fontFamily))).map((rule) => {
1324
+ const baseUrl = rule.parentStyleSheet ? rule.parentStyleSheet.href : null;
1325
+ return embedResources(rule.cssText, baseUrl, options);
1326
+ }));
1327
+ return cssTexts.join("\n");
1328
+ }
1329
+ async function embedWebFonts(clonedNode, options) {
1330
+ const cssText = options.fontEmbedCSS != null ? options.fontEmbedCSS : options.skipFonts ? null : await getWebFontCSS(clonedNode, options);
1331
+ if (cssText) {
1332
+ const styleNode = document.createElement("style");
1333
+ const sytleContent = document.createTextNode(cssText);
1334
+ styleNode.appendChild(sytleContent);
1335
+ if (clonedNode.firstChild) {
1336
+ clonedNode.insertBefore(styleNode, clonedNode.firstChild);
1337
+ } else {
1338
+ clonedNode.appendChild(styleNode);
1339
+ }
1340
+ }
1341
+ }
1342
+
1343
+ // node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/index.js
1344
+ async function toSvg(node, options = {}) {
1345
+ const { width, height } = getImageSize(node, options);
1346
+ const clonedNode = await cloneNode(node, options, true);
1347
+ await embedWebFonts(clonedNode, options);
1348
+ await embedImages(clonedNode, options);
1349
+ applyStyle(clonedNode, options);
1350
+ const datauri = await nodeToDataURL(clonedNode, width, height);
1351
+ return datauri;
1352
+ }
1353
+ async function toCanvas(node, options = {}) {
1354
+ const { width, height } = getImageSize(node, options);
1355
+ const svg = await toSvg(node, options);
1356
+ const img = await createImage(svg);
1357
+ const canvas = document.createElement("canvas");
1358
+ const context = canvas.getContext("2d");
1359
+ const ratio = options.pixelRatio || getPixelRatio();
1360
+ const canvasWidth = options.canvasWidth || width;
1361
+ const canvasHeight = options.canvasHeight || height;
1362
+ canvas.width = canvasWidth * ratio;
1363
+ canvas.height = canvasHeight * ratio;
1364
+ if (!options.skipAutoScale) {
1365
+ checkCanvasDimensions(canvas);
1366
+ }
1367
+ canvas.style.width = `${canvasWidth}`;
1368
+ canvas.style.height = `${canvasHeight}`;
1369
+ if (options.backgroundColor) {
1370
+ context.fillStyle = options.backgroundColor;
1371
+ context.fillRect(0, 0, canvas.width, canvas.height);
1372
+ }
1373
+ context.drawImage(img, 0, 0, canvas.width, canvas.height);
1374
+ return canvas;
1375
+ }
1376
+
1377
+ // packages/magic-edit/src/capture.ts
1378
+ var maxCaptureEdge = 1600;
1379
+ function drawingContext(canvas) {
1380
+ const context = canvas.getContext("2d");
1381
+ if (!context) throw new Error("Image capture is unavailable in this browser.");
1382
+ return context;
1383
+ }
1384
+ function targetMedia(element) {
1385
+ return element.localName === "video" || element.localName === "img" ? element : element.querySelector("video, img");
1386
+ }
1387
+ function captureMedia(element) {
1388
+ const media = targetMedia(element);
1389
+ if (!media) throw new Error("Open the target image or video before capturing it.");
1390
+ const video = media.localName === "video" ? media : void 0;
1391
+ const width = video ? video.videoWidth : media.naturalWidth;
1392
+ const height = video ? video.videoHeight : media.naturalHeight;
1393
+ if (!width || !height || video && (video.seeking || video.readyState < 2)) {
1394
+ throw new Error("Wait for the image or video frame to load and try again.");
1395
+ }
1396
+ video?.pause();
1397
+ const scale = Math.min(1, maxCaptureEdge / Math.max(width, height));
1398
+ const image = document.createElement("canvas");
1399
+ image.width = Math.max(1, Math.round(width * scale));
1400
+ image.height = Math.max(1, Math.round(height * scale));
1401
+ const context = drawingContext(image);
1402
+ context.drawImage(media, 0, 0, image.width, image.height);
1403
+ return { image, element: media, sourceTimeMs: video && Math.round(video.currentTime * 1e3) };
1404
+ }
1405
+ async function captureTarget({ element, ...target }) {
1406
+ const { width, height } = element.getBoundingClientRect();
1407
+ if (!element.isConnected || width < 1 || height < 1) {
1408
+ throw new Error("Open a visible target before capturing it.");
1409
+ }
1410
+ let image;
1411
+ let surface = element;
1412
+ if (target.kind === "media") {
1413
+ const media = captureMedia(element);
1414
+ image = media.image;
1415
+ surface = media.element;
1416
+ target.sourceTimeMs ??= media.sourceTimeMs;
1417
+ } else {
1418
+ image = await toCanvas(element, {
1419
+ pixelRatio: Math.min(1, maxCaptureEdge / Math.max(width, height)),
1420
+ width,
1421
+ height,
1422
+ skipFonts: true,
1423
+ includeQueryParams: true
1424
+ });
1425
+ }
1426
+ try {
1427
+ drawingContext(image).getImageData(0, 0, 1, 1);
1428
+ } catch {
1429
+ throw new Error(
1430
+ "This source cannot be captured. Open a project-hosted image or video and retry."
1431
+ );
1432
+ }
1433
+ return { image, target, element: surface };
1434
+ }
1435
+
1436
+ // packages/magic-edit/src/client.tsx
1437
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1438
+ function message(error, fallback) {
1439
+ return error instanceof Error ? error.message : fallback;
1440
+ }
1441
+ var interactive = 'button, a, input, textarea, select, [role="button"], [role="tab"], [role="radio"], [role="checkbox"], [role="slider"], [contenteditable]';
1442
+ function selectedElement(element, global) {
1443
+ const owner = element.ownerDocument;
1444
+ if (global && (element.closest('nav, [role="navigation"]') || owner === document && !element.closest("main")))
1445
+ return;
1446
+ const HTML = owner.defaultView?.HTMLElement;
1447
+ if (!HTML) return;
1448
+ const target = element.closest(interactive) ?? element.closest("video, img, canvas, [data-magic-edit-label]") ?? element.closest('h1, h2, h3, h4, h5, h6, p, li, figcaption, tr, [role="row"]') ?? (element instanceof HTML ? element : element.parentElement);
1449
+ return target instanceof HTML && !target.matches('body, html, main, [role="main"]') ? target : void 0;
1450
+ }
1451
+ function pageTarget(element) {
1452
+ const kind = element.localName === "video" || element.localName === "img" ? "media" : "surface";
1453
+ const visibleText = kind === "surface" ? element.innerText.trim().replace(/\s+/g, " ").slice(0, 280) : "";
1454
+ const label = (element.dataset.magicEditLabel || element.getAttribute("aria-label") || element.getAttribute("alt") || element.querySelector("h1, h2, h3, figcaption")?.textContent || visibleText || (kind === "media" ? "Media frame" : "Page selection")).trim().slice(0, 80);
1455
+ return {
1456
+ element,
1457
+ kind,
1458
+ id: crypto.randomUUID(),
1459
+ label: label || "Page selection",
1460
+ ...visibleText && visibleText !== label ? { visibleText } : {}
1461
+ };
1462
+ }
1463
+ function markBounds(element, kind) {
1464
+ const isMedia = kind === "media" || element.matches("video, img");
1465
+ if (!isMedia) return element.getBoundingClientRect();
1466
+ const media = targetMedia(element);
1467
+ if (!media) return;
1468
+ return "videoWidth" in media ? pictureBounds(media, media.videoWidth, media.videoHeight) : pictureBounds(media, media.naturalWidth, media.naturalHeight);
1469
+ }
1470
+ function download(image) {
1471
+ const url = URL.createObjectURL(image);
1472
+ const link = document.createElement("a");
1473
+ link.href = url;
1474
+ link.download = "magic-edit.png";
1475
+ link.click();
1476
+ window.setTimeout(() => URL.revokeObjectURL(url), 1e4);
1477
+ }
1478
+ function richRequest(instruction, image) {
1479
+ return new Promise((resolve, reject) => {
1480
+ const reader = new FileReader();
1481
+ reader.addEventListener("error", () => reject(reader.error));
1482
+ reader.addEventListener("load", () => {
1483
+ const container = document.createElement("div");
1484
+ const text = document.createElement("pre");
1485
+ text.textContent = instruction;
1486
+ const preview = document.createElement("img");
1487
+ preview.alt = "Marked frame";
1488
+ preview.src = String(reader.result);
1489
+ container.append(text, preview);
1490
+ resolve(new Blob([container.outerHTML], { type: "text/html" }));
1491
+ });
1492
+ reader.readAsDataURL(image);
1493
+ });
1494
+ }
1495
+ var copiedForChat = "Text and image copied. Paste into your agent chat.";
1496
+ async function copyRequest(instruction, image) {
1497
+ try {
1498
+ if (!navigator.clipboard || !globalThis.ClipboardItem) return false;
1499
+ const html = image.then((blob) => richRequest(instruction, blob));
1500
+ void html.catch(() => {
1501
+ });
1502
+ await navigator.clipboard.write([
1503
+ new ClipboardItem({
1504
+ "text/plain": new Blob([instruction], { type: "text/plain" }),
1505
+ "text/html": html,
1506
+ "image/png": image
1507
+ })
1508
+ ]);
1509
+ return true;
1510
+ } catch {
1511
+ return false;
1512
+ }
1513
+ }
1514
+ function RequestActions({ prepared, onContinue }) {
1515
+ const [copyStatus, setCopyStatus] = useState2(prepared.copied ? copiedForChat : "");
1516
+ const [clipboardError, setClipboardError] = useState2(!prepared.copied);
1517
+ const { instruction } = prepared;
1518
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
1519
+ /* @__PURE__ */ jsx2("p", { className: "magic-edit-feedback", role: "status", children: copyStatus || "Ready to copy the instruction and marked image." }),
1520
+ /* @__PURE__ */ jsxs2("div", { className: "magic-edit-actions", children: [
1521
+ /* @__PURE__ */ jsx2(
1522
+ Button,
1523
+ {
1524
+ label: "Copy for Chat",
1525
+ width: "hug",
1526
+ onClick: async () => {
1527
+ const copied = await copyRequest(instruction, Promise.resolve(prepared.image));
1528
+ setCopyStatus(copied ? copiedForChat : "");
1529
+ setClipboardError(!copied);
1530
+ }
1531
+ }
1532
+ ),
1533
+ /* @__PURE__ */ jsx2(
1534
+ Button,
1535
+ {
1536
+ label: "Download marked PNG",
1537
+ variant: "text-tonal-sm",
1538
+ width: "hug",
1539
+ onClick: () => download(prepared.image)
1540
+ }
1541
+ ),
1542
+ /* @__PURE__ */ jsx2(Button, { label: "New request", variant: "text-ghost-sm", width: "hug", onClick: onContinue })
1543
+ ] }),
1544
+ /* @__PURE__ */ jsxs2("details", { open: clipboardError || void 0, children: [
1545
+ /* @__PURE__ */ jsx2("summary", { children: "Copy instruction separately" }),
1546
+ /* @__PURE__ */ jsx2(
1547
+ Button,
1548
+ {
1549
+ label: "Copy instruction",
1550
+ variant: "text-tonal-sm",
1551
+ width: "hug",
1552
+ onClick: async () => {
1553
+ try {
1554
+ await navigator.clipboard.writeText(instruction);
1555
+ setCopyStatus("Instruction copied.");
1556
+ setClipboardError(false);
1557
+ } catch {
1558
+ setClipboardError(true);
1559
+ }
1560
+ }
1561
+ }
1562
+ ),
1563
+ /* @__PURE__ */ jsx2(
1564
+ "textarea",
1565
+ {
1566
+ className: "magic-edit-instruction",
1567
+ "aria-label": "Instruction for your agent",
1568
+ value: instruction,
1569
+ readOnly: true,
1570
+ rows: 5,
1571
+ onFocus: (event) => event.currentTarget.select()
1572
+ }
1573
+ )
1574
+ ] }),
1575
+ clipboardError && /* @__PURE__ */ jsx2("p", { className: "magic-edit-feedback", role: "alert", children: "Could not copy the text and image together. Copy the instruction and download the image separately." })
1576
+ ] });
1577
+ }
1578
+ function MagicEdit({
1579
+ projectId,
1580
+ targets,
1581
+ tools: tools2,
1582
+ global = false,
1583
+ embeddedProject
1584
+ }) {
1585
+ const [open, setOpen] = useState2(false);
1586
+ const [tool, setTool] = useState2("select");
1587
+ const [hovered, setHovered] = useState2();
1588
+ const [choosing, setChoosing] = useState2(false);
1589
+ const [choices, setChoices] = useState2([]);
1590
+ const [selection, setSelection] = useState2();
1591
+ const [capture, setCapture] = useState2();
1592
+ const [regions, setRegions] = useState2([]);
1593
+ const [pagePreview, setPagePreview] = useState2();
1594
+ const [instruction, setInstruction] = useState2("");
1595
+ const [prepared, setPrepared] = useState2();
1596
+ const [work, setWork] = useState2();
1597
+ const [error, setError] = useState2("");
1598
+ const [frameDocument, setFrameDocument] = useState2();
1599
+ const [toolbarSlot, setToolbarSlot] = useState2(null);
1600
+ const dock = useRef3(null);
1601
+ const composer = useRef3(null);
1602
+ const effects = useRef3(null);
1603
+ const triggerBounds = useRef3(void 0);
1604
+ const prompt = useRef3(null);
1605
+ const previousFocus = useRef3(null);
1606
+ const captureRequest = useRef3(0);
1607
+ const pageMark = useRef3(void 0);
1608
+ const suppressClick = useRef3(void 0);
1609
+ const frame = embeddedProject?.frame;
1610
+ const embeddedId = embeddedProject?.projectId;
1611
+ const captureBounds = useCaptureBounds(
1612
+ open && !choosing && !prepared ? capture : void 0,
1613
+ frame?.current
1614
+ );
1615
+ const anchor = captureBounds?.visible;
1616
+ useLayoutEffect2(() => {
1617
+ const panel = composer.current;
1618
+ if (!panel || !anchor) return;
1619
+ function place() {
1620
+ if (!panel || !anchor) return;
1621
+ const margin = 8;
1622
+ const bottom = window.innerHeight - 68;
1623
+ const width = panel.offsetWidth;
1624
+ const height = panel.offsetHeight;
1625
+ const centered = anchor.left + (anchor.width - width) / 2;
1626
+ const above = anchor.top - height - margin;
1627
+ const vertical = above >= margin ? above : Math.min(anchor.bottom + margin, bottom - height);
1628
+ panel.style.left = `${Math.max(margin, Math.min(centered, window.innerWidth - width - margin))}px`;
1629
+ panel.style.top = `${Math.max(margin, Math.min(vertical, bottom - height))}px`;
1630
+ }
1631
+ place();
1632
+ const observer = new ResizeObserver(place);
1633
+ observer.observe(panel);
1634
+ return () => {
1635
+ observer.disconnect();
1636
+ panel.style.removeProperty("left");
1637
+ panel.style.removeProperty("top");
1638
+ };
1639
+ }, [anchor]);
1640
+ function registeredTargets(owner) {
1641
+ const candidates = owner === document ? targets?.() ?? [] : readMagicEditSource(owner)?.targets() ?? [];
1642
+ return candidates.filter(({ element }) => element.ownerDocument === owner);
1643
+ }
1644
+ function availableTargets() {
1645
+ return [
1646
+ ...registeredTargets(document),
1647
+ ...frameDocument ? registeredTargets(frameDocument) : []
1648
+ ].filter(
1649
+ ({ element }) => screenBounds(element, element.getBoundingClientRect(), frame?.current, true)
1650
+ );
1651
+ }
1652
+ function inCurrentDocument(element) {
1653
+ return element.isConnected && (element.ownerDocument === document || element.ownerDocument === embeddedDocument(frame?.current));
1654
+ }
1655
+ async function takeSnapshot(target, markWhole) {
1656
+ if (!inCurrentDocument(target.element)) {
1657
+ setError("The target is no longer in the active workbench. Select it again.");
1658
+ return;
1659
+ }
1660
+ const request = ++captureRequest.current;
1661
+ setSelection(target);
1662
+ setCapture(void 0);
1663
+ setRegions([]);
1664
+ setError("");
1665
+ setWork("capture");
1666
+ try {
1667
+ const frame2 = await captureTarget(target);
1668
+ if (captureRequest.current === request) {
1669
+ if (!inCurrentDocument(target.element)) {
1670
+ setError("The target is no longer in the active workbench. Select it again.");
1671
+ return;
1672
+ }
1673
+ setCapture(frame2);
1674
+ if (markWhole) setRegions([{ kind: "frame" }]);
1675
+ }
1676
+ } catch (cause) {
1677
+ if (captureRequest.current === request)
1678
+ setError(message(cause, "The selected target could not be captured."));
1679
+ } finally {
1680
+ if (captureRequest.current === request) setWork(void 0);
1681
+ }
1682
+ }
1683
+ function select(id) {
1684
+ const target = availableTargets().find((choice) => choice.id === id);
1685
+ if (target) void takeSnapshot(target, tool === "select");
1686
+ else {
1687
+ captureRequest.current += 1;
1688
+ setCapture(void 0);
1689
+ setRegions([]);
1690
+ setWork(void 0);
1691
+ setChoosing(true);
1692
+ setError("This target is no longer visible. Select another one.");
1693
+ }
1694
+ }
1695
+ function clearSelection() {
1696
+ captureRequest.current += 1;
1697
+ pageMark.current = void 0;
1698
+ setPagePreview(void 0);
1699
+ setPrepared(void 0);
1700
+ setInstruction("");
1701
+ setCapture(void 0);
1702
+ setSelection(void 0);
1703
+ setRegions([]);
1704
+ setHovered(void 0);
1705
+ setError("");
1706
+ setWork(void 0);
1707
+ }
1708
+ function toggle(active) {
1709
+ if (active) {
1710
+ triggerBounds.current = dock.current?.querySelector("button")?.getBoundingClientRect();
1711
+ const focused = document.activeElement === frame?.current ? frameDocument?.activeElement : document.activeElement;
1712
+ previousFocus.current = focused ?? null;
1713
+ setChoices(availableTargets());
1714
+ setChoosing(true);
1715
+ setOpen(true);
1716
+ return;
1717
+ }
1718
+ if (work === "prepare") return;
1719
+ clearSelection();
1720
+ setChoosing(false);
1721
+ setOpen(false);
1722
+ }
1723
+ useLayoutEffect2(() => {
1724
+ const layer = effects.current;
1725
+ const button = triggerBounds.current;
1726
+ const surface = frame?.current ?? document.querySelector("main");
1727
+ if (!open || !layer || !button || !surface) return;
1728
+ const wave = playMagicEditWave(layer, surface.getBoundingClientRect(), button);
1729
+ return () => wave?.abort();
1730
+ }, [open, frame]);
1731
+ function targetAt(element) {
1732
+ const candidates = registeredTargets(element.ownerDocument);
1733
+ for (let current = element; current; current = current.parentElement) {
1734
+ const target = candidates.find((choice) => choice.element === current);
1735
+ if (target) return target;
1736
+ }
1737
+ }
1738
+ const onPageClick = useEffectEvent((event) => {
1739
+ const clicked = elementNode(event.target);
1740
+ if (event.detail && clicked && clicked.ownerDocument === suppressClick.current) {
1741
+ event.preventDefault();
1742
+ event.stopPropagation();
1743
+ return;
1744
+ }
1745
+ if (!open || !choosing || work || !clicked || dock.current?.contains(clicked)) return;
1746
+ const registered = targetAt(clicked);
1747
+ const element = registered?.element ?? selectedElement(clicked, global);
1748
+ if (!element) return;
1749
+ event.preventDefault();
1750
+ event.stopPropagation();
1751
+ setChoosing(false);
1752
+ setHovered(void 0);
1753
+ void takeSnapshot(registered ?? pageTarget(element), tool === "select");
1754
+ });
1755
+ const onPagePointerDown = useEffectEvent((event) => {
1756
+ if (!open || !choosing || work || tool === "select" || !event.isPrimary || event.button !== 0)
1757
+ return;
1758
+ const clicked = elementNode(event.target);
1759
+ if (!clicked || dock.current?.contains(clicked)) return;
1760
+ const registered = targetAt(clicked);
1761
+ const element = registered?.element ?? selectedElement(clicked, global);
1762
+ if (!element) return;
1763
+ const target = registered ?? pageTarget(element);
1764
+ const bounds = markBounds(target.element, target.kind);
1765
+ if (!bounds?.width || !bounds.height) return;
1766
+ event.preventDefault();
1767
+ event.stopPropagation();
1768
+ clicked.setPointerCapture(event.pointerId);
1769
+ const region = startRegion(tool, pointInBounds(event, bounds));
1770
+ pageMark.current = { owner: clicked.ownerDocument, pointerId: event.pointerId, bounds, region };
1771
+ setPagePreview(region);
1772
+ setChoosing(false);
1773
+ setHovered(void 0);
1774
+ void takeSnapshot(target, false);
1775
+ });
1776
+ const onPointerMove = useEffectEvent((event) => {
1777
+ const pending = pageMark.current;
1778
+ if (pending?.pointerId === event.pointerId) {
1779
+ event.preventDefault();
1780
+ event.stopPropagation();
1781
+ pending.region = extendRegion(pending.region, pointInBounds(event, pending.bounds));
1782
+ setPagePreview(pending.region);
1783
+ return;
1784
+ }
1785
+ if (!choosing || tool !== "select") return;
1786
+ const pointed = elementNode(event.target);
1787
+ if (!pointed || dock.current?.contains(pointed)) {
1788
+ setHovered(void 0);
1789
+ return;
1790
+ }
1791
+ const registered = targetAt(pointed);
1792
+ const element = registered?.element ?? selectedElement(pointed, global);
1793
+ const bounds = element && markBounds(element, registered?.kind);
1794
+ const next = element && bounds && screenBounds(element, bounds, frame?.current, true);
1795
+ setHovered(
1796
+ (previous) => previous?.x === next?.x && previous?.y === next?.y && previous?.width === next?.width && previous?.height === next?.height ? previous : next
1797
+ );
1798
+ });
1799
+ const onPagePointerEnd = useEffectEvent((event) => {
1800
+ const pending = pageMark.current;
1801
+ if (pending?.pointerId !== event.pointerId) return;
1802
+ event.preventDefault();
1803
+ event.stopPropagation();
1804
+ pageMark.current = void 0;
1805
+ setPagePreview(void 0);
1806
+ if (event.type !== "pointerup") return;
1807
+ suppressClick.current = pending.owner;
1808
+ window.setTimeout(() => {
1809
+ if (suppressClick.current === pending.owner) suppressClick.current = void 0;
1810
+ }, 0);
1811
+ const region = extendRegion(pending.region, pointInBounds(event, pending.bounds));
1812
+ if (validRegion(region)) setRegions([region]);
1813
+ });
1814
+ const onShortcut = useEffectEvent((event) => {
1815
+ if (open && event.key === "Escape" && !event.defaultPrevented) {
1816
+ if (selection && !prepared && work !== "prepare") {
1817
+ clearSelection();
1818
+ setChoosing(true);
1819
+ dock.current?.querySelector('[aria-pressed="true"]')?.focus();
1820
+ } else toggle(false);
1821
+ event.preventDefault();
1822
+ return;
1823
+ }
1824
+ const target = elementNode(event.target);
1825
+ const modal = 'dialog[open], [role="dialog"][aria-modal="true"]';
1826
+ if (open || event.defaultPrevented || event.repeat || event.isComposing || event.key.toLowerCase() !== "c" || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || document.querySelector(modal) || target?.ownerDocument.querySelector(modal)) {
1827
+ return;
1828
+ }
1829
+ if (target && (target.closest(
1830
+ 'input, textarea, select, .ProseMirror, [role="textbox"], [role="combobox"], [role="dialog"], [role="alertdialog"]'
1831
+ ) || target.isContentEditable)) {
1832
+ return;
1833
+ }
1834
+ event.preventDefault();
1835
+ toggle(true);
1836
+ requestAnimationFrame(
1837
+ () => dock.current?.querySelector('[aria-pressed="true"]')?.focus()
1838
+ );
1839
+ });
1840
+ const onTargetGone = useEffectEvent(() => {
1841
+ if (prepared) return;
1842
+ captureRequest.current += 1;
1843
+ pageMark.current = void 0;
1844
+ setPagePreview(void 0);
1845
+ setSelection(void 0);
1846
+ setCapture(void 0);
1847
+ setRegions([]);
1848
+ setWork(void 0);
1849
+ setHovered(void 0);
1850
+ setChoosing(open);
1851
+ });
1852
+ const onFrameChanged = useEffectEvent((owner) => {
1853
+ if (selection?.element.ownerDocument === owner) onTargetGone();
1854
+ });
1855
+ useEffect3(() => {
1856
+ const iframe = frame?.current;
1857
+ const clear = () => setHovered(void 0);
1858
+ function listen(owner2) {
1859
+ owner2.addEventListener("keydown", onShortcut);
1860
+ owner2.addEventListener("click", onPageClick, true);
1861
+ owner2.addEventListener("pointerdown", onPagePointerDown, true);
1862
+ owner2.addEventListener("pointermove", onPointerMove, true);
1863
+ owner2.addEventListener("pointerup", onPagePointerEnd, true);
1864
+ owner2.addEventListener("pointercancel", onPagePointerEnd, true);
1865
+ owner2.addEventListener("scroll", clear, true);
1866
+ owner2.defaultView?.addEventListener("resize", clear);
1867
+ return () => {
1868
+ owner2.removeEventListener("keydown", onShortcut);
1869
+ owner2.removeEventListener("click", onPageClick, true);
1870
+ owner2.removeEventListener("pointerdown", onPagePointerDown, true);
1871
+ owner2.removeEventListener("pointermove", onPointerMove, true);
1872
+ owner2.removeEventListener("pointerup", onPagePointerEnd, true);
1873
+ owner2.removeEventListener("pointercancel", onPagePointerEnd, true);
1874
+ owner2.removeEventListener("scroll", clear, true);
1875
+ owner2.defaultView?.removeEventListener("resize", clear);
1876
+ };
1877
+ }
1878
+ const stopHome = listen(document);
1879
+ let owner;
1880
+ let stopProject;
1881
+ function attachProject() {
1882
+ const next = embeddedDocument(iframe);
1883
+ if (owner === next) return;
1884
+ stopProject?.();
1885
+ if (owner) onFrameChanged(owner);
1886
+ owner = next;
1887
+ stopProject = next ? listen(next) : void 0;
1888
+ setFrameDocument(next);
1889
+ clear();
1890
+ }
1891
+ attachProject();
1892
+ iframe?.addEventListener("load", attachProject);
1893
+ return () => {
1894
+ stopHome();
1895
+ stopProject?.();
1896
+ iframe?.removeEventListener("load", attachProject);
1897
+ };
1898
+ }, [frame, embeddedId]);
1899
+ useEffect3(() => {
1900
+ if (!selection) return;
1901
+ const element = selection.element;
1902
+ const observer = new MutationObserver(() => {
1903
+ if (!element.isConnected) onTargetGone();
1904
+ });
1905
+ observer.observe(element.ownerDocument, { childList: true, subtree: true });
1906
+ return () => observer.disconnect();
1907
+ }, [selection]);
1908
+ useEffect3(() => {
1909
+ if (open || !previousFocus.current) return;
1910
+ const restore = previousFocus.current;
1911
+ previousFocus.current = null;
1912
+ const frame2 = requestAnimationFrame(() => {
1913
+ if (restore.isConnected && restore !== restore.ownerDocument.body)
1914
+ restore.focus({ preventScroll: true });
1915
+ if (restore.ownerDocument.activeElement !== restore || restore === restore.ownerDocument.body)
1916
+ dock.current?.querySelector("button")?.focus({ preventScroll: true });
1917
+ });
1918
+ return () => cancelAnimationFrame(frame2);
1919
+ }, [open]);
1920
+ useEffect3(() => {
1921
+ if (capture && open && !choosing && !prepared && !pagePreview)
1922
+ prompt.current?.focus({ preventScroll: true });
1923
+ }, [capture, open, choosing, prepared, pagePreview]);
1924
+ async function prepare() {
1925
+ if (!capture || !regions.length || !instruction.trim() || work) return;
1926
+ if (!inCurrentDocument(capture.element)) {
1927
+ setError("The target is no longer in the active workbench. Select it again.");
1928
+ return;
1929
+ }
1930
+ const request = captureRequest.current;
1931
+ setWork("prepare");
1932
+ setError("");
1933
+ try {
1934
+ const { width: imageWidth, height: imageHeight } = capture.image;
1935
+ const text = handoffText({
1936
+ projectId: capture.element.ownerDocument === document ? projectId ?? capture.element.closest("[data-magic-edit-project]")?.dataset.magicEditProject : embeddedId,
1937
+ target: capture.target,
1938
+ instruction: instruction.trim(),
1939
+ regions,
1940
+ frame: { width: imageWidth, height: imageHeight }
1941
+ });
1942
+ const imageRequest = markedPng(capture.image, regions);
1943
+ const [image, copied] = await Promise.all([imageRequest, copyRequest(text, imageRequest)]);
1944
+ if (request !== captureRequest.current) return;
1945
+ setPrepared({ image, copied, instruction: text });
1946
+ } catch (cause) {
1947
+ if (request === captureRequest.current)
1948
+ setError(
1949
+ message(cause, "Could not create the marked image. Your instruction is still here.")
1950
+ );
1951
+ } finally {
1952
+ if (request === captureRequest.current) setWork(void 0);
1953
+ }
1954
+ }
1955
+ const capturedTime = capture && formatTargetTime(capture.target);
1956
+ const mountProjectTools = open && frameDocument && readMagicEditSource(frameDocument)?.mountTools;
1957
+ const projectToolTarget = !choosing && capture?.element.ownerDocument === frameDocument ? capture?.target : void 0;
1958
+ const projectRegionCount = projectToolTarget ? regions.length : 0;
1959
+ const disabled = work !== void 0;
1960
+ useEffect3(() => {
1961
+ if (!mountProjectTools) return;
1962
+ return () => mountProjectTools(null);
1963
+ }, [mountProjectTools]);
1964
+ useEffect3(() => {
1965
+ if (!mountProjectTools || !toolbarSlot) return;
1966
+ mountProjectTools({
1967
+ element: toolbarSlot,
1968
+ props: { target: projectToolTarget ?? null, regionCount: projectRegionCount, disabled }
1969
+ });
1970
+ }, [mountProjectTools, toolbarSlot, projectToolTarget, projectRegionCount, disabled]);
1971
+ return /* @__PURE__ */ jsxs2("div", { className: "magic-edit", ref: dock, "data-vivix-magic-edit-global": global || void 0, children: [
1972
+ /* @__PURE__ */ jsx2("div", { ref: effects, className: "magic-edit-effects", "data-active": open, "aria-hidden": "true", children: /* @__PURE__ */ jsx2("div", { className: "magic-edit-glow" }) }),
1973
+ open && choosing && hovered && /* @__PURE__ */ jsx2(
1974
+ "div",
1975
+ {
1976
+ className: "magic-edit-highlight",
1977
+ "aria-hidden": "true",
1978
+ style: {
1979
+ left: hovered.left,
1980
+ top: hovered.top,
1981
+ width: hovered.width,
1982
+ height: hovered.height
1983
+ }
1984
+ }
1985
+ ),
1986
+ open && /* @__PURE__ */ jsxs2(
1987
+ "section",
1988
+ {
1989
+ ref: composer,
1990
+ className: "magic-edit-content",
1991
+ "aria-label": "Magic Edit",
1992
+ "data-anchored": anchor ? "" : void 0,
1993
+ "data-gesture": pagePreview ? "" : void 0,
1994
+ "data-picking": choosing && !error && !work && !prepared ? "" : void 0,
1995
+ children: [
1996
+ prepared ? /* @__PURE__ */ jsx2(
1997
+ RequestActions,
1998
+ {
1999
+ prepared,
2000
+ onContinue: () => {
2001
+ clearSelection();
2002
+ setChoosing(true);
2003
+ }
2004
+ }
2005
+ ) : /* @__PURE__ */ jsxs2(Fragment, { children: [
2006
+ choosing && /* @__PURE__ */ jsxs2(Fragment, { children: [
2007
+ /* @__PURE__ */ jsx2("p", { className: "magic-edit-feedback", children: tool === "select" ? "Select an element." : "Drag over the area to mark." }),
2008
+ choices.length > 0 && /* @__PURE__ */ jsx2("div", { className: "magic-edit-actions", children: /* @__PURE__ */ jsxs2(
2009
+ "select",
2010
+ {
2011
+ className: "magic-edit-select",
2012
+ "aria-label": "Magic Edit target",
2013
+ value: "",
2014
+ onFocus: () => setChoices(availableTargets()),
2015
+ onChange: (event) => {
2016
+ setChoosing(false);
2017
+ setHovered(void 0);
2018
+ select(event.target.value);
2019
+ },
2020
+ children: [
2021
+ /* @__PURE__ */ jsx2("option", { value: "", children: "Choose a target" }),
2022
+ choices.map((target) => /* @__PURE__ */ jsx2("option", { value: target.id, children: target.label }, target.id))
2023
+ ]
2024
+ }
2025
+ ) })
2026
+ ] }),
2027
+ work === "capture" && /* @__PURE__ */ jsx2("p", { role: "status", children: "Capturing target\u2026" }),
2028
+ !choosing && !capture && selection && !work && /* @__PURE__ */ jsxs2("div", { className: "magic-edit-actions", children: [
2029
+ /* @__PURE__ */ jsx2(
2030
+ Button,
2031
+ {
2032
+ label: "Retry capture",
2033
+ width: "hug",
2034
+ onClick: () => void takeSnapshot(selection, tool === "select")
2035
+ }
2036
+ ),
2037
+ /* @__PURE__ */ jsx2(Button, { label: "Change target", width: "hug", onClick: () => setChoosing(true) })
2038
+ ] }),
2039
+ capture && !choosing && /* @__PURE__ */ jsxs2(Fragment, { children: [
2040
+ /* @__PURE__ */ jsxs2("div", { className: "magic-edit-actions magic-edit-target", children: [
2041
+ /* @__PURE__ */ jsxs2("span", { title: capture.target.label, children: [
2042
+ capture.target.label,
2043
+ capturedTime && ` \xB7 ${capturedTime}`
2044
+ ] }),
2045
+ /* @__PURE__ */ jsx2(
2046
+ Button,
2047
+ {
2048
+ label: "Change target",
2049
+ variant: "text-ghost-sm",
2050
+ width: "hug",
2051
+ disabled: work !== void 0,
2052
+ onClick: () => {
2053
+ setChoices(availableTargets());
2054
+ setChoosing(true);
2055
+ }
2056
+ }
2057
+ )
2058
+ ] }),
2059
+ /* @__PURE__ */ jsx2(
2060
+ "fieldset",
2061
+ {
2062
+ className: "magic-edit-marking",
2063
+ "aria-label": "Mark the target",
2064
+ disabled: work === "prepare",
2065
+ children: /* @__PURE__ */ jsx2(
2066
+ AnnotationEditor,
2067
+ {
2068
+ capture,
2069
+ bounds: captureBounds,
2070
+ tool,
2071
+ regions,
2072
+ pagePreview,
2073
+ onChange: setRegions
2074
+ },
2075
+ tool
2076
+ )
2077
+ }
2078
+ ),
2079
+ /* @__PURE__ */ jsxs2(
2080
+ "form",
2081
+ {
2082
+ className: "magic-edit-prompt",
2083
+ "aria-label": "Magic Edit instruction",
2084
+ onSubmit: (event) => {
2085
+ event.preventDefault();
2086
+ void prepare();
2087
+ },
2088
+ children: [
2089
+ /* @__PURE__ */ jsx2("span", { className: "magic-edit-prompt-tag", children: capture.target.kind === "media" ? "Frame" : "Selection" }),
2090
+ /* @__PURE__ */ jsx2(
2091
+ "input",
2092
+ {
2093
+ ref: prompt,
2094
+ "aria-label": "What should change?",
2095
+ value: instruction,
2096
+ maxLength: 4e3,
2097
+ disabled: work === "prepare",
2098
+ placeholder: "Describe the change",
2099
+ onChange: (event) => setInstruction(event.target.value)
2100
+ }
2101
+ ),
2102
+ /* @__PURE__ */ jsx2(
2103
+ Button,
2104
+ {
2105
+ label: "Copy for Chat",
2106
+ type: "submit",
2107
+ width: "hug",
2108
+ loading: work === "prepare",
2109
+ disabled: !regions.length || !instruction.trim() || work !== void 0
2110
+ }
2111
+ )
2112
+ ]
2113
+ }
2114
+ )
2115
+ ] })
2116
+ ] }),
2117
+ error && /* @__PURE__ */ jsx2("p", { className: "magic-edit-feedback magic-edit-error", role: "alert", children: error })
2118
+ ]
2119
+ }
2120
+ ),
2121
+ /* @__PURE__ */ jsx2(
2122
+ AnnotationToolbar,
2123
+ {
2124
+ active: open,
2125
+ tool,
2126
+ actions: /* @__PURE__ */ jsxs2(Fragment, { children: [
2127
+ tools2?.map(({ id, component: Tool }) => /* @__PURE__ */ jsx2(
2128
+ Tool,
2129
+ {
2130
+ target: choosing ? null : capture?.target ?? null,
2131
+ regionCount: choosing ? 0 : regions.length,
2132
+ disabled
2133
+ },
2134
+ id
2135
+ )),
2136
+ mountProjectTools && /* @__PURE__ */ jsx2("span", { className: "magic-edit-project-tools", ref: setToolbarSlot })
2137
+ ] }),
2138
+ onActiveChange: toggle,
2139
+ onToolChange: (next) => {
2140
+ setHovered(void 0);
2141
+ setTool(next);
2142
+ }
2143
+ }
2144
+ )
2145
+ ] });
2146
+ }
2147
+ function activate(client) {
2148
+ client.registerUI({
2149
+ id: "magic-edit.panel",
2150
+ title: "Magic Edit",
2151
+ kind: "component",
2152
+ component: (props) => /* @__PURE__ */ jsx2(
2153
+ MagicEdit,
2154
+ {
2155
+ ...props,
2156
+ projectId: client.scope?.kind === "user" ? void 0 : client.project.id
2157
+ }
2158
+ )
2159
+ });
2160
+ }
2161
+ export {
2162
+ activate as default
2163
+ };