UNPKG

circuit-bricks

Version:

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

1,541 lines (1,534 loc) 85.6 kB
"use client"; "use strict"; const require_chunk = require('./chunks/chunk-DkEB4ore.cjs'); const require_registry = require('./chunks/registry-Clke7-Ur.cjs'); const require_CircuitCanvas = require('./chunks/CircuitCanvas-ChVdnpl7.cjs'); const require_ssrUtils = require('./chunks/ssrUtils-BTgsP4pb.cjs'); const require_SSRSafeCircuitCanvas = require('./chunks/SSRSafeCircuitCanvas-BSa852kP.cjs'); const require_llm = require('./chunks/llm-CzENYj9R.cjs'); const require_performanceUtils = require('./chunks/performanceUtils-DbSScEs-.cjs'); const require_touchUtils = require('./chunks/touchUtils-_BBqMLob.cjs'); const require_responsiveUtils = require('./chunks/responsiveUtils-DYIcaAqt.cjs'); require('./chunks/server-Vc500-G9.cjs'); const react = require_chunk.__toESM(require("react")); const react_jsx_runtime = require_chunk.__toESM(require("react/jsx-runtime")); //#region src/hooks/useCircuit.ts /** * React hook for managing circuit state * * @param initialState - Optional initial state for the circuit * @returns A tuple containing the circuit state and action functions */ function useCircuit(initialState) { const undoStack = (0, react.useRef)([]); const redoStack = (0, react.useRef)([]); const [state, setState] = (0, react.useState)({ components: initialState?.components || [], wires: initialState?.wires || [], selectedComponentIds: initialState?.selectedComponentIds || [], selectedWireIds: initialState?.selectedWireIds || [] }); const saveStateForUndo = (0, react.useCallback)((prevState) => { undoStack.current.push(JSON.parse(JSON.stringify(prevState))); redoStack.current = []; }, []); const setStateWithHistory = (0, react.useCallback)((updater) => { setState((prev) => { saveStateForUndo(prev); return updater(prev); }); }, [saveStateForUndo]); const generateId = (0, react.useCallback)((prefix) => { return `${prefix}-${Math.random().toString(36).substring(2, 11)}`; }, []); const addComponent = (0, react.useCallback)((component) => { const id = generateId("component"); setStateWithHistory((prev) => ({ ...prev, components: [...prev.components, { ...component, id }] })); return id; }, [generateId, setStateWithHistory]); const updateComponent = (0, react.useCallback)((id, updates) => { setStateWithHistory((prev) => ({ ...prev, components: prev.components.map((c) => c.id === id ? { ...c, ...updates } : c) })); }, [setStateWithHistory]); const removeComponent = (0, react.useCallback)((id) => { setStateWithHistory((prev) => { const components = prev.components.filter((c) => c.id !== id); const wires = prev.wires.filter((w) => w.from.componentId !== id && w.to.componentId !== id); const selectedComponentIds = prev.selectedComponentIds.filter((selectedId) => selectedId !== id); return { ...prev, components, wires, selectedComponentIds }; }); }, [setStateWithHistory]); const addWire = (0, react.useCallback)((from, to, style) => { const id = generateId("wire"); setStateWithHistory((prev) => ({ ...prev, wires: [...prev.wires, { id, from, to, style }] })); return id; }, [generateId, setStateWithHistory]); const updateWire = (0, react.useCallback)((id, updates) => { setStateWithHistory((prev) => ({ ...prev, wires: prev.wires.map((w) => w.id === id ? { ...w, ...updates } : w) })); }, [setStateWithHistory]); const removeWire = (0, react.useCallback)((id) => { setStateWithHistory((prev) => ({ ...prev, wires: prev.wires.filter((w) => w.id !== id), selectedWireIds: prev.selectedWireIds.filter((wireId) => wireId !== id) })); }, [setStateWithHistory]); const selectComponent = (0, react.useCallback)((id, addToSelection = false) => { setState((prev) => { const selectedComponentIds = addToSelection ? [...prev.selectedComponentIds, id] : [id]; return { ...prev, selectedComponentIds: [...new Set(selectedComponentIds)] }; }); }, []); const selectWire = (0, react.useCallback)((id, addToSelection = false) => { setState((prev) => { const selectedWireIds = addToSelection ? [...prev.selectedWireIds, id] : [id]; return { ...prev, selectedWireIds: [...new Set(selectedWireIds)] }; }); }, []); const deselectAll = (0, react.useCallback)(() => { setState((prev) => ({ ...prev, selectedComponentIds: [], selectedWireIds: [] })); }, []); const getSelectedComponents = (0, react.useCallback)(() => { return state.components.filter((c) => state.selectedComponentIds.includes(c.id)); }, [state.components, state.selectedComponentIds]); const getSelectedWires = (0, react.useCallback)(() => { return state.wires.filter((w) => state.selectedWireIds.includes(w.id)); }, [state.wires, state.selectedWireIds]); const moveComponent = (0, react.useCallback)((id, newPosition) => { setStateWithHistory((prev) => ({ ...prev, components: prev.components.map((c) => c.id === id ? { ...c, position: newPosition } : c) })); }, [setStateWithHistory]); const moveSelectedComponents = (0, react.useCallback)((dx, dy) => { setStateWithHistory((prev) => ({ ...prev, components: prev.components.map((c) => { if (prev.selectedComponentIds.includes(c.id)) return { ...c, position: { x: c.position.x + dx, y: c.position.y + dy } }; return c; }) })); }, [setStateWithHistory]); const rotateComponent = (0, react.useCallback)((id, degrees) => { setStateWithHistory((prev) => ({ ...prev, components: prev.components.map((c) => { if (c.id === id) { const currentRotation = c.rotation || 0; const newRotation = (currentRotation + degrees) % 360; return { ...c, rotation: newRotation < 0 ? newRotation + 360 : newRotation }; } return c; }) })); }, [setStateWithHistory]); const rotateSelectedComponents = (0, react.useCallback)((degrees) => { setStateWithHistory((prev) => ({ ...prev, components: prev.components.map((c) => { if (prev.selectedComponentIds.includes(c.id)) { const currentRotation = c.rotation || 0; const newRotation = (currentRotation + degrees) % 360; return { ...c, rotation: newRotation < 0 ? newRotation + 360 : newRotation }; } return c; }) })); }, [setStateWithHistory]); const undo = (0, react.useCallback)(() => { if (undoStack.current.length > 0) { redoStack.current.push(JSON.parse(JSON.stringify(state))); const prevState = undoStack.current.pop(); setState(prevState); } }, [state]); const redo = (0, react.useCallback)(() => { if (redoStack.current.length > 0) { undoStack.current.push(JSON.parse(JSON.stringify(state))); const nextState = redoStack.current.pop(); setState(nextState); } }, [state]); const canUndo = (0, react.useCallback)(() => { return undoStack.current.length > 0; }, []); const canRedo = (0, react.useCallback)(() => { return redoStack.current.length > 0; }, []); const [wireDrawingState, setWireDrawingState] = (0, react.useState)({ isDrawing: false, fromComponentId: null, fromPortId: null }); const startWireDrawing = (0, react.useCallback)((componentId, portId) => { setWireDrawingState({ isDrawing: true, fromComponentId: componentId, fromPortId: portId }); }, []); const completeWireDrawing = (0, react.useCallback)((toComponentId, toPortId) => { const { isDrawing, fromComponentId, fromPortId } = wireDrawingState; if (!isDrawing || !fromComponentId || !fromPortId) return false; const isValid = isValidConnection(fromComponentId, fromPortId, toComponentId, toPortId); if (isValid) { addWire({ componentId: fromComponentId, portId: fromPortId }, { componentId: toComponentId, portId: toPortId }); setWireDrawingState({ isDrawing: false, fromComponentId: null, fromPortId: null }); return true; } return false; }, [wireDrawingState, addWire]); const cancelWireDrawing = (0, react.useCallback)(() => { setWireDrawingState({ isDrawing: false, fromComponentId: null, fromPortId: null }); }, []); const isValidConnection = (0, react.useCallback)((fromId, fromPortId, toId, toPortId) => { if (fromId === toId) return false; const isDuplicate = state.wires.some((wire) => wire.from.componentId === fromId && wire.from.portId === fromPortId && wire.to.componentId === toId && wire.to.portId === toPortId || wire.from.componentId === toId && wire.from.portId === toPortId && wire.to.componentId === fromId && wire.to.portId === fromPortId); if (isDuplicate) return false; const fromComponent = state.components.find((c) => c.id === fromId); const toComponent = state.components.find((c) => c.id === toId); if (!fromComponent || !toComponent) return false; const fromSchema = require_registry.getComponentSchema(fromComponent.type); const toSchema = require_registry.getComponentSchema(toComponent.type); if (!fromSchema || !toSchema) return false; const fromPort = fromSchema.ports.find((p) => p.id === fromPortId); const toPort = toSchema.ports.find((p) => p.id === toPortId); if (!fromPort || !toPort) return false; if (fromPort.type === "output" && toPort.type === "input") return true; if (fromPort.type === "input" && toPort.type === "output") return true; if (fromPort.type === "inout" || toPort.type === "inout") return true; return false; }, [state.components, state.wires]); const actions = { addComponent, updateComponent, removeComponent, addWire, updateWire, removeWire, selectComponent, selectWire, deselectAll, getSelectedComponents, getSelectedWires, moveComponent, moveSelectedComponents, startWireDrawing, completeWireDrawing, cancelWireDrawing, isValidConnection, rotateComponent, rotateSelectedComponents, undo, redo, canUndo, canRedo }; return [state, actions]; } var useCircuit_default = useCircuit; //#endregion //#region src/hooks/usePortPosition.ts /** * A hook to track the positions of ports connected by a wire * * @param wire - The wire connecting the ports * @param deps - Additional dependencies that should trigger position updates * @returns Object containing port positions and error state */ function usePortPosition(wire, deps = []) { const [state, setState] = (0, react.useState)({ from: null, to: null, error: null }); const updatePositions = (0, react.useCallback)(() => { if (!wire) { setState({ from: null, to: null, error: "No wire provided" }); return; } setTimeout(() => { try { const fromPos = require_CircuitCanvas.getPortPosition(wire.from.componentId, wire.from.portId); const toPos = require_CircuitCanvas.getPortPosition(wire.to.componentId, wire.to.portId); setState({ from: fromPos, to: toPos, error: !fromPos || !toPos ? `Could not find ports for wire ${wire.id}` : null }); } catch (error) { setState({ from: null, to: null, error: `Error getting port positions: ${error}` }); } }, 0); }, [wire]); (0, react.useEffect)(() => { updatePositions(); window.addEventListener("resize", updatePositions); const handleViewportChange = () => { updatePositions(); }; const canvas = document.querySelector("[data-circuit-canvas]"); if (canvas) { canvas.addEventListener("wheel", updatePositions, { passive: true }); canvas.addEventListener("viewport-change", handleViewportChange); } return () => { window.removeEventListener("resize", updatePositions); if (canvas) { canvas.removeEventListener("wheel", updatePositions); canvas.removeEventListener("viewport-change", handleViewportChange); } }; }, [ wire, updatePositions, ...deps ]); return state; } /** * Hook to track a single port position * * @param componentId - The component ID * @param portId - The port ID * @param deps - Additional dependencies that should trigger position updates * @returns The port position or null if not found */ function useSinglePortPosition(componentId, portId, deps = []) { const [position, setPosition] = (0, react.useState)(null); const updatePosition = (0, react.useCallback)(() => { if (!componentId || !portId) { setPosition(null); return; } setTimeout(() => { try { const pos = require_CircuitCanvas.getPortPosition(componentId, portId); setPosition(pos); } catch (error) { console.warn(`Error getting port position: ${error}`); setPosition(null); } }, 0); }, [componentId, portId]); (0, react.useEffect)(() => { updatePosition(); window.addEventListener("resize", updatePosition); const handleViewportChange = () => { updatePosition(); }; const canvas = document.querySelector("[data-circuit-canvas]"); if (canvas) { canvas.addEventListener("wheel", updatePosition, { passive: true }); canvas.addEventListener("viewport-change", handleViewportChange); } return () => { window.removeEventListener("resize", updatePosition); if (canvas) { canvas.removeEventListener("wheel", updatePosition); canvas.removeEventListener("viewport-change", handleViewportChange); } }; }, [ componentId, portId, updatePosition, ...deps ]); return position; } var usePortPosition_default = usePortPosition; //#endregion //#region src/ui/headless/HeadlessPropertyPanel.tsx const HeadlessPropertyPanel = ({ component, onPropertyChange, className = "", classNames = {}, style = {}, styles = {} }) => { if (!component) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-empty ${classNames.emptyContainer || ""} ${className}`, style, "data-testid": "property-panel-empty", children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", { x: "2", y: "2", width: "20", height: "8", rx: "2", ry: "2" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", { x: "2", y: "14", width: "20", height: "8", rx: "2", ry: "2" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "6.01", y2: "6" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", { x1: "6", y1: "18", x2: "6.01", y2: "18" }) ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "No component selected" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "Select a component to edit its properties" }) ] }); const schema = require_registry.getComponentSchema(component.type); if (!schema) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-error ${classNames.errorContainer || ""} ${className}`, style, "data-testid": "property-panel-error", role: "alert", children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "10" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", { x1: "12", y1: "8", x2: "12", y2: "12" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" }) ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [ "Error: Unknown component type '", component.type, "'" ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "This component type is not registered in the system" }) ] }); const handleChange = (key, event) => { const property = schema.properties.find((prop) => prop.key === key); if (!property) return; let value = event.target.value; if (property.type === "number") { value = parseFloat(value); if (property.min !== void 0 && value < property.min) value = property.min; if (property.max !== void 0 && value > property.max) value = property.max; } else if (property.type === "boolean") value = event.target.checked; onPropertyChange?.(component.id, key, value); }; return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel ${className}`, style, "data-testid": "property-panel", children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-header ${classNames.header || ""}`, style: styles.header, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", { className: `cb-property-panel-title ${classNames.title || ""}`, style: styles.title, children: [schema.name, " Properties"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { className: `cb-property-panel-component-id ${classNames.componentId || ""}`, style: styles.componentId, children: ["ID: ", component.id] })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `cb-property-panel-content ${classNames.content || ""}`, style: styles.content, children: schema.properties.map((property) => { const currentValue = component.props[property.key] ?? property.default; const inputId = `prop-${component.id}-${property.key}`; return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-item ${classNames.propertyItem || ""}`, style: styles.propertyItem, "data-property-key": property.key, children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { htmlFor: inputId, className: `cb-property-label ${classNames.propertyLabel || ""}`, style: styles.propertyLabel, children: [property.label, property.unit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `cb-property-unit ${classNames.propertyUnit || ""}`, style: styles.propertyUnit, children: property.unit })] }), property.type === "number" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { id: inputId, type: "number", value: currentValue, onChange: (e) => handleChange(property.key, e), min: property.min, max: property.max, step: "any", className: `cb-property-number-input ${classNames.numberInput || ""}`, style: styles.numberInput, "aria-label": `${property.label} ${property.unit || ""}` }), property.type === "text" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { id: inputId, type: "text", value: currentValue, onChange: (e) => handleChange(property.key, e), className: `cb-property-text-input ${classNames.textInput || ""}`, style: styles.textInput, "aria-label": property.label }), property.type === "boolean" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-checkbox-container ${classNames.checkboxContainer || ""}`, style: styles.checkboxContainer, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { id: inputId, type: "checkbox", checked: currentValue, onChange: (e) => handleChange(property.key, e), className: `cb-property-checkbox ${classNames.checkbox || ""}`, style: styles.checkbox, "aria-label": property.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `cb-property-checkbox-label ${classNames.checkboxLabel || ""}`, style: styles.checkboxLabel, children: currentValue ? "Enabled" : "Disabled" })] }), property.type === "select" && property.options && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", { id: inputId, value: currentValue, onChange: (e) => handleChange(property.key, e), className: `cb-property-select ${classNames.select || ""}`, style: styles.select, "aria-label": property.label, children: property.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: option.value, children: option.label }, option.value)) }), property.type === "color" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-color-container ${classNames.colorContainer || ""}`, style: styles.colorContainer, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { id: inputId, type: "color", value: currentValue, onChange: (e) => handleChange(property.key, e), className: `cb-property-color-input ${classNames.colorInput || ""}`, style: styles.colorInput, "aria-label": `${property.label} color picker` }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { type: "text", value: currentValue, onChange: (e) => handleChange(property.key, e), className: `cb-property-color-text-input ${classNames.colorTextInput || ""}`, style: styles.colorTextInput, "aria-label": `${property.label} color value` })] }) ] }, property.key); }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-position ${classNames.positionSection || ""}`, style: styles.positionSection, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { className: `cb-property-panel-position-title ${classNames.positionTitle || ""}`, style: styles.positionTitle, children: "Position" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-position-inputs ${classNames.positionInputs || ""}`, style: styles.positionInputs, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-position-input-group ${classNames.positionInputGroup || ""}`, style: styles.positionInputGroup, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { className: `cb-property-panel-position-label ${classNames.positionLabel || ""}`, style: styles.positionLabel, htmlFor: "position-x", children: "X:" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { id: "position-x", type: "number", value: component.position.x, onChange: (e) => onPropertyChange?.(component.id, "position", { ...component.position, x: parseFloat(e.target.value) }), className: `cb-property-panel-position-input ${classNames.positionInput || ""}`, style: styles.positionInput, "aria-label": "X position" })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-position-input-group ${classNames.positionInputGroup || ""}`, style: styles.positionInputGroup, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { className: `cb-property-panel-position-label ${classNames.positionLabel || ""}`, style: styles.positionLabel, htmlFor: "position-y", children: "Y:" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { id: "position-y", type: "number", value: component.position.y, onChange: (e) => onPropertyChange?.(component.id, "position", { ...component.position, y: parseFloat(e.target.value) }), className: `cb-property-panel-position-input ${classNames.positionInput || ""}`, style: styles.positionInput, "aria-label": "Y position" })] })] })] }), component.rotation !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-rotation ${classNames.rotationSection || ""}`, style: styles.rotationSection, children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { className: `cb-property-panel-rotation-title ${classNames.rotationTitle || ""}`, style: styles.rotationTitle, children: "Rotation" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-rotation-slider ${classNames.rotationSlider || ""}`, style: styles.rotationSlider, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { type: "range", min: "0", max: "360", step: "90", value: component.rotation, onChange: (e) => onPropertyChange?.(component.id, "rotation", parseFloat(e.target.value)), className: `cb-property-panel-rotation-range ${classNames.rotationRange || ""}`, style: styles.rotationRange, "aria-label": "Rotation angle" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-property-panel-rotation-value ${classNames.rotationValue || ""}`, style: styles.rotationValue, children: [component.rotation, "°"] })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `cb-property-panel-rotation-presets ${classNames.rotationPresets || ""}`, style: styles.rotationPresets, children: [ 0, 90, 180, 270, 360 ].map((angle) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", { onClick: () => onPropertyChange?.(component.id, "rotation", angle), className: `cb-property-panel-rotation-preset-button ${classNames.rotationPresetButton || ""}`, style: styles.rotationPresetButton, "data-active": component.rotation === angle, "aria-label": `Set rotation to ${angle} degrees`, "aria-pressed": component.rotation === angle, children: [angle, "°"] }, angle)) }) ] }) ] }); }; var HeadlessPropertyPanel_default = HeadlessPropertyPanel; //#endregion //#region src/ui/PropertyPanel.tsx const PropertyPanel = ({ component, onPropertyChange, className = "", style = {} }) => { const rootStyle = { backgroundColor: "#1a202c", borderRadius: "12px", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", overflow: "hidden", border: "1px solid #2d3748", color: "#e2e8f0", ...style }; const defaultStyles = { emptyContainer: { backgroundColor: "#1a202c", borderRadius: "12px", padding: "24px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", color: "#a0aec0", height: "100%", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", border: "1px solid rgb(21, 25, 32)", textAlign: "center" }, errorContainer: { backgroundColor: "#1a202c", borderRadius: "12px", padding: "24px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", color: "#f56565", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", border: "1px solid rgb(21, 26, 34)", textAlign: "center" }, header: { padding: "16px", borderBottom: "1px solid #2d3748", background: "#202938" }, title: { margin: "0 0 4px 0", fontSize: "18px", fontWeight: 600, color: "#e2e8f0" }, componentId: { fontSize: "12px", color: "#a0aec0", opacity: .7 }, content: { padding: "16px" }, propertyItem: { marginBottom: "16px" }, propertyLabel: { display: "block", marginBottom: "6px", fontSize: "14px", fontWeight: 500, color: "#cbd5e0" }, propertyUnit: { marginLeft: "4px", color: "#a0aec0", fontSize: "12px" }, numberInput: { width: "100%", padding: "8px 12px", borderRadius: "6px", border: "1px solid #4a5568", backgroundColor: "#2d3748", color: "#e2e8f0", fontSize: "14px", outline: "none", transition: "all 0.2s ease", boxSizing: "border-box" }, textInput: { width: "100%", padding: "8px 12px", borderRadius: "6px", border: "1px solid #4a5568", backgroundColor: "#2d3748", color: "#e2e8f0", fontSize: "14px", outline: "none", transition: "all 0.2s ease", boxSizing: "border-box" }, checkboxContainer: { display: "flex", alignItems: "center" }, checkbox: { width: "18px", height: "18px", cursor: "pointer", accentColor: "#4299e1", borderRadius: "4px", marginRight: "8px" }, checkboxLabel: { fontSize: "14px", color: "#cbd5e0" }, select: { width: "100%", padding: "8px 12px", borderRadius: "6px", border: "1px solid #4a5568", backgroundColor: "#2d3748", color: "#e2e8f0", fontSize: "14px", outline: "none", transition: "all 0.2s ease", cursor: "pointer", appearance: "none", 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\")", backgroundRepeat: "no-repeat", backgroundPosition: "right 12px top 50%", backgroundSize: "12px auto", paddingRight: "30px" }, colorContainer: { display: "flex", alignItems: "center", gap: "10px" }, colorInput: { width: "36px", height: "36px", border: "none", borderRadius: "6px", backgroundColor: "transparent", cursor: "pointer" }, colorTextInput: { flexGrow: 1, padding: "8px 12px", borderRadius: "6px", border: "1px solid #4a5568", backgroundColor: "#2d3748", color: "#e2e8f0", fontSize: "14px" }, positionSection: { padding: "0 16px 16px", borderTop: "1px solid #2d3748", marginTop: "8px" }, positionTitle: { fontSize: "16px", fontWeight: 500, margin: "16px 0 12px", color: "#cbd5e0" }, positionInputs: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px" }, positionInputGroup: { display: "flex", flexDirection: "column" }, positionLabel: { fontSize: "14px", fontWeight: 500, marginBottom: "6px", color: "#cbd5e0" }, positionInput: { width: "100%", padding: "8px 12px", borderRadius: "6px", border: "1px solid #4a5568", backgroundColor: "#2d3748", color: "#e2e8f0", fontSize: "14px", outline: "none", transition: "all 0.2s ease" }, rotationSection: { padding: "0 16px 16px", borderTop: "1px solid #2d3748" }, rotationTitle: { fontSize: "16px", fontWeight: 500, margin: "16px 0 12px", color: "#cbd5e0" }, rotationSlider: { display: "flex", alignItems: "center", gap: "12px" }, rotationRange: { flex: 1, height: "6px", borderRadius: "3px", backgroundColor: "#4a5568", appearance: "none", outline: "none", cursor: "pointer", accentColor: "#4299e1" }, rotationValue: { padding: "6px 10px", backgroundColor: "#2d3748", borderRadius: "6px", border: "1px solid #4a5568", fontSize: "14px", fontWeight: 500, color: "#e2e8f0", minWidth: "48px", textAlign: "center" }, rotationPresets: { display: "flex", justifyContent: "space-between", marginTop: "8px" }, rotationPresetButton: { background: "#2d3748", border: "none", borderRadius: "4px", width: "32px", height: "24px", fontSize: "12px", color: "#cbd5e0", cursor: "pointer", transition: "all 0.2s ease", display: "flex", alignItems: "center", justifyContent: "center" } }; return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeadlessPropertyPanel_default, { component, onPropertyChange, className, style: rootStyle, styles: defaultStyles }); }; var PropertyPanel_default = PropertyPanel; //#endregion //#region src/ui/headless/HeadlessComponentPalette.tsx const HeadlessComponentPalette = ({ onSelectComponent, onDragComponent, className = "", classNames = {}, style = {}, styles = {} }) => { const [searchTerm, setSearchTerm] = (0, react.useState)(""); const [activeCategory, setActiveCategory] = (0, react.useState)(null); const allComponents = require_registry.getAllComponents(); const allCategories = Array.from(new Set(allComponents.map((comp) => comp.category))).sort(); const filteredComponents = activeCategory ? require_registry.getComponentsByCategory(activeCategory) : allComponents; const searchFilteredComponents = searchTerm ? filteredComponents.filter((comp) => comp.name.toLowerCase().includes(searchTerm.toLowerCase()) || comp.description.toLowerCase().includes(searchTerm.toLowerCase())) : filteredComponents; const tabsRef = (0, react.useRef)(null); const listRef = (0, react.useRef)(null); const [showTabsLeftShadow, setShowTabsLeftShadow] = (0, react.useState)(false); const [showTabsRightShadow, setShowTabsRightShadow] = (0, react.useState)(false); const [showListTopShadow, setShowListTopShadow] = (0, react.useState)(false); const [showListBottomShadow, setShowListBottomShadow] = (0, react.useState)(false); const handleTabsScroll = () => { if (tabsRef.current) { const { scrollLeft, scrollWidth, clientWidth } = tabsRef.current; setShowTabsLeftShadow(scrollLeft > 0); setShowTabsRightShadow(scrollLeft < scrollWidth - clientWidth - 1); } }; const handleListScroll = () => { if (listRef.current) { const { scrollTop, scrollHeight, clientHeight } = listRef.current; setShowListTopShadow(scrollTop > 0); setShowListBottomShadow(scrollTop < scrollHeight - clientHeight - 1); } }; (0, react.useEffect)(() => { handleTabsScroll(); handleListScroll(); const handleResize = () => { handleTabsScroll(); handleListScroll(); }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, []); return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-component-palette ${className}`, style, "data-testid": "component-palette", children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-component-palette-header ${classNames.header || ""}`, style: styles.header, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { className: `cb-component-palette-title ${classNames.title || ""}`, style: styles.title, children: "Components" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-component-palette-search-container ${classNames.searchContainer || ""}`, style: styles.searchContainer, children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { type: "text", placeholder: "Search components...", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), className: `cb-component-palette-search-input ${classNames.searchInput || ""}`, style: styles.searchInput, "aria-label": "Search components" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: `cb-component-palette-search-icon ${classNames.searchIcon || ""}`, style: styles.searchIcon, "aria-hidden": "true", children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", { cx: "11", cy: "11", r: "8" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })] }), searchTerm && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", { onClick: () => setSearchTerm(""), className: `cb-component-palette-clear-search ${classNames.clearSearchButton || ""}`, style: styles.clearSearchButton, "aria-label": "Clear search", children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) }) ] })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: "cb-component-palette-tabs-wrapper", children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { ref: tabsRef, className: `cb-component-palette-category-tabs ${classNames.categoryTabs || ""}`, style: styles.categoryTabs, onScroll: handleTabsScroll, children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-component-palette-category-tabs-inner ${classNames.categoryTabsInner || ""}`, style: styles.categoryTabsInner, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", { className: `cb-component-palette-category-tab ${activeCategory === null ? "cb-component-palette-category-tab-active" : ""} ${classNames.categoryTab || ""} ${activeCategory === null ? classNames.activeCategoryTab || "" : ""}`, onClick: () => setActiveCategory(null), style: activeCategory === null ? { ...styles.categoryTab, ...styles.activeCategoryTab } : styles.categoryTab, "data-active": activeCategory === null, "aria-pressed": activeCategory === null, children: "All" }), allCategories.map((category) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", { className: `cb-component-palette-category-tab ${activeCategory === category ? "cb-component-palette-category-tab-active" : ""} ${classNames.categoryTab || ""} ${activeCategory === category ? classNames.activeCategoryTab || "" : ""}`, onClick: () => setActiveCategory(category), style: activeCategory === category ? { ...styles.categoryTab, ...styles.activeCategoryTab } : styles.categoryTab, "data-active": activeCategory === category, "aria-pressed": activeCategory === category, children: category.charAt(0).toUpperCase() + category.slice(1) }, category))] }) }), showTabsLeftShadow && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `cb-component-palette-left-shadow ${classNames.leftShadow || ""}`, style: styles.leftShadow, "aria-hidden": "true" }), showTabsRightShadow && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `cb-component-palette-right-shadow ${classNames.rightShadow || ""}`, style: styles.rightShadow, "aria-hidden": "true" }) ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: "cb-component-palette-list-wrapper", children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { ref: listRef, className: `cb-component-palette-component-list ${classNames.componentList || ""}`, style: styles.componentList, onScroll: handleListScroll, children: searchFilteredComponents.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-component-palette-empty-state ${classNames.emptyState || ""}`, style: styles.emptyState, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", { width: "40", height: "40", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", { cx: "11", cy: "11", r: "8" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "No components found." })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "cb-component-palette-component-items", children: searchFilteredComponents.map((component) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeadlessComponentListItem, { component, onSelect: () => onSelectComponent(component.id), onDrag: onDragComponent ? (e) => onDragComponent(component.id, e) : void 0, classNames, styles }, component.id)) }) }), showListTopShadow && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `cb-component-palette-top-shadow ${classNames.topShadow || ""}`, style: styles.topShadow, "aria-hidden": "true" }), showListBottomShadow && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `cb-component-palette-bottom-shadow ${classNames.bottomShadow || ""}`, style: styles.bottomShadow, "aria-hidden": "true" }) ] }) ] }); }; const HeadlessComponentListItem = ({ component, onSelect, onDrag, classNames = {}, styles = {} }) => { const handleDragStart = (event) => { event.dataTransfer.setData("application/circuit-component", component.id); event.dataTransfer.effectAllowed = "copy"; const dragImg = document.createElement("div"); dragImg.innerHTML = ` <svg width="40" height="40" viewBox="0 0 ${component.defaultWidth} ${component.defaultHeight}"> <path d="${component.svgPath}" fill="none" stroke="#333" stroke-width="2" /> </svg> `; dragImg.style.position = "absolute"; dragImg.style.top = "-1000px"; document.body.appendChild(dragImg); event.dataTransfer.setDragImage(dragImg, 20, 20); onDrag?.(event); setTimeout(() => { document.body.removeChild(dragImg); }, 0); }; return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { className: `cb-component-item ${classNames.componentItem || ""}`, onClick: onSelect, title: component.description, draggable: true, onDragStart: handleDragStart, style: styles.componentItem, "data-component-id": component.id, "data-component-category": component.category, children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `cb-component-icon ${classNames.componentIcon || ""}`, style: styles.componentIcon, children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", { width: "32", height: "32", viewBox: `0 0 ${component.defaultWidth} ${component.defaultHeight}`, preserveAspectRatio: "xMidYMid meet", "aria-hidden": "true", children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: component.svgPath, fill: "none", stroke: "currentColor", strokeWidth: 2, vectorEffect: "non-scaling-stroke" }) }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-component-details ${classNames.componentDetails || ""}`, style: styles.componentDetails, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `cb-component-name ${classNames.componentName || ""}`, style: styles.componentName, children: component.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `cb-component-category ${classNames.componentCategory || ""}`, style: styles.componentCategory, children: component.category })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: `cb-component-arrow ${classNames.componentArrow || ""}`, style: styles.componentArrow, "aria-hidden": "true", children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "9 18 15 12 9 6" }) }) }) ] }); }; var HeadlessComponentPalette_default = HeadlessComponentPalette; //#endregion //#region src/ui/ComponentPalette.tsx const ComponentPalette = ({ onSelectComponent, onDragComponent, className = "", style = {} }) => { const rootStyle = { display: "flex", flexDirection: "column", background: "#2d3748", borderRadius: "12px", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)", overflow: "hidden", height: "100%", border: "1px solid #1a202c", ...style }; const defaultStyles = { header: { padding: "16px", borderBottom: "1px solid #1a202c", background: "#202938" }, title: { margin: "0 0 12px 0", fontSize: "18px", fontWeight: 600, color: "#e2e8f0" }, searchContainer: { position: "relative", display: "flex", alignItems: "center" }, searchInput: { width: "100%", padding: "10px 12px 10px 36px", fontSize: "14px", border: "1px solid #4a5568", borderRadius: "8px", backgroundColor: "#1a202c", color: "#e2e8f0", transition: "all 0.2s ease", outline: "none", boxSizing: "border-box", boxShadow: "inset 0 1px 2px rgba(0, 0, 0, 0.2)" }, searchIcon: { position: "absolute", left: "12px", pointerEvents: "none", color: "#a0aec0" }, clearSearchButton: { position: "absolute", right: "12px", background: "none", border: "none", cursor: "pointer", padding: "0", display: "flex", alignItems: "center", justifyContent: "center", color: "#a0aec0" }, categoryTabs: { display: "flex", overflowX: "auto", padding: "0 16px", scrollbarWidth: "none", msOverflowStyle: "none", background: "#202938", position: "relative", zIndex: 1 }, categoryTabsInner: { display: "flex", padding: "8px 0", gap: "8px" }, categoryTab: { padding: "8px 16px", borderRadius: "6px", fontSize: "14px", fontWeight: 500, border: "none", background: "transparent", color: "#a0aec0", cursor: "pointer", whiteSpace: "nowrap", transition: "all 0.2s ease", boxShadow: "none" }, activeCategoryTab: { background: "#3182ce", color: "#fff", boxShadow: "0 1px 3px rgba(0, 0, 0, 0.2)" }, componentList: { overflowY: "auto", height: "100%", padding: "0 16px 16px", position: "relative", zIndex: 1 }, emptyState: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "32px 0", color: "#718096", textAlign: "center", margin: "16px 0" }, componentItems: { padding: 0, margin: "16px 0 0 0", display: "grid", gap: "10px" }, componentItem: { display: "flex", alignItems: "center", padding: "10px 12px", borderRadius: "8px", background: "#1a202c", border: "1px solid #2d3748", boxShadow: "0 1px 3px rgba(0, 0, 0, 0.2)", cursor: "pointer", transition: "all 0.2s ease", gap: "12px", listStyle: "none" }, componentIcon: { display: "flex", alignItems: "center", justifyContent: "center", width: "48px", height: "48px", borderRadius: "6px", background: "#2d3748", color: "#a0aec0", flexShrink: 0 }, componentDetails: { display: "flex", flexDirection: "column", overflow: "hidden" }, componentName: { fontWeight: 500, fontSize: "14px", color: "#e2e8f0", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", opacity: .9 }, componentCategory: { fontSize: "12px", color: "#a0aec0", marginTop: "2px", opacity: .7 }, componentArrow: { marginLeft: "auto", opacity: .4, flexShrink: 0, color: "#718096" }, leftShadow: { position: "absolute", top: 0, left: 0, width: "16px", height: "100%", background: "linear-gradient(to right, rgba(32, 41, 56, 0.9), rgba(32, 41, 56, 0))", zIndex: 2, pointerEvents: "none" }, rightShadow: { position: "absolute", top: 0, right: 0, width: "16px", height: "100%", background: "linear-gradient(to left, rgba(32, 41, 56, 0.9), rgba(32, 41, 56, 0))", zIndex: 2, pointerEvents: "none" }, topShadow: { position: "absolute", top: 0, left: 0, right: 0, height: "12px", background: "linear-gradient(to bottom, rgba(45, 55, 72, 0.9), rgba(45, 55, 72, 0))", zIndex: 2, pointerEvents: "none" }, bottomShadow: { position: "absolute", bottom: 0, left: 0, right: 0, height: "12px", background: "linear-gradient(to top, rgba(45, 55, 72, 0.9), rgba(45, 55, 72, 0))", zIndex: 2, pointerEvents: "none" } }; return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeadlessComponentPalette_default, { onSelectComponent, onDragComponent, className, style: rootStyle, styles: defaultStyles }); }; var ComponentPalette_default = ComponentPalette; //#endregion //#region src/ui/headless/HeadlessCircuitToolbar.tsx const HeadlessCircuitToolbar = ({ onAction, hasSelection = false, canUndo = false, canRedo = false, showGrid = true, validationIssues = 0, className = "", classNames = {}, style = {}, styles = {} }) => { return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `cb-circuit-toolbar ${className}`, style, "data-testid": "circuit-toolbar", children: [ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: `