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>
This commit is contained in:
Marcel
2026-05-31 16:28:22 +02:00
parent 7a655ce6f4
commit f6da95014e

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { NODE_W, NODE_H } from '$lib/person/genealogy/layout/buildLayout'; 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'; import type { components } from '$lib/generated/api';
type PersonNodeDTO = components['schemas']['PersonNodeDTO']; type PersonNodeDTO = components['schemas']['PersonNodeDTO'];
@@ -8,10 +9,12 @@ interface Props {
node: PersonNodeDTO; node: PersonNodeDTO;
pos: { x: number; y: number }; pos: { x: number; y: number };
selected: boolean; selected: boolean;
/** Dim the whole node when a lineage is highlighted and this person is outside it. */
dimmed?: boolean;
onSelect: (id: string) => void; onSelect: (id: string) => void;
} }
let { node, pos, selected, onSelect }: Props = $props(); let { node, pos, selected, dimmed = false, onSelect }: Props = $props();
// Each node owns its own focus-ring state (the focus ring is decorative; the // Each node owns its own focus-ring state (the focus ring is decorative; the
// `<g role="button">` is the real focus target). // `<g role="button">` is the real focus target).
@@ -35,11 +38,12 @@ const datesLabel = $derived(
aria-label="{node.displayName}{datesLabel}" aria-label="{node.displayName}{datesLabel}"
aria-expanded={selected} aria-expanded={selected}
transform="translate({pos.x}, {pos.y})" transform="translate({pos.x}, {pos.y})"
opacity={dimmed ? DIMMED_OPACITY : undefined}
onclick={() => onSelect(node.id)} onclick={() => onSelect(node.id)}
onkeydown={handleKey} onkeydown={handleKey}
onfocus={() => (focused = true)} onfocus={() => (focused = true)}
onblur={() => (focused = false)} onblur={() => (focused = false)}
class="cursor-pointer focus:outline-none" class="lineage-fade cursor-pointer focus:outline-none"
> >
{#if focused} {#if focused}
<rect <rect
@@ -88,3 +92,15 @@ const datesLabel = $derived(
</text> </text>
{/if} {/if}
</g> </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>