# 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
FAMILIENARCHIV 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
FAMILIENARCHIV☰
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

Element | Tailwind classes | Real size | Notes
--- | --- | --- | ---
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
FAMILIENARCHIVDokumentePersonenKorrespondenz
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
FAMILIENARCHIVDokumentePersonenKorrespondenz
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
FAMILIENARCHIVDokumentePersonenKorrespondenz
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
FAMILIENARCHIVDokumentePersonenKorrespondenz
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
›
Neues Dokument in dieser Korrespondenz
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
FAMILIENARCHIV☰
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
FAMILIENARCHIVDokumentePersonenKorrespondenz
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

Element | Tailwind classes | Real size | Notes
--- | --- | --- | ---
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