Files
familienarchiv/docs/adr/027-stammbaum-custom-viewbox-pan-zoom.md
Marcel 95d35c20b2
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m35s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m38s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
fix(stammbaum): address re-review nits — opaque rail, stale docs, rail clarity (#692)
- Rail chip background opaque (was /85) so G{n} labels stay AA-legible over
  tree content (Leonie).
- Rail effect: replace the reactKey hack with an inputsFinite guard that both
  tracks deps and guards NaN; name the fallback-stack magics; correct the stale
  'xMidYMid' comment (the CTM mapping is preserveAspectRatio-agnostic) (Felix/Markus).
- GLOSSARY zoom range 0.25–3.0 → 0.25–10; ADR-027 preserveAspectRatio note
  xMidYMid → xMinYMin (Elicit traceability).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:21:13 +02:00

2.9 KiB

ADR-027 — Stammbaum pan/zoom is a custom viewBox layer, not a third-party library

Date: 2026-05-29 Status: Accepted Issue: #692 (mobile read path — pan, zoom, fit-to-view); supersedes OQ-007 Milestone: Stammbaum mobile read path


Context

#692 makes /stammbaum usable on phones: drag-to-pan, pinch/keyboard/wheel zoom, fit-to-screen, recentre-on-person, a shareable URL view state, and an edge-fade affordance. During issue grooming, OQ-007 was resolved to adopt the panzoom library (timmywil v4.x) on the team's recommendation, pinned per NFR-MAINT-001.

That recommendation predated a load-bearing implementation detail: StammbaumTree.svelte already renders zoom by deriving the SVG viewBox (w = baseW / z, centred on the layout bounding box, preserveAspectRatio="xMinYMin meet" so a fresh visit anchors to the tree's top-left corner) — not by applying a CSS transform. The panzoom library operates by writing transform to a DOM node. Adopting it would mean:

  • abandoning the proven viewBox derivation and the in-SVG generation gutter (#689), which lives in SVG user-space coordinates and would have to be reconciled with a CSS-transformed parent;
  • re-deriving fit-to-screen, recentre, and the ?cx&cy&z URL state against the library's transform coordinate system;
  • a client-only lazy import to keep the SSR-rendered tree from touching window at module load; and
  • ~8 KB of bundle for behaviour we can express in a few pure functions.

Decision

Build pan/zoom as a thin custom layer over the existing viewBox, with no third-party dependency. This reverses OQ-007.

  • All geometry is pure and unit-tested in frontend/src/lib/person/genealogy/panZoom.ts: clampZoom, parsePanZoomParams/serializePanZoomParams, screenDeltaToSvg, zoomAtPoint (centroid-anchored), clampPan (edge-clamp), recentreOn, lerpView.
  • Pan offsets shift the viewBox centre; zoom scales its width/height. The default {x:0, y:0, z:1} already frames the whole tree, so fit-to-screen is a reset to the default — no bounding-box recomputation.
  • DOM event wiring lives in the panZoomGestures action (pointer/wheel/pinch + inertia, reduced-motion aware) and a keyboard handler on the SVG; both delegate to the pure module.

Consequences

  • NFR-MAINT-001 (library pinning + feature-flag fallback) is moot — no library is adopted. The "swap-out point" is panZoom.ts + panZoomGestures.ts.
  • Text stays vector-crisp at any zoom (SVG-native scaling), satisfying US-PAN-002 AC5.
  • The #689 gutter and the #361 seeded-rank invariant are untouched by the pan/zoom layer.
  • Geometry is testable in the fast node project; only the DOM glue needs the browser project.
  • Trade-off: we own the inertia/pinch code (~a few hundred lines across the action) rather than delegating it. This is acceptable given the testability and zero-dependency wins.

The issue body's OQ-007 row is updated to point at this ADR.