From 8c2bdbd777da23befced8630708900d4a70eedd0 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 22:35:28 +0100 Subject: [PATCH 01/16] feat(frontend): add floating bottom panel to document detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the left sidebar layout with: - Full-viewport PDF/image viewer (never resizes, position: absolute) - Fixed floating bottom panel with tabs: Metadaten, Transkription, Diskussion, Verlauf - Compact top bar with title, date · sender → receivers row, and Annotieren / Edit / Download actions - Drag-to-resize panel with localStorage persistence of open/height/tab - Panel opens automatically to Diskussion when an annotation is clicked - Documents without a file default to showing the Metadaten tab New components: DocumentTopBar, DocumentViewer, DocumentBottomPanel, PanelMetadata, PanelTranscription, PanelDiscussion, PanelHistory PdfViewer: annotateMode and activeAnnotationId lifted to bindable props; AnnotationCommentPanel removed (discussion moves to the Diskussion tab). Closes #62 Co-Authored-By: Claude Sonnet 4.6 --- frontend/e2e/bottom-panel.spec.ts | 180 ++++ frontend/messages/de.json | 9 +- frontend/messages/en.json | 9 +- frontend/messages/es.json | 9 +- .../lib/components/DocumentBottomPanel.svelte | 196 ++++ .../src/lib/components/DocumentTopBar.svelte | 142 +++ .../src/lib/components/DocumentViewer.svelte | 93 ++ .../src/lib/components/PanelDiscussion.svelte | 85 ++ .../src/lib/components/PanelHistory.svelte | 446 ++++++++ .../src/lib/components/PanelMetadata.svelte | 202 ++++ .../lib/components/PanelTranscription.svelte | 42 + frontend/src/lib/components/PdfViewer.svelte | 79 +- .../src/routes/documents/[id]/+page.svelte | 952 ++---------------- 13 files changed, 1521 insertions(+), 923 deletions(-) create mode 100644 frontend/e2e/bottom-panel.spec.ts create mode 100644 frontend/src/lib/components/DocumentBottomPanel.svelte create mode 100644 frontend/src/lib/components/DocumentTopBar.svelte create mode 100644 frontend/src/lib/components/DocumentViewer.svelte create mode 100644 frontend/src/lib/components/PanelDiscussion.svelte create mode 100644 frontend/src/lib/components/PanelHistory.svelte create mode 100644 frontend/src/lib/components/PanelMetadata.svelte create mode 100644 frontend/src/lib/components/PanelTranscription.svelte diff --git a/frontend/e2e/bottom-panel.spec.ts b/frontend/e2e/bottom-panel.spec.ts new file mode 100644 index 00000000..55c2d074 --- /dev/null +++ b/frontend/e2e/bottom-panel.spec.ts @@ -0,0 +1,180 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PDF_FIXTURE = path.resolve(__dirname, 'fixtures/minimal.pdf'); + +/** + * Bottom panel E2E tests — issue #62. + * Verifies the new document detail layout: full-viewport viewer + floating bottom panel. + */ + +let pdfDocHref: string; +let noFileDocHref: string; + +test.describe('Document bottom panel', () => { + test.beforeAll(async ({ request }) => { + const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000'; + + // Create a document with a PDF and a date for metadata tests. + const createRes = await request.post('/api/documents', { + multipart: { title: 'E2E Bottom Panel Test', documentDate: '1945-05-08' } + }); + if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`); + const doc = await createRes.json(); + + const uploadRes = await request.put(`/api/documents/${doc.id}`, { + multipart: { + title: doc.title, + documentDate: '1945-05-08', + transcription: 'Dies ist eine vollständige Transkription des Dokuments für den E2E-Test.', + file: { + name: 'minimal.pdf', + mimeType: 'application/pdf', + buffer: fs.readFileSync(PDF_FIXTURE) + } + } + }); + if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`); + pdfDocHref = `${baseURL}/documents/${doc.id}`; + + // Create a document WITHOUT a file — panel should open to Metadaten by default. + const noFileRes = await request.post('/api/documents', { + multipart: { title: 'E2E Bottom Panel No-File Test' } + }); + if (!noFileRes.ok()) throw new Error(`Create no-file document failed: ${noFileRes.status()}`); + noFileDocHref = `${baseURL}/documents/${noFileRes.json().then ? (await noFileRes.json()).id : ''}`; + const noFileDoc = await noFileRes.json(); + noFileDocHref = `${baseURL}/documents/${noFileDoc.id}`; + }); + + test('bottom panel tab bar is visible and panel content is closed by default on a PDF document', async ({ + page + }) => { + test.setTimeout(30_000); + // Clear localStorage to ensure no previous panel state. + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + // Tab bar must always be visible. + await expect(page.getByRole('button', { name: 'Metadaten' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Transkription' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Diskussion' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Verlauf' })).toBeVisible(); + + // Panel content must NOT be visible when closed. + await expect(page.locator('[data-testid="bottom-panel-content"]')).not.toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-closed-default.png' }); + }); + + test('clicking Metadaten tab opens the panel and shows metadata content', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + await page.getByRole('button', { name: 'Metadaten' }).click(); + + // Panel content becomes visible. + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + + // Metadata section heading should be present. + await expect(page.getByText('Details', { exact: false })).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-metadata.png' }); + }); + + test('clicking Transkription tab shows transcription text', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + await page.getByRole('button', { name: 'Transkription' }).click(); + + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + await expect( + page.getByText('Dies ist eine vollständige Transkription', { exact: false }) + ).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-transcription.png' }); + }); + + test('clicking Diskussion tab shows the comment input', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + await page.getByRole('button', { name: 'Diskussion' }).click(); + + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + await expect(page.getByPlaceholder('Kommentar schreiben…')).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-discussion.png' }); + }); + + test('clicking × close button collapses the panel content', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + // Open the panel first. + await page.getByRole('button', { name: 'Metadaten' }).click(); + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + + // Close it. + await page.locator('[data-testid="panel-close-btn"]').click(); + await expect(page.locator('[data-testid="bottom-panel-content"]')).not.toBeVisible(); + + // Tab bar still visible after closing. + await expect(page.getByRole('button', { name: 'Metadaten' })).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-closed-after-x.png' }); + }); + + test('panel open state persists after page reload', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(pdfDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + // Open the panel to Diskussion. + await page.getByRole('button', { name: 'Diskussion' }).click(); + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + + // Reload — panel should re-open on the same tab. + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + await expect(page.getByPlaceholder('Kommentar schreiben…')).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-persisted.png' }); + }); + + test('document without a file opens panel to Metadaten by default', async ({ page }) => { + test.setTimeout(30_000); + await page.goto(noFileDocHref); + await page.evaluate(() => localStorage.clear()); + await page.reload(); + await page.waitForSelector('[data-hydrated]'); + + // Panel should be open to Metadaten by default when there is no file. + await expect(page.locator('[data-testid="bottom-panel-content"]')).toBeVisible(); + await expect(page.getByText('Details', { exact: false })).toBeVisible(); + + await page.screenshot({ path: 'test-results/e2e/bottom-panel-no-file-default.png' }); + }); +}); diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 8d8a86c9..992698cb 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -250,5 +250,12 @@ "comment_btn_reply": "Antworten", "comment_edited_label": "· bearbeitet", "comment_panel_title": "Kommentare", - "comment_panel_close": "Schließen" + "comment_panel_close": "Schließen", + "doc_panel_tab_metadata": "Metadaten", + "doc_panel_tab_transcription": "Transkription", + "doc_panel_tab_discussion": "Diskussion", + "doc_panel_tab_history": "Verlauf", + "doc_panel_annotate": "Annotieren", + "doc_panel_annotate_stop": "Fertig", + "doc_panel_annotation_thread_title": "Annotation" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 8b9fbdf5..e6124604 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -250,5 +250,12 @@ "comment_btn_reply": "Reply", "comment_edited_label": "· edited", "comment_panel_title": "Comments", - "comment_panel_close": "Close" + "comment_panel_close": "Close", + "doc_panel_tab_metadata": "Metadata", + "doc_panel_tab_transcription": "Transcription", + "doc_panel_tab_discussion": "Discussion", + "doc_panel_tab_history": "History", + "doc_panel_annotate": "Annotate", + "doc_panel_annotate_stop": "Done", + "doc_panel_annotation_thread_title": "Annotation" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 01d37915..1a672c48 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -250,5 +250,12 @@ "comment_btn_reply": "Responder", "comment_edited_label": "· editado", "comment_panel_title": "Comentarios", - "comment_panel_close": "Cerrar" + "comment_panel_close": "Cerrar", + "doc_panel_tab_metadata": "Metadatos", + "doc_panel_tab_transcription": "Transcripción", + "doc_panel_tab_discussion": "Discusión", + "doc_panel_tab_history": "Historial", + "doc_panel_annotate": "Anotar", + "doc_panel_annotate_stop": "Listo", + "doc_panel_annotation_thread_title": "Anotación" } diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte new file mode 100644 index 00000000..1a0725f0 --- /dev/null +++ b/frontend/src/lib/components/DocumentBottomPanel.svelte @@ -0,0 +1,196 @@ + + +
+ + + + +
+ {#each tabs as tab (tab.id)} + + {/each} + + +
+ + {#if open} + + {/if} +
+ + + {#if open} +
+ {#if activeTab === 'metadata'} + + {:else if activeTab === 'transcription'} + + {:else if activeTab === 'discussion'} + + {:else if activeTab === 'history'} + + {/if} +
+ {/if} +
diff --git a/frontend/src/lib/components/DocumentTopBar.svelte b/frontend/src/lib/components/DocumentTopBar.svelte new file mode 100644 index 00000000..fdca043d --- /dev/null +++ b/frontend/src/lib/components/DocumentTopBar.svelte @@ -0,0 +1,142 @@ + + +
+ +
+ +
+ +
+ +
+ +
+

+ {doc.title || doc.originalFilename} +

+ {#if compactMeta} +

+ {compactMeta} +

+ {/if} +
+
+ + +
+ {#if canAnnotate && isPdf} + + {/if} + + {#if canWrite} + + + {m.btn_edit()} + + {/if} + + {#if doc.filePath} + + + + {/if} +
+
diff --git a/frontend/src/lib/components/DocumentViewer.svelte b/frontend/src/lib/components/DocumentViewer.svelte new file mode 100644 index 00000000..bf13e0d8 --- /dev/null +++ b/frontend/src/lib/components/DocumentViewer.svelte @@ -0,0 +1,93 @@ + + +
+ {#if isLoading} +
+ + + + + {m.doc_loading()} +
+ {:else if error} +
+

{error}

+ {#if doc.filePath} + + {m.doc_download_link()} + + {/if} +
+ {:else if !doc.filePath} +
+
+ +
+

{m.doc_no_scan()}

+
+ {:else if fileUrl && doc.contentType?.startsWith('application/pdf')} + + {:else if fileUrl} +
+ {m.doc_image_alt()} +
+ {/if} +
diff --git a/frontend/src/lib/components/PanelDiscussion.svelte b/frontend/src/lib/components/PanelDiscussion.svelte new file mode 100644 index 00000000..89810ca8 --- /dev/null +++ b/frontend/src/lib/components/PanelDiscussion.svelte @@ -0,0 +1,85 @@ + + +
+ + {#if activeAnnotationId} +
+

+ {m.doc_panel_annotation_thread_title()} +

+ {#key activeAnnotationId} + onAnnotationCommentCountChange?.(activeAnnotationId, count)} + /> + {/key} +
+ {/if} + + +
+ {#if activeAnnotationId} +

+ {m.comment_section_title()} +

+ {/if} + +
+
diff --git a/frontend/src/lib/components/PanelHistory.svelte b/frontend/src/lib/components/PanelHistory.svelte new file mode 100644 index 00000000..766c268b --- /dev/null +++ b/frontend/src/lib/components/PanelHistory.svelte @@ -0,0 +1,446 @@ + + +
+ {#if historyLoading} +

{m.history_loading()}

+ {:else if !historyLoaded} + + {:else if versions.length === 0} +

{m.history_empty()}

+ {:else} + +
+ +
+ + {#if compareMode} +
+
+ + +
+
+ + +
+ +
+ {:else} + +
    + {#each versions as v, i (v.id)} +
  • + +
  • + {/each} +
+ {/if} + + + {#if diffLoading} +

{m.history_loading()}

+ {:else if noDiff} +
+ {m.history_diff_no_changes()} +
+ {:else if diffEntries.length > 0} +
+ {#each diffEntries as entry (entry.field)} +
+ {entry.label} + {#if entry.kind === 'text'} +

+ {#each entry.parts as part, partIdx (partIdx)} + {#if part.added} + {part.value} + {:else if part.removed} + {part.value} + {:else} + {part.value} + {/if} + {/each} +

+ {:else if entry.kind === 'scalar'} +
+ {entry.oldVal || '—'} + + {entry.newVal || '—'} +
+ {:else if entry.kind === 'relation'} +
+ {#each entry.removed as item (item)} + {item} + {/each} + {#each entry.added as item (item)} + {item} + {/each} +
+ {/if} +
+ {/each} +
+ {/if} + {/if} +
diff --git a/frontend/src/lib/components/PanelMetadata.svelte b/frontend/src/lib/components/PanelMetadata.svelte new file mode 100644 index 00000000..25a8219c --- /dev/null +++ b/frontend/src/lib/components/PanelMetadata.svelte @@ -0,0 +1,202 @@ + + +
+ +
+

+ {m.doc_section_details()} +

+
+ +
+ + + +
+ + {doc.documentDate ? formatDate(doc.documentDate) : '—'} + + {m.doc_label_document_date()} +
+
+ + +
+ + + +
+ + {doc.location ? doc.location : '—'} + + {m.doc_label_creation_location()} +
+
+ + + {#if doc.documentLocation} +
+ + + +
+ + {doc.documentLocation} + + {m.doc_label_archive_location_original()} +
+
+ {/if} + + + {#if doc.tags && doc.tags.length > 0} +
+ + + +
+
+ {#each doc.tags as tag (tag.id)} + + {tag.name} + + {/each} +
+ {m.form_label_tags()} +
+
+ {/if} +
+
+ + +
+

+ {m.doc_section_persons()} +

+ + + +
+ {m.form_label_receivers()} + {#if doc.receivers && doc.receivers.length > 0} +
+ {#each doc.receivers as receiver (receiver.id)} + + {/each} +
+ {:else} + {m.doc_no_receivers()} + {/if} +
+
+
diff --git a/frontend/src/lib/components/PanelTranscription.svelte b/frontend/src/lib/components/PanelTranscription.svelte new file mode 100644 index 00000000..f2129ca3 --- /dev/null +++ b/frontend/src/lib/components/PanelTranscription.svelte @@ -0,0 +1,42 @@ + + +
+
+ {#if !doc.summary && !doc.transcription} +

+ {/if} + + {#if doc.summary} +
+ + {m.doc_label_summary()} + +

{doc.summary}

+
+ {/if} + + {#if doc.transcription} +
+ + {m.form_label_transcription()} + +

+ {doc.transcription} +

+
+ {/if} +
+
diff --git a/frontend/src/lib/components/PdfViewer.svelte b/frontend/src/lib/components/PdfViewer.svelte index 1a9ff68a..2ac3e6b2 100644 --- a/frontend/src/lib/components/PdfViewer.svelte +++ b/frontend/src/lib/components/PdfViewer.svelte @@ -3,22 +3,19 @@ import { onMount } from 'svelte'; import { SvelteMap } from 'svelte/reactivity'; import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from 'pdfjs-dist'; import AnnotationLayer from './AnnotationLayer.svelte'; -import AnnotationCommentPanel from './AnnotationCommentPanel.svelte'; let { url, documentId = '', - canAnnotate = false, - canComment, - currentUserId, - canAdmin + annotateMode = $bindable(false), + activeAnnotationId = $bindable(null), + onAnnotationClick }: { url: string; documentId?: string; - canAnnotate?: boolean; - canComment?: boolean; - currentUserId?: string | null; - canAdmin?: boolean; + annotateMode?: boolean; + activeAnnotationId?: string | null; + onAnnotationClick?: (id: string) => void; } = $props(); let pdfDoc = $state(null); @@ -54,10 +51,8 @@ type Annotation = { }; let annotations = $state([]); -let annotateMode = $state(false); let annotateColor = $state('#ffff00'); let commentCounts = new SvelteMap(); -let activeAnnotationId = $state(null); onMount(async () => { // Dynamic import keeps pdfjs out of the SSR bundle entirely @@ -218,6 +213,7 @@ async function handleAnnotationDraw(rect: { x: number; y: number; width: number; const created: Annotation = await res.json(); annotations = [...annotations, created]; activeAnnotationId = created.id; + onAnnotationClick?.(created.id); } } catch { // ignore @@ -238,6 +234,11 @@ async function handleAnnotationDelete(annotationId: string) { } } +function handleAnnotationClick(id: string) { + activeAnnotationId = id; + onAnnotationClick?.(id); +} + $effect(() => { if (pdfjsReady && url) { loadDocument(url); @@ -385,35 +386,15 @@ function zoomOut() { - - {#if canAnnotate} -
- - {#if annotateMode} - - {/if} -
- {:else} - + + {#if annotateMode} + {/if} @@ -445,27 +426,11 @@ function zoomOut() { onDraw={handleAnnotationDraw} onDelete={handleAnnotationDelete} commentCounts={Object.fromEntries(commentCounts)} - onAnnotationClick={(id) => (activeAnnotationId = id)} + onAnnotationClick={handleAnnotationClick} /> {/if} - - {#key activeAnnotationId} - {#if activeAnnotationId} - (activeAnnotationId = null)} - onCountChange={(count) => { - if (activeAnnotationId) commentCounts.set(activeAnnotationId, count); - }} - /> - {/if} - {/key} {/if} diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index 2ec4747c..d2f96141 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -1,10 +1,10 @@ -
- -
-
- -
- -
- {m.btn_back()} -
+ + {doc.title || doc.originalFilename || 'Dokument'} + -
-

- {doc.title || doc.originalFilename} -

- - {doc.status} - -
-
+
+ -
- {#if data.canWrite} - - - {m.btn_edit()} - - {/if} - - {#if doc.filePath} - - - - {/if} -
-
- - -
- - - - -
- {#if isLoading} -
- - - - - {m.doc_loading()} -
- {:else if error} -
-

{error}

- {#if doc.filePath} - - {m.doc_download_link()} - - {/if} -
- {:else if !doc.filePath} -
-
- -
-

{m.doc_no_scan()}

-
- {:else if fileUrl && doc.contentType?.startsWith('application/pdf')} - - {:else if fileUrl} -
- {m.doc_image_alt()} -
- {/if} -
+
+ { + activeAnnotationId = id; + }} + />
+ + -- 2.49.1 From ecfd80bf9ad204ec878d9d58e25c71e5f5770dd6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 22:45:35 +0100 Subject: [PATCH 02/16] 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; + }} /> -- 2.49.1 From ee85ce466866d290a14c052fe26c73bd76406812 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 22:49:44 +0100 Subject: [PATCH 03/16] feat(frontend): keep annotation tab after switching to document discussion Clicking the Diskussion sub-tab no longer deselects the active annotation, so the Annotation tab stays visible and accessible for easy toggling back. The annotation is cleared only via Escape or clicking elsewhere on the PDF. Removes the now-unused onClearAnnotation callback chain. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/DocumentBottomPanel.svelte | 3 --- frontend/src/lib/components/PanelDiscussion.svelte | 3 --- frontend/src/routes/documents/[id]/+page.svelte | 4 ---- 3 files changed, 10 deletions(-) diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte index adc8c2f9..514f808f 100644 --- a/frontend/src/lib/components/DocumentBottomPanel.svelte +++ b/frontend/src/lib/components/DocumentBottomPanel.svelte @@ -50,7 +50,6 @@ type Props = { activeTab: Tab; activeAnnotationId: string | null; activeAnnotationPage: number | null; - onClearAnnotation?: () => void; onAnnotationCommentCountChange?: (annotationId: string, count: number) => void; }; @@ -65,7 +64,6 @@ let { activeTab = $bindable(), activeAnnotationId, activeAnnotationPage, - onClearAnnotation, onAnnotationCommentCountChange }: Props = $props(); @@ -191,7 +189,6 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT); canComment={canComment} currentUserId={currentUserId} canAdmin={canAdmin} - onClearAnnotation={onClearAnnotation} onAnnotationCommentCountChange={onAnnotationCommentCountChange} /> {:else if activeTab === 'history'} diff --git a/frontend/src/lib/components/PanelDiscussion.svelte b/frontend/src/lib/components/PanelDiscussion.svelte index e15658fb..8ebf4abd 100644 --- a/frontend/src/lib/components/PanelDiscussion.svelte +++ b/frontend/src/lib/components/PanelDiscussion.svelte @@ -29,7 +29,6 @@ type Props = { canComment: boolean; currentUserId: string | null; canAdmin: boolean; - onClearAnnotation?: () => void; onAnnotationCommentCountChange?: (annotationId: string, count: number) => void; }; @@ -41,7 +40,6 @@ let { canComment, currentUserId, canAdmin, - onClearAnnotation, onAnnotationCommentCountChange }: Props = $props(); @@ -64,7 +62,6 @@ $effect(() => { function selectDocumentTab() { activeSubTab = 'document'; - onClearAnnotation?.(); } function selectAnnotationTab() { diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index e5c820d6..a16e7c13 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -162,8 +162,4 @@ $effect(() => { bind:activeTab={activeTab} activeAnnotationId={activeAnnotationId} activeAnnotationPage={activeAnnotationPage} - onClearAnnotation={() => { - activeAnnotationId = null; - activeAnnotationPage = null; - }} /> -- 2.49.1 From 8519fbb48a4d91ce3de3fb2d3621974b04ce7044 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 22:53:02 +0100 Subject: [PATCH 04/16] fix(frontend): lock document page to viewport with position: fixed The global layout wraps pages in min-h-screen + main.py-6, which pushed the h-screen document container below the sticky nav and caused page-level scrolling. Switching to fixed inset-0 z-50 fully escapes the layout flow: - DocumentTopBar always visible (no scrolling it away) - PDF controls always visible - Only the PDF canvas area scrolls - DocumentBottomPanel moved inside the fixed container (logically grouped) Co-Authored-By: Claude Sonnet 4.6 --- .../src/routes/documents/[id]/+page.svelte | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index a16e7c13..b866dc0f 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -126,7 +126,7 @@ $effect(() => { {doc.title || doc.originalFilename || 'Dokument'} -
+
{ }} />
-
- + +
-- 2.49.1 From c9b4e6dad4f1f70fc3ba185b9a4df2c2526493d4 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 22:59:53 +0100 Subject: [PATCH 05/16] feat(frontend): add annotation visibility toggle to PDF toolbar Eye/eye-slash button in the PDF controls bar lets the user hide all annotation highlights to read the document unobstructed and show them again with one click. - Button only renders when at least one annotation exists - Active state (hidden) highlighted with brand-mint/bg-white/10 so the current state is always clear - i18n keys added for de/en/es Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 4 +- frontend/messages/en.json | 4 +- frontend/messages/es.json | 4 +- frontend/src/lib/components/PdfViewer.svelte | 69 +++++++++++++++++--- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 6004793b..6d663f90 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -258,5 +258,7 @@ "doc_panel_annotate": "Annotieren", "doc_panel_annotate_stop": "Fertig", "doc_panel_annotation_thread_title": "Annotation", - "doc_panel_discussion_annotation_tab": "Annotation · Seite {page}" + "doc_panel_discussion_annotation_tab": "Annotation · Seite {page}", + "pdf_annotations_show": "Annotierungen anzeigen", + "pdf_annotations_hide": "Annotierungen verbergen" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index c4669686..92ce9ceb 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -258,5 +258,7 @@ "doc_panel_annotate": "Annotate", "doc_panel_annotate_stop": "Done", "doc_panel_annotation_thread_title": "Annotation", - "doc_panel_discussion_annotation_tab": "Annotation · Page {page}" + "doc_panel_discussion_annotation_tab": "Annotation · Page {page}", + "pdf_annotations_show": "Show annotations", + "pdf_annotations_hide": "Hide annotations" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 7e9ff8e8..8e3dd84b 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -258,5 +258,7 @@ "doc_panel_annotate": "Anotar", "doc_panel_annotate_stop": "Listo", "doc_panel_annotation_thread_title": "Anotación", - "doc_panel_discussion_annotation_tab": "Anotación · Página {page}" + "doc_panel_discussion_annotation_tab": "Anotación · Página {page}", + "pdf_annotations_show": "Mostrar anotaciones", + "pdf_annotations_hide": "Ocultar anotaciones" } diff --git a/frontend/src/lib/components/PdfViewer.svelte b/frontend/src/lib/components/PdfViewer.svelte index 5f15bc9a..6c3deb33 100644 --- a/frontend/src/lib/components/PdfViewer.svelte +++ b/frontend/src/lib/components/PdfViewer.svelte @@ -3,6 +3,7 @@ import { onMount } from 'svelte'; import { SvelteMap } from 'svelte/reactivity'; import type { PDFDocumentProxy, PDFPageProxy, RenderTask } from 'pdfjs-dist'; import AnnotationLayer from './AnnotationLayer.svelte'; +import { m } from '$lib/paraglide/messages.js'; let { url, @@ -55,6 +56,7 @@ type Annotation = { let annotations = $state([]); let annotateColor = $state('#ffff00'); let commentCounts = new SvelteMap(); +let showAnnotations = $state(true); onMount(async () => { // Dynamic import keeps pdfjs out of the SSR bundle entirely @@ -403,6 +405,53 @@ function zoomOut() { {/if}
+ + {#if annotations.length > 0} + + {/if} +
{#if loading} @@ -424,15 +473,17 @@ function zoomOut() { class="textLayer" style="position: absolute; top: 0; left: 0; overflow: hidden; pointer-events: none; line-height: 1;" >
- a.pageNumber === currentPage)} - canAnnotate={annotateMode} - color={annotateColor} - onDraw={handleAnnotationDraw} - onDelete={handleAnnotationDelete} - commentCounts={Object.fromEntries(commentCounts)} - onAnnotationClick={handleAnnotationClick} - /> + {#if showAnnotations} + a.pageNumber === currentPage)} + canAnnotate={annotateMode} + color={annotateColor} + onDraw={handleAnnotationDraw} + onDelete={handleAnnotationDelete} + commentCounts={Object.fromEntries(commentCounts)} + onAnnotationClick={handleAnnotationClick} + /> + {/if}
{/if} -- 2.49.1 From a392e85f4308a56945ef43fbe1833ae389853ecd Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 23:03:37 +0100 Subject: [PATCH 06/16] fix(frontend): move annotation toggle into PDF toolbar and add text label Button was rendered outside the controls bar (below the toolbar). Moved it inside so it stays in the same row as zoom and page controls. Added a text label next to the eye icon so the action is self-descriptive. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/PdfViewer.svelte | 69 +++++++++----------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/frontend/src/lib/components/PdfViewer.svelte b/frontend/src/lib/components/PdfViewer.svelte index 6c3deb33..323c3cc6 100644 --- a/frontend/src/lib/components/PdfViewer.svelte +++ b/frontend/src/lib/components/PdfViewer.svelte @@ -403,54 +403,45 @@ function zoomOut() { title="Farbe wählen" /> {/if} - - - - {#if annotations.length > 0} - - {/if} + {showAnnotations ? m.pdf_annotations_hide() : m.pdf_annotations_show()} + + {/if} +
-- 2.49.1 From f18649fb79a2b7767da69cf16f4ed8621e210039 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 23:06:42 +0100 Subject: [PATCH 07/16] feat(frontend): open bottom panel at full height (80vh) by default Panel now opens to 80 % of the viewport height so the user can immediately read comments and metadata without having to drag it up first. The user can drag the top handle down to make it smaller; that size is persisted to localStorage and restored on the next visit. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/DocumentBottomPanel.svelte | 5 ++--- frontend/src/routes/documents/[id]/+page.svelte | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte index 514f808f..f2bdfd3c 100644 --- a/frontend/src/lib/components/DocumentBottomPanel.svelte +++ b/frontend/src/lib/components/DocumentBottomPanel.svelte @@ -68,7 +68,6 @@ let { }: Props = $props(); const MIN_HEIGHT = 52; // drag handle (8px) + tab bar (~44px) -const DEFAULT_HEIGHT = 320; let isDragging = $state(false); let dragStartY = 0; @@ -78,7 +77,7 @@ function openTab(tab: Tab) { activeTab = tab; if (!open) { open = true; - if (height <= MIN_HEIGHT) height = DEFAULT_HEIGHT; + if (height <= MIN_HEIGHT) height = Math.floor(window.innerHeight * 0.8); } } @@ -104,7 +103,7 @@ function onDragMove(e: PointerEvent) { open = false; } else { open = true; - height = Math.max(DEFAULT_HEIGHT / 4, Math.min(newHeight, maxHeight)); + height = Math.max(80, Math.min(newHeight, maxHeight)); } } diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index b866dc0f..5e26f946 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -73,7 +73,7 @@ const LS_KEY_HEIGHT = 'doc-panel-height'; const LS_KEY_TAB = 'doc-panel-tab'; let panelOpen = $state(false); -let panelHeight = $state(320); +let panelHeight = $state(0); // set to 80vh on mount let activeTab = $state('metadata'); let localStorageRestored = $state(false); @@ -85,6 +85,7 @@ onMount(() => { if (savedTab && ['metadata', 'transcription', 'discussion', 'history'].includes(savedTab)) { activeTab = savedTab as Tab; } + panelHeight = Math.floor(window.innerHeight * 0.8); if (savedHeight) { const h = parseInt(savedHeight, 10); if (!isNaN(h) && h >= 80) panelHeight = h; -- 2.49.1 From 1eb2659ba0d8a99b7802ff3278076b527d5e4e46 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 23:10:26 +0100 Subject: [PATCH 08/16] fix(frontend): open bottom panel to full height below the document header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of an arbitrary 80 % cap, the panel now measures the actual DocumentTopBar height at open time and fills the remaining viewport exactly — so the PDF is fully covered and the drawer reaches right up to the header. Drag-to-shrink still works as before. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/DocumentBottomPanel.svelte | 9 +++++++-- frontend/src/lib/components/DocumentTopBar.svelte | 1 + frontend/src/routes/documents/[id]/+page.svelte | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte index f2bdfd3c..c3e509ca 100644 --- a/frontend/src/lib/components/DocumentBottomPanel.svelte +++ b/frontend/src/lib/components/DocumentBottomPanel.svelte @@ -73,11 +73,16 @@ let isDragging = $state(false); let dragStartY = 0; let dragStartHeight = 0; +function fullHeight() { + const topbar = document.querySelector('[data-topbar]'); + return window.innerHeight - (topbar?.getBoundingClientRect().height ?? 0); +} + function openTab(tab: Tab) { activeTab = tab; if (!open) { open = true; - if (height <= MIN_HEIGHT) height = Math.floor(window.innerHeight * 0.8); + if (height <= MIN_HEIGHT) height = fullHeight(); } } @@ -96,7 +101,7 @@ function onDragMove(e: PointerEvent) { if (!isDragging) return; const delta = dragStartY - e.clientY; // positive = dragging up = bigger panel const newHeight = dragStartHeight + delta; - const maxHeight = Math.floor(window.innerHeight * 0.8); + const maxHeight = fullHeight(); if (newHeight <= MIN_HEIGHT + 20) { // collapsed past threshold → close diff --git a/frontend/src/lib/components/DocumentTopBar.svelte b/frontend/src/lib/components/DocumentTopBar.svelte index fdca043d..16325051 100644 --- a/frontend/src/lib/components/DocumentTopBar.svelte +++ b/frontend/src/lib/components/DocumentTopBar.svelte @@ -59,6 +59,7 @@ const compactMeta = $derived.by(() => {
diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index 5e26f946..91bb46f3 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -85,7 +85,8 @@ onMount(() => { if (savedTab && ['metadata', 'transcription', 'discussion', 'history'].includes(savedTab)) { activeTab = savedTab as Tab; } - panelHeight = Math.floor(window.innerHeight * 0.8); + const topbar = document.querySelector('[data-topbar]'); + panelHeight = window.innerHeight - (topbar?.getBoundingClientRect().height ?? 0); if (savedHeight) { const h = parseInt(savedHeight, 10); if (!isNaN(h) && h >= 80) panelHeight = h; -- 2.49.1 From 65457a5650cfad01c0f360cc4ff3422b7f3e5c5f Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 23:15:47 +0100 Subject: [PATCH 09/16] feat(frontend): show history diff inline below the selected version Instead of rendering the diff at the bottom of the list (requiring the user to scroll down), it now appears directly below whichever version item was clicked. Compare-mode diff stays at the bottom of the compare form where it makes sense, since it is not tied to a specific list item. Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/PanelHistory.svelte | 217 ++++++++++++------ 1 file changed, 145 insertions(+), 72 deletions(-) diff --git a/frontend/src/lib/components/PanelHistory.svelte b/frontend/src/lib/components/PanelHistory.svelte index 766c268b..2124be49 100644 --- a/frontend/src/lib/components/PanelHistory.svelte +++ b/frontend/src/lib/components/PanelHistory.svelte @@ -333,8 +333,79 @@ $effect(() => { {m.history_compare_apply()}
+ + + {#if diffLoading} +

{m.history_loading()}

+ {:else if noDiff} +
+ {m.history_diff_no_changes()} +
+ {:else if diffEntries.length > 0} +
+ {#each diffEntries as entry (entry.field)} +
+ {entry.label} + {#if entry.kind === 'text'} +

+ {#each entry.parts as part, partIdx (partIdx)} + {#if part.added} + {part.value} + {:else if part.removed} + {part.value} + {:else} + {part.value} + {/if} + {/each} +

+ {:else if entry.kind === 'scalar'} +
+ {entry.oldVal || '—'} + + {entry.newVal || '—'} +
+ {:else if entry.kind === 'relation'} +
+ {#each entry.removed as item (item)} + {item} + {/each} + {#each entry.added as item (item)} + {item} + {/each} +
+ {/if} +
+ {/each} +
+ {/if} {:else} - +
    {#each versions as v, i (v.id)}
  • @@ -367,80 +438,82 @@ $effect(() => {
{/if} + + + {#if selectedVersionId === v.id} + {#if diffLoading} +

{m.history_loading()}

+ {:else if noDiff} +
+ {m.history_diff_no_changes()} +
+ {:else if diffEntries.length > 0} +
+ {#each diffEntries as entry (entry.field)} +
+ {entry.label} + {#if entry.kind === 'text'} +

+ {#each entry.parts as part, partIdx (partIdx)} + {#if part.added} + {part.value} + {:else if part.removed} + {part.value} + {:else} + {part.value} + {/if} + {/each} +

+ {:else if entry.kind === 'scalar'} +
+ {entry.oldVal || '—'} + + {entry.newVal || '—'} +
+ {:else if entry.kind === 'relation'} +
+ {#each entry.removed as item (item)} + {item} + {/each} + {#each entry.added as item (item)} + {item} + {/each} +
+ {/if} +
+ {/each} +
+ {/if} + {/if} {/each} {/if} - - - {#if diffLoading} -

{m.history_loading()}

- {:else if noDiff} -
- {m.history_diff_no_changes()} -
- {:else if diffEntries.length > 0} -
- {#each diffEntries as entry (entry.field)} -
- {entry.label} - {#if entry.kind === 'text'} -

- {#each entry.parts as part, partIdx (partIdx)} - {#if part.added} - {part.value} - {:else if part.removed} - {part.value} - {:else} - {part.value} - {/if} - {/each} -

- {:else if entry.kind === 'scalar'} -
- {entry.oldVal || '—'} - - {entry.newVal || '—'} -
- {:else if entry.kind === 'relation'} -
- {#each entry.removed as item (item)} - {item} - {/each} - {#each entry.added as item (item)} - {item} - {/each} -
- {/if} -
- {/each} -
- {/if} {/if}
-- 2.49.1 From db9d8ed45796c3c3b5a5844d5cfceae061234480 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 23:18:21 +0100 Subject: [PATCH 10/16] feat(frontend): add Note-Add-MD icon to the Annotieren button Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/DocumentTopBar.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/DocumentTopBar.svelte b/frontend/src/lib/components/DocumentTopBar.svelte index 16325051..7375f1bc 100644 --- a/frontend/src/lib/components/DocumentTopBar.svelte +++ b/frontend/src/lib/components/DocumentTopBar.svelte @@ -101,10 +101,16 @@ const compactMeta = $derived.by(() => { {/if} -- 2.49.1 From c19f7b3b1af87f9deeb22e628ffe75e1e4c80fa9 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 23:20:49 +0100 Subject: [PATCH 11/16] fix(frontend): correct path for Note-Add-MD icon on Annotieren button Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/DocumentTopBar.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/components/DocumentTopBar.svelte b/frontend/src/lib/components/DocumentTopBar.svelte index 7375f1bc..06bc3a17 100644 --- a/frontend/src/lib/components/DocumentTopBar.svelte +++ b/frontend/src/lib/components/DocumentTopBar.svelte @@ -106,7 +106,7 @@ const compactMeta = $derived.by(() => { : 'border border-brand-navy text-brand-navy hover:bg-brand-navy hover:text-white'}" > Date: Tue, 24 Mar 2026 23:21:38 +0100 Subject: [PATCH 12/16] feat(frontend): close bottom panel when entering annotate mode Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/documents/[id]/+page.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index 91bb46f3..f0c39298 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -66,6 +66,11 @@ $effect(() => { } }); +// Close the panel when entering annotate mode so the PDF is fully visible. +$effect(() => { + if (annotateMode) panelOpen = false; +}); + // ── Bottom panel state ──────────────────────────────────────────────────────── const LS_KEY_OPEN = 'doc-panel-open'; -- 2.49.1 From 5ea5590c89714a9548361ffe8f942afe551af484 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 24 Mar 2026 23:26:29 +0100 Subject: [PATCH 13/16] fix(frontend): restore global nav bar on document detail page The document viewer container was using fixed inset-0 z-50 which covered the sticky global nav bar. Now measures nav height at mount and offsets the container top accordingly, dropping z-index to z-40. Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/DocumentBottomPanel.svelte | 2 +- frontend/src/routes/documents/[id]/+page.svelte | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte index c3e509ca..cb2dbc02 100644 --- a/frontend/src/lib/components/DocumentBottomPanel.svelte +++ b/frontend/src/lib/components/DocumentBottomPanel.svelte @@ -75,7 +75,7 @@ let dragStartHeight = 0; function fullHeight() { const topbar = document.querySelector('[data-topbar]'); - return window.innerHeight - (topbar?.getBoundingClientRect().height ?? 0); + return window.innerHeight - (topbar?.getBoundingClientRect().bottom ?? 0); } function openTab(tab: Tab) { diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index f0c39298..1e76b93c 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -78,11 +78,14 @@ const LS_KEY_HEIGHT = 'doc-panel-height'; const LS_KEY_TAB = 'doc-panel-tab'; let panelOpen = $state(false); -let panelHeight = $state(0); // set to 80vh on mount +let panelHeight = $state(0); // set to full height on mount +let navHeight = $state(0); let activeTab = $state('metadata'); let localStorageRestored = $state(false); onMount(() => { + navHeight = document.querySelector('header')?.getBoundingClientRect().height ?? 0; + const savedOpen = localStorage.getItem(LS_KEY_OPEN); const savedHeight = localStorage.getItem(LS_KEY_HEIGHT); const savedTab = localStorage.getItem(LS_KEY_TAB); @@ -91,7 +94,7 @@ onMount(() => { activeTab = savedTab as Tab; } const topbar = document.querySelector('[data-topbar]'); - panelHeight = window.innerHeight - (topbar?.getBoundingClientRect().height ?? 0); + panelHeight = window.innerHeight - navHeight - (topbar?.getBoundingClientRect().height ?? 0); if (savedHeight) { const h = parseInt(savedHeight, 10); if (!isNaN(h) && h >= 80) panelHeight = h; @@ -133,7 +136,11 @@ $effect(() => { {doc.title || doc.originalFilename || 'Dokument'} -
+
Date: Wed, 25 Mar 2026 07:04:20 +0100 Subject: [PATCH 14/16] fix(frontend): always start with panel closed on document open Removed localStorage persistence for the open/closed state so the PDF is always visible first when navigating to a document. Height and active tab are still remembered across visits. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/documents/[id]/+page.svelte | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index 1e76b93c..b3682c10 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -73,7 +73,6 @@ $effect(() => { // ── Bottom panel state ──────────────────────────────────────────────────────── -const LS_KEY_OPEN = 'doc-panel-open'; const LS_KEY_HEIGHT = 'doc-panel-height'; const LS_KEY_TAB = 'doc-panel-tab'; @@ -86,7 +85,6 @@ let localStorageRestored = $state(false); onMount(() => { navHeight = document.querySelector('header')?.getBoundingClientRect().height ?? 0; - const savedOpen = localStorage.getItem(LS_KEY_OPEN); const savedHeight = localStorage.getItem(LS_KEY_HEIGHT); const savedTab = localStorage.getItem(LS_KEY_TAB); @@ -99,13 +97,6 @@ onMount(() => { const h = parseInt(savedHeight, 10); if (!isNaN(h) && h >= 80) panelHeight = h; } - if (savedOpen !== null) { - panelOpen = savedOpen === 'true'; - } else if (!doc.filePath) { - // No previous state and no file → open to Metadaten by default - panelOpen = true; - activeTab = 'metadata'; - } localStorageRestored = true; @@ -126,7 +117,6 @@ onMount(() => { // Persist panel state whenever it changes (after initial restore). $effect(() => { if (!localStorageRestored) return; - localStorage.setItem(LS_KEY_OPEN, String(panelOpen)); localStorage.setItem(LS_KEY_HEIGHT, String(panelHeight)); localStorage.setItem(LS_KEY_TAB, activeTab); }); -- 2.49.1 From f71712ab4bca0eff563971b54b90be931642b4eb Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 25 Mar 2026 07:23:20 +0100 Subject: [PATCH 15/16] feat(frontend): move annotation comments to right-side panel Annotation threads now open in a slide-in side panel (320 px, right edge of the PDF viewer) instead of expanding the bottom drawer. The PDF stays visible while the user reads and writes annotation comments. - Add AnnotationSidePanel component (absolute-positioned, CSS slide transition, keyed CommentThread, close via X or Escape) - Remove the $effect that opened the bottom drawer on annotation click - Simplify PanelDiscussion back to document-level thread only (no annotation sub-tabs) - Remove annotation-related props from DocumentBottomPanel and PanelDiscussion Co-Authored-By: Claude Sonnet 4.6 --- .../lib/components/AnnotationSidePanel.svelte | 65 +++++++++++ .../lib/components/DocumentBottomPanel.svelte | 11 +- .../src/lib/components/PanelDiscussion.svelte | 105 ++---------------- .../src/routes/documents/[id]/+page.svelte | 23 ++-- 4 files changed, 88 insertions(+), 116 deletions(-) create mode 100644 frontend/src/lib/components/AnnotationSidePanel.svelte diff --git a/frontend/src/lib/components/AnnotationSidePanel.svelte b/frontend/src/lib/components/AnnotationSidePanel.svelte new file mode 100644 index 00000000..0a337662 --- /dev/null +++ b/frontend/src/lib/components/AnnotationSidePanel.svelte @@ -0,0 +1,65 @@ + + +
+ +
+ + {m.doc_panel_discussion_annotation_tab({ page: String(activeAnnotationPage ?? '?') })} + + +
+ + +
+ {#if activeAnnotationId} + {#key activeAnnotationId} + + {/key} + {/if} +
+
diff --git a/frontend/src/lib/components/DocumentBottomPanel.svelte b/frontend/src/lib/components/DocumentBottomPanel.svelte index cb2dbc02..33de26e6 100644 --- a/frontend/src/lib/components/DocumentBottomPanel.svelte +++ b/frontend/src/lib/components/DocumentBottomPanel.svelte @@ -48,9 +48,6 @@ type Props = { open: boolean; height: number; activeTab: Tab; - activeAnnotationId: string | null; - activeAnnotationPage: number | null; - onAnnotationCommentCountChange?: (annotationId: string, count: number) => void; }; let { @@ -61,10 +58,7 @@ let { canAdmin, open = $bindable(), height = $bindable(), - activeTab = $bindable(), - activeAnnotationId, - activeAnnotationPage, - onAnnotationCommentCountChange + activeTab = $bindable() }: Props = $props(); const MIN_HEIGHT = 52; // drag handle (8px) + tab bar (~44px) @@ -187,13 +181,10 @@ const panelHeight = $derived(open ? height : MIN_HEIGHT); {:else if activeTab === 'discussion'} {:else if activeTab === 'history'} diff --git a/frontend/src/lib/components/PanelDiscussion.svelte b/frontend/src/lib/components/PanelDiscussion.svelte index 8ebf4abd..22fb9d6b 100644 --- a/frontend/src/lib/components/PanelDiscussion.svelte +++ b/frontend/src/lib/components/PanelDiscussion.svelte @@ -1,5 +1,4 @@ -
- - {#if activeAnnotationId} -
- - -
- {/if} - - -
- {#if !activeAnnotationId || activeSubTab === 'document'} - - (docCommentCount = count)} - /> - {:else} - - {#key activeAnnotationId} - onAnnotationCommentCountChange?.(activeAnnotationId, count)} - /> - {/key} - {/if} -
+
+
diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index b3682c10..9142083d 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -3,6 +3,7 @@ import { onMount } from 'svelte'; import DocumentTopBar from '$lib/components/DocumentTopBar.svelte'; import DocumentViewer from '$lib/components/DocumentViewer.svelte'; import DocumentBottomPanel from '$lib/components/DocumentBottomPanel.svelte'; +import AnnotationSidePanel from '$lib/components/AnnotationSidePanel.svelte'; type Tab = 'metadata' | 'transcription' | 'discussion' | 'history'; @@ -58,14 +59,6 @@ let annotateMode = $state(false); let activeAnnotationId = $state(null); let activeAnnotationPage = $state(null); -// When an annotation is clicked, open the Diskussion tab. -$effect(() => { - if (activeAnnotationId) { - activeTab = 'discussion'; - panelOpen = true; - } -}); - // Close the panel when entering annotate mode so the PDF is fully visible. $effect(() => { if (annotateMode) panelOpen = false; @@ -152,6 +145,18 @@ $effect(() => { activeAnnotationId = id; }} /> + { + activeAnnotationId = null; + activeAnnotationPage = null; + }} + />
{ bind:open={panelOpen} bind:height={panelHeight} bind:activeTab={activeTab} - activeAnnotationId={activeAnnotationId} - activeAnnotationPage={activeAnnotationPage} />
-- 2.49.1 From dd360ade8b2a024d54b2f593f2a91360cd67969b Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 25 Mar 2026 07:33:59 +0100 Subject: [PATCH 16/16] fix(frontend): fix side panel X button click falling through to PDF toolbar pointer-events-none and pointer-events-auto were both present as static and conditional Tailwind classes simultaneously. CSS specificity meant pointer-events-none always won, so clicks passed through to the annotation toggle button behind the panel. Now pointer-events-none is only applied when the panel is hidden (translated off-screen). Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/components/AnnotationSidePanel.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/AnnotationSidePanel.svelte b/frontend/src/lib/components/AnnotationSidePanel.svelte index 0a337662..0b264414 100644 --- a/frontend/src/lib/components/AnnotationSidePanel.svelte +++ b/frontend/src/lib/components/AnnotationSidePanel.svelte @@ -26,9 +26,9 @@ const visible = $derived(activeAnnotationId !== null);
-- 2.49.1