Tree: a Legend by the pan/zoom hint, and clickable ×N duplicate badges
Two small tree-view aids prompted by "why do some people show ×2". - Legend: a hover/focus "Legend" link next to the "drag to pan…" hint, explaining the ×N badge (a person drawn N times in the view because they connect through more than one line — a shared ancestor or an intermarriage), the gender card colors, and the pan/zoom/recenter controls. - The ×N badge is now clearly clickable (cursor + hover state); clicking it flashes every copy of that person in the current view (a bronze outline pulse), so you can spot where else they appear. Implemented by delegating on the chart container and matching the d3-bound person id across cards; capture-phase + stopPropagation so a badge click flashes instead of recentering. Frontend only. Honest follow-up: flashing finds copies that are on-screen; a true "fly to an off-screen copy" needs d3-zoom transform work (the chart pans by transform, not scroll) — a later enhancement. Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -381,9 +381,25 @@
|
||||
color: rgb(255, 251, 220);
|
||||
background-color: rgba(255, 251, 220, 0);
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
padding: 2px 4px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
|
||||
}
|
||||
.f3 .f3-card-duplicate-tag:hover {
|
||||
background-color: rgba(255, 251, 220, 0.9);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Click the ×N badge → every copy of that person flashes (see tree/page.tsx). */
|
||||
@keyframes f3-card-flash {
|
||||
0%, 100% { outline-color: rgba(160, 106, 66, 0); }
|
||||
30%, 70% { outline-color: rgba(160, 106, 66, 1); }
|
||||
}
|
||||
.f3 .f3-card-flash .card-inner {
|
||||
outline: 4px solid rgba(160, 106, 66, 1);
|
||||
animation: f3-card-flash 0.55s ease-in-out 3;
|
||||
}
|
||||
|
||||
.f3 .f3-card-duplicate-hover div.card-inner {
|
||||
transform: translate(0, -2px);
|
||||
|
||||
@@ -252,6 +252,36 @@ export default function TreePage() {
|
||||
[mode],
|
||||
);
|
||||
|
||||
// Click the "×N" duplicate badge on a card to flash every copy of that person
|
||||
// in the current view. They're the same record drawn in two places (a shared
|
||||
// ancestor, or an intermarriage). Delegated on the container so it survives
|
||||
// chart rebuilds; capture-phase + stopPropagation so it doesn't also recenter.
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
type Bound = Element & { __data__?: { data?: { id?: string } } };
|
||||
const idOf = (node: Element | null) => (node as Bound | null)?.__data__?.data?.id;
|
||||
function onClick(e: MouseEvent) {
|
||||
const tag = (e.target as HTMLElement).closest?.(".f3-card-duplicate-tag");
|
||||
if (!tag) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const id = idOf(tag.closest(".card_cont"));
|
||||
if (!id) return;
|
||||
el!.querySelectorAll(".card_cont").forEach((cont) => {
|
||||
if (idOf(cont) !== id) return;
|
||||
const card = cont.querySelector(".card") as HTMLElement | null;
|
||||
if (!card) return;
|
||||
card.classList.remove("f3-card-flash");
|
||||
void card.offsetWidth; // restart the animation on repeat clicks
|
||||
card.classList.add("f3-card-flash");
|
||||
window.setTimeout(() => card.classList.remove("f3-card-flash"), 1900);
|
||||
});
|
||||
}
|
||||
el.addEventListener("click", onClick, true);
|
||||
return () => el.removeEventListener("click", onClick, true);
|
||||
}, []);
|
||||
|
||||
// Mirror the focused person into the URL (?focus=…) so navigating away and
|
||||
// back — or sharing the link — keeps the tree centered where you left it.
|
||||
// `replace` (not push) so each recenter doesn't pile up in browser history.
|
||||
@@ -402,11 +432,46 @@ export default function TreePage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className="text-sm text-[var(--muted)]">
|
||||
{mode === "fan"
|
||||
? "Click an ancestor to recenter the fan."
|
||||
: "Drag to pan · scroll to zoom · click a person to recenter."}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[var(--muted)]">
|
||||
<span>
|
||||
{mode === "fan"
|
||||
? "Click an ancestor to recenter the fan."
|
||||
: "Drag to pan · scroll to zoom · click a person to recenter."}
|
||||
</span>
|
||||
{mode !== "fan" && (
|
||||
<div className="group relative">
|
||||
<button
|
||||
type="button"
|
||||
className="underline decoration-dotted underline-offset-2 hover:text-bronze focus-visible:text-bronze focus-visible:outline-none"
|
||||
>
|
||||
Legend
|
||||
</button>
|
||||
<div className="invisible absolute bottom-full left-0 z-30 mb-2 w-80 rounded-lg border border-[var(--border)] bg-[var(--surface)] p-3 text-xs text-[var(--foreground)] opacity-0 shadow-lg transition-opacity group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100">
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<span className="font-semibold text-bronze">×N</span> on a card — this
|
||||
person appears N times in the current view. The same record is drawn in
|
||||
two places because they connect through more than one line (a shared
|
||||
ancestor, or an intermarriage).{" "}
|
||||
<span className="text-[var(--muted)]">Click the ×N to flash the other copies.</span>
|
||||
</li>
|
||||
<li className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-sm" style={{ background: "rgb(120,159,172)" }} /> male
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-sm" style={{ background: "rgb(196,138,146)" }} /> female
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-sm" style={{ background: "lightgray" }} /> sex not set
|
||||
</span>
|
||||
</li>
|
||||
<li>Drag to pan, scroll to zoom, and click any card to recenter the tree on that person.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user