From f09489f498d33e9118e011e34b6602b0c4eb1d32 Mon Sep 17 00:00:00 2001 From: David Montero Date: Tue, 18 Aug 2026 00:17:38 +0200 Subject: [PATCH] fix(editor): toolbar never overlaps on a narrow bar; layout switches live in the View menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things went wrong when the window was small and the AI chat docked: 1. Libraries / Output painted over the board selector. The editor zone has min-width: max-content, but its child .editor-toolbar-wrapper is an inline-size query container, and size containment makes it contribute zero intrinsic width — so the floor resolved to nothing, flexbox squeezed the zone under its buttons and the bar never got to wrap. Containment is now off for the in-bar mount (the wrapper keeps it for its standalone mobile mount); the two label collapses that depended on it are re-keyed to the unified-toolbar container at the same thresholds the canvas controls use (1010 / 905px), so the row degrades in step and, when it truly cannot fit, wraps instead. 2. The explorer / Code / Both / Circuit segmented toggle now hides once the shared bar is under 760px (its inline display moved to CSS so a container query can do that). Every switch it had is a checkmarked row in the View menu: File Explorer, and Code / Both / Circuit as a radio group under a View mode label. explorerOpen moves from EditorPage state into useEditorStore so the menu can read it. --- frontend/src/App.css | 27 ++++++++++ .../src/components/editor/EditorMenuBar.tsx | 49 ++++++++++++++++++- .../src/components/editor/EditorToolbar.css | 22 +++++++++ frontend/src/pages/EditorPage.tsx | 18 +++---- frontend/src/store/useEditorStore.ts | 9 ++++ 5 files changed, 115 insertions(+), 10 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index b8c3bf29..6ef5d8dc 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -188,12 +188,27 @@ body { /* Collapse the Code/Both/Circuit labels to icon-only first (biggest fixed block in the bar) as the shared bar narrows. */ +.view-mode-toggle { + display: flex; +} + @container unified-toolbar (max-width: 1140px) { .view-mode-toggle .vm-label { display: none; } } +/* Narrow bar (small window with the AI chat docked): drop the explorer / + Code / Both / Circuit toggle altogether. Every one of its switches is a + checkmarked row in the header's View menu, so nothing is lost — and the + ~140px it frees is what keeps the Run group and the canvas controls on + one line instead of overlapping. */ +@container unified-toolbar (max-width: 760px) { + .view-mode-toggle { + display: none; + } +} + .unified-toolbar-editor { flex: 1 1 auto; /* Floor the editor zone at its own content width so it can never shrink @@ -212,6 +227,18 @@ body { min-width: 0; } +/* The wrapper is an inline-size query container for its standalone (mobile) + mount. Inside the shared bar that containment is exactly what let the + editor zone shrink under its buttons: size containment makes the wrapper + contribute ZERO intrinsic width, so the zone's `min-width: max-content` + above resolved to nothing, flexbox squeezed it, and Libraries / Output + painted over the board selector instead of the bar wrapping. Turn it off + here — the label collapses are keyed to the `unified-toolbar` container + in EditorToolbar.css for this mount. */ +.unified-toolbar-editor .editor-toolbar-wrapper { + container-type: normal; +} + .unified-toolbar-editor .editor-toolbar { border-bottom: none; } diff --git a/frontend/src/components/editor/EditorMenuBar.tsx b/frontend/src/components/editor/EditorMenuBar.tsx index ba076baa..9f0a94ff 100644 --- a/frontend/src/components/editor/EditorMenuBar.tsx +++ b/frontend/src/components/editor/EditorMenuBar.tsx @@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import { useSimulatorStore } from '../../store/useSimulatorStore'; import { useOscilloscopeStore } from '../../store/useOscilloscopeStore'; +import { useEditorStore, type EditorViewMode } from '../../store/useEditorStore'; import { LOCALES, LOCALE_META, type Locale } from '../../i18n/config'; import { getLocaleFromPath, switchLocale } from '../../i18n/path'; import { @@ -78,6 +79,13 @@ export const EditorMenuBar: React.FC = () => { const toggleSerialMonitor = useSimulatorStore((s) => s.toggleSerialMonitor); const scopeOpen = useOscilloscopeStore((s) => s.open); const toggleOscilloscope = useOscilloscopeStore((s) => s.toggleOscilloscope); + // Layout rows: the same switches as the toolbar's explorer / Code / Both / + // Circuit toggle, which hides on a narrow bar (App.css) — the menu is then + // the only way to reach them, so they carry live checkmarks. + const explorerOpen = useEditorStore((s) => s.explorerOpen); + const toggleExplorer = useEditorStore((s) => s.toggleExplorer); + const viewMode = useEditorStore((s) => s.viewMode); + const setViewMode = useEditorStore((s) => s.setViewMode); const undo = useSimulatorStore((s) => s.undo); const redo = useSimulatorStore((s) => s.redo); const history = useSimulatorStore((s) => s.history); @@ -182,13 +190,14 @@ export const EditorMenuBar: React.FC = () => { // everything view-shaped lives in the View menu, like the desktop app. const editItems: Item[] = []; + // File Explorer is rendered as a checkmarked row in the View block below + // (it reads the store), not as a plain command here. const viewItems: Item[] = [ { kind: 'command', id: 'sim.compile', label: t('editor.menu.compile', 'Compile'), shortcut: 'Ctrl+B' }, { kind: 'command', id: 'sim.run', label: t('editor.menu.run', 'Run') }, { kind: 'command', id: 'sim.stop', label: t('editor.toolbar.stop', 'Stop') }, { kind: 'command', id: 'sim.resetBoard', label: t('editor.toolbar.reset', 'Reset') }, { kind: 'separator' }, - { kind: 'command', id: 'view.toggleExplorer', label: t('editor.menu.toggleExplorer', 'File Explorer') }, { kind: 'command', id: 'view.toggleConsole', label: t('editor.menu.toggleConsole', 'Output Console') }, { kind: 'separator' }, { kind: 'command', id: 'view.reset', label: t('editor.menu.centerView', 'Center canvas view') }, @@ -196,6 +205,12 @@ export const EditorMenuBar: React.FC = () => { { kind: 'command', id: 'view.zoomOut', label: t('editor.canvas.zoomOut', 'Zoom out') }, ]; + const layoutModes: { key: EditorViewMode; label: string }[] = [ + { key: 'code', label: t('editor.shell.code', 'Code') }, + { key: 'both', label: t('editor.shell.both', 'Both') }, + { key: 'circuit', label: t('editor.shell.circuit', 'Circuit') }, + ]; + const renderLink = (item: Extract): React.ReactNode => ( { {scopeOpen ? '✓' : ''}
+ {/* Layout: explorer pane + Code / Both / Circuit. Mirrors the + toolbar's segmented toggle, which App.css hides once the + shared bar gets narrow (small window + docked AI chat). */} +
{t('editor.shell.viewMode', 'View mode')}
+ + {layoutModes.map((m) => ( + + ))} +
)} {which === 'account' && ( diff --git a/frontend/src/components/editor/EditorToolbar.css b/frontend/src/components/editor/EditorToolbar.css index 28e81f81..5c068480 100644 --- a/frontend/src/components/editor/EditorToolbar.css +++ b/frontend/src/components/editor/EditorToolbar.css @@ -171,6 +171,28 @@ } } +/* Same two collapses when the toolbar rides inside the shared desktop bar + (App.css turns the wrapper's own containment off there so the bar can + wrap instead of overlap): key them to the BAR's width, at the thresholds + where the canvas controls drop their labels (1010px) and the board + selector shrinks (905px), so the whole row degrades in step. */ +@container unified-toolbar (max-width: 1010px) { + .tb-libraries-label { + display: none; + } + .tb-btn-libraries { + padding: 0 6px; + } +} +@container unified-toolbar (max-width: 905px) { + .tb-board-pill-label { + display: none; + } + .tb-board-pill { + padding: 3px 6px; + } +} + /* Import / Export moved to the File menu in the header (EditorMenuBar): their inline buttons and the responsive overflow twins are gone, which is ~70px the row no longer has to find on laptop widths. */ diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index 26b1480f..93962585 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -79,6 +79,9 @@ export const EditorPage: React.FC = () => { // Lets users hide a pane to give the right-docked chat more room. const viewMode = useEditorStore((s) => s.viewMode); const setViewMode = useEditorStore((s) => s.setViewMode); + const explorerOpen = useEditorStore((s) => s.explorerOpen); + const setExplorerOpen = useEditorStore((s) => s.setExplorerOpen); + const toggleExplorer = useEditorStore((s) => s.toggleExplorer); const containerRef = useRef(null); const resizingRef = useRef(false); const serialMonitorOpen = useSimulatorStore((s) => s.serialMonitorOpen); @@ -217,7 +220,6 @@ export const EditorPage: React.FC = () => { localStorage.setItem('velxio_star_clicked', '1'); setShowStarBanner(false); }; - const [explorerOpen, setExplorerOpen] = useState(true); const [explorerWidth, setExplorerWidth] = useState(EXPLORER_DEFAULT); const [isMobile, setIsMobile] = useState( () => window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches, @@ -256,7 +258,7 @@ export const EditorPage: React.FC = () => { update(mq); mq.addEventListener('change', update); return () => mq.removeEventListener('change', update); - }, []); + }, [setExplorerOpen]); // Ctrl+S shortcut useEffect(() => { @@ -277,15 +279,13 @@ export const EditorPage: React.FC = () => { const offNew = registerEditorCommand('project.new', () => { void handleNewClick(); }); - const offExplorer = registerEditorCommand('view.toggleExplorer', () => - setExplorerOpen((v) => !v), - ); + const offExplorer = registerEditorCommand('view.toggleExplorer', () => toggleExplorer()); return () => { offSave(); offNew(); offExplorer(); }; - }, [handleSaveClick, handleNewClick]); + }, [handleSaveClick, handleNewClick, toggleExplorer]); // Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z — canvas undo/redo. Skipped when the // user is typing in any input/textarea/contenteditable so the Monaco @@ -446,7 +446,7 @@ export const EditorPage: React.FC = () => { aria-label={t('editor.shell.viewMode')} className="view-mode-toggle" style={{ - display: 'flex', + // display comes from App.css (flex; none on a narrow bar or mobile). // Never squeezed below its buttons: flexbox used to crush // this block to a third of its width and clip two of them. flexShrink: 0, @@ -460,7 +460,7 @@ export const EditorPage: React.FC = () => { }} >