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
Copy file name to clipboardExpand all lines: docs/specs/layout.md
+34-1Lines changed: 34 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -283,6 +283,38 @@ 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 (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
+
286
318
## Corner cases
287
319
288
320
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
294
326
7.**Asymmetric back-navigation**: breadcrumb tracks last direction + origin for opposite-direction return.
295
327
8.**Center drop merges panels**: intercepted at group-level `model.onWillDrop` and converted to a swap.
296
328
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'`.
0 commit comments