@deckedout/visual-editor 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,4037 @@
1
+ "use client"
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+
34
+ // src/utils/editorUtils.ts
35
+ var editorUtils_exports = {};
36
+ __export(editorUtils_exports, {
37
+ bringToFront: () => bringToFront,
38
+ checkOverlap: () => checkOverlap,
39
+ clamp: () => clamp,
40
+ cloneElement: () => cloneElement,
41
+ constrainToCanvas: () => constrainToCanvas,
42
+ createElement: () => createElement,
43
+ degToRad: () => degToRad,
44
+ distance: () => distance,
45
+ duplicateElement: () => duplicateElement,
46
+ exportToJSON: () => exportToJSON,
47
+ generateElementId: () => generateElementId,
48
+ getElementCenter: () => getElementCenter,
49
+ getMaxZIndex: () => getMaxZIndex,
50
+ getRotatedBoundingBox: () => getRotatedBoundingBox,
51
+ importFromJSON: () => importFromJSON,
52
+ isValidCanvasExport: () => isValidCanvasExport,
53
+ isValidElement: () => isValidElement,
54
+ pointInRect: () => pointInRect,
55
+ radToDeg: () => radToDeg,
56
+ sendToBack: () => sendToBack,
57
+ snapPositionToGrid: () => snapPositionToGrid,
58
+ snapToGrid: () => snapToGrid,
59
+ sortByZIndex: () => sortByZIndex
60
+ });
61
+ var import_uuid, generateElementId, createElement, cloneElement, duplicateElement, sortByZIndex, getMaxZIndex, bringToFront, sendToBack, checkOverlap, pointInRect, snapToGrid, snapPositionToGrid, clamp, constrainToCanvas, getRotatedBoundingBox, exportToJSON, importFromJSON, getElementCenter, distance, degToRad, radToDeg, isValidElement, isValidCanvasExport;
62
+ var init_editorUtils = __esm({
63
+ "src/utils/editorUtils.ts"() {
64
+ "use strict";
65
+ import_uuid = require("uuid");
66
+ generateElementId = () => {
67
+ return `element-${(0, import_uuid.v4)()}`;
68
+ };
69
+ createElement = (type, props, options) => {
70
+ return {
71
+ id: generateElementId(),
72
+ type,
73
+ position: options?.position || { x: 0, y: 0 },
74
+ size: options?.size || { width: 100, height: 100 },
75
+ rotation: options?.rotation || 0,
76
+ opacity: options?.opacity ?? 1,
77
+ zIndex: options?.zIndex || 0,
78
+ visible: options?.visible ?? true,
79
+ locked: options?.locked ?? false,
80
+ displayName: options?.displayName,
81
+ props
82
+ };
83
+ };
84
+ cloneElement = (element) => {
85
+ return {
86
+ ...element,
87
+ id: generateElementId(),
88
+ // New ID for the clone
89
+ props: { ...element.props },
90
+ position: { ...element.position },
91
+ size: { ...element.size }
92
+ };
93
+ };
94
+ duplicateElement = (element, offset = { x: 20, y: 20 }) => {
95
+ const cloned = cloneElement(element);
96
+ return {
97
+ ...cloned,
98
+ position: {
99
+ x: element.position.x + offset.x,
100
+ y: element.position.y + offset.y
101
+ },
102
+ zIndex: element.zIndex + 1
103
+ // Place on top
104
+ };
105
+ };
106
+ sortByZIndex = (elements) => {
107
+ return [...elements].sort((a, b) => a.zIndex - b.zIndex);
108
+ };
109
+ getMaxZIndex = (elements) => {
110
+ if (elements.length === 0) return 0;
111
+ return Math.max(...elements.map((el) => el.zIndex));
112
+ };
113
+ bringToFront = (elements, elementId) => {
114
+ const maxZ = getMaxZIndex(elements);
115
+ return elements.map((el) => el.id === elementId ? { ...el, zIndex: maxZ + 1 } : el);
116
+ };
117
+ sendToBack = (elements, elementId) => {
118
+ const minZ = Math.min(...elements.map((el) => el.zIndex));
119
+ return elements.map((el) => el.id === elementId ? { ...el, zIndex: minZ - 1 } : el);
120
+ };
121
+ checkOverlap = (rect1, rect2) => {
122
+ return !(rect1.x + rect1.width < rect2.x || rect2.x + rect2.width < rect1.x || rect1.y + rect1.height < rect2.y || rect2.y + rect2.height < rect1.y);
123
+ };
124
+ pointInRect = (point, rect) => {
125
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
126
+ };
127
+ snapToGrid = (value, gridSize) => {
128
+ return Math.round(value / gridSize) * gridSize;
129
+ };
130
+ snapPositionToGrid = (position, gridSize) => {
131
+ return {
132
+ x: snapToGrid(position.x, gridSize),
133
+ y: snapToGrid(position.y, gridSize)
134
+ };
135
+ };
136
+ clamp = (value, min, max) => {
137
+ return Math.min(Math.max(value, min), max);
138
+ };
139
+ constrainToCanvas = (position, size, canvasSize) => {
140
+ return {
141
+ x: clamp(position.x, 0, canvasSize.width - size.width),
142
+ y: clamp(position.y, 0, canvasSize.height - size.height)
143
+ };
144
+ };
145
+ getRotatedBoundingBox = (x, y, width, height, rotation) => {
146
+ const rad = rotation * Math.PI / 180;
147
+ const cos = Math.abs(Math.cos(rad));
148
+ const sin = Math.abs(Math.sin(rad));
149
+ const newWidth = width * cos + height * sin;
150
+ const newHeight = width * sin + height * cos;
151
+ return {
152
+ x: x - (newWidth - width) / 2,
153
+ y: y - (newHeight - height) / 2,
154
+ width: newWidth,
155
+ height: newHeight
156
+ };
157
+ };
158
+ exportToJSON = (data) => {
159
+ return JSON.stringify(data, null, 2);
160
+ };
161
+ importFromJSON = (json) => {
162
+ try {
163
+ const data = JSON.parse(json);
164
+ if (!data.width || !data.height || !Array.isArray(data.elements)) {
165
+ throw new Error("Invalid canvas data structure");
166
+ }
167
+ const normalizedElements = data.elements.map((element) => ({
168
+ ...element,
169
+ visible: element.visible ?? true,
170
+ locked: element.locked ?? false
171
+ }));
172
+ return {
173
+ ...data,
174
+ elements: normalizedElements
175
+ };
176
+ } catch (error) {
177
+ throw new Error(`Failed to parse canvas data: ${error.message}`);
178
+ }
179
+ };
180
+ getElementCenter = (element) => {
181
+ return {
182
+ x: element.position.x + element.size.width / 2,
183
+ y: element.position.y + element.size.height / 2
184
+ };
185
+ };
186
+ distance = (p1, p2) => {
187
+ const dx = p2.x - p1.x;
188
+ const dy = p2.y - p1.y;
189
+ return Math.sqrt(dx * dx + dy * dy);
190
+ };
191
+ degToRad = (degrees) => {
192
+ return degrees * Math.PI / 180;
193
+ };
194
+ radToDeg = (radians) => {
195
+ return radians * 180 / Math.PI;
196
+ };
197
+ isValidElement = (element) => {
198
+ return element && typeof element.id === "string" && typeof element.type === "string" && element.position && typeof element.position.x === "number" && typeof element.position.y === "number" && element.size && typeof element.size.width === "number" && typeof element.size.height === "number" && typeof element.rotation === "number" && typeof element.zIndex === "number" && element.props !== void 0;
199
+ };
200
+ isValidCanvasExport = (data) => {
201
+ return data && typeof data.width === "number" && typeof data.height === "number" && Array.isArray(data.elements) && data.elements.every(isValidElement);
202
+ };
203
+ }
204
+ });
205
+
206
+ // src/index.ts
207
+ var index_exports = {};
208
+ __export(index_exports, {
209
+ AssetPicker: () => AssetPicker,
210
+ ElementRegistry: () => ElementRegistry,
211
+ ImageElementRenderer: () => ImageElementRenderer,
212
+ Inspector: () => Inspector,
213
+ LayersPanel: () => LayersPanel,
214
+ TextElementRenderer: () => TextElementRenderer,
215
+ VisualEditor: () => VisualEditor,
216
+ VisualEditorWorkspace: () => VisualEditorWorkspace,
217
+ bringToFront: () => bringToFront,
218
+ checkOverlap: () => checkOverlap,
219
+ clamp: () => clamp,
220
+ cloneElement: () => cloneElement,
221
+ constrainToCanvas: () => constrainToCanvas,
222
+ createElement: () => createElement,
223
+ defaultElements: () => defaultElements,
224
+ degToRad: () => degToRad,
225
+ distance: () => distance,
226
+ duplicateElement: () => duplicateElement,
227
+ exportToJSON: () => exportToJSON,
228
+ generateElementId: () => generateElementId,
229
+ getElementCenter: () => getElementCenter,
230
+ getMaxZIndex: () => getMaxZIndex,
231
+ getRotatedBoundingBox: () => getRotatedBoundingBox,
232
+ getSnappingPosition: () => getSnappingPosition,
233
+ globalElementRegistry: () => globalElementRegistry,
234
+ imageElementRenderer: () => imageElementRenderer,
235
+ importFromJSON: () => importFromJSON,
236
+ isValidCanvasExport: () => isValidCanvasExport,
237
+ isValidElement: () => isValidElement,
238
+ pointInRect: () => pointInRect,
239
+ radToDeg: () => radToDeg,
240
+ renderField: () => renderField,
241
+ sendToBack: () => sendToBack,
242
+ snapPositionToGrid: () => snapPositionToGrid,
243
+ snapToGrid: () => snapToGrid,
244
+ sortByZIndex: () => sortByZIndex,
245
+ textElementRenderer: () => textElementRenderer,
246
+ useEditorState: () => useEditorState,
247
+ useElementRegistry: () => useElementRegistry
248
+ });
249
+ module.exports = __toCommonJS(index_exports);
250
+
251
+ // src/core/VisualEditor.tsx
252
+ var import_react5 = require("react");
253
+ var import_react_konva3 = require("react-konva");
254
+
255
+ // src/core/useEditorState.ts
256
+ var import_react = require("react");
257
+ var createInitialState = (mode) => ({
258
+ elements: [],
259
+ selectedElementId: null,
260
+ canvasSize: mode?.defaultCanvasSize || { width: 800, height: 600 },
261
+ zoom: 1,
262
+ pan: { x: 0, y: 0 },
263
+ mode,
264
+ history: {
265
+ past: [],
266
+ future: []
267
+ }
268
+ });
269
+ var editorReducer = (state, action) => {
270
+ switch (action.type) {
271
+ case "ADD_ELEMENT": {
272
+ const normalizedElement = {
273
+ ...action.element,
274
+ visible: action.element.visible ?? true,
275
+ locked: action.element.locked ?? false
276
+ };
277
+ const newElements = [...state.elements, normalizedElement];
278
+ return {
279
+ ...state,
280
+ elements: newElements,
281
+ selectedElementId: normalizedElement.id,
282
+ history: {
283
+ past: [...state.history.past, state.elements],
284
+ future: []
285
+ }
286
+ };
287
+ }
288
+ case "UPDATE_ELEMENT": {
289
+ const newElements = state.elements.map(
290
+ (el) => el.id === action.id ? { ...el, ...action.updates } : el
291
+ );
292
+ return {
293
+ ...state,
294
+ elements: newElements,
295
+ history: {
296
+ past: [...state.history.past, state.elements],
297
+ future: []
298
+ }
299
+ };
300
+ }
301
+ case "REMOVE_ELEMENT": {
302
+ const newElements = state.elements.filter((el) => el.id !== action.id);
303
+ return {
304
+ ...state,
305
+ elements: newElements,
306
+ selectedElementId: state.selectedElementId === action.id ? null : state.selectedElementId,
307
+ history: {
308
+ past: [...state.history.past, state.elements],
309
+ future: []
310
+ }
311
+ };
312
+ }
313
+ case "SELECT_ELEMENT":
314
+ return {
315
+ ...state,
316
+ selectedElementId: action.id
317
+ };
318
+ case "SET_ELEMENTS":
319
+ return {
320
+ ...state,
321
+ elements: action.elements,
322
+ history: {
323
+ past: [...state.history.past, state.elements],
324
+ future: []
325
+ }
326
+ };
327
+ case "LOAD_ELEMENTS":
328
+ return {
329
+ ...state,
330
+ elements: action.elements.map((el) => ({
331
+ ...el,
332
+ visible: el.visible ?? true,
333
+ locked: el.locked ?? false
334
+ }))
335
+ };
336
+ case "REORDER_ELEMENT": {
337
+ const currentIndex = state.elements.findIndex((el) => el.id === action.elementId);
338
+ if (currentIndex === -1 || currentIndex === action.newIndex) return state;
339
+ const newElements = [...state.elements];
340
+ const [element] = newElements.splice(currentIndex, 1);
341
+ newElements.splice(action.newIndex, 0, element);
342
+ newElements.forEach((el, index) => {
343
+ el.zIndex = index;
344
+ });
345
+ return {
346
+ ...state,
347
+ elements: newElements,
348
+ history: {
349
+ past: [...state.history.past, state.elements],
350
+ future: []
351
+ }
352
+ };
353
+ }
354
+ case "SET_CANVAS_SIZE":
355
+ return {
356
+ ...state,
357
+ canvasSize: {
358
+ width: action.width,
359
+ height: action.height
360
+ }
361
+ };
362
+ case "SET_ZOOM":
363
+ return {
364
+ ...state,
365
+ zoom: action.zoom
366
+ };
367
+ case "SET_PAN":
368
+ return {
369
+ ...state,
370
+ pan: {
371
+ x: action.x,
372
+ y: action.y
373
+ }
374
+ };
375
+ case "SET_MODE":
376
+ return {
377
+ ...state,
378
+ mode: action.mode,
379
+ canvasSize: action.mode.defaultCanvasSize
380
+ };
381
+ case "CLEAR":
382
+ return {
383
+ ...state,
384
+ elements: [],
385
+ selectedElementId: null,
386
+ history: {
387
+ past: [...state.history.past, state.elements],
388
+ future: []
389
+ }
390
+ };
391
+ case "UNDO": {
392
+ if (state.history.past.length === 0) return state;
393
+ const previous = state.history.past[state.history.past.length - 1];
394
+ const newPast = state.history.past.slice(0, -1);
395
+ return {
396
+ ...state,
397
+ elements: previous,
398
+ history: {
399
+ past: newPast,
400
+ future: [state.elements, ...state.history.future]
401
+ }
402
+ };
403
+ }
404
+ case "REDO": {
405
+ if (state.history.future.length === 0) return state;
406
+ const next = state.history.future[0];
407
+ const newFuture = state.history.future.slice(1);
408
+ return {
409
+ ...state,
410
+ elements: next,
411
+ history: {
412
+ past: [...state.history.past, state.elements],
413
+ future: newFuture
414
+ }
415
+ };
416
+ }
417
+ default:
418
+ return state;
419
+ }
420
+ };
421
+ var useEditorState = (initialMode = null) => {
422
+ const [state, dispatch] = (0, import_react.useReducer)(editorReducer, createInitialState(initialMode));
423
+ const addElement = (0, import_react.useCallback)((element) => {
424
+ dispatch({ type: "ADD_ELEMENT", element });
425
+ }, []);
426
+ const updateElement = (0, import_react.useCallback)((id, updates) => {
427
+ dispatch({ type: "UPDATE_ELEMENT", id, updates });
428
+ }, []);
429
+ const removeElement = (0, import_react.useCallback)((id) => {
430
+ dispatch({ type: "REMOVE_ELEMENT", id });
431
+ }, []);
432
+ const selectElement = (0, import_react.useCallback)((id) => {
433
+ dispatch({ type: "SELECT_ELEMENT", id });
434
+ }, []);
435
+ const getSelectedElement = (0, import_react.useCallback)(() => {
436
+ if (!state.selectedElementId) return null;
437
+ return state.elements.find((el) => el.id === state.selectedElementId) || null;
438
+ }, [state.selectedElementId, state.elements]);
439
+ const getAllElements = (0, import_react.useCallback)(() => {
440
+ return state.elements;
441
+ }, [state.elements]);
442
+ const moveElement = (0, import_react.useCallback)(
443
+ (id, deltaX, deltaY) => {
444
+ const element = state.elements.find((el) => el.id === id);
445
+ if (!element) return;
446
+ updateElement(id, {
447
+ position: {
448
+ x: element.position.x + deltaX,
449
+ y: element.position.y + deltaY
450
+ }
451
+ });
452
+ },
453
+ [state.elements, updateElement]
454
+ );
455
+ const rotateElement = (0, import_react.useCallback)(
456
+ (id, angle) => {
457
+ updateElement(id, { rotation: angle });
458
+ },
459
+ [updateElement]
460
+ );
461
+ const resizeElement = (0, import_react.useCallback)(
462
+ (id, width, height) => {
463
+ updateElement(id, {
464
+ size: { width, height }
465
+ });
466
+ },
467
+ [updateElement]
468
+ );
469
+ const updateZIndex = (0, import_react.useCallback)(
470
+ (id, zIndex) => {
471
+ updateElement(id, { zIndex });
472
+ },
473
+ [updateElement]
474
+ );
475
+ const reorderElement = (0, import_react.useCallback)((id, newIndex) => {
476
+ dispatch({ type: "REORDER_ELEMENT", elementId: id, newIndex });
477
+ }, []);
478
+ const exportJSON = (0, import_react.useCallback)(() => {
479
+ return {
480
+ width: state.canvasSize.width,
481
+ height: state.canvasSize.height,
482
+ elements: state.elements,
483
+ metadata: {
484
+ version: "1.0.0",
485
+ mode: state.mode?.name,
486
+ created: (/* @__PURE__ */ new Date()).toISOString()
487
+ }
488
+ };
489
+ }, [state.canvasSize, state.elements, state.mode]);
490
+ const importJSON = (0, import_react.useCallback)((data) => {
491
+ dispatch({
492
+ type: "SET_CANVAS_SIZE",
493
+ width: data.width,
494
+ height: data.height
495
+ });
496
+ dispatch({ type: "SET_ELEMENTS", elements: data.elements });
497
+ }, []);
498
+ const clear = (0, import_react.useCallback)(() => {
499
+ dispatch({ type: "CLEAR" });
500
+ }, []);
501
+ const setCanvasSize = (0, import_react.useCallback)((width, height) => {
502
+ dispatch({ type: "SET_CANVAS_SIZE", width, height });
503
+ }, []);
504
+ const setMode = (0, import_react.useCallback)((mode) => {
505
+ dispatch({ type: "SET_MODE", mode });
506
+ }, []);
507
+ const undo = (0, import_react.useCallback)(() => {
508
+ dispatch({ type: "UNDO" });
509
+ }, []);
510
+ const redo = (0, import_react.useCallback)(() => {
511
+ dispatch({ type: "REDO" });
512
+ }, []);
513
+ const copyElement = (0, import_react.useCallback)(
514
+ (id = null) => {
515
+ const elementToCopy = id ? state.elements.find((el) => el.id === id) : getSelectedElement();
516
+ if (!elementToCopy) return null;
517
+ return {
518
+ ...elementToCopy,
519
+ props: { ...elementToCopy.props },
520
+ position: { ...elementToCopy.position },
521
+ size: { ...elementToCopy.size }
522
+ };
523
+ },
524
+ [state.elements, getSelectedElement]
525
+ );
526
+ const duplicateElement2 = (0, import_react.useCallback)(
527
+ (id = null, offset = { x: 20, y: 20 }) => {
528
+ const elementToDuplicate = id ? state.elements.find((el) => el.id === id) : getSelectedElement();
529
+ if (!elementToDuplicate) return;
530
+ const { duplicateElement: duplicateUtil } = (init_editorUtils(), __toCommonJS(editorUtils_exports));
531
+ const duplicated = duplicateUtil(elementToDuplicate, offset);
532
+ addElement(duplicated);
533
+ },
534
+ [state.elements, getSelectedElement, addElement]
535
+ );
536
+ const pasteElement = (0, import_react.useCallback)(
537
+ (copiedElement, offset = { x: 20, y: 20 }) => {
538
+ if (!copiedElement) return;
539
+ const { generateElementId: generateElementId2 } = (init_editorUtils(), __toCommonJS(editorUtils_exports));
540
+ const pasted = {
541
+ ...copiedElement,
542
+ id: generateElementId2(),
543
+ props: { ...copiedElement.props },
544
+ position: {
545
+ x: copiedElement.position.x + offset.x,
546
+ y: copiedElement.position.y + offset.y
547
+ },
548
+ size: { ...copiedElement.size },
549
+ zIndex: Math.max(...state.elements.map((el) => el.zIndex), 0) + 1
550
+ };
551
+ addElement(pasted);
552
+ },
553
+ [state.elements, addElement]
554
+ );
555
+ const clearHistory = (0, import_react.useCallback)(() => {
556
+ state.history.past = [];
557
+ state.history.future = [];
558
+ }, []);
559
+ const loadElements = (0, import_react.useCallback)((elements) => {
560
+ dispatch({ type: "LOAD_ELEMENTS", elements });
561
+ }, []);
562
+ const api = (0, import_react.useMemo)(
563
+ () => ({
564
+ addElement,
565
+ updateElement,
566
+ removeElement,
567
+ selectElement,
568
+ getSelectedElement,
569
+ getAllElements,
570
+ moveElement,
571
+ rotateElement,
572
+ resizeElement,
573
+ updateZIndex,
574
+ reorderElement,
575
+ exportJSON,
576
+ importJSON,
577
+ clear,
578
+ copyElement,
579
+ duplicateElement: duplicateElement2,
580
+ pasteElement,
581
+ clearHistory,
582
+ loadElements
583
+ }),
584
+ [
585
+ addElement,
586
+ updateElement,
587
+ removeElement,
588
+ selectElement,
589
+ getSelectedElement,
590
+ getAllElements,
591
+ moveElement,
592
+ rotateElement,
593
+ resizeElement,
594
+ updateZIndex,
595
+ reorderElement,
596
+ exportJSON,
597
+ importJSON,
598
+ clear,
599
+ copyElement,
600
+ duplicateElement2,
601
+ pasteElement,
602
+ clearHistory,
603
+ loadElements
604
+ ]
605
+ );
606
+ return {
607
+ state,
608
+ api,
609
+ // Additional helpers
610
+ setCanvasSize,
611
+ setMode,
612
+ undo,
613
+ redo,
614
+ canUndo: state.history.past.length > 0,
615
+ canRedo: state.history.future.length > 0
616
+ };
617
+ };
618
+
619
+ // src/core/ElementRegistry.ts
620
+ var import_react2 = require("react");
621
+ var ElementRegistry = class {
622
+ constructor() {
623
+ this.renderers = /* @__PURE__ */ new Map();
624
+ }
625
+ /**
626
+ * Register a new element renderer
627
+ */
628
+ register(renderer) {
629
+ if (this.renderers.has(renderer.type)) {
630
+ console.warn(
631
+ `Element renderer with type "${renderer.type}" is already registered. Skipping.`
632
+ );
633
+ return;
634
+ }
635
+ this.renderers.set(renderer.type, renderer);
636
+ }
637
+ /**
638
+ * Register multiple element renderers at once
639
+ */
640
+ registerMany(renderers) {
641
+ renderers.forEach((renderer) => this.register(renderer));
642
+ }
643
+ /**
644
+ * Get a renderer by type
645
+ */
646
+ get(type) {
647
+ return this.renderers.get(type);
648
+ }
649
+ /**
650
+ * Check if a renderer exists for a type
651
+ */
652
+ has(type) {
653
+ return this.renderers.has(type);
654
+ }
655
+ /**
656
+ * Get all registered renderers
657
+ */
658
+ getAll() {
659
+ return Array.from(this.renderers.values());
660
+ }
661
+ /**
662
+ * Get the internal renderers map
663
+ */
664
+ getMap() {
665
+ return this.renderers;
666
+ }
667
+ /**
668
+ * Get all registered types
669
+ */
670
+ getAllTypes() {
671
+ return Array.from(this.renderers.keys());
672
+ }
673
+ /**
674
+ * Unregister a renderer
675
+ */
676
+ unregister(type) {
677
+ return this.renderers.delete(type);
678
+ }
679
+ /**
680
+ * Clear all registered renderers
681
+ */
682
+ clear() {
683
+ this.renderers.clear();
684
+ }
685
+ /**
686
+ * Get the number of registered renderers
687
+ */
688
+ get size() {
689
+ return this.renderers.size;
690
+ }
691
+ };
692
+ var globalElementRegistry = new ElementRegistry();
693
+ var useElementRegistry = (initialRenderers) => {
694
+ return (0, import_react2.useMemo)(() => {
695
+ const registry = new ElementRegistry();
696
+ if (initialRenderers) {
697
+ registry.registerMany(initialRenderers);
698
+ }
699
+ return registry;
700
+ }, [initialRenderers]);
701
+ };
702
+
703
+ // src/elements/TextElement.tsx
704
+ var import_react3 = __toESM(require("react"));
705
+ var import_react_konva = require("react-konva");
706
+ var import_lucide_react = require("lucide-react");
707
+
708
+ // src/utils/snapping.ts
709
+ function getRotatedBounds(x, y, width, height, rotation) {
710
+ if (Math.abs(rotation) < 0.1 || Math.abs(rotation - 360) < 0.1) {
711
+ return {
712
+ left: x,
713
+ right: x + width,
714
+ top: y,
715
+ bottom: y + height,
716
+ centerX: x + width / 2,
717
+ centerY: y + height / 2
718
+ };
719
+ }
720
+ const rad = rotation * Math.PI / 180;
721
+ const cos = Math.cos(rad);
722
+ const sin = Math.sin(rad);
723
+ const corners = [
724
+ { x: 0, y: 0 },
725
+ // top-left (rotation origin)
726
+ { x: width, y: 0 },
727
+ // top-right
728
+ { x: width, y: height },
729
+ // bottom-right
730
+ { x: 0, y: height }
731
+ // bottom-left
732
+ ];
733
+ let minX = Infinity;
734
+ let maxX = -Infinity;
735
+ let minY = Infinity;
736
+ let maxY = -Infinity;
737
+ for (const corner of corners) {
738
+ const rotatedX = x + corner.x * cos - corner.y * sin;
739
+ const rotatedY = y + corner.x * sin + corner.y * cos;
740
+ minX = Math.min(minX, rotatedX);
741
+ maxX = Math.max(maxX, rotatedX);
742
+ minY = Math.min(minY, rotatedY);
743
+ maxY = Math.max(maxY, rotatedY);
744
+ }
745
+ const centerX = x + width / 2 * cos - height / 2 * sin;
746
+ const centerY = y + width / 2 * sin + height / 2 * cos;
747
+ return {
748
+ left: minX,
749
+ right: maxX,
750
+ top: minY,
751
+ bottom: maxY,
752
+ centerX,
753
+ centerY
754
+ };
755
+ }
756
+ function getSnappingPosition(draggedElement, x, y, allElements, options = {}) {
757
+ const { threshold = 5, snapToElements = true, snapToCanvas = true, canvasSize } = options;
758
+ let snappedX = x;
759
+ let snappedY = y;
760
+ const verticalGuides = [];
761
+ const horizontalGuides = [];
762
+ const draggedBounds = getRotatedBounds(
763
+ x,
764
+ y,
765
+ draggedElement.size.width,
766
+ draggedElement.size.height,
767
+ draggedElement.rotation
768
+ );
769
+ const draggedLeft = draggedBounds.left;
770
+ const draggedRight = draggedBounds.right;
771
+ const draggedTop = draggedBounds.top;
772
+ const draggedBottom = draggedBounds.bottom;
773
+ const draggedCenterX = draggedBounds.centerX;
774
+ const draggedCenterY = draggedBounds.centerY;
775
+ const isRotated = Math.abs(draggedElement.rotation) > 0.1 && Math.abs(draggedElement.rotation - 360) > 0.1;
776
+ let closestVerticalSnap = threshold;
777
+ let closestHorizontalSnap = threshold;
778
+ let verticalOffset = 0;
779
+ let horizontalOffset = 0;
780
+ if (snapToCanvas && canvasSize) {
781
+ if (Math.abs(draggedLeft) <= closestVerticalSnap) {
782
+ verticalOffset = -draggedLeft;
783
+ closestVerticalSnap = Math.abs(draggedLeft);
784
+ verticalGuides.push({ position: 0, orientation: "vertical", type: "canvas" });
785
+ }
786
+ if (Math.abs(draggedRight - canvasSize.width) <= closestVerticalSnap) {
787
+ verticalOffset = canvasSize.width - draggedRight;
788
+ closestVerticalSnap = Math.abs(draggedRight - canvasSize.width);
789
+ verticalGuides.push({
790
+ position: canvasSize.width,
791
+ orientation: "vertical",
792
+ type: "canvas"
793
+ });
794
+ }
795
+ if (Math.abs(draggedTop) <= closestHorizontalSnap) {
796
+ horizontalOffset = -draggedTop;
797
+ closestHorizontalSnap = Math.abs(draggedTop);
798
+ horizontalGuides.push({ position: 0, orientation: "horizontal", type: "canvas" });
799
+ }
800
+ if (Math.abs(draggedBottom - canvasSize.height) <= closestHorizontalSnap) {
801
+ horizontalOffset = canvasSize.height - draggedBottom;
802
+ closestHorizontalSnap = Math.abs(draggedBottom - canvasSize.height);
803
+ horizontalGuides.push({
804
+ position: canvasSize.height,
805
+ orientation: "horizontal",
806
+ type: "canvas"
807
+ });
808
+ }
809
+ const canvasCenterX = canvasSize.width / 2;
810
+ if (Math.abs(draggedCenterX - canvasCenterX) <= closestVerticalSnap) {
811
+ verticalOffset = canvasCenterX - draggedCenterX;
812
+ closestVerticalSnap = Math.abs(draggedCenterX - canvasCenterX);
813
+ verticalGuides.length = 0;
814
+ verticalGuides.push({
815
+ position: canvasCenterX,
816
+ orientation: "vertical",
817
+ type: "center"
818
+ });
819
+ }
820
+ const canvasCenterY = canvasSize.height / 2;
821
+ if (Math.abs(draggedCenterY - canvasCenterY) <= closestHorizontalSnap) {
822
+ horizontalOffset = canvasCenterY - draggedCenterY;
823
+ closestHorizontalSnap = Math.abs(draggedCenterY - canvasCenterY);
824
+ horizontalGuides.length = 0;
825
+ horizontalGuides.push({
826
+ position: canvasCenterY,
827
+ orientation: "horizontal",
828
+ type: "center"
829
+ });
830
+ }
831
+ }
832
+ if (snapToElements) {
833
+ for (const element of allElements) {
834
+ if (element.id === draggedElement.id) continue;
835
+ if (element.visible === false) continue;
836
+ const elementBounds = getRotatedBounds(
837
+ element.position.x,
838
+ element.position.y,
839
+ element.size.width,
840
+ element.size.height,
841
+ element.rotation
842
+ );
843
+ const elementLeft = elementBounds.left;
844
+ const elementRight = elementBounds.right;
845
+ const elementTop = elementBounds.top;
846
+ const elementBottom = elementBounds.bottom;
847
+ const elementCenterX = elementBounds.centerX;
848
+ const elementCenterY = elementBounds.centerY;
849
+ const isElementRotated = Math.abs(element.rotation) > 0.1 && Math.abs(element.rotation - 360) > 0.1;
850
+ const shouldSkipEdges = false;
851
+ if (!shouldSkipEdges) {
852
+ if (Math.abs(draggedLeft - elementLeft) <= closestVerticalSnap) {
853
+ verticalOffset = elementLeft - draggedLeft;
854
+ closestVerticalSnap = Math.abs(draggedLeft - elementLeft);
855
+ verticalGuides.length = 0;
856
+ verticalGuides.push({ position: elementLeft, orientation: "vertical", type: "edge" });
857
+ }
858
+ if (Math.abs(draggedRight - elementRight) <= closestVerticalSnap) {
859
+ verticalOffset = elementRight - draggedRight;
860
+ closestVerticalSnap = Math.abs(draggedRight - elementRight);
861
+ verticalGuides.length = 0;
862
+ verticalGuides.push({
863
+ position: elementRight,
864
+ orientation: "vertical",
865
+ type: "edge"
866
+ });
867
+ }
868
+ if (Math.abs(draggedLeft - elementRight) <= closestVerticalSnap) {
869
+ verticalOffset = elementRight - draggedLeft;
870
+ closestVerticalSnap = Math.abs(draggedLeft - elementRight);
871
+ verticalGuides.length = 0;
872
+ verticalGuides.push({
873
+ position: elementRight,
874
+ orientation: "vertical",
875
+ type: "edge"
876
+ });
877
+ }
878
+ if (Math.abs(draggedRight - elementLeft) <= closestVerticalSnap) {
879
+ verticalOffset = elementLeft - draggedRight;
880
+ closestVerticalSnap = Math.abs(draggedRight - elementLeft);
881
+ verticalGuides.length = 0;
882
+ verticalGuides.push({ position: elementLeft, orientation: "vertical", type: "edge" });
883
+ }
884
+ }
885
+ if (Math.abs(draggedCenterX - elementCenterX) <= closestVerticalSnap) {
886
+ verticalOffset = elementCenterX - draggedCenterX;
887
+ closestVerticalSnap = Math.abs(draggedCenterX - elementCenterX);
888
+ verticalGuides.length = 0;
889
+ verticalGuides.push({
890
+ position: elementCenterX,
891
+ orientation: "vertical",
892
+ type: "center"
893
+ });
894
+ }
895
+ if (!shouldSkipEdges) {
896
+ if (Math.abs(draggedTop - elementTop) <= closestHorizontalSnap) {
897
+ horizontalOffset = elementTop - draggedTop;
898
+ closestHorizontalSnap = Math.abs(draggedTop - elementTop);
899
+ horizontalGuides.length = 0;
900
+ horizontalGuides.push({
901
+ position: elementTop,
902
+ orientation: "horizontal",
903
+ type: "edge"
904
+ });
905
+ }
906
+ if (Math.abs(draggedBottom - elementBottom) <= closestHorizontalSnap) {
907
+ horizontalOffset = elementBottom - draggedBottom;
908
+ closestHorizontalSnap = Math.abs(draggedBottom - elementBottom);
909
+ horizontalGuides.length = 0;
910
+ horizontalGuides.push({
911
+ position: elementBottom,
912
+ orientation: "horizontal",
913
+ type: "edge"
914
+ });
915
+ }
916
+ if (Math.abs(draggedTop - elementBottom) <= closestHorizontalSnap) {
917
+ horizontalOffset = elementBottom - draggedTop;
918
+ closestHorizontalSnap = Math.abs(draggedTop - elementBottom);
919
+ horizontalGuides.length = 0;
920
+ horizontalGuides.push({
921
+ position: elementBottom,
922
+ orientation: "horizontal",
923
+ type: "edge"
924
+ });
925
+ }
926
+ if (Math.abs(draggedBottom - elementTop) <= closestHorizontalSnap) {
927
+ horizontalOffset = elementTop - draggedBottom;
928
+ closestHorizontalSnap = Math.abs(draggedBottom - elementTop);
929
+ horizontalGuides.length = 0;
930
+ horizontalGuides.push({
931
+ position: elementTop,
932
+ orientation: "horizontal",
933
+ type: "edge"
934
+ });
935
+ }
936
+ }
937
+ if (Math.abs(draggedCenterY - elementCenterY) <= closestHorizontalSnap) {
938
+ horizontalOffset = elementCenterY - draggedCenterY;
939
+ closestHorizontalSnap = Math.abs(draggedCenterY - elementCenterY);
940
+ horizontalGuides.length = 0;
941
+ horizontalGuides.push({
942
+ position: elementCenterY,
943
+ orientation: "horizontal",
944
+ type: "center"
945
+ });
946
+ }
947
+ }
948
+ }
949
+ snappedX = x + verticalOffset;
950
+ snappedY = y + horizontalOffset;
951
+ return {
952
+ x: snappedX,
953
+ y: snappedY,
954
+ verticalGuides,
955
+ horizontalGuides
956
+ };
957
+ }
958
+
959
+ // src/elements/TextElement.tsx
960
+ var import_jsx_runtime = require("react/jsx-runtime");
961
+ var TextElementRenderer = ({
962
+ element,
963
+ isSelected,
964
+ onSelect,
965
+ onTransform,
966
+ allElements = [],
967
+ canvasSize,
968
+ onSnapGuides,
969
+ onClearSnapGuides,
970
+ elementId
971
+ }) => {
972
+ const shapeRef = import_react3.default.useRef(null);
973
+ const isVisible = element.visible !== false;
974
+ const isLocked = element.locked === true;
975
+ if (!isVisible) {
976
+ return null;
977
+ }
978
+ const handleClick = (e) => {
979
+ if (isLocked) return;
980
+ e.evt.button !== 0 ? void 0 : onSelect();
981
+ };
982
+ const handleDragMove = (e) => {
983
+ if (!canvasSize || !onSnapGuides || !isSelected || e.evt.button !== 0) return;
984
+ const node = e.target;
985
+ const snapResult = getSnappingPosition(element, node.x(), node.y(), allElements, {
986
+ threshold: 5,
987
+ snapToElements: true,
988
+ snapToCanvas: true,
989
+ canvasSize
990
+ });
991
+ node.x(snapResult.x);
992
+ node.y(snapResult.y);
993
+ onSnapGuides({
994
+ vertical: snapResult.verticalGuides,
995
+ horizontal: snapResult.horizontalGuides
996
+ });
997
+ };
998
+ const handleDragEnd = (e) => {
999
+ if (onClearSnapGuides) {
1000
+ onClearSnapGuides();
1001
+ }
1002
+ onTransform({
1003
+ position: {
1004
+ x: e.target.x(),
1005
+ y: e.target.y()
1006
+ }
1007
+ });
1008
+ };
1009
+ const handleTransform = () => {
1010
+ const node = shapeRef.current;
1011
+ if (!node) return;
1012
+ const scaleX = node.scaleX();
1013
+ const scaleY = node.scaleY();
1014
+ const newWidth = Math.max(20, node.width() * scaleX);
1015
+ const newHeight = Math.max(20, node.height() * scaleY);
1016
+ node.scaleX(1);
1017
+ node.scaleY(1);
1018
+ node.width(newWidth);
1019
+ node.height(newHeight);
1020
+ };
1021
+ const getTextDecoration = () => {
1022
+ const decorations = [];
1023
+ if (element.props.underline) decorations.push("underline");
1024
+ if (element.props.strikethrough) decorations.push("line-through");
1025
+ if (element.props.overline) decorations.push("overline");
1026
+ return decorations.join(" ") || "";
1027
+ };
1028
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1029
+ import_react_konva.Text,
1030
+ {
1031
+ ref: shapeRef,
1032
+ id: elementId || element.id,
1033
+ x: element.position.x,
1034
+ y: element.position.y,
1035
+ width: element.size.width,
1036
+ height: element.size.height,
1037
+ text: element.props.content,
1038
+ fontSize: element.props.fontSize,
1039
+ fontFamily: element.props.fontFamily || "Arial",
1040
+ opacity: element.opacity,
1041
+ fill: element.props.color,
1042
+ align: element.props.align || "left",
1043
+ fontStyle: `${element.props.bold ? "bold" : ""} ${element.props.italic ? "italic" : ""}`.trim() || "normal",
1044
+ textDecoration: getTextDecoration(),
1045
+ rotation: element.rotation,
1046
+ draggable: !isLocked && isSelected,
1047
+ listening: !isLocked,
1048
+ onClick: handleClick,
1049
+ onTap: isLocked ? void 0 : onSelect,
1050
+ onDragMove: handleDragMove,
1051
+ onDragEnd: handleDragEnd,
1052
+ onTransform: handleTransform,
1053
+ ellipsis: element.props.textOverflow === "ellipsis",
1054
+ verticalAlign: element.props.verticalAlign || "top",
1055
+ stroke: element.props.strokeColor || "#000000",
1056
+ strokeWidth: element.props.strokeWidth || 0,
1057
+ strokeEnabled: true,
1058
+ fillAfterStrokeEnabled: true
1059
+ }
1060
+ ) });
1061
+ };
1062
+ var textElementRenderer = {
1063
+ type: "text",
1064
+ displayName: "Text",
1065
+ render: (element) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1066
+ "Text: ",
1067
+ element.props.content
1068
+ ] }),
1069
+ // Placeholder for non-Konva contexts
1070
+ renderComponent: TextElementRenderer,
1071
+ // Konva rendering component
1072
+ icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Type, { className: "w-4 h-4" }),
1073
+ defaultProps: {
1074
+ content: "Text",
1075
+ fontSize: 16,
1076
+ fontFamily: "Arial",
1077
+ color: "#000000",
1078
+ strokeColor: "#000000",
1079
+ strokeWidth: 0,
1080
+ align: "left",
1081
+ verticalAlign: "top",
1082
+ bold: false,
1083
+ italic: false,
1084
+ underline: false,
1085
+ overline: false,
1086
+ strikethrough: false,
1087
+ wordWrap: "break-word"
1088
+ },
1089
+ defaultSize: {
1090
+ width: 200,
1091
+ height: 50
1092
+ },
1093
+ inspectorSchema: [
1094
+ {
1095
+ name: "content",
1096
+ type: "string",
1097
+ label: "Text Content",
1098
+ defaultValue: "Text"
1099
+ },
1100
+ {
1101
+ name: "fontSize",
1102
+ type: "number",
1103
+ label: "Font Size",
1104
+ min: 0,
1105
+ max: 1024,
1106
+ step: 1,
1107
+ defaultValue: 16
1108
+ },
1109
+ {
1110
+ name: "fontFamily",
1111
+ type: "select",
1112
+ label: "Font Family",
1113
+ options: [
1114
+ { value: "Arial", label: "Arial" },
1115
+ { value: "Times New Roman", label: "Times New Roman" },
1116
+ { value: "Courier New", label: "Courier New" },
1117
+ { value: "Georgia", label: "Georgia" },
1118
+ { value: "Verdana", label: "Verdana" },
1119
+ { value: "Outfit", label: "Outfit" }
1120
+ ],
1121
+ defaultValue: "Outfit"
1122
+ },
1123
+ {
1124
+ name: "color",
1125
+ type: "color",
1126
+ label: "Text Color",
1127
+ defaultValue: "#000000"
1128
+ },
1129
+ {
1130
+ name: "strokeColor",
1131
+ type: "color",
1132
+ label: "Stroke Color",
1133
+ defaultValue: "#000000"
1134
+ },
1135
+ {
1136
+ name: "strokeWidth",
1137
+ type: "number",
1138
+ label: "Stroke Width",
1139
+ min: 0,
1140
+ max: 50,
1141
+ step: 1,
1142
+ defaultValue: 0
1143
+ },
1144
+ {
1145
+ name: "align",
1146
+ type: "select",
1147
+ label: "Horizontal Alignment",
1148
+ options: [
1149
+ { value: "left", label: "Left" },
1150
+ { value: "center", label: "Center" },
1151
+ { value: "right", label: "Right" }
1152
+ ],
1153
+ defaultValue: "left"
1154
+ },
1155
+ {
1156
+ name: "verticalAlign",
1157
+ type: "select",
1158
+ label: "Vertical Alignment",
1159
+ options: [
1160
+ { value: "top", label: "Top" },
1161
+ { value: "middle", label: "Middle" },
1162
+ { value: "bottom", label: "Bottom" }
1163
+ ],
1164
+ defaultValue: "top"
1165
+ },
1166
+ {
1167
+ name: "bold",
1168
+ type: "boolean",
1169
+ label: "Bold",
1170
+ defaultValue: false
1171
+ },
1172
+ {
1173
+ name: "italic",
1174
+ type: "boolean",
1175
+ label: "Italic",
1176
+ defaultValue: false
1177
+ },
1178
+ {
1179
+ name: "underline",
1180
+ type: "boolean",
1181
+ label: "Underline",
1182
+ defaultValue: false
1183
+ },
1184
+ {
1185
+ name: "strikethrough",
1186
+ type: "boolean",
1187
+ label: "Strikethrough",
1188
+ defaultValue: false
1189
+ }
1190
+ ]
1191
+ };
1192
+
1193
+ // src/elements/ImageElement.tsx
1194
+ var import_react4 = __toESM(require("react"));
1195
+ var import_react_konva2 = require("react-konva");
1196
+ var import_use_image = __toESM(require("use-image"));
1197
+ var import_lucide_react2 = require("lucide-react");
1198
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1199
+ var ImageElementRenderer = ({
1200
+ element,
1201
+ isSelected,
1202
+ onSelect,
1203
+ onTransform,
1204
+ allElements = [],
1205
+ canvasSize,
1206
+ onSnapGuides,
1207
+ onClearSnapGuides,
1208
+ imageUrls,
1209
+ elementId,
1210
+ onNodeUpdate
1211
+ }) => {
1212
+ const imageSrc = import_react4.default.useMemo(() => {
1213
+ const src = element.props.src;
1214
+ if (src && (src.startsWith("http") || src.startsWith("blob:") || src.startsWith("data:"))) {
1215
+ return src;
1216
+ }
1217
+ if (src && imageUrls) {
1218
+ const resolvedUrl = imageUrls.get(src);
1219
+ if (resolvedUrl) {
1220
+ return resolvedUrl;
1221
+ }
1222
+ }
1223
+ return src;
1224
+ }, [element.props.src, imageUrls]);
1225
+ const [image] = (0, import_use_image.default)(imageSrc);
1226
+ const shapeRef = import_react4.default.useRef(null);
1227
+ const transformerRef = import_react4.default.useRef(null);
1228
+ const isVisible = element.visible !== false;
1229
+ const isLocked = element.locked === true;
1230
+ import_react4.default.useEffect(() => {
1231
+ if (isSelected && transformerRef.current && shapeRef.current) {
1232
+ transformerRef.current.nodes([shapeRef.current]);
1233
+ transformerRef.current.getLayer().batchDraw();
1234
+ }
1235
+ }, [isSelected]);
1236
+ import_react4.default.useEffect(() => {
1237
+ if (shapeRef.current) {
1238
+ const layer = shapeRef.current.getLayer();
1239
+ if (layer) {
1240
+ layer.batchDraw();
1241
+ }
1242
+ if (onNodeUpdate) {
1243
+ onNodeUpdate();
1244
+ }
1245
+ }
1246
+ }, [image, onNodeUpdate]);
1247
+ if (!isVisible) {
1248
+ return null;
1249
+ }
1250
+ const handleClick = (e) => {
1251
+ if (isLocked) return;
1252
+ e.evt.button !== 0 ? void 0 : onSelect();
1253
+ };
1254
+ const handleDragMove = (e) => {
1255
+ if (!canvasSize || !onSnapGuides || e.evt.button !== 0) return;
1256
+ const node = e.target;
1257
+ const snapResult = getSnappingPosition(element, node.x(), node.y(), allElements, {
1258
+ threshold: 5,
1259
+ snapToElements: true,
1260
+ snapToCanvas: true,
1261
+ canvasSize
1262
+ });
1263
+ node.x(snapResult.x);
1264
+ node.y(snapResult.y);
1265
+ onSnapGuides({
1266
+ vertical: snapResult.verticalGuides,
1267
+ horizontal: snapResult.horizontalGuides
1268
+ });
1269
+ };
1270
+ const handleDragEnd = (e) => {
1271
+ if (onClearSnapGuides) {
1272
+ onClearSnapGuides();
1273
+ }
1274
+ onTransform({
1275
+ position: {
1276
+ x: e.target.x(),
1277
+ y: e.target.y()
1278
+ }
1279
+ });
1280
+ };
1281
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: image ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1282
+ import_react_konva2.Image,
1283
+ {
1284
+ ref: shapeRef,
1285
+ id: elementId || element.id,
1286
+ x: element.position.x,
1287
+ y: element.position.y,
1288
+ width: element.size.width,
1289
+ height: element.size.height,
1290
+ image,
1291
+ opacity: element.opacity,
1292
+ rotation: element.rotation,
1293
+ draggable: !isLocked && isSelected,
1294
+ listening: !isLocked,
1295
+ onClick: handleClick,
1296
+ onTap: isLocked ? void 0 : onSelect,
1297
+ onDragMove: handleDragMove,
1298
+ onDragEnd: handleDragEnd
1299
+ }
1300
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1301
+ import_react_konva2.Group,
1302
+ {
1303
+ ref: shapeRef,
1304
+ id: elementId || element.id,
1305
+ x: element.position.x,
1306
+ y: element.position.y,
1307
+ width: element.size.width,
1308
+ height: element.size.height,
1309
+ rotation: element.rotation,
1310
+ draggable: !isLocked && isSelected,
1311
+ listening: !isLocked,
1312
+ onClick: isLocked ? void 0 : handleClick,
1313
+ onTap: isLocked ? void 0 : onSelect,
1314
+ onDragMove: handleDragMove,
1315
+ onDragEnd: handleDragEnd,
1316
+ children: [
1317
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1318
+ import_react_konva2.Rect,
1319
+ {
1320
+ width: element.size.width,
1321
+ height: element.size.height,
1322
+ fill: "#f0f0f0",
1323
+ stroke: "#999999",
1324
+ strokeWidth: 2,
1325
+ dash: [10, 5],
1326
+ opacity: element.opacity
1327
+ }
1328
+ ),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1330
+ import_react_konva2.Text,
1331
+ {
1332
+ width: element.size.width,
1333
+ height: element.size.height,
1334
+ text: "No Image",
1335
+ fontSize: 16,
1336
+ fill: "#666666",
1337
+ align: "center",
1338
+ verticalAlign: "middle"
1339
+ }
1340
+ )
1341
+ ]
1342
+ }
1343
+ ) });
1344
+ };
1345
+ var imageElementRenderer = {
1346
+ type: "image",
1347
+ displayName: "Image",
1348
+ render: (element) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
1349
+ "Image: ",
1350
+ element.props.src
1351
+ ] }),
1352
+ // Placeholder for non-Konva contexts
1353
+ renderComponent: ImageElementRenderer,
1354
+ // Konva rendering component
1355
+ icon: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.Image, { className: "w-4 h-4" }),
1356
+ defaultProps: {
1357
+ src: "",
1358
+ fit: "fill"
1359
+ },
1360
+ defaultSize: {
1361
+ width: 200,
1362
+ height: 200
1363
+ },
1364
+ inspectorSchema: [
1365
+ {
1366
+ name: "src",
1367
+ type: "image",
1368
+ label: "Image Source",
1369
+ description: "URL or path to the image",
1370
+ defaultValue: ""
1371
+ },
1372
+ {
1373
+ name: "fit",
1374
+ type: "select",
1375
+ label: "Fit Mode",
1376
+ options: [{ value: "fill", label: "Fill" }],
1377
+ defaultValue: "contain"
1378
+ }
1379
+ ]
1380
+ };
1381
+
1382
+ // src/elements/index.ts
1383
+ var defaultElements = [textElementRenderer, imageElementRenderer];
1384
+
1385
+ // src/core/VisualEditor.tsx
1386
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1387
+ var VisualEditor = ({
1388
+ mode,
1389
+ initialData,
1390
+ width: propWidth,
1391
+ height: propHeight,
1392
+ readonly = false,
1393
+ onChange,
1394
+ onSelectionChange,
1395
+ onExport,
1396
+ customElements = [],
1397
+ showToolbar = true,
1398
+ showInspector = true,
1399
+ className = "",
1400
+ style = {}
1401
+ }) => {
1402
+ const containerRef = (0, import_react5.useRef)(null);
1403
+ const stageRef = (0, import_react5.useRef)(null);
1404
+ const {
1405
+ state,
1406
+ api,
1407
+ setCanvasSize,
1408
+ setMode: setEditorMode,
1409
+ undo,
1410
+ redo,
1411
+ canUndo,
1412
+ canRedo
1413
+ } = useEditorState(mode || null);
1414
+ const registry = useElementRegistry([
1415
+ ...defaultElements,
1416
+ ...mode?.registeredElements || [],
1417
+ ...customElements
1418
+ ]);
1419
+ const canvasWidth = propWidth || state.canvasSize.width || mode?.defaultCanvasSize.width || 800;
1420
+ const canvasHeight = propHeight || state.canvasSize.height || mode?.defaultCanvasSize.height || 600;
1421
+ (0, import_react5.useEffect)(() => {
1422
+ if (propWidth && propHeight) {
1423
+ setCanvasSize(propWidth, propHeight);
1424
+ }
1425
+ }, [propWidth, propHeight, setCanvasSize]);
1426
+ (0, import_react5.useEffect)(() => {
1427
+ if (initialData) {
1428
+ api.importJSON(initialData);
1429
+ }
1430
+ }, [initialData, api]);
1431
+ (0, import_react5.useEffect)(() => {
1432
+ if (mode) {
1433
+ setEditorMode(mode);
1434
+ if (mode.registeredElements) {
1435
+ mode.registeredElements.forEach((el) => registry.register(el));
1436
+ }
1437
+ if (mode.onModeActivate) {
1438
+ mode.onModeActivate(api);
1439
+ }
1440
+ }
1441
+ }, [mode, setEditorMode, registry, api]);
1442
+ (0, import_react5.useEffect)(() => {
1443
+ if (onChange) {
1444
+ const data = api.exportJSON();
1445
+ onChange(data);
1446
+ }
1447
+ }, [state.elements, onChange, api]);
1448
+ (0, import_react5.useEffect)(() => {
1449
+ if (onSelectionChange) {
1450
+ const selected = api.getSelectedElement();
1451
+ onSelectionChange(selected);
1452
+ }
1453
+ }, [state.selectedElementId, onSelectionChange, api]);
1454
+ (0, import_react5.useEffect)(() => {
1455
+ const handleKeyDown = (e) => {
1456
+ if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
1457
+ e.preventDefault();
1458
+ undo();
1459
+ }
1460
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "z" || (e.ctrlKey || e.metaKey) && e.key === "y") {
1461
+ e.preventDefault();
1462
+ redo();
1463
+ }
1464
+ if ((e.key === "Delete" || e.key === "Backspace") && !readonly) {
1465
+ e.preventDefault();
1466
+ const selected = api.getSelectedElement();
1467
+ if (selected) {
1468
+ api.removeElement(selected.id);
1469
+ }
1470
+ }
1471
+ if (e.key === "Escape") {
1472
+ e.preventDefault();
1473
+ api.selectElement(null);
1474
+ }
1475
+ };
1476
+ window.addEventListener("keydown", handleKeyDown);
1477
+ return () => window.removeEventListener("keydown", handleKeyDown);
1478
+ }, [undo, redo, readonly, api]);
1479
+ const renderElement = (0, import_react5.useCallback)(
1480
+ (element) => {
1481
+ const renderer = registry.get(element.type);
1482
+ if (!renderer) {
1483
+ console.warn(`No renderer found for element type: ${element.type}`);
1484
+ return null;
1485
+ }
1486
+ const RendererComponent = renderer.renderComponent;
1487
+ if (!RendererComponent) {
1488
+ console.warn(`No renderComponent found for element type: ${element.type}`);
1489
+ return null;
1490
+ }
1491
+ const isSelected = state.selectedElementId === element.id;
1492
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1493
+ RendererComponent,
1494
+ {
1495
+ element,
1496
+ isSelected,
1497
+ onSelect: () => !readonly && api.selectElement(element.id),
1498
+ onTransform: (updates) => !readonly && api.updateElement(element.id, updates)
1499
+ },
1500
+ element.id
1501
+ );
1502
+ },
1503
+ [registry, state.selectedElementId, readonly, api]
1504
+ );
1505
+ const handleStageClick = (0, import_react5.useCallback)(
1506
+ (e) => {
1507
+ if (e.target === e.target.getStage()) {
1508
+ api.selectElement(null);
1509
+ }
1510
+ },
1511
+ [api]
1512
+ );
1513
+ const sortedElements = [...state.elements].sort((a, b) => a.zIndex - b.zIndex);
1514
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1515
+ "div",
1516
+ {
1517
+ ref: containerRef,
1518
+ className: `visual-editor ${className}`,
1519
+ style: {
1520
+ width: "100%",
1521
+ height: "100%",
1522
+ display: "flex",
1523
+ flexDirection: "column",
1524
+ overflow: "hidden",
1525
+ backgroundColor: mode?.backgroundColor || "#f0f0f0",
1526
+ ...style
1527
+ },
1528
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1529
+ "div",
1530
+ {
1531
+ style: {
1532
+ flex: 1,
1533
+ display: "flex",
1534
+ justifyContent: "center",
1535
+ alignItems: "center",
1536
+ overflow: "auto"
1537
+ },
1538
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1539
+ import_react_konva3.Stage,
1540
+ {
1541
+ ref: stageRef,
1542
+ width: canvasWidth,
1543
+ height: canvasHeight,
1544
+ onClick: handleStageClick,
1545
+ onTap: handleStageClick,
1546
+ style: {
1547
+ backgroundColor: "#ffffff",
1548
+ boxShadow: "0 2px 8px rgba(0,0,0,0.1)"
1549
+ },
1550
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_konva3.Layer, { children: sortedElements.map(renderElement) })
1551
+ }
1552
+ )
1553
+ }
1554
+ )
1555
+ }
1556
+ );
1557
+ };
1558
+
1559
+ // src/components/Inspector.tsx
1560
+ var import_react6 = __toESM(require("react"));
1561
+
1562
+ // src/ui/input.tsx
1563
+ var React4 = __toESM(require("react"));
1564
+
1565
+ // src/lib/utils.ts
1566
+ var import_clsx = require("clsx");
1567
+ var import_tailwind_merge = require("tailwind-merge");
1568
+ function cn(...inputs) {
1569
+ return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
1570
+ }
1571
+
1572
+ // src/ui/input.tsx
1573
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1574
+ var Input = React4.forwardRef(
1575
+ ({ className, type, ...props }, ref) => {
1576
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1577
+ "input",
1578
+ {
1579
+ type,
1580
+ className: cn(
1581
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
1582
+ className
1583
+ ),
1584
+ ref,
1585
+ ...props
1586
+ }
1587
+ );
1588
+ }
1589
+ );
1590
+ Input.displayName = "Input";
1591
+
1592
+ // src/ui/label.tsx
1593
+ var React5 = __toESM(require("react"));
1594
+ var LabelPrimitive = __toESM(require("@radix-ui/react-label"));
1595
+ var import_class_variance_authority = require("class-variance-authority");
1596
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1597
+ var labelVariants = (0, import_class_variance_authority.cva)(
1598
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
1599
+ );
1600
+ var Label = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1601
+ LabelPrimitive.Root,
1602
+ {
1603
+ ref,
1604
+ className: cn(labelVariants(), className),
1605
+ ...props
1606
+ }
1607
+ ));
1608
+ Label.displayName = LabelPrimitive.Root.displayName;
1609
+
1610
+ // src/ui/slider.tsx
1611
+ var React6 = __toESM(require("react"));
1612
+ var SliderPrimitive = __toESM(require("@radix-ui/react-slider"));
1613
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1614
+ var Slider = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1615
+ SliderPrimitive.Root,
1616
+ {
1617
+ ref,
1618
+ className: cn(
1619
+ "relative flex w-full touch-none select-none items-center",
1620
+ className
1621
+ ),
1622
+ ...props,
1623
+ children: [
1624
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SliderPrimitive.Track, { className: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SliderPrimitive.Range, { className: "absolute h-full bg-primary" }) }),
1625
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SliderPrimitive.Thumb, { className: "block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" })
1626
+ ]
1627
+ }
1628
+ ));
1629
+ Slider.displayName = SliderPrimitive.Root.displayName;
1630
+
1631
+ // src/ui/separator.tsx
1632
+ var React7 = __toESM(require("react"));
1633
+ var SeparatorPrimitive = __toESM(require("@radix-ui/react-separator"));
1634
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1635
+ var Separator = React7.forwardRef(
1636
+ ({ className, orientation = "horizontal", decorative = true, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1637
+ SeparatorPrimitive.Root,
1638
+ {
1639
+ ref,
1640
+ decorative,
1641
+ orientation,
1642
+ className: cn(
1643
+ "shrink-0 bg-border",
1644
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
1645
+ className
1646
+ ),
1647
+ ...props
1648
+ }
1649
+ )
1650
+ );
1651
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
1652
+
1653
+ // src/ui/select.tsx
1654
+ var React8 = __toESM(require("react"));
1655
+ var SelectPrimitive = __toESM(require("@radix-ui/react-select"));
1656
+ var import_lucide_react3 = require("lucide-react");
1657
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1658
+ var Select = SelectPrimitive.Root;
1659
+ var SelectValue = SelectPrimitive.Value;
1660
+ var SelectTrigger = React8.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1661
+ SelectPrimitive.Trigger,
1662
+ {
1663
+ ref,
1664
+ className: cn(
1665
+ "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
1666
+ className
1667
+ ),
1668
+ ...props,
1669
+ children: [
1670
+ children,
1671
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.ChevronDown, { className: "h-4 w-4 opacity-50" }) })
1672
+ ]
1673
+ }
1674
+ ));
1675
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
1676
+ var SelectScrollUpButton = React8.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1677
+ SelectPrimitive.ScrollUpButton,
1678
+ {
1679
+ ref,
1680
+ className: cn(
1681
+ "flex cursor-default items-center justify-center py-1",
1682
+ className
1683
+ ),
1684
+ ...props,
1685
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.ChevronUp, { className: "h-4 w-4" })
1686
+ }
1687
+ ));
1688
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
1689
+ var SelectScrollDownButton = React8.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1690
+ SelectPrimitive.ScrollDownButton,
1691
+ {
1692
+ ref,
1693
+ className: cn(
1694
+ "flex cursor-default items-center justify-center py-1",
1695
+ className
1696
+ ),
1697
+ ...props,
1698
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.ChevronDown, { className: "h-4 w-4" })
1699
+ }
1700
+ ));
1701
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
1702
+ var SelectContent = React8.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectPrimitive.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1703
+ SelectPrimitive.Content,
1704
+ {
1705
+ ref,
1706
+ className: cn(
1707
+ "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
1708
+ position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
1709
+ className
1710
+ ),
1711
+ position,
1712
+ ...props,
1713
+ children: [
1714
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectScrollUpButton, {}),
1715
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1716
+ SelectPrimitive.Viewport,
1717
+ {
1718
+ className: cn(
1719
+ "p-1",
1720
+ position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
1721
+ ),
1722
+ children
1723
+ }
1724
+ ),
1725
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectScrollDownButton, {})
1726
+ ]
1727
+ }
1728
+ ) }));
1729
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
1730
+ var SelectLabel = React8.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1731
+ SelectPrimitive.Label,
1732
+ {
1733
+ ref,
1734
+ className: cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className),
1735
+ ...props
1736
+ }
1737
+ ));
1738
+ SelectLabel.displayName = SelectPrimitive.Label.displayName;
1739
+ var SelectItem = React8.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1740
+ SelectPrimitive.Item,
1741
+ {
1742
+ ref,
1743
+ className: cn(
1744
+ "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
1745
+ className
1746
+ ),
1747
+ ...props,
1748
+ children: [
1749
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react3.Check, { className: "h-4 w-4" }) }) }),
1750
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectPrimitive.ItemText, { children })
1751
+ ]
1752
+ }
1753
+ ));
1754
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
1755
+ var SelectSeparator = React8.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1756
+ SelectPrimitive.Separator,
1757
+ {
1758
+ ref,
1759
+ className: cn("-mx-1 my-1 h-px bg-muted", className),
1760
+ ...props
1761
+ }
1762
+ ));
1763
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
1764
+
1765
+ // src/ui/textarea.tsx
1766
+ var React9 = __toESM(require("react"));
1767
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1768
+ var Textarea = React9.forwardRef(({ className, ...props }, ref) => {
1769
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1770
+ "textarea",
1771
+ {
1772
+ className: cn(
1773
+ "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
1774
+ className
1775
+ ),
1776
+ ref,
1777
+ ...props
1778
+ }
1779
+ );
1780
+ });
1781
+ Textarea.displayName = "Textarea";
1782
+
1783
+ // src/ui/checkbox.tsx
1784
+ var React10 = __toESM(require("react"));
1785
+ var CheckboxPrimitive = __toESM(require("@radix-ui/react-checkbox"));
1786
+ var import_lucide_react4 = require("lucide-react");
1787
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1788
+ var Checkbox = React10.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1789
+ CheckboxPrimitive.Root,
1790
+ {
1791
+ ref,
1792
+ className: cn(
1793
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
1794
+ className
1795
+ ),
1796
+ ...props,
1797
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1798
+ CheckboxPrimitive.Indicator,
1799
+ {
1800
+ className: cn("flex items-center justify-center text-current"),
1801
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react4.Check, { className: "h-4 w-4" })
1802
+ }
1803
+ )
1804
+ }
1805
+ ));
1806
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
1807
+
1808
+ // src/components/Inspector.tsx
1809
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1810
+ var Inspector = ({
1811
+ selectedElement,
1812
+ elementRenderer,
1813
+ api,
1814
+ mode,
1815
+ canvasSize,
1816
+ setCanvasSize,
1817
+ style,
1818
+ className
1819
+ }) => {
1820
+ const [editingValues, setEditingValues] = import_react6.default.useState({});
1821
+ const debounceTimers = import_react6.default.useRef({});
1822
+ import_react6.default.useEffect(() => {
1823
+ return () => {
1824
+ Object.values(debounceTimers.current).forEach((timer) => clearTimeout(timer));
1825
+ };
1826
+ }, []);
1827
+ if (!selectedElement || !elementRenderer) {
1828
+ if (mode?.inspectorPlaceholder) {
1829
+ const PlaceholderComponent = mode.inspectorPlaceholder;
1830
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: `w-full bg-card h-full ${className}`, style, children: [
1831
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex h-10 sticky top-0 items-center justify-between px-3 py-2 border-b bg-popover", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "text-sm font-semibold", children: "Inspector" }) }),
1832
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1833
+ PlaceholderComponent,
1834
+ {
1835
+ api,
1836
+ canvasSize,
1837
+ setCanvasSize,
1838
+ ...mode.context || {}
1839
+ }
1840
+ )
1841
+ ] });
1842
+ }
1843
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: `w-full bg-card h-full ${className}`, style, children: [
1844
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex h-10 items-center justify-between px-3 py-2 border-b bg-popover", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "text-sm font-semibold", children: "Inspector" }) }),
1845
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "p-4 text-sm text-muted-foreground", children: "No element selected. Click on an element to edit its properties." })
1846
+ ] });
1847
+ }
1848
+ const schema = elementRenderer.inspectorSchema || [];
1849
+ const handlePropChange = (propName, value) => {
1850
+ api.updateElement(selectedElement.id, {
1851
+ props: {
1852
+ ...selectedElement.props,
1853
+ [propName]: value
1854
+ }
1855
+ });
1856
+ };
1857
+ const handleColorChange = (propName, value) => {
1858
+ if (debounceTimers.current[propName]) {
1859
+ clearTimeout(debounceTimers.current[propName]);
1860
+ }
1861
+ setEditingValues((prev) => ({ ...prev, [propName]: value }));
1862
+ debounceTimers.current[propName] = setTimeout(() => {
1863
+ handlePropChange(propName, value);
1864
+ setEditingValues((prev) => {
1865
+ const newState = { ...prev };
1866
+ delete newState[propName];
1867
+ return newState;
1868
+ });
1869
+ delete debounceTimers.current[propName];
1870
+ }, 300);
1871
+ };
1872
+ const handleTransformChange = (field, value) => {
1873
+ const roundedValue = Math.round(value * 100) / 100;
1874
+ if (field === "x" || field === "y") {
1875
+ api.updateElement(selectedElement.id, {
1876
+ position: {
1877
+ ...selectedElement.position,
1878
+ [field]: roundedValue
1879
+ }
1880
+ });
1881
+ } else if (field === "width" || field === "height") {
1882
+ api.updateElement(selectedElement.id, {
1883
+ size: {
1884
+ ...selectedElement.size,
1885
+ [field]: roundedValue
1886
+ }
1887
+ });
1888
+ } else if (field === "rotation") {
1889
+ const width = selectedElement.size.width;
1890
+ const height = selectedElement.size.height;
1891
+ const oldRotRad = selectedElement.rotation * Math.PI / 180;
1892
+ const centerOffsetX = width / 2;
1893
+ const centerOffsetY = height / 2;
1894
+ const rotatedCenterX = selectedElement.position.x + centerOffsetX * Math.cos(oldRotRad) - centerOffsetY * Math.sin(oldRotRad);
1895
+ const rotatedCenterY = selectedElement.position.y + centerOffsetX * Math.sin(oldRotRad) + centerOffsetY * Math.cos(oldRotRad);
1896
+ const newRotRad = roundedValue * Math.PI / 180;
1897
+ const newX = rotatedCenterX - (centerOffsetX * Math.cos(newRotRad) - centerOffsetY * Math.sin(newRotRad));
1898
+ const newY = rotatedCenterY - (centerOffsetX * Math.sin(newRotRad) + centerOffsetY * Math.cos(newRotRad));
1899
+ api.updateElement(selectedElement.id, {
1900
+ position: {
1901
+ x: Math.round(newX * 100) / 100,
1902
+ y: Math.round(newY * 100) / 100
1903
+ },
1904
+ rotation: roundedValue
1905
+ });
1906
+ } else if (field === "opacity") {
1907
+ const clampedValue = Math.max(0, Math.min(1, roundedValue));
1908
+ api.updateElement(selectedElement.id, {
1909
+ opacity: clampedValue
1910
+ });
1911
+ }
1912
+ };
1913
+ const getDisplayValue = (field) => {
1914
+ if (editingValues[field] !== void 0) {
1915
+ return editingValues[field];
1916
+ }
1917
+ let value;
1918
+ if (field === "x") value = selectedElement.position.x;
1919
+ else if (field === "y") value = selectedElement.position.y;
1920
+ else if (field === "width") value = selectedElement.size.width;
1921
+ else if (field === "height") value = selectedElement.size.height;
1922
+ else if (field === "rotation") value = selectedElement.rotation;
1923
+ else if (field === "opacity") value = selectedElement.opacity;
1924
+ else return "0";
1925
+ return value.toFixed(2);
1926
+ };
1927
+ const handleInputChange = (field, value) => {
1928
+ setEditingValues((prev) => ({ ...prev, [field]: value }));
1929
+ };
1930
+ const handleInputBlur = (field) => {
1931
+ const value = editingValues[field];
1932
+ if (value !== void 0) {
1933
+ const numValue = parseFloat(value);
1934
+ if (!isNaN(numValue)) {
1935
+ handleTransformChange(field, numValue);
1936
+ }
1937
+ setEditingValues((prev) => {
1938
+ const newState = { ...prev };
1939
+ delete newState[field];
1940
+ return newState;
1941
+ });
1942
+ }
1943
+ };
1944
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: `h-full bg-card overflow-y-auto ${className}`, style, children: [
1945
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex h-10 sticky top-0 items-center justify-between px-3 py-2 border-b bg-popover", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "text-sm font-semibold", children: "Inspector" }) }),
1946
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "p-4", children: [
1947
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h3", { className: "text-lg font-semibold mb-1", children: elementRenderer.displayName || selectedElement.type }),
1948
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { className: "text-xs text-muted-foreground mb-4", children: [
1949
+ "ID: ",
1950
+ selectedElement.id
1951
+ ] }),
1952
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Separator, { className: "my-4" }),
1953
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "mb-4", children: [
1954
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: "display-name", className: "text-xs", children: "Display Name" }),
1955
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1956
+ Input,
1957
+ {
1958
+ id: "display-name",
1959
+ type: "text",
1960
+ value: selectedElement.displayName || "",
1961
+ onChange: (e) => {
1962
+ api.updateElement(selectedElement.id, {
1963
+ displayName: e.target.value
1964
+ });
1965
+ },
1966
+ placeholder: elementRenderer.displayName || selectedElement.type,
1967
+ className: "h-8 mt-1"
1968
+ }
1969
+ )
1970
+ ] }),
1971
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Separator, { className: "my-4" }),
1972
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h4", { className: "text-sm font-medium mb-3", children: "Transform" }),
1973
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-3", children: [
1974
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-2", children: [
1975
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-2 items-start", children: [
1976
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: "x-position", className: "text-xs", children: "X" }),
1977
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1978
+ Input,
1979
+ {
1980
+ id: "x-position",
1981
+ type: "number",
1982
+ step: 0.5,
1983
+ value: getDisplayValue("x"),
1984
+ onChange: (e) => handleInputChange("x", e.target.value),
1985
+ onBlur: () => handleInputBlur("x"),
1986
+ className: "h-8"
1987
+ }
1988
+ )
1989
+ ] }),
1990
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-2 items-start", children: [
1991
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: "y-position", className: "text-xs", children: "Y" }),
1992
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
1993
+ Input,
1994
+ {
1995
+ id: "y-position",
1996
+ type: "number",
1997
+ step: 0.5,
1998
+ value: getDisplayValue("y"),
1999
+ onChange: (e) => handleInputChange("y", e.target.value),
2000
+ onBlur: () => handleInputBlur("y"),
2001
+ className: "h-8"
2002
+ }
2003
+ )
2004
+ ] })
2005
+ ] }),
2006
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-2", children: [
2007
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-2 items-start", children: [
2008
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: "width", className: "text-xs", children: "Width" }),
2009
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2010
+ Input,
2011
+ {
2012
+ id: "width",
2013
+ type: "number",
2014
+ step: 0.5,
2015
+ value: getDisplayValue("width"),
2016
+ onChange: (e) => handleInputChange("width", e.target.value),
2017
+ onBlur: () => handleInputBlur("width"),
2018
+ className: "h-8"
2019
+ }
2020
+ )
2021
+ ] }),
2022
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex h-8 gap-2", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-2 items-start", children: [
2023
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: "height", className: "text-xs", children: "Height" }),
2024
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2025
+ Input,
2026
+ {
2027
+ id: "height",
2028
+ type: "number",
2029
+ step: 0.5,
2030
+ value: getDisplayValue("height"),
2031
+ onChange: (e) => handleInputChange("height", e.target.value),
2032
+ onBlur: () => handleInputBlur("height"),
2033
+ className: "h-8"
2034
+ }
2035
+ )
2036
+ ] }) })
2037
+ ] }),
2038
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-2", children: [
2039
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-2 items-start flex-1", children: [
2040
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: "rotation", className: "text-xs", children: "Rotation" }),
2041
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2042
+ Input,
2043
+ {
2044
+ id: "rotation",
2045
+ type: "number",
2046
+ step: 1,
2047
+ value: getDisplayValue("rotation"),
2048
+ onChange: (e) => handleInputChange("rotation", e.target.value),
2049
+ onBlur: () => handleInputBlur("rotation"),
2050
+ className: "h-8 flex-1"
2051
+ }
2052
+ )
2053
+ ] }),
2054
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-2 items-start flex-1", children: [
2055
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: "opacity", className: "text-xs", children: "Opacity" }),
2056
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2057
+ Input,
2058
+ {
2059
+ id: "opacity",
2060
+ type: "number",
2061
+ step: 0.01,
2062
+ min: 0,
2063
+ max: 1,
2064
+ value: getDisplayValue("opacity"),
2065
+ onChange: (e) => handleInputChange("opacity", e.target.value),
2066
+ onBlur: () => handleInputBlur("opacity"),
2067
+ className: "h-8 flex-1"
2068
+ }
2069
+ )
2070
+ ] })
2071
+ ] }),
2072
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Separator, { className: "my-4" }),
2073
+ schema.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
2074
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h4", { className: "text-sm font-medium mb-3", children: "Properties" }),
2075
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "space-y-3", children: schema.map(
2076
+ (field) => renderField(
2077
+ field,
2078
+ selectedElement.props,
2079
+ editingValues,
2080
+ handlePropChange,
2081
+ handleColorChange,
2082
+ mode
2083
+ )
2084
+ ) })
2085
+ ] })
2086
+ ] })
2087
+ ] })
2088
+ ] });
2089
+ };
2090
+ function renderField(field, props, editingValues, onChange, onColorChange, mode) {
2091
+ const value = editingValues[field.name] ?? props[field.name] ?? field.defaultValue;
2092
+ switch (field.type) {
2093
+ case "custom":
2094
+ if (!field.customRenderer) {
2095
+ console.error(`Custom field "${field.name}" has no customRenderer defined`);
2096
+ return null;
2097
+ }
2098
+ const CustomComponent = field.customRenderer;
2099
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2100
+ CustomComponent,
2101
+ {
2102
+ value,
2103
+ onChange: (newValue) => onChange(field.name, newValue),
2104
+ field,
2105
+ elementProps: props,
2106
+ mode
2107
+ }
2108
+ ) }, field.name);
2109
+ case "string":
2110
+ const isMultiline = field.name === "content" || field.name === "text";
2111
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
2112
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: field.name, className: "text-xs", children: field.label }),
2113
+ isMultiline ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2114
+ Textarea,
2115
+ {
2116
+ id: field.name,
2117
+ value: value || "",
2118
+ onChange: (e) => onChange(field.name, e.target.value),
2119
+ className: "mt-1",
2120
+ rows: 3
2121
+ }
2122
+ ) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2123
+ Input,
2124
+ {
2125
+ id: field.name,
2126
+ value: value || "",
2127
+ onChange: (e) => onChange(field.name, e.target.value),
2128
+ className: "h-8 mt-1"
2129
+ }
2130
+ ),
2131
+ field.description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-muted-foreground mt-1", children: field.description })
2132
+ ] }, field.name);
2133
+ case "number":
2134
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
2135
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: field.name, className: "text-xs", children: field.label }),
2136
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2137
+ Input,
2138
+ {
2139
+ id: field.name,
2140
+ type: "number",
2141
+ value: value ?? 0,
2142
+ onChange: (e) => onChange(field.name, parseFloat(Number(e.target.value).toFixed(2))),
2143
+ min: field.min,
2144
+ max: field.max,
2145
+ step: field.step || 1,
2146
+ className: "h-8 mt-1"
2147
+ }
2148
+ ),
2149
+ field.description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-muted-foreground mt-1", children: field.description })
2150
+ ] }, field.name);
2151
+ case "color":
2152
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
2153
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: field.name, className: "text-xs", children: field.label }),
2154
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2155
+ Input,
2156
+ {
2157
+ id: field.name,
2158
+ type: "color",
2159
+ value: value || "#000000",
2160
+ onChange: (e) => onColorChange(field.name, e.target.value),
2161
+ className: "h-10 mt-1"
2162
+ }
2163
+ ),
2164
+ field.description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-muted-foreground mt-1", children: field.description })
2165
+ ] }, field.name);
2166
+ case "select":
2167
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
2168
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: field.name, className: "text-xs", children: field.label }),
2169
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2170
+ Select,
2171
+ {
2172
+ value: value || field.defaultValue || "",
2173
+ onValueChange: (newValue) => onChange(field.name, newValue),
2174
+ children: [
2175
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SelectTrigger, { className: "h-8 mt-1", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SelectValue, { placeholder: field.label }) }),
2176
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SelectContent, { children: field.options?.map((option) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SelectItem, { value: String(option.value), children: option.label }, option.value)) })
2177
+ ]
2178
+ }
2179
+ ),
2180
+ field.description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-muted-foreground mt-1", children: field.description })
2181
+ ] }, field.name);
2182
+ case "boolean":
2183
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-start space-x-2", children: [
2184
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2185
+ Checkbox,
2186
+ {
2187
+ id: field.name,
2188
+ checked: Boolean(value),
2189
+ onCheckedChange: (checked) => onChange(field.name, checked),
2190
+ className: "mt-1"
2191
+ }
2192
+ ),
2193
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
2194
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: field.name, className: "text-xs font-medium cursor-pointer", children: field.label }),
2195
+ field.description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-muted-foreground", children: field.description })
2196
+ ] })
2197
+ ] }, field.name);
2198
+ case "slider":
2199
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
2200
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [
2201
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { className: "text-xs", children: field.label }),
2202
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-xs text-muted-foreground", children: value })
2203
+ ] }),
2204
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2205
+ Slider,
2206
+ {
2207
+ value: [Number(value ?? field.defaultValue ?? 0)],
2208
+ onValueChange: ([newValue]) => onChange(field.name, newValue),
2209
+ min: field.min || 0,
2210
+ max: field.max || 100,
2211
+ step: field.step || 1,
2212
+ className: "mt-1"
2213
+ }
2214
+ ),
2215
+ field.description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-muted-foreground mt-1", children: field.description })
2216
+ ] }, field.name);
2217
+ case "image":
2218
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
2219
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Label, { htmlFor: field.name, className: "text-xs", children: field.label }),
2220
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2221
+ Input,
2222
+ {
2223
+ id: field.name,
2224
+ value: value || "",
2225
+ onChange: (e) => onChange(field.name, e.target.value),
2226
+ className: "h-8 mt-1",
2227
+ placeholder: "Image URL or path"
2228
+ }
2229
+ ),
2230
+ field.description && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-xs text-muted-foreground mt-1", children: field.description })
2231
+ ] }, field.name);
2232
+ default:
2233
+ return null;
2234
+ }
2235
+ }
2236
+
2237
+ // src/components/LayersPanel.tsx
2238
+ var import_react7 = __toESM(require("react"));
2239
+
2240
+ // src/ui/badge.tsx
2241
+ var import_class_variance_authority2 = require("class-variance-authority");
2242
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2243
+ var badgeVariants = (0, import_class_variance_authority2.cva)(
2244
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
2245
+ {
2246
+ variants: {
2247
+ variant: {
2248
+ default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
2249
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
2250
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
2251
+ outline: "text-foreground"
2252
+ }
2253
+ },
2254
+ defaultVariants: {
2255
+ variant: "default"
2256
+ }
2257
+ }
2258
+ );
2259
+ function Badge({ className, variant, ...props }) {
2260
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: cn(badgeVariants({ variant }), className), ...props });
2261
+ }
2262
+
2263
+ // src/ui/scroll-area.tsx
2264
+ var React12 = __toESM(require("react"));
2265
+ var ScrollAreaPrimitive = __toESM(require("@radix-ui/react-scroll-area"));
2266
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2267
+ var ScrollArea = React12.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
2268
+ ScrollAreaPrimitive.Root,
2269
+ {
2270
+ ref,
2271
+ className: cn("relative overflow-hidden", className),
2272
+ ...props,
2273
+ children: [
2274
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollAreaPrimitive.Viewport, { className: "h-full w-full rounded-[inherit]", children }),
2275
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollBar, {}),
2276
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollAreaPrimitive.Corner, {})
2277
+ ]
2278
+ }
2279
+ ));
2280
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
2281
+ var ScrollBar = React12.forwardRef(({ className, orientation = "vertical", ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
2282
+ ScrollAreaPrimitive.ScrollAreaScrollbar,
2283
+ {
2284
+ ref,
2285
+ orientation,
2286
+ className: cn(
2287
+ "flex touch-none select-none transition-colors",
2288
+ orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
2289
+ orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
2290
+ className
2291
+ ),
2292
+ ...props,
2293
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ScrollAreaPrimitive.ScrollAreaThumb, { className: "relative flex-1 rounded-full bg-border" })
2294
+ }
2295
+ ));
2296
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
2297
+
2298
+ // src/ui/general/TooltipButton.tsx
2299
+ var React15 = __toESM(require("react"));
2300
+
2301
+ // src/ui/button.tsx
2302
+ var React13 = __toESM(require("react"));
2303
+ var import_react_slot = require("@radix-ui/react-slot");
2304
+ var import_class_variance_authority3 = require("class-variance-authority");
2305
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2306
+ var buttonVariants = (0, import_class_variance_authority3.cva)(
2307
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
2308
+ {
2309
+ variants: {
2310
+ variant: {
2311
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
2312
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
2313
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
2314
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
2315
+ ghost: "hover:bg-accent hover:text-accent-foreground",
2316
+ link: "text-primary underline-offset-4 hover:underline"
2317
+ },
2318
+ size: {
2319
+ default: "h-10 px-4 py-2",
2320
+ sm: "h-9 rounded-md px-3",
2321
+ lg: "h-11 rounded-md px-8",
2322
+ icon: "h-10 w-10"
2323
+ }
2324
+ },
2325
+ defaultVariants: {
2326
+ variant: "default",
2327
+ size: "default"
2328
+ }
2329
+ }
2330
+ );
2331
+ var Button = React13.forwardRef(
2332
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
2333
+ const Comp = asChild ? import_react_slot.Slot : "button";
2334
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2335
+ Comp,
2336
+ {
2337
+ className: cn(buttonVariants({ variant, size, className })),
2338
+ ref,
2339
+ ...props
2340
+ }
2341
+ );
2342
+ }
2343
+ );
2344
+ Button.displayName = "Button";
2345
+
2346
+ // src/ui/tooltip.tsx
2347
+ var React14 = __toESM(require("react"));
2348
+ var TooltipPrimitive = __toESM(require("@radix-ui/react-tooltip"));
2349
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2350
+ var TooltipProvider = TooltipPrimitive.Provider;
2351
+ var Tooltip = TooltipPrimitive.Root;
2352
+ var TooltipTrigger = TooltipPrimitive.Trigger;
2353
+ var TooltipContent = React14.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2354
+ TooltipPrimitive.Content,
2355
+ {
2356
+ ref,
2357
+ sideOffset,
2358
+ className: cn(
2359
+ "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
2360
+ className
2361
+ ),
2362
+ ...props
2363
+ }
2364
+ ));
2365
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
2366
+
2367
+ // src/ui/general/TooltipWrapper.tsx
2368
+ var import_jsx_runtime16 = require("react/jsx-runtime");
2369
+ var TooltipWrapper = ({
2370
+ tooltip,
2371
+ shortcut,
2372
+ tooltipSide = "top",
2373
+ tooltipAlign = "center",
2374
+ tooltipDelay = 700,
2375
+ children,
2376
+ triggerClassName = "flex-1",
2377
+ asChild = true
2378
+ }) => {
2379
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TooltipProvider, { delayDuration: tooltipDelay, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(Tooltip, { children: [
2380
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TooltipTrigger, { className: triggerClassName, asChild, children }),
2381
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TooltipContent, { side: tooltipSide, align: tooltipAlign, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
2382
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: tooltip }),
2383
+ shortcut && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("kbd", { className: "inline-flex items-center px-1.5 py-0.5 text-xs font-mono bg-muted text-gray rounded border", children: shortcut })
2384
+ ] }) })
2385
+ ] }) });
2386
+ };
2387
+ TooltipWrapper.displayName = "TooltipWrapper";
2388
+
2389
+ // src/ui/general/TooltipButton.tsx
2390
+ var import_jsx_runtime17 = require("react/jsx-runtime");
2391
+ var TooltipButton = React15.forwardRef(
2392
+ ({
2393
+ tooltip,
2394
+ shortcut,
2395
+ tooltipSide = "top",
2396
+ tooltipAlign = "center",
2397
+ tooltipDelay = 700,
2398
+ children,
2399
+ className,
2400
+ ...props
2401
+ }, ref) => {
2402
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
2403
+ TooltipWrapper,
2404
+ {
2405
+ tooltip,
2406
+ shortcut,
2407
+ tooltipSide,
2408
+ tooltipAlign,
2409
+ tooltipDelay,
2410
+ asChild: true,
2411
+ triggerClassName: "",
2412
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { ref, className, ...props, children })
2413
+ }
2414
+ );
2415
+ }
2416
+ );
2417
+ TooltipButton.displayName = "TooltipButton";
2418
+
2419
+ // src/components/LayersPanel.tsx
2420
+ var import_lucide_react5 = require("lucide-react");
2421
+ var import_jsx_runtime18 = require("react/jsx-runtime");
2422
+ var getElementIcon = (type) => {
2423
+ switch (type) {
2424
+ case "text":
2425
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Type, { className: "h-4 w-4" });
2426
+ case "image":
2427
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Image, { className: "h-4 w-4" });
2428
+ default:
2429
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Layers, { className: "h-4 w-4" });
2430
+ }
2431
+ };
2432
+ var getElementDisplayName = (element, renderer) => {
2433
+ if (element.displayName) return element.displayName;
2434
+ if (renderer?.displayName) return renderer.displayName;
2435
+ if (element.type === "text" && element.props.content) {
2436
+ const preview = element.props.content.substring(0, 20);
2437
+ return `${preview}${element.props.content.length > 20 ? "..." : ""}`;
2438
+ }
2439
+ return element.type.charAt(0).toUpperCase() + element.type.slice(1);
2440
+ };
2441
+ var LayersPanel = ({
2442
+ elements,
2443
+ selectedElementId,
2444
+ api,
2445
+ elementRenderers,
2446
+ style,
2447
+ className
2448
+ }) => {
2449
+ const [filter, setFilter] = import_react7.default.useState("all");
2450
+ const availableTypes = Array.from(elementRenderers.keys());
2451
+ const sortedElements = [...elements].sort((a, b) => b.zIndex - a.zIndex);
2452
+ const filteredElements = sortedElements.filter((el) => {
2453
+ if (filter === "all") return true;
2454
+ return el.type === filter;
2455
+ });
2456
+ const handleMoveUp = (element, e) => {
2457
+ e.stopPropagation();
2458
+ const currentIndex = elements.findIndex((el) => el.id === element.id);
2459
+ if (currentIndex < elements.length - 1) {
2460
+ api.reorderElement(element.id, currentIndex + 1);
2461
+ }
2462
+ };
2463
+ const handleMoveDown = (element, e) => {
2464
+ e.stopPropagation();
2465
+ const currentIndex = elements.findIndex((el) => el.id === element.id);
2466
+ if (currentIndex > 0) {
2467
+ api.reorderElement(element.id, currentIndex - 1);
2468
+ }
2469
+ };
2470
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: cn("flex flex-col bg-card border-l", className), style, children: [
2471
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex h-10 items-center justify-between px-3 py-2 border-b bg-popover", children: [
2472
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { className: "text-sm font-semibold", children: "Layers" }),
2473
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Badge, { variant: "default", className: "text-xs", children: filteredElements.length })
2474
+ ] }),
2475
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex gap-2 items-center p-3 border-b", children: [
2476
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Filter, { className: "icon" }),
2477
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(Select, { value: filter, onValueChange: (value) => setFilter(value), children: [
2478
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SelectTrigger, { id: "layer-filter", className: "h-8", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SelectValue, {}) }),
2479
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(SelectContent, { children: [
2480
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SelectItem, { value: "all", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center gap-2", children: [
2481
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Layers, { className: "h-4 w-4" }),
2482
+ "All Elements"
2483
+ ] }) }),
2484
+ availableTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SelectItem, { value: type, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center gap-2", children: [
2485
+ getElementIcon(type),
2486
+ type.charAt(0).toUpperCase() + type.slice(1),
2487
+ " Only"
2488
+ ] }) }, type))
2489
+ ] })
2490
+ ] })
2491
+ ] }),
2492
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ScrollArea, { className: "flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "p-2 space-y-1", children: filteredElements.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-center py-8 text-sm text-muted-foreground", children: filter === "all" ? "No elements yet" : `No ${filter} elements` }) : filteredElements.map((element) => {
2493
+ const renderer = elementRenderers.get(element.type);
2494
+ const isSelected = element.id === selectedElementId;
2495
+ const isVisible = element.visible !== false;
2496
+ const isLocked = element.locked === true;
2497
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
2498
+ "div",
2499
+ {
2500
+ className: cn(
2501
+ "group flex items-center gap-2 px-2 py-2 rounded cursor-pointer transition-all",
2502
+ isSelected ? "bg-primary/15 border border-primary/30 shadow-sm" : "hover:bg-muted/70",
2503
+ isLocked && "opacity-60",
2504
+ !isVisible && "opacity-40"
2505
+ ),
2506
+ onClick: () => !isLocked && api.selectElement(element.id),
2507
+ children: [
2508
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-muted-foreground", children: getElementIcon(element.type) }),
2509
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex-1 min-w-0", children: [
2510
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs font-medium truncate", children: getElementDisplayName(element, renderer) }),
2511
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "flex items-center gap-2 text-[10px] text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { children: [
2512
+ "z:",
2513
+ element.zIndex
2514
+ ] }) })
2515
+ ] }),
2516
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity", children: [
2517
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-col", children: [
2518
+ element.zIndex < elements.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2519
+ TooltipButton,
2520
+ {
2521
+ variant: "ghost",
2522
+ size: "icon",
2523
+ className: "h-3 w-6",
2524
+ onClick: (e) => handleMoveUp(element, e),
2525
+ tooltip: "Move Upward",
2526
+ tooltipDelay: 500,
2527
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.ChevronUp, { className: "h-3 w-3" })
2528
+ }
2529
+ ),
2530
+ element.zIndex > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2531
+ TooltipButton,
2532
+ {
2533
+ variant: "ghost",
2534
+ size: "icon",
2535
+ className: "h-3 w-6",
2536
+ onClick: (e) => handleMoveDown(element, e),
2537
+ tooltip: "Move Downward",
2538
+ tooltipDelay: 500,
2539
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.ChevronDown, { className: "h-3 w-3" })
2540
+ }
2541
+ )
2542
+ ] }),
2543
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2544
+ TooltipButton,
2545
+ {
2546
+ variant: "ghost",
2547
+ size: "icon",
2548
+ className: "h-6 w-6",
2549
+ onClick: (e) => {
2550
+ e.stopPropagation();
2551
+ api.updateElement(element.id, { visible: !isVisible });
2552
+ },
2553
+ tooltip: isVisible ? "Hide" : "Show",
2554
+ tooltipDelay: 500,
2555
+ children: isVisible ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.EyeOff, { className: "h-3 w-3" })
2556
+ }
2557
+ ),
2558
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2559
+ TooltipButton,
2560
+ {
2561
+ variant: "ghost",
2562
+ size: "icon",
2563
+ className: "h-6 w-6",
2564
+ onClick: (e) => {
2565
+ e.stopPropagation();
2566
+ api.updateElement(element.id, { locked: !isLocked });
2567
+ },
2568
+ tooltip: isLocked ? "Unlock" : "Lock",
2569
+ tooltipDelay: 500,
2570
+ children: isLocked ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Lock, { className: "h-3 w-3" }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Unlock, { className: "h-3 w-3" })
2571
+ }
2572
+ ),
2573
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
2574
+ TooltipButton,
2575
+ {
2576
+ variant: "ghost",
2577
+ size: "icon",
2578
+ className: "h-6 w-6 text-destructive hover:text-destructive",
2579
+ onClick: (e) => {
2580
+ e.stopPropagation();
2581
+ api.removeElement(element.id);
2582
+ },
2583
+ tooltip: "Delete",
2584
+ tooltipDelay: 500,
2585
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react5.Trash2, { className: "h-3 w-3" })
2586
+ }
2587
+ )
2588
+ ] })
2589
+ ]
2590
+ },
2591
+ element.id
2592
+ );
2593
+ }) }) }),
2594
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "border-t p-2", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [
2595
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { children: [
2596
+ elements.filter((e) => e.visible !== false).length,
2597
+ " visible"
2598
+ ] }),
2599
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "\u2022" }),
2600
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { children: [
2601
+ elements.filter((e) => e.locked === true).length,
2602
+ " locked"
2603
+ ] })
2604
+ ] }) })
2605
+ ] });
2606
+ };
2607
+
2608
+ // src/components/VisualEditorWorkspace.tsx
2609
+ var import_react10 = __toESM(require("react"));
2610
+
2611
+ // src/components/Topbar.tsx
2612
+ var import_lucide_react7 = require("lucide-react");
2613
+
2614
+ // src/ui/switch.tsx
2615
+ var React17 = __toESM(require("react"));
2616
+ var SwitchPrimitives = __toESM(require("@radix-ui/react-switch"));
2617
+ var import_jsx_runtime19 = require("react/jsx-runtime");
2618
+ var Switch = React17.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2619
+ SwitchPrimitives.Root,
2620
+ {
2621
+ className: cn(
2622
+ "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-secondary data-[state=unchecked]:bg-input",
2623
+ className
2624
+ ),
2625
+ ...props,
2626
+ ref,
2627
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
2628
+ SwitchPrimitives.Thumb,
2629
+ {
2630
+ className: cn(
2631
+ "pointer-events-none block h-5 w-5 rounded-full bg-primary shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0 data-[state=checked]:bg-background"
2632
+ )
2633
+ }
2634
+ )
2635
+ }
2636
+ ));
2637
+ Switch.displayName = SwitchPrimitives.Root.displayName;
2638
+
2639
+ // src/components/CustomActionRenderer.tsx
2640
+ var import_lucide_react6 = require("lucide-react");
2641
+ var import_jsx_runtime20 = require("react/jsx-runtime");
2642
+ var CustomActionRenderer = ({
2643
+ action,
2644
+ api,
2645
+ layout = "horizontal"
2646
+ }) => {
2647
+ const isDisabled = (disabled) => {
2648
+ if (typeof disabled === "function") {
2649
+ return disabled(api);
2650
+ }
2651
+ return disabled || false;
2652
+ };
2653
+ const isVertical = layout === "vertical";
2654
+ switch (action.type) {
2655
+ case "button":
2656
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2657
+ TooltipButton,
2658
+ {
2659
+ variant: isVertical ? "outline" : "ghost",
2660
+ size: isVertical ? "sm" : "icon",
2661
+ onClick: () => action.onClick(api),
2662
+ disabled: isDisabled(action.disabled),
2663
+ className: isVertical ? "icon-button" : "h-8 w-8",
2664
+ tooltip: action.label,
2665
+ shortcut: action.shortcut,
2666
+ tooltipDelay: 500,
2667
+ tooltipSide: isVertical ? "right" : void 0,
2668
+ children: action.icon || /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(import_lucide_react6.Plus, { className: "h-4 w-4" })
2669
+ },
2670
+ action.id
2671
+ );
2672
+ case "dropdown":
2673
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2674
+ "div",
2675
+ {
2676
+ className: isVertical ? "flex flex-col gap-1 px-1" : "flex items-center gap-2",
2677
+ children: [
2678
+ isVertical ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Label, { className: "text-xs", children: action.label }) : action.icon && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-muted-foreground", children: action.icon }),
2679
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2680
+ Select,
2681
+ {
2682
+ value: action.value,
2683
+ onValueChange: (value) => action.onChange(value, api),
2684
+ disabled: isDisabled(action.disabled),
2685
+ children: [
2686
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SelectTrigger, { className: isVertical ? "h-8 w-full" : "h-8 w-auto min-w-[120px]", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SelectValue, { placeholder: action.placeholder || action.label }) }),
2687
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SelectContent, { children: action.options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(SelectItem, { value: option.value, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-2", children: [
2688
+ option.icon && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { children: option.icon }),
2689
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { children: option.label })
2690
+ ] }) }, option.value)) })
2691
+ ]
2692
+ }
2693
+ )
2694
+ ]
2695
+ },
2696
+ action.id
2697
+ );
2698
+ case "input":
2699
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2700
+ "div",
2701
+ {
2702
+ className: isVertical ? "flex flex-col gap-1 px-1" : "flex items-center gap-2",
2703
+ children: [
2704
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Label, { className: "text-xs whitespace-nowrap", children: [
2705
+ action.label,
2706
+ isVertical ? "" : ":"
2707
+ ] }),
2708
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2709
+ Input,
2710
+ {
2711
+ type: action.inputType || "text",
2712
+ value: action.value,
2713
+ onChange: (e) => action.onChange(e.target.value, api),
2714
+ placeholder: action.placeholder,
2715
+ disabled: isDisabled(action.disabled),
2716
+ className: isVertical ? "h-8 w-full" : "h-8 w-32",
2717
+ min: action.min,
2718
+ max: action.max,
2719
+ step: action.step
2720
+ }
2721
+ )
2722
+ ]
2723
+ },
2724
+ action.id
2725
+ );
2726
+ case "color":
2727
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2728
+ "div",
2729
+ {
2730
+ className: isVertical ? "flex flex-col gap-1 px-1" : "flex items-center gap-2",
2731
+ children: [
2732
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Label, { className: "text-xs whitespace-nowrap", children: [
2733
+ action.label,
2734
+ isVertical ? "" : ":"
2735
+ ] }),
2736
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2737
+ Input,
2738
+ {
2739
+ type: "color",
2740
+ value: action.value,
2741
+ onChange: (e) => action.onChange(e.target.value, api),
2742
+ disabled: isDisabled(action.disabled),
2743
+ className: isVertical ? "h-8 w-full p-1 cursor-pointer" : "h-8 w-16 p-1 cursor-pointer"
2744
+ }
2745
+ )
2746
+ ]
2747
+ },
2748
+ action.id
2749
+ );
2750
+ case "toggle":
2751
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
2752
+ "div",
2753
+ {
2754
+ className: isVertical ? "flex items-center justify-between gap-2 px-1" : "flex items-center gap-2",
2755
+ children: [
2756
+ action.icon && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-muted-foreground", children: action.icon }),
2757
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Label, { className: "text-xs whitespace-nowrap", children: action.label }),
2758
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2759
+ Switch,
2760
+ {
2761
+ checked: action.value,
2762
+ onCheckedChange: (checked) => action.onChange(checked, api),
2763
+ disabled: isDisabled(action.disabled)
2764
+ }
2765
+ )
2766
+ ]
2767
+ },
2768
+ action.id
2769
+ );
2770
+ case "separator":
2771
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2772
+ Separator,
2773
+ {
2774
+ orientation: isVertical ? "horizontal" : "vertical",
2775
+ className: isVertical ? void 0 : "h-6"
2776
+ },
2777
+ action.id
2778
+ );
2779
+ default:
2780
+ return null;
2781
+ }
2782
+ };
2783
+ var CustomActionRenderer_default = CustomActionRenderer;
2784
+
2785
+ // src/components/Topbar.tsx
2786
+ var import_jsx_runtime21 = require("react/jsx-runtime");
2787
+ var Topbar = ({
2788
+ api,
2789
+ canvasSize,
2790
+ canUndo,
2791
+ canRedo,
2792
+ onUndo,
2793
+ onRedo,
2794
+ onExport,
2795
+ onImport,
2796
+ setCanvasSize,
2797
+ backgroundColor,
2798
+ setBackgroundColor,
2799
+ backgroundImage,
2800
+ setBackgroundImage,
2801
+ imageUrls,
2802
+ clipboardRef,
2803
+ enableSnapGuides = true,
2804
+ onSnapGuidesChange,
2805
+ config,
2806
+ style,
2807
+ className
2808
+ }) => {
2809
+ const selectedElement = api.getSelectedElement();
2810
+ const {
2811
+ showUndo = true,
2812
+ showRedo = true,
2813
+ showDelete = true,
2814
+ showCopy = true,
2815
+ showPaste = true,
2816
+ showDuplicate = true,
2817
+ showExport = true,
2818
+ showImport = true,
2819
+ showCanvasSize = true,
2820
+ showSnapGuides = true,
2821
+ actionsStart = [],
2822
+ actionsEnd = [],
2823
+ actionsStartClassName = "",
2824
+ actionsEndClassName = "",
2825
+ customActions = []
2826
+ // Backwards compatibility - defaults to end
2827
+ } = config || {};
2828
+ const defaultSnapGuidesAction = showSnapGuides && onSnapGuidesChange ? [
2829
+ {
2830
+ type: "toggle",
2831
+ id: "snap-guides-toggle",
2832
+ label: "Snap Guides",
2833
+ value: enableSnapGuides,
2834
+ onChange: (value) => {
2835
+ onSnapGuidesChange(value);
2836
+ }
2837
+ }
2838
+ ] : [];
2839
+ const endActions = [...actionsEnd, ...defaultSnapGuidesAction, ...customActions];
2840
+ const handleDelete = () => {
2841
+ if (selectedElement) {
2842
+ api.removeElement(selectedElement.id);
2843
+ }
2844
+ };
2845
+ const handleCopy = () => {
2846
+ const copied = api.copyElement();
2847
+ if (copied && clipboardRef) {
2848
+ clipboardRef.current = copied;
2849
+ }
2850
+ };
2851
+ const handlePaste = () => {
2852
+ if (clipboardRef?.current) {
2853
+ api.pasteElement(clipboardRef.current);
2854
+ }
2855
+ };
2856
+ const handleDuplicate = () => {
2857
+ api.duplicateElement();
2858
+ };
2859
+ const isDisabled = (disabled) => {
2860
+ if (typeof disabled === "function") {
2861
+ return disabled(api);
2862
+ }
2863
+ return disabled || false;
2864
+ };
2865
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
2866
+ "div",
2867
+ {
2868
+ className: `flex h-12 items-center px-3 py-1 bg-popover border-b gap-2 ${className}`,
2869
+ style,
2870
+ children: [
2871
+ showCanvasSize && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-2", children: [
2872
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-sm", children: "Canvas:" }),
2873
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
2874
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-xs text-muted-foreground whitespace-nowrap", children: "W:" }),
2875
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2876
+ Input,
2877
+ {
2878
+ placeholder: "Width",
2879
+ className: "h-8 w-20",
2880
+ type: "number",
2881
+ step: 1,
2882
+ value: canvasSize.width,
2883
+ onChange: (e) => setCanvasSize(Number(Number(e.target.value).toFixed(0)), canvasSize.height)
2884
+ }
2885
+ )
2886
+ ] }),
2887
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
2888
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-xs text-muted-foreground whitespace-nowrap", children: "H:" }),
2889
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2890
+ Input,
2891
+ {
2892
+ placeholder: "Height",
2893
+ className: "h-8 w-20",
2894
+ type: "number",
2895
+ step: 1,
2896
+ value: canvasSize.height,
2897
+ onChange: (e) => setCanvasSize(canvasSize.width, Number(Number(e.target.value).toFixed(0)))
2898
+ }
2899
+ )
2900
+ ] }),
2901
+ setBackgroundColor && backgroundColor && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
2902
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-xs text-muted-foreground whitespace-nowrap", children: "BG:" }),
2903
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2904
+ Input,
2905
+ {
2906
+ placeholder: "#000000",
2907
+ className: "h-8 w-24",
2908
+ type: "color",
2909
+ value: backgroundColor,
2910
+ onChange: (e) => setBackgroundColor(e.target.value)
2911
+ }
2912
+ )
2913
+ ] }),
2914
+ setBackgroundImage && imageUrls && imageUrls.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
2915
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "text-xs text-muted-foreground whitespace-nowrap", children: "Texture:" }),
2916
+ backgroundImage ? /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
2917
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Select, { value: backgroundImage, onValueChange: setBackgroundImage, children: [
2918
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SelectTrigger, { className: "h-8 w-32", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SelectValue, { placeholder: "None" }) }),
2919
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SelectContent, { children: Array.from(imageUrls.keys()).map((imageName) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SelectItem, { value: imageName, children: imageName }, imageName)) })
2920
+ ] }),
2921
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2922
+ Button,
2923
+ {
2924
+ variant: "ghost",
2925
+ size: "icon",
2926
+ className: "h-8 w-8",
2927
+ onClick: () => setBackgroundImage(""),
2928
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.X, { className: "h-3 w-3" })
2929
+ }
2930
+ )
2931
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Select, { value: "", onValueChange: setBackgroundImage, children: [
2932
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SelectTrigger, { className: "h-8 w-32", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SelectValue, { placeholder: "None", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-2", children: [
2933
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.Image, { className: "h-3 w-3" }),
2934
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: "None" })
2935
+ ] }) }) }),
2936
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SelectContent, { children: Array.from(imageUrls.keys()).map((imageName) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(SelectItem, { value: imageName, children: imageName }, imageName)) })
2937
+ ] })
2938
+ ] })
2939
+ ] }),
2940
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex w-full gap-2", children: [
2941
+ actionsStart.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
2942
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: `flex items-center gap-1 ${actionsStartClassName}`, children: actionsStart.map((action) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2943
+ CustomActionRenderer_default,
2944
+ {
2945
+ action,
2946
+ api,
2947
+ layout: "horizontal"
2948
+ },
2949
+ action.id
2950
+ )) }),
2951
+ (showUndo || showRedo || showDelete || showExport && onExport || showImport && onImport || endActions.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Separator, { orientation: "vertical", className: "h-6" })
2952
+ ] }),
2953
+ (showUndo || showRedo) && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
2954
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
2955
+ showUndo && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2956
+ TooltipButton,
2957
+ {
2958
+ variant: "ghost",
2959
+ size: "icon",
2960
+ onClick: onUndo,
2961
+ disabled: !canUndo,
2962
+ className: "h-8 w-8",
2963
+ tooltip: "Undo",
2964
+ shortcut: "Ctrl+Z",
2965
+ tooltipDelay: 500,
2966
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.Undo, { className: "h-4 w-4" })
2967
+ }
2968
+ ),
2969
+ showRedo && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2970
+ TooltipButton,
2971
+ {
2972
+ variant: "ghost",
2973
+ size: "icon",
2974
+ onClick: onRedo,
2975
+ disabled: !canRedo,
2976
+ className: "h-8 w-8",
2977
+ tooltip: "Redo",
2978
+ shortcut: "Ctrl+Y",
2979
+ tooltipDelay: 500,
2980
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.Redo, { className: "h-4 w-4" })
2981
+ }
2982
+ )
2983
+ ] }),
2984
+ (showCopy || showPaste || showDuplicate || showDelete || showExport && onExport || showImport && onImport || endActions.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Separator, { orientation: "vertical", className: "h-6" })
2985
+ ] }),
2986
+ (showCopy || showPaste || showDuplicate || showDelete) && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
2987
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
2988
+ showCopy && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2989
+ TooltipButton,
2990
+ {
2991
+ variant: "ghost",
2992
+ size: "icon",
2993
+ onClick: handleCopy,
2994
+ disabled: !selectedElement,
2995
+ className: "h-8 w-8",
2996
+ tooltip: "Copy",
2997
+ shortcut: "Ctrl+C",
2998
+ tooltipDelay: 500,
2999
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.Copy, { className: "h-4 w-4" })
3000
+ }
3001
+ ),
3002
+ showPaste && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3003
+ TooltipButton,
3004
+ {
3005
+ variant: "ghost",
3006
+ size: "icon",
3007
+ onClick: handlePaste,
3008
+ disabled: !clipboardRef?.current,
3009
+ className: "h-8 w-8",
3010
+ tooltip: "Paste",
3011
+ shortcut: "Ctrl+V",
3012
+ tooltipDelay: 500,
3013
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.Clipboard, { className: "h-4 w-4" })
3014
+ }
3015
+ ),
3016
+ showDuplicate && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3017
+ TooltipButton,
3018
+ {
3019
+ variant: "ghost",
3020
+ size: "icon",
3021
+ onClick: handleDuplicate,
3022
+ disabled: !selectedElement,
3023
+ className: "h-8 w-8",
3024
+ tooltip: "Duplicate",
3025
+ shortcut: "Ctrl+D",
3026
+ tooltipDelay: 500,
3027
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.CopyPlus, { className: "h-4 w-4" })
3028
+ }
3029
+ ),
3030
+ showDelete && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3031
+ TooltipButton,
3032
+ {
3033
+ variant: "ghost",
3034
+ size: "icon",
3035
+ onClick: handleDelete,
3036
+ disabled: !selectedElement,
3037
+ className: "h-8 w-8 text-destructive hover:text-destructive",
3038
+ tooltip: "Delete Selected",
3039
+ shortcut: "Delete",
3040
+ tooltipDelay: 500,
3041
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.Trash2, { className: "h-4 w-4" })
3042
+ }
3043
+ )
3044
+ ] }),
3045
+ (showExport && onExport || showImport && onImport || endActions.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Separator, { orientation: "vertical", className: "h-6" })
3046
+ ] }),
3047
+ (showExport && onExport || showImport && onImport) && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
3048
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex items-center gap-1", children: [
3049
+ showExport && onExport && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3050
+ TooltipButton,
3051
+ {
3052
+ variant: "ghost",
3053
+ size: "icon",
3054
+ onClick: onExport,
3055
+ className: "h-8 w-8",
3056
+ tooltip: "Export to JSON",
3057
+ tooltipDelay: 500,
3058
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.Download, { className: "h-4 w-4" })
3059
+ }
3060
+ ),
3061
+ showImport && onImport && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
3062
+ TooltipButton,
3063
+ {
3064
+ variant: "ghost",
3065
+ size: "icon",
3066
+ onClick: onImport,
3067
+ className: "h-8 w-8",
3068
+ tooltip: "Import from JSON",
3069
+ tooltipDelay: 500,
3070
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_lucide_react7.Upload, { className: "h-4 w-4" })
3071
+ }
3072
+ )
3073
+ ] }),
3074
+ endActions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Separator, { orientation: "vertical", className: "h-6" })
3075
+ ] }),
3076
+ endActions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: `flex items-center gap-1 ${actionsEndClassName}`, children: endActions.map((action) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(CustomActionRenderer_default, { action, api, layout: "horizontal" }, action.id)) })
3077
+ ] })
3078
+ ]
3079
+ }
3080
+ );
3081
+ };
3082
+
3083
+ // src/components/Canvas.tsx
3084
+ var import_react9 = __toESM(require("react"));
3085
+ var import_react_konva6 = require("react-konva");
3086
+
3087
+ // src/components/SnapGuides.tsx
3088
+ var import_react_konva4 = require("react-konva");
3089
+ var import_jsx_runtime22 = require("react/jsx-runtime");
3090
+ var SnapGuides = ({
3091
+ verticalGuides,
3092
+ horizontalGuides,
3093
+ canvasSize
3094
+ }) => {
3095
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
3096
+ verticalGuides.map((guide, index) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3097
+ import_react_konva4.Line,
3098
+ {
3099
+ points: [guide.position, 0, guide.position, canvasSize.height],
3100
+ stroke: guide.type === "center" ? "#ff00ff" : "#00ff00",
3101
+ strokeWidth: 1,
3102
+ dash: guide.type === "center" ? [4, 4] : void 0,
3103
+ listening: false
3104
+ },
3105
+ `v-${index}`
3106
+ )),
3107
+ horizontalGuides.map((guide, index) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
3108
+ import_react_konva4.Line,
3109
+ {
3110
+ points: [0, guide.position, canvasSize.width, guide.position],
3111
+ stroke: guide.type === "center" ? "#ff00ff" : "#00ff00",
3112
+ strokeWidth: 1,
3113
+ dash: guide.type === "center" ? [4, 4] : void 0,
3114
+ listening: false
3115
+ },
3116
+ `h-${index}`
3117
+ ))
3118
+ ] });
3119
+ };
3120
+
3121
+ // src/components/CentralizedTransformer.tsx
3122
+ var import_react8 = require("react");
3123
+ var import_react_konva5 = require("react-konva");
3124
+ var import_jsx_runtime23 = require("react/jsx-runtime");
3125
+ var CentralizedTransformer = ({
3126
+ selectedElementId,
3127
+ stageRef,
3128
+ isLocked = false,
3129
+ updateTrigger = 0,
3130
+ onTransformEnd,
3131
+ onTransform
3132
+ }) => {
3133
+ const transformerRef = (0, import_react8.useRef)(null);
3134
+ (0, import_react8.useEffect)(() => {
3135
+ const transformer = transformerRef.current;
3136
+ const stage = stageRef.current;
3137
+ if (!transformer || !stage || !selectedElementId || isLocked) {
3138
+ if (transformer) {
3139
+ transformer.nodes([]);
3140
+ transformer.getLayer()?.batchDraw();
3141
+ }
3142
+ return;
3143
+ }
3144
+ const findNodeById = (id) => {
3145
+ const layer = stage.getLayers()[0];
3146
+ if (!layer) return null;
3147
+ const findInChildren = (node) => {
3148
+ if (node.attrs.id === id || node.id() === id) {
3149
+ return node;
3150
+ }
3151
+ const children = node.getChildren?.();
3152
+ if (children) {
3153
+ for (const child of children) {
3154
+ const found = findInChildren(child);
3155
+ if (found) return found;
3156
+ }
3157
+ }
3158
+ return null;
3159
+ };
3160
+ return findInChildren(layer);
3161
+ };
3162
+ const selectedNode = findNodeById(selectedElementId);
3163
+ if (selectedNode && selectedNode.draggable()) {
3164
+ transformer.nodes([selectedNode]);
3165
+ transformer.getLayer()?.batchDraw();
3166
+ } else {
3167
+ transformer.nodes([]);
3168
+ transformer.getLayer()?.batchDraw();
3169
+ }
3170
+ }, [selectedElementId, stageRef, isLocked, updateTrigger]);
3171
+ const handleTransform = (e) => {
3172
+ if (!onTransform || !selectedElementId) return;
3173
+ const node = e.target;
3174
+ const scaleX = node.scaleX();
3175
+ const scaleY = node.scaleY();
3176
+ const updates = {
3177
+ position: {
3178
+ x: node.x(),
3179
+ y: node.y()
3180
+ },
3181
+ size: {
3182
+ width: Math.max(5, node.width() * scaleX),
3183
+ height: Math.max(5, node.height() * scaleY)
3184
+ },
3185
+ rotation: node.rotation()
3186
+ };
3187
+ onTransform(selectedElementId, updates);
3188
+ };
3189
+ const handleTransformEnd = (e) => {
3190
+ if (!onTransformEnd || !selectedElementId) return;
3191
+ const node = e.target;
3192
+ const scaleX = node.scaleX();
3193
+ const scaleY = node.scaleY();
3194
+ node.scaleX(1);
3195
+ node.scaleY(1);
3196
+ const updates = {
3197
+ position: {
3198
+ x: node.x(),
3199
+ y: node.y()
3200
+ },
3201
+ size: {
3202
+ width: Math.max(5, node.width() * scaleX),
3203
+ height: Math.max(5, node.height() * scaleY)
3204
+ },
3205
+ rotation: node.rotation()
3206
+ };
3207
+ onTransformEnd(selectedElementId, updates);
3208
+ };
3209
+ if (isLocked || !selectedElementId) {
3210
+ return null;
3211
+ }
3212
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
3213
+ import_react_konva5.Transformer,
3214
+ {
3215
+ ref: transformerRef,
3216
+ rotateEnabled: true,
3217
+ rotationSnaps: [0, 90, 180, 270],
3218
+ keepRatio: false,
3219
+ anchorCornerRadius: 2,
3220
+ flipEnabled: false,
3221
+ enabledAnchors: [
3222
+ "top-left",
3223
+ "top-right",
3224
+ "bottom-left",
3225
+ "bottom-right",
3226
+ "middle-left",
3227
+ "middle-right",
3228
+ "top-center",
3229
+ "bottom-center"
3230
+ ],
3231
+ boundBoxFunc: (oldBox, newBox) => {
3232
+ if (newBox.width < 20 || newBox.height < 20) {
3233
+ return oldBox;
3234
+ }
3235
+ return newBox;
3236
+ },
3237
+ onTransform: handleTransform,
3238
+ onTransformEnd: handleTransformEnd
3239
+ }
3240
+ );
3241
+ };
3242
+
3243
+ // src/components/Canvas.tsx
3244
+ var import_jsx_runtime24 = require("react/jsx-runtime");
3245
+ var Canvas = ({
3246
+ canvasSize,
3247
+ elements,
3248
+ selectedElementId,
3249
+ registry,
3250
+ mode,
3251
+ readonly = false,
3252
+ enableSnapGuides = true,
3253
+ enablePanZoom = true,
3254
+ backgroundImageUrl,
3255
+ hideElements = false,
3256
+ onSelectElement,
3257
+ onTransformElement,
3258
+ style,
3259
+ className = ""
3260
+ }) => {
3261
+ const stageRef = (0, import_react9.useRef)(null);
3262
+ const containerRef = (0, import_react9.useRef)(null);
3263
+ const [snapGuides, setSnapGuides] = (0, import_react9.useState)({ vertical: [], horizontal: [] });
3264
+ const [stageScale, setStageScale] = (0, import_react9.useState)(1);
3265
+ const [stagePosition, setStagePosition] = (0, import_react9.useState)({ x: 0, y: 0 });
3266
+ const isPanning = (0, import_react9.useRef)(false);
3267
+ const [containerSize, setContainerSize] = (0, import_react9.useState)({ width: 800, height: 600 });
3268
+ const [backgroundImage, setBackgroundImage] = (0, import_react9.useState)(null);
3269
+ const [transformerUpdateTrigger, setTransformerUpdateTrigger] = (0, import_react9.useState)(0);
3270
+ import_react9.default.useEffect(() => {
3271
+ if (!backgroundImageUrl) {
3272
+ setBackgroundImage(null);
3273
+ return;
3274
+ }
3275
+ const img = new window.Image();
3276
+ img.crossOrigin = "anonymous";
3277
+ img.onload = () => {
3278
+ setBackgroundImage(img);
3279
+ };
3280
+ img.onerror = (err) => {
3281
+ console.error("Failed to load canvas background image:", err);
3282
+ setBackgroundImage(null);
3283
+ };
3284
+ img.src = backgroundImageUrl;
3285
+ return () => {
3286
+ img.onload = null;
3287
+ img.onerror = null;
3288
+ };
3289
+ }, [backgroundImageUrl]);
3290
+ import_react9.default.useEffect(() => {
3291
+ const updateSize = () => {
3292
+ if (containerRef.current) {
3293
+ const rect = containerRef.current.getBoundingClientRect();
3294
+ setContainerSize({
3295
+ width: rect.width,
3296
+ height: rect.height
3297
+ });
3298
+ }
3299
+ };
3300
+ updateSize();
3301
+ const resizeObserver = new ResizeObserver(updateSize);
3302
+ if (containerRef.current) {
3303
+ resizeObserver.observe(containerRef.current);
3304
+ }
3305
+ return () => {
3306
+ resizeObserver.disconnect();
3307
+ };
3308
+ }, []);
3309
+ const stageSize = import_react9.default.useMemo(() => {
3310
+ const width = Math.max(containerSize.width, canvasSize.width);
3311
+ const height = Math.max(containerSize.height, canvasSize.height);
3312
+ return { width, height };
3313
+ }, [containerSize, canvasSize]);
3314
+ const layerOffset = import_react9.default.useMemo(() => {
3315
+ return {
3316
+ x: Math.max(0, (stageSize.width - canvasSize.width) / 2),
3317
+ y: Math.max(0, (stageSize.height - canvasSize.height) / 2)
3318
+ };
3319
+ }, [stageSize, canvasSize]);
3320
+ const handleStageClick = (0, import_react9.useCallback)(
3321
+ (e) => {
3322
+ if (e.target === e.target.getStage()) {
3323
+ onSelectElement(null);
3324
+ }
3325
+ },
3326
+ [onSelectElement]
3327
+ );
3328
+ const onClearSnapGuides = (0, import_react9.useCallback)(() => {
3329
+ setSnapGuides({ vertical: [], horizontal: [] });
3330
+ }, []);
3331
+ const onTransformerUpdate = (0, import_react9.useCallback)(() => {
3332
+ setTransformerUpdateTrigger((prev) => prev + 1);
3333
+ }, []);
3334
+ const handleWheel = (0, import_react9.useCallback)(
3335
+ (e) => {
3336
+ if (!enablePanZoom) return;
3337
+ e.evt.preventDefault();
3338
+ const stage = stageRef.current;
3339
+ if (!stage) return;
3340
+ const oldScale = stage.scaleX();
3341
+ const pointer = stage.getPointerPosition();
3342
+ const mousePointTo = {
3343
+ x: (pointer.x - stage.x()) / oldScale,
3344
+ y: (pointer.y - stage.y()) / oldScale
3345
+ };
3346
+ const scaleBy = 1.05;
3347
+ const direction = e.evt.deltaY > 0 ? -1 : 1;
3348
+ let newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
3349
+ newScale = Math.max(0.1, Math.min(5, newScale));
3350
+ const newPos = {
3351
+ x: pointer.x - mousePointTo.x * newScale,
3352
+ y: pointer.y - mousePointTo.y * newScale
3353
+ };
3354
+ setStageScale(newScale);
3355
+ setStagePosition(newPos);
3356
+ },
3357
+ [enablePanZoom]
3358
+ );
3359
+ const handleMouseDown = (0, import_react9.useCallback)(
3360
+ (e) => {
3361
+ if (!enablePanZoom) return;
3362
+ if (e.evt.button === 1 || e.evt.button === 0 && e.evt.shiftKey) {
3363
+ e.evt.preventDefault();
3364
+ isPanning.current = true;
3365
+ }
3366
+ },
3367
+ [enablePanZoom]
3368
+ );
3369
+ const handleMouseMove = (0, import_react9.useCallback)(
3370
+ (e) => {
3371
+ if (!enablePanZoom || !isPanning.current) return;
3372
+ e.evt.preventDefault();
3373
+ const stage = stageRef.current;
3374
+ if (!stage) return;
3375
+ const newPos = {
3376
+ x: stagePosition.x + e.evt.movementX,
3377
+ y: stagePosition.y + e.evt.movementY
3378
+ };
3379
+ setStagePosition(newPos);
3380
+ },
3381
+ [enablePanZoom, stagePosition]
3382
+ );
3383
+ const handleMouseUp = (0, import_react9.useCallback)(() => {
3384
+ isPanning.current = false;
3385
+ }, []);
3386
+ import_react9.default.useEffect(() => {
3387
+ const stage = stageRef.current;
3388
+ if (stage) {
3389
+ stage.scale({ x: stageScale, y: stageScale });
3390
+ stage.position(stagePosition);
3391
+ stage.batchDraw();
3392
+ }
3393
+ }, [stageScale, stagePosition]);
3394
+ const renderElement = (0, import_react9.useCallback)(
3395
+ (element) => {
3396
+ const isSelected = selectedElementId === element.id;
3397
+ const renderer = registry.get(element.type);
3398
+ if (!renderer) {
3399
+ console.warn(`No renderer found for element type: ${element.type}`);
3400
+ return null;
3401
+ }
3402
+ const RendererComponent = renderer.renderComponent;
3403
+ if (!RendererComponent) {
3404
+ console.warn(`No renderComponent found for element type: ${element.type}`);
3405
+ return null;
3406
+ }
3407
+ const commonProps = {
3408
+ element,
3409
+ isSelected,
3410
+ onSelect: () => !readonly && onSelectElement(element.id),
3411
+ onTransform: (updates) => !readonly && onTransformElement(element.id, updates),
3412
+ // Snapping callbacks
3413
+ allElements: elements,
3414
+ canvasSize,
3415
+ onSnapGuides: enableSnapGuides ? setSnapGuides : void 0,
3416
+ onClearSnapGuides: enableSnapGuides ? onClearSnapGuides : void 0,
3417
+ // Pass element ID for centralized transformer lookup
3418
+ elementId: element.id,
3419
+ // Disable individual transformers (centralized transformer will handle it)
3420
+ disableTransformer: true,
3421
+ // Callback to notify when node structure changes (e.g., image loads)
3422
+ onNodeUpdate: onTransformerUpdate,
3423
+ // Pass mode context to renderers (e.g., cardData for TemplatedText)
3424
+ ...mode?.context || {}
3425
+ };
3426
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(RendererComponent, { ...commonProps }, element.id);
3427
+ },
3428
+ [
3429
+ selectedElementId,
3430
+ registry,
3431
+ readonly,
3432
+ onSelectElement,
3433
+ onTransformElement,
3434
+ elements,
3435
+ canvasSize,
3436
+ enableSnapGuides,
3437
+ onClearSnapGuides,
3438
+ onTransformerUpdate,
3439
+ mode
3440
+ ]
3441
+ );
3442
+ const sortedElements = [...elements].sort((a, b) => a.zIndex - b.zIndex);
3443
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3444
+ "div",
3445
+ {
3446
+ className: `flex flex-1 overflow-hidden items-center justify-center ${className}`,
3447
+ style,
3448
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
3449
+ "div",
3450
+ {
3451
+ ref: containerRef,
3452
+ className: "flex relative w-full h-full items-center justify-center overflow-hidden",
3453
+ children: [
3454
+ enablePanZoom && stageScale !== 1 && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
3455
+ "div",
3456
+ {
3457
+ style: {
3458
+ position: "absolute",
3459
+ top: 10,
3460
+ right: 10,
3461
+ zIndex: 10,
3462
+ background: "rgba(0, 0, 0, 0.7)",
3463
+ color: "white",
3464
+ padding: "4px 12px",
3465
+ borderRadius: "4px",
3466
+ fontSize: "12px",
3467
+ fontWeight: 500,
3468
+ pointerEvents: "none"
3469
+ },
3470
+ children: [
3471
+ Math.round(stageScale * 100),
3472
+ "%"
3473
+ ]
3474
+ }
3475
+ ),
3476
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3477
+ import_react_konva6.Stage,
3478
+ {
3479
+ className: "w-full h-full",
3480
+ ref: stageRef,
3481
+ width: stageSize.width,
3482
+ height: stageSize.height,
3483
+ onClick: handleStageClick,
3484
+ onTap: handleStageClick,
3485
+ onWheel: handleWheel,
3486
+ onMouseDown: handleMouseDown,
3487
+ onMouseMove: handleMouseMove,
3488
+ onMouseUp: handleMouseUp,
3489
+ style: {
3490
+ boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
3491
+ cursor: isPanning.current ? "grabbing" : "default"
3492
+ },
3493
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_react_konva6.Layer, { x: layerOffset.x, y: layerOffset.y, listening: true, children: [
3494
+ backgroundImage ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3495
+ import_react_konva6.Image,
3496
+ {
3497
+ x: 0,
3498
+ y: 0,
3499
+ width: canvasSize.width,
3500
+ height: canvasSize.height,
3501
+ image: backgroundImage,
3502
+ listening: false
3503
+ }
3504
+ ) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3505
+ import_react_konva6.Rect,
3506
+ {
3507
+ x: 0,
3508
+ y: 0,
3509
+ width: canvasSize.width,
3510
+ height: canvasSize.height,
3511
+ fill: mode?.backgroundColor || "#ffffff",
3512
+ listening: false
3513
+ }
3514
+ ),
3515
+ !hideElements && sortedElements.map(renderElement),
3516
+ enableSnapGuides && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3517
+ SnapGuides,
3518
+ {
3519
+ verticalGuides: snapGuides.vertical,
3520
+ horizontalGuides: snapGuides.horizontal,
3521
+ canvasSize
3522
+ }
3523
+ ),
3524
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
3525
+ CentralizedTransformer,
3526
+ {
3527
+ selectedElementId,
3528
+ stageRef,
3529
+ isLocked: readonly,
3530
+ updateTrigger: transformerUpdateTrigger,
3531
+ onTransform: (elementId, updates) => {
3532
+ if (!enableSnapGuides) return;
3533
+ const element = elements.find((el) => el.id === elementId);
3534
+ if (!element) return;
3535
+ const tempElement = {
3536
+ ...element,
3537
+ position: updates.position || element.position,
3538
+ size: updates.size || element.size,
3539
+ rotation: updates.rotation ?? element.rotation
3540
+ };
3541
+ const snapResult = getSnappingPosition(
3542
+ tempElement,
3543
+ tempElement.position.x,
3544
+ tempElement.position.y,
3545
+ elements.filter((el) => el.id !== elementId),
3546
+ {
3547
+ threshold: 5,
3548
+ snapToElements: true,
3549
+ snapToCanvas: true,
3550
+ canvasSize
3551
+ }
3552
+ );
3553
+ setSnapGuides({
3554
+ vertical: snapResult.verticalGuides,
3555
+ horizontal: snapResult.horizontalGuides
3556
+ });
3557
+ },
3558
+ onTransformEnd: (elementId, updates) => {
3559
+ onTransformElement(elementId, updates);
3560
+ setSnapGuides({ vertical: [], horizontal: [] });
3561
+ }
3562
+ }
3563
+ )
3564
+ ] })
3565
+ }
3566
+ )
3567
+ ]
3568
+ }
3569
+ )
3570
+ }
3571
+ );
3572
+ };
3573
+
3574
+ // src/components/VisualEditorWorkspace.tsx
3575
+ init_editorUtils();
3576
+
3577
+ // src/components/Toolbar.tsx
3578
+ var import_lucide_react8 = require("lucide-react");
3579
+ init_editorUtils();
3580
+ var import_jsx_runtime25 = require("react/jsx-runtime");
3581
+ var Toolbar = ({
3582
+ api,
3583
+ elementRenderers,
3584
+ canvasSize,
3585
+ config,
3586
+ style,
3587
+ className
3588
+ }) => {
3589
+ const selectedElement = api.getSelectedElement();
3590
+ const {
3591
+ showElementTools = true,
3592
+ hiddenElementTypes = [],
3593
+ toolsStart = [],
3594
+ toolsEnd = [],
3595
+ toolsStartClassName = "",
3596
+ toolsEndClassName = "",
3597
+ customTools = []
3598
+ // Backwards compatibility - defaults to end
3599
+ } = config || {};
3600
+ const endTools = [...toolsEnd, ...customTools];
3601
+ const visibleRenderers = showElementTools ? elementRenderers.filter((renderer) => !hiddenElementTypes.includes(renderer.type)) : [];
3602
+ const handleCreateElement = (renderer) => {
3603
+ const newElement = createElement(renderer.type, renderer.defaultProps, {
3604
+ position: { x: canvasSize.width / 2, y: canvasSize.height / 2 },
3605
+ // Center of canvas
3606
+ size: {
3607
+ width: renderer.defaultSize?.width || 100,
3608
+ height: renderer.defaultSize?.height || 100
3609
+ },
3610
+ zIndex: api.getAllElements().length
3611
+ // Top of stack
3612
+ });
3613
+ api.addElement(newElement);
3614
+ api.selectElement(newElement.id);
3615
+ };
3616
+ const isDisabled = (disabled) => {
3617
+ if (typeof disabled === "function") {
3618
+ return disabled(api);
3619
+ }
3620
+ return disabled || false;
3621
+ };
3622
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
3623
+ "div",
3624
+ {
3625
+ className: `flex flex-col justify-start items-center bg-popover gap-2 p-2 ${className}`,
3626
+ style,
3627
+ children: [
3628
+ toolsStart.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
3629
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: `flex flex-col gap-1 ${toolsStartClassName}`, children: toolsStart.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(CustomActionRenderer_default, { action: tool, api, layout: "vertical" }, tool.id)) }),
3630
+ (visibleRenderers.length > 0 || endTools.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Separator, { orientation: "horizontal" })
3631
+ ] }),
3632
+ visibleRenderers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(import_jsx_runtime25.Fragment, { children: [
3633
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: "flex flex-col gap-1", children: visibleRenderers.map((renderer) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
3634
+ TooltipButton,
3635
+ {
3636
+ variant: "outline",
3637
+ size: "sm",
3638
+ onClick: () => handleCreateElement(renderer),
3639
+ className: "icon-button",
3640
+ tooltip: `Add ${renderer.displayName}`,
3641
+ tooltipDelay: 500,
3642
+ tooltipSide: "right",
3643
+ children: renderer.icon || /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(import_lucide_react8.Plus, { className: "h-4 w-4" })
3644
+ },
3645
+ renderer.type
3646
+ )) }),
3647
+ endTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Separator, { orientation: "horizontal" })
3648
+ ] }),
3649
+ endTools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: `flex flex-col gap-1 ${toolsEndClassName}`, children: endTools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(CustomActionRenderer_default, { action: tool, api, layout: "vertical" }, tool.id)) })
3650
+ ]
3651
+ }
3652
+ );
3653
+ };
3654
+
3655
+ // src/components/VisualEditorWorkspace.tsx
3656
+ var import_jsx_runtime26 = require("react/jsx-runtime");
3657
+ var VisualEditorWorkspace = ({
3658
+ mode,
3659
+ initialData,
3660
+ width,
3661
+ height,
3662
+ readonly = false,
3663
+ onChange,
3664
+ customElements = [],
3665
+ showTopbar = true,
3666
+ showToolbar = true,
3667
+ showInspector = true,
3668
+ showLayers = true,
3669
+ showAssetPicker = true,
3670
+ showCanvas = true,
3671
+ enableSnapGuides = true,
3672
+ enablePanZoom = true,
3673
+ backgroundImageUrl,
3674
+ hideElements = false,
3675
+ className = "",
3676
+ apiRef
3677
+ }) => {
3678
+ const elementList = import_react10.default.useMemo(() => {
3679
+ return [...defaultElements, ...mode?.registeredElements || [], ...customElements];
3680
+ }, [mode, customElements]);
3681
+ const registry = useElementRegistry(elementList);
3682
+ const { state, api, undo, redo, canUndo, canRedo, setCanvasSize } = useEditorState(mode || null);
3683
+ const clipboardRef = import_react10.default.useRef(null);
3684
+ const [snapGuidesEnabled, setSnapGuidesEnabled] = import_react10.default.useState(enableSnapGuides);
3685
+ const [backgroundColor, setBackgroundColor] = import_react10.default.useState(
3686
+ mode?.backgroundColor || "#1a1a1a"
3687
+ );
3688
+ const [backgroundImage, setBackgroundImage] = import_react10.default.useState(
3689
+ mode?.backgroundImage || ""
3690
+ );
3691
+ import_react10.default.useEffect(() => {
3692
+ setSnapGuidesEnabled(enableSnapGuides);
3693
+ }, [enableSnapGuides]);
3694
+ import_react10.default.useEffect(() => {
3695
+ if (mode?.backgroundColor) {
3696
+ setBackgroundColor(mode.backgroundColor);
3697
+ }
3698
+ }, [mode?.backgroundColor]);
3699
+ import_react10.default.useEffect(() => {
3700
+ setBackgroundImage(mode?.backgroundImage || "");
3701
+ }, [mode?.backgroundImage]);
3702
+ const canvasSize = state.canvasSize;
3703
+ (0, import_react10.useImperativeHandle)(apiRef, () => api, [api]);
3704
+ import_react10.default.useEffect(() => {
3705
+ if (initialData && initialData.elements && initialData.elements.length > 0) {
3706
+ api.loadElements(initialData.elements);
3707
+ if (initialData.width && initialData.height) {
3708
+ setCanvasSize(initialData.width, initialData.height);
3709
+ }
3710
+ }
3711
+ }, []);
3712
+ const availableRenderers = Array.from(registry.getAll().values());
3713
+ const selectedElement = state.selectedElementId ? state.elements.find((el) => el.id === state.selectedElementId) || null : null;
3714
+ const selectedRenderer = selectedElement ? registry.get(selectedElement.type) || null : null;
3715
+ const handleExport = (0, import_react10.useCallback)(() => {
3716
+ const data = {
3717
+ width: canvasSize.width,
3718
+ height: canvasSize.height,
3719
+ elements: state.elements,
3720
+ metadata: {
3721
+ version: "1.0",
3722
+ mode: mode?.name || "default",
3723
+ created: (/* @__PURE__ */ new Date()).toISOString()
3724
+ }
3725
+ };
3726
+ const json = exportToJSON(data);
3727
+ const blob = new Blob([json], { type: "application/json" });
3728
+ const url = URL.createObjectURL(blob);
3729
+ const a = document.createElement("a");
3730
+ a.href = url;
3731
+ a.download = `canvas-${Date.now()}.json`;
3732
+ a.click();
3733
+ URL.revokeObjectURL(url);
3734
+ }, [state.elements, canvasSize, mode]);
3735
+ const handleImport = (0, import_react10.useCallback)(() => {
3736
+ const input = document.createElement("input");
3737
+ input.type = "file";
3738
+ input.accept = ".json";
3739
+ input.onchange = async (e) => {
3740
+ const file = e.target.files?.[0];
3741
+ if (!file) return;
3742
+ try {
3743
+ const text = await file.text();
3744
+ const imported = importFromJSON(text);
3745
+ if (imported) {
3746
+ state.elements.forEach((el) => api.removeElement(el.id));
3747
+ imported.elements.forEach((el) => api.addElement(el));
3748
+ if (imported.width && imported.height) {
3749
+ setCanvasSize(imported.width, imported.height);
3750
+ }
3751
+ }
3752
+ } catch (error) {
3753
+ console.error("Failed to import:", error);
3754
+ alert(`Failed to import: ${error.message}`);
3755
+ }
3756
+ };
3757
+ input.click();
3758
+ }, [state.elements, api]);
3759
+ const onChangeRef = import_react10.default.useRef(onChange);
3760
+ import_react10.default.useEffect(() => {
3761
+ onChangeRef.current = onChange;
3762
+ }, [onChange]);
3763
+ import_react10.default.useEffect(() => {
3764
+ if (onChangeRef.current) {
3765
+ onChangeRef.current({
3766
+ width: canvasSize.width,
3767
+ height: canvasSize.height,
3768
+ elements: state.elements,
3769
+ metadata: {
3770
+ backgroundColor,
3771
+ backgroundImage
3772
+ }
3773
+ });
3774
+ }
3775
+ }, [state.elements, canvasSize, backgroundColor, backgroundImage]);
3776
+ import_react10.default.useEffect(() => {
3777
+ const handleKeyDown = (e) => {
3778
+ if (readonly) return;
3779
+ if (e.ctrlKey && e.key === "z" && !e.shiftKey) {
3780
+ e.preventDefault();
3781
+ if (canUndo) undo();
3782
+ } else if (e.ctrlKey && e.key === "y" || e.ctrlKey && e.shiftKey && e.key === "Z") {
3783
+ e.preventDefault();
3784
+ if (canRedo) redo();
3785
+ } else if (e.key === "Delete" && selectedElement) {
3786
+ e.preventDefault();
3787
+ api.removeElement(selectedElement.id);
3788
+ } else if (e.key === "Escape") {
3789
+ e.preventDefault();
3790
+ api.selectElement(null);
3791
+ } else if (e.ctrlKey && e.key === "c" && selectedElement) {
3792
+ e.preventDefault();
3793
+ const copied = api.copyElement();
3794
+ if (copied) {
3795
+ clipboardRef.current = copied;
3796
+ }
3797
+ } else if (e.ctrlKey && e.key === "v" && clipboardRef.current) {
3798
+ e.preventDefault();
3799
+ api.pasteElement(clipboardRef.current);
3800
+ } else if (e.ctrlKey && e.key === "d" && selectedElement) {
3801
+ e.preventDefault();
3802
+ api.duplicateElement();
3803
+ }
3804
+ const isInputFocused = () => {
3805
+ const active = document.activeElement;
3806
+ if (!active) return false;
3807
+ if (active === document.body) return false;
3808
+ if (active.isContentEditable) return true;
3809
+ const tag = active.tagName?.toLowerCase();
3810
+ if (tag === "input" || tag === "textarea" || tag === "select") return true;
3811
+ const role = active.getAttribute ? active.getAttribute("role") : null;
3812
+ if (role === "textbox") return true;
3813
+ return false;
3814
+ };
3815
+ if (selectedElement && !isInputFocused()) {
3816
+ if (e.key === "ArrowUp") {
3817
+ e.preventDefault();
3818
+ api.moveElement(selectedElement.id, 0, -1);
3819
+ } else if (e.key === "ArrowDown") {
3820
+ e.preventDefault();
3821
+ api.moveElement(selectedElement.id, 0, 1);
3822
+ } else if (e.key === "ArrowLeft") {
3823
+ e.preventDefault();
3824
+ api.moveElement(selectedElement.id, -1, 0);
3825
+ } else if (e.key === "ArrowRight") {
3826
+ e.preventDefault();
3827
+ api.moveElement(selectedElement.id, 1, 0);
3828
+ }
3829
+ }
3830
+ };
3831
+ window.addEventListener("keydown", handleKeyDown);
3832
+ return () => window.removeEventListener("keydown", handleKeyDown);
3833
+ }, [canUndo, canRedo, undo, redo, selectedElement, api, readonly]);
3834
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: `flex flex-col ${className}`, children: [
3835
+ showTopbar && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3836
+ Topbar,
3837
+ {
3838
+ api,
3839
+ canvasSize,
3840
+ canUndo,
3841
+ canRedo,
3842
+ onUndo: undo,
3843
+ onRedo: redo,
3844
+ onExport: handleExport,
3845
+ onImport: handleImport,
3846
+ setCanvasSize,
3847
+ backgroundColor,
3848
+ setBackgroundColor,
3849
+ backgroundImage,
3850
+ setBackgroundImage,
3851
+ imageUrls: mode?.context?.imageUrls,
3852
+ clipboardRef,
3853
+ enableSnapGuides: snapGuidesEnabled,
3854
+ onSnapGuidesChange: setSnapGuidesEnabled,
3855
+ config: mode?.topbarConfig
3856
+ }
3857
+ ),
3858
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex w-full h-full overflow-hidden", children: [
3859
+ showToolbar && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3860
+ Toolbar,
3861
+ {
3862
+ api,
3863
+ elementRenderers: availableRenderers,
3864
+ canvasSize,
3865
+ config: mode?.toolbarConfig
3866
+ }
3867
+ ),
3868
+ showCanvas && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3869
+ Canvas,
3870
+ {
3871
+ canvasSize,
3872
+ elements: state.elements,
3873
+ selectedElementId: state.selectedElementId,
3874
+ registry,
3875
+ mode: mode ? { ...mode, backgroundColor } : void 0,
3876
+ readonly,
3877
+ enableSnapGuides: snapGuidesEnabled,
3878
+ enablePanZoom,
3879
+ backgroundImageUrl: backgroundImage && mode?.context?.imageUrls ? mode.context.imageUrls.get(backgroundImage) : void 0,
3880
+ hideElements,
3881
+ onSelectElement: (id) => api.selectElement(id),
3882
+ onTransformElement: (id, updates) => api.updateElement(id, updates)
3883
+ }
3884
+ ),
3885
+ showLayers && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3886
+ LayersPanel,
3887
+ {
3888
+ elements: state.elements,
3889
+ selectedElementId: state.selectedElementId,
3890
+ api,
3891
+ elementRenderers: registry.getMap(),
3892
+ className: "w-64"
3893
+ }
3894
+ ),
3895
+ showInspector && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "w-80 h-full border-l bg-background flex flex-col", children: [
3896
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "flex-1 min-h-0 overflow-auto", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3897
+ Inspector,
3898
+ {
3899
+ selectedElement,
3900
+ elementRenderer: selectedRenderer,
3901
+ api,
3902
+ mode,
3903
+ canvasSize,
3904
+ setCanvasSize
3905
+ }
3906
+ ) }),
3907
+ showAssetPicker && mode?.assetPickerComponent && (!mode.assetPickerPosition || mode.assetPickerPosition === "bottom") && import_react10.default.createElement(mode.assetPickerComponent, {
3908
+ ...mode.assetPickerProps,
3909
+ api,
3910
+ className: mode.assetPickerHeight || "h-[40%]"
3911
+ })
3912
+ ] }),
3913
+ showAssetPicker && mode?.assetPickerComponent && mode.assetPickerPosition === "right" && import_react10.default.createElement(mode.assetPickerComponent, {
3914
+ ...mode.assetPickerProps,
3915
+ api,
3916
+ className: `border-l ${mode.assetPickerHeight || "w-80"}`
3917
+ })
3918
+ ] })
3919
+ ] });
3920
+ };
3921
+
3922
+ // src/components/AssetPicker.tsx
3923
+ var import_react11 = __toESM(require("react"));
3924
+ var import_lucide_react9 = require("lucide-react");
3925
+ var import_jsx_runtime27 = require("react/jsx-runtime");
3926
+ var AssetPicker = ({
3927
+ assets,
3928
+ onAssetSelect,
3929
+ renderAsset,
3930
+ className = "",
3931
+ title = "Assets"
3932
+ }) => {
3933
+ const [searchQuery, setSearchQuery] = import_react11.default.useState("");
3934
+ const filteredAssets = import_react11.default.useMemo(() => {
3935
+ if (!searchQuery) return assets;
3936
+ const query = searchQuery.toLowerCase();
3937
+ return assets.filter((asset) => asset.name.toLowerCase().includes(query));
3938
+ }, [assets, searchQuery]);
3939
+ const defaultRenderAsset = import_react11.default.useCallback(
3940
+ (asset) => {
3941
+ const isImage = asset.name.match(/\.(jpg|jpeg|png|gif|svg|webp)$/i);
3942
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3943
+ "div",
3944
+ {
3945
+ className: "flex flex-col items-center p-2 border rounded hover:bg-accent cursor-pointer transition-colors",
3946
+ onClick: () => onAssetSelect?.(asset.path),
3947
+ title: asset.name,
3948
+ children: [
3949
+ isImage ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "w-16 h-16 flex items-center justify-center bg-muted rounded mb-1", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3950
+ "img",
3951
+ {
3952
+ src: `asset:///${asset.path}`,
3953
+ alt: asset.name,
3954
+ className: "max-w-full max-h-full object-contain",
3955
+ onError: (e) => {
3956
+ e.currentTarget.style.display = "none";
3957
+ e.currentTarget.parentElement.innerHTML = `
3958
+ <div class="text-xs text-muted-foreground">Image</div>
3959
+ `;
3960
+ }
3961
+ }
3962
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "w-16 h-16 flex items-center justify-center bg-muted rounded mb-1", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "text-xs text-muted-foreground", children: "File" }) }),
3963
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "text-xs text-center truncate w-full", children: asset.name })
3964
+ ]
3965
+ },
3966
+ asset.path
3967
+ );
3968
+ },
3969
+ [onAssetSelect]
3970
+ );
3971
+ const assetRenderer = renderAsset || defaultRenderAsset;
3972
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: `flex flex-col bg-background border-t ${className || "h-full"}`, children: [
3973
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "p-3 border-b", children: [
3974
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("h3", { className: "text-sm font-semibold mb-2", children: title }),
3975
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "relative", children: [
3976
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_lucide_react9.Search, { className: "absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" }),
3977
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3978
+ Input,
3979
+ {
3980
+ type: "text",
3981
+ placeholder: "Search assets...",
3982
+ value: searchQuery,
3983
+ onChange: (e) => setSearchQuery(e.target.value),
3984
+ className: "pl-8 h-8 text-sm"
3985
+ }
3986
+ )
3987
+ ] })
3988
+ ] }),
3989
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(ScrollArea, { className: "flex-1", children: filteredAssets.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "grid grid-cols-3 gap-2 p-3", children: filteredAssets.map((asset) => assetRenderer(asset)) }) : /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "flex items-center justify-center h-32 text-sm text-muted-foreground", children: searchQuery ? "No assets found" : "No assets available" }) })
3990
+ ] });
3991
+ };
3992
+
3993
+ // src/index.ts
3994
+ init_editorUtils();
3995
+ // Annotate the CommonJS export names for ESM import in node:
3996
+ 0 && (module.exports = {
3997
+ AssetPicker,
3998
+ ElementRegistry,
3999
+ ImageElementRenderer,
4000
+ Inspector,
4001
+ LayersPanel,
4002
+ TextElementRenderer,
4003
+ VisualEditor,
4004
+ VisualEditorWorkspace,
4005
+ bringToFront,
4006
+ checkOverlap,
4007
+ clamp,
4008
+ cloneElement,
4009
+ constrainToCanvas,
4010
+ createElement,
4011
+ defaultElements,
4012
+ degToRad,
4013
+ distance,
4014
+ duplicateElement,
4015
+ exportToJSON,
4016
+ generateElementId,
4017
+ getElementCenter,
4018
+ getMaxZIndex,
4019
+ getRotatedBoundingBox,
4020
+ getSnappingPosition,
4021
+ globalElementRegistry,
4022
+ imageElementRenderer,
4023
+ importFromJSON,
4024
+ isValidCanvasExport,
4025
+ isValidElement,
4026
+ pointInRect,
4027
+ radToDeg,
4028
+ renderField,
4029
+ sendToBack,
4030
+ snapPositionToGrid,
4031
+ snapToGrid,
4032
+ sortByZIndex,
4033
+ textElementRenderer,
4034
+ useEditorState,
4035
+ useElementRegistry
4036
+ });
4037
+ //# sourceMappingURL=index.js.map