Close: bubble-up DOM hide + key-bump on full collapse

Two improvements to the close-pane workaround:

1. When both children of a parent split are now hidden, the .side hide
   bubbles up to the parent split's own .side and hides that too. Needed
   for deeply-nested splits where closing leaves at the bottom should
   propagate the visual collapse upward.

2. When closeLeaf returns null (the user closed the last remaining
   leaf), force a full Pane remount via {#key renderKey} bump. The
   DOM-hide approach can't simulate mounting a fresh tree node, so this
   is the one place where we take the cost of a full unmount + remount.
   Only fires when the entire tree resets — not on intermediate closes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-22 17:04:00 +01:00
parent 8d5c49155b
commit 40ce27251d

View file

@ -56,6 +56,11 @@
if (activeLeafId === id) activeLeafId = null;
}
// Bumped to force a full Pane unmount+remount when the tree's structure
// changes in a way the DOM-hide workaround can't simulate (specifically,
// when ALL panes have been closed and we need to render a fresh leaf).
let renderKey = $state(0);
// ---- tree mutation handlers (closures over tree $state) -----------------
function handleSplit(leafId: NodeId, orientation: Orientation) {
const parent = findLeaf(tree, leafId);
@ -73,21 +78,45 @@
void killPane(paneId).catch((e) => console.warn("killPane failed:", e));
orch.paneIdByLeaf.delete(leafId);
}
// Hide the closed pane's flex container and the adjacent gutter so the
// sibling pane visually fills the freed space.
// DOM-hide the .side wrapping this leaf and the adjacent gutter so the
// sibling pane visually fills. If both sides of the parent split are
// now hidden, bubble up and hide that whole split too (recursive
// collapse — needed for nested closes).
const leafEl = document.querySelector(`[data-leaf-id="${leafId}"]`);
const sideEl = leafEl?.closest(".side") as HTMLElement | null;
const splitEl = sideEl?.parentElement;
if (sideEl && splitEl) {
let sideEl = leafEl?.closest(".side") as HTMLElement | null;
while (sideEl) {
sideEl.style.display = "none";
const splitEl = sideEl.parentElement;
if (!splitEl) break;
// Hide the gutter in this split (only one).
Array.from(splitEl.children).forEach((c) => {
const child = c as HTMLElement;
if (child.classList.contains("gutter")) child.style.display = "none";
});
// If a sibling side is still visible, flex will auto-fill; we're done.
const visibleSiblings = Array.from(splitEl.children).filter((c) => {
const child = c as HTMLElement;
return (
child.classList.contains("side") &&
child.style.display !== "none"
);
});
if (visibleSiblings.length > 0) break;
// Otherwise, this whole split is empty — climb up and hide its parent side.
sideEl = splitEl.closest(".side") as HTMLElement | null;
}
// Update tree state (used by broadcast routing, palette, persistence).
// Update tree state for persistence / palette / broadcast routing.
const next = closeLeaf(tree, leafId);
tree = next ?? newLeaf({ distro: defaultDistro });
if (next === null) {
// Tree fully collapsed. The DOM-hide workaround can't simulate
// mounting a brand-new leaf, so force a clean remount via key bump.
tree = newLeaf({ distro: defaultDistro });
renderKey += 1;
} else {
tree = next;
}
clearActiveIf(leafId);
}
@ -332,7 +361,9 @@
<div class="pane-wrap">
{#if ready}
<Pane node={tree} {activeLeafId} />
{#key renderKey}
<Pane node={tree} {activeLeafId} />
{/key}
{/if}
</div>