Person Detail — Korrespondenz-Überblick · Tiered Edition
Redesign of /persons/[id]. The first spec assumed the Walter de Gruyter case (851 letters · 42 years · 87 correspondents) was the default layout — it is not. In this archive, ~90 % of persons have ≤ 5 letters, and only 4 persons exceed 100. This edition inverts the polarity: one shared dashboard block (navy header + 4-cell stats strip) is the baseline at every tier. Compact ends there and covers ~90 % of pages; Standard and Rich append progressive sections below the stats strip only when data density earns them.
REV 2 · FINAL
Tier thresholds
Compact ≤ 5 · Standard 6–49 · Rich ≥ 50
Visual system
One dashboard block · shared navy header + stats strip at every tier
Progressive sections
Standard adds direction + top list · Rich adds histogram + locations + cloud
Rich-only blocks
Activity histogram · tag cloud (eager) → lazy-loaded
What changed from REV 1. REV 1 shipped a six-block dashboard on every person page. REV 1's "Sparse" state (letterCount < 10) was treated as the edge case; in reality it is the archive's baseline. REV 2 makes Compact the default, drops the histogram and tag cloud from the Standard tier, and gates the original six-block treatment behind letterCount ≥ 50. Backend aggregations for the heavy blocks are skipped entirely at Compact and Standard tiers.
Reading this spec. Mockups in Sections 03–05 are scaled to ~55 % of real pixel values so that multiple viewports fit on one page. Never copy pixel sizes from the mockups. Use the impl-ref tables for exact Tailwind classes and real pixel values.
Inhalt
- 01 Data reality & design response why we rebuilt this
- 02 Tier decision logic thresholds · what each tier shows
- 03 Compact tier — the 90 % case 3 viewports
- 04 Standard tier — middle volume 3 viewports
- 05 Rich tier — archival giants 3 viewports · 4 persons qualify
- 06 Empty & loading states new person · in-flight
- 07 Accessibility contract WCAG AA/AAA
- 08 Implementation notes tiered API · lazy components · shipping order
01Data Reality — Why We Rebuilt This
A good dashboard respects the shape of the archive it describes. As of 2026-04-23 the distribution of letters per person in the Familienarchiv looks like this:
| Tier | Criterion | Share | Example | What it needs |
| Empty |
letterCount == 0 |
— |
Newly imported person · no documents yet |
Reassurance card. Charts of nothing are noise. |
| Compact |
1 ≤ letterCount ≤ 5 |
~ 90 % |
Elsbeth Brandt · 3 letters · 1919–1922 |
A single stat line. The sent/received lists below already answer "which letters?". |
| Standard |
6 ≤ letterCount ≤ 49 |
~ 6 % |
Herbert Cram · 18 letters · 8 correspondents |
Stats strip · direction bar · top‑5 correspondents tile list. |
| Rich |
letterCount ≥ 50 |
< 1 % |
Walter de Gruyter · 851 letters · 87 corresp. · 42 years |
Everything in Standard plus activity histogram · top locations · tag cloud. |
The cost of REV 1 and the savings of REV 2.
- REV 1 planned six eager components on every person page — stats strip, activity histogram, direction split, top correspondents, top locations, tag cloud.
- For the 90 % of persons with ≤ 5 letters, four of those blocks were either hidden or collapsed to placeholders. Build cost went to invisible UI.
- Backend aggregations for the heavy blocks (
activityByYear, topLocations, topTags) ran for every person page load regardless of relevance.
- REV 2 ships three tiers: Compact is trivial to build and serves 90 %, Standard serves the middle, Rich is the only tier that earns the original six-block treatment.
02Tier Decision Logic
The tier is derived from one number: letterCount = outCount + inCount. The backend uses the same rule to decide which aggregations to compute, so expensive queries (per-year histogram, tag tallies) are skipped for ~95 % of persons.
Tier thresholdsserver + client use the same rule
| letterCount | Tier | Blocks shown | Expensive queries? |
| 0 | EMPTY | Reassurance card only | No |
| 1 – 5 | COMPACT | Shared dashboard chrome: header + CTA + 4-cell stats strip | No |
| 6 – 49 | STANDARD | Stats strip · direction bar · top correspondents (≤ 5) | Partial: top correspondents only |
| ≥ 50 | RICH | All of Standard · activity histogram · top locations · tag cloud | Yes: full set |
Why 5 and why 50?
- 5 is the point below which a histogram has fewer bars than x-axis ticks, a "top list" is just the list, and a tag cloud shows ≤ 3 tags total. Below 5 letters, the sent/received document lists immediately below the stats strip are the per-letter detail view.
- 50 is the point at which per-year density begins to average ≥ 1 letter / year across a ~40-year lifespan — the histogram finally reveals shape rather than scattered dots. It is also the threshold at which "top locations" holds more than 2–3 entries and a tag cloud distinguishes sizes meaningfully.
- Between 5 and 50 the data is too thin for histograms but thick enough that raw document lists become hard to scan — that is what Standard's stats strip and top-correspondents tile solve.
03Compact Tier Compact · ~90 %
The shared dashboard chrome is the anchor of every non-Empty tier — navy header with the title and "↗ Briefwechsel öffnen" CTA, followed by the 4-cell stats strip. At Compact, that's where the dashboard ends. No other sections, no extra widgets. The sent & received PersonDocumentList components below are unchanged and carry the per-letter detail.
01Elsbeth Brandt · 3 Briefe · 1919 – 1922 Compact
Three total letters — two sent, one received, across a 4-year span. The dashboard renders header + stats and stops. The four stat cells (gesamt · ausgehend · eingehend · Jahre) are the same four cells shown at Standard and Rich — exactly the same markup, just without the sections that would render below when the data warrants.
320 px · Mobile176 px @ 55%
← Zurück
EB
Elsbeth Brandt
1890 – 1963
Gesendet · 2
1919Brief an W.
1922Brief an H.
Empfangen · 1
1920Antwort
768 px · Tablet422 px @ 55%
Familienarchiv
PersonenBriefwechsel
← Zurück
EB
Elsbeth Brandt
1890 – 1963
Gesendet · 2Empfangen · 1
26.03.1919An Walter de Gruyter — Genesungswünsche
14.08.1922An Herbert Cram — Einladung
02.11.1920Von Walter de Gruyter — Dank
1440 px · Desktop780 px @ 55%
familienarchiv.de/persons/elsbeth-…
Familienarchiv
DokumentePersonenBriefwechselChronik
← Zurück
EB
Elsbeth Brandt
1890 – 1963
Gesendet · 2 Briefe
26.03.1919An Walter de Gruyter — Genesungswünsche nach Lazarettaufenthalt
14.08.1922An Herbert Cram — Einladung zur Sommerreise nach Bad Kissingen
Empfangen · 1 Brief
02.11.1920Von Walter de Gruyter — Dank für Geburtstagsgrüße
Why not more sections? At 3 letters, a histogram would be 3 dots on a 4-year axis — decorative, not informative. A top-correspondents list would be 2 rows with no proportional meaning. A tag cloud would show at most 2 tags. The document rows below already surface every atom of per-letter data; adding derived views on top of 3 data points wastes vertical space and introduces noise.
Design invariant. The dashboard block is the same component across Compact, Standard, and Rich — same chrome, same stats strip as the always-rendered baseline. Higher tiers append more sections below the stats. This is the system's consistency contract — you never look at two person pages and see two fundamentally different dashboards, only the same dashboard with more or fewer sections.
Shared dashboard chromerenders at every non-Empty tier · the Compact tier renders only this
| Part | Classes | Real | Note |
| Container | overflow-hidden rounded-sm border border-line bg-surface shadow-sm | 1 px border | Matches the card pattern from CLAUDE.md |
| Header strip | flex items-center justify-between gap-3 bg-primary text-primary-fg px-5 py-3 | 12 px y padding | Dark navy — the visual anchor that makes this block recognisable at every tier |
| Title | font-serif text-base font-bold · text: m.person_dashboard_title() → "Korrespondenz-Überblick" | 16 px / 700 | Merriweather. Abbreviates to "Überblick" at < 640 px |
| CTA "↗ Briefwechsel öffnen" | bg-accent text-primary text-xs font-extrabold uppercase tracking-wide px-3 py-1.5 rounded-sm min-h-[44px] min-w-[44px] inline-flex items-center gap-1.5 | 44 px min | WCAG 2.2 touch target. Mobile shows "↗" only; tablet+ shows the full label |
| Stats strip container | grid grid-cols-2 sm:grid-cols-4 gap-px bg-line | 1 px gap | Separators are the parent background showing through |
| Stat cell | bg-muted px-4 py-3.5 text-center | 14 px y padding | Uses bg-muted (not bg-surface) so the gap-px separators read |
| Stat number | font-serif text-[22px] font-black leading-none tabular-nums tracking-tight | 22 px / 900 | Merriweather Black · .out = primary, .in = accent-fg |
| Stat label | mt-1 text-[10px] font-bold uppercase tracking-wide text-ink-3 | 10 px | Responsive abbreviation: "Briefe gesamt" → "gesamt" → "ges." via Paraglide _short keys |
| Semantic wrapper | <dl> with <dt>/<dd> pairs | — | Screen readers announce "Briefe gesamt: 3, ausgehend: 2, eingehend: 1, Jahre: 4" |
04Standard Tier Standard · 6–49
For persons with meaningful volume but not enough density for charts. The dashboard grows a header strip, a 4-cell stats block, a proportional direction bar, and a top-correspondents tile list — all server-computed and cheap. The histogram, locations, and tag cloud stay off. The top-correspondents list replaces the legacy CoCorrespondentsList in this tier.
02Herbert Cram · 18 Briefe · 1905 – 1934 Standard
Eighteen letters across 30 years, 8 distinct correspondents. The stats strip gives exact counts; the direction bar shows the letter balance at a glance; the top-5 list reveals who Herbert wrote to or received from most often. No year-level histogram yet — density is still too thin for a readable chart.
320 px · Mobile176 px @ 55%
← Zurück
HC
Herbert Cram
1881 – 1967
Top Korrespondenten
W. de Gruyter6
E. Brandt4
G. Rofden3
768 px · Tablet422 px @ 55%
Familienarchiv
PersonenBriefwechsel
← Zurück
HC
Herbert Cram
1881 – 1967
Richtungsverteilung
→ 11 · 61%← 7 · 39%
Top Korrespondenten 5 von 8
Walter de Gruyter6
Elsbeth Brandt4
Gertrud Rofden3
Eugenie de Gruyter2
Käthe Dieckmann1
1440 px · Desktop780 px @ 55%
familienarchiv.de/persons/herbert-cram-…
Familienarchiv
DokumentePersonenBriefwechselChronik
← Zurück
HC
Herbert Cram
1881 – 1967
Richtungsverteilung
→ 11 ausgehend · 61%← 7 eingehend · 39%
Top Korrespondenten 5 von 8 · Zeitraum 1905 – 1934
Walter de Gruyter6
Elsbeth Brandt4
Gertrud von Rofden3
Eugenie de Gruyter2
Käthe Dieckmann1
Why no histogram at this tier? With 18 letters over 30 years, a histogram averages 0.6 letters/year. Most bars would be empty or 1-tall — noise around a few spikes. Users glean nothing they don't already read in the "30 Jahre" stat. Histograms are a shape-reading tool; they need density to reveal shape.
05Rich Tier Rich · ≥ 50
Reserved for the archival giants — currently the four persons with correspondence volumes above 50 letters (Walter de Gruyter, Walter Dieckmann, Herbert Cram's extended record, Ella Dieckmann). Here the full six-block dashboard earns its place: histogram reveals decade-scale activity, top-locations surface travel patterns, tag cloud shows recurring themes. This is the only tier that warrants the heavy lift.
03Walter de Gruyter · 851 Briefe · 1898 – 1940 · 87 Korrespondenten Rich
The archival giant. All blocks render. The histogram has one bar per year (1898 → 1940 = 43 bars); peak bar highlighted in primary. Top correspondents & top locations sit in a 2-column grid. Tag cloud shows frequency-sized chips, with muted tags for counts < 5. Every tile is a deep link into /briefwechsel with filters pre-applied.
320 px · Mobile176 px @ 55%
← Zurück
WG
Walter de Gruyter
1862 – 1923
Top
W. Dieckmann184
H. Cram143
E. Dieckmann88
Schlagwörter
VerlagFamilieGeburtstagKur
768 px · Tablet422 px @ 55%
…/persons/walter-de-gruyter
Familienarchiv
PersonenBriefwechsel
← Zurück
WG
Walter de Gruyter
1862 – 1923
Aktivität 1922 · 78
189819221940
Richtung
→ 612 · 72%← 239 · 28%
Top Korresp.
W. Dieckmann184
H. Cram143
E. Dieckmann88
E. de Gruyter77
Top Orte
Berlin412
B.Lichterfelde180
Bad Kissingen58
Cöln37
Schlagwörter
VerlagFamilieGeburtstagWeihnachtenKurReiseKriegKrankheitTod
1440 px · Desktop780 px @ 55%
familienarchiv.de/persons/walter-de-gruyter-…
Familienarchiv
DokumentePersonenBriefwechselChronik
← Zurück
WG
Walter de Gruyter
1862 – 1923
Aktivität über die Jahre Spitzenjahr 1922 · 78 Briefe
189819221940
Richtungsverteilung
→ 612 ausgehend · 72%← 239 eingehend · 28%
Top Korrespondenten 6 von 87
Walter Dieckmann184
Herbert Cram143
Ella Dieckmann88
Eugenie de Gruyter77
Gertrud von Rofden58
Top Orte 5 von 42
Berlin412
B.Lichterfelde180
Bad Kissingen58
Cöln37
Belgard26
Beliebte Schlagwörter Klick filtert den Briefwechsel
VerlagFamilieGeburtstagWeihnachtenKuraufenthaltReiseGeschäftKriegKrankheitSchuleHochzeitNeujahr
Deep-link grammar (unchanged from REV 1). Every Rich-tier tile is a link into /briefwechsel with filters applied: histogram bar → ?senderId={id}&from={y}-01-01&to={y}-12-31 · correspondent row → ?senderId={id}&receiverId={other} · location row → ?senderId={id}&location={slug} · tag chip → ?senderId={id}&tagId={tagId}. Requires new direction, location, tagId query params on /briefwechsel.
06Empty & Loading States
New persons (imported but no documents yet) render the Empty state. The Loading state covers the brief window while the dashboard endpoint resolves — person card is already painted from /api/persons/{id} (fast), dashboard slot shows a single skeleton shape sized for the likely tier.
04Empty — person has no letters yet Empty
Single reassurance card. No stats strip (there are no stats). No CTA to the Briefwechsel (it would lead to an empty view). "Bearbeiten" remains on the PersonCard.
320 px · Mobile176 px @ 55%
← Zurück
Überblick
Noch keine Briefe
Sobald ein Brief zugewiesen wird, erscheint der Überblick hier.
768 px · Tablet422 px @ 55%
← Zurück
Korrespondenz-Überblick
Noch keine Briefe
Diese Person hat noch keine Korrespondenz im Archiv. Sobald ein Brief als Absender oder Empfänger zugewiesen wird, erscheint der Überblick hier automatisch.
1440 px · Desktop780 px @ 55%
familienarchiv.de/persons/new-id
Familienarchiv
PersonenBriefwechsel
← Zurück
Korrespondenz-Überblick
Noch keine Briefe
Diese Person hat noch keine Korrespondenz im Archiv. Sobald ein Brief als Absender oder Empfänger zugewiesen wird, erscheint der Überblick hier automatisch.
05Loading — skeleton sized for the expected tier
Dashboard endpoint in flight. Skeleton shows only the bare silhouette — a single rectangle at Compact, a stats+list silhouette at Standard+. Reduced-motion users see a static gradient instead of the shimmer animation.
320 px · Mobile176 px @ 55%
← Zurück
WG
Walter de Gruyter
1862 – 1923
768 px · Tablet422 px @ 55%
← Zurück
WG
Walter de Gruyter
1862 – 1923
1440 px · Desktop780 px @ 55%
familienarchiv.de/persons/…
← Zurück
WG
Walter de Gruyter
1862 – 1923
Anti-flicker rule. If the letterCount from /api/persons/{id} (fast) arrives before /api/persons/{id}/dashboard, the skeleton already knows which tier to paint — so the skeleton shape matches the final rendered shape and the page does not jump when data arrives. Use the count hint eagerly.
07Accessibility Contract · WCAG AA/AAA
Every rendered colour pair has been measured in light and dark mode. Semantics are chosen per tier: Compact uses a <dl> stat line; Standard/Rich reuse that and add <ol> for tile lists; Rich adds role="img" on the histogram with a text alternative that carries the shape information.
Light Mode — Contrast Verificationlayout.css tokens
| Pair | Value | Ratio | WCAG |
| Stat number (primary on muted) | #002850 on #fafaf5 | 14.0:1 | AAA ✓ |
| Stat label (ink-3 on muted) | #666666 on #fafaf5 | 5.4:1 | AA ✓ |
| Stat "in" (accent on muted, 22 px/900) | #2F9E95 on #fafaf5 | 4.5:1 | AA ✓ (large text rule) |
| Dashboard header (surface on primary) | #ffffff on #002850 | 14.5:1 | AAA ✓ |
| CTA "Briefwechsel öffnen" (primary on accent) | #002850 on #a6dad8 | 8.1:1 | AAA ✓ |
| Histogram peak bar (primary on surface) | #002850 on #ffffff | 14.5:1 | AAA ✓ |
| Tag chip (primary on accent) | #002850 on #a6dad8 | 8.1:1 | AAA ✓ |
| Muted tag (ink-3 on line) | #666666 on #eee8dc | 5.1:1 | AA ✓ |
| Focus ring (primary on surface, 2 px offset) | #002850 | 14.5:1 | AAA ✓ |
Dark Mode — Contrast Verificationdata-theme="dark"
| Pair | Value | Ratio | WCAG |
| Stat number (ink on canvas-2) | #f0efe9 on #011526 | 15.1:1 | AAA ✓ |
| Stat "out" (mint on canvas-2) | #a1dcd8 on #011526 | 9.6:1 | AAA ✓ |
| Stat "in" (turquoise on canvas-2) | #00c7b1 on #011526 | 6.8:1 | AA ✓ |
| Histogram peak (mint on canvas) | #a1dcd8 on #010e1e | 9.2:1 | AAA ✓ |
| Tag (turquoise on tint) | #00c7b1 on rgba(0,199,177,.2) | 6.3:1 | AA ✓ |
Non-negotiable accessibility rules per tier.
- All non-Empty tiers (shared chrome). Stats strip is a real
<dl> — screen readers announce "Briefe gesamt: 3, ausgehend: 2, eingehend: 1, Jahre: 4". The header CTA has an aria-label that includes the person's name ("Briefwechsel von Elsbeth Brandt öffnen").
- Compact. Nothing renders below the stats strip. No additional semantic concerns beyond the shared chrome.
- Standard. Adds direction bar as
role="img" with aria-label="11 von 18 Briefen ausgehend, 7 eingehend". Top correspondents are a semantic <ol> — "Top Korrespondenten, 5 Einträge, 1 von 5 Walter de Gruyter, 6 Briefe".
- Rich. Adds histogram as
role="img" with aria-label describing range & peak: "Aktivität über 42 Jahre, Spitzenjahr 1922 mit 78 Briefen". Tag cloud is a <ul> of <li><a>; the visual size does not encode meaning beyond the text — count is exposed via title + aria-label.
- All tiers. Focus ring:
focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2. Touch targets: CTA 44 px, top-list row 44 px on mobile / 32 px on desktop, tag chip 44 px min width, histogram bar 32 px invisible click target.
- Reduced motion. Skeleton shimmer collapses to a static gradient; tag hover lift is disabled; histogram bar fade-in on first render is skipped. Honor
@media (prefers-reduced-motion: reduce).
08Implementation Notes — Tiered API, Lazy Components, Shipping Order
Endpointtier-aware response
| Field | Value | Note |
| Route | GET /api/persons/{id}/dashboard | Separate from /api/persons/{id} — lets person entity load stay lean and the dashboard query stay cache-friendly |
| Permission | @RequirePermission(Permission.READ_ALL) | Same as /api/persons/{id} |
| Cache key | (personId, dataVersion) | Invalidated on any Document write that touches this person (sender or receiver change, tag change, location change) |
| Expensive aggregations | Only run when computed tier == RICH | activityByYear, topLocations, topTags are null at lower tiers — DB query planner skips those subqueries entirely |
Response schemaPersonDashboardDTO — nullable fields by tier
| Field | Type | Populated at tier |
tier | "EMPTY" | "COMPACT" | "STANDARD" | "RICH" | always · advisory hint; frontend may re-derive |
totalCount | int | always |
outCount | int | always |
inCount | int | always |
firstLetterYear | int? | Compact+ (null when totalCount == 0) |
lastLetterYear | int? | Compact+ |
yearSpan | int? | Compact+ · last - first + 1 |
correspondentCount | int? | Standard+ |
topCorrespondents | List<CorrespondentTileDTO>? | Standard+ · max 5 |
activityByYear | Map<int, int>? | Rich only · contiguous from first to last year |
peakYear / peakYearCount | int? · int? | Rich only |
topLocations | List<LocationTileDTO>? | Rich only · max 5 |
topTags | List<TagTileDTO>? | Rich only · max 12 · minimum count threshold 3 |
Component structurebaseline eager · Rich-only lazy
| File | Tier | Load | Responsibility |
PersonDashboard.svelte | all | eager | Orchestrator — renders header + stats always, then conditionally appends sections based on tier |
DashboardHeader.svelte | Compact, Standard, Rich | eager | Navy strip: title + "↗ Briefwechsel öffnen" CTA · shared across all non-Empty tiers |
StatStrip.svelte | Compact, Standard, Rich | eager | 4-cell stats grid with direction colouring · shared across all non-Empty tiers |
DistributionBar.svelte | Standard, Rich | eager | Shared with briefwechsel-thumbnail-rows-spec — not new |
TopTileList.svelte | Standard, Rich | eager | Ordered list of tiles (name + bar + count) · generic (used for correspondents and locations) |
ActivityHistogram.svelte | Rich only | lazy | Dynamic import: {#await import('./ActivityHistogram.svelte')} |
TagCloud.svelte | Rich only | lazy | Dynamic import; size buckets derived from counts |
EmptyNotice.svelte | Empty | eager | Single card with reassurance text |
Route changes/persons/[id] shell
| File | Change |
+page.server.ts | Add parallel call to /api/persons/{id}/dashboard. Keep existing error handling pattern (result.response.ok check · getErrorMessage(code)). |
+page.svelte | Replace <CoCorrespondentsList> with <PersonDashboard dashboard={data.dashboard} />. Remove the local coCorrespondents derivation — backend owns it. |
CoCorrespondentsList.svelte | Keep temporarily (used nowhere else after this ships). Delete in a follow-up commit once the new route is green in QA. |
Shipping order.
- Phase 1 — backend endpoint
GET /api/persons/{id}/dashboard returning the tiered PersonDashboardDTO. Tests for Empty / Compact / Standard / Rich. Cache keyed on person data-version. Skip expensive aggregations for non-Rich persons.
- Phase 2 —
PersonDashboard.svelte + DashboardHeader.svelte + StatStrip.svelte + EmptyNotice.svelte. Replaces CoCorrespondentsList in the right column. This phase alone ships the Compact tier — value for ~90 % of persons.
- Phase 3 —
TopTileList.svelte + reuse DistributionBar.svelte. Standard tier's direction bar and top-correspondents tiles go live for ~6 % of persons.
- Phase 4 —
ActivityHistogram.svelte, TagCloud.svelte (both lazy). Wire new direction, location, tagId query params on /briefwechsel. Rich tier goes live for the 4 power-correspondence persons.
- Phase 5 — axe-playwright checks at 320 / 768 / 1440 in light + dark for each tier. Visual regression snapshots for all five states (Empty · Compact · Standard · Rich · Loading).
- Phase 6 (cleanup) — delete
CoCorrespondentsList.svelte and any now-unused co-correspondent derivation helpers.
Migration story. The old spec's PersonDashboard (six eager blocks) has not shipped yet — there is no user-facing migration. The existing CoCorrespondentsList is what each tier replaces. Compact and Standard tiers are strict improvements over CoCorrespondentsList; Rich tier is the only place the original six-block vision survives, and it only renders for persons who demonstrably have the data to fill it.