Files
familienarchiv/docs/specs/documents-page-spec.html
Marcel 148710f2ed
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m35s
CI / OCR Service Tests (push) Successful in 27s
CI / Backend Unit Tests (push) Failing after 1m26s
CI / OCR Service Tests (pull_request) Successful in 31s
CI / Backend Unit Tests (pull_request) Failing after 1m28s
CI / Unit & Component Tests (pull_request) Failing after 2m33s
docs(spec): add /documents page design spec with mobile breakpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 21:39:56 +02:00

666 lines
46 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Dokumente-Seite — Design Spec</title>
<style>
:root{
--navy:#002850;--mint:#A6DAD8;--sand:#E4E2D7;
--surface:#FAFAF7;--bg:#E8E7E2;--border:#D8D7D0;
--text:#1C1C18;--muted:#6B6A63;--subtle:#9B9A93;
--orange:#C26A00;--orange-bg:#FEF4E2;
--green:#2E6E39;--green-bg:#EAF5EA;
--purple:#5B5EA6;--purple-bg:#EEEDFE;
--font:system-ui,sans-serif;--mono:'Courier New',monospace;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14px;line-height:1.6;}
.doc{max-width:1100px;margin:0 auto;padding:48px 32px 96px;}
hr{border:none;border-top:1px solid var(--border);margin:48px 0;}
/* Header */
.hdr{background:var(--navy);color:#fff;padding:32px 32px 28px;border-radius:8px 8px 0 0;}
.hdr h1{font-family:Georgia,serif;font-size:26px;font-weight:400;letter-spacing:-.02em;margin-bottom:8px;}
.hdr-meta{font-family:var(--mono);font-size:11px;color:rgba(255,255,255,.45);margin-top:10px;}
.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;letter-spacing:.05em;background:var(--mint);color:var(--navy);}
.badge-g{background:rgba(255,255,255,.15);color:rgba(255,255,255,.9);}
.badges{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;}
.decision-box{background:#fff;border:1px solid var(--border);border-top:none;border-radius:0 0 6px 6px;padding:20px 28px 24px;margin-bottom:40px;}
.decision-box h2{font-family:Georgia,serif;font-size:16px;font-weight:400;color:var(--navy);margin-bottom:8px;}
.prose{font-size:13px;color:var(--muted);line-height:1.65;max-width:720px;margin-bottom:10px;}
.prose:last-child{margin-bottom:0;}
/* Sections */
.sec{margin-bottom:52px;}
.sec-label{font-size:10px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);padding-bottom:8px;border-bottom:1px solid var(--border);margin-bottom:22px;}
.sec-title{font-family:Georgia,serif;font-size:20px;font-weight:400;color:var(--navy);margin-bottom:4px;}
.sec-sub{font-size:13px;color:var(--muted);margin-bottom:16px;}
/* Callout */
.callout{display:flex;gap:12px;padding:14px 16px;border-radius:4px;margin-bottom:16px;font-size:12px;line-height:1.55;}
.callout.orange{background:var(--orange-bg);border-left:3px solid var(--orange);}
.callout.green{background:var(--green-bg);border-left:3px solid var(--green);}
.callout.navy{background:rgba(0,40,80,.05);border-left:3px solid var(--navy);}
.callout.purple{background:var(--purple-bg);border-left:3px solid var(--purple);}
.callout strong{font-weight:700;}
.callout strong.o{color:var(--orange);}
.callout strong.g{color:var(--green);}
.callout strong.n{color:var(--navy);}
/* impl-ref */
.impl-ref{margin-top:20px;}
.impl-ref table{width:100%;border-collapse:collapse;font-size:12px;}
.impl-ref th{background:var(--navy);color:#fff;padding:6px 10px;text-align:left;font-size:10px;font-weight:600;letter-spacing:.06em;}
.impl-ref td{padding:7px 10px;border-bottom:1px solid var(--border);vertical-align:top;line-height:1.6;}
.impl-ref tr:nth-child(even) td{background:var(--surface);}
.impl-ref code{font-family:var(--mono);font-size:11px;background:rgba(0,40,80,.06);padding:1px 4px;border-radius:2px;}
.impl-ref .new{color:var(--green);font-weight:600;}
.impl-ref .changed{color:var(--orange);font-weight:600;}
/* caption */
.caption{font-family:var(--mono);font-size:10px;color:var(--muted);display:block;margin-top:6px;}
/* Two-col layout */
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:24px;}
.three-col{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:24px;}
/* ─── MOCKUP FRAME ───────────────────────────── */
/* Scaled at ~56% (640px wide renders as ~1140px concept) */
.frame-wrap{background:var(--surface);border:1px solid var(--border);border-radius:6px;overflow:hidden;box-shadow:0 4px 16px rgba(0,0,0,.08);margin-bottom:8px;}
/* Topbar */
.f-topbar{background:var(--navy);height:26px;display:flex;align-items:center;padding:0 14px;gap:12px;}
.f-logo{font-size:6.5px;font-weight:700;color:#fff;letter-spacing:.7px;}
.f-navlinks{display:flex;gap:8px;margin-left:6px;}
.f-navlink{font-size:5.5px;color:rgba(255,255,255,.4);font-weight:600;text-transform:uppercase;letter-spacing:.05em;}
.f-navlink.on{color:rgba(255,255,255,.9);border-bottom:1.5px solid var(--mint);padding-bottom:1px;}
.f-navr{margin-left:auto;display:flex;align-items:center;gap:4px;}
.f-uname{font-size:5.5px;color:rgba(255,255,255,.4);text-transform:uppercase;font-weight:600;}
.f-av{width:14px;height:14px;border-radius:50%;background:var(--mint);display:flex;align-items:center;justify-content:center;font-size:4.5px;font-weight:800;color:var(--navy);}
/* Search bar */
.f-searchbar{background:#fff;border-bottom:1px solid var(--sand);padding:7px 14px;display:flex;align-items:center;gap:8px;}
.f-search-input{flex:1;height:18px;border:1.5px solid var(--navy);border-radius:2px;padding:0 6px;display:flex;align-items:center;gap:4px;}
.f-search-q{font-size:6.5px;color:var(--navy);font-weight:600;}
.f-search-count{font-size:5.5px;font-weight:600;color:var(--subtle);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;}
.f-newbtn{height:18px;padding:0 7px;background:var(--navy);border-radius:2px;font-size:5.5px;font-weight:700;color:#fff;text-transform:uppercase;letter-spacing:.05em;display:flex;align-items:center;}
/* Sort bar */
.f-sortbar{background:#fff;border-bottom:1px solid var(--sand);padding:5px 14px;display:flex;align-items:center;gap:8px;}
.f-sortcount{flex:1;font-size:5.5px;color:var(--subtle);}
.f-sortlabel{font-size:5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--subtle);}
.f-sortsel{height:14px;border:1px solid var(--sand);border-radius:2px;padding:0 5px;font-size:5.5px;color:var(--navy);background:var(--sand);display:flex;align-items:center;}
.f-filterbtn{height:14px;padding:0 6px;background:var(--navy);border-radius:2px;font-size:5px;font-weight:700;color:#fff;text-transform:uppercase;display:flex;align-items:center;gap:3px;}
.f-fbadge{background:var(--mint);color:var(--navy);font-size:4.5px;font-weight:800;border-radius:8px;padding:0 3px;}
/* Filter panel (open) */
.f-filterpanel{background:#fff;border-bottom:1px solid var(--sand);padding:8px 14px;display:flex;gap:16px;}
.f-fpgroup{flex:1;}
.f-fplabel{font-size:4.5px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--subtle);margin-bottom:4px;display:block;}
.f-fpinput{height:12px;border:1px solid var(--sand);border-radius:2px;padding:0 5px;font-size:5px;color:var(--subtle);background:var(--sand);width:100%;display:flex;align-items:center;}
.f-fpdate{display:flex;gap:3px;}
.f-fpdate .f-fpinput{flex:1;}
.f-fptag{font-size:4.5px;font-weight:700;background:var(--mint);color:var(--navy);border-radius:2px;padding:1px 5px;display:inline-block;margin-right:2px;margin-bottom:2px;}
.f-fptag-e{font-size:4.5px;font-weight:700;background:var(--sand);color:var(--subtle);border-radius:2px;padding:1px 5px;display:inline-block;margin-right:2px;margin-bottom:2px;}
/* List body */
.f-body{padding:8px 14px;}
/* Year card */
.f-yearcard{border:1px solid var(--border);background:#fff;margin-bottom:8px;overflow:hidden;box-shadow:0 1px 2px rgba(0,0,0,.04);}
.f-yearhead{background:var(--sand);padding:3px 10px;font-size:5.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--subtle);border-bottom:1px solid var(--border);}
/* Document row */
.f-docrow{display:flex;border-bottom:1px solid #ece9e0;}
.f-docrow:last-child{border-bottom:none;}
.f-docrow:hover{background:#fafaf8;}
.f-docleft{flex:1;min-width:0;padding:8px 10px;border-right:1px solid #ece9e0;}
.f-docright{width:110px;flex-shrink:0;padding:6px 9px;display:flex;flex-direction:column;justify-content:space-between;}
.f-doctitle{font-family:Georgia,serif;font-size:7px;font-weight:700;color:var(--navy);margin-bottom:3px;line-height:1.3;}
.f-doctitle .hl{border-bottom:1.5px solid var(--navy);}
.f-docsnip{font-family:Georgia,serif;font-size:5.5px;color:#4b5563;font-style:italic;line-height:1.5;margin-bottom:4px;}
.f-docsnip .hl{border-bottom:1.5px solid var(--navy);}
.f-doctags{display:flex;gap:2px;flex-wrap:wrap;}
.f-doctag{font-size:4.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;background:#dbeafe;color:var(--navy);border-radius:1px;padding:1px 4px;display:flex;align-items:center;gap:2px;}
.f-doctag .dot{width:4px;height:4px;border-radius:50%;background:#3b82f6;flex-shrink:0;}
.f-doctag.fam{background:#dcfce7;}.f-doctag.fam .dot{background:#16a34a;}
.f-ml{display:flex;align-items:center;gap:3px;font-size:5px;color:var(--muted);margin-bottom:2px;}
.f-ml strong{font-size:4.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--navy);}
.f-meta-bottom{display:flex;align-items:center;justify-content:space-between;gap:4px;margin-top:4px;}
/* Ring */
.f-ring{position:relative;width:20px;height:20px;flex-shrink:0;}
.f-ring svg{position:absolute;top:0;left:0;transform:rotate(-90deg);}
.f-ring-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:4px;font-weight:800;}
/* Contributors */
.f-contribs{display:flex;}
.f-cav{width:12px;height:12px;border-radius:50%;border:1.5px solid white;display:flex;align-items:center;justify-content:center;font-size:4px;font-weight:800;color:#fff;margin-left:-3px;}
.f-contribs .f-cav:first-child{margin-left:0;}
/* annotation box */
.anno-box{background:#fff;border:1px solid var(--border);border-radius:6px;padding:16px 20px;margin-bottom:16px;}
.anno-box h4{font-size:12px;font-weight:700;color:var(--navy);margin-bottom:6px;}
.anno-box p{font-size:12px;color:var(--muted);line-height:1.55;margin-bottom:8px;}
.anno-box p:last-child{margin-bottom:0;}
.anno-box code{font-family:var(--mono);font-size:10px;background:rgba(0,40,80,.06);padding:1px 4px;border-radius:2px;}
</style>
</head>
<body>
<div class="doc">
<!-- ══════════════════════════════════════
HEADER
══════════════════════════════════════ -->
<div class="hdr">
<div class="badges">
<span class="badge">Neue Route</span>
<span class="badge badge-g">Frontend</span>
<span class="badge badge-g">Backend</span>
</div>
<h1>Dokumente-Seite — /documents</h1>
<p style="font-size:13px;color:rgba(255,255,255,.6);margin-top:6px;max-width:680px;">
Dedicated search and browse page for all documents. Separates the document list from the dashboard hub. Uses per-year group cards with flat divide-y rows, a horizontal split row (content left · metadata right), a circular progress ring, and contributor avatars.
</p>
<div class="hdr-meta">Spec · Leonie Voss · 2026-04-19 · Issue TBD</div>
</div>
<div class="decision-box">
<h2>Design decisions</h2>
<p class="prose">The hub (<code>/</code>) becomes pure dashboard — no more dual-mode switching. The "Documents" nav tab points to <code>/documents</code>, a focused search/browse page.</p>
<p class="prose">Row layout: two-column split — title and snippet occupy the full left column for maximum scan width; date, sender, receiver, archive location, progress ring and contributor avatars live in a fixed 240px right panel. This keeps metadata consistently positioned across all rows.</p>
<p class="prose">List structure: one white card container per year group (matching the current <code>border border-line bg-surface shadow-sm</code> pattern), rows separated by <code>divide-y</code> dividers — no gaps, no individual row cards. The year label is an inset header row within each card.</p>
<p class="prose">Progress ring shows work completion as a percentage (0100%). It is driven by a new <code>completionPercentage</code> field on the search result DTO, computed server-side from annotation block counts. Contributor avatars require a new <code>contributors</code> array (initials + color) on the search DTO.</p>
</div>
<hr/>
<!-- ══════════════════════════════════════
SECTION 1 — FULL MOCKUP
══════════════════════════════════════ -->
<div class="sec">
<div class="sec-label">Section 1</div>
<div class="sec-title">Full page mockup — filter panel open, search active</div>
<div class="sec-sub">Scaled at ~56%. Desktop 1200px concept width.</div>
<div class="frame-wrap">
<!-- Topbar -->
<div class="f-topbar">
<div class="f-logo">FAMILIENARCHIV</div>
<div class="f-navlinks">
<span class="f-navlink on">Documents</span>
<span class="f-navlink">Persons</span>
<span class="f-navlink">Letters</span>
<span class="f-navlink">Admin</span>
</div>
<div class="f-navr"><span class="f-uname">Hochlader</span><div class="f-av">MR</div></div>
</div>
<!-- Search bar -->
<div class="f-searchbar">
<div class="f-search-input">
<svg width="8" height="8" viewBox="0 0 20 20" fill="none"><circle cx="8.5" cy="8.5" r="5.75" stroke="#9ca3af" stroke-width="1.5"/><path d="M13 13l3.5 3.5" stroke="#9ca3af" stroke-width="1.5" stroke-linecap="round"/></svg>
<span class="f-search-q">brief</span>
</div>
<span class="f-search-count">31 Dokumente</span>
<div class="f-newbtn">+ New Document</div>
</div>
<!-- Sort bar -->
<div class="f-sortbar">
<span class="f-sortcount">31 documents</span>
<span class="f-sortlabel">Sort</span>
<div class="f-sortsel">Date ↓</div>
<div class="f-filterbtn">
<svg width="7" height="7" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M4 8h8M6 12h4" stroke="white" stroke-width="1.5" stroke-linecap="round"/></svg>
Filters <span class="f-fbadge">1</span>
</div>
</div>
<!-- Filter panel -->
<div class="f-filterpanel">
<div class="f-fpgroup">
<span class="f-fplabel">Date range</span>
<div class="f-fpdate"><div class="f-fpinput">From</div><div class="f-fpinput">To</div></div>
</div>
<div class="f-fpgroup">
<span class="f-fplabel">Sender</span>
<div class="f-fpinput">Search person…</div>
</div>
<div class="f-fpgroup">
<span class="f-fplabel">Receiver</span>
<div class="f-fpinput">Search person…</div>
</div>
<div class="f-fpgroup">
<span class="f-fplabel">Tags</span>
<span class="f-fptag">Brief</span><span class="f-fptag-e">Foto</span><span class="f-fptag-e">Postkarte</span><span class="f-fptag-e">Urkunde</span>
</div>
</div>
<!-- List body -->
<div class="f-body">
<!-- 1924 card -->
<div class="f-yearcard">
<div class="f-yearhead">1924</div>
<div class="f-docrow">
<div class="f-docleft">
<div class="f-doctitle">Demo: Ierlicher <span class="hl">Brief</span> — Belgern</div>
<div class="f-docsnip">… Hiermit übersende ich Ihnen den gewünschten <span class="hl">Brief</span> meines Vaters, welcher einige interessante Hinweise zur Familiengeschichte enthält …</div>
<div class="f-doctags"><div class="f-doctag"><div class="dot"></div>Brief</div><div class="f-doctag fam"><div class="dot"></div>Familie</div></div>
</div>
<div class="f-docright">
<div>
<div class="f-ml"><strong>Date</strong> 31. Mai 1924</div>
<div class="f-ml"><strong>From</strong> Louise Aon Boden</div>
<div class="f-ml"><strong>To</strong> Marcel Raddatz</div>
<div class="f-ml"><strong>Archive</strong> Box 3 · Folder A</div>
</div>
<div class="f-meta-bottom">
<div class="f-ring">
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" fill="none" stroke="#E4E2D7" stroke-width="2"/><circle cx="10" cy="10" r="7" fill="none" stroke="#A6DAD8" stroke-width="2" stroke-dasharray="44 44" stroke-linecap="round"/></svg>
<div class="f-ring-label" style="color:#5bbab7">100%</div>
</div>
<div class="f-contribs"><div class="f-cav" style="background:#7c3aed">MR</div><div class="f-cav" style="background:#0891b2">LS</div></div>
</div>
</div>
</div>
</div>
<!-- 1923 card -->
<div class="f-yearcard">
<div class="f-yearhead">1923</div>
<div class="f-docrow">
<div class="f-docleft">
<div class="f-doctitle">W-0614 8. September 1923 Tölz</div>
<div class="f-docsnip">… Clara schreibt über die Ankunft in Tölz und erwähnt den letzten <span class="hl">Brief</span> von Fauld Rupley, der noch keine Antwort erhalten hat …</div>
<div class="f-doctags"><div class="f-doctag"><div class="dot"></div>Brief</div></div>
</div>
<div class="f-docright">
<div>
<div class="f-ml"><strong>Date</strong> 8. Sept. 1923</div>
<div class="f-ml"><strong>From</strong> Clara Lam</div>
<div class="f-ml"><strong>To</strong> Fauld Rupley</div>
<div class="f-ml"><strong>Archive</strong> Box 1 · Folder C</div>
</div>
<div class="f-meta-bottom">
<div class="f-ring">
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" fill="none" stroke="#E4E2D7" stroke-width="2"/><circle cx="10" cy="10" r="7" fill="none" stroke="#A6DAD8" stroke-width="2" stroke-dasharray="33 44" stroke-linecap="round"/></svg>
<div class="f-ring-label" style="color:#5bbab7">75%</div>
</div>
<div class="f-contribs"><div class="f-cav" style="background:#dc2626">AK</div></div>
</div>
</div>
</div>
<div class="f-docrow">
<div class="f-docleft">
<div class="f-doctitle">W-0196 2. September 1923 B. Lichterfelde</div>
<div class="f-docsnip">… Prediger's Haushaltung enthält einen <span class="hl">Brief</span>; Zusammen mit der Vollmacht aus dem Vorjahr ergibt sich folgendes Bild …</div>
<div class="f-doctags"><div class="f-doctag"><div class="dot"></div>Brief</div></div>
</div>
<div class="f-docright">
<div>
<div class="f-ml"><strong>Date</strong> 2. Sept. 1923</div>
<div class="f-ml"><strong>From</strong> Müller de Gruym</div>
<div class="f-ml"><strong>To</strong> Herbert Cram</div>
</div>
<div class="f-meta-bottom">
<div class="f-ring">
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" fill="none" stroke="#E4E2D7" stroke-width="2"/><circle cx="10" cy="10" r="7" fill="none" stroke="#A6DAD8" stroke-width="2" stroke-dasharray="18 44" stroke-linecap="round"/></svg>
<div class="f-ring-label" style="color:#9ca3af">40%</div>
</div>
<div class="f-contribs"><div class="f-cav" style="background:#7c3aed">MR</div><div class="f-cav" style="background:#0891b2">LS</div><div class="f-cav" style="background:#dc2626">AK</div></div>
</div>
</div>
</div>
<div class="f-docrow">
<div class="f-docleft">
<div class="f-doctitle">W-0397 2. September 1923 B. Lichterfelde</div>
<div class="f-docsnip">… zum einleitend Kommentar hieraus, den Herrn, zum <span class="hl">Brief</span> az sechzig und weitere Passagen …</div>
<div class="f-doctags"><div class="f-doctag"><div class="dot"></div>Brief</div></div>
</div>
<div class="f-docright">
<div>
<div class="f-ml"><strong>Date</strong> 2. Sept. 1923</div>
<div class="f-ml"><strong>From</strong> Müller de Gruym</div>
</div>
<div class="f-meta-bottom">
<div class="f-ring">
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" fill="none" stroke="#E4E2D7" stroke-width="2"/></svg>
<div class="f-ring-label" style="color:#9ca3af">0%</div>
</div>
<span style="font-size:4.5px;color:#9ca3af;font-weight:600;text-transform:uppercase;letter-spacing:.08em;">No contributors</span>
</div>
</div>
</div>
</div>
</div>
</div>
<span class="caption">Fig 1 — /documents · 1200px · search: "brief" · filter panel open · sort: Date ↓</span>
</div>
<hr/>
<!-- ══════════════════════════════════════
SECTION 2 — PAGE STRUCTURE
══════════════════════════════════════ -->
<div class="sec">
<div class="sec-label">Section 2</div>
<div class="sec-title">Page structure & zones</div>
<div class="three-col">
<div class="anno-box">
<h4>① Global search bar</h4>
<p>Full-width row below the topbar. Contains the search input (flex-1), result count (right of input), and "+ New Document" button. Background white, bottom border <code>border-line</code>. Sticky — stays visible on scroll.</p>
<p>Same search bar pattern as the current homepage. Debounce 500 ms on text input; immediate on clear.</p>
</div>
<div class="anno-box">
<h4>② Sort / count bar</h4>
<p>Slim bar below search. Shows result count (left), sort dropdown (right), and Filters toggle button (far right). Background white, bottom border <code>border-line</code>. Sticky — stacks below search bar on scroll.</p>
<p>Filters button shows a mint badge with active filter count. When filters are open the button fills navy.</p>
</div>
<div class="anno-box">
<h4>③ Collapsible filter panel</h4>
<p>Drops open below the sort bar. Contains four groups: Date range (two inputs), Sender (PersonTypeahead), Receiver (PersonTypeahead), Tags (clickable pills). White background, bottom border <code>border-line</code>.</p>
<p>Closed by default on page load unless URL already has active filter params. Animate open/close with <code>transition-all duration-200</code>.</p>
</div>
</div>
<div class="callout navy">
<div><strong class="n">Routing:</strong> New route <code>frontend/src/routes/documents/+page.svelte</code> and <code>+page.server.ts</code>. AppNav "Documents" tab <code>href</code> changes from <code>/</code> to <code>/documents</code>. Homepage <code>+page.svelte</code> loses dual-mode — always renders dashboard. No redirect from <code>/?q=…</code>.</div>
</div>
</div>
<hr/>
<!-- ══════════════════════════════════════
SECTION 2b — MOBILE BREAKPOINTS
══════════════════════════════════════ -->
<div class="sec">
<div class="sec-label">Section 2b</div>
<div class="sec-title">Mobile breakpoints</div>
<div class="sec-sub">Three responsive tiers: &lt;sm (mobile), smlg (tablet), lg+ (desktop).</div>
<div class="three-col">
<div class="anno-box">
<h4>&lt; sm — &lt; 640px (mobile)</h4>
<p>Document row: single-column block. Left/right split collapses. Metadata (date, from, to, archive) moves below the tags row as a 2×2 compact grid. Progress ring and contributor stack appear in a bottom row directly below the grid.</p>
<p>Filter panel: single-column stack (<code>flex-col</code>). Sort bar wraps if needed.</p>
</div>
<div class="anno-box">
<h4>sm lg — 6401023px</h4>
<p>Document row: two-column split restored. Metadata column narrower: <code>sm:w-48</code> (192px) instead of <code>w-60</code> to fit tablet viewports.</p>
<p>Sticky bars span full width via negative margins. Filter panel: <code>flex-row flex-wrap</code>, groups can wrap.</p>
</div>
<div class="anno-box">
<h4>lg+ — ≥ 1024px (desktop)</h4>
<p>Full two-column split. Metadata column: <code>lg:w-60</code> (240px). Filter panel: four groups in a single row. Max content width <code>max-w-7xl</code> (1280px) — from app layout container, no extra padding on list body.</p>
</div>
</div>
<!-- Mobile mockup at ~375px concept width, rendered at ~220px -->
<div class="two-col">
<div>
<div class="frame-wrap" style="width:220px;">
<div class="f-topbar" style="height:20px;padding:0 8px;gap:6px;">
<span class="f-logo" style="font-size:5px;">FAMILIENARCHIV</span>
<div style="margin-left:auto;display:flex;gap:6px;align-items:center;">
<span class="f-navlink on" style="font-size:4.5px;border-bottom-width:1px;">Dokumente</span>
<div class="f-av" style="width:12px;height:12px;font-size:4px;">MR</div>
</div>
</div>
<div class="f-searchbar" style="padding:5px 8px;gap:5px;">
<div class="f-search-input" style="height:14px;padding:0 5px;">
<svg width="6" height="6" viewBox="0 0 20 20" fill="none"><circle cx="8.5" cy="8.5" r="5.75" stroke="#9ca3af" stroke-width="1.5"/><path d="M13 13l3.5 3.5" stroke="#9ca3af" stroke-width="1.5" stroke-linecap="round"/></svg>
<span style="font-size:5px;color:#002850;font-weight:600;">brief</span>
</div>
<span style="font-size:4.5px;color:#9B9A93;white-space:nowrap;font-weight:700;text-transform:uppercase;letter-spacing:.06em;">31 Dok.</span>
</div>
<div class="f-sortbar" style="padding:3px 8px;gap:5px;">
<span class="f-sortcount">31 documents</span>
<span class="f-sortlabel">Sort</span>
<div class="f-sortsel" style="height:11px;font-size:4.5px;padding:0 4px;">Date ↓</div>
<div class="f-filterbtn" style="height:11px;font-size:4px;padding:0 4px;">Filters</div>
</div>
<div style="padding:5px 8px;">
<div class="f-yearcard">
<div class="f-yearhead">1924</div>
<!-- mobile stacked row 1 -->
<div style="padding:7px 8px;border-bottom:1px solid #ece9e0;">
<div class="f-doctitle" style="margin-bottom:2px;">Demo: Ierlicher <span class="hl">Brief</span> — Belgern</div>
<div class="f-docsnip" style="margin-bottom:3px;">… Hiermit übersende ich Ihnen den gewünschten <span class="hl">Brief</span></div>
<div class="f-doctags" style="margin-bottom:0;"><div class="f-doctag"><div class="dot"></div>Brief</div></div>
<div style="border-top:1px solid #ece9e0;margin-top:4px;padding-top:3px;display:grid;grid-template-columns:1fr 1fr;gap:0 8px;">
<div class="f-ml"><strong>Date</strong> 31. Mai 1924</div>
<div class="f-ml"><strong>From</strong> L. von Boden</div>
<div class="f-ml"><strong>Archive</strong> Box 3 · A</div>
<div class="f-ml"><strong>To</strong> M. Raddatz</div>
</div>
<div class="f-meta-bottom" style="margin-top:4px;">
<div class="f-ring">
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" fill="none" stroke="#E4E2D7" stroke-width="2"/><circle cx="10" cy="10" r="7" fill="none" stroke="#A6DAD8" stroke-width="2" stroke-dasharray="44 44" stroke-linecap="round"/></svg>
<div class="f-ring-label" style="color:#5bbab7">100%</div>
</div>
<div class="f-contribs"><div class="f-cav" style="background:#7c3aed">MR</div><div class="f-cav" style="background:#0891b2">LS</div></div>
</div>
</div>
<!-- mobile stacked row 2 -->
<div style="padding:7px 8px;">
<div class="f-doctitle" style="margin-bottom:2px;">W-0614 Sept. 1923 Tölz</div>
<div class="f-docsnip" style="margin-bottom:3px;">… Clara schreibt über den letzten <span class="hl">Brief</span> von Fauld Rupley …</div>
<div class="f-doctags" style="margin-bottom:0;"><div class="f-doctag"><div class="dot"></div>Brief</div></div>
<div style="border-top:1px solid #ece9e0;margin-top:4px;padding-top:3px;display:grid;grid-template-columns:1fr 1fr;gap:0 8px;">
<div class="f-ml"><strong>Date</strong> 8. Sept. 1923</div>
<div class="f-ml"><strong>From</strong> Clara Lam</div>
<div class="f-ml"></div>
<div class="f-ml"><strong>To</strong> F. Rupley</div>
</div>
<div class="f-meta-bottom" style="margin-top:4px;">
<div class="f-ring">
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" fill="none" stroke="#E4E2D7" stroke-width="2"/><circle cx="10" cy="10" r="7" fill="none" stroke="#A6DAD8" stroke-width="2" stroke-dasharray="33 44" stroke-linecap="round"/></svg>
<div class="f-ring-label" style="color:#5bbab7">75%</div>
</div>
<div class="f-contribs"><div class="f-cav" style="background:#dc2626">AK</div></div>
</div>
</div>
</div>
</div>
</div>
<span class="caption">Fig 2 — /documents · 375px mobile · search "brief" · filter closed</span>
</div>
<div class="anno-box" style="align-self:start">
<h4>Mobile row — CSS-only approach</h4>
<p>No JS needed. The <code>&lt;a&gt;</code> link is always <code>block</code>. On <code>sm+</code> the inner element switches to <code>flex items-stretch</code>, showing the right metadata column (<code>hidden sm:flex</code>) and hiding the mobile compact grid (<code>sm:hidden</code>).</p>
<p>This means the DOM contains both layouts simultaneously — the metadata grid inside the left column (mobile only) and the right metadata panel (sm+ only). Both share the same data, just rendered differently.</p>
<p>Minimum touch target: the entire row is the <code>&lt;a&gt;</code>, guaranteed ≥44px on mobile given title + snippet + tags + metadata grid.</p>
</div>
</div>
</div>
<hr/>
<!-- ══════════════════════════════════════
SECTION 3 — YEAR GROUP CARD
══════════════════════════════════════ -->
<div class="sec">
<div class="sec-label">Section 3</div>
<div class="sec-title">Year group card</div>
<div class="sec-sub">One card per year group. Rows inside use divide-y — no gaps between rows.</div>
<div class="two-col">
<div class="anno-box">
<h4>Card container</h4>
<p>Matches current DocumentList outer container exactly: <code>border border-line bg-surface shadow-sm</code>. No border-radius (keeps it flush). Margin between consecutive year cards: <code>mb-4</code>.</p>
<p>Rendered only when sort = DATE. For other sort modes (SENDER, RECEIVER, TITLE) the year header is replaced by the relevant group label using the same card pattern.</p>
</div>
<div class="anno-box">
<h4>Year header row</h4>
<p>First child of each card. Background <code>bg-sand</code>, text <code>text-xs font-bold uppercase tracking-widest text-ink-3</code>. Height <code>py-1.5 px-5</code>. Bottom border <code>border-b border-line</code>.</p>
<p>Not a standalone divider — it is part of the card so the top border of the card frames the year label on three sides.</p>
</div>
</div>
</div>
<hr/>
<!-- ══════════════════════════════════════
SECTION 4 — DOCUMENT ROW
══════════════════════════════════════ -->
<div class="sec">
<div class="sec-label">Section 4</div>
<div class="sec-title">Document row — two-column split</div>
<div class="two-col">
<div class="anno-box">
<h4>Left column — content</h4>
<p>Flex-1, min-width 0. Padding <code>p-4 pr-5</code>. Right border <code>border-r border-line-2</code>.</p>
<p><strong>Title</strong><code>font-serif text-base font-bold text-ink</code> with search highlight underlines. <code>mb-1.5</code>.</p>
<p><strong>Snippet</strong><code>font-serif text-sm italic text-ink-2 line-clamp-2 mb-2</code> with highlight underlines. Only rendered when a match snippet is present.</p>
<p><strong>Tags</strong> — existing tag pill pattern <code>bg-muted text-ink text-[10px] font-bold uppercase tracking-widest rounded px-2 py-0.5</code>. Gap <code>gap-1.5 flex-wrap</code>.</p>
</div>
<div class="anno-box">
<h4>Right column — metadata panel</h4>
<p>Fixed width <code>w-60</code> (240px). Padding <code>p-3.5</code>. Flex column, <code>justify-between</code>.</p>
<p><strong>Meta lines</strong> (top group) — <code>font-sans text-[11px] text-ink-2 mb-1</code>. Label: <code>font-bold uppercase tracking-wide text-[10px] text-ink-3 mr-1.5</code>. Lines: Date · From · To · Archive (Box · Folder). Archive only rendered when <code>archiveBox</code> is set.</p>
<p><strong>Bottom row</strong> — flexbox, space-between. Left: progress ring. Right: ContributorStack.</p>
</div>
</div>
<div class="callout orange">
<div><strong class="o">Accessibility:</strong> The ring conveys progress by both percentage text and arc fill — not colour alone. Contributors show initials as text inside the avatar. Both pass the redundant-cue requirement from the Leonie Voss persona. Minimum touch target for the row link: the full row is the <code>&lt;a&gt;</code> element, always ≥44px tall given the content. Row hover: <code>hover:bg-muted/50 transition-colors duration-200</code>.</div>
</div>
</div>
<hr/>
<!-- ══════════════════════════════════════
SECTION 5 — PROGRESS RING
══════════════════════════════════════ -->
<div class="sec">
<div class="sec-label">Section 5</div>
<div class="sec-title">Progress ring</div>
<div class="two-col">
<div class="anno-box">
<h4>Anatomy</h4>
<p>SVG donut ring, 36×36px. Track circle: <code>stroke="#E4E2D7"</code> (<code>stroke-brand-sand</code>) width 3px. Fill arc: <code>stroke="#A6DAD8"</code> (<code>stroke-accent</code>) width 3px, <code>stroke-linecap="round"</code>. Rotated 90° so arc starts at 12 o'clock.</p>
<p>Centre label: percentage text <code>font-sans text-[8px] font-bold</code>. Colour: mint (<code>text-accent-dark</code>) when &gt;0%, gray-400 when 0%.</p>
<p>Circumference of r=13: <code>×13 ≈ 81.7px</code>. Stroke-dasharray: <code>{pct * 81.7} 81.7</code>.</p>
</div>
<div class="anno-box">
<h4>Data source — new API field</h4>
<p>New field <code>completionPercentage: number</code> (0100, integer) on the document search result DTO. Computed server-side:</p>
<p><code>round((reviewedBlocks / max(totalBlocks, 1)) * 100)</code></p>
<p>If a document has no annotation blocks yet (no transcription started), returns 0. Backend change: new subquery in the document search repository to COUNT annotation blocks (all vs. reviewed) per document, joined into the search projection.</p>
</div>
</div>
</div>
<hr/>
<!-- ══════════════════════════════════════
SECTION 6 — CONTRIBUTOR AVATARS
══════════════════════════════════════ -->
<div class="sec">
<div class="sec-label">Section 6</div>
<div class="sec-title">Contributor avatar stack</div>
<div class="two-col">
<div class="anno-box">
<h4>Anatomy</h4>
<p>Reuse existing <code>ContributorStack.svelte</code> component (added in commit 031f6ea). Avatars 22×22px, <code>-ml-1.5</code> overlap, white 2px border.</p>
<p>Show max 3 avatars. If more: <code>+N</code> text element in gray-400. When no contributors: render <code>text-[9px] text-ink-3 uppercase tracking-wide</code> label "No contributors".</p>
</div>
<div class="anno-box">
<h4>Data source — new API field</h4>
<p>New field <code>contributors: ActivityActorDTO[]</code> on the document search result DTO. <code>ActivityActorDTO</code> already exists (used in dashboard queue items): <code>{ initials: string, color: string, name?: string }</code>.</p>
<p>Backend: join from document → annotation_blocks → created_by → users. Distinct by user. Order by most-recent contribution. Limit 4. New query in document search repository.</p>
</div>
</div>
</div>
<hr/>
<!-- ══════════════════════════════════════
SECTION 7 — BACKEND CHANGES
══════════════════════════════════════ -->
<div class="sec">
<div class="sec-label">Section 7</div>
<div class="sec-title">Backend changes required</div>
<div class="callout green">
<div><strong class="g">New fields on document search DTO</strong> — Two new fields must be added to the object returned by <code>GET /api/documents/search</code>. These require a new projection or join in the repository layer. No schema migration needed — purely computed from existing annotation_block data.</div>
</div>
<div class="impl-ref">
<table>
<thead><tr><th>Field</th><th>Type</th><th>Source</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>completionPercentage</code></td><td><code>int</code> (0100)</td><td>COUNT(reviewed annotation blocks) / COUNT(all blocks)</td><td>0 when no blocks exist</td></tr>
<tr><td><code>contributors</code></td><td><code>ActivityActorDTO[]</code></td><td>Distinct users with annotation_block contributions, ordered by recency</td><td>Max 4; reuse existing DTO</td></tr>
<tr><td><code>archiveBox</code></td><td><code>String?</code></td><td>Already on Document entity — just not in search response</td><td><span class="changed">Expose existing field</span></td></tr>
<tr><td><code>archiveFolder</code></td><td><code>String?</code></td><td>Already on Document entity — just not in search response</td><td><span class="changed">Expose existing field</span></td></tr>
</tbody>
</table>
</div>
</div>
<hr/>
<!-- ══════════════════════════════════════
SECTION 8 — IMPL-REF TABLE
══════════════════════════════════════ -->
<div class="sec">
<div class="sec-label">Section 8 — Implementation Reference</div>
<div class="sec-title">Exact Tailwind classes & pixel values</div>
<div class="impl-ref">
<table>
<thead><tr><th>Element</th><th>Tailwind classes</th><th>Pixels / value</th><th>Notes</th></tr></thead>
<tbody>
<tr><td colspan="4" style="background:rgba(0,40,80,.05);font-size:10px;font-weight:700;color:var(--navy);text-transform:uppercase;letter-spacing:.08em;">Page chrome</td></tr>
<tr><td>Search bar wrapper</td><td><code>bg-white border-b border-line -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 py-3.5 flex items-center gap-3 sticky top-[65px] z-20</code></td><td>padding 14px responsive</td><td>Topbar = 1px accent + 64px nav = 65px. Negative margins break out of container padding so bar spans full container width.</td></tr>
<tr><td>Search input</td><td><code>flex-1 h-9 border border-ink rounded-sm px-3 font-sans text-sm text-ink bg-white</code></td><td>height 36px</td><td>Active: navy border</td></tr>
<tr><td>Sort bar wrapper</td><td><code>bg-white border-b border-line -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 py-2.5 flex items-center gap-3 sticky top-[113px] z-20</code></td><td>padding 10px responsive</td><td>Stacks below search bar (65 + 48 = 113px)</td></tr>
<tr><td>Filters toggle (closed)</td><td><code>h-7 px-3 border border-line rounded-sm font-sans text-[10px] font-bold uppercase tracking-wide text-ink flex items-center gap-1.5</code></td><td>height 28px</td><td></td></tr>
<tr><td>Filters toggle (open)</td><td><code>h-7 px-3 bg-ink text-white rounded-sm font-sans text-[10px] font-bold uppercase tracking-wide flex items-center gap-1.5</code></td><td>height 28px</td><td>Navy fill when active</td></tr>
<tr><td>Filter panel wrapper</td><td><code>bg-white border-b border-line -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 py-4 flex flex-col sm:flex-row sm:flex-wrap gap-4</code></td><td>padding 16px responsive</td><td>Use Svelte <code>slide</code> transition; stacks vertically on mobile</td></tr>
<tr><td>List body</td><td><code>py-5</code></td><td>vertical padding only</td><td>No extra horizontal padding — app container handles it</td></tr>
<tr><td colspan="4" style="background:rgba(0,40,80,.05);font-size:10px;font-weight:700;color:var(--navy);text-transform:uppercase;letter-spacing:.08em;">Year group card</td></tr>
<tr><td>Card container</td><td><code>border border-line bg-surface shadow-sm mb-4 overflow-hidden</code></td><td></td><td>Matches current DocumentList outer div exactly</td></tr>
<tr><td>Year header</td><td><code>bg-sand border-b border-line px-5 py-1.5 font-sans text-[10px] font-bold uppercase tracking-widest text-ink-3</code></td><td>padding 6px 20px</td><td></td></tr>
<tr><td>Row list</td><td><code>divide-y divide-line-2</code></td><td></td><td>Matches current <code>&lt;ul&gt;</code> pattern</td></tr>
<tr><td colspan="4" style="background:rgba(0,40,80,.05);font-size:10px;font-weight:700;color:var(--navy);text-transform:uppercase;letter-spacing:.08em;">Document row</td></tr>
<tr><td>Row wrapper <code>&lt;li&gt;</code></td><td><code>group transition-colors duration-200 hover:bg-muted/50</code></td><td></td><td>Same hover pattern as current</td></tr>
<tr><td>Row inner (link)</td><td><code>block sm:flex sm:items-stretch</code></td><td></td><td>Full-row <code>&lt;a href="/documents/{id}"&gt;</code>; flex only on sm+</td></tr>
<tr><td>Left column</td><td><code>p-4 sm:flex-1 sm:min-w-0 sm:pr-5 sm:border-r sm:border-line-2</code></td><td>padding 16px</td><td>Right border only on sm+</td></tr>
<tr><td>Right column (sm+)</td><td><code>hidden sm:flex sm:w-48 lg:w-60 flex-shrink-0 p-3.5 flex-col justify-between gap-2</code></td><td>sm: 192px · lg: 240px</td><td>Hidden on mobile; narrower on tablet</td></tr>
<tr><td>Mobile metadata grid</td><td><code>sm:hidden border-t border-line-2 mt-3 pt-3 grid grid-cols-2 gap-x-4 gap-y-0.5</code></td><td></td><td>2×2 compact grid shown only on mobile, inside left col</td></tr>
<tr><td>Mobile meta bottom row</td><td><code>sm:hidden flex items-center justify-between mt-3</code></td><td></td><td>Ring + contributors on mobile, shown only &lt;sm</td></tr>
<tr><td>Document title</td><td><code>font-serif text-base font-bold text-ink mb-1.5 leading-snug group-hover:underline</code></td><td>16px / 700</td><td></td></tr>
<tr><td>Snippet text</td><td><code>font-serif text-sm italic text-ink-2 line-clamp-2 mb-2</code></td><td>14px</td><td>Only when snippet present</td></tr>
<tr><td>Meta label</td><td><code>font-sans text-[10px] font-bold uppercase tracking-wide text-ink-3 mr-1.5</code></td><td>10px / 700</td><td>DATE · FROM · TO · ARCHIVE</td></tr>
<tr><td>Meta value</td><td><code>font-sans text-[11px] text-ink-2</code></td><td>11px</td><td></td></tr>
<tr><td colspan="4" style="background:rgba(0,40,80,.05);font-size:10px;font-weight:700;color:var(--navy);text-transform:uppercase;letter-spacing:.08em;">Progress ring</td></tr>
<tr><td>SVG container</td><td><code>relative w-9 h-9 flex-shrink-0</code></td><td>36×36px</td><td></td></tr>
<tr><td>Track circle</td><td><code>stroke="var(--c-sand)"</code> stroke-width="3"</td><td>r=13, circumference 81.7px</td><td></td></tr>
<tr><td>Fill arc</td><td><code>stroke="var(--c-accent)"</code> stroke-width="3" stroke-linecap="round"</td><td>dasharray = pct/100 × 81.7</td><td>rotate(90deg)</td></tr>
<tr><td>Percentage label</td><td><code>absolute inset-0 flex items-center justify-center font-sans text-[8px] font-bold</code></td><td>8px / 800</td><td>Mint when &gt;0, gray-400 when 0</td></tr>
<tr><td colspan="4" style="background:rgba(0,40,80,.05);font-size:10px;font-weight:700;color:var(--navy);text-transform:uppercase;letter-spacing:.08em;">New files</td></tr>
<tr><td><span class="new">NEW</span> <code>frontend/src/routes/documents/+page.svelte</code></td><td></td><td></td><td>Document list page (extract from homepage)</td></tr>
<tr><td><span class="new">NEW</span> <code>frontend/src/routes/documents/+page.server.ts</code></td><td></td><td></td><td>Loads search results, same API call as current homepage</td></tr>
<tr><td><span class="changed">CHANGED</span> <code>frontend/src/routes/AppNav.svelte</code></td><td></td><td></td><td>Documents tab href: <code>/</code><code>/documents</code></td></tr>
<tr><td><span class="changed">CHANGED</span> <code>frontend/src/routes/+page.svelte</code></td><td></td><td></td><td>Remove dual-mode logic; always render dashboard</td></tr>
<tr><td><span class="changed">CHANGED</span> <code>frontend/src/routes/+page.server.ts</code></td><td></td><td></td><td>Remove search branch; always fetch dashboard data</td></tr>
<tr><td><span class="changed">CHANGED</span> <code>frontend/src/routes/DocumentList.svelte</code></td><td></td><td></td><td>Refactor to new two-column layout + year cards</td></tr>
<tr><td><span class="new">NEW query</span> <code>backend/.../DocumentSearchRepository</code></td><td></td><td></td><td>Add completionPercentage + contributors to search projection</td></tr>
</tbody>
</table>
</div>
</div>
</div><!-- /doc -->
</body>
</html>