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 {
|
interface Props {
|
||||||
edges: RelationshipDTO[];
|
edges: RelationshipDTO[];
|
||||||
positions: Layout['positions'];
|
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
|
* Whether the connector joining two people is active (full strength). A
|
||||||
* connector is active only when both endpoints are active; otherwise it is
|
* connector is active only when both endpoints are active; otherwise it is
|
||||||
@@ -17,7 +24,18 @@ interface Props {
|
|||||||
isConnectorActive?: (aId: string, bId: string) => boolean;
|
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. */
|
/** SVG group opacity for a connector: full when active, dimmed otherwise. */
|
||||||
function connectorOpacity(active: boolean): number | undefined {
|
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). -->
|
centres (a child without a position drops out of both together). -->
|
||||||
{@const childActive =
|
{@const childActive =
|
||||||
isConnectorActive(group.parentA, cc.id) && isConnectorActive(group.parentB, cc.id)}
|
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)}>
|
<g class="lineage-fade" opacity={connectorOpacity(childActive)}>
|
||||||
<line
|
<line
|
||||||
x1={cc.x}
|
x1={cc.x}
|
||||||
@@ -157,6 +177,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
|||||||
y2={childTopY}
|
y2={childTopY}
|
||||||
stroke="var(--c-primary)"
|
stroke="var(--c-primary)"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
|
stroke-dasharray={childCross ? CROSS_LINK_DASH : undefined}
|
||||||
|
stroke-opacity={childCross ? CROSS_LINK_OPACITY : undefined}
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -172,6 +194,9 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
|||||||
{@const childTopY = childCenter.y - NODE_H / 2}
|
{@const childTopY = childCenter.y - NODE_H / 2}
|
||||||
{@const barY = (parentBottomY + childTopY) / 2}
|
{@const barY = (parentBottomY + childTopY) / 2}
|
||||||
{@const active = isConnectorActive(link.parentId, link.childId)}
|
{@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)}>
|
<g class="lineage-fade" opacity={connectorOpacity(active)}>
|
||||||
<line
|
<line
|
||||||
x1={parentCenter.x}
|
x1={parentCenter.x}
|
||||||
@@ -180,6 +205,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
|||||||
y2={barY}
|
y2={barY}
|
||||||
stroke="var(--c-primary)"
|
stroke="var(--c-primary)"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
|
stroke-dasharray={dash}
|
||||||
|
stroke-opacity={strokeOpacity}
|
||||||
/>
|
/>
|
||||||
{#if parentCenter.x !== childCenter.x}
|
{#if parentCenter.x !== childCenter.x}
|
||||||
<line
|
<line
|
||||||
@@ -189,6 +216,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
|||||||
y2={barY}
|
y2={barY}
|
||||||
stroke="var(--c-primary)"
|
stroke="var(--c-primary)"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
|
stroke-dasharray={dash}
|
||||||
|
stroke-opacity={strokeOpacity}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<line
|
<line
|
||||||
@@ -198,6 +227,8 @@ const parentLinks = $derived.by<ParentLinks>(() => {
|
|||||||
y2={childTopY}
|
y2={childTopY}
|
||||||
stroke="var(--c-primary)"
|
stroke="var(--c-primary)"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
|
stroke-dasharray={dash}
|
||||||
|
stroke-opacity={strokeOpacity}
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ function handleCanvasKey(event: KeyboardEvent) {
|
|||||||
<StammbaumConnectors
|
<StammbaumConnectors
|
||||||
edges={edges}
|
edges={edges}
|
||||||
positions={layout.positions}
|
positions={layout.positions}
|
||||||
|
crossLinks={layout.crossLinks}
|
||||||
isConnectorActive={isConnectorActive}
|
isConnectorActive={isConnectorActive}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user