/** * Component Picker Modal * * Modal interface for searching and selecting components from the wokwi-elements library. * Features: * - Search bar with real-time filtering * - Category tabs for filtering * - Grid layout with component thumbnails * - Click to select and add component */ import React, { useState, useEffect, useMemo } from 'react'; import { ComponentRegistry } from '../services/ComponentRegistry'; import type { ComponentMetadata, ComponentCategory } from '../types/component-metadata'; import './ComponentPickerModal.css'; interface ComponentPickerModalProps { isOpen: boolean; onClose: () => void; onSelectComponent: (metadata: ComponentMetadata) => void; } export const ComponentPickerModal: React.FC = ({ isOpen, onClose, onSelectComponent, }) => { const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState('all'); const [registry] = useState(() => ComponentRegistry.getInstance()); const [isLoading, setIsLoading] = useState(true); // Wait for registry to load useEffect(() => { const loadRegistry = async () => { await registry.load(); setIsLoading(false); }; loadRegistry(); }, [registry]); // Filter components based on search and category const filteredComponents = useMemo(() => { if (isLoading) return []; let components = searchQuery ? registry.search(searchQuery) : registry.getAllComponents(); if (selectedCategory !== 'all') { components = components.filter(c => c.category === selectedCategory); } return components; }, [searchQuery, selectedCategory, registry, isLoading]); // Get available categories const categories = useMemo(() => { if (isLoading) return []; return registry.getCategories(); }, [registry, isLoading]); // Handle ESC key to close modal useEffect(() => { const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; if (isOpen) { window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); } }, [isOpen, onClose]); if (!isOpen) return null; return (
e.stopPropagation()}> {/* Header */}

Add Component

{/* Search Bar */}
🔍 setSearchQuery(e.target.value)} autoFocus /> {searchQuery && ( )}
{/* Category Tabs */}
{categories.map((category) => ( ))}
{/* Components Grid */}
{isLoading ? (

Loading components...

) : filteredComponents.length === 0 ? (

No components found

{searchQuery && ( )}
) : ( filteredComponents.map((component) => ( onSelectComponent(component)} /> )) )}
{/* Footer Info */}
{filteredComponents.length} component{filteredComponents.length !== 1 ? 's' : ''} available
); }; /** * Component Card - Individual component display in the grid */ interface ComponentCardProps { component: ComponentMetadata; onSelect: () => void; } const ComponentCard: React.FC = ({ component, onSelect }) => { const thumbnailRef = React.useRef(null); // Render actual web component as thumbnail React.useEffect(() => { if (!thumbnailRef.current) return; // Create the actual wokwi element const element = document.createElement(component.tagName); // Scale factors for different component types let scale = 0.5; if (component.tagName.includes('arduino') || component.tagName.includes('esp32')) { scale = 0.35; // Boards are larger, scale them down more } else if (component.tagName.includes('lcd') || component.tagName.includes('display')) { scale = 0.4; // Displays need a bit more space } (element as HTMLElement).style.transform = `scale(${scale})`; (element as HTMLElement).style.transformOrigin = 'center center'; // Set default properties for better preview appearance if (component.tagName === 'wokwi-led') { (element as any).value = true; // Turn on LED (element as any).color = component.defaultValues?.color || 'red'; } else if (component.tagName === 'wokwi-rgb-led') { (element as any).red = true; (element as any).green = true; (element as any).blue = true; } else if (component.tagName === 'wokwi-pushbutton') { (element as any).color = component.defaultValues?.color || 'red'; } else if (component.tagName === 'wokwi-lcd1602' || component.tagName === 'wokwi-lcd2004') { (element as any).text = 'Hello World!'; } thumbnailRef.current.innerHTML = ''; thumbnailRef.current.appendChild(element); return () => { if (thumbnailRef.current) { thumbnailRef.current.innerHTML = ''; } }; }, [component.tagName, component.defaultValues]); return ( ); };