Pedigree connector lines + 4 grandparents #11
@@ -54,3 +54,55 @@ h3,
|
|||||||
::selection {
|
::selection {
|
||||||
background: color-mix(in srgb, var(--color-bronze) 22%, transparent);
|
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%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ export default function FamilyViewPage() {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [firstName, setFirstName] = useState("");
|
const [firstName, setFirstName] = useState("");
|
||||||
// Inline add-relative form: which anchor + kind is open, and the typed name.
|
// 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 [addName, setAddName] = useState("");
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
@@ -179,8 +181,18 @@ export default function FamilyViewPage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddSlot = ({ kind, anchor, label }: { kind: AddKind; anchor: string; label: string }) =>
|
const AddSlot = ({
|
||||||
adding && adding.kind === kind && adding.anchor === anchor ? (
|
formKey,
|
||||||
|
kind,
|
||||||
|
anchor,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
formKey: string;
|
||||||
|
kind: AddKind;
|
||||||
|
anchor: string;
|
||||||
|
label: string;
|
||||||
|
}) =>
|
||||||
|
adding?.key === formKey ? (
|
||||||
<form onSubmit={submitAdd} className="flex w-44 flex-col gap-1">
|
<form onSubmit={submitAdd} className="flex w-44 flex-col gap-1">
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -205,7 +217,7 @@ export default function FamilyViewPage() {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAdding({ kind, anchor });
|
setAdding({ key: formKey, kind, anchor });
|
||||||
setAddName("");
|
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"
|
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() {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
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 ? (
|
||||||
|
<PersonBox id={slotPersonId} muted={depth > 0} />
|
||||||
|
) : (
|
||||||
|
<AddSlot formKey={keyPrefix} kind="parent" anchor={childId} label="add parent" />
|
||||||
|
);
|
||||||
|
if (!slotPersonId || depth >= 2) {
|
||||||
|
return <div className="ped-person">{box}</div>;
|
||||||
|
}
|
||||||
|
const ps = parentsOf(slotPersonId);
|
||||||
|
return (
|
||||||
|
<div className="ped-person">
|
||||||
|
<div className="ped-self">{box}</div>
|
||||||
|
<div className="ped-branch">
|
||||||
|
<div className="ped-leaf">
|
||||||
|
{renderNode(ps[0] ?? null, slotPersonId, `${keyPrefix}-a`, depth + 1)}
|
||||||
|
</div>
|
||||||
|
<div className="ped-leaf">
|
||||||
|
{renderNode(ps[1] ?? null, slotPersonId, `${keyPrefix}-b`, depth + 1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const partners = partnersOf(focus.id);
|
const partners = partnersOf(focus.id);
|
||||||
const children = childrenOf(focus.id);
|
const children = childrenOf(focus.id);
|
||||||
|
|
||||||
@@ -237,46 +281,10 @@ export default function FamilyViewPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pedigree: focus → parents → grandparents */}
|
{/* Pedigree: focus → parents → grandparents, with bracket connectors */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="overflow-x-auto p-6">
|
<CardContent className="overflow-x-auto p-6">
|
||||||
<div className="flex min-w-[40rem] items-stretch gap-8">
|
<div className="min-w-[44rem]">{renderNode(focus.id, focus.id, "ped", 0)}</div>
|
||||||
<div className="flex flex-1 flex-col justify-center gap-3">
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase tracking-wider text-[var(--muted)]">
|
|
||||||
Focus
|
|
||||||
</div>
|
|
||||||
<PersonBox id={focus.id} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col justify-center gap-4">
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase tracking-wider text-[var(--muted)]">
|
|
||||||
Parents
|
|
||||||
</div>
|
|
||||||
{parents.map((pid) => (
|
|
||||||
<PersonBox key={pid} id={pid} muted />
|
|
||||||
))}
|
|
||||||
{parents.length < 2 && <AddSlot kind="parent" anchor={focus.id} label="add parent" />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col justify-center gap-4">
|
|
||||||
<div className="mb-1 text-xs font-semibold uppercase tracking-wider text-[var(--muted)]">
|
|
||||||
Grandparents
|
|
||||||
</div>
|
|
||||||
{parents.length === 0 && (
|
|
||||||
<div className="text-sm text-[var(--muted)]">Add parents first.</div>
|
|
||||||
)}
|
|
||||||
{parents.map((pid) => (
|
|
||||||
<div key={pid} className="flex flex-col gap-2">
|
|
||||||
{parentsOf(pid).map((gp) => (
|
|
||||||
<PersonBox key={gp} id={gp} muted />
|
|
||||||
))}
|
|
||||||
{parentsOf(pid).length < 2 && (
|
|
||||||
<AddSlot kind="parent" anchor={pid} label="add parent" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -289,7 +297,12 @@ export default function FamilyViewPage() {
|
|||||||
{partners.map((id) => (
|
{partners.map((id) => (
|
||||||
<PersonBox key={id} id={id} muted />
|
<PersonBox key={id} id={id} muted />
|
||||||
))}
|
))}
|
||||||
<AddSlot kind="partner" anchor={focus.id} label="add spouse" />
|
<AddSlot
|
||||||
|
formKey={`partner-${focus.id}`}
|
||||||
|
kind="partner"
|
||||||
|
anchor={focus.id}
|
||||||
|
label="add spouse"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -301,7 +314,12 @@ export default function FamilyViewPage() {
|
|||||||
{children.map((id) => (
|
{children.map((id) => (
|
||||||
<PersonBox key={id} id={id} muted />
|
<PersonBox key={id} id={id} muted />
|
||||||
))}
|
))}
|
||||||
<AddSlot kind="child" anchor={focus.id} label="add child" />
|
<AddSlot
|
||||||
|
formKey={`child-${focus.id}`}
|
||||||
|
kind="child"
|
||||||
|
anchor={focus.id}
|
||||||
|
label="add child"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user