/** * Component Registry * * Singleton service that loads and provides access to component metadata. * Loads from components-metadata.json generated at build time. */ import type { ComponentMetadata, ComponentCategory, ComponentMetadataCollection, } from '../types/component-metadata'; export class ComponentRegistry { private static instance: ComponentRegistry; private metadata: Map = new Map(); private categories: Map = new Map(); private allComponents: ComponentMetadata[] = []; private loaded = false; private constructor() {} /** * Get singleton instance */ static getInstance(): ComponentRegistry { if (!ComponentRegistry.instance) { ComponentRegistry.instance = new ComponentRegistry(); } return ComponentRegistry.instance; } /** * Load metadata from JSON file */ async load(): Promise { if (this.loaded) return; try { const response = await fetch('/components-metadata.json'); if (!response.ok) { throw new Error(`Failed to load metadata: ${response.statusText}`); } const data: ComponentMetadataCollection = await response.json(); this.processMetadata(data.components); this.loaded = true; console.log(`Loaded ${this.allComponents.length} components from metadata`); } catch (error) { console.error('Failed to load component metadata:', error); // Continue with empty registry - app should still work with manual component addition } } /** * Process and index metadata */ private processMetadata(components: ComponentMetadata[]): void { this.allComponents = components; this.metadata.clear(); this.categories.clear(); // Index by ID components.forEach(component => { this.metadata.set(component.id, component); // Group by category const categoryComponents = this.categories.get(component.category) || []; categoryComponents.push(component); this.categories.set(component.category, categoryComponents); }); } /** * Get all components */ getAllComponents(): ComponentMetadata[] { return [...this.allComponents]; } /** * Get components by category */ getByCategory(category: ComponentCategory): ComponentMetadata[] { return this.categories.get(category) || []; } /** * Get component by ID */ getById(id: string): ComponentMetadata | undefined { return this.metadata.get(id); } /** * Search components by query (name, description, tags) */ search(query: string): ComponentMetadata[] { if (!query.trim()) { return this.getAllComponents(); } const lowerQuery = query.toLowerCase(); return this.allComponents.filter(component => { return ( component.name.toLowerCase().includes(lowerQuery) || component.id.toLowerCase().includes(lowerQuery) || component.description?.toLowerCase().includes(lowerQuery) || component.tags.some(tag => tag.toLowerCase().includes(lowerQuery)) ); }); } /** * Get all available categories */ getCategories(): ComponentCategory[] { return Array.from(this.categories.keys()); } /** * Reload metadata (for hot-reload in dev mode) */ async reload(): Promise { this.loaded = false; await this.load(); } /** * Check if registry is loaded */ isLoaded(): boolean { return this.loaded; } /** * Get component count */ getComponentCount(): number { return this.allComponents.length; } /** * Get category display name */ static getCategoryDisplayName(category: ComponentCategory): string { const displayNames: Record = { boards: 'Boards', sensors: 'Sensors', displays: 'Displays', input: 'Input', output: 'Output', motors: 'Motors', communication: 'Communication', passive: 'Passive', other: 'Other', }; return displayNames[category] || category; } } // Auto-load on module import const registry = ComponentRegistry.getInstance(); registry.load(); export default registry;