@anaralabs/lector 3.14.12 → 3.14.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, memo, forwardRef, useMemo, cloneElement, useEffect, useRef, useState, useCallback, useContext, useLayoutEffect, createRef, useSyncExternalStore, useImperativeHandle } from 'react';
3
+ import React, { createContext, memo, forwardRef, useMemo, cloneElement, useEffect, useRef, useLayoutEffect, useContext, useState, useCallback, useSyncExternalStore, createRef, useImperativeHandle } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { useFloating, autoUpdate, offset, flip, shift, useDismiss, useInteractions, inline } from '@floating-ui/react';
6
6
  import { useStore, createStore } from 'zustand';
@@ -9,7 +9,7 @@ import { useShallow } from 'zustand/react/shallow';
9
9
  import { useDebounce } from 'use-debounce';
10
10
  import { v4 } from 'uuid';
11
11
  import { Slot } from '@radix-ui/react-slot';
12
- import { useVirtualizer, elementScroll, debounce } from '@tanstack/react-virtual';
12
+ import { useVirtualizer, elementScroll, observeElementRect } from '@tanstack/react-virtual';
13
13
 
14
14
  // src/lib/clamp.ts
15
15
  var clamp = (value, minimum, maximum) => {
@@ -219,6 +219,19 @@ function createDarkModeColorMap(colors) {
219
219
  return map;
220
220
  }
221
221
 
222
+ // src/lib/viewport-zoom.ts
223
+ var viewportZoomHandlers = /* @__PURE__ */ new WeakMap();
224
+ function applyViewportZoom(viewport, zoom) {
225
+ if (viewport) viewportZoomHandlers.get(viewport)?.(zoom);
226
+ }
227
+ function registerViewportZoom(viewport, apply) {
228
+ viewportZoomHandlers.set(viewport, apply);
229
+ return () => {
230
+ if (viewportZoomHandlers.get(viewport) === apply)
231
+ viewportZoomHandlers.delete(viewport);
232
+ };
233
+ }
234
+
222
235
  // src/lib/zoom.ts
223
236
  var USE_LAYOUT_ZOOM = typeof navigator !== "undefined" && /AppleWebKit/i.test(navigator.userAgent) && !/Chrome|Chromium|Android/i.test(navigator.userAgent);
224
237
  var getFitWidthZoom = (containerWidth, viewports, zoomOptions) => {
@@ -271,6 +284,7 @@ var PDFStore = createZustandContext(
271
284
  minZoom,
272
285
  maxZoom
273
286
  );
287
+ applyViewportZoom(state.viewportRef.current, newZoom);
274
288
  return newZoom === state.zoom && isZoomFitWidth === state.isZoomFitWidth ? state : { zoom: newZoom, isZoomFitWidth };
275
289
  });
276
290
  },
@@ -285,7 +299,8 @@ var PDFStore = createZustandContext(
285
299
  get().updateZoom(clampedZoom, true);
286
300
  return clampedZoom;
287
301
  },
288
- currentPage: 1,
302
+ currentPage: initialState.initialPage ?? 1,
303
+ initialPage: initialState.initialPage ?? 1,
289
304
  setCurrentPage: (val) => {
290
305
  set(
291
306
  (state) => state.currentPage === val ? state : { currentPage: val }
@@ -310,9 +325,18 @@ var PDFStore = createZustandContext(
310
325
  );
311
326
  },
312
327
  pageProxies: initialState.pageProxies,
328
+ pagesLoaded: initialState.pageProxies.length === initialState.pdfDocumentProxy.numPages,
329
+ loadPdfPageProxies: () => initialState.pageResources ? initialState.pageResources.loadAll() : Promise.resolve(get().pageProxies),
330
+ isPageLoaded: (pageNumber) => Boolean(
331
+ initialState.pageResources?.get(pageNumber) ?? get().pageProxies[pageNumber - 1]
332
+ ),
333
+ loadPdfPageProxy: (pageNumber) => initialState.pageResources ? initialState.pageResources.load(pageNumber) : Promise.resolve().then(() => get().getPdfPageProxy(pageNumber)),
313
334
  getPdfPageProxy: (pageNumber) => {
314
- const proxy = get().pageProxies[pageNumber - 1];
315
- if (!proxy) throw new Error(`Page ${pageNumber} does not exist`);
335
+ const proxy = initialState.pageResources?.get(pageNumber) ?? get().pageProxies[pageNumber - 1];
336
+ if (!proxy)
337
+ throw new Error(
338
+ initialState.pageResources && Number.isInteger(pageNumber) && pageNumber >= 1 && pageNumber <= initialState.pdfDocumentProxy.numPages ? `Page ${pageNumber} is not loaded. Await loadPdfPageProxy(${pageNumber}) first.` : `Page ${pageNumber} does not exist`
339
+ );
316
340
  return proxy;
317
341
  },
318
342
  textContent: [],
@@ -665,23 +689,44 @@ var AnnotationTooltip = ({
665
689
  };
666
690
  var createAnnotationStore = () => createStore((set) => ({
667
691
  annotations: [],
668
- addAnnotation: (annotation) => set((state) => ({
669
- annotations: [...state.annotations, annotation]
670
- })),
671
- updateAnnotation: (id, updates) => set((state) => ({
672
- annotations: state.annotations.map(
673
- (annotation) => annotation.id === id ? {
692
+ addAnnotation: (annotation) => set((state) => {
693
+ const annotations = [...state.annotations, annotation];
694
+ updatePageIndex(state.annotations, annotations, /* @__PURE__ */ new Map(), [
695
+ annotation
696
+ ]);
697
+ return { annotations };
698
+ }),
699
+ updateAnnotation: (id, updates) => set((state) => {
700
+ const changes = /* @__PURE__ */ new Map();
701
+ const keys = Object.keys(updates);
702
+ const annotations = state.annotations.map((annotation) => {
703
+ if (annotation.id !== id || keys.every((key) => Object.is(annotation[key], updates[key])))
704
+ return annotation;
705
+ const updated = changes.get(annotation) ?? {
674
706
  ...annotation,
675
707
  ...updates
676
- } : annotation
677
- )
678
- })),
679
- deleteAnnotation: (id) => set((state) => ({
680
- annotations: state.annotations.filter(
681
- (annotation) => annotation.id !== id
682
- )
683
- })),
684
- setAnnotations: (annotations) => set({ annotations })
708
+ };
709
+ changes.set(annotation, updated);
710
+ return updated;
711
+ });
712
+ if (!changes.size) return state;
713
+ updatePageIndex(state.annotations, annotations, changes);
714
+ return { annotations };
715
+ }),
716
+ deleteAnnotation: (id) => set((state) => {
717
+ const changes = /* @__PURE__ */ new Map();
718
+ const annotations = state.annotations.filter((annotation) => {
719
+ if (annotation.id !== id) return true;
720
+ changes.set(annotation, null);
721
+ return false;
722
+ });
723
+ if (!changes.size) return state;
724
+ updatePageIndex(state.annotations, annotations, changes);
725
+ return { annotations };
726
+ }),
727
+ setAnnotations: (annotations) => set(
728
+ (state) => state.annotations === annotations ? state : { annotations }
729
+ )
685
730
  }));
686
731
  var AnnotationsStore = createZustandContext(
687
732
  () => createAnnotationStore()
@@ -713,9 +758,7 @@ function annotationsByPage(annotations) {
713
758
  if (index) return index;
714
759
  index = /* @__PURE__ */ new Map();
715
760
  for (const annotation of annotations) {
716
- const pages = /* @__PURE__ */ new Set([annotation.pageNumber]);
717
- for (const rect of annotation.highlights) pages.add(rect.pageNumber);
718
- for (const rect of annotation.underlines ?? []) pages.add(rect.pageNumber);
761
+ const pages = annotationPages(annotation);
719
762
  for (const page of pages) {
720
763
  let values = index.get(page);
721
764
  if (!values) {
@@ -728,6 +771,45 @@ function annotationsByPage(annotations) {
728
771
  indexes.set(annotations, index);
729
772
  return index;
730
773
  }
774
+ function annotationPages(annotation) {
775
+ const pages = /* @__PURE__ */ new Set([annotation.pageNumber]);
776
+ for (const rect of annotation.highlights) pages.add(rect.pageNumber);
777
+ for (const rect of annotation.underlines ?? []) pages.add(rect.pageNumber);
778
+ return pages;
779
+ }
780
+ function isOnPage(annotation, page) {
781
+ return annotation.pageNumber === page || annotation.highlights.some((rect) => rect.pageNumber === page) || annotation.underlines?.some((rect) => rect.pageNumber === page);
782
+ }
783
+ function updatePageIndex(previous, next, changes, appended = []) {
784
+ const previousIndex = indexes.get(previous);
785
+ if (!previousIndex) return;
786
+ const index = new Map(previousIndex);
787
+ const affected = /* @__PURE__ */ new Set();
788
+ const movedTo = /* @__PURE__ */ new Set();
789
+ for (const [before, after] of changes) {
790
+ const oldPages = annotationPages(before);
791
+ for (const page of oldPages) affected.add(page);
792
+ if (after)
793
+ for (const page of annotationPages(after)) {
794
+ affected.add(page);
795
+ if (!oldPages.has(page)) movedTo.add(page);
796
+ }
797
+ }
798
+ for (const page of affected) {
799
+ const values = movedTo.has(page) ? next.filter((annotation) => isOnPage(annotation, page)) : (previousIndex.get(page) ?? []).flatMap((annotation) => {
800
+ if (!changes.has(annotation)) return [annotation];
801
+ const updated = changes.get(annotation);
802
+ return updated && isOnPage(updated, page) ? [updated] : [];
803
+ });
804
+ if (values.length) index.set(page, values);
805
+ else index.delete(page);
806
+ }
807
+ for (const annotation of appended)
808
+ for (const page of annotationPages(annotation)) {
809
+ index.set(page, [...index.get(page) ?? [], annotation]);
810
+ }
811
+ indexes.set(next, index);
812
+ }
731
813
  function usePageAnnotations(pageNumber) {
732
814
  return useAnnotations(
733
815
  useShallow(
@@ -1035,7 +1117,7 @@ var usePdfJump = () => {
1035
1117
  }
1036
1118
  let scrollOffset;
1037
1119
  if (align === "center") {
1038
- const viewportHeight = virtualizer.scrollElement?.clientHeight || 0;
1120
+ const viewportHeight = virtualizer.scrollRect?.height ?? 0;
1039
1121
  const rectCenter = pageStart + rectTop + rectHeight / 2;
1040
1122
  scrollOffset = rectCenter - viewportHeight / 2;
1041
1123
  } else {
@@ -2108,6 +2190,69 @@ function applyContextRecolor(ctx, map, options) {
2108
2190
  function removeContextRecolor(ctx) {
2109
2191
  ctx[RECOLOR_CLEANUP]?.();
2110
2192
  }
2193
+
2194
+ // src/lib/viewport-invalidation.ts
2195
+ var viewportInvalidationRegistry = /* @__PURE__ */ new WeakMap();
2196
+ var notifyCallbacks = (entry) => {
2197
+ entry.rafId = null;
2198
+ entry.callbacks.forEach((callback) => {
2199
+ callback();
2200
+ });
2201
+ };
2202
+ var scheduleInvalidation = (entry) => {
2203
+ if (entry.rafId !== null) {
2204
+ return;
2205
+ }
2206
+ entry.rafId = requestAnimationFrame(() => {
2207
+ notifyCallbacks(entry);
2208
+ });
2209
+ };
2210
+ var subscribeToViewportInvalidation = (viewport, callback) => {
2211
+ let entry = viewportInvalidationRegistry.get(viewport);
2212
+ if (!entry) {
2213
+ const callbacks = /* @__PURE__ */ new Set();
2214
+ const schedule = () => {
2215
+ const currentEntry = viewportInvalidationRegistry.get(viewport);
2216
+ if (!currentEntry) {
2217
+ return;
2218
+ }
2219
+ scheduleInvalidation(currentEntry);
2220
+ };
2221
+ entry = {
2222
+ callbacks,
2223
+ scrollListener: schedule,
2224
+ windowResizeListener: schedule,
2225
+ resizeObserver: typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(schedule),
2226
+ rafId: null
2227
+ };
2228
+ viewport.addEventListener("scroll", entry.scrollListener, {
2229
+ passive: true
2230
+ });
2231
+ window.addEventListener("resize", entry.windowResizeListener, {
2232
+ passive: true
2233
+ });
2234
+ entry.resizeObserver?.observe(viewport);
2235
+ viewportInvalidationRegistry.set(viewport, entry);
2236
+ }
2237
+ entry.callbacks.add(callback);
2238
+ return () => {
2239
+ const currentEntry = viewportInvalidationRegistry.get(viewport);
2240
+ if (!currentEntry) {
2241
+ return;
2242
+ }
2243
+ currentEntry.callbacks.delete(callback);
2244
+ if (currentEntry.callbacks.size > 0) {
2245
+ return;
2246
+ }
2247
+ if (currentEntry.rafId !== null) {
2248
+ cancelAnimationFrame(currentEntry.rafId);
2249
+ }
2250
+ viewport.removeEventListener("scroll", currentEntry.scrollListener);
2251
+ window.removeEventListener("resize", currentEntry.windowResizeListener);
2252
+ currentEntry.resizeObserver?.disconnect();
2253
+ viewportInvalidationRegistry.delete(viewport);
2254
+ };
2255
+ };
2111
2256
  var listeners = /* @__PURE__ */ new Set();
2112
2257
  var media;
2113
2258
  var getSnapshot = () => Math.min(window.devicePixelRatio || 1, 3);
@@ -2369,7 +2514,53 @@ var useCanvasLayer = ({ background }) => {
2369
2514
  releaseBuffer();
2370
2515
  throw error;
2371
2516
  }
2517
+ let continuation = null;
2518
+ let idleTimer = null;
2519
+ let unsubscribeViewport = null;
2520
+ let priorityStopped = false;
2521
+ const clearPriorityWait = () => {
2522
+ if (idleTimer !== null) clearTimeout(idleTimer);
2523
+ idleTimer = null;
2524
+ unsubscribeViewport?.();
2525
+ unsubscribeViewport = null;
2526
+ };
2527
+ const resumeWhenVisible = () => {
2528
+ if (priorityStopped || !continuation) return;
2529
+ const { virtualizer, viewportRef } = store.getState();
2530
+ const item = virtualizer?.getVirtualItems().find((candidate) => candidate.index === pageNumber - 1);
2531
+ const offset3 = virtualizer?.scrollOffset;
2532
+ const height = virtualizer?.scrollRect?.height;
2533
+ if (virtualizer?.isScrolling && item && offset3 != null && height && (item.end < offset3 || item.start > offset3 + height)) {
2534
+ if (!unsubscribeViewport && viewportRef.current) {
2535
+ unsubscribeViewport = subscribeToViewportInvalidation(
2536
+ viewportRef.current,
2537
+ resumeWhenVisible
2538
+ );
2539
+ }
2540
+ if (idleTimer === null) {
2541
+ idleTimer = setTimeout(() => {
2542
+ idleTimer = null;
2543
+ resumeWhenVisible();
2544
+ }, 160);
2545
+ }
2546
+ return;
2547
+ }
2548
+ clearPriorityWait();
2549
+ const proceed = continuation;
2550
+ continuation = null;
2551
+ proceed();
2552
+ };
2553
+ const stopPriority = () => {
2554
+ priorityStopped = true;
2555
+ continuation = null;
2556
+ clearPriorityWait();
2557
+ };
2558
+ renderingTask.onContinue = (proceed) => {
2559
+ continuation = proceed;
2560
+ resumeWhenVisible();
2561
+ };
2372
2562
  renderingTask.promise.then(() => {
2563
+ stopPriority();
2373
2564
  if (cancelled) {
2374
2565
  restoreRecolor?.();
2375
2566
  releaseBuffer();
@@ -2414,6 +2605,7 @@ var useCanvasLayer = ({ background }) => {
2414
2605
  releaseBuffer();
2415
2606
  }
2416
2607
  }).catch((error) => {
2608
+ stopPriority();
2417
2609
  restoreRecolor?.();
2418
2610
  releaseBuffer();
2419
2611
  if (cancelled) return;
@@ -2423,6 +2615,7 @@ var useCanvasLayer = ({ background }) => {
2423
2615
  });
2424
2616
  return () => {
2425
2617
  cancelled = true;
2618
+ stopPriority();
2426
2619
  void renderingTask.cancel();
2427
2620
  };
2428
2621
  }, [
@@ -2454,71 +2647,6 @@ var useCanvasLayer = ({ background }) => {
2454
2647
  canvasRef
2455
2648
  };
2456
2649
  };
2457
-
2458
- // src/lib/viewport-invalidation.ts
2459
- var viewportInvalidationRegistry = /* @__PURE__ */ new WeakMap();
2460
- var notifyCallbacks = (entry) => {
2461
- entry.rafId = null;
2462
- entry.callbacks.forEach((callback) => {
2463
- callback();
2464
- });
2465
- };
2466
- var scheduleInvalidation = (entry) => {
2467
- if (entry.rafId !== null) {
2468
- return;
2469
- }
2470
- entry.rafId = requestAnimationFrame(() => {
2471
- notifyCallbacks(entry);
2472
- });
2473
- };
2474
- var subscribeToViewportInvalidation = (viewport, callback) => {
2475
- let entry = viewportInvalidationRegistry.get(viewport);
2476
- if (!entry) {
2477
- const callbacks = /* @__PURE__ */ new Set();
2478
- const schedule = () => {
2479
- const currentEntry = viewportInvalidationRegistry.get(viewport);
2480
- if (!currentEntry) {
2481
- return;
2482
- }
2483
- scheduleInvalidation(currentEntry);
2484
- };
2485
- entry = {
2486
- callbacks,
2487
- scrollListener: schedule,
2488
- windowResizeListener: schedule,
2489
- resizeObserver: typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(schedule),
2490
- rafId: null
2491
- };
2492
- viewport.addEventListener("scroll", entry.scrollListener, {
2493
- passive: true
2494
- });
2495
- window.addEventListener("resize", entry.windowResizeListener, {
2496
- passive: true
2497
- });
2498
- entry.resizeObserver?.observe(viewport);
2499
- viewportInvalidationRegistry.set(viewport, entry);
2500
- }
2501
- entry.callbacks.add(callback);
2502
- return () => {
2503
- const currentEntry = viewportInvalidationRegistry.get(viewport);
2504
- if (!currentEntry) {
2505
- return;
2506
- }
2507
- currentEntry.callbacks.delete(callback);
2508
- if (currentEntry.callbacks.size > 0) {
2509
- return;
2510
- }
2511
- if (currentEntry.rafId !== null) {
2512
- cancelAnimationFrame(currentEntry.rafId);
2513
- }
2514
- viewport.removeEventListener("scroll", currentEntry.scrollListener);
2515
- window.removeEventListener("resize", currentEntry.windowResizeListener);
2516
- currentEntry.resizeObserver?.disconnect();
2517
- viewportInvalidationRegistry.delete(viewport);
2518
- };
2519
- };
2520
-
2521
- // src/hooks/layers/useDetailCanvasLayer.tsx
2522
2650
  var DETAIL_RENDER_IDLE_MS = 80;
2523
2651
  var SCROLL_POLL_MS = 160;
2524
2652
  var ZOOM_DEBOUNCE_MS = 200;
@@ -4311,6 +4439,37 @@ var Outline = ({
4311
4439
  });
4312
4440
  }) });
4313
4441
  };
4442
+ function PageLoadingBoundary({
4443
+ pageNumber,
4444
+ children,
4445
+ fallback = null
4446
+ }) {
4447
+ const loaded = usePdf((state) => state.isPageLoaded(pageNumber));
4448
+ const loadPage = usePdf((state) => state.loadPdfPageProxy);
4449
+ const [failure, setFailure] = useState(null);
4450
+ const [attempt, setAttempt] = useState(0);
4451
+ useEffect(() => {
4452
+ if (loaded) return;
4453
+ let disposed = false;
4454
+ setFailure(null);
4455
+ void loadPage(pageNumber).catch((error) => {
4456
+ if (!disposed) setFailure({ pageNumber, error });
4457
+ });
4458
+ return () => {
4459
+ disposed = true;
4460
+ };
4461
+ }, [pageNumber, loaded, loadPage, attempt]);
4462
+ if (loaded) return children;
4463
+ if (failure?.pageNumber === pageNumber)
4464
+ return /* @__PURE__ */ jsxs("div", { role: "alert", children: [
4465
+ "Unable to load page ",
4466
+ pageNumber,
4467
+ ".",
4468
+ " ",
4469
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setAttempt((value) => value + 1), children: "Retry" })
4470
+ ] });
4471
+ return fallback;
4472
+ }
4314
4473
  var Page = memo(
4315
4474
  ({
4316
4475
  children,
@@ -4318,14 +4477,16 @@ var Page = memo(
4318
4477
  style,
4319
4478
  ...props
4320
4479
  }) => {
4321
- const pdfPageProxy = usePdf((state) => state.getPdfPageProxy(pageNumber));
4480
+ const loaded = usePdf((state) => state.isPageLoaded(pageNumber));
4481
+ const getPage = usePdf((state) => state.getPdfPageProxy);
4482
+ const proxy = loaded ? getPage(pageNumber) : void 0;
4322
4483
  const viewport = usePdf((state) => state.viewports[pageNumber - 1]);
4323
4484
  const colorScheme = usePdf((state) => state.colorScheme);
4324
4485
  const darkModeColors = usePdf((state) => state.darkModeColors);
4325
4486
  const pageBackground = colorScheme === "dark" ? createDarkModeColorMap(darkModeColors)("#ffffff") : "white";
4326
- const width = viewport?.width ?? (pdfPageProxy.view[2] ?? 0) - (pdfPageProxy.view[0] ?? 0);
4327
- const height = viewport?.height ?? (pdfPageProxy.view[3] ?? 0) - (pdfPageProxy.view[1] ?? 0);
4328
- return /* @__PURE__ */ jsx(PDFPageNumberContext.Provider, { value: pdfPageProxy.pageNumber, children: /* @__PURE__ */ jsx(
4487
+ const width = viewport?.width ?? (proxy?.view[2] ?? 0) - (proxy?.view[0] ?? 0);
4488
+ const height = viewport?.height ?? (proxy?.view[3] ?? 0) - (proxy?.view[1] ?? 0);
4489
+ return /* @__PURE__ */ jsx(PDFPageNumberContext.Provider, { value: pageNumber, children: /* @__PURE__ */ jsx(
4329
4490
  Primitive.div,
4330
4491
  {
4331
4492
  style: {
@@ -4343,8 +4504,9 @@ var Page = memo(
4343
4504
  height,
4344
4505
  ...style
4345
4506
  },
4507
+ "aria-busy": !loaded || void 0,
4346
4508
  ...props,
4347
- children
4509
+ children: /* @__PURE__ */ jsx(PageLoadingBoundary, { pageNumber, children })
4348
4510
  }
4349
4511
  )
4350
4512
  }
@@ -4465,6 +4627,28 @@ var addEventListenerOptions = {
4465
4627
  };
4466
4628
  var useObserveElement = () => {
4467
4629
  const store = PDFStore.useContext();
4630
+ const observeElementRect$1 = (instance, cb) => {
4631
+ let physicalRect;
4632
+ const publish = () => {
4633
+ if (!physicalRect) return;
4634
+ const zoom = store.getState().zoom;
4635
+ cb({
4636
+ width: physicalRect.width / zoom,
4637
+ height: physicalRect.height / zoom
4638
+ });
4639
+ };
4640
+ const stopObserving = observeElementRect(instance, (rect) => {
4641
+ physicalRect = rect;
4642
+ publish();
4643
+ });
4644
+ const unsubscribe = store.subscribe((state, previous) => {
4645
+ if (state.zoom !== previous.zoom) publish();
4646
+ });
4647
+ return () => {
4648
+ stopObserving?.();
4649
+ unsubscribe();
4650
+ };
4651
+ };
4468
4652
  const observeElementOffset = (instance, cb) => {
4469
4653
  const element = instance.scrollElement;
4470
4654
  if (!element) {
@@ -4474,33 +4658,46 @@ var useObserveElement = () => {
4474
4658
  if (!targetWindow) {
4475
4659
  return;
4476
4660
  }
4477
- let offset3 = 0;
4478
- const fallback = instance.options.useScrollendEvent && supportsScrollend ? () => void 0 : debounce(
4479
- targetWindow,
4480
- () => {
4481
- cb(offset3, false);
4482
- },
4483
- instance.options.isScrollingResetDelay
4484
- );
4485
- const createHandler = (isScrolling) => () => {
4661
+ let isScrolling = false;
4662
+ let idleTimer;
4663
+ const needsIdleFallback = !instance.options.useScrollendEvent || !supportsScrollend;
4664
+ const publish = (scrolling) => {
4665
+ isScrolling = scrolling;
4486
4666
  const { horizontal, isRtl } = instance.options;
4487
- offset3 = horizontal ? element.scrollLeft * (isRtl && -1 || 1) : element.scrollTop;
4488
- const zoom = store.getState().zoom;
4489
- offset3 = offset3 / zoom;
4490
- fallback();
4491
- cb(offset3, isScrolling);
4667
+ const physicalOffset = horizontal ? element.scrollLeft * (isRtl ? -1 : 1) : element.scrollTop;
4668
+ cb(physicalOffset / store.getState().zoom, isScrolling);
4669
+ };
4670
+ const clearIdleTimer = () => {
4671
+ if (idleTimer !== void 0) targetWindow.clearTimeout(idleTimer);
4672
+ idleTimer = void 0;
4673
+ };
4674
+ const createHandler = (scrolling) => () => {
4675
+ clearIdleTimer();
4676
+ publish(scrolling);
4677
+ if (scrolling && needsIdleFallback) {
4678
+ idleTimer = targetWindow.setTimeout(() => {
4679
+ idleTimer = void 0;
4680
+ publish(false);
4681
+ }, instance.options.isScrollingResetDelay);
4682
+ }
4492
4683
  };
4493
4684
  const handler = createHandler(true);
4494
4685
  const endHandler = createHandler(false);
4495
4686
  endHandler();
4687
+ const unsubscribe = store.subscribe((state, previous) => {
4688
+ if (state.zoom !== previous.zoom) publish(isScrolling);
4689
+ });
4496
4690
  element.addEventListener("scroll", handler, addEventListenerOptions);
4497
4691
  element.addEventListener("scrollend", endHandler, addEventListenerOptions);
4498
4692
  return () => {
4693
+ unsubscribe();
4694
+ clearIdleTimer();
4499
4695
  element.removeEventListener("scroll", handler);
4500
4696
  element.removeEventListener("scrollend", endHandler);
4501
4697
  };
4502
4698
  };
4503
4699
  return {
4700
+ observeElementRect: observeElementRect$1,
4504
4701
  observeElementOffset
4505
4702
  };
4506
4703
  };
@@ -4510,7 +4707,11 @@ var useScrollFn = () => {
4510
4707
  (_offset, canSmooth, instance) => {
4511
4708
  const zoom = store.getState().zoom;
4512
4709
  const offset3 = _offset * zoom;
4513
- elementScroll(offset3, canSmooth, instance);
4710
+ elementScroll(
4711
+ offset3,
4712
+ { ...canSmooth, adjustments: (canSmooth.adjustments ?? 0) * zoom },
4713
+ instance
4714
+ );
4514
4715
  },
4515
4716
  [store]
4516
4717
  );
@@ -4628,7 +4829,7 @@ function _toPrimitive(input, hint) {
4628
4829
  if (typeof input !== "object" || input === null) return input;
4629
4830
  var prim = input[Symbol.toPrimitive];
4630
4831
  if (prim !== void 0) {
4631
- var res = prim.call(input, hint || "default");
4832
+ var res = prim.call(input, hint);
4632
4833
  if (typeof res !== "object") return res;
4633
4834
  throw new TypeError("@@toPrimitive must return a primitive value.");
4634
4835
  }
@@ -6485,6 +6686,8 @@ var useViewportContainer = ({
6485
6686
  });
6486
6687
  const zoomRafRef = useRef(null);
6487
6688
  const lastPushedZoomRef = useRef(null);
6689
+ const initializedZoomRef = useRef(false);
6690
+ const appliedZoomRef = useRef(1);
6488
6691
  const updateTransform = useCallback(
6489
6692
  (zoomUpdate) => {
6490
6693
  if (!elementRef.current || !containerRef.current || !elementWrapperRef.current) {
@@ -6514,17 +6717,36 @@ var useViewportContainer = ({
6514
6717
  elementWrapperRef.current.style.height = `${naturalHeight * zoom2}px`;
6515
6718
  containerRef.current.scrollTop = translateY;
6516
6719
  containerRef.current.scrollLeft = translateX;
6517
- if (zoomUpdate && zoomRafRef.current === null) {
6518
- zoomRafRef.current = requestAnimationFrame(() => {
6519
- zoomRafRef.current = null;
6520
- const z = transformations.current.zoom;
6521
- lastPushedZoomRef.current = z;
6522
- updateZoom(() => z);
6523
- });
6720
+ appliedZoomRef.current = zoom2;
6721
+ if (zoomUpdate) {
6722
+ lastPushedZoomRef.current = zoom2;
6723
+ updateZoom(() => zoom2);
6524
6724
  }
6525
6725
  },
6526
6726
  [containerRef, elementRef, elementWrapperRef, updateZoom, store]
6527
6727
  );
6728
+ useLayoutEffect(() => {
6729
+ const viewport = containerRef.current;
6730
+ if (!viewport) return;
6731
+ return registerViewportZoom(viewport, (nextZoom) => {
6732
+ if (initializedZoomRef.current && appliedZoomRef.current === nextZoom && zoomRafRef.current === null)
6733
+ return;
6734
+ if (zoomRafRef.current !== null) {
6735
+ cancelAnimationFrame(zoomRafRef.current);
6736
+ zoomRafRef.current = null;
6737
+ }
6738
+ lastPushedZoomRef.current = null;
6739
+ const previousZoom = initializedZoomRef.current ? appliedZoomRef.current : store.getState().zoom;
6740
+ const ratio = previousZoom > 0 && Number.isFinite(previousZoom) ? nextZoom / previousZoom : 1;
6741
+ transformations.current = {
6742
+ zoom: nextZoom,
6743
+ translateX: viewport.scrollLeft * ratio,
6744
+ translateY: viewport.scrollTop * ratio
6745
+ };
6746
+ initializedZoomRef.current = true;
6747
+ updateTransform();
6748
+ });
6749
+ }, [containerRef, store, updateTransform]);
6528
6750
  useEffect(() => {
6529
6751
  if (USE_LAYOUT_ZOOM && gestureTransformAppliedRef.current && !isPinching)
6530
6752
  updateTransform();
@@ -6537,6 +6759,16 @@ var useViewportContainer = ({
6537
6759
  };
6538
6760
  }, []);
6539
6761
  useEffect(() => {
6762
+ if (!initializedZoomRef.current && containerRef.current) {
6763
+ initializedZoomRef.current = true;
6764
+ transformations.current = {
6765
+ zoom,
6766
+ translateX: containerRef.current.scrollLeft,
6767
+ translateY: containerRef.current.scrollTop
6768
+ };
6769
+ updateTransform();
6770
+ return;
6771
+ }
6540
6772
  if (transformations.current.zoom === zoom || !containerRef.current) {
6541
6773
  return;
6542
6774
  }
@@ -6549,7 +6781,7 @@ var useViewportContainer = ({
6549
6781
  zoomRafRef.current = null;
6550
6782
  }
6551
6783
  lastPushedZoomRef.current = null;
6552
- const prevZoom = transformations.current.zoom;
6784
+ const prevZoom = appliedZoomRef.current;
6553
6785
  if (!prevZoom || !Number.isFinite(prevZoom)) {
6554
6786
  transformations.current = {
6555
6787
  translateX: containerRef.current.scrollLeft,
@@ -6689,11 +6921,26 @@ var useViewportContainer = ({
6689
6921
  translateY: newTranslateY
6690
6922
  };
6691
6923
  newMemo.lastZoom = newZoom;
6692
- updateTransform(true);
6924
+ if (zoomRafRef.current === null) {
6925
+ zoomRafRef.current = requestAnimationFrame(() => {
6926
+ zoomRafRef.current = null;
6927
+ updateTransform(true);
6928
+ });
6929
+ }
6693
6930
  return newMemo;
6694
6931
  },
6695
6932
  onPinchStart: () => setIsPinching(true),
6696
- onPinchEnd: () => setIsPinching(false)
6933
+ onPinchEnd: () => {
6934
+ const pendingFrame = zoomRafRef.current !== null;
6935
+ if (zoomRafRef.current !== null) {
6936
+ cancelAnimationFrame(zoomRafRef.current);
6937
+ zoomRafRef.current = null;
6938
+ }
6939
+ setIsPinching(false);
6940
+ if (pendingFrame || gestureTransformAppliedRef.current) {
6941
+ updateTransform(pendingFrame);
6942
+ }
6943
+ }
6697
6944
  },
6698
6945
  {
6699
6946
  target: containerRef
@@ -6746,6 +6993,7 @@ var Pages = ({
6746
6993
  const [tempItems, setTempItems] = useState([]);
6747
6994
  const viewports = usePdf((state) => state.viewports);
6748
6995
  const numPages = usePdf((state) => state.pdfDocumentProxy.numPages);
6996
+ const initialPage = usePdf((state) => state.initialPage);
6749
6997
  const isPinching = usePdf((state) => state.isPinching);
6750
6998
  const elementWrapperRef = useRef(null);
6751
6999
  const elementRef = useRef(null);
@@ -6757,7 +7005,7 @@ var Pages = ({
6757
7005
  });
6758
7006
  const setVirtualizer = usePdf((state) => state.setVirtualizer);
6759
7007
  const { scrollToFn } = useScrollFn();
6760
- const { observeElementOffset } = useObserveElement();
7008
+ const { observeElementOffset, observeElementRect } = useObserveElement();
6761
7009
  const viewportsRef = useRef(viewports);
6762
7010
  viewportsRef.current = viewports;
6763
7011
  const estimateSize = useCallback(
@@ -6769,15 +7017,22 @@ var Pages = ({
6769
7017
  []
6770
7018
  // Stable — reads from ref
6771
7019
  );
7020
+ const [startingOffset] = useState(
7021
+ () => initialOffset ?? viewports.slice(0, initialPage - 1).reduce(
7022
+ (offset3, viewport) => offset3 + viewport.height + EXTRA_HEIGHT + gap,
7023
+ 0
7024
+ )
7025
+ );
6772
7026
  const virtualizer = useVirtualizer({
6773
7027
  count: numPages || 0,
6774
7028
  getScrollElement: () => containerRef.current,
6775
7029
  estimateSize,
6776
7030
  observeElementOffset,
7031
+ observeElementRect,
6777
7032
  overscan: virtualizerOptions?.overscan ?? 0,
6778
7033
  scrollToFn,
6779
7034
  gap,
6780
- initialOffset,
7035
+ initialOffset: startingOffset,
6781
7036
  // Never trust scrollend alone to reset isScrolling: a single missed
6782
7037
  // event (a documented browser flake — TanStack flipped this default to
6783
7038
  // false in later 3.x) would pin isScrolling=true forever, and every
@@ -6785,6 +7040,21 @@ var Pages = ({
6785
7040
  // false keeps the isScrollingResetDelay debounce active as a fallback.
6786
7041
  useScrollendEvent: false
6787
7042
  });
7043
+ const previousViewports = useRef(viewports);
7044
+ useLayoutEffect(() => {
7045
+ if (isPinching) return;
7046
+ const previous = previousViewports.current;
7047
+ previousViewports.current = viewports;
7048
+ if (previous === viewports) return;
7049
+ let resized = false;
7050
+ for (let index = 0; index < viewports.length; index++) {
7051
+ if (previous[index]?.height !== viewports[index]?.height) {
7052
+ virtualizer.resizeItem(index, viewports[index].height + EXTRA_HEIGHT);
7053
+ resized = true;
7054
+ }
7055
+ }
7056
+ if (resized) setTempItems([]);
7057
+ }, [viewports, virtualizer, isPinching]);
6788
7058
  useEffect(() => {
6789
7059
  if (onOffsetChange && virtualizer.scrollOffset)
6790
7060
  onOffsetChange(virtualizer.scrollOffset);
@@ -6931,6 +7201,164 @@ async function mapConcurrent(items, concurrency, map, isCancelled = () => false)
6931
7201
  return results;
6932
7202
  }
6933
7203
 
7204
+ // src/lib/page-resources.ts
7205
+ var PageResources = class {
7206
+ constructor(document2, firstPage, rotation, onError, concurrency = 16) {
7207
+ this.document = document2;
7208
+ this.rotation = rotation;
7209
+ this.onError = onError;
7210
+ this.concurrency = concurrency;
7211
+ this.pages.set(firstPage.pageNumber, firstPage);
7212
+ const viewport = this.viewportFor(firstPage);
7213
+ this.viewports = Array.from({ length: document2.numPages }, () => viewport);
7214
+ if (document2.numPages === 1) this.completePages = [firstPage];
7215
+ }
7216
+ pages = /* @__PURE__ */ new Map();
7217
+ requests = /* @__PURE__ */ new Map();
7218
+ queue = [];
7219
+ active = 0;
7220
+ disposed = false;
7221
+ listeners = /* @__PURE__ */ new Set();
7222
+ notification;
7223
+ viewports;
7224
+ completePages;
7225
+ viewportUpdates = /* @__PURE__ */ new Map();
7226
+ viewportFor(page) {
7227
+ return page.getViewport({
7228
+ scale: 1,
7229
+ rotation: this.rotation + (page.rotate || 0)
7230
+ });
7231
+ }
7232
+ get(pageNumber) {
7233
+ return this.pages.get(pageNumber);
7234
+ }
7235
+ getSnapshot = () => ({
7236
+ viewports: this.viewports,
7237
+ pageProxies: this.completePages
7238
+ });
7239
+ subscribe = (listener) => {
7240
+ this.listeners.add(listener);
7241
+ return () => {
7242
+ this.listeners.delete(listener);
7243
+ };
7244
+ };
7245
+ /** Visible pages move ahead of the background queue; in-flight requests are shared. */
7246
+ load = (pageNumber) => {
7247
+ if (this.disposed)
7248
+ return Promise.reject(new Error("Document page loading was cancelled"));
7249
+ if (!Number.isInteger(pageNumber) || pageNumber < 1 || pageNumber > this.document.numPages)
7250
+ return Promise.reject(
7251
+ new RangeError(`Page ${pageNumber} does not exist`)
7252
+ );
7253
+ const page = this.pages.get(pageNumber);
7254
+ if (page) return Promise.resolve(page);
7255
+ const existing = this.requests.get(pageNumber);
7256
+ if (existing) {
7257
+ const queued = this.queue.indexOf(pageNumber);
7258
+ if (queued > 0) {
7259
+ this.queue.splice(queued, 1);
7260
+ this.queue.unshift(pageNumber);
7261
+ }
7262
+ return existing.promise;
7263
+ }
7264
+ const request = this.createRequest(pageNumber);
7265
+ this.queue.unshift(pageNumber);
7266
+ this.pump();
7267
+ return request.promise;
7268
+ };
7269
+ createRequest(pageNumber) {
7270
+ let resolve;
7271
+ let reject;
7272
+ const promise = new Promise((res, rej) => {
7273
+ resolve = res;
7274
+ reject = rej;
7275
+ });
7276
+ const request = { promise, resolve, reject };
7277
+ this.requests.set(pageNumber, request);
7278
+ return request;
7279
+ }
7280
+ /** Start after the reader mounts, so unrelated page metadata cannot delay its first render. */
7281
+ start = () => {
7282
+ if (this.disposed) return;
7283
+ for (let pageNumber = 1; pageNumber <= this.document.numPages; pageNumber++) {
7284
+ if (this.pages.has(pageNumber) || this.requests.has(pageNumber)) continue;
7285
+ const request = this.createRequest(pageNumber);
7286
+ void request.promise.catch(() => {
7287
+ });
7288
+ this.queue.push(pageNumber);
7289
+ }
7290
+ this.pump();
7291
+ };
7292
+ loadAll = async () => {
7293
+ if (this.disposed) throw new Error("Document page loading was cancelled");
7294
+ if (this.completePages) return this.completePages;
7295
+ this.start();
7296
+ await Promise.all(
7297
+ Array.from({ length: this.document.numPages }, (_, index) => {
7298
+ const pageNumber = index + 1;
7299
+ return this.pages.get(pageNumber) ?? this.requests.get(pageNumber).promise;
7300
+ })
7301
+ );
7302
+ return this.completePages;
7303
+ };
7304
+ pump() {
7305
+ while (!this.disposed && this.active < this.concurrency && this.queue.length) {
7306
+ const pageNumber = this.queue.shift();
7307
+ const request = this.requests.get(pageNumber);
7308
+ this.active++;
7309
+ void Promise.resolve().then(() => {
7310
+ if (this.disposed)
7311
+ throw new Error("Document page loading was cancelled");
7312
+ return this.document.getPage(pageNumber);
7313
+ }).then((page) => {
7314
+ if (this.disposed) return;
7315
+ const viewport = this.viewportFor(page);
7316
+ this.pages.set(pageNumber, page);
7317
+ this.viewportUpdates.set(pageNumber - 1, viewport);
7318
+ if (this.pages.size === this.document.numPages) {
7319
+ this.completePages = Array.from(
7320
+ { length: this.document.numPages },
7321
+ (_, index) => this.pages.get(index + 1)
7322
+ );
7323
+ }
7324
+ this.requests.delete(pageNumber);
7325
+ request.resolve(page);
7326
+ this.notify();
7327
+ }).catch((error) => {
7328
+ if (!this.disposed) {
7329
+ this.requests.delete(pageNumber);
7330
+ request.reject(error);
7331
+ this.onError(error);
7332
+ }
7333
+ }).finally(() => {
7334
+ this.active--;
7335
+ this.pump();
7336
+ });
7337
+ }
7338
+ }
7339
+ notify() {
7340
+ if (this.notification !== void 0) return;
7341
+ this.notification = setTimeout(() => {
7342
+ this.notification = void 0;
7343
+ if (this.disposed) return;
7344
+ this.viewports = this.viewports.slice();
7345
+ for (const [index, viewport] of this.viewportUpdates)
7346
+ this.viewports[index] = viewport;
7347
+ this.viewportUpdates.clear();
7348
+ for (const listener of this.listeners) listener();
7349
+ }, 0);
7350
+ }
7351
+ dispose = () => {
7352
+ this.disposed = true;
7353
+ clearTimeout(this.notification);
7354
+ this.listeners.clear();
7355
+ this.queue = [];
7356
+ for (const request of this.requests.values())
7357
+ request.reject(new Error("Document page loading was cancelled"));
7358
+ this.requests.clear();
7359
+ };
7360
+ };
7361
+
6934
7362
  // src/lib/pdfjs.ts
6935
7363
  var pdfJsPromise = null;
6936
7364
  var loadPdfJs = () => {
@@ -7056,6 +7484,8 @@ var usePDFDocumentContext = ({
7056
7484
  onError,
7057
7485
  source,
7058
7486
  initialRotation = 0,
7487
+ progressive = false,
7488
+ initialPage = 1,
7059
7489
  isZoomFitWidth,
7060
7490
  zoom = 1,
7061
7491
  zoomOptions,
@@ -7078,7 +7508,32 @@ var usePDFDocumentContext = ({
7078
7508
  darkModeColorsRef.current = darkModeColors;
7079
7509
  useEffect(() => {
7080
7510
  let isDisposed = false;
7511
+ let pageResources;
7081
7512
  const generateViewports = async (pdf) => {
7513
+ const firstPageNumber = Number.isFinite(initialPage) ? Math.max(1, Math.min(pdf.numPages, Math.trunc(initialPage))) : 1;
7514
+ if (progressive) {
7515
+ const firstPage = await pdf.getPage(firstPageNumber);
7516
+ if (isDisposed) return;
7517
+ pageResources = new PageResources(pdf, firstPage, rotation, (error) => {
7518
+ if (isDisposed) return;
7519
+ console.error("Error generating PDF viewports", error);
7520
+ onErrorRef.current?.({ error, phase: "viewport-generation", source });
7521
+ });
7522
+ setInitialState({
7523
+ isZoomFitWidth,
7524
+ viewports: pageResources.getSnapshot().viewports,
7525
+ pageProxies: pageResources.getSnapshot().pageProxies ?? [],
7526
+ pdfDocumentProxy: pdf,
7527
+ zoom,
7528
+ zoomOptions,
7529
+ pageResources,
7530
+ initialPage: firstPageNumber,
7531
+ colorScheme: colorSchemeRef.current,
7532
+ darkModeColors: darkModeColorsRef.current,
7533
+ renderColorMapRef
7534
+ });
7535
+ return;
7536
+ }
7082
7537
  const pageProxies = [];
7083
7538
  const viewports = await mapConcurrent(
7084
7539
  Array.from({ length: pdf.numPages }, (_, index) => index),
@@ -7104,6 +7559,7 @@ var usePDFDocumentContext = ({
7104
7559
  pdfDocumentProxy: pdf,
7105
7560
  zoom,
7106
7561
  zoomOptions,
7562
+ initialPage: firstPageNumber,
7107
7563
  colorScheme: colorSchemeRef.current,
7108
7564
  darkModeColors: darkModeColorsRef.current,
7109
7565
  renderColorMapRef
@@ -7166,6 +7622,7 @@ var usePDFDocumentContext = ({
7166
7622
  });
7167
7623
  return () => {
7168
7624
  isDisposed = true;
7625
+ pageResources?.dispose();
7169
7626
  void loadingTask?.destroy();
7170
7627
  };
7171
7628
  };
@@ -7207,6 +7664,28 @@ var ColorSchemeSync = ({
7207
7664
  ]);
7208
7665
  return null;
7209
7666
  };
7667
+ var PageResourcesSync = ({ resources }) => {
7668
+ const store = PDFStore.useContext();
7669
+ useEffect(() => {
7670
+ if (!resources) return;
7671
+ const update = () => {
7672
+ const snapshot = resources.getSnapshot();
7673
+ store.setState((state) => ({
7674
+ viewports: snapshot.viewports,
7675
+ pageProxies: snapshot.pageProxies ?? state.pageProxies,
7676
+ pagesLoaded: Boolean(snapshot.pageProxies)
7677
+ }));
7678
+ };
7679
+ const unsubscribe = resources.subscribe(update);
7680
+ update();
7681
+ const timer = setTimeout(resources.start, 0);
7682
+ return () => {
7683
+ clearTimeout(timer);
7684
+ unsubscribe();
7685
+ };
7686
+ }, [resources, store]);
7687
+ return null;
7688
+ };
7210
7689
  var Root = forwardRef(
7211
7690
  ({
7212
7691
  children,
@@ -7218,6 +7697,8 @@ var Root = forwardRef(
7218
7697
  zoom,
7219
7698
  zoomOptions,
7220
7699
  documentOptions,
7700
+ progressive,
7701
+ initialPage,
7221
7702
  colorScheme,
7222
7703
  darkModeColors,
7223
7704
  ...props
@@ -7230,6 +7711,8 @@ var Root = forwardRef(
7230
7711
  zoom,
7231
7712
  zoomOptions,
7232
7713
  documentOptions,
7714
+ progressive,
7715
+ initialPage,
7233
7716
  colorScheme,
7234
7717
  darkModeColors
7235
7718
  });
@@ -7245,6 +7728,7 @@ var Root = forwardRef(
7245
7728
  };
7246
7729
  }, [documentId]);
7247
7730
  return /* @__PURE__ */ jsx(Primitive.div, { ref, ...props, children: initialState ? /* @__PURE__ */ jsxs(PDFStore.Provider, { initialValue: initialState, children: [
7731
+ /* @__PURE__ */ jsx(PageResourcesSync, { resources: initialState.pageResources }),
7248
7732
  /* @__PURE__ */ jsx(
7249
7733
  ColorSchemeSync,
7250
7734
  {
@@ -7339,7 +7823,10 @@ var Search = ({
7339
7823
  }) => {
7340
7824
  const [result, setResult] = useState(null);
7341
7825
  const [attempt, setAttempt] = useState(0);
7826
+ const document2 = usePdf((state) => state.pdfDocumentProxy);
7342
7827
  const proxies = usePdf((state) => state.pageProxies);
7828
+ const pagesLoaded = usePdf((state) => state.pagesLoaded);
7829
+ const loadPages = usePdf((state) => state.loadPdfPageProxies);
7343
7830
  const setTextContent = usePdf((state) => state.setTextContent);
7344
7831
  const retry = useCallback(() => {
7345
7832
  setResult(null);
@@ -7348,22 +7835,27 @@ var Search = ({
7348
7835
  useEffect(() => {
7349
7836
  let disposed = false;
7350
7837
  setResult(null);
7351
- const task = acquireDocumentText(proxies);
7352
- void task.promise.then((text) => {
7353
- if (disposed) return;
7838
+ let task;
7839
+ void loadPages().then((pages) => {
7840
+ if (disposed || !pagesLoaded) return;
7841
+ task = acquireDocumentText(pages);
7842
+ return task.promise;
7843
+ }).then((text) => {
7844
+ if (disposed || !text) return;
7354
7845
  setTextContent(text);
7355
- setResult({ pages: proxies, status: "ready" });
7846
+ setResult({ document: document2, pages: proxies, status: "ready" });
7356
7847
  }).catch((error) => {
7357
7848
  if (disposed) return;
7358
7849
  console.error("Error extracting PDF text", error);
7359
- setResult({ pages: proxies, status: "error", error });
7850
+ setResult({ document: document2, pages: proxies, status: "error", error });
7360
7851
  });
7361
7852
  return () => {
7362
7853
  disposed = true;
7363
- task.release();
7854
+ task?.release();
7364
7855
  };
7365
- }, [proxies, setTextContent, attempt]);
7366
- if (!result || result.pages !== proxies) return loading;
7856
+ }, [document2, proxies, pagesLoaded, loadPages, setTextContent, attempt]);
7857
+ if (!result || result.document !== document2 || result.pages !== proxies)
7858
+ return loading;
7367
7859
  if (result.status === "error") {
7368
7860
  if (errorFallback) return errorFallback({ error: result.error, retry });
7369
7861
  return /* @__PURE__ */ jsxs("div", { role: "alert", children: [
@@ -7463,7 +7955,7 @@ var useThumbnail = (pageNumber, config = {}) => {
7463
7955
  isVisible
7464
7956
  };
7465
7957
  };
7466
- var Thumbnail = ({
7958
+ var LoadedThumbnail = ({
7467
7959
  pageNumber = 1,
7468
7960
  eager = false,
7469
7961
  ...props
@@ -7472,30 +7964,65 @@ var Thumbnail = ({
7472
7964
  isFirstPage: eager || pageNumber < 5
7473
7965
  });
7474
7966
  const { jumpToPage } = usePdfJump();
7967
+ const isCurrentPage = usePdf((state) => state.currentPage === pageNumber);
7968
+ const spacePressed = useRef(false);
7475
7969
  return /* @__PURE__ */ jsx("div", { ref: containerRef, style: { minHeight: "150px", minWidth: "10px" }, children: isVisible && /* @__PURE__ */ jsx(
7476
7970
  Primitive.canvas,
7477
7971
  {
7972
+ "aria-label": `Page ${pageNumber}`,
7973
+ "aria-current": isCurrentPage ? "page" : void 0,
7478
7974
  ...props,
7479
7975
  role: "button",
7480
7976
  tabIndex: 0,
7481
- onClick: (e) => {
7482
- if (props.onClick) {
7483
- props.onClick(e);
7484
- }
7485
- jumpToPage(pageNumber, { behavior: "auto" });
7977
+ onClick: (event) => {
7978
+ props.onClick?.(event);
7979
+ if (!event.defaultPrevented)
7980
+ jumpToPage(pageNumber, { behavior: "auto" });
7486
7981
  },
7487
- onKeyDown: (e) => {
7488
- if (props.onKeyDown) {
7489
- props.onKeyDown(e);
7982
+ onKeyDown: (event) => {
7983
+ props.onKeyDown?.(event);
7984
+ if (event.defaultPrevented) return;
7985
+ if (event.key === "Enter") {
7986
+ event.preventDefault();
7987
+ jumpToPage(pageNumber, { behavior: "auto" });
7988
+ } else if (event.key === " ") {
7989
+ event.preventDefault();
7990
+ spacePressed.current = true;
7490
7991
  }
7491
- if (e.key === "Enter") {
7992
+ },
7993
+ onKeyUp: (event) => {
7994
+ props.onKeyUp?.(event);
7995
+ if (event.key !== " ") return;
7996
+ const activate = spacePressed.current && !event.defaultPrevented;
7997
+ spacePressed.current = false;
7998
+ if (activate) {
7999
+ event.preventDefault();
7492
8000
  jumpToPage(pageNumber, { behavior: "auto" });
7493
8001
  }
7494
8002
  },
8003
+ onBlur: (event) => {
8004
+ spacePressed.current = false;
8005
+ props.onBlur?.(event);
8006
+ },
7495
8007
  ref: canvasRef
7496
8008
  }
7497
8009
  ) });
7498
8010
  };
8011
+ var Thumbnail = (props) => /* @__PURE__ */ jsx(
8012
+ PageLoadingBoundary,
8013
+ {
8014
+ pageNumber: props.pageNumber ?? 1,
8015
+ fallback: /* @__PURE__ */ jsx(
8016
+ "div",
8017
+ {
8018
+ "aria-label": `Loading page ${props.pageNumber ?? 1}`,
8019
+ "aria-busy": "true",
8020
+ style: { minHeight: 150 }
8021
+ }
8022
+ ),
8023
+ children: /* @__PURE__ */ jsx(LoadedThumbnail, { ...props })
8024
+ }
8025
+ );
7499
8026
  function VirtualizedThumbnails({
7500
8027
  children,
7501
8028
  count,
@@ -7508,6 +8035,9 @@ function VirtualizedThumbnails({
7508
8035
  const containerRef = useRef(null);
7509
8036
  useImperativeHandle(ref, () => containerRef.current);
7510
8037
  const [focusPage, setFocusPage] = useState(null);
8038
+ const focusPageLoaded = usePdf(
8039
+ (state) => focusPage === null ? false : state.isPageLoaded(focusPage)
8040
+ );
7511
8041
  const itemHeight = Number.isFinite(virtualize.itemHeight) ? Math.max(1, virtualize.itemHeight) : 150;
7512
8042
  const virtualizer = useVirtualizer({
7513
8043
  count,
@@ -7525,7 +8055,7 @@ function VirtualizedThumbnails({
7525
8055
  canvas.focus({ preventScroll: true });
7526
8056
  setFocusPage(null);
7527
8057
  }
7528
- }, [focusPage, items]);
8058
+ }, [focusPage, focusPageLoaded, items]);
7529
8059
  return /* @__PURE__ */ jsx(
7530
8060
  Primitive.div,
7531
8061
  {
@@ -7658,12 +8188,42 @@ var CurrentZoom = ({ ...props }) => {
7658
8188
  // src/lib/search.ts
7659
8189
  var normalizedPages = /* @__PURE__ */ new WeakMap();
7660
8190
  function normalize(page) {
7661
- let cached = normalizedPages.get(page);
7662
- if (!cached || cached.text !== page.text) {
7663
- cached = { text: page.text, lower: page.text.toLowerCase() };
7664
- normalizedPages.set(page, cached);
7665
- }
7666
- return cached.lower;
8191
+ const cached = normalizedPages.get(page);
8192
+ if (cached?.text === page.text) return cached;
8193
+ const lower = page.text.toLowerCase();
8194
+ const result = { text: page.text, lower };
8195
+ if (lower.length !== page.text.length) {
8196
+ result.starts = new Uint32Array(lower.length);
8197
+ result.ends = new Uint32Array(lower.length);
8198
+ let original = 0;
8199
+ let normalized = 0;
8200
+ for (const character of page.text) {
8201
+ const length = character.toLowerCase().length;
8202
+ for (let i = 0; i < length; i++) {
8203
+ result.starts[normalized + i] = original + (length === character.length ? i : 0);
8204
+ result.ends[normalized + i] = original + (length === character.length ? i + 1 : character.length);
8205
+ }
8206
+ original += character.length;
8207
+ normalized += length;
8208
+ }
8209
+ }
8210
+ normalizedPages.set(page, result);
8211
+ return result;
8212
+ }
8213
+ function createMatch(page, normalized, start, length, searchText, textSize, score) {
8214
+ const end = Math.min(normalized.lower.length, start + length);
8215
+ const matchIndex = normalized.starts?.[start] ?? start;
8216
+ const matchEnd = normalized.ends?.[end - 1] ?? end;
8217
+ const matchLength = matchEnd - matchIndex;
8218
+ return {
8219
+ pageNumber: page.pageNumber,
8220
+ text: page.text.slice(matchIndex, matchEnd + textSize),
8221
+ score,
8222
+ matchIndex,
8223
+ isExactMatch: score === 1,
8224
+ searchText,
8225
+ ...matchLength !== searchText.length ? { matchLength } : {}
8226
+ };
7667
8227
  }
7668
8228
  function createBoundedDistance(query, maxDistance) {
7669
8229
  const length = query.length;
@@ -7726,7 +8286,8 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
7726
8286
  let steps = 0;
7727
8287
  let deadline = cooperative ? performance.now() + budget : 0;
7728
8288
  for (const page of pages) {
7729
- const lower = normalize(page);
8289
+ const normalized = normalize(page);
8290
+ const lower = normalized.lower;
7730
8291
  const exactStarts = [];
7731
8292
  let index = 0;
7732
8293
  while (true) {
@@ -7739,16 +8300,19 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
7739
8300
  exactCount++;
7740
8301
  if (maxDistance > 0) exactStarts.push(matchIndex);
7741
8302
  if (exactMatches.length < limit) {
7742
- exactMatches.push({
7743
- pageNumber: page.pageNumber,
7744
- text: page.text.substr(matchIndex, searchText.length + textSize),
7745
- score: 1,
7746
- matchIndex,
7747
- isExactMatch: true,
7748
- searchText
7749
- });
8303
+ exactMatches.push(
8304
+ createMatch(
8305
+ page,
8306
+ normalized,
8307
+ matchIndex,
8308
+ query.length,
8309
+ searchText,
8310
+ textSize,
8311
+ 1
8312
+ )
8313
+ );
7750
8314
  }
7751
- index = matchIndex + searchText.length;
8315
+ index = matchIndex + query.length;
7752
8316
  }
7753
8317
  if (!(maxDistance > 0)) continue;
7754
8318
  index = 0;
@@ -7758,11 +8322,11 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
7758
8322
  yield;
7759
8323
  deadline = performance.now() + budget;
7760
8324
  }
7761
- while (exactIndex < exactStarts.length && exactStarts[exactIndex] + searchText.length <= index)
8325
+ while (exactIndex < exactStarts.length && exactStarts[exactIndex] + query.length <= index)
7762
8326
  exactIndex++;
7763
8327
  const exactStart = exactStarts[exactIndex];
7764
8328
  if (exactStart !== void 0 && index >= exactStart) {
7765
- index = exactStart + searchText.length;
8329
+ index = exactStart + query.length;
7766
8330
  continue;
7767
8331
  }
7768
8332
  const edits = distance(lower, index);
@@ -7772,14 +8336,15 @@ function* runSearch(pages, searchText, options = {}, cooperative = false, budget
7772
8336
  if (fuzzyMatches.length < limit || score > (fuzzyMatches[limit - 1]?.score ?? Infinity)) {
7773
8337
  insertMatch(
7774
8338
  fuzzyMatches,
7775
- {
7776
- pageNumber: page.pageNumber,
7777
- text: page.text.substr(index, query.length + textSize),
7778
- score,
7779
- matchIndex: index,
7780
- isExactMatch: false,
7781
- searchText
7782
- },
8339
+ createMatch(
8340
+ page,
8341
+ normalized,
8342
+ index,
8343
+ query.length,
8344
+ searchText,
8345
+ textSize,
8346
+ score
8347
+ ),
7783
8348
  limit
7784
8349
  );
7785
8350
  }
@@ -7885,32 +8450,37 @@ var useSearch = () => {
7885
8450
 
7886
8451
  // src/hooks/search/useSearchPosition.tsx
7887
8452
  async function calculateHighlightRects(pageProxy, textMatch) {
7888
- const matchLength = textMatch.searchText ? textMatch.searchText.length : textMatch.text.length;
7889
- if (matchLength === 0 || !Number.isFinite(textMatch.matchIndex) || textMatch.matchIndex < 0)
8453
+ const matchLength = textMatch.matchLength ?? (textMatch.searchText ? textMatch.searchText.length : textMatch.text.length);
8454
+ if (!Number.isFinite(matchLength) || matchLength <= 0 || !Number.isFinite(textMatch.matchIndex) || textMatch.matchIndex < 0)
7890
8455
  return [];
7891
8456
  const matchEnd = textMatch.matchIndex + matchLength;
7892
8457
  const reader = pageProxy.streamTextContent().getReader();
7893
8458
  const matchRects = [];
7894
8459
  const viewport = pageProxy.getViewport({ scale: 1 });
8460
+ const styles = {};
7895
8461
  let offset3 = 0;
7896
8462
  let foundEnd = false;
7897
8463
  try {
7898
8464
  read: while (true) {
7899
8465
  const { value, done } = await reader.read();
7900
8466
  if (done) break;
8467
+ Object.assign(styles, value.styles);
7901
8468
  for (const item of value.items) {
7902
8469
  if (!("str" in item) || item.str.length === 0) continue;
7903
8470
  const length = item.str.length;
7904
8471
  const start = Math.max(0, textMatch.matchIndex - offset3);
7905
8472
  const end = Math.min(length, matchEnd - offset3);
7906
- if (end > start) {
7907
- matchRects.push({
7908
- pageNumber: textMatch.pageNumber,
7909
- left: item.transform[4] + start * (item.width / length),
7910
- top: viewport.height - (item.transform[5] + item.height),
7911
- width: (end - start) * (item.width / length),
7912
- height: item.height
7913
- });
8473
+ if (end > start && item.str.slice(start, end).trim()) {
8474
+ matchRects.push(
8475
+ getItemRect(
8476
+ item,
8477
+ styles[item.fontName],
8478
+ viewport,
8479
+ start,
8480
+ end,
8481
+ textMatch.pageNumber
8482
+ )
8483
+ );
7914
8484
  }
7915
8485
  offset3 += length;
7916
8486
  if (offset3 >= matchEnd) {
@@ -7927,6 +8497,61 @@ async function calculateHighlightRects(pageProxy, textMatch) {
7927
8497
  }
7928
8498
  return mergeAdjacentRects(matchRects);
7929
8499
  }
8500
+ var measurementContext;
8501
+ function getTextFractions(item, style, start, end) {
8502
+ const length = item.str.length;
8503
+ let from = start / length;
8504
+ let to = end / length;
8505
+ if ((start > 0 || end < length) && style?.fontFamily && !style.vertical && typeof document !== "undefined") {
8506
+ if (measurementContext === void 0) {
8507
+ const canvas = document.createElement("canvas");
8508
+ canvas.width = canvas.height = 0;
8509
+ measurementContext = canvas.getContext("2d");
8510
+ }
8511
+ if (measurementContext) {
8512
+ measurementContext.font = `100px ${style.fontFamily}`;
8513
+ const width = measurementContext.measureText(item.str).width;
8514
+ if (width > 0) {
8515
+ from = start === 0 ? 0 : measurementContext.measureText(item.str.slice(0, start)).width / width;
8516
+ to = end === length ? 1 : measurementContext.measureText(item.str.slice(0, end)).width / width;
8517
+ }
8518
+ }
8519
+ }
8520
+ return item.dir === "rtl" ? [1 - to, 1 - from] : [from, to];
8521
+ }
8522
+ function getItemRect(item, style, viewport, start, end, pageNumber) {
8523
+ const angle = Math.atan2(item.transform[1], item.transform[0]) - (style?.vertical ? Math.PI / 2 : 0);
8524
+ const cosine = Math.cos(angle);
8525
+ const sine = Math.sin(angle);
8526
+ const height = Math.hypot(item.transform[2], item.transform[3]);
8527
+ const ascent = (style?.ascent ?? (style?.descent ? 1 + style.descent : 1)) * height;
8528
+ const width = style?.vertical ? item.height : item.width;
8529
+ const [startFraction, endFraction] = getTextFractions(
8530
+ item,
8531
+ style,
8532
+ start,
8533
+ end
8534
+ );
8535
+ const from = startFraction * width;
8536
+ const to = endFraction * width;
8537
+ let left = Infinity;
8538
+ let top = Infinity;
8539
+ let right = -Infinity;
8540
+ let bottom = -Infinity;
8541
+ for (const along of [from, to]) {
8542
+ for (const above of [ascent, ascent - height]) {
8543
+ const [x, y] = viewport.convertToViewportPoint(
8544
+ item.transform[4] + along * cosine - above * sine,
8545
+ item.transform[5] + along * sine + above * cosine
8546
+ );
8547
+ left = Math.min(left, x);
8548
+ top = Math.min(top, y);
8549
+ right = Math.max(right, x);
8550
+ bottom = Math.max(bottom, y);
8551
+ }
8552
+ }
8553
+ return { pageNumber, left, top, width: right - left, height: bottom - top };
8554
+ }
7930
8555
  function mergeAdjacentRects(rects) {
7931
8556
  if (rects.length <= 1) return rects;
7932
8557
  const merged = [];
@@ -7935,10 +8560,13 @@ function mergeAdjacentRects(rects) {
7935
8560
  for (let i = 1; i < rects.length; i++) {
7936
8561
  const next = rects[i];
7937
8562
  if (!next) continue;
7938
- if (Math.abs(current.top - next.top) < 2 && Math.abs(current.height - next.height) < 2) {
8563
+ if (Math.abs(current.top - next.top) < 2 && Math.abs(current.height - next.height) < 2 && Math.max(current.left, next.left) <= Math.min(current.left + current.width, next.left + next.width) + 2) {
7939
8564
  current = {
7940
8565
  ...current,
7941
- width: next.left + next.width - current.left
8566
+ left: Math.min(current.left, next.left),
8567
+ top: Math.min(current.top, next.top),
8568
+ width: Math.max(current.left + current.width, next.left + next.width) - Math.min(current.left, next.left),
8569
+ height: Math.max(current.top + current.height, next.top + next.height) - Math.min(current.top, next.top)
7942
8570
  };
7943
8571
  } else {
7944
8572
  merged.push(current);