viewdoc 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,946 @@
1
+ // src/core/ViewerShell.tsx
2
+ import { useCallback as useCallback2, useEffect, useRef, useState as useState2 } from "react";
3
+
4
+ // src/core/useDraggable.ts
5
+ import { useCallback, useState } from "react";
6
+ function useDraggable(options = {}) {
7
+ const { defaultPosition = { x: 80, y: 80 }, draggable = true } = options;
8
+ const [position, setPosition] = useState(defaultPosition);
9
+ const onDragHandlePointerDown = useCallback(
10
+ (e) => {
11
+ if (!draggable) return;
12
+ e.preventDefault();
13
+ const startX = e.clientX;
14
+ const startY = e.clientY;
15
+ setPosition((current) => {
16
+ const originX = current.x;
17
+ const originY = current.y;
18
+ const onMove = (ev) => {
19
+ setPosition({ x: originX + (ev.clientX - startX), y: originY + (ev.clientY - startY) });
20
+ };
21
+ const onUp = () => {
22
+ window.removeEventListener("pointermove", onMove);
23
+ window.removeEventListener("pointerup", onUp);
24
+ };
25
+ window.addEventListener("pointermove", onMove);
26
+ window.addEventListener("pointerup", onUp);
27
+ return current;
28
+ });
29
+ },
30
+ [draggable]
31
+ );
32
+ return { position, onDragHandlePointerDown };
33
+ }
34
+
35
+ // src/core/icons.tsx
36
+ import { jsx, jsxs } from "react/jsx-runtime";
37
+ function Svg(props) {
38
+ return /* @__PURE__ */ jsx(
39
+ "svg",
40
+ {
41
+ width: "16",
42
+ height: "16",
43
+ viewBox: "0 0 24 24",
44
+ fill: "none",
45
+ stroke: "currentColor",
46
+ strokeWidth: "2",
47
+ strokeLinecap: "round",
48
+ strokeLinejoin: "round",
49
+ "aria-hidden": "true",
50
+ focusable: "false",
51
+ ...props
52
+ }
53
+ );
54
+ }
55
+ function ZoomOutIcon() {
56
+ return /* @__PURE__ */ jsxs(Svg, { children: [
57
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
58
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" }),
59
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "11", x2: "14", y2: "11" })
60
+ ] });
61
+ }
62
+ function ZoomInIcon() {
63
+ return /* @__PURE__ */ jsxs(Svg, { children: [
64
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
65
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" }),
66
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "8", x2: "11", y2: "14" }),
67
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "11", x2: "14", y2: "11" })
68
+ ] });
69
+ }
70
+ function DownloadIcon() {
71
+ return /* @__PURE__ */ jsxs(Svg, { children: [
72
+ /* @__PURE__ */ jsx("path", { d: "M12 3v12" }),
73
+ /* @__PURE__ */ jsx("path", { d: "M7 10l5 5 5-5" }),
74
+ /* @__PURE__ */ jsx("path", { d: "M5 21h14" })
75
+ ] });
76
+ }
77
+ function FullscreenEnterIcon() {
78
+ return /* @__PURE__ */ jsxs(Svg, { children: [
79
+ /* @__PURE__ */ jsx("polyline", { points: "15 3 21 3 21 9" }),
80
+ /* @__PURE__ */ jsx("polyline", { points: "9 21 3 21 3 15" }),
81
+ /* @__PURE__ */ jsx("line", { x1: "21", y1: "3", x2: "14", y2: "10" }),
82
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "21", x2: "10", y2: "14" })
83
+ ] });
84
+ }
85
+ function ResetIcon() {
86
+ return /* @__PURE__ */ jsxs(Svg, { children: [
87
+ /* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 1 0 3-6.7" }),
88
+ /* @__PURE__ */ jsx("polyline", { points: "3 3 3 8 8 8" })
89
+ ] });
90
+ }
91
+ function GripIcon() {
92
+ return /* @__PURE__ */ jsxs(Svg, { children: [
93
+ /* @__PURE__ */ jsx("polyline", { points: "5 9 2 12 5 15" }),
94
+ /* @__PURE__ */ jsx("polyline", { points: "9 5 12 2 15 5" }),
95
+ /* @__PURE__ */ jsx("polyline", { points: "15 19 12 22 9 19" }),
96
+ /* @__PURE__ */ jsx("polyline", { points: "19 9 22 12 19 15" }),
97
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "12", x2: "22", y2: "12" }),
98
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "2", x2: "12", y2: "22" })
99
+ ] });
100
+ }
101
+ function FullscreenExitIcon() {
102
+ return /* @__PURE__ */ jsxs(Svg, { children: [
103
+ /* @__PURE__ */ jsx("polyline", { points: "9 3 9 9 3 9" }),
104
+ /* @__PURE__ */ jsx("polyline", { points: "15 21 15 15 21 15" }),
105
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "9", x2: "3", y2: "3" }),
106
+ /* @__PURE__ */ jsx("line", { x1: "15", y1: "15", x2: "21", y2: "21" })
107
+ ] });
108
+ }
109
+
110
+ // src/core/Toolbar.tsx
111
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
112
+ function Toolbar({
113
+ scale,
114
+ onZoomIn,
115
+ onZoomOut,
116
+ onReset,
117
+ onFullscreen,
118
+ onDownload,
119
+ isFullscreen = false,
120
+ showZoomControls = true,
121
+ dragHandle,
122
+ onDragHandlePointerDown,
123
+ extra
124
+ }) {
125
+ return /* @__PURE__ */ jsxs2("div", { className: "vd-toolbar", role: "toolbar", "aria-label": "Document viewer controls", children: [
126
+ dragHandle && /* @__PURE__ */ jsx2(
127
+ "span",
128
+ {
129
+ className: "vd-toolbar-drag-handle",
130
+ onPointerDown: onDragHandlePointerDown,
131
+ title: "Drag to move",
132
+ "aria-label": "Drag to move",
133
+ children: dragHandle
134
+ }
135
+ ),
136
+ showZoomControls && /* @__PURE__ */ jsxs2(Fragment, { children: [
137
+ /* @__PURE__ */ jsx2(
138
+ "button",
139
+ {
140
+ type: "button",
141
+ className: "vd-toolbar-btn",
142
+ onClick: onZoomOut,
143
+ "aria-label": "Zoom out",
144
+ title: "Zoom out",
145
+ children: /* @__PURE__ */ jsx2(ZoomOutIcon, {})
146
+ }
147
+ ),
148
+ /* @__PURE__ */ jsxs2("span", { className: "vd-toolbar-scale", "aria-live": "polite", children: [
149
+ Math.round(scale * 100),
150
+ "%"
151
+ ] }),
152
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "vd-toolbar-btn", onClick: onZoomIn, "aria-label": "Zoom in", title: "Zoom in", children: /* @__PURE__ */ jsx2(ZoomInIcon, {}) }),
153
+ /* @__PURE__ */ jsx2(
154
+ "button",
155
+ {
156
+ type: "button",
157
+ className: "vd-toolbar-btn",
158
+ onClick: onReset,
159
+ "aria-label": "Reset zoom to 100%",
160
+ title: "Reset zoom to 100%",
161
+ children: /* @__PURE__ */ jsx2(ResetIcon, {})
162
+ }
163
+ )
164
+ ] }),
165
+ extra,
166
+ /* @__PURE__ */ jsx2("span", { className: "vd-toolbar-spacer" }),
167
+ onDownload && /* @__PURE__ */ jsx2(
168
+ "button",
169
+ {
170
+ type: "button",
171
+ className: "vd-toolbar-btn",
172
+ onClick: onDownload,
173
+ "aria-label": "Download file",
174
+ title: "Download file",
175
+ children: /* @__PURE__ */ jsx2(DownloadIcon, {})
176
+ }
177
+ ),
178
+ onFullscreen && /* @__PURE__ */ jsx2(
179
+ "button",
180
+ {
181
+ type: "button",
182
+ className: "vd-toolbar-btn",
183
+ onClick: onFullscreen,
184
+ "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
185
+ title: isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
186
+ children: isFullscreen ? /* @__PURE__ */ jsx2(FullscreenExitIcon, {}) : /* @__PURE__ */ jsx2(FullscreenEnterIcon, {})
187
+ }
188
+ )
189
+ ] });
190
+ }
191
+
192
+ // src/core/theme.ts
193
+ var THEME_VAR_MAP = {
194
+ background: "--vd-bg",
195
+ toolbarBackground: "--vd-toolbar-bg",
196
+ toolbarBorderColor: "--vd-toolbar-border-color",
197
+ toolbarButtonHoverBackground: "--vd-toolbar-btn-hover-bg",
198
+ textColor: "--vd-text-color",
199
+ borderRadius: "--vd-border-radius",
200
+ toolbarButtonRadius: "--vd-toolbar-btn-radius"
201
+ };
202
+ function themeToStyle(theme) {
203
+ if (!theme) return void 0;
204
+ const style = {};
205
+ for (const key of Object.keys(theme)) {
206
+ const value = theme[key];
207
+ if (value) style[THEME_VAR_MAP[key]] = value;
208
+ }
209
+ return style;
210
+ }
211
+
212
+ // src/core/ViewerShell.tsx
213
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
214
+ function ViewerShell({
215
+ zoomPan,
216
+ onDownload,
217
+ toolbarExtra,
218
+ children,
219
+ enableZoomControls = true,
220
+ enableFullscreen = true,
221
+ enablePan = true,
222
+ theme,
223
+ width,
224
+ height,
225
+ floating = true,
226
+ draggable = true,
227
+ defaultPosition,
228
+ transformOrigin
229
+ }) {
230
+ const rootRef = useRef(null);
231
+ const [isFullscreen, setIsFullscreen] = useState2(false);
232
+ const effectiveWidth = typeof width === "number" ? width : 1200;
233
+ const effectiveHeight = typeof height === "number" ? height : 700;
234
+ const resolvedDefaultPosition = defaultPosition ?? (typeof window !== "undefined" ? {
235
+ x: Math.max(0, (window.innerWidth - effectiveWidth) / 2),
236
+ y: Math.max(0, (window.innerHeight - effectiveHeight) / 2)
237
+ } : { x: 80, y: 80 });
238
+ const { position, onDragHandlePointerDown } = useDraggable({
239
+ defaultPosition: resolvedDefaultPosition,
240
+ draggable
241
+ });
242
+ useEffect(() => {
243
+ const onChange = () => setIsFullscreen(document.fullscreenElement === rootRef.current);
244
+ document.addEventListener("fullscreenchange", onChange);
245
+ return () => document.removeEventListener("fullscreenchange", onChange);
246
+ }, []);
247
+ const onFullscreen = useCallback2(() => {
248
+ const el = rootRef.current;
249
+ if (!el) return;
250
+ if (document.fullscreenElement) {
251
+ document.exitFullscreen();
252
+ } else {
253
+ el.requestFullscreen();
254
+ }
255
+ }, []);
256
+ const sizeStyle = {
257
+ width: width ?? 1200,
258
+ height: height ?? 700
259
+ };
260
+ const floatingStyle = floating ? { position: "fixed", left: position.x, top: position.y } : void 0;
261
+ return /* @__PURE__ */ jsxs3(
262
+ "div",
263
+ {
264
+ className: `vd-shell${floating ? " vd-shell--floating" : ""}`,
265
+ ref: rootRef,
266
+ style: { ...themeToStyle(theme), ...sizeStyle, ...floatingStyle },
267
+ children: [
268
+ /* @__PURE__ */ jsx3(
269
+ Toolbar,
270
+ {
271
+ scale: zoomPan.scale,
272
+ onZoomIn: zoomPan.zoomIn,
273
+ onZoomOut: zoomPan.zoomOut,
274
+ onReset: zoomPan.resetZoom,
275
+ onFullscreen: enableFullscreen ? onFullscreen : void 0,
276
+ onDownload,
277
+ isFullscreen,
278
+ showZoomControls: enableZoomControls,
279
+ dragHandle: floating && draggable ? /* @__PURE__ */ jsx3(GripIcon, {}) : void 0,
280
+ onDragHandlePointerDown: floating && draggable ? onDragHandlePointerDown : void 0,
281
+ extra: toolbarExtra
282
+ }
283
+ ),
284
+ /* @__PURE__ */ jsx3(
285
+ "div",
286
+ {
287
+ className: `vd-canvas${enablePan ? "" : " vd-canvas--no-pan"}`,
288
+ ref: zoomPan.containerRef,
289
+ onWheel: zoomPan.handlers.onWheel,
290
+ onPointerDown: zoomPan.handlers.onPointerDown,
291
+ onPointerMove: zoomPan.handlers.onPointerMove,
292
+ onPointerUp: zoomPan.handlers.onPointerUp,
293
+ children: /* @__PURE__ */ jsx3(
294
+ "div",
295
+ {
296
+ className: "vd-canvas-content",
297
+ style: {
298
+ transform: `translate(${zoomPan.translateX}px, ${zoomPan.translateY}px) scale(${zoomPan.scale})`,
299
+ transformOrigin
300
+ },
301
+ children
302
+ }
303
+ )
304
+ }
305
+ )
306
+ ]
307
+ }
308
+ );
309
+ }
310
+
311
+ // src/core/useZoomPan.ts
312
+ import { useCallback as useCallback3, useRef as useRef2, useState as useState3 } from "react";
313
+ var DEFAULT_MIN = 0.1;
314
+ var DEFAULT_MAX = 8;
315
+ var DEFAULT_STEP = 0.25;
316
+ function useZoomPan(options = {}) {
317
+ const {
318
+ minScale = DEFAULT_MIN,
319
+ maxScale = DEFAULT_MAX,
320
+ zoomStep = DEFAULT_STEP,
321
+ enablePan = true,
322
+ enableWheelZoom = true,
323
+ enableDragScroll = false
324
+ } = options;
325
+ const [state, setState] = useState3({ scale: 1, translateX: 0, translateY: 0 });
326
+ const containerRef = useRef2(null);
327
+ const dragState = useRef2({
328
+ dragging: false,
329
+ startX: 0,
330
+ startY: 0,
331
+ originX: 0,
332
+ originY: 0
333
+ });
334
+ const scrollDragState = useRef2(
335
+ { dragging: false, startX: 0, startY: 0, originLeft: 0, originTop: 0 }
336
+ );
337
+ const clamp = useCallback3(
338
+ (value) => Math.min(maxScale, Math.max(minScale, value)),
339
+ [minScale, maxScale]
340
+ );
341
+ const setScale = useCallback3(
342
+ (scale) => setState((prev) => ({ ...prev, scale: clamp(scale) })),
343
+ [clamp]
344
+ );
345
+ const zoomIn = useCallback3(() => setScale(state.scale + zoomStep), [setScale, state.scale, zoomStep]);
346
+ const zoomOut = useCallback3(() => setScale(state.scale - zoomStep), [setScale, state.scale, zoomStep]);
347
+ const resetZoom = useCallback3(() => setState({ scale: 1, translateX: 0, translateY: 0 }), []);
348
+ const onWheel = useCallback3(
349
+ (e) => {
350
+ if (!enableWheelZoom) return;
351
+ if (!e.ctrlKey && !e.metaKey) return;
352
+ e.preventDefault();
353
+ const delta = e.deltaY > 0 ? -zoomStep : zoomStep;
354
+ setScale(state.scale + delta);
355
+ },
356
+ [enableWheelZoom, setScale, state.scale, zoomStep]
357
+ );
358
+ const onPointerDown = useCallback3(
359
+ (e) => {
360
+ if (enablePan) {
361
+ dragState.current = {
362
+ dragging: true,
363
+ startX: e.clientX,
364
+ startY: e.clientY,
365
+ originX: state.translateX,
366
+ originY: state.translateY
367
+ };
368
+ e.target.setPointerCapture(e.pointerId);
369
+ } else if (enableDragScroll && containerRef.current) {
370
+ scrollDragState.current = {
371
+ dragging: true,
372
+ startX: e.clientX,
373
+ startY: e.clientY,
374
+ originLeft: containerRef.current.scrollLeft,
375
+ originTop: containerRef.current.scrollTop
376
+ };
377
+ e.target.setPointerCapture(e.pointerId);
378
+ }
379
+ },
380
+ [enablePan, enableDragScroll, state.translateX, state.translateY]
381
+ );
382
+ const onPointerMove = useCallback3(
383
+ (e) => {
384
+ if (enablePan) {
385
+ if (!dragState.current.dragging) return;
386
+ const dx = e.clientX - dragState.current.startX;
387
+ const dy = e.clientY - dragState.current.startY;
388
+ setState((prev) => ({
389
+ ...prev,
390
+ translateX: dragState.current.originX + dx,
391
+ translateY: dragState.current.originY + dy
392
+ }));
393
+ } else if (enableDragScroll) {
394
+ if (!scrollDragState.current.dragging || !containerRef.current) return;
395
+ const dx = e.clientX - scrollDragState.current.startX;
396
+ const dy = e.clientY - scrollDragState.current.startY;
397
+ containerRef.current.scrollLeft = scrollDragState.current.originLeft - dx;
398
+ containerRef.current.scrollTop = scrollDragState.current.originTop - dy;
399
+ }
400
+ },
401
+ [enablePan, enableDragScroll]
402
+ );
403
+ const onPointerUp = useCallback3(
404
+ (e) => {
405
+ if (enablePan) {
406
+ dragState.current.dragging = false;
407
+ e.target.releasePointerCapture(e.pointerId);
408
+ } else if (enableDragScroll) {
409
+ scrollDragState.current.dragging = false;
410
+ e.target.releasePointerCapture(e.pointerId);
411
+ }
412
+ },
413
+ [enablePan, enableDragScroll]
414
+ );
415
+ return {
416
+ ...state,
417
+ containerRef,
418
+ zoomIn,
419
+ zoomOut,
420
+ resetZoom,
421
+ setScale,
422
+ handlers: { onWheel, onPointerDown, onPointerMove, onPointerUp }
423
+ };
424
+ }
425
+
426
+ // src/viewers/ImageViewer.tsx
427
+ import { jsx as jsx4 } from "react/jsx-runtime";
428
+ function defaultDownload(uri, fileName) {
429
+ const link = document.createElement("a");
430
+ link.href = uri;
431
+ link.download = fileName ?? "";
432
+ link.rel = "noopener";
433
+ document.body.appendChild(link);
434
+ link.click();
435
+ document.body.removeChild(link);
436
+ }
437
+ function ImageViewer({
438
+ uri,
439
+ fileName,
440
+ className,
441
+ style,
442
+ width,
443
+ height,
444
+ minScale,
445
+ maxScale,
446
+ zoomStep,
447
+ enableZoomControls = true,
448
+ enableWheelZoom = true,
449
+ enablePan = true,
450
+ enableFullscreen = true,
451
+ enableDownload = true,
452
+ onDownload,
453
+ theme,
454
+ floating = true,
455
+ windowDraggable = true,
456
+ defaultPosition
457
+ }) {
458
+ const zoomPan = useZoomPan({ minScale, maxScale, zoomStep, enablePan, enableWheelZoom });
459
+ const handleDownload = enableDownload ? onDownload ?? (() => defaultDownload(uri, fileName)) : void 0;
460
+ const wrapperStyle = floating ? { ...style } : { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", ...style };
461
+ return /* @__PURE__ */ jsx4("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsx4(
462
+ ViewerShell,
463
+ {
464
+ zoomPan,
465
+ onDownload: handleDownload,
466
+ enableZoomControls,
467
+ enableFullscreen,
468
+ enablePan,
469
+ theme,
470
+ width,
471
+ height,
472
+ floating,
473
+ draggable: windowDraggable,
474
+ defaultPosition,
475
+ children: /* @__PURE__ */ jsx4("img", { className: "vd-image", src: uri, alt: fileName ?? "document image", draggable: false })
476
+ }
477
+ ) });
478
+ }
479
+
480
+ // src/viewers/PdfPage.tsx
481
+ import { useEffect as useEffect2, useRef as useRef3 } from "react";
482
+ import { jsx as jsx5 } from "react/jsx-runtime";
483
+ function PdfPage({ doc, pageNumber, renderScale = 1.5 }) {
484
+ const canvasRef = useRef3(null);
485
+ const renderTaskRef = useRef3(null);
486
+ useEffect2(() => {
487
+ let cancelled = false;
488
+ doc.getPage(pageNumber).then((page) => {
489
+ if (cancelled) return;
490
+ const viewport = page.getViewport({ scale: renderScale });
491
+ const canvas = canvasRef.current;
492
+ if (!canvas) return;
493
+ const context = canvas.getContext("2d");
494
+ if (!context) return;
495
+ canvas.width = viewport.width;
496
+ canvas.height = viewport.height;
497
+ canvas.style.width = `${viewport.width / renderScale}px`;
498
+ canvas.style.height = `${viewport.height / renderScale}px`;
499
+ const renderTask = page.render({ canvasContext: context, viewport, canvas });
500
+ renderTaskRef.current = renderTask;
501
+ renderTask.promise.catch(() => {
502
+ });
503
+ });
504
+ return () => {
505
+ cancelled = true;
506
+ renderTaskRef.current?.cancel();
507
+ };
508
+ }, [doc, pageNumber, renderScale]);
509
+ return /* @__PURE__ */ jsx5("canvas", { ref: canvasRef, className: "vd-pdf-page", "data-page-number": pageNumber });
510
+ }
511
+
512
+ // src/viewers/usePdfDocument.ts
513
+ import { useEffect as useEffect3, useState as useState4 } from "react";
514
+ import * as pdfjsLib from "pdfjs-dist";
515
+ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString();
516
+ function usePdfDocument(uri) {
517
+ const [doc, setDoc] = useState4(null);
518
+ const [error, setError] = useState4(null);
519
+ const [loading, setLoading] = useState4(true);
520
+ useEffect3(() => {
521
+ let cancelled = false;
522
+ setLoading(true);
523
+ setError(null);
524
+ setDoc(null);
525
+ const loadingTask = pdfjsLib.getDocument({ url: uri });
526
+ loadingTask.promise.then((pdf) => {
527
+ if (cancelled) return;
528
+ setDoc(pdf);
529
+ setLoading(false);
530
+ }).catch((err) => {
531
+ if (!cancelled) {
532
+ setError(err);
533
+ setLoading(false);
534
+ }
535
+ });
536
+ return () => {
537
+ cancelled = true;
538
+ loadingTask.destroy();
539
+ };
540
+ }, [uri]);
541
+ return { doc, numPages: doc?.numPages ?? 0, error, loading };
542
+ }
543
+
544
+ // src/viewers/useVisiblePage.ts
545
+ import { useEffect as useEffect4, useState as useState5 } from "react";
546
+ function useVisiblePage(containerRef, numPages) {
547
+ const [currentPage, setCurrentPage] = useState5(1);
548
+ useEffect4(() => {
549
+ const root = containerRef.current;
550
+ if (!root || numPages === 0) return;
551
+ const observer = new IntersectionObserver(
552
+ (entries) => {
553
+ const mostVisible = entries.filter((e) => e.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
554
+ if (mostVisible) {
555
+ const page = Number(mostVisible.target.dataset.pageNumber);
556
+ if (page) setCurrentPage(page);
557
+ }
558
+ },
559
+ { root, threshold: [0.25, 0.5, 0.75] }
560
+ );
561
+ const pages = root.querySelectorAll("[data-page-number]");
562
+ pages.forEach((el) => observer.observe(el));
563
+ return () => observer.disconnect();
564
+ }, [containerRef, numPages]);
565
+ return currentPage;
566
+ }
567
+
568
+ // src/viewers/PdfViewer.tsx
569
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
570
+ function defaultDownload2(uri, fileName) {
571
+ const link = document.createElement("a");
572
+ link.href = uri;
573
+ link.download = fileName ?? "";
574
+ link.rel = "noopener";
575
+ document.body.appendChild(link);
576
+ link.click();
577
+ document.body.removeChild(link);
578
+ }
579
+ function PdfViewer({
580
+ uri,
581
+ fileName,
582
+ className,
583
+ style,
584
+ width,
585
+ height,
586
+ minScale = 0.25,
587
+ maxScale = 5,
588
+ zoomStep,
589
+ renderScale,
590
+ enableZoomControls = true,
591
+ enableWheelZoom = true,
592
+ enableFullscreen = true,
593
+ enableDownload = true,
594
+ onDownload,
595
+ theme,
596
+ floating = true,
597
+ windowDraggable = true,
598
+ defaultPosition
599
+ }) {
600
+ const zoomPan = useZoomPan({
601
+ minScale,
602
+ maxScale,
603
+ zoomStep,
604
+ enablePan: false,
605
+ enableDragScroll: true,
606
+ enableWheelZoom
607
+ });
608
+ const { doc, numPages, error, loading } = usePdfDocument(uri);
609
+ const currentPage = useVisiblePage(zoomPan.containerRef, numPages);
610
+ const handleDownload = enableDownload ? onDownload ?? (() => defaultDownload2(uri, fileName)) : void 0;
611
+ const wrapperStyle = floating ? { ...style } : { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", ...style };
612
+ return /* @__PURE__ */ jsx6("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsxs4(
613
+ ViewerShell,
614
+ {
615
+ zoomPan,
616
+ onDownload: handleDownload,
617
+ enableZoomControls,
618
+ enableFullscreen,
619
+ enablePan: false,
620
+ theme,
621
+ width,
622
+ height,
623
+ floating,
624
+ draggable: windowDraggable,
625
+ defaultPosition,
626
+ transformOrigin: "top center",
627
+ toolbarExtra: numPages > 0 ? /* @__PURE__ */ jsxs4("span", { className: "vd-pdf-page-indicator", children: [
628
+ "Page ",
629
+ currentPage,
630
+ " / ",
631
+ numPages
632
+ ] }) : void 0,
633
+ children: [
634
+ loading && /* @__PURE__ */ jsx6("span", { className: "vd-pdf-page-indicator", children: "Loading PDF\u2026" }),
635
+ error && /* @__PURE__ */ jsxs4("span", { className: "vd-pdf-page-indicator", children: [
636
+ "Failed to load PDF: ",
637
+ error.message
638
+ ] }),
639
+ doc && /* @__PURE__ */ jsx6("div", { className: "vd-pdf-pages", children: Array.from({ length: numPages }, (_, i) => /* @__PURE__ */ jsx6(PdfPage, { doc, pageNumber: i + 1, renderScale }, i + 1)) })
640
+ ]
641
+ }
642
+ ) });
643
+ }
644
+
645
+ // src/viewers/useDocxHtml.ts
646
+ import { useEffect as useEffect5, useState as useState6 } from "react";
647
+ import mammoth from "mammoth";
648
+ function useDocxHtml(uri) {
649
+ const [html, setHtml] = useState6(null);
650
+ const [error, setError] = useState6(null);
651
+ const [loading, setLoading] = useState6(true);
652
+ useEffect5(() => {
653
+ let cancelled = false;
654
+ setLoading(true);
655
+ setError(null);
656
+ setHtml(null);
657
+ fetch(uri).then((res) => {
658
+ if (!res.ok) throw new Error(`Failed to fetch document: ${res.status} ${res.statusText}`);
659
+ return res.arrayBuffer();
660
+ }).then((arrayBuffer) => mammoth.convertToHtml({ arrayBuffer })).then((result) => {
661
+ if (cancelled) return;
662
+ setHtml(result.value);
663
+ setLoading(false);
664
+ }).catch((err) => {
665
+ if (!cancelled) {
666
+ setError(err);
667
+ setLoading(false);
668
+ }
669
+ });
670
+ return () => {
671
+ cancelled = true;
672
+ };
673
+ }, [uri]);
674
+ return { html, error, loading };
675
+ }
676
+
677
+ // src/viewers/DocxViewer.tsx
678
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
679
+ function defaultDownload3(uri, fileName) {
680
+ const link = document.createElement("a");
681
+ link.href = uri;
682
+ link.download = fileName ?? "";
683
+ link.rel = "noopener";
684
+ document.body.appendChild(link);
685
+ link.click();
686
+ document.body.removeChild(link);
687
+ }
688
+ function DocxViewer({
689
+ uri,
690
+ fileName,
691
+ className,
692
+ style,
693
+ width,
694
+ height,
695
+ minScale = 0.5,
696
+ maxScale = 3,
697
+ zoomStep,
698
+ enableZoomControls = true,
699
+ enableWheelZoom = true,
700
+ enableFullscreen = true,
701
+ enableDownload = true,
702
+ onDownload,
703
+ theme,
704
+ floating = true,
705
+ windowDraggable = true,
706
+ defaultPosition
707
+ }) {
708
+ const zoomPan = useZoomPan({
709
+ minScale,
710
+ maxScale,
711
+ zoomStep,
712
+ enablePan: false,
713
+ enableDragScroll: true,
714
+ enableWheelZoom
715
+ });
716
+ const { html, error, loading } = useDocxHtml(uri);
717
+ const handleDownload = enableDownload ? onDownload ?? (() => defaultDownload3(uri, fileName)) : void 0;
718
+ const wrapperStyle = floating ? { ...style } : { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", ...style };
719
+ return /* @__PURE__ */ jsx7("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsxs5(
720
+ ViewerShell,
721
+ {
722
+ zoomPan,
723
+ onDownload: handleDownload,
724
+ enableZoomControls,
725
+ enableFullscreen,
726
+ enablePan: false,
727
+ theme,
728
+ width,
729
+ height,
730
+ floating,
731
+ draggable: windowDraggable,
732
+ defaultPosition,
733
+ transformOrigin: "top center",
734
+ children: [
735
+ loading && /* @__PURE__ */ jsx7("span", { className: "vd-doc-status", children: "Loading document\u2026" }),
736
+ error && /* @__PURE__ */ jsxs5("span", { className: "vd-doc-status", children: [
737
+ "Failed to load document: ",
738
+ error.message
739
+ ] }),
740
+ html && /* @__PURE__ */ jsx7("div", { className: "vd-docx-page", dangerouslySetInnerHTML: { __html: html } })
741
+ ]
742
+ }
743
+ ) });
744
+ }
745
+
746
+ // src/viewers/XlsxViewer.tsx
747
+ import { useMemo, useState as useState8 } from "react";
748
+ import * as XLSX2 from "xlsx";
749
+
750
+ // src/viewers/useXlsxWorkbook.ts
751
+ import { useEffect as useEffect6, useState as useState7 } from "react";
752
+ import * as XLSX from "xlsx";
753
+ function useXlsxWorkbook(uri) {
754
+ const [workbook, setWorkbook] = useState7(null);
755
+ const [error, setError] = useState7(null);
756
+ const [loading, setLoading] = useState7(true);
757
+ useEffect6(() => {
758
+ let cancelled = false;
759
+ setLoading(true);
760
+ setError(null);
761
+ setWorkbook(null);
762
+ fetch(uri).then((res) => {
763
+ if (!res.ok) throw new Error(`Failed to fetch spreadsheet: ${res.status} ${res.statusText}`);
764
+ return res.arrayBuffer();
765
+ }).then((arrayBuffer) => {
766
+ if (cancelled) return;
767
+ const wb = XLSX.read(arrayBuffer, { type: "array" });
768
+ setWorkbook(wb);
769
+ setLoading(false);
770
+ }).catch((err) => {
771
+ if (!cancelled) {
772
+ setError(err);
773
+ setLoading(false);
774
+ }
775
+ });
776
+ return () => {
777
+ cancelled = true;
778
+ };
779
+ }, [uri]);
780
+ return { workbook, error, loading };
781
+ }
782
+
783
+ // src/viewers/XlsxViewer.tsx
784
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
785
+ function defaultDownload4(uri, fileName) {
786
+ const link = document.createElement("a");
787
+ link.href = uri;
788
+ link.download = fileName ?? "";
789
+ link.rel = "noopener";
790
+ document.body.appendChild(link);
791
+ link.click();
792
+ document.body.removeChild(link);
793
+ }
794
+ function XlsxViewer({
795
+ uri,
796
+ fileName,
797
+ className,
798
+ style,
799
+ width,
800
+ height,
801
+ minScale = 0.5,
802
+ maxScale = 3,
803
+ zoomStep,
804
+ enableZoomControls = true,
805
+ enableWheelZoom = true,
806
+ enableFullscreen = true,
807
+ enableDownload = true,
808
+ onDownload,
809
+ theme,
810
+ floating = true,
811
+ windowDraggable = true,
812
+ defaultPosition
813
+ }) {
814
+ const zoomPan = useZoomPan({
815
+ minScale,
816
+ maxScale,
817
+ zoomStep,
818
+ enablePan: false,
819
+ enableDragScroll: true,
820
+ enableWheelZoom
821
+ });
822
+ const { workbook, error, loading } = useXlsxWorkbook(uri);
823
+ const [activeSheet, setActiveSheet] = useState8(0);
824
+ const handleDownload = enableDownload ? onDownload ?? (() => defaultDownload4(uri, fileName)) : void 0;
825
+ const tableHtml = useMemo(() => {
826
+ if (!workbook) return null;
827
+ const sheetName = workbook.SheetNames[activeSheet];
828
+ const sheet = workbook.Sheets[sheetName];
829
+ if (!sheet) return null;
830
+ return XLSX2.utils.sheet_to_html(sheet, { editable: false });
831
+ }, [workbook, activeSheet]);
832
+ const wrapperStyle = floating ? { ...style } : { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", ...style };
833
+ return /* @__PURE__ */ jsx8("div", { className, style: wrapperStyle, children: /* @__PURE__ */ jsxs6(
834
+ ViewerShell,
835
+ {
836
+ zoomPan,
837
+ onDownload: handleDownload,
838
+ enableZoomControls,
839
+ enableFullscreen,
840
+ enablePan: false,
841
+ theme,
842
+ width,
843
+ height,
844
+ floating,
845
+ draggable: windowDraggable,
846
+ defaultPosition,
847
+ transformOrigin: "top left",
848
+ children: [
849
+ loading && /* @__PURE__ */ jsx8("span", { className: "vd-doc-status", children: "Loading spreadsheet\u2026" }),
850
+ error && /* @__PURE__ */ jsxs6("span", { className: "vd-doc-status", children: [
851
+ "Failed to load spreadsheet: ",
852
+ error.message
853
+ ] }),
854
+ workbook && /* @__PURE__ */ jsxs6("div", { className: "vd-xlsx", children: [
855
+ workbook.SheetNames.length > 1 && /* @__PURE__ */ jsx8("div", { className: "vd-xlsx-tabs", children: workbook.SheetNames.map((name, i) => /* @__PURE__ */ jsx8(
856
+ "button",
857
+ {
858
+ type: "button",
859
+ className: `vd-xlsx-tab${i === activeSheet ? " vd-xlsx-tab--active" : ""}`,
860
+ onClick: () => setActiveSheet(i),
861
+ children: name
862
+ },
863
+ name
864
+ )) }),
865
+ tableHtml && /* @__PURE__ */ jsx8("div", { className: "vd-xlsx-table", dangerouslySetInnerHTML: { __html: tableHtml } })
866
+ ] })
867
+ ]
868
+ }
869
+ ) });
870
+ }
871
+
872
+ // src/detectFileType.ts
873
+ var EXTENSION_MAP = {
874
+ png: "image",
875
+ jpg: "image",
876
+ jpeg: "image",
877
+ gif: "image",
878
+ webp: "image",
879
+ svg: "image",
880
+ bmp: "image",
881
+ ico: "image",
882
+ avif: "image",
883
+ pdf: "pdf",
884
+ docx: "docx",
885
+ xlsx: "xlsx",
886
+ xls: "xlsx",
887
+ xlsm: "xlsx",
888
+ csv: "xlsx"
889
+ };
890
+ function getUrlExtension(uri) {
891
+ let pathname;
892
+ try {
893
+ pathname = new URL(uri, typeof window !== "undefined" ? window.location.href : "http://localhost").pathname;
894
+ } catch {
895
+ pathname = uri.split("?")[0].split("#")[0];
896
+ }
897
+ const match = pathname.match(/\.([a-zA-Z0-9]+)$/);
898
+ return match ? match[1].toLowerCase() : null;
899
+ }
900
+ function detectFileType(uri, hint) {
901
+ if (hint) {
902
+ const normalizedHint = hint.toLowerCase().replace(/^\./, "");
903
+ if (normalizedHint in EXTENSION_MAP) return EXTENSION_MAP[normalizedHint];
904
+ if (normalizedHint.startsWith("image/")) return "image";
905
+ if (normalizedHint === "application/pdf") return "pdf";
906
+ }
907
+ const extension = getUrlExtension(uri);
908
+ if (extension && extension in EXTENSION_MAP) return EXTENSION_MAP[extension];
909
+ return null;
910
+ }
911
+
912
+ // src/DocViewer.tsx
913
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
914
+ function DocViewer({ uri, fileName, type, fallback, ...rest }) {
915
+ const detected = detectFileType(uri, type ?? fileName?.split(".").pop());
916
+ switch (detected) {
917
+ case "image":
918
+ return /* @__PURE__ */ jsx9(ImageViewer, { uri, fileName, ...rest });
919
+ case "pdf":
920
+ return /* @__PURE__ */ jsx9(PdfViewer, { uri, fileName, ...rest });
921
+ case "docx":
922
+ return /* @__PURE__ */ jsx9(DocxViewer, { uri, fileName, ...rest });
923
+ case "xlsx":
924
+ return /* @__PURE__ */ jsx9(XlsxViewer, { uri, fileName, ...rest });
925
+ default:
926
+ return /* @__PURE__ */ jsx9(Fragment2, { children: fallback ?? /* @__PURE__ */ jsxs7("div", { className: "vd-doc-status", children: [
927
+ 'Unable to detect file type for "',
928
+ fileName ?? uri,
929
+ '".'
930
+ ] }) });
931
+ }
932
+ }
933
+ export {
934
+ DocViewer,
935
+ DocxViewer,
936
+ ImageViewer,
937
+ PdfViewer,
938
+ Toolbar,
939
+ ViewerShell,
940
+ XlsxViewer,
941
+ detectFileType,
942
+ getUrlExtension,
943
+ useDraggable,
944
+ useZoomPan
945
+ };
946
+ //# sourceMappingURL=index.js.map