From 0c935c5b29f74c1a9ce671203f7a1b530419e1a7 Mon Sep 17 00:00:00 2001 From: David Montero Date: Fri, 19 Jun 2026 19:35:46 +0200 Subject: [PATCH] feat(simulator): discoverable wire-color UI on desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changing a wire's color on desktop was keyboard-only (select wire + 0-9/c/l/m/p/y) with no visible control — the floating color palette (SelectionActionBar) only rendered on touch devices, so desktop users had no way to discover it. Now: - the wire SelectionActionBar (top-center, with the color palette) also shows on desktop when a wire is selected and the sim is stopped (it is pinned top-center so it never covers pins); component/board bars stay touch-only. - right-clicking a wire opens a context menu with the color swatches + delete. Keyboard shortcuts still work. Mirrors the existing board context-menu pattern. --- .../components/simulator/SimulatorCanvas.tsx | 144 ++++++++++++++++-- 1 file changed, 133 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 476995c1..75bb7f35 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -230,6 +230,12 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { x: number; y: number; } | null>(null); + // Right-click context menu for a wire (color swatches + delete). + const [wireContextMenu, setWireContextMenu] = useState<{ + wireId: string; + x: number; + y: number; + } | null>(null); // Board removal confirmation dialog const [boardToRemove, setBoardToRemove] = useState(null); // Board Options modal — id of the board whose options are being edited. @@ -2431,7 +2437,23 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { }} onContextMenu={(e) => { e.preventDefault(); - if (wireInProgress) cancelWireCreation(); + if (wireInProgress) { + cancelWireCreation(); + return; + } + // Right-click on a wire → open its color / delete context menu. + // Right-clicks on a board are handled by the board element's own + // onContextMenu (which stops propagation), so this only fires over + // empty canvas or a wire. Disabled while the simulation runs + // (canvas is interact-only then). + if (interactionRunning) return; + const world = toWorld(e.clientX, e.clientY); + const threshold = 8 / zoomRef.current; + const wire = findWireNearPoint(wiresRef.current, world.x, world.y, threshold); + if (wire) { + setSelectedWire(wire.id); + setWireContextMenu({ wireId: wire.id, x: e.clientX, y: e.clientY }); + } }} onClick={(e) => { if (wireInProgress) { @@ -2592,14 +2614,16 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { /> )} - {/* Floating action bar for the current selection — primary delete UI - for touch devices (no Delete key, no right-click). Hidden: - - while creating a wire (would fight the wire-mode banner) - - on desktop (delete is bound to the Delete key, rotate is in - the right-click menu — the floating bar covered nearby pins - and intercepted clicks on buttons during simulation) - - while the simulator is running (canvas is read-only). */} - {!wireInProgress && isTouchDevice && !interactionRunning && + {/* Floating action bar (top-center) for the current selection. Hidden + while creating a wire (fights the wire-mode banner) and while the + simulator is running (canvas is read-only). + - WIRE selection shows on BOTH desktop and touch: it carries the + color palette, which is otherwise only reachable on desktop via + the 0-9 / c,l,m,p,y keyboard shortcuts (not discoverable). The + bar is pinned top-center so it never covers pins near the wire. + - COMPONENT selection stays touch-only — desktop already has the + Delete key + the right-click rotate menu. */} + {!wireInProgress && !interactionRunning && (() => { if (selectedWireId) { const wire = wires.find((w) => w.id === selectedWireId); @@ -2613,7 +2637,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { recordUpdateWire(selectedWireId, { color: wire.color }, { color }); }} onDelete={() => { - // Recorded so the touch / mobile delete is also undoable. + // Recorded so the delete is also undoable. recordRemoveWire(selectedWireId); setSelectedWire(null); }} @@ -2621,7 +2645,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { /> ); } - if (selectedComponentId) { + if (isTouchDevice && selectedComponentId) { const c = components.find((x) => x.id === selectedComponentId); if (!c) return null; const meta = registry.getById(c.metadataId); @@ -2855,6 +2879,104 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { /> {/* Board right-click context menu */} + {wireContextMenu && + (() => { + const wire = wires.find((w) => w.id === wireContextMenu.wireId); + if (!wire) return null; + return ( + <> +
setWireContextMenu(null)} + onContextMenu={(e) => { + e.preventDefault(); + setWireContextMenu(null); + }} + /> +
+
+ {t('editor.selectionBar.changeColor')} +
+
+ {Object.values(WIRE_KEY_COLORS).map((color) => ( +
+ +
+ + ); + })()} + {boardContextMenu && (() => { const board = boards.find((b) => b.id === boardContextMenu.boardId);