feat(frontend): add floating bottom panel to document detail page
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user