2 Commits

Author SHA1 Message Date
justin f6bcf198ee Make the people index a scalable scrollable directory
A flat wrap of every person didn't scale to imported trees. Replace it with a bounded (max-height, scrollable) searchable directory: clean name + birth–death-year rows, focus highlight, a result count, and a 200-row cap with a 'refine your search' notice so a thousand-person tree stays fast and usable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
2026-06-06 22:54:08 -04:00
justin b13fafd624 Merge pull request 'Phase 2: GEDCOM import/export' (#12) from phase2-gedcom into main
build-backend / build (push) Successful in 26s
build-frontend / build (push) Successful in 1m22s
2026-06-06 22:46:50 -04:00
+33 -20
View File
@@ -268,6 +268,7 @@ export default function FamilyViewPage() {
const matches = search const matches = search
? sorted.filter((p) => (p.primary_name ?? "").toLowerCase().includes(search.toLowerCase())) ? sorted.filter((p) => (p.primary_name ?? "").toLowerCase().includes(search.toLowerCase()))
: sorted; : sorted;
const shown = matches.slice(0, 200); // cap DOM nodes; refine search to narrow
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@@ -325,32 +326,44 @@ export default function FamilyViewPage() {
</Card> </Card>
</div> </div>
{/* Searchable index of everyone in the tree */} {/* Scrollable, searchable people directory (scales to large trees) */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<h2 className="font-serif text-base font-semibold">All people ({people.length})</h2> <h2 className="font-serif text-base font-semibold">People ({people.length})</h2>
<Input <Input
className="w-56" className="w-64"
placeholder="Search…" placeholder="Search by name…"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
</div> </div>
<div className="flex flex-wrap gap-2"> <Card className="overflow-hidden">
{matches.map((p) => ( <div className="max-h-96 overflow-y-auto">
<button {shown.length === 0 ? (
key={p.id} <div className="px-4 py-6 text-sm text-[var(--muted)]">No matches.</div>
onClick={() => setFocusId(p.id)} ) : (
className={`rounded-full border px-3 py-1 text-sm transition-colors ${ shown.map((p, i) => (
p.id === focusId <button
? "border-bronze bg-bronze/[0.08] text-bronze" key={p.id}
: "border-[var(--border)] hover:border-bronze/60" onClick={() => setFocusId(p.id)}
}`} className={`flex w-full items-center justify-between gap-3 px-4 py-2.5 text-left text-sm transition-colors ${
> i > 0 ? "border-t border-[var(--border)]" : ""
{p.primary_name ?? "Unnamed"} } ${p.id === focusId ? "bg-bronze/[0.08]" : "hover:bg-bronze/[0.05]"}`}
</button> >
))} <span className="truncate font-medium">{p.primary_name ?? "Unnamed"}</span>
</div> <span className="shrink-0 text-xs text-[var(--muted)]">
{years.get(p.id) ?? ""}
</span>
</button>
))
)}
</div>
{matches.length > shown.length && (
<div className="border-t border-[var(--border)] bg-[var(--surface)] px-4 py-2 text-xs text-[var(--muted)]">
Showing {shown.length} of {matches.length} refine your search to narrow.
</div>
)}
</Card>
</div> </div>
</div> </div>
); );