@varianta/sdk 0.1.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.
@@ -0,0 +1,2354 @@
1
+ import { jsx as Z } from "react/jsx-runtime";
2
+ import { forwardRef as $, useRef as k, useEffect as L, useImperativeHandle as q, useState as S, useCallback as M } from "react";
3
+ class B {
4
+ constructor(e) {
5
+ this.animationFrameId = null, this.needsRender = !0, this.areas = [], this.contents = /* @__PURE__ */ new Map(), this.artboard = null, this.bgImageElement = null, this.loadedImages = /* @__PURE__ */ new Map(), this.loadingImages = /* @__PURE__ */ new Set(), this.zoom = 1, this.pan = { x: 0, y: 0 }, this.selectedAreaId = null, this.hoveredHandle = null, this.eventListeners = /* @__PURE__ */ new Map(), this.safeArea = null, this.safeAreaViolations = /* @__PURE__ */ new Set(), this.showAreaBorders = !0, this.areaSelectionEnabled = !0, this.renderLoop = () => {
6
+ try {
7
+ this.needsRender && (this.render(), this.needsRender = !1, this.emit("render", void 0));
8
+ } catch (i) {
9
+ console.error("Error in render loop:", i);
10
+ }
11
+ this.animationFrameId = requestAnimationFrame(this.renderLoop);
12
+ }, this.canvas = e;
13
+ const t = e.getContext("2d");
14
+ if (!t)
15
+ throw new Error("Failed to get 2D context from canvas");
16
+ this.ctx = t;
17
+ }
18
+ /**
19
+ * Start the render loop
20
+ */
21
+ start() {
22
+ this.animationFrameId === null && this.renderLoop();
23
+ }
24
+ /**
25
+ * Stop the render loop
26
+ */
27
+ stop() {
28
+ this.animationFrameId !== null && (cancelAnimationFrame(this.animationFrameId), this.animationFrameId = null);
29
+ }
30
+ /**
31
+ * Set artboard configuration
32
+ */
33
+ setArtboard(e) {
34
+ this.artboard = e, this.requestRender();
35
+ }
36
+ /**
37
+ * Get artboard configuration
38
+ */
39
+ getArtboard() {
40
+ return this.artboard;
41
+ }
42
+ /**
43
+ * Set areas to render
44
+ */
45
+ setAreas(e) {
46
+ this.areas = [...e].sort((t, i) => t.zIndex - i.zIndex), this.requestRender();
47
+ }
48
+ /**
49
+ * Get areas
50
+ */
51
+ getAreas() {
52
+ return this.areas;
53
+ }
54
+ /**
55
+ * Set area contents
56
+ */
57
+ setContents(e) {
58
+ this.contents = e, this.loadContentImages(), this.requestRender();
59
+ }
60
+ /**
61
+ * Get area contents
62
+ */
63
+ getContents() {
64
+ return this.contents;
65
+ }
66
+ /**
67
+ * Update content for a single area
68
+ */
69
+ updateContent(e, t) {
70
+ this.contents.set(e, t), t.type === "image" && this.loadContentImages(), this.requestRender(), this.emit("content:change", { areaId: e, content: t });
71
+ }
72
+ /**
73
+ * Remove content for an area
74
+ */
75
+ removeContent(e) {
76
+ this.contents.delete(e), this.loadedImages.delete(e), this.requestRender();
77
+ }
78
+ /**
79
+ * Set background image
80
+ */
81
+ setBackgroundImage(e) {
82
+ if (this.backgroundImage = e, e?.url && e.url !== this.bgImageElement?.src) {
83
+ const t = new Image();
84
+ t.crossOrigin = "anonymous", t.onload = () => {
85
+ this.bgImageElement = t, this.requestRender();
86
+ }, t.onerror = () => {
87
+ this.bgImageElement = null, console.error("Failed to load background image:", e.url), this.requestRender();
88
+ }, t.src = e.url;
89
+ } else e?.url || (this.bgImageElement = null);
90
+ this.requestRender();
91
+ }
92
+ /**
93
+ * Set zoom level
94
+ */
95
+ setZoom(e) {
96
+ this.zoom = Math.max(0.1, Math.min(5, e)), this.requestRender();
97
+ }
98
+ /**
99
+ * Set pan offset
100
+ */
101
+ setPan(e) {
102
+ this.pan = e, this.requestRender();
103
+ }
104
+ /**
105
+ * Get zoom level
106
+ */
107
+ getZoom() {
108
+ return this.zoom;
109
+ }
110
+ /**
111
+ * Get pan offset
112
+ */
113
+ getPan() {
114
+ return this.pan;
115
+ }
116
+ /**
117
+ * Set whether area borders are shown
118
+ */
119
+ setShowAreaBorders(e) {
120
+ this.showAreaBorders = e, this.requestRender();
121
+ }
122
+ /**
123
+ * Set whether area selection is enabled
124
+ */
125
+ setAreaSelectionEnabled(e) {
126
+ this.areaSelectionEnabled = e;
127
+ }
128
+ /**
129
+ * Calculate the pan offset needed to center the artboard at a given zoom level
130
+ */
131
+ getCenteredPan(e) {
132
+ if (!this.artboard)
133
+ return null;
134
+ const { width: t, height: i } = this.artboard, n = window.devicePixelRatio || 1, r = this.canvas.width / n, s = this.canvas.height / n;
135
+ return {
136
+ x: (r / e - t) / 2,
137
+ y: (s / e - i) / 2
138
+ };
139
+ }
140
+ /**
141
+ * Request a render on the next frame
142
+ */
143
+ requestRender() {
144
+ this.needsRender = !0;
145
+ }
146
+ /**
147
+ * Fit view to show the entire artboard
148
+ */
149
+ fitToView() {
150
+ if (!this.artboard)
151
+ return;
152
+ const e = 0, { width: t, height: i } = this.artboard, n = window.devicePixelRatio || 1, r = this.canvas.width / n - e * 2, s = this.canvas.height / n - e * 2;
153
+ if (r <= 0 || s <= 0)
154
+ return;
155
+ const a = r / t, o = s / i, d = Math.min(a, o), l = this.canvas.width / n, h = this.canvas.height / n, u = (l / d - t) / 2, g = (h / d - i) / 2;
156
+ this.zoom = d, this.pan = { x: u, y: g }, this.requestRender();
157
+ }
158
+ /**
159
+ * Set the selected area
160
+ */
161
+ setSelectedArea(e) {
162
+ this.selectedAreaId !== e && (this.selectedAreaId = e, this.requestRender(), this.emit("area:select", { areaId: e }));
163
+ }
164
+ /**
165
+ * Get the selected area ID
166
+ */
167
+ getSelectedArea() {
168
+ return this.selectedAreaId;
169
+ }
170
+ /**
171
+ * Set the hovered handle for highlighting
172
+ */
173
+ setHoveredHandle(e) {
174
+ this.hoveredHandle !== e && (this.hoveredHandle = e, this.requestRender());
175
+ }
176
+ /**
177
+ * Get the currently hovered handle
178
+ */
179
+ getHoveredHandle() {
180
+ return this.hoveredHandle;
181
+ }
182
+ /**
183
+ * Convert screen coordinates to canvas coordinates
184
+ */
185
+ screenToCanvas(e, t) {
186
+ const i = window.devicePixelRatio || 1, n = e / i, r = t / i, s = n / this.zoom - this.pan.x, a = r / this.zoom - this.pan.y;
187
+ return { x: s, y: a };
188
+ }
189
+ /**
190
+ * Get content bounds for an area (in artboard coordinates)
191
+ */
192
+ getContentBounds(e) {
193
+ const t = this.areas.find((d) => d.id === e);
194
+ if (!t)
195
+ return null;
196
+ const i = this.contents.get(e);
197
+ if (!i)
198
+ return null;
199
+ const { location: n } = t, { x: r, y: s, width: a, height: o } = n;
200
+ if (i.type === "text") {
201
+ const d = i.offset || { x: 0, y: 0 }, l = i.scale ?? 1, h = i.rotation ?? 0, u = a * l, g = o * l, m = r + a / 2 + d.x, b = s + o / 2 + d.y;
202
+ return {
203
+ x: m - u / 2,
204
+ y: b - g / 2,
205
+ width: u,
206
+ height: g,
207
+ rotation: h,
208
+ centerX: m,
209
+ centerY: b
210
+ };
211
+ } else if (i.type === "image") {
212
+ const d = this.loadedImages.get(e);
213
+ if (!d)
214
+ return null;
215
+ const l = i.offset || { x: 0, y: 0 }, h = i.scale ?? 1, u = i.rotation ?? 0, g = d.naturalWidth / d.naturalHeight, m = a / o;
216
+ let b, y;
217
+ g > m ? (b = a, y = a / g) : (y = o, b = o * g);
218
+ const v = b * h, w = y * h, p = r + a / 2 + l.x, C = s + o / 2 + l.y;
219
+ return {
220
+ x: p - v / 2,
221
+ y: C - w / 2,
222
+ width: v,
223
+ height: w,
224
+ rotation: u,
225
+ centerX: p,
226
+ centerY: C
227
+ };
228
+ }
229
+ return null;
230
+ }
231
+ /**
232
+ * Hit test content at given canvas coordinates
233
+ * Returns area ID if content was hit, null otherwise
234
+ */
235
+ hitTestContent(e, t) {
236
+ if (!this.areaSelectionEnabled)
237
+ return null;
238
+ for (let i = this.areas.length - 1; i >= 0; i--) {
239
+ const n = this.areas[i], r = this.getContentBounds(n.id);
240
+ if (r && this.isPointInBounds(e, t, r))
241
+ return n.id;
242
+ }
243
+ return null;
244
+ }
245
+ /**
246
+ * Hit test area at given canvas coordinates (ignoring content)
247
+ * Returns area ID if point is inside area bounds
248
+ */
249
+ hitTestArea(e, t) {
250
+ if (!this.areaSelectionEnabled)
251
+ return null;
252
+ for (let i = this.areas.length - 1; i >= 0; i--) {
253
+ const n = this.areas[i], { x: r, y: s, width: a, height: o } = n.location;
254
+ if (e >= r && e <= r + a && t >= s && t <= s + o)
255
+ return n.id;
256
+ }
257
+ return null;
258
+ }
259
+ /**
260
+ * Test if a point is inside rotated bounds
261
+ */
262
+ isPointInBounds(e, t, i) {
263
+ const { centerX: n, centerY: r, width: s, height: a, rotation: o } = i, d = -o * Math.PI / 180, l = Math.cos(d), h = Math.sin(d), u = e - n, g = t - r, m = u * l - g * h, b = u * h + g * l;
264
+ return m >= -s / 2 && m <= s / 2 && b >= -a / 2 && b <= a / 2;
265
+ }
266
+ /**
267
+ * Get handle positions for the selected content
268
+ */
269
+ getContentHandlePositions(e) {
270
+ const t = this.areas.find((v) => v.id === e);
271
+ if (!t)
272
+ return null;
273
+ const i = this.contents.get(e);
274
+ if (!i)
275
+ return null;
276
+ const n = this.getContentBounds(e);
277
+ if (!n)
278
+ return null;
279
+ const { centerX: r, centerY: s, width: a, height: o, rotation: d } = n, l = 6 / this.zoom, h = 30 / this.zoom, u = d * Math.PI / 180, g = Math.cos(u), m = Math.sin(u), b = (v, w) => ({
280
+ x: r + v * g - w * m,
281
+ y: s + v * m + w * g
282
+ }), y = [];
283
+ if (i.type === "image" && t.imageOptions?.allowScaling || i.type === "text" && t.textOptions?.allowScaling) {
284
+ const v = a / 2, w = o / 2, p = b(-v, -w);
285
+ y.push({ type: "nw", ...p, radius: l });
286
+ const C = b(v, -w);
287
+ y.push({ type: "ne", ...C, radius: l });
288
+ const I = b(v, w);
289
+ y.push({ type: "se", ...I, radius: l });
290
+ const x = b(-v, w);
291
+ y.push({ type: "sw", ...x, radius: l });
292
+ }
293
+ if (i.type === "image" && t.imageOptions?.allowRotation || i.type === "text" && t.textOptions?.allowRotation) {
294
+ const v = b(0, -o / 2 - h);
295
+ y.push({ type: "rotate", ...v, radius: l });
296
+ }
297
+ return y;
298
+ }
299
+ /**
300
+ * Hit test handles at given canvas coordinates
301
+ */
302
+ hitTestHandle(e, t, i) {
303
+ const n = this.getContentHandlePositions(i);
304
+ if (!n)
305
+ return null;
306
+ for (const r of n) {
307
+ const s = e - r.x, a = t - r.y;
308
+ if (Math.sqrt(s * s + a * a) <= r.radius * 2)
309
+ return r.type;
310
+ }
311
+ return null;
312
+ }
313
+ /**
314
+ * Check if positioning is allowed for an area
315
+ */
316
+ isPositioningAllowed(e) {
317
+ const t = this.areas.find((n) => n.id === e);
318
+ if (!t)
319
+ return !1;
320
+ const i = this.contents.get(e);
321
+ return i ? i.type === "text" ? t.textOptions?.allowPositioning ?? !1 : t.imageOptions?.allowPositioning ?? !1 : !1;
322
+ }
323
+ /**
324
+ * Get area location (for constraint calculations)
325
+ */
326
+ getAreaLocation(e) {
327
+ const t = this.areas.find((i) => i.id === e);
328
+ return t ? t.location : null;
329
+ }
330
+ /**
331
+ * Get area by ID
332
+ */
333
+ getArea(e) {
334
+ return this.areas.find((t) => t.id === e);
335
+ }
336
+ /**
337
+ * Set safe area margins for content safety guides
338
+ */
339
+ setSafeArea(e) {
340
+ this.safeArea = e ?? null, this.requestRender();
341
+ }
342
+ /**
343
+ * Check if any area content extends beyond the safe area bounds.
344
+ * Returns array of violating area IDs.
345
+ */
346
+ checkSafeAreaViolations() {
347
+ if (!this.safeArea || !this.artboard)
348
+ return [];
349
+ const e = [], { top: t, right: i, bottom: n, left: r } = this.safeArea, s = r, a = t, o = this.artboard.width - i, d = this.artboard.height - n;
350
+ for (const l of this.areas) {
351
+ if (!this.contents.get(l.id))
352
+ continue;
353
+ const h = this.getContentBounds(l.id);
354
+ if (!h)
355
+ continue;
356
+ const { centerX: u, centerY: g, width: m, height: b, rotation: y } = h, v = y * Math.PI / 180, w = Math.abs(Math.cos(v)), p = Math.abs(Math.sin(v)), C = m * w + b * p, I = m * p + b * w, x = u - C / 2, R = g - I / 2, D = u + C / 2, U = g + I / 2;
357
+ (x < s || R < a || D > o || U > d) && e.push(l.id);
358
+ }
359
+ return this.safeAreaViolations = new Set(e), this.requestRender(), e;
360
+ }
361
+ /**
362
+ * Add event listener
363
+ */
364
+ on(e, t) {
365
+ return this.eventListeners.has(e) || this.eventListeners.set(e, /* @__PURE__ */ new Set()), this.eventListeners.get(e).add(t), () => {
366
+ this.eventListeners.get(e)?.delete(t);
367
+ };
368
+ }
369
+ /**
370
+ * Emit event
371
+ */
372
+ emit(e, t) {
373
+ const i = this.eventListeners.get(e);
374
+ if (i)
375
+ for (const n of i)
376
+ n(t);
377
+ }
378
+ /**
379
+ * Export to PNG at full resolution
380
+ */
381
+ async exportToPNG() {
382
+ if (!this.artboard)
383
+ throw new Error("No artboard configured");
384
+ const { width: e, height: t, backgroundColor: i } = this.artboard, n = document.createElement("canvas");
385
+ n.width = e, n.height = t;
386
+ const r = n.getContext("2d");
387
+ if (!r)
388
+ throw new Error("Failed to get 2D context for export");
389
+ r.fillStyle = i, r.fillRect(0, 0, e, t), this.backgroundImage?.position === "behind" && this.bgImageElement && this.drawBackgroundImageOnContext(r, e, t);
390
+ for (const s of this.areas)
391
+ this.drawAreaContentOnContext(r, s);
392
+ return this.backgroundImage?.position === "overlay" && this.bgImageElement && this.drawBackgroundImageOnContext(r, e, t), new Promise((s, a) => {
393
+ n.toBlob((o) => {
394
+ o ? s(o) : a(new Error("Failed to create PNG blob"));
395
+ }, "image/png", 1);
396
+ });
397
+ }
398
+ /**
399
+ * Load images from content data URLs
400
+ */
401
+ loadContentImages() {
402
+ for (const [e, t] of this.contents)
403
+ if (t.type === "image" && !this.loadedImages.has(e) && !this.loadingImages.has(e)) {
404
+ this.loadingImages.add(e);
405
+ const i = new Image();
406
+ i.onload = () => {
407
+ this.loadedImages.set(e, i), this.loadingImages.delete(e), this.requestRender();
408
+ }, i.onerror = () => {
409
+ this.loadingImages.delete(e), console.error("Failed to load image for area:", e);
410
+ }, i.src = t.dataUrl;
411
+ }
412
+ for (const e of this.loadedImages.keys()) {
413
+ const t = this.contents.get(e);
414
+ (!t || t.type !== "image") && this.loadedImages.delete(e);
415
+ }
416
+ }
417
+ /**
418
+ * Render the canvas
419
+ */
420
+ render() {
421
+ const { ctx: e, canvas: t } = this;
422
+ if (e.fillStyle = "#f3f4f6", e.fillRect(0, 0, t.width, t.height), !this.artboard)
423
+ return;
424
+ e.save();
425
+ const i = window.devicePixelRatio || 1;
426
+ e.scale(i, i), e.scale(this.zoom, this.zoom), e.translate(this.pan.x, this.pan.y);
427
+ const { width: n, height: r, backgroundColor: s } = this.artboard;
428
+ e.fillStyle = s, e.fillRect(0, 0, n, r), e.strokeStyle = "#d1d5db", e.lineWidth = 1 / this.zoom, e.strokeRect(0, 0, n, r), this.backgroundImage?.position === "behind" && this.renderBackgroundImage();
429
+ for (const a of this.areas)
430
+ this.renderAreaWithContent(a);
431
+ this.backgroundImage?.position === "overlay" && this.renderBackgroundImage(), this.renderSafeAreaGuide();
432
+ for (const a of this.safeAreaViolations) {
433
+ const o = this.areas.find((g) => g.id === a);
434
+ if (!o)
435
+ continue;
436
+ const { x: d, y: l, width: h, height: u } = o.location;
437
+ e.save(), e.fillStyle = "rgba(239, 68, 68, 0.15)", e.fillRect(d, l, h, u), e.restore();
438
+ }
439
+ this.selectedAreaId && this.renderSelectionOverlay(this.selectedAreaId), e.restore();
440
+ }
441
+ /**
442
+ * Render selection overlay with handles
443
+ */
444
+ renderSelectionOverlay(e) {
445
+ const t = this.getContentBounds(e);
446
+ if (!t) {
447
+ const l = this.areas.find((y) => y.id === e);
448
+ if (!l)
449
+ return;
450
+ const { ctx: h } = this, { x: u, y: g, width: m, height: b } = l.location;
451
+ h.save(), h.strokeStyle = "#3b82f6", h.lineWidth = 2 / this.zoom, h.setLineDash([]), h.strokeRect(u, g, m, b), h.restore();
452
+ return;
453
+ }
454
+ const { ctx: i } = this, { centerX: n, centerY: r, width: s, height: a, rotation: o } = t;
455
+ i.save(), i.translate(n, r), i.rotate(o * Math.PI / 180), i.strokeStyle = "#3b82f6", i.lineWidth = 2 / this.zoom, i.setLineDash([]), i.strokeRect(-s / 2, -a / 2, s, a), i.restore();
456
+ const d = this.getContentHandlePositions(e);
457
+ if (d)
458
+ for (const l of d) {
459
+ i.save();
460
+ const h = this.hoveredHandle === l.type, u = h ? "#dbeafe" : "#ffffff", g = h ? "#2563eb" : "#3b82f6";
461
+ if (l.type === "rotate") {
462
+ const m = this.getRotatedPoint(n, r - a / 2, n, r, o);
463
+ i.beginPath(), i.strokeStyle = g, i.lineWidth = 1 / this.zoom, i.setLineDash([4 / this.zoom, 4 / this.zoom]), i.moveTo(m.x, m.y), i.lineTo(l.x, l.y), i.stroke(), i.setLineDash([]), i.beginPath(), i.fillStyle = u, i.strokeStyle = g, i.lineWidth = 2 / this.zoom, i.arc(l.x, l.y, l.radius, 0, Math.PI * 2), i.fill(), i.stroke(), i.beginPath(), i.strokeStyle = g, i.lineWidth = 1.5 / this.zoom, i.arc(l.x, l.y, l.radius * 0.5, -Math.PI * 0.8, Math.PI * 0.5), i.stroke();
464
+ } else
465
+ i.fillStyle = u, i.strokeStyle = g, i.lineWidth = 2 / this.zoom, i.fillRect(l.x - l.radius, l.y - l.radius, l.radius * 2, l.radius * 2), i.strokeRect(l.x - l.radius, l.y - l.radius, l.radius * 2, l.radius * 2);
466
+ i.restore();
467
+ }
468
+ }
469
+ /**
470
+ * Rotate a point around a center
471
+ */
472
+ getRotatedPoint(e, t, i, n, r) {
473
+ const s = r * Math.PI / 180, a = Math.cos(s), o = Math.sin(s), d = e - i, l = t - n;
474
+ return {
475
+ x: i + d * a - l * o,
476
+ y: n + d * o + l * a
477
+ };
478
+ }
479
+ /**
480
+ * Render background image
481
+ */
482
+ renderBackgroundImage() {
483
+ if (!this.bgImageElement || !this.artboard || !this.backgroundImage)
484
+ return;
485
+ const { ctx: e } = this, { opacity: t } = this.backgroundImage, { width: i, height: n } = this.artboard;
486
+ e.save(), e.globalAlpha = t;
487
+ const r = this.bgImageElement.naturalWidth / this.bgImageElement.naturalHeight, s = i / n;
488
+ let a, o, d, l;
489
+ r > s ? (o = n, a = n * r, d = (i - a) / 2, l = 0) : (a = i, o = i / r, d = 0, l = (n - o) / 2), e.drawImage(this.bgImageElement, d, l, a, o), e.restore();
490
+ }
491
+ /**
492
+ * Render safe area guide as dashed green rectangle
493
+ */
494
+ renderSafeAreaGuide() {
495
+ if (!this.safeArea || !this.artboard)
496
+ return;
497
+ const { ctx: e } = this, { top: t, right: i, bottom: n, left: r } = this.safeArea, s = r, a = t, o = this.artboard.width - r - i, d = this.artboard.height - t - n;
498
+ o <= 0 || d <= 0 || (e.save(), e.strokeStyle = "#22c55e", e.lineWidth = 1 / this.zoom, e.setLineDash([6 / this.zoom, 4 / this.zoom]), e.strokeRect(s, a, o, d), e.setLineDash([]), e.restore());
499
+ }
500
+ /**
501
+ * Render an area with its content
502
+ */
503
+ renderAreaWithContent(e) {
504
+ const { ctx: t } = this, { location: i, backgroundColor: n } = e, { x: r, y: s, width: a, height: o } = i;
505
+ n && (t.fillStyle = n, t.fillRect(r, s, a, o));
506
+ const d = this.contents.get(e.id);
507
+ d && (d.type === "text" ? this.renderTextContent(e, d) : d.type === "image" && this.renderImageContent(e, d)), this.showAreaBorders && e.showBorder && (t.save(), t.strokeStyle = e.borderColor || "#3b82f6", t.lineWidth = 1 / this.zoom, t.setLineDash([4 / this.zoom, 4 / this.zoom]), t.strokeRect(r, s, a, o), t.setLineDash([]), t.restore());
508
+ }
509
+ /**
510
+ * Render text content in an area
511
+ */
512
+ renderTextContent(e, t) {
513
+ if (!t.text.trim())
514
+ return;
515
+ const { ctx: i } = this, { location: n } = e, { x: r, y: s, width: a, height: o } = n, d = t.offset || { x: 0, y: 0 }, l = t.scale ?? 1, h = t.rotation ?? 0, u = r + a / 2 + d.x, g = s + o / 2 + d.y;
516
+ i.save(), i.beginPath(), i.rect(r, s, a, o), i.clip(), i.translate(u, g), i.rotate(h * Math.PI / 180), i.scale(l, l), i.font = `${t.size}px ${t.font}, sans-serif`, i.fillStyle = t.color, i.textBaseline = "middle";
517
+ let m;
518
+ switch (t.align) {
519
+ case "left":
520
+ i.textAlign = "left", m = -a / 2 + 10;
521
+ break;
522
+ case "right":
523
+ i.textAlign = "right", m = a / 2 - 10;
524
+ break;
525
+ case "center":
526
+ default:
527
+ i.textAlign = "center", m = 0;
528
+ break;
529
+ }
530
+ i.fillText(t.text, m, 0), i.restore();
531
+ }
532
+ /**
533
+ * Render image content in an area
534
+ */
535
+ renderImageContent(e, t) {
536
+ const i = this.loadedImages.get(e.id);
537
+ if (!i)
538
+ return;
539
+ const { ctx: n } = this, { location: r } = e, { x: s, y: a, width: o, height: d } = r, l = t.offset || { x: 0, y: 0 }, h = t.scale ?? 1, u = t.rotation ?? 0;
540
+ n.save(), n.beginPath(), n.rect(s, a, o, d), n.clip();
541
+ const g = i.naturalWidth / i.naturalHeight, m = o / d;
542
+ let b, y;
543
+ g > m ? (b = o, y = o / g) : (y = d, b = d * g);
544
+ const v = b * h, w = y * h, p = s + o / 2 + l.x, C = a + d / 2 + l.y;
545
+ n.translate(p, C), n.rotate(u * Math.PI / 180), n.drawImage(i, -v / 2, -w / 2, v, w), n.restore();
546
+ }
547
+ /**
548
+ * Draw background image on a specific context (for export)
549
+ */
550
+ drawBackgroundImageOnContext(e, t, i) {
551
+ if (!this.bgImageElement || !this.backgroundImage)
552
+ return;
553
+ const { opacity: n } = this.backgroundImage;
554
+ e.save(), e.globalAlpha = n;
555
+ const r = this.bgImageElement.naturalWidth / this.bgImageElement.naturalHeight, s = t / i;
556
+ let a, o, d, l;
557
+ r > s ? (o = i, a = i * r, d = (t - a) / 2, l = 0) : (a = t, o = t / r, d = 0, l = (i - o) / 2), e.drawImage(this.bgImageElement, d, l, a, o), e.restore();
558
+ }
559
+ /**
560
+ * Draw area content on a specific context (for export)
561
+ */
562
+ drawAreaContentOnContext(e, t) {
563
+ const { location: i, backgroundColor: n } = t, { x: r, y: s, width: a, height: o } = i;
564
+ n && (e.fillStyle = n, e.fillRect(r, s, a, o));
565
+ const d = this.contents.get(t.id);
566
+ if (d) {
567
+ if (d.type === "text") {
568
+ if (!d.text.trim())
569
+ return;
570
+ const l = d.offset || { x: 0, y: 0 }, h = d.scale ?? 1, u = d.rotation ?? 0, g = r + a / 2 + l.x, m = s + o / 2 + l.y;
571
+ e.save(), e.beginPath(), e.rect(r, s, a, o), e.clip(), e.translate(g, m), e.rotate(u * Math.PI / 180), e.scale(h, h), e.font = `${d.size}px ${d.font}, sans-serif`, e.fillStyle = d.color, e.textBaseline = "middle";
572
+ let b;
573
+ switch (d.align) {
574
+ case "left":
575
+ e.textAlign = "left", b = -a / 2 + 10;
576
+ break;
577
+ case "right":
578
+ e.textAlign = "right", b = a / 2 - 10;
579
+ break;
580
+ case "center":
581
+ default:
582
+ e.textAlign = "center", b = 0;
583
+ break;
584
+ }
585
+ e.fillText(d.text, b, 0), e.restore();
586
+ } else if (d.type === "image") {
587
+ const l = this.loadedImages.get(t.id);
588
+ if (!l)
589
+ return;
590
+ const h = d.offset || { x: 0, y: 0 }, u = d.scale ?? 1, g = d.rotation ?? 0;
591
+ e.save(), e.beginPath(), e.rect(r, s, a, o), e.clip();
592
+ const m = l.naturalWidth / l.naturalHeight, b = a / o;
593
+ let y, v;
594
+ m > b ? (y = a, v = a / m) : (v = o, y = o * m);
595
+ const w = y * u, p = v * u, C = r + a / 2 + h.x, I = s + o / 2 + h.y;
596
+ e.translate(C, I), e.rotate(g * Math.PI / 180), e.drawImage(l, -w / 2, -p / 2, w, p), e.restore();
597
+ }
598
+ }
599
+ }
600
+ /**
601
+ * Cleanup resources
602
+ */
603
+ destroy() {
604
+ this.stop(), this.bgImageElement = null, this.loadedImages.clear(), this.loadingImages.clear(), this.eventListeners.clear();
605
+ }
606
+ }
607
+ function P(f) {
608
+ const e = f.textOptions;
609
+ return {
610
+ type: "text",
611
+ text: e?.defaultText || "",
612
+ font: e?.defaultFont || "Inter",
613
+ size: e?.defaultSize || 24,
614
+ color: e?.defaultColor || "#000000",
615
+ align: e?.defaultAlign || "center"
616
+ };
617
+ }
618
+ function A(f) {
619
+ return f.contentType === "text" || f.contentType === "both";
620
+ }
621
+ function O(f) {
622
+ return f.contentType === "image" || f.contentType === "both";
623
+ }
624
+ const Y = 50;
625
+ class j {
626
+ constructor(e) {
627
+ this.history = [], this.historyIndex = -1, this.listeners = /* @__PURE__ */ new Set(), this.isRestoring = !1, this.state = this.cloneState(e), this.history.push({
628
+ state: this.cloneState(e),
629
+ timestamp: Date.now(),
630
+ description: "Initial state"
631
+ }), this.historyIndex = 0;
632
+ }
633
+ getState() {
634
+ return this.state;
635
+ }
636
+ /**
637
+ * Update state and save to history
638
+ */
639
+ setState(e, t, i = !1) {
640
+ const n = this.state;
641
+ this.state = { ...this.state, ...e }, !i && !this.isRestoring && this.saveToHistory(n, t), this.notifyListeners();
642
+ }
643
+ /**
644
+ * Update state without saving to history
645
+ * Use for transient changes like zoom/pan
646
+ */
647
+ update(e) {
648
+ this.state = { ...this.state, ...e }, this.notifyListeners();
649
+ }
650
+ subscribe(e) {
651
+ return this.listeners.add(e), () => this.listeners.delete(e);
652
+ }
653
+ undo() {
654
+ return this.canUndo() ? (this.historyIndex--, this.restoreFromHistory(), !0) : !1;
655
+ }
656
+ redo() {
657
+ return this.canRedo() ? (this.historyIndex++, this.restoreFromHistory(), !0) : !1;
658
+ }
659
+ canUndo() {
660
+ return this.historyIndex > 0;
661
+ }
662
+ canRedo() {
663
+ return this.historyIndex < this.history.length - 1;
664
+ }
665
+ clearHistory() {
666
+ this.history = [], this.historyIndex = -1;
667
+ }
668
+ getHistory() {
669
+ return this.history;
670
+ }
671
+ saveToHistory(e, t) {
672
+ this.historyIndex < this.history.length - 1 && (this.history = this.history.slice(0, this.historyIndex + 1)), this.history.push({
673
+ state: this.cloneState(e),
674
+ timestamp: Date.now(),
675
+ description: t
676
+ }), this.history.length > Y && this.history.shift(), this.historyIndex = this.history.length - 1;
677
+ }
678
+ restoreFromHistory() {
679
+ const e = this.history[this.historyIndex];
680
+ e && (this.isRestoring = !0, this.state = this.cloneState(e.state), this.notifyListeners(), this.isRestoring = !1);
681
+ }
682
+ notifyListeners() {
683
+ const e = this.state;
684
+ for (const t of this.listeners)
685
+ try {
686
+ t(e);
687
+ } catch (i) {
688
+ console.error("Error in state change listener:", i);
689
+ }
690
+ }
691
+ /**
692
+ * Deep clone state. Contents are already serialized as Array<[string, AreaContent]>
693
+ * so JSON.parse/stringify works correctly.
694
+ */
695
+ cloneState(e) {
696
+ return JSON.parse(JSON.stringify(e));
697
+ }
698
+ }
699
+ class X {
700
+ constructor(e) {
701
+ this.listeners = /* @__PURE__ */ new Set(), this.areas = e, this.contents = this.buildInitialContents();
702
+ }
703
+ buildInitialContents() {
704
+ const e = /* @__PURE__ */ new Map();
705
+ for (const t of this.areas)
706
+ A(t) && e.set(t.id, P(t));
707
+ return e;
708
+ }
709
+ getContents() {
710
+ return this.contents;
711
+ }
712
+ getContent(e) {
713
+ return this.contents.get(e);
714
+ }
715
+ setTextContent(e, t) {
716
+ const i = this.contents.get(e), n = this.areas.find((s) => s.id === e), r = n ? P(n) : {
717
+ type: "text",
718
+ text: "",
719
+ font: "Inter",
720
+ size: 24,
721
+ color: "#000000",
722
+ align: "center"
723
+ };
724
+ i?.type === "text" ? this.contents.set(e, { ...i, ...t }) : this.contents.set(e, { ...r, ...t }), this.notify();
725
+ }
726
+ setImageContent(e, t, i) {
727
+ const n = {
728
+ type: "image",
729
+ dataUrl: t,
730
+ filename: i
731
+ };
732
+ this.contents.set(e, n), this.notify();
733
+ }
734
+ clearContent(e) {
735
+ const t = this.areas.find((i) => i.id === e);
736
+ t && A(t) ? this.contents.set(e, P(t)) : this.contents.delete(e), this.notify();
737
+ }
738
+ setContentOffset(e, t) {
739
+ const i = this.contents.get(e);
740
+ i && (this.contents.set(e, { ...i, offset: t }), this.notify());
741
+ }
742
+ setImageScale(e, t) {
743
+ const i = this.contents.get(e);
744
+ if (!i || i.type !== "image") return;
745
+ const n = Math.max(0.1, Math.min(5, t));
746
+ this.contents.set(e, { ...i, scale: n }), this.notify();
747
+ }
748
+ setImageRotation(e, t) {
749
+ const i = this.contents.get(e);
750
+ if (!i || i.type !== "image") return;
751
+ let n = t % 360;
752
+ n < 0 && (n += 360), this.contents.set(e, { ...i, rotation: n }), this.notify();
753
+ }
754
+ reset() {
755
+ this.contents = this.buildInitialContents(), this.notify();
756
+ }
757
+ /** Serialize for undo/redo (Map -> Array) */
758
+ toJSON() {
759
+ return Array.from(this.contents.entries());
760
+ }
761
+ /** Restore from serialized data */
762
+ fromJSON(e) {
763
+ this.contents = new Map(e), this.notify();
764
+ }
765
+ /** Replace the entire contents map (e.g. from undo/redo) without notification */
766
+ setContentsQuiet(e) {
767
+ this.contents = new Map(e);
768
+ }
769
+ subscribe(e) {
770
+ return this.listeners.add(e), () => this.listeners.delete(e);
771
+ }
772
+ notify() {
773
+ for (const e of this.listeners)
774
+ try {
775
+ e();
776
+ } catch (t) {
777
+ console.error("Error in AreaContentManager listener:", t);
778
+ }
779
+ }
780
+ }
781
+ class T {
782
+ constructor() {
783
+ this.listeners = /* @__PURE__ */ new Map();
784
+ }
785
+ /**
786
+ * Register an event listener
787
+ *
788
+ * @param event - Event name
789
+ * @param handler - Event handler function
790
+ * @returns Unsubscribe function
791
+ */
792
+ on(e, t) {
793
+ return this.listeners.has(e) || this.listeners.set(e, /* @__PURE__ */ new Set()), this.listeners.get(e).add(t), () => this.off(e, t);
794
+ }
795
+ /**
796
+ * Register a one-time event listener
797
+ *
798
+ * @param event - Event name
799
+ * @param handler - Event handler function
800
+ * @returns Unsubscribe function
801
+ */
802
+ once(e, t) {
803
+ const i = (n) => {
804
+ t(n), this.off(e, i);
805
+ };
806
+ return this.on(e, i);
807
+ }
808
+ /**
809
+ * Remove an event listener
810
+ *
811
+ * @param event - Event name
812
+ * @param handler - Event handler function to remove
813
+ */
814
+ off(e, t) {
815
+ const i = this.listeners.get(e);
816
+ i && (i.delete(t), i.size === 0 && this.listeners.delete(e));
817
+ }
818
+ /**
819
+ * Emit an event to all registered listeners
820
+ *
821
+ * @param event - Event name
822
+ * @param data - Event data
823
+ */
824
+ emit(e, t) {
825
+ const i = this.listeners.get(e);
826
+ if (i) {
827
+ const n = Array.from(i);
828
+ for (const r of n)
829
+ try {
830
+ r(t);
831
+ } catch (s) {
832
+ console.error(`Error in event handler for "${String(e)}":`, s);
833
+ }
834
+ }
835
+ }
836
+ /**
837
+ * Remove all listeners for an event, or all listeners if no event specified
838
+ *
839
+ * @param event - Optional event name to clear (clears all if not specified)
840
+ */
841
+ clear(e) {
842
+ e ? this.listeners.delete(e) : this.listeners.clear();
843
+ }
844
+ /**
845
+ * Get the number of listeners for an event
846
+ *
847
+ * @param event - Event name
848
+ * @returns Number of listeners
849
+ */
850
+ listenerCount(e) {
851
+ return this.listeners.get(e)?.size ?? 0;
852
+ }
853
+ }
854
+ class W extends T {
855
+ constructor() {
856
+ super(...arguments), this.activeTouches = /* @__PURE__ */ new Map(), this.initialPinchDistance = null, this.lastPinchCenter = null, this.isTwoFingerGesture = !1;
857
+ }
858
+ /** Whether a multi-touch gesture is currently active */
859
+ get isMultiTouch() {
860
+ return this.isTwoFingerGesture;
861
+ }
862
+ handleTouchStart(e) {
863
+ for (let t = 0; t < e.changedTouches.length; t++) {
864
+ const i = e.changedTouches[t];
865
+ this.activeTouches.set(i.identifier, {
866
+ x: i.clientX,
867
+ y: i.clientY
868
+ });
869
+ }
870
+ if (this.activeTouches.size === 2) {
871
+ this.isTwoFingerGesture = !0, e.preventDefault();
872
+ const [t, i] = Array.from(this.activeTouches.values());
873
+ this.initialPinchDistance = this.getDistance(t, i), this.lastPinchCenter = this.getCenter(t, i);
874
+ }
875
+ }
876
+ handleTouchMove(e) {
877
+ for (let t = 0; t < e.changedTouches.length; t++) {
878
+ const i = e.changedTouches[t];
879
+ this.activeTouches.set(i.identifier, {
880
+ x: i.clientX,
881
+ y: i.clientY
882
+ });
883
+ }
884
+ if (this.activeTouches.size === 2 && this.initialPinchDistance !== null && this.lastPinchCenter) {
885
+ e.preventDefault();
886
+ const [t, i] = Array.from(this.activeTouches.values()), n = this.getDistance(t, i), r = this.getCenter(t, i), s = n / this.initialPinchDistance;
887
+ this.emit("pinch", {
888
+ scale: s,
889
+ centerX: r.x,
890
+ centerY: r.y
891
+ });
892
+ const a = r.x - this.lastPinchCenter.x, o = r.y - this.lastPinchCenter.y;
893
+ (Math.abs(a) > 0.5 || Math.abs(o) > 0.5) && this.emit("pan", { deltaX: a, deltaY: o }), this.initialPinchDistance = n, this.lastPinchCenter = r;
894
+ }
895
+ }
896
+ handleTouchEnd(e) {
897
+ for (let t = 0; t < e.changedTouches.length; t++) {
898
+ const i = e.changedTouches[t];
899
+ this.activeTouches.delete(i.identifier);
900
+ }
901
+ this.activeTouches.size < 2 && (this.initialPinchDistance = null, this.lastPinchCenter = null, this.isTwoFingerGesture = !1);
902
+ }
903
+ reset() {
904
+ this.activeTouches.clear(), this.initialPinchDistance = null, this.lastPinchCenter = null, this.isTwoFingerGesture = !1;
905
+ }
906
+ getDistance(e, t) {
907
+ const i = t.x - e.x, n = t.y - e.y;
908
+ return Math.sqrt(i * i + n * n);
909
+ }
910
+ getCenter(e, t) {
911
+ return {
912
+ x: (e.x + t.x) / 2,
913
+ y: (e.y + t.y) / 2
914
+ };
915
+ }
916
+ }
917
+ class G extends T {
918
+ constructor(e, t, i, n) {
919
+ super(), this.interactionState = { mode: "idle" }, this.initialPinchZoom = 1, this.canvas = e, this.engine = t, this.getContents = i, this.getSelectedAreaId = n, this.gestureHandler = new W(), this.boundMouseDown = this.handleMouseDown.bind(this), this.boundMouseMove = this.handleMouseMove.bind(this), this.boundMouseUp = this.handleMouseUp.bind(this), this.boundMouseLeave = this.handleMouseLeave.bind(this), this.boundContextMenu = this.handleContextMenu.bind(this), this.boundDoubleClick = this.handleDoubleClick.bind(this), this.boundTouchStart = this.handleTouchStart.bind(this), this.boundTouchMove = this.handleTouchMove.bind(this), this.boundTouchEnd = this.handleTouchEnd.bind(this), e.addEventListener("mousedown", this.boundMouseDown), e.addEventListener("mousemove", this.boundMouseMove), e.addEventListener("mouseup", this.boundMouseUp), e.addEventListener("mouseleave", this.boundMouseLeave), e.addEventListener("contextmenu", this.boundContextMenu), e.addEventListener("dblclick", this.boundDoubleClick), e.addEventListener("touchstart", this.boundTouchStart, { passive: !1 }), e.addEventListener("touchmove", this.boundTouchMove, { passive: !1 }), e.addEventListener("touchend", this.boundTouchEnd), this.gestureHandler.on("pinch", ({ scale: r }) => {
920
+ const s = Math.max(0.1, Math.min(5, this.initialPinchZoom * r));
921
+ this.emit("zoom", { zoom: s });
922
+ }), this.gestureHandler.on("pan", ({ deltaX: r, deltaY: s }) => {
923
+ const a = this.engine.getPan(), o = this.engine.getZoom();
924
+ this.emit("pan", {
925
+ pan: {
926
+ x: a.x + r / o,
927
+ y: a.y + s / o
928
+ }
929
+ });
930
+ });
931
+ }
932
+ getCanvasPosition(e, t) {
933
+ const i = this.canvas.getBoundingClientRect(), n = window.devicePixelRatio || 1, r = (e - i.left) * n, s = (t - i.top) * n;
934
+ return this.engine.screenToCanvas(r, s);
935
+ }
936
+ handleMouseDown(e) {
937
+ const t = this.getCanvasPosition(e.clientX, e.clientY);
938
+ this.processPointerDown(t);
939
+ }
940
+ handleMouseMove(e) {
941
+ const t = this.getCanvasPosition(e.clientX, e.clientY);
942
+ this.processPointerMove(t);
943
+ }
944
+ handleMouseUp() {
945
+ this.processPointerUp();
946
+ }
947
+ handleMouseLeave() {
948
+ this.processPointerUp(), this.emit("cursor", { cursor: "default" });
949
+ }
950
+ handleContextMenu(e) {
951
+ e.preventDefault();
952
+ }
953
+ handleDoubleClick(e) {
954
+ const t = this.getCanvasPosition(e.clientX, e.clientY);
955
+ let i = this.engine.hitTestContent(t.x, t.y);
956
+ i || (i = this.engine.hitTestArea(t.x, t.y) ?? null), this.emit("context-menu", { areaId: i, clientX: e.clientX, clientY: e.clientY });
957
+ }
958
+ handleTouchStart(e) {
959
+ if (this.gestureHandler.handleTouchStart(e), this.gestureHandler.isMultiTouch) {
960
+ this.initialPinchZoom = this.engine.getZoom(), this.interactionState.mode !== "idle" && (this.interactionState = { mode: "idle" });
961
+ return;
962
+ }
963
+ if (e.touches.length !== 1) return;
964
+ const t = e.touches[0], i = this.getCanvasPosition(t.clientX, t.clientY);
965
+ this.processPointerDown(i) && e.preventDefault();
966
+ }
967
+ handleTouchMove(e) {
968
+ if (this.gestureHandler.handleTouchMove(e), this.gestureHandler.isMultiTouch || e.touches.length !== 1 || this.interactionState.mode === "idle") return;
969
+ const t = e.touches[0], i = this.getCanvasPosition(t.clientX, t.clientY);
970
+ this.processPointerMove(i), e.preventDefault();
971
+ }
972
+ handleTouchEnd(e) {
973
+ this.gestureHandler.handleTouchEnd(e), this.gestureHandler.isMultiTouch || this.processPointerUp();
974
+ }
975
+ /**
976
+ * Process pointer down. Returns true if an interaction started.
977
+ */
978
+ processPointerDown(e) {
979
+ const t = this.getSelectedAreaId(), i = this.getContents();
980
+ if (t) {
981
+ const s = this.engine.hitTestHandle(e.x, e.y, t);
982
+ if (s) {
983
+ const a = i.get(t), o = this.engine.getContentBounds(t);
984
+ if (s === "rotate" && a?.type === "image" && o) {
985
+ const d = Math.atan2(e.y - o.centerY, e.x - o.centerX) * (180 / Math.PI);
986
+ return this.interactionState = {
987
+ mode: "rotating",
988
+ areaId: t,
989
+ startRotation: a.rotation ?? 0,
990
+ centerPoint: { x: o.centerX, y: o.centerY },
991
+ startAngle: d
992
+ }, !0;
993
+ } else if (s !== "rotate" && a?.type === "image" && o)
994
+ return this.interactionState = {
995
+ mode: "scaling",
996
+ areaId: t,
997
+ handle: s,
998
+ startScale: a.scale ?? 1,
999
+ startMousePos: e,
1000
+ pivotPoint: { x: o.centerX, y: o.centerY }
1001
+ }, !0;
1002
+ }
1003
+ }
1004
+ const n = this.engine.hitTestContent(e.x, e.y);
1005
+ if (n) {
1006
+ if (this.emit("area:select", { areaId: n }), this.engine.isPositioningAllowed(n)) {
1007
+ const s = i.get(n)?.offset ?? { x: 0, y: 0 };
1008
+ return this.interactionState = {
1009
+ mode: "dragging",
1010
+ areaId: n,
1011
+ startOffset: s,
1012
+ startMousePos: e
1013
+ }, !0;
1014
+ }
1015
+ return !0;
1016
+ }
1017
+ const r = this.engine.hitTestArea(e.x, e.y);
1018
+ return r ? (this.emit("area:select", { areaId: r }), !1) : (this.emit("area:select", { areaId: null }), !1);
1019
+ }
1020
+ processPointerMove(e) {
1021
+ if (this.interactionState.mode === "dragging") {
1022
+ const { areaId: t, startOffset: i, startMousePos: n } = this.interactionState, r = e.x - n.x, s = e.y - n.y;
1023
+ let a = {
1024
+ x: i.x + r,
1025
+ y: i.y + s
1026
+ };
1027
+ const o = this.engine.getAreaLocation(t);
1028
+ if (o) {
1029
+ const d = o.width / 2 - 10, l = o.height / 2 - 10;
1030
+ a.x = Math.max(-d, Math.min(d, a.x)), a.y = Math.max(-l, Math.min(l, a.y));
1031
+ }
1032
+ this.emit("content:drag", { areaId: t, offset: a });
1033
+ return;
1034
+ }
1035
+ if (this.interactionState.mode === "scaling") {
1036
+ const { areaId: t, startScale: i, startMousePos: n, pivotPoint: r } = this.interactionState, s = Math.sqrt(
1037
+ Math.pow(n.x - r.x, 2) + Math.pow(n.y - r.y, 2)
1038
+ ), a = Math.sqrt(
1039
+ Math.pow(e.x - r.x, 2) + Math.pow(e.y - r.y, 2)
1040
+ );
1041
+ if (s > 0) {
1042
+ const o = a / s, d = Math.max(0.1, Math.min(5, i * o));
1043
+ this.emit("content:scale", { areaId: t, scale: d });
1044
+ }
1045
+ return;
1046
+ }
1047
+ if (this.interactionState.mode === "rotating") {
1048
+ const { areaId: t, startRotation: i, centerPoint: n, startAngle: r } = this.interactionState, s = Math.atan2(e.y - n.y, e.x - n.x) * (180 / Math.PI);
1049
+ let a = i + (s - r);
1050
+ a = a % 360, a < 0 && (a += 360);
1051
+ const o = [0, 90, 180, 270, 360];
1052
+ for (const d of o)
1053
+ if (Math.abs(a - d) < 5) {
1054
+ a = d === 360 ? 0 : d;
1055
+ break;
1056
+ }
1057
+ this.emit("content:rotate", { areaId: t, rotation: a });
1058
+ return;
1059
+ }
1060
+ this.updateCursor(e);
1061
+ }
1062
+ processPointerUp() {
1063
+ this.interactionState.mode !== "idle" && (this.interactionState = { mode: "idle" });
1064
+ }
1065
+ updateCursor(e) {
1066
+ const t = this.getSelectedAreaId();
1067
+ let i = "default";
1068
+ if (t) {
1069
+ const n = this.engine.hitTestHandle(e.x, e.y, t);
1070
+ n ? i = n === "rotate" ? "crosshair" : n === "nw" || n === "se" ? "nwse-resize" : "nesw-resize" : this.engine.hitTestContent(e.x, e.y) === t && (i = this.engine.isPositioningAllowed(t) ? "move" : "pointer");
1071
+ } else
1072
+ this.engine.hitTestContent(e.x, e.y) && (i = "pointer");
1073
+ this.emit("cursor", { cursor: i });
1074
+ }
1075
+ setSelectedArea(e) {
1076
+ this.engine.setSelectedArea(e);
1077
+ }
1078
+ destroy() {
1079
+ this.canvas.removeEventListener("mousedown", this.boundMouseDown), this.canvas.removeEventListener("mousemove", this.boundMouseMove), this.canvas.removeEventListener("mouseup", this.boundMouseUp), this.canvas.removeEventListener("mouseleave", this.boundMouseLeave), this.canvas.removeEventListener("contextmenu", this.boundContextMenu), this.canvas.removeEventListener("dblclick", this.boundDoubleClick), this.canvas.removeEventListener("touchstart", this.boundTouchStart), this.canvas.removeEventListener("touchmove", this.boundTouchMove), this.canvas.removeEventListener("touchend", this.boundTouchEnd), this.gestureHandler.reset(), this.clear();
1080
+ }
1081
+ }
1082
+ class J {
1083
+ constructor(e = "/api") {
1084
+ this.baseUrl = e;
1085
+ }
1086
+ async getTemplate(e) {
1087
+ try {
1088
+ const t = await fetch(`${this.baseUrl}/templates/${e}`);
1089
+ if (!t.ok) {
1090
+ let i = `Failed to load template (${t.status} ${t.statusText}): ${e}
1091
+
1092
+ `;
1093
+ throw t.status === 404 ? i += "Template not found." : t.status === 403 ? i += "Access forbidden." : t.status >= 500 && (i += "Server error."), new Error(i);
1094
+ }
1095
+ return (await t.json()).templateJson;
1096
+ } catch (t) {
1097
+ throw t instanceof Error && t.message.includes("Failed to fetch") ? new Error(`Network error loading template: ${e}`) : t;
1098
+ }
1099
+ }
1100
+ async finalizeDesign(e, t) {
1101
+ try {
1102
+ const i = await fetch(`${this.baseUrl}/finalize`, {
1103
+ method: "POST",
1104
+ headers: { "Content-Type": "application/json" },
1105
+ body: JSON.stringify({
1106
+ shopId: e,
1107
+ templateId: t.templateId,
1108
+ designJson: t
1109
+ })
1110
+ });
1111
+ if (!i.ok) {
1112
+ let n = `Failed to finalize design (${i.status} ${i.statusText})
1113
+
1114
+ `;
1115
+ if (i.status === 400) {
1116
+ const r = await i.json().catch(() => ({}));
1117
+ n += r.error || "Invalid design data.";
1118
+ } else i.status === 429 ? n += "Rate limit exceeded." : i.status >= 500 && (n += "Server error.");
1119
+ throw new Error(n);
1120
+ }
1121
+ return i.json();
1122
+ } catch (i) {
1123
+ throw i instanceof Error && i.message.includes("Failed to fetch") ? new Error("Network error finalizing design") : i;
1124
+ }
1125
+ }
1126
+ async uploadAsset(e, t) {
1127
+ try {
1128
+ const i = new FormData();
1129
+ i.append("shopId", t), i.append("file", e);
1130
+ const n = await fetch(`${this.baseUrl}/assets/upload`, {
1131
+ method: "POST",
1132
+ body: i
1133
+ });
1134
+ if (!n.ok) {
1135
+ let r = `Failed to upload asset (${n.status} ${n.statusText})
1136
+
1137
+ `;
1138
+ if (n.status === 400) {
1139
+ const s = await n.json().catch(() => ({}));
1140
+ r += s.error || "Invalid file.";
1141
+ } else n.status === 413 ? r += "File too large (max 15MB)." : n.status >= 500 && (r += "Server error.");
1142
+ throw new Error(r);
1143
+ }
1144
+ return n.json();
1145
+ } catch (i) {
1146
+ throw i instanceof Error && i.message.includes("Failed to fetch") ? new Error("Network error uploading asset") : i;
1147
+ }
1148
+ }
1149
+ async getStorageUsage(e) {
1150
+ try {
1151
+ const t = await fetch(`${this.baseUrl}/assets/usage?shopId=${encodeURIComponent(e)}`);
1152
+ if (!t.ok)
1153
+ throw new Error(`Failed to fetch storage usage (${t.status})`);
1154
+ return t.json();
1155
+ } catch (t) {
1156
+ throw t instanceof Error && t.message.includes("Failed to fetch") ? new Error("Network error fetching storage usage") : t;
1157
+ }
1158
+ }
1159
+ }
1160
+ function c(f, e, ...t) {
1161
+ const i = document.createElement(f);
1162
+ return e && Object.entries(e).forEach(([n, r]) => {
1163
+ n === "class" ? i.className = r : n in i ? i[n] = r : i.setAttribute(n, String(r));
1164
+ }), t.forEach((n) => {
1165
+ typeof n == "string" ? i.appendChild(document.createTextNode(n)) : i.appendChild(n);
1166
+ }), i;
1167
+ }
1168
+ function V(f, e) {
1169
+ let t = null, i = null;
1170
+ const n = (...r) => {
1171
+ i = r, t && clearTimeout(t), t = setTimeout(() => {
1172
+ i && f(...i), t = null, i = null;
1173
+ }, e);
1174
+ };
1175
+ return n.flush = () => {
1176
+ t && (clearTimeout(t), t = null), i && (f(...i), i = null);
1177
+ }, n.cancel = () => {
1178
+ t && (clearTimeout(t), t = null), i = null;
1179
+ }, n;
1180
+ }
1181
+ class K extends T {
1182
+ constructor() {
1183
+ super(), this.percentLabel = c("span", { class: "zoom-percent" }, "100%");
1184
+ const e = c("button", {
1185
+ class: "zoom-btn",
1186
+ title: "Zoom out"
1187
+ }, "−");
1188
+ e.addEventListener("click", () => this.emit("zoom-out", void 0));
1189
+ const t = c("button", {
1190
+ class: "zoom-btn",
1191
+ title: "Fit to view"
1192
+ }, "Fit");
1193
+ t.addEventListener("click", () => this.emit("zoom-fit", void 0));
1194
+ const i = c("button", {
1195
+ class: "zoom-btn",
1196
+ title: "Zoom in"
1197
+ }, "+");
1198
+ i.addEventListener("click", () => this.emit("zoom-in", void 0)), this.element = c("div", { class: "zoom-toolbar" }), this.element.appendChild(e), this.element.appendChild(t), this.element.appendChild(i), this.element.appendChild(this.percentLabel);
1199
+ }
1200
+ setZoom(e) {
1201
+ this.percentLabel.textContent = `${Math.round(e * 100)}%`;
1202
+ }
1203
+ getElement() {
1204
+ return this.element;
1205
+ }
1206
+ }
1207
+ const Q = [
1208
+ { label: "Inter", value: "Inter" },
1209
+ { label: "Arial", value: "Arial" },
1210
+ { label: "Georgia", value: "Georgia" },
1211
+ { label: "Times New Roman", value: "Times New Roman" },
1212
+ { label: "Courier New", value: "Courier New" }
1213
+ ], _ = [
1214
+ { label: "L", value: "left" },
1215
+ { label: "C", value: "center" },
1216
+ { label: "R", value: "right" }
1217
+ ], H = ["image/png", "image/jpeg", "image/webp", "image/svg+xml"], ee = 15 * 1024 * 1024;
1218
+ class te extends T {
1219
+ constructor(e) {
1220
+ super(), this.mode = "text", this.isSelected = !1, this.textInputEl = null, this.fontSelectEl = null, this.sizeInputEl = null, this.colorInputEl = null, this.alignGroupEl = null, this.area = e;
1221
+ const t = A(e), i = O(e);
1222
+ this.mode = i && !t ? "image" : "text", this.element = c("div", { class: "area-card" }), this.element.addEventListener("click", (o) => {
1223
+ o.target.tagName === "INPUT" || o.target.tagName === "SELECT" || o.target.tagName === "BUTTON" || this.emit("select", { areaId: e.id });
1224
+ });
1225
+ const n = c("div", { class: "area-card-header" }), r = c("div", { class: "area-card-name-row" }), s = c("span", { class: "area-card-name" }, e.name);
1226
+ if (r.appendChild(s), e.required) {
1227
+ const o = c("span", { class: "area-card-badge" }, "Required");
1228
+ r.appendChild(o);
1229
+ }
1230
+ n.appendChild(r);
1231
+ const a = c("button", {
1232
+ class: "area-card-reset-btn",
1233
+ title: "Reset content"
1234
+ }, "Reset");
1235
+ if (a.addEventListener("click", (o) => {
1236
+ o.stopPropagation(), this.emit("clear", { areaId: e.id });
1237
+ }), n.appendChild(a), this.element.appendChild(n), t && i) {
1238
+ const o = c("div", { class: "area-card-mode-switcher" }), d = c("button", {
1239
+ class: "mode-btn mode-btn-active"
1240
+ }, "Text");
1241
+ d.dataset.mode = "text";
1242
+ const l = c("button", { class: "mode-btn" }, "Image");
1243
+ l.dataset.mode = "image", d.addEventListener("click", (h) => {
1244
+ h.stopPropagation(), this.setMode("text"), d.classList.add("mode-btn-active"), l.classList.remove("mode-btn-active"), this.currentContent?.type === "image" && this.emit("clear", { areaId: e.id });
1245
+ }), l.addEventListener("click", (h) => {
1246
+ h.stopPropagation(), this.setMode("image"), l.classList.add("mode-btn-active"), d.classList.remove("mode-btn-active"), this.currentContent?.type === "text" && this.emit("clear", { areaId: e.id });
1247
+ }), o.appendChild(d), o.appendChild(l), this.element.appendChild(o);
1248
+ }
1249
+ this.contentContainer = c("div", { class: "area-card-content" }), this.element.appendChild(this.contentContainer), this.transformContainer = c("div", { class: "area-card-transforms" }), this.element.appendChild(this.transformContainer), this.renderContent();
1250
+ }
1251
+ setMode(e) {
1252
+ this.mode = e, this.renderContent();
1253
+ }
1254
+ setContent(e) {
1255
+ const t = this.currentContent?.type;
1256
+ this.currentContent = e, e?.type === "image" && (this.mode = "image"), this.element.querySelectorAll(".mode-btn").forEach((i) => {
1257
+ const n = i;
1258
+ n.classList.toggle("mode-btn-active", n.dataset.mode === this.mode);
1259
+ }), e?.type !== t || e?.type === "image" || e === void 0 ? this.renderContent() : e?.type === "text" && this.updateTextValues(e), this.renderTransforms();
1260
+ }
1261
+ setSelected(e) {
1262
+ this.isSelected = e, this.element.classList.toggle("area-card-selected", e), this.renderTransforms();
1263
+ }
1264
+ updateTextValues(e) {
1265
+ this.textInputEl && this.textInputEl.value !== e.text && (this.textInputEl.value = e.text), this.fontSelectEl && this.fontSelectEl.value !== e.font && (this.fontSelectEl.value = e.font), this.sizeInputEl && this.sizeInputEl.value !== String(e.size) && (this.sizeInputEl.value = String(e.size)), this.colorInputEl && this.colorInputEl.value !== e.color && (this.colorInputEl.value = e.color), this.alignGroupEl && this.alignGroupEl.querySelectorAll(".align-btn").forEach((t) => {
1266
+ const i = t, n = i.title === "Left" ? "left" : i.title === "Center" ? "center" : "right";
1267
+ i.classList.toggle("align-btn-active", n === e.align);
1268
+ });
1269
+ }
1270
+ renderContent() {
1271
+ this.contentContainer.innerHTML = "", this.textInputEl = null, this.fontSelectEl = null, this.sizeInputEl = null, this.colorInputEl = null, this.alignGroupEl = null;
1272
+ const e = A(this.area), t = O(this.area), i = !t || e && t && this.mode === "text", n = !e || e && t && this.mode === "image";
1273
+ e && i && this.renderTextControls(), t && n && this.renderImageControls();
1274
+ }
1275
+ renderTextControls() {
1276
+ const e = this.currentContent, t = e?.type === "text" ? e : P(this.area), i = c("input", {
1277
+ class: "area-input-text",
1278
+ type: "text",
1279
+ placeholder: this.area.placeholder || "Enter text..."
1280
+ });
1281
+ i.value = t.text, this.area.textOptions?.maxLength && (i.maxLength = this.area.textOptions.maxLength), i.addEventListener("input", () => {
1282
+ this.emit("text:change", { areaId: this.area.id, updates: { text: i.value } });
1283
+ }), this.contentContainer.appendChild(i), this.textInputEl = i;
1284
+ const n = c("div", { class: "area-input-row" }), r = c("select", { class: "area-input-font" });
1285
+ for (const l of Q) {
1286
+ const h = c("option");
1287
+ h.value = l.value, h.textContent = l.label, l.value === t.font && (h.selected = !0), r.appendChild(h);
1288
+ }
1289
+ r.addEventListener("change", () => {
1290
+ this.emit("text:change", { areaId: this.area.id, updates: { font: r.value } });
1291
+ }), n.appendChild(r), this.fontSelectEl = r;
1292
+ const s = c("input", {
1293
+ class: "area-input-size",
1294
+ type: "number"
1295
+ });
1296
+ s.value = String(t.size), s.min = String(this.area.textOptions?.minSize || 8), s.max = String(this.area.textOptions?.maxSize || 200), s.addEventListener("change", () => {
1297
+ this.emit("text:change", {
1298
+ areaId: this.area.id,
1299
+ updates: { size: parseInt(s.value, 10) || 24 }
1300
+ });
1301
+ }), n.appendChild(s), this.sizeInputEl = s, this.contentContainer.appendChild(n);
1302
+ const a = c("div", { class: "area-input-row" }), o = c("div", { class: "area-input-align" });
1303
+ for (const l of _) {
1304
+ const h = c("button", {
1305
+ class: `align-btn${t.align === l.value ? " align-btn-active" : ""}`,
1306
+ title: l.label === "L" ? "Left" : l.label === "C" ? "Center" : "Right"
1307
+ }, l.label);
1308
+ h.addEventListener("click", (u) => {
1309
+ u.stopPropagation(), this.emit("text:change", { areaId: this.area.id, updates: { align: l.value } }), o.querySelectorAll(".align-btn").forEach((g) => g.classList.remove("align-btn-active")), h.classList.add("align-btn-active");
1310
+ }), o.appendChild(h);
1311
+ }
1312
+ a.appendChild(o), this.alignGroupEl = o;
1313
+ const d = c("input", {
1314
+ class: "area-input-color",
1315
+ type: "color"
1316
+ });
1317
+ d.value = t.color, d.addEventListener("input", () => {
1318
+ this.emit("text:change", { areaId: this.area.id, updates: { color: d.value } });
1319
+ }), a.appendChild(d), this.colorInputEl = d, this.contentContainer.appendChild(a);
1320
+ }
1321
+ renderImageControls() {
1322
+ const e = this.currentContent;
1323
+ if (e?.type === "image") {
1324
+ const t = c("div", { class: "area-image-preview" }), i = c("img", { class: "area-image-thumb" });
1325
+ i.src = e.dataUrl, i.alt = e.filename || "Uploaded image", t.appendChild(i);
1326
+ const n = c("div", { class: "area-image-info" });
1327
+ n.appendChild(c("span", { class: "area-image-name" }, e.filename || "Image"));
1328
+ const r = c("button", { class: "area-image-remove-btn" }, "Remove");
1329
+ r.addEventListener("click", (s) => {
1330
+ s.stopPropagation(), this.emit("clear", { areaId: this.area.id });
1331
+ }), n.appendChild(r), t.appendChild(n), this.contentContainer.appendChild(t);
1332
+ } else {
1333
+ const t = c("button", { class: "area-upload-btn" }, "Upload Image"), i = c("input", { type: "file" });
1334
+ i.accept = H.join(","), i.style.display = "none";
1335
+ const n = c("div", { class: "area-validation-error" });
1336
+ n.style.display = "none", i.addEventListener("change", () => {
1337
+ const r = i.files?.[0];
1338
+ if (!r) return;
1339
+ if (!H.includes(r.type)) {
1340
+ const a = "Only PNG, JPEG, WebP, and SVG images are accepted";
1341
+ n.textContent = a, n.style.display = "block", this.emit("validation:error", { areaId: this.area.id, message: a }), i.value = "";
1342
+ return;
1343
+ }
1344
+ if (r.size > ee) {
1345
+ const a = "File must be smaller than 15MB";
1346
+ n.textContent = a, n.style.display = "block", this.emit("validation:error", { areaId: this.area.id, message: a }), i.value = "";
1347
+ return;
1348
+ }
1349
+ n.style.display = "none";
1350
+ const s = new FileReader();
1351
+ s.onload = () => {
1352
+ const a = s.result;
1353
+ this.emit("image:change", { areaId: this.area.id, dataUrl: a, filename: r.name });
1354
+ }, s.readAsDataURL(r), i.value = "";
1355
+ }), t.addEventListener("click", (r) => {
1356
+ r.stopPropagation(), i.click();
1357
+ }), this.contentContainer.appendChild(t), this.contentContainer.appendChild(i), this.contentContainer.appendChild(n);
1358
+ }
1359
+ }
1360
+ renderTransforms() {
1361
+ this.transformContainer.innerHTML = "";
1362
+ const e = this.currentContent;
1363
+ if (!this.isSelected || !e) return;
1364
+ const t = e.type === "text" ? this.area.textOptions?.allowPositioning : this.area.imageOptions?.allowPositioning, i = e.type === "image" && this.area.imageOptions?.allowScaling, n = e.type === "image" && this.area.imageOptions?.allowRotation;
1365
+ if (!t && !i && !n) return;
1366
+ const r = c("div", { class: "area-card-divider" });
1367
+ this.transformContainer.appendChild(r);
1368
+ const s = c("div", { class: "transform-header" });
1369
+ s.appendChild(c("span", { class: "transform-title" }, "Transform"));
1370
+ const a = c("button", { class: "transform-reset-btn" }, "Reset");
1371
+ a.addEventListener("click", (d) => {
1372
+ d.stopPropagation(), t && this.emit("offset:change", { areaId: this.area.id, offset: { x: 0, y: 0 } }), i && this.emit("scale:change", { areaId: this.area.id, scale: 1 }), n && this.emit("rotation:change", { areaId: this.area.id, rotation: 0 });
1373
+ }), s.appendChild(a), this.transformContainer.appendChild(s);
1374
+ const o = e.offset ?? { x: 0, y: 0 };
1375
+ if (t) {
1376
+ const d = c("div", { class: "area-input-row" }), l = c("label", { class: "transform-label" }, "X"), h = c("input", { class: "transform-input", type: "number" });
1377
+ h.value = String(Math.round(o.x)), h.addEventListener("change", () => {
1378
+ this.emit("offset:change", {
1379
+ areaId: this.area.id,
1380
+ offset: { ...o, x: parseInt(h.value, 10) || 0 }
1381
+ });
1382
+ });
1383
+ const u = c("label", { class: "transform-label" }, "Y"), g = c("input", { class: "transform-input", type: "number" });
1384
+ g.value = String(Math.round(o.y)), g.addEventListener("change", () => {
1385
+ this.emit("offset:change", {
1386
+ areaId: this.area.id,
1387
+ offset: { ...o, y: parseInt(g.value, 10) || 0 }
1388
+ });
1389
+ }), d.appendChild(l), d.appendChild(h), d.appendChild(u), d.appendChild(g), this.transformContainer.appendChild(d);
1390
+ }
1391
+ if (i && e.type === "image") {
1392
+ const d = e.scale ?? 1, l = c("div", { class: "area-input-row transform-slider-row" }), h = c("label", { class: "transform-label" }, `Scale: ${Math.round(d * 100)}%`), u = c("input", { class: "transform-slider", type: "range" });
1393
+ u.min = "10", u.max = "500", u.step = "5", u.value = String(Math.round(d * 100)), u.addEventListener("input", () => {
1394
+ const g = parseInt(u.value, 10) / 100;
1395
+ h.textContent = `Scale: ${Math.round(g * 100)}%`, this.emit("scale:change", { areaId: this.area.id, scale: g });
1396
+ }), l.appendChild(h), l.appendChild(u), this.transformContainer.appendChild(l);
1397
+ }
1398
+ if (n && e.type === "image") {
1399
+ const d = e.rotation ?? 0, l = c("div", { class: "area-input-row transform-slider-row" }), h = c("label", { class: "transform-label" }, `Rotation: ${Math.round(d)}°`), u = c("input", { class: "transform-slider", type: "range" });
1400
+ u.min = "0", u.max = "360", u.step = "1", u.value = String(Math.round(d)), u.addEventListener("input", () => {
1401
+ const g = parseInt(u.value, 10);
1402
+ h.textContent = `Rotation: ${g}°`, this.emit("rotation:change", { areaId: this.area.id, rotation: g });
1403
+ }), l.appendChild(h), l.appendChild(u), this.transformContainer.appendChild(l);
1404
+ }
1405
+ }
1406
+ getElement() {
1407
+ return this.element;
1408
+ }
1409
+ }
1410
+ class ie extends T {
1411
+ constructor(e) {
1412
+ super(), this.cards = /* @__PURE__ */ new Map(), this.isMobile = !1, this.boundEscapeHandler = (n) => {
1413
+ n.key === "Escape" && this.emit("dismiss", void 0);
1414
+ }, this.backdrop = c("div", { class: "area-panel-backdrop" }), this.backdrop.addEventListener("click", () => {
1415
+ this.emit("dismiss", void 0);
1416
+ }), this.element = c("div", { class: "area-panel" });
1417
+ const t = c("div", { class: "area-panel-header" });
1418
+ t.appendChild(c("span", { class: "area-panel-title" }, "Customize"));
1419
+ const i = c("button", { class: "area-panel-close-btn", title: "Close" }, "×");
1420
+ if (i.addEventListener("click", () => {
1421
+ this.emit("dismiss", void 0);
1422
+ }), t.appendChild(i), this.element.appendChild(t), this.panelContent = c("div", { class: "area-panel-content" }), e.length === 0) {
1423
+ const n = c("p", { class: "area-panel-empty" }, "No customization areas defined.");
1424
+ this.panelContent.appendChild(n);
1425
+ } else
1426
+ for (const n of e) {
1427
+ const r = new te(n);
1428
+ this.cards.set(n.id, r), r.on("text:change", (s) => this.emit("text:change", s)), r.on("image:change", (s) => this.emit("image:change", s)), r.on("clear", (s) => this.emit("clear", s)), r.on("select", (s) => this.emit("select", s)), r.on("offset:change", (s) => this.emit("offset:change", s)), r.on("scale:change", (s) => this.emit("scale:change", s)), r.on("rotation:change", (s) => this.emit("rotation:change", s)), r.on("validation:error", (s) => this.emit("validation:error", s)), this.panelContent.appendChild(r.getElement());
1429
+ }
1430
+ this.element.appendChild(this.panelContent);
1431
+ }
1432
+ setContents(e) {
1433
+ for (const [t, i] of this.cards)
1434
+ i.setContent(e.get(t));
1435
+ }
1436
+ setSelectedArea(e) {
1437
+ for (const [t, i] of this.cards) {
1438
+ const n = t === e;
1439
+ i.setSelected(n), i.getElement().style.display = n ? "" : "none";
1440
+ }
1441
+ e ? (this.element.classList.add("area-panel-visible"), this.isMobile && this.backdrop.classList.add("area-panel-backdrop-visible"), document.addEventListener("keydown", this.boundEscapeHandler)) : (this.element.classList.remove("area-panel-visible"), this.isMobile && this.backdrop.classList.remove("area-panel-backdrop-visible"), document.removeEventListener("keydown", this.boundEscapeHandler));
1442
+ }
1443
+ setMobile(e) {
1444
+ this.isMobile = e, this.element.classList.toggle("area-panel-mobile", e), e || (this.element.classList.remove("area-panel-visible"), this.backdrop.classList.remove("area-panel-backdrop-visible"));
1445
+ }
1446
+ getElement() {
1447
+ return this.element;
1448
+ }
1449
+ getBackdrop() {
1450
+ return this.backdrop;
1451
+ }
1452
+ }
1453
+ class ne {
1454
+ constructor(e = window) {
1455
+ this.target = e, this.commands = [], this.isEnabled = !0, this.handleKeyDown = (t) => {
1456
+ if (!this.isEnabled) return;
1457
+ const i = t.target;
1458
+ if (!(i.tagName === "INPUT" || i.tagName === "TEXTAREA" || i.isContentEditable)) {
1459
+ for (const n of this.commands)
1460
+ if (this.matchesCommand(t, n)) {
1461
+ t.preventDefault(), n.handler(t);
1462
+ break;
1463
+ }
1464
+ }
1465
+ }, this.target.addEventListener("keydown", this.handleKeyDown);
1466
+ }
1467
+ /**
1468
+ * Register a keyboard command
1469
+ */
1470
+ register(e) {
1471
+ return this.commands.push(e), () => {
1472
+ const t = this.commands.indexOf(e);
1473
+ t !== -1 && this.commands.splice(t, 1);
1474
+ };
1475
+ }
1476
+ /**
1477
+ * Enable keyboard handling
1478
+ */
1479
+ enable() {
1480
+ this.isEnabled = !0;
1481
+ }
1482
+ /**
1483
+ * Disable keyboard handling
1484
+ */
1485
+ disable() {
1486
+ this.isEnabled = !1;
1487
+ }
1488
+ /**
1489
+ * Check if keyboard handling is enabled
1490
+ */
1491
+ isActive() {
1492
+ return this.isEnabled;
1493
+ }
1494
+ /**
1495
+ * Check if event matches a command
1496
+ */
1497
+ matchesCommand(e, t) {
1498
+ return !(e.key.toLowerCase() !== t.key.toLowerCase() || t.ctrl !== void 0 && t.ctrl !== e.ctrlKey || t.shift !== void 0 && t.shift !== e.shiftKey || t.alt !== void 0 && t.alt !== e.altKey || t.meta !== void 0 && t.meta !== e.metaKey);
1499
+ }
1500
+ /**
1501
+ * Cleanup
1502
+ */
1503
+ destroy() {
1504
+ this.target.removeEventListener("keydown", this.handleKeyDown), this.commands = [];
1505
+ }
1506
+ }
1507
+ function re() {
1508
+ return /Mac|iPod|iPhone|iPad/.test(navigator.platform);
1509
+ }
1510
+ function ae() {
1511
+ return re() ? "meta" : "ctrl";
1512
+ }
1513
+ function se(f, e, t, i) {
1514
+ const n = f / t, r = e / i;
1515
+ return Math.min(n, r);
1516
+ }
1517
+ function oe(f, e, t = 8, i = 10, n = 300) {
1518
+ const r = se(f, e, t, i), s = r >= n;
1519
+ return {
1520
+ actualDPI: Math.round(r),
1521
+ targetDPI: n,
1522
+ width: f,
1523
+ height: e,
1524
+ passed: s,
1525
+ warning: s ? void 0 : `Image resolution is ${Math.round(r)} DPI at 100% scale. For best print quality, use images with at least ${n} DPI. This may result in pixelated or blurry prints.`
1526
+ };
1527
+ }
1528
+ const F = ':host{--editor-bg: #f3f4f6;--editor-surface: #ffffff;--editor-border: #e5e7eb;--editor-text: #111827;--editor-text-muted: #6b7280;--editor-primary: #3b82f6;--editor-primary-hover: #2563eb;--editor-danger: #ef4444;--editor-radius: 8px;--editor-radius-sm: 4px;--editor-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--editor-panel-width: 280px;display:block;width:100%;height:100%;font-family:var(--editor-font);font-size:13px;color:var(--editor-text);line-height:1.4}:host([theme="dark"]){--editor-bg: #1f2937;--editor-surface: #374151;--editor-border: #4b5563;--editor-text: #f9fafb;--editor-text-muted: #9ca3af}.editor-container{display:flex;flex-direction:column;width:100%;height:100%;position:relative;overflow:hidden;background:var(--editor-bg)}.layout-desktop .editor-main{display:flex;flex:1;min-height:0}.layout-desktop .canvas-area{flex:1;min-width:0;position:relative;display:flex;flex-direction:column}.layout-desktop .area-panel{width:var(--editor-panel-width);border-left:1px solid var(--editor-border);background:var(--editor-surface);display:none;flex-direction:column;overflow:hidden}.layout-desktop .area-panel.area-panel-visible{display:flex}.layout-mobile .editor-main{display:flex;flex-direction:column;flex:1;min-height:0}.layout-mobile .canvas-area{flex:1;position:relative;display:flex;flex-direction:column;min-height:0}.layout-mobile .area-panel{position:absolute;bottom:0;left:0;right:0;max-height:50%;background:var(--editor-surface);border-top:1px solid var(--editor-border);border-radius:var(--editor-radius) var(--editor-radius) 0 0;box-shadow:0 -4px 20px #00000026;transform:translateY(100%);transition:transform .25s ease-out;z-index:20;display:flex;flex-direction:column;overflow:hidden}.layout-mobile .area-panel.area-panel-visible{transform:translateY(0)}.area-panel-backdrop{display:none}.layout-mobile .area-panel-backdrop{display:block;position:absolute;inset:0;background:#0000004d;z-index:15;opacity:0;pointer-events:none;transition:opacity .25s ease-out}.layout-mobile .area-panel-backdrop.area-panel-backdrop-visible{opacity:1;pointer-events:auto}.zoom-toolbar{display:flex;align-items:center;gap:4px;padding:8px 12px;background:var(--editor-surface);border-bottom:1px solid var(--editor-border)}.zoom-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);background:var(--editor-surface);color:var(--editor-text);cursor:pointer;font-size:14px;font-family:var(--editor-font);transition:background .15s;-webkit-tap-highlight-color:transparent;touch-action:manipulation}.zoom-btn:hover{background:var(--editor-bg)}.zoom-btn:active{background:var(--editor-border)}.zoom-percent{margin-left:8px;font-size:12px;color:var(--editor-text-muted);min-width:40px}.canvas-wrapper{flex:1;position:relative;overflow:hidden;min-height:200px}.editor-canvas{position:absolute;top:0;left:0}.area-panel-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--editor-border);flex-shrink:0}.area-panel-title{font-weight:600;font-size:14px}.area-panel-close-btn{width:28px;height:28px;border:none;background:none;color:var(--editor-text-muted);cursor:pointer;font-size:18px;line-height:1;border-radius:var(--editor-radius-sm);display:flex;align-items:center;justify-content:center}.area-panel-close-btn:hover{background:var(--editor-bg);color:var(--editor-text)}.area-panel-content{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:12px}.area-panel-empty{color:var(--editor-text-muted);font-size:12px;text-align:center;padding:20px 0}.area-card{padding:12px;border:1px solid var(--editor-border);border-radius:var(--editor-radius);background:var(--editor-surface);cursor:pointer;transition:border-color .15s}.area-card:hover{border-color:var(--editor-primary)}.area-card-selected{border-color:var(--editor-primary);box-shadow:0 0 0 1px var(--editor-primary)}.area-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.area-card-name-row{display:flex;align-items:center;gap:6px}.area-card-name{font-weight:600;font-size:13px}.area-card-badge{display:inline-block;padding:1px 6px;font-size:10px;font-weight:500;background:#fef3c7;color:#92400e;border-radius:10px}.area-card-reset-btn{padding:2px 8px;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);background:none;color:var(--editor-text-muted);cursor:pointer;font-size:11px;font-family:var(--editor-font)}.area-card-reset-btn:hover{background:var(--editor-bg);color:var(--editor-danger);border-color:var(--editor-danger)}.area-card-mode-switcher{display:flex;gap:4px;margin-bottom:8px}.mode-btn{flex:1;padding:4px 8px;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);background:var(--editor-surface);color:var(--editor-text);cursor:pointer;font-size:12px;font-family:var(--editor-font);transition:all .15s}.mode-btn-active{background:var(--editor-primary);color:#fff;border-color:var(--editor-primary)}.area-input-text{width:100%;padding:6px 8px;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);font-family:var(--editor-font);font-size:13px;color:var(--editor-text);background:var(--editor-surface);box-sizing:border-box;margin-bottom:6px}.area-input-text:focus{outline:none;border-color:var(--editor-primary);box-shadow:0 0 0 1px var(--editor-primary)}.area-input-row{display:flex;gap:6px;align-items:center;margin-bottom:6px}.area-input-font{flex:1;padding:4px 6px;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);font-family:var(--editor-font);font-size:12px;color:var(--editor-text);background:var(--editor-surface);min-width:0}.area-input-size{width:56px;padding:4px 6px;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);font-family:var(--editor-font);font-size:12px;color:var(--editor-text);background:var(--editor-surface)}.area-input-align{display:flex;gap:2px}.align-btn{width:28px;height:28px;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);background:var(--editor-surface);color:var(--editor-text);cursor:pointer;font-size:12px;font-weight:600;font-family:var(--editor-font)}.align-btn-active{background:var(--editor-primary);color:#fff;border-color:var(--editor-primary)}.area-input-color{width:32px;height:28px;padding:0;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);cursor:pointer;background:none}.area-upload-btn{width:100%;padding:10px;border:2px dashed var(--editor-border);border-radius:var(--editor-radius);background:var(--editor-bg);color:var(--editor-text-muted);cursor:pointer;font-family:var(--editor-font);font-size:12px;text-align:center;transition:all .15s;min-height:44px}.area-upload-btn:hover{border-color:var(--editor-primary);color:var(--editor-primary)}.area-image-preview{display:flex;gap:8px;align-items:center}.area-image-thumb{width:48px;height:48px;object-fit:cover;border-radius:var(--editor-radius-sm);border:1px solid var(--editor-border)}.area-image-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}.area-image-name{font-size:12px;color:var(--editor-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.area-image-remove-btn{padding:2px 8px;border:1px solid var(--editor-danger);border-radius:var(--editor-radius-sm);background:none;color:var(--editor-danger);cursor:pointer;font-size:11px;font-family:var(--editor-font);width:fit-content}.area-image-remove-btn:hover{background:#fef2f2}.area-card-divider{height:1px;background:var(--editor-border);margin:8px 0}.transform-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}.transform-title{font-weight:600;font-size:12px}.transform-reset-btn{padding:2px 8px;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);background:none;color:var(--editor-text-muted);cursor:pointer;font-size:11px;font-family:var(--editor-font)}.transform-reset-btn:hover{background:var(--editor-bg)}.transform-label{font-size:11px;color:var(--editor-text-muted);white-space:nowrap}.transform-input{width:60px;padding:3px 6px;border:1px solid var(--editor-border);border-radius:var(--editor-radius-sm);font-family:var(--editor-font);font-size:12px;color:var(--editor-text);background:var(--editor-surface)}.transform-slider-row{flex-direction:column;align-items:stretch}.transform-slider{width:100%;accent-color:var(--editor-primary)}.loading-container{display:flex;justify-content:center;align-items:center;width:100%;height:100%;min-height:200px}.loading-spinner{width:32px;height:32px;border:3px solid var(--editor-border);border-top-color:var(--editor-primary);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error-container{display:flex;flex-direction:column;justify-content:center;align-items:center;width:100%;height:100%;min-height:200px;padding:20px;text-align:center}.error-title{font-weight:600;font-size:16px;color:var(--editor-danger);margin-bottom:8px}.error-message{font-size:13px;color:var(--editor-text-muted);max-width:400px;white-space:pre-wrap}:host(:focus){outline:2px solid var(--editor-primary);outline-offset:-2px}input:focus,select:focus,button:focus-visible{outline:2px solid var(--editor-primary);outline-offset:-1px}.context-menu{position:absolute;z-index:100;min-width:160px;background:var(--editor-surface);border:1px solid var(--editor-border);border-radius:var(--editor-radius);box-shadow:0 4px 12px #00000026;padding:4px 0;font-size:13px}.context-menu-header{padding:6px 12px;font-weight:600;font-size:12px;color:var(--editor-text-muted);user-select:none}.context-menu-divider{height:1px;background:var(--editor-border);margin:4px 0}.context-menu-item{padding:6px 12px;cursor:pointer;color:var(--editor-text);user-select:none;transition:background .1s}.context-menu-item:hover{background:var(--editor-bg)}.context-menu-item:active{background:var(--editor-border)}.context-menu-item-disabled{color:var(--editor-text-muted);cursor:default;pointer-events:none}.editor-toast{position:absolute;top:16px;left:50%;transform:translate(-50%);padding:10px 20px;border-radius:var(--editor-radius);font-size:13px;font-family:var(--editor-font);line-height:1.4;z-index:200;max-width:400px;text-align:center;animation:toast-in .25s ease-out,toast-out .25s ease-in forwards;animation-delay:0s,var(--toast-duration, 3.75s);pointer-events:none;box-shadow:0 4px 12px #00000026}.editor-toast-warning{background:#fef3c7;color:#92400e;border:1px solid #f59e0b}.editor-toast-error{background:#fef2f2;color:#991b1b;border:1px solid var(--editor-danger)}.editor-toast-info{background:#eff6ff;color:#1e40af;border:1px solid var(--editor-primary)}@keyframes toast-in{0%{opacity:0;transform:translate(-50%) translateY(-10px)}to{opacity:1;transform:translate(-50%) translateY(0)}}@keyframes toast-out{0%{opacity:1;transform:translate(-50%) translateY(0)}to{opacity:0;transform:translate(-50%) translateY(-10px)}}.editor-modal-overlay{position:absolute;inset:0;background:#0006;z-index:300;display:flex;align-items:center;justify-content:center;animation:modal-overlay-in .2s ease-out}@keyframes modal-overlay-in{0%{opacity:0}to{opacity:1}}.editor-modal{background:var(--editor-surface);border-radius:var(--editor-radius);box-shadow:0 8px 32px #0003;max-width:380px;width:calc(100% - 32px);padding:20px;animation:modal-in .2s ease-out}@keyframes modal-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.editor-modal-title{font-weight:600;font-size:15px;margin-bottom:8px;color:var(--editor-text)}.editor-modal-body{font-size:13px;color:var(--editor-text-muted);margin-bottom:16px;line-height:1.5}.editor-modal-actions{display:flex;justify-content:flex-end;gap:8px}.editor-modal-btn{padding:8px 16px;border-radius:var(--editor-radius-sm);font-size:13px;font-family:var(--editor-font);cursor:pointer;border:1px solid var(--editor-border);background:var(--editor-surface);color:var(--editor-text);transition:background .15s}.editor-modal-btn:hover{background:var(--editor-bg)}.editor-modal-btn-primary{background:var(--editor-primary);color:#fff;border-color:var(--editor-primary)}.editor-modal-btn-primary:hover{background:var(--editor-primary-hover)}.area-validation-error{font-size:11px;color:var(--editor-danger);margin-top:4px}@media(pointer:coarse){.zoom-btn{min-width:44px;min-height:44px}.area-input-text,.area-input-font,.area-input-size{min-height:44px;font-size:16px}.align-btn{min-width:44px;min-height:44px}.area-upload-btn{min-height:48px}}', de = "2.0.0", le = 768;
1529
+ class he extends HTMLElement {
1530
+ constructor() {
1531
+ super(), this.resizeObserver = null, this.isReady = !1, this.currentTemplate = null, this.loadingTemplateId = null, this.isMobileLayout = !1, this.subscriptions = [], this.storageUsage = null, this.storageUsageLastRefresh = 0, this.emitChange = V(() => {
1532
+ this.dispatchEvent(
1533
+ new CustomEvent("change", {
1534
+ detail: { design: this.getDesign() },
1535
+ bubbles: !0,
1536
+ composed: !0
1537
+ })
1538
+ );
1539
+ }, 300), this.shadow = this.attachShadow({ mode: "open" });
1540
+ }
1541
+ static get observedAttributes() {
1542
+ return ["template-id", "theme", "mode", "store-id", "api-url"];
1543
+ }
1544
+ async connectedCallback() {
1545
+ try {
1546
+ this.hasAttribute("tabindex") || this.setAttribute("tabindex", "0"), this.renderLoading(), await this.initialize();
1547
+ } catch (e) {
1548
+ this.showError("Failed to initialize editor", e);
1549
+ }
1550
+ }
1551
+ disconnectedCallback() {
1552
+ this.cleanup();
1553
+ }
1554
+ attributeChangedCallback(e, t, i) {
1555
+ t !== i && e === "template-id" && this.isReady && i && this.loadTemplate(i);
1556
+ }
1557
+ // ─── Public API ───
1558
+ getDesign() {
1559
+ if (!this.currentTemplate) throw new Error("No design loaded");
1560
+ return {
1561
+ templateId: this.currentTemplate.metadata.id,
1562
+ engineVersion: de,
1563
+ contents: this.contentManager.toJSON(),
1564
+ userData: {}
1565
+ };
1566
+ }
1567
+ setDesign(e) {
1568
+ if (e.templateId !== this.currentTemplate?.metadata.id)
1569
+ throw new Error("Design template ID does not match current template");
1570
+ this.contentManager.fromJSON(e.contents), this.syncEngineContents(), this.stateManager.setState({
1571
+ contents: this.contentManager.toJSON(),
1572
+ isDirty: !0
1573
+ });
1574
+ }
1575
+ undo() {
1576
+ this.stateManager.undo() && this.syncFromState();
1577
+ }
1578
+ redo() {
1579
+ this.stateManager.redo() && this.syncFromState();
1580
+ }
1581
+ clear() {
1582
+ this.contentManager.reset(), this.syncEngineContents(), this.stateManager.setState({
1583
+ contents: this.contentManager.toJSON(),
1584
+ selectedAreaId: null,
1585
+ isDirty: !1
1586
+ }), this.engine.setSelectedArea(null), this.inputPanel.setSelectedArea(null), this.engine.fitToView(), this.stateManager.update({
1587
+ zoom: this.engine.getZoom(),
1588
+ pan: this.engine.getPan()
1589
+ });
1590
+ }
1591
+ selectArea(e) {
1592
+ this.handleAreaSelect(e);
1593
+ }
1594
+ getSelectedAreaId() {
1595
+ return this.stateManager.getState().selectedAreaId;
1596
+ }
1597
+ setAreaContent(e, t) {
1598
+ t.type === "text" ? this.contentManager.setTextContent(e, t) : this.contentManager.setImageContent(e, t.dataUrl, t.filename), this.syncEngineContents(), this.saveContentState();
1599
+ }
1600
+ getAreaContent(e) {
1601
+ return this.contentManager.getContent(e);
1602
+ }
1603
+ async finalize() {
1604
+ const e = this.engine.checkSafeAreaViolations();
1605
+ if (e.length > 0) {
1606
+ const r = this.currentTemplate?.areas || [], s = e.map((a) => r.find((o) => o.id === a)?.name || a).join(", ");
1607
+ if (!await this.showConfirmation(
1608
+ "Content outside safe area",
1609
+ `Content in ${s} extends beyond the safe area. It may be trimmed during printing. Continue?`,
1610
+ "Cancel",
1611
+ "Proceed Anyway"
1612
+ ))
1613
+ throw new Error("Finalization cancelled by user");
1614
+ }
1615
+ const t = this.getDesign(), i = this.getAttribute("store-id") || "demo-shop", n = await this.apiClient.finalizeDesign(i, t);
1616
+ return this.dispatchEvent(
1617
+ new CustomEvent("customizer:finalize", {
1618
+ detail: {
1619
+ designId: n.designId,
1620
+ proofUrl: n.proofUrl,
1621
+ templateId: t.templateId,
1622
+ designJson: t,
1623
+ status: n.status
1624
+ },
1625
+ bubbles: !0,
1626
+ composed: !0
1627
+ })
1628
+ ), n;
1629
+ }
1630
+ // ─── Initialization ───
1631
+ renderLoading() {
1632
+ const e = document.createElement("style");
1633
+ e.textContent = F, this.shadow.appendChild(e);
1634
+ const t = c(
1635
+ "div",
1636
+ { class: "loading-container" },
1637
+ c("div", { class: "loading-spinner" })
1638
+ );
1639
+ this.shadow.appendChild(t);
1640
+ }
1641
+ async initialize() {
1642
+ const e = this.getAttribute("api-url") || "http://localhost:4000/public";
1643
+ this.apiClient = new J(e);
1644
+ const t = this.getAttribute("template-id");
1645
+ if (!t)
1646
+ throw new Error("template-id attribute is required");
1647
+ await this.loadTemplate(t), this.isReady = !0, this.dispatchEvent(new CustomEvent("ready", { bubbles: !0, composed: !0 }));
1648
+ }
1649
+ async loadTemplate(e) {
1650
+ if (this.loadingTemplateId !== e) {
1651
+ this.loadingTemplateId = e;
1652
+ try {
1653
+ const t = await this.apiClient.getTemplate(e);
1654
+ if (this.loadingTemplateId !== e) return;
1655
+ this.currentTemplate = t;
1656
+ const i = t.areas || [];
1657
+ this.contentManager = new X(i);
1658
+ const n = {
1659
+ template: t,
1660
+ contents: this.contentManager.toJSON(),
1661
+ selectedAreaId: null,
1662
+ zoom: 1,
1663
+ pan: { x: 0, y: 0 },
1664
+ isDirty: !1,
1665
+ warnings: []
1666
+ };
1667
+ this.stateManager = new j(n), this.buildUI(t, i), this.engine = new B(this.canvas), this.engine.setArtboard({
1668
+ width: t.artboard.width,
1669
+ height: t.artboard.height,
1670
+ backgroundColor: t.artboard.backgroundColor ?? "#ffffff"
1671
+ }), this.engine.setAreas(i), this.engine.setContents(this.contentManager.getContents()), t.backgroundImage && this.engine.setBackgroundImage(t.backgroundImage);
1672
+ const r = t.artboard.safeArea ?? t.safeArea;
1673
+ r && this.engine.setSafeArea(r), this.engine.start(), this.engine.fitToView(), this.stateManager.update({
1674
+ zoom: this.engine.getZoom(),
1675
+ pan: this.engine.getPan()
1676
+ }), this.zoomToolbar.setZoom(this.engine.getZoom()), this.interaction = new G(
1677
+ this.canvas,
1678
+ this.engine,
1679
+ () => this.contentManager.getContents(),
1680
+ () => this.stateManager.getState().selectedAreaId
1681
+ ), this.wireInteractionEvents(), this.subscriptions.push(
1682
+ this.contentManager.subscribe(() => {
1683
+ this.syncEngineContents(), this.inputPanel.setContents(this.contentManager.getContents());
1684
+ })
1685
+ ), this.keyboard = new ne(this), this.setupKeyboardShortcuts(), this.inputPanel.setContents(this.contentManager.getContents());
1686
+ const s = this.getAttribute("store-id");
1687
+ s && this.apiClient.getStorageUsage(s).then((a) => {
1688
+ this.storageUsage = a, a.usagePercent >= 100 ? this.showToast("Storage quota exceeded. Upgrade your plan for more storage.", "error") : a.usagePercent >= 80 && this.showToast(`Storage ${a.usagePercent}% full. Consider upgrading your plan.`, "warning");
1689
+ }).catch(() => {
1690
+ }), this.loadingTemplateId = null;
1691
+ } catch (t) {
1692
+ throw this.loadingTemplateId = null, new Error(`Failed to load template: ${t.message}`);
1693
+ }
1694
+ }
1695
+ }
1696
+ buildUI(e, t) {
1697
+ this.shadow.innerHTML = "";
1698
+ const i = document.createElement("style");
1699
+ i.textContent = F, this.shadow.appendChild(i), this.container = c("div", { class: "editor-container" }), this.zoomToolbar = new K(), this.subscriptions.push(this.zoomToolbar.on("zoom-in", () => this.handleZoomIn())), this.subscriptions.push(this.zoomToolbar.on("zoom-out", () => this.handleZoomOut())), this.subscriptions.push(this.zoomToolbar.on("zoom-fit", () => this.handleZoomFit()));
1700
+ const n = c("div", { class: "canvas-area" });
1701
+ n.appendChild(this.zoomToolbar.getElement()), this.canvasWrapper = c("div", { class: "canvas-wrapper" }), this.canvas = document.createElement("canvas"), this.canvas.className = "editor-canvas", this.canvasWrapper.appendChild(this.canvas), n.appendChild(this.canvasWrapper), this.inputPanel = new ie(t), this.wireInputPanelEvents();
1702
+ const r = c("div", { class: "editor-main" });
1703
+ r.appendChild(n), r.appendChild(this.inputPanel.getBackdrop()), r.appendChild(this.inputPanel.getElement()), this.container.appendChild(r), this.shadow.appendChild(this.container), this.resizeObserver = new ResizeObserver((s) => {
1704
+ for (const a of s)
1705
+ a.target === this.canvasWrapper && this.handleCanvasResize(a.contentRect), a.target === this.container && this.handleLayoutResize(a.contentRect);
1706
+ }), this.resizeObserver.observe(this.canvasWrapper), this.resizeObserver.observe(this.container);
1707
+ }
1708
+ // ─── Event Wiring ───
1709
+ wireInteractionEvents() {
1710
+ this.subscriptions.push(
1711
+ this.interaction.on("area:select", ({ areaId: e }) => {
1712
+ this.handleAreaSelect(e);
1713
+ })
1714
+ ), this.subscriptions.push(
1715
+ this.interaction.on("content:drag", ({ areaId: e, offset: t }) => {
1716
+ this.contentManager.setContentOffset(e, t), this.saveContentState();
1717
+ })
1718
+ ), this.subscriptions.push(
1719
+ this.interaction.on("content:scale", ({ areaId: e, scale: t }) => {
1720
+ this.contentManager.setImageScale(e, t), this.saveContentState();
1721
+ })
1722
+ ), this.subscriptions.push(
1723
+ this.interaction.on("content:rotate", ({ areaId: e, rotation: t }) => {
1724
+ this.contentManager.setImageRotation(e, t), this.saveContentState();
1725
+ })
1726
+ ), this.subscriptions.push(
1727
+ this.interaction.on("zoom", ({ zoom: e }) => {
1728
+ this.engine.setZoom(e);
1729
+ const t = this.engine.getCenteredPan(e);
1730
+ t && this.engine.setPan(t), this.stateManager.update({ zoom: this.engine.getZoom(), pan: this.engine.getPan() }), this.zoomToolbar.setZoom(this.engine.getZoom());
1731
+ })
1732
+ ), this.subscriptions.push(
1733
+ this.interaction.on("pan", ({ pan: e }) => {
1734
+ this.engine.setPan(e), this.stateManager.update({ pan: e });
1735
+ })
1736
+ ), this.subscriptions.push(
1737
+ this.interaction.on("cursor", ({ cursor: e }) => {
1738
+ this.canvas.style.cursor = e;
1739
+ })
1740
+ ), this.subscriptions.push(
1741
+ this.interaction.on("context-menu", ({ areaId: e, clientX: t, clientY: i }) => {
1742
+ this.handleContextMenu(e, t, i);
1743
+ })
1744
+ );
1745
+ }
1746
+ wireInputPanelEvents() {
1747
+ this.subscriptions.push(
1748
+ this.inputPanel.on("text:change", ({ areaId: e, updates: t }) => {
1749
+ this.contentManager.setTextContent(e, t), this.saveContentState();
1750
+ })
1751
+ ), this.subscriptions.push(
1752
+ this.inputPanel.on("image:change", async ({ areaId: e, dataUrl: t, filename: i }) => {
1753
+ const n = this.contentManager.getContents();
1754
+ let r = 0;
1755
+ for (const [a, o] of n)
1756
+ o.type === "image" && a !== e && r++;
1757
+ if (r >= 10) {
1758
+ this.showToast("Maximum of 10 images per design reached", "error");
1759
+ return;
1760
+ }
1761
+ if (await this.refreshStorageUsage(), this.storageUsage && this.storageUsage.usagePercent >= 100) {
1762
+ this.showToast("Storage quota exceeded. Upgrade your plan for more storage.", "error");
1763
+ return;
1764
+ }
1765
+ const s = this.currentTemplate?.areas.find((a) => a.id === e);
1766
+ if (s && this.currentTemplate) {
1767
+ const a = this.currentTemplate.print.targetDpi;
1768
+ try {
1769
+ const o = new Image(), d = await new Promise((g, m) => {
1770
+ o.onload = () => g({ width: o.naturalWidth, height: o.naturalHeight }), o.onerror = () => m(new Error("Failed to load image")), o.src = t;
1771
+ }), l = s.location.width / a, h = s.location.height / a, u = oe(
1772
+ d.width,
1773
+ d.height,
1774
+ l,
1775
+ h,
1776
+ a
1777
+ );
1778
+ if (u.actualDPI < 150) {
1779
+ if (!await this.showConfirmation(
1780
+ "Low resolution detected",
1781
+ `This image is ${u.actualDPI} DPI at print size. Print may appear blurry. Recommended: ${a} DPI.`,
1782
+ "Cancel",
1783
+ "Use Anyway"
1784
+ )) return;
1785
+ } else u.actualDPI < a && (this.showToast(
1786
+ `Image resolution is ${u.actualDPI} DPI (${a} recommended)`,
1787
+ "warning"
1788
+ ), this.stateManager.update({
1789
+ warnings: [
1790
+ ...this.stateManager.getState().warnings,
1791
+ { type: "dpi", message: `${u.actualDPI} DPI`, areaId: e }
1792
+ ]
1793
+ }));
1794
+ } catch {
1795
+ }
1796
+ }
1797
+ if (this.contentManager.setImageContent(e, t, i), this.saveContentState(), this.storageUsage) {
1798
+ const a = Math.round(t.length * 0.75);
1799
+ this.storageUsage.storageUsed += a, this.storageUsage.usagePercent = this.storageUsage.storageQuota > 0 ? Math.round(this.storageUsage.storageUsed / this.storageUsage.storageQuota * 100) : 0;
1800
+ }
1801
+ })
1802
+ ), this.subscriptions.push(
1803
+ this.inputPanel.on("validation:error", ({ message: e }) => {
1804
+ this.showToast(e, "error");
1805
+ })
1806
+ ), this.subscriptions.push(
1807
+ this.inputPanel.on("clear", ({ areaId: e }) => {
1808
+ this.contentManager.clearContent(e), this.saveContentState();
1809
+ })
1810
+ ), this.subscriptions.push(
1811
+ this.inputPanel.on("select", ({ areaId: e }) => {
1812
+ this.handleAreaSelect(e);
1813
+ })
1814
+ ), this.subscriptions.push(
1815
+ this.inputPanel.on("offset:change", ({ areaId: e, offset: t }) => {
1816
+ this.contentManager.setContentOffset(e, t), this.saveContentState();
1817
+ })
1818
+ ), this.subscriptions.push(
1819
+ this.inputPanel.on("scale:change", ({ areaId: e, scale: t }) => {
1820
+ this.contentManager.setImageScale(e, t), this.saveContentState();
1821
+ })
1822
+ ), this.subscriptions.push(
1823
+ this.inputPanel.on("rotation:change", ({ areaId: e, rotation: t }) => {
1824
+ this.contentManager.setImageRotation(e, t), this.saveContentState();
1825
+ })
1826
+ ), this.subscriptions.push(
1827
+ this.inputPanel.on("dismiss", () => {
1828
+ this.handleAreaSelect(null);
1829
+ })
1830
+ );
1831
+ }
1832
+ // ─── Handlers ───
1833
+ handleAreaSelect(e, t = !1) {
1834
+ this.stateManager.update({ selectedAreaId: e }), this.engine.setSelectedArea(e), this.inputPanel.setSelectedArea(t ? e : null), e && document.activeElement !== this && this.focus(), this.dispatchEvent(
1835
+ new CustomEvent("area:select", {
1836
+ detail: { areaId: e },
1837
+ bubbles: !0,
1838
+ composed: !0
1839
+ })
1840
+ );
1841
+ }
1842
+ handleContextMenu(e, t, i) {
1843
+ this.handleAreaSelect(e, !!e);
1844
+ }
1845
+ handleZoomIn() {
1846
+ const e = this.engine.getZoom(), t = Math.min(e * 1.25, 5), i = this.engine.getCenteredPan(t);
1847
+ this.engine.setZoom(t), i && this.engine.setPan(i), this.stateManager.update({ zoom: this.engine.getZoom(), pan: this.engine.getPan() }), this.zoomToolbar.setZoom(this.engine.getZoom());
1848
+ }
1849
+ handleZoomOut() {
1850
+ const e = this.engine.getZoom(), t = Math.max(e * 0.8, 0.1), i = this.engine.getCenteredPan(t);
1851
+ this.engine.setZoom(t), i && this.engine.setPan(i), this.stateManager.update({ zoom: this.engine.getZoom(), pan: this.engine.getPan() }), this.zoomToolbar.setZoom(this.engine.getZoom());
1852
+ }
1853
+ handleZoomFit() {
1854
+ this.engine.fitToView(), this.stateManager.update({ zoom: this.engine.getZoom(), pan: this.engine.getPan() }), this.zoomToolbar.setZoom(this.engine.getZoom());
1855
+ }
1856
+ handleCanvasResize(e) {
1857
+ const { width: t, height: i } = e;
1858
+ if (t === 0 || i === 0) return;
1859
+ const n = window.devicePixelRatio || 1;
1860
+ this.canvas.width = t * n, this.canvas.height = i * n, this.canvas.style.width = `${t}px`, this.canvas.style.height = `${i}px`, this.engine && (this.engine.fitToView(), this.stateManager.update({ zoom: this.engine.getZoom(), pan: this.engine.getPan() }), this.zoomToolbar.setZoom(this.engine.getZoom()));
1861
+ }
1862
+ handleLayoutResize(e) {
1863
+ const t = e.width < le;
1864
+ t !== this.isMobileLayout && (this.isMobileLayout = t, this.container.classList.toggle("layout-desktop", !t), this.container.classList.toggle("layout-mobile", t), this.inputPanel.setMobile(t));
1865
+ }
1866
+ // ─── State Management ───
1867
+ async refreshStorageUsage() {
1868
+ if (Date.now() - this.storageUsageLastRefresh < 3e4) return;
1869
+ const e = this.getAttribute("store-id");
1870
+ if (e)
1871
+ try {
1872
+ this.storageUsage = await this.apiClient.getStorageUsage(e), this.storageUsageLastRefresh = Date.now();
1873
+ } catch {
1874
+ }
1875
+ }
1876
+ saveContentState() {
1877
+ this.stateManager.setState({
1878
+ contents: this.contentManager.toJSON(),
1879
+ isDirty: !0
1880
+ }), this.engine.checkSafeAreaViolations(), this.emitChange();
1881
+ }
1882
+ syncEngineContents() {
1883
+ this.engine?.setContents(this.contentManager.getContents());
1884
+ }
1885
+ syncFromState() {
1886
+ const e = this.stateManager.getState();
1887
+ this.contentManager.setContentsQuiet(e.contents), this.syncEngineContents(), this.inputPanel.setContents(this.contentManager.getContents()), this.engine.setSelectedArea(e.selectedAreaId), this.inputPanel.setSelectedArea(e.selectedAreaId);
1888
+ }
1889
+ // ─── Keyboard ───
1890
+ setupKeyboardShortcuts() {
1891
+ const e = ae();
1892
+ this.keyboard.register({
1893
+ key: "z",
1894
+ [e]: !0,
1895
+ handler: () => this.undo()
1896
+ }), this.keyboard.register({
1897
+ key: "z",
1898
+ [e]: !0,
1899
+ shift: !0,
1900
+ handler: () => this.redo()
1901
+ }), this.keyboard.register({
1902
+ key: "Escape",
1903
+ handler: () => this.handleAreaSelect(null)
1904
+ });
1905
+ }
1906
+ // ─── Toast & Modal ───
1907
+ showToast(e, t = "info") {
1908
+ const i = c("div", { class: `editor-toast editor-toast-${t}` }, e);
1909
+ this.shadow.appendChild(i), setTimeout(() => i.remove(), 4e3);
1910
+ }
1911
+ showConfirmation(e, t, i = "Cancel", n = "Continue") {
1912
+ return new Promise((r) => {
1913
+ const s = c("div", { class: "editor-modal-overlay" }), a = c("div", { class: "editor-modal" });
1914
+ a.appendChild(c("div", { class: "editor-modal-title" }, e)), a.appendChild(c("div", { class: "editor-modal-body" }, t));
1915
+ const o = c("div", { class: "editor-modal-actions" }), d = c("button", { class: "editor-modal-btn" }, i);
1916
+ d.addEventListener("click", () => {
1917
+ s.remove(), r(!1);
1918
+ });
1919
+ const l = c("button", { class: "editor-modal-btn editor-modal-btn-primary" }, n);
1920
+ l.addEventListener("click", () => {
1921
+ s.remove(), r(!0);
1922
+ }), o.appendChild(d), o.appendChild(l), a.appendChild(o), s.appendChild(a), s.addEventListener("click", (h) => {
1923
+ h.target === s && (s.remove(), r(!1));
1924
+ }), this.shadow.appendChild(s);
1925
+ });
1926
+ }
1927
+ // ─── Error / Cleanup ───
1928
+ showError(e, t) {
1929
+ this.shadow.innerHTML = "";
1930
+ const i = document.createElement("style");
1931
+ i.textContent = F, this.shadow.appendChild(i);
1932
+ const n = c("div", { class: "error-container" });
1933
+ n.appendChild(c("div", { class: "error-title" }, e)), n.appendChild(c("div", { class: "error-message" }, t?.message ?? "")), this.shadow.appendChild(n), this.dispatchEvent(
1934
+ new CustomEvent("error", {
1935
+ detail: { message: e, error: t },
1936
+ bubbles: !0,
1937
+ composed: !0
1938
+ })
1939
+ );
1940
+ }
1941
+ cleanup() {
1942
+ for (const e of this.subscriptions)
1943
+ e();
1944
+ this.subscriptions = [], this.resizeObserver && (this.resizeObserver.disconnect(), this.resizeObserver = null), this.engine?.destroy(), this.interaction?.destroy(), this.keyboard?.destroy(), this.isReady = !1, this.currentTemplate = null;
1945
+ }
1946
+ }
1947
+ customElements.get("customizer-editor") || customElements.define("customizer-editor", he);
1948
+ function ce(f, e) {
1949
+ const t = typeof f == "string" ? document.querySelector(f) : f;
1950
+ if (!t)
1951
+ throw new Error(
1952
+ `Container not found: ${typeof f == "string" ? f : "provided element is null"}`
1953
+ );
1954
+ const i = document.createElement("customizer-editor");
1955
+ i.setAttribute("template-id", e.templateId), e.theme && i.setAttribute("theme", e.theme), e.mode && i.setAttribute("mode", e.mode), e.className && i.classList.add(e.className), i.style.width = "100%", i.style.height = "100%";
1956
+ const n = [], r = (a, o) => {
1957
+ i.addEventListener(a, o), n.push({ event: a, handler: o });
1958
+ };
1959
+ t.innerHTML = "", t.appendChild(i);
1960
+ const s = {
1961
+ getDesign() {
1962
+ if (typeof i.getDesign != "function")
1963
+ throw new Error("Editor not ready: getDesign method not available");
1964
+ return i.getDesign();
1965
+ },
1966
+ setDesign(a) {
1967
+ if (typeof i.setDesign != "function")
1968
+ throw new Error("Editor not ready: setDesign method not available");
1969
+ i.setDesign(a);
1970
+ },
1971
+ undo() {
1972
+ if (typeof i.undo != "function")
1973
+ throw new Error("Editor not ready: undo method not available");
1974
+ i.undo();
1975
+ },
1976
+ redo() {
1977
+ if (typeof i.redo != "function")
1978
+ throw new Error("Editor not ready: redo method not available");
1979
+ i.redo();
1980
+ },
1981
+ canUndo() {
1982
+ return typeof i.canUndo != "function" ? !1 : i.canUndo();
1983
+ },
1984
+ canRedo() {
1985
+ return typeof i.canRedo != "function" ? !1 : i.canRedo();
1986
+ },
1987
+ async finalize() {
1988
+ const a = this.getDesign(), o = e.apiUrl || "https://api.varianta.io";
1989
+ try {
1990
+ const d = await fetch(`${o}/public/finalize`, {
1991
+ method: "POST",
1992
+ headers: {
1993
+ "Content-Type": "application/json"
1994
+ },
1995
+ body: JSON.stringify({
1996
+ templateId: e.templateId,
1997
+ design: a
1998
+ })
1999
+ });
2000
+ if (!d.ok) {
2001
+ const h = await d.json().catch(() => ({
2002
+ message: "Finalization failed"
2003
+ }));
2004
+ throw new Error(h.message || "Finalization failed");
2005
+ }
2006
+ const l = await d.json();
2007
+ return e.onFinalize && e.onFinalize(l), l;
2008
+ } catch (d) {
2009
+ const l = {
2010
+ code: "FINALIZE_ERROR",
2011
+ message: d instanceof Error ? d.message : "Unknown error",
2012
+ details: d
2013
+ };
2014
+ throw e.onError && e.onError(l), d;
2015
+ }
2016
+ },
2017
+ addTextLayer(a) {
2018
+ if (typeof i.addTextLayer != "function")
2019
+ throw new Error("Editor not ready: addTextLayer method not available");
2020
+ i.addTextLayer(a);
2021
+ },
2022
+ async addImageLayer(a) {
2023
+ if (typeof i.addImageLayer != "function")
2024
+ throw new Error("Editor not ready: addImageLayer method not available");
2025
+ return i.addImageLayer(a);
2026
+ },
2027
+ removeLayer(a) {
2028
+ if (typeof i.removeLayer != "function")
2029
+ throw new Error("Editor not ready: removeLayer method not available");
2030
+ i.removeLayer(a);
2031
+ },
2032
+ selectLayer(a) {
2033
+ if (typeof i.selectLayer != "function")
2034
+ throw new Error("Editor not ready: selectLayer method not available");
2035
+ i.selectLayer(a);
2036
+ },
2037
+ getSelectedLayerId() {
2038
+ return typeof i.getSelectedLayerId != "function" ? null : i.getSelectedLayerId();
2039
+ },
2040
+ setTheme(a) {
2041
+ i.setAttribute("theme", a);
2042
+ },
2043
+ setMode(a) {
2044
+ i.setAttribute("mode", a);
2045
+ },
2046
+ destroy() {
2047
+ n.forEach(({ event: a, handler: o }) => {
2048
+ i.removeEventListener(a, o);
2049
+ }), i.parentNode && i.parentNode.removeChild(i);
2050
+ },
2051
+ getElement() {
2052
+ return i;
2053
+ }
2054
+ };
2055
+ return e.onReady && r("ready", (() => {
2056
+ e.onReady?.(s);
2057
+ })), e.onChange && r("change", ((a) => {
2058
+ const o = a.detail.design;
2059
+ e.onChange?.(o);
2060
+ })), e.onLayerSelect && r("layer:select", ((a) => {
2061
+ e.onLayerSelect?.(a.detail.layerId);
2062
+ })), e.onLayerAdd && r("layer:add", ((a) => {
2063
+ e.onLayerAdd?.(a.detail.layerId);
2064
+ })), e.onLayerRemove && r("layer:remove", ((a) => {
2065
+ e.onLayerRemove?.(a.detail.layerId);
2066
+ })), e.onLayerUpdate && r("layer:update", ((a) => {
2067
+ e.onLayerUpdate?.(a.detail.layerId);
2068
+ })), e.onError && r("error", ((a) => {
2069
+ const o = a.detail.error;
2070
+ e.onError?.(o);
2071
+ })), e.initialDesign && i.addEventListener(
2072
+ "ready",
2073
+ () => {
2074
+ e.initialDesign && i.setDesign(e.initialDesign);
2075
+ },
2076
+ { once: !0 }
2077
+ ), e.debug && (console.log("[Customizer SDK] Initialized with options:", e), console.log("[Customizer SDK] Instance:", s)), s;
2078
+ }
2079
+ const ue = $(
2080
+ (f, e) => {
2081
+ const {
2082
+ templateId: t,
2083
+ apiUrl: i,
2084
+ theme: n = "light",
2085
+ mode: r = "edit",
2086
+ debug: s = !1,
2087
+ className: a,
2088
+ style: o,
2089
+ initialDesign: d,
2090
+ onReady: l,
2091
+ onChange: h,
2092
+ onLayerSelect: u,
2093
+ onLayerAdd: g,
2094
+ onLayerRemove: m,
2095
+ onLayerUpdate: b,
2096
+ onError: y,
2097
+ onFinalize: v
2098
+ } = f, w = k(null), p = k(null), C = k({
2099
+ onReady: l,
2100
+ onChange: h,
2101
+ onLayerSelect: u,
2102
+ onLayerAdd: g,
2103
+ onLayerRemove: m,
2104
+ onLayerUpdate: b,
2105
+ onError: y,
2106
+ onFinalize: v
2107
+ });
2108
+ return L(() => {
2109
+ C.current = {
2110
+ onReady: l,
2111
+ onChange: h,
2112
+ onLayerSelect: u,
2113
+ onLayerAdd: g,
2114
+ onLayerRemove: m,
2115
+ onLayerUpdate: b,
2116
+ onError: y,
2117
+ onFinalize: v
2118
+ };
2119
+ }, [l, h, u, g, m, b, y, v]), L(() => {
2120
+ if (!w.current) return;
2121
+ const I = {
2122
+ templateId: t,
2123
+ apiUrl: i,
2124
+ theme: n,
2125
+ mode: r,
2126
+ debug: s,
2127
+ initialDesign: d,
2128
+ // Use callback refs to always get latest callbacks
2129
+ onReady: () => C.current.onReady?.(),
2130
+ onChange: (x) => C.current.onChange?.(x),
2131
+ onLayerSelect: (x) => C.current.onLayerSelect?.(x),
2132
+ onLayerAdd: (x) => C.current.onLayerAdd?.(x),
2133
+ onLayerRemove: (x) => C.current.onLayerRemove?.(x),
2134
+ onLayerUpdate: (x) => C.current.onLayerUpdate?.(x),
2135
+ onError: (x) => C.current.onError?.(x),
2136
+ onFinalize: (x) => C.current.onFinalize?.(x)
2137
+ };
2138
+ try {
2139
+ const x = ce(w.current, I);
2140
+ p.current = x;
2141
+ } catch (x) {
2142
+ console.error("[Customizer React] Failed to initialize:", x), C.current.onError?.({
2143
+ code: "INIT_ERROR",
2144
+ message: x instanceof Error ? x.message : "Unknown error",
2145
+ details: x
2146
+ });
2147
+ }
2148
+ return () => {
2149
+ p.current && (p.current.destroy(), p.current = null);
2150
+ };
2151
+ }, [t, i, s, d]), L(() => {
2152
+ p.current && p.current.setTheme(n);
2153
+ }, [n]), L(() => {
2154
+ p.current && p.current.setMode(r);
2155
+ }, [r]), q(
2156
+ e,
2157
+ () => ({
2158
+ getDesign: () => {
2159
+ if (!p.current)
2160
+ throw new Error("Editor not initialized");
2161
+ return p.current.getDesign();
2162
+ },
2163
+ setDesign: (I) => {
2164
+ if (!p.current)
2165
+ throw new Error("Editor not initialized");
2166
+ p.current.setDesign(I);
2167
+ },
2168
+ undo: () => {
2169
+ if (!p.current)
2170
+ throw new Error("Editor not initialized");
2171
+ p.current.undo();
2172
+ },
2173
+ redo: () => {
2174
+ if (!p.current)
2175
+ throw new Error("Editor not initialized");
2176
+ p.current.redo();
2177
+ },
2178
+ canUndo: () => p.current ? p.current.canUndo() : !1,
2179
+ canRedo: () => p.current ? p.current.canRedo() : !1,
2180
+ finalize: async () => {
2181
+ if (!p.current)
2182
+ throw new Error("Editor not initialized");
2183
+ return p.current.finalize();
2184
+ },
2185
+ addTextLayer: (I) => {
2186
+ if (!p.current)
2187
+ throw new Error("Editor not initialized");
2188
+ p.current.addTextLayer(I);
2189
+ },
2190
+ addImageLayer: async (I) => {
2191
+ if (!p.current)
2192
+ throw new Error("Editor not initialized");
2193
+ return p.current.addImageLayer(I);
2194
+ },
2195
+ removeLayer: (I) => {
2196
+ if (!p.current)
2197
+ throw new Error("Editor not initialized");
2198
+ p.current.removeLayer(I);
2199
+ },
2200
+ selectLayer: (I) => {
2201
+ if (!p.current)
2202
+ throw new Error("Editor not initialized");
2203
+ p.current.selectLayer(I);
2204
+ },
2205
+ getSelectedLayerId: () => p.current ? p.current.getSelectedLayerId() : null,
2206
+ setTheme: (I) => {
2207
+ if (!p.current)
2208
+ throw new Error("Editor not initialized");
2209
+ p.current.setTheme(I);
2210
+ },
2211
+ setMode: (I) => {
2212
+ if (!p.current)
2213
+ throw new Error("Editor not initialized");
2214
+ p.current.setMode(I);
2215
+ },
2216
+ destroy: () => {
2217
+ p.current && (p.current.destroy(), p.current = null);
2218
+ },
2219
+ getElement: () => {
2220
+ if (!p.current)
2221
+ throw new Error("Editor not initialized");
2222
+ return p.current.getElement();
2223
+ }
2224
+ }),
2225
+ []
2226
+ // Empty deps is OK - we always read from instanceRef.current
2227
+ ), /* @__PURE__ */ Z(
2228
+ "div",
2229
+ {
2230
+ ref: w,
2231
+ className: a,
2232
+ style: {
2233
+ width: "100%",
2234
+ height: "100%",
2235
+ ...o
2236
+ }
2237
+ }
2238
+ );
2239
+ }
2240
+ );
2241
+ ue.displayName = "Customizer";
2242
+ function me(f = {}) {
2243
+ const {
2244
+ autoSave: e = !1,
2245
+ autoSaveKey: t = "customizer-design",
2246
+ autoSaveDebounce: i = 1e3
2247
+ } = f, n = k(null), [r, s] = S(null), [a, o] = S(!1), [d, l] = S(!1), [h, u] = S(null), [g, m] = S(!1), [b, y] = S(
2248
+ null
2249
+ ), v = k();
2250
+ L(() => {
2251
+ if (!(!e || !r))
2252
+ return v.current && clearTimeout(v.current), v.current = setTimeout(() => {
2253
+ try {
2254
+ localStorage.setItem(t, JSON.stringify(r));
2255
+ } catch (E) {
2256
+ console.error("[useCustomizer] Auto-save failed:", E);
2257
+ }
2258
+ }, i), () => {
2259
+ v.current && clearTimeout(v.current);
2260
+ };
2261
+ }, [r, e, t, i]);
2262
+ const w = M(() => {
2263
+ if (!n.current) return null;
2264
+ try {
2265
+ return n.current.getDesign();
2266
+ } catch (E) {
2267
+ return console.error("[useCustomizer] getDesign failed:", E), null;
2268
+ }
2269
+ }, []), p = M((E) => {
2270
+ if (n.current)
2271
+ try {
2272
+ n.current.setDesign(E), s(E);
2273
+ } catch (z) {
2274
+ console.error("[useCustomizer] setDesign failed:", z);
2275
+ }
2276
+ }, []), C = M(() => {
2277
+ if (n.current)
2278
+ try {
2279
+ n.current.undo(), o(n.current.canUndo()), l(n.current.canRedo());
2280
+ } catch (E) {
2281
+ console.error("[useCustomizer] undo failed:", E);
2282
+ }
2283
+ }, []), I = M(() => {
2284
+ if (n.current)
2285
+ try {
2286
+ n.current.redo(), o(n.current.canUndo()), l(n.current.canRedo());
2287
+ } catch (E) {
2288
+ console.error("[useCustomizer] redo failed:", E);
2289
+ }
2290
+ }, []), x = M(async () => {
2291
+ if (!n.current || g) return null;
2292
+ m(!0);
2293
+ try {
2294
+ const E = await n.current.finalize();
2295
+ return y(E), E;
2296
+ } catch (E) {
2297
+ return console.error("[useCustomizer] finalize failed:", E), null;
2298
+ } finally {
2299
+ m(!1);
2300
+ }
2301
+ }, [g]), R = M((E) => {
2302
+ if (n.current)
2303
+ try {
2304
+ n.current.addTextLayer(E);
2305
+ } catch (z) {
2306
+ console.error("[useCustomizer] addTextLayer failed:", z);
2307
+ }
2308
+ }, []), D = M(async (E) => {
2309
+ if (n.current)
2310
+ try {
2311
+ await n.current.addImageLayer(E);
2312
+ } catch (z) {
2313
+ console.error("[useCustomizer] addImageLayer failed:", z);
2314
+ }
2315
+ }, []), U = M((E) => {
2316
+ if (n.current)
2317
+ try {
2318
+ n.current.removeLayer(E);
2319
+ } catch (z) {
2320
+ console.error("[useCustomizer] removeLayer failed:", z);
2321
+ }
2322
+ }, []), N = M((E) => {
2323
+ if (n.current)
2324
+ try {
2325
+ n.current.selectLayer(E), u(E);
2326
+ } catch (z) {
2327
+ console.error("[useCustomizer] selectLayer failed:", z);
2328
+ }
2329
+ }, []);
2330
+ return {
2331
+ customizerRef: n,
2332
+ design: r,
2333
+ canUndo: a,
2334
+ canRedo: d,
2335
+ selectedLayerId: h,
2336
+ isFinalizing: g,
2337
+ finalizeResult: b,
2338
+ getDesign: w,
2339
+ setDesign: p,
2340
+ undo: C,
2341
+ redo: I,
2342
+ finalize: x,
2343
+ addTextLayer: R,
2344
+ addImageLayer: D,
2345
+ removeLayer: U,
2346
+ selectLayer: N
2347
+ };
2348
+ }
2349
+ export {
2350
+ ue as Customizer,
2351
+ ce as initCustomizer,
2352
+ me as useCustomizer
2353
+ };
2354
+ //# sourceMappingURL=index.esm.js.map