@plannotator/ui 0.22.0 → 0.24.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.
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import * as TabsPrimitive from "@radix-ui/react-tabs";
2
+ import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
3
3
  import { cn } from "../../lib/utils";
4
4
 
5
5
  const Tabs = TabsPrimitive.Root;
@@ -14,18 +14,18 @@ const TabsList = React.forwardRef<
14
14
  {...props}
15
15
  />
16
16
  ));
17
- TabsList.displayName = TabsPrimitive.List.displayName;
17
+ TabsList.displayName = "TabsList";
18
18
 
19
19
  const TabsTrigger = React.forwardRef<
20
- React.ComponentRef<typeof TabsPrimitive.Trigger>,
21
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
20
+ React.ComponentRef<typeof TabsPrimitive.Tab>,
21
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Tab>
22
22
  >(({ className, ...props }, ref) => (
23
- <TabsPrimitive.Trigger
23
+ <TabsPrimitive.Tab
24
24
  ref={ref}
25
25
  className={cn(
26
26
  "inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-[13px] font-medium text-foreground",
27
27
  "hover:bg-surface-1",
28
- "data-[state=active]:bg-primary/10 data-[state=active]:text-foreground",
28
+ "data-active:bg-primary/10 data-active:text-foreground",
29
29
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
30
30
  "disabled:pointer-events-none disabled:opacity-50",
31
31
  className,
@@ -33,14 +33,14 @@ const TabsTrigger = React.forwardRef<
33
33
  {...props}
34
34
  />
35
35
  ));
36
- TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
36
+ TabsTrigger.displayName = "TabsTrigger";
37
37
 
38
38
  const TabsContent = React.forwardRef<
39
- React.ComponentRef<typeof TabsPrimitive.Content>,
40
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
39
+ React.ComponentRef<typeof TabsPrimitive.Panel>,
40
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Panel>
41
41
  >(({ className, ...props }, ref) => (
42
- <TabsPrimitive.Content ref={ref} className={cn("flex-1 outline-none", className)} {...props} />
42
+ <TabsPrimitive.Panel ref={ref} className={cn("flex-1 outline-none", className)} {...props} />
43
43
  ));
44
- TabsContent.displayName = TabsPrimitive.Content.displayName;
44
+ TabsContent.displayName = "TabsContent";
45
45
 
46
46
  export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -174,6 +174,12 @@ const escapeAttrValue = (value: string): string => {
174
174
  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
175
175
  };
176
176
 
177
+ /** Whitespace-insensitive comparison for restore verification: a highlight
178
+ * spanning element boundaries legitimately differs from `originalText` in
179
+ * whitespace, so only content differences count as a mismatch. */
180
+ const normalizeForRestoreCompare = (value: string): string =>
181
+ value.replace(/\s+/g, ' ').trim();
182
+
177
183
  const applyMathAnnotationClass = (
178
184
  element: HTMLElement,
179
185
  id: string,
@@ -227,6 +233,15 @@ export interface UseAnnotationHighlighterOptions {
227
233
  selectedAnnotationId: string | null;
228
234
  mode: EditorMode;
229
235
  enabled?: boolean;
236
+ /** Opt-in: after a meta-based restore (`fromStore`), verify the painted text
237
+ * matches the annotation's `originalText` (whitespace-normalized). On
238
+ * mismatch the wrong highlight is removed and the text-search fallback
239
+ * runs instead; if that also fails, `onRestoreMismatch` fires and nothing
240
+ * is painted. Default false — today's behavior (positions are trusted). */
241
+ verifyRestoredContent?: boolean;
242
+ /** Fires when a restore was rejected (content mismatch) and the text-search
243
+ * fallback could not re-anchor the annotation either. */
244
+ onRestoreMismatch?: (annotation: Annotation, restoredText: string) => void;
230
245
  }
231
246
 
232
247
  export interface UseAnnotationHighlighterReturn {
@@ -258,6 +273,8 @@ export function useAnnotationHighlighter({
258
273
  selectedAnnotationId,
259
274
  mode,
260
275
  enabled = true,
276
+ verifyRestoredContent = false,
277
+ onRestoreMismatch,
261
278
  }: UseAnnotationHighlighterOptions): UseAnnotationHighlighterReturn {
262
279
  const highlighterRef = useRef<Highlighter | null>(null);
263
280
  const modeRef = useRef<EditorMode>(mode);
@@ -278,6 +295,8 @@ export function useAnnotationHighlighter({
278
295
  useEffect(() => { modeRef.current = mode; }, [mode]);
279
296
  useEffect(() => { onAddAnnotationRef.current = onAddAnnotation; }, [onAddAnnotation]);
280
297
  useEffect(() => { onSelectAnnotationRef.current = onSelectAnnotation; }, [onSelectAnnotation]);
298
+ const onRestoreMismatchRef = useRef(onRestoreMismatch);
299
+ useEffect(() => { onRestoreMismatchRef.current = onRestoreMismatch; }, [onRestoreMismatch]);
281
300
 
282
301
  const clearPendingSelection = useCallback(() => {
283
302
  pendingSourceRef.current = null;
@@ -625,23 +644,42 @@ export function useAnnotationHighlighter({
625
644
  }
626
645
  }
627
646
 
647
+ // When a meta-based restore paints text that no longer matches the
648
+ // anchor's originalText (document drift), remember what it painted so
649
+ // the mismatch can be reported if the text fallback also fails.
650
+ let rejectedRestoreText: string | null = null;
651
+
628
652
  if (ann.startMeta && ann.endMeta) {
629
653
  try {
630
654
  highlighter.fromStore(ann.startMeta, ann.endMeta, ann.originalText, ann.id);
631
655
  const restoredDoms = highlighter.getDoms(ann.id);
632
656
  if (restoredDoms && restoredDoms.length > 0) {
633
- if (ann.type === AnnotationType.DELETION) {
634
- highlighter.addClass('deletion', ann.id);
635
- } else if (ann.type === AnnotationType.COMMENT) {
636
- highlighter.addClass('comment', ann.id);
657
+ const restoredText = restoredDoms.map(dom => dom.textContent ?? '').join('');
658
+ if (
659
+ verifyRestoredContent &&
660
+ normalizeForRestoreCompare(restoredText) !== normalizeForRestoreCompare(ann.originalText)
661
+ ) {
662
+ // Positions resolved, but onto the WRONG text — remove the bad
663
+ // highlight and fall through to the text-search fallback.
664
+ try { highlighter.remove(ann.id); } catch {}
665
+ rejectedRestoreText = restoredText;
666
+ } else {
667
+ if (ann.type === AnnotationType.DELETION) {
668
+ highlighter.addClass('deletion', ann.id);
669
+ } else if (ann.type === AnnotationType.COMMENT) {
670
+ highlighter.addClass('comment', ann.id);
671
+ }
672
+ return;
637
673
  }
638
- return;
639
674
  }
640
675
  } catch {}
641
676
  }
642
677
 
643
678
  const range = findTextInDOM(ann.originalText);
644
679
  if (!range) {
680
+ if (rejectedRestoreText !== null) {
681
+ onRestoreMismatchRef.current?.(ann, rejectedRestoreText);
682
+ }
645
683
  console.warn(`Could not find text for annotation ${ann.id}: "${ann.originalText.slice(0, 50)}..."`);
646
684
  return;
647
685
  }
@@ -720,7 +758,7 @@ export function useAnnotationHighlighter({
720
758
  console.warn(`Failed to apply highlight for annotation ${ann.id}:`, e);
721
759
  }
722
760
  });
723
- }, [findMathElementsForAnnotation, findTextInDOM]);
761
+ }, [findMathElementsForAnnotation, findTextInDOM, verifyRestoredContent]);
724
762
 
725
763
  const removeHighlight = useCallback((id: string) => {
726
764
  highlighterRef.current?.remove(id);
@@ -20,11 +20,6 @@ interface DraftData {
20
20
  ts: number;
21
21
  }
22
22
 
23
- interface MissingDraftData {
24
- found?: false;
25
- draftGeneration?: number;
26
- }
27
-
28
23
  function readDraftGeneration(value: unknown): number | null {
29
24
  return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : null;
30
25
  }
@@ -1,7 +1,7 @@
1
- import { useEffect, useState, useCallback, type RefObject } from 'react';
1
+ import { useEffect, useState, type RefObject } from 'react';
2
2
  import type Highlighter from '@plannotator/web-highlighter';
3
3
  import type { InputMethod } from '../types';
4
- import { resolvePinpointTarget, type PinpointTarget } from '../utils/blockTargeting';
4
+ import { resolvePinpointTarget } from '../utils/blockTargeting';
5
5
 
6
6
  export interface UsePinpointOptions {
7
7
  containerRef: RefObject<HTMLElement | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/ui",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./components/*": "./components/*.tsx",
@@ -47,7 +47,7 @@
47
47
  "!test-setup"
48
48
  ],
49
49
  "dependencies": {
50
- "@plannotator/atomic-editor": "^0.5.0",
50
+ "@base-ui/react": "^1.6.0",
51
51
  "@codemirror/autocomplete": "^6.20.3",
52
52
  "@codemirror/commands": "^6.10.3",
53
53
  "@codemirror/lang-javascript": "^6.2.5",
@@ -62,18 +62,13 @@
62
62
  "@codemirror/view": "^6.43.0",
63
63
  "@fontsource-variable/geist-mono": "5.2.7",
64
64
  "@fontsource-variable/inter": "^5.2.8",
65
- "@pierre/diffs": "1.2.8",
66
65
  "@lezer/common": "^1.5.2",
67
66
  "@lezer/highlight": "^1.2.3",
67
+ "@pierre/diffs": "1.2.8",
68
+ "@plannotator/atomic-editor": "^0.5.0",
68
69
  "@plannotator/core": "0.22.0",
69
70
  "@plannotator/markdown-editor": "^0.2.0",
70
71
  "@plannotator/web-highlighter": "^0.8.1",
71
- "@radix-ui/react-dialog": "^1.1.15",
72
- "@radix-ui/react-dropdown-menu": "^2.1.16",
73
- "@radix-ui/react-popover": "^1.1.15",
74
- "@radix-ui/react-slot": "^1.2.4",
75
- "@radix-ui/react-tabs": "^1.1.13",
76
- "@radix-ui/react-tooltip": "^1.2.8",
77
72
  "@tanstack/react-table": "^8.21.3",
78
73
  "@viz-js/viz": "^3.25.0",
79
74
  "class-variance-authority": "^0.7.1",
@@ -93,8 +88,7 @@
93
88
  "peerDependencies": {
94
89
  "react": "^19.2.3",
95
90
  "react-dom": "^19.2.3",
96
- "tailwindcss": "^4.1.18",
97
- "tailwindcss-animate": "^1.0.7"
91
+ "tailwindcss": "^4.1.18"
98
92
  },
99
93
  "devDependencies": {
100
94
  "@happy-dom/global-registrator": "^20.10.1",
@@ -105,7 +99,6 @@
105
99
  "react": "^19.2.3",
106
100
  "react-dom": "^19.2.3",
107
101
  "tailwindcss": "^4.1.18",
108
- "tailwindcss-animate": "^1.0.7",
109
102
  "typescript": "~5.8.2",
110
103
  "vite": "^6.2.0"
111
104
  },