diff --git a/frontend/app/trees/[id]/tree/chart.css b/frontend/app/trees/[id]/tree/chart.css index 27ed84c..8ed16cf 100644 --- a/frontend/app/trees/[id]/tree/chart.css +++ b/frontend/app/trees/[id]/tree/chart.css @@ -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); diff --git a/frontend/app/trees/[id]/tree/page.tsx b/frontend/app/trees/[id]/tree/page.tsx index d37dc16..083a4f7 100644 --- a/frontend/app/trees/[id]/tree/page.tsx +++ b/frontend/app/trees/[id]/tree/page.tsx @@ -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() { /> )} -
- {mode === "fan" - ? "Click an ancestor to recenter the fan." - : "Drag to pan · scroll to zoom · click a person to recenter."} -
+