From b031f2736bda5839023600c92081fff0d509f2cd Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 13 Jun 2026 19:36:03 +0200 Subject: [PATCH] feat(timeline): add EventPill for derived + curated event pills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centered axis pill: derived life-events (* Geburt / † Tod / ⚭ Heirat) and curated PERSONAL events (★, mint border) via getAccentConfig. Glyph wrapped aria-hidden + sr-only label (REQ-018). Edit affordance only for a curated event with eventId, never derived/null (REQ-008). REQ-007. Refs #779 Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/timeline/EventPill.svelte | 59 ++++++++++++ .../src/lib/timeline/EventPill.svelte.spec.ts | 90 +++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 frontend/src/lib/timeline/EventPill.svelte create mode 100644 frontend/src/lib/timeline/EventPill.svelte.spec.ts diff --git a/frontend/src/lib/timeline/EventPill.svelte b/frontend/src/lib/timeline/EventPill.svelte new file mode 100644 index 00000000..9b125cb7 --- /dev/null +++ b/frontend/src/lib/timeline/EventPill.svelte @@ -0,0 +1,59 @@ + + +
+
+ + + {config.label} + + + {#if entry.title} + {entry.title} + {/if} + {#if dateLabel} + {dateLabel} + {/if} + + {#if canEdit} + + + {m.btn_edit()} + + {/if} +
+
diff --git a/frontend/src/lib/timeline/EventPill.svelte.spec.ts b/frontend/src/lib/timeline/EventPill.svelte.spec.ts new file mode 100644 index 00000000..945ea65f --- /dev/null +++ b/frontend/src/lib/timeline/EventPill.svelte.spec.ts @@ -0,0 +1,90 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import EventPill from './EventPill.svelte'; +import { makeEntry } from './test-factories'; + +afterEach(() => cleanup()); + +const EVENT_ID = '33333333-3333-3333-3333-333333333333'; + +function derived(derivedType: 'BIRTH' | 'DEATH' | 'MARRIAGE', title: string) { + return makeEntry({ + kind: 'EVENT', + derived: true, + derivedType, + title, + senderName: '', + receiverName: '', + precision: 'YEAR', + eventDate: '1914-01-01', + documentId: undefined + }); +} + +describe('EventPill', () => { + it('renders a derived marriage as ⚭ + "Heirat" + title (REQ-007)', () => { + render(EventPill, { entry: derived('MARRIAGE', 'Heirat: Karl & Elfriede') }); + expect(document.body.textContent).toContain('⚭'); + expect(document.body.textContent).toContain('Heirat'); + expect(document.body.textContent).toContain('Heirat: Karl & Elfriede'); + }); + + it('renders a derived birth as * + "Geburt" (REQ-007)', () => { + render(EventPill, { entry: derived('BIRTH', 'Geburt: Hans') }); + expect(document.body.textContent).toContain('*'); + expect(document.body.textContent).toContain('Geburt'); + }); + + it('renders a derived death as † + "Tod" (REQ-007)', () => { + render(EventPill, { entry: derived('DEATH', 'Tod: Karl') }); + expect(document.body.textContent).toContain('†'); + expect(document.body.textContent).toContain('Tod'); + }); + + it('wraps the glyph aria-hidden with an sr-only label sibling (REQ-018)', () => { + render(EventPill, { entry: derived('BIRTH', 'Geburt: Hans') }); + const hidden = document.querySelector('[aria-hidden="true"]'); + expect(hidden?.textContent).toBe('*'); + const srOnly = document.querySelector('.sr-only'); + expect(srOnly?.textContent).toBe('Geburt'); + }); + + it('shows an edit affordance for a curated PERSONAL event with an eventId (REQ-008)', () => { + render(EventPill, { + entry: makeEntry({ + kind: 'EVENT', + derived: false, + type: 'PERSONAL', + eventId: EVENT_ID, + title: 'Auswanderung', + senderName: '', + receiverName: '', + documentId: undefined + }) + }); + const edit = document.querySelector('[data-testid="event-edit"]') as HTMLAnchorElement | null; + expect(edit).not.toBeNull(); + expect(edit?.getAttribute('href')).toContain(EVENT_ID); + }); + + it('shows no edit affordance when eventId is null (REQ-008)', () => { + render(EventPill, { + entry: makeEntry({ + kind: 'EVENT', + derived: false, + type: 'PERSONAL', + eventId: undefined, + title: 'Auswanderung', + senderName: '', + receiverName: '', + documentId: undefined + }) + }); + expect(document.querySelector('[data-testid="event-edit"]')).toBeNull(); + }); + + it('shows no edit affordance for a derived event (REQ-008)', () => { + render(EventPill, { entry: derived('MARRIAGE', 'Heirat') }); + expect(document.querySelector('[data-testid="event-edit"]')).toBeNull(); + }); +});