feat(timeline): thread event clustering through TimelineView; drop the grouping meta segment
TimelineView builds the event lookup once over the whole timeline and threads it (plus canWrite) to every YearBand, so a curated event's letters cluster under it inline. The /zeitstrahl meta-line drops its 'Gruppierung: Datum' segment (toggle-free view, REQ-011); the now-unused timeline_grouping_date key is removed from de/en/es and the messages parity guard, which now asserts the new show-more/less keys. Refs #850
This commit is contained in:
@@ -74,9 +74,10 @@ describe('message key parity', () => {
|
||||
// every locale so no surface ever falls back to a missing translation.
|
||||
it('zeitstrahl visual-fidelity keys are present in all locales (#833 REQ-015)', () => {
|
||||
const requiredKeys = [
|
||||
'timeline_grouping_date',
|
||||
'timeline_provenance_derived',
|
||||
'timeline_provenance_curated',
|
||||
'timeline_bucket_show_more',
|
||||
'timeline_bucket_show_less',
|
||||
'timeline_letter_glyph_label',
|
||||
'timeline_layer_historical_suffix',
|
||||
'timeline_strip_density_caption',
|
||||
|
||||
@@ -6,6 +6,7 @@ import LetterCard from './LetterCard.svelte';
|
||||
import EventPill from './EventPill.svelte';
|
||||
import WorldBand from './WorldBand.svelte';
|
||||
import { entryKey } from './entryKey';
|
||||
import { buildEventLookup } from './eventClustering';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type TimelineDTO = components['schemas']['TimelineDTO'];
|
||||
@@ -18,6 +19,11 @@ type TimelineYearDTO = components['schemas']['TimelineYearDTO'];
|
||||
* empty timeline shows a calm message (REQ-017). `personId` is a declared seam
|
||||
* for the per-person rail (issue #10) and is undefined here; it is not passed to
|
||||
* leaf cards (REQ-025). Owns no <main> — the layout does.
|
||||
*
|
||||
* The event lookup is built once over the whole (already layer-filtered) timeline
|
||||
* and threaded to every band so a curated event's letters cluster under it inline
|
||||
* (#850, REQ-002). The undated bucket stays plain (events as pills, letters as
|
||||
* cards) — out of clustering scope.
|
||||
*/
|
||||
let {
|
||||
timeline,
|
||||
@@ -25,6 +31,8 @@ let {
|
||||
canWrite = false
|
||||
}: { timeline: TimelineDTO; personId?: string; canWrite?: boolean } = $props();
|
||||
|
||||
const eventLookup = $derived(buildEventLookup(timeline));
|
||||
|
||||
type Row = { t: 'band'; year: TimelineYearDTO } | { t: 'gap'; from: number; to: number };
|
||||
|
||||
const rows = $derived.by<Row[]>(() => {
|
||||
@@ -54,7 +62,7 @@ const isEmpty = $derived(timeline.years.length === 0 && timeline.undated.length
|
||||
{#each rows as row (row.t === 'band' ? `band-${row.year.year}` : `gap-${row.from}`)}
|
||||
<li>
|
||||
{#if row.t === 'band'}
|
||||
<YearBand year={row.year} canWrite={canWrite} />
|
||||
<YearBand year={row.year} canWrite={canWrite} eventLookup={eventLookup} />
|
||||
{:else}
|
||||
<GapSpan from={row.from} to={row.to} />
|
||||
{/if}
|
||||
|
||||
@@ -341,4 +341,34 @@ describe('TimelineView', () => {
|
||||
expect(hrefs).toContain('/zeitstrahl/events/wb/edit');
|
||||
expect(hrefs).toContain('/zeitstrahl/events/wu/edit');
|
||||
});
|
||||
|
||||
it('builds the event lookup and clusters a curated event + same-year linked letter into an event-card (#850)', () => {
|
||||
const evId = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee';
|
||||
const event = makeEntry({
|
||||
kind: 'EVENT',
|
||||
type: 'PERSONAL',
|
||||
derived: false,
|
||||
eventId: evId,
|
||||
eventDate: '1916-07-06',
|
||||
precision: 'DAY',
|
||||
title: 'Ein gewaltiger Stadtbrand',
|
||||
senderName: '',
|
||||
receiverName: '',
|
||||
documentId: undefined
|
||||
});
|
||||
const letter = makeEntry({
|
||||
eventDate: '1916-05-10',
|
||||
documentId: 'doc-linked',
|
||||
title: 'Brief',
|
||||
linkedEventId: evId
|
||||
});
|
||||
render(TimelineView, {
|
||||
timeline: makeTimelineDTO({ years: [makeYear(1916, [event, letter])] })
|
||||
});
|
||||
expect(document.querySelector('[data-testid="event-card"]')).not.toBeNull();
|
||||
expect(document.querySelector('a.lcard.ev')).not.toBeNull();
|
||||
// the title reads once — the event is the card header, not also a loose pill
|
||||
const titles = (document.body.textContent?.match(/Ein gewaltiger Stadtbrand/g) ?? []).length;
|
||||
expect(titles).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user