/** * NodePalette - Draggable component library for canvas * * Features: * - Draggable node items for canvas drop * - Collapsible mode (icons only) * - Filterable by node type * - Works with both AtomizerCanvas and SpecRenderer */ import { DragEvent } from 'react'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { Box, Cpu, SlidersHorizontal, FlaskConical, Target, ShieldAlert, BrainCircuit, Rocket, LucideIcon, } from 'lucide-react'; import { NodeType } from '../../../lib/canvas/schema'; // ============================================================================ // Types // ============================================================================ export interface PaletteItem { type: NodeType; label: string; icon: LucideIcon; description: string; color: string; /** Whether this can be added via drag-drop (synthetic nodes cannot) */ canAdd: boolean; } export interface NodePaletteProps { /** Whether palette is collapsed (icon-only mode) */ collapsed?: boolean; /** Callback when collapse state changes */ onToggleCollapse?: () => void; /** Custom className for container */ className?: string; /** Filter which node types to show */ visibleTypes?: NodeType[]; /** Show toggle button */ showToggle?: boolean; } // ============================================================================ // Constants // ============================================================================ /** Singleton node types - only one of each allowed on canvas */ export const SINGLETON_TYPES: NodeType[] = ['model', 'solver', 'algorithm', 'surrogate']; export const PALETTE_ITEMS: PaletteItem[] = [ { type: 'model', label: 'Model', icon: Box, description: 'NX model file (.prt, .sim)', color: 'text-blue-400', canAdd: true, // Singleton - only one allowed }, { type: 'solver', label: 'Solver', icon: Cpu, description: 'Analysis solver config', color: 'text-violet-400', canAdd: true, // Singleton - only one allowed }, { type: 'designVar', label: 'Design Variable', icon: SlidersHorizontal, description: 'Parameter to optimize', color: 'text-emerald-400', canAdd: true, }, { type: 'extractor', label: 'Extractor', icon: FlaskConical, description: 'Physics result extraction', color: 'text-cyan-400', canAdd: true, }, { type: 'objective', label: 'Objective', icon: Target, description: 'Optimization goal', color: 'text-rose-400', canAdd: true, }, { type: 'constraint', label: 'Constraint', icon: ShieldAlert, description: 'Design constraint', color: 'text-amber-400', canAdd: true, }, { type: 'algorithm', label: 'Algorithm', icon: BrainCircuit, description: 'Optimization method', color: 'text-indigo-400', canAdd: true, // Singleton - only one allowed }, { type: 'surrogate', label: 'Surrogate', icon: Rocket, description: 'Neural acceleration', color: 'text-pink-400', canAdd: true, // Singleton - only one allowed }, ]; /** Items that can be added via drag-drop */ export const ADDABLE_ITEMS = PALETTE_ITEMS.filter(item => item.canAdd); // ============================================================================ // Component // ============================================================================ export function NodePalette({ collapsed = false, onToggleCollapse, className = '', visibleTypes, showToggle = true, }: NodePaletteProps) { // Filter items if visibleTypes is provided const items = visibleTypes ? PALETTE_ITEMS.filter(item => visibleTypes.includes(item.type)) : PALETTE_ITEMS; const onDragStart = (event: DragEvent, item: PaletteItem) => { if (!item.canAdd) { event.preventDefault(); return; } event.dataTransfer.setData('application/reactflow', item.type); event.dataTransfer.effectAllowed = 'move'; }; // Collapsed mode - icons only if (collapsed) { return (
Drag to canvas