Public tree view: add generation depth controls (shared with member view)
The public tree chart was fixed at 3 ancestors / 2 descendants. Add the same Generations controls the member view has (slider + number stepper + "All" per direction), applied live around the focused person. Extracts the member page's inline DepthControl into a shared components/depth-control.tsx and uses it in both, so they stay in sync. The public chart gains anc/prog depth state + an apply effect (setAncestryDepth/ setProgenyDepth + updateTree) mirroring the member behavior. tsc clean; next build passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
// Slider + number stepper + "All" toggle for one generation direction
|
||||
// (ancestors or descendants). Shared by the member and public tree views.
|
||||
export function 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;
|
||||
}) {
|
||||
return (
|
||||
<div className={`flex items-center gap-2 ${disabled ? "opacity-40" : ""}`}>
|
||||
<span className="flex w-24 items-center gap-1 text-xs text-[var(--muted)]">
|
||||
<span aria-hidden>{icon}</span> {label}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={12}
|
||||
step={1}
|
||||
value={all ? 12 : value}
|
||||
disabled={disabled || all}
|
||||
onChange={(e) => onValue(Number(e.target.value))}
|
||||
className="w-28 accent-bronze"
|
||||
aria-label={`${label} generations`}
|
||||
/>
|
||||
{all ? (
|
||||
<span className="w-10 text-center text-sm font-medium text-bronze">All</span>
|
||||
) : (
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={99}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => onAll(!all)}
|
||||
title={`Show all ${label.toLowerCase()}`}
|
||||
className={`rounded-md px-2 py-1 text-xs transition-colors ${
|
||||
all
|
||||
? "bg-bronze text-paper"
|
||||
: "border border-[var(--border)] text-[var(--muted)] hover:text-[var(--foreground)]"
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
// Vendored family-chart styles (the package blocks the CSS subpath export).
|
||||
import "../app/trees/[id]/tree/chart.css";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { components } from "@/lib/api/schema";
|
||||
import { DepthControl } from "@/components/depth-control";
|
||||
|
||||
type Person = components["schemas"]["PersonRead"];
|
||||
type Relationship = components["schemas"]["RelationshipRead"];
|
||||
@@ -42,6 +43,16 @@ export function PublicTreeChart({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const chartRef = useRef<any>(null);
|
||||
|
||||
// Generations to show around the focus, each settable (or "all"). ALL_DEPTH is
|
||||
// a number bigger than any real lineage; the chart only renders real people.
|
||||
const [ancDepth, setAncDepth] = useState(3);
|
||||
const [progDepth, setProgDepth] = useState(2);
|
||||
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;
|
||||
|
||||
const parentsOf = useCallback(
|
||||
(id: string) =>
|
||||
rels.filter((x) => x.type === "parent_child" && x.person_to_id === id).map((x) => x.person_from_id),
|
||||
@@ -126,8 +137,8 @@ export function PublicTreeChart({
|
||||
const chart = f3.createChart(containerRef.current, data);
|
||||
chart.setCardHtml().setCardDisplay([["first name", "last name"], ["birthday"]]);
|
||||
chart.setOrientationHorizontal();
|
||||
chart.setAncestryDepth?.(3);
|
||||
chart.setProgenyDepth?.(2);
|
||||
chart.setAncestryDepth?.(effAnc);
|
||||
chart.setProgenyDepth?.(effProg);
|
||||
chart.setAfterUpdate?.(() => {
|
||||
const md = chart.getMainDatum?.();
|
||||
const id = md?.id ?? md?.data?.id;
|
||||
@@ -155,8 +166,41 @@ export function PublicTreeChart({
|
||||
}
|
||||
}, [focusId]);
|
||||
|
||||
// Apply depth changes to the live chart without a full rebuild.
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
chartRef.current.setAncestryDepth?.(effAnc);
|
||||
chartRef.current.setProgenyDepth?.(effProg);
|
||||
chartRef.current.updateTree?.();
|
||||
}, [effAnc, effProg]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 rounded-lg border border-[var(--border)] bg-[var(--surface)] px-4 py-2.5">
|
||||
<span className="text-sm font-medium">Generations</span>
|
||||
<DepthControl
|
||||
label="Ancestors"
|
||||
icon="↑"
|
||||
value={ancDepth}
|
||||
all={ancAll}
|
||||
onValue={(v) => {
|
||||
setAncAll(false);
|
||||
setAncDepth(v);
|
||||
}}
|
||||
onAll={setAncAll}
|
||||
/>
|
||||
<DepthControl
|
||||
label="Descendants"
|
||||
icon="↓"
|
||||
value={progDepth}
|
||||
all={progAll}
|
||||
onValue={(v) => {
|
||||
setProgAll(false);
|
||||
setProgDepth(v);
|
||||
}}
|
||||
onAll={setProgAll}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="f3 rounded-xl border border-[var(--border)]"
|
||||
|
||||
Reference in New Issue
Block a user