chore: add Claude personas, skills, memory, and project docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-14 20:22:39 +02:00
parent e4719b9487
commit 3d3d4b8616
26 changed files with 12123 additions and 0 deletions

View File

@@ -0,0 +1,887 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard — Classic Split · Final Design Spec · Familienarchiv</title>
<style>
*,*::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}
.doc{max-width:1440px;margin:0 auto;padding:48px 32px}
/* ── Masthead ─── */
.mast{background:#0D2240;border-radius:10px;padding:32px 40px;margin-bottom:48px}
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:16px}
.mast h1{font-size:22px;font-weight:900;color:#fff;letter-spacing:-.4px;margin-bottom:6px}
.mast p{font-size:12px;color:rgba(255,255,255,.5);max-width:660px;line-height:1.7}
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;flex-shrink:0;margin-top:4px;background:#A6DAD8;color:#002850}
.decisions{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:20px;border-top:1px solid rgba(255,255,255,.1);padding-top:16px}
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
.dec-label{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.35);margin-bottom:5px}
.dec-value{font-size:9.5px;font-weight:700;color:#fff;line-height:1.5}
.dec-value s{color:rgba(255,255,255,.3);font-weight:400}
/* ── Section headings ─── */
.sec{margin-bottom:64px}
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:56px}
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:20px;display:flex;align-items:center;gap:10px}
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
.sec-num{background:#0D2240;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
/* ── Screen grid ─── */
.sg{display:grid;gap:20px;align-items:start}
.sg-2{grid-template-columns:1fr 1fr}
.sg-mob{grid-template-columns:240px 1fr}
.sb{display:flex;flex-direction:column;gap:10px}
.sl{font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px}
.sz{background:#E8E4DF;color:#666;padding:1px 5px;border-radius:3px;font-size:8px}
.sc{font-size:8.5px;color:#888;margin-top:6px;font-style:italic;line-height:1.5}
/* ── Annotation callouts ─── */
.ann{display:inline-block;font-size:7.5px;font-weight:700;color:#C2410C;background:#FFF7ED;border:1px solid #FDBA74;border-radius:3px;padding:1px 5px;white-space:nowrap}
.ann-block{background:#FFF7ED;border:1px solid #FDBA74;border-radius:5px;padding:8px 10px;font-size:10px;color:#7C2D12;line-height:1.5;margin-top:10px}
.ann-block strong{font-weight:800}
.ann-block ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
/* ── Mock browser 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:24px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 9px;gap:4px}
.dot{width:7px;height:7px;border-radius:50%}
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
.urlbar{flex:1;height:11px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
.urlbar span{font-size:7.5px;color:#888;font-family:monospace}
/* ── Nav bar ─── */
.N{height:34px;background:#002850;display:flex;align-items:center;padding:0 14px;gap:10px;flex-shrink:0}
.N-accent{height:2px;background:#A6DAD8}
.logo{font-size:7.5px;font-weight:900;color:#fff;letter-spacing:1px;text-transform:uppercase}
.nl{font-size:6.5px;color:rgba(255,255,255,.45);font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 6px}
.nl.on{color:#fff}
.nr{margin-left:auto;display:flex;gap:5px;align-items:center}
.nico{width:18px;height:18px;background:rgba(255,255,255,.1);border-radius:3px;display:flex;align-items:center;justify-content:center}
.av{width:18px;height:18px;background:#A6DAD8;border-radius:50%;font-size:5.5px;font-weight:900;color:#002850;display:flex;align-items:center;justify-content:center}
.bell-dot{width:4px;height:4px;background:#A6DAD8;border-radius:50%;position:absolute;top:2px;right:2px}
/* ── Page body ─── */
.MAIN{padding:12px 16px;background:#F5F4EF;display:flex;flex-direction:column;gap:8px}
/* ── Search bar ─── */
.SEARCH{display:flex;gap:6px;align-items:center}
.SEARCH-BOX{flex:1;height:28px;background:#fff;border:1.5px solid #E0DDD5;border-radius:3px;display:flex;align-items:center;gap:6px;padding:0 9px}
.SEARCH-BOX input{border:none;outline:none;font-size:8px;color:#1a1a1a;flex:1;background:transparent}
.SEARCH-BOX input::placeholder{color:#C8C4BE}
.FILTER-BTN{height:28px;background:#fff;border:1.5px solid #E0DDD5;border-radius:3px;padding:0 9px;font-size:7px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#555;display:flex;align-items:center;gap:4px}
/* ── Resume strip ─── */
.RESUME{background:#fff;border:1px solid #E0DDD5;border-radius:3px;padding:5px 10px;font-size:7.5px;color:#555;display:flex;align-items:center;gap:5px}
.RESUME strong{color:#002850;font-weight:600}
/* ── Dashboard grid ─── */
/* No align-items → default is stretch → equal column heights */
.DASH-GRID{display:grid;grid-template-columns:1fr 200px;gap:8px}
/* ── Recent docs card ─── */
.CARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;display:flex;flex-direction:column}
.CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:8px 10px 7px;border-bottom:1px solid #E0DDD5}
.CARD-HEAD h3{font-size:7px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
.CARD-HEAD a{font-size:7px;font-weight:600;color:#002850;opacity:.45;text-decoration:none}
.DOC-ROW{display:flex;align-items:baseline;justify-content:space-between;padding:5px 10px;border-bottom:1px solid #F0EDE6}
.DOC-ROW:last-of-type{border-bottom:none}
.DOC-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850}
.DOC-DATE{font-size:6.5px;color:#C8C4BE;white-space:nowrap;margin-left:6px;flex-shrink:0}
.CARD-FOOT{padding:6px 10px;border-top:1px solid #F0EDE6}
.CARD-FOOT-TEXT{font-size:6.5px;color:#C8C4BE}
/* ── Sidebar ─── */
/* height:100% fills the grid cell so right column matches left column height */
.SIDEBAR{display:flex;flex-direction:column;gap:8px;height:100%}
/* ── Upload zone ─── */
.UPLOAD{border:1.5px dashed rgba(166,218,216,.7);border-radius:3px;background:rgba(166,218,216,.06);padding:14px 10px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;text-align:center;cursor:pointer}
.UPLOAD:hover{background:rgba(166,218,216,.12)}
.UPLOAD-LABEL{font-size:7px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#002850;opacity:.65}
.UPLOAD-HINT{font-size:6.5px;color:#C8C4BE;line-height:1.5}
/* ── Needs metadata card ─── */
/* flex:1 fills remaining sidebar height after the upload zone */
.META-CARD{background:#fff;border:1px solid #E0DDD5;border-top:2px solid #F39C12;border-radius:3px;overflow:hidden;flex:1;display:flex;flex-direction:column}
.META-CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:7px 10px 6px;border-bottom:1px solid #F0EDE6}
.META-CARD-HEAD h3{font-size:7px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
.META-PILL{background:#FFF3CD;color:#856404;font-size:7px;font-weight:800;padding:1px 6px;border-radius:10px}
.META-ROW{display:flex;align-items:center;padding:5px 10px;border-bottom:1px solid #F0EDE6}
.META-ROW:last-of-type{border-bottom:none}
.META-TITLE{font-family:Georgia,serif;font-size:7.5px;color:#002850;flex:1}
.META-CARD-FOOT{padding:5px 10px;border-top:1px solid #F0EDE6}
/* ── Mobile chrome ─── */
.WF-M{background:#fff;border:2px solid #B8B4AE;border-radius:14px;overflow:hidden;width:220px;box-shadow:0 4px 18px rgba(0,0,0,.08)}
.WF-M-STATUS{height:16px;background:#002850;display:flex;align-items:center;justify-content:space-between;padding:0 8px}
.WF-M-TIME{font-size:6.5px;color:#fff;font-weight:700}
.N-M{height:28px;background:#002850;display:flex;align-items:center;padding:0 10px;gap:8px}
.MAIN-M{padding:8px 10px;display:flex;flex-direction:column;gap:6px;background:#F5F4EF}
.PH{height:6px;background:#E8E4DF;border-radius:2px}
.w80{width:80%}.w70{width:70%}.w60{width:60%}.w50{width:50%}.w40{width:40%}
/* ── Changes panel ─── */
.CHANGES{background:#fff;border:1.5px solid #E0DDD6;border-radius:8px;padding:20px 24px;margin-bottom:0}
.CHANGES h2{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #E8E4DF}
.CHANGES-GRID{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.C-COL h3{font-size:10px;font-weight:800;color:#444;margin-bottom:8px}
.C-COL ul{list-style:none;display:flex;flex-direction:column;gap:5px}
.C-COL ul li{font-size:11px;color:#555;padding-left:18px;position:relative;line-height:1.5}
.C-COL.add li::before{content:'✦';position:absolute;left:0;color:#002850;font-size:8px;top:2px}
.C-COL.remove li::before{content:'✗';position:absolute;left:0;color:#DC2626;top:1px}
.C-COL.keep li::before{content:'→';position:absolute;left:0;color:#888}
.C-COL li code{font-family:monospace;font-size:10px;background:#F5F5F5;padding:0 4px;border-radius:2px}
/* ── Edge cases ─── */
.EDGE{background:#FFFBF0;border:1px solid #F0D090;border-radius:6px;padding:11px 15px;margin-bottom:8px}
.EDGE-LABEL{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#92400E;margin-bottom:4px}
.EDGE-BODY{font-size:11px;color:#555;line-height:1.6}
.EDGE-BODY code{font-family:monospace;font-size:10px;background:rgba(0,0,0,.06);padding:0 4px;border-radius:2px}
/* ── AC list ─── */
.AC{counter-reset:ac;display:flex;flex-direction:column;gap:7px}
.AC-ITEM{display:flex;align-items:flex-start;gap:10px;background:#fff;border:1px solid #E0DDD5;border-radius:5px;padding:10px 14px;font-size:11px;color:#333;line-height:1.6}
.AC-ITEM::before{counter-increment:ac;content:counter(ac);display:flex;align-items:center;justify-content:center;width:20px;height:20px;min-width:20px;border-radius:50%;background:#002850;color:#fff;font-size:9px;font-weight:900;margin-top:1px}
.AC-ITEM code{font-family:monospace;font-size:10px;background:#F5F5F5;padding:0 4px;border-radius:2px}
.AC-ITEM .tag{font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;padding:1px 5px;border-radius:3px;margin-left:6px}
.AC-ITEM .tag-a11y{background:#EDE9FE;color:#5B21B6}
.AC-ITEM .tag-mobile{background:#DCFCE7;color:#166534}
.AC-ITEM .tag-data{background:#DBEAFE;color:#1E40AF}
/* ── Spec disclaimer ─── */
.spec-disclaimer{background:#FFF8E1;border:1.5px solid #FFC107;border-radius:6px;padding:11px 16px;font-size:11px;color:#6D4C00;margin-bottom:32px;line-height:1.6}
.spec-disclaimer strong{font-weight:800}
/* ── Agent Implementation Reference ─── */
.impl-ref{background:#0d1117;border-radius:8px;margin-top:20px;overflow:hidden;border:1px solid #30363d}
.impl-ref-hdr{background:#161b22;padding:9px 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:8px 14px;border-bottom:1px solid #21262d}
.impl-ref td{padding:6px 14px;border-bottom:1px solid #161b22;vertical-align:top;line-height:1.6;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:200px}
.impl-ref td code{font-family:'SFMono-Regular',Consolas,monospace;font-size:9.5px;background:#161b22;color:#a5d6ff;padding:1px 5px;border-radius:3px;white-space:nowrap}
.impl-ref .ir-px{color:#7ee787;font-family:monospace;font-size:9.5px}
</style>
</head>
<body>
<div class="doc">
<!-- ══════════════════════════════════
MASTHEAD
══════════════════════════════════ -->
<div class="mast">
<div class="mast-top">
<div>
<h1>Dashboard — Classic Split · Final Design Spec</h1>
<p>Refocus the homepage on documents. The notification widget is removed from the dashboard — it already lives in the bell dropdown. The page is restructured into a two-column "Command Center": recent activity on the left, upload zone and missing-metadata queue on the right. Stats are demoted to a quiet footnote.</p>
</div>
<span class="mast-badge">Final · Ready for implementation</span>
</div>
<div class="decisions">
<div class="dec">
<div class="dec-label">Notification widget</div>
<div class="dec-value"><s>On dashboard</s><br>→ Bell dropdown only</div>
</div>
<div class="dec">
<div class="dec-label">Layout</div>
<div class="dec-value"><s>Single column stacked</s><br>→ 2-col split (desktop)</div>
</div>
<div class="dec">
<div class="dec-label">Upload button in action bar</div>
<div class="dec-value"><s>Redundant button</s><br>→ Upload zone only</div>
</div>
<div class="dec">
<div class="dec-label">Stats</div>
<div class="dec-value"><s>Prominent stat chips</s><br>→ Quiet footnote text</div>
</div>
<div class="dec">
<div class="dec-label">Backend changes</div>
<div class="dec-value">None — <code style="color:#A6DAD8;font-size:9px">/api/stats</code> already exists</div>
</div>
</div>
</div>
<!-- spec disclaimer -->
<div class="spec-disclaimer">
<strong>📐 Mockup scale notice —</strong> all font-size, height, and padding values in the mockup CSS below are scaled to ~55% of actual implementation values.
<strong>Do not copy sizes from mockup CSS.</strong> Use the ⚙ Implementation Reference tables after each section.
</div>
<!-- ══════════════════════════════════
SECTION 1 — DESKTOP LAYOUT
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">1</span> Desktop Layout — ≥ 1024 px</div>
<div class="sg sg-2" style="align-items:start">
<div class="sb">
<div class="sl">Full page <span class="sz">1440px</span></div>
<!-- Mock browser -->
<div class="wf">
<div class="wf-bar">
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
<div class="urlbar"><span>familienarchiv.local /</span></div>
</div>
<!-- Nav -->
<div class="N">
<span class="logo">Familienarchiv</span>
<span class="nl on">Documents</span>
<span class="nl">Persons</span>
<span class="nl">Correspondence</span>
<div class="nr">
<!-- settings icon -->
<div class="nico"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09"/></svg></div>
<!-- bell with dot -->
<div class="nico" style="position:relative">
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
<div style="position:absolute;top:2px;right:2px;width:4px;height:4px;background:#A6DAD8;border-radius:50%;"></div>
</div>
<div class="av">BC</div>
</div>
</div>
<div class="N-accent"></div>
<!-- Page body -->
<div class="MAIN">
<!-- Search -->
<div class="SEARCH">
<div class="SEARCH-BOX">
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input type="text" placeholder="Search in title, content, location…" disabled>
</div>
<button class="FILTER-BTN">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="4" y1="6" x2="20" y2="6"/><line x1="8" y1="12" x2="16" y2="12"/><line x1="11" y1="18" x2="13" y2="18"/></svg>
Filter
</button>
</div>
<!-- Resume strip -->
<div class="RESUME">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4z"/></svg>
Continue where you left off: <strong>E28 History Tee Dokument (bearbeitet)</strong>
</div>
<!-- 2-col dashboard grid -->
<div class="DASH-GRID">
<!-- LEFT: Recent docs -->
<div class="CARD">
<div class="CARD-HEAD">
<h3>Recent Activity</h3>
<a href="#">All documents →</a>
</div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 Hash Tee — review</span><span class="DOC-DATE">30. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 Hash Tee — version</span><span class="DOC-DATE">30. März 2026</span></div>
<div class="CARD-FOOT">
<span class="CARD-FOOT-TEXT">248 Documents &nbsp;·&nbsp; 34 Persons</span>
</div>
</div>
<!-- RIGHT: Sidebar -->
<div class="SIDEBAR">
<!-- Upload zone -->
<div class="UPLOAD">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
<div class="UPLOAD-LABEL">Drop files here</div>
<div class="UPLOAD-HINT">PDF, PNG, JPG, ODS, XLS<br>or click to browse</div>
</div>
<!-- Needs metadata -->
<div class="META-CARD">
<div class="META-CARD-HEAD">
<h3>Needs Metadata</h3>
<span class="META-PILL">5</span>
</div>
<div class="META-ROW"><span class="META-TITLE">E28 History Tee Dokument</span></div>
<div class="META-ROW"><span class="META-TITLE">E28 History Tee Dokument</span></div>
<div class="META-ROW"><span class="META-TITLE">E28 History Tee Dokument</span></div>
<div class="META-CARD-FOOT"><a href="#" style="font-size:6.5px;font-weight:600;color:#002850;opacity:.45;text-decoration:none">Show all →</a></div>
</div>
</div>
</div><!-- /DASH-GRID -->
</div><!-- /MAIN -->
</div><!-- /wf -->
<div class="ann-block">
<strong>Key decisions visible here</strong>
<ul>
<li>Notification widget removed entirely — bell badge in header is sufficient</li>
<li>Upload zone replaces the action-bar button — no redundancy</li>
<li>Stats footnote: <em>quiet</em>, not a chip — does not compete for attention</li>
<li>Right column is 300 px fixed — enough for upload + short metadata list</li>
</ul>
</div>
</div>
<!-- Annotated callouts panel -->
<div class="sb">
<div class="sl">Annotations</div>
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;color:#333;line-height:1.7;display:flex;flex-direction:column;gap:14px;">
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">① Search bar</div>
Unchanged — existing <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">SearchFilterBar</code> component. Full width, no upload button appended.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">② Resume strip</div>
Unchanged — <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">DashboardResumeStrip</code>. Only renders when localStorage has a last-visited document. Hidden otherwise — no empty gap.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">③ Dashboard grid</div>
<code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-4</code><br>
No <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">items-start</code> — the CSS Grid default <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">align-items: stretch</code> makes both columns the same height for free. The right column wrapper needs <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">h-full</code> so the flex container fills that height, and the metadata card gets <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">flex-1</code> to consume the space left after the upload zone. Both columns are always flush at the bottom.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">④ Recent Activity card</div>
<code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">DashboardRecentDocuments</code> receives a new optional <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">stats</code> prop. The footnote only renders when <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">stats?.totalDocuments != null</code>.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#F39C12;margin-bottom:4px;">⑤ Upload zone</div>
Existing <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">DropZone</code> component, no internal changes. Wrapped in <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">{#if data.canWrite}</code> — hidden for read-only users.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#F39C12;margin-bottom:4px;">⑥ Needs Metadata card</div>
Unchanged <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">DashboardNeedsMetadata</code>. Already renders nothing when <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">incompleteDocs.length === 0</code>. Amber top border signals "action required" without relying on color alone — the heading "Needs Metadata" and count pill are redundant cues.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:4px;">⑦ Right column empty state</div>
If both upload zone (no canWrite) and needs-metadata (no incomplete docs) are absent, the right grid cell is an empty <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">&lt;div&gt;</code>. The equal-height stretch still applies but an invisible column causes no visual artefact. When this case is detectable server-side, consider conditionally omitting the grid class so the left column runs full width — but this is a polish improvement, not a blocker.
</div>
</div>
</div>
</div>
<!-- impl-ref: Desktop layout -->
<div class="impl-ref">
<div class="impl-ref-hdr">Implementation Reference — Desktop Layout
<span>Real values · mockup above is ~55% scale · do not copy mockup CSS</span>
</div>
<table>
<thead><tr><th>Element</th><th>Tailwind classes</th><th>Real size</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td>Page wrapper</td>
<td><code>mx-auto max-w-7xl px-4 py-8 font-sans sm:px-6 lg:px-8</code></td>
<td><span class="ir-px">py 32px, max-w 1280px</span></td>
<td>Unchanged from current <code>+page.svelte</code></td>
</tr>
<tr>
<td>Resume strip</td>
<td><code>mb-4</code> (existing component, no change)</td>
<td><span class="ir-px">mb 16px</span></td>
<td>Unchanged</td>
</tr>
<tr>
<td>Dashboard grid wrapper</td>
<td><code>mt-4 grid grid-cols-1 gap-4 lg:grid-cols-[1fr_300px]</code></td>
<td><span class="ir-px">gap 16px, right col 300px fixed</span></td>
<td><strong>No <code>items-start</code>.</strong> CSS Grid default is <code>align-items: stretch</code> — both columns are automatically the same height. Replaces the current conditional mentions+metadata grid.</td>
</tr>
<tr>
<td>Right column inner wrapper</td>
<td><code>flex flex-col gap-4 h-full</code></td>
<td><span class="ir-px">gap 16px, full grid cell height</span></td>
<td><code>h-full</code> is required so the flex container fills the stretched grid cell. Without it the column stops at content height and <code>flex-1</code> on the child has nothing to fill.</td>
</tr>
<tr>
<td>DashboardNeedsMetadata wrapper</td>
<td><code>flex-1 flex flex-col min-h-0</code> (wraps the component)</td>
<td><span class="ir-px">grows to fill remaining height</span></td>
<td><code>flex-1</code> consumes the space left after the DropZone. <code>min-h-0</code> prevents flex overflow. The component's inner card should be <code>h-full</code> so the card border fills the space — content sits at the top, not stretched.</td>
</tr>
<tr>
<td>DropZone guard</td>
<td><code>{#if data.canWrite}</code></td>
<td></td>
<td>No changes to the DropZone component itself — wrapper condition only. When absent, the metadata card's <code>flex-1</code> still fills the full right column height.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 2 — MOBILE LAYOUT
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">2</span> Mobile Layout — &lt; 1024 px · stacking order</div>
<div class="sg sg-mob" style="align-items:start;gap:32px">
<!-- Mobile mockup -->
<div class="sb">
<div class="sl">375 px <span class="sz">iPhone</span></div>
<div class="WF-M">
<div class="WF-M-STATUS">
<span class="WF-M-TIME">09:41</span>
<div style="display:flex;gap:3px"><div style="width:5px;height:5px;background:rgba(255,255,255,.4);border-radius:1px"></div><div style="width:5px;height:5px;background:rgba(255,255,255,.4);border-radius:1px"></div><div style="width:5px;height:5px;background:rgba(255,255,255,.4);border-radius:1px"></div></div>
</div>
<div class="N-M">
<span class="logo" style="font-size:7px">Familienarchiv</span>
<div class="nr">
<div class="nico" style="position:relative">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
<div style="position:absolute;top:2px;right:2px;width:3px;height:3px;background:#A6DAD8;border-radius:50%;"></div>
</div>
<div class="av" style="width:16px;height:16px;font-size:5px">BC</div>
</div>
</div>
<div class="N-accent"></div>
<div class="MAIN-M">
<!-- search -->
<div style="height:22px;background:#fff;border:1.5px solid #E0DDD5;border-radius:3px;display:flex;align-items:center;padding:0 8px;gap:5px">
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<div class="PH w70" style="height:5px"></div>
</div>
<!-- resume -->
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:3px;padding:4px 8px;display:flex;align-items:center;gap:4px">
<div class="PH w60" style="height:5px;background:#E8E4DF"></div>
</div>
<!-- ① recent docs -->
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden">
<div style="padding:5px 8px;border-bottom:1px solid #F0EDE6;display:flex;justify-content:space-between">
<div class="PH w40" style="height:5px"></div>
<div class="PH w20" style="height:5px;width:20%"></div>
</div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w80" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w70" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w80" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w60" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w70" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:5px 8px;border-top:1px solid #F0EDE6"><div class="PH w40" style="height:4px"></div></div>
</div>
<!-- ② upload -->
<div style="border:1.5px dashed rgba(166,218,216,.7);border-radius:3px;background:rgba(166,218,216,.06);padding:12px 8px;display:flex;flex-direction:column;align-items:center;gap:3px">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
<div class="PH w50" style="height:5px"></div>
<div class="PH w70" style="height:4px"></div>
</div>
<!-- ③ needs metadata -->
<div style="background:#fff;border:1px solid #E0DDD5;border-top:2px solid #F39C12;border-radius:3px;overflow:hidden">
<div style="padding:5px 8px;border-bottom:1px solid #F0EDE6;display:flex;justify-content:space-between;align-items:center">
<div class="PH w40" style="height:5px"></div>
<div style="background:#FFF3CD;border-radius:8px;padding:1px 5px;font-size:6px;font-weight:800;color:#856404">5</div>
</div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w70" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px;border-bottom:1px solid #F0EDE6"><div class="PH w60" style="height:6px;background:#E0DDD5"></div></div>
<div style="padding:4px 8px"><div class="PH w70" style="height:6px;background:#E0DDD5"></div></div>
</div>
</div>
</div>
<div class="sc">Stacking order on mobile: recent docs → upload → metadata</div>
</div>
<!-- Explanation -->
<div class="sb">
<div class="sl">Stacking order rationale</div>
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;line-height:1.7;display:flex;flex-direction:column;gap:14px;color:#333">
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">① Recent Activity — first</div>
The most common task on mobile: browsing recently-touched documents. This should be immediately visible without scrolling past an upload zone most users won't use every visit.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:4px;">② Upload zone — second</div>
Mobile uploads happen but are less frequent than browsing. Positioned after the list so it doesn't block the primary use case, but still reachable with a single scroll.
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#F39C12;margin-bottom:4px;">③ Needs Metadata — last</div>
Metadata enrichment on mobile is uncommon (small screen, lots of form fields). It appears last — accessible to those who need it, invisible noise to everyone else.
</div>
<div style="background:#F0FDF4;border:1px solid #86EFAC;border-radius:5px;padding:10px 12px;font-size:11px;color:#14532D">
<strong>Touch targets:</strong> All interactive rows in DashboardRecentDocuments and DashboardNeedsMetadata must meet <strong>min-height 44px</strong> (WCAG 2.5.5). The upload zone's click target is the full box — no small button.
</div>
</div>
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;line-height:1.7;color:#333;margin-top:0">
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:10px">Dual-audience notes</div>
<div style="display:flex;flex-direction:column;gap:10px">
<div>
<span style="font-size:8px;font-weight:800;background:#002850;color:#A6DAD8;padding:1px 6px;border-radius:3px;text-transform:uppercase;letter-spacing:.5px">Seniors 60+</span>
<div style="margin-top:4px">Document title is Merriweather serif at <strong>18px minimum</strong> — the most commonly undersized element in this type of list. Date label in <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">text-ink-3</code> at 12px — acceptable for supplementary metadata but never below that. Sufficient line-height (1.6) and border separators provide clear row breaks without relying on color.</div>
</div>
<div>
<span style="font-size:8px;font-weight:800;background:#374151;color:#D1FAE5;padding:1px 6px;border-radius:3px;text-transform:uppercase;letter-spacing:.5px">Millennials</span>
<div style="margin-top:4px">Information density is preserved on desktop. The upload zone accepts drag-and-drop natively — no button required for the gesture-native user. Stats footnote satisfies curiosity without cluttering the primary view.</div>
</div>
</div>
</div>
</div>
</div>
<!-- impl-ref: Mobile -->
<div class="impl-ref">
<div class="impl-ref-hdr">Implementation Reference — Mobile Stacking
<span>Real values · mockup above is ~55% scale</span>
</div>
<table>
<thead><tr><th>Element</th><th>Tailwind classes</th><th>Real size</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td>Grid (mobile)</td>
<td><code>grid-cols-1</code> (default, overridden at lg)</td>
<td><span class="ir-px">full width</span></td>
<td>No explicit mobile grid — single column is the default</td>
</tr>
<tr>
<td>Doc row touch target</td>
<td><code>flex items-center justify-between py-3 border-b border-line</code></td>
<td><span class="ir-px">min-h 44px via py-3 + content</span></td>
<td><strong>Most commonly undersized.</strong> py-3 (12px × 2) + 18px text = ~42px. Add <code>min-h-[44px]</code> to guarantee WCAG 2.5.5</td>
</tr>
<tr>
<td>Metadata row touch target</td>
<td><code>flex items-center border-b border-line py-3</code></td>
<td><span class="ir-px">min-h 44px</span></td>
<td>Same rule — <code>min-h-[44px]</code> required</td>
</tr>
<tr>
<td>Upload zone (mobile)</td>
<td>Existing DropZone — no change to component</td>
<td><span class="ir-px">full width, py-6</span></td>
<td>Entire zone is the click target — WCAG 2.5.5 satisfied by size</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 3 — RECENT ACTIVITY CARD + STATS FOOTNOTE
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">3</span> Recent Activity Card — stats footnote detail</div>
<div class="sg sg-2" style="align-items:start">
<div class="sb">
<div class="sl">Component detail</div>
<!-- Card mockup -->
<div class="CARD" style="max-width:460px">
<div class="CARD-HEAD">
<h3>Recent Activity</h3>
<a href="#">All documents →</a>
</div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 History Tee Dokument (bearbeitet)</span><span class="DOC-DATE">31. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 Hash Tee — review</span><span class="DOC-DATE">30. März 2026</span></div>
<div class="DOC-ROW"><span class="DOC-TITLE">E28 Hash Tee — version</span><span class="DOC-DATE">30. März 2026</span></div>
<div class="CARD-FOOT" style="display:flex;align-items:center;justify-content:space-between">
<span class="CARD-FOOT-TEXT">248 Documents &nbsp;·&nbsp; 34 Persons</span>
<!-- middle dot acts as separator, not a color-only cue -->
</div>
</div>
<div class="ann-block">
<strong>Stats footnote rules</strong>
<ul>
<li>Only renders when <code>stats?.totalDocuments != null</code></li>
<li>Persons count follows only when <code>stats?.totalPersons != null</code></li>
<li>The middle dot <code>·</code> is a text separator — not a visual-only cue</li>
<li>Uses <code>text-ink-3</code> token — light enough to recede, but still WCAG AA (4.5:1) on white surface</li>
<li>No units abbreviation: "248 Documents", not "248 docs" — plain language for seniors</li>
</ul>
</div>
</div>
<div class="sb">
<div class="sl">Component prop change</div>
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;line-height:1.7;color:#333;display:flex;flex-direction:column;gap:12px">
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:6px">DashboardRecentDocuments.svelte — new prop</div>
<pre style="background:#0d1117;color:#a5d6ff;font-family:monospace;font-size:10px;padding:12px 14px;border-radius:5px;line-height:1.8;overflow-x:auto">interface Props {
recentDocs: Document[];
stats?: {
totalDocuments?: number;
totalPersons?: number;
} | null;
}</pre>
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:6px">Footnote template snippet</div>
<pre style="background:#0d1117;color:#a5d6ff;font-family:monospace;font-size:10px;padding:12px 14px;border-radius:5px;line-height:1.8;overflow-x:auto">{#if stats?.totalDocuments != null}
&lt;div class="mt-2 border-t border-line
pt-3 font-sans text-xs
text-ink-3"&gt;
{stats.totalDocuments}
{m.dashboard_stats_documents()}
{#if stats.totalPersons != null}
&nbsp;·&nbsp;
{stats.totalPersons}
{m.dashboard_stats_persons()}
{/if}
&lt;/div&gt;
{/if}</pre>
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888;margin-bottom:6px">New i18n keys (all 3 locales)</div>
<table style="width:100%;font-size:10px;border-collapse:collapse">
<thead><tr style="border-bottom:1.5px solid #E0DDD5"><th style="text-align:left;padding:4px 6px;font-size:8px;font-weight:800;color:#888;text-transform:uppercase">Key</th><th style="text-align:left;padding:4px 6px;font-size:8px;font-weight:800;color:#888;text-transform:uppercase">de</th><th style="text-align:left;padding:4px 6px;font-size:8px;font-weight:800;color:#888;text-transform:uppercase">en</th><th style="text-align:left;padding:4px 6px;font-size:8px;font-weight:800;color:#888;text-transform:uppercase">es</th></tr></thead>
<tbody>
<tr style="border-bottom:1px solid #F0EDE6"><td style="padding:4px 6px;font-family:monospace;font-size:9px">dashboard_stats_documents</td><td style="padding:4px 6px">Dokumente</td><td style="padding:4px 6px">Documents</td><td style="padding:4px 6px">Documentos</td></tr>
<tr><td style="padding:4px 6px;font-family:monospace;font-size:9px">dashboard_stats_persons</td><td style="padding:4px 6px">Personen</td><td style="padding:4px 6px">Persons</td><td style="padding:4px 6px">Personas</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="impl-ref">
<div class="impl-ref-hdr">Implementation Reference — Recent Activity Card
<span>Real values · mockup above is ~55% scale</span>
</div>
<table>
<thead><tr><th>Element</th><th>Tailwind classes</th><th>Real size</th><th>Notes</th></tr></thead>
<tbody>
<tr>
<td>Card container</td>
<td><code>rounded-sm border border-line bg-surface</code></td>
<td><span class="ir-px">border 1px</span></td>
<td>Unchanged from existing DashboardRecentDocuments styles</td>
</tr>
<tr>
<td>Card heading row</td>
<td><code>flex items-center justify-between px-4 pt-4 pb-3</code></td>
<td><span class="ir-px">px 16px, pt 16px, pb 12px</span></td>
<td>Unchanged</td>
</tr>
<tr>
<td>Section heading text</td>
<td><code>font-sans text-xs font-bold tracking-widest text-gray-400 uppercase</code></td>
<td><span class="ir-px">12px / 700</span></td>
<td>Unchanged</td>
</tr>
<tr>
<td>Document title</td>
<td><code>font-serif text-lg text-ink hover:text-ink-2 hover:underline</code></td>
<td><span class="ir-px">18px / 400 — most commonly undersized</span></td>
<td><strong>Must not fall below 18px.</strong> Serves both readability (seniors) and visual hierarchy</td>
</tr>
<tr>
<td>Date label</td>
<td><code>ml-2 shrink-0 font-sans text-xs text-gray-400</code></td>
<td><span class="ir-px">12px</span></td>
<td>Minimum permitted size for supplementary metadata — do not reduce further</td>
</tr>
<tr>
<td>Document row</td>
<td><code>flex items-center justify-between border-b border-line py-2 px-4 last:border-0</code></td>
<td><span class="ir-px">py 8px, min-h ~44px with 18px text</span></td>
<td>Add <code>min-h-[44px]</code> to guarantee WCAG 2.5.5 touch target</td>
</tr>
<tr>
<td>Stats footnote wrapper</td>
<td><code>mt-2 border-t border-line px-4 pt-3 pb-4 font-sans text-xs text-ink-3</code></td>
<td><span class="ir-px">12px / 400, pt 12px, pb 16px</span></td>
<td>New addition. <code>text-ink-3</code> token must pass 4.5:1 on <code>bg-surface</code> — verify in both light and dark mode</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 4 — SERVER DATA CHANGES
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">4</span> Server Data Changes — +page.server.ts</div>
<div class="sg sg-2" style="align-items:start">
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:6px;padding:16px 18px;font-size:11px;line-height:1.7;color:#333;display:flex;flex-direction:column;gap:14px">
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#DC2626;margin-bottom:6px">Remove</div>
<ul style="padding-left:16px;display:flex;flex-direction:column;gap:4px">
<li>The <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">/api/notifications</code> fetch from <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">Promise.allSettled</code> — the bell component fetches its own data client-side</li>
<li>The <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">mentions: NotificationDTO[]</code> variable and its allSettled result handling</li>
<li>The <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">mentions</code> key from the return object</li>
<li>The <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">NotificationDTO</code> type import (no longer used in this file)</li>
</ul>
</div>
<div>
<div style="font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#002850;margin-bottom:6px">Add</div>
<ul style="padding-left:16px;display:flex;flex-direction:column;gap:4px">
<li>A <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">/api/stats</code> GET call inside the <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">isDashboard</code> allSettled block</li>
<li><code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">stats: StatsDTO | null</code> in the return — <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">null</code> on any failure</li>
<li><code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">StatsDTO</code> import from generated types (already in <code style="background:#F5F5F5;padding:0 4px;border-radius:2px;font-size:10px">src/lib/generated/api.ts</code>)</li>
</ul>
</div>
</div>
<div style="background:#0d1117;border-radius:6px;overflow:hidden;border:1px solid #30363d">
<div style="background:#161b22;padding:8px 14px;font-size:9px;font-weight:800;color:#f0883e;border-bottom:1px solid #30363d;text-transform:uppercase;letter-spacing:.4px">allSettled block — after change</div>
<pre style="color:#a5d6ff;font-family:monospace;font-size:10px;padding:14px 16px;line-height:1.8;overflow-x:auto">const [incompleteResult,
recentResult,
statsResult] =
await Promise.allSettled([
api.GET('/api/documents/incomplete',
{ params: { query: { size: 5 } } }),
api.GET('/api/documents/recent-activity',
{ params: { query: { size: 5 } } }),
api.GET('/api/stats'),
]);
<span style="color:#8b949e">// … existing incomplete/recent handling …</span>
let stats: StatsDTO | null = null;
if (statsResult.status === 'fulfilled'
&& statsResult.value.response.ok) {
stats = statsResult.value.data ?? null;
}</pre>
</div>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 5 — CHANGES SUMMARY
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">5</span> Changes Summary</div>
<div class="CHANGES">
<h2>All files touched</h2>
<div class="CHANGES-GRID">
<div class="C-COL add">
<h3>Added / New behaviour</h3>
<ul>
<li><code>mt-4 grid grid-cols-1 gap-4 lg:grid-cols-[1fr_300px]</code> grid in +page.svelte</li>
<li><code>stats</code> prop on DashboardRecentDocuments</li>
<li>Stats footnote inside DashboardRecentDocuments</li>
<li><code>/api/stats</code> fetch in +page.server.ts</li>
<li><code>dashboard_stats_documents</code> i18n key (de / en / es)</li>
<li><code>dashboard_stats_persons</code> i18n key (de / en / es)</li>
</ul>
</div>
<div class="C-COL remove">
<h3>Removed</h3>
<ul>
<li><code>DashboardMentions</code> import and usage in +page.svelte</li>
<li><code>/api/notifications</code> fetch from server load</li>
<li><code>mentions</code> variable and return value in server load</li>
<li><code>NotificationDTO</code> import in +page.server.ts</li>
<li>The conditional 2-col grid for mentions+metadata</li>
</ul>
</div>
<div class="C-COL keep">
<h3>Kept unchanged</h3>
<ul>
<li><code>DashboardMentions.svelte</code> file (not deleted)</li>
<li><code>DashboardNeedsMetadata.svelte</code> (no changes)</li>
<li><code>DashboardResumeStrip.svelte</code> (no changes)</li>
<li><code>DropZone.svelte</code> (no changes)</li>
<li><code>NotificationBell.svelte</code> — already has "View all" link</li>
<li><code>SearchFilterBar.svelte</code> (no changes)</li>
</ul>
</div>
<div class="C-COL keep">
<h3>Explicitly out of scope</h3>
<ul>
<li>Dedicated <code>/notifications</code> overview page</li>
<li>DropZone accepted file types or upload behaviour</li>
<li>Dark mode token adjustments</li>
<li>Backend changes (none needed)</li>
<li>Any changes to admin, persons, or correspondence routes</li>
</ul>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 6 — EDGE CASES
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">6</span> Edge Cases</div>
<div class="sg sg-2" style="align-items:start">
<div>
<div class="EDGE">
<div class="EDGE-LABEL">Read-only user (no canWrite)</div>
<div class="EDGE-BODY">DropZone is hidden. Right column contains only DashboardNeedsMetadata. If there are also no incomplete documents, the right <code>&lt;div class="flex flex-col gap-4"&gt;</code> is empty — the grid column produces no visual gap, recent activity expands naturally.</div>
</div>
<div class="EDGE">
<div class="EDGE-LABEL">No incomplete documents</div>
<div class="EDGE-BODY">DashboardNeedsMetadata renders nothing (already guarded by <code>incompleteDocs.length > 0</code>). Combined with a canWrite user, the right column shows only the upload zone.</div>
</div>
<div class="EDGE">
<div class="EDGE-LABEL">No recent documents (new / empty archive)</div>
<div class="EDGE-BODY">DashboardRecentDocuments already handles empty state (renders nothing when <code>recentDocs.length === 0</code>). Stats footnote still renders as long as the API call succeeded — "0 Documents · 0 Persons" is valid and informative.</div>
</div>
</div>
<div>
<div class="EDGE">
<div class="EDGE-LABEL">/api/stats fetch fails</div>
<div class="EDGE-BODY"><code>Promise.allSettled</code> isolates the failure. <code>stats</code> is returned as <code>null</code>. The <code>{#if stats?.totalDocuments != null}</code> guard silently suppresses the footnote. Everything else renders normally — no error banner, no visual regression.</div>
</div>
<div class="EDGE">
<div class="EDGE-LABEL">No last-visited document in localStorage</div>
<div class="EDGE-BODY">DashboardResumeStrip already handles this — it renders nothing. No gap between search bar and the dashboard grid.</div>
</div>
<div class="EDGE">
<div class="EDGE-LABEL">Very long document title in recent activity</div>
<div class="EDGE-BODY">Title should be truncated with <code>truncate</code> Tailwind class (already present in existing component — verify). The date label has <code>shrink-0</code> so it is never squeezed off-screen.</div>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════
SECTION 7 — ACCEPTANCE CRITERIA
══════════════════════════════════ -->
<div class="sec">
<div class="sec-h"><span class="sec-num">7</span> Acceptance Criteria</div>
<div class="AC">
<div class="AC-ITEM">Dashboard page no longer renders the notifications/mentions widget. The bell icon in the header continues to work and its dropdown still shows the "View all notifications" link.</div>
<div class="AC-ITEM">On viewports ≥ 1024 px the dashboard shows a two-column grid: recent activity left (~remaining width), sidebar right (300 px fixed). <span class="tag tag-mobile">mobile</span></div>
<div class="AC-ITEM">On viewports &lt; 1024 px the columns stack: recent docs first, upload zone second, needs-metadata third. <span class="tag tag-mobile">mobile</span></div>
<div class="AC-ITEM">All interactive document rows have a minimum touch target height of 44 px. <span class="tag tag-a11y">WCAG 2.5.5</span></div>
<div class="AC-ITEM">Document titles in the recent-activity list render at minimum 18 px (Merriweather serif, <code>text-lg</code>). <span class="tag tag-a11y">WCAG 1.4.4</span></div>
<div class="AC-ITEM">Stats footnote "248 Documents · 34 Persons" appears at the bottom of the recent-activity card in <code>text-xs text-ink-3</code>. It is absent when the <code>/api/stats</code> call fails or returns null. <span class="tag tag-data">data</span></div>
<div class="AC-ITEM">Read-only users (no canWrite permission) do not see the upload zone. The dashboard still renders correctly without it.</div>
<div class="AC-ITEM">When no incomplete documents exist, the Needs Metadata card is absent. The right column shows only the upload zone (or is empty for read-only users) — no visual gap or empty box.</div>
<div class="AC-ITEM"><code>npm run check</code> passes — no TypeScript errors. The new <code>stats</code> prop on DashboardRecentDocuments is typed as <code>StatsDTO | null | undefined</code>.</div>
<div class="AC-ITEM"><code>npm run lint</code> passes — no Prettier or ESLint errors.</div>
</div>
</div>
</div>
</body>
</html>