react-pdf-highlighter-plus 1.1.2 → 1.1.4

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/esm/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // src/components/PdfHighlighter.tsx
2
- import debounce from "lodash.debounce";
3
2
  import React6, {
4
3
  useEffect as useEffect4,
5
4
  useLayoutEffect as useLayoutEffect2,
@@ -659,9 +658,12 @@ var HighlightLayer = ({
659
658
  scrolledToHighlightId,
660
659
  viewer,
661
660
  highlightBindings,
662
- children
661
+ children,
662
+ shouldRenderHighlight
663
663
  }) => {
664
- const currentHighlights = highlightsByPage[pageNumber] || [];
664
+ const currentHighlights = (highlightsByPage[pageNumber] || []).filter(
665
+ (highlight) => shouldRenderHighlight?.(highlight) ?? true
666
+ );
665
667
  return /* @__PURE__ */ React2.createElement("div", null, currentHighlights.map((highlight, index) => {
666
668
  const viewportHighlight = {
667
669
  ...highlight,
@@ -1111,11 +1113,13 @@ var TipContainer = ({
1111
1113
 
1112
1114
  // src/components/PdfHighlighter.tsx
1113
1115
  var EventBus;
1116
+ var PDFFindController;
1114
1117
  var PDFLinkService;
1115
1118
  var PDFViewer;
1116
1119
  (async () => {
1117
1120
  const pdfjs = await import("pdfjs-dist/web/pdf_viewer.mjs");
1118
1121
  EventBus = pdfjs.EventBus;
1122
+ PDFFindController = pdfjs.PDFFindController;
1119
1123
  PDFLinkService = pdfjs.PDFLinkService;
1120
1124
  PDFViewer = pdfjs.PDFViewer;
1121
1125
  })();
@@ -1143,6 +1147,22 @@ var findOrCreateHighlightLayer = (textLayer) => {
1143
1147
  "PdfHighlighter__highlight-layer"
1144
1148
  );
1145
1149
  };
1150
+ var findOrCreateNoteLayer = (textLayer) => {
1151
+ const pageLayer = textLayer.closest(".page");
1152
+ const container = pageLayer;
1153
+ if (!container) return null;
1154
+ const doc = getDocument(container);
1155
+ let layer = Array.from(container.children).find(
1156
+ (child) => child.classList.contains("PdfHighlighter__note-layer")
1157
+ );
1158
+ if (!layer && container.children.length) {
1159
+ layer = doc.createElement("div");
1160
+ layer.className = "PdfHighlighter__note-layer";
1161
+ container.appendChild(layer);
1162
+ }
1163
+ return layer;
1164
+ };
1165
+ var isFreetextHighlight = (highlight) => "type" in highlight && highlight.type === "freetext";
1146
1166
  var disableTextSelection = (viewer, flag) => {
1147
1167
  viewer.viewer?.classList.toggle("PdfHighlighter--disable-selection", flag);
1148
1168
  };
@@ -1189,6 +1209,9 @@ var PdfHighlighter = ({
1189
1209
  const highlightBindingsRef = useRef5(
1190
1210
  {}
1191
1211
  );
1212
+ const noteBindingsRef = useRef5({});
1213
+ const highlightsRef = useRef5(highlights);
1214
+ const childrenRef = useRef5(children);
1192
1215
  const ghostHighlightRef = useRef5(null);
1193
1216
  const selectionRef = useRef5(null);
1194
1217
  const scrolledToHighlightIdRef = useRef5(null);
@@ -1204,41 +1227,59 @@ var PdfHighlighter = ({
1204
1227
  })
1205
1228
  );
1206
1229
  const resizeObserverRef = useRef5(null);
1230
+ const renderRetryTimeoutsRef = useRef5(
1231
+ []
1232
+ );
1233
+ const resumeScrollAwayTimeoutRef = useRef5(
1234
+ null
1235
+ );
1236
+ const findControllerRef = useRef5(null);
1207
1237
  const viewerRef = useRef5(null);
1238
+ highlightsRef.current = highlights;
1239
+ childrenRef.current = children;
1208
1240
  useLayoutEffect2(() => {
1209
1241
  if (!containerNodeRef.current) return;
1210
- const debouncedDocumentInit = debounce(() => {
1211
- viewerRef.current = viewerRef.current || new PDFViewer({
1212
- container: containerNodeRef.current,
1213
- eventBus: eventBusRef.current,
1214
- textLayerMode: 2,
1215
- removePageBorders: true,
1216
- linkService: linkServiceRef.current
1217
- });
1218
- viewerRef.current.setDocument(pdfDocument);
1219
- linkServiceRef.current.setDocument(pdfDocument);
1220
- linkServiceRef.current.setViewer(viewerRef.current);
1221
- setIsViewerReady(true);
1222
- }, 100);
1223
- debouncedDocumentInit();
1224
- return () => {
1225
- debouncedDocumentInit.cancel();
1226
- };
1227
- }, [document]);
1242
+ findControllerRef.current = findControllerRef.current || new PDFFindController({
1243
+ eventBus: eventBusRef.current,
1244
+ linkService: linkServiceRef.current
1245
+ });
1246
+ viewerRef.current = viewerRef.current || new PDFViewer({
1247
+ container: containerNodeRef.current,
1248
+ eventBus: eventBusRef.current,
1249
+ findController: findControllerRef.current,
1250
+ textLayerMode: 2,
1251
+ removePageBorders: true,
1252
+ linkService: linkServiceRef.current
1253
+ });
1254
+ viewerRef.current.setDocument(pdfDocument);
1255
+ linkServiceRef.current.setDocument(pdfDocument);
1256
+ linkServiceRef.current.setViewer(viewerRef.current);
1257
+ setIsViewerReady(true);
1258
+ }, [pdfDocument]);
1228
1259
  useLayoutEffect2(() => {
1229
1260
  if (!containerNodeRef.current) return;
1230
1261
  resizeObserverRef.current = new ResizeObserver(handleScaleValue);
1231
1262
  resizeObserverRef.current.observe(containerNodeRef.current);
1232
1263
  const doc = containerNodeRef.current.ownerDocument;
1233
- eventBusRef.current.on("textlayerrendered", renderHighlightLayers);
1264
+ eventBusRef.current.on("textlayerrendered", scheduleRenderHighlightLayers);
1265
+ eventBusRef.current.on("pagerendered", scheduleRenderHighlightLayers);
1234
1266
  eventBusRef.current.on("pagesinit", handleScaleValue);
1235
1267
  doc.addEventListener("keydown", handleKeyDown);
1236
- renderHighlightLayers();
1268
+ doc.addEventListener("copy", handleCopy, true);
1269
+ scheduleRenderHighlightLayers();
1237
1270
  return () => {
1238
1271
  eventBusRef.current.off("pagesinit", handleScaleValue);
1239
- eventBusRef.current.off("textlayerrendered", renderHighlightLayers);
1272
+ eventBusRef.current.off("pagerendered", scheduleRenderHighlightLayers);
1273
+ eventBusRef.current.off("textlayerrendered", scheduleRenderHighlightLayers);
1240
1274
  doc.removeEventListener("keydown", handleKeyDown);
1275
+ doc.removeEventListener("copy", handleCopy, true);
1241
1276
  resizeObserverRef.current?.disconnect();
1277
+ renderRetryTimeoutsRef.current.forEach(clearTimeout);
1278
+ renderRetryTimeoutsRef.current = [];
1279
+ if (resumeScrollAwayTimeoutRef.current) {
1280
+ clearTimeout(resumeScrollAwayTimeoutRef.current);
1281
+ resumeScrollAwayTimeoutRef.current = null;
1282
+ }
1242
1283
  };
1243
1284
  }, [selectionTip, highlights, onSelectionFinished]);
1244
1285
  const handleScroll = () => {
@@ -1358,26 +1399,46 @@ var PdfHighlighter = ({
1358
1399
  setTip(null);
1359
1400
  }
1360
1401
  };
1402
+ const handleCopy = (event) => {
1403
+ const container = containerNodeRef.current;
1404
+ if (!container || !event.clipboardData) return;
1405
+ const target = event.target;
1406
+ const targetElement = target instanceof HTMLElement ? target : target instanceof Node ? target.parentElement : null;
1407
+ if (targetElement && (targetElement.closest("input, textarea, [contenteditable='true']") || targetElement.closest(".PdfHighlighter__tip-container"))) {
1408
+ return;
1409
+ }
1410
+ const selection = getWindow(container).getSelection();
1411
+ const range = selection?.rangeCount ? selection.getRangeAt(0) : null;
1412
+ if (!selection || selection.isCollapsed || !range || !container.contains(range.commonAncestorContainer)) {
1413
+ return;
1414
+ }
1415
+ const text = selectionRef.current?.content.text?.trim() || selection.toString().split("\n").join(" ").trim();
1416
+ if (!text) return;
1417
+ event.clipboardData.setData("text/plain", text);
1418
+ event.preventDefault();
1419
+ event.stopPropagation();
1420
+ };
1361
1421
  const handleScaleValue = () => {
1362
1422
  if (viewerRef.current) {
1363
1423
  viewerRef.current.currentScaleValue = pdfScaleValue.toString();
1364
1424
  }
1365
1425
  };
1366
- const renderHighlightLayer = (highlightBindings, pageNumber) => {
1426
+ const renderHighlightLayer = (highlightBindings, pageNumber, shouldRenderHighlight) => {
1367
1427
  if (!viewerRef.current) return;
1368
1428
  highlightBindings.reactRoot.render(
1369
1429
  /* @__PURE__ */ React6.createElement(PdfHighlighterContext.Provider, { value: pdfHighlighterUtils }, /* @__PURE__ */ React6.createElement(
1370
1430
  HighlightLayer,
1371
1431
  {
1372
1432
  highlightsByPage: group_highlights_by_page_default([
1373
- ...highlights,
1433
+ ...highlightsRef.current,
1374
1434
  ghostHighlightRef.current
1375
1435
  ]),
1376
1436
  pageNumber,
1377
1437
  scrolledToHighlightId: scrolledToHighlightIdRef.current,
1378
1438
  viewer: viewerRef.current,
1379
1439
  highlightBindings,
1380
- children
1440
+ shouldRenderHighlight,
1441
+ children: childrenRef.current
1381
1442
  }
1382
1443
  ))
1383
1444
  );
@@ -1385,31 +1446,61 @@ var PdfHighlighter = ({
1385
1446
  const renderHighlightLayers = () => {
1386
1447
  if (!viewerRef.current) return;
1387
1448
  for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber++) {
1388
- const highlightBindings = highlightBindingsRef.current[pageNumber];
1389
- if (highlightBindings?.container?.isConnected) {
1390
- renderHighlightLayer(highlightBindings, pageNumber);
1391
- } else {
1392
- const { textLayer } = viewerRef.current.getPageView(pageNumber - 1) || {};
1393
- if (!textLayer) continue;
1394
- const highlightLayer = findOrCreateHighlightLayer(
1395
- textLayer.div
1396
- );
1397
- if (highlightLayer) {
1398
- const reactRoot = createRoot(highlightLayer);
1399
- highlightBindingsRef.current[pageNumber] = {
1400
- reactRoot,
1449
+ const { textLayer } = viewerRef.current.getPageView(pageNumber - 1) || {};
1450
+ if (!textLayer) continue;
1451
+ const textLayerDiv = textLayer.div;
1452
+ const highlightLayer = findOrCreateHighlightLayer(textLayerDiv);
1453
+ const noteLayer = findOrCreateNoteLayer(textLayerDiv);
1454
+ if (highlightLayer) {
1455
+ let highlightBindings = highlightBindingsRef.current[pageNumber];
1456
+ if (!highlightBindings?.container?.isConnected) {
1457
+ highlightBindings = {
1458
+ reactRoot: createRoot(highlightLayer),
1401
1459
  container: highlightLayer,
1402
- textLayer: textLayer.div
1403
- // textLayer.div for version >=3.0 and textLayer.textLayerDiv otherwise.
1460
+ textLayer: textLayerDiv
1404
1461
  };
1405
- renderHighlightLayer(
1406
- highlightBindingsRef.current[pageNumber],
1407
- pageNumber
1408
- );
1462
+ highlightBindingsRef.current[pageNumber] = highlightBindings;
1463
+ }
1464
+ renderHighlightLayer(
1465
+ highlightBindings,
1466
+ pageNumber,
1467
+ (highlight) => !isFreetextHighlight(highlight)
1468
+ );
1469
+ }
1470
+ if (noteLayer) {
1471
+ let noteBindings = noteBindingsRef.current[pageNumber];
1472
+ if (!noteBindings?.container?.isConnected) {
1473
+ noteBindings = {
1474
+ reactRoot: createRoot(noteLayer),
1475
+ container: noteLayer,
1476
+ textLayer: textLayerDiv
1477
+ };
1478
+ noteBindingsRef.current[pageNumber] = noteBindings;
1409
1479
  }
1480
+ renderHighlightLayer(noteBindings, pageNumber, isFreetextHighlight);
1410
1481
  }
1411
1482
  }
1412
1483
  };
1484
+ const scheduleRenderHighlightLayers = () => {
1485
+ renderHighlightLayers();
1486
+ renderRetryTimeoutsRef.current.forEach(clearTimeout);
1487
+ renderRetryTimeoutsRef.current = [50, 150, 350, 750, 1200].map(
1488
+ (delay) => setTimeout(renderHighlightLayers, delay)
1489
+ );
1490
+ };
1491
+ const resumeScrollAwayListenerAfterNavigation = () => {
1492
+ const container = viewerRef.current?.container;
1493
+ if (!container) return;
1494
+ if (resumeScrollAwayTimeoutRef.current) {
1495
+ clearTimeout(resumeScrollAwayTimeoutRef.current);
1496
+ }
1497
+ resumeScrollAwayTimeoutRef.current = setTimeout(() => {
1498
+ container.addEventListener("scroll", handleScroll, {
1499
+ once: true
1500
+ });
1501
+ resumeScrollAwayTimeoutRef.current = null;
1502
+ }, 1200);
1503
+ };
1413
1504
  const isEditingOrHighlighting = () => {
1414
1505
  return Boolean(selectionRef.current) || Boolean(ghostHighlightRef.current) || isAreaSelectionInProgressRef.current || isEditInProgressRef.current;
1415
1506
  };
@@ -1461,12 +1552,49 @@ var PdfHighlighter = ({
1461
1552
  ]
1462
1553
  });
1463
1554
  scrolledToHighlightIdRef.current = highlight.id;
1464
- renderHighlightLayers();
1465
- setTimeout(() => {
1466
- viewerRef.current.container.addEventListener("scroll", handleScroll, {
1467
- once: true
1468
- });
1469
- }, 100);
1555
+ scheduleRenderHighlightLayers();
1556
+ resumeScrollAwayListenerAfterNavigation();
1557
+ };
1558
+ const dispatchFind = (query, findPrevious2, options = {}, type) => {
1559
+ eventBusRef.current.dispatch("find", {
1560
+ source: findControllerRef.current || viewerRef.current,
1561
+ type,
1562
+ query,
1563
+ phraseSearch: true,
1564
+ caseSensitive: options.caseSensitive ?? false,
1565
+ entireWord: options.entireWord ?? false,
1566
+ highlightAll: options.highlightAll ?? true,
1567
+ findPrevious: findPrevious2,
1568
+ matchDiacritics: options.matchDiacritics ?? false
1569
+ });
1570
+ };
1571
+ const currentSearchRef = useRef5({
1572
+ query: "",
1573
+ options: {}
1574
+ });
1575
+ const search = (query, options = {}) => {
1576
+ currentSearchRef.current = { query, options };
1577
+ if (!query.trim()) {
1578
+ clearSearch();
1579
+ return;
1580
+ }
1581
+ dispatchFind(query, false, options);
1582
+ };
1583
+ const findNext = () => {
1584
+ const { query, options } = currentSearchRef.current;
1585
+ if (!query.trim()) return;
1586
+ dispatchFind(query, false, options, "again");
1587
+ };
1588
+ const findPrevious = () => {
1589
+ const { query, options } = currentSearchRef.current;
1590
+ if (!query.trim()) return;
1591
+ dispatchFind(query, true, options, "again");
1592
+ };
1593
+ const clearSearch = () => {
1594
+ currentSearchRef.current = { query: "", options: {} };
1595
+ eventBusRef.current.dispatch("findbarclose", {
1596
+ source: findControllerRef.current || viewerRef.current
1597
+ });
1470
1598
  };
1471
1599
  const pdfHighlighterUtils = {
1472
1600
  isEditingOrHighlighting,
@@ -1483,6 +1611,10 @@ var PdfHighlighter = ({
1483
1611
  updateTipPosition: updateTipPositionRef.current,
1484
1612
  getLinkService: () => linkServiceRef.current,
1485
1613
  getEventBus: () => eventBusRef.current,
1614
+ search,
1615
+ findNext,
1616
+ findPrevious,
1617
+ clearSearch,
1486
1618
  goToPage: (pageNumber) => {
1487
1619
  console.log("[PdfHighlighter] goToPage called with page:", pageNumber);
1488
1620
  const viewer = viewerRef.current;
@@ -1591,6 +1723,9 @@ var PdfHighlighter = ({
1591
1723
  .PdfHighlighter--dark .PdfHighlighter__highlight-layer {
1592
1724
  filter: invert(${resolvedTheme.darkModeInvertIntensity}) hue-rotate(180deg) brightness(0.95);
1593
1725
  }
1726
+ .PdfHighlighter--dark .PdfHighlighter__note-layer {
1727
+ filter: invert(${resolvedTheme.darkModeInvertIntensity}) hue-rotate(180deg) brightness(0.95);
1728
+ }
1594
1729
  ` : ""}
1595
1730
  `),
1596
1731
  isViewerReady && /* @__PURE__ */ React6.createElement(
@@ -1676,10 +1811,78 @@ var PdfHighlighter = ({
1676
1811
  import React7, {
1677
1812
  useState as useState6,
1678
1813
  useRef as useRef6,
1679
- useEffect as useEffect5
1814
+ useEffect as useEffect5,
1815
+ useLayoutEffect as useLayoutEffect3
1680
1816
  } from "react";
1817
+ import { createPortal } from "react-dom";
1818
+
1819
+ // src/lib/copy-highlight-content.ts
1820
+ var intersects = (a, b) => a.left < b.left + b.width && a.left + a.width > b.left && a.top < b.top + b.height && a.top + a.height > b.top;
1821
+ var copyTextToClipboard = async (text) => {
1822
+ if (!text) return;
1823
+ if (navigator.clipboard?.writeText) {
1824
+ await navigator.clipboard.writeText(text);
1825
+ return;
1826
+ }
1827
+ const textarea = document.createElement("textarea");
1828
+ textarea.value = text;
1829
+ textarea.style.position = "fixed";
1830
+ textarea.style.opacity = "0";
1831
+ document.body.appendChild(textarea);
1832
+ textarea.select();
1833
+ document.execCommand("copy");
1834
+ textarea.remove();
1835
+ };
1836
+ var extractTextFromHighlightRect = (anchor, rect) => {
1837
+ const page = anchor.closest(".page");
1838
+ const textLayer = page?.querySelector(".textLayer");
1839
+ if (!page || !textLayer) return "";
1840
+ const pageRect = page.getBoundingClientRect();
1841
+ const targetRect = {
1842
+ left: pageRect.left + rect.left,
1843
+ top: pageRect.top + rect.top,
1844
+ width: rect.width,
1845
+ height: rect.height
1846
+ };
1847
+ const matches = Array.from(textLayer.querySelectorAll("span")).map((span) => {
1848
+ const spanRect = span.getBoundingClientRect();
1849
+ return {
1850
+ text: span.textContent || "",
1851
+ rect: {
1852
+ left: spanRect.left,
1853
+ top: spanRect.top,
1854
+ width: spanRect.width,
1855
+ height: spanRect.height
1856
+ }
1857
+ };
1858
+ }).filter(({ text, rect: rect2 }) => text.trim() && intersects(rect2, targetRect)).sort((a, b) => {
1859
+ const lineDelta = a.rect.top - b.rect.top;
1860
+ return Math.abs(lineDelta) > 4 ? lineDelta : a.rect.left - b.rect.left;
1861
+ });
1862
+ return matches.map(({ text }) => text).join(" ").replace(/\s+/g, " ").trim();
1863
+ };
1864
+
1865
+ // src/lib/highlight-config-layer.ts
1866
+ var findOrCreateHighlightConfigLayer = (anchor) => {
1867
+ const pageLayer = anchor.closest(".page");
1868
+ if (!pageLayer) return null;
1869
+ const doc = getDocument(pageLayer);
1870
+ let layer = Array.from(pageLayer.children).find(
1871
+ (child) => child.classList.contains("PdfHighlighter__config-layer")
1872
+ );
1873
+ if (!layer) {
1874
+ layer = doc.createElement("div");
1875
+ layer.className = "PdfHighlighter__config-layer";
1876
+ pageLayer.appendChild(layer);
1877
+ }
1878
+ return layer;
1879
+ };
1880
+
1881
+ // src/components/TextHighlight.tsx
1681
1882
  var DefaultStyleIcon = () => /* @__PURE__ */ React7.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React7.createElement("path", { d: "M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" }));
1682
1883
  var DefaultDeleteIcon = () => /* @__PURE__ */ React7.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React7.createElement("path", { d: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" }));
1884
+ var DefaultCopyIcon = () => /* @__PURE__ */ React7.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React7.createElement("path", { d: "M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" }));
1885
+ var DefaultCopiedIcon = () => /* @__PURE__ */ React7.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React7.createElement("path", { d: "M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" }));
1683
1886
  var HighlightIcon = () => /* @__PURE__ */ React7.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React7.createElement("path", { d: "M6 14l3 3v5h6v-5l3-3V9H6v5zm5-12h2v3h-2V2zM3.5 5.875L4.914 4.46l2.12 2.122L5.622 8 3.5 5.875zm13.46.71l2.123-2.12 1.414 1.414L18.375 8l-1.414-1.414z" }));
1684
1887
  var UnderlineIcon = () => /* @__PURE__ */ React7.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React7.createElement("path", { d: "M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z" }));
1685
1888
  var StrikethroughIcon = () => /* @__PURE__ */ React7.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React7.createElement("path", { d: "M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z" }));
@@ -1709,12 +1912,28 @@ var TextHighlight = ({
1709
1912
  onDelete,
1710
1913
  styleIcon,
1711
1914
  deleteIcon,
1915
+ copyText,
1712
1916
  colorPresets = DEFAULT_COLOR_PRESETS
1713
1917
  }) => {
1714
1918
  const [isStylePanelOpen, setIsStylePanelOpen] = useState6(false);
1715
1919
  const [isHovered, setIsHovered] = useState6(false);
1920
+ const [isCopied, setIsCopied] = useState6(false);
1921
+ const [configLayer, setConfigLayer] = useState6(null);
1716
1922
  const stylePanelRef = useRef6(null);
1717
1923
  const containerRef = useRef6(null);
1924
+ const copyResetTimeoutRef = useRef6(null);
1925
+ useLayoutEffect3(() => {
1926
+ if (containerRef.current) {
1927
+ setConfigLayer(findOrCreateHighlightConfigLayer(containerRef.current));
1928
+ }
1929
+ }, []);
1930
+ useEffect5(() => {
1931
+ return () => {
1932
+ if (copyResetTimeoutRef.current) {
1933
+ window.clearTimeout(copyResetTimeoutRef.current);
1934
+ }
1935
+ };
1936
+ }, []);
1718
1937
  useEffect5(() => {
1719
1938
  if (!isStylePanelOpen) return;
1720
1939
  const handleClickOutside = (e) => {
@@ -1753,6 +1972,19 @@ var TextHighlight = ({
1753
1972
  }
1754
1973
  return baseStyle;
1755
1974
  };
1975
+ const handleCopy = async (event) => {
1976
+ event.stopPropagation();
1977
+ const text = copyText || highlight.content?.text || (containerRef.current && firstRect ? extractTextFromHighlightRect(containerRef.current, firstRect) : "");
1978
+ await copyTextToClipboard(text);
1979
+ setIsCopied(true);
1980
+ if (copyResetTimeoutRef.current) {
1981
+ window.clearTimeout(copyResetTimeoutRef.current);
1982
+ }
1983
+ copyResetTimeoutRef.current = window.setTimeout(() => {
1984
+ setIsCopied(false);
1985
+ copyResetTimeoutRef.current = null;
1986
+ }, 1500);
1987
+ };
1756
1988
  return /* @__PURE__ */ React7.createElement(
1757
1989
  "div",
1758
1990
  {
@@ -1760,107 +1992,120 @@ var TextHighlight = ({
1760
1992
  onContextMenu,
1761
1993
  ref: containerRef
1762
1994
  },
1763
- (onStyleChange || onDelete) && firstRect && /* @__PURE__ */ React7.createElement(
1764
- "div",
1765
- {
1766
- className: "TextHighlight__toolbar-wrapper",
1767
- style: {
1768
- position: "absolute",
1769
- left: firstRect.left,
1770
- top: firstRect.top - 28,
1771
- paddingBottom: 12
1772
- },
1773
- onMouseEnter: () => setIsHovered(true),
1774
- onMouseLeave: () => setIsHovered(false)
1775
- },
1995
+ configLayer && (onStyleChange || onDelete) && firstRect && createPortal(
1776
1996
  /* @__PURE__ */ React7.createElement(
1777
1997
  "div",
1778
1998
  {
1779
- className: `TextHighlight__toolbar ${isHovered || isStylePanelOpen ? "TextHighlight__toolbar--visible" : ""}`
1999
+ className: "TextHighlight__toolbar-wrapper",
2000
+ style: {
2001
+ position: "absolute",
2002
+ left: firstRect.left,
2003
+ top: firstRect.top - 28,
2004
+ paddingBottom: 12
2005
+ },
2006
+ onMouseEnter: () => setIsHovered(true),
2007
+ onMouseLeave: () => setIsHovered(false)
1780
2008
  },
1781
- onStyleChange && /* @__PURE__ */ React7.createElement(
1782
- "button",
2009
+ /* @__PURE__ */ React7.createElement(
2010
+ "div",
1783
2011
  {
1784
- className: "TextHighlight__style-button",
1785
- onClick: (e) => {
1786
- e.stopPropagation();
1787
- setIsStylePanelOpen(!isStylePanelOpen);
1788
- },
1789
- title: "Change style",
1790
- type: "button"
2012
+ className: `TextHighlight__toolbar ${isHovered || isScrolledTo || isStylePanelOpen ? "TextHighlight__toolbar--visible" : ""}`
1791
2013
  },
1792
- styleIcon || /* @__PURE__ */ React7.createElement(DefaultStyleIcon, null)
2014
+ onStyleChange && /* @__PURE__ */ React7.createElement(
2015
+ "button",
2016
+ {
2017
+ className: "TextHighlight__style-button",
2018
+ onClick: (e) => {
2019
+ e.stopPropagation();
2020
+ setIsStylePanelOpen(!isStylePanelOpen);
2021
+ },
2022
+ title: "Change style",
2023
+ type: "button"
2024
+ },
2025
+ styleIcon || /* @__PURE__ */ React7.createElement(DefaultStyleIcon, null)
2026
+ ),
2027
+ /* @__PURE__ */ React7.createElement(
2028
+ "button",
2029
+ {
2030
+ className: "TextHighlight__copy-button",
2031
+ onClick: handleCopy,
2032
+ title: isCopied ? "Copied" : "Copy text",
2033
+ type: "button"
2034
+ },
2035
+ isCopied ? /* @__PURE__ */ React7.createElement(DefaultCopiedIcon, null) : /* @__PURE__ */ React7.createElement(DefaultCopyIcon, null)
2036
+ ),
2037
+ onDelete && /* @__PURE__ */ React7.createElement(
2038
+ "button",
2039
+ {
2040
+ className: "TextHighlight__delete-button",
2041
+ onClick: (e) => {
2042
+ e.stopPropagation();
2043
+ onDelete();
2044
+ },
2045
+ title: "Delete",
2046
+ type: "button"
2047
+ },
2048
+ deleteIcon || /* @__PURE__ */ React7.createElement(DefaultDeleteIcon, null)
2049
+ )
1793
2050
  ),
1794
- onDelete && /* @__PURE__ */ React7.createElement(
1795
- "button",
2051
+ isStylePanelOpen && onStyleChange && /* @__PURE__ */ React7.createElement(
2052
+ "div",
1796
2053
  {
1797
- className: "TextHighlight__delete-button",
1798
- onClick: (e) => {
1799
- e.stopPropagation();
1800
- onDelete();
1801
- },
1802
- title: "Delete",
1803
- type: "button"
2054
+ className: "TextHighlight__style-panel",
2055
+ ref: stylePanelRef,
2056
+ onClick: (e) => e.stopPropagation()
1804
2057
  },
1805
- deleteIcon || /* @__PURE__ */ React7.createElement(DefaultDeleteIcon, null)
2058
+ /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__style-row" }, /* @__PURE__ */ React7.createElement("label", null, "Style"), /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__style-buttons" }, /* @__PURE__ */ React7.createElement(
2059
+ "button",
2060
+ {
2061
+ type: "button",
2062
+ className: `TextHighlight__style-type-button ${highlightStyle === "highlight" ? "active" : ""}`,
2063
+ onClick: () => onStyleChange({ highlightStyle: "highlight" }),
2064
+ title: "Highlight"
2065
+ },
2066
+ /* @__PURE__ */ React7.createElement(HighlightIcon, null)
2067
+ ), /* @__PURE__ */ React7.createElement(
2068
+ "button",
2069
+ {
2070
+ type: "button",
2071
+ className: `TextHighlight__style-type-button ${highlightStyle === "underline" ? "active" : ""}`,
2072
+ onClick: () => onStyleChange({ highlightStyle: "underline" }),
2073
+ title: "Underline"
2074
+ },
2075
+ /* @__PURE__ */ React7.createElement(UnderlineIcon, null)
2076
+ ), /* @__PURE__ */ React7.createElement(
2077
+ "button",
2078
+ {
2079
+ type: "button",
2080
+ className: `TextHighlight__style-type-button ${highlightStyle === "strikethrough" ? "active" : ""}`,
2081
+ onClick: () => onStyleChange({ highlightStyle: "strikethrough" }),
2082
+ title: "Strikethrough"
2083
+ },
2084
+ /* @__PURE__ */ React7.createElement(StrikethroughIcon, null)
2085
+ ))),
2086
+ /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__style-row" }, /* @__PURE__ */ React7.createElement("label", null, "Color"), /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__color-options" }, /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__color-presets" }, colorPresets.map((c) => /* @__PURE__ */ React7.createElement(
2087
+ "button",
2088
+ {
2089
+ key: c,
2090
+ type: "button",
2091
+ className: `TextHighlight__color-preset ${highlightColor === c ? "active" : ""}`,
2092
+ style: { backgroundColor: c },
2093
+ onClick: () => onStyleChange({ highlightColor: c }),
2094
+ title: c
2095
+ }
2096
+ ))), /* @__PURE__ */ React7.createElement(
2097
+ "input",
2098
+ {
2099
+ type: "color",
2100
+ value: highlightColor,
2101
+ onChange: (e) => {
2102
+ onStyleChange({ highlightColor: e.target.value });
2103
+ }
2104
+ }
2105
+ )))
1806
2106
  )
1807
2107
  ),
1808
- isStylePanelOpen && onStyleChange && /* @__PURE__ */ React7.createElement(
1809
- "div",
1810
- {
1811
- className: "TextHighlight__style-panel",
1812
- ref: stylePanelRef,
1813
- onClick: (e) => e.stopPropagation()
1814
- },
1815
- /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__style-row" }, /* @__PURE__ */ React7.createElement("label", null, "Style"), /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__style-buttons" }, /* @__PURE__ */ React7.createElement(
1816
- "button",
1817
- {
1818
- type: "button",
1819
- className: `TextHighlight__style-type-button ${highlightStyle === "highlight" ? "active" : ""}`,
1820
- onClick: () => onStyleChange({ highlightStyle: "highlight" }),
1821
- title: "Highlight"
1822
- },
1823
- /* @__PURE__ */ React7.createElement(HighlightIcon, null)
1824
- ), /* @__PURE__ */ React7.createElement(
1825
- "button",
1826
- {
1827
- type: "button",
1828
- className: `TextHighlight__style-type-button ${highlightStyle === "underline" ? "active" : ""}`,
1829
- onClick: () => onStyleChange({ highlightStyle: "underline" }),
1830
- title: "Underline"
1831
- },
1832
- /* @__PURE__ */ React7.createElement(UnderlineIcon, null)
1833
- ), /* @__PURE__ */ React7.createElement(
1834
- "button",
1835
- {
1836
- type: "button",
1837
- className: `TextHighlight__style-type-button ${highlightStyle === "strikethrough" ? "active" : ""}`,
1838
- onClick: () => onStyleChange({ highlightStyle: "strikethrough" }),
1839
- title: "Strikethrough"
1840
- },
1841
- /* @__PURE__ */ React7.createElement(StrikethroughIcon, null)
1842
- ))),
1843
- /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__style-row" }, /* @__PURE__ */ React7.createElement("label", null, "Color"), /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__color-options" }, /* @__PURE__ */ React7.createElement("div", { className: "TextHighlight__color-presets" }, colorPresets.map((c) => /* @__PURE__ */ React7.createElement(
1844
- "button",
1845
- {
1846
- key: c,
1847
- type: "button",
1848
- className: `TextHighlight__color-preset ${highlightColor === c ? "active" : ""}`,
1849
- style: { backgroundColor: c },
1850
- onClick: () => onStyleChange({ highlightColor: c }),
1851
- title: c
1852
- }
1853
- ))), /* @__PURE__ */ React7.createElement(
1854
- "input",
1855
- {
1856
- type: "color",
1857
- value: highlightColor,
1858
- onChange: (e) => {
1859
- onStyleChange({ highlightColor: e.target.value });
1860
- }
1861
- }
1862
- )))
1863
- )
2108
+ configLayer
1864
2109
  ),
1865
2110
  /* @__PURE__ */ React7.createElement(
1866
2111
  "div",
@@ -1966,11 +2211,15 @@ var MonitoredHighlightContainer = ({
1966
2211
  import React10, {
1967
2212
  useState as useState7,
1968
2213
  useRef as useRef9,
1969
- useEffect as useEffect7
2214
+ useEffect as useEffect7,
2215
+ useLayoutEffect as useLayoutEffect4
1970
2216
  } from "react";
2217
+ import { createPortal as createPortal2 } from "react-dom";
1971
2218
  import { Rnd } from "react-rnd";
1972
2219
  var DefaultStyleIcon2 = () => /* @__PURE__ */ React10.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React10.createElement("path", { d: "M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" }));
1973
2220
  var DefaultDeleteIcon2 = () => /* @__PURE__ */ React10.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React10.createElement("path", { d: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" }));
2221
+ var DefaultCopyIcon2 = () => /* @__PURE__ */ React10.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React10.createElement("path", { d: "M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" }));
2222
+ var DefaultCopiedIcon2 = () => /* @__PURE__ */ React10.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React10.createElement("path", { d: "M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" }));
1974
2223
  var DEFAULT_COLOR_PRESETS2 = [
1975
2224
  "rgba(255, 226, 143, 1)",
1976
2225
  // Yellow (default)
@@ -1996,11 +2245,28 @@ var AreaHighlight = ({
1996
2245
  onDelete,
1997
2246
  styleIcon,
1998
2247
  deleteIcon,
2248
+ copyText,
1999
2249
  colorPresets = DEFAULT_COLOR_PRESETS2
2000
2250
  }) => {
2001
2251
  const [isStylePanelOpen, setIsStylePanelOpen] = useState7(false);
2002
2252
  const [isHovered, setIsHovered] = useState7(false);
2253
+ const [isCopied, setIsCopied] = useState7(false);
2254
+ const [configLayer, setConfigLayer] = useState7(null);
2003
2255
  const stylePanelRef = useRef9(null);
2256
+ const containerRef = useRef9(null);
2257
+ const copyResetTimeoutRef = useRef9(null);
2258
+ useLayoutEffect4(() => {
2259
+ if (containerRef.current) {
2260
+ setConfigLayer(findOrCreateHighlightConfigLayer(containerRef.current));
2261
+ }
2262
+ }, []);
2263
+ useEffect7(() => {
2264
+ return () => {
2265
+ if (copyResetTimeoutRef.current) {
2266
+ window.clearTimeout(copyResetTimeoutRef.current);
2267
+ }
2268
+ };
2269
+ }, []);
2004
2270
  useEffect7(() => {
2005
2271
  if (!isStylePanelOpen) return;
2006
2272
  const handleClickOutside = (e) => {
@@ -2022,85 +2288,115 @@ var AreaHighlight = ({
2022
2288
  ...style,
2023
2289
  backgroundColor: highlightColor
2024
2290
  };
2291
+ const handleCopy = async (event) => {
2292
+ event.stopPropagation();
2293
+ const text = copyText || highlight.content?.text || (containerRef.current ? extractTextFromHighlightRect(
2294
+ containerRef.current,
2295
+ highlight.position.boundingRect
2296
+ ) : "");
2297
+ await copyTextToClipboard(text);
2298
+ setIsCopied(true);
2299
+ if (copyResetTimeoutRef.current) {
2300
+ window.clearTimeout(copyResetTimeoutRef.current);
2301
+ }
2302
+ copyResetTimeoutRef.current = window.setTimeout(() => {
2303
+ setIsCopied(false);
2304
+ copyResetTimeoutRef.current = null;
2305
+ }, 1500);
2306
+ };
2025
2307
  return /* @__PURE__ */ React10.createElement(
2026
2308
  "div",
2027
2309
  {
2028
2310
  className: `AreaHighlight ${highlightClass}`,
2029
- onContextMenu
2311
+ onContextMenu,
2312
+ ref: containerRef
2030
2313
  },
2031
- (onStyleChange || onDelete) && /* @__PURE__ */ React10.createElement(
2032
- "div",
2033
- {
2034
- className: "AreaHighlight__toolbar-wrapper",
2035
- style: {
2036
- position: "absolute",
2037
- left: highlight.position.boundingRect.left,
2038
- top: highlight.position.boundingRect.top - 28,
2039
- paddingBottom: 12
2040
- },
2041
- onMouseEnter: () => setIsHovered(true),
2042
- onMouseLeave: () => setIsHovered(false)
2043
- },
2314
+ configLayer && (onStyleChange || onDelete) && createPortal2(
2044
2315
  /* @__PURE__ */ React10.createElement(
2045
2316
  "div",
2046
2317
  {
2047
- className: `AreaHighlight__toolbar ${isHovered || isStylePanelOpen ? "AreaHighlight__toolbar--visible" : ""}`
2318
+ className: "AreaHighlight__toolbar-wrapper",
2319
+ style: {
2320
+ position: "absolute",
2321
+ left: highlight.position.boundingRect.left,
2322
+ top: highlight.position.boundingRect.top - 28,
2323
+ paddingBottom: 12
2324
+ },
2325
+ onMouseEnter: () => setIsHovered(true),
2326
+ onMouseLeave: () => setIsHovered(false)
2048
2327
  },
2049
- onStyleChange && /* @__PURE__ */ React10.createElement(
2050
- "button",
2328
+ /* @__PURE__ */ React10.createElement(
2329
+ "div",
2051
2330
  {
2052
- className: "AreaHighlight__style-button",
2053
- onClick: (e) => {
2054
- e.stopPropagation();
2055
- setIsStylePanelOpen(!isStylePanelOpen);
2056
- },
2057
- title: "Change color",
2058
- type: "button"
2331
+ className: `AreaHighlight__toolbar ${isHovered || isScrolledTo || isStylePanelOpen ? "AreaHighlight__toolbar--visible" : ""}`
2059
2332
  },
2060
- styleIcon || /* @__PURE__ */ React10.createElement(DefaultStyleIcon2, null)
2333
+ onStyleChange && /* @__PURE__ */ React10.createElement(
2334
+ "button",
2335
+ {
2336
+ className: "AreaHighlight__style-button",
2337
+ onClick: (e) => {
2338
+ e.stopPropagation();
2339
+ setIsStylePanelOpen(!isStylePanelOpen);
2340
+ },
2341
+ title: "Change color",
2342
+ type: "button"
2343
+ },
2344
+ styleIcon || /* @__PURE__ */ React10.createElement(DefaultStyleIcon2, null)
2345
+ ),
2346
+ /* @__PURE__ */ React10.createElement(
2347
+ "button",
2348
+ {
2349
+ className: "AreaHighlight__copy-button",
2350
+ onClick: handleCopy,
2351
+ title: isCopied ? "Copied" : "Copy text",
2352
+ type: "button"
2353
+ },
2354
+ isCopied ? /* @__PURE__ */ React10.createElement(DefaultCopiedIcon2, null) : /* @__PURE__ */ React10.createElement(DefaultCopyIcon2, null)
2355
+ ),
2356
+ onDelete && /* @__PURE__ */ React10.createElement(
2357
+ "button",
2358
+ {
2359
+ className: "AreaHighlight__delete-button",
2360
+ onClick: (e) => {
2361
+ e.stopPropagation();
2362
+ onDelete();
2363
+ },
2364
+ title: "Delete",
2365
+ type: "button"
2366
+ },
2367
+ deleteIcon || /* @__PURE__ */ React10.createElement(DefaultDeleteIcon2, null)
2368
+ )
2061
2369
  ),
2062
- onDelete && /* @__PURE__ */ React10.createElement(
2063
- "button",
2370
+ isStylePanelOpen && onStyleChange && /* @__PURE__ */ React10.createElement(
2371
+ "div",
2064
2372
  {
2065
- className: "AreaHighlight__delete-button",
2066
- onClick: (e) => {
2067
- e.stopPropagation();
2068
- onDelete();
2069
- },
2070
- title: "Delete",
2071
- type: "button"
2373
+ className: "AreaHighlight__style-panel",
2374
+ ref: stylePanelRef,
2375
+ onClick: (e) => e.stopPropagation()
2072
2376
  },
2073
- deleteIcon || /* @__PURE__ */ React10.createElement(DefaultDeleteIcon2, null)
2377
+ /* @__PURE__ */ React10.createElement("div", { className: "AreaHighlight__style-row" }, /* @__PURE__ */ React10.createElement("label", null, "Color"), /* @__PURE__ */ React10.createElement("div", { className: "AreaHighlight__color-options" }, /* @__PURE__ */ React10.createElement("div", { className: "AreaHighlight__color-presets" }, colorPresets.map((c) => /* @__PURE__ */ React10.createElement(
2378
+ "button",
2379
+ {
2380
+ key: c,
2381
+ type: "button",
2382
+ className: `AreaHighlight__color-preset ${highlightColor === c ? "active" : ""}`,
2383
+ style: { backgroundColor: c },
2384
+ onClick: () => onStyleChange({ highlightColor: c }),
2385
+ title: c
2386
+ }
2387
+ ))), /* @__PURE__ */ React10.createElement(
2388
+ "input",
2389
+ {
2390
+ type: "color",
2391
+ value: highlightColor,
2392
+ onChange: (e) => {
2393
+ onStyleChange({ highlightColor: e.target.value });
2394
+ }
2395
+ }
2396
+ )))
2074
2397
  )
2075
2398
  ),
2076
- isStylePanelOpen && onStyleChange && /* @__PURE__ */ React10.createElement(
2077
- "div",
2078
- {
2079
- className: "AreaHighlight__style-panel",
2080
- ref: stylePanelRef,
2081
- onClick: (e) => e.stopPropagation()
2082
- },
2083
- /* @__PURE__ */ React10.createElement("div", { className: "AreaHighlight__style-row" }, /* @__PURE__ */ React10.createElement("label", null, "Color"), /* @__PURE__ */ React10.createElement("div", { className: "AreaHighlight__color-options" }, /* @__PURE__ */ React10.createElement("div", { className: "AreaHighlight__color-presets" }, colorPresets.map((c) => /* @__PURE__ */ React10.createElement(
2084
- "button",
2085
- {
2086
- key: c,
2087
- type: "button",
2088
- className: `AreaHighlight__color-preset ${highlightColor === c ? "active" : ""}`,
2089
- style: { backgroundColor: c },
2090
- onClick: () => onStyleChange({ highlightColor: c }),
2091
- title: c
2092
- }
2093
- ))), /* @__PURE__ */ React10.createElement(
2094
- "input",
2095
- {
2096
- type: "color",
2097
- value: highlightColor,
2098
- onChange: (e) => {
2099
- onStyleChange({ highlightColor: e.target.value });
2100
- }
2101
- }
2102
- )))
2103
- )
2399
+ configLayer
2104
2400
  ),
2105
2401
  /* @__PURE__ */ React10.createElement(
2106
2402
  Rnd,
@@ -2157,6 +2453,8 @@ var DefaultDragIcon = () => /* @__PURE__ */ React11.createElement("svg", { width
2157
2453
  var DefaultEditIcon = () => /* @__PURE__ */ React11.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React11.createElement("path", { d: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" }));
2158
2454
  var DefaultStyleIcon3 = () => /* @__PURE__ */ React11.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React11.createElement("path", { d: "M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" }));
2159
2455
  var DefaultDeleteIcon3 = () => /* @__PURE__ */ React11.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React11.createElement("path", { d: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" }));
2456
+ var DefaultCompactIcon = () => /* @__PURE__ */ React11.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React11.createElement("path", { d: "M6 3h9l5 5v13H6V3zm8 1.5V9h4.5L14 4.5zM8 12h8v1.5H8V12zm0 3h8v1.5H8V15zm0 3h5v1.5H8V18z" }));
2457
+ var DefaultCollapseIcon = () => /* @__PURE__ */ React11.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React11.createElement("path", { d: "M7 10v2h10v-2H7zm-2-7h14c1.1 0 2 .9 2 2v14c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V5c0-1.1.9-2 2-2zm0 2v14h14V5H5z" }));
2160
2458
  var DEFAULT_BACKGROUND_PRESETS = ["transparent", "#ffffc8", "#ffcdd2", "#c8e6c9", "#bbdefb", "#e1bee7"];
2161
2459
  var DEFAULT_TEXT_PRESETS = ["#333333", "#d32f2f", "#1976d2", "#388e3c", "#7b1fa2"];
2162
2460
  var FreetextHighlight = ({
@@ -2180,9 +2478,13 @@ var FreetextHighlight = ({
2180
2478
  backgroundColorPresets = DEFAULT_BACKGROUND_PRESETS,
2181
2479
  textColorPresets = DEFAULT_TEXT_PRESETS,
2182
2480
  onDelete,
2183
- deleteIcon
2481
+ deleteIcon,
2482
+ compact = false,
2483
+ compactSize = 32,
2484
+ compactIcon
2184
2485
  }) => {
2185
2486
  const [isEditing, setIsEditing] = useState8(false);
2487
+ const [isExpanded, setIsExpanded] = useState8(!compact);
2186
2488
  const [isStylePanelOpen, setIsStylePanelOpen] = useState8(false);
2187
2489
  const [text, setText] = useState8(highlight.content?.text || "");
2188
2490
  const textareaRef = useRef10(null);
@@ -2190,6 +2492,13 @@ var FreetextHighlight = ({
2190
2492
  useEffect8(() => {
2191
2493
  setText(highlight.content?.text || "");
2192
2494
  }, [highlight.content?.text]);
2495
+ useEffect8(() => {
2496
+ setIsExpanded(!compact);
2497
+ setIsStylePanelOpen(false);
2498
+ if (!compact) {
2499
+ setIsEditing(false);
2500
+ }
2501
+ }, [compact]);
2193
2502
  useEffect8(() => {
2194
2503
  if (isEditing && textareaRef.current) {
2195
2504
  textareaRef.current.focus();
@@ -2213,10 +2522,14 @@ var FreetextHighlight = ({
2213
2522
  }, [isStylePanelOpen]);
2214
2523
  const highlightClass = isScrolledTo ? "FreetextHighlight--scrolledTo" : "";
2215
2524
  const editingClass = isEditing ? "FreetextHighlight--editing" : "";
2216
- const key = `${highlight.position.boundingRect.width}${highlight.position.boundingRect.height}${highlight.position.boundingRect.left}${highlight.position.boundingRect.top}`;
2525
+ const compactClass = compact ? "FreetextHighlight--compact" : "";
2526
+ const isCompactCollapsed = compact && !isExpanded && !isEditing && !isStylePanelOpen;
2527
+ const collapsedClass = isCompactCollapsed ? "FreetextHighlight--collapsed" : "";
2528
+ const key = `${highlight.position.boundingRect.width}${highlight.position.boundingRect.height}${highlight.position.boundingRect.left}${highlight.position.boundingRect.top}${isCompactCollapsed ? "collapsed" : "expanded"}`;
2217
2529
  const handleTextClick = (e) => {
2218
2530
  e.stopPropagation();
2219
2531
  if (!isEditing) {
2532
+ setIsExpanded(true);
2220
2533
  setIsEditing(true);
2221
2534
  onEditStart?.();
2222
2535
  }
@@ -2251,7 +2564,7 @@ var FreetextHighlight = ({
2251
2564
  return /* @__PURE__ */ React11.createElement(
2252
2565
  "div",
2253
2566
  {
2254
- className: `FreetextHighlight ${highlightClass} ${editingClass}`,
2567
+ className: `FreetextHighlight ${highlightClass} ${editingClass} ${compactClass} ${collapsedClass}`,
2255
2568
  onContextMenu
2256
2569
  },
2257
2570
  /* @__PURE__ */ React11.createElement(
@@ -2274,14 +2587,14 @@ var FreetextHighlight = ({
2274
2587
  default: {
2275
2588
  x: highlight.position.boundingRect.left,
2276
2589
  y: highlight.position.boundingRect.top,
2277
- width: highlight.position.boundingRect.width || 150,
2278
- height: highlight.position.boundingRect.height || 80
2590
+ width: isCompactCollapsed ? compactSize : highlight.position.boundingRect.width || 150,
2591
+ height: isCompactCollapsed ? compactSize : highlight.position.boundingRect.height || 80
2279
2592
  },
2280
- minWidth: 100,
2281
- minHeight: 50,
2593
+ minWidth: isCompactCollapsed ? compactSize : 100,
2594
+ minHeight: isCompactCollapsed ? compactSize : 50,
2282
2595
  key,
2283
2596
  bounds,
2284
- enableResizing: {
2597
+ enableResizing: isCompactCollapsed ? false : {
2285
2598
  top: false,
2286
2599
  right: true,
2287
2600
  bottom: true,
@@ -2306,9 +2619,21 @@ var FreetextHighlight = ({
2306
2619
  onEditStart?.();
2307
2620
  }
2308
2621
  },
2309
- cancel: ".FreetextHighlight__text, .FreetextHighlight__input, .FreetextHighlight__edit-button, .FreetextHighlight__style-button, .FreetextHighlight__style-panel, .FreetextHighlight__delete-button"
2622
+ cancel: ".FreetextHighlight__text, .FreetextHighlight__input, .FreetextHighlight__edit-button, .FreetextHighlight__style-button, .FreetextHighlight__style-panel, .FreetextHighlight__delete-button, .FreetextHighlight__collapse-button, .FreetextHighlight__compact-button"
2310
2623
  },
2311
- /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__container", style: containerStyle }, /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__toolbar" }, /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__drag-handle", title: "Drag to move" }, dragIcon || /* @__PURE__ */ React11.createElement(DefaultDragIcon, null)), /* @__PURE__ */ React11.createElement(
2624
+ /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__container", style: containerStyle }, isCompactCollapsed ? /* @__PURE__ */ React11.createElement(
2625
+ "button",
2626
+ {
2627
+ className: "FreetextHighlight__compact-button",
2628
+ type: "button",
2629
+ title: text || "Open note",
2630
+ onClick: (event) => {
2631
+ event.stopPropagation();
2632
+ setIsExpanded(true);
2633
+ }
2634
+ },
2635
+ compactIcon || /* @__PURE__ */ React11.createElement(DefaultCompactIcon, null)
2636
+ ) : /* @__PURE__ */ React11.createElement(React11.Fragment, null, /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__toolbar" }, /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__drag-handle", title: "Drag to move" }, dragIcon || /* @__PURE__ */ React11.createElement(DefaultDragIcon, null)), /* @__PURE__ */ React11.createElement(
2312
2637
  "button",
2313
2638
  {
2314
2639
  className: "FreetextHighlight__edit-button",
@@ -2329,6 +2654,19 @@ var FreetextHighlight = ({
2329
2654
  type: "button"
2330
2655
  },
2331
2656
  styleIcon || /* @__PURE__ */ React11.createElement(DefaultStyleIcon3, null)
2657
+ ), compact && /* @__PURE__ */ React11.createElement(
2658
+ "button",
2659
+ {
2660
+ className: "FreetextHighlight__collapse-button",
2661
+ onClick: (e) => {
2662
+ e.stopPropagation();
2663
+ setIsExpanded(false);
2664
+ setIsStylePanelOpen(false);
2665
+ },
2666
+ title: "Collapse note",
2667
+ type: "button"
2668
+ },
2669
+ /* @__PURE__ */ React11.createElement(DefaultCollapseIcon, null)
2332
2670
  ), onDelete && /* @__PURE__ */ React11.createElement(
2333
2671
  "button",
2334
2672
  {
@@ -2433,7 +2771,7 @@ var FreetextHighlight = ({
2433
2771
  onKeyDown: handleKeyDown,
2434
2772
  onClick: (e) => e.stopPropagation()
2435
2773
  }
2436
- ) : /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__text" }, text || "New note")))
2774
+ ) : /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__text" }, text || "New note"))))
2437
2775
  )
2438
2776
  );
2439
2777
  };
@@ -2687,7 +3025,8 @@ var SignaturePad = ({
2687
3025
  };
2688
3026
 
2689
3027
  // src/components/DrawingHighlight.tsx
2690
- import React14, { useState as useState9, useCallback as useCallback4, useEffect as useEffect10, useRef as useRef12 } from "react";
3028
+ import React14, { useState as useState9, useCallback as useCallback4, useEffect as useEffect10, useLayoutEffect as useLayoutEffect5, useRef as useRef12 } from "react";
3029
+ import { createPortal as createPortal3 } from "react-dom";
2691
3030
  import { Rnd as Rnd4 } from "react-rnd";
2692
3031
  var DRAWING_COLORS = ["#000000", "#FF0000", "#0000FF", "#00FF00", "#FFFF00"];
2693
3032
  var STROKE_WIDTHS = [
@@ -2734,7 +3073,15 @@ var DrawingHighlight = ({
2734
3073
  }) => {
2735
3074
  const highlightClass = isScrolledTo ? "DrawingHighlight--scrolledTo" : "";
2736
3075
  const [showStyleControls, setShowStyleControls] = useState9(false);
3076
+ const [isHovered, setIsHovered] = useState9(false);
3077
+ const [configLayer, setConfigLayer] = useState9(null);
2737
3078
  const styleControlsRef = useRef12(null);
3079
+ const containerRef = useRef12(null);
3080
+ useLayoutEffect5(() => {
3081
+ if (containerRef.current) {
3082
+ setConfigLayer(findOrCreateHighlightConfigLayer(containerRef.current));
3083
+ }
3084
+ }, []);
2738
3085
  useEffect10(() => {
2739
3086
  if (!showStyleControls) return;
2740
3087
  const handleClickOutside = (e) => {
@@ -2787,8 +3134,78 @@ var DrawingHighlight = ({
2787
3134
  "div",
2788
3135
  {
2789
3136
  className: `DrawingHighlight ${highlightClass}`,
2790
- onContextMenu
3137
+ onContextMenu,
3138
+ ref: containerRef
2791
3139
  },
3140
+ configLayer && createPortal3(
3141
+ /* @__PURE__ */ React14.createElement(
3142
+ "div",
3143
+ {
3144
+ className: `DrawingHighlight__toolbar DrawingHighlight__toolbar--floating ${isHovered || isScrolledTo || showStyleControls ? "DrawingHighlight__toolbar--visible" : ""}`,
3145
+ style: {
3146
+ left: highlight.position.boundingRect.left + 4,
3147
+ top: highlight.position.boundingRect.top + 4
3148
+ },
3149
+ onMouseEnter: () => setIsHovered(true),
3150
+ onMouseLeave: () => setIsHovered(false)
3151
+ },
3152
+ /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__drag-handle", title: "Drag to move" }, dragIcon || /* @__PURE__ */ React14.createElement(DefaultDragIcon3, null)),
3153
+ strokes && strokes.length > 0 && onStyleChange && /* @__PURE__ */ React14.createElement(
3154
+ "button",
3155
+ {
3156
+ type: "button",
3157
+ className: "DrawingHighlight__style-button",
3158
+ title: "Edit style",
3159
+ onClick: (e) => {
3160
+ e.stopPropagation();
3161
+ setShowStyleControls(!showStyleControls);
3162
+ }
3163
+ },
3164
+ /* @__PURE__ */ React14.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React14.createElement("path", { d: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" }))
3165
+ ),
3166
+ onDelete && /* @__PURE__ */ React14.createElement(
3167
+ "button",
3168
+ {
3169
+ className: "DrawingHighlight__delete-button",
3170
+ onClick: (e) => {
3171
+ e.stopPropagation();
3172
+ onDelete();
3173
+ },
3174
+ title: "Delete",
3175
+ type: "button"
3176
+ },
3177
+ deleteIcon || /* @__PURE__ */ React14.createElement(DefaultDeleteIcon5, null)
3178
+ ),
3179
+ showStyleControls && strokes && strokes.length > 0 && onStyleChange && /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__style-controls", ref: styleControlsRef }, /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__color-picker" }, DRAWING_COLORS.map((color) => /* @__PURE__ */ React14.createElement(
3180
+ "button",
3181
+ {
3182
+ key: color,
3183
+ type: "button",
3184
+ className: `DrawingHighlight__color-button ${currentColor === color ? "active" : ""}`,
3185
+ style: { backgroundColor: color },
3186
+ onClick: (e) => {
3187
+ e.stopPropagation();
3188
+ handleColorChange(color);
3189
+ },
3190
+ title: `Color: ${color}`
3191
+ }
3192
+ ))), /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__width-picker" }, STROKE_WIDTHS.map((w) => /* @__PURE__ */ React14.createElement(
3193
+ "button",
3194
+ {
3195
+ key: w.value,
3196
+ type: "button",
3197
+ className: `DrawingHighlight__width-button ${currentWidth === w.value ? "active" : ""}`,
3198
+ onClick: (e) => {
3199
+ e.stopPropagation();
3200
+ handleWidthChange(w.value);
3201
+ },
3202
+ title: w.label
3203
+ },
3204
+ w.label
3205
+ ))))
3206
+ ),
3207
+ configLayer
3208
+ ),
2792
3209
  /* @__PURE__ */ React14.createElement(
2793
3210
  Rnd4,
2794
3211
  {
@@ -2826,72 +3243,29 @@ var DrawingHighlight = ({
2826
3243
  key,
2827
3244
  bounds,
2828
3245
  lockAspectRatio: false,
2829
- dragHandleClassName: "DrawingHighlight__drag-handle",
2830
3246
  onClick: (event) => {
2831
3247
  event.stopPropagation();
2832
3248
  event.preventDefault();
2833
3249
  },
2834
3250
  style
2835
3251
  },
2836
- /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__container" }, /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__toolbar" }, /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__drag-handle", title: "Drag to move" }, dragIcon || /* @__PURE__ */ React14.createElement(DefaultDragIcon3, null)), strokes && strokes.length > 0 && onStyleChange && /* @__PURE__ */ React14.createElement(
2837
- "button",
2838
- {
2839
- type: "button",
2840
- className: "DrawingHighlight__style-button",
2841
- title: "Edit style",
2842
- onClick: (e) => {
2843
- e.stopPropagation();
2844
- setShowStyleControls(!showStyleControls);
2845
- }
2846
- },
2847
- /* @__PURE__ */ React14.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React14.createElement("path", { d: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" }))
2848
- ), onDelete && /* @__PURE__ */ React14.createElement(
2849
- "button",
2850
- {
2851
- className: "DrawingHighlight__delete-button",
2852
- onClick: (e) => {
2853
- e.stopPropagation();
2854
- onDelete();
2855
- },
2856
- title: "Delete",
2857
- type: "button"
2858
- },
2859
- deleteIcon || /* @__PURE__ */ React14.createElement(DefaultDeleteIcon5, null)
2860
- )), showStyleControls && strokes && strokes.length > 0 && onStyleChange && /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__style-controls", ref: styleControlsRef }, /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__color-picker" }, DRAWING_COLORS.map((color) => /* @__PURE__ */ React14.createElement(
2861
- "button",
2862
- {
2863
- key: color,
2864
- type: "button",
2865
- className: `DrawingHighlight__color-button ${currentColor === color ? "active" : ""}`,
2866
- style: { backgroundColor: color },
2867
- onClick: (e) => {
2868
- e.stopPropagation();
2869
- handleColorChange(color);
2870
- },
2871
- title: `Color: ${color}`
2872
- }
2873
- ))), /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__width-picker" }, STROKE_WIDTHS.map((w) => /* @__PURE__ */ React14.createElement(
2874
- "button",
3252
+ /* @__PURE__ */ React14.createElement(
3253
+ "div",
2875
3254
  {
2876
- key: w.value,
2877
- type: "button",
2878
- className: `DrawingHighlight__width-button ${currentWidth === w.value ? "active" : ""}`,
2879
- onClick: (e) => {
2880
- e.stopPropagation();
2881
- handleWidthChange(w.value);
2882
- },
2883
- title: w.label
3255
+ className: "DrawingHighlight__container",
3256
+ onMouseEnter: () => setIsHovered(true),
3257
+ onMouseLeave: () => setIsHovered(false)
2884
3258
  },
2885
- w.label
2886
- )))), /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__content" }, imageUrl ? /* @__PURE__ */ React14.createElement(
2887
- "img",
2888
- {
2889
- src: imageUrl,
2890
- alt: "Drawing",
2891
- className: "DrawingHighlight__image",
2892
- draggable: false
2893
- }
2894
- ) : /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__placeholder" }, "No drawing")))
3259
+ /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__content" }, imageUrl ? /* @__PURE__ */ React14.createElement(
3260
+ "img",
3261
+ {
3262
+ src: imageUrl,
3263
+ alt: "Drawing",
3264
+ className: "DrawingHighlight__image",
3265
+ draggable: false
3266
+ }
3267
+ ) : /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__placeholder" }, "No drawing"))
3268
+ )
2895
3269
  )
2896
3270
  );
2897
3271
  };
@@ -2900,8 +3274,10 @@ var DrawingHighlight = ({
2900
3274
  import React15, {
2901
3275
  useState as useState10,
2902
3276
  useRef as useRef13,
2903
- useEffect as useEffect11
3277
+ useEffect as useEffect11,
3278
+ useLayoutEffect as useLayoutEffect6
2904
3279
  } from "react";
3280
+ import { createPortal as createPortal4 } from "react-dom";
2905
3281
  import { Rnd as Rnd5 } from "react-rnd";
2906
3282
  var DefaultStyleIcon4 = () => /* @__PURE__ */ React15.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React15.createElement("path", { d: "M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-.99 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8zm-5.5 9c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12zm3-4C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8zm5 0c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8zm3 4c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z" }));
2907
3283
  var DefaultDeleteIcon6 = () => /* @__PURE__ */ React15.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor" }, /* @__PURE__ */ React15.createElement("path", { d: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" }));
@@ -2944,7 +3320,14 @@ var ShapeHighlight = ({
2944
3320
  }) => {
2945
3321
  const [isStylePanelOpen, setIsStylePanelOpen] = useState10(false);
2946
3322
  const [isHovered, setIsHovered] = useState10(false);
3323
+ const [configLayer, setConfigLayer] = useState10(null);
2947
3324
  const stylePanelRef = useRef13(null);
3325
+ const containerRef = useRef13(null);
3326
+ useLayoutEffect6(() => {
3327
+ if (containerRef.current) {
3328
+ setConfigLayer(findOrCreateHighlightConfigLayer(containerRef.current));
3329
+ }
3330
+ }, []);
2948
3331
  useEffect11(() => {
2949
3332
  if (!isStylePanelOpen) return;
2950
3333
  const handleClickOutside = (e) => {
@@ -3056,92 +3439,96 @@ var ShapeHighlight = ({
3056
3439
  "div",
3057
3440
  {
3058
3441
  className: `ShapeHighlight ${highlightClass}`,
3059
- onContextMenu
3442
+ onContextMenu,
3443
+ ref: containerRef
3060
3444
  },
3061
- (onStyleChange || onDelete) && /* @__PURE__ */ React15.createElement(
3062
- "div",
3063
- {
3064
- className: "ShapeHighlight__toolbar-wrapper",
3065
- style: {
3066
- position: "absolute",
3067
- left: highlight.position.boundingRect.left,
3068
- top: highlight.position.boundingRect.top - 28,
3069
- paddingBottom: 12
3070
- },
3071
- onMouseEnter: () => setIsHovered(true),
3072
- onMouseLeave: () => setIsHovered(false)
3073
- },
3445
+ configLayer && (onStyleChange || onDelete) && createPortal4(
3074
3446
  /* @__PURE__ */ React15.createElement(
3075
3447
  "div",
3076
3448
  {
3077
- className: `ShapeHighlight__toolbar ${isHovered || isStylePanelOpen ? "ShapeHighlight__toolbar--visible" : ""}`
3449
+ className: "ShapeHighlight__toolbar-wrapper",
3450
+ style: {
3451
+ position: "absolute",
3452
+ left: highlight.position.boundingRect.left,
3453
+ top: highlight.position.boundingRect.top - 28,
3454
+ paddingBottom: 12
3455
+ },
3456
+ onMouseEnter: () => setIsHovered(true),
3457
+ onMouseLeave: () => setIsHovered(false)
3078
3458
  },
3079
- onStyleChange && /* @__PURE__ */ React15.createElement(
3080
- "button",
3459
+ /* @__PURE__ */ React15.createElement(
3460
+ "div",
3081
3461
  {
3082
- className: "ShapeHighlight__style-button",
3083
- onClick: (e) => {
3084
- e.stopPropagation();
3085
- setIsStylePanelOpen(!isStylePanelOpen);
3086
- },
3087
- title: "Change style",
3088
- type: "button"
3462
+ className: `ShapeHighlight__toolbar ${isHovered || isScrolledTo || isStylePanelOpen ? "ShapeHighlight__toolbar--visible" : ""}`
3089
3463
  },
3090
- styleIcon || /* @__PURE__ */ React15.createElement(DefaultStyleIcon4, null)
3464
+ onStyleChange && /* @__PURE__ */ React15.createElement(
3465
+ "button",
3466
+ {
3467
+ className: "ShapeHighlight__style-button",
3468
+ onClick: (e) => {
3469
+ e.stopPropagation();
3470
+ setIsStylePanelOpen(!isStylePanelOpen);
3471
+ },
3472
+ title: "Change style",
3473
+ type: "button"
3474
+ },
3475
+ styleIcon || /* @__PURE__ */ React15.createElement(DefaultStyleIcon4, null)
3476
+ ),
3477
+ onDelete && /* @__PURE__ */ React15.createElement(
3478
+ "button",
3479
+ {
3480
+ className: "ShapeHighlight__delete-button",
3481
+ onClick: (e) => {
3482
+ e.stopPropagation();
3483
+ onDelete();
3484
+ },
3485
+ title: "Delete",
3486
+ type: "button"
3487
+ },
3488
+ deleteIcon || /* @__PURE__ */ React15.createElement(DefaultDeleteIcon6, null)
3489
+ )
3091
3490
  ),
3092
- onDelete && /* @__PURE__ */ React15.createElement(
3093
- "button",
3491
+ isStylePanelOpen && onStyleChange && /* @__PURE__ */ React15.createElement(
3492
+ "div",
3094
3493
  {
3095
- className: "ShapeHighlight__delete-button",
3096
- onClick: (e) => {
3097
- e.stopPropagation();
3098
- onDelete();
3099
- },
3100
- title: "Delete",
3101
- type: "button"
3494
+ className: "ShapeHighlight__style-panel",
3495
+ ref: stylePanelRef,
3496
+ onClick: (e) => e.stopPropagation()
3102
3497
  },
3103
- deleteIcon || /* @__PURE__ */ React15.createElement(DefaultDeleteIcon6, null)
3498
+ /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__style-row" }, /* @__PURE__ */ React15.createElement("label", null, "Color"), /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__color-options" }, /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__color-presets" }, colorPresets.map((c) => /* @__PURE__ */ React15.createElement(
3499
+ "button",
3500
+ {
3501
+ key: c,
3502
+ type: "button",
3503
+ className: `ShapeHighlight__color-preset ${strokeColor === c ? "active" : ""}`,
3504
+ style: { backgroundColor: c },
3505
+ onClick: () => onStyleChange({ strokeColor: c }),
3506
+ title: c
3507
+ }
3508
+ ))), /* @__PURE__ */ React15.createElement(
3509
+ "input",
3510
+ {
3511
+ type: "color",
3512
+ value: strokeColor,
3513
+ onChange: (e) => {
3514
+ onStyleChange({ strokeColor: e.target.value });
3515
+ }
3516
+ }
3517
+ ))),
3518
+ /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__style-row" }, /* @__PURE__ */ React15.createElement("label", null, "Width"), /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__width-options" }, STROKE_WIDTHS2.map((w) => /* @__PURE__ */ React15.createElement(
3519
+ "button",
3520
+ {
3521
+ key: w.value,
3522
+ type: "button",
3523
+ className: `ShapeHighlight__width-button ${strokeWidth === w.value ? "active" : ""}`,
3524
+ onClick: () => onStyleChange({ strokeWidth: w.value }),
3525
+ title: w.label
3526
+ },
3527
+ w.label
3528
+ ))))
3104
3529
  )
3105
3530
  ),
3106
- isStylePanelOpen && onStyleChange && /* @__PURE__ */ React15.createElement(
3107
- "div",
3108
- {
3109
- className: "ShapeHighlight__style-panel",
3110
- ref: stylePanelRef,
3111
- onClick: (e) => e.stopPropagation()
3112
- },
3113
- /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__style-row" }, /* @__PURE__ */ React15.createElement("label", null, "Color"), /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__color-options" }, /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__color-presets" }, colorPresets.map((c) => /* @__PURE__ */ React15.createElement(
3114
- "button",
3115
- {
3116
- key: c,
3117
- type: "button",
3118
- className: `ShapeHighlight__color-preset ${strokeColor === c ? "active" : ""}`,
3119
- style: { backgroundColor: c },
3120
- onClick: () => onStyleChange({ strokeColor: c }),
3121
- title: c
3122
- }
3123
- ))), /* @__PURE__ */ React15.createElement(
3124
- "input",
3125
- {
3126
- type: "color",
3127
- value: strokeColor,
3128
- onChange: (e) => {
3129
- onStyleChange({ strokeColor: e.target.value });
3130
- }
3131
- }
3132
- ))),
3133
- /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__style-row" }, /* @__PURE__ */ React15.createElement("label", null, "Width"), /* @__PURE__ */ React15.createElement("div", { className: "ShapeHighlight__width-options" }, STROKE_WIDTHS2.map((w) => /* @__PURE__ */ React15.createElement(
3134
- "button",
3135
- {
3136
- key: w.value,
3137
- type: "button",
3138
- className: `ShapeHighlight__width-button ${strokeWidth === w.value ? "active" : ""}`,
3139
- onClick: () => onStyleChange({ strokeWidth: w.value }),
3140
- title: w.label
3141
- },
3142
- w.label
3143
- ))))
3144
- )
3531
+ configLayer
3145
3532
  ),
3146
3533
  /* @__PURE__ */ React15.createElement(
3147
3534
  Rnd5,
@@ -3204,7 +3591,10 @@ var DEFAULT_ERROR_MESSAGE = (error) => /* @__PURE__ */ React16.createElement("di
3204
3591
  var DEFAULT_ON_ERROR = (error) => {
3205
3592
  throw new Error(`Error loading PDF document: ${error.message}!`);
3206
3593
  };
3207
- var DEFAULT_WORKER_SRC = "https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.min.mjs";
3594
+ var DEFAULT_WORKER_SRC = new URL(
3595
+ "pdfjs-dist/build/pdf.worker.min.mjs",
3596
+ import.meta.url
3597
+ ).toString();
3208
3598
  var PdfLoader = ({
3209
3599
  document: document2,
3210
3600
  beforeLoad = DEFAULT_BEFORE_LOAD,
@@ -3245,405 +3635,708 @@ var PdfLoader = ({
3245
3635
  return error ? errorMessage(error) : loadingProgress ? beforeLoad(loadingProgress) : pdfDocumentRef.current && children(pdfDocumentRef.current);
3246
3636
  };
3247
3637
 
3248
- // src/lib/export-pdf.ts
3249
- import { PDFDocument, rgb, StandardFonts } from "pdf-lib";
3250
- function parseColor(color) {
3251
- const rgbaMatch = color.match(
3252
- /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/
3253
- );
3254
- if (rgbaMatch) {
3255
- return {
3256
- r: parseInt(rgbaMatch[1]) / 255,
3257
- g: parseInt(rgbaMatch[2]) / 255,
3258
- b: parseInt(rgbaMatch[3]) / 255,
3259
- a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1
3260
- };
3638
+ // src/lib/extract-sentences.ts
3639
+ var DEFAULT_OPTIONS = {
3640
+ pages: "all",
3641
+ includePositions: false,
3642
+ includeSources: false,
3643
+ normalize: true,
3644
+ locale: "en",
3645
+ idPrefix: "",
3646
+ includeTextUnitTypes: ["paragraph"],
3647
+ readingOrder: "auto",
3648
+ columnDetection: "auto"
3649
+ };
3650
+ var isTextItem = (item) => {
3651
+ return typeof item === "object" && item !== null && "str" in item && typeof item.str === "string" && "transform" in item && Array.isArray(item.transform);
3652
+ };
3653
+ var shouldInsertSpace = (previous, next) => {
3654
+ if (!previous || !next) return false;
3655
+ if (/\s$/.test(previous) || /^\s/.test(next)) return false;
3656
+ if (/^[,.;:!?)]/.test(next)) return false;
3657
+ if (/[([{]$/.test(previous)) return false;
3658
+ return true;
3659
+ };
3660
+ var getLastNonWhitespace = (text) => {
3661
+ const match = text.match(/\S(?=\s*$)/);
3662
+ return match?.[0] ?? "";
3663
+ };
3664
+ var getTrimmedChars = (text) => Array.from(text.trim());
3665
+ var getItemTextKind = (text) => {
3666
+ if (!text.trim()) return "space";
3667
+ if (/^[,.;:!?)\]}]+$/.test(text.trim())) return "punctuation";
3668
+ const chars = getTrimmedChars(text);
3669
+ if (chars.length === 1 && /[\p{L}\p{N}]/u.test(chars[0])) {
3670
+ return "singleGlyph";
3261
3671
  }
3262
- const hex = color.replace("#", "");
3263
- if (hex.length === 3) {
3264
- return {
3265
- r: parseInt(hex[0] + hex[0], 16) / 255,
3266
- g: parseInt(hex[1] + hex[1], 16) / 255,
3267
- b: parseInt(hex[2] + hex[2], 16) / 255,
3268
- a: 1
3269
- };
3672
+ if (/[\p{L}\p{N}]/u.test(text)) return "wordLike";
3673
+ return "other";
3674
+ };
3675
+ var getMedianGlyphWidth = (textItems) => {
3676
+ const glyphWidths = textItems.map((item) => {
3677
+ const textLength = Math.max(getTrimmedChars(item.text).length, 1);
3678
+ return item.rect.width / textLength;
3679
+ }).filter((width) => Number.isFinite(width) && width > 0).sort((a, b) => a - b);
3680
+ if (glyphWidths.length === 0) return 0;
3681
+ return glyphWidths[Math.floor(glyphWidths.length / 2)];
3682
+ };
3683
+ var getHorizontalGap = (previousItem, nextItem, medianGlyphWidth) => {
3684
+ const previousRight = previousItem.rect.left + previousItem.rect.width;
3685
+ const gap = nextItem.rect.left - previousRight;
3686
+ const zeroTolerance = Math.max(medianGlyphWidth * 0.08, 0.35);
3687
+ return Math.abs(gap) <= zeroTolerance ? 0 : gap;
3688
+ };
3689
+ var shouldInsertSpaceBetweenItems = (previousItem, nextItem, currentText, averageHeight, medianGlyphWidth) => {
3690
+ if (!previousItem || !currentText || !nextItem.text) return false;
3691
+ if (/\s$/.test(currentText) || /^\s/.test(nextItem.text)) return false;
3692
+ const previousChar = getLastNonWhitespace(currentText);
3693
+ const nextChar = Array.from(nextItem.text.trim())[0] ?? "";
3694
+ const previousKind = getItemTextKind(previousItem.text);
3695
+ const nextKind = getItemTextKind(nextItem.text);
3696
+ if (!nextChar) return false;
3697
+ if (nextKind === "punctuation" || /^[,.;:!?)]/.test(nextChar)) return false;
3698
+ if (/[([{]$/.test(previousChar)) return false;
3699
+ const gap = getHorizontalGap(previousItem, nextItem, medianGlyphWidth);
3700
+ if (gap < 0) return false;
3701
+ const glyphWidth = Math.max(
3702
+ medianGlyphWidth,
3703
+ averageHeight * 0.18,
3704
+ 1
3705
+ );
3706
+ const singleGlyphPair = previousKind === "singleGlyph" && nextKind === "singleGlyph";
3707
+ if (singleGlyphPair) {
3708
+ return gap > Math.max(glyphWidth * 0.35, 1);
3270
3709
  }
3271
- if (hex.length === 6) {
3272
- return {
3273
- r: parseInt(hex.slice(0, 2), 16) / 255,
3274
- g: parseInt(hex.slice(2, 4), 16) / 255,
3275
- b: parseInt(hex.slice(4, 6), 16) / 255,
3276
- a: 1
3277
- };
3710
+ if ((previousKind === "wordLike" || previousKind === "singleGlyph") && (nextKind === "wordLike" || nextKind === "singleGlyph")) {
3711
+ return gap > Math.max(glyphWidth * 0.08, 0.35);
3278
3712
  }
3279
- return { r: 1, g: 0.89, b: 0.56, a: 0.5 };
3280
- }
3281
- function scaledToPdfPoints(scaled, page) {
3282
- const pdfWidth = page.getWidth();
3283
- const pdfHeight = page.getHeight();
3284
- const xRatio = pdfWidth / scaled.width;
3285
- const yRatio = pdfHeight / scaled.height;
3286
- const x = scaled.x1 * xRatio;
3287
- const width = (scaled.x2 - scaled.x1) * xRatio;
3288
- const height = (scaled.y2 - scaled.y1) * yRatio;
3289
- const y = pdfHeight - scaled.y1 * yRatio - height;
3290
- return { x, y, width, height };
3291
- }
3292
- function dataUrlToBytes(dataUrl) {
3293
- const base64 = dataUrl.split(",")[1];
3294
- const byteString = atob(base64);
3295
- const bytes = new Uint8Array(byteString.length);
3296
- for (let i = 0; i < byteString.length; i++) {
3297
- bytes[i] = byteString.charCodeAt(i);
3713
+ return shouldInsertSpace(currentText, nextItem.text);
3714
+ };
3715
+ var appendTextItem = (currentText, item, previousItem, averageHeight, medianGlyphWidth) => {
3716
+ return `${currentText}${shouldInsertSpaceBetweenItems(
3717
+ previousItem,
3718
+ item,
3719
+ currentText,
3720
+ averageHeight,
3721
+ medianGlyphWidth
3722
+ ) ? " " : ""}${item.text}`;
3723
+ };
3724
+ var getAverageItemHeight = (textItems) => {
3725
+ const heights = textItems.map((item) => item.rect.height).filter((height) => height > 0).sort((a, b) => a - b);
3726
+ if (heights.length === 0) return 0;
3727
+ return heights[Math.floor(heights.length / 2)];
3728
+ };
3729
+ var areTextItemsOnSameLine = (previous, next, averageHeight) => {
3730
+ const previousMiddle = previous.rect.top + previous.rect.height / 2;
3731
+ const nextMiddle = next.rect.top + next.rect.height / 2;
3732
+ const tolerance = Math.max(averageHeight * 0.45, 2);
3733
+ return Math.abs(previousMiddle - nextMiddle) <= tolerance;
3734
+ };
3735
+ var getTextItemRect = (item, viewport, pageNumber) => {
3736
+ const [, , , , x, y] = item.transform;
3737
+ const width = Math.abs(item.width ?? 0);
3738
+ const height = Math.abs(item.height ?? 0);
3739
+ const [x1, y1, x2, y2] = viewport.convertToViewportRectangle([
3740
+ x,
3741
+ y,
3742
+ x + width,
3743
+ y + height
3744
+ ]);
3745
+ return {
3746
+ left: Math.min(x1, x2),
3747
+ top: Math.min(y1, y2),
3748
+ width: Math.abs(x2 - x1),
3749
+ height: Math.abs(y2 - y1),
3750
+ pageNumber
3751
+ };
3752
+ };
3753
+ var resolvePageNumbers = (pdfDocument, pages) => {
3754
+ if (pages === "all" || pages === void 0) {
3755
+ return Array.from({ length: pdfDocument.numPages }, (_, index) => index + 1);
3298
3756
  }
3299
- const type = dataUrl.includes("image/png") ? "png" : "jpg";
3300
- return { bytes, type };
3301
- }
3302
- function wrapText(text, font, fontSize, maxWidth) {
3303
- if (!text || maxWidth <= 0) return [];
3304
- const lines = [];
3305
- const paragraphs = text.split(/\n/);
3306
- for (const paragraph of paragraphs) {
3307
- if (!paragraph.trim()) {
3308
- lines.push("");
3309
- continue;
3757
+ return [...new Set(pages)].filter((pageNumber) => pageNumber >= 1 && pageNumber <= pdfDocument.numPages).sort((a, b) => a - b);
3758
+ };
3759
+ var buildPageTextStream = (textItems) => {
3760
+ let text = "";
3761
+ const charItemIndexes = [];
3762
+ const averageHeight = getAverageItemHeight(textItems);
3763
+ const medianGlyphWidth = getMedianGlyphWidth(textItems);
3764
+ let previousItem;
3765
+ textItems.forEach((item) => {
3766
+ if (!item.text) return;
3767
+ if (shouldInsertSpaceBetweenItems(
3768
+ previousItem,
3769
+ item,
3770
+ text,
3771
+ averageHeight,
3772
+ medianGlyphWidth
3773
+ )) {
3774
+ text += " ";
3775
+ charItemIndexes.push(null);
3310
3776
  }
3311
- const words = paragraph.split(/\s+/);
3312
- let currentLine = "";
3313
- for (const word of words) {
3314
- const testLine = currentLine ? `${currentLine} ${word}` : word;
3315
- const testWidth = font.widthOfTextAtSize(testLine, fontSize);
3316
- if (testWidth <= maxWidth) {
3317
- currentLine = testLine;
3318
- } else {
3319
- if (currentLine) {
3320
- lines.push(currentLine);
3321
- currentLine = "";
3322
- }
3323
- if (font.widthOfTextAtSize(word, fontSize) > maxWidth) {
3324
- let remaining = word;
3325
- while (remaining.length > 0) {
3326
- let charCount = 1;
3327
- while (charCount < remaining.length && font.widthOfTextAtSize(remaining.substring(0, charCount + 1), fontSize) <= maxWidth) {
3328
- charCount++;
3329
- }
3330
- const chunk = remaining.substring(0, charCount);
3331
- remaining = remaining.substring(charCount);
3332
- if (remaining.length > 0) {
3333
- lines.push(chunk);
3334
- } else {
3335
- currentLine = chunk;
3336
- }
3337
- }
3338
- } else {
3339
- currentLine = word;
3340
- }
3341
- }
3777
+ for (const char of item.text) {
3778
+ text += char;
3779
+ charItemIndexes.push(item.index);
3342
3780
  }
3343
- if (currentLine) lines.push(currentLine);
3344
- }
3345
- return lines;
3346
- }
3347
- function groupByPage(highlights) {
3348
- const map = /* @__PURE__ */ new Map();
3349
- for (const h of highlights) {
3350
- const pageNum = h.position.boundingRect.pageNumber;
3351
- if (!map.has(pageNum)) map.set(pageNum, []);
3352
- map.get(pageNum).push(h);
3781
+ previousItem = item;
3782
+ });
3783
+ return { text, charItemIndexes };
3784
+ };
3785
+ var normalizePdfLigatures = (text) => {
3786
+ return text.replace(/ffi/g, "ffi").replace(/ffl/g, "ffl").replace(/fi/g, "fi").replace(/fl/g, "fl").replace(/ff/g, "ff");
3787
+ };
3788
+ var cleanupPdfHyphenSpacing = (text) => {
3789
+ return text.replace(
3790
+ /\b([\p{L}\p{N}]{2,})\s+-\s+([\p{L}\p{N}]{2,})\b/gu,
3791
+ "$1-$2"
3792
+ );
3793
+ };
3794
+ var normalizePdfSentenceText = (text) => {
3795
+ return cleanupPdfHyphenSpacing(normalizePdfLigatures(text)).replace(/(\p{L}+)-\s+(\p{L}+)/gu, "$1-$2").replace(/\[\s*([^\]]*?)\s*\]/g, (_, citation) => {
3796
+ const normalizedCitation = citation.replace(/\s*,\s*/g, ", ").replace(/\s+/g, " ").trim();
3797
+ return `[${normalizedCitation}]`;
3798
+ }).replace(/\s+/g, " ").trim();
3799
+ };
3800
+ var splitPdfSentences = (text, locale = DEFAULT_OPTIONS.locale) => {
3801
+ const segmenterConstructor = Intl.Segmenter;
3802
+ if (segmenterConstructor) {
3803
+ const segmenter = new segmenterConstructor(locale, {
3804
+ granularity: "sentence"
3805
+ });
3806
+ return Array.from(segmenter.segment(text)).map((segment) => ({
3807
+ text: segment.segment.trim(),
3808
+ start: segment.index + segment.segment.search(/\S/),
3809
+ end: segment.index + segment.segment.trimEnd().length
3810
+ })).filter((sentence) => sentence.text.length > 0 && sentence.start >= 0);
3353
3811
  }
3354
- return map;
3355
- }
3356
- async function renderTextHighlight(page, highlight, options) {
3357
- const colorStr = highlight.highlightColor || options.textHighlightColor || "rgba(255, 226, 143, 0.5)";
3358
- const color = parseColor(colorStr);
3359
- const highlightStyle = highlight.highlightStyle || "highlight";
3360
- const rects = highlight.position.rects.length > 0 ? highlight.position.rects : [highlight.position.boundingRect];
3361
- for (const rect of rects) {
3362
- const { x, y, width, height } = scaledToPdfPoints(rect, page);
3363
- if (highlightStyle === "highlight") {
3364
- page.drawRectangle({
3365
- x,
3366
- y,
3367
- width,
3368
- height,
3369
- color: rgb(color.r, color.g, color.b),
3370
- opacity: color.a
3371
- });
3372
- } else if (highlightStyle === "underline") {
3373
- const lineThickness = Math.max(1, height * 0.1);
3374
- page.drawRectangle({
3375
- x,
3376
- y,
3377
- width,
3378
- height: lineThickness,
3379
- color: rgb(color.r, color.g, color.b),
3380
- opacity: color.a
3381
- });
3382
- } else if (highlightStyle === "strikethrough") {
3383
- const lineThickness = Math.max(1, height * 0.1);
3384
- const lineY = y + height / 2 - lineThickness / 2;
3385
- page.drawRectangle({
3386
- x,
3387
- y: lineY,
3388
- width,
3389
- height: lineThickness,
3390
- color: rgb(color.r, color.g, color.b),
3391
- opacity: color.a
3812
+ const ranges = [];
3813
+ let start = 0;
3814
+ for (let index = 0; index < text.length; index += 1) {
3815
+ const char = text[index];
3816
+ if (!/[.!?]/.test(char)) continue;
3817
+ const nextText = text.slice(index + 1);
3818
+ const nextNonSpace = nextText.match(/\S/);
3819
+ const nextChar = nextNonSpace?.[0] ?? "";
3820
+ const shouldSplit = nextChar === "" || /[A-Z([{"']/.test(nextChar);
3821
+ if (!shouldSplit) continue;
3822
+ const rawSentence = text.slice(start, index + 1);
3823
+ const leadingWhitespace2 = rawSentence.search(/\S/);
3824
+ const sentenceStart = start + Math.max(leadingWhitespace2, 0);
3825
+ const sentenceText = rawSentence.trim();
3826
+ if (sentenceText) {
3827
+ ranges.push({
3828
+ text: sentenceText,
3829
+ start: sentenceStart,
3830
+ end: start + rawSentence.trimEnd().length
3392
3831
  });
3393
3832
  }
3833
+ start = index + 1;
3394
3834
  }
3395
- }
3396
- async function renderAreaHighlight(page, highlight, options) {
3397
- const colorStr = highlight.highlightColor || options.areaHighlightColor || "rgba(255, 226, 143, 0.5)";
3398
- const color = parseColor(colorStr);
3399
- const { x, y, width, height } = scaledToPdfPoints(
3400
- highlight.position.boundingRect,
3401
- page
3402
- );
3403
- page.drawRectangle({
3404
- x,
3405
- y,
3406
- width,
3407
- height,
3408
- color: rgb(color.r, color.g, color.b),
3409
- opacity: color.a
3835
+ const finalRawSentence = text.slice(start);
3836
+ const leadingWhitespace = finalRawSentence.search(/\S/);
3837
+ const finalText = finalRawSentence.trim();
3838
+ if (finalText) {
3839
+ ranges.push({
3840
+ text: finalText,
3841
+ start: start + Math.max(leadingWhitespace, 0),
3842
+ end: start + finalRawSentence.trimEnd().length
3843
+ });
3844
+ }
3845
+ return ranges;
3846
+ };
3847
+ var getSentenceTextItemIndexes = (charItemIndexes, start, end) => {
3848
+ const indexes = /* @__PURE__ */ new Set();
3849
+ for (let index = start; index < end; index += 1) {
3850
+ const itemIndex = charItemIndexes[index];
3851
+ if (itemIndex !== null && itemIndex !== void 0) {
3852
+ indexes.add(itemIndex);
3853
+ }
3854
+ }
3855
+ return Array.from(indexes).sort((a, b) => a - b);
3856
+ };
3857
+ var getScaledPosition = (textItems, textItemIndexes, page) => {
3858
+ const indexSet = new Set(textItemIndexes);
3859
+ const rects = textItems.filter((item) => indexSet.has(item.index)).map((item) => item.rect).filter((rect) => rect.width > 0 && rect.height > 0);
3860
+ if (rects.length === 0) return void 0;
3861
+ const boundingRect = get_bounding_rect_default(rects);
3862
+ return {
3863
+ boundingRect: viewportToScaled(boundingRect, page),
3864
+ rects: rects.map((rect) => viewportToScaled(rect, page))
3865
+ };
3866
+ };
3867
+ var getTextItemsPosition = (textItems, textItemIndexes, page, includePositions) => {
3868
+ return includePositions ? getScaledPosition(textItems, textItemIndexes, page) : void 0;
3869
+ };
3870
+ var getTextItemsBoundingRect = (textItems) => {
3871
+ const rects = textItems.map((item) => item.rect).filter((rect) => rect.width > 0 && rect.height > 0);
3872
+ return rects.length > 0 ? get_bounding_rect_default(rects) : void 0;
3873
+ };
3874
+ var getOrderedTextItems = (textItems, readingOrder) => {
3875
+ if (readingOrder === "document") return [...textItems];
3876
+ const averageHeight = getAverageItemHeight(textItems);
3877
+ return [...textItems].sort((a, b) => {
3878
+ const topDelta = a.rect.top - b.rect.top;
3879
+ if (Math.abs(topDelta) > Math.max(averageHeight * 0.45, 2)) {
3880
+ return topDelta;
3881
+ }
3882
+ return a.rect.left - b.rect.left;
3410
3883
  });
3411
- }
3412
- async function renderFreetextHighlight(page, highlight, options, font) {
3413
- const text = highlight.content?.text || "";
3414
- const textColor = parseColor(
3415
- highlight.color || options.defaultFreetextColor || "#333333"
3416
- );
3417
- const { x, y, width, height } = scaledToPdfPoints(
3418
- highlight.position.boundingRect,
3419
- page
3420
- );
3421
- const pdfHeight = page.getHeight();
3422
- const yRatio = pdfHeight / highlight.position.boundingRect.height;
3423
- const storedFontSize = parseInt(highlight.fontSize || "") || options.defaultFreetextFontSize || 14;
3424
- const fontSize = storedFontSize * yRatio;
3425
- console.log("Freetext export:", {
3426
- storedFontSize,
3427
- yRatio,
3428
- fontSize,
3429
- boxDimensions: { x, y, width, height },
3430
- text: text.substring(0, 50)
3884
+ };
3885
+ var groupTextItemsIntoLines = (textItems) => {
3886
+ const averageHeight = getAverageItemHeight(textItems);
3887
+ const sortedItems = getOrderedTextItems(textItems, "position");
3888
+ const lines = [];
3889
+ sortedItems.forEach((item) => {
3890
+ const currentLine = lines[lines.length - 1];
3891
+ const previousItem = currentLine?.[currentLine.length - 1];
3892
+ if (currentLine && previousItem && areTextItemsOnSameLine(previousItem, item, averageHeight)) {
3893
+ currentLine.push(item);
3894
+ currentLine.sort((a, b) => a.rect.left - b.rect.left);
3895
+ return;
3896
+ }
3897
+ lines.push([item]);
3431
3898
  });
3432
- const bgColorValue = highlight.backgroundColor || options.defaultFreetextBgColor || "#ffffc8";
3433
- if (bgColorValue !== "transparent") {
3434
- const bgColor = parseColor(bgColorValue);
3435
- page.drawRectangle({
3436
- x,
3437
- y,
3438
- width,
3439
- height,
3440
- color: rgb(bgColor.r, bgColor.g, bgColor.b),
3441
- opacity: bgColor.a
3442
- });
3899
+ return lines;
3900
+ };
3901
+ var isLikelyBodyTextItem = (item, page) => {
3902
+ const text = item.text.trim();
3903
+ if (!text) return false;
3904
+ if (item.rect.top < page.height * 0.12 && page.pageNumber === 1 || item.rect.top > page.height * 0.88) {
3905
+ return false;
3443
3906
  }
3444
- const padding = 4 * yRatio;
3445
- const maxWidth = width - padding * 2;
3446
- const lineHeight = fontSize * 1.3;
3447
- if (maxWidth > 0 && text) {
3448
- const lines = wrapText(text, font, fontSize, maxWidth);
3449
- let currentY = y + height - fontSize - padding;
3450
- for (const line of lines) {
3451
- if (currentY < y + padding) break;
3452
- if (line.trim()) {
3453
- page.drawText(line, {
3454
- x: x + padding,
3455
- y: currentY,
3456
- size: fontSize,
3457
- font,
3458
- color: rgb(textColor.r, textColor.g, textColor.b)
3459
- });
3460
- }
3461
- currentY -= lineHeight;
3907
+ if (isLikelyFootnoteOrReference(text)) return false;
3908
+ return true;
3909
+ };
3910
+ var getColumnRanges = (textItems, page) => {
3911
+ if (textItems.length < 24) return void 0;
3912
+ const minLeft = Math.min(...textItems.map((item) => item.rect.left));
3913
+ const maxRight = Math.max(
3914
+ ...textItems.map((item) => item.rect.left + item.rect.width)
3915
+ );
3916
+ const pageSpan = maxRight - minLeft;
3917
+ const binCount = 80;
3918
+ const bins = Array.from({ length: binCount }, () => 0);
3919
+ textItems.forEach((item) => {
3920
+ const left = item.rect.left;
3921
+ const right = item.rect.left + item.rect.width;
3922
+ const startBin = Math.max(
3923
+ 0,
3924
+ Math.floor((left - minLeft) / pageSpan * binCount)
3925
+ );
3926
+ const endBin = Math.min(
3927
+ binCount - 1,
3928
+ Math.ceil((right - minLeft) / pageSpan * binCount)
3929
+ );
3930
+ for (let index = startBin; index <= endBin; index += 1) {
3931
+ bins[index] += 1;
3932
+ }
3933
+ });
3934
+ const centerStart = Math.floor(binCount * 0.35);
3935
+ const centerEnd = Math.ceil(binCount * 0.65);
3936
+ let bestGapStart = -1;
3937
+ let bestGapEnd = -1;
3938
+ let currentGapStart = -1;
3939
+ for (let index = centerStart; index <= centerEnd; index += 1) {
3940
+ if (bins[index] === 0) {
3941
+ if (currentGapStart === -1) currentGapStart = index;
3942
+ continue;
3943
+ }
3944
+ if (currentGapStart !== -1 && index - currentGapStart > bestGapEnd - bestGapStart) {
3945
+ bestGapStart = currentGapStart;
3946
+ bestGapEnd = index;
3462
3947
  }
3948
+ currentGapStart = -1;
3463
3949
  }
3464
- }
3465
- function transformToRawCoordinates(page, x, y, width, height) {
3466
- const rotation = page.getRotation().angle;
3467
- const pageWidth = page.getWidth();
3468
- const pageHeight = page.getHeight();
3469
- if (rotation === 90) {
3950
+ if (currentGapStart !== -1 && centerEnd + 1 - currentGapStart > bestGapEnd - bestGapStart) {
3951
+ bestGapStart = currentGapStart;
3952
+ bestGapEnd = centerEnd + 1;
3953
+ }
3954
+ const gapBins = bestGapEnd - bestGapStart;
3955
+ const gapWidth = gapBins / binCount * pageSpan;
3956
+ const minGapWidth = Math.max(page.width * 0.035, 14);
3957
+ if (bestGapStart < 0 || gapWidth < minGapWidth) return void 0;
3958
+ const gapLeft = minLeft + bestGapStart / binCount * pageSpan;
3959
+ const gapRight = minLeft + bestGapEnd / binCount * pageSpan;
3960
+ const splitX = (gapLeft + gapRight) / 2;
3961
+ const leftItems = textItems.filter(
3962
+ (item) => item.rect.left + item.rect.width / 2 < splitX
3963
+ );
3964
+ const rightItems = textItems.filter(
3965
+ (item) => item.rect.left + item.rect.width / 2 >= splitX
3966
+ );
3967
+ if (leftItems.length < 10 || rightItems.length < 10) return void 0;
3968
+ const leftRange = {
3969
+ left: Math.min(...leftItems.map((item) => item.rect.left)),
3970
+ right: Math.max(...leftItems.map((item) => item.rect.left + item.rect.width))
3971
+ };
3972
+ const rightRange = {
3973
+ left: Math.min(...rightItems.map((item) => item.rect.left)),
3974
+ right: Math.max(...rightItems.map((item) => item.rect.left + item.rect.width))
3975
+ };
3976
+ const leftHeight = getAverageItemHeight(leftItems);
3977
+ const rightHeight = getAverageItemHeight(rightItems);
3978
+ if (leftHeight > 0 && rightHeight > 0 && Math.max(leftHeight, rightHeight) / Math.min(leftHeight, rightHeight) > 1.45) {
3979
+ return void 0;
3980
+ }
3981
+ return [leftRange, rightRange];
3982
+ };
3983
+ var assignColumnsToPage = (page, columnDetection) => {
3984
+ if (columnDetection === "none") {
3470
3985
  return {
3471
- x: y,
3472
- y: pageWidth - x - width,
3473
- width: height,
3474
- height: width
3986
+ ...page,
3987
+ textItems: page.textItems.map((item) => ({ ...item, columnIndex: 0 })),
3988
+ columns: [
3989
+ {
3990
+ index: 0,
3991
+ left: 0,
3992
+ right: page.width,
3993
+ width: page.width,
3994
+ textItemIndexes: page.textItems.map((item) => item.index)
3995
+ }
3996
+ ]
3475
3997
  };
3476
- } else if (rotation === 180) {
3998
+ }
3999
+ const bodyItems = page.textItems.filter(
4000
+ (item) => isLikelyBodyTextItem(item, page)
4001
+ );
4002
+ const ranges = getColumnRanges(bodyItems, page);
4003
+ if (!ranges) {
3477
4004
  return {
3478
- x: pageWidth - x - width,
3479
- y: pageHeight - y - height,
3480
- width,
3481
- height
4005
+ ...page,
4006
+ textItems: page.textItems.map((item) => ({ ...item, columnIndex: 0 })),
4007
+ columns: [
4008
+ {
4009
+ index: 0,
4010
+ left: 0,
4011
+ right: page.width,
4012
+ width: page.width,
4013
+ textItemIndexes: page.textItems.map((item) => item.index)
4014
+ }
4015
+ ]
3482
4016
  };
3483
- } else if (rotation === 270) {
4017
+ }
4018
+ const splitX = (ranges[0].right + ranges[1].left) / 2;
4019
+ const textItems = page.textItems.map((item) => {
4020
+ const centerX = item.rect.left + item.rect.width / 2;
3484
4021
  return {
3485
- x: pageHeight - y - height,
3486
- y: x,
3487
- width: height,
3488
- height: width
4022
+ ...item,
4023
+ columnIndex: centerX < splitX ? 0 : 1
3489
4024
  };
3490
- }
3491
- return { x, y, width, height };
3492
- }
3493
- async function renderImageHighlight(pdfDoc, page, highlight) {
3494
- const imageDataUrl = highlight.content?.image;
3495
- if (!imageDataUrl) return;
3496
- try {
3497
- const { bytes, type } = dataUrlToBytes(imageDataUrl);
3498
- const image = type === "png" ? await pdfDoc.embedPng(bytes) : await pdfDoc.embedJpg(bytes);
3499
- const visualCoords = scaledToPdfPoints(
3500
- highlight.position.boundingRect,
3501
- page
3502
- );
3503
- const rawCoords = transformToRawCoordinates(
3504
- page,
3505
- visualCoords.x,
3506
- visualCoords.y,
3507
- visualCoords.width,
3508
- visualCoords.height
4025
+ });
4026
+ const columns = ranges.map((range, index) => ({
4027
+ index,
4028
+ left: range.left,
4029
+ right: range.right,
4030
+ width: range.right - range.left,
4031
+ textItemIndexes: textItems.filter((item) => item.columnIndex === index).map((item) => item.index)
4032
+ }));
4033
+ return { ...page, textItems, columns };
4034
+ };
4035
+ var isFullWidthBlock = (block, page, detectedColumnCount) => {
4036
+ if (detectedColumnCount < 2) return false;
4037
+ const textItems = block.flat();
4038
+ const boundingRect = getTextItemsBoundingRect(textItems);
4039
+ if (!boundingRect) return false;
4040
+ return boundingRect.width > page.width * 0.72;
4041
+ };
4042
+ var buildBlocksForItems = (textItems, page) => {
4043
+ const lines = groupTextItemsIntoLines(textItems);
4044
+ const averageHeight = getAverageItemHeight(textItems);
4045
+ const blocks = [];
4046
+ lines.forEach((line) => {
4047
+ const currentBlock = blocks[blocks.length - 1];
4048
+ const previousLine = currentBlock?.[currentBlock.length - 1];
4049
+ if (currentBlock && previousLine && !shouldStartNewBlock(previousLine, line, page, averageHeight)) {
4050
+ currentBlock.push(line);
4051
+ return;
4052
+ }
4053
+ blocks.push([line]);
4054
+ });
4055
+ return blocks;
4056
+ };
4057
+ var compareBlocksByPosition = (left, right) => {
4058
+ const leftRect = getTextItemsBoundingRect(left.block.flat());
4059
+ const rightRect = getTextItemsBoundingRect(right.block.flat());
4060
+ return (leftRect?.top ?? 0) - (rightRect?.top ?? 0);
4061
+ };
4062
+ var compareBlocksByColumnReadingOrder = (left, right) => {
4063
+ const columnDelta = (left.columnIndex ?? 0) - (right.columnIndex ?? 0);
4064
+ if (columnDelta !== 0) return columnDelta;
4065
+ return compareBlocksByPosition(left, right);
4066
+ };
4067
+ var buildTextUnit = ({
4068
+ block,
4069
+ columnIndex,
4070
+ indexInPage,
4071
+ includePositions,
4072
+ idPrefix,
4073
+ page,
4074
+ shouldNormalize
4075
+ }) => {
4076
+ const textItems = block.flat();
4077
+ const rawText = block.map(getLineText).join(" ");
4078
+ const textItemIndexes = textItems.map((item) => item.index);
4079
+ const type = classifyTextUnit(rawText, textItemIndexes, page);
4080
+ const position = getTextItemsPosition(
4081
+ page.textItems,
4082
+ textItemIndexes,
4083
+ page,
4084
+ includePositions
4085
+ );
4086
+ return {
4087
+ id: `${idPrefix}p${page.pageNumber}-u${indexInPage}`,
4088
+ type,
4089
+ text: shouldNormalize ? normalizePdfSentenceText(rawText) : rawText.trim(),
4090
+ rawText: rawText.trim(),
4091
+ pageNumber: page.pageNumber,
4092
+ indexInPage,
4093
+ ...columnIndex !== void 0 ? { columnIndex } : {},
4094
+ ...position ? { position } : {},
4095
+ source: {
4096
+ textItemIndexes
4097
+ }
4098
+ };
4099
+ };
4100
+ var getLineText = (line) => {
4101
+ const averageHeight = getAverageItemHeight(line);
4102
+ const medianGlyphWidth = getMedianGlyphWidth(line);
4103
+ return line.reduce((text, item, index) => {
4104
+ return appendTextItem(
4105
+ text,
4106
+ item,
4107
+ line[index - 1],
4108
+ averageHeight,
4109
+ medianGlyphWidth
3509
4110
  );
3510
- console.log("Image export:", {
3511
- rotation: page.getRotation().angle,
3512
- visualCoords,
3513
- rawCoords
3514
- });
3515
- page.drawImage(image, {
3516
- x: rawCoords.x,
3517
- y: rawCoords.y,
3518
- width: rawCoords.width,
3519
- height: rawCoords.height
3520
- });
3521
- } catch (error) {
3522
- console.error("Failed to embed image:", error);
4111
+ }, "");
4112
+ };
4113
+ var getLineRect = (line) => get_bounding_rect_default(line.map((item) => item.rect));
4114
+ var shouldStartNewBlock = (previousLine, nextLine, page, averageHeight) => {
4115
+ const previousRect = getLineRect(previousLine);
4116
+ const nextRect = getLineRect(nextLine);
4117
+ const verticalGap = nextRect.top - (previousRect.top + previousRect.height);
4118
+ const leftDelta = Math.abs(nextRect.left - previousRect.left);
4119
+ const previousText = getLineText(previousLine).trim();
4120
+ const nextText = getLineText(nextLine).trim();
4121
+ if (verticalGap > Math.max(averageHeight * 1.15, 7)) return true;
4122
+ if (leftDelta > page.width * 0.18) return true;
4123
+ if (isLikelyStandaloneBlock(previousText) || isLikelyStandaloneBlock(nextText)) {
4124
+ return true;
3523
4125
  }
3524
- }
3525
- async function renderShapeHighlight(page, highlight) {
3526
- const shapeType = highlight.content?.shape?.shapeType || highlight.shapeType || "rectangle";
3527
- const strokeColorStr = highlight.content?.shape?.strokeColor || highlight.strokeColor || "#000000";
3528
- const strokeWidth = highlight.content?.shape?.strokeWidth || highlight.strokeWidth || 2;
3529
- const color = parseColor(strokeColorStr);
3530
- const { x, y, width, height } = scaledToPdfPoints(
3531
- highlight.position.boundingRect,
3532
- page
4126
+ return false;
4127
+ };
4128
+ var isLikelyStandaloneBlock = (text) => {
4129
+ const normalizedText = normalizePdfSentenceText(text);
4130
+ if (!normalizedText) return false;
4131
+ if (isLikelySectionHeading(normalizedText)) return true;
4132
+ if (isLikelyFootnoteOrReference(normalizedText)) return true;
4133
+ return false;
4134
+ };
4135
+ var isLikelySectionHeading = (text) => {
4136
+ if (text.length > 80) return false;
4137
+ if (/^\d+(\.\d+)*\.?\s+[A-Z]/.test(text)) return true;
4138
+ const letters = text.replace(/[^A-Za-z]/g, "");
4139
+ if (letters.length < 3) return false;
4140
+ const uppercaseLetters = text.replace(/[^A-Z]/g, "");
4141
+ return uppercaseLetters.length / letters.length > 0.75;
4142
+ };
4143
+ var isLikelyFootnoteOrReference = (text) => {
4144
+ return /^\d+\s*https?:\/\//i.test(text) || /^\[\d+\]/.test(text) || /https?:\/\/|doi\.org|arxiv\.org/i.test(text);
4145
+ };
4146
+ var isLikelyAuthor = (text) => {
4147
+ return text.length <= 80 && !/[.!?]$/.test(text) && /^[A-Z][A-Za-z'.-]+(?:\s+[A-Z][A-Za-z'.-]+){1,4}$/.test(text);
4148
+ };
4149
+ var isLikelyAffiliation = (text) => {
4150
+ return text.length <= 160 && /university|institute|department|school|college|faculty|laboratory|germany|usa|china|france|italy|spain|canada|japan|korea/i.test(
4151
+ text
3533
4152
  );
3534
- switch (shapeType) {
3535
- case "rectangle":
3536
- page.drawRectangle({
3537
- x,
3538
- y,
3539
- width,
3540
- height,
3541
- borderColor: rgb(color.r, color.g, color.b),
3542
- borderWidth: strokeWidth,
3543
- opacity: color.a
3544
- });
3545
- break;
3546
- case "circle":
3547
- page.drawEllipse({
3548
- x: x + width / 2,
3549
- y: y + height / 2,
3550
- xScale: width / 2,
3551
- yScale: height / 2,
3552
- borderColor: rgb(color.r, color.g, color.b),
3553
- borderWidth: strokeWidth,
3554
- opacity: color.a
3555
- });
3556
- break;
3557
- case "arrow": {
3558
- const startPt = highlight.content?.shape?.startPoint;
3559
- const endPt = highlight.content?.shape?.endPoint;
3560
- const startX = startPt ? x + startPt.x * width : x;
3561
- const startY = startPt ? y + (1 - startPt.y) * height : y + height / 2;
3562
- const endX = endPt ? x + endPt.x * width : x + width;
3563
- const endY = endPt ? y + (1 - endPt.y) * height : y + height / 2;
3564
- page.drawLine({
3565
- start: { x: startX, y: startY },
3566
- end: { x: endX, y: endY },
3567
- color: rgb(color.r, color.g, color.b),
3568
- thickness: strokeWidth,
3569
- opacity: color.a
3570
- });
3571
- const angle = Math.atan2(endY - startY, endX - startX);
3572
- const arrowSize = Math.min(15, width * 0.2, height * 0.4);
3573
- const arrowAngle = Math.PI / 6;
3574
- page.drawLine({
3575
- start: {
3576
- x: endX - arrowSize * Math.cos(angle - arrowAngle),
3577
- y: endY - arrowSize * Math.sin(angle - arrowAngle)
3578
- },
3579
- end: { x: endX, y: endY },
3580
- color: rgb(color.r, color.g, color.b),
3581
- thickness: strokeWidth,
3582
- opacity: color.a
4153
+ };
4154
+ var classifyTextUnit = (rawText, textItemIndexes, page) => {
4155
+ const text = normalizePdfSentenceText(rawText);
4156
+ const textItems = page.textItems.filter(
4157
+ (item) => textItemIndexes.includes(item.index)
4158
+ );
4159
+ const rects = textItems.map((item) => item.rect);
4160
+ const boundingRect = rects.length > 0 ? get_bounding_rect_default(rects) : void 0;
4161
+ const averagePageHeight = getAverageItemHeight(page.textItems);
4162
+ const averageUnitHeight = getAverageItemHeight(textItems);
4163
+ const isTopOfFirstPage = page.pageNumber === 1 && (boundingRect?.top ?? page.height) < page.height * 0.2;
4164
+ const isBottomOfPage = (boundingRect?.top ?? 0) > page.height * 0.78;
4165
+ const isLargeText = averagePageHeight > 0 && averageUnitHeight > averagePageHeight * 1.25;
4166
+ const isCentered = boundingRect !== void 0 && Math.abs(boundingRect.left + boundingRect.width / 2 - page.width / 2) < page.width * 0.16;
4167
+ if (isLikelyFootnoteOrReference(text) || isBottomOfPage && /^\d+\s/.test(text)) {
4168
+ return text.includes("://") || /^\d+\s*https?:\/\//i.test(text) ? "footnote" : "reference";
4169
+ }
4170
+ if (isLikelySectionHeading(text)) return "heading";
4171
+ if (isTopOfFirstPage && isLargeText && isCentered) return "title";
4172
+ if (isTopOfFirstPage && isLikelyAuthor(text)) return "author";
4173
+ if (isTopOfFirstPage && isLikelyAffiliation(text)) return "affiliation";
4174
+ return "paragraph";
4175
+ };
4176
+ var extractTextUnitsFromPages = (pages, options = {}) => {
4177
+ const includePositions = options.includePositions ?? DEFAULT_OPTIONS.includePositions;
4178
+ const includeSources = options.includeSources ?? DEFAULT_OPTIONS.includeSources;
4179
+ const shouldNormalize = options.normalize ?? DEFAULT_OPTIONS.normalize;
4180
+ const idPrefix = options.idPrefix ?? DEFAULT_OPTIONS.idPrefix;
4181
+ const readingOrder = options.readingOrder ?? DEFAULT_OPTIONS.readingOrder;
4182
+ const columnDetection = options.columnDetection ?? DEFAULT_OPTIONS.columnDetection;
4183
+ const units = [];
4184
+ pages.forEach((page) => {
4185
+ const pageWithColumns = assignColumnsToPage(page, columnDetection);
4186
+ const detectedColumnCount = pageWithColumns.columns?.length ?? 1;
4187
+ const columnIndexes = detectedColumnCount > 1 && readingOrder !== "document" ? pageWithColumns.columns?.map((column) => column.index) ?? [0] : [0];
4188
+ const blocks = [];
4189
+ if (readingOrder === "document") {
4190
+ blocks.push(
4191
+ ...buildBlocksForItems(pageWithColumns.textItems, pageWithColumns).map(
4192
+ (block) => ({ block })
4193
+ )
4194
+ );
4195
+ } else {
4196
+ const allBlocks = columnIndexes.flatMap(
4197
+ (columnIndex) => buildBlocksForItems(
4198
+ pageWithColumns.textItems.filter(
4199
+ (item) => item.columnIndex === columnIndex
4200
+ ),
4201
+ pageWithColumns
4202
+ ).map((block) => ({ block, columnIndex }))
4203
+ );
4204
+ const fullWidthBlocks = allBlocks.filter(
4205
+ ({ block }) => isFullWidthBlock(block, pageWithColumns, detectedColumnCount)
4206
+ );
4207
+ const columnBlocks = allBlocks.filter(
4208
+ ({ block }) => !isFullWidthBlock(block, pageWithColumns, detectedColumnCount)
4209
+ );
4210
+ const topFullWidthBlocks = fullWidthBlocks.filter(({ block }) => {
4211
+ const rect = getTextItemsBoundingRect(block.flat());
4212
+ return (rect?.top ?? 0) < pageWithColumns.height * 0.3;
3583
4213
  });
3584
- page.drawLine({
3585
- start: {
3586
- x: endX - arrowSize * Math.cos(angle + arrowAngle),
3587
- y: endY - arrowSize * Math.sin(angle + arrowAngle)
3588
- },
3589
- end: { x: endX, y: endY },
3590
- color: rgb(color.r, color.g, color.b),
3591
- thickness: strokeWidth,
3592
- opacity: color.a
4214
+ const bottomFullWidthBlocks = fullWidthBlocks.filter(({ block }) => {
4215
+ const rect = getTextItemsBoundingRect(block.flat());
4216
+ return (rect?.top ?? 0) >= pageWithColumns.height * 0.3;
3593
4217
  });
3594
- break;
4218
+ blocks.push(
4219
+ ...topFullWidthBlocks.sort(compareBlocksByPosition),
4220
+ ...columnBlocks.sort(compareBlocksByColumnReadingOrder),
4221
+ ...bottomFullWidthBlocks.sort(compareBlocksByPosition)
4222
+ );
3595
4223
  }
4224
+ blocks.forEach(({ block, columnIndex }, indexInPage) => {
4225
+ units.push(
4226
+ buildTextUnit({
4227
+ block,
4228
+ columnIndex,
4229
+ indexInPage,
4230
+ includePositions,
4231
+ idPrefix,
4232
+ page: pageWithColumns,
4233
+ shouldNormalize
4234
+ })
4235
+ );
4236
+ });
4237
+ });
4238
+ if (!includeSources) {
4239
+ return units.map(({ source: _source, ...rest }) => rest);
3596
4240
  }
3597
- }
3598
- async function exportPdf(pdfSource, highlights, options = {}) {
3599
- let pdfBytes;
3600
- if (typeof pdfSource === "string") {
3601
- const response = await fetch(pdfSource);
3602
- pdfBytes = await response.arrayBuffer();
3603
- } else {
3604
- pdfBytes = pdfSource instanceof Uint8Array ? pdfSource.buffer.slice(
3605
- pdfSource.byteOffset,
3606
- pdfSource.byteOffset + pdfSource.byteLength
3607
- ) : pdfSource;
4241
+ return units;
4242
+ };
4243
+ var extractTextUnits = async (pdfDocument, options = {}) => {
4244
+ const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
4245
+ const pages = await extractPageTextItems(pdfDocument, {
4246
+ pages: resolvedOptions.pages,
4247
+ columnDetection: resolvedOptions.columnDetection
4248
+ });
4249
+ return extractTextUnitsFromPages(pages, resolvedOptions);
4250
+ };
4251
+ var extractPageTextItems = async (pdfDocument, options = {}) => {
4252
+ const pageNumbers = resolvePageNumbers(pdfDocument, options.pages);
4253
+ const columnDetection = options.columnDetection ?? DEFAULT_OPTIONS.columnDetection;
4254
+ const extractedPages = [];
4255
+ for (const pageNumber of pageNumbers) {
4256
+ const page = await pdfDocument.getPage(pageNumber);
4257
+ const viewport = page.getViewport({ scale: 1 });
4258
+ const textContent = await page.getTextContent();
4259
+ const textItems = [];
4260
+ textContent.items.forEach((item, index) => {
4261
+ if (!isTextItem(item) || item.str.length === 0) return;
4262
+ textItems.push({
4263
+ text: item.str,
4264
+ index,
4265
+ pageNumber,
4266
+ rect: getTextItemRect(item, viewport, pageNumber)
4267
+ });
4268
+ });
4269
+ extractedPages.push(assignColumnsToPage({
4270
+ pageNumber,
4271
+ width: viewport.width,
4272
+ height: viewport.height,
4273
+ textItems
4274
+ }, columnDetection));
3608
4275
  }
3609
- const pdfDoc = await PDFDocument.load(pdfBytes);
3610
- const pages = pdfDoc.getPages();
3611
- const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
3612
- const byPage = groupByPage(highlights);
3613
- const totalPages = byPage.size;
3614
- let currentPage = 0;
3615
- for (const [pageNum, pageHighlights] of byPage) {
3616
- const page = pages[pageNum - 1];
3617
- if (!page) continue;
3618
- for (const highlight of pageHighlights) {
3619
- switch (highlight.type) {
3620
- case "text":
3621
- await renderTextHighlight(page, highlight, options);
3622
- break;
3623
- case "area":
3624
- await renderAreaHighlight(page, highlight, options);
3625
- break;
3626
- case "freetext":
3627
- await renderFreetextHighlight(page, highlight, options, font);
3628
- break;
3629
- case "image":
3630
- await renderImageHighlight(pdfDoc, page, highlight);
3631
- break;
3632
- case "drawing":
3633
- await renderImageHighlight(pdfDoc, page, highlight);
3634
- break;
3635
- case "shape":
3636
- await renderShapeHighlight(page, highlight);
3637
- break;
3638
- default:
3639
- await renderAreaHighlight(page, highlight, options);
3640
- }
3641
- }
3642
- currentPage++;
3643
- options.onProgress?.(currentPage, totalPages);
4276
+ return extractedPages;
4277
+ };
4278
+ var extractSentences = async (pdfDocument, options = {}) => {
4279
+ const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
4280
+ const pages = await extractPageTextItems(pdfDocument, {
4281
+ pages: resolvedOptions.pages,
4282
+ columnDetection: resolvedOptions.columnDetection
4283
+ });
4284
+ const textUnits = extractTextUnitsFromPages(pages, { ...resolvedOptions, includeSources: true });
4285
+ const sentences = [];
4286
+ let globalIndex = 0;
4287
+ textUnits.filter((unit) => resolvedOptions.includeTextUnitTypes.includes(unit.type)).forEach((unit) => {
4288
+ const page = pages.find((candidatePage) => candidatePage.pageNumber === unit.pageNumber);
4289
+ if (!page) return;
4290
+ const pageTextItemsByIndex = new Map(
4291
+ page.textItems.map((item) => [item.index, item])
4292
+ );
4293
+ const stream = buildPageTextStream(
4294
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4295
+ unit.source.textItemIndexes.map((textItemIndex) => pageTextItemsByIndex.get(textItemIndex)).filter((item) => item !== void 0)
4296
+ );
4297
+ if (!stream.text.trim()) return;
4298
+ const sentenceRanges = splitPdfSentences(
4299
+ stream.text,
4300
+ resolvedOptions.locale
4301
+ );
4302
+ sentenceRanges.forEach((sentenceRange, indexInPage) => {
4303
+ const textItemIndexes = getSentenceTextItemIndexes(
4304
+ stream.charItemIndexes,
4305
+ sentenceRange.start,
4306
+ sentenceRange.end
4307
+ );
4308
+ const position = resolvedOptions.includePositions ? getScaledPosition(page.textItems, textItemIndexes, page) : void 0;
4309
+ const id = `${resolvedOptions.idPrefix}p${page.pageNumber}-u${unit.indexInPage}-s${indexInPage}`;
4310
+ const rawText = sentenceRange.text;
4311
+ sentences.push({
4312
+ id,
4313
+ text: resolvedOptions.normalize ? normalizePdfSentenceText(rawText) : rawText,
4314
+ rawText,
4315
+ pageNumber: page.pageNumber,
4316
+ indexInPage,
4317
+ globalIndex,
4318
+ ...unit.columnIndex !== void 0 ? { columnIndex: unit.columnIndex } : {},
4319
+ ...position ? { position } : {},
4320
+ ...resolvedOptions.includeSources ? { source: { startOffset: sentenceRange.start, endOffset: sentenceRange.end, textItemIndexes } } : {}
4321
+ });
4322
+ globalIndex += 1;
4323
+ });
4324
+ });
4325
+ return sentences;
4326
+ };
4327
+ var sentenceToHighlight = (sentence, options = {}) => {
4328
+ if (!sentence.position) {
4329
+ throw new Error(
4330
+ "Cannot convert sentence to highlight because it has no position. Call extractSentences with includePositions: true."
4331
+ );
3644
4332
  }
3645
- return pdfDoc.save();
3646
- }
4333
+ return {
4334
+ id: options.id ?? sentence.id,
4335
+ type: "text",
4336
+ content: { text: sentence.text },
4337
+ position: sentence.position
4338
+ };
4339
+ };
3647
4340
 
3648
4341
  // src/components/leftpanel/LeftPanel.tsx
3649
4342
  import React21, { useState as useState17, useMemo as useMemo3, useCallback as useCallback8, useRef as useRef19, useEffect as useEffect18 } from "react";
@@ -4971,6 +5664,12 @@ var LeftPanel = ({
4971
5664
  )
4972
5665
  ));
4973
5666
  };
5667
+
5668
+ // src/index.ts
5669
+ var exportPdf = async (...args) => {
5670
+ const { exportPdf: exportPdf2 } = await import("./export-pdf-W2QGWADM.js");
5671
+ return exportPdf2(...args);
5672
+ };
4974
5673
  export {
4975
5674
  AreaHighlight,
4976
5675
  DocumentOutline,
@@ -4990,7 +5689,11 @@ export {
4990
5689
  ThumbnailItem,
4991
5690
  ThumbnailPanel,
4992
5691
  exportPdf,
5692
+ extractPageTextItems,
5693
+ extractSentences,
5694
+ extractTextUnits,
4993
5695
  scaledPositionToViewport,
5696
+ sentenceToHighlight,
4994
5697
  useDocumentOutline,
4995
5698
  useHighlightContainerContext,
4996
5699
  useLeftPanelContext,