@tangle-network/agent-app 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/DesignCanvas-3JEEIT6Y.js +10 -0
  2. package/dist/DesignCanvas-3JEEIT6Y.js.map +1 -0
  3. package/dist/DesignCanvasEditor-37LPJIIR.js +9 -0
  4. package/dist/DesignCanvasEditor-37LPJIIR.js.map +1 -0
  5. package/dist/TimelineEditor-OXPJZDP2.js +12 -0
  6. package/dist/TimelineEditor-OXPJZDP2.js.map +1 -0
  7. package/dist/apply-Cp8c3K9D.d.ts +249 -0
  8. package/dist/chunk-2Q73HGDI.js +1743 -0
  9. package/dist/chunk-2Q73HGDI.js.map +1 -0
  10. package/dist/chunk-6UOE5CTA.js +1647 -0
  11. package/dist/chunk-6UOE5CTA.js.map +1 -0
  12. package/dist/chunk-7QCIYDGC.js +1119 -0
  13. package/dist/chunk-7QCIYDGC.js.map +1 -0
  14. package/dist/chunk-A76ZHWNF.js +194 -0
  15. package/dist/chunk-A76ZHWNF.js.map +1 -0
  16. package/dist/chunk-ABGSFUJQ.js +111 -0
  17. package/dist/chunk-ABGSFUJQ.js.map +1 -0
  18. package/dist/{chunk-4YTWB5MG.js → chunk-ETX4O4BB.js} +98 -1
  19. package/dist/chunk-ETX4O4BB.js.map +1 -0
  20. package/dist/chunk-F5KTWRO7.js +2276 -0
  21. package/dist/chunk-F5KTWRO7.js.map +1 -0
  22. package/dist/chunk-IHR6K3GF.js +2367 -0
  23. package/dist/chunk-IHR6K3GF.js.map +1 -0
  24. package/dist/chunk-JZAJE3JL.js +990 -0
  25. package/dist/chunk-JZAJE3JL.js.map +1 -0
  26. package/dist/chunk-ZYBWGSAZ.js +130 -0
  27. package/dist/chunk-ZYBWGSAZ.js.map +1 -0
  28. package/dist/design-canvas/drizzle.d.ts +569 -0
  29. package/dist/design-canvas/drizzle.js +183 -0
  30. package/dist/design-canvas/drizzle.js.map +1 -0
  31. package/dist/design-canvas/index.d.ts +261 -0
  32. package/dist/design-canvas/index.js +96 -0
  33. package/dist/design-canvas/index.js.map +1 -0
  34. package/dist/design-canvas-react/index.d.ts +916 -0
  35. package/dist/design-canvas-react/index.js +423 -0
  36. package/dist/design-canvas-react/index.js.map +1 -0
  37. package/dist/export-presets-Dl5Aa5xj.d.ts +284 -0
  38. package/dist/index.d.ts +11 -2
  39. package/dist/index.js +224 -6
  40. package/dist/mcp-CIupfjxV.d.ts +112 -0
  41. package/dist/mcp-rpc-DLw_r9PQ.d.ts +55 -0
  42. package/dist/model-BHLN208Z.d.ts +183 -0
  43. package/dist/runtime/index.d.ts +108 -1
  44. package/dist/runtime/index.js +7 -1
  45. package/dist/sequences/drizzle.d.ts +1244 -0
  46. package/dist/sequences/drizzle.js +368 -0
  47. package/dist/sequences/drizzle.js.map +1 -0
  48. package/dist/sequences/index.d.ts +331 -0
  49. package/dist/sequences/index.js +114 -0
  50. package/dist/sequences/index.js.map +1 -0
  51. package/dist/sequences-react/index.d.ts +752 -0
  52. package/dist/sequences-react/index.js +241 -0
  53. package/dist/sequences-react/index.js.map +1 -0
  54. package/dist/store-CUStmtdH.d.ts +64 -0
  55. package/dist/store-gckrNq-g.d.ts +242 -0
  56. package/dist/tools/index.d.ts +25 -108
  57. package/dist/tools/index.js +16 -6
  58. package/package.json +62 -2
  59. package/dist/chunk-4YTWB5MG.js.map +0 -1
  60. package/dist/chunk-OLCVUGGI.js +0 -137
  61. package/dist/chunk-OLCVUGGI.js.map +0 -1
@@ -0,0 +1,1743 @@
1
+ import {
2
+ BleedTrimOverlay,
3
+ DesignCanvas,
4
+ addElementCommand,
5
+ createSceneCommandStack,
6
+ deleteElementCommand,
7
+ groupElementsCommand,
8
+ multiSetAttrsCommand,
9
+ setAttrsCommand,
10
+ ungroupElementCommand
11
+ } from "./chunk-F5KTWRO7.js";
12
+ import {
13
+ boundsIntersect,
14
+ elementAabb,
15
+ findElement
16
+ } from "./chunk-JZAJE3JL.js";
17
+
18
+ // src/design-canvas-react/components/Workspace.tsx
19
+ import {
20
+ useCallback,
21
+ useEffect as useEffect4,
22
+ useLayoutEffect,
23
+ useMemo,
24
+ useRef as useRef4,
25
+ useState as useState2
26
+ } from "react";
27
+ import { Stage, Layer as Layer4, Rect as Rect2, Group as Group2 } from "react-konva";
28
+
29
+ // src/design-canvas-react/engine/snap.ts
30
+ var KIND_PRIORITY = {
31
+ "guide": 0,
32
+ "page-edge": 1,
33
+ "page-center": 2,
34
+ "element-edge": 3,
35
+ "element-center": 4,
36
+ "grid": 5
37
+ };
38
+ function createSnapEngine() {
39
+ return {
40
+ collectTargets(state, excludeIds) {
41
+ const page = state.document.pages.find((p) => p.id === state.activePageId);
42
+ if (!page) throw new Error(`collectTargets: active page ${state.activePageId} not found`);
43
+ const vertical = [];
44
+ const horizontal = [];
45
+ const excludeSet = new Set(excludeIds);
46
+ vertical.push({ position: 0, kind: "page-edge" });
47
+ vertical.push({ position: page.width, kind: "page-edge" });
48
+ vertical.push({ position: page.width / 2, kind: "page-center" });
49
+ horizontal.push({ position: 0, kind: "page-edge" });
50
+ horizontal.push({ position: page.height, kind: "page-edge" });
51
+ horizontal.push({ position: page.height / 2, kind: "page-center" });
52
+ for (const pos of page.guides.vertical) {
53
+ vertical.push({ position: pos, kind: "guide" });
54
+ }
55
+ for (const pos of page.guides.horizontal) {
56
+ horizontal.push({ position: pos, kind: "guide" });
57
+ }
58
+ collectElementTargets(page.elements, excludeSet, vertical, horizontal);
59
+ return { vertical, horizontal };
60
+ },
61
+ apply(bounds, targets, thresholdPx, zoom) {
62
+ if (!Number.isFinite(zoom) || zoom <= 0) {
63
+ throw new Error(`snap.apply: zoom must be a positive finite number, got ${zoom}`);
64
+ }
65
+ if (!Number.isFinite(thresholdPx) || thresholdPx < 0) {
66
+ throw new Error(`snap.apply: thresholdPx must be a non-negative finite number, got ${thresholdPx}`);
67
+ }
68
+ const threshold = thresholdPx / zoom;
69
+ const snappedX = snapAxis(bounds.x, bounds.x + bounds.width / 2, bounds.x + bounds.width, targets.vertical, threshold);
70
+ const snappedY = snapAxis(bounds.y, bounds.y + bounds.height / 2, bounds.y + bounds.height, targets.horizontal, threshold);
71
+ return {
72
+ x: snappedX !== null ? snappedX.docPosition - snappedX.elementOffset : bounds.x,
73
+ y: snappedY !== null ? snappedY.docPosition - snappedY.elementOffset : bounds.y,
74
+ activeVertical: snappedX !== null ? snappedX.target : null,
75
+ activeHorizontal: snappedY !== null ? snappedY.target : null
76
+ };
77
+ }
78
+ };
79
+ }
80
+ function collectGridTargets(bounds, gridSize, page, thresholdDocPx) {
81
+ if (!Number.isFinite(gridSize) || gridSize <= 0) {
82
+ throw new Error(`collectGridTargets: gridSize must be positive finite, got ${gridSize}`);
83
+ }
84
+ const vertical = [];
85
+ const horizontal = [];
86
+ const xMin = Math.floor((bounds.x - thresholdDocPx) / gridSize) * gridSize;
87
+ const xMax = Math.ceil((bounds.x + bounds.width + thresholdDocPx) / gridSize) * gridSize;
88
+ for (let x = xMin; x <= xMax; x += gridSize) {
89
+ if (x >= 0 && x <= page.width) vertical.push({ position: x, kind: "grid" });
90
+ }
91
+ const yMin = Math.floor((bounds.y - thresholdDocPx) / gridSize) * gridSize;
92
+ const yMax = Math.ceil((bounds.y + bounds.height + thresholdDocPx) / gridSize) * gridSize;
93
+ for (let y = yMin; y <= yMax; y += gridSize) {
94
+ if (y >= 0 && y <= page.height) horizontal.push({ position: y, kind: "grid" });
95
+ }
96
+ return { vertical, horizontal };
97
+ }
98
+ function snapAxis(start, center, end, targets, threshold) {
99
+ const candidates = [
100
+ { value: start, offset: 0 },
101
+ { value: center, offset: center - start },
102
+ { value: end, offset: end - start }
103
+ ];
104
+ let best = null;
105
+ let bestDistance = Infinity;
106
+ let bestPriority = Infinity;
107
+ for (const { value, offset } of candidates) {
108
+ for (const target of targets) {
109
+ const distance = Math.abs(target.position - value);
110
+ if (distance > threshold) continue;
111
+ const priority = KIND_PRIORITY[target.kind];
112
+ if (distance < bestDistance || distance === bestDistance && priority < bestPriority) {
113
+ bestDistance = distance;
114
+ bestPriority = priority;
115
+ best = { target, docPosition: target.position, elementOffset: offset };
116
+ }
117
+ }
118
+ }
119
+ return best;
120
+ }
121
+ function collectElementTargets(elements, excludeIds, vertical, horizontal) {
122
+ for (const el of elements) {
123
+ if (!el.visible || el.locked) continue;
124
+ if (excludeIds.has(el.id)) continue;
125
+ const aabb = elementAabb(el);
126
+ vertical.push({ position: aabb.x, kind: "element-edge" });
127
+ vertical.push({ position: aabb.x + aabb.width / 2, kind: "element-center" });
128
+ vertical.push({ position: aabb.x + aabb.width, kind: "element-edge" });
129
+ horizontal.push({ position: aabb.y, kind: "element-edge" });
130
+ horizontal.push({ position: aabb.y + aabb.height / 2, kind: "element-center" });
131
+ horizontal.push({ position: aabb.y + aabb.height, kind: "element-edge" });
132
+ if (el.kind === "group") {
133
+ collectElementTargets(el.children, excludeIds, vertical, horizontal);
134
+ }
135
+ }
136
+ }
137
+
138
+ // src/design-canvas-react/engine/zoom-pan.ts
139
+ function createZoomPanMath(config) {
140
+ const { minZoom, maxZoom } = config;
141
+ if (!Number.isFinite(minZoom) || minZoom <= 0) {
142
+ throw new Error(`minZoom must be a positive finite number, got ${minZoom}`);
143
+ }
144
+ if (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {
145
+ throw new Error(`maxZoom must be finite and greater than minZoom (${minZoom}), got ${maxZoom}`);
146
+ }
147
+ return {
148
+ minZoom,
149
+ maxZoom,
150
+ zoomAtPoint(state, factor, screenX, screenY) {
151
+ if (!Number.isFinite(factor) || factor <= 0) {
152
+ throw new Error(`zoomAtPoint: factor must be a positive finite number, got ${factor}`);
153
+ }
154
+ if (!Number.isFinite(screenX) || !Number.isFinite(screenY)) {
155
+ throw new Error(`zoomAtPoint: screenX/screenY must be finite numbers`);
156
+ }
157
+ if (!Number.isFinite(state.zoom) || state.zoom <= 0) {
158
+ throw new Error(`zoomAtPoint: state.zoom must be a positive finite number, got ${state.zoom}`);
159
+ }
160
+ const newZoom = Math.min(maxZoom, Math.max(minZoom, state.zoom * factor));
161
+ const docX = (screenX - state.panX) / state.zoom;
162
+ const docY = (screenY - state.panY) / state.zoom;
163
+ const panX = screenX - docX * newZoom;
164
+ const panY = screenY - docY * newZoom;
165
+ return { zoom: newZoom, panX, panY };
166
+ },
167
+ fitPage(page, viewport, paddingPx = 48) {
168
+ if (!Number.isFinite(page.width) || page.width <= 0) {
169
+ throw new Error(`fitPage: page.width must be a positive finite number, got ${page.width}`);
170
+ }
171
+ if (!Number.isFinite(page.height) || page.height <= 0) {
172
+ throw new Error(`fitPage: page.height must be a positive finite number, got ${page.height}`);
173
+ }
174
+ if (!Number.isFinite(viewport.width) || viewport.width <= 0) {
175
+ throw new Error(`fitPage: viewport.width must be a positive finite number, got ${viewport.width}`);
176
+ }
177
+ if (!Number.isFinite(viewport.height) || viewport.height <= 0) {
178
+ throw new Error(`fitPage: viewport.height must be a positive finite number, got ${viewport.height}`);
179
+ }
180
+ if (!Number.isFinite(paddingPx) || paddingPx < 0) {
181
+ throw new Error(`fitPage: paddingPx must be a non-negative finite number, got ${paddingPx}`);
182
+ }
183
+ const availW = viewport.width - paddingPx * 2;
184
+ const availH = viewport.height - paddingPx * 2;
185
+ const zoom = Math.min(maxZoom, Math.max(minZoom, Math.min(availW / page.width, availH / page.height)));
186
+ const panX = (viewport.width - page.width * zoom) / 2;
187
+ const panY = (viewport.height - page.height * zoom) / 2;
188
+ return { zoom, panX, panY };
189
+ },
190
+ documentToScreen(state, x, y) {
191
+ return { x: x * state.zoom + state.panX, y: y * state.zoom + state.panY };
192
+ },
193
+ screenToDocument(state, x, y) {
194
+ if (!Number.isFinite(state.zoom) || state.zoom === 0) {
195
+ throw new Error(`screenToDocument: zoom must be a non-zero finite number, got ${state.zoom}`);
196
+ }
197
+ return { x: (x - state.panX) / state.zoom, y: (y - state.panY) / state.zoom };
198
+ }
199
+ };
200
+ }
201
+
202
+ // src/design-canvas-react/engine/selection.ts
203
+ function marqueeSelect(page, rect, opts = {}) {
204
+ assertValidBounds(rect, "marqueeSelect rect");
205
+ const result = [];
206
+ collectMarqueeIds(page.elements, rect, opts.requireFullContainment ?? false, result);
207
+ return result;
208
+ }
209
+ function collectMarqueeIds(elements, rect, requireContainment, result) {
210
+ for (const el of elements) {
211
+ if (!el.visible || el.locked) continue;
212
+ const aabb = elementAabb(el);
213
+ if (el.kind === "group") {
214
+ if (boundsIntersect(rect, aabb)) {
215
+ collectMarqueeIds(el.children, rect, requireContainment, result);
216
+ }
217
+ continue;
218
+ }
219
+ const hit = requireContainment ? boundsContain(rect, aabb) : boundsIntersect(rect, aabb);
220
+ if (hit) result.push(el.id);
221
+ }
222
+ }
223
+ function boundsContain(outer, inner) {
224
+ return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
225
+ }
226
+ var NUDGE_NORMAL_PX = 1;
227
+ var NUDGE_SHIFT_PX = 10;
228
+ function nudgeDelta(key, shift) {
229
+ const step = shift ? NUDGE_SHIFT_PX : NUDGE_NORMAL_PX;
230
+ switch (key) {
231
+ case "ArrowLeft":
232
+ return { dx: -step, dy: 0 };
233
+ case "ArrowRight":
234
+ return { dx: step, dy: 0 };
235
+ case "ArrowUp":
236
+ return { dx: 0, dy: -step };
237
+ case "ArrowDown":
238
+ return { dx: 0, dy: step };
239
+ }
240
+ }
241
+ var DUPLICATE_OFFSET = { dx: 10, dy: 10 };
242
+ function assertValidBounds(bounds, label) {
243
+ if (!Number.isFinite(bounds.x) || !Number.isFinite(bounds.y) || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height)) {
244
+ throw new Error(`${label}: all fields must be finite numbers`);
245
+ }
246
+ }
247
+
248
+ // src/design-canvas-react/components/ElementNode.tsx
249
+ import { useEffect, useRef, useState } from "react";
250
+ import { Group, Rect, Ellipse, Line, Text, Image as KonvaImage } from "react-konva";
251
+
252
+ // src/design-canvas-react/components/transform-math.ts
253
+ function bakeRectTransform(node) {
254
+ return {
255
+ x: node.x,
256
+ y: node.y,
257
+ width: Math.abs(node.width * node.scaleX),
258
+ height: Math.abs(node.height * node.scaleY),
259
+ rotation: node.rotation
260
+ };
261
+ }
262
+ function bakeLineTransform(node) {
263
+ const points = node.points.map((v, i) => i % 2 === 0 ? v * node.scaleX : v * node.scaleY);
264
+ return {
265
+ x: node.x,
266
+ y: node.y,
267
+ // Width/height for a line derive from its points; baking is for points only.
268
+ // We still return them so callers have a uniform shape.
269
+ width: node.width * Math.abs(node.scaleX),
270
+ height: node.height * Math.abs(node.scaleY),
271
+ rotation: node.rotation,
272
+ points
273
+ };
274
+ }
275
+ function bakeTextTransform(node) {
276
+ return {
277
+ x: node.x,
278
+ y: node.y,
279
+ width: Math.abs(node.width * node.scaleX),
280
+ // Height is excluded — it re-derives from content.
281
+ height: node.height,
282
+ rotation: node.rotation,
283
+ fontSize: Math.max(1, node.fontSize * Math.abs(node.scaleY))
284
+ };
285
+ }
286
+ function ellipseCenterFromTopLeft(topLeft) {
287
+ return {
288
+ x: topLeft.x + topLeft.width / 2,
289
+ y: topLeft.y + topLeft.height / 2,
290
+ radiusX: topLeft.width / 2,
291
+ radiusY: topLeft.height / 2
292
+ };
293
+ }
294
+ function ellipseTopLeftFromCenter(center) {
295
+ return {
296
+ x: center.x - center.radiusX,
297
+ y: center.y - center.radiusY,
298
+ width: center.radiusX * 2,
299
+ height: center.radiusY * 2
300
+ };
301
+ }
302
+ function normalizeMarquee(startX, startY, endX, endY) {
303
+ const x = Math.min(startX, endX);
304
+ const y = Math.min(startY, endY);
305
+ return {
306
+ x,
307
+ y,
308
+ width: Math.abs(endX - startX),
309
+ height: Math.abs(endY - startY)
310
+ };
311
+ }
312
+ function computeTextOverlayPosition(input) {
313
+ const left = input.panX + input.elementX * input.zoom + input.stageLeft;
314
+ const top = input.panY + input.elementY * input.zoom + input.stageTop;
315
+ return {
316
+ left,
317
+ top,
318
+ width: input.elementWidth * input.zoom,
319
+ fontSize: input.elementFontSize * input.zoom
320
+ };
321
+ }
322
+ var SNAP_ANGLES_DEG = [0, 45, 90, 135, 180, 225, 270, 315, 360];
323
+ function snapRotation(angleDeg, thresholdDeg = 5) {
324
+ const normalized = (angleDeg % 360 + 360) % 360;
325
+ let best = normalized;
326
+ let bestDist = Infinity;
327
+ for (const snap of SNAP_ANGLES_DEG) {
328
+ const dist = Math.abs(normalized - snap);
329
+ if (dist < bestDist) {
330
+ bestDist = dist;
331
+ best = snap % 360;
332
+ }
333
+ }
334
+ return bestDist <= thresholdDeg ? best : normalized;
335
+ }
336
+ function nudgeDelta2(key, shift) {
337
+ const step = shift ? 10 : 1;
338
+ switch (key) {
339
+ case "ArrowLeft":
340
+ return { dx: -step, dy: 0 };
341
+ case "ArrowRight":
342
+ return { dx: step, dy: 0 };
343
+ case "ArrowUp":
344
+ return { dx: 0, dy: -step };
345
+ case "ArrowDown":
346
+ return { dx: 0, dy: step };
347
+ }
348
+ }
349
+ function gridVisible(gridSize, zoom, minScreenPx = 4) {
350
+ return gridSize * zoom >= minScreenPx;
351
+ }
352
+
353
+ // src/design-canvas-react/components/ElementNode.tsx
354
+ import { jsx } from "react/jsx-runtime";
355
+ var IMAGE_CACHE_MAX = 256;
356
+ var imageCache = /* @__PURE__ */ new Map();
357
+ function imageCacheSet(src, img) {
358
+ if (imageCache.size >= IMAGE_CACHE_MAX) {
359
+ const oldest = imageCache.keys().next().value;
360
+ if (oldest !== void 0) imageCache.delete(oldest);
361
+ }
362
+ imageCache.set(src, img);
363
+ }
364
+ function useImage(src) {
365
+ const [, setVersion] = useState(0);
366
+ useEffect(() => {
367
+ if (imageCache.has(src)) return;
368
+ const img = new window.Image();
369
+ img.crossOrigin = "anonymous";
370
+ img.onload = () => {
371
+ imageCacheSet(src, img);
372
+ setVersion((v) => v + 1);
373
+ };
374
+ img.onerror = () => {
375
+ imageCacheSet(src, img);
376
+ setVersion((v) => v + 1);
377
+ };
378
+ img.src = src;
379
+ }, [src]);
380
+ return imageCache.get(src) ?? null;
381
+ }
382
+ function ElementNode(props) {
383
+ const { element } = props;
384
+ if (!element.visible) return null;
385
+ switch (element.kind) {
386
+ case "rect":
387
+ return /* @__PURE__ */ jsx(RectNode, { ...props, element });
388
+ case "ellipse":
389
+ return /* @__PURE__ */ jsx(EllipseNode, { ...props, element });
390
+ case "line":
391
+ return /* @__PURE__ */ jsx(LineNode, { ...props, element });
392
+ case "text":
393
+ return /* @__PURE__ */ jsx(TextNode, { ...props, element });
394
+ case "image":
395
+ return /* @__PURE__ */ jsx(ImageNode, { ...props, element });
396
+ case "video":
397
+ return /* @__PURE__ */ jsx(VideoNode, { ...props, element });
398
+ case "group":
399
+ return /* @__PURE__ */ jsx(GroupNode, { ...props, element });
400
+ }
401
+ }
402
+ function useDragBindings(props, originX, originY) {
403
+ const { element, onClick, onDragStart, onDragMove, onDragEnd, onDoubleClick } = props;
404
+ const isDraggable = !element.locked && !!onDragEnd;
405
+ const originRef = useRef({ x: originX, y: originY });
406
+ originRef.current = { x: originX, y: originY };
407
+ return {
408
+ draggable: isDraggable,
409
+ // locked elements still receive click for selection; listening must be true.
410
+ listening: true,
411
+ onDragStart: isDraggable ? () => {
412
+ onDragStart?.(element.id);
413
+ } : void 0,
414
+ onDragMove: isDraggable ? (e) => {
415
+ onDragMove?.(element.id, e.target.x() - originRef.current.x, e.target.y() - originRef.current.y);
416
+ } : void 0,
417
+ onDragEnd: isDraggable ? (e) => {
418
+ onDragEnd?.(element.id, e.target.x(), e.target.y());
419
+ } : void 0,
420
+ onClick: () => onClick?.(element.id),
421
+ onDblClick: () => onDoubleClick?.(element.id)
422
+ };
423
+ }
424
+ function RectNode({ element, ...rest }) {
425
+ const drag = useDragBindings({ element, ...rest }, element.x, element.y);
426
+ return /* @__PURE__ */ jsx(
427
+ Rect,
428
+ {
429
+ name: element.id,
430
+ x: element.x,
431
+ y: element.y,
432
+ width: element.width,
433
+ height: element.height,
434
+ rotation: element.rotation,
435
+ opacity: element.opacity,
436
+ fill: element.fill,
437
+ stroke: element.stroke,
438
+ strokeWidth: element.strokeWidth,
439
+ cornerRadius: element.cornerRadius ?? 0,
440
+ ...drag
441
+ }
442
+ );
443
+ }
444
+ function EllipseNode({ element, ...rest }) {
445
+ const { x: cx, y: cy, radiusX, radiusY } = ellipseCenterFromTopLeft(element);
446
+ const drag = useDragBindings({ element, ...rest }, element.x, element.y);
447
+ return /* @__PURE__ */ jsx(
448
+ Ellipse,
449
+ {
450
+ name: element.id,
451
+ x: cx,
452
+ y: cy,
453
+ radiusX,
454
+ radiusY,
455
+ rotation: element.rotation,
456
+ opacity: element.opacity,
457
+ fill: element.fill,
458
+ stroke: element.stroke,
459
+ strokeWidth: element.strokeWidth,
460
+ ...drag
461
+ }
462
+ );
463
+ }
464
+ function LineNode({ element, ...rest }) {
465
+ const drag = useDragBindings({ element, ...rest }, element.x, element.y);
466
+ return /* @__PURE__ */ jsx(
467
+ Line,
468
+ {
469
+ name: element.id,
470
+ x: element.x,
471
+ y: element.y,
472
+ points: element.points,
473
+ rotation: element.rotation,
474
+ opacity: element.opacity,
475
+ stroke: element.stroke,
476
+ strokeWidth: element.strokeWidth,
477
+ dash: element.dash,
478
+ ...drag
479
+ }
480
+ );
481
+ }
482
+ var FONT_STYLE_MAP = {
483
+ "normal": { fontStyle: "normal", fontVariant: "normal", fontWeight: "normal" },
484
+ "bold": { fontStyle: "normal", fontVariant: "normal", fontWeight: "bold" },
485
+ "italic": { fontStyle: "italic", fontVariant: "normal", fontWeight: "normal" },
486
+ "bold italic": { fontStyle: "italic", fontVariant: "normal", fontWeight: "bold" }
487
+ };
488
+ function TextNode({ element, ...rest }) {
489
+ const drag = useDragBindings({ element, ...rest }, element.x, element.y);
490
+ const { fontStyle, fontWeight } = FONT_STYLE_MAP[element.fontStyle];
491
+ return /* @__PURE__ */ jsx(
492
+ Text,
493
+ {
494
+ name: element.id,
495
+ x: element.x,
496
+ y: element.y,
497
+ width: element.width,
498
+ rotation: element.rotation,
499
+ opacity: element.opacity,
500
+ text: element.text,
501
+ fontFamily: element.fontFamily,
502
+ fontSize: element.fontSize,
503
+ fontStyle: `${fontStyle} ${fontWeight}`.trim(),
504
+ fill: element.fill,
505
+ align: element.align,
506
+ lineHeight: element.lineHeight,
507
+ letterSpacing: element.letterSpacing,
508
+ ...drag
509
+ }
510
+ );
511
+ }
512
+ var BROKEN_FILL = "#e5e7eb";
513
+ var BROKEN_STROKE = "#9ca3af";
514
+ function ImageNode({ element, ...rest }) {
515
+ const img = useImage(element.src);
516
+ const drag = useDragBindings({ element, ...rest }, element.x, element.y);
517
+ if (!img || !img.complete || img.naturalWidth === 0) {
518
+ return /* @__PURE__ */ jsx(
519
+ Rect,
520
+ {
521
+ name: element.id,
522
+ x: element.x,
523
+ y: element.y,
524
+ width: element.width,
525
+ height: element.height,
526
+ rotation: element.rotation,
527
+ opacity: element.opacity,
528
+ fill: BROKEN_FILL,
529
+ stroke: BROKEN_STROKE,
530
+ strokeWidth: 1,
531
+ ...drag
532
+ }
533
+ );
534
+ }
535
+ let cropProps = {};
536
+ if (element.fit === "cover") {
537
+ const srcAspect = img.naturalWidth / img.naturalHeight;
538
+ const dstAspect = element.width / element.height;
539
+ if (srcAspect > dstAspect) {
540
+ const visibleW = img.naturalHeight * dstAspect;
541
+ cropProps = {
542
+ crop: {
543
+ x: (img.naturalWidth - visibleW) / 2,
544
+ y: 0,
545
+ width: visibleW,
546
+ height: img.naturalHeight
547
+ }
548
+ };
549
+ } else {
550
+ const visibleH = img.naturalWidth / dstAspect;
551
+ cropProps = {
552
+ crop: {
553
+ x: 0,
554
+ y: (img.naturalHeight - visibleH) / 2,
555
+ width: img.naturalWidth,
556
+ height: visibleH
557
+ }
558
+ };
559
+ }
560
+ }
561
+ return /* @__PURE__ */ jsx(
562
+ KonvaImage,
563
+ {
564
+ name: element.id,
565
+ x: element.x,
566
+ y: element.y,
567
+ width: element.width,
568
+ height: element.height,
569
+ rotation: element.rotation,
570
+ opacity: element.opacity,
571
+ image: img,
572
+ ...cropProps,
573
+ ...drag
574
+ }
575
+ );
576
+ }
577
+ function VideoNode({ element, ...rest }) {
578
+ const img = useImage(element.posterSrc ?? "");
579
+ const drag = useDragBindings({ element, ...rest }, element.x, element.y);
580
+ if (!element.posterSrc || !img || !img.complete || img.naturalWidth === 0) {
581
+ return /* @__PURE__ */ jsx(
582
+ Rect,
583
+ {
584
+ name: element.id,
585
+ x: element.x,
586
+ y: element.y,
587
+ width: element.width,
588
+ height: element.height,
589
+ rotation: element.rotation,
590
+ opacity: element.opacity,
591
+ fill: "#1f2937",
592
+ stroke: "#374151",
593
+ strokeWidth: 1,
594
+ ...drag
595
+ }
596
+ );
597
+ }
598
+ return /* @__PURE__ */ jsx(
599
+ KonvaImage,
600
+ {
601
+ name: element.id,
602
+ x: element.x,
603
+ y: element.y,
604
+ width: element.width,
605
+ height: element.height,
606
+ rotation: element.rotation,
607
+ opacity: element.opacity,
608
+ image: img,
609
+ ...drag
610
+ }
611
+ );
612
+ }
613
+ function GroupNode({ element, ...rest }) {
614
+ const drag = useDragBindings({ element, ...rest }, element.x, element.y);
615
+ return /* @__PURE__ */ jsx(
616
+ Group,
617
+ {
618
+ name: element.id,
619
+ x: element.x,
620
+ y: element.y,
621
+ rotation: element.rotation,
622
+ opacity: element.opacity,
623
+ ...drag,
624
+ children: element.children.map((child) => /* @__PURE__ */ jsx(ElementNode, { ...rest, element: child }, child.id))
625
+ }
626
+ );
627
+ }
628
+
629
+ // src/design-canvas-react/components/SelectionLayer.tsx
630
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
631
+ import { Layer, Transformer } from "react-konva";
632
+ import { jsx as jsx2 } from "react/jsx-runtime";
633
+ var MIN_SIZE = 4;
634
+ function SelectionLayer({
635
+ stageRef,
636
+ selectedIds,
637
+ selectedElements,
638
+ canWrite,
639
+ onTransformEnd,
640
+ pageId
641
+ }) {
642
+ const trRef = useRef2(null);
643
+ useEffect2(() => {
644
+ const tr = trRef.current;
645
+ const stage = stageRef.current;
646
+ if (!tr || !stage) return;
647
+ const nodes = [];
648
+ for (const id of selectedIds) {
649
+ const node = stage.findOne(`[name="${id}"]`);
650
+ if (node) nodes.push(node);
651
+ }
652
+ tr.nodes(nodes);
653
+ tr.getLayer()?.batchDraw();
654
+ }, [selectedIds, stageRef]);
655
+ function handleTransformEnd() {
656
+ const tr = trRef.current;
657
+ if (!tr) return;
658
+ const nodes = tr.nodes();
659
+ if (nodes.length === 0) return;
660
+ const entries = [];
661
+ for (const node of nodes) {
662
+ const elementId = node.name();
663
+ const element = selectedElements.find((e) => e.id === elementId);
664
+ if (!element) continue;
665
+ const priorAttrs = elementToPriorAttrs(element);
666
+ const finalAttrs = bakeNodeToAttrs(node, element);
667
+ if (!finalAttrs) continue;
668
+ entries.push({ pageId, elementId, attrs: finalAttrs, priorAttrs });
669
+ }
670
+ if (entries.length > 0) onTransformEnd(entries);
671
+ }
672
+ const multiSelect = selectedIds.length > 1;
673
+ return /* @__PURE__ */ jsx2(Layer, { name: "overlay:selection", listening: false, children: /* @__PURE__ */ jsx2(
674
+ Transformer,
675
+ {
676
+ ref: trRef,
677
+ name: "overlay:transformer",
678
+ keepRatio: multiSelect,
679
+ rotationSnaps: [0, 45, 90, 135, 180, 225, 270, 315],
680
+ rotationSnapTolerance: 5,
681
+ boundBoxFunc: (oldBox, newBox) => {
682
+ if (newBox.width < MIN_SIZE || newBox.height < MIN_SIZE) return oldBox;
683
+ return newBox;
684
+ },
685
+ listening: canWrite,
686
+ onTransformEnd: canWrite ? handleTransformEnd : void 0
687
+ }
688
+ ) });
689
+ }
690
+ function elementToPriorAttrs(element) {
691
+ const base = {
692
+ x: element.x,
693
+ y: element.y,
694
+ rotation: element.rotation
695
+ };
696
+ switch (element.kind) {
697
+ case "rect":
698
+ case "image":
699
+ case "video":
700
+ return { ...base, width: element.width, height: element.height };
701
+ case "ellipse":
702
+ return { ...base, width: element.width, height: element.height };
703
+ case "line":
704
+ return { ...base, points: element.points.slice() };
705
+ case "text":
706
+ return { ...base, width: element.width, fontSize: element.fontSize };
707
+ case "group":
708
+ return { ...base, width: void 0, height: void 0 };
709
+ }
710
+ }
711
+ function bakeNodeToAttrs(node, element) {
712
+ const rawRotation = node.rotation();
713
+ const snappedRotation = snapRotation(rawRotation, 5);
714
+ const baseNode = {
715
+ x: node.x(),
716
+ y: node.y(),
717
+ width: node.width(),
718
+ height: node.height(),
719
+ scaleX: node.scaleX(),
720
+ scaleY: node.scaleY(),
721
+ rotation: snappedRotation
722
+ };
723
+ switch (element.kind) {
724
+ case "rect":
725
+ case "image":
726
+ case "video":
727
+ case "group": {
728
+ const baked = bakeRectTransform(baseNode);
729
+ return {
730
+ x: baked.x,
731
+ y: baked.y,
732
+ width: Math.max(MIN_SIZE, baked.width),
733
+ height: Math.max(MIN_SIZE, baked.height),
734
+ rotation: baked.rotation
735
+ };
736
+ }
737
+ case "ellipse": {
738
+ const baked = bakeRectTransform(baseNode);
739
+ const topLeft = ellipseTopLeftFromCenter({
740
+ x: baked.x,
741
+ y: baked.y,
742
+ // radiusX/radiusY = half of baked width/height
743
+ radiusX: baked.width / 2,
744
+ radiusY: baked.height / 2
745
+ });
746
+ return {
747
+ x: topLeft.x,
748
+ y: topLeft.y,
749
+ width: Math.max(MIN_SIZE, topLeft.width),
750
+ height: Math.max(MIN_SIZE, topLeft.height),
751
+ rotation: baked.rotation
752
+ };
753
+ }
754
+ case "line": {
755
+ const points = node.points();
756
+ const baked = bakeLineTransform({ ...baseNode, points });
757
+ return {
758
+ x: baked.x,
759
+ y: baked.y,
760
+ rotation: baked.rotation,
761
+ points: baked.points
762
+ };
763
+ }
764
+ case "text": {
765
+ const baked = bakeTextTransform({
766
+ ...baseNode,
767
+ fontSize: element.fontSize
768
+ });
769
+ return {
770
+ x: baked.x,
771
+ y: baked.y,
772
+ width: Math.max(MIN_SIZE, baked.width),
773
+ rotation: baked.rotation,
774
+ fontSize: Math.max(1, baked.fontSize)
775
+ };
776
+ }
777
+ }
778
+ }
779
+
780
+ // src/design-canvas-react/components/GridLayer.tsx
781
+ import { Layer as Layer2, Line as Line2 } from "react-konva";
782
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
783
+ function GridLayer({
784
+ pageWidth,
785
+ pageHeight,
786
+ gridSize,
787
+ zoom,
788
+ color = "#c0c0c0",
789
+ opacity = 0.5
790
+ }) {
791
+ if (!gridVisible(gridSize, zoom, 4)) return null;
792
+ const verticals = [];
793
+ for (let x = gridSize; x < pageWidth; x += gridSize) {
794
+ verticals.push(x);
795
+ }
796
+ const horizontals = [];
797
+ for (let y = gridSize; y < pageHeight; y += gridSize) {
798
+ horizontals.push(y);
799
+ }
800
+ return /* @__PURE__ */ jsxs(Layer2, { name: "overlay:grid", listening: false, children: [
801
+ verticals.map((x) => /* @__PURE__ */ jsx3(
802
+ Line2,
803
+ {
804
+ name: "overlay:grid-line",
805
+ points: [x, 0, x, pageHeight],
806
+ stroke: color,
807
+ strokeWidth: 1 / zoom,
808
+ opacity,
809
+ listening: false,
810
+ perfectDrawEnabled: false
811
+ },
812
+ `v-${x}`
813
+ )),
814
+ horizontals.map((y) => /* @__PURE__ */ jsx3(
815
+ Line2,
816
+ {
817
+ name: "overlay:grid-line",
818
+ points: [0, y, pageWidth, y],
819
+ stroke: color,
820
+ strokeWidth: 1 / zoom,
821
+ opacity,
822
+ listening: false,
823
+ perfectDrawEnabled: false
824
+ },
825
+ `h-${y}`
826
+ ))
827
+ ] });
828
+ }
829
+
830
+ // src/design-canvas-react/components/SnapGuidesOverlay.tsx
831
+ import { Layer as Layer3, Line as Line3 } from "react-konva";
832
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
833
+ var KIND_COLOR = {
834
+ "grid": "#a0a0a0",
835
+ "guide": "#3b82f6",
836
+ "page-edge": "#f59e0b",
837
+ "page-center": "#f59e0b",
838
+ "element-edge": "#f43f5e",
839
+ "element-center": "#f43f5e"
840
+ };
841
+ function SnapGuidesOverlay({
842
+ pageWidth,
843
+ pageHeight,
844
+ activeVertical,
845
+ activeHorizontal,
846
+ zoom
847
+ }) {
848
+ if (!activeVertical && !activeHorizontal) return null;
849
+ const strokeWidth = 1 / zoom;
850
+ return /* @__PURE__ */ jsxs2(Layer3, { name: "overlay:snap", listening: false, children: [
851
+ activeVertical && /* @__PURE__ */ jsx4(
852
+ Line3,
853
+ {
854
+ name: "overlay:snap-vertical",
855
+ points: [activeVertical.position, -99999, activeVertical.position, 99999],
856
+ stroke: KIND_COLOR[activeVertical.kind],
857
+ strokeWidth,
858
+ dash: [4 / zoom, 3 / zoom],
859
+ listening: false,
860
+ perfectDrawEnabled: false
861
+ }
862
+ ),
863
+ activeHorizontal && /* @__PURE__ */ jsx4(
864
+ Line3,
865
+ {
866
+ name: "overlay:snap-horizontal",
867
+ points: [-99999, activeHorizontal.position, 99999, activeHorizontal.position],
868
+ stroke: KIND_COLOR[activeHorizontal.kind],
869
+ strokeWidth,
870
+ dash: [4 / zoom, 3 / zoom],
871
+ listening: false,
872
+ perfectDrawEnabled: false
873
+ }
874
+ )
875
+ ] });
876
+ }
877
+
878
+ // src/design-canvas-react/components/InlineTextEditor.tsx
879
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
880
+ import { jsx as jsx5 } from "react/jsx-runtime";
881
+ function InlineTextEditor({
882
+ element,
883
+ zoom,
884
+ panX,
885
+ panY,
886
+ stageRect,
887
+ onCommit,
888
+ onCancel
889
+ }) {
890
+ const ref = useRef3(null);
891
+ const pos = computeTextOverlayPosition({
892
+ elementX: element.x,
893
+ elementY: element.y,
894
+ elementWidth: element.width,
895
+ elementHeight: element.fontSize * element.lineHeight * 4,
896
+ // generous initial height
897
+ zoom,
898
+ panX,
899
+ panY,
900
+ stageLeft: stageRect.left,
901
+ stageTop: stageRect.top,
902
+ elementFontSize: element.fontSize
903
+ });
904
+ useEffect3(() => {
905
+ const el = ref.current;
906
+ if (!el) return;
907
+ el.focus();
908
+ el.select();
909
+ }, []);
910
+ function handleKeyDown(e) {
911
+ if (e.key === "Escape") {
912
+ e.preventDefault();
913
+ onCancel();
914
+ return;
915
+ }
916
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
917
+ e.preventDefault();
918
+ onCommit(ref.current?.value ?? element.text);
919
+ }
920
+ }
921
+ function handleBlur() {
922
+ onCommit(ref.current?.value ?? element.text);
923
+ }
924
+ const fontWeight = element.fontStyle.includes("bold") ? "bold" : "normal";
925
+ const fontStyle = element.fontStyle.includes("italic") ? "italic" : "normal";
926
+ return /* @__PURE__ */ jsx5(
927
+ "textarea",
928
+ {
929
+ ref,
930
+ defaultValue: element.text,
931
+ onKeyDown: handleKeyDown,
932
+ onBlur: handleBlur,
933
+ style: {
934
+ position: "absolute",
935
+ left: pos.left,
936
+ top: pos.top,
937
+ width: pos.width,
938
+ // Height auto-grows via CSS; min so single-line text has room.
939
+ minHeight: pos.fontSize * element.lineHeight * 1.5,
940
+ fontSize: pos.fontSize,
941
+ fontFamily: element.fontFamily,
942
+ fontWeight,
943
+ fontStyle,
944
+ textAlign: element.align,
945
+ lineHeight: element.lineHeight,
946
+ letterSpacing: element.letterSpacing * zoom,
947
+ color: element.fill,
948
+ background: "rgba(255,255,255,0.95)",
949
+ border: "2px solid #3b82f6",
950
+ borderRadius: 2,
951
+ padding: 2,
952
+ resize: "none",
953
+ outline: "none",
954
+ overflow: "hidden",
955
+ boxSizing: "border-box",
956
+ zIndex: 1e3
957
+ // Do NOT apply rotation — see V1 simplification note in module header.
958
+ },
959
+ "aria-label": `Editing text element "${element.name}"`
960
+ }
961
+ );
962
+ }
963
+
964
+ // src/design-canvas-react/components/Workspace.tsx
965
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
966
+ var SNAP_THRESHOLD_SCREEN_PX = 8;
967
+ var DUPLICATE_OFFSET2 = 10;
968
+ var ZOOM_FACTOR_WHEEL = 0.998;
969
+ var ZOOM_MIN = 0.05;
970
+ var ZOOM_MAX = 32;
971
+ var NO_MARQUEE = { active: false, startDocX: 0, startDocY: 0, endDocX: 0, endDocY: 0 };
972
+ function WorkspaceView({
973
+ canWrite,
974
+ onApplyOperations,
975
+ onSelectionChange,
976
+ renderAgentPanel,
977
+ renderSidePanel,
978
+ className,
979
+ stack,
980
+ activePage,
981
+ onFitRef
982
+ }) {
983
+ const [, setTick] = useState2(0);
984
+ const forceRender = useCallback(() => setTick((t) => t + 1), []);
985
+ useEffect4(() => stack.subscribe(forceRender), [stack, forceRender]);
986
+ const state = stack.getState();
987
+ const { document, activePageId, selectedElementIds, zoom, panX, panY, gridEnabled, gridSize, snapEnabled, showBleed } = state;
988
+ const zoomPanMath = useMemo(() => createZoomPanMath({ minZoom: ZOOM_MIN, maxZoom: ZOOM_MAX }), []);
989
+ const snapEngine = useMemo(() => createSnapEngine(), []);
990
+ const containerRef = useRef4(null);
991
+ const [containerSize, setContainerSize] = useState2({ width: 800, height: 600 });
992
+ useLayoutEffect(() => {
993
+ const el = containerRef.current;
994
+ if (!el) return;
995
+ const ro = new ResizeObserver((entries) => {
996
+ const entry = entries[0];
997
+ if (!entry) return;
998
+ const { width, height } = entry.contentRect;
999
+ setContainerSize({ width, height });
1000
+ });
1001
+ ro.observe(el);
1002
+ return () => ro.disconnect();
1003
+ }, []);
1004
+ const stageRef = useRef4(null);
1005
+ useEffect4(() => {
1006
+ if (!onFitRef) return;
1007
+ onFitRef.current = () => {
1008
+ const { width, height } = containerSize;
1009
+ if (width <= 0 || height <= 0) return;
1010
+ const view = zoomPanMath.fitPage(activePage, { width, height });
1011
+ stack.setView(view);
1012
+ };
1013
+ return () => {
1014
+ if (onFitRef.current !== null) onFitRef.current = null;
1015
+ };
1016
+ });
1017
+ const gestureRef = useRef4("idle");
1018
+ const panOriginRef = useRef4({ screenX: 0, screenY: 0, panX: 0, panY: 0 });
1019
+ const [marquee, setMarquee] = useState2(NO_MARQUEE);
1020
+ const marqueeRef = useRef4(NO_MARQUEE);
1021
+ const spaceHeldRef = useRef4(false);
1022
+ const dragOriginRef = useRef4(/* @__PURE__ */ new Map());
1023
+ const [activeSnap, setActiveSnap] = useState2(null);
1024
+ const [editingElementId, setEditingElementId] = useState2(null);
1025
+ const editingPreRef = useRef4("");
1026
+ const nudgeHeldRef = useRef4(null);
1027
+ async function persist(command) {
1028
+ if (!canWrite) return;
1029
+ stack.execute(command);
1030
+ try {
1031
+ const result = await onApplyOperations(command.operations());
1032
+ if (result.document) stack.reset(result.document);
1033
+ } catch {
1034
+ stack.rollback(command);
1035
+ }
1036
+ }
1037
+ function handleWheel(e) {
1038
+ e.preventDefault();
1039
+ const rect = containerRef.current?.getBoundingClientRect();
1040
+ if (!rect) return;
1041
+ const screenX = e.clientX - rect.left;
1042
+ const screenY = e.clientY - rect.top;
1043
+ const factor = Math.pow(ZOOM_FACTOR_WHEEL, e.deltaY);
1044
+ const next = zoomPanMath.zoomAtPoint({ zoom, panX, panY }, factor, screenX, screenY);
1045
+ stack.setView(next);
1046
+ }
1047
+ function handlePointerDown(e) {
1048
+ if (e.button === 1 || spaceHeldRef.current) {
1049
+ e.preventDefault();
1050
+ e.currentTarget.setPointerCapture(e.pointerId);
1051
+ gestureRef.current = "pan";
1052
+ panOriginRef.current = { screenX: e.clientX, screenY: e.clientY, panX, panY };
1053
+ return;
1054
+ }
1055
+ if (e.button !== 0) return;
1056
+ const stage = stageRef.current;
1057
+ const rect = containerRef.current?.getBoundingClientRect();
1058
+ if (!stage || !rect) return;
1059
+ const screenX = e.clientX - rect.left;
1060
+ const screenY = e.clientY - rect.top;
1061
+ const hitNode = stage.getIntersection({ x: screenX, y: screenY });
1062
+ if (hitNode && !hitNode.name().startsWith("overlay:") && hitNode.name() !== "page-background") {
1063
+ return;
1064
+ }
1065
+ e.preventDefault();
1066
+ e.currentTarget.setPointerCapture(e.pointerId);
1067
+ gestureRef.current = "marquee";
1068
+ const docPos = zoomPanMath.screenToDocument({ zoom, panX, panY }, screenX, screenY);
1069
+ const m = { active: true, startDocX: docPos.x, startDocY: docPos.y, endDocX: docPos.x, endDocY: docPos.y };
1070
+ marqueeRef.current = m;
1071
+ setMarquee(m);
1072
+ stack.setView({ selectedElementIds: [] });
1073
+ }
1074
+ function handlePointerMove(e) {
1075
+ const mode = gestureRef.current;
1076
+ const rect = containerRef.current?.getBoundingClientRect();
1077
+ if (!rect) return;
1078
+ if (mode === "pan") {
1079
+ const dx = e.clientX - panOriginRef.current.screenX;
1080
+ const dy = e.clientY - panOriginRef.current.screenY;
1081
+ stack.setView({ panX: panOriginRef.current.panX + dx, panY: panOriginRef.current.panY + dy });
1082
+ return;
1083
+ }
1084
+ if (mode === "marquee") {
1085
+ const screenX = e.clientX - rect.left;
1086
+ const screenY = e.clientY - rect.top;
1087
+ const docPos = zoomPanMath.screenToDocument({ zoom, panX, panY }, screenX, screenY);
1088
+ const m = { ...marqueeRef.current, active: true, endDocX: docPos.x, endDocY: docPos.y };
1089
+ marqueeRef.current = m;
1090
+ setMarquee(m);
1091
+ const normalized = normalizeMarquee(m.startDocX, m.startDocY, m.endDocX, m.endDocY);
1092
+ const ids = marqueeSelect(activePage, normalized);
1093
+ stack.setView({ selectedElementIds: ids });
1094
+ }
1095
+ }
1096
+ function handlePointerUp(e) {
1097
+ gestureRef.current = "idle";
1098
+ setMarquee(NO_MARQUEE);
1099
+ marqueeRef.current = NO_MARQUEE;
1100
+ setActiveSnap(null);
1101
+ e.currentTarget.releasePointerCapture(e.pointerId);
1102
+ }
1103
+ function handleElementDragStart(elementId) {
1104
+ if (!canWrite) return;
1105
+ const ids = selectedElementIds.includes(elementId) ? selectedElementIds : [elementId];
1106
+ const origins = /* @__PURE__ */ new Map();
1107
+ for (const id of ids) {
1108
+ const found = findElement(activePage, id);
1109
+ if (found) {
1110
+ const aabb = elementAabb(found.element);
1111
+ origins.set(id, { x: found.element.x, y: found.element.y, width: aabb.width, height: aabb.height });
1112
+ }
1113
+ }
1114
+ dragOriginRef.current = origins;
1115
+ gestureRef.current = "drag";
1116
+ if (!selectedElementIds.includes(elementId)) {
1117
+ stack.setView({ selectedElementIds: [elementId] });
1118
+ }
1119
+ }
1120
+ function handleElementDragMove(elementId, dx, dy) {
1121
+ if (!canWrite) return;
1122
+ const origin = dragOriginRef.current.get(elementId);
1123
+ if (!origin) return;
1124
+ const dragging = selectedElementIds.includes(elementId) ? selectedElementIds : [elementId];
1125
+ const proposedX = origin.x + dx;
1126
+ const proposedY = origin.y + dy;
1127
+ const snapBounds = { x: proposedX, y: proposedY, width: origin.width, height: origin.height };
1128
+ if (snapEnabled) {
1129
+ const targets = snapEngine.collectTargets(state, dragging);
1130
+ if (gridEnabled) {
1131
+ const thresholdDoc = SNAP_THRESHOLD_SCREEN_PX / zoom;
1132
+ const gt = collectGridTargets(snapBounds, gridSize, activePage, thresholdDoc);
1133
+ targets.vertical.push(...gt.vertical);
1134
+ targets.horizontal.push(...gt.horizontal);
1135
+ }
1136
+ const snapResult = snapEngine.apply(snapBounds, targets, SNAP_THRESHOLD_SCREEN_PX, zoom);
1137
+ setActiveSnap(snapResult);
1138
+ }
1139
+ }
1140
+ async function handleElementDragEnd(elementId, finalX, finalY) {
1141
+ if (!canWrite) return;
1142
+ gestureRef.current = "idle";
1143
+ setActiveSnap(null);
1144
+ const origin = dragOriginRef.current.get(elementId);
1145
+ if (!origin) return;
1146
+ const dx = finalX - origin.x;
1147
+ const dy = finalY - origin.y;
1148
+ const draggingIds = selectedElementIds.includes(elementId) ? selectedElementIds : [elementId];
1149
+ if (draggingIds.length === 1) {
1150
+ const el = findElement(activePage, elementId)?.element;
1151
+ if (!el) return;
1152
+ await persist(
1153
+ setAttrsCommand({
1154
+ pageId: activePageId,
1155
+ elementId,
1156
+ attrs: { x: finalX, y: finalY },
1157
+ priorAttrs: { x: el.x, y: el.y }
1158
+ })
1159
+ );
1160
+ } else {
1161
+ const entries = [];
1162
+ for (const id of draggingIds) {
1163
+ const el = findElement(activePage, id)?.element;
1164
+ const orig = dragOriginRef.current.get(id);
1165
+ if (!el || !orig) continue;
1166
+ entries.push({
1167
+ pageId: activePageId,
1168
+ elementId: id,
1169
+ attrs: { x: orig.x + dx, y: orig.y + dy },
1170
+ priorAttrs: { x: orig.x, y: orig.y }
1171
+ });
1172
+ }
1173
+ if (entries.length > 0) await persist(multiSetAttrsCommand(entries));
1174
+ }
1175
+ dragOriginRef.current = /* @__PURE__ */ new Map();
1176
+ }
1177
+ function handleElementClick(elementId) {
1178
+ stack.setView({ selectedElementIds: [elementId] });
1179
+ }
1180
+ function handleElementDoubleClick(elementId) {
1181
+ if (!canWrite) return;
1182
+ const found = findElement(activePage, elementId);
1183
+ if (!found || found.element.kind !== "text") return;
1184
+ editingPreRef.current = found.element.text;
1185
+ setEditingElementId(elementId);
1186
+ }
1187
+ async function handleTextCommit(text) {
1188
+ const id = editingElementId;
1189
+ setEditingElementId(null);
1190
+ if (!id || !canWrite) return;
1191
+ const found = findElement(activePage, id);
1192
+ if (!found || found.element.kind !== "text") return;
1193
+ if (text === editingPreRef.current) return;
1194
+ await persist(
1195
+ setAttrsCommand({
1196
+ pageId: activePageId,
1197
+ elementId: id,
1198
+ attrs: { text },
1199
+ priorAttrs: { text: editingPreRef.current }
1200
+ })
1201
+ );
1202
+ }
1203
+ function handleTextCancel() {
1204
+ setEditingElementId(null);
1205
+ }
1206
+ async function handleTransformEnd(entries) {
1207
+ if (!canWrite || entries.length === 0) return;
1208
+ await persist(multiSetAttrsCommand(entries));
1209
+ }
1210
+ function handleKeyDown(e) {
1211
+ if (e.key === " ") {
1212
+ e.preventDefault();
1213
+ spaceHeldRef.current = true;
1214
+ return;
1215
+ }
1216
+ const mod = e.metaKey || e.ctrlKey;
1217
+ if (e.key === "Escape") {
1218
+ e.preventDefault();
1219
+ if (gestureRef.current !== "idle") {
1220
+ gestureRef.current = "idle";
1221
+ setMarquee(NO_MARQUEE);
1222
+ setActiveSnap(null);
1223
+ } else if (editingElementId) {
1224
+ setEditingElementId(null);
1225
+ } else {
1226
+ stack.setView({ selectedElementIds: [] });
1227
+ }
1228
+ return;
1229
+ }
1230
+ if (editingElementId) return;
1231
+ if ((e.key === "Delete" || e.key === "Backspace") && selectedElementIds.length > 0) {
1232
+ e.preventDefault();
1233
+ for (const id of [...selectedElementIds]) {
1234
+ try {
1235
+ persist(deleteElementCommand({ document, pageId: activePageId, elementId: id }));
1236
+ } catch {
1237
+ }
1238
+ }
1239
+ stack.setView({ selectedElementIds: [] });
1240
+ return;
1241
+ }
1242
+ if (mod && e.key === "z" && !e.shiftKey) {
1243
+ e.preventDefault();
1244
+ if (stack.canUndo()) stack.undo();
1245
+ return;
1246
+ }
1247
+ if (mod && (e.key === "z" && e.shiftKey || e.key === "y")) {
1248
+ e.preventDefault();
1249
+ if (stack.canRedo()) stack.redo();
1250
+ return;
1251
+ }
1252
+ if (mod && e.key === "a") {
1253
+ e.preventDefault();
1254
+ const ids = activePage.elements.filter((el) => !el.locked && el.visible).map((el) => el.id);
1255
+ stack.setView({ selectedElementIds: ids });
1256
+ return;
1257
+ }
1258
+ if (mod && e.key === "d" && selectedElementIds.length > 0) {
1259
+ e.preventDefault();
1260
+ for (const id of selectedElementIds) {
1261
+ const found = findElement(activePage, id);
1262
+ if (!found) continue;
1263
+ const clone = {
1264
+ ...structuredClone(found.element),
1265
+ id: crypto.randomUUID(),
1266
+ x: found.element.x + DUPLICATE_OFFSET2,
1267
+ y: found.element.y + DUPLICATE_OFFSET2
1268
+ };
1269
+ persist(addElementCommand({ pageId: activePageId, element: clone }));
1270
+ }
1271
+ return;
1272
+ }
1273
+ if (mod && !e.shiftKey && e.key === "g" && selectedElementIds.length >= 2) {
1274
+ e.preventDefault();
1275
+ persist(groupElementsCommand({
1276
+ document,
1277
+ pageId: activePageId,
1278
+ elementIds: selectedElementIds,
1279
+ groupId: crypto.randomUUID()
1280
+ }));
1281
+ return;
1282
+ }
1283
+ if (mod && e.shiftKey && e.key === "g" && selectedElementIds.length === 1) {
1284
+ e.preventDefault();
1285
+ const id = selectedElementIds[0];
1286
+ persist(ungroupElementCommand({ document, pageId: activePageId, groupId: id }));
1287
+ return;
1288
+ }
1289
+ if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key) && selectedElementIds.length > 0) {
1290
+ e.preventDefault();
1291
+ const key = e.key;
1292
+ if (nudgeHeldRef.current && nudgeHeldRef.current.key !== key) {
1293
+ flushNudge();
1294
+ }
1295
+ if (!nudgeHeldRef.current) {
1296
+ const origins = /* @__PURE__ */ new Map();
1297
+ for (const id of selectedElementIds) {
1298
+ const found = findElement(activePage, id);
1299
+ if (found) origins.set(id, { x: found.element.x, y: found.element.y });
1300
+ }
1301
+ nudgeHeldRef.current = { key, ids: selectedElementIds.slice(), origin: origins };
1302
+ }
1303
+ const { dx, dy } = nudgeDelta2(key, e.shiftKey);
1304
+ const entries = [];
1305
+ for (const [id, orig] of nudgeHeldRef.current.origin) {
1306
+ const found = findElement(activePage, id);
1307
+ if (!found) continue;
1308
+ entries.push({
1309
+ pageId: activePageId,
1310
+ elementId: id,
1311
+ attrs: { x: found.element.x + dx, y: found.element.y + dy },
1312
+ priorAttrs: { x: found.element.x, y: found.element.y }
1313
+ });
1314
+ }
1315
+ if (entries.length > 0) {
1316
+ stack.execute(multiSetAttrsCommand(entries));
1317
+ }
1318
+ }
1319
+ }
1320
+ function handleKeyUp(e) {
1321
+ if (e.key === " ") {
1322
+ spaceHeldRef.current = false;
1323
+ return;
1324
+ }
1325
+ if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
1326
+ flushNudge();
1327
+ }
1328
+ }
1329
+ function flushNudge() {
1330
+ const held = nudgeHeldRef.current;
1331
+ if (!held) return;
1332
+ nudgeHeldRef.current = null;
1333
+ if (!canWrite || held.ids.length === 0) return;
1334
+ const entries = [];
1335
+ for (const id of held.ids) {
1336
+ const orig = held.origin.get(id);
1337
+ const found = findElement(activePage, id);
1338
+ if (!orig || !found) continue;
1339
+ entries.push({
1340
+ pageId: activePageId,
1341
+ elementId: id,
1342
+ attrs: { x: found.element.x, y: found.element.y },
1343
+ priorAttrs: { x: orig.x, y: orig.y }
1344
+ });
1345
+ }
1346
+ if (entries.length > 0) {
1347
+ onApplyOperations(multiSetAttrsCommand(entries).operations()).catch(() => {
1348
+ for (const [id, orig] of held.origin) {
1349
+ const found = findElement(activePage, id);
1350
+ if (!found) continue;
1351
+ persist(setAttrsCommand({
1352
+ pageId: activePageId,
1353
+ elementId: id,
1354
+ attrs: { x: orig.x, y: orig.y },
1355
+ priorAttrs: { x: found.element.x, y: found.element.y }
1356
+ }));
1357
+ }
1358
+ });
1359
+ }
1360
+ }
1361
+ useEffect4(() => {
1362
+ if (!onSelectionChange) return;
1363
+ const elements = selectedElementIds.map((id) => findElement(activePage, id)?.element).filter((el) => !!el);
1364
+ onSelectionChange(elements);
1365
+ }, [selectedElementIds, activePage, onSelectionChange]);
1366
+ const selectedElements = useMemo(
1367
+ () => selectedElementIds.map((id) => findElement(activePage, id)?.element).filter((el) => !!el),
1368
+ [selectedElementIds, activePage]
1369
+ );
1370
+ const editingTextElement = editingElementId ? findElement(activePage, editingElementId)?.element ?? null : null;
1371
+ const stageRect = containerRef.current?.getBoundingClientRect() ?? { left: 0, top: 0 };
1372
+ const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
1373
+ const pageScreenX = panX;
1374
+ const pageScreenY = panY;
1375
+ const pageScreenW = activePage.width * zoom;
1376
+ const pageScreenH = activePage.height * zoom;
1377
+ const activeVerticalGuide = activeSnap?.activeVertical ?? null;
1378
+ const activeHorizontalGuide = activeSnap?.activeHorizontal ?? null;
1379
+ const normalizedMarquee = marquee.active ? normalizeMarquee(marquee.startDocX, marquee.startDocY, marquee.endDocX, marquee.endDocY) : null;
1380
+ return /* @__PURE__ */ jsxs3(
1381
+ "div",
1382
+ {
1383
+ className: `design-canvas-workspace relative overflow-hidden bg-[#1a1a1a] outline-none ${className ?? ""}`,
1384
+ ref: containerRef,
1385
+ tabIndex: 0,
1386
+ onWheel: handleWheel,
1387
+ onPointerDown: handlePointerDown,
1388
+ onPointerMove: handlePointerMove,
1389
+ onPointerUp: handlePointerUp,
1390
+ onKeyDown: handleKeyDown,
1391
+ onKeyUp: handleKeyUp,
1392
+ style: { cursor: spaceHeldRef.current ? "grab" : "default" },
1393
+ children: [
1394
+ renderSidePanel && /* @__PURE__ */ jsx6("div", { className: "absolute left-0 top-0 bottom-0 z-20 w-60 pointer-events-auto", children: renderSidePanel() }),
1395
+ /* @__PURE__ */ jsxs3(
1396
+ Stage,
1397
+ {
1398
+ ref: stageRef,
1399
+ width: containerSize.width,
1400
+ height: containerSize.height,
1401
+ pixelRatio: dpr,
1402
+ listening: true,
1403
+ children: [
1404
+ gridEnabled && /* @__PURE__ */ jsx6(
1405
+ GridLayer,
1406
+ {
1407
+ pageWidth: activePage.width,
1408
+ pageHeight: activePage.height,
1409
+ gridSize,
1410
+ zoom
1411
+ }
1412
+ ),
1413
+ /* @__PURE__ */ jsx6(Layer4, { children: /* @__PURE__ */ jsxs3(Group2, { x: panX, y: panY, scaleX: zoom, scaleY: zoom, children: [
1414
+ /* @__PURE__ */ jsx6(
1415
+ Rect2,
1416
+ {
1417
+ name: "page-background",
1418
+ x: 0,
1419
+ y: 0,
1420
+ width: activePage.width,
1421
+ height: activePage.height,
1422
+ fill: activePage.background,
1423
+ shadowColor: "rgba(0,0,0,0.4)",
1424
+ shadowBlur: 24 / zoom,
1425
+ shadowOffset: { x: 0, y: 4 / zoom },
1426
+ listening: false
1427
+ }
1428
+ ),
1429
+ /* @__PURE__ */ jsx6(
1430
+ Group2,
1431
+ {
1432
+ clipX: 0,
1433
+ clipY: 0,
1434
+ clipWidth: activePage.width,
1435
+ clipHeight: activePage.height,
1436
+ children: activePage.elements.map((element) => /* @__PURE__ */ jsx6(
1437
+ ElementNode,
1438
+ {
1439
+ element,
1440
+ isSelected: selectedElementIds.includes(element.id),
1441
+ zoom,
1442
+ onClick: handleElementClick,
1443
+ onDragStart: handleElementDragStart,
1444
+ onDragMove: handleElementDragMove,
1445
+ onDragEnd: handleElementDragEnd,
1446
+ onDoubleClick: handleElementDoubleClick
1447
+ },
1448
+ element.id
1449
+ ))
1450
+ }
1451
+ )
1452
+ ] }) }),
1453
+ (activeVerticalGuide || activeHorizontalGuide) && /* @__PURE__ */ jsx6(Layer4, { children: /* @__PURE__ */ jsx6(Group2, { x: panX, y: panY, scaleX: zoom, scaleY: zoom, children: /* @__PURE__ */ jsx6(
1454
+ SnapGuidesOverlay,
1455
+ {
1456
+ pageWidth: activePage.width,
1457
+ pageHeight: activePage.height,
1458
+ activeVertical: activeVerticalGuide,
1459
+ activeHorizontal: activeHorizontalGuide,
1460
+ zoom
1461
+ }
1462
+ ) }) }),
1463
+ /* @__PURE__ */ jsx6(
1464
+ SelectionLayer,
1465
+ {
1466
+ stageRef,
1467
+ selectedIds: selectedElementIds,
1468
+ selectedElements,
1469
+ canWrite,
1470
+ onTransformEnd: handleTransformEnd,
1471
+ pageId: activePageId
1472
+ }
1473
+ )
1474
+ ]
1475
+ }
1476
+ ),
1477
+ showBleed && activePage.bleed && /* @__PURE__ */ jsx6(
1478
+ "div",
1479
+ {
1480
+ className: "pointer-events-none absolute",
1481
+ style: { left: pageScreenX, top: pageScreenY, width: pageScreenW, height: pageScreenH },
1482
+ children: /* @__PURE__ */ jsx6(
1483
+ BleedTrimOverlay,
1484
+ {
1485
+ pageWidthPx: pageScreenW,
1486
+ pageHeightPx: pageScreenH,
1487
+ bleed: {
1488
+ top: activePage.bleed.top * zoom,
1489
+ right: activePage.bleed.right * zoom,
1490
+ bottom: activePage.bleed.bottom * zoom,
1491
+ left: activePage.bleed.left * zoom
1492
+ }
1493
+ }
1494
+ )
1495
+ }
1496
+ ),
1497
+ normalizedMarquee && /* @__PURE__ */ jsx6(
1498
+ "div",
1499
+ {
1500
+ className: "pointer-events-none absolute border border-blue-400 bg-blue-400/10",
1501
+ style: {
1502
+ left: panX + normalizedMarquee.x * zoom,
1503
+ top: panY + normalizedMarquee.y * zoom,
1504
+ width: normalizedMarquee.width * zoom,
1505
+ height: normalizedMarquee.height * zoom
1506
+ }
1507
+ }
1508
+ ),
1509
+ editingTextElement && /* @__PURE__ */ jsx6(
1510
+ InlineTextEditor,
1511
+ {
1512
+ element: editingTextElement,
1513
+ zoom,
1514
+ panX,
1515
+ panY,
1516
+ stageRect: { left: stageRect.left, top: stageRect.top },
1517
+ onCommit: handleTextCommit,
1518
+ onCancel: handleTextCancel
1519
+ }
1520
+ ),
1521
+ renderAgentPanel && /* @__PURE__ */ jsx6("div", { className: "absolute right-0 top-0 bottom-0 z-20 w-80 pointer-events-auto", children: renderAgentPanel({ selectedElements, activePageId }) })
1522
+ ]
1523
+ }
1524
+ );
1525
+ }
1526
+ function Workspace(props) {
1527
+ const stackRef = useRef4(
1528
+ createSceneCommandStack(
1529
+ props.document,
1530
+ props.document.pages[0]?.id ?? ""
1531
+ )
1532
+ );
1533
+ const [, setTick] = useState2(0);
1534
+ const forceRender = useCallback(() => setTick((t) => t + 1), []);
1535
+ useEffect4(() => {
1536
+ return stackRef.current.subscribe(forceRender);
1537
+ }, [forceRender]);
1538
+ const prevRevRef = useRef4(props.rev);
1539
+ useEffect4(() => {
1540
+ if (props.rev !== prevRevRef.current) {
1541
+ prevRevRef.current = props.rev;
1542
+ stackRef.current.reset(props.document);
1543
+ }
1544
+ }, [props.rev, props.document]);
1545
+ const state = stackRef.current.getState();
1546
+ const activePage = state.document.pages.find((p) => p.id === state.activePageId) ?? state.document.pages[0];
1547
+ if (!activePage) return null;
1548
+ return /* @__PURE__ */ jsx6(
1549
+ WorkspaceView,
1550
+ {
1551
+ stack: stackRef.current,
1552
+ activePage,
1553
+ canWrite: props.canWrite,
1554
+ onApplyOperations: props.onApplyOperations,
1555
+ onSelectionChange: props.onSelectionChange,
1556
+ renderAgentPanel: props.renderAgentPanel,
1557
+ renderSidePanel: props.renderSidePanel,
1558
+ className: props.className
1559
+ }
1560
+ );
1561
+ }
1562
+
1563
+ // src/design-canvas-react/components/DesignCanvasEditor.tsx
1564
+ import { jsx as jsx7 } from "react/jsx-runtime";
1565
+ var THUMBNAIL_HEIGHT_PX = 96;
1566
+ var thumbnailCache = /* @__PURE__ */ new Map();
1567
+ var THUMBNAIL_CACHE_LIMIT = 200;
1568
+ function evictIfNeeded() {
1569
+ if (thumbnailCache.size < THUMBNAIL_CACHE_LIMIT) return;
1570
+ const toDrop = Math.floor(THUMBNAIL_CACHE_LIMIT * 0.2);
1571
+ let dropped = 0;
1572
+ for (const key of thumbnailCache.keys()) {
1573
+ if (dropped >= toDrop) break;
1574
+ thumbnailCache.delete(key);
1575
+ dropped++;
1576
+ }
1577
+ }
1578
+ function cheapHash(value) {
1579
+ const s = JSON.stringify(value) ?? "";
1580
+ let h = 2166136261;
1581
+ for (let i = 0; i < s.length; i++) {
1582
+ h ^= s.charCodeAt(i);
1583
+ h = Math.imul(h, 16777619);
1584
+ }
1585
+ return (h >>> 0).toString(36);
1586
+ }
1587
+ async function renderPageThumbnail(page) {
1588
+ const cacheKey = `${page.id}:${cheapHash(page.elements)}`;
1589
+ const cached = thumbnailCache.get(cacheKey);
1590
+ if (cached !== void 0) return cached;
1591
+ let Konva;
1592
+ try {
1593
+ const mod = await import("konva");
1594
+ Konva = mod.default;
1595
+ } catch {
1596
+ return null;
1597
+ }
1598
+ const aspectRatio = page.width > 0 ? page.width / page.height : 1;
1599
+ const thumbH = THUMBNAIL_HEIGHT_PX;
1600
+ const thumbW = Math.round(thumbH * aspectRatio);
1601
+ const scale = page.height > 0 ? thumbH / page.height : 1;
1602
+ let stage = null;
1603
+ try {
1604
+ const container = globalThis.document?.createElement("div");
1605
+ if (!container) return null;
1606
+ container.style.position = "absolute";
1607
+ container.style.left = "-9999px";
1608
+ container.style.top = "-9999px";
1609
+ globalThis.document.body.appendChild(container);
1610
+ stage = new Konva.Stage({ container, width: thumbW, height: thumbH });
1611
+ const layer = new Konva.Layer();
1612
+ stage.add(layer);
1613
+ layer.add(new Konva.Rect({
1614
+ x: 0,
1615
+ y: 0,
1616
+ width: thumbW,
1617
+ height: thumbH,
1618
+ fill: page.background,
1619
+ listening: false
1620
+ }));
1621
+ const group = new Konva.Group({ x: 0, y: 0, scaleX: scale, scaleY: scale, listening: false });
1622
+ layer.add(group);
1623
+ paintElements(Konva, group, page.elements);
1624
+ const dataUrl = stage.toDataURL({ mimeType: "image/png", pixelRatio: 1 });
1625
+ evictIfNeeded();
1626
+ thumbnailCache.set(cacheKey, dataUrl);
1627
+ return dataUrl;
1628
+ } catch {
1629
+ return null;
1630
+ } finally {
1631
+ if (stage) {
1632
+ stage.destroy();
1633
+ const el = stage.container();
1634
+ el.parentNode?.removeChild(el);
1635
+ }
1636
+ }
1637
+ }
1638
+ function paintElements(Konva, parent, elements) {
1639
+ for (const el of elements) {
1640
+ if (!el.visible) continue;
1641
+ switch (el.kind) {
1642
+ case "rect":
1643
+ parent.add(new Konva.Rect({
1644
+ x: el.x,
1645
+ y: el.y,
1646
+ width: el.width,
1647
+ height: el.height,
1648
+ rotation: el.rotation,
1649
+ opacity: el.opacity,
1650
+ fill: el.fill ?? void 0,
1651
+ cornerRadius: el.cornerRadius ?? 0,
1652
+ listening: false
1653
+ }));
1654
+ break;
1655
+ case "ellipse":
1656
+ parent.add(new Konva.Ellipse({
1657
+ x: el.x + el.width / 2,
1658
+ y: el.y + el.height / 2,
1659
+ radiusX: el.width / 2,
1660
+ radiusY: el.height / 2,
1661
+ rotation: el.rotation,
1662
+ opacity: el.opacity,
1663
+ fill: el.fill ?? void 0,
1664
+ listening: false
1665
+ }));
1666
+ break;
1667
+ case "text":
1668
+ parent.add(new Konva.Text({
1669
+ x: el.x,
1670
+ y: el.y,
1671
+ width: el.width,
1672
+ rotation: el.rotation,
1673
+ opacity: el.opacity,
1674
+ text: el.text,
1675
+ fontSize: el.fontSize,
1676
+ fontFamily: el.fontFamily,
1677
+ fill: el.fill,
1678
+ align: el.align,
1679
+ listening: false
1680
+ }));
1681
+ break;
1682
+ case "group": {
1683
+ const g = new Konva.Group({
1684
+ x: el.x,
1685
+ y: el.y,
1686
+ rotation: el.rotation,
1687
+ opacity: el.opacity,
1688
+ listening: false
1689
+ });
1690
+ parent.add(g);
1691
+ paintElements(Konva, g, el.children);
1692
+ break;
1693
+ }
1694
+ // image and video: skip — async loading not viable in sync thumbnail render
1695
+ default:
1696
+ break;
1697
+ }
1698
+ }
1699
+ }
1700
+ function DesignCanvasEditor(props) {
1701
+ return /* @__PURE__ */ jsx7(
1702
+ DesignCanvas,
1703
+ {
1704
+ ...props,
1705
+ renderWorkspace: (ctx) => {
1706
+ if (!ctx.activePage) return null;
1707
+ return /* @__PURE__ */ jsx7(
1708
+ WorkspaceView,
1709
+ {
1710
+ stack: ctx.stack,
1711
+ activePage: ctx.activePage,
1712
+ canWrite: ctx.canWrite,
1713
+ onApplyOperations: props.onApplyOperations,
1714
+ onSelectionChange: props.onSelectionChange,
1715
+ renderAgentPanel: props.renderAgentPanel,
1716
+ renderSidePanel: props.renderSidePanel,
1717
+ onFitRef: ctx.onFitRef
1718
+ }
1719
+ );
1720
+ },
1721
+ renderThumbnail: renderPageThumbnail
1722
+ }
1723
+ );
1724
+ }
1725
+
1726
+ export {
1727
+ createSnapEngine,
1728
+ collectGridTargets,
1729
+ marqueeSelect,
1730
+ nudgeDelta,
1731
+ DUPLICATE_OFFSET,
1732
+ createZoomPanMath,
1733
+ bakeRectTransform,
1734
+ bakeLineTransform,
1735
+ bakeTextTransform,
1736
+ ElementNode,
1737
+ SelectionLayer,
1738
+ GridLayer,
1739
+ WorkspaceView,
1740
+ Workspace,
1741
+ DesignCanvasEditor
1742
+ };
1743
+ //# sourceMappingURL=chunk-2Q73HGDI.js.map