672 lines
38 KiB
HTML
672 lines
38 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Stammbaum — Relationship Badge · Document Detail · Familienarchiv</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Tinos:ital,wght@0,400;0,700&family=Montserrat:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
|
<style>
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:'Montserrat',system-ui,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5;font-size:13px}
|
|
.doc{max-width:1300px;margin:0 auto;padding:48px 32px 120px}
|
|
|
|
/* ── Masthead ── */
|
|
.mh{padding-bottom:24px;border-bottom:3px solid #012851;margin-bottom:60px}
|
|
.mh h1{font-size:23px;font-weight:900;color:#012851;letter-spacing:-.4px}
|
|
.mh p{font-size:13px;color:#555;max-width:740px;line-height:1.75;margin-top:8px}
|
|
.mh .byline{font-size:9px;color:#999;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;margin-top:10px}
|
|
.tag-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
|
.tag{background:#012851;color:#A1DCD8;padding:2px 8px;border-radius:2px;font-size:8px;font-weight:700;letter-spacing:.8px;text-transform:uppercase}
|
|
.tag.amber{background:#7c4a00;color:#fde68a}
|
|
|
|
/* ── Section headers ── */
|
|
.sh{margin:0 0 28px}
|
|
.sh h2{font-size:16px;font-weight:900;color:#012851;letter-spacing:-.2px}
|
|
.sh p{font-size:12.5px;color:#666;max-width:720px;line-height:1.7;margin-top:5px}
|
|
.section{margin-bottom:80px;padding-bottom:80px;border-bottom:2px dashed #C8C4BE}
|
|
.section:last-of-type{border-bottom:none;margin-bottom:0;padding-bottom:0}
|
|
|
|
/* ── Token tables ── */
|
|
.token-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
|
|
.token-table{border-radius:6px;overflow:hidden}
|
|
.token-table.light{background:#fff;border:1px solid #E0DDD6}
|
|
.token-table.dark{background:#0F1923;border:1px solid #1E2D3D}
|
|
.token-head{padding:8px 14px;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid #E0DDD6}
|
|
.token-table.light .token-head{background:#F4F2EC;color:#888;border-bottom-color:#E0DDD6}
|
|
.token-table.dark .token-head{background:#0A1218;color:#4E6070;border-bottom-color:#1E2D3D}
|
|
.token-table table{width:100%;border-collapse:collapse;font-size:11px}
|
|
.token-table.light td{padding:6px 14px;border-bottom:1px solid #F0EEE8;vertical-align:middle}
|
|
.token-table.dark td{padding:6px 14px;border-bottom:1px solid #1A2830;vertical-align:middle;color:#8AAABB}
|
|
.token-table tr:last-child td{border-bottom:none}
|
|
.token-table.light td:first-child{font-size:9px;font-weight:700;color:#888;width:160px}
|
|
.token-table.dark td:first-child{font-size:9px;font-weight:700;color:#4E6070;width:160px}
|
|
.swatch{display:inline-block;width:12px;height:12px;border-radius:2px;vertical-align:middle;margin-right:6px}
|
|
.swatch.bordered{border:1px solid #DDD}
|
|
.warn{display:inline-block;background:#FEF3C7;color:#92400E;font-size:8px;font-weight:700;padding:1px 5px;border-radius:2px;margin-left:4px;vertical-align:middle}
|
|
.pass{display:inline-block;background:#D1FAE5;color:#065F46;font-size:8px;font-weight:700;padding:1px 5px;border-radius:2px;margin-left:4px;vertical-align:middle}
|
|
|
|
/* ── Browser chrome ── */
|
|
.chrome{border:1.5px solid #C4C0BA;border-radius:8px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.1)}
|
|
.chrome.dark{background:#010e1e;border-color:#0d3358}
|
|
.chrome-bar{height:20px;background:#E8E6E0;border-bottom:1px solid #C4C0BA;display:flex;align-items:center;padding:0 8px;gap:4px;flex-shrink:0}
|
|
.chrome.dark .chrome-bar{background:#010a18;border-bottom-color:#0d3358}
|
|
.chrome-dot{width:6px;height:6px;border-radius:50%;background:#BDB8B1}
|
|
.chrome.dark .chrome-dot{background:#1a2a3a}
|
|
.chrome-url{flex:1;height:9px;background:#CCC8C2;border-radius:5px;margin-left:6px}
|
|
.chrome.dark .chrome-url{background:#1a2a3a}
|
|
|
|
/* ── App nav ── */
|
|
.app-nav{height:30px;background:#012851;display:flex;align-items:center;padding:0 12px;gap:10px;flex-shrink:0}
|
|
.app-logo{font-family:'Tinos',Georgia,serif;font-size:7px;font-weight:700;color:#fff;border-bottom:2px solid #A1DCD8;padding-bottom:1px}
|
|
.app-link{font-size:5.5px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:rgba(255,255,255,.4);white-space:nowrap}
|
|
.app-link.on{color:rgba(255,255,255,.9)}
|
|
.app-nav-r{margin-left:auto;display:flex;gap:6px;align-items:center}
|
|
.app-av{width:16px;height:16px;border-radius:50%;background:rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font-size:5px;font-weight:800;color:rgba(255,255,255,.5)}
|
|
|
|
/* ── Document top bar (simulated) ── */
|
|
.doc-topbar{height:56px;background:#ffffff;border-bottom:1px solid #E4E2D8;display:flex;align-items:center;padding-right:12px;flex-shrink:0}
|
|
.chrome.dark .doc-topbar{background:#011526;border-bottom-color:#0d3358}
|
|
.accent-bar{width:3px;height:56px;background:#012851;flex-shrink:0}
|
|
.chrome.dark .accent-bar{background:#A1DCD8}
|
|
.back-btn{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#888;flex-shrink:0;margin:0 4px}
|
|
.tb-divider{width:1px;height:20px;background:#E4E2D8;flex-shrink:0;margin:0 6px}
|
|
.chrome.dark .tb-divider{background:#0d3358}
|
|
.tb-title-block{flex:1;min-width:0;padding:0 4px}
|
|
.tb-title{font-family:'Tinos',Georgia,serif;font-size:10px;font-weight:700;color:#012851;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.chrome.dark .tb-title{color:#f0efe9}
|
|
.tb-date{font-size:8px;color:#6b7280;margin-top:1px}
|
|
.chrome.dark .tb-date{color:#8b97a5}
|
|
.tb-btn-details{height:22px;padding:0 8px;border:1.5px solid #E4E2D8;border-radius:3px;font-size:7px;font-weight:700;color:#4b5563;display:flex;align-items:center;gap:3px;flex-shrink:0}
|
|
.chrome.dark .tb-btn-details{border-color:#0d3358;color:#8b97a5}
|
|
.tb-btn-primary{height:22px;padding:0 8px;background:#012851;border-radius:3px;font-size:7px;font-weight:700;color:#A1DCD8;display:flex;align-items:center;gap:3px;flex-shrink:0;margin-left:6px}
|
|
.chrome.dark .tb-btn-primary{background:#A1DCD8;color:#012851}
|
|
|
|
/* ── Metadata drawer ── */
|
|
.meta-drawer{background:#ffffff;border-bottom:1px solid #E4E2D8;padding:14px 16px;flex-shrink:0}
|
|
.chrome.dark .meta-drawer{background:#011526;border-bottom-color:#0d3358}
|
|
.meta-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px}
|
|
.meta-col-head{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.09em;color:#6b7280;margin-bottom:8px}
|
|
.chrome.dark .meta-col-head{color:#8b97a5}
|
|
.meta-field{margin-bottom:8px}
|
|
.meta-label{font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#6b7280;margin-bottom:3px}
|
|
.chrome.dark .meta-label{color:#8b97a5}
|
|
.meta-value{font-family:'Tinos',Georgia,serif;font-size:10px;color:#012851}
|
|
.chrome.dark .meta-value{color:#f0efe9}
|
|
|
|
/* ── Person card ── */
|
|
.person-card{display:flex;align-items:center;gap:5px;padding:3px 5px;border-radius:3px;cursor:default}
|
|
.person-card:hover{background:#f5f4ef}
|
|
.chrome.dark .person-card:hover{background:#011a30}
|
|
.p-av{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:6.5px;font-weight:800;color:#fff;flex-shrink:0}
|
|
.p-name{font-family:'Tinos',Georgia,serif;font-size:9.5px;color:#012851}
|
|
.chrome.dark .p-name{color:#f0efe9}
|
|
|
|
/* ── RELATIONSHIP BADGE ── */
|
|
.rel-badge{}
|
|
.rel-badge-label{font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#6b7280;margin-bottom:3px}
|
|
.chrome.dark .rel-badge-label{color:#8b97a5}
|
|
.rel-badge-row{display:flex;align-items:center;gap:5px;padding:0 5px}
|
|
.rel-value{font-family:'Tinos',Georgia,serif;font-size:10px;font-weight:700;color:#012851}
|
|
.chrome.dark .rel-value{color:#f0efe9}
|
|
.rel-arrow{width:9px;height:9px;flex-shrink:0;color:#A1DCD8}
|
|
.chrome.dark .rel-arrow{color:#00c7b1}
|
|
|
|
/* ── PDF placeholder ── */
|
|
.pdf-area{background:#d4d0c8;flex:1;display:flex;align-items:center;justify-content:center;min-height:80px}
|
|
.chrome.dark .pdf-area{background:#010e1e}
|
|
.paper{background:#FFFEF8;width:40%;box-shadow:0 2px 8px rgba(0,0,0,.14);border-radius:1px;padding:8px 10px;display:flex;flex-direction:column;gap:2px}
|
|
.chrome.dark .paper{background:#0d1820}
|
|
.pl{height:3px;background:#C4BDB0;border-radius:1px;opacity:.5;margin-bottom:2px}
|
|
.ps{height:2px;background:#C4BDB0;border-radius:1px;opacity:.28;margin-bottom:1.5px}
|
|
.chrome.dark .pl,.chrome.dark .ps{background:#1E2D3D}
|
|
|
|
/* ── Side-by-side layout ── */
|
|
.split-screens{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:16px}
|
|
.screen-lbl{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:8px;display:flex;align-items:center;gap:5px}
|
|
.lbl-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
|
|
.cap{font-size:10px;color:#999;font-style:italic;line-height:1.6;margin-top:10px;max-width:460px}
|
|
|
|
/* ── Edge-case row ── */
|
|
.edge-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:12px}
|
|
.edge-card{background:#fff;border:1px solid #E0DDD6;border-radius:6px;overflow:hidden}
|
|
.edge-head{background:#F4F2EC;padding:8px 12px;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;border-bottom:1px solid #E0DDD6}
|
|
.edge-body{padding:10px 12px}
|
|
.edge-note{font-size:10.5px;color:#555;line-height:1.65;margin-top:8px}
|
|
.no-badge{font-family:'Tinos',Georgia,serif;font-size:9px;color:#aaa;font-style:italic;padding:4px 5px}
|
|
|
|
/* ── Data flow diagram ── */
|
|
.flow{display:flex;flex-direction:column;gap:0;margin-bottom:24px;max-width:680px}
|
|
.flow-node{background:#fff;border:1.5px solid #E0DDD6;border-radius:4px;padding:8px 14px;font-size:11px;color:#333;display:flex;align-items:flex-start;gap:8px}
|
|
.flow-node.highlight{border-color:#012851;background:#EEF3FB}
|
|
.flow-node.new{border-color:#A1DCD8;background:#EAF7F6}
|
|
.flow-tag{font-size:7.5px;font-weight:800;padding:1px 5px;border-radius:10px;white-space:nowrap;margin-left:auto;flex-shrink:0;align-self:center}
|
|
.flow-tag.change{background:#DBEAFE;color:#1E40AF}
|
|
.flow-tag.new{background:#D1FAE5;color:#065F46}
|
|
.flow-tag.unchanged{background:#F3F4F6;color:#555}
|
|
.flow-arrow{text-align:center;font-size:14px;color:#C8C4BE;line-height:1;padding:3px 0}
|
|
.flow-cond{background:#FFFBEB;border:1.5px solid #FDE68A;border-radius:4px;padding:8px 14px;font-size:10.5px;color:#78350F;margin:0}
|
|
.flow-cond code{background:rgba(0,0,0,.06);padding:1px 4px;border-radius:2px;font-size:9.5px}
|
|
|
|
/* ── Rules table ── */
|
|
.rules{background:#fff;border:1px solid #E0DDD6;border-radius:6px;overflow:hidden}
|
|
.rules table{width:100%;border-collapse:collapse}
|
|
.rules th{background:#F4F2EC;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;padding:8px 12px;text-align:left;border-bottom:1px solid #E0DDD6}
|
|
.rules td{font-size:11px;color:#444;padding:8px 12px;border-bottom:1px solid #F0EEE8;vertical-align:top;line-height:1.6}
|
|
.rules tr:last-child td{border-bottom:none}
|
|
.rules td:first-child{font-size:9px;font-weight:700;color:#012851;white-space:nowrap;width:170px}
|
|
.rules td code{font-size:9px;background:#F0EFE9;padding:1px 4px;border-radius:2px;color:#555;white-space:nowrap}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="doc">
|
|
|
|
<!-- ══ MASTHEAD ══════════════════════════════════════════════════════════════ -->
|
|
<div class="mh">
|
|
<h1>Stammbaum — Relationship Badge · Document Detail</h1>
|
|
<p>
|
|
Spec for Part C of issue <strong>#358</strong>. A "Verwandtschaft" metadata row appears inside the
|
|
metadata drawer's Personen column when a family kinship can be inferred between the document's sender
|
|
and its single receiver. Implemented as a new presentational component
|
|
<code>RelationshipBadge.svelte</code>.
|
|
</p>
|
|
<div class="byline">Familienarchiv · 2026-04-27 · Leonie Voss, UX Lead</div>
|
|
<div class="tag-row">
|
|
<span class="tag">Issue #358</span>
|
|
<span class="tag">Part C — Badge only</span>
|
|
<span class="tag amber">Depends on PersonSummaryDTO.familyMember</span>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ══ SECTION 1 — DESIGN TOKENS ════════════════════════════════════════════ -->
|
|
<div class="section">
|
|
<div class="sh">
|
|
<h2>1 · Design tokens</h2>
|
|
<p>All colour values used by the badge and its context. The component uses only semantic Tailwind tokens — no hardcoded hex. Light and dark themes are handled automatically by <code>layout.css</code>.</p>
|
|
</div>
|
|
|
|
<div class="token-grid">
|
|
<!-- Light -->
|
|
<div class="token-table light">
|
|
<div class="token-head">Light theme — bg-surface = #ffffff</div>
|
|
<table>
|
|
<tr>
|
|
<td>text-ink-3</td>
|
|
<td><span class="swatch" style="background:#6b7280"></span>#6b7280 — "Verwandtschaft" label<span class="pass">4.8:1 AA ✓</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>text-ink</td>
|
|
<td><span class="swatch" style="background:#012851"></span>#012851 — label values ("Sohn", "Vater")<span class="pass">14.5:1 AAA ✓</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>text-accent</td>
|
|
<td><span class="swatch" style="background:#a1dcd8;border:1px solid #ccc"></span>#a1dcd8 — decorative arrow <code>aria-hidden</code><span class="warn">1.5:1 — non-text only</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>font-serif</td>
|
|
<td>Tinos — used for label values (matches person card names in same column)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>font-sans</td>
|
|
<td>Montserrat — used for "Verwandtschaft" label (matches "Von" / "An" labels above)</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<!-- Dark -->
|
|
<div class="token-table dark">
|
|
<div class="token-head">Dark theme — bg-surface = #011526</div>
|
|
<table>
|
|
<tr>
|
|
<td>text-ink-3</td>
|
|
<td><span class="swatch" style="background:#8b97a5"></span>#8b97a5 — "Verwandtschaft" label<span class="pass" style="background:rgba(209,250,229,.15);color:#6EE7B7;border:none">7.1:1 AAA ✓</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>text-ink</td>
|
|
<td><span class="swatch" style="background:#f0efe9"></span>#f0efe9 — label values ("Sohn", "Vater")<span class="pass" style="background:rgba(209,250,229,.15);color:#6EE7B7;border:none">14.5:1 AAA ✓</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>text-accent</td>
|
|
<td><span class="swatch" style="background:#00c7b1"></span>#00c7b1 — decorative arrow <code>aria-hidden</code><span class="warn" style="background:rgba(254,243,199,.1);color:#FDE68A;border:none">non-text only</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>bg-surface</td>
|
|
<td><span class="swatch" style="background:#011526;border:1px solid #0d3358"></span>#011526 — drawer background in dark mode</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<p style="font-size:10.5px;color:#888;font-style:italic;margin-top:6px">
|
|
⚠ <code>text-accent</code> (#a1dcd8 light / #00c7b1 dark) must only be used for the decorative arrow SVG.
|
|
It fails WCAG at small text sizes. The arrow carries no information — it is purely visual and is marked <code>aria-hidden="true"</code>.
|
|
</p>
|
|
</div>
|
|
|
|
|
|
<!-- ══ SECTION 2 — VISUAL MOCKUP ════════════════════════════════════════════ -->
|
|
<div class="section">
|
|
<div class="sh">
|
|
<h2>2 · Visual mockup — light & dark</h2>
|
|
<p>
|
|
The badge renders as the last item in the Personen column of the expandable metadata drawer.
|
|
Shown at ~65% scale. Drawer is open. Both themes side by side.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="split-screens">
|
|
|
|
<!-- Light -->
|
|
<div>
|
|
<div class="screen-lbl"><span class="lbl-dot" style="background:#A1DCD8;border:1px solid #ccc"></span>Light theme</div>
|
|
<div class="chrome">
|
|
<div class="chrome-bar"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
|
|
<div class="app-nav">
|
|
<div class="app-logo">Familienarchiv</div>
|
|
<div class="app-link">Dokumente</div>
|
|
<div class="app-link">Personen</div>
|
|
<div class="app-link on">—</div>
|
|
<div class="app-nav-r"><div class="app-av">M</div></div>
|
|
</div>
|
|
<!-- Doc top bar -->
|
|
<div class="doc-topbar">
|
|
<div class="accent-bar"></div>
|
|
<div class="back-btn">
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>
|
|
</div>
|
|
<div class="tb-divider"></div>
|
|
<div class="tb-title-block">
|
|
<div class="tb-title">Brief an die Eltern, 15. März 1923</div>
|
|
<div class="tb-date">15. März 1923</div>
|
|
</div>
|
|
<div class="tb-btn-details">Details ▾</div>
|
|
<div class="tb-btn-primary">Bearbeiten</div>
|
|
</div>
|
|
<!-- Metadata drawer OPEN -->
|
|
<div class="meta-drawer">
|
|
<div class="meta-grid">
|
|
<!-- Col 1: Details -->
|
|
<div>
|
|
<div class="meta-col-head">Details</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">Datum</div>
|
|
<div class="meta-value">15. März 1923</div>
|
|
</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">Ort</div>
|
|
<div class="meta-value">München</div>
|
|
</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">Status</div>
|
|
<div class="meta-value">Transkribiert</div>
|
|
</div>
|
|
</div>
|
|
<!-- Col 2: Personen — with badge -->
|
|
<div>
|
|
<div class="meta-col-head">Personen</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">Von</div>
|
|
<div class="person-card">
|
|
<div class="p-av" style="background:#5b7fa6">KR</div>
|
|
<div class="p-name">Karl Raddatz</div>
|
|
</div>
|
|
</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">An</div>
|
|
<div class="person-card">
|
|
<div class="p-av" style="background:#7a6b52">HR</div>
|
|
<div class="p-name">Heinrich Raddatz</div>
|
|
</div>
|
|
</div>
|
|
<!-- RELATIONSHIP BADGE -->
|
|
<div class="rel-badge">
|
|
<div class="rel-badge-label">Verwandtschaft</div>
|
|
<div class="rel-badge-row">
|
|
<span class="rel-value">Sohn</span>
|
|
<svg class="rel-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5-5 5M6 12h12"/></svg>
|
|
<span class="rel-value">Vater</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Col 3: Tags -->
|
|
<div>
|
|
<div class="meta-col-head">Schlagwörter</div>
|
|
<div style="display:flex;flex-wrap:wrap;gap:4px">
|
|
<span style="background:#f5f4ef;padding:2px 7px;border-radius:2px;font-size:7.5px;font-weight:800;text-transform:uppercase;color:#012851">Familie</span>
|
|
<span style="background:#f5f4ef;padding:2px 7px;border-radius:2px;font-size:7.5px;font-weight:800;text-transform:uppercase;color:#012851">Krieg</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="pdf-area">
|
|
<div class="paper"><div class="pl"></div><div class="ps"></div><div class="pl" style="width:80%"></div><div class="ps" style="width:65%"></div><div class="pl"></div><div class="ps" style="width:75%"></div></div>
|
|
</div>
|
|
</div>
|
|
<p class="cap">Drawer open. "Verwandtschaft" row sits below the single receiver card, using identical label and value typography to "Von" / "An". Arrow is mint accent, aria-hidden.</p>
|
|
</div>
|
|
|
|
<!-- Dark -->
|
|
<div>
|
|
<div class="screen-lbl"><span class="lbl-dot" style="background:#1a2a3a"></span>Dark theme</div>
|
|
<div class="chrome dark">
|
|
<div class="chrome-bar"><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-url"></div></div>
|
|
<div class="app-nav">
|
|
<div class="app-logo">Familienarchiv</div>
|
|
<div class="app-link">Dokumente</div>
|
|
<div class="app-link">Personen</div>
|
|
<div class="app-link on">—</div>
|
|
<div class="app-nav-r"><div class="app-av">M</div></div>
|
|
</div>
|
|
<div class="doc-topbar">
|
|
<div class="accent-bar"></div>
|
|
<div class="back-btn" style="color:#4E6070">
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>
|
|
</div>
|
|
<div class="tb-divider"></div>
|
|
<div class="tb-title-block">
|
|
<div class="tb-title">Brief an die Eltern, 15. März 1923</div>
|
|
<div class="tb-date">15. März 1923</div>
|
|
</div>
|
|
<div class="tb-btn-details">Details ▾</div>
|
|
<div class="tb-btn-primary">Bearbeiten</div>
|
|
</div>
|
|
<div class="meta-drawer">
|
|
<div class="meta-grid">
|
|
<div>
|
|
<div class="meta-col-head">Details</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">Datum</div>
|
|
<div class="meta-value">15. März 1923</div>
|
|
</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">Ort</div>
|
|
<div class="meta-value">München</div>
|
|
</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">Status</div>
|
|
<div class="meta-value">Transkribiert</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="meta-col-head">Personen</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">Von</div>
|
|
<div class="person-card">
|
|
<div class="p-av" style="background:#5b7fa6">KR</div>
|
|
<div class="p-name">Karl Raddatz</div>
|
|
</div>
|
|
</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label">An</div>
|
|
<div class="person-card">
|
|
<div class="p-av" style="background:#7a6b52">HR</div>
|
|
<div class="p-name">Heinrich Raddatz</div>
|
|
</div>
|
|
</div>
|
|
<div class="rel-badge">
|
|
<div class="rel-badge-label">Verwandtschaft</div>
|
|
<div class="rel-badge-row">
|
|
<span class="rel-value">Sohn</span>
|
|
<svg class="rel-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5-5 5M6 12h12"/></svg>
|
|
<span class="rel-value">Vater</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="meta-col-head">Schlagwörter</div>
|
|
<div style="display:flex;flex-wrap:wrap;gap:4px">
|
|
<span style="background:#011a30;padding:2px 7px;border-radius:2px;font-size:7.5px;font-weight:800;text-transform:uppercase;color:#A1DCD8">Familie</span>
|
|
<span style="background:#011a30;padding:2px 7px;border-radius:2px;font-size:7.5px;font-weight:800;text-transform:uppercase;color:#A1DCD8">Krieg</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="pdf-area">
|
|
<div class="paper"><div class="pl"></div><div class="ps"></div><div class="pl" style="width:80%"></div><div class="ps" style="width:65%"></div><div class="pl"></div><div class="ps" style="width:75%"></div></div>
|
|
</div>
|
|
</div>
|
|
<p class="cap">Dark mode. Same semantic tokens; surface flips to #011526, ink to #f0efe9, accent arrow to #00c7b1 (turquoise). Both label and value pass WCAG AAA on the dark surface.</p>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ══ SECTION 3 — EDGE CASES ════════════════════════════════════════════════ -->
|
|
<div class="section">
|
|
<div class="sh">
|
|
<h2>3 · Edge cases — when the badge is silent</h2>
|
|
<p>All three cases result in no "Verwandtschaft" row. The drawer's Personen column is unaffected.</p>
|
|
</div>
|
|
|
|
<div class="edge-grid">
|
|
|
|
<div class="edge-card">
|
|
<div class="edge-head">Not a family member</div>
|
|
<div class="edge-body">
|
|
<div class="meta-field">
|
|
<div class="meta-label" style="font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#6b7280;margin-bottom:3px">Von</div>
|
|
<div class="person-card" style="background:#fff"><div class="p-av" style="background:#5b7fa6">KR</div><div class="p-name" style="font-family:'Tinos',serif;font-size:9.5px;color:#012851">Karl Raddatz</div></div>
|
|
</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label" style="font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#6b7280;margin-bottom:3px">An</div>
|
|
<div class="person-card" style="background:#fff"><div class="p-av" style="background:#888">NE</div><div class="p-name" style="font-family:'Tinos',serif;font-size:9.5px;color:#012851">N.N. Engel</div></div>
|
|
</div>
|
|
<div class="no-badge">— no Verwandtschaft row —</div>
|
|
<div class="edge-note">Receiver does not have <code style="font-size:9px;background:#F0EFE9;padding:1px 4px;border-radius:2px">familyMember = true</code>. Inference endpoint is never called.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="edge-card">
|
|
<div class="edge-head">Multiple receivers</div>
|
|
<div class="edge-body">
|
|
<div class="meta-field">
|
|
<div class="meta-label" style="font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#6b7280;margin-bottom:3px">Von</div>
|
|
<div class="person-card" style="background:#fff"><div class="p-av" style="background:#5b7fa6">KR</div><div class="p-name" style="font-family:'Tinos',serif;font-size:9.5px;color:#012851">Karl Raddatz</div></div>
|
|
</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label" style="font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#6b7280;margin-bottom:3px">An</div>
|
|
<div class="person-card" style="background:#fff"><div class="p-av" style="background:#7a6b52">HR</div><div class="p-name" style="font-family:'Tinos',serif;font-size:9.5px;color:#012851">Heinrich Raddatz</div></div>
|
|
<div class="person-card" style="background:#fff"><div class="p-av" style="background:#6a7a52">ER</div><div class="p-name" style="font-family:'Tinos',serif;font-size:9.5px;color:#012851">Elfriede Raddatz</div></div>
|
|
</div>
|
|
<div class="no-badge">— no Verwandtschaft row —</div>
|
|
<div class="edge-note"><code style="font-size:9px;background:#F0EFE9;padding:1px 4px;border-radius:2px">receivers.length > 1</code>: badge is silently omitted. Multi-receiver documents are rare in this archive; revisit if data shows otherwise.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="edge-card">
|
|
<div class="edge-head">No kinship path found</div>
|
|
<div class="edge-body">
|
|
<div class="meta-field">
|
|
<div class="meta-label" style="font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#6b7280;margin-bottom:3px">Von</div>
|
|
<div class="person-card" style="background:#fff"><div class="p-av" style="background:#5b7fa6">KR</div><div class="p-name" style="font-family:'Tinos',serif;font-size:9.5px;color:#012851">Karl Raddatz</div></div>
|
|
</div>
|
|
<div class="meta-field">
|
|
<div class="meta-label" style="font-size:7px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:#6b7280;margin-bottom:3px">An</div>
|
|
<div class="person-card" style="background:#fff"><div class="p-av" style="background:#7a6b52">HR</div><div class="p-name" style="font-family:'Tinos',serif;font-size:9.5px;color:#012851">Heinrich Raddatz</div></div>
|
|
</div>
|
|
<div class="no-badge">— no Verwandtschaft row —</div>
|
|
<div class="edge-note">Both are family members, but the backend returns <code style="font-size:9px;background:#F0EFE9;padding:1px 4px;border-radius:2px">404</code> (no path in graph). <code>inferredRelationship</code> is set to <code>null</code>. No error shown.</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ══ SECTION 4 — DATA FLOW ═════════════════════════════════════════════════ -->
|
|
<div class="section">
|
|
<div class="sh">
|
|
<h2>4 · Data flow</h2>
|
|
<p><code>inferredRelationship</code> is loaded server-side and passed as a <strong>separate prop</strong> alongside the document — it is not added to the document object.</p>
|
|
</div>
|
|
|
|
<div class="flow">
|
|
<div class="flow-node highlight">
|
|
<div>
|
|
<strong>+page.server.ts</strong><br>
|
|
<span style="font-size:10.5px;color:#555">Loads document as today. Then, if <code style="font-size:9.5px;background:#eef3fb;padding:1px 3px;border-radius:2px">sender.familyMember && receivers.length === 1 && receivers[0].familyMember</code>, calls <code style="font-size:9.5px;background:#eef3fb;padding:1px 3px;border-radius:2px">GET /api/persons/{senderId}/relationship-to/{receiverId}</code>. 404 → null. Returns <code style="font-size:9.5px;background:#eef3fb;padding:1px 3px;border-radius:2px">{ document, inferredRelationship }</code>.</span>
|
|
</div>
|
|
<span class="flow-tag change">Modified</span>
|
|
</div>
|
|
<div class="flow-arrow">↓</div>
|
|
<div class="flow-node">
|
|
<div>
|
|
<strong>+page.svelte</strong><br>
|
|
<span style="font-size:10.5px;color:#555">Receives <code style="font-size:9.5px;background:#f0efe9;padding:1px 3px;border-radius:2px">data.inferredRelationship</code>. Passes to <code style="font-size:9.5px;background:#f0efe9;padding:1px 3px;border-radius:2px"><DocumentTopBar></code> as a new optional prop.</span>
|
|
</div>
|
|
<span class="flow-tag change">Modified</span>
|
|
</div>
|
|
<div class="flow-arrow">↓</div>
|
|
<div class="flow-node">
|
|
<div>
|
|
<strong>DocumentTopBar.svelte</strong><br>
|
|
<span style="font-size:10.5px;color:#555">New optional prop <code style="font-size:9.5px;background:#f0efe9;padding:1px 3px;border-radius:2px">inferredRelationship?: { labelFromA: string; labelFromB: string } | null</code>. Passes through to <code style="font-size:9.5px;background:#f0efe9;padding:1px 3px;border-radius:2px"><DocumentMetadataDrawer></code>.</span>
|
|
</div>
|
|
<span class="flow-tag change">Modified</span>
|
|
</div>
|
|
<div class="flow-arrow">↓</div>
|
|
<div class="flow-node">
|
|
<div>
|
|
<strong>DocumentMetadataDrawer.svelte</strong><br>
|
|
<span style="font-size:10.5px;color:#555">New optional prop. Renders <code style="font-size:9.5px;background:#f0efe9;padding:1px 3px;border-radius:2px">{#if inferredRelationship} <RelationshipBadge .../> {/if}</code> at the bottom of the Personen column.</span>
|
|
</div>
|
|
<span class="flow-tag change">Modified</span>
|
|
</div>
|
|
<div class="flow-arrow">↓</div>
|
|
<div class="flow-node new">
|
|
<div>
|
|
<strong>RelationshipBadge.svelte</strong> — <code style="font-size:9.5px;background:#eaf7f6;padding:1px 3px;border-radius:2px">src/lib/components/RelationshipBadge.svelte</code><br>
|
|
<span style="font-size:10.5px;color:#555">Props: <code style="font-size:9.5px;background:#eaf7f6;padding:1px 3px;border-radius:2px">labelFromA: string</code>, <code style="font-size:9.5px;background:#eaf7f6;padding:1px 3px;border-radius:2px">labelFromB: string</code>. Purely presentational — no logic, no API calls.</span>
|
|
</div>
|
|
<span class="flow-tag new">New</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flow-cond">
|
|
<strong>Conditional in +page.server.ts</strong><br><br>
|
|
<code>sender.familyMember</code> requires <code>PersonSummaryDTO.familyMember: boolean</code> on the backend (part of issue #358 backend work). Until that field ships, the condition evaluates to <code>false</code> and the badge never renders — silent, correct fallback. No feature flag needed.
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ══ SECTION 5 — COMPONENT MARKUP ═════════════════════════════════════════ -->
|
|
<div class="section">
|
|
<div class="sh">
|
|
<h2>5 · RelationshipBadge.svelte — exact markup</h2>
|
|
</div>
|
|
|
|
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:6px;overflow:hidden;margin-bottom:24px">
|
|
<div style="background:#F4F2EC;padding:8px 14px;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;border-bottom:1px solid #E0DDD6">src/lib/components/RelationshipBadge.svelte</div>
|
|
<pre style="padding:16px;font-size:11px;color:#333;line-height:1.7;overflow-x:auto"><code><script lang="ts">
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
|
|
type Props = { labelFromA: string; labelFromB: string };
|
|
let { labelFromA, labelFromB }: Props = $props();
|
|
</script>
|
|
|
|
<div>
|
|
<p class="mb-1 font-sans text-xs font-medium text-ink-3">
|
|
{m.doc_details_field_relationship()}
|
|
</p>
|
|
<div class="flex items-center gap-1.5 px-2 font-serif text-sm text-ink">
|
|
<span class="font-semibold">{labelFromA}</span>
|
|
<svg
|
|
class="h-3 w-3 shrink-0 text-accent"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2.5"
|
|
aria-hidden="true"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5-5 5M6 12h12" />
|
|
</svg>
|
|
<span class="font-semibold">{labelFromB}</span>
|
|
</div>
|
|
</div></code></pre>
|
|
</div>
|
|
|
|
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:6px;overflow:hidden">
|
|
<div style="background:#F4F2EC;padding:8px 14px;font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;border-bottom:1px solid #E0DDD6">Placement in DocumentMetadataDrawer.svelte — Personen column, end of block</div>
|
|
<pre style="padding:16px;font-size:11px;color:#333;line-height:1.7;overflow-x:auto"><code><!-- existing receiver section -->
|
|
{#if receivers.length > 0}
|
|
<div>
|
|
<p class="mb-1 font-sans text-xs font-medium text-ink-3">
|
|
{m.doc_details_field_receivers()}
|
|
</p>
|
|
<!-- ... receiver cards ... -->
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- new: relationship badge —— after receivers -->
|
|
{#if inferredRelationship}
|
|
<RelationshipBadge
|
|
labelFromA={inferredRelationship.labelFromA}
|
|
labelFromB={inferredRelationship.labelFromB}
|
|
/>
|
|
{/if}</code></pre>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ══ SECTION 6 — i18n ══════════════════════════════════════════════════════ -->
|
|
<div class="section">
|
|
<div class="sh">
|
|
<h2>6 · i18n</h2>
|
|
<p>One new key. The relationship label strings (<code>labelFromA</code> / <code>labelFromB</code>) come pre-translated from the backend — no additional frontend keys needed for them.</p>
|
|
</div>
|
|
|
|
<div class="rules">
|
|
<table>
|
|
<thead><tr><th>Key</th><th>de (default)</th><th>en</th><th>es</th></tr></thead>
|
|
<tbody>
|
|
<tr><td>doc_details_field_relationship</td><td>Verwandtschaft</td><td>Relationship</td><td>Parentesco</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ══ SECTION 7 — IMPLEMENTATION NOTES ═════════════════════════════════════ -->
|
|
<div class="section">
|
|
<div class="sh">
|
|
<h2>7 · Implementation notes</h2>
|
|
</div>
|
|
<div class="rules">
|
|
<table>
|
|
<tbody>
|
|
<tr>
|
|
<td>Arrow is decorative</td>
|
|
<td>The SVG arrow carries no semantic information — the directional meaning is already in the label order (sender's label first, receiver's label second). Always <code>aria-hidden="true"</code>. Screen readers announce: "Verwandtschaft: Sohn. Vater." which is unambiguous.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>text-accent on arrow</td>
|
|
<td>Use <code>text-accent</code> (maps to <code>#a1dcd8</code> light / <code>#00c7b1</code> dark) only on the arrow SVG stroke. Never on any text element. Both values fail WCAG at body text sizes — they are only safe for non-text decorative elements.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>No logic in component</td>
|
|
<td><code>RelationshipBadge.svelte</code> has no conditional logic and no API calls. All conditions (familyMember flags, receiver count, 404 handling) live in <code>+page.server.ts</code>. If <code>inferredRelationship</code> is non-null, the badge renders — full stop.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Backend prerequisite</td>
|
|
<td><code>PersonSummaryDTO</code> must expose <code>familyMember: boolean</code>. Until it ships, <code>sender.familyMember</code> is <code>undefined</code> → condition is <code>false</code> → no API call made → badge silently absent. No code changes needed on both sides of the ship date.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Multiple receivers</td>
|
|
<td>Check <code>receivers.length === 1</code> in <code>+page.server.ts</code> before calling the inference endpoint. Do not call it for 0 or 2+ receivers. Badge is absent on multi-receiver documents regardless of family membership.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Prop threading</td>
|
|
<td><code>inferredRelationship</code> is typed as <code>{ labelFromA: string; labelFromB: string } | null</code> on <code>DocumentTopBar</code> and <code>DocumentMetadataDrawer</code>. The <code>path</code> field from <code>InferredRelationshipDTO</code> is loaded in the server but not passed to the component — reserved for a future tooltip.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Future tooltip</td>
|
|
<td>The inference path (<code>data.inferredRelationship.path</code>) is available server-side and can be surfaced as a tooltip on the badge in a follow-up. No design work needed now — just thread the prop when the time comes.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /doc -->
|
|
</body>
|
|
</html>
|