fix(editor): toolbar never overlaps on a narrow bar; layout switches live in the View menu

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.
This commit is contained in:
David Montero 2026-08-18 00:17:38 +02:00
parent 51b775997a
commit f09489f498
5 changed files with 115 additions and 10 deletions

View File

@ -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;
}

View File

@ -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<Item, { kind: 'link' }>): React.ReactNode => (
<a
key={item.href}
@ -269,6 +284,38 @@ export const EditorMenuBar: React.FC = () => {
<span className="emb-shortcut">{scopeOpen ? '✓' : ''}</span>
</button>
<div className="emb-separator" />
{/* 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). */}
<div className="emb-section-label">{t('editor.shell.viewMode', 'View mode')}</div>
<button
role="menuitemcheckbox"
aria-checked={explorerOpen}
className="emb-item"
onClick={() => {
setOpen(null);
toggleExplorer();
}}
>
<span>{t('editor.menu.toggleExplorer', 'File Explorer')}</span>
<span className="emb-shortcut">{explorerOpen ? '✓' : ''}</span>
</button>
{layoutModes.map((m) => (
<button
key={m.key}
role="menuitemradio"
aria-checked={viewMode === m.key}
className="emb-item"
onClick={() => {
setOpen(null);
setViewMode(m.key);
}}
>
<span>{m.label}</span>
<span className="emb-shortcut">{viewMode === m.key ? '✓' : ''}</span>
</button>
))}
<div className="emb-separator" />
</>
)}
{which === 'account' && (

View File

@ -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. */

View File

@ -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<HTMLDivElement>(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 = () => {
}}
>
<button
onClick={() => setExplorerOpen((v) => !v)}
onClick={() => toggleExplorer()}
aria-pressed={explorerOpen}
title={explorerOpen ? t('editor.menu.hideExplorer', 'Hide file explorer') : t('editor.menu.showExplorer', 'Show file explorer')}
style={{
@ -663,7 +663,7 @@ export const EditorPage: React.FC = () => {
<div style={{ display: 'flex', alignItems: 'stretch', flexShrink: 0 }}>
<button
className="explorer-toggle-btn"
onClick={() => setExplorerOpen((v) => !v)}
onClick={() => toggleExplorer()}
title={explorerOpen ? t('editor.menu.hideExplorer', 'Hide file explorer') : t('editor.menu.showExplorer', 'Show file explorer')}
>
<svg

View File

@ -152,6 +152,12 @@ interface EditorState {
fontSize: number;
viewMode: EditorViewMode;
setViewMode: (mode: EditorViewMode) => void;
/** Desktop file-explorer pane. Lives in the store (not EditorPage state) so
* the header's View menu can show its checkmark and toggle it — the
* toolbar's own segmented toggle hides on narrow bars. */
explorerOpen: boolean;
setExplorerOpen: (open: boolean) => void;
toggleExplorer: () => void;
// ── File groups (one per board) ──────────────────────────────────────────
/** Map of groupId → WorkspaceFile[]. Stored as plain object for Zustand. */
@ -229,6 +235,9 @@ export const useEditorStore = create<EditorState>((set, get) => ({
fontSize: 14,
viewMode: 'both',
setViewMode: (mode) => set({ viewMode: mode }),
explorerOpen: true,
setExplorerOpen: (open) => set({ explorerOpen: open }),
toggleExplorer: () => set((s) => ({ explorerOpen: !s.explorerOpen })),
// File groups — initial state has one group for the default Arduino Uno board
fileGroups: {