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>
107 lines
2.6 KiB
Svelte
107 lines
2.6 KiB
Svelte
<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>
|