diff --git a/frontend/src/components/simulator/SimulatorCanvas.css b/frontend/src/components/simulator/SimulatorCanvas.css index 9d819ae..7c2178c 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.css +++ b/frontend/src/components/simulator/SimulatorCanvas.css @@ -183,7 +183,8 @@ } /* ── Zoom controls ───────────────────────────────── */ -.zoom-controls { +.zoom-controls, +.undo-controls { display: flex; align-items: center; gap: 2px; @@ -208,11 +209,16 @@ flex-shrink: 0; } -.zoom-btn:hover { +.zoom-btn:hover:not(:disabled) { background: #3a3a3a; color: #ccc; } +.zoom-btn:disabled { + opacity: 0.3; + cursor: default; +} + .zoom-level { min-width: 42px; text-align: center; @@ -279,6 +285,51 @@ background: rgba(255, 255, 255, 0.35); } +/* ── Wire selected banner (delete/deselect) ─────── */ +.wire-selected-banner { + position: absolute; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + z-index: 100; + background: rgba(60, 60, 70, 0.92); + color: #fff; + padding: 8px 16px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 12px; + font-size: 13px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + white-space: nowrap; + pointer-events: auto; +} + +.wire-selected-banner button { + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 4px; + padding: 4px 14px; + cursor: pointer; + font-size: 12px; + font-weight: 600; + background: rgba(255, 255, 255, 0.2); + transition: background 0.15s; +} + +.wire-selected-banner button:hover { + background: rgba(255, 255, 255, 0.35); +} + +.wire-selected-banner button.btn-danger { + background: rgba(220, 53, 69, 0.7); +} + +.wire-selected-banner button.btn-danger:hover { + background: rgba(220, 53, 69, 0.9); +} + /* ── Mobile-friendly adjustments ────────────────── */ @media (max-width: 768px) { .canvas-header { @@ -325,7 +376,8 @@ display: none; } - .wire-mode-banner { + .wire-mode-banner, + .wire-selected-banner { font-size: 12px; padding: 6px 12px; gap: 8px; diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index b95425a..af6297f 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -235,6 +235,8 @@ interface SimulatorState { wires: Wire[]; selectedWireId: string | null; wireInProgress: WireInProgress | null; + wireUndoStack: Wire[][]; + wireRedoStack: Wire[][]; addWire: (wire: Wire) => void; removeWire: (wireId: string) => void; updateWire: (wireId: string, updates: Partial) => void; @@ -248,6 +250,8 @@ interface SimulatorState { cancelWireCreation: () => void; updateWirePositions: (componentId: string) => void; recalculateAllWirePositions: () => void; + undoWire: () => void; + redoWire: () => void; // ── Serial monitor ────────────────────────────────────────────────────── toggleSerialMonitor: () => void; @@ -1085,6 +1089,8 @@ export const useSimulatorStore = create((set, get) => { ], selectedWireId: null, wireInProgress: null, + wireUndoStack: [], + wireRedoStack: [], addComponent: (component) => set((state) => ({ components: [...state.components, component] })), @@ -1114,14 +1120,22 @@ export const useSimulatorStore = create((set, get) => { setComponents: (components) => set({ components }), - addWire: (wire) => set((state) => ({ wires: [...state.wires, wire] })), + addWire: (wire) => set((state) => ({ + wireUndoStack: [...state.wireUndoStack, state.wires].slice(-50), + wireRedoStack: [], + wires: [...state.wires, wire], + })), removeWire: (wireId) => set((state) => ({ + wireUndoStack: [...state.wireUndoStack, state.wires].slice(-50), + wireRedoStack: [], wires: state.wires.filter((w) => w.id !== wireId), selectedWireId: state.selectedWireId === wireId ? null : state.selectedWireId, })), updateWire: (wireId, updates) => set((state) => ({ + wireUndoStack: [...state.wireUndoStack, state.wires].slice(-50), + wireRedoStack: [], wires: state.wires.map((w) => w.id === wireId ? { ...w, ...updates } : w), })), @@ -1173,11 +1187,38 @@ export const useSimulatorStore = create((set, get) => { waypoints, color, }; - set((state) => ({ wires: [...state.wires, newWire], wireInProgress: null })); + set((state) => ({ + wireUndoStack: [...state.wireUndoStack, state.wires].slice(-50), + wireRedoStack: [], + wires: [...state.wires, newWire], + wireInProgress: null, + })); }, cancelWireCreation: () => set({ wireInProgress: null }), + undoWire: () => set((state) => { + if (state.wireUndoStack.length === 0) return state; + const prev = state.wireUndoStack[state.wireUndoStack.length - 1]; + return { + wireUndoStack: state.wireUndoStack.slice(0, -1), + wireRedoStack: [...state.wireRedoStack, state.wires].slice(-50), + wires: prev, + selectedWireId: null, + }; + }), + + redoWire: () => set((state) => { + if (state.wireRedoStack.length === 0) return state; + const next = state.wireRedoStack[state.wireRedoStack.length - 1]; + return { + wireRedoStack: state.wireRedoStack.slice(0, -1), + wireUndoStack: [...state.wireUndoStack, state.wires].slice(-50), + wires: next, + selectedWireId: null, + }; + }), + updateWirePositions: (componentId) => { set((state) => { const component = state.components.find((c) => c.id === componentId);