refactor(briefwechsel): ConversationTimeline renders ThumbnailRow per letter
Drops the inline row markup, arrow icons, status-dot helper, and the otherPartyName helper that only fed it. Each visible row is now a ThumbnailRow, which owns its own aria-label, border color, meta and tag rendering. The year-divider and "new document" footer are untouched — they were always intended to stay as timeline chrome. Also widens the documents prop shape to include the summary, tags and thumbnail metadata that ThumbnailRow consumes; the backend already returns these fields via the Document schema so no server change was required. Refs #305 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { formatDate } from '$lib/utils/date';
|
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import DistributionBar from '$lib/components/DistributionBar.svelte';
|
import DistributionBar from '$lib/components/DistributionBar.svelte';
|
||||||
|
import ThumbnailRow from '$lib/components/ThumbnailRow.svelte';
|
||||||
|
|
||||||
|
type Person = { id: string; firstName?: string | null; lastName: string; displayName: string };
|
||||||
|
type Tag = { id: string; name: string };
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
documents: {
|
documents: {
|
||||||
@@ -10,14 +13,15 @@ interface Props {
|
|||||||
originalFilename: string;
|
originalFilename: string;
|
||||||
documentDate?: string;
|
documentDate?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
status: string;
|
summary?: string;
|
||||||
sender?: {
|
contentType?: string;
|
||||||
id: string;
|
thumbnailKey?: string;
|
||||||
firstName?: string | null;
|
thumbnailGeneratedAt?: string;
|
||||||
lastName: string;
|
thumbnailAspect?: 'PORTRAIT' | 'LANDSCAPE';
|
||||||
displayName: string;
|
pageCount?: number;
|
||||||
} | null;
|
sender?: Person | null;
|
||||||
receivers?: { id: string; firstName?: string | null; lastName: string; displayName: string }[];
|
receivers?: Person[];
|
||||||
|
tags?: Tag[];
|
||||||
}[];
|
}[];
|
||||||
senderId: string;
|
senderId: string;
|
||||||
receiverId?: string;
|
receiverId?: string;
|
||||||
@@ -54,25 +58,7 @@ const outCount = $derived(documents.filter((d) => d.sender?.id === senderId).len
|
|||||||
const inCount = $derived(documents.length - outCount);
|
const inCount = $derived(documents.length - outCount);
|
||||||
|
|
||||||
const isBilateral = $derived(!!senderId && !!receiverId);
|
const isBilateral = $derived(!!senderId && !!receiverId);
|
||||||
|
const showOtherParty = $derived(!receiverId);
|
||||||
function statusDotClass(status: string): string {
|
|
||||||
const map: Record<string, string> = {
|
|
||||||
PLACEHOLDER: 'bg-brand-sand',
|
|
||||||
UPLOADED: 'bg-brand-mint',
|
|
||||||
TRANSCRIBED: 'bg-brand-mint',
|
|
||||||
REVIEWED: 'bg-brand-navy/70',
|
|
||||||
ARCHIVED: 'bg-brand-navy'
|
|
||||||
};
|
|
||||||
return map[status] ?? 'bg-brand-sand';
|
|
||||||
}
|
|
||||||
|
|
||||||
function otherPartyName(doc: (typeof documents)[number]): string {
|
|
||||||
if (doc.sender?.id === senderId) {
|
|
||||||
const r = doc.receivers?.[0];
|
|
||||||
return r ? r.displayName : m.conv_no_party();
|
|
||||||
}
|
|
||||||
return doc.sender ? doc.sender.displayName : m.conv_no_party();
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDocUrl = $derived(
|
const newDocUrl = $derived(
|
||||||
`/documents/new?senderId=${encodeURIComponent(senderId)}${receiverId ? `&receiverId=${encodeURIComponent(receiverId)}` : ''}`
|
`/documents/new?senderId=${encodeURIComponent(senderId)}${receiverId ? `&receiverId=${encodeURIComponent(receiverId)}` : ''}`
|
||||||
@@ -100,50 +86,7 @@ const newDocUrl = $derived(
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<a
|
<ThumbnailRow doc={doc} isOut={isOut} showOtherParty={showOtherParty} />
|
||||||
href="/documents/{doc.id}"
|
|
||||||
aria-label="{doc.title || doc.originalFilename}, {doc.documentDate
|
|
||||||
? formatDate(doc.documentDate)
|
|
||||||
: ''}"
|
|
||||||
class="group flex min-h-[44px] cursor-pointer items-center gap-[9px] border-b border-l-[3px] border-line-2 px-[14px] py-[10px] transition-colors last:border-b-0 hover:bg-muted"
|
|
||||||
class:border-l-primary={isOut}
|
|
||||||
class:border-l-accent={!isOut}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={isOut
|
|
||||||
? '/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Right-MD.svg'
|
|
||||||
: '/degruyter-icons/Simple/Medium-24px/SVG/Action/Long-Arrow/Long-Arrow-Left-MD.svg'}
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
class="h-4 w-4 shrink-0 opacity-60"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="mb-[2px] truncate text-sm font-bold text-ink">
|
|
||||||
{doc.title || doc.originalFilename}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-[5px] text-sm text-ink-3">
|
|
||||||
<span>{doc.documentDate ? formatDate(doc.documentDate) : '—'}</span>
|
|
||||||
{#if doc.location}
|
|
||||||
<span class="text-line">·</span>
|
|
||||||
<span>{doc.location}</span>
|
|
||||||
{/if}
|
|
||||||
{#if !receiverId}
|
|
||||||
<span class="text-line">·</span>
|
|
||||||
<span>{otherPartyName(doc)}</span>
|
|
||||||
{/if}
|
|
||||||
<span
|
|
||||||
class="ml-[3px] h-[6px] w-[6px] shrink-0 rounded-full {statusDotClass(doc.status)}"
|
|
||||||
title={doc.status}
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="shrink-0 text-sm text-ink-3 opacity-0 transition-opacity group-hover:opacity-100"
|
|
||||||
aria-hidden="true">›</span
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if canWrite}
|
{#if canWrite}
|
||||||
|
|||||||
Reference in New Issue
Block a user