# Project Agent Guide ## Project overview This repository contains **juri-merger**, a German-language SvelteKit application branded as **beA-Edit**. Users add one or more ZIP archives in the browser, inspect and reorder supported attachments, optionally group attachments into named sub-documents, and export merged PDFs. Nested ZIP archives and XJustiz metadata are handled client-side; there is no upload, server-side archive processing, database, analytics, or persistence of case data. The application also contains German imprint and privacy-policy routes. ## Technology stack - SvelteKit 2 and Svelte 5, with runes mode forced for project source files - TypeScript in strict mode - Vite 8 - Tailwind CSS 4 through `@tailwindcss/vite` - shadcn-svelte 1 with the `nova` style and CSS-variable theme tokens - bits-ui, tailwind-variants, clsx, and tailwind-merge - Lucide Svelte icons plus project-specific SVG icon components - fflate for ZIP extraction and image-export ZIP creation - pdf-lib for PDF assembly, `@cantoo/pdf-lib` for encryption, and pdfjs-dist for rendering - `@sveltejs/adapter-node` - pnpm with a committed lockfile - Prettier with Svelte and Tailwind plugins Use **pnpm**, not npm or yarn. The production container uses Node 24 and pnpm 11.15.1. ## Commands ```sh pnpm install # Install dependencies pnpm dev # Start the development server pnpm build # Create the adapter-node production build pnpm preview # Preview the production build pnpm check # Run Svelte and TypeScript diagnostics pnpm check:watch # Run diagnostics in watch mode pnpm test:e2e # Run the Playwright toolbox tests in Chromium pnpm lint # Check repository formatting with Prettier pnpm format # Format the repository with Prettier ``` For code changes, run at least: ```sh pnpm check pnpm test:e2e pnpm lint pnpm build ``` For documentation-only changes, a targeted Prettier check is sufficient. If pre-existing warnings or formatting failures remain outside changed files, report them instead of making unrelated edits. The Docker image can be built from the repository root. It installs with `--frozen-lockfile`, builds and prunes dependencies, then runs `node build/index.js` as a non-root user on port 3000. ## Repository structure ```text src/ ├── app.html # German HTML shell ├── app.d.ts # SvelteKit application declarations ├── lib/ │ ├── actions/ripple.ts # Button ripple Svelte action │ ├── assets/ # Favicon and local Nunito fonts │ ├── components/ │ │ ├── AttachmentPreview.svelte # Browser-rendered PDF/image thumbnails │ │ ├── BeaArchiveProcessing.svelte │ │ ├── BeaWorkspaceControls.svelte # beA workspace toolbar, settings popover, dialogs │ │ ├── ProcessedZipArchiveEditor.svelte │ │ ├── SubDocumentEditor.svelte │ │ ├── FileDropzone.svelte # Shared PDF/image/ZIP file picker and drop target │ │ ├── ZipFilePicker.svelte # Compact ZIP picker button with hidden resettable input │ │ ├── PdfPageGrid.svelte # Accessible PDF thumbnail selection grid │ │ ├── RasterizationWarning.svelte │ │ ├── ZipDropzone.svelte # beA-specific wrapper around FileDropzone │ │ ├── icons/ # Project-specific SVG components │ │ └── ui/ # Local shadcn-svelte component source │ ├── services/ │ │ ├── xml-reading.service.ts # Namespace-tolerant XJustiz parsing │ │ └── zip-inflating.service.ts # Async fflate wrapper │ ├── pdf-processing.ts # Reusable PDF merge helper │ ├── pdf-thumbnails.ts # Managed pdfjs thumbnail loading and cleanup │ ├── utils.ts # cn() and shared component utility types │ └── zip-processing.ts # Archive model, transforms, and PDF export └── routes/ ├── +layout.svelte # Shared logo header and branded footer ├── +page.svelte # Toolbox hub and global beA ZIP drop target ├── layout.css # Tailwind, shadcn, theme, fonts, global CSS ├── tools/ │ ├── +layout.svelte # Shared back-navigation shell │ ├── bea/+page.svelte # Existing ZIP archive workflow │ └── {merge,separate,watermark,encrypt,decrypt,compress,convert}/ ├── datenschutz/+page.svelte # Privacy policy └── impressum/+page.svelte # Imprint static/ # Public logo, footer art, and robots.txt components.json # shadcn-svelte registry configuration Dockerfile # Node adapter production image plan/ # Design and implementation notes ``` Generated directories such as `.svelte-kit/`, `build/`, and `node_modules/` must not be edited or committed. ## Application flow and state - `/` is the toolbox hub. A ZIP dropped anywhere on that route is queued in memory and forwarded to `/tools/bea`; the file is not uploaded or persisted. - `tools/bea/+page.svelte` owns selected files, processed archives, pending work, and the thumbnail-width preference. Thumbnail width is the only persisted UI setting and uses localStorage. - The loaded workspace renders `BeaWorkspaceControls.svelte`, a top-aligned toolbar with the workspace status and two archive lifecycle actions: - **“Neue ZIP bearbeiten”** (replace) opens a ZIP picker and, after the user confirms the replacement dialog, calls `replaceWithZipFiles(files)` which resets the workspace and enqueues the new files against the incremented generation. Files picked for replacement stay in temporary component state until confirmation; cancelling the picker or the dialog must never mutate the current workspace. - **“Weitere ZIP hinzufügen”** (append) lives in the settings popover, uses the plain append path (`handleFilesSelected`), and never clears current archives. Keep the two labels distinct: replace is “Neue ZIP bearbeiten”, append is “Weitere ZIP hinzufügen”. - `BeaWorkspaceControls.svelte` owns only ephemeral UI state (popover/dialog open state and pending replacement files). The route remains the source of truth for files, jobs, archives, and the persisted thumbnail width. - `resetWorkspace()` is the single teardown helper: it increments `archiveGeneration` and clears selected files, processed archives, and jobs. Never clear those arrays without going through it. - Accepted ZIP types and the file filter for the dropzone and both compact pickers are centralized in `src/lib/zip-selection.ts`; `ZipFilePicker.svelte` resets its input after every selection so the same file can be chosen again. - ZIP files are processed concurrently. `archiveGeneration` prevents results from an old batch from reappearing after the user deletes all archives **or** confirms a replacement. - `ProcessedZipArchiveEditor.svelte` edits one archive and emits immutable replacements through `onArchiveChange`; the route-level archive array remains the source of truth. - Attachments may remain loose or belong to exactly one sub-document. Drag-and-drop supports reordering and moving attachments between lists, while sub-document headers can be reordered. - Selection mode creates sub-documents from loose attachments. Exiting with **Fertig** or Escape clears the pending selection. - Per-archive export supports one flattened PDF or separate PDFs for sub-documents. Each sub-document also has its own direct download action that exports only that sub-document's attachments. Bulk export from the page always creates one flattened PDF per archive. When changing archive transforms, preserve these invariants: - An attachment move must not duplicate or discard data. - Sub-document IDs remain stable across edits and reordering. - Single-PDF order is all sub-documents in order, each attachment in order, followed by loose attachments in order. - Dissolving a sub-document appends its attachments to the loose list. - State helpers in `zip-processing.ts` should return new archive/list objects rather than mutating component-owned state. ## ZIP, XML, and PDF processing All case-file processing is intentionally local to the browser. - `extractZipArchives` recursively inflates nested ZIP files. - Ignore macOS metadata (`__MACOSX`, `.DS_Store`, and `._*`) and unsupported entries. - `xjustiz_nachricht.xml` is metadata, not an export attachment. Its sender, receiver, and declared document names determine the generated PDF name and preferred attachment order. - XJustiz element matching must remain namespace tolerant. Malformed or absent metadata falls back safely instead of preventing extraction. - Supported attachments are PDFs and the image extensions listed in `zip-processing.ts`. Update the extension classification and downstream preview/export behavior together when adding formats. - PDF attachments are copied with pdf-lib. JPEG and PNG files can be embedded directly; other image formats are decoded through browser image/canvas APIs and converted to PNG first. - `AttachmentPreview.svelte` dynamically loads pdfjs-dist in `onMount`, copies PDF bytes before rendering, and revokes object URLs during cleanup. Preserve those memory and data-integrity safeguards. - Empty archives/export groups should continue to fail explicitly rather than creating invalid PDFs. Browser-only APIs used by this flow include `File`, `DOMParser`, `Image`, canvas, `Blob`, `URL`, localStorage, and `crypto.randomUUID()`. Do not invoke browser-only work during SSR or at module initialization. Keep it in event handlers, `onMount`, effects guarded by hydration, or functions only called in the browser. ## Svelte conventions - Use Svelte 5 runes and current event syntax: `$props()`, `$state`, `$derived`, `$effect`, snippets, `{@render ...}`, and handlers such as `onclick` rather than legacy APIs. - Keep component scripts typed with `