feat(editor): enhance toolbar layout with center slot for file tabs and improve responsiveness

This commit is contained in:
David Montero Crespo 2026-04-28 23:23:20 -03:00
parent 7a26d01965
commit b39041ca4c
4 changed files with 173 additions and 40 deletions

View File

@ -1,45 +1,89 @@
/* ── Toolbar container ──────────────────────────── */
/* Toolbar container
* Single-row layout: [left actions] [file tabs flex] [right actions]
* The center slot (file tabs) flex-grows and scrolls horizontally so the
* action icons on either side stay pinned and visible no matter how narrow
* the editor pane gets when the user drags the editor/canvas divider.
*
* Uses CSS container queries so the Libraries label collapses based on the
* toolbar's *own* width (not the viewport) which is what changes when the
* user drags the editor/canvas divider.
*/
.editor-toolbar-wrapper {
container-type: inline-size;
container-name: editor-toolbar;
}
.editor-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
height: 46px;
align-items: stretch;
justify-content: flex-start;
padding: 0 6px;
height: 38px;
background: #252526;
border-bottom: 1px solid #333;
flex-shrink: 0;
gap: 8px;
gap: 4px;
min-width: 0;
}
/* ── Groups ─────────────────────────────────────── */
.toolbar-group {
display: flex;
align-items: center;
gap: 2px;
gap: 1px;
flex-shrink: 0;
}
.toolbar-group-right {
margin-left: auto;
gap: 6px;
gap: 4px;
}
/* Center slot fills the gap between left/right groups, allows file tabs
to scroll horizontally inside it without pushing the icons off-screen. */
.toolbar-center-slot {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: stretch;
overflow: hidden;
}
.toolbar-center-slot > .file-tabs {
flex: 1;
min-width: 0;
border-bottom: none;
background: transparent;
min-height: 0;
height: 100%;
}
.toolbar-center-slot .file-tab {
height: 100%;
padding: 0 8px;
min-width: 70px;
max-width: 160px;
font-size: 11.5px;
}
.toolbar-center-slot .file-tab-active {
background: #1e1e1e;
border-top: 2px solid #4fc3f7;
}
/* ── Divider ─────────────────────────────────────── */
.tb-divider {
width: 1px;
height: 20px;
height: 18px;
background: #3a3a3a;
margin: 0 4px;
margin: 0 3px;
align-self: center;
}
/* ── Icon buttons ───────────────────────────────── */
.tb-btn {
width: 38px;
height: 38px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 5px;
border-radius: 4px;
cursor: pointer;
background: transparent;
color: #9d9d9d;
@ -47,6 +91,7 @@
background 0.12s,
color 0.12s;
flex-shrink: 0;
align-self: center;
}
.tb-btn:disabled {
@ -94,14 +139,15 @@
.tb-btn-libraries {
display: flex;
align-items: center;
gap: 5px;
padding: 4px 10px 4px 8px;
gap: 4px;
height: 26px;
padding: 0 8px 0 7px;
border: 1px solid rgba(0, 184, 212, 0.3);
border-radius: 6px;
border-radius: 5px;
cursor: pointer;
background: rgba(0, 184, 212, 0.08);
color: #00e5ff;
font-size: 12px;
font-size: 11px;
font-weight: 500;
font-family: inherit;
white-space: nowrap;
@ -110,6 +156,7 @@
border-color 0.15s,
color 0.15s;
flex-shrink: 0;
align-self: center;
}
.tb-btn-libraries:hover {
background: rgba(0, 184, 212, 0.18);
@ -119,6 +166,27 @@
.tb-libraries-label {
line-height: 1;
}
/* Hide the label when the toolbar is narrow icon alone keeps the button
reachable without stealing horizontal space from the file tabs. The
container query reacts to the toolbar's own width, so it triggers when
the user drags the editor/canvas divider, not just on small viewports. */
@container editor-toolbar (max-width: 600px) {
.tb-libraries-label {
display: none;
}
.tb-btn-libraries {
padding: 0 6px;
}
}
/* When even narrower, also collapse the board pill label. */
@container editor-toolbar (max-width: 460px) {
.tb-board-pill-label {
display: none;
}
.tb-board-pill {
padding: 3px 6px;
}
}
/* Legacy lib icon-only button (for overflow items) */
.tb-btn-lib {

View File

@ -28,6 +28,13 @@ interface EditorToolbarProps {
setConsoleOpen: (open: boolean | ((v: boolean) => boolean)) => void;
compileLogs: CompilationLog[];
setCompileLogs: (logs: CompilationLog[] | ((prev: CompilationLog[]) => CompilationLog[])) => void;
/**
* Optional element rendered between the left action group and the right
* action group. The editor passes <FileTabs /> here so the tabs share the
* same row as the toolbar keeping every action icon pinned and visible
* regardless of how narrow the editor pane gets.
*/
centerSlot?: React.ReactNode;
}
const BOARD_PILL_ICON: Record<BoardKind, string> = {
@ -57,6 +64,7 @@ export const EditorToolbar = ({
setConsoleOpen,
compileLogs: _compileLogs,
setCompileLogs,
centerSlot,
}: EditorToolbarProps) => {
const { files, codeChangedSinceLastCompile, markCompiled } = useEditorStore();
const {
@ -613,8 +621,8 @@ export const EditorToolbar = ({
>
{compiling ? (
<svg
width="22"
height="22"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@ -627,8 +635,8 @@ export const EditorToolbar = ({
</svg>
) : (
<svg
width="22"
height="22"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@ -656,7 +664,7 @@ export const EditorToolbar = ({
: 'Run (auto-compiles if needed)'
}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<polygon points="5,3 19,12 5,21" />
</svg>
</button>
@ -668,7 +676,7 @@ export const EditorToolbar = ({
className="tb-btn tb-btn-stop"
title="Stop"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<rect x="3" y="3" width="18" height="18" rx="2" />
</svg>
</button>
@ -681,8 +689,8 @@ export const EditorToolbar = ({
title="Reset"
>
<svg
width="22"
height="22"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@ -707,8 +715,8 @@ export const EditorToolbar = ({
title="Compile all boards"
>
<svg
width="22"
height="22"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@ -728,7 +736,7 @@ export const EditorToolbar = ({
className="tb-btn tb-btn-run-all"
title="Run all boards"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<polygon points="3,3 11,12 3,21" />
<polygon points="13,3 21,12 13,21" />
</svg>
@ -737,6 +745,9 @@ export const EditorToolbar = ({
)}
</div>
{/* Center slot — file tabs share the row so action icons stay pinned. */}
{centerSlot && <div className="toolbar-center-slot">{centerSlot}</div>}
<div className="toolbar-group toolbar-group-right">
{/* Hidden file input for import (always present) */}
<input
@ -765,8 +776,8 @@ export const EditorToolbar = ({
title="Search and install Arduino libraries"
>
<svg
width="18"
height="18"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@ -788,7 +799,7 @@ export const EditorToolbar = ({
className={`tb-btn tb-btn-overflow${overflowOpen ? ' tb-btn-overflow-active' : ''}`}
title="Import / Export"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none">
<circle cx="5" cy="12" r="2" />
<circle cx="12" cy="12" r="2" />
<circle cx="19" cy="12" r="2" />
@ -880,8 +891,8 @@ export const EditorToolbar = ({
title="Toggle Output Console"
>
<svg
width="22"
height="22"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"

View File

@ -66,16 +66,67 @@ const COMP_DEFS: Record<string, CompDef> = {
'wokwi-servo': { svg: 'servo.svg', w: 170, h: 120 },
};
// ── Inline ATtiny85 SVG (no wokwi-element exists for this DIP-8 chip) ───────
// Small schematic-style DIP-8 so multi-board ATtiny85 examples render in the
// gallery preview. Sizes match the canvas board size (160×100 — see
// `BoardOnCanvas.tsx::BOARD_SIZE.attiny85`) so wire centres land on the chip.
const Attiny85InlinePreview: React.FC<{ w: number; h: number }> = ({ w, h }) => (
<svg width={w} height={h} viewBox="0 0 160 100" xmlns="http://www.w3.org/2000/svg">
{/* Pin stubs (left + right, 4 each) */}
{[20, 40, 60, 80].map((py) => (
<g key={py}>
<line x1="30" y1={py} x2="2" y2={py} stroke="#aaa" strokeWidth="3" strokeLinecap="round" />
<line
x1="130"
y1={py}
x2="158"
y2={py}
stroke="#aaa"
strokeWidth="3"
strokeLinecap="round"
/>
</g>
))}
{/* IC body */}
<rect x="30" y="10" width="100" height="80" rx="4" fill="#1a1a2e" stroke="#4a4a7a" strokeWidth="1.5" />
<path d="M73 10 A7 7 0 0 1 87 10" fill="none" stroke="#4a4a7a" strokeWidth="1.5" />
<text x="80" y="48" fontSize="10" fontWeight="bold" fontFamily="monospace" fill="#c8c8f0" textAnchor="middle">
ATtiny85
</text>
<text x="80" y="62" fontSize="8" fontFamily="monospace" fill="#7a7aaa" textAnchor="middle">
8-bit AVR
</text>
</svg>
);
// Board SVG definitions — used when the board is implicit (pico / esp32 pattern)
// or when a multi-board example references the board by `boardKind`.
//
// IMPORTANT: every entry from `BoardKind` (frontend/src/types/board.ts) must
// appear here, otherwise the gallery card falls back to "No components" and
// the user sees an empty preview. New board kinds must be added in BOTH
// places (the type union and this map).
const BOARD_DEFS: Record<string, CompDef> = {
'arduino-uno': { svg: 'arduino-uno.svg', w: 274, h: 202 },
'arduino-nano': { svg: 'arduino-nano.svg', w: 170, h: 67 },
'arduino-mega': { svg: 'arduino-mega.svg', w: 388, h: 192 },
'raspberry-pi-pico': { svg: 'raspberry-pi-pico.svg', w: 79, h: 200 },
'pi-pico-w': { svg: 'raspberry-pi-pico-w.svg', w: 105, h: 264 },
'raspberry-pi-3': { svg: 'raspberry-pi-3.svg', w: 200, h: 135 },
// ESP32 family — every variant currently maps to the DevKit V1 art (no
// dedicated SVGs are extracted for the variants yet).
esp32: { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
'esp32-devkit-c-v4': { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
'esp32-cam': { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
'wemos-lolin32-lite': { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
'esp32-s3': { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
'xiao-esp32-s3': { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
'arduino-nano-esp32': { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
'esp32-c3': { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
'xiao-esp32-c3': { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
'aitewinrobot-esp32c3-supermini': { svg: 'esp32-devkit-v1.svg', w: 107, h: 204 },
// ATtiny85 — DIP-8, drawn inline (no wokwi element ships its SVG).
attiny85: { svg: '', inline: Attiny85InlinePreview, w: 160, h: 100 },
};
// LED color → SVG filename
@ -123,9 +174,12 @@ function isBoardType(type: string): boolean {
type.includes('arduino-uno') ||
type.includes('arduino-nano') ||
type.includes('arduino-mega') ||
type.includes('esp32-devkit') ||
type.includes('esp32') || // covers esp32, esp32-s3, esp32-c3, esp32-cam, …
type.includes('raspberry-pi-pico') ||
type.includes('nano-rp2040')
type.includes('pi-pico-w') ||
type.includes('nano-rp2040') ||
type.includes('attiny85') ||
type.includes('raspberry-pi-3')
);
}

View File

@ -355,19 +355,19 @@ export const EditorPage: React.FC = () => {
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
</button>
<div style={{ flex: 1 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<EditorToolbar
consoleOpen={consoleOpen}
setConsoleOpen={setConsoleOpen}
compileLogs={compileLogs}
setCompileLogs={setCompileLogs}
/* File tabs share the toolbar row keeps every action
icon pinned regardless of editor-pane width. */
centerSlot={!isRaspberryPi3 ? <FileTabs /> : null}
/>
</div>
</div>
{/* File tabs — hidden when Pi workspace is active */}
{!isRaspberryPi3 && <FileTabs />}
{/* Editor area: Pi workspace or Monaco editor */}
<div className="editor-wrapper" style={{ flex: 1, overflow: 'hidden', minHeight: 0 }}>
{isRaspberryPi3 && activeBoardId ? (