feat(timeline): contain buckets with a colour rail and collapse oversized ones

The grouped view flooded: buckets had no visual containment (a tiny floating pill
over cards identical to the ungrouped view) and the >12-letter density collapse was
gone, so "Weitere Briefe · 325" / "Sonstiges · 10" dumped every card.

LetterBucket now binds each cluster with a coloured left rail (tag colour in Thema,
mint for an Ereignis cluster, neutral for the fallback), renders compact cards, and
— above BUCKET_DENSE_THRESHOLD (6) — collapses to the existing month-density
YearLetterStrip instead of a flood. Adds a `nested` mode (no header) for letters that
sit under their event pill, and shares the tag-colour token allow-list via tagColorVar.

Refs #827
This commit is contained in:
Marcel
2026-06-15 12:01:47 +02:00
parent ca06293dc5
commit 23534fb077
3 changed files with 165 additions and 33 deletions

View File

@@ -14,6 +14,41 @@ export type GroupingMode = 'date' | 'event' | 'thema';
/** The default mode — chronological, as #779 shipped. */
export const DEFAULT_GROUPING: GroupingMode = 'date';
/**
* A bucket larger than this collapses to a month-density strip instead of flooding the
* timeline with individual cards (#827) — the catch-all "Weitere Briefe"/"Ohne Thema"
* buckets are always the biggest, so without this they swamp the grouped view. Lower than
* Datum mode's `DENSE_THRESHOLD` (12) because a bucket is a narrower context than a year.
*/
export const BUCKET_DENSE_THRESHOLD = 6;
export function isBucketDense(letterCount: number): boolean {
return letterCount > BUCKET_DENSE_THRESHOLD;
}
/** The `--c-tag-*` colour-name tokens (sage/sienna/…); shared by the chip and the rail. */
const TAG_COLOR_TOKENS = new Set([
'sage',
'sienna',
'amber',
'slate',
'violet',
'rose',
'cobalt',
'moss',
'sand',
'coral'
]);
/**
* Maps a root-tag colour-name token to its CSS variable reference, or `null` for an absent
* or unknown token (so a colourless/unrecognised tag falls back to a neutral rail, never a
* broken `var(--c-tag-undefined)`).
*/
export function tagColorVar(token: string | null | undefined): string | null {
return token && TAG_COLOR_TOKENS.has(token) ? `var(--c-tag-${token})` : null;
}
/**
* One bundle of loose letters under a single header, within a year (Ereignis/Thema modes).
* `kind` decides the header: a curated-event title, a tinted root-tag chip, or the localized