fix(bulk-upload): spec-compliant split-panel layout with local PDF preview

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>
This commit is contained in:
Marcel
2026-04-24 19:17:36 +02:00
committed by marcel
parent ef7a51fe30
commit 539842e849
8 changed files with 311 additions and 187 deletions

View File

@@ -10,6 +10,7 @@ export interface FileEntry {
file: File;
title: string;
status: 'idle' | 'error';
previewUrl: string;
}
function makeFiles(n: number): FileEntry[] {
@@ -17,7 +18,8 @@ function makeFiles(n: number): FileEntry[] {
id: `id-${i}`,
file: new File([''], `file${i}.pdf`),
title: `File ${i}`,
status: 'idle' as const
status: 'idle' as const,
previewUrl: ''
}));
}
@@ -65,7 +67,13 @@ describe('FileSwitcherStrip', () => {
it('error chip has aria-label containing warning indicator', async () => {
const files: FileEntry[] = [
{ id: 'e1', file: new File([''], 'bad.pdf'), title: 'Bad file', status: 'error' }
{
id: 'e1',
file: new File([''], 'bad.pdf'),
title: 'Bad file',
status: 'error',
previewUrl: ''
}
];
const { container } = render(FileSwitcherStrip, {
files,
@@ -85,7 +93,7 @@ describe('FileSwitcherStrip', () => {
onSelect: vi.fn(),
onRemove: vi.fn()
});
const firstBtn = container.querySelectorAll('[role="button"]')[0] as HTMLElement;
const firstBtn = container.querySelectorAll('[data-chip-id]')[0] as HTMLElement;
firstBtn.focus();
await userEvent.keyboard('{ArrowRight}');
const focused = document.activeElement;