Skip to content

Commit 2a46138

Browse files
grubmanItayclaude
andauthored
feat: bidirectional scroll navigation between annotations and highlights (#253)
* feat: bidirectional scroll navigation between annotations and highlights When reviewing long plans with many annotations, it's hard to visually connect which annotation card corresponds to which highlighted line. - Click annotation card → scrolls content to the highlight + applies a bright cyan "focused" color for visibility - Click highlighted text → scrolls the right panel to bring the corresponding annotation card into view - Works with both web-highlighter and manually created (shared/imported) annotations - Handles edge cases: global comments (no highlight), multi-node selections, already-visible elements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: scroll to center and skip focus on annotation creation - Change scrollIntoView block from 'nearest' to 'center' so targets appear prominently in the middle of the viewport - Track just-created annotation IDs to skip scroll+focus effect when a new annotation is added (user is already looking at it) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d259748 commit 2a46138

File tree

3 files changed

+73
-1
lines changed

3 files changed

+73
-1
lines changed

packages/editor/index.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,18 @@ pre code.hljs .hljs-code {
249249
background: oklch(0.70 0.20 60 / 0.15);
250250
}
251251

252+
.annotation-highlight.focused {
253+
background: oklch(0.70 0.20 200 / 0.45) !important;
254+
box-shadow: 0 0 8px oklch(0.70 0.20 200 / 0.4);
255+
border-bottom: 2px solid oklch(0.70 0.20 200);
256+
filter: none;
257+
}
258+
259+
.light .annotation-highlight.focused {
260+
background: oklch(0.70 0.22 200 / 0.3) !important;
261+
box-shadow: 0 0 6px oklch(0.60 0.20 200 / 0.3);
262+
}
263+
252264
.annotation-highlight:hover {
253265
filter: brightness(1.2);
254266
cursor: pointer;

packages/ui/components/AnnotationPanel.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,19 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
3434
onDeleteEditorAnnotation,
3535
}) => {
3636
const [copied, setCopied] = useState(false);
37+
const listRef = useRef<HTMLDivElement>(null);
3738
const sortedAnnotations = [...annotations].sort((a, b) => a.createdA - b.createdA);
3839
const totalCount = annotations.length + (editorAnnotations?.length ?? 0);
3940

41+
// Scroll selected annotation card into view
42+
useEffect(() => {
43+
if (!selectedId || !listRef.current) return;
44+
const card = listRef.current.querySelector(`[data-annotation-id="${selectedId}"]`);
45+
if (card) {
46+
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
47+
}
48+
}, [selectedId]);
49+
4050
const handleQuickShare = async () => {
4151
if (!shareUrl) return;
4252
try {
@@ -65,7 +75,7 @@ export const AnnotationPanel: React.FC<PanelProps> = ({
6575
</div>
6676

6777
{/* List */}
68-
<div className="flex-1 overflow-y-auto p-2 space-y-1.5">
78+
<div ref={listRef} className="flex-1 overflow-y-auto p-2 space-y-1.5">
6979
{totalCount === 0 ? (
7080
<div className="flex flex-col items-center justify-center h-40 text-center px-4">
7181
<div className="w-10 h-10 rounded-full bg-muted/50 flex items-center justify-center mb-3">
@@ -276,6 +286,7 @@ const AnnotationCard: React.FC<{
276286

277287
return (
278288
<div
289+
data-annotation-id={annotation.id}
279290
onClick={onSelect}
280291
className={`
281292
group relative p-2.5 rounded-lg border cursor-pointer transition-all

packages/ui/components/Viewer.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
121121
const modeRef = useRef<EditorMode>(mode);
122122
const onAddAnnotationRef = useRef(onAddAnnotation);
123123
const pendingSourceRef = useRef<any>(null);
124+
const justCreatedIdRef = useRef<string | null>(null);
124125
const [toolbarState, setToolbarState] = useState<{
125126
element: HTMLElement;
126127
source: any;
@@ -260,6 +261,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
260261
highlighter.addClass('comment', source.id);
261262
}
262263

264+
justCreatedIdRef.current = newAnnotation.id;
263265
onAddAnnotationRef.current(newAnnotation);
264266
};
265267

@@ -572,6 +574,52 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
572574
});
573575
}, [annotations]);
574576

577+
// Scroll to and focus the selected annotation's highlight in the content
578+
useEffect(() => {
579+
if (!containerRef.current) return;
580+
581+
// Clear all previously focused highlights
582+
containerRef.current.querySelectorAll('.annotation-highlight.focused').forEach(el => {
583+
el.classList.remove('focused');
584+
});
585+
586+
if (!selectedAnnotationId) return;
587+
588+
// Skip scroll+focus when annotation was just created (user is already looking at it)
589+
if (justCreatedIdRef.current === selectedAnnotationId) {
590+
justCreatedIdRef.current = null;
591+
return;
592+
}
593+
594+
// Find highlight elements: try web-highlighter first, then manual marks
595+
const highlighter = highlighterRef.current;
596+
let targetElements: Element[] = [];
597+
598+
if (highlighter) {
599+
try {
600+
const doms = highlighter.getDoms(selectedAnnotationId);
601+
if (doms && doms.length > 0) {
602+
targetElements = Array.from(doms);
603+
}
604+
} catch (e) {}
605+
}
606+
607+
if (targetElements.length === 0) {
608+
const manualMarks = containerRef.current.querySelectorAll(
609+
`[data-bind-id="${selectedAnnotationId}"]`
610+
);
611+
if (manualMarks.length > 0) {
612+
targetElements = Array.from(manualMarks);
613+
}
614+
}
615+
616+
if (targetElements.length === 0) return;
617+
618+
// Apply focused class to all elements and scroll the first one into view
619+
targetElements.forEach(el => el.classList.add('focused'));
620+
targetElements[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
621+
}, [selectedAnnotationId]);
622+
575623
const handleAnnotate = (type: AnnotationType) => {
576624
const highlighter = highlighterRef.current;
577625
if (!toolbarState || !highlighter) return;
@@ -622,6 +670,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
622670
images,
623671
};
624672

673+
justCreatedIdRef.current = newAnnotation.id;
625674
onAddAnnotationRef.current(newAnnotation);
626675
window.getSelection()?.removeAllRanges();
627676
};

0 commit comments

Comments
 (0)