diff --git a/docs/specs/briefwechsel-thumbnail-rows-spec.html b/docs/specs/briefwechsel-thumbnail-rows-spec.html new file mode 100644 index 00000000..00d79449 --- /dev/null +++ b/docs/specs/briefwechsel-thumbnail-rows-spec.html @@ -0,0 +1,1073 @@ + + + + + +Briefwechsel — Thumbnail Rows · Final Design Spec · Familienarchiv + + + +
+ + +
+
+
+

Briefwechsel — Thumbnail Rows

+

Final row design for /briefwechsel. PDF thumbnail anchors each row; summary reads as a quote; no status lifecycle, no script-type indicator. Designed for fun discovery, not dense scanning. Scales from 320 px mobile to 1440 px desktop, light and dark. Serves both the millennial audience (25–42) and the senior family audience (60 +) — the senior constraint drives touch targets, line height, and summary legibility.

+
+
FINAL
+
+
+
Route
/briefwechsel · list surface
+
Row height (desktop)
128 px · comfortable
+
Thumbnail
82×106 portrait · 104×72 landscape
+
Removed
status dot · script type · archive box
+
+
+ +
+ Reading this spec. Mockups in Section 02 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 class + pixel value. Close-ups in Section 03 are rendered at ~100 % scale for pixel-accurate reference. +
+ + +
+
Inhalt
+
    +
  1. 01 Page anatomy default · 1440 px
  2. +
  3. 02 Content states × 3 viewports 5 states · 15 frames
  4. +
  5. 03 Row anatomy close-ups 4 row types @ real size
  6. +
  7. 04 Distribution bar bilateral mode only
  8. +
  9. 05 Accessibility contract WCAG AA/AAA
  10. +
  11. 06 Implementation notes data · thumbnails · routing
  12. +
+
+ + +
+

01Page Anatomy — Default State at 1440 px (single-person)

+

The page is a single vertical column (max-w-7xl). Filter card sticks to the top of the content region; the row list starts immediately below, grouped by year dividers. All viewports render the same regions in the same order — they only adapt spacing and thumbnail size, never rearrange.

+ +
+
+
+
familienarchiv.de/briefwechsel?senderId=…
+
+
+ +
DokumentePersonenBriefwechselChronik
+
+
+ +
+
+
Person
Walter de Gruyter
+
Korrespondent — optional
Alle Korrespondenten
+
+
+
Newest ↓
+
▾ Filter
+
851 Briefe
+
+
📋 Alle Briefe von Walter de Gruyter — wähle einen Korrespondenten oben um einzugrenzen
+
+ +
19401 Brief
+ +
+
+
+
+
Demo leserlicher Brief
+
letzte Lebenstage von W. Dörpfeld in Griechenland — ausführlicher Bericht aus Belgard
+
← eingehendGertrud von Rofden·📍 Belgard·DörpfeldGriechenland
+
+
31. Mai 1940
vor 85 Jahren
+
+
+ +
19235 Briefe
+
+
+
+
+
W-0397 – 2. September 1923 – B.Lichterfelde
+
von Elsbeth geschriebener Kommentar, den Herbert zum Brief erzählte
+
→ ausgehendan Herbert Cram·📍 B.Lichterfelde·VerlagFamilie
+
+
2. September 1923
vor 102 Jahren
+
+
+
+
+
Ansichtskarte – 2. September 1923 – B.Lichterfelde
+
kurze Grüße aus B.Lichterfelde, Hinweis auf den kommenden Besuch
+
→ ausgehendan Herbert Cram·📍 B.Lichterfelde·✉ Postkarte
+
+
2. September 1923
vor 102 Jahren
+
+
+
4 S.
+
+
W-0524 – 31. Juli 1923 – Berlin
+
Glückwunsch zum 60. Geburtstag, Bericht über den Verlag und den Umzug
+
→ ausgehendan Walter Dieckmann·📍 Berlin·Geburtstag
+
+
31. Juli 1923
vor 102 Jahren
+
+
+
+
+
+
+
+
A · Filter card
Two inputs (person required, correspondent optional) + action row + hint bar. Uses bg-surface wrapper, not a card — the hint bar gives it closure.
+
B · Year divider
Sticky-looking band between year groups. Large navy numeral + brief count. Uses bg-muted and a 1 px rule above/below.
+
C · Row list
Single <ul> per year group. Each row is an <a> with role="listitem" ancestor. Border-left accent colors direction: navy = outgoing, mint-darker = incoming.
+
Row · Thumbnail cell
Fixed 104 × 120 px cell on desktop; portrait and landscape both centered in the same cell so row height stays consistent across mixed media.
+
Row · Body
Serif title · italic serif summary (with mint quote glyphs) · sans meta line with direction + counterpart + location + tags. Summary omitted entirely when empty.
+
Row · Right column
Date (serif, bold) + relative age ("vor 102 Jahren"). No status, no archive location — deliberately calm.
+
+
+ +
+
Implementation Reference — Page ShellTailwind 4 · tokens from layout.css
+ + + + + + + + + + + + +
ElementClassesRealNote
Page containermx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8max 80remMatches production /briefwechsel
Filter card wrappermb-8 rounded-sm border border-line bg-surface p-6 shadow-smpadding 24 pxExisting CorrespondenzPersonBar container
Year dividerflex items-baseline gap-3 border-y border-line bg-muted px-[14px] py-[8px]border 1 px both sidesKeep production styling — only row changes
Year numeralfont-serif text-2xl font-black tracking-tight text-primary24 px / 900 / -0.025emMerriweather Black
Year counttext-sm font-bold text-ink-314 px / 700"5 Briefe" / Paraglide plural
Row list wrapperoverflow-hidden rounded-sm border border-line bg-surface1 px borderHides row borders at ends
Rowgroup grid grid-cols-[104px_1fr_auto] gap-5 items-center px-5 py-[14px] border-b border-line-2 border-l-[3px] min-h-[128px] cursor-pointer transition-colors hover:bg-muted128 px min · 20 × 14 paddingborder-l-primary out · border-l-accent in
Touch targetFull row is clickable; row height 128 px > WCAG 44 px minimum × ~3128 ≥ 44Senior audience: comfort over density
+
+
+ + +
+

02Content States × 3 Viewports

+

+ Five states covering the combinations that matter. Every frame renders the full page shell (header → filter card → list). Reading order per state: 320 px (mobile S) → 768 px (tablet) → 1440 px (desktop). Watch for filter card wrap at 320, thumbnail shrinkage, and the right-column behaviour under content pressure. +

+ + +
+
01Default · Single person with mixed row types
+
The happy path. Four rows shown: incoming typed letter, outgoing handwritten letter, outgoing postcard (landscape thumbnail), outgoing multi-page letter (page badge). Summaries present on three of four — the fourth row shows the clean no-summary variant.
+
+ +
+ 320 px · Mobile176 px @ 55% +
+
9:41
+
+
Briefwechsel
+
+
Person
Walter de Gruyter
Korrespondent
alle
851
+
19235
+
+
W-0397
Elsbeths Kommentar
H. Cram
2. Sep
+
Ansichtskarte
H. Cram
2. Sep
+
4
W-0524
Geburtstag & Umzug
W. Dieckmann
31. Jul
+
W-0396
H. Cram
2. Sep
+
+
+
+
+
+ +
+ 768 px · Tablet422 px @ 55% +
+
familienarchiv.de/briefwechsel
+
+
DokumenteBriefwechsel
+
+
Person
Walter de Gruyter
Korrespondent — optional
Alle Korrespondenten
Newest ↓
▾ Filter
851 Briefe
📋 Alle Briefe von Walter de Gruyter
+
19235 Briefe
+
+
W-0397 – 2. September 1923
von Elsbeth geschriebener Kommentar, den Herbert zum Brief erzählte
an Herbert CramVerlag
2. Sep 1923
vor 102 J.
+
Ansichtskarte – 2. September 1923
kurze Grüße aus B.Lichterfelde
an Herbert Cram✉ Postkarte
2. Sep 1923
vor 102 J.
+
4 S.
W-0524 – 31. Juli 1923 – Berlin
Glückwunsch zum 60. Geburtstag, Bericht über den Verlag
an Walter DieckmannGeburtstag
31. Jul 1923
vor 102 J.
+
W-0396 – 2. September 1923
an Herbert Cram
2. Sep 1923
vor 102 J.
+
+
+
+
+
+ +
+ 1440 px · Desktop720 px @ 55% +
+
familienarchiv.de/briefwechsel?senderId=…
+
+
DokumentePersonenBriefwechselChronik
+
+
Person
Walter de Gruyter
Korrespondent — optional
Alle Korrespondenten
Newest ↓
▾ Filter
851 Briefe
📋 Alle Briefe von Walter de Gruyter — wähle einen Korrespondenten oben um einzugrenzen
+
19235 Briefe
+
+
W-0397 – 2. September 1923 – B.Lichterfelde
von Elsbeth geschriebener Kommentar, den Herbert zum Brief erzählte
→ ausgehendan Herbert Cram·📍 B.Lichterfelde·VerlagFamilie
2. September 1923
vor 102 Jahren
+
Ansichtskarte – 2. September 1923 – B.Lichterfelde
kurze Grüße aus B.Lichterfelde, Hinweis auf den kommenden Besuch
→ ausgehendan Herbert Cram·📍 B.Lichterfelde·✉ Postkarte
2. September 1923
vor 102 Jahren
+
4 S.
W-0524 – 31. Juli 1923 – Berlin
Glückwunsch zum 60. Geburtstag, Bericht über den Verlag und den Umzug
→ ausgehendan Walter Dieckmann·📍 Berlin·GeburtstagVerlag
31. Juli 1923
vor 102 Jahren
+
W-0396 – 2. September 1923 – B.Lichterfelde
→ ausgehendan Herbert Cram·📍 B.Lichterfelde
2. September 1923
vor 102 Jahren
+
+
+
+
+
+
+
Layout-Beobachtungen. +
    +
  • 320 px: Filter card collapses to a single column. Title truncates with ellipsis (W-0397), summary keeps 1 line max; counterpart shortens to initials+last (H. Cram). Date format is 2. Sep — no year (year dividers provide it).
  • +
  • 768 px: Two-column filter returns. Title shows full label; summary gets 2 lines; date is 2. Sep 1923; location meta omitted (kept to 2 items), tags trimmed to one.
  • +
  • 1440 px: Full meta (direction word, counterpart, location, 2 tags). Relative date appears below the absolute date.
  • +
  • Row 4 (no summary) retains the exact same row height as others — the row grid is min-h-[128px] at desktop so mixed-summary lists don't visually jump.
  • +
+
+
+ + +
+
02Bilateral · Both filters set + distribution bar
+
Sender and receiver both selected. A distribution bar appears above the row list, pattern lifted from production ConversationTimeline. Rows show compact direction glyph instead of the word — the bar above already established direction semantics.
+
+
+ 320 px · Mobile176 px @ 55% +
+
9:41
+
+
Briefwechsel
+
+
Person
Walter
Korrespondent
Herbert
143
+
87 Walter →← 56 Herbert
+
+
W-0397
Elsbeths Kommentar
B.Lichterfelde
2. Sep
+
H-0213
Antwort zur Herbstlieferung
Leipzig
29. Aug
+
Ansichtskarte
Thür. Wald
20. Aug
+
+
+
+
+
+
+ 768 px · Tablet422 px @ 55% +
+
…/briefwechsel?senderId=&receiverId=
+
+
DokumenteBriefwechsel
+
+
Person
Walter de Gruyter
Korrespondent
Herbert Cram
⇄ Tauschen
Newest ↓
▾ Filter
143 Briefe
+
87 von Walter de Gruyter →← 56 von Herbert Cram
+
+
W-0397 – 2. September 1923
von Elsbeth geschriebener Kommentar
Walter an HerbertVerlag
2. Sep 1923
vor 102 J.
+
H-0213 – 29. August 1923 – Leipzig
Antwort auf Walters Anfrage zur Herbstauslieferung
Herbert an WalterVerlag
29. Aug 1923
vor 102 J.
+
Ansichtskarte – 20. August 1923
Urlaubsgruß aus Thüringen
Herbert an Walter✉ Postkarte
20. Aug 1923
vor 102 J.
+
+
+
+
+
+
+ 1440 px · Desktop720 px @ 55% +
+
familienarchiv.de/briefwechsel?senderId=…&receiverId=…
+
+
DokumentePersonenBriefwechselChronik
+
+
Person
Walter de Gruyter
Korrespondent
Herbert Cram
⇄ Tauschen
Newest ↓
▾ Filter
143 Briefe im Zeitraum
+ +
+
W-0397 – 2. September 1923 – B.Lichterfelde
von Elsbeth geschriebener Kommentar, den Herbert zum Brief erzählte
Walter an Herbert·📍 B.Lichterfelde·Verlag
2. September 1923
vor 102 Jahren
+
H-0213 – 29. August 1923 – Leipzig
Antwort auf Walters Anfrage zur Herbstauslieferung
Herbert an Walter·📍 Leipzig·Verlag
29. August 1923
vor 102 Jahren
+
Ansichtskarte – 20. August 1923 – Thüringer Wald
Urlaubsgruß, kurze Notiz über Wetter und geplante Rückkehr
Herbert an Walter·📍 Thüringer Wald·✉ Postkarte
20. August 1923
vor 102 Jahren
+
+
+
+
+
+
+
Distribution bar — only renders when both senderId and receiverId are set. +
    +
  • Labels are right/left-aligned matching the bar direction (out on left, in on right). Bar widths come from backend-calculated counts, not percentages on the client.
  • +
  • role="img" with a descriptive aria-label — screen readers hear the full distribution in one sentence.
  • +
  • Below 320 px: labels stack vertically with a 4 px gap. Never truncate a count.
  • +
  • In meta line, direction word collapses to glyph ("→ / ←") because the distribution bar above has already named the parties.
  • +
+
+
+ + +
+
03Loading · Skeleton (all three viewports render the same pattern)
+
SSR renders without thumbnails. While thumbnails are fetching, show a paper-coloured skeleton in the thumbnail cell. Title, summary and meta remain as normal text (the data is already present). No spinner, no pulse on the text — only the thumbnail shimmers.
+
+
+ 320 px · Mobile176 px @ 55% +
+
9:41
+
+
+
+
Person
Walter
Korresp.
alle
851
+
19235
+
+
W-0397
Elsbeths Kommentar
H. Cram
2. Sep
+
W-0396
H. Cram
2. Sep
+
+
+
+
+
+
+ 768 px · Tablet422 px @ 55% +
+
…/briefwechsel
+
+
+
+
Person
Walter de Gruyter
Korresp.
Alle
Newest ↓
851 Briefe
+
19235 Briefe
+
+
W-0397 – 2. September 1923
Elsbeths Kommentar
Herbert Cram
2. Sep 1923
+
W-0396 – 2. September 1923
Herbert Cram
2. Sep 1923
+
+
+
+
+
+
+ 1440 px · Desktop720 px @ 55% +
+
familienarchiv.de/briefwechsel
+
+
+
+
Person
Walter de Gruyter
Korrespondent
Alle
Newest ↓
▾ Filter
851 Briefe
+
19235 Briefe
+
+
W-0397 – 2. September 1923 – B.Lichterfelde
von Elsbeth geschriebener Kommentar, den Herbert zum Brief erzählte
→ ausgehendan Herbert Cram
2. September 1923
+
W-0396 – 2. September 1923 – B.Lichterfelde
→ ausgehendan Herbert Cram
2. September 1923
+
+
+
+
+
+
+
+ + +
+
04Empty · No results matching current filters
+
Filter combination returns zero letters. Empty card sits below the filter card. Primary use case: date range that excludes all letters. Message gives the user a clear reset path.
+
+
+ 320 px · Mobile176 px @ 55% +
+
9:41
+
+
+
+
Person
Walter
Korresp.
alle
0
+
Keine Briefe
Für diesen Filter gibt es keine Einträge. Zeitraum anpassen oder Filter zurücksetzen.
+
+
+
+
+
+ 768 px · Tablet422 px @ 55% +
+
…/briefwechsel?from=1950&to=1960
+
+
+
+
Person
Walter de Gruyter
Korresp.
Alle
Newest ↓
▾ Filter
0 Briefe
+
Keine Briefe in diesem Zeitraum
Von 1950 bis 1960 gibt es keine Korrespondenz. Zeitraum erweitern oder Filter zurücksetzen.
+
+
+
+
+
+ 1440 px · Desktop720 px @ 55% +
+
familienarchiv.de/briefwechsel?from=1950&to=1960
+
+
+
+
Person
Walter de Gruyter
Korrespondent
Alle
Newest ↓
▾ Filter
0 Briefe
+
Keine Briefe in diesem Zeitraum
Von 1950 bis 1960 gibt es keine Korrespondenz mit Walter de Gruyter. Passe den Zeitraum an oder setze die Filter zurück.
+
+
+
+
+
+
+ + +
+
05Single-person hint — reminder to narrow
+
Already shown in production. Stays exactly as is. Re-rendered here so developers confirm it still renders above the first year divider when only senderId is set. Not shown in bilateral mode.
+
Kein Redesign. Die bestehende SinglePersonHintBar.svelte bleibt unverändert und rendert zwischen Filter-Card und erster Jahres-Trennlinie. Nur in Single-Person-Modus, nicht bilateral.
+
+ +
+
Implementation Reference — Content Stateslist rendering + skeleton
+ + + + + + + + + + + +
ElementClassesRealNote
Skeleton thumbanimate-pulse bg-gradient-to-r from-[#f5f4ef] via-[#eceae4] to-[#f5f4ef] rounded-[1px]shimmer 1.4 sApplied only to .bw-thumb, never to text
Empty cardflex flex-col items-center justify-center rounded-sm border border-line bg-muted py-24 text-center shadow-smpadding 96 px yMatches production empty state
Empty titlefont-serif text-ink18 px desktopParaglide: m.conv_no_results_heading()
Empty bodymt-2 text-sm text-ink-3 max-w-prose mx-auto14 pxParaglide: m.conv_no_results_text()
Distribution barflex flex-col gap-1 border-b border-line bg-muted px-[18px] py-2role="img"aria-label: "Briefverteilung: X von A, Y von B"
Distbar labelsflex justify-between text-sm font-bold · .out text-primary · .in text-accent14 px / 700Counts in tabular-nums
Distbar barflex h-[5px] overflow-hidden rounded-full bg-line5 pxSegments animated with transition-[width]
+
+
+ + +
+

03Row Anatomy · Close-Ups at ~100% Scale

+

Four row types at near-real pixel sizes. These are the reference renderings developers check against when implementing ConversationTimeline.svelte (or its successor ThumbnailRow.svelte).

+ + +
+
Type A · Portrait letter with summary + tags
+
+
+
+
W-0397 – 2. September 1923 – B.Lichterfelde
+
von Elsbeth geschriebener Kommentar, den Herbert zum Brief erzählte — Notiz auf der Rückseite
+
→ ausgehendan Herbert Cram·📍 B.Lichterfelde·VerlagFamilie
+
+
2. September 1923
vor 102 Jahren
+
+
+
Type A — Portrait Letter with Summaryrendered from Document + thumbnail URL
+ + + + + + + + + + + + + + + +
PartClassesRealNote
Row containergroup grid grid-cols-[104px_1fr_auto] gap-5 items-center px-5 py-[14px] min-h-[128px] border-b border-line-2 border-l-[3px] border-l-primary transition-colors hover:bg-muted128 px min<a href="/documents/{id}"> · keyboard reachable
Thumbnail cellw-[104px] h-[120px] flex items-center justify-center shrink-0104 × 120Centers any aspect ratio
Thumbnail imgw-[82px] h-[106px] rounded-[1px] shadow-sm ring-1 ring-white/80 transition-transform group-hover:-translate-y-[1px] group-hover:shadow-md82 × 106 portraitloading="lazy" · alt="" (decorative, title covers meaning)
Titlefont-serif text-base font-bold text-ink leading-[1.35] truncate16 px / 700Merriweather Bold
Summaryfont-serif italic text-sm text-ink-2 leading-[1.55] line-clamp-214 px italicOmit element entirely when doc.summary is empty — no placeholder
Summary quote marks::before & ::after pseudos, color text-accent22 px„…" (German curly quotes)
Meta rowmt-0.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-ink-3 items-center12 pxSeparators use · with text-line
Direction chiptext-[13px] font-extrabold text-primary (out) · text-accent (in)13 px / 800"→ ausgehend" / "← eingehend" (word omitted in bilateral mode)
Tag chipinline-flex items-center text-[10px] font-bold bg-accent/80 text-primary px-[7px] py-0.5 rounded-full10 px / 700Max 2 tags visible at 1440; 1 at 768; 0 at 320
Right column — datefont-serif text-sm font-bold text-ink-2 whitespace-nowrap text-right14 px / 700Intl.DateTimeFormat de-DE (see CLAUDE.md)
Right column — relativetext-[10px] text-ink-3 font-semibold10 px"vor X Jahren" — calculated client-side
+
+
+ + +
+
Type B · Portrait letter without summary (clean variant)
+
+
+
+
W-0396 – 2. September 1923 – B.Lichterfelde
+
→ ausgehendan Herbert Cram·📍 B.Lichterfelde
+
+
2. September 1923
vor 102 Jahren
+
+
No placeholder when summary is missing. The summary element is not rendered at all — row height still hits min-h-[128px] so the list stays rhythmic. Tags are also omitted when empty (no empty chip row).
+
+ + +
+
Type C · Postcard · landscape thumbnail with stamp + postmark
+
+
+
+
Ansichtskarte – 20. August 1923 – Thüringer Wald
+
Urlaubsgruß, kurze Notiz über Wetter und geplante Rückkehr
+
← eingehendvon Herbert Cram·📍 Thüringer Wald·✉ Postkarte
+
+
20. August 1923
vor 102 Jahren
+
+
+
Type C — Postcard (landscape)aspect ratio detection + kind chip
+ + + + + + + +
PartClassesRealNote
Thumbnailw-[104px] h-[72px] rounded-[1px] shadow-sm ring-1 ring-white/80104 × 72 landscapeAspect ratio detected server-side from PDF page 1 dimensions (w/h > 1.1 → landscape)
Kind chipinline-flex items-center text-[10px] font-bold uppercase tracking-wide bg-line text-ink-2 px-[7px] py-0.5 rounded-full10 px / 700 uppercaseParaglide: m.doc_kind_postcard() — shown only when thumbnail is landscape
Stamp cornerCSS pseudo-element on thumbnail — 16×18 px gradient square top-right 5 pxdecorativeIn production: rendered by the thumbnail service as part of the real scan; the CSS is only for spec rendering
+
+
+ + +
+
Type D · Multi-page letter with "N Seiten" badge
+
+
+
+ 4 S. +
+
+
W-0524 – 31. Juli 1923 – Berlin
+
Glückwunsch zum 60. Geburtstag, Bericht über den Verlag und den anstehenden Umzug nach B.Lichterfelde
+
→ ausgehendan Walter Dieckmann·📍 Berlin·GeburtstagVerlag
+
+
31. Juli 1923
vor 102 Jahren
+
+
+
Type D — Page-count Badgeonly when pages > 1
+ + + + + + + +
PartClassesRealNote
Badge containerabsolute top-1 -right-1 bg-primary text-primary-fg text-[10px] font-bold px-[7px] py-0.5 rounded-full ring-2 ring-white10 px / 700Overlaps the thumbnail by 4 px right
LabelParaglide: m.doc_pages_count({ count })"4 S."Abbreviated form for the badge; full "4 Seiten" appears in the document detail page
Visibility ruleRender {#if doc.pageCount > 1}Never show "1 S."
+
+
+
+ + +
+

04Distribution Bar · Close-Up

+

Only rendered in bilateral mode (both senderId and receiverId set). This component already exists in production as part of ConversationTimeline.svelte — this spec keeps its API and visual treatment identical but moves it out of the timeline header into a standalone component above the row list, so it can sit between the filter card and the year dividers.

+ +
+
Distribution bar · bilateral Walter ↔ Herbert
+
+
+ 87 von Walter de Gruyter → + ← 56 von Herbert Cram +
+
+ + +
+
+
+
Distribution Barrole="img" + aria-label carries the data
+ + + + + + + + + + + +
PartClassesRealNote
Wrapperflex flex-col gap-1 border-b border-line bg-muted px-[18px] py-28 px y paddingrole="img" · aria-label describes full distribution
Out labelinline-flex items-center gap-1 text-primary text-sm font-bold tabular-nums14 px / 700Format: "{count} von {sender} →"
In labelinline-flex items-center gap-1 text-accent text-sm font-bold tabular-nums14 px / 700Format: "← {count} von {receiver}"
Barflex h-[5px] overflow-hidden rounded-full bg-line5 px tallSegments use transition-[width] duration-300 ease-out
Out segmentbg-primary h-fullwidth from APIPercentage computed backend-side from counts
In segmentbg-accent h-fullcomplementaryNever use 100% - out; both come from the API separately
Mobile (320 px)Labels stack with flex-col gap-1; bar stays full-widthNo truncation of counts — numbers must always be legible
+
+
+
+ + +
+

05Accessibility Contract · WCAG AA/AAA

+

Every colour pair on the rendered row has been measured. AAA where reasonably achievable; AA is the floor. The row is a link, not a button — keyboard navigation is native tab-through-list semantics.

+ +
+
Light Mode — Contrast Verificationlayout.css tokens
+ + + + + + + + + + + + +
PairValueRatioWCAG
Title (ink on surface)#1A1A1A on #ffffff19.6:1AAA ✓
Summary (ink-2 on surface)#444444 on #ffffff9.7:1AAA ✓ (body)
Meta (ink-3 on surface)#666666 on #ffffff5.7:1AA ✓
Direction out (primary on surface)#002850 on #ffffff14.5:1AAA ✓
Direction in (accent on surface)#2F9E95 on #ffffff4.6:1AA ✓ (normal)
Tag chip (primary on mint)#002850 on #a6dad88.1:1AAA ✓
Quote marks (accent on surface)#a6dad8 decorativen/aDecorative — summary text carries meaning
Focus ring (primary on surface)#002850 on #ffffff, 2px offset14.5:1AAA ✓
+
+ +
+
Dark Mode — Contrast Verificationremap via data-theme="dark"
+ + + + + + + + + + +
PairValueRatioWCAG
Title (ink on surface-dark)#f0efe9 on #011a3015.1:1AAA ✓
Summary (ink-2 on surface-dark)#c5cbd4 on #011a3011.2:1AAA ✓
Meta (ink-3 on surface-dark)#9ca3af on #011a307.8:1AAA ✓ (body)
Direction out (mint on canvas)#a1dcd8 on #010e1e9.6:1AAA ✓
Direction in (turquoise on canvas)#00c7b1 on #010e1e6.8:1AA ✓
Tag chip (turquoise on tint)#00c7b1 on rgba(0,199,177,.2)6.3:1AA ✓
+
+ +
Non-negotiable accessibility rules. +
    +
  • Row is rendered as <a href="/documents/{id}"> — never <div onclick>. Keyboard Tab enters, Enter opens, Shift-Tab leaves.
  • +
  • Focus ring: focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 — always visible on keyboard focus, never on mouse click.
  • +
  • Thumbnail <img alt=""> — empty alt because the title next to it names the letter. A descriptive alt would be announced twice.
  • +
  • Direction glyph is color and shape (arrow direction). Never rely on color alone — the arrow "→" vs "←" carries meaning even in monochrome.
  • +
  • Distribution bar uses role="img" with a full-sentence aria-label. Screen readers hear the whole distribution in one announcement, not each half.
  • +
  • Minimum body text 14 px; minimum meta text 12 px. Never below 12 px for any visible text.
  • +
  • Touch target: 128 px row height ≫ 44 px WCAG minimum. Comfortable for senior users on phones.
  • +
  • prefers-reduced-motion: hover lift on thumbnail collapses to transition-duration: 0.01ms. Required (project CLAUDE.md + WCAG 2.3.3).
  • +
+
+
+ + +
+

06Implementation Notes — Data, Thumbnails, Routing

+ +
+
Data contract — fields read per row/api/documents/conversation
+ + + + + + + + + + + + + + +
FieldFromUsed forFallback
idDocumentRow key, hrefrequired
titleDocumentRow titleoriginalFilename
summaryDocumentQuote line (omit when empty)element not rendered
documentDateDocumentYear group, right-column date, relative time"—" placeholder, year group "Ohne Datum"
locationDocumentMeta linehidden
sender / receiversDocumentDirection + counterpart namedirection omitted, name = m.conv_no_party()
tags[]DocumentMeta line (max 2 at 1440, 1 at 768, 0 at 320)no chips rendered
pageCountDocument (new, from thumbnail service)Badge when > 1no badge
thumbnailUrlDocument (new, from thumbnail service)<img src>skeleton until fetched
thumbnailAspectDocument (new, from thumbnail service)portrait / landscape classdefaults to portrait
+
+ +
+
Thumbnail service — new endpointsdepends on open issue "thumbnail generation"
+ + + + + + + + + + +
ConcernDecisionNote
StorageMinIO bucket thumbnailsMirrors document ID path; WEBP at 2× target resolution
URL/api/documents/{id}/thumbnailRedirects (302) to a presigned MinIO URL · Cache-Control: public, max-age=2592000 (30 d)
AspectComputed once on generation, persisted as Document.thumbnailAspect enum PORTRAIT \| LANDSCAPEThreshold w/h > 1.1 → LANDSCAPE
Page countPersisted as Document.pageCount on upload / reprocessNot computed client-side
Loading strategy<img loading="lazy" decoding="async"> with intersection observer for rows below the foldSkeleton state until onload fires
FallbackPaper-coloured placeholder (matches thumbnail gradient) with document iconNever break the row layout
+
+ +
+
Component structurenew files
+ + + + + + + + + +
FileResponsibilityReplaces
ThumbnailRow.svelteSingle row with thumbnail, title, summary, meta, right columnRow rendering inside ConversationTimeline.svelte
DistributionBar.svelteThe bilateral distribution barLifts existing markup out of ConversationTimeline.svelte
YearDivider.svelteYear number + Briefe countAlready exists; no change required
ConversationTimeline.svelteOrchestrator · renders distribution bar + year dividers + ThumbnailRowsSimplified — no longer does row markup directly
DocumentThumbnail.svelteReusable thumbnail element with lazy-load + aspect + page badgenew · also usable on /documents list pages
+
+ +
Shipping order. +
    +
  • Phase 1 — land ThumbnailRow, DistributionBar (extracted), and new typography/spacing without real thumbnails. Thumbnail cell renders the skeleton permanently. Ship and observe.
  • +
  • Phase 2 — wire up thumbnail service (open issue "PDF thumbnail generation"). Replace skeleton with real thumbnails. Add thumbnailAspect + pageCount to the Document entity and the /api/documents/conversation response.
  • +
  • Phase 3 — add lazy-loading + intersection observer for rows outside viewport. Measure perf on 851-letter lists.
  • +
+
+
+ +
+ + diff --git a/docs/specs/person-dashboard-spec.html b/docs/specs/person-dashboard-spec.html new file mode 100644 index 00000000..5d5eff45 --- /dev/null +++ b/docs/specs/person-dashboard-spec.html @@ -0,0 +1,1043 @@ + + + + + +Person Detail — Korrespondenz-Überblick · Final Design Spec · Familienarchiv + + + +
+ + +
+
+
+

Person Detail — Korrespondenz-Überblick

+

Final design for the dashboard block that extends /persons/[id]. Gives every person page a correspondence-at-a-glance view — stats, activity per year, direction split, top correspondents, top locations, tag cloud — and turns each element into a filter shortcut into /briefwechsel. Replaces the current CoCorrespondentsList block.

+
+
FINAL
+
+
+
Route
/persons/[id] · right column
+
Layout
35% / 65% split · stacks below 768 px
+
Sections shown
Stats · Histogram · Direction · Correspondents · Locations · Tags
+
Deep links
Every tile → /briefwechsel with filters applied
+
+
+ +
+ Reading this spec. Mockups in Section 02 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 class + pixel value. Close-ups in Section 03 render each dashboard block at ~100 % scale for pixel-accurate reference. +
+ + +
+
Inhalt
+
    +
  1. 01 Page anatomy default · 1440 px
  2. +
  3. 02 Content states × 3 viewports 4 states · 12 frames
  4. +
  5. 03 Dashboard block close-ups 6 blocks @ real size
  6. +
  7. 04 Deep-link grammar every tile → /briefwechsel
  8. +
  9. 05 Accessibility contract WCAG AA/AAA
  10. +
  11. 06 Implementation notes backend API · components
  12. +
+
+ + +
+

01Page Anatomy — Default State at 1440 px

+

The page is a 35% / 65% split (existing lg:grid-cols-[35%_65%]). Left column keeps PersonCard and NameHistoryCard. Right column replaces CoCorrespondentsList with the new PersonDashboard block at the top, followed by the existing sent/received document lists.

+ +
+
+
+
familienarchiv.de/persons/{id}
+
+
DokumentePersonenBriefwechselChronik
+
+
← Zurück
+
+ +
+
WG
+
Walter de Gruyter
+
1862 – 1923
+
Bearbeiten
Briefwechsel
+
+ +
+

Korrespondenz-Überblick

↗ Briefwechsel
+
+
851
gesamt
+
612
ausgehend
+
239
eingehend
+
42
Jahre
+
+
+

Aktivität über die Jahre Spitzenjahr 1922 · 78

+
+
189819221940
+
+
+

Richtungsverteilung

+
→ 612 · 72%← 239 · 28%
+
+
+
+
+

Top Korrespondenten von 87

+
+
Walter Dieckmann184
+
Herbert Cram143
+
Ella Dieckmann88
+
Eugenie de Gruyter77
+
Gertrud v. Rofden58
+
+
+
+

Top Orte von 42

+
+
Berlin412
+
B.Lichterfelde180
+
Bad Kissingen58
+
Cöln37
+
Belgard26
+
+
+
+
+

Beliebte Schlagwörter Klick filtert den Briefwechsel

+
VerlagFamilieGeburtstagWeihnachtenKuraufenthaltReiseGeschäftKriegKrankheitSchuleHochzeitNeujahr
+
+
+
+
+
+
+
+
+
A · Person card
Unchanged in this spec. Avatar, full name, lifespan, bearbeiten + Briefwechsel buttons. The primary action moves from "Edit" to "Open Briefwechsel" because that is what users actually do next.
+
B · Korrespondenz-Überblick
New dashboard block. Dark navy header strip with "Briefwechsel öffnen" CTA on the right. Stats strip below header, then 4 sections separated by 1 px rules.
+
Stats strip
4 cells at desktop / 2×2 at tablet / 4 stacked at mobile. Numbers in serif (Merriweather Black) at 22 px. Direction colours match row border colours elsewhere (out = navy, in = accent).
+
Activity histogram
One bar per year in the range (1898 → 1940 = 43 bars). Peak bar uses bg-primary, others bg-accent/60. Hovering a bar shows "{year} · {count} Briefe" tooltip; clicking filters /briefwechsel?senderId=…&from=YYYY-01-01&to=YYYY-12-31.
+
Top correspondents
Up to 6 rows. Name + proportional bar + count. Click opens /briefwechsel?senderId=<this>&receiverId=<other> (bilateral view). "Alle N Korrespondenten →" link below.
+
Top locations & Tag cloud
Location tiles mirror correspondents. Tag cloud sized by frequency — xl > 100, l > 50, m > 20, muted ≤ 20. Click on any tag: /briefwechsel?senderId=<this>&tag=<id>.
+
+
+ +
+
Implementation Reference — Page Shellextends existing /persons/[id]
+ + + + + + + + + + + + +
ElementClassesRealNote
Page containermx-auto max-w-6xl px-4 py-10max 72remUnchanged · existing route shell
2-column gridlg:grid lg:grid-cols-[35%_65%] lg:gap-832 px gapExisting · unchanged
Left column stackPersonCard → NameHistoryCard (mt-6)24 px mtExisting · unchanged
Right columnPersonDashboard → PersonDocumentList(sent) → PersonDocumentList(received)new + existingPersonDashboard replaces CoCorrespondentsList
Dashboard containeroverflow-hidden rounded-sm border border-line bg-surface shadow-sm1 px borderMatches card pattern from CLAUDE.md
Dashboard headerflex items-center justify-between gap-3 bg-primary text-primary-fg px-5 py-312 px y paddingDark navy strip — sets dashboard apart from body card patterns
Dashboard titlefont-serif text-base font-bold16 px / 700Merriweather
"Briefwechsel öffnen" CTAbg-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-center44 px minWCAG 2.2 AA touch target; Paraglide m.person_open_conversation()
+
+
+ + +
+

02Content States × 3 Viewports

+

Four states. Every frame renders the page shell (header → back link → split grid). Reading order per state: 320 px → 768 px → 1440 px. At 320 and 768, the grid stacks and the dashboard flows below the person card.

+ + +
+
01Default · Full dataset (851 letters · 42 years · 87 correspondents)
+
The happy path. Every section has data. Numbers are formatted with German number formatting; year range in histogram auto-fits to data span; top lists truncate to 5 on tablet, 6 on desktop, 3 on mobile.
+
+ +
+ 320 px · Mobile176 px @ 55% +
+
9:41
+
+
Personen
+
+
← Zurück
+
+
WG
Walter de Gruyter
1862 – 1923
Briefwechsel
+
+

Überblick

+
+
851
ges.
+
612
+
239
+
42J
Jahre
+
+
+

Aktivität

+
+
+
+

Top

+
+
W. Dieckmann184
+
H. Cram143
+
E. Dieckmann88
+
+
+
+

Schlagwörter

+
VerlagFamilieGeburtstagKur
+
+
+
+
+
+
+
+ +
+ 768 px · Tablet422 px @ 55% +
+
…/persons/9ed8…
+
+
PersonenBriefwechsel
+
+
← Zurück
+
+
WG
Walter de Gruyter
1862 – 1923
Edit
Briefwechsel
+
+

Korrespondenz-Überblick

↗ Öffnen
+
+
851
gesamt
+
612
ausgehend
+
239
eingehend
+
42
Jahre
+
+
+

Aktivität 1922 · 78

+
+
189819221940
+
+
+

Richtung

+
→ 612 · 72%← 239 · 28%
+
+
+
+
+

Top Korresp.

+
+
W. Dieckmann184
+
H. Cram143
+
E. Dieckmann88
+
E. de Gruyter77
+
G. Rofden58
+
+
+
+

Top Orte

+
+
Berlin412
+
B.Lichterfelde180
+
Bad Kissingen58
+
Cöln37
+
+
+
+
+

Schlagwörter

+
VerlagFamilieGeburtstagWeihnachtenKurReiseKriegKrankheitTod
+
+
+
+
+
+
+
+ +
+ 1440 px · Desktop780 px @ 55% +
+
familienarchiv.de/persons/9ed8fd47-…
+
+
DokumentePersonenBriefwechselChronik
+
+
← Zurück
+
+
WG
Walter de Gruyter
1862 – 1923
Bearbeiten
Briefwechsel
+
+

Korrespondenz-Überblick

↗ Briefwechsel öffnen
+
+
851
Briefe gesamt
+
612
ausgehend
+
239
eingehend
+
42
Jahre
+
+
+

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
+
Käthe Dieckmann47
+
+
+
+

Top Orte 5 von 42

+
+
Berlin412
+
B.Lichterfelde180
+
Bad Kissingen58
+
Cöln37
+
Belgard26
+
+
+
+
+

Beliebte Schlagwörter Klick filtert den Briefwechsel

+
VerlagFamilieGeburtstagWeihnachtenKuraufenthaltReiseGeschäftKriegKrankheitSchuleHochzeitNeujahr
+
+
+
+
+
+
+
+
+
Wrap behaviour by viewport. +
    +
  • 320 px: grid stacks. Person card then dashboard. Stats drop to 2×2 cells. Histogram shows aggregated year-groups (6 bars), not individual years. Top lists truncate to 3. Location section collapses.
  • +
  • 768 px: grid stacks. 4×1 stats. Full histogram. Two-col section shows correspondents & locations side-by-side. Tag cloud shows 8–9 tags.
  • +
  • 1440 px: 35/65 split. All sections fully rendered. Tag cloud shows all significant tags (those with count ≥ 5) plus up to 5 muted smaller ones.
  • +
+
+
+ + +
+
02Empty · Person has no letters yet
+
Person exists (imported, created manually) but no documents reference them as sender or receiver. Dashboard collapses to a single reassurance card inviting the user to upload.
+
+
+ 320 px · Mobile176 px @ 55% +
9:41
+
← Zurück
NN
Neue Person

Überblick

Noch keine Briefe
Diese Person hat noch keine Korrespondenz im Archiv.
+
+
+
+ 768 px · Tablet422 px @ 55% +
…/persons/new-id
+
Personen
← Zurück
NN
Neue Person
Bearbeiten

Korrespondenz-Überblick

Noch keine Briefe
Diese Person hat noch keine Korrespondenz im Archiv. Sobald ein Brief zugewiesen wird, erscheint der Überblick hier automatisch.
+
+
+
+ 1440 px · Desktop780 px @ 55% +
familienarchiv.de/persons/new-id
+
PersonenBriefwechsel
← Zurück
NN
Neue Person
Bearbeiten

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.
+
+
+
+
+ + +
+
03Sparse · Few letters (< 10)
+
Person has between 1 and 9 letters. Histogram would be uninformative; it collapses to a single line with year range. Top lists show whatever is present — no "von N" note. No tag cloud unless at least 3 different tags exist.
+
+
+ 320 px · Mobile176 px @ 55% +
9:41
+
← Zurück
EB
Elsbeth Brandt
1890 – 1963

Überblick

7
ges.
4
3
3J
Jahre

Zeitraum

1919 – 1922

Top

W. de Gruyter4
H. Cram3
+
+
+
+ 768 px · Tablet422 px @ 55% +
…/persons/elsbeth
+
← Zurück
EB
Elsbeth Brandt
1890 – 1963
Briefwechsel

Korrespondenz-Überblick

↗ Öffnen
7
gesamt
4
ausgehend
3
eingehend
3
Jahre

Zeitraum

1919 · erster Brief1922 · letzter Brief

Korrespondenten

Walter de Gruyter4
Herbert Cram3
+
+
+
+ 1440 px · Desktop780 px @ 55% +
familienarchiv.de/persons/elsbeth
+
Personen
← Zurück
EB
Elsbeth Brandt
1890 – 1963
Bearbeiten
Briefwechsel

Korrespondenz-Überblick

↗ Briefwechsel öffnen
7
Briefe gesamt
4
ausgehend
3
eingehend
3
Jahre

Zeitraum

1919 · erster Brief1922 · letzter Brief

Korrespondenten

Walter de Gruyter4
Herbert Cram3

Orte

B.Lichterfelde5
Berlin2
+
+
+
+
Sparse-mode simplifications. +
    +
  • Histogram replaced with a single "Zeitraum" line showing first and last letter years. Histogram returns as soon as letterCount >= 10 and yearSpan >= 3.
  • +
  • Tag cloud hidden when fewer than 3 distinct tags across all letters. No "empty tag cloud" placeholder.
  • +
  • "Top of N" notes dropped — numbers speak for themselves.
  • +
+
+
+ + +
+
04Loading · Skeleton
+
While the aggregation endpoint is in-flight. Person card renders from the /api/persons/{id} payload (fast); the dashboard shows skeleton rectangles for each section in the same grid slots.
+
+
+ 320 px · Mobile176 px @ 55% +
9:41
+
← Zurück
WG
Walter de Gruyter
1862 – 1923

Überblick

+
+
+
+ 768 px · Tablet422 px @ 55% +
…/persons/…
+
← Zurück
WG
Walter de Gruyter
1862 – 1923
Edit
Briefwechsel

Korrespondenz-Überblick

+
+
+
+ 1440 px · Desktop780 px @ 55% +
familienarchiv.de/persons/…
+
← Zurück
WG
Walter de Gruyter
1862 – 1923

Korrespondenz-Überblick

+
+
+
+
+
+ + +
+

03Dashboard Block Close-Ups · ~100% Scale

+

Six blocks rendered at near-real pixel sizes. These are the reference renderings developers check against when implementing PersonDashboard.svelte and its sub-components.

+ + +
+
Block A · Stats strip
+
+
851
Briefe gesamt
+
612
ausgehend
+
239
eingehend
+
42
Jahre
+
+
+
Block A — Stats StripDesktop = 4 cells · Tablet = 4 cells · Mobile = 2×2
+ + + + + + + + + +
PartClassesRealNote
Strip containergrid grid-cols-2 sm:grid-cols-4 gap-px bg-line border-b border-line1 px gap (shows as lines)Separators are the background showing through
Cellbg-muted px-4 py-3.5 text-center14 px y paddingUses bg-muted not bg-surface so separators read
Numberfont-serif text-[22px] font-black text-primary leading-none tabular-nums tracking-tight22 px / 900Merriweather Black · .out = primary, .in = accent
Labelmt-1 text-[10px] font-bold uppercase tracking-wide text-ink-310 pxDirection labels match number colour
Mobile formatterAbbreviate "Briefe gesamt" → "gesamt"m.person_stats_total_short()Paraglide key with _short suffix
+
+
+ + +
+
Block B · Activity histogram (one bar per year)
+
+
+
+
18981922 ▲ Spitzenjahr · 78 Briefe1940
+
+
Block B — Activity Histogramdeep-links via from/to params
+ + + + + + + + + + + +
PartClassesRealNote
Containerflex items-end gap-0.5 h-[72px] pt-172 px heightHeight tuned to comfortable reading — not too small
Bar (normal)flex-1 bg-accent/60 rounded-t-sm hover:bg-accent/90 transition-colors cursor-pointervariable widthMin-width 3 px for ≤ 30 years; thinner for longer ranges
Bar (peak)flex-1 bg-primary rounded-t-smmax heightExactly one peak bar highlighted
Bar link<a href="/briefwechsel?senderId={id}&from={year}-01-01&to={year}-12-31">Whole bar is the link; tooltip announces "Jahr {year} · {count} Briefe"
Year labelsflex justify-between text-[10px] font-bold text-ink-3 mt-1.510 pxOnly show: earliest year · peak year · latest year
Tooltipnative title attribute on bar"1922 · 78 Briefe" — Paraglide pluralized
Empty-year barsrendered as min-height: 2px placeholder2 pxKeeps bar spacing regular across decades with gaps
+
+
+ + +
+
Block C · Direction split (same component as /briefwechsel distribution bar, re-used)
+
+
→ 612 ausgehend · 72%← 239 eingehend · 28%
+
+
+
Re-use, do not duplicate. The bilateral DistributionBar.svelte component from the thumbnail rows spec is also used here. Same props (outCount, outLabel, inCount, inLabel), same aria-label pattern.
+
+ + +
+
Block D · Top correspondents
+
+
Walter Dieckmann184
+
Herbert Cram143
+
Ella Dieckmann88
+
Eugenie de Gruyter77
+
Gertrud von Rofden58
+
Käthe Dieckmann47
+
+
+
Block D — Top List (correspondents & locations share this)rendered as <ol> for semantic ranking
+ + + + + + + + + + + +
PartClassesRealNote
Wrapper<ol> · flex flex-col gap-28 px gapOrdered list — rank order matters, screen readers announce "1 of 6"
Item<li> containing <a> · flex items-center gap-3 text-sm px-1.5 py-1 rounded-sm min-h-[32px] hover:bg-muted32 px minNot 44 px because these are secondary links inside a card — desktop focus. Mobile bumps to 44 via md:min-h-[32px] min-h-[44px]
Nameflex-1 font-semibold text-ink truncate14 px / 600Truncate middle-ellipsis on very long German names at < 768 px
Proportional bar wrapperw-[120px] h-[7px] bg-line rounded overflow-hidden shrink-0 hidden sm:block120 × 7Hidden on mobile — the number carries the data
Proportional bar fillh-full bg-primary roundedwidth = value ÷ maxWidths computed client-side from the list's max value
Countw-10 text-right text-sm text-ink-3 font-bold tabular-nums shrink-014 px / 700Tabular figures so columns align
"Alle N anzeigen →"mt-2.5 text-xs font-bold text-primary border-b border-dashed border-primary/60 hover:border-primary12 px / 700Appears when total > shown count
+
+
+ + +
+
Block E · Top locations — identical component to Block D
+
+
📍 Berlin412
+
📍 B.Lichterfelde180
+
📍 Bad Kissingen58
+
📍 Cöln37
+
📍 Belgard26
+
+
Emoji pin is decorative. Rendered via ::before with aria-hidden="true". The location string alone carries semantic meaning. Future iteration may replace with a map icon component.
+
+ + +
+
Block F · Tag cloud — frequency-sized
+
+ VerlagFamilieGeburtstagWeihnachtenKuraufenthaltReiseGeschäftKriegKrankheitSchuleHochzeitNeujahr +
+
+
Block F — Tag Cloudsize buckets map to count thresholds
+ + + + + + + + + + + +
SizeClassesCount thresholdNote
xltext-[15px] font-bold px-3.5 py-1 bg-accent text-primary rounded-full≥ 100Max 2 tags at this size — visual anchor
ltext-[13px] font-bold px-3 py-1 bg-accent text-primary rounded-full50–99Up to 4 tags
mtext-xs font-bold px-2.5 py-0.5 bg-accent text-primary rounded-full20–49Up to 6 tags
regulartext-xs font-bold px-2.5 py-0.5 bg-accent text-primary rounded-full5–19All tags meeting threshold
mutedtext-xs font-semibold px-2.5 py-0.5 bg-line text-ink-3 rounded-full< 5Up to 8 muted tags, sorted alphabetically
Click targetmin-h-[28px] min-w-[44px] inline-flex items-center44 px min widthVery short tag names ("Kur") still meet touch target
Hoverhover:-translate-y-px transition-transform1 px liftBypassed when prefers-reduced-motion
+
+
+
+ + +
+

04Deep-Link Grammar — every tile → /briefwechsel

+

The dashboard is the discovery surface; /briefwechsel is the reading surface. Every clickable element in the dashboard adds filters to the existing /briefwechsel query string so the transition feels continuous.

+ +
+
Link grammarreplace placeholders with real values
+ + + + + + + + + + + + +
ElementLinkNote
Header "↗ Briefwechsel öffnen"/briefwechsel?senderId={id}&dir=DESCOpens all letters for this person · no date filter · newest first
Stats — "gesamt"Same as headerEntire count is tap-to-open
Stats — "ausgehend"/briefwechsel?senderId={id}&direction=OUT&dir=DESCRequires new direction query param on /briefwechsel
Stats — "eingehend"/briefwechsel?senderId={id}&direction=IN&dir=DESCSame
Histogram bar/briefwechsel?senderId={id}&from={year}-01-01&to={year}-12-31Opens bilateral or single view scoped to that year
Top correspondent row/briefwechsel?senderId={id}&receiverId={otherId}&dir=DESCOpens the bilateral view for the pair
Top location row/briefwechsel?senderId={id}&location={locSlug}Requires new location query param on /briefwechsel
Tag chip/briefwechsel?senderId={id}&tagId={tagId}Requires new tagId query param on /briefwechsel
+
+ +
New query parameters required on /briefwechsel. +
    +
  • direction=OUT|IN — filter to letters in one direction only (existing endpoint already distinguishes sender vs receiver; this adds symmetry for the single-person view).
  • +
  • location=<slug> — case-insensitive match on Document.location. Slug because German locations can contain spaces and dots ("B.Lichterfelde", "Bad Kissingen").
  • +
  • tagId=<uuid> — filter to letters that reference this tag. If omitted, no tag filter is applied.
  • +
+
+
+ + +
+

05Accessibility Contract · WCAG AA/AAA

+

Every colour pair on the dashboard has been measured. Semantics matter as much as colour: the stats use real numbers (not SVG), the histogram has tooltip text, the top lists are ordered lists, the tag cloud is a list of links.

+ +
+
Light Mode — Contrast Verificationlayout.css tokens
+ + + + + + + + + + + + + + +
PairValueRatioWCAG
Stat number (primary on muted)#002850 on #f7f5f214.0:1AAA ✓
Stat label (ink-3 on muted)#666666 on #f7f5f25.4:1AA ✓
Stat "in" (accent on muted)#2F9E95 on #f7f5f24.5:1AA ✓ (borderline — number is 22 px / 900 qualifies as large)
Dashboard header title (surface on primary)#ffffff on #00285014.5:1AAA ✓
CTA "Briefwechsel öffnen" (primary on accent)#002850 on #a6dad88.1:1AAA ✓
Histogram bar (accent/60 on surface)rgba(47,158,149,.6) on #ffffff2.8:1Decorative (bars have titles; not text)
Histogram peak bar (primary on surface)#002850 on #ffffff14.5:1AAA ✓
Tag chip (primary on accent)#002850 on #a6dad88.1:1AAA ✓
Muted tag (ink-3 on line)#666666 on #eee8dc5.1:1AA ✓
Focus ring (primary on surface, 2 px offset)#00285014.5:1AAA ✓
+
+ +
+
Dark Mode — Contrast Verificationdata-theme="dark"
+ + + + + + + + + + + + +
PairValueRatioWCAG
Stat number (ink on canvas-2)#f0efe9 on #01152615.1:1AAA ✓
Stat "out" (mint on canvas-2)#a1dcd8 on #0115269.6:1AAA ✓
Stat "in" (turquoise on canvas-2)#00c7b1 on #0115266.8:1AA ✓
Stat label (ink-3 on canvas-2)#8b97a5 on #0115267.1:1AAA ✓
Dashboard header (ink on navy-2)#f0efe9 on #01223f13.8:1AAA ✓
CTA (primary on mint)#012851 on #a1dcd89.6:1AAA ✓
Histogram peak (mint on canvas)#a1dcd8 on #010e1e9.2:1AAA ✓
Tag (turquoise on tint)#00c7b1 on rgba(0,199,177,.2)6.3:1AA ✓
+
+ +
Non-negotiable accessibility rules. +
    +
  • Stats are real <dl> / <dt> / <dd> pairs. Screen readers announce "Briefe gesamt: 851, ausgehend: 612, …".
  • +
  • Histogram wrapped in role="img" with aria-label="Aktivität über 42 Jahre, Spitzenjahr 1922 mit 78 Briefen". Each bar has a title attribute for sighted tooltip.
  • +
  • Top lists are semantic <ol> elements — screen readers announce "Top Korrespondenten, list 6 items, 1 of 6 Walter Dieckmann, 184 Briefe".
  • +
  • Tag cloud is a <ul> of <li><a>. The visual size does not carry meaning that text cannot — the count is not exposed to screen readers via size, only via the tooltip / aria-label "Schlagwort Verlag, {count} Briefe".
  • +
  • Focus ring: focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 on every interactive element (histogram bars, top-list rows, tag chips, header CTA).
  • +
  • Touch targets: CTA 44 px, histogram bar 32 px min width (padded invisibly if needed), top-list row 44 px on mobile / 32 px on desktop, tag chip 44 px min width.
  • +
  • prefers-reduced-motion: disable bar width animation on first render and the tag-chip hover lift. The skeleton shimmer collapses to a static gradient.
  • +
+
+
+ + +
+

06Implementation Notes — Backend + Components

+ +
+
New backend endpointaggregation for dashboard
+ + + + + + + + +
FieldValueNote
RouteGET /api/persons/{id}/dashboardSeparate from /api/persons/{id} to keep person entity lean and let the dashboard query be cache-friendly
Permission@RequirePermission(Permission.READ_ALL)Same as /api/persons/{id}
CacheServer-side cache keyed on (personId, dataVersion)Invalidate on Document write that references this person (see DocumentService update hooks)
Response schemasee next tableAll counts server-computed — never client-computed
+
+ +
+
Response schemaPersonDashboardDTO
+ + + + + + + + + + + + + + + +
FieldTypeNote
totalCountintoutCount + inCount
outCountintLetters where this person is sender
inCountintLetters where this person is in receivers
yearSpanintlatestYear - earliestYear + 1 (null when no letters)
correspondentCountintDistinct counterparts
activityByYearMap<int, int>year → count · always contiguous (missing years = 0 · dashboard decides display)
peakYearintYear with most letters (null when no letters)
peakYearCountint
topCorrespondentsList<CorrespondentTileDTO>Max 10 · sorted desc · each has personId · displayName · count
topLocationsList<LocationTileDTO>Max 10 · each has location · count
topTagsList<TagTileDTO>Max 20 · each has tagId · label · count · frontend buckets into size tiers
+
+ +
+
Component structurenew files + changes
+ + + + + + + + + + + + +
FileResponsibilityChange
PersonDashboard.svelteOrchestrator · renders header + stats + sectionsnew
StatStrip.svelte4-cell stats grid with direction colouringnew
ActivityHistogram.svelteOne bar per year, peak highlight, hover tooltipnew
DistributionBar.svelteAlready introduced in briefwechsel-thumbnail-rows-spec.html · re-used here with different labelsshared
TopTileList.svelteOrdered list of tiles (name + bar + count) — used for correspondents and locationsnew · generic
TagCloud.svelteFrequency-sized chips with size bucketsnew
PersonPageShell.svelte / +page.svelteRenders 2-column gridReplace CoCorrespondentsList with PersonDashboard. Remove coCorrespondents derivation in +page.svelte — dashboard owns it.
+page.server.tsLoads person dataAdd parallel call to /api/persons/{id}/dashboard; keep error handling identical
+
+ +
Shipping order. +
    +
  • Phase 1 — backend endpoint GET /api/persons/{id}/dashboard with PersonDashboardDTO. Cache on person-write hooks. Tests for empty / sparse / full.
  • +
  • Phase 2PersonDashboard.svelte + its six sub-components. Replaces CoCorrespondentsList in the right column.
  • +
  • Phase 3 — new query params on /briefwechsel: direction, location, tagId. Wire every dashboard element to them.
  • +
  • Phase 4 — axe-playwright tests at 320 / 768 / 1440 in light + dark. Visual regression snapshots for all four states.
  • +
+
+
+ +
+ +