The archive has ~4 persons over 100 letters and ~90% with five or fewer — the original spec's 851-letter default fit no one. Redesign introduces three tiers gated on letterCount (Compact ≤ 5, Standard 6–49, Rich ≥ 50) sharing one dashboard block: navy header + 4-cell stats strip at every non-Empty tier, with Standard appending direction bar + top correspondents and Rich further appending histogram + top locations + tag cloud. Backend skips expensive aggregations for non-Rich persons; histogram and tag cloud ship lazy-loaded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1146 lines
92 KiB
HTML
1146 lines
92 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Person Detail — Korrespondenz-Überblick · Tiered Edition · Familienarchiv</title>
|
||
<style>
|
||
/* ═══════════════════════════════════════════════════════════
|
||
PERSON DASHBOARD — /persons/[id] · Tiered Edition
|
||
Three tiers mapped to the archive's real data distribution:
|
||
· Compact — ≤ 5 letters · ~90% of persons (baseline)
|
||
· Standard — 6–49 letters · ~6% of persons
|
||
· Rich — ≥ 50 letters · <1% (4 real cases)
|
||
The Rich tier is the former 6-block dashboard. Compact replaces
|
||
it as the default because the archive is 90% short-correspondence
|
||
persons — charts for 3 letters are noise, not signal.
|
||
By Leonie Voss (UX/Design). 2026-04-23 · REV 2.
|
||
═══════════════════════════════════════════════════════════ */
|
||
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:'Helvetica Neue',Arial,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||
.doc{max-width:1400px;margin:0 auto;padding:48px 28px}
|
||
|
||
/* ── Masthead ──────────────────────────────────────────── */
|
||
.mast{background:#012851;border-radius:10px;padding:32px 40px;margin-bottom:40px;color:#fff}
|
||
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:14px}
|
||
.mast h1{font-size:22px;font-weight:900;letter-spacing:-.4px;margin-bottom:6px}
|
||
.mast p{font-size:12px;color:rgba(255,255,255,.6);max-width:820px;line-height:1.7}
|
||
.mast p code{background:rgba(255,255,255,.1);padding:1px 5px;border-radius:2px;font-family:monospace}
|
||
.mast p b{color:#fff}
|
||
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;flex-shrink:0;margin-top:4px;background:#a1dcd8;color:#012851}
|
||
.decisions{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:18px;border-top:1px solid rgba(255,255,255,.1);padding-top:14px}
|
||
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
|
||
.dec-label{font-size:7.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.4);margin-bottom:5px}
|
||
.dec-value{font-size:10px;font-weight:700;color:#fff;line-height:1.5}
|
||
.dec-value s{color:rgba(255,255,255,.3);font-weight:400}
|
||
|
||
.spec-disclaimer{background:#FFFBEB;border:1px solid #FCD34D;border-radius:6px;padding:14px 18px;font-size:11.5px;color:#78350F;line-height:1.6;margin-bottom:32px}
|
||
.spec-disclaimer strong{color:#92400E}
|
||
.spec-disclaimer code{background:#FEF3C7;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:10.5px}
|
||
|
||
.rev-note{background:#FEF2F2;border:1px solid #FCA5A5;border-radius:6px;padding:14px 18px;font-size:11.5px;color:#7F1D1D;line-height:1.6;margin-bottom:32px}
|
||
.rev-note strong{color:#991B1B}
|
||
.rev-note code{background:#FEE2E2;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:10.5px}
|
||
|
||
/* ── TOC ──────────────────────────────────────────── */
|
||
.toc{background:#fff;border:1px solid #DDD8CE;border-radius:8px;padding:18px 22px;margin-bottom:40px}
|
||
.toc-t{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:1px;color:#666;margin-bottom:10px}
|
||
.toc ol{list-style:none;display:grid;grid-template-columns:repeat(2,1fr);gap:6px 24px}
|
||
.toc li{font-size:12px;color:#012851;display:flex;align-items:baseline;gap:8px}
|
||
.toc li b{background:#012851;color:#fff;padding:1px 6px;border-radius:3px;font-size:9px}
|
||
.toc li span{color:#888;font-size:10.5px;margin-left:auto}
|
||
|
||
/* ── Sections ──────────────────────────────────────── */
|
||
.sec{margin-bottom:56px}
|
||
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:48px}
|
||
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#666;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
|
||
.sec-num{background:#012851;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
|
||
.sec-intro{font-size:12.5px;color:#555;line-height:1.65;max-width:820px;margin-bottom:24px}
|
||
.sec-intro b{color:#012851}
|
||
.sec-intro code{background:#EAE7DC;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:11px;color:#012851}
|
||
|
||
/* Tier badges */
|
||
.tier-badge{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:12px;font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;font-family:'Helvetica Neue',Arial,sans-serif;vertical-align:middle}
|
||
.tier-badge.empty{background:#F5F4EF;color:#888;border:1px solid #E4E2D7}
|
||
.tier-badge.compact{background:#E4E2D7;color:#555;border:1px solid #D4D1C3}
|
||
.tier-badge.standard{background:#a1dcd8;color:#012851}
|
||
.tier-badge.rich{background:#012851;color:#fff}
|
||
|
||
/* Data reality table */
|
||
.reality-tbl{width:100%;border-collapse:collapse;background:#fff;border:1px solid #DDD8CE;border-radius:8px;overflow:hidden;font-size:12px;margin-bottom:18px}
|
||
.reality-tbl th{background:#F5F4EF;text-align:left;padding:11px 16px;font-size:9.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#666;border-bottom:1px solid #DDD8CE}
|
||
.reality-tbl td{padding:14px 16px;border-bottom:1px solid #F1ECE0;color:#333;vertical-align:top;line-height:1.6}
|
||
.reality-tbl tr:last-child td{border-bottom:0}
|
||
.reality-tbl td code{background:#F1ECE0;padding:1px 6px;border-radius:3px;font-family:monospace;font-size:11px;color:#012851}
|
||
.reality-tbl td.share{font-family:Georgia,serif;font-size:18px;font-weight:900;color:#012851;letter-spacing:-.4px;width:90px}
|
||
.reality-tbl td.tier-col{width:120px}
|
||
.reality-tbl td.example{color:#666;font-style:italic;font-size:11px}
|
||
|
||
/* ── Wireframe chrome ────────────────────────────── */
|
||
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.08)}
|
||
.wf-bar{height:22px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 8px;gap:4px}
|
||
.dot{width:6px;height:6px;border-radius:50%;background:#C8C4BE;display:inline-block}
|
||
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
|
||
.urlbar{flex:1;height:10px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
|
||
.urlbar span{font-size:7px;color:#888;font-family:monospace}
|
||
.wf.dark{background:#010e1e;border-color:#0d3358}
|
||
.wf.dark .wf-bar{background:#0d2240;border-bottom-color:#081a30}
|
||
.wf.dark .urlbar{background:#012851}
|
||
.wf.dark .urlbar span{color:#7a8a9a}
|
||
.wf-m{width:176px;border-radius:14px}
|
||
.wf-m .wf-bar{display:none}
|
||
.wf-m-status{height:13px;background:#012851;display:flex;align-items:center;justify-content:space-between;padding:0 10px;border-top-left-radius:12px;border-top-right-radius:12px}
|
||
.wf-m-status span{font-size:6px;color:#fff;font-weight:700}
|
||
.wf-m-status .dots{display:flex;gap:2px}
|
||
.wf-m-status .dots i{width:4px;height:4px;border-radius:50%;background:rgba(255,255,255,.6)}
|
||
.wf-t{width:422px}
|
||
.wf-d{width:780px}
|
||
|
||
/* ── State block ──────────────────────────────────── */
|
||
.state-block{background:#fff;border:1px solid #DDD8CE;border-radius:10px;padding:22px;margin-bottom:20px}
|
||
.state-hdr{display:flex;align-items:baseline;gap:12px;margin-bottom:4px;flex-wrap:wrap}
|
||
.state-num{background:#012851;color:#fff;font-size:9px;font-weight:900;padding:3px 7px;border-radius:10px;flex-shrink:0}
|
||
.state-title{font-size:14px;font-weight:800;color:#012851}
|
||
.state-desc{font-size:11.5px;color:#555;line-height:1.6;margin-bottom:18px;max-width:820px}
|
||
.state-desc code{background:#EAE7DC;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:10.5px;color:#012851}
|
||
.state-vps{display:grid;gap:20px;grid-template-columns:min-content min-content 1fr;align-items:flex-start}
|
||
.state-vp-col{display:flex;flex-direction:column;gap:6px;align-items:center}
|
||
.vp-tag{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#fff;background:#012851;padding:3px 8px;border-radius:3px}
|
||
.vp-dim{font-size:8px;color:#888;font-family:monospace;margin-top:-2px}
|
||
|
||
/* ── Person page content (scaled ~55%) ─────────── */
|
||
.pp{background:#ECEAE4;font-family:'Helvetica Neue',Arial,sans-serif;color:#012851}
|
||
.pp.dark{background:#010e1e;color:#f0efe9}
|
||
.pp-gh{height:18px;background:#012851;display:flex;align-items:center;padding:0 10px;gap:8px;border-bottom:1px solid #a1dcd8}
|
||
.pp-gh-logo{font-size:6px;font-weight:900;color:#fff;letter-spacing:.12em;text-transform:uppercase}
|
||
.pp-gh-nav{display:flex;gap:8px;margin-left:auto}
|
||
.pp-gh-nav span{font-size:5.5px;font-weight:600;color:rgba(255,255,255,.55)}
|
||
.pp-gh-nav span.on{color:#fff;border-bottom:1px solid #a1dcd8;padding-bottom:1px}
|
||
.pp-wrap{padding:8px 8px 12px;max-width:100%}
|
||
.wf-t .pp-wrap{padding:14px 18px 18px}
|
||
.wf-d .pp-wrap{padding:18px 28px 22px}
|
||
.pp-back{font-size:5px;color:#888;font-weight:700;text-transform:uppercase;margin-bottom:6px}
|
||
.wf-t .pp-back{font-size:8px;margin-bottom:10px}
|
||
.wf-d .pp-back{font-size:10px;margin-bottom:14px}
|
||
|
||
/* Layout split */
|
||
.pp-grid{display:grid;grid-template-columns:35% 1fr;gap:10px}
|
||
.wf-m .pp-grid{grid-template-columns:1fr;gap:8px}
|
||
.wf-t .pp-grid{gap:14px}
|
||
.wf-d .pp-grid{gap:22px}
|
||
|
||
/* Left: person card (unchanged) */
|
||
.pp-card{background:#fff;border:1px solid #e4e2d7;border-radius:2px;padding:10px 8px;display:flex;flex-direction:column;align-items:center;gap:5px}
|
||
.pp.dark .pp-card{background:#011a30;border-color:#0d3358}
|
||
.wf-t .pp-card{padding:16px 14px;gap:9px}
|
||
.wf-d .pp-card{padding:22px 18px;gap:12px}
|
||
.pp-av{width:28px;height:28px;border-radius:50%;background:#a1dcd8;color:#012851;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:900;font-family:Georgia,serif}
|
||
.pp.dark .pp-av{background:rgba(166,218,216,.15);color:#a1dcd8}
|
||
.wf-t .pp-av{width:50px;height:50px;font-size:16px}
|
||
.wf-d .pp-av{width:72px;height:72px;font-size:22px}
|
||
.pp-name{font-family:Georgia,serif;font-size:7.5px;font-weight:700;color:#012851;text-align:center;line-height:1.3}
|
||
.pp.dark .pp-name{color:#f0efe9}
|
||
.wf-t .pp-name{font-size:13px}
|
||
.wf-d .pp-name{font-size:17px}
|
||
.pp-dates{font-size:5.5px;color:#888;font-weight:700}
|
||
.pp.dark .pp-dates{color:#6e7a8a}
|
||
.wf-t .pp-dates{font-size:9px}
|
||
.wf-d .pp-dates{font-size:11px}
|
||
.pp-actions{display:flex;gap:3px;width:100%;margin-top:4px}
|
||
.wf-t .pp-actions{gap:5px;margin-top:8px}
|
||
.wf-d .pp-actions{gap:6px;margin-top:10px}
|
||
.pp-btn{flex:1;height:12px;background:#f7f5f2;border:1px solid #e4e2d7;border-radius:2px;font-size:4.5px;font-weight:800;color:#444;text-transform:uppercase;display:flex;align-items:center;justify-content:center;gap:2px}
|
||
.pp.dark .pp-btn{background:#0d2240;border-color:#0d3358;color:#d0d6de}
|
||
.pp-btn.primary{background:#012851;color:#fff;border-color:#012851}
|
||
.pp.dark .pp-btn.primary{background:#a1dcd8;color:#012851;border-color:#a1dcd8}
|
||
.wf-t .pp-btn{height:22px;font-size:7.5px}
|
||
.wf-d .pp-btn{height:28px;font-size:9px}
|
||
|
||
/* ── Shared dashboard chrome (all non-Empty tiers) ─── */
|
||
.pp-dash{background:#fff;border:1px solid #e4e2d7;border-radius:2px;overflow:hidden}
|
||
.pp.dark .pp-dash{background:#011a30;border-color:#0d3358}
|
||
.pp-dash-hdr{background:#012851;color:#fff;padding:4px 6px;display:flex;justify-content:space-between;align-items:center;gap:4px}
|
||
.pp.dark .pp-dash-hdr{background:#01223f}
|
||
.wf-t .pp-dash-hdr{padding:9px 14px}
|
||
.wf-d .pp-dash-hdr{padding:12px 18px}
|
||
.pp-dash-hdr h2{font-family:Georgia,serif;font-size:6px;font-weight:700}
|
||
.wf-t .pp-dash-hdr h2{font-size:11px}
|
||
.wf-d .pp-dash-hdr h2{font-size:14px}
|
||
.pp-open-conv{background:#a1dcd8;color:#012851;font-size:4.5px;font-weight:800;padding:1px 4px;border-radius:2px;text-transform:uppercase;letter-spacing:.3px}
|
||
.wf-t .pp-open-conv{font-size:7.5px;padding:4px 9px}
|
||
.wf-d .pp-open-conv{font-size:9.5px;padding:5px 12px}
|
||
|
||
/* Stats strip (Standard + Rich) */
|
||
.pp-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.5px;background:#e4e2d7;border-bottom:1px solid #e4e2d7}
|
||
.pp.dark .pp-stats{background:#0d3358;border-bottom-color:#0d3358}
|
||
.pp-stats div{background:#fafaf5;padding:3px 2px;text-align:center}
|
||
.pp.dark .pp-stats div{background:#011526}
|
||
.pp-stats .v{font-family:Georgia,serif;font-size:7.5px;font-weight:900;color:#012851;letter-spacing:-.3px}
|
||
.pp.dark .pp-stats .v{color:#f0efe9}
|
||
.wf-t .pp-stats div{padding:8px}
|
||
.wf-d .pp-stats div{padding:12px 6px}
|
||
.wf-t .pp-stats .v{font-size:16px}
|
||
.wf-d .pp-stats .v{font-size:22px}
|
||
.pp-stats .v.out{color:#012851}
|
||
.pp-stats .v.in{color:#2F9E95}
|
||
.pp.dark .pp-stats .v.out{color:#a1dcd8}
|
||
.pp.dark .pp-stats .v.in{color:#00c7b1}
|
||
.pp-stats .k{font-size:4px;color:#888;font-weight:800;text-transform:uppercase;letter-spacing:.4px}
|
||
.pp.dark .pp-stats .k{color:#6e7a8a}
|
||
.wf-t .pp-stats .k{font-size:8px}
|
||
.wf-d .pp-stats .k{font-size:10px}
|
||
|
||
/* Dashboard section */
|
||
.pp-dsec{padding:4px 6px;border-top:1px solid #f1ede3}
|
||
.pp.dark .pp-dsec{border-top-color:#092843}
|
||
.pp-dsec:first-of-type{border-top:0}
|
||
.wf-t .pp-dsec{padding:12px 14px}
|
||
.wf-d .pp-dsec{padding:16px 20px}
|
||
.pp-dsec h3{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#888;margin-bottom:4px;display:flex;justify-content:space-between;align-items:baseline}
|
||
.pp.dark .pp-dsec h3{color:#6e7a8a}
|
||
.wf-t .pp-dsec h3{font-size:8px;margin-bottom:8px}
|
||
.wf-d .pp-dsec h3{font-size:10px;margin-bottom:10px}
|
||
.pp-dsec h3 .note{font-size:5px;color:#555;font-weight:600;text-transform:none;letter-spacing:0}
|
||
.pp.dark .pp-dsec h3 .note{color:#9ca3af}
|
||
.wf-t .pp-dsec h3 .note{font-size:8.5px}
|
||
.wf-d .pp-dsec h3 .note{font-size:10.5px}
|
||
.pp-dsec h3 .note b{color:#012851}
|
||
.pp.dark .pp-dsec h3 .note b{color:#f0efe9}
|
||
|
||
/* Direction split */
|
||
.pp-dsplit{display:flex;justify-content:space-between;font-size:5px;font-weight:700;margin-bottom:3px}
|
||
.wf-t .pp-dsplit{font-size:9px;margin-bottom:5px}
|
||
.wf-d .pp-dsplit{font-size:11px;margin-bottom:7px}
|
||
.pp-dsplit .out{color:#012851}
|
||
.pp-dsplit .in{color:#2F9E95}
|
||
.pp.dark .pp-dsplit .out{color:#a1dcd8}
|
||
.pp.dark .pp-dsplit .in{color:#00c7b1}
|
||
.pp-dbar{height:3px;display:flex;border-radius:2px;overflow:hidden;background:#e4e2d7}
|
||
.pp.dark .pp-dbar{background:#0d3358}
|
||
.wf-t .pp-dbar{height:6px}
|
||
.wf-d .pp-dbar{height:8px}
|
||
.pp-dbar .out{background:#012851}
|
||
.pp-dbar .in{background:#2F9E95}
|
||
.pp.dark .pp-dbar .out{background:#a1dcd8}
|
||
.pp.dark .pp-dbar .in{background:#00c7b1}
|
||
|
||
/* Top list */
|
||
.pp-toplist{display:flex;flex-direction:column;gap:2px}
|
||
.wf-t .pp-toplist{gap:4px}
|
||
.wf-d .pp-toplist{gap:6px}
|
||
.pp-ti{display:flex;align-items:center;gap:3px;font-size:5px;padding:1px 2px;border-radius:2px;cursor:pointer}
|
||
.pp-ti:hover{background:#f7f5f2}
|
||
.pp.dark .pp-ti:hover{background:rgba(255,255,255,.04)}
|
||
.wf-t .pp-ti{font-size:9px;gap:6px;padding:2px 4px}
|
||
.wf-d .pp-ti{font-size:11px;gap:9px;padding:3px 6px}
|
||
.pp-ti .nm{flex:1;color:#012851;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.pp.dark .pp-ti .nm{color:#f0efe9}
|
||
.pp-ti .bw{width:34px;height:2px;background:#e4e2d7;border-radius:1px;overflow:hidden;flex-shrink:0}
|
||
.pp.dark .pp-ti .bw{background:#0d3358}
|
||
.wf-t .pp-ti .bw{width:60px;height:4px;border-radius:2px}
|
||
.wf-d .pp-ti .bw{width:90px;height:6px;border-radius:3px}
|
||
.pp-ti .bw span{display:block;height:100%;background:#012851}
|
||
.pp.dark .pp-ti .bw span{background:#a1dcd8}
|
||
.pp-ti .val{width:14px;text-align:right;font-size:4.5px;color:#888;font-weight:700;font-variant-numeric:tabular-nums}
|
||
.pp.dark .pp-ti .val{color:#6e7a8a}
|
||
.wf-t .pp-ti .val{width:22px;font-size:8px}
|
||
.wf-d .pp-ti .val{width:30px;font-size:10px}
|
||
|
||
/* Histogram (Rich only) */
|
||
.pp-hist{display:flex;align-items:flex-end;gap:.5px;height:20px;padding:2px 0 0}
|
||
.pp-hist .bar{flex:1;background:#a1dcd8;opacity:.65;border-radius:.5px .5px 0 0;cursor:pointer}
|
||
.pp.dark .pp-hist .bar{background:#00c7b1;opacity:.6}
|
||
.pp-hist .bar.peak{background:#012851;opacity:.9}
|
||
.pp.dark .pp-hist .bar.peak{background:#a1dcd8;opacity:.9}
|
||
.wf-t .pp-hist{height:50px;gap:1px}
|
||
.wf-d .pp-hist{height:72px;gap:1px}
|
||
.pp-hist-labels{display:flex;justify-content:space-between;font-size:4px;color:#888;margin-top:2px;font-weight:700}
|
||
.wf-t .pp-hist-labels{font-size:8px;margin-top:5px}
|
||
.wf-d .pp-hist-labels{font-size:10px;margin-top:6px}
|
||
|
||
/* Two-col arrangement (Rich only) */
|
||
.pp-twocol{display:grid;grid-template-columns:1fr 1fr;gap:0}
|
||
.pp-twocol > div{border-left:1px solid #f1ede3;padding-left:4px}
|
||
.pp-twocol > div:first-child{border-left:0;padding-left:0}
|
||
.pp.dark .pp-twocol > div{border-left-color:#092843}
|
||
.wf-m .pp-twocol{grid-template-columns:1fr}
|
||
.wf-m .pp-twocol > div{border-left:0;border-top:1px solid #f1ede3;padding-left:0;padding-top:4px;margin-top:4px}
|
||
|
||
/* Tag cloud (Rich only) */
|
||
.pp-cloud{display:flex;flex-wrap:wrap;gap:1px}
|
||
.wf-t .pp-cloud{gap:4px}
|
||
.wf-d .pp-cloud{gap:5px}
|
||
.pp-cloud .tag{cursor:pointer;padding:.5px 2px;background:#a1dcd8;color:#012851;font-weight:700;border-radius:3px;font-size:4.5px}
|
||
.pp.dark .pp-cloud .tag{background:rgba(0,199,177,.2);color:#00c7b1}
|
||
.wf-t .pp-cloud .tag{font-size:8px;padding:2px 7px;border-radius:10px}
|
||
.wf-d .pp-cloud .tag{font-size:10px;padding:2px 9px;border-radius:12px}
|
||
.pp-cloud .tag.xl{font-size:6px}
|
||
.wf-t .pp-cloud .tag.xl{font-size:12px;padding:3px 10px}
|
||
.wf-d .pp-cloud .tag.xl{font-size:14px;padding:3px 11px}
|
||
.pp-cloud .tag.l{font-size:5.5px}
|
||
.wf-t .pp-cloud .tag.l{font-size:10px;padding:2px 8px}
|
||
.wf-d .pp-cloud .tag.l{font-size:12px;padding:3px 10px}
|
||
.pp-cloud .tag.m{font-size:5px}
|
||
.wf-t .pp-cloud .tag.m{font-size:9px}
|
||
.wf-d .pp-cloud .tag.m{font-size:11px}
|
||
.pp-cloud .tag.muted{background:#EEE8DC;color:#666;font-weight:600}
|
||
.pp.dark .pp-cloud .tag.muted{background:rgba(255,255,255,.06);color:#9ca3af}
|
||
|
||
/* Fake document list under the dashboard (existing PersonDocumentList) */
|
||
.pp-doclist{background:#fff;border:1px solid #e4e2d7;border-radius:2px;margin-top:4px;padding:4px 6px}
|
||
.pp.dark .pp-doclist{background:#011a30;border-color:#0d3358}
|
||
.wf-t .pp-doclist{margin-top:10px;padding:10px 14px}
|
||
.wf-d .pp-doclist{margin-top:14px;padding:14px 20px}
|
||
.pp-doclist-h{font-size:5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#888;margin-bottom:3px;display:flex;justify-content:space-between}
|
||
.pp.dark .pp-doclist-h{color:#6e7a8a}
|
||
.wf-t .pp-doclist-h{font-size:8px;margin-bottom:6px}
|
||
.wf-d .pp-doclist-h{font-size:10px;margin-bottom:8px}
|
||
.pp-docrow{display:flex;align-items:center;gap:3px;font-size:5px;padding:2px 0;border-top:1px solid #f5f3ec}
|
||
.pp-docrow:first-child{border-top:0}
|
||
.pp.dark .pp-docrow{border-top-color:#0a1f33}
|
||
.wf-t .pp-docrow{font-size:9px;padding:4px 0;gap:6px}
|
||
.wf-d .pp-docrow{font-size:10.5px;padding:6px 0;gap:9px}
|
||
.pp-docrow .d{color:#888;font-family:monospace;font-size:4.5px;width:26px;flex-shrink:0;font-weight:700}
|
||
.wf-t .pp-docrow .d{font-size:8px;width:52px}
|
||
.wf-d .pp-docrow .d{font-size:10px;width:64px}
|
||
.pp-docrow .t{flex:1;color:#012851;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.pp.dark .pp-docrow .t{color:#f0efe9}
|
||
|
||
/* Empty dashboard */
|
||
.pp-empty{padding:20px 8px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:3px}
|
||
.wf-t .pp-empty{padding:40px 20px;gap:5px}
|
||
.wf-d .pp-empty{padding:60px 40px;gap:8px}
|
||
.pp-empty-t{font-family:Georgia,serif;font-size:7px;font-weight:700;color:#012851}
|
||
.pp.dark .pp-empty-t{color:#f0efe9}
|
||
.wf-t .pp-empty-t{font-size:13px}
|
||
.wf-d .pp-empty-t{font-size:16px}
|
||
.pp-empty-b{font-size:5px;color:#555;line-height:1.55;max-width:130px}
|
||
.pp.dark .pp-empty-b{color:#9ca3af}
|
||
.wf-t .pp-empty-b{font-size:8.5px;max-width:260px}
|
||
.wf-d .pp-empty-b{font-size:11px;max-width:380px}
|
||
|
||
/* Skeleton */
|
||
.sk{background:linear-gradient(90deg,#f5f4ef,#eceae4,#f5f4ef);border-radius:1px;animation:shimmer 1.4s infinite;background-size:200px 100%}
|
||
.pp.dark .sk{background:linear-gradient(90deg,#011a30,#011526,#011a30);background-size:200px 100%}
|
||
@keyframes shimmer{0%{background-position:-200px 0}100%{background-position:200px 0}}
|
||
|
||
/* Callouts */
|
||
.ann{background:#EFF6FF;border:1px solid #BFDBFE;border-radius:6px;padding:14px 16px;margin-top:16px;font-size:11.5px;color:#1E3A5F;line-height:1.6}
|
||
.ann strong{color:#0D2240;display:block;margin-bottom:4px}
|
||
.ann ul{margin-top:4px;padding-left:18px}
|
||
.ann ul li{margin-top:3px}
|
||
.ann code{background:#DBEAFE;padding:1px 5px;border-radius:2px;font-family:monospace;font-size:10.5px}
|
||
|
||
.callout-grid{display:flex;flex-direction:column;gap:10px;padding-top:18px}
|
||
.callout-grid .cg{background:#fff;border:1px solid #DDD8CE;border-radius:6px;padding:12px 14px}
|
||
.callout-grid .cg .cg-t{font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#012851;margin-bottom:4px}
|
||
.callout-grid .cg .cg-b{font-size:10.5px;color:#444;line-height:1.55}
|
||
|
||
/* Impl-ref */
|
||
.impl-ref{background:#0d1117;border-radius:7px;margin-top:16px;overflow:hidden;border:1px solid #30363d}
|
||
.impl-ref-hdr{background:#161b22;padding:8px 16px;font-size:9.5px;font-weight:800;color:#f0883e;border-bottom:1px solid #30363d;display:flex;align-items:center;gap:8px;letter-spacing:.4px;text-transform:uppercase}
|
||
.impl-ref-hdr::before{content:'⚙';font-size:12px}
|
||
.impl-ref-hdr span{color:rgba(240,136,62,.55);font-weight:400;margin-left:auto;font-size:9px;text-transform:none;letter-spacing:0}
|
||
.impl-ref table{width:100%;border-collapse:collapse;font-size:10px}
|
||
.impl-ref th{text-align:left;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#8b949e;padding:7px 14px;border-bottom:1px solid #21262d}
|
||
.impl-ref td{padding:6px 14px;border-bottom:1px solid #161b22;vertical-align:top;line-height:1.5;color:#c9d1d9}
|
||
.impl-ref tr:last-child td{border-bottom:none}
|
||
.impl-ref td:first-child{color:#79c0ff;font-weight:700;white-space:nowrap;width:220px}
|
||
.impl-ref td code{font-family:'SFMono-Regular',Consolas,monospace;font-size:9.5px;background:#161b22;color:#a5d6ff;padding:1px 5px;border-radius:3px}
|
||
.impl-ref .ir-px{color:#7ee787;font-family:monospace;font-size:9.5px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="doc">
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
MASTHEAD
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<header class="mast">
|
||
<div class="mast-top">
|
||
<div>
|
||
<h1>Person Detail — Korrespondenz-Überblick · Tiered Edition</h1>
|
||
<p>Redesign of <code>/persons/[id]</code>. 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, <b>~90 % of persons have ≤ 5 letters</b>, and only <b>4 persons exceed 100</b>. This edition inverts the polarity: one shared dashboard block (navy header + 4-cell stats strip) is the baseline at every tier. <b>Compact</b> ends there and covers ~90 % of pages; <b>Standard</b> and <b>Rich</b> append progressive sections below the stats strip only when data density earns them.</p>
|
||
</div>
|
||
<div class="mast-badge">REV 2 · FINAL</div>
|
||
</div>
|
||
<div class="decisions">
|
||
<div class="dec"><div class="dec-label">Tier thresholds</div><div class="dec-value">Compact ≤ 5 · Standard 6–49 · Rich ≥ 50</div></div>
|
||
<div class="dec"><div class="dec-label">Visual system</div><div class="dec-value">One dashboard block · shared navy header + stats strip at every tier</div></div>
|
||
<div class="dec"><div class="dec-label">Progressive sections</div><div class="dec-value">Standard adds direction + top list · Rich adds histogram + locations + cloud</div></div>
|
||
<div class="dec"><div class="dec-label">Rich-only blocks</div><div class="dec-value">Activity histogram · tag cloud <s>(eager)</s> → lazy-loaded</div></div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="rev-note">
|
||
<strong>What changed from REV 1.</strong> REV 1 shipped a six-block dashboard on every person page. REV 1's "Sparse" state (<code>letterCount < 10</code>) 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 <code>letterCount ≥ 50</code>. Backend aggregations for the heavy blocks are skipped entirely at Compact and Standard tiers.
|
||
</div>
|
||
|
||
<div class="spec-disclaimer">
|
||
<strong>Reading this spec.</strong> Mockups in Sections 03–05 are scaled to ~55 % of real pixel values so that multiple viewports fit on one page. <strong>Never copy pixel sizes from the mockups.</strong> Use the <code>impl-ref</code> tables for exact Tailwind classes and real pixel values.
|
||
</div>
|
||
|
||
<!-- TOC -->
|
||
<div class="toc">
|
||
<div class="toc-t">Inhalt</div>
|
||
<ol>
|
||
<li><b>01</b> Data reality & design response <span>why we rebuilt this</span></li>
|
||
<li><b>02</b> Tier decision logic <span>thresholds · what each tier shows</span></li>
|
||
<li><b>03</b> Compact tier — the 90 % case <span>3 viewports</span></li>
|
||
<li><b>04</b> Standard tier — middle volume <span>3 viewports</span></li>
|
||
<li><b>05</b> Rich tier — archival giants <span>3 viewports · 4 persons qualify</span></li>
|
||
<li><b>06</b> Empty & loading states <span>new person · in-flight</span></li>
|
||
<li><b>07</b> Accessibility contract <span>WCAG AA/AAA</span></li>
|
||
<li><b>08</b> Implementation notes <span>tiered API · lazy components · shipping order</span></li>
|
||
</ol>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 01 — DATA REALITY
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">01</span>Data Reality — Why We Rebuilt This</h2>
|
||
<p class="sec-intro">A good dashboard respects the shape of the archive it describes. As of <b>2026-04-23</b> the distribution of letters per person in the Familienarchiv looks like this:</p>
|
||
|
||
<table class="reality-tbl">
|
||
<thead>
|
||
<tr><th>Tier</th><th>Criterion</th><th>Share</th><th>Example</th><th>What it needs</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td class="tier-col"><span class="tier-badge empty">Empty</span></td>
|
||
<td><code>letterCount == 0</code></td>
|
||
<td class="share">—</td>
|
||
<td class="example">Newly imported person · no documents yet</td>
|
||
<td>Reassurance card. Charts of nothing are noise.</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="tier-col"><span class="tier-badge compact">Compact</span></td>
|
||
<td><code>1 ≤ letterCount ≤ 5</code></td>
|
||
<td class="share">~ 90 %</td>
|
||
<td class="example">Elsbeth Brandt · 3 letters · 1919–1922</td>
|
||
<td>A single stat line. The sent/received lists below already answer "which letters?".</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="tier-col"><span class="tier-badge standard">Standard</span></td>
|
||
<td><code>6 ≤ letterCount ≤ 49</code></td>
|
||
<td class="share">~ 6 %</td>
|
||
<td class="example">Herbert Cram · 18 letters · 8 correspondents</td>
|
||
<td>Stats strip · direction bar · top‑5 correspondents tile list.</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="tier-col"><span class="tier-badge rich">Rich</span></td>
|
||
<td><code>letterCount ≥ 50</code></td>
|
||
<td class="share">< 1 %</td>
|
||
<td class="example">Walter de Gruyter · 851 letters · 87 corresp. · 42 years</td>
|
||
<td>Everything in Standard <em>plus</em> activity histogram · top locations · tag cloud.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="ann">
|
||
<strong>The cost of REV 1 and the savings of REV 2.</strong>
|
||
<ul>
|
||
<li>REV 1 planned six eager components on every person page — stats strip, activity histogram, direction split, top correspondents, top locations, tag cloud.</li>
|
||
<li>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.</li>
|
||
<li>Backend aggregations for the heavy blocks (<code>activityByYear</code>, <code>topLocations</code>, <code>topTags</code>) ran for every person page load regardless of relevance.</li>
|
||
<li>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.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 02 — TIER DECISION LOGIC
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">02</span>Tier Decision Logic</h2>
|
||
<p class="sec-intro">The tier is derived from one number: <code>letterCount = outCount + inCount</code>. 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.</p>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Tier thresholds<span>server + client use the same rule</span></div>
|
||
<table>
|
||
<thead><tr><th>letterCount</th><th>Tier</th><th>Blocks shown</th><th>Expensive queries?</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>0</td><td>EMPTY</td><td>Reassurance card only</td><td>No</td></tr>
|
||
<tr><td>1 – 5</td><td>COMPACT</td><td>Shared dashboard chrome: header + CTA + 4-cell stats strip</td><td>No</td></tr>
|
||
<tr><td>6 – 49</td><td>STANDARD</td><td>Stats strip · direction bar · top correspondents (≤ 5)</td><td>Partial: top correspondents only</td></tr>
|
||
<tr><td>≥ 50</td><td>RICH</td><td>All of Standard · activity histogram · top locations · tag cloud</td><td>Yes: full set</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="ann">
|
||
<strong>Why 5 and why 50?</strong>
|
||
<ul>
|
||
<li><b>5</b> is the point below which a histogram has fewer bars than x-axis ticks, a "top list" is just <em>the</em> list, and a tag cloud shows ≤ 3 tags total. Below 5 letters, the sent/received document lists immediately below the stats strip <em>are</em> the per-letter detail view.</li>
|
||
<li><b>50</b> 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.</li>
|
||
<li>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.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 03 — COMPACT TIER
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">03</span>Compact Tier <span class="tier-badge compact">Compact · ~90 %</span></h2>
|
||
<p class="sec-intro">The <b>shared dashboard chrome</b> 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 <code>PersonDocumentList</code> components below are unchanged and carry the per-letter detail.</p>
|
||
|
||
<div class="state-block">
|
||
<div class="state-hdr"><span class="state-num">01</span><span class="state-title">Elsbeth Brandt · 3 Briefe · 1919 – 1922</span> <span class="tier-badge compact">Compact</span></div>
|
||
<div class="state-desc">Three total letters — two sent, one received, across a 4-year span. The dashboard renders header + stats and stops. The four stat cells (<code>gesamt · ausgehend · eingehend · Jahre</code>) 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.</div>
|
||
<div class="state-vps">
|
||
<!-- 320 -->
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
|
||
<div class="wf wf-m">
|
||
<div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
|
||
<div class="pp">
|
||
<div class="pp-gh"><div class="pp-gh-logo">FA</div><div class="pp-gh-nav"><span class="on">Personen</span></div></div>
|
||
<div class="pp-wrap">
|
||
<div class="pp-back">← Zurück</div>
|
||
<div class="pp-grid">
|
||
<div class="pp-card"><div class="pp-av">EB</div><div class="pp-name">Elsbeth Brandt</div><div class="pp-dates">1890 – 1963</div><div class="pp-actions"><div class="pp-btn primary">Briefwechsel</div></div></div>
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Überblick</h2><a class="pp-open-conv">↗</a></div>
|
||
<div class="pp-stats">
|
||
<div><div class="v">3</div><div class="k">ges.</div></div>
|
||
<div><div class="v out">2</div><div class="k">→</div></div>
|
||
<div><div class="v in">1</div><div class="k">←</div></div>
|
||
<div><div class="v">4J</div><div class="k">Jahre</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="pp-doclist">
|
||
<div class="pp-doclist-h"><span>Gesendet · 2</span></div>
|
||
<div class="pp-docrow"><span class="d">1919</span><span class="t">Brief an W.</span></div>
|
||
<div class="pp-docrow"><span class="d">1922</span><span class="t">Brief an H.</span></div>
|
||
</div>
|
||
<div class="pp-doclist">
|
||
<div class="pp-doclist-h"><span>Empfangen · 1</span></div>
|
||
<div class="pp-docrow"><span class="d">1920</span><span class="t">Antwort</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 768 -->
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
|
||
<div class="wf wf-t">
|
||
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/elsbeth</span></div></div>
|
||
<div class="pp">
|
||
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span><span>Briefwechsel</span></div></div>
|
||
<div class="pp-wrap">
|
||
<div class="pp-back">← Zurück</div>
|
||
<div class="pp-grid">
|
||
<div class="pp-card"><div class="pp-av">EB</div><div class="pp-name">Elsbeth Brandt</div><div class="pp-dates">1890 – 1963</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
|
||
<div style="display:flex;flex-direction:column;gap:0">
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Öffnen</a></div>
|
||
<div class="pp-stats">
|
||
<div><div class="v">3</div><div class="k">gesamt</div></div>
|
||
<div><div class="v out">2</div><div class="k">ausgehend</div></div>
|
||
<div><div class="v in">1</div><div class="k">eingehend</div></div>
|
||
<div><div class="v">4</div><div class="k">Jahre</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="pp-doclist">
|
||
<div class="pp-doclist-h"><span>Gesendet · 2</span><span>Empfangen · 1</span></div>
|
||
<div class="pp-docrow"><span class="d">26.03.1919</span><span class="t">An Walter de Gruyter — Genesungswünsche</span></div>
|
||
<div class="pp-docrow"><span class="d">14.08.1922</span><span class="t">An Herbert Cram — Einladung</span></div>
|
||
<div class="pp-docrow"><span class="d">02.11.1920</span><span class="t">Von Walter de Gruyter — Dank</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 1440 -->
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
|
||
<div class="wf wf-d">
|
||
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/elsbeth-…</span></div></div>
|
||
<div class="pp">
|
||
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span>Dokumente</span><span class="on">Personen</span><span>Briefwechsel</span><span>Chronik</span></div></div>
|
||
<div class="pp-wrap">
|
||
<div class="pp-back">← Zurück</div>
|
||
<div class="pp-grid">
|
||
<div class="pp-card"><div class="pp-av">EB</div><div class="pp-name">Elsbeth Brandt</div><div class="pp-dates">1890 – 1963</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
|
||
<div style="display:flex;flex-direction:column;gap:0">
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Briefwechsel öffnen</a></div>
|
||
<div class="pp-stats">
|
||
<div><div class="v">3</div><div class="k">Briefe gesamt</div></div>
|
||
<div><div class="v out">2</div><div class="k">ausgehend</div></div>
|
||
<div><div class="v in">1</div><div class="k">eingehend</div></div>
|
||
<div><div class="v">4</div><div class="k">Jahre</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="pp-doclist">
|
||
<div class="pp-doclist-h"><span>Gesendet · 2 Briefe</span></div>
|
||
<div class="pp-docrow"><span class="d">26.03.1919</span><span class="t">An Walter de Gruyter — Genesungswünsche nach Lazarettaufenthalt</span></div>
|
||
<div class="pp-docrow"><span class="d">14.08.1922</span><span class="t">An Herbert Cram — Einladung zur Sommerreise nach Bad Kissingen</span></div>
|
||
</div>
|
||
<div class="pp-doclist">
|
||
<div class="pp-doclist-h"><span>Empfangen · 1 Brief</span></div>
|
||
<div class="pp-docrow"><span class="d">02.11.1920</span><span class="t">Von Walter de Gruyter — Dank für Geburtstagsgrüße</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ann">
|
||
<strong>Why not more sections?</strong> 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.
|
||
</div>
|
||
<div class="ann">
|
||
<strong>Design invariant.</strong> The dashboard block is <em>the same component</em> across Compact, Standard, and Rich — same chrome, same stats strip as the always-rendered baseline. Higher tiers append more sections <em>below</em> 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.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Shared dashboard chrome<span>renders at every non-Empty tier · the Compact tier renders only this</span></div>
|
||
<table>
|
||
<thead><tr><th>Part</th><th>Classes</th><th>Real</th><th>Note</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Container</td><td><code>overflow-hidden rounded-sm border border-line bg-surface shadow-sm</code></td><td class="ir-px">1 px border</td><td>Matches the card pattern from CLAUDE.md</td></tr>
|
||
<tr><td>Header strip</td><td><code>flex items-center justify-between gap-3 bg-primary text-primary-fg px-5 py-3</code></td><td class="ir-px">12 px y padding</td><td>Dark navy — the visual anchor that makes this block recognisable at every tier</td></tr>
|
||
<tr><td>Title</td><td><code>font-serif text-base font-bold</code> · text: <code>m.person_dashboard_title()</code> → "Korrespondenz-Überblick"</td><td class="ir-px">16 px / 700</td><td>Merriweather. Abbreviates to "Überblick" at < 640 px</td></tr>
|
||
<tr><td>CTA "↗ Briefwechsel öffnen"</td><td><code>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</code></td><td class="ir-px">44 px min</td><td>WCAG 2.2 touch target. Mobile shows "↗" only; tablet+ shows the full label</td></tr>
|
||
<tr><td>Stats strip container</td><td><code>grid grid-cols-2 sm:grid-cols-4 gap-px bg-line</code></td><td class="ir-px">1 px gap</td><td>Separators are the parent background showing through</td></tr>
|
||
<tr><td>Stat cell</td><td><code>bg-muted px-4 py-3.5 text-center</code></td><td class="ir-px">14 px y padding</td><td>Uses <code>bg-muted</code> (not <code>bg-surface</code>) so the gap-px separators read</td></tr>
|
||
<tr><td>Stat number</td><td><code>font-serif text-[22px] font-black leading-none tabular-nums tracking-tight</code></td><td class="ir-px">22 px / 900</td><td>Merriweather Black · <code>.out</code> = primary, <code>.in</code> = accent-fg</td></tr>
|
||
<tr><td>Stat label</td><td><code>mt-1 text-[10px] font-bold uppercase tracking-wide text-ink-3</code></td><td class="ir-px">10 px</td><td>Responsive abbreviation: "Briefe gesamt" → "gesamt" → "ges." via Paraglide <code>_short</code> keys</td></tr>
|
||
<tr><td>Semantic wrapper</td><td><code><dl></code> with <code><dt>/<dd></code> pairs</td><td class="ir-px">—</td><td>Screen readers announce "Briefe gesamt: 3, ausgehend: 2, eingehend: 1, Jahre: 4"</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 04 — STANDARD TIER
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">04</span>Standard Tier <span class="tier-badge standard">Standard · 6–49</span></h2>
|
||
<p class="sec-intro">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 <code>CoCorrespondentsList</code> in this tier.</p>
|
||
|
||
<div class="state-block">
|
||
<div class="state-hdr"><span class="state-num">02</span><span class="state-title">Herbert Cram · 18 Briefe · 1905 – 1934</span> <span class="tier-badge standard">Standard</span></div>
|
||
<div class="state-desc">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.</div>
|
||
<div class="state-vps">
|
||
<!-- 320 -->
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
|
||
<div class="wf wf-m">
|
||
<div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
|
||
<div class="pp">
|
||
<div class="pp-gh"><div class="pp-gh-logo">FA</div><div class="pp-gh-nav"><span class="on">Personen</span></div></div>
|
||
<div class="pp-wrap">
|
||
<div class="pp-back">← Zurück</div>
|
||
<div class="pp-grid">
|
||
<div class="pp-card"><div class="pp-av">HC</div><div class="pp-name">Herbert Cram</div><div class="pp-dates">1881 – 1967</div><div class="pp-actions"><div class="pp-btn primary">Briefwechsel</div></div></div>
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Überblick</h2><a class="pp-open-conv">↗</a></div>
|
||
<div class="pp-stats">
|
||
<div><div class="v">18</div><div class="k">ges.</div></div>
|
||
<div><div class="v out">11</div><div class="k">→</div></div>
|
||
<div><div class="v in">7</div><div class="k">←</div></div>
|
||
<div><div class="v">30J</div><div class="k">Jahre</div></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Richtung</h3>
|
||
<div class="pp-dsplit"><span class="out">→ 61%</span><span class="in">← 39%</span></div>
|
||
<div class="pp-dbar"><span class="out" style="width:61%"></span><span class="in" style="width:39%"></span></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Top Korrespondenten</h3>
|
||
<div class="pp-toplist">
|
||
<div class="pp-ti"><span class="nm">W. de Gruyter</span><span class="val">6</span></div>
|
||
<div class="pp-ti"><span class="nm">E. Brandt</span><span class="val">4</span></div>
|
||
<div class="pp-ti"><span class="nm">G. Rofden</span><span class="val">3</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 768 -->
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
|
||
<div class="wf wf-t">
|
||
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/herbert-cram</span></div></div>
|
||
<div class="pp">
|
||
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span><span>Briefwechsel</span></div></div>
|
||
<div class="pp-wrap">
|
||
<div class="pp-back">← Zurück</div>
|
||
<div class="pp-grid">
|
||
<div class="pp-card"><div class="pp-av">HC</div><div class="pp-name">Herbert Cram</div><div class="pp-dates">1881 – 1967</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Öffnen</a></div>
|
||
<div class="pp-stats">
|
||
<div><div class="v">18</div><div class="k">gesamt</div></div>
|
||
<div><div class="v out">11</div><div class="k">ausgehend</div></div>
|
||
<div><div class="v in">7</div><div class="k">eingehend</div></div>
|
||
<div><div class="v">30</div><div class="k">Jahre</div></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Richtungsverteilung</h3>
|
||
<div class="pp-dsplit"><span class="out">→ 11 · 61%</span><span class="in">← 7 · 39%</span></div>
|
||
<div class="pp-dbar"><span class="out" style="width:61%"></span><span class="in" style="width:39%"></span></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Top Korrespondenten <span class="note">5 von 8</span></h3>
|
||
<div class="pp-toplist">
|
||
<div class="pp-ti"><span class="nm">Walter de Gruyter</span><span class="bw"><span style="width:100%"></span></span><span class="val">6</span></div>
|
||
<div class="pp-ti"><span class="nm">Elsbeth Brandt</span><span class="bw"><span style="width:67%"></span></span><span class="val">4</span></div>
|
||
<div class="pp-ti"><span class="nm">Gertrud Rofden</span><span class="bw"><span style="width:50%"></span></span><span class="val">3</span></div>
|
||
<div class="pp-ti"><span class="nm">Eugenie de Gruyter</span><span class="bw"><span style="width:33%"></span></span><span class="val">2</span></div>
|
||
<div class="pp-ti"><span class="nm">Käthe Dieckmann</span><span class="bw"><span style="width:17%"></span></span><span class="val">1</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 1440 -->
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
|
||
<div class="wf wf-d">
|
||
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/herbert-cram-…</span></div></div>
|
||
<div class="pp">
|
||
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span>Dokumente</span><span class="on">Personen</span><span>Briefwechsel</span><span>Chronik</span></div></div>
|
||
<div class="pp-wrap">
|
||
<div class="pp-back">← Zurück</div>
|
||
<div class="pp-grid">
|
||
<div class="pp-card"><div class="pp-av">HC</div><div class="pp-name">Herbert Cram</div><div class="pp-dates">1881 – 1967</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Briefwechsel öffnen</a></div>
|
||
<div class="pp-stats">
|
||
<div><div class="v">18</div><div class="k">Briefe gesamt</div></div>
|
||
<div><div class="v out">11</div><div class="k">ausgehend</div></div>
|
||
<div><div class="v in">7</div><div class="k">eingehend</div></div>
|
||
<div><div class="v">30</div><div class="k">Jahre</div></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Richtungsverteilung</h3>
|
||
<div class="pp-dsplit"><span class="out">→ 11 ausgehend · 61%</span><span class="in">← 7 eingehend · 39%</span></div>
|
||
<div class="pp-dbar"><span class="out" style="width:61%"></span><span class="in" style="width:39%"></span></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Top Korrespondenten <span class="note">5 von 8 · Zeitraum 1905 – 1934</span></h3>
|
||
<div class="pp-toplist">
|
||
<div class="pp-ti"><span class="nm">Walter de Gruyter</span><span class="bw"><span style="width:100%"></span></span><span class="val">6</span></div>
|
||
<div class="pp-ti"><span class="nm">Elsbeth Brandt</span><span class="bw"><span style="width:67%"></span></span><span class="val">4</span></div>
|
||
<div class="pp-ti"><span class="nm">Gertrud von Rofden</span><span class="bw"><span style="width:50%"></span></span><span class="val">3</span></div>
|
||
<div class="pp-ti"><span class="nm">Eugenie de Gruyter</span><span class="bw"><span style="width:33%"></span></span><span class="val">2</span></div>
|
||
<div class="pp-ti"><span class="nm">Käthe Dieckmann</span><span class="bw"><span style="width:17%"></span></span><span class="val">1</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ann">
|
||
<strong>Why no histogram at this tier?</strong> 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.
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 05 — RICH TIER
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">05</span>Rich Tier <span class="tier-badge rich">Rich · ≥ 50</span></h2>
|
||
<p class="sec-intro">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 <em>only</em> tier that warrants the heavy lift.</p>
|
||
|
||
<div class="state-block">
|
||
<div class="state-hdr"><span class="state-num">03</span><span class="state-title">Walter de Gruyter · 851 Briefe · 1898 – 1940 · 87 Korrespondenten</span> <span class="tier-badge rich">Rich</span></div>
|
||
<div class="state-desc">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 <code>/briefwechsel</code> with filters pre-applied.</div>
|
||
<div class="state-vps">
|
||
<!-- 320 -->
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
|
||
<div class="wf wf-m">
|
||
<div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
|
||
<div class="pp">
|
||
<div class="pp-gh"><div class="pp-gh-logo">FA</div><div class="pp-gh-nav"><span class="on">Personen</span></div></div>
|
||
<div class="pp-wrap">
|
||
<div class="pp-back">← Zurück</div>
|
||
<div class="pp-grid">
|
||
<div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 – 1923</div><div class="pp-actions"><div class="pp-btn primary">Briefwechsel</div></div></div>
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Überblick</h2><a class="pp-open-conv">↗</a></div>
|
||
<div class="pp-stats">
|
||
<div><div class="v">851</div><div class="k">ges.</div></div>
|
||
<div><div class="v out">612</div><div class="k">→</div></div>
|
||
<div><div class="v in">239</div><div class="k">←</div></div>
|
||
<div><div class="v">42J</div><div class="k">Jahre</div></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Aktivität</h3>
|
||
<div class="pp-hist"><div class="bar" style="height:15%"></div><div class="bar" style="height:40%"></div><div class="bar" style="height:60%"></div><div class="bar" style="height:75%"></div><div class="bar peak" style="height:100%"></div><div class="bar" style="height:70%"></div><div class="bar" style="height:40%"></div><div class="bar" style="height:20%"></div><div class="bar" style="height:8%"></div></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Top</h3>
|
||
<div class="pp-toplist">
|
||
<div class="pp-ti"><span class="nm">W. Dieckmann</span><span class="val">184</span></div>
|
||
<div class="pp-ti"><span class="nm">H. Cram</span><span class="val">143</span></div>
|
||
<div class="pp-ti"><span class="nm">E. Dieckmann</span><span class="val">88</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Schlagwörter</h3>
|
||
<div class="pp-cloud"><span class="tag xl">Verlag</span><span class="tag l">Familie</span><span class="tag">Geburtstag</span><span class="tag m">Kur</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 768 -->
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
|
||
<div class="wf wf-t">
|
||
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/walter-de-gruyter</span></div></div>
|
||
<div class="pp">
|
||
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span><span>Briefwechsel</span></div></div>
|
||
<div class="pp-wrap">
|
||
<div class="pp-back">← Zurück</div>
|
||
<div class="pp-grid">
|
||
<div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 – 1923</div><div class="pp-actions"><div class="pp-btn">Edit</div><div class="pp-btn primary">Briefwechsel</div></div></div>
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Öffnen</a></div>
|
||
<div class="pp-stats">
|
||
<div><div class="v">851</div><div class="k">gesamt</div></div>
|
||
<div><div class="v out">612</div><div class="k">ausgehend</div></div>
|
||
<div><div class="v in">239</div><div class="k">eingehend</div></div>
|
||
<div><div class="v">42</div><div class="k">Jahre</div></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Aktivität <span class="note"><b>1922 · 78</b></span></h3>
|
||
<div class="pp-hist"><div class="bar" style="height:10%"></div><div class="bar" style="height:20%"></div><div class="bar" style="height:30%"></div><div class="bar" style="height:42%"></div><div class="bar" style="height:55%"></div><div class="bar" style="height:68%"></div><div class="bar" style="height:82%"></div><div class="bar peak" style="height:100%"></div><div class="bar" style="height:72%"></div><div class="bar" style="height:56%"></div><div class="bar" style="height:42%"></div><div class="bar" style="height:30%"></div><div class="bar" style="height:20%"></div><div class="bar" style="height:12%"></div><div class="bar" style="height:6%"></div></div>
|
||
<div class="pp-hist-labels"><span>1898</span><span>1922</span><span>1940</span></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Richtung</h3>
|
||
<div class="pp-dsplit"><span class="out">→ 612 · 72%</span><span class="in">← 239 · 28%</span></div>
|
||
<div class="pp-dbar"><span class="out" style="width:72%"></span><span class="in" style="width:28%"></span></div>
|
||
</div>
|
||
<div class="pp-twocol">
|
||
<div class="pp-dsec">
|
||
<h3>Top Korresp.</h3>
|
||
<div class="pp-toplist">
|
||
<div class="pp-ti"><span class="nm">W. Dieckmann</span><span class="bw"><span style="width:100%"></span></span><span class="val">184</span></div>
|
||
<div class="pp-ti"><span class="nm">H. Cram</span><span class="bw"><span style="width:78%"></span></span><span class="val">143</span></div>
|
||
<div class="pp-ti"><span class="nm">E. Dieckmann</span><span class="bw"><span style="width:48%"></span></span><span class="val">88</span></div>
|
||
<div class="pp-ti"><span class="nm">E. de Gruyter</span><span class="bw"><span style="width:42%"></span></span><span class="val">77</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Top Orte</h3>
|
||
<div class="pp-toplist">
|
||
<div class="pp-ti"><span class="nm">Berlin</span><span class="bw"><span style="width:100%"></span></span><span class="val">412</span></div>
|
||
<div class="pp-ti"><span class="nm">B.Lichterfelde</span><span class="bw"><span style="width:44%"></span></span><span class="val">180</span></div>
|
||
<div class="pp-ti"><span class="nm">Bad Kissingen</span><span class="bw"><span style="width:14%"></span></span><span class="val">58</span></div>
|
||
<div class="pp-ti"><span class="nm">Cöln</span><span class="bw"><span style="width:9%"></span></span><span class="val">37</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Schlagwörter</h3>
|
||
<div class="pp-cloud"><span class="tag xl">Verlag</span><span class="tag xl">Familie</span><span class="tag l">Geburtstag</span><span class="tag l">Weihnachten</span><span class="tag m">Kur</span><span class="tag m">Reise</span><span class="tag">Krieg</span><span class="tag muted">Krankheit</span><span class="tag muted">Tod</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 1440 -->
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
|
||
<div class="wf wf-d">
|
||
<div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/walter-de-gruyter-…</span></div></div>
|
||
<div class="pp">
|
||
<div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span>Dokumente</span><span class="on">Personen</span><span>Briefwechsel</span><span>Chronik</span></div></div>
|
||
<div class="pp-wrap">
|
||
<div class="pp-back">← Zurück</div>
|
||
<div class="pp-grid">
|
||
<div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 – 1923</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div><div class="pp-btn primary">Briefwechsel</div></div></div>
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Briefwechsel öffnen</a></div>
|
||
<div class="pp-stats">
|
||
<div><div class="v">851</div><div class="k">Briefe gesamt</div></div>
|
||
<div><div class="v out">612</div><div class="k">ausgehend</div></div>
|
||
<div><div class="v in">239</div><div class="k">eingehend</div></div>
|
||
<div><div class="v">42</div><div class="k">Jahre</div></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Aktivität über die Jahre <span class="note">Spitzenjahr <b>1922 · 78 Briefe</b></span></h3>
|
||
<div class="pp-hist"><div class="bar" style="height:12%"></div><div class="bar" style="height:18%"></div><div class="bar" style="height:26%"></div><div class="bar" style="height:38%"></div><div class="bar" style="height:44%"></div><div class="bar" style="height:52%"></div><div class="bar" style="height:60%"></div><div class="bar" style="height:68%"></div><div class="bar" style="height:80%"></div><div class="bar" style="height:88%"></div><div class="bar peak" style="height:100%"></div><div class="bar" style="height:72%"></div><div class="bar" style="height:58%"></div><div class="bar" style="height:48%"></div><div class="bar" style="height:38%"></div><div class="bar" style="height:28%"></div><div class="bar" style="height:22%"></div><div class="bar" style="height:18%"></div><div class="bar" style="height:14%"></div><div class="bar" style="height:10%"></div><div class="bar" style="height:6%"></div><div class="bar" style="height:4%"></div><div class="bar" style="height:2%"></div></div>
|
||
<div class="pp-hist-labels"><span>1898</span><span>1922</span><span>1940</span></div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Richtungsverteilung</h3>
|
||
<div class="pp-dsplit"><span class="out">→ 612 ausgehend · 72%</span><span class="in">← 239 eingehend · 28%</span></div>
|
||
<div class="pp-dbar"><span class="out" style="width:72%"></span><span class="in" style="width:28%"></span></div>
|
||
</div>
|
||
<div class="pp-twocol">
|
||
<div class="pp-dsec">
|
||
<h3>Top Korrespondenten <span class="note">6 von 87</span></h3>
|
||
<div class="pp-toplist">
|
||
<div class="pp-ti"><span class="nm">Walter Dieckmann</span><span class="bw"><span style="width:100%"></span></span><span class="val">184</span></div>
|
||
<div class="pp-ti"><span class="nm">Herbert Cram</span><span class="bw"><span style="width:78%"></span></span><span class="val">143</span></div>
|
||
<div class="pp-ti"><span class="nm">Ella Dieckmann</span><span class="bw"><span style="width:48%"></span></span><span class="val">88</span></div>
|
||
<div class="pp-ti"><span class="nm">Eugenie de Gruyter</span><span class="bw"><span style="width:42%"></span></span><span class="val">77</span></div>
|
||
<div class="pp-ti"><span class="nm">Gertrud von Rofden</span><span class="bw"><span style="width:32%"></span></span><span class="val">58</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Top Orte <span class="note">5 von 42</span></h3>
|
||
<div class="pp-toplist">
|
||
<div class="pp-ti"><span class="nm">Berlin</span><span class="bw"><span style="width:100%"></span></span><span class="val">412</span></div>
|
||
<div class="pp-ti"><span class="nm">B.Lichterfelde</span><span class="bw"><span style="width:44%"></span></span><span class="val">180</span></div>
|
||
<div class="pp-ti"><span class="nm">Bad Kissingen</span><span class="bw"><span style="width:14%"></span></span><span class="val">58</span></div>
|
||
<div class="pp-ti"><span class="nm">Cöln</span><span class="bw"><span style="width:9%"></span></span><span class="val">37</span></div>
|
||
<div class="pp-ti"><span class="nm">Belgard</span><span class="bw"><span style="width:6%"></span></span><span class="val">26</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="pp-dsec">
|
||
<h3>Beliebte Schlagwörter <span class="note">Klick filtert den Briefwechsel</span></h3>
|
||
<div class="pp-cloud"><span class="tag xl">Verlag</span><span class="tag xl">Familie</span><span class="tag l">Geburtstag</span><span class="tag l">Weihnachten</span><span class="tag m">Kuraufenthalt</span><span class="tag m">Reise</span><span class="tag m">Geschäft</span><span class="tag">Krieg</span><span class="tag muted">Krankheit</span><span class="tag muted">Schule</span><span class="tag muted">Hochzeit</span><span class="tag muted">Neujahr</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ann">
|
||
<strong>Deep-link grammar (unchanged from REV 1).</strong> Every Rich-tier tile is a link into <code>/briefwechsel</code> with filters applied: histogram bar → <code>?senderId={id}&from={y}-01-01&to={y}-12-31</code> · correspondent row → <code>?senderId={id}&receiverId={other}</code> · location row → <code>?senderId={id}&location={slug}</code> · tag chip → <code>?senderId={id}&tagId={tagId}</code>. Requires new <code>direction</code>, <code>location</code>, <code>tagId</code> query params on <code>/briefwechsel</code>.
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 06 — EMPTY & LOADING
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">06</span>Empty & Loading States</h2>
|
||
<p class="sec-intro">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 <code>/api/persons/{id}</code> (fast), dashboard slot shows a single skeleton shape sized for the likely tier.</p>
|
||
|
||
<!-- EMPTY -->
|
||
<div class="state-block">
|
||
<div class="state-hdr"><span class="state-num">04</span><span class="state-title">Empty — person has no letters yet</span> <span class="tier-badge empty">Empty</span></div>
|
||
<div class="state-desc">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.</div>
|
||
<div class="state-vps">
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
|
||
<div class="wf wf-m"><div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
|
||
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">FA</div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">NN</div><div class="pp-name">Neue Person</div><div class="pp-dates">—</div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Überblick</h2></div><div class="pp-empty"><div class="pp-empty-t">Noch keine Briefe</div><div class="pp-empty-b">Sobald ein Brief zugewiesen wird, erscheint der Überblick hier.</div></div></div></div></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
|
||
<div class="wf wf-t"><div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/new-id</span></div></div>
|
||
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span></div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">NN</div><div class="pp-name">Neue Person</div><div class="pp-dates">—</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div></div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2></div><div class="pp-empty"><div class="pp-empty-t">Noch keine Briefe</div><div class="pp-empty-b">Diese Person hat noch keine Korrespondenz im Archiv. Sobald ein Brief als Absender oder Empfänger zugewiesen wird, erscheint der Überblick hier automatisch.</div></div></div></div></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
|
||
<div class="wf wf-d"><div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/new-id</span></div></div>
|
||
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div><div class="pp-gh-nav"><span class="on">Personen</span><span>Briefwechsel</span></div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">NN</div><div class="pp-name">Neue Person</div><div class="pp-dates">—</div><div class="pp-actions"><div class="pp-btn">Bearbeiten</div></div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2></div><div class="pp-empty"><div class="pp-empty-t">Noch keine Briefe</div><div class="pp-empty-b">Diese Person hat noch keine Korrespondenz im Archiv. Sobald ein Brief als Absender oder Empfänger zugewiesen wird, erscheint der Überblick hier automatisch.</div></div></div></div></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- LOADING -->
|
||
<div class="state-block">
|
||
<div class="state-hdr"><span class="state-num">05</span><span class="state-title">Loading — skeleton sized for the expected tier</span></div>
|
||
<div class="state-desc">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.</div>
|
||
<div class="state-vps">
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">320 px · Mobile</span><span class="vp-dim">176 px @ 55%</span>
|
||
<div class="wf wf-m"><div class="wf-m-status"><span>9:41</span><div class="dots"><i></i><i></i><i></i></div></div>
|
||
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">FA</div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 – 1923</div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Überblick</h2></div><div class="pp-stats"><div><div class="sk" style="height:9px;width:14px;margin:auto"></div></div><div><div class="sk" style="height:9px;width:14px;margin:auto"></div></div><div><div class="sk" style="height:9px;width:14px;margin:auto"></div></div><div><div class="sk" style="height:9px;width:14px;margin:auto"></div></div></div></div></div></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">768 px · Tablet</span><span class="vp-dim">422 px @ 55%</span>
|
||
<div class="wf wf-t"><div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>…/persons/…</span></div></div>
|
||
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 – 1923</div><div class="pp-actions"><div class="pp-btn">Edit</div><div class="pp-btn primary">Briefwechsel</div></div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2></div><div class="pp-stats"><div><div class="sk" style="height:18px;width:30px;margin:auto"></div></div><div><div class="sk" style="height:18px;width:30px;margin:auto"></div></div><div><div class="sk" style="height:18px;width:30px;margin:auto"></div></div><div><div class="sk" style="height:18px;width:30px;margin:auto"></div></div></div><div class="pp-dsec"><div class="sk" style="height:6px;width:100%;margin-bottom:4px"></div><div class="sk" style="height:9px;width:100%"></div></div><div class="pp-dsec"><div class="sk" style="height:9px;width:90%;margin-bottom:4px"></div><div class="sk" style="height:9px;width:80%;margin-bottom:4px"></div><div class="sk" style="height:9px;width:65%"></div></div></div></div></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="state-vp-col">
|
||
<span class="vp-tag">1440 px · Desktop</span><span class="vp-dim">780 px @ 55%</span>
|
||
<div class="wf wf-d"><div class="wf-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><div class="urlbar"><span>familienarchiv.de/persons/…</span></div></div>
|
||
<div class="pp"><div class="pp-gh"><div class="pp-gh-logo">Familienarchiv</div></div><div class="pp-wrap"><div class="pp-back">← Zurück</div><div class="pp-grid"><div class="pp-card"><div class="pp-av">WG</div><div class="pp-name">Walter de Gruyter</div><div class="pp-dates">1862 – 1923</div></div><div class="pp-dash"><div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2></div><div class="pp-stats"><div><div class="sk" style="height:22px;width:40px;margin:auto"></div></div><div><div class="sk" style="height:22px;width:40px;margin:auto"></div></div><div><div class="sk" style="height:22px;width:40px;margin:auto"></div></div><div><div class="sk" style="height:22px;width:40px;margin:auto"></div></div></div><div class="pp-dsec"><div class="sk" style="height:72px;width:100%"></div></div><div class="pp-dsec"><div class="sk" style="height:8px;width:100%;margin-bottom:5px"></div><div class="sk" style="height:12px;width:100%"></div></div><div class="pp-twocol"><div class="pp-dsec"><div class="sk" style="height:11px;width:90%;margin-bottom:5px"></div><div class="sk" style="height:11px;width:80%;margin-bottom:5px"></div><div class="sk" style="height:11px;width:65%"></div></div><div class="pp-dsec"><div class="sk" style="height:11px;width:90%;margin-bottom:5px"></div><div class="sk" style="height:11px;width:80%;margin-bottom:5px"></div><div class="sk" style="height:11px;width:65%"></div></div></div></div></div></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ann">
|
||
<strong>Anti-flicker rule.</strong> If the <code>letterCount</code> from <code>/api/persons/{id}</code> (fast) arrives before <code>/api/persons/{id}/dashboard</code>, 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.
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 07 — ACCESSIBILITY
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">07</span>Accessibility Contract · WCAG AA/AAA</h2>
|
||
<p class="sec-intro">Every rendered colour pair has been measured in light and dark mode. Semantics are chosen per tier: Compact uses a <code><dl></code> stat line; Standard/Rich reuse that and add <code><ol></code> for tile lists; Rich adds <code>role="img"</code> on the histogram with a text alternative that carries the shape information.</p>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Light Mode — Contrast Verification<span>layout.css tokens</span></div>
|
||
<table>
|
||
<thead><tr><th>Pair</th><th>Value</th><th>Ratio</th><th>WCAG</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Stat number (primary on muted)</td><td><code>#002850 on #fafaf5</code></td><td class="ir-px">14.0:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>Stat label (ink-3 on muted)</td><td><code>#666666 on #fafaf5</code></td><td class="ir-px">5.4:1</td><td>AA ✓</td></tr>
|
||
<tr><td>Stat "in" (accent on muted, 22 px/900)</td><td><code>#2F9E95 on #fafaf5</code></td><td class="ir-px">4.5:1</td><td>AA ✓ (large text rule)</td></tr>
|
||
<tr><td>Dashboard header (surface on primary)</td><td><code>#ffffff on #002850</code></td><td class="ir-px">14.5:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>CTA "Briefwechsel öffnen" (primary on accent)</td><td><code>#002850 on #a6dad8</code></td><td class="ir-px">8.1:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>Histogram peak bar (primary on surface)</td><td><code>#002850 on #ffffff</code></td><td class="ir-px">14.5:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>Tag chip (primary on accent)</td><td><code>#002850 on #a6dad8</code></td><td class="ir-px">8.1:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>Muted tag (ink-3 on line)</td><td><code>#666666 on #eee8dc</code></td><td class="ir-px">5.1:1</td><td>AA ✓</td></tr>
|
||
<tr><td>Focus ring (primary on surface, 2 px offset)</td><td><code>#002850</code></td><td class="ir-px">14.5:1</td><td>AAA ✓</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="impl-ref" style="margin-top:16px">
|
||
<div class="impl-ref-hdr">Dark Mode — Contrast Verification<span>data-theme="dark"</span></div>
|
||
<table>
|
||
<thead><tr><th>Pair</th><th>Value</th><th>Ratio</th><th>WCAG</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Stat number (ink on canvas-2)</td><td><code>#f0efe9 on #011526</code></td><td class="ir-px">15.1:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>Stat "out" (mint on canvas-2)</td><td><code>#a1dcd8 on #011526</code></td><td class="ir-px">9.6:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>Stat "in" (turquoise on canvas-2)</td><td><code>#00c7b1 on #011526</code></td><td class="ir-px">6.8:1</td><td>AA ✓</td></tr>
|
||
<tr><td>Histogram peak (mint on canvas)</td><td><code>#a1dcd8 on #010e1e</code></td><td class="ir-px">9.2:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>Tag (turquoise on tint)</td><td><code>#00c7b1 on rgba(0,199,177,.2)</code></td><td class="ir-px">6.3:1</td><td>AA ✓</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="ann">
|
||
<strong>Non-negotiable accessibility rules per tier.</strong>
|
||
<ul>
|
||
<li><b>All non-Empty tiers (shared chrome).</b> Stats strip is a real <code><dl></code> — screen readers announce "Briefe gesamt: 3, ausgehend: 2, eingehend: 1, Jahre: 4". The header CTA has an <code>aria-label</code> that includes the person's name ("Briefwechsel von Elsbeth Brandt öffnen").</li>
|
||
<li><b>Compact.</b> Nothing renders below the stats strip. No additional semantic concerns beyond the shared chrome.</li>
|
||
<li><b>Standard.</b> Adds direction bar as <code>role="img"</code> with <code>aria-label="11 von 18 Briefen ausgehend, 7 eingehend"</code>. Top correspondents are a semantic <code><ol></code> — "Top Korrespondenten, 5 Einträge, 1 von 5 Walter de Gruyter, 6 Briefe".</li>
|
||
<li><b>Rich.</b> Adds histogram as <code>role="img"</code> with <code>aria-label</code> describing range & peak: "Aktivität über 42 Jahre, Spitzenjahr 1922 mit 78 Briefen". Tag cloud is a <code><ul></code> of <code><li><a></code>; the visual size does <em>not</em> encode meaning beyond the text — count is exposed via <code>title</code> + <code>aria-label</code>.</li>
|
||
<li><b>All tiers.</b> Focus ring: <code>focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2</code>. 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.</li>
|
||
<li><b>Reduced motion.</b> Skeleton shimmer collapses to a static gradient; tag hover lift is disabled; histogram bar fade-in on first render is skipped. Honor <code>@media (prefers-reduced-motion: reduce)</code>.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 08 — IMPLEMENTATION
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">08</span>Implementation Notes — Tiered API, Lazy Components, Shipping Order</h2>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Endpoint<span>tier-aware response</span></div>
|
||
<table>
|
||
<thead><tr><th>Field</th><th>Value</th><th>Note</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Route</td><td><code>GET /api/persons/{id}/dashboard</code></td><td>Separate from <code>/api/persons/{id}</code> — lets person entity load stay lean and the dashboard query stay cache-friendly</td></tr>
|
||
<tr><td>Permission</td><td><code>@RequirePermission(Permission.READ_ALL)</code></td><td>Same as <code>/api/persons/{id}</code></td></tr>
|
||
<tr><td>Cache key</td><td><code>(personId, dataVersion)</code></td><td>Invalidated on any Document write that touches this person (sender or receiver change, tag change, location change)</td></tr>
|
||
<tr><td>Expensive aggregations</td><td>Only run when computed <code>tier == RICH</code></td><td><code>activityByYear</code>, <code>topLocations</code>, <code>topTags</code> are <code>null</code> at lower tiers — DB query planner skips those subqueries entirely</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="impl-ref" style="margin-top:16px">
|
||
<div class="impl-ref-hdr">Response schema<span>PersonDashboardDTO — nullable fields by tier</span></div>
|
||
<table>
|
||
<thead><tr><th>Field</th><th>Type</th><th>Populated at tier</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>tier</code></td><td><code>"EMPTY" | "COMPACT" | "STANDARD" | "RICH"</code></td><td>always · advisory hint; frontend may re-derive</td></tr>
|
||
<tr><td><code>totalCount</code></td><td>int</td><td>always</td></tr>
|
||
<tr><td><code>outCount</code></td><td>int</td><td>always</td></tr>
|
||
<tr><td><code>inCount</code></td><td>int</td><td>always</td></tr>
|
||
<tr><td><code>firstLetterYear</code></td><td>int?</td><td>Compact+ (null when totalCount == 0)</td></tr>
|
||
<tr><td><code>lastLetterYear</code></td><td>int?</td><td>Compact+</td></tr>
|
||
<tr><td><code>yearSpan</code></td><td>int?</td><td>Compact+ · <code>last - first + 1</code></td></tr>
|
||
<tr><td><code>correspondentCount</code></td><td>int?</td><td>Standard+</td></tr>
|
||
<tr><td><code>topCorrespondents</code></td><td>List<CorrespondentTileDTO>?</td><td>Standard+ · max 5</td></tr>
|
||
<tr><td><code>activityByYear</code></td><td>Map<int, int>?</td><td><b>Rich only</b> · contiguous from first to last year</td></tr>
|
||
<tr><td><code>peakYear / peakYearCount</code></td><td>int? · int?</td><td>Rich only</td></tr>
|
||
<tr><td><code>topLocations</code></td><td>List<LocationTileDTO>?</td><td>Rich only · max 5</td></tr>
|
||
<tr><td><code>topTags</code></td><td>List<TagTileDTO>?</td><td>Rich only · max 12 · minimum count threshold 3</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="impl-ref" style="margin-top:16px">
|
||
<div class="impl-ref-hdr">Component structure<span>baseline eager · Rich-only lazy</span></div>
|
||
<table>
|
||
<thead><tr><th>File</th><th>Tier</th><th>Load</th><th>Responsibility</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>PersonDashboard.svelte</code></td><td>all</td><td>eager</td><td>Orchestrator — renders header + stats always, then conditionally appends sections based on <code>tier</code></td></tr>
|
||
<tr><td><code>DashboardHeader.svelte</code></td><td>Compact, Standard, Rich</td><td>eager</td><td>Navy strip: title + "↗ Briefwechsel öffnen" CTA · shared across all non-Empty tiers</td></tr>
|
||
<tr><td><code>StatStrip.svelte</code></td><td>Compact, Standard, Rich</td><td>eager</td><td>4-cell stats grid with direction colouring · shared across all non-Empty tiers</td></tr>
|
||
<tr><td><code>DistributionBar.svelte</code></td><td>Standard, Rich</td><td>eager</td><td><em>Shared</em> with <code>briefwechsel-thumbnail-rows-spec</code> — not new</td></tr>
|
||
<tr><td><code>TopTileList.svelte</code></td><td>Standard, Rich</td><td>eager</td><td>Ordered list of tiles (name + bar + count) · generic (used for correspondents and locations)</td></tr>
|
||
<tr><td><code>ActivityHistogram.svelte</code></td><td>Rich only</td><td><b>lazy</b></td><td>Dynamic import: <code>{#await import('./ActivityHistogram.svelte')}</code></td></tr>
|
||
<tr><td><code>TagCloud.svelte</code></td><td>Rich only</td><td><b>lazy</b></td><td>Dynamic import; size buckets derived from counts</td></tr>
|
||
<tr><td><code>EmptyNotice.svelte</code></td><td>Empty</td><td>eager</td><td>Single card with reassurance text</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="impl-ref" style="margin-top:16px">
|
||
<div class="impl-ref-hdr">Route changes<span>/persons/[id] shell</span></div>
|
||
<table>
|
||
<thead><tr><th>File</th><th>Change</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>+page.server.ts</code></td><td>Add parallel call to <code>/api/persons/{id}/dashboard</code>. Keep existing error handling pattern (<code>result.response.ok</code> check · <code>getErrorMessage(code)</code>).</td></tr>
|
||
<tr><td><code>+page.svelte</code></td><td>Replace <code><CoCorrespondentsList></code> with <code><PersonDashboard dashboard={data.dashboard} /></code>. Remove the local <code>coCorrespondents</code> derivation — backend owns it.</td></tr>
|
||
<tr><td><code>CoCorrespondentsList.svelte</code></td><td>Keep temporarily (used nowhere else after this ships). Delete in a follow-up commit once the new route is green in QA.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="ann">
|
||
<strong>Shipping order.</strong>
|
||
<ul>
|
||
<li><b>Phase 1</b> — backend endpoint <code>GET /api/persons/{id}/dashboard</code> returning the tiered <code>PersonDashboardDTO</code>. Tests for Empty / Compact / Standard / Rich. Cache keyed on person data-version. Skip expensive aggregations for non-Rich persons.</li>
|
||
<li><b>Phase 2</b> — <code>PersonDashboard.svelte</code> + <code>DashboardHeader.svelte</code> + <code>StatStrip.svelte</code> + <code>EmptyNotice.svelte</code>. Replaces <code>CoCorrespondentsList</code> in the right column. <b>This phase alone ships the Compact tier — value for ~90 % of persons.</b></li>
|
||
<li><b>Phase 3</b> — <code>TopTileList.svelte</code> + reuse <code>DistributionBar.svelte</code>. Standard tier's direction bar and top-correspondents tiles go live for ~6 % of persons.</li>
|
||
<li><b>Phase 4</b> — <code>ActivityHistogram.svelte</code>, <code>TagCloud.svelte</code> (both lazy). Wire new <code>direction</code>, <code>location</code>, <code>tagId</code> query params on <code>/briefwechsel</code>. Rich tier goes live for the 4 power-correspondence persons.</li>
|
||
<li><b>Phase 5</b> — 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).</li>
|
||
<li><b>Phase 6 (cleanup)</b> — delete <code>CoCorrespondentsList.svelte</code> and any now-unused co-correspondent derivation helpers.</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="ann" style="background:#F0FDF4;border-color:#86EFAC;color:#14532D">
|
||
<strong style="color:#166534">Migration story.</strong> The old spec's <code>PersonDashboard</code> (six eager blocks) has not shipped yet — there is no user-facing migration. The existing <code>CoCorrespondentsList</code> is what each tier replaces. Compact and Standard tiers are strict improvements over <code>CoCorrespondentsList</code>; 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.
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|