feat(stammbaum): land a fresh visit on the tree's top-left corner (#692)
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m35s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m33s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s

At z=3 a pan of {0,0} centres on the tree midpoint; a fresh visit (no shared
?z) now anchors the viewBox to the tree's top-left corner via topLeftView
(the negative clamp limit), emitted on mount. Shared links still win.
Verified live: lands at cx<0, cy<0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-29 19:25:03 +02:00
parent 01b902e885
commit b1309db8db
4 changed files with 34 additions and 1 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte';
import { untrack, onMount } from 'svelte';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { components } from '$lib/generated/api';
import {
@@ -13,6 +13,7 @@ import {
type PanZoomState,
clampZoom,
recentreOn,
topLeftView,
ZOOM_STEP_KB
} from '$lib/person/genealogy/panZoom';
import { panZoomGestures } from '$lib/person/genealogy/panZoomGestures';
@@ -32,6 +33,8 @@ interface Props {
centreOnId?: string | null;
/** Fired on the first pointer interaction with the canvas (affordance dismiss). */
onActivity?: () => void;
/** When true, the initial view is anchored to the tree's top-left corner. */
anchorTopLeft?: boolean;
onSelect: (id: string) => void;
/**
* Force-show or force-hide the generation gutter. When undefined, falls
@@ -50,6 +53,7 @@ let {
onPanZoom = () => {},
centreOnId = null,
onActivity,
anchorTopLeft = false,
onSelect,
showGutter
}: Props = $props();
@@ -139,6 +143,12 @@ const railRows = $derived(
.map((r) => ({ rank: r.rank, label: r.label, centerY: r.y + NODE_H / 2 }))
);
// A fresh visit (no shared URL state) lands on the tree's top-left corner rather
// than its centre (#692). Runs once after layout is available.
onMount(() => {
if (anchorTopLeft) onPanZoom(topLeftView(baseDims.w, baseDims.h, panZoom.z));
});
const viewBox = $derived.by(() => {
const w = baseDims.w / panZoom.z;
const h = baseDims.h / panZoom.z;