Tree: Legend by the pan/zoom hint + clickable ×N duplicate badges #248
@@ -381,9 +381,25 @@
|
|||||||
color: rgb(255, 251, 220);
|
color: rgb(255, 251, 220);
|
||||||
background-color: rgba(255, 251, 220, 0);
|
background-color: rgba(255, 251, 220, 0);
|
||||||
border-radius: 50%;
|
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;
|
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 {
|
.f3 .f3-card-duplicate-hover div.card-inner {
|
||||||
transform: translate(0, -2px);
|
transform: translate(0, -2px);
|
||||||
|
|||||||
@@ -252,6 +252,36 @@ export default function TreePage() {
|
|||||||
[mode],
|
[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
|
// 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.
|
// 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.
|
// `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)]">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[var(--muted)]">
|
||||||
{mode === "fan"
|
<span>
|
||||||
? "Click an ancestor to recenter the fan."
|
{mode === "fan"
|
||||||
: "Drag to pan · scroll to zoom · click a person to recenter."}
|
? "Click an ancestor to recenter the fan."
|
||||||
</p>
|
: "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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user