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>
1685 lines
101 KiB
HTML
1685 lines
101 KiB
HTML
<!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 · 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 · 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><input type=file multiple></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 + <code>bg-accent text-primary dark:bg-primary dark:text-primary-fg</code> + <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 + <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><div role="status" aria-live="polite"></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 (> 20 files)</strong>: switcher scrolls horizontally. At > 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<MultipartFile> 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>
|