# Plan: Download an individual sub-document **Status: Done** ## Goal Each sub-document displayed inside a processed ZIP archive shall have its own download action. The action creates and downloads one PDF containing only that sub-document's attachments, in their current order. This is an additional interaction. The existing archive-level export modes remain available: - **Eine PDF** continues to flatten all sub-documents and loose attachments into one PDF. - **Getrennt** continues to download every non-empty sub-document plus the loose-attachment PDF. - The new sub-document action downloads only the selected sub-document and does not include loose attachments or sibling sub-documents. ## Current implementation - `ProcessedZipArchiveEditor.svelte` owns archive-level download state and the browser download helper. - `SubDocumentEditor.svelte` renders each sub-document but currently exposes only edit, drag/drop, and dissolve callbacks. - `mergeAttachments()` in `src/lib/zip-processing.ts` already creates a PDF from an ordered attachment list. - `buildArchiveExport(archive, 'separate')` already defines the desired sub-document naming convention: `{archiveName} - {subDocumentName}.pdf`. It currently creates exports for every sub-document, so it should not be called for an individual download. ## UX and behavior Add a **PDF herunterladen** button to each sub-document header, before **Auflösen**. - Clicking it downloads one PDF for that sub-document. - The PDF page order matches `subDocument.attachments` exactly. - The filename remains consistent with separate archive export: `{archiveName without .pdf} - {subDocumentName without .pdf}.pdf`. - The action is disabled when the sub-document has no attachments. - While an individual PDF is being generated, its button reads **PDF wird erstellt** and cannot be triggered again. - Only one PDF-generation operation may run in an archive editor at a time. While an individual download is active, archive-level and sibling download actions are disabled; while archive-level export is active, individual actions are disabled. - A failure leaves the archive and sub-document unchanged and shows the existing inline error area with a sub-document-specific German message. - Existing drag/drop, rename, selection, dissolve, and archive export behavior must remain unchanged. ## Implementation ### 1. Add a focused export helper Update `src/lib/zip-processing.ts` with an exported helper: ```ts export const buildSubDocumentExport = async ( archiveName: string, subDocument: SubDocument ): Promise ``` Behavior: 1. Reject an empty attachment list with the same explicit empty-export behavior used by `mergeAttachments()`. 2. Merge `subDocument.attachments` by calling `mergeAttachments()`. 3. Return one `ExportUnit` using the existing archive/sub-document filename convention. 4. Keep `.pdf` normalization centralized through the existing `stripPdfExtension()` helper. Refactor the `mode === 'separate'` branch of `buildArchiveExport()` to call `buildSubDocumentExport()` for each non-empty sub-document. This ensures bulk-separate and individual exports cannot drift in PDF contents or naming. Preserve sequential generation and the existing loose-attachment export behavior. Do not construct a temporary `ProcessedZipArchive` and do not invoke `buildArchiveExport(..., 'separate')` for one item: both approaches encode the wrong scope and risk exporting unrelated attachments later. ### 2. Add individual download orchestration to the archive editor Update `src/lib/components/ProcessedZipArchiveEditor.svelte`: - Import `buildSubDocumentExport`. - Add `downloadingSubDocumentId: string | null` state alongside `isMerging` and `mergeError`. - Derive whether any export is active from `isMerging || downloadingSubDocumentId !== null`. - Add `downloadSubDocument(subDocument: SubDocument)` that: 1. Returns when another export is active or the sub-document is empty. 2. Stores the selected sub-document ID and clears `mergeError`. 3. Calls `buildSubDocumentExport(archiveName, subDocument)`. 4. Passes the returned bytes and name to the existing `downloadPdfBytes()` helper. 5. On failure, logs context including the archive and sub-document and sets `Teildokument „{name}“ konnte nicht als PDF erstellt werden.`. 6. Clears the active ID in `finally`. - Update `downloadArchive()` to guard against any active export, not only `isMerging`. - Disable the archive-level download button and export-mode controls while an individual export is active. The mode controls should remain visible and retain their selected value. - Pass these props to every `SubDocumentEditor`: - `onDownload={() => downloadSubDocument(subDocument)}` - `isDownloading={downloadingSubDocumentId === subDocument.id}` - `downloadDisabled={isMerging || downloadingSubDocumentId !== null}` The browser download implementation stays in the parent so Blob/object-URL creation, filename normalization, loading coordination, and errors remain centralized. ### 3. Add the sub-document header action Update `src/lib/components/SubDocumentEditor.svelte`: - Import the existing local `Button` component. - Extend `Props` with: - `onDownload?: () => void` - `isDownloading?: boolean` - `downloadDisabled?: boolean` - Give all new props safe defaults. - Render a bordered button before **Auflösen**: - Normal label: **PDF herunterladen** - Active label: **PDF wird erstellt** - Disabled when `downloadDisabled`, `isDownloading`, or `attachments.length === 0` - Keep the control as a native button through the shared `Button` component, with a clear accessible label that includes the sub-document name if the visible label alone is ambiguous in assistive technology. - Prevent the button from participating in drag initiation in the same way as the existing name and dissolve controls. - Keep **Auflösen** visually distinct as the destructive action; do not change its behavior. Use existing button variants and semantic theme utilities. Do not introduce a new UI dependency or a second download utility in the child component. ## Files changed - `src/lib/zip-processing.ts` - Add `buildSubDocumentExport()`. - Reuse it from separate archive export. - `src/lib/components/ProcessedZipArchiveEditor.svelte` - Coordinate individual export state, errors, and browser download. - `src/lib/components/SubDocumentEditor.svelte` - Add the individual download button and callback props. - `agents.md` - Clarify that per-archive separate export and direct per-sub-document export are both supported. No route, server endpoint, persistence, package, lockfile, or Docker changes are required. ## Verification ### Automated checks Run: ```sh pnpm exec prettier --write \ src/lib/zip-processing.ts \ src/lib/components/ProcessedZipArchiveEditor.svelte \ src/lib/components/SubDocumentEditor.svelte \ agents.md pnpm check pnpm lint pnpm build ``` There is no test framework in the repository. Do not add one solely for this feature. If a test framework is introduced separately, cover `buildSubDocumentExport()` with non-empty, empty, ordering, and filename cases. ### Manual scenarios 1. Create an archive with two sub-documents and at least one loose attachment. 2. Download the first sub-document and verify exactly one PDF is triggered. 3. Confirm the PDF contains only the first sub-document's attachments and preserves their displayed order. 4. Confirm the filename is `{archive} - {sub-document}.pdf` without duplicate `.pdf` suffixes. 5. Download the second sub-document and verify it has independent contents and naming. 6. Confirm an empty sub-document's download action is disabled. 7. Start an individual export and verify duplicate, sibling, and archive-level export actions cannot start until it finishes. 8. Force an invalid PDF/image merge and verify a German inline error appears, loading state clears, and editor state is unchanged. 9. Verify **Eine PDF** still exports all sub-documents followed by loose attachments. 10. Verify **Getrennt** still exports every non-empty sub-document and the loose-attachment PDF. 11. Verify rename, reorder, move, selection, Escape, and **Auflösen** interactions still work. 12. Verify the new control remains usable on a narrow viewport and by keyboard. ## Acceptance criteria - Every non-empty sub-document has a directly accessible download action. - The action downloads one PDF containing only that sub-document's ordered attachments. - Individual and archive-level exports use the same filename convention and merge implementation. - Empty sub-documents cannot be exported. - Loading and error states are visible, duplicate operations are prevented, and state always resets. - Existing archive-level export behavior and all editor interactions are preserved. - `pnpm check`, `pnpm lint`, and `pnpm build` pass without new issues.