From 99a660485e88dfe0a7c9218d94c9c29d55da5b63 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Sat, 6 Jun 2026 22:32:10 -0400 Subject: [PATCH] Pedigree: connector lines + correct 4-grandparent structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilds the family view's pedigree as a recursive bracket chart with CSS connector lines — focus links to its two parents (2 lines), and each parent links to its two parents (4 lines to grandparents). Fixes the prior ambiguity where grandparent slots weren't tied to a specific parent: now every parent shows its own two parent slots, so a person clearly has up to four grandparents grouped by lineage. Height-robust connectors (each leaf draws its own spine half + stub). Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Justin Paul --- frontend/app/globals.css | 52 +++++++++++++++ frontend/app/trees/[id]/page.tsx | 108 ++++++++++++++++++------------- 2 files changed, 115 insertions(+), 45 deletions(-) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 55e685a..dfbe818 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -54,3 +54,55 @@ h3, ::selection { background: color-mix(in srgb, var(--color-bronze) 22%, transparent); } + +/* Pedigree bracket connectors (ancestors grow rightward). Each leaf draws its + own half of the vertical spine + a horizontal stub, so lines stay correct + regardless of box heights: focus → 2 parents, each parent → 2 grandparents. */ +.ped-person { + display: flex; + align-items: center; +} +.ped-self { + flex-shrink: 0; +} +.ped-branch { + position: relative; + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-left: 2.5rem; +} +.ped-branch::before { + content: ""; + position: absolute; + left: -2.5rem; + top: 50%; + width: 2.5rem; + border-top: 1px solid var(--border); +} +.ped-leaf { + position: relative; + padding-left: 1.5rem; +} +.ped-leaf::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + width: 1.5rem; + border-top: 1px solid var(--border); +} +.ped-leaf::after { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + border-left: 1px solid var(--border); +} +.ped-leaf:first-child::after { + top: 50%; +} +.ped-leaf:last-child::after { + bottom: 50%; +} diff --git a/frontend/app/trees/[id]/page.tsx b/frontend/app/trees/[id]/page.tsx index 32a4ad6..e533e59 100644 --- a/frontend/app/trees/[id]/page.tsx +++ b/frontend/app/trees/[id]/page.tsx @@ -36,7 +36,9 @@ export default function FamilyViewPage() { const [search, setSearch] = useState(""); const [firstName, setFirstName] = useState(""); // Inline add-relative form: which anchor + kind is open, and the typed name. - const [adding, setAdding] = useState<{ kind: AddKind; anchor: string } | null>(null); + // `key` keeps each empty slot's inline form independent (a person has 2 + // parents, 4 grandparents — many same-kind/anchor slots can coexist). + const [adding, setAdding] = useState<{ key: string; kind: AddKind; anchor: string } | null>(null); const [addName, setAddName] = useState(""); const load = useCallback(async () => { @@ -179,8 +181,18 @@ export default function FamilyViewPage() { ); }; - const AddSlot = ({ kind, anchor, label }: { kind: AddKind; anchor: string; label: string }) => - adding && adding.kind === kind && adding.anchor === anchor ? ( + const AddSlot = ({ + formKey, + kind, + anchor, + label, + }: { + formKey: string; + kind: AddKind; + anchor: string; + label: string; + }) => + adding?.key === formKey ? (
{ - setAdding({ kind, anchor }); + setAdding({ key: formKey, kind, anchor }); setAddName(""); }} className="w-44 rounded-lg border border-dashed border-[var(--border)] px-3 py-2 text-left text-sm text-[var(--muted)] hover:border-bronze hover:text-bronze" @@ -214,7 +226,39 @@ export default function FamilyViewPage() { ); - const parents = parentsOf(focus.id); + // Recursive ancestor chart (grows rightward): a node is its box plus a + // two-leaf "branch" of its parents, with CSS bracket connectors. Depth 0 = + // focus, capped at grandparents (depth 2). + const renderNode = ( + slotPersonId: string | null, + childId: string, + keyPrefix: string, + depth: number, + ): React.ReactNode => { + const box = slotPersonId ? ( + 0} /> + ) : ( + + ); + if (!slotPersonId || depth >= 2) { + return
{box}
; + } + const ps = parentsOf(slotPersonId); + return ( +
+
{box}
+
+
+ {renderNode(ps[0] ?? null, slotPersonId, `${keyPrefix}-a`, depth + 1)} +
+
+ {renderNode(ps[1] ?? null, slotPersonId, `${keyPrefix}-b`, depth + 1)} +
+
+
+ ); + }; + const partners = partnersOf(focus.id); const children = childrenOf(focus.id); @@ -237,46 +281,10 @@ export default function FamilyViewPage() { - {/* Pedigree: focus → parents → grandparents */} + {/* Pedigree: focus → parents → grandparents, with bracket connectors */} -
-
-
- Focus -
- -
- -
-
- Parents -
- {parents.map((pid) => ( - - ))} - {parents.length < 2 && } -
- -
-
- Grandparents -
- {parents.length === 0 && ( -
Add parents first.
- )} - {parents.map((pid) => ( -
- {parentsOf(pid).map((gp) => ( - - ))} - {parentsOf(pid).length < 2 && ( - - )} -
- ))} -
-
+
{renderNode(focus.id, focus.id, "ped", 0)}
@@ -289,7 +297,12 @@ export default function FamilyViewPage() { {partners.map((id) => ( ))} - + @@ -301,7 +314,12 @@ export default function FamilyViewPage() { {children.map((id) => ( ))} - + -- 2.52.0