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:
parent
b70bb8ab83
commit
ffb4fc0159
|
|
@ -477,3 +477,56 @@ body {
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.velxio-ants { animation: none; }
|
.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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ interface AppHeaderProps {
|
||||||
* toolbar is starved of on small screens; the logo still links home.
|
* toolbar is starved of on small screens; the logo still links home.
|
||||||
* Same mechanism the Tauri desktop build uses (VITE_DESKTOP). */
|
* Same mechanism the Tauri desktop build uses (VITE_DESKTOP). */
|
||||||
editorMenu?: React.ReactNode;
|
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 }> = {
|
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 location = useLocation();
|
||||||
const currentProject = useProjectStore((s) => s.currentProject);
|
const currentProject = useProjectStore((s) => s.currentProject);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
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' : '';
|
location.pathname === localize(path) ? ' header-nav-link-active' : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="app-header">
|
<header className={"app-header" + (editorToolbar ? ' app-header--with-toolbar' : '')}>
|
||||||
<div className="header-content">
|
<div className="header-content">
|
||||||
<div className="header-left">
|
<div className="header-left">
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
|
|
@ -216,6 +221,9 @@ export const AppHeader: React.FC<AppHeaderProps> = ({ autoSave, editorMenu }) =>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Editor toolbar strip — fills the middle the nav vacated. */}
|
||||||
|
{editorToolbar && <div className="header-editor-toolbar">{editorToolbar}</div>}
|
||||||
|
|
||||||
{/* Right: language + share + auth + mobile hamburger */}
|
{/* Right: language + share + auth + mobile hamburger */}
|
||||||
<div className="header-right">
|
<div className="header-right">
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|
|
||||||
|
|
@ -376,62 +376,16 @@ export const EditorPage: React.FC = () => {
|
||||||
[explorerWidth],
|
[explorerWidth],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
/* ── Unified toolbar (desktop) ──
|
||||||
<div className="app">
|
Editor controls + canvas controls in one strip; the canvas side is
|
||||||
<AppHeader autoSave={autoSave} editorMenu={!isMobile ? <EditorMenuBar /> : undefined} />
|
portaled into `canvasHeaderSlot` from SimulatorCanvas. Since the
|
||||||
|
marketing nav left the editor header, the header's middle is empty
|
||||||
{/* ── Mobile tab bar (top, above panels) ── */}
|
space — so this strip now rides INSIDE the AppHeader row (via the
|
||||||
{isMobile && (
|
editorToolbar prop) instead of being a second bar: one 44px row where
|
||||||
<nav className="mobile-tab-bar">
|
there used to be 44+38. On narrow widths the strip wraps internally
|
||||||
<button
|
and the header grows; the docked AI chat is avoided by the same
|
||||||
className={`mobile-tab-btn${mobileView === 'code' ? ' mobile-tab-btn--active' : ''}`}
|
padding-right the strip always had. */
|
||||||
onClick={() => setMobileView('code')}
|
const unifiedToolbar = !isMobile ? (
|
||||||
>
|
|
||||||
<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></> {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 && (
|
|
||||||
<div className="unified-toolbar">
|
<div className="unified-toolbar">
|
||||||
<button
|
<button
|
||||||
className="explorer-toggle-btn unified-toolbar-explorer-toggle"
|
className="explorer-toggle-btn unified-toolbar-explorer-toggle"
|
||||||
|
|
@ -524,8 +478,63 @@ export const EditorPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="unified-toolbar-canvas" ref={setCanvasHeaderSlot} />
|
<div className="unified-toolbar-canvas" ref={setCanvasHeaderSlot} />
|
||||||
</div>
|
</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></> {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}>
|
<div className="app-container" ref={containerRef}>
|
||||||
{/* ── Editor side ── */}
|
{/* ── Editor side ── */}
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue