Korrespondenz — Final Design Spec

Correspondence Log with Compact Strip + Permanent Second Row. All design decisions from the exploration session locked in.

Final · Ready for implementation
Page name
Gespräche
→ Korrespondenz
Timeline renderer
Chat bubbles
→ Correspondence Log
Filter
Compact strip
2 rows · always visible
Receiver
Required
→ Optional (single-person mode)
Discovery
Top correspondents
in person B dropdown
Ghost cards
Considered
→ Dropped
Hub sidebar
Considered
→ Dropped (person detail)
Secondary filters
Permanent row 2
Von / Bis / Sort / Count
📐 Mockup scale notice — all font-size, height, padding, and spacing values in the mockup CSS below are scaled to ~55% of their real implementation values so they fit on screen. Do not copy sizes from the mockup HTML/CSS. Each section ends with an ⚙ Implementation Reference table listing the exact Tailwind classes and real pixel values to use in code.

What changes vs. current implementation

New / changed

  • Nav label: "Gespräche" → "Korrespondenz" (i18n key update)
  • Filter card → compact 2-row strip (redesign ConversationFilterBar)
  • Row 2 always visible: Von · Bis date inputs + live letter count + sort toggle
  • Receiver field: required → optional; single-person mode works without it
  • Person B input shows top correspondents as suggestions when person A is set (uses existing restrictToCorrespondentsOf — just surface the results)
  • Timeline renderer: chat bubbles → correspondence log cards (redesign ConversationTimeline)
  • Log card: direction arrow (→/←) + left border color + title + date + sender/recipient + status dot
  • Asymmetry bar visible when both persons selected
  • Empty state: centered prompt with person search + recent chips (remove "select both" gate)
  • Single-person mode hint strip: amber bar explaining scope

Kept unchanged

  • PersonTypeahead component — used in both fields as-is
  • restrictToCorrespondentsOf prop — drives the suggestions
  • Swap button (⇄) — same behaviour
  • /api/documents/conversation endpoint — no backend change for bilateral view
  • URL params: ?senderId=…&receiverId=…&from=…&to=…&dir=…
  • SvelteKit navigation with goto()
  • Year divider logic from ConversationTimeline
  • canWrite / new document link
1 Empty state — no person selected
Desktop ≥768px No selection
/korrespondenz
DokumentePersonen Korrespondenz
Person
🔍 Name eingeben…
Korrespondent — optional
Alle Korrespondenten
Zeitraum
Von…
Bis…
Neueste ↓
Korrespondenz durchsuchen
Wähle eine Person aus dem Archiv um deren Briefe zu sehen — mit oder ohne Korrespondent.
🔍 Person suchen…
oder
Zuletzt geöffnet
OF
Otto Familienname
MW
Maria Weber
KF
Klaus Fischer
Row 2 dimmed but visible — communicates the feature exists before it's usable. Clicking the "Person suchen" input focuses the strip's Person field. Recent chips use localStorage; shown only if history exists.
Mobile 375px
09:41
Person
🔍 Name eingeben…
Korrespondent — optional
Alle Korrespondenten
Zeitraum
Von…
Bis…
Neueste ↓
Korrespondenz durchsuchen
Person oben eingeben um Briefe zu sehen.
OF
Otto F.
MW
Maria W.
Mobile: both person fields stacked full-width (44px touch target). Row 2 stacked beneath. Recent chips condensed to initials + first name.
Implementation Reference — Filter Strip & Empty State Real values · mockup above is ~55% scale
ElementTailwind classesReal sizeNotes
Strip Row 1 container flex items-end gap-4 px-4 sm:px-6 py-3 bg-surface border-b border-line h ~56px, px 16–24px, py 12px White background, 1px bottom border
Field label (.FL) text-xs font-bold uppercase tracking-widest text-ink-2 mb-1.5 12px / 700 "optional" suffix: font-normal not-italic text-ink-3
Text input / typeahead h-10 w-full px-3 text-sm border border-line bg-surface text-ink placeholder:text-ink-3 focus:border-ink focus:ring-1 focus:ring-ink focus:outline-none 40px tall, 14px text Optional field: border-dashed bg-muted when empty
Swap button h-10 w-10 flex shrink-0 items-center justify-center border border-line text-ink-2 hover:text-ink hover:border-ink transition-colors self-end 40×40px Active (both set): border-ink text-ink
Strip Row 2 container flex items-center gap-3 px-4 sm:px-6 py-2 bg-canvas border-b border-line h ~36px, py 8px When no person selected: opacity-40 pointer-events-none
Row 2 period label text-xs font-bold uppercase tracking-widest text-ink-3 shrink-0 12px / 700 "Zeitraum" label before the date fields
Date input (Von / Bis) h-8 w-24 px-2 text-xs border border-line bg-surface text-ink placeholder:text-ink-3 focus:border-ink focus:outline-none 32px tall, 12px text, 96px wide When set: border-ink. Format: dd.mm.yyyy
Letter count ml-auto text-sm font-bold text-ink-2 14px / 700 When filtered (≠ total): text-primary
Sort toggle button h-8 px-3 text-xs font-bold border border-line text-ink-2 hover:text-ink hover:border-ink transition-colors 32px tall, 12px text Active: border-primary text-primary
Empty state icon circle w-16 h-16 rounded-full bg-muted flex items-center justify-center text-3xl mb-4 64×64px, 30px icon
Empty state heading font-serif text-xl font-bold text-ink mb-2 20px / 700
Empty state sub-text text-sm text-ink-2 max-w-xs text-center leading-relaxed mb-6 14px
Recent person chip inline-flex items-center gap-2 px-3 py-1.5 border border-line bg-surface text-sm font-semibold text-ink hover:border-primary transition-colors h ~32px, 14px text Avatar: w-5 h-5 rounded-full text-[10px]
2 Single-person mode — Person A set, picking correspondent
Person B input focused Suggestions shown
/korrespondenz?senderId=otto-id
DokumentePersonenKorrespondenz
Person
OF
Otto Familienname
Korrespondent — optional
🔍Ottos Korrespondenten…
Häufigste Korrespondenten
MW
Maria Weber
32 Briefe
KF
Klaus Fischer
11 Briefe
HM
Helene Müller
4 Briefe
oder ohne Einschränkung
Alle Korrespondenten Ottos
47 Briefe
Zeitraum
Von…
Bis…
47 Briefe
Neueste ↓
📋 Alle Briefe Ottos — wähle einen Korrespondenten oben um einzugrenzen
19435 Briefe
Brief an Maria — letzte Nachrichten
3. Sept. 1943·Maria Weber
Brief an Klaus Fischer
12. Sept. 1943·Klaus Fischer
Antwort von Klaus
18. Sept. 1943·Klaus Fischer
Dropdown uses restrictToCorrespondentsOf — only actual correspondents shown. "Alle Korrespondenten" row is the explicit opt-out. Log shows immediately (no blank screen), hint bar explains scope.
Single-person log No correspondent
/korrespondenz?senderId=otto-id
DokumentePersonenKorrespondenz
Person
OF
Otto Familienname
Korrespondent — optional
Alle Korrespondenten
Zeitraum
Von…
Bis…
47 Briefe
Neueste ↓
📋 Alle Briefe Ottos — wähle einen Korrespondenten oben um einzugrenzen
19435 Briefe
Brief an Maria — letzte Nachrichten
3. Sept. 1943·Maria Weber
Brief an Klaus Fischer
12. Sept. 1943·Klaus Fischer
Antwort von Klaus
18. Sept. 1943·Klaus Fischer
19428 Briefe
Brief aus dem Feld
14. März 1942·Maria Weber
Neuigkeiten aus München
22. Jan. 1942·Maria Weber
In single-person mode the recipient/sender name appears in the meta line — the only way to see who each letter is addressed to without filtering. Dashed border on correspondent field signals it's optional.
Date range active Filtered + sorted
/korrespondenz?senderId=otto-id&from=1940-01-01&to=1943-12-31&dir=ASC
DokumentePersonenKorrespondenz
Person
OF
Otto Familienname
Korrespondent — optional
Alle Korrespondenten
Zeitraum
01.01.1940
31.12.1943
13 Briefe
Älteste ↑
📋 Alle Briefe Ottos · 1940–1943 · Älteste zuerst
19403 Briefe
Erster Brief aus dem Krieg
3. Sept. 1940·Maria Weber
Brief über die Zustände
15. Nov. 1940·Maria Weber
19414 Briefe
Marias Antwort — Weihnachten
22. Dez. 1941·Maria Weber
When a date range is set, the hint bar updates to summarise active filters. Count in row 2 turns navy and updates live. Both date inputs show navy border when set.
3 Bilateral mode — both persons selected
Desktop ≥768px Both selected
/korrespondenz?senderId=otto-id&receiverId=maria-id
DokumentePersonenKorrespondenz
Person
OF
Otto Familienname
Korrespondent
MW
Maria Weber
Zeitraum
Von…
Bis…
32 Briefe
Neueste ↓
28 von Otto → 4 von Maria ←
19433 Briefe
Brief an Maria — letzte Nachrichten
3. Sept. 1943·Prag
Marias Antwort — September
10. Sept. 1943·Wien
Abschiedsbrief
28. Sept. 1943·Ostfront
19427 Briefe
Brief aus dem Feld
14. März 1942·Frankreich
Neuigkeiten aus München
22. Jan. 1942
Weihnachtsbrief von Maria
20. Dez. 1942·Wien
Gedanken zu Weihnachten
24. Dez. 1942
Asymmetry bar only appears in bilateral mode — hidden in single-person mode. Location shown in meta when present. Hover reveals "›" action chevron per row. Sender/recipient omitted from meta in bilateral mode (implicit).
Mobile 375px Both selected
09:41
OF
Otto F.
MW
Maria W.
Von…
Bis…
32 Br.
Neu ↓
28 von Otto →4 von Maria ←
19433
Brief — letzte Nachrichten
3. Sept. 1943
Marias Antwort
10. Sept. 1943
Abschiedsbrief
28. Sept. 1943
19427
Brief aus dem Feld
14. März 1942
Mobile: persons side-by-side (condensed names) with swap between. Row 2 compressed. Each log row ≥44px touch target. Full card tap → document detail.
4 Bilateral + date filter active
Date range set Filtered bilateral
/korrespondenz?senderId=otto-id&receiverId=maria-id&from=1921-01-01&to=1935-12-31&dir=ASC
DokumentePersonenKorrespondenz
Person
OF
Otto Familienname
Korrespondent
MW
Maria Weber
Zeitraum
01.01.1921
31.12.1935
20 Briefe
Älteste ↑
18 von Otto →2 von Maria ←
19218 Briefe
Brief an Maria — Reise nach Wien
12. März 1921·Wien
Nachricht zum Geburtstag
4. April 1921
Marias Antwort auf Geburtstag
10. April 1921·Wien
Bericht aus dem Büro
22. Mai 1921
192212 Briefe
Neujahrsgrüße 1922
2. Jan. 1922
Date inputs show the active range. Count updates to the filtered set (20, not 32). Sort label changes to "Älteste ↑". Asymmetry bar recalculates for the visible set.
Status dots — colour legend
  • PLACEHOLDER — Platzhalter, keine Datei
  • UPLOADED — Datei vorhanden
  • TRANSCRIBED — Transkribiert
  • REVIEWED — Überprüft
  • ARCHIVED — Archiviert
Direction border colours
  • Sent (→) — Navy #002850
  • Received (←) — Mint #A6DAD8
Row 2 behaviour rules
  • Always rendered in the DOM
  • Dimmed (opacity: 0.4) when no person is selected
  • Fully active as soon as any person is set
  • Count updates on every filter change via documents.length
  • Sort button: click toggles dir param between ASC ↔ DESC
  • Date inputs: type or date picker; ISO sent in URL, German displayed
Asymmetry bar — when shown
  • Only rendered when both senderId and receiverId are set
  • Calculates from documents array: filter(d => d.sender?.id === senderId).length
  • Hidden in single-person mode
Implementation Reference — Correspondence Log Real values · mockup above is ~55% scale
ElementTailwind classesReal sizeNotes
Log container border border-line rounded-sm overflow-hidden bg-surface White card wrapping year bands + rows
Year band (.LOG-YEAR) flex items-baseline gap-3 px-4 py-2 bg-muted border-b border-line border-t border-t-line-2 h ~40px, py 8px, px 16px First year band: omit border-t
Year number text-2xl font-bold text-ink leading-none 24px / 700 This is the most commonly undersized element — 24px minimum
Year letter count text-xs text-ink-3 font-medium 12px "5 Briefe" — always visible next to year
Log row (.LOG-ROW) flex items-start gap-3 px-4 py-3 border-b border-line last:border-b-0 hover:bg-muted transition-colors cursor-pointer min-h-[44px] border-l-2 min 44px tall, py 12px, px 16px Sent: border-l-primary. Received: border-l-accent
Direction arrow text-sm font-black shrink-0 pt-0.5 w-4 text-center 14px / 900, 16px wide Sent: text-primary "→". Received: text-[#0F5755] "←"
Row body flex-1 min-w-0 Flex column inside: title + meta
Document title text-sm font-semibold text-ink leading-snug truncate 14px / 600 Single line, truncate with ellipsis on overflow
Meta line (date · sender · status) text-xs text-ink-2 mt-0.5 flex items-center gap-1.5 12px Separator: text-line. Status dot: w-2 h-2 rounded-full shrink-0
Row action chevron shrink-0 w-6 h-6 flex items-center justify-center border border-line bg-muted text-ink-3 opacity-0 group-hover:opacity-100 transition-opacity 24×24px Add group to LOG-ROW; chevron fades in on hover
Asymmetry bar container px-4 sm:px-6 py-2 bg-canvas border-b border-line py 8px Only rendered when both senderId and receiverId are set
Asymmetry bar track h-1.5 rounded-full bg-line overflow-hidden flex mt-1 6px tall Navy fill: bg-primary. Mint fill: bg-accent
Single-person hint bar px-4 sm:px-6 py-2 bg-amber-50 border-b border-amber-200 text-xs font-medium text-amber-800 flex items-center gap-2 h ~36px, 12px text Amber warning tone. Only when senderId set but no receiverId.

Implementation notes

ConversationFilterBar.svelte

  • Replace the large p-8 card with a two-row strip
  • Row 1: flex row, border-bottom: 1px solid #EAE7E0, bg-white
  • Row 2: flex row, bg-surface (#F7F5F2), border-bottom: 1.5px solid #E0DDD6
  • Row 2 always mounted; opacity-40 pointer-events-none when no person set
  • Receiver field: remove required validation, add dashed border style when empty
  • Suggestion dropdown: populate from restrictToCorrespondentsOf results when field focused + person A set
  • Add "Alle Korrespondenten" row at bottom of dropdown as explicit opt-out
  • Sort: click toggles dir via ontoggleSort — keep existing handler
  • Count: derived from documents.length passed as prop

ConversationTimeline.svelte

  • Remove: central line div, chat bubble markup, left/right justify logic
  • Replace with: LOG container, LOG-YEAR bands, LOG-ROW cards
  • Direction: isOut = doc.sender?.id === senderId → navy border + "→" arrow
  • In bilateral mode: hide sender/recipient in meta (implicit). In single-person mode: show other party name
  • Asymmetry bar: new component or inline block, only when senderId && receiverId
  • Summary bar: keep data-testid="conv-summary" but move count to Row 2 of filter strip
  • Keep canWrite new-document link — move to bottom of log list
  • Location in meta: show when doc.location is set
  • Status dot: map doc.status to dot colour (5 states)

+page.svelte / +page.server.ts

  • Remove hard gate: {#if !senderId || !receiverId} block replaced with empty-state component + single-person mode support
  • Single-person API: when only senderId set, call GET /api/documents filtered by sender (or extend /api/documents/conversation to allow null receiver)
  • Recent persons: store last 3 visited person IDs in localStorage, resolve names on mount
  • Hint bar: show when senderId && !receiverId
  • Page title: m.conv_heading() → update i18n key value to "Korrespondenz"
  • Nav label: update +layout.svelte nav link text key
  • Existing spec tests in page.svelte.spec.ts: update selectors for new log markup, add single-person mode tests