`showDate = !compact || !entry.title` dropped the date chip for ANY titled compact letter. But titles are free-form OCR/import text — a letter titled "Brief an Mutter" lost its month/day entirely, and inside an event card the band frames only the year. The chip now drops only when the formatted date actually appears in the title (e.g. "H-0023 – 6. Juli 1916"), so the row-height win holds where valid and no information is lost otherwise. The spec that asserted the date vanishes for any title is rewritten to the correct contract, plus an inverse test. Fixes review finding #4. Refs #850 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
3.9 KiB
Svelte
87 lines
3.9 KiB
Svelte
<script lang="ts">
|
||
import * as m from '$lib/paraglide/messages.js';
|
||
import { timelineDateLabel } from './dateLabel';
|
||
import GlyphLabel from './GlyphLabel.svelte';
|
||
import TagChip from './TagChip.svelte';
|
||
import type { components } from '$lib/generated/api';
|
||
|
||
type TimelineEntryDTO = components['schemas']['TimelineEntryDTO'];
|
||
|
||
/**
|
||
* A single archive letter on the timeline: sender → receiver, title, and a
|
||
* precision-aware date chip, linking to the document. Names/titles are
|
||
* OCR/import-derived — rendered via default `{...}` escaping with
|
||
* `whitespace-pre-line` for line breaks (REQ-010); never the raw-HTML directive.
|
||
*
|
||
* Inside an event cluster the card sits in the contained event card and renders as
|
||
* the `.lcard.ev` `compact` variant (#850, REQ-002): tighter row, and the redundant
|
||
* date chip is dropped when the title already embeds the date. The per-letter tag
|
||
* chip can be suppressed via `suppressTagChip` for callers that already convey it.
|
||
*/
|
||
let {
|
||
entry,
|
||
variant = 'plain',
|
||
suppressTagChip = false,
|
||
compact = false
|
||
}: {
|
||
entry: TimelineEntryDTO;
|
||
variant?: 'plain' | 'event';
|
||
suppressTagChip?: boolean;
|
||
compact?: boolean;
|
||
} = $props();
|
||
|
||
const isEventVariant = $derived(variant === 'event');
|
||
const dateLabel = $derived(timelineDateLabel(entry.eventDate, entry.precision, entry.eventDateEnd));
|
||
// Inside an event card the band frames the time, so a compact in-card letter drops the
|
||
// redundant date chip — but ONLY when the (free-form OCR) title actually embeds the formatted
|
||
// date, e.g. "H-0023 – 6. Juli 1916". A title without the date keeps its chip, so a letter like
|
||
// "Brief an Mutter" never loses its month/day (the band frames only the year) — #850, finding #4.
|
||
const titleEmbedsDate = $derived(!!dateLabel && !!entry.title && entry.title.includes(dateLabel));
|
||
const showDate = $derived(!compact || !titleEmbedsDate);
|
||
const sender = $derived(entry.senderName === '' ? m.timeline_unknown_person() : entry.senderName);
|
||
const receiver = $derived(
|
||
entry.receiverName === '' ? m.timeline_unknown_person() : entry.receiverName
|
||
);
|
||
</script>
|
||
|
||
<!-- Box layout inline (not just utility classes) so the 44px touch target holds
|
||
even before the stylesheet loads — an <a> is inline by default and would
|
||
ignore min-height otherwise. WCAG 2.5.5 (REQ-020). -->
|
||
<a
|
||
href="/documents/{entry.documentId}"
|
||
style="display: flex; flex-direction: column; justify-content: center; min-height: 44px"
|
||
class="lcard rounded-sm border border-l-[3px] border-line border-l-brand-mint bg-surface px-3 shadow-sm transition-colors hover:border-brand-mint focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-navy"
|
||
class:py-2={!compact}
|
||
class:py-1={compact}
|
||
class:ev={isEventVariant}
|
||
class:compact={compact}
|
||
>
|
||
{#if entry.title}
|
||
<!-- ✉ + sr-only label are static chrome rendered as sibling nodes, NEVER
|
||
interpolated into the escaped user title; the title keeps its own
|
||
pre-line span for multi-line OCR text (REQ-008/016/021). -->
|
||
<span
|
||
class="font-serif font-bold break-words text-ink"
|
||
class:text-sm={!compact}
|
||
class:text-xs={compact}
|
||
>
|
||
<GlyphLabel glyph="✉" label={m.timeline_letter_glyph_label()} />
|
||
<span class="whitespace-pre-line">{entry.title}</span>
|
||
</span>
|
||
{/if}
|
||
<span class="font-sans text-xs break-words text-ink-3" class:mt-0.5={!compact}>
|
||
<span class="font-serif whitespace-pre-line">{sender}</span>
|
||
<span aria-hidden="true">→</span>
|
||
<span class="font-serif whitespace-pre-line">{receiver}</span>
|
||
{#if dateLabel && showDate}
|
||
<span data-testid="letter-date"> · {dateLabel}</span>
|
||
{/if}
|
||
</span>
|
||
{#if entry.rootTagName && !suppressTagChip}
|
||
<!-- The primary root-tag chip sits on its own line beneath the meta line
|
||
(#835 §3); absent when the letter has no tag (REQ-006), and suppressed when
|
||
the caller already conveys the topic (suppressTagChip). -->
|
||
<TagChip name={entry.rootTagName} color={entry.rootTagColor ?? null} />
|
||
{/if}
|
||
</a>
|