From ac8fad9038643587813d2e6d6a0200ff46107388 Mon Sep 17 00:00:00 2001 From: Vidya Rupak Date: Mon, 22 Dec 2025 14:02:10 -0700 Subject: [PATCH] added dimming to bg when popover is open. --- packages/memory-graph/CHANGELOG.md | 44 +++++++++++++ .../src/components/graph-canvas.tsx | 63 +++++++++++++++++-- .../src/components/memory-graph.tsx | 1 + .../src/components/node-popover.tsx | 15 ++++- packages/memory-graph/src/types.ts | 2 + 5 files changed, 118 insertions(+), 7 deletions(-) diff --git a/packages/memory-graph/CHANGELOG.md b/packages/memory-graph/CHANGELOG.md index 20ed454a..f2902a01 100644 --- a/packages/memory-graph/CHANGELOG.md +++ b/packages/memory-graph/CHANGELOG.md @@ -1,5 +1,49 @@ # Memory Graph Changes +> **Testing Playground:** To test changes, run these 2 commands in separate terminals: +> +> **Terminal 1** - Build memory-graph in watch mode: +> ```bash +> cd packages/memory-graph && bun run dev +> ``` +> +> **Terminal 2** - Run the playground: +> ```bash +> cd apps/memory-graph-playground && bun run dev +> ``` +> +> Then open http://localhost:3000 in your browser. + +## Visual & Layout Improvements (2025-12-22) + +### Background Dimming When Popover is Open +**Feature:** When a popover is opened for a doc/memory node, the background dims while the selected node and popover remain in full focus. + +**Implementation:** +- Smooth animated dimming: 200ms ease-out cubic transition +- Canvas-based dimming: non-selected nodes reduced to 20% opacity +- Edges not connected to selected node reduced to 10% opacity +- Selected node remains at full opacity (1.0) for clear focus +- Transparent backdrop for click-outside-to-close functionality +- Escape key handler to close popover +- Popover positioned at z-index 1000 + +**User Experience:** +- Smooth, polished transition when opening/closing popovers +- Creates clear visual hierarchy when inspecting individual nodes +- Selected node stays bright and visible +- Reduces visual noise from surrounding graph elements +- Makes popover content easier to read +- Multiple ways to close: click backdrop, click X button, or press Escape key + +**Files Changed:** +- `src/components/node-popover.tsx:3,20-29` - Escape key handler and transparent backdrop +- `src/components/graph-canvas.tsx:53-54,62-91,289,294,301,306,317,435,454,577` - Smooth animation and dimming logic +- `src/components/memory-graph.tsx:620` - Pass selectedNodeId to canvas +- `src/types.ts:86` - Added selectedNodeId prop to GraphCanvasProps + +--- + ## Visual & Layout Improvements (2025-12-21) ### Smart Positioning Node Popover diff --git a/packages/memory-graph/src/components/graph-canvas.tsx b/packages/memory-graph/src/components/graph-canvas.tsx index 243c82cc..066c0d0b 100644 --- a/packages/memory-graph/src/components/graph-canvas.tsx +++ b/packages/memory-graph/src/components/graph-canvas.tsx @@ -7,6 +7,7 @@ import { useLayoutEffect, useMemo, useRef, + useState, } from "react" import { colors } from "@/constants" import type { @@ -43,18 +44,57 @@ export const GraphCanvas = memo( draggingNodeId, highlightDocumentIds, isSimulationActive = false, + selectedNodeId = null, }) => { const canvasRef = useRef(null) const animationRef = useRef(0) const startTimeRef = useRef(Date.now()) const mousePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 }) const currentHoveredNode = useRef(null) + const dimProgress = useRef(selectedNodeId ? 1 : 0) + const dimAnimationRef = useRef(0) + const [, forceRender] = useState(0) // Initialize start time once useEffect(() => { startTimeRef.current = Date.now() }, []) + // Smooth dimming animation + useEffect(() => { + const targetDim = selectedNodeId ? 1 : 0 + const duration = 200 // milliseconds + const startDim = dimProgress.current + const startTime = Date.now() + + const animate = () => { + const elapsed = Date.now() - startTime + const progress = Math.min(elapsed / duration, 1) + + // Ease-out cubic easing for smooth deceleration + const eased = 1 - Math.pow(1 - progress, 3) + dimProgress.current = startDim + (targetDim - startDim) * eased + + // Force re-render to update canvas during animation + forceRender(prev => prev + 1) + + if (progress < 1) { + dimAnimationRef.current = requestAnimationFrame(animate) + } + } + + if (dimAnimationRef.current) { + cancelAnimationFrame(dimAnimationRef.current) + } + animate() + + return () => { + if (dimAnimationRef.current) { + cancelAnimationFrame(dimAnimationRef.current) + } + } + }, [selectedNodeId]) + // Efficient hit detection const getNodeAtPosition = useCallback( (x: number, y: number): string | null => { @@ -246,22 +286,29 @@ export const GraphCanvas = memo( } } + // Check if edge should be dimmed (not connected to selected node) + const edgeShouldDim = selectedNodeId !== null && + sourceNode.id !== selectedNodeId && + targetNode.id !== selectedNodeId + // Smooth edge opacity: interpolate between full and 0.1 (dimmed) + const edgeDimOpacity = 1 - (dimProgress.current * 0.9) + // Enhanced connection styling based on edge type let connectionColor = colors.connection.weak let dashPattern: number[] = [] - let opacity = edge.visualProps.opacity + let opacity = edgeShouldDim ? edgeDimOpacity : edge.visualProps.opacity let lineWidth = Math.max(1, edge.visualProps.thickness * zoom) if (edge.edgeType === "doc-memory") { // Doc-memory: Solid thin lines, subtle dashPattern = [] connectionColor = colors.connection.memory - opacity = 0.9 + opacity = edgeShouldDim ? edgeDimOpacity : 0.9 lineWidth = 1 } else if (edge.edgeType === "doc-doc") { // Doc-doc: Thick dashed lines with strong similarity emphasis dashPattern = useSimplifiedRendering ? [] : [10, 5] // Solid lines when zoomed out - opacity = Math.max(0, edge.similarity * 0.5) + opacity = edgeShouldDim ? edgeDimOpacity : Math.max(0, edge.similarity * 0.5) lineWidth = Math.max(1, edge.similarity * 2) // Thicker for stronger similarity if (edge.similarity > 0.85) @@ -272,7 +319,7 @@ export const GraphCanvas = memo( // Version chains: Double line effect with relation-specific colors dashPattern = [] connectionColor = edge.color || colors.relations.updates - opacity = 0.8 + opacity = edgeShouldDim ? edgeDimOpacity : 0.8 lineWidth = 2 } @@ -389,6 +436,10 @@ export const GraphCanvas = memo( const isHovered = currentHoveredNode.current === node.id const isDragging = node.isDragging + const isSelected = selectedNodeId === node.id + const shouldDim = selectedNodeId !== null && !isSelected + // Smooth opacity: interpolate between 1 (full) and 0.2 (dimmed) based on animation progress + const nodeOpacity = shouldDim ? 1 - (dimProgress.current * 0.8) : 1 const isHighlightedDocument = (() => { if (node.type !== "document" || highlightSet.size === 0) return false const doc = node.data as DocumentWithMemories @@ -407,7 +458,7 @@ export const GraphCanvas = memo( : isHovered ? colors.document.secondary : colors.document.primary - ctx.globalAlpha = 1 + ctx.globalAlpha = nodeOpacity // Enhanced border with subtle glow ctx.strokeStyle = isDragging @@ -530,7 +581,7 @@ export const GraphCanvas = memo( const radius = nodeSize / 2 ctx.fillStyle = fillColor - ctx.globalAlpha = isLatest ? 1 : 0.4 + ctx.globalAlpha = shouldDim ? nodeOpacity : (isLatest ? 1 : 0.4) ctx.strokeStyle = borderColor ctx.lineWidth = isDragging ? 3 : isHovered ? 2 : 1.5 diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index 98340f4e..70ee5470 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -617,6 +617,7 @@ export const MemoryGraph = ({ panY={panY} width={containerSize.width} zoom={zoom} + selectedNodeId={selectedNode} /> )} diff --git a/packages/memory-graph/src/components/node-popover.tsx b/packages/memory-graph/src/components/node-popover.tsx index abe2e807..2657d10c 100644 --- a/packages/memory-graph/src/components/node-popover.tsx +++ b/packages/memory-graph/src/components/node-popover.tsx @@ -1,6 +1,6 @@ "use client" -import { memo } from "react" +import { memo, useEffect } from "react" import type { GraphNode } from "@/types" export interface NodePopoverProps { @@ -16,6 +16,18 @@ export const NodePopover = memo(function NodePopover({ y, onClose, }) { + // Handle Escape key to close popover + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [onClose]) + return ( <> {/* Invisible backdrop to catch clicks outside */} @@ -26,6 +38,7 @@ export const NodePopover = memo(function NodePopover({ inset: 0, zIndex: 999, pointerEvents: "auto", + backgroundColor: "transparent", }} /> diff --git a/packages/memory-graph/src/types.ts b/packages/memory-graph/src/types.ts index dbdc2e4a..3352d562 100644 --- a/packages/memory-graph/src/types.ts +++ b/packages/memory-graph/src/types.ts @@ -82,6 +82,8 @@ export interface GraphCanvasProps { highlightDocumentIds?: string[] // Physics simulation state isSimulationActive?: boolean + // Selected node ID - dims all other nodes and edges + selectedNodeId?: string | null } export interface MemoryGraphProps {