feat(timeline): center the year badge and add spine markers (REQ-003/004/005)

The year badge now centers on the axis at ≥1024px and hugs the left spine
below that (sticky top:4rem preserved), with a navy node marker so it
visibly interrupts the spine. Each letter row gains a connector dot (white
fill, mint ring) on the spine: centered between card and axis on desktop,
on the left spine clear of the indented card on phone. Spine geometry is
commented to track TimelineView's spine so the markers can't silently desync.

Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-14 11:10:05 +02:00
parent e4da28d795
commit 18934413bb
2 changed files with 132 additions and 9 deletions

View File

@@ -1,9 +1,13 @@
import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import YearBand from './YearBand.svelte';
import { makeEntry, makeYear } from './test-factories';
afterEach(() => cleanup());
afterEach(async () => {
cleanup();
await page.viewport(1280, 720);
});
function manyLetters(year: number, count: number) {
return Array.from({ length: count }, (_, i) =>
@@ -81,4 +85,59 @@ describe('YearBand', () => {
expect(document.body.textContent).toContain('Heirat');
expect(document.querySelector('[data-testid="world-range"]')).not.toBeNull();
});
it('centers the year badge on the axis at desktop (REQ-003)', async () => {
await page.viewport(1440, 900);
render(YearBand, { year: makeYear(1914, [makeEntry()]) });
const section = document.querySelector('section') as HTMLElement;
const badge = document.querySelector('[data-testid="year-label"]') as HTMLElement;
const s = section.getBoundingClientRect();
const b = badge.getBoundingClientRect();
const sectionCenter = s.left + s.width / 2;
const badgeCenter = b.left + b.width / 2;
expect(Math.abs(badgeCenter - sectionCenter)).toBeLessThan(8);
});
it('left-aligns the year badge at phone width (REQ-003)', async () => {
await page.viewport(375, 800);
render(YearBand, { year: makeYear(1914, [makeEntry()]) });
const section = document.querySelector('section') as HTMLElement;
const badge = document.querySelector('[data-testid="year-label"]') as HTMLElement;
const s = section.getBoundingClientRect();
const b = badge.getBoundingClientRect();
// hugs the left spine — clearly not centered
expect(b.left - s.left).toBeLessThan(s.width / 3);
});
it('keeps the sticky year heading at top:4rem (REQ-003)', () => {
render(YearBand, { year: makeYear(1914, [makeEntry()]) });
const h2 = document.querySelector('h2') as HTMLElement;
const cs = getComputedStyle(h2);
expect(cs.position).toBe('sticky');
expect(cs.top).toBe('64px');
});
it('renders a year-badge node marker that clears the badge text on phone (REQ-004)', async () => {
await page.viewport(375, 800);
render(YearBand, { year: makeYear(1914, [makeEntry()]) });
const node = document.querySelector('[data-testid="year-node"]') as HTMLElement;
const label = document.querySelector('[data-testid="year-label"]') as HTMLElement;
expect(node).not.toBeNull();
const n = node.getBoundingClientRect();
const l = label.getBoundingClientRect();
expect(n.right).toBeLessThanOrEqual(l.left + 0.5);
});
it('renders one connector dot per letter row, each clearing its card on phone (REQ-005)', async () => {
await page.viewport(375, 800);
render(YearBand, { year: makeYear(1909, manyLetters(1909, 3)) });
const dots = document.querySelectorAll('[data-testid="letter-dot"]');
expect(dots).toHaveLength(3);
const row = document.querySelector('.letter-row') as HTMLElement;
const dot = row.querySelector('[data-testid="letter-dot"]') as HTMLElement;
const card = row.querySelector('a') as HTMLElement;
expect(dot.getBoundingClientRect().right).toBeLessThanOrEqual(
card.getBoundingClientRect().left + 0.5
);
});
});