import { useCallback } from 'react'; import { BlockDefinition, BlockShape, CATEGORY_COLORS, CATEGORY_ICONS } from '../../types/blocks'; interface BlockPaletteProps { categories: string[]; activeCategory: string | null; onCategoryChange: (category: string) => void; blocksByCategory: Map; blocks: BlockDefinition[]; } const CATEGORY_LABELS: Record = { variables: 'Variables', logic: 'Logic', loops: 'Loops', functions: 'Functions', events: 'Events', dom: 'DOM', css: 'CSS', html: 'HTML', types: 'Types', electron: 'Electron', ipc: 'IPC', dialog: 'Dialogs', xaml: 'XAML', csharp: 'C#', maui: '.NET MAUI', node_fs: 'File System', node_http: 'HTTP', node_express: 'Express', node_db: 'Database', node_async: 'Async', data: 'Data', math: 'Math', text: 'Text', input_output: 'I/O', }; const COMPACT_SHAPE_CLASS: Record = { hat: 'hat', stack: '', boolean: 'boolean', reporter: 'reporter', 'c-block': '', cap: 'cap', }; function truncate(s: string, n: number) { if (typeof s !== 'string') return String(s ?? ''); return s.length > n ? s.slice(0, n) + '…' : s; } function renderMiniInput(field: any, value: any): string { if (value === undefined || value === null || value === '') return field.label; switch (field.type) { case 'boolean': return value ? '✓' : '✗'; case 'select': const opt = field.options?.find((o: any) => o.value === value); return truncate(opt?.label || String(value), 14); case 'textarea': case 'code': return truncate(String(value), 18); default: return truncate(String(value), 18); } } function MiniBlock({ block }: { block: BlockDefinition }) { const color = block.color || CATEGORY_COLORS[block.category] || '#6366f1'; const shape = block.shape || 'stack'; const shapeClass = COMPACT_SHAPE_CLASS[shape]; const cfg = block.config || []; // For reporters/booleans show 1 input if (shape === 'reporter' || shape === 'boolean') { const f = cfg[0]; return (
{block.icon && {block.icon}} {block.label} {f && {renderMiniInput(f, f.default)}}
); } // For hat, show label + 1 input if (shape === 'hat') { const f = cfg[0]; return (
{block.icon && {block.icon}} {block.label} {f && {renderMiniInput(f, f.default)}}
); } // For stack/hat/c-block/cap: show label + 1-2 inputs const inputs = cfg.slice(0, 2); return (
{block.icon && {block.icon}} {block.label} {inputs.map((f, i) => ( {renderMiniInput(f, f.default)} ))}
); } export default function BlockPalette({ categories, activeCategory, onCategoryChange, blocks, }: BlockPaletteProps) { return (
{/* Category Tabs (vertical strip on left) - Scratch-style colored circles */}
{categories.map((cat) => { const color = CATEGORY_COLORS[cat] || '#6366f1'; const icon = CATEGORY_ICONS[cat] || '■'; const isActive = activeCategory === cat; return ( ); })}
{/* Block List - shows actual block shapes */}
{activeCategory && (
{CATEGORY_LABELS[activeCategory] || activeCategory}
)} {blocks.map((block) => ( ))} {blocks.length === 0 && (
No blocks in this category
)}
); } function BlockCard({ block }: { block: BlockDefinition }) { const handleDragStart = useCallback( (event: React.DragEvent) => { event.dataTransfer.setData('application/block', JSON.stringify(block)); event.dataTransfer.effectAllowed = 'move'; }, [block] ); return (
); }