feat(timeline): layer filter (Personal / Historical / Letters) for /zeitstrahl #843

Merged
marcel merged 8 commits from feat/issue-780-timeline-layer-filter into main 2026-06-14 22:11:39 +02:00

8 Commits

Author SHA1 Message Date
Marcel
096b4a0f4a fix(timeline): track the meta-line counts to the filtered view
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m46s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 5m10s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 24s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
SDD Gate / RTM Check (pull_request) Successful in 14s
SDD Gate / Contract Validate (pull_request) Successful in 22s
SDD Gate / Constitution Impact (pull_request) Successful in 17s
The /zeitstrahl header sub-line counted the unfiltered timeline, so a
hidden layer (e.g. Letters off) still showed its entries in the totals
("1 Brief" with no letters on screen) — the documented D1 limitation.
Derive the meta from filteredTimeline so the range and letter/event
counts always match what is actually rendered. hasContent stays on the
full timeline so the filter bar and meta line still appear whenever the
archive has content.

Refs #780

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 21:38:02 +02:00
Marcel
85cac0f089 docs(rtm): trace #780 timeline-layer-filter requirements
Some checks failed
SDD Gate / Contract Validate (pull_request) Successful in 25s
SDD Gate / Constitution Impact (pull_request) Successful in 17s
CI / Unit & Component Tests (pull_request) Successful in 4m43s
CI / fail2ban Regex (pull_request) Failing after 47s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m17s
CI / Semgrep Security Scan (pull_request) Successful in 25s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m10s
SDD Gate / RTM Check (pull_request) Successful in 14s
Add the ten REQ-001…REQ-010 rows for the /zeitstrahl layer filter, each linked
to its implementation file(s) and test(s), all Done.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:43:03 +02:00
Marcel
5f9139d188 test(timeline): add e2e journey + 375px axe for the layer filter
Playwright spec for /zeitstrahl: the primary journey (hide Letters → letter
cards vanish + trigger reports "1 aktiv" → reset restores) and a 375px axe pass
with the collapsible open in light and dark mode. Not skipped — #779 ships the
route. E2E is not wired into CI, so this runs locally only for now.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:38:43 +02:00
Marcel
0c73c7dbe4 test(timeline): guard the layer filter against navigation and fetch
A static boundary gate (mirroring the project's no-`{@html}` greps) that reads
TimelineFilters.svelte and /zeitstrahl/+page.svelte and fails if either ever
reintroduces goto(, url.searchParams, api.GET, or fetch( — the filter must stay
presentation-only and fully client-side.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:35:49 +02:00
Marcel
26b5bf92a7 feat(timeline): wire the layer filter into /zeitstrahl
The route holds the three layer toggles in $state, binds them into
TimelineFilters, and derives a client-side filtered view of the SSR-loaded
timeline that it passes to TimelineView — no goto, no URL param, no extra
fetch. When the active toggles leave nothing visible it renders a calm
filtered-empty message plus a one-click reset below the still-open filter bar,
never a blank page and never the generic "no events" state. The meta-line keeps
counting the unfiltered timeline (D1 known limitation).

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:33:35 +02:00
Marcel
fac09dab20 feat(timeline): add TimelineFilters presentation component
A dumb, client-side layer-filter bar for /zeitstrahl: three $bindable layer
toggles (Personal/Historical/Letters) in a fieldset/legend, a sticky
"Filter (N aktiv)" trigger driven by hiddenLayerCount, and a reset text button
shown only when a layer is off. Toggles mirror the SearchFilterBar undated-toggle
markup (aria-pressed, ✓ glyph, 44px touch target, semantic tokens). The
collapsible slide honours prefers-reduced-motion by zeroing its duration. No
goto, no fetch — the route owns the state and the filtered view.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:26:57 +02:00
Marcel
294c3f7f31 feat(timeline): add layer-filter i18n keys (de/en/es)
Eight Paraglide keys for the /zeitstrahl layer filter — layer labels, the
fieldset legend, the sticky trigger (distinct timeline_filter_trigger and
timeline_filter_trigger_active({count}) so it never reads "Filter (0 aktiv)"),
the reset button, and the filtered-empty message — added to all three locales.
messages.spec asserts their presence and the {count} signature.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:20:27 +02:00
Marcel
aac6d14b70 feat(timeline): add client-side layer-filter helpers
Pure helpers for the /zeitstrahl layer filter: isDefaultState and
hiddenLayerCount drive the "Filter (N active)" trigger, and filterTimeline
derives a client-side view that hides personal/historical/letter layers and
drops year bands left empty. Letters ride the Letters layer, HISTORICAL events
the Historical layer, and curated PERSONAL plus derived life-events the
Personal layer.

Refs #780
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 20:17:02 +02:00