From c86771034ca1d069083b0a4cdc7b25d94c4bcf82 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Mon, 8 Jun 2026 22:20:07 -0400 Subject: [PATCH] Tree view: configurable generation depth (ancestors/descendants + All) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Depth was hardcoded (3 ancestors, 2 descendants). Add a controls row to set each direction independently — a slider plus a number stepper, with an "All" toggle per direction — applied around whoever is currently focused. - ancestor/descendant depth held in state; effective value is a large cap when "All" is on (the chart only renders people that exist, so the cap is free). - changes apply to the live chart via setAncestryDepth/setProgenyDepth + updateTree without a full rebuild. - fan mode (ancestors only) takes the ancestor depth via its `generations` prop, capped at 8 to avoid the radial layout's 2^n blow-up; its descendants control is disabled with a note. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Justin Paul --- frontend/app/trees/[id]/tree/page.tsx | 120 +++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/frontend/app/trees/[id]/tree/page.tsx b/frontend/app/trees/[id]/tree/page.tsx index af2547b..ee9ad00 100644 --- a/frontend/app/trees/[id]/tree/page.tsx +++ b/frontend/app/trees/[id]/tree/page.tsx @@ -46,6 +46,16 @@ export default function TreePage() { const [homeId, setHomeId] = useState(null); const [mode, setMode] = useState("landscape"); const [renderNote, setRenderNote] = useState(null); + // How many generations to show around the focus, each independently settable + // (or "all"). ALL_DEPTH is just a number bigger than any real lineage; the + // chart only renders people that exist, so a high cap costs nothing. + const [ancDepth, setAncDepth] = useState(3); // ancestors (backwards) + const [progDepth, setProgDepth] = useState(2); // descendants (forwards) + const [ancAll, setAncAll] = useState(false); + const [progAll, setProgAll] = useState(false); + const ALL_DEPTH = 100; + const effAnc = ancAll ? ALL_DEPTH : ancDepth; + const effProg = progAll ? ALL_DEPTH : progDepth; useEffect(() => { let cancelled = false; @@ -185,9 +195,9 @@ export default function TreePage() { chart.setCardHtml().setCardDisplay([["first name", "last name"], ["birthday"]]); if (mode === "portrait") chart.setOrientationVertical(); else chart.setOrientationHorizontal(); - // Show enough generations that a recenter reveals grandparents + children. - chart.setAncestryDepth?.(3); - chart.setProgenyDepth?.(2); + // Generations to show around the focus (configurable; see depth controls). + chart.setAncestryDepth?.(effAnc); + chart.setProgenyDepth?.(effProg); // Default card click recenters the whole hourglass; sync focus for the // "Open profile" link after every (re)build. chart.setAfterUpdate?.(() => { @@ -219,6 +229,15 @@ export default function TreePage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [status, mode, people, rels, events]); + // Apply depth changes to the already-built chart without a full rebuild + // (landscape/portrait only; the fan reads its own `generations` prop). + useEffect(() => { + if (mode === "fan" || !chartRef.current) return; + chartRef.current.setAncestryDepth?.(effAnc); + chartRef.current.setProgenyDepth?.(effProg); + chartRef.current.updateTree?.(); + }, [effAnc, effProg, mode]); + // Jump the tree (or fan) to a person and rebuild the hourglass around them. const goTo = useCallback( (id: string) => { @@ -261,6 +280,68 @@ export default function TreePage() { ); + // Slider + number stepper + "All" for one generation direction. + const DepthControl = ({ + label, + icon, + value, + all, + onValue, + onAll, + disabled, + }: { + label: string; + icon: string; + value: number; + all: boolean; + onValue: (v: number) => void; + onAll: (b: boolean) => void; + disabled?: boolean; + }) => ( +
+ + {icon} {label} + + onValue(Number(e.target.value))} + className="w-28 accent-bronze" + aria-label={`${label} generations`} + /> + {all ? ( + All + ) : ( + onValue(Math.max(0, Math.min(99, Number(e.target.value) || 0)))} + className="h-7 w-12 rounded-md border border-[var(--border)] bg-[var(--surface)] px-1 text-center text-sm" + /> + )} + +
+ ); + return (
@@ -319,6 +400,38 @@ export default function TreePage() {
+ {status === "ready" && ( +
+ Generations + { + setAncAll(false); + setAncDepth(v); + }} + onAll={setAncAll} + /> + { + setProgAll(false); + setProgDepth(v); + }} + onAll={setProgAll} + disabled={mode === "fan"} + /> + {mode === "fan" && ( + Fan shows ancestors only. + )} +
+ )} + {status === "empty" && (

No people yet — add some under People, or import a GEDCOM.

)} @@ -338,6 +451,7 @@ export default function TreePage() { nameOf={nameOf} yearOf={yearOf} onSelect={setFocusId} + generations={Math.min(effAnc, 8)} /> ) : ( -- 2.52.0