@particle-academy/react-fancy 5.53.1 → 5.54.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/CHANGELOG.md CHANGED
@@ -11,6 +11,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [5.54.0] - 2026-10-21
15
+
16
+ ### Added
17
+
18
+ - **`PdfViewer` page-image mode (`pages`), with an access `policy` and a
19
+ `watermark`** (react-fancy#36). For showing a PDF without handing over the
20
+ file: your app renders the PDF to page images on its server, behind its own
21
+ auth, and the viewer only displays them.
22
+ - `pages={{ count, url: (page, scale) => string, aspectRatio? }}`. The page
23
+ images are the **only** requests the viewer makes. It never fetches or
24
+ references the PDF, and `src` is ignored in this mode even if passed.
25
+ `scale` is zoom x devicePixelRatio, rounded up to 1, 1.5, 2, 3 or 4.
26
+ - Its own toolbar: previous / next page, zoom out / reset / in, fit width,
27
+ fit page. With `frame`, full screen comes from the `FauxClient` frame.
28
+ Pages near the viewport get images; the rest are sized placeholders.
29
+ - Controlled `page` / `onPageChange` and `zoom` / `onZoomChange`, with stable
30
+ `data-react-fancy-pdf-viewer-*` handles on each page and button.
31
+ - **No search**: page images have no text to search. That is the cost of
32
+ not sending the PDF.
33
+ - `policy={{ download?, print?, copy?, screenshot? }}`, all allowed by default.
34
+ **Every flag is a deterrent, not a guarantee.** The real protection is that
35
+ the PDF never reaches the browser:
36
+ - `download: false`: no context menu on the pages and the images are not
37
+ draggable. Page images can still be captured (devtools, cache, screen).
38
+ - `print: false`: print CSS blanks the viewer, and Ctrl/Cmd+P is
39
+ intercepted while focus is inside it.
40
+ - `copy: false`: user selection off (there is no text layer anyway).
41
+ - `screenshot: "deter"`: the pages blur while the tab is hidden or the
42
+ window loses focus, and for 1.5 s after PrintScreen (which also tries to
43
+ clear the clipboard, best effort). **A web page cannot prevent
44
+ screenshots.**
45
+ - `watermark={{ text, opacity?, angle?, density? }}`: a tiled overlay on every
46
+ page, `pointer-events: none`. **It can be removed with devtools.** If it
47
+ must hold, burn it into the images on your server; `url(page, scale)` is
48
+ where to ask for that.
49
+ - `policy` and `watermark` apply only in page-image mode. The native-plugin
50
+ mode (`src`) is unchanged and cannot be restricted.
51
+ - New types: `PdfPageSource`, `PdfViewerPolicy`, `PdfViewerWatermark`.
52
+ - `MediaViewer` (`pdfProps`) and fancy-code's `FileViewer`
53
+ (`mediaProps.pdfProps`) already forward these untouched, so neither needed a
54
+ change.
55
+ - **What to do:** nothing unless you want it. `src` is now optional in the
56
+ types, since page-image mode does not need one; every existing call compiles
57
+ and renders as before. No new dependencies.
58
+
14
59
  ## [5.53.1] - 2026-10-21
15
60
 
16
61
  ### Fixed
@@ -0,0 +1,97 @@
1
+ import { F as FauxClientProps } from './FauxClient.types-Dtk9aRs6.js';
2
+ import { CSSProperties } from 'react';
3
+
4
+ /**
5
+ * Pages the APP rendered to images, behind its own auth. With this the viewer
6
+ * never fetches or references the PDF: these URLs are its only requests.
7
+ */
8
+ interface PdfPageSource {
9
+ /** Number of pages. */
10
+ count: number;
11
+ /**
12
+ * The image URL for a page (1-based). `scale` is a resolution hint, the zoom
13
+ * times the device pixel ratio rounded UP to one of 1, 1.5, 2, 3 or 4, so a
14
+ * page is fetched at a handful of sizes rather than one per zoom click.
15
+ * Render scale 1 at whatever width reads sharply at fit-width in your layout.
16
+ * This is also the place to burn a watermark in on the server.
17
+ */
18
+ url: (page: number, scale: number) => string;
19
+ /**
20
+ * Page width / height, used to size a page before its image arrives (each
21
+ * page switches to its image's real ratio once loaded). Default A4 portrait,
22
+ * `1 / Math.SQRT2`.
23
+ */
24
+ aspectRatio?: number;
25
+ }
26
+ /**
27
+ * What a viewer of the page images may do. Every flag defaults to allowed.
28
+ * These are DETERRENTS: a web page cannot stop a determined viewer, and a page
29
+ * image that reached the screen can always be captured. Real control is the
30
+ * app never serving the PDF, which page-image mode makes possible.
31
+ */
32
+ interface PdfViewerPolicy {
33
+ /** `false`: no context menu ("Save image as") and no dragging the images out. */
34
+ download?: boolean;
35
+ /** `false`: print CSS blanks the viewer, and Ctrl/Cmd+P is swallowed while focus is inside it. */
36
+ print?: boolean;
37
+ /** `false`: user selection is off. (Page images have no text layer anyway.) */
38
+ copy?: boolean;
39
+ /**
40
+ * `"deter"`: the pages blur while the tab is hidden or the window has lost
41
+ * focus, and briefly after a PrintScreen key. It CANNOT prevent a screenshot.
42
+ * Default `"allow"`.
43
+ */
44
+ screenshot?: "allow" | "deter";
45
+ }
46
+ /** A tiled text overlay drawn over every page, in the browser. */
47
+ interface PdfViewerWatermark {
48
+ text: string;
49
+ /** 0-1. Default 0.15. */
50
+ opacity?: number;
51
+ /** Degrees. Default -30. */
52
+ angle?: number;
53
+ /** Repeats per area; 2 tiles twice as tightly. Default 1. */
54
+ density?: number;
55
+ }
56
+ interface PdfViewerProps {
57
+ /**
58
+ * Show the PDF in react-fancy's `FauxClient` frame with a full-screen button,
59
+ * revealed on hover. Zoom stays on the browser's own PDF toolbar (with
60
+ * `pages`, on the viewer's toolbar). Default `false`.
61
+ */
62
+ frame?: boolean;
63
+ /** Passed to the frame (`variant`, `onFullscreenChange`, …). */
64
+ frameProps?: Partial<FauxClientProps>;
65
+ /**
66
+ * PDF source — an `http(s):`, `data:`, or `blob:` URL — shown in the
67
+ * browser's native PDF plugin. Ignored, and never referenced, when `pages` is set.
68
+ */
69
+ src?: string;
70
+ /**
71
+ * Page-image mode: show server-rendered page images instead of embedding the
72
+ * PDF. `policy` and `watermark` apply only in this mode.
73
+ */
74
+ pages?: PdfPageSource;
75
+ /** The current page (1-based), controlled. */
76
+ page?: number;
77
+ /** Starting page when uncontrolled. Default 1. */
78
+ defaultPage?: number;
79
+ onPageChange?: (page: number) => void;
80
+ /** Zoom, 1 = fit width, controlled (page-image mode). */
81
+ zoom?: number;
82
+ /** Default 1. */
83
+ defaultZoom?: number;
84
+ onZoomChange?: (zoom: number) => void;
85
+ /** What the viewer may do with the pages (page-image mode). Default: everything allowed. */
86
+ policy?: PdfViewerPolicy;
87
+ /** A tiled overlay over every page (page-image mode). Removable with devtools; burn it in server-side when it must hold. */
88
+ watermark?: PdfViewerWatermark;
89
+ /** Accessible title for the document (default `"PDF document"`). */
90
+ title?: string;
91
+ /** Additional CSS classes for the container. */
92
+ className?: string;
93
+ /** Inline styles for the container (e.g. a fixed height). */
94
+ style?: CSSProperties;
95
+ }
96
+
97
+ export type { PdfPageSource as P, PdfViewerPolicy as a, PdfViewerProps as b, PdfViewerWatermark as c };
@@ -0,0 +1,97 @@
1
+ import { F as FauxClientProps } from './FauxClient.types-Dtk9aRs6.cjs';
2
+ import { CSSProperties } from 'react';
3
+
4
+ /**
5
+ * Pages the APP rendered to images, behind its own auth. With this the viewer
6
+ * never fetches or references the PDF: these URLs are its only requests.
7
+ */
8
+ interface PdfPageSource {
9
+ /** Number of pages. */
10
+ count: number;
11
+ /**
12
+ * The image URL for a page (1-based). `scale` is a resolution hint, the zoom
13
+ * times the device pixel ratio rounded UP to one of 1, 1.5, 2, 3 or 4, so a
14
+ * page is fetched at a handful of sizes rather than one per zoom click.
15
+ * Render scale 1 at whatever width reads sharply at fit-width in your layout.
16
+ * This is also the place to burn a watermark in on the server.
17
+ */
18
+ url: (page: number, scale: number) => string;
19
+ /**
20
+ * Page width / height, used to size a page before its image arrives (each
21
+ * page switches to its image's real ratio once loaded). Default A4 portrait,
22
+ * `1 / Math.SQRT2`.
23
+ */
24
+ aspectRatio?: number;
25
+ }
26
+ /**
27
+ * What a viewer of the page images may do. Every flag defaults to allowed.
28
+ * These are DETERRENTS: a web page cannot stop a determined viewer, and a page
29
+ * image that reached the screen can always be captured. Real control is the
30
+ * app never serving the PDF, which page-image mode makes possible.
31
+ */
32
+ interface PdfViewerPolicy {
33
+ /** `false`: no context menu ("Save image as") and no dragging the images out. */
34
+ download?: boolean;
35
+ /** `false`: print CSS blanks the viewer, and Ctrl/Cmd+P is swallowed while focus is inside it. */
36
+ print?: boolean;
37
+ /** `false`: user selection is off. (Page images have no text layer anyway.) */
38
+ copy?: boolean;
39
+ /**
40
+ * `"deter"`: the pages blur while the tab is hidden or the window has lost
41
+ * focus, and briefly after a PrintScreen key. It CANNOT prevent a screenshot.
42
+ * Default `"allow"`.
43
+ */
44
+ screenshot?: "allow" | "deter";
45
+ }
46
+ /** A tiled text overlay drawn over every page, in the browser. */
47
+ interface PdfViewerWatermark {
48
+ text: string;
49
+ /** 0-1. Default 0.15. */
50
+ opacity?: number;
51
+ /** Degrees. Default -30. */
52
+ angle?: number;
53
+ /** Repeats per area; 2 tiles twice as tightly. Default 1. */
54
+ density?: number;
55
+ }
56
+ interface PdfViewerProps {
57
+ /**
58
+ * Show the PDF in react-fancy's `FauxClient` frame with a full-screen button,
59
+ * revealed on hover. Zoom stays on the browser's own PDF toolbar (with
60
+ * `pages`, on the viewer's toolbar). Default `false`.
61
+ */
62
+ frame?: boolean;
63
+ /** Passed to the frame (`variant`, `onFullscreenChange`, …). */
64
+ frameProps?: Partial<FauxClientProps>;
65
+ /**
66
+ * PDF source — an `http(s):`, `data:`, or `blob:` URL — shown in the
67
+ * browser's native PDF plugin. Ignored, and never referenced, when `pages` is set.
68
+ */
69
+ src?: string;
70
+ /**
71
+ * Page-image mode: show server-rendered page images instead of embedding the
72
+ * PDF. `policy` and `watermark` apply only in this mode.
73
+ */
74
+ pages?: PdfPageSource;
75
+ /** The current page (1-based), controlled. */
76
+ page?: number;
77
+ /** Starting page when uncontrolled. Default 1. */
78
+ defaultPage?: number;
79
+ onPageChange?: (page: number) => void;
80
+ /** Zoom, 1 = fit width, controlled (page-image mode). */
81
+ zoom?: number;
82
+ /** Default 1. */
83
+ defaultZoom?: number;
84
+ onZoomChange?: (zoom: number) => void;
85
+ /** What the viewer may do with the pages (page-image mode). Default: everything allowed. */
86
+ policy?: PdfViewerPolicy;
87
+ /** A tiled overlay over every page (page-image mode). Removable with devtools; burn it in server-side when it must hold. */
88
+ watermark?: PdfViewerWatermark;
89
+ /** Accessible title for the document (default `"PDF document"`). */
90
+ title?: string;
91
+ /** Additional CSS classes for the container. */
92
+ className?: string;
93
+ /** Inline styles for the container (e.g. a fixed height). */
94
+ style?: CSSProperties;
95
+ }
96
+
97
+ export type { PdfPageSource as P, PdfViewerPolicy as a, PdfViewerProps as b, PdfViewerWatermark as c };
@@ -0,0 +1,414 @@
1
+ 'use strict';
2
+
3
+ var chunkM5SEYDSS_cjs = require('./chunk-M5SEYDSS.cjs');
4
+ var chunkSNHU2AI4_cjs = require('./chunk-SNHU2AI4.cjs');
5
+ var chunkZU5Q77OT_cjs = require('./chunk-ZU5Q77OT.cjs');
6
+ var react = require('react');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+
9
+ var ZOOM_STEP = 1.25;
10
+ var MIN_ZOOM = 0.25;
11
+ var MAX_ZOOM = 4;
12
+ var SCALE_BUCKETS = [1, 1.5, 2, 3, 4];
13
+ var PRINT_SCREEN_MS = 1500;
14
+ var GAP = 16;
15
+ var clamp = (n, min, max) => Math.min(max, Math.max(min, n));
16
+ function scaleBucket(zoom) {
17
+ const dpr = typeof window !== "undefined" && window.devicePixelRatio > 0 ? window.devicePixelRatio : 1;
18
+ const want = zoom * dpr;
19
+ return SCALE_BUCKETS.find((b) => b >= want - 1e-9) ?? SCALE_BUCKETS[SCALE_BUCKETS.length - 1];
20
+ }
21
+ var PRINT_BLOCK_CSS = '@media print{[data-react-fancy-pdf-viewer][data-print-blocked="true"],[data-react-fancy-pdf-viewer][data-print-blocked="true"] *{visibility:hidden!important}}';
22
+ var PdfPages = react.forwardRef(
23
+ ({
24
+ pages,
25
+ page: controlledPage,
26
+ defaultPage = 1,
27
+ onPageChange,
28
+ zoom: controlledZoom,
29
+ defaultZoom = 1,
30
+ onZoomChange,
31
+ policy,
32
+ watermark,
33
+ title,
34
+ className,
35
+ style
36
+ }, ref) => {
37
+ const count = Math.max(0, Math.floor(pages.count));
38
+ const download = policy?.download !== false;
39
+ const print = policy?.print !== false;
40
+ const copy = policy?.copy !== false;
41
+ const deter = policy?.screenshot === "deter";
42
+ const [rawPage, setPage] = chunkSNHU2AI4_cjs.useControllableState(controlledPage, defaultPage, onPageChange);
43
+ const page = clamp(rawPage, 1, Math.max(1, count));
44
+ const [zoom, setZoom] = chunkSNHU2AI4_cjs.useControllableState(controlledZoom, defaultZoom, onZoomChange);
45
+ const scale = scaleBucket(zoom);
46
+ const scrollerRef = react.useRef(null);
47
+ const [ratios, setRatios] = react.useState({});
48
+ const ratioOf = (n) => ratios[n] ?? pages.aspectRatio ?? 1 / Math.SQRT2;
49
+ const observable = typeof IntersectionObserver !== "undefined";
50
+ const [near, setNear] = react.useState(() => /* @__PURE__ */ new Set([page, page + 1]));
51
+ const isNear = (n) => !observable || n === page || near.has(n);
52
+ react.useEffect(() => {
53
+ const scroller = scrollerRef.current;
54
+ if (!observable || !scroller) return;
55
+ const io = new IntersectionObserver(
56
+ (entries) => {
57
+ setNear((prev) => {
58
+ const next = new Set(prev);
59
+ for (const e of entries) {
60
+ const n = Number(e.target.dataset.page);
61
+ if (e.isIntersecting) next.add(n);
62
+ else next.delete(n);
63
+ }
64
+ return next;
65
+ });
66
+ },
67
+ // A viewport's worth above and below, so a page is there before it scrolls in.
68
+ { root: scroller, rootMargin: "100% 0px" }
69
+ );
70
+ scroller.querySelectorAll("[data-react-fancy-pdf-viewer-page]").forEach((el) => io.observe(el));
71
+ return () => io.disconnect();
72
+ }, [observable, count]);
73
+ const fromScroll = react.useRef(null);
74
+ const scrollToPage = react.useCallback((n) => {
75
+ const scroller = scrollerRef.current;
76
+ const slot = scroller?.querySelector(`[data-react-fancy-pdf-viewer-page][data-page="${n}"]`);
77
+ if (scroller && slot) scroller.scrollTop = Math.max(0, slot.offsetTop - GAP);
78
+ }, []);
79
+ react.useEffect(() => {
80
+ if (fromScroll.current === page) {
81
+ fromScroll.current = null;
82
+ return;
83
+ }
84
+ scrollToPage(page);
85
+ }, [page, scrollToPage]);
86
+ const firstZoom = react.useRef(true);
87
+ react.useLayoutEffect(() => {
88
+ if (firstZoom.current) {
89
+ firstZoom.current = false;
90
+ return;
91
+ }
92
+ scrollToPage(page);
93
+ }, [zoom]);
94
+ const pageRef = react.useRef(page);
95
+ pageRef.current = page;
96
+ react.useEffect(() => {
97
+ const scroller = scrollerRef.current;
98
+ if (!scroller || typeof ResizeObserver === "undefined") return;
99
+ let width = null;
100
+ const ro = new ResizeObserver(() => {
101
+ const w = scroller.clientWidth;
102
+ if (width !== null && w !== width) scrollToPage(pageRef.current);
103
+ width = w;
104
+ });
105
+ ro.observe(scroller);
106
+ return () => ro.disconnect();
107
+ }, [scrollToPage]);
108
+ const frame = react.useRef(0);
109
+ const onScroll = () => {
110
+ if (frame.current || typeof requestAnimationFrame === "undefined") return;
111
+ frame.current = requestAnimationFrame(() => {
112
+ frame.current = 0;
113
+ const scroller = scrollerRef.current;
114
+ if (!scroller) return;
115
+ const line = scroller.getBoundingClientRect().top + scroller.clientHeight / 3;
116
+ let current = 1;
117
+ scroller.querySelectorAll("[data-react-fancy-pdf-viewer-page]").forEach((el) => {
118
+ if (el.getBoundingClientRect().top <= line) current = Number(el.dataset.page);
119
+ });
120
+ if (current !== page) {
121
+ fromScroll.current = current;
122
+ setPage(current);
123
+ }
124
+ });
125
+ };
126
+ react.useEffect(() => () => {
127
+ if (frame.current) cancelAnimationFrame(frame.current);
128
+ }, []);
129
+ const zoomTo = (z) => setZoom(clamp(Math.round(z * 100) / 100, MIN_ZOOM, MAX_ZOOM));
130
+ const fitPage = () => {
131
+ const scroller = scrollerRef.current;
132
+ if (!scroller || scroller.clientWidth <= GAP * 2) return;
133
+ const pageHeightAtFit = (scroller.clientWidth - GAP * 2) / ratioOf(page);
134
+ zoomTo((scroller.clientHeight - GAP * 2) / pageHeightAtFit);
135
+ };
136
+ const [hidden, setHidden] = react.useState(false);
137
+ const [blurred, setBlurred] = react.useState(false);
138
+ const [snapped, setSnapped] = react.useState(false);
139
+ react.useEffect(() => {
140
+ if (!deter || typeof window === "undefined") return;
141
+ let timer;
142
+ const onVisibility = () => setHidden(document.visibilityState === "hidden");
143
+ const onBlur = () => setBlurred(true);
144
+ const onFocus = () => setBlurred(false);
145
+ const onKeyUp = (e) => {
146
+ if (e.key !== "PrintScreen") return;
147
+ setSnapped(true);
148
+ clearTimeout(timer);
149
+ timer = setTimeout(() => setSnapped(false), PRINT_SCREEN_MS);
150
+ void navigator.clipboard?.writeText?.("").catch(() => {
151
+ });
152
+ };
153
+ document.addEventListener("visibilitychange", onVisibility);
154
+ window.addEventListener("blur", onBlur);
155
+ window.addEventListener("focus", onFocus);
156
+ window.addEventListener("keyup", onKeyUp);
157
+ return () => {
158
+ clearTimeout(timer);
159
+ document.removeEventListener("visibilitychange", onVisibility);
160
+ window.removeEventListener("blur", onBlur);
161
+ window.removeEventListener("focus", onFocus);
162
+ window.removeEventListener("keyup", onKeyUp);
163
+ };
164
+ }, [deter]);
165
+ const obscured = deter && (hidden || blurred || snapped);
166
+ const onKeyDown = (e) => {
167
+ if (!print && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "p") e.preventDefault();
168
+ };
169
+ const block = (e) => e.preventDefault();
170
+ const markId = react.useId().replace(/[^a-zA-Z0-9_-]/g, "");
171
+ return /* @__PURE__ */ jsxRuntime.jsxs(
172
+ "div",
173
+ {
174
+ ref,
175
+ "data-react-fancy-pdf-viewer": "",
176
+ "data-mode": "pages",
177
+ "data-print-blocked": print ? void 0 : "true",
178
+ "data-obscured": String(obscured),
179
+ "aria-label": title,
180
+ role: "document",
181
+ onKeyDown,
182
+ className: chunkZU5Q77OT_cjs.cn(
183
+ "relative flex h-full min-h-[20rem] w-full flex-col overflow-hidden rounded-md bg-zinc-100 dark:bg-zinc-900",
184
+ className
185
+ ),
186
+ style: copy ? style : { ...style, userSelect: "none", WebkitUserSelect: "none" },
187
+ children: [
188
+ !print && /* @__PURE__ */ jsxRuntime.jsx("style", { children: PRINT_BLOCK_CSS }),
189
+ /* @__PURE__ */ jsxRuntime.jsxs(
190
+ "div",
191
+ {
192
+ "data-react-fancy-pdf-viewer-toolbar": "",
193
+ className: "flex shrink-0 items-center gap-1 border-b border-zinc-200 bg-[color:var(--fancy-surface,var(--color-white))] px-2 py-1 text-xs text-[color:var(--fancy-text-muted,var(--color-zinc-500))] dark:border-zinc-800 dark:bg-[color:var(--fancy-surface-dark,var(--color-zinc-900))] dark:text-[color:var(--fancy-text-muted-dark,var(--color-zinc-400))]",
194
+ children: [
195
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { attr: "prev", label: "Previous page", disabled: page <= 1, onClick: () => setPage(page - 1), children: /* @__PURE__ */ jsxRuntime.jsx(Chevron, { dir: "up" }) }),
196
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-react-fancy-pdf-viewer-page-label": "", className: "min-w-[3.5rem] text-center tabular-nums", "aria-live": "polite", children: [
197
+ page,
198
+ " / ",
199
+ count
200
+ ] }),
201
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { attr: "next", label: "Next page", disabled: page >= count, onClick: () => setPage(page + 1), children: /* @__PURE__ */ jsxRuntime.jsx(Chevron, { dir: "down" }) }),
202
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 h-4 w-px bg-zinc-200 dark:bg-zinc-700", "aria-hidden": "true" }),
203
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { attr: "zoom-out", label: "Zoom out", disabled: zoom <= MIN_ZOOM, onClick: () => zoomTo(zoom / ZOOM_STEP), children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2212" }) }),
204
+ /* @__PURE__ */ jsxRuntime.jsxs(ToolButton, { attr: "zoom-reset", label: "Reset zoom", wide: true, onClick: () => zoomTo(1), children: [
205
+ Math.round(zoom * 100),
206
+ "%"
207
+ ] }),
208
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { attr: "zoom-in", label: "Zoom in", disabled: zoom >= MAX_ZOOM, onClick: () => zoomTo(zoom * ZOOM_STEP), children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "+" }) }),
209
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { attr: "fit-width", label: "Fit width", wide: true, onClick: () => zoomTo(1), children: "Width" }),
210
+ /* @__PURE__ */ jsxRuntime.jsx(ToolButton, { attr: "fit-page", label: "Fit page", wide: true, onClick: fitPage, children: "Page" })
211
+ ]
212
+ }
213
+ ),
214
+ /* @__PURE__ */ jsxRuntime.jsx(
215
+ "div",
216
+ {
217
+ ref: scrollerRef,
218
+ "data-react-fancy-pdf-viewer-pages": "",
219
+ tabIndex: 0,
220
+ onScroll,
221
+ onContextMenu: download ? void 0 : block,
222
+ onDragStart: download ? void 0 : block,
223
+ className: "relative min-h-0 flex-1 overflow-auto outline-none transition-[filter] duration-150",
224
+ style: { padding: GAP, filter: obscured ? "blur(24px)" : void 0 },
225
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto flex flex-col", style: { width: `${zoom * 100}%`, gap: GAP }, children: Array.from({ length: count }, (_, i) => {
226
+ const n = i + 1;
227
+ return /* @__PURE__ */ jsxRuntime.jsxs(
228
+ "div",
229
+ {
230
+ "data-react-fancy-pdf-viewer-page": "",
231
+ "data-page": n,
232
+ className: "relative w-full overflow-hidden bg-white shadow-sm",
233
+ style: { aspectRatio: String(ratioOf(n)) },
234
+ children: [
235
+ isNear(n) && /* @__PURE__ */ jsxRuntime.jsx(
236
+ "img",
237
+ {
238
+ "data-react-fancy-pdf-viewer-image": "",
239
+ src: pages.url(n, scale),
240
+ alt: `Page ${n}`,
241
+ decoding: "async",
242
+ draggable: download ? void 0 : false,
243
+ className: "block h-full w-full",
244
+ onLoad: (e) => {
245
+ const img = e.currentTarget;
246
+ if (img.naturalWidth > 0 && img.naturalHeight > 0) {
247
+ const r = img.naturalWidth / img.naturalHeight;
248
+ setRatios((prev) => prev[n] === r ? prev : { ...prev, [n]: r });
249
+ }
250
+ }
251
+ }
252
+ ),
253
+ watermark?.text && /* @__PURE__ */ jsxRuntime.jsx(Watermark, { id: `${markId}-${n}`, ...watermark })
254
+ ]
255
+ },
256
+ n
257
+ );
258
+ }) })
259
+ }
260
+ )
261
+ ]
262
+ }
263
+ );
264
+ }
265
+ );
266
+ PdfPages.displayName = "PdfPages";
267
+ function Watermark({ id, text, opacity = 0.15, angle = -30, density = 1 }) {
268
+ const fontSize = 14;
269
+ const gap = 80 / Math.max(0.1, density);
270
+ const w = text.length * fontSize * 0.55 + gap;
271
+ const h = fontSize + gap;
272
+ return /* @__PURE__ */ jsxRuntime.jsxs(
273
+ "svg",
274
+ {
275
+ "data-react-fancy-pdf-viewer-watermark": "",
276
+ "aria-hidden": "true",
277
+ className: "absolute inset-0 h-full w-full select-none",
278
+ style: { pointerEvents: "none" },
279
+ children: [
280
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("pattern", { id, width: w, height: h, patternUnits: "userSpaceOnUse", patternTransform: `rotate(${angle})`, children: /* @__PURE__ */ jsxRuntime.jsx(
281
+ "text",
282
+ {
283
+ x: gap / 2,
284
+ y: h / 2,
285
+ fontSize,
286
+ fontFamily: "ui-sans-serif, system-ui, sans-serif",
287
+ fillOpacity: clamp(opacity, 0, 1),
288
+ style: { fill: "var(--fancy-text, var(--color-zinc-700))" },
289
+ children: text
290
+ }
291
+ ) }) }),
292
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "100%", height: "100%", fill: `url(#${id})` })
293
+ ]
294
+ }
295
+ );
296
+ }
297
+ function ToolButton({
298
+ attr,
299
+ label,
300
+ disabled,
301
+ wide,
302
+ onClick,
303
+ children
304
+ }) {
305
+ return /* @__PURE__ */ jsxRuntime.jsx(
306
+ "button",
307
+ {
308
+ type: "button",
309
+ ...{ [`data-react-fancy-pdf-viewer-${attr}`]: "" },
310
+ "aria-label": label,
311
+ title: label,
312
+ disabled,
313
+ onClick,
314
+ className: chunkZU5Q77OT_cjs.cn(
315
+ "flex h-6 items-center justify-center rounded-full tabular-nums transition-colors disabled:pointer-events-none disabled:opacity-30",
316
+ "hover:bg-zinc-900/5 hover:text-zinc-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 dark:hover:bg-white/10 dark:hover:text-zinc-200",
317
+ wide ? "min-w-[2.25rem] px-1.5" : "w-6"
318
+ ),
319
+ children
320
+ }
321
+ );
322
+ }
323
+ function Chevron({ dir }) {
324
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: dir === "up" ? "m18 15-6-6-6 6" : "m6 9 6 6 6-6" }) });
325
+ }
326
+ var PdfViewer = react.forwardRef(
327
+ ({
328
+ src,
329
+ pages,
330
+ page,
331
+ defaultPage,
332
+ onPageChange,
333
+ zoom,
334
+ defaultZoom,
335
+ onZoomChange,
336
+ policy,
337
+ watermark,
338
+ title = "PDF document",
339
+ className,
340
+ style,
341
+ frame = false,
342
+ frameProps
343
+ }, ref) => {
344
+ if (pages) {
345
+ const pageViewer = /* @__PURE__ */ jsxRuntime.jsx(
346
+ PdfPages,
347
+ {
348
+ ref,
349
+ pages,
350
+ page,
351
+ defaultPage,
352
+ onPageChange,
353
+ zoom,
354
+ defaultZoom,
355
+ onZoomChange,
356
+ policy,
357
+ watermark,
358
+ title,
359
+ className: chunkZU5Q77OT_cjs.cn(frame && "rounded-none", className),
360
+ style
361
+ }
362
+ );
363
+ if (!frame) return pageViewer;
364
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkM5SEYDSS_cjs.FauxClient, { variant: "browser", url: title, controls: { zoom: false }, fill: true, ...frameProps, children: pageViewer });
365
+ }
366
+ const viewer = /* @__PURE__ */ jsxRuntime.jsx(
367
+ "div",
368
+ {
369
+ ref,
370
+ "data-react-fancy-pdf-viewer": "",
371
+ className: chunkZU5Q77OT_cjs.cn(
372
+ "relative h-full min-h-[20rem] w-full overflow-hidden rounded-md bg-zinc-100 dark:bg-zinc-900",
373
+ className
374
+ ),
375
+ style,
376
+ children: /* @__PURE__ */ jsxRuntime.jsxs("object", { data: src, type: "application/pdf", className: "h-full w-full", children: [
377
+ /* @__PURE__ */ jsxRuntime.jsx(
378
+ "iframe",
379
+ {
380
+ src,
381
+ title,
382
+ className: "h-full w-full border-0"
383
+ }
384
+ ),
385
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-2 p-4 text-center text-sm text-[color:var(--fancy-text-muted,var(--color-zinc-500))] dark:text-[color:var(--fancy-text-muted-dark,var(--color-zinc-400))]", children: [
386
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "This browser can't display the PDF inline." }),
387
+ /* @__PURE__ */ jsxRuntime.jsx(
388
+ "a",
389
+ {
390
+ href: src,
391
+ download: true,
392
+ className: "font-medium text-blue-600 hover:underline dark:text-blue-400",
393
+ children: "Download PDF"
394
+ }
395
+ )
396
+ ] })
397
+ ] })
398
+ }
399
+ );
400
+ if (!frame) return viewer;
401
+ return (
402
+ // The browser variant, because the plugin swallows the pointer: hover over
403
+ // the PDF never reaches the frame, so controls over it could not be
404
+ // revealed, and its top-right corner is the PDF toolbar's. In the address
405
+ // bar they are reachable and collide with nothing.
406
+ /* @__PURE__ */ jsxRuntime.jsx(chunkM5SEYDSS_cjs.FauxClient, { variant: "browser", url: title, controls: { zoom: false }, fill: true, ...frameProps, children: viewer })
407
+ );
408
+ }
409
+ );
410
+ PdfViewer.displayName = "PdfViewer";
411
+
412
+ exports.PdfViewer = PdfViewer;
413
+ //# sourceMappingURL=chunk-7MDHCYEB.cjs.map
414
+ //# sourceMappingURL=chunk-7MDHCYEB.cjs.map