feat(timeline): render letter buckets in TimelineView/YearBand
Thread groupingMode through TimelineView → YearBand. TimelineView resolves the event lookup once over the filtered view (so Ereignis clusters never reference a filtered-out event). In non-Datum modes YearBand keeps its event pills/world-bands identical (REQ-001) and replaces the loose letters with per-year LetterBuckets (REQ-002/003/004); Datum keeps the original card/strip path. The undated bucket is unchanged in every mode. Refs #827 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,14 @@ import EventPill from './EventPill.svelte';
|
||||
import WorldBand from './WorldBand.svelte';
|
||||
import LetterCard from './LetterCard.svelte';
|
||||
import YearLetterStrip from './YearLetterStrip.svelte';
|
||||
import LetterBucket from './LetterBucket.svelte';
|
||||
import { isDense } from './timelineDensity';
|
||||
import { entryKey } from './entryKey';
|
||||
import {
|
||||
bucketLetters,
|
||||
type GroupingMode,
|
||||
type LetterBucket as LetterBucketModel
|
||||
} from './timelineGrouping';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type TimelineYearDTO = components['schemas']['TimelineYearDTO'];
|
||||
@@ -15,19 +21,48 @@ type TimelineEntryDTO = components['schemas']['TimelineEntryDTO'];
|
||||
* render in DTO order as pills/bands; letters render as individual cards while
|
||||
* the band holds ≤ 12 (REQ-011) or collapse to a single density strip above that
|
||||
* (REQ-012). Entries are never re-sorted — DTO order is preserved (REQ-003).
|
||||
*
|
||||
* In Ereignis/Thema mode (#827) the event pills/world-bands render identically
|
||||
* (REQ-001); only the loose letters re-bundle into per-year buckets below them
|
||||
* (REQ-002/003/004). Datum mode is the original individual-card / density-strip
|
||||
* path, untouched.
|
||||
*/
|
||||
let { year, canWrite = false }: { year: TimelineYearDTO; canWrite?: boolean } = $props();
|
||||
let {
|
||||
year,
|
||||
canWrite = false,
|
||||
groupingMode = 'date',
|
||||
eventLookup = new Map<string, string>()
|
||||
}: {
|
||||
year: TimelineYearDTO;
|
||||
canWrite?: boolean;
|
||||
groupingMode?: GroupingMode;
|
||||
eventLookup?: Map<string, string>;
|
||||
} = $props();
|
||||
|
||||
type Row =
|
||||
| { t: 'event'; entry: TimelineEntryDTO }
|
||||
| { t: 'letter'; entry: TimelineEntryDTO; side: 'left' | 'right' }
|
||||
| { t: 'strip' };
|
||||
| { t: 'strip' }
|
||||
| { t: 'bucket'; bucket: LetterBucketModel };
|
||||
|
||||
const letters = $derived(year.entries.filter((e) => e.kind === 'LETTER'));
|
||||
const dense = $derived(isDense(letters.length));
|
||||
const grouped = $derived(groupingMode !== 'date');
|
||||
const bucketMode = $derived(groupingMode === 'thema' ? 'thema' : 'event');
|
||||
|
||||
const rows = $derived.by<Row[]>(() => {
|
||||
const out: Row[] = [];
|
||||
if (grouped) {
|
||||
// Events stay on the axis, identical to Datum mode (REQ-001); only the loose
|
||||
// letters re-bundle into per-year buckets below them (REQ-003/004).
|
||||
for (const entry of year.entries) {
|
||||
if (entry.kind === 'EVENT') out.push({ t: 'event', entry });
|
||||
}
|
||||
for (const bucket of bucketLetters(letters, bucketMode, eventLookup)) {
|
||||
out.push({ t: 'bucket', bucket });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
let stripInserted = false;
|
||||
let letterIndex = 0;
|
||||
for (const entry of year.entries) {
|
||||
@@ -43,6 +78,12 @@ const rows = $derived.by<Row[]>(() => {
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
function rowKey(row: Row): string {
|
||||
if (row.t === 'strip') return `strip-${year.year}`;
|
||||
if (row.t === 'bucket') return row.bucket.key;
|
||||
return entryKey(row.entry);
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="py-2">
|
||||
@@ -56,7 +97,7 @@ const rows = $derived.by<Row[]>(() => {
|
||||
</h2>
|
||||
|
||||
<div class="mt-3 space-y-3">
|
||||
{#each rows as row (row.t === 'strip' ? `strip-${year.year}` : entryKey(row.entry))}
|
||||
{#each rows as row (rowKey(row))}
|
||||
{#if row.t === 'event'}
|
||||
{#if row.entry.type === 'HISTORICAL'}
|
||||
<WorldBand entry={row.entry} canWrite={canWrite} />
|
||||
@@ -68,6 +109,8 @@ const rows = $derived.by<Row[]>(() => {
|
||||
<span data-testid="letter-dot" class="letter-dot bg-surface" aria-hidden="true"></span>
|
||||
<LetterCard entry={row.entry} />
|
||||
</div>
|
||||
{:else if row.t === 'bucket'}
|
||||
<LetterBucket bucket={row.bucket} mode={bucketMode} />
|
||||
{:else}
|
||||
<YearLetterStrip letters={letters} year={year.year} />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user