added dimming to bg when popover is open.

This commit is contained in:
Vidya Rupak 2025-12-22 14:02:10 -07:00
parent 696f5bbffa
commit ac8fad9038
5 changed files with 118 additions and 7 deletions

View file

@ -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

View file

@ -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<GraphCanvasProps>(
draggingNodeId,
highlightDocumentIds,
isSimulationActive = false,
selectedNodeId = null,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const animationRef = useRef<number>(0)
const startTimeRef = useRef<number>(Date.now())
const mousePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
const currentHoveredNode = useRef<string | null>(null)
const dimProgress = useRef<number>(selectedNodeId ? 1 : 0)
const dimAnimationRef = useRef<number>(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<GraphCanvasProps>(
}
}
// 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<GraphCanvasProps>(
// 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<GraphCanvasProps>(
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<GraphCanvasProps>(
: 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<GraphCanvasProps>(
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

View file

@ -617,6 +617,7 @@ export const MemoryGraph = ({
panY={panY}
width={containerSize.width}
zoom={zoom}
selectedNodeId={selectedNode}
/>
)}

View file

@ -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<NodePopoverProps>(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<NodePopoverProps>(function NodePopover({
inset: 0,
zIndex: 999,
pointerEvents: "auto",
backgroundColor: "transparent",
}}
/>

View file

@ -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 {