mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-22 05:26:17 +00:00
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.
This commit is contained in:
parent
2cebe81512
commit
5bbcc07e93
2 changed files with 103 additions and 15 deletions
90
packages/memory-graph/src/__tests__/popover-quadrant.test.ts
Normal file
90
packages/memory-graph/src/__tests__/popover-quadrant.test.ts
Normal file
|
|
@ -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",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue