Tree: Legend by the pan/zoom hint + clickable ×N duplicate badges #248
@@ -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