Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
Transcribe button now uses border-primary/bg-primary/text-primary-fg matching the other action buttons (Bearbeiten). Turquoise is reserved for annotation overlays and block focus borders on the PDF. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
289 lines
9.4 KiB
Svelte
289 lines
9.4 KiB
Svelte
<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 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;
|
||
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 Props = {
|
||
doc: Doc;
|
||
canWrite: boolean;
|
||
fileUrl: string;
|
||
transcribeMode: boolean;
|
||
};
|
||
|
||
let { doc, canWrite, fileUrl, transcribeMode = $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));
|
||
const overflowPersons = $derived(receivers.slice(2));
|
||
|
||
const shortDate = $derived(doc.documentDate ? formatDate(doc.documentDate, 'short') : null);
|
||
const longDate = $derived(doc.documentDate ? formatDate(doc.documentDate, 'long') : null);
|
||
|
||
let mobileMenuOpen = $state(false);
|
||
</script>
|
||
|
||
{#snippet transcribeBtn(mobile: boolean)}
|
||
<button
|
||
onclick={() => {
|
||
transcribeMode = true;
|
||
if (mobile) mobileMenuOpen = false;
|
||
}}
|
||
aria-label={m.transcription_mode_label()}
|
||
aria-pressed={false}
|
||
class={mobile
|
||
? 'flex w-full items-center gap-2 rounded px-3 py-2 text-left text-[16px] text-ink transition hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary'
|
||
: 'hidden items-center gap-1.5 rounded border border-primary px-3 py-1.5 font-sans text-[16px] font-medium text-ink transition hover:bg-primary hover:text-primary-fg focus-visible:ring-2 focus-visible:ring-primary md:flex'}
|
||
>
|
||
<svg
|
||
class="h-5 w-5 shrink-0"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
stroke-width="1.5"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||
/>
|
||
</svg>
|
||
{m.transcription_mode_label()}
|
||
</button>
|
||
{/snippet}
|
||
|
||
{#snippet transcribeStopBtn(mobile: boolean)}
|
||
<button
|
||
onclick={() => {
|
||
transcribeMode = false;
|
||
if (mobile) mobileMenuOpen = false;
|
||
}}
|
||
aria-label={m.transcription_mode_stop()}
|
||
aria-pressed={true}
|
||
class={mobile
|
||
? 'flex w-full items-center gap-2 rounded bg-primary px-3 py-2 text-left text-[16px] text-primary-fg transition focus-visible:ring-2 focus-visible:ring-primary'
|
||
: 'flex items-center gap-1.5 rounded bg-primary px-3 py-1.5 font-sans text-[16px] font-medium text-primary-fg transition focus-visible:ring-2 focus-visible:ring-primary'}
|
||
>
|
||
<svg
|
||
class="h-5 w-5 shrink-0"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
stroke-width="1.5"
|
||
>
|
||
<path
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||
/>
|
||
</svg>
|
||
{m.transcription_mode_stop()}
|
||
</button>
|
||
{/snippet}
|
||
|
||
{#snippet downloadLink(mobile: boolean)}
|
||
<a
|
||
href={fileUrl}
|
||
download={doc.originalFilename}
|
||
onclick={() => {
|
||
if (mobile) mobileMenuOpen = false;
|
||
}}
|
||
class={mobile
|
||
? 'flex items-center gap-2 rounded px-3 py-2 text-[16px] text-ink transition hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary'
|
||
: 'hidden rounded border border-transparent bg-muted p-1.5 text-ink transition hover:bg-accent focus-visible:ring-2 focus-visible:ring-primary md:block'}
|
||
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 shrink-0"
|
||
/>
|
||
{#if mobile}{m.doc_download_title()}{/if}
|
||
</a>
|
||
{/snippet}
|
||
|
||
<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 xs:h-[88px]">
|
||
<!-- Accent bar -->
|
||
<div class="h-full w-[3px] shrink-0 bg-primary"></div>
|
||
|
||
<!-- Back link — 44×44px touch target -->
|
||
<a
|
||
href="/"
|
||
aria-label={m.topbar_back_label()}
|
||
class="group -ml-0.5 flex h-11 w-11 shrink-0 items-center justify-center rounded-full transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary"
|
||
>
|
||
<img
|
||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg"
|
||
alt=""
|
||
aria-hidden="true"
|
||
class="h-5 w-5"
|
||
/>
|
||
</a>
|
||
|
||
<!-- Divider -->
|
||
<div class="mx-2 h-6 w-px shrink-0 bg-line"></div>
|
||
|
||
<!-- Title + meta -->
|
||
<div class="min-w-0 flex-1 overflow-hidden">
|
||
<h1
|
||
class="truncate font-serif text-[18px] leading-tight text-ink lg:text-[20px]"
|
||
title={doc.title ?? doc.originalFilename ?? ''}
|
||
>
|
||
{doc.title || doc.originalFilename}
|
||
</h1>
|
||
{#if shortDate}
|
||
<p class="font-sans text-[16px] text-ink-2">
|
||
<span class="lg:hidden">{shortDate}</span>
|
||
<span class="hidden lg:inline">{longDate}</span>
|
||
</p>
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- 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 pr-3 font-sans">
|
||
{#if canWrite && isPdf && !transcribeMode}
|
||
{@render transcribeBtn(false)}
|
||
{/if}
|
||
|
||
{#if transcribeMode}
|
||
{@render transcribeStopBtn(false)}
|
||
{/if}
|
||
|
||
{#if canWrite && !transcribeMode}
|
||
<a
|
||
href="/documents/{doc.id}/edit"
|
||
aria-label={m.btn_edit()}
|
||
class="flex items-center gap-1.5 rounded border border-primary bg-transparent px-3 py-1.5 text-[16px] font-medium text-ink transition hover:bg-primary hover:text-primary-fg focus-visible:ring-2 focus-visible:ring-primary"
|
||
>
|
||
<img
|
||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg"
|
||
alt=""
|
||
aria-hidden="true"
|
||
class="h-5 w-5"
|
||
/>
|
||
<span class="hidden sm:inline">{m.btn_edit()}</span>
|
||
</a>
|
||
{/if}
|
||
|
||
{#if doc.filePath && !transcribeMode}
|
||
{@render downloadLink(false)}
|
||
{/if}
|
||
|
||
<!-- Kebab menu — mobile only, contains actions hidden below md -->
|
||
{#if (canWrite && isPdf) || doc.filePath}
|
||
<div
|
||
role="group"
|
||
class="relative md:hidden"
|
||
use:clickOutside
|
||
onclickoutside={() => (mobileMenuOpen = false)}
|
||
>
|
||
<button
|
||
type="button"
|
||
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
||
aria-label={m.topbar_more_actions()}
|
||
aria-haspopup="true"
|
||
aria-expanded={mobileMenuOpen}
|
||
class="flex h-9 w-9 items-center justify-center rounded border border-line bg-muted transition hover:bg-accent focus-visible:ring-2 focus-visible:ring-primary"
|
||
>
|
||
<img
|
||
src="/degruyter-icons/Simple/Medium-24px/SVG/Action/View-More-MD.svg"
|
||
alt=""
|
||
aria-hidden="true"
|
||
class="h-5 w-5"
|
||
/>
|
||
</button>
|
||
|
||
{#if mobileMenuOpen}
|
||
<div
|
||
role="menu"
|
||
class="absolute top-full right-0 z-50 mt-1 min-w-[200px] rounded-md border border-line bg-surface p-2 shadow-lg"
|
||
>
|
||
{#if canWrite && isPdf && !transcribeMode}
|
||
{@render transcribeBtn(true)}
|
||
{/if}
|
||
|
||
{#if doc.filePath}
|
||
{@render downloadLink(true)}
|
||
{/if}
|
||
</div>
|
||
{/if}
|
||
</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] : []}
|
||
/>
|
||
</div>
|
||
{/if}
|
||
</div>
|