- /stammbaum/+page.server.ts loads GET /api/network (already filtered
to family members on the backend) and returns nodes + edges.
- +page.svelte holds the page shell, manages selectedId (with
?focus={id} deep-link support) and zoom state, renders the empty
state when nodes.length === 0 (icon + heading + body + link to
/persons), or the tree + side panel otherwise.
- StammbaumTree.svelte: BFS-based generation assignment from roots,
spouses promoted to the deeper generation so couples sit on the same
row, alphabetical sort within row, simple grid layout. SVG nodes are
role="button" + aria-label="{name}, {birth}–{death}" +
aria-expanded={selected}, with click + Enter/Space activation. Solid
parent→child connectors; mint spouse line with midpoint circle, dashed
if SPOUSE_OF.toYear is set (former spouse). Zoom maps to viewBox.
- StammbaumSidePanel.svelte: lazily loads
/api/persons/{id}/relationships and /inferred-relationships when the
selection changes; shows direct chips (mint), top-5 derived chips
(grey), and a "Zur Personenseite →" link. Escape closes the panel.
Refs #358.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.5 KiB
Frontend — Familienarchiv
Overview
SvelteKit 2 application providing the Familienarchiv web UI. Server-side rendered (SSR) where beneficial, with client-side interactivity for document viewing, transcription, annotation, and admin workflows.
Tech Stack
- Framework: SvelteKit 2 with Svelte 5 (runes mode)
- Language: TypeScript 5.9
- Styling: Tailwind CSS 4.1 + custom brand utilities
- Build Tool: Vite 7
- Adapter:
@sveltejs/adapter-node(Node.js server, not static) - i18n: Paraglide.js 2.5 (
@inlang/paraglide-js) — German (default), English, Spanish - API Client:
openapi-fetch+openapi-typescript(generated from backend OpenAPI spec) - PDF Rendering:
pdfjs-dist(PDF.js) - Testing:
- Unit/Server: Vitest 4 (Node environment)
- Component: Vitest Browser Mode with Playwright (Chromium)
- E2E: Playwright (
frontend/e2e/)
Project Structure
src/
├── routes/ # SvelteKit file-based routing
│ ├── +layout.svelte # Global layout: header, nav, auth state
│ ├── +layout.server.ts # Loads current user, injects auth cookie
│ ├── +page.svelte # Home / document search dashboard
│ ├── documents/ # Document CRUD, detail, edit, upload
│ ├── persons/ # Person directory, detail, edit, merge
│ ├── briefwechsel/ # Bilateral conversation timeline
│ ├── chronik/ # Unified activity feed
│ ├── admin/ # User, group, tag, OCR, system management
│ ├── api/ # Internal API proxies (server-side only)
│ ├── login/ logout/ # Auth pages
│ └── ...
├── lib/
│ ├── components/ # Reusable Svelte components
│ │ ├── document/ # Document-specific components
│ │ ├── chronik/ # Activity feed components
│ │ └── user/ # User-related components
│ ├── generated/ # Auto-generated API types (openapi-typescript)
│ ├── server/ # Server-only utilities (db, auth helpers)
│ ├── services/ # Client-side service logic
│ ├── stores/ # Svelte stores (global state)
│ ├── types.ts # Shared TypeScript types
│ ├── errors.ts # Error code mapping (mirrors backend ErrorCode)
│ ├── api.server.ts # Typed API client factory
│ ├── utils.ts # Shared utilities
│ ├── relativeTime.ts # Time formatting
│ ├── search.ts # Search utilities
│ └── paraglide/ # Generated i18n code
├── hooks/ # SvelteKit hooks (handle, handleFetch)
└── actions/ # Custom Svelte actions (click outside, etc.)
API Client Pattern
All server-side API calls use the typed client from $lib/api.server.ts:
const api = createApiClient(fetch);
const result = await api.GET('/api/persons/{id}', { params: { path: { id } } });
// Always check via response.ok, NOT result.error
if (!result.response.ok) {
const code = (result.error as unknown as { code?: string })?.code;
throw error(result.response.status, getErrorMessage(code));
}
return { person: result.data! };
Key rules:
- Use
!result.response.okfor error checking (notif (result.error)— breaks when spec has no error responses defined) - Cast errors as
result.error as unknown as { code?: string }to extract backend error code - Use
result.data!after an ok check
For multipart/form-data (file uploads), bypass the typed client and use raw fetch.
Form Actions Pattern
// +page.server.ts
export const actions = {
default: async ({ request, fetch }) => {
const formData = await request.formData();
const name = formData.get('name') as string;
// ...
return fail(400, { error: 'message' }); // on error
throw redirect(303, '/target'); // on success
}
};
Date Handling
- Forms: German format
dd.mm.yyyywith auto-dot insertion viahandleDateInput(). A hidden<input type="hidden" name="documentDate" value={dateIso}>sends ISO to the backend. - Display: Always use
Intl.DateTimeFormatwithT12:00:00suffix to prevent UTC off-by-one:new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }).format( new Date(doc.documentDate + 'T12:00:00') );
Styling Conventions (Tailwind CSS 4)
Brand color utilities (defined in layout.css):
| Class | Value | Usage |
|---|---|---|
brand-navy |
#002850 |
Primary text, buttons, headers |
brand-mint |
#A6DAD8 |
Accents, hover underlines, icons |
brand-sand |
#E4E2D7 |
Page background, card borders |
Typography:
font-serif(Merriweather) — body text, document titles, namesfont-sans(Montserrat) — labels, metadata, UI chrome
Card pattern for content sections:
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Section</h2>
<!-- content -->
</div>
Key UI Components
| Component | Props | Description |
|---|---|---|
PersonTypeahead |
name, label, value, initialName, on:change |
Single-person selector with typeahead |
PersonMultiSelect |
selectedPersons (bind) |
Chip-based multi-person selector |
TagInput |
tags (bind), allowCreation?, on:change |
Tag chip input with typeahead |
PdfViewer |
url, annotations, on:annotation |
PDF rendering with annotation overlay |
TranscriptionBlock |
block, mode |
Read/edit transcription block |
DocumentTopBar |
document |
Responsive document metadata header |
How to Run
Development
cd frontend
npm install
npm run dev # Dev server on port 5173 (or 3000 if --port 3000)
Build & Preview
npm run build # Production build
npm run preview # Preview production build
Code Quality
npm run lint # Prettier + ESLint check
npm run format # Auto-fix formatting
npm run check # svelte-check (type checking)
Testing
npm run test # Vitest unit + server tests (headless)
npm run test:coverage # Coverage report (server project only)
npm run test:e2e # Playwright E2E tests
npm run test:e2e:headed # Playwright E2E with visible browser
npm run test:e2e:ui # Playwright UI mode
Regenerate API Types
Requires backend running with --spring.profiles.active=dev:
npm run generate:api
Vite Proxy
During development, /api calls are proxied to the Spring Boot backend. The proxy injects the Authorization header from the auth_token cookie automatically (see vite.config.ts).
i18n (Paraglide)
Translations live in messages/{de,en,es}.json. The compiler generates type-safe helpers in src/lib/paraglide/. Run compilation manually with:
npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide
Or let the Vite plugin handle it automatically during dev/build.