Add the grouping $state beside the #780 layer-filter state, render the
GroupingControl stacked above the filter trigger (disabled, but kept in place,
when no loose letters remain), make the meta-line grouping label track the
active mode, and thread groupingMode into TimelineView — filter-then-group,
no refetch.
Refs #827
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap the heading in a flex-wrap header so the gated add-event link drops
below the title at narrow widths instead of overflowing; the CTA targets
#781's /zeitstrahl/events/new and only shows for a WRITE_ALL viewer. Also
thread canWrite into TimelineView so a curator sees the edit pencils on
the real page. New i18n key timeline_add_event in de/en/es.
Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
A count of one rendered "1 Briefe" / "1 Ereignisse". Add _singular
companion keys (de/en/es) and select them when the count is exactly one,
following the project's _singular/_plural convention.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A timeline with content in only one category rendered a misleading
"0 Briefe" / "0 Ereignisse" segment. Only push a count segment when its
count is greater than zero; the grouping label and (when present) the
range are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The page is already bg-canvas, so the frame's border was the only thing
making it visible; per review it reads cleaner without it. Keep the padded
bg-canvas surface; the timeline sits on the page without a frame line.
Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The timeline now sits inside a bordered, rounded bg-canvas sheet. Below the
heading a sub-line composes the year range, the letter and event counts
(from timelineMeta), and the static "Gruppierung: Datum" — joined by " · "
so the range drops out when there are no year bands and the whole line is
absent for an empty timeline. Semantic tokens only; AA-legible text-xs.
Refs #833
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SSR-first load fetches GET /api/timeline via createApiClient (auth cookie
forwarded), no query params for the global view (REQ-001), returns { timeline }
with no client-side fetch (REQ-002); 401 -> /login, any other non-ok ->
error(status, getErrorMessage(...)), never raw JSON, no PII logged (REQ-022).
The page renders <TimelineView> under the layout's <main>. Adds the Zeitstrahl
nav link (desktop + mobile) and 'timeline' to the eslint routes boundary
allow-list so the route may import the domain.
Refs #779
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>