feat(stammbaum): centre the tapped person above the bottom sheet (#703)
On a touch viewport (below the md breakpoint, where the bottom sheet overlays the lower part of the canvas), tapping a person now auto-centres them via recentreAbove with a 0.3 height bias, so the highlighted anchor lands in the band above the sheet instead of behind it (AC8). On desktop the side panel is a flex sibling that never covers the tree, so the bias is 0 and selection does not pan. StammbaumTree's recentre effect takes a centreBiasFraction prop and the page drives it from a matchMedia flag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
|||||||
type PanZoomState,
|
type PanZoomState,
|
||||||
clampZoom,
|
clampZoom,
|
||||||
clampPan,
|
clampPan,
|
||||||
recentreOn,
|
recentreAbove,
|
||||||
cornerView,
|
cornerView,
|
||||||
ZOOM_STEP_KB
|
ZOOM_STEP_KB
|
||||||
} from '$lib/person/genealogy/panZoom';
|
} from '$lib/person/genealogy/panZoom';
|
||||||
@@ -35,6 +35,11 @@ interface Props {
|
|||||||
onPanZoom?: (state: PanZoomState) => void;
|
onPanZoom?: (state: PanZoomState) => void;
|
||||||
/** When set to a node id, the canvas recentres on that node (US-PAN-005). */
|
/** When set to a node id, the canvas recentres on that node (US-PAN-005). */
|
||||||
centreOnId?: string | null;
|
centreOnId?: string | null;
|
||||||
|
/**
|
||||||
|
* Fraction of the viewBox height to lift a recentred node above the centre,
|
||||||
|
* so on a phone the anchor clears the bottom sheet (#703 AC8). 0 centres it.
|
||||||
|
*/
|
||||||
|
centreBiasFraction?: number;
|
||||||
/** Fired on the first pointer interaction with the canvas (affordance dismiss). */
|
/** Fired on the first pointer interaction with the canvas (affordance dismiss). */
|
||||||
onActivity?: () => void;
|
onActivity?: () => void;
|
||||||
/** When true, the initial view is anchored to the tree's top-left corner. */
|
/** When true, the initial view is anchored to the tree's top-left corner. */
|
||||||
@@ -56,6 +61,7 @@ let {
|
|||||||
panZoom,
|
panZoom,
|
||||||
onPanZoom = () => {},
|
onPanZoom = () => {},
|
||||||
centreOnId = null,
|
centreOnId = null,
|
||||||
|
centreBiasFraction = 0,
|
||||||
onActivity,
|
onActivity,
|
||||||
anchorTopLeft = false,
|
anchorTopLeft = false,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -219,7 +225,7 @@ $effect(() => {
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
const c = nodeCenter(id);
|
const c = nodeCenter(id);
|
||||||
if (c) onPanZoom(recentreOn(c, baseCentre, panZoom, true));
|
if (c) onPanZoom(recentreAbove(c, baseCentre, panZoom, baseDims.h, centreBiasFraction));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,31 @@ async function centreOnSelected() {
|
|||||||
centreOnId = null;
|
centreOnId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Below the md breakpoint the side panel is replaced by a bottom sheet that
|
||||||
|
// overlays the lower ~60dvh of the canvas. On a phone we therefore auto-centre
|
||||||
|
// the tapped person into the band above the sheet (#703 AC8); on desktop the
|
||||||
|
// panel is a flex sibling that never covers the tree, so no centring is needed.
|
||||||
|
const MOBILE_QUERY = '(max-width: 767px)';
|
||||||
|
/** How far above the viewBox centre to lift the tapped anchor on mobile. */
|
||||||
|
const MOBILE_CENTRE_BIAS = 0.3;
|
||||||
|
let isMobile = $state(
|
||||||
|
typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
||||||
|
? window.matchMedia(MOBILE_QUERY).matches
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
||||||
|
const mq = window.matchMedia(MOBILE_QUERY);
|
||||||
|
const handler = (e: MediaQueryListEvent) => (isMobile = e.matches);
|
||||||
|
mq.addEventListener('change', handler);
|
||||||
|
return () => mq.removeEventListener('change', handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function selectPerson(id: string) {
|
||||||
|
selectedId = id;
|
||||||
|
if (isMobile) await centreOnSelected();
|
||||||
|
}
|
||||||
|
|
||||||
let cancelAnimation = () => {};
|
let cancelAnimation = () => {};
|
||||||
function fitToScreen() {
|
function fitToScreen() {
|
||||||
cancelAnimation();
|
cancelAnimation();
|
||||||
@@ -140,10 +165,11 @@ $effect(() => {
|
|||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
panZoom={view}
|
panZoom={view}
|
||||||
centreOnId={centreOnId}
|
centreOnId={centreOnId}
|
||||||
|
centreBiasFraction={isMobile ? MOBILE_CENTRE_BIAS : 0}
|
||||||
anchorTopLeft={!page.url.searchParams.has('z')}
|
anchorTopLeft={!page.url.searchParams.has('z')}
|
||||||
onPanZoom={(v) => (view = v)}
|
onPanZoom={(v) => (view = v)}
|
||||||
onActivity={() => (canvasActivity = true)}
|
onActivity={() => (canvasActivity = true)}
|
||||||
onSelect={(id) => (selectedId = id)}
|
onSelect={selectPerson}
|
||||||
/>
|
/>
|
||||||
<StammbaumAffordance dismissed={canvasActivity} />
|
<StammbaumAffordance dismissed={canvasActivity} />
|
||||||
<StammbaumControls onZoomIn={zoomIn} onZoomOut={zoomOut} onFit={fitToScreen} />
|
<StammbaumControls onZoomIn={zoomIn} onZoomOut={zoomOut} onFit={fitToScreen} />
|
||||||
|
|||||||
Reference in New Issue
Block a user