react-os-shell 3.16.2 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/{chunk-RNXBHVCZ.js → Browser-KXCAIGTI.js} +11 -13
  2. package/dist/Browser-KXCAIGTI.js.map +1 -0
  3. package/dist/{Calculator-TADU6MLU.js → Calculator-HDM5XUJQ.js} +4 -4
  4. package/dist/{Calculator-TADU6MLU.js.map → Calculator-HDM5XUJQ.js.map} +1 -1
  5. package/dist/{CurrencyConverter-VOYRUDXD.js → CurrencyConverter-PYK6QTXW.js} +4 -4
  6. package/dist/{CurrencyConverter-VOYRUDXD.js.map → CurrencyConverter-PYK6QTXW.js.map} +1 -1
  7. package/dist/{Documents-KQ2YMWX5.js → Documents-L7QLJ55H.js} +5 -4
  8. package/dist/Documents-L7QLJ55H.js.map +1 -0
  9. package/dist/{chunk-EFTDLYWR.js → Files-YXVD6YCR.js} +14 -14
  10. package/dist/Files-YXVD6YCR.js.map +1 -0
  11. package/dist/{Notepad-NOMFXBKY.js → Notepad-YHRY4V5U.js} +5 -4
  12. package/dist/Notepad-YHRY4V5U.js.map +1 -0
  13. package/dist/{PomodoroTimer-BQVEKF45.js → PomodoroTimer-LZDIIB5B.js} +5 -5
  14. package/dist/{PomodoroTimer-BQVEKF45.js.map → PomodoroTimer-LZDIIB5B.js.map} +1 -1
  15. package/dist/{chunk-4BSHCQM6.js → Preview-PYB2RDOP.js} +17 -1120
  16. package/dist/Preview-PYB2RDOP.js.map +1 -0
  17. package/dist/Spreadsheet-3JFPATSG.js +506 -0
  18. package/dist/Spreadsheet-3JFPATSG.js.map +1 -0
  19. package/dist/{Stock-MOZKPNGI.js → Stock-H2OKWJJY.js} +4 -4
  20. package/dist/{Stock-MOZKPNGI.js.map → Stock-H2OKWJJY.js.map} +1 -1
  21. package/dist/{Weather-MHLLLOB5.js → Weather-6ZS3IHVX.js} +4 -4
  22. package/dist/{Weather-MHLLLOB5.js.map → Weather-6ZS3IHVX.js.map} +1 -1
  23. package/dist/{WorldClock-MPI4DPOT.js → WorldClock-HVKWZENG.js} +4 -4
  24. package/dist/{WorldClock-MPI4DPOT.js.map → WorldClock-HVKWZENG.js.map} +1 -1
  25. package/dist/apps/index.d.ts +97 -75
  26. package/dist/apps/index.js +20 -21
  27. package/dist/apps/index.js.map +1 -1
  28. package/dist/chunk-4SQU5YV6.js +24 -0
  29. package/dist/chunk-4SQU5YV6.js.map +1 -0
  30. package/dist/chunk-BFBWLCO6.js +16 -0
  31. package/dist/chunk-BFBWLCO6.js.map +1 -0
  32. package/dist/chunk-BV6SQ46N.js +24 -0
  33. package/dist/chunk-BV6SQ46N.js.map +1 -0
  34. package/dist/chunk-DINNWYVB.js +98 -0
  35. package/dist/chunk-DINNWYVB.js.map +1 -0
  36. package/dist/{chunk-Z5EHMTK6.js → chunk-DUNE4UD7.js} +3 -3
  37. package/dist/{chunk-Z5EHMTK6.js.map → chunk-DUNE4UD7.js.map} +1 -1
  38. package/dist/{chunk-ZCL4X6PU.js → chunk-FPR25GPO.js} +3 -3
  39. package/dist/{chunk-ZCL4X6PU.js.map → chunk-FPR25GPO.js.map} +1 -1
  40. package/dist/{chunk-ZMDLIYIK.js → chunk-GP4Y3VCB.js} +4 -510
  41. package/dist/chunk-GP4Y3VCB.js.map +1 -0
  42. package/dist/chunk-I65D3VT3.js +7 -0
  43. package/dist/chunk-I65D3VT3.js.map +1 -0
  44. package/dist/{chunk-QCGWXJX3.js → chunk-KFINIFFS.js} +6 -8
  45. package/dist/chunk-KFINIFFS.js.map +1 -0
  46. package/dist/chunk-QU5L5ZYM.js +1103 -0
  47. package/dist/chunk-QU5L5ZYM.js.map +1 -0
  48. package/dist/{chunk-OPDXBMON.js → chunk-QY6X3DSL.js} +3 -3
  49. package/dist/{chunk-OPDXBMON.js.map → chunk-QY6X3DSL.js.map} +1 -1
  50. package/dist/{chunk-SJNGOGS5.js → chunk-UFR3CXIF.js} +4 -95
  51. package/dist/chunk-UFR3CXIF.js.map +1 -0
  52. package/dist/chunk-UPO23MX4.js +17 -0
  53. package/dist/chunk-UPO23MX4.js.map +1 -0
  54. package/dist/index.d.ts +12 -3
  55. package/dist/index.js +74 -19
  56. package/dist/index.js.map +1 -1
  57. package/package.json +1 -1
  58. package/dist/Browser-NM54NOXT.js +0 -7
  59. package/dist/Browser-NM54NOXT.js.map +0 -1
  60. package/dist/Documents-KQ2YMWX5.js.map +0 -1
  61. package/dist/Files-GOQMZWQU.js +0 -12
  62. package/dist/Files-GOQMZWQU.js.map +0 -1
  63. package/dist/Notepad-NOMFXBKY.js.map +0 -1
  64. package/dist/Preview-GGC5263S.js +0 -8
  65. package/dist/Preview-GGC5263S.js.map +0 -1
  66. package/dist/Spreadsheet-S6BVMBSM.js +0 -7
  67. package/dist/Spreadsheet-S6BVMBSM.js.map +0 -1
  68. package/dist/chunk-4BSHCQM6.js.map +0 -1
  69. package/dist/chunk-EFTDLYWR.js.map +0 -1
  70. package/dist/chunk-QCGWXJX3.js.map +0 -1
  71. package/dist/chunk-RNXBHVCZ.js.map +0 -1
  72. package/dist/chunk-SJNGOGS5.js.map +0 -1
  73. package/dist/chunk-ZMDLIYIK.js.map +0 -1
@@ -1,1106 +1,16 @@
1
+ import { ImageAnnotator_default } from './chunk-QU5L5ZYM.js';
1
2
  import { toast_default } from './chunk-VENYVK3L.js';
2
- import { AboutApp } from './chunk-QCGWXJX3.js';
3
- import { WindowTitle, registerModalEscapeInterceptor, getActiveModalId } from './chunk-ZCL4X6PU.js';
4
- import { forwardRef, useRef, useState, useMemo, useEffect, useImperativeHandle, createContext, useContext } from 'react';
3
+ import { peekPdfPreviewStage, claimPdfPreviewStage, PDF_PREVIEW_UPDATE_EVENT } from './chunk-BV6SQ46N.js';
4
+ import { AboutApp } from './chunk-KFINIFFS.js';
5
+ import './chunk-I65D3VT3.js';
6
+ import { WindowTitle, registerModalEscapeInterceptor, getActiveModalId } from './chunk-FPR25GPO.js';
7
+ import './chunk-UBN4IUDE.js';
8
+ import './chunk-ZF6AYO4G.js';
9
+ import { createContext, useRef, useEffect, useState, useContext } from 'react';
5
10
  import { createPortal } from 'react-dom';
6
11
  import * as pdfjsLib from 'pdfjs-dist';
7
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
13
 
9
- var COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6", "#000000", "#ffffff"];
10
- var FONTS = [
11
- { id: "system", label: "System", css: "-apple-system, system-ui, sans-serif" },
12
- { id: "serif", label: "Serif", css: 'Georgia, "Times New Roman", serif' },
13
- { id: "mono", label: "Mono", css: 'ui-monospace, "SF Mono", Menlo, monospace' },
14
- { id: "cursive", label: "Cursive", css: '"Brush Script MT", "Comic Sans MS", cursive' }
15
- ];
16
- var STROKE_DEFAULT = 4;
17
- var TEXT_SIZE_DEFAULT = 24;
18
- var TEXT_PADDING_DEFAULT = 6;
19
- var RECT_RADIUS_DEFAULT = 12;
20
- var MOSAIC_BLOCK = 12;
21
- var ZOOM_MIN = 0.25;
22
- var ZOOM_MAX = 4;
23
- var ZOOM_STEP = 0.25;
24
- var newId = () => Math.random().toString(36).slice(2, 9);
25
- var ImageAnnotator = forwardRef(function ImageAnnotator2({ src, filename, onApply, onCancel }, ref) {
26
- const wrapRef = useRef(null);
27
- const canvasRef = useRef(null);
28
- const svgRef = useRef(null);
29
- const imageRef = useRef(null);
30
- const [tool, setTool] = useState("select");
31
- const [color, setColor] = useState(COLORS[0]);
32
- const [stroke, setStroke] = useState(STROKE_DEFAULT);
33
- const [textSize, setTextSize] = useState(TEXT_SIZE_DEFAULT);
34
- const [textFont, setTextFont] = useState(FONTS[0].id);
35
- const [textBold, setTextBold] = useState(true);
36
- const [textItalic, setTextItalic] = useState(false);
37
- const [textUnderline, setTextUnderline] = useState(false);
38
- const [textBorderColor, setTextBorderColor] = useState(null);
39
- const [textBg, setTextBg] = useState(null);
40
- const [textPadding, setTextPadding] = useState(TEXT_PADDING_DEFAULT);
41
- const [rectRadius, setRectRadius] = useState(RECT_RADIUS_DEFAULT);
42
- const [annotations, setAnnotations] = useState([]);
43
- const [selectedId, setSelectedId] = useState(null);
44
- const [imageSize, setImageSize] = useState(null);
45
- const [fitSize, setFitSize] = useState(null);
46
- const [zoom, setZoom] = useState(1);
47
- const [preview, setPreview] = useState(null);
48
- const [pendingText, setPendingText] = useState(null);
49
- const [pendingCrop, setPendingCrop] = useState(null);
50
- const dragRef = useRef(null);
51
- const [isDragging, setIsDragging] = useState(false);
52
- const displaySize = useMemo(() => {
53
- if (!fitSize) return null;
54
- return { w: fitSize.w * zoom, h: fitSize.h * zoom };
55
- }, [fitSize, zoom]);
56
- const scale = displaySize && imageSize ? displaySize.w / imageSize.w : 1;
57
- const selected = useMemo(
58
- () => annotations.find((a) => a.id === selectedId) ?? null,
59
- [annotations, selectedId]
60
- );
61
- useEffect(() => {
62
- if (tool === "select" && selected) {
63
- if ("color" in selected) setColor(selected.color);
64
- if ("stroke" in selected) setStroke(selected.stroke);
65
- if (selected.type === "rect") setRectRadius(selected.radius);
66
- if (selected.type === "text") {
67
- setTextSize(selected.size);
68
- setTextFont(selected.font);
69
- setTextBold(selected.bold);
70
- setTextItalic(selected.italic);
71
- setTextUnderline(selected.underline);
72
- setTextBorderColor(selected.borderColor);
73
- setTextBg(selected.background);
74
- setTextPadding(selected.padding);
75
- }
76
- }
77
- }, [selectedId]);
78
- useEffect(() => {
79
- const img = new Image();
80
- img.crossOrigin = "anonymous";
81
- img.onload = () => {
82
- imageRef.current = img;
83
- setImageSize({ w: img.naturalWidth, h: img.naturalHeight });
84
- setAnnotations([]);
85
- setSelectedId(null);
86
- };
87
- img.onerror = () => toast_default.error("Failed to load image");
88
- img.src = src;
89
- }, [src]);
90
- useEffect(() => {
91
- if (!imageSize) return;
92
- const update = () => {
93
- const wrap = wrapRef.current;
94
- if (!wrap) return;
95
- const r = wrap.getBoundingClientRect();
96
- const availW = Math.max(0, r.width - 32);
97
- const availH = Math.max(0, r.height - 32);
98
- if (availW === 0 || availH === 0) return;
99
- const ratio = imageSize.w / imageSize.h;
100
- let w = imageSize.w, h = imageSize.h;
101
- if (w > availW) {
102
- w = availW;
103
- h = w / ratio;
104
- }
105
- if (h > availH) {
106
- h = availH;
107
- w = h * ratio;
108
- }
109
- setFitSize({ w, h });
110
- };
111
- update();
112
- const ro = new ResizeObserver(update);
113
- if (wrapRef.current) ro.observe(wrapRef.current);
114
- return () => ro.disconnect();
115
- }, [imageSize]);
116
- const mosaicAnnos = useMemo(
117
- () => annotations.filter((a) => a.type === "mosaic"),
118
- [annotations]
119
- );
120
- useEffect(() => {
121
- const img = imageRef.current;
122
- const c = canvasRef.current;
123
- if (!img || !c || !imageSize) return;
124
- c.width = imageSize.w;
125
- c.height = imageSize.h;
126
- const ctx = c.getContext("2d");
127
- ctx.drawImage(img, 0, 0);
128
- for (const m of mosaicAnnos) applyMosaic(ctx, m);
129
- }, [imageSize, mosaicAnnos, fitSize]);
130
- const evToImage = (e) => {
131
- const svg = svgRef.current;
132
- if (!svg) return { x: 0, y: 0 };
133
- const pt = svg.createSVGPoint();
134
- pt.x = e.clientX;
135
- pt.y = e.clientY;
136
- const ctm = svg.getScreenCTM();
137
- if (!ctm) return { x: 0, y: 0 };
138
- const t = pt.matrixTransform(ctm.inverse());
139
- return { x: t.x, y: t.y };
140
- };
141
- const beginDrag = (drag) => {
142
- dragRef.current = drag;
143
- setIsDragging(true);
144
- };
145
- useEffect(() => {
146
- if (!isDragging) return;
147
- const onMove = (ev) => {
148
- const drag = dragRef.current;
149
- if (!drag) return;
150
- const p = evToImage(ev);
151
- if (drag.kind === "draw") {
152
- setPreview(makeShape(tool, drag.start, p, color, stroke, rectRadius));
153
- } else if (drag.kind === "pen") {
154
- const last = drag.points[drag.points.length - 1];
155
- if (Math.abs(p.x - last.x) > 1 || Math.abs(p.y - last.y) > 1) {
156
- drag.points.push(p);
157
- setPreview({ id: "", type: "draw", points: drag.points.slice(), color, stroke });
158
- }
159
- } else if (drag.kind === "crop") {
160
- setPendingCrop(normalizeRect(drag.start, p));
161
- } else if (drag.kind === "move") {
162
- const dx = p.x - drag.start.x;
163
- const dy = p.y - drag.start.y;
164
- setAnnotations((prev) => prev.map((a) => a.id === drag.id ? translate(drag.original, dx, dy) : a));
165
- } else if (drag.kind === "resize") {
166
- setAnnotations((prev) => prev.map((a) => a.id === drag.id ? resize(drag.original, drag.corner, p) : a));
167
- }
168
- };
169
- const onUp = () => {
170
- const drag = dragRef.current;
171
- dragRef.current = null;
172
- setIsDragging(false);
173
- if (drag?.kind === "draw") {
174
- setPreview((p) => {
175
- if (!p || isTrivial(p)) return null;
176
- const anno = { ...p, id: newId() };
177
- setAnnotations((prev) => [...prev, anno]);
178
- setSelectedId(anno.id);
179
- setTool("select");
180
- return null;
181
- });
182
- } else if (drag?.kind === "pen") {
183
- if (drag.points.length < 2) {
184
- setPreview(null);
185
- return;
186
- }
187
- const anno = { id: newId(), type: "draw", points: drag.points, color, stroke };
188
- setAnnotations((prev) => [...prev, anno]);
189
- setSelectedId(anno.id);
190
- setTool("select");
191
- setPreview(null);
192
- }
193
- };
194
- window.addEventListener("pointermove", onMove);
195
- window.addEventListener("pointerup", onUp);
196
- window.addEventListener("pointercancel", onUp);
197
- return () => {
198
- window.removeEventListener("pointermove", onMove);
199
- window.removeEventListener("pointerup", onUp);
200
- window.removeEventListener("pointercancel", onUp);
201
- };
202
- }, [isDragging, tool, color, stroke, rectRadius]);
203
- const handleAnnoPointerDown = (e, anno) => {
204
- if (tool !== "select") return;
205
- e.stopPropagation();
206
- setSelectedId(anno.id);
207
- beginDrag({ kind: "move", id: anno.id, start: evToImage(e), original: anno });
208
- };
209
- const handleHandlePointerDown = (e, anno, corner) => {
210
- e.stopPropagation();
211
- beginDrag({ kind: "resize", id: anno.id, corner, start: evToImage(e), original: anno });
212
- };
213
- const handleSvgPointerDown = (e) => {
214
- if (tool === "select") {
215
- setSelectedId(null);
216
- return;
217
- }
218
- if (tool === "text") {
219
- if (pendingText) {
220
- commitText();
221
- return;
222
- }
223
- const p = evToImage(e);
224
- setPendingText({ x: p.x, y: p.y, value: "" });
225
- return;
226
- }
227
- const start = evToImage(e);
228
- if (tool === "crop") {
229
- beginDrag({ kind: "crop", start });
230
- return;
231
- }
232
- if (tool === "draw") {
233
- beginDrag({ kind: "pen", points: [start] });
234
- setPreview({ id: "", type: "draw", points: [start], color, stroke });
235
- return;
236
- }
237
- setPreview(makeShape(tool, start, start, color, stroke, rectRadius));
238
- beginDrag({ kind: "draw", start });
239
- };
240
- const handleAnnoDoubleClick = (anno) => {
241
- if (anno.type !== "text") return;
242
- setPendingText({ x: anno.x, y: anno.y, value: anno.text, editingId: anno.id });
243
- };
244
- useEffect(() => {
245
- const onKey = (ev) => {
246
- if (document.activeElement && /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) return;
247
- if ((ev.metaKey || ev.ctrlKey) && (ev.key === "z" || ev.key === "Z") && !ev.shiftKey) {
248
- ev.preventDefault();
249
- setAnnotations((prev) => prev.slice(0, -1));
250
- setSelectedId(null);
251
- return;
252
- }
253
- if (!selectedId) return;
254
- if (ev.key === "Delete" || ev.key === "Backspace") {
255
- ev.preventDefault();
256
- setAnnotations((prev) => prev.filter((a) => a.id !== selectedId));
257
- setSelectedId(null);
258
- } else if (ev.key === "Escape") {
259
- setSelectedId(null);
260
- }
261
- };
262
- window.addEventListener("keydown", onKey);
263
- return () => window.removeEventListener("keydown", onKey);
264
- }, [selectedId]);
265
- const setColorAndApply = (c) => {
266
- setColor(c);
267
- if (!selectedId) return;
268
- setAnnotations((prev) => prev.map((a) => {
269
- if (a.id !== selectedId) return a;
270
- if (a.type === "mosaic") return a;
271
- return { ...a, color: c };
272
- }));
273
- };
274
- const setStrokeAndApply = (s) => {
275
- setStroke(s);
276
- if (!selectedId) return;
277
- setAnnotations((prev) => prev.map((a) => {
278
- if (a.id !== selectedId) return a;
279
- if (a.type === "rect" || a.type === "circle" || a.type === "arrow" || a.type === "draw") {
280
- return { ...a, stroke: s };
281
- }
282
- return a;
283
- }));
284
- };
285
- const setRectRadiusAndApply = (r) => {
286
- setRectRadius(r);
287
- if (!selectedId) return;
288
- setAnnotations((prev) => prev.map(
289
- (a) => a.id === selectedId && a.type === "rect" ? { ...a, radius: r } : a
290
- ));
291
- };
292
- const setTextSizeAndApply = (s) => {
293
- setTextSize(s);
294
- if (!selectedId) return;
295
- setAnnotations((prev) => prev.map(
296
- (a) => a.id === selectedId && a.type === "text" ? { ...a, size: s } : a
297
- ));
298
- };
299
- const setTextFontAndApply = (f) => {
300
- setTextFont(f);
301
- if (!selectedId) return;
302
- setAnnotations((prev) => prev.map(
303
- (a) => a.id === selectedId && a.type === "text" ? { ...a, font: f } : a
304
- ));
305
- };
306
- const toggleTextStyleAndApply = (which) => {
307
- const next = !{ bold: textBold, italic: textItalic, underline: textUnderline }[which];
308
- if (which === "bold") setTextBold(next);
309
- if (which === "italic") setTextItalic(next);
310
- if (which === "underline") setTextUnderline(next);
311
- if (!selectedId) return;
312
- setAnnotations((prev) => prev.map(
313
- (a) => a.id === selectedId && a.type === "text" ? { ...a, [which]: next } : a
314
- ));
315
- };
316
- const setTextBorderColorAndApply = (c) => {
317
- setTextBorderColor(c);
318
- if (!selectedId) return;
319
- setAnnotations((prev) => prev.map(
320
- (a) => a.id === selectedId && a.type === "text" ? { ...a, borderColor: c } : a
321
- ));
322
- };
323
- const setTextBgAndApply = (c) => {
324
- setTextBg(c);
325
- if (!selectedId) return;
326
- setAnnotations((prev) => prev.map(
327
- (a) => a.id === selectedId && a.type === "text" ? { ...a, background: c } : a
328
- ));
329
- };
330
- const setTextPaddingAndApply = (p) => {
331
- setTextPadding(p);
332
- if (!selectedId) return;
333
- setAnnotations((prev) => prev.map(
334
- (a) => a.id === selectedId && a.type === "text" ? { ...a, padding: p } : a
335
- ));
336
- };
337
- const commitText = (explicitValue) => {
338
- if (!pendingText) return;
339
- const value = explicitValue ?? pendingText.value;
340
- if (!value.trim()) {
341
- if (pendingText.editingId) {
342
- setAnnotations((prev) => prev.filter((a) => a.id !== pendingText.editingId));
343
- }
344
- setPendingText(null);
345
- return;
346
- }
347
- if (pendingText.editingId) {
348
- setAnnotations((prev) => prev.map(
349
- (a) => a.id === pendingText.editingId && a.type === "text" ? { ...a, text: value } : a
350
- ));
351
- } else {
352
- const anno = {
353
- id: newId(),
354
- type: "text",
355
- x: pendingText.x,
356
- y: pendingText.y,
357
- text: value,
358
- color,
359
- size: textSize,
360
- font: textFont,
361
- bold: textBold,
362
- italic: textItalic,
363
- underline: textUnderline,
364
- borderColor: textBorderColor,
365
- background: textBg,
366
- padding: textPadding
367
- };
368
- setAnnotations((prev) => [...prev, anno]);
369
- setSelectedId(anno.id);
370
- setTool("select");
371
- }
372
- setPendingText(null);
373
- };
374
- const applyCrop = () => {
375
- if (!pendingCrop || !imageRef.current || !imageSize) return;
376
- const r = pendingCrop;
377
- const tmp = document.createElement("canvas");
378
- tmp.width = Math.round(r.w);
379
- tmp.height = Math.round(r.h);
380
- const tctx = tmp.getContext("2d");
381
- const sourceCanvas = document.createElement("canvas");
382
- sourceCanvas.width = imageSize.w;
383
- sourceCanvas.height = imageSize.h;
384
- const sctx = sourceCanvas.getContext("2d");
385
- sctx.drawImage(imageRef.current, 0, 0);
386
- for (const m of mosaicAnnos) applyMosaic(sctx, m);
387
- tctx.drawImage(sourceCanvas, r.x, r.y, r.w, r.h, 0, 0, r.w, r.h);
388
- const newImg = new Image();
389
- newImg.onload = () => {
390
- imageRef.current = newImg;
391
- setImageSize({ w: newImg.naturalWidth, h: newImg.naturalHeight });
392
- setAnnotations(
393
- (prev) => prev.filter((a) => a.type !== "mosaic").map((a) => translate(a, -r.x, -r.y)).filter((a) => withinBounds(a, newImg.naturalWidth, newImg.naturalHeight))
394
- );
395
- setPendingCrop(null);
396
- };
397
- newImg.src = tmp.toDataURL("image/png");
398
- };
399
- const cancelCrop = () => setPendingCrop(null);
400
- const undoLast = () => {
401
- setAnnotations((prev) => prev.slice(0, -1));
402
- setSelectedId(null);
403
- };
404
- const compositeToCanvas = async () => {
405
- const c = canvasRef.current;
406
- const svg = svgRef.current;
407
- if (!c || !svg || !imageSize) return null;
408
- const out = document.createElement("canvas");
409
- out.width = imageSize.w;
410
- out.height = imageSize.h;
411
- const octx = out.getContext("2d");
412
- octx.drawImage(c, 0, 0);
413
- const clone = svg.cloneNode(true);
414
- clone.querySelectorAll("[data-chrome]").forEach((n) => n.remove());
415
- const xml = new XMLSerializer().serializeToString(clone);
416
- const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
417
- const svgUrl = URL.createObjectURL(svgBlob);
418
- const svgImg = new Image();
419
- await new Promise((resolve) => {
420
- svgImg.onload = () => resolve();
421
- svgImg.onerror = () => resolve();
422
- svgImg.src = svgUrl;
423
- });
424
- octx.drawImage(svgImg, 0, 0, imageSize.w, imageSize.h);
425
- URL.revokeObjectURL(svgUrl);
426
- return out;
427
- };
428
- useImperativeHandle(ref, () => ({
429
- save: async () => {
430
- const out = await compositeToCanvas();
431
- if (!out) return;
432
- out.toBlob((blob) => {
433
- if (!blob) {
434
- toast_default.error("Failed to export");
435
- return;
436
- }
437
- const url = URL.createObjectURL(blob);
438
- const a = document.createElement("a");
439
- a.href = url;
440
- const base = filename.replace(/\.[^.]+$/, "");
441
- a.download = `${base}-annotated.png`;
442
- a.click();
443
- URL.revokeObjectURL(url);
444
- }, "image/png");
445
- },
446
- copy: async () => {
447
- if (!("clipboard" in navigator) || typeof ClipboardItem === "undefined") {
448
- toast_default.error("Clipboard images not supported in this browser");
449
- return;
450
- }
451
- const out = await compositeToCanvas();
452
- if (!out) return;
453
- out.toBlob(async (blob) => {
454
- if (!blob) {
455
- toast_default.error("Failed to copy");
456
- return;
457
- }
458
- try {
459
- await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
460
- toast_default.success("Copied to clipboard");
461
- } catch {
462
- toast_default.error("Copy failed (clipboard permission?)");
463
- }
464
- }, "image/png");
465
- }
466
- }), [imageSize, filename]);
467
- const tools = useMemo(() => [
468
- { 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" }) }) },
469
- { 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" }) }) },
470
- { 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" }) }) },
471
- { 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" }) }) },
472
- { 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" }) }) },
473
- { 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" }) }) },
474
- { 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" }) }) },
475
- { 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" }) }) }
476
- ], []);
477
- const btnClass = (active) => `p-1.5 rounded transition-colors ${active ? "bg-blue-500 text-white" : "text-gray-700 hover:bg-gray-200"}`;
478
- const ctxType = selected?.type ?? (tool === "select" ? null : tool);
479
- const showStrokeControl = ctxType === "rect" || ctxType === "circle" || ctxType === "arrow" || ctxType === "draw";
480
- const showRectRadius = ctxType === "rect";
481
- const showTextControls = ctxType === "text";
482
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-gray-100", children: [
483
- /* @__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: [
484
- tools.map((t) => /* @__PURE__ */ jsx(
485
- "button",
486
- {
487
- onClick: () => {
488
- setTool(t.id);
489
- setSelectedId(null);
490
- },
491
- title: t.label,
492
- className: btnClass(tool === t.id),
493
- children: t.icon
494
- },
495
- t.id
496
- )),
497
- /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
498
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: COLORS.map((c) => /* @__PURE__ */ jsx(
499
- "button",
500
- {
501
- onClick: () => setColorAndApply(c),
502
- title: c,
503
- className: `h-5 w-5 rounded-full border ${color === c ? "ring-2 ring-blue-500 ring-offset-1" : "border-gray-300"}`,
504
- style: { background: c }
505
- },
506
- c
507
- )) }),
508
- showStrokeControl && /* @__PURE__ */ jsxs(Fragment, { children: [
509
- /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
510
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
511
- /* @__PURE__ */ jsx("span", { children: "Weight" }),
512
- /* @__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" }),
513
- /* @__PURE__ */ jsx("span", { className: "tabular-nums w-5 text-right", children: stroke })
514
- ] })
515
- ] }),
516
- showRectRadius && /* @__PURE__ */ jsxs(Fragment, { children: [
517
- /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
518
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
519
- /* @__PURE__ */ jsx("span", { children: "Radius" }),
520
- /* @__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" }),
521
- /* @__PURE__ */ jsx("span", { className: "tabular-nums w-6 text-right", children: rectRadius })
522
- ] })
523
- ] }),
524
- showTextControls && /* @__PURE__ */ jsxs(Fragment, { children: [
525
- /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
526
- /* @__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)) }),
527
- /* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("bold"), className: btnClass(textBold), title: "Bold", children: /* @__PURE__ */ jsx("span", { className: "font-bold", children: "B" }) }),
528
- /* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("italic"), className: btnClass(textItalic), title: "Italic", children: /* @__PURE__ */ jsx("span", { className: "italic", children: "I" }) }),
529
- /* @__PURE__ */ jsx("button", { onClick: () => toggleTextStyleAndApply("underline"), className: btnClass(textUnderline), title: "Underline", children: /* @__PURE__ */ jsx("span", { className: "underline", children: "U" }) }),
530
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
531
- /* @__PURE__ */ jsx("span", { children: "Size" }),
532
- /* @__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" }),
533
- /* @__PURE__ */ jsx("span", { className: "tabular-nums w-7 text-right", children: textSize })
534
- ] }),
535
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1 text-gray-600", children: [
536
- /* @__PURE__ */ jsx("span", { children: "Border" }),
537
- textBorderColor ? /* @__PURE__ */ jsxs(Fragment, { children: [
538
- /* @__PURE__ */ jsx(
539
- "input",
540
- {
541
- type: "color",
542
- value: textBorderColor,
543
- onChange: (e) => setTextBorderColorAndApply(e.target.value),
544
- title: "Text-box border color",
545
- className: "h-5 w-6 rounded border border-gray-300 bg-white p-0 cursor-pointer"
546
- }
547
- ),
548
- /* @__PURE__ */ jsx("button", { onClick: () => setTextBorderColorAndApply(null), title: "Remove border", className: "px-0.5 text-gray-400 hover:text-gray-700", children: "\u2715" })
549
- ] }) : /* @__PURE__ */ jsx("button", { onClick: () => setTextBorderColorAndApply(color), title: "Add a border", className: "px-1.5 py-0.5 rounded border border-gray-300 text-gray-600 hover:bg-gray-100", children: "Add" })
550
- ] }),
551
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1 text-gray-600", children: [
552
- /* @__PURE__ */ jsx("span", { children: "Fill" }),
553
- textBg ? /* @__PURE__ */ jsxs(Fragment, { children: [
554
- /* @__PURE__ */ jsx(
555
- "input",
556
- {
557
- type: "color",
558
- value: textBg,
559
- onChange: (e) => setTextBgAndApply(e.target.value),
560
- title: "Text-box background color",
561
- className: "h-5 w-6 rounded border border-gray-300 bg-white p-0 cursor-pointer"
562
- }
563
- ),
564
- /* @__PURE__ */ jsx("button", { onClick: () => setTextBgAndApply(null), title: "Remove fill", className: "px-0.5 text-gray-400 hover:text-gray-700", children: "\u2715" })
565
- ] }) : /* @__PURE__ */ jsx("button", { onClick: () => setTextBgAndApply("#ffffff"), title: "Add a background fill", className: "px-1.5 py-0.5 rounded border border-gray-300 text-gray-600 hover:bg-gray-100", children: "Add" })
566
- ] }),
567
- /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-gray-600", children: [
568
- /* @__PURE__ */ jsx("span", { children: "Pad" }),
569
- /* @__PURE__ */ jsx("input", { type: "range", min: 0, max: 40, step: 1, value: textPadding, onChange: (e) => setTextPaddingAndApply(Number(e.target.value)), className: "w-14 accent-blue-500" }),
570
- /* @__PURE__ */ jsx("span", { className: "tabular-nums w-6 text-right", children: textPadding })
571
- ] })
572
- ] }),
573
- /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
574
- /* @__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" }),
575
- /* @__PURE__ */ jsxs("span", { className: "text-gray-600 tabular-nums w-10 text-center", children: [
576
- Math.round(zoom * 100),
577
- "%"
578
- ] }),
579
- /* @__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: "+" }),
580
- /* @__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" }),
581
- /* @__PURE__ */ jsx("div", { className: "h-5 w-px bg-gray-300 mx-1" }),
582
- /* @__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" }),
583
- /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
584
- pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
585
- /* @__PURE__ */ jsx("button", { onClick: applyCrop, className: "px-2 py-1 rounded bg-blue-500 text-white hover:bg-blue-600", children: "Apply Crop" }),
586
- /* @__PURE__ */ jsx("button", { onClick: cancelCrop, className: "px-2 py-1 rounded hover:bg-gray-200 text-gray-700", children: "Cancel" })
587
- ] }),
588
- onApply && !pendingCrop && /* @__PURE__ */ jsxs(Fragment, { children: [
589
- onCancel && /* @__PURE__ */ jsx("button", { onClick: onCancel, className: "px-3 py-1 rounded hover:bg-gray-200 text-gray-700 text-xs font-medium", children: "Cancel" }),
590
- /* @__PURE__ */ jsx(
591
- "button",
592
- {
593
- onClick: async () => {
594
- const out = await compositeToCanvas();
595
- if (!out) {
596
- toast_default.error("Failed to export");
597
- return;
598
- }
599
- out.toBlob((blob) => {
600
- if (!blob) {
601
- toast_default.error("Failed to export");
602
- return;
603
- }
604
- onApply(blob);
605
- }, "image/png");
606
- },
607
- className: "px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 text-xs font-medium",
608
- children: "Apply"
609
- }
610
- )
611
- ] })
612
- ] })
613
- ] }),
614
- /* @__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(
615
- "div",
616
- {
617
- className: "relative shadow-lg rounded overflow-hidden bg-white shrink-0",
618
- style: { width: displaySize.w, height: displaySize.h },
619
- children: [
620
- /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" } }),
621
- /* @__PURE__ */ jsxs(
622
- "svg",
623
- {
624
- ref: svgRef,
625
- viewBox: `0 0 ${imageSize.w} ${imageSize.h}`,
626
- preserveAspectRatio: "none",
627
- style: {
628
- position: "absolute",
629
- inset: 0,
630
- width: "100%",
631
- height: "100%",
632
- touchAction: "none",
633
- cursor: tool === "select" ? "default" : tool === "text" ? "text" : "crosshair"
634
- },
635
- onPointerDown: handleSvgPointerDown,
636
- children: [
637
- annotations.map((a) => /* @__PURE__ */ jsx(
638
- AnnotationView,
639
- {
640
- anno: a,
641
- selected: selectedId === a.id,
642
- zoom,
643
- onPointerDown: (e) => handleAnnoPointerDown(e, a),
644
- onDoubleClick: () => handleAnnoDoubleClick(a),
645
- onHandlePointerDown: (e, corner) => handleHandlePointerDown(e, a, corner)
646
- },
647
- a.id
648
- )),
649
- preview && /* @__PURE__ */ jsx(AnnotationView, { anno: { ...preview, id: "__preview" }, preview: true }),
650
- pendingCrop && /* @__PURE__ */ jsx(CropOverlay, { rect: pendingCrop, imageSize })
651
- ]
652
- }
653
- ),
654
- pendingText && /* @__PURE__ */ jsx(
655
- PendingTextEditor,
656
- {
657
- pendingText,
658
- color,
659
- size: textSize,
660
- font: FONTS.find((f) => f.id === textFont)?.css ?? FONTS[0].css,
661
- bold: textBold,
662
- italic: textItalic,
663
- underline: textUnderline,
664
- borderColor: textBorderColor,
665
- background: textBg,
666
- padding: textPadding,
667
- scale,
668
- onChange: (value) => setPendingText({ ...pendingText, value }),
669
- onCommit: commitText,
670
- onCancel: () => setPendingText(null)
671
- }
672
- )
673
- ]
674
- }
675
- ) }),
676
- /* @__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." })
677
- ] });
678
- });
679
- var ImageAnnotator_default = ImageAnnotator;
680
- function AnnotationView({ anno, selected, preview, zoom = 1, onPointerDown, onDoubleClick, onHandlePointerDown }) {
681
- const dim = boundingBox(anno);
682
- const interactive = onPointerDown ? { onPointerDown, pointerEvents: "all", style: { cursor: "move" } } : {};
683
- const dblc = onDoubleClick ? { onDoubleClick } : {};
684
- let body;
685
- if (anno.type === "rect") {
686
- body = /* @__PURE__ */ jsx(
687
- "rect",
688
- {
689
- x: anno.x,
690
- y: anno.y,
691
- width: anno.w,
692
- height: anno.h,
693
- rx: anno.radius,
694
- ry: anno.radius,
695
- fill: "none",
696
- stroke: anno.color,
697
- strokeWidth: anno.stroke,
698
- strokeLinecap: "round",
699
- strokeLinejoin: "round",
700
- ...interactive
701
- }
702
- );
703
- } else if (anno.type === "circle") {
704
- body = /* @__PURE__ */ jsx(
705
- "ellipse",
706
- {
707
- cx: anno.x + anno.w / 2,
708
- cy: anno.y + anno.h / 2,
709
- rx: anno.w / 2,
710
- ry: anno.h / 2,
711
- fill: "none",
712
- stroke: anno.color,
713
- strokeWidth: anno.stroke,
714
- ...interactive
715
- }
716
- );
717
- } else if (anno.type === "arrow") {
718
- body = /* @__PURE__ */ jsx(ArrowShape, { anno, interactive });
719
- } else if (anno.type === "draw") {
720
- body = /* @__PURE__ */ jsx(
721
- "path",
722
- {
723
- d: pointsToPath(anno.points),
724
- fill: "none",
725
- stroke: anno.color,
726
- strokeWidth: anno.stroke,
727
- strokeLinecap: "round",
728
- strokeLinejoin: "round",
729
- ...interactive
730
- }
731
- );
732
- } else if (anno.type === "mosaic") {
733
- body = /* @__PURE__ */ jsx(
734
- "rect",
735
- {
736
- x: anno.x,
737
- y: anno.y,
738
- width: anno.w,
739
- height: anno.h,
740
- fill: "rgba(0,0,0,0.001)",
741
- stroke: selected ? "#3b82f6" : "none",
742
- strokeWidth: selected ? 2 : 0,
743
- strokeDasharray: selected ? "6,4" : void 0,
744
- ...interactive
745
- }
746
- );
747
- } else {
748
- const font = FONTS.find((f) => f.id === anno.font)?.css ?? FONTS[0].css;
749
- const pad = anno.padding ?? 0;
750
- const hasBox = !!anno.borderColor || !!anno.background;
751
- const box = boundingBox(anno);
752
- const borderW = anno.borderColor ? Math.max(1, Math.round(anno.size / 12)) : 0;
753
- body = /* @__PURE__ */ jsxs("g", { ...interactive, ...dblc, children: [
754
- hasBox && /* @__PURE__ */ jsx(
755
- "rect",
756
- {
757
- x: box.x - pad,
758
- y: box.y - pad,
759
- width: Math.max(0, box.w) + pad * 2,
760
- height: Math.max(0, box.h) + pad * 2,
761
- rx: Math.min(8, pad),
762
- ry: Math.min(8, pad),
763
- fill: anno.background ?? "none",
764
- stroke: anno.borderColor ?? "none",
765
- strokeWidth: borderW
766
- }
767
- ),
768
- /* @__PURE__ */ jsx(
769
- "text",
770
- {
771
- x: anno.x,
772
- y: anno.y + anno.size,
773
- fill: anno.color,
774
- fontSize: anno.size,
775
- fontWeight: anno.bold ? 700 : 400,
776
- fontStyle: anno.italic ? "italic" : "normal",
777
- textDecoration: anno.underline ? "underline" : void 0,
778
- fontFamily: font,
779
- style: { userSelect: "none" },
780
- children: anno.text.split("\n").map((line, i) => /* @__PURE__ */ jsx("tspan", { x: anno.x, dy: i === 0 ? 0 : anno.size * 1.2, children: line }, i))
781
- }
782
- )
783
- ] });
784
- }
785
- return /* @__PURE__ */ jsxs("g", { children: [
786
- body,
787
- selected && !preview && /* @__PURE__ */ jsxs(Fragment, { children: [
788
- /* @__PURE__ */ jsx(
789
- "rect",
790
- {
791
- "data-chrome": "selection",
792
- x: dim.x - 4,
793
- y: dim.y - 4,
794
- width: dim.w + 8,
795
- height: dim.h + 8,
796
- fill: "none",
797
- stroke: "#3b82f6",
798
- strokeWidth: 2 / zoom,
799
- strokeDasharray: `${6 / zoom},${4 / zoom}`,
800
- pointerEvents: "none"
801
- }
802
- ),
803
- onHandlePointerDown && /* @__PURE__ */ jsx(ResizeHandles, { anno, zoom, onHandlePointerDown })
804
- ] })
805
- ] });
806
- }
807
- function ResizeHandles({
808
- anno,
809
- zoom,
810
- onHandlePointerDown
811
- }) {
812
- const r = 6 / zoom;
813
- const sw = 1.5 / zoom;
814
- const handleProps = (cursor) => ({
815
- fill: "#fff",
816
- stroke: "#3b82f6",
817
- strokeWidth: sw,
818
- pointerEvents: "all",
819
- style: { cursor },
820
- "data-chrome": "handle"
821
- });
822
- if (anno.type === "arrow") {
823
- return /* @__PURE__ */ jsxs(Fragment, { children: [
824
- /* @__PURE__ */ jsx("circle", { cx: anno.x1, cy: anno.y1, r, ...handleProps("grab"), onPointerDown: (e) => onHandlePointerDown(e, "start") }),
825
- /* @__PURE__ */ jsx("circle", { cx: anno.x2, cy: anno.y2, r, ...handleProps("grab"), onPointerDown: (e) => onHandlePointerDown(e, "end") })
826
- ] });
827
- }
828
- if (anno.type === "text" || anno.type === "draw") {
829
- return null;
830
- }
831
- const x = anno.x, y = anno.y, w = anno.w, h = anno.h;
832
- return /* @__PURE__ */ jsxs(Fragment, { children: [
833
- /* @__PURE__ */ jsx("circle", { cx: x, cy: y, r, ...handleProps("nwse-resize"), onPointerDown: (e) => onHandlePointerDown(e, "nw") }),
834
- /* @__PURE__ */ jsx("circle", { cx: x + w, cy: y, r, ...handleProps("nesw-resize"), onPointerDown: (e) => onHandlePointerDown(e, "ne") }),
835
- /* @__PURE__ */ jsx("circle", { cx: x + w, cy: y + h, r, ...handleProps("nwse-resize"), onPointerDown: (e) => onHandlePointerDown(e, "se") }),
836
- /* @__PURE__ */ jsx("circle", { cx: x, cy: y + h, r, ...handleProps("nesw-resize"), onPointerDown: (e) => onHandlePointerDown(e, "sw") })
837
- ] });
838
- }
839
- function ArrowShape({ anno, interactive }) {
840
- const headLen = Math.max(12, anno.stroke * 4);
841
- const angle = Math.atan2(anno.y2 - anno.y1, anno.x2 - anno.x1);
842
- const a1 = angle + Math.PI - Math.PI / 7;
843
- const a2 = angle + Math.PI + Math.PI / 7;
844
- const head = `M${anno.x2},${anno.y2} L${anno.x2 + headLen * Math.cos(a1)},${anno.y2 + headLen * Math.sin(a1)} L${anno.x2 + headLen * Math.cos(a2)},${anno.y2 + headLen * Math.sin(a2)} Z`;
845
- return /* @__PURE__ */ jsxs("g", { ...interactive, children: [
846
- /* @__PURE__ */ jsx(
847
- "line",
848
- {
849
- x1: anno.x1,
850
- y1: anno.y1,
851
- x2: anno.x2,
852
- y2: anno.y2,
853
- stroke: anno.color,
854
- strokeWidth: anno.stroke,
855
- strokeLinecap: "round"
856
- }
857
- ),
858
- /* @__PURE__ */ jsx("path", { d: head, fill: anno.color })
859
- ] });
860
- }
861
- function CropOverlay({ rect, imageSize }) {
862
- return /* @__PURE__ */ jsxs("g", { pointerEvents: "none", children: [
863
- /* @__PURE__ */ jsx(
864
- "path",
865
- {
866
- d: `M0,0 H${imageSize.w} V${imageSize.h} H0 Z M${rect.x},${rect.y} V${rect.y + rect.h} H${rect.x + rect.w} V${rect.y} Z`,
867
- fill: "rgba(0,0,0,0.45)",
868
- fillRule: "evenodd"
869
- }
870
- ),
871
- /* @__PURE__ */ jsx(
872
- "rect",
873
- {
874
- x: rect.x,
875
- y: rect.y,
876
- width: rect.w,
877
- height: rect.h,
878
- fill: "none",
879
- stroke: "#fff",
880
- strokeWidth: 2,
881
- strokeDasharray: "8,6"
882
- }
883
- )
884
- ] });
885
- }
886
- function PendingTextEditor({
887
- pendingText,
888
- color,
889
- size,
890
- font,
891
- bold,
892
- italic,
893
- underline,
894
- borderColor,
895
- background,
896
- padding,
897
- scale,
898
- onChange,
899
- onCommit,
900
- onCancel
901
- }) {
902
- const ref = useRef(null);
903
- useEffect(() => {
904
- const id = requestAnimationFrame(() => {
905
- ref.current?.focus();
906
- ref.current?.select?.();
907
- });
908
- return () => cancelAnimationFrame(id);
909
- }, []);
910
- const commit = () => onCommit(ref.current?.value);
911
- const borderW = borderColor ? Math.max(1, Math.round(size / 12)) : 0;
912
- return /* @__PURE__ */ jsx(
913
- "div",
914
- {
915
- style: {
916
- position: "absolute",
917
- left: `${pendingText.x * scale}px`,
918
- top: `${pendingText.y * scale}px`,
919
- transform: "translateY(-2px)",
920
- zIndex: 5
921
- },
922
- onPointerDown: (e) => e.stopPropagation(),
923
- children: /* @__PURE__ */ jsx(
924
- "textarea",
925
- {
926
- ref,
927
- value: pendingText.value,
928
- onChange: (e) => onChange(e.target.value),
929
- onBlur: commit,
930
- onKeyDown: (e) => {
931
- if (e.key === "Escape") {
932
- e.preventDefault();
933
- onCancel();
934
- } else if (e.key === "Enter" && !e.shiftKey) {
935
- e.preventDefault();
936
- commit();
937
- }
938
- },
939
- placeholder: "Type then Enter\u2026",
940
- rows: 1,
941
- className: "outline-none resize-none shadow-md",
942
- style: {
943
- color,
944
- fontSize: `${size * scale}px`,
945
- fontFamily: font,
946
- fontWeight: bold ? 700 : 400,
947
- fontStyle: italic ? "italic" : "normal",
948
- textDecoration: underline ? "underline" : void 0,
949
- minWidth: 80,
950
- // Mirror the committed box so the editor is WYSIWYG. With no custom
951
- // border, show a dashed hint so the edit box stays visible.
952
- padding: `${padding * scale}px`,
953
- borderRadius: `${Math.min(8, padding) * scale}px`,
954
- border: borderColor ? `${borderW * scale}px solid ${borderColor}` : "1px dashed #60a5fa",
955
- background: background ?? "rgba(255,255,255,0.95)"
956
- }
957
- }
958
- )
959
- }
960
- );
961
- }
962
- function makeShape(tool, start, end, color, stroke, rectRadius) {
963
- if (tool === "rect") {
964
- const r = normalizeRect(start, end);
965
- return { id: "", type: "rect", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke, radius: rectRadius };
966
- }
967
- if (tool === "circle") {
968
- const r = normalizeRect(start, end);
969
- return { id: "", type: "circle", x: r.x, y: r.y, w: r.w, h: r.h, color, stroke };
970
- }
971
- if (tool === "arrow") {
972
- return { id: "", type: "arrow", x1: start.x, y1: start.y, x2: end.x, y2: end.y, color, stroke };
973
- }
974
- if (tool === "mosaic") {
975
- const r = normalizeRect(start, end);
976
- return { id: "", type: "mosaic", x: r.x, y: r.y, w: r.w, h: r.h };
977
- }
978
- return null;
979
- }
980
- function isTrivial(a) {
981
- if (a.type === "arrow") return Math.abs(a.x2 - a.x1) < 4 && Math.abs(a.y2 - a.y1) < 4;
982
- if (a.type === "draw") return a.points.length < 2;
983
- if ("w" in a && "h" in a) return a.w < 4 || a.h < 4;
984
- return false;
985
- }
986
- function translate(a, dx, dy) {
987
- if (a.type === "arrow") return { ...a, x1: a.x1 + dx, y1: a.y1 + dy, x2: a.x2 + dx, y2: a.y2 + dy };
988
- if (a.type === "draw") return { ...a, points: a.points.map((p) => ({ x: p.x + dx, y: p.y + dy })) };
989
- if (a.type === "text") return { ...a, x: a.x + dx, y: a.y + dy };
990
- return { ...a, x: a.x + dx, y: a.y + dy };
991
- }
992
- function resize(original, corner, p) {
993
- if (original.type === "arrow") {
994
- if (corner === "start") return { ...original, x1: p.x, y1: p.y };
995
- if (corner === "end") return { ...original, x2: p.x, y2: p.y };
996
- return original;
997
- }
998
- if (original.type === "text" || original.type === "draw") return original;
999
- const left = original.x;
1000
- const right = original.x + original.w;
1001
- const top = original.y;
1002
- const bottom = original.y + original.h;
1003
- let x1 = left, y1 = top, x2 = right, y2 = bottom;
1004
- if (corner === "nw") {
1005
- x1 = p.x;
1006
- y1 = p.y;
1007
- }
1008
- if (corner === "ne") {
1009
- x2 = p.x;
1010
- y1 = p.y;
1011
- }
1012
- if (corner === "se") {
1013
- x2 = p.x;
1014
- y2 = p.y;
1015
- }
1016
- if (corner === "sw") {
1017
- x1 = p.x;
1018
- y2 = p.y;
1019
- }
1020
- const r = normalizeRect({ x: x1, y: y1 }, { x: x2, y: y2 });
1021
- return { ...original, x: r.x, y: r.y, w: r.w, h: r.h };
1022
- }
1023
- function boundingBox(a) {
1024
- if (a.type === "arrow") {
1025
- const x = Math.min(a.x1, a.x2);
1026
- const y = Math.min(a.y1, a.y2);
1027
- return { x, y, w: Math.abs(a.x2 - a.x1), h: Math.abs(a.y2 - a.y1) };
1028
- }
1029
- if (a.type === "draw") {
1030
- if (a.points.length === 0) return { x: 0, y: 0, w: 0, h: 0 };
1031
- let minX = a.points[0].x, maxX = a.points[0].x, minY = a.points[0].y, maxY = a.points[0].y;
1032
- for (const p of a.points) {
1033
- if (p.x < minX) minX = p.x;
1034
- if (p.x > maxX) maxX = p.x;
1035
- if (p.y < minY) minY = p.y;
1036
- if (p.y > maxY) maxY = p.y;
1037
- }
1038
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1039
- }
1040
- if (a.type === "text") {
1041
- const lines = a.text.split("\n");
1042
- const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
1043
- return { x: a.x, y: a.y, w: longest * a.size * 0.55, h: lines.length * a.size * 1.2 };
1044
- }
1045
- return { x: a.x, y: a.y, w: a.w, h: a.h };
1046
- }
1047
- function normalizeRect(a, b) {
1048
- const x = Math.min(a.x, b.x);
1049
- const y = Math.min(a.y, b.y);
1050
- const w = Math.abs(a.x - b.x);
1051
- const h = Math.abs(a.y - b.y);
1052
- return { x, y, w, h };
1053
- }
1054
- function withinBounds(a, w, h) {
1055
- const bb = boundingBox(a);
1056
- return bb.x + bb.w > 0 && bb.y + bb.h > 0 && bb.x < w && bb.y < h;
1057
- }
1058
- function pointsToPath(points) {
1059
- if (points.length === 0) return "";
1060
- let d = `M ${points[0].x} ${points[0].y}`;
1061
- for (let i = 1; i < points.length; i++) d += ` L ${points[i].x} ${points[i].y}`;
1062
- return d;
1063
- }
1064
- function applyMosaic(ctx, rect) {
1065
- const x = Math.round(Math.max(0, rect.x));
1066
- const y = Math.round(Math.max(0, rect.y));
1067
- const w = Math.round(Math.min(ctx.canvas.width - x, rect.w));
1068
- const h = Math.round(Math.min(ctx.canvas.height - y, rect.h));
1069
- if (w < 2 || h < 2) return;
1070
- const data = ctx.getImageData(x, y, w, h);
1071
- const block = MOSAIC_BLOCK;
1072
- for (let by = 0; by < h; by += block) {
1073
- for (let bx = 0; bx < w; bx += block) {
1074
- let r = 0, g = 0, b = 0, a = 0, n = 0;
1075
- const blockW = Math.min(block, w - bx);
1076
- const blockH = Math.min(block, h - by);
1077
- for (let yy = 0; yy < blockH; yy++) {
1078
- for (let xx = 0; xx < blockW; xx++) {
1079
- const i = ((by + yy) * w + (bx + xx)) * 4;
1080
- r += data.data[i];
1081
- g += data.data[i + 1];
1082
- b += data.data[i + 2];
1083
- a += data.data[i + 3];
1084
- n++;
1085
- }
1086
- }
1087
- r = Math.round(r / n);
1088
- g = Math.round(g / n);
1089
- b = Math.round(b / n);
1090
- a = Math.round(a / n);
1091
- for (let yy = 0; yy < blockH; yy++) {
1092
- for (let xx = 0; xx < blockW; xx++) {
1093
- const i = ((by + yy) * w + (bx + xx)) * 4;
1094
- data.data[i] = r;
1095
- data.data[i + 1] = g;
1096
- data.data[i + 2] = b;
1097
- data.data[i + 3] = a;
1098
- }
1099
- }
1100
- }
1101
- }
1102
- ctx.putImageData(data, x, y);
1103
- }
1104
14
  var ToolbarSlotContext = createContext(null);
1105
15
  function PanelActions({ children }) {
1106
16
  const slot = useContext(ToolbarSlotContext);
@@ -1127,27 +37,14 @@ function truncateForTitle(s) {
1127
37
  if (typeof window !== "undefined" && !pdfjsLib.GlobalWorkerOptions.workerSrc) {
1128
38
  pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
1129
39
  }
1130
- var EVENT_NAME = "react-os-shell:pdf-preview-update";
1131
- var pending = null;
1132
- var nextToken = 0;
1133
- function setPdfPreview(data) {
1134
- const token = ++nextToken;
1135
- pending = { token, data };
1136
- return {
1137
- update(next) {
1138
- if (typeof window === "undefined") return;
1139
- window.dispatchEvent(new CustomEvent(EVENT_NAME, { detail: { token, data: next } }));
1140
- }
1141
- };
1142
- }
1143
40
  function Preview() {
1144
41
  const consumedRef = useRef(void 0);
1145
42
  if (consumedRef.current === void 0) {
1146
- consumedRef.current = pending;
43
+ consumedRef.current = peekPdfPreviewStage();
1147
44
  }
1148
45
  useEffect(() => {
1149
- if (consumedRef.current !== null && pending === consumedRef.current) {
1150
- pending = null;
46
+ if (consumedRef.current != null) {
47
+ claimPdfPreviewStage(consumedRef.current);
1151
48
  }
1152
49
  }, []);
1153
50
  const [data, setData] = useState(consumedRef.current?.data ?? null);
@@ -1164,8 +61,8 @@ function Preview() {
1164
61
  return detail.data;
1165
62
  });
1166
63
  };
1167
- window.addEventListener(EVENT_NAME, handler);
1168
- return () => window.removeEventListener(EVENT_NAME, handler);
64
+ window.addEventListener(PDF_PREVIEW_UPDATE_EVENT, handler);
65
+ return () => window.removeEventListener(PDF_PREVIEW_UPDATE_EVENT, handler);
1169
66
  }, []);
1170
67
  useEffect(() => () => {
1171
68
  if (data?.url?.startsWith("blob:")) URL.revokeObjectURL(data.url);
@@ -4388,6 +3285,6 @@ function ImagePanel({ url, filename, onDownload, onEmail }) {
4388
3285
  ] });
4389
3286
  }
4390
3287
 
4391
- export { ImageAnnotator_default, Preview, setPdfPreview };
4392
- //# sourceMappingURL=chunk-4BSHCQM6.js.map
4393
- //# sourceMappingURL=chunk-4BSHCQM6.js.map
3288
+ export { Preview as default };
3289
+ //# sourceMappingURL=Preview-PYB2RDOP.js.map
3290
+ //# sourceMappingURL=Preview-PYB2RDOP.js.map