diff --git a/frontend/src/lib/person/genealogy/StammbaumTree.svelte b/frontend/src/lib/person/genealogy/StammbaumTree.svelte index 3fa83409..db74a366 100644 --- a/frontend/src/lib/person/genealogy/StammbaumTree.svelte +++ b/frontend/src/lib/person/genealogy/StammbaumTree.svelte @@ -8,7 +8,7 @@ import { ROW_GAP, type Layout } from '$lib/person/genealogy/layout/buildLayout'; -import type { PanZoomState } from '$lib/person/genealogy/panZoom'; +import { type PanZoomState, clampZoom, ZOOM_STEP_KB } from '$lib/person/genealogy/panZoom'; type PersonNodeDTO = components['schemas']['PersonNodeDTO']; type RelationshipDTO = components['schemas']['RelationshipDTO']; @@ -18,6 +18,8 @@ interface Props { edges: RelationshipDTO[]; selectedId: string | null; panZoom: PanZoomState; + /** Emitted when the keyboard, a gesture, or a recentre changes the view. */ + onPanZoom?: (state: PanZoomState) => void; onSelect: (id: string) => void; /** * Force-show or force-hide the generation gutter. When undefined, falls @@ -28,7 +30,15 @@ interface Props { showGutter?: boolean; } -let { nodes, edges, selectedId, panZoom, onSelect, showGutter }: Props = $props(); +let { + nodes, + edges, + selectedId, + panZoom, + onPanZoom = () => {}, + onSelect, + showGutter +}: Props = $props(); const layout = $derived.by(() => buildLayout(nodes, edges)); @@ -112,6 +122,38 @@ function handleNodeKey(event: KeyboardEvent, id: string) { } } +// Canvas-level keyboard: `+`/`-` zoom by the fixed step (OQ-002), arrows pan by +// a tenth of the visible extent. Nodes keep their own Enter/Space selection. +function handleCanvasKey(event: KeyboardEvent) { + const stepX = (baseDims.w / panZoom.z) * 0.1; + const stepY = (baseDims.h / panZoom.z) * 0.1; + switch (event.key) { + case '+': + case '=': + onPanZoom({ ...panZoom, z: clampZoom(panZoom.z + ZOOM_STEP_KB) }); + break; + case '-': + case '_': + onPanZoom({ ...panZoom, z: clampZoom(panZoom.z - ZOOM_STEP_KB) }); + break; + case 'ArrowLeft': + onPanZoom({ ...panZoom, x: panZoom.x - stepX }); + break; + case 'ArrowRight': + onPanZoom({ ...panZoom, x: panZoom.x + stepX }); + break; + case 'ArrowUp': + onPanZoom({ ...panZoom, y: panZoom.y - stepY }); + break; + case 'ArrowDown': + onPanZoom({ ...panZoom, y: panZoom.y + stepY }); + break; + default: + return; + } + event.preventDefault(); +} + const parentEdges = $derived(edges.filter((e) => e.relationType === 'PARENT_OF')); const spouseEdges = $derived(edges.filter((e) => e.relationType === 'SPOUSE_OF')); @@ -182,11 +224,18 @@ const parentLinks = $derived.by(() => { }); + + +