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>
This commit is contained in:
@@ -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">
|
||||||
|
{shown.length === 0 ? (
|
||||||
|
<div className="px-4 py-6 text-sm text-[var(--muted)]">No matches.</div>
|
||||||
|
) : (
|
||||||
|
shown.map((p, i) => (
|
||||||
<button
|
<button
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onClick={() => setFocusId(p.id)}
|
onClick={() => setFocusId(p.id)}
|
||||||
className={`rounded-full border px-3 py-1 text-sm transition-colors ${
|
className={`flex w-full items-center justify-between gap-3 px-4 py-2.5 text-left text-sm transition-colors ${
|
||||||
p.id === focusId
|
i > 0 ? "border-t border-[var(--border)]" : ""
|
||||||
? "border-bronze bg-bronze/[0.08] text-bronze"
|
} ${p.id === focusId ? "bg-bronze/[0.08]" : "hover:bg-bronze/[0.05]"}`}
|
||||||
: "border-[var(--border)] hover:border-bronze/60"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{p.primary_name ?? "Unnamed"}
|
<span className="truncate font-medium">{p.primary_name ?? "Unnamed"}</span>
|
||||||
|
<span className="shrink-0 text-xs text-[var(--muted)]">
|
||||||
|
{years.get(p.id) ?? ""}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user