react-pdf-highlighter-plus 1.1.4 → 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
@@ -87,6 +87,323 @@ var scaledPositionToViewport = ({ boundingRect, rects, usePdfCoordinates }, view
87
87
  };
88
88
  };
89
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
+
90
407
  // src/lib/get-bounding-rect.ts
91
408
  var getBoundingRect = (clientRects) => {
92
409
  const rects = Array.from(clientRects).map((rect) => {
@@ -292,16 +609,6 @@ var getPagesFromRange = (range) => {
292
609
  }
293
610
  return pages;
294
611
  };
295
- var findOrCreateContainerLayer = (container, className) => {
296
- const doc = getDocument(container);
297
- let layer = container.querySelector(`.${className}`);
298
- if (!layer && container.children.length) {
299
- layer = doc.createElement("div");
300
- layer.className = className;
301
- container.appendChild(layer);
302
- }
303
- return layer;
304
- };
305
612
 
306
613
  // src/components/DrawingCanvas.tsx
307
614
  import React, {
@@ -1126,41 +1433,79 @@ var PDFViewer;
1126
1433
  var SCROLL_MARGIN = 10;
1127
1434
  var DEFAULT_SCALE_VALUE = "auto";
1128
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
+ };
1129
1474
  var defaultLightTheme = {
1130
1475
  mode: "light",
1131
1476
  containerBackgroundColor: "#e5e5e5",
1132
1477
  scrollbarThumbColor: "#9f9f9f",
1133
1478
  scrollbarTrackColor: "#d1d1d1",
1134
- darkModeInvertIntensity: 0.9
1479
+ darkModeInvertIntensity: 0.9,
1480
+ darkModeColors: DEFAULT_DARK_MODE_COLORS2
1135
1481
  };
1136
1482
  var defaultDarkTheme = {
1137
1483
  mode: "dark",
1138
1484
  containerBackgroundColor: "#3a3a3a",
1139
- // Lighter than PDF page (~#1a1a1a) for contrast
1485
+ // Lighter than PDF page for contrast
1140
1486
  scrollbarThumbColor: "#6b6b6b",
1141
1487
  scrollbarTrackColor: "#2c2c2c",
1142
- darkModeInvertIntensity: 0.9
1488
+ darkModeInvertIntensity: 0.9,
1489
+ darkModeColors: DEFAULT_DARK_MODE_COLORS2
1143
1490
  };
1144
- var findOrCreateHighlightLayer = (textLayer) => {
1145
- return findOrCreateContainerLayer(
1146
- textLayer,
1147
- "PdfHighlighter__highlight-layer"
1148
- );
1149
- };
1150
- var findOrCreateNoteLayer = (textLayer) => {
1151
- const pageLayer = textLayer.closest(".page");
1152
- const container = pageLayer;
1153
- if (!container) return null;
1154
- const doc = getDocument(container);
1155
- let layer = Array.from(container.children).find(
1156
- (child) => child.classList.contains("PdfHighlighter__note-layer")
1157
- );
1158
- if (!layer && container.children.length) {
1159
- layer = doc.createElement("div");
1160
- layer.className = "PdfHighlighter__note-layer";
1161
- container.appendChild(layer);
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;
1162
1507
  }
1163
- return layer;
1508
+ return binding;
1164
1509
  };
1165
1510
  var isFreetextHighlight = (highlight) => "type" in highlight && highlight.type === "freetext";
1166
1511
  var disableTextSelection = (viewer, flag) => {
@@ -1170,6 +1515,9 @@ var PdfHighlighter = ({
1170
1515
  highlights,
1171
1516
  onScrollAway,
1172
1517
  pdfScaleValue = DEFAULT_SCALE_VALUE,
1518
+ onZoomChange,
1519
+ initialPage,
1520
+ onPageChange,
1173
1521
  onSelection: onSelectionFinished,
1174
1522
  onCreateGhostHighlight,
1175
1523
  onRemoveGhostHighlight,
@@ -1189,12 +1537,12 @@ var PdfHighlighter = ({
1189
1537
  enableDrawingMode,
1190
1538
  onDrawingComplete,
1191
1539
  onDrawingCancel,
1192
- drawingStrokeColor = "#000000",
1540
+ drawingStrokeColor: drawingStrokeColorProp,
1193
1541
  drawingStrokeWidth = 3,
1194
1542
  enableShapeMode,
1195
1543
  onShapeComplete,
1196
1544
  onShapeCancel,
1197
- shapeStrokeColor = "#000000",
1545
+ shapeStrokeColor: shapeStrokeColorProp,
1198
1546
  shapeStrokeWidth = 2,
1199
1547
  theme: userTheme
1200
1548
  }) => {
@@ -1203,6 +1551,18 @@ var PdfHighlighter = ({
1203
1551
  const defaults = mode === "light" ? defaultLightTheme : defaultDarkTheme;
1204
1552
  return { ...defaults, ...userTheme, mode };
1205
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;
1206
1566
  const [tip, setTip] = useState5(null);
1207
1567
  const [isViewerReady, setIsViewerReady] = useState5(false);
1208
1568
  const containerNodeRef = useRef5(null);
@@ -1235,10 +1595,17 @@ var PdfHighlighter = ({
1235
1595
  );
1236
1596
  const findControllerRef = useRef5(null);
1237
1597
  const viewerRef = useRef5(null);
1598
+ const prevDocRef = useRef5(null);
1599
+ const lastRenderKeyRef = useRef5(null);
1238
1600
  highlightsRef.current = highlights;
1239
1601
  childrenRef.current = children;
1240
1602
  useLayoutEffect2(() => {
1241
1603
  if (!containerNodeRef.current) return;
1604
+ const renderKey = `${pdfDocument.fingerprints ?? pdfDocument.numPages}::${recolorKey}`;
1605
+ if (lastRenderKeyRef.current === renderKey) {
1606
+ console.log("[PdfHighlighter] skip duplicate setDocument", renderKey);
1607
+ return;
1608
+ }
1242
1609
  findControllerRef.current = findControllerRef.current || new PDFFindController({
1243
1610
  eventBus: eventBusRef.current,
1244
1611
  linkService: linkServiceRef.current
@@ -1251,11 +1618,51 @@ var PdfHighlighter = ({
1251
1618
  removePageBorders: true,
1252
1619
  linkService: linkServiceRef.current
1253
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;
1254
1661
  viewerRef.current.setDocument(pdfDocument);
1255
1662
  linkServiceRef.current.setDocument(pdfDocument);
1256
1663
  linkServiceRef.current.setViewer(viewerRef.current);
1257
1664
  setIsViewerReady(true);
1258
- }, [pdfDocument]);
1665
+ }, [pdfDocument, recolorKey]);
1259
1666
  useLayoutEffect2(() => {
1260
1667
  if (!containerNodeRef.current) return;
1261
1668
  resizeObserverRef.current = new ResizeObserver(handleScaleValue);
@@ -1282,6 +1689,222 @@ var PdfHighlighter = ({
1282
1689
  }
1283
1690
  };
1284
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]);
1285
1908
  const handleScroll = () => {
1286
1909
  onScrollAway && onScrollAway();
1287
1910
  scrolledToHighlightIdRef.current = null;
@@ -1449,34 +2072,25 @@ var PdfHighlighter = ({
1449
2072
  const { textLayer } = viewerRef.current.getPageView(pageNumber - 1) || {};
1450
2073
  if (!textLayer) continue;
1451
2074
  const textLayerDiv = textLayer.div;
1452
- const highlightLayer = findOrCreateHighlightLayer(textLayerDiv);
1453
- const noteLayer = findOrCreateNoteLayer(textLayerDiv);
1454
- if (highlightLayer) {
1455
- let highlightBindings = highlightBindingsRef.current[pageNumber];
1456
- if (!highlightBindings?.container?.isConnected) {
1457
- highlightBindings = {
1458
- reactRoot: createRoot(highlightLayer),
1459
- container: highlightLayer,
1460
- textLayer: textLayerDiv
1461
- };
1462
- highlightBindingsRef.current[pageNumber] = highlightBindings;
1463
- }
1464
- renderHighlightLayer(
1465
- highlightBindings,
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,
1466
2090
  pageNumber,
1467
- (highlight) => !isFreetextHighlight(highlight)
2091
+ pageEl,
2092
+ "PdfHighlighter__note-layer"
1468
2093
  );
1469
- }
1470
- if (noteLayer) {
1471
- let noteBindings = noteBindingsRef.current[pageNumber];
1472
- if (!noteBindings?.container?.isConnected) {
1473
- noteBindings = {
1474
- reactRoot: createRoot(noteLayer),
1475
- container: noteLayer,
1476
- textLayer: textLayerDiv
1477
- };
1478
- noteBindingsRef.current[pageNumber] = noteBindings;
1479
- }
1480
2094
  renderHighlightLayer(noteBindings, pageNumber, isFreetextHighlight);
1481
2095
  }
1482
2096
  }
@@ -1532,25 +2146,28 @@ var PdfHighlighter = ({
1532
2146
  const scrollToHighlight = (highlight) => {
1533
2147
  const { boundingRect, usePdfCoordinates } = highlight.position;
1534
2148
  const pageNumber = boundingRect.pageNumber;
1535
- viewerRef.current.container.removeEventListener("scroll", handleScroll);
1536
- const pageViewport = viewerRef.current.getPageView(
1537
- pageNumber - 1
1538
- ).viewport;
1539
- viewerRef.current.scrollPageIntoView({
1540
- pageNumber,
1541
- destArray: [
1542
- null,
1543
- // null since we pass pageNumber already as an arg
1544
- { name: "XYZ" },
1545
- ...pageViewport.convertToPdfPoint(
1546
- 0,
1547
- // Default x coord
1548
- scaledToViewport(boundingRect, pageViewport, usePdfCoordinates).top - SCROLL_MARGIN
1549
- ),
1550
- 0
1551
- // Default z coord
1552
- ]
1553
- });
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"
2167
+ });
2168
+ } else {
2169
+ viewer.scrollPageIntoView({ pageNumber });
2170
+ }
1554
2171
  scrolledToHighlightIdRef.current = highlight.id;
1555
2172
  scheduleRenderHighlightLayers();
1556
2173
  resumeScrollAwayListenerAfterNavigation();
@@ -1716,17 +2333,6 @@ var PdfHighlighter = ({
1716
2333
  .PdfHighlighter::-webkit-scrollbar-track-piece {
1717
2334
  background-color: ${resolvedTheme.scrollbarTrackColor};
1718
2335
  }
1719
- ${resolvedTheme.mode === "dark" ? `
1720
- .PdfHighlighter--dark .page {
1721
- filter: invert(${resolvedTheme.darkModeInvertIntensity}) hue-rotate(180deg) brightness(1.05);
1722
- }
1723
- .PdfHighlighter--dark .PdfHighlighter__highlight-layer {
1724
- filter: invert(${resolvedTheme.darkModeInvertIntensity}) hue-rotate(180deg) brightness(0.95);
1725
- }
1726
- .PdfHighlighter--dark .PdfHighlighter__note-layer {
1727
- filter: invert(${resolvedTheme.darkModeInvertIntensity}) hue-rotate(180deg) brightness(0.95);
1728
- }
1729
- ` : ""}
1730
2336
  `),
1731
2337
  isViewerReady && /* @__PURE__ */ React6.createElement(
1732
2338
  TipContainer,
@@ -1965,7 +2571,7 @@ var TextHighlight = ({
1965
2571
  const getPartStyle = (rect) => {
1966
2572
  const baseStyle = { ...rect, ...style };
1967
2573
  if (highlightStyle === "highlight") {
1968
- baseStyle.backgroundColor = highlightColor;
2574
+ baseStyle.backgroundColor = `color-mix(in srgb, ${highlightColor} var(--hl-fill-alpha, 100%), transparent)`;
1969
2575
  } else {
1970
2576
  baseStyle.backgroundColor = "transparent";
1971
2577
  baseStyle.color = highlightColor;
@@ -2286,7 +2892,7 @@ var AreaHighlight = ({
2286
2892
  const key = `${highlight.position.boundingRect.width}${highlight.position.boundingRect.height}${highlight.position.boundingRect.left}${highlight.position.boundingRect.top}`;
2287
2893
  const mergedStyle = {
2288
2894
  ...style,
2289
- backgroundColor: highlightColor
2895
+ backgroundColor: `color-mix(in srgb, ${highlightColor} var(--hl-fill-alpha, 100%), transparent)`
2290
2896
  };
2291
2897
  const handleCopy = async (event) => {
2292
2898
  event.stopPropagation();
@@ -3586,7 +4192,40 @@ var ShapeHighlight = ({
3586
4192
  // src/components/PdfLoader.tsx
3587
4193
  import React16, { useEffect as useEffect12, useRef as useRef14, useState as useState11 } from "react";
3588
4194
  import { GlobalWorkerOptions, getDocument as getDocument2 } from "pdfjs-dist";
3589
- var DEFAULT_BEFORE_LOAD = (progress) => /* @__PURE__ */ React16.createElement("div", { style: { color: "black" } }, "Loading ", Math.floor(progress.loaded / progress.total * 100), "%");
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
+ };
3590
4229
  var DEFAULT_ERROR_MESSAGE = (error) => /* @__PURE__ */ React16.createElement("div", { style: { color: "black" } }, error.message);
3591
4230
  var DEFAULT_ON_ERROR = (error) => {
3592
4231
  throw new Error(`Error loading PDF document: ${error.message}!`);
@@ -3595,44 +4234,138 @@ var DEFAULT_WORKER_SRC = new URL(
3595
4234
  "pdfjs-dist/build/pdf.worker.min.mjs",
3596
4235
  import.meta.url
3597
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
+ };
3598
4280
  var PdfLoader = ({
3599
4281
  document: document2,
3600
4282
  beforeLoad = DEFAULT_BEFORE_LOAD,
3601
4283
  errorMessage = DEFAULT_ERROR_MESSAGE,
3602
4284
  children,
3603
4285
  onError = DEFAULT_ON_ERROR,
3604
- workerSrc = DEFAULT_WORKER_SRC
4286
+ workerSrc = DEFAULT_WORKER_SRC,
4287
+ disableAutoFetch = true,
4288
+ disableStream,
4289
+ rangeChunkSize,
4290
+ withCredentials,
4291
+ httpHeaders,
4292
+ enableCache = true
3605
4293
  }) => {
3606
- const pdfLoadingTaskRef = useRef14(null);
3607
- const pdfDocumentRef = useRef14(null);
4294
+ const [pdfDocument, setPdfDocument] = useState11(null);
3608
4295
  const [error, setError] = useState11(null);
3609
4296
  const [loadingProgress, setLoadingProgress] = useState11(null);
4297
+ const onErrorRef = useRef14(onError);
4298
+ onErrorRef.current = onError;
4299
+ const httpHeadersKey = httpHeaders ? JSON.stringify(httpHeaders) : "";
3610
4300
  useEffect12(() => {
3611
4301
  GlobalWorkerOptions.workerSrc = workerSrc;
3612
- pdfLoadingTaskRef.current = getDocument2(document2);
3613
- pdfLoadingTaskRef.current.onProgress = (progress) => {
3614
- setLoadingProgress(progress.loaded > progress.total ? null : progress);
3615
- };
3616
- pdfLoadingTaskRef.current.promise.then((pdfDocument) => {
3617
- pdfDocumentRef.current = pdfDocument;
3618
- }).catch((error2) => {
3619
- if (error2.message != "Worker was destroyed") {
3620
- setError(error2);
3621
- 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();
3622
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);
3623
4343
  }).finally(() => {
3624
- setLoadingProgress(null);
4344
+ if (!cancelled) setLoadingProgress(null);
3625
4345
  });
3626
4346
  return () => {
3627
- if (pdfLoadingTaskRef.current) {
3628
- pdfLoadingTaskRef.current.destroy();
3629
- }
3630
- if (pdfDocumentRef.current) {
3631
- 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();
3632
4354
  }
3633
4355
  };
3634
- }, [document2]);
3635
- 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);
3636
4369
  };
3637
4370
 
3638
4371
  // src/lib/extract-sentences.ts
@@ -4337,6 +5070,151 @@ var sentenceToHighlight = (sentence, options = {}) => {
4337
5070
  position: sentence.position
4338
5071
  };
4339
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];
5104
+ }
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;
5126
+ }
5127
+ }
5128
+ from = at + 1;
5129
+ }
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
+ };
4340
5218
 
4341
5219
  // src/components/leftpanel/LeftPanel.tsx
4342
5220
  import React21, { useState as useState17, useMemo as useMemo3, useCallback as useCallback8, useRef as useRef19, useEffect as useEffect18 } from "react";
@@ -4771,13 +5649,13 @@ var OutlineItem = ({
4771
5649
  const defaultExpandIconStyle = {
4772
5650
  width: 12,
4773
5651
  height: 12,
4774
- color: "#64748b"
5652
+ color: "var(--lp-muted, #64748b)"
4775
5653
  };
4776
5654
  const defaultActiveIndicatorStyle = {
4777
5655
  width: 6,
4778
5656
  height: 6,
4779
5657
  borderRadius: "50%",
4780
- backgroundColor: "#3b82f6",
5658
+ backgroundColor: "var(--lp-accent, #3b82f6)",
4781
5659
  flexShrink: 0
4782
5660
  };
4783
5661
  const defaultTitleStyle = {
@@ -4788,23 +5666,23 @@ var OutlineItem = ({
4788
5666
  fontSize: "13px",
4789
5667
  fontWeight: 400,
4790
5668
  fontStyle: item.italic ? "italic" : "normal",
4791
- color: "#475569",
5669
+ color: "var(--lp-text, #475569)",
4792
5670
  lineHeight: 1.4
4793
5671
  };
4794
5672
  const defaultTitleActiveStyle = {
4795
5673
  fontWeight: 500,
4796
- color: "#1e40af"
5674
+ color: "var(--lp-accent, #1e40af)"
4797
5675
  };
4798
5676
  const defaultPageNumberStyle = {
4799
5677
  fontSize: "11px",
4800
- color: "#94a3b8",
5678
+ color: "var(--lp-muted, #94a3b8)",
4801
5679
  flexShrink: 0,
4802
5680
  fontVariantNumeric: "tabular-nums",
4803
5681
  minWidth: "20px",
4804
5682
  textAlign: "right"
4805
5683
  };
4806
5684
  const defaultPageNumberActiveStyle = {
4807
- color: "#3b82f6"
5685
+ color: "var(--lp-accent, #3b82f6)"
4808
5686
  };
4809
5687
  const defaultChildrenContainerStyle = {
4810
5688
  position: "relative"
@@ -4812,7 +5690,7 @@ var OutlineItem = ({
4812
5690
  const containerStyle = {
4813
5691
  ...defaultContainerStyle,
4814
5692
  ...styles?.container,
4815
- ...isHovered && !isActive ? { backgroundColor: "#f8fafc", ...styles?.containerHover } : {},
5693
+ ...isHovered && !isActive ? { backgroundColor: "var(--lp-hover, #f8fafc)", ...styles?.containerHover } : {},
4816
5694
  ...isActive ? styles?.containerActive : {}
4817
5695
  };
4818
5696
  const expandButtonStyle = {
@@ -4881,7 +5759,7 @@ var OutlineItem = ({
4881
5759
  },
4882
5760
  style: expandButtonStyle,
4883
5761
  onMouseEnter: (e) => {
4884
- e.currentTarget.style.backgroundColor = "#e5e7eb";
5762
+ e.currentTarget.style.backgroundColor = "var(--lp-hover, #e5e7eb)";
4885
5763
  },
4886
5764
  onMouseLeave: (e) => {
4887
5765
  e.currentTarget.style.backgroundColor = "transparent";
@@ -4938,13 +5816,13 @@ var DocumentOutline = ({
4938
5816
  const defaultLoadingSpinnerStyle = {
4939
5817
  width: 28,
4940
5818
  height: 28,
4941
- color: "#94a3b8",
5819
+ color: "var(--lp-muted, #94a3b8)",
4942
5820
  marginBottom: 12,
4943
5821
  animation: "spin 1s linear infinite"
4944
5822
  };
4945
5823
  const defaultLoadingTextStyle = {
4946
5824
  fontSize: 13,
4947
- color: "#64748b"
5825
+ color: "var(--lp-muted, #64748b)"
4948
5826
  };
4949
5827
  const defaultEmptyContainerStyle = {
4950
5828
  display: "flex",
@@ -4957,7 +5835,7 @@ var DocumentOutline = ({
4957
5835
  width: 56,
4958
5836
  height: 56,
4959
5837
  borderRadius: "50%",
4960
- backgroundColor: "#f1f5f9",
5838
+ backgroundColor: "var(--lp-hover, #f1f5f9)",
4961
5839
  display: "flex",
4962
5840
  alignItems: "center",
4963
5841
  justifyContent: "center",
@@ -4966,17 +5844,17 @@ var DocumentOutline = ({
4966
5844
  const defaultEmptyIconStyle = {
4967
5845
  width: 28,
4968
5846
  height: 28,
4969
- color: "#94a3b8"
5847
+ color: "var(--lp-muted, #94a3b8)"
4970
5848
  };
4971
5849
  const defaultEmptyTitleStyle = {
4972
5850
  fontSize: 14,
4973
5851
  fontWeight: 500,
4974
- color: "#475569",
5852
+ color: "var(--lp-text, #475569)",
4975
5853
  marginBottom: 4
4976
5854
  };
4977
5855
  const defaultEmptyDescriptionStyle = {
4978
5856
  fontSize: 13,
4979
- color: "#94a3b8",
5857
+ color: "var(--lp-muted, #94a3b8)",
4980
5858
  textAlign: "center"
4981
5859
  };
4982
5860
  const defaultContainerStyle = {
@@ -5088,7 +5966,7 @@ var ThumbnailItem = React19.memo(({
5088
5966
  width: "85%",
5089
5967
  aspectRatio: "8.5 / 11",
5090
5968
  // Standard page aspect ratio
5091
- backgroundColor: "#ffffff",
5969
+ backgroundColor: "var(--lp-bg, #ffffff)",
5092
5970
  borderRadius: "4px",
5093
5971
  overflow: "hidden",
5094
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)",
@@ -5098,7 +5976,7 @@ var ThumbnailItem = React19.memo(({
5098
5976
  marginTop: "6px",
5099
5977
  fontSize: "11px",
5100
5978
  fontWeight: 500,
5101
- color: isActive ? "#3b82f6" : "#6b7280",
5979
+ color: isActive ? "var(--lp-accent, #3b82f6)" : "var(--lp-muted, #6b7280)",
5102
5980
  transition: "color 0.15s ease"
5103
5981
  };
5104
5982
  return /* @__PURE__ */ React19.createElement(
@@ -5130,7 +6008,7 @@ var ThumbnailItem = React19.memo(({
5130
6008
  display: "flex",
5131
6009
  alignItems: "center",
5132
6010
  justifyContent: "center",
5133
- backgroundColor: "#f9fafb"
6011
+ backgroundColor: "var(--lp-hover, #f9fafb)"
5134
6012
  }
5135
6013
  },
5136
6014
  /* @__PURE__ */ React19.createElement(
@@ -5139,7 +6017,7 @@ var ThumbnailItem = React19.memo(({
5139
6017
  style: {
5140
6018
  width: 24,
5141
6019
  height: 24,
5142
- color: "#9ca3af",
6020
+ color: "var(--lp-muted, #9ca3af)",
5143
6021
  animation: "spin 1s linear infinite"
5144
6022
  }
5145
6023
  }
@@ -5180,7 +6058,7 @@ var ThumbnailItem = React19.memo(({
5180
6058
  display: "flex",
5181
6059
  alignItems: "center",
5182
6060
  justifyContent: "center",
5183
- backgroundColor: "#f9fafb"
6061
+ backgroundColor: "var(--lp-hover, #f9fafb)"
5184
6062
  }
5185
6063
  },
5186
6064
  /* @__PURE__ */ React19.createElement(
@@ -5189,7 +6067,7 @@ var ThumbnailItem = React19.memo(({
5189
6067
  style: {
5190
6068
  fontSize: 24,
5191
6069
  fontWeight: 300,
5192
- color: "#d1d5db"
6070
+ color: "var(--lp-muted, #d1d5db)"
5193
6071
  }
5194
6072
  },
5195
6073
  pageNumber
@@ -5246,7 +6124,7 @@ var ThumbnailPanel = ({
5246
6124
  padding: "48px 16px"
5247
6125
  }
5248
6126
  },
5249
- /* @__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")
5250
6128
  );
5251
6129
  }
5252
6130
  return /* @__PURE__ */ React20.createElement(
@@ -5320,6 +6198,14 @@ var defaultTheme = {
5320
6198
  mutedTextColor: "#6b7280",
5321
6199
  hoverBackgroundColor: "#f9fafb"
5322
6200
  };
6201
+ var defaultDarkTheme2 = {
6202
+ backgroundColor: "#1f1d1b",
6203
+ borderColor: "#3a3733",
6204
+ accentColor: "#7aa2f7",
6205
+ textColor: "#eae6e0",
6206
+ mutedTextColor: "#a8a29a",
6207
+ hoverBackgroundColor: "#2a2724"
6208
+ };
5323
6209
  var LeftPanel = ({
5324
6210
  pdfDocument,
5325
6211
  viewer = null,
@@ -5338,6 +6224,7 @@ var LeftPanel = ({
5338
6224
  onPageSelect,
5339
6225
  thumbnailWidth = 180,
5340
6226
  children,
6227
+ mode = "light",
5341
6228
  theme: userTheme,
5342
6229
  showFooter = true,
5343
6230
  showToggleButton = true,
@@ -5352,7 +6239,13 @@ var LeftPanel = ({
5352
6239
  outlineItemStyles,
5353
6240
  outlineItemClassNames
5354
6241
  }) => {
5355
- const theme = useMemo3(() => ({ ...defaultTheme, ...userTheme }), [userTheme]);
6242
+ const theme = useMemo3(
6243
+ () => ({
6244
+ ...mode === "dark" ? defaultDarkTheme2 : defaultTheme,
6245
+ ...userTheme
6246
+ }),
6247
+ [mode, userTheme]
6248
+ );
5356
6249
  const [internalIsOpen, setInternalIsOpen] = useState17(true);
5357
6250
  const [activeTab, setActiveTab] = useState17(defaultTab);
5358
6251
  const isOpen = controlledIsOpen !== void 0 ? controlledIsOpen : internalIsOpen;
@@ -5692,6 +6585,7 @@ export {
5692
6585
  extractPageTextItems,
5693
6586
  extractSentences,
5694
6587
  extractTextUnits,
6588
+ getTextPosition,
5695
6589
  scaledPositionToViewport,
5696
6590
  sentenceToHighlight,
5697
6591
  useDocumentOutline,