/** * SelectionActionBar * * Floating toolbar pinned to the top-center of the simulator canvas. Visible * whenever the user has something selected (wire / component / board). Gives * touch users a clear way to delete or rotate a selection without keyboard * shortcuts or right-click — the only way these actions were available before. * * Renders nothing when nothing is selected. * * Touch propagation note: the simulator canvas binds *native* touch listeners * with `addEventListener` and calls `preventDefault()` on touchend, which * suppresses the synthetic `click` event for buttons rendered above the * canvas. We therefore call `stopPropagation()` on the *native* touchstart * (React's synthetic stopPropagation does not affect native listeners) and * also wire each button to fire on `touchend` directly so taps work. */ import React, { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { WIRE_KEY_COLORS } from '../../utils/wireUtils'; export type SelectionKind = 'wire' | 'component' | 'board'; interface SelectionActionBarProps { kind: SelectionKind | null; /** Human label, e.g. "Wire", "Arduino Uno", "LED" */ label: string; /** Show the rotate button (components only). */ canRotate?: boolean; onDelete: () => void; onRotate?: () => void; onDeselect: () => void; onColorChange?: (color: string) => void; currentColor?: string; } const ICON_SIZE = 16; const TrashIcon: React.FC = () => ( ); const RotateIcon: React.FC = () => ( ); const CloseIcon: React.FC = () => ( ); /** * Bind native touch listeners to a button so taps fire the action even when * the simulator canvas (with its own native `addEventListener('touchend')` * + preventDefault) would otherwise swallow the synthetic click. * * Strategy: stopPropagation on the native touchstart so the canvas listener * never sees the event, and call the handler directly on touchend. This * runs as a layout effect so the handler closure is current after every * render — no ref-during-render hacks needed. */ function useTouchSafeAction( ref: React.RefObject, handler: () => void, ) { useEffect(() => { const el = ref.current; if (!el) return; const stopNative = (e: Event) => e.stopPropagation(); const onTouchEndNative = (e: TouchEvent) => { e.stopPropagation(); e.preventDefault(); handler(); }; el.addEventListener('touchstart', stopNative, { passive: false }); el.addEventListener('touchend', onTouchEndNative, { passive: false }); return () => { el.removeEventListener('touchstart', stopNative); el.removeEventListener('touchend', onTouchEndNative); }; }, [ref, handler]); } export const SelectionActionBar: React.FC = ({ kind, label, canRotate, onDelete, onRotate, onDeselect, onColorChange, currentColor, }) => { const { t } = useTranslation(); const [showPalette, setShowPalette] = React.useState(false); const containerRef = useRef(null); const deleteRef = useRef(null); const rotateRef = useRef(null); const deselectRef = useRef(null); const colorToggleRef = useRef(null); // Reset the palette when the selection kind or colour-change handler changes // (i.e. the user switched from one wire to another, or deselected). useEffect(() => { setShowPalette(false); }, [kind, onColorChange]); // Stop native touch events from reaching the canvas listener so it can't // call preventDefault on touchend (which would kill the synthetic click). useEffect(() => { const el = containerRef.current; if (!el) return; const stop = (e: Event) => e.stopPropagation(); el.addEventListener('touchstart', stop, { passive: false }); el.addEventListener('touchmove', stop, { passive: false }); el.addEventListener('touchend', stop, { passive: false }); el.addEventListener('mousedown', stop); return () => { el.removeEventListener('touchstart', stop); el.removeEventListener('touchmove', stop); el.removeEventListener('touchend', stop); el.removeEventListener('mousedown', stop); }; }, [kind]); useTouchSafeAction(deleteRef, onDelete); useTouchSafeAction(rotateRef, onRotate ?? noop); useTouchSafeAction(deselectRef, onDeselect); useTouchSafeAction(colorToggleRef, () => setShowPalette(!showPalette)); if (!kind) return null; return (
e.stopPropagation()} onClick={(e) => e.stopPropagation()} style={{ position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)', display: 'flex', alignItems: 'center', gap: 6, background: '#252526', border: '1px solid #3c3c3c', borderRadius: 8, padding: '6px 8px', boxShadow: '0 4px 16px rgba(0,0,0,0.5)', zIndex: 50, fontSize: 13, color: '#e0e0e0', pointerEvents: 'auto', touchAction: 'none', }} > {label} {kind === 'wire' && onColorChange && (
{showPalette && (
{Object.values(WIRE_KEY_COLORS).map((color) => ( { onColorChange(color); setShowPalette(false); }} /> ))}
)}
)} {canRotate && onRotate && ( )}
); }; const ColorButton: React.FC<{ color: string; isSelected: boolean; onClick: () => void; }> = ({ color, isSelected, onClick }) => { const ref = useRef(null); useTouchSafeAction(ref, onClick); return (