feat(stammbaum): render cross-level links with a distinct dash (#724)
StammbaumConnectors takes the layout's crossLinks and draws those parent->child connectors with a 2 6 dash at reduced opacity — deliberately distinct from the ended-marriage spouse dash (4 4) and from a solid parent drop. Geometry still lands on the child top, so the meaning is carried redundantly (WCAG 1.4.1). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,13 @@ type RelationshipDTO = components['schemas']['RelationshipDTO'];
|
||||
interface Props {
|
||||
edges: RelationshipDTO[];
|
||||
positions: Layout['positions'];
|
||||
/**
|
||||
* Displaced parent→child edges (cross-level intra-family marriages): the
|
||||
* child lives in a spouse's run elsewhere, so the connector is drawn with a
|
||||
* distinct dash so it never reads as a normal parent drop or an ended
|
||||
* marriage. Geometry still lands on the child, so meaning is redundant.
|
||||
*/
|
||||
crossLinks?: Layout['crossLinks'];
|
||||
/**
|
||||
* Whether the connector joining two people is active (full strength). A
|
||||
* connector is active only when both endpoints are active; otherwise it is
|
||||
@@ -17,7 +24,18 @@ interface Props {
|
||||
isConnectorActive?: (aId: string, bId: string) => boolean;
|
||||
}
|
||||
|
||||
let { edges, positions, isConnectorActive = () => true }: Props = $props();
|
||||
let { edges, positions, crossLinks = [], isConnectorActive = () => true }: Props = $props();
|
||||
|
||||
// Dash cadence + opacity for a cross-link, deliberately distinct from the
|
||||
// ended-marriage spouse dash (`4 4`) so the two never read alike (WCAG 1.4.1:
|
||||
// geometry carries the meaning too, not stroke alone).
|
||||
const CROSS_LINK_DASH = '2 6';
|
||||
const CROSS_LINK_OPACITY = 0.55;
|
||||
|
||||
const crossLinkSet = $derived(new SvelteSet(crossLinks.map((c) => `${c.parentId}->${c.childId}`)));
|
||||
function isCrossLink(parentId: string, childId: string): boolean {
|
||||
return crossLinkSet.has(`${parentId}->${childId}`);
|
||||
}
|
||||
|
||||
/** SVG group opacity for a connector: full when active, dimmed otherwise. */
|
||||
function connectorOpacity(active: boolean): number | undefined {
|
||||
@@ -149,6 +167,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
centres (a child without a position drops out of both together). -->
|
||||
{@const childActive =
|
||||
isConnectorActive(group.parentA, cc.id) && isConnectorActive(group.parentB, cc.id)}
|
||||
{@const childCross =
|
||||
isCrossLink(group.parentA, cc.id) || isCrossLink(group.parentB, cc.id)}
|
||||
<g class="lineage-fade" opacity={connectorOpacity(childActive)}>
|
||||
<line
|
||||
x1={cc.x}
|
||||
@@ -157,6 +177,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
y2={childTopY}
|
||||
stroke="var(--c-primary)"
|
||||
stroke-width="1.5"
|
||||
stroke-dasharray={childCross ? CROSS_LINK_DASH : undefined}
|
||||
stroke-opacity={childCross ? CROSS_LINK_OPACITY : undefined}
|
||||
/>
|
||||
</g>
|
||||
{/each}
|
||||
@@ -172,6 +194,9 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
{@const childTopY = childCenter.y - NODE_H / 2}
|
||||
{@const barY = (parentBottomY + childTopY) / 2}
|
||||
{@const active = isConnectorActive(link.parentId, link.childId)}
|
||||
{@const cross = isCrossLink(link.parentId, link.childId)}
|
||||
{@const dash = cross ? CROSS_LINK_DASH : undefined}
|
||||
{@const strokeOpacity = cross ? CROSS_LINK_OPACITY : undefined}
|
||||
<g class="lineage-fade" opacity={connectorOpacity(active)}>
|
||||
<line
|
||||
x1={parentCenter.x}
|
||||
@@ -180,6 +205,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
y2={barY}
|
||||
stroke="var(--c-primary)"
|
||||
stroke-width="1.5"
|
||||
stroke-dasharray={dash}
|
||||
stroke-opacity={strokeOpacity}
|
||||
/>
|
||||
{#if parentCenter.x !== childCenter.x}
|
||||
<line
|
||||
@@ -189,6 +216,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
y2={barY}
|
||||
stroke="var(--c-primary)"
|
||||
stroke-width="1.5"
|
||||
stroke-dasharray={dash}
|
||||
stroke-opacity={strokeOpacity}
|
||||
/>
|
||||
{/if}
|
||||
<line
|
||||
@@ -198,6 +227,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
||||
y2={childTopY}
|
||||
stroke="var(--c-primary)"
|
||||
stroke-width="1.5"
|
||||
stroke-dasharray={dash}
|
||||
stroke-opacity={strokeOpacity}
|
||||
/>
|
||||
</g>
|
||||
{/if}
|
||||
|
||||
@@ -309,6 +309,7 @@ function handleCanvasKey(event: KeyboardEvent) {
|
||||
<StammbaumConnectors
|
||||
edges={edges}
|
||||
positions={layout.positions}
|
||||
crossLinks={layout.crossLinks}
|
||||
isConnectorActive={isConnectorActive}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user