Third Phase 5 split. The desktop action buttons — transcribe, transcribe-stop, edit link, download link — become their own component with a focused props interface (documentId, canWrite, isPdf, transcribeMode bindable, filePath, originalFilename, fileUrl). TDD: 8 tests covering empty render, transcribe button gating (canWrite × isPdf × transcribeMode), stop-transcribe rendering, edit link with documentId href, download link with filePath gating, all hidden when in transcribe mode. After the test was red the component was created. DocumentTopBar dropped from 303 lines to 166. The orchestrator now just composes BackButton, DocumentTopBarTitle, PersonChipRow, OverflowPillButton, the details toggle, DocumentTopBarActions, DocumentMobileMenu, and DocumentMetadataDrawer — each visual region named in one or two words. Refs #496. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
167 lines
4.9 KiB
Svelte
167 lines
4.9 KiB
Svelte
<script lang="ts">
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
import { slide } from 'svelte/transition';
|
|
import PersonChipRow from '$lib/person/PersonChipRow.svelte';
|
|
import OverflowPillButton from '$lib/shared/primitives/OverflowPillButton.svelte';
|
|
import DocumentMetadataDrawer from './DocumentMetadataDrawer.svelte';
|
|
import DocumentTopBarTitle from './DocumentTopBarTitle.svelte';
|
|
import DocumentTopBarActions from './DocumentTopBarActions.svelte';
|
|
import DocumentMobileMenu from './DocumentMobileMenu.svelte';
|
|
import BackButton from '$lib/shared/primitives/BackButton.svelte';
|
|
|
|
type Person = { id: string; firstName?: string | null; lastName: string; displayName: string };
|
|
type Tag = { id: string; name: 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;
|
|
location?: string | null;
|
|
status?: string | null;
|
|
tags?: Tag[] | null;
|
|
};
|
|
|
|
type GeschichteSummary = {
|
|
id: string;
|
|
title: string;
|
|
publishedAt?: string;
|
|
author?: { firstName?: string; lastName?: string; email: string };
|
|
};
|
|
|
|
type Props = {
|
|
doc: Doc;
|
|
canWrite: boolean;
|
|
fileUrl: string;
|
|
transcribeMode: boolean;
|
|
inferredRelationship?: { labelFromA: string; labelFromB: string } | null;
|
|
geschichten?: GeschichteSummary[];
|
|
canBlogWrite?: boolean;
|
|
};
|
|
|
|
let {
|
|
doc,
|
|
canWrite,
|
|
fileUrl,
|
|
transcribeMode = $bindable(),
|
|
inferredRelationship = null,
|
|
geschichten = [],
|
|
canBlogWrite = false
|
|
}: 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));
|
|
const overflowPersons = $derived(receivers.slice(2));
|
|
</script>
|
|
|
|
<div data-topbar class="relative z-10 border-b border-line bg-surface shadow-sm">
|
|
<!-- Main row -->
|
|
<div class="flex h-[75px] shrink-0 items-center pr-4 xs:h-[88px]">
|
|
<!-- Accent bar -->
|
|
<div class="h-full w-[3px] shrink-0 bg-primary"></div>
|
|
|
|
<!-- Back button -->
|
|
<BackButton
|
|
class="-ml-0.5 h-11 w-11 shrink-0 justify-center rounded-full hover:bg-muted"
|
|
showLabel={false}
|
|
/>
|
|
|
|
<!-- Divider -->
|
|
<div class="mx-2 h-6 w-px shrink-0 bg-line"></div>
|
|
|
|
<!-- Title + meta -->
|
|
<DocumentTopBarTitle
|
|
title={doc.title}
|
|
originalFilename={doc.originalFilename}
|
|
documentDate={doc.documentDate}
|
|
/>
|
|
|
|
<!-- Chip row — desktop only, hidden on small screens to make room for buttons -->
|
|
<div class="mx-3 hidden min-w-0 shrink-0 md:block">
|
|
<PersonChipRow sender={doc.sender} receivers={receivers} abbreviated={true} extraCount={0} />
|
|
</div>
|
|
|
|
<!-- Overflow pill button (desktop) + status dot -->
|
|
{#if extraCount > 0}
|
|
<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.5 rounded border px-3 py-1 font-sans text-sm 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>
|
|
|
|
<!-- Action buttons -->
|
|
<div class="flex shrink-0 items-center gap-1.5 font-sans">
|
|
<DocumentTopBarActions
|
|
documentId={doc.id}
|
|
canWrite={canWrite}
|
|
isPdf={!!isPdf}
|
|
bind:transcribeMode={transcribeMode}
|
|
filePath={doc.filePath}
|
|
originalFilename={doc.originalFilename}
|
|
fileUrl={fileUrl}
|
|
/>
|
|
|
|
{#if (canWrite && isPdf) || doc.filePath}
|
|
<div class="md:hidden">
|
|
<DocumentMobileMenu
|
|
canWrite={canWrite}
|
|
isPdf={!!isPdf}
|
|
bind:transcribeMode={transcribeMode}
|
|
filePath={doc.filePath}
|
|
originalFilename={doc.originalFilename}
|
|
fileUrl={fileUrl}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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] : []}
|
|
inferredRelationship={inferredRelationship}
|
|
geschichten={geschichten}
|
|
documentId={doc.id}
|
|
canBlogWrite={canBlogWrite}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|