UX Spec · Bulk Upload

# Uploading multiple documents in a single pass

Extends issue #294 (new-document split-panel) with bulk uploads. When a user drops N files, every metadata field applies once to all of them — only the title is per-file, pre-filled from the filename and editable inline. A single save POST creates N documents.

Prepared by Leonie Voss · 2026-04-24 · Draft 1 · References: #294, #305
feature ui a11y 320px+ backend ready

## Design goals

- One-pass feel: drop → fill shared fields → save. No wizard, no per-file detour.
- Every field is shared except the title, which is always set (filename-derived).
- No mode switch: 1 file and N files use the same screen — more files reveal more chrome.
- Scales to 20+ files without the form losing scan-ability on mobile.
- Reuses the #294 split-panel layout (DocumentEditLayout) — minimum new surface.
- a11y-first: 44px targets, focus states, `aria-current` on active file, keyboard-navigable.

A
Concept A
Flat Stack — shared header · file cards · sticky save

A single vertical flow: drop zone on top, then a Gilt für alle metadata card, then stacked file cards (thumbnail · editable title · remove). No split panel, no tabs. Scrolling down reveals all files; the save bar sticks to the bottom.

Best for Small-screen workflows. Seniors who prefer linear flows over tabs.
Trade-off: no PDF preview until you click through to the document after save. Harder to verify you grabbed the right files before committing.
375 · mobile
← Zurück
Neue Dokumente
5
⇪
Weitere Dateien hinzufügen
PDF, JPEG, PNG, TIFF · max 50 MB
Gilt für alle 5 Angaben
Absender
Hans Müller
Empfänger
Anna Schmidt
Datum
1950-06
Ort
Berlin
Tags
Familie × Krieg ×
5 Dateien · Titel bearbeiten
Brief_1940_Hans
Brief_1940_Hans.pdf · 2.4 MB
✕
Brief_1940_Anna
Brief_1940_Anna.pdf · 1.8 MB
✕
Brief_1941_Clara
Brief_1941_Clara.pdf · 890 kB
✕
Postkarte_Venedig
Postkarte_Venedig.jpg · 1.1 MB
✕
Alle verwerfen
Als Platzhalter
5 speichern →
B
Concept B · RECOMMENDED
Split-Panel with File Switcher

Reuses the DocumentEditLayout from issue #294 and adds a horizontal file-switcher strip under the PDF preview. Right column splits into two cards: Gilt nur für diese Datei (title only, mint accent) and Gilt für alle N Dokumente (everything else). When N=1 the switcher disappears and the screen is byte-identical to #294.

Best for The project's primary use case. Desktop + tablet, matches #294 DNA.
Trade-off: on mobile the split has to collapse into tabs ("Vorschau / Angaben"). We reuse the same responsive pattern that DocumentEditLayout already ships with.
1280 · desktop
← Dokumente
Neue Dokumente
5 werden erstellt
Alle verwerfen
◀
▶
+
−
Seite 1 / 2 · Datei 1 von 5
‹
1 Brief_1940_Hans.pdf
2 Brief_1940_Anna.pdf
3 Brief_1941_Clara.pdf
4 Postkarte_Venedig.jpg
5 Urkunde_1942.pdf
›
Nur diese Datei 1 / 5 · Brief_1940_Hans.pdf
Titel *
Brief an Anna, 1940
Gilt für alle 5 Gemeinsame Angaben
Absender
Hans Müller
Empfänger
Anna Schmidt
Datum
15.06.1950
Ort
z.B. Berlin
Tags
Familie × Krieg × Briefwechsel ×
Archivbox
z.B. B-12
Mappe
z.B. M-3
Alle verwerfen
Als Platzhalter
5 speichern →
C
Concept C
Progressive Accordion — shared sticky header · file cards expand inline

Shared metadata sticks at the top of the page. Below, each file is a collapsed card; clicking a card expands it to show the PDF preview + title field inline. Only one card is expanded at a time. Scales well to 20+ files — the list stays readable, you only look at the PDFs you want to verify.

Best for Large batches (10+ files) where you want to spot-check a few.
Trade-off: two different visual languages — cards collapsed vs. cards expanded with PDF. New pattern for the project; costs familiarity.
1280 · desktop
← Zurück
Neue Dokumente
5
Gilt für alle 5 Gemeinsame Angaben
Absender
Hans Müller
Empfänger
Anna Schmidt
Datum
15.06.1950
Tags
Familie ×Krieg ×
Ort
z.B. Berlin
5 Dateien
▸
Brief an Anna, 1940
Brief_1940_Hans.pdf · 2.4 MB
✕
▾
Brief von Anna, Antwort
Brief_1940_Anna.pdf · 1.8 MB
✕
Nur diese Datei 2 / 5
Titel *
Brief von Anna, Antwort
▸
Brief_1941_Clara
Brief_1941_Clara.pdf · 890 kB
✕
▸
Postkarte_Venedig
Postkarte_Venedig.jpg · 1.1 MB
✕
▸
Urkunde_1942
Urkunde_1942.pdf · 3.1 MB
✕
Alle verwerfen
Als Platzhalter
5 speichern →

## Decision matrix

All three concepts meet the core requirement (shared metadata + per-file title + one save). Graded against what matters for the senior audience, the responsive constraint, and the #294 architectural commitment.

Dimension | A · Stack | B · Split-Panel | C · Accordion
--- | --- | --- | ---
Reuses #294 layout | ✕ | ✓ | ✕
Single-file mode unchanged | rewrite | identical | different
PDF visible before save | no | always | one at a time
Works at 320px | native | via tab collapse | native
Scales to 20 files | long scroll | switcher scrolls | collapsed list
New Svelte components | 3 new | 1 new (switcher) | 4 new
Familiar pattern | yes | yes (post-#294) | new to app

Recommendation

## Ship Concept B

Concept B treats bulk upload as a polymorphic state of the existing single-document layout rather than a separate screen. A user who drops one file gets exactly the #294 experience. A user who drops five gets the same screen plus a horizontal file-switcher and a two-card split (Nur diese Datei vs. Gilt für alle). Nothing about the single-file flow changes.

- Keeps the mental model: "one form, one save" regardless of file count.
- PDF preview is persistent — you can spot-check each scan before committing.
- The per-file title is visually promoted with a mint border so it reads as the one thing that differs per file.
- Reuses DocumentEditLayout: the delta is ~1 new component (`FileSwitcherStrip`) + two cards in the form.
- Single-file mode is byte-identical to #294 — no regression risk for existing users.
- Backend is already ready (`POST /api/documents/quick-upload` accepts N files in one multipart).

## Implementation reference — Concept B

### Top bar (when N > 1)

Element | Tailwind | Px / value | Note
--- | --- | --- | ---
Count pill "N werden erstellt" | `bg-accent text-primary rounded-full px-3 py-1 text-sm font-bold` | 14px · 700 | brand-mint on brand-navy
"Alle verwerfen" link | `ml-auto text-sm font-bold text-red-600 hover:text-red-800 focus-visible:outline-2 focus-visible:outline-red-600` | 14px / 44px target | confirm dialog before wiping

### FileSwitcherStrip (new component)

Element | Tailwind | Px / value | Note
--- | --- | --- | ---
Strip container | `flex items-center gap-1 bg-ink/95 px-2 py-2 border-t border-ink/80` | height 48px | under the PDF toolbar, on the dark panel
Arrow buttons | `h-10 w-10 rounded-sm bg-white/8 text-surface/60 hover:bg-white/15 focus-visible:outline-2` | 40×40 (44 w/padding) | `aria-label="Vorherige Datei"` / "Nächste Datei"
File chip (inactive) | `px-3 py-2 rounded-sm bg-white/6 text-sm font-bold text-surface/55 whitespace-nowrap hover:bg-white/12` | 14px / h 40px | horizontal scroll container uses `snap-x snap-mandatory`
File chip (active) | `... bg-accent text-primary` + `aria-current="true"` | 14px / h 40px | mint pill, primary text — 7.2:1 contrast passes AAA
Chip number prefix | `bg-primary/25 rounded-sm px-1 mr-2 text-xs font-extrabold` | 12px / 800 | "1", "2", … — for quick scanning

### "Nur diese Datei" card (per-file scope)

Element | Tailwind | Px / value | Note
--- | --- | --- | ---
Card container | `bg-accent/20 border border-accent rounded-sm p-4 mb-4` | padding 16px | mint tint signals "different per file"
Scope badge | `bg-primary/90 text-accent rounded-sm px-2 py-1 text-xs font-extrabold uppercase tracking-wide` | 12px · 800 | Paraglide key: `bulk_only_this_file`
Title input | `h-11 text-base font-semibold text-ink bg-white border border-line rounded-sm px-3 focus-visible:border-ink focus-visible:ring-2 focus-visible:ring-ink/20` | 44px min-height · 16px | pre-filled from filename without extension

### "Gilt für alle" card (shared scope)

Element | Tailwind | Px / value | Note
--- | --- | --- | ---
Card container | `bg-surface border border-line rounded-sm p-4 mb-3` | padding 16px | neutral (no accent tint)
Scope badge | `bg-accent text-primary rounded-sm px-2 py-1 text-xs font-extrabold uppercase tracking-wide` | 12px · 800 | Paraglide: `bulk_shared_count` ("Gilt für alle {count}")
Field grid | `grid grid-cols-1 md:grid-cols-2 gap-3` | 12px gap | single column at 320px, two at ≥ 768px

### Save bar

Element | Tailwind | Px / value | Note
--- | --- | --- | ---
Primary save button | `h-11 px-5 bg-green-700 hover:bg-green-800 text-white font-extrabold rounded-sm text-sm focus-visible:ring-2 focus-visible:ring-green-900` | 44px min · 14px | label `{count} speichern →` (plural-aware Paraglide)
"Als Platzhalter" (outline) | `h-11 px-4 border border-line bg-white text-ink-3 font-bold rounded-sm text-sm` | 44px | posts with `metadataComplete=false` for all

### Responsive collapse (≤ 767px)

Element | Tailwind | Px / value | Note
--- | --- | --- | ---
Panel mode switch | reuses DocumentEditLayout's existing tab collapse — "Vorschau / Angaben" tabs | tab height 48px | already shipped with #294
File switcher stays on "Vorschau" tab | `snap-x snap-mandatory overflow-x-auto` | h 44px | horizontal swipe; arrow buttons removed at mobile

Interactions + behaviour

- Drop a file after the initial batch: append to the end of the list and switch focus to the newly added file. No modal, no confirmation.
- Remove a file (X on the chip) → confirm only if it's the currently-previewed one; otherwise silent. When count drops to 1 the switcher strip animates away (200ms); when it drops to 0 we redirect back to the drop-zone state.
- Title auto-fill: `filename.replace(/\.(pdf|jpe?g|png|tiff?)$/i, '').replace(/[_-]+/g, ' ').trim()`. Marks the title input as `suggested` until the user edits it (mint left border, same treatment as #294's filename-derived fields).
- Title field visibility: always rendered (never collapsed) even in single-file mode, so there's zero layout jump when N changes from 1 to 2.
- Save flow: single POST to `/api/documents/quick-upload` with N files + JSON metadata object containing shared fields + titles array. Backend maps title[i] to files[i] by index. Response splits into `created[] / updated[] / errors[]` — show a summary toast + inline error markers per file for the `errors[]` list.
- Keyboard navigation: `←`/`→` on the switcher strip moves file focus; `Tab` cycles through form fields inside whichever card is active; `Esc` on the discard button opens the confirm dialog.
- Focus management on file switch: when the user clicks a different file, the title input of the new file receives focus automatically (so the main editable field is always reachable).
- Progress indicator during save: replace the save button with a determinate progress bar showing "Lade Datei 3 von 5…" for batches that take > 500ms.

Edge cases + a11y

- Duplicate filenames in the batch: accept, but show a warning icon next to both — backend will create both with unique IDs.
- Mixed content types: PDF + image in the same batch is fine; the preview panel renders whichever the active file is (DocumentEditLayout already handles both).
- Large batches (> 20 files): the switcher strip becomes scrollable; consider a "Jump to file…" combobox at > 30 files (out of scope for v1).
- Upload failure per file: mark the chip red (`bg-red-600/20 text-red-800 border border-red-600`), show inline error in the chip's tooltip, don't block the rest of the batch from retrying.
- Screen reader announcement: when file count changes, fire a polite live region announce — "5 Dateien bereit zum Speichern" via `role="status" aria-live="polite"`.
- Colour-alone warning: active file chip uses color + `aria-current="true"` + a ▸ caret prefix so it's distinguishable for color-blind users.