"use client" import { memo, useEffect } from "react" import type { GraphNode } from "@/types" export interface NodePopoverProps { node: GraphNode x: number // Screen X position y: number // Screen Y position onClose: () => void containerBounds?: DOMRect // Optional container bounds to limit backdrop onBackdropClick?: () => void // Optional callback when backdrop is clicked } export const NodePopover = memo(function NodePopover({ node, x, y, onClose, containerBounds, onBackdropClick, }) { // 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]) // Calculate backdrop bounds - use container bounds if provided, otherwise full viewport const backdropStyle = containerBounds ? { position: "fixed" as const, left: `${containerBounds.left}px`, top: `${containerBounds.top}px`, width: `${containerBounds.width}px`, height: `${containerBounds.height}px`, zIndex: 999, pointerEvents: "auto" as const, backgroundColor: "transparent", } : { position: "fixed" as const, inset: 0, zIndex: 999, pointerEvents: "auto" as const, backgroundColor: "transparent", } const handleBackdropClick = () => { onBackdropClick?.() onClose() } return ( <> {/* Invisible backdrop to catch clicks outside */}
{/* Popover content */}
e.stopPropagation()} // Prevent closing when clicking inside style={{ position: "fixed", left: `${x}px`, top: `${y}px`, background: "rgba(255, 255, 255, 0.05)", backdropFilter: "blur(12px)", WebkitBackdropFilter: "blur(12px)", border: "1px solid rgba(255, 255, 255, 0.25)", borderRadius: "12px", padding: "16px", width: "320px", zIndex: 1000, pointerEvents: "auto", boxShadow: "0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3)", }} > {node.type === "document" ? ( // Document popover
{/* Header */}

Document

{/* Sections */}
{/* Title */}
Title

{(node.data as any).title || "Untitled Document"}

{/* Summary - truncated to 2 lines */} {(node.data as any).summary && (
Summary

{(node.data as any).summary}

)} {/* Type */}
Type

{(node.data as any).type || "Document"}

{/* Memory Count */}
Memory Count

{(node.data as any).memoryEntries?.length || 0} memories

{/* URL */} {((node.data as any).url || (node.data as any).customId) && ( )} {/* Footer with metadata */}
{new Date((node.data as any).createdAt).toLocaleDateString()}
{node.id}
) : ( // Memory popover
{/* Header */}

Memory

{/* Sections */}
{/* Memory content */}
Memory

{(node.data as any).memory || (node.data as any).content || "No content"}

{(node.data as any).isForgotten && (
Forgotten
)} {/* Expires (inline with memory if exists) */} {(node.data as any).forgetAfter && (

Expires: {new Date((node.data as any).forgetAfter).toLocaleDateString()} {(node.data as any).forgetReason && ` - ${(node.data as any).forgetReason}`}

)}
{/* Space */}
Space

{(node.data as any).spaceId || "Default"}

{/* Footer with metadata */}
{new Date((node.data as any).createdAt).toLocaleDateString()}
{node.id}
)}
) })