feat(frontend): add floating bottom panel to document detail page
Some checks failed
CI / Unit & Component Tests (push) Successful in 4m47s
CI / Backend Unit Tests (push) Successful in 2m20s
CI / E2E Tests (push) Failing after 24m42s

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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-24 22:35:28 +01:00
parent 34c66f80fc
commit 8c2bdbd777
13 changed files with 1521 additions and 923 deletions

View File

@@ -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<string | null>(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<PDFDocumentProxy | null>(null);
@@ -54,10 +51,8 @@ type Annotation = {
};
let annotations = $state<Annotation[]>([]);
let annotateMode = $state(false);
let annotateColor = $state('#ffff00');
let commentCounts = new SvelteMap<string, number>();
let activeAnnotationId = $state<string | null>(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() {
</button>
</div>
<!-- Annotate controls -->
{#if canAnnotate}
<div class="flex items-center gap-1">
<button
onclick={() => (annotateMode = !annotateMode)}
aria-label={annotateMode ? 'Annotieren beenden' : 'Annotieren'}
class="rounded px-2 py-1 font-sans text-xs text-gray-300 transition hover:bg-white/10 {annotateMode ? 'bg-white/20' : ''}"
>
{annotateMode ? 'Fertig' : 'Annotieren'}
</button>
{#if annotateMode}
<input
type="color"
bind:value={annotateColor}
aria-label="Farbe wählen"
class="h-6 w-6 cursor-pointer rounded border-0 bg-transparent p-0"
title="Farbe wählen"
/>
{/if}
</div>
{:else}
<button
disabled
title="Sie benötigen die Berechtigung ANNOTATE_ALL zum Annotieren"
class="cursor-not-allowed rounded px-2 py-1 font-sans text-xs text-gray-500"
aria-label="Annotieren (keine Berechtigung)"
>
Annotieren
</button>
<!-- Color picker (shown in annotate mode) -->
{#if annotateMode}
<input
type="color"
bind:value={annotateColor}
aria-label="Farbe wählen"
class="h-6 w-6 cursor-pointer rounded border-0 bg-transparent p-0"
title="Farbe wählen"
/>
{/if}
</div>
@@ -445,27 +426,11 @@ function zoomOut() {
onDraw={handleAnnotationDraw}
onDelete={handleAnnotationDelete}
commentCounts={Object.fromEntries(commentCounts)}
onAnnotationClick={(id) => (activeAnnotationId = id)}
onAnnotationClick={handleAnnotationClick}
/>
</div>
</div>
{/if}
</div>
{#key activeAnnotationId}
{#if activeAnnotationId}
<AnnotationCommentPanel
documentId={documentId}
annotationId={activeAnnotationId}
canComment={canComment ?? false}
currentUserId={currentUserId ?? null}
canAdmin={canAdmin ?? false}
onClose={() => (activeAnnotationId = null)}
onCountChange={(count) => {
if (activeAnnotationId) commentCounts.set(activeAnnotationId, count);
}}
/>
{/if}
{/key}
</div>
{/if}