Enforce minimum pane size (180px) on split and gutter drag

Spamming the ⇥/⇣ split buttons (or their Ctrl+Shift+E/O shortcuts)
used to subdivide panes indefinitely, leaving toolbar-only slivers
that were unusable.

- tree.ts: MIN_PANE_PX = 180 constant.
- App.tsx: the `split` orchestration callback now computes the active
  pane's pixel dimensions from its layout slot + the container rect,
  and refuses to split if either child would fall below MIN_PANE_PX.
  Surfaces the refusal via a `notify(...)` toast so the user knows
  why nothing happened.
- Gutter.tsx: pointermove clamps the new ratio so the smaller child
  stays at least MIN_PANE_PX wide/tall. Falls back to the old 0.05
  floor only if the parent is so small that two min-sized panes
  can't both fit (degraded but functional).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-22 22:01:41 +01:00
parent a5209e08ae
commit daf0d4e88a
3 changed files with 50 additions and 11 deletions

View file

@ -27,6 +27,7 @@ import {
updateSplitRatio,
swapLeaves,
findNeighborInDirection,
MIN_PANE_PX,
type Direction,
serialize,
deserialize,
@ -145,9 +146,41 @@ export default function App() {
return () => clearInterval(interval);
}, []);
// notify is defined up here (and not next to dismissNotification) because
// the split callback below uses it to warn when a pane is too small to
// subdivide further. Build-mode tsc balks on use-before-declaration.
const notify = useCallback((message: string) => {
const id = nextNotifIdRef.current++;
setNotifications((ns) => [...ns, { id, message }]);
window.setTimeout(() => {
setNotifications((ns) => ns.filter((n) => n.id !== id));
}, 5000);
}, []);
// ---- orchestration callbacks --------------------------------------------
const split = useCallback(
(leafId: NodeId, orientation: Orientation) => {
// Refuse the split if it would produce a child pane below MIN_PANE_PX
// — otherwise spamming the split buttons shrinks panes into nothing.
const container = paneWrapRef.current;
if (container) {
const slot = flattenLayout(treeRef.current).leaves.find(
(s) => s.leaf.id === leafId,
);
if (slot) {
const rect = container.getBoundingClientRect();
const paneW = slot.box.width * rect.width;
const paneH = slot.box.height * rect.height;
const childW = orientation === "h" ? paneW / 2 : paneW;
const childH = orientation === "v" ? paneH / 2 : paneH;
if (childW < MIN_PANE_PX || childH < MIN_PANE_PX) {
notify(
`Pane too small to split — would create ${Math.round(childW)}×${Math.round(childH)}px (min ${MIN_PANE_PX}px)`,
);
return;
}
}
}
setTree((t) => {
const parent = findLeaf(t, leafId);
const inherit = parent
@ -156,7 +189,7 @@ export default function App() {
return splitLeaf(t, leafId, orientation, inherit);
});
},
[defaultDistro],
[defaultDistro, notify],
);
const close = useCallback(
@ -317,14 +350,6 @@ export default function App() {
[],
);
const notify = useCallback((message: string) => {
const id = nextNotifIdRef.current++;
setNotifications((ns) => [...ns, { id, message }]);
window.setTimeout(() => {
setNotifications((ns) => ns.filter((n) => n.id !== id));
}, 5000);
}, []);
const dismissNotification = useCallback((id: number) => {
setNotifications((ns) => ns.filter((n) => n.id !== id));
}, []);