/** * Contextual zoom: when zoomed out past a threshold, nodes render as compact * icon-only views using each type's menuIcon from the node registry. * @see https://reactflow.dev/examples/interaction/contextual-zoom */ import React from 'react' import { useViewport } from '@xyflow/react' import { getNodeType, getDefaultStyle } from '@/lib/graph/nodeRegistry' import { InputHandle, OutputHandle } from '@/components/graph/NodeHandles' import { cn } from '@/lib/utils' const MIN_ZOOM = 0.1 const MAX_ZOOM = 2 /** When zoom <= this value, show compact icon view (bottom ~30% of zoom range). */ export const CONTEXTUAL_ZOOM_THRESHOLD = MIN_ZOOM + 0.1 * (MAX_ZOOM - MIN_ZOOM) /** Handle id per type for compact view (input, output). */ const COMPACT_HANDLES: Record = { config: { input: 'ain', output: 'out' }, render: { input: 'ain' }, variable: { output: 'out' }, function: { input: 'in', output: 'out' }, data: { output: 'out' }, } type NodeProps = { id: string type?: string data?: Record selected?: boolean width?: number height?: number [key: string]: unknown } function CompactNodeView({ id, type, selected, width, height }: NodeProps) { const descriptor = type ? getNodeType(type) : undefined const icon = descriptor?.menuIcon ?? null const handles = type ? COMPACT_HANDLES[type] ?? { output: 'out' } : {} const defaultStyle = type ? getDefaultStyle(type) : { width: 320, height: 320 } const w = width ?? defaultStyle.width const h = height ?? defaultStyle.height return (
{handles.input && ( )}
{icon} {id}
{handles.output && ( )}
) } /** * Wraps a node component so that when the viewport zoom is at or below * CONTEXTUAL_ZOOM_THRESHOLD, the node renders as a compact icon-only view. * Inner is always mounted (hidden when compact) so switching zoom does not * remount and re-trigger effects (e.g. RenderingNode fetch). */ export function createContextualNode

( Inner: React.ComponentType

): React.ComponentType

{ function ContextualZoomNode(props: P) { const { zoom } = useViewport() const showCompact = zoom <= CONTEXTUAL_ZOOM_THRESHOLD const p = props as NodeProps const type = p.type const defaultStyle = type ? getDefaultStyle(type) : { width: 320, height: 320 } const w = p.width ?? defaultStyle.width const h = p.height ?? defaultStyle.height return (

{showCompact && ( )}
) } ContextualZoomNode.displayName = `ContextualZoom(${Inner.displayName ?? Inner.name ?? 'Node'})` return ContextualZoomNode as React.ComponentType

}