/** * 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'; 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; } 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, }) => { const containerRef = useRef(null); const deleteRef = useRef(null); const rotateRef = useRef(null); const deselectRef = useRef(null); // 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); if (!kind) return null; return (
{label} {canRotate && onRotate && ( )}
); }; const noop = () => {}; const buttonStyle: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: 6, padding: '6px 10px', background: 'transparent', border: '1px solid transparent', borderRadius: 6, color: 'inherit', cursor: 'pointer', fontSize: 13, lineHeight: 1, minHeight: 36, // touch-friendly hit target }; const buttonLabelStyle: React.CSSProperties = { // Hidden on very narrow toolbars; kept visible by default for clarity };