You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Preserve door focus across auto-spawn; document animations
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>
Copy file name to clipboardExpand all lines: docs/specs/layout.md
+39-1Lines changed: 39 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -283,6 +283,43 @@ Colors use a two-layer CSS variable strategy: `@theme --color-*` tokens → `var
283
283
284
284
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)`.
285
285
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 (ghost crush + FLIP reclaim)
301
+
302
+
`orchestrateKill(api, killedId)` in `Pond.tsx` runs on kill confirmation:
303
+
304
+
1. Snapshot `getBoundingClientRect` for every other panel's group element.
305
+
2.`destroyTerminal` + `api.removePanel`; dockview snaps the layout.
306
+
3. Measure post-rects. Any panel whose rect grew is a "grower."
307
+
4. Pick a crush direction by comparing growers' post-rect centers against the killed rect's center:
308
+
- Growers to one side only (horizontal axis) → `.pane-crushing-to-{left,right}`.
309
+
- Growers to one side only (vertical axis) → `.pane-crushing-{up,down}`.
310
+
- Growers on both sides of the killed pane's center (horizontal or vertical) → `.pane-crushing-to-{hcenter,vcenter}` (middle-kill crush).
311
+
- No growers (last-pane kill) → `.pane-crushing-to-br` (crushes toward bottom-right corner, opposite of where the auto-spawned replacement reveals from).
312
+
5. Mount a solid `var(--color-surface)` ghost overlay at the killed rect (`position: fixed`, `z-index: 55`) with that class; it removes itself on `animationend` with a 1s timeout safety net.
313
+
6. 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`.
314
+
315
+
Case detection 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.
316
+
317
+
### Auto-spawn delay
318
+
319
+
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).
320
+
321
+
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.
322
+
286
323
## Corner cases
287
324
288
325
1.**Dual React instance**: dockview bundles its own React. Fixed with `resolve.dedupe: ['react', 'react-dom']` in Vite config.
@@ -294,7 +331,8 @@ Dockview's separator borders, sash handles, and groupview borders are all set to
294
331
7.**Asymmetric back-navigation**: breadcrumb tracks last direction + origin for opposite-direction return.
295
332
8.**Center drop merges panels**: intercepted at group-level `model.onWillDrop` and converted to a swap.
296
333
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 whenever the last visible pane is removed, whether or not doors exist — there is always a pane visible. The new pane receives the just-removed pane's id as `paneToCopy` for future cwd/terminal-kind inheritance.
334
+
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 new pane receives the just-removed pane's id as `paneToCopy` for future cwd/terminal-kind inheritance. The `addPanel` call is delayed 440ms (see "Auto-spawn delay" under Animations) so the outgoing kill/detach animation finishes first.
335
+
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'`.
0 commit comments