diff --git a/frontend/src/lib/timeline/eventCardConfig.spec.ts b/frontend/src/lib/timeline/eventCardConfig.spec.ts new file mode 100644 index 00000000..8fd33355 --- /dev/null +++ b/frontend/src/lib/timeline/eventCardConfig.spec.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest'; +import { getAccentConfig } from './eventCardConfig'; +import type { components } from '$lib/generated/api'; + +type TimelineEntryDTO = components['schemas']['TimelineEntryDTO']; + +function event(overrides: Partial): TimelineEntryDTO { + return { + kind: 'EVENT', + precision: 'YEAR', + derived: false, + senderName: '', + receiverName: '', + ...overrides + }; +} + +describe('getAccentConfig', () => { + it('maps a derived birth to the * glyph and "Geburt"', () => { + const cfg = getAccentConfig(event({ derived: true, derivedType: 'BIRTH' })); + expect(cfg.glyph).toBe('*'); + expect(cfg.label).toBe('Geburt'); + expect(cfg.accent).toBe('derived'); + }); + + it('maps a derived death to the † glyph and "Tod"', () => { + const cfg = getAccentConfig(event({ derived: true, derivedType: 'DEATH' })); + expect(cfg.glyph).toBe('†'); + expect(cfg.label).toBe('Tod'); + expect(cfg.accent).toBe('derived'); + }); + + it('maps a derived marriage to the ⚭ glyph and "Heirat"', () => { + const cfg = getAccentConfig(event({ derived: true, derivedType: 'MARRIAGE' })); + expect(cfg.glyph).toBe('⚭'); + expect(cfg.label).toBe('Heirat'); + expect(cfg.accent).toBe('derived'); + }); + + it('maps a HISTORICAL event to the world glyph and "Weltgeschehen"', () => { + const cfg = getAccentConfig(event({ type: 'HISTORICAL' })); + expect(cfg.glyph).toBe('◍'); + expect(cfg.label).toBe('Weltgeschehen'); + expect(cfg.accent).toBe('historical'); + }); + + it('maps a curated PERSONAL event to the ★ glyph and "Familie"', () => { + const cfg = getAccentConfig(event({ type: 'PERSONAL', eventId: 'e-1' })); + expect(cfg.glyph).toBe('★'); + expect(cfg.label).toBe('Familie'); + expect(cfg.accent).toBe('curated'); + }); +}); diff --git a/frontend/src/lib/timeline/eventCardConfig.ts b/frontend/src/lib/timeline/eventCardConfig.ts new file mode 100644 index 00000000..715249b3 --- /dev/null +++ b/frontend/src/lib/timeline/eventCardConfig.ts @@ -0,0 +1,38 @@ +import * as m from '$lib/paraglide/messages.js'; +import type { components } from '$lib/generated/api'; + +type TimelineEntryDTO = components['schemas']['TimelineEntryDTO']; + +/** Styling discriminant for an axis pill/band. */ +export type TimelineAccent = 'derived' | 'curated' | 'historical'; + +export interface AccentConfig { + /** Visible Unicode glyph — render `aria-hidden`, paired with an sr-only label. */ + glyph: string; + /** German layer/life-event label — used as the sr-only text and as visible chrome. */ + label: string; + accent: TimelineAccent; +} + +/** + * Maps a timeline EVENT entry to its glyph, redundant non-color label, and accent + * (REQ-007/008/018). Derived life-events use the * / † / ⚭ glyphs that match + * `personLifeDates.ts`; HISTORICAL events get the muted world band; everything + * else (curated PERSONAL) gets the mint family pill. + */ +export function getAccentConfig(entry: TimelineEntryDTO): AccentConfig { + if (entry.derived) { + switch (entry.derivedType) { + case 'BIRTH': + return { glyph: '*', label: m.timeline_derived_birth(), accent: 'derived' }; + case 'DEATH': + return { glyph: '†', label: m.timeline_derived_death(), accent: 'derived' }; + case 'MARRIAGE': + return { glyph: '⚭', label: m.timeline_derived_marriage(), accent: 'derived' }; + } + } + if (entry.type === 'HISTORICAL') { + return { glyph: '◍', label: m.timeline_layer_world(), accent: 'historical' }; + } + return { glyph: '★', label: m.timeline_layer_family(), accent: 'curated' }; +} diff --git a/frontend/src/lib/timeline/test-factories.ts b/frontend/src/lib/timeline/test-factories.ts new file mode 100644 index 00000000..25bc3026 --- /dev/null +++ b/frontend/src/lib/timeline/test-factories.ts @@ -0,0 +1,34 @@ +import type { components } from '$lib/generated/api'; + +type TimelineEntryDTO = components['schemas']['TimelineEntryDTO']; +type TimelineYearDTO = components['schemas']['TimelineYearDTO']; +type TimelineDTO = components['schemas']['TimelineDTO']; + +/** + * Builds a `TimelineEntryDTO` mirroring the real wire shape (no `year`, + * `description`, or `snippet` fields). Defaults to a dated DAY-precision letter; + * override `kind`/`derived`/`type`/`derivedType` etc. for events. + */ +export function makeEntry(overrides: Partial = {}): TimelineEntryDTO { + return { + kind: 'LETTER', + precision: 'DAY', + derived: false, + senderName: 'Karl Raddatz', + receiverName: 'Elfriede Raddatz', + eventDate: '1915-06-15', + title: 'Brief aus dem Feld', + documentId: '11111111-1111-1111-1111-111111111111', + ...overrides + }; +} + +export function makeYear(year: number, entries: TimelineEntryDTO[]): TimelineYearDTO { + return { year, entries }; +} + +export function makeTimelineDTO( + opts: { years?: TimelineYearDTO[]; undated?: TimelineEntryDTO[] } = {} +): TimelineDTO { + return { years: opts.years ?? [], undated: opts.undated ?? [] }; +}