From ecfd80bf9ad204ec878d9d58e25c71e5f5770dd6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 22:45:35 +0100 Subject: [PATCH] feat(frontend): add discussion sub-tab navigation for annotation threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Within the Diskussion panel tab, show two sub-tabs when an annotation is active: «Diskussion» (document-level thread, with comment-count badge) and «Annotation · Seite N» (annotation-specific thread). Behaviour: - Clicking an annotation auto-switches to the Annotation sub-tab - Clicking the Diskussion sub-tab deselects the annotation and returns to the document thread - Escape clears the active annotation (or collapses the panel if none) - activeAnnotationPage is now lifted from PdfViewer → DocumentViewer → page → DocumentBottomPanel → PanelDiscussion so the tab label shows the correct page number Closes #60 Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 3 +- frontend/messages/en.json | 3 +- frontend/messages/es.json | 3 +- .../lib/components/DocumentBottomPanel.svelte | 6 ++ .../src/lib/components/DocumentViewer.svelte | 3 + .../src/lib/components/PanelDiscussion.svelte | 97 ++++++++++++++----- frontend/src/lib/components/PdfViewer.svelte | 5 + .../src/routes/documents/[id]/+page.svelte | 20 ++++ 8 files changed, 112 insertions(+), 28 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 992698cb..6004793b 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -257,5 +257,6 @@ "doc_panel_tab_history": "Verlauf", "doc_panel_annotate": "Annotieren", "doc_panel_annotate_stop": "Fertig", - "doc_panel_annotation_thread_title": "Annotation" + "doc_panel_annotation_thread_title": "Annotation", + "doc_panel_discussion_annotation_tab": "Annotation · Seite {page}" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index e6124604..c4669686 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -257,5 +257,6 @@ "doc_panel_tab_history": "History", "doc_panel_annotate": "Annotate", "doc_panel_annotate_stop": "Done", - "doc_panel_annotation_thread_title": "Annotation" + "doc_panel_annotation_thread_title": "Annotation", + "doc_panel_discussion_annotation_tab": "Annotation · Page {page}" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 1a672c48..7e9ff8e8 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -257,5 +257,6 @@ "doc_panel_tab_history": "Historial", "doc_panel_annotate": "Anotar", "doc_panel_annotate_stop": "Listo", - "doc_panel_annotation_thread_title": "Anotación" + "doc_panel_annotation_thread_title": "Anotación", + "doc_panel_discussion_annotation_tab": "Anotación · Página {page}" } diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte index 1a0725f0..adc8c2f9 100644 --- a/frontend/src/lib/components/DocumentBottomPanel.svelte +++ b/frontend/src/lib/components/DocumentBottomPanel.svelte @@ -49,6 +49,8 @@ type Props = { height: number; activeTab: Tab; activeAnnotationId: string | null; + activeAnnotationPage: number | null; + onClearAnnotation?: () => void; onAnnotationCommentCountChange?: (annotationId: string, count: number) => void; }; @@ -62,6 +64,8 @@ let { height = $bindable(), activeTab = $bindable(), activeAnnotationId, + activeAnnotationPage, + onClearAnnotation, onAnnotationCommentCountChange }: Props = $props(); @@ -182,10 +186,12 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT); {:else if activeTab === 'history'} diff --git a/frontend/src/lib/components/DocumentViewer.svelte b/frontend/src/lib/components/DocumentViewer.svelte index bf13e0d8..3ae7e8b6 100644 --- a/frontend/src/lib/components/DocumentViewer.svelte +++ b/frontend/src/lib/components/DocumentViewer.svelte @@ -15,6 +15,7 @@ type Props = { error: string; annotateMode: boolean; activeAnnotationId: string | null; + activeAnnotationPage: number | null; onAnnotationClick: (id: string) => void; }; @@ -25,6 +26,7 @@ let { error, annotateMode = $bindable(), activeAnnotationId = $bindable(), + activeAnnotationPage = $bindable(), onAnnotationClick }: Props = $props(); @@ -79,6 +81,7 @@ let { documentId={doc.id} bind:annotateMode={annotateMode} bind:activeAnnotationId={activeAnnotationId} + bind:activeAnnotationPage={activeAnnotationPage} onAnnotationClick={onAnnotationClick} /> {:else if fileUrl} diff --git a/frontend/src/lib/components/PanelDiscussion.svelte b/frontend/src/lib/components/PanelDiscussion.svelte index 89810ca8..e15658fb 100644 --- a/frontend/src/lib/components/PanelDiscussion.svelte +++ b/frontend/src/lib/components/PanelDiscussion.svelte @@ -24,33 +24,98 @@ type Comment = { type Props = { documentId: string; activeAnnotationId: string | null; + activeAnnotationPage: number | null; initialComments: Comment[]; canComment: boolean; currentUserId: string | null; canAdmin: boolean; + onClearAnnotation?: () => void; onAnnotationCommentCountChange?: (annotationId: string, count: number) => void; }; let { documentId, activeAnnotationId, + activeAnnotationPage, initialComments, canComment, currentUserId, canAdmin, + onClearAnnotation, onAnnotationCommentCountChange }: Props = $props(); + +// Sub-tab within the discussion panel: 'document' or 'annotation' +type DiscussionTab = 'document' | 'annotation'; +let activeSubTab = $state('document'); + +// Track document-level comment count for badge. +// CommentThread calls onCountChange immediately on mount with the accurate total. +let docCommentCount = $state(0); + +// When an annotation becomes active, switch to the annotation sub-tab automatically. +$effect(() => { + if (activeAnnotationId) { + activeSubTab = 'annotation'; + } else { + activeSubTab = 'document'; + } +}); + +function selectDocumentTab() { + activeSubTab = 'document'; + onClearAnnotation?.(); +} + +function selectAnnotationTab() { + activeSubTab = 'annotation'; +} -
- +
+ {#if activeAnnotationId} -
-

+

+ {m.doc_panel_tab_discussion()} + {#if docCommentCount > 0} + + {docCommentCount} + + {/if} + + +
+ {/if} + + +
+ {#if !activeAnnotationId || activeSubTab === 'document'} + + (docCommentCount = count)} + /> + {:else} + {#key activeAnnotationId} onAnnotationCommentCountChange?.(activeAnnotationId, count)} /> {/key} -
- {/if} - - -
- {#if activeAnnotationId} -

- {m.comment_section_title()} -

{/if} -
diff --git a/frontend/src/lib/components/PdfViewer.svelte b/frontend/src/lib/components/PdfViewer.svelte index 2ac3e6b2..5f15bc9a 100644 --- a/frontend/src/lib/components/PdfViewer.svelte +++ b/frontend/src/lib/components/PdfViewer.svelte @@ -9,12 +9,14 @@ let { documentId = '', annotateMode = $bindable(false), activeAnnotationId = $bindable(null), + activeAnnotationPage = $bindable(null), onAnnotationClick }: { url: string; documentId?: string; annotateMode?: boolean; activeAnnotationId?: string | null; + activeAnnotationPage?: number | null; onAnnotationClick?: (id: string) => void; } = $props(); @@ -213,6 +215,7 @@ async function handleAnnotationDraw(rect: { x: number; y: number; width: number; const created: Annotation = await res.json(); annotations = [...annotations, created]; activeAnnotationId = created.id; + activeAnnotationPage = created.pageNumber; onAnnotationClick?.(created.id); } } catch { @@ -236,6 +239,8 @@ async function handleAnnotationDelete(annotationId: string) { function handleAnnotationClick(id: string) { activeAnnotationId = id; + const ann = annotations.find((a) => a.id === id); + activeAnnotationPage = ann?.pageNumber ?? null; onAnnotationClick?.(id); } diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index d2f96141..e5c820d6 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -56,6 +56,7 @@ async function loadFile(id: string) { let annotateMode = $state(false); let activeAnnotationId = $state(null); +let activeAnnotationPage = $state(null); // When an annotation is clicked, open the Diskussion tab. $effect(() => { @@ -97,6 +98,19 @@ onMount(() => { } localStorageRestored = true; + + function onKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') { + if (activeAnnotationId) { + activeAnnotationId = null; + activeAnnotationPage = null; + } else if (panelOpen) { + panelOpen = false; + } + } + } + document.addEventListener('keydown', onKeyDown); + return () => document.removeEventListener('keydown', onKeyDown); }); // Persist panel state whenever it changes (after initial restore). @@ -129,6 +143,7 @@ $effect(() => { error={fileError} bind:annotateMode={annotateMode} bind:activeAnnotationId={activeAnnotationId} + bind:activeAnnotationPage={activeAnnotationPage} onAnnotationClick={(id) => { activeAnnotationId = id; }} @@ -146,4 +161,9 @@ $effect(() => { bind:height={panelHeight} bind:activeTab={activeTab} activeAnnotationId={activeAnnotationId} + activeAnnotationPage={activeAnnotationPage} + onClearAnnotation={() => { + activeAnnotationId = null; + activeAnnotationPage = null; + }} />