Production bug — the backend serialises the document UUID as `id`, but
BulkEditEntry typed it as `documentId`. The runtime cast in /documents/
bulk-edit/+page.svelte was a TypeScript lie: every `entry.documentId`
became undefined, the SvelteMap collapsed all selections under the
undefined key, and the PATCH fired with `documentIds: []` (which the
controller correctly rejected with 400). Field semantics ACs could
therefore never fire end-to-end.
Renamed `BulkEditEntry.documentId` → `id`. The FileEntry built from each
summary still carries both `id` (local map key) and `documentId` (PATCH
payload) so the save handler is unchanged.
Reported by Elicit (B1) on PR #331.
Refs #225
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five Playwright scenarios on the bulk-edit feature:
- sticky bar appears with count when checkboxes are toggled
- Alles aufheben hides the bar
- Massenbearbeitung navigates to /documents/bulk-edit and the edit-mode
onboarding callout is rendered
- direct navigation to /documents/bulk-edit with no selection redirects back
- the same bar drives /enrich (skipped when the test DB has no incomplete docs)
Refs #225
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server load redirects READ_ALL-only users (or unauthenticated) to /documents.
Page load: onMount reads bulkSelectionStore — redirects to /documents when the
store is empty, otherwise POSTs the IDs to /api/documents/batch-metadata and
hands the resulting summaries to BulkDocumentEditLayout in mode="edit".
Refs #225
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New FieldLabelBadge component (additive / replace variants, WCAG AA contrast)
- WhoWhenSection: hideDate prop, editMode prop renders badges next to sender
and receivers, hides the meta_location field
- DescriptionSection: editMode prop renders badges next to tags and archive
fields; new bindable archiveBox / archiveFolder inputs only in editMode
- PersonTypeahead: optional badge prop forwards to FieldLabelBadge
- FileSwitcherStrip FileEntry: file is now optional, documentId added so
edit-mode entries reference an existing document by UUID
- BulkDocumentEditLayout: mode prop branches drop zone / read-only title /
callout / save handler. Edit save chunks 500 IDs per PATCH, stops on chunk
failure with retry, marks per-document errors as chips, clears the bulk
selection store on full success.
Refs #225
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BulkSelectionBar component: sticky bottom bar shown only when canWrite
and selection is non-empty. Buttons meet WCAG 44px touch targets and
iOS safe-area inset is honoured.
- Bar mounted on /documents and /enrich.
- Alle X editieren button on /documents replaces the selection with
every UUID matching the active filter (via /api/documents/ids) and
jumps to /documents/bulk-edit.
Refs #225
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each row in the document search list and the enrichment queue gets a
WCAG-compliant (44px touch target) checkbox bound to bulkSelectionStore.
Checkbox click does not trigger the row's stretched-link navigation —
it sits inside the z-10 content sibling, the link is in the z-0 sibling,
so click events do not bubble between them.
Refs #225
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Module-singleton live accumulator: selection persists across pagination
and route changes within /documents and /enrich. Cleared on successful
bulk save or via Alles aufheben.
Refs #225
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 14 new Paraglide keys in de/en/es for the bulk-edit UI strings (selection
bar, callout, badges, save progress, retry, error)
- BULK_EDIT_TOO_MANY_IDS added to errors.ts type union and getErrorMessage()
- Regenerated api.ts now includes /api/documents/{bulk,batch-metadata,ids}
and the DocumentBulkEditDTO / BulkEditResult / DocumentBatchSummary schemas
Refs #225
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rule cards now show before→after examples; strikethrough rule input
renders with CSS line-through so the visual context is honest
- Illegible-words rule shows output only — can't represent unreadable
text as readable characters
- Intro drops fictional family names in favour of "egal wer tippt"
- Wikipedia card copy is more direct; link uses icon instead of
parenthetical "(öffnet in neuem Tab)" text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses getConfirmService() (optional — null fallback when context is absent so
unit tests that don't exercise the discard path need no CONFIRM_KEY context)
and the new bulk_discard_confirm i18n key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds pointer-events-none left/right gradient fade overlays on the
FileSwitcherStrip track div so mouse-only users can see when more
chips are hidden beyond the visible area. The scrollbar is hidden
(scrollbar-width:none) so gradients are the only overflow signal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chip label text increased from 11px to 12px (text-xs) and number badge
from 9px to 11px for the 60+ senior audience on laptops/tablets.
After removing a chip via the × button, focus moves to the previous chip
(falling back to the next chip when the first chip is removed) so keyboard
users are not stranded on <body>. Uses Svelte tick() to wait for DOM update.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
save() now wraps each chunk fetch in try/catch — a thrown network error
marks all files in that chunk as errored. Also handles HTTP 200 responses
with a non-empty errors array (partial success): only the named filenames
are marked as errored rather than all files in the chunk. Navigation is
suppressed whenever any file fails.
Tests added:
- network error marks all chunk files as errored, no navigation
- HTTP 200 with errors array marks only affected files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a saving $state flag that blocks re-entry while a chunk upload is
in flight. The UploadSaveBar save button is disabled via a new disabled
prop while saving is true. Tested: clicking Save twice fires fetch only
once.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Senior users on tablets need at least 44×44px touch targets (WCAG 2.2).
Added min-h-[44px] flex items-center px-2 to the discard button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The error-path test (goto not called on failure) had no matching positive
assertion. Added: save() navigates to /documents when all chunks succeed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generated type had tags?: string but Java DTO declares List<String> tagNames.
Corrected to tagNames?: string[] to match the backend contract.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ! indicator was aria-hidden with no sr-only fallback, making failed
uploads invisible to assistive technology. Added sr-only span with
bulk_file_error_chip_label before the visual indicator.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/brand-mint/ never matched (component uses border-accent bg-accent-bg);
companion test also updated to assert the meaningful negative.
getByText('5') fixed to exact:true to avoid strict-mode ambiguity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BulkDropZone: link description <p> to drop zone region via aria-describedby
- UploadSaveBar: add explicit aria-valuenow/aria-valuemin/aria-valuemax to
<progress> element for consistent screen reader support across browsers
- FileSwitcherStrip: add non-color error indicator (red !) to error chips so
error state is not communicated by color alone (WCAG 1.4.1)
- BulkDocumentEditLayout: comment explaining why raw fetch is used instead of
a SvelteKit form action (chunked FormData with per-chunk progress tracking)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
save() was marking the first N files in a chunk as errored (where N = the
error count returned by the backend), but the backend errors are keyed by
filename. A failure for file[2] would incorrectly mark file[0] as the error.
Now builds a Set of error filenames and matches chunk entries by file.name.
Test added: save marks only the file whose filename matches the backend error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three message files had a bare `<<<<<<< HEAD` at line 814 with no
corresponding separator or closing marker, making them invalid JSON and
breaking the Paraglide build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Long filenames caused chips to overflow the strip. Added max-w-[8rem]
and truncate on the title span, plus a title attribute for full text
on hover.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prev/next scroll buttons were 24×20px, below the WCAG 2.2 SC 2.5.5
minimum of 44×44px. Changed to h-[44px] w-[44px].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The sr-only aria-live div was always empty, so screen readers never
announced file switches. Derived activeAnnouncement from the active
entry and bound it to the div's text content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
<progress> had no accessible name, failing WCAG 1.3.1 and 4.1.2.
Labels it with the already-existing bulk_upload_progress i18n key.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'Neues Dokument' / 'Neue Dokumente' in BulkDocumentEditLayout topbar
bypassed Paraglide. Added bulk_title_single and bulk_title_multi keys
to de/en/es message files and switched to m.*() calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
goto('/documents') fired unconditionally, discarding error chips and
leaving the user with no feedback on which files failed. Now only
navigates when hadErrors is false after all chunks complete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tags were silently dropped because the metadata object built in save()
never included a tagNames field; they never reached the backend.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add bulk_drop_desc, bulk_select_files, bulk_drop_zone_label, bulk_remove_file
keys to de/en/es message files
- BulkDropZone: use m.bulk_drop_zone_label(), m.bulk_drop_desc(),
m.bulk_select_files() — removes all hardcoded German
- FileSwitcherStrip: use m.bulk_remove_file() on × button; move aria-live
from <ul> to a dedicated visually-hidden region above the strip (screen
readers now announce changes without coupling the live region to the list)
- Spec: import FileEntry from component instead of re-declaring; use
data-remove-id selector instead of hardcoded German aria-label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The autofocus prop was added conditionally but still triggered on the
bulk-upload page. Removing it completely — callers that need focus
management can handle it independently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PersonMultiSelect naturally renders at 44px due to nested padding (outer p-2 + inner p-1).
Apply py-3 px-2 to the date input and PersonTypeahead default mode so all three fields
align visually.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Default mode was text-base (16px) and rounded-md — date field uses text-sm
(14px) and rounded. Aligning these makes Sender/Date/Receiver rows consistent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
min-h-[42px] → min-h-[38px] to match p-2 text-sm input height.
Add shadow-sm (was missing vs date/sender inputs).
focus-within:ring-1 ring-ink → focus-within:ring-2 ring-focus-ring to match
the focus style used consistently across all other form inputs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace JS navHeight measurement with CSS var(--header-height) so the fixed
panel renders in its final position on first paint — no onMount shift.
Add autofocus prop to WhoWhenSection (default true, preserves document-edit
behaviour) and pass autofocus={false} from BulkDocumentEditLayout so the date
field does not steal focus before the user has even dropped any files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop non-PDF accept types from file input and update format hint strings
in all three languages. JPEG/PNG/TIFF were never officially supported.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Drop zone box doubled: max-w-xl, larger icon (80px), bigger padding and text
- Title field wrapped in its own card (matches WhoWhenSection/DescriptionSection)
- Removed double-wrapping outer card around WhoWhenSection + DescriptionSection
- Added space-y-4 between form sections for consistent breathing room
- ScopeCard per-file label: text-accent → text-primary for legible contrast in light theme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rewrites BulkDocumentEditLayout to match the spec exactly:
- Fixed viewport layout (same as DocumentEditLayout) filling viewport below nav
- Split panel visible in all states (N=0/1/≥2) — was fullscreen dark drop zone
- N=0: centered drop-zone-box in left panel; shared form visible but greyed out
- N≥1: real PDF preview via URL.createObjectURL (no server upload required)
- N≥2: FileSwitcherStrip at bottom of left panel; count pill + discard in topbar
- FileEntry gains previewUrl; blob URLs created on add, revoked on remove/destroy
- save() checks response.ok and marks failed files with status: 'error'
- BulkDropZone redesigned: spec-accurate box with circular mint icon, serif title
- FileSwitcherStrip: number badges, arrows, keyboard nav via data-chip-id selector
- ScopeCard, UploadSaveBar: hardcoded German replaced with Paraglide i18n keys
- +page.svelte simplified to bare component render (layout is self-contained)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New type from the bulk-upload metadata part added in #317.
Generated from backend running with --spring.profiles.active=dev.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the single-file form-action flow with BulkDocumentEditLayout,
enabling multi-file drag-and-drop upload with local preview, per-file
title editing, and shared metadata. Server load function unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Save bar with sticky positioning, a determinate progress bar while
uploading chunks, plural save CTA, and a destructive discard link.
Replaces broken ICU plural in bulk_save_cta with two-key approach
(bulk_save_cta_one / bulk_save_cta) since Paraglide 2.5 does not support
ICU plural syntax.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Card container with two variants: per-file (mint tint) and shared (neutral
with file-count badge). Used to visually separate per-file vs shared
metadata sections in the bulk upload layout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Horizontal chip strip for switching between files in a bulk upload session.
Supports keyboard navigation (arrow keys cycle within the strip), error state
chips, and onSelect/onRemove callbacks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full-panel drop target that supports multi-file selection via drag-and-drop
or file picker. Fires onFilesAdded callback with the full File array.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Converts a raw filename into a human-readable title candidate by
stripping the extension and replacing underscore/hyphen runs with spaces.
Reuses the existing stripExtension() helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>