Design spec for a dashboard widget that surfaces documents needing metadata after batch upload. Placed between Resume strip and MissionControlStrip rather than as a 4th strip column (strip at visual capacity; batch reality makes count tiles useless for seniors). Covers responsive behavior at 320/768/1440, row anatomy with 72/64px touch targets, state matrix (empty/loading/error/ after-upload), full a11y contract, dark-mode verification notes, and an impl-ref table with exact Tailwind classes. Refs #296 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
578 lines
32 KiB
HTML
578 lines
32 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>Ergänzungs-Liste — Dashboard-Block · Spec (Issue t.b.d.)</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;
|
||
--red:#A83232;--red-bg:#FCEBEB;
|
||
--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;}
|
||
.prose strong{color:var(--text);}
|
||
|
||
/* 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:20px;max-width:740px;}
|
||
|
||
/* 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.red{background:var(--red-bg);border-left:3px solid var(--red);}
|
||
.callout strong{font-weight:700;}
|
||
|
||
/* Mock frame wrappers — scaled visual mockups (~55% of real) */
|
||
.frames-row{display:flex;gap:32px;flex-wrap:wrap;align-items:flex-start;margin-bottom:20px;}
|
||
.frame-caption{font-family:var(--mono);font-size:10px;color:var(--muted);display:block;margin-top:8px;max-width:100%;}
|
||
|
||
/* ========== MOBILE MOCK (176px wide = 320 × .55) ========== */
|
||
.m-phone{width:176px;background:#fff;border:6px solid #2A2A2A;border-radius:22px;overflow:hidden;box-shadow:0 6px 20px rgba(0,0,0,.15);}
|
||
.m-status{height:11px;background:#000;}
|
||
.m-body{background:var(--surface);padding:7px;}
|
||
|
||
/* Eyebrow */
|
||
.mb-eyebrow{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;padding:0 2px;}
|
||
.mb-eyebrow-l{font-size:6px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);}
|
||
.mb-eyebrow-count{background:var(--navy);color:#fff;font-size:6px;font-weight:700;padding:1px 5px;border-radius:8px;}
|
||
|
||
.mb-block{background:#fff;border:1px solid var(--sand);border-radius:3px;overflow:hidden;}
|
||
.mb-row{display:flex;align-items:center;padding:6px 7px;gap:5px;border-bottom:1px solid var(--sand);}
|
||
.mb-row:last-child{border-bottom:none;}
|
||
.mb-icon{width:14px;height:14px;background:rgba(0,40,80,.06);border-radius:2px;display:flex;align-items:center;justify-content:center;font-size:5px;font-weight:700;color:var(--navy);flex-shrink:0;}
|
||
.mb-icon.pdf{background:rgba(168,50,50,.12);color:var(--red);}
|
||
.mb-icon.jpg{background:rgba(46,110,57,.12);color:var(--green);}
|
||
.mb-main{flex:1;min-width:0;}
|
||
.mb-title{font-family:Georgia,serif;font-size:7px;color:var(--navy);line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||
.mb-meta{font-size:5.5px;color:var(--muted);margin-top:1px;}
|
||
.mb-chev{color:var(--subtle);font-size:8px;flex-shrink:0;}
|
||
|
||
.mb-footer{padding:5px 7px;border-top:1px solid var(--sand);background:rgba(0,40,80,.02);text-align:center;}
|
||
.mb-footer-link{font-size:6px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--navy);}
|
||
|
||
/* ========== DESKTOP MOCK (640px wide = ~1160 content-col × .55) ========== */
|
||
.d-frame{width:640px;background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:14px;}
|
||
.d-eyebrow{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;}
|
||
.d-eyebrow-l{font-size:9px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);}
|
||
.d-eyebrow-count{background:var(--navy);color:#fff;font-size:9px;font-weight:700;padding:2px 8px;border-radius:10px;}
|
||
|
||
.d-block{background:#fff;border:1px solid var(--sand);border-radius:4px;overflow:hidden;}
|
||
.d-row{display:flex;align-items:center;padding:9px 14px;gap:10px;border-bottom:1px solid var(--sand);cursor:pointer;}
|
||
.d-row:last-child{border-bottom:none;}
|
||
.d-row:hover{background:rgba(166,218,216,.08);}
|
||
.d-row.focused{background:rgba(166,218,216,.08);box-shadow:inset 0 0 0 2px var(--navy);}
|
||
.d-icon{width:20px;height:20px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:7px;font-weight:700;flex-shrink:0;}
|
||
.d-icon.pdf{background:rgba(168,50,50,.12);color:var(--red);}
|
||
.d-icon.jpg{background:rgba(46,110,57,.12);color:var(--green);}
|
||
.d-icon.tif{background:rgba(91,94,166,.12);color:#5B5EA6;}
|
||
.d-main{flex:1;min-width:0;}
|
||
.d-title{font-family:Georgia,serif;font-size:12px;color:var(--navy);line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||
.d-meta{font-size:9px;color:var(--muted);margin-top:2px;}
|
||
.d-chev{color:var(--subtle);font-size:14px;flex-shrink:0;}
|
||
|
||
.d-footer{padding:8px 14px;border-top:1px solid var(--sand);background:rgba(0,40,80,.02);display:flex;justify-content:flex-end;}
|
||
.d-footer-link{font-size:9px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--navy);}
|
||
|
||
/* Empty / not shown state */
|
||
.empty-note{background:rgba(46,110,57,.06);border:1px dashed rgba(46,110,57,.25);border-radius:4px;padding:14px 16px;font-size:11px;color:var(--green);text-align:center;font-style:italic;}
|
||
|
||
/* After-upload banner */
|
||
.banner{background:var(--navy);color:#fff;border-radius:4px;padding:10px 14px;display:flex;align-items:center;gap:10px;width:640px;margin-bottom:12px;}
|
||
.banner-check{width:18px;height:18px;border-radius:50%;background:var(--mint);color:var(--navy);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;flex-shrink:0;}
|
||
.banner-t{font-size:11px;flex:1;}
|
||
.banner-t strong{color:var(--mint);}
|
||
.banner-cta{background:var(--mint);color:var(--navy);font-size:9px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;padding:4px 10px;border-radius:3px;}
|
||
.banner-x{color:rgba(255,255,255,.5);font-size:14px;cursor:pointer;padding:0 4px;}
|
||
|
||
/* Impl-ref table */
|
||
.impl-ref{background:#fff;border:1px solid var(--border);border-radius:6px;overflow:hidden;margin-top:20px;}
|
||
.impl-ref table{width:100%;border-collapse:collapse;}
|
||
.impl-ref th{background:rgba(0,40,80,.04);font-size:9px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);padding:8px 12px;text-align:left;border-bottom:1px solid var(--border);}
|
||
.impl-ref td{font-size:11px;color:var(--text);padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top;line-height:1.55;}
|
||
.impl-ref tr:last-child td{border-bottom:none;}
|
||
.impl-ref td:first-child{font-weight:700;color:var(--navy);white-space:nowrap;}
|
||
.impl-ref code{font-family:var(--mono);font-size:10px;background:rgba(0,40,80,.06);padding:1px 4px;border-radius:2px;color:var(--navy);}
|
||
.impl-ref td.px{font-family:var(--mono);font-size:10px;color:var(--muted);white-space:nowrap;}
|
||
.impl-ref td.note{font-size:10px;color:var(--muted);font-style:italic;}
|
||
|
||
/* A11y box */
|
||
.a11y-box{background:rgba(91,94,166,.06);border-left:3px solid #5B5EA6;border-radius:0 4px 4px 0;padding:14px 18px;}
|
||
.a11y-box h4{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#5B5EA6;margin-bottom:8px;}
|
||
.a11y-box ul{list-style:none;padding-left:0;}
|
||
.a11y-box li{font-size:12px;color:var(--text);padding-left:18px;position:relative;margin-bottom:6px;line-height:1.55;}
|
||
.a11y-box li::before{content:'✓';position:absolute;left:0;color:#5B5EA6;font-weight:700;}
|
||
.a11y-box code{font-family:var(--mono);font-size:10px;background:rgba(91,94,166,.1);padding:1px 4px;border-radius:2px;}
|
||
|
||
/* Two-col grid */
|
||
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;}
|
||
.two-col h3{font-size:12px;font-weight:700;color:var(--navy);margin-bottom:6px;letter-spacing:.02em;}
|
||
.two-col p{font-size:12px;color:var(--muted);line-height:1.55;}
|
||
|
||
/* Inline code */
|
||
code.inline{font-family:var(--mono);font-size:11px;background:rgba(0,40,80,.06);padding:1px 5px;border-radius:3px;color:var(--navy);}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="doc">
|
||
|
||
<!-- ===== HEADER ===== -->
|
||
<div class="hdr">
|
||
<div class="badges">
|
||
<span class="badge">DASHBOARD</span>
|
||
<span class="badge badge-g">NEW BLOCK</span>
|
||
<span class="badge badge-g">MOBILE-FIRST</span>
|
||
</div>
|
||
<h1>Ergänzungs-Liste — Dashboard-Block</h1>
|
||
<div class="hdr-meta">Familienarchiv · 2026-04-20 · Leonie Voss, UX Lead</div>
|
||
</div>
|
||
|
||
<div class="decision-box">
|
||
<h2>The call</h2>
|
||
<p class="prose">A dedicated list-block on the dashboard, placed <strong>between the Resume strip and the MissionControlStrip</strong>, surfaces the documents that were just uploaded and still need metadata. It replaces the orphaned <code class="inline">DashboardNeedsMetadata.svelte</code> component with a spec'd, senior-friendly row design.</p>
|
||
<p class="prose"><strong>Why here, not a 4th column in MissionControlStrip:</strong> (1) strip is already at 3-column visual capacity; (2) batch-upload reality (10–15 docs arriving at once) makes a count tile useless — seniors need to see <em>which</em> docs are waiting; (3) placing it directly under the DropZone sightline makes upload→enrich the most visually-coupled pair on the page.</p>
|
||
<p class="prose"><strong>Scope of this spec:</strong> the block itself (layout, row anatomy, empty/loading/error states, a11y, responsive behavior at 320/768/1440). The post-upload success banner is included as a coupled interaction. The strip redesign, pagination, and the other three pipeline stages (segment / transcribe / review) are <em>not</em> in scope — they earn their own specs.</p>
|
||
</div>
|
||
|
||
<!-- ===== SECTION 1 — ANATOMY ===== -->
|
||
<div class="sec">
|
||
<div class="sec-label">01 — Block anatomy</div>
|
||
<h2 class="sec-title">Three pieces: eyebrow, list, footer</h2>
|
||
<p class="sec-sub">The block is a single <code class="inline"><section></code> landmark with an eyebrow heading, a divided list of up to 5 rows, and an optional footer link when more than 5 docs are pending. At <code class="inline">incompleteDocs.length === 0</code> the block renders <strong>nothing</strong> — no "all clear" state, no empty card. A clean dashboard means no work to do.</p>
|
||
|
||
<div class="frames-row">
|
||
<div>
|
||
<div class="m-phone">
|
||
<div class="m-status"></div>
|
||
<div class="m-body">
|
||
<div class="mb-eyebrow">
|
||
<span class="mb-eyebrow-l">Benötigen Metadaten</span>
|
||
<span class="mb-eyebrow-count">12</span>
|
||
</div>
|
||
<div class="mb-block">
|
||
<div class="mb-row">
|
||
<div class="mb-icon pdf">PDF</div>
|
||
<div class="mb-main">
|
||
<div class="mb-title">Brief Oma Hilde 1962</div>
|
||
<div class="mb-meta">vor 2 Min.</div>
|
||
</div>
|
||
<div class="mb-chev">›</div>
|
||
</div>
|
||
<div class="mb-row">
|
||
<div class="mb-icon pdf">PDF</div>
|
||
<div class="mb-main">
|
||
<div class="mb-title">Geburtsurkunde Opa</div>
|
||
<div class="mb-meta">vor 2 Min.</div>
|
||
</div>
|
||
<div class="mb-chev">›</div>
|
||
</div>
|
||
<div class="mb-row">
|
||
<div class="mb-icon jpg">JPG</div>
|
||
<div class="mb-main">
|
||
<div class="mb-title">Foto Hochzeit Tante Elsa</div>
|
||
<div class="mb-meta">vor 2 Min.</div>
|
||
</div>
|
||
<div class="mb-chev">›</div>
|
||
</div>
|
||
<div class="mb-row">
|
||
<div class="mb-icon pdf">PDF</div>
|
||
<div class="mb-main">
|
||
<div class="mb-title">Postkarte Bodensee</div>
|
||
<div class="mb-meta">vor 2 Min.</div>
|
||
</div>
|
||
<div class="mb-chev">›</div>
|
||
</div>
|
||
<div class="mb-row">
|
||
<div class="mb-icon pdf">PDF</div>
|
||
<div class="mb-main">
|
||
<div class="mb-title">Meldebescheinigung 1971</div>
|
||
<div class="mb-meta">vor 3 Min.</div>
|
||
</div>
|
||
<div class="mb-chev">›</div>
|
||
</div>
|
||
<div class="mb-footer">
|
||
<span class="mb-footer-link">Alle 12 anzeigen →</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<span class="frame-caption">320px · 12 docs pending · 5 shown, rest via footer link to /enrich</span>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="d-frame">
|
||
<div class="d-eyebrow">
|
||
<span class="d-eyebrow-l">Benötigen Metadaten</span>
|
||
<span class="d-eyebrow-count">12</span>
|
||
</div>
|
||
<div class="d-block">
|
||
<div class="d-row">
|
||
<div class="d-icon pdf">PDF</div>
|
||
<div class="d-main">
|
||
<div class="d-title">Brief Oma Hilde 1962 — An meinen lieben Ludwig</div>
|
||
<div class="d-meta">vor 2 Minuten hochgeladen · 3 Seiten</div>
|
||
</div>
|
||
<div class="d-chev">›</div>
|
||
</div>
|
||
<div class="d-row focused">
|
||
<div class="d-icon pdf">PDF</div>
|
||
<div class="d-main">
|
||
<div class="d-title">Geburtsurkunde Opa Friedrich</div>
|
||
<div class="d-meta">vor 2 Minuten hochgeladen · 1 Seite</div>
|
||
</div>
|
||
<div class="d-chev">›</div>
|
||
</div>
|
||
<div class="d-row">
|
||
<div class="d-icon jpg">JPG</div>
|
||
<div class="d-main">
|
||
<div class="d-title">Foto Hochzeit Tante Elsa — Standesamt Ulm</div>
|
||
<div class="d-meta">vor 2 Minuten hochgeladen</div>
|
||
</div>
|
||
<div class="d-chev">›</div>
|
||
</div>
|
||
<div class="d-row">
|
||
<div class="d-icon pdf">PDF</div>
|
||
<div class="d-main">
|
||
<div class="d-title">Postkarte Bodensee 1958</div>
|
||
<div class="d-meta">vor 2 Minuten hochgeladen · 2 Seiten</div>
|
||
</div>
|
||
<div class="d-chev">›</div>
|
||
</div>
|
||
<div class="d-row">
|
||
<div class="d-icon tif">TIF</div>
|
||
<div class="d-main">
|
||
<div class="d-title">Meldebescheinigung Stadt Tübingen 1971</div>
|
||
<div class="d-meta">vor 3 Minuten hochgeladen · 1 Seite</div>
|
||
</div>
|
||
<div class="d-chev">›</div>
|
||
</div>
|
||
<div class="d-footer">
|
||
<span class="d-footer-link">Alle 12 anzeigen →</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<span class="frame-caption">1440px · main content column (768–960px target) · 12 docs pending · row 2 shows keyboard focus state</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="callout navy">
|
||
<div><strong>Row cap = 5.</strong> When pending docs exceed 5, the block shows the 5 most-recently-uploaded and a footer link to <code class="inline">/enrich</code>. This caps block height so the dashboard stays navigable, and reinforces <code class="inline">/enrich</code> as the canonical "long queue" page.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== SECTION 2 — ROW ANATOMY ===== -->
|
||
<div class="sec">
|
||
<div class="sec-label">02 — Row anatomy</div>
|
||
<h2 class="sec-title">Icon · title · upload time · chevron</h2>
|
||
<p class="sec-sub">The entire row is one <code class="inline"><a href="/enrich/{id}"></code>. No nested interactive elements — one tap, one destination. Touch target: <strong>72px minimum row height on mobile, 64px on desktop</strong>. Both exceed the 48px WCAG 2.2 AA floor for the senior audience.</p>
|
||
|
||
<div class="two-col">
|
||
<div>
|
||
<h3>Left: file-type badge (20×20px)</h3>
|
||
<p>Visual differentiation between PDF / JPG / PNG / TIF. Not decorative — gives seniors a scannable category cue without reading. Uses redundant color + text ("PDF", "JPG") so it passes WCAG 1.4.1.</p>
|
||
</div>
|
||
<div>
|
||
<h3>Center: title + upload time</h3>
|
||
<p>Title in <code class="inline">font-serif text-base</code> (16px mobile) / <code class="inline">text-lg</code> (18px desktop). Truncated with <code class="inline">text-ellipsis</code> on narrow viewports. Relative time ("vor 2 Min.") in <code class="inline">font-sans text-xs text-ink-2</code>.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="two-col">
|
||
<div>
|
||
<h3>Right: chevron (decorative)</h3>
|
||
<p>Navigation affordance. <code class="inline">aria-hidden="true"</code>. Becomes slightly more prominent on hover (<code class="inline">opacity-30 → 70</code>).</p>
|
||
</div>
|
||
<div>
|
||
<h3>States: hover, focus-visible, active</h3>
|
||
<p>Hover: <code class="inline">bg-brand-mint/10</code> wash. Focus-visible: <code class="inline">ring-2 ring-brand-navy ring-offset-2</code> <em>inside</em> the row (visible against row bg). Active: <code class="inline">bg-brand-mint/20</code>.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="callout orange">
|
||
<div><strong>DTO extension needed.</strong> Current <code class="inline">IncompleteDocumentDTO</code> only carries <code class="inline">id</code> and <code class="inline">title</code>. To render "vor 2 Min." and the file-type badge, add <code class="inline">uploadedAt: Instant</code> and <code class="inline">mimeType: String</code>. Small, cheap backend change. Without these fields the block still works (skip meta line, use a generic doc icon) but the senior-facing UX is meaningfully worse.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== SECTION 3 — STATES ===== -->
|
||
<div class="sec">
|
||
<div class="sec-label">03 — States</div>
|
||
<h2 class="sec-title">Empty · loading · error · after-upload</h2>
|
||
|
||
<div class="two-col" style="margin-bottom:24px;">
|
||
<div>
|
||
<h3>Empty (<code class="inline">length === 0</code>)</h3>
|
||
<p>Render <code class="inline">null</code>. The block disappears entirely. Seniors don't need a "nothing to do" card — absence is clear.</p>
|
||
<div class="empty-note" style="margin-top:10px;">(block not rendered)</div>
|
||
</div>
|
||
<div>
|
||
<h3>Loading</h3>
|
||
<p>On initial dashboard SSR this data comes in with the page load — no spinner needed. If you later add client-side refresh, use a skeleton (3 rows of gray blocks at 72px height, <code class="inline">animate-pulse</code>, respects <code class="inline">prefers-reduced-motion</code>).</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="two-col">
|
||
<div>
|
||
<h3>Error</h3>
|
||
<p>If the <code class="inline">/api/documents/incomplete</code> call fails, render the block in a muted error state: eyebrow reads "Liste konnte nicht geladen werden", with a retry link. Do not suppress — the user needs to know their queue may be out of date.</p>
|
||
</div>
|
||
<div>
|
||
<h3>After-upload (immediately after DropZone fires)</h3>
|
||
<p>Transient success banner above the block. Auto-dismiss after 8s, <strong>with</strong> a manual close X. Content: "12 Dokumente hochgeladen" + "Jetzt ergänzen →" CTA → scrolls/focuses the list block. Use <code class="inline">aria-live="polite"</code> so screen readers announce the count.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Banner mockup -->
|
||
<div style="margin-top:24px;">
|
||
<div class="banner">
|
||
<div class="banner-check">✓</div>
|
||
<div class="banner-t"><strong>12 Dokumente hochgeladen.</strong> Jetzt ergänzen, damit sie durchsuchbar werden.</div>
|
||
<span class="banner-cta">Ergänzen →</span>
|
||
<span class="banner-x">×</span>
|
||
</div>
|
||
<span class="frame-caption">After-upload success banner · renders above the list block · auto-dismiss 8s · manual dismiss always available</span>
|
||
</div>
|
||
|
||
<div class="callout green" style="margin-top:16px;">
|
||
<div><strong>No auto-redirect.</strong> After a batch of 12 uploads, dumping the user into <code class="inline">/enrich/{firstId}</code> is disorienting. The banner gives them the moment without taking control away. The list block becomes the persistent landing spot for every return visit.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== SECTION 4 — RESPONSIVE ===== -->
|
||
<div class="sec">
|
||
<div class="sec-label">04 — Responsive behavior</div>
|
||
<h2 class="sec-title">Mobile-first, three breakpoints</h2>
|
||
|
||
<div class="impl-ref">
|
||
<table>
|
||
<thead>
|
||
<tr><th style="width:90px;">Breakpoint</th><th>Row height</th><th>Title size</th><th>Meta visibility</th><th>Padding</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>320px</td>
|
||
<td class="px">72px</td>
|
||
<td class="px">text-base (16px)</td>
|
||
<td>Relative time only</td>
|
||
<td class="px">px-3 py-3</td>
|
||
</tr>
|
||
<tr>
|
||
<td>768px</td>
|
||
<td class="px">72px</td>
|
||
<td class="px">text-lg (18px)</td>
|
||
<td>Time + page count</td>
|
||
<td class="px">px-5 py-4</td>
|
||
</tr>
|
||
<tr>
|
||
<td>1440px</td>
|
||
<td class="px">64px</td>
|
||
<td class="px">text-lg (18px)</td>
|
||
<td>Time + page count</td>
|
||
<td class="px">px-6 py-4</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="callout navy" style="margin-top:16px;">
|
||
<div><strong>Block width follows the main content column.</strong> On the dashboard grid this is <code class="inline">1fr</code> minus the DropZone sidebar — not the full viewport. At 1440px total width, the block lands around 900–960px wide.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== SECTION 5 — IMPL REF ===== -->
|
||
<div class="sec">
|
||
<div class="sec-label">05 — Implementation reference</div>
|
||
<h2 class="sec-title">Exact Tailwind classes & pixel values</h2>
|
||
<p class="sec-sub">Rewire and extend <code class="inline">frontend/src/lib/components/DashboardNeedsMetadata.svelte</code>. The component already exists and is correctly typed — the changes are: new row anatomy, file-type icon, relative time, 5-item cap with footer, a11y landmark, and focus/hover states.</p>
|
||
|
||
<div class="impl-ref">
|
||
<table>
|
||
<thead>
|
||
<tr><th style="width:170px;">Region</th><th>Tailwind</th><th>Pixel values</th><th class="note">Notes</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Section wrapper</td>
|
||
<td><code><section aria-labelledby="enrich-heading" class="mb-6"></code></td>
|
||
<td class="px">margin-bottom 24px</td>
|
||
<td class="note">Landmark for SR nav</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Eyebrow row</td>
|
||
<td><code>flex items-center justify-between mb-3 px-1</code></td>
|
||
<td class="px">mb 12px</td>
|
||
<td class="note">Title + count badge</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Eyebrow heading</td>
|
||
<td><code>text-xs font-bold uppercase tracking-widest text-gray-500</code></td>
|
||
<td class="px">12px / 700</td>
|
||
<td class="note">Not gray-400 — must pass AA on sand bg</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Count badge</td>
|
||
<td><code>bg-brand-navy text-white text-xs font-bold px-2 py-0.5 rounded-full</code></td>
|
||
<td class="px">12px / 700</td>
|
||
<td class="note">Use <code>aria-live="polite"</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>List container</td>
|
||
<td><code>bg-white border border-line rounded-sm shadow-sm overflow-hidden</code></td>
|
||
<td class="px">border 1px, radius 2px</td>
|
||
<td class="note">Matches card pattern</td>
|
||
</tr>
|
||
<tr>
|
||
<td>List element</td>
|
||
<td><code><ol class="divide-y divide-line-2"></code></td>
|
||
<td class="px">divider 1px</td>
|
||
<td class="note">Ordered — upload time is the order</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Row link</td>
|
||
<td><code>group flex items-center gap-3 px-3 py-3 md:px-5 md:py-4 min-h-[72px] lg:min-h-[64px] hover:bg-brand-mint/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:ring-inset transition-colors</code></td>
|
||
<td class="px">min-height 72px mobile / 64px desktop</td>
|
||
<td class="note">Whole row is the <a></td>
|
||
</tr>
|
||
<tr>
|
||
<td>File-type badge</td>
|
||
<td><code>shrink-0 w-5 h-5 md:w-6 md:h-6 rounded-sm flex items-center justify-center text-[10px] font-bold</code></td>
|
||
<td class="px">20×20px mobile / 24×24 desktop</td>
|
||
<td class="note">Color per mime: red/PDF, green/JPG|PNG, purple/TIF</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Title</td>
|
||
<td><code>font-serif text-base md:text-lg text-ink group-hover:underline truncate</code></td>
|
||
<td class="px">16px mobile / 18px desktop</td>
|
||
<td class="note">Single-line truncate</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Meta line</td>
|
||
<td><code>font-sans text-xs text-ink-2 mt-0.5</code></td>
|
||
<td class="px">12px</td>
|
||
<td class="note">"vor N Min./Std." + optional "· N Seiten"</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Chevron</td>
|
||
<td><code>shrink-0 w-5 h-5 opacity-30 group-hover:opacity-70 transition-opacity</code></td>
|
||
<td class="px">20×20px</td>
|
||
<td class="note">Use <code>aria-hidden="true"</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Footer (when >5)</td>
|
||
<td><code>border-t border-line bg-brand-sand/20 px-5 py-3 flex justify-end</code></td>
|
||
<td class="px">py 12px</td>
|
||
<td class="note">Only render when <code>length > 5</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Footer link</td>
|
||
<td><code>font-sans text-xs font-bold uppercase tracking-widest text-brand-navy hover:underline</code></td>
|
||
<td class="px">12px / 700</td>
|
||
<td class="note">"Alle {n} anzeigen →"</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== SECTION 6 — ACCESSIBILITY ===== -->
|
||
<div class="sec">
|
||
<div class="sec-label">06 — Accessibility (WCAG 2.2 AA)</div>
|
||
<h2 class="sec-title">What axe-playwright must confirm</h2>
|
||
|
||
<div class="a11y-box">
|
||
<h4>A11y contract</h4>
|
||
<ul>
|
||
<li><strong>Landmark:</strong> <code><section aria-labelledby="enrich-heading"></code> with an <code><h2 id="enrich-heading"></code>. SR users reach the block via landmark nav.</li>
|
||
<li><strong>Ordered list:</strong> <code><ol></code>, not <code><div></code>. Upload time <em>is</em> the implicit order.</li>
|
||
<li><strong>Count live region:</strong> count badge gets <code>aria-live="polite"</code> so SR announces "12 Dokumente benötigen Metadaten" when the number changes after upload.</li>
|
||
<li><strong>Touch target:</strong> 72px row height on mobile ≫ WCAG 2.2 floor of 48px. The whole row is tappable, not just the title.</li>
|
||
<li><strong>Focus visibility:</strong> <code>focus-visible:ring-2 ring-brand-navy ring-inset</code>. Outer ring would clip; inner ring is always visible against hover/active bg.</li>
|
||
<li><strong>File-type cue:</strong> color <em>plus</em> text ("PDF", "JPG") — passes 1.4.1 (not color alone).</li>
|
||
<li><strong>Contrast:</strong> title <code>text-ink</code> on white = 14.5:1 (AAA). Meta <code>text-ink-2</code> on white — must verify ≥4.5:1 in <em>both</em> light and dark mode.</li>
|
||
<li><strong>Reduced motion:</strong> skeleton pulse and hover transitions respect <code>prefers-reduced-motion</code> (transition-duration 0.01ms).</li>
|
||
<li><strong>Banner:</strong> <code>role="status" aria-live="polite"</code>; manual dismiss button labeled <code>aria-label="Benachrichtigung schließen"</code>.</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== SECTION 7 — DARK MODE ===== -->
|
||
<div class="sec">
|
||
<div class="sec-label">07 — Dark mode</div>
|
||
<h2 class="sec-title">Works via semantic tokens, verify contrast separately</h2>
|
||
<p class="sec-sub">All colors come from existing tokens (<code class="inline">bg-white</code>, <code class="inline">text-ink</code>, <code class="inline">text-ink-2</code>, <code class="inline">border-line</code>, <code class="inline">bg-surface</code>). No hard-coded hex values. Dark mode inherits the remapped tokens.</p>
|
||
|
||
<div class="callout orange">
|
||
<div><strong>Verify in dark mode:</strong> the file-type badges use mime-specific colors (red/green/purple) against white—in dark mode those same colors sit on a near-black background. Test each badge at its dark-mode contrast ratio; some may need lightness adjustment (e.g. <code class="inline">dark:text-red-300</code> instead of <code class="inline">text-red-600</code>). Run axe-playwright in both themes per project convention.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== SECTION 8 — OUT OF SCOPE ===== -->
|
||
<div class="sec">
|
||
<div class="sec-label">08 — Explicitly out of scope</div>
|
||
<h2 class="sec-title">What this spec does NOT cover</h2>
|
||
<div class="callout navy">
|
||
<div>
|
||
<strong>Future work (separate specs):</strong>
|
||
<ul style="margin-top:8px;padding-left:20px;">
|
||
<li>List-block treatment for the <em>other</em> pipeline stages (segment, transcribe, review). If this pattern works, they likely graduate out of MissionControlStrip too.</li>
|
||
<li>Bulk-enrich ("same sender for all", "apply these 3 tags to all") — a natural follow-up for batch-upload reality.</li>
|
||
<li>Sort/filter on <code class="inline">/enrich</code> (by upload date, by file type, by uploader).</li>
|
||
<li>Strip redesign once a 4th+ pipeline stage forces the issue.</li>
|
||
<li>Thumbnails in rows (deferred — depends on backend preview generation).</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== TEST PLAN ===== -->
|
||
<div class="sec">
|
||
<div class="sec-label">09 — Test plan</div>
|
||
<h2 class="sec-title">What QA (Sara) will verify</h2>
|
||
<div class="impl-ref">
|
||
<table>
|
||
<thead><tr><th style="width:170px;">Test</th><th>What it verifies</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Vitest: component renders</td><td>With 0 docs, renders nothing. With 1–5, no footer. With 6+, footer link shows "Alle {n} anzeigen".</td></tr>
|
||
<tr><td>Vitest: count badge</td><td>Badge reflects <code>incompleteDocs.length</code>, not capped list length.</td></tr>
|
||
<tr><td>Playwright: axe, light mode</td><td>Dashboard passes a11y with block populated. Focus ring visible on keyboard nav through rows.</td></tr>
|
||
<tr><td>Playwright: axe, dark mode</td><td>Same as above with <code>[data-theme="dark"]</code>. File-type badge contrast ratios verified.</td></tr>
|
||
<tr><td>Playwright: 320/768/1440 screenshots</td><td>Block renders at all three breakpoints without overflow or truncation beyond title.</td></tr>
|
||
<tr><td>Playwright: upload → banner → click CTA</td><td>After DropZone fires with 3+ files: banner appears, list block populates, CTA scrolls/focuses list. Banner auto-dismisses at 8s. Manual X dismisses immediately.</td></tr>
|
||
<tr><td>Playwright: keyboard flow</td><td>Tab from DropZone reaches banner (if present), then list rows in order, then footer link. Enter on row navigates to <code>/enrich/{id}</code>.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<p style="font-family:var(--mono);font-size:10px;color:var(--muted);text-align:center;">
|
||
Leonie Voss · UX Lead · Familienarchiv · 2026-04-20
|
||
</p>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|