Exploration spec (sort-integration-spec.html) covers 4 placement variants with comparison matrix. Final spec (sort-inline-final-spec.html) locks in Variant A (inline sort in search bar row) with full desktop/mobile states, dropdown interaction anatomy, loading/empty states, and backend wiring checklist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1053 lines
57 KiB
HTML
1053 lines
57 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Sort — Inline Variant A · Final Design Spec · Familienarchiv #180</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:1400px;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:640px;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}
|
||
.mb-final{background:#A6DAD8;color:#002850}
|
||
.decisions{display:grid;grid-template-columns:repeat(4,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}
|
||
|
||
/* ── 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}
|
||
|
||
/* ── Grid ─── */
|
||
.sg{display:grid;gap:20px;align-items:start}
|
||
.sg-2{grid-template-columns:1fr 1fr}
|
||
.sg-3{grid-template-columns:1fr 1fr 1fr}
|
||
.sg-4{grid-template-columns:1fr 1fr 1fr 1fr}
|
||
.sg-mob{grid-template-columns:1fr 220px}
|
||
.sb{display:flex;flex-direction:column}
|
||
.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}
|
||
.sc{font-size:10px;color:#888;margin-top:8px;font-style:italic;line-height:1.5}
|
||
|
||
/* ── Browser chrome ─── */
|
||
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:visible;box-shadow:0 4px 18px rgba(0,0,0,.08);max-width:860px}
|
||
.wf-inner{border-radius:10px;overflow:hidden}
|
||
.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%;background:#C8C4BE}
|
||
.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}
|
||
.N{height:40px;background:#0D2240;display:flex;align-items:center;padding:0 16px;gap:12px;flex-shrink:0}
|
||
.logo{font-size:9px;font-weight:900;color:#fff;letter-spacing:1px}
|
||
.nl{font-size:7.5px;color:rgba(255,255,255,.4);font-weight:700;text-transform:uppercase;letter-spacing:.5px}
|
||
.nl.on{color:#fff;border-bottom:2px solid #A6DAD8;padding-bottom:2px}
|
||
.nr{margin-left:auto;display:flex;gap:7px;align-items:center}
|
||
.nico{width:20px;height:20px;background:rgba(255,255,255,.1);border-radius:4px}
|
||
.MAIN{padding:14px 18px;background:#ECEAE4}
|
||
|
||
/* ── Search card ─── */
|
||
.SCARD{background:#fff;border:1.5px solid #E0DDD6;border-radius:3px;padding:11px 14px;box-shadow:0 1px 3px rgba(0,0,0,.06);margin-bottom:10px}
|
||
.SROW1{display:flex;align-items:center;gap:6px}
|
||
.SINPUT{flex:1;height:24px;border:1.5px solid #D1D5DB;border-radius:2px;display:flex;align-items:center;padding:0 8px;font-size:8.5px;color:#9CA3AF;font-style:italic;gap:5px}
|
||
.SINPUT.has-value{color:#1A1A1A;font-style:normal}
|
||
.SINPUT.focused{border-color:#002850;box-shadow:0 0 0 2px rgba(0,40,80,.07)}
|
||
.SICON{font-size:9px;opacity:.35;flex-shrink:0}
|
||
.SPINNER{width:9px;height:9px;border:1.5px solid #E0DDD6;border-top-color:#002850;border-radius:50%;flex-shrink:0;animation:spin .7s linear infinite}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
|
||
/* ── Sort trigger button ─── */
|
||
.SBTN-SORT{height:24px;border:1.5px solid #D1D5DB;border-radius:2px;display:flex;align-items:center;padding:0 8px;font-size:8px;font-weight:700;color:#666;gap:4px;cursor:pointer;white-space:nowrap;background:#F7F5F2;flex-shrink:0}
|
||
.SBTN-SORT .sort-prefix{font-size:7px;font-weight:400;color:#AAA}
|
||
.SBTN-SORT .sort-chev{font-size:7px;opacity:.5;margin-left:1px}
|
||
/* States */
|
||
.SBTN-SORT.hover{background:#ECEAE4;color:#444}
|
||
.SBTN-SORT.active{border-color:#002850;color:#002850;background:#fff}
|
||
.SBTN-SORT.active .sort-prefix{color:rgba(0,40,80,.5)}
|
||
.SBTN-SORT.active .sort-chev{opacity:.8}
|
||
.SBTN-SORT.focus{border-color:#002850;box-shadow:0 0 0 2px rgba(0,40,80,.12)}
|
||
|
||
/* ── Filter + Reset buttons ─── */
|
||
.SBTN-FILTER{height:24px;border:1.5px solid #D1D5DB;border-radius:2px;display:flex;align-items:center;padding:0 9px;font-size:8px;font-weight:700;color:#555;gap:4px;cursor:pointer;white-space:nowrap;background:#F7F5F2;flex-shrink:0}
|
||
.SBTN-FILTER.open{border-color:#002850;color:#002850;background:#fff}
|
||
.SBTN-RESET{height:24px;border:1.5px solid transparent;border-radius:2px;display:flex;align-items:center;justify-content:center;padding:0 6px;font-size:9px;color:#9CA3AF;cursor:pointer;flex-shrink:0}
|
||
.SBTN-RESET.hover{color:#DC2626}
|
||
|
||
/* ── Sort dropdown ─── */
|
||
.SDROP-WRAP{position:relative;flex-shrink:0}
|
||
.SDROP{position:absolute;top:calc(100% + 2px);left:0;min-width:175px;background:#fff;border:1.5px solid #002850;border-radius:3px;box-shadow:0 6px 16px rgba(0,0,0,.12);z-index:50}
|
||
.SDROP-HDR{font-size:6.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#AAA;padding:5px 9px 3px;border-bottom:1px solid #F0EDE8}
|
||
.SDROP-ROW{display:flex;align-items:center;padding:5px 9px;border-bottom:1px solid #F0EDE8;cursor:pointer;gap:6px;min-height:24px}
|
||
.SDROP-ROW:last-child{border-bottom:none}
|
||
.SDROP-ROW.hover{background:#F7F5F2}
|
||
.SDROP-ROW.active{background:#EFF6FF}
|
||
.SDROP-LABEL{flex:1;font-size:8px;font-weight:600;color:#1A1A1A}
|
||
.SDROP-ROW.active .SDROP-LABEL{color:#002850;font-weight:700}
|
||
.SDROP-DIRS{display:flex;gap:2px;flex-shrink:0}
|
||
.SDIR{font-size:7px;font-weight:800;padding:1px 5px;border-radius:2px;border:1px solid #E0DDD6;color:#AAA;cursor:pointer}
|
||
.SDIR.hover{border-color:#888;color:#555}
|
||
.SDIR.active{background:#002850;color:#fff;border-color:#002850}
|
||
|
||
/* ── Result header ─── */
|
||
.RESHEAD{display:flex;align-items:center;gap:6px;padding:0 2px;margin-bottom:8px}
|
||
.RESCOUNT{font-size:8px;font-weight:600;color:#666;flex:1}
|
||
.RESCOUNT strong{color:#002850;font-weight:800}
|
||
.RESLIVE{font-size:7.5px;color:#AAA;font-style:italic}
|
||
|
||
/* ── Document list ─── */
|
||
.DOCROW{background:#fff;border:1.5px solid #E0DDD6;border-radius:3px;padding:8px 12px;margin-bottom:5px;display:flex;align-items:flex-start;gap:8px;cursor:pointer}
|
||
.DOCROW:hover{border-color:#A6DAD8}
|
||
.THUMB{width:22px;height:28px;background:#F0EDE8;border:1px solid #E0DDD6;border-radius:2px;display:flex;align-items:center;justify-content:center;font-size:6.5px;color:#AAA;flex-shrink:0}
|
||
.DOCBODY{flex:1;min-width:0}
|
||
.DOCTITLE{font-size:8.5px;font-weight:700;color:#0D2240;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.DOCMETA{font-size:7.5px;color:#888;display:flex;align-items:center;gap:5px;flex-wrap:wrap}
|
||
.METASEP{color:#D1CCC8}
|
||
.DOCTAG{background:#F0EDE8;border-radius:20px;padding:1px 6px;font-size:6.5px;font-weight:700;color:#555}
|
||
.PH{height:6px;background:#E8E4DF;border-radius:2px}
|
||
.w90{width:90%}.w80{width:80%}.w70{width:70%}.w60{width:60%}.w50{width:50%}.w40{width:40%}.w30{width:30%}
|
||
|
||
/* ── State pill labels ─── */
|
||
.state-label{display:inline-block;font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;border-radius:3px;padding:1px 5px;margin-bottom:5px}
|
||
.st-default{background:#E8E4DF;color:#555}
|
||
.st-hover{background:#DBEAFE;color:#1D4ED8}
|
||
.st-focus{background:#EDE9FE;color:#6D28D9}
|
||
.st-active{background:#DCFCE7;color:#166534}
|
||
.st-loading{background:#FEF9C3;color:#854D0E}
|
||
.st-open{background:#FEE2E2;color:#991B1B}
|
||
|
||
/* ── Mobile chrome ─── */
|
||
.WF-M{background:#fff;border:2px solid #B8B4AE;border-radius:16px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.08);width:215px}
|
||
.WF-M-STATUS{height:16px;background:#0D2240;display:flex;align-items:center;justify-content:space-between;padding:0 10px}
|
||
.WF-M-TIME{font-size:6px;color:#fff;font-weight:700}
|
||
.WF-M-ICONS{display:flex;gap:3px}
|
||
.WF-M-ICON{width:5px;height:5px;background:rgba(255,255,255,.5);border-radius:1px}
|
||
.N-M{height:34px;background:#0D2240;display:flex;align-items:center;padding:0 10px;justify-content:space-between}
|
||
.MAIN-SM{padding:8px 10px;background:#ECEAE4;display:flex;flex-direction:column;gap:7px}
|
||
.SCARD-SM{background:#fff;border:1.5px solid #E0DDD6;border-radius:3px;padding:8px 10px}
|
||
.SROW1-SM{display:flex;gap:4px;margin-bottom:5px}
|
||
.SROW2-SM{display:flex;gap:4px}
|
||
.SBTN-SORT-SM{height:20px;border:1.5px solid #D1D5DB;border-radius:2px;display:flex;align-items:center;justify-content:center;padding:0 8px;font-size:7.5px;font-weight:700;color:#666;gap:3px;cursor:pointer;flex:1;white-space:nowrap;background:#F7F5F2}
|
||
.SBTN-SORT-SM.active{border-color:#002850;color:#002850;background:#fff}
|
||
.SBTN-FILTER-SM{height:20px;border:1.5px solid #D1D5DB;border-radius:2px;display:flex;align-items:center;justify-content:center;padding:0 8px;font-size:7.5px;font-weight:700;color:#555;gap:3px;cursor:pointer;flex:1;white-space:nowrap;background:#F7F5F2}
|
||
.SBTN-RESET-SM{height:20px;border:1.5px solid transparent;display:flex;align-items:center;padding:0 5px;font-size:9px;color:#9CA3AF;flex-shrink:0}
|
||
|
||
/* ── Annotation callouts ─── */
|
||
.ann-block{background:#FFF7ED;border:1px solid #FDBA74;border-radius:5px;padding:10px 14px;font-size:10.5px;color:#7C2D12;line-height:1.6;margin-top:14px}
|
||
.ann-block strong{font-weight:800}
|
||
.ann-block ul{padding-left:16px;display:flex;flex-direction:column;gap:3px;margin-top:6px}
|
||
.ann-info{background:#EFF6FF;border:1px solid #BFDBFE;border-radius:5px;padding:10px 14px;font-size:10.5px;color:#1E3A5F;line-height:1.6;margin-top:14px}
|
||
.ann-info strong{font-weight:800}
|
||
|
||
/* ── 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}
|
||
|
||
/* ── impl-ref ─── */
|
||
.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:210px}
|
||
.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>Sort — Inline Variant · Final Design Spec</h1>
|
||
<p>Sort dropdown sits inline in the search bar row, between the text input and the Filter button. Always visible, always labelled. Selected from 4 exploration variants (see <code style="font-size:10px;color:#A6DAD8">sort-integration-spec.html</code>). Addresses Issue #180 Problem 1.</p>
|
||
</div>
|
||
<span class="mast-badge mb-final">Final · Ready for implementation</span>
|
||
</div>
|
||
<div class="decisions">
|
||
<div class="dec">
|
||
<div class="dec-label">Issue</div>
|
||
<div class="dec-value">#180 — Sort & Search UX</div>
|
||
</div>
|
||
<div class="dec">
|
||
<div class="dec-label">Sort options (6)</div>
|
||
<div class="dec-value">Datum · Titel · Absender · Empfänger · Tag · Hochgeladen</div>
|
||
</div>
|
||
<div class="dec">
|
||
<div class="dec-label">Default</div>
|
||
<div class="dec-value">Datum ↓ (newest first) — no URL param needed for default</div>
|
||
</div>
|
||
<div class="dec">
|
||
<div class="dec-label">New URL params</div>
|
||
<div class="dec-value"><code style="font-size:8px;color:#A6DAD8">?sort=date&dir=desc</code> — omitted when default</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 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 — ANATOMY AT REST (DESKTOP)
|
||
══════════════════════════════════════ -->
|
||
<div class="sec">
|
||
<div class="sec-h">
|
||
<span class="sec-num">1</span>
|
||
Anatomy at rest — desktop
|
||
</div>
|
||
|
||
<div class="sg sg-2" style="gap:24px;align-items:start">
|
||
|
||
<!-- State: default (q empty, default sort) -->
|
||
<div class="sb">
|
||
<span class="state-label st-default">Default — no active search, default sort</span>
|
||
<div class="wf">
|
||
<div class="wf-inner">
|
||
<div class="wf-bar">
|
||
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||
<div class="urlbar"><span>localhost:3000/</span></div>
|
||
</div>
|
||
<div class="N">
|
||
<span class="logo">FAMILIENARCHIV</span>
|
||
<span class="nl on">Dokumente</span>
|
||
<span class="nl">Personen</span>
|
||
<div class="nr"><div class="nico"></div></div>
|
||
</div>
|
||
<div class="MAIN">
|
||
<div class="SCARD">
|
||
<div class="SROW1">
|
||
<div class="SINPUT">
|
||
<span class="SICON">⌕</span>
|
||
<span style="font-size:8px;color:#9CA3AF;font-style:italic">Titel, Personen, Tags durchsuchen …</span>
|
||
</div>
|
||
<!-- Sort button: default state, label shows default -->
|
||
<div class="SBTN-SORT">
|
||
<span class="sort-prefix">Sortieren:</span>
|
||
Datum ↓
|
||
<span class="sort-chev">▾</span>
|
||
</div>
|
||
<div class="SBTN-FILTER">⊞ Filter <span style="font-size:7px;opacity:.5">▾</span></div>
|
||
<div class="SBTN-RESET">✕</div>
|
||
</div>
|
||
</div>
|
||
<!-- Dashboard content placeholder -->
|
||
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:3px;padding:14px;font-size:8px;color:#AAA;text-align:center">Dashboard widgets appear here when no search is active</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">On the dashboard (no active search) the sort button is still visible and operable. Sorting here has no effect until a search is triggered — the button communicates the "ready" state.</div>
|
||
</div>
|
||
|
||
<!-- State: active search, non-default sort -->
|
||
<div class="sb">
|
||
<span class="state-label st-active">Active — search running, custom sort set</span>
|
||
<div class="wf">
|
||
<div class="wf-inner">
|
||
<div class="wf-bar">
|
||
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||
<div class="urlbar"><span>localhost:3000/?q=raddatz&sort=sender&dir=asc</span></div>
|
||
</div>
|
||
<div class="N">
|
||
<span class="logo">FAMILIENARCHIV</span>
|
||
<span class="nl on">Dokumente</span>
|
||
<span class="nl">Personen</span>
|
||
<div class="nr"><div class="nico"></div></div>
|
||
</div>
|
||
<div class="MAIN">
|
||
<div class="SCARD">
|
||
<div class="SROW1">
|
||
<div class="SINPUT has-value">
|
||
<span class="SICON">⌕</span>
|
||
<span style="font-size:8px">raddatz</span>
|
||
</div>
|
||
<!-- Sort button: active state (non-default sort) -->
|
||
<div class="SBTN-SORT active">
|
||
<span class="sort-prefix">Sortieren:</span>
|
||
Absender ↑
|
||
<span class="sort-chev">▾</span>
|
||
</div>
|
||
<div class="SBTN-FILTER">⊞ Filter <span style="font-size:7px;opacity:.5">▾</span></div>
|
||
<div class="SBTN-RESET">✕</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="RESHEAD">
|
||
<span class="RESCOUNT"><strong>8 Dokumente</strong> gefunden</span>
|
||
</div>
|
||
|
||
<div class="DOCROW">
|
||
<div class="THUMB">PDF</div>
|
||
<div class="DOCBODY">
|
||
<div class="DOCTITLE">Brief an Tante Klara, Weihnachten 1954</div>
|
||
<div class="DOCMETA"><span>Ernst Raddatz</span><span class="METASEP">·</span><span>15. Dez 1954</span><span class="METASEP">·</span><span class="DOCTAG">Briefe</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="DOCROW">
|
||
<div class="THUMB">PDF</div>
|
||
<div class="DOCBODY">
|
||
<div class="DOCTITLE">Urlaubspostkarte Schwarzwald 1962</div>
|
||
<div class="DOCMETA"><span>Hildegard Raddatz</span><span class="METASEP">·</span><span>8. Aug 1962</span><span class="METASEP">·</span><span class="DOCTAG">Karten</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">Active sort changes the button border and text color to navy. The label updates to the selected field + direction arrow. Reset button (✕) clears both search and sort, returning to the dashboard.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Implementation Reference — §1 Search bar anatomy
|
||
<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>Search card container</td>
|
||
<td><code>bg-surface border border-line rounded-sm p-4 shadow-sm mb-2</code></td>
|
||
<td><span class="ir-px">p 16px</span></td>
|
||
<td>Existing <code>SearchFilterBar.svelte</code> outer wrapper — no change needed</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Row 1 flex container</td>
|
||
<td><code>flex items-center gap-3</code></td>
|
||
<td>—</td>
|
||
<td>Existing — insert sort button between <code>SINPUT</code> and filter button</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Search input</td>
|
||
<td><code>flex-1 h-10 flex items-center gap-2 px-3 border border-line rounded-sm text-base placeholder-ink-3 focus-visible:ring-2 focus-visible:ring-focus-ring focus:outline-none</code></td>
|
||
<td><span class="ir-px">h 40px, text 16px</span></td>
|
||
<td>Placeholder: <em>"Titel, Personen, Tags durchsuchen …"</em>. Addresses #180 Problem 2 discoverability.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Sort trigger button — default</td>
|
||
<td><code>h-10 shrink-0 flex items-center gap-1.5 px-3 border border-line rounded-sm bg-muted text-sm font-bold text-ink-2 hover:bg-surface hover:text-ink transition whitespace-nowrap cursor-pointer</code></td>
|
||
<td><span class="ir-px">h 40px, px 12px, text 14px / 700</span></td>
|
||
<td>Shows "Sortieren: Datum ↓". "Sortieren:" prefix: <code>text-xs font-normal text-ink-3 mr-0.5</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Sort trigger button — active</td>
|
||
<td>Same + override: <code>border-brand-navy text-brand-navy bg-surface</code></td>
|
||
<td>—</td>
|
||
<td>Active when sort ≠ default (date/desc). "Sortieren:" prefix color: <code>text-brand-navy/50</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Sort trigger button — focus</td>
|
||
<td>Add: <code>focus-visible:ring-2 focus-visible:ring-focus-ring focus:outline-none</code></td>
|
||
<td>—</td>
|
||
<td>Never remove focus ring. Tab order: input → sort → filter → reset</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Filter button (unchanged)</td>
|
||
<td><code>h-10 shrink-0 flex items-center gap-2 px-3 border border-line rounded-sm bg-muted text-sm font-bold text-ink-2 uppercase tracking-wide hover:text-ink transition</code></td>
|
||
<td><span class="ir-px">h 40px</span></td>
|
||
<td>Existing — no change</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Reset button (unchanged)</td>
|
||
<td><code>h-10 shrink-0 flex items-center justify-center px-2 border border-transparent text-ink-3 hover:text-red-500 transition</code></td>
|
||
<td><span class="ir-px">h 40px, min-w 40px</span></td>
|
||
<td>Existing — no change</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Result count line</td>
|
||
<td><code>text-sm font-medium text-ink-2 mb-3</code> — <code><strong class="text-brand-navy font-bold"></code></td>
|
||
<td><span class="ir-px">14px / 500</span></td>
|
||
<td><code>aria-live="polite"</code>. Only rendered when <code>!isDashboard</code>.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- ══════════════════════════════════════
|
||
SECTION 2 — DROPDOWN OPEN STATES
|
||
══════════════════════════════════════ -->
|
||
<div class="sec">
|
||
<div class="sec-h">
|
||
<span class="sec-num">2</span>
|
||
Sort dropdown — open states & interaction anatomy
|
||
</div>
|
||
|
||
<div class="sg sg-3" style="gap:20px;align-items:start">
|
||
|
||
<!-- Dropdown: default sort active -->
|
||
<div class="sb">
|
||
<span class="state-label st-default">Open — default sort (Datum ↓)</span>
|
||
<div style="background:#ECEAE4;padding:10px;border-radius:6px">
|
||
<!-- Trigger -->
|
||
<div class="SBTN-SORT active" style="display:inline-flex;margin-bottom:2px">
|
||
<span class="sort-prefix">Sortieren:</span>
|
||
Datum ↓
|
||
<span class="sort-chev" style="transform:rotate(180deg);display:inline-block">▾</span>
|
||
</div>
|
||
<!-- Dropdown -->
|
||
<div class="SDROP" style="position:static;width:175px">
|
||
<div class="SDROP-HDR">Sortieren nach</div>
|
||
<div class="SDROP-ROW active">
|
||
<span class="SDROP-LABEL">Datum</span>
|
||
<div class="SDROP-DIRS">
|
||
<span class="SDIR">↑</span>
|
||
<span class="SDIR active">↓</span>
|
||
</div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Titel</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Absender</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Empfänger</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Tag</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Hochgeladen</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">Active option row has light blue tint. The currently selected direction arrow is navy-filled. Inactive rows show both ↑ ↓ with no fill.</div>
|
||
</div>
|
||
|
||
<!-- Dropdown: hovering a non-active row -->
|
||
<div class="sb">
|
||
<span class="state-label st-hover">Hover — "Absender" row, ↑ direction hovered</span>
|
||
<div style="background:#ECEAE4;padding:10px;border-radius:6px">
|
||
<div class="SBTN-SORT active" style="display:inline-flex;margin-bottom:2px">
|
||
<span class="sort-prefix">Sortieren:</span>
|
||
Datum ↓
|
||
<span class="sort-chev" style="transform:rotate(180deg);display:inline-block">▾</span>
|
||
</div>
|
||
<div class="SDROP" style="position:static;width:175px">
|
||
<div class="SDROP-HDR">Sortieren nach</div>
|
||
<div class="SDROP-ROW active">
|
||
<span class="SDROP-LABEL">Datum</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR active">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Titel</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<!-- Hovered row -->
|
||
<div class="SDROP-ROW hover">
|
||
<span class="SDROP-LABEL" style="color:#002850">Absender</span>
|
||
<div class="SDROP-DIRS">
|
||
<span class="SDIR hover">↑</span>
|
||
<span class="SDIR">↓</span>
|
||
</div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Empfänger</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Tag</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Hochgeladen</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">Hovering a row highlights the whole row in muted gray. Hovering a specific direction arrow highlights that arrow with a border. Clicking either closes the dropdown and triggers a search.</div>
|
||
</div>
|
||
|
||
<!-- Dropdown: new sort selected (Absender ↑) -->
|
||
<div class="sb">
|
||
<span class="state-label st-active">After selection — "Absender ↑" now active</span>
|
||
<div style="background:#ECEAE4;padding:10px;border-radius:6px">
|
||
<div class="SBTN-SORT active" style="display:inline-flex;margin-bottom:2px">
|
||
<span class="sort-prefix">Sortieren:</span>
|
||
Absender ↑
|
||
<span class="sort-chev">▾</span>
|
||
</div>
|
||
<div class="SDROP" style="position:static;width:175px">
|
||
<div class="SDROP-HDR">Sortieren nach</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Datum</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Titel</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW active">
|
||
<span class="SDROP-LABEL">Absender</span>
|
||
<div class="SDROP-DIRS">
|
||
<span class="SDIR active">↑</span>
|
||
<span class="SDIR">↓</span>
|
||
</div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Empfänger</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Tag</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW">
|
||
<span class="SDROP-LABEL">Hochgeladen</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">After selection: trigger button label updates instantly. Dropdown typically closes after a direction is chosen. The user can re-open to change direction without re-selecting the field.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ann-info" style="margin-top:0">
|
||
<strong>Interaction model:</strong> Clicking a <em>field label</em> selects that field and retains the last-used direction for that field (defaults to ↓ if no prior selection). Clicking a <em>direction arrow</em> selects that field + direction and closes the dropdown. This means two distinct affordances — quick tap on a label re-sorts with same direction, precise tap on an arrow picks direction explicitly.
|
||
</div>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Implementation Reference — §2 Sort dropdown
|
||
<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>Dropdown container</td>
|
||
<td><code>absolute top-[calc(100%+4px)] left-0 z-50 min-w-[200px] bg-surface border border-brand-navy rounded-sm shadow-[0_6px_20px_rgba(0,0,0,.12)]</code></td>
|
||
<td><span class="ir-px">min-w 200px</span></td>
|
||
<td>Use Svelte's <code>use:clickOutside</code> action to close. Also close on <kbd>Escape</kbd>. Position: <code>position: relative</code> on <code>.SDROP-WRAP</code>.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Dropdown header</td>
|
||
<td><code>text-xs font-bold uppercase tracking-widest text-ink-3 px-3 py-2 border-b border-line</code></td>
|
||
<td><span class="ir-px">12px / 700</span></td>
|
||
<td>"SORTIEREN NACH" — orientation label for screen readers + seniors</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Option row — default</td>
|
||
<td><code>flex items-center px-3 py-2.5 gap-4 border-b border-line last:border-b-0 cursor-pointer hover:bg-muted</code></td>
|
||
<td><span class="ir-px">h ~44px, py 10px</span></td>
|
||
<td>Touch target exactly 44px. Most commonly undersized element — do not reduce py.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Option row — active</td>
|
||
<td>Same + <code>bg-blue-50</code></td>
|
||
<td>—</td>
|
||
<td><code>aria-selected="true"</code> on the active row. Role: <code>role="option"</code> on each row, <code>role="listbox"</code> on dropdown.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Option label</td>
|
||
<td><code>flex-1 text-sm font-semibold text-ink</code></td>
|
||
<td><span class="ir-px">14px / 600</span></td>
|
||
<td>Active row: <code>text-brand-navy font-bold</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Direction toggle group</td>
|
||
<td><code>flex gap-1 shrink-0</code></td>
|
||
<td>—</td>
|
||
<td><code>role="group"</code> with <code>aria-label="Richtung"</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Direction button — inactive</td>
|
||
<td><code>text-xs font-bold px-2 py-1 rounded-sm border border-line text-ink-3 hover:border-ink-2 hover:text-ink-2 transition min-w-[28px] text-center</code></td>
|
||
<td><span class="ir-px">28px min-width, 24px h</span></td>
|
||
<td>Labels: "↑" + <code>aria-label="Aufsteigend"</code> / "↓" + <code>aria-label="Absteigend"</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Direction button — active</td>
|
||
<td>Same + <code>bg-brand-navy text-white border-brand-navy</code></td>
|
||
<td>—</td>
|
||
<td><code>aria-pressed="true"</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Keyboard navigation</td>
|
||
<td>—</td>
|
||
<td>—</td>
|
||
<td><kbd>↑</kbd>/<kbd>↓</kbd> to move between rows. <kbd>Enter</kbd> / <kbd>Space</kbd> to select. <kbd>Escape</kbd> to close without change. <kbd>Tab</kbd> moves through direction arrows within a row. Focus returns to trigger on close.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Svelte state</td>
|
||
<td>—</td>
|
||
<td>—</td>
|
||
<td><code>let sort = $state('date')</code>, <code>let dir = $state('desc')</code>. Add both to <code>triggerSearch()</code> URL params. Bind to new <code>sort</code> and <code>dir</code> props on <code>SearchFilterBar</code>.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- ══════════════════════════════════════
|
||
SECTION 3 — MOBILE LAYOUT (320px)
|
||
══════════════════════════════════════ -->
|
||
<div class="sec">
|
||
<div class="sec-h">
|
||
<span class="sec-num">3</span>
|
||
Mobile layout — 320px breakpoint
|
||
</div>
|
||
|
||
<div class="sg sg-3" style="gap:20px;align-items:start">
|
||
|
||
<!-- Mobile: default state -->
|
||
<div class="sb">
|
||
<span class="state-label st-default">Default — no search</span>
|
||
<div class="WF-M">
|
||
<div class="WF-M-STATUS">
|
||
<span class="WF-M-TIME">9:41</span>
|
||
<div class="WF-M-ICONS"><div class="WF-M-ICON"></div><div class="WF-M-ICON"></div><div class="WF-M-ICON"></div></div>
|
||
</div>
|
||
<div class="N-M"><span class="logo" style="font-size:7px">FAMILIENARCHIV</span><div class="nico"></div></div>
|
||
<div class="MAIN-SM">
|
||
<div class="SCARD-SM">
|
||
<!-- Row 1: input + reset -->
|
||
<div class="SROW1-SM">
|
||
<div class="SINPUT" style="height:22px;flex:1">
|
||
<span class="SICON">⌕</span>
|
||
<span style="font-size:7.5px;color:#9CA3AF;font-style:italic">Durchsuchen…</span>
|
||
</div>
|
||
<div class="SBTN-RESET-SM">✕</div>
|
||
</div>
|
||
<!-- Row 2: sort + filter equal-width -->
|
||
<div class="SROW2-SM">
|
||
<div class="SBTN-SORT-SM">Datum ↓ <span style="font-size:7px;opacity:.5">▾</span></div>
|
||
<div class="SBTN-FILTER-SM">⊞ Filter</div>
|
||
</div>
|
||
</div>
|
||
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:3px;padding:10px;font-size:7px;color:#AAA;text-align:center">Dashboard</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">Row 1: input + reset only. Row 2: sort and filter as equal-width buttons, full-width of the card.</div>
|
||
</div>
|
||
|
||
<!-- Mobile: active search + custom sort -->
|
||
<div class="sb">
|
||
<span class="state-label st-active">Active — custom sort set</span>
|
||
<div class="WF-M">
|
||
<div class="WF-M-STATUS">
|
||
<span class="WF-M-TIME">9:41</span>
|
||
<div class="WF-M-ICONS"><div class="WF-M-ICON"></div><div class="WF-M-ICON"></div><div class="WF-M-ICON"></div></div>
|
||
</div>
|
||
<div class="N-M"><span class="logo" style="font-size:7px">FAMILIENARCHIV</span><div class="nico"></div></div>
|
||
<div class="MAIN-SM">
|
||
<div class="SCARD-SM">
|
||
<div class="SROW1-SM">
|
||
<div class="SINPUT has-value" style="height:22px;flex:1">
|
||
<span class="SICON">⌕</span>
|
||
<span style="font-size:7.5px">raddatz</span>
|
||
</div>
|
||
<div class="SBTN-RESET-SM">✕</div>
|
||
</div>
|
||
<div class="SROW2-SM">
|
||
<!-- Active sort button on mobile -->
|
||
<div class="SBTN-SORT-SM active">Absender ↑ <span style="font-size:7px;opacity:.8">▾</span></div>
|
||
<div class="SBTN-FILTER-SM">⊞ Filter</div>
|
||
</div>
|
||
</div>
|
||
<div style="font-size:7px;font-weight:700;color:#555;padding:0 2px">8 Dokumente gefunden</div>
|
||
<div class="DOCROW" style="padding:6px 9px">
|
||
<div class="THUMB" style="width:18px;height:22px">PDF</div>
|
||
<div class="DOCBODY"><div class="PH w80" style="margin-bottom:4px"></div><div class="PH w50"></div></div>
|
||
</div>
|
||
<div class="DOCROW" style="padding:6px 9px">
|
||
<div class="THUMB" style="width:18px;height:22px">PDF</div>
|
||
<div class="DOCBODY"><div class="PH w70" style="margin-bottom:4px"></div><div class="PH w40"></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">Sort button adopts navy border + text when non-default, identical to desktop active state. Button label shortens to field name + direction (no "Sortieren:" prefix).</div>
|
||
</div>
|
||
|
||
<!-- Mobile: sort dropdown open (bottom sheet) -->
|
||
<div class="sb">
|
||
<span class="state-label st-open">Dropdown open — bottom sheet on mobile</span>
|
||
<div class="WF-M" style="position:relative">
|
||
<div class="WF-M-STATUS">
|
||
<span class="WF-M-TIME">9:41</span>
|
||
<div class="WF-M-ICONS"><div class="WF-M-ICON"></div><div class="WF-M-ICON"></div><div class="WF-M-ICON"></div></div>
|
||
</div>
|
||
<div class="N-M"><span class="logo" style="font-size:7px">FAMILIENARCHIV</span><div class="nico"></div></div>
|
||
<div class="MAIN-SM">
|
||
<div class="SCARD-SM">
|
||
<div class="SROW1-SM">
|
||
<div class="SINPUT has-value" style="height:22px;flex:1"><span style="font-size:7.5px">raddatz</span></div>
|
||
<div class="SBTN-RESET-SM">✕</div>
|
||
</div>
|
||
<div class="SROW2-SM">
|
||
<div class="SBTN-SORT-SM active">Absender ↑ <span style="font-size:7px">▲</span></div>
|
||
<div class="SBTN-FILTER-SM">⊞ Filter</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Bottom sheet overlay -->
|
||
<div style="position:absolute;bottom:0;left:0;right:0;background:#fff;border-top:2px solid #002850;border-radius:12px 12px 0 0;box-shadow:0 -4px 20px rgba(0,0,0,.15);z-index:100">
|
||
<!-- Handle -->
|
||
<div style="display:flex;justify-content:center;padding:6px 0 3px">
|
||
<div style="width:24px;height:3px;background:#D1D5DB;border-radius:2px"></div>
|
||
</div>
|
||
<div class="SDROP-HDR" style="padding:4px 10px 4px;font-size:6.5px">Sortieren nach</div>
|
||
<div class="SDROP-ROW" style="padding:6px 10px;min-height:26px">
|
||
<span class="SDROP-LABEL">Datum</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW" style="padding:6px 10px;min-height:26px">
|
||
<span class="SDROP-LABEL">Titel</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW active" style="padding:6px 10px;min-height:26px">
|
||
<span class="SDROP-LABEL">Absender</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR active">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW" style="padding:6px 10px;min-height:26px">
|
||
<span class="SDROP-LABEL">Empfänger</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW" style="padding:6px 10px;min-height:26px">
|
||
<span class="SDROP-LABEL">Tag</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
<div class="SDROP-ROW" style="padding:6px 10px;border-bottom:none;min-height:26px">
|
||
<span class="SDROP-LABEL">Hochgeladen</span>
|
||
<div class="SDROP-DIRS"><span class="SDIR">↑</span><span class="SDIR">↓</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">On mobile (<768px) the dropdown renders as a bottom sheet with a drag handle and rounded top corners. Overlay taps the backdrop to close. Option rows use generous py for thumb targets.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Implementation Reference — §3 Mobile layout
|
||
<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>Search card row 1 (mobile)</td>
|
||
<td><code>flex items-center gap-2 mb-2</code></td>
|
||
<td>—</td>
|
||
<td>Input + reset only. Sort and filter move to row 2. Breakpoint: <code>sm:hidden</code> on row 2 wrapper, show on mobile only.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Search card row 2 (mobile)</td>
|
||
<td><code>flex items-center gap-2 sm:hidden</code></td>
|
||
<td>—</td>
|
||
<td>Sort + filter as two equal-width buttons. Desktop keeps sort in row 1 between input and filter.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Sort button label — mobile</td>
|
||
<td>—</td>
|
||
<td>—</td>
|
||
<td>Omit "Sortieren:" prefix (hidden with <code>hidden sm:inline</code>). Show field name + direction only: "Absender ↑"</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Sort button width — mobile</td>
|
||
<td><code>flex-1 justify-center min-h-[44px]</code></td>
|
||
<td><span class="ir-px">h 44px min, flex-1</span></td>
|
||
<td>Equal width with filter button. Most commonly undersized on mobile.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Bottom sheet wrapper</td>
|
||
<td><code>fixed inset-x-0 bottom-0 z-50 bg-surface border-t-2 border-brand-navy rounded-t-2xl shadow-[0_-4px_24px_rgba(0,0,0,.15)] sm:hidden</code></td>
|
||
<td>—</td>
|
||
<td>Only on <code><768px</code>. Desktop: <code>absolute</code> dropdown. Backdrop: <code>fixed inset-0 z-40 bg-black/20</code> behind sheet.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Bottom sheet drag handle</td>
|
||
<td><code>mx-auto mt-2 mb-1 w-10 h-1 rounded-full bg-line</code></td>
|
||
<td><span class="ir-px">40px × 4px</span></td>
|
||
<td>Decorative — does not need to be draggable. Tap backdrop or select option to close.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Bottom sheet option rows</td>
|
||
<td><code>flex items-center px-4 py-3.5 gap-4 border-b border-line last:border-b-0</code></td>
|
||
<td><span class="ir-px">py 14px → row h ~52px</span></td>
|
||
<td>Taller than desktop rows for thumb comfort. 52px exceeds 44px minimum — intentional for seniors.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Direction buttons — mobile</td>
|
||
<td><code>text-sm font-bold min-w-[40px] min-h-[40px] flex items-center justify-center rounded-sm border border-line</code></td>
|
||
<td><span class="ir-px">40px × 40px min</span></td>
|
||
<td>Larger touch target than desktop. Active: <code>bg-brand-navy text-white border-brand-navy</code></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- ══════════════════════════════════════
|
||
SECTION 4 — LOADING & EMPTY STATES
|
||
══════════════════════════════════════ -->
|
||
<div class="sec">
|
||
<div class="sec-h">
|
||
<span class="sec-num">4</span>
|
||
Loading & empty states
|
||
</div>
|
||
|
||
<div class="sg sg-2" style="gap:24px;align-items:start">
|
||
|
||
<!-- Loading: search in progress -->
|
||
<div class="sb">
|
||
<span class="state-label st-loading">Loading — search request in flight</span>
|
||
<div class="wf">
|
||
<div class="wf-inner">
|
||
<div class="wf-bar">
|
||
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||
<div class="urlbar"><span>localhost:3000/?q=raddatz&sort=sender&dir=asc</span></div>
|
||
</div>
|
||
<div class="N">
|
||
<span class="logo">FAMILIENARCHIV</span>
|
||
<span class="nl on">Dokumente</span>
|
||
<span class="nl">Personen</span>
|
||
<div class="nr"><div class="nico"></div></div>
|
||
</div>
|
||
<div class="MAIN">
|
||
<div class="SCARD">
|
||
<div class="SROW1">
|
||
<div class="SINPUT has-value">
|
||
<!-- Spinner replaces search icon while loading -->
|
||
<div class="SPINNER"></div>
|
||
<span style="font-size:8px">raddatz</span>
|
||
</div>
|
||
<div class="SBTN-SORT active">
|
||
<span class="sort-prefix">Sortieren:</span>
|
||
Absender ↑
|
||
<span class="sort-chev">▾</span>
|
||
</div>
|
||
<div class="SBTN-FILTER">⊞ Filter <span style="font-size:7px;opacity:.5">▾</span></div>
|
||
<div class="SBTN-RESET">✕</div>
|
||
</div>
|
||
</div>
|
||
<!-- Loading skeleton -->
|
||
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:3px;padding:10px 12px;margin-bottom:5px">
|
||
<div class="PH w60" style="margin-bottom:6px"></div>
|
||
<div class="PH w40"></div>
|
||
</div>
|
||
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:3px;padding:10px 12px;margin-bottom:5px">
|
||
<div class="PH w80" style="margin-bottom:6px"></div>
|
||
<div class="PH w50"></div>
|
||
</div>
|
||
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:3px;padding:10px 12px;opacity:.6">
|
||
<div class="PH w70" style="margin-bottom:6px"></div>
|
||
<div class="PH w30"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">Spinner (animated ring) replaces the search icon inside the input while the request is in-flight. Skeleton rows replace the document list. Sort button remains interactive during loading so users can change sort without waiting.</div>
|
||
</div>
|
||
|
||
<!-- Empty state -->
|
||
<div class="sb">
|
||
<span class="state-label st-default">Empty state — no results for query</span>
|
||
<div class="wf">
|
||
<div class="wf-inner">
|
||
<div class="wf-bar">
|
||
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||
<div class="urlbar"><span>localhost:3000/?q=beethoven&sort=date&dir=desc</span></div>
|
||
</div>
|
||
<div class="N">
|
||
<span class="logo">FAMILIENARCHIV</span>
|
||
<span class="nl on">Dokumente</span>
|
||
<span class="nl">Personen</span>
|
||
<div class="nr"><div class="nico"></div></div>
|
||
</div>
|
||
<div class="MAIN">
|
||
<div class="SCARD">
|
||
<div class="SROW1">
|
||
<div class="SINPUT has-value">
|
||
<span class="SICON">⌕</span>
|
||
<span style="font-size:8px">beethoven</span>
|
||
</div>
|
||
<div class="SBTN-SORT" style="color:#9CA3AF;border-color:#E0DDD6">
|
||
<span class="sort-prefix">Sortieren:</span>
|
||
Datum ↓
|
||
<span class="sort-chev">▾</span>
|
||
</div>
|
||
<div class="SBTN-FILTER">⊞ Filter <span style="font-size:7px;opacity:.5">▾</span></div>
|
||
<div class="SBTN-RESET">✕</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Empty state message -->
|
||
<div style="background:#fff;border:1.5px solid #E0DDD6;border-radius:3px;display:flex;flex-direction:column;align-items:center;padding:32px 20px;text-align:center">
|
||
<div style="width:36px;height:36px;background:#F0EDE8;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:16px;margin-bottom:10px">🔍</div>
|
||
<div style="font-size:11px;font-weight:800;color:#0D2240;font-family:Georgia,serif;margin-bottom:4px">Keine Dokumente gefunden</div>
|
||
<div style="font-size:8.5px;color:#888;max-width:240px;line-height:1.7;margin-bottom:12px">Keine Treffer für <strong>„beethoven"</strong>. Versuche andere Begriffe oder entferne Filter.</div>
|
||
<a href="/" style="font-size:8px;font-weight:700;color:#002850;text-decoration:none;border:1.5px solid #002850;border-radius:2px;padding:4px 10px">Suche zurücksetzen</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sc">Zero-results state shows an icon, a friendly headline, and the search term quoted back. Sort button dims to indicate it has no effect. A single CTA resets the search. Addresses #180 Problem 4 (empty state).</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="impl-ref">
|
||
<div class="impl-ref-hdr">Implementation Reference — §4 Loading & empty states
|
||
<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>Spinner (inside input)</td>
|
||
<td><code>w-4 h-4 rounded-full border-2 border-line border-t-brand-navy shrink-0 animate-spin</code></td>
|
||
<td><span class="ir-px">16px × 16px</span></td>
|
||
<td>Replaces search icon during <code>isLoading</code> state. <code>aria-label="Suche läuft"</code> on a visually hidden span. Add <code>let isLoading = $state(false)</code> to <code>+page.svelte</code>; set true on <code>goto()</code>, false when <code>data</code> reactive updates.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Skeleton row</td>
|
||
<td><code>bg-surface border border-line rounded-sm px-4 py-3 mb-2 flex flex-col gap-2</code></td>
|
||
<td><span class="ir-px">py 12px</span></td>
|
||
<td>Two <code><div class="h-3 rounded bg-muted animate-pulse"></code> lines (80% and 50% width). 3 skeleton rows sufficient.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Sort button — no results</td>
|
||
<td>Add <code>opacity-40 pointer-events-none</code> when result count is 0</td>
|
||
<td>—</td>
|
||
<td>Dimmed to signal no effect. Do not disable entirely — allow user to change sort to try different result order.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Empty state container</td>
|
||
<td><code>bg-surface border border-line rounded-sm py-16 px-6 flex flex-col items-center text-center</code></td>
|
||
<td><span class="ir-px">py 64px</span></td>
|
||
<td>Only rendered when <code>!isLoading && documents.length === 0 && !isDashboard</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Empty state icon</td>
|
||
<td><code>w-14 h-14 rounded-full bg-muted flex items-center justify-center text-2xl mb-4</code></td>
|
||
<td><span class="ir-px">56px × 56px</span></td>
|
||
<td>Use a search/magnifier SVG, not an emoji, in production</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Empty state headline</td>
|
||
<td><code>font-serif text-xl font-bold text-ink mb-2</code></td>
|
||
<td><span class="ir-px">20px / 700</span></td>
|
||
<td>"Keine Dokumente gefunden" — most commonly undersized on empty states</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Empty state subtext</td>
|
||
<td><code>text-sm text-ink-3 max-w-xs leading-relaxed mb-5</code></td>
|
||
<td><span class="ir-px">14px, leading 1.625</span></td>
|
||
<td>Quote the search term: <code>„{q}"</code>. Paraglide key: <code>docs_empty_state_text</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Empty state CTA</td>
|
||
<td><code>text-sm font-bold text-brand-navy border border-brand-navy rounded-sm px-4 py-2 hover:bg-brand-navy hover:text-white transition</code></td>
|
||
<td><span class="ir-px">h ~40px</span></td>
|
||
<td><code><a href="/"></code> — no JS needed. Paraglide key: <code>docs_btn_reset_title</code></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- ══════════════════════════════════════
|
||
SECTION 5 — BACKEND & DATA WIRING
|
||
══════════════════════════════════════ -->
|
||
<div class="sec">
|
||
<div class="sec-h">
|
||
<span class="sec-num">5</span>
|
||
Backend & data wiring — implementation checklist
|
||
</div>
|
||
|
||
<div class="impl-ref" style="margin-top:0">
|
||
<div class="impl-ref-hdr">Implementation Reference — §5 Full-stack wiring
|
||
<span>Not a visual section — agent checklist only</span>
|
||
</div>
|
||
<table>
|
||
<thead><tr><th>Layer</th><th>What to change</th><th>Detail</th><th>Notes</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Frontend state</td>
|
||
<td>Add <code>sort</code> + <code>dir</code> to <code>+page.svelte</code></td>
|
||
<td><code>let sort = $state(untrack(() => data.filters?.sort || 'date'))</code><br><code>let dir = $state(untrack(() => data.filters?.dir || 'desc'))</code></td>
|
||
<td>Default: date/desc. Omit from URL when default to keep URLs clean.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Frontend search trigger</td>
|
||
<td>Add sort + dir to <code>triggerSearch()</code></td>
|
||
<td><code>if (sort !== 'date' || dir !== 'desc') { params.set('sort', sort); params.set('dir', dir); }</code></td>
|
||
<td>Only append if non-default — avoids polluting the URL for most users.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Frontend server load</td>
|
||
<td>Read <code>sort</code> + <code>dir</code> in <code>+page.server.ts</code></td>
|
||
<td><code>const sort = url.searchParams.get('sort') || 'date';</code><br><code>const dir = url.searchParams.get('dir') || 'desc';</code></td>
|
||
<td>Pass to <code>/api/documents/search</code> query params. Add to the returned <code>filters</code> object for sync.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Frontend sync effect</td>
|
||
<td>Sync <code>sort</code> + <code>dir</code> in the <code>$effect</code> that syncs filter state from data</td>
|
||
<td><code>sort = data.filters?.sort || 'date';</code><br><code>dir = data.filters?.dir || 'desc';</code></td>
|
||
<td>Keeps sort state consistent on browser back/forward navigation.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>SearchFilterBar props</td>
|
||
<td>Add <code>sort</code> + <code>dir</code> as bindable props</td>
|
||
<td><code>sort = $bindable('date')</code>, <code>dir = $bindable('desc')</code></td>
|
||
<td>Pass <code>bind:sort</code> + <code>bind:dir</code> from <code>+page.svelte</code>.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Backend endpoint</td>
|
||
<td>Extend <code>GET /api/documents/search</code></td>
|
||
<td>Add query params: <code>sort</code> (string, default "date") and <code>dir</code> (string, default "desc")</td>
|
||
<td>Map values: <code>date→documentDate</code>, <code>title→title</code>, <code>sender→sender.lastName</code>, <code>receiver→receivers[0].lastName</code>, <code>tag→tags[0].name</code>, <code>uploaded→createdAt</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Backend sort mapping</td>
|
||
<td>In <code>DocumentService.search()</code>, add <code>Sort</code> parameter</td>
|
||
<td>Build <code>Sort sort = Sort.by(direction, field)</code>. Pass to <code>repository.findAll(spec, pageable)</code> or equivalent query method.</td>
|
||
<td>Sender/receiver sort requires a JOIN — use <code>@Query</code> with explicit ORDER BY or a <code>Specification</code> with <code>Join</code>. Tag sort: sort by first tag name alphabetically.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>API type regeneration</td>
|
||
<td>Run <code>npm run generate:api</code> after backend changes</td>
|
||
<td>Requires backend running with <code>--spring.profiles.active=dev</code></td>
|
||
<td>The new <code>sort</code> + <code>dir</code> params must appear in the OpenAPI spec for the typed client to pick them up.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>i18n keys needed</td>
|
||
<td>Add to <code>messages/de.json</code>, <code>en.json</code>, <code>es.json</code></td>
|
||
<td><code>docs_sort_label</code>, <code>docs_sort_date</code>, <code>docs_sort_title</code>, <code>docs_sort_sender</code>, <code>docs_sort_receiver</code>, <code>docs_sort_tag</code>, <code>docs_sort_uploaded</code>, <code>docs_sort_dir_asc</code>, <code>docs_sort_dir_desc</code>, <code>docs_empty_state_text</code></td>
|
||
<td>Also update <code>docs_search_placeholder</code> to "Titel, Personen, Tags durchsuchen …" to address #180 Problem 2.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /doc -->
|
||
</body>
|
||
</html>
|