Two production-ready specs following the chronik-spec format (scaled wireframes × 3 viewports + impl-ref tables with exact Tailwind classes and pixel values + WCAG contrast verification): - briefwechsel-thumbnail-rows-spec.html — /briefwechsel row redesign with PDF thumbnail, summary-as-quote, bilateral distribution bar; drops status lifecycle and script-type indicators. - person-dashboard-spec.html — new Korrespondenz-Überblick block on /persons/[id] with stats, activity histogram, direction split, top correspondents/locations, tag cloud. Every tile deep-links to /briefwechsel with filters. Both specs share the DistributionBar.svelte component. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1044 lines
92 KiB
HTML
1044 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 · Final Design Spec · Familienarchiv</title>
|
||
<style>
|
||
/* ═══════════════════════════════════════════════════════════
|
||
PERSON DASHBOARD — /persons/[id] extension
|
||
Adds "Korrespondenz-Überblick" block to the right column:
|
||
stats, activity histogram, direction split, top correspondents,
|
||
top locations, tag cloud. Every element deep-links into /briefwechsel.
|
||
By Leonie Voss (UX/Design). 2026-04-22.
|
||
═══════════════════════════════════════════════════════════ */
|
||
|
||
*,*::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,.55);max-width:780px;line-height:1.7}
|
||
.mast p code{background:rgba(255,255,255,.1);padding:1px 5px;border-radius:2px;font-family:monospace}
|
||
.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}
|
||
|
||
/* ── 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:780px;margin-bottom:24px}
|
||
|
||
/* ── 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}
|
||
.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}
|
||
.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 */
|
||
.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}
|
||
|
||
/* Right: dashboard */
|
||
.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 */
|
||
.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}
|
||
|
||
/* Histogram */
|
||
.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}
|
||
|
||
/* 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}
|
||
|
||
/* Two-col arrangement in dashboard */
|
||
.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}
|
||
|
||
/* Cloud */
|
||
.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}
|
||
|
||
/* 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}}
|
||
|
||
/* ── Close-ups at ~100% ─────────────────────── */
|
||
.cu{background:#fff;border:1px solid #e4e2d7;border-radius:6px;padding:20px 24px;margin-bottom:20px}
|
||
.cu-t{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#666;margin-bottom:14px}
|
||
|
||
/* Close-up dashboard parts */
|
||
.cu-hist{display:flex;align-items:flex-end;gap:2px;height:110px;padding:6px 0 0;background:#fff;border:1px solid #e4e2d7;border-radius:4px;padding:16px 20px 0}
|
||
.cu-hist .bar{flex:1;background:#a1dcd8;opacity:.65;border-radius:2px 2px 0 0;cursor:pointer}
|
||
.cu-hist .bar.peak{background:#012851;opacity:.9}
|
||
.cu-hist-labels{display:flex;justify-content:space-between;font-size:10px;color:#888;margin-top:6px;font-weight:700;padding:0 20px 14px}
|
||
.cu-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:#e4e2d7;border-radius:4px;overflow:hidden;border:1px solid #e4e2d7}
|
||
.cu-stats div{background:#fafaf5;padding:18px 14px;text-align:center}
|
||
.cu-stats .v{font-family:Georgia,serif;font-size:28px;font-weight:900;color:#012851;letter-spacing:-.5px}
|
||
.cu-stats .v.out{color:#012851}
|
||
.cu-stats .v.in{color:#2F9E95}
|
||
.cu-stats .k{font-size:11px;color:#888;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-top:2px}
|
||
|
||
.cu-toplist{display:flex;flex-direction:column;gap:8px;background:#fff;border:1px solid #e4e2d7;border-radius:4px;padding:14px 18px}
|
||
.cu-ti{display:flex;align-items:center;gap:12px;font-size:13px;padding:5px 6px;border-radius:3px;cursor:pointer}
|
||
.cu-ti:hover{background:#f7f5f2}
|
||
.cu-ti .nm{flex:1;color:#012851;font-weight:600}
|
||
.cu-ti .bw{width:160px;height:7px;background:#f0ede5;border-radius:4px;overflow:hidden;flex-shrink:0}
|
||
.cu-ti .bw span{display:block;height:100%;background:#012851;border-radius:4px}
|
||
.cu-ti .val{width:38px;text-align:right;font-size:12px;color:#888;font-weight:700;font-variant-numeric:tabular-nums}
|
||
|
||
.cu-cloud{display:flex;flex-wrap:wrap;gap:6px;background:#fff;border:1px solid #e4e2d7;border-radius:4px;padding:14px 18px}
|
||
.cu-cloud .tag{padding:4px 12px;background:#a1dcd8;color:#012851;font-weight:700;border-radius:14px;font-size:12px;cursor:pointer}
|
||
.cu-cloud .tag.xl{font-size:15px;padding:5px 14px}
|
||
.cu-cloud .tag.l{font-size:13px}
|
||
.cu-cloud .tag.muted{background:#EEE8DC;color:#666}
|
||
|
||
.cu-dsplit{background:#fff;border:1px solid #e4e2d7;border-radius:4px;padding:14px 18px}
|
||
.cu-dsplit-labels{display:flex;justify-content:space-between;font-size:14px;font-weight:700;margin-bottom:8px}
|
||
.cu-dsplit-labels .out{color:#012851}
|
||
.cu-dsplit-labels .in{color:#2F9E95}
|
||
.cu-dbar{height:10px;display:flex;border-radius:5px;overflow:hidden;background:#e4e2d7}
|
||
.cu-dbar .out{background:#012851}
|
||
.cu-dbar .in{background:#2F9E95}
|
||
|
||
/* 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</h1>
|
||
<p>Final design for the dashboard block that extends <code>/persons/[id]</code>. 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 <code>/briefwechsel</code>. Replaces the current <code>CoCorrespondentsList</code> block.</p>
|
||
</div>
|
||
<div class="mast-badge">FINAL</div>
|
||
</div>
|
||
<div class="decisions">
|
||
<div class="dec"><div class="dec-label">Route</div><div class="dec-value">/persons/[id] · right column</div></div>
|
||
<div class="dec"><div class="dec-label">Layout</div><div class="dec-value">35% / 65% split · stacks below 768 px</div></div>
|
||
<div class="dec"><div class="dec-label">Sections shown</div><div class="dec-value">Stats · Histogram · Direction · Correspondents · Locations · Tags</div></div>
|
||
<div class="dec"><div class="dec-label">Deep links</div><div class="dec-value">Every tile → /briefwechsel with filters applied</div></div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="spec-disclaimer">
|
||
<strong>Reading this spec.</strong> Mockups in Section 02 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 class + pixel value. Close-ups in Section 03 render each dashboard block at ~100 % scale for pixel-accurate reference.
|
||
</div>
|
||
|
||
<!-- TOC -->
|
||
<div class="toc">
|
||
<div class="toc-t">Inhalt</div>
|
||
<ol>
|
||
<li><b>01</b> Page anatomy <span>default · 1440 px</span></li>
|
||
<li><b>02</b> Content states × 3 viewports <span>4 states · 12 frames</span></li>
|
||
<li><b>03</b> Dashboard block close-ups <span>6 blocks @ real size</span></li>
|
||
<li><b>04</b> Deep-link grammar <span>every tile → /briefwechsel</span></li>
|
||
<li><b>05</b> Accessibility contract <span>WCAG AA/AAA</span></li>
|
||
<li><b>06</b> Implementation notes <span>backend API · components</span></li>
|
||
</ol>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 01 — PAGE ANATOMY
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">01</span>Page Anatomy — Default State at 1440 px</h2>
|
||
<p class="sec-intro">The page is a 35% / 65% split (existing <code>lg:grid-cols-[35%_65%]</code>). Left column keeps <code>PersonCard</code> and <code>NameHistoryCard</code>. Right column replaces <code>CoCorrespondentsList</code> with the new <code>PersonDashboard</code> block at the top, followed by the existing sent/received document lists.</p>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 300px;gap:32px;align-items:flex-start">
|
||
<div>
|
||
<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/{id}</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">
|
||
<!-- A · Person card -->
|
||
<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>
|
||
<!-- B · Dashboard -->
|
||
<div class="pp-dash">
|
||
<div class="pp-dash-hdr"><h2>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Briefwechsel</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 über die Jahre <span class="note">Spitzenjahr <b>1922 · 78</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 · 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 Korrespondenten <span class="note">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 v. 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">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 class="callout-grid">
|
||
<div class="cg"><div class="cg-t">A · Person card</div><div class="cg-b">Unchanged in this spec. Avatar, full name, lifespan, bearbeiten + <b>Briefwechsel</b> buttons. The primary action moves from "Edit" to "Open Briefwechsel" because that is what users actually do next.</div></div>
|
||
<div class="cg"><div class="cg-t">B · Korrespondenz-Überblick</div><div class="cg-b">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.</div></div>
|
||
<div class="cg"><div class="cg-t">Stats strip</div><div class="cg-b">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).</div></div>
|
||
<div class="cg"><div class="cg-t">Activity histogram</div><div class="cg-b">One bar per year in the range (1898 → 1940 = 43 bars). Peak bar uses <code>bg-primary</code>, others <code>bg-accent/60</code>. Hovering a bar shows "{year} · {count} Briefe" tooltip; clicking filters <code>/briefwechsel?senderId=…&from=YYYY-01-01&to=YYYY-12-31</code>.</div></div>
|
||
<div class="cg"><div class="cg-t">Top correspondents</div><div class="cg-b">Up to 6 rows. Name + proportional bar + count. Click opens <code>/briefwechsel?senderId=<this>&receiverId=<other></code> (bilateral view). "Alle N Korrespondenten →" link below.</div></div>
|
||
<div class="cg"><div class="cg-t">Top locations & Tag cloud</div><div class="cg-b">Location tiles mirror correspondents. Tag cloud sized by frequency — <code>xl</code> > 100, <code>l</code> > 50, <code>m</code> > 20, muted ≤ 20. Click on any tag: <code>/briefwechsel?senderId=<this>&tag=<id></code>.</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Implementation Reference — Page Shell<span>extends existing /persons/[id]</span></div>
|
||
<table>
|
||
<thead><tr><th>Element</th><th>Classes</th><th>Real</th><th>Note</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Page container</td><td><code>mx-auto max-w-6xl px-4 py-10</code></td><td class="ir-px">max 72rem</td><td>Unchanged · existing route shell</td></tr>
|
||
<tr><td>2-column grid</td><td><code>lg:grid lg:grid-cols-[35%_65%] lg:gap-8</code></td><td class="ir-px">32 px gap</td><td>Existing · unchanged</td></tr>
|
||
<tr><td>Left column stack</td><td><code>PersonCard → NameHistoryCard (mt-6)</code></td><td class="ir-px">24 px mt</td><td>Existing · unchanged</td></tr>
|
||
<tr><td>Right column</td><td><code>PersonDashboard → PersonDocumentList(sent) → PersonDocumentList(received)</code></td><td class="ir-px">new + existing</td><td><code>PersonDashboard</code> replaces <code>CoCorrespondentsList</code></td></tr>
|
||
<tr><td>Dashboard 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 card pattern from CLAUDE.md</td></tr>
|
||
<tr><td>Dashboard header</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 strip — sets dashboard apart from body card patterns</td></tr>
|
||
<tr><td>Dashboard title</td><td><code>font-serif text-base font-bold</code></td><td class="ir-px">16 px / 700</td><td>Merriweather</td></tr>
|
||
<tr><td>"Briefwechsel öffnen" CTA</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</code></td><td class="ir-px">44 px min</td><td>WCAG 2.2 AA touch target; Paraglide <code>m.person_open_conversation()</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 02 — CONTENT STATES × 3 VIEWPORTS
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">02</span>Content States × 3 Viewports</h2>
|
||
<p class="sec-intro">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.</p>
|
||
|
||
<!-- ══════════════════ STATE 01 · DEFAULT ═════════════ -->
|
||
<div class="state-block">
|
||
<div class="state-hdr"><span class="state-num">01</span><span class="state-title">Default · Full dataset (851 letters · 42 years · 87 correspondents)</span></div>
|
||
<div class="state-desc">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.</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/9ed8…</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 class="pp-ti"><span class="nm">G. 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</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/9ed8fd47-…</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 class="pp-ti"><span class="nm">Käthe Dieckmann</span><span class="bw"><span style="width:26%"></span></span><span class="val">47</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>Wrap behaviour by viewport.</strong>
|
||
<ul>
|
||
<li>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.</li>
|
||
<li>768 px: grid stacks. 4×1 stats. Full histogram. Two-col section shows correspondents & locations side-by-side. Tag cloud shows 8–9 tags.</li>
|
||
<li>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.</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════ STATE 02 · EMPTY ═══════════════ -->
|
||
<div class="state-block">
|
||
<div class="state-hdr"><span class="state-num">02</span><span class="state-title">Empty · Person has no letters yet</span></div>
|
||
<div class="state-desc">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.</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">Diese Person hat noch keine Korrespondenz im Archiv.</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 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>
|
||
|
||
<!-- ══════════════════ STATE 03 · SPARSE ═══════════════ -->
|
||
<div class="state-block">
|
||
<div class="state-hdr"><span class="state-num">03</span><span class="state-title">Sparse · Few letters (< 10)</span></div>
|
||
<div class="state-desc">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.</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">EB</div><div class="pp-name">Elsbeth Brandt</div><div class="pp-dates">1890 – 1963</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">7</div><div class="k">ges.</div></div><div><div class="v out">4</div><div class="k">→</div></div><div><div class="v in">3</div><div class="k">←</div></div><div><div class="v">3J</div><div class="k">Jahre</div></div></div><div class="pp-dsec"><h3>Zeitraum</h3><div class="pp-dsplit" style="font-size:5px"><span>1919 – 1922</span></div></div><div class="pp-dsec"><h3>Top</h3><div class="pp-toplist"><div class="pp-ti"><span class="nm">W. de Gruyter</span><span class="val">4</span></div><div class="pp-ti"><span class="nm">H. Cram</span><span class="val">3</span></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/elsbeth</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">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>Korrespondenz-Überblick</h2><a class="pp-open-conv">↗ Öffnen</a></div><div class="pp-stats"><div><div class="v">7</div><div class="k">gesamt</div></div><div><div class="v out">4</div><div class="k">ausgehend</div></div><div><div class="v in">3</div><div class="k">eingehend</div></div><div><div class="v">3</div><div class="k">Jahre</div></div></div><div class="pp-dsec"><h3>Zeitraum</h3><div class="pp-dsplit" style="font-size:9px"><span>1919 · erster Brief</span><span>1922 · letzter Brief</span></div></div><div class="pp-dsec"><h3>Korrespondenten</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">4</span></div><div class="pp-ti"><span class="nm">Herbert Cram</span><span class="bw"><span style="width:75%"></span></span><span class="val">3</span></div></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/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></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 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">7</div><div class="k">Briefe gesamt</div></div><div><div class="v out">4</div><div class="k">ausgehend</div></div><div><div class="v in">3</div><div class="k">eingehend</div></div><div><div class="v">3</div><div class="k">Jahre</div></div></div><div class="pp-dsec"><h3>Zeitraum</h3><div class="pp-dsplit" style="font-size:11px"><span style="color:#012851">1919 · erster Brief</span><span style="color:#012851">1922 · letzter Brief</span></div></div><div class="pp-twocol"><div class="pp-dsec"><h3>Korrespondenten</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">4</span></div><div class="pp-ti"><span class="nm">Herbert Cram</span><span class="bw"><span style="width:75%"></span></span><span class="val">3</span></div></div></div><div class="pp-dsec"><h3>Orte</h3><div class="pp-toplist"><div class="pp-ti"><span class="nm">B.Lichterfelde</span><span class="bw"><span style="width:100%"></span></span><span class="val">5</span></div><div class="pp-ti"><span class="nm">Berlin</span><span class="bw"><span style="width:40%"></span></span><span class="val">2</span></div></div></div></div></div></div></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="ann"><strong>Sparse-mode simplifications.</strong>
|
||
<ul>
|
||
<li>Histogram replaced with a single "Zeitraum" line showing first and last letter years. Histogram returns as soon as <code>letterCount >= 10</code> and <code>yearSpan >= 3</code>.</li>
|
||
<li>Tag cloud hidden when fewer than 3 distinct tags across all letters. No "empty tag cloud" placeholder.</li>
|
||
<li>"Top of N" notes dropped — numbers speak for themselves.</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════ STATE 04 · LOADING ═══════════════ -->
|
||
<div class="state-block">
|
||
<div class="state-hdr"><span class="state-num">04</span><span class="state-title">Loading · Skeleton</span></div>
|
||
<div class="state-desc">While the aggregation endpoint is in-flight. Person card renders from the <code>/api/persons/{id}</code> payload (fast); the dashboard shows skeleton rectangles for each section in the same grid slots.</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 class="pp-dsec"><div class="sk" style="height:18px;width:100%"></div></div><div class="pp-dsec"><div class="sk" style="height:5px;width:80%;margin-bottom:3px"></div><div class="sk" style="height:5px;width:70%;margin-bottom:3px"></div><div class="sk" style="height:5px;width:50%"></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:50px;width:100%"></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>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 03 — DASHBOARD BLOCK CLOSE-UPS
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">03</span>Dashboard Block Close-Ups · ~100% Scale</h2>
|
||
<p class="sec-intro">Six blocks rendered at near-real pixel sizes. These are the reference renderings developers check against when implementing <code>PersonDashboard.svelte</code> and its sub-components.</p>
|
||
|
||
<!-- Block 1: Stats strip -->
|
||
<div class="cu">
|
||
<div class="cu-t">Block A · Stats strip</div>
|
||
<div class="cu-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="impl-ref">
|
||
<div class="impl-ref-hdr">Block A — Stats Strip<span>Desktop = 4 cells · Tablet = 4 cells · Mobile = 2×2</span></div>
|
||
<table>
|
||
<thead><tr><th>Part</th><th>Classes</th><th>Real</th><th>Note</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Strip container</td><td><code>grid grid-cols-2 sm:grid-cols-4 gap-px bg-line border-b border-line</code></td><td class="ir-px">1 px gap (shows as lines)</td><td>Separators are the background showing through</td></tr>
|
||
<tr><td>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 separators read</td></tr>
|
||
<tr><td>Number</td><td><code>font-serif text-[22px] font-black text-primary 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</td></tr>
|
||
<tr><td>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>Direction labels match number colour</td></tr>
|
||
<tr><td>Mobile formatter</td><td>Abbreviate "Briefe gesamt" → "gesamt"</td><td class="ir-px">m.person_stats_total_short()</td><td>Paraglide key with <code>_short</code> suffix</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Block 2: Activity histogram -->
|
||
<div class="cu">
|
||
<div class="cu-t">Block B · Activity histogram (one bar per year)</div>
|
||
<div class="cu-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="cu-hist-labels"><span>1898</span><span>1922 ▲ Spitzenjahr · 78 Briefe</span><span>1940</span></div>
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Block B — Activity Histogram<span>deep-links via from/to params</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>flex items-end gap-0.5 h-[72px] pt-1</code></td><td class="ir-px">72 px height</td><td>Height tuned to comfortable reading — not too small</td></tr>
|
||
<tr><td>Bar (normal)</td><td><code>flex-1 bg-accent/60 rounded-t-sm hover:bg-accent/90 transition-colors cursor-pointer</code></td><td class="ir-px">variable width</td><td>Min-width 3 px for ≤ 30 years; thinner for longer ranges</td></tr>
|
||
<tr><td>Bar (peak)</td><td><code>flex-1 bg-primary rounded-t-sm</code></td><td class="ir-px">max height</td><td>Exactly one peak bar highlighted</td></tr>
|
||
<tr><td>Bar link</td><td><code><a href="/briefwechsel?senderId={id}&from={year}-01-01&to={year}-12-31"></code></td><td class="ir-px">—</td><td>Whole bar is the link; tooltip announces "Jahr {year} · {count} Briefe"</td></tr>
|
||
<tr><td>Year labels</td><td><code>flex justify-between text-[10px] font-bold text-ink-3 mt-1.5</code></td><td class="ir-px">10 px</td><td>Only show: earliest year · peak year · latest year</td></tr>
|
||
<tr><td>Tooltip</td><td>native <code>title</code> attribute on bar</td><td class="ir-px">—</td><td>"1922 · 78 Briefe" — Paraglide pluralized</td></tr>
|
||
<tr><td>Empty-year bars</td><td>rendered as <code>min-height: 2px</code> placeholder</td><td class="ir-px">2 px</td><td>Keeps bar spacing regular across decades with gaps</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Block 3: Direction split -->
|
||
<div class="cu">
|
||
<div class="cu-t">Block C · Direction split (same component as /briefwechsel distribution bar, re-used)</div>
|
||
<div class="cu-dsplit">
|
||
<div class="cu-dsplit-labels"><span class="out">→ 612 ausgehend · 72%</span><span class="in">← 239 eingehend · 28%</span></div>
|
||
<div class="cu-dbar"><span class="out" style="width:72%"></span><span class="in" style="width:28%"></span></div>
|
||
</div>
|
||
<div class="ann"><strong>Re-use, do not duplicate.</strong> The bilateral <code>DistributionBar.svelte</code> component from the thumbnail rows spec is also used here. Same props (<code>outCount</code>, <code>outLabel</code>, <code>inCount</code>, <code>inLabel</code>), same aria-label pattern.</div>
|
||
</div>
|
||
|
||
<!-- Block 4: Top correspondents -->
|
||
<div class="cu">
|
||
<div class="cu-t">Block D · Top correspondents</div>
|
||
<div class="cu-toplist">
|
||
<div class="cu-ti"><span class="nm">Walter Dieckmann</span><span class="bw"><span style="width:100%"></span></span><span class="val">184</span></div>
|
||
<div class="cu-ti"><span class="nm">Herbert Cram</span><span class="bw"><span style="width:78%"></span></span><span class="val">143</span></div>
|
||
<div class="cu-ti"><span class="nm">Ella Dieckmann</span><span class="bw"><span style="width:48%"></span></span><span class="val">88</span></div>
|
||
<div class="cu-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="cu-ti"><span class="nm">Gertrud von Rofden</span><span class="bw"><span style="width:32%"></span></span><span class="val">58</span></div>
|
||
<div class="cu-ti"><span class="nm">Käthe Dieckmann</span><span class="bw"><span style="width:26%"></span></span><span class="val">47</span></div>
|
||
</div>
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Block D — Top List (correspondents & locations share this)<span>rendered as <ol> for semantic ranking</span></div>
|
||
<table>
|
||
<thead><tr><th>Part</th><th>Classes</th><th>Real</th><th>Note</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Wrapper</td><td><code><ol></code> · <code>flex flex-col gap-2</code></td><td class="ir-px">8 px gap</td><td>Ordered list — rank order matters, screen readers announce "1 of 6"</td></tr>
|
||
<tr><td>Item</td><td><code><li></code> containing <code><a></code> · <code>flex items-center gap-3 text-sm px-1.5 py-1 rounded-sm min-h-[32px] hover:bg-muted</code></td><td class="ir-px">32 px min</td><td>Not 44 px because these are secondary links inside a card — desktop focus. Mobile bumps to 44 via <code>md:min-h-[32px] min-h-[44px]</code></td></tr>
|
||
<tr><td>Name</td><td><code>flex-1 font-semibold text-ink truncate</code></td><td class="ir-px">14 px / 600</td><td>Truncate middle-ellipsis on very long German names at < 768 px</td></tr>
|
||
<tr><td>Proportional bar wrapper</td><td><code>w-[120px] h-[7px] bg-line rounded overflow-hidden shrink-0 hidden sm:block</code></td><td class="ir-px">120 × 7</td><td>Hidden on mobile — the number carries the data</td></tr>
|
||
<tr><td>Proportional bar fill</td><td><code>h-full bg-primary rounded</code></td><td class="ir-px">width = value ÷ max</td><td>Widths computed client-side from the list's max value</td></tr>
|
||
<tr><td>Count</td><td><code>w-10 text-right text-sm text-ink-3 font-bold tabular-nums shrink-0</code></td><td class="ir-px">14 px / 700</td><td>Tabular figures so columns align</td></tr>
|
||
<tr><td>"Alle N anzeigen →"</td><td><code>mt-2.5 text-xs font-bold text-primary border-b border-dashed border-primary/60 hover:border-primary</code></td><td class="ir-px">12 px / 700</td><td>Appears when total > shown count</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Block 5: Top locations -->
|
||
<div class="cu">
|
||
<div class="cu-t">Block E · Top locations — identical component to Block D</div>
|
||
<div class="cu-toplist">
|
||
<div class="cu-ti"><span class="nm">📍 Berlin</span><span class="bw"><span style="width:100%"></span></span><span class="val">412</span></div>
|
||
<div class="cu-ti"><span class="nm">📍 B.Lichterfelde</span><span class="bw"><span style="width:44%"></span></span><span class="val">180</span></div>
|
||
<div class="cu-ti"><span class="nm">📍 Bad Kissingen</span><span class="bw"><span style="width:14%"></span></span><span class="val">58</span></div>
|
||
<div class="cu-ti"><span class="nm">📍 Cöln</span><span class="bw"><span style="width:9%"></span></span><span class="val">37</span></div>
|
||
<div class="cu-ti"><span class="nm">📍 Belgard</span><span class="bw"><span style="width:6%"></span></span><span class="val">26</span></div>
|
||
</div>
|
||
<div class="ann"><strong>Emoji pin is decorative.</strong> Rendered via <code>::before</code> with <code>aria-hidden="true"</code>. The location string alone carries semantic meaning. Future iteration may replace with a map icon component.</div>
|
||
</div>
|
||
|
||
<!-- Block 6: Tag cloud -->
|
||
<div class="cu">
|
||
<div class="cu-t">Block F · Tag cloud — frequency-sized</div>
|
||
<div class="cu-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">Kuraufenthalt</span><span class="tag">Reise</span><span class="tag">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 class="impl-ref">
|
||
<div class="impl-ref-hdr">Block F — Tag Cloud<span>size buckets map to count thresholds</span></div>
|
||
<table>
|
||
<thead><tr><th>Size</th><th>Classes</th><th>Count threshold</th><th>Note</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>xl</td><td><code>text-[15px] font-bold px-3.5 py-1 bg-accent text-primary rounded-full</code></td><td class="ir-px">≥ 100</td><td>Max 2 tags at this size — visual anchor</td></tr>
|
||
<tr><td>l</td><td><code>text-[13px] font-bold px-3 py-1 bg-accent text-primary rounded-full</code></td><td class="ir-px">50–99</td><td>Up to 4 tags</td></tr>
|
||
<tr><td>m</td><td><code>text-xs font-bold px-2.5 py-0.5 bg-accent text-primary rounded-full</code></td><td class="ir-px">20–49</td><td>Up to 6 tags</td></tr>
|
||
<tr><td>regular</td><td><code>text-xs font-bold px-2.5 py-0.5 bg-accent text-primary rounded-full</code></td><td class="ir-px">5–19</td><td>All tags meeting threshold</td></tr>
|
||
<tr><td>muted</td><td><code>text-xs font-semibold px-2.5 py-0.5 bg-line text-ink-3 rounded-full</code></td><td class="ir-px">< 5</td><td>Up to 8 muted tags, sorted alphabetically</td></tr>
|
||
<tr><td>Click target</td><td><code>min-h-[28px] min-w-[44px] inline-flex items-center</code></td><td class="ir-px">44 px min width</td><td>Very short tag names ("Kur") still meet touch target</td></tr>
|
||
<tr><td>Hover</td><td><code>hover:-translate-y-px transition-transform</code></td><td class="ir-px">1 px lift</td><td>Bypassed when <code>prefers-reduced-motion</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 04 — DEEP-LINK GRAMMAR
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">04</span>Deep-Link Grammar — every tile → /briefwechsel</h2>
|
||
<p class="sec-intro">The dashboard is the discovery surface; <code>/briefwechsel</code> is the reading surface. Every clickable element in the dashboard adds filters to the existing <code>/briefwechsel</code> query string so the transition feels continuous.</p>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Link grammar<span>replace placeholders with real values</span></div>
|
||
<table>
|
||
<thead><tr><th>Element</th><th>Link</th><th>Note</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Header "↗ Briefwechsel öffnen"</td><td><code>/briefwechsel?senderId={id}&dir=DESC</code></td><td>Opens all letters for this person · no date filter · newest first</td></tr>
|
||
<tr><td>Stats — "gesamt"</td><td>Same as header</td><td>Entire count is tap-to-open</td></tr>
|
||
<tr><td>Stats — "ausgehend"</td><td><code>/briefwechsel?senderId={id}&direction=OUT&dir=DESC</code></td><td>Requires new <code>direction</code> query param on /briefwechsel</td></tr>
|
||
<tr><td>Stats — "eingehend"</td><td><code>/briefwechsel?senderId={id}&direction=IN&dir=DESC</code></td><td>Same</td></tr>
|
||
<tr><td>Histogram bar</td><td><code>/briefwechsel?senderId={id}&from={year}-01-01&to={year}-12-31</code></td><td>Opens bilateral or single view scoped to that year</td></tr>
|
||
<tr><td>Top correspondent row</td><td><code>/briefwechsel?senderId={id}&receiverId={otherId}&dir=DESC</code></td><td>Opens the bilateral view for the pair</td></tr>
|
||
<tr><td>Top location row</td><td><code>/briefwechsel?senderId={id}&location={locSlug}</code></td><td>Requires new <code>location</code> query param on /briefwechsel</td></tr>
|
||
<tr><td>Tag chip</td><td><code>/briefwechsel?senderId={id}&tagId={tagId}</code></td><td>Requires new <code>tagId</code> query param on /briefwechsel</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="ann"><strong>New query parameters required on /briefwechsel.</strong>
|
||
<ul>
|
||
<li><code>direction=OUT|IN</code> — filter to letters in one direction only (existing endpoint already distinguishes sender vs receiver; this adds symmetry for the single-person view).</li>
|
||
<li><code>location=<slug></code> — case-insensitive match on <code>Document.location</code>. Slug because German locations can contain spaces and dots ("B.Lichterfelde", "Bad Kissingen").</li>
|
||
<li><code>tagId=<uuid></code> — filter to letters that reference this tag. If omitted, no tag filter is applied.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 05 — ACCESSIBILITY
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">05</span>Accessibility Contract · WCAG AA/AAA</h2>
|
||
<p class="sec-intro">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.</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 #f7f5f2</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 #f7f5f2</code></td><td class="ir-px">5.4:1</td><td>AA ✓</td></tr>
|
||
<tr><td>Stat "in" (accent on muted)</td><td><code>#2F9E95 on #f7f5f2</code></td><td class="ir-px">4.5:1</td><td>AA ✓ (borderline — number is 22 px / 900 qualifies as large)</td></tr>
|
||
<tr><td>Dashboard header title (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 bar (accent/60 on surface)</td><td><code>rgba(47,158,149,.6) on #ffffff</code></td><td class="ir-px">2.8:1</td><td>Decorative (bars have titles; not text)</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>Stat label (ink-3 on canvas-2)</td><td><code>#8b97a5 on #011526</code></td><td class="ir-px">7.1:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>Dashboard header (ink on navy-2)</td><td><code>#f0efe9 on #01223f</code></td><td class="ir-px">13.8:1</td><td>AAA ✓</td></tr>
|
||
<tr><td>CTA (primary on mint)</td><td><code>#012851 on #a1dcd8</code></td><td class="ir-px">9.6:1</td><td>AAA ✓</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.</strong>
|
||
<ul>
|
||
<li>Stats are real <code><dl></code> / <code><dt></code> / <code><dd></code> pairs. Screen readers announce "Briefe gesamt: 851, ausgehend: 612, …".</li>
|
||
<li>Histogram wrapped in <code>role="img"</code> with <code>aria-label="Aktivität über 42 Jahre, Spitzenjahr 1922 mit 78 Briefen"</code>. Each bar has a <code>title</code> attribute for sighted tooltip.</li>
|
||
<li>Top lists are semantic <code><ol></code> elements — screen readers announce "Top Korrespondenten, list 6 items, 1 of 6 Walter Dieckmann, 184 Briefe".</li>
|
||
<li>Tag cloud is a <code><ul></code> of <code><li><a></code>. The visual size does <em>not</em> 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".</li>
|
||
<li>Focus ring: <code>focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2</code> on every interactive element (histogram bars, top-list rows, tag chips, header CTA).</li>
|
||
<li>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.</li>
|
||
<li><code>prefers-reduced-motion</code>: disable bar width animation on first render and the tag-chip hover lift. The skeleton shimmer collapses to a static gradient.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════
|
||
SECTION 06 — IMPLEMENTATION NOTES
|
||
═══════════════════════════════════════════════════════════ -->
|
||
<section class="sec">
|
||
<h2 class="sec-h"><span class="sec-num">06</span>Implementation Notes — Backend + Components</h2>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">New backend endpoint<span>aggregation for dashboard</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> to keep person entity lean and let the dashboard query be 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</td><td>Server-side cache keyed on <code>(personId, dataVersion)</code></td><td>Invalidate on Document write that references this person (see <code>DocumentService</code> update hooks)</td></tr>
|
||
<tr><td>Response schema</td><td>see next table</td><td>All counts server-computed — never client-computed</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="impl-ref" style="margin-top:16px">
|
||
<div class="impl-ref-hdr">Response schema<span>PersonDashboardDTO</span></div>
|
||
<table>
|
||
<thead><tr><th>Field</th><th>Type</th><th>Note</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>totalCount</code></td><td>int</td><td>outCount + inCount</td></tr>
|
||
<tr><td><code>outCount</code></td><td>int</td><td>Letters where this person is sender</td></tr>
|
||
<tr><td><code>inCount</code></td><td>int</td><td>Letters where this person is in receivers</td></tr>
|
||
<tr><td><code>yearSpan</code></td><td>int</td><td>latestYear - earliestYear + 1 (null when no letters)</td></tr>
|
||
<tr><td><code>correspondentCount</code></td><td>int</td><td>Distinct counterparts</td></tr>
|
||
<tr><td><code>activityByYear</code></td><td>Map<int, int></td><td>year → count · always contiguous (missing years = 0 · dashboard decides display)</td></tr>
|
||
<tr><td><code>peakYear</code></td><td>int</td><td>Year with most letters (null when no letters)</td></tr>
|
||
<tr><td><code>peakYearCount</code></td><td>int</td><td></td></tr>
|
||
<tr><td><code>topCorrespondents</code></td><td>List<CorrespondentTileDTO></td><td>Max 10 · sorted desc · each has <code>personId · displayName · count</code></td></tr>
|
||
<tr><td><code>topLocations</code></td><td>List<LocationTileDTO></td><td>Max 10 · each has <code>location · count</code></td></tr>
|
||
<tr><td><code>topTags</code></td><td>List<TagTileDTO></td><td>Max 20 · each has <code>tagId · label · count</code> · frontend buckets into size tiers</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="impl-ref" style="margin-top:16px">
|
||
<div class="impl-ref-hdr">Component structure<span>new files + changes</span></div>
|
||
<table>
|
||
<thead><tr><th>File</th><th>Responsibility</th><th>Change</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>PersonDashboard.svelte</code></td><td>Orchestrator · renders header + stats + sections</td><td>new</td></tr>
|
||
<tr><td><code>StatStrip.svelte</code></td><td>4-cell stats grid with direction colouring</td><td>new</td></tr>
|
||
<tr><td><code>ActivityHistogram.svelte</code></td><td>One bar per year, peak highlight, hover tooltip</td><td>new</td></tr>
|
||
<tr><td><code>DistributionBar.svelte</code></td><td>Already introduced in <code>briefwechsel-thumbnail-rows-spec.html</code> · re-used here with different labels</td><td>shared</td></tr>
|
||
<tr><td><code>TopTileList.svelte</code></td><td>Ordered list of tiles (name + bar + count) — used for correspondents and locations</td><td>new · generic</td></tr>
|
||
<tr><td><code>TagCloud.svelte</code></td><td>Frequency-sized chips with size buckets</td><td>new</td></tr>
|
||
<tr><td><code>PersonPageShell.svelte</code> / <code>+page.svelte</code></td><td>Renders 2-column grid</td><td>Replace <code>CoCorrespondentsList</code> with <code>PersonDashboard</code>. Remove <code>coCorrespondents</code> derivation in <code>+page.svelte</code> — dashboard owns it.</td></tr>
|
||
<tr><td><code>+page.server.ts</code></td><td>Loads person data</td><td>Add parallel call to <code>/api/persons/{id}/dashboard</code>; keep error handling identical</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> with <code>PersonDashboardDTO</code>. Cache on person-write hooks. Tests for empty / sparse / full.</li>
|
||
<li><b>Phase 2</b> — <code>PersonDashboard.svelte</code> + its six sub-components. Replaces <code>CoCorrespondentsList</code> in the right column.</li>
|
||
<li><b>Phase 3</b> — new query params on <code>/briefwechsel</code>: <code>direction</code>, <code>location</code>, <code>tagId</code>. Wire every dashboard element to them.</li>
|
||
<li><b>Phase 4</b> — axe-playwright tests at 320 / 768 / 1440 in light + dark. Visual regression snapshots for all four states.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|