Tree search + click-rebuild; searchable relationship picker; gender dropdown
- Tree page: add a "Find a person" search box that jumps the chart to a match and rebuilds the hourglass (parents/grandparents/partner/children) around them. Clicking any card recenters via family-chart's default behavior (setAncestryDepth 3 / setProgenyDepth 2), syncing focus through setAfterUpdate for the "Open profile" link. - Person detail: replace the relationship "add" <select> with a type-to-filter PersonCombobox so long people lists are searchable. - Person detail: gender is now a Male/Female dropdown, not free text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { components } from "@/lib/api/schema";
|
||||
|
||||
type Person = components["schemas"]["PersonRead"];
|
||||
|
||||
/**
|
||||
* A type-to-filter person picker. Shows a text input; as you type, a dropdown
|
||||
* of matching people appears. Selecting one sets `value` (a person id) and
|
||||
* fills the input with their name. Replaces a plain <select> when the list is
|
||||
* long enough that scanning it by hand is painful.
|
||||
*/
|
||||
export function PersonCombobox({
|
||||
people,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Search for a person…",
|
||||
className,
|
||||
}: {
|
||||
people: Person[];
|
||||
value: string;
|
||||
onChange: (id: string) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const nameOf = useMemo(
|
||||
() => new Map(people.map((p) => [p.id, p.primary_name ?? "Unnamed"])),
|
||||
[people],
|
||||
);
|
||||
|
||||
// Keep the input text in sync when the selection changes externally
|
||||
// (e.g. cleared to "" after a successful add).
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setQuery("");
|
||||
} else if (!open) {
|
||||
setQuery(nameOf.get(value) ?? "");
|
||||
}
|
||||
}, [value, open, nameOf]);
|
||||
|
||||
// Close on outside click.
|
||||
useEffect(() => {
|
||||
function onDoc(e: MouseEvent) {
|
||||
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpen(false);
|
||||
}
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
return () => document.removeEventListener("mousedown", onDoc);
|
||||
}, []);
|
||||
|
||||
const matches = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
const pool = q
|
||||
? people.filter((p) => (p.primary_name ?? "").toLowerCase().includes(q))
|
||||
: people;
|
||||
return pool.slice(0, 10);
|
||||
}, [query, people]);
|
||||
|
||||
const base =
|
||||
"h-9 w-56 rounded-md border border-[var(--border)] bg-[var(--surface)] px-2 text-sm placeholder:text-[var(--muted)] focus-visible:border-bronze focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bronze/40";
|
||||
|
||||
return (
|
||||
<div ref={wrapRef} className="relative">
|
||||
<input
|
||||
className={`${base} ${className ?? ""}`}
|
||||
value={query}
|
||||
placeholder={placeholder}
|
||||
onFocus={() => setOpen(true)}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
setOpen(true);
|
||||
if (value) onChange(""); // typing invalidates the prior pick
|
||||
}}
|
||||
/>
|
||||
{open && matches.length > 0 && (
|
||||
<ul className="absolute z-30 mt-1 max-h-64 w-72 overflow-auto rounded-lg border border-[var(--border)] bg-[var(--surface)] shadow-lg">
|
||||
{matches.map((p) => (
|
||||
<li key={p.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(p.id);
|
||||
setQuery(p.primary_name ?? "Unnamed");
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`block w-full px-3 py-2 text-left text-sm hover:bg-[var(--muted-bg,rgba(0,0,0,0.04))] ${
|
||||
p.id === value ? "text-bronze" : ""
|
||||
}`}
|
||||
>
|
||||
{p.primary_name ?? "Unnamed"}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user