pdfjs-reader-core 0.1.5 → 0.2.1

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
@@ -310,6 +310,9 @@ var init_highlight_storage = __esm({
310
310
 
311
311
  // src/store/viewer-store.ts
312
312
  import { createStore } from "zustand/vanilla";
313
+ function generateRequestId() {
314
+ return `scroll-${Date.now()}-${++requestCounter}`;
315
+ }
313
316
  function createViewerStore(initialOverrides = {}) {
314
317
  return createStore()((set, get) => ({
315
318
  ...initialState,
@@ -362,6 +365,46 @@ function createViewerStore(initialOverrides = {}) {
362
365
  set({ currentPage: currentPage - 1 });
363
366
  }
364
367
  },
368
+ // Scroll coordination actions
369
+ requestScrollToPage: (page, behavior = "smooth") => {
370
+ const { numPages } = get();
371
+ const validPage = Math.max(1, Math.min(page, numPages));
372
+ return new Promise((resolve) => {
373
+ const requestId = generateRequestId();
374
+ const timeoutId = setTimeout(() => {
375
+ const callback = scrollCallbacks.get(requestId);
376
+ if (callback) {
377
+ scrollCallbacks.delete(requestId);
378
+ callback.resolve();
379
+ }
380
+ const currentRequest = get().scrollToPageRequest;
381
+ if (currentRequest?.requestId === requestId) {
382
+ set({ scrollToPageRequest: null });
383
+ }
384
+ }, SCROLL_TIMEOUT_MS);
385
+ scrollCallbacks.set(requestId, { resolve, timeoutId });
386
+ set({
387
+ currentPage: validPage,
388
+ scrollToPageRequest: {
389
+ page: validPage,
390
+ requestId,
391
+ behavior
392
+ }
393
+ });
394
+ });
395
+ },
396
+ completeScrollRequest: (requestId) => {
397
+ const callback = scrollCallbacks.get(requestId);
398
+ if (callback) {
399
+ clearTimeout(callback.timeoutId);
400
+ scrollCallbacks.delete(requestId);
401
+ callback.resolve();
402
+ }
403
+ const currentRequest = get().scrollToPageRequest;
404
+ if (currentRequest?.requestId === requestId) {
405
+ set({ scrollToPageRequest: null });
406
+ }
407
+ },
365
408
  // Zoom actions
366
409
  setScale: (scale) => {
367
410
  const clampedScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale));
@@ -428,13 +471,16 @@ function createViewerStore(initialOverrides = {}) {
428
471
  }
429
472
  }));
430
473
  }
431
- var ZOOM_LEVELS, MIN_SCALE, MAX_SCALE, initialState;
474
+ var ZOOM_LEVELS, MIN_SCALE, MAX_SCALE, SCROLL_TIMEOUT_MS, scrollCallbacks, requestCounter, initialState;
432
475
  var init_viewer_store = __esm({
433
476
  "src/store/viewer-store.ts"() {
434
477
  "use strict";
435
478
  ZOOM_LEVELS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4];
436
479
  MIN_SCALE = 0.1;
437
480
  MAX_SCALE = 10;
481
+ SCROLL_TIMEOUT_MS = 3e3;
482
+ scrollCallbacks = /* @__PURE__ */ new Map();
483
+ requestCounter = 0;
438
484
  initialState = {
439
485
  // Document state
440
486
  document: null,
@@ -445,6 +491,8 @@ var init_viewer_store = __esm({
445
491
  currentPage: 1,
446
492
  scale: 1,
447
493
  rotation: 0,
494
+ // Scroll coordination
495
+ scrollToPageRequest: null,
448
496
  // UI state
449
497
  viewMode: "single",
450
498
  scrollMode: "single",
@@ -1253,6 +1301,267 @@ var init_agent_api = __esm({
1253
1301
  }
1254
1302
  });
1255
1303
 
1304
+ // src/utils/text-search.ts
1305
+ async function extractPageText(document2, pageNumber) {
1306
+ const page = await document2.getPage(pageNumber);
1307
+ const textContent = await page.getTextContent();
1308
+ const viewport = page.getViewport({ scale: 1 });
1309
+ let fullText = "";
1310
+ const textItems = [];
1311
+ for (const item of textContent.items) {
1312
+ if ("str" in item && item.str) {
1313
+ textItems.push({
1314
+ text: item.str,
1315
+ transform: item.transform,
1316
+ width: item.width ?? 0,
1317
+ height: item.height ?? 12
1318
+ });
1319
+ fullText += item.str;
1320
+ }
1321
+ }
1322
+ return { fullText, textItems, viewport };
1323
+ }
1324
+ function calculateMatchRects(textItems, startOffset, length, viewport) {
1325
+ const rects = [];
1326
+ let currentOffset = 0;
1327
+ for (const item of textItems) {
1328
+ const itemStart = currentOffset;
1329
+ const itemEnd = currentOffset + item.text.length;
1330
+ if (itemEnd > startOffset && itemStart < startOffset + length) {
1331
+ const [, , c, d, tx, ty] = item.transform;
1332
+ const x = tx;
1333
+ const y = viewport.height - ty;
1334
+ const height = Math.sqrt(c * c + d * d);
1335
+ const matchStartInItem = Math.max(0, startOffset - itemStart);
1336
+ const matchEndInItem = Math.min(item.text.length, startOffset + length - itemStart);
1337
+ const charWidth = item.text.length > 0 ? item.width / item.text.length : item.width;
1338
+ const matchWidth = charWidth * (matchEndInItem - matchStartInItem);
1339
+ const matchX = x + charWidth * matchStartInItem;
1340
+ const yOffset = height * 0.15;
1341
+ rects.push({
1342
+ x: matchX,
1343
+ y: y - height + yOffset,
1344
+ width: matchWidth,
1345
+ height
1346
+ });
1347
+ }
1348
+ currentOffset = itemEnd;
1349
+ }
1350
+ return rects;
1351
+ }
1352
+ async function findTextOnPage(document2, pageNumber, query, options = {}) {
1353
+ const { caseSensitive = false, wholeWord = false } = options;
1354
+ if (!query || pageNumber < 1 || pageNumber > document2.numPages) {
1355
+ return [];
1356
+ }
1357
+ const { fullText, textItems, viewport } = await extractPageText(document2, pageNumber);
1358
+ const matches = [];
1359
+ const searchText = caseSensitive ? query : query.toLowerCase();
1360
+ const textToSearch = caseSensitive ? fullText : fullText.toLowerCase();
1361
+ let startIndex = 0;
1362
+ while (true) {
1363
+ const matchIndex = textToSearch.indexOf(searchText, startIndex);
1364
+ if (matchIndex === -1) break;
1365
+ if (wholeWord) {
1366
+ const beforeChar = matchIndex > 0 ? textToSearch[matchIndex - 1] : " ";
1367
+ const afterChar = matchIndex + query.length < textToSearch.length ? textToSearch[matchIndex + query.length] : " ";
1368
+ if (/\w/.test(beforeChar) || /\w/.test(afterChar)) {
1369
+ startIndex = matchIndex + 1;
1370
+ continue;
1371
+ }
1372
+ }
1373
+ const matchRects = calculateMatchRects(textItems, matchIndex, query.length, viewport);
1374
+ if (matchRects.length > 0) {
1375
+ matches.push({
1376
+ text: fullText.substring(matchIndex, matchIndex + query.length),
1377
+ rects: matchRects,
1378
+ pageNumber,
1379
+ startIndex: matchIndex
1380
+ });
1381
+ }
1382
+ startIndex = matchIndex + 1;
1383
+ }
1384
+ return matches;
1385
+ }
1386
+ async function findTextInDocument(document2, query, options = {}) {
1387
+ const { pageRange, ...findOptions } = options;
1388
+ const pagesToSearch = pageRange ?? Array.from({ length: document2.numPages }, (_, i) => i + 1);
1389
+ const allMatches = [];
1390
+ for (const pageNum of pagesToSearch) {
1391
+ if (pageNum < 1 || pageNum > document2.numPages) continue;
1392
+ try {
1393
+ const matches = await findTextOnPage(document2, pageNum, query, findOptions);
1394
+ allMatches.push(...matches);
1395
+ } catch {
1396
+ }
1397
+ }
1398
+ return allMatches;
1399
+ }
1400
+ function mergeAdjacentRects(rects) {
1401
+ if (rects.length === 0) return [];
1402
+ const sorted = [...rects].sort((a, b) => a.y - b.y || a.x - b.x);
1403
+ const merged = [];
1404
+ let current = { ...sorted[0] };
1405
+ for (let i = 1; i < sorted.length; i++) {
1406
+ const rect = sorted[i];
1407
+ if (Math.abs(rect.y - current.y) < 2 && rect.x <= current.x + current.width + 2) {
1408
+ const newRight = Math.max(current.x + current.width, rect.x + rect.width);
1409
+ current.width = newRight - current.x;
1410
+ current.height = Math.max(current.height, rect.height);
1411
+ } else {
1412
+ merged.push(current);
1413
+ current = { ...rect };
1414
+ }
1415
+ }
1416
+ merged.push(current);
1417
+ return merged;
1418
+ }
1419
+ async function getPageText(document2, pageNumber) {
1420
+ if (pageNumber < 1 || pageNumber > document2.numPages) {
1421
+ return "";
1422
+ }
1423
+ const page = await document2.getPage(pageNumber);
1424
+ const textContent = await page.getTextContent();
1425
+ return textContent.items.filter((item) => "str" in item).map((item) => item.str).join("");
1426
+ }
1427
+ async function countTextOnPage(document2, pageNumber, query, options = {}) {
1428
+ const { caseSensitive = false, wholeWord = false } = options;
1429
+ if (!query || pageNumber < 1 || pageNumber > document2.numPages) {
1430
+ return 0;
1431
+ }
1432
+ const text = await getPageText(document2, pageNumber);
1433
+ const searchText = caseSensitive ? query : query.toLowerCase();
1434
+ const textToSearch = caseSensitive ? text : text.toLowerCase();
1435
+ let count = 0;
1436
+ let startIndex = 0;
1437
+ while (true) {
1438
+ const matchIndex = textToSearch.indexOf(searchText, startIndex);
1439
+ if (matchIndex === -1) break;
1440
+ if (wholeWord) {
1441
+ const beforeChar = matchIndex > 0 ? textToSearch[matchIndex - 1] : " ";
1442
+ const afterChar = matchIndex + query.length < textToSearch.length ? textToSearch[matchIndex + query.length] : " ";
1443
+ if (/\w/.test(beforeChar) || /\w/.test(afterChar)) {
1444
+ startIndex = matchIndex + 1;
1445
+ continue;
1446
+ }
1447
+ }
1448
+ count++;
1449
+ startIndex = matchIndex + 1;
1450
+ }
1451
+ return count;
1452
+ }
1453
+ var init_text_search = __esm({
1454
+ "src/utils/text-search.ts"() {
1455
+ "use strict";
1456
+ }
1457
+ });
1458
+
1459
+ // src/utils/coordinates.ts
1460
+ function pdfToViewport(x, y, scale, pageHeight) {
1461
+ return {
1462
+ x: x * scale,
1463
+ y: (pageHeight - y) * scale
1464
+ };
1465
+ }
1466
+ function viewportToPDF(x, y, scale, pageHeight) {
1467
+ return {
1468
+ x: x / scale,
1469
+ y: pageHeight - y / scale
1470
+ };
1471
+ }
1472
+ function percentToPDF(xPercent, yPercent, pageWidth, pageHeight) {
1473
+ return {
1474
+ x: xPercent / 100 * pageWidth,
1475
+ y: yPercent / 100 * pageHeight
1476
+ };
1477
+ }
1478
+ function pdfToPercent(x, y, pageWidth, pageHeight) {
1479
+ return {
1480
+ x: x / pageWidth * 100,
1481
+ y: y / pageHeight * 100
1482
+ };
1483
+ }
1484
+ function percentToViewport(xPercent, yPercent, pageWidth, pageHeight, scale) {
1485
+ return {
1486
+ x: xPercent / 100 * pageWidth * scale,
1487
+ y: yPercent / 100 * pageHeight * scale
1488
+ };
1489
+ }
1490
+ function viewportToPercent(x, y, pageWidth, pageHeight, scale) {
1491
+ return {
1492
+ x: x / (pageWidth * scale) * 100,
1493
+ y: y / (pageHeight * scale) * 100
1494
+ };
1495
+ }
1496
+ function applyRotation(x, y, rotation, pageWidth, pageHeight) {
1497
+ const normalizedRotation = (rotation % 360 + 360) % 360;
1498
+ switch (normalizedRotation) {
1499
+ case 90:
1500
+ return { x: y, y: pageWidth - x };
1501
+ case 180:
1502
+ return { x: pageWidth - x, y: pageHeight - y };
1503
+ case 270:
1504
+ return { x: pageHeight - y, y: x };
1505
+ default:
1506
+ return { x, y };
1507
+ }
1508
+ }
1509
+ function removeRotation(x, y, rotation, pageWidth, pageHeight) {
1510
+ const normalizedRotation = (rotation % 360 + 360) % 360;
1511
+ switch (normalizedRotation) {
1512
+ case 90:
1513
+ return { x: pageWidth - y, y: x };
1514
+ case 180:
1515
+ return { x: pageWidth - x, y: pageHeight - y };
1516
+ case 270:
1517
+ return { x: y, y: pageHeight - x };
1518
+ default:
1519
+ return { x, y };
1520
+ }
1521
+ }
1522
+ function getRotatedDimensions(width, height, rotation) {
1523
+ const normalizedRotation = (rotation % 360 + 360) % 360;
1524
+ if (normalizedRotation === 90 || normalizedRotation === 270) {
1525
+ return { width: height, height: width };
1526
+ }
1527
+ return { width, height };
1528
+ }
1529
+ function scaleRect(rect, fromScale, toScale) {
1530
+ const ratio = toScale / fromScale;
1531
+ return {
1532
+ x: rect.x * ratio,
1533
+ y: rect.y * ratio,
1534
+ width: rect.width * ratio,
1535
+ height: rect.height * ratio
1536
+ };
1537
+ }
1538
+ function isPointInRect(point, rect) {
1539
+ return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
1540
+ }
1541
+ function doRectsIntersect(rectA, rectB) {
1542
+ 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);
1543
+ }
1544
+ function getRectIntersection(rectA, rectB) {
1545
+ const x = Math.max(rectA.x, rectB.x);
1546
+ const y = Math.max(rectA.y, rectB.y);
1547
+ const right = Math.min(rectA.x + rectA.width, rectB.x + rectB.width);
1548
+ const bottom = Math.min(rectA.y + rectA.height, rectB.y + rectB.height);
1549
+ if (right <= x || bottom <= y) {
1550
+ return null;
1551
+ }
1552
+ return {
1553
+ x,
1554
+ y,
1555
+ width: right - x,
1556
+ height: bottom - y
1557
+ };
1558
+ }
1559
+ var init_coordinates = __esm({
1560
+ "src/utils/coordinates.ts"() {
1561
+ "use strict";
1562
+ }
1563
+ });
1564
+
1256
1565
  // src/utils/index.ts
1257
1566
  var init_utils = __esm({
1258
1567
  "src/utils/index.ts"() {
@@ -1265,6 +1574,8 @@ var init_utils = __esm({
1265
1574
  init_export_annotations();
1266
1575
  init_student_storage();
1267
1576
  init_agent_api();
1577
+ init_text_search();
1578
+ init_coordinates();
1268
1579
  }
1269
1580
  });
1270
1581
 
@@ -1321,7 +1632,7 @@ function createSearchStore(initialOverrides = {}) {
1321
1632
  }
1322
1633
  }
1323
1634
  const matchText = pageText.substring(startIndex, startIndex + query.length);
1324
- const rects = calculateMatchRects(textItems, startIndex, query.length, viewport);
1635
+ const rects = calculateMatchRects2(textItems, startIndex, query.length, viewport);
1325
1636
  results.push({
1326
1637
  pageNumber: pageNum,
1327
1638
  matchIndex: matchIndex++,
@@ -1382,7 +1693,7 @@ function createSearchStore(initialOverrides = {}) {
1382
1693
  }
1383
1694
  }));
1384
1695
  }
1385
- function calculateMatchRects(textItems, startOffset, length, viewport) {
1696
+ function calculateMatchRects2(textItems, startOffset, length, viewport) {
1386
1697
  const rects = [];
1387
1698
  let currentOffset = 0;
1388
1699
  for (const item of textItems) {
@@ -1397,9 +1708,10 @@ function calculateMatchRects(textItems, startOffset, length, viewport) {
1397
1708
  const matchEndInItem = Math.min(item.text.length, startOffset + length - itemStart);
1398
1709
  const matchWidth = item.width / item.text.length * (matchEndInItem - matchStartInItem);
1399
1710
  const matchX = x + item.width / item.text.length * matchStartInItem;
1711
+ const yOffset = height * 0.15;
1400
1712
  rects.push({
1401
1713
  x: matchX,
1402
- y: y - height,
1714
+ y: y - height + yOffset,
1403
1715
  width: matchWidth,
1404
1716
  height
1405
1717
  });
@@ -7889,6 +8201,8 @@ var init_DocumentContainer = __esm({
7889
8201
  nextPage,
7890
8202
  previousPage
7891
8203
  } = usePDFViewer();
8204
+ const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
8205
+ const { viewerStore } = usePDFViewerStores();
7892
8206
  const [currentPageObj, setCurrentPageObj] = useState18(null);
7893
8207
  const [isLoadingPage, setIsLoadingPage] = useState18(false);
7894
8208
  const containerRef = useRef16(null);
@@ -7954,6 +8268,11 @@ var init_DocumentContainer = __esm({
7954
8268
  const page = await document2.getPage(currentPage);
7955
8269
  if (!cancelled && document2 === documentRef.current) {
7956
8270
  setCurrentPageObj(page);
8271
+ if (scrollToPageRequest && scrollToPageRequest.page === currentPage) {
8272
+ requestAnimationFrame(() => {
8273
+ viewerStore.getState().completeScrollRequest(scrollToPageRequest.requestId);
8274
+ });
8275
+ }
7957
8276
  }
7958
8277
  } catch (error) {
7959
8278
  if (!cancelled) {
@@ -7972,7 +8291,7 @@ var init_DocumentContainer = __esm({
7972
8291
  return () => {
7973
8292
  cancelled = true;
7974
8293
  };
7975
- }, [document2, currentPage]);
8294
+ }, [document2, currentPage, scrollToPageRequest, viewerStore]);
7976
8295
  const getPageElement = useCallback29(() => {
7977
8296
  return containerRef.current?.querySelector(`[data-page-number="${currentPage}"]`);
7978
8297
  }, [currentPage]);
@@ -8114,6 +8433,8 @@ var init_VirtualizedDocumentContainer = __esm({
8114
8433
  nextPage,
8115
8434
  previousPage
8116
8435
  } = usePDFViewer();
8436
+ const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
8437
+ const { viewerStore } = usePDFViewerStores();
8117
8438
  const containerRef = useRef17(null);
8118
8439
  const scrollContainerRef = useRef17(null);
8119
8440
  const documentRef = useRef17(null);
@@ -8251,6 +8572,45 @@ var init_VirtualizedDocumentContainer = __esm({
8251
8572
  loadPages();
8252
8573
  }, [document2, visiblePages, pageObjects]);
8253
8574
  useEffect20(() => {
8575
+ if (!scrollToPageRequest || !scrollContainerRef.current || pageInfos.length === 0) return;
8576
+ const { page, requestId, behavior } = scrollToPageRequest;
8577
+ const pageInfo = pageInfos.find((p) => p.pageNumber === page);
8578
+ if (!pageInfo) {
8579
+ viewerStore.getState().completeScrollRequest(requestId);
8580
+ return;
8581
+ }
8582
+ const container = scrollContainerRef.current;
8583
+ const targetScroll = pageInfo.top - pageGap;
8584
+ const scrollTop = container.scrollTop;
8585
+ const viewportHeight = container.clientHeight;
8586
+ const isVisible = targetScroll >= scrollTop && pageInfo.top + pageInfo.height <= scrollTop + viewportHeight;
8587
+ if (isVisible) {
8588
+ viewerStore.getState().completeScrollRequest(requestId);
8589
+ return;
8590
+ }
8591
+ container.scrollTo({
8592
+ top: targetScroll,
8593
+ behavior
8594
+ });
8595
+ if (behavior === "instant") {
8596
+ requestAnimationFrame(() => {
8597
+ viewerStore.getState().completeScrollRequest(requestId);
8598
+ });
8599
+ } else {
8600
+ let scrollEndTimeout;
8601
+ const handleScrollEnd = () => {
8602
+ clearTimeout(scrollEndTimeout);
8603
+ scrollEndTimeout = setTimeout(() => {
8604
+ container.removeEventListener("scroll", handleScrollEnd);
8605
+ viewerStore.getState().completeScrollRequest(requestId);
8606
+ }, 100);
8607
+ };
8608
+ container.addEventListener("scroll", handleScrollEnd, { passive: true });
8609
+ handleScrollEnd();
8610
+ }
8611
+ }, [scrollToPageRequest, pageInfos, pageGap, viewerStore]);
8612
+ useEffect20(() => {
8613
+ if (scrollToPageRequest) return;
8254
8614
  if (!scrollContainerRef.current || pageInfos.length === 0) return;
8255
8615
  const pageInfo = pageInfos.find((p) => p.pageNumber === currentPage);
8256
8616
  if (pageInfo) {
@@ -8265,7 +8625,7 @@ var init_VirtualizedDocumentContainer = __esm({
8265
8625
  });
8266
8626
  }
8267
8627
  }
8268
- }, [currentPage, pageInfos, pageGap]);
8628
+ }, [currentPage, pageInfos, pageGap, scrollToPageRequest]);
8269
8629
  const handlePinchZoom = useCallback30(
8270
8630
  (pinchScale) => {
8271
8631
  const newScale = Math.max(0.25, Math.min(4, baseScaleRef.current * pinchScale));
@@ -8472,6 +8832,8 @@ var init_DualPageContainer = __esm({
8472
8832
  setScale,
8473
8833
  goToPage
8474
8834
  } = usePDFViewer();
8835
+ const scrollToPageRequest = useViewerStore((s) => s.scrollToPageRequest);
8836
+ const { viewerStore } = usePDFViewerStores();
8475
8837
  const containerRef = useRef18(null);
8476
8838
  const documentRef = useRef18(null);
8477
8839
  const baseScaleRef = useRef18(scale);
@@ -8557,6 +8919,14 @@ var init_DualPageContainer = __esm({
8557
8919
  if (!cancelled) {
8558
8920
  setLeftPage(left);
8559
8921
  setRightPage(right);
8922
+ if (scrollToPageRequest) {
8923
+ const requestedPage = scrollToPageRequest.page;
8924
+ if (requestedPage === spread2.left || requestedPage === spread2.right) {
8925
+ requestAnimationFrame(() => {
8926
+ viewerStore.getState().completeScrollRequest(scrollToPageRequest.requestId);
8927
+ });
8928
+ }
8929
+ }
8560
8930
  }
8561
8931
  } catch (error) {
8562
8932
  if (!cancelled) {
@@ -8572,7 +8942,7 @@ var init_DualPageContainer = __esm({
8572
8942
  return () => {
8573
8943
  cancelled = true;
8574
8944
  };
8575
- }, [document2, currentPage, getSpreadPages]);
8945
+ }, [document2, currentPage, getSpreadPages, scrollToPageRequest, viewerStore]);
8576
8946
  const goToPreviousSpread = useCallback31(() => {
8577
8947
  const spread2 = getSpreadPages(currentPage);
8578
8948
  const leftmostPage = spread2.left || spread2.right || currentPage;
@@ -8769,10 +9139,14 @@ var init_FloatingZoomControls = __esm({
8769
9139
  const scale = useViewerStore((s) => s.scale);
8770
9140
  const document2 = useViewerStore((s) => s.document);
8771
9141
  const handleZoomIn = useCallback32(() => {
8772
- viewerStore.getState().zoomIn();
9142
+ const currentScale = viewerStore.getState().scale;
9143
+ const newScale = Math.min(4, currentScale + 0.05);
9144
+ viewerStore.getState().setScale(newScale);
8773
9145
  }, [viewerStore]);
8774
9146
  const handleZoomOut = useCallback32(() => {
8775
- viewerStore.getState().zoomOut();
9147
+ const currentScale = viewerStore.getState().scale;
9148
+ const newScale = Math.max(0.1, currentScale - 0.05);
9149
+ viewerStore.getState().setScale(newScale);
8776
9150
  }, [viewerStore]);
8777
9151
  const handleFitToWidth = useCallback32(() => {
8778
9152
  viewerStore.getState().setScale(1);
@@ -8909,24 +9283,33 @@ function getSrcIdentifier(src) {
8909
9283
  const last = Array.from(data.slice(-4)).map((b) => b.toString(16).padStart(2, "0")).join("");
8910
9284
  return `binary:${len}:${first}:${last}`;
8911
9285
  }
8912
- function mergeRects2(rects) {
8913
- if (rects.length === 0) return [];
8914
- const sorted = [...rects].sort((a, b) => a.y - b.y || a.x - b.x);
8915
- const merged = [];
8916
- let current = { ...sorted[0] };
8917
- for (let i = 1; i < sorted.length; i++) {
8918
- const rect = sorted[i];
8919
- if (Math.abs(rect.y - current.y) < 2 && rect.x <= current.x + current.width + 2) {
8920
- const newRight = Math.max(current.x + current.width, rect.x + rect.width);
8921
- current.width = newRight - current.x;
8922
- current.height = Math.max(current.height, rect.height);
8923
- } else {
8924
- merged.push(current);
8925
- current = { ...rect };
9286
+ function calculateMatchRects3(textItems, startOffset, length, viewport) {
9287
+ const rects = [];
9288
+ let currentOffset = 0;
9289
+ for (const item of textItems) {
9290
+ const itemStart = currentOffset;
9291
+ const itemEnd = currentOffset + item.text.length;
9292
+ if (itemEnd > startOffset && itemStart < startOffset + length) {
9293
+ const [, , c, d, tx, ty] = item.transform;
9294
+ const x = tx;
9295
+ const y = viewport.height - ty;
9296
+ const height = Math.sqrt(c * c + d * d);
9297
+ const matchStartInItem = Math.max(0, startOffset - itemStart);
9298
+ const matchEndInItem = Math.min(item.text.length, startOffset + length - itemStart);
9299
+ const charWidth = item.text.length > 0 ? item.width / item.text.length : item.width;
9300
+ const matchWidth = charWidth * (matchEndInItem - matchStartInItem);
9301
+ const matchX = x + charWidth * matchStartInItem;
9302
+ const yOffset = height * 0.15;
9303
+ rects.push({
9304
+ x: matchX,
9305
+ y: y - height + yOffset,
9306
+ width: matchWidth,
9307
+ height
9308
+ });
8926
9309
  }
9310
+ currentOffset = itemEnd;
8927
9311
  }
8928
- merged.push(current);
8929
- return merged;
9312
+ return rects;
8930
9313
  }
8931
9314
  var PDFViewerInner, PDFViewerInnerWithRef, PDFViewerClient;
8932
9315
  var init_PDFViewerClient = __esm({
@@ -8945,6 +9328,7 @@ var init_PDFViewerClient = __esm({
8945
9328
  PDFViewerInner = memo25(function PDFViewerInner2({
8946
9329
  src,
8947
9330
  initialPage = 1,
9331
+ page: controlledPage,
8948
9332
  initialScale = "auto",
8949
9333
  showToolbar = true,
8950
9334
  showSidebar = true,
@@ -8954,9 +9338,17 @@ var init_PDFViewerClient = __esm({
8954
9338
  onDocumentLoad,
8955
9339
  onPageChange,
8956
9340
  onScaleChange,
9341
+ onZoomChange,
8957
9342
  onError,
9343
+ onPageRenderStart,
9344
+ onPageRenderComplete,
9345
+ onHighlightAdded,
9346
+ onHighlightRemoved,
9347
+ onAnnotationAdded,
8958
9348
  workerSrc,
8959
9349
  className,
9350
+ loadingComponent,
9351
+ errorComponent,
8960
9352
  onReady
8961
9353
  }) {
8962
9354
  const { viewerStore, annotationStore, searchStore } = usePDFViewerStores();
@@ -8966,12 +9358,26 @@ var init_PDFViewerClient = __esm({
8966
9358
  const onErrorRef = useRef19(onError);
8967
9359
  const onPageChangeRef = useRef19(onPageChange);
8968
9360
  const onScaleChangeRef = useRef19(onScaleChange);
9361
+ const onZoomChangeRef = useRef19(onZoomChange);
9362
+ const onPageRenderStartRef = useRef19(onPageRenderStart);
9363
+ const onPageRenderCompleteRef = useRef19(onPageRenderComplete);
9364
+ const onHighlightAddedRef = useRef19(onHighlightAdded);
9365
+ const onHighlightRemovedRef = useRef19(onHighlightRemoved);
9366
+ const onAnnotationAddedRef = useRef19(onAnnotationAdded);
8969
9367
  const onReadyRef = useRef19(onReady);
8970
9368
  onDocumentLoadRef.current = onDocumentLoad;
8971
9369
  onErrorRef.current = onError;
8972
9370
  onPageChangeRef.current = onPageChange;
8973
9371
  onScaleChangeRef.current = onScaleChange;
9372
+ onZoomChangeRef.current = onZoomChange;
9373
+ onPageRenderStartRef.current = onPageRenderStart;
9374
+ onPageRenderCompleteRef.current = onPageRenderComplete;
9375
+ onHighlightAddedRef.current = onHighlightAdded;
9376
+ onHighlightRemovedRef.current = onHighlightRemoved;
9377
+ onAnnotationAddedRef.current = onAnnotationAdded;
8974
9378
  onReadyRef.current = onReady;
9379
+ const isControlled = controlledPage !== void 0;
9380
+ const prevControlledPageRef = useRef19(controlledPage);
8975
9381
  const srcIdRef = useRef19(null);
8976
9382
  const currentPage = useViewerStore((s) => s.currentPage);
8977
9383
  const scale = useViewerStore((s) => s.scale);
@@ -9000,26 +9406,15 @@ var init_PDFViewerClient = __esm({
9000
9406
  const textContent = await page.getTextContent();
9001
9407
  const viewport = page.getViewport({ scale: 1 });
9002
9408
  let fullText = "";
9003
- const charPositions = [];
9409
+ const textItems = [];
9004
9410
  for (const item of textContent.items) {
9005
9411
  if ("str" in item && item.str) {
9006
- const tx = item.transform;
9007
- const x = tx[4];
9008
- const y = viewport.height - tx[5];
9009
- const width = item.width ?? 0;
9010
- const height = item.height ?? 12;
9011
- const charWidth = item.str.length > 0 ? width / item.str.length : width;
9012
- for (let i = 0; i < item.str.length; i++) {
9013
- charPositions.push({
9014
- char: item.str[i],
9015
- rect: {
9016
- x: x + i * charWidth,
9017
- y: y - height,
9018
- width: charWidth,
9019
- height
9020
- }
9021
- });
9022
- }
9412
+ textItems.push({
9413
+ text: item.str,
9414
+ transform: item.transform,
9415
+ width: item.width ?? 0,
9416
+ height: item.height ?? 12
9417
+ });
9023
9418
  fullText += item.str;
9024
9419
  }
9025
9420
  }
@@ -9028,18 +9423,16 @@ var init_PDFViewerClient = __esm({
9028
9423
  while (true) {
9029
9424
  const matchIndex = textToSearch.indexOf(searchText, startIndex);
9030
9425
  if (matchIndex === -1) break;
9031
- const matchRects = [];
9032
- for (let i = matchIndex; i < matchIndex + text.length && i < charPositions.length; i++) {
9033
- matchRects.push(charPositions[i].rect);
9426
+ const matchRects = calculateMatchRects3(textItems, matchIndex, text.length, viewport);
9427
+ if (matchRects.length > 0) {
9428
+ const highlight = annotationStore.getState().addHighlight({
9429
+ pageNumber: pageNum,
9430
+ rects: matchRects,
9431
+ color,
9432
+ text: fullText.substring(matchIndex, matchIndex + text.length)
9433
+ });
9434
+ highlightIds.push(highlight.id);
9034
9435
  }
9035
- const mergedRects = mergeRects2(matchRects);
9036
- const highlight = annotationStore.getState().addHighlight({
9037
- pageNumber: pageNum,
9038
- rects: mergedRects,
9039
- color,
9040
- text: fullText.substring(matchIndex, matchIndex + text.length)
9041
- });
9042
- highlightIds.push(highlight.id);
9043
9436
  startIndex = matchIndex + 1;
9044
9437
  }
9045
9438
  } catch {
@@ -9112,8 +9505,9 @@ var init_PDFViewerClient = __esm({
9112
9505
  }
9113
9506
  },
9114
9507
  // ==================== Navigation ====================
9115
- goToPage: (page) => {
9116
- viewerStore.getState().goToPage(page);
9508
+ goToPage: async (page, options) => {
9509
+ const behavior = options?.behavior ?? "smooth";
9510
+ await viewerStore.getState().requestScrollToPage(page, behavior);
9117
9511
  },
9118
9512
  nextPage: () => {
9119
9513
  viewerStore.getState().nextPage();
@@ -9173,6 +9567,233 @@ var init_PDFViewerClient = __esm({
9173
9567
  clearSearch: () => {
9174
9568
  searchStore.getState().clearSearch();
9175
9569
  },
9570
+ // ==================== Combined Search & Highlight ====================
9571
+ searchAndHighlight: async (query, options) => {
9572
+ const doc = viewerStore.getState().document;
9573
+ if (!doc) {
9574
+ return { matchCount: 0, highlightIds: [], matches: [] };
9575
+ }
9576
+ const color = options?.color ?? "yellow";
9577
+ const caseSensitive = options?.caseSensitive ?? false;
9578
+ const wholeWord = options?.wholeWord ?? false;
9579
+ const scrollToFirst = options?.scrollToFirst ?? true;
9580
+ const clearPrevious = options?.clearPrevious ?? true;
9581
+ if (clearPrevious) {
9582
+ const existingHighlights = annotationStore.getState().highlights;
9583
+ for (const h of existingHighlights) {
9584
+ if (h.source === "search") {
9585
+ annotationStore.getState().removeHighlight(h.id);
9586
+ }
9587
+ }
9588
+ }
9589
+ let pagesToSearch;
9590
+ if (options?.pageRange) {
9591
+ if (Array.isArray(options.pageRange)) {
9592
+ pagesToSearch = options.pageRange;
9593
+ } else {
9594
+ const { start, end } = options.pageRange;
9595
+ pagesToSearch = Array.from({ length: end - start + 1 }, (_, i) => start + i);
9596
+ }
9597
+ } else {
9598
+ pagesToSearch = Array.from({ length: doc.numPages }, (_, i) => i + 1);
9599
+ }
9600
+ const result = {
9601
+ matchCount: 0,
9602
+ highlightIds: [],
9603
+ matches: []
9604
+ };
9605
+ const searchText = caseSensitive ? query : query.toLowerCase();
9606
+ for (const pageNum of pagesToSearch) {
9607
+ if (pageNum < 1 || pageNum > doc.numPages) continue;
9608
+ try {
9609
+ const page = await doc.getPage(pageNum);
9610
+ const textContent = await page.getTextContent();
9611
+ const viewport = page.getViewport({ scale: 1 });
9612
+ let fullText = "";
9613
+ const textItems = [];
9614
+ for (const item of textContent.items) {
9615
+ if ("str" in item && item.str) {
9616
+ textItems.push({
9617
+ text: item.str,
9618
+ transform: item.transform,
9619
+ width: item.width ?? 0,
9620
+ height: item.height ?? 12
9621
+ });
9622
+ fullText += item.str;
9623
+ }
9624
+ }
9625
+ const textToSearch = caseSensitive ? fullText : fullText.toLowerCase();
9626
+ let startIndex = 0;
9627
+ while (true) {
9628
+ const matchIndex = textToSearch.indexOf(searchText, startIndex);
9629
+ if (matchIndex === -1) break;
9630
+ if (wholeWord) {
9631
+ const beforeChar = matchIndex > 0 ? textToSearch[matchIndex - 1] : " ";
9632
+ const afterChar = matchIndex + query.length < textToSearch.length ? textToSearch[matchIndex + query.length] : " ";
9633
+ if (/\w/.test(beforeChar) || /\w/.test(afterChar)) {
9634
+ startIndex = matchIndex + 1;
9635
+ continue;
9636
+ }
9637
+ }
9638
+ const matchRects = calculateMatchRects3(textItems, matchIndex, query.length, viewport);
9639
+ if (matchRects.length > 0) {
9640
+ const highlight = annotationStore.getState().addHighlight({
9641
+ pageNumber: pageNum,
9642
+ rects: matchRects,
9643
+ color,
9644
+ text: fullText.substring(matchIndex, matchIndex + query.length),
9645
+ source: "search"
9646
+ });
9647
+ result.matchCount++;
9648
+ result.highlightIds.push(highlight.id);
9649
+ result.matches.push({
9650
+ pageNumber: pageNum,
9651
+ text: fullText.substring(matchIndex, matchIndex + query.length),
9652
+ highlightId: highlight.id,
9653
+ rects: matchRects
9654
+ });
9655
+ }
9656
+ startIndex = matchIndex + 1;
9657
+ }
9658
+ } catch {
9659
+ }
9660
+ }
9661
+ if (scrollToFirst && result.matches.length > 0) {
9662
+ const firstMatch = result.matches[0];
9663
+ await viewerStore.getState().requestScrollToPage(firstMatch.pageNumber, "smooth");
9664
+ }
9665
+ return result;
9666
+ },
9667
+ // ==================== Agent Tools ====================
9668
+ agentTools: {
9669
+ navigateToPage: async (page) => {
9670
+ try {
9671
+ const { currentPage: currentPage2, numPages } = viewerStore.getState();
9672
+ if (numPages === 0) {
9673
+ return {
9674
+ success: false,
9675
+ error: { code: "NO_DOCUMENT", message: "No document is loaded" }
9676
+ };
9677
+ }
9678
+ if (page < 1 || page > numPages) {
9679
+ return {
9680
+ success: false,
9681
+ error: { code: "INVALID_PAGE", message: `Page ${page} is out of range (1-${numPages})` }
9682
+ };
9683
+ }
9684
+ const previousPage = currentPage2;
9685
+ await viewerStore.getState().requestScrollToPage(page, "smooth");
9686
+ return {
9687
+ success: true,
9688
+ data: { previousPage, currentPage: page }
9689
+ };
9690
+ } catch (err) {
9691
+ return {
9692
+ success: false,
9693
+ error: { code: "NAVIGATION_FAILED", message: err instanceof Error ? err.message : "Unknown error" }
9694
+ };
9695
+ }
9696
+ },
9697
+ highlightText: async (text, options) => {
9698
+ try {
9699
+ const highlightIds = await handle.highlightText(text, options);
9700
+ return {
9701
+ success: true,
9702
+ data: { matchCount: highlightIds.length, highlightIds }
9703
+ };
9704
+ } catch (err) {
9705
+ return {
9706
+ success: false,
9707
+ error: { code: "HIGHLIGHT_FAILED", message: err instanceof Error ? err.message : "Unknown error" }
9708
+ };
9709
+ }
9710
+ },
9711
+ getPageContent: async (page) => {
9712
+ try {
9713
+ const doc = viewerStore.getState().document;
9714
+ if (!doc) {
9715
+ return {
9716
+ success: false,
9717
+ error: { code: "NO_DOCUMENT", message: "No document is loaded" }
9718
+ };
9719
+ }
9720
+ if (page < 1 || page > doc.numPages) {
9721
+ return {
9722
+ success: false,
9723
+ error: { code: "INVALID_PAGE", message: `Page ${page} is out of range (1-${doc.numPages})` }
9724
+ };
9725
+ }
9726
+ const pageObj = await doc.getPage(page);
9727
+ const textContent = await pageObj.getTextContent();
9728
+ const text = textContent.items.filter((item) => "str" in item).map((item) => item.str).join("");
9729
+ return {
9730
+ success: true,
9731
+ data: { text }
9732
+ };
9733
+ } catch (err) {
9734
+ return {
9735
+ success: false,
9736
+ error: { code: "CONTENT_FETCH_FAILED", message: err instanceof Error ? err.message : "Unknown error" }
9737
+ };
9738
+ }
9739
+ },
9740
+ clearAllVisuals: async () => {
9741
+ try {
9742
+ const highlights = annotationStore.getState().highlights;
9743
+ for (const h of highlights) {
9744
+ annotationStore.getState().removeHighlight(h.id);
9745
+ }
9746
+ const annotations = annotationStore.getState().annotations;
9747
+ for (const a of annotations) {
9748
+ annotationStore.getState().removeAnnotation(a.id);
9749
+ }
9750
+ return { success: true };
9751
+ } catch (err) {
9752
+ return {
9753
+ success: false,
9754
+ error: { code: "CLEAR_FAILED", message: err instanceof Error ? err.message : "Unknown error" }
9755
+ };
9756
+ }
9757
+ }
9758
+ },
9759
+ // ==================== Coordinate Helpers ====================
9760
+ coordinates: {
9761
+ getPageDimensions: (page) => {
9762
+ const doc = viewerStore.getState().document;
9763
+ if (!doc || page < 1 || page > doc.numPages) {
9764
+ return null;
9765
+ }
9766
+ try {
9767
+ return {
9768
+ width: 612,
9769
+ // Default US Letter width
9770
+ height: 792,
9771
+ // Default US Letter height
9772
+ rotation: viewerStore.getState().rotation
9773
+ };
9774
+ } catch {
9775
+ return null;
9776
+ }
9777
+ },
9778
+ percentToPixels: (xPercent, yPercent, page) => {
9779
+ const dimensions = handle.coordinates.getPageDimensions(page);
9780
+ if (!dimensions) return null;
9781
+ const scale2 = viewerStore.getState().scale;
9782
+ return {
9783
+ x: xPercent / 100 * dimensions.width * scale2,
9784
+ y: yPercent / 100 * dimensions.height * scale2
9785
+ };
9786
+ },
9787
+ pixelsToPercent: (x, y, page) => {
9788
+ const dimensions = handle.coordinates.getPageDimensions(page);
9789
+ if (!dimensions) return null;
9790
+ const scale2 = viewerStore.getState().scale;
9791
+ return {
9792
+ x: x / (dimensions.width * scale2) * 100,
9793
+ y: y / (dimensions.height * scale2) * 100
9794
+ };
9795
+ }
9796
+ },
9176
9797
  // ==================== Document ====================
9177
9798
  getDocument: () => {
9178
9799
  return viewerStore.getState().document;
@@ -9259,10 +9880,36 @@ var init_PDFViewerClient = __esm({
9259
9880
  if (prevScaleRef.current !== scale) {
9260
9881
  prevScaleRef.current = scale;
9261
9882
  onScaleChangeRef.current?.(scale);
9883
+ onZoomChangeRef.current?.(scale);
9262
9884
  }
9263
9885
  }, [scale]);
9886
+ useEffect22(() => {
9887
+ if (!isControlled || controlledPage === void 0) return;
9888
+ if (prevControlledPageRef.current === controlledPage) return;
9889
+ prevControlledPageRef.current = controlledPage;
9890
+ const { numPages, currentPage: currentPage2 } = viewerStore.getState();
9891
+ if (numPages > 0 && controlledPage !== currentPage2) {
9892
+ viewerStore.getState().requestScrollToPage(controlledPage, "smooth");
9893
+ }
9894
+ }, [controlledPage, isControlled, viewerStore]);
9264
9895
  const themeClass = theme === "dark" ? "dark" : "";
9265
9896
  if (error) {
9897
+ if (errorComponent) {
9898
+ const errorContent = typeof errorComponent === "function" ? errorComponent(error, handleRetry) : errorComponent;
9899
+ return /* @__PURE__ */ jsx26(
9900
+ "div",
9901
+ {
9902
+ className: cn(
9903
+ "pdf-viewer pdf-viewer-error",
9904
+ "flex flex-col h-full",
9905
+ "bg-white dark:bg-gray-900",
9906
+ themeClass,
9907
+ className
9908
+ ),
9909
+ children: errorContent
9910
+ }
9911
+ );
9912
+ }
9266
9913
  return /* @__PURE__ */ jsx26(
9267
9914
  "div",
9268
9915
  {
@@ -9318,7 +9965,7 @@ var init_PDFViewerClient = __esm({
9318
9965
  renderContainer()
9319
9966
  ] }),
9320
9967
  showFloatingZoom && /* @__PURE__ */ jsx26(FloatingZoomControls, { position: "bottom-right" }),
9321
- isLoading && /* @__PURE__ */ jsx26("div", { className: "absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-900/80", children: /* @__PURE__ */ jsxs22("div", { className: "flex flex-col items-center", children: [
9968
+ isLoading && /* @__PURE__ */ jsx26("div", { className: "absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-900/80", children: loadingComponent ?? /* @__PURE__ */ jsxs22("div", { className: "flex flex-col items-center", children: [
9322
9969
  /* @__PURE__ */ jsx26("div", { className: "w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" }),
9323
9970
  /* @__PURE__ */ jsx26("div", { className: "mt-2 text-sm text-gray-500", children: "Loading PDF..." })
9324
9971
  ] }) })
@@ -10692,10 +11339,247 @@ var Minimap = memo34(function Minimap2({
10692
11339
  // src/components/index.ts
10693
11340
  init_FloatingZoomControls2();
10694
11341
 
11342
+ // src/components/PDFThumbnailNav/PDFThumbnailNav.tsx
11343
+ init_hooks();
11344
+ init_utils();
11345
+ import { memo as memo35, useEffect as useEffect26, useState as useState28, useRef as useRef25, useCallback as useCallback41 } from "react";
11346
+ import { jsx as jsx36, jsxs as jsxs30 } from "react/jsx-runtime";
11347
+ var DEFAULT_WIDTH = 612;
11348
+ var DEFAULT_HEIGHT = 792;
11349
+ var PDFThumbnailNav = memo35(function PDFThumbnailNav2({
11350
+ thumbnailScale = 0.15,
11351
+ orientation = "vertical",
11352
+ maxVisible = 10,
11353
+ className,
11354
+ onThumbnailClick,
11355
+ gap = 8,
11356
+ showPageNumbers = true
11357
+ }) {
11358
+ const { document: document2, numPages, currentPage } = usePDFViewer();
11359
+ const { viewerStore } = usePDFViewerStores();
11360
+ const containerRef = useRef25(null);
11361
+ const [thumbnails, setThumbnails] = useState28(/* @__PURE__ */ new Map());
11362
+ const [visibleRange, setVisibleRange] = useState28({ start: 1, end: maxVisible });
11363
+ const renderQueueRef = useRef25(/* @__PURE__ */ new Set());
11364
+ const pageCache = useRef25(/* @__PURE__ */ new Map());
11365
+ const thumbnailWidth = Math.floor(DEFAULT_WIDTH * thumbnailScale);
11366
+ const thumbnailHeight = Math.floor(DEFAULT_HEIGHT * thumbnailScale);
11367
+ const updateVisibleRange = useCallback41(() => {
11368
+ if (!containerRef.current || numPages === 0) return;
11369
+ const container = containerRef.current;
11370
+ const isHorizontal2 = orientation === "horizontal";
11371
+ const scrollPosition = isHorizontal2 ? container.scrollLeft : container.scrollTop;
11372
+ const viewportSize = isHorizontal2 ? container.clientWidth : container.clientHeight;
11373
+ const itemSize = (isHorizontal2 ? thumbnailWidth : thumbnailHeight) + gap;
11374
+ const firstVisible = Math.max(1, Math.floor(scrollPosition / itemSize) + 1);
11375
+ const visibleCount = Math.ceil(viewportSize / itemSize) + 2;
11376
+ const lastVisible = Math.min(numPages, firstVisible + visibleCount);
11377
+ setVisibleRange({ start: firstVisible, end: lastVisible });
11378
+ }, [numPages, orientation, thumbnailWidth, thumbnailHeight, gap]);
11379
+ useEffect26(() => {
11380
+ const container = containerRef.current;
11381
+ if (!container) return;
11382
+ const handleScroll = () => {
11383
+ requestAnimationFrame(updateVisibleRange);
11384
+ };
11385
+ container.addEventListener("scroll", handleScroll, { passive: true });
11386
+ updateVisibleRange();
11387
+ return () => container.removeEventListener("scroll", handleScroll);
11388
+ }, [updateVisibleRange]);
11389
+ useEffect26(() => {
11390
+ if (!document2) {
11391
+ setThumbnails(/* @__PURE__ */ new Map());
11392
+ pageCache.current.clear();
11393
+ return;
11394
+ }
11395
+ const renderThumbnails = async () => {
11396
+ const newThumbnails = new Map(thumbnails);
11397
+ const pagesToRender = [];
11398
+ for (let i = visibleRange.start; i <= visibleRange.end; i++) {
11399
+ if (!newThumbnails.has(i) && !renderQueueRef.current.has(i)) {
11400
+ pagesToRender.push(i);
11401
+ renderQueueRef.current.add(i);
11402
+ }
11403
+ }
11404
+ for (const pageNum of pagesToRender) {
11405
+ try {
11406
+ let page = pageCache.current.get(pageNum);
11407
+ if (!page) {
11408
+ page = await document2.getPage(pageNum);
11409
+ pageCache.current.set(pageNum, page);
11410
+ }
11411
+ const viewport = page.getViewport({ scale: thumbnailScale });
11412
+ const canvas = window.document.createElement("canvas");
11413
+ canvas.width = Math.floor(viewport.width);
11414
+ canvas.height = Math.floor(viewport.height);
11415
+ const ctx = canvas.getContext("2d");
11416
+ if (ctx) {
11417
+ await page.render({
11418
+ canvasContext: ctx,
11419
+ viewport
11420
+ }).promise;
11421
+ newThumbnails.set(pageNum, {
11422
+ pageNumber: pageNum,
11423
+ canvas,
11424
+ width: canvas.width,
11425
+ height: canvas.height
11426
+ });
11427
+ }
11428
+ } catch (error) {
11429
+ console.error(`Failed to render thumbnail for page ${pageNum}:`, error);
11430
+ } finally {
11431
+ renderQueueRef.current.delete(pageNum);
11432
+ }
11433
+ }
11434
+ if (pagesToRender.length > 0) {
11435
+ setThumbnails(newThumbnails);
11436
+ }
11437
+ };
11438
+ renderThumbnails();
11439
+ }, [document2, visibleRange, thumbnailScale, thumbnails]);
11440
+ useEffect26(() => {
11441
+ if (!containerRef.current || numPages === 0) return;
11442
+ const container = containerRef.current;
11443
+ const isHorizontal2 = orientation === "horizontal";
11444
+ const itemSize = (isHorizontal2 ? thumbnailWidth : thumbnailHeight) + gap;
11445
+ const targetPosition = (currentPage - 1) * itemSize;
11446
+ const scrollPosition = isHorizontal2 ? container.scrollLeft : container.scrollTop;
11447
+ const viewportSize = isHorizontal2 ? container.clientWidth : container.clientHeight;
11448
+ if (targetPosition < scrollPosition || targetPosition + itemSize > scrollPosition + viewportSize) {
11449
+ const targetScroll = targetPosition - (viewportSize - itemSize) / 2;
11450
+ container.scrollTo({
11451
+ [isHorizontal2 ? "left" : "top"]: Math.max(0, targetScroll),
11452
+ behavior: "smooth"
11453
+ });
11454
+ }
11455
+ }, [currentPage, numPages, orientation, thumbnailWidth, thumbnailHeight, gap]);
11456
+ const handleThumbnailClick = useCallback41((pageNum) => {
11457
+ onThumbnailClick?.(pageNum);
11458
+ viewerStore.getState().requestScrollToPage(pageNum, "smooth");
11459
+ }, [onThumbnailClick, viewerStore]);
11460
+ if (!document2 || numPages === 0) {
11461
+ return /* @__PURE__ */ jsx36(
11462
+ "div",
11463
+ {
11464
+ className: cn(
11465
+ "pdf-thumbnail-nav",
11466
+ "flex items-center justify-center",
11467
+ "bg-gray-100 dark:bg-gray-800",
11468
+ "text-gray-500 dark:text-gray-400",
11469
+ "text-sm",
11470
+ className
11471
+ ),
11472
+ style: {
11473
+ width: orientation === "vertical" ? thumbnailWidth + 24 : "100%",
11474
+ height: orientation === "horizontal" ? thumbnailHeight + 40 : "100%"
11475
+ },
11476
+ children: "No document"
11477
+ }
11478
+ );
11479
+ }
11480
+ const isHorizontal = orientation === "horizontal";
11481
+ const totalSize = numPages * ((isHorizontal ? thumbnailWidth : thumbnailHeight) + gap) - gap;
11482
+ return /* @__PURE__ */ jsx36(
11483
+ "div",
11484
+ {
11485
+ ref: containerRef,
11486
+ className: cn(
11487
+ "pdf-thumbnail-nav",
11488
+ "overflow-auto",
11489
+ "bg-gray-100 dark:bg-gray-800",
11490
+ isHorizontal ? "flex-row" : "flex-col",
11491
+ className
11492
+ ),
11493
+ style: {
11494
+ ...isHorizontal ? { overflowX: "auto", overflowY: "hidden" } : { overflowX: "hidden", overflowY: "auto" }
11495
+ },
11496
+ children: /* @__PURE__ */ jsx36(
11497
+ "div",
11498
+ {
11499
+ className: cn(
11500
+ "relative",
11501
+ isHorizontal ? "flex flex-row items-center" : "flex flex-col items-center"
11502
+ ),
11503
+ style: {
11504
+ [isHorizontal ? "width" : "height"]: totalSize,
11505
+ [isHorizontal ? "minWidth" : "minHeight"]: totalSize,
11506
+ padding: gap / 2,
11507
+ gap
11508
+ },
11509
+ children: Array.from({ length: numPages }, (_, i) => i + 1).map((pageNum) => {
11510
+ const thumbnail = thumbnails.get(pageNum);
11511
+ const isActive = pageNum === currentPage;
11512
+ const isVisible = pageNum >= visibleRange.start && pageNum <= visibleRange.end;
11513
+ return /* @__PURE__ */ jsxs30(
11514
+ "div",
11515
+ {
11516
+ className: cn(
11517
+ "pdf-thumbnail",
11518
+ "flex-shrink-0 cursor-pointer transition-all duration-200",
11519
+ "border-2 rounded shadow-sm hover:shadow-md",
11520
+ isActive ? "border-blue-500 ring-2 ring-blue-200 dark:ring-blue-800" : "border-gray-300 dark:border-gray-600 hover:border-blue-400"
11521
+ ),
11522
+ style: {
11523
+ width: thumbnailWidth,
11524
+ height: thumbnailHeight + (showPageNumbers ? 24 : 0)
11525
+ },
11526
+ onClick: () => handleThumbnailClick(pageNum),
11527
+ role: "button",
11528
+ tabIndex: 0,
11529
+ "aria-label": `Go to page ${pageNum}`,
11530
+ "aria-current": isActive ? "page" : void 0,
11531
+ onKeyDown: (e) => {
11532
+ if (e.key === "Enter" || e.key === " ") {
11533
+ e.preventDefault();
11534
+ handleThumbnailClick(pageNum);
11535
+ }
11536
+ },
11537
+ children: [
11538
+ /* @__PURE__ */ jsx36(
11539
+ "div",
11540
+ {
11541
+ className: "relative bg-white dark:bg-gray-700",
11542
+ style: {
11543
+ width: thumbnailWidth,
11544
+ height: thumbnailHeight
11545
+ },
11546
+ children: isVisible && thumbnail?.canvas ? /* @__PURE__ */ jsx36(
11547
+ "img",
11548
+ {
11549
+ src: thumbnail.canvas.toDataURL(),
11550
+ alt: `Page ${pageNum}`,
11551
+ className: "w-full h-full object-contain",
11552
+ loading: "lazy"
11553
+ }
11554
+ ) : /* @__PURE__ */ jsx36("div", { className: "absolute inset-0 flex items-center justify-center text-gray-400 dark:text-gray-500 text-xs", children: pageNum })
11555
+ }
11556
+ ),
11557
+ showPageNumbers && /* @__PURE__ */ jsx36(
11558
+ "div",
11559
+ {
11560
+ className: cn(
11561
+ "text-center text-xs py-1",
11562
+ "bg-gray-50 dark:bg-gray-700",
11563
+ isActive ? "text-blue-600 dark:text-blue-400 font-medium" : "text-gray-600 dark:text-gray-400"
11564
+ ),
11565
+ children: pageNum
11566
+ }
11567
+ )
11568
+ ]
11569
+ },
11570
+ pageNum
11571
+ );
11572
+ })
11573
+ }
11574
+ )
11575
+ }
11576
+ );
11577
+ });
11578
+
10695
11579
  // src/components/ErrorBoundary/PDFErrorBoundary.tsx
10696
11580
  init_utils();
10697
11581
  import { Component } from "react";
10698
- import { jsx as jsx36, jsxs as jsxs30 } from "react/jsx-runtime";
11582
+ import { jsx as jsx37, jsxs as jsxs31 } from "react/jsx-runtime";
10699
11583
  var PDFErrorBoundary = class extends Component {
10700
11584
  constructor(props) {
10701
11585
  super(props);
@@ -10723,7 +11607,7 @@ var PDFErrorBoundary = class extends Component {
10723
11607
  return fallback;
10724
11608
  }
10725
11609
  if (showDefaultUI) {
10726
- return /* @__PURE__ */ jsx36(
11610
+ return /* @__PURE__ */ jsx37(
10727
11611
  DefaultErrorUI,
10728
11612
  {
10729
11613
  error,
@@ -10742,7 +11626,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10742
11626
  const isNetworkError = error.message.includes("fetch") || error.message.includes("network") || error.message.includes("Failed to load");
10743
11627
  let title = "Something went wrong";
10744
11628
  let description = error.message;
10745
- let icon = /* @__PURE__ */ jsx36("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx36(
11629
+ let icon = /* @__PURE__ */ jsx37("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx37(
10746
11630
  "path",
10747
11631
  {
10748
11632
  strokeLinecap: "round",
@@ -10754,7 +11638,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10754
11638
  if (isPDFError) {
10755
11639
  title = "Unable to load PDF";
10756
11640
  description = "The PDF file could not be loaded. It may be corrupted or in an unsupported format.";
10757
- icon = /* @__PURE__ */ jsx36("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx36(
11641
+ icon = /* @__PURE__ */ jsx37("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx37(
10758
11642
  "path",
10759
11643
  {
10760
11644
  strokeLinecap: "round",
@@ -10766,7 +11650,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10766
11650
  } else if (isNetworkError) {
10767
11651
  title = "Network error";
10768
11652
  description = "Unable to fetch the PDF file. Please check your internet connection and try again.";
10769
- icon = /* @__PURE__ */ jsx36("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx36(
11653
+ icon = /* @__PURE__ */ jsx37("svg", { className: "w-12 h-12 text-red-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx37(
10770
11654
  "path",
10771
11655
  {
10772
11656
  strokeLinecap: "round",
@@ -10776,7 +11660,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10776
11660
  }
10777
11661
  ) });
10778
11662
  }
10779
- return /* @__PURE__ */ jsxs30(
11663
+ return /* @__PURE__ */ jsxs31(
10780
11664
  "div",
10781
11665
  {
10782
11666
  className: cn(
@@ -10789,14 +11673,14 @@ function DefaultErrorUI({ error, onReset, className }) {
10789
11673
  ),
10790
11674
  children: [
10791
11675
  icon,
10792
- /* @__PURE__ */ jsx36("h2", { className: "mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100", children: title }),
10793
- /* @__PURE__ */ jsx36("p", { className: "mt-2 text-sm text-gray-600 dark:text-gray-400 max-w-md", children: description }),
10794
- /* @__PURE__ */ jsxs30("details", { className: "mt-4 text-left max-w-md w-full", children: [
10795
- /* @__PURE__ */ jsx36("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" }),
10796
- /* @__PURE__ */ jsx36("pre", { className: "mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto", children: error.stack || error.message })
11676
+ /* @__PURE__ */ jsx37("h2", { className: "mt-4 text-xl font-semibold text-gray-900 dark:text-gray-100", children: title }),
11677
+ /* @__PURE__ */ jsx37("p", { className: "mt-2 text-sm text-gray-600 dark:text-gray-400 max-w-md", children: description }),
11678
+ /* @__PURE__ */ jsxs31("details", { className: "mt-4 text-left max-w-md w-full", children: [
11679
+ /* @__PURE__ */ jsx37("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" }),
11680
+ /* @__PURE__ */ jsx37("pre", { className: "mt-2 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto", children: error.stack || error.message })
10797
11681
  ] }),
10798
- /* @__PURE__ */ jsxs30("div", { className: "mt-6 flex gap-3", children: [
10799
- /* @__PURE__ */ jsx36(
11682
+ /* @__PURE__ */ jsxs31("div", { className: "mt-6 flex gap-3", children: [
11683
+ /* @__PURE__ */ jsx37(
10800
11684
  "button",
10801
11685
  {
10802
11686
  onClick: onReset,
@@ -10810,7 +11694,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10810
11694
  children: "Try again"
10811
11695
  }
10812
11696
  ),
10813
- /* @__PURE__ */ jsx36(
11697
+ /* @__PURE__ */ jsx37(
10814
11698
  "button",
10815
11699
  {
10816
11700
  onClick: () => window.location.reload(),
@@ -10830,7 +11714,7 @@ function DefaultErrorUI({ error, onReset, className }) {
10830
11714
  );
10831
11715
  }
10832
11716
  function withErrorBoundary({ component, ...props }) {
10833
- return /* @__PURE__ */ jsx36(PDFErrorBoundary, { ...props, children: component });
11717
+ return /* @__PURE__ */ jsx37(PDFErrorBoundary, { ...props, children: component });
10834
11718
  }
10835
11719
 
10836
11720
  // src/index.ts
@@ -10864,6 +11748,7 @@ export {
10864
11748
  OutlinePanel,
10865
11749
  PDFErrorBoundary,
10866
11750
  PDFPage,
11751
+ PDFThumbnailNav,
10867
11752
  PDFViewer,
10868
11753
  PDFViewerClient,
10869
11754
  PDFViewerContext,
@@ -10882,9 +11767,11 @@ export {
10882
11767
  ThumbnailPanel,
10883
11768
  Toolbar,
10884
11769
  VirtualizedDocumentContainer,
11770
+ applyRotation,
10885
11771
  clearHighlights,
10886
11772
  clearStudentData,
10887
11773
  cn,
11774
+ countTextOnPage,
10888
11775
  createAgentAPI,
10889
11776
  createAgentStore,
10890
11777
  createAnnotationStore,
@@ -10893,6 +11780,7 @@ export {
10893
11780
  createSearchStore,
10894
11781
  createStudentStore,
10895
11782
  createViewerStore,
11783
+ doRectsIntersect,
10896
11784
  downloadAnnotationsAsJSON,
10897
11785
  downloadAnnotationsAsMarkdown,
10898
11786
  downloadFile,
@@ -10900,25 +11788,39 @@ export {
10900
11788
  exportAnnotationsAsMarkdown,
10901
11789
  exportHighlightsAsJSON,
10902
11790
  exportHighlightsAsMarkdown,
11791
+ extractPageText,
11792
+ findTextInDocument,
11793
+ findTextOnPage,
10903
11794
  generateDocumentId,
10904
11795
  getAllDocumentIds,
10905
11796
  getAllStudentDataDocumentIds,
10906
11797
  getMetadata,
10907
11798
  getOutline,
10908
11799
  getPage,
11800
+ getPageText,
10909
11801
  getPageTextContent,
10910
11802
  getPluginManager,
11803
+ getRectIntersection,
11804
+ getRotatedDimensions,
10911
11805
  getStorageStats,
10912
11806
  importHighlightsFromJSON,
10913
11807
  initializePDFJS,
10914
11808
  isPDFJSInitialized,
11809
+ isPointInRect,
10915
11810
  loadDocument,
10916
11811
  loadHighlights,
10917
11812
  loadStudentData,
11813
+ mergeAdjacentRects,
11814
+ pdfToPercent,
11815
+ pdfToViewport,
10918
11816
  pdfjsLib,
11817
+ percentToPDF,
11818
+ percentToViewport,
10919
11819
  quickViewer,
11820
+ removeRotation,
10920
11821
  saveHighlights,
10921
11822
  saveStudentData,
11823
+ scaleRect,
10922
11824
  useAgentContext,
10923
11825
  useAgentStore,
10924
11826
  useAnnotationStore,
@@ -10940,6 +11842,8 @@ export {
10940
11842
  useTouchGestures,
10941
11843
  useViewerStore,
10942
11844
  useZoom,
11845
+ viewportToPDF,
11846
+ viewportToPercent,
10943
11847
  withErrorBoundary
10944
11848
  };
10945
11849
  //# sourceMappingURL=index.js.map