382 lines
24 KiB
Markdown
382 lines
24 KiB
Markdown
# Project Agent Guide
|
|
|
|
## Project overview
|
|
|
|
This repository contains **juri-merger**, a German-language SvelteKit application branded as
|
|
**beA-Edit**. Users add one or more beA ZIP archives or PDF files in the browser, inspect and
|
|
reorder supported attachments, group attachments into named sub-documents (including page-level
|
|
PDF splitting), 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`
|
|
- PWA/offline support through `@vite-pwa/sveltekit` (Workbox precache, generated web manifest)
|
|
- 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 test:pwa # PWA offline/update tests against the production build (requires pnpm build)
|
|
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
|
|
│ │ ├── AttachmentMenu.svelte # Per-attachment dropdown menu (move, split, remove)
|
|
│ │ ├── BatchFileList.svelte # Shared batch rows: status chips, downloads, retry
|
|
│ │ ├── BeaArchiveProcessing.svelte
|
|
│ │ ├── BeaWorkspaceControls.svelte # beA workspace toolbar, settings popover, dialogs
|
|
│ │ ├── ProcessedZipArchiveEditor.svelte
|
|
│ │ ├── PdfSplitDialog.svelte # Page-level PDF splitting into Teildokumente
|
|
│ │ ├── 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
|
|
│ │ ├── PwaUpdatePrompt.svelte # Service-worker registration and update/offline toast
|
|
│ │ ├── 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-errors.ts # Shared encrypted-PDF detection and German messages
|
|
│ ├── pdf-metadata.ts # PDF metadata inspection and cleaning (pdf-lib only)
|
|
│ ├── pdf-overlay.ts # Shared overlay positions, preview CSS and color parsing
|
|
│ ├── pdf-pagination.ts # Sheet-number labels and the pagination pipeline
|
|
│ ├── pdf-processing.ts # Reusable PDF merge helper
|
|
│ ├── pdf-text-overlay.ts # Shared framed/rotated text overlay drawing
|
|
│ ├── pdf-thumbnails.ts # Managed pdfjs thumbnails, on-demand page previews, 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
|
|
├── +layout.ts # `prerender = true` for all routes (PWA offline documents)
|
|
├── +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
|
|
│ └── {pagination,merge,separate,stamp,watermark,encrypt,decrypt,compress,convert,rotate,metadata}/
|
|
├── datenschutz/+page.svelte # Privacy policy
|
|
└── impressum/+page.svelte # Imprint
|
|
static/ # Public logo, footer art, PWA icons, and robots.txt
|
|
components.json # shadcn-svelte registry configuration
|
|
Dockerfile # Node adapter production image
|
|
playwright.pwa.config.ts # Playwright config for `pnpm test:pwa`
|
|
plan/ # Design and implementation notes
|
|
```
|
|
|
|
Generated directories such as `.svelte-kit/`, `build/`, and `node_modules/` must not be edited or
|
|
committed.
|
|
|
|
## PWA / offline mode
|
|
|
|
- `SvelteKitPWA` is configured in `vite.config.ts` with `registerType: 'prompt'`: a waiting service
|
|
worker never activates by itself. `PwaUpdatePrompt.svelte` registers the worker in the browser and
|
|
shows a toast (“Neue Version verfügbar” with “Neu laden”/“Später”, plus a one-time hint when the
|
|
app is ready offline). Never force a reload — an in-progress archive must survive a declined
|
|
update.
|
|
- All routes are prerendered (`src/routes/+layout.ts`), so route documents land in the precache and
|
|
offline navigation works for every page, including a direct deep link. `navigateFallback: '/'`
|
|
covers unknown paths.
|
|
- Precache contents come from `workbox.globPatterns`: the plugin's client/prerendered defaults plus
|
|
`mjs` (the pdfjs worker chunk, required for offline rendering) and the local fonts; the unused
|
|
`static/Nunito/**` font drop is excluded via `globIgnores`. Precache size is ~4.5 MB.
|
|
- PWA icons in `static/icons/` (192/512, maskable, apple-touch) were generated once from
|
|
`static/favicon.svg`; regenerate them with ImageMagick/rsvg if the logo changes. The manifest link
|
|
is injected client-side from `virtual:pwa-info` in the root layout, `theme-color` and the
|
|
apple-touch-icon live in `app.html`.
|
|
- `devOptions.enabled` stays `false`: `pnpm dev` never registers a service worker, so development
|
|
caches stay clean. Verify PWA behaviour only against a production build (`pnpm build` and
|
|
`pnpm preview`, or `pnpm test:pwa`, which also covers the offline processing chain and the update
|
|
toast).
|
|
- Simulating a deployment in tests requires restarting `node build/index.js`: adapter-node's static
|
|
handler (sirv, non-dev mode) walks the build directory once at startup and caches the ETags, so
|
|
swapped files are invisible to a running process. Browsers revalidate `/sw.js` via its ETag on
|
|
navigation and `registration.update()`, so the normal container restart on deploy is sufficient.
|
|
|
|
## Application flow and state
|
|
|
|
- `/` is the toolbox hub. A beA ZIP or PDF dropped anywhere on that route is queued in memory and
|
|
forwarded to `/tools/bea`; the file is not uploaded or persisted. The same `queuePendingFiles` /
|
|
`takePendingFiles` handoff carries the merged PDF from `/tools/merge` to `/tools/pagination`.
|
|
- `/tools/pagination` is a single-PDF tool: it renders one page at a time through
|
|
`loadPdfPageRenderer` (never the all-pages thumbnail array), composes every label with
|
|
`buildPaginationLabel` — the single source for both the live preview and the export — and stamps
|
|
all pages through `drawTextOverlay`. The page number is always on every page; the optional free
|
|
text (Aktenzeichen) follows its own “every page” / “first page only” scope.
|
|
- `/tools/metadata` is a single-PDF inspection tool. `src/lib/pdf-metadata.ts` reads document
|
|
properties with pdf-lib only (no rasterization), returns a discriminated `{ status: 'encrypted' }`
|
|
result for password-protected files, and detects embedded-file markers by scanning raw bytes.
|
|
Cleaning neutralizes the standard Info dictionary fields and re-saves through pdf-lib; embedded
|
|
files and XMP streams are out of scope and are disclosed in the UI instead.
|
|
- `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 Datei bearbeiten”** (replace) opens a file 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 Datei 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 Datei bearbeiten”, append is “Weitere Datei 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 file types for the workspace (beA ZIP **and** PDF) are centralized in
|
|
`src/lib/zip-selection.ts` and shared by the dropzone, the start-page drop target, and both
|
|
compact pickers; `ZipFilePicker.svelte` resets its input after every selection so the
|
|
same file can be chosen again.
|
|
- Workspace files are processed concurrently. PDF files become single-attachment archives via
|
|
`pdfFileToArchive`; ZIP files go through `extractZipArchives`. `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.
|
|
- **Gruppieren** (grouping mode) offers archive-wide multi-select over loose and sub-document
|
|
attachments: create a new Teildokument from the selection, move the selection into an existing
|
|
Teildokument, select all/none. Exiting with **Fertig** or Escape clears the pending selection.
|
|
New Teildokumente are numbered via `nextSubDocumentNumber` (max existing trailing number, not
|
|
list length), and the created block's name input is focused and scrolled into view.
|
|
- Each attachment card has an **“⋯” menu** (`AttachmentMenu.svelte`) as the keyboard/touch path:
|
|
move into an existing or new Teildokument, move back to the loose list, split PDFs, and remove
|
|
(with confirmation). It must never become the only way to perform an action that drag-and-drop
|
|
also covers.
|
|
- **PDF splitting**: `PdfSplitDialog.svelte` opens per PDF attachment, renders pages through
|
|
`loadPdfDocument` + `PdfPageGrid`, and assigns page sets to named groups (manual selection,
|
|
“Jede Seite einzeln”, or “Alle N Seiten”). Cut is the default; “Seiten im Original behalten”
|
|
switches to copy. `applyPdfSplit` replaces the source with the remaining pages (or removes it),
|
|
inserts the new Teildokumente after the source's parent block, and the synthetic attachments
|
|
get unique `#seiten-…` path suffixes because `path` is the identity for keyed lists, drag
|
|
payloads, and 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. Splitting cuts pages by default and only
|
|
duplicates them when the user explicitly chooses copy mode.
|
|
- 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.
|
|
- Splitting inserts the new Teildokumente directly after the source's parent block (loose source:
|
|
appended at the end) so the reading order is preserved.
|
|
- 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.
|
|
- Page-set copies (separate/extract tools, PDF splitting) share
|
|
`copyPagesToNewPdf(sourceBytes, pageIndexes)` in `src/lib/pdf-processing.ts` so the copy
|
|
semantics cannot drift apart.
|
|
- `src/lib/pdf-text-overlay.ts` owns text fitting, the rotated-box placement and the optional
|
|
`/Rotate` compensation. Stamp keeps `compensatePageRotation` off so its output does not change;
|
|
pagination turns it on so numbers stay at the visual position on rotated scans. Share it instead
|
|
of re-implementing overlay maths in a route.
|
|
- `src/lib/pdf-thumbnails.ts` offers two loaders: `loadPdfDocument` (all thumbnails, for pickers and
|
|
page grids) and `loadPdfPageRenderer` (one page at a time, for long documents). Both revoke object
|
|
URLs and tear down the pdfjs loading task in `destroy()`.
|
|
- `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.
|
|
|
|
## Batch processing pattern for single-PDF tools
|
|
|
|
Tools that transform one PDF per run (compress, watermark, stamp, encrypt, decrypt) accept multiple
|
|
files. Follow the established compress retrofit when adding the pattern to another tool; see
|
|
`plan/done/batch-processing-plan.md` for the full design.
|
|
|
|
- `src/lib/batch.ts` owns the engine: `createBatchItem(file)`, `runBatch(items, process, update,
|
|
options?)` and `formatBytes`. `runBatch` processes 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 (a string, or a formatter that can inspect the thrown error) — and consults the
|
|
optional `shouldProcess` predicate so rows removed mid-run are skipped.
|
|
- `src/lib/components/BatchFileList.svelte` renders 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 a `process(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. Keep thumbnails, previews, and page-specific selection to a single dropped
|
|
file; multi-file runs apply the tool to every page of every file.
|
|
- Per-tool ZIP base names: compress uses `komprimiert`, watermark `wasserzeichen`, stamp `stempel`,
|
|
encrypt `geschuetzt`, decrypt `entsperrt`. 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 as `onclick` rather than legacy APIs.
|
|
- Keep component scripts typed with `<script lang="ts">`.
|
|
- Define a local `Props` type 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:
|
|
|
|
```sh
|
|
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`/`grid` with `gap-*`, responsive variants, and `size-*` 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 no `tailwind.config` file.
|
|
- 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.svelte` and `impressum/+page.svelte` unless 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`, or
|
|
`application/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.
|