From 5bbcc07e93c189a13c426f5d00e47f655b1b2b9e Mon Sep 17 00:00:00 2001 From: abhay-codes07 Date: Sun, 12 Jul 2026 23:26:12 +0530 Subject: [PATCH] fix(memory-graph): make popover quadrant fallback pick the side with most space pickBestQuadrant flattened each side's available space to -1 when the popover did not fully fit, then sorted those values for the fallback. Once every side was -1 the sort was meaningless and the fallback always returned right, even for a node hugging the right edge of the container where the left side has ~45x more room. The clamped popover then slides over the node it is annotating. Keep the raw space measurements alongside the required size so the preference pass still checks fit, while the fallback can genuinely rank sides by available space. Also export the helper and cover the preference order, the exact-fit boundary, and the fallback ranking with unit tests. --- .../src/__tests__/popover-quadrant.test.ts | 90 +++++++++++++++++++ .../src/components/node-hover-popover.tsx | 28 +++--- 2 files changed, 103 insertions(+), 15 deletions(-) create mode 100644 packages/memory-graph/src/__tests__/popover-quadrant.test.ts diff --git a/packages/memory-graph/src/__tests__/popover-quadrant.test.ts b/packages/memory-graph/src/__tests__/popover-quadrant.test.ts new file mode 100644 index 00000000..9f4acc58 --- /dev/null +++ b/packages/memory-graph/src/__tests__/popover-quadrant.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "vitest" +import { pickBestQuadrant } from "../components/node-hover-popover" + +/** + * pickBestQuadrant(screenX, screenY, nodeRadius, containerWidth, + * containerHeight, popoverWidth, popoverHeight) + * + * gap = 24 in the implementation, so available space per side is: + * right: containerWidth - (screenX + nodeRadius + 24) + * left: screenX - nodeRadius - 24 + * above: screenY - nodeRadius - 24 + * below: containerHeight - (screenY + nodeRadius + 24) + */ +describe("pickBestQuadrant", () => { + const RADIUS = 10 + const POPOVER_W = 476 // CARD_W(280) + 12 + SHORTCUTS_W(160) + GAP(24) + const POPOVER_H = 230 + + it("prefers right when there is room on the right", () => { + expect( + pickBestQuadrant(100, 300, RADIUS, 1200, 600, POPOVER_W, POPOVER_H), + ).toBe("right") + }) + + it("falls back to left when only the left side fits", () => { + // right space: 1200 - (1100 + 10 + 24) = 66 -> no fit + // left space: 1100 - 10 - 24 = 1066 -> fits + expect( + pickBestQuadrant(1100, 40, RADIUS, 1200, 260, POPOVER_W, POPOVER_H), + ).toBe("left") + }) + + it("falls back to below when neither horizontal side fits", () => { + // container is narrow (500 wide) so neither left nor right fits 476, + // but there is 600 - (100 + 10 + 24) = 466 below -> fits 230 + expect( + pickBestQuadrant(250, 100, RADIUS, 500, 600, POPOVER_W, POPOVER_H), + ).toBe("below") + }) + + it("uses above when it is the only side that fits", () => { + // node near the bottom of a narrow container + // above space: 500 - 10 - 24 = 466 -> fits 230 + // below space: 600 - (500 + 10 + 24) = 66 -> no fit + expect( + pickBestQuadrant(250, 500, RADIUS, 500, 600, POPOVER_W, POPOVER_H), + ).toBe("above") + }) + + it("treats exactly-enough space as a fit", () => { + // right space: 800 - (280 + 10 + 24) = 486 -> not 476? use exact math + // choose screenX so right space === POPOVER_W: screenX = 800 - 24 - 10 - 476 = 290 + expect( + pickBestQuadrant(290, 300, RADIUS, 800, 600, POPOVER_W, POPOVER_H), + ).toBe("right") + }) + + describe("when no quadrant fully fits", () => { + it("picks the side with the most available space instead of defaulting to right", () => { + // container 800x300, node hugging the right edge at (750, 150) + // right: 800 - (750 + 10 + 24) = 16 + // left: 750 - 10 - 24 = 716 <- most space, but < 800 popover width + // above: 150 - 10 - 24 = 116 + // below: 300 - (150 + 10 + 24) = 116 + expect(pickBestQuadrant(750, 150, RADIUS, 800, 300, 800, 400)).toBe( + "left", + ) + }) + + it("picks below when the node hugs the top of a short container", () => { + // container 400x500, node at (200, 60) + // right: 400 - (200 + 10 + 24) = 166 + // left: 200 - 10 - 24 = 166 + // above: 60 - 10 - 24 = 26 + // below: 500 - (60 + 10 + 24) = 406 <- most space + expect(pickBestQuadrant(200, 60, RADIUS, 400, 500, 600, 600)).toBe( + "below", + ) + }) + + it("picks above when the node hugs the bottom of a short container", () => { + // container 400x500, node at (200, 440) + // above: 440 - 10 - 24 = 406 <- most space + // below: 500 - (440 + 10 + 24) = 26 + expect(pickBestQuadrant(200, 440, RADIUS, 400, 500, 600, 600)).toBe( + "above", + ) + }) + }) +}) diff --git a/packages/memory-graph/src/components/node-hover-popover.tsx b/packages/memory-graph/src/components/node-hover-popover.tsx index dad01fa8..404b7b23 100644 --- a/packages/memory-graph/src/components/node-hover-popover.tsx +++ b/packages/memory-graph/src/components/node-hover-popover.tsx @@ -190,9 +190,9 @@ function CopyableId({ ) } -type Quadrant = "right" | "left" | "above" | "below" +export type Quadrant = "right" | "left" | "above" | "below" -function pickBestQuadrant( +export function pickBestQuadrant( screenX: number, screenY: number, nodeRadius: number, @@ -202,25 +202,23 @@ function pickBestQuadrant( popoverHeight: number, ): Quadrant { const gap = 24 - const spaceRight = containerWidth - (screenX + nodeRadius + gap) - const spaceLeft = screenX - nodeRadius - gap - const spaceAbove = screenY - nodeRadius - gap - const spaceBelow = containerHeight - (screenY + nodeRadius + gap) - - const fits: [Quadrant, number][] = [ - ["right", spaceRight >= popoverWidth ? spaceRight : -1], - ["left", spaceLeft >= popoverWidth ? spaceLeft : -1], - ["above", spaceAbove >= popoverHeight ? spaceAbove : -1], - ["below", spaceBelow >= popoverHeight ? spaceBelow : -1], + // [quadrant, available space, space required for the popover to fit] + const candidates: [Quadrant, number, number][] = [ + ["right", containerWidth - (screenX + nodeRadius + gap), popoverWidth], + ["left", screenX - nodeRadius - gap, popoverWidth], + ["above", screenY - nodeRadius - gap, popoverHeight], + ["below", containerHeight - (screenY + nodeRadius + gap), popoverHeight], ] const preferred: Quadrant[] = ["right", "left", "below", "above"] for (const q of preferred) { - const entry = fits.find(([dir]) => dir === q) - if (entry && entry[1] > 0) return q + const entry = candidates.find(([dir]) => dir === q) + if (entry && entry[1] >= entry[2]) return q } - return fits.sort((a, b) => b[1] - a[1])[0]?.[0] ?? "right" + // Nothing fits fully — fall back to the side with the most room so the + // clamped popover overlaps the node as little as possible. + return candidates.sort((a, b) => b[1] - a[1])[0]?.[0] ?? "right" } function truncate(s: string, max: number) {