17 KiB
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
novastyle 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-libfor 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
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:
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
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
│ │ ├── BatchFileList.svelte # Shared batch rows: status chips, downloads, retry
│ │ ├── 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
│ ├── batch.ts # Sequential batch engine for multi-file tools
│ ├── pdf-compression.ts # Shared compression presets and rasterize pipeline
│ ├── pdf-overlay.ts # Shared PDF overlay positions and color parsing
│ ├── 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,stamp,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.svelteowns 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”.
- “Neue ZIP bearbeiten” (replace) opens a ZIP picker and, after the user confirms the
replacement dialog, calls
BeaWorkspaceControls.svelteowns 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 incrementsarchiveGenerationand 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.svelteresets its input after every selection so the same file can be chosen again. - ZIP files are processed concurrently.
archiveGenerationprevents results from an old batch from reappearing after the user deletes all archives or confirms a replacement. ProcessedZipArchiveEditor.svelteedits one archive and emits immutable replacements throughonArchiveChange; 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.tsshould 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.
extractZipArchivesrecursively inflates nested ZIP files.- Ignore macOS metadata (
__MACOSX,.DS_Store, and._*) and unsupported entries. xjustiz_nachricht.xmlis 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.sveltedynamically loads pdfjs-dist inonMount, 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.
Batch processing pattern for single-PDF tools
Tools that transform one PDF per run (compress, watermark, encrypt, …) are being retrofitted to
accept multiple files. Follow the established compress retrofit when adding the pattern to another
tool; see plan/batch-processing-plan.md for the full design.
src/lib/batch.tsowns the engine:createBatchItem(file),runBatch(items, process, update, options?)andformatBytes.runBatchprocesses strictly sequentially (one PDF in flight for memory safety), never throws — per-item failures are caught and written to the item as a German error message — and consults the optionalshouldProcesspredicate so rows removed mid-run are skipped.src/lib/components/BatchFileList.svelterenders the rows: name, size, status chip (Wartet / Verarbeite… / Fertig ✓ / Fehler), per-row download, remove, and retry buttons with accessible names including the file name, a progress line (“Datei 3 von 7 wird verarbeitet…”), a total result-size summary, a memory warning (>20 files or any file >25 MB), and the bulk “Alle herunterladen” ZIP action (via fflate, named{toolname}_{date}.zip) shown once ≥2 results exist. Duplicate result names inside the ZIP are suffixed instead of overwriting.- Page wiring: keep
items = $state<BatchItem[]>([]), append newly dropped files (do not replace; deduplicate by name + size), pass aprocess(file)callback that reads the current option state so option changes mid-run affect only subsequent files, and gate the run button on pending items. Files too large to rasterize (>100 MB) are marked as error rows immediately on selection instead of blocking the batch. - Per-tool ZIP base names: compress uses
komprimiert, watermarkwasserzeichen, encryptgeschuetzt, decryptentsperrt. Choose an analogous German name per tool and cover the batch flow with Playwright tests (multi-file ZIP contents, failure continuation, retry, removal).
Svelte conventions
- Use Svelte 5 runes and current event syntax:
$props(),$state,$derived,$effect, snippets,{@render ...}, and handlers such asonclickrather than legacy APIs. - Keep component scripts typed with
<script lang="ts">. - Define a local
Propstype for non-trivial component props. - Use callback props for child-to-parent updates; do not mutate prop objects in place.
- Use
$lib/...aliases for application imports instead of long relative paths. - Name reusable components in PascalCase and route files according to SvelteKit conventions.
- Preserve semantic HTML, accessible names, visible focus states, and keyboard alternatives. Do not make drag-and-drop the only way to perform a critical action.
- Clean up global listeners, object URLs, PDF loading tasks, and other browser resources.
The Vite configuration forces runes mode for project files, so do not introduce legacy Svelte component patterns.
shadcn-svelte and component conventions
components.json configures the official shadcn-svelte registry, the nova style, Lucide icons, and
these aliases:
- UI components:
$lib/components/ui - General components:
$lib/components - Utilities:
$lib/utils
Use existing local UI components before creating replacements. Follow the exports actually present
on disk: the card is composed through import * as Card from '$lib/components/ui/card', while the
current button is a default import from $lib/components/ui/button.svelte.
When adding a registry component, use the project runner, inspect every generated file, and preserve local customizations:
pnpm dlx shadcn-svelte@latest add <component>
Do not overwrite or bulk-update customized components without explicit approval. Use cn() for
conditional class merging and existing variants before adding one-off component styling.
Styling conventions
Use Tailwind utilities in Svelte markup and semantic theme tokens from src/routes/layout.css.
- Prefer
bg-background,text-foreground,bg-primary,text-muted-foreground,border-border, and other configured semantic utilities over raw brand colors. - Preserve the established Nominandum visual language: pale blue-gray background, white cards, dark blue primary text, red/coral secondary actions, rounded containers, and Nunito typography.
- Use
flex/gridwithgap-*, responsive variants, andsize-*when width and height are equal. - Use
cn()rather than constructing partial Tailwind class names. Keep complete class names visible to Tailwind's scanner. - Prefer component variants and theme variables over overriding UI-component colors in call sites.
- Add global CSS only for imports, tokens, font faces, animations, or behavior that utilities cannot reasonably express.
- Tailwind 4 is configured through Vite and
layout.css; there is notailwind.configfile. - Maintain contrast, responsive layouts, drag-state feedback, and visible focus treatment.
Theme-level changes belong in src/routes/layout.css. Do not reintroduce the removed Skeleton theme
or refer to the old nom-theme.css architecture.
Formatting
Follow the checked-in Prettier configuration:
- Tabs for indentation
- Single quotes in scripts and styles where supported
- No trailing commas
- 100-character print width
- Svelte-aware and Tailwind-aware formatting
Use pnpm exec prettier --write <changed-files> for targeted formatting. Avoid formatting unrelated
files solely to clean up existing differences.
Product and content constraints
- User-facing copy is German unless a task explicitly requests another language.
- Preserve legal text in
datenschutz/+page.svelteandimpressum/+page.svelteunless explicitly asked to change it. - Keep the Nominandum logo, footer artwork, and theme consistent across routes.
- ZIP selection accepts multiple files matching
.zip,application/zip, orapplication/x-zip-compressed. - Resetting the file input after selection is intentional so the same archive can be selected again.
- Do not add uploads, network requests, server processing, analytics, or case-data persistence without an explicit requirement and privacy review.
Change discipline
- Keep changes scoped and preserve unrelated work, including untracked files.
- Inspect existing components, archive helpers, and theme tokens before adding abstractions.
- Keep browser processing behavior and archive-order invariants covered when editing shared helpers.
- Do not hand-edit generated output or lockfile contents. Update the lockfile only through pnpm when dependency changes are required.
- Avoid dependencies when the task can be completed with Svelte, browser APIs, Tailwind, or existing packages.
- After implementation, review changed files, run relevant checks, and mention remaining warnings or limitations.