@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.
- package/components/AnnotationPanel.tsx +68 -37
- package/components/AnnotationToolbar.tsx +1 -1
- package/components/BlockRenderer.tsx +1 -1
- package/components/EditorAnnotationCard.tsx +13 -10
- package/components/ImageAnnotator/index.tsx +1 -1
- package/components/OpenInAppButton.tsx +25 -29
- package/components/PopoutDialog.tsx +46 -32
- package/components/Popover.tsx +18 -12
- package/components/SearchableSelect.tsx +4 -7
- package/components/Tooltip.tsx +50 -20
- package/components/Viewer.tsx +23 -9
- package/components/blocks/CodeBlock.tsx +2 -2
- package/components/blocks/HtmlBlock.tsx +1 -1
- package/components/blocks/TableBlock.tsx +1 -1
- package/components/blocks/TablePopout.tsx +1 -1
- package/components/ui/badge.tsx +15 -10
- package/components/ui/button.tsx +10 -10
- package/components/ui/dialog.tsx +17 -17
- package/components/ui/dropdown-menu.tsx +80 -66
- package/components/ui/tabs.tsx +11 -11
- package/hooks/useAnnotationHighlighter.ts +44 -6
- package/hooks/useCodeAnnotationDraft.ts +0 -5
- package/hooks/usePinpoint.ts +2 -2
- package/package.json +5 -12
- package/styles.css +1 -1
- package/theme.css +17 -0
- package/utils/parser.ts +1 -1
- package/utils/sharing.ts +1 -1
package/components/ui/tabs.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import
|
|
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 =
|
|
17
|
+
TabsList.displayName = "TabsList";
|
|
18
18
|
|
|
19
19
|
const TabsTrigger = React.forwardRef<
|
|
20
|
-
React.ComponentRef<typeof TabsPrimitive.
|
|
21
|
-
React.ComponentPropsWithoutRef<typeof TabsPrimitive.
|
|
20
|
+
React.ComponentRef<typeof TabsPrimitive.Tab>,
|
|
21
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Tab>
|
|
22
22
|
>(({ className, ...props }, ref) => (
|
|
23
|
-
<TabsPrimitive.
|
|
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-
|
|
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 =
|
|
36
|
+
TabsTrigger.displayName = "TabsTrigger";
|
|
37
37
|
|
|
38
38
|
const TabsContent = React.forwardRef<
|
|
39
|
-
React.ComponentRef<typeof TabsPrimitive.
|
|
40
|
-
React.ComponentPropsWithoutRef<typeof TabsPrimitive.
|
|
39
|
+
React.ComponentRef<typeof TabsPrimitive.Panel>,
|
|
40
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Panel>
|
|
41
41
|
>(({ className, ...props }, ref) => (
|
|
42
|
-
<TabsPrimitive.
|
|
42
|
+
<TabsPrimitive.Panel ref={ref} className={cn("flex-1 outline-none", className)} {...props} />
|
|
43
43
|
));
|
|
44
|
-
TabsContent.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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
}
|
package/hooks/usePinpoint.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useEffect, useState,
|
|
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
|
|
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.
|
|
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
|
-
"@
|
|
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
|
},
|