From 1c89cf067aa4190fcef67491b423eeb5d174dbc5 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 12 Jun 2026 14:42:30 +0000 Subject: [PATCH] Simplify document comment panel (PAP-10960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streamline the inline document annotation UI per board feedback: - Drop the "Comments" panel header, the Open/Resolved/Stale/Orphaned filter chips, and the "No open comments yet" empty state. The revision indicator now lives top-right next to the close button, and all anchorable threads render in a single list. - Remove the per-thread "Open · just now" status/timestamp row; keep the per-comment timestamp beside each author avatar. - Show the current user's avatar + name directly above the composer when writing a new comment. - Clamp quote snippets to two lines with a roomier line-height so the next line's letter tops are never clipped. - Drop the "⌘↵ to submit" hints from the composer/reply placeholders. - Keep the selected segment brightly highlighted in the document while a comment is being composed (pendingHighlightText) so the selection no longer disappears when the panel opens. - Make thread creation, replies, and resolve/reopen optimistic so submission feels instant instead of flashing empty then repopulating. - Clamp the floating desktop panel below the sticky top nav so the comments thread no longer tucks under the nav bar while scrolling. Adds a Storybook story (Product/Document Comments) covering the empty, populated, and composing states. Updates annotation tests for the filter-less single-list behaviour. Co-Authored-By: Claude Opus 4.8 --- ui/src/components/DocumentAnnotationLayer.tsx | 74 +++-- ui/src/components/DocumentAnnotationPanel.tsx | 306 ++++++++++++------ .../IssueDocumentAnnotations.test.tsx | 31 +- .../components/IssueDocumentAnnotations.tsx | 8 +- .../stories/document-comments.stories.tsx | 188 +++++++++++ 5 files changed, 464 insertions(+), 143 deletions(-) create mode 100644 ui/storybook/stories/document-comments.stories.tsx diff --git a/ui/src/components/DocumentAnnotationLayer.tsx b/ui/src/components/DocumentAnnotationLayer.tsx index 6859922ab60..a31bcdc777f 100644 --- a/ui/src/components/DocumentAnnotationLayer.tsx +++ b/ui/src/components/DocumentAnnotationLayer.tsx @@ -48,8 +48,17 @@ export interface AnnotationLayerProps { * pending anchor for the keyboard shortcut path. */ captureSelectionRequestId?: number; + /** + * Text of a comment currently being composed. We keep this segment brightly + * highlighted in the document even after the native browser selection is lost + * (e.g. once focus moves into the composer textarea). + */ + pendingHighlightText?: string | null; } +/** Synthetic thread id used to render the in-progress (pending) comment highlight. */ +const PENDING_HIGHLIGHT_THREAD_ID = "__paperclip-pending-annotation__"; + interface HighlightRect { threadId: string; status: DocumentAnnotationThreadStatus; @@ -60,6 +69,8 @@ interface HighlightRect { height: number; /** True for the last rect of this thread (used to anchor a glyph at the run end). */ isTail: boolean; + /** True when this run should render with the brighter focused/pending treatment. */ + focused: boolean; } interface ToolbarPosition { @@ -203,6 +214,7 @@ export function DocumentAnnotationLayer({ newCommentDisabledReason = null, hideResolved = true, captureSelectionRequestId, + pendingHighlightText = null, }: AnnotationLayerProps) { const [highlightRects, setHighlightRects] = useState([]); const [toolbarPosition, setToolbarPosition] = useState(null); @@ -231,19 +243,17 @@ export function DocumentAnnotationLayer({ const overlayRect = overlay.getBoundingClientRect(); const next: HighlightRect[] = []; const nativeRanges = emptyNativeHighlightRanges(); - for (const thread of visibleThreads) { - if (thread.anchorState === "orphaned") continue; - const isFocused = thread.id === focusedThreadId; - const isStale = thread.anchorState === "stale"; - const isResolved = thread.status === "resolved"; - const nativeKind = nativeHighlightKind({ - focused: isFocused, - stale: isStale, - resolved: isResolved, - }); + const pushRunRects = (run: { + threadId: string; + status: DocumentAnnotationThreadStatus; + anchorState: DocumentAnnotationAnchorState; + focused: boolean; + selectedText: string; + nativeKind: NativeHighlightKind; + }) => { const ranges = rangesForNormalizedSpan({ container, - selectedText: thread.selectedText, + selectedText: run.selectedText, }); const startIndex = next.length; for (const range of ranges) { @@ -256,9 +266,10 @@ export function DocumentAnnotationLayer({ if (!visibleRect) continue; rangeIsVisible = true; next.push({ - threadId: thread.id, - status: thread.status, - anchorState: thread.anchorState, + threadId: run.threadId, + status: run.status, + anchorState: run.anchorState, + focused: run.focused, top: visibleRect.top - overlayRect.top, left: visibleRect.left - overlayRect.left, width: visibleRect.width, @@ -266,15 +277,41 @@ export function DocumentAnnotationLayer({ isTail: false, }); } - if (rangeIsVisible) nativeRanges[nativeKind].push(range); + if (rangeIsVisible) nativeRanges[run.nativeKind].push(range); } if (next.length > startIndex) { next[next.length - 1].isTail = true; } + }; + for (const thread of visibleThreads) { + if (thread.anchorState === "orphaned") continue; + const isFocused = thread.id === focusedThreadId; + const isStale = thread.anchorState === "stale"; + const isResolved = thread.status === "resolved"; + pushRunRects({ + threadId: thread.id, + status: thread.status, + anchorState: thread.anchorState, + focused: isFocused, + selectedText: thread.selectedText, + nativeKind: nativeHighlightKind({ focused: isFocused, stale: isStale, resolved: isResolved }), + }); + } + // Keep the in-progress (pending) comment selection brightly highlighted so the + // segment stays anchored in the document while the composer is open. + if (pendingHighlightText && pendingHighlightText.trim().length > 0) { + pushRunRects({ + threadId: PENDING_HIGHLIGHT_THREAD_ID, + status: "open", + anchorState: "active", + focused: true, + selectedText: pendingHighlightText, + nativeKind: "focused", + }); } setNativeHighlightRanges(nativeHighlightInstanceId, nativeRanges); setHighlightRects(next); - }, [containerRef, focusedThreadId, nativeHighlightInstanceId, visibleThreads]); + }, [containerRef, focusedThreadId, nativeHighlightInstanceId, pendingHighlightText, visibleThreads]); useLayoutEffect(() => { computeHighlightRects(); @@ -398,7 +435,7 @@ export function DocumentAnnotationLayer({