# Timeline Date-Range Filter · Spec #385

Horizontal bar-chart widget placed between the SearchFilterBar and DocumentList on `/documents`. Bar height per month reflects document density across the archive. Drag or click to select a date range; the document list filters to that period. Hidden when calendar view is active.

Familienarchiv · 2026-05-03 · Leonie Voss, UX Lead · Issue #385

## 1 · Anatomy

Seven named zones. Every implementation decision maps back to one of these.

① Widget-Header (Label + Gesamtanzahl) ② Gesamtanzahl-Badge
③ Bar (1 Monat)
④ Auswahl-Bereich (navy)
1900 1910 1920 1930 1940 1950
⑤ Linker Zieh-Griff (Start) ⑥ Rechter Zieh-Griff (Ende) ⑦ Löschen-Button (×)
① Widget-Header — Label "Zeitachse" + Gesamtanzahl aller Dokumente im Archiv.
③ Bar — Ein Balken pro Monat. Höhe proportional zur Dokumentenanzahl dieses Monats. Max-Höhe = Monat mit den meisten Dokumenten.
④–⑥ Auswahl — Ausgewählte Balken werden navy (#012851). Zieh-Griffe definieren Start/Ende. ⑦ Löschen-Button erscheint sobald eine Auswahl aktiv ist.

## 2 · Design-Tokens

Das Widget verwendet ausschließlich diese Tokens — keine Hard-coded-Werte in der Komponente.

Light theme

bg-surface | #FFFFFF — Widget-Hintergrund
border-line | #D8D6CF — Widget-Rahmen
bar-idle | #D4EDE9 — Balken ohne Auswahl (helles Mint)
bar-hover | #A1DCD8 — Balken on hover (brand-mint)
bar-selected | #012851 — Balken im Auswahl-Bereich (navy)
bar-outside | #E8E6E0 — Balken außerhalb der Auswahl (gedimmt)
handle | #012851 mit weißem Kern — Zieh-Griff
tooltip-bg | #1A1A1A — Tooltip-Hintergrund

Dark theme

bg-surface | #0A1218 — Widget-Hintergrund
bar-idle | #0E2535 — Balken ohne Auswahl (dunkles Teal)
bar-hover | #1E4060 — hover
bar-selected | #A1DCD8 — Auswahl in Dark mode (Mint, invertiert)
bar-outside | #0A1218 — außerhalb, kaum sichtbar
handle | #A1DCD8 mit dunklem Kern

## 3 · Zustandsmodell

Vier Zustände. Gleiche Markup-Struktur — nur CSS-Klassen und ARIA-Attribute ändern sich.

State AIdle — keine Auswahl
Dokumente durchsuchen …
Sortierung
▼ Filter
Zeitachse 412 Dokumente · 1900–1950
190019101920193019401950
1915 · 24 Dokumente
Brief über die Lage an der Westfront
Karl Raddatz → Elfriede Raddatz
März 1915
Feldpost aus Verdun
Hans Raddatz → Karl Raddatz
Juli 1915
Idle: Alle Balken in hellem Mint. Kein Griff, kein Löschen-Button. Cursor: `crosshair` über den Balken.
State BHover — Tooltip
Zeitachse 412 Dokumente · 1900–1950
August 1915 · 24 Dok.
190019101920193019401950
Hover: Balken wechselt zu brand-mint. Tooltip erscheint: „Monat Jahr · N Dok." Cursor: `pointer`.
State CAktive Auswahl — 1914–1920 (Erster Weltkrieg)
Dokumente durchsuchen …
Sortierung
▼ Filter
Jan 1914 – Dez 1920 ×
Zeitachse 187 Dokumente · Jan 1914 – Dez 1920
190019101920193019401950
1915 · 24 Dokumente
Brief über die Lage an der Westfront
Karl Raddatz → Elfriede Raddatz
März 1915
Feldpost aus Verdun
Hans Raddatz → Karl Raddatz
Juli 1915
1914 · 18 Dokumente
Kriegsausbruch — Brief an die Familie
Karl Raddatz → Elfriede Raddatz, Hans Raddatz
Aug 1914
Aktive Auswahl: Balken im Bereich navy, außerhalb gedimmt. Handles links/rechts ziehbar. Label in SearchFilterBar zeigt „Jan 1914 – Dez 1920". Dokumentenliste zeigt nur Dokumente im gewählten Zeitraum.

## 4 · Dark Mode

Gleiches Verhalten, Farben remappt. Auswahl-Balken wechseln zu Mint (invertiert zu Light mode).

Dark · Idle
Dokumente durchsuchen …
▼ Filter
Zeitachse 412 Dokumente · 1900–1950
190019101920193019401950
Dark · Auswahl aktiv1914–1920
Dokumente durchsuchen …
Jan 1914 – Dez 1920 ×
Zeitachse 187 Dokumente · Jan 1914 – Dez 1920
190019101920193019401950
Dark mode: Ausgewählte Balken = Mint (#A1DCD8), außerhalb = fast schwarz (#0A1218). Badge in SearchFilterBar: Mint-Hintergrund mit Navy-Text.

## 5 · Verhaltensregeln

Szenario | Verhalten
--- | ---
Click auf einzelnen Balken | Setzt Start und Ende auf denselben Monat → Einzelmonat-Filter. Direkt gefiltert, kein Drag nötig.
Click + Drag | Start = Mousedown-Position, Ende = Mousemove-Position. Richtung egal (rechts → links und links → rechts erlaubt). Filter wird live aktualisiert während des Drags.
Handle drag | Start- oder End-Handle verschieben justiert die bestehende Auswahl. Der andere Handle bleibt fixiert.
Click außerhalb der Balken | Auswahl wird gelöscht. Liste zeigt alle Dokumente.
Löschen-Button (×) | Auswahl löschen. Badge in SearchFilterBar verschwindet. Bars kehren zu Idle zurück.
Kombination mit anderen Filtern | AND-Semantik. Zeitachsen-Filter + Personen-Filter + Tag-Filter sind kumulativ.
Wechsel zu Kalenderansicht | Timeline-Widget wird ausgeblendet (`display:none`). Aktive Auswahl bleibt im State erhalten und wird wieder angezeigt beim Rückwechsel.
Monate ohne Dokumente | Bar hat Mindesthöhe von 2px (sichtbar, aber minimal). Tooltip zeigt „Monat Jahr · 0 Dokumente". Klick auf Null-Bar setzt Single-Month-Filter (leere Liste erwartet).
Granularität (OQ-2) | Entscheidung: ganze Monate. Start = 1. des Startmonats, Ende = letzter Tag des Endmonats. Keine Wochen oder Tage auswählbar.
Startmonat beim Öffnen (OQ-1) | Entscheidung: auto-derived range. X-Achse leitet sich aus frühestem und spätestem `documentDate` ab. Kein festes Datum (1880–heute).
Keyboard-Zugang | Tab fokussiert das Widget. Pfeiltasten verschieben einen virtuellen Cursor Monat für Monat. Enter/Space setzt Start; zweites Enter setzt Ende. Escape bricht Drag ab.
Mobile (<768 px) | Widget kollabiert auf kompakte Zeile: „Zeitraum: [Jan 1914 – Dez 1920] ×" — kein Balkendiagramm. Tap öffnet ein Modal mit dem vollen Widget.

## 6 · Offene Fragen — Entscheidungen

OQ-1: Zeitachsen-Bereich. Auto-derived aus frühestem und spätestem `documentDate` in der Datenbank. Beim Laden der Seite holt der Server `GET /api/documents/stats` (oder ähnlich) mit `minDate` und `maxDate`. Kein hard-coded Bereich.
OQ-2: Granularität. Ganze Monate. Ein Balken = ein Monat. Kein Sub-Monats-Drag. Begründung: Die Archive haben typischerweise 1–30 Briefe pro Monat — wöchentliche Granularität brächgt keine Zusatzinfo, würde aber 4× mehr Balken erzeugen.
Backend-Anforderung. Neuer Query-Parameter `?dateFrom=YYYY-MM&dateTo=YYYY-MM` auf dem bestehenden Search-Endpoint, oder dedizierter Density-Endpoint `GET /api/documents/density?from=1900-01&to=1950-12` der `[{month:"1915-08", count:24}, ...]` zurückgibt. Das bestehende Offset-Paging entfällt für diesen Widget-Call.

## 7 · Implementation Reference

Element | Tailwind-Klassen | Pixelwert | Anmerkung
--- | --- | --- | ---
Widget-Container | `bg-surface border border-line rounded-sm shadow-sm px-4 py-3` | padding 16px 12px | Gleiche Karte wie SearchFilterBar und DocumentList
Widget-Header Zeile | `flex items-center justify-between mb-2` | margin-bottom 8px | Label links, Gesamtanzahl rechts
Header Label | `text-[10px] font-bold uppercase tracking-widest text-ink-3` | 10px / 700 | Identisch zu anderen Section-Labels
Gesamtanzahl-Text | `text-[10px] text-ink-3` — wenn Auswahl aktiv: `text-primary font-bold` | 10px | Zeigt gefilterte Anzahl sobald Auswahl aktiv
Bar-Container | `flex items-end gap-px h-10 w-full` | height 40px | `h-12` (48px) wenn Platz es erlaubt
Bar idle | `flex-1 rounded-t-sm min-h-[2px]` + inline `height` als %-Wert | var | CSS custom property `--bar-h` setzen via JS
Bar: Farbe idle | `bg-[#D4EDE9]` (light) / `bg-[#0E2535]` (dark) | — | Helles Mint, nicht brand-mint — zu dominant
Bar: Farbe hover | `hover:bg-mint` | — | `transition-colors duration-100`
Bar: Farbe selected | `bg-primary` (light) / `bg-mint` (dark) | — | Invertierung in Dark mode beabsichtigt
Bar: Farbe outside | `bg-muted` | — | Gedimmt, aber noch sichtbar
Baseline | `border-t border-line mt-0.5 mb-1` | 1px | Trennt Balken von X-Achsen-Labels
X-Achse Labels | `flex justify-between text-[9px] text-ink-3` | 9px | Nur Jahres-Labels alle 10 Jahre; mehr wenn Platz
Drag Handle | `absolute w-3 h-3 rounded-full bg-primary border-2 border-surface shadow cursor-ew-resize -top-1.5 left-1/2 -translate-x-1/2 z-10` | 12px / 12px | Touch target: mind. 44px durch `p-4` auf einem unsichtbaren Wrapper
Tooltip | `absolute bottom-full left-1/2 -translate-x-1/2 mb-1 bg-ink text-surface text-[9px] font-bold px-1.5 py-0.5 rounded-sm whitespace-nowrap z-20 pointer-events-none` | 9px | Erscheint via `group-hover:block hidden`
Badge in SearchFilterBar | `flex items-center gap-1 bg-primary text-primary-fg text-[9px] font-bold px-2 py-1 rounded-sm` | 9px | Löschen-Button innerhalb: `ml-1 text-sm leading-none hover:opacity-70`
Mobile Collapse | Widget: `sm:block hidden` — Mobile-Badge: `sm:hidden flex items-center gap-2` | — | Unter 640px nur Badge-Zeile sichtbar, kein Chart
Komponenten-Name | `TimelineDensityFilter.svelte` | — | Props: `density: MonthBucket[]`, `bind:from`, `bind:to`, `onchange`