From 18934413bb8b5eb8a0b13780e950b6c754d8d96c Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 14 Jun 2026 11:10:05 +0200 Subject: [PATCH] feat(timeline): center the year badge and add spine markers (REQ-003/004/005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/lib/timeline/YearBand.svelte | 80 +++++++++++++++++-- .../src/lib/timeline/YearBand.svelte.spec.ts | 61 +++++++++++++- 2 files changed, 132 insertions(+), 9 deletions(-) diff --git a/frontend/src/lib/timeline/YearBand.svelte b/frontend/src/lib/timeline/YearBand.svelte index a612a750..64a19fdd 100644 --- a/frontend/src/lib/timeline/YearBand.svelte +++ b/frontend/src/lib/timeline/YearBand.svelte @@ -46,10 +46,13 @@ const rows = $derived.by(() => {
-

- {year.year} +

+ + {year.year}

@@ -62,6 +65,7 @@ const rows = $derived.by(() => { {/if} {:else if row.t === 'letter'}
+
{:else} @@ -73,19 +77,69 @@ const rows = $derived.by(() => { diff --git a/frontend/src/lib/timeline/YearBand.svelte.spec.ts b/frontend/src/lib/timeline/YearBand.svelte.spec.ts index 5d43a6e5..ce1abff0 100644 --- a/frontend/src/lib/timeline/YearBand.svelte.spec.ts +++ b/frontend/src/lib/timeline/YearBand.svelte.spec.ts @@ -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 + ); + }); });