feat(editor): toolbar rides inside the header — one row, not two

With the marketing nav gone from the editor header, its middle was ~700px
of dead space at 1280 while the toolbar occupied a whole second 38px bar.
The unified toolbar strip now fills that middle through a new AppHeader
editorToolbar slot: one 44px row where there used to be 44+38 — a full
row returned to the code and the canvas.

The strip keeps its own class names, so everything that keyed on them
keeps working untouched: the container queries, the docked-chat
padding-right, and the internal flex-wrap. When it truly cannot fit, the
strip wraps and the header grows (height: auto on the modifier class)
instead of clipping or overlapping; brand and the right-side controls
stay pinned to the first line. Inside the header the strip drops its own
background and border so it reads as one bar, not a box within a bar.

Mobile keeps the previous layout (no strip; the mobile tab bar remains).
This commit is contained in:
David Montero Crespo 2026-07-30 18:56:24 +02:00
parent b70bb8ab83
commit ffb4fc0159
3 changed files with 128 additions and 58 deletions

View File

@ -477,3 +477,56 @@ body {
@media (prefers-reduced-motion: reduce) {
.velxio-ants { animation: none; }
}
/* Editor: toolbar riding inside the header
With the marketing nav gone from the editor header, its middle was dead
space while the toolbar occupied a whole second bar. The strip now fills
that middle: one 44px row instead of 44+38 a full row returned to the
code/canvas. The strip keeps its own class names (container queries, the
docked-chat padding-right) and wraps internally when it truly cannot
fit; the header then grows instead of clipping or overlapping. */
.app-header--with-toolbar {
height: auto;
min-height: 44px;
}
.header-editor-toolbar {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-self: stretch;
margin: 0 10px;
}
.header-editor-toolbar .unified-toolbar {
flex: 1 1 auto;
min-width: 0;
/* Skin: it is part of the header now, not a bar of its own. */
background: transparent;
border-bottom: none;
min-height: 44px;
align-items: center;
}
/* Keep the brand block and the right-side controls pinned to the first
line when the strip wraps the header taller. */
.app-header--with-toolbar .header-left,
.app-header--with-toolbar .header-right {
align-self: flex-start;
min-height: 44px;
display: flex;
align-items: center;
}
.app-header--with-toolbar .header-content {
align-items: stretch;
}
/* The editor zone / toolbar inside the strip: no own chrome, blend into
the header instead of painting a nested dark box with its own border. */
.header-editor-toolbar .editor-toolbar,
.header-editor-toolbar .unified-toolbar-editor .editor-toolbar {
background: transparent;
border-bottom: none;
height: 44px;
}

View File

@ -22,6 +22,11 @@ interface AppHeaderProps {
* toolbar is starved of on small screens; the logo still links home.
* Same mechanism the Tauri desktop build uses (VITE_DESKTOP). */
editorMenu?: React.ReactNode;
/** Editor variant: the unified toolbar strip rendered in the header's
* middle the space the marketing nav used to occupy. One row instead
* of header + toolbar stacked; the strip wraps internally when narrow
* and the header grows to fit (height: auto on the modifier class). */
editorToolbar?: React.ReactNode;
}
const SAVE_STATUS_COPY: Record<AutoSaveState['status'], { label: string; color: string }> = {
@ -68,7 +73,7 @@ const AutoSaveIndicator: React.FC<{ state: AutoSaveState }> = ({ state }) => {
);
};
export const AppHeader: React.FC<AppHeaderProps> = ({ autoSave, editorMenu }) => {
export const AppHeader: React.FC<AppHeaderProps> = ({ autoSave, editorMenu, editorToolbar }) => {
const location = useLocation();
const currentProject = useProjectStore((s) => s.currentProject);
const [menuOpen, setMenuOpen] = useState(false);
@ -98,7 +103,7 @@ export const AppHeader: React.FC<AppHeaderProps> = ({ autoSave, editorMenu }) =>
location.pathname === localize(path) ? ' header-nav-link-active' : '';
return (
<header className="app-header">
<header className={"app-header" + (editorToolbar ? ' app-header--with-toolbar' : '')}>
<div className="header-content">
<div className="header-left">
{/* Brand */}
@ -216,6 +221,9 @@ export const AppHeader: React.FC<AppHeaderProps> = ({ autoSave, editorMenu }) =>
)}
</div>
{/* Editor toolbar strip — fills the middle the nav vacated. */}
{editorToolbar && <div className="header-editor-toolbar">{editorToolbar}</div>}
{/* Right: language + share + auth + mobile hamburger */}
<div className="header-right">
<LanguageSwitcher />

View File

@ -376,62 +376,16 @@ export const EditorPage: React.FC = () => {
[explorerWidth],
);
return (
<div className="app">
<AppHeader autoSave={autoSave} editorMenu={!isMobile ? <EditorMenuBar /> : undefined} />
{/* ── Mobile tab bar (top, above panels) ── */}
{isMobile && (
<nav className="mobile-tab-bar">
<button
className={`mobile-tab-btn${mobileView === 'code' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('code')}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
<span>&lt;/&gt; {t('editor.shell.code')}</span>
</button>
<button
className={`mobile-tab-btn${mobileView === 'circuit' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('circuit')}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="2" y="7" width="20" height="14" rx="2" />
<path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
<line x1="12" y1="12" x2="12" y2="16" />
<line x1="10" y1="14" x2="14" y2="14" />
</svg>
<span>{t('editor.shell.circuit')}</span>
</button>
</nav>
)}
{/* Unified top toolbar (desktop only)
Editor controls + canvas controls share a single full-width row so
the bar doesn't reflow when the editor/canvas splitter is dragged.
The canvas controls (board selector, Serial, Scope, zoom, Add) are
portaled into `canvasHeaderSlot` from inside SimulatorCanvas. */}
{!isMobile && (
/* Unified toolbar (desktop)
Editor controls + canvas controls in one strip; the canvas side is
portaled into `canvasHeaderSlot` from SimulatorCanvas. Since the
marketing nav left the editor header, the header's middle is empty
space so this strip now rides INSIDE the AppHeader row (via the
editorToolbar prop) instead of being a second bar: one 44px row where
there used to be 44+38. On narrow widths the strip wraps internally
and the header grows; the docked AI chat is avoided by the same
padding-right the strip always had. */
const unifiedToolbar = !isMobile ? (
<div className="unified-toolbar">
<button
className="explorer-toggle-btn unified-toolbar-explorer-toggle"
@ -524,8 +478,63 @@ export const EditorPage: React.FC = () => {
</div>
<div className="unified-toolbar-canvas" ref={setCanvasHeaderSlot} />
</div>
) : undefined;
return (
<div className="app">
<AppHeader
autoSave={autoSave}
editorMenu={!isMobile ? <EditorMenuBar /> : undefined}
editorToolbar={unifiedToolbar}
/>
{/* ── Mobile tab bar (top, above panels) ── */}
{isMobile && (
<nav className="mobile-tab-bar">
<button
className={`mobile-tab-btn${mobileView === 'code' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('code')}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
<span>&lt;/&gt; {t('editor.shell.code')}</span>
</button>
<button
className={`mobile-tab-btn${mobileView === 'circuit' ? ' mobile-tab-btn--active' : ''}`}
onClick={() => setMobileView('circuit')}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="2" y="7" width="20" height="14" rx="2" />
<path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
<line x1="12" y1="12" x2="12" y2="16" />
<line x1="10" y1="14" x2="14" y2="14" />
</svg>
<span>{t('editor.shell.circuit')}</span>
</button>
</nav>
)}
<div className="app-container" ref={containerRef}>
{/* ── Editor side ── */}
<div