feat(topbar): add expandable metadata drawer with Details toggle (#175)

- DocumentMetadataDrawer: 3-column grid (≥1024px), single-column mobile
  Shows document date, location, status, person cards, tag chips
  Person names link to /persons/{id}, tags link to filtered search
  Empty states for missing persons/tags, receiver cap with expand button
- DocumentTopBar: "Details" toggle button with animated SVG chevron
  44×44px tap target, aria-expanded, Svelte slide transition
  Semantic color tokens for dark mode compatibility
- Remove DocumentBottomPanel from document detail page
  Bottom panel replaced by topbar drawer for metadata access
  Simplify +page.server.ts (remove comments loading)
  Update page.server.spec.ts for new load signature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-05 11:22:38 +02:00
parent 234f83c40b
commit 5211e0b9f7
6 changed files with 197 additions and 152 deletions

View File

@@ -1,12 +1,15 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
import { slide } from 'svelte/transition';
import { formatDate } from '$lib/utils/personFormat';
import { clickOutside } from '$lib/actions/clickOutside';
import PersonChipRow from './PersonChipRow.svelte';
import AnnotateHintStrip from './AnnotateHintStrip.svelte';
import OverflowPillButton from './OverflowPillButton.svelte';
import DocumentMetadataDrawer from './DocumentMetadataDrawer.svelte';
type Person = { id: string; firstName: string; lastName: string };
type Tag = { id: string; name: string };
type Doc = {
id: string;
@@ -17,6 +20,9 @@ type Doc = {
receivers?: Person[] | null;
filePath?: string | null;
contentType?: string | null;
location?: string | null;
status?: string | null;
tags?: Tag[] | null;
};
type Props = {
@@ -29,6 +35,8 @@ type Props = {
let { doc, canWrite, canAnnotate, fileUrl, annotateMode = $bindable() }: Props = $props();
let detailsOpen = $state(false);
const isPdf = $derived(!!doc.filePath && doc.contentType?.startsWith('application/pdf'));
const receivers = $derived(doc.receivers ?? []);
const extraCount = $derived(Math.max(0, receivers.length - 2));
@@ -155,6 +163,27 @@ let mobileMenuOpen = $state(false);
<OverflowPillButton extraCount={extraCount} persons={overflowPersons} />
{/if}
<!-- Details toggle -->
<button
type="button"
onclick={() => (detailsOpen = !detailsOpen)}
aria-expanded={detailsOpen}
aria-label={m.doc_details_toggle()}
class="ml-2 inline-flex min-h-[44px] shrink-0 items-center gap-1 rounded border px-2 py-1 font-sans text-xs font-semibold transition-colors {detailsOpen ? 'border-primary bg-primary text-primary-fg' : 'border-line text-ink-2 hover:bg-muted hover:text-ink'}"
>
{m.doc_details_toggle()}
<svg
class="h-3.5 w-3.5 transition-transform duration-200 {detailsOpen ? 'rotate-180' : ''}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- Divider between metadata and actions -->
<div class="mx-3 hidden h-6 w-px shrink-0 bg-line md:block"></div>
@@ -233,4 +262,18 @@ let mobileMenuOpen = $state(false);
<!-- Hint strip — only when annotateMode, only at ≥768px -->
<AnnotateHintStrip annotateMode={annotateMode} />
<!-- Metadata drawer -->
{#if detailsOpen}
<div transition:slide={{ duration: 200 }}>
<DocumentMetadataDrawer
documentDate={doc.documentDate ?? null}
location={doc.location ?? null}
status={doc.status ?? 'PLACEHOLDER'}
sender={doc.sender ?? null}
receivers={doc.receivers ? [...doc.receivers] : []}
tags={doc.tags ? [...doc.tags] : []}
/>
</div>
{/if}
</div>