Group opacity on the node <g> made the whole node translucent — including its card fill — so the connector lines drawn beneath a dimmed node showed through it. Render the card fill at full strength outside the dim group and move the lineage focus+dim onto an inner content group (outline + labels) only. The focus ring also leaves the dim group, so a dimmed-but-focused node keeps a full-strength ring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
3.0 KiB
Svelte
118 lines
3.0 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 node's outline + labels 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})"
|
||
onclick={() => onSelect(node.id)}
|
||
onkeydown={handleKey}
|
||
onfocus={() => (focused = true)}
|
||
onblur={() => (focused = false)}
|
||
class="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}
|
||
<!-- Opaque card fill — full strength even when dimmed, so the connectors
|
||
drawn beneath the node never bleed through. The lineage dim lives on the
|
||
content group below; group opacity here would make the fill translucent. -->
|
||
<rect
|
||
width={NODE_W}
|
||
height={NODE_H}
|
||
rx="4"
|
||
fill={selected ? 'var(--c-primary)' : 'var(--c-surface)'}
|
||
/>
|
||
<!-- Outline + labels carry the lineage focus+dim; the box stays opaque. -->
|
||
<g class="lineage-fade" opacity={dimmed ? DIMMED_OPACITY : undefined}>
|
||
<rect
|
||
width={NODE_W}
|
||
height={NODE_H}
|
||
rx="4"
|
||
fill="none"
|
||
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>
|
||
</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>
|