@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,2276 @@
1
+ import {
2
+ SIZE_PRESETS,
3
+ applySceneOperation,
4
+ applySceneOperations,
5
+ findElement,
6
+ matchPreset,
7
+ requireElement,
8
+ requirePage
9
+ } from "./chunk-JZAJE3JL.js";
10
+
11
+ // src/design-canvas-react/components/DesignCanvas.tsx
12
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo, useRef as useRef4, useState as useState4, useSyncExternalStore } from "react";
13
+
14
+ // src/design-canvas-react/engine/command-stack.ts
15
+ var SCENE_COMMAND_HISTORY_LIMIT = 200;
16
+ function createSceneCommandStack(document, activePageId) {
17
+ let state = {
18
+ document,
19
+ activePageId,
20
+ selectedElementIds: [],
21
+ zoom: 1,
22
+ panX: 0,
23
+ panY: 0,
24
+ gridEnabled: false,
25
+ gridSize: 10,
26
+ snapEnabled: true,
27
+ showRulers: true,
28
+ showBleed: false
29
+ };
30
+ const undoStack = [];
31
+ const redoStack = [];
32
+ const listeners = /* @__PURE__ */ new Set();
33
+ const notify = () => {
34
+ for (const listener of [...listeners]) listener();
35
+ };
36
+ return {
37
+ execute(command) {
38
+ state = command.execute(state);
39
+ undoStack.push(command);
40
+ if (undoStack.length > SCENE_COMMAND_HISTORY_LIMIT) {
41
+ undoStack.splice(0, undoStack.length - SCENE_COMMAND_HISTORY_LIMIT);
42
+ }
43
+ redoStack.length = 0;
44
+ notify();
45
+ },
46
+ // Both transforms run BEFORE the stacks move: a throwing transform (e.g.
47
+ // missing element after reset()) leaves history and state exactly as they
48
+ // were. The entry is never silently destroyed — the caller can retry after
49
+ // the next server refresh restores the target.
50
+ undo() {
51
+ const command = undoStack[undoStack.length - 1];
52
+ if (!command) throw new Error("nothing to undo \u2014 guard with canUndo() before calling undo()");
53
+ state = command.undo(state);
54
+ undoStack.pop();
55
+ redoStack.push(command);
56
+ notify();
57
+ return command;
58
+ },
59
+ redo() {
60
+ const command = redoStack[redoStack.length - 1];
61
+ if (!command) throw new Error("nothing to redo \u2014 guard with canRedo() before calling redo()");
62
+ state = command.execute(state);
63
+ redoStack.pop();
64
+ undoStack.push(command);
65
+ notify();
66
+ return command;
67
+ },
68
+ canUndo() {
69
+ return undoStack.length > 0;
70
+ },
71
+ canRedo() {
72
+ return redoStack.length > 0;
73
+ },
74
+ subscribe(listener) {
75
+ listeners.add(listener);
76
+ return () => {
77
+ listeners.delete(listener);
78
+ };
79
+ },
80
+ getState() {
81
+ return state;
82
+ },
83
+ setView(patch) {
84
+ state = { ...state, ...patch };
85
+ notify();
86
+ },
87
+ rollback(command) {
88
+ const idx = undoStack.lastIndexOf(command);
89
+ if (idx === -1) return;
90
+ state = command.undo(state);
91
+ undoStack.splice(idx, 1);
92
+ redoStack.length = 0;
93
+ notify();
94
+ },
95
+ /** Rebase onto a server-refreshed document. History survives; selection
96
+ * drops ids the refresh removed so view state never dangles. */
97
+ reset(newDocument) {
98
+ const liveElementIds = /* @__PURE__ */ new Set();
99
+ for (const page of newDocument.pages) {
100
+ collectElementIds(page.elements, liveElementIds);
101
+ }
102
+ const activePageExists = newDocument.pages.some((p) => p.id === state.activePageId);
103
+ const activePageId2 = activePageExists ? state.activePageId : newDocument.pages[0]?.id ?? state.activePageId;
104
+ state = {
105
+ ...state,
106
+ document: newDocument,
107
+ activePageId: activePageId2,
108
+ selectedElementIds: state.selectedElementIds.filter((id) => liveElementIds.has(id))
109
+ };
110
+ notify();
111
+ }
112
+ };
113
+ }
114
+ function collectElementIds(elements, ids) {
115
+ for (const el of elements) {
116
+ ids.add(el.id);
117
+ if (el.kind === "group" && Array.isArray(el.children)) {
118
+ collectElementIds(el.children, ids);
119
+ }
120
+ }
121
+ }
122
+
123
+ // src/design-canvas-react/engine/commands.ts
124
+ function applyOps(state, ops) {
125
+ return { ...state, document: applySceneOperations(state.document, ops) };
126
+ }
127
+ function applyOp(state, op) {
128
+ return { ...state, document: applySceneOperation(state.document, op) };
129
+ }
130
+ function addElementCommand(input) {
131
+ const addOp = {
132
+ type: "add_element",
133
+ pageId: input.pageId,
134
+ element: structuredClone(input.element),
135
+ ...input.index !== void 0 ? { index: input.index } : {},
136
+ ...input.parentGroupId !== void 0 ? { parentGroupId: input.parentGroupId } : {}
137
+ };
138
+ const deleteOp = {
139
+ type: "delete_element",
140
+ pageId: input.pageId,
141
+ elementId: input.element.id
142
+ };
143
+ return {
144
+ label: `Add ${input.element.kind}`,
145
+ execute: (state) => applyOp(state, addOp),
146
+ undo: (state) => applyOp(state, deleteOp),
147
+ operations: () => [structuredClone(addOp)],
148
+ inverseOperations: () => [structuredClone(deleteOp)]
149
+ };
150
+ }
151
+ function setAttrsCommand(input) {
152
+ const forwardOp = {
153
+ type: "set_attrs",
154
+ pageId: input.pageId,
155
+ elementId: input.elementId,
156
+ attrs: structuredClone(input.attrs)
157
+ };
158
+ const inverseOp = {
159
+ type: "set_attrs",
160
+ pageId: input.pageId,
161
+ elementId: input.elementId,
162
+ attrs: structuredClone(input.priorAttrs)
163
+ };
164
+ return {
165
+ label: "Edit element",
166
+ execute: (state) => applyOp(state, forwardOp),
167
+ undo: (state) => applyOp(state, inverseOp),
168
+ operations: () => [structuredClone(forwardOp)],
169
+ inverseOperations: () => [structuredClone(inverseOp)]
170
+ };
171
+ }
172
+ function multiSetAttrsCommand(entries) {
173
+ if (entries.length === 0) throw new Error("multiSetAttrsCommand: entries must not be empty");
174
+ const forwardOps = entries.map((e) => ({
175
+ type: "set_attrs",
176
+ pageId: e.pageId,
177
+ elementId: e.elementId,
178
+ attrs: structuredClone(e.attrs)
179
+ }));
180
+ const inverseOps = entries.map((e) => ({
181
+ type: "set_attrs",
182
+ pageId: e.pageId,
183
+ elementId: e.elementId,
184
+ attrs: structuredClone(e.priorAttrs)
185
+ }));
186
+ return {
187
+ label: `Edit ${entries.length} elements`,
188
+ execute: (state) => applyOps(state, forwardOps),
189
+ undo: (state) => applyOps(state, inverseOps),
190
+ operations: () => structuredClone(forwardOps),
191
+ inverseOperations: () => structuredClone(inverseOps)
192
+ };
193
+ }
194
+ function reorderElementCommand(input) {
195
+ let capturedFromIndex = null;
196
+ const forwardOp = {
197
+ type: "reorder_element",
198
+ pageId: input.pageId,
199
+ elementId: input.elementId,
200
+ toIndex: input.toIndex
201
+ };
202
+ return {
203
+ label: "Reorder element",
204
+ execute: (state) => {
205
+ if (capturedFromIndex === null) {
206
+ const page = requirePage(state.document, input.pageId);
207
+ const { index } = requireElement(page, input.elementId);
208
+ capturedFromIndex = index;
209
+ }
210
+ return applyOp(state, forwardOp);
211
+ },
212
+ undo: (state) => {
213
+ if (capturedFromIndex === null) {
214
+ throw new Error("reorderElementCommand: undo called before execute");
215
+ }
216
+ return applyOp(state, {
217
+ type: "reorder_element",
218
+ pageId: input.pageId,
219
+ elementId: input.elementId,
220
+ toIndex: capturedFromIndex
221
+ });
222
+ },
223
+ operations: () => [structuredClone(forwardOp)],
224
+ inverseOperations: () => {
225
+ if (capturedFromIndex === null) {
226
+ throw new Error("reorderElementCommand: inverseOperations called before execute");
227
+ }
228
+ return [{
229
+ type: "reorder_element",
230
+ pageId: input.pageId,
231
+ elementId: input.elementId,
232
+ toIndex: capturedFromIndex
233
+ }];
234
+ }
235
+ };
236
+ }
237
+ function deleteElementCommand(input) {
238
+ const page = requirePage(input.document, input.pageId);
239
+ const { element, owner, index } = requireElement(page, input.elementId);
240
+ const snapshot = structuredClone(element);
241
+ const parentGroupId = findParentGroupId(page, owner);
242
+ const deleteOp = {
243
+ type: "delete_element",
244
+ pageId: input.pageId,
245
+ elementId: input.elementId
246
+ };
247
+ const addOp = {
248
+ type: "add_element",
249
+ pageId: input.pageId,
250
+ element: snapshot,
251
+ index,
252
+ ...parentGroupId !== void 0 ? { parentGroupId } : {}
253
+ };
254
+ return {
255
+ label: `Delete ${element.kind}`,
256
+ execute: (state) => {
257
+ const next = applyOp(state, deleteOp);
258
+ return {
259
+ ...next,
260
+ selectedElementIds: next.selectedElementIds.filter((id) => id !== input.elementId)
261
+ };
262
+ },
263
+ undo: (state) => applyOp(state, addOp),
264
+ operations: () => [structuredClone(deleteOp)],
265
+ inverseOperations: () => [structuredClone(addOp)]
266
+ };
267
+ }
268
+ function findParentGroupId(page, owner) {
269
+ if (owner === page.elements) return void 0;
270
+ return findGroupWithChildren(page.elements, owner);
271
+ }
272
+ function findGroupWithChildren(elements, target) {
273
+ for (const el of elements) {
274
+ if (el.kind === "group") {
275
+ if (el.children === target) return el.id;
276
+ const found = findGroupWithChildren(el.children, target);
277
+ if (found !== void 0) return found;
278
+ }
279
+ }
280
+ return void 0;
281
+ }
282
+ function groupElementsCommand(input) {
283
+ if (input.elementIds.length < 2) {
284
+ throw new Error("groupElementsCommand: requires \u2265 2 elementIds");
285
+ }
286
+ const groupOp = {
287
+ type: "group_elements",
288
+ pageId: input.pageId,
289
+ elementIds: input.elementIds.slice(),
290
+ groupId: input.groupId,
291
+ ...input.name !== void 0 ? { name: input.name } : {}
292
+ };
293
+ const ungroupOp = {
294
+ type: "ungroup_element",
295
+ pageId: input.pageId,
296
+ groupId: input.groupId
297
+ };
298
+ return {
299
+ label: `Group ${input.elementIds.length} elements`,
300
+ execute: (state) => {
301
+ const next = applyOp(state, groupOp);
302
+ return {
303
+ ...next,
304
+ selectedElementIds: [input.groupId]
305
+ };
306
+ },
307
+ undo: (state) => {
308
+ const next = applyOp(state, ungroupOp);
309
+ return { ...next, selectedElementIds: input.elementIds.slice() };
310
+ },
311
+ operations: () => [structuredClone(groupOp)],
312
+ inverseOperations: () => [structuredClone(ungroupOp)]
313
+ };
314
+ }
315
+ function ungroupElementCommand(input) {
316
+ const page = requirePage(input.document, input.pageId);
317
+ const { element } = requireElement(page, input.groupId);
318
+ if (element.kind !== "group") {
319
+ throw new Error(`ungroupElementCommand: element ${input.groupId} is kind ${element.kind}, not group`);
320
+ }
321
+ const childIds = element.children.map((c) => c.id);
322
+ const ungroupOp = {
323
+ type: "ungroup_element",
324
+ pageId: input.pageId,
325
+ groupId: input.groupId
326
+ };
327
+ const regroupOp = {
328
+ type: "group_elements",
329
+ pageId: input.pageId,
330
+ elementIds: childIds,
331
+ groupId: input.groupId,
332
+ name: element.name
333
+ };
334
+ return {
335
+ label: `Ungroup`,
336
+ execute: (state) => {
337
+ const next = applyOp(state, ungroupOp);
338
+ return { ...next, selectedElementIds: childIds.slice() };
339
+ },
340
+ undo: (state) => {
341
+ const next = applyOp(state, regroupOp);
342
+ return { ...next, selectedElementIds: [input.groupId] };
343
+ },
344
+ operations: () => [structuredClone(ungroupOp)],
345
+ inverseOperations: () => [structuredClone(regroupOp)]
346
+ };
347
+ }
348
+ function addPageCommand(input) {
349
+ const addOp = {
350
+ type: "add_page",
351
+ pageId: input.pageId,
352
+ ...input.options !== void 0 ? { options: input.options } : {},
353
+ ...input.index !== void 0 ? { index: input.index } : {}
354
+ };
355
+ const deleteOp = { type: "delete_page", pageId: input.pageId };
356
+ return {
357
+ label: "Add page",
358
+ execute: (state) => {
359
+ const next = applyOp(state, addOp);
360
+ return { ...next, activePageId: input.pageId };
361
+ },
362
+ undo: (state) => {
363
+ const next = applyOp(state, deleteOp);
364
+ const activeExists = next.document.pages.some((p) => p.id === next.activePageId);
365
+ return activeExists ? next : { ...next, activePageId: next.document.pages[0].id };
366
+ },
367
+ operations: () => [structuredClone(addOp)],
368
+ inverseOperations: () => [structuredClone(deleteOp)]
369
+ };
370
+ }
371
+ function duplicatePageCommand(input) {
372
+ requirePage(input.document, input.sourcePageId);
373
+ const dupOp = {
374
+ type: "duplicate_page",
375
+ sourcePageId: input.sourcePageId,
376
+ pageId: input.pageId
377
+ };
378
+ const deleteOp = { type: "delete_page", pageId: input.pageId };
379
+ return {
380
+ label: "Duplicate page",
381
+ execute: (state) => {
382
+ const next = applyOp(state, dupOp);
383
+ return { ...next, activePageId: input.pageId };
384
+ },
385
+ undo: (state) => {
386
+ const next = applyOp(state, deleteOp);
387
+ const activeExists = next.document.pages.some((p) => p.id === next.activePageId);
388
+ return activeExists ? next : { ...next, activePageId: input.sourcePageId };
389
+ },
390
+ operations: () => [structuredClone(dupOp)],
391
+ inverseOperations: () => [structuredClone(deleteOp)]
392
+ };
393
+ }
394
+ function deletePageCommand(input) {
395
+ if (input.document.pages.length <= 1) {
396
+ throw new Error("deletePageCommand: cannot delete the last page");
397
+ }
398
+ const page = requirePage(input.document, input.pageId);
399
+ const currentIndex = input.document.pages.findIndex((p) => p.id === input.pageId);
400
+ const snapshot = structuredClone(page);
401
+ const deleteOp = { type: "delete_page", pageId: input.pageId };
402
+ function buildRestoreOps() {
403
+ const ops = [
404
+ {
405
+ type: "add_page",
406
+ pageId: snapshot.id,
407
+ options: {
408
+ name: snapshot.name,
409
+ width: snapshot.width,
410
+ height: snapshot.height,
411
+ background: snapshot.background
412
+ },
413
+ index: currentIndex
414
+ }
415
+ ];
416
+ if (snapshot.bleed) {
417
+ ops.push({ type: "set_page_props", pageId: snapshot.id, bleed: snapshot.bleed });
418
+ }
419
+ if (snapshot.guides.vertical.length > 0 || snapshot.guides.horizontal.length > 0) {
420
+ ops.push({ type: "set_page_guides", pageId: snapshot.id, guides: snapshot.guides });
421
+ }
422
+ for (let i = 0; i < snapshot.elements.length; i++) {
423
+ ops.push({
424
+ type: "add_element",
425
+ pageId: snapshot.id,
426
+ element: structuredClone(snapshot.elements[i]),
427
+ index: i
428
+ });
429
+ }
430
+ return ops;
431
+ }
432
+ return {
433
+ label: "Delete page",
434
+ execute: (state) => {
435
+ const next = applyOp(state, deleteOp);
436
+ const activeExists = next.document.pages.some((p) => p.id === next.activePageId);
437
+ if (activeExists) return next;
438
+ const fallbackIndex = Math.min(currentIndex, next.document.pages.length - 1);
439
+ return { ...next, activePageId: next.document.pages[fallbackIndex].id };
440
+ },
441
+ undo: (state) => {
442
+ const next = applyOps(state, buildRestoreOps());
443
+ return { ...next, activePageId: input.pageId };
444
+ },
445
+ operations: () => [structuredClone(deleteOp)],
446
+ inverseOperations: () => buildRestoreOps()
447
+ };
448
+ }
449
+ function reorderPageCommand(input) {
450
+ let capturedFromIndex = null;
451
+ const forwardOp = {
452
+ type: "reorder_page",
453
+ pageId: input.pageId,
454
+ toIndex: input.toIndex
455
+ };
456
+ return {
457
+ label: "Reorder page",
458
+ execute: (state) => {
459
+ if (capturedFromIndex === null) {
460
+ capturedFromIndex = state.document.pages.findIndex((p) => p.id === input.pageId);
461
+ if (capturedFromIndex === -1) throw new Error(`reorderPageCommand: page ${input.pageId} not found`);
462
+ }
463
+ return applyOp(state, forwardOp);
464
+ },
465
+ undo: (state) => {
466
+ if (capturedFromIndex === null) throw new Error("reorderPageCommand: undo called before execute");
467
+ return applyOp(state, {
468
+ type: "reorder_page",
469
+ pageId: input.pageId,
470
+ toIndex: capturedFromIndex
471
+ });
472
+ },
473
+ operations: () => [structuredClone(forwardOp)],
474
+ inverseOperations: () => {
475
+ if (capturedFromIndex === null) throw new Error("reorderPageCommand: inverseOperations called before execute");
476
+ return [{ type: "reorder_page", pageId: input.pageId, toIndex: capturedFromIndex }];
477
+ }
478
+ };
479
+ }
480
+ function setPagePropsCommand(input) {
481
+ const page = requirePage(input.document, input.pageId);
482
+ const prior = {
483
+ type: "set_page_props",
484
+ pageId: input.pageId,
485
+ ...input.props.name !== void 0 ? { name: page.name } : {},
486
+ ...input.props.width !== void 0 ? { width: page.width } : {},
487
+ ...input.props.height !== void 0 ? { height: page.height } : {},
488
+ ...input.props.background !== void 0 ? { background: page.background } : {},
489
+ ...input.props.bleed !== void 0 ? { bleed: page.bleed } : {}
490
+ };
491
+ const forwardOp = {
492
+ type: "set_page_props",
493
+ pageId: input.pageId,
494
+ ...input.props
495
+ };
496
+ return {
497
+ label: "Edit page",
498
+ execute: (state) => applyOp(state, forwardOp),
499
+ undo: (state) => applyOp(state, prior),
500
+ operations: () => [structuredClone(forwardOp)],
501
+ inverseOperations: () => [structuredClone(prior)]
502
+ };
503
+ }
504
+ function setPageGuidesCommand(input) {
505
+ const page = requirePage(input.document, input.pageId);
506
+ const priorGuides = structuredClone(page.guides);
507
+ const forwardOp = {
508
+ type: "set_page_guides",
509
+ pageId: input.pageId,
510
+ guides: structuredClone(input.guides)
511
+ };
512
+ const inverseOp = {
513
+ type: "set_page_guides",
514
+ pageId: input.pageId,
515
+ guides: priorGuides
516
+ };
517
+ return {
518
+ label: "Edit guides",
519
+ execute: (state) => applyOp(state, forwardOp),
520
+ undo: (state) => applyOp(state, inverseOp),
521
+ operations: () => [structuredClone(forwardOp)],
522
+ inverseOperations: () => [structuredClone(inverseOp)]
523
+ };
524
+ }
525
+ function bindSlotCommand(input) {
526
+ const page = requirePage(input.document, input.pageId);
527
+ const { element } = requireElement(page, input.elementId);
528
+ const priorSlot = element.slot ?? null;
529
+ const forwardOp = {
530
+ type: "bind_slot",
531
+ pageId: input.pageId,
532
+ elementId: input.elementId,
533
+ slot: input.slot
534
+ };
535
+ const inverseOp = {
536
+ type: "bind_slot",
537
+ pageId: input.pageId,
538
+ elementId: input.elementId,
539
+ slot: priorSlot
540
+ };
541
+ return {
542
+ label: input.slot === null ? "Unbind slot" : `Bind slot "${input.slot}"`,
543
+ execute: (state) => applyOp(state, forwardOp),
544
+ undo: (state) => applyOp(state, inverseOp),
545
+ operations: () => [structuredClone(forwardOp)],
546
+ inverseOperations: () => [structuredClone(inverseOp)]
547
+ };
548
+ }
549
+ function setDocumentTitleCommand(input) {
550
+ const priorTitle = input.document.title;
551
+ const forwardOp = { type: "set_document_title", title: input.title };
552
+ const inverseOp = { type: "set_document_title", title: priorTitle };
553
+ return {
554
+ label: `Rename document`,
555
+ execute: (state) => applyOp(state, forwardOp),
556
+ undo: (state) => applyOp(state, inverseOp),
557
+ operations: () => [structuredClone(forwardOp)],
558
+ inverseOperations: () => [structuredClone(inverseOp)]
559
+ };
560
+ }
561
+
562
+ // src/design-canvas-react/components/ruler-math.ts
563
+ var TICK_STEP_CANDIDATES_PX = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3];
564
+ function selectTickStep(input) {
565
+ if (!Number.isFinite(input.zoom) || input.zoom <= 0) {
566
+ throw new Error(`zoom must be a positive finite number, got ${input.zoom}`);
567
+ }
568
+ const minMajor = input.minMajorSpacingPx ?? 40;
569
+ const minMinor = input.minMinorSpacingPx ?? 8;
570
+ let major = TICK_STEP_CANDIDATES_PX[TICK_STEP_CANDIDATES_PX.length - 1];
571
+ for (const candidate of TICK_STEP_CANDIDATES_PX) {
572
+ if (candidate * input.zoom >= minMajor) {
573
+ major = candidate;
574
+ break;
575
+ }
576
+ }
577
+ while (major * input.zoom < minMajor) {
578
+ major = major * 2;
579
+ }
580
+ const minor = major / 5;
581
+ const drawMinor = minor * input.zoom >= minMinor;
582
+ return { major, minor, drawMinor };
583
+ }
584
+ function buildRulerTicks(input) {
585
+ if (input.documentLength <= 0) return [];
586
+ const { major, minor, drawMinor } = input.step;
587
+ const ticks = [];
588
+ for (let pos = 0; pos <= input.documentLength; pos += major) {
589
+ ticks.push({ position: pos, label: formatRulerLabel(pos) });
590
+ if (!drawMinor) continue;
591
+ for (let m = 1; m < 5; m += 1) {
592
+ const minorPos = pos + m * minor;
593
+ if (minorPos >= input.documentLength) break;
594
+ ticks.push({ position: minorPos, label: null });
595
+ }
596
+ }
597
+ return ticks;
598
+ }
599
+ function formatRulerLabel(value) {
600
+ if (!Number.isFinite(value)) return "";
601
+ if (Math.abs(value) >= 1e3) {
602
+ const k = value / 1e3;
603
+ return `${Number.isInteger(k) ? k : k.toFixed(1)}k`;
604
+ }
605
+ return Number.isInteger(value) ? String(value) : value.toFixed(1);
606
+ }
607
+ function screenToDocumentPosition(input) {
608
+ if (!Number.isFinite(input.zoom) || input.zoom <= 0) {
609
+ throw new Error(`zoom must be a positive finite number, got ${input.zoom}`);
610
+ }
611
+ return input.pointerScreenPx / input.zoom + input.scrollOffset;
612
+ }
613
+ function topIndex(ownerLength) {
614
+ return ownerLength - 1;
615
+ }
616
+ function indexForward(current, ownerLength) {
617
+ return Math.min(current + 1, ownerLength - 1);
618
+ }
619
+ function indexBackward(current) {
620
+ return Math.max(current - 1, 0);
621
+ }
622
+ function clampIndex(target, ownerLength) {
623
+ return Math.max(0, Math.min(target, ownerLength - 1));
624
+ }
625
+
626
+ // src/design-canvas-react/components/BleedTrimOverlay.tsx
627
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
628
+ var TRIM_MARK_PX = 12;
629
+ var TRIM_MARK_OFFSET_PX = 4;
630
+ function BleedTrimOverlay({ pageWidthPx, pageHeightPx, bleed }) {
631
+ const totalW = bleed.left + pageWidthPx + bleed.right;
632
+ const totalH = bleed.top + pageHeightPx + bleed.bottom;
633
+ return /* @__PURE__ */ jsxs(
634
+ "div",
635
+ {
636
+ "data-node": "overlay:bleed",
637
+ className: "pointer-events-none absolute",
638
+ style: {
639
+ top: -bleed.top,
640
+ left: -bleed.left,
641
+ width: totalW,
642
+ height: totalH
643
+ },
644
+ "aria-hidden": true,
645
+ children: [
646
+ /* @__PURE__ */ jsx(
647
+ "div",
648
+ {
649
+ className: "absolute bg-rose-500/10",
650
+ style: { top: 0, left: 0, width: totalW, height: bleed.top }
651
+ }
652
+ ),
653
+ /* @__PURE__ */ jsx(
654
+ "div",
655
+ {
656
+ className: "absolute bg-rose-500/10",
657
+ style: { bottom: 0, left: 0, width: totalW, height: bleed.bottom }
658
+ }
659
+ ),
660
+ /* @__PURE__ */ jsx(
661
+ "div",
662
+ {
663
+ className: "absolute bg-rose-500/10",
664
+ style: { top: bleed.top, left: 0, width: bleed.left, height: pageHeightPx }
665
+ }
666
+ ),
667
+ /* @__PURE__ */ jsx(
668
+ "div",
669
+ {
670
+ className: "absolute bg-rose-500/10",
671
+ style: { top: bleed.top, right: 0, width: bleed.right, height: pageHeightPx }
672
+ }
673
+ ),
674
+ /* @__PURE__ */ jsx(TrimMark, { corner: "tl", bleed }),
675
+ /* @__PURE__ */ jsx(TrimMark, { corner: "tr", bleed, pageWidthPx }),
676
+ /* @__PURE__ */ jsx(TrimMark, { corner: "bl", bleed, pageHeightPx }),
677
+ /* @__PURE__ */ jsx(TrimMark, { corner: "br", bleed, pageWidthPx, pageHeightPx })
678
+ ]
679
+ }
680
+ );
681
+ }
682
+ function TrimMark({ corner, bleed, pageWidthPx = 0, pageHeightPx = 0 }) {
683
+ const isRight = corner === "tr" || corner === "br";
684
+ const isBottom = corner === "bl" || corner === "br";
685
+ const xBase = isRight ? bleed.left + pageWidthPx : bleed.left;
686
+ const yBase = isBottom ? bleed.top + pageHeightPx : bleed.top;
687
+ const hX = isRight ? xBase + TRIM_MARK_OFFSET_PX : xBase - TRIM_MARK_OFFSET_PX - TRIM_MARK_PX;
688
+ const hY = yBase - 0.5;
689
+ const vX = xBase - 0.5;
690
+ const vY = isBottom ? yBase + TRIM_MARK_OFFSET_PX : yBase - TRIM_MARK_OFFSET_PX - TRIM_MARK_PX;
691
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
692
+ /* @__PURE__ */ jsx(
693
+ "div",
694
+ {
695
+ className: "absolute bg-[var(--text-muted)]",
696
+ style: { left: hX, top: hY, width: TRIM_MARK_PX, height: 1 }
697
+ }
698
+ ),
699
+ /* @__PURE__ */ jsx(
700
+ "div",
701
+ {
702
+ className: "absolute bg-[var(--text-muted)]",
703
+ style: { left: vX, top: vY, width: 1, height: TRIM_MARK_PX }
704
+ }
705
+ )
706
+ ] });
707
+ }
708
+
709
+ // src/design-canvas-react/components/PagesStrip.tsx
710
+ import { useEffect, useRef, useState } from "react";
711
+
712
+ // src/design-canvas-react/components/glyphs.tsx
713
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
714
+ function glyph(paths) {
715
+ return function Glyph({ className }) {
716
+ return /* @__PURE__ */ jsx2(
717
+ "svg",
718
+ {
719
+ className,
720
+ viewBox: "0 0 24 24",
721
+ fill: "none",
722
+ stroke: "currentColor",
723
+ strokeWidth: "2",
724
+ strokeLinecap: "round",
725
+ strokeLinejoin: "round",
726
+ "aria-hidden": true,
727
+ children: paths
728
+ }
729
+ );
730
+ };
731
+ }
732
+ var UndoGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M3 7v6h6M3 13a9 9 0 1 0 3-7.7" }));
733
+ var RedoGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M21 7v6h-6M21 13a9 9 0 1 1-3-7.7" }));
734
+ var EyeGlyph = glyph(
735
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
736
+ /* @__PURE__ */ jsx2("path", { d: "M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z" }),
737
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "3" })
738
+ ] })
739
+ );
740
+ var EyeOffGlyph = glyph(
741
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
742
+ /* @__PURE__ */ jsx2("path", { d: "M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94" }),
743
+ /* @__PURE__ */ jsx2("path", { d: "M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19" }),
744
+ /* @__PURE__ */ jsx2("path", { d: "m1 1 22 22" })
745
+ ] })
746
+ );
747
+ var LockGlyph = glyph(
748
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
749
+ /* @__PURE__ */ jsx2("rect", { x: "5", y: "11", width: "14", height: "10", rx: "2" }),
750
+ /* @__PURE__ */ jsx2("path", { d: "M8 11V7a4 4 0 0 1 8 0v4" })
751
+ ] })
752
+ );
753
+ var UnlockGlyph = glyph(
754
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
755
+ /* @__PURE__ */ jsx2("rect", { x: "5", y: "11", width: "14", height: "10", rx: "2" }),
756
+ /* @__PURE__ */ jsx2("path", { d: "M8 11V7a4 4 0 1 1 8 0" })
757
+ ] })
758
+ );
759
+ var TrashGlyph = glyph(
760
+ /* @__PURE__ */ jsx2(Fragment2, { children: /* @__PURE__ */ jsx2("path", { d: "M3 6h18M19 6l-1 14H6L5 6M10 6V4h4v2" }) })
761
+ );
762
+ var GroupGlyph = glyph(
763
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
764
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "2", width: "8", height: "8", rx: "1" }),
765
+ /* @__PURE__ */ jsx2("rect", { x: "14", y: "2", width: "8", height: "8", rx: "1" }),
766
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "14", width: "8", height: "8", rx: "1" }),
767
+ /* @__PURE__ */ jsx2("rect", { x: "14", y: "14", width: "8", height: "8", rx: "1" })
768
+ ] })
769
+ );
770
+ var UngroupGlyph = glyph(
771
+ /* @__PURE__ */ jsx2(Fragment2, { children: /* @__PURE__ */ jsx2("path", { d: "M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2" }) })
772
+ );
773
+ var BringFrontGlyph = glyph(
774
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
775
+ /* @__PURE__ */ jsx2("rect", { x: "8", y: "8", width: "12", height: "12", rx: "1" }),
776
+ /* @__PURE__ */ jsx2("path", { d: "M4 4h12v4H4z", opacity: ".4" })
777
+ ] })
778
+ );
779
+ var SendBackGlyph = glyph(
780
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
781
+ /* @__PURE__ */ jsx2("rect", { x: "4", y: "4", width: "12", height: "12", rx: "1", opacity: ".4" }),
782
+ /* @__PURE__ */ jsx2("path", { d: "M8 8h12v12H8z" })
783
+ ] })
784
+ );
785
+ var AlignLeftGlyph = glyph(
786
+ /* @__PURE__ */ jsx2(Fragment2, { children: /* @__PURE__ */ jsx2("path", { d: "M3 4v16M7 8h10M7 16h6" }) })
787
+ );
788
+ var AlignCenterGlyph = glyph(
789
+ /* @__PURE__ */ jsx2(Fragment2, { children: /* @__PURE__ */ jsx2("path", { d: "M12 4v16M7 8h10M9 16h6" }) })
790
+ );
791
+ var AlignRightGlyph = glyph(
792
+ /* @__PURE__ */ jsx2(Fragment2, { children: /* @__PURE__ */ jsx2("path", { d: "M21 4v16M7 8h10M11 16h6" }) })
793
+ );
794
+ var BoldGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M6 4h8a4 4 0 0 1 0 8H6zM6 12h9a4 4 0 0 1 0 8H6z", fill: "currentColor", stroke: "none" }));
795
+ var ItalicGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M11 4h6M7 20h6M14 4 8 20" }));
796
+ var PlusGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M12 5v14M5 12h14" }));
797
+ var ChevronDownGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "m6 9 6 6 6-6" }));
798
+ var ChevronRightGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "m9 18 6-6-6-6" }));
799
+ var RectGlyph = glyph(/* @__PURE__ */ jsx2("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }));
800
+ var EllipseGlyph = glyph(/* @__PURE__ */ jsx2("ellipse", { cx: "12", cy: "12", rx: "10", ry: "7" }));
801
+ var LineGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M5 19 19 5" }));
802
+ var TextGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M4 7V4h16v3M9 20h6M12 4v16" }));
803
+ var ImageGlyph = glyph(
804
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
805
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
806
+ /* @__PURE__ */ jsx2("circle", { cx: "9", cy: "9", r: "2" }),
807
+ /* @__PURE__ */ jsx2("path", { d: "m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21" })
808
+ ] })
809
+ );
810
+ var VideoGlyph = glyph(
811
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
812
+ /* @__PURE__ */ jsx2("rect", { x: "2", y: "3", width: "20", height: "18", rx: "2" }),
813
+ /* @__PURE__ */ jsx2("path", { d: "m10 8 6 4-6 4z", fill: "currentColor", stroke: "none" })
814
+ ] })
815
+ );
816
+ var SlotGlyph = glyph(
817
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
818
+ /* @__PURE__ */ jsx2("circle", { cx: "12", cy: "12", r: "3" }),
819
+ /* @__PURE__ */ jsx2("path", { d: "M12 1v4M12 19v4M4.2 4.2l2.8 2.8M17 17l2.8 2.8M1 12h4M19 12h4M4.2 19.8l2.8-2.8M17 7 19.8 4.2" })
820
+ ] })
821
+ );
822
+ var FitGlyph = glyph(
823
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
824
+ /* @__PURE__ */ jsx2("path", { d: "M3 3h4v4H3zM17 3h4v4h-4zM3 17h4v4H3zM17 17h4v4h-4z" }),
825
+ /* @__PURE__ */ jsx2("path", { d: "M7 5h10M5 7v10M19 7v10M7 19h10" })
826
+ ] })
827
+ );
828
+ var PageGlyph = glyph(
829
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
830
+ /* @__PURE__ */ jsx2("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
831
+ /* @__PURE__ */ jsx2("polyline", { points: "14 2 14 8 20 8" })
832
+ ] })
833
+ );
834
+ var GridGlyph = glyph(
835
+ /* @__PURE__ */ jsx2(Fragment2, { children: /* @__PURE__ */ jsx2("path", { d: "M3 3h18v18H3zM3 9h18M3 15h18M9 3v18M15 3v18" }) })
836
+ );
837
+ var RulerGlyph = glyph(
838
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
839
+ /* @__PURE__ */ jsx2("path", { d: "M1 9v6l12 6V9L1 3z" }),
840
+ /* @__PURE__ */ jsx2("path", { d: "m13 15 9-4.5V4.5L13 9" }),
841
+ /* @__PURE__ */ jsx2("path", { d: "M5 12v3M8 13.5v2.5M11 15v3" })
842
+ ] })
843
+ );
844
+ var MagnetGlyph = glyph(
845
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
846
+ /* @__PURE__ */ jsx2("path", { d: "m6 15-4-4 6.75-6.77a7.79 7.79 0 0 1 11 11L13 22l-4-4 6.39-6.36a2.14 2.14 0 0 0-3-3z" }),
847
+ /* @__PURE__ */ jsx2("path", { d: "m5 8 4 4M12 15l4 4" })
848
+ ] })
849
+ );
850
+ var BleedGlyph = glyph(
851
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
852
+ /* @__PURE__ */ jsx2("rect", { x: "4", y: "4", width: "16", height: "16", strokeDasharray: "3 2" }),
853
+ /* @__PURE__ */ jsx2("rect", { x: "7", y: "7", width: "10", height: "10" })
854
+ ] })
855
+ );
856
+ var DuplicateGlyph = glyph(
857
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
858
+ /* @__PURE__ */ jsx2("rect", { x: "8", y: "8", width: "12", height: "12", rx: "2" }),
859
+ /* @__PURE__ */ jsx2("path", { d: "M4 16V4a2 2 0 0 1 2-2h12" })
860
+ ] })
861
+ );
862
+ var ZoomFitGlyph = glyph(
863
+ /* @__PURE__ */ jsx2(Fragment2, { children: /* @__PURE__ */ jsx2("path", { d: "M15 3h6v6M14 10l6.1-6.1M9 21H3v-6M10 14l-6.1 6.1" }) })
864
+ );
865
+
866
+ // src/design-canvas-react/components/PagesStrip.tsx
867
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
868
+ var THUMBNAIL_W = 80;
869
+ var THUMBNAIL_H = 56;
870
+ var BTN = "flex h-6 w-6 items-center justify-center rounded border border-[var(--border-default)] text-[var(--text-secondary)] transition hover:text-[var(--text-primary)] disabled:cursor-default disabled:opacity-40";
871
+ function PagesStrip({
872
+ pages,
873
+ activePageId,
874
+ canWrite,
875
+ renderThumbnail,
876
+ onSelectPage,
877
+ onAddPage,
878
+ onDuplicatePage,
879
+ onDeletePage,
880
+ onReorderPage
881
+ }) {
882
+ const [thumbnails, setThumbnails] = useState({});
883
+ const thumbnailVersionRef = useRef(0);
884
+ useEffect(() => {
885
+ const version = ++thumbnailVersionRef.current;
886
+ let cancelled = false;
887
+ async function generate() {
888
+ const results = {};
889
+ for (const page of pages) {
890
+ if (cancelled) return;
891
+ try {
892
+ results[page.id] = await renderThumbnail(page);
893
+ } catch {
894
+ results[page.id] = null;
895
+ }
896
+ }
897
+ if (!cancelled && thumbnailVersionRef.current === version) {
898
+ setThumbnails(results);
899
+ }
900
+ }
901
+ void generate();
902
+ return () => {
903
+ cancelled = true;
904
+ };
905
+ }, [pages]);
906
+ const dragIndexRef = useRef(null);
907
+ const [dragOverIndex, setDragOverIndex] = useState(null);
908
+ return /* @__PURE__ */ jsxs3(
909
+ "div",
910
+ {
911
+ className: "flex h-[84px] shrink-0 items-center gap-2 overflow-x-auto border-t border-[var(--border-default)] bg-[var(--bg-input)] px-2",
912
+ "aria-label": "Pages",
913
+ children: [
914
+ pages.map((page, index) => {
915
+ const isActive = page.id === activePageId;
916
+ const thumbUrl = thumbnails[page.id];
917
+ return /* @__PURE__ */ jsxs3(
918
+ "div",
919
+ {
920
+ role: "button",
921
+ tabIndex: 0,
922
+ "aria-label": `Page ${index + 1}: ${page.name}${isActive ? " (active)" : ""}`,
923
+ "aria-pressed": isActive,
924
+ draggable: canWrite,
925
+ onDragStart: () => {
926
+ dragIndexRef.current = index;
927
+ },
928
+ onDragOver: (event) => {
929
+ if (dragIndexRef.current === null) return;
930
+ event.preventDefault();
931
+ setDragOverIndex(index);
932
+ },
933
+ onDragLeave: () => setDragOverIndex(null),
934
+ onDrop: () => {
935
+ const from = dragIndexRef.current;
936
+ if (from !== null && from !== index) {
937
+ onReorderPage(pages[from].id, index);
938
+ }
939
+ dragIndexRef.current = null;
940
+ setDragOverIndex(null);
941
+ },
942
+ onDragEnd: () => {
943
+ dragIndexRef.current = null;
944
+ setDragOverIndex(null);
945
+ },
946
+ onClick: () => onSelectPage(page.id),
947
+ onKeyDown: (event) => {
948
+ if (event.key === "Enter" || event.key === " ") {
949
+ event.preventDefault();
950
+ onSelectPage(page.id);
951
+ }
952
+ },
953
+ className: [
954
+ "group relative flex shrink-0 cursor-pointer flex-col items-center gap-1 rounded p-1 transition",
955
+ isActive ? "ring-2 ring-[var(--brand-primary)]" : "hover:bg-[var(--border-default)]/40",
956
+ dragOverIndex === index ? "ring-1 ring-[var(--brand-primary)]/60" : ""
957
+ ].join(" "),
958
+ children: [
959
+ /* @__PURE__ */ jsx3(
960
+ "div",
961
+ {
962
+ className: "overflow-hidden rounded border border-[var(--border-default)] bg-white",
963
+ style: { width: THUMBNAIL_W, height: THUMBNAIL_H },
964
+ children: thumbUrl ? /* @__PURE__ */ jsx3(
965
+ "img",
966
+ {
967
+ src: thumbUrl,
968
+ alt: page.name,
969
+ className: "h-full w-full object-cover",
970
+ draggable: false
971
+ }
972
+ ) : /* @__PURE__ */ jsx3("div", { className: "flex h-full w-full items-center justify-center", children: /* @__PURE__ */ jsx3(PageGlyph, { className: "h-5 w-5 text-[var(--text-muted)]" }) })
973
+ }
974
+ ),
975
+ /* @__PURE__ */ jsx3("span", { className: "max-w-[80px] truncate text-[10px] text-[var(--text-secondary)]", children: page.name }),
976
+ canWrite ? /* @__PURE__ */ jsxs3("div", { className: "pointer-events-none absolute -top-1 right-0 flex gap-0.5 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100", children: [
977
+ /* @__PURE__ */ jsx3(
978
+ "button",
979
+ {
980
+ type: "button",
981
+ "aria-label": `Duplicate page ${page.name}`,
982
+ onClick: (event) => {
983
+ event.stopPropagation();
984
+ onDuplicatePage(page.id);
985
+ },
986
+ className: BTN,
987
+ children: /* @__PURE__ */ jsx3(DuplicateGlyph, { className: "h-3 w-3" })
988
+ }
989
+ ),
990
+ /* @__PURE__ */ jsx3(
991
+ "button",
992
+ {
993
+ type: "button",
994
+ "aria-label": `Delete page ${page.name}`,
995
+ disabled: pages.length <= 1,
996
+ onClick: (event) => {
997
+ event.stopPropagation();
998
+ if (pages.length > 1) onDeletePage(page.id);
999
+ },
1000
+ className: BTN,
1001
+ children: /* @__PURE__ */ jsx3(TrashGlyph, { className: "h-3 w-3 text-rose-400" })
1002
+ }
1003
+ )
1004
+ ] }) : null
1005
+ ]
1006
+ },
1007
+ page.id
1008
+ );
1009
+ }),
1010
+ canWrite ? /* @__PURE__ */ jsxs3(
1011
+ "button",
1012
+ {
1013
+ type: "button",
1014
+ "aria-label": "Add page",
1015
+ onClick: onAddPage,
1016
+ className: "flex h-[72px] w-[80px] shrink-0 flex-col items-center justify-center gap-1 rounded border border-dashed border-[var(--border-default)] text-[var(--text-muted)] transition hover:border-[var(--brand-primary)] hover:text-[var(--brand-primary)]",
1017
+ children: [
1018
+ /* @__PURE__ */ jsx3(PlusGlyph, { className: "h-4 w-4" }),
1019
+ /* @__PURE__ */ jsx3("span", { className: "text-[10px]", children: "Add page" })
1020
+ ]
1021
+ }
1022
+ ) : null
1023
+ ]
1024
+ }
1025
+ );
1026
+ }
1027
+
1028
+ // src/design-canvas-react/components/Rulers.tsx
1029
+ import { useRef as useRef2, useState as useState2 } from "react";
1030
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1031
+ var RULER_SIZE_PX = 20;
1032
+ var DELETE_THRESHOLD_PX = RULER_SIZE_PX + 4;
1033
+ function Rulers({ pageWidth, pageHeight, zoom, scrollLeft, scrollTop, showRulers, guides, onGuidesChange }) {
1034
+ if (!showRulers) return null;
1035
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
1036
+ /* @__PURE__ */ jsx4(
1037
+ "div",
1038
+ {
1039
+ className: "absolute top-0 left-0 z-20 shrink-0 border-b border-r border-[var(--border-default)] bg-[var(--bg-input)]",
1040
+ style: { width: RULER_SIZE_PX, height: RULER_SIZE_PX }
1041
+ }
1042
+ ),
1043
+ /* @__PURE__ */ jsx4(
1044
+ HorizontalRuler,
1045
+ {
1046
+ pageWidth,
1047
+ zoom,
1048
+ scrollLeft,
1049
+ guides,
1050
+ onGuidesChange
1051
+ }
1052
+ ),
1053
+ /* @__PURE__ */ jsx4(
1054
+ VerticalRuler,
1055
+ {
1056
+ pageHeight,
1057
+ zoom,
1058
+ scrollTop,
1059
+ guides,
1060
+ onGuidesChange
1061
+ }
1062
+ )
1063
+ ] });
1064
+ }
1065
+ function HorizontalRuler({ pageWidth, zoom, scrollLeft, guides, onGuidesChange }) {
1066
+ const ref = useRef2(null);
1067
+ const [pointerX, setPointerX] = useState2(null);
1068
+ const [dragGuideX, setDragGuideX] = useState2(null);
1069
+ const dragGuideIndexRef = useRef2(null);
1070
+ const step = selectTickStep({ zoom, minMajorSpacingPx: 40 });
1071
+ const ticks = buildRulerTicks({ documentLength: pageWidth, step });
1072
+ function screenXToDoc(clientX) {
1073
+ if (!ref.current) return 0;
1074
+ const rect = ref.current.getBoundingClientRect();
1075
+ return screenToDocumentPosition({ pointerScreenPx: clientX - rect.left, scrollOffset: scrollLeft, zoom });
1076
+ }
1077
+ function handlePointerDown(event) {
1078
+ if (event.button !== 0) return;
1079
+ event.preventDefault();
1080
+ event.currentTarget.setPointerCapture(event.pointerId);
1081
+ const docX = screenXToDoc(event.clientX);
1082
+ const threshold = 4 / zoom;
1083
+ const nearIdx = guides.vertical.findIndex((g) => Math.abs(g - docX) <= threshold);
1084
+ if (nearIdx >= 0) {
1085
+ dragGuideIndexRef.current = nearIdx;
1086
+ } else {
1087
+ dragGuideIndexRef.current = null;
1088
+ }
1089
+ setDragGuideX(docX);
1090
+ }
1091
+ function handlePointerMove(event) {
1092
+ if (!ref.current) return;
1093
+ const rect = ref.current.getBoundingClientRect();
1094
+ const localY = event.clientY - rect.top;
1095
+ setPointerX(event.clientX - rect.left);
1096
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return;
1097
+ const docX = screenXToDoc(event.clientX);
1098
+ if (localY > DELETE_THRESHOLD_PX) {
1099
+ setDragGuideX(docX);
1100
+ } else {
1101
+ setDragGuideX(null);
1102
+ }
1103
+ }
1104
+ function handlePointerUp(event) {
1105
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return;
1106
+ event.currentTarget.releasePointerCapture(event.pointerId);
1107
+ const rect = ref.current?.getBoundingClientRect();
1108
+ const localY = rect ? event.clientY - rect.top : 0;
1109
+ const deletingExisting = dragGuideIndexRef.current !== null;
1110
+ const docX = screenXToDoc(event.clientX);
1111
+ const vertical = [...guides.vertical];
1112
+ if (localY <= DELETE_THRESHOLD_PX && deletingExisting) {
1113
+ vertical.splice(dragGuideIndexRef.current, 1);
1114
+ } else if (localY > DELETE_THRESHOLD_PX) {
1115
+ if (deletingExisting) {
1116
+ vertical[dragGuideIndexRef.current] = docX;
1117
+ } else {
1118
+ vertical.push(docX);
1119
+ }
1120
+ }
1121
+ dragGuideIndexRef.current = null;
1122
+ setDragGuideX(null);
1123
+ onGuidesChange({ ...guides, vertical });
1124
+ }
1125
+ function handlePointerLeave() {
1126
+ setPointerX(null);
1127
+ }
1128
+ return /* @__PURE__ */ jsxs4(
1129
+ "div",
1130
+ {
1131
+ ref,
1132
+ className: "absolute top-0 left-0 right-0 z-10 cursor-ew-resize select-none overflow-hidden border-b border-[var(--border-default)] bg-[var(--bg-input)]",
1133
+ style: { height: RULER_SIZE_PX, marginLeft: RULER_SIZE_PX },
1134
+ onPointerDown: handlePointerDown,
1135
+ onPointerMove: handlePointerMove,
1136
+ onPointerUp: handlePointerUp,
1137
+ onPointerLeave: handlePointerLeave,
1138
+ children: [
1139
+ ticks.map((tick) => {
1140
+ const screenX = tick.position * zoom - scrollLeft * zoom;
1141
+ return /* @__PURE__ */ jsx4(
1142
+ "div",
1143
+ {
1144
+ className: `absolute bottom-0 w-px bg-[var(--border-default)] ${tick.label !== null ? "top-1.5" : "top-[14px]"}`,
1145
+ style: { left: screenX },
1146
+ children: tick.label !== null ? /* @__PURE__ */ jsx4("span", { className: "absolute -top-1 left-0.5 whitespace-nowrap font-mono text-[9px] leading-none text-[var(--text-muted)]", children: tick.label }) : null
1147
+ },
1148
+ tick.position
1149
+ );
1150
+ }),
1151
+ pointerX !== null ? /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute top-0 bottom-0 w-px bg-[var(--brand-primary)]/60", style: { left: pointerX } }) : null,
1152
+ dragGuideX !== null ? /* @__PURE__ */ jsx4(
1153
+ "div",
1154
+ {
1155
+ className: "pointer-events-none absolute top-0 bottom-0 w-px bg-[var(--brand-primary)]",
1156
+ style: { left: dragGuideX * zoom - scrollLeft * zoom }
1157
+ }
1158
+ ) : null
1159
+ ]
1160
+ }
1161
+ );
1162
+ }
1163
+ function VerticalRuler({ pageHeight, zoom, scrollTop, guides, onGuidesChange }) {
1164
+ const ref = useRef2(null);
1165
+ const [pointerY, setPointerY] = useState2(null);
1166
+ const [dragGuideY, setDragGuideY] = useState2(null);
1167
+ const dragGuideIndexRef = useRef2(null);
1168
+ const step = selectTickStep({ zoom, minMajorSpacingPx: 40 });
1169
+ const ticks = buildRulerTicks({ documentLength: pageHeight, step });
1170
+ function screenYToDoc(clientY) {
1171
+ if (!ref.current) return 0;
1172
+ const rect = ref.current.getBoundingClientRect();
1173
+ return screenToDocumentPosition({ pointerScreenPx: clientY - rect.top, scrollOffset: scrollTop, zoom });
1174
+ }
1175
+ function handlePointerDown(event) {
1176
+ if (event.button !== 0) return;
1177
+ event.preventDefault();
1178
+ event.currentTarget.setPointerCapture(event.pointerId);
1179
+ const docY = screenYToDoc(event.clientY);
1180
+ const threshold = 4 / zoom;
1181
+ const nearIdx = guides.horizontal.findIndex((g) => Math.abs(g - docY) <= threshold);
1182
+ dragGuideIndexRef.current = nearIdx >= 0 ? nearIdx : null;
1183
+ setDragGuideY(docY);
1184
+ }
1185
+ function handlePointerMove(event) {
1186
+ if (!ref.current) return;
1187
+ const rect = ref.current.getBoundingClientRect();
1188
+ const localX = event.clientX - rect.left;
1189
+ setPointerY(event.clientY - rect.top);
1190
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return;
1191
+ const docY = screenYToDoc(event.clientY);
1192
+ if (localX > DELETE_THRESHOLD_PX) {
1193
+ setDragGuideY(docY);
1194
+ } else {
1195
+ setDragGuideY(null);
1196
+ }
1197
+ }
1198
+ function handlePointerUp(event) {
1199
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) return;
1200
+ event.currentTarget.releasePointerCapture(event.pointerId);
1201
+ const rect = ref.current?.getBoundingClientRect();
1202
+ const localX = rect ? event.clientX - rect.left : 0;
1203
+ const deletingExisting = dragGuideIndexRef.current !== null;
1204
+ const docY = screenYToDoc(event.clientY);
1205
+ const horizontal = [...guides.horizontal];
1206
+ if (localX <= DELETE_THRESHOLD_PX && deletingExisting) {
1207
+ horizontal.splice(dragGuideIndexRef.current, 1);
1208
+ } else if (localX > DELETE_THRESHOLD_PX) {
1209
+ if (deletingExisting) {
1210
+ horizontal[dragGuideIndexRef.current] = docY;
1211
+ } else {
1212
+ horizontal.push(docY);
1213
+ }
1214
+ }
1215
+ dragGuideIndexRef.current = null;
1216
+ setDragGuideY(null);
1217
+ onGuidesChange({ ...guides, horizontal });
1218
+ }
1219
+ function handlePointerLeave() {
1220
+ setPointerY(null);
1221
+ }
1222
+ return /* @__PURE__ */ jsxs4(
1223
+ "div",
1224
+ {
1225
+ ref,
1226
+ className: "absolute top-0 left-0 bottom-0 z-10 cursor-ns-resize select-none overflow-hidden border-r border-[var(--border-default)] bg-[var(--bg-input)]",
1227
+ style: { width: RULER_SIZE_PX, marginTop: RULER_SIZE_PX },
1228
+ onPointerDown: handlePointerDown,
1229
+ onPointerMove: handlePointerMove,
1230
+ onPointerUp: handlePointerUp,
1231
+ onPointerLeave: handlePointerLeave,
1232
+ children: [
1233
+ ticks.map((tick) => {
1234
+ const screenY = tick.position * zoom - scrollTop * zoom;
1235
+ return /* @__PURE__ */ jsx4(
1236
+ "div",
1237
+ {
1238
+ className: `absolute right-0 h-px bg-[var(--border-default)] ${tick.label !== null ? "left-1.5" : "left-[14px]"}`,
1239
+ style: { top: screenY },
1240
+ children: tick.label !== null ? /* @__PURE__ */ jsx4(
1241
+ "span",
1242
+ {
1243
+ className: "absolute top-0.5 left-0 whitespace-nowrap font-mono text-[9px] leading-none text-[var(--text-muted)]",
1244
+ style: { transform: "rotate(-90deg)", transformOrigin: "0 0", marginTop: 4 },
1245
+ children: tick.label
1246
+ }
1247
+ ) : null
1248
+ },
1249
+ tick.position
1250
+ );
1251
+ }),
1252
+ pointerY !== null ? /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute left-0 right-0 h-px bg-[var(--brand-primary)]/60", style: { top: pointerY } }) : null,
1253
+ dragGuideY !== null ? /* @__PURE__ */ jsx4(
1254
+ "div",
1255
+ {
1256
+ className: "pointer-events-none absolute left-0 right-0 h-px bg-[var(--brand-primary)]",
1257
+ style: { top: dragGuideY * zoom - scrollTop * zoom }
1258
+ }
1259
+ ) : null
1260
+ ]
1261
+ }
1262
+ );
1263
+ }
1264
+
1265
+ // src/design-canvas-react/components/Toolbar.tsx
1266
+ import { useState as useState3 } from "react";
1267
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1268
+ var BTN2 = "flex h-7 w-7 items-center justify-center rounded border border-[var(--border-default)] text-[var(--text-secondary)] transition hover:text-[var(--text-primary)] disabled:cursor-default disabled:opacity-40";
1269
+ var BTN_ACTIVE = `${BTN2} border-[var(--brand-primary)] text-[var(--brand-primary)] hover:text-[var(--brand-primary)]`;
1270
+ var SEP = /* @__PURE__ */ jsx5("div", { className: "mx-1 h-5 w-px bg-[var(--border-default)]" });
1271
+ function NumberInput({
1272
+ label,
1273
+ value,
1274
+ onCommit,
1275
+ min,
1276
+ step = 1,
1277
+ className = "w-16"
1278
+ }) {
1279
+ const [raw, setRaw] = useState3(null);
1280
+ function commit(v) {
1281
+ const n = parseFloat(v);
1282
+ if (Number.isFinite(n) && (min === void 0 || n >= min)) onCommit(n);
1283
+ setRaw(null);
1284
+ }
1285
+ return /* @__PURE__ */ jsxs5("label", { className: "flex flex-col items-center gap-0.5", children: [
1286
+ /* @__PURE__ */ jsx5("span", { className: "text-[9px] uppercase tracking-wide text-[var(--text-muted)]", children: label }),
1287
+ /* @__PURE__ */ jsx5(
1288
+ "input",
1289
+ {
1290
+ type: "number",
1291
+ value: raw ?? value,
1292
+ min,
1293
+ step,
1294
+ onChange: (event) => setRaw(event.target.value),
1295
+ onBlur: (event) => commit(event.target.value),
1296
+ onKeyDown: (event) => {
1297
+ if (event.key === "Enter") commit(event.target.value);
1298
+ if (event.key === "Escape") setRaw(null);
1299
+ },
1300
+ className: `${className} rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-center text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]`
1301
+ }
1302
+ )
1303
+ ] });
1304
+ }
1305
+ function ColorSwatch({ label, value, onCommit }) {
1306
+ return /* @__PURE__ */ jsxs5("label", { className: "flex flex-col items-center gap-0.5 cursor-pointer", children: [
1307
+ /* @__PURE__ */ jsx5("span", { className: "text-[9px] uppercase tracking-wide text-[var(--text-muted)]", children: label }),
1308
+ /* @__PURE__ */ jsx5(
1309
+ "input",
1310
+ {
1311
+ type: "color",
1312
+ value: value.startsWith("#") ? value : "#ffffff",
1313
+ onChange: (event) => onCommit(event.target.value),
1314
+ className: "h-6 w-10 cursor-pointer rounded border border-[var(--border-default)] p-0.5"
1315
+ }
1316
+ )
1317
+ ] });
1318
+ }
1319
+ function Toolbar({
1320
+ page,
1321
+ selectedElements,
1322
+ canWrite,
1323
+ canUndo,
1324
+ canRedo,
1325
+ gridEnabled,
1326
+ snapEnabled,
1327
+ showRulers,
1328
+ showBleed,
1329
+ onUndo,
1330
+ onRedo,
1331
+ onToggleGrid,
1332
+ onToggleSnap,
1333
+ onToggleRulers,
1334
+ onToggleBleed,
1335
+ onSetAttrs,
1336
+ onSetPageProps,
1337
+ onSetPageGuides,
1338
+ onReorder,
1339
+ onGroup,
1340
+ onUngroup,
1341
+ onDelete,
1342
+ onBindSlot
1343
+ }) {
1344
+ const hasSelection = selectedElements.length > 0;
1345
+ const single = selectedElements.length === 1 ? selectedElements[0] : null;
1346
+ const allSameKind = selectedElements.length > 0 && selectedElements.every((e) => e.kind === selectedElements[0].kind);
1347
+ const firstKind = selectedElements[0]?.kind;
1348
+ function patchAll(attrs) {
1349
+ for (const el of selectedElements) onSetAttrs(el.id, attrs);
1350
+ }
1351
+ function reorderSingle(direction) {
1352
+ if (!single) return;
1353
+ onReorder(single.id, 0, page.elements.length, direction);
1354
+ }
1355
+ const selectedIds = selectedElements.map((e) => e.id);
1356
+ const isGroup = single?.kind === "group";
1357
+ const groupable = selectedElements.length >= 2;
1358
+ return /* @__PURE__ */ jsxs5("div", { className: "flex h-11 shrink-0 items-center gap-2 overflow-x-auto border-b border-[var(--border-default)] bg-[var(--bg-input)] px-3", children: [
1359
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Undo", disabled: !canUndo || !canWrite, onClick: onUndo, className: BTN2, children: /* @__PURE__ */ jsx5(UndoGlyph, { className: "h-3.5 w-3.5" }) }),
1360
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Redo", disabled: !canRedo || !canWrite, onClick: onRedo, className: BTN2, children: /* @__PURE__ */ jsx5(RedoGlyph, { className: "h-3.5 w-3.5" }) }),
1361
+ SEP,
1362
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Toggle rulers", "aria-pressed": showRulers, onClick: onToggleRulers, className: showRulers ? BTN_ACTIVE : BTN2, children: /* @__PURE__ */ jsx5(RulerGlyph, { className: "h-3.5 w-3.5" }) }),
1363
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Toggle grid", "aria-pressed": gridEnabled, onClick: onToggleGrid, className: gridEnabled ? BTN_ACTIVE : BTN2, children: /* @__PURE__ */ jsx5(GridGlyph, { className: "h-3.5 w-3.5" }) }),
1364
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Toggle snap", "aria-pressed": snapEnabled, onClick: onToggleSnap, className: snapEnabled ? BTN_ACTIVE : BTN2, children: /* @__PURE__ */ jsx5(MagnetGlyph, { className: "h-3.5 w-3.5" }) }),
1365
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Toggle bleed overlay", "aria-pressed": showBleed, onClick: onToggleBleed, className: showBleed ? BTN_ACTIVE : BTN2, disabled: !page.bleed, children: /* @__PURE__ */ jsx5(BleedGlyph, { className: "h-3.5 w-3.5" }) }),
1366
+ SEP,
1367
+ hasSelection ? /* @__PURE__ */ jsx5(
1368
+ SelectionControls,
1369
+ {
1370
+ elements: selectedElements,
1371
+ single,
1372
+ isGroup,
1373
+ groupable,
1374
+ allSameKind,
1375
+ firstKind,
1376
+ canWrite,
1377
+ patchAll,
1378
+ reorderSingle,
1379
+ onGroup: () => onGroup(selectedIds),
1380
+ onUngroup: () => {
1381
+ if (single) onUngroup(single.id);
1382
+ },
1383
+ onDelete: () => onDelete(selectedIds),
1384
+ onBindSlot: single ? (slot) => onBindSlot(single.id, slot) : void 0,
1385
+ currentSlot: single?.slot ?? null
1386
+ }
1387
+ ) : /* @__PURE__ */ jsx5(
1388
+ PagePropsControls,
1389
+ {
1390
+ page,
1391
+ canWrite,
1392
+ onSetPageProps,
1393
+ onSetPageGuides
1394
+ }
1395
+ )
1396
+ ] });
1397
+ }
1398
+ function SelectionControls({
1399
+ elements,
1400
+ single,
1401
+ isGroup,
1402
+ groupable,
1403
+ allSameKind,
1404
+ firstKind,
1405
+ canWrite,
1406
+ patchAll,
1407
+ reorderSingle,
1408
+ onGroup,
1409
+ onUngroup,
1410
+ onDelete,
1411
+ onBindSlot,
1412
+ currentSlot
1413
+ }) {
1414
+ const [slotPopoverOpen, setSlotPopoverOpen] = useState3(false);
1415
+ const [slotInput, setSlotInput] = useState3("");
1416
+ const firstEl = elements[0];
1417
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
1418
+ allSameKind && firstKind === "text" && single ? /* @__PURE__ */ jsx5(TextControls, { element: single, canWrite, onPatch: (attrs) => patchAll(attrs) }) : null,
1419
+ allSameKind && firstKind === "rect" && single ? /* @__PURE__ */ jsx5(ShapeControls, { element: single, canWrite, onPatch: (attrs) => patchAll(attrs), showCornerRadius: true }) : null,
1420
+ allSameKind && firstKind === "ellipse" && single ? /* @__PURE__ */ jsx5(ShapeControls, { element: single, canWrite, onPatch: (attrs) => patchAll(attrs), showCornerRadius: false }) : null,
1421
+ allSameKind && firstKind === "image" && single ? /* @__PURE__ */ jsx5(ImageControls, { element: single, canWrite, onPatch: (attrs) => patchAll(attrs) }) : null,
1422
+ SEP,
1423
+ /* @__PURE__ */ jsx5(
1424
+ NumberInput,
1425
+ {
1426
+ label: "Opacity",
1427
+ value: Math.round((firstEl.opacity ?? 1) * 100),
1428
+ min: 0,
1429
+ onCommit: (v) => patchAll({ opacity: Math.max(0, Math.min(1, v / 100)) }),
1430
+ className: "w-14"
1431
+ }
1432
+ ),
1433
+ /* @__PURE__ */ jsx5(
1434
+ NumberInput,
1435
+ {
1436
+ label: "Rotation",
1437
+ value: Math.round(firstEl.rotation ?? 0),
1438
+ onCommit: (v) => patchAll({ rotation: v }),
1439
+ className: "w-14"
1440
+ }
1441
+ ),
1442
+ SEP,
1443
+ single ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
1444
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Bring to front", disabled: !canWrite, onClick: () => reorderSingle("front"), className: BTN2, children: /* @__PURE__ */ jsx5(BringFrontGlyph, { className: "h-3.5 w-3.5" }) }),
1445
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Send to back", disabled: !canWrite, onClick: () => reorderSingle("back"), className: BTN2, children: /* @__PURE__ */ jsx5(SendBackGlyph, { className: "h-3.5 w-3.5" }) }),
1446
+ SEP
1447
+ ] }) : null,
1448
+ groupable ? /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Group elements", disabled: !canWrite, onClick: onGroup, className: BTN2, children: /* @__PURE__ */ jsx5(GroupGlyph, { className: "h-3.5 w-3.5" }) }) : null,
1449
+ isGroup ? /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Ungroup", disabled: !canWrite, onClick: onUngroup, className: BTN2, children: /* @__PURE__ */ jsx5(UngroupGlyph, { className: "h-3.5 w-3.5" }) }) : null,
1450
+ single ? /* @__PURE__ */ jsx5(
1451
+ "button",
1452
+ {
1453
+ type: "button",
1454
+ "aria-label": single.locked ? "Unlock element" : "Lock element",
1455
+ disabled: !canWrite,
1456
+ onClick: () => patchAll({ locked: !single.locked }),
1457
+ className: single.locked ? BTN_ACTIVE : BTN2,
1458
+ children: single.locked ? /* @__PURE__ */ jsx5(LockGlyph, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx5(UnlockGlyph, { className: "h-3.5 w-3.5" })
1459
+ }
1460
+ ) : null,
1461
+ single && onBindSlot ? /* @__PURE__ */ jsxs5("div", { className: "relative", children: [
1462
+ /* @__PURE__ */ jsx5(
1463
+ "button",
1464
+ {
1465
+ type: "button",
1466
+ "aria-label": currentSlot ? `Slot: ${currentSlot}` : "Bind slot",
1467
+ onClick: () => {
1468
+ setSlotInput(currentSlot ?? "");
1469
+ setSlotPopoverOpen((v) => !v);
1470
+ },
1471
+ className: currentSlot ? BTN_ACTIVE : BTN2,
1472
+ title: currentSlot ? `Slot: ${currentSlot}` : "Bind slot",
1473
+ children: /* @__PURE__ */ jsx5(SlotGlyph, { className: "h-3.5 w-3.5" })
1474
+ }
1475
+ ),
1476
+ slotPopoverOpen ? /* @__PURE__ */ jsxs5("div", { className: "absolute top-full left-0 z-50 mt-1 flex w-48 flex-col gap-2 rounded border border-[var(--border-default)] bg-[var(--bg-input)] p-2 shadow-lg", children: [
1477
+ /* @__PURE__ */ jsx5(
1478
+ "input",
1479
+ {
1480
+ autoFocus: true,
1481
+ value: slotInput,
1482
+ onChange: (event) => setSlotInput(event.target.value),
1483
+ placeholder: "slot-name",
1484
+ className: "rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]"
1485
+ }
1486
+ ),
1487
+ /* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [
1488
+ /* @__PURE__ */ jsx5(
1489
+ "button",
1490
+ {
1491
+ type: "button",
1492
+ onClick: () => {
1493
+ onBindSlot(slotInput.trim() || null);
1494
+ setSlotPopoverOpen(false);
1495
+ },
1496
+ className: "flex-1 rounded border border-[var(--brand-primary)] px-2 py-0.5 text-[11px] text-[var(--brand-primary)] hover:bg-[var(--brand-primary)]/10",
1497
+ children: slotInput.trim() ? "Bind" : "Unbind"
1498
+ }
1499
+ ),
1500
+ /* @__PURE__ */ jsx5(
1501
+ "button",
1502
+ {
1503
+ type: "button",
1504
+ onClick: () => setSlotPopoverOpen(false),
1505
+ className: "rounded border border-[var(--border-default)] px-2 py-0.5 text-[11px] text-[var(--text-secondary)]",
1506
+ children: "Cancel"
1507
+ }
1508
+ )
1509
+ ] })
1510
+ ] }) : null
1511
+ ] }) : null,
1512
+ SEP,
1513
+ /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Delete selection", disabled: !canWrite, onClick: onDelete, className: BTN2, children: /* @__PURE__ */ jsx5(TrashGlyph, { className: "h-3.5 w-3.5 text-rose-400" }) })
1514
+ ] });
1515
+ }
1516
+ function TextControls({ element, canWrite, onPatch }) {
1517
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
1518
+ /* @__PURE__ */ jsx5(
1519
+ "input",
1520
+ {
1521
+ type: "text",
1522
+ "aria-label": "Font family",
1523
+ value: element.fontFamily,
1524
+ disabled: !canWrite,
1525
+ onChange: (event) => onPatch({ fontFamily: event.target.value }),
1526
+ className: "w-28 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-0.5 text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]",
1527
+ placeholder: "Font"
1528
+ }
1529
+ ),
1530
+ /* @__PURE__ */ jsx5(NumberInput, { label: "Size", value: element.fontSize, min: 1, onCommit: (v) => onPatch({ fontSize: v }), className: "w-12" }),
1531
+ /* @__PURE__ */ jsx5(
1532
+ "button",
1533
+ {
1534
+ type: "button",
1535
+ "aria-label": "Bold",
1536
+ disabled: !canWrite,
1537
+ onClick: () => onPatch({ fontStyle: element.fontStyle === "bold" || element.fontStyle === "bold italic" ? element.fontStyle === "bold italic" ? "italic" : "normal" : element.fontStyle === "italic" ? "bold italic" : "bold" }),
1538
+ className: element.fontStyle?.includes("bold") ? BTN_ACTIVE : BTN2,
1539
+ children: /* @__PURE__ */ jsx5(BoldGlyph, { className: "h-3.5 w-3.5" })
1540
+ }
1541
+ ),
1542
+ /* @__PURE__ */ jsx5(
1543
+ "button",
1544
+ {
1545
+ type: "button",
1546
+ "aria-label": "Italic",
1547
+ disabled: !canWrite,
1548
+ onClick: () => onPatch({ fontStyle: element.fontStyle === "italic" || element.fontStyle === "bold italic" ? element.fontStyle === "bold italic" ? "bold" : "normal" : element.fontStyle === "bold" ? "bold italic" : "italic" }),
1549
+ className: element.fontStyle?.includes("italic") ? BTN_ACTIVE : BTN2,
1550
+ children: /* @__PURE__ */ jsx5(ItalicGlyph, { className: "h-3.5 w-3.5" })
1551
+ }
1552
+ ),
1553
+ ["left", "center", "right"].map((align) => /* @__PURE__ */ jsx5(
1554
+ "button",
1555
+ {
1556
+ type: "button",
1557
+ "aria-label": `Align ${align}`,
1558
+ disabled: !canWrite,
1559
+ onClick: () => onPatch({ align }),
1560
+ className: element.align === align ? BTN_ACTIVE : BTN2,
1561
+ children: align === "left" ? /* @__PURE__ */ jsx5(AlignLeftGlyph, { className: "h-3.5 w-3.5" }) : align === "center" ? /* @__PURE__ */ jsx5(AlignCenterGlyph, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx5(AlignRightGlyph, { className: "h-3.5 w-3.5" })
1562
+ },
1563
+ align
1564
+ )),
1565
+ /* @__PURE__ */ jsx5(NumberInput, { label: "Line H", value: element.lineHeight, step: 0.1, min: 0.5, onCommit: (v) => onPatch({ lineHeight: v }), className: "w-12" }),
1566
+ /* @__PURE__ */ jsx5(NumberInput, { label: "Spacing", value: element.letterSpacing, step: 0.5, onCommit: (v) => onPatch({ letterSpacing: v }), className: "w-14" }),
1567
+ /* @__PURE__ */ jsx5(ColorSwatch, { label: "Fill", value: element.fill, onCommit: (v) => onPatch({ fill: v }) })
1568
+ ] });
1569
+ }
1570
+ function ShapeControls({ element, canWrite, onPatch, showCornerRadius }) {
1571
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
1572
+ /* @__PURE__ */ jsx5(ColorSwatch, { label: "Fill", value: element.fill, onCommit: (v) => onPatch({ fill: v }) }),
1573
+ /* @__PURE__ */ jsx5(ColorSwatch, { label: "Stroke", value: element.stroke ?? "#000000", onCommit: (v) => onPatch({ stroke: v }) }),
1574
+ /* @__PURE__ */ jsx5(NumberInput, { label: "Stroke W", value: element.strokeWidth ?? 0, min: 0, onCommit: (v) => onPatch({ strokeWidth: v }), className: "w-14" }),
1575
+ showCornerRadius && "cornerRadius" in element ? /* @__PURE__ */ jsx5(NumberInput, { label: "Corner R", value: element.cornerRadius ?? 0, min: 0, onCommit: (v) => onPatch({ cornerRadius: v }), className: "w-14" }) : null
1576
+ ] });
1577
+ }
1578
+ function ImageControls({ element, canWrite, onPatch }) {
1579
+ return /* @__PURE__ */ jsxs5("label", { className: "flex flex-col items-center gap-0.5", children: [
1580
+ /* @__PURE__ */ jsx5("span", { className: "text-[9px] uppercase tracking-wide text-[var(--text-muted)]", children: "Fit" }),
1581
+ /* @__PURE__ */ jsxs5(
1582
+ "select",
1583
+ {
1584
+ value: element.fit,
1585
+ disabled: !canWrite,
1586
+ onChange: (event) => onPatch({ fit: event.target.value }),
1587
+ className: "rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]",
1588
+ children: [
1589
+ /* @__PURE__ */ jsx5("option", { value: "fill", children: "Fill" }),
1590
+ /* @__PURE__ */ jsx5("option", { value: "cover", children: "Cover" }),
1591
+ /* @__PURE__ */ jsx5("option", { value: "contain", children: "Contain" })
1592
+ ]
1593
+ }
1594
+ )
1595
+ ] });
1596
+ }
1597
+ function PagePropsControls({ page, canWrite, onSetPageProps, onSetPageGuides }) {
1598
+ const matchedPreset = matchPreset(page.width, page.height);
1599
+ const [customW, setCustomW] = useState3(null);
1600
+ const [customH, setCustomH] = useState3(null);
1601
+ function commitDimension(dim, raw) {
1602
+ const v = parseFloat(raw);
1603
+ if (Number.isFinite(v) && v > 0) onSetPageProps({ [dim]: v });
1604
+ if (dim === "width") setCustomW(null);
1605
+ else setCustomH(null);
1606
+ }
1607
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
1608
+ /* @__PURE__ */ jsx5(
1609
+ "input",
1610
+ {
1611
+ type: "text",
1612
+ "aria-label": "Page name",
1613
+ value: page.name,
1614
+ disabled: !canWrite,
1615
+ onChange: (event) => onSetPageProps({ name: event.target.value }),
1616
+ className: "w-28 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-2 py-0.5 text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]"
1617
+ }
1618
+ ),
1619
+ SEP,
1620
+ /* @__PURE__ */ jsxs5("label", { className: "flex flex-col items-center gap-0.5", children: [
1621
+ /* @__PURE__ */ jsx5("span", { className: "text-[9px] uppercase tracking-wide text-[var(--text-muted)]", children: "Preset" }),
1622
+ /* @__PURE__ */ jsxs5(
1623
+ "select",
1624
+ {
1625
+ value: matchedPreset?.id ?? "custom",
1626
+ disabled: !canWrite,
1627
+ onChange: (event) => {
1628
+ const preset = SIZE_PRESETS.find((p) => p.id === event.target.value);
1629
+ if (preset) onSetPageProps({ width: preset.width, height: preset.height });
1630
+ },
1631
+ className: "rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]",
1632
+ children: [
1633
+ /* @__PURE__ */ jsx5("option", { value: "custom", children: "Custom" }),
1634
+ SIZE_PRESETS.map((p) => /* @__PURE__ */ jsx5("option", { value: p.id, children: p.label }, p.id))
1635
+ ]
1636
+ }
1637
+ )
1638
+ ] }),
1639
+ /* @__PURE__ */ jsxs5("label", { className: "flex flex-col items-center gap-0.5", children: [
1640
+ /* @__PURE__ */ jsx5("span", { className: "text-[9px] uppercase tracking-wide text-[var(--text-muted)]", children: "W" }),
1641
+ /* @__PURE__ */ jsx5(
1642
+ "input",
1643
+ {
1644
+ type: "number",
1645
+ value: customW ?? page.width,
1646
+ min: 1,
1647
+ disabled: !canWrite,
1648
+ onChange: (event) => setCustomW(event.target.value),
1649
+ onBlur: (event) => commitDimension("width", event.target.value),
1650
+ onKeyDown: (event) => {
1651
+ if (event.key === "Enter") commitDimension("width", event.target.value);
1652
+ if (event.key === "Escape") setCustomW(null);
1653
+ },
1654
+ className: "w-16 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-center text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]"
1655
+ }
1656
+ )
1657
+ ] }),
1658
+ /* @__PURE__ */ jsx5("span", { className: "text-[var(--text-muted)]", children: "\xD7" }),
1659
+ /* @__PURE__ */ jsxs5("label", { className: "flex flex-col items-center gap-0.5", children: [
1660
+ /* @__PURE__ */ jsx5("span", { className: "text-[9px] uppercase tracking-wide text-[var(--text-muted)]", children: "H" }),
1661
+ /* @__PURE__ */ jsx5(
1662
+ "input",
1663
+ {
1664
+ type: "number",
1665
+ value: customH ?? page.height,
1666
+ min: 1,
1667
+ disabled: !canWrite,
1668
+ onChange: (event) => setCustomH(event.target.value),
1669
+ onBlur: (event) => commitDimension("height", event.target.value),
1670
+ onKeyDown: (event) => {
1671
+ if (event.key === "Enter") commitDimension("height", event.target.value);
1672
+ if (event.key === "Escape") setCustomH(null);
1673
+ },
1674
+ className: "w-16 rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-1 py-0.5 text-center text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]"
1675
+ }
1676
+ )
1677
+ ] }),
1678
+ SEP,
1679
+ /* @__PURE__ */ jsx5(ColorSwatch, { label: "BG", value: page.background, onCommit: (v) => onSetPageProps({ background: v }) }),
1680
+ SEP,
1681
+ /* @__PURE__ */ jsx5(BleedControls, { page, canWrite, onSetPageProps })
1682
+ ] });
1683
+ }
1684
+ function BleedControls({ page, canWrite, onSetPageProps }) {
1685
+ const bleed = page.bleed;
1686
+ function setBleedSide(side, value) {
1687
+ const current = bleed ?? { top: 0, right: 0, bottom: 0, left: 0 };
1688
+ onSetPageProps({ bleed: { ...current, [side]: value } });
1689
+ }
1690
+ if (!bleed) {
1691
+ return /* @__PURE__ */ jsx5(
1692
+ "button",
1693
+ {
1694
+ type: "button",
1695
+ disabled: !canWrite,
1696
+ onClick: () => onSetPageProps({ bleed: { top: 3, right: 3, bottom: 3, left: 3 } }),
1697
+ className: BTN2,
1698
+ title: "Enable bleed",
1699
+ children: /* @__PURE__ */ jsx5(BleedGlyph, { className: "h-3.5 w-3.5" })
1700
+ }
1701
+ );
1702
+ }
1703
+ return /* @__PURE__ */ jsxs5(Fragment4, { children: [
1704
+ ["top", "right", "bottom", "left"].map((side) => /* @__PURE__ */ jsx5(
1705
+ NumberInput,
1706
+ {
1707
+ label: `Bleed ${side[0].toUpperCase()}`,
1708
+ value: bleed[side],
1709
+ min: 0,
1710
+ onCommit: (v) => setBleedSide(side, v),
1711
+ className: "w-12"
1712
+ },
1713
+ side
1714
+ )),
1715
+ /* @__PURE__ */ jsx5(
1716
+ "button",
1717
+ {
1718
+ type: "button",
1719
+ disabled: !canWrite,
1720
+ onClick: () => onSetPageProps({ bleed: null }),
1721
+ className: BTN2,
1722
+ title: "Remove bleed",
1723
+ children: "\xD7"
1724
+ }
1725
+ )
1726
+ ] });
1727
+ }
1728
+
1729
+ // src/design-canvas-react/components/ZoomControls.tsx
1730
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1731
+ var STEP = 0.1;
1732
+ var MIN = 0.05;
1733
+ var MAX = 32;
1734
+ var BTN3 = "flex h-6 w-6 items-center justify-center rounded border border-[var(--border-default)] text-[var(--text-secondary)] transition hover:text-[var(--text-primary)] disabled:cursor-default disabled:opacity-40";
1735
+ function ZoomControls({ zoom, onZoom, onFit }) {
1736
+ function zoomOut() {
1737
+ onZoom(Math.max(MIN, parseFloat((zoom - STEP).toFixed(4))));
1738
+ }
1739
+ function zoomIn() {
1740
+ onZoom(Math.min(MAX, parseFloat((zoom + STEP).toFixed(4))));
1741
+ }
1742
+ function resetHundred() {
1743
+ onZoom(1);
1744
+ }
1745
+ return /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1 px-2", children: [
1746
+ /* @__PURE__ */ jsx6(
1747
+ "button",
1748
+ {
1749
+ type: "button",
1750
+ "aria-label": "Fit page to viewport",
1751
+ onClick: onFit,
1752
+ className: BTN3,
1753
+ title: "Fit page (F)",
1754
+ children: /* @__PURE__ */ jsx6(ZoomFitGlyph, { className: "h-3.5 w-3.5" })
1755
+ }
1756
+ ),
1757
+ /* @__PURE__ */ jsx6(
1758
+ "button",
1759
+ {
1760
+ type: "button",
1761
+ "aria-label": "Zoom out",
1762
+ onClick: zoomOut,
1763
+ disabled: zoom <= MIN,
1764
+ className: BTN3,
1765
+ children: /* @__PURE__ */ jsx6("span", { className: "text-base leading-none", children: "\u2212" })
1766
+ }
1767
+ ),
1768
+ /* @__PURE__ */ jsxs6(
1769
+ "button",
1770
+ {
1771
+ type: "button",
1772
+ "aria-label": "Reset to 100%",
1773
+ onClick: resetHundred,
1774
+ className: "rounded px-1.5 py-0.5 font-mono text-[11px] tabular-nums text-[var(--text-secondary)] transition hover:bg-[var(--border-default)] hover:text-[var(--text-primary)]",
1775
+ title: "Reset to 100%",
1776
+ children: [
1777
+ Math.round(zoom * 100),
1778
+ "%"
1779
+ ]
1780
+ }
1781
+ ),
1782
+ /* @__PURE__ */ jsx6(
1783
+ "button",
1784
+ {
1785
+ type: "button",
1786
+ "aria-label": "Zoom in",
1787
+ onClick: zoomIn,
1788
+ disabled: zoom >= MAX,
1789
+ className: BTN3,
1790
+ children: /* @__PURE__ */ jsx6("span", { className: "text-sm leading-none", children: "+" })
1791
+ }
1792
+ )
1793
+ ] });
1794
+ }
1795
+
1796
+ // src/design-canvas-react/components/DesignCanvas.tsx
1797
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1798
+ function mintId() {
1799
+ const uuid = globalThis.crypto && "randomUUID" in globalThis.crypto ? globalThis.crypto.randomUUID() : null;
1800
+ return `local-${uuid ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}`;
1801
+ }
1802
+ function isTypingTarget(target) {
1803
+ return target instanceof Element && target.closest('input, textarea, select, [contenteditable="true"]') !== null;
1804
+ }
1805
+ function useCommitCommand(stack, onApplyOperations, canWrite, setError) {
1806
+ return useCallback2(
1807
+ (command) => {
1808
+ if (!canWrite) return;
1809
+ try {
1810
+ stack.execute(command);
1811
+ } catch (error) {
1812
+ setError(error instanceof Error ? error.message : String(error));
1813
+ return;
1814
+ }
1815
+ const ops = command.operations();
1816
+ void onApplyOperations(ops).then((result) => {
1817
+ if (result.document) {
1818
+ stack.reset(result.document);
1819
+ }
1820
+ }).catch((error) => {
1821
+ stack.rollback(command);
1822
+ setError(error instanceof Error ? error.message : String(error));
1823
+ });
1824
+ },
1825
+ [stack, onApplyOperations, canWrite, setError]
1826
+ );
1827
+ }
1828
+ function DesignCanvas({
1829
+ document: initialDocument,
1830
+ rev: initialRev,
1831
+ canWrite,
1832
+ onApplyOperations,
1833
+ onSelectionChange,
1834
+ renderAgentPanel,
1835
+ renderSidePanel,
1836
+ onExport,
1837
+ className,
1838
+ renderWorkspace,
1839
+ renderThumbnail
1840
+ }) {
1841
+ const stack = useMemo(
1842
+ () => createSceneCommandStack(initialDocument, initialDocument.pages[0]?.id ?? "page-1"),
1843
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1844
+ []
1845
+ );
1846
+ const editorState = useSyncExternalStore(stack.subscribe, stack.getState, stack.getState);
1847
+ const appliedDocumentRef = useRef4(initialDocument);
1848
+ useEffect2(() => {
1849
+ if (appliedDocumentRef.current === initialDocument) return;
1850
+ appliedDocumentRef.current = initialDocument;
1851
+ stack.reset(initialDocument);
1852
+ }, [initialDocument, stack]);
1853
+ const [commitError, setCommitError] = useState4(null);
1854
+ const commit = useCommitCommand(stack, onApplyOperations, canWrite, setCommitError);
1855
+ const selectionChangeRef = useRef4(onSelectionChange);
1856
+ selectionChangeRef.current = onSelectionChange;
1857
+ useEffect2(() => {
1858
+ const page = editorState.document.pages.find((p) => p.id === editorState.activePageId);
1859
+ if (!page) return;
1860
+ const selected = editorState.selectedElementIds.map((id) => page.elements.find((el) => el.id === id)).filter((el) => el !== void 0);
1861
+ selectionChangeRef.current?.(selected);
1862
+ }, [editorState.selectedElementIds, editorState.activePageId, editorState.document]);
1863
+ const fitRef = useRef4(null);
1864
+ const setZoom = useCallback2((zoom) => stack.setView({ zoom }), [stack]);
1865
+ const setPan = useCallback2((panX, panY) => stack.setView({ panX, panY }), [stack]);
1866
+ const setActivePage = useCallback2((activePageId) => stack.setView({ activePageId, selectedElementIds: [] }), [stack]);
1867
+ const setSelectedElements = useCallback2(
1868
+ (ids, additive) => {
1869
+ if (!additive) {
1870
+ stack.setView({ selectedElementIds: ids });
1871
+ return;
1872
+ }
1873
+ const current = new Set(editorState.selectedElementIds);
1874
+ for (const id of ids) {
1875
+ if (current.has(id)) current.delete(id);
1876
+ else current.add(id);
1877
+ }
1878
+ stack.setView({ selectedElementIds: [...current] });
1879
+ },
1880
+ [stack, editorState.selectedElementIds]
1881
+ );
1882
+ const handleSetAttrs = useCallback2(
1883
+ (elementId, attrs) => {
1884
+ const page = requirePage(editorState.document, editorState.activePageId);
1885
+ const found = findElement(page, elementId);
1886
+ if (!found) return;
1887
+ if (found.element.locked) return;
1888
+ const priorAttrs = Object.fromEntries(
1889
+ Object.keys(attrs).map((k) => [k, found.element[k]])
1890
+ );
1891
+ commit(
1892
+ setAttrsCommand({
1893
+ pageId: editorState.activePageId,
1894
+ elementId,
1895
+ attrs,
1896
+ priorAttrs
1897
+ })
1898
+ );
1899
+ },
1900
+ [commit, editorState.activePageId, editorState.document]
1901
+ );
1902
+ const handleMultiSetAttrs = useCallback2(
1903
+ (patches) => {
1904
+ const page = requirePage(editorState.document, editorState.activePageId);
1905
+ const entries = patches.flatMap(({ elementId, attrs }) => {
1906
+ const found = findElement(page, elementId);
1907
+ if (!found || found.element.locked) return [];
1908
+ const priorAttrs = Object.fromEntries(
1909
+ Object.keys(attrs).map((k) => [k, found.element[k]])
1910
+ );
1911
+ return [{ pageId: editorState.activePageId, elementId, attrs, priorAttrs }];
1912
+ });
1913
+ if (entries.length === 0) return;
1914
+ commit(multiSetAttrsCommand(entries));
1915
+ },
1916
+ [commit, editorState.activePageId, editorState.document]
1917
+ );
1918
+ const handleReorder = useCallback2(
1919
+ (elementId, toIndex, ownerLength, direction) => {
1920
+ const page = requirePage(editorState.document, editorState.activePageId);
1921
+ const found = findElement(page, elementId);
1922
+ if (!found) return;
1923
+ const currentIndex = found.index;
1924
+ const target = direction === "front" ? topIndex(ownerLength) : direction === "back" ? 0 : direction === "forward" ? indexForward(currentIndex, ownerLength) : indexBackward(currentIndex);
1925
+ const clamped = clampIndex(target, ownerLength);
1926
+ if (clamped === currentIndex) return;
1927
+ commit(reorderElementCommand({ pageId: editorState.activePageId, elementId, toIndex: clamped }));
1928
+ },
1929
+ [commit, editorState.activePageId, editorState.document]
1930
+ );
1931
+ const handleDelete = useCallback2(
1932
+ (elementIds) => {
1933
+ for (const elementId of elementIds) {
1934
+ commit(
1935
+ deleteElementCommand({
1936
+ document: editorState.document,
1937
+ pageId: editorState.activePageId,
1938
+ elementId
1939
+ })
1940
+ );
1941
+ }
1942
+ },
1943
+ [commit, editorState.document, editorState.activePageId]
1944
+ );
1945
+ const handleGroup = useCallback2(
1946
+ (elementIds) => {
1947
+ commit(
1948
+ groupElementsCommand({
1949
+ document: editorState.document,
1950
+ pageId: editorState.activePageId,
1951
+ elementIds,
1952
+ groupId: mintId()
1953
+ })
1954
+ );
1955
+ },
1956
+ [commit, editorState.document, editorState.activePageId]
1957
+ );
1958
+ const handleUngroup = useCallback2(
1959
+ (groupId) => {
1960
+ commit(ungroupElementCommand({ document: editorState.document, pageId: editorState.activePageId, groupId }));
1961
+ },
1962
+ [commit, editorState.document, editorState.activePageId]
1963
+ );
1964
+ const handleBindSlot = useCallback2(
1965
+ (elementId, slot) => {
1966
+ commit(bindSlotCommand({ document: editorState.document, pageId: editorState.activePageId, elementId, slot }));
1967
+ },
1968
+ [commit, editorState.document, editorState.activePageId]
1969
+ );
1970
+ const handleSetPageProps = useCallback2(
1971
+ (props) => {
1972
+ commit(setPagePropsCommand({ document: editorState.document, pageId: editorState.activePageId, props }));
1973
+ },
1974
+ [commit, editorState.document, editorState.activePageId]
1975
+ );
1976
+ const handleSetPageGuides = useCallback2(
1977
+ (guides) => {
1978
+ commit(setPageGuidesCommand({ document: editorState.document, pageId: editorState.activePageId, guides }));
1979
+ },
1980
+ [commit, editorState.document, editorState.activePageId]
1981
+ );
1982
+ const handleAddPage = useCallback2(() => {
1983
+ const pageId = mintId();
1984
+ commit(addPageCommand({ pageId }));
1985
+ setActivePage(pageId);
1986
+ }, [commit, setActivePage]);
1987
+ const handleDuplicatePage = useCallback2(
1988
+ (sourcePageId) => {
1989
+ const pageId = mintId();
1990
+ commit(duplicatePageCommand({ document: editorState.document, sourcePageId, pageId }));
1991
+ setActivePage(pageId);
1992
+ },
1993
+ [commit, editorState.document, setActivePage]
1994
+ );
1995
+ const handleDeletePage = useCallback2(
1996
+ (pageId) => {
1997
+ commit(deletePageCommand({ document: editorState.document, pageId }));
1998
+ },
1999
+ [commit, editorState.document]
2000
+ );
2001
+ const handleReorderPage = useCallback2(
2002
+ (pageId, toIndex) => {
2003
+ commit(reorderPageCommand({ pageId, toIndex }));
2004
+ },
2005
+ [commit]
2006
+ );
2007
+ const handleUndo = useCallback2(() => {
2008
+ if (!canWrite || !stack.canUndo()) return;
2009
+ let command;
2010
+ try {
2011
+ command = stack.undo();
2012
+ } catch (error) {
2013
+ setCommitError(`Undo failed: ${error instanceof Error ? error.message : String(error)}`);
2014
+ return;
2015
+ }
2016
+ void onApplyOperations(command.inverseOperations()).then((result) => {
2017
+ if (result.document) stack.reset(result.document);
2018
+ }).catch((error) => {
2019
+ if (stack.canRedo()) {
2020
+ try {
2021
+ stack.redo();
2022
+ } catch {
2023
+ }
2024
+ }
2025
+ setCommitError(error instanceof Error ? error.message : String(error));
2026
+ });
2027
+ }, [stack, canWrite, onApplyOperations]);
2028
+ const handleRedo = useCallback2(() => {
2029
+ if (!canWrite || !stack.canRedo()) return;
2030
+ let command;
2031
+ try {
2032
+ command = stack.redo();
2033
+ } catch (error) {
2034
+ setCommitError(`Redo failed: ${error instanceof Error ? error.message : String(error)}`);
2035
+ return;
2036
+ }
2037
+ void onApplyOperations(command.operations()).then((result) => {
2038
+ if (result.document) stack.reset(result.document);
2039
+ }).catch((error) => {
2040
+ if (stack.canUndo()) {
2041
+ try {
2042
+ stack.undo();
2043
+ } catch {
2044
+ }
2045
+ }
2046
+ setCommitError(error instanceof Error ? error.message : String(error));
2047
+ });
2048
+ }, [stack, canWrite, onApplyOperations]);
2049
+ useEffect2(() => {
2050
+ function onKeyDown(event) {
2051
+ const mod = event.metaKey || event.ctrlKey;
2052
+ if (mod && !isTypingTarget(event.target)) {
2053
+ if (event.key.toLowerCase() === "z") {
2054
+ event.preventDefault();
2055
+ if (event.shiftKey) handleRedo();
2056
+ else handleUndo();
2057
+ return;
2058
+ }
2059
+ if (event.key.toLowerCase() === "y") {
2060
+ event.preventDefault();
2061
+ handleRedo();
2062
+ return;
2063
+ }
2064
+ }
2065
+ if ((event.key === "Delete" || event.key === "Backspace") && !isTypingTarget(event.target)) {
2066
+ if (!canWrite || editorState.selectedElementIds.length === 0) return;
2067
+ event.preventDefault();
2068
+ handleDelete(editorState.selectedElementIds);
2069
+ return;
2070
+ }
2071
+ if (event.key.toLowerCase() === "f" && !isTypingTarget(event.target)) {
2072
+ event.preventDefault();
2073
+ fitRef.current?.();
2074
+ }
2075
+ }
2076
+ window.addEventListener("keydown", onKeyDown);
2077
+ return () => window.removeEventListener("keydown", onKeyDown);
2078
+ });
2079
+ const activePage = useMemo(
2080
+ () => editorState.document.pages.find((p) => p.id === editorState.activePageId),
2081
+ [editorState.document, editorState.activePageId]
2082
+ );
2083
+ const selectedElements = useMemo(() => {
2084
+ if (!activePage) return [];
2085
+ return editorState.selectedElementIds.map((id) => activePage.elements.find((el) => el.id === id)).filter((el) => el !== void 0);
2086
+ }, [activePage, editorState.selectedElementIds]);
2087
+ if (!activePage) {
2088
+ return /* @__PURE__ */ jsx7("div", { className: `flex h-full items-center justify-center bg-[var(--bg-input)] text-[var(--text-muted)] ${className ?? ""}`, children: "No pages in document" });
2089
+ }
2090
+ const bleedScreen = editorState.showBleed && activePage.bleed ? {
2091
+ top: activePage.bleed.top * editorState.zoom,
2092
+ right: activePage.bleed.right * editorState.zoom,
2093
+ bottom: activePage.bleed.bottom * editorState.zoom,
2094
+ left: activePage.bleed.left * editorState.zoom
2095
+ } : null;
2096
+ return /* @__PURE__ */ jsxs7("div", { className: `flex h-full min-h-0 bg-[var(--bg-input)] text-[var(--text-primary)] ${className ?? ""}`, children: [
2097
+ renderSidePanel ? /* @__PURE__ */ jsx7("aside", { className: "flex w-64 shrink-0 flex-col overflow-hidden border-r border-[var(--border-default)]", children: renderSidePanel() }) : null,
2098
+ /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 flex-1 flex-col", children: [
2099
+ /* @__PURE__ */ jsx7(
2100
+ Toolbar,
2101
+ {
2102
+ page: activePage,
2103
+ selectedElements,
2104
+ canWrite,
2105
+ canUndo: stack.canUndo(),
2106
+ canRedo: stack.canRedo(),
2107
+ gridEnabled: editorState.gridEnabled,
2108
+ snapEnabled: editorState.snapEnabled,
2109
+ showRulers: editorState.showRulers,
2110
+ showBleed: editorState.showBleed,
2111
+ onUndo: handleUndo,
2112
+ onRedo: handleRedo,
2113
+ onToggleGrid: () => stack.setView({ gridEnabled: !editorState.gridEnabled }),
2114
+ onToggleSnap: () => stack.setView({ snapEnabled: !editorState.snapEnabled }),
2115
+ onToggleRulers: () => stack.setView({ showRulers: !editorState.showRulers }),
2116
+ onToggleBleed: () => stack.setView({ showBleed: !editorState.showBleed }),
2117
+ onSetAttrs: handleSetAttrs,
2118
+ onSetPageProps: handleSetPageProps,
2119
+ onSetPageGuides: handleSetPageGuides,
2120
+ onReorder: handleReorder,
2121
+ onGroup: handleGroup,
2122
+ onUngroup: handleUngroup,
2123
+ onDelete: handleDelete,
2124
+ onBindSlot: handleBindSlot
2125
+ }
2126
+ ),
2127
+ commitError ? /* @__PURE__ */ jsxs7(
2128
+ "div",
2129
+ {
2130
+ className: "flex shrink-0 items-center justify-between gap-3 border-b border-rose-500/30 bg-rose-500/10 px-3 py-1.5 text-xs text-rose-300",
2131
+ role: "alert",
2132
+ children: [
2133
+ /* @__PURE__ */ jsx7("span", { className: "min-w-0 truncate", children: commitError }),
2134
+ /* @__PURE__ */ jsx7(
2135
+ "button",
2136
+ {
2137
+ type: "button",
2138
+ onClick: () => setCommitError(null),
2139
+ className: "shrink-0 underline-offset-2 hover:underline",
2140
+ children: "Dismiss"
2141
+ }
2142
+ )
2143
+ ]
2144
+ }
2145
+ ) : null,
2146
+ /* @__PURE__ */ jsxs7("div", { className: "relative min-h-0 flex-1", children: [
2147
+ /* @__PURE__ */ jsx7(
2148
+ Rulers,
2149
+ {
2150
+ pageWidth: activePage.width,
2151
+ pageHeight: activePage.height,
2152
+ zoom: editorState.zoom,
2153
+ scrollLeft: -editorState.panX / editorState.zoom,
2154
+ scrollTop: -editorState.panY / editorState.zoom,
2155
+ showRulers: editorState.showRulers,
2156
+ guides: activePage.guides,
2157
+ onGuidesChange: handleSetPageGuides
2158
+ }
2159
+ ),
2160
+ bleedScreen && activePage.bleed ? /* @__PURE__ */ jsx7(
2161
+ "div",
2162
+ {
2163
+ className: "pointer-events-none absolute inset-0 z-10 overflow-hidden",
2164
+ "aria-hidden": true,
2165
+ children: /* @__PURE__ */ jsx7(
2166
+ "div",
2167
+ {
2168
+ style: {
2169
+ position: "absolute",
2170
+ left: editorState.panX,
2171
+ top: editorState.panY
2172
+ },
2173
+ children: /* @__PURE__ */ jsx7(
2174
+ BleedTrimOverlay,
2175
+ {
2176
+ pageWidthPx: activePage.width * editorState.zoom,
2177
+ pageHeightPx: activePage.height * editorState.zoom,
2178
+ bleed: bleedScreen
2179
+ }
2180
+ )
2181
+ }
2182
+ )
2183
+ }
2184
+ ) : null,
2185
+ renderWorkspace({
2186
+ document: editorState.document,
2187
+ activePageId: editorState.activePageId,
2188
+ selectedElementIds: editorState.selectedElementIds,
2189
+ zoom: editorState.zoom,
2190
+ panX: editorState.panX,
2191
+ panY: editorState.panY,
2192
+ gridEnabled: editorState.gridEnabled,
2193
+ gridSize: editorState.gridSize,
2194
+ snapEnabled: editorState.snapEnabled,
2195
+ showBleed: editorState.showBleed,
2196
+ canWrite,
2197
+ stack,
2198
+ activePage,
2199
+ onFitRef: fitRef,
2200
+ onZoomChange: setZoom,
2201
+ onPanChange: setPan,
2202
+ onSelectElements: setSelectedElements
2203
+ })
2204
+ ] }),
2205
+ /* @__PURE__ */ jsxs7("div", { className: "flex shrink-0 items-stretch border-t border-[var(--border-default)]", children: [
2206
+ /* @__PURE__ */ jsx7("div", { className: "min-w-0 flex-1 overflow-hidden", children: /* @__PURE__ */ jsx7(
2207
+ PagesStrip,
2208
+ {
2209
+ pages: editorState.document.pages,
2210
+ activePageId: editorState.activePageId,
2211
+ canWrite,
2212
+ renderThumbnail,
2213
+ onSelectPage: setActivePage,
2214
+ onAddPage: handleAddPage,
2215
+ onDuplicatePage: handleDuplicatePage,
2216
+ onDeletePage: handleDeletePage,
2217
+ onReorderPage: handleReorderPage
2218
+ }
2219
+ ) }),
2220
+ /* @__PURE__ */ jsx7("div", { className: "flex shrink-0 items-center border-l border-[var(--border-default)]", children: /* @__PURE__ */ jsx7(
2221
+ ZoomControls,
2222
+ {
2223
+ zoom: editorState.zoom,
2224
+ onZoom: setZoom,
2225
+ onFit: () => fitRef.current?.()
2226
+ }
2227
+ ) })
2228
+ ] })
2229
+ ] }),
2230
+ renderAgentPanel ? /* @__PURE__ */ jsx7("aside", { className: "flex w-80 shrink-0 flex-col overflow-hidden border-l border-[var(--border-default)]", children: renderAgentPanel({ selectedElements, activePageId: editorState.activePageId }) }) : null
2231
+ ] });
2232
+ }
2233
+ var DesignCanvas_default = DesignCanvas;
2234
+
2235
+ export {
2236
+ SCENE_COMMAND_HISTORY_LIMIT,
2237
+ createSceneCommandStack,
2238
+ addElementCommand,
2239
+ setAttrsCommand,
2240
+ multiSetAttrsCommand,
2241
+ reorderElementCommand,
2242
+ deleteElementCommand,
2243
+ groupElementsCommand,
2244
+ ungroupElementCommand,
2245
+ addPageCommand,
2246
+ duplicatePageCommand,
2247
+ deletePageCommand,
2248
+ reorderPageCommand,
2249
+ setPagePropsCommand,
2250
+ setPageGuidesCommand,
2251
+ bindSlotCommand,
2252
+ setDocumentTitleCommand,
2253
+ selectTickStep,
2254
+ buildRulerTicks,
2255
+ formatRulerLabel,
2256
+ BleedTrimOverlay,
2257
+ EyeGlyph,
2258
+ EyeOffGlyph,
2259
+ LockGlyph,
2260
+ UnlockGlyph,
2261
+ GroupGlyph,
2262
+ RectGlyph,
2263
+ EllipseGlyph,
2264
+ LineGlyph,
2265
+ TextGlyph,
2266
+ ImageGlyph,
2267
+ VideoGlyph,
2268
+ SlotGlyph,
2269
+ PagesStrip,
2270
+ Rulers,
2271
+ Toolbar,
2272
+ ZoomControls,
2273
+ DesignCanvas,
2274
+ DesignCanvas_default
2275
+ };
2276
+ //# sourceMappingURL=chunk-F5KTWRO7.js.map