# 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="xMidYMid meet"`) — 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.