From 7ead021ee2ab8754854fc99e0ff92de650aa1c93 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Mon, 5 Oct 2026 11:23:57 -0400 Subject: [PATCH] docs(docs): fix cursor handling and hover tip refactor in docs UI components --- apps/docs/islands/DependencySpine.tsx | 6 +++++- apps/docs/islands/SolvedRing.tsx | 10 +++++----- apps/docs/islands/SubmissionHeatmap.tsx | 8 ++++---- apps/docs/islands/TopicBubbles.tsx | 8 ++++---- apps/docs/lib/stats.ts | 10 ++++++---- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/apps/docs/islands/DependencySpine.tsx b/apps/docs/islands/DependencySpine.tsx index 7df387d..84e6c43 100644 --- a/apps/docs/islands/DependencySpine.tsx +++ b/apps/docs/islands/DependencySpine.tsx @@ -193,6 +193,8 @@ export default function DependencySpine() { }; const [routes, setRoutes] = useState | null>(null); const drag = useRef<{ x: number; y: number; moved: boolean } | null>(null); + // The cursor reads this, not `drag`: a ref read during render never re-renders. + const [panning, setPanning] = useState(false); useEffect(() => { let live = true; @@ -244,6 +246,7 @@ export default function DependencySpine() { // Capture only once it is a drag: capturing on pointerdown would retarget // pointerup (and so the click) to the , and a node's link would never fire. d.moved = true; + setPanning(true); e.currentTarget.setPointerCapture(e.pointerId); } d.x = e.clientX; @@ -254,6 +257,7 @@ export default function DependencySpine() { }; const onPointerUp = (e: PointerEvent) => { if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId); + setPanning(false); // Cleared on the next tick so a node's click handler can still see `moved`. setTimeout(() => { drag.current = null; }, 0); }; @@ -293,7 +297,7 @@ export default function DependencySpine() { xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="dependency-spine-title dependency-spine-desc" - style={{ display: "block", width: "100%", height: "auto", touchAction: "none", cursor: drag.current?.moved ? "grabbing" : zoom > 1.001 ? "grab" : "default", userSelect: "none" }} + style={{ display: "block", width: "100%", height: "auto", touchAction: "none", cursor: panning ? "grabbing" : zoom > 1.001 ? "grab" : "default", userSelect: "none" }} onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} diff --git a/apps/docs/islands/SolvedRing.tsx b/apps/docs/islands/SolvedRing.tsx index e36ca37..e04000b 100644 --- a/apps/docs/islands/SolvedRing.tsx +++ b/apps/docs/islands/SolvedRing.tsx @@ -57,14 +57,14 @@ export default function SolvedRing() { const done = rows.reduce((n, r) => n + r.done, 0); const span = SWEEP - GAP * (rows.length - 1); + const segments: ((typeof rows)[number] & { from: number; to: number; solvedTo: number })[] = []; let cursor = START; - const segments = rows.map((r) => { + for (const r of rows) { const size = total > 0 ? (span * r.total) / total : 0; - const from = cursor; - cursor -= size + GAP; const solved = r.total > 0 ? size * (r.done / r.total) : 0; - return { ...r, from, to: from - size, solvedTo: from - solved }; - }); + segments.push({ ...r, from: cursor, to: cursor - size, solvedTo: cursor - solved }); + cursor -= size + GAP; + } const focused = focus ? rows.find((r) => r.difficulty === focus) : undefined; const arc = { innerRadius: "80%", outerRadius: "88%", cornerRadius: 99, stroke: "none", dataKey: "value" } as const; diff --git a/apps/docs/islands/SubmissionHeatmap.tsx b/apps/docs/islands/SubmissionHeatmap.tsx index 88a2880..2619e81 100644 --- a/apps/docs/islands/SubmissionHeatmap.tsx +++ b/apps/docs/islands/SubmissionHeatmap.tsx @@ -76,7 +76,7 @@ function layout(year: Day[]): { blocks: Block[]; width: number } { export default function SubmissionHeatmap() { const load = useStats(); - const hover = useHoverTip(); + const { ref, show, hide, node } = useHoverTip(); if (load.phase !== "ready") { return ( @@ -116,7 +116,7 @@ export default function SubmissionHeatmap() { -
+
- hover.show( + show( event, <>
{DAY.format(cell.date)}
@@ -157,7 +157,7 @@ export default function SubmissionHeatmap() { ))} - {hover.node} + {node}
diff --git a/apps/docs/islands/TopicBubbles.tsx b/apps/docs/islands/TopicBubbles.tsx index e12da72..a75f754 100644 --- a/apps/docs/islands/TopicBubbles.tsx +++ b/apps/docs/islands/TopicBubbles.tsx @@ -35,7 +35,7 @@ function lines(name: string): string[] { export default function TopicBubbles() { const load = useStats(); - const hover = useHoverTip(); + const { ref, show, hide, node } = useHoverTip(); const clip = useId().replace(/:/g, ""); if (load.phase !== "ready") { @@ -70,7 +70,7 @@ export default function TopicBubbles() { -
+
- hover.show( + show( event, <>
{c.name}
@@ -131,7 +131,7 @@ export default function TopicBubbles() { ); })} - {hover.node} + {node}
diff --git a/apps/docs/lib/stats.ts b/apps/docs/lib/stats.ts index fd9321e..025a9b0 100644 --- a/apps/docs/lib/stats.ts +++ b/apps/docs/lib/stats.ts @@ -66,10 +66,12 @@ let request: Promise | undefined; export function useStats(): Load { const [load, setLoad] = useState({ phase: "loading" }); useEffect(() => { - request ??= fetch(`${API_BASE}/api/stats`) - .then((res) => (res.ok ? res.json() : null)) - .then((body: unknown) => (isStats(body) ? body : null)) - .catch(() => null); + if (!request) { + request = fetch(`${API_BASE}/api/stats`) + .then((res) => (res.ok ? res.json() : null)) + .then((body: unknown) => (isStats(body) ? body : null)) + .catch(() => null); + } let live = true; request.then((stats) => { if (live) setLoad(stats ? { phase: "ready", stats } : { phase: "error" });