pdfjs-reader-core 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -332,6 +332,9 @@ var init_highlight_storage = __esm({
332
332
  });
333
333
 
334
334
  // src/store/viewer-store.ts
335
+ function generateRequestId() {
336
+ return `scroll-${Date.now()}-${++requestCounter}`;
337
+ }
335
338
  function createViewerStore(initialOverrides = {}) {
336
339
  return (0, import_vanilla.createStore)()((set, get) => ({
337
340
  ...initialState,
@@ -384,6 +387,46 @@ function createViewerStore(initialOverrides = {}) {
384
387
  set({ currentPage: currentPage - 1 });
385
388
  }
386
389
  },
390
+ // Scroll coordination actions
391
+ requestScrollToPage: (page, behavior = "smooth") => {
392
+ const { numPages } = get();
393
+ const validPage = Math.max(1, Math.min(page, numPages));
394
+ return new Promise((resolve) => {
395
+ const requestId = generateRequestId();
396
+ const timeoutId = setTimeout(() => {
397
+ const callback = scrollCallbacks.get(requestId);
398
+ if (callback) {
399
+ scrollCallbacks.delete(requestId);
400
+ callback.resolve();
401
+ }
402
+ const currentRequest = get().scrollToPageRequest;
403
+ if (currentRequest?.requestId === requestId) {
404
+ set({ scrollToPageRequest: null });
405
+ }
406
+ }, SCROLL_TIMEOUT_MS);
407
+ scrollCallbacks.set(requestId, { resolve, timeoutId });
408
+ set({
409
+ currentPage: validPage,
410
+ scrollToPageRequest: {
411
+ page: validPage,
412
+ requestId,
413
+ behavior
414
+ }
415
+ });
416
+ });
417
+ },
418
+ completeScrollRequest: (requestId) => {
419
+ const callback = scrollCallbacks.get(requestId);
420
+ if (callback) {
421
+ clearTimeout(callback.timeoutId);
422
+ scrollCallbacks.delete(requestId);
423
+ callback.resolve();
424
+ }
425
+ const currentRequest = get().scrollToPageRequest;
426
+ if (currentRequest?.requestId === requestId) {
427
+ set({ scrollToPageRequest: null });
428
+ }
429
+ },
387
430
  // Zoom actions
388
431
  setScale: (scale) => {
389
432
  const clampedScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
@@ -450,7 +493,7 @@ function createViewerStore(initialOverrides = {}) {
450
493
  }
451
494
  }));
452
495
  }
453
- var import_vanilla, ZOOM_LEVELS, MIN_SCALE, MAX_SCALE, initialState;
496
+ var import_vanilla, ZOOM_LEVELS, MIN_SCALE, MAX_SCALE, SCROLL_TIMEOUT_MS, scrollCallbacks, requestCounter, initialState;
454
497
  var init_viewer_store = __esm({
455
498
  "src/store/viewer-store.ts"() {
456
499
  "use strict";
@@ -458,6 +501,9 @@ var init_viewer_store = __esm({
458
501
  ZOOM_LEVELS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
459
502
  MIN_SCALE = 0.1;
460
503
  MAX_SCALE = 10;
504
+ SCROLL_TIMEOUT_MS = 3e3;
505
+ scrollCallbacks = /* @__PURE__ */ new Map();
506
+ requestCounter = 0;
461
507
  initialState = {
462
508
  // Document state
463
509
  document: null,
@@ -468,6 +514,8 @@ var init_viewer_store = __esm({
468
514
  currentPage: 1,
469
515
  scale: 1,
470
516
  rotation: 0,
517
+ // Scroll coordination
518
+ scrollToPageRequest: null,
471
519
  // UI state
472
520
  viewMode: "single",
473
521
  scrollMode: "single",
@@ -1277,6 +1325,252 @@ var init_agent_api = __esm({
1277
1325
  }
1278
1326
  });
1279
1327
 
1328
+ // src/utils/text-search.ts
1329
+ async function extractPageText(document2, pageNumber) {
1330
+ const page = await document2.getPage(pageNumber);
1331
+ const textContent = await page.getTextContent();
1332
+ const viewport = page.getViewport({ scale: 1 });
1333
+ let fullText = "";
1334
+ const charPositions = [];
1335
+ for (const item of textContent.items) {
1336
+ if ("str" in item && item.str) {
1337
+ const tx = item.transform;
1338
+ const x = tx[4];
1339
+ const y = viewport.height - tx[5];
1340
+ const width = item.width ?? 0;
1341
+ const height = item.height ?? 12;
1342
+ const charWidth = item.str.length > 0 ? width / item.str.length : width;
1343
+ for (let i = 0; i < item.str.length; i++) {
1344
+ charPositions.push({
1345
+ char: item.str[i],
1346
+ rect: {
1347
+ x: x + i * charWidth,
1348
+ y: y - height,
1349
+ width: charWidth,
1350
+ height
1351
+ }
1352
+ });
1353
+ }
1354
+ fullText += item.str;
1355
+ }
1356
+ }
1357
+ return { fullText, charPositions };
1358
+ }
1359
+ async function findTextOnPage(document2, pageNumber, query, options = {}) {
1360
+ const { caseSensitive = false, wholeWord = false } = options;
1361
+ if (!query || pageNumber < 1 || pageNumber > document2.numPages) {
1362
+ return [];
1363
+ }
1364
+ const { fullText, charPositions } = await extractPageText(document2, pageNumber);
1365
+ const matches = [];
1366
+ const searchText = caseSensitive ? query : query.toLowerCase();
1367
+ const textToSearch = caseSensitive ? fullText : fullText.toLowerCase();
1368
+ let startIndex = 0;
1369
+ while (true) {
1370
+ const matchIndex = textToSearch.indexOf(searchText, startIndex);
1371
+ if (matchIndex === -1) break;
1372
+ if (wholeWord) {
1373
+ const beforeChar = matchIndex > 0 ? textToSearch[matchIndex - 1] : " ";
1374
+ const afterChar = matchIndex + query.length < textToSearch.length ? textToSearch[matchIndex + query.length] : " ";
1375
+ if (/\w/.test(beforeChar) || /\w/.test(afterChar)) {
1376
+ startIndex = matchIndex + 1;
1377
+ continue;
1378
+ }
1379
+ }
1380
+ const matchRects = [];
1381
+ for (let i = matchIndex; i < matchIndex + query.length && i < charPositions.length; i++) {
1382
+ matchRects.push(charPositions[i].rect);
1383
+ }
1384
+ const mergedRects = mergeAdjacentRects(matchRects);
1385
+ matches.push({
1386
+ text: fullText.substring(matchIndex, matchIndex + query.length),
1387
+ rects: mergedRects,
1388
+ pageNumber,
1389
+ startIndex: matchIndex
1390
+ });
1391
+ startIndex = matchIndex + 1;
1392
+ }
1393
+ return matches;
1394
+ }
1395
+ async function findTextInDocument(document2, query, options = {}) {
1396
+ const { pageRange, ...findOptions } = options;
1397
+ const pagesToSearch = pageRange ?? Array.from({ length: document2.numPages }, (_, i) => i + 1);
1398
+ const allMatches = [];
1399
+ for (const pageNum of pagesToSearch) {
1400
+ if (pageNum < 1 || pageNum > document2.numPages) continue;
1401
+ try {
1402
+ const matches = await findTextOnPage(document2, pageNum, query, findOptions);
1403
+ allMatches.push(...matches);
1404
+ } catch {
1405
+ }
1406
+ }
1407
+ return allMatches;
1408
+ }
1409
+ function mergeAdjacentRects(rects) {
1410
+ if (rects.length === 0) return [];
1411
+ const sorted = [...rects].sort((a, b) => a.y - b.y || a.x - b.x);
1412
+ const merged = [];
1413
+ let current = { ...sorted[0] };
1414
+ for (let i = 1; i < sorted.length; i++) {
1415
+ const rect = sorted[i];
1416
+ if (Math.abs(rect.y - current.y) < 2 && rect.x <= current.x + current.width + 2) {
1417
+ const newRight = Math.max(current.x + current.width, rect.x + rect.width);
1418
+ current.width = newRight - current.x;
1419
+ current.height = Math.max(current.height, rect.height);
1420
+ } else {
1421
+ merged.push(current);
1422
+ current = { ...rect };
1423
+ }
1424
+ }
1425
+ merged.push(current);
1426
+ return merged;
1427
+ }
1428
+ async function getPageText(document2, pageNumber) {
1429
+ if (pageNumber < 1 || pageNumber > document2.numPages) {
1430
+ return "";
1431
+ }
1432
+ const page = await document2.getPage(pageNumber);
1433
+ const textContent = await page.getTextContent();
1434
+ return textContent.items.filter((item) => "str" in item).map((item) => item.str).join("");
1435
+ }
1436
+ async function countTextOnPage(document2, pageNumber, query, options = {}) {
1437
+ const { caseSensitive = false, wholeWord = false } = options;
1438
+ if (!query || pageNumber < 1 || pageNumber > document2.numPages) {
1439
+ return 0;
1440
+ }
1441
+ const text = await getPageText(document2, pageNumber);
1442
+ const searchText = caseSensitive ? query : query.toLowerCase();
1443
+ const textToSearch = caseSensitive ? text : text.toLowerCase();
1444
+ let count = 0;
1445
+ let startIndex = 0;
1446
+ while (true) {
1447
+ const matchIndex = textToSearch.indexOf(searchText, startIndex);
1448
+ if (matchIndex === -1) break;
1449
+ if (wholeWord) {
1450
+ const beforeChar = matchIndex > 0 ? textToSearch[matchIndex - 1] : " ";
1451
+ const afterChar = matchIndex + query.length < textToSearch.length ? textToSearch[matchIndex + query.length] : " ";
1452
+ if (/\w/.test(beforeChar) || /\w/.test(afterChar)) {
1453
+ startIndex = matchIndex + 1;
1454
+ continue;
1455
+ }
1456
+ }
1457
+ count++;
1458
+ startIndex = matchIndex + 1;
1459
+ }
1460
+ return count;
1461
+ }
1462
+ var init_text_search = __esm({
1463
+ "src/utils/text-search.ts"() {
1464
+ "use strict";
1465
+ }
1466
+ });
1467
+
1468
+ // src/utils/coordinates.ts
1469
+ function pdfToViewport(x, y, scale, pageHeight) {
1470
+ return {
1471
+ x: x * scale,
1472
+ y: (pageHeight - y) * scale
1473
+ };
1474
+ }
1475
+ function viewportToPDF(x, y, scale, pageHeight) {
1476
+ return {
1477
+ x: x / scale,
1478
+ y: pageHeight - y / scale
1479
+ };
1480
+ }
1481
+ function percentToPDF(xPercent, yPercent, pageWidth, pageHeight) {
1482
+ return {
1483
+ x: xPercent / 100 * pageWidth,
1484
+ y: yPercent / 100 * pageHeight
1485
+ };
1486
+ }
1487
+ function pdfToPercent(x, y, pageWidth, pageHeight) {
1488
+ return {
1489
+ x: x / pageWidth * 100,
1490
+ y: y / pageHeight * 100
1491
+ };
1492
+ }
1493
+ function percentToViewport(xPercent, yPercent, pageWidth, pageHeight, scale) {
1494
+ return {
1495
+ x: xPercent / 100 * pageWidth * scale,
1496
+ y: yPercent / 100 * pageHeight * scale
1497
+ };
1498
+ }
1499
+ function viewportToPercent(x, y, pageWidth, pageHeight, scale) {
1500
+ return {
1501
+ x: x / (pageWidth * scale) * 100,
1502
+ y: y / (pageHeight * scale) * 100
1503
+ };
1504
+ }
1505
+ function applyRotation(x, y, rotation, pageWidth, pageHeight) {
1506
+ const normalizedRotation = (rotation % 360 + 360) % 360;
1507
+ switch (normalizedRotation) {
1508
+ case 90:
1509
+ return { x: y, y: pageWidth - x };
1510
+ case 180:
1511
+ return { x: pageWidth - x, y: pageHeight - y };
1512
+ case 270:
1513
+ return { x: pageHeight - y, y: x };
1514
+ default:
1515
+ return { x, y };
1516
+ }
1517
+ }
1518
+ function removeRotation(x, y, rotation, pageWidth, pageHeight) {
1519
+ const normalizedRotation = (rotation % 360 + 360) % 360;
1520
+ switch (normalizedRotation) {
1521
+ case 90:
1522
+ return { x: pageWidth - y, y: x };
1523
+ case 180:
1524
+ return { x: pageWidth - x, y: pageHeight - y };
1525
+ case 270:
1526
+ return { x: y, y: pageHeight - x };
1527
+ default:
1528
+ return { x, y };
1529
+ }
1530
+ }
1531
+ function getRotatedDimensions(width, height, rotation) {
1532
+ const normalizedRotation = (rotation % 360 + 360) % 360;
1533
+ if (normalizedRotation === 90 || normalizedRotation === 270) {
1534
+ return { width: height, height: width };
1535
+ }
1536
+ return { width, height };
1537
+ }
1538
+ function scaleRect(rect, fromScale, toScale) {
1539
+ const ratio = toScale / fromScale;
1540
+ return {
1541
+ x: rect.x * ratio,
1542
+ y: rect.y * ratio,
1543
+ width: rect.width * ratio,
1544
+ height: rect.height * ratio
1545
+ };
1546
+ }
1547
+ function isPointInRect(point, rect) {
1548
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
1549
+ }
1550
+ function doRectsIntersect(rectA, rectB) {
1551
+ return !(rectA.x + rectA.width < rectB.x || rectB.x + rectB.width < rectA.x || rectA.y + rectA.height < rectB.y || rectB.y + rectB.height < rectA.y);
1552
+ }
1553
+ function getRectIntersection(rectA, rectB) {
1554
+ const x = Math.max(rectA.x, rectB.x);
1555
+ const y = Math.max(rectA.y, rectB.y);
1556
+ const right = Math.min(rectA.x + rectA.width, rectB.x + rectB.width);
1557
+ const bottom = Math.min(rectA.y + rectA.height, rectB.y + rectB.height);
1558
+ if (right <= x || bottom <= y) {
1559
+ return null;
1560
+ }
1561
+ return {
1562
+ x,
1563
+ y,
1564
+ width: right - x,
1565
+ height: bottom - y
1566
+ };
1567
+ }
1568
+ var init_coordinates = __esm({
1569
+ "src/utils/coordinates.ts"() {
1570
+ "use strict";
1571
+ }
1572
+ });
1573
+
1280
1574
  // src/utils/index.ts
1281
1575
  var init_utils = __esm({
1282
1576
  "src/utils/index.ts"() {
@@ -1289,6 +1583,8 @@ var init_utils = __esm({
1289
1583
  init_export_annotations();
1290
1584
  init_student_storage();
1291
1585
  init_agent_api();
1586
+ init_text_search();
1587
+ init_coordinates();
1292
1588
  }
1293
1589
  });
1294
1590
 
@@ -7926,6 +8222,8 @@ var init_DocumentContainer = __esm({
7926
8222
  nextPage,
7927
8223
  previousPage
7928
8224
  } = usePDFViewer();
8225
+ const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
8226
+ const { viewerStore } = usePDFViewerStores();
7929
8227
  const [currentPageObj, setCurrentPageObj] = (0, import_react35.useState)(null);
7930
8228
  const [isLoadingPage, setIsLoadingPage] = (0, import_react35.useState)(false);
7931
8229
  const containerRef = (0, import_react35.useRef)(null);
@@ -7991,6 +8289,11 @@ var init_DocumentContainer = __esm({
7991
8289
  const page = await document2.getPage(currentPage);
7992
8290
  if (!cancelled && document2 === documentRef.current) {
7993
8291
  setCurrentPageObj(page);
8292
+ if (scrollToPageRequest && scrollToPageRequest.page === currentPage) {
8293
+ requestAnimationFrame(() => {
8294
+ viewerStore.getState().completeScrollRequest(scrollToPageRequest.requestId);
8295
+ });
8296
+ }
7994
8297
  }
7995
8298
  } catch (error) {
7996
8299
  if (!cancelled) {
@@ -8009,7 +8312,7 @@ var init_DocumentContainer = __esm({
8009
8312
  return () => {
8010
8313
  cancelled = true;
8011
8314
  };
8012
- }, [document2, currentPage]);
8315
+ }, [document2, currentPage, scrollToPageRequest, viewerStore]);
8013
8316
  const getPageElement = (0, import_react35.useCallback)(() => {
8014
8317
  return containerRef.current?.querySelector(`[data-page-number="${currentPage}"]`);
8015
8318
  }, [currentPage]);
@@ -8151,6 +8454,8 @@ var init_VirtualizedDocumentContainer = __esm({
8151
8454
  nextPage,
8152
8455
  previousPage
8153
8456
  } = usePDFViewer();
8457
+ const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
8458
+ const { viewerStore } = usePDFViewerStores();
8154
8459
  const containerRef = (0, import_react36.useRef)(null);
8155
8460
  const scrollContainerRef = (0, import_react36.useRef)(null);
8156
8461
  const documentRef = (0, import_react36.useRef)(null);
@@ -8288,6 +8593,45 @@ var init_VirtualizedDocumentContainer = __esm({
8288
8593
  loadPages();
8289
8594
  }, [document2, visiblePages, pageObjects]);
8290
8595
  (0, import_react36.useEffect)(() => {
8596
+ if (!scrollToPageRequest || !scrollContainerRef.current || pageInfos.length === 0) return;
8597
+ const { page, requestId, behavior } = scrollToPageRequest;
8598
+ const pageInfo = pageInfos.find((p) => p.pageNumber === page);
8599
+ if (!pageInfo) {
8600
+ viewerStore.getState().completeScrollRequest(requestId);
8601
+ return;
8602
+ }
8603
+ const container = scrollContainerRef.current;
8604
+ const targetScroll = pageInfo.top - pageGap;
8605
+ const scrollTop = container.scrollTop;
8606
+ const viewportHeight = container.clientHeight;
8607
+ const isVisible = targetScroll >= scrollTop && pageInfo.top + pageInfo.height <= scrollTop + viewportHeight;
8608
+ if (isVisible) {
8609
+ viewerStore.getState().completeScrollRequest(requestId);
8610
+ return;
8611
+ }
8612
+ container.scrollTo({
8613
+ top: targetScroll,
8614
+ behavior
8615
+ });
8616
+ if (behavior === "instant") {
8617
+ requestAnimationFrame(() => {
8618
+ viewerStore.getState().completeScrollRequest(requestId);
8619
+ });
8620
+ } else {
8621
+ let scrollEndTimeout;
8622
+ const handleScrollEnd = () => {
8623
+ clearTimeout(scrollEndTimeout);
8624
+ scrollEndTimeout = setTimeout(() => {
8625
+ container.removeEventListener("scroll", handleScrollEnd);
8626
+ viewerStore.getState().completeScrollRequest(requestId);
8627
+ }, 100);
8628
+ };
8629
+ container.addEventListener("scroll", handleScrollEnd, { passive: true });
8630
+ handleScrollEnd();
8631
+ }
8632
+ }, [scrollToPageRequest, pageInfos, pageGap, viewerStore]);
8633
+ (0, import_react36.useEffect)(() => {
8634
+ if (scrollToPageRequest) return;
8291
8635
  if (!scrollContainerRef.current || pageInfos.length === 0) return;
8292
8636
  const pageInfo = pageInfos.find((p) => p.pageNumber === currentPage);
8293
8637
  if (pageInfo) {
@@ -8302,7 +8646,7 @@ var init_VirtualizedDocumentContainer = __esm({
8302
8646
  });
8303
8647
  }
8304
8648
  }
8305
- }, [currentPage, pageInfos, pageGap]);
8649
+ }, [currentPage, pageInfos, pageGap, scrollToPageRequest]);
8306
8650
  const handlePinchZoom = (0, import_react36.useCallback)(
8307
8651
  (pinchScale) => {
8308
8652
  const newScale = Math.max(0.25, Math.min(4, baseScaleRef.current * pinchScale));
@@ -8509,6 +8853,8 @@ var init_DualPageContainer = __esm({
8509
8853
  setScale,
8510
8854
  goToPage
8511
8855
  } = usePDFViewer();
8856
+ const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
8857
+ const { viewerStore } = usePDFViewerStores();
8512
8858
  const containerRef = (0, import_react38.useRef)(null);
8513
8859
  const documentRef = (0, import_react38.useRef)(null);
8514
8860
  const baseScaleRef = (0, import_react38.useRef)(scale);
@@ -8594,6 +8940,14 @@ var init_DualPageContainer = __esm({
8594
8940
  if (!cancelled) {
8595
8941
  setLeftPage(left);
8596
8942
  setRightPage(right);
8943
+ if (scrollToPageRequest) {
8944
+ const requestedPage = scrollToPageRequest.page;
8945
+ if (requestedPage === spread2.left || requestedPage === spread2.right) {
8946
+ requestAnimationFrame(() => {
8947
+ viewerStore.getState().completeScrollRequest(scrollToPageRequest.requestId);
8948
+ });
8949
+ }
8950
+ }
8597
8951
  }
8598
8952
  } catch (error) {
8599
8953
  if (!cancelled) {
@@ -8609,7 +8963,7 @@ var init_DualPageContainer = __esm({
8609
8963
  return () => {
8610
8964
  cancelled = true;
8611
8965
  };
8612
- }, [document2, currentPage, getSpreadPages]);
8966
+ }, [document2, currentPage, getSpreadPages, scrollToPageRequest, viewerStore]);
8613
8967
  const goToPreviousSpread = (0, import_react38.useCallback)(() => {
8614
8968
  const spread2 = getSpreadPages(currentPage);
8615
8969
  const leftmostPage = spread2.left || spread2.right || currentPage;
@@ -8806,10 +9160,14 @@ var init_FloatingZoomControls = __esm({
8806
9160
  const scale = useViewerStore((s) => s.scale);
8807
9161
  const document2 = useViewerStore((s) => s.document);
8808
9162
  const handleZoomIn = (0, import_react39.useCallback)(() => {
8809
- viewerStore.getState().zoomIn();
9163
+ const currentScale = viewerStore.getState().scale;
9164
+ const newScale = Math.min(4, currentScale + 0.05);
9165
+ viewerStore.getState().setScale(newScale);
8810
9166
  }, [viewerStore]);
8811
9167
  const handleZoomOut = (0, import_react39.useCallback)(() => {
8812
- viewerStore.getState().zoomOut();
9168
+ const currentScale = viewerStore.getState().scale;
9169
+ const newScale = Math.max(0.1, currentScale - 0.05);
9170
+ viewerStore.getState().setScale(newScale);
8813
9171
  }, [viewerStore]);
8814
9172
  const handleFitToWidth = (0, import_react39.useCallback)(() => {
8815
9173
  viewerStore.getState().setScale(1);
@@ -8975,6 +9333,7 @@ var init_PDFViewerClient = __esm({
8975
9333
  PDFViewerInner = (0, import_react40.memo)(function PDFViewerInner2({
8976
9334
  src,
8977
9335
  initialPage = 1,
9336
+ page: controlledPage,
8978
9337
  initialScale = "auto",
8979
9338
  showToolbar = true,
8980
9339
  showSidebar = true,
@@ -8984,9 +9343,17 @@ var init_PDFViewerClient = __esm({
8984
9343
  onDocumentLoad,
8985
9344
  onPageChange,
8986
9345
  onScaleChange,
9346
+ onZoomChange,
8987
9347
  onError,
9348
+ onPageRenderStart,
9349
+ onPageRenderComplete,
9350
+ onHighlightAdded,
9351
+ onHighlightRemoved,
9352
+ onAnnotationAdded,
8988
9353
  workerSrc,
8989
9354
  className,
9355
+ loadingComponent,
9356
+ errorComponent,
8990
9357
  onReady
8991
9358
  }) {
8992
9359
  const { viewerStore, annotationStore, searchStore } = usePDFViewerStores();
@@ -8996,12 +9363,26 @@ var init_PDFViewerClient = __esm({
8996
9363
  const onErrorRef = (0, import_react40.useRef)(onError);
8997
9364
  const onPageChangeRef = (0, import_react40.useRef)(onPageChange);
8998
9365
  const onScaleChangeRef = (0, import_react40.useRef)(onScaleChange);
9366
+ const onZoomChangeRef = (0, import_react40.useRef)(onZoomChange);
9367
+ const onPageRenderStartRef = (0, import_react40.useRef)(onPageRenderStart);
9368
+ const onPageRenderCompleteRef = (0, import_react40.useRef)(onPageRenderComplete);
9369
+ const onHighlightAddedRef = (0, import_react40.useRef)(onHighlightAdded);
9370
+ const onHighlightRemovedRef = (0, import_react40.useRef)(onHighlightRemoved);
9371
+ const onAnnotationAddedRef = (0, import_react40.useRef)(onAnnotationAdded);
8999
9372
  const onReadyRef = (0, import_react40.useRef)(onReady);
9000
9373
  onDocumentLoadRef.current = onDocumentLoad;
9001
9374
  onErrorRef.current = onError;
9002
9375
  onPageChangeRef.current = onPageChange;
9003
9376
  onScaleChangeRef.current = onScaleChange;
9377
+ onZoomChangeRef.current = onZoomChange;
9378
+ onPageRenderStartRef.current = onPageRenderStart;
9379
+ onPageRenderCompleteRef.current = onPageRenderComplete;
9380
+ onHighlightAddedRef.current = onHighlightAdded;
9381
+ onHighlightRemovedRef.current = onHighlightRemoved;
9382
+ onAnnotationAddedRef.current = onAnnotationAdded;
9004
9383
  onReadyRef.current = onReady;
9384
+ const isControlled = controlledPage !== void 0;
9385
+ const prevControlledPageRef = (0, import_react40.useRef)(controlledPage);
9005
9386
  const srcIdRef = (0, import_react40.useRef)(null);
9006
9387
  const currentPage = useViewerStore((s) => s.currentPage);
9007
9388
  const scale = useViewerStore((s) => s.scale);
@@ -9142,8 +9523,9 @@ var init_PDFViewerClient = __esm({
9142
9523
  }
9143
9524
  },
9144
9525
  // ==================== Navigation ====================
9145
- goToPage: (page) => {
9146
- viewerStore.getState().goToPage(page);
9526
+ goToPage: async (page, options) => {
9527
+ const behavior = options?.behavior ?? "smooth";
9528
+ await viewerStore.getState().requestScrollToPage(page, behavior);
9147
9529
  },
9148
9530
  nextPage: () => {
9149
9531
  viewerStore.getState().nextPage();
@@ -9203,6 +9585,246 @@ var init_PDFViewerClient = __esm({
9203
9585
  clearSearch: () => {
9204
9586
  searchStore.getState().clearSearch();
9205
9587
  },
9588
+ // ==================== Combined Search & Highlight ====================
9589
+ searchAndHighlight: async (query, options) => {
9590
+ const doc = viewerStore.getState().document;
9591
+ if (!doc) {
9592
+ return { matchCount: 0, highlightIds: [], matches: [] };
9593
+ }
9594
+ const color = options?.color ?? "yellow";
9595
+ const caseSensitive = options?.caseSensitive ?? false;
9596
+ const wholeWord = options?.wholeWord ?? false;
9597
+ const scrollToFirst = options?.scrollToFirst ?? true;
9598
+ const clearPrevious = options?.clearPrevious ?? true;
9599
+ if (clearPrevious) {
9600
+ const existingHighlights = annotationStore.getState().highlights;
9601
+ for (const h of existingHighlights) {
9602
+ if (h.source === "search") {
9603
+ annotationStore.getState().removeHighlight(h.id);
9604
+ }
9605
+ }
9606
+ }
9607
+ let pagesToSearch;
9608
+ if (options?.pageRange) {
9609
+ if (Array.isArray(options.pageRange)) {
9610
+ pagesToSearch = options.pageRange;
9611
+ } else {
9612
+ const { start, end } = options.pageRange;
9613
+ pagesToSearch = Array.from({ length: end - start + 1 }, (_, i) => start + i);
9614
+ }
9615
+ } else {
9616
+ pagesToSearch = Array.from({ length: doc.numPages }, (_, i) => i + 1);
9617
+ }
9618
+ const result = {
9619
+ matchCount: 0,
9620
+ highlightIds: [],
9621
+ matches: []
9622
+ };
9623
+ const searchText = caseSensitive ? query : query.toLowerCase();
9624
+ for (const pageNum of pagesToSearch) {
9625
+ if (pageNum < 1 || pageNum > doc.numPages) continue;
9626
+ try {
9627
+ const page = await doc.getPage(pageNum);
9628
+ const textContent = await page.getTextContent();
9629
+ const viewport = page.getViewport({ scale: 1 });
9630
+ let fullText = "";
9631
+ const charPositions = [];
9632
+ for (const item of textContent.items) {
9633
+ if ("str" in item && item.str) {
9634
+ const tx = item.transform;
9635
+ const x = tx[4];
9636
+ const y = viewport.height - tx[5];
9637
+ const width = item.width ?? 0;
9638
+ const height = item.height ?? 12;
9639
+ const charWidth = item.str.length > 0 ? width / item.str.length : width;
9640
+ for (let i = 0; i < item.str.length; i++) {
9641
+ charPositions.push({
9642
+ char: item.str[i],
9643
+ rect: {
9644
+ x: x + i * charWidth,
9645
+ y: y - height,
9646
+ width: charWidth,
9647
+ height
9648
+ }
9649
+ });
9650
+ }
9651
+ fullText += item.str;
9652
+ }
9653
+ }
9654
+ const textToSearch = caseSensitive ? fullText : fullText.toLowerCase();
9655
+ let startIndex = 0;
9656
+ while (true) {
9657
+ let matchIndex = textToSearch.indexOf(searchText, startIndex);
9658
+ if (matchIndex === -1) break;
9659
+ if (wholeWord) {
9660
+ const beforeChar = matchIndex > 0 ? textToSearch[matchIndex - 1] : " ";
9661
+ const afterChar = matchIndex + query.length < textToSearch.length ? textToSearch[matchIndex + query.length] : " ";
9662
+ if (/\w/.test(beforeChar) || /\w/.test(afterChar)) {
9663
+ startIndex = matchIndex + 1;
9664
+ continue;
9665
+ }
9666
+ }
9667
+ const matchRects = [];
9668
+ for (let i = matchIndex; i < matchIndex + query.length && i < charPositions.length; i++) {
9669
+ matchRects.push(charPositions[i].rect);
9670
+ }
9671
+ const mergedRects = mergeRects2(matchRects);
9672
+ const highlight = annotationStore.getState().addHighlight({
9673
+ pageNumber: pageNum,
9674
+ rects: mergedRects,
9675
+ color,
9676
+ text: fullText.substring(matchIndex, matchIndex + query.length),
9677
+ source: "search"
9678
+ });
9679
+ result.matchCount++;
9680
+ result.highlightIds.push(highlight.id);
9681
+ result.matches.push({
9682
+ pageNumber: pageNum,
9683
+ text: fullText.substring(matchIndex, matchIndex + query.length),
9684
+ highlightId: highlight.id,
9685
+ rects: mergedRects
9686
+ });
9687
+ startIndex = matchIndex + 1;
9688
+ }
9689
+ } catch {
9690
+ }
9691
+ }
9692
+ if (scrollToFirst && result.matches.length > 0) {
9693
+ const firstMatch = result.matches[0];
9694
+ await viewerStore.getState().requestScrollToPage(firstMatch.pageNumber, "smooth");
9695
+ }
9696
+ return result;
9697
+ },
9698
+ // ==================== Agent Tools ====================
9699
+ agentTools: {
9700
+ navigateToPage: async (page) => {
9701
+ try {
9702
+ const { currentPage: currentPage2, numPages } = viewerStore.getState();
9703
+ if (numPages === 0) {
9704
+ return {
9705
+ success: false,
9706
+ error: { code: "NO_DOCUMENT", message: "No document is loaded" }
9707
+ };
9708
+ }
9709
+ if (page < 1 || page > numPages) {
9710
+ return {
9711
+ success: false,
9712
+ error: { code: "INVALID_PAGE", message: `Page ${page} is out of range (1-${numPages})` }
9713
+ };
9714
+ }
9715
+ const previousPage = currentPage2;
9716
+ await viewerStore.getState().requestScrollToPage(page, "smooth");
9717
+ return {
9718
+ success: true,
9719
+ data: { previousPage, currentPage: page }
9720
+ };
9721
+ } catch (err) {
9722
+ return {
9723
+ success: false,
9724
+ error: { code: "NAVIGATION_FAILED", message: err instanceof Error ? err.message : "Unknown error" }
9725
+ };
9726
+ }
9727
+ },
9728
+ highlightText: async (text, options) => {
9729
+ try {
9730
+ const highlightIds = await handle.highlightText(text, options);
9731
+ return {
9732
+ success: true,
9733
+ data: { matchCount: highlightIds.length, highlightIds }
9734
+ };
9735
+ } catch (err) {
9736
+ return {
9737
+ success: false,
9738
+ error: { code: "HIGHLIGHT_FAILED", message: err instanceof Error ? err.message : "Unknown error" }
9739
+ };
9740
+ }
9741
+ },
9742
+ getPageContent: async (page) => {
9743
+ try {
9744
+ const doc = viewerStore.getState().document;
9745
+ if (!doc) {
9746
+ return {
9747
+ success: false,
9748
+ error: { code: "NO_DOCUMENT", message: "No document is loaded" }
9749
+ };
9750
+ }
9751
+ if (page < 1 || page > doc.numPages) {
9752
+ return {
9753
+ success: false,
9754
+ error: { code: "INVALID_PAGE", message: `Page ${page} is out of range (1-${doc.numPages})` }
9755
+ };
9756
+ }
9757
+ const pageObj = await doc.getPage(page);
9758
+ const textContent = await pageObj.getTextContent();
9759
+ const text = textContent.items.filter((item) => "str" in item).map((item) => item.str).join("");
9760
+ return {
9761
+ success: true,
9762
+ data: { text }
9763
+ };
9764
+ } catch (err) {
9765
+ return {
9766
+ success: false,
9767
+ error: { code: "CONTENT_FETCH_FAILED", message: err instanceof Error ? err.message : "Unknown error" }
9768
+ };
9769
+ }
9770
+ },
9771
+ clearAllVisuals: async () => {
9772
+ try {
9773
+ const highlights = annotationStore.getState().highlights;
9774
+ for (const h of highlights) {
9775
+ annotationStore.getState().removeHighlight(h.id);
9776
+ }
9777
+ const annotations = annotationStore.getState().annotations;
9778
+ for (const a of annotations) {
9779
+ annotationStore.getState().removeAnnotation(a.id);
9780
+ }
9781
+ return { success: true };
9782
+ } catch (err) {
9783
+ return {
9784
+ success: false,
9785
+ error: { code: "CLEAR_FAILED", message: err instanceof Error ? err.message : "Unknown error" }
9786
+ };
9787
+ }
9788
+ }
9789
+ },
9790
+ // ==================== Coordinate Helpers ====================
9791
+ coordinates: {
9792
+ getPageDimensions: (page) => {
9793
+ const doc = viewerStore.getState().document;
9794
+ if (!doc || page < 1 || page > doc.numPages) {
9795
+ return null;
9796
+ }
9797
+ try {
9798
+ return {
9799
+ width: 612,
9800
+ // Default US Letter width
9801
+ height: 792,
9802
+ // Default US Letter height
9803
+ rotation: viewerStore.getState().rotation
9804
+ };
9805
+ } catch {
9806
+ return null;
9807
+ }
9808
+ },
9809
+ percentToPixels: (xPercent, yPercent, page) => {
9810
+ const dimensions = handle.coordinates.getPageDimensions(page);
9811
+ if (!dimensions) return null;
9812
+ const scale2 = viewerStore.getState().scale;
9813
+ return {
9814
+ x: xPercent / 100 * dimensions.width * scale2,
9815
+ y: yPercent / 100 * dimensions.height * scale2
9816
+ };
9817
+ },
9818
+ pixelsToPercent: (x, y, page) => {
9819
+ const dimensions = handle.coordinates.getPageDimensions(page);
9820
+ if (!dimensions) return null;
9821
+ const scale2 = viewerStore.getState().scale;
9822
+ return {
9823
+ x: x / (dimensions.width * scale2) * 100,
9824
+ y: y / (dimensions.height * scale2) * 100
9825
+ };
9826
+ }
9827
+ },
9206
9828
  // ==================== Document ====================
9207
9829
  getDocument: () => {
9208
9830
  return viewerStore.getState().document;
@@ -9289,10 +9911,36 @@ var init_PDFViewerClient = __esm({
9289
9911
  if (prevScaleRef.current !== scale) {
9290
9912
  prevScaleRef.current = scale;
9291
9913
  onScaleChangeRef.current?.(scale);
9914
+ onZoomChangeRef.current?.(scale);
9292
9915
  }
9293
9916
  }, [scale]);
9917
+ (0, import_react40.useEffect)(() => {
9918
+ if (!isControlled || controlledPage === void 0) return;
9919
+ if (prevControlledPageRef.current === controlledPage) return;
9920
+ prevControlledPageRef.current = controlledPage;
9921
+ const { numPages, currentPage: currentPage2 } = viewerStore.getState();
9922
+ if (numPages > 0 && controlledPage !== currentPage2) {
9923
+ viewerStore.getState().requestScrollToPage(controlledPage, "smooth");
9924
+ }
9925
+ }, [controlledPage, isControlled, viewerStore]);
9294
9926
  const themeClass = theme === "dark" ? "dark" : "";
9295
9927
  if (error) {
9928
+ if (errorComponent) {
9929
+ const errorContent = typeof errorComponent === "function" ? errorComponent(error, handleRetry) : errorComponent;
9930
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
9931
+ "div",
9932
+ {
9933
+ className: cn(
9934
+ "pdf-viewer pdf-viewer-error",
9935
+ "flex flex-col h-full",
9936
+ "bg-white dark:bg-gray-900",
9937
+ themeClass,
9938
+ className
9939
+ ),
9940
+ children: errorContent
9941
+ }
9942
+ );
9943
+ }
9296
9944
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
9297
9945
  "div",
9298
9946
  {
@@ -9348,7 +9996,7 @@ var init_PDFViewerClient = __esm({
9348
9996
  renderContainer()
9349
9997
  ] }),
9350
9998
  showFloatingZoom && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(FloatingZoomControls, { position: "bottom-right" }),
9351
- isLoading && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-900/80", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-col items-center", children: [
9999
+ isLoading && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-900/80", children: loadingComponent ?? /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-col items-center", children: [
9352
10000
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" }),
9353
10001
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "mt-2 text-sm text-gray-500", children: "Loading PDF..." })
9354
10002
  ] }) })
@@ -9461,6 +10109,7 @@ __export(index_exports, {
9461
10109
  OutlinePanel: () => OutlinePanel,
9462
10110
  PDFErrorBoundary: () => PDFErrorBoundary,
9463
10111
  PDFPage: () => PDFPage,
10112
+ PDFThumbnailNav: () => PDFThumbnailNav,
9464
10113
  PDFViewer: () => PDFViewer,
9465
10114
  PDFViewerClient: () => PDFViewerClient,
9466
10115
  PDFViewerContext: () => PDFViewerContext,
@@ -9479,9 +10128,11 @@ __export(index_exports, {
9479
10128
  ThumbnailPanel: () => ThumbnailPanel,
9480
10129
  Toolbar: () => Toolbar,
9481
10130
  VirtualizedDocumentContainer: () => VirtualizedDocumentContainer,
10131
+ applyRotation: () => applyRotation,
9482
10132
  clearHighlights: () => clearHighlights,
9483
10133
  clearStudentData: () => clearStudentData,
9484
10134
  cn: () => cn,
10135
+ countTextOnPage: () => countTextOnPage,
9485
10136
  createAgentAPI: () => createAgentAPI,
9486
10137
  createAgentStore: () => createAgentStore,
9487
10138
  createAnnotationStore: () => createAnnotationStore,
@@ -9490,6 +10141,7 @@ __export(index_exports, {
9490
10141
  createSearchStore: () => createSearchStore,
9491
10142
  createStudentStore: () => createStudentStore,
9492
10143
  createViewerStore: () => createViewerStore,
10144
+ doRectsIntersect: () => doRectsIntersect,
9493
10145
  downloadAnnotationsAsJSON: () => downloadAnnotationsAsJSON,
9494
10146
  downloadAnnotationsAsMarkdown: () => downloadAnnotationsAsMarkdown,
9495
10147
  downloadFile: () => downloadFile,
@@ -9497,25 +10149,39 @@ __export(index_exports, {
9497
10149
  exportAnnotationsAsMarkdown: () => exportAnnotationsAsMarkdown,
9498
10150
  exportHighlightsAsJSON: () => exportHighlightsAsJSON,
9499
10151
  exportHighlightsAsMarkdown: () => exportHighlightsAsMarkdown,
10152
+ extractPageText: () => extractPageText,
10153
+ findTextInDocument: () => findTextInDocument,
10154
+ findTextOnPage: () => findTextOnPage,
9500
10155
  generateDocumentId: () => generateDocumentId,
9501
10156
  getAllDocumentIds: () => getAllDocumentIds,
9502
10157
  getAllStudentDataDocumentIds: () => getAllStudentDataDocumentIds,
9503
10158
  getMetadata: () => getMetadata,
9504
10159
  getOutline: () => getOutline,
9505
10160
  getPage: () => getPage,
10161
+ getPageText: () => getPageText,
9506
10162
  getPageTextContent: () => getPageTextContent,
9507
10163
  getPluginManager: () => getPluginManager,
10164
+ getRectIntersection: () => getRectIntersection,
10165
+ getRotatedDimensions: () => getRotatedDimensions,
9508
10166
  getStorageStats: () => getStorageStats,
9509
10167
  importHighlightsFromJSON: () => importHighlightsFromJSON,
9510
10168
  initializePDFJS: () => initializePDFJS,
9511
10169
  isPDFJSInitialized: () => isPDFJSInitialized,
10170
+ isPointInRect: () => isPointInRect,
9512
10171
  loadDocument: () => loadDocument,
9513
10172
  loadHighlights: () => loadHighlights,
9514
10173
  loadStudentData: () => loadStudentData,
10174
+ mergeAdjacentRects: () => mergeAdjacentRects,
10175
+ pdfToPercent: () => pdfToPercent,
10176
+ pdfToViewport: () => pdfToViewport,
9515
10177
  pdfjsLib: () => pdfjsLib,
10178
+ percentToPDF: () => percentToPDF,
10179
+ percentToViewport: () => percentToViewport,
9516
10180
  quickViewer: () => quickViewer,
10181
+ removeRotation: () => removeRotation,
9517
10182
  saveHighlights: () => saveHighlights,
9518
10183
  saveStudentData: () => saveStudentData,
10184
+ scaleRect: () => scaleRect,
9519
10185
  useAgentContext: () => useAgentContext,
9520
10186
  useAgentStore: () => useAgentStore,
9521
10187
  useAnnotationStore: () => useAnnotationStore,
@@ -9537,6 +10203,8 @@ __export(index_exports, {
9537
10203
  useTouchGestures: () => useTouchGestures,
9538
10204
  useViewerStore: () => useViewerStore,
9539
10205
  useZoom: () => useZoom,
10206
+ viewportToPDF: () => viewportToPDF,
10207
+ viewportToPercent: () => viewportToPercent,
9540
10208
  withErrorBoundary: () => withErrorBoundary
9541
10209
  });
9542
10210
  module.exports = __toCommonJS(index_exports);
@@ -10826,11 +11494,248 @@ var Minimap = (0, import_react49.memo)(function Minimap2({
10826
11494
  // src/components/index.ts
10827
11495
  init_FloatingZoomControls2();
10828
11496
 
10829
- // src/components/ErrorBoundary/PDFErrorBoundary.tsx
11497
+ // src/components/PDFThumbnailNav/PDFThumbnailNav.tsx
10830
11498
  var import_react50 = require("react");
11499
+ init_hooks();
10831
11500
  init_utils();
10832
11501
  var import_jsx_runtime36 = require("react/jsx-runtime");
10833
- var PDFErrorBoundary = class extends import_react50.Component {
11502
+ var DEFAULT_WIDTH = 612;
11503
+ var DEFAULT_HEIGHT = 792;
11504
+ var PDFThumbnailNav = (0, import_react50.memo)(function PDFThumbnailNav2({
11505
+ thumbnailScale = 0.15,
11506
+ orientation = "vertical",
11507
+ maxVisible = 10,
11508
+ className,
11509
+ onThumbnailClick,
11510
+ gap = 8,
11511
+ showPageNumbers = true
11512
+ }) {
11513
+ const { document: document2, numPages, currentPage } = usePDFViewer();
11514
+ const { viewerStore } = usePDFViewerStores();
11515
+ const containerRef = (0, import_react50.useRef)(null);
11516
+ const [thumbnails, setThumbnails] = (0, import_react50.useState)(/* @__PURE__ */ new Map());
11517
+ const [visibleRange, setVisibleRange] = (0, import_react50.useState)({ start: 1, end: maxVisible });
11518
+ const renderQueueRef = (0, import_react50.useRef)(/* @__PURE__ */ new Set());
11519
+ const pageCache = (0, import_react50.useRef)(/* @__PURE__ */ new Map());
11520
+ const thumbnailWidth = Math.floor(DEFAULT_WIDTH * thumbnailScale);
11521
+ const thumbnailHeight = Math.floor(DEFAULT_HEIGHT * thumbnailScale);
11522
+ const updateVisibleRange = (0, import_react50.useCallback)(() => {
11523
+ if (!containerRef.current || numPages === 0) return;
11524
+ const container = containerRef.current;
11525
+ const isHorizontal2 = orientation === "horizontal";
11526
+ const scrollPosition = isHorizontal2 ? container.scrollLeft : container.scrollTop;
11527
+ const viewportSize = isHorizontal2 ? container.clientWidth : container.clientHeight;
11528
+ const itemSize = (isHorizontal2 ? thumbnailWidth : thumbnailHeight) + gap;
11529
+ const firstVisible = Math.max(1, Math.floor(scrollPosition / itemSize) + 1);
11530
+ const visibleCount = Math.ceil(viewportSize / itemSize) + 2;
11531
+ const lastVisible = Math.min(numPages, firstVisible + visibleCount);
11532
+ setVisibleRange({ start: firstVisible, end: lastVisible });
11533
+ }, [numPages, orientation, thumbnailWidth, thumbnailHeight, gap]);
11534
+ (0, import_react50.useEffect)(() => {
11535
+ const container = containerRef.current;
11536
+ if (!container) return;
11537
+ const handleScroll = () => {
11538
+ requestAnimationFrame(updateVisibleRange);
11539
+ };
11540
+ container.addEventListener("scroll", handleScroll, { passive: true });
11541
+ updateVisibleRange();
11542
+ return () => container.removeEventListener("scroll", handleScroll);
11543
+ }, [updateVisibleRange]);
11544
+ (0, import_react50.useEffect)(() => {
11545
+ if (!document2) {
11546
+ setThumbnails(/* @__PURE__ */ new Map());
11547
+ pageCache.current.clear();
11548
+ return;
11549
+ }
11550
+ const renderThumbnails = async () => {
11551
+ const newThumbnails = new Map(thumbnails);
11552
+ const pagesToRender = [];
11553
+ for (let i = visibleRange.start; i <= visibleRange.end; i++) {
11554
+ if (!newThumbnails.has(i) && !renderQueueRef.current.has(i)) {
11555
+ pagesToRender.push(i);
11556
+ renderQueueRef.current.add(i);
11557
+ }
11558
+ }
11559
+ for (const pageNum of pagesToRender) {
11560
+ try {
11561
+ let page = pageCache.current.get(pageNum);
11562
+ if (!page) {
11563
+ page = await document2.getPage(pageNum);
11564
+ pageCache.current.set(pageNum, page);
11565
+ }
11566
+ const viewport = page.getViewport({ scale: thumbnailScale });
11567
+ const canvas = window.document.createElement("canvas");
11568
+ canvas.width = Math.floor(viewport.width);
11569
+ canvas.height = Math.floor(viewport.height);
11570
+ const ctx = canvas.getContext("2d");
11571
+ if (ctx) {
11572
+ await page.render({
11573
+ canvasContext: ctx,
11574
+ viewport
11575
+ }).promise;
11576
+ newThumbnails.set(pageNum, {
11577
+ pageNumber: pageNum,
11578
+ canvas,
11579
+ width: canvas.width,
11580
+ height: canvas.height
11581
+ });
11582
+ }
11583
+ } catch (error) {
11584
+ console.error(`Failed to render thumbnail for page ${pageNum}:`, error);
11585
+ } finally {
11586
+ renderQueueRef.current.delete(pageNum);
11587
+ }
11588
+ }
11589
+ if (pagesToRender.length > 0) {
11590
+ setThumbnails(newThumbnails);
11591
+ }
11592
+ };
11593
+ renderThumbnails();
11594
+ }, [document2, visibleRange, thumbnailScale, thumbnails]);
11595
+ (0, import_react50.useEffect)(() => {
11596
+ if (!containerRef.current || numPages === 0) return;
11597
+ const container = containerRef.current;
11598
+ const isHorizontal2 = orientation === "horizontal";
11599
+ const itemSize = (isHorizontal2 ? thumbnailWidth : thumbnailHeight) + gap;
11600
+ const targetPosition = (currentPage - 1) * itemSize;
11601
+ const scrollPosition = isHorizontal2 ? container.scrollLeft : container.scrollTop;
11602
+ const viewportSize = isHorizontal2 ? container.clientWidth : container.clientHeight;
11603
+ if (targetPosition < scrollPosition || targetPosition + itemSize > scrollPosition + viewportSize) {
11604
+ const targetScroll = targetPosition - (viewportSize - itemSize) / 2;
11605
+ container.scrollTo({
11606
+ [isHorizontal2 ? "left" : "top"]: Math.max(0, targetScroll),
11607
+ behavior: "smooth"
11608
+ });
11609
+ }
11610
+ }, [currentPage, numPages, orientation, thumbnailWidth, thumbnailHeight, gap]);
11611
+ const handleThumbnailClick = (0, import_react50.useCallback)((pageNum) => {
11612
+ onThumbnailClick?.(pageNum);
11613
+ viewerStore.getState().requestScrollToPage(pageNum, "smooth");
11614
+ }, [onThumbnailClick, viewerStore]);
11615
+ if (!document2 || numPages === 0) {
11616
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11617
+ "div",
11618
+ {
11619
+ className: cn(
11620
+ "pdf-thumbnail-nav",
11621
+ "flex items-center justify-center",
11622
+ "bg-gray-100 dark:bg-gray-800",
11623
+ "text-gray-500 dark:text-gray-400",
11624
+ "text-sm",
11625
+ className
11626
+ ),
11627
+ style: {
11628
+ width: orientation === "vertical" ? thumbnailWidth + 24 : "100%",
11629
+ height: orientation === "horizontal" ? thumbnailHeight + 40 : "100%"
11630
+ },
11631
+ children: "No document"
11632
+ }
11633
+ );
11634
+ }
11635
+ const isHorizontal = orientation === "horizontal";
11636
+ const totalSize = numPages * ((isHorizontal ? thumbnailWidth : thumbnailHeight) + gap) - gap;
11637
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11638
+ "div",
11639
+ {
11640
+ ref: containerRef,
11641
+ className: cn(
11642
+ "pdf-thumbnail-nav",
11643
+ "overflow-auto",
11644
+ "bg-gray-100 dark:bg-gray-800",
11645
+ isHorizontal ? "flex-row" : "flex-col",
11646
+ className
11647
+ ),
11648
+ style: {
11649
+ ...isHorizontal ? { overflowX: "auto", overflowY: "hidden" } : { overflowX: "hidden", overflowY: "auto" }
11650
+ },
11651
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11652
+ "div",
11653
+ {
11654
+ className: cn(
11655
+ "relative",
11656
+ isHorizontal ? "flex flex-row items-center" : "flex flex-col items-center"
11657
+ ),
11658
+ style: {
11659
+ [isHorizontal ? "width" : "height"]: totalSize,
11660
+ [isHorizontal ? "minWidth" : "minHeight"]: totalSize,
11661
+ padding: gap / 2,
11662
+ gap
11663
+ },
11664
+ children: Array.from({ length: numPages }, (_, i) => i + 1).map((pageNum) => {
11665
+ const thumbnail = thumbnails.get(pageNum);
11666
+ const isActive = pageNum === currentPage;
11667
+ const isVisible = pageNum >= visibleRange.start && pageNum <= visibleRange.end;
11668
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
11669
+ "div",
11670
+ {
11671
+ className: cn(
11672
+ "pdf-thumbnail",
11673
+ "flex-shrink-0 cursor-pointer transition-all duration-200",
11674
+ "border-2 rounded shadow-sm hover:shadow-md",
11675
+ isActive ? "border-blue-500 ring-2 ring-blue-200 dark:ring-blue-800" : "border-gray-300 dark:border-gray-600 hover:border-blue-400"
11676
+ ),
11677
+ style: {
11678
+ width: thumbnailWidth,
11679
+ height: thumbnailHeight + (showPageNumbers ? 24 : 0)
11680
+ },
11681
+ onClick: () => handleThumbnailClick(pageNum),
11682
+ role: "button",
11683
+ tabIndex: 0,
11684
+ "aria-label": `Go to page ${pageNum}`,
11685
+ "aria-current": isActive ? "page" : void 0,
11686
+ onKeyDown: (e) => {
11687
+ if (e.key === "Enter" || e.key === " ") {
11688
+ e.preventDefault();
11689
+ handleThumbnailClick(pageNum);
11690
+ }
11691
+ },
11692
+ children: [
11693
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11694
+ "div",
11695
+ {
11696
+ className: "relative bg-white dark:bg-gray-700",
11697
+ style: {
11698
+ width: thumbnailWidth,
11699
+ height: thumbnailHeight
11700
+ },
11701
+ children: isVisible && thumbnail?.canvas ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11702
+ "img",
11703
+ {
11704
+ src: thumbnail.canvas.toDataURL(),
11705
+ alt: `Page ${pageNum}`,
11706
+ className: "w-full h-full object-contain",
11707
+ loading: "lazy"
11708
+ }
11709
+ ) : /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("div", { className: "absolute inset-0 flex items-center justify-center text-gray-400 dark:text-gray-500 text-xs", children: pageNum })
11710
+ }
11711
+ ),
11712
+ showPageNumbers && /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11713
+ "div",
11714
+ {
11715
+ className: cn(
11716
+ "text-center text-xs py-1",
11717
+ "bg-gray-50 dark:bg-gray-700",
11718
+ isActive ? "text-blue-600 dark:text-blue-400 font-medium" : "text-gray-600 dark:text-gray-400"
11719
+ ),
11720
+ children: pageNum
11721
+ }
11722
+ )
11723
+ ]
11724
+ },
11725
+ pageNum
11726
+ );
11727
+ })
11728
+ }
11729
+ )
11730
+ }
11731
+ );
11732
+ });
11733
+
11734
+ // src/components/ErrorBoundary/PDFErrorBoundary.tsx
11735
+ var import_react51 = require("react");
11736
+ init_utils();
11737
+ var import_jsx_runtime37 = require("react/jsx-runtime");
11738
+ var PDFErrorBoundary = class extends import_react51.Component {
10834
11739
  constructor(props) {
10835
11740
  super(props);
10836
11741
  this.handleReset = () => {
@@ -10857,7 +11762,7 @@ var PDFErrorBoundary = class extends import_react50.Component {
10857
11762
  return fallback;
10858
11763
  }
10859
11764
  if (showDefaultUI) {
10860
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11765
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
10861
11766
  DefaultErrorUI,
10862
11767
  {
10863
11768
  error,
@@ -10876,7 +11781,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10876
11781
  const isNetworkError = error.message.includes("fetch") || error.message.includes("network") || error.message.includes("Failed to load");
10877
11782
  let title = "Something went wrong";
10878
11783
  let description = error.message;
10879
- let icon = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11784
+ let icon = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
10880
11785
  "path",
10881
11786
  {
10882
11787
  strokeLinecap: "round",
@@ -10888,7 +11793,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10888
11793
  if (isPDFError) {
10889
11794
  title = "Unable to load PDF";
10890
11795
  description = "The PDF file could not be loaded. It may be corrupted or in an unsupported format.";
10891
- icon = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11796
+ icon = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
10892
11797
  "path",
10893
11798
  {
10894
11799
  strokeLinecap: "round",
@@ -10900,7 +11805,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10900
11805
  } else if (isNetworkError) {
10901
11806
  title = "Network error";
10902
11807
  description = "Unable to fetch the PDF file. Please check your internet connection and try again.";
10903
- icon = /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11808
+ icon = /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
10904
11809
  "path",
10905
11810
  {
10906
11811
  strokeLinecap: "round",
@@ -10910,7 +11815,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10910
11815
  }
10911
11816
  ) });
10912
11817
  }
10913
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
11818
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
10914
11819
  "div",
10915
11820
  {
10916
11821
  className: cn(
@@ -10923,14 +11828,14 @@ function DefaultErrorUI({ error, onReset, className }) {
10923
11828
  ),
10924
11829
  children: [
10925
11830
  icon,
10926
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("h2", { className: "mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100", children: title }),
10927
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("p", { className: "mt-2 text-sm text-gray-600 dark:text-gray-400 max-w-md", children: description }),
10928
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("details", { className: "mt-4 text-left max-w-md w-full", children: [
10929
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("summary", { className: "cursor-pointer text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200", children: "Technical details" }),
10930
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("pre", { className: "mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto", children: error.stack || error.message })
11831
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("h2", { className: "mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100", children: title }),
11832
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("p", { className: "mt-2 text-sm text-gray-600 dark:text-gray-400 max-w-md", children: description }),
11833
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("details", { className: "mt-4 text-left max-w-md w-full", children: [
11834
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("summary", { className: "cursor-pointer text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200", children: "Technical details" }),
11835
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("pre", { className: "mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto", children: error.stack || error.message })
10931
11836
  ] }),
10932
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)("div", { className: "mt-6 flex gap-3", children: [
10933
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11837
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { className: "mt-6 flex gap-3", children: [
11838
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
10934
11839
  "button",
10935
11840
  {
10936
11841
  onClick: onReset,
@@ -10944,7 +11849,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10944
11849
  children: "Try again"
10945
11850
  }
10946
11851
  ),
10947
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
11852
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
10948
11853
  "button",
10949
11854
  {
10950
11855
  onClick: () => window.location.reload(),
@@ -10964,7 +11869,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10964
11869
  );
10965
11870
  }
10966
11871
  function withErrorBoundary({ component, ...props }) {
10967
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(PDFErrorBoundary, { ...props, children: component });
11872
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(PDFErrorBoundary, { ...props, children: component });
10968
11873
  }
10969
11874
 
10970
11875
  // src/index.ts
@@ -10999,6 +11904,7 @@ init_utils();
10999
11904
  OutlinePanel,
11000
11905
  PDFErrorBoundary,
11001
11906
  PDFPage,
11907
+ PDFThumbnailNav,
11002
11908
  PDFViewer,
11003
11909
  PDFViewerClient,
11004
11910
  PDFViewerContext,
@@ -11017,9 +11923,11 @@ init_utils();
11017
11923
  ThumbnailPanel,
11018
11924
  Toolbar,
11019
11925
  VirtualizedDocumentContainer,
11926
+ applyRotation,
11020
11927
  clearHighlights,
11021
11928
  clearStudentData,
11022
11929
  cn,
11930
+ countTextOnPage,
11023
11931
  createAgentAPI,
11024
11932
  createAgentStore,
11025
11933
  createAnnotationStore,
@@ -11028,6 +11936,7 @@ init_utils();
11028
11936
  createSearchStore,
11029
11937
  createStudentStore,
11030
11938
  createViewerStore,
11939
+ doRectsIntersect,
11031
11940
  downloadAnnotationsAsJSON,
11032
11941
  downloadAnnotationsAsMarkdown,
11033
11942
  downloadFile,
@@ -11035,25 +11944,39 @@ init_utils();
11035
11944
  exportAnnotationsAsMarkdown,
11036
11945
  exportHighlightsAsJSON,
11037
11946
  exportHighlightsAsMarkdown,
11947
+ extractPageText,
11948
+ findTextInDocument,
11949
+ findTextOnPage,
11038
11950
  generateDocumentId,
11039
11951
  getAllDocumentIds,
11040
11952
  getAllStudentDataDocumentIds,
11041
11953
  getMetadata,
11042
11954
  getOutline,
11043
11955
  getPage,
11956
+ getPageText,
11044
11957
  getPageTextContent,
11045
11958
  getPluginManager,
11959
+ getRectIntersection,
11960
+ getRotatedDimensions,
11046
11961
  getStorageStats,
11047
11962
  importHighlightsFromJSON,
11048
11963
  initializePDFJS,
11049
11964
  isPDFJSInitialized,
11965
+ isPointInRect,
11050
11966
  loadDocument,
11051
11967
  loadHighlights,
11052
11968
  loadStudentData,
11969
+ mergeAdjacentRects,
11970
+ pdfToPercent,
11971
+ pdfToViewport,
11053
11972
  pdfjsLib,
11973
+ percentToPDF,
11974
+ percentToViewport,
11054
11975
  quickViewer,
11976
+ removeRotation,
11055
11977
  saveHighlights,
11056
11978
  saveStudentData,
11979
+ scaleRect,
11057
11980
  useAgentContext,
11058
11981
  useAgentStore,
11059
11982
  useAnnotationStore,
@@ -11075,6 +11998,8 @@ init_utils();
11075
11998
  useTouchGestures,
11076
11999
  useViewerStore,
11077
12000
  useZoom,
12001
+ viewportToPDF,
12002
+ viewportToPercent,
11078
12003
  withErrorBoundary
11079
12004
  });
11080
12005
  //# sourceMappingURL=index.cjs.map