RealBlocks / client /src /components /BlockEditor /BlockPalette.tsx
incognitolm's picture
Fix TS error: cast --block-color CSS var as React.CSSProperties
a5bce32
Raw
History Blame Contribute Delete
5.75 kB
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>
);
}