diff --git a/docs/specs/bulk-upload-concepts.html b/docs/specs/bulk-upload-concepts.html new file mode 100644 index 00000000..bf0a2e0d --- /dev/null +++ b/docs/specs/bulk-upload-concepts.html @@ -0,0 +1,996 @@ + + + + + +Bulk Upload — 3 Concept Designs · Familienarchiv + + + + +
+ + + + +
+
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. +

+ +
+ feature + ui + a11y 320px+ + backend ready +
+
+ + +
+

Design goals

+ +
+ + + + +
+
+
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
+
+
+ +
+
MR
+
+
+
+
← 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
+
+
+ + + + + +
+
MR
+
+
+
+
← 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
+
+
+ + + +
MR
+
+
+
← 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. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DimensionA · StackB · Split-PanelC · Accordion
Reuses #294 layout
Single-file mode unchangedrewriteidenticaldifferent
PDF visible before savenoalwaysone at a time
Works at 320pxnativevia tab collapsenative
Scales to 20 fileslong scrollswitcher scrollscollapsed list
New Svelte components3 new1 new (switcher)4 new
Familiar patternyesyes (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. +

+ +
+ + + + +
+

Implementation reference — Concept B

+ +

Top bar (when N > 1)

+ + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Count pill "N werden erstellt"bg-accent text-primary rounded-full px-3 py-1 text-sm font-bold14px · 700brand-mint on brand-navy
"Alle verwerfen" linkml-auto text-sm font-bold text-red-600 hover:text-red-800 focus-visible:outline-2 focus-visible:outline-red-60014px / 44px targetconfirm dialog before wiping
+ +

FileSwitcherStrip (new component)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Strip containerflex items-center gap-1 bg-ink/95 px-2 py-2 border-t border-ink/80height 48pxunder the PDF toolbar, on the dark panel
Arrow buttonsh-10 w-10 rounded-sm bg-white/8 text-surface/60 hover:bg-white/15 focus-visible:outline-240×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/1214px / h 40pxhorizontal scroll container uses snap-x snap-mandatory
File chip (active)... bg-accent text-primary + aria-current="true"14px / h 40pxmint pill, primary text — 7.2:1 contrast passes AAA
Chip number prefixbg-primary/25 rounded-sm px-1 mr-2 text-xs font-extrabold12px / 800"1", "2", … — for quick scanning
+ +

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

+ + + + + + + + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Card containerbg-accent/20 border border-accent rounded-sm p-4 mb-4padding 16pxmint tint signals "different per file"
Scope badgebg-primary/90 text-accent rounded-sm px-2 py-1 text-xs font-extrabold uppercase tracking-wide12px · 800Paraglide key: bulk_only_this_file
Title inputh-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/2044px min-height · 16pxpre-filled from filename without extension
+ +

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

+ + + + + + + + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Card containerbg-surface border border-line rounded-sm p-4 mb-3padding 16pxneutral (no accent tint)
Scope badgebg-accent text-primary rounded-sm px-2 py-1 text-xs font-extrabold uppercase tracking-wide12px · 800Paraglide: bulk_shared_count ("Gilt für alle {count}")
Field gridgrid grid-cols-1 md:grid-cols-2 gap-312px gapsingle column at 320px, two at ≥ 768px
+ +

Save bar

+ + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Primary save buttonh-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-90044px min · 14pxlabel {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-sm44pxposts with metadataComplete=false for all
+ +

Responsive collapse (≤ 767px)

+ + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Panel mode switchreuses DocumentEditLayout's existing tab collapse — "Vorschau / Angaben" tabstab height 48pxalready shipped with #294
File switcher stays on "Vorschau" tabsnap-x snap-mandatory overflow-x-autoh 44pxhorizontal 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.
  • +
+
+
+ +
+ + diff --git a/docs/specs/bulk-upload-split-panel-spec.html b/docs/specs/bulk-upload-split-panel-spec.html new file mode 100644 index 00000000..95a870e3 --- /dev/null +++ b/docs/specs/bulk-upload-split-panel-spec.html @@ -0,0 +1,1684 @@ + + + + + +Bulk Upload · Split-Panel Spec · Familienarchiv + + + + +
+ + + + +
+
UI/UX Spec · Implementation-ready
+

Bulk upload — split-panel with file switcher

+

+ Extends the #294 split-panel 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 /api/documents/quick-upload creates N documents in one pass. +

+ +
+ feature + ui + a11y 320px+ + light + dark + backend ready +
+
+ + + + +
+

The one-screen model

+

+ /documents/new 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. +

+
+
+
N = 0
+
Empty
+
+ 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. +
+
+
+
+
N = 1
+
Single file
+
+ Left = PDF preview. Right = title card + shared card. + Byte-identical to the shipped #294 layout. No file switcher, + no "1/1" subtitles — this state should feel unchanged to existing users. +
+
+
+
+
N ≥ 2
+
Multiple files
+
+ 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. +
+
+
+
+ + + + +
+
+ State 1 of 3 +

Empty state — drop zone with bulk-first copy

+

+ When the user hits /documents/new the PDF panel is the drop target. It's not a tiny button — + the whole left panel 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. +

+
+ +
+ Empty · N = 0 + desktop · 1280 + Light +
+ + +
+
+
+ +
1280 · Desktop · Light
+
+
+ + + + + +
MR
+
+
+
← Dokumente
+
Neues Dokument
+
+
+
+
Keine Datei ausgewählt
+
+
+
+
Eine oder mehrere Dateien ablegen
+
+ 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 für alle Dokumente gemeinsam. +
+
Dateien auswählen
+
PDF · JPEG · PNG · TIFF  ·  max 50 MB pro Datei
+
+
+
+
+
+
+
+ Gilt für alle + Gemeinsame Angaben +
+
+
Absender
+
Empfänger
+
+
+
Datum
+
Ort
+
+
+
Tags
+
+
+
Archivbox
+
Mappe
+
+
+
+
+
+
Speichern →
+
+
+
+
+
+ + +
+ Empty · N = 0 + desktop · 1280 + Dark +
+
+
+
+ +
1280 · Desktop · Dark
+
+
+ + + + + +
MR
+
+
+
← Dokumente
+
Neues Dokument
+
+
+
+
Keine Datei ausgewählt
+
+
+
+
Eine oder mehrere Dateien ablegen
+
+ 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 für alle Dokumente gemeinsam. +
+
Dateien auswählen
+
PDF · JPEG · PNG · TIFF  ·  max 50 MB pro Datei
+
+
+
+
+
+
+
+ Gilt für alle + Gemeinsame Angaben +
+
+
Absender
+
Empfänger
+
+
+
Datum
+
Ort
+
+
+
Tags
+
+
+
+
+
+
Speichern →
+
+
+
+
+
+ + +
+ Empty · N = 0 + mobile · 375 + Light + One-panel view — tabs appear only when a file is loaded +
+
+
+
+ +
375 · Mob
+
+
+ +
MR
+
+
+
← Zurück
+
Neues Dokument
+
+
+
+
Keine Datei ausgewählt
+
+
+
+
Eine oder mehrere Dateien ablegen
+
+ Jede Datei wird ein eigenes Dokument. Der Titel kommt aus dem Dateinamen — + alle anderen Felder gelten für alle. +
+
Dateien auswählen
+
PDF · JPEG · PNG · TIFF · max 50 MB
+
+
+
+
+
+
+
Speichern →
+
+
+
+
+ + + + +
+
+ State 2 of 3 +

Single-file state — zero change from #294

+

+ When exactly one file is loaded, the screen is byte-identical to the #294 DocumentEditLayout + 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. +

+
+ +
+ Single · N = 1 + desktop · 1280 + Light + Reference only — no new components render here +
+
+
+
+ +
1280 · Desktop · Light
+
+
+ + + + +
MR
+
+
+
← Dokumente
+
Neues Dokument
+
+
+
+
+
+
+
+
Seite 1 / 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Titel * +
Brief an Anna, 1940
+
+
+
+ Angaben +
+
+
Absender
Hans Müller
+
Empfänger
Anna Schmidt
+
+
+
Datum
15.06.1940
+
Ort
Berlin
+
+
+
Tags
+ Familie × + Krieg × +
+
+
+
+
+
Verwerfen
+
+
Als Platzhalter
+
Speichern →
+
+
+
+
+
+
+ + + + +
+
+ State 3 of 3 +

Multi-file state — file switcher + two-card form

+

+ When N ≥ 2, 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 + Nur diese Datei card on top (title only), the neutral Gilt für alle 5 card below + (everything else). +

+
+ + +
+ Multi · N = 5 + desktop · 1280 + Light +
+
+
+
+ +
1280 · Desktop · Light
+
+
+ + + + + +
MR
+
+
+
← Dokumente
+
Neue Dokumente
+
5 werden erstellt
+
Alle verwerfen
+
+
+
+
+
+
+
+
Seite 1 / 2 · Datei 1 / 5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1Brief_1940_Hans.pdf
+
2Brief_1940_Anna.pdf
+
3Brief_1941_Clara.pdf
+
4Postkarte_Venedig.jpg
+
5Urkunde_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.1940
+
Ort
z.B. Berlin
+
+
+
Tags
+ Familie × + Krieg × + Briefwechsel × +
+
+
+
Archivbox
z.B. B-12
+
Mappe
z.B. M-3
+
+
+
+
+
Verwerfen
+
+
Als Platzhalter
+
5 speichern →
+
+
+
+
+
+ + +
+
+
1Count pill
+
Mint background, navy text (7.2:1 AAA). Only visible at N ≥ 2. Live-region announces changes.
+
+
+
2"Alle verwerfen"
+
Danger-coloured link at the far right of the top bar. Triggers a confirm dialog; never a silent wipe.
+
+
+
3File switcher
+
Active file uses mint bg + aria-current + a ▸ caret. Three redundant cues for colour-blind users.
+
+
+
4"Nur diese Datei"
+
Mint-tinted card: the ONE thing that differs per file lives here. Currently just the title.
+
+
+
5"Gilt für alle N"
+
Neutral card with everything that applies to every document. The count is interpolated from N.
+
+
+
6Save CTA
+
"5 speichern" — count is plural-aware via Paraglide. Becomes a progress bar on slow saves.
+
+
+ + +
+ Multi · N = 5 + desktop · 1280 + Dark +
+
+
+
+ +
1280 · Desktop · Dark
+
+
+ + + + +
MR
+
+
+
← Dokumente
+
Neue Dokumente
+
5 werden erstellt
+
Alle verwerfen
+
+
+
+
+
+
+
+
Seite 1 / 2 · Datei 1 / 5
+
+
+
+
+
+
+
+
+
+
+
+
+
1Brief_1940_Hans.pdf
+
2Brief_1940_Anna.pdf
+
3Brief_1941_Clara.pdf ⚠
+
4Postkarte_Venedig.jpg
+
5Urkunde_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.1940
+
Ort
z.B. Berlin
+
+
+
Tags
+ Familie × + Krieg × +
+
+
+
+
+
Verwerfen
+
+
Als Platzhalter
+
5 speichern →
+
+
+
+
+
+ + +
+ Multi · N = 5 + tablet · 768 + Light + Split is kept — PDF pane narrows, form stays readable +
+
+
+
+ +
768 · Tab
+
+
+ + + +
MR
+
+
+
← Dok
+
Neue Dokumente
+
5
+
Verwerfen
+
+
+
+
+
+
1/5
+
+
+
+
+
+
+
+
+
+
+
1Brief_Hans
+
2Brief_Anna
+
3Brief_Clara
+
+
+
+
+
+
+
+
+ Diese Datei + 1/5 +
+
+ Titel * +
Brief an Anna, 1940
+
+
+
+
+ Alle 5 +
+
+
Absender
Hans Müller
+
+
+
Empfänger
Anna Schmidt
+
+
+
Datum
1940
+
Ort
+
+
+
+
+
+
5 speichern
+
+
+
+
+
+ + +
+ Multi · N = 5 + tablet · 768 + Dark +
+
+
+
+ +
768 · Tab · Dark
+
+
+ + + +
MR
+
+
+
← Dok
+
Neue Dokumente
+
5
+
Verwerfen
+
+
+
+
+
+
1/5
+
+
+
+
+
+
+
+
+
+
+
1Brief_Hans
+
2Brief_Anna
+
3Brief_Clara
+
+
+
+
+
+
+
+
+ Diese Datei + 1/5 +
+
+ Titel * +
Brief an Anna, 1940
+
+
+
+
+ Alle 5 +
+
+
Absender
Hans Müller
+
+
+
Datum
1940
+
Ort
+
+
+
+
+
+
5 speichern
+
+
+
+
+
+ + +
+ Multi · N = 5 + mobile · 375 + Light + Split collapses into "Vorschau / Angaben" tabs — reuses DocumentEditLayout's pattern +
+
+ +
+
+
+ +
375 · Tab: Vorschau
+
+
+ +
MR
+
+
+
+
Neue Dokumente
+
5
+
+
+
Vorschau 1/5
+
Angaben
+
+
+
+
+
Seite 1 / 2
+
+
+
+
+
+
+
+
+
+
+
1Brief_Hans
+
2Brief_Anna
+
3Brief_Clara
+
+
+
+
+
+
5 speichern
+
+
+
+ + +
+
+
+ +
375 · Tab: Angaben
+
+
+ +
MR
+
+
+
+
Neue Dokumente
+
5
+
+
+
Vorschau 1/5
+
Angaben
+
+
+
+
+
+ Datei 1/5 + Brief_1940_Hans.pdf +
+
+ Titel * +
Brief an Anna, 1940
+
+
+
+
+ Alle 5 +
+
+
Absender
Hans Müller
+
+
+
Empfänger
Anna Schmidt
+
+
+
Datum
15.06.1940
+
+
+
Tags
+ Familie × + Krieg × +
+
+
+
+
+
+
5 speichern
+
+
+
+
+
+ + +
+ Multi · N = 5 + mobile · 375 + Dark +
+
+
+
+
+ +
375 · Dark · Vorschau
+
+
+ +
MR
+
+
+
+
Neue Dokumente
+
5
+
+
+
Vorschau 1/5
+
Angaben
+
+
+
+
+
Seite 1 / 2
+
+
+
+
+
+
+
+
+
+
1Brief_Hans
+
2Brief_Anna
+
3Brief_Clara
+
+
+
+
+
+
5 speichern
+
+
+
+ +
+
+
+ +
375 · Dark · Angaben
+
+
+ +
MR
+
+
+
+
Neue Dokumente
+
5
+
+
+
Vorschau 1/5
+
Angaben
+
+
+
+
+
+ Datei 1/5 + Brief_1940_Hans.pdf +
+
+ Titel * +
Brief an Anna, 1940
+
+
+
+
+ Alle 5 +
+
+
Absender
Hans Müller
+
+
+
Datum
1940
+
+
+
Tags
+ Familie × + Krieg × +
+
+
+
+
+
+
5 speichern
+
+
+
+
+
+ + +
+ Multi · N = 5 + mobile · 320 + Light + Narrowest supported viewport — same structure, tighter paddings +
+
+
+
+ +
320
+
+
+ +
MR
+
+
+
+
Neue Dokumente
+
5
+
+
+
Vorschau 1/5
+
Angaben
+
+
+
+
+
+ 1/5 + Brief_Hans.pdf +
+
+ Titel * +
Brief an Anna, 1940
+
+
+
+
+ Alle 5 +
+
+
Absender
Hans Müller
+
+
+
Empfänger
Anna Schmidt
+
+
+
+
+
+
5 speichern
+
+
+
+
+
+ + + + +
+

Implementation reference — tokens, classes, behaviour

+ +

Empty-state drop zone (PDF panel, N = 0)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Outer panel (drop target)flex-1 bg-pdf-bg flex flex-colfull heightthe entire left panel accepts drops, not just the inner box
Inner dashed boxborder-2 border-dashed border-accent rounded-md p-9 max-w-[340px] bg-surface/50 dark:bg-white/5padding 36pxvisual anchor for mouse drops; not the hit target
Upload icon circlew-14 h-14 rounded-full bg-accent text-primary flex items-center justify-center text-2xl font-extrabold56×56mint on white · primary on dark (token swap is automatic)
Headlinefont-serif text-base font-bold text-ink16px · 700Paraglide upload_dropzone_heading
Supporting copytext-sm text-ink-2 leading-relaxed max-w-prose14pxParaglide upload_dropzone_body — explains title vs. shared semantics
"Dateien auswählen" CTAh-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-ring44px min · 14pxtriggers native <input type=file multiple>
Formats linetext-xs text-ink-3 tracking-wide12pxParaglide upload_dropzone_formats
+ +

Top bar — count pill + discard link (N ≥ 2)

+ + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Count pillbg-accent text-primary dark:bg-primary dark:text-primary-fg rounded-full px-3 py-1 text-sm font-bold14px · 700text "{n} werden erstellt" · Paraglide plural
Discard linkml-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-center14px · 44px tapfires confirm dialog; never silent
+ +

FileSwitcherStrip (new — only renders at N ≥ 2)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Strip containerflex items-center gap-1 bg-pdf-ctrl border-t border-line px-2 py-2height 48pxsits under the PDF toolbar, on the dark PDF panel
Arrow buttons (desktop only)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-ring40×40 (with px-2 pad → 44)aria-label="Vorherige Datei" / "Nächste Datei"
Track (scroll container)flex-1 flex gap-1 overflow-x-auto snap-x snap-mandatory scroll-smoothmobile: gesture-swipe; desktop: arrows scroll scrollBy({left:±120})
File chip · inactivesnap-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-214px / h 40pxmax-width 180px, title truncates with truncate
File chip · activesame base + bg-accent text-primary dark:bg-primary dark:text-primary-fgaria-current="true"14px / h 40pxcaret "▸" prefix via ::before — redundant non-colour cue
File chip · errorbg-danger/15 text-danger border border-dashed border-dangertooltip = error message, ⚠ suffix, still clickable
Chip number prefixbg-primary/20 dark:bg-primary-fg/20 rounded-sm px-1 text-xs font-extrabold12px · 800"1", "2", …
+ +

"Nur diese Datei" card (per-file scope, title only)

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Card containerbg-accent-bg border border-accent rounded-sm p-4 mb-4padding 16pxrenders for N ≥ 2 only; at N = 1 title lives outside any card
Scope badgebg-primary text-primary-fg rounded-sm px-2 py-1 text-xs font-extrabold uppercase tracking-wide12px · 800Paraglide bulk_only_this_file
Subtitle (1 / 5 · filename)text-xs font-bold text-ink-2 tracking-tight truncate12pxfilename truncates when long
Title inputh-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-ring44px · 16pxstarts as suggested state; mint border drops to border-line on first user edit
+ +

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Card containerbg-muted border border-line rounded-sm p-4 mb-3padding 16pxneutral (no accent tint) — signals "shared, not special"
Scope badgebg-accent text-primary dark:bg-primary dark:text-primary-fg rounded-sm px-2 py-1 text-xs font-extrabold uppercase tracking-wide12px · 800Paraglide bulk_shared_count "Gilt für alle {count}"
Field gridgrid grid-cols-1 md:grid-cols-2 gap-312px gapsingle column at ≤ 767px, two cols at ≥ 768px
Disabled (empty) statewrap card in aria-disabled="true" opacity-60 pointer-events-noneonly when N = 0; turns live on first file drop
+ +

Save bar (primary CTA, tri-mode)

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Primary save · idleh-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-90044px · 14pxlabel {count} speichern → — Paraglide plural "Speichern" at 1, "N speichern" at ≥ 2
Primary save · savingsame + bg-green-700/90 cursor-progress relative overflow-hidden with inner progress bar absolute inset-y-0 left-0 bg-white/20 transition-[width]replaces label with "Lade Datei {i} von {n}…" when > 500ms
"Als Platzhalter"h-11 px-4 border border-line bg-surface text-ink-2 font-bold rounded-sm text-sm44pxposts with metadataComplete=false for all N documents
"Verwerfen" skip linktext-sm font-bold text-ink-3 hover:text-ink min-h-[44px] flex items-center14px · 44pxat N ≥ 2 this is a link to "Alle verwerfen" in the top bar; hide in save bar to avoid duplicate
+ +

Mobile responsive (≤ 767px)

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementTailwindPx / valueNote
Tab barflex h-[38px] border-b border-line — reuses DocumentEditLayout's mobile tabs38pxlabels: "Vorschau" (with "1/N" pill) · "Angaben"
Tab (active)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-wide14px · 48px taparia-selected="true"
"1/N" tab pillml-2 bg-accent text-primary dark:bg-primary dark:text-primary-fg rounded-full px-2 py-0.5 text-xs font-extrabold12pxhide at N = 1
File switcher on mobilearrows removed (md:flex), track full-width, snap-swipeannouncer fires on snap-end: "Datei 3 von 5"
+ +

Paraglide keys (de/en/es)

+ + + + + + + + + + + + + + + + + + +
Keydeenes
upload_dropzone_headingEine oder mehrere Dateien ablegenDrop one or more files hereSuelta uno o más archivos aquí
upload_dropzone_bodyFü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.Each file becomes its own document. The title is pre-filled from the filename and editable per file — all other fields are shared.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.
upload_dropzone_ctaDateien auswählenChoose filesElegir archivos
upload_dropzone_formatsPDF · JPEG · PNG · TIFF · max 50 MB pro DateiPDF · JPEG · PNG · TIFF · max 50 MB per filePDF · JPEG · PNG · TIFF · máx. 50 MB por archivo
bulk_count_creating{count} werden erstellt{count} will be created{count} se crearán
bulk_discard_allAlle verwerfenDiscard allDescartar todo
bulk_discard_confirm{count} Dateien verwerfen?Discard {count} files?¿Descartar {count} archivos?
bulk_only_this_fileNur diese DateiThis file onlySolo este archivo
bulk_only_subtitle{index} / {count} · {filename}{index} / {count} · {filename}{index} / {count} · {filename}
bulk_shared_countGilt für alle {count}Applies to all {count}Se aplica a todos los {count}
bulk_save_cta{count, plural, one {Speichern →} other {{count} speichern →}}{count, plural, one {Save →} other {Save {count} →}}{count, plural, one {Guardar →} other {Guardar {count} →}}
bulk_save_progressLade Datei {i} von {count}…Uploading file {i} of {count}…Subiendo archivo {i} de {count}…
bulk_save_placeholderAls PlatzhalterSave as placeholderGuardar como marcador
bulk_file_nav_prevVorherige DateiPrevious fileArchivo anterior
bulk_file_nav_nextNächste DateiNext fileSiguiente archivo
bulk_announce_count{count} Dateien bereit zum Speichern{count} files ready to save{count} archivos listos para guardar
+ +
+
Interaction + behaviour spec
+
    +
  • Drop a file after the first batch → append to the end of the switcher, auto-focus the new chip, the title input inherits the filename-derived suggestion.
  • +
  • Remove a file 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.
  • +
  • Filename → title: basename.replace(/\.(pdf|jpe?g|png|tiff?)$/i, '').replace(/[_-]+/g, ' ').trim(). Input marked suggested (mint border + accent-bg) until the user edits it, then border drops to border-line.
  • +
  • Title always rendered: 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.
  • +
  • Keyboard nav in the switcher: / cycle files when focus is inside the strip. Tab moves focus out. Enter/Space selects a chip (identical to click).
  • +
  • Focus on file switch: 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.
  • +
  • Save flow: one POST /api/documents/quick-upload with files[] + a JSON metadata part containing shared fields plus a titles[] array matched by index. Response splits into created[] / updated[] / errors[]; show a post-save toast + mark error chips red. Successful creates redirect to /documents (not to a single detail page — we just made N).
  • +
  • Progress indicator: 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.
  • +
  • Live-region announces: <div role="status" aria-live="polite"> 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.
  • +
  • Page leave protection: if N ≥ 1 and shared form has any non-default value, beforeunload prompts. Suppress during explicit "Alle verwerfen" / save.
  • +
+
+ +
+
Edge cases + a11y requirements
+
    +
  • Single file, matching #294 exactly: at N = 1 do NOT render FileSwitcherStrip, 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.
  • +
  • Duplicate filenames in one batch: accept both, show a warning icon next to both chips. Backend creates two documents with distinct UUIDs.
  • +
  • Mixed content types: PDF + image + TIFF in one batch — preview panel switches renderer per active file (DocumentEditLayout already handles this).
  • +
  • One file fails upload: chip goes red-dashed. Other files still create. Post-save toast lists the failure with the backend error code mapped via getErrorMessage(). "Retry" button on the chip re-POSTs that file alone.
  • +
  • Large batches (> 20 files): switcher scrolls horizontally. At > 30 consider a "Jump to file…" combobox (follow-up, not v1).
  • +
  • Screen reader on file switch: role="tablist" is wrong here because the chips aren't tabs for a tabbed content; use a visually-hidden "Datei auswählen" label with aria-live="polite" announcement on selection change.
  • +
  • Colour-alone check (WCAG 1.4.1): active chip = colour and caret prefix and aria-current="true". Error chip = colour and dashed border and ⚠ suffix. Three redundant cues on both states.
  • +
  • Contrast — light: mint (#a1dcd8) on navy (#012851) = 7.2:1 AAA · navy on mint = 7.2:1 (inverse). Title field suggested uses ink (#012851) on accent-bg (15% mint) = ~9:1 AAA.
  • +
  • Contrast — dark: 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.
  • +
  • Reduced motion: the 200ms switcher-fade and progress bar fill respect prefers-reduced-motion: reduce via the existing global @media rule — cuts transition-duration to 0.01ms.
  • +
  • Touch targets: 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 ::after + a sibling button) to avoid the "I tried to select but removed" trap.
  • +
+
+ +

Component tree (frontend)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentStatusResponsibility
documents/new/+page.svelterewriteState owner: files array, active index, shared metadata. Mode switches by files.length.
DocumentEditLayout.svelteaccept propsTakes { files, activeIndex }; emits switch + remove events. Existing props for single-file unchanged.
BulkDropZone.sveltenewFull-panel drop target for N=0 and for "add more" at N≥1. Wraps the dashed box + CTA + formats line.
FileSwitcherStrip.sveltenewHorizontal chip list + arrows + aria-live region. Emits select + remove.
ScopeCard.sveltenewWraps "Nur diese Datei" / "Gilt für alle N" with the correct badge + tint. One component, two variants.
UploadSaveBar.svelteextendAlready exists for single-file. Add plural-count label + determinate progress state.
+ +

Backend contract — single POST for the whole batch

+ + + + + + + + + + + + + + + + + + + + + + +
ElementTailwind / ValueNote
EndpointPOST /api/documents/quick-uploadalready exists — accepts List<MultipartFile> files
Request — files partrepeated files multipart entries, one per file, in UI orderbackend preserves order so titles[i] matches files[i]
Request — metadata partJSON part named metadata containing { senderId, receiverId, documentDate, location, tags[], archiveBox, archiveFolder, metadataComplete, titles[] }backend change: add a new overload of quickUpload that reads the JSON part and applies shared fields to every created Document
Response{ created: DocRef[], updated: DocRef[], errors: { filename, code }[] }existing shape — no breaking change
+ +
+
Out of scope for this spec
+
    +
  • Per-file metadata overrides beyond title: a future "expand row" could let the user override sender / date per file. Not in v1.
  • +
  • Drag-to-reorder: the order of created documents matches the drop order. Reordering is a nice-to-have.
  • +
  • Resume interrupted uploads: if the browser crashes mid-save, the user restarts. Chunked/resumable is a follow-up.
  • +
  • Folder upload (webkitdirectory): expands the batch beyond what the user meant. Off by default; revisit if users ask.
  • +
  • Pre-populate sender/date from OCR on a sample file: interesting but async — ships as a second feature.
  • +
+
+
+ +
+ +