fix(editor): the workspace panel stacks its actions, and the toolbar stops growing a hole

Two places where the editor bar spent width on nothing.

The file explorer put its label and its buttons on one line: WORKSPACE next
to new/open/save, and every board section's name next to rename / new file /
new folder plus the status dot. Between them those set a floor under the
panel's width, and the board name — the one thing a user reads there — got
whatever was left ("M5 Cardput..." in a 150px pane). Both headers now stack:
the label owns its row, the actions sit above (board) or below (workspace),
and the status dot rides in the actions row, where it costs the name
nothing. The actions row keeps its height reserved whether or not the
buttons are showing, so hovering a section never shoves the files below it
down. Default pane width 150 -> 124px; the freed px go to the code editor.

The toolbar's center slot is a flexible spacer that pins the right icons to
the far right, and nothing renders into it (no caller passes centerSlot). So
whenever the bar had width to spare — closing the docked AI chat is exactly
that — the spacer ate it: 191px of black between Reset and Libraries on a
1600px window. Empty, it no longer grows, so the editor controls stay packed
and the surplus collects at the one boundary that already carries a divider,
in front of the canvas controls (which belong over the canvas pane).
This commit is contained in:
David Montero Crespo 2026-08-18 18:42:33 +02:00
parent 13a19c127a
commit c21bfca220
4 changed files with 205 additions and 143 deletions

View File

@ -47,6 +47,15 @@
overflow: hidden;
}
/* Empty (nothing renders here today), it must NOT grow: a spacer that eats
the surplus width turns it into a black hole between the Run group and
Libraries whenever the bar gets roomier — closing the AI chat is exactly
that. Packed, the surplus collects at ONE place instead: the boundary in
front of the canvas controls, which already carries a divider. */
.toolbar-center-slot:empty {
flex: 0 0 0;
}
/* ── Divider ─────────────────────────────────────── */
.tb-divider {
width: 1px;

View File

@ -9,13 +9,18 @@
font-family: 'Segoe UI', sans-serif;
}
/* Two rows, not one: the title on top, the three actions under it. Side by
side they set the pane's minimum width between them (a truncated "WORK..."
next to buttons that must stay reachable); stacked, the panel is free to be
narrow and the code editor takes the px that frees up. Vertical space is
what this column has to spare. */
.file-explorer-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
padding: 0 4px 0 6px;
height: 46px;
flex-direction: column;
align-items: stretch;
justify-content: center;
gap: 3px;
padding: 6px 6px 6px 8px;
color: #9d9d9d;
font-size: 10px;
font-weight: 600;
@ -24,10 +29,7 @@
flex-shrink: 0;
}
/* The title gives way before the actions do: at narrow pane widths the
three buttons must stay reachable, the word can truncate. */
.file-explorer-title {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@ -38,7 +40,7 @@
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
margin-left: -2px; /* optical: line the first glyph up with the title */
}
.file-explorer-new-btn {
@ -230,11 +232,15 @@
margin-bottom: 2px;
}
/* A board section header is two rows: its actions, then its name. The name
row then spans the full pane width, so "M5 Cardputer ADV" reads as a name
instead of an ellipsis squeezed between three buttons and a status dot. */
.fe-board-header {
display: flex;
align-items: center;
gap: 4px;
padding: 3px 4px 3px 2px;
flex-direction: column;
align-items: stretch;
gap: 1px;
padding: 2px 4px 3px 2px;
cursor: pointer;
color: #aaa;
border-radius: 3px;
@ -243,6 +249,29 @@
position: relative;
}
.fe-board-name-row {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
}
/* The actions keep their row reserved whether or not they are showing: a row
that appears on hover would shove every file below it down by its height
under the pointer. */
.fe-board-actions-row {
display: flex;
align-items: center;
gap: 1px;
min-height: 16px;
padding-left: 2px;
}
/* Pushes the buttons to the right end and leaves the status dot at the left. */
.fe-board-actions-spacer {
flex: 1 1 auto;
}
.fe-board-header:hover {
background: rgba(255, 255, 255, 0.06);
color: #ddd;
@ -306,13 +335,13 @@
border: none;
color: #9d9d9d;
cursor: pointer;
padding: 1px;
padding: 0;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
width: 16px;
height: 16px;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.1s;
@ -324,8 +353,8 @@
}
.fe-board-new-btn svg {
width: 16px;
height: 16px;
width: 14px;
height: 14px;
}
.fe-board-new-btn:hover {

View File

@ -773,6 +773,11 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
return (
<div className="file-explorer">
{/* Stacked, not side by side: the title owns the first line and the
actions the second. Side by side, the three buttons and the word
set the pane's minimum width between them — and every px the
explorer does not need goes to the code editor. Vertical room is
what this panel has to spare. */}
<div className="file-explorer-header">
<span className="file-explorer-title">{t('editor.fileExplorer.workspace')}</span>
<div className="file-explorer-header-actions">
@ -834,90 +839,101 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
}}
title={`${boardDisplayName(board)} — ${t('editor.fileExplorer.clickToEdit')}`}
>
<button
className="fe-collapse-btn"
onClick={(e) => {
e.stopPropagation();
toggleCollapse(board.id);
}}
title={isOpen ? t('editor.fileExplorer.collapse') : t('editor.fileExplorer.expand')}
>
<IcoChevron open={isOpen} />
</button>
<span className="fe-board-icon" style={{ color }}>
{BOARD_ICON[board.boardKind] ?? PRO_FALLBACK_ICON}
</span>
{renamingSection?.id === board.id && renamingSection.kind === 'board' ? (
<input
ref={sectionRenameInputRef}
className="file-explorer-rename-input fe-section-rename-input"
value={sectionRenameValue}
onChange={(e) => setSectionRenameValue(e.target.value)}
onBlur={commitSectionRename}
onKeyDown={(e) => {
if (e.key === 'Enter') commitSectionRename();
if (e.key === 'Escape') cancelSectionRename();
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
{/* Actions ride ABOVE the name, not beside it: sharing the
line, three buttons plus the status dot left a board name
like "M5 Cardputer ADV" a stub of the row and set a floor
under the pane's width. */}
<div className="fe-board-actions-row">
{/* The run/compile dot rides here too: on the name row it
cost the name another 11px, and it is not hover-gated,
so this row is never empty. */}
<span
className="fe-board-label"
onDoubleClick={(e) => {
className="fe-status-dot"
style={{ background: statusColor }}
title={
board.running
? t('editor.fileExplorer.status.running')
: board.compiledProgram
? t('editor.fileExplorer.status.compiled')
: t('editor.fileExplorer.status.idle')
}
/>
<span className="fe-board-actions-spacer" />
<button
className="fe-board-new-btn"
title="Rename board (or double-click the name)"
onClick={(e) => {
e.stopPropagation();
startBoardRename(board);
}}
title="Double-click to rename"
>
{boardDisplayName(board)}
<IcoPencil />
</button>
<button
className="fe-board-new-btn"
title={t('editor.fileExplorer.newFileInBoard')}
onClick={(e) => {
e.stopPropagation();
startCreateFile(board.id, groupId);
}}
>
<IcoNewFile />
</button>
<button
className="fe-board-new-btn"
title={t('editor.fileExplorer.newFolderInBoard', 'New folder')}
onClick={(e) => {
e.stopPropagation();
startCreateFolder(board.id, groupId);
}}
>
<IcoNewFolder />
</button>
</div>
<div className="fe-board-name-row">
<button
className="fe-collapse-btn"
onClick={(e) => {
e.stopPropagation();
toggleCollapse(board.id);
}}
title={isOpen ? t('editor.fileExplorer.collapse') : t('editor.fileExplorer.expand')}
>
<IcoChevron open={isOpen} />
</button>
<span className="fe-board-icon" style={{ color }}>
{BOARD_ICON[board.boardKind] ?? PRO_FALLBACK_ICON}
</span>
)}
<span
className="fe-status-dot"
style={{ background: statusColor }}
title={
board.running
? t('editor.fileExplorer.status.running')
: board.compiledProgram
? t('editor.fileExplorer.status.compiled')
: t('editor.fileExplorer.status.idle')
}
/>
{renamingSection?.id === board.id && renamingSection.kind === 'board' ? (
<input
ref={sectionRenameInputRef}
className="file-explorer-rename-input fe-section-rename-input"
value={sectionRenameValue}
onChange={(e) => setSectionRenameValue(e.target.value)}
onBlur={commitSectionRename}
onKeyDown={(e) => {
if (e.key === 'Enter') commitSectionRename();
if (e.key === 'Escape') cancelSectionRename();
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<span
className="fe-board-label"
onDoubleClick={(e) => {
e.stopPropagation();
startBoardRename(board);
}}
title="Double-click to rename"
>
{boardDisplayName(board)}
</span>
)}
{/* Rename + new-file buttons — visible on hover */}
<button
className="fe-board-new-btn"
title="Rename board (or double-click the name)"
onClick={(e) => {
e.stopPropagation();
startBoardRename(board);
}}
>
<IcoPencil />
</button>
<button
className="fe-board-new-btn"
title={t('editor.fileExplorer.newFileInBoard')}
onClick={(e) => {
e.stopPropagation();
startCreateFile(board.id, groupId);
}}
>
<IcoNewFile />
</button>
<button
className="fe-board-new-btn"
title={t('editor.fileExplorer.newFolderInBoard', 'New folder')}
onClick={(e) => {
e.stopPropagation();
startCreateFolder(board.id, groupId);
}}
>
<IcoNewFolder />
</button>
</div>
</div>
{/* Files under this board, as a folder tree */}
@ -1159,59 +1175,65 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
}}
title={`${chipName} — ${t('editor.fileExplorer.clickToEdit')}`}
>
<button
className="fe-collapse-btn"
onClick={(e) => {
e.stopPropagation();
toggleCollapse(chip.id);
}}
title={isOpen ? t('editor.fileExplorer.collapse') : t('editor.fileExplorer.expand')}
>
<IcoChevron open={isOpen} />
</button>
{/* Same two-row shape as a board section: the rename button
sits above, the chip's name gets the full row. */}
<div className="fe-board-actions-row">
{!(renamingSection?.id === chip.id && renamingSection.kind === 'chip') && (
<button
className="fe-board-new-btn"
title="Rename chip (or double-click the name)"
onClick={(e) => {
e.stopPropagation();
startChipRename(chip.id, chipName);
}}
>
<IcoPencil />
</button>
)}
</div>
<span className="fe-board-icon" style={{ color: '#c4b5fd' }}>
<IcoChip />
</span>
{renamingSection?.id === chip.id && renamingSection.kind === 'chip' ? (
<input
ref={sectionRenameInputRef}
className="file-explorer-rename-input fe-section-rename-input"
value={sectionRenameValue}
onChange={(e) => setSectionRenameValue(e.target.value)}
onBlur={commitSectionRename}
onKeyDown={(e) => {
if (e.key === 'Enter') commitSectionRename();
if (e.key === 'Escape') cancelSectionRename();
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<span
className="fe-board-label"
onDoubleClick={(e) => {
e.stopPropagation();
startChipRename(chip.id, chipName);
}}
title="Double-click to rename"
>
{chipName}
</span>
)}
{!(renamingSection?.id === chip.id && renamingSection.kind === 'chip') && (
<div className="fe-board-name-row">
<button
className="fe-board-new-btn"
title="Rename chip (or double-click the name)"
className="fe-collapse-btn"
onClick={(e) => {
e.stopPropagation();
startChipRename(chip.id, chipName);
toggleCollapse(chip.id);
}}
title={isOpen ? t('editor.fileExplorer.collapse') : t('editor.fileExplorer.expand')}
>
<IcoPencil />
<IcoChevron open={isOpen} />
</button>
)}
<span className="fe-board-icon" style={{ color: '#c4b5fd' }}>
<IcoChip />
</span>
{renamingSection?.id === chip.id && renamingSection.kind === 'chip' ? (
<input
ref={sectionRenameInputRef}
className="file-explorer-rename-input fe-section-rename-input"
value={sectionRenameValue}
onChange={(e) => setSectionRenameValue(e.target.value)}
onBlur={commitSectionRename}
onKeyDown={(e) => {
if (e.key === 'Enter') commitSectionRename();
if (e.key === 'Escape') cancelSectionRename();
}}
onClick={(e) => e.stopPropagation()}
/>
) : (
<span
className="fe-board-label"
onDoubleClick={(e) => {
e.stopPropagation();
startChipRename(chip.id, chipName);
}}
title="Double-click to rename"
>
{chipName}
</span>
)}
</div>
</div>
{isOpen && (

View File

@ -45,10 +45,12 @@ const BOTTOM_PANEL_DEFAULT = 200;
const EXPLORER_MIN = 100;
const EXPLORER_MAX = 500;
// Narrow by default: the explorer rows are compact (see FileExplorer.css),
// so 150px fits a board header with its actions and typical file names,
// and every px saved here goes to the code editor.
const EXPLORER_DEFAULT = 150;
// Narrow by default: the explorer rows are compact and both headers stack
// their actions ABOVE their label instead of beside it (see
// FileExplorer.css), so nothing has to share a line — 124px fits a board
// name and typical file names, and every px saved here goes to the code
// editor.
const EXPLORER_DEFAULT = 124;
// Once per full page load: the pristine-visit starter dialog must not pop
// again when the user closes it and later navigates away from and back to