feat(desktop): let a tool panel shrink to its collapsed header

The terminal and logs carried a 7.5rem minHeight, so dragging the seam down
jammed against a floor with a sliver of unusable terminal still showing.

Tool-panel zones now floor at COLLAPSED_ZONE_PX (the h-7 header strip)
instead of the generic 80px, and releasing at that floor minimizes the zone
rather than persisting the sliver — it folds to its collapsed header,
vertical rail or horizontal strip depending on the parent axis. The sliver
size is never written, so restoring returns the height it had before.
This commit is contained in:
Brooklyn Nicholson
2026-07-31 23:49:28 -05:00
parent 4802d2d88d
commit aca571ef49
2 changed files with 48 additions and 5 deletions
@@ -19,6 +19,15 @@ import type { FloatingAnchor } from './floating-rect'
export const MIN_PANE_PX = 80
/**
* The floor for a TOOL PANEL zone (terminal / logs) instead of `MIN_PANE_PX`.
* A tool panel is meant to be draggable down to nothing — the minimized rail
* (its header strip, `h-7`) is the smallest meaningful form, so the sash lets
* it shrink to exactly that and then collapses the zone rather than jamming
* against an 80px floor with a sliver of unusable content still showing.
*/
export const COLLAPSED_ZONE_PX = 28
/** Optional CSS sizing a pane contributes (`data.width` / `data.minWidth`…).
* Applied to the pane's GROUP along the axis of the split that contains it —
* the same semantics as the app's `Pane width/minWidth/maxWidth` props:
@@ -22,12 +22,15 @@ import {
$collapsedTreeSides,
$hiddenTreePanes,
$narrowViewport,
isCollapsePane,
persistTree,
presetSplitWeights,
setTreeGroupMinimized,
setTreeSplitWeights
} from '../store'
import {
COLLAPSED_ZONE_PX,
computedPx,
cssMax,
edgeFixedZone,
@@ -43,6 +46,18 @@ import {
} from './track-model'
import { TreeNode } from './tree-node'
/** The single group id a subtree resolves to, or null when it holds several
* zones — the sash can only collapse a boundary that IS exactly one zone. */
function groupIdOf(node: LayoutNode): null | string {
if (node.type === 'group') {
return node.id
}
const ids = new Set(node.children.map(groupIdOf))
return ids.size === 1 ? [...ids][0] : null
}
/**
* The size overrides for a fixed set of panes, referentially stable until one
* of THEM changes. Sash drags churn `$paneStates` every frame; subscribing the
@@ -197,6 +212,11 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo
// Clamps live on the zone's split-child WRAPPER (where we render them).
const el = zoneEl?.parentElement ?? wrapper
const cs = window.getComputedStyle(el)
// A tool panel (terminal / logs) may be dragged down to its collapsed
// header — the generic 80px floor is not its floor. Below that the
// release minimizes the zone instead of leaving a useless sliver.
const toolZone = allPaneIds(child).length > 0 && allPaneIds(child).every(isCollapsePane)
const floor = toolZone ? COLLAPSED_ZONE_PX : MIN_PANE_PX
return {
// EVERY shown pane of the zone: the zone's track is the max() of its
@@ -206,8 +226,10 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo
paneIds: zone ? shownPaneIds(zone, trackCtx) : [],
fixed: Boolean(zone),
size: sizeOf(zoneEl ?? wrapper),
min: Math.max(MIN_PANE_PX, computedPx(horizontal ? cs.minWidth : cs.minHeight, 0)),
max: computedPx(horizontal ? cs.maxWidth : cs.maxHeight, Number.POSITIVE_INFINITY)
min: toolZone ? floor : Math.max(floor, computedPx(horizontal ? cs.minWidth : cs.minHeight, 0)),
max: computedPx(horizontal ? cs.maxWidth : cs.maxHeight, Number.POSITIVE_INFINITY),
collapseId: toolZone ? (zone?.id ?? groupIdOf(child)) : null,
floor
}
}
@@ -310,9 +332,21 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo
resize.finish()
if (lastShift !== null) {
// One store commit; the re-render rewrites `flex` and clears the
// preview overrides.
applyShift(lastShift)
// Dragged a tool panel down to its collapsed header? Fold the zone
// to its rail instead of persisting a sliver — and DON'T write the
// sliver size, so restoring brings back the size it had before.
const collapsedSide =
(a.collapseId && a0px + lastShift <= a.floor && a.collapseId) ||
(b.collapseId && b0px - lastShift <= b.floor && b.collapseId) ||
null
if (collapsedSide) {
setTreeGroupMinimized(collapsedSide, true)
} else {
// One store commit; the re-render rewrites `flex` and clears the
// preview overrides.
applyShift(lastShift)
}
} else {
// Click without movement: nothing will re-render, so put the
// wrappers' inline styles back exactly as React last wrote them.