react-os-shell 0.2.20 → 0.2.21

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,10 +1,442 @@
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 } from 'react';
3
+ import { createContext, useState, useEffect, useRef, useContext, useMemo } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import * as pdfjsLib from 'pdfjs-dist';
6
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
7
 
8
+ var COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6", "#000000", "#ffffff"];
9
+ var STROKE_DEFAULT = 4;
10
+ var MOSAIC_BLOCK = 12;
11
+ function ImageAnnotator({ src, filename, onClose }) {
12
+ const canvasRef = useRef(null);
13
+ const overlayRef = useRef(null);
14
+ const wrapRef = useRef(null);
15
+ const [tool, setTool] = useState("rect");
16
+ const [color, setColor] = useState(COLORS[0]);
17
+ const [stroke, setStroke] = useState(STROKE_DEFAULT);
18
+ const historyRef = useRef([]);
19
+ const [historyDepth, setHistoryDepth] = useState(0);
20
+ const [imageReady, setImageReady] = useState(false);
21
+ const [pendingText, setPendingText] = useState(null);
22
+ const [pendingCrop, setPendingCrop] = useState(null);
23
+ const [displayScale, setDisplayScale] = useState(1);
24
+ useEffect(() => {
25
+ const img = new Image();
26
+ img.crossOrigin = "anonymous";
27
+ img.onload = () => {
28
+ const c = canvasRef.current;
29
+ const o = overlayRef.current;
30
+ if (!c || !o) return;
31
+ c.width = img.naturalWidth;
32
+ c.height = img.naturalHeight;
33
+ o.width = img.naturalWidth;
34
+ o.height = img.naturalHeight;
35
+ const ctx = c.getContext("2d");
36
+ ctx.drawImage(img, 0, 0);
37
+ historyRef.current = [];
38
+ setHistoryDepth(0);
39
+ setImageReady(true);
40
+ };
41
+ img.onerror = () => toast_default.error("Failed to load image");
42
+ img.src = src;
43
+ }, [src]);
44
+ useEffect(() => {
45
+ if (!imageReady) return;
46
+ const update = () => {
47
+ const c = canvasRef.current;
48
+ if (!c) return;
49
+ const rect = c.getBoundingClientRect();
50
+ if (rect.width > 0) setDisplayScale(c.width / rect.width);
51
+ };
52
+ update();
53
+ const ro = new ResizeObserver(update);
54
+ if (wrapRef.current) ro.observe(wrapRef.current);
55
+ return () => ro.disconnect();
56
+ }, [imageReady]);
57
+ const pushHistory = () => {
58
+ const c = canvasRef.current;
59
+ if (!c) return;
60
+ const ctx = c.getContext("2d");
61
+ historyRef.current.push(ctx.getImageData(0, 0, c.width, c.height));
62
+ if (historyRef.current.length > 50) historyRef.current.shift();
63
+ setHistoryDepth(historyRef.current.length);
64
+ };
65
+ const undo = () => {
66
+ if (historyRef.current.length === 0) return;
67
+ const prev = historyRef.current.pop();
68
+ setHistoryDepth(historyRef.current.length);
69
+ const c = canvasRef.current;
70
+ if (!c) return;
71
+ c.getContext("2d").putImageData(prev, 0, 0);
72
+ setPendingCrop(null);
73
+ clearOverlay();
74
+ };
75
+ const clearOverlay = () => {
76
+ const o = overlayRef.current;
77
+ if (!o) return;
78
+ o.getContext("2d").clearRect(0, 0, o.width, o.height);
79
+ };
80
+ const evToCanvas = (e) => {
81
+ const rect = e.currentTarget.getBoundingClientRect();
82
+ const x = (e.clientX - rect.left) * displayScale;
83
+ const y = (e.clientY - rect.top) * displayScale;
84
+ return { x, y };
85
+ };
86
+ const dragRef = useRef(null);
87
+ const drawShapePreview = (start, end) => {
88
+ const o = overlayRef.current;
89
+ if (!o) return;
90
+ const ctx = o.getContext("2d");
91
+ ctx.clearRect(0, 0, o.width, o.height);
92
+ ctx.strokeStyle = color;
93
+ ctx.fillStyle = color;
94
+ ctx.lineWidth = stroke;
95
+ ctx.lineCap = "round";
96
+ ctx.lineJoin = "round";
97
+ if (tool === "rect") {
98
+ drawRoundedRect(ctx, start, end, stroke);
99
+ } else if (tool === "circle") {
100
+ drawEllipse(ctx, start, end);
101
+ } else if (tool === "arrow") {
102
+ drawArrow(ctx, start, end, stroke);
103
+ } else if (tool === "mosaic") {
104
+ ctx.fillStyle = `${color}33`;
105
+ ctx.strokeStyle = color;
106
+ ctx.lineWidth = 1.5;
107
+ const r = normalizeRect(start, end);
108
+ ctx.fillRect(r.x, r.y, r.w, r.h);
109
+ ctx.strokeRect(r.x, r.y, r.w, r.h);
110
+ } else if (tool === "crop") {
111
+ ctx.strokeStyle = "#ffffff";
112
+ ctx.lineWidth = 2;
113
+ ctx.setLineDash([8, 6]);
114
+ const r = normalizeRect(start, end);
115
+ ctx.fillStyle = "rgba(0,0,0,0.45)";
116
+ ctx.fillRect(0, 0, o.width, r.y);
117
+ ctx.fillRect(0, r.y + r.h, o.width, o.height - (r.y + r.h));
118
+ ctx.fillRect(0, r.y, r.x, r.h);
119
+ ctx.fillRect(r.x + r.w, r.y, o.width - (r.x + r.w), r.h);
120
+ ctx.strokeRect(r.x, r.y, r.w, r.h);
121
+ ctx.setLineDash([]);
122
+ }
123
+ };
124
+ const onPointerDown = (e) => {
125
+ if (!imageReady) return;
126
+ const pos = evToCanvas(e);
127
+ if (tool === "text") {
128
+ setPendingText({ x: pos.x, y: pos.y, value: "" });
129
+ return;
130
+ }
131
+ e.currentTarget.setPointerCapture(e.pointerId);
132
+ dragRef.current = { start: pos, current: pos };
133
+ drawShapePreview(pos, pos);
134
+ };
135
+ const onPointerMove = (e) => {
136
+ if (!dragRef.current) return;
137
+ const pos = evToCanvas(e);
138
+ dragRef.current.current = pos;
139
+ drawShapePreview(dragRef.current.start, pos);
140
+ };
141
+ const onPointerUp = (e) => {
142
+ if (!dragRef.current) return;
143
+ e.currentTarget.releasePointerCapture(e.pointerId);
144
+ const { start, current } = dragRef.current;
145
+ dragRef.current = null;
146
+ if (Math.abs(current.x - start.x) < 2 && Math.abs(current.y - start.y) < 2) {
147
+ clearOverlay();
148
+ return;
149
+ }
150
+ if (tool === "crop") {
151
+ const r = normalizeRect(start, current);
152
+ setPendingCrop(r);
153
+ return;
154
+ }
155
+ pushHistory();
156
+ const c = canvasRef.current;
157
+ const ctx = c.getContext("2d");
158
+ ctx.strokeStyle = color;
159
+ ctx.fillStyle = color;
160
+ ctx.lineWidth = stroke;
161
+ ctx.lineCap = "round";
162
+ ctx.lineJoin = "round";
163
+ if (tool === "rect") drawRoundedRect(ctx, start, current, stroke);
164
+ else if (tool === "circle") drawEllipse(ctx, start, current);
165
+ else if (tool === "arrow") drawArrow(ctx, start, current, stroke);
166
+ else if (tool === "mosaic") applyMosaic(ctx, normalizeRect(start, current));
167
+ clearOverlay();
168
+ };
169
+ const commitText = () => {
170
+ if (!pendingText) return;
171
+ if (!pendingText.value.trim()) {
172
+ setPendingText(null);
173
+ return;
174
+ }
175
+ pushHistory();
176
+ const c = canvasRef.current;
177
+ const ctx = c.getContext("2d");
178
+ const fontSize = Math.max(16, stroke * 6);
179
+ ctx.fillStyle = color;
180
+ ctx.font = `600 ${fontSize}px -apple-system, system-ui, sans-serif`;
181
+ ctx.textBaseline = "top";
182
+ const lines = pendingText.value.split("\n");
183
+ lines.forEach((line, i) => {
184
+ ctx.fillText(line, pendingText.x, pendingText.y + i * fontSize * 1.2);
185
+ });
186
+ setPendingText(null);
187
+ };
188
+ const applyCrop = () => {
189
+ if (!pendingCrop) return;
190
+ const c = canvasRef.current;
191
+ pushHistory();
192
+ const r = pendingCrop;
193
+ const data = c.getContext("2d").getImageData(r.x, r.y, r.w, r.h);
194
+ c.width = Math.round(r.w);
195
+ c.height = Math.round(r.h);
196
+ overlayRef.current.width = c.width;
197
+ overlayRef.current.height = c.height;
198
+ c.getContext("2d").putImageData(data, 0, 0);
199
+ setPendingCrop(null);
200
+ clearOverlay();
201
+ requestAnimationFrame(() => {
202
+ const rect = c.getBoundingClientRect();
203
+ if (rect.width > 0) setDisplayScale(c.width / rect.width);
204
+ });
205
+ };
206
+ const cancelCrop = () => {
207
+ setPendingCrop(null);
208
+ clearOverlay();
209
+ };
210
+ const downloadAnnotated = () => {
211
+ const c = canvasRef.current;
212
+ if (!c) return;
213
+ c.toBlob((blob) => {
214
+ if (!blob) {
215
+ toast_default.error("Failed to export");
216
+ return;
217
+ }
218
+ const url = URL.createObjectURL(blob);
219
+ const a = document.createElement("a");
220
+ a.href = url;
221
+ const base = filename.replace(/\.[^.]+$/, "");
222
+ a.download = `${base}-annotated.png`;
223
+ a.click();
224
+ URL.revokeObjectURL(url);
225
+ }, "image/png");
226
+ };
227
+ const tools = useMemo(() => [
228
+ { 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" }) }) },
229
+ { 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" }) }) },
230
+ { 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" }) }) },
231
+ { id: "mosaic", label: "Mosaic", 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: "M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z" }) }) },
232
+ { id: "text", label: "Text", 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: "M4 6h16M12 6v14M9 20h6" }) }) },
233
+ { 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" }) }) }
234
+ ], []);
235
+ const btnClass = (active) => `p-1.5 rounded transition-colors ${active ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"}`;
236
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-gray-100", children: [
237
+ /* @__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: [
238
+ tools.map((t) => /* @__PURE__ */ jsx(
239
+ "button",
240
+ {
241
+ onClick: () => setTool(t.id),
242
+ title: t.label,
243
+ className: btnClass(tool === t.id),
244
+ children: t.icon
245
+ },
246
+ t.id
247
+ )),
248
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
249
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: COLORS.map((c) => /* @__PURE__ */ jsx(
250
+ "button",
251
+ {
252
+ onClick: () => setColor(c),
253
+ title: c,
254
+ className: `h-5 w-5 rounded-full border ${color === c ? "ring-2 ring-blue-500 ring-offset-1" : "border-gray-300"}`,
255
+ style: { background: c }
256
+ },
257
+ c
258
+ )) }),
259
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
260
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-[11px] text-gray-600", children: [
261
+ /* @__PURE__ */ jsx("span", { children: "Size" }),
262
+ /* @__PURE__ */ jsx(
263
+ "input",
264
+ {
265
+ type: "range",
266
+ min: 2,
267
+ max: 12,
268
+ step: 1,
269
+ value: stroke,
270
+ onChange: (e) => setStroke(Number(e.target.value)),
271
+ className: "w-16 accent-blue-500"
272
+ }
273
+ ),
274
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums w-4 text-right", children: stroke })
275
+ ] }),
276
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
277
+ /* @__PURE__ */ jsx("button", { onClick: undo, disabled: historyDepth === 0, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 disabled:opacity-30 text-gray-700", children: "Undo" }),
278
+ /* @__PURE__ */ jsx("button", { onClick: downloadAnnotated, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Save" }),
279
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
280
+ pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
281
+ /* @__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" }),
282
+ /* @__PURE__ */ jsx("button", { onClick: cancelCrop, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Cancel" })
283
+ ] }),
284
+ /* @__PURE__ */ jsx("button", { onClick: onClose, className: "px-2 py-1 text-xs rounded hover:bg-gray-200 text-gray-700", children: "Exit" })
285
+ ] })
286
+ ] }),
287
+ /* @__PURE__ */ jsx("div", { ref: wrapRef, className: "flex-1 overflow-auto bg-gray-200 flex items-center justify-center p-4 relative", children: /* @__PURE__ */ jsxs("div", { className: "relative inline-block max-w-full max-h-full", children: [
288
+ /* @__PURE__ */ jsx(
289
+ "canvas",
290
+ {
291
+ ref: canvasRef,
292
+ onPointerDown,
293
+ onPointerMove,
294
+ onPointerUp,
295
+ onPointerCancel: () => {
296
+ dragRef.current = null;
297
+ clearOverlay();
298
+ },
299
+ style: { touchAction: "none", maxWidth: "100%", maxHeight: "100%", display: "block", cursor: tool === "text" ? "text" : "crosshair" },
300
+ className: "shadow-lg rounded bg-white"
301
+ }
302
+ ),
303
+ /* @__PURE__ */ jsx(
304
+ "canvas",
305
+ {
306
+ ref: overlayRef,
307
+ style: {
308
+ position: "absolute",
309
+ inset: 0,
310
+ pointerEvents: "none",
311
+ maxWidth: "100%",
312
+ maxHeight: "100%",
313
+ width: "100%",
314
+ height: "100%"
315
+ }
316
+ }
317
+ ),
318
+ pendingText && /* @__PURE__ */ jsx(
319
+ "div",
320
+ {
321
+ style: {
322
+ position: "absolute",
323
+ left: `${pendingText.x / displayScale}px`,
324
+ top: `${pendingText.y / displayScale}px`,
325
+ transform: "translateY(-2px)"
326
+ },
327
+ className: "z-10",
328
+ children: /* @__PURE__ */ jsx(
329
+ "textarea",
330
+ {
331
+ autoFocus: true,
332
+ value: pendingText.value,
333
+ onChange: (e) => setPendingText({ ...pendingText, value: e.target.value }),
334
+ onBlur: commitText,
335
+ onKeyDown: (e) => {
336
+ if (e.key === "Escape") {
337
+ setPendingText(null);
338
+ } else if (e.key === "Enter" && !e.shiftKey) {
339
+ e.preventDefault();
340
+ commitText();
341
+ }
342
+ },
343
+ placeholder: "Type then Enter\u2026",
344
+ rows: 1,
345
+ className: "bg-white/95 border border-blue-400 rounded px-1 py-0.5 text-sm outline-none resize-none shadow-md",
346
+ style: { color, fontWeight: 600, minWidth: 80 }
347
+ }
348
+ )
349
+ }
350
+ )
351
+ ] }) })
352
+ ] });
353
+ }
354
+ function normalizeRect(a, b) {
355
+ const x = Math.min(a.x, b.x);
356
+ const y = Math.min(a.y, b.y);
357
+ const w = Math.abs(a.x - b.x);
358
+ const h = Math.abs(a.y - b.y);
359
+ return { x, y, w, h };
360
+ }
361
+ function drawRoundedRect(ctx, start, end, stroke) {
362
+ const r = normalizeRect(start, end);
363
+ const radius = Math.min(r.w, r.h, 16) * 0.4;
364
+ ctx.beginPath();
365
+ if (ctx.roundRect) {
366
+ ctx.roundRect(r.x, r.y, r.w, r.h, radius);
367
+ } else {
368
+ ctx.moveTo(r.x + radius, r.y);
369
+ ctx.arcTo(r.x + r.w, r.y, r.x + r.w, r.y + r.h, radius);
370
+ ctx.arcTo(r.x + r.w, r.y + r.h, r.x, r.y + r.h, radius);
371
+ ctx.arcTo(r.x, r.y + r.h, r.x, r.y, radius);
372
+ ctx.arcTo(r.x, r.y, r.x + r.w, r.y, radius);
373
+ ctx.closePath();
374
+ }
375
+ ctx.lineWidth = stroke;
376
+ ctx.stroke();
377
+ }
378
+ function drawEllipse(ctx, start, end) {
379
+ const r = normalizeRect(start, end);
380
+ ctx.beginPath();
381
+ ctx.ellipse(r.x + r.w / 2, r.y + r.h / 2, r.w / 2, r.h / 2, 0, 0, Math.PI * 2);
382
+ ctx.stroke();
383
+ }
384
+ function drawArrow(ctx, start, end, stroke) {
385
+ ctx.beginPath();
386
+ ctx.moveTo(start.x, start.y);
387
+ ctx.lineTo(end.x, end.y);
388
+ ctx.stroke();
389
+ const headLen = Math.max(12, stroke * 4);
390
+ const angle = Math.atan2(end.y - start.y, end.x - start.x);
391
+ const a1 = angle + Math.PI - Math.PI / 7;
392
+ const a2 = angle + Math.PI + Math.PI / 7;
393
+ ctx.beginPath();
394
+ ctx.moveTo(end.x, end.y);
395
+ ctx.lineTo(end.x + headLen * Math.cos(a1), end.y + headLen * Math.sin(a1));
396
+ ctx.lineTo(end.x + headLen * Math.cos(a2), end.y + headLen * Math.sin(a2));
397
+ ctx.closePath();
398
+ ctx.fill();
399
+ }
400
+ function applyMosaic(ctx, rect) {
401
+ const x = Math.round(Math.max(0, rect.x));
402
+ const y = Math.round(Math.max(0, rect.y));
403
+ const w = Math.round(Math.min(ctx.canvas.width - x, rect.w));
404
+ const h = Math.round(Math.min(ctx.canvas.height - y, rect.h));
405
+ if (w < 2 || h < 2) return;
406
+ const data = ctx.getImageData(x, y, w, h);
407
+ const block = MOSAIC_BLOCK;
408
+ for (let by = 0; by < h; by += block) {
409
+ for (let bx = 0; bx < w; bx += block) {
410
+ let r = 0, g = 0, b = 0, a = 0, n = 0;
411
+ const blockW = Math.min(block, w - bx);
412
+ const blockH = Math.min(block, h - by);
413
+ for (let yy = 0; yy < blockH; yy++) {
414
+ for (let xx = 0; xx < blockW; xx++) {
415
+ const i = ((by + yy) * w + (bx + xx)) * 4;
416
+ r += data.data[i];
417
+ g += data.data[i + 1];
418
+ b += data.data[i + 2];
419
+ a += data.data[i + 3];
420
+ n++;
421
+ }
422
+ }
423
+ r = Math.round(r / n);
424
+ g = Math.round(g / n);
425
+ b = Math.round(b / n);
426
+ a = Math.round(a / n);
427
+ for (let yy = 0; yy < blockH; yy++) {
428
+ for (let xx = 0; xx < blockW; xx++) {
429
+ const i = ((by + yy) * w + (bx + xx)) * 4;
430
+ data.data[i] = r;
431
+ data.data[i + 1] = g;
432
+ data.data[i + 2] = b;
433
+ data.data[i + 3] = a;
434
+ }
435
+ }
436
+ }
437
+ }
438
+ ctx.putImageData(data, x, y);
439
+ }
8
440
  var ToolbarSlotContext = createContext(null);
9
441
  function PanelActions({ children }) {
10
442
  const slot = useContext(ToolbarSlotContext);
@@ -1383,6 +1815,7 @@ function StepPanel({ url, filename, onDownload, onEmail }) {
1383
1815
  function ImagePanel({ url, filename, onDownload, onEmail }) {
1384
1816
  const [zoom, setZoom] = useState(1);
1385
1817
  const [error, setError] = useState(false);
1818
+ const [annotating, setAnnotating] = useState(false);
1386
1819
  const handleDefaultDownload = () => {
1387
1820
  const a = document.createElement("a");
1388
1821
  a.href = url;
@@ -1390,6 +1823,9 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
1390
1823
  a.click();
1391
1824
  };
1392
1825
  const btn = "px-2 py-1 rounded hover:bg-gray-200 transition-colors text-gray-600 flex items-center gap-1";
1826
+ if (annotating) {
1827
+ return /* @__PURE__ */ jsx(ImageAnnotator, { src: url, filename, onClose: () => setAnnotating(false) });
1828
+ }
1393
1829
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
1394
1830
  /* @__PURE__ */ jsxs(PanelActions, { children: [
1395
1831
  /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.max(0.1, Math.round((z - 0.25) * 100) / 100)), className: btn, children: "\u2212" }),
@@ -1400,6 +1836,10 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
1400
1836
  /* @__PURE__ */ jsx("button", { onClick: () => setZoom((z) => Math.min(8, Math.round((z + 0.25) * 100) / 100)), className: btn, children: "+" }),
1401
1837
  /* @__PURE__ */ jsx("button", { onClick: () => setZoom(1), className: btn, children: "1:1" }),
1402
1838
  /* @__PURE__ */ jsx("div", { className: "h-4 w-px bg-gray-300 mx-1" }),
1839
+ /* @__PURE__ */ jsxs("button", { onClick: () => setAnnotating(true), className: btn, title: "Annotate this image", children: [
1840
+ /* @__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" }) }),
1841
+ "Annotate"
1842
+ ] }),
1403
1843
  /* @__PURE__ */ jsxs("button", { onClick: onDownload ?? handleDefaultDownload, className: btn, children: [
1404
1844
  /* @__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" }) }),
1405
1845
  "Download"
@@ -1423,5 +1863,5 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
1423
1863
  }
1424
1864
 
1425
1865
  export { Preview, setPdfPreview };
1426
- //# sourceMappingURL=chunk-4J466VCS.js.map
1427
- //# sourceMappingURL=chunk-4J466VCS.js.map
1866
+ //# sourceMappingURL=chunk-YLXY6AZK.js.map
1867
+ //# sourceMappingURL=chunk-YLXY6AZK.js.map