Files
familienarchiv/frontend/src/lib/document/DocumentTopBar.svelte
Marcel f4f853be8b i18n(transcription): add reader read-label and panel title strings (#697)
transcription_read_label ("Transkription lesen") for the read-only entry
control and transcription_panel_title ("Transkription") for the plain
header readers see instead of the Lesen/Bearbeiten toggle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 13:28:37 +02:00

180 lines
5.4 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';
import type { DatePrecision } from '$lib/shared/utils/documentDate';
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;
metaDatePrecision?: DatePrecision | null;
metaDateEnd?: string | null;
metaDateRaw?: string | null;
sender?: Person | null;
receivers?: Person[] | null;
filePath?: string | null;
contentType?: string | null;
location?: string | null;
status?: string | null;
tags?: Tag[] | null;
hasTranscription?: boolean;
};
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}
metaDatePrecision={doc.metaDatePrecision}
metaDateEnd={doc.metaDateEnd}
metaDateRaw={doc.metaDateRaw}
/>
<!-- 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}
hasTranscription={!!doc.hasTranscription}
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}
hasTranscription={!!doc.hasTranscription}
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}
metaDatePrecision={doc.metaDatePrecision ?? null}
metaDateEnd={doc.metaDateEnd ?? null}
metaDateRaw={doc.metaDateRaw ?? 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>