ADR-0005: The family tree view
Metadata
- Status: Accepted
- Date: 2026-07-23 (decided); implemented 2026-07-25
- Deciders: EagraΓ Clainne Team (decisions taken in a design interview, 2026-07-23)
- Context: Implemented 2026-07-25 β see "Implementation notes" for the concrete choices and deviations
- Related: ADR-0004 (derived family graph β this view is its first consumer); the frontend plan (member marks as the identity thread; quality floor: keyboard access, 44px targets, reduced motion)
Context
The Family page shows the household as a grid of tiles and a Remembering list. What it cannot show is shape: who belongs to whom, across generations, in one picture. With ADR-0004 the backend will hold a real family graph β parent edges without a cap, partners, exes, and a derived extended family β and a graph that rich deserves a view that draws it.
The awkward truth that drives most decisions here: a blended family is not a tree. Multiple parents, remarriage, and partner changes make it a DAG, and the view must draw that honestly on a phone, within the house rules β no third-party libraries, keyboard access everywhere, member marks as the identity thread.
Decision
A view toggle on the Family page
The tree is a way of seeing Family, not a new place: a Grid | Tree choice-row on the existing Family tab (the same pattern as Jobs' Mine/Everyone). It inherits the page's data load, its member sheet, and its permissions; the tab bar stays at five destinations.
Generational rows
Layout follows genealogy convention: horizontal bands per generation, oldest at the top. Partners sit adjacent joined by a partner bar; children hang below with a line to each of their parents β a third or fourth parent is just another line, so the uncapped-parents rule renders without special cases. Layout rules the implementation must honour:
- Generation rank comes from parent-edge depth; partners are pulled onto the same row even when their own edges would rank them elsewhere.
- Disconnected clans stack vertically in the same scene, separated by whitespace, not hidden behind tabs.
- Crossing lines are tolerated (rare at household scale) rather than prevented by reordering people meaningfully.
A pure SVG scene with fully interactive nodes
The whole view is one SVG: nodes, partner bars, parent lines, all in one
coordinate space driven by the viewBox. Interactivity is built in rather
than layered on:
- Every member is a focusable SVG group (
tabindex,role="button", anaria-labelnaming them) with a β₯ 44px hit circle; the node visual is their mark and name β the identity thread, now literally the family picture. - Tab walks the family in generation order; Enter or tap opens the member sheet; the focus ring is drawn in SVG so keyboard users see where they are.
Pan and zoom, with parity
The scene opens at Fit (the whole family visible) and then behaves like a map β with browser conventions respected so it never steals the page:
- Touch: pinch zooms, one-finger drag pans (
touch-action: nonescoped to the scene only), double-tap zooms to the tapped person. - Desktop:
Ctrl+wheel zooms (plain wheel continues to scroll the page), drag pans. - Keyboard:
[+][β][Fit]buttons give the same power as pinch, and Tab-focusing an off-screen member auto-pans them into view.
The present-tense picture
- Ex-partner edges are drawn as quiet dashed arcs. (Amended 2026-07-25 β the original decision was not to draw them, and "dashed ex-partner edges" sat in the rejected alternatives. Living with the tree reversed that: an ex co-parent floating unconnected beside the family read as missing data rather than discretion.) The dashed arc is visually subordinate to the solid partner bar, and ex edges remain inert for layout β they never pull rank or adjacency, so the present-tense structure is unchanged.
- Also amended 2026-07-25: members with no parents of their own sink to the row just above their topmost child rather than floating to the top of the scene β an ex co-parent otherwise reads as a grandparent.
- Also amended 2026-07-25: relationship lines are colour-coded with the member-mark Okabe-Ito palette (parent/child sky, partners orange, ex-partners pink), and a legend sits under the scene showing only the line kinds actually drawn. Sibling arcs were tried in the same change and removed on sight: even colour-coded they added ink, not information β siblinghood stays implied by the shared parent lines.
- Remembered members are in the tree, dimmed and carrying their years β the tree is exactly where Grandad belongs.
- Members with no stored edges gather in a quiet "Not linked yet" strip beneath the scene; for admins, tapping one opens their sheet at Link a relation β the empty state is an invitation to build the tree.
Sequencing
The tree ships after ADR-0004 and consumes the backend's stored
PARENT/PARTNER edges plus derived structure β no client-side graph walking.
It thereby becomes the derived graph's first real consumer, proving the
derived_relations shape before the CLI does.
Consequences
Positive
- The family's shape becomes visible β the noticeboard finally shows who belongs to whom, and uncapped parents render as naturally as two.
- One SVG coordinate space keeps nodes and edges in lockstep through pan, zoom, and resize; no HTML/SVG position syncing.
- The keyboard story is complete: tab order, Enter-to-open, zoom buttons, and focus-follows-pan hold the quality floor inside a graphic.
- Marks-as-nodes makes the tree readable by the same six-year-old the marks were designed for.
- Building on ADR-0004 means one derivation implementation, exercised by a real consumer immediately.
Negative / trade-offs
- Pure SVG re-implements what HTML gives free: focus rings, hit areas, text truncation for long names, and touch targets are all hand-drawn and hand-tested.
- Pan/zoom machinery (gesture handling, viewBox math, parity buttons) is real code for what is usually a seven-person picture.
- A generational-row layout of a DAG will sometimes cross lines; we accept the crossings rather than grow a layout optimiser.
- The view is blocked behind ADR-0004's backend work β nothing shows until the graph lands.
touch-action: noneon the scene means the tree area doesn't scroll the page on touch; the surrounding page must stay reachable around it.
Implementation notes (2026-07-25)
Shipped as designed β web/src/tree/layout.ts (pure generational-row
layout), web/src/tree/FamilyTree.tsx (the SVG scene, gestures, and the
"Not linked yet" strip), and a Grid | Tree choice-row on the Family page β
with the following concrete choices and deviations recorded honestly:
- Tap vs double-tap needed a beat. The ADR gives touch both tap-opens-the-sheet and double-tap-zooms-to-the-person, which conflict at the first tap. On touch, opening the sheet waits ~340 ms for a possible second tap; a double-tap cancels the open and zooms instead. Mouse and keyboard open instantly.
- The 44px hit circle is held against zoom. A fixed SVG radius cannot promise on-screen pixels, so the invisible hit circle grows in scene units (via a ResizeObserver on the canvas) whenever the fitted family would shrink it below 44 px under a finger. Hit circles may overlap when zoomed far out; the topmost (later-generation) node wins, which is tolerable.
- "Not linked yet" means no stored edge on either side. Ex-partner edges count as linked (they are stored edges, per ADR-0004's deletion rule), so a member whose only tie is an ended partnership appears in the scene as an unconnected node rather than in the strip.
- Motion. Button zooms, Fit, double-tap zoom, and focus-follows-pan use
a ~200 ms viewBox tween; direct gestures (drag, pinch, Ctrl+wheel) are
never animated. Both
prefers-reduced-motionand the app's own reduce-motion setting make every tween instant. - Fit has a floor. A two-person family opens at a sensible scale rather than poster-sized; zoom is clamped between "whole family Γ 2 out" and a close-up of about three columns.
- Layout determinism. Ranks come from constraint relaxation (child > each parent, partners equalised, iteration-capped as defence against degenerate data); clans sort biggest-first then alphabetically; rows order children under the mean position of their parents with partner groups kept adjacent; remaining crossings are tolerated by decision.
- One ADR-0004 client cleanup was pulled in. The sheet's link form now writes one edge per link (no more PARENT+CHILD double-write, relying on the backend's CHILD auto-inversion) and offers Partner alongside Parent and Child β without it the tree's partner bars would be undrawable from the UI. Relation labels in the sheet now also cover the stored and derived kinds.
- No web test runner exists in the repo, so the layout module ships as pure functions (data in, coordinates out) per the plan's fallback; it was exercised against blended-family, multi-clan, cycle, ex-only, and dangling-edge fixtures during development.
- The view toggle defaults to Grid on every visit and is not persisted β the tree is a way of seeing Family, not a mode to be trapped in.
Alternatives considered
- A sixth tab β most discoverable, but crowds the mobile bar for what is a view of Family, not a destination.
- Tree inside the member sheet (person-centric only) β cheap and contextual, but there is never a whole-family picture.
- Person-centric hourglass layout β handles any mess by re-centring, but "whose tree am I looking at" becomes permanent UI and the single family picture never exists.
- Force-directed graph β no layout decisions, but generations stop reading and the picture differs every visit.
- HTML nodes + SVG edge underlay β accessibility by construction and the original recommendation, but two coordinate systems must be kept in sync through pan/zoom; rejected in favour of one SVG scene with the a11y built in deliberately.
- CSS-only connectors β pseudo-element origami that breaks on the first uneven generation.
- Presentation-only SVG (grid stays the accessible path) β defensible on a shared page, but tap-a-face-to-open is the tree's most natural gesture.
- Fit-width with sideways scroll, no zoom β the recommended lean option; rejected by choice for the map feel, with the parity suite adopted to keep the floor.
- Gestures only / modal zoom mode β either bends the keyboard floor or adds a mode to learn; rejected for buttons + browser conventions.
- Dashed ex-partner edges (adopted by amendment, 2026-07-25 β see The present-tense picture) β honest history, but the graph made exes inert and a child's daily board needn't re-draw a divorce.
- Living-only tree β contradicts Remembering; Grandad's line matters most on the tree.
- Client-side interim walking today's reciprocal edges β sooner on screen, but builds the walk twice against data with no partners in it.