react-os-shell 0.2.23 → 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);
@@ -29,10 +43,30 @@ function ImageAnnotator({ src, filename, onClose }) {
29
43
  const [pendingText, setPendingText] = useState(null);
30
44
  const [pendingCrop, setPendingCrop] = useState(null);
31
45
  const dragRef = useRef(null);
46
+ const [isDragging, setIsDragging] = useState(false);
32
47
  const displaySize = useMemo(() => {
33
48
  if (!fitSize) return null;
34
49
  return { w: fitSize.w * zoom, h: fitSize.h * zoom };
35
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]);
36
70
  useEffect(() => {
37
71
  const img = new Image();
38
72
  img.crossOrigin = "anonymous";
@@ -55,8 +89,7 @@ function ImageAnnotator({ src, filename, onClose }) {
55
89
  const availH = Math.max(0, r.height - 32);
56
90
  if (availW === 0 || availH === 0) return;
57
91
  const ratio = imageSize.w / imageSize.h;
58
- let w = imageSize.w;
59
- let h = imageSize.h;
92
+ let w = imageSize.w, h = imageSize.h;
60
93
  if (w > availW) {
61
94
  w = availW;
62
95
  h = w / ratio;
@@ -85,7 +118,7 @@ function ImageAnnotator({ src, filename, onClose }) {
85
118
  const ctx = c.getContext("2d");
86
119
  ctx.drawImage(img, 0, 0);
87
120
  for (const m of mosaicAnnos) applyMosaic(ctx, m);
88
- }, [imageSize, mosaicAnnos]);
121
+ }, [imageSize, mosaicAnnos, fitSize]);
89
122
  const evToImage = (e) => {
90
123
  const svg = svgRef.current;
91
124
  if (!svg) return { x: 0, y: 0 };
@@ -97,13 +130,77 @@ function ImageAnnotator({ src, filename, onClose }) {
97
130
  const t = pt.matrixTransform(ctm.inverse());
98
131
  return { x: t.x, y: t.y };
99
132
  };
133
+ const beginDrag = (drag) => {
134
+ dragRef.current = drag;
135
+ setIsDragging(true);
136
+ };
137
+ useEffect(() => {
138
+ if (!isDragging) return;
139
+ const onMove = (ev) => {
140
+ const drag = dragRef.current;
141
+ if (!drag) return;
142
+ const p = evToImage(ev);
143
+ if (drag.kind === "draw") {
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
+ }
151
+ } else if (drag.kind === "crop") {
152
+ setPendingCrop(normalizeRect(drag.start, p));
153
+ } else if (drag.kind === "move") {
154
+ const dx = p.x - drag.start.x;
155
+ const dy = p.y - drag.start.y;
156
+ setAnnotations((prev) => prev.map((a) => a.id === drag.id ? translate(drag.original, dx, dy) : a));
157
+ } else if (drag.kind === "resize") {
158
+ setAnnotations((prev) => prev.map((a) => a.id === drag.id ? resize(drag.original, drag.corner, p) : a));
159
+ }
160
+ };
161
+ const onUp = () => {
162
+ const drag = dragRef.current;
163
+ dragRef.current = null;
164
+ setIsDragging(false);
165
+ if (drag?.kind === "draw") {
166
+ setPreview((p) => {
167
+ if (!p || isTrivial(p)) return null;
168
+ const anno = { ...p, id: newId() };
169
+ setAnnotations((prev) => [...prev, anno]);
170
+ setSelectedId(anno.id);
171
+ setTool("select");
172
+ return null;
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);
184
+ }
185
+ };
186
+ window.addEventListener("pointermove", onMove);
187
+ window.addEventListener("pointerup", onUp);
188
+ window.addEventListener("pointercancel", onUp);
189
+ return () => {
190
+ window.removeEventListener("pointermove", onMove);
191
+ window.removeEventListener("pointerup", onUp);
192
+ window.removeEventListener("pointercancel", onUp);
193
+ };
194
+ }, [isDragging, tool, color, stroke, rectRadius]);
100
195
  const handleAnnoPointerDown = (e, anno) => {
101
196
  if (tool !== "select") return;
102
197
  e.stopPropagation();
103
198
  setSelectedId(anno.id);
104
- const start = evToImage(e);
105
- dragRef.current = { kind: "move", id: anno.id, start, original: anno };
106
- e.currentTarget.setPointerCapture?.(e.pointerId);
199
+ beginDrag({ kind: "move", id: anno.id, start: evToImage(e), original: anno });
200
+ };
201
+ const handleHandlePointerDown = (e, anno, corner) => {
202
+ e.stopPropagation();
203
+ beginDrag({ kind: "resize", id: anno.id, corner, start: evToImage(e), original: anno });
107
204
  };
108
205
  const handleSvgPointerDown = (e) => {
109
206
  if (tool === "select") {
@@ -117,44 +214,16 @@ function ImageAnnotator({ src, filename, onClose }) {
117
214
  }
118
215
  const start = evToImage(e);
119
216
  if (tool === "crop") {
120
- dragRef.current = { kind: "crop", start };
121
- e.currentTarget.setPointerCapture(e.pointerId);
217
+ beginDrag({ kind: "crop", start });
122
218
  return;
123
219
  }
124
- dragRef.current = { kind: "draw", start };
125
- e.currentTarget.setPointerCapture(e.pointerId);
126
- setPreview(makeShape(tool, start, start, color, stroke));
127
- };
128
- const handleSvgPointerMove = (e) => {
129
- const drag = dragRef.current;
130
- if (!drag) return;
131
- const p = evToImage(e);
132
- if (drag.kind === "draw") {
133
- setPreview(makeShape(tool, drag.start, p, color, stroke));
134
- } else if (drag.kind === "crop") {
135
- setPendingCrop(normalizeRect(drag.start, p));
136
- } else if (drag.kind === "move") {
137
- const dx = p.x - drag.start.x;
138
- const dy = p.y - drag.start.y;
139
- setAnnotations((prev) => prev.map((a) => a.id === drag.id ? translate(drag.original, dx, dy) : a));
220
+ if (tool === "draw") {
221
+ beginDrag({ kind: "pen", points: [start] });
222
+ setPreview({ id: "", type: "draw", points: [start], color, stroke });
223
+ return;
140
224
  }
141
- };
142
- const handleSvgPointerUp = (e) => {
143
- const drag = dragRef.current;
144
- if (!drag) return;
145
- dragRef.current = null;
146
- e.currentTarget.releasePointerCapture?.(e.pointerId);
147
- if (drag.kind === "draw") {
148
- const p = preview;
149
- if (!p) return;
150
- const tooSmall = isTrivial(p);
151
- setPreview(null);
152
- if (tooSmall) return;
153
- const anno = { ...p, id: newId() };
154
- setAnnotations((prev) => [...prev, anno]);
155
- setSelectedId(anno.id);
156
- setTool("select");
157
- } else if (drag.kind === "crop") ; else if (drag.kind === "move") ;
225
+ setPreview(makeShape(tool, start, start, color, stroke, rectRadius));
226
+ beginDrag({ kind: "draw", start });
158
227
  };
159
228
  const handleAnnoDoubleClick = (anno) => {
160
229
  if (anno.type !== "text") return;
@@ -162,8 +231,14 @@ function ImageAnnotator({ src, filename, onClose }) {
162
231
  };
163
232
  useEffect(() => {
164
233
  const onKey = (ev) => {
165
- if (!selectedId) return;
166
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;
167
242
  if (ev.key === "Delete" || ev.key === "Backspace") {
168
243
  ev.preventDefault();
169
244
  setAnnotations((prev) => prev.filter((a) => a.id !== selectedId));
@@ -175,17 +250,67 @@ function ImageAnnotator({ src, filename, onClose }) {
175
250
  window.addEventListener("keydown", onKey);
176
251
  return () => window.removeEventListener("keydown", onKey);
177
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
+ };
178
304
  const commitText = () => {
179
305
  if (!pendingText) return;
180
- const value = pendingText.value.trim();
181
- if (!value) {
306
+ const value = pendingText.value;
307
+ if (!value.trim()) {
182
308
  if (pendingText.editingId) {
183
309
  setAnnotations((prev) => prev.filter((a) => a.id !== pendingText.editingId));
184
310
  }
185
311
  setPendingText(null);
186
312
  return;
187
313
  }
188
- const fontSize = Math.max(16, stroke * 6);
189
314
  if (pendingText.editingId) {
190
315
  setAnnotations((prev) => prev.map(
191
316
  (a) => a.id === pendingText.editingId && a.type === "text" ? { ...a, text: value } : a
@@ -198,7 +323,11 @@ function ImageAnnotator({ src, filename, onClose }) {
198
323
  y: pendingText.y,
199
324
  text: value,
200
325
  color,
201
- size: fontSize
326
+ size: textSize,
327
+ font: textFont,
328
+ bold: textBold,
329
+ italic: textItalic,
330
+ underline: textUnderline
202
331
  };
203
332
  setAnnotations((prev) => [...prev, anno]);
204
333
  setSelectedId(anno.id);
@@ -236,10 +365,10 @@ function ImageAnnotator({ src, filename, onClose }) {
236
365
  setAnnotations((prev) => prev.slice(0, -1));
237
366
  setSelectedId(null);
238
367
  };
239
- const downloadAnnotated = async () => {
368
+ const compositeToCanvas = async () => {
240
369
  const c = canvasRef.current;
241
370
  const svg = svgRef.current;
242
- if (!c || !svg || !imageSize) return;
371
+ if (!c || !svg || !imageSize) return null;
243
372
  const out = document.createElement("canvas");
244
373
  out.width = imageSize.w;
245
374
  out.height = imageSize.h;
@@ -251,30 +380,57 @@ function ImageAnnotator({ src, filename, onClose }) {
251
380
  const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
252
381
  const svgUrl = URL.createObjectURL(svgBlob);
253
382
  const svgImg = new Image();
254
- await new Promise((resolve, reject) => {
383
+ await new Promise((resolve) => {
255
384
  svgImg.onload = () => resolve();
256
- svgImg.onerror = reject;
385
+ svgImg.onerror = () => resolve();
257
386
  svgImg.src = svgUrl;
258
- }).catch(() => {
259
387
  });
260
388
  octx.drawImage(svgImg, 0, 0, imageSize.w, imageSize.h);
261
389
  URL.revokeObjectURL(svgUrl);
262
- out.toBlob((blob) => {
263
- if (!blob) {
264
- toast_default.error("Failed to export");
390
+ return out;
391
+ };
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");
265
413
  return;
266
414
  }
267
- const url = URL.createObjectURL(blob);
268
- const a = document.createElement("a");
269
- a.href = url;
270
- const base = filename.replace(/\.[^.]+$/, "");
271
- a.download = `${base}-annotated.png`;
272
- a.click();
273
- URL.revokeObjectURL(url);
274
- }, "image/png");
275
- };
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");
429
+ }
430
+ }), [imageSize, filename]);
276
431
  const tools = useMemo(() => [
277
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" }) }) },
278
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" }) }) },
279
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" }) }) },
280
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" }) }) },
@@ -283,9 +439,12 @@ function ImageAnnotator({ src, filename, onClose }) {
283
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" }) }) }
284
440
  ], []);
285
441
  const btnClass = (active) => `p-1.5 rounded transition-colors ${active ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"}`;
286
- 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";
287
446
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-gray-100", children: [
288
- /* @__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: [
289
448
  tools.map((t) => /* @__PURE__ */ jsx(
290
449
  "button",
291
450
  {
@@ -303,53 +462,55 @@ function ImageAnnotator({ src, filename, onClose }) {
303
462
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: COLORS.map((c) => /* @__PURE__ */ jsx(
304
463
  "button",
305
464
  {
306
- onClick: () => {
307
- setColor(c);
308
- if (selectedId) {
309
- setAnnotations((prev) => prev.map((a) => a.id === selectedId && a.type !== "mosaic" ? { ...a, color: c } : a));
310
- }
311
- },
465
+ onClick: () => setColorAndApply(c),
312
466
  title: c,
313
467
  className: `h-5 w-5 rounded-full border ${color === c ? "ring-2 ring-blue-500 ring-offset-1" : "border-gray-300"}`,
314
468
  style: { background: c }
315
469
  },
316
470
  c
317
471
  )) }),
318
- /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
319
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-[11px] text-gray-600", children: [
320
- /* @__PURE__ */ jsx("span", { children: "Size" }),
321
- /* @__PURE__ */ jsx(
322
- "input",
323
- {
324
- type: "range",
325
- min: 2,
326
- max: 12,
327
- step: 1,
328
- value: stroke,
329
- onChange: (e) => setStroke(Number(e.target.value)),
330
- className: "w-16 accent-blue-500"
331
- }
332
- ),
333
- /* @__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
+ ] })
334
499
  ] }),
335
500
  /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
336
- /* @__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" }),
337
- /* @__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: [
338
503
  Math.round(zoom * 100),
339
504
  "%"
340
505
  ] }),
341
- /* @__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: "+" }),
342
- /* @__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" }),
343
508
  /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
344
- /* @__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" }),
345
- /* @__PURE__ */ jsx("button", { onClick: downloadAnnotated, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Save" }),
346
- /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
347
- pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
348
- /* @__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" }),
349
- /* @__PURE__ */ jsx("button", { onClick: cancelCrop, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Cancel" })
350
- ] }),
351
- /* @__PURE__ */ jsx("button", { onClick: onClose, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Exit" })
352
- ] })
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
+ ] }) })
353
514
  ] }),
354
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(
355
516
  "div",
@@ -357,13 +518,7 @@ function ImageAnnotator({ src, filename, onClose }) {
357
518
  className: "relative shadow-lg rounded overflow-hidden bg-white shrink-0",
358
519
  style: { width: displaySize.w, height: displaySize.h },
359
520
  children: [
360
- /* @__PURE__ */ jsx(
361
- "canvas",
362
- {
363
- ref: canvasRef,
364
- style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }
365
- }
366
- ),
521
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" } }),
367
522
  /* @__PURE__ */ jsxs(
368
523
  "svg",
369
524
  {
@@ -379,20 +534,16 @@ function ImageAnnotator({ src, filename, onClose }) {
379
534
  cursor: tool === "select" ? "default" : tool === "text" ? "text" : "crosshair"
380
535
  },
381
536
  onPointerDown: handleSvgPointerDown,
382
- onPointerMove: handleSvgPointerMove,
383
- onPointerUp: handleSvgPointerUp,
384
- onPointerCancel: () => {
385
- dragRef.current = null;
386
- setPreview(null);
387
- },
388
537
  children: [
389
538
  annotations.map((a) => /* @__PURE__ */ jsx(
390
539
  AnnotationView,
391
540
  {
392
541
  anno: a,
393
542
  selected: selectedId === a.id,
543
+ zoom,
394
544
  onPointerDown: (e) => handleAnnoPointerDown(e, a),
395
- onDoubleClick: () => handleAnnoDoubleClick(a)
545
+ onDoubleClick: () => handleAnnoDoubleClick(a),
546
+ onHandlePointerDown: (e, corner) => handleHandlePointerDown(e, a, corner)
396
547
  },
397
548
  a.id
398
549
  )),
@@ -402,51 +553,34 @@ function ImageAnnotator({ src, filename, onClose }) {
402
553
  }
403
554
  ),
404
555
  pendingText && /* @__PURE__ */ jsx(
405
- "div",
556
+ PendingTextEditor,
406
557
  {
407
- style: {
408
- position: "absolute",
409
- left: `${pendingText.x * scale}px`,
410
- top: `${pendingText.y * scale}px`,
411
- transform: "translateY(-2px)",
412
- zIndex: 5
413
- },
414
- children: /* @__PURE__ */ jsx(
415
- "textarea",
416
- {
417
- autoFocus: true,
418
- value: pendingText.value,
419
- onChange: (e) => setPendingText({ ...pendingText, value: e.target.value }),
420
- onBlur: commitText,
421
- onKeyDown: (e) => {
422
- if (e.key === "Escape") {
423
- setPendingText(null);
424
- } else if (e.key === "Enter" && !e.shiftKey) {
425
- e.preventDefault();
426
- commitText();
427
- }
428
- },
429
- placeholder: "Type then Enter\u2026",
430
- rows: 1,
431
- className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5 text-sm outline-none resize-none shadow-md",
432
- style: { color, fontWeight: 600, minWidth: 80 }
433
- }
434
- )
558
+ pendingText,
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,
565
+ scale,
566
+ onChange: (value) => setPendingText({ ...pendingText, value }),
567
+ onCommit: commitText,
568
+ onCancel: () => setPendingText(null)
435
569
  }
436
570
  )
437
571
  ]
438
572
  }
439
573
  ) }),
440
- /* @__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." })
441
575
  ] });
442
- }
443
- function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick }) {
576
+ });
577
+ var ImageAnnotator_default = ImageAnnotator;
578
+ function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDoubleClick, onHandlePointerDown }) {
444
579
  const dim = boundingBox(anno);
445
- const interactive = onPointerDown ? { onPointerDown, style: { cursor: "move" } } : {};
580
+ const interactive = onPointerDown ? { onPointerDown, pointerEvents: "all", style: { cursor: "move" } } : {};
446
581
  const dblc = onDoubleClick ? { onDoubleClick } : {};
447
582
  let body;
448
583
  if (anno.type === "rect") {
449
- const radius = Math.min(anno.w, anno.h, 16) * 0.4;
450
584
  body = /* @__PURE__ */ jsx(
451
585
  "rect",
452
586
  {
@@ -454,8 +588,8 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
454
588
  y: anno.y,
455
589
  width: anno.w,
456
590
  height: anno.h,
457
- rx: radius,
458
- ry: radius,
591
+ rx: anno.radius,
592
+ ry: anno.radius,
459
593
  fill: "none",
460
594
  stroke: anno.color,
461
595
  strokeWidth: anno.stroke,
@@ -480,6 +614,19 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
480
614
  );
481
615
  } else if (anno.type === "arrow") {
482
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
+ );
483
630
  } else if (anno.type === "mosaic") {
484
631
  body = /* @__PURE__ */ jsx(
485
632
  "rect",
@@ -496,6 +643,7 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
496
643
  }
497
644
  );
498
645
  } else {
646
+ const font = FONTS.find((f) => f.id === anno.font)?.css ?? FONTS[0].css;
499
647
  body = /* @__PURE__ */ jsx(
500
648
  "text",
501
649
  {
@@ -503,8 +651,10 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
503
651
  y: anno.y + anno.size,
504
652
  fill: anno.color,
505
653
  fontSize: anno.size,
506
- fontWeight: 600,
507
- 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,
508
658
  style: { userSelect: "none" },
509
659
  ...interactive,
510
660
  ...dblc,
@@ -514,21 +664,56 @@ function AnnotationView({ anno, selected, preview, onPointerDown, onDoubleClick
514
664
  }
515
665
  return /* @__PURE__ */ jsxs("g", { children: [
516
666
  body,
517
- selected && !preview && /* @__PURE__ */ jsx(
518
- "rect",
519
- {
520
- "data-chrome": "selection",
521
- x: dim.x - 4,
522
- y: dim.y - 4,
523
- width: dim.w + 8,
524
- height: dim.h + 8,
525
- fill: "none",
526
- stroke: "#3b82f6",
527
- strokeWidth: 2,
528
- strokeDasharray: "6,4",
529
- pointerEvents: "none"
530
- }
531
- )
667
+ selected && !preview && /* @__PURE__ */ jsxs(Fragment, { children: [
668
+ /* @__PURE__ */ jsx(
669
+ "rect",
670
+ {
671
+ "data-chrome": "selection",
672
+ x: dim.x - 4,
673
+ y: dim.y - 4,
674
+ width: dim.w + 8,
675
+ height: dim.h + 8,
676
+ fill: "none",
677
+ stroke: "#3b82f6",
678
+ strokeWidth: 2 / zoom,
679
+ strokeDasharray: `${6 / zoom},${4 / zoom}`,
680
+ pointerEvents: "none"
681
+ }
682
+ ),
683
+ onHandlePointerDown && /* @__PURE__ */ jsx(ResizeHandles, { anno, zoom, onHandlePointerDown })
684
+ ] })
685
+ ] });
686
+ }
687
+ function ResizeHandles({
688
+ anno,
689
+ zoom,
690
+ onHandlePointerDown
691
+ }) {
692
+ const r = 6 / zoom;
693
+ const sw = 1.5 / zoom;
694
+ const handleProps = (cursor) => ({
695
+ fill: "#fff",
696
+ stroke: "#3b82f6",
697
+ strokeWidth: sw,
698
+ pointerEvents: "all",
699
+ style: { cursor },
700
+ "data-chrome": "handle"
701
+ });
702
+ if (anno.type === "arrow") {
703
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
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") })
706
+ ] });
707
+ }
708
+ if (anno.type === "text" || anno.type === "draw") {
709
+ return null;
710
+ }
711
+ const x = anno.x, y = anno.y, w = anno.w, h = anno.h;
712
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
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") })
532
717
  ] });
533
718
  }
534
719
  function ArrowShape({ anno, interactive }) {
@@ -578,10 +763,79 @@ function CropOverlay({ rect, imageSize }) {
578
763
  )
579
764
  ] });
580
765
  }
581
- function makeShape(tool, start, end, color, stroke) {
582
- if (tool === "rect" || tool === "circle") {
766
+ function PendingTextEditor({
767
+ pendingText,
768
+ color,
769
+ size,
770
+ font,
771
+ bold,
772
+ italic,
773
+ underline,
774
+ scale,
775
+ onChange,
776
+ onCommit,
777
+ onCancel
778
+ }) {
779
+ const ref = useRef(null);
780
+ useEffect(() => {
781
+ const id = requestAnimationFrame(() => {
782
+ ref.current?.focus();
783
+ ref.current?.select?.();
784
+ });
785
+ return () => cancelAnimationFrame(id);
786
+ }, []);
787
+ return /* @__PURE__ */ jsx(
788
+ "div",
789
+ {
790
+ style: {
791
+ position: "absolute",
792
+ left: `${pendingText.x * scale}px`,
793
+ top: `${pendingText.y * scale}px`,
794
+ transform: "translateY(-2px)",
795
+ zIndex: 5
796
+ },
797
+ onPointerDown: (e) => e.stopPropagation(),
798
+ children: /* @__PURE__ */ jsx(
799
+ "textarea",
800
+ {
801
+ ref,
802
+ value: pendingText.value,
803
+ onChange: (e) => onChange(e.target.value),
804
+ onBlur: onCommit,
805
+ onKeyDown: (e) => {
806
+ if (e.key === "Escape") {
807
+ e.preventDefault();
808
+ onCancel();
809
+ } else if (e.key === "Enter" && !e.shiftKey) {
810
+ e.preventDefault();
811
+ onCommit();
812
+ }
813
+ },
814
+ placeholder: "Type then Enter\u2026",
815
+ rows: 1,
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
+ }
826
+ }
827
+ )
828
+ }
829
+ );
830
+ }
831
+ function makeShape(tool, start, end, color, stroke, rectRadius) {
832
+ if (tool === "rect") {
583
833
  const r = normalizeRect(start, end);
584
- return { id: "", type: tool, x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
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") {
837
+ const r = normalizeRect(start, end);
838
+ return { id: "", type: "circle", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
585
839
  }
586
840
  if (tool === "arrow") {
587
841
  return { id: "", type: "arrow", x1: start.x, y1: start.y, x2: end.x, y2: end.y, color, stroke };
@@ -593,22 +847,47 @@ function makeShape(tool, start, end, color, stroke) {
593
847
  return null;
594
848
  }
595
849
  function isTrivial(a) {
596
- if (a.type === "arrow") {
597
- return Math.abs(a.x2 - a.x1) < 4 && Math.abs(a.y2 - a.y1) < 4;
598
- }
599
- if ("w" in a && "h" in a) {
600
- return a.w < 4 || a.h < 4;
601
- }
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;
602
853
  return false;
603
854
  }
604
855
  function translate(a, dx, dy) {
605
- if (a.type === "arrow") {
606
- return { ...a, x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
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 };
859
+ return { ...a, x: a.x + dx, y: a.y + dy };
860
+ }
861
+ function resize(original, corner, p) {
862
+ if (original.type === "arrow") {
863
+ if (corner === "start") return { ...original, x1: p.x, y1: p.y };
864
+ if (corner === "end") return { ...original, x2: p.x, y2: p.y };
865
+ return original;
607
866
  }
608
- if (a.type === "text") {
609
- return { ...a, x: a.x + dx, y: a.y + dy };
867
+ if (original.type === "text" || original.type === "draw") return original;
868
+ const left = original.x;
869
+ const right = original.x + original.w;
870
+ const top = original.y;
871
+ const bottom = original.y + original.h;
872
+ let x1 = left, y1 = top, x2 = right, y2 = bottom;
873
+ if (corner === "nw") {
874
+ x1 = p.x;
875
+ y1 = p.y;
610
876
  }
611
- return { ...a, x: a.x + dx, y: a.y + dy };
877
+ if (corner === "ne") {
878
+ x2 = p.x;
879
+ y1 = p.y;
880
+ }
881
+ if (corner === "se") {
882
+ x2 = p.x;
883
+ y2 = p.y;
884
+ }
885
+ if (corner === "sw") {
886
+ x1 = p.x;
887
+ y2 = p.y;
888
+ }
889
+ const r = normalizeRect({ x: x1, y: y1 }, { x: x2, y: y2 });
890
+ return { ...original, x: r.x, y: r.y, w: r.w, h: r.h };
612
891
  }
613
892
  function boundingBox(a) {
614
893
  if (a.type === "arrow") {
@@ -616,6 +895,17 @@ function boundingBox(a) {
616
895
  const y = Math.min(a.y1, a.y2);
617
896
  return { x, y, w: Math.abs(a.x2 - a.x1), h: Math.abs(a.y2 - a.y1) };
618
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
+ }
619
909
  if (a.type === "text") {
620
910
  const lines = a.text.split("\n");
621
911
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -634,6 +924,12 @@ function withinBounds(a, w, h) {
634
924
  const bb = boundingBox(a);
635
925
  return bb.x + bb.w > 0 && bb.y + bb.h > 0 && bb.x < w && bb.y < h;
636
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
+ }
637
933
  function applyMosaic(ctx, rect) {
638
934
  const x = Math.round(Math.max(0, rect.x));
639
935
  const y = Math.round(Math.max(0, rect.y));
@@ -2053,6 +2349,7 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2053
2349
  const [zoom, setZoom] = useState(1);
2054
2350
  const [error, setError] = useState(false);
2055
2351
  const [annotating, setAnnotating] = useState(false);
2352
+ const annotatorRef = useRef(null);
2056
2353
  const handleDefaultDownload = () => {
2057
2354
  const a = document.createElement("a");
2058
2355
  a.href = url;
@@ -2060,22 +2357,35 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2060
2357
  a.click();
2061
2358
  };
2062
2359
  const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
2063
- if (annotating) {
2064
- return /* @__PURE__ */ jsx(ImageAnnotator, { src: url, filename, onClose: () => setAnnotating(false) });
2065
- }
2066
2360
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
2067
2361
  /* @__PURE__ */ jsxs(PanelActions, { children: [
2068
- /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(0.1, Math.round((z - 0.25) * 100) / 100)), className: btn, children: "\u2212" }),
2069
- /* @__PURE__ */ jsxs("span", { className: "text-gray-500 w-12 text-center tabular-nums", children: [
2070
- Math.round(zoom * 100),
2071
- "%"
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
+ ] })
2072
2375
  ] }),
2073
- /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(8, Math.round((z + 0.25) * 100) / 100)), className: btn, children: "+" }),
2074
- /* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: btn, children: "1:1" }),
2075
- /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300 mx-1" }),
2076
- /* @__PURE__ */ jsxs("button", { onClick: () => setAnnotating(true), className: btn, title: "Annotate this image", children: [
2077
- /* @__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" }) }),
2078
- "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
+ ] })
2079
2389
  ] }),
2080
2390
  /* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
2081
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" }) }),
@@ -2086,7 +2396,7 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2086
2396
  "Email"
2087
2397
  ] })
2088
2398
  ] }),
2089
- /* @__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(
2090
2400
  "img",
2091
2401
  {
2092
2402
  src: url,
@@ -2100,5 +2410,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
2100
2410
  }
2101
2411
 
2102
2412
  export { Preview, setPdfPreview };
2103
- //# sourceMappingURL=chunk-HQJRNCTU.js.map
2104
- //# sourceMappingURL=chunk-HQJRNCTU.js.map
2413
+ //# sourceMappingURL=chunk-6IV6OWF3.js.map
2414
+ //# sourceMappingURL=chunk-6IV6OWF3.js.map