Spaces:
Running
Running
| 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<string, BlockDefinition[]>; | |
| blocks: BlockDefinition[]; | |
| } | |
| const CATEGORY_LABELS: Record<string, string> = { | |
| 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<BlockShape, string> = { | |
| 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 ( | |
| <div | |
| className={`palette-block-compact ${shapeClass}`} | |
| style={{ '--block-color': color, backgroundColor: color } as React.CSSProperties} | |
| title={block.description} | |
| > | |
| {block.icon && <span>{block.icon}</span>} | |
| <span style={{ opacity: 0.9 }}>{block.label}</span> | |
| {f && <span className="palette-input">{renderMiniInput(f, f.default)}</span>} | |
| </div> | |
| ); | |
| } | |
| // For hat, show label + 1 input | |
| if (shape === 'hat') { | |
| const f = cfg[0]; | |
| return ( | |
| <div | |
| className={`palette-block-compact ${shapeClass}`} | |
| style={{ '--block-color': color, backgroundColor: color } as React.CSSProperties} | |
| title={block.description} | |
| > | |
| {block.icon && <span>{block.icon}</span>} | |
| <span>{block.label}</span> | |
| {f && <span className="palette-input">{renderMiniInput(f, f.default)}</span>} | |
| </div> | |
| ); | |
| } | |
| // For stack/hat/c-block/cap: show label + 1-2 inputs | |
| const inputs = cfg.slice(0, 2); | |
| return ( | |
| <div | |
| className={`palette-block-compact ${shapeClass}`} | |
| style={{ '--block-color': color, backgroundColor: color } as React.CSSProperties} | |
| title={block.description} | |
| > | |
| {block.icon && <span>{block.icon}</span>} | |
| <span>{block.label}</span> | |
| {inputs.map((f, i) => ( | |
| <span key={i} className="palette-input">{renderMiniInput(f, f.default)}</span> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| export default function BlockPalette({ | |
| categories, | |
| activeCategory, | |
| onCategoryChange, | |
| blocks, | |
| }: BlockPaletteProps) { | |
| return ( | |
| <div className="flex h-full"> | |
| {/* Category Tabs (vertical strip on left) - Scratch-style colored circles */} | |
| <div className="palette-tabs"> | |
| {categories.map((cat) => { | |
| const color = CATEGORY_COLORS[cat] || '#6366f1'; | |
| const icon = CATEGORY_ICONS[cat] || '■'; | |
| const isActive = activeCategory === cat; | |
| return ( | |
| <button | |
| key={cat} | |
| onClick={() => onCategoryChange(cat)} | |
| className={`palette-tab ${isActive ? 'palette-tab-active' : ''}`} | |
| style={{ | |
| backgroundColor: color, | |
| color: '#fff', | |
| '--cat-color': color, | |
| } as React.CSSProperties} | |
| title={CATEGORY_LABELS[cat] || cat} | |
| > | |
| <span>{icon}</span> | |
| <span className="palette-tab-label">{CATEGORY_LABELS[cat] || cat}</span> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| {/* Block List - shows actual block shapes */} | |
| <div className="palette-block-list"> | |
| {activeCategory && ( | |
| <div | |
| className="palette-category-header" | |
| style={{ color: CATEGORY_COLORS[activeCategory] || '#6366f1' }} | |
| > | |
| {CATEGORY_LABELS[activeCategory] || activeCategory} | |
| </div> | |
| )} | |
| {blocks.map((block) => ( | |
| <BlockCard key={block.id} block={block} /> | |
| ))} | |
| {blocks.length === 0 && ( | |
| <div className="text-surface-500 text-xs text-center py-8"> | |
| No blocks in this category | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| 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 ( | |
| <div | |
| draggable | |
| onDragStart={handleDragStart} | |
| className="palette-block-item" | |
| > | |
| <MiniBlock block={block} /> | |
| </div> | |
| ); | |
| } | |