From 5bff428954f5c617e6ecea877be32c99a8c1dad3 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 13 Jun 2026 19:36:57 +0200 Subject: [PATCH] feat(timeline): add YearLetterStrip for dense years Letter count + 12-month density sparkline + a >=44px keyboard-focusable expand toggle that reveals that year's LetterCards (REQ-012). Sparkline values from the shared monthHistogram. Refs #779 Co-Authored-By: Claude Opus 4.8 --- .../src/lib/timeline/YearLetterStrip.svelte | 52 +++++++++++++++++++ .../timeline/YearLetterStrip.svelte.spec.ts | 42 +++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 frontend/src/lib/timeline/YearLetterStrip.svelte create mode 100644 frontend/src/lib/timeline/YearLetterStrip.svelte.spec.ts diff --git a/frontend/src/lib/timeline/YearLetterStrip.svelte b/frontend/src/lib/timeline/YearLetterStrip.svelte new file mode 100644 index 00000000..8409734c --- /dev/null +++ b/frontend/src/lib/timeline/YearLetterStrip.svelte @@ -0,0 +1,52 @@ + + +
+
+ {m.timeline_letters_count({ count: letters.length })} + +
+ + + + {#if expanded} +
    + {#each letters as letter (letter.documentId)} +
  • + {/each} +
+ {/if} +
diff --git a/frontend/src/lib/timeline/YearLetterStrip.svelte.spec.ts b/frontend/src/lib/timeline/YearLetterStrip.svelte.spec.ts new file mode 100644 index 00000000..4c78ea0e --- /dev/null +++ b/frontend/src/lib/timeline/YearLetterStrip.svelte.spec.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { tick } from 'svelte'; +import YearLetterStrip from './YearLetterStrip.svelte'; +import { makeEntry } from './test-factories'; + +afterEach(() => cleanup()); + +function denseLetters(year: number, count: number) { + return Array.from({ length: count }, (_, i) => + makeEntry({ + eventDate: `${year}-${String((i % 12) + 1).padStart(2, '0')}-10`, + documentId: `doc-${i}` + }) + ); +} + +describe('YearLetterStrip', () => { + it('shows the letter count and a 12-bar sparkline (REQ-012)', () => { + render(YearLetterStrip, { letters: denseLetters(1915, 30), year: 1915 }); + expect(document.body.textContent).toContain('30'); + const bars = document.querySelectorAll('[data-testid="sparkline-bar"]'); + expect(bars).toHaveLength(12); + }); + + it('has a keyboard-focusable expand toggle of at least 44px (REQ-012)', () => { + render(YearLetterStrip, { letters: denseLetters(1915, 30), year: 1915 }); + const toggle = document.querySelector('[data-testid="strip-expand"]') as HTMLButtonElement; + expect(toggle).not.toBeNull(); + expect(toggle.tagName).toBe('BUTTON'); + expect(toggle.getBoundingClientRect().height).toBeGreaterThanOrEqual(44); + }); + + it('reveals all letter cards when expanded (REQ-012)', async () => { + render(YearLetterStrip, { letters: denseLetters(1915, 30), year: 1915 }); + expect(document.querySelectorAll('a').length).toBe(0); + const toggle = document.querySelector('[data-testid="strip-expand"]') as HTMLButtonElement; + toggle.click(); + await tick(); + expect(document.querySelectorAll('a').length).toBe(30); + }); +});