Skip to content

feat(auto-hide edge groups): VS pinnable tool windows + reusable core primitives#1379

Merged
mathuo merged 12 commits into
v8-branchfrom
feat/auto-hide-edge-groups
Jun 28, 2026
Merged

feat(auto-hide edge groups): VS pinnable tool windows + reusable core primitives#1379
mathuo merged 12 commits into
v8-branchfrom
feat/auto-hide-edge-groups

Conversation

@mathuo

@mathuo mathuo commented Jun 24, 2026

Copy link
Copy Markdown
Owner

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)

  • Auto-hidden — the collapsed edge group shows its tabs as a strip. Click a tab to peek that panel out as a non-reflowing overlay with a title bar (title + pin + close); click the tab again / click outside / Esc to hide; click another tab to switch.
  • Pinned — the pushpin docks the group: it renders with the title bar on top and the tab strip at the bottom; the pushpin auto-hides it back to the strip. Close closes the panel.
  • Both render modes (onlyWhenVisible and always) slide out; a fixed clip frame makes the panel emerge from the strip's inner edge.
  • Opt-in via 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-core rather than being bespoke to one module:

  • createDismissableLayer — shared Esc / outside-pointerdown / resize dismissal; PopupService and the peek both consume it.
  • DockviewGroupPanelModel.getPanelForTab — robust tab-element → panel lookup (inverse of getTabId).
  • prefersReducedMotion, resolveOpaqueBackground (dom), createCloseButton / createPinButton (svg).
  • 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 panel.

Removability

Module absent → no peek/dock chrome, the *EdgeGroup apis no-op, and edge-group collapse still works (EdgeGroupModule).

Tests

dockview-core + dockview-modules unit 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

mathuo and others added 2 commits June 24, 2026 22:14
…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>
@mathuo mathuo changed the title feat(dockview-modules): auto-hide edge groups — Phase 1 (collapsed-strip activators) feat(dockview-modules): auto-hide edge groups — Phases 1 + 2 (strip + slide-out peek) Jun 24, 2026
@mathuo

mathuo commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Added Phase 2 — the slide-out peek (same branch/PR).

  • Clicking a collapsed edge group's activator now peeks (slides the panel out as a non-reflowing overlay) instead of 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.
  • Limited to onlyWhenVisible renderers; an always-renderer panel falls back to pinning (its live element lives in the shared render overlay — later phase).
  • Core seam (additive): floatingOverlayHost + getEdgeGroupExpandedSize on the host; peek() on the service; public api.peekEdgeGroup(position, peek).

Verified in a real browser (the no-reflow rule needs real layout): new e2e auto-hide-edge-groups.spec.ts — peek slides out, main content width unchanged (no reflow), pin reflows once. Plus jsdom: click peeks + reparents + stays collapsed, pin re-docks, Esc restores content. 1081 core + 133 modules + 10 e2e green; lint 0 errors.

PR now covers Phases 1 + 2. Remaining: hover/focus debounce state machine (3), slide animation + isPeeking (4), pinned serialization (5), a11y (6), and always-renderer peek.

…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>
@mathuo mathuo changed the title feat(dockview-modules): auto-hide edge groups — Phases 1 + 2 (strip + slide-out peek) feat(dockview-modules): auto-hide edge groups — Phases 1–3 (strip + peek + hover/focus) Jun 24, 2026
@mathuo

mathuo commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Added Phase 3 — hover/focus peek + debounce (same branch/PR).

  • Hover a strip activator → peek opens after openDelay (250ms default). Keyboard focus opens immediately. Click still opens immediately too.
  • Leaving both the strip and the overlay closes after closeDelay (300ms). A single shared close timer, cancelled by re-entering either — the fix for pointer-leave flicker crossing the strip↔overlay gap. focusout of the whole subtree also closes.
  • New options openDelay / closeDelay.

Test: 4 jsdom fake-timer cases (opens after openDelay, closes after closeDelay, re-enter cancels the close = no flicker, focus opens immediately) + e2e (real hover opens, moving away closes). 1081 core + 137 modules + 11 e2e green; lint 0 errors.

PR now covers Phases 1–3 (strip activators · slide-out peek · hover/focus + debounce). Remaining: slide animation + isPeeking api (4), pinned serialization (5), a11y (6), and always-renderer peek.

…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>
@mathuo mathuo changed the title feat(dockview-modules): auto-hide edge groups — Phases 1–3 (strip + peek + hover/focus) feat(dockview-modules): auto-hide edge groups — Phases 1–4 Jun 24, 2026
@mathuo

mathuo commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Added Phase 4 — slide animation + observable peek state.

  • Slide-in animation: the peek overlay slides in from the strip edge (transform transition); respects prefers-reduced-motion + the new animate option (default true). Purely cosmetic — no layout impact.
  • group.api.isPeeking() + onDidPeekChange (new DockviewGroupPanelPeekChangeEvent). The module records peek state via a new host hook setEdgeGroupPeeking; the component tracks it (_peekingGroups) and fires the group event.

Test: isPeeking() flips true/false with the peek and onDidPeekChange fires [true, false]. 1081 core + 138 modules + e2e green; lint 0 errors; gen updated.

PR now covers Phases 1–4 (strip · peek · hover/focus+debounce · animation+isPeeking). Remaining: pinned serialization (5), a11y (6), always-renderer peek.

mathuo and others added 2 commits June 25, 2026 20:56
… 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>
@mathuo

mathuo commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

Both render modes now slide out (previously always panels fell back to pinning).

The peek reparents the content container (not the panel content) into the slide-out overlay:

  • onlyWhenVisible content rides inside the container.
  • always content is never reparented — it stays in the shared render overlay (that's how always preserves state); 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 so always content stays synced frame-by-frame.

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 always) + 4 e2e (hover peek · always slide · click pin · pin button). 1081 core + 135 modules + 12 e2e green; lint clean.

mathuo and others added 5 commits June 25, 2026 21:36
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>
@mathuo mathuo changed the title feat(dockview-modules): auto-hide edge groups — Phases 1–4 feat(auto-hide edge groups): VS pinnable tool windows + reusable core primitives Jun 26, 2026
- 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>
@sonarqubecloud

Copy link
Copy Markdown

@mathuo mathuo merged commit d254624 into v8-branch Jun 28, 2026
9 checks passed
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