From 8cc6031ef0ed36923410c9c12ece4652269a5974 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 29 May 2026 21:42:53 +0200 Subject: [PATCH] refactor(stammbaum): split StammbaumTree into Connectors + Node components (#692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the three SVG connector layers (+ the parent-link graph computation) into StammbaumConnectors.svelte and the node into StammbaumNode.svelte (which now owns its own focus-ring state). StammbaumTree drops 546→308 lines and is now an orchestrator: layout, gutter/reduced-motion state, viewBox, gestures, rail, anchor. Rendered SVG is byte-identical, so the existing browser tests are unchanged. Verified live: 62 nodes + 58 connector lines render, node-tap selects. Co-Authored-By: Claude Opus 4.8 --- .../genealogy/StammbaumConnectors.svelte | 189 +++++++++++++ .../lib/person/genealogy/StammbaumNode.svelte | 90 ++++++ .../lib/person/genealogy/StammbaumTree.svelte | 258 +----------------- 3 files changed, 289 insertions(+), 248 deletions(-) create mode 100644 frontend/src/lib/person/genealogy/StammbaumConnectors.svelte create mode 100644 frontend/src/lib/person/genealogy/StammbaumNode.svelte diff --git a/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte b/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte new file mode 100644 index 00000000..0a647b70 --- /dev/null +++ b/frontend/src/lib/person/genealogy/StammbaumConnectors.svelte @@ -0,0 +1,189 @@ + + + +{#each parentLinks.shared as group (group.key)} + {@const aCenter = nodeCenter(group.parentA)} + {@const bCenter = nodeCenter(group.parentB)} + {@const childCenters = group.childIds + .map((id) => nodeCenter(id)) + .filter((c): c is { x: number; y: number } => c !== null)} + {#if aCenter && bCenter && childCenters.length > 0} + {@const midX = (aCenter.x + bCenter.x) / 2} + {@const parentBottomY = aCenter.y + NODE_H / 2} + {@const childTopY = childCenters[0].y - NODE_H / 2} + {@const barY = (parentBottomY + childTopY) / 2} + {@const xs = childCenters.map((c) => c.x)} + {@const minX = Math.min(midX, ...xs)} + {@const maxX = Math.max(midX, ...xs)} + + {#if minX !== maxX} + + {/if} + {#each childCenters as cc, i (group.childIds[i])} + + {/each} + {/if} +{/each} + + +{#each parentLinks.single as link (link.key)} + {@const parentCenter = nodeCenter(link.parentId)} + {@const childCenter = nodeCenter(link.childId)} + {#if parentCenter && childCenter} + {@const parentBottomY = parentCenter.y + NODE_H / 2} + {@const childTopY = childCenter.y - NODE_H / 2} + {@const barY = (parentBottomY + childTopY) / 2} + + {#if parentCenter.x !== childCenter.x} + + {/if} + + {/if} +{/each} + + +{#each spouseEdges as e (e.id)} + {@const aCenter = nodeCenter(e.personId)} + {@const bCenter = nodeCenter(e.relatedPersonId)} + {#if aCenter && bCenter} + + + {/if} +{/each} diff --git a/frontend/src/lib/person/genealogy/StammbaumNode.svelte b/frontend/src/lib/person/genealogy/StammbaumNode.svelte new file mode 100644 index 00000000..f2877994 --- /dev/null +++ b/frontend/src/lib/person/genealogy/StammbaumNode.svelte @@ -0,0 +1,90 @@ + + + onSelect(node.id)} + onkeydown={handleKey} + onfocus={() => (focused = true)} + onblur={() => (focused = false)} + class="cursor-pointer focus:outline-none" +> + {#if focused} + + {/if} + + {#if selected} + + {/if} + + {node.displayName} + + {#if node.birthYear || node.deathYear} + + {node.birthYear ?? '?'}–{node.deathYear ?? ''} + + {/if} + diff --git a/frontend/src/lib/person/genealogy/StammbaumTree.svelte b/frontend/src/lib/person/genealogy/StammbaumTree.svelte index 711dc6f2..7d6caab8 100644 --- a/frontend/src/lib/person/genealogy/StammbaumTree.svelte +++ b/frontend/src/lib/person/genealogy/StammbaumTree.svelte @@ -1,6 +1,6 @@ @@ -364,180 +288,18 @@ const parentLinks = $derived.by(() => { {/each} {/if} - - {#each parentLinks.shared as group (group.key)} - {@const aCenter = nodeCenter(group.parentA)} - {@const bCenter = nodeCenter(group.parentB)} - {@const childCenters = group.childIds - .map((id) => nodeCenter(id)) - .filter((c): c is { x: number; y: number } => c !== null)} - {#if aCenter && bCenter && childCenters.length > 0} - {@const midX = (aCenter.x + bCenter.x) / 2} - {@const parentBottomY = aCenter.y + NODE_H / 2} - {@const childTopY = childCenters[0].y - NODE_H / 2} - {@const barY = (parentBottomY + childTopY) / 2} - {@const xs = childCenters.map((c) => c.x)} - {@const minX = Math.min(midX, ...xs)} - {@const maxX = Math.max(midX, ...xs)} - - {#if minX !== maxX} - - {/if} - {#each childCenters as cc, i (group.childIds[i])} - - {/each} - {/if} - {/each} - - - {#each parentLinks.single as link (link.key)} - {@const parentCenter = nodeCenter(link.parentId)} - {@const childCenter = nodeCenter(link.childId)} - {#if parentCenter && childCenter} - {@const parentBottomY = parentCenter.y + NODE_H / 2} - {@const childTopY = childCenter.y - NODE_H / 2} - {@const barY = (parentBottomY + childTopY) / 2} - - {#if parentCenter.x !== childCenter.x} - - {/if} - - {/if} - {/each} - - - {#each spouseEdges as e (e.id)} - {@const aCenter = nodeCenter(e.personId)} - {@const bCenter = nodeCenter(e.relatedPersonId)} - {#if aCenter && bCenter} - - - {/if} - {/each} + {#each nodes as node (node.id)} {@const pos = layout.positions.get(node.id)} {#if pos} - {@const isSelected = selectedId === node.id} - {@const isFocused = focusedId === node.id} - onSelect(node.id)} - onkeydown={(e) => handleNodeKey(e, node.id)} - onfocus={() => (focusedId = node.id)} - onblur={() => (focusedId = null)} - class="cursor-pointer focus:outline-none" - > - {#if isFocused} - - {/if} - - {#if isSelected} - - {/if} - - {node.displayName} - - {#if node.birthYear || node.deathYear} - - {node.birthYear ?? '?'}–{node.deathYear ?? ''} - - {/if} - + {/if} {/each}