UNPKG

circuit-bricks

Version:

A modular, Lego-style SVG circuit component system for React (ALPHA - Not for production use)

1 lines 151 kB
{"version":3,"file":"index.mjs","names":["initialState?: Partial<CircuitState>","prevState: CircuitState","updater: (prev: CircuitState) => CircuitState","prefix: string","component: Omit<ComponentInstance, 'id'>","id: string","updates: Partial<ComponentInstance>","from: Wire['from']","to: Wire['to']","style?: Wire['style']","updates: Partial<Wire>","addToSelection: boolean","newPosition: Point","dx: number","dy: number","degrees: number","componentId: string","portId: string","toComponentId: string","toPortId: string","fromId: string","fromPortId: string","toId: string","actions: CircuitActions","wire: Wire | null","deps: any[]","componentId: string | null","portId: string | null","HeadlessPropertyPanel: React.FC<HeadlessPropertyPanelProps>","key: string","event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>","value: any","PropertyPanel: React.FC<PropertyPanelProps>","rootStyle: CSSProperties","defaultStyles: Record<string, CSSProperties>","HeadlessPropertyPanel","HeadlessComponentPalette: React.FC<HeadlessComponentPaletteProps>","HeadlessComponentListItem: React.FC<HeadlessComponentListItemProps>","event: React.DragEvent","ComponentPalette: React.FC<ComponentPaletteProps>","rootStyle: CSSProperties","defaultStyles: Record<string, CSSProperties>","HeadlessComponentPalette","HeadlessCircuitToolbar: React.FC<HeadlessCircuitToolbarProps>","CircuitToolbar: React.FC<CircuitToolbarProps>","rootStyle: CSSProperties","defaultStyles: Record<string, CSSProperties>","HeadlessCircuitToolbar","SimpleCircuitExample: React.FC","id: string","key: string","value: any","componentType: string","action: string","CircuitToolbar","ComponentPalette","CircuitCanvas","PropertyPanel","InteractiveCircuitExample: React.FC","componentType: string","position: Point","action: string","id: string","key: string","value: any","componentId: string","portId: string","event: React.MouseEvent","CircuitToolbar","ComponentPalette","CircuitCanvas","PropertyPanel","VoltageRegulatorExample: React.FC","CircuitCanvas","TimerCircuitExample: React.FC","CircuitCanvas","componentCountByCategory: Record<string, number>","componentNames: Record<string, string>","componentDescriptions: Record<string, string>","component: ComponentSchema"],"sources":["../src/hooks/useCircuit.ts","../src/hooks/usePortPosition.ts","../src/ui/headless/HeadlessPropertyPanel.tsx","../src/ui/PropertyPanel.tsx","../src/ui/headless/HeadlessComponentPalette.tsx","../src/ui/ComponentPalette.tsx","../src/ui/headless/HeadlessCircuitToolbar.tsx","../src/ui/CircuitToolbar.tsx","../src/examples/SimpleCircuitExample.tsx","../src/examples/InteractiveCircuitExample.tsx","../src/examples/VoltageRegulatorExample.tsx","../src/examples/TimerCircuitExample.tsx","../src/registry/metadata.ts","../src/index.ts"],"sourcesContent":["/**\n * useCircuit Hook\n *\n * A React hook for managing circuit state and operations. Provides a complete\n * state management solution for circuit components and wires, with support for\n * selection, wire drawing, and undo/redo functionality.\n *\n * @returns {[CircuitState, CircuitActions]} A tuple containing the current circuit state and actions to modify it\n *\n * @example\n * const [state, actions] = useCircuit();\n *\n * // Add a new component\n * const resistorId = actions.addComponent({\n * type: 'resistor',\n * position: { x: 100, y: 100 },\n * props: { resistance: 1000 }\n * });\n *\n * // Connect components with a wire\n * actions.addWire(\n * { componentId: resistorId, portId: 'left' },\n * { componentId: 'battery-1', portId: 'positive' }\n * );\n *\n * // Access current state\n * console.log(state.components, state.wires);\n */\n\nimport { useState, useCallback, useRef } from 'react';\nimport { ComponentInstance, Wire, CircuitState, Point } from '../schemas/componentSchema';\nimport { getComponentSchema } from '../registry';\n\nexport interface CircuitActions {\n /** Adds a new component to the circuit and returns its generated ID */\n addComponent: (component: Omit<ComponentInstance, 'id'>) => string;\n\n /** Updates properties of an existing component */\n updateComponent: (id: string, updates: Partial<ComponentInstance>) => void;\n\n /** Removes a component from the circuit */\n removeComponent: (id: string) => void;\n\n /** Adds a new wire between two ports and returns its generated ID */\n addWire: (from: Wire['from'], to: Wire['to'], style?: Wire['style']) => string;\n\n /** Updates properties of an existing wire */\n updateWire: (id: string, updates: Partial<Wire>) => void;\n\n /** Removes a wire from the circuit */\n removeWire: (id: string) => void;\n\n /** Selects a component, optionally adding to the current selection */\n selectComponent: (id: string, addToSelection?: boolean) => void;\n\n /** Selects a wire, optionally adding to the current selection */\n selectWire: (id: string, addToSelection?: boolean) => void;\n\n /** Deselects all currently selected components and wires */\n deselectAll: () => void;\n\n /** Returns an array of currently selected components */\n getSelectedComponents: () => ComponentInstance[];\n\n /** Returns an array of currently selected wires */\n getSelectedWires: () => Wire[];\n\n /** Moves a component to a new position */\n moveComponent: (id: string, newPosition: Point) => void;\n\n /** Moves all currently selected components by a delta */\n moveSelectedComponents: (dx: number, dy: number) => void;\n\n /** Starts the process of drawing a wire from a component port */\n startWireDrawing: (componentId: string, portId: string) => void;\n\n /** Completes the wire drawing process, connecting to another port */\n completeWireDrawing: (componentId: string, portId: string) => boolean;\n\n /** Cancels the wire drawing process */\n cancelWireDrawing: () => void;\n\n /** Checks if a connection between two ports is valid */\n isValidConnection: (fromId: string, fromPortId: string, toId: string, toPortId: string) => boolean;\n\n /** Rotates a component by the specified degrees */\n rotateComponent: (id: string, degrees: number) => void;\n\n /** Rotates all currently selected components by the specified degrees */\n rotateSelectedComponents: (degrees: number) => void;\n\n /** Undoes the last action */\n undo: () => void;\n\n /** Redoes the last undone action */\n redo: () => void;\n\n /** Checks if an undo action is possible */\n canUndo: () => boolean;\n\n /** Checks if a redo action is possible */\n canRedo: () => boolean;\n}\n\n/**\n * React hook for managing circuit state\n *\n * @param initialState - Optional initial state for the circuit\n * @returns A tuple containing the circuit state and action functions\n */\nexport function useCircuit(initialState?: Partial<CircuitState>): [CircuitState, CircuitActions] {\n // History for undo/redo\n const undoStack = useRef<CircuitState[]>([]);\n const redoStack = useRef<CircuitState[]>([]);\n\n const [state, setState] = useState<CircuitState>({\n components: initialState?.components || [],\n wires: initialState?.wires || [],\n selectedComponentIds: initialState?.selectedComponentIds || [],\n selectedWireIds: initialState?.selectedWireIds || []\n });\n\n // Helper to save state for undo\n const saveStateForUndo = useCallback((prevState: CircuitState) => {\n undoStack.current.push(JSON.parse(JSON.stringify(prevState)));\n // Clear redo stack when a new action is performed\n redoStack.current = [];\n }, []);\n\n // Wrapped setState with history tracking\n const setStateWithHistory = useCallback((updater: (prev: CircuitState) => CircuitState) => {\n setState(prev => {\n // Save the current state for undoing\n saveStateForUndo(prev);\n // Apply the update\n return updater(prev);\n });\n }, [saveStateForUndo]);\n\n // Generate a unique ID for new components/wires\n const generateId = useCallback((prefix: string): string => {\n return `${prefix}-${Math.random().toString(36).substring(2, 11)}`;\n }, []);\n\n // Add a component\n const addComponent = useCallback((component: Omit<ComponentInstance, 'id'>): string => {\n const id = generateId('component');\n setStateWithHistory(prev => ({\n ...prev,\n components: [...prev.components, { ...component, id }]\n }));\n return id;\n }, [generateId, setStateWithHistory]);\n\n // Update a component\n const updateComponent = useCallback((id: string, updates: Partial<ComponentInstance>): void => {\n setStateWithHistory(prev => ({\n ...prev,\n components: prev.components.map(c =>\n c.id === id ? { ...c, ...updates } : c\n )\n }));\n }, [setStateWithHistory]);\n\n // Remove a component\n const removeComponent = useCallback((id: string): void => {\n setStateWithHistory(prev => {\n // Remove the component\n const components = prev.components.filter(c => c.id !== id);\n\n // Remove any wires connected to this component\n const wires = prev.wires.filter(w =>\n w.from.componentId !== id && w.to.componentId !== id\n );\n\n // Remove from selection if selected\n const selectedComponentIds = prev.selectedComponentIds.filter(selectedId => selectedId !== id);\n\n return {\n ...prev,\n components,\n wires,\n selectedComponentIds\n };\n });\n }, [setStateWithHistory]);\n\n // Add a wire\n const addWire = useCallback((from: Wire['from'], to: Wire['to'], style?: Wire['style']): string => {\n const id = generateId('wire');\n setStateWithHistory(prev => ({\n ...prev,\n wires: [...prev.wires, { id, from, to, style }]\n }));\n return id;\n }, [generateId, setStateWithHistory]);\n\n // Update a wire\n const updateWire = useCallback((id: string, updates: Partial<Wire>): void => {\n setStateWithHistory(prev => ({\n ...prev,\n wires: prev.wires.map(w =>\n w.id === id ? { ...w, ...updates } : w\n )\n }));\n }, [setStateWithHistory]);\n\n // Remove a wire\n const removeWire = useCallback((id: string): void => {\n setStateWithHistory(prev => ({\n ...prev,\n wires: prev.wires.filter(w => w.id !== id),\n selectedWireIds: prev.selectedWireIds.filter(wireId => wireId !== id)\n }));\n }, [setStateWithHistory]);\n\n // Select a component\n const selectComponent = useCallback((id: string, addToSelection: boolean = false): void => {\n setState(prev => {\n // If adding to selection, keep previous selections, otherwise start fresh\n const selectedComponentIds = addToSelection\n ? [...prev.selectedComponentIds, id]\n : [id];\n\n return {\n ...prev,\n selectedComponentIds: [...new Set(selectedComponentIds)] // Deduplicate\n };\n });\n }, []);\n\n // Select a wire\n const selectWire = useCallback((id: string, addToSelection: boolean = false): void => {\n setState(prev => {\n // If adding to selection, keep previous selections, otherwise start fresh\n const selectedWireIds = addToSelection\n ? [...prev.selectedWireIds, id]\n : [id];\n\n return {\n ...prev,\n selectedWireIds: [...new Set(selectedWireIds)] // Deduplicate\n };\n });\n }, []);\n\n // Deselect all components and wires\n const deselectAll = useCallback((): void => {\n setState(prev => ({\n ...prev,\n selectedComponentIds: [],\n selectedWireIds: []\n }));\n }, []);\n\n // Get selected components\n const getSelectedComponents = useCallback((): ComponentInstance[] => {\n return state.components.filter(c =>\n state.selectedComponentIds.includes(c.id)\n );\n }, [state.components, state.selectedComponentIds]);\n\n // Get selected wires\n const getSelectedWires = useCallback((): Wire[] => {\n return state.wires.filter(w =>\n state.selectedWireIds.includes(w.id)\n );\n }, [state.wires, state.selectedWireIds]);\n\n // Move a component to a new position\n const moveComponent = useCallback((id: string, newPosition: Point): void => {\n setStateWithHistory(prev => ({\n ...prev,\n components: prev.components.map(c =>\n c.id === id ? { ...c, position: newPosition } : c\n )\n }));\n }, [setStateWithHistory]);\n\n // Move all selected components by a delta\n const moveSelectedComponents = useCallback((dx: number, dy: number): void => {\n setStateWithHistory(prev => ({\n ...prev,\n components: prev.components.map(c => {\n if (prev.selectedComponentIds.includes(c.id)) {\n return {\n ...c,\n position: {\n x: c.position.x + dx,\n y: c.position.y + dy\n }\n };\n }\n return c;\n })\n }));\n }, [setStateWithHistory]);\n\n // Rotate a component by the specified degrees\n const rotateComponent = useCallback((id: string, degrees: number): void => {\n setStateWithHistory(prev => ({\n ...prev,\n components: prev.components.map(c => {\n if (c.id === id) {\n // Calculate new rotation, normalizing between 0-359\n const currentRotation = c.rotation || 0;\n const newRotation = (currentRotation + degrees) % 360;\n return {\n ...c,\n rotation: newRotation < 0 ? newRotation + 360 : newRotation\n };\n }\n return c;\n })\n }));\n }, [setStateWithHistory]);\n\n // Rotate all selected components\n const rotateSelectedComponents = useCallback((degrees: number): void => {\n setStateWithHistory(prev => ({\n ...prev,\n components: prev.components.map(c => {\n if (prev.selectedComponentIds.includes(c.id)) {\n // Calculate new rotation, normalizing between 0-359\n const currentRotation = c.rotation || 0;\n const newRotation = (currentRotation + degrees) % 360;\n return {\n ...c,\n rotation: newRotation < 0 ? newRotation + 360 : newRotation\n };\n }\n return c;\n })\n }));\n }, [setStateWithHistory]);\n\n // Undo the last action\n const undo = useCallback((): void => {\n if (undoStack.current.length > 0) {\n // Save current state to redo stack\n redoStack.current.push(JSON.parse(JSON.stringify(state)));\n\n // Pop the last state from undo stack\n const prevState = undoStack.current.pop()!;\n\n // Apply the previous state\n setState(prevState);\n }\n }, [state]);\n\n // Redo the last undone action\n const redo = useCallback((): void => {\n if (redoStack.current.length > 0) {\n // Save current state to undo stack\n undoStack.current.push(JSON.parse(JSON.stringify(state)));\n\n // Pop the last state from redo stack\n const nextState = redoStack.current.pop()!;\n\n // Apply the next state\n setState(nextState);\n }\n }, [state]);\n\n // Check if undo is possible\n const canUndo = useCallback((): boolean => {\n return undoStack.current.length > 0;\n }, []);\n\n // Check if redo is possible\n const canRedo = useCallback((): boolean => {\n return redoStack.current.length > 0;\n }, []);\n\n // Wire drawing state\n const [wireDrawingState, setWireDrawingState] = useState<{\n isDrawing: boolean;\n fromComponentId: string | null;\n fromPortId: string | null;\n }>({\n isDrawing: false,\n fromComponentId: null,\n fromPortId: null\n });\n\n // Start drawing a wire from a component port\n const startWireDrawing = useCallback((componentId: string, portId: string): void => {\n setWireDrawingState({\n isDrawing: true,\n fromComponentId: componentId,\n fromPortId: portId\n });\n }, []);\n\n // Complete wire drawing by connecting to another port\n const completeWireDrawing = useCallback((toComponentId: string, toPortId: string): boolean => {\n const { isDrawing, fromComponentId, fromPortId } = wireDrawingState;\n\n if (!isDrawing || !fromComponentId || !fromPortId) {\n return false;\n }\n\n // Check if this is a valid connection\n const isValid = isValidConnection(\n fromComponentId,\n fromPortId,\n toComponentId,\n toPortId\n );\n\n if (isValid) {\n // Create the wire\n addWire(\n { componentId: fromComponentId, portId: fromPortId },\n { componentId: toComponentId, portId: toPortId }\n );\n\n // Reset wire drawing state\n setWireDrawingState({\n isDrawing: false,\n fromComponentId: null,\n fromPortId: null\n });\n\n return true;\n }\n\n return false;\n }, [wireDrawingState, addWire]);\n\n // Cancel wire drawing\n const cancelWireDrawing = useCallback((): void => {\n setWireDrawingState({\n isDrawing: false,\n fromComponentId: null,\n fromPortId: null\n });\n }, []);\n\n // Check if a connection between two ports is valid\n const isValidConnection = useCallback((\n fromId: string,\n fromPortId: string,\n toId: string,\n toPortId: string\n ): boolean => {\n // Don't allow connections to the same component\n if (fromId === toId) {\n return false;\n }\n\n // Don't allow duplicate connections\n const isDuplicate = state.wires.some(wire =>\n (wire.from.componentId === fromId && wire.from.portId === fromPortId &&\n wire.to.componentId === toId && wire.to.portId === toPortId) ||\n (wire.from.componentId === toId && wire.from.portId === toPortId &&\n wire.to.componentId === fromId && wire.to.portId === fromPortId)\n );\n\n if (isDuplicate) {\n return false;\n }\n\n // Get component schemas for port type checking\n const fromComponent = state.components.find(c => c.id === fromId);\n const toComponent = state.components.find(c => c.id === toId);\n\n if (!fromComponent || !toComponent) {\n return false;\n }\n\n const fromSchema = getComponentSchema(fromComponent.type);\n const toSchema = getComponentSchema(toComponent.type);\n\n if (!fromSchema || !toSchema) {\n return false;\n }\n\n // Get port info\n const fromPort = fromSchema.ports.find(p => p.id === fromPortId);\n const toPort = toSchema.ports.find(p => p.id === toPortId);\n\n if (!fromPort || !toPort) {\n return false;\n }\n\n // Basic port compatibility check - outputs can connect to inputs\n if (fromPort.type === 'output' && toPort.type === 'input') {\n return true;\n }\n\n // Inputs can connect to outputs\n if (fromPort.type === 'input' && toPort.type === 'output') {\n return true;\n }\n\n // Bidirectional ports can connect to anything\n if (fromPort.type === 'inout' || toPort.type === 'inout') {\n return true;\n }\n\n return false;\n }, [state.components, state.wires]);\n\n const actions: CircuitActions = {\n addComponent,\n updateComponent,\n removeComponent,\n addWire,\n updateWire,\n removeWire,\n selectComponent,\n selectWire,\n deselectAll,\n getSelectedComponents,\n getSelectedWires,\n moveComponent,\n moveSelectedComponents,\n startWireDrawing,\n completeWireDrawing,\n cancelWireDrawing,\n isValidConnection,\n rotateComponent,\n rotateSelectedComponents,\n undo,\n redo,\n canUndo,\n canRedo\n };\n\n return [state, actions];\n}\n\nexport default useCircuit;\n","/**\n * usePortPosition Hook\n *\n * A React hook for tracking the position of component ports.\n * Compatible with infinite circuit canvas with panning and zooming.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { Point, Wire } from '../schemas/componentSchema';\nimport { getPortPosition } from '../utils/getPortPosition';\n\ninterface PortPositionState {\n from: Point | null;\n to: Point | null;\n error: string | null;\n}\n\n/**\n * A hook to track the positions of ports connected by a wire\n *\n * @param wire - The wire connecting the ports\n * @param deps - Additional dependencies that should trigger position updates\n * @returns Object containing port positions and error state\n */\nexport function usePortPosition(\n wire: Wire | null,\n deps: any[] = []\n): PortPositionState {\n const [state, setState] = useState<PortPositionState>({\n from: null,\n to: null,\n error: null\n });\n\n // Use callback to get port positions to avoid useEffect dependencies\n const updatePositions = useCallback(() => {\n if (!wire) {\n setState({\n from: null,\n to: null,\n error: 'No wire provided'\n });\n return;\n }\n\n // Small delay to ensure DOM has been updated\n setTimeout(() => {\n try {\n // Get source port position\n const fromPos = getPortPosition(wire.from.componentId, wire.from.portId);\n\n // Get destination port position\n const toPos = getPortPosition(wire.to.componentId, wire.to.portId);\n\n setState({\n from: fromPos,\n to: toPos,\n error: (!fromPos || !toPos)\n ? `Could not find ports for wire ${wire.id}`\n : null\n });\n } catch (error) {\n setState({\n from: null,\n to: null,\n error: `Error getting port positions: ${error}`\n });\n }\n }, 0);\n }, [wire]);\n\n // Update positions when wire or dependencies change\n useEffect(() => {\n updatePositions();\n\n // Add event listeners for window resize and canvas transformations to update positions\n window.addEventListener('resize', updatePositions);\n\n // Listen to custom event for viewport transformations\n const handleViewportChange = () => {\n updatePositions();\n };\n\n // Find circuit canvas element to attach transformation event listeners\n const canvas = document.querySelector('[data-circuit-canvas]');\n if (canvas) {\n // Listen to mouse wheel events for zoom changes\n canvas.addEventListener('wheel', updatePositions, { passive: true });\n\n // Custom event for pan/zoom that can be dispatched by CircuitCanvas\n canvas.addEventListener('viewport-change', handleViewportChange);\n }\n\n return () => {\n window.removeEventListener('resize', updatePositions);\n\n if (canvas) {\n canvas.removeEventListener('wheel', updatePositions);\n canvas.removeEventListener('viewport-change', handleViewportChange);\n }\n };\n }, [wire, updatePositions, ...deps]);\n\n return state;\n}\n\n/**\n * Hook to track a single port position\n *\n * @param componentId - The component ID\n * @param portId - The port ID\n * @param deps - Additional dependencies that should trigger position updates\n * @returns The port position or null if not found\n */\nexport function useSinglePortPosition(\n componentId: string | null,\n portId: string | null,\n deps: any[] = []\n): Point | null {\n const [position, setPosition] = useState<Point | null>(null);\n\n const updatePosition = useCallback(() => {\n if (!componentId || !portId) {\n setPosition(null);\n return;\n }\n\n setTimeout(() => {\n try {\n const pos = getPortPosition(componentId, portId);\n setPosition(pos);\n } catch (error) {\n console.warn(`Error getting port position: ${error}`);\n setPosition(null);\n }\n }, 0);\n }, [componentId, portId]);\n\n useEffect(() => {\n updatePosition();\n\n // Add event listeners for window resize and canvas transformations\n window.addEventListener('resize', updatePosition);\n\n // Listen to custom event for viewport transformations\n const handleViewportChange = () => {\n updatePosition();\n };\n\n // Find circuit canvas element to attach transformation event listeners\n const canvas = document.querySelector('[data-circuit-canvas]');\n if (canvas) {\n // Listen to mouse wheel events for zoom changes\n canvas.addEventListener('wheel', updatePosition, { passive: true });\n\n // Custom event for pan/zoom that can be dispatched by CircuitCanvas\n canvas.addEventListener('viewport-change', handleViewportChange);\n }\n\n return () => {\n window.removeEventListener('resize', updatePosition);\n\n if (canvas) {\n canvas.removeEventListener('wheel', updatePosition);\n canvas.removeEventListener('viewport-change', handleViewportChange);\n }\n };\n }, [componentId, portId, updatePosition, ...deps]);\n\n return position;\n}\n\nexport default usePortPosition;\n","/**\n * HeadlessPropertyPanel Component\n *\n * A headless (unstyled) component for editing the properties of a circuit component.\n * This component provides all the functionality without any styling, allowing users\n * to apply their own styling using their preferred method.\n */\n\nimport React from 'react';\nimport { ComponentInstance } from '../../schemas/componentSchema';\nimport { getComponentSchema } from '../../registry';\n\nexport interface HeadlessPropertyPanelProps {\n /** The component instance to edit properties for */\n component: ComponentInstance | null;\n /** Callback when a property changes */\n onPropertyChange?: (id: string, key: string, value: any) => void;\n /** Additional class name for the root element */\n className?: string;\n /** Additional class names for sub-components */\n classNames?: {\n /** Class for the empty state container */\n emptyContainer?: string;\n /** Class for the error state container */\n errorContainer?: string;\n /** Class for the header section */\n header?: string;\n /** Class for the title */\n title?: string;\n /** Class for the component ID */\n componentId?: string;\n /** Class for the content section */\n content?: string;\n /** Class for each property item */\n propertyItem?: string;\n /** Class for property labels */\n propertyLabel?: string;\n /** Class for property unit text */\n propertyUnit?: string;\n /** Class for number inputs */\n numberInput?: string;\n /** Class for text inputs */\n textInput?: string;\n /** Class for checkbox containers */\n checkboxContainer?: string;\n /** Class for checkboxes */\n checkbox?: string;\n /** Class for checkbox labels */\n checkboxLabel?: string;\n /** Class for select inputs */\n select?: string;\n /** Class for color input containers */\n colorContainer?: string;\n /** Class for color inputs */\n colorInput?: string;\n /** Class for color text inputs */\n colorTextInput?: string;\n /** Class for the position section */\n positionSection?: string;\n /** Class for position section title */\n positionTitle?: string;\n /** Class for position inputs container */\n positionInputs?: string;\n /** Class for position input groups */\n positionInputGroup?: string;\n /** Class for position labels */\n positionLabel?: string;\n /** Class for position inputs */\n positionInput?: string;\n /** Class for the rotation section */\n rotationSection?: string;\n /** Class for rotation section title */\n rotationTitle?: string;\n /** Class for rotation slider container */\n rotationSlider?: string;\n /** Class for rotation range input */\n rotationRange?: string;\n /** Class for rotation value display */\n rotationValue?: string;\n /** Class for rotation presets container */\n rotationPresets?: string;\n /** Class for rotation preset buttons */\n rotationPresetButton?: string;\n };\n /** Additional inline styles for the root element */\n style?: React.CSSProperties;\n /** Additional inline styles for sub-components */\n styles?: {\n [key: string]: React.CSSProperties;\n };\n}\n\nconst HeadlessPropertyPanel: React.FC<HeadlessPropertyPanelProps> = ({\n component,\n onPropertyChange,\n className = '',\n classNames = {},\n style = {},\n styles = {}\n}) => {\n if (!component) {\n return (\n <div\n className={`cb-property-panel-empty ${classNames.emptyContainer || ''} ${className}`}\n style={style}\n data-testid=\"property-panel-empty\"\n >\n <svg\n width=\"48\"\n height=\"48\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <rect x=\"2\" y=\"2\" width=\"20\" height=\"8\" rx=\"2\" ry=\"2\"></rect>\n <rect x=\"2\" y=\"14\" width=\"20\" height=\"8\" rx=\"2\" ry=\"2\"></rect>\n <line x1=\"6\" y1=\"6\" x2=\"6.01\" y2=\"6\"></line>\n <line x1=\"6\" y1=\"18\" x2=\"6.01\" y2=\"18\"></line>\n </svg>\n <p>No component selected</p>\n <p>Select a component to edit its properties</p>\n </div>\n );\n }\n\n const schema = getComponentSchema(component.type);\n\n if (!schema) {\n return (\n <div\n className={`cb-property-panel-error ${classNames.errorContainer || ''} ${className}`}\n style={style}\n data-testid=\"property-panel-error\"\n role=\"alert\"\n >\n <svg\n width=\"48\"\n height=\"48\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\"></circle>\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"></line>\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"></line>\n </svg>\n <p>Error: Unknown component type '{component.type}'</p>\n <p>This component type is not registered in the system</p>\n </div>\n );\n }\n\n const handleChange = (\n key: string,\n event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>\n ) => {\n const property = schema.properties.find(prop => prop.key === key);\n if (!property) return;\n\n let value: any = event.target.value;\n\n // Convert value based on property type\n if (property.type === 'number') {\n value = parseFloat(value);\n\n // Validate min/max if specified\n if (property.min !== undefined && value < property.min) {\n value = property.min;\n }\n if (property.max !== undefined && value > property.max) {\n value = property.max;\n }\n } else if (property.type === 'boolean') {\n value = (event.target as HTMLInputElement).checked;\n }\n\n onPropertyChange?.(component.id, key, value);\n };\n\n return (\n <div\n className={`cb-property-panel ${className}`}\n style={style}\n data-testid=\"property-panel\"\n >\n <div className={`cb-property-panel-header ${classNames.header || ''}`} style={styles.header}>\n <h3 className={`cb-property-panel-title ${classNames.title || ''}`} style={styles.title}>\n {schema.name} Properties\n </h3>\n <span\n className={`cb-property-panel-component-id ${classNames.componentId || ''}`}\n style={styles.componentId}\n >\n ID: {component.id}\n </span>\n </div>\n\n <div className={`cb-property-panel-content ${classNames.content || ''}`} style={styles.content}>\n {schema.properties.map(property => {\n const currentValue = component.props[property.key] ?? property.default;\n const inputId = `prop-${component.id}-${property.key}`;\n\n return (\n <div\n key={property.key}\n className={`cb-property-item ${classNames.propertyItem || ''}`}\n style={styles.propertyItem}\n data-property-key={property.key}\n >\n <label\n htmlFor={inputId}\n className={`cb-property-label ${classNames.propertyLabel || ''}`}\n style={styles.propertyLabel}\n >\n {property.label}\n {property.unit && (\n <span\n className={`cb-property-unit ${classNames.propertyUnit || ''}`}\n style={styles.propertyUnit}\n >\n {property.unit}\n </span>\n )}\n </label>\n\n {property.type === 'number' && (\n <input\n id={inputId}\n type=\"number\"\n value={currentValue}\n onChange={e => handleChange(property.key, e)}\n min={property.min}\n max={property.max}\n step=\"any\"\n className={`cb-property-number-input ${classNames.numberInput || ''}`}\n style={styles.numberInput}\n aria-label={`${property.label} ${property.unit || ''}`}\n />\n )}\n\n {property.type === 'text' && (\n <input\n id={inputId}\n type=\"text\"\n value={currentValue}\n onChange={e => handleChange(property.key, e)}\n className={`cb-property-text-input ${classNames.textInput || ''}`}\n style={styles.textInput}\n aria-label={property.label}\n />\n )}\n\n {property.type === 'boolean' && (\n <div\n className={`cb-property-checkbox-container ${classNames.checkboxContainer || ''}`}\n style={styles.checkboxContainer}\n >\n <input\n id={inputId}\n type=\"checkbox\"\n checked={currentValue}\n onChange={e => handleChange(property.key, e)}\n className={`cb-property-checkbox ${classNames.checkbox || ''}`}\n style={styles.checkbox}\n aria-label={property.label}\n />\n <span\n className={`cb-property-checkbox-label ${classNames.checkboxLabel || ''}`}\n style={styles.checkboxLabel}\n >\n {currentValue ? 'Enabled' : 'Disabled'}\n </span>\n </div>\n )}\n\n {property.type === 'select' && property.options && (\n <select\n id={inputId}\n value={currentValue}\n onChange={e => handleChange(property.key, e)}\n className={`cb-property-select ${classNames.select || ''}`}\n style={styles.select}\n aria-label={property.label}\n >\n {property.options.map(option => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n )}\n\n {property.type === 'color' && (\n <div\n className={`cb-property-color-container ${classNames.colorContainer || ''}`}\n style={styles.colorContainer}\n >\n <input\n id={inputId}\n type=\"color\"\n value={currentValue}\n onChange={e => handleChange(property.key, e)}\n className={`cb-property-color-input ${classNames.colorInput || ''}`}\n style={styles.colorInput}\n aria-label={`${property.label} color picker`}\n />\n <input\n type=\"text\"\n value={currentValue}\n onChange={e => handleChange(property.key, e)}\n className={`cb-property-color-text-input ${classNames.colorTextInput || ''}`}\n style={styles.colorTextInput}\n aria-label={`${property.label} color value`}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n\n {/* Position Section */}\n <div\n className={`cb-property-panel-position ${classNames.positionSection || ''}`}\n style={styles.positionSection}\n >\n <h4\n className={`cb-property-panel-position-title ${classNames.positionTitle || ''}`}\n style={styles.positionTitle}\n >\n Position\n </h4>\n <div\n className={`cb-property-panel-position-inputs ${classNames.positionInputs || ''}`}\n style={styles.positionInputs}\n >\n <div\n className={`cb-property-panel-position-input-group ${classNames.positionInputGroup || ''}`}\n style={styles.positionInputGroup}\n >\n <label\n className={`cb-property-panel-position-label ${classNames.positionLabel || ''}`}\n style={styles.positionLabel}\n htmlFor=\"position-x\"\n >\n X:\n </label>\n <input\n id=\"position-x\"\n type=\"number\"\n value={component.position.x}\n onChange={e => onPropertyChange?.(\n component.id,\n 'position',\n { ...component.position, x: parseFloat(e.target.value) }\n )}\n className={`cb-property-panel-position-input ${classNames.positionInput || ''}`}\n style={styles.positionInput}\n aria-label=\"X position\"\n />\n </div>\n <div\n className={`cb-property-panel-position-input-group ${classNames.positionInputGroup || ''}`}\n style={styles.positionInputGroup}\n >\n <label\n className={`cb-property-panel-position-label ${classNames.positionLabel || ''}`}\n style={styles.positionLabel}\n htmlFor=\"position-y\"\n >\n Y:\n </label>\n <input\n id=\"position-y\"\n type=\"number\"\n value={component.position.y}\n onChange={e => onPropertyChange?.(\n component.id,\n 'position',\n { ...component.position, y: parseFloat(e.target.value) }\n )}\n className={`cb-property-panel-position-input ${classNames.positionInput || ''}`}\n style={styles.positionInput}\n aria-label=\"Y position\"\n />\n </div>\n </div>\n </div>\n\n {/* Rotation Section */}\n {component.rotation !== undefined && (\n <div\n className={`cb-property-panel-rotation ${classNames.rotationSection || ''}`}\n style={styles.rotationSection}\n >\n <h4\n className={`cb-property-panel-rotation-title ${classNames.rotationTitle || ''}`}\n style={styles.rotationTitle}\n >\n Rotation\n </h4>\n <div\n className={`cb-property-panel-rotation-slider ${classNames.rotationSlider || ''}`}\n style={styles.rotationSlider}\n >\n <input\n type=\"range\"\n min=\"0\"\n max=\"360\"\n step=\"90\"\n value={component.rotation}\n onChange={e => onPropertyChange?.(\n component.id,\n 'rotation',\n parseFloat(e.target.value)\n )}\n className={`cb-property-panel-rotation-range ${classNames.rotationRange || ''}`}\n style={styles.rotationRange}\n aria-label=\"Rotation angle\"\n />\n <div\n className={`cb-property-panel-rotation-value ${classNames.rotationValue || ''}`}\n style={styles.rotationValue}\n >\n {component.rotation}°\n </div>\n </div>\n <div\n className={`cb-property-panel-rotation-presets ${classNames.rotationPresets || ''}`}\n style={styles.rotationPresets}\n >\n {[0, 90, 180, 270, 360].map(angle => (\n <button\n key={angle}\n onClick={() => onPropertyChange?.(\n component.id,\n 'rotation',\n angle\n )}\n className={`cb-property-panel-rotation-preset-button ${classNames.rotationPresetButton || ''}`}\n style={styles.rotationPresetButton}\n data-active={component.rotation === angle}\n aria-label={`Set rotation to ${angle} degrees`}\n aria-pressed={component.rotation === angle}\n >\n {angle}°\n </button>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n};\n\nexport default HeadlessPropertyPanel;\n","/**\n * PropertyPanel Component\n *\n * A component for editing the properties of a circuit component.\n * This is a styled wrapper around the HeadlessPropertyPanel component.\n */\n\nimport React, { CSSProperties } from 'react';\nimport { ComponentInstance } from '../schemas/componentSchema';\nimport HeadlessPropertyPanel from './headless/HeadlessPropertyPanel';\n\nexport interface PropertyPanelProps {\n component: ComponentInstance | null;\n onPropertyChange?: (id: string, key: string, value: any) => void;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst PropertyPanel: React.FC<PropertyPanelProps> = ({\n component,\n onPropertyChange,\n className = '',\n style = {}\n}) => {\n // Root style with user's custom style\n const rootStyle: CSSProperties = {\n backgroundColor: '#1a202c',\n borderRadius: '12px',\n boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',\n overflow: 'hidden',\n border: '1px solid #2d3748',\n color: '#e2e8f0',\n ...style\n };\n\n // Default styles for the PropertyPanel\n const defaultStyles: Record<string, CSSProperties> = {\n // Empty state\n emptyContainer: {\n backgroundColor: '#1a202c',\n borderRadius: '12px',\n padding: '24px',\n display: 'flex',\n flexDirection: 'column' as const,\n alignItems: 'center',\n justifyContent: 'center',\n color: '#a0aec0',\n height: '100%',\n boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',\n border: '1px solid rgb(21, 25, 32)',\n textAlign: 'center'\n },\n\n // Error state\n errorContainer: {\n backgroundColor: '#1a202c',\n borderRadius: '12px',\n padding: '24px',\n display: 'flex',\n flexDirection: 'column' as const,\n alignItems: 'center',\n justifyContent: 'center',\n color: '#f56565',\n boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',\n border: '1px solid rgb(21, 26, 34)',\n textAlign: 'center'\n },\n\n // Header section\n header: {\n padding: '16px',\n borderBottom: '1px solid #2d3748',\n background: '#202938'\n },\n\n // Title\n title: {\n margin: '0 0 4px 0',\n fontSize: '18px',\n fontWeight: 600,\n color: '#e2e8f0'\n },\n\n // Component ID\n componentId: {\n fontSize: '12px',\n color: '#a0aec0',\n opacity: 0.7\n },\n\n // Content section\n content: {\n padding: '16px'\n },\n\n // Property item\n propertyItem: {\n marginBottom: '16px'\n },\n\n // Property label\n propertyLabel: {\n display: 'block',\n marginBottom: '6px',\n fontSize: '14px',\n fontWeight: 500,\n color: '#cbd5e0'\n },\n\n // Property unit\n propertyUnit: {\n marginLeft: '4px',\n color: '#a0aec0',\n fontSize: '12px'\n },\n\n // Number input\n numberInput: {\n width: '100%',\n padding: '8px 12px',\n borderRadius: '6px',\n border: '1px solid #4a5568',\n backgroundColor: '#2d3748',\n color: '#e2e8f0',\n fontSize: '14px',\n outline: 'none',\n transition: 'all 0.2s ease',\n boxSizing: 'border-box'\n },\n\n // Text input\n textInput: {\n width: '100%',\n padding: '8px 12px',\n borderRadius: '6px',\n border: '1px solid #4a5568',\n backgroundColor: '#2d3748',\n color: '#e2e8f0',\n fontSize: '14px',\n outline: 'none',\n transition: 'all 0.2s ease',\n boxSizing: 'border-box'\n },\n\n // Checkbox container\n checkboxContainer: {\n display: 'flex',\n alignItems: 'center'\n },\n\n // Checkbox\n checkbox: {\n width: '18px',\n height: '18px',\n cursor: 'pointer',\n accentColor: '#4299e1',\n borderRadius: '4px',\n marginRight: '8px'\n },\n\n // Checkbox label\n checkboxLabel: {\n fontSize: '14px',\n color: '#cbd5e0'\n },\n\n // Select input\n select: {\n width: '100%',\n padding: '8px 12px',\n borderRadius: '6px',\n border: '1px solid #4a5568',\n backgroundColor: '#2d3748',\n color: '#e2e8f0',\n fontSize: '14px',\n outline: 'none',\n transition: 'all 0.2s ease',\n cursor: 'pointer',\n appearance: 'none',\n backgroundImage: 'url(\"data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23a0aec0%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E\")',\n backgroundRepeat: 'no-repeat',\n backgroundPosition: 'right 12px top 50%',\n backgroundSize: '12px auto',\n paddingRight: '30px'\n },\n\n // Color container\n colorContainer: {\n display: 'flex',\n alignItems: 'center',\n gap: '10px'\n },\n\n // Color input\n colorInput: {\n width: '36px',\n height: '36px',\n border: 'none',\n borderRadius: '6px',\n backgroundColor: 'transparent',\n cursor: 'pointer'\n },\n\n // Color text input\n colorTextInput: {\n flexGrow: 1,\n padding: '8px 12px',\n borderRadius: '6px',\n border: '1px solid #4a5568',\n backgroundColor: '#2d3748',\n color: '#e2e8f0',\n fontSize: '14px'\n },\n\n // Position section\n positionSection: {\n padding: '0 16px 16px',\n borderTop: '1px solid #2d3748',\n marginTop: '8px'\n },\n\n // Position title\n positionTitle: {\n fontSize: '16px',\n fontWeight: 500,\n margin: '16px 0 12px',\n color: '#cbd5e0'\n },\n\n // Position inputs\n positionInputs: {\n display: 'grid',\n gridTemplateColumns: '1fr 1fr',\n gap: '12px'\n },\n\n // Position input group\n positionInputGroup: {\n display: 'flex',\n flexDirection: 'column' as const\n },\n\n // Position label\n positionLabel: {\n fontSize: '14px',\n fontWeight: 500,\n marginBottom: '6px',\n color: '#cbd5e0'\n },\n\n // Position input\n positionInput: {\n width: '100%',\n padding: '8px 12px',\n borderRadius: '6px',\n border: '1px solid #4a5568',\n backgroundColor: '#2d3748',\n color: '#e2e8f0',\n fontSize: '14px',\n outline: 'none',\n transition: 'all 0.2s ease'\n },\n\n // Rotation section\n rotationSection: {\n padding: '0 16px 16px',\n borderTop: '1px solid #2d3748'\n },\n\n // Rotation title\n rotationTitle: {\n fontSize: '16px',\n fontWeight: 500,\n margin: '16px 0 12px',\n color: '#cbd5e0'\n },\n\n // Rotation slider\n rotationSlider: {\n display: 'flex',\n alignItems: 'center',\n gap: '12px'\n },\n\n // Rotation range\n rotationRange: {\n flex: 1,\n height: '6px',\n borderRadius: '3px',\n backgroundColor: '#4a5568',\n appearance: 'none',\n outline: 'none',\n cursor: 'pointer',\n accentColor: '#4299e1'\n },\n\n // Rotation value\n rotationValue: {\n padding: '6px 10px',\n backgroundColor: '#2d3748',\n borderRadius: '6px',\n border: '1px solid #4a5568',\n fontSize: '14px',\n fontWeight: 500,\n color: '#e2e8f0',\n minWidth: '48px',\n textAlign: 'center'\n },\n\n // Rotation presets\n rotationPresets: {\n display: 'flex',\n justifyContent: 'space-between',\n marginTop: '8px'\n },\n\n // Rotation preset button\n rotationPresetButton: {\n background: '#2d3748',\n border: 'none',\n borderRadius: '4px',\n width: '32px',\n height: '24px',\n fontSize: '12px',\n color: '#cbd5e0',\n cursor: 'pointer',\n transition: 'all 0.2s ease',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center'\n }\n };\n\n return (\n <HeadlessPropertyPanel\n component={component}\n onPropertyChange={onPropertyChange}\n className={className}\n style={rootStyle}\n styles={defaultStyles}\n />\n );\n};\n\nexport default PropertyPanel;\n","/**\n * HeadlessComponentPalette Component\n *\n * A headless (unstyled) component for selecting and adding circuit components to a canvas.\n * This component provides all the functionality without any styling, allowing users\n * to apply their own styling using their preferred method.\n */\n\nimport React, { useState, useRef, useEffect } from 'react';\nimport { getAllComponents, getComponentsByCategory } from '.