From e25001f7c9b9c59fb795c00ccddca15dd4095bd1 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 13 Jun 2026 19:30:55 +0200 Subject: [PATCH] feat(timeline): add LetterCard component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single archive letter: sender → receiver (Unbekannt fallback for empty names, REQ-014), title, precision date chip via timelineDateLabel (omitted when null, REQ-013), linking to exactly /documents/{documentId} with no target (REQ-023). 44px touch target enforced inline + focus-visible ring (REQ-020). OCR/import text via {...} escaping + whitespace-pre-line, no {@html} (REQ-021). Refs #779 Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/timeline/LetterCard.svelte | 42 ++++++++++++++ .../lib/timeline/LetterCard.svelte.spec.ts | 58 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 frontend/src/lib/timeline/LetterCard.svelte create mode 100644 frontend/src/lib/timeline/LetterCard.svelte.spec.ts diff --git a/frontend/src/lib/timeline/LetterCard.svelte b/frontend/src/lib/timeline/LetterCard.svelte new file mode 100644 index 00000000..f5f90302 --- /dev/null +++ b/frontend/src/lib/timeline/LetterCard.svelte @@ -0,0 +1,42 @@ + + + + + {#if entry.title} + {entry.title} + {/if} + + {sender} + + {receiver} + {#if dateLabel} + · {dateLabel} + {/if} + + diff --git a/frontend/src/lib/timeline/LetterCard.svelte.spec.ts b/frontend/src/lib/timeline/LetterCard.svelte.spec.ts new file mode 100644 index 00000000..28df3886 --- /dev/null +++ b/frontend/src/lib/timeline/LetterCard.svelte.spec.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import LetterCard from './LetterCard.svelte'; +import { timelineDateLabel } from './dateLabel'; +import { makeEntry } from './test-factories'; + +afterEach(() => cleanup()); + +const DOC_ID = '22222222-2222-2222-2222-222222222222'; + +describe('LetterCard', () => { + it('renders sender, receiver, and title', () => { + render(LetterCard, { + entry: makeEntry({ senderName: 'Karl', receiverName: 'Elfriede', title: 'Feldpost' }) + }); + expect(document.body.textContent).toContain('Karl'); + expect(document.body.textContent).toContain('Elfriede'); + expect(document.body.textContent).toContain('Feldpost'); + }); + + it('renders the precision date exactly as timelineDateLabel returns (REQ-013)', () => { + const entry = makeEntry({ eventDate: '1915-06-15', precision: 'MONTH' }); + const expected = timelineDateLabel(entry.eventDate, entry.precision, entry.eventDateEnd); + expect(expected).toBeTruthy(); + render(LetterCard, { entry }); + expect(document.body.textContent).toContain(expected as string); + }); + + it('renders no date chip when timelineDateLabel returns null (REQ-013)', () => { + const entry = makeEntry({ precision: 'UNKNOWN', eventDate: undefined }); + render(LetterCard, { entry }); + const chip = document.querySelector('[data-testid="letter-date"]'); + expect(chip).toBeNull(); + }); + + it('shows "Unbekannt" for an empty sender, never a bare arrow (REQ-014)', () => { + render(LetterCard, { entry: makeEntry({ senderName: '', receiverName: 'Elfriede' }) }); + expect(document.body.textContent).toContain('Unbekannt'); + }); + + it('shows "Unbekannt" for an empty receiver (REQ-014)', () => { + render(LetterCard, { entry: makeEntry({ senderName: 'Karl', receiverName: '' }) }); + expect(document.body.textContent).toContain('Unbekannt'); + }); + + it('links to exactly /documents/{documentId} with no target (REQ-023)', () => { + render(LetterCard, { entry: makeEntry({ documentId: DOC_ID }) }); + const link = document.querySelector('a') as HTMLAnchorElement; + expect(link.getAttribute('href')).toBe(`/documents/${DOC_ID}`); + expect(link.hasAttribute('target')).toBe(false); + }); + + it('has a touch target of at least 44px (REQ-020)', () => { + render(LetterCard, { entry: makeEntry() }); + const link = document.querySelector('a') as HTMLAnchorElement; + expect(link.getBoundingClientRect().height).toBeGreaterThanOrEqual(44); + }); +});