2026-03-04 05:30:25 +07:00
|
|
|
/**
|
|
|
|
|
* 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<string, ComponentMetadata> = new Map();
|
|
|
|
|
private categories: Map<ComponentCategory, ComponentMetadata[]> = 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<void> {
|
|
|
|
|
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;
|
|
|
|
|
|
2026-03-05 08:05:23 +07:00
|
|
|
console.log(`Loaded ${this.allComponents.length} components from metadata`);
|
2026-03-04 05:30:25 +07:00
|
|
|
} catch (error) {
|
2026-03-05 08:05:23 +07:00
|
|
|
console.error('Failed to load component metadata:', error);
|
2026-03-04 05:30:25 +07:00
|
|
|
// 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<void> {
|
|
|
|
|
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<ComponentCategory, string> = {
|
2026-03-04 06:42:17 +07:00
|
|
|
boards: 'Boards',
|
|
|
|
|
sensors: 'Sensors',
|
|
|
|
|
displays: 'Displays',
|
|
|
|
|
input: 'Input',
|
|
|
|
|
output: 'Output',
|
|
|
|
|
motors: 'Motors',
|
|
|
|
|
communication: 'Communication',
|
|
|
|
|
passive: 'Passive',
|
|
|
|
|
other: 'Other',
|
2026-03-04 05:30:25 +07:00
|
|
|
};
|
|
|
|
|
return displayNames[category] || category;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auto-load on module import
|
|
|
|
|
const registry = ComponentRegistry.getInstance();
|
|
|
|
|
registry.load();
|
|
|
|
|
|
|
|
|
|
export default registry;
|