@anaralabs/lector 3.5.0 → 3.5.2

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/README.md CHANGED
@@ -1,5 +1,4 @@
1
1
  <p align="center">
2
- <!-- <h1 align="center"><img src="https://www.anara.ai/icons/logo.png" width="196" /></h1> -->
3
2
  <p align="center">
4
3
  <i>Simple primitives to compose powerful PDF viewing experiences.<br>powered by <code><a href="https://mozilla.github.io/pdf.js/">PDF.js</a></code> and <code><a href="https://reactjs.org/">React</a></code></i>
5
4
  </p>
@@ -39,17 +39,18 @@ declare interface Annotation {
39
39
  export { Annotation }
40
40
  export { Annotation as Annotation_alias_1 }
41
41
 
42
- export declare const AnnotationHighlightLayer: ({ className, style, renderTooltipContent, focusedAnnotationId, onAnnotationClick, }: AnnotationHighlightLayerProps) => JSX.Element;
42
+ export declare const AnnotationHighlightLayer: ({ className, style, renderTooltipContent, tooltipClassName, focusedAnnotationId, onAnnotationClick, }: AnnotationHighlightLayerProps) => JSX.Element;
43
43
 
44
44
  declare interface AnnotationHighlightLayerProps {
45
45
  className?: string;
46
46
  style?: React.CSSProperties;
47
- renderTooltipContent?: (props: {
47
+ renderTooltipContent: (props: {
48
48
  annotation: Annotation;
49
49
  onClose: () => void;
50
50
  }) => React.ReactNode;
51
51
  focusedAnnotationId?: string;
52
52
  onAnnotationClick?: (annotation: Annotation) => void;
53
+ tooltipClassName?: string;
53
54
  }
54
55
 
55
56
  /**
@@ -89,7 +90,7 @@ declare interface AnnotationState {
89
90
  setAnnotations: (annotations: Annotation[]) => void;
90
91
  }
91
92
 
92
- export declare const AnnotationTooltip: ({ annotation, children, tooltipContent, onOpenChange, isOpen: controlledIsOpen, }: AnnotationTooltipProps) => JSX.Element;
93
+ export declare const AnnotationTooltip: ({ annotation, children, tooltipContent, onOpenChange, className, isOpen: controlledIsOpen, }: AnnotationTooltipProps) => JSX.Element;
93
94
 
94
95
  declare interface AnnotationTooltipProps {
95
96
  annotation: Annotation;
@@ -97,6 +98,7 @@ declare interface AnnotationTooltipProps {
97
98
  tooltipContent: React.ReactNode;
98
99
  onOpenChange?: (open: boolean) => void;
99
100
  isOpen?: boolean;
101
+ className?: string;
100
102
  }
101
103
 
102
104
  /**
@@ -328,8 +330,6 @@ export declare interface SearchResults {
328
330
  hasMoreResults: boolean;
329
331
  }
330
332
 
331
- declare type Selection_2 = Omit<CollapsibleSelection, "isCollapsed">;
332
-
333
333
  export declare const SelectionTooltip: ({ children }: SelectionTooltipProps) => JSX.Element;
334
334
 
335
335
  declare interface SelectionTooltipProps {
@@ -362,24 +362,6 @@ export declare const Thumbnails: ({ children, ...props }: HTMLProps<HTMLDivEleme
362
362
 
363
363
  export declare const TotalPages: ({ ...props }: HTMLProps<HTMLDivElement>) => JSX.Element;
364
364
 
365
- export declare const useAnnotationActions: ({ annotation, onClose, }: UseAnnotationActionsProps) => UseAnnotationActionsReturn;
366
-
367
- declare interface UseAnnotationActionsProps {
368
- annotation: Annotation;
369
- onClose?: () => void;
370
- }
371
-
372
- declare interface UseAnnotationActionsReturn {
373
- comment: string;
374
- isEditing: boolean;
375
- setComment: (comment: string) => void;
376
- setIsEditing: (isEditing: boolean) => void;
377
- handleSaveComment: () => void;
378
- handleColorChange: (color: string) => void;
379
- handleCancelEdit: () => void;
380
- colors: string[];
381
- }
382
-
383
365
  declare const useAnnotations: UseBoundStore<StoreApi<AnnotationState>>;
384
366
  export { useAnnotations }
385
367
  export { useAnnotations as useAnnotations_alias_1 }
@@ -458,8 +440,12 @@ export declare const useSearch: () => {
458
440
  };
459
441
 
460
442
  export declare const useSelectionDimensions: () => {
461
- getDimension: () => CollapsibleSelection | undefined;
462
- getSelection: () => Selection_2;
443
+ getDimension: () => {
444
+ highlights: HighlightRect[];
445
+ text: string;
446
+ isCollapsed: boolean;
447
+ } | undefined;
448
+ getSelection: () => CollapsibleSelection;
463
449
  };
464
450
 
465
451
  export declare const ZoomIn: ({ ...props }: HTMLProps<HTMLButtonElement>) => JSX.Element;
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import React, { createContext, forwardRef, useCallback, useContext, useState, useRef, useEffect, cloneElement, createRef, useLayoutEffect } from 'react';
4
- import { useFloating, autoUpdate, offset, shift, useDismiss, useInteractions, flip } from '@floating-ui/react';
4
+ import { createPortal } from 'react-dom';
5
+ import { useFloating, autoUpdate, offset, shift, useDismiss, useInteractions, inline, flip } from '@floating-ui/react';
5
6
  import { useVirtualizer, elementScroll, debounce } from '@tanstack/react-virtual';
6
7
  import { create, useStore, createStore } from 'zustand';
7
8
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
@@ -150,6 +151,7 @@ var useAnnotationTooltip = ({
150
151
  const isNewAnnotation = Date.now() - new Date(annotation.createdAt).getTime() < 1e3;
151
152
  const [isOpen, setIsOpen] = useState(isNewAnnotation);
152
153
  const viewportRef = usePdf((state) => state.viewportRef);
154
+ const scale = usePdf((state) => state.zoom);
153
155
  const {
154
156
  refs,
155
157
  floatingStyles,
@@ -163,9 +165,11 @@ var useAnnotationTooltip = ({
163
165
  },
164
166
  whileElementsMounted: autoUpdate,
165
167
  middleware: [
168
+ inline(),
166
169
  offset(10),
167
170
  flip({
168
- crossAxis: false
171
+ crossAxis: false,
172
+ fallbackAxisSideDirection: "end"
169
173
  }),
170
174
  shift({ padding: 8 })
171
175
  ]
@@ -174,32 +178,56 @@ var useAnnotationTooltip = ({
174
178
  const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);
175
179
  const updateTooltipPosition = useCallback(() => {
176
180
  if (!annotation.highlights.length) return;
177
- const lastHighlight = annotation.highlights[annotation.highlights.length - 1];
181
+ const highlightRects = annotation.highlights;
182
+ let minLeft = Infinity;
183
+ let maxRight = -Infinity;
184
+ let minTop = Infinity;
185
+ let maxBottom = -Infinity;
178
186
  refs.setReference({
179
- getBoundingClientRect: () => {
187
+ getBoundingClientRect() {
180
188
  const viewportElement = viewportRef.current;
181
- if (!viewportElement || !lastHighlight) return defaultRect;
189
+ if (!viewportElement) return defaultRect;
182
190
  const pageElement = viewportElement.querySelector(`[data-page-number="${annotation.pageNumber}"]`);
183
191
  if (!pageElement) return defaultRect;
184
192
  const pageRect = pageElement.getBoundingClientRect();
185
- const left = pageRect.left + lastHighlight.left;
186
- const top = pageRect.top + lastHighlight.top;
187
- const width = lastHighlight.width;
188
- const height = lastHighlight.height;
189
- return {
193
+ highlightRects.forEach((highlight) => {
194
+ const scaledLeft = highlight.left * scale;
195
+ const scaledWidth = highlight.width * scale;
196
+ const scaledTop = highlight.top * scale;
197
+ const scaledHeight = highlight.height * scale;
198
+ const left = pageRect.left + scaledLeft;
199
+ const right = left + scaledWidth;
200
+ const top = pageRect.top + scaledTop;
201
+ const bottom = top + scaledHeight;
202
+ minLeft = Math.min(minLeft, left);
203
+ maxRight = Math.max(maxRight, right);
204
+ minTop = Math.min(minTop, top);
205
+ maxBottom = Math.max(maxBottom, bottom);
206
+ });
207
+ const width = maxRight - minLeft;
208
+ const height = maxBottom - minTop;
209
+ const centerX = minLeft + width / 2;
210
+ const centerY = minTop + height / 2;
211
+ const rect = {
190
212
  width,
191
213
  height,
192
- x: left,
193
- y: top,
194
- top,
195
- right: left + width,
196
- bottom: top + height,
197
- left
214
+ x: centerX - width / 2,
215
+ y: centerY - height / 2,
216
+ top: centerY - height / 2,
217
+ right: centerX + width / 2,
218
+ bottom: centerY + height / 2,
219
+ left: centerX - width / 2
198
220
  };
199
- }
221
+ return rect;
222
+ },
223
+ getClientRects() {
224
+ return [this.getBoundingClientRect()];
225
+ },
226
+ contextElement: viewportRef.current || void 0
200
227
  });
201
- }, [annotation.highlights, annotation.pageNumber, refs, viewportRef]);
228
+ }, [annotation.highlights, annotation.pageNumber, refs, viewportRef, scale]);
202
229
  useEffect(() => {
230
+ const viewport = viewportRef.current;
203
231
  updateTooltipPosition();
204
232
  const handleScroll = () => {
205
233
  requestAnimationFrame(updateTooltipPosition);
@@ -207,15 +235,15 @@ var useAnnotationTooltip = ({
207
235
  const handleResize = () => {
208
236
  requestAnimationFrame(updateTooltipPosition);
209
237
  };
210
- if (viewportRef.current) {
211
- viewportRef.current.addEventListener("scroll", handleScroll, {
238
+ if (viewport) {
239
+ viewport.addEventListener("scroll", handleScroll, {
212
240
  passive: true
213
241
  });
214
242
  }
215
243
  window.addEventListener("resize", handleResize, { passive: true });
216
244
  return () => {
217
- if (viewportRef.current) {
218
- viewportRef.current.removeEventListener("scroll", handleScroll);
245
+ if (viewport) {
246
+ viewport.removeEventListener("scroll", handleScroll);
219
247
  }
220
248
  window.removeEventListener("resize", handleResize);
221
249
  };
@@ -234,8 +262,10 @@ var AnnotationTooltip = ({
234
262
  children,
235
263
  tooltipContent,
236
264
  onOpenChange,
265
+ className,
237
266
  isOpen: controlledIsOpen
238
267
  }) => {
268
+ const viewportRef = usePdf((state) => state.viewportRef);
239
269
  const {
240
270
  isOpen: uncontrolledIsOpen,
241
271
  setIsOpen,
@@ -263,19 +293,22 @@ var AnnotationTooltip = ({
263
293
  children
264
294
  }
265
295
  ),
266
- isOpen && /* @__PURE__ */ jsx(
267
- "div",
268
- {
269
- ref: refs.setFloating,
270
- className: "bg-white shadow-lg rounded-lg p-3 z-50 min-w-[200px]",
271
- style: {
272
- ...floatingStyles,
273
- position: "fixed",
274
- zIndex: 9999
275
- },
276
- ...getFloatingProps(),
277
- children: tooltipContent
278
- }
296
+ isOpen && viewportRef.current && createPortal(
297
+ /* @__PURE__ */ jsx(
298
+ "div",
299
+ {
300
+ ref: refs.setFloating,
301
+ className,
302
+ style: {
303
+ ...floatingStyles,
304
+ position: "absolute",
305
+ pointerEvents: "auto"
306
+ },
307
+ ...getFloatingProps(),
308
+ children: tooltipContent
309
+ }
310
+ ),
311
+ viewportRef.current
279
312
  )
280
313
  ] });
281
314
  };
@@ -310,120 +343,11 @@ var PDFPageNumberContext = createContext(0);
310
343
  var usePDFPageNumber = () => {
311
344
  return useContext(PDFPageNumberContext);
312
345
  };
313
- var useAnnotationActions = ({
314
- annotation,
315
- onClose
316
- }) => {
317
- const { updateAnnotation } = useAnnotations();
318
- const [comment, setComment] = useState(annotation.comment || "");
319
- const [isEditing, setIsEditing] = useState(false);
320
- const handleSaveComment = useCallback(() => {
321
- updateAnnotation(annotation.id, { comment });
322
- setIsEditing(false);
323
- onClose?.();
324
- }, [annotation.id, comment, updateAnnotation, onClose]);
325
- const handleColorChange = useCallback(
326
- (color) => {
327
- updateAnnotation(annotation.id, { color });
328
- onClose?.();
329
- },
330
- [annotation.id, updateAnnotation, onClose]
331
- );
332
- const handleCancelEdit = useCallback(() => {
333
- setIsEditing(false);
334
- onClose?.();
335
- }, [onClose]);
336
- const colors = [
337
- "rgba(255, 255, 0, 0.3)",
338
- // Yellow
339
- "rgba(0, 255, 0, 0.3)",
340
- // Green
341
- "rgba(255, 182, 193, 0.3)",
342
- // Pink
343
- "rgba(135, 206, 235, 0.3)"
344
- // Sky Blue
345
- ];
346
- return {
347
- comment,
348
- isEditing,
349
- setComment,
350
- setIsEditing,
351
- handleSaveComment,
352
- handleColorChange,
353
- handleCancelEdit,
354
- colors
355
- };
356
- };
357
- var DefaultAnnotationTooltipContent = ({
358
- annotation,
359
- onClose
360
- }) => {
361
- const {
362
- comment,
363
- isEditing,
364
- setComment,
365
- setIsEditing,
366
- handleSaveComment,
367
- handleColorChange,
368
- handleCancelEdit,
369
- colors
370
- } = useAnnotationActions({
371
- annotation,
372
- onClose
373
- });
374
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
375
- /* @__PURE__ */ jsx("div", { className: "flex gap-2", children: colors.map((color) => /* @__PURE__ */ jsx(
376
- "button",
377
- {
378
- className: "w-6 h-6 rounded",
379
- style: { backgroundColor: color },
380
- onClick: () => handleColorChange(color)
381
- },
382
- color
383
- )) }),
384
- isEditing ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
385
- /* @__PURE__ */ jsx(
386
- "textarea",
387
- {
388
- value: comment,
389
- onChange: (e) => setComment(e.target.value),
390
- className: "border rounded p-2 text-sm",
391
- placeholder: "Add a comment...",
392
- rows: 3
393
- }
394
- ),
395
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
396
- /* @__PURE__ */ jsx(
397
- "button",
398
- {
399
- onClick: handleCancelEdit,
400
- className: "px-2 py-1 text-sm text-gray-600 hover:text-gray-800",
401
- children: "Cancel"
402
- }
403
- ),
404
- /* @__PURE__ */ jsx(
405
- "button",
406
- {
407
- onClick: handleSaveComment,
408
- className: "px-2 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600",
409
- children: "Save"
410
- }
411
- )
412
- ] })
413
- ] }) : /* @__PURE__ */ jsx("div", { children: annotation.comment ? /* @__PURE__ */ jsx("div", { className: "text-sm text-gray-700", children: annotation.comment }) : /* @__PURE__ */ jsx(
414
- "button",
415
- {
416
- onClick: () => setIsEditing(true),
417
- className: "text-sm text-blue-500 hover:text-blue-600",
418
- children: "Add comment"
419
- }
420
- ) })
421
- ] });
422
- };
423
346
  var AnnotationHighlightLayer = ({
424
347
  className,
425
348
  style,
426
349
  renderTooltipContent,
350
+ tooltipClassName,
427
351
  focusedAnnotationId,
428
352
  onAnnotationClick
429
353
  }) => {
@@ -436,24 +360,18 @@ var AnnotationHighlightLayer = ({
436
360
  AnnotationTooltip,
437
361
  {
438
362
  annotation,
363
+ className: tooltipClassName,
439
364
  isOpen: focusedAnnotationId === annotation.id,
440
365
  onOpenChange: (open) => {
441
366
  if (open && onAnnotationClick) {
442
367
  onAnnotationClick(annotation);
443
368
  }
444
369
  },
445
- tooltipContent: renderTooltipContent ? renderTooltipContent({
370
+ tooltipContent: renderTooltipContent({
446
371
  annotation,
447
372
  onClose: () => {
448
373
  }
449
- }) : /* @__PURE__ */ jsx(
450
- DefaultAnnotationTooltipContent,
451
- {
452
- annotation,
453
- onClose: () => {
454
- }
455
- }
456
- ),
374
+ }),
457
375
  children: /* @__PURE__ */ jsx(
458
376
  "div",
459
377
  {
@@ -469,10 +387,11 @@ var AnnotationHighlightLayer = ({
469
387
  width: highlight.width,
470
388
  height: highlight.height,
471
389
  backgroundColor: annotation.color || "rgba(255, 255, 0, 0.3)",
472
- transition: "background-color 0.2s ease",
473
- cursor: "pointer",
474
- opacity: focusedAnnotationId === annotation.id ? 1 : 0.5
475
- }
390
+ mixBlendMode: "multiply",
391
+ transition: "all 0.2s ease",
392
+ cursor: "pointer"
393
+ },
394
+ "data-highlight-id": annotation.id
476
395
  },
477
396
  index
478
397
  ))
@@ -1020,13 +939,13 @@ var CanvasLayer = ({
1020
939
  };
1021
940
 
1022
941
  // src/hooks/useSelectionDimensions.tsx
1023
- var MERGE_THRESHOLD = 20;
1024
- var shouldMergeRectangles = (rect1, rect2) => {
942
+ var MERGE_THRESHOLD = 5;
943
+ var shouldMergeRects = (rect1, rect2) => {
1025
944
  const verticalOverlap = !(rect1.top > rect2.top + rect2.height || rect2.top > rect1.top + rect1.height);
1026
945
  const horizontallyClose = Math.abs(rect1.left + rect1.width - rect2.left) < MERGE_THRESHOLD || Math.abs(rect2.left + rect2.width - rect1.left) < MERGE_THRESHOLD || rect1.left < rect2.left + rect2.width && rect2.left < rect1.left + rect1.width;
1027
946
  return verticalOverlap && horizontallyClose;
1028
947
  };
1029
- var mergeRectangles = (rect1, rect2) => {
948
+ var mergeRects = (rect1, rect2) => {
1030
949
  return {
1031
950
  left: Math.min(rect1.left, rect2.left),
1032
951
  top: Math.min(rect1.top, rect2.top),
@@ -1037,38 +956,28 @@ var mergeRectangles = (rect1, rect2) => {
1037
956
  };
1038
957
  var consolidateRects = (rects) => {
1039
958
  if (rects.length <= 1) return rects;
1040
- const sortedRectangles = rects.toSorted((a, b) => a.top - b.top);
1041
- let withinThresholdMergedRectangles = sortedRectangles[0];
1042
- const allMergedRectangles = [];
1043
- const blowUpMergedRectangles = (rect) => {
1044
- return {
1045
- ...rect,
1046
- left: rect.left - 2,
1047
- top: rect.top - 2,
1048
- width: rect.width + 4,
1049
- height: rect.height + 2
1050
- };
1051
- };
1052
- for (const [index, currentRectangle] of sortedRectangles.entries()) {
1053
- withinThresholdMergedRectangles = mergeRectangles(
1054
- withinThresholdMergedRectangles,
1055
- currentRectangle
1056
- );
1057
- const nextRectangle = sortedRectangles[index + 1];
1058
- if (!nextRectangle) {
1059
- allMergedRectangles.push(
1060
- blowUpMergedRectangles(withinThresholdMergedRectangles)
1061
- );
1062
- break;
1063
- }
1064
- if (!shouldMergeRectangles(withinThresholdMergedRectangles, nextRectangle)) {
1065
- allMergedRectangles.push(
1066
- blowUpMergedRectangles(withinThresholdMergedRectangles)
1067
- );
1068
- withinThresholdMergedRectangles = nextRectangle;
959
+ const sortedRects = [...rects].sort((a, b) => a.top - b.top);
960
+ let hasChanges;
961
+ do {
962
+ hasChanges = false;
963
+ let currentRect = sortedRects[0];
964
+ const tempResult = [];
965
+ for (let i = 1; i < sortedRects.length; i++) {
966
+ const sorted = sortedRects[i];
967
+ if (!currentRect || !sorted) continue;
968
+ if (shouldMergeRects(currentRect, sorted)) {
969
+ currentRect = mergeRects(currentRect, sorted);
970
+ hasChanges = true;
971
+ } else {
972
+ tempResult.push(currentRect);
973
+ currentRect = sorted;
974
+ }
1069
975
  }
1070
- }
1071
- return allMergedRectangles;
976
+ if (currentRect) tempResult.push(currentRect);
977
+ sortedRects.length = 0;
978
+ sortedRects.push(...tempResult);
979
+ } while (hasChanges);
980
+ return sortedRects;
1072
981
  };
1073
982
  var useSelectionDimensions = () => {
1074
983
  const store = PDFStore.useContext();
@@ -1397,13 +1306,15 @@ var ColoredHighlightLayer = ({
1397
1306
  onHighlight
1398
1307
  }) => {
1399
1308
  const pageNumber = usePDFPageNumber();
1400
- const { getSelection } = useSelectionDimensions();
1309
+ const { getDimension } = useSelectionDimensions();
1401
1310
  const highlights = usePdf(
1402
1311
  (state) => state.coloredHighlights
1403
1312
  );
1404
1313
  const addColoredHighlight = usePdf((state) => state.addColoredHighlight);
1405
1314
  const handleHighlighting = useCallback((color) => {
1406
- const { highlights: highlights2, text } = getSelection();
1315
+ const dimension = getDimension();
1316
+ if (!dimension) return;
1317
+ const { highlights: highlights2, text } = dimension;
1407
1318
  if (highlights2[0]) {
1408
1319
  const highlight = {
1409
1320
  uuid: v4(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anaralabs/lector",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "description": "Headless PDF viewer for React",
5
5
  "author": "andrewdr (https://github.com/andrewdoro)",
6
6
  "license": "MIT",