@printwithsynergy/artwork-pdf-editor 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +117 -0
  2. package/dist/components/DielineLibraryModal.d.ts +9 -0
  3. package/dist/components/DielineLibraryModal.d.ts.map +1 -0
  4. package/dist/components/DielineLibraryModal.js +107 -0
  5. package/dist/components/DielineLibraryModal.js.map +1 -0
  6. package/dist/components/EditorApp.d.ts +37 -0
  7. package/dist/components/EditorApp.d.ts.map +1 -0
  8. package/dist/components/EditorApp.js +89 -0
  9. package/dist/components/EditorApp.js.map +1 -0
  10. package/dist/components/EditorCanvas.d.ts +41 -0
  11. package/dist/components/EditorCanvas.d.ts.map +1 -0
  12. package/dist/components/EditorCanvas.js +852 -0
  13. package/dist/components/EditorCanvas.js.map +1 -0
  14. package/dist/components/FileDropZone.d.ts +6 -0
  15. package/dist/components/FileDropZone.d.ts.map +1 -0
  16. package/dist/components/FileDropZone.js +48 -0
  17. package/dist/components/FileDropZone.js.map +1 -0
  18. package/dist/components/LayersPanel.d.ts +12 -0
  19. package/dist/components/LayersPanel.d.ts.map +1 -0
  20. package/dist/components/LayersPanel.js +101 -0
  21. package/dist/components/LayersPanel.js.map +1 -0
  22. package/dist/components/MobileToolDrawer.d.ts +45 -0
  23. package/dist/components/MobileToolDrawer.d.ts.map +1 -0
  24. package/dist/components/MobileToolDrawer.js +164 -0
  25. package/dist/components/MobileToolDrawer.js.map +1 -0
  26. package/dist/components/ModeToggle.d.ts +8 -0
  27. package/dist/components/ModeToggle.d.ts.map +1 -0
  28. package/dist/components/ModeToggle.js +27 -0
  29. package/dist/components/ModeToggle.js.map +1 -0
  30. package/dist/components/PreflightPanel.d.ts +9 -0
  31. package/dist/components/PreflightPanel.d.ts.map +1 -0
  32. package/dist/components/PreflightPanel.js +59 -0
  33. package/dist/components/PreflightPanel.js.map +1 -0
  34. package/dist/components/SeparationsPanel.d.ts +9 -0
  35. package/dist/components/SeparationsPanel.d.ts.map +1 -0
  36. package/dist/components/SeparationsPanel.js +147 -0
  37. package/dist/components/SeparationsPanel.js.map +1 -0
  38. package/dist/components/TopBar.d.ts +57 -0
  39. package/dist/components/TopBar.d.ts.map +1 -0
  40. package/dist/components/TopBar.js +70 -0
  41. package/dist/components/TopBar.js.map +1 -0
  42. package/dist/data/dielines.json +186 -0
  43. package/dist/hooks/useEditorMode.d.ts +15 -0
  44. package/dist/hooks/useEditorMode.d.ts.map +1 -0
  45. package/dist/hooks/useEditorMode.js +51 -0
  46. package/dist/hooks/useEditorMode.js.map +1 -0
  47. package/dist/hooks/useIsMobile.d.ts +19 -0
  48. package/dist/hooks/useIsMobile.d.ts.map +1 -0
  49. package/dist/hooks/useIsMobile.js +34 -0
  50. package/dist/hooks/useIsMobile.js.map +1 -0
  51. package/dist/hooks/usePreflight.d.ts +23 -0
  52. package/dist/hooks/usePreflight.d.ts.map +1 -0
  53. package/dist/hooks/usePreflight.js +48 -0
  54. package/dist/hooks/usePreflight.js.map +1 -0
  55. package/dist/index.d.ts +23 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +23 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/lib/bleed.d.ts +27 -0
  60. package/dist/lib/bleed.d.ts.map +1 -0
  61. package/dist/lib/bleed.js +60 -0
  62. package/dist/lib/bleed.js.map +1 -0
  63. package/dist/lib/dieline-template.d.ts +33 -0
  64. package/dist/lib/dieline-template.d.ts.map +1 -0
  65. package/dist/lib/dieline-template.js +43 -0
  66. package/dist/lib/dieline-template.js.map +1 -0
  67. package/dist/lib/editor-config.d.ts +69 -0
  68. package/dist/lib/editor-config.d.ts.map +1 -0
  69. package/dist/lib/editor-config.js +66 -0
  70. package/dist/lib/editor-config.js.map +1 -0
  71. package/dist/lib/preflight/checks.d.ts +8 -0
  72. package/dist/lib/preflight/checks.d.ts.map +1 -0
  73. package/dist/lib/preflight/checks.js +276 -0
  74. package/dist/lib/preflight/checks.js.map +1 -0
  75. package/dist/lib/preflight/index.d.ts +2 -0
  76. package/dist/lib/preflight/index.d.ts.map +1 -0
  77. package/dist/lib/preflight/index.js +3 -0
  78. package/dist/lib/preflight/index.js.map +1 -0
  79. package/dist/lib/preflight/types.d.ts +24 -0
  80. package/dist/lib/preflight/types.d.ts.map +1 -0
  81. package/dist/lib/preflight/types.js +80 -0
  82. package/dist/lib/preflight/types.js.map +1 -0
  83. package/package.json +49 -0
@@ -0,0 +1,852 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ "use client";
3
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
4
+ import { PDFDocument } from "pdf-lib";
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ import { Ellipse, Image as KonvaImage, Layer, Line, Rect, Stage, Text, Transformer, } from "react-konva";
7
+ import { DEFAULT_BLEED_MM, formatBleed } from "../lib/bleed";
8
+ import { templateToInitialState } from "../lib/dieline-template";
9
+ import { DielineLibraryModal } from "./DielineLibraryModal";
10
+ import { LayersPanel } from "./LayersPanel";
11
+ import { MobileToolDrawer } from "./MobileToolDrawer";
12
+ import { SeparationsPanel } from "./SeparationsPanel";
13
+ // ── constants ─────────────────────────────────────────────────────────────────
14
+ const SERVICE_URL = (process.env.NEXT_PUBLIC_SERVICE_URL ?? "http://localhost:3001").replace(/\/$/, "");
15
+ const BRAND = "#fc5102";
16
+ const PANEL_BG = "#1a0f08";
17
+ const BORDER = "#3d1a00";
18
+ const MUTED = "#666";
19
+ // ── helper: load image element ────────────────────────────────────────────────
20
+ function loadImage(src) {
21
+ return new Promise((res, rej) => {
22
+ const img = new window.Image();
23
+ img.onload = () => res(img);
24
+ img.onerror = rej;
25
+ img.src = src;
26
+ });
27
+ }
28
+ // ── helper: get pointer relative to stage content (ignoring stage transform) ──
29
+ function stagePointer(stage) {
30
+ const pos = stage.getPointerPosition() ?? { x: 0, y: 0 };
31
+ return {
32
+ x: (pos.x - stage.x()) / stage.scaleX(),
33
+ y: (pos.y - stage.y()) / stage.scaleY(),
34
+ };
35
+ }
36
+ function ObjNode({ obj, selected, onSelect, onDragEnd, onTransformEnd, onDblClick }) {
37
+ const sharedProps = {
38
+ id: obj.id,
39
+ x: obj.x,
40
+ y: obj.y,
41
+ opacity: obj.opacity,
42
+ draggable: true,
43
+ onClick: onSelect,
44
+ onTap: onSelect,
45
+ onDblClick: onDblClick,
46
+ onDragEnd: (e) => onDragEnd(e.target.x(), e.target.y()),
47
+ onTransformEnd: (e) => {
48
+ const node = e.target;
49
+ const scaleX = node.scaleX();
50
+ const scaleY = node.scaleY();
51
+ node.scaleX(1);
52
+ node.scaleY(1);
53
+ onTransformEnd(node.x(), node.y(), Math.max(4, node.width() * scaleX), Math.max(4, node.height() * scaleY));
54
+ },
55
+ };
56
+ if (obj.type === "rect") {
57
+ return (_jsx(Rect, { ...sharedProps, width: obj.width, height: obj.height, fill: obj.fill, stroke: selected ? BRAND : obj.stroke, strokeWidth: selected ? Math.max(obj.strokeWidth, 1) : obj.strokeWidth }));
58
+ }
59
+ if (obj.type === "ellipse") {
60
+ return (_jsx(Ellipse, { ...sharedProps, radiusX: obj.width / 2, radiusY: obj.height / 2, offsetX: -obj.width / 2, offsetY: -obj.height / 2, fill: obj.fill, stroke: selected ? BRAND : obj.stroke, strokeWidth: selected ? Math.max(obj.strokeWidth, 1) : obj.strokeWidth }));
61
+ }
62
+ if (obj.type === "text") {
63
+ return (_jsx(Text, { ...sharedProps, text: obj.text ?? "Text", fontSize: obj.fontSize ?? 16, fill: obj.fill, width: obj.width }));
64
+ }
65
+ if (obj.type === "image" && obj.imageEl) {
66
+ return (_jsx(KonvaImage, { ...sharedProps, image: obj.imageEl, width: obj.width, height: obj.height, stroke: selected ? BRAND : obj.stroke, strokeWidth: selected ? 1 : obj.strokeWidth }));
67
+ }
68
+ return null;
69
+ }
70
+ // ── main component ─────────────────────────────────────────────────────────────
71
+ export function EditorCanvas({ file, report, demo = false, initialObjects, initialPageSize, mode = "basic", onModeChange, config, bleedMm: bleedMmProp = DEFAULT_BLEED_MM, isMobile = false, menuOpen = false, onMenuOpenChange, }) {
72
+ const containerRef = useRef(null);
73
+ const stageRef = useRef(null);
74
+ const trRef = useRef(null);
75
+ const [containerSize, setContainerSize] = useState({ width: 800, height: 600 });
76
+ const [pageSize, setPageSize] = useState(initialPageSize ?? { width: 595, height: 842 }); // A4 in pt
77
+ const [zoom, setZoom] = useState(1);
78
+ const [stagePos, setStagePos] = useState({ x: 0, y: 0 });
79
+ const [bleedMm, setBleedMm] = useState(bleedMmProp);
80
+ const [currentTemplate, setCurrentTemplate] = useState(null);
81
+ const [objects, setObjects] = useState(initialObjects ?? []);
82
+ const [history, setHistory] = useState([initialObjects ?? []]);
83
+ const [historyIdx, setHistoryIdx] = useState(0);
84
+ const [selectedId, setSelectedId] = useState(null);
85
+ const [tool, setTool] = useState("select");
86
+ const [drawing, setDrawing] = useState(null);
87
+ const [fillColor, setFillColor] = useState(BRAND);
88
+ const [strokeColor, setStrokeColor] = useState("transparent");
89
+ const [strokeWidth, setStrokeWidth] = useState(1);
90
+ const [opacity, setOpacity] = useState(1);
91
+ const [exportStatus, setExportStatus] = useState("idle");
92
+ const [exportJobId, setExportJobId] = useState(null);
93
+ const imageInputRef = useRef(null);
94
+ // Pro mode state — dieline modal + per-ink visibility filter for separations.
95
+ const [dielineOpen, setDielineOpen] = useState(false);
96
+ const [hiddenInks, setHiddenInks] = useState(new Set());
97
+ // ── sync bleed from prop ────────────────────────────────────────────────────
98
+ useEffect(() => {
99
+ setBleedMm(bleedMmProp);
100
+ }, [bleedMmProp]);
101
+ // ── recompute page + dieline when bleed changes ────────────────────────────
102
+ useEffect(() => {
103
+ if (!currentTemplate)
104
+ return;
105
+ const { objects: seeded, pageSize: newPageSize } = templateToInitialState(currentTemplate, bleedMm);
106
+ setPageSize(newPageSize);
107
+ setObjects((prev) => {
108
+ const dielineObj = seeded[0];
109
+ if (!dielineObj)
110
+ return prev;
111
+ return [dielineObj, ...prev.filter((o) => !/dieline/i.test(o.id))];
112
+ });
113
+ }, [bleedMm, currentTemplate]);
114
+ // ── container resize ────────────────────────────────────────────────────────
115
+ useEffect(() => {
116
+ const el = containerRef.current;
117
+ if (!el)
118
+ return;
119
+ const ro = new ResizeObserver((entries) => {
120
+ for (const e of entries) {
121
+ setContainerSize({ width: e.contentRect.width, height: e.contentRect.height });
122
+ }
123
+ });
124
+ ro.observe(el);
125
+ return () => ro.disconnect();
126
+ }, []);
127
+ // ── parse page size from uploaded file ─────────────────────────────────────
128
+ useEffect(() => {
129
+ if (!file)
130
+ return;
131
+ const isPdf = file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
132
+ if (isPdf) {
133
+ file
134
+ .arrayBuffer()
135
+ .then((buf) => PDFDocument.load(buf, { ignoreEncryption: true }))
136
+ .then((doc) => {
137
+ const page = doc.getPages()[0];
138
+ if (page) {
139
+ const { width, height } = page.getSize();
140
+ setPageSize({ width, height });
141
+ }
142
+ })
143
+ .catch(() => undefined);
144
+ }
145
+ else if (file.type.startsWith("image/")) {
146
+ const url = URL.createObjectURL(file);
147
+ const img = new window.Image();
148
+ img.onload = () => {
149
+ URL.revokeObjectURL(url);
150
+ const max = 600;
151
+ const s = Math.min(1, max / Math.max(img.naturalWidth, img.naturalHeight));
152
+ setPageSize({ width: img.naturalWidth * s, height: img.naturalHeight * s });
153
+ };
154
+ img.src = url;
155
+ }
156
+ }, [file]);
157
+ // ── fit to page when container or page size changes ─────────────────────────
158
+ const fitPage = useCallback((cw, ch, pw, ph) => {
159
+ if (cw <= 0 || ch <= 0 || pw <= 0 || ph <= 0)
160
+ return;
161
+ const pad = 80;
162
+ const s = Math.min((cw - pad * 2) / pw, (ch - pad * 2) / ph, 3);
163
+ const newZoom = Math.max(s, 0.05);
164
+ setZoom(newZoom);
165
+ setStagePos({ x: (cw - pw * newZoom) / 2, y: (ch - ph * newZoom) / 2 });
166
+ }, []);
167
+ useEffect(() => {
168
+ fitPage(containerSize.width, containerSize.height, pageSize.width, pageSize.height);
169
+ }, [pageSize, containerSize, fitPage]);
170
+ // ── transformer sync ────────────────────────────────────────────────────────
171
+ useEffect(() => {
172
+ const tr = trRef.current;
173
+ const stage = stageRef.current;
174
+ if (!tr || !stage)
175
+ return;
176
+ if (selectedId) {
177
+ const node = stage.findOne(`#${selectedId}`);
178
+ if (node) {
179
+ tr.nodes([node]);
180
+ tr.getLayer()?.batchDraw();
181
+ }
182
+ }
183
+ else {
184
+ tr.nodes([]);
185
+ tr.getLayer()?.batchDraw();
186
+ }
187
+ }, [selectedId]);
188
+ // ── history helpers ─────────────────────────────────────────────────────────
189
+ function commit(next) {
190
+ const newHist = history.slice(0, historyIdx + 1).concat([next]);
191
+ setHistory(newHist);
192
+ setHistoryIdx(newHist.length - 1);
193
+ setObjects(next);
194
+ }
195
+ function undo() {
196
+ const idx = Math.max(0, historyIdx - 1);
197
+ setHistoryIdx(idx);
198
+ setObjects(history[idx] ?? []);
199
+ setSelectedId(null);
200
+ }
201
+ function redo() {
202
+ const idx = Math.min(history.length - 1, historyIdx + 1);
203
+ setHistoryIdx(idx);
204
+ setObjects(history[idx] ?? []);
205
+ setSelectedId(null);
206
+ }
207
+ // ── keyboard shortcuts ──────────────────────────────────────────────────────
208
+ // biome-ignore lint/correctness/useExhaustiveDependencies: commit/undo/redo capture history via closure
209
+ useEffect(() => {
210
+ function onKey(e) {
211
+ const tag = e.target.tagName;
212
+ if (tag === "TEXTAREA" || tag === "INPUT")
213
+ return;
214
+ if (e.key === "Delete" || e.key === "Backspace") {
215
+ if (selectedId) {
216
+ commit(objects.filter((o) => o.id !== selectedId));
217
+ setSelectedId(null);
218
+ }
219
+ return;
220
+ }
221
+ if (e.key === "Escape") {
222
+ setSelectedId(null);
223
+ setTool("select");
224
+ return;
225
+ }
226
+ const mod = e.ctrlKey || e.metaKey;
227
+ if (mod && e.key === "z") {
228
+ e.preventDefault();
229
+ e.shiftKey ? redo() : undo();
230
+ return;
231
+ }
232
+ if (mod && e.key === "y") {
233
+ e.preventDefault();
234
+ redo();
235
+ return;
236
+ }
237
+ if (!mod) {
238
+ if (e.key === "v")
239
+ setTool("select");
240
+ if (e.key === "r")
241
+ setTool("rect");
242
+ if (e.key === "e")
243
+ setTool("ellipse");
244
+ if (e.key === "t")
245
+ setTool("text");
246
+ if (e.key === "i")
247
+ setTool("image");
248
+ }
249
+ }
250
+ window.addEventListener("keydown", onKey);
251
+ return () => window.removeEventListener("keydown", onKey);
252
+ }, [objects, selectedId]);
253
+ // ── stage pointer events (mouse + touch) ───────────────────────────────────
254
+ function onStagePointerDown(e) {
255
+ if (tool === "select") {
256
+ if (e.target === stageRef.current)
257
+ setSelectedId(null);
258
+ return;
259
+ }
260
+ if (!stageRef.current)
261
+ return;
262
+ e.evt.preventDefault();
263
+ const pos = stagePointer(stageRef.current);
264
+ setDrawing({ x: pos.x, y: pos.y, w: 0, h: 0 });
265
+ }
266
+ function onStagePointerMove(e) {
267
+ if (!drawing)
268
+ return;
269
+ e.evt.preventDefault();
270
+ if (!stageRef.current)
271
+ return;
272
+ const pos = stagePointer(stageRef.current);
273
+ setDrawing((d) => (d ? { ...d, w: pos.x - d.x, h: pos.y - d.y } : null));
274
+ }
275
+ async function onStagePointerUp() {
276
+ if (!drawing)
277
+ return;
278
+ const { x, y, w, h } = drawing;
279
+ setDrawing(null);
280
+ if (Math.abs(w) < 4 || Math.abs(h) < 4)
281
+ return;
282
+ const nx = w < 0 ? x + w : x;
283
+ const ny = h < 0 ? y + h : y;
284
+ const nw = Math.abs(w);
285
+ const nh = Math.abs(h);
286
+ const base = {
287
+ id: crypto.randomUUID(),
288
+ x: nx,
289
+ y: ny,
290
+ width: nw,
291
+ height: nh,
292
+ fill: fillColor,
293
+ stroke: strokeColor,
294
+ strokeWidth,
295
+ opacity,
296
+ };
297
+ let obj;
298
+ if (tool === "text") {
299
+ obj = { ...base, type: "text", text: "Text", fontSize: 16, fill: fillColor };
300
+ }
301
+ else {
302
+ obj = { ...base, type: tool };
303
+ }
304
+ commit([...objects, obj]);
305
+ setSelectedId(obj.id);
306
+ setTool("select");
307
+ }
308
+ function onWheel(e) {
309
+ e.evt.preventDefault();
310
+ const stage = stageRef.current;
311
+ if (!stage)
312
+ return;
313
+ const factor = e.evt.deltaY < 0 ? 1.08 : 1 / 1.08;
314
+ const pointer = stage.getPointerPosition() ?? { x: 0, y: 0 };
315
+ const newZoom = Math.min(Math.max(zoom * factor, 0.05), 8);
316
+ const mx = (pointer.x - stagePos.x) / zoom;
317
+ const my = (pointer.y - stagePos.y) / zoom;
318
+ setZoom(newZoom);
319
+ setStagePos({ x: pointer.x - mx * newZoom, y: pointer.y - my * newZoom });
320
+ }
321
+ // ── text double-click inline editing ───────────────────────────────────────
322
+ function onTextDblClick(id, e) {
323
+ const textNode = e.target;
324
+ const stage = stageRef.current;
325
+ if (!stage)
326
+ return;
327
+ const container = stage.container();
328
+ const cr = container.getBoundingClientRect();
329
+ const absPos = textNode.getAbsolutePosition();
330
+ const ta = document.createElement("textarea");
331
+ ta.value = textNode.text();
332
+ ta.style.cssText = [
333
+ "position:fixed",
334
+ `top:${cr.top + absPos.y}px`,
335
+ `left:${cr.left + absPos.x}px`,
336
+ `width:${Math.max(textNode.width() * zoom, 120)}px`,
337
+ `min-height:${(textNode.fontSize() ?? 16) * zoom * 1.4}px`,
338
+ `font-size:${(textNode.fontSize() ?? 16) * zoom}px`,
339
+ "font-family:sans-serif",
340
+ "background:#1a0f08",
341
+ "color:#fff",
342
+ "border:1px solid #fc5102",
343
+ "border-radius:2px",
344
+ "padding:2px 4px",
345
+ "resize:none",
346
+ "outline:none",
347
+ "z-index:9999",
348
+ "line-height:1.4",
349
+ ].join(";");
350
+ document.body.appendChild(ta);
351
+ ta.focus();
352
+ ta.select();
353
+ const finish = () => {
354
+ const val = ta.value;
355
+ document.body.removeChild(ta);
356
+ commit(objects.map((o) => (o.id === id ? { ...o, text: val } : o)));
357
+ };
358
+ ta.addEventListener("blur", finish, { once: true });
359
+ ta.addEventListener("keydown", (ev) => {
360
+ if (ev.key === "Escape" || (ev.key === "Enter" && !ev.shiftKey)) {
361
+ ev.preventDefault();
362
+ ta.blur();
363
+ }
364
+ });
365
+ }
366
+ // ── image import ────────────────────────────────────────────────────────────
367
+ async function handleImageFile(f) {
368
+ // Try uploading to the service so the asset has a persistent URL.
369
+ // Falls back to a local data URL when the service is unavailable or in demo mode.
370
+ let src;
371
+ if (!demo) {
372
+ try {
373
+ const form = new FormData();
374
+ form.append("file", f);
375
+ const res = await fetch(`${SERVICE_URL}/assets`, { method: "POST", body: form });
376
+ if (res.ok) {
377
+ const json = (await res.json());
378
+ src = `${SERVICE_URL}${json.url}`;
379
+ }
380
+ else {
381
+ throw new Error("upload failed");
382
+ }
383
+ }
384
+ catch {
385
+ src = await new Promise((res) => {
386
+ const reader = new FileReader();
387
+ reader.onload = (ev) => res(ev.target?.result);
388
+ reader.readAsDataURL(f);
389
+ });
390
+ }
391
+ }
392
+ else {
393
+ src = await new Promise((res) => {
394
+ const reader = new FileReader();
395
+ reader.onload = (ev) => res(ev.target?.result);
396
+ reader.readAsDataURL(f);
397
+ });
398
+ }
399
+ const imgEl = await loadImage(src);
400
+ const max = Math.min(pageSize.width, pageSize.height) * 0.5;
401
+ const s = Math.min(1, max / Math.max(imgEl.naturalWidth, imgEl.naturalHeight));
402
+ const w = imgEl.naturalWidth * s;
403
+ const h = imgEl.naturalHeight * s;
404
+ const obj = {
405
+ id: crypto.randomUUID(),
406
+ type: "image",
407
+ x: (pageSize.width - w) / 2,
408
+ y: (pageSize.height - h) / 2,
409
+ width: w,
410
+ height: h,
411
+ fill: "transparent",
412
+ stroke: "transparent",
413
+ strokeWidth: 0,
414
+ opacity: 1,
415
+ src,
416
+ imageEl: imgEl,
417
+ };
418
+ commit([...objects, obj]);
419
+ setSelectedId(obj.id);
420
+ setTool("select");
421
+ }
422
+ // ── update selected object properties ──────────────────────────────────────
423
+ const selected = objects.find((o) => o.id === selectedId) ?? null;
424
+ function updateSelected(patch) {
425
+ if (!selectedId)
426
+ return;
427
+ commit(objects.map((o) => (o.id === selectedId ? { ...o, ...patch } : o)));
428
+ }
429
+ // ── PDF export ──────────────────────────────────────────────────────────────
430
+ async function handleClientExport() {
431
+ const stage = stageRef.current;
432
+ if (!stage)
433
+ return;
434
+ setExportStatus("sending");
435
+ try {
436
+ // Rasterize just the page area at 2x for a crisp embed.
437
+ const png = stage.toDataURL({
438
+ x: stagePos.x,
439
+ y: stagePos.y,
440
+ width: pageSize.width * zoom,
441
+ height: pageSize.height * zoom,
442
+ pixelRatio: 2,
443
+ mimeType: "image/png",
444
+ });
445
+ setSelectedId(null);
446
+ const pdf = await PDFDocument.create();
447
+ const page = pdf.addPage([pageSize.width, pageSize.height]);
448
+ const img = await pdf.embedPng(png);
449
+ page.drawImage(img, {
450
+ x: 0,
451
+ y: 0,
452
+ width: pageSize.width,
453
+ height: pageSize.height,
454
+ });
455
+ const bytes = await pdf.save();
456
+ const blob = new Blob([new Uint8Array(bytes)], { type: "application/pdf" });
457
+ const url = URL.createObjectURL(blob);
458
+ const link = document.createElement("a");
459
+ link.href = url;
460
+ link.download = "artwork-demo.pdf";
461
+ link.click();
462
+ URL.revokeObjectURL(url);
463
+ setExportStatus("done");
464
+ setTimeout(() => setExportStatus("idle"), 3000);
465
+ }
466
+ catch {
467
+ setExportStatus("error");
468
+ }
469
+ }
470
+ async function handleExport() {
471
+ if (demo) {
472
+ await handleClientExport();
473
+ return;
474
+ }
475
+ setExportStatus("sending");
476
+ const doc = {
477
+ version: "2",
478
+ width: pageSize.width,
479
+ height: pageSize.height,
480
+ unit: "pt",
481
+ separations: [],
482
+ layers: [
483
+ {
484
+ id: "layer-1",
485
+ type: "artwork",
486
+ name: "Artwork",
487
+ visible: true,
488
+ objects: objects.map((o) => ({
489
+ id: o.id,
490
+ type: o.type,
491
+ x: o.x,
492
+ y: o.y,
493
+ width: o.width,
494
+ height: o.height,
495
+ fill: o.fill,
496
+ stroke: o.stroke,
497
+ ...(o.strokeWidth !== undefined ? { strokeWidth: o.strokeWidth } : {}),
498
+ ...(o.opacity !== undefined ? { opacity: o.opacity } : {}),
499
+ ...(o.text !== undefined ? { text: o.text } : {}),
500
+ ...(o.fontSize !== undefined ? { fontSize: o.fontSize } : {}),
501
+ ...(o.fontFamily !== undefined ? { fontFamily: o.fontFamily } : {}),
502
+ ...(o.src !== undefined ? { src: o.src } : {}),
503
+ ...(o.pathData !== undefined ? { pathData: o.pathData } : {}),
504
+ })),
505
+ },
506
+ ],
507
+ };
508
+ const req = {
509
+ document: doc,
510
+ output: { format: "pdf-x4" },
511
+ ...(report ? { preflightReport: report } : {}),
512
+ };
513
+ try {
514
+ const res = await fetch(`${SERVICE_URL}/jobs`, {
515
+ method: "POST",
516
+ headers: { "Content-Type": "application/json" },
517
+ body: JSON.stringify(req),
518
+ });
519
+ const { id } = (await res.json());
520
+ setExportJobId(id);
521
+ setExportStatus("polling");
522
+ pollJob(id);
523
+ }
524
+ catch {
525
+ setExportStatus("error");
526
+ }
527
+ }
528
+ function pollJob(id) {
529
+ let attempts = 0;
530
+ const tick = async () => {
531
+ attempts++;
532
+ if (attempts > 60) {
533
+ setExportStatus("error");
534
+ return;
535
+ }
536
+ try {
537
+ const r = await fetch(`${SERVICE_URL}/jobs/${id}`);
538
+ const { status } = (await r.json());
539
+ if (status === "done") {
540
+ const rr = await fetch(`${SERVICE_URL}/jobs/${id}/result`);
541
+ const result = (await rr.json());
542
+ const link = document.createElement("a");
543
+ link.href = `data:application/pdf;base64,${result.pdfBase64}`;
544
+ link.download = result.filename ?? "artwork.pdf";
545
+ link.click();
546
+ setExportStatus("done");
547
+ setTimeout(() => setExportStatus("idle"), 3000);
548
+ }
549
+ else if (status === "failed") {
550
+ setExportStatus("error");
551
+ }
552
+ else {
553
+ setTimeout(tick, 2000);
554
+ }
555
+ }
556
+ catch {
557
+ setExportStatus("error");
558
+ }
559
+ };
560
+ setTimeout(tick, 1000);
561
+ }
562
+ // ── pro mode helpers ───────────────────────────────────────────────────────
563
+ function reorderObject(id, direction) {
564
+ const idx = objects.findIndex((o) => o.id === id);
565
+ if (idx === -1)
566
+ return;
567
+ const swapWith = direction === "up" ? idx + 1 : idx - 1;
568
+ if (swapWith < 0 || swapWith >= objects.length)
569
+ return;
570
+ const next = objects.slice();
571
+ const a = next[idx];
572
+ const b = next[swapWith];
573
+ if (!a || !b)
574
+ return;
575
+ next[idx] = b;
576
+ next[swapWith] = a;
577
+ commit(next);
578
+ }
579
+ function toggleVisible(id) {
580
+ commit(objects.map((o) => (o.id === id ? { ...o, opacity: o.opacity === 0 ? 1 : 0 } : o)));
581
+ }
582
+ function applyDieline(template) {
583
+ setCurrentTemplate(template);
584
+ const { objects: seeded, pageSize: newPageSize } = templateToInitialState(template, bleedMm);
585
+ setPageSize(newPageSize);
586
+ // Replace any existing dieline rect so swapping templates is one click.
587
+ const next = [...seeded, ...objects.filter((o) => !/dieline/i.test(o.id))];
588
+ commit(next);
589
+ setSelectedId(null);
590
+ }
591
+ function toggleInk(color) {
592
+ setHiddenInks((prev) => {
593
+ const next = new Set(prev);
594
+ if (next.has(color))
595
+ next.delete(color);
596
+ else
597
+ next.add(color);
598
+ return next;
599
+ });
600
+ }
601
+ // Objects whose fill/stroke match a hidden ink are rendered with opacity 0
602
+ // for the preview. We don't mutate the actual object — separations preview
603
+ // is non-destructive.
604
+ const visibleObjects = config.enable_separations_panel && hiddenInks.size > 0
605
+ ? objects.map((o) => {
606
+ const fillHidden = hiddenInks.has(o.fill.toLowerCase());
607
+ const strokeHidden = hiddenInks.has(o.stroke.toLowerCase());
608
+ if (fillHidden && strokeHidden)
609
+ return { ...o, opacity: 0 };
610
+ return o;
611
+ })
612
+ : objects;
613
+ // ── render ──────────────────────────────────────────────────────────────────
614
+ const cursor = tool === "select" ? "default" : tool === "image" ? "cell" : "crosshair";
615
+ const exportLabel = exportStatus === "sending"
616
+ ? "Sending…"
617
+ : exportStatus === "polling"
618
+ ? "Rendering…"
619
+ : exportStatus === "done"
620
+ ? "Downloaded ✓"
621
+ : exportStatus === "error"
622
+ ? "Error — retry"
623
+ : demo
624
+ ? "Download PDF"
625
+ : "Export PDF/X-4";
626
+ return (_jsxs("div", { style: {
627
+ display: "flex",
628
+ flexDirection: "column",
629
+ width: "100%",
630
+ height: "100%",
631
+ background: "#ffffff",
632
+ }, children: [!isMobile && (_jsxs("div", { style: {
633
+ display: "flex",
634
+ alignItems: "center",
635
+ gap: "0.5rem",
636
+ padding: "0.375rem 0.75rem",
637
+ background: PANEL_BG,
638
+ borderBottom: `1px solid ${BORDER}`,
639
+ flexShrink: 0,
640
+ flexWrap: "wrap",
641
+ }, children: [["select", "rect", "ellipse", "text", "image"].map((t) => (_jsx(ToolBtn, { active: tool === t, onClick: () => {
642
+ setTool(t);
643
+ if (t === "image")
644
+ imageInputRef.current?.click();
645
+ }, children: t === "select"
646
+ ? "↖ Select"
647
+ : t === "rect"
648
+ ? "▭ Rect"
649
+ : t === "ellipse"
650
+ ? "◯ Ellipse"
651
+ : t === "text"
652
+ ? "T Text"
653
+ : "⬚ Image" }, t))), _jsx("div", { style: { width: 1, height: 20, background: BORDER, margin: "0 0.25rem" } }), _jsx("button", { type: "button", onClick: undo, disabled: historyIdx === 0, style: iconBtnStyle(historyIdx === 0), children: "\u21A9 Undo" }), _jsx("button", { type: "button", onClick: redo, disabled: historyIdx >= history.length - 1, style: iconBtnStyle(historyIdx >= history.length - 1), children: "\u21AA Redo" }), _jsx("div", { style: { width: 1, height: 20, background: BORDER, margin: "0 0.25rem" } }), _jsx("button", { type: "button", onClick: () => fitPage(containerSize.width, containerSize.height, pageSize.width, pageSize.height), style: iconBtnStyle(false), children: "\u22A1 Fit" }), _jsxs("span", { style: { fontSize: "0.75rem", color: MUTED }, children: [Math.round(zoom * 100), "%"] }), config.enable_dieline_chooser && (_jsxs(_Fragment, { children: [_jsx("div", { style: { width: 1, height: 20, background: BORDER, margin: "0 0.25rem" } }), _jsx("button", { type: "button", onClick: () => setDielineOpen(true), style: iconBtnStyle(false), children: "\u25A6 Dielines" })] })), _jsx("div", { style: { flex: 1 } }), _jsxs("label", { style: {
654
+ display: "flex",
655
+ alignItems: "center",
656
+ gap: "0.3rem",
657
+ fontSize: "0.75rem",
658
+ color: "#999",
659
+ }, children: ["Fill", _jsx("input", { type: "color", value: fillColor, onChange: (e) => {
660
+ setFillColor(e.target.value);
661
+ if (selected)
662
+ updateSelected({ fill: e.target.value });
663
+ }, style: {
664
+ width: 24,
665
+ height: 20,
666
+ border: "none",
667
+ padding: 0,
668
+ background: "none",
669
+ cursor: "pointer",
670
+ } })] }), _jsxs("label", { style: {
671
+ display: "flex",
672
+ alignItems: "center",
673
+ gap: "0.3rem",
674
+ fontSize: "0.75rem",
675
+ color: "#999",
676
+ }, children: ["Stroke", _jsx("input", { type: "color", value: strokeColor === "transparent" ? "#000000" : strokeColor, onChange: (e) => {
677
+ setStrokeColor(e.target.value);
678
+ if (selected)
679
+ updateSelected({ stroke: e.target.value });
680
+ }, style: {
681
+ width: 24,
682
+ height: 20,
683
+ border: "none",
684
+ padding: 0,
685
+ background: "none",
686
+ cursor: "pointer",
687
+ } })] }), _jsx("div", { style: { width: 1, height: 20, background: BORDER, margin: "0 0.25rem" } }), config.enable_export_button && (_jsx("button", { type: "button", onClick: handleExport, disabled: exportStatus === "sending" || exportStatus === "polling", style: {
688
+ background: exportStatus === "done"
689
+ ? "#2e7d32"
690
+ : exportStatus === "error"
691
+ ? "#b71c1c"
692
+ : BRAND,
693
+ color: "#fff",
694
+ border: "none",
695
+ borderRadius: 4,
696
+ padding: "0.3rem 0.85rem",
697
+ fontSize: "0.8rem",
698
+ fontWeight: 600,
699
+ cursor: exportStatus === "sending" || exportStatus === "polling" ? "wait" : "pointer",
700
+ opacity: exportStatus === "sending" || exportStatus === "polling" ? 0.7 : 1,
701
+ }, children: exportLabel }))] })), isMobile && config.enable_export_button && (_jsx("div", { style: {
702
+ display: "flex",
703
+ justifyContent: "flex-end",
704
+ padding: "0.4rem 0.75rem",
705
+ background: PANEL_BG,
706
+ borderBottom: `1px solid ${BORDER}`,
707
+ flexShrink: 0,
708
+ }, children: _jsx("button", { type: "button", onClick: handleExport, disabled: exportStatus === "sending" || exportStatus === "polling", style: {
709
+ background: exportStatus === "done" ? "#2e7d32" : exportStatus === "error" ? "#b71c1c" : BRAND,
710
+ color: "#fff",
711
+ border: "none",
712
+ borderRadius: 4,
713
+ padding: "0.35rem 0.95rem",
714
+ fontSize: "0.85rem",
715
+ fontWeight: 600,
716
+ cursor: exportStatus === "sending" || exportStatus === "polling" ? "wait" : "pointer",
717
+ opacity: exportStatus === "sending" || exportStatus === "polling" ? 0.7 : 1,
718
+ }, children: exportLabel }) })), _jsxs("div", { style: { display: "flex", flex: 1, overflow: "hidden" }, children: [config.enable_layers_panel && !isMobile && (_jsx(LayersPanel, { objects: objects, selectedId: selectedId, onSelect: (id) => setSelectedId(id), onReorder: reorderObject, onDelete: (id) => {
719
+ commit(objects.filter((o) => o.id !== id));
720
+ if (id === selectedId)
721
+ setSelectedId(null);
722
+ }, onToggleVisible: toggleVisible })), _jsxs("div", { ref: containerRef, style: { flex: 1, overflow: "hidden", position: "relative" }, children: [_jsx(Stage, { ref: stageRef, width: containerSize.width, height: containerSize.height, scaleX: zoom, scaleY: zoom, x: stagePos.x, y: stagePos.y, onMouseDown: onStagePointerDown, onMouseMove: onStagePointerMove, onMouseUp: onStagePointerUp, onTouchStart: onStagePointerDown, onTouchMove: onStagePointerMove, onTouchEnd: onStagePointerUp, onWheel: onWheel, style: { cursor }, children: _jsxs(Layer, { children: [_jsx(Rect, { x: 3, y: 3, width: pageSize.width, height: pageSize.height, fill: "rgba(0,0,0,0.08)" }), _jsx(Rect, { x: 0, y: 0, width: pageSize.width, height: pageSize.height, fill: "#ffffff", stroke: "#d4d4d8", strokeWidth: 0.5 / zoom, onClick: () => {
723
+ if (tool === "select")
724
+ setSelectedId(null);
725
+ } }), config.enable_canvas_grid && _jsx(GridLines, { pageSize: pageSize, zoom: zoom }), config.enable_bleed_visualization && (_jsxs(_Fragment, { children: [_jsx(Rect, { x: 0, y: 0, width: pageSize.width, height: pageSize.height, stroke: "#0ea5e9", strokeWidth: 1 / zoom, dash: [6 / zoom, 4 / zoom], listening: false }), _jsx(Text, { text: `BLEED ${formatBleed(bleedMm, "in")}`, x: 6, y: -14 / zoom, fontSize: 10 / zoom, fill: "#0ea5e9", listening: false })] })), visibleObjects.map((obj) => (_jsx(ObjNode, { obj: obj, selected: obj.id === selectedId, onSelect: () => setSelectedId(obj.id), onDragEnd: (x, y) => commit(objects.map((o) => (o.id === obj.id ? { ...o, x, y } : o))), onTransformEnd: (x, y, w, h) => commit(objects.map((o) => o.id === obj.id ? { ...o, x, y, width: w, height: h } : o)), onDblClick: (e) => obj.type === "text" && onTextDblClick(obj.id, e) }, obj.id))), _jsx(Transformer, { ref: trRef, borderStroke: BRAND, borderStrokeWidth: 1, anchorStroke: BRAND, anchorFill: "#fff", anchorSize: 8, rotateEnabled: false, keepRatio: false }), drawing && tool !== "select" && (_jsx(Rect, { x: drawing.w < 0 ? drawing.x + drawing.w : drawing.x, y: drawing.h < 0 ? drawing.y + drawing.h : drawing.y, width: Math.abs(drawing.w), height: Math.abs(drawing.h), fill: tool === "text" ? "rgba(252,81,2,0.08)" : `${fillColor}80`, stroke: BRAND, strokeWidth: 1 / zoom, dash: [4 / zoom, 4 / zoom] }))] }) }), objects.length === 0 && (_jsx("div", { style: {
726
+ position: "absolute",
727
+ inset: 0,
728
+ display: "flex",
729
+ alignItems: "center",
730
+ justifyContent: "center",
731
+ pointerEvents: "none",
732
+ }, children: _jsx("span", { style: { fontSize: "0.8rem", color: "#94a3b8" }, children: "Use the toolbar to draw shapes, add text, or import an image" }) }))] }), config.enable_separations_panel && !isMobile && (_jsx(SeparationsPanel, { objects: objects, hidden: hiddenInks, onToggle: toggleInk }))] }), selected && (_jsxs("div", { style: {
733
+ display: "flex",
734
+ alignItems: "center",
735
+ gap: "1rem",
736
+ padding: "0.375rem 0.75rem",
737
+ background: PANEL_BG,
738
+ borderTop: `1px solid ${BORDER}`,
739
+ flexShrink: 0,
740
+ fontSize: "0.75rem",
741
+ color: "#999",
742
+ flexWrap: "wrap",
743
+ }, children: [_jsx("span", { style: { color: BRAND, fontWeight: 600 }, children: selected.type }), _jsx(PropNum, { label: "X", value: Math.round(selected.x), onChange: (v) => updateSelected({ x: v }) }), _jsx(PropNum, { label: "Y", value: Math.round(selected.y), onChange: (v) => updateSelected({ y: v }) }), _jsx(PropNum, { label: "W", value: Math.round(selected.width), onChange: (v) => updateSelected({ width: Math.max(1, v) }) }), _jsx(PropNum, { label: "H", value: Math.round(selected.height), onChange: (v) => updateSelected({ height: Math.max(1, v) }) }), _jsxs("label", { style: { display: "flex", alignItems: "center", gap: "0.3rem" }, children: ["Fill", _jsx("input", { type: "color", value: selected.fill === "transparent" ? "#ffffff" : selected.fill, onChange: (e) => updateSelected({ fill: e.target.value }), style: {
744
+ width: 22,
745
+ height: 18,
746
+ border: "none",
747
+ padding: 0,
748
+ background: "none",
749
+ cursor: "pointer",
750
+ } })] }), _jsxs("label", { style: { display: "flex", alignItems: "center", gap: "0.3rem" }, children: ["Stroke", _jsx("input", { type: "color", value: selected.stroke === "transparent" ? "#000000" : selected.stroke, onChange: (e) => updateSelected({ stroke: e.target.value }), style: {
751
+ width: 22,
752
+ height: 18,
753
+ border: "none",
754
+ padding: 0,
755
+ background: "none",
756
+ cursor: "pointer",
757
+ } })] }), _jsxs("label", { style: { display: "flex", alignItems: "center", gap: "0.3rem" }, children: ["Opacity", _jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: selected.opacity, onChange: (e) => updateSelected({ opacity: Number(e.target.value) }), style: { width: 60 } }), _jsxs("span", { children: [Math.round(selected.opacity * 100), "%"] })] }), selected.type === "text" && (_jsxs(_Fragment, { children: [_jsx(PropNum, { label: "Size", value: selected.fontSize ?? 16, onChange: (v) => updateSelected({ fontSize: Math.max(4, v) }) }), _jsx("button", { type: "button", onClick: () => {
758
+ // Spawn inline text edit by focusing textarea overlay
759
+ const stage = stageRef.current;
760
+ if (!stage)
761
+ return;
762
+ const node = stage.findOne(`#${selected.id}`);
763
+ if (!node)
764
+ return;
765
+ onTextDblClick(selected.id, {
766
+ target: node,
767
+ });
768
+ }, style: { ...iconBtnStyle(false), fontSize: "0.72rem" }, children: "Edit text" })] })), _jsx("button", { type: "button", onClick: () => {
769
+ commit(objects.filter((o) => o.id !== selectedId));
770
+ setSelectedId(null);
771
+ }, style: {
772
+ marginLeft: "auto",
773
+ background: "transparent",
774
+ border: "1px solid #5a1a1a",
775
+ color: "#e57373",
776
+ borderRadius: 3,
777
+ padding: "0.2rem 0.5rem",
778
+ cursor: "pointer",
779
+ fontSize: "0.72rem",
780
+ }, children: "Delete" })] })), _jsx("input", { ref: imageInputRef, type: "file", accept: "image/*", style: { display: "none" }, onChange: (e) => {
781
+ const f = e.target.files?.[0];
782
+ if (f)
783
+ handleImageFile(f);
784
+ e.target.value = "";
785
+ } }), config.enable_dieline_chooser && (_jsx(DielineLibraryModal, { open: dielineOpen, onClose: () => setDielineOpen(false), onSelect: applyDieline })), isMobile && (_jsx(MobileToolDrawer, { isOpen: menuOpen, onClose: () => onMenuOpenChange?.(false), config: config, activeTool: tool, onSelectTool: (t) => {
786
+ setTool(t);
787
+ if (t === "image")
788
+ imageInputRef.current?.click();
789
+ }, canUndo: historyIdx > 0, canRedo: historyIdx < history.length - 1, onUndo: undo, onRedo: redo, zoomPct: Math.round(zoom * 100), onFit: () => fitPage(containerSize.width, containerSize.height, pageSize.width, pageSize.height), onOpenDielineChooser: () => setDielineOpen(true), fillColor: fillColor, strokeColor: strokeColor === "transparent" ? "#000000" : strokeColor, onFillChange: (hex) => {
790
+ setFillColor(hex);
791
+ if (selected)
792
+ updateSelected({ fill: hex });
793
+ }, onStrokeChange: (hex) => {
794
+ setStrokeColor(hex);
795
+ if (selected)
796
+ updateSelected({ stroke: hex });
797
+ }, bleedMm: bleedMm, onBleedMmChange: setBleedMm, mode: mode, onModeChange: (m) => onModeChange?.(m), onExport: handleExport, exportLabel: exportLabel, exportBusy: exportStatus === "sending" || exportStatus === "polling" }))] }));
798
+ }
799
+ // ── grid overlay ──────────────────────────────────────────────────────────────
800
+ const MM_TO_PT_GRID = 2.83465;
801
+ function GridLines({ pageSize, zoom, }) {
802
+ const minor = 10 * MM_TO_PT_GRID; // 10 mm → pt
803
+ const major = 50 * MM_TO_PT_GRID; // 50 mm → pt
804
+ const lines = [];
805
+ for (let x = 0; x <= pageSize.width; x += minor) {
806
+ const isMajor = Math.abs(x % major) < 0.01 || Math.abs((x % major) - major) < 0.01;
807
+ lines.push(_jsx(Line, { points: [x, 0, x, pageSize.height], stroke: isMajor ? "#cbd5e1" : "#e5e7eb", strokeWidth: 0.5 / zoom, listening: false }, `v-${x}`));
808
+ }
809
+ for (let y = 0; y <= pageSize.height; y += minor) {
810
+ const isMajor = Math.abs(y % major) < 0.01 || Math.abs((y % major) - major) < 0.01;
811
+ lines.push(_jsx(Line, { points: [0, y, pageSize.width, y], stroke: isMajor ? "#cbd5e1" : "#e5e7eb", strokeWidth: 0.5 / zoom, listening: false }, `h-${y}`));
812
+ }
813
+ return _jsx(_Fragment, { children: lines });
814
+ }
815
+ // ── small helper components ───────────────────────────────────────────────────
816
+ function ToolBtn({ active, onClick, children, }) {
817
+ return (_jsx("button", { type: "button", onClick: onClick, style: {
818
+ background: active ? BRAND : "transparent",
819
+ color: active ? "#fff" : "#aaa",
820
+ border: `1px solid ${active ? BRAND : BORDER}`,
821
+ borderRadius: 4,
822
+ padding: "0.2rem 0.6rem",
823
+ fontSize: "0.75rem",
824
+ cursor: "pointer",
825
+ fontFamily: "inherit",
826
+ transition: "background 0.1s, color 0.1s",
827
+ }, children: children }));
828
+ }
829
+ function iconBtnStyle(disabled) {
830
+ return {
831
+ background: "transparent",
832
+ color: disabled ? "#444" : "#aaa",
833
+ border: `1px solid ${disabled ? "#2a1200" : BORDER}`,
834
+ borderRadius: 4,
835
+ padding: "0.2rem 0.55rem",
836
+ fontSize: "0.75rem",
837
+ cursor: disabled ? "not-allowed" : "pointer",
838
+ fontFamily: "inherit",
839
+ };
840
+ }
841
+ function PropNum({ label, value, onChange, }) {
842
+ return (_jsxs("label", { style: { display: "flex", alignItems: "center", gap: "0.25rem" }, children: [label, _jsx("input", { type: "number", value: value, onChange: (e) => onChange(Number(e.target.value)), style: {
843
+ width: 52,
844
+ background: "#120a04",
845
+ border: `1px solid ${BORDER}`,
846
+ color: "#ccc",
847
+ borderRadius: 3,
848
+ padding: "0.1rem 0.3rem",
849
+ fontSize: "0.72rem",
850
+ } })] }));
851
+ }
852
+ //# sourceMappingURL=EditorCanvas.js.map