feat(timeline): add YearBand (section + sticky h2, cards vs strip)

One <section> per year with a sticky <h2> at top:4rem (REQ-006). Events render in
DTO order as pills/bands; letters render as individual cards while <= 12 (REQ-011)
or collapse to one density strip above that (REQ-012); DTO order is never re-sorted
(REQ-003). Letters carry an alternating data-side for the centered desktop axis
(REQ-004); single left column on phone (REQ-005). Derived-safe {#each} key.

Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-13 19:43:47 +02:00
parent 5bff428954
commit f9ddcf0374
2 changed files with 192 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import YearBand from './YearBand.svelte';
import { makeEntry, makeYear } from './test-factories';
afterEach(() => cleanup());
function manyLetters(year: number, count: number) {
return Array.from({ length: count }, (_, i) =>
makeEntry({ eventDate: `${year}-01-10`, documentId: `doc-${i}` })
);
}
describe('YearBand', () => {
it('renders a section with a sticky h2 at top:4rem showing the year (REQ-006)', () => {
render(YearBand, { year: makeYear(1914, [makeEntry()]) });
const section = document.querySelector('section');
expect(section).not.toBeNull();
const h2 = section?.querySelector('h2');
expect(h2?.textContent).toContain('1914');
const cs = getComputedStyle(h2 as HTMLElement);
expect(cs.position).toBe('sticky');
expect(cs.top).toBe('64px');
});
it('renders each letter as a card when the band holds <= 12 letters (REQ-011)', () => {
render(YearBand, { year: makeYear(1909, manyLetters(1909, 3)) });
expect(document.querySelectorAll('a')).toHaveLength(3);
expect(document.querySelector('[data-testid="strip-expand"]')).toBeNull();
});
it('renders a single strip when the band holds > 12 letters (REQ-012)', () => {
render(YearBand, { year: makeYear(1915, manyLetters(1915, 30)) });
expect(document.querySelector('[data-testid="strip-expand"]')).not.toBeNull();
// collapsed: no individual letter links yet
expect(document.querySelectorAll('a')).toHaveLength(0);
});
it('renders entries in DTO order — DAY-precision letter above a YEAR-precision letter (REQ-003)', () => {
const dayLetter = makeEntry({
precision: 'DAY',
eventDate: '1923-04-12',
title: 'Tagesgenau',
documentId: 'day'
});
const yearLetter = makeEntry({
precision: 'YEAR',
eventDate: '1923-01-01',
title: 'Nur Jahr',
documentId: 'year'
});
render(YearBand, { year: makeYear(1923, [dayLetter, yearLetter]) });
const links = Array.from(document.querySelectorAll('a'));
expect(links[0].getAttribute('href')).toBe('/documents/day');
expect(links[1].getAttribute('href')).toBe('/documents/year');
});
it('renders an EVENT as a pill and a HISTORICAL event as a band', () => {
const pill = makeEntry({
kind: 'EVENT',
derived: true,
derivedType: 'MARRIAGE',
title: 'Heirat',
senderName: '',
receiverName: '',
documentId: undefined
});
const band = makeEntry({
kind: 'EVENT',
derived: false,
type: 'HISTORICAL',
precision: 'RANGE',
eventDate: '1914-01-01',
eventDateEnd: '1918-12-31',
title: 'Erster Weltkrieg',
senderName: '',
receiverName: '',
documentId: undefined
});
render(YearBand, { year: makeYear(1914, [pill, band]) });
expect(document.body.textContent).toContain('Heirat');
expect(document.querySelector('[data-testid="world-range"]')).not.toBeNull();
});
});