mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-15 23:31:05 +00:00
Fix CI test import, popover pan repositioning, and renderer improvements
- Change bun:test import to vitest for CI compatibility - Add viewportVersion counter for popover recalculation during pan - Add hex color guard in lightenColor for non-6-digit formats - Optimize version edge filtering with batch property - Simplify stroke style conditions in drawDocumentNode
This commit is contained in:
parent
c81c05c14f
commit
cfac6e8898
4 changed files with 38 additions and 22 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { describe, expect, test } from "vitest"
|
||||
import { lightenColor } from "../canvas/renderer"
|
||||
|
||||
describe("lightenColor", () => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ export interface RenderState {
|
|||
const edgeBatches = new Map<string, PreparedEdge[]>()
|
||||
|
||||
// Cache for lightenColor results to avoid per-frame hex parsing
|
||||
let _lightenCache: { input: string; amount: number; result: string } | null = null
|
||||
let _lightenCache: { input: string; amount: number; result: string } | null =
|
||||
null
|
||||
|
||||
export function renderFrame(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
|
|
@ -522,7 +523,12 @@ function drawDocumentNode(
|
|||
}
|
||||
|
||||
// Subtle gradient fill for document nodes
|
||||
const grad = ctx.createLinearGradient(sx - half, sy - half, sx + half, sy + half)
|
||||
const grad = ctx.createLinearGradient(
|
||||
sx - half,
|
||||
sy - half,
|
||||
sx + half,
|
||||
sy + half,
|
||||
)
|
||||
grad.addColorStop(0, colors.docFill)
|
||||
grad.addColorStop(1, lightenColor(colors.docFill, 0.08))
|
||||
ctx.fillStyle = grad
|
||||
|
|
@ -856,15 +862,28 @@ function drawDocOutline(
|
|||
|
||||
/** Lighten a 6-digit hex color by a fraction (0-1). Cached to avoid per-frame parsing. */
|
||||
export function lightenColor(hex: string, amount: number): string {
|
||||
if (_lightenCache && _lightenCache.input === hex && _lightenCache.amount === amount) {
|
||||
if (
|
||||
_lightenCache &&
|
||||
_lightenCache.input === hex &&
|
||||
_lightenCache.amount === amount
|
||||
) {
|
||||
return _lightenCache.result
|
||||
}
|
||||
const h = hex.replace("#", "")
|
||||
// Only handle standard 6-digit hex; return input unchanged for other formats
|
||||
if (h.length !== 6) return hex
|
||||
const r = Math.min(255, Number.parseInt(h.substring(0, 2), 16) + Math.round(255 * amount))
|
||||
const g = Math.min(255, Number.parseInt(h.substring(2, 4), 16) + Math.round(255 * amount))
|
||||
const b = Math.min(255, Number.parseInt(h.substring(4, 6), 16) + Math.round(255 * amount))
|
||||
const r = Math.min(
|
||||
255,
|
||||
Number.parseInt(h.substring(0, 2), 16) + Math.round(255 * amount),
|
||||
)
|
||||
const g = Math.min(
|
||||
255,
|
||||
Number.parseInt(h.substring(2, 4), 16) + Math.round(255 * amount),
|
||||
)
|
||||
const b = Math.min(
|
||||
255,
|
||||
Number.parseInt(h.substring(4, 6), 16) + Math.round(255 * amount),
|
||||
)
|
||||
const result = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`
|
||||
_lightenCache = { input: hex, amount, result }
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ export const GraphCanvas = memo<ExtendedGraphCanvasProps>(function GraphCanvas({
|
|||
}, [selectedNodeId])
|
||||
|
||||
// Create viewport + input handler (once per variant)
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: canvasRef and externalViewportRef are refs — mutations do not trigger re-renders, intentionally omitted
|
||||
useLayoutEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
|
@ -154,7 +155,7 @@ export const GraphCanvas = memo<ExtendedGraphCanvasProps>(function GraphCanvas({
|
|||
renderNeeded.current = true
|
||||
},
|
||||
onNodeClick: (id) => cb.current.onNodeClick(id),
|
||||
onNodeDragStart: (id, node) => {
|
||||
onNodeDragStart: (id, _node) => {
|
||||
cb.current.onNodeDragStart(id)
|
||||
cb.current.simulation?.reheat()
|
||||
},
|
||||
|
|
@ -175,6 +176,7 @@ export const GraphCanvas = memo<ExtendedGraphCanvasProps>(function GraphCanvas({
|
|||
const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1
|
||||
dprRef.current = dpr
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: canvasRef is a ref — mutations do not trigger re-renders, intentionally omitted
|
||||
useLayoutEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas || width === 0 || height === 0) return
|
||||
|
|
@ -196,6 +198,7 @@ export const GraphCanvas = memo<ExtendedGraphCanvasProps>(function GraphCanvas({
|
|||
}, [width, height, dpr])
|
||||
|
||||
// Single render loop — runs for component lifetime, reads everything from refs
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally empty deps — all state read via refs inside tick(), not reactive props
|
||||
useEffect(() => {
|
||||
let lastReportedZoom = 0
|
||||
|
||||
|
|
@ -244,13 +247,8 @@ export const GraphCanvas = memo<ExtendedGraphCanvasProps>(function GraphCanvas({
|
|||
return
|
||||
renderNeeded.current = false
|
||||
|
||||
// Throttled zoom reporting for NavigationControls
|
||||
if (
|
||||
vpMoving &&
|
||||
cb.current.onViewportChange &&
|
||||
Math.abs(vp.zoom - lastReportedZoom) > 0.005
|
||||
) {
|
||||
lastReportedZoom = vp.zoom
|
||||
// Report viewport changes (zoom + pan) so popover positions update
|
||||
if (vpMoving && cb.current.onViewportChange) {
|
||||
cb.current.onViewportChange(vp.zoom)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,7 @@ import { VersionChainIndex } from "../canvas/version-chain"
|
|||
import type { ViewportState } from "../canvas/viewport"
|
||||
import { useGraphData } from "../hooks/use-graph-data"
|
||||
import { useGraphTheme } from "../hooks/use-graph-theme"
|
||||
import type {
|
||||
GraphApiDocument,
|
||||
GraphApiEdge,
|
||||
GraphNode,
|
||||
GraphThemeColors,
|
||||
MemoryGraphProps,
|
||||
} from "../types"
|
||||
import type { GraphThemeColors, MemoryGraphProps } from "../types"
|
||||
import { GraphCanvas } from "./graph-canvas"
|
||||
import { Legend } from "./legend"
|
||||
import { LoadingIndicator } from "./loading-indicator"
|
||||
|
|
@ -52,6 +46,9 @@ export function MemoryGraph({
|
|||
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
|
||||
const [selectedNode, setSelectedNode] = useState<string | null>(null)
|
||||
const [zoomDisplay, setZoomDisplay] = useState(50)
|
||||
// Monotonic counter that increments on any viewport change (pan or zoom)
|
||||
// Used as a dependency proxy to recalculate popover positions
|
||||
const [viewportVersion, setViewportVersion] = useState(0)
|
||||
|
||||
// Limit documents if maxNodes is set
|
||||
const limitedDocuments = useMemo(() => {
|
||||
|
|
@ -175,6 +172,7 @@ export function MemoryGraph({
|
|||
|
||||
const handleViewportChange = useCallback((zoom: number) => {
|
||||
setZoomDisplay(Math.round(zoom * 100))
|
||||
setViewportVersion((v) => v + 1)
|
||||
}, [])
|
||||
|
||||
// Navigation
|
||||
|
|
@ -455,6 +453,7 @@ export function MemoryGraph({
|
|||
return nodes.find((n) => n.id === activeNodeId) ?? null
|
||||
}, [activeNodeId, nodes])
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: zoomDisplay intentionally used as proxy for viewport state changes
|
||||
const activePopoverPosition = useMemo(() => {
|
||||
if (!activeNodeData || !viewportRef.current) return null
|
||||
const vp = viewportRef.current
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue