Strengthen the zoom-clamp test to assert z floors at 0.25 in the URL (was a
'does not throw' smoke test) and move the affordance localStorage reset to a
beforeEach so the e2e tests are order-independent (QA review).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a deterministic stubbed-rAF test for animateView's animated path (was only
covering the reduced-motion branch) and assert the server load redirects on 401
and throws on a network 500 (QA review).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fresh visit (no URL state) now opens at INITIAL_VIEW (z=3) so node tiles and
generation labels are legible on arrival; the fit-to-screen control still zooms
out to the whole tree (DEFAULT_VIEW, z=1). Shared links with ?z still win.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
replaceState throws 'before the router is initialized' during hydration, which
killed the sync $effect on its first tick so the URL never updated on pan/zoom.
Gate the write behind a flag flipped after the first post-mount tick() (router
started) plus a defensive try/catch. Verified live: zoom now updates ?z=.
The prior component test mocked replaceState and masked this.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add StammbaumAffordance: a touch-only "drag to explore · pinch to zoom" hint
that auto-dismisses on the first canvas pointer interaction (wired via the
gesture action's onGestureStart) or the explicit close, and stays dismissed for
30 days via a localStorage timestamp (boolean gate only, never rendered).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an onCentre control to StammbaumSidePanel (title row, both desktop aside
and mobile sheet). The page drives a one-shot centreOnId so StammbaumTree
recentres the canvas on the focal node (US-PAN-005). Also tighten the panel
spec's deathYear fixture to a valid type.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap the mobile person panel in StammbaumBottomSheet: drag-handle grip with
swipe-down-to-dismiss (≥80px), full-screen backdrop button for tap-outside
dismiss, role=dialog + aria-label, focus trap, and Escape (NFR-A11Y-004).
Pan/zoom state is untouched by open/close (US-PANEL-001/002).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A view-keyed effect mirrors pan/zoom into the URL via replaceState (URL read
untracked to avoid a feedback loop). State survives panel open/close
(US-PANEL-002 AC1) and a shared link reproduces the view (AC2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The server load parses and sanitises the shareable pan/zoom params (degrading
Infinity/NaN, clamping zoom) into initialView, which seeds the page view. A
crafted link can no longer blank the SVG (Nora). US-PANEL-002 AC2 groundwork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fit-to-screen tweens to the default view over 300ms via animateView (eased,
lerpView-driven) and snaps instantly when prefers-reduced-motion is set
(US-PAN-004 AC2, NFR-A11Y-003).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move zoom controls out of the page header into a docked bottom-right cluster
inside the canvas (one-handed phone reach, Leonie) and add a fit-to-screen
button (data-testid=fit-to-screen). Add the 5 new i18n keys to de/en/es.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a panZoomGestures action: one-finger/left-button drag pans, two-finger
pinch and Ctrl+wheel zoom around the centroid, plain wheel pans. Pan is
edge-clamped via clampPan (no infinite scroll), a real drag suppresses the
trailing node click, and inertia decays after release unless prefers-reduced-
motion. Canvas container switches from native scroll to overflow-hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
+/- zoom by the fixed step and arrow keys pan by a tenth of the visible
extent, emitted via onPanZoom. Provides the keyboard-only alternative path
required by NFR-A11Y-002. Nodes keep their own Enter/Space selection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the scalar zoom prop with a {x,y,z} PanZoomState. The viewBox centre
is offset by the pan and width/height scaled by zoom; the default {0,0,1}
frames the whole tree (fit-to-screen). Page header buttons now step view.z
through clampZoom over the resolved 0.25–3.0 range.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
empty state vs. populated, zoom controls visibility tied to node count,
URL ?focus= preselection (matching id selects, missing id does not),
zoom-out clamping safety. $app/state mocked at module boundary so the
test can drive page.url and page.data.canWrite without a SvelteKit
runtime.
Six tests focused on user-observable behaviour — one logical behaviour
per test (Sara's guidance).
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The focus deep-link is a one-time load param — $derived + $effect caused
a deferred write that left the node unselected on first paint. Initialising
$state inline reads the URL once at component mount with no reactive cycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @leonie blocker: zoom buttons in /stammbaum had no visible focus
indicator for keyboard users. Applied focus-visible:ring-2 focus-visible:ring-focus-ring
focus-visible:outline-none matching the pattern used on nav links.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raise chip labels from 10px to 12px (text-xs) in StammbaumCard,
StammbaumSidePanel and StammbaumTree SVG text. Widen zoom buttons
from 32px to 44px for senior-audience touch targets.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 268px width came from the spec mock; real names plus the
relationship pill ("Eugenie de Gruyter" + "Elternteil") need more
breathing room.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the inline-edit affordance from
docs/specs/stammbaum-tree-spec.html (section 3): a low-opacity
"+ Beziehung hinzufügen" button below the direct relationships list
expands into a compact form (type select, person typeahead,
optional Von/Bis Jahr inputs, Abbrechen + Speichern). On save the
form POSTs to /api/persons/{id}/relationships, reloads the panel's
own data, and calls invalidateAll() so the tree picks up the new
edge without a hard refresh.
The panel takes a new canWrite prop, plumbed through from the
+layout.server.ts data already exposed on page.data.
Also pins the /stammbaum canvas to the viewport (-my-6 cancels
<main>'s py-6, h-[calc(100dvh-4.25rem)] subtracts the navbar) so
the page no longer overflows below the fold.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- /stammbaum: drop the global py-6 top gap so the page header butts
up against the navbar, matching its full-bleed canvas layout
- person detail: add mt-6 around the document lists so they don't
sit flush against the Beziehungen card
- person edit: add mt-6 to PersonMergePanel so the merge box doesn't
collide with the StammbaumCard above it
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- /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>