@sesionario/blocks 1.23.0 → 1.24.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/pdf.cjs ADDED
@@ -0,0 +1,1045 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactPdf = require('react-pdf');
5
+ var lucideReact = require('lucide-react');
6
+ var clsx = require('clsx');
7
+ var tailwindMerge = require('tailwind-merge');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+ require('react-pdf/dist/Page/TextLayer.css');
10
+ require('react-pdf/dist/Page/AnnotationLayer.css');
11
+ var DialogPrimitive = require('@radix-ui/react-dialog');
12
+
13
+ function _interopNamespace(e) {
14
+ if (e && e.__esModule) return e;
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n.default = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var DialogPrimitive__namespace = /*#__PURE__*/_interopNamespace(DialogPrimitive);
32
+
33
+ function cn(...inputs) {
34
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
35
+ }
36
+ var ZOOM_PAN_MIN_SCALE = 1;
37
+ var ZOOM_PAN_MAX_SCALE = 4;
38
+ var ZOOM_PAN_DOUBLE_TAP_MS = 300;
39
+ var ZOOM_PAN_SWIPE_LOCK_PX = 8;
40
+ var ZOOM_PAN_SWIPE_COMMIT_PX = 56;
41
+ var ZOOM_PAN_SWIPE_SNAP_MS = 180;
42
+ var IDENTITY = { scale: 1, tx: 0, ty: 0 };
43
+ function clamp(value, min, max) {
44
+ return Math.min(max, Math.max(min, value));
45
+ }
46
+ function zoomToward(prev, nextScale, originX, originY, minScale = ZOOM_PAN_MIN_SCALE, maxScale = ZOOM_PAN_MAX_SCALE) {
47
+ const scale = clamp(nextScale, minScale, maxScale);
48
+ if (scale === minScale) return { ...IDENTITY, scale: minScale };
49
+ const ratio = scale / prev.scale;
50
+ return {
51
+ scale,
52
+ tx: originX - (originX - prev.tx) * ratio,
53
+ ty: originY - (originY - prev.ty) * ratio
54
+ };
55
+ }
56
+ function pointerDistance(a, b) {
57
+ return Math.hypot(b.x - a.x, b.y - a.y);
58
+ }
59
+ function prefersReducedMotion() {
60
+ if (typeof window === "undefined" || !window.matchMedia) return false;
61
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
62
+ }
63
+ function useZoomPanGestures(options = {}) {
64
+ const {
65
+ minScale = ZOOM_PAN_MIN_SCALE,
66
+ maxScale = ZOOM_PAN_MAX_SCALE,
67
+ swipeEnabled = true,
68
+ onSwipeCommit,
69
+ wheelZoom = true,
70
+ doubleTap = "reset"
71
+ } = options;
72
+ const [transform, setTransform] = react.useState({
73
+ ...IDENTITY,
74
+ scale: minScale
75
+ });
76
+ const transformRef = react.useRef(transform);
77
+ transformRef.current = transform;
78
+ const [isSwipeDragging, setIsSwipeDragging] = react.useState(false);
79
+ const [snapBack, setSnapBack] = react.useState(false);
80
+ const stageRef = react.useRef(null);
81
+ const pointersRef = react.useRef(/* @__PURE__ */ new Map());
82
+ const pinchStartRef = react.useRef(null);
83
+ const panRef = react.useRef(null);
84
+ const swipeRef = react.useRef(null);
85
+ const lastTapRef = react.useRef(
86
+ null
87
+ );
88
+ const onSwipeCommitRef = react.useRef(onSwipeCommit);
89
+ onSwipeCommitRef.current = onSwipeCommit;
90
+ const swipeEnabledRef = react.useRef(swipeEnabled);
91
+ swipeEnabledRef.current = swipeEnabled;
92
+ const minScaleRef = react.useRef(minScale);
93
+ minScaleRef.current = minScale;
94
+ const maxScaleRef = react.useRef(maxScale);
95
+ maxScaleRef.current = maxScale;
96
+ const doubleTapRef = react.useRef(doubleTap);
97
+ doubleTapRef.current = doubleTap;
98
+ const reset = react.useCallback(() => {
99
+ setTransform({ ...IDENTITY, scale: minScaleRef.current });
100
+ setIsSwipeDragging(false);
101
+ setSnapBack(false);
102
+ }, []);
103
+ const setScale = react.useCallback((next) => {
104
+ setTransform(
105
+ (prev) => zoomToward(prev, next, 0, 0, minScaleRef.current, maxScaleRef.current)
106
+ );
107
+ }, []);
108
+ const clearSnapBack = react.useCallback(() => {
109
+ setSnapBack(false);
110
+ }, []);
111
+ const stageOrigin = react.useCallback((clientX, clientY) => {
112
+ const stage = stageRef.current;
113
+ if (!stage) return { x: 0, y: 0 };
114
+ const rect = stage.getBoundingClientRect();
115
+ return {
116
+ x: clientX - rect.left - rect.width / 2,
117
+ y: clientY - rect.top - rect.height / 2
118
+ };
119
+ }, []);
120
+ react.useEffect(() => {
121
+ if (!wheelZoom) return;
122
+ const stage = stageRef.current;
123
+ if (!stage) return;
124
+ const handleWheel = (e) => {
125
+ e.preventDefault();
126
+ e.stopPropagation();
127
+ const prev = transformRef.current;
128
+ const factor = e.deltaY > 0 ? 0.9 : 1.1;
129
+ const { x, y } = stageOrigin(e.clientX, e.clientY);
130
+ setTransform(
131
+ zoomToward(
132
+ prev,
133
+ prev.scale * factor,
134
+ x,
135
+ y,
136
+ minScaleRef.current,
137
+ maxScaleRef.current
138
+ )
139
+ );
140
+ };
141
+ stage.addEventListener("wheel", handleWheel, { passive: false });
142
+ return () => stage.removeEventListener("wheel", handleWheel);
143
+ }, [stageOrigin, wheelZoom]);
144
+ function clearSwipe() {
145
+ swipeRef.current = null;
146
+ setIsSwipeDragging(false);
147
+ }
148
+ function beginPinch() {
149
+ const pts = [...pointersRef.current.values()];
150
+ const [a, b] = pts;
151
+ if (!a || !b) return;
152
+ const midX = (a.x + b.x) / 2;
153
+ const midY = (a.y + b.y) / 2;
154
+ const { x, y } = stageOrigin(midX, midY);
155
+ pinchStartRef.current = {
156
+ distance: Math.max(pointerDistance(a, b), 1),
157
+ originX: x,
158
+ originY: y,
159
+ transform: { ...transformRef.current }
160
+ };
161
+ panRef.current = null;
162
+ clearSwipe();
163
+ setSnapBack(false);
164
+ }
165
+ function onPointerDown(e) {
166
+ e.stopPropagation();
167
+ const stage = stageRef.current;
168
+ if (!stage) return;
169
+ if (typeof stage.setPointerCapture === "function") {
170
+ stage.setPointerCapture(e.pointerId);
171
+ }
172
+ pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
173
+ setSnapBack(false);
174
+ if (pointersRef.current.size === 2) {
175
+ beginPinch();
176
+ return;
177
+ }
178
+ if (transformRef.current.scale > minScaleRef.current) {
179
+ panRef.current = {
180
+ pointerId: e.pointerId,
181
+ startX: e.clientX,
182
+ startY: e.clientY,
183
+ originTx: transformRef.current.tx,
184
+ originTy: transformRef.current.ty,
185
+ moved: false
186
+ };
187
+ return;
188
+ }
189
+ if (swipeEnabledRef.current) {
190
+ swipeRef.current = {
191
+ pointerId: e.pointerId,
192
+ startX: e.clientX,
193
+ startY: e.clientY,
194
+ moved: false,
195
+ axis: null
196
+ };
197
+ setIsSwipeDragging(true);
198
+ }
199
+ }
200
+ function onPointerMove(e) {
201
+ if (!pointersRef.current.has(e.pointerId)) return;
202
+ pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
203
+ const pinchStart = pinchStartRef.current;
204
+ if (pointersRef.current.size >= 2 && pinchStart) {
205
+ const pts = [...pointersRef.current.values()];
206
+ const [a, b] = pts;
207
+ if (!a || !b) return;
208
+ const distance = Math.max(pointerDistance(a, b), 1);
209
+ const nextScale = pinchStart.transform.scale * (distance / pinchStart.distance);
210
+ setTransform(
211
+ zoomToward(
212
+ pinchStart.transform,
213
+ nextScale,
214
+ pinchStart.originX,
215
+ pinchStart.originY,
216
+ minScaleRef.current,
217
+ maxScaleRef.current
218
+ )
219
+ );
220
+ return;
221
+ }
222
+ const pan = panRef.current;
223
+ if (pan && pan.pointerId === e.pointerId) {
224
+ const dx2 = e.clientX - pan.startX;
225
+ const dy2 = e.clientY - pan.startY;
226
+ if (Math.abs(dx2) > 2 || Math.abs(dy2) > 2) pan.moved = true;
227
+ setTransform((prev) => ({
228
+ ...prev,
229
+ tx: pan.originTx + dx2,
230
+ ty: pan.originTy + dy2
231
+ }));
232
+ return;
233
+ }
234
+ const swipe = swipeRef.current;
235
+ if (!swipe || swipe.pointerId !== e.pointerId) return;
236
+ const dx = e.clientX - swipe.startX;
237
+ const dy = e.clientY - swipe.startY;
238
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) swipe.moved = true;
239
+ if (swipe.axis === null) {
240
+ const absX = Math.abs(dx);
241
+ const absY = Math.abs(dy);
242
+ if (absX < ZOOM_PAN_SWIPE_LOCK_PX && absY < ZOOM_PAN_SWIPE_LOCK_PX) {
243
+ return;
244
+ }
245
+ swipe.axis = absX >= absY ? "x" : "y";
246
+ if (swipe.axis === "y") {
247
+ clearSwipe();
248
+ setTransform({ ...IDENTITY, scale: minScaleRef.current });
249
+ return;
250
+ }
251
+ }
252
+ if (swipe.axis === "x") {
253
+ setTransform({
254
+ scale: minScaleRef.current,
255
+ tx: dx,
256
+ ty: 0
257
+ });
258
+ }
259
+ }
260
+ function finishSwipe(e) {
261
+ const swipe = swipeRef.current;
262
+ if (!swipe || swipe.pointerId !== e.pointerId) return false;
263
+ const dx = e.clientX - swipe.startX;
264
+ const wasMoved = swipe.moved;
265
+ const axis = swipe.axis;
266
+ clearSwipe();
267
+ if (axis === "x" && Math.abs(dx) >= ZOOM_PAN_SWIPE_COMMIT_PX) {
268
+ onSwipeCommitRef.current?.(dx < 0 ? "next" : "prev");
269
+ return true;
270
+ }
271
+ if (wasMoved || transformRef.current.tx !== 0) {
272
+ if (!prefersReducedMotion()) setSnapBack(true);
273
+ setTransform({ ...IDENTITY, scale: minScaleRef.current });
274
+ }
275
+ return wasMoved;
276
+ }
277
+ function onPointerUp(e) {
278
+ const stage = stageRef.current;
279
+ if (stage && typeof stage.hasPointerCapture === "function" && stage.hasPointerCapture(e.pointerId) && typeof stage.releasePointerCapture === "function") {
280
+ stage.releasePointerCapture(e.pointerId);
281
+ }
282
+ pointersRef.current.delete(e.pointerId);
283
+ if (pointersRef.current.size < 2) {
284
+ pinchStartRef.current = null;
285
+ }
286
+ if (pointersRef.current.size === 1) {
287
+ const remaining = [...pointersRef.current.entries()][0];
288
+ if (remaining && transformRef.current.scale > minScaleRef.current) {
289
+ const [pointerId, point] = remaining;
290
+ panRef.current = {
291
+ pointerId,
292
+ startX: point.x,
293
+ startY: point.y,
294
+ originTx: transformRef.current.tx,
295
+ originTy: transformRef.current.ty,
296
+ moved: true
297
+ };
298
+ }
299
+ }
300
+ const swipeHandled = finishSwipe(e);
301
+ if (swipeHandled) return;
302
+ const pan = panRef.current;
303
+ if (pan && pan.pointerId === e.pointerId) {
304
+ const wasMoved = pan.moved;
305
+ panRef.current = null;
306
+ if (wasMoved) return;
307
+ }
308
+ if (pointersRef.current.size === 0 && e.button === 0) {
309
+ const now = Date.now();
310
+ const last = lastTapRef.current;
311
+ if (last && now - last.time < ZOOM_PAN_DOUBLE_TAP_MS && Math.hypot(e.clientX - last.x, e.clientY - last.y) < 24) {
312
+ lastTapRef.current = null;
313
+ if (doubleTapRef.current === "toggle" && transformRef.current.scale <= minScaleRef.current) {
314
+ const { x, y } = stageOrigin(e.clientX, e.clientY);
315
+ setTransform(
316
+ zoomToward(
317
+ transformRef.current,
318
+ Math.min(2, maxScaleRef.current),
319
+ x,
320
+ y,
321
+ minScaleRef.current,
322
+ maxScaleRef.current
323
+ )
324
+ );
325
+ } else {
326
+ reset();
327
+ }
328
+ return;
329
+ }
330
+ lastTapRef.current = { time: now, x: e.clientX, y: e.clientY };
331
+ }
332
+ }
333
+ return {
334
+ stageRef,
335
+ transform,
336
+ isDragging: isSwipeDragging,
337
+ snapBack,
338
+ handlers: {
339
+ onPointerDown,
340
+ onPointerMove,
341
+ onPointerUp,
342
+ onPointerCancel: onPointerUp
343
+ },
344
+ reset,
345
+ setScale,
346
+ clearSnapBack
347
+ };
348
+ }
349
+ function LoadingState({
350
+ label = "Cargando\u2026",
351
+ className
352
+ }) {
353
+ return /* @__PURE__ */ jsxRuntime.jsxs(
354
+ "div",
355
+ {
356
+ role: "status",
357
+ "aria-live": "polite",
358
+ className: cn(
359
+ "flex items-center justify-center gap-2 py-12 text-sm text-muted-foreground",
360
+ className
361
+ ),
362
+ children: [
363
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "size-4 animate-spin", "aria-hidden": "true" }),
364
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: label })
365
+ ]
366
+ }
367
+ );
368
+ }
369
+ function ErrorState({
370
+ title = "Algo sali\xF3 mal",
371
+ description,
372
+ action,
373
+ className
374
+ }) {
375
+ return /* @__PURE__ */ jsxRuntime.jsxs(
376
+ "div",
377
+ {
378
+ role: "alert",
379
+ className: cn(
380
+ "flex flex-col items-center gap-3 rounded-xl border border-dashed border-destructive/40 bg-destructive/5 py-12 text-center",
381
+ className
382
+ ),
383
+ children: [
384
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "size-6 text-destructive", "aria-hidden": "true" }),
385
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
386
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: title }),
387
+ description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description }) : null
388
+ ] }),
389
+ action
390
+ ]
391
+ }
392
+ );
393
+ }
394
+ function LazyThumbnail({
395
+ pageNumber,
396
+ active,
397
+ width,
398
+ onClick,
399
+ ariaLabel
400
+ }) {
401
+ const ref = react.useRef(null);
402
+ const [visible, setVisible] = react.useState(false);
403
+ react.useEffect(() => {
404
+ const node = ref.current;
405
+ if (!node || typeof IntersectionObserver === "undefined") {
406
+ setVisible(true);
407
+ return;
408
+ }
409
+ const observer = new IntersectionObserver(
410
+ (entries) => {
411
+ if (entries.some((e) => e.isIntersecting)) {
412
+ setVisible(true);
413
+ observer.disconnect();
414
+ }
415
+ },
416
+ { rootMargin: "120px 0px" }
417
+ );
418
+ observer.observe(node);
419
+ return () => observer.disconnect();
420
+ }, []);
421
+ return /* @__PURE__ */ jsxRuntime.jsxs(
422
+ "button",
423
+ {
424
+ ref,
425
+ type: "button",
426
+ onClick,
427
+ "aria-label": ariaLabel,
428
+ "aria-current": active ? "page" : void 0,
429
+ className: cn(
430
+ "block w-full overflow-hidden rounded-md border bg-background p-1 transition-colors hover:border-primary",
431
+ active ? "border-primary ring-2 ring-primary/30" : "border-border"
432
+ ),
433
+ children: [
434
+ visible ? /* @__PURE__ */ jsxRuntime.jsx(reactPdf.Thumbnail, { pageNumber, width, className: "mx-auto" }) : /* @__PURE__ */ jsxRuntime.jsx(
435
+ "div",
436
+ {
437
+ className: "mx-auto bg-muted",
438
+ style: { width, height: Math.round(width * 1.4) },
439
+ "aria-hidden": true
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 block text-center text-[10px] text-muted-foreground", children: pageNumber })
443
+ ]
444
+ }
445
+ );
446
+ }
447
+ var configuredSrc = null;
448
+ function configurePdfWorker(workerSrc) {
449
+ if (!workerSrc || configuredSrc === workerSrc) return;
450
+ reactPdf.pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
451
+ configuredSrc = workerSrc;
452
+ }
453
+ var ZOOM_STEPS = [0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3];
454
+ var THUMB_WIDTH = 120;
455
+ function PdfViewer({
456
+ fileUrl,
457
+ fileName,
458
+ downloadUrl,
459
+ workerSrc,
460
+ initialPage = 1,
461
+ className,
462
+ labels
463
+ }) {
464
+ configurePdfWorker(workerSrc);
465
+ const [numPages, setNumPages] = react.useState(0);
466
+ const [pageNumber, setPageNumber] = react.useState(initialPage);
467
+ const [zoomIndex, setZoomIndex] = react.useState(2);
468
+ const [fitMode, setFitMode] = react.useState("width");
469
+ const [rotation, setRotation] = react.useState(0);
470
+ const [containerWidth, setContainerWidth] = react.useState(0);
471
+ const [containerHeight, setContainerHeight] = react.useState(0);
472
+ const [isFullscreen, setIsFullscreen] = react.useState(false);
473
+ const [thumbsOpen, setThumbsOpen] = react.useState(false);
474
+ const [loadError, setLoadError] = react.useState(false);
475
+ const [pageDraft, setPageDraft] = react.useState(String(initialPage));
476
+ const viewportRef = react.useRef(null);
477
+ const shellRef = react.useRef(null);
478
+ const pageNumberRef = react.useRef(pageNumber);
479
+ pageNumberRef.current = pageNumber;
480
+ const numPagesRef = react.useRef(numPages);
481
+ numPagesRef.current = numPages;
482
+ const baseScale = ZOOM_STEPS[zoomIndex] ?? 1;
483
+ const goToPage = react.useCallback((next) => {
484
+ const total = numPagesRef.current;
485
+ if (total <= 0) return;
486
+ const clamped = Math.min(Math.max(1, next), total);
487
+ setPageNumber(clamped);
488
+ setPageDraft(String(clamped));
489
+ }, []);
490
+ const onSwipeCommit = react.useCallback(
491
+ (direction) => {
492
+ if (direction === "next") {
493
+ goToPage(pageNumberRef.current + 1);
494
+ } else {
495
+ goToPage(pageNumberRef.current - 1);
496
+ }
497
+ },
498
+ [goToPage]
499
+ );
500
+ const {
501
+ stageRef,
502
+ transform,
503
+ isDragging,
504
+ snapBack,
505
+ handlers,
506
+ reset,
507
+ clearSnapBack
508
+ } = useZoomPanGestures({
509
+ swipeEnabled: numPages > 1,
510
+ onSwipeCommit,
511
+ wheelZoom: true,
512
+ doubleTap: "toggle"
513
+ });
514
+ react.useEffect(() => {
515
+ setPageNumber(initialPage);
516
+ setPageDraft(String(initialPage));
517
+ setZoomIndex(2);
518
+ setFitMode("width");
519
+ setRotation(0);
520
+ setNumPages(0);
521
+ setLoadError(false);
522
+ setThumbsOpen(false);
523
+ reset();
524
+ }, [fileUrl, initialPage, reset]);
525
+ react.useEffect(() => {
526
+ reset();
527
+ }, [pageNumber, reset]);
528
+ react.useEffect(() => {
529
+ if (!viewportRef.current) return;
530
+ const el = viewportRef.current;
531
+ const observer = new ResizeObserver((entries) => {
532
+ const rect = entries[0]?.contentRect;
533
+ if (!rect) return;
534
+ setContainerWidth(rect.width);
535
+ setContainerHeight(rect.height);
536
+ });
537
+ observer.observe(el);
538
+ setContainerWidth(el.clientWidth);
539
+ setContainerHeight(el.clientHeight);
540
+ return () => observer.disconnect();
541
+ }, []);
542
+ react.useEffect(() => {
543
+ function onFullscreenChange() {
544
+ setIsFullscreen(document.fullscreenElement === shellRef.current);
545
+ }
546
+ document.addEventListener("fullscreenchange", onFullscreenChange);
547
+ return () => document.removeEventListener("fullscreenchange", onFullscreenChange);
548
+ }, []);
549
+ react.useEffect(() => {
550
+ function onKeyDown(e) {
551
+ const target = e.target;
552
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) {
553
+ return;
554
+ }
555
+ switch (e.key) {
556
+ case "ArrowLeft":
557
+ case "PageUp":
558
+ e.preventDefault();
559
+ goToPage(pageNumberRef.current - 1);
560
+ break;
561
+ case "ArrowRight":
562
+ case "PageDown":
563
+ e.preventDefault();
564
+ goToPage(pageNumberRef.current + 1);
565
+ break;
566
+ case "Home":
567
+ e.preventDefault();
568
+ goToPage(1);
569
+ break;
570
+ case "End":
571
+ e.preventDefault();
572
+ goToPage(numPagesRef.current);
573
+ break;
574
+ case "+":
575
+ case "=":
576
+ e.preventDefault();
577
+ setZoomIndex((i) => Math.min(i + 1, ZOOM_STEPS.length - 1));
578
+ break;
579
+ case "-":
580
+ e.preventDefault();
581
+ setZoomIndex((i) => Math.max(i - 1, 0));
582
+ break;
583
+ }
584
+ }
585
+ window.addEventListener("keydown", onKeyDown);
586
+ return () => window.removeEventListener("keydown", onKeyDown);
587
+ }, [goToPage]);
588
+ const fitWidth = react.useCallback(() => {
589
+ setFitMode("width");
590
+ setZoomIndex(2);
591
+ reset();
592
+ }, [reset]);
593
+ const fitPage = react.useCallback(() => {
594
+ setFitMode("page");
595
+ setZoomIndex(2);
596
+ reset();
597
+ }, [reset]);
598
+ const zoomIn = react.useCallback(() => {
599
+ setZoomIndex((i) => Math.min(i + 1, ZOOM_STEPS.length - 1));
600
+ }, []);
601
+ const zoomOut = react.useCallback(() => {
602
+ setZoomIndex((i) => Math.max(i - 1, 0));
603
+ }, []);
604
+ const toggleFullscreen = react.useCallback(async () => {
605
+ if (!shellRef.current) return;
606
+ if (document.fullscreenElement === shellRef.current) {
607
+ await document.exitFullscreen();
608
+ } else {
609
+ await shellRef.current.requestFullscreen();
610
+ }
611
+ }, []);
612
+ function submitPageJump(e) {
613
+ e.preventDefault();
614
+ const parsed = Number.parseInt(pageDraft, 10);
615
+ if (Number.isFinite(parsed)) goToPage(parsed);
616
+ else setPageDraft(String(pageNumber));
617
+ }
618
+ const pageWidth = fitMode === "width" ? Math.max(containerWidth - 32, 240) * baseScale : void 0;
619
+ const pageHeight = fitMode === "page" ? Math.max(containerHeight - 32, 240) * baseScale : void 0;
620
+ const rawDownload = downloadUrl ?? fileUrl;
621
+ const downloadHref = `${rawDownload}${rawDownload.includes("?") ? "&" : "?"}download=1`;
622
+ const { scale: gestureScale, tx, ty } = transform;
623
+ const transition = snapBack && !isDragging && !prefersReducedMotion() ? `transform ${ZOOM_PAN_SWIPE_SNAP_MS}ms ease-out` : void 0;
624
+ const pageOf = labels.pageOf?.(pageNumber, numPages) ?? (numPages > 0 ? `${pageNumber} / ${numPages}` : "\u2014");
625
+ function renderThumbList(onSelect) {
626
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: Array.from({ length: numPages }, (_, i) => {
627
+ const n = i + 1;
628
+ return /* @__PURE__ */ jsxRuntime.jsx(
629
+ LazyThumbnail,
630
+ {
631
+ pageNumber: n,
632
+ active: n === pageNumber,
633
+ width: THUMB_WIDTH,
634
+ onClick: () => onSelect(n),
635
+ ariaLabel: labels.thumbnailAria?.(n) ?? `P\xE1gina ${n}`
636
+ },
637
+ n
638
+ );
639
+ }) });
640
+ }
641
+ return /* @__PURE__ */ jsxRuntime.jsxs(
642
+ "div",
643
+ {
644
+ ref: shellRef,
645
+ className: cn(
646
+ "relative flex h-full min-h-0 w-full flex-col bg-background",
647
+ className
648
+ ),
649
+ children: [
650
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-1 overflow-x-auto border-b px-2 py-2", children: [
651
+ /* @__PURE__ */ jsxRuntime.jsx(
652
+ "button",
653
+ {
654
+ type: "button",
655
+ className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border bg-background text-foreground hover:bg-accent",
656
+ onClick: () => setThumbsOpen((v) => !v),
657
+ "aria-label": thumbsOpen ? labels.hideThumbnails ?? "Ocultar miniaturas" : labels.showThumbnails ?? "Mostrar miniaturas",
658
+ "aria-pressed": thumbsOpen,
659
+ children: thumbsOpen ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftClose, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeft, { className: "h-4 w-4" })
660
+ }
661
+ ),
662
+ /* @__PURE__ */ jsxRuntime.jsx(
663
+ "button",
664
+ {
665
+ type: "button",
666
+ className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border bg-background hover:bg-accent disabled:opacity-50",
667
+ onClick: () => goToPage(pageNumber - 1),
668
+ disabled: pageNumber <= 1,
669
+ "aria-label": labels.previousPage ?? "P\xE1gina anterior",
670
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-4 w-4" })
671
+ }
672
+ ),
673
+ /* @__PURE__ */ jsxRuntime.jsxs(
674
+ "form",
675
+ {
676
+ onSubmit: submitPageJump,
677
+ className: "flex shrink-0 items-center gap-1 text-xs text-muted-foreground",
678
+ children: [
679
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "sr-only", htmlFor: "pdf-page-jump", children: labels.pageJumpAria ?? "Ir a p\xE1gina" }),
680
+ /* @__PURE__ */ jsxRuntime.jsx(
681
+ "input",
682
+ {
683
+ id: "pdf-page-jump",
684
+ value: pageDraft,
685
+ onChange: (e) => setPageDraft(e.target.value),
686
+ className: "h-8 w-12 rounded-md border bg-background px-1 text-center text-foreground",
687
+ inputMode: "numeric"
688
+ }
689
+ ),
690
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-10 whitespace-nowrap", children: pageOf })
691
+ ]
692
+ }
693
+ ),
694
+ /* @__PURE__ */ jsxRuntime.jsx(
695
+ "button",
696
+ {
697
+ type: "button",
698
+ className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border bg-background hover:bg-accent disabled:opacity-50",
699
+ onClick: () => goToPage(pageNumber + 1),
700
+ disabled: pageNumber >= numPages,
701
+ "aria-label": labels.nextPage ?? "P\xE1gina siguiente",
702
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" })
703
+ }
704
+ ),
705
+ /* @__PURE__ */ jsxRuntime.jsx(
706
+ "button",
707
+ {
708
+ type: "button",
709
+ className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border bg-background hover:bg-accent disabled:opacity-50",
710
+ onClick: zoomOut,
711
+ disabled: zoomIndex <= 0,
712
+ "aria-label": labels.zoomOut ?? "Alejar",
713
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZoomOut, { className: "h-4 w-4" })
714
+ }
715
+ ),
716
+ /* @__PURE__ */ jsxRuntime.jsx(
717
+ "button",
718
+ {
719
+ type: "button",
720
+ className: "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-background px-2 text-xs hover:bg-accent",
721
+ onClick: fitMode === "width" ? fitPage : fitWidth,
722
+ children: fitMode === "width" ? labels.fitPage ?? "Ajustar p\xE1gina" : labels.fitWidth ?? "Ajustar ancho"
723
+ }
724
+ ),
725
+ /* @__PURE__ */ jsxRuntime.jsx(
726
+ "button",
727
+ {
728
+ type: "button",
729
+ className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border bg-background hover:bg-accent disabled:opacity-50",
730
+ onClick: zoomIn,
731
+ disabled: zoomIndex >= ZOOM_STEPS.length - 1,
732
+ "aria-label": labels.zoomIn ?? "Acercar",
733
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ZoomIn, { className: "h-4 w-4" })
734
+ }
735
+ ),
736
+ /* @__PURE__ */ jsxRuntime.jsx(
737
+ "button",
738
+ {
739
+ type: "button",
740
+ className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border bg-background hover:bg-accent",
741
+ onClick: () => setRotation((r) => (r + 90) % 360),
742
+ "aria-label": labels.rotate ?? "Rotar",
743
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCw, { className: "h-4 w-4" })
744
+ }
745
+ ),
746
+ /* @__PURE__ */ jsxRuntime.jsx(
747
+ "button",
748
+ {
749
+ type: "button",
750
+ className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md border bg-background hover:bg-accent",
751
+ onClick: () => void toggleFullscreen(),
752
+ "aria-label": isFullscreen ? labels.exitFullscreen ?? "Salir de pantalla completa" : labels.enterFullscreen ?? "Pantalla completa",
753
+ children: isFullscreen ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minimize2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize2, { className: "h-4 w-4" })
754
+ }
755
+ ),
756
+ /* @__PURE__ */ jsxRuntime.jsxs(
757
+ "a",
758
+ {
759
+ href: downloadHref,
760
+ download: fileName,
761
+ "aria-label": labels.downloadAria ?? labels.download ?? "Descargar",
762
+ className: "inline-flex h-8 shrink-0 items-center gap-1 rounded-md border bg-background px-2 text-xs hover:bg-accent",
763
+ children: [
764
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { className: "h-4 w-4" }),
765
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: labels.download ?? "Descargar" })
766
+ ]
767
+ }
768
+ )
769
+ ] }),
770
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex min-h-0 flex-1", children: [
771
+ numPages > 0 && /* @__PURE__ */ jsxRuntime.jsx(
772
+ "div",
773
+ {
774
+ className: cn(
775
+ "hidden w-40 shrink-0 overflow-y-auto border-r bg-background p-2 lg:block",
776
+ !thumbsOpen && "lg:hidden"
777
+ ),
778
+ children: renderThumbList(goToPage)
779
+ }
780
+ ),
781
+ thumbsOpen && numPages > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
782
+ /* @__PURE__ */ jsxRuntime.jsx(
783
+ "button",
784
+ {
785
+ type: "button",
786
+ "aria-label": labels.hideThumbnails ? `${labels.hideThumbnails} (fondo)` : "Cerrar miniaturas (fondo)",
787
+ className: "absolute inset-0 z-20 bg-black/40 lg:hidden",
788
+ onClick: () => setThumbsOpen(false)
789
+ }
790
+ ),
791
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 z-30 w-40 overflow-y-auto border-r bg-background p-2 shadow-lg lg:hidden", children: renderThumbList((n) => {
792
+ goToPage(n);
793
+ setThumbsOpen(false);
794
+ }) })
795
+ ] }),
796
+ /* @__PURE__ */ jsxRuntime.jsx(
797
+ "div",
798
+ {
799
+ ref: viewportRef,
800
+ className: cn(
801
+ "relative min-h-0 flex-1 overflow-hidden bg-muted/40",
802
+ gestureScale > ZOOM_PAN_MIN_SCALE && (isDragging ? "cursor-grabbing" : "cursor-grab")
803
+ ),
804
+ children: loadError ? /* @__PURE__ */ jsxRuntime.jsx(
805
+ ErrorState,
806
+ {
807
+ title: labels.loadError ?? "No se pudo cargar el PDF",
808
+ description: labels.loadErrorDescription ?? "El archivo puede estar da\xF1ado, cifrado o bloqueado.",
809
+ action: /* @__PURE__ */ jsxRuntime.jsx(
810
+ "a",
811
+ {
812
+ href: downloadHref,
813
+ download: fileName,
814
+ className: "text-sm text-primary underline",
815
+ children: labels.download ?? "Descargar"
816
+ }
817
+ )
818
+ }
819
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
820
+ "div",
821
+ {
822
+ ref: stageRef,
823
+ className: "flex min-h-full touch-none items-start justify-center p-4",
824
+ style: {
825
+ transform: `translate(${tx}px, ${ty}px) scale(${gestureScale})`,
826
+ transition
827
+ },
828
+ onPointerDown: handlers.onPointerDown,
829
+ onPointerMove: handlers.onPointerMove,
830
+ onPointerUp: handlers.onPointerUp,
831
+ onPointerCancel: handlers.onPointerCancel,
832
+ onTransitionEnd: clearSnapBack,
833
+ children: /* @__PURE__ */ jsxRuntime.jsx(
834
+ reactPdf.Document,
835
+ {
836
+ file: fileUrl,
837
+ onLoadSuccess: ({ numPages: pages }) => {
838
+ setNumPages(pages);
839
+ setLoadError(false);
840
+ const start = Math.min(Math.max(1, initialPage), pages);
841
+ setPageNumber(start);
842
+ setPageDraft(String(start));
843
+ },
844
+ onLoadError: () => setLoadError(true),
845
+ loading: /* @__PURE__ */ jsxRuntime.jsx(LoadingState, { label: labels.loading ?? "Cargando PDF\u2026" }),
846
+ error: /* @__PURE__ */ jsxRuntime.jsx(
847
+ ErrorState,
848
+ {
849
+ title: labels.loadError ?? "No se pudo cargar el PDF"
850
+ }
851
+ ),
852
+ children: numPages > 0 && /* @__PURE__ */ jsxRuntime.jsx(
853
+ reactPdf.Page,
854
+ {
855
+ pageNumber,
856
+ width: pageWidth,
857
+ height: pageHeight,
858
+ rotate: rotation,
859
+ renderTextLayer: true,
860
+ renderAnnotationLayer: true
861
+ }
862
+ )
863
+ }
864
+ )
865
+ }
866
+ )
867
+ }
868
+ )
869
+ ] })
870
+ ]
871
+ }
872
+ );
873
+ }
874
+ function Dialog({
875
+ ...props
876
+ }) {
877
+ return /* @__PURE__ */ jsxRuntime.jsx(DialogPrimitive__namespace.Root, { "data-slot": "dialog", ...props });
878
+ }
879
+ function DialogPortal({
880
+ ...props
881
+ }) {
882
+ return /* @__PURE__ */ jsxRuntime.jsx(DialogPrimitive__namespace.Portal, { "data-slot": "dialog-portal", ...props });
883
+ }
884
+ function DialogOverlay({
885
+ className,
886
+ ...props
887
+ }) {
888
+ return /* @__PURE__ */ jsxRuntime.jsx(
889
+ DialogPrimitive__namespace.Overlay,
890
+ {
891
+ "data-slot": "dialog-overlay",
892
+ className: cn(
893
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
894
+ className
895
+ ),
896
+ ...props
897
+ }
898
+ );
899
+ }
900
+ function DialogContent({
901
+ className,
902
+ children,
903
+ showCloseButton = true,
904
+ ...props
905
+ }) {
906
+ return /* @__PURE__ */ jsxRuntime.jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [
907
+ /* @__PURE__ */ jsxRuntime.jsx(DialogOverlay, {}),
908
+ /* @__PURE__ */ jsxRuntime.jsxs(
909
+ DialogPrimitive__namespace.Content,
910
+ {
911
+ "data-slot": "dialog-content",
912
+ className: cn(
913
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
914
+ className
915
+ ),
916
+ ...props,
917
+ children: [
918
+ children,
919
+ showCloseButton && /* @__PURE__ */ jsxRuntime.jsxs(
920
+ DialogPrimitive__namespace.Close,
921
+ {
922
+ "data-slot": "dialog-close",
923
+ className: "ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
924
+ children: [
925
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.XIcon, {}),
926
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "sr-only", children: "Close" })
927
+ ]
928
+ }
929
+ )
930
+ ]
931
+ }
932
+ )
933
+ ] });
934
+ }
935
+ function DialogTitle({
936
+ className,
937
+ ...props
938
+ }) {
939
+ return /* @__PURE__ */ jsxRuntime.jsx(
940
+ DialogPrimitive__namespace.Title,
941
+ {
942
+ "data-slot": "dialog-title",
943
+ className: cn("text-lg leading-none font-semibold", className),
944
+ ...props
945
+ }
946
+ );
947
+ }
948
+ function DialogDescription({
949
+ className,
950
+ ...props
951
+ }) {
952
+ return /* @__PURE__ */ jsxRuntime.jsx(
953
+ DialogPrimitive__namespace.Description,
954
+ {
955
+ "data-slot": "dialog-description",
956
+ className: cn("text-muted-foreground text-sm", className),
957
+ ...props
958
+ }
959
+ );
960
+ }
961
+ function MediaViewerDialog({
962
+ open,
963
+ onOpenChange,
964
+ children,
965
+ labels,
966
+ className,
967
+ contentClassName,
968
+ showCloseButton = true
969
+ }) {
970
+ const closeLabel = labels?.close ?? "Cerrar";
971
+ const title = labels?.title ?? "Visor";
972
+ const description = labels?.description ?? title;
973
+ return /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(
974
+ DialogContent,
975
+ {
976
+ showCloseButton: false,
977
+ className: cn(
978
+ "inset-0 flex h-dvh max-h-dvh w-screen max-w-none translate-x-0 translate-y-0 top-0 left-0 gap-0 overflow-hidden rounded-none border-0 p-0 shadow-none sm:max-w-none",
979
+ contentClassName,
980
+ className
981
+ ),
982
+ children: [
983
+ /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { className: "sr-only", children: title }),
984
+ /* @__PURE__ */ jsxRuntime.jsx(DialogDescription, { className: "sr-only", children: description }),
985
+ showCloseButton && /* @__PURE__ */ jsxRuntime.jsx(
986
+ "button",
987
+ {
988
+ type: "button",
989
+ onClick: () => onOpenChange(false),
990
+ "aria-label": closeLabel,
991
+ className: "absolute top-3 right-3 z-20 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70",
992
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5" })
993
+ }
994
+ ),
995
+ children
996
+ ]
997
+ }
998
+ ) });
999
+ }
1000
+ function PdfViewerDialog({
1001
+ open,
1002
+ onOpenChange,
1003
+ title,
1004
+ fileUrl,
1005
+ fileName,
1006
+ downloadUrl,
1007
+ workerSrc,
1008
+ initialPage,
1009
+ labels
1010
+ }) {
1011
+ const viewerLabels = labels;
1012
+ return /* @__PURE__ */ jsxRuntime.jsx(
1013
+ MediaViewerDialog,
1014
+ {
1015
+ open,
1016
+ onOpenChange,
1017
+ labels: {
1018
+ close: labels.close,
1019
+ title,
1020
+ description: labels.dialogDescription ?? title
1021
+ },
1022
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full min-h-0 flex-col pt-12", children: [
1023
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b px-4 py-2 pr-14", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-medium", children: title }) }),
1024
+ /* @__PURE__ */ jsxRuntime.jsx(
1025
+ PdfViewer,
1026
+ {
1027
+ fileUrl,
1028
+ fileName: fileName ?? title,
1029
+ downloadUrl,
1030
+ workerSrc,
1031
+ initialPage,
1032
+ labels: viewerLabels,
1033
+ className: "min-h-0 flex-1"
1034
+ }
1035
+ )
1036
+ ] })
1037
+ }
1038
+ );
1039
+ }
1040
+
1041
+ exports.PdfViewer = PdfViewer;
1042
+ exports.PdfViewerDialog = PdfViewerDialog;
1043
+ exports.configurePdfWorker = configurePdfWorker;
1044
+ //# sourceMappingURL=pdf.cjs.map
1045
+ //# sourceMappingURL=pdf.cjs.map