From b39041ca4ce20ab3b3b7ecc0e73a07ffabaf82d5 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Tue, 28 Apr 2026 23:23:20 -0300 Subject: [PATCH] feat(editor): enhance toolbar layout with center slot for file tabs and improve responsiveness --- .../src/components/editor/EditorToolbar.css | 104 +++++++++++++++--- .../src/components/editor/EditorToolbar.tsx | 43 +++++--- .../components/examples/CircuitPreview.tsx | 58 +++++++++- frontend/src/pages/EditorPage.tsx | 8 +- 4 files changed, 173 insertions(+), 40 deletions(-) diff --git a/frontend/src/components/editor/EditorToolbar.css b/frontend/src/components/editor/EditorToolbar.css index 55f119a1..686eaf09 100644 --- a/frontend/src/components/editor/EditorToolbar.css +++ b/frontend/src/components/editor/EditorToolbar.css @@ -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 { diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 90e04c9f..aaa13767 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -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 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 = { @@ -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 ? ( ) : ( - + @@ -668,7 +676,7 @@ export const EditorToolbar = ({ className="tb-btn tb-btn-stop" title="Stop" > - + @@ -681,8 +689,8 @@ export const EditorToolbar = ({ title="Reset" > - + @@ -737,6 +745,9 @@ export const EditorToolbar = ({ )} + {/* Center slot — file tabs share the row so action icons stay pinned. */} + {centerSlot &&
{centerSlot}
} +
{/* Hidden file input for import (always present) */} - + @@ -880,8 +891,8 @@ export const EditorToolbar = ({ title="Toggle Output Console" > = { '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 }) => ( + + {/* Pin stubs (left + right, 4 each) */} + {[20, 40, 60, 80].map((py) => ( + + + + + ))} + {/* IC body */} + + + + ATtiny85 + + + 8-bit AVR + + +); + // 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 = { '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') ); } diff --git a/frontend/src/pages/EditorPage.tsx b/frontend/src/pages/EditorPage.tsx index c9fa358f..c640857b 100644 --- a/frontend/src/pages/EditorPage.tsx +++ b/frontend/src/pages/EditorPage.tsx @@ -355,19 +355,19 @@ export const EditorPage: React.FC = () => { -
+
: null} />
- {/* File tabs — hidden when Pi workspace is active */} - {!isRaspberryPi3 && } - {/* Editor area: Pi workspace or Monaco editor */}
{isRaspberryPi3 && activeBoardId ? (