Files
familienarchiv/frontend/src/lib/components/DocumentTopBar.svelte
Marcel 8c2bdbd777
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
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>
2026-03-24 22:35:28 +01:00

143 lines
4.0 KiB
Svelte

<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
type Person = { id: string; firstName: string; lastName: string };
type Doc = {
id: string;
title?: string | null;
originalFilename?: string | null;
documentDate?: string | null;
sender?: Person | null;
receivers?: Person[] | null;
filePath?: string | null;
contentType?: string | null;
};
type Props = {
doc: Doc;
canWrite: boolean;
canAnnotate: boolean;
fileUrl: string;
annotateMode: boolean;
};
let { doc, canWrite, canAnnotate, fileUrl, annotateMode = $bindable() }: Props = $props();
const isPdf = $derived(!!doc.filePath && doc.contentType?.startsWith('application/pdf'));
const receiverDisplay = $derived.by(() => {
const receivers = doc.receivers ?? [];
if (receivers.length === 0) return null;
const shown = receivers.slice(0, 2);
const extra = receivers.length - shown.length;
const names = shown.map((r) => `${r.firstName} ${r.lastName}`).join(', ');
return extra > 0 ? `${names} +${extra}` : names;
});
const compactMeta = $derived.by(() => {
const parts: string[] = [];
if (doc.documentDate) {
parts.push(
new Intl.DateTimeFormat('de-DE', {
day: 'numeric',
month: 'numeric',
year: 'numeric'
}).format(new Date(doc.documentDate + 'T12:00:00'))
);
}
if (doc.sender) {
const senderName = `${doc.sender.firstName} ${doc.sender.lastName}`;
const receiver = receiverDisplay;
parts.push(receiver ? `${senderName}${receiver}` : senderName);
} else if (receiverDisplay) {
parts.push(`→ ${receiverDisplay}`);
}
return parts.join(' · ');
});
</script>
<div
class="z-20 flex shrink-0 items-center justify-between border-b border-brand-sand bg-white px-6 py-3 shadow-sm"
>
<!-- Left: back + title -->
<div class="flex min-w-0 items-center gap-4 overflow-hidden">
<a
href="/"
class="group flex shrink-0 items-center gap-2 font-sans text-sm font-medium text-gray-500 transition-colors hover:text-brand-navy"
>
<div
class="flex h-8 w-8 items-center justify-center rounded-full bg-brand-sand transition-colors group-hover:bg-brand-mint"
>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg"
alt=""
aria-hidden="true"
class="h-4 w-4"
/>
</div>
<span class="hidden sm:inline">{m.btn_back()}</span>
</a>
<div class="min-w-0 border-l border-gray-200 pl-4">
<h1
class="truncate font-serif text-base leading-tight text-brand-navy"
title={doc.title ?? doc.originalFilename ?? ''}
>
{doc.title || doc.originalFilename}
</h1>
{#if compactMeta}
<p class="truncate font-sans text-xs text-gray-500" title={compactMeta}>
{compactMeta}
</p>
{/if}
</div>
</div>
<!-- Right: actions -->
<div class="ml-4 flex shrink-0 items-center gap-2 font-sans">
{#if canAnnotate && isPdf}
<button
onclick={() => (annotateMode = !annotateMode)}
aria-label={annotateMode ? m.doc_panel_annotate_stop() : m.doc_panel_annotate()}
class="rounded px-3 py-1.5 font-sans text-xs font-medium transition {annotateMode
? 'bg-brand-navy text-white'
: 'border border-brand-navy text-brand-navy hover:bg-brand-navy hover:text-white'}"
>
{annotateMode ? m.doc_panel_annotate_stop() : m.doc_panel_annotate()}
</button>
{/if}
{#if canWrite}
<a
href="/documents/{doc.id}/edit"
class="flex items-center gap-2 rounded border border-brand-navy bg-transparent px-3 py-1.5 text-xs font-medium text-brand-navy transition hover:bg-brand-navy hover:text-white"
>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg"
alt=""
aria-hidden="true"
class="h-4 w-4"
/>
{m.btn_edit()}
</a>
{/if}
{#if doc.filePath}
<a
href={fileUrl}
download={doc.originalFilename}
class="rounded border border-transparent bg-brand-sand/50 p-1.5 text-brand-navy transition hover:bg-brand-mint"
title={m.doc_download_title()}
>
<img
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Download-MD.svg"
alt=""
aria-hidden="true"
class="h-5 w-5"
/>
</a>
{/if}
</div>
</div>