feat(stammbaum): edge-fade mask when zoomed past fit (#692)
Permanent 4-edge mask-image gradient cues off-screen content when the tree is zoomed in; nothing fades at fit. Replaces the dropped US-PAN-006 AC3 idle cue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,19 @@ const viewBox = $derived.by(() => {
|
|||||||
return `${cx - w / 2} ${cy - h / 2} ${w} ${h}`;
|
return `${cx - w / 2} ${cy - h / 2} ${w} ${h}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Permanent edge-fade affordance (#692, replaces US-PAN-006 AC3). When the tree
|
||||||
|
// is zoomed past fit, content is clipped at the viewport edges, so a 24px fade
|
||||||
|
// on all four edges cues that more tree exists off-screen. Zero JS beyond this
|
||||||
|
// reactive style; nothing fades at fit (z <= 1, whole tree visible).
|
||||||
|
const EDGE_FADE = 24;
|
||||||
|
const maskStyle = $derived(
|
||||||
|
panZoom.z > 1
|
||||||
|
? `-webkit-mask-image:linear-gradient(to right,transparent,#000 ${EDGE_FADE}px,#000 calc(100% - ${EDGE_FADE}px),transparent),linear-gradient(to bottom,transparent,#000 ${EDGE_FADE}px,#000 calc(100% - ${EDGE_FADE}px),transparent);` +
|
||||||
|
`mask-image:linear-gradient(to right,transparent,#000 ${EDGE_FADE}px,#000 calc(100% - ${EDGE_FADE}px),transparent),linear-gradient(to bottom,transparent,#000 ${EDGE_FADE}px,#000 calc(100% - ${EDGE_FADE}px),transparent);` +
|
||||||
|
`-webkit-mask-composite:source-in;mask-composite:intersect;`
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
function nodeCenter(id: string): { x: number; y: number } | null {
|
function nodeCenter(id: string): { x: number; y: number } | null {
|
||||||
const p = layout.positions.get(id);
|
const p = layout.positions.get(id);
|
||||||
if (!p) return null;
|
if (!p) return null;
|
||||||
@@ -273,6 +286,7 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
|||||||
role="img"
|
role="img"
|
||||||
aria-label="Stammbaum"
|
aria-label="Stammbaum"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
style={maskStyle}
|
||||||
onkeydown={handleCanvasKey}
|
onkeydown={handleCanvasKey}
|
||||||
use:panZoomGestures={{
|
use:panZoomGestures={{
|
||||||
state: panZoom,
|
state: panZoom,
|
||||||
|
|||||||
@@ -610,6 +610,28 @@ describe('StammbaumTree keyboard pan/zoom (#692)', () => {
|
|||||||
expect(view.z).toBe(1);
|
expect(view.z).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('omits the edge-fade mask at fit (z = 1) (#692)', async () => {
|
||||||
|
render(StammbaumTree, {
|
||||||
|
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
|
||||||
|
edges: [],
|
||||||
|
selectedId: null,
|
||||||
|
panZoom: { x: 0, y: 0, z: 1 },
|
||||||
|
onSelect: () => {}
|
||||||
|
});
|
||||||
|
expect(document.querySelector('svg')!.getAttribute('style') ?? '').not.toContain('mask-image');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies the edge-fade mask when zoomed past fit (#692)', async () => {
|
||||||
|
render(StammbaumTree, {
|
||||||
|
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
|
||||||
|
edges: [],
|
||||||
|
selectedId: null,
|
||||||
|
panZoom: { x: 0, y: 0, z: 2 },
|
||||||
|
onSelect: () => {}
|
||||||
|
});
|
||||||
|
expect(document.querySelector('svg')!.getAttribute('style') ?? '').toContain('mask-image');
|
||||||
|
});
|
||||||
|
|
||||||
it('does not call onSelect for other keys', async () => {
|
it('does not call onSelect for other keys', async () => {
|
||||||
const onSelect = vi.fn();
|
const onSelect = vi.fn();
|
||||||
render(StammbaumTree, {
|
render(StammbaumTree, {
|
||||||
|
|||||||
Reference in New Issue
Block a user