feat(stammbaum): dim connectors outside the highlighted lineage (#703)
StammbaumConnectors gains an isConnectorActive(a, b) predicate prop and wraps each logical connector in a <g opacity> group. A connector is full strength only when both joined people are active; otherwise it dims to DIMMED_OPACITY. The shared parent-pair drop+bar keys on both parents, while each child vertical keys on both parents AND that child — so the bar stays lit to a lineage child yet dims to a collateral sibling on the same row. Defaults to always-active, so no highlight means no dimming. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import { type Layout, 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 RelationshipDTO = components['schemas']['RelationshipDTO'];
|
||||
@@ -8,9 +9,20 @@ type RelationshipDTO = components['schemas']['RelationshipDTO'];
|
||||
interface Props {
|
||||
edges: RelationshipDTO[];
|
||||
positions: Layout['positions'];
|
||||
/**
|
||||
* Whether the connector joining two people is active (full strength). A
|
||||
* connector is active only when both endpoints are active; otherwise it is
|
||||
* dimmed. Defaults to always-active, so no lineage highlight means no dimming.
|
||||
*/
|
||||
isConnectorActive?: (aId: string, bId: string) => boolean;
|
||||
}
|
||||
|
||||
let { edges, positions }: Props = $props();
|
||||
let { edges, positions, isConnectorActive = () => true }: Props = $props();
|
||||
|
||||
/** SVG group opacity for a connector: full when active, dimmed otherwise. */
|
||||
function connectorOpacity(active: boolean): number | undefined {
|
||||
return active ? undefined : DIMMED_OPACITY;
|
||||
}
|
||||
|
||||
function nodeCenter(id: string): { x: number; y: number } | null {
|
||||
const p = positions.get(id);
|
||||
@@ -104,6 +116,9 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
{@const xs = childCenters.map((c) => c.x)}
|
||||
{@const minX = Math.min(midX, ...xs)}
|
||||
{@const maxX = Math.max(midX, ...xs)}
|
||||
{@const pairActive = isConnectorActive(group.parentA, group.parentB)}
|
||||
<!-- Drop from the spouse midpoint to the sibling bar: joins the parent pair. -->
|
||||
<g class="lineage-fade" opacity={connectorOpacity(pairActive)}>
|
||||
<line
|
||||
x1={midX}
|
||||
y1={parentBottomY}
|
||||
@@ -113,9 +128,24 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
{#if minX !== maxX}
|
||||
<line x1={minX} y1={barY} x2={maxX} y2={barY} stroke="var(--c-primary)" stroke-width="1.5" />
|
||||
<line
|
||||
x1={minX}
|
||||
y1={barY}
|
||||
x2={maxX}
|
||||
y2={barY}
|
||||
stroke="var(--c-primary)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
{/if}
|
||||
</g>
|
||||
{#each childCenters as cc, i (group.childIds[i])}
|
||||
<!-- Each vertical joins the parent pair to one child: active only when
|
||||
both parents and that child are active, so the same bar can stay lit
|
||||
to the lineage child while dimming to a collateral sibling. -->
|
||||
{@const childActive =
|
||||
isConnectorActive(group.parentA, group.childIds[i]) &&
|
||||
isConnectorActive(group.parentB, group.childIds[i])}
|
||||
<g class="lineage-fade" opacity={connectorOpacity(childActive)}>
|
||||
<line
|
||||
x1={cc.x}
|
||||
y1={barY}
|
||||
@@ -124,6 +154,7 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
stroke="var(--c-primary)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</g>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -136,6 +167,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
{@const parentBottomY = parentCenter.y + NODE_H / 2}
|
||||
{@const childTopY = childCenter.y - NODE_H / 2}
|
||||
{@const barY = (parentBottomY + childTopY) / 2}
|
||||
{@const active = isConnectorActive(link.parentId, link.childId)}
|
||||
<g class="lineage-fade" opacity={connectorOpacity(active)}>
|
||||
<line
|
||||
x1={parentCenter.x}
|
||||
y1={parentBottomY}
|
||||
@@ -162,6 +195,7 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
stroke="var(--c-primary)"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</g>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -170,6 +204,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
{@const aCenter = nodeCenter(e.personId)}
|
||||
{@const bCenter = nodeCenter(e.relatedPersonId)}
|
||||
{#if aCenter && bCenter}
|
||||
{@const active = isConnectorActive(e.personId, e.relatedPersonId)}
|
||||
<g class="lineage-fade" opacity={connectorOpacity(active)}>
|
||||
<line
|
||||
x1={aCenter.x}
|
||||
y1={aCenter.y}
|
||||
@@ -185,5 +221,18 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
r="6"
|
||||
fill="var(--c-primary)"
|
||||
/>
|
||||
</g>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user