diff --git a/frontend/src/components/editor/EditorToolbar.css b/frontend/src/components/editor/EditorToolbar.css index 51ef13f4..9f8dd7fa 100644 --- a/frontend/src/components/editor/EditorToolbar.css +++ b/frontend/src/components/editor/EditorToolbar.css @@ -459,6 +459,62 @@ color: var(--color-fg-default); } +/* ── Run split-button (multi-board: primary Run = all boards + menu) ──── */ +.tb-run-split { + position: relative; + display: inline-flex; + align-items: center; +} +.tb-btn-run-caret { + width: 16px; + color: var(--color-feedback-success); +} +.tb-btn-run-caret:hover:not(:disabled) { + background: rgba(48, 209, 88, 0.14); + color: var(--color-feedback-success); +} +.tb-run-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + z-index: 200; + background: #2a2a2c; + border: 1px solid #3a3a3c; + border-radius: 10px; + padding: 6px; + min-width: 200px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + display: flex; + flex-direction: column; + gap: 2px; +} +.tb-run-menu-item { + display: flex; + align-items: center; + width: 100%; + padding: 8px 10px; + background: transparent; + border: none; + border-radius: 7px; + color: #ccc; + font-size: 13px; + font-family: inherit; + cursor: pointer; + text-align: left; + white-space: nowrap; + transition: + background 0.12s, + color 0.12s; +} +.tb-run-menu-item:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.08); + color: #fff; +} +.tb-run-menu-item:disabled { + opacity: 0.5; + cursor: default; +} + .tb-overflow-label { flex: 1; text-align: left; diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index e380f9f1..d9a2a623 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -196,6 +196,10 @@ export const EditorToolbar = ({ // Run-All / Stop buttons (the flat `running` flag only tracks the ACTIVE // board, so it misreports a multi-board or non-active-board run). const anyBoardRunning = boards.some((b) => b.running); + // Multi-board: the primary Run button runs ALL boards (the whole wired + // project is one system — running a subset is almost never intended), with a + // split-menu to still run just the active board. Single-board is unchanged. + const isMultiBoard = boards.length > 1; // A "run target" is a board OR a programmable custom-chip (a CPU that runs a // ROM). When there is more than one target — two boards, a board + a chip, or @@ -252,6 +256,9 @@ export const EditorToolbar = ({ const [missingLibHint, setMissingLibHint] = useState(false); const [moreMenuOpen, setMoreMenuOpen] = useState(false); const moreMenuRef = useRef(null); + // Split-button menu for the multi-board Run control ("Run all" / "Run active only"). + const [runMenuOpen, setRunMenuOpen] = useState(false); + const runMenuRef = useRef(null); // Open the Library Manager when another component (e.g. the velxio.json entry // in the FileExplorer) asks for it via a window event. Avoids prop-drilling @@ -301,6 +308,25 @@ export const EditorToolbar = ({ }; }, [moreMenuOpen]); + // Close the Run split-menu on outside click / Escape (mirrors the more-menu). + useEffect(() => { + if (!runMenuOpen) return; + const onClickOutside = (e: MouseEvent) => { + if (runMenuRef.current && !runMenuRef.current.contains(e.target as Node)) { + setRunMenuOpen(false); + } + }; + const onEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') setRunMenuOpen(false); + }; + document.addEventListener('mousedown', onClickOutside); + document.addEventListener('keydown', onEsc); + return () => { + document.removeEventListener('mousedown', onClickOutside); + document.removeEventListener('keydown', onEsc); + }; + }, [runMenuOpen]); + // Compile All / Run All — runs sequentially, logs to console (no dialog) const [compileAllRunning, setCompileAllRunning] = useState(false); @@ -1461,49 +1487,109 @@ export const EditorToolbar = ({
- {/* Run */} - )} - + {isMultiBoard && runMenuOpen && ( +
+ + +
+ )} +
{/* Stop */} - {/* Run All */} - + {/* Run All — only when the primary Run isn't already the + "run all boards" action (i.e. board + chip or chips-only + projects). For 2+ boards the split Run button covers it. */} + {!isMultiBoard && ( + + )} )} diff --git a/frontend/src/i18n/locales/en/common2.json b/frontend/src/i18n/locales/en/common2.json index 43591003..45636a1a 100644 --- a/frontend/src/i18n/locales/en/common2.json +++ b/frontend/src/i18n/locales/en/common2.json @@ -11,7 +11,9 @@ "run": { "addBoard": "Add a board to run", "runMicropython": "Run MicroPython", - "run": "Run (auto-compiles if needed)" + "run": "Run (auto-compiles if needed)", + "runActiveOnly": "Run only {{name}}", + "options": "Run options" }, "stop": "Stop", "reset": "Reset", diff --git a/frontend/src/i18n/locales/es/common2.json b/frontend/src/i18n/locales/es/common2.json index 64710db6..e32ea7ad 100644 --- a/frontend/src/i18n/locales/es/common2.json +++ b/frontend/src/i18n/locales/es/common2.json @@ -11,7 +11,9 @@ "run": { "addBoard": "Añadir una placa para ejecutar", "runMicropython": "Ejecutar MicroPython", - "run": "Ejecutar (compila automáticamente si es necesario)" + "run": "Ejecutar (compila automáticamente si es necesario)", + "runActiveOnly": "Ejecutar solo {{name}}", + "options": "Opciones de ejecución" }, "stop": "Detener", "reset": "Reiniciar",