feat(auto-hide edge groups): VS pinnable tool windows + reusable core primitives#1379
Conversation
…rip activators) VS Code-style "auto hide" for edge groups, built on the free edge-group collapse machinery (never re-implements layout/sizing). - New `AutoHideEdgeGroupModule` (in `dockview-modules`, `dependsOn: [EdgeGroupModule]`): a collapsed edge group renders clickable activators (one per panel) in its strip; clicking one pins (expands) the group and activates that panel. The strip tracks panel add/remove and shows only while collapsed. - Opt-in via `autoHideEdgeGroups` (default off → today's empty baseline strip is unchanged). Public api `pinEdgeGroup(position)` / `autoHideEdgeGroup(position)`. - Core seam (additive): `IAutoHideEdgeGroupHost` / `IAutoHideEdgeGroupService` in moduleContracts, `autoHideEdgeGroupService` slot, `getEdgeGroupPanel` host method, the `autoHideEdgeGroups` option type, and `EdgeGroupModule` exported for `dependsOn`. The component implements the host (all but `getEdgeGroupPanel` already existed). Pin/collapse always go through `setEdgeGroupCollapsed`. Scope: Phase 1 (strip + click-to-pin). The slide-out peek overlay, hover/focus state machine, animation, `pinned` serialization and a11y are later phases. Removability holds: module absent → no activators, `pinEdgeGroup`/ `autoHideEdgeGroup` no-op, edge collapse still works. Test: `autoHideEdgeGroups.spec.ts` (5) — activators per panel on collapse, click pins + clears, api toggle, tracks panel add/remove, off=baseline. 1081 core + 131 modules green; lint 0 errors; gen clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking a collapsed edge group's activator now slides the panel out as a non-reflowing overlay (peek), rather than pinning. A pin button re-docks it; Esc / pointer-down outside closes it. - The live content element is reparented onto the shared floating-overlay host (state preserved) and restored on close; the splitview view stays locked at collapsedSize, so the grid does NOT reflow during a peek. - Overlay is anchored to the strip's inner edge and sized to the group's expanded size, per edge; sits above grid content with its own pointer events. - Limited to `onlyWhenVisible` renderers; an `always`-renderer panel falls back to pinning (its live element lives in the shared render overlay — a later phase). - Core seam (additive): `floatingOverlayHost` + `getEdgeGroupExpandedSize` on the host (+ `ShellManager.getEdgeGroupExpandedSize`); `peek()` on the service; public `api.peekEdgeGroup(position, peek)`. Scope: peek open/close + pin + Esc/outside-close. Hover/focus debounce, slide animation, `pinned` serialization and a11y are later phases. Test: `autoHideEdgeGroups.spec.ts` updated (jsdom: click peeks + reparents + no-reflow, pin re-docks, Esc restores, api toggles) and new e2e `auto-hide-edge-groups.spec.ts` (real browser: peek slides out, main content width unchanged = no reflow, pin reflows once). 1081 core + 133 modules + 10 e2e green; lint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Added Phase 2 — the slide-out peek (same branch/PR).
Verified in a real browser (the no-reflow rule needs real layout): new e2e PR now covers Phases 1 + 2. Remaining: hover/focus debounce state machine (3), slide animation + |
…peek + debounce) The peek now opens on hover/focus, not just click, with the VS-style timing. - **Hover** a strip activator → peek opens after `openDelay` (default 250ms). - **Keyboard focus** an activator → opens immediately (discoverability). - **Leave** both the strip and the overlay → closes after `closeDelay` (default 300ms). A **single shared close timer**, cancelled by re-entering either the strip or the overlay — the fix for pointer-leave flicker crossing the gap between them. `focusout` of the whole peek subtree also closes. - Click still opens immediately (kept). New options `openDelay` / `closeDelay`. Test: 4 jsdom cases with fake timers (hover opens after openDelay, leave closes after closeDelay, re-enter cancels the close = no flicker, focus opens immediately) + an e2e (real hover opens the peek, moving away closes it). 1081 core + 137 modules + 11 e2e green; lint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Added Phase 3 — hover/focus peek + debounce (same branch/PR).
Test: 4 jsdom fake-timer cases (opens after PR now covers Phases 1–3 (strip activators · slide-out peek · hover/focus + debounce). Remaining: slide animation + |
…ion + isPeeking) - **Slide-in animation**: the peek overlay slides in from the strip edge via a transform transition; respects `prefers-reduced-motion` and the new `animate` option (default true). Cosmetic — no layout impact. - **Observable peek state**: `group.api.isPeeking()` + `onDidPeekChange` (new `DockviewGroupPanelPeekChangeEvent`). The module records peek state through a new host hook `setEdgeGroupPeeking`, which the component tracks (`_peekingGroups`) and surfaces via `isEdgeGroupPeeking` + the group api's `_onDidPeekChange`. Test: a module test asserts `isPeeking()` flips true on peek / false on close and `onDidPeekChange` fires `[true, false]`. 1081 core + 138 modules + e2e green; lint 0 errors; gen updated (`DockviewGroupPanelPeekChangeEvent`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Added Phase 4 — slide animation + observable peek state.
Test: PR now covers Phases 1–4 (strip · peek · hover/focus+debounce · animation+isPeeking). Remaining: |
… peek styling A collapsed edge group already renders its tabs along the strip, so the parallel "activator strip" was redundant. Reuse the native tabs as the peek triggers instead: - **Triggers**: hover / focus / click the collapsed strip's own tabs (no separate activator DOM). Inherits the tabs' existing theming + ARIA. - **Mount**: the peek mounts on the shell overlay root (`host.overlayRoot`) — the same element the `OverlayRenderContainer` roots on — and fills it via inline styles so layout never depends on the consumer's stylesheet. - **Styling**: `.dv-edge-peek` gets a border + shadow so the slid-out panel reads as floating, and a floated `Pin` header button (cosmetic only). - **Scope**: peek currently supports `onlyWhenVisible` panels; an `always`-rendered panel (whose live element lives in the shared render overlay, a different coordinate space) falls back to pinning — proper render-overlay re-anchoring is a dedicated follow-up. Drops the now-unused `showIcons` option. Tests rewritten around the native-tab triggers (135 modules green); e2e fixture now loads the stylesheet so overlay positioning matches real usage (11 e2e green); 1081 core green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously only `onlyWhenVisible` panels peeked; `always` panels fell back to pinning. Now both slide out. The peek reparents the content CONTAINER (not the panel content) into the slide-out overlay: - `onlyWhenVisible` content lives inside the container and moves with it. - `always` content is never reparented — it stays in the shared render overlay (that's how `always` works). The container is just the box that overlay anchors to, so a new host hook `repositionOverlays()` re-runs the render overlay positioning as the container slides. The slide is a manual rAF loop (not a CSS transition) so every frame also re-anchors the `always` content over the container's moving box — otherwise the two modes would desync (inside-overlay content slides, render-overlay content jumps). Interaction model is now VS-style: **hover / keyboard-focus** the collapsed strip's native tab → peek; **click** the tab → natively expands (pins) the group. Clicking previously raced the native expand; making it the explicit pin gesture removes that conflict. Focus-triggered peeks are deferred out of the focus event (a synchronous reparent mid-dispatch makes the group auto-expand). e2e fixture now parametrises the edge panel renderer + exposes `peekEdge`; a new e2e asserts an `always` panel slides over the peek with its parent unchanged. 1081 core + 135 modules + 12 e2e green; lint clean; gen unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Both render modes now slide out (previously The peek reparents the content container (not the panel content) into the slide-out overlay:
Interaction is now VS-style: hover / keyboard-focus a collapsed tab → peek; click → natively expands (pins). (Clicking raced the native expand; making it the explicit pin gesture removes the conflict.) Verified live in the docs demo (which uses |
The peek floats over other content, so a transparent background lets the grid show through. It defaulted to `var(--dv-group-view-background-color)` via the stylesheet, but if the consumer hasn't set that variable the peek was see-through. Derive an opaque background from the live group at peek time (walking up to the first opaque ancestor), set inline, so the peek is never transparent regardless of theming. 136 modules + 4 e2e green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (no visibility fighting) Replaces the broad `repositionOverlays()` (updateAllPositions) with a targeted `repositionPanelOverlay(panel, forceVisible)`: - updateAllPositions deliberately SKIPS panels it considers not-visible (an optimization). A collapsed edge panel can read as not-visible, so the peek's `always` content was never repositioned into the peek and stayed stranded. - The new path repositions a single panel's render overlay over its reference container directly, optionally force-showing it — so the peek slides the `always` panel's own render element (never reparented, parent stays constant) without depending on, or mutating, the panel's visibility state. OverlayRenderContainer gains `repositionPanelOverlay(panelId, forceVisible)` and `resize(forceVisible)`. 1081 core + 136 modules + 4 e2e green; lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… peek backdrop An `always`-rendered panel's content lives in a separate render overlay (sibling of the peek), so the peek's opaque backdrop (z-index 999) painted over it — the content was positioned correctly but hidden. It looked like an empty peek. Fix the z-layering: peek backdrop (999) < peeked render overlay (1000) < Pin header (1001). The Pin header is now a separate sibling overlay (click-through except the button) so it can sit above the always content; `repositionPanelOverlay` lifts the force-shown overlay's z-index. Also: keep-open is now driven by pointer GEOMETRY (the peek's box) rather than element enter/leave on the peek — hovering an `always` panel's content (a sibling overlay on top) now keeps the peek open. `_scheduleClose` is idempotent so the document pointermove can't perpetually restart the timer. e2e now asserts STACKING (elementFromPoint over the peek is the content, not the backdrop) — the position-only check gave a false pass. 1081 core + 136 modules + 4 e2e green; lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… core primitives Redesign the auto-hide edge-group interaction to the Visual Studio tool-window model, and extract the generic glue it sits on into dockview-core. Auto-hide module: - Click-driven peek (hover removed): click a collapsed tab to peek that panel as a non-reflowing overlay; click again / outside / Esc to hide; empty strip space is a no-op; click another tab to switch. - Peek and docked share a title bar (panel title, pin, close) — the tab close icon plus a monotone pin icon; pin docks, close closes the panel. - Pinned/docked = tool-window chrome: tabs moved to the bottom (setHeaderPosition) with the title bar on top; the pushpin auto-hides back to the strip. Reconciled with collapse state (deferred init so a collapsed strip is never docked before the shell settles). - A fixed clip frame makes the panel emerge from the strip's inner edge. Core primitives: - createDismissableLayer — shared Esc / outside-pointerdown / resize dismissal; PopupService and the peek consume it. - DockviewGroupPanelModel.getPanelForTab — robust tab-element -> panel lookup (inverse of getTabId). - dom: prefersReducedMotion, resolveOpaqueBackground. - svg: createPinButton (and export createCloseButton). - OverlayRenderContainer.repositionPanelOverlay gains an optional clip rect, with sticky per-panel force-visible/clip state so a concurrent resize can't blank a peeked always-rendered panel. dockview-core + dockview-modules unit suites and the auto-hide Playwright e2e are all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the new public exports (createDismissableLayer / DismissableLayerOptions, prefersReducedMotion, resolveOpaqueBackground, createCloseButton, createPinButton) to __generated__/dockview-core-exports.txt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Close the peek when focus leaves it (the VS "slide back on focus loss"). Generalized into `createDismissableLayer` as a `focusOut` signal with an `isFocusInside` predicate (geometry, since `always` content is a sibling overlay) — PopupService is a latent second consumer. The peek now consumes the layer's full dismissal set (Esc + outside-pointerdown + resize + focus-out) rather than a bespoke handler. - Drop the title-bar layout inline styles that duplicated `overlay.scss` (keep only the dynamic opaque background). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|



VS-style auto-hide edge groups — an edge group can collapse to a strip and behave like a Visual Studio tool window: click a tab to peek it out over the content, pin it to dock, auto-hide it back. Built on the existing edge-group collapse machinery (never re-implements layout / sizing / DnD).
Interaction (Visual Studio tool windows — click-driven, no hover)
Escto hide; click another tab to switch.onlyWhenVisibleandalways) slide out; a fixed clip frame makes the panel emerge from the strip's inner edge.autoHideEdgeGroups(default off → unchanged). API:api.peekEdgeGroup/api.pinEdgeGroup/api.autoHideEdgeGroup;group.api.isPeeking()+onDidPeekChange.Reusable core primitives (extracted)
The generic glue the feature needs now lives in
dockview-corerather than being bespoke to one module:createDismissableLayer— shared Esc / outside-pointerdown / resize dismissal;PopupServiceand the peek both consume it.DockviewGroupPanelModel.getPanelForTab— robust tab-element → panel lookup (inverse ofgetTabId).prefersReducedMotion,resolveOpaqueBackground(dom),createCloseButton/createPinButton(svg).OverlayRenderContainer.repositionPanelOverlaygains an optional clip rect, with sticky per-panel force-visible / clip state so a concurrent resize can't blank a peekedalwayspanel.Removability
Module absent → no peek/dock chrome, the
*EdgeGroupapis no-op, and edge-group collapse still works (EdgeGroupModule).Tests
dockview-core+dockview-modulesunit suites and a Playwright e2e (peek / click-toggle / outside-close / pin→dock / docked geometry /always-clip / tab-switch) all green. Format + exports-gen snapshot updated.Targets
v8-branch.🤖 Generated with Claude Code