Files
familienarchiv/docs/specs/bulk-upload-split-panel-spec.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

1685 lines
101 KiB
HTML
Raw 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 · Split-Panel Spec · 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:1320px;margin:0 auto;padding:48px 32px 120px}
/* ── Masthead ── */
.mh{padding-bottom:24px;border-bottom:3px solid #012851;margin-bottom:54px}
.mh .kicker{font-size:9px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#A6DAD8}
.mh h1{font-size:30px;font-weight:900;color:#012851;letter-spacing:-.5px;margin-top:6px}
.mh p{font-size:13.5px;color:#555;max-width:820px;line-height:1.8;margin-top:12px}
.mh .byline{font-size:9px;color:#999;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;margin-top:16px}
.tag-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
.tag{background:#012851;color:#A6DAD8;padding:3px 9px;border-radius:2px;font-size:8.5px;font-weight:800;letter-spacing:.8px;text-transform:uppercase}
.tag.green{background:#1e5e34;color:#d1fae5}
.tag.mint{background:#A6DAD8;color:#012851}
.tag.amber{background:#7c4a00;color:#fde68a}
/* ── Section rhythm ── */
.section{margin-bottom:92px}
.section-head{margin-bottom:32px}
.section-kicker{font-size:9px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#A6DAD8;display:block;margin-bottom:6px}
.section-head h2{font-family:'Merriweather',Georgia,serif;font-size:26px;font-weight:700;color:#012851;letter-spacing:-.3px}
.section-head p{font-size:13px;color:#555;line-height:1.75;max-width:780px;margin-top:10px}
/* ── State label chip ── */
.state-row{display:flex;align-items:center;gap:10px;margin-bottom:18px;flex-wrap:wrap}
.state-chip{background:#012851;color:#fff;padding:4px 10px;border-radius:2px;font-size:9px;font-weight:800;letter-spacing:.8px;text-transform:uppercase}
.state-chip.empty{background:#6b7280}
.state-chip.single{background:#1A7040}
.state-chip.multi{background:#012851}
.viewport-chip{background:#F0EEE8;color:#4b5563;padding:4px 10px;border-radius:2px;font-size:9px;font-weight:800;letter-spacing:.8px;text-transform:uppercase;border:1px solid #E4E2D7}
.theme-chip{padding:4px 10px;border-radius:2px;font-size:9px;font-weight:800;letter-spacing:.8px;text-transform:uppercase}
.theme-chip.light{background:#fff;color:#012851;border:1px solid #E4E2D7}
.theme-chip.dark{background:#011526;color:#f0efe9;border:1px solid #0d3358}
.state-note{font-size:11.5px;color:#888;margin-left:6px}
/* ═══════════════════════════════════════════════════════════════════════ */
/* ══ COMMON MOCKUP COMPONENTS ══ */
/* ═══════════════════════════════════════════════════════════════════════ */
.screen{margin:0 auto 28px}
.screen.w-desktop{max-width:1040px}
.screen.w-tablet{max-width:720px}
.screen.w-mobile{max-width:380px}
.chrome{border-radius:8px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.12);border:1.5px solid rgba(0,0,0,.08)}
/* ── Theme tokens ── */
.chrome.theme-light{
--c-canvas:#f0efe9; --c-surface:#ffffff; --c-muted:#f5f4ef; --c-overlay:#ffffff;
--c-line:#e4e2d7; --c-line-2:#eeede8;
--c-ink:#012851; --c-ink-2:#4b5563; --c-ink-3:#6b7280;
--c-accent:#a1dcd8; --c-accent-bg:rgba(161,220,216,.20);
--c-primary:#012851; --c-primary-fg:#ffffff;
--c-header:#012851;
--c-pdf-bg:#ebebeb; --c-pdf-ctrl:#d8d8d8; --c-pdf-text:#333333;
--c-danger:#c0392b;
--c-chrome-bg:#E8E6E0; --c-chrome-border:#C4C0BA;
--c-turquoise:#00c7b1;
}
.chrome.theme-dark{
--c-canvas:#010e1e; --c-surface:#011526; --c-muted:#011a30; --c-overlay:#011e38;
--c-line:#0d3358; --c-line-2:#092843;
--c-ink:#f0efe9; --c-ink-2:#9ca3af; --c-ink-3:#8b97a5;
--c-accent:#00c7b1; --c-accent-bg:rgba(0,199,177,.15);
--c-primary:#a1dcd8; --c-primary-fg:#012851;
--c-header:#012851;
--c-pdf-bg:#010e1e; --c-pdf-ctrl:#011526; --c-pdf-text:#f0efe9;
--c-danger:#e55347;
--c-chrome-bg:#0b1a2e; --c-chrome-border:#1a2d47;
--c-turquoise:#00c7b1;
}
/* ── Chrome bar ── */
.bar{height:22px;background:var(--c-chrome-bg);border-bottom:1px solid var(--c-chrome-border);display:flex;align-items:center;padding:0 9px;gap:5px}
.bar .dot{width:7px;height:7px;border-radius:50%;background:var(--c-chrome-border);opacity:.7}
.bar .url{flex:1;height:10px;background:var(--c-chrome-border);border-radius:5px;margin-left:8px;opacity:.6}
.bar .vp{font-size:7.5px;font-weight:800;color:var(--c-accent);letter-spacing:1px;text-transform:uppercase;padding:3px 8px;background:var(--c-header);border-radius:2px;margin-left:8px}
/* ── App nav ── */
.app-nav{height:32px;background:var(--c-header);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 var(--c-accent);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,.6)}
/* ── Top bar ── */
.topbar{height:40px;background:var(--c-surface);border-bottom:1px solid var(--c-line);display:flex;align-items:center;padding:0 14px;gap:10px;flex-shrink:0}
.tb-back{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:var(--c-ink-3);display:flex;align-items:center;gap:4px}
.tb-title{font-family:'Merriweather',Georgia,serif;font-size:10px;font-weight:700;color:var(--c-ink)}
.tb-count{background:var(--c-accent);color:var(--c-primary);padding:3px 8px;border-radius:10px;font-size:7px;font-weight:800;letter-spacing:.3px}
.chrome.theme-dark .tb-count{background:var(--c-primary);color:var(--c-primary-fg)}
.tb-discard{margin-left:auto;font-size:7.5px;font-weight:700;color:var(--c-danger);letter-spacing:.2px}
/* ── Split ── */
.split{display:flex;min-height:auto;background:var(--c-canvas)}
.pdf-panel{flex:55;background:var(--c-pdf-bg);display:flex;flex-direction:column;border-right:1px solid var(--c-line)}
.form-panel{flex:45;background:var(--c-surface);display:flex;flex-direction:column}
.chrome.theme-dark .pdf-panel{background:#010e1e;border-right:1px solid #0d3358}
/* ── PDF toolbar ── */
.pdf-toolbar{height:28px;background:var(--c-pdf-ctrl);display:flex;align-items:center;padding:0 10px;gap:8px}
.chrome.theme-dark .pdf-toolbar{background:#011526;border-bottom:1px solid #0d3358}
.pdf-btn{width:16px;height:16px;border-radius:2px;background:rgba(0,0,0,.08);display:flex;align-items:center;justify-content:center;font-size:7px;color:var(--c-ink-3)}
.chrome.theme-dark .pdf-btn{background:rgba(255,255,255,.08);color:rgba(240,239,233,.5)}
.pdf-page{font-size:6.5px;color:var(--c-ink-3);margin-left:auto;font-weight:700;letter-spacing:.5px}
.chrome.theme-dark .pdf-page{color:rgba(240,239,233,.45)}
/* ── PDF view ── */
.pdf-view{flex:1;display:flex;justify-content:center;align-items:center;padding:18px;overflow:hidden;position:relative}
.pdf-paper{background:#FFFEF8;box-shadow:0 3px 14px rgba(0,0,0,.28);border-radius:1px;padding:14px 16px;display:flex;flex-direction:column;gap:0;width:195px;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}
/* ── Empty-state drop zone (in PDF panel) ── */
.drop-zone{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:22px;gap:14px;text-align:center}
.drop-zone-box{border:2px dashed var(--c-accent);border-radius:6px;padding:36px 22px;background:rgba(255,255,255,.04);display:flex;flex-direction:column;align-items:center;gap:12px;max-width:340px;width:100%}
.chrome.theme-light .drop-zone-box{background:rgba(255,255,255,.50)}
.drop-icon{width:56px;height:56px;border-radius:50%;background:var(--c-accent);display:flex;align-items:center;justify-content:center;color:var(--c-primary);font-size:24px;font-weight:800}
.chrome.theme-dark .drop-icon{color:var(--c-primary-fg)}
.drop-title{font-family:'Merriweather',Georgia,serif;font-size:13px;font-weight:700;color:var(--c-ink)}
.chrome.theme-dark .drop-title{color:var(--c-ink)}
.drop-sub{font-size:9px;color:var(--c-ink-2);line-height:1.6}
.drop-sub strong{color:var(--c-ink);font-weight:800}
.drop-browse{background:var(--c-primary);color:var(--c-primary-fg);padding:8px 16px;border-radius:2px;font-size:8px;font-weight:800;letter-spacing:.5px;text-transform:uppercase}
.chrome.theme-dark .drop-browse{background:var(--c-primary);color:var(--c-primary-fg)}
.drop-formats{font-size:7.5px;color:var(--c-ink-3);letter-spacing:.2px;margin-top:4px}
/* ── File switcher strip ── */
.filebar{background:var(--c-pdf-ctrl);border-top:1px solid var(--c-line);display:flex;align-items:center;padding:0 8px;gap:4px;height:38px;flex-shrink:0}
.chrome.theme-dark .filebar{background:#011526;border-top:1px solid #0d3358}
.fb-arrow{width:20px;height:24px;border-radius:2px;background:rgba(0,0,0,.06);display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--c-ink-3)}
.chrome.theme-dark .fb-arrow{background:rgba(255,255,255,.08);color:rgba(240,239,233,.6)}
.fb-track{flex:1;display:flex;gap:4px;padding:0 4px;overflow:hidden}
.fb-item{padding:4px 7px;border-radius:2px;font-size:6.5px;font-weight:700;color:var(--c-ink-2);background:rgba(0,0,0,.04);display:flex;align-items:center;gap:4px;white-space:nowrap;height:24px}
.chrome.theme-dark .fb-item{background:rgba(255,255,255,.06);color:rgba(240,239,233,.55)}
.fb-item.on{background:var(--c-accent);color:var(--c-primary)}
.chrome.theme-dark .fb-item.on{background:var(--c-primary);color:var(--c-primary-fg)}
.fb-item.err{background:rgba(192,57,43,.15);color:var(--c-danger);border:1px dashed var(--c-danger)}
.fb-num{background:rgba(0,0,0,.18);border-radius:2px;padding:1px 4px;font-size:6px;font-weight:800;color:inherit;opacity:.85}
.fb-item.on .fb-num{background:rgba(0,0,0,.25)}
.chrome.theme-dark .fb-item.on .fb-num{background:rgba(1,40,81,.3)}
/* ── Form scroll ── */
.form-scroll{flex:1;overflow-y:auto;padding:14px}
/* ── Per-file card ── */
.only-card{background:var(--c-accent-bg);border:1.5px solid var(--c-accent);border-radius:3px;padding:11px 13px;margin-bottom:12px}
.chrome.theme-dark .only-card{background:rgba(0,199,177,.08);border-color:var(--c-accent)}
.only-head{display:flex;align-items:center;gap:7px;margin-bottom:8px;flex-wrap:wrap}
.only-badge{background:var(--c-primary);color:var(--c-primary-fg);padding:2px 7px;border-radius:2px;font-size:6.5px;font-weight:800;letter-spacing:.4px;text-transform:uppercase}
.only-sub{font-size:7px;color:var(--c-ink-2);font-weight:700;letter-spacing:.2px}
.only-sub strong{color:var(--c-ink);font-weight:800}
/* ── Shared card ── */
.shared-card{background:var(--c-muted);border:1px solid var(--c-line);border-radius:3px;padding:11px 13px;margin-bottom:10px}
.shared-head{display:flex;align-items:center;gap:7px;margin-bottom:9px}
.shared-badge{background:var(--c-accent);color:var(--c-primary);padding:2px 7px;border-radius:2px;font-size:6.5px;font-weight:800;letter-spacing:.4px;text-transform:uppercase}
.chrome.theme-dark .shared-badge{background:var(--c-primary);color:var(--c-primary-fg)}
.shared-sub{font-size:7px;color:var(--c-ink-2);font-weight:600}
/* ── Form rows ── */
.row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px}
.row.full{grid-template-columns:1fr}
.field{display:flex;flex-direction:column;gap:3px}
.f-label{font-size:6.5px;font-weight:700;color:var(--c-ink-2);letter-spacing:.2px;text-transform:uppercase}
.f-req{color:var(--c-danger)}
.f-input{height:22px;border:1px solid var(--c-line);border-radius:2px;background:var(--c-surface);font-size:7.5px;padding:0 7px;color:var(--c-ink);display:flex;align-items:center}
.f-input.tall{height:28px;font-weight:600}
.f-input.filled{color:var(--c-ink);font-weight:600;background:var(--c-surface)}
.f-input.empty{color:var(--c-ink-3);font-style:italic}
.f-input.suggested{border-color:var(--c-accent);background:var(--c-accent-bg);color:var(--c-ink);font-weight:600}
.f-input.focus{border-color:var(--c-ink);box-shadow:0 0 0 2px rgba(1,40,81,.18)}
.chrome.theme-dark .f-input.focus{border-color:var(--c-accent);box-shadow:0 0 0 2px rgba(161,220,216,.25)}
.f-tags{display:flex;gap:3px;flex-wrap:wrap;min-height:22px;border:1px solid var(--c-line);border-radius:2px;padding:3px 5px;background:var(--c-surface);align-items:center}
.f-chip{background:var(--c-primary);color:var(--c-primary-fg);border-radius:2px;font-size:6.5px;font-weight:700;padding:2px 5px 2px 6px;display:flex;align-items:center;gap:3px}
.f-chip .rm{color:rgba(255,255,255,.55);font-weight:400}
.chrome.theme-dark .f-chip .rm{color:rgba(1,40,81,.55)}
/* ── Action bar ── */
.action-bar{height:48px;background:var(--c-surface);border-top:1px solid var(--c-line);display:flex;align-items:center;padding:0 14px;gap:8px;flex-shrink:0}
.btn-skip{font-size:7.5px;font-weight:700;color:var(--c-ink-3);letter-spacing:.2px}
.btn-spacer{flex:1}
.btn-outline{height:26px;padding:0 12px;border:1px solid var(--c-line);border-radius:2px;font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:var(--c-ink-2);display:flex;align-items:center;background:var(--c-surface)}
.btn-primary{height:26px;padding:0 12px;border-radius:2px;font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;background:var(--c-primary);color:var(--c-primary-fg);display:flex;align-items:center;gap:4px}
.btn-primary.green{background:#1A7040;color:#fff}
.chrome.theme-dark .btn-primary.green{background:#0d5028}
/* ── Mobile-only: tab switcher replaces split ── */
.mtabs{display:flex;background:var(--c-surface);border-bottom:1px solid var(--c-line);height:38px;flex-shrink:0}
.mtab{flex:1;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:800;color:var(--c-ink-3);letter-spacing:.5px;text-transform:uppercase;gap:5px}
.mtab.on{color:var(--c-ink);border-bottom:2px solid var(--c-accent)}
.chrome.theme-dark .mtab.on{border-bottom-color:var(--c-primary)}
.mtab-pill{background:var(--c-accent);color:var(--c-primary);padding:2px 5px;border-radius:8px;font-size:6.5px;font-weight:800}
.chrome.theme-dark .mtab-pill{background:var(--c-primary);color:var(--c-primary-fg)}
/* ── Annotation callouts ── */
.callouts{margin-top:20px;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}
.callout{background:#fff;border:1px solid #E4E2D7;border-radius:3px;padding:10px 13px}
.co-num{background:#012851;color:#fff;width:18px;height:18px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;margin-right:6px}
.co-label{font-size:9px;font-weight:800;letter-spacing:.4px;text-transform:uppercase;color:#012851}
.co-text{font-size:11px;color:#555;line-height:1.55;margin-top:6px}
/* ─── Decision / impl sections ─── */
.impl{background:#fff;border:1px solid #E4E2D7;border-radius:4px;padding:28px 32px;box-shadow:0 1px 3px rgba(0,0,0,.04);margin-top:48px}
.impl h2{font-size:11px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#012851;margin-bottom:16px}
.impl h3{font-family:'Merriweather',Georgia,serif;font-size:17px;color:#012851;margin:26px 0 12px}
.impl h3:first-of-type{margin-top:0}
.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:#012851;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:#012851;width:22%}
.impl-table td code{font-family:'SF Mono','Menlo',monospace;font-size:10.5px;background:#F0EEE8;padding:1px 5px;border-radius:2px;color:#012851;display:inline-block;line-height:1.7}
.impl-table td.px{color:#777;font-size:11.5px;width:14%}
.impl-table td.note{color:#888;font-size:11.5px;font-style:italic;width:22%}
/* ── Callout box (interactions) ── */
.notes{background:#F9F8F5;border-left:3px solid #A6DAD8;padding:16px 22px;border-radius:0 4px 4px 0;margin-top:26px}
.notes.danger{border-left-color:#C0392B}
.notes .nh{font-size:9px;font-weight:800;letter-spacing:1px;text-transform:uppercase;color:#012851;margin-bottom:8px}
.notes.danger .nh{color:#C0392B}
.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}
.notes.danger li::before{color:#C0392B}
.notes li code{font-family:'SF Mono','Menlo',monospace;font-size:11px;background:#F0EEE8;padding:1px 5px;border-radius:2px;color:#012851}
.state-diagram{background:#fff;border:1px solid #E4E2D7;border-radius:4px;padding:26px 32px;box-shadow:0 1px 3px rgba(0,0,0,.04);margin-bottom:48px}
.state-diagram h2{font-size:11px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#012851;margin-bottom:12px}
.states{display:flex;align-items:center;gap:10px;margin-top:20px;flex-wrap:wrap}
.state-node{flex:1;min-width:200px;background:#F9F8F5;border:1.5px solid #E4E2D7;border-radius:4px;padding:16px 18px}
.state-node.empty{border-color:#6b7280}
.state-node.single{border-color:#1A7040}
.state-node.multi{border-color:#012851}
.sn-label{font-size:9px;font-weight:800;letter-spacing:.8px;text-transform:uppercase;color:#012851;margin-bottom:6px}
.sn-title{font-family:'Merriweather',Georgia,serif;font-size:14px;color:#012851;font-weight:700;margin-bottom:8px}
.sn-desc{font-size:11.5px;color:#555;line-height:1.6}
.state-arrow{font-size:24px;color:#A6DAD8;font-weight:800}
/* Viewport grid */
.vp-grid{display:grid;grid-template-columns:1fr 1fr;gap:32px;align-items:start}
.vp-grid.three{grid-template-columns:1fr 1fr 1fr}
@media (max-width:980px){.vp-grid,.vp-grid.three{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="doc">
<!-- ════════════════════════════════════════════ -->
<!-- ═══════════════ MASTHEAD ══════════════ -->
<!-- ════════════════════════════════════════════ -->
<div class="mh">
<div class="kicker">UI/UX Spec · Implementation-ready</div>
<h1>Bulk upload — split-panel with file switcher</h1>
<p>
Extends the <strong>#294 split-panel</strong> layout so the same screen covers 0 files, 1 file, or N files.
When the user drops multiple PDFs, each gets its own title (pre-filled from the filename, editable) while
every other metadata field — sender, receiver, date, location, tags, archive box — is shared across all
documents. A single POST to <code style="font-family:'SF Mono',monospace;background:#F0EEE8;padding:1px 5px;border-radius:2px;font-size:12px">/api/documents/quick-upload</code> creates N documents in one pass.
</p>
<div class="byline">Leonie Voss · 2026-04-24 · Final spec · Supersedes the 3-concept exploration</div>
<div class="tag-row">
<span class="tag">feature</span>
<span class="tag mint">ui</span>
<span class="tag">a11y 320px+</span>
<span class="tag">light + dark</span>
<span class="tag green">backend ready</span>
</div>
</div>
<!-- ════════════════════════════════════════════ -->
<!-- ═══════════ STATE DIAGRAM ════════════════ -->
<!-- ════════════════════════════════════════════ -->
<div class="state-diagram">
<h2>The one-screen model</h2>
<p style="font-size:13px;color:#555;line-height:1.75;max-width:780px">
<strong>/documents/new</strong> is a single route with three visual states. The same DOM skeleton
renders for all three — only three pieces of chrome appear/disappear based on file count:
the PDF preview area, the file-switcher strip, and the per-file title scope card.
</p>
<div class="states">
<div class="state-node empty">
<div class="sn-label">N = 0</div>
<div class="sn-title">Empty</div>
<div class="sn-desc">
Left panel shows a drop zone with copy: "Eine oder mehrere Dateien ablegen…".
Right panel shows the shared metadata form, pre-disabled until a file lands.
No per-file title card. No file switcher.
</div>
</div>
<div class="state-arrow"></div>
<div class="state-node single">
<div class="sn-label">N = 1</div>
<div class="sn-title">Single file</div>
<div class="sn-desc">
Left = PDF preview. Right = title card + shared card.
<strong>Byte-identical to the shipped #294 layout.</strong> No file switcher,
no "1/1" subtitles — this state should feel unchanged to existing users.
</div>
</div>
<div class="state-arrow"></div>
<div class="state-node multi">
<div class="sn-label">N ≥ 2</div>
<div class="sn-title">Multiple files</div>
<div class="sn-desc">
Left = PDF preview + file-switcher strip along the bottom of the PDF panel.
Right = per-file title card ("Nur diese Datei · 1/5") + shared card ("Gilt für alle 5").
Save button reads "5 speichern →". One multipart POST on save.
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════ -->
<!-- ══════════ STATE 1: EMPTY ══════════════ -->
<!-- ════════════════════════════════════════════ -->
<section class="section">
<div class="section-head">
<span class="section-kicker">State 1 of 3</span>
<h2>Empty state — drop zone with bulk-first copy</h2>
<p>
When the user hits <code>/documents/new</code> the PDF panel is the drop target. It's not a tiny button —
the <em>whole left panel</em> is the affordance. The copy makes it explicit that the user can drop one
file or many; the supporting line lists accepted formats and the per-file size limit. Right panel shows
the shared form in a muted "waiting" state so the user sees what they'll need to fill in once files
land.
</p>
</div>
<div class="state-row">
<span class="state-chip empty">Empty · N = 0</span>
<span class="viewport-chip">desktop · 1280</span>
<span class="theme-chip light">Light</span>
</div>
<!-- Desktop · Empty · Light -->
<div class="screen w-desktop">
<div class="chrome theme-light">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">1280 · Desktop · Light</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="topbar">
<div class="tb-back">← Dokumente</div>
<div class="tb-title">Neues Dokument</div>
</div>
<div class="split" style="min-height:440px">
<div class="pdf-panel">
<div class="pdf-toolbar"><div class="pdf-page">Keine Datei ausgewählt</div></div>
<div class="drop-zone">
<div class="drop-zone-box">
<div class="drop-icon"></div>
<div class="drop-title">Eine oder mehrere Dateien ablegen</div>
<div class="drop-sub">
Für jede Datei wird ein eigenes Dokument erstellt.<br/>
<strong>Der Titel</strong> wird aus dem Dateinamen vorausgefüllt und ist pro Datei
editierbar — <strong>alle anderen Felder</strong> gelten für alle Dokumente gemeinsam.
</div>
<div class="drop-browse">Dateien auswählen</div>
<div class="drop-formats">PDF · JPEG · PNG · TIFF &nbsp;·&nbsp; max 50 MB pro Datei</div>
</div>
</div>
</div>
<div class="form-panel">
<div class="form-scroll">
<div class="shared-card" style="opacity:.6">
<div class="shared-head">
<span class="shared-badge">Gilt für alle</span>
<span class="shared-sub">Gemeinsame Angaben</span>
</div>
<div class="row">
<div class="field"><span class="f-label">Absender</span><div class="f-input empty"></div></div>
<div class="field"><span class="f-label">Empfänger</span><div class="f-input empty"></div></div>
</div>
<div class="row">
<div class="field"><span class="f-label">Datum</span><div class="f-input empty"></div></div>
<div class="field"><span class="f-label">Ort</span><div class="f-input empty"></div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Tags</span><div class="f-tags"></div></div>
</div>
<div class="row">
<div class="field"><span class="f-label">Archivbox</span><div class="f-input empty"></div></div>
<div class="field"><span class="f-label">Mappe</span><div class="f-input empty"></div></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary" style="opacity:.4">Speichern →</div>
</div>
</div>
</div>
</div>
</div>
<!-- Desktop · Empty · Dark -->
<div class="state-row">
<span class="state-chip empty">Empty · N = 0</span>
<span class="viewport-chip">desktop · 1280</span>
<span class="theme-chip dark">Dark</span>
</div>
<div class="screen w-desktop">
<div class="chrome theme-dark">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">1280 · Desktop · Dark</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="topbar">
<div class="tb-back">← Dokumente</div>
<div class="tb-title">Neues Dokument</div>
</div>
<div class="split" style="min-height:440px">
<div class="pdf-panel">
<div class="pdf-toolbar"><div class="pdf-page">Keine Datei ausgewählt</div></div>
<div class="drop-zone">
<div class="drop-zone-box">
<div class="drop-icon"></div>
<div class="drop-title">Eine oder mehrere Dateien ablegen</div>
<div class="drop-sub">
Für jede Datei wird ein eigenes Dokument erstellt.<br/>
<strong>Der Titel</strong> wird aus dem Dateinamen vorausgefüllt und ist pro Datei
editierbar — <strong>alle anderen Felder</strong> gelten für alle Dokumente gemeinsam.
</div>
<div class="drop-browse">Dateien auswählen</div>
<div class="drop-formats">PDF · JPEG · PNG · TIFF &nbsp;·&nbsp; max 50 MB pro Datei</div>
</div>
</div>
</div>
<div class="form-panel">
<div class="form-scroll">
<div class="shared-card" style="opacity:.6">
<div class="shared-head">
<span class="shared-badge">Gilt für alle</span>
<span class="shared-sub">Gemeinsame Angaben</span>
</div>
<div class="row">
<div class="field"><span class="f-label">Absender</span><div class="f-input empty"></div></div>
<div class="field"><span class="f-label">Empfänger</span><div class="f-input empty"></div></div>
</div>
<div class="row">
<div class="field"><span class="f-label">Datum</span><div class="f-input empty"></div></div>
<div class="field"><span class="f-label">Ort</span><div class="f-input empty"></div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Tags</span><div class="f-tags"></div></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary" style="opacity:.4">Speichern →</div>
</div>
</div>
</div>
</div>
</div>
<!-- Mobile · Empty · Light (single-panel collapse) -->
<div class="state-row">
<span class="state-chip empty">Empty · N = 0</span>
<span class="viewport-chip">mobile · 375</span>
<span class="theme-chip light">Light</span>
<span class="state-note">One-panel view — tabs appear only when a file is loaded</span>
</div>
<div class="screen w-mobile">
<div class="chrome theme-light">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">375 · Mob</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="topbar">
<div class="tb-back">← Zurück</div>
<div class="tb-title">Neues Dokument</div>
</div>
<div style="background:var(--c-canvas);min-height:460px">
<div class="pdf-panel" style="flex:1;border-right:none;border-bottom:1px solid var(--c-line)">
<div class="pdf-toolbar"><div class="pdf-page">Keine Datei ausgewählt</div></div>
<div class="drop-zone" style="min-height:280px">
<div class="drop-zone-box" style="padding:22px 16px;max-width:300px">
<div class="drop-icon" style="width:44px;height:44px;font-size:18px"></div>
<div class="drop-title" style="font-size:11px">Eine oder mehrere Dateien ablegen</div>
<div class="drop-sub" style="font-size:8.5px">
Jede Datei wird ein eigenes Dokument. Der Titel kommt aus dem Dateinamen —
alle anderen Felder gelten für alle.
</div>
<div class="drop-browse">Dateien auswählen</div>
<div class="drop-formats">PDF · JPEG · PNG · TIFF · max 50 MB</div>
</div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary" style="opacity:.4">Speichern →</div>
</div>
</div>
</div>
</section>
<!-- ════════════════════════════════════════════ -->
<!-- ══════════ STATE 2: SINGLE ═════════════ -->
<!-- ════════════════════════════════════════════ -->
<section class="section">
<div class="section-head">
<span class="section-kicker">State 2 of 3</span>
<h2>Single-file state — zero change from #294</h2>
<p>
When exactly one file is loaded, the screen is byte-identical to the #294 <code>DocumentEditLayout</code>
single-document flow. No file-switcher strip, no "1/1" subtitle, no "Alle verwerfen" link. This is
the invariant that makes the bulk extension a safe ship — existing users see no new chrome for the
single-file case they've always used.
</p>
</div>
<div class="state-row">
<span class="state-chip single">Single · N = 1</span>
<span class="viewport-chip">desktop · 1280</span>
<span class="theme-chip light">Light</span>
<span class="state-note">Reference only — no new components render here</span>
</div>
<div class="screen w-desktop">
<div class="chrome theme-light">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">1280 · Desktop · Light</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-nav-r"><div class="app-avatar">MR</div></div>
</div>
<div class="topbar">
<div class="tb-back">← Dokumente</div>
<div class="tb-title">Neues Dokument</div>
</div>
<div class="split" style="min-height:440px">
<div class="pdf-panel">
<div class="pdf-toolbar">
<div class="pdf-btn"></div><div class="pdf-btn"></div>
<div class="pdf-btn">+</div><div class="pdf-btn"></div>
<div class="pdf-page">Seite 1 / 2</div>
</div>
<div class="pdf-view">
<div class="pdf-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>
</div>
</div>
<div class="form-panel">
<div class="form-scroll">
<div class="field" style="margin-bottom:12px">
<span class="f-label">Titel <span class="f-req">*</span></span>
<div class="f-input tall filled">Brief an Anna, 1940</div>
</div>
<div class="shared-card">
<div class="shared-head">
<span class="shared-sub">Angaben</span>
</div>
<div class="row">
<div class="field"><span class="f-label">Absender</span><div class="f-input filled">Hans Müller</div></div>
<div class="field"><span class="f-label">Empfänger</span><div class="f-input filled">Anna Schmidt</div></div>
</div>
<div class="row">
<div class="field"><span class="f-label">Datum</span><div class="f-input filled">15.06.1940</div></div>
<div class="field"><span class="f-label">Ort</span><div class="f-input empty">Berlin</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Tags</span><div class="f-tags">
<span class="f-chip">Familie <span class="rm">×</span></span>
<span class="f-chip">Krieg <span class="rm">×</span></span>
</div></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-skip">Verwerfen</div>
<div class="btn-spacer"></div>
<div class="btn-outline">Als Platzhalter</div>
<div class="btn-primary green">Speichern →</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ════════════════════════════════════════════ -->
<!-- ═══════════ STATE 3: MULTI ═══════════ -->
<!-- ════════════════════════════════════════════ -->
<section class="section">
<div class="section-head">
<span class="section-kicker">State 3 of 3</span>
<h2>Multi-file state — file switcher + two-card form</h2>
<p>
When <strong>N ≥ 2</strong>, three things appear: (1) a count pill in the top bar "5 werden erstellt" plus an
"Alle verwerfen" link; (2) a file-switcher strip below the PDF toolbar, with the active file in a mint pill,
inactive files in subtle pills, and any upload-errored file in red-dashed; (3) a two-card form: the mint-tinted
<em>Nur diese Datei</em> card on top (title only), the neutral <em>Gilt für alle 5</em> card below
(everything else).
</p>
</div>
<!-- ═══════════ Desktop · Light ═══════════ -->
<div class="state-row">
<span class="state-chip multi">Multi · N = 5</span>
<span class="viewport-chip">desktop · 1280</span>
<span class="theme-chip light">Light</span>
</div>
<div class="screen w-desktop">
<div class="chrome theme-light">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">1280 · Desktop · Light</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="topbar">
<div class="tb-back">← Dokumente</div>
<div class="tb-title">Neue Dokumente</div>
<div class="tb-count">5 werden erstellt</div>
<div class="tb-discard">Alle verwerfen</div>
</div>
<div class="split" style="min-height:460px">
<div class="pdf-panel">
<div class="pdf-toolbar">
<div class="pdf-btn"></div><div class="pdf-btn"></div>
<div class="pdf-btn">+</div><div class="pdf-btn"></div>
<div class="pdf-page">Seite 1 / 2 · Datei 1 / 5</div>
</div>
<div class="pdf-view">
<div class="pdf-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>
</div>
<div class="filebar">
<div class="fb-arrow"></div>
<div class="fb-track">
<div class="fb-item on"><span class="fb-num">1</span>Brief_1940_Hans.pdf</div>
<div class="fb-item"><span class="fb-num">2</span>Brief_1940_Anna.pdf</div>
<div class="fb-item"><span class="fb-num">3</span>Brief_1941_Clara.pdf</div>
<div class="fb-item"><span class="fb-num">4</span>Postkarte_Venedig.jpg</div>
<div class="fb-item"><span class="fb-num">5</span>Urkunde_1942.pdf</div>
</div>
<div class="fb-arrow"></div>
</div>
</div>
<div class="form-panel">
<div class="form-scroll">
<div class="only-card">
<div class="only-head">
<span class="only-badge">Nur diese Datei</span>
<span class="only-sub"><strong>1 / 5</strong> · Brief_1940_Hans.pdf</span>
</div>
<div class="row full">
<div class="field">
<span class="f-label">Titel <span class="f-req">*</span></span>
<div class="f-input tall suggested">Brief an Anna, 1940</div>
</div>
</div>
</div>
<div class="shared-card">
<div class="shared-head">
<span class="shared-badge">Gilt für alle 5</span>
<span class="shared-sub">Gemeinsame Angaben</span>
</div>
<div class="row">
<div class="field"><span class="f-label">Absender</span><div class="f-input filled">Hans Müller</div></div>
<div class="field"><span class="f-label">Empfänger</span><div class="f-input filled">Anna Schmidt</div></div>
</div>
<div class="row">
<div class="field"><span class="f-label">Datum</span><div class="f-input filled">15.06.1940</div></div>
<div class="field"><span class="f-label">Ort</span><div class="f-input empty">z.B. Berlin</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Tags</span><div class="f-tags">
<span class="f-chip">Familie <span class="rm">×</span></span>
<span class="f-chip">Krieg <span class="rm">×</span></span>
<span class="f-chip">Briefwechsel <span class="rm">×</span></span>
</div></div>
</div>
<div class="row">
<div class="field"><span class="f-label">Archivbox</span><div class="f-input empty">z.B. B-12</div></div>
<div class="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">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>
<!-- Anatomy callouts -->
<div class="callouts">
<div class="callout">
<div><span class="co-num">1</span><span class="co-label">Count pill</span></div>
<div class="co-text">Mint background, navy text (7.2:1 AAA). Only visible at N ≥ 2. Live-region announces changes.</div>
</div>
<div class="callout">
<div><span class="co-num">2</span><span class="co-label">"Alle verwerfen"</span></div>
<div class="co-text">Danger-coloured link at the far right of the top bar. Triggers a confirm dialog; never a silent wipe.</div>
</div>
<div class="callout">
<div><span class="co-num">3</span><span class="co-label">File switcher</span></div>
<div class="co-text">Active file uses mint bg + aria-current + a ▸ caret. Three redundant cues for colour-blind users.</div>
</div>
<div class="callout">
<div><span class="co-num">4</span><span class="co-label">"Nur diese Datei"</span></div>
<div class="co-text">Mint-tinted card: the ONE thing that differs per file lives here. Currently just the title.</div>
</div>
<div class="callout">
<div><span class="co-num">5</span><span class="co-label">"Gilt für alle N"</span></div>
<div class="co-text">Neutral card with everything that applies to every document. The count is interpolated from N.</div>
</div>
<div class="callout">
<div><span class="co-num">6</span><span class="co-label">Save CTA</span></div>
<div class="co-text">"5 speichern" — count is plural-aware via Paraglide. Becomes a progress bar on slow saves.</div>
</div>
</div>
<!-- ═══════════ Desktop · Dark ═══════════ -->
<div class="state-row" style="margin-top:40px">
<span class="state-chip multi">Multi · N = 5</span>
<span class="viewport-chip">desktop · 1280</span>
<span class="theme-chip dark">Dark</span>
</div>
<div class="screen w-desktop">
<div class="chrome theme-dark">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">1280 · Desktop · Dark</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-nav-r"><div class="app-avatar">MR</div></div>
</div>
<div class="topbar">
<div class="tb-back">← Dokumente</div>
<div class="tb-title">Neue Dokumente</div>
<div class="tb-count">5 werden erstellt</div>
<div class="tb-discard">Alle verwerfen</div>
</div>
<div class="split" style="min-height:460px">
<div class="pdf-panel">
<div class="pdf-toolbar">
<div class="pdf-btn"></div><div class="pdf-btn"></div>
<div class="pdf-btn">+</div><div class="pdf-btn"></div>
<div class="pdf-page">Seite 1 / 2 · Datei 1 / 5</div>
</div>
<div class="pdf-view">
<div class="pdf-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>
</div>
<div class="filebar">
<div class="fb-arrow"></div>
<div class="fb-track">
<div class="fb-item on"><span class="fb-num">1</span>Brief_1940_Hans.pdf</div>
<div class="fb-item"><span class="fb-num">2</span>Brief_1940_Anna.pdf</div>
<div class="fb-item err"><span class="fb-num">3</span>Brief_1941_Clara.pdf ⚠</div>
<div class="fb-item"><span class="fb-num">4</span>Postkarte_Venedig.jpg</div>
<div class="fb-item"><span class="fb-num">5</span>Urkunde_1942.pdf</div>
</div>
<div class="fb-arrow"></div>
</div>
</div>
<div class="form-panel">
<div class="form-scroll">
<div class="only-card">
<div class="only-head">
<span class="only-badge">Nur diese Datei</span>
<span class="only-sub"><strong>1 / 5</strong> · Brief_1940_Hans.pdf</span>
</div>
<div class="row full">
<div class="field">
<span class="f-label">Titel <span class="f-req">*</span></span>
<div class="f-input tall suggested">Brief an Anna, 1940</div>
</div>
</div>
</div>
<div class="shared-card">
<div class="shared-head">
<span class="shared-badge">Gilt für alle 5</span>
<span class="shared-sub">Gemeinsame Angaben</span>
</div>
<div class="row">
<div class="field"><span class="f-label">Absender</span><div class="f-input filled">Hans Müller</div></div>
<div class="field"><span class="f-label">Empfänger</span><div class="f-input filled">Anna Schmidt</div></div>
</div>
<div class="row">
<div class="field"><span class="f-label">Datum</span><div class="f-input filled">15.06.1940</div></div>
<div class="field"><span class="f-label">Ort</span><div class="f-input empty">z.B. Berlin</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Tags</span><div class="f-tags">
<span class="f-chip">Familie <span class="rm">×</span></span>
<span class="f-chip">Krieg <span class="rm">×</span></span>
</div></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-skip">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>
<!-- ═══════════ Tablet · 768 · Light ═══════════ -->
<div class="state-row" style="margin-top:40px">
<span class="state-chip multi">Multi · N = 5</span>
<span class="viewport-chip">tablet · 768</span>
<span class="theme-chip light">Light</span>
<span class="state-note">Split is kept — PDF pane narrows, form stays readable</span>
</div>
<div class="screen w-tablet">
<div class="chrome theme-light">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">768 · Tab</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="topbar">
<div class="tb-back">← Dok</div>
<div class="tb-title">Neue Dokumente</div>
<div class="tb-count">5</div>
<div class="tb-discard">Verwerfen</div>
</div>
<div class="split" style="min-height:420px">
<div class="pdf-panel" style="flex:50">
<div class="pdf-toolbar">
<div class="pdf-btn"></div><div class="pdf-btn"></div>
<div class="pdf-page">1/5</div>
</div>
<div class="pdf-view" style="padding:10px">
<div class="pdf-paper" style="width:130px">
<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>
</div>
<div class="filebar">
<div class="fb-arrow"></div>
<div class="fb-track">
<div class="fb-item on"><span class="fb-num">1</span>Brief_Hans</div>
<div class="fb-item"><span class="fb-num">2</span>Brief_Anna</div>
<div class="fb-item"><span class="fb-num">3</span>Brief_Clara</div>
</div>
<div class="fb-arrow"></div>
</div>
</div>
<div class="form-panel" style="flex:50">
<div class="form-scroll">
<div class="only-card">
<div class="only-head">
<span class="only-badge">Diese Datei</span>
<span class="only-sub"><strong>1/5</strong></span>
</div>
<div class="field">
<span class="f-label">Titel <span class="f-req">*</span></span>
<div class="f-input tall suggested">Brief an Anna, 1940</div>
</div>
</div>
<div class="shared-card">
<div class="shared-head">
<span class="shared-badge">Alle 5</span>
</div>
<div class="row full">
<div class="field"><span class="f-label">Absender</span><div class="f-input filled">Hans Müller</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Empfänger</span><div class="f-input filled">Anna Schmidt</div></div>
</div>
<div class="row">
<div class="field"><span class="f-label">Datum</span><div class="f-input filled">1940</div></div>
<div class="field"><span class="f-label">Ort</span><div class="f-input empty"></div></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary green">5 speichern</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════ Tablet · 768 · Dark ═══════════ -->
<div class="state-row" style="margin-top:40px">
<span class="state-chip multi">Multi · N = 5</span>
<span class="viewport-chip">tablet · 768</span>
<span class="theme-chip dark">Dark</span>
</div>
<div class="screen w-tablet">
<div class="chrome theme-dark">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">768 · Tab · Dark</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="topbar">
<div class="tb-back">← Dok</div>
<div class="tb-title">Neue Dokumente</div>
<div class="tb-count">5</div>
<div class="tb-discard">Verwerfen</div>
</div>
<div class="split" style="min-height:420px">
<div class="pdf-panel" style="flex:50">
<div class="pdf-toolbar">
<div class="pdf-btn"></div><div class="pdf-btn"></div>
<div class="pdf-page">1/5</div>
</div>
<div class="pdf-view" style="padding:10px">
<div class="pdf-paper" style="width:130px">
<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>
</div>
<div class="filebar">
<div class="fb-arrow"></div>
<div class="fb-track">
<div class="fb-item on"><span class="fb-num">1</span>Brief_Hans</div>
<div class="fb-item"><span class="fb-num">2</span>Brief_Anna</div>
<div class="fb-item"><span class="fb-num">3</span>Brief_Clara</div>
</div>
<div class="fb-arrow"></div>
</div>
</div>
<div class="form-panel" style="flex:50">
<div class="form-scroll">
<div class="only-card">
<div class="only-head">
<span class="only-badge">Diese Datei</span>
<span class="only-sub"><strong>1/5</strong></span>
</div>
<div class="field">
<span class="f-label">Titel <span class="f-req">*</span></span>
<div class="f-input tall suggested">Brief an Anna, 1940</div>
</div>
</div>
<div class="shared-card">
<div class="shared-head">
<span class="shared-badge">Alle 5</span>
</div>
<div class="row full">
<div class="field"><span class="f-label">Absender</span><div class="f-input filled">Hans Müller</div></div>
</div>
<div class="row">
<div class="field"><span class="f-label">Datum</span><div class="f-input filled">1940</div></div>
<div class="field"><span class="f-label">Ort</span><div class="f-input empty"></div></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary green">5 speichern</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════ Mobile · 375 · Light ═══════════ -->
<div class="state-row" style="margin-top:40px">
<span class="state-chip multi">Multi · N = 5</span>
<span class="viewport-chip">mobile · 375</span>
<span class="theme-chip light">Light</span>
<span class="state-note">Split collapses into "Vorschau / Angaben" tabs — reuses DocumentEditLayout's pattern</span>
</div>
<div class="vp-grid">
<!-- Mobile · Vorschau tab -->
<div class="screen w-mobile" style="margin:0">
<div class="chrome theme-light">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">375 · Tab: Vorschau</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="topbar">
<div class="tb-back"></div>
<div class="tb-title">Neue Dokumente</div>
<div class="tb-count">5</div>
</div>
<div class="mtabs">
<div class="mtab on">Vorschau <span class="mtab-pill">1/5</span></div>
<div class="mtab">Angaben</div>
</div>
<div class="pdf-panel" style="flex:1;border-right:none">
<div class="pdf-toolbar">
<div class="pdf-btn"></div><div class="pdf-btn"></div>
<div class="pdf-page">Seite 1 / 2</div>
</div>
<div class="pdf-view" style="min-height:240px">
<div class="pdf-paper" style="width:150px">
<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"></div>
</div>
</div>
<div class="filebar">
<div class="fb-track">
<div class="fb-item on"><span class="fb-num">1</span>Brief_Hans</div>
<div class="fb-item"><span class="fb-num">2</span>Brief_Anna</div>
<div class="fb-item"><span class="fb-num">3</span>Brief_Clara</div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary green">5 speichern</div>
</div>
</div>
</div>
<!-- Mobile · Angaben tab -->
<div class="screen w-mobile" style="margin:0">
<div class="chrome theme-light">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">375 · Tab: Angaben</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="topbar">
<div class="tb-back"></div>
<div class="tb-title">Neue Dokumente</div>
<div class="tb-count">5</div>
</div>
<div class="mtabs">
<div class="mtab">Vorschau <span class="mtab-pill">1/5</span></div>
<div class="mtab on">Angaben</div>
</div>
<div class="form-panel" style="flex:1;background:var(--c-canvas)">
<div class="form-scroll">
<div class="only-card">
<div class="only-head">
<span class="only-badge">Datei 1/5</span>
<span class="only-sub">Brief_1940_Hans.pdf</span>
</div>
<div class="field">
<span class="f-label">Titel <span class="f-req">*</span></span>
<div class="f-input tall suggested">Brief an Anna, 1940</div>
</div>
</div>
<div class="shared-card">
<div class="shared-head">
<span class="shared-badge">Alle 5</span>
</div>
<div class="row full">
<div class="field"><span class="f-label">Absender</span><div class="f-input filled">Hans Müller</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Empfänger</span><div class="f-input filled">Anna Schmidt</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Datum</span><div class="f-input filled">15.06.1940</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Tags</span><div class="f-tags">
<span class="f-chip">Familie <span class="rm">×</span></span>
<span class="f-chip">Krieg <span class="rm">×</span></span>
</div></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary green">5 speichern</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════ Mobile · 375 · Dark ═══════════ -->
<div class="state-row" style="margin-top:40px">
<span class="state-chip multi">Multi · N = 5</span>
<span class="viewport-chip">mobile · 375</span>
<span class="theme-chip dark">Dark</span>
</div>
<div class="vp-grid">
<div class="screen w-mobile" style="margin:0">
<div class="chrome theme-dark">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">375 · Dark · Vorschau</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="topbar">
<div class="tb-back"></div>
<div class="tb-title">Neue Dokumente</div>
<div class="tb-count">5</div>
</div>
<div class="mtabs">
<div class="mtab on">Vorschau <span class="mtab-pill">1/5</span></div>
<div class="mtab">Angaben</div>
</div>
<div class="pdf-panel" style="flex:1;border-right:none">
<div class="pdf-toolbar">
<div class="pdf-btn"></div><div class="pdf-btn"></div>
<div class="pdf-page">Seite 1 / 2</div>
</div>
<div class="pdf-view" style="min-height:240px">
<div class="pdf-paper" style="width:150px">
<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>
</div>
<div class="filebar">
<div class="fb-track">
<div class="fb-item on"><span class="fb-num">1</span>Brief_Hans</div>
<div class="fb-item"><span class="fb-num">2</span>Brief_Anna</div>
<div class="fb-item"><span class="fb-num">3</span>Brief_Clara</div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary green">5 speichern</div>
</div>
</div>
</div>
<div class="screen w-mobile" style="margin:0">
<div class="chrome theme-dark">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">375 · Dark · Angaben</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="topbar">
<div class="tb-back"></div>
<div class="tb-title">Neue Dokumente</div>
<div class="tb-count">5</div>
</div>
<div class="mtabs">
<div class="mtab">Vorschau <span class="mtab-pill">1/5</span></div>
<div class="mtab on">Angaben</div>
</div>
<div class="form-panel" style="flex:1;background:var(--c-canvas)">
<div class="form-scroll">
<div class="only-card">
<div class="only-head">
<span class="only-badge">Datei 1/5</span>
<span class="only-sub">Brief_1940_Hans.pdf</span>
</div>
<div class="field">
<span class="f-label">Titel <span class="f-req">*</span></span>
<div class="f-input tall suggested">Brief an Anna, 1940</div>
</div>
</div>
<div class="shared-card">
<div class="shared-head">
<span class="shared-badge">Alle 5</span>
</div>
<div class="row full">
<div class="field"><span class="f-label">Absender</span><div class="f-input filled">Hans Müller</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Datum</span><div class="f-input filled">1940</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Tags</span><div class="f-tags">
<span class="f-chip">Familie <span class="rm">×</span></span>
<span class="f-chip">Krieg <span class="rm">×</span></span>
</div></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary green">5 speichern</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════ 320px edge ═══════════ -->
<div class="state-row" style="margin-top:40px">
<span class="state-chip multi">Multi · N = 5</span>
<span class="viewport-chip">mobile · 320</span>
<span class="theme-chip light">Light</span>
<span class="state-note">Narrowest supported viewport — same structure, tighter paddings</span>
</div>
<div class="screen" style="max-width:320px">
<div class="chrome theme-light">
<div class="bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<div class="url"></div><div class="vp">320</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="topbar">
<div class="tb-back"></div>
<div class="tb-title" style="font-size:9px">Neue Dokumente</div>
<div class="tb-count">5</div>
</div>
<div class="mtabs">
<div class="mtab">Vorschau <span class="mtab-pill">1/5</span></div>
<div class="mtab on">Angaben</div>
</div>
<div class="form-panel" style="flex:1;background:var(--c-canvas)">
<div class="form-scroll" style="padding:10px">
<div class="only-card" style="padding:9px 11px">
<div class="only-head">
<span class="only-badge">1/5</span>
<span class="only-sub" style="font-size:6.5px">Brief_Hans.pdf</span>
</div>
<div class="field">
<span class="f-label">Titel *</span>
<div class="f-input tall suggested">Brief an Anna, 1940</div>
</div>
</div>
<div class="shared-card" style="padding:9px 11px">
<div class="shared-head">
<span class="shared-badge">Alle 5</span>
</div>
<div class="row full">
<div class="field"><span class="f-label">Absender</span><div class="f-input filled">Hans Müller</div></div>
</div>
<div class="row full">
<div class="field"><span class="f-label">Empfänger</span><div class="f-input filled">Anna Schmidt</div></div>
</div>
</div>
</div>
<div class="action-bar">
<div class="btn-spacer"></div>
<div class="btn-primary green" style="padding:0 10px">5 speichern</div>
</div>
</div>
</div>
</div>
</section>
<!-- ════════════════════════════════════════════ -->
<!-- ══════════ IMPLEMENTATION REF ══════════ -->
<!-- ════════════════════════════════════════════ -->
<div class="impl">
<h2>Implementation reference — tokens, classes, behaviour</h2>
<h3>Empty-state drop zone (PDF panel, N = 0)</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Outer panel (drop target)</td>
<td><code>flex-1 bg-pdf-bg flex flex-col</code></td>
<td class="px">full height</td>
<td class="note">the entire left panel accepts drops, not just the inner box</td>
</tr>
<tr>
<td>Inner dashed box</td>
<td><code>border-2 border-dashed border-accent rounded-md p-9 max-w-[340px] bg-surface/50 dark:bg-white/5</code></td>
<td class="px">padding 36px</td>
<td class="note">visual anchor for mouse drops; not the hit target</td>
</tr>
<tr>
<td>Upload icon circle</td>
<td><code>w-14 h-14 rounded-full bg-accent text-primary flex items-center justify-center text-2xl font-extrabold</code></td>
<td class="px">56×56</td>
<td class="note">mint on white · primary on dark (token swap is automatic)</td>
</tr>
<tr>
<td>Headline</td>
<td><code>font-serif text-base font-bold text-ink</code></td>
<td class="px">16px · 700</td>
<td class="note">Paraglide <code>upload_dropzone_heading</code></td>
</tr>
<tr>
<td>Supporting copy</td>
<td><code>text-sm text-ink-2 leading-relaxed max-w-prose</code></td>
<td class="px">14px</td>
<td class="note">Paraglide <code>upload_dropzone_body</code> — explains title vs. shared semantics</td>
</tr>
<tr>
<td>"Dateien auswählen" CTA</td>
<td><code>h-11 px-4 bg-primary text-primary-fg font-extrabold text-sm rounded-sm uppercase tracking-wide focus-visible:ring-2 focus-visible:ring-focus-ring</code></td>
<td class="px">44px min · 14px</td>
<td class="note">triggers native <code>&lt;input type=file multiple&gt;</code></td>
</tr>
<tr>
<td>Formats line</td>
<td><code>text-xs text-ink-3 tracking-wide</code></td>
<td class="px">12px</td>
<td class="note">Paraglide <code>upload_dropzone_formats</code></td>
</tr>
</table>
<h3>Top bar — count pill + discard link (N ≥ 2)</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Count pill</td>
<td><code>bg-accent text-primary dark:bg-primary dark:text-primary-fg rounded-full px-3 py-1 text-sm font-bold</code></td>
<td class="px">14px · 700</td>
<td class="note">text "{n} werden erstellt" · Paraglide plural</td>
</tr>
<tr>
<td>Discard link</td>
<td><code>ml-auto text-sm font-bold text-danger hover:underline focus-visible:outline-2 focus-visible:outline-danger focus-visible:outline-offset-2 min-h-[44px] flex items-center</code></td>
<td class="px">14px · 44px tap</td>
<td class="note">fires confirm dialog; never silent</td>
</tr>
</table>
<h3>FileSwitcherStrip (new — only renders at N ≥ 2)</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-pdf-ctrl border-t border-line px-2 py-2</code></td>
<td class="px">height 48px</td>
<td class="note">sits under the PDF toolbar, on the dark PDF panel</td>
</tr>
<tr>
<td>Arrow buttons (desktop only)</td>
<td><code>hidden md:flex h-10 w-10 rounded-sm bg-ink/10 dark:bg-white/10 hover:bg-ink/20 dark:hover:bg-white/20 text-ink-3 items-center justify-center focus-visible:outline-2 focus-visible:outline-focus-ring</code></td>
<td class="px">40×40 (with px-2 pad → 44)</td>
<td class="note">aria-label="Vorherige Datei" / "Nächste Datei"</td>
</tr>
<tr>
<td>Track (scroll container)</td>
<td><code>flex-1 flex gap-1 overflow-x-auto snap-x snap-mandatory scroll-smooth</code></td>
<td class="px"></td>
<td class="note">mobile: gesture-swipe; desktop: arrows scroll <code>scrollBy({left:±120})</code></td>
</tr>
<tr>
<td>File chip · inactive</td>
<td><code>snap-start shrink-0 px-3 py-2 min-h-[40px] rounded-sm bg-ink/5 dark:bg-white/8 text-sm font-bold text-ink-2 hover:bg-ink/10 focus-visible:outline-2 focus-visible:outline-focus-ring flex items-center gap-2</code></td>
<td class="px">14px / h 40px</td>
<td class="note">max-width 180px, title truncates with <code>truncate</code></td>
</tr>
<tr>
<td>File chip · active</td>
<td>same base +&nbsp;<code>bg-accent text-primary dark:bg-primary dark:text-primary-fg</code> +&nbsp;<code>aria-current="true"</code></td>
<td class="px">14px / h 40px</td>
<td class="note">caret "▸" prefix via <code>::before</code> — redundant non-colour cue</td>
</tr>
<tr>
<td>File chip · error</td>
<td><code>bg-danger/15 text-danger border border-dashed border-danger</code></td>
<td class="px"></td>
<td class="note">tooltip = error message, ⚠ suffix, still clickable</td>
</tr>
<tr>
<td>Chip number prefix</td>
<td><code>bg-primary/20 dark:bg-primary-fg/20 rounded-sm px-1 text-xs font-extrabold</code></td>
<td class="px">12px · 800</td>
<td class="note">"1", "2", …</td>
</tr>
</table>
<h3>"Nur diese Datei" card (per-file scope, title only)</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-bg border border-accent rounded-sm p-4 mb-4</code></td>
<td class="px">padding 16px</td>
<td class="note">renders for N ≥ 2 only; at N = 1 title lives outside any card</td>
</tr>
<tr>
<td>Scope badge</td>
<td><code>bg-primary text-primary-fg 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_only_this_file</code></td>
</tr>
<tr>
<td>Subtitle (1 / 5 · filename)</td>
<td><code>text-xs font-bold text-ink-2 tracking-tight truncate</code></td>
<td class="px">12px</td>
<td class="note">filename truncates when long</td>
</tr>
<tr>
<td>Title input</td>
<td><code>h-11 w-full text-base font-semibold text-ink bg-surface border border-accent rounded-sm px-3 focus-visible:border-ink focus-visible:ring-2 focus-visible:ring-focus-ring</code></td>
<td class="px">44px · 16px</td>
<td class="note">starts as <code>suggested</code> state; mint border drops to <code>border-line</code> on first user edit</td>
</tr>
</table>
<h3>"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-muted border border-line rounded-sm p-4 mb-3</code></td>
<td class="px">padding 16px</td>
<td class="note">neutral (no accent tint) — signals "shared, not special"</td>
</tr>
<tr>
<td>Scope badge</td>
<td><code>bg-accent text-primary dark:bg-primary dark:text-primary-fg 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 ≤ 767px, two cols at ≥ 768px</td>
</tr>
<tr>
<td>Disabled (empty) state</td>
<td>wrap card in <code>aria-disabled="true" opacity-60 pointer-events-none</code></td>
<td class="px"></td>
<td class="note">only when N = 0; turns live on first file drop</td>
</tr>
</table>
<h3>Save bar (primary CTA, tri-mode)</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Primary save · idle</td>
<td><code>h-11 px-5 bg-green-700 hover:bg-green-800 dark:bg-green-800 dark:hover:bg-green-700 text-white font-extrabold rounded-sm text-sm focus-visible:ring-2 focus-visible:ring-green-900</code></td>
<td class="px">44px · 14px</td>
<td class="note">label <code>{count} speichern →</code> — Paraglide plural "Speichern" at 1, "N speichern" at ≥ 2</td>
</tr>
<tr>
<td>Primary save · saving</td>
<td>same +&nbsp;<code>bg-green-700/90 cursor-progress relative overflow-hidden</code> with inner progress bar <code>absolute inset-y-0 left-0 bg-white/20 transition-[width]</code></td>
<td class="px"></td>
<td class="note">replaces label with "Lade Datei {i} von {n}…" when > 500ms</td>
</tr>
<tr>
<td>"Als Platzhalter"</td>
<td><code>h-11 px-4 border border-line bg-surface text-ink-2 font-bold rounded-sm text-sm</code></td>
<td class="px">44px</td>
<td class="note">posts with <code>metadataComplete=false</code> for all N documents</td>
</tr>
<tr>
<td>"Verwerfen" skip link</td>
<td><code>text-sm font-bold text-ink-3 hover:text-ink min-h-[44px] flex items-center</code></td>
<td class="px">14px · 44px</td>
<td class="note">at N ≥ 2 this is a link to "Alle verwerfen" in the top bar; hide in save bar to avoid duplicate</td>
</tr>
</table>
<h3>Mobile responsive (≤ 767px)</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind</th><th>Px / value</th><th>Note</th></tr>
<tr>
<td>Tab bar</td>
<td><code>flex h-[38px] border-b border-line</code> — reuses DocumentEditLayout's mobile tabs</td>
<td class="px">38px</td>
<td class="note">labels: "Vorschau" (with "1/N" pill) · "Angaben"</td>
</tr>
<tr>
<td>Tab (active)</td>
<td><code>flex-1 h-full flex items-center justify-center text-sm font-extrabold text-ink border-b-2 border-accent dark:border-primary uppercase tracking-wide</code></td>
<td class="px">14px · 48px tap</td>
<td class="note">aria-selected="true"</td>
</tr>
<tr>
<td>"1/N" tab pill</td>
<td><code>ml-2 bg-accent text-primary dark:bg-primary dark:text-primary-fg rounded-full px-2 py-0.5 text-xs font-extrabold</code></td>
<td class="px">12px</td>
<td class="note">hide at N = 1</td>
</tr>
<tr>
<td>File switcher on mobile</td>
<td>arrows removed (<code>md:flex</code>), track full-width, snap-swipe</td>
<td class="px"></td>
<td class="note">announcer fires on snap-end: "Datei 3 von 5"</td>
</tr>
</table>
<h3>Paraglide keys (de/en/es)</h3>
<table class="impl-table">
<tr><th>Key</th><th>de</th><th>en</th><th>es</th></tr>
<tr><td>upload_dropzone_heading</td><td>Eine oder mehrere Dateien ablegen</td><td>Drop one or more files here</td><td>Suelta uno o más archivos aquí</td></tr>
<tr><td>upload_dropzone_body</td><td>Für jede Datei wird ein eigenes Dokument erstellt. Der Titel wird aus dem Dateinamen vorausgefüllt und ist pro Datei editierbar — alle anderen Felder gelten gemeinsam.</td><td>Each file becomes its own document. The title is pre-filled from the filename and editable per file — all other fields are shared.</td><td>Cada archivo se convierte en su propio documento. El título se rellena con el nombre del archivo y se puede editar por archivo; los demás campos son compartidos.</td></tr>
<tr><td>upload_dropzone_cta</td><td>Dateien auswählen</td><td>Choose files</td><td>Elegir archivos</td></tr>
<tr><td>upload_dropzone_formats</td><td>PDF · JPEG · PNG · TIFF · max 50 MB pro Datei</td><td>PDF · JPEG · PNG · TIFF · max 50 MB per file</td><td>PDF · JPEG · PNG · TIFF · máx. 50 MB por archivo</td></tr>
<tr><td>bulk_count_creating</td><td>{count} werden erstellt</td><td>{count} will be created</td><td>{count} se crearán</td></tr>
<tr><td>bulk_discard_all</td><td>Alle verwerfen</td><td>Discard all</td><td>Descartar todo</td></tr>
<tr><td>bulk_discard_confirm</td><td>{count} Dateien verwerfen?</td><td>Discard {count} files?</td><td>¿Descartar {count} archivos?</td></tr>
<tr><td>bulk_only_this_file</td><td>Nur diese Datei</td><td>This file only</td><td>Solo este archivo</td></tr>
<tr><td>bulk_only_subtitle</td><td>{index} / {count} · {filename}</td><td>{index} / {count} · {filename}</td><td>{index} / {count} · {filename}</td></tr>
<tr><td>bulk_shared_count</td><td>Gilt für alle {count}</td><td>Applies to all {count}</td><td>Se aplica a todos los {count}</td></tr>
<tr><td>bulk_save_cta</td><td>{count, plural, one {Speichern →} other {{count} speichern →}}</td><td>{count, plural, one {Save →} other {Save {count} →}}</td><td>{count, plural, one {Guardar →} other {Guardar {count} →}}</td></tr>
<tr><td>bulk_save_progress</td><td>Lade Datei {i} von {count}…</td><td>Uploading file {i} of {count}…</td><td>Subiendo archivo {i} de {count}…</td></tr>
<tr><td>bulk_save_placeholder</td><td>Als Platzhalter</td><td>Save as placeholder</td><td>Guardar como marcador</td></tr>
<tr><td>bulk_file_nav_prev</td><td>Vorherige Datei</td><td>Previous file</td><td>Archivo anterior</td></tr>
<tr><td>bulk_file_nav_next</td><td>Nächste Datei</td><td>Next file</td><td>Siguiente archivo</td></tr>
<tr><td>bulk_announce_count</td><td>{count} Dateien bereit zum Speichern</td><td>{count} files ready to save</td><td>{count} archivos listos para guardar</td></tr>
</table>
<div class="notes">
<div class="nh">Interaction + behaviour spec</div>
<ul>
<li><strong>Drop a file after the first batch</strong> → append to the end of the switcher, auto-focus the new chip, the title input inherits the filename-derived suggestion.</li>
<li><strong>Remove a file</strong> via X button on the active chip → confirm only for the currently-previewed file, else silent. When count drops to 1 the switcher animates away (200ms); to 0 the page returns to empty state.</li>
<li><strong>Filename → title</strong>: <code>basename.replace(/\.(pdf|jpe?g|png|tiff?)$/i, '').replace(/[_-]+/g, ' ').trim()</code>. Input marked <code>suggested</code> (mint border + accent-bg) until the user edits it, then border drops to <code>border-line</code>.</li>
<li><strong>Title always rendered</strong>: at N = 1 the title input sits above the shared card with no wrapping; at N ≥ 2 it's inside the "Nur diese Datei" card. Zero layout jump between 1 and 2.</li>
<li><strong>Keyboard nav in the switcher</strong>: <kbd></kbd>/<kbd></kbd> cycle files when focus is inside the strip. <kbd>Tab</kbd> moves focus out. <kbd>Enter</kbd>/<kbd>Space</kbd> selects a chip (identical to click).</li>
<li><strong>Focus on file switch</strong>: the title input of the new file auto-focuses so it's the first editable field under the user's cursor — matches the priority of the per-file scope.</li>
<li><strong>Save flow</strong>: one <code>POST /api/documents/quick-upload</code> with <code>files[]</code> + a JSON <code>metadata</code> part containing shared fields plus a <code>titles[]</code> array matched by index. Response splits into <code>created[] / updated[] / errors[]</code>; show a post-save toast + mark error chips red. Successful creates redirect to <code>/documents</code> (not to a single detail page — we just made N).</li>
<li><strong>Progress indicator</strong>: on save, replace the save button body with a determinate progress bar + text "Lade Datei {i} von {n}…". Switch to indeterminate shimmer only if the server doesn't stream progress.</li>
<li><strong>Live-region announces</strong>: <code>&lt;div role="status" aria-live="polite"&gt;</code> in the top bar. Fires on (a) file count change (add/remove), (b) active file switch ("Datei 3 von 5"), (c) save progress, (d) save complete.</li>
<li><strong>Page leave protection</strong>: if N ≥ 1 and shared form has any non-default value, <code>beforeunload</code> prompts. Suppress during explicit "Alle verwerfen" / save.</li>
</ul>
</div>
<div class="notes danger">
<div class="nh">Edge cases + a11y requirements</div>
<ul>
<li><strong>Single file, matching #294 exactly</strong>: at N = 1 do NOT render <code>FileSwitcherStrip</code>, do NOT render the count pill or "Alle verwerfen" link, do NOT render the "Nur diese Datei" card (title sits directly above shared). Any deviation breaks the invariant — covered by a component-level snapshot test.</li>
<li><strong>Duplicate filenames in one batch</strong>: accept both, show a warning icon next to both chips. Backend creates two documents with distinct UUIDs.</li>
<li><strong>Mixed content types</strong>: PDF + image + TIFF in one batch — preview panel switches renderer per active file (DocumentEditLayout already handles this).</li>
<li><strong>One file fails upload</strong>: chip goes red-dashed. Other files still create. Post-save toast lists the failure with the backend error code mapped via <code>getErrorMessage()</code>. "Retry" button on the chip re-POSTs that file alone.</li>
<li><strong>Large batches (&gt; 20 files)</strong>: switcher scrolls horizontally. At &gt; 30 consider a "Jump to file…" combobox (follow-up, not v1).</li>
<li><strong>Screen reader on file switch</strong>: <code>role="tablist"</code> is wrong here because the chips aren't tabs for a tabbed content; use a visually-hidden "Datei auswählen" label with <code>aria-live="polite"</code> announcement on selection change.</li>
<li><strong>Colour-alone check (WCAG 1.4.1)</strong>: active chip = colour <em>and</em> caret prefix <em>and</em> <code>aria-current="true"</code>. Error chip = colour <em>and</em> dashed border <em>and</em> ⚠ suffix. Three redundant cues on both states.</li>
<li><strong>Contrast — light</strong>: mint (#a1dcd8) on navy (#012851) = 7.2:1 AAA · navy on mint = 7.2:1 (inverse). Title field <code>suggested</code> uses ink (#012851) on accent-bg (15% mint) = ~9:1 AAA.</li>
<li><strong>Contrast — dark</strong>: mint-fg (#a1dcd8) on navy surface (#011526) = 9.2:1 AAA · turquoise accent (#00c7b1) on surface = 5.4:1 AA large. Active chip uses primary (mint) bg with navy fg — 9.2:1 AAA.</li>
<li><strong>Reduced motion</strong>: the 200ms switcher-fade and progress bar fill respect <code>prefers-reduced-motion: reduce</code> via the existing global @media rule — cuts transition-duration to 0.01ms.</li>
<li><strong>Touch targets</strong>: every interactive element — arrow buttons, file chips, remove X, save button, tab headers — ≥ 44×44. Chip X is outside the chip's clickable area (uses <code>::after</code> + a sibling button) to avoid the "I tried to select but removed" trap.</li>
</ul>
</div>
<h3>Component tree (frontend)</h3>
<table class="impl-table">
<tr><th>Component</th><th>Status</th><th>Responsibility</th></tr>
<tr>
<td><code>documents/new/+page.svelte</code></td>
<td>rewrite</td>
<td>State owner: files array, active index, shared metadata. Mode switches by files.length.</td>
</tr>
<tr>
<td><code>DocumentEditLayout.svelte</code></td>
<td>accept props</td>
<td>Takes <code>{ files, activeIndex }</code>; emits switch + remove events. Existing props for single-file unchanged.</td>
</tr>
<tr>
<td><code>BulkDropZone.svelte</code></td>
<td>new</td>
<td>Full-panel drop target for N=0 and for "add more" at N≥1. Wraps the dashed box + CTA + formats line.</td>
</tr>
<tr>
<td><code>FileSwitcherStrip.svelte</code></td>
<td>new</td>
<td>Horizontal chip list + arrows + aria-live region. Emits <code>select</code> + <code>remove</code>.</td>
</tr>
<tr>
<td><code>ScopeCard.svelte</code></td>
<td>new</td>
<td>Wraps "Nur diese Datei" / "Gilt für alle N" with the correct badge + tint. One component, two variants.</td>
</tr>
<tr>
<td><code>UploadSaveBar.svelte</code></td>
<td>extend</td>
<td>Already exists for single-file. Add plural-count label + determinate progress state.</td>
</tr>
</table>
<h3>Backend contract — single POST for the whole batch</h3>
<table class="impl-table">
<tr><th>Element</th><th>Tailwind / Value</th><th>Note</th></tr>
<tr>
<td>Endpoint</td>
<td><code>POST /api/documents/quick-upload</code></td>
<td class="note">already exists — accepts <code>List&lt;MultipartFile&gt; files</code></td>
</tr>
<tr>
<td>Request — files part</td>
<td>repeated <code>files</code> multipart entries, one per file, in UI order</td>
<td class="note">backend preserves order so <code>titles[i]</code> matches <code>files[i]</code></td>
</tr>
<tr>
<td>Request — metadata part</td>
<td>JSON part named <code>metadata</code> containing <code>{ senderId, receiverId, documentDate, location, tags[], archiveBox, archiveFolder, metadataComplete, titles[] }</code></td>
<td class="note">backend change: add a new overload of <code>quickUpload</code> that reads the JSON part and applies shared fields to every created Document</td>
</tr>
<tr>
<td>Response</td>
<td><code>{ created: DocRef[], updated: DocRef[], errors: { filename, code }[] }</code></td>
<td class="note">existing shape — no breaking change</td>
</tr>
</table>
<div class="notes">
<div class="nh">Out of scope for this spec</div>
<ul>
<li><strong>Per-file metadata overrides beyond title</strong>: a future "expand row" could let the user override sender / date per file. Not in v1.</li>
<li><strong>Drag-to-reorder</strong>: the order of created documents matches the drop order. Reordering is a nice-to-have.</li>
<li><strong>Resume interrupted uploads</strong>: if the browser crashes mid-save, the user restarts. Chunked/resumable is a follow-up.</li>
<li><strong>Folder upload</strong> (<code>webkitdirectory</code>): expands the batch beyond what the user meant. Off by default; revisit if users ask.</li>
<li><strong>Pre-populate sender/date from OCR on a sample file</strong>: interesting but async — ships as a second feature.</li>
</ul>
</div>
</div>
</div>
</body>
</html>