Files
familienarchiv/frontend/src/lib/person/genealogy/StammbaumNode.svelte
Marcel f6da95014e feat(stammbaum): dim a node when outside the highlighted lineage (#703)
StammbaumNode gains an optional `dimmed` prop that sets group-level
opacity (DIMMED_OPACITY) on the node's root <g>, so the box, accent bar,
name, and dates fade together as one unit. A lineage-fade CSS transition
eases the change and is neutralised under prefers-reduced-motion. The
selected-node styling (active fill + mint accent bar) is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 16:28:22 +02:00

107 lines
2.6 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { NODE_W, NODE_H } from '$lib/person/genealogy/layout/buildLayout';
import { DIMMED_OPACITY } from '$lib/person/genealogy/layout/highlightLineage';
import type { components } from '$lib/generated/api';
type PersonNodeDTO = components['schemas']['PersonNodeDTO'];
interface Props {
node: PersonNodeDTO;
pos: { x: number; y: number };
selected: boolean;
/** Dim the whole node when a lineage is highlighted and this person is outside it. */
dimmed?: boolean;
onSelect: (id: string) => void;
}
let { node, pos, selected, dimmed = false, onSelect }: Props = $props();
// Each node owns its own focus-ring state (the focus ring is decorative; the
// `<g role="button">` is the real focus target).
let focused = $state(false);
function handleKey(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onSelect(node.id);
}
}
const datesLabel = $derived(
node.birthYear || node.deathYear ? `, ${node.birthYear ?? '?'}${node.deathYear ?? ''}` : ''
);
</script>
<g
role="button"
tabindex="0"
aria-label="{node.displayName}{datesLabel}"
aria-expanded={selected}
transform="translate({pos.x}, {pos.y})"
opacity={dimmed ? DIMMED_OPACITY : undefined}
onclick={() => onSelect(node.id)}
onkeydown={handleKey}
onfocus={() => (focused = true)}
onblur={() => (focused = false)}
class="lineage-fade cursor-pointer focus:outline-none"
>
{#if focused}
<rect
x="-3"
y="-3"
width={NODE_W + 6}
height={NODE_H + 6}
rx="6"
fill="none"
stroke="var(--c-focus-ring)"
stroke-width="2"
/>
{/if}
<rect
width={NODE_W}
height={NODE_H}
rx="4"
fill={selected ? 'var(--c-primary)' : 'var(--c-surface)'}
stroke="var(--c-primary)"
stroke-width="1.5"
/>
{#if selected}
<rect width="4" height={NODE_H} rx="2" fill="var(--c-accent)" />
{/if}
<text
x={NODE_W / 2}
y={NODE_H / 2 - 6}
text-anchor="middle"
font-family="serif"
font-size="16"
fill={selected ? 'var(--c-primary-fg)' : 'var(--c-ink)'}
>
{node.displayName}
</text>
{#if node.birthYear || node.deathYear}
<text
x={NODE_W / 2}
y={NODE_H / 2 + 12}
text-anchor="middle"
font-family="sans-serif"
font-size="12"
fill={selected ? 'var(--c-primary-fg)' : 'var(--c-ink-3)'}
opacity={selected ? 0.75 : 1}
>
{node.birthYear ?? '?'}{node.deathYear ?? ''}
</text>
{/if}
</g>
<style>
/* Ease the lineage focus+dim transition; instant for reduced-motion users. */
.lineage-fade {
transition: opacity 200ms ease;
}
@media (prefers-reduced-motion: reduce) {
.lineage-fade {
transition: none;
}
}
</style>