diff --git a/frontend/src/components/simulator/BoardOnCanvas.tsx b/frontend/src/components/simulator/BoardOnCanvas.tsx index e3d33a1..521c5db 100644 --- a/frontend/src/components/simulator/BoardOnCanvas.tsx +++ b/frontend/src/components/simulator/BoardOnCanvas.tsx @@ -39,6 +39,7 @@ interface BoardOnCanvasProps { led13?: boolean; isActive?: boolean; onMouseDown: (e: React.MouseEvent) => void; + onContextMenu?: (e: React.MouseEvent) => void; onPinClick: (componentId: string, pinName: string, x: number, y: number) => void; zoom?: number; } @@ -49,6 +50,7 @@ export const BoardOnCanvas = ({ led13 = false, isActive = false, onMouseDown, + onContextMenu, onPinClick, zoom = 1, }: BoardOnCanvasProps) => { @@ -146,6 +148,7 @@ export const BoardOnCanvas = ({ zIndex: 1, }} onMouseDown={(e) => { e.stopPropagation(); onMouseDown(e); }} + onContextMenu={onContextMenu} /> )} diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index bf2a77f..85a2cea 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -46,6 +46,7 @@ export const SimulatorCanvas = () => { updateComponentState, addComponent, removeComponent, + removeBoard, updateComponent, serialMonitorOpen, toggleSerialMonitor, @@ -104,6 +105,11 @@ export const SimulatorCanvas = () => { const [sensorControlComponentId, setSensorControlComponentId] = useState(null); const [sensorControlMetadataId, setSensorControlMetadataId] = useState(null); + // Board context menu (right-click) + const [boardContextMenu, setBoardContextMenu] = useState<{ boardId: string; x: number; y: number } | null>(null); + // Board removal confirmation dialog + const [boardToRemove, setBoardToRemove] = useState(null); + // Click vs drag detection const [clickStartTime, setClickStartTime] = useState(0); const [clickStartPos, setClickStartPos] = useState({ x: 0, y: 0 }); @@ -754,18 +760,23 @@ export const SimulatorCanvas = () => { return () => cleanups.forEach(fn => fn()); }, [components, wires, boards]); - // Handle keyboard delete + // Handle keyboard delete for components and boards useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if ((e.key === 'Delete' || e.key === 'Backspace') && selectedComponentId) { - removeComponent(selectedComponentId); - setSelectedComponentId(null); + if (e.key === 'Delete' || e.key === 'Backspace') { + if (selectedComponentId) { + removeComponent(selectedComponentId); + setSelectedComponentId(null); + } else if (activeBoardId && boards.length > 1) { + // Only allow deleting boards if more than one exists + setBoardToRemove(activeBoardId); + } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedComponentId, removeComponent]); + }, [selectedComponentId, removeComponent, activeBoardId, boards.length]); // Handle component selection from modal const handleSelectComponent = (metadata: ComponentMetadata) => { @@ -1400,6 +1411,11 @@ export const SimulatorCanvas = () => { setDraggedComponentId(`__board__:${board.id}`); setDragOffset({ x: world.x - board.x, y: world.y - board.y }); }} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + setBoardContextMenu({ boardId: board.id, x: e.clientX, y: e.clientY }); + }} onPinClick={handlePinClick} zoom={zoom} /> @@ -1478,6 +1494,101 @@ export const SimulatorCanvas = () => { }} /> + {/* Board right-click context menu */} + {boardContextMenu && (() => { + const board = boards.find((b) => b.id === boardContextMenu.boardId); + const label = board ? BOARD_KIND_LABELS[board.boardKind] : 'Board'; + const connectedWires = wires.filter( + (w) => w.start.componentId === boardContextMenu.boardId || w.end.componentId === boardContextMenu.boardId + ).length; + return ( + <> +
setBoardContextMenu(null)} + onContextMenu={(e) => { e.preventDefault(); setBoardContextMenu(null); }} + /> +
+
+ {label} +
+ +
+ + ); + })()} + + {/* Board removal confirmation dialog */} + {boardToRemove && (() => { + const board = boards.find((b) => b.id === boardToRemove); + const label = board ? BOARD_KIND_LABELS[board.boardKind] : 'Board'; + const connectedWires = wires.filter( + (w) => w.start.componentId === boardToRemove || w.end.componentId === boardToRemove + ).length; + return ( +
+
+

Remove {label}?

+

+ This will remove the board from the workspace + {connectedWires > 0 && <> and {connectedWires} connected wire{connectedWires > 1 ? 's' : ''}} + . This action cannot be undone. +

+
+ + +
+
+
+ ); + })()} +
); }; diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index 013ddea..e0d6feb 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -472,6 +472,7 @@ export const useSimulatorStore = create((set, get) => { }, removeBoard: (boardId: string) => { + const board = get().boards.find((b) => b.id === boardId); getBoardSimulator(boardId)?.stop(); simulatorMap.delete(boardId); pinManagerMap.delete(boardId); @@ -484,8 +485,16 @@ export const useSimulatorStore = create((set, get) => { const activeBoardId = s.activeBoardId === boardId ? (boards[0]?.id ?? null) : s.activeBoardId; - return { boards, activeBoardId }; + // Remove wires connected to this board + const wires = s.wires.filter((w) => + w.start.componentId !== boardId && w.end.componentId !== boardId + ); + return { boards, activeBoardId, wires }; }); + // Clean up file group in editor store + if (board) { + useEditorStore.getState().deleteFileGroup(board.activeFileGroupId); + } }, updateBoard: (boardId: string, updates: Partial) => {