Files
familienarchiv/docs/specs/bulk-upload-concepts.html
Marcel 8f28a99e00
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 28s
CI / Backend Unit Tests (push) Failing after 2m51s
docs(specs): bulk upload split-panel spec + concept exploration
Adds two specs for extending issue #294 with bulk uploads:

- bulk-upload-concepts.html — three concepts (stack, split-panel
  with file switcher, progressive accordion) with a decision
  matrix and the Concept B recommendation.
- bulk-upload-split-panel-spec.html — refined final spec for
  Concept B. Covers all three states (N=0 empty · N=1 single ·
  N≥2 multi) across 320 / 375 / 768 / 1280 viewports in both
  light and dark mode, using the real tokens from layout.css.
  Includes impl-ref tables for every new surface, Paraglide keys
  in de/en/es, component tree, and backend contract.

The polymorphic-state model means /documents/new is a single
route: N=1 is byte-identical to #294, N=0 shows a whole-panel
drop zone with bulk-first copy, N≥2 grows a file-switcher strip
under the PDF preview plus a two-card form split.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 10:31:42 +02:00

997 lines
60 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Bulk Upload — 3 Concept Designs · Familienarchiv</title>
<link href="https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;1,300&family=Montserrat:wght@400;500;600;700;800;900&display=swap" rel="stylesheet"/>
<style>
/* ── Reset ── */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Montserrat',system-ui,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5;font-size:13px}
.doc{max-width:1300px;margin:0 auto;padding:48px 32px 120px}
/* ── Masthead ── */
.mh{padding-bottom:24px;border-bottom:3px solid #002850;margin-bottom:60px}
.mh .kicker{font-size:9px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#A6DAD8}
.mh h1{font-size:28px;font-weight:900;color:#002850;letter-spacing:-.4px;margin-top:6px}
.mh p{font-size:13px;color:#555;max-width:780px;line-height:1.75;margin-top:10px}
.mh .byline{font-size:9px;color:#999;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;margin-top:14px}
.tag-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
.tag{background:#002850;color:#A6DAD8;padding:3px 9px;border-radius:2px;font-size:8.5px;font-weight:700;letter-spacing:.8px;text-transform:uppercase}
.tag.amber{background:#7c4a00;color:#fde68a}
.tag.green{background:#1e5e34;color:#d1fae5}
.tag.gray{background:#4b5563;color:#e5e7eb}
.tag.mint{background:#A6DAD8;color:#002850}
/* ── Goals card ── */
.goals{background:#fff;border:1px solid #E4E2D7;border-radius:4px;padding:22px 26px;margin:0 0 60px;box-shadow:0 1px 3px rgba(0,0,0,.04)}
.goals h2{font-size:11px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#002850;margin-bottom:14px}
.goals ul{list-style:none;display:grid;grid-template-columns:1fr 1fr;gap:10px 28px}
.goals li{font-size:12.5px;color:#333;padding-left:20px;position:relative;line-height:1.55}
.goals li::before{content:"→";position:absolute;left:0;top:0;color:#A6DAD8;font-weight:800}
/* ── Concept section ── */
.concept{margin-bottom:88px;padding-bottom:88px;border-bottom:2px dashed #C8C4BE}
.concept:last-of-type{border-bottom:none;margin-bottom:0;padding-bottom:0}
.concept-header{display:flex;align-items:flex-start;gap:24px;margin-bottom:36px}
.concept-num{font-size:84px;font-weight:900;color:#E0DDD6;line-height:1;flex-shrink:0;width:96px}
.concept-label{font-size:8.5px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#A6DAD8;margin-bottom:5px}
.concept-title{font-family:'Merriweather',Georgia,serif;font-size:24px;font-weight:700;color:#002850;margin-bottom:10px}
.concept-desc{font-size:13.5px;color:#555;max-width:740px;line-height:1.75}
.concept-best{margin-top:14px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.best-label{background:#A6DAD8;color:#002850;padding:3px 9px;border-radius:2px;font-size:8.5px;font-weight:800;letter-spacing:.6px;text-transform:uppercase}
.best-text{font-size:12px;font-weight:600;color:#444}
.concept-tradeoff{margin-top:8px;font-size:12px;color:#888;font-style:italic;max-width:680px;line-height:1.7}
/* ── Browser chrome ── */
.screen{max-width:980px;margin:0 auto}
.screen.narrow{max-width:400px}
.chrome{background:#F5F4EE;border:1.5px solid #C4C0BA;border-radius:8px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.1)}
.chrome-bar{height:22px;background:#E8E6E0;border-bottom:1px solid #C4C0BA;display:flex;align-items:center;padding:0 9px;gap:5px;flex-shrink:0}
.chrome-dot{width:7px;height:7px;border-radius:50%;background:#BDB8B1}
.chrome-url{flex:1;height:10px;background:#CCC8C2;border-radius:5px;margin-left:8px}
.viewport-hint{font-size:7.5px;font-weight:800;color:#A6DAD8;letter-spacing:1px;text-transform:uppercase;padding:4px 9px;background:#002850;border-radius:2px;margin-left:8px}
/* ── App nav ── */
.app-nav{height:32px;background:#002850;display:flex;align-items:center;padding:0 14px;gap:12px;flex-shrink:0}
.app-logo{font-family:'Merriweather',Georgia,serif;font-size:8px;font-weight:700;color:#fff;border-bottom:2px solid #A6DAD8;padding-bottom:1px}
.app-link{font-size:6px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:rgba(255,255,255,.45);white-space:nowrap}
.app-link.on{color:rgba(255,255,255,.9)}
.app-nav-r{margin-left:auto;display:flex;gap:8px;align-items:center}
.app-avatar{width:18px;height:18px;border-radius:50%;background:rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font-size:6px;font-weight:800;color:rgba(255,255,255,.5)}
/* ── Common form element styles ── */
.f-label{font-size:6.5px;font-weight:700;color:#666;letter-spacing:.2px;text-transform:uppercase}
.f-req{color:#C0392B}
.f-input{height:20px;border:1px solid #D4D0CA;border-radius:2px;background:#fff;font-size:7.5px;padding:0 7px;color:#333;display:flex;align-items:center}
.f-input.focus{border-color:#002850;box-shadow:0 0 0 2px rgba(0,40,80,.12)}
.f-input.filled{color:#002850;font-weight:600;background:#FAFBFF}
.f-input.suggested{border-color:#A6DAD8;background:#F0FAFA;color:#005858;font-weight:600}
.f-input.empty{color:#BBB;font-style:italic}
.f-input.tall{height:28px}
.f-tags{display:flex;gap:3px;flex-wrap:wrap;min-height:20px;border:1px solid #D4D0CA;border-radius:2px;padding:2px 4px;background:#fff;align-items:center}
.f-chip{background:#002850;color:#A6DAD8;border-radius:2px;font-size:6px;font-weight:700;padding:1px 4px 1px 5px;display:flex;align-items:center;gap:2px}
.f-chip-rm{color:rgba(166,218,216,.5);font-weight:400}
/* ── Action bar ── */
.action-bar{height:46px;background:#F5F4EE;border-top:1px solid #E4E2D7;display:flex;align-items:center;padding:0 14px;gap:8px;flex-shrink:0}
.btn-skip{font-size:7px;font-weight:700;color:#AAA;letter-spacing:.2px;cursor:pointer}
.btn-spacer{flex:1}
.btn-outline{height:24px;padding:0 12px;border:1px solid #C0BDB6;border-radius:2px;font-size:6.5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#777;display:flex;align-items:center;cursor:pointer;background:#fff}
.btn-primary{height:24px;padding:0 12px;border-radius:2px;font-size:6.5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;background:#002850;color:#fff;display:flex;align-items:center;cursor:pointer;gap:4px}
.btn-primary.green{background:#1A7040}
/* ─────────────────────────────────────── */
/* ── CONCEPT A — Stack (mobile-first) ── */
/* ─────────────────────────────────────── */
.ca-top-bar{height:34px;background:#F5F4EE;border-bottom:1px solid #E4E2D7;display:flex;align-items:center;padding:0 12px;gap:8px}
.ca-back{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888}
.ca-title{flex:1;text-align:center;font-family:'Merriweather',Georgia,serif;font-size:9px;color:#002850;font-weight:600}
.ca-count{font-size:7px;font-weight:700;color:#002850;background:#A6DAD8;padding:2px 6px;border-radius:10px;letter-spacing:.3px}
.ca-body{background:#ECEAE4;padding:14px 12px;overflow-y:auto}
.ca-drop{background:#fff;border:2px dashed #A6DAD8;border-radius:4px;padding:14px;text-align:center;margin-bottom:14px}
.ca-drop-icon{font-size:18px;color:#A6DAD8;margin-bottom:4px}
.ca-drop-title{font-size:8.5px;font-weight:700;color:#002850;margin-bottom:2px}
.ca-drop-sub{font-size:6.5px;color:#999}
.ca-shared-card{background:#fff;border:1px solid #E4E2D7;border-radius:3px;padding:12px 14px;margin-bottom:14px;box-shadow:0 1px 2px rgba(0,0,0,.03)}
.ca-shared-head{display:flex;align-items:center;gap:6px;margin-bottom:11px}
.ca-shared-badge{background:#A6DAD8;color:#002850;padding:2px 7px;border-radius:2px;font-size:6px;font-weight:800;letter-spacing:.4px;text-transform:uppercase}
.ca-shared-title{font-family:'Merriweather',Georgia,serif;font-size:9.5px;color:#002850;font-weight:700}
.ca-shared-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 10px}
.ca-shared-grid .full{grid-column:1/-1}
.ca-shared-field{display:flex;flex-direction:column;gap:3px}
.ca-files-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;padding:0 2px}
.ca-files-title{font-size:7px;font-weight:800;letter-spacing:1px;text-transform:uppercase;color:#B0ADA6}
.ca-files-add{font-size:7px;font-weight:700;color:#002850;display:flex;align-items:center;gap:3px}
.ca-file{background:#fff;border:1px solid #E4E2D7;border-radius:3px;padding:9px 10px;margin-bottom:7px;display:flex;align-items:center;gap:10px}
.ca-file.active{border-color:#002850;box-shadow:0 0 0 2px rgba(0,40,80,.08)}
.ca-thumb{width:28px;height:36px;background:#FFFEF8;border:1px solid #E4E2D7;border-radius:1px;flex-shrink:0;display:flex;flex-direction:column;padding:3px;gap:1px}
.ca-thumb .tl{height:2px;background:#C4BDB0;opacity:.6;border-radius:1px}
.ca-thumb .tl.s{width:60%;opacity:.35}
.ca-thumb .tl.m{width:82%}
.ca-file-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.ca-file-title{font-size:8px;color:#002850;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ca-file-title.placeholder{color:#888;font-weight:400;font-style:italic}
.ca-file-meta{font-size:6.5px;color:#AAA}
.ca-file-rm{font-size:10px;color:#B0ADA6;padding:0 4px;cursor:pointer}
/* ───────────────────────────────────────────── */
/* ── CONCEPT B — Split-panel + file switcher ── */
/* ───────────────────────────────────────────── */
.cb-top-bar{height:38px;background:#F5F4EE;border-bottom:1px solid #E4E2D7;display:flex;align-items:center;padding:0 14px;gap:10px}
.cb-back{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#888}
.cb-title{font-family:'Merriweather',Georgia,serif;font-size:9px;font-weight:700;color:#002850}
.cb-count{background:#A6DAD8;color:#002850;padding:2px 7px;border-radius:10px;font-size:7px;font-weight:800;letter-spacing:.3px}
.cb-discard{margin-left:auto;font-size:7px;font-weight:700;color:#C0392B;letter-spacing:.2px}
.cb-split{display:flex;min-height:440px}
.cb-pdf{flex:55;background:#5E5C59;display:flex;flex-direction:column;border-right:1px solid #3A3836}
.cb-pdf-toolbar{height:28px;background:#3A3836;display:flex;align-items:center;padding:0 10px;gap:8px}
.cb-pdf-btn{width:16px;height:16px;border-radius:2px;background:rgba(255,255,255,.1);display:flex;align-items:center;justify-content:center;font-size:7px;color:rgba(255,255,255,.6)}
.cb-pdf-page{font-size:6.5px;color:rgba(255,255,255,.4);margin-left:auto;font-weight:700;letter-spacing:.5px}
.cb-pdf-view{flex:1;display:flex;justify-content:center;padding:14px;overflow:hidden}
.cb-paper{background:#FFFEF8;box-shadow:0 2px 10px rgba(0,0,0,.3);border-radius:1px;padding:14px 16px;display:flex;flex-direction:column;gap:0;width:180px;flex-shrink:0}
.pl{height:4px;background:#C4BDB0;border-radius:1px;opacity:.55;margin-bottom:3px}
.pl.h{height:6px;opacity:.75;margin-bottom:5px}
.pl.s{width:55%;opacity:.3}
.pl.m{width:80%}
.pl.sp{height:7px;background:transparent}
.cb-filebar{background:#434140;border-top:1px solid #3A3836;display:flex;align-items:center;padding:0 8px;gap:3px;height:36px;flex-shrink:0}
.cb-fb-arrow{width:18px;height:22px;border-radius:2px;background:rgba(255,255,255,.08);display:flex;align-items:center;justify-content:center;font-size:9px;color:rgba(255,255,255,.6)}
.cb-fb-track{flex:1;display:flex;gap:3px;padding:0 3px;overflow:hidden}
.cb-fb-item{padding:3px 6px;border-radius:2px;font-size:6px;font-weight:700;color:rgba(255,255,255,.55);background:rgba(255,255,255,.06);display:flex;align-items:center;gap:4px;white-space:nowrap}
.cb-fb-item.on{background:#A6DAD8;color:#002850}
.cb-fb-num{background:rgba(0,0,0,.15);border-radius:2px;padding:0 3px;font-size:5.5px;font-weight:800}
.cb-fb-item.on .cb-fb-num{background:rgba(0,40,80,.25);color:#002850}
.cb-form{flex:45;background:#fff;display:flex;flex-direction:column}
.cb-form-scroll{flex:1;overflow-y:auto;padding:14px}
.cb-only-card{background:#F0FAFA;border:1px solid #A6DAD8;border-radius:3px;padding:10px 12px;margin-bottom:12px}
.cb-only-head{display:flex;align-items:center;gap:6px;margin-bottom:7px}
.cb-only-badge{background:#005858;color:#A6DAD8;padding:2px 7px;border-radius:2px;font-size:6px;font-weight:800;letter-spacing:.4px;text-transform:uppercase}
.cb-only-subtitle{font-size:6.5px;color:#005858;font-weight:600;letter-spacing:.3px}
.cb-shared-card{background:#F9F8F5;border:1px solid #E4E2D7;border-radius:3px;padding:10px 12px;margin-bottom:10px}
.cb-shared-head{display:flex;align-items:center;gap:6px;margin-bottom:9px}
.cb-shared-badge{background:#A6DAD8;color:#002850;padding:2px 7px;border-radius:2px;font-size:6px;font-weight:800;letter-spacing:.4px;text-transform:uppercase}
.cb-shared-subtitle{font-size:6.5px;color:#002850;font-weight:600}
.cb-row{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-bottom:7px}
.cb-row.full{grid-template-columns:1fr}
.cb-field{display:flex;flex-direction:column;gap:3px}
/* ─────────────────────────────────────── */
/* ── CONCEPT C — Progressive accordion ── */
/* ─────────────────────────────────────── */
.cc-top-bar{height:34px;background:#F5F4EE;border-bottom:1px solid #E4E2D7;display:flex;align-items:center;padding:0 14px;gap:8px}
.cc-body{background:#ECEAE4;padding:14px;display:flex;flex-direction:column;gap:11px;max-height:540px;overflow-y:auto}
.cc-shared{background:#fff;border:1px solid #E4E2D7;border-radius:3px;padding:12px 14px;box-shadow:0 1px 2px rgba(0,0,0,.03);position:sticky;top:0;z-index:2}
.cc-shared-head{display:flex;align-items:center;gap:7px;margin-bottom:11px}
.cc-shared-badge{background:#A6DAD8;color:#002850;padding:2px 7px;border-radius:2px;font-size:6px;font-weight:800;letter-spacing:.4px;text-transform:uppercase}
.cc-shared-title{font-family:'Merriweather',Georgia,serif;font-size:10px;color:#002850;font-weight:700}
.cc-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px 10px}
.cc-grid .span2{grid-column:span 2}
.cc-files-label{font-size:7px;font-weight:800;letter-spacing:1px;text-transform:uppercase;color:#B0ADA6;padding:0 2px;margin-top:6px}
.cc-file{background:#fff;border:1px solid #E4E2D7;border-radius:3px;overflow:hidden}
.cc-file.open{border-color:#002850;box-shadow:0 2px 6px rgba(0,40,80,.08)}
.cc-file-head{display:flex;align-items:center;gap:10px;padding:9px 12px;cursor:pointer}
.cc-file-head.open{border-bottom:1px solid #E4E2D7;background:#F9F8F5}
.cc-caret{font-size:9px;color:#A6DAD8;width:10px}
.cc-file-thumb{width:22px;height:28px;background:#FFFEF8;border:1px solid #E4E2D7;border-radius:1px;padding:2px;display:flex;flex-direction:column;gap:1px;flex-shrink:0}
.cc-file-thumb .tl{height:2px;background:#C4BDB0;opacity:.55;border-radius:1px}
.cc-file-body{flex:1;min-width:0}
.cc-file-titlerow{display:flex;align-items:center;gap:7px}
.cc-file-title{font-size:8.5px;color:#002850;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cc-file-title.placeholder{color:#888;font-weight:400;font-style:italic}
.cc-file-meta{font-size:6.5px;color:#AAA;margin-top:2px}
.cc-file-rm{font-size:11px;color:#B0ADA6;padding:0 4px}
.cc-file-open{display:flex;background:#F5F4EE}
.cc-preview{flex:45;background:#5E5C59;padding:12px;display:flex;justify-content:center}
.cc-preview-paper{background:#FFFEF8;border-radius:1px;padding:8px 10px;width:110px;flex-shrink:0;display:flex;flex-direction:column;box-shadow:0 2px 6px rgba(0,0,0,.25)}
.cc-file-form{flex:55;padding:12px 14px;background:#fff;display:flex;flex-direction:column;gap:7px}
/* ─────────── Decision matrix ─────────── */
.decision{background:#fff;border:1px solid #E4E2D7;border-radius:4px;padding:28px 32px;margin:88px 0 60px;box-shadow:0 1px 3px rgba(0,0,0,.04)}
.decision h2{font-size:11px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#002850;margin-bottom:6px}
.decision p.lead{font-size:13.5px;color:#555;line-height:1.7;margin-bottom:22px;max-width:820px}
.dm{width:100%;border-collapse:collapse;margin-top:12px;font-size:12px}
.dm th{text-align:left;font-size:9.5px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;color:#002850;padding:9px 12px;background:#F9F8F5;border-bottom:2px solid #E4E2D7}
.dm td{padding:13px 12px;border-bottom:1px solid #EFEDE7;vertical-align:top;line-height:1.6}
.dm td:first-child{font-weight:700;color:#002850;width:18%;white-space:nowrap}
.dm td.score{font-size:15px;text-align:center;width:12%}
.dm td.ok{color:#1A7040}
.dm td.mid{color:#A07100}
.dm td.bad{color:#C0392B}
/* ─────────── Recommendation ─────────── */
.reco{background:#002850;color:#fff;border-radius:6px;padding:36px 40px;margin:48px 0 64px;box-shadow:0 4px 20px rgba(0,40,80,.15)}
.reco .kicker{font-size:9px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#A6DAD8}
.reco h2{font-family:'Merriweather',Georgia,serif;font-size:26px;font-weight:700;margin-top:6px}
.reco .why{font-size:13.5px;line-height:1.85;color:rgba(255,255,255,.88);max-width:780px;margin-top:14px}
.reco ul{list-style:none;margin-top:14px;display:grid;grid-template-columns:1fr 1fr;gap:9px 26px}
.reco ul li{font-size:12.5px;color:rgba(255,255,255,.9);padding-left:22px;position:relative;line-height:1.6}
.reco ul li::before{content:"✓";position:absolute;left:0;top:0;color:#A6DAD8;font-weight:800}
/* ─────────── Impl-ref ─────────── */
.impl{background:#fff;border:1px solid #E4E2D7;border-radius:4px;padding:28px 32px;box-shadow:0 1px 3px rgba(0,0,0,.04)}
.impl h2{font-size:11px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#002850;margin-bottom:16px}
.impl h3{font-family:'Merriweather',Georgia,serif;font-size:15px;color:#002850;margin:22px 0 10px}
.impl-table{width:100%;border-collapse:collapse;margin-top:6px;font-size:12px}
.impl-table th{text-align:left;font-size:9px;font-weight:800;letter-spacing:.6px;text-transform:uppercase;color:#002850;padding:8px 10px;background:#F9F8F5;border-bottom:2px solid #E4E2D7}
.impl-table td{padding:10px;border-bottom:1px solid #EFEDE7;vertical-align:top;line-height:1.55}
.impl-table td:first-child{font-weight:700;color:#002850;width:22%}
.impl-table td code{font-family:'SF Mono','Menlo',monospace;font-size:11px;background:#F0EEE8;padding:1px 6px;border-radius:2px;color:#002850}
.impl-table td.px{color:#777;font-size:11.5px;width:16%}
.impl-table td.note{color:#888;font-size:11.5px;font-style:italic;width:22%}
.impl h3.ix{margin-top:32px}
.notes{background:#F9F8F5;border-left:3px solid #A6DAD8;padding:16px 22px;border-radius:0 4px 4px 0;margin-top:26px}
.notes .nh{font-size:9px;font-weight:800;letter-spacing:1px;text-transform:uppercase;color:#002850;margin-bottom:8px}
.notes ul{list-style:none;display:flex;flex-direction:column;gap:6px}
.notes li{font-size:12px;color:#333;padding-left:18px;position:relative;line-height:1.7}
.notes li::before{content:"•";position:absolute;left:0;top:0;color:#A6DAD8;font-weight:800}
</style>
</head>
<body>
<div class="doc">
<!-- ════════════════════════════════════════════ -->
<!-- ═══════════════ MASTHEAD ══════════════ -->
<!-- ════════════════════════════════════════════ -->
<div class="mh">
<div class="kicker">UX Spec · Bulk Upload</div>
<h1>Uploading multiple documents in a single pass</h1>
<p>
Extends issue <strong>#294</strong> (new-document split-panel) with bulk uploads. When a user drops
N&nbsp;files, every metadata field applies once to all of them — only the <em>title</em> is per-file,
pre-filled from the filename and editable inline. A single save POST creates N documents.
</p>
<div class="byline">Prepared by Leonie Voss · 2026-04-24 · Draft 1 · References: #294, #305</div>
<div class="tag-row">
<span class="tag">feature</span>
<span class="tag mint">ui</span>
<span class="tag gray">a11y 320px+</span>
<span class="tag green">backend ready</span>
</div>
</div>
<!-- Goals -->
<div class="goals">
<h2>Design goals</h2>
<ul>
<li><strong>One-pass feel</strong>: drop → fill shared fields → save. No wizard, no per-file detour.</li>
<li><strong>Every field is shared except the title</strong>, which is always set (filename-derived).</li>
<li><strong>No mode switch</strong>: 1 file and N files use the same screen — more files reveal more chrome.</li>
<li><strong>Scales to 20+ files</strong> without the form losing scan-ability on mobile.</li>
<li><strong>Reuses the #294 split-panel layout</strong> (DocumentEditLayout) — minimum new surface.</li>
<li><strong>a11y-first</strong>: 44px targets, focus states, `aria-current` on active file, keyboard-navigable.</li>
</ul>
</div>
<!-- ════════════════════════════════════════════ -->
<!-- ═════════ CONCEPT A — STACK ═════════ -->
<!-- ════════════════════════════════════════════ -->
<section class="concept">
<div class="concept-header">
<div class="concept-num">A</div>
<div>
<div class="concept-label">Concept A</div>
<div class="concept-title">Flat Stack — shared header · file cards · sticky save</div>
<p class="concept-desc">
A single vertical flow: drop zone on top, then a <em>Gilt für alle</em> metadata card,
then stacked file cards (thumbnail · editable title · remove). No split panel, no tabs.
Scrolling down reveals all files; the save bar sticks to the bottom.
</p>
<div class="concept-best">
<span class="best-label">Best for</span>
<span class="best-text">Small-screen workflows. Seniors who prefer linear flows over tabs.</span>
</div>
<div class="concept-tradeoff">
Trade-off: no PDF preview until you click through to the document after save. Harder to verify
you grabbed the right files before committing.
</div>
</div>
</div>
<!-- mobile mockup -->
<div class="screen narrow">
<div class="chrome">
<div class="chrome-bar">
<div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div>
<div class="chrome-url"></div>
<div class="viewport-hint">375 · mobile</div>
</div>
<div class="app-nav">
<div class="app-logo">Familienarchiv</div>
<div class="app-nav-r">
<div class="app-avatar">MR</div>
</div>
</div>
<div class="ca-top-bar">
<div class="ca-back">← Zurück</div>
<div class="ca-title">Neue Dokumente</div>
<div class="ca-count">5</div>
</div>
<div class="ca-body" style="height:500px">
<!-- drop zone -->
<div class="ca-drop">
<div class="ca-drop-icon"></div>
<div class="ca-drop-title">Weitere Dateien hinzufügen</div>
<div class="ca-drop-sub">PDF, JPEG, PNG, TIFF · max 50 MB</div>
</div>
<!-- shared card -->
<div class="ca-shared-card">
<div class="ca-shared-head">
<span class="ca-shared-badge">Gilt für alle 5</span>
<span class="ca-shared-title">Angaben</span>
</div>
<div class="ca-shared-grid">
<div class="ca-shared-field">
<span class="f-label">Absender</span>
<div class="f-input filled">Hans Müller</div>
</div>
<div class="ca-shared-field">
<span class="f-label">Empfänger</span>
<div class="f-input filled">Anna Schmidt</div>
</div>
<div class="ca-shared-field">
<span class="f-label">Datum</span>
<div class="f-input filled">1950-06</div>
</div>
<div class="ca-shared-field">
<span class="f-label">Ort</span>
<div class="f-input empty">Berlin</div>
</div>
<div class="ca-shared-field full">
<span class="f-label">Tags</span>
<div class="f-tags">
<span class="f-chip">Familie <span class="f-chip-rm">×</span></span>
<span class="f-chip">Krieg <span class="f-chip-rm">×</span></span>
</div>
</div>
</div>
</div>
<!-- files list -->
<div class="ca-files-head">
<div class="ca-files-title">5 Dateien · Titel bearbeiten</div>
</div>
<div class="ca-file active">
<div class="ca-thumb"><div class="tl h"></div><div class="tl"></div><div class="tl m"></div><div class="tl s"></div></div>
<div class="ca-file-body">
<div class="ca-file-title">Brief_1940_Hans</div>
<div class="ca-file-meta">Brief_1940_Hans.pdf · 2.4 MB</div>
</div>
<div class="ca-file-rm"></div>
</div>
<div class="ca-file">
<div class="ca-thumb"><div class="tl h"></div><div class="tl"></div><div class="tl"></div><div class="tl s"></div></div>
<div class="ca-file-body">
<div class="ca-file-title">Brief_1940_Anna</div>
<div class="ca-file-meta">Brief_1940_Anna.pdf · 1.8 MB</div>
</div>
<div class="ca-file-rm"></div>
</div>
<div class="ca-file">
<div class="ca-thumb"><div class="tl h"></div><div class="tl m"></div><div class="tl"></div><div class="tl"></div></div>
<div class="ca-file-body">
<div class="ca-file-title">Brief_1941_Clara</div>
<div class="ca-file-meta">Brief_1941_Clara.pdf · 890 kB</div>
</div>
<div class="ca-file-rm"></div>
</div>
<div class="ca-file">
<div class="ca-thumb"><div class="tl h"></div><div class="tl"></div><div class="tl s"></div><div class="tl m"></div></div>
<div class="ca-file-body">
<div class="ca-file-title placeholder">Postkarte_Venedig</div>
<div class="ca-file-meta">Postkarte_Venedig.jpg · 1.1 MB</div>
</div>
<div class="ca-file-rm"></div>
</div>
</div>
<div class="action-bar">
<div class="btn-skip">Alle verwerfen</div>
<div class="btn-spacer"></div>
<div class="btn-outline">Als Platzhalter</div>
<div class="btn-primary">5 speichern →</div>
</div>
</div>
</div>
</section>
<!-- ════════════════════════════════════════════ -->
<!-- ═══ CONCEPT B — SPLIT-PANEL + SWITCHER ══ -->
<!-- ════════════════════════════════════════════ -->
<section class="concept">
<div class="concept-header">
<div class="concept-num">B</div>
<div>
<div class="concept-label">Concept B · RECOMMENDED</div>
<div class="concept-title">Split-Panel with File Switcher</div>
<p class="concept-desc">
Reuses the <em>DocumentEditLayout</em> from issue #294 and adds a horizontal file-switcher strip
under the PDF preview. Right column splits into two cards: <em>Gilt nur für diese Datei</em>
(title only, mint accent) and <em>Gilt für alle N Dokumente</em> (everything else).
When N=1 the switcher disappears and the screen is byte-identical to #294.
</p>
<div class="concept-best">
<span class="best-label">Best for</span>
<span class="best-text">The project's primary use case. Desktop + tablet, matches #294 DNA.</span>
</div>
<div class="concept-tradeoff">
Trade-off: on mobile the split has to collapse into tabs ("Vorschau / Angaben"). We reuse the
same responsive pattern that DocumentEditLayout already ships with.
</div>
</div>
</div>
<!-- desktop mockup -->
<div class="screen">
<div class="chrome">
<div class="chrome-bar">
<div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div>
<div class="chrome-url"></div>
<div class="viewport-hint">1280 · desktop</div>
</div>
<div class="app-nav">
<div class="app-logo">Familienarchiv</div>
<div class="app-link on">Dokumente</div>
<div class="app-link">Personen</div>
<div class="app-link">Briefwechsel</div>
<div class="app-link">Chronik</div>
<div class="app-nav-r">
<div class="app-avatar">MR</div>
</div>
</div>
<div class="cb-top-bar">
<div class="cb-back">← Dokumente</div>
<div class="cb-title">Neue Dokumente</div>
<div class="cb-count">5 werden erstellt</div>
<div class="cb-discard">Alle verwerfen</div>
</div>
<div class="cb-split">
<!-- PDF side -->
<div class="cb-pdf">
<div class="cb-pdf-toolbar">
<div class="cb-pdf-btn"></div>
<div class="cb-pdf-btn"></div>
<div class="cb-pdf-btn">+</div>
<div class="cb-pdf-btn"></div>
<div class="cb-pdf-page">Seite 1 / 2 · Datei 1 von 5</div>
</div>
<div class="cb-pdf-view">
<div class="cb-paper">
<div class="pl h"></div><div class="pl h s"></div><div class="pl sp"></div>
<div class="pl"></div><div class="pl m"></div><div class="pl"></div>
<div class="pl s"></div><div class="pl sp"></div>
<div class="pl"></div><div class="pl m"></div><div class="pl"></div>
<div class="pl"></div><div class="pl s"></div><div class="pl sp"></div>
<div class="pl"></div><div class="pl m"></div><div class="pl s"></div>
</div>
</div>
<!-- file switcher -->
<div class="cb-filebar">
<div class="cb-fb-arrow"></div>
<div class="cb-fb-track">
<div class="cb-fb-item on"><span class="cb-fb-num">1</span> Brief_1940_Hans.pdf</div>
<div class="cb-fb-item"><span class="cb-fb-num">2</span> Brief_1940_Anna.pdf</div>
<div class="cb-fb-item"><span class="cb-fb-num">3</span> Brief_1941_Clara.pdf</div>
<div class="cb-fb-item"><span class="cb-fb-num">4</span> Postkarte_Venedig.jpg</div>
<div class="cb-fb-item"><span class="cb-fb-num">5</span> Urkunde_1942.pdf</div>
</div>
<div class="cb-fb-arrow"></div>
</div>
</div>
<!-- Form side -->
<div class="cb-form">
<div class="cb-form-scroll">
<!-- PER-FILE card -->
<div class="cb-only-card">
<div class="cb-only-head">
<span class="cb-only-badge">Nur diese Datei</span>
<span class="cb-only-subtitle">1 / 5 · Brief_1940_Hans.pdf</span>
</div>
<div class="cb-row full">
<div class="cb-field">
<span class="f-label">Titel <span class="f-req">*</span></span>
<div class="f-input filled tall">Brief an Anna, 1940</div>
</div>
</div>
</div>
<!-- SHARED card -->
<div class="cb-shared-card">
<div class="cb-shared-head">
<span class="cb-shared-badge">Gilt für alle 5</span>
<span class="cb-shared-subtitle">Gemeinsame Angaben</span>
</div>
<div class="cb-row">
<div class="cb-field">
<span class="f-label">Absender</span>
<div class="f-input filled">Hans Müller</div>
</div>
<div class="cb-field">
<span class="f-label">Empfänger</span>
<div class="f-input filled">Anna Schmidt</div>
</div>
</div>
<div class="cb-row">
<div class="cb-field">
<span class="f-label">Datum</span>
<div class="f-input filled">15.06.1950</div>
</div>
<div class="cb-field">
<span class="f-label">Ort</span>
<div class="f-input empty">z.B. Berlin</div>
</div>
</div>
<div class="cb-row full">
<div class="cb-field">
<span class="f-label">Tags</span>
<div class="f-tags">
<span class="f-chip">Familie <span class="f-chip-rm">×</span></span>
<span class="f-chip">Krieg <span class="f-chip-rm">×</span></span>
<span class="f-chip">Briefwechsel <span class="f-chip-rm">×</span></span>
</div>
</div>
</div>
<div class="cb-row">
<div class="cb-field">
<span class="f-label">Archivbox</span>
<div class="f-input empty">z.B. B-12</div>
</div>
<div class="cb-field">
<span class="f-label">Mappe</span>
<div class="f-input empty">z.B. M-3</div>
</div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-skip">Alle verwerfen</div>
<div class="btn-spacer"></div>
<div class="btn-outline">Als Platzhalter</div>
<div class="btn-primary green">5 speichern →</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ════════════════════════════════════════════ -->
<!-- ══ CONCEPT C — PROGRESSIVE ACCORDION ══ -->
<!-- ════════════════════════════════════════════ -->
<section class="concept">
<div class="concept-header">
<div class="concept-num">C</div>
<div>
<div class="concept-label">Concept C</div>
<div class="concept-title">Progressive Accordion — shared sticky header · file cards expand inline</div>
<p class="concept-desc">
Shared metadata sticks at the top of the page. Below, each file is a collapsed card; clicking
a card expands it to show the PDF preview + title field inline. Only one card is expanded at a
time. Scales well to 20+ files — the list stays readable, you only look at the PDFs you want
to verify.
</p>
<div class="concept-best">
<span class="best-label">Best for</span>
<span class="best-text">Large batches (10+ files) where you want to spot-check a few.</span>
</div>
<div class="concept-tradeoff">
Trade-off: two different visual languages — cards collapsed vs. cards expanded with PDF. New
pattern for the project; costs familiarity.
</div>
</div>
</div>
<div class="screen">
<div class="chrome">
<div class="chrome-bar">
<div class="chrome-dot"></div><div class="chrome-dot"></div><div class="chrome-dot"></div>
<div class="chrome-url"></div>
<div class="viewport-hint">1280 · desktop</div>
</div>
<div class="app-nav">
<div class="app-logo">Familienarchiv</div>
<div class="app-link on">Dokumente</div>
<div class="app-link">Personen</div>
<div class="app-nav-r"><div class="app-avatar">MR</div></div>
</div>
<div class="cc-top-bar">
<div class="ca-back">← Zurück</div>
<div class="ca-title">Neue Dokumente</div>
<div class="ca-count">5</div>
</div>
<div class="cc-body">
<!-- sticky shared card -->
<div class="cc-shared">
<div class="cc-shared-head">
<span class="cc-shared-badge">Gilt für alle 5</span>
<span class="cc-shared-title">Gemeinsame Angaben</span>
</div>
<div class="cc-grid">
<div class="cb-field"><span class="f-label">Absender</span><div class="f-input filled">Hans Müller</div></div>
<div class="cb-field"><span class="f-label">Empfänger</span><div class="f-input filled">Anna Schmidt</div></div>
<div class="cb-field"><span class="f-label">Datum</span><div class="f-input filled">15.06.1950</div></div>
<div class="cb-field span2"><span class="f-label">Tags</span><div class="f-tags"><span class="f-chip">Familie <span class="f-chip-rm">×</span></span><span class="f-chip">Krieg <span class="f-chip-rm">×</span></span></div></div>
<div class="cb-field"><span class="f-label">Ort</span><div class="f-input empty">z.B. Berlin</div></div>
</div>
</div>
<div class="cc-files-label">5 Dateien</div>
<!-- collapsed card -->
<div class="cc-file">
<div class="cc-file-head">
<div class="cc-caret"></div>
<div class="cc-file-thumb"><div class="tl"></div><div class="tl"></div><div class="tl"></div></div>
<div class="cc-file-body">
<div class="cc-file-titlerow">
<div class="cc-file-title">Brief an Anna, 1940</div>
</div>
<div class="cc-file-meta">Brief_1940_Hans.pdf · 2.4 MB</div>
</div>
<div class="cc-file-rm"></div>
</div>
</div>
<!-- expanded card -->
<div class="cc-file open">
<div class="cc-file-head open">
<div class="cc-caret" style="color:#002850"></div>
<div class="cc-file-thumb"><div class="tl"></div><div class="tl"></div><div class="tl"></div></div>
<div class="cc-file-body">
<div class="cc-file-titlerow">
<div class="cc-file-title">Brief von Anna, Antwort</div>
</div>
<div class="cc-file-meta">Brief_1940_Anna.pdf · 1.8 MB</div>
</div>
<div class="cc-file-rm"></div>
</div>
<div class="cc-file-open">
<div class="cc-preview">
<div class="cc-preview-paper">
<div class="pl h"></div><div class="pl h s"></div><div class="pl sp"></div>
<div class="pl"></div><div class="pl m"></div><div class="pl s"></div>
<div class="pl sp"></div>
<div class="pl"></div><div class="pl"></div><div class="pl m"></div>
</div>
</div>
<div class="cc-file-form">
<div class="cb-only-head">
<span class="cb-only-badge">Nur diese Datei</span>
<span class="cb-only-subtitle">2 / 5</span>
</div>
<div class="cb-field">
<span class="f-label">Titel <span class="f-req">*</span></span>
<div class="f-input filled tall">Brief von Anna, Antwort</div>
</div>
</div>
</div>
</div>
<!-- more collapsed -->
<div class="cc-file">
<div class="cc-file-head">
<div class="cc-caret"></div>
<div class="cc-file-thumb"><div class="tl"></div><div class="tl"></div><div class="tl"></div></div>
<div class="cc-file-body">
<div class="cc-file-titlerow">
<div class="cc-file-title placeholder">Brief_1941_Clara</div>
</div>
<div class="cc-file-meta">Brief_1941_Clara.pdf · 890 kB</div>
</div>
<div class="cc-file-rm"></div>
</div>
</div>
<div class="cc-file">
<div class="cc-file-head">
<div class="cc-caret"></div>
<div class="cc-file-thumb"><div class="tl"></div><div class="tl"></div><div class="tl"></div></div>
<div class="cc-file-body">
<div class="cc-file-titlerow">
<div class="cc-file-title placeholder">Postkarte_Venedig</div>
</div>
<div class="cc-file-meta">Postkarte_Venedig.jpg · 1.1 MB</div>
</div>
<div class="cc-file-rm"></div>
</div>
</div>
<div class="cc-file">
<div class="cc-file-head">
<div class="cc-caret"></div>
<div class="cc-file-thumb"><div class="tl"></div><div class="tl"></div><div class="tl"></div></div>
<div class="cc-file-body">
<div class="cc-file-titlerow">
<div class="cc-file-title placeholder">Urkunde_1942</div>
</div>
<div class="cc-file-meta">Urkunde_1942.pdf · 3.1 MB</div>
</div>
<div class="cc-file-rm"></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-skip">Alle verwerfen</div>
<div class="btn-spacer"></div>
<div class="btn-outline">Als Platzhalter</div>
<div class="btn-primary green">5 speichern →</div>
</div>
</div>
</div>
</section>
<!-- ════════════════════════════════════════════ -->
<!-- ══════════ DECISION MATRIX ════════════ -->
<!-- ════════════════════════════════════════════ -->
<div class="decision">
<h2>Decision matrix</h2>
<p class="lead">
All three concepts meet the core requirement (shared metadata + per-file title + one save).
Graded against what matters for the senior audience, the responsive constraint, and the #294
architectural commitment.
</p>
<table class="dm">
<thead>
<tr>
<th>Dimension</th>
<th>A · Stack</th>
<th>B · Split-Panel</th>
<th>C · Accordion</th>
</tr>
</thead>
<tbody>
<tr>
<td>Reuses #294 layout</td>
<td class="score bad"></td>
<td class="score ok"></td>
<td class="score bad"></td>
</tr>
<tr>
<td>Single-file mode unchanged</td>
<td class="score mid">rewrite</td>
<td class="score ok">identical</td>
<td class="score bad">different</td>
</tr>
<tr>
<td>PDF visible before save</td>
<td class="score bad">no</td>
<td class="score ok">always</td>
<td class="score mid">one at a time</td>
</tr>
<tr>
<td>Works at 320px</td>
<td class="score ok">native</td>
<td class="score mid">via tab collapse</td>
<td class="score ok">native</td>
</tr>
<tr>
<td>Scales to 20 files</td>
<td class="score mid">long scroll</td>
<td class="score ok">switcher scrolls</td>
<td class="score ok">collapsed list</td>
</tr>
<tr>
<td>New Svelte components</td>
<td class="score bad">3 new</td>
<td class="score ok">1 new (switcher)</td>
<td class="score bad">4 new</td>
</tr>
<tr>
<td>Familiar pattern</td>
<td class="score ok">yes</td>
<td class="score ok">yes (post-#294)</td>
<td class="score mid">new to app</td>
</tr>
</tbody>
</table>
</div>
<!-- ════════════════════════════════════════════ -->
<!-- ══════════ RECOMMENDATION ════════════ -->
<!-- ════════════════════════════════════════════ -->
<div class="reco">
<div class="kicker">Recommendation</div>
<h2>Ship Concept B</h2>
<p class="why">
Concept&nbsp;B treats bulk upload as a <em>polymorphic state</em> of the existing single-document
layout rather than a separate screen. A user who drops one file gets exactly the #294 experience.
A user who drops five gets the same screen plus a horizontal file-switcher and a two-card split
(<em>Nur diese Datei</em> vs. <em>Gilt für alle</em>). Nothing about the single-file flow changes.
</p>
<ul>
<li>Keeps the mental model: "one form, one save" regardless of file count.</li>
<li>PDF preview is persistent — you can spot-check each scan before committing.</li>
<li>The per-file title is visually promoted with a mint border so it reads as the one thing that differs per file.</li>
<li>Reuses DocumentEditLayout: the delta is ~1 new component (<code>FileSwitcherStrip</code>) + two cards in the form.</li>
<li>Single-file mode is byte-identical to #294 — no regression risk for existing users.</li>
<li>Backend is already ready (<code>POST /api/documents/quick-upload</code> accepts N files in one multipart).</li>
</ul>
</div>
<!-- ════════════════════════════════════════════ -->
<!-- ══════════ IMPL-REF · CONCEPT B ═══════ -->
<!-- ════════════════════════════════════════════ -->
<div class="impl">
<h2>Implementation reference — Concept B</h2>
<h3>Top bar (when N > 1)</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Count pill "N werden erstellt"</td>
<td><code>bg-accent text-primary rounded-full px-3 py-1 text-sm font-bold</code></td>
<td class="px">14px · 700</td>
<td class="note">brand-mint on brand-navy</td>
</tr>
<tr>
<td>"Alle verwerfen" link</td>
<td><code>ml-auto text-sm font-bold text-red-600 hover:text-red-800 focus-visible:outline-2 focus-visible:outline-red-600</code></td>
<td class="px">14px / 44px target</td>
<td class="note">confirm dialog before wiping</td>
</tr>
</table>
<h3 class="ix">FileSwitcherStrip (new component)</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Strip container</td>
<td><code>flex items-center gap-1 bg-ink/95 px-2 py-2 border-t border-ink/80</code></td>
<td class="px">height 48px</td>
<td class="note">under the PDF toolbar, on the dark panel</td>
</tr>
<tr>
<td>Arrow buttons</td>
<td><code>h-10 w-10 rounded-sm bg-white/8 text-surface/60 hover:bg-white/15 focus-visible:outline-2</code></td>
<td class="px">40×40 (44 w/padding)</td>
<td class="note"><code>aria-label="Vorherige Datei"</code> / "Nächste Datei"</td>
</tr>
<tr>
<td>File chip (inactive)</td>
<td><code>px-3 py-2 rounded-sm bg-white/6 text-sm font-bold text-surface/55 whitespace-nowrap hover:bg-white/12</code></td>
<td class="px">14px / h 40px</td>
<td class="note">horizontal scroll container uses <code>snap-x snap-mandatory</code></td>
</tr>
<tr>
<td>File chip (active)</td>
<td><code>... bg-accent text-primary</code> + <code>aria-current="true"</code></td>
<td class="px">14px / h 40px</td>
<td class="note">mint pill, primary text — 7.2:1 contrast passes AAA</td>
</tr>
<tr>
<td>Chip number prefix</td>
<td><code>bg-primary/25 rounded-sm px-1 mr-2 text-xs font-extrabold</code></td>
<td class="px">12px / 800</td>
<td class="note">"1", "2", … — for quick scanning</td>
</tr>
</table>
<h3 class="ix">"Nur diese Datei" card (per-file scope)</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Card container</td>
<td><code>bg-accent/20 border border-accent rounded-sm p-4 mb-4</code></td>
<td class="px">padding 16px</td>
<td class="note">mint tint signals "different per file"</td>
</tr>
<tr>
<td>Scope badge</td>
<td><code>bg-primary/90 text-accent rounded-sm px-2 py-1 text-xs font-extrabold uppercase tracking-wide</code></td>
<td class="px">12px · 800</td>
<td class="note">Paraglide key: <code>bulk_only_this_file</code></td>
</tr>
<tr>
<td>Title input</td>
<td><code>h-11 text-base font-semibold text-ink bg-white border border-line rounded-sm px-3 focus-visible:border-ink focus-visible:ring-2 focus-visible:ring-ink/20</code></td>
<td class="px">44px min-height · 16px</td>
<td class="note">pre-filled from filename <em>without extension</em></td>
</tr>
</table>
<h3 class="ix">"Gilt für alle" card (shared scope)</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Card container</td>
<td><code>bg-surface border border-line rounded-sm p-4 mb-3</code></td>
<td class="px">padding 16px</td>
<td class="note">neutral (no accent tint)</td>
</tr>
<tr>
<td>Scope badge</td>
<td><code>bg-accent text-primary rounded-sm px-2 py-1 text-xs font-extrabold uppercase tracking-wide</code></td>
<td class="px">12px · 800</td>
<td class="note">Paraglide: <code>bulk_shared_count</code> ("Gilt für alle {count}")</td>
</tr>
<tr>
<td>Field grid</td>
<td><code>grid grid-cols-1 md:grid-cols-2 gap-3</code></td>
<td class="px">12px gap</td>
<td class="note">single column at 320px, two at ≥ 768px</td>
</tr>
</table>
<h3 class="ix">Save bar</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Primary save button</td>
<td><code>h-11 px-5 bg-green-700 hover:bg-green-800 text-white font-extrabold rounded-sm text-sm focus-visible:ring-2 focus-visible:ring-green-900</code></td>
<td class="px">44px min · 14px</td>
<td class="note">label <code>{count} speichern →</code> (plural-aware Paraglide)</td>
</tr>
<tr>
<td>"Als Platzhalter" (outline)</td>
<td><code>h-11 px-4 border border-line bg-white text-ink-3 font-bold rounded-sm text-sm</code></td>
<td class="px">44px</td>
<td class="note">posts with <code>metadataComplete=false</code> for all</td>
</tr>
</table>
<h3 class="ix">Responsive collapse (≤ 767px)</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Panel mode switch</td>
<td>reuses DocumentEditLayout's existing tab collapse — "Vorschau / Angaben" tabs</td>
<td class="px">tab height 48px</td>
<td class="note">already shipped with #294</td>
</tr>
<tr>
<td>File switcher stays on "Vorschau" tab</td>
<td><code>snap-x snap-mandatory overflow-x-auto</code></td>
<td class="px">h 44px</td>
<td class="note">horizontal swipe; arrow buttons removed at mobile</td>
</tr>
</table>
<div class="notes">
<div class="nh">Interactions + behaviour</div>
<ul>
<li><strong>Drop a file after the initial batch</strong>: append to the end of the list and switch focus to the newly added file. No modal, no confirmation.</li>
<li><strong>Remove a file</strong> (X on the chip) → confirm only if it's the currently-previewed one; otherwise silent. When count drops to 1 the switcher strip animates away (200ms); when it drops to 0 we redirect back to the drop-zone state.</li>
<li><strong>Title auto-fill</strong>: <code>filename.replace(/\.(pdf|jpe?g|png|tiff?)$/i, '').replace(/[_-]+/g, ' ').trim()</code>. Marks the title input as <code>suggested</code> until the user edits it (mint left border, same treatment as #294's filename-derived fields).</li>
<li><strong>Title field visibility</strong>: always rendered (never collapsed) even in single-file mode, so there's zero layout jump when N changes from 1 to 2.</li>
<li><strong>Save flow</strong>: single POST to <code>/api/documents/quick-upload</code> with N files + JSON metadata object containing shared fields + titles array. Backend maps title[i] to files[i] by index. Response splits into <code>created[] / updated[] / errors[]</code> — show a summary toast + inline error markers per file for the <code>errors[]</code> list.</li>
<li><strong>Keyboard navigation</strong>: <kbd></kbd>/<kbd></kbd> on the switcher strip moves file focus; <kbd>Tab</kbd> cycles through form fields inside whichever card is active; <kbd>Esc</kbd> on the discard button opens the confirm dialog.</li>
<li><strong>Focus management on file switch</strong>: when the user clicks a different file, the title input of the new file receives focus automatically (so the main editable field is always reachable).</li>
<li><strong>Progress indicator during save</strong>: replace the save button with a determinate progress bar showing "Lade Datei 3 von 5…" for batches that take > 500ms.</li>
</ul>
</div>
<div class="notes" style="margin-top:14px;border-left-color:#C0392B">
<div class="nh" style="color:#C0392B">Edge cases + a11y</div>
<ul>
<li><strong>Duplicate filenames in the batch</strong>: accept, but show a warning icon next to both — backend will create both with unique IDs.</li>
<li><strong>Mixed content types</strong>: PDF + image in the same batch is fine; the preview panel renders whichever the active file is (DocumentEditLayout already handles both).</li>
<li><strong>Large batches (> 20 files)</strong>: the switcher strip becomes scrollable; consider a "Jump to file…" combobox at > 30 files (out of scope for v1).</li>
<li><strong>Upload failure per file</strong>: mark the chip red (<code>bg-red-600/20 text-red-800 border border-red-600</code>), show inline error in the chip's tooltip, don't block the rest of the batch from retrying.</li>
<li><strong>Screen reader announcement</strong>: when file count changes, fire a polite live region announce — "5 Dateien bereit zum Speichern" via <code>role="status" aria-live="polite"</code>.</li>
<li><strong>Colour-alone warning</strong>: active file chip uses color + <code>aria-current="true"</code> + a ▸ caret prefix so it's distinguishable for color-blind users.</li>
</ul>
</div>
</div>
</div>
</body>
</html>