Skip to content

Commit 3cb1abb

Browse files
authored
Animate kills (#16)
2 parents bfec4cb + 57c69b4 commit 3cb1abb

File tree

3 files changed

+312
-26
lines changed

3 files changed

+312
-26
lines changed

docs/specs/layout.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,38 @@ Colors use a two-layer CSS variable strategy: `@theme --color-*` tokens → `var
283283

284284
Dockview's separator borders, sash handles, and groupview borders are all set to transparent/none — the 6px gap is the only visual separator between panes. All dockview container backgrounds are flattened to `var(--color-surface)`.
285285

286+
## Animations
287+
288+
All pane-related motion is 440ms with `cubic-bezier(0.22, 1, 0.36, 1)` and uses `clip-path` (not `transform`) so `getBoundingClientRect` remains accurate during animation — the selection overlay measures the real post-animation bounds without lag. Reduced-motion users skip every animation described below.
289+
290+
### Spawn (new pane reveal)
291+
292+
When a pane is added, its dockview group element gets a directional `.pane-spawning-from-{left,top,top-left}` class. The clip-path starts fully closed from the opposite edge(s) and reveals to `inset(0)`. Direction is chosen by how the pane was born:
293+
294+
- **Horizontal split** (new pane on the right) → reveal from the left edge.
295+
- **Vertical split** (new pane below) → reveal from the top edge.
296+
- **Auto-spawn after last-pane kill/detach** → reveal from the top-left corner.
297+
298+
The direction is carried via `FreshlySpawnedContext` — a `Map<paneId, SpawnDirection>` written by the spawn call site and consumed once by `TerminalPanel`'s `useLayoutEffect` on first mount.
299+
300+
### Kill (in-place fade + FLIP reclaim)
301+
302+
`orchestrateKill(api, killedId)` in `Pond.tsx` runs on kill confirmation. It fades the real pane element in place (its content dissolves against the same-colored background), then removes the panel and FLIP-reveals the survivors:
303+
304+
1. Add `.pane-fading-out` (or `.pane-fading-and-shrinking-to-br` for a last-pane kill) to the killed pane's group element. Block pointer events during the fade.
305+
2. On `animationend`, snapshot `getBoundingClientRect` for every surviving panel's group element.
306+
3. `destroyTerminal` + `api.removePanel`; dockview snaps the layout.
307+
4. Measure post-rects. Any panel whose rect grew is a "grower."
308+
5. For each grower, apply an inline `clip-path: inset(...)` with the newly-claimed territory clipped off, force a reflow, then transition to `inset(0)`. This reveals the grower into the vacated space without affecting `getBoundingClientRect`. Clears on `transitionend`.
309+
310+
Case handling is purely rect-based (measure before and after removal), so 2-pane splits, linear 3+ rows/columns, and nested splits all fall through the same code path with no per-case branching.
311+
312+
### Auto-spawn delay
313+
314+
When `onDidRemovePanel` triggers the "always keep one pane visible" auto-spawn (see corner case #10), the `api.addPanel` call is deferred by 440ms. This lets the outgoing animation (kill ghost crush, or detach's selection-overlay slide to the door) complete before the replacement's reveal starts — they play sequentially in the same screen region instead of fighting each other. The deferred spawn re-checks `totalPanels` at fire time and becomes a no-op if anything repopulated the pane area during the delay (e.g. a door reattach).
315+
316+
The deferred spawn also only calls `selectPanel` if selection is null. The kill handler clears selection to null, so the new pane takes focus. The detach flow sets selection to the just-created door; preserving that door focus across the delay is the point.
317+
286318
## Corner cases
287319

288320
1. **Dual React instance**: dockview bundles its own React. Fixed with `resolve.dedupe: ['react', 'react-dom']` in Vite config.
@@ -294,7 +326,8 @@ Dockview's separator borders, sash handles, and groupview borders are all set to
294326
7. **Asymmetric back-navigation**: breadcrumb tracks last direction + origin for opposite-direction return.
295327
8. **Center drop merges panels**: intercepted at group-level `model.onWillDrop` and converted to a swap.
296328
9. **Group drag has null panelId**: falls back to `api.getGroup(groupId).activePanel.id`.
297-
10. **Auto-spawn on empty**: `onDidRemovePanel` creates a new session when the last pane is removed and no doors exist.
329+
10. **Auto-spawn on empty**: `onDidRemovePanel` creates a new session whenever the last visible pane is removed, whether or not doors exist — there is always a pane visible. The `addPanel` call is delayed 440ms (see "Auto-spawn delay" under Animations) so the outgoing kill/detach animation finishes first.
330+
11. **Door focus survives auto-spawn**: `api.addPanel` auto-activates the new panel, firing `onDidActivePanelChange`. When the current selection is a door (e.g., just-detached last pane), that listener must not flip `selectedId` to the new pane — otherwise `selectedType === 'door'` + `selectedId === newPaneId` desyncs and the door loses its highlight while the SelectionOverlay is stuck on the stale door rect. The listener early-returns when `selectedType === 'door'`.
298331

299332
## Files
300333

0 commit comments

Comments
 (0)