Hold the toolbar's widths still while stepping and editing

The stepper's middle is sized to the axis's longest level rather than the
current one, and Save and the dirty dot keep their slots while hidden, so
nothing shifts under the cursor.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-25 13:06:22 -04:00
parent 45e45bfa60
commit b8b5220583
2 changed files with 20 additions and 6 deletions

View File

@ -35,6 +35,9 @@ export default function DepthButtons({ label, levels, depth, onPick, totalLabel
const known = depth != null const known = depth != null
const current = !known ? '—' : (depth === 0 ? totalLabel : levels[depth - 1]) const current = !known ? '—' : (depth === 0 ? totalLabel : levels[depth - 1])
const step = (d) => onPick(Math.min(max, Math.max(0, d))) const step = (d) => onPick(Math.min(max, Math.max(0, d)))
// Capped so one long field name cannot push the rest of the toolbar off;
// past the cap the label truncates, with the full name in the title.
const widest = Math.min(18, Math.max(...[totalLabel, ...levels].map(l => String(l).length)))
// One joined control, in the style of the operation panel's // One joined control, in the style of the operation panel's
// Scale | Recode | Clone. The arrows used to be separate outlined boxes a // Scale | Recode | Clone. The arrows used to be separate outlined boxes a
@ -53,9 +56,13 @@ export default function DepthButtons({ label, levels, depth, onPick, totalLabel
title={`Collapse ${label.toLowerCase()} by one level`} title={`Collapse ${label.toLowerCase()} by one level`}
onClick={() => step((depth ?? 0) - 1)}>◂</button> onClick={() => step((depth ?? 0) - 1)}>◂</button>
{/* Sized to the axis's longest level, not the current one. Sized to
its content it resized on every step, so the ▸ moved out from
under the cursor between clicks. */}
<button onClick={() => setOpen(o => !o)} <button onClick={() => setOpen(o => !o)}
title={`${label}: showing ${current}`} title={`${label}: showing ${current}`}
className={`px-2 py-0.5 max-w-[10rem] truncate transition-colors style={{ width: `calc(${widest}ch + 1rem)` }}
className={`px-2 py-0.5 truncate text-left transition-colors
${open ? 'bg-blue-50 text-blue-700' : 'bg-white text-gray-700 hover:bg-gray-100'}`}> ${open ? 'bg-blue-50 text-blue-700' : 'bg-white text-gray-700 hover:bg-gray-100'}`}>
{current} {current}
</button> </button>

View File

@ -123,15 +123,22 @@ export default function LayoutMenu({
${open ? 'border-blue-300 text-blue-700 bg-blue-50' ${open ? 'border-blue-300 text-blue-700 bg-blue-50'
: 'border-gray-200 text-gray-600 hover:border-gray-400'}`}> : 'border-gray-200 text-gray-600 hover:border-gray-400'}`}>
<span className="truncate">{active ? active.name : 'Unsaved'}</span> <span className="truncate">{active ? active.name : 'Unsaved'}</span>
{dirty && <span title="Changed since it was saved" className="text-amber-500 leading-none">•</span>} {/* Always rendered, hidden when clean, so the button does not widen
by a dot the moment the pivot changes. */}
<span title={dirty ? 'Changed since it was saved' : undefined}
className={`text-amber-500 leading-none ${dirty ? '' : 'invisible'}`}>•</span>
<span className="text-gray-400 leading-none">▾</span> <span className="text-gray-400 leading-none">▾</span>
</button> </button>
{/* Save is the common action and stays outside the menu, but only when {/* Save is the common action and stays outside the menu, but only when
there is something to save it to and it is yours to overwrite. */} there is something to save it to and it is yours to overwrite. Its
{active?.can_edit && dirty && ( slot is held while the layout is clean: appearing and vanishing, it
<button onClick={() => onSave(active)} shoved the Rows and Columns controls sideways under the cursor. */}
className="border border-blue-200 text-blue-600 hover:text-blue-800 rounded px-2 py-0.5"> {active?.can_edit && (
<button onClick={() => onSave(active)} disabled={!dirty}
aria-hidden={!dirty} tabIndex={dirty ? 0 : -1}
className={`border border-blue-200 text-blue-600 hover:text-blue-800 rounded px-2 py-0.5
${dirty ? '' : 'invisible'}`}>
Save Save
</button> </button>
)} )}