Skip to content

Animate kills#16

Merged
nedtwigg merged 9 commits intomainfrom
animate-kills
Apr 17, 2026
Merged

Animate kills#16
nedtwigg merged 9 commits intomainfrom
animate-kills

Conversation

@nedtwigg
Copy link
Copy Markdown
Member

So that it is easier to tell what happened to last pane to get killed or the last pane to get detached.

nedtwigg and others added 9 commits April 17, 2026 11:20
When the last visible pane is killed or detached, auto-spawn a replacement so
the content area is never blank. The replacement receives the removed pane's
id as paneToCopy (dead infrastructure for now — future work will copy cwd and
terminal kind from it). Split also records paneToCopy → the pane it was split
from.

Freshly-spawned panes animate in via a clip-path reveal on the dockview group
(header + body together). clip-path is used instead of scale so the selection
overlay's getBoundingClientRect stays accurate during the animation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On kill: ghost overlay crushes from the pane's original rect toward the
replacer (edge/centerline/bottom-right), and surviving panes FLIP-reveal
their newly claimed territory via clip-path. Case detection is rect-based
(measure pre/post bounds and classify by which sides grew), so it handles
2-pane, 3+ linear, and nested splits uniformly. Reduced-motion skips the
animation entirely.

Spawn animation is now directional:
  - horizontal split → reveal from left edge
  - vertical split → reveal from top edge
  - auto-spawn after last-pane kill → reveal from top-left corner

Also fixes a one-frame flash where KillConfirmOverlay rendered its
viewport-center fallback before the useEffect measured the panel
(switched to useLayoutEffect so the rect lands before first paint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On last-pane kill/detach, the outgoing-pane animation (crush or
detach-to-door) and the incoming-pane reveal used to start at the same
time and fight for the same screen region. Now the spawn is deferred by
440ms (the animation duration) so the two play sequentially. If any
other action re-populates the pane area during the delay (e.g. door
reattach), the deferred spawn becomes a no-op.

Reduced-motion skips the delay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto-spawn was introduced with a 440ms delay so the kill/detach
animation can finish before the new pane animates in. But detachPanel
calls selectDoor(id) synchronously, then 440ms later the delayed spawn
called selectPanel(newId) and stole focus off the just-detached door.

Only call selectPanel if nothing is selected (the kill path clears
selection before the delay). On detach the door selection stays put.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
api.addPanel auto-activates the new panel, firing onDidActivePanelChange,
which was calling setSelectedId(panel.id) unconditionally. When the
current selection was a door (just-detached last pane), that flipped
selectedId to the new pane's id while selectedType stayed 'door' —
desyncing the door highlight and stranding SelectionOverlay on a stale
door rect. Early-return from the listener when selectedType === 'door'.

Also fleshes out docs/specs/layout.md with a new Animations section
(spawn / kill crush / FLIP reclaim / auto-spawn delay) and adds a corner
case entry for the door-focus preservation above. Updates corner case
#10 to reference the 440ms auto-spawn delay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The directional crush (to-left/right/up/down/hcenter/vcenter/br) added
motion that competed with the grower's FLIP reveal, which already
carries the direction. Replacing with a uniform opacity fade reads
cleaner and eliminates the case-detection branching — every kill path
(edge / middle / last-pane) now uses the same .pane-fading-out class.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A same-color ghost over a same-color background had nothing to fade
against — the previous fade was effectively invisible. Fade the actual
killed pane's group element instead, so the real header/terminal content
dissolves. Then finalize removal + FLIP the growers after animationend.

killInProgressRef is set across api.removePanel so the onDidRemovePanel
auto-spawn handler skips its own 440ms delay — the fade already consumed
that time. Detach path is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes to last-pane kill:

1. Auto-spawn was silently dropped. api.addPanel called re-entrantly
   from inside the onDidRemovePanel handler was not taking effect.
   Always defer the spawn through setTimeout(spawn, delay), even when
   delay is 0, so the addPanel runs after the remove event fully settles.

2. The focus ring did not follow the shrinking pane. The shrink was
   using clip-path (doesn't affect getBoundingClientRect), so the
   SelectionOverlay never saw a change. Switched to transform: scale()
   with transform-origin: 100% 100%, and gave the overlay element a
   matching .ring-shrinking-to-br animation via a new ref so both
   collapse in lockstep toward their bottom-right corners. The ring
   class is removed in finalize() before the next overlay render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nedtwigg nedtwigg merged commit 3cb1abb into main Apr 17, 2026
2 of 4 checks passed
@nedtwigg nedtwigg deleted the animate-kills branch April 17, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant