Dock manager

IDE-style dockable workspace built on splitters and tab stacks. Panes can be dragged between stacks, torn off into floating windows, and the resulting arrangement is round-trippable as a JSON DockLayoutSnapshot for persistence.

Basic usage

Keyboard shortcuts
  • Tab / Shift+Tab — move focus through tabstrips, splitter dividers, intersection handles, and floating-pane close buttons
  • ← / → / Home / End — navigate between tabs in the focused stack
  • On a focused splitter divider: ←/→ (or ↑/↓ for vertical splits) resize by 10% (Shift: 1%), Home/End to limits
  • On a focused intersection handle (the round glyph at split crossings): same arrow keys, but ←/→ drives the vertical divider and ↑/↓ drives the horizontal one. Home/End drive the vertical divider to its limits, and Shift is the 1% fine step here too.
  • On a floating pane's edge or corner handle: arrow keys resize along that handle's axis (10px per step, Shift for 1px).
  • Enter / Space on a tab — activate it
  • Floating pane close button (×) — also focusable; Enter / Space closes
  • M on a focused tab — arm pane move mode. Only the bare key arms it; Ctrl/Alt/⌘ + M is left to the browser or the app.
  • In move mode: T / R / B / L docks the pane to the top / right / bottom / left of the current stack · F tears it off into a floating window · Esc cancels
  • In move mode, to reach a stack other than the current one: →/↓ and ←/↑ cycle forward and backward through every drop target — the four sides of each other docked stack, then “float” — announcing “side of pane names, option N of M” and outlining the target. Enter commits the highlighted target; it lands exactly where the equivalent mouse drop would.
  • Move mode stays armed while focus moves around inside the dock and cancels once focus leaves it; the commit always applies to the pane that was focused when M was pressed. Letters typed into an input, textarea, select or contenteditable inside a pane stay text and commit nothing.

Live layout

Drag a pane to capture a layout.

  
    
      
        <bs-dock-manager [(layout)]="layout">
        
    
      
          <ng-container *bsDockManagerPane="let pane">
        
    
      
            @switch (pane) {
        
    
      
              @case ('panel-1') { <div class="p-3"><h3>Panel 1</h3></div> }
        
    
      
              @case ('panel-2') { <div class="p-3"><h3>Panel 2</h3></div> }
        
    
      
              @case ('panel-3') { <div class="p-3"><h3>Panel 3</h3></div> }
        
    
      
            }
        
    
      
          </ng-container>
        
    
      
        </bs-dock-manager>
        
    
  
    
      
        import { Component, signal } from '@angular/core';
        
    
      
        import {
        
    
      
          BsDockManagerComponent,
        
    
      
          BsDockManagerPaneDirective,
        
    
      
          DockLayoutInput,
        
    
      
        } from '@mintplayer/ng-bootstrap/dock';
        
    
      
        
        
    
      
        @Component({
        
    
      
          selector: 'my-workspace',
        
    
      
          templateUrl: './my-workspace.component.html',
        
    
      
          imports: [BsDockManagerComponent, BsDockManagerPaneDirective],
        
    
      
        })
        
    
      
        export class MyWorkspaceComponent {
        
    
      
          layout = signal<DockLayoutInput>({
        
    
      
            root: {
        
    
      
              kind: 'split',
        
    
      
              direction: 'horizontal',
        
    
      
              sizes: [1, 2],
        
    
      
              children: [
        
    
      
                { kind: 'stack', panes: ['panel-1', 'panel-2'], activePane: 'panel-1' },
        
    
      
                { kind: 'stack', panes: ['panel-3'] },
        
    
      
              ],
        
    
      
            },
        
    
      
            floating: [],
        
    
      
            titles: {
        
    
      
              'panel-1': 'Panel 1',
        
    
      
              'panel-2': 'Panel 2',
        
    
      
              'panel-3': 'Panel 3',
        
    
      
            },
        
    
      
          });
        
    
      
        }
        
    

Floating panes

  
    
      
        // Floating panes are tear-off windows positioned in screen pixels.
        
    
      
        // Drag a tab out of its stack — or press M then F on a focused tab —
        
    
      
        // and the dock writes back a layout with the pane moved into the floating[] array.
        
    
      
        layout = signal<DockLayoutInput>({
        
    
      
          root: {
        
    
      
            kind: 'stack',
        
    
      
            panes: ['main'],
        
    
      
            activePane: 'main',
        
    
      
          },
        
    
      
          floating: [
        
    
      
            {
        
    
      
              id: 'floating-inspector',
        
    
      
              root: { kind: 'stack', panes: ['inspector'], activePane: 'inspector' },
        
    
      
              activePane: 'inspector',
        
    
      
              bounds: { left: 680, top: 96, width: 320, height: 220 },
        
    
      
            },
        
    
      
          ],
        
    
      
          titles: { main: 'Editor', inspector: 'Inspector' },
        
    
      
        });
        
    

Capture & restore layout

  
    
      
        // Persist the user's arrangement. captureLayout() returns a fresh
        
    
      
        // DockLayoutSnapshot you can JSON.stringify into localStorage / a DB.
        
    
      
        readonly dockManager = viewChild(BsDockManagerComponent);
        
    
      
        
        
    
      
        saveLayout(): void {
        
    
      
          const snapshot = this.dockManager()?.captureLayout();
        
    
      
          if (snapshot) localStorage.setItem('dock-layout', JSON.stringify(snapshot));
        
    
      
        }
        
    
      
        
        
    
      
        restoreLayout(): void {
        
    
      
          const raw = localStorage.getItem('dock-layout');
        
    
      
          if (raw) this.layout.set(JSON.parse(raw));
        
    
      
        }