feat(timeline): show the root-tag chip on the letter card

LetterCard now renders a TagChip beneath the sender→receiver/date line
whenever the entry carries a rootTagName, mapping rootTagColor to the chip
(neutral when null). Because the chip lives on LetterCard it shows up wherever
a LetterCard does — the global timeline and the expanded YearLetterStrip — with
no per-surface special-casing; a tagless letter shows no chip. A long name
truncates inline so the card never overflows at 320px.

Refs #835
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-14 15:13:21 +02:00
parent c19d4be3fe
commit 8376a520c5
2 changed files with 46 additions and 0 deletions

View File

@@ -2,6 +2,7 @@
import * as m from '$lib/paraglide/messages.js'; import * as m from '$lib/paraglide/messages.js';
import { timelineDateLabel } from './dateLabel'; import { timelineDateLabel } from './dateLabel';
import GlyphLabel from './GlyphLabel.svelte'; import GlyphLabel from './GlyphLabel.svelte';
import TagChip from './TagChip.svelte';
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
type TimelineEntryDTO = components['schemas']['TimelineEntryDTO']; type TimelineEntryDTO = components['schemas']['TimelineEntryDTO'];
@@ -46,4 +47,9 @@ const receiver = $derived(
<span data-testid="letter-date"> · {dateLabel}</span> <span data-testid="letter-date"> · {dateLabel}</span>
{/if} {/if}
</span> </span>
{#if entry.rootTagName}
<!-- The primary root-tag chip sits on its own line beneath the meta line
(#835 §3); absent when the letter has no tag (REQ-005). -->
<TagChip name={entry.rootTagName} color={entry.rootTagColor ?? null} />
{/if}
</a> </a>

View File

@@ -1,7 +1,9 @@
import { describe, it, expect, afterEach } from 'vitest'; import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte'; import { cleanup, render } from 'vitest-browser-svelte';
import { tick } from 'svelte';
import * as m from '$lib/paraglide/messages.js'; import * as m from '$lib/paraglide/messages.js';
import LetterCard from './LetterCard.svelte'; import LetterCard from './LetterCard.svelte';
import YearLetterStrip from './YearLetterStrip.svelte';
import { timelineDateLabel } from './dateLabel'; import { timelineDateLabel } from './dateLabel';
import { makeEntry } from './test-factories'; import { makeEntry } from './test-factories';
@@ -86,4 +88,42 @@ describe('LetterCard', () => {
expect(document.body.textContent).toContain(evil); expect(document.body.textContent).toContain(evil);
expect(document.querySelector('a script')).toBeNull(); expect(document.querySelector('a script')).toBeNull();
}); });
it('renders one root-tag chip beneath the meta line when rootTagName is present (REQ-008)', () => {
render(LetterCard, { entry: makeEntry({ rootTagName: 'Familie', rootTagColor: 'sage' }) });
const chips = document.querySelectorAll('[data-testid="tag-chip"]');
expect(chips).toHaveLength(1);
expect(chips[0].textContent).toContain('Familie');
});
it('renders no chip when the letter has no root tag (REQ-005/006)', () => {
render(LetterCard, { entry: makeEntry({ rootTagName: undefined, rootTagColor: undefined }) });
expect(document.querySelector('[data-testid="tag-chip"]')).toBeNull();
});
it('keeps a long tag name from overflowing the card at 320px, full name in the title (REQ-008a)', () => {
document.body.style.width = '320px';
render(LetterCard, {
entry: makeEntry({
rootTagName: 'Briefe von der Front und aus der Heimat',
rootTagColor: 'sienna'
})
});
const link = document.querySelector('a') as HTMLAnchorElement;
expect(link.scrollWidth).toBeLessThanOrEqual(link.clientWidth);
const chip = document.querySelector('[data-testid="tag-chip"]') as HTMLElement;
expect(chip.getAttribute('title')).toBe('Briefe von der Front und aus der Heimat');
document.body.style.width = '';
});
it('renders the chip inside an expanded YearLetterStrip too (REQ-012)', async () => {
render(YearLetterStrip, {
letters: [makeEntry({ rootTagName: 'Familie', rootTagColor: 'sage', documentId: 'doc-1' })],
year: 1909
});
(document.querySelector('[data-testid="strip-expand"]') as HTMLButtonElement).click();
await tick();
const chip = document.querySelector('[data-testid="tag-chip"]');
expect(chip?.textContent).toContain('Familie');
});
}); });