Files
familienarchiv/frontend/CLAUDE.md
Marcel 242e10179d feat(stammbaum): /stammbaum page — SVG tree + side panel + empty state
- /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>
2026-04-28 19:32:17 +02:00

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.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

// +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:
    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:

<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.