diff --git a/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte b/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte index 57acf7df..4e0c1b66 100644 --- a/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte +++ b/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte @@ -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(() => { 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)} (() => { y2={childTopY} stroke="var(--c-primary)" stroke-width="1.5" + stroke-dasharray={childCross ? CROSS_LINK_DASH : undefined} + stroke-opacity={childCross ? CROSS_LINK_OPACITY : undefined} /> {/each} @@ -172,6 +194,9 @@ const parentLinks = $derived.by(() => { {@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} (() => { y2={barY} stroke="var(--c-primary)" stroke-width="1.5" + stroke-dasharray={dash} + stroke-opacity={strokeOpacity} /> {#if parentCenter.x !== childCenter.x} (() => { y2={barY} stroke="var(--c-primary)" stroke-width="1.5" + stroke-dasharray={dash} + stroke-opacity={strokeOpacity} /> {/if} (() => { y2={childTopY} stroke="var(--c-primary)" stroke-width="1.5" + stroke-dasharray={dash} + stroke-opacity={strokeOpacity} /> {/if} diff --git a/frontend/src/lib/person/genealogy/StammbaumTree.svelte b/frontend/src/lib/person/genealogy/StammbaumTree.svelte index 53ccf02b..edef45bf 100644 --- a/frontend/src/lib/person/genealogy/StammbaumTree.svelte +++ b/frontend/src/lib/person/genealogy/StammbaumTree.svelte @@ -309,6 +309,7 @@ function handleCanvasKey(event: KeyboardEvent) {