- /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>
198 lines
7.5 KiB
Markdown
198 lines
7.5 KiB
Markdown
# 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`:
|
|
|
|
```typescript
|
|
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.ok` for error checking (not `if (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
|
|
|
|
```typescript
|
|
// +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.yyyy` with auto-dot insertion via `handleDateInput()`. A hidden `<input type="hidden" name="documentDate" value={dateIso}>` sends ISO to the backend.
|
|
- **Display**: Always use `Intl.DateTimeFormat` with `T12:00:00` suffix to prevent UTC off-by-one:
|
|
```typescript
|
|
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, names
|
|
- `font-sans` (Montserrat) — labels, metadata, UI chrome
|
|
|
|
Card pattern for content sections:
|
|
|
|
```svelte
|
|
<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
|
|
|
|
```bash
|
|
cd frontend
|
|
npm install
|
|
npm run dev # Dev server on port 5173 (or 3000 if --port 3000)
|
|
```
|
|
|
|
### Build & Preview
|
|
|
|
```bash
|
|
npm run build # Production build
|
|
npm run preview # Preview production build
|
|
```
|
|
|
|
### Code Quality
|
|
|
|
```bash
|
|
npm run lint # Prettier + ESLint check
|
|
npm run format # Auto-fix formatting
|
|
npm run check # svelte-check (type checking)
|
|
```
|
|
|
|
### Testing
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide
|
|
```
|
|
|
|
Or let the Vite plugin handle it automatically during dev/build.
|