react-os-shell 0.2.20 → 0.2.22

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