react-os-shell 0.2.24 → 0.2.25

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.
@@ -1,18 +1,26 @@
1
1
  import { toast_default } from './chunk-WIJ45SYD.js';
2
2
  import { WindowTitle, getActiveModalId } from './chunk-QXY6ZHRX.js';
3
- import { createContext, useState, useEffect, useRef, useContext, useMemo } from 'react';
3
+ import { forwardRef, useRef, useState, useMemo, useEffect, useImperativeHandle, createContext, useContext } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import * as pdfjsLib from 'pdfjs-dist';
6
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
 
8
8
  var COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6", "#000000", "#ffffff"];
9
+ var FONTS = [
10
+ { id: "system", label: "System", css: "-apple-system, system-ui, sans-serif" },
11
+ { id: "serif", label: "Serif", css: 'Georgia, "Times New Roman", serif' },
12
+ { id: "mono", label: "Mono", css: 'ui-monospace, "SF Mono", Menlo, monospace' },
13
+ { id: "cursive", label: "Cursive", css: '"Brush Script MT", "Comic Sans MS", cursive' }
14
+ ];
9
15
  var STROKE_DEFAULT = 4;
16
+ var TEXT_SIZE_DEFAULT = 24;
17
+ var RECT_RADIUS_DEFAULT = 12;
10
18
  var MOSAIC_BLOCK = 12;
11
19
  var ZOOM_MIN = 0.25;
12
20
  var ZOOM_MAX = 4;
13
21
  var ZOOM_STEP = 0.25;
14
22
  var newId = () => Math.random().toString(36).slice(2, 9);
15
- function ImageAnnotator({ src, filename, onClose }) {
23
+ var ImageAnnotator = forwardRef(function ImageAnnotator2({ src, filename }, ref) {
16
24
  const wrapRef = useRef(null);
17
25
  const canvasRef = useRef(null);
18
26
  const svgRef = useRef(null);
@@ -20,6 +28,12 @@ function ImageAnnotator({ src, filename, onClose }) {
20
28
  const [tool, setTool] = useState("select");
21
29
  const [color, setColor] = useState(COLORS[0]);
22
30
  const [stroke, setStroke] = useState(STROKE_DEFAULT);
31
+ const [textSize, setTextSize] = useState(TEXT_SIZE_DEFAULT);
32
+ const [textFont, setTextFont] = useState(FONTS[0].id);
33
+ const [textBold, setTextBold] = useState(true);
34
+ const [textItalic, setTextItalic] = useState(false);
35
+ const [textUnderline, setTextUnderline] = useState(false);
36
+ const [rectRadius, setRectRadius] = useState(RECT_RADIUS_DEFAULT);
23
37
  const [annotations, setAnnotations] = useState([]);
24
38
  const [selectedId, setSelectedId] = useState(null);
25
39
  const [imageSize, setImageSize] = useState(null);
@@ -34,6 +48,25 @@ function ImageAnnotator({ src, filename, onClose }) {
34
48
  if (!fitSize) return null;
35
49
  return { w: fitSize.w * zoom, h: fitSize.h * zoom };
36
50
  }, [fitSize, zoom]);
51
+ const scale = displaySize && imageSize ? displaySize.w / imageSize.w : 1;
52
+ const selected = useMemo(
53
+ () => annotations.find((a) => a.id === selectedId) ?? null,
54
+ [annotations, selectedId]
55
+ );
56
+ useEffect(() => {
57
+ if (tool === "select" && selected) {
58
+ if ("color" in selected) setColor(selected.color);
59
+ if ("stroke" in selected) setStroke(selected.stroke);
60
+ if (selected.type === "rect") setRectRadius(selected.radius);
61
+ if (selected.type === "text") {
62
+ setTextSize(selected.size);
63
+ setTextFont(selected.font);
64
+ setTextBold(selected.bold);
65
+ setTextItalic(selected.italic);
66
+ setTextUnderline(selected.underline);
67
+ }
68
+ }
69
+ }, [selectedId]);
37
70
  useEffect(() => {
38
71
  const img = new Image();
39
72
  img.crossOrigin = "anonymous";
@@ -56,8 +89,7 @@ function ImageAnnotator({ src, filename, onClose }) {
56
89
  const availH = Math.max(0, r.height - 32);
57
90
  if (availW === 0 || availH === 0) return;
58
91
  const ratio = imageSize.w / imageSize.h;
59
- let w = imageSize.w;
60
- let h = imageSize.h;
92
+ let w = imageSize.w, h = imageSize.h;
61
93
  if (w > availW) {
62
94
  w = availW;
63
95
  h = w / ratio;
@@ -109,7 +141,13 @@ function ImageAnnotator({ src, filename, onClose }) {
109
141
  if (!drag) return;
110
142
  const p = evToImage(ev);
111
143
  if (drag.kind === "draw") {
112
- setPreview(makeShape(tool, drag.start, p, color, stroke));
144
+ setPreview(makeShape(tool, drag.start, p, color, stroke, rectRadius));
145
+ } else if (drag.kind === "pen") {
146
+ const last = drag.points[drag.points.length - 1];
147
+ if (Math.abs(p.x - last.x) > 1 || Math.abs(p.y - last.y) > 1) {
148
+ drag.points.push(p);
149
+ setPreview({ id: "", type: "draw", points: drag.points.slice(), color, stroke });
150
+ }
113
151
  } else if (drag.kind === "crop") {
114
152
  setPendingCrop(normalizeRect(drag.start, p));
115
153
  } else if (drag.kind === "move") {
@@ -133,6 +171,16 @@ function ImageAnnotator({ src, filename, onClose }) {
133
171
  setTool("select");
134
172
  return null;
135
173
  });
174
+ } else if (drag?.kind === "pen") {
175
+ if (drag.points.length < 2) {
176
+ setPreview(null);
177
+ return;
178
+ }
179
+ const anno = { id: newId(), type: "draw", points: drag.points, color, stroke };
180
+ setAnnotations((prev) => [...prev, anno]);
181
+ setSelectedId(anno.id);
182
+ setTool("select");
183
+ setPreview(null);
136
184
  }
137
185
  };
138
186
  window.addEventListener("pointermove", onMove);
@@ -143,7 +191,7 @@ function ImageAnnotator({ src, filename, onClose }) {
143
191
  window.removeEventListener("pointerup", onUp);
144
192
  window.removeEventListener("pointercancel", onUp);
145
193
  };
146
- }, [isDragging, tool, color, stroke]);
194
+ }, [isDragging, tool, color, stroke, rectRadius]);
147
195
  const handleAnnoPointerDown = (e, anno) => {
148
196
  if (tool !== "select") return;
149
197
  e.stopPropagation();
@@ -169,7 +217,12 @@ function ImageAnnotator({ src, filename, onClose }) {
169
217
  beginDrag({ kind: "crop", start });
170
218
  return;
171
219
  }
172
- setPreview(makeShape(tool, start, start, color, stroke));
220
+ if (tool === "draw") {
221
+ beginDrag({ kind: "pen", points: [start] });
222
+ setPreview({ id: "", type: "draw", points: [start], color, stroke });
223
+ return;
224
+ }
225
+ setPreview(makeShape(tool, start, start, color, stroke, rectRadius));
173
226
  beginDrag({ kind: "draw", start });
174
227
  };
175
228
  const handleAnnoDoubleClick = (anno) => {
@@ -178,8 +231,14 @@ function ImageAnnotator({ src, filename, onClose }) {
178
231
  };
179
232
  useEffect(() => {
180
233
  const onKey = (ev) => {
181
- if (!selectedId) return;
182
234
  if (document.activeElement && /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) return;
235
+ if ((ev.metaKey || ev.ctrlKey) && (ev.key === "z" || ev.key === "Z") && !ev.shiftKey) {
236
+ ev.preventDefault();
237
+ setAnnotations((prev) => prev.slice(0, -1));
238
+ setSelectedId(null);
239
+ return;
240
+ }
241
+ if (!selectedId) return;
183
242
  if (ev.key === "Delete" || ev.key === "Backspace") {
184
243
  ev.preventDefault();
185
244
  setAnnotations((prev) => prev.filter((a) => a.id !== selectedId));
@@ -191,17 +250,67 @@ function ImageAnnotator({ src, filename, onClose }) {
191
250
  window.addEventListener("keydown", onKey);
192
251
  return () => window.removeEventListener("keydown", onKey);
193
252
  }, [selectedId]);
253
+ const setColorAndApply = (c) => {
254
+ setColor(c);
255
+ if (!selectedId) return;
256
+ setAnnotations((prev) => prev.map((a) => {
257
+ if (a.id !== selectedId) return a;
258
+ if (a.type === "mosaic") return a;
259
+ return { ...a, color: c };
260
+ }));
261
+ };
262
+ const setStrokeAndApply = (s) => {
263
+ setStroke(s);
264
+ if (!selectedId) return;
265
+ setAnnotations((prev) => prev.map((a) => {
266
+ if (a.id !== selectedId) return a;
267
+ if (a.type === "rect" || a.type === "circle" || a.type === "arrow" || a.type === "draw") {
268
+ return { ...a, stroke: s };
269
+ }
270
+ return a;
271
+ }));
272
+ };
273
+ const setRectRadiusAndApply = (r) => {
274
+ setRectRadius(r);
275
+ if (!selectedId) return;
276
+ setAnnotations((prev) => prev.map(
277
+ (a) => a.id === selectedId && a.type === "rect" ? { ...a, radius: r } : a
278
+ ));
279
+ };
280
+ const setTextSizeAndApply = (s) => {
281
+ setTextSize(s);
282
+ if (!selectedId) return;
283
+ setAnnotations((prev) => prev.map(
284
+ (a) => a.id === selectedId && a.type === "text" ? { ...a, size: s } : a
285
+ ));
286
+ };
287
+ const setTextFontAndApply = (f) => {
288
+ setTextFont(f);
289
+ if (!selectedId) return;
290
+ setAnnotations((prev) => prev.map(
291
+ (a) => a.id === selectedId && a.type === "text" ? { ...a, font: f } : a
292
+ ));
293
+ };
294
+ const toggleTextStyleAndApply = (which) => {
295
+ const next = !{ bold: textBold, italic: textItalic, underline: textUnderline }[which];
296
+ if (which === "bold") setTextBold(next);
297
+ if (which === "italic") setTextItalic(next);
298
+ if (which === "underline") setTextUnderline(next);
299
+ if (!selectedId) return;
300
+ setAnnotations((prev) => prev.map(
301
+ (a) => a.id === selectedId && a.type === "text" ? { ...a, [which]: next } : a
302
+ ));
303
+ };
194
304
  const commitText = () => {
195
305
  if (!pendingText) return;
196
- const value = pendingText.value.trim();
197
- if (!value) {
306
+ const value = pendingText.value;
307
+ if (!value.trim()) {
198
308
  if (pendingText.editingId) {
199
309
  setAnnotations((prev) => prev.filter((a) => a.id !== pendingText.editingId));
200
310
  }
201
311
  setPendingText(null);
202
312
  return;
203
313
  }
204
- const fontSize = Math.max(16, stroke * 6);
205
314
  if (pendingText.editingId) {
206
315
  setAnnotations((prev) => prev.map(
207
316
  (a) => a.id === pendingText.editingId && a.type === "text" ? { ...a, text: value } : a
@@ -214,7 +323,11 @@ function ImageAnnotator({ src, filename, onClose }) {
214
323
  y: pendingText.y,
215
324
  text: value,
216
325
  color,
217
- size: fontSize
326
+ size: textSize,
327
+ font: textFont,
328
+ bold: textBold,
329
+ italic: textItalic,
330
+ underline: textUnderline
218
331
  };
219
332
  setAnnotations((prev) => [...prev, anno]);
220
333
  setSelectedId(anno.id);
@@ -276,45 +389,48 @@ function ImageAnnotator({ src, filename, onClose }) {
276
389
  URL.revokeObjectURL(svgUrl);
277
390
  return out;
278
391
  };
279
- const downloadAnnotated = async () => {
280
- const out = await compositeToCanvas();
281
- if (!out) return;
282
- out.toBlob((blob) => {
283
- if (!blob) {
284
- toast_default.error("Failed to export");
392
+ useImperativeHandle(ref, () => ({
393
+ save: async () => {
394
+ const out = await compositeToCanvas();
395
+ if (!out) return;
396
+ out.toBlob((blob) => {
397
+ if (!blob) {
398
+ toast_default.error("Failed to export");
399
+ return;
400
+ }
401
+ const url = URL.createObjectURL(blob);
402
+ const a = document.createElement("a");
403
+ a.href = url;
404
+ const base = filename.replace(/\.[^.]+$/, "");
405
+ a.download = `${base}-annotated.png`;
406
+ a.click();
407
+ URL.revokeObjectURL(url);
408
+ }, "image/png");
409
+ },
410
+ copy: async () => {
411
+ if (!("clipboard" in navigator) || typeof ClipboardItem === "undefined") {
412
+ toast_default.error("Clipboard images not supported in this browser");
285
413
  return;
286
414
  }
287
- const url = URL.createObjectURL(blob);
288
- const a = document.createElement("a");
289
- a.href = url;
290
- const base = filename.replace(/\.[^.]+$/, "");
291
- a.download = `${base}-annotated.png`;
292
- a.click();
293
- URL.revokeObjectURL(url);
294
- }, "image/png");
295
- };
296
- const copyToClipboard = async () => {
297
- if (!("clipboard" in navigator) || typeof ClipboardItem === "undefined") {
298
- toast_default.error("Clipboard images are not supported in this browser");
299
- return;
415
+ const out = await compositeToCanvas();
416
+ if (!out) return;
417
+ out.toBlob(async (blob) => {
418
+ if (!blob) {
419
+ toast_default.error("Failed to copy");
420
+ return;
421
+ }
422
+ try {
423
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
424
+ toast_default.success("Copied to clipboard");
425
+ } catch {
426
+ toast_default.error("Copy failed (clipboard permission?)");
427
+ }
428
+ }, "image/png");
300
429
  }
301
- const out = await compositeToCanvas();
302
- if (!out) return;
303
- out.toBlob(async (blob) => {
304
- if (!blob) {
305
- toast_default.error("Failed to copy");
306
- return;
307
- }
308
- try {
309
- await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
310
- toast_default.success("Copied to clipboard");
311
- } catch {
312
- toast_default.error("Copy failed (clipboard permission?)");
313
- }
314
- }, "image/png");
315
- };
430
+ }), [imageSize, filename]);
316
431
  const tools = useMemo(() => [
317
432
  { id: "select", label: "Select / Move", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 3l7 17 2-7 7-2L5 3z" }) }) },
433
+ { id: "draw", label: "Pen / Draw", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487z" }) }) },
318
434
  { id: "rect", label: "Rectangle", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("rect", { x: "4", y: "6", width: "16", height: "12", rx: "3" }) }) },
319
435
  { id: "circle", label: "Ellipse", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "12", rx: "8", ry: "6" }) }) },
320
436
  { id: "arrow", label: "Arrow", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 19L19 5m0 0h-7m7 0v7" }) }) },
@@ -323,9 +439,12 @@ function ImageAnnotator({ src, filename, onClose }) {
323
439
  { id: "crop", label: "Crop", icon: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.8, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 2v15a1 1 0 001 1h15M2 6h15a1 1 0 011 1v15" }) }) }
324
440
  ], []);
325
441
  const btnClass = (active) => `p-1.5 rounded transition-colors ${active ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"}`;
326
- const scale = displaySize && imageSize ? displaySize.w / imageSize.w : 1;
442
+ const ctxType = selected?.type ?? (tool === "select" ? null : tool);
443
+ const showStrokeControl = ctxType === "rect" || ctxType === "circle" || ctxType === "arrow" || ctxType === "draw";
444
+ const showRectRadius = ctxType === "rect";
445
+ const showTextControls = ctxType === "text";
327
446
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-gray-100", children: [
328
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-gray-200 bg-white shrink-0 flex-wrap", children: [
447
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-3 py-2 border-b border-gray-200 bg-white shrink-0 flex-wrap text-[12px]", children: [
329
448
  tools.map((t) => /* @__PURE__ */ jsx(
330
449
  "button",
331
450
  {
@@ -343,54 +462,55 @@ function ImageAnnotator({ src, filename, onClose }) {
343
462
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: COLORS.map((c) => /* @__PURE__ */ jsx(
344
463
  "button",
345
464
  {
346
- onClick: () => {
347
- setColor(c);
348
- if (selectedId) {
349
- setAnnotations((prev) => prev.map((a) => a.id === selectedId && a.type !== "mosaic" ? { ...a, color: c } : a));
350
- }
351
- },
465
+ onClick: () => setColorAndApply(c),
352
466
  title: c,
353
467
  className: `h-5 w-5 rounded-full border ${color === c ? "ring-2 ring-blue-500 ring-offset-1" : "border-gray-300"}`,
354
468
  style: { background: c }
355
469
  },
356
470
  c
357
471
  )) }),
358
- /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
359
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-[11px] text-gray-600", children: [
360
- /* @__PURE__ */ jsx("span", { children: "Size" }),
361
- /* @__PURE__ */ jsx(
362
- "input",
363
- {
364
- type: "range",
365
- min: 2,
366
- max: 12,
367
- step: 1,
368
- value: stroke,
369
- onChange: (e) => setStroke(Number(e.target.value)),
370
- className: "w-16 accent-blue-500"
371
- }
372
- ),
373
- /* @__PURE__ */ jsx("span", { className: "tabular-nums w-4 text-right", children: stroke })
472
+ showStrokeControl && /* @__PURE__ */ jsxs(Fragment, { children: [
473
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
474
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
475
+ /* @__PURE__ */ jsx("span", { children: "Weight" }),
476
+ /* @__PURE__ */ jsx("input", { type: "range", min: 1, max: 20, step: 1, value: stroke, onChange: (e) => setStrokeAndApply(Number(e.target.value)), className: "w-16 accent-blue-500" }),
477
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums w-5 text-right", children: stroke })
478
+ ] })
479
+ ] }),
480
+ showRectRadius && /* @__PURE__ */ jsxs(Fragment, { children: [
481
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
482
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
483
+ /* @__PURE__ */ jsx("span", { children: "Radius" }),
484
+ /* @__PURE__ */ jsx("input", { type: "range", min: 0, max: 48, step: 1, value: rectRadius, onChange: (e) => setRectRadiusAndApply(Number(e.target.value)), className: "w-16 accent-blue-500" }),
485
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums w-6 text-right", children: rectRadius })
486
+ ] })
487
+ ] }),
488
+ showTextControls && /* @__PURE__ */ jsxs(Fragment, { children: [
489
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
490
+ /* @__PURE__ */ jsx("select", { value: textFont, onChange: (e) => setTextFontAndApply(e.target.value), className: "text-xs border border-gray-300 rounded px-1 py-0.5 bg-white", children: FONTS.map((f) => /* @__PURE__ */ jsx("option", { value: f.id, children: f.label }, f.id)) }),
491
+ /* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("bold"), className: btnClass(textBold), title: "Bold", children: /* @__PURE__ */ jsx("span", { className: "font-bold", children: "B" }) }),
492
+ /* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("italic"), className: btnClass(textItalic), title: "Italic", children: /* @__PURE__ */ jsx("span", { className: "italic", children: "I" }) }),
493
+ /* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("underline"), className: btnClass(textUnderline), title: "Underline", children: /* @__PURE__ */ jsx("span", { className: "underline", children: "U" }) }),
494
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
495
+ /* @__PURE__ */ jsx("span", { children: "Size" }),
496
+ /* @__PURE__ */ jsx("input", { type: "range", min: 10, max: 96, step: 1, value: textSize, onChange: (e) => setTextSizeAndApply(Number(e.target.value)), className: "w-16 accent-blue-500" }),
497
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums w-7 text-right", children: textSize })
498
+ ] })
374
499
  ] }),
375
500
  /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
376
- /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(ZOOM_MIN, Math.round((z - ZOOM_STEP) * 100) / 100)), className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", title: "Zoom out", children: "\u2212" }),
377
- /* @__PURE__ */ jsxs("span", { className: "text-[11px] text-gray-600 tabular-nums w-10 text-center", children: [
501
+ /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(ZOOM_MIN, Math.round((z - ZOOM_STEP) * 100) / 100)), className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", title: "Zoom out", children: "\u2212" }),
502
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-600 tabular-nums w-10 text-center", children: [
378
503
  Math.round(zoom * 100),
379
504
  "%"
380
505
  ] }),
381
- /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(ZOOM_MAX, Math.round((z + ZOOM_STEP) * 100) / 100)), className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", title: "Zoom in", children: "+" }),
382
- /* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", title: "Fit to area", children: "Fit" }),
506
+ /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(ZOOM_MAX, Math.round((z + ZOOM_STEP) * 100) / 100)), className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", title: "Zoom in", children: "+" }),
507
+ /* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", title: "Fit to area", children: "Fit" }),
383
508
  /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
384
- /* @__PURE__ */ jsx("button", { onClick: undoLast, disabled: annotations.length === 0, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 disabled:opacity-30 text-gray-700", children: "Undo" }),
385
- /* @__PURE__ */ jsx("button", { onClick: copyToClipboard, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Copy" }),
386
- /* @__PURE__ */ jsx("button", { onClick: downloadAnnotated, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Save" }),
387
- /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
388
- pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
389
- /* @__PURE__ */ jsx("button", { onClick: applyCrop, className: "px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600", children: "Apply Crop" }),
390
- /* @__PURE__ */ jsx("button", { onClick: cancelCrop, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Cancel" })
391
- ] }),
392
- /* @__PURE__ */ jsx("button", { onClick: onClose, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Exit" })
393
- ] })
509
+ /* @__PURE__ */ jsx("button", { onClick: undoLast, disabled: annotations.length === 0, className: "px-2 py-1 rounded hover:bg-gray-200 disabled:opacity-30 text-gray-700", children: "Undo" }),
510
+ /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-2", children: pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
511
+ /* @__PURE__ */ jsx("button", { onClick: applyCrop, className: "px-2 py-1 rounded bg-blue-500 text-white hover:bg-blue-600", children: "Apply Crop" }),
512
+ /* @__PURE__ */ jsx("button", { onClick: cancelCrop, className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", children: "Cancel" })
513
+ ] }) })
394
514
  ] }),
395
515
  /* @__PURE__ */ jsx("div", { ref: wrapRef, className: "flex-1 overflow-auto bg-gray-200 flex items-center justify-center p-4 relative", children: displaySize && imageSize && /* @__PURE__ */ jsxs(
396
516
  "div",
@@ -398,13 +518,7 @@ function ImageAnnotator({ src, filename, onClose }) {
398
518
  className: "relative shadow-lg rounded overflow-hidden bg-white shrink-0",
399
519
  style: { width: displaySize.w, height: displaySize.h },
400
520
  children: [
401
- /* @__PURE__ */ jsx(
402
- "canvas",
403
- {
404
- ref: canvasRef,
405
- style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }
406
- }
407
- ),
521
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" } }),
408
522
  /* @__PURE__ */ jsxs(
409
523
  "svg",
410
524
  {
@@ -443,6 +557,11 @@ function ImageAnnotator({ src, filename, onClose }) {
443
557
  {
444
558
  pendingText,
445
559
  color,
560
+ size: textSize,
561
+ font: FONTS.find((f) => f.id === textFont)?.css ?? FONTS[0].css,
562
+ bold: textBold,
563
+ italic: textItalic,
564
+ underline: textUnderline,
446
565
  scale,
447
566
  onChange: (value) => setPendingText({ ...pendingText, value }),
448
567
  onCommit: commitText,
@@ -452,16 +571,16 @@ function ImageAnnotator({ src, filename, onClose }) {
452
571
  ]
453
572
  }
454
573
  ) }),
455
- /* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 border-t border-gray-200 bg-white text-[11px] text-gray-500 shrink-0", children: tool === "select" ? selectedId ? "Drag to move. Delete / Backspace removes. Click outside to deselect." : "Tap a shape to select it. Double-click text to edit." : tool === "text" ? "Click to drop a text label." : tool === "crop" ? "Drag a rectangle to set the crop region." : "Drag on the image to draw." })
574
+ /* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 border-t border-gray-200 bg-white text-[11px] text-gray-500 shrink-0", children: tool === "select" ? selectedId ? "Drag to move. Drag a corner to resize. Delete / Backspace removes. Click outside to deselect." : "Tap a shape to select. Double-click text to edit." : tool === "text" ? "Click to drop a text label." : tool === "crop" ? "Drag a rectangle to set the crop region." : tool === "draw" ? "Drag to draw freehand." : "Drag on the image to draw." })
456
575
  ] });
457
- }
576
+ });
577
+ var ImageAnnotator_default = ImageAnnotator;
458
578
  function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDoubleClick, onHandlePointerDown }) {
459
579
  const dim = boundingBox(anno);
460
580
  const interactive = onPointerDown ? { onPointerDown, pointerEvents: "all", style: { cursor: "move" } } : {};
461
581
  const dblc = onDoubleClick ? { onDoubleClick } : {};
462
582
  let body;
463
583
  if (anno.type === "rect") {
464
- const radius = Math.min(anno.w, anno.h, 16) * 0.4;
465
584
  body = /* @__PURE__ */ jsx(
466
585
  "rect",
467
586
  {
@@ -469,8 +588,8 @@ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDo
469
588
  y: anno.y,
470
589
  width: anno.w,
471
590
  height: anno.h,
472
- rx: radius,
473
- ry: radius,
591
+ rx: anno.radius,
592
+ ry: anno.radius,
474
593
  fill: "none",
475
594
  stroke: anno.color,
476
595
  strokeWidth: anno.stroke,
@@ -495,6 +614,19 @@ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDo
495
614
  );
496
615
  } else if (anno.type === "arrow") {
497
616
  body = /* @__PURE__ */ jsx(ArrowShape, { anno, interactive });
617
+ } else if (anno.type === "draw") {
618
+ body = /* @__PURE__ */ jsx(
619
+ "path",
620
+ {
621
+ d: pointsToPath(anno.points),
622
+ fill: "none",
623
+ stroke: anno.color,
624
+ strokeWidth: anno.stroke,
625
+ strokeLinecap: "round",
626
+ strokeLinejoin: "round",
627
+ ...interactive
628
+ }
629
+ );
498
630
  } else if (anno.type === "mosaic") {
499
631
  body = /* @__PURE__ */ jsx(
500
632
  "rect",
@@ -511,6 +643,7 @@ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDo
511
643
  }
512
644
  );
513
645
  } else {
646
+ const font = FONTS.find((f) => f.id === anno.font)?.css ?? FONTS[0].css;
514
647
  body = /* @__PURE__ */ jsx(
515
648
  "text",
516
649
  {
@@ -518,8 +651,10 @@ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDo
518
651
  y: anno.y + anno.size,
519
652
  fill: anno.color,
520
653
  fontSize: anno.size,
521
- fontWeight: 600,
522
- fontFamily: "-apple-system, system-ui, sans-serif",
654
+ fontWeight: anno.bold ? 700 : 400,
655
+ fontStyle: anno.italic ? "italic" : "normal",
656
+ textDecoration: anno.underline ? "underline" : void 0,
657
+ fontFamily: font,
523
658
  style: { userSelect: "none" },
524
659
  ...interactive,
525
660
  ...dblc,
@@ -561,31 +696,81 @@ function ResizeHandles({
561
696
  stroke: "#3b82f6",
562
697
  strokeWidth: sw,
563
698
  pointerEvents: "all",
564
- style: { cursor }
699
+ style: { cursor },
700
+ "data-chrome": "handle"
565
701
  });
566
702
  if (anno.type === "arrow") {
567
703
  return /* @__PURE__ */ jsxs(Fragment, { children: [
568
- /* @__PURE__ */ jsx("circle", { cx: anno.x1, cy: anno.y1, r, ...handleProps("grab"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "start") }),
569
- /* @__PURE__ */ jsx("circle", { cx: anno.x2, cy: anno.y2, r, ...handleProps("grab"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "end") })
704
+ /* @__PURE__ */ jsx("circle", { cx: anno.x1, cy: anno.y1, r, ...handleProps("grab"), onPointerDown: (e) => onHandlePointerDown(e, "start") }),
705
+ /* @__PURE__ */ jsx("circle", { cx: anno.x2, cy: anno.y2, r, ...handleProps("grab"), onPointerDown: (e) => onHandlePointerDown(e, "end") })
570
706
  ] });
571
707
  }
572
- if (anno.type === "text") {
708
+ if (anno.type === "text" || anno.type === "draw") {
573
709
  return null;
574
710
  }
575
- const x = anno.x;
576
- const y = anno.y;
577
- const w = anno.w;
578
- const h = anno.h;
711
+ const x = anno.x, y = anno.y, w = anno.w, h = anno.h;
579
712
  return /* @__PURE__ */ jsxs(Fragment, { children: [
580
- /* @__PURE__ */ jsx("circle", { cx: x, cy: y, r, ...handleProps("nwse-resize"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "nw") }),
581
- /* @__PURE__ */ jsx("circle", { cx: x + w, cy: y, r, ...handleProps("nesw-resize"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "ne") }),
582
- /* @__PURE__ */ jsx("circle", { cx: x + w, cy: y + h, r, ...handleProps("nwse-resize"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "se") }),
583
- /* @__PURE__ */ jsx("circle", { cx: x, cy: y + h, r, ...handleProps("nesw-resize"), "data-chrome": "handle", onPointerDown: (e) => onHandlePointerDown(e, "sw") })
713
+ /* @__PURE__ */ jsx("circle", { cx: x, cy: y, r, ...handleProps("nwse-resize"), onPointerDown: (e) => onHandlePointerDown(e, "nw") }),
714
+ /* @__PURE__ */ jsx("circle", { cx: x + w, cy: y, r, ...handleProps("nesw-resize"), onPointerDown: (e) => onHandlePointerDown(e, "ne") }),
715
+ /* @__PURE__ */ jsx("circle", { cx: x + w, cy: y + h, r, ...handleProps("nwse-resize"), onPointerDown: (e) => onHandlePointerDown(e, "se") }),
716
+ /* @__PURE__ */ jsx("circle", { cx: x, cy: y + h, r, ...handleProps("nesw-resize"), onPointerDown: (e) => onHandlePointerDown(e, "sw") })
717
+ ] });
718
+ }
719
+ function ArrowShape({ anno, interactive }) {
720
+ const headLen = Math.max(12, anno.stroke * 4);
721
+ const angle = Math.atan2(anno.y2 - anno.y1, anno.x2 - anno.x1);
722
+ const a1 = angle + Math.PI - Math.PI / 7;
723
+ const a2 = angle + Math.PI + Math.PI / 7;
724
+ const head = `M${anno.x2},${anno.y2} L${anno.x2 + headLen * Math.cos(a1)},${anno.y2 + headLen * Math.sin(a1)} L${anno.x2 + headLen * Math.cos(a2)},${anno.y2 + headLen * Math.sin(a2)} Z`;
725
+ return /* @__PURE__ */ jsxs("g", { ...interactive, children: [
726
+ /* @__PURE__ */ jsx(
727
+ "line",
728
+ {
729
+ x1: anno.x1,
730
+ y1: anno.y1,
731
+ x2: anno.x2,
732
+ y2: anno.y2,
733
+ stroke: anno.color,
734
+ strokeWidth: anno.stroke,
735
+ strokeLinecap: "round"
736
+ }
737
+ ),
738
+ /* @__PURE__ */ jsx("path", { d: head, fill: anno.color })
739
+ ] });
740
+ }
741
+ function CropOverlay({ rect, imageSize }) {
742
+ return /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
743
+ /* @__PURE__ */ jsx(
744
+ "path",
745
+ {
746
+ d: `M0,0 H${imageSize.w} V${imageSize.h} H0 Z M${rect.x},${rect.y} V${rect.y + rect.h} H${rect.x + rect.w} V${rect.y} Z`,
747
+ fill: "rgba(0,0,0,0.45)",
748
+ fillRule: "evenodd"
749
+ }
750
+ ),
751
+ /* @__PURE__ */ jsx(
752
+ "rect",
753
+ {
754
+ x: rect.x,
755
+ y: rect.y,
756
+ width: rect.w,
757
+ height: rect.h,
758
+ fill: "none",
759
+ stroke: "#fff",
760
+ strokeWidth: 2,
761
+ strokeDasharray: "8,6"
762
+ }
763
+ )
584
764
  ] });
585
765
  }
586
766
  function PendingTextEditor({
587
767
  pendingText,
588
768
  color,
769
+ size,
770
+ font,
771
+ bold,
772
+ italic,
773
+ underline,
589
774
  scale,
590
775
  onChange,
591
776
  onCommit,
@@ -628,64 +813,29 @@ function PendingTextEditor({
628
813
  },
629
814
  placeholder: "Type then Enter\u2026",
630
815
  rows: 1,
631
- className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5 text-sm outline-none resize-none shadow-md",
632
- style: { color, fontWeight: 600, minWidth: 80 }
816
+ className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5 outline-none resize-none shadow-md",
817
+ style: {
818
+ color,
819
+ fontSize: `${size * scale}px`,
820
+ fontFamily: font,
821
+ fontWeight: bold ? 700 : 400,
822
+ fontStyle: italic ? "italic" : "normal",
823
+ textDecoration: underline ? "underline" : void 0,
824
+ minWidth: 80
825
+ }
633
826
  }
634
827
  )
635
828
  }
636
829
  );
637
830
  }
638
- function ArrowShape({ anno, interactive }) {
639
- const headLen = Math.max(12, anno.stroke * 4);
640
- const angle = Math.atan2(anno.y2 - anno.y1, anno.x2 - anno.x1);
641
- const a1 = angle + Math.PI - Math.PI / 7;
642
- const a2 = angle + Math.PI + Math.PI / 7;
643
- const head = `M${anno.x2},${anno.y2} L${anno.x2 + headLen * Math.cos(a1)},${anno.y2 + headLen * Math.sin(a1)} L${anno.x2 + headLen * Math.cos(a2)},${anno.y2 + headLen * Math.sin(a2)} Z`;
644
- return /* @__PURE__ */ jsxs("g", { ...interactive, children: [
645
- /* @__PURE__ */ jsx(
646
- "line",
647
- {
648
- x1: anno.x1,
649
- y1: anno.y1,
650
- x2: anno.x2,
651
- y2: anno.y2,
652
- stroke: anno.color,
653
- strokeWidth: anno.stroke,
654
- strokeLinecap: "round"
655
- }
656
- ),
657
- /* @__PURE__ */ jsx("path", { d: head, fill: anno.color })
658
- ] });
659
- }
660
- function CropOverlay({ rect, imageSize }) {
661
- return /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
662
- /* @__PURE__ */ jsx(
663
- "path",
664
- {
665
- d: `M0,0 H${imageSize.w} V${imageSize.h} H0 Z M${rect.x},${rect.y} V${rect.y + rect.h} H${rect.x + rect.w} V${rect.y} Z`,
666
- fill: "rgba(0,0,0,0.45)",
667
- fillRule: "evenodd"
668
- }
669
- ),
670
- /* @__PURE__ */ jsx(
671
- "rect",
672
- {
673
- x: rect.x,
674
- y: rect.y,
675
- width: rect.w,
676
- height: rect.h,
677
- fill: "none",
678
- stroke: "#fff",
679
- strokeWidth: 2,
680
- strokeDasharray: "8,6"
681
- }
682
- )
683
- ] });
684
- }
685
- function makeShape(tool, start, end, color, stroke) {
686
- if (tool === "rect" || tool === "circle") {
831
+ function makeShape(tool, start, end, color, stroke, rectRadius) {
832
+ if (tool === "rect") {
833
+ const r = normalizeRect(start, end);
834
+ return { id: "", type: "rect", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke, radius: rectRadius };
835
+ }
836
+ if (tool === "circle") {
687
837
  const r = normalizeRect(start, end);
688
- return { id: "", type: tool, x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
838
+ return { id: "", type: "circle", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
689
839
  }
690
840
  if (tool === "arrow") {
691
841
  return { id: "", type: "arrow", x1: start.x, y1: start.y, x2: end.x, y2: end.y, color, stroke };
@@ -697,21 +847,15 @@ function makeShape(tool, start, end, color, stroke) {
697
847
  return null;
698
848
  }
699
849
  function isTrivial(a) {
700
- if (a.type === "arrow") {
701
- return Math.abs(a.x2 - a.x1) < 4 && Math.abs(a.y2 - a.y1) < 4;
702
- }
703
- if ("w" in a && "h" in a) {
704
- return a.w < 4 || a.h < 4;
705
- }
850
+ if (a.type === "arrow") return Math.abs(a.x2 - a.x1) < 4 && Math.abs(a.y2 - a.y1) < 4;
851
+ if (a.type === "draw") return a.points.length < 2;
852
+ if ("w" in a && "h" in a) return a.w < 4 || a.h < 4;
706
853
  return false;
707
854
  }
708
855
  function translate(a, dx, dy) {
709
- if (a.type === "arrow") {
710
- return { ...a, x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
711
- }
712
- if (a.type === "text") {
713
- return { ...a, x: a.x + dx, y: a.y + dy };
714
- }
856
+ if (a.type === "arrow") return { ...a, x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
857
+ if (a.type === "draw") return { ...a, points: a.points.map((p) => ({ x: p.x + dx, y: p.y + dy })) };
858
+ if (a.type === "text") return { ...a, x: a.x + dx, y: a.y + dy };
715
859
  return { ...a, x: a.x + dx, y: a.y + dy };
716
860
  }
717
861
  function resize(original, corner, p) {
@@ -720,7 +864,7 @@ function resize(original, corner, p) {
720
864
  if (corner === "end") return { ...original, x2: p.x, y2: p.y };
721
865
  return original;
722
866
  }
723
- if (original.type === "text") return original;
867
+ if (original.type === "text" || original.type === "draw") return original;
724
868
  const left = original.x;
725
869
  const right = original.x + original.w;
726
870
  const top = original.y;
@@ -751,6 +895,17 @@ function boundingBox(a) {
751
895
  const y = Math.min(a.y1, a.y2);
752
896
  return { x, y, w: Math.abs(a.x2 - a.x1), h: Math.abs(a.y2 - a.y1) };
753
897
  }
898
+ if (a.type === "draw") {
899
+ if (a.points.length === 0) return { x: 0, y: 0, w: 0, h: 0 };
900
+ let minX = a.points[0].x, maxX = a.points[0].x, minY = a.points[0].y, maxY = a.points[0].y;
901
+ for (const p of a.points) {
902
+ if (p.x < minX) minX = p.x;
903
+ if (p.x > maxX) maxX = p.x;
904
+ if (p.y < minY) minY = p.y;
905
+ if (p.y > maxY) maxY = p.y;
906
+ }
907
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
908
+ }
754
909
  if (a.type === "text") {
755
910
  const lines = a.text.split("\n");
756
911
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -769,6 +924,12 @@ function withinBounds(a, w, h) {
769
924
  const bb = boundingBox(a);
770
925
  return bb.x + bb.w > 0 && bb.y + bb.h > 0 && bb.x < w && bb.y < h;
771
926
  }
927
+ function pointsToPath(points) {
928
+ if (points.length === 0) return "";
929
+ let d = `M ${points[0].x} ${points[0].y}`;
930
+ for (let i = 1; i < points.length; i++) d += ` L ${points[i].x} ${points[i].y}`;
931
+ return d;
932
+ }
772
933
  function applyMosaic(ctx, rect) {
773
934
  const x = Math.round(Math.max(0, rect.x));
774
935
  const y = Math.round(Math.max(0, rect.y));
@@ -2188,6 +2349,7 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2188
2349
  const [zoom, setZoom] = useState(1);
2189
2350
  const [error, setError] = useState(false);
2190
2351
  const [annotating, setAnnotating] = useState(false);
2352
+ const annotatorRef = useRef(null);
2191
2353
  const handleDefaultDownload = () => {
2192
2354
  const a = document.createElement("a");
2193
2355
  a.href = url;
@@ -2195,22 +2357,35 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2195
2357
  a.click();
2196
2358
  };
2197
2359
  const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
2198
- if (annotating) {
2199
- return /* @__PURE__ */ jsx(ImageAnnotator, { src: url, filename, onClose: () => setAnnotating(false) });
2200
- }
2201
2360
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
2202
2361
  /* @__PURE__ */ jsxs(PanelActions, { children: [
2203
- /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(0.1, Math.round((z - 0.25) * 100) / 100)), className: btn, children: "\u2212" }),
2204
- /* @__PURE__ */ jsxs("span", { className: "text-gray-500 w-12 text-center tabular-nums", children: [
2205
- Math.round(zoom * 100),
2206
- "%"
2362
+ !annotating && /* @__PURE__ */ jsxs(Fragment, { children: [
2363
+ /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(0.1, Math.round((z - 0.25) * 100) / 100)), className: btn, children: "\u2212" }),
2364
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-500 w-12 text-center tabular-nums", children: [
2365
+ Math.round(zoom * 100),
2366
+ "%"
2367
+ ] }),
2368
+ /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(8, Math.round((z + 0.25) * 100) / 100)), className: btn, children: "+" }),
2369
+ /* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: btn, children: "1:1" }),
2370
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300 mx-1" }),
2371
+ /* @__PURE__ */ jsxs("button", { onClick: () => setAnnotating(true), className: btn, title: "Annotate this image", children: [
2372
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" }) }),
2373
+ "Annotate"
2374
+ ] })
2207
2375
  ] }),
2208
- /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(8, Math.round((z + 0.25) * 100) / 100)), className: btn, children: "+" }),
2209
- /* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: btn, children: "1:1" }),
2210
- /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300 mx-1" }),
2211
- /* @__PURE__ */ jsxs("button", { onClick: () => setAnnotating(true), className: btn, title: "Annotate this image", children: [
2212
- /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" }) }),
2213
- "Annotate"
2376
+ annotating && /* @__PURE__ */ jsxs(Fragment, { children: [
2377
+ /* @__PURE__ */ jsxs("button", { onClick: () => setAnnotating(false), className: btn, title: "Back to viewer", children: [
2378
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15.75 19.5L8.25 12l7.5-7.5" }) }),
2379
+ "View"
2380
+ ] }),
2381
+ /* @__PURE__ */ jsxs("button", { onClick: () => annotatorRef.current?.copy(), className: btn, children: [
2382
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" }) }),
2383
+ "Copy"
2384
+ ] }),
2385
+ /* @__PURE__ */ jsxs("button", { onClick: () => annotatorRef.current?.save(), className: btn, children: [
2386
+ /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }),
2387
+ "Save"
2388
+ ] })
2214
2389
  ] }),
2215
2390
  /* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
2216
2391
  /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) }),
@@ -2221,7 +2396,7 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2221
2396
  "Email"
2222
2397
  ] })
2223
2398
  ] }),
2224
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto bg-gray-100 flex items-center justify-center p-4", children: error ? /* @__PURE__ */ jsx("div", { className: "text-sm text-red-600", children: "Failed to load image." }) : /* @__PURE__ */ jsx(
2399
+ annotating ? /* @__PURE__ */ jsx(ImageAnnotator_default, { ref: annotatorRef, src: url, filename }) : /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto bg-gray-100 flex items-center justify-center p-4", children: error ? /* @__PURE__ */ jsx("div", { className: "text-sm text-red-600", children: "Failed to load image." }) : /* @__PURE__ */ jsx(
2225
2400
  "img",
2226
2401
  {
2227
2402
  src: url,
@@ -2235,5 +2410,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2235
2410
  }
2236
2411
 
2237
2412
  export { Preview, setPdfPreview };
2238
- //# sourceMappingURL=chunk-LJ6DLGTY.js.map
2239
- //# sourceMappingURL=chunk-LJ6DLGTY.js.map
2413
+ //# sourceMappingURL=chunk-6IV6OWF3.js.map
2414
+ //# sourceMappingURL=chunk-6IV6OWF3.js.map