Files
Atomizer/atomizer-dashboard/frontend/src/components/canvas/palette/NodePalette.tsx
Anto01 a3f18dc377 chore: Project cleanup and Canvas UX improvements (Phase 7-9)
## Cleanup (v0.5.0)
- Delete 102+ orphaned MCP session temp files
- Remove build artifacts (htmlcov, dist, __pycache__)
- Archive superseded plan docs (RALPH_LOOP V2/V3, CANVAS V3, etc.)
- Move debug/analysis scripts from tests/ to tools/analysis/
- Archive redundant NX journals to archive/nx_journals/
- Archive monolithic PROTOCOL.md to docs/archive/
- Update .gitignore with missing patterns
- Clean old study files (optimization_log_old.txt, run_optimization_old.py)

## Canvas UX (Phases 7-9)
- Phase 7: Resizable panels with localStorage persistence
  - Left sidebar: 200-400px, Right panel: 280-600px
  - New useResizablePanel hook and ResizeHandle component
- Phase 8: Enable all palette items
  - All 8 node types now draggable
  - Singleton logic for model/solver/algorithm/surrogate
- Phase 9: Solver configuration
  - Add SolverEngine type (nxnastran, mscnastran, python, etc.)
  - Add NastranSolutionType (SOL101-SOL200)
  - Engine/solution dropdowns in config panel
  - Python script path support

## Documentation
- Update CHANGELOG.md with recent versions
- Update docs/00_INDEX.md
- Create examples/README.md
- Add docs/plans/CANVAS_UX_IMPROVEMENTS.md
2026-01-24 15:17:34 -05:00

259 lines
7.7 KiB
TypeScript

/**
* 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 (
<div className={`w-14 bg-dark-850 border-r border-dark-700 flex flex-col ${className}`}>
{/* Toggle Button */}
{showToggle && onToggleCollapse && (
<button
onClick={onToggleCollapse}
className="p-4 border-b border-dark-700 hover:bg-dark-800 transition-colors flex items-center justify-center"
title="Expand palette"
>
<ChevronRight size={18} className="text-dark-400" />
</button>
)}
{/* Collapsed Items */}
<div className="flex-1 overflow-y-auto py-2">
{items.map((item) => {
const Icon = item.icon;
const isDraggable = item.canAdd;
return (
<div
key={item.type}
draggable={isDraggable}
onDragStart={(e) => onDragStart(e, item)}
className={`p-3 mx-2 my-1 rounded-lg transition-all flex items-center justify-center
${isDraggable
? 'cursor-grab hover:bg-dark-800 active:cursor-grabbing'
: 'cursor-default opacity-50'
}`}
title={`${item.label}${!isDraggable ? ' (auto-created)' : ''}`}
>
<Icon size={18} className={item.color} />
</div>
);
})}
</div>
</div>
);
}
// Expanded mode - full display
return (
<div className={`w-60 bg-dark-850 border-r border-dark-700 flex flex-col ${className}`}>
{/* Header */}
<div className="p-4 border-b border-dark-700 flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold text-dark-300 uppercase tracking-wider">
Components
</h3>
<p className="text-xs text-dark-400 mt-1">
Drag to canvas
</p>
</div>
{showToggle && onToggleCollapse && (
<button
onClick={onToggleCollapse}
className="p-1.5 rounded hover:bg-dark-800 transition-colors"
title="Collapse palette"
>
<ChevronLeft size={16} className="text-dark-400" />
</button>
)}
</div>
{/* Items */}
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{items.map((item) => {
const Icon = item.icon;
const isDraggable = item.canAdd;
return (
<div
key={item.type}
draggable={isDraggable}
onDragStart={(e) => onDragStart(e, item)}
className={`flex items-center gap-3 px-3 py-3 rounded-lg border transition-all group
${isDraggable
? 'bg-dark-800/50 border-dark-700/50 cursor-grab hover:border-primary-500/50 hover:bg-dark-800 active:cursor-grabbing'
: 'bg-dark-900/30 border-dark-800/30 cursor-default'
}`}
title={!isDraggable ? 'Auto-created from study configuration' : undefined}
>
<div className={`${item.color} ${isDraggable ? 'opacity-90 group-hover:opacity-100' : 'opacity-50'} transition-opacity`}>
<Icon size={18} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-semibold text-sm leading-tight ${isDraggable ? 'text-white' : 'text-dark-400'}`}>
{item.label}
</div>
<div className="text-xs text-dark-400 truncate">
{isDraggable ? item.description : 'Auto-created'}
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
export default NodePalette;