react-pdf-highlighter-plus 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,
@@ -88,6 +87,323 @@ var scaledPositionToViewport = ({ boundingRect, rects, usePdfCoordinates }, view
88
87
  };
89
88
  };
90
89
 
90
+ // src/lib/dark-mode.ts
91
+ var DEFAULT_DARK_MODE_COLORS = {
92
+ background: "#141210",
93
+ foreground: "#eae6e0"
94
+ };
95
+ var NAMED_COLORS = {
96
+ white: [1, 1, 1, 1],
97
+ black: [0, 0, 0, 1],
98
+ transparent: [0, 0, 0, 0]
99
+ };
100
+ function clamp01(value) {
101
+ return value < 0 ? 0 : value > 1 ? 1 : value;
102
+ }
103
+ function parseColor(input) {
104
+ const str = input.trim().toLowerCase();
105
+ const named = Object.hasOwn(NAMED_COLORS, str) ? NAMED_COLORS[str] : void 0;
106
+ if (named) return named;
107
+ if (str.startsWith("#")) {
108
+ const hex = str.slice(1);
109
+ if (!/^[0-9a-f]{3,8}$/.test(hex)) return null;
110
+ if (hex.length === 3 || hex.length === 4) {
111
+ const r = Number.parseInt(hex[0], 16) / 15;
112
+ const g = Number.parseInt(hex[1], 16) / 15;
113
+ const b = Number.parseInt(hex[2], 16) / 15;
114
+ const a = hex.length === 4 ? Number.parseInt(hex[3], 16) / 15 : 1;
115
+ return [r, g, b, a];
116
+ }
117
+ if (hex.length === 6 || hex.length === 8) {
118
+ const r = Number.parseInt(hex.slice(0, 2), 16) / 255;
119
+ const g = Number.parseInt(hex.slice(2, 4), 16) / 255;
120
+ const b = Number.parseInt(hex.slice(4, 6), 16) / 255;
121
+ const a = hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) / 255 : 1;
122
+ return [r, g, b, a];
123
+ }
124
+ return null;
125
+ }
126
+ const fn = /^rgba?\(([^)]+)\)$/.exec(str);
127
+ if (fn) {
128
+ const parts = fn[1].split(/[,\s/]+/).filter(Boolean);
129
+ if (parts.length < 3) return null;
130
+ const channel = (part) => part.endsWith("%") ? Number.parseFloat(part) / 100 : Number.parseFloat(part) / 255;
131
+ const alpha = (part) => part.endsWith("%") ? Number.parseFloat(part) / 100 : Number.parseFloat(part);
132
+ const r = channel(parts[0]);
133
+ const g = channel(parts[1]);
134
+ const b = channel(parts[2]);
135
+ const a = parts[3] !== void 0 ? alpha(parts[3]) : 1;
136
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) || Number.isNaN(a))
137
+ return null;
138
+ return [clamp01(r), clamp01(g), clamp01(b), clamp01(a)];
139
+ }
140
+ const normalized = normalizeCssColor(str);
141
+ if (normalized !== null && normalized !== str) return parseColor(normalized);
142
+ return null;
143
+ }
144
+ var normalizeCtx;
145
+ function normalizeCssColor(input) {
146
+ if (normalizeCtx === void 0) {
147
+ normalizeCtx = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d", {
148
+ willReadFrequently: true
149
+ });
150
+ }
151
+ const ctx = normalizeCtx;
152
+ if (!ctx) return null;
153
+ ctx.fillStyle = "#000000";
154
+ ctx.fillStyle = input;
155
+ const first = ctx.fillStyle;
156
+ ctx.fillStyle = "#ffffff";
157
+ ctx.fillStyle = input;
158
+ return first === ctx.fillStyle ? first : null;
159
+ }
160
+ function srgbToLinear(c) {
161
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
162
+ }
163
+ function linearToSrgb(c) {
164
+ return c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
165
+ }
166
+ function rgbToOklab(r, g, b) {
167
+ const lr = srgbToLinear(r);
168
+ const lg = srgbToLinear(g);
169
+ const lb = srgbToLinear(b);
170
+ const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
171
+ const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
172
+ const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
173
+ return [
174
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
175
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
176
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
177
+ ];
178
+ }
179
+ function oklabToLinearRgb(L, a, b) {
180
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
181
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
182
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
183
+ return [
184
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
185
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
186
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
187
+ ];
188
+ }
189
+ var GAMUT_EPSILON = 5e-4;
190
+ function oklabToSrgbExact(L, a, b) {
191
+ const [lr, lg, lb] = oklabToLinearRgb(L, a, b);
192
+ if (lr < -GAMUT_EPSILON || lr > 1 + GAMUT_EPSILON || lg < -GAMUT_EPSILON || lg > 1 + GAMUT_EPSILON || lb < -GAMUT_EPSILON || lb > 1 + GAMUT_EPSILON) {
193
+ return null;
194
+ }
195
+ return [
196
+ linearToSrgb(clamp01(lr)),
197
+ linearToSrgb(clamp01(lg)),
198
+ linearToSrgb(clamp01(lb))
199
+ ];
200
+ }
201
+ function oklabToSrgbGamutMapped(L, a, b) {
202
+ const exact = oklabToSrgbExact(L, a, b);
203
+ if (exact) return exact;
204
+ let lo = 0;
205
+ let hi = 1;
206
+ for (let i = 0; i < 12; i++) {
207
+ const mid = (lo + hi) / 2;
208
+ if (oklabToSrgbExact(L, a * mid, b * mid)) lo = mid;
209
+ else hi = mid;
210
+ }
211
+ const [lr, lg, lb] = oklabToLinearRgb(L, a * lo, b * lo);
212
+ return [
213
+ linearToSrgb(clamp01(lr)),
214
+ linearToSrgb(clamp01(lg)),
215
+ linearToSrgb(clamp01(lb))
216
+ ];
217
+ }
218
+ function toHexByte(channel) {
219
+ return Math.round(clamp01(channel) * 255).toString(16).padStart(2, "0");
220
+ }
221
+ function toCssColor(r, g, b, a) {
222
+ const rgb = `#${toHexByte(r)}${toHexByte(g)}${toHexByte(b)}`;
223
+ return a >= 1 ? rgb : `${rgb}${toHexByte(a)}`;
224
+ }
225
+ var NEUTRAL_CHROMA_THRESHOLD = 0.04;
226
+ var COLOR_CACHE_MAX = 4096;
227
+ var mapInstances = /* @__PURE__ */ new Map();
228
+ function createDarkModeColorMap(colors) {
229
+ const background = colors?.background ?? DEFAULT_DARK_MODE_COLORS.background;
230
+ const foreground = colors?.foreground ?? DEFAULT_DARK_MODE_COLORS.foreground;
231
+ const instanceKey = `${background}|${foreground}`;
232
+ const existing = mapInstances.get(instanceKey);
233
+ if (existing) {
234
+ mapInstances.delete(instanceKey);
235
+ mapInstances.set(instanceKey, existing);
236
+ return existing;
237
+ }
238
+ const bgRgba = parseColor(background) ?? parseColor(DEFAULT_DARK_MODE_COLORS.background);
239
+ const fgRgba = parseColor(foreground) ?? parseColor(DEFAULT_DARK_MODE_COLORS.foreground);
240
+ const bgLab = rgbToOklab(bgRgba[0], bgRgba[1], bgRgba[2]);
241
+ const fgLab = rgbToOklab(fgRgba[0], fgRgba[1], fgRgba[2]);
242
+ const cache = /* @__PURE__ */ new Map();
243
+ const backgroundCss = toCssColor(bgRgba[0], bgRgba[1], bgRgba[2], 1);
244
+ const foregroundCss = toCssColor(fgRgba[0], fgRgba[1], fgRgba[2], 1);
245
+ const seedPoles = () => {
246
+ for (const white of ["#ffffff", "#fff", "white"])
247
+ cache.set(white, backgroundCss);
248
+ for (const black of ["#000000", "#000", "black"])
249
+ cache.set(black, foregroundCss);
250
+ };
251
+ seedPoles();
252
+ const transform = (input) => {
253
+ const parsed = parseColor(input);
254
+ if (!parsed || parsed[3] === 0) return input;
255
+ const [r, g, b, alpha] = parsed;
256
+ const [, labA, labB] = rgbToOklab(r, g, b);
257
+ const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
258
+ const rampL = fgLab[0] + (bgLab[0] - fgLab[0]) * luma;
259
+ const rampA = fgLab[1] + (bgLab[1] - fgLab[1]) * luma;
260
+ const rampB = fgLab[2] + (bgLab[2] - fgLab[2]) * luma;
261
+ const chroma = Math.hypot(labA, labB);
262
+ const hueWeight = Math.min(1, chroma / NEUTRAL_CHROMA_THRESHOLD);
263
+ const outA = rampA + (labA - rampA) * hueWeight;
264
+ const outB = rampB + (labB - rampB) * hueWeight;
265
+ const [sr, sg, sb] = oklabToSrgbGamutMapped(rampL, outA, outB);
266
+ return toCssColor(sr, sg, sb, alpha);
267
+ };
268
+ const map = (input) => {
269
+ const hit = cache.get(input);
270
+ if (hit !== void 0) return hit;
271
+ const out = transform(input);
272
+ if (cache.size >= COLOR_CACHE_MAX) {
273
+ cache.clear();
274
+ seedPoles();
275
+ }
276
+ cache.set(input, out);
277
+ return out;
278
+ };
279
+ if (mapInstances.size >= 16) {
280
+ const oldest = mapInstances.keys().next().value;
281
+ if (oldest !== void 0) mapInstances.delete(oldest);
282
+ }
283
+ mapInstances.set(instanceKey, map);
284
+ return map;
285
+ }
286
+
287
+ // src/lib/recolor-context.ts
288
+ var RECOLOR_CLEANUP = /* @__PURE__ */ Symbol("recolorCleanup");
289
+ var RECOLOR_PAINTED = /* @__PURE__ */ Symbol("recolorPainted");
290
+ var FILL_METHODS = ["fill", "fillRect", "fillText"];
291
+ var STROKE_METHODS = ["stroke", "strokeRect", "strokeText"];
292
+ var GRADIENT_METHODS = [
293
+ "createLinearGradient",
294
+ "createRadialGradient"
295
+ ];
296
+ function ntscLuma(r, g, b) {
297
+ return 0.3 * r + 0.59 * g + 0.11 * b;
298
+ }
299
+ function parseHexLuma(color) {
300
+ if (!/^#[0-9a-f]{6}$/i.test(color)) return null;
301
+ return ntscLuma(
302
+ Number.parseInt(color.slice(1, 3), 16),
303
+ Number.parseInt(color.slice(3, 5), 16),
304
+ Number.parseInt(color.slice(5, 7), 16)
305
+ );
306
+ }
307
+ function correctLuminosityMask(source, mappedWhiteLuma, mappedBlackLuma) {
308
+ if (typeof document === "undefined") return null;
309
+ if (!(typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement) && !(typeof OffscreenCanvas !== "undefined" && source instanceof OffscreenCanvas)) {
310
+ return null;
311
+ }
312
+ const width = source.width;
313
+ const height = source.height;
314
+ if (!width || !height) return null;
315
+ const span = mappedWhiteLuma - mappedBlackLuma;
316
+ if (Math.abs(span) < 1) return null;
317
+ const tmp = document.createElement("canvas");
318
+ tmp.width = width;
319
+ tmp.height = height;
320
+ const tmpCtx = tmp.getContext("2d", { willReadFrequently: true });
321
+ if (!tmpCtx) return null;
322
+ tmpCtx.drawImage(source, 0, 0);
323
+ let image;
324
+ try {
325
+ image = tmpCtx.getImageData(0, 0, width, height);
326
+ } catch {
327
+ return null;
328
+ }
329
+ const data = image.data;
330
+ for (let i = 0; i < data.length; i += 4) {
331
+ const luma = ntscLuma(data[i], data[i + 1], data[i + 2]);
332
+ const t = (luma - mappedBlackLuma) / span;
333
+ const gray = t <= 0 ? 0 : t >= 1 ? 255 : Math.round(t * 255);
334
+ data[i] = data[i + 1] = data[i + 2] = gray;
335
+ }
336
+ tmpCtx.putImageData(image, 0, 0);
337
+ return tmp;
338
+ }
339
+ function applyContextRecolor(ctx, map) {
340
+ const target = ctx;
341
+ target[RECOLOR_CLEANUP]?.();
342
+ const mappedWhiteLuma = parseHexLuma(map("#ffffff"));
343
+ const mappedBlackLuma = parseHexLuma(map("#000000"));
344
+ const withMaskCorrection = (original) => function(...args) {
345
+ if (mappedWhiteLuma !== null && mappedBlackLuma !== null && this.globalCompositeOperation === "destination-in" && typeof this.filter === "string" && this.filter.includes("luminosity")) {
346
+ const source = args[0];
347
+ const sourcePainted = (typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement || typeof OffscreenCanvas !== "undefined" && source instanceof OffscreenCanvas) && source.getContext("2d")?.[RECOLOR_PAINTED] === true;
348
+ const corrected = sourcePainted ? correctLuminosityMask(source, mappedWhiteLuma, mappedBlackLuma) : null;
349
+ if (corrected) {
350
+ const next = [corrected, ...args.slice(1)];
351
+ return original.apply(this, next);
352
+ }
353
+ }
354
+ return original.apply(this, args);
355
+ };
356
+ const withMappedStyle = (original, styleProp) => function(...args) {
357
+ const style = this[styleProp];
358
+ if (typeof style === "string") {
359
+ const mapped = map(style);
360
+ if (mapped !== style) {
361
+ this[RECOLOR_PAINTED] = true;
362
+ this[styleProp] = mapped;
363
+ try {
364
+ return original.apply(this, args);
365
+ } finally {
366
+ this[styleProp] = style;
367
+ }
368
+ }
369
+ }
370
+ return original.apply(this, args);
371
+ };
372
+ const withMappedStops = (original) => function(...args) {
373
+ const gradient = original.apply(this, args);
374
+ const addColorStop = gradient.addColorStop.bind(gradient);
375
+ const context = this;
376
+ gradient.addColorStop = (offset, color) => {
377
+ const mapped = map(color);
378
+ if (mapped !== color) context[RECOLOR_PAINTED] = true;
379
+ addColorStop(offset, mapped);
380
+ };
381
+ return gradient;
382
+ };
383
+ const record = target;
384
+ for (const name of FILL_METHODS)
385
+ record[name] = withMappedStyle(target[name], "fillStyle");
386
+ for (const name of STROKE_METHODS)
387
+ record[name] = withMappedStyle(target[name], "strokeStyle");
388
+ for (const name of GRADIENT_METHODS)
389
+ record[name] = withMappedStops(target[name]);
390
+ record.drawImage = withMaskCorrection(target.drawImage);
391
+ const cleanup = () => {
392
+ if (target[RECOLOR_CLEANUP] !== cleanup) return;
393
+ for (const name of [
394
+ ...FILL_METHODS,
395
+ ...STROKE_METHODS,
396
+ ...GRADIENT_METHODS,
397
+ "drawImage"
398
+ ])
399
+ delete record[name];
400
+ delete target[RECOLOR_CLEANUP];
401
+ delete target[RECOLOR_PAINTED];
402
+ };
403
+ target[RECOLOR_CLEANUP] = cleanup;
404
+ return cleanup;
405
+ }
406
+
91
407
  // src/lib/get-bounding-rect.ts
92
408
  var getBoundingRect = (clientRects) => {
93
409
  const rects = Array.from(clientRects).map((rect) => {
@@ -293,16 +609,6 @@ var getPagesFromRange = (range) => {
293
609
  }
294
610
  return pages;
295
611
  };
296
- var findOrCreateContainerLayer = (container, className) => {
297
- const doc = getDocument(container);
298
- let layer = container.querySelector(`.${className}`);
299
- if (!layer && container.children.length) {
300
- layer = doc.createElement("div");
301
- layer.className = className;
302
- container.appendChild(layer);
303
- }
304
- return layer;
305
- };
306
612
 
307
613
  // src/components/DrawingCanvas.tsx
308
614
  import React, {
@@ -659,9 +965,12 @@ var HighlightLayer = ({
659
965
  scrolledToHighlightId,
660
966
  viewer,
661
967
  highlightBindings,
662
- children
968
+ children,
969
+ shouldRenderHighlight
663
970
  }) => {
664
- const currentHighlights = highlightsByPage[pageNumber] || [];
971
+ const currentHighlights = (highlightsByPage[pageNumber] || []).filter(
972
+ (highlight) => shouldRenderHighlight?.(highlight) ?? true
973
+ );
665
974
  return /* @__PURE__ */ React2.createElement("div", null, currentHighlights.map((highlight, index) => {
666
975
  const viewportHighlight = {
667
976
  ...highlight,
@@ -1111,38 +1420,94 @@ var TipContainer = ({
1111
1420
 
1112
1421
  // src/components/PdfHighlighter.tsx
1113
1422
  var EventBus;
1423
+ var PDFFindController;
1114
1424
  var PDFLinkService;
1115
1425
  var PDFViewer;
1116
1426
  (async () => {
1117
1427
  const pdfjs = await import("pdfjs-dist/web/pdf_viewer.mjs");
1118
1428
  EventBus = pdfjs.EventBus;
1429
+ PDFFindController = pdfjs.PDFFindController;
1119
1430
  PDFLinkService = pdfjs.PDFLinkService;
1120
1431
  PDFViewer = pdfjs.PDFViewer;
1121
1432
  })();
1122
1433
  var SCROLL_MARGIN = 10;
1123
1434
  var DEFAULT_SCALE_VALUE = "auto";
1124
1435
  var DEFAULT_TEXT_SELECTION_COLOR = "rgba(153,193,218,255)";
1436
+ var DEFAULT_DARK_MODE_COLORS2 = {
1437
+ background: "#141210",
1438
+ foreground: "#eae6e0"
1439
+ };
1440
+ var unmountReactRoot = (root) => {
1441
+ if (!root) return;
1442
+ queueMicrotask(() => {
1443
+ try {
1444
+ root.unmount();
1445
+ } catch {
1446
+ }
1447
+ });
1448
+ };
1449
+ var RECOLOR_PATCHED = /* @__PURE__ */ Symbol("pdfRecolorPatched");
1450
+ var patchPageRenderRecolor = (pdfDocument, getMap) => {
1451
+ const doc = pdfDocument;
1452
+ if (doc[RECOLOR_PATCHED]) return;
1453
+ doc[RECOLOR_PATCHED] = true;
1454
+ const origGetPage = doc.getPage.bind(doc);
1455
+ doc.getPage = (pageNumber) => origGetPage(pageNumber).then((page) => {
1456
+ const p = page;
1457
+ if (p[RECOLOR_PATCHED]) return p;
1458
+ p[RECOLOR_PATCHED] = true;
1459
+ const origRender = p.render.bind(p);
1460
+ p.render = ((params) => {
1461
+ const map = getMap();
1462
+ const ctx = params?.canvasContext;
1463
+ if (map && ctx) {
1464
+ const cleanup = applyContextRecolor(ctx, map);
1465
+ const task = origRender(params);
1466
+ task.promise.then(cleanup, cleanup);
1467
+ return task;
1468
+ }
1469
+ return origRender(params);
1470
+ });
1471
+ return p;
1472
+ });
1473
+ };
1125
1474
  var defaultLightTheme = {
1126
1475
  mode: "light",
1127
1476
  containerBackgroundColor: "#e5e5e5",
1128
1477
  scrollbarThumbColor: "#9f9f9f",
1129
1478
  scrollbarTrackColor: "#d1d1d1",
1130
- darkModeInvertIntensity: 0.9
1479
+ darkModeInvertIntensity: 0.9,
1480
+ darkModeColors: DEFAULT_DARK_MODE_COLORS2
1131
1481
  };
1132
1482
  var defaultDarkTheme = {
1133
1483
  mode: "dark",
1134
1484
  containerBackgroundColor: "#3a3a3a",
1135
- // Lighter than PDF page (~#1a1a1a) for contrast
1485
+ // Lighter than PDF page for contrast
1136
1486
  scrollbarThumbColor: "#6b6b6b",
1137
1487
  scrollbarTrackColor: "#2c2c2c",
1138
- darkModeInvertIntensity: 0.9
1488
+ darkModeInvertIntensity: 0.9,
1489
+ darkModeColors: DEFAULT_DARK_MODE_COLORS2
1139
1490
  };
1140
- var findOrCreateHighlightLayer = (textLayer) => {
1141
- return findOrCreateContainerLayer(
1142
- textLayer,
1143
- "PdfHighlighter__highlight-layer"
1144
- );
1491
+ var ensurePersistentLayer = (bindings, pageNumber, parent, className) => {
1492
+ let binding = bindings[pageNumber];
1493
+ if (!binding) {
1494
+ const doc = getDocument(parent);
1495
+ const layer = doc.createElement("div");
1496
+ layer.className = className;
1497
+ parent.appendChild(layer);
1498
+ binding = {
1499
+ reactRoot: createRoot(layer),
1500
+ container: layer,
1501
+ textLayer: parent
1502
+ };
1503
+ bindings[pageNumber] = binding;
1504
+ } else if (binding.container.parentNode !== parent) {
1505
+ parent.appendChild(binding.container);
1506
+ binding.textLayer = parent;
1507
+ }
1508
+ return binding;
1145
1509
  };
1510
+ var isFreetextHighlight = (highlight) => "type" in highlight && highlight.type === "freetext";
1146
1511
  var disableTextSelection = (viewer, flag) => {
1147
1512
  viewer.viewer?.classList.toggle("PdfHighlighter--disable-selection", flag);
1148
1513
  };
@@ -1150,6 +1515,9 @@ var PdfHighlighter = ({
1150
1515
  highlights,
1151
1516
  onScrollAway,
1152
1517
  pdfScaleValue = DEFAULT_SCALE_VALUE,
1518
+ onZoomChange,
1519
+ initialPage,
1520
+ onPageChange,
1153
1521
  onSelection: onSelectionFinished,
1154
1522
  onCreateGhostHighlight,
1155
1523
  onRemoveGhostHighlight,
@@ -1169,12 +1537,12 @@ var PdfHighlighter = ({
1169
1537
  enableDrawingMode,
1170
1538
  onDrawingComplete,
1171
1539
  onDrawingCancel,
1172
- drawingStrokeColor = "#000000",
1540
+ drawingStrokeColor: drawingStrokeColorProp,
1173
1541
  drawingStrokeWidth = 3,
1174
1542
  enableShapeMode,
1175
1543
  onShapeComplete,
1176
1544
  onShapeCancel,
1177
- shapeStrokeColor = "#000000",
1545
+ shapeStrokeColor: shapeStrokeColorProp,
1178
1546
  shapeStrokeWidth = 2,
1179
1547
  theme: userTheme
1180
1548
  }) => {
@@ -1183,12 +1551,27 @@ var PdfHighlighter = ({
1183
1551
  const defaults = mode === "light" ? defaultLightTheme : defaultDarkTheme;
1184
1552
  return { ...defaults, ...userTheme, mode };
1185
1553
  }, [userTheme]);
1554
+ const recolorKey = resolvedTheme.mode === "dark" ? `${resolvedTheme.darkModeColors.background}|${resolvedTheme.darkModeColors.foreground}` : "light";
1555
+ const recolorMap = useMemo(
1556
+ () => resolvedTheme.mode === "dark" ? createDarkModeColorMap(resolvedTheme.darkModeColors) : null,
1557
+ // recolorKey captures the palette + mode; map identity is stable per key.
1558
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1559
+ [recolorKey]
1560
+ );
1561
+ const recolorMapRef = useRef5(recolorMap);
1562
+ recolorMapRef.current = recolorMap;
1563
+ const defaultStrokeColor = resolvedTheme.mode === "dark" ? "#ffffff" : "#000000";
1564
+ const drawingStrokeColor = drawingStrokeColorProp ?? defaultStrokeColor;
1565
+ const shapeStrokeColor = shapeStrokeColorProp ?? defaultStrokeColor;
1186
1566
  const [tip, setTip] = useState5(null);
1187
1567
  const [isViewerReady, setIsViewerReady] = useState5(false);
1188
1568
  const containerNodeRef = useRef5(null);
1189
1569
  const highlightBindingsRef = useRef5(
1190
1570
  {}
1191
1571
  );
1572
+ const noteBindingsRef = useRef5({});
1573
+ const highlightsRef = useRef5(highlights);
1574
+ const childrenRef = useRef5(children);
1192
1575
  const ghostHighlightRef = useRef5(null);
1193
1576
  const selectionRef = useRef5(null);
1194
1577
  const scrolledToHighlightIdRef = useRef5(null);
@@ -1204,43 +1587,324 @@ var PdfHighlighter = ({
1204
1587
  })
1205
1588
  );
1206
1589
  const resizeObserverRef = useRef5(null);
1590
+ const renderRetryTimeoutsRef = useRef5(
1591
+ []
1592
+ );
1593
+ const resumeScrollAwayTimeoutRef = useRef5(
1594
+ null
1595
+ );
1596
+ const findControllerRef = useRef5(null);
1207
1597
  const viewerRef = useRef5(null);
1598
+ const prevDocRef = useRef5(null);
1599
+ const lastRenderKeyRef = useRef5(null);
1600
+ highlightsRef.current = highlights;
1601
+ childrenRef.current = children;
1208
1602
  useLayoutEffect2(() => {
1209
1603
  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]);
1604
+ const renderKey = `${pdfDocument.fingerprints ?? pdfDocument.numPages}::${recolorKey}`;
1605
+ if (lastRenderKeyRef.current === renderKey) {
1606
+ console.log("[PdfHighlighter] skip duplicate setDocument", renderKey);
1607
+ return;
1608
+ }
1609
+ findControllerRef.current = findControllerRef.current || new PDFFindController({
1610
+ eventBus: eventBusRef.current,
1611
+ linkService: linkServiceRef.current
1612
+ });
1613
+ viewerRef.current = viewerRef.current || new PDFViewer({
1614
+ container: containerNodeRef.current,
1615
+ eventBus: eventBusRef.current,
1616
+ findController: findControllerRef.current,
1617
+ textLayerMode: 2,
1618
+ removePageBorders: true,
1619
+ linkService: linkServiceRef.current
1620
+ });
1621
+ patchPageRenderRecolor(pdfDocument, () => recolorMapRef.current);
1622
+ const isToggle = prevDocRef.current === pdfDocument;
1623
+ prevDocRef.current = pdfDocument;
1624
+ let scrollFraction = 0;
1625
+ let savedScaleValue;
1626
+ if (isToggle && viewerRef.current.container) {
1627
+ const c = viewerRef.current.container;
1628
+ const range = c.scrollHeight - c.clientHeight;
1629
+ scrollFraction = range > 0 ? c.scrollTop / range : 0;
1630
+ savedScaleValue = viewerRef.current.currentScaleValue ?? viewerRef.current.currentScale;
1631
+ }
1632
+ console.log("[PdfHighlighter] recolor active?", {
1633
+ mode: resolvedTheme.mode,
1634
+ hasMap: !!recolorMapRef.current,
1635
+ palette: resolvedTheme.darkModeColors,
1636
+ isToggle,
1637
+ scrollFraction,
1638
+ savedScaleValue
1639
+ });
1640
+ if (isToggle) {
1641
+ const restoreView = () => {
1642
+ eventBusRef.current.off("pagesinit", restoreView);
1643
+ if (savedScaleValue != null && viewerRef.current) {
1644
+ viewerRef.current.currentScaleValue = String(savedScaleValue);
1645
+ }
1646
+ requestAnimationFrame(() => {
1647
+ const c = viewerRef.current?.container;
1648
+ if (!c) return;
1649
+ const range = c.scrollHeight - c.clientHeight;
1650
+ c.scrollTop = scrollFraction * Math.max(0, range);
1651
+ console.log("[PdfHighlighter] restored view after toggle", {
1652
+ scrollFraction,
1653
+ scrollTop: c.scrollTop,
1654
+ scaleValue: viewerRef.current?.currentScaleValue
1655
+ });
1656
+ });
1657
+ };
1658
+ eventBusRef.current.on("pagesinit", restoreView);
1659
+ }
1660
+ lastRenderKeyRef.current = renderKey;
1661
+ viewerRef.current.setDocument(pdfDocument);
1662
+ linkServiceRef.current.setDocument(pdfDocument);
1663
+ linkServiceRef.current.setViewer(viewerRef.current);
1664
+ setIsViewerReady(true);
1665
+ }, [pdfDocument, recolorKey]);
1228
1666
  useLayoutEffect2(() => {
1229
1667
  if (!containerNodeRef.current) return;
1230
1668
  resizeObserverRef.current = new ResizeObserver(handleScaleValue);
1231
1669
  resizeObserverRef.current.observe(containerNodeRef.current);
1232
1670
  const doc = containerNodeRef.current.ownerDocument;
1233
- eventBusRef.current.on("textlayerrendered", renderHighlightLayers);
1671
+ eventBusRef.current.on("textlayerrendered", scheduleRenderHighlightLayers);
1672
+ eventBusRef.current.on("pagerendered", scheduleRenderHighlightLayers);
1234
1673
  eventBusRef.current.on("pagesinit", handleScaleValue);
1235
1674
  doc.addEventListener("keydown", handleKeyDown);
1236
- renderHighlightLayers();
1675
+ doc.addEventListener("copy", handleCopy, true);
1676
+ scheduleRenderHighlightLayers();
1237
1677
  return () => {
1238
1678
  eventBusRef.current.off("pagesinit", handleScaleValue);
1239
- eventBusRef.current.off("textlayerrendered", renderHighlightLayers);
1679
+ eventBusRef.current.off("pagerendered", scheduleRenderHighlightLayers);
1680
+ eventBusRef.current.off("textlayerrendered", scheduleRenderHighlightLayers);
1240
1681
  doc.removeEventListener("keydown", handleKeyDown);
1682
+ doc.removeEventListener("copy", handleCopy, true);
1241
1683
  resizeObserverRef.current?.disconnect();
1684
+ renderRetryTimeoutsRef.current.forEach(clearTimeout);
1685
+ renderRetryTimeoutsRef.current = [];
1686
+ if (resumeScrollAwayTimeoutRef.current) {
1687
+ clearTimeout(resumeScrollAwayTimeoutRef.current);
1688
+ resumeScrollAwayTimeoutRef.current = null;
1689
+ }
1242
1690
  };
1243
1691
  }, [selectionTip, highlights, onSelectionFinished]);
1692
+ useEffect4(() => {
1693
+ return () => {
1694
+ for (const binding of Object.values(highlightBindingsRef.current))
1695
+ unmountReactRoot(binding?.reactRoot);
1696
+ for (const binding of Object.values(noteBindingsRef.current))
1697
+ unmountReactRoot(binding?.reactRoot);
1698
+ };
1699
+ }, []);
1700
+ const onPageChangeRef = useRef5(onPageChange);
1701
+ onPageChangeRef.current = onPageChange;
1702
+ const initialPageRef = useRef5(initialPage);
1703
+ initialPageRef.current = initialPage;
1704
+ const initialPageAppliedRef = useRef5(false);
1705
+ const pendingInitialPageRef = useRef5(null);
1706
+ useEffect4(() => {
1707
+ const eventBus = eventBusRef.current;
1708
+ const handlePageChanging = (evt) => {
1709
+ if (pendingInitialPageRef.current != null) return;
1710
+ onPageChangeRef.current?.(evt.pageNumber);
1711
+ };
1712
+ eventBus.on("pagechanging", handlePageChanging);
1713
+ const handlePagesInit = () => {
1714
+ if (initialPageAppliedRef.current) return;
1715
+ initialPageAppliedRef.current = true;
1716
+ const page = initialPageRef.current;
1717
+ console.log("[PdfHighlighter] pagesinit, initialPage =", page);
1718
+ if (!page || page <= 1) return;
1719
+ pendingInitialPageRef.current = page;
1720
+ let tries = 0;
1721
+ let matches = 0;
1722
+ const tick = () => {
1723
+ const viewer = viewerRef.current;
1724
+ if (!viewer || pendingInitialPageRef.current == null) return;
1725
+ tries++;
1726
+ if (viewer.currentPageNumber === page) {
1727
+ matches++;
1728
+ } else {
1729
+ matches = 0;
1730
+ try {
1731
+ viewer.scrollPageIntoView({ pageNumber: page });
1732
+ } catch (e) {
1733
+ console.log("[PdfHighlighter] scrollPageIntoView threw", e);
1734
+ }
1735
+ }
1736
+ if (matches >= 3) {
1737
+ pendingInitialPageRef.current = null;
1738
+ onPageChangeRef.current?.(page);
1739
+ console.log("[PdfHighlighter] initialPage settled at", page);
1740
+ return;
1741
+ }
1742
+ if (tries > 40) {
1743
+ pendingInitialPageRef.current = null;
1744
+ console.log("[PdfHighlighter] initialPage gave up at", page);
1745
+ return;
1746
+ }
1747
+ setTimeout(tick, 50);
1748
+ };
1749
+ setTimeout(tick, 0);
1750
+ };
1751
+ eventBus.on("pagesinit", handlePagesInit);
1752
+ return () => {
1753
+ eventBus.off("pagechanging", handlePageChanging);
1754
+ eventBus.off("pagesinit", handlePagesInit);
1755
+ pendingInitialPageRef.current = null;
1756
+ };
1757
+ }, []);
1758
+ const onZoomChangeRef = useRef5(onZoomChange);
1759
+ onZoomChangeRef.current = onZoomChange;
1760
+ useEffect4(() => {
1761
+ const container = containerNodeRef.current;
1762
+ if (!container || !isViewerReady) return;
1763
+ const MIN_SCALE = 0.25;
1764
+ const MAX_SCALE = 10;
1765
+ const COMMIT_DELAY = 140;
1766
+ const g = {
1767
+ active: false,
1768
+ startScale: 1,
1769
+ startScrollLeft: 0,
1770
+ startScrollTop: 0,
1771
+ anchorX: 0,
1772
+ anchorY: 0,
1773
+ originX: 0,
1774
+ originY: 0,
1775
+ k: 1,
1776
+ raf: 0,
1777
+ commitTimer: 0
1778
+ };
1779
+ const pdfViewerEl = () => container.querySelector(".pdfViewer");
1780
+ const clampK = (k) => Math.min(
1781
+ MAX_SCALE / g.startScale,
1782
+ Math.max(MIN_SCALE / g.startScale, k)
1783
+ );
1784
+ const begin = (anchorX, anchorY) => {
1785
+ const viewer = viewerRef.current;
1786
+ const el = pdfViewerEl();
1787
+ if (!viewer || !el) return false;
1788
+ g.active = true;
1789
+ g.startScale = viewer.currentScale;
1790
+ g.startScrollLeft = container.scrollLeft;
1791
+ g.startScrollTop = container.scrollTop;
1792
+ g.anchorX = anchorX;
1793
+ g.anchorY = anchorY;
1794
+ g.originX = container.scrollLeft + anchorX;
1795
+ g.originY = container.scrollTop + anchorY;
1796
+ g.k = 1;
1797
+ el.style.transformOrigin = `${g.originX}px ${g.originY}px`;
1798
+ el.style.willChange = "transform";
1799
+ return true;
1800
+ };
1801
+ const previewRaf = () => {
1802
+ g.raf = 0;
1803
+ const el = pdfViewerEl();
1804
+ if (el) el.style.transform = `scale(${g.k})`;
1805
+ };
1806
+ const preview = () => {
1807
+ if (!g.raf) g.raf = requestAnimationFrame(previewRaf);
1808
+ };
1809
+ const commit = () => {
1810
+ if (!g.active) return;
1811
+ g.active = false;
1812
+ if (g.raf) {
1813
+ cancelAnimationFrame(g.raf);
1814
+ g.raf = 0;
1815
+ }
1816
+ const viewer = viewerRef.current;
1817
+ const el = pdfViewerEl();
1818
+ if (el) {
1819
+ el.style.transform = "";
1820
+ el.style.transformOrigin = "";
1821
+ el.style.willChange = "";
1822
+ }
1823
+ if (!viewer) return;
1824
+ const finalScale = Math.min(
1825
+ MAX_SCALE,
1826
+ Math.max(MIN_SCALE, g.startScale * g.k)
1827
+ );
1828
+ const ratio = finalScale / g.startScale;
1829
+ viewer.currentScaleValue = String(finalScale);
1830
+ container.scrollLeft = g.originX * ratio - g.anchorX;
1831
+ container.scrollTop = g.originY * ratio - g.anchorY;
1832
+ onZoomChangeRef.current?.(finalScale);
1833
+ console.log("[PdfHighlighter] pinch commit", {
1834
+ from: g.startScale,
1835
+ to: finalScale
1836
+ });
1837
+ };
1838
+ const scheduleCommit = () => {
1839
+ clearTimeout(g.commitTimer);
1840
+ g.commitTimer = setTimeout(commit, COMMIT_DELAY);
1841
+ };
1842
+ const handleWheel = (e) => {
1843
+ if (!e.ctrlKey && !e.metaKey) return;
1844
+ e.preventDefault();
1845
+ const rect = container.getBoundingClientRect();
1846
+ const ax = e.clientX - rect.left;
1847
+ const ay = e.clientY - rect.top;
1848
+ if (!g.active && !begin(ax, ay)) return;
1849
+ g.k = clampK(g.k * Math.exp(-e.deltaY * 0.01));
1850
+ preview();
1851
+ scheduleCommit();
1852
+ };
1853
+ container.addEventListener("wheel", handleWheel, { passive: false });
1854
+ const pointers = /* @__PURE__ */ new Map();
1855
+ let startDist = 0;
1856
+ const spread = () => {
1857
+ const [a, b] = [...pointers.values()];
1858
+ return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
1859
+ };
1860
+ const onPointerDown = (e) => {
1861
+ if (e.pointerType === "touch") pointers.set(e.pointerId, e);
1862
+ };
1863
+ const onPointerMove = (e) => {
1864
+ if (!pointers.has(e.pointerId)) return;
1865
+ pointers.set(e.pointerId, e);
1866
+ if (pointers.size !== 2) return;
1867
+ e.preventDefault();
1868
+ const d = spread();
1869
+ const [a, b] = [...pointers.values()];
1870
+ const rect = container.getBoundingClientRect();
1871
+ const cx = (a.clientX + b.clientX) / 2 - rect.left;
1872
+ const cy = (a.clientY + b.clientY) / 2 - rect.top;
1873
+ if (!g.active) {
1874
+ startDist = d;
1875
+ if (!begin(cx, cy)) return;
1876
+ return;
1877
+ }
1878
+ g.k = clampK(d / startDist);
1879
+ preview();
1880
+ };
1881
+ const onPointerUp = (e) => {
1882
+ pointers.delete(e.pointerId);
1883
+ if (pointers.size < 2 && g.active) {
1884
+ startDist = 0;
1885
+ commit();
1886
+ }
1887
+ };
1888
+ container.addEventListener("pointerdown", onPointerDown);
1889
+ container.addEventListener("pointermove", onPointerMove, { passive: false });
1890
+ container.addEventListener("pointerup", onPointerUp);
1891
+ container.addEventListener("pointercancel", onPointerUp);
1892
+ return () => {
1893
+ container.removeEventListener("wheel", handleWheel);
1894
+ container.removeEventListener("pointerdown", onPointerDown);
1895
+ container.removeEventListener("pointermove", onPointerMove);
1896
+ container.removeEventListener("pointerup", onPointerUp);
1897
+ container.removeEventListener("pointercancel", onPointerUp);
1898
+ clearTimeout(g.commitTimer);
1899
+ if (g.raf) cancelAnimationFrame(g.raf);
1900
+ const el = pdfViewerEl();
1901
+ if (el) {
1902
+ el.style.transform = "";
1903
+ el.style.transformOrigin = "";
1904
+ el.style.willChange = "";
1905
+ }
1906
+ };
1907
+ }, [isViewerReady]);
1244
1908
  const handleScroll = () => {
1245
1909
  onScrollAway && onScrollAway();
1246
1910
  scrolledToHighlightIdRef.current = null;
@@ -1358,26 +2022,46 @@ var PdfHighlighter = ({
1358
2022
  setTip(null);
1359
2023
  }
1360
2024
  };
2025
+ const handleCopy = (event) => {
2026
+ const container = containerNodeRef.current;
2027
+ if (!container || !event.clipboardData) return;
2028
+ const target = event.target;
2029
+ const targetElement = target instanceof HTMLElement ? target : target instanceof Node ? target.parentElement : null;
2030
+ if (targetElement && (targetElement.closest("input, textarea, [contenteditable='true']") || targetElement.closest(".PdfHighlighter__tip-container"))) {
2031
+ return;
2032
+ }
2033
+ const selection = getWindow(container).getSelection();
2034
+ const range = selection?.rangeCount ? selection.getRangeAt(0) : null;
2035
+ if (!selection || selection.isCollapsed || !range || !container.contains(range.commonAncestorContainer)) {
2036
+ return;
2037
+ }
2038
+ const text = selectionRef.current?.content.text?.trim() || selection.toString().split("\n").join(" ").trim();
2039
+ if (!text) return;
2040
+ event.clipboardData.setData("text/plain", text);
2041
+ event.preventDefault();
2042
+ event.stopPropagation();
2043
+ };
1361
2044
  const handleScaleValue = () => {
1362
2045
  if (viewerRef.current) {
1363
2046
  viewerRef.current.currentScaleValue = pdfScaleValue.toString();
1364
2047
  }
1365
2048
  };
1366
- const renderHighlightLayer = (highlightBindings, pageNumber) => {
2049
+ const renderHighlightLayer = (highlightBindings, pageNumber, shouldRenderHighlight) => {
1367
2050
  if (!viewerRef.current) return;
1368
2051
  highlightBindings.reactRoot.render(
1369
2052
  /* @__PURE__ */ React6.createElement(PdfHighlighterContext.Provider, { value: pdfHighlighterUtils }, /* @__PURE__ */ React6.createElement(
1370
2053
  HighlightLayer,
1371
2054
  {
1372
2055
  highlightsByPage: group_highlights_by_page_default([
1373
- ...highlights,
2056
+ ...highlightsRef.current,
1374
2057
  ghostHighlightRef.current
1375
2058
  ]),
1376
2059
  pageNumber,
1377
2060
  scrolledToHighlightId: scrolledToHighlightIdRef.current,
1378
2061
  viewer: viewerRef.current,
1379
2062
  highlightBindings,
1380
- children
2063
+ shouldRenderHighlight,
2064
+ children: childrenRef.current
1381
2065
  }
1382
2066
  ))
1383
2067
  );
@@ -1385,31 +2069,52 @@ var PdfHighlighter = ({
1385
2069
  const renderHighlightLayers = () => {
1386
2070
  if (!viewerRef.current) return;
1387
2071
  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
2072
+ const { textLayer } = viewerRef.current.getPageView(pageNumber - 1) || {};
2073
+ if (!textLayer) continue;
2074
+ const textLayerDiv = textLayer.div;
2075
+ const highlightBindings = ensurePersistentLayer(
2076
+ highlightBindingsRef.current,
2077
+ pageNumber,
2078
+ textLayerDiv,
2079
+ "PdfHighlighter__highlight-layer"
2080
+ );
2081
+ renderHighlightLayer(
2082
+ highlightBindings,
2083
+ pageNumber,
2084
+ (highlight) => !isFreetextHighlight(highlight)
2085
+ );
2086
+ const pageEl = textLayerDiv.closest(".page");
2087
+ if (pageEl) {
2088
+ const noteBindings = ensurePersistentLayer(
2089
+ noteBindingsRef.current,
2090
+ pageNumber,
2091
+ pageEl,
2092
+ "PdfHighlighter__note-layer"
1396
2093
  );
1397
- if (highlightLayer) {
1398
- const reactRoot = createRoot(highlightLayer);
1399
- highlightBindingsRef.current[pageNumber] = {
1400
- reactRoot,
1401
- container: highlightLayer,
1402
- textLayer: textLayer.div
1403
- // textLayer.div for version >=3.0 and textLayer.textLayerDiv otherwise.
1404
- };
1405
- renderHighlightLayer(
1406
- highlightBindingsRef.current[pageNumber],
1407
- pageNumber
1408
- );
1409
- }
2094
+ renderHighlightLayer(noteBindings, pageNumber, isFreetextHighlight);
1410
2095
  }
1411
2096
  }
1412
2097
  };
2098
+ const scheduleRenderHighlightLayers = () => {
2099
+ renderHighlightLayers();
2100
+ renderRetryTimeoutsRef.current.forEach(clearTimeout);
2101
+ renderRetryTimeoutsRef.current = [50, 150, 350, 750, 1200].map(
2102
+ (delay) => setTimeout(renderHighlightLayers, delay)
2103
+ );
2104
+ };
2105
+ const resumeScrollAwayListenerAfterNavigation = () => {
2106
+ const container = viewerRef.current?.container;
2107
+ if (!container) return;
2108
+ if (resumeScrollAwayTimeoutRef.current) {
2109
+ clearTimeout(resumeScrollAwayTimeoutRef.current);
2110
+ }
2111
+ resumeScrollAwayTimeoutRef.current = setTimeout(() => {
2112
+ container.addEventListener("scroll", handleScroll, {
2113
+ once: true
2114
+ });
2115
+ resumeScrollAwayTimeoutRef.current = null;
2116
+ }, 1200);
2117
+ };
1413
2118
  const isEditingOrHighlighting = () => {
1414
2119
  return Boolean(selectionRef.current) || Boolean(ghostHighlightRef.current) || isAreaSelectionInProgressRef.current || isEditInProgressRef.current;
1415
2120
  };
@@ -1441,32 +2146,72 @@ var PdfHighlighter = ({
1441
2146
  const scrollToHighlight = (highlight) => {
1442
2147
  const { boundingRect, usePdfCoordinates } = highlight.position;
1443
2148
  const pageNumber = boundingRect.pageNumber;
1444
- viewerRef.current.container.removeEventListener("scroll", handleScroll);
1445
- const pageViewport = viewerRef.current.getPageView(
1446
- pageNumber - 1
1447
- ).viewport;
1448
- viewerRef.current.scrollPageIntoView({
1449
- pageNumber,
1450
- destArray: [
1451
- null,
1452
- // null since we pass pageNumber already as an arg
1453
- { name: "XYZ" },
1454
- ...pageViewport.convertToPdfPoint(
1455
- 0,
1456
- // Default x coord
1457
- scaledToViewport(boundingRect, pageViewport, usePdfCoordinates).top - SCROLL_MARGIN
1458
- ),
1459
- 0
1460
- // Default z coord
1461
- ]
1462
- });
1463
- scrolledToHighlightIdRef.current = highlight.id;
1464
- renderHighlightLayers();
1465
- setTimeout(() => {
1466
- viewerRef.current.container.addEventListener("scroll", handleScroll, {
1467
- once: true
2149
+ const viewer = viewerRef.current;
2150
+ if (!viewer) return;
2151
+ viewer.container.removeEventListener("scroll", handleScroll);
2152
+ const pageView = viewer.getPageView(pageNumber - 1);
2153
+ const container = viewer.container;
2154
+ if (pageView?.div && pageView.viewport) {
2155
+ const topInPage = scaledToViewport(
2156
+ boundingRect,
2157
+ pageView.viewport,
2158
+ usePdfCoordinates
2159
+ ).top;
2160
+ const pageRect = pageView.div.getBoundingClientRect();
2161
+ const containerRect = container.getBoundingClientRect();
2162
+ const target = container.scrollTop + (pageRect.top - containerRect.top) + topInPage - SCROLL_MARGIN;
2163
+ const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
2164
+ container.scrollTo({
2165
+ top: Math.max(0, target),
2166
+ behavior: prefersReducedMotion ? "auto" : "smooth"
1468
2167
  });
1469
- }, 100);
2168
+ } else {
2169
+ viewer.scrollPageIntoView({ pageNumber });
2170
+ }
2171
+ scrolledToHighlightIdRef.current = highlight.id;
2172
+ scheduleRenderHighlightLayers();
2173
+ resumeScrollAwayListenerAfterNavigation();
2174
+ };
2175
+ const dispatchFind = (query, findPrevious2, options = {}, type) => {
2176
+ eventBusRef.current.dispatch("find", {
2177
+ source: findControllerRef.current || viewerRef.current,
2178
+ type,
2179
+ query,
2180
+ phraseSearch: true,
2181
+ caseSensitive: options.caseSensitive ?? false,
2182
+ entireWord: options.entireWord ?? false,
2183
+ highlightAll: options.highlightAll ?? true,
2184
+ findPrevious: findPrevious2,
2185
+ matchDiacritics: options.matchDiacritics ?? false
2186
+ });
2187
+ };
2188
+ const currentSearchRef = useRef5({
2189
+ query: "",
2190
+ options: {}
2191
+ });
2192
+ const search = (query, options = {}) => {
2193
+ currentSearchRef.current = { query, options };
2194
+ if (!query.trim()) {
2195
+ clearSearch();
2196
+ return;
2197
+ }
2198
+ dispatchFind(query, false, options);
2199
+ };
2200
+ const findNext = () => {
2201
+ const { query, options } = currentSearchRef.current;
2202
+ if (!query.trim()) return;
2203
+ dispatchFind(query, false, options, "again");
2204
+ };
2205
+ const findPrevious = () => {
2206
+ const { query, options } = currentSearchRef.current;
2207
+ if (!query.trim()) return;
2208
+ dispatchFind(query, true, options, "again");
2209
+ };
2210
+ const clearSearch = () => {
2211
+ currentSearchRef.current = { query: "", options: {} };
2212
+ eventBusRef.current.dispatch("findbarclose", {
2213
+ source: findControllerRef.current || viewerRef.current
2214
+ });
1470
2215
  };
1471
2216
  const pdfHighlighterUtils = {
1472
2217
  isEditingOrHighlighting,
@@ -1483,6 +2228,10 @@ var PdfHighlighter = ({
1483
2228
  updateTipPosition: updateTipPositionRef.current,
1484
2229
  getLinkService: () => linkServiceRef.current,
1485
2230
  getEventBus: () => eventBusRef.current,
2231
+ search,
2232
+ findNext,
2233
+ findPrevious,
2234
+ clearSearch,
1486
2235
  goToPage: (pageNumber) => {
1487
2236
  console.log("[PdfHighlighter] goToPage called with page:", pageNumber);
1488
2237
  const viewer = viewerRef.current;
@@ -1584,14 +2333,6 @@ var PdfHighlighter = ({
1584
2333
  .PdfHighlighter::-webkit-scrollbar-track-piece {
1585
2334
  background-color: ${resolvedTheme.scrollbarTrackColor};
1586
2335
  }
1587
- ${resolvedTheme.mode === "dark" ? `
1588
- .PdfHighlighter--dark .page {
1589
- filter: invert(${resolvedTheme.darkModeInvertIntensity}) hue-rotate(180deg) brightness(1.05);
1590
- }
1591
- .PdfHighlighter--dark .PdfHighlighter__highlight-layer {
1592
- filter: invert(${resolvedTheme.darkModeInvertIntensity}) hue-rotate(180deg) brightness(0.95);
1593
- }
1594
- ` : ""}
1595
2336
  `),
1596
2337
  isViewerReady && /* @__PURE__ */ React6.createElement(
1597
2338
  TipContainer,
@@ -1676,10 +2417,78 @@ var PdfHighlighter = ({
1676
2417
  import React7, {
1677
2418
  useState as useState6,
1678
2419
  useRef as useRef6,
1679
- useEffect as useEffect5
2420
+ useEffect as useEffect5,
2421
+ useLayoutEffect as useLayoutEffect3
1680
2422
  } from "react";
2423
+ import { createPortal } from "react-dom";
2424
+
2425
+ // src/lib/copy-highlight-content.ts
2426
+ 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;
2427
+ var copyTextToClipboard = async (text) => {
2428
+ if (!text) return;
2429
+ if (navigator.clipboard?.writeText) {
2430
+ await navigator.clipboard.writeText(text);
2431
+ return;
2432
+ }
2433
+ const textarea = document.createElement("textarea");
2434
+ textarea.value = text;
2435
+ textarea.style.position = "fixed";
2436
+ textarea.style.opacity = "0";
2437
+ document.body.appendChild(textarea);
2438
+ textarea.select();
2439
+ document.execCommand("copy");
2440
+ textarea.remove();
2441
+ };
2442
+ var extractTextFromHighlightRect = (anchor, rect) => {
2443
+ const page = anchor.closest(".page");
2444
+ const textLayer = page?.querySelector(".textLayer");
2445
+ if (!page || !textLayer) return "";
2446
+ const pageRect = page.getBoundingClientRect();
2447
+ const targetRect = {
2448
+ left: pageRect.left + rect.left,
2449
+ top: pageRect.top + rect.top,
2450
+ width: rect.width,
2451
+ height: rect.height
2452
+ };
2453
+ const matches = Array.from(textLayer.querySelectorAll("span")).map((span) => {
2454
+ const spanRect = span.getBoundingClientRect();
2455
+ return {
2456
+ text: span.textContent || "",
2457
+ rect: {
2458
+ left: spanRect.left,
2459
+ top: spanRect.top,
2460
+ width: spanRect.width,
2461
+ height: spanRect.height
2462
+ }
2463
+ };
2464
+ }).filter(({ text, rect: rect2 }) => text.trim() && intersects(rect2, targetRect)).sort((a, b) => {
2465
+ const lineDelta = a.rect.top - b.rect.top;
2466
+ return Math.abs(lineDelta) > 4 ? lineDelta : a.rect.left - b.rect.left;
2467
+ });
2468
+ return matches.map(({ text }) => text).join(" ").replace(/\s+/g, " ").trim();
2469
+ };
2470
+
2471
+ // src/lib/highlight-config-layer.ts
2472
+ var findOrCreateHighlightConfigLayer = (anchor) => {
2473
+ const pageLayer = anchor.closest(".page");
2474
+ if (!pageLayer) return null;
2475
+ const doc = getDocument(pageLayer);
2476
+ let layer = Array.from(pageLayer.children).find(
2477
+ (child) => child.classList.contains("PdfHighlighter__config-layer")
2478
+ );
2479
+ if (!layer) {
2480
+ layer = doc.createElement("div");
2481
+ layer.className = "PdfHighlighter__config-layer";
2482
+ pageLayer.appendChild(layer);
2483
+ }
2484
+ return layer;
2485
+ };
2486
+
2487
+ // src/components/TextHighlight.tsx
1681
2488
  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
2489
  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" }));
2490
+ 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" }));
2491
+ 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
2492
  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
2493
  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
2494
  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 +2518,28 @@ var TextHighlight = ({
1709
2518
  onDelete,
1710
2519
  styleIcon,
1711
2520
  deleteIcon,
2521
+ copyText,
1712
2522
  colorPresets = DEFAULT_COLOR_PRESETS
1713
2523
  }) => {
1714
2524
  const [isStylePanelOpen, setIsStylePanelOpen] = useState6(false);
1715
2525
  const [isHovered, setIsHovered] = useState6(false);
2526
+ const [isCopied, setIsCopied] = useState6(false);
2527
+ const [configLayer, setConfigLayer] = useState6(null);
1716
2528
  const stylePanelRef = useRef6(null);
1717
2529
  const containerRef = useRef6(null);
2530
+ const copyResetTimeoutRef = useRef6(null);
2531
+ useLayoutEffect3(() => {
2532
+ if (containerRef.current) {
2533
+ setConfigLayer(findOrCreateHighlightConfigLayer(containerRef.current));
2534
+ }
2535
+ }, []);
2536
+ useEffect5(() => {
2537
+ return () => {
2538
+ if (copyResetTimeoutRef.current) {
2539
+ window.clearTimeout(copyResetTimeoutRef.current);
2540
+ }
2541
+ };
2542
+ }, []);
1718
2543
  useEffect5(() => {
1719
2544
  if (!isStylePanelOpen) return;
1720
2545
  const handleClickOutside = (e) => {
@@ -1746,13 +2571,26 @@ var TextHighlight = ({
1746
2571
  const getPartStyle = (rect) => {
1747
2572
  const baseStyle = { ...rect, ...style };
1748
2573
  if (highlightStyle === "highlight") {
1749
- baseStyle.backgroundColor = highlightColor;
2574
+ baseStyle.backgroundColor = `color-mix(in srgb, ${highlightColor} var(--hl-fill-alpha, 100%), transparent)`;
1750
2575
  } else {
1751
2576
  baseStyle.backgroundColor = "transparent";
1752
2577
  baseStyle.color = highlightColor;
1753
2578
  }
1754
2579
  return baseStyle;
1755
2580
  };
2581
+ const handleCopy = async (event) => {
2582
+ event.stopPropagation();
2583
+ const text = copyText || highlight.content?.text || (containerRef.current && firstRect ? extractTextFromHighlightRect(containerRef.current, firstRect) : "");
2584
+ await copyTextToClipboard(text);
2585
+ setIsCopied(true);
2586
+ if (copyResetTimeoutRef.current) {
2587
+ window.clearTimeout(copyResetTimeoutRef.current);
2588
+ }
2589
+ copyResetTimeoutRef.current = window.setTimeout(() => {
2590
+ setIsCopied(false);
2591
+ copyResetTimeoutRef.current = null;
2592
+ }, 1500);
2593
+ };
1756
2594
  return /* @__PURE__ */ React7.createElement(
1757
2595
  "div",
1758
2596
  {
@@ -1760,107 +2598,120 @@ var TextHighlight = ({
1760
2598
  onContextMenu,
1761
2599
  ref: containerRef
1762
2600
  },
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
- },
2601
+ configLayer && (onStyleChange || onDelete) && firstRect && createPortal(
1776
2602
  /* @__PURE__ */ React7.createElement(
1777
2603
  "div",
1778
2604
  {
1779
- className: `TextHighlight__toolbar ${isHovered || isStylePanelOpen ? "TextHighlight__toolbar--visible" : ""}`
2605
+ className: "TextHighlight__toolbar-wrapper",
2606
+ style: {
2607
+ position: "absolute",
2608
+ left: firstRect.left,
2609
+ top: firstRect.top - 28,
2610
+ paddingBottom: 12
2611
+ },
2612
+ onMouseEnter: () => setIsHovered(true),
2613
+ onMouseLeave: () => setIsHovered(false)
1780
2614
  },
1781
- onStyleChange && /* @__PURE__ */ React7.createElement(
1782
- "button",
2615
+ /* @__PURE__ */ React7.createElement(
2616
+ "div",
1783
2617
  {
1784
- className: "TextHighlight__style-button",
1785
- onClick: (e) => {
1786
- e.stopPropagation();
1787
- setIsStylePanelOpen(!isStylePanelOpen);
1788
- },
1789
- title: "Change style",
1790
- type: "button"
2618
+ className: `TextHighlight__toolbar ${isHovered || isScrolledTo || isStylePanelOpen ? "TextHighlight__toolbar--visible" : ""}`
1791
2619
  },
1792
- styleIcon || /* @__PURE__ */ React7.createElement(DefaultStyleIcon, null)
2620
+ onStyleChange && /* @__PURE__ */ React7.createElement(
2621
+ "button",
2622
+ {
2623
+ className: "TextHighlight__style-button",
2624
+ onClick: (e) => {
2625
+ e.stopPropagation();
2626
+ setIsStylePanelOpen(!isStylePanelOpen);
2627
+ },
2628
+ title: "Change style",
2629
+ type: "button"
2630
+ },
2631
+ styleIcon || /* @__PURE__ */ React7.createElement(DefaultStyleIcon, null)
2632
+ ),
2633
+ /* @__PURE__ */ React7.createElement(
2634
+ "button",
2635
+ {
2636
+ className: "TextHighlight__copy-button",
2637
+ onClick: handleCopy,
2638
+ title: isCopied ? "Copied" : "Copy text",
2639
+ type: "button"
2640
+ },
2641
+ isCopied ? /* @__PURE__ */ React7.createElement(DefaultCopiedIcon, null) : /* @__PURE__ */ React7.createElement(DefaultCopyIcon, null)
2642
+ ),
2643
+ onDelete && /* @__PURE__ */ React7.createElement(
2644
+ "button",
2645
+ {
2646
+ className: "TextHighlight__delete-button",
2647
+ onClick: (e) => {
2648
+ e.stopPropagation();
2649
+ onDelete();
2650
+ },
2651
+ title: "Delete",
2652
+ type: "button"
2653
+ },
2654
+ deleteIcon || /* @__PURE__ */ React7.createElement(DefaultDeleteIcon, null)
2655
+ )
1793
2656
  ),
1794
- onDelete && /* @__PURE__ */ React7.createElement(
1795
- "button",
2657
+ isStylePanelOpen && onStyleChange && /* @__PURE__ */ React7.createElement(
2658
+ "div",
1796
2659
  {
1797
- className: "TextHighlight__delete-button",
1798
- onClick: (e) => {
1799
- e.stopPropagation();
1800
- onDelete();
1801
- },
1802
- title: "Delete",
1803
- type: "button"
2660
+ className: "TextHighlight__style-panel",
2661
+ ref: stylePanelRef,
2662
+ onClick: (e) => e.stopPropagation()
1804
2663
  },
1805
- deleteIcon || /* @__PURE__ */ React7.createElement(DefaultDeleteIcon, null)
1806
- )
1807
- ),
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 });
2664
+ /* @__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(
2665
+ "button",
2666
+ {
2667
+ type: "button",
2668
+ className: `TextHighlight__style-type-button ${highlightStyle === "highlight" ? "active" : ""}`,
2669
+ onClick: () => onStyleChange({ highlightStyle: "highlight" }),
2670
+ title: "Highlight"
2671
+ },
2672
+ /* @__PURE__ */ React7.createElement(HighlightIcon, null)
2673
+ ), /* @__PURE__ */ React7.createElement(
2674
+ "button",
2675
+ {
2676
+ type: "button",
2677
+ className: `TextHighlight__style-type-button ${highlightStyle === "underline" ? "active" : ""}`,
2678
+ onClick: () => onStyleChange({ highlightStyle: "underline" }),
2679
+ title: "Underline"
2680
+ },
2681
+ /* @__PURE__ */ React7.createElement(UnderlineIcon, null)
2682
+ ), /* @__PURE__ */ React7.createElement(
2683
+ "button",
2684
+ {
2685
+ type: "button",
2686
+ className: `TextHighlight__style-type-button ${highlightStyle === "strikethrough" ? "active" : ""}`,
2687
+ onClick: () => onStyleChange({ highlightStyle: "strikethrough" }),
2688
+ title: "Strikethrough"
2689
+ },
2690
+ /* @__PURE__ */ React7.createElement(StrikethroughIcon, null)
2691
+ ))),
2692
+ /* @__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(
2693
+ "button",
2694
+ {
2695
+ key: c,
2696
+ type: "button",
2697
+ className: `TextHighlight__color-preset ${highlightColor === c ? "active" : ""}`,
2698
+ style: { backgroundColor: c },
2699
+ onClick: () => onStyleChange({ highlightColor: c }),
2700
+ title: c
1860
2701
  }
1861
- }
1862
- )))
1863
- )
2702
+ ))), /* @__PURE__ */ React7.createElement(
2703
+ "input",
2704
+ {
2705
+ type: "color",
2706
+ value: highlightColor,
2707
+ onChange: (e) => {
2708
+ onStyleChange({ highlightColor: e.target.value });
2709
+ }
2710
+ }
2711
+ )))
2712
+ )
2713
+ ),
2714
+ configLayer
1864
2715
  ),
1865
2716
  /* @__PURE__ */ React7.createElement(
1866
2717
  "div",
@@ -1966,11 +2817,15 @@ var MonitoredHighlightContainer = ({
1966
2817
  import React10, {
1967
2818
  useState as useState7,
1968
2819
  useRef as useRef9,
1969
- useEffect as useEffect7
2820
+ useEffect as useEffect7,
2821
+ useLayoutEffect as useLayoutEffect4
1970
2822
  } from "react";
2823
+ import { createPortal as createPortal2 } from "react-dom";
1971
2824
  import { Rnd } from "react-rnd";
1972
2825
  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
2826
  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" }));
2827
+ 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" }));
2828
+ 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
2829
  var DEFAULT_COLOR_PRESETS2 = [
1975
2830
  "rgba(255, 226, 143, 1)",
1976
2831
  // Yellow (default)
@@ -1996,11 +2851,28 @@ var AreaHighlight = ({
1996
2851
  onDelete,
1997
2852
  styleIcon,
1998
2853
  deleteIcon,
2854
+ copyText,
1999
2855
  colorPresets = DEFAULT_COLOR_PRESETS2
2000
2856
  }) => {
2001
2857
  const [isStylePanelOpen, setIsStylePanelOpen] = useState7(false);
2002
2858
  const [isHovered, setIsHovered] = useState7(false);
2859
+ const [isCopied, setIsCopied] = useState7(false);
2860
+ const [configLayer, setConfigLayer] = useState7(null);
2003
2861
  const stylePanelRef = useRef9(null);
2862
+ const containerRef = useRef9(null);
2863
+ const copyResetTimeoutRef = useRef9(null);
2864
+ useLayoutEffect4(() => {
2865
+ if (containerRef.current) {
2866
+ setConfigLayer(findOrCreateHighlightConfigLayer(containerRef.current));
2867
+ }
2868
+ }, []);
2869
+ useEffect7(() => {
2870
+ return () => {
2871
+ if (copyResetTimeoutRef.current) {
2872
+ window.clearTimeout(copyResetTimeoutRef.current);
2873
+ }
2874
+ };
2875
+ }, []);
2004
2876
  useEffect7(() => {
2005
2877
  if (!isStylePanelOpen) return;
2006
2878
  const handleClickOutside = (e) => {
@@ -2020,87 +2892,117 @@ var AreaHighlight = ({
2020
2892
  const key = `${highlight.position.boundingRect.width}${highlight.position.boundingRect.height}${highlight.position.boundingRect.left}${highlight.position.boundingRect.top}`;
2021
2893
  const mergedStyle = {
2022
2894
  ...style,
2023
- backgroundColor: highlightColor
2895
+ backgroundColor: `color-mix(in srgb, ${highlightColor} var(--hl-fill-alpha, 100%), transparent)`
2896
+ };
2897
+ const handleCopy = async (event) => {
2898
+ event.stopPropagation();
2899
+ const text = copyText || highlight.content?.text || (containerRef.current ? extractTextFromHighlightRect(
2900
+ containerRef.current,
2901
+ highlight.position.boundingRect
2902
+ ) : "");
2903
+ await copyTextToClipboard(text);
2904
+ setIsCopied(true);
2905
+ if (copyResetTimeoutRef.current) {
2906
+ window.clearTimeout(copyResetTimeoutRef.current);
2907
+ }
2908
+ copyResetTimeoutRef.current = window.setTimeout(() => {
2909
+ setIsCopied(false);
2910
+ copyResetTimeoutRef.current = null;
2911
+ }, 1500);
2024
2912
  };
2025
2913
  return /* @__PURE__ */ React10.createElement(
2026
2914
  "div",
2027
2915
  {
2028
2916
  className: `AreaHighlight ${highlightClass}`,
2029
- onContextMenu
2917
+ onContextMenu,
2918
+ ref: containerRef
2030
2919
  },
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
- },
2920
+ configLayer && (onStyleChange || onDelete) && createPortal2(
2044
2921
  /* @__PURE__ */ React10.createElement(
2045
2922
  "div",
2046
2923
  {
2047
- className: `AreaHighlight__toolbar ${isHovered || isStylePanelOpen ? "AreaHighlight__toolbar--visible" : ""}`
2924
+ className: "AreaHighlight__toolbar-wrapper",
2925
+ style: {
2926
+ position: "absolute",
2927
+ left: highlight.position.boundingRect.left,
2928
+ top: highlight.position.boundingRect.top - 28,
2929
+ paddingBottom: 12
2930
+ },
2931
+ onMouseEnter: () => setIsHovered(true),
2932
+ onMouseLeave: () => setIsHovered(false)
2048
2933
  },
2049
- onStyleChange && /* @__PURE__ */ React10.createElement(
2050
- "button",
2934
+ /* @__PURE__ */ React10.createElement(
2935
+ "div",
2051
2936
  {
2052
- className: "AreaHighlight__style-button",
2053
- onClick: (e) => {
2054
- e.stopPropagation();
2055
- setIsStylePanelOpen(!isStylePanelOpen);
2056
- },
2057
- title: "Change color",
2058
- type: "button"
2937
+ className: `AreaHighlight__toolbar ${isHovered || isScrolledTo || isStylePanelOpen ? "AreaHighlight__toolbar--visible" : ""}`
2059
2938
  },
2060
- styleIcon || /* @__PURE__ */ React10.createElement(DefaultStyleIcon2, null)
2939
+ onStyleChange && /* @__PURE__ */ React10.createElement(
2940
+ "button",
2941
+ {
2942
+ className: "AreaHighlight__style-button",
2943
+ onClick: (e) => {
2944
+ e.stopPropagation();
2945
+ setIsStylePanelOpen(!isStylePanelOpen);
2946
+ },
2947
+ title: "Change color",
2948
+ type: "button"
2949
+ },
2950
+ styleIcon || /* @__PURE__ */ React10.createElement(DefaultStyleIcon2, null)
2951
+ ),
2952
+ /* @__PURE__ */ React10.createElement(
2953
+ "button",
2954
+ {
2955
+ className: "AreaHighlight__copy-button",
2956
+ onClick: handleCopy,
2957
+ title: isCopied ? "Copied" : "Copy text",
2958
+ type: "button"
2959
+ },
2960
+ isCopied ? /* @__PURE__ */ React10.createElement(DefaultCopiedIcon2, null) : /* @__PURE__ */ React10.createElement(DefaultCopyIcon2, null)
2961
+ ),
2962
+ onDelete && /* @__PURE__ */ React10.createElement(
2963
+ "button",
2964
+ {
2965
+ className: "AreaHighlight__delete-button",
2966
+ onClick: (e) => {
2967
+ e.stopPropagation();
2968
+ onDelete();
2969
+ },
2970
+ title: "Delete",
2971
+ type: "button"
2972
+ },
2973
+ deleteIcon || /* @__PURE__ */ React10.createElement(DefaultDeleteIcon2, null)
2974
+ )
2061
2975
  ),
2062
- onDelete && /* @__PURE__ */ React10.createElement(
2063
- "button",
2976
+ isStylePanelOpen && onStyleChange && /* @__PURE__ */ React10.createElement(
2977
+ "div",
2064
2978
  {
2065
- className: "AreaHighlight__delete-button",
2066
- onClick: (e) => {
2067
- e.stopPropagation();
2068
- onDelete();
2069
- },
2070
- title: "Delete",
2071
- type: "button"
2979
+ className: "AreaHighlight__style-panel",
2980
+ ref: stylePanelRef,
2981
+ onClick: (e) => e.stopPropagation()
2072
2982
  },
2073
- deleteIcon || /* @__PURE__ */ React10.createElement(DefaultDeleteIcon2, null)
2983
+ /* @__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(
2984
+ "button",
2985
+ {
2986
+ key: c,
2987
+ type: "button",
2988
+ className: `AreaHighlight__color-preset ${highlightColor === c ? "active" : ""}`,
2989
+ style: { backgroundColor: c },
2990
+ onClick: () => onStyleChange({ highlightColor: c }),
2991
+ title: c
2992
+ }
2993
+ ))), /* @__PURE__ */ React10.createElement(
2994
+ "input",
2995
+ {
2996
+ type: "color",
2997
+ value: highlightColor,
2998
+ onChange: (e) => {
2999
+ onStyleChange({ highlightColor: e.target.value });
3000
+ }
3001
+ }
3002
+ )))
2074
3003
  )
2075
3004
  ),
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
- )
3005
+ configLayer
2104
3006
  ),
2105
3007
  /* @__PURE__ */ React10.createElement(
2106
3008
  Rnd,
@@ -2157,6 +3059,8 @@ var DefaultDragIcon = () => /* @__PURE__ */ React11.createElement("svg", { width
2157
3059
  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
3060
  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
3061
  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" }));
3062
+ 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" }));
3063
+ 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
3064
  var DEFAULT_BACKGROUND_PRESETS = ["transparent", "#ffffc8", "#ffcdd2", "#c8e6c9", "#bbdefb", "#e1bee7"];
2161
3065
  var DEFAULT_TEXT_PRESETS = ["#333333", "#d32f2f", "#1976d2", "#388e3c", "#7b1fa2"];
2162
3066
  var FreetextHighlight = ({
@@ -2180,9 +3084,13 @@ var FreetextHighlight = ({
2180
3084
  backgroundColorPresets = DEFAULT_BACKGROUND_PRESETS,
2181
3085
  textColorPresets = DEFAULT_TEXT_PRESETS,
2182
3086
  onDelete,
2183
- deleteIcon
3087
+ deleteIcon,
3088
+ compact = false,
3089
+ compactSize = 32,
3090
+ compactIcon
2184
3091
  }) => {
2185
3092
  const [isEditing, setIsEditing] = useState8(false);
3093
+ const [isExpanded, setIsExpanded] = useState8(!compact);
2186
3094
  const [isStylePanelOpen, setIsStylePanelOpen] = useState8(false);
2187
3095
  const [text, setText] = useState8(highlight.content?.text || "");
2188
3096
  const textareaRef = useRef10(null);
@@ -2190,6 +3098,13 @@ var FreetextHighlight = ({
2190
3098
  useEffect8(() => {
2191
3099
  setText(highlight.content?.text || "");
2192
3100
  }, [highlight.content?.text]);
3101
+ useEffect8(() => {
3102
+ setIsExpanded(!compact);
3103
+ setIsStylePanelOpen(false);
3104
+ if (!compact) {
3105
+ setIsEditing(false);
3106
+ }
3107
+ }, [compact]);
2193
3108
  useEffect8(() => {
2194
3109
  if (isEditing && textareaRef.current) {
2195
3110
  textareaRef.current.focus();
@@ -2213,10 +3128,14 @@ var FreetextHighlight = ({
2213
3128
  }, [isStylePanelOpen]);
2214
3129
  const highlightClass = isScrolledTo ? "FreetextHighlight--scrolledTo" : "";
2215
3130
  const editingClass = isEditing ? "FreetextHighlight--editing" : "";
2216
- const key = `${highlight.position.boundingRect.width}${highlight.position.boundingRect.height}${highlight.position.boundingRect.left}${highlight.position.boundingRect.top}`;
3131
+ const compactClass = compact ? "FreetextHighlight--compact" : "";
3132
+ const isCompactCollapsed = compact && !isExpanded && !isEditing && !isStylePanelOpen;
3133
+ const collapsedClass = isCompactCollapsed ? "FreetextHighlight--collapsed" : "";
3134
+ const key = `${highlight.position.boundingRect.width}${highlight.position.boundingRect.height}${highlight.position.boundingRect.left}${highlight.position.boundingRect.top}${isCompactCollapsed ? "collapsed" : "expanded"}`;
2217
3135
  const handleTextClick = (e) => {
2218
3136
  e.stopPropagation();
2219
3137
  if (!isEditing) {
3138
+ setIsExpanded(true);
2220
3139
  setIsEditing(true);
2221
3140
  onEditStart?.();
2222
3141
  }
@@ -2251,7 +3170,7 @@ var FreetextHighlight = ({
2251
3170
  return /* @__PURE__ */ React11.createElement(
2252
3171
  "div",
2253
3172
  {
2254
- className: `FreetextHighlight ${highlightClass} ${editingClass}`,
3173
+ className: `FreetextHighlight ${highlightClass} ${editingClass} ${compactClass} ${collapsedClass}`,
2255
3174
  onContextMenu
2256
3175
  },
2257
3176
  /* @__PURE__ */ React11.createElement(
@@ -2274,14 +3193,14 @@ var FreetextHighlight = ({
2274
3193
  default: {
2275
3194
  x: highlight.position.boundingRect.left,
2276
3195
  y: highlight.position.boundingRect.top,
2277
- width: highlight.position.boundingRect.width || 150,
2278
- height: highlight.position.boundingRect.height || 80
3196
+ width: isCompactCollapsed ? compactSize : highlight.position.boundingRect.width || 150,
3197
+ height: isCompactCollapsed ? compactSize : highlight.position.boundingRect.height || 80
2279
3198
  },
2280
- minWidth: 100,
2281
- minHeight: 50,
3199
+ minWidth: isCompactCollapsed ? compactSize : 100,
3200
+ minHeight: isCompactCollapsed ? compactSize : 50,
2282
3201
  key,
2283
3202
  bounds,
2284
- enableResizing: {
3203
+ enableResizing: isCompactCollapsed ? false : {
2285
3204
  top: false,
2286
3205
  right: true,
2287
3206
  bottom: true,
@@ -2306,9 +3225,21 @@ var FreetextHighlight = ({
2306
3225
  onEditStart?.();
2307
3226
  }
2308
3227
  },
2309
- cancel: ".FreetextHighlight__text, .FreetextHighlight__input, .FreetextHighlight__edit-button, .FreetextHighlight__style-button, .FreetextHighlight__style-panel, .FreetextHighlight__delete-button"
3228
+ cancel: ".FreetextHighlight__text, .FreetextHighlight__input, .FreetextHighlight__edit-button, .FreetextHighlight__style-button, .FreetextHighlight__style-panel, .FreetextHighlight__delete-button, .FreetextHighlight__collapse-button, .FreetextHighlight__compact-button"
2310
3229
  },
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(
3230
+ /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__container", style: containerStyle }, isCompactCollapsed ? /* @__PURE__ */ React11.createElement(
3231
+ "button",
3232
+ {
3233
+ className: "FreetextHighlight__compact-button",
3234
+ type: "button",
3235
+ title: text || "Open note",
3236
+ onClick: (event) => {
3237
+ event.stopPropagation();
3238
+ setIsExpanded(true);
3239
+ }
3240
+ },
3241
+ compactIcon || /* @__PURE__ */ React11.createElement(DefaultCompactIcon, null)
3242
+ ) : /* @__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
3243
  "button",
2313
3244
  {
2314
3245
  className: "FreetextHighlight__edit-button",
@@ -2329,6 +3260,19 @@ var FreetextHighlight = ({
2329
3260
  type: "button"
2330
3261
  },
2331
3262
  styleIcon || /* @__PURE__ */ React11.createElement(DefaultStyleIcon3, null)
3263
+ ), compact && /* @__PURE__ */ React11.createElement(
3264
+ "button",
3265
+ {
3266
+ className: "FreetextHighlight__collapse-button",
3267
+ onClick: (e) => {
3268
+ e.stopPropagation();
3269
+ setIsExpanded(false);
3270
+ setIsStylePanelOpen(false);
3271
+ },
3272
+ title: "Collapse note",
3273
+ type: "button"
3274
+ },
3275
+ /* @__PURE__ */ React11.createElement(DefaultCollapseIcon, null)
2332
3276
  ), onDelete && /* @__PURE__ */ React11.createElement(
2333
3277
  "button",
2334
3278
  {
@@ -2433,7 +3377,7 @@ var FreetextHighlight = ({
2433
3377
  onKeyDown: handleKeyDown,
2434
3378
  onClick: (e) => e.stopPropagation()
2435
3379
  }
2436
- ) : /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__text" }, text || "New note")))
3380
+ ) : /* @__PURE__ */ React11.createElement("div", { className: "FreetextHighlight__text" }, text || "New note"))))
2437
3381
  )
2438
3382
  );
2439
3383
  };
@@ -2687,7 +3631,8 @@ var SignaturePad = ({
2687
3631
  };
2688
3632
 
2689
3633
  // src/components/DrawingHighlight.tsx
2690
- import React14, { useState as useState9, useCallback as useCallback4, useEffect as useEffect10, useRef as useRef12 } from "react";
3634
+ import React14, { useState as useState9, useCallback as useCallback4, useEffect as useEffect10, useLayoutEffect as useLayoutEffect5, useRef as useRef12 } from "react";
3635
+ import { createPortal as createPortal3 } from "react-dom";
2691
3636
  import { Rnd as Rnd4 } from "react-rnd";
2692
3637
  var DRAWING_COLORS = ["#000000", "#FF0000", "#0000FF", "#00FF00", "#FFFF00"];
2693
3638
  var STROKE_WIDTHS = [
@@ -2734,7 +3679,15 @@ var DrawingHighlight = ({
2734
3679
  }) => {
2735
3680
  const highlightClass = isScrolledTo ? "DrawingHighlight--scrolledTo" : "";
2736
3681
  const [showStyleControls, setShowStyleControls] = useState9(false);
3682
+ const [isHovered, setIsHovered] = useState9(false);
3683
+ const [configLayer, setConfigLayer] = useState9(null);
2737
3684
  const styleControlsRef = useRef12(null);
3685
+ const containerRef = useRef12(null);
3686
+ useLayoutEffect5(() => {
3687
+ if (containerRef.current) {
3688
+ setConfigLayer(findOrCreateHighlightConfigLayer(containerRef.current));
3689
+ }
3690
+ }, []);
2738
3691
  useEffect10(() => {
2739
3692
  if (!showStyleControls) return;
2740
3693
  const handleClickOutside = (e) => {
@@ -2787,8 +3740,78 @@ var DrawingHighlight = ({
2787
3740
  "div",
2788
3741
  {
2789
3742
  className: `DrawingHighlight ${highlightClass}`,
2790
- onContextMenu
3743
+ onContextMenu,
3744
+ ref: containerRef
2791
3745
  },
3746
+ configLayer && createPortal3(
3747
+ /* @__PURE__ */ React14.createElement(
3748
+ "div",
3749
+ {
3750
+ className: `DrawingHighlight__toolbar DrawingHighlight__toolbar--floating ${isHovered || isScrolledTo || showStyleControls ? "DrawingHighlight__toolbar--visible" : ""}`,
3751
+ style: {
3752
+ left: highlight.position.boundingRect.left + 4,
3753
+ top: highlight.position.boundingRect.top + 4
3754
+ },
3755
+ onMouseEnter: () => setIsHovered(true),
3756
+ onMouseLeave: () => setIsHovered(false)
3757
+ },
3758
+ /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__drag-handle", title: "Drag to move" }, dragIcon || /* @__PURE__ */ React14.createElement(DefaultDragIcon3, null)),
3759
+ strokes && strokes.length > 0 && onStyleChange && /* @__PURE__ */ React14.createElement(
3760
+ "button",
3761
+ {
3762
+ type: "button",
3763
+ className: "DrawingHighlight__style-button",
3764
+ title: "Edit style",
3765
+ onClick: (e) => {
3766
+ e.stopPropagation();
3767
+ setShowStyleControls(!showStyleControls);
3768
+ }
3769
+ },
3770
+ /* @__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" }))
3771
+ ),
3772
+ onDelete && /* @__PURE__ */ React14.createElement(
3773
+ "button",
3774
+ {
3775
+ className: "DrawingHighlight__delete-button",
3776
+ onClick: (e) => {
3777
+ e.stopPropagation();
3778
+ onDelete();
3779
+ },
3780
+ title: "Delete",
3781
+ type: "button"
3782
+ },
3783
+ deleteIcon || /* @__PURE__ */ React14.createElement(DefaultDeleteIcon5, null)
3784
+ ),
3785
+ 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(
3786
+ "button",
3787
+ {
3788
+ key: color,
3789
+ type: "button",
3790
+ className: `DrawingHighlight__color-button ${currentColor === color ? "active" : ""}`,
3791
+ style: { backgroundColor: color },
3792
+ onClick: (e) => {
3793
+ e.stopPropagation();
3794
+ handleColorChange(color);
3795
+ },
3796
+ title: `Color: ${color}`
3797
+ }
3798
+ ))), /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__width-picker" }, STROKE_WIDTHS.map((w) => /* @__PURE__ */ React14.createElement(
3799
+ "button",
3800
+ {
3801
+ key: w.value,
3802
+ type: "button",
3803
+ className: `DrawingHighlight__width-button ${currentWidth === w.value ? "active" : ""}`,
3804
+ onClick: (e) => {
3805
+ e.stopPropagation();
3806
+ handleWidthChange(w.value);
3807
+ },
3808
+ title: w.label
3809
+ },
3810
+ w.label
3811
+ ))))
3812
+ ),
3813
+ configLayer
3814
+ ),
2792
3815
  /* @__PURE__ */ React14.createElement(
2793
3816
  Rnd4,
2794
3817
  {
@@ -2826,72 +3849,29 @@ var DrawingHighlight = ({
2826
3849
  key,
2827
3850
  bounds,
2828
3851
  lockAspectRatio: false,
2829
- dragHandleClassName: "DrawingHighlight__drag-handle",
2830
3852
  onClick: (event) => {
2831
3853
  event.stopPropagation();
2832
3854
  event.preventDefault();
2833
3855
  },
2834
3856
  style
2835
3857
  },
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",
3858
+ /* @__PURE__ */ React14.createElement(
3859
+ "div",
2875
3860
  {
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
3861
+ className: "DrawingHighlight__container",
3862
+ onMouseEnter: () => setIsHovered(true),
3863
+ onMouseLeave: () => setIsHovered(false)
2884
3864
  },
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")))
3865
+ /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__content" }, imageUrl ? /* @__PURE__ */ React14.createElement(
3866
+ "img",
3867
+ {
3868
+ src: imageUrl,
3869
+ alt: "Drawing",
3870
+ className: "DrawingHighlight__image",
3871
+ draggable: false
3872
+ }
3873
+ ) : /* @__PURE__ */ React14.createElement("div", { className: "DrawingHighlight__placeholder" }, "No drawing"))
3874
+ )
2895
3875
  )
2896
3876
  );
2897
3877
  };
@@ -2900,8 +3880,10 @@ var DrawingHighlight = ({
2900
3880
  import React15, {
2901
3881
  useState as useState10,
2902
3882
  useRef as useRef13,
2903
- useEffect as useEffect11
3883
+ useEffect as useEffect11,
3884
+ useLayoutEffect as useLayoutEffect6
2904
3885
  } from "react";
3886
+ import { createPortal as createPortal4 } from "react-dom";
2905
3887
  import { Rnd as Rnd5 } from "react-rnd";
2906
3888
  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
3889
  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 +3926,14 @@ var ShapeHighlight = ({
2944
3926
  }) => {
2945
3927
  const [isStylePanelOpen, setIsStylePanelOpen] = useState10(false);
2946
3928
  const [isHovered, setIsHovered] = useState10(false);
3929
+ const [configLayer, setConfigLayer] = useState10(null);
2947
3930
  const stylePanelRef = useRef13(null);
3931
+ const containerRef = useRef13(null);
3932
+ useLayoutEffect6(() => {
3933
+ if (containerRef.current) {
3934
+ setConfigLayer(findOrCreateHighlightConfigLayer(containerRef.current));
3935
+ }
3936
+ }, []);
2948
3937
  useEffect11(() => {
2949
3938
  if (!isStylePanelOpen) return;
2950
3939
  const handleClickOutside = (e) => {
@@ -3056,92 +4045,96 @@ var ShapeHighlight = ({
3056
4045
  "div",
3057
4046
  {
3058
4047
  className: `ShapeHighlight ${highlightClass}`,
3059
- onContextMenu
4048
+ onContextMenu,
4049
+ ref: containerRef
3060
4050
  },
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
- },
4051
+ configLayer && (onStyleChange || onDelete) && createPortal4(
3074
4052
  /* @__PURE__ */ React15.createElement(
3075
4053
  "div",
3076
4054
  {
3077
- className: `ShapeHighlight__toolbar ${isHovered || isStylePanelOpen ? "ShapeHighlight__toolbar--visible" : ""}`
4055
+ className: "ShapeHighlight__toolbar-wrapper",
4056
+ style: {
4057
+ position: "absolute",
4058
+ left: highlight.position.boundingRect.left,
4059
+ top: highlight.position.boundingRect.top - 28,
4060
+ paddingBottom: 12
4061
+ },
4062
+ onMouseEnter: () => setIsHovered(true),
4063
+ onMouseLeave: () => setIsHovered(false)
3078
4064
  },
3079
- onStyleChange && /* @__PURE__ */ React15.createElement(
3080
- "button",
4065
+ /* @__PURE__ */ React15.createElement(
4066
+ "div",
3081
4067
  {
3082
- className: "ShapeHighlight__style-button",
3083
- onClick: (e) => {
3084
- e.stopPropagation();
3085
- setIsStylePanelOpen(!isStylePanelOpen);
3086
- },
3087
- title: "Change style",
3088
- type: "button"
4068
+ className: `ShapeHighlight__toolbar ${isHovered || isScrolledTo || isStylePanelOpen ? "ShapeHighlight__toolbar--visible" : ""}`
3089
4069
  },
3090
- styleIcon || /* @__PURE__ */ React15.createElement(DefaultStyleIcon4, null)
4070
+ onStyleChange && /* @__PURE__ */ React15.createElement(
4071
+ "button",
4072
+ {
4073
+ className: "ShapeHighlight__style-button",
4074
+ onClick: (e) => {
4075
+ e.stopPropagation();
4076
+ setIsStylePanelOpen(!isStylePanelOpen);
4077
+ },
4078
+ title: "Change style",
4079
+ type: "button"
4080
+ },
4081
+ styleIcon || /* @__PURE__ */ React15.createElement(DefaultStyleIcon4, null)
4082
+ ),
4083
+ onDelete && /* @__PURE__ */ React15.createElement(
4084
+ "button",
4085
+ {
4086
+ className: "ShapeHighlight__delete-button",
4087
+ onClick: (e) => {
4088
+ e.stopPropagation();
4089
+ onDelete();
4090
+ },
4091
+ title: "Delete",
4092
+ type: "button"
4093
+ },
4094
+ deleteIcon || /* @__PURE__ */ React15.createElement(DefaultDeleteIcon6, null)
4095
+ )
3091
4096
  ),
3092
- onDelete && /* @__PURE__ */ React15.createElement(
3093
- "button",
4097
+ isStylePanelOpen && onStyleChange && /* @__PURE__ */ React15.createElement(
4098
+ "div",
3094
4099
  {
3095
- className: "ShapeHighlight__delete-button",
3096
- onClick: (e) => {
3097
- e.stopPropagation();
3098
- onDelete();
3099
- },
3100
- title: "Delete",
3101
- type: "button"
4100
+ className: "ShapeHighlight__style-panel",
4101
+ ref: stylePanelRef,
4102
+ onClick: (e) => e.stopPropagation()
3102
4103
  },
3103
- deleteIcon || /* @__PURE__ */ React15.createElement(DefaultDeleteIcon6, null)
4104
+ /* @__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(
4105
+ "button",
4106
+ {
4107
+ key: c,
4108
+ type: "button",
4109
+ className: `ShapeHighlight__color-preset ${strokeColor === c ? "active" : ""}`,
4110
+ style: { backgroundColor: c },
4111
+ onClick: () => onStyleChange({ strokeColor: c }),
4112
+ title: c
4113
+ }
4114
+ ))), /* @__PURE__ */ React15.createElement(
4115
+ "input",
4116
+ {
4117
+ type: "color",
4118
+ value: strokeColor,
4119
+ onChange: (e) => {
4120
+ onStyleChange({ strokeColor: e.target.value });
4121
+ }
4122
+ }
4123
+ ))),
4124
+ /* @__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(
4125
+ "button",
4126
+ {
4127
+ key: w.value,
4128
+ type: "button",
4129
+ className: `ShapeHighlight__width-button ${strokeWidth === w.value ? "active" : ""}`,
4130
+ onClick: () => onStyleChange({ strokeWidth: w.value }),
4131
+ title: w.label
4132
+ },
4133
+ w.label
4134
+ ))))
3104
4135
  )
3105
4136
  ),
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
- )
4137
+ configLayer
3145
4138
  ),
3146
4139
  /* @__PURE__ */ React15.createElement(
3147
4140
  Rnd5,
@@ -3195,455 +4188,1033 @@ var ShapeHighlight = ({
3195
4188
  )
3196
4189
  );
3197
4190
  };
3198
-
3199
- // src/components/PdfLoader.tsx
3200
- import React16, { useEffect as useEffect12, useRef as useRef14, useState as useState11 } from "react";
3201
- import { GlobalWorkerOptions, getDocument as getDocument2 } from "pdfjs-dist";
3202
- var DEFAULT_BEFORE_LOAD = (progress) => /* @__PURE__ */ React16.createElement("div", { style: { color: "black" } }, "Loading ", Math.floor(progress.loaded / progress.total * 100), "%");
4191
+
4192
+ // src/components/PdfLoader.tsx
4193
+ import React16, { useEffect as useEffect12, useRef as useRef14, useState as useState11 } from "react";
4194
+ import { GlobalWorkerOptions, getDocument as getDocument2 } from "pdfjs-dist";
4195
+ var DEFAULT_BEFORE_LOAD = (progress) => {
4196
+ const pct = progress && progress.total ? Math.min(100, Math.floor(progress.loaded / progress.total * 100)) : null;
4197
+ return /* @__PURE__ */ React16.createElement(
4198
+ "div",
4199
+ {
4200
+ style: {
4201
+ display: "flex",
4202
+ flexDirection: "column",
4203
+ alignItems: "center",
4204
+ justifyContent: "center",
4205
+ gap: 12,
4206
+ height: "100%",
4207
+ color: "currentColor",
4208
+ opacity: 0.7,
4209
+ fontSize: 13
4210
+ }
4211
+ },
4212
+ /* @__PURE__ */ React16.createElement(
4213
+ "div",
4214
+ {
4215
+ style: {
4216
+ width: 28,
4217
+ height: 28,
4218
+ border: "3px solid currentColor",
4219
+ borderTopColor: "transparent",
4220
+ borderRadius: "50%",
4221
+ animation: "pdfloader-spin 0.8s linear infinite"
4222
+ }
4223
+ }
4224
+ ),
4225
+ /* @__PURE__ */ React16.createElement("div", null, pct !== null ? `Loading ${pct}%` : "Loading\u2026"),
4226
+ /* @__PURE__ */ React16.createElement("style", null, "@keyframes pdfloader-spin{to{transform:rotate(360deg)}}")
4227
+ );
4228
+ };
3203
4229
  var DEFAULT_ERROR_MESSAGE = (error) => /* @__PURE__ */ React16.createElement("div", { style: { color: "black" } }, error.message);
3204
4230
  var DEFAULT_ON_ERROR = (error) => {
3205
4231
  throw new Error(`Error loading PDF document: ${error.message}!`);
3206
4232
  };
3207
- var DEFAULT_WORKER_SRC = "https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.min.mjs";
4233
+ var DEFAULT_WORKER_SRC = new URL(
4234
+ "pdfjs-dist/build/pdf.worker.min.mjs",
4235
+ import.meta.url
4236
+ ).toString();
4237
+ var CACHE_MAX = 3;
4238
+ var lruTick = 0;
4239
+ var docCache = /* @__PURE__ */ new Map();
4240
+ var evictIfNeeded = () => {
4241
+ if (docCache.size <= CACHE_MAX) return;
4242
+ let victimKey;
4243
+ let oldest = Infinity;
4244
+ for (const [key, entry] of docCache) {
4245
+ if (entry.refs === 0 && entry.used < oldest) {
4246
+ oldest = entry.used;
4247
+ victimKey = key;
4248
+ }
4249
+ }
4250
+ if (victimKey !== void 0) {
4251
+ const victim = docCache.get(victimKey);
4252
+ docCache.delete(victimKey);
4253
+ victim.task.destroy();
4254
+ }
4255
+ };
4256
+ var cacheKeyOf = (document2) => {
4257
+ if (typeof document2 === "string") return document2;
4258
+ if (document2 instanceof URL) return document2.href;
4259
+ if (document2 && typeof document2 === "object" && !ArrayBuffer.isView(document2) && typeof document2.url === "string") {
4260
+ return document2.url;
4261
+ }
4262
+ return null;
4263
+ };
4264
+ var stripUndefined = (obj) => {
4265
+ const out = {};
4266
+ for (const [k, v] of Object.entries(obj))
4267
+ if (v !== void 0) out[k] = v;
4268
+ return out;
4269
+ };
4270
+ var buildSource = (document2, perf) => {
4271
+ const cleaned = stripUndefined(perf);
4272
+ if (typeof document2 === "string" || document2 instanceof URL) {
4273
+ return { url: document2, ...cleaned };
4274
+ }
4275
+ if (ArrayBuffer.isView(document2)) {
4276
+ return document2;
4277
+ }
4278
+ return { ...cleaned, ...document2 };
4279
+ };
3208
4280
  var PdfLoader = ({
3209
4281
  document: document2,
3210
4282
  beforeLoad = DEFAULT_BEFORE_LOAD,
3211
4283
  errorMessage = DEFAULT_ERROR_MESSAGE,
3212
4284
  children,
3213
4285
  onError = DEFAULT_ON_ERROR,
3214
- workerSrc = DEFAULT_WORKER_SRC
4286
+ workerSrc = DEFAULT_WORKER_SRC,
4287
+ disableAutoFetch = true,
4288
+ disableStream,
4289
+ rangeChunkSize,
4290
+ withCredentials,
4291
+ httpHeaders,
4292
+ enableCache = true
3215
4293
  }) => {
3216
- const pdfLoadingTaskRef = useRef14(null);
3217
- const pdfDocumentRef = useRef14(null);
4294
+ const [pdfDocument, setPdfDocument] = useState11(null);
3218
4295
  const [error, setError] = useState11(null);
3219
4296
  const [loadingProgress, setLoadingProgress] = useState11(null);
4297
+ const onErrorRef = useRef14(onError);
4298
+ onErrorRef.current = onError;
4299
+ const httpHeadersKey = httpHeaders ? JSON.stringify(httpHeaders) : "";
3220
4300
  useEffect12(() => {
3221
4301
  GlobalWorkerOptions.workerSrc = workerSrc;
3222
- pdfLoadingTaskRef.current = getDocument2(document2);
3223
- pdfLoadingTaskRef.current.onProgress = (progress) => {
3224
- setLoadingProgress(progress.loaded > progress.total ? null : progress);
3225
- };
3226
- pdfLoadingTaskRef.current.promise.then((pdfDocument) => {
3227
- pdfDocumentRef.current = pdfDocument;
3228
- }).catch((error2) => {
3229
- if (error2.message != "Worker was destroyed") {
3230
- setError(error2);
3231
- onError(error2);
4302
+ let cancelled = false;
4303
+ setPdfDocument(null);
4304
+ setError(null);
4305
+ setLoadingProgress(null);
4306
+ const key = enableCache ? cacheKeyOf(document2) : null;
4307
+ let entry;
4308
+ const cached = key ? docCache.get(key) : void 0;
4309
+ if (cached) {
4310
+ console.log("[PdfLoader] cache hit", key);
4311
+ entry = cached;
4312
+ entry.refs += 1;
4313
+ entry.used = ++lruTick;
4314
+ } else {
4315
+ console.log("[PdfLoader] loading", key ?? "(bytes)", {
4316
+ disableAutoFetch
4317
+ });
4318
+ const task = getDocument2(
4319
+ buildSource(document2, {
4320
+ disableAutoFetch,
4321
+ disableStream,
4322
+ rangeChunkSize,
4323
+ withCredentials,
4324
+ httpHeaders
4325
+ })
4326
+ );
4327
+ task.onProgress = (progress) => {
4328
+ if (!cancelled)
4329
+ setLoadingProgress(progress.loaded > progress.total ? null : progress);
4330
+ };
4331
+ entry = { task, promise: task.promise, refs: 1, used: ++lruTick };
4332
+ if (key) {
4333
+ docCache.set(key, entry);
4334
+ evictIfNeeded();
3232
4335
  }
4336
+ }
4337
+ entry.promise.then((proxy) => {
4338
+ if (!cancelled) setPdfDocument(proxy);
4339
+ }).catch((err) => {
4340
+ if (cancelled || err.message === "Worker was destroyed") return;
4341
+ setError(err);
4342
+ onErrorRef.current(err);
3233
4343
  }).finally(() => {
3234
- setLoadingProgress(null);
4344
+ if (!cancelled) setLoadingProgress(null);
3235
4345
  });
3236
4346
  return () => {
3237
- if (pdfLoadingTaskRef.current) {
3238
- pdfLoadingTaskRef.current.destroy();
3239
- }
3240
- if (pdfDocumentRef.current) {
3241
- pdfDocumentRef.current.destroy();
4347
+ cancelled = true;
4348
+ entry.refs -= 1;
4349
+ if (key && docCache.has(key)) {
4350
+ entry.used = ++lruTick;
4351
+ evictIfNeeded();
4352
+ } else {
4353
+ entry.task.destroy();
3242
4354
  }
3243
4355
  };
3244
- }, [document2]);
3245
- return error ? errorMessage(error) : loadingProgress ? beforeLoad(loadingProgress) : pdfDocumentRef.current && children(pdfDocumentRef.current);
4356
+ }, [
4357
+ document2,
4358
+ workerSrc,
4359
+ disableAutoFetch,
4360
+ disableStream,
4361
+ rangeChunkSize,
4362
+ withCredentials,
4363
+ httpHeadersKey,
4364
+ enableCache
4365
+ ]);
4366
+ if (error) return errorMessage(error);
4367
+ if (pdfDocument) return children(pdfDocument);
4368
+ return beforeLoad(loadingProgress);
3246
4369
  };
3247
4370
 
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
- };
4371
+ // src/lib/extract-sentences.ts
4372
+ var DEFAULT_OPTIONS = {
4373
+ pages: "all",
4374
+ includePositions: false,
4375
+ includeSources: false,
4376
+ normalize: true,
4377
+ locale: "en",
4378
+ idPrefix: "",
4379
+ includeTextUnitTypes: ["paragraph"],
4380
+ readingOrder: "auto",
4381
+ columnDetection: "auto"
4382
+ };
4383
+ var isTextItem = (item) => {
4384
+ return typeof item === "object" && item !== null && "str" in item && typeof item.str === "string" && "transform" in item && Array.isArray(item.transform);
4385
+ };
4386
+ var shouldInsertSpace = (previous, next) => {
4387
+ if (!previous || !next) return false;
4388
+ if (/\s$/.test(previous) || /^\s/.test(next)) return false;
4389
+ if (/^[,.;:!?)]/.test(next)) return false;
4390
+ if (/[([{]$/.test(previous)) return false;
4391
+ return true;
4392
+ };
4393
+ var getLastNonWhitespace = (text) => {
4394
+ const match = text.match(/\S(?=\s*$)/);
4395
+ return match?.[0] ?? "";
4396
+ };
4397
+ var getTrimmedChars = (text) => Array.from(text.trim());
4398
+ var getItemTextKind = (text) => {
4399
+ if (!text.trim()) return "space";
4400
+ if (/^[,.;:!?)\]}]+$/.test(text.trim())) return "punctuation";
4401
+ const chars = getTrimmedChars(text);
4402
+ if (chars.length === 1 && /[\p{L}\p{N}]/u.test(chars[0])) {
4403
+ return "singleGlyph";
3261
4404
  }
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
- };
4405
+ if (/[\p{L}\p{N}]/u.test(text)) return "wordLike";
4406
+ return "other";
4407
+ };
4408
+ var getMedianGlyphWidth = (textItems) => {
4409
+ const glyphWidths = textItems.map((item) => {
4410
+ const textLength = Math.max(getTrimmedChars(item.text).length, 1);
4411
+ return item.rect.width / textLength;
4412
+ }).filter((width) => Number.isFinite(width) && width > 0).sort((a, b) => a - b);
4413
+ if (glyphWidths.length === 0) return 0;
4414
+ return glyphWidths[Math.floor(glyphWidths.length / 2)];
4415
+ };
4416
+ var getHorizontalGap = (previousItem, nextItem, medianGlyphWidth) => {
4417
+ const previousRight = previousItem.rect.left + previousItem.rect.width;
4418
+ const gap = nextItem.rect.left - previousRight;
4419
+ const zeroTolerance = Math.max(medianGlyphWidth * 0.08, 0.35);
4420
+ return Math.abs(gap) <= zeroTolerance ? 0 : gap;
4421
+ };
4422
+ var shouldInsertSpaceBetweenItems = (previousItem, nextItem, currentText, averageHeight, medianGlyphWidth) => {
4423
+ if (!previousItem || !currentText || !nextItem.text) return false;
4424
+ if (/\s$/.test(currentText) || /^\s/.test(nextItem.text)) return false;
4425
+ const previousChar = getLastNonWhitespace(currentText);
4426
+ const nextChar = Array.from(nextItem.text.trim())[0] ?? "";
4427
+ const previousKind = getItemTextKind(previousItem.text);
4428
+ const nextKind = getItemTextKind(nextItem.text);
4429
+ if (!nextChar) return false;
4430
+ if (nextKind === "punctuation" || /^[,.;:!?)]/.test(nextChar)) return false;
4431
+ if (/[([{]$/.test(previousChar)) return false;
4432
+ const gap = getHorizontalGap(previousItem, nextItem, medianGlyphWidth);
4433
+ if (gap < 0) return false;
4434
+ const glyphWidth = Math.max(
4435
+ medianGlyphWidth,
4436
+ averageHeight * 0.18,
4437
+ 1
4438
+ );
4439
+ const singleGlyphPair = previousKind === "singleGlyph" && nextKind === "singleGlyph";
4440
+ if (singleGlyphPair) {
4441
+ return gap > Math.max(glyphWidth * 0.35, 1);
3270
4442
  }
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
- };
4443
+ if ((previousKind === "wordLike" || previousKind === "singleGlyph") && (nextKind === "wordLike" || nextKind === "singleGlyph")) {
4444
+ return gap > Math.max(glyphWidth * 0.08, 0.35);
3278
4445
  }
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);
4446
+ return shouldInsertSpace(currentText, nextItem.text);
4447
+ };
4448
+ var appendTextItem = (currentText, item, previousItem, averageHeight, medianGlyphWidth) => {
4449
+ return `${currentText}${shouldInsertSpaceBetweenItems(
4450
+ previousItem,
4451
+ item,
4452
+ currentText,
4453
+ averageHeight,
4454
+ medianGlyphWidth
4455
+ ) ? " " : ""}${item.text}`;
4456
+ };
4457
+ var getAverageItemHeight = (textItems) => {
4458
+ const heights = textItems.map((item) => item.rect.height).filter((height) => height > 0).sort((a, b) => a - b);
4459
+ if (heights.length === 0) return 0;
4460
+ return heights[Math.floor(heights.length / 2)];
4461
+ };
4462
+ var areTextItemsOnSameLine = (previous, next, averageHeight) => {
4463
+ const previousMiddle = previous.rect.top + previous.rect.height / 2;
4464
+ const nextMiddle = next.rect.top + next.rect.height / 2;
4465
+ const tolerance = Math.max(averageHeight * 0.45, 2);
4466
+ return Math.abs(previousMiddle - nextMiddle) <= tolerance;
4467
+ };
4468
+ var getTextItemRect = (item, viewport, pageNumber) => {
4469
+ const [, , , , x, y] = item.transform;
4470
+ const width = Math.abs(item.width ?? 0);
4471
+ const height = Math.abs(item.height ?? 0);
4472
+ const [x1, y1, x2, y2] = viewport.convertToViewportRectangle([
4473
+ x,
4474
+ y,
4475
+ x + width,
4476
+ y + height
4477
+ ]);
4478
+ return {
4479
+ left: Math.min(x1, x2),
4480
+ top: Math.min(y1, y2),
4481
+ width: Math.abs(x2 - x1),
4482
+ height: Math.abs(y2 - y1),
4483
+ pageNumber
4484
+ };
4485
+ };
4486
+ var resolvePageNumbers = (pdfDocument, pages) => {
4487
+ if (pages === "all" || pages === void 0) {
4488
+ return Array.from({ length: pdfDocument.numPages }, (_, index) => index + 1);
3298
4489
  }
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;
4490
+ return [...new Set(pages)].filter((pageNumber) => pageNumber >= 1 && pageNumber <= pdfDocument.numPages).sort((a, b) => a - b);
4491
+ };
4492
+ var buildPageTextStream = (textItems) => {
4493
+ let text = "";
4494
+ const charItemIndexes = [];
4495
+ const averageHeight = getAverageItemHeight(textItems);
4496
+ const medianGlyphWidth = getMedianGlyphWidth(textItems);
4497
+ let previousItem;
4498
+ textItems.forEach((item) => {
4499
+ if (!item.text) return;
4500
+ if (shouldInsertSpaceBetweenItems(
4501
+ previousItem,
4502
+ item,
4503
+ text,
4504
+ averageHeight,
4505
+ medianGlyphWidth
4506
+ )) {
4507
+ text += " ";
4508
+ charItemIndexes.push(null);
3310
4509
  }
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
- }
4510
+ for (const char of item.text) {
4511
+ text += char;
4512
+ charItemIndexes.push(item.index);
3342
4513
  }
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);
4514
+ previousItem = item;
4515
+ });
4516
+ return { text, charItemIndexes };
4517
+ };
4518
+ var normalizePdfLigatures = (text) => {
4519
+ return text.replace(/ffi/g, "ffi").replace(/ffl/g, "ffl").replace(/fi/g, "fi").replace(/fl/g, "fl").replace(/ff/g, "ff");
4520
+ };
4521
+ var cleanupPdfHyphenSpacing = (text) => {
4522
+ return text.replace(
4523
+ /\b([\p{L}\p{N}]{2,})\s+-\s+([\p{L}\p{N}]{2,})\b/gu,
4524
+ "$1-$2"
4525
+ );
4526
+ };
4527
+ var normalizePdfSentenceText = (text) => {
4528
+ return cleanupPdfHyphenSpacing(normalizePdfLigatures(text)).replace(/(\p{L}+)-\s+(\p{L}+)/gu, "$1-$2").replace(/\[\s*([^\]]*?)\s*\]/g, (_, citation) => {
4529
+ const normalizedCitation = citation.replace(/\s*,\s*/g, ", ").replace(/\s+/g, " ").trim();
4530
+ return `[${normalizedCitation}]`;
4531
+ }).replace(/\s+/g, " ").trim();
4532
+ };
4533
+ var splitPdfSentences = (text, locale = DEFAULT_OPTIONS.locale) => {
4534
+ const segmenterConstructor = Intl.Segmenter;
4535
+ if (segmenterConstructor) {
4536
+ const segmenter = new segmenterConstructor(locale, {
4537
+ granularity: "sentence"
4538
+ });
4539
+ return Array.from(segmenter.segment(text)).map((segment) => ({
4540
+ text: segment.segment.trim(),
4541
+ start: segment.index + segment.segment.search(/\S/),
4542
+ end: segment.index + segment.segment.trimEnd().length
4543
+ })).filter((sentence) => sentence.text.length > 0 && sentence.start >= 0);
3353
4544
  }
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
4545
+ const ranges = [];
4546
+ let start = 0;
4547
+ for (let index = 0; index < text.length; index += 1) {
4548
+ const char = text[index];
4549
+ if (!/[.!?]/.test(char)) continue;
4550
+ const nextText = text.slice(index + 1);
4551
+ const nextNonSpace = nextText.match(/\S/);
4552
+ const nextChar = nextNonSpace?.[0] ?? "";
4553
+ const shouldSplit = nextChar === "" || /[A-Z([{"']/.test(nextChar);
4554
+ if (!shouldSplit) continue;
4555
+ const rawSentence = text.slice(start, index + 1);
4556
+ const leadingWhitespace2 = rawSentence.search(/\S/);
4557
+ const sentenceStart = start + Math.max(leadingWhitespace2, 0);
4558
+ const sentenceText = rawSentence.trim();
4559
+ if (sentenceText) {
4560
+ ranges.push({
4561
+ text: sentenceText,
4562
+ start: sentenceStart,
4563
+ end: start + rawSentence.trimEnd().length
3392
4564
  });
3393
4565
  }
4566
+ start = index + 1;
3394
4567
  }
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
4568
+ const finalRawSentence = text.slice(start);
4569
+ const leadingWhitespace = finalRawSentence.search(/\S/);
4570
+ const finalText = finalRawSentence.trim();
4571
+ if (finalText) {
4572
+ ranges.push({
4573
+ text: finalText,
4574
+ start: start + Math.max(leadingWhitespace, 0),
4575
+ end: start + finalRawSentence.trimEnd().length
4576
+ });
4577
+ }
4578
+ return ranges;
4579
+ };
4580
+ var getSentenceTextItemIndexes = (charItemIndexes, start, end) => {
4581
+ const indexes = /* @__PURE__ */ new Set();
4582
+ for (let index = start; index < end; index += 1) {
4583
+ const itemIndex = charItemIndexes[index];
4584
+ if (itemIndex !== null && itemIndex !== void 0) {
4585
+ indexes.add(itemIndex);
4586
+ }
4587
+ }
4588
+ return Array.from(indexes).sort((a, b) => a - b);
4589
+ };
4590
+ var getScaledPosition = (textItems, textItemIndexes, page) => {
4591
+ const indexSet = new Set(textItemIndexes);
4592
+ const rects = textItems.filter((item) => indexSet.has(item.index)).map((item) => item.rect).filter((rect) => rect.width > 0 && rect.height > 0);
4593
+ if (rects.length === 0) return void 0;
4594
+ const boundingRect = get_bounding_rect_default(rects);
4595
+ return {
4596
+ boundingRect: viewportToScaled(boundingRect, page),
4597
+ rects: rects.map((rect) => viewportToScaled(rect, page))
4598
+ };
4599
+ };
4600
+ var getTextItemsPosition = (textItems, textItemIndexes, page, includePositions) => {
4601
+ return includePositions ? getScaledPosition(textItems, textItemIndexes, page) : void 0;
4602
+ };
4603
+ var getTextItemsBoundingRect = (textItems) => {
4604
+ const rects = textItems.map((item) => item.rect).filter((rect) => rect.width > 0 && rect.height > 0);
4605
+ return rects.length > 0 ? get_bounding_rect_default(rects) : void 0;
4606
+ };
4607
+ var getOrderedTextItems = (textItems, readingOrder) => {
4608
+ if (readingOrder === "document") return [...textItems];
4609
+ const averageHeight = getAverageItemHeight(textItems);
4610
+ return [...textItems].sort((a, b) => {
4611
+ const topDelta = a.rect.top - b.rect.top;
4612
+ if (Math.abs(topDelta) > Math.max(averageHeight * 0.45, 2)) {
4613
+ return topDelta;
4614
+ }
4615
+ return a.rect.left - b.rect.left;
3410
4616
  });
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)
4617
+ };
4618
+ var groupTextItemsIntoLines = (textItems) => {
4619
+ const averageHeight = getAverageItemHeight(textItems);
4620
+ const sortedItems = getOrderedTextItems(textItems, "position");
4621
+ const lines = [];
4622
+ sortedItems.forEach((item) => {
4623
+ const currentLine = lines[lines.length - 1];
4624
+ const previousItem = currentLine?.[currentLine.length - 1];
4625
+ if (currentLine && previousItem && areTextItemsOnSameLine(previousItem, item, averageHeight)) {
4626
+ currentLine.push(item);
4627
+ currentLine.sort((a, b) => a.rect.left - b.rect.left);
4628
+ return;
4629
+ }
4630
+ lines.push([item]);
3431
4631
  });
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
- });
4632
+ return lines;
4633
+ };
4634
+ var isLikelyBodyTextItem = (item, page) => {
4635
+ const text = item.text.trim();
4636
+ if (!text) return false;
4637
+ if (item.rect.top < page.height * 0.12 && page.pageNumber === 1 || item.rect.top > page.height * 0.88) {
4638
+ return false;
3443
4639
  }
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;
4640
+ if (isLikelyFootnoteOrReference(text)) return false;
4641
+ return true;
4642
+ };
4643
+ var getColumnRanges = (textItems, page) => {
4644
+ if (textItems.length < 24) return void 0;
4645
+ const minLeft = Math.min(...textItems.map((item) => item.rect.left));
4646
+ const maxRight = Math.max(
4647
+ ...textItems.map((item) => item.rect.left + item.rect.width)
4648
+ );
4649
+ const pageSpan = maxRight - minLeft;
4650
+ const binCount = 80;
4651
+ const bins = Array.from({ length: binCount }, () => 0);
4652
+ textItems.forEach((item) => {
4653
+ const left = item.rect.left;
4654
+ const right = item.rect.left + item.rect.width;
4655
+ const startBin = Math.max(
4656
+ 0,
4657
+ Math.floor((left - minLeft) / pageSpan * binCount)
4658
+ );
4659
+ const endBin = Math.min(
4660
+ binCount - 1,
4661
+ Math.ceil((right - minLeft) / pageSpan * binCount)
4662
+ );
4663
+ for (let index = startBin; index <= endBin; index += 1) {
4664
+ bins[index] += 1;
4665
+ }
4666
+ });
4667
+ const centerStart = Math.floor(binCount * 0.35);
4668
+ const centerEnd = Math.ceil(binCount * 0.65);
4669
+ let bestGapStart = -1;
4670
+ let bestGapEnd = -1;
4671
+ let currentGapStart = -1;
4672
+ for (let index = centerStart; index <= centerEnd; index += 1) {
4673
+ if (bins[index] === 0) {
4674
+ if (currentGapStart === -1) currentGapStart = index;
4675
+ continue;
4676
+ }
4677
+ if (currentGapStart !== -1 && index - currentGapStart > bestGapEnd - bestGapStart) {
4678
+ bestGapStart = currentGapStart;
4679
+ bestGapEnd = index;
3462
4680
  }
4681
+ currentGapStart = -1;
3463
4682
  }
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) {
4683
+ if (currentGapStart !== -1 && centerEnd + 1 - currentGapStart > bestGapEnd - bestGapStart) {
4684
+ bestGapStart = currentGapStart;
4685
+ bestGapEnd = centerEnd + 1;
4686
+ }
4687
+ const gapBins = bestGapEnd - bestGapStart;
4688
+ const gapWidth = gapBins / binCount * pageSpan;
4689
+ const minGapWidth = Math.max(page.width * 0.035, 14);
4690
+ if (bestGapStart < 0 || gapWidth < minGapWidth) return void 0;
4691
+ const gapLeft = minLeft + bestGapStart / binCount * pageSpan;
4692
+ const gapRight = minLeft + bestGapEnd / binCount * pageSpan;
4693
+ const splitX = (gapLeft + gapRight) / 2;
4694
+ const leftItems = textItems.filter(
4695
+ (item) => item.rect.left + item.rect.width / 2 < splitX
4696
+ );
4697
+ const rightItems = textItems.filter(
4698
+ (item) => item.rect.left + item.rect.width / 2 >= splitX
4699
+ );
4700
+ if (leftItems.length < 10 || rightItems.length < 10) return void 0;
4701
+ const leftRange = {
4702
+ left: Math.min(...leftItems.map((item) => item.rect.left)),
4703
+ right: Math.max(...leftItems.map((item) => item.rect.left + item.rect.width))
4704
+ };
4705
+ const rightRange = {
4706
+ left: Math.min(...rightItems.map((item) => item.rect.left)),
4707
+ right: Math.max(...rightItems.map((item) => item.rect.left + item.rect.width))
4708
+ };
4709
+ const leftHeight = getAverageItemHeight(leftItems);
4710
+ const rightHeight = getAverageItemHeight(rightItems);
4711
+ if (leftHeight > 0 && rightHeight > 0 && Math.max(leftHeight, rightHeight) / Math.min(leftHeight, rightHeight) > 1.45) {
4712
+ return void 0;
4713
+ }
4714
+ return [leftRange, rightRange];
4715
+ };
4716
+ var assignColumnsToPage = (page, columnDetection) => {
4717
+ if (columnDetection === "none") {
3470
4718
  return {
3471
- x: y,
3472
- y: pageWidth - x - width,
3473
- width: height,
3474
- height: width
4719
+ ...page,
4720
+ textItems: page.textItems.map((item) => ({ ...item, columnIndex: 0 })),
4721
+ columns: [
4722
+ {
4723
+ index: 0,
4724
+ left: 0,
4725
+ right: page.width,
4726
+ width: page.width,
4727
+ textItemIndexes: page.textItems.map((item) => item.index)
4728
+ }
4729
+ ]
3475
4730
  };
3476
- } else if (rotation === 180) {
4731
+ }
4732
+ const bodyItems = page.textItems.filter(
4733
+ (item) => isLikelyBodyTextItem(item, page)
4734
+ );
4735
+ const ranges = getColumnRanges(bodyItems, page);
4736
+ if (!ranges) {
3477
4737
  return {
3478
- x: pageWidth - x - width,
3479
- y: pageHeight - y - height,
3480
- width,
3481
- height
4738
+ ...page,
4739
+ textItems: page.textItems.map((item) => ({ ...item, columnIndex: 0 })),
4740
+ columns: [
4741
+ {
4742
+ index: 0,
4743
+ left: 0,
4744
+ right: page.width,
4745
+ width: page.width,
4746
+ textItemIndexes: page.textItems.map((item) => item.index)
4747
+ }
4748
+ ]
3482
4749
  };
3483
- } else if (rotation === 270) {
4750
+ }
4751
+ const splitX = (ranges[0].right + ranges[1].left) / 2;
4752
+ const textItems = page.textItems.map((item) => {
4753
+ const centerX = item.rect.left + item.rect.width / 2;
3484
4754
  return {
3485
- x: pageHeight - y - height,
3486
- y: x,
3487
- width: height,
3488
- height: width
4755
+ ...item,
4756
+ columnIndex: centerX < splitX ? 0 : 1
3489
4757
  };
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
4758
+ });
4759
+ const columns = ranges.map((range, index) => ({
4760
+ index,
4761
+ left: range.left,
4762
+ right: range.right,
4763
+ width: range.right - range.left,
4764
+ textItemIndexes: textItems.filter((item) => item.columnIndex === index).map((item) => item.index)
4765
+ }));
4766
+ return { ...page, textItems, columns };
4767
+ };
4768
+ var isFullWidthBlock = (block, page, detectedColumnCount) => {
4769
+ if (detectedColumnCount < 2) return false;
4770
+ const textItems = block.flat();
4771
+ const boundingRect = getTextItemsBoundingRect(textItems);
4772
+ if (!boundingRect) return false;
4773
+ return boundingRect.width > page.width * 0.72;
4774
+ };
4775
+ var buildBlocksForItems = (textItems, page) => {
4776
+ const lines = groupTextItemsIntoLines(textItems);
4777
+ const averageHeight = getAverageItemHeight(textItems);
4778
+ const blocks = [];
4779
+ lines.forEach((line) => {
4780
+ const currentBlock = blocks[blocks.length - 1];
4781
+ const previousLine = currentBlock?.[currentBlock.length - 1];
4782
+ if (currentBlock && previousLine && !shouldStartNewBlock(previousLine, line, page, averageHeight)) {
4783
+ currentBlock.push(line);
4784
+ return;
4785
+ }
4786
+ blocks.push([line]);
4787
+ });
4788
+ return blocks;
4789
+ };
4790
+ var compareBlocksByPosition = (left, right) => {
4791
+ const leftRect = getTextItemsBoundingRect(left.block.flat());
4792
+ const rightRect = getTextItemsBoundingRect(right.block.flat());
4793
+ return (leftRect?.top ?? 0) - (rightRect?.top ?? 0);
4794
+ };
4795
+ var compareBlocksByColumnReadingOrder = (left, right) => {
4796
+ const columnDelta = (left.columnIndex ?? 0) - (right.columnIndex ?? 0);
4797
+ if (columnDelta !== 0) return columnDelta;
4798
+ return compareBlocksByPosition(left, right);
4799
+ };
4800
+ var buildTextUnit = ({
4801
+ block,
4802
+ columnIndex,
4803
+ indexInPage,
4804
+ includePositions,
4805
+ idPrefix,
4806
+ page,
4807
+ shouldNormalize
4808
+ }) => {
4809
+ const textItems = block.flat();
4810
+ const rawText = block.map(getLineText).join(" ");
4811
+ const textItemIndexes = textItems.map((item) => item.index);
4812
+ const type = classifyTextUnit(rawText, textItemIndexes, page);
4813
+ const position = getTextItemsPosition(
4814
+ page.textItems,
4815
+ textItemIndexes,
4816
+ page,
4817
+ includePositions
4818
+ );
4819
+ return {
4820
+ id: `${idPrefix}p${page.pageNumber}-u${indexInPage}`,
4821
+ type,
4822
+ text: shouldNormalize ? normalizePdfSentenceText(rawText) : rawText.trim(),
4823
+ rawText: rawText.trim(),
4824
+ pageNumber: page.pageNumber,
4825
+ indexInPage,
4826
+ ...columnIndex !== void 0 ? { columnIndex } : {},
4827
+ ...position ? { position } : {},
4828
+ source: {
4829
+ textItemIndexes
4830
+ }
4831
+ };
4832
+ };
4833
+ var getLineText = (line) => {
4834
+ const averageHeight = getAverageItemHeight(line);
4835
+ const medianGlyphWidth = getMedianGlyphWidth(line);
4836
+ return line.reduce((text, item, index) => {
4837
+ return appendTextItem(
4838
+ text,
4839
+ item,
4840
+ line[index - 1],
4841
+ averageHeight,
4842
+ medianGlyphWidth
3509
4843
  );
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);
4844
+ }, "");
4845
+ };
4846
+ var getLineRect = (line) => get_bounding_rect_default(line.map((item) => item.rect));
4847
+ var shouldStartNewBlock = (previousLine, nextLine, page, averageHeight) => {
4848
+ const previousRect = getLineRect(previousLine);
4849
+ const nextRect = getLineRect(nextLine);
4850
+ const verticalGap = nextRect.top - (previousRect.top + previousRect.height);
4851
+ const leftDelta = Math.abs(nextRect.left - previousRect.left);
4852
+ const previousText = getLineText(previousLine).trim();
4853
+ const nextText = getLineText(nextLine).trim();
4854
+ if (verticalGap > Math.max(averageHeight * 1.15, 7)) return true;
4855
+ if (leftDelta > page.width * 0.18) return true;
4856
+ if (isLikelyStandaloneBlock(previousText) || isLikelyStandaloneBlock(nextText)) {
4857
+ return true;
3523
4858
  }
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
4859
+ return false;
4860
+ };
4861
+ var isLikelyStandaloneBlock = (text) => {
4862
+ const normalizedText = normalizePdfSentenceText(text);
4863
+ if (!normalizedText) return false;
4864
+ if (isLikelySectionHeading(normalizedText)) return true;
4865
+ if (isLikelyFootnoteOrReference(normalizedText)) return true;
4866
+ return false;
4867
+ };
4868
+ var isLikelySectionHeading = (text) => {
4869
+ if (text.length > 80) return false;
4870
+ if (/^\d+(\.\d+)*\.?\s+[A-Z]/.test(text)) return true;
4871
+ const letters = text.replace(/[^A-Za-z]/g, "");
4872
+ if (letters.length < 3) return false;
4873
+ const uppercaseLetters = text.replace(/[^A-Z]/g, "");
4874
+ return uppercaseLetters.length / letters.length > 0.75;
4875
+ };
4876
+ var isLikelyFootnoteOrReference = (text) => {
4877
+ return /^\d+\s*https?:\/\//i.test(text) || /^\[\d+\]/.test(text) || /https?:\/\/|doi\.org|arxiv\.org/i.test(text);
4878
+ };
4879
+ var isLikelyAuthor = (text) => {
4880
+ return text.length <= 80 && !/[.!?]$/.test(text) && /^[A-Z][A-Za-z'.-]+(?:\s+[A-Z][A-Za-z'.-]+){1,4}$/.test(text);
4881
+ };
4882
+ var isLikelyAffiliation = (text) => {
4883
+ return text.length <= 160 && /university|institute|department|school|college|faculty|laboratory|germany|usa|china|france|italy|spain|canada|japan|korea/i.test(
4884
+ text
3533
4885
  );
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
4886
+ };
4887
+ var classifyTextUnit = (rawText, textItemIndexes, page) => {
4888
+ const text = normalizePdfSentenceText(rawText);
4889
+ const textItems = page.textItems.filter(
4890
+ (item) => textItemIndexes.includes(item.index)
4891
+ );
4892
+ const rects = textItems.map((item) => item.rect);
4893
+ const boundingRect = rects.length > 0 ? get_bounding_rect_default(rects) : void 0;
4894
+ const averagePageHeight = getAverageItemHeight(page.textItems);
4895
+ const averageUnitHeight = getAverageItemHeight(textItems);
4896
+ const isTopOfFirstPage = page.pageNumber === 1 && (boundingRect?.top ?? page.height) < page.height * 0.2;
4897
+ const isBottomOfPage = (boundingRect?.top ?? 0) > page.height * 0.78;
4898
+ const isLargeText = averagePageHeight > 0 && averageUnitHeight > averagePageHeight * 1.25;
4899
+ const isCentered = boundingRect !== void 0 && Math.abs(boundingRect.left + boundingRect.width / 2 - page.width / 2) < page.width * 0.16;
4900
+ if (isLikelyFootnoteOrReference(text) || isBottomOfPage && /^\d+\s/.test(text)) {
4901
+ return text.includes("://") || /^\d+\s*https?:\/\//i.test(text) ? "footnote" : "reference";
4902
+ }
4903
+ if (isLikelySectionHeading(text)) return "heading";
4904
+ if (isTopOfFirstPage && isLargeText && isCentered) return "title";
4905
+ if (isTopOfFirstPage && isLikelyAuthor(text)) return "author";
4906
+ if (isTopOfFirstPage && isLikelyAffiliation(text)) return "affiliation";
4907
+ return "paragraph";
4908
+ };
4909
+ var extractTextUnitsFromPages = (pages, options = {}) => {
4910
+ const includePositions = options.includePositions ?? DEFAULT_OPTIONS.includePositions;
4911
+ const includeSources = options.includeSources ?? DEFAULT_OPTIONS.includeSources;
4912
+ const shouldNormalize = options.normalize ?? DEFAULT_OPTIONS.normalize;
4913
+ const idPrefix = options.idPrefix ?? DEFAULT_OPTIONS.idPrefix;
4914
+ const readingOrder = options.readingOrder ?? DEFAULT_OPTIONS.readingOrder;
4915
+ const columnDetection = options.columnDetection ?? DEFAULT_OPTIONS.columnDetection;
4916
+ const units = [];
4917
+ pages.forEach((page) => {
4918
+ const pageWithColumns = assignColumnsToPage(page, columnDetection);
4919
+ const detectedColumnCount = pageWithColumns.columns?.length ?? 1;
4920
+ const columnIndexes = detectedColumnCount > 1 && readingOrder !== "document" ? pageWithColumns.columns?.map((column) => column.index) ?? [0] : [0];
4921
+ const blocks = [];
4922
+ if (readingOrder === "document") {
4923
+ blocks.push(
4924
+ ...buildBlocksForItems(pageWithColumns.textItems, pageWithColumns).map(
4925
+ (block) => ({ block })
4926
+ )
4927
+ );
4928
+ } else {
4929
+ const allBlocks = columnIndexes.flatMap(
4930
+ (columnIndex) => buildBlocksForItems(
4931
+ pageWithColumns.textItems.filter(
4932
+ (item) => item.columnIndex === columnIndex
4933
+ ),
4934
+ pageWithColumns
4935
+ ).map((block) => ({ block, columnIndex }))
4936
+ );
4937
+ const fullWidthBlocks = allBlocks.filter(
4938
+ ({ block }) => isFullWidthBlock(block, pageWithColumns, detectedColumnCount)
4939
+ );
4940
+ const columnBlocks = allBlocks.filter(
4941
+ ({ block }) => !isFullWidthBlock(block, pageWithColumns, detectedColumnCount)
4942
+ );
4943
+ const topFullWidthBlocks = fullWidthBlocks.filter(({ block }) => {
4944
+ const rect = getTextItemsBoundingRect(block.flat());
4945
+ return (rect?.top ?? 0) < pageWithColumns.height * 0.3;
3555
4946
  });
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
4947
+ const bottomFullWidthBlocks = fullWidthBlocks.filter(({ block }) => {
4948
+ const rect = getTextItemsBoundingRect(block.flat());
4949
+ return (rect?.top ?? 0) >= pageWithColumns.height * 0.3;
3570
4950
  });
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
4951
+ blocks.push(
4952
+ ...topFullWidthBlocks.sort(compareBlocksByPosition),
4953
+ ...columnBlocks.sort(compareBlocksByColumnReadingOrder),
4954
+ ...bottomFullWidthBlocks.sort(compareBlocksByPosition)
4955
+ );
4956
+ }
4957
+ blocks.forEach(({ block, columnIndex }, indexInPage) => {
4958
+ units.push(
4959
+ buildTextUnit({
4960
+ block,
4961
+ columnIndex,
4962
+ indexInPage,
4963
+ includePositions,
4964
+ idPrefix,
4965
+ page: pageWithColumns,
4966
+ shouldNormalize
4967
+ })
4968
+ );
4969
+ });
4970
+ });
4971
+ if (!includeSources) {
4972
+ return units.map(({ source: _source, ...rest }) => rest);
4973
+ }
4974
+ return units;
4975
+ };
4976
+ var extractTextUnits = async (pdfDocument, options = {}) => {
4977
+ const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
4978
+ const pages = await extractPageTextItems(pdfDocument, {
4979
+ pages: resolvedOptions.pages,
4980
+ columnDetection: resolvedOptions.columnDetection
4981
+ });
4982
+ return extractTextUnitsFromPages(pages, resolvedOptions);
4983
+ };
4984
+ var extractPageTextItems = async (pdfDocument, options = {}) => {
4985
+ const pageNumbers = resolvePageNumbers(pdfDocument, options.pages);
4986
+ const columnDetection = options.columnDetection ?? DEFAULT_OPTIONS.columnDetection;
4987
+ const extractedPages = [];
4988
+ for (const pageNumber of pageNumbers) {
4989
+ const page = await pdfDocument.getPage(pageNumber);
4990
+ const viewport = page.getViewport({ scale: 1 });
4991
+ const textContent = await page.getTextContent();
4992
+ const textItems = [];
4993
+ textContent.items.forEach((item, index) => {
4994
+ if (!isTextItem(item) || item.str.length === 0) return;
4995
+ textItems.push({
4996
+ text: item.str,
4997
+ index,
4998
+ pageNumber,
4999
+ rect: getTextItemRect(item, viewport, pageNumber)
3583
5000
  });
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
5001
+ });
5002
+ extractedPages.push(assignColumnsToPage({
5003
+ pageNumber,
5004
+ width: viewport.width,
5005
+ height: viewport.height,
5006
+ textItems
5007
+ }, columnDetection));
5008
+ }
5009
+ return extractedPages;
5010
+ };
5011
+ var extractSentences = async (pdfDocument, options = {}) => {
5012
+ const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
5013
+ const pages = await extractPageTextItems(pdfDocument, {
5014
+ pages: resolvedOptions.pages,
5015
+ columnDetection: resolvedOptions.columnDetection
5016
+ });
5017
+ const textUnits = extractTextUnitsFromPages(pages, { ...resolvedOptions, includeSources: true });
5018
+ const sentences = [];
5019
+ let globalIndex = 0;
5020
+ textUnits.filter((unit) => resolvedOptions.includeTextUnitTypes.includes(unit.type)).forEach((unit) => {
5021
+ const page = pages.find((candidatePage) => candidatePage.pageNumber === unit.pageNumber);
5022
+ if (!page) return;
5023
+ const pageTextItemsByIndex = new Map(
5024
+ page.textItems.map((item) => [item.index, item])
5025
+ );
5026
+ const stream = buildPageTextStream(
5027
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5028
+ unit.source.textItemIndexes.map((textItemIndex) => pageTextItemsByIndex.get(textItemIndex)).filter((item) => item !== void 0)
5029
+ );
5030
+ if (!stream.text.trim()) return;
5031
+ const sentenceRanges = splitPdfSentences(
5032
+ stream.text,
5033
+ resolvedOptions.locale
5034
+ );
5035
+ sentenceRanges.forEach((sentenceRange, indexInPage) => {
5036
+ const textItemIndexes = getSentenceTextItemIndexes(
5037
+ stream.charItemIndexes,
5038
+ sentenceRange.start,
5039
+ sentenceRange.end
5040
+ );
5041
+ const position = resolvedOptions.includePositions ? getScaledPosition(page.textItems, textItemIndexes, page) : void 0;
5042
+ const id = `${resolvedOptions.idPrefix}p${page.pageNumber}-u${unit.indexInPage}-s${indexInPage}`;
5043
+ const rawText = sentenceRange.text;
5044
+ sentences.push({
5045
+ id,
5046
+ text: resolvedOptions.normalize ? normalizePdfSentenceText(rawText) : rawText,
5047
+ rawText,
5048
+ pageNumber: page.pageNumber,
5049
+ indexInPage,
5050
+ globalIndex,
5051
+ ...unit.columnIndex !== void 0 ? { columnIndex: unit.columnIndex } : {},
5052
+ ...position ? { position } : {},
5053
+ ...resolvedOptions.includeSources ? { source: { startOffset: sentenceRange.start, endOffset: sentenceRange.end, textItemIndexes } } : {}
3593
5054
  });
3594
- break;
3595
- }
5055
+ globalIndex += 1;
5056
+ });
5057
+ });
5058
+ return sentences;
5059
+ };
5060
+ var sentenceToHighlight = (sentence, options = {}) => {
5061
+ if (!sentence.position) {
5062
+ throw new Error(
5063
+ "Cannot convert sentence to highlight because it has no position. Call extractSentences with includePositions: true."
5064
+ );
3596
5065
  }
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;
5066
+ return {
5067
+ id: options.id ?? sentence.id,
5068
+ type: "text",
5069
+ content: { text: sentence.text },
5070
+ position: sentence.position
5071
+ };
5072
+ };
5073
+ var stripWhitespaceWithMap = (raw) => {
5074
+ let stripped = "";
5075
+ const map = [];
5076
+ for (let i = 0; i < raw.length; i += 1) {
5077
+ const c = raw[i];
5078
+ if (/\s/.test(c)) continue;
5079
+ let ch = c.toLowerCase();
5080
+ if (ch === "\u2018" || ch === "\u2019") ch = "'";
5081
+ else if (ch === "\u201C" || ch === "\u201D") ch = '"';
5082
+ stripped += ch;
5083
+ map.push(i);
5084
+ }
5085
+ return { stripped, map };
5086
+ };
5087
+ var boundedLevenshtein = (a, b, max) => {
5088
+ const al = a.length;
5089
+ const bl = b.length;
5090
+ if (Math.abs(al - bl) > max) return max + 1;
5091
+ let prev = new Array(bl + 1);
5092
+ let curr = new Array(bl + 1);
5093
+ for (let j = 0; j <= bl; j++) prev[j] = j;
5094
+ for (let i = 1; i <= al; i++) {
5095
+ curr[0] = i;
5096
+ let rowMin = curr[0];
5097
+ for (let j = 1; j <= bl; j++) {
5098
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
5099
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
5100
+ if (curr[j] < rowMin) rowMin = curr[j];
5101
+ }
5102
+ if (rowMin > max) return max + 1;
5103
+ [prev, curr] = [curr, prev];
3608
5104
  }
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);
5105
+ return prev[bl];
5106
+ };
5107
+ var fuzzyFind = (haystack, needle, maxDistance) => {
5108
+ const len = needle.length;
5109
+ if (len === 0) return -1;
5110
+ const anchor = needle.slice(0, Math.min(8, len));
5111
+ let best = -1;
5112
+ let bestDist = maxDistance + 1;
5113
+ let from = 0;
5114
+ while (true) {
5115
+ const at = haystack.indexOf(anchor, from);
5116
+ if (at === -1) break;
5117
+ for (let offset = -2; offset <= 2; offset++) {
5118
+ const start = at + offset;
5119
+ if (start < 0 || start + len > haystack.length) continue;
5120
+ const window2 = haystack.slice(start, start + len);
5121
+ const dist = boundedLevenshtein(needle, window2, bestDist - 1);
5122
+ if (dist < bestDist) {
5123
+ bestDist = dist;
5124
+ best = start;
5125
+ if (dist === 0) return best;
3640
5126
  }
3641
5127
  }
3642
- currentPage++;
3643
- options.onProgress?.(currentPage, totalPages);
5128
+ from = at + 1;
3644
5129
  }
3645
- return pdfDoc.save();
3646
- }
5130
+ return bestDist <= maxDistance ? best : -1;
5131
+ };
5132
+ var rectsForRange = (items, itemStarts, start, end, pageNumber) => {
5133
+ const rects = [];
5134
+ for (let k = 0; k < items.length; k++) {
5135
+ const itemStart = itemStarts[k];
5136
+ const item = items[k];
5137
+ const len = item.text.length;
5138
+ if (len === 0) continue;
5139
+ const itemEnd = itemStart + len;
5140
+ const a = Math.max(start, itemStart);
5141
+ const b = Math.min(end, itemEnd);
5142
+ if (b <= a) continue;
5143
+ const { left, top, width, height } = item.rect;
5144
+ if (!(width > 0) || !(height > 0)) continue;
5145
+ const s = (a - itemStart) / len;
5146
+ const e = (b - itemStart) / len;
5147
+ rects.push({
5148
+ left: left + s * width,
5149
+ top,
5150
+ width: (e - s) * width,
5151
+ height,
5152
+ pageNumber
5153
+ });
5154
+ }
5155
+ rects.sort((p, q) => p.top - q.top || p.left - q.left);
5156
+ const merged = [];
5157
+ for (const r of rects) {
5158
+ const last = merged[merged.length - 1];
5159
+ if (last && Math.abs(last.top - r.top) < Math.max(2, r.height * 0.4) && r.left - (last.left + last.width) < Math.max(4, r.height * 0.6)) {
5160
+ const right = Math.max(last.left + last.width, r.left + r.width);
5161
+ last.left = Math.min(last.left, r.left);
5162
+ last.width = right - last.left;
5163
+ last.height = Math.max(last.height, r.height);
5164
+ } else {
5165
+ merged.push({ ...r });
5166
+ }
5167
+ }
5168
+ return merged;
5169
+ };
5170
+ var getTextPosition = async (pdfDocument, query, options = {}) => {
5171
+ const { stripped: needle } = stripWhitespaceWithMap(query);
5172
+ if (!needle) return null;
5173
+ const allowFuzzy = options.fuzzy ?? true;
5174
+ const maxDistance = Math.max(2, Math.floor(needle.length * 0.15));
5175
+ const pages = await extractPageTextItems(pdfDocument, {
5176
+ pages: options.pages ?? "all"
5177
+ });
5178
+ for (const page of pages) {
5179
+ const items = page.textItems.filter((it) => it.text).map((it) => ({ text: it.text, rect: it.rect }));
5180
+ if (items.length === 0) continue;
5181
+ const itemStarts = [];
5182
+ let raw = "";
5183
+ for (const it of items) {
5184
+ itemStarts.push(raw.length);
5185
+ raw += it.text;
5186
+ }
5187
+ const { stripped, map } = stripWhitespaceWithMap(raw);
5188
+ let at = stripped.indexOf(needle);
5189
+ let confidence = "exact";
5190
+ if (at === -1 && allowFuzzy) {
5191
+ at = fuzzyFind(stripped, needle, maxDistance);
5192
+ confidence = "fuzzy";
5193
+ }
5194
+ if (at === -1) continue;
5195
+ const rawStart = map[at];
5196
+ const rawEnd = map[Math.min(at + needle.length - 1, map.length - 1)] + 1;
5197
+ const rects = rectsForRange(
5198
+ items,
5199
+ itemStarts,
5200
+ rawStart,
5201
+ rawEnd,
5202
+ page.pageNumber
5203
+ );
5204
+ if (rects.length === 0) continue;
5205
+ const boundingRect = get_bounding_rect_default(rects);
5206
+ return {
5207
+ position: {
5208
+ boundingRect: viewportToScaled(boundingRect, page),
5209
+ rects: rects.map((r) => viewportToScaled(r, page))
5210
+ },
5211
+ pageNumber: page.pageNumber,
5212
+ matchedText: raw.slice(rawStart, rawEnd),
5213
+ confidence
5214
+ };
5215
+ }
5216
+ return null;
5217
+ };
3647
5218
 
3648
5219
  // src/components/leftpanel/LeftPanel.tsx
3649
5220
  import React21, { useState as useState17, useMemo as useMemo3, useCallback as useCallback8, useRef as useRef19, useEffect as useEffect18 } from "react";
@@ -4078,13 +5649,13 @@ var OutlineItem = ({
4078
5649
  const defaultExpandIconStyle = {
4079
5650
  width: 12,
4080
5651
  height: 12,
4081
- color: "#64748b"
5652
+ color: "var(--lp-muted, #64748b)"
4082
5653
  };
4083
5654
  const defaultActiveIndicatorStyle = {
4084
5655
  width: 6,
4085
5656
  height: 6,
4086
5657
  borderRadius: "50%",
4087
- backgroundColor: "#3b82f6",
5658
+ backgroundColor: "var(--lp-accent, #3b82f6)",
4088
5659
  flexShrink: 0
4089
5660
  };
4090
5661
  const defaultTitleStyle = {
@@ -4095,23 +5666,23 @@ var OutlineItem = ({
4095
5666
  fontSize: "13px",
4096
5667
  fontWeight: 400,
4097
5668
  fontStyle: item.italic ? "italic" : "normal",
4098
- color: "#475569",
5669
+ color: "var(--lp-text, #475569)",
4099
5670
  lineHeight: 1.4
4100
5671
  };
4101
5672
  const defaultTitleActiveStyle = {
4102
5673
  fontWeight: 500,
4103
- color: "#1e40af"
5674
+ color: "var(--lp-accent, #1e40af)"
4104
5675
  };
4105
5676
  const defaultPageNumberStyle = {
4106
5677
  fontSize: "11px",
4107
- color: "#94a3b8",
5678
+ color: "var(--lp-muted, #94a3b8)",
4108
5679
  flexShrink: 0,
4109
5680
  fontVariantNumeric: "tabular-nums",
4110
5681
  minWidth: "20px",
4111
5682
  textAlign: "right"
4112
5683
  };
4113
5684
  const defaultPageNumberActiveStyle = {
4114
- color: "#3b82f6"
5685
+ color: "var(--lp-accent, #3b82f6)"
4115
5686
  };
4116
5687
  const defaultChildrenContainerStyle = {
4117
5688
  position: "relative"
@@ -4119,7 +5690,7 @@ var OutlineItem = ({
4119
5690
  const containerStyle = {
4120
5691
  ...defaultContainerStyle,
4121
5692
  ...styles?.container,
4122
- ...isHovered && !isActive ? { backgroundColor: "#f8fafc", ...styles?.containerHover } : {},
5693
+ ...isHovered && !isActive ? { backgroundColor: "var(--lp-hover, #f8fafc)", ...styles?.containerHover } : {},
4123
5694
  ...isActive ? styles?.containerActive : {}
4124
5695
  };
4125
5696
  const expandButtonStyle = {
@@ -4188,7 +5759,7 @@ var OutlineItem = ({
4188
5759
  },
4189
5760
  style: expandButtonStyle,
4190
5761
  onMouseEnter: (e) => {
4191
- e.currentTarget.style.backgroundColor = "#e5e7eb";
5762
+ e.currentTarget.style.backgroundColor = "var(--lp-hover, #e5e7eb)";
4192
5763
  },
4193
5764
  onMouseLeave: (e) => {
4194
5765
  e.currentTarget.style.backgroundColor = "transparent";
@@ -4245,13 +5816,13 @@ var DocumentOutline = ({
4245
5816
  const defaultLoadingSpinnerStyle = {
4246
5817
  width: 28,
4247
5818
  height: 28,
4248
- color: "#94a3b8",
5819
+ color: "var(--lp-muted, #94a3b8)",
4249
5820
  marginBottom: 12,
4250
5821
  animation: "spin 1s linear infinite"
4251
5822
  };
4252
5823
  const defaultLoadingTextStyle = {
4253
5824
  fontSize: 13,
4254
- color: "#64748b"
5825
+ color: "var(--lp-muted, #64748b)"
4255
5826
  };
4256
5827
  const defaultEmptyContainerStyle = {
4257
5828
  display: "flex",
@@ -4264,7 +5835,7 @@ var DocumentOutline = ({
4264
5835
  width: 56,
4265
5836
  height: 56,
4266
5837
  borderRadius: "50%",
4267
- backgroundColor: "#f1f5f9",
5838
+ backgroundColor: "var(--lp-hover, #f1f5f9)",
4268
5839
  display: "flex",
4269
5840
  alignItems: "center",
4270
5841
  justifyContent: "center",
@@ -4273,17 +5844,17 @@ var DocumentOutline = ({
4273
5844
  const defaultEmptyIconStyle = {
4274
5845
  width: 28,
4275
5846
  height: 28,
4276
- color: "#94a3b8"
5847
+ color: "var(--lp-muted, #94a3b8)"
4277
5848
  };
4278
5849
  const defaultEmptyTitleStyle = {
4279
5850
  fontSize: 14,
4280
5851
  fontWeight: 500,
4281
- color: "#475569",
5852
+ color: "var(--lp-text, #475569)",
4282
5853
  marginBottom: 4
4283
5854
  };
4284
5855
  const defaultEmptyDescriptionStyle = {
4285
5856
  fontSize: 13,
4286
- color: "#94a3b8",
5857
+ color: "var(--lp-muted, #94a3b8)",
4287
5858
  textAlign: "center"
4288
5859
  };
4289
5860
  const defaultContainerStyle = {
@@ -4395,7 +5966,7 @@ var ThumbnailItem = React19.memo(({
4395
5966
  width: "85%",
4396
5967
  aspectRatio: "8.5 / 11",
4397
5968
  // Standard page aspect ratio
4398
- backgroundColor: "#ffffff",
5969
+ backgroundColor: "var(--lp-bg, #ffffff)",
4399
5970
  borderRadius: "4px",
4400
5971
  overflow: "hidden",
4401
5972
  boxShadow: isActive ? "0 4px 12px rgba(59, 130, 246, 0.25)" : isHovered ? "0 4px 12px rgba(0, 0, 0, 0.12)" : "0 1px 3px rgba(0, 0, 0, 0.08)",
@@ -4405,7 +5976,7 @@ var ThumbnailItem = React19.memo(({
4405
5976
  marginTop: "6px",
4406
5977
  fontSize: "11px",
4407
5978
  fontWeight: 500,
4408
- color: isActive ? "#3b82f6" : "#6b7280",
5979
+ color: isActive ? "var(--lp-accent, #3b82f6)" : "var(--lp-muted, #6b7280)",
4409
5980
  transition: "color 0.15s ease"
4410
5981
  };
4411
5982
  return /* @__PURE__ */ React19.createElement(
@@ -4437,7 +6008,7 @@ var ThumbnailItem = React19.memo(({
4437
6008
  display: "flex",
4438
6009
  alignItems: "center",
4439
6010
  justifyContent: "center",
4440
- backgroundColor: "#f9fafb"
6011
+ backgroundColor: "var(--lp-hover, #f9fafb)"
4441
6012
  }
4442
6013
  },
4443
6014
  /* @__PURE__ */ React19.createElement(
@@ -4446,7 +6017,7 @@ var ThumbnailItem = React19.memo(({
4446
6017
  style: {
4447
6018
  width: 24,
4448
6019
  height: 24,
4449
- color: "#9ca3af",
6020
+ color: "var(--lp-muted, #9ca3af)",
4450
6021
  animation: "spin 1s linear infinite"
4451
6022
  }
4452
6023
  }
@@ -4487,7 +6058,7 @@ var ThumbnailItem = React19.memo(({
4487
6058
  display: "flex",
4488
6059
  alignItems: "center",
4489
6060
  justifyContent: "center",
4490
- backgroundColor: "#f9fafb"
6061
+ backgroundColor: "var(--lp-hover, #f9fafb)"
4491
6062
  }
4492
6063
  },
4493
6064
  /* @__PURE__ */ React19.createElement(
@@ -4496,7 +6067,7 @@ var ThumbnailItem = React19.memo(({
4496
6067
  style: {
4497
6068
  fontSize: 24,
4498
6069
  fontWeight: 300,
4499
- color: "#d1d5db"
6070
+ color: "var(--lp-muted, #d1d5db)"
4500
6071
  }
4501
6072
  },
4502
6073
  pageNumber
@@ -4553,7 +6124,7 @@ var ThumbnailPanel = ({
4553
6124
  padding: "48px 16px"
4554
6125
  }
4555
6126
  },
4556
- /* @__PURE__ */ React20.createElement("p", { style: { fontSize: 13, color: "#9ca3af" } }, "No pages to display")
6127
+ /* @__PURE__ */ React20.createElement("p", { style: { fontSize: 13, color: "var(--lp-muted, #9ca3af)" } }, "No pages to display")
4557
6128
  );
4558
6129
  }
4559
6130
  return /* @__PURE__ */ React20.createElement(
@@ -4627,6 +6198,14 @@ var defaultTheme = {
4627
6198
  mutedTextColor: "#6b7280",
4628
6199
  hoverBackgroundColor: "#f9fafb"
4629
6200
  };
6201
+ var defaultDarkTheme2 = {
6202
+ backgroundColor: "#1f1d1b",
6203
+ borderColor: "#3a3733",
6204
+ accentColor: "#7aa2f7",
6205
+ textColor: "#eae6e0",
6206
+ mutedTextColor: "#a8a29a",
6207
+ hoverBackgroundColor: "#2a2724"
6208
+ };
4630
6209
  var LeftPanel = ({
4631
6210
  pdfDocument,
4632
6211
  viewer = null,
@@ -4645,6 +6224,7 @@ var LeftPanel = ({
4645
6224
  onPageSelect,
4646
6225
  thumbnailWidth = 180,
4647
6226
  children,
6227
+ mode = "light",
4648
6228
  theme: userTheme,
4649
6229
  showFooter = true,
4650
6230
  showToggleButton = true,
@@ -4659,7 +6239,13 @@ var LeftPanel = ({
4659
6239
  outlineItemStyles,
4660
6240
  outlineItemClassNames
4661
6241
  }) => {
4662
- const theme = useMemo3(() => ({ ...defaultTheme, ...userTheme }), [userTheme]);
6242
+ const theme = useMemo3(
6243
+ () => ({
6244
+ ...mode === "dark" ? defaultDarkTheme2 : defaultTheme,
6245
+ ...userTheme
6246
+ }),
6247
+ [mode, userTheme]
6248
+ );
4663
6249
  const [internalIsOpen, setInternalIsOpen] = useState17(true);
4664
6250
  const [activeTab, setActiveTab] = useState17(defaultTab);
4665
6251
  const isOpen = controlledIsOpen !== void 0 ? controlledIsOpen : internalIsOpen;
@@ -4971,6 +6557,12 @@ var LeftPanel = ({
4971
6557
  )
4972
6558
  ));
4973
6559
  };
6560
+
6561
+ // src/index.ts
6562
+ var exportPdf = async (...args) => {
6563
+ const { exportPdf: exportPdf2 } = await import("./export-pdf-W2QGWADM.js");
6564
+ return exportPdf2(...args);
6565
+ };
4974
6566
  export {
4975
6567
  AreaHighlight,
4976
6568
  DocumentOutline,
@@ -4990,7 +6582,12 @@ export {
4990
6582
  ThumbnailItem,
4991
6583
  ThumbnailPanel,
4992
6584
  exportPdf,
6585
+ extractPageTextItems,
6586
+ extractSentences,
6587
+ extractTextUnits,
6588
+ getTextPosition,
4993
6589
  scaledPositionToViewport,
6590
+ sentenceToHighlight,
4994
6591
  useDocumentOutline,
4995
6592
  useHighlightContainerContext,
4996
6593
  useLeftPanelContext,