v2.6.0

<bmx-dock-layout>

A docking workspace, as in an IDE or a trading desk: the page's panels are arranged in resizable rows and columns of tabbed groups. Drag a tab to another group, to a side of one (a new group is made there), to an edge of the whole workspace, or out into a floating window; fold a group to an edge where it slides out when wanted; maximise one for a while; close a panel and reopen it where it was. Everything can be done from the keyboard too, including the moves - Alt+Shift+M opens a dialog that chooses a place without a mouse.

6 properties · 3 events · 12 methods · 5 parts

Example

HP-1042 - Fenwick Joinery

Oak shelving, 12 units - due Thursday

Depot
Leeds North
Carrier
Northline, van 7
Window
08:00 - 12:00
Value
£4,820.00

    ⚠️ HP-1039: the delivery window overlaps the depot's closing time.

    ⚠️ HP-1047: no carrier booked.

    Drag a tab into another group, onto a side of one, to an edge, or hold Shift to float it.

    Alt+Shift+M moves the panel from the keyboard; Alt+Shift+arrows send it to the next group.

    F6 goes to the next group; Shift+F10 opens a tab's options.

    Drag the tabs about, fold the Shortcuts group to an edge from its options, or type in Notes - it keeps what you typed wherever it goes.

    Show markup
    <bmx-dock-layout id="ex-dl" style="--bmx-dock-layout-height: 34rem">
      <nav data-panel="orders" data-title="Orders" data-icon="📦" data-closable="false" style="padding: 8px; overflow: auto">
        <ul id="ex-dl-orders" style="list-style: none; margin: 0; padding: 0; display: grid; gap: 4px"></ul>
      </nav>
      <section data-panel="details" data-title="Order HP-1042" data-closable="false" style="padding: 16px; overflow: auto">
        <h3 id="ex-dl-heading" style="margin: 0 0 4px">HP-1042 - Fenwick Joinery</h3>
        <p id="ex-dl-summary" style="margin: 0 0 12px; color: var(--bmx-text-muted)">Oak shelving, 12 units - due Thursday</p>
        <dl style="display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0">
          <dt style="color: var(--bmx-text-muted)">Depot</dt><dd style="margin: 0">Leeds North</dd>
          <dt style="color: var(--bmx-text-muted)">Carrier</dt><dd style="margin: 0">Northline, van 7</dd>
          <dt style="color: var(--bmx-text-muted)">Window</dt><dd style="margin: 0">08:00 - 12:00</dd>
          <dt style="color: var(--bmx-text-muted)">Value</dt><dd style="margin: 0">£4,820.00</dd>
        </dl>
      </section>
      <div data-panel="notes" data-title="Notes" style="display: flex; padding: 8px">
        <label for="ex-dl-notes" style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%)">Notes on this order</label>
        <textarea id="ex-dl-notes" style="flex: 1; resize: none; font: inherit; padding: 8px; border: 1px solid var(--bmx-border); border-radius: 6px; background: var(--bmx-surface); color: var(--bmx-text)">Call the site manager on arrival - loading bay 2.</textarea>
      </div>
      <div data-panel="activity" data-title="Activity" style="padding: 8px 12px; overflow: auto">
        <ol id="ex-dl-activity" style="margin: 0; padding-inline-start: 18px; display: grid; gap: 4px; color: var(--bmx-text-muted)"></ol>
      </div>
      <div data-panel="problems" data-title="Problems" data-badge="2" style="padding: 8px 12px; overflow: auto">
        <p style="margin: 0 0 6px">⚠️ HP-1039: the delivery window overlaps the depot's closing time.</p>
        <p style="margin: 0">⚠️ HP-1047: no carrier booked.</p>
      </div>
      <div data-panel="help" data-title="Shortcuts" data-icon="⌨️" style="padding: 8px 12px; overflow: auto; font-size: 0.8125rem">
        <p style="margin: 0 0 6px"><strong>Drag a tab</strong> into another group, onto a side of one, to an edge, or hold Shift to float it.</p>
        <p style="margin: 0 0 6px"><strong>Alt+Shift+M</strong> moves the panel from the keyboard; <strong>Alt+Shift+arrows</strong> send it to the next group.</p>
        <p style="margin: 0"><strong>F6</strong> goes to the next group; <strong>Shift+F10</strong> opens a tab's options.</p>
      </div>
    </bmx-dock-layout>
    <p class="note" id="ex-dl-out" role="status" style="margin: 12px 0 0">Drag the tabs about, fold the Shortcuts group to an edge from its options, or type in Notes - it keeps what you typed wherever it goes.</p>
    
    <script type="module">
      await customElements.whenDefined('bmx-dock-layout');
      const ws = document.getElementById('ex-dl');
      const out = document.getElementById('ex-dl-out');
      const orders = [
        ['HP-1042', 'Fenwick Joinery', 'Oak shelving, 12 units - due Thursday'],
        ['HP-1039', 'Castle Interiors', 'Pine desks, 4 units - due today'],
        ['HP-1047', 'Marsh & Co', 'Fittings, 30 boxes - due Friday'],
        ['HP-1050', 'Old Mill Studio', 'Oak shelving, 2 units - due Monday'],
      ];
      const list = document.getElementById('ex-dl-orders');
      for (const [id, customer] of orders) {
        const li = document.createElement('li');
        const button = document.createElement('button');
        button.type = 'button';
        button.textContent = `${id} · ${customer}`;
        button.style.cssText = 'width: 100%; text-align: start; font: inherit; padding: 6px 8px; border: 0; border-radius: 6px; background: transparent; color: var(--bmx-text); cursor: pointer';
        button.addEventListener('click', () => {
          const [, name, summary] = orders.find(o => o[0] === id);
          document.getElementById('ex-dl-heading').textContent = `${id} - ${name}`;
          document.getElementById('ex-dl-summary').textContent = summary;
          ws.querySelector('[data-panel="details"]').dataset.title = `Order ${id}`;
          ws.openPanel('details');
        });
        li.append(button);
        list.append(li);
      }
      document.getElementById('ex-dl-activity').innerHTML = '<li>08:12 HP-1042 picked</li><li>08:40 HP-1039 loaded on van 3</li><li>09:05 HP-1047 awaiting a carrier</li>';
    
      // The layout in the shorthand: a row of three - the orders list, a column (details over notes) and the activity group.
      ws.layout = {
        row: [
          { stack: ['orders'], size: 22 },
          { column: [{ stack: ['details'], size: 62 }, { stack: ['notes', 'problems'], size: 38 }], size: 50 },
          { stack: ['activity', 'help'], size: 28 },
        ],
      };
    
      // Typing in the notes marks the tab with an unsaved-changes dot, and the page refuses to close it until it is saved.
      const notes = ws.querySelector('[data-panel="notes"]');
      document.getElementById('ex-dl-notes').addEventListener('input', () => notes.setAttribute('data-dirty', ''));
      ws.addEventListener('bmxClose', e => {
        if (e.detail.panel === 'notes' && notes.hasAttribute('data-dirty')) {
          e.preventDefault();
          out.textContent = 'bmxClose was cancelled: Notes has unsaved changes, so it stays open. (Your page would offer to save here.)';
        }
      });
      ws.addEventListener('bmxChange', e => {
        const layout = e.detail.layout;
        out.textContent = `bmxChange: ${e.detail.reason}${e.detail.panels.length ? ` (${e.detail.panels.join(', ')})` : ''}. ${layout.floating.length} floating, ${layout.autoHide.length} folded to an edge, ${layout.closed.length} closed.`;
      });
    </script>
    <bmx-dock-layout id="ws" persist-key="my-app">
      <div data-panel="files" data-title="Files">...</div>
      <div data-panel="editor" data-title="Editor" data-closable="false">...</div>
      <div data-panel="terminal" data-title="Terminal">...</div>
    </bmx-dock-layout>
    <script>
      ws.layout = { row: [{ stack: ['files'], size: 20 }, { column: ['editor', { stack: ['terminal'], size: 30 }], size: 80 }] };
      ws.addEventListener('bmxChange', e => save(e.detail.layout));
    </script>
    

    Panels are the page's own elements, marked data-panel. They are never moved in the document - each is shown through a slot in whichever group holds it - so a panel keeps its state (scroll position, an editor's undo, a playing video, an iframe that must not reload) wherever it is dragged. The layout is plain JSON in layout and every bmxChange; nothing is stored unless persist-key asks for the browser's local storage.

    Accessibility: each group is a tab list with its tab panel; F6 moves between groups and floating windows; splitters are focusable separators with their value; every move, float, fold and close is announced. Narrower than compact-below, the workspace becomes one tab list of every open panel, so it works on a phone without changing the layout it keeps.

    Properties

    PropertyAttributeTypeDefaultDescription
    compactBelow compact-below number 600 Below this width (px) the workspace is shown as one tab list of every open panel. 0 never does.
    label label string — The workspace's accessible name. Default: "Workspace".
    layout layout BmxDockArrangement | Record<string, unknown> | unknown[] | string '' The arrangement: the object bmxChange gives, JSON, or the shorthand ({ row: [...] }, { column: [...] }, { stack: ['a', 'b'], size: 30 }, a panel id for a group of one, an array for a row). Mutable: every change is written back as the full object. Panels the layout does not name join the first group.
    locked locked boolean false No dragging, closing, floating or folding; the groups can still be resized and their tabs chosen.
    persistKey persist-key string — A name to keep the layout under in the browser's local storage, so it survives a reload. Nothing is stored without it. A stored layout wins over the first one the page sets after loading, which becomes the layout resetLayout goes back to.
    strings strings Partial<BmxDockLayoutStrings> | string — Words to show instead of the English ones, as an object or JSON.

    Events

    EventDetailDescription
    bmxActivate BmxDockActivateDetail Fired when a panel becomes the one showing in its group.
    bmxChange BmxDockChangeDetail Fired when the layout changes, with the whole new layout.
    bmxClose BmxDockCloseDetail Fired before the user closes a panel. Cancel it to keep the panel open.

    Methods

    MethodSignatureDescription
    autoHide autoHide(panel: string, edge?: BmxDockSide) => Promise<void> Folds the group holding a panel to an edge (the nearest, if none is given).
    closePanel closePanel(panel: string) => Promise<void> Closes a panel (no bmxClose event: the page asked).
    floatPanel floatPanel(panel: string, rect?: Partial<BmxDockRect>) => Promise<void> Takes a panel out into a floating window (at rect, in pixels from the workspace's top-left corner, if given).
    getLayout getLayout() => Promise<BmxDockArrangement> The layout as it is now: plain JSON, to store and hand back through layout or setLayout.
    getPanels getPanels() => Promise<BmxDockPanelInfo[]> The panels the page has, with the titles and settings read from their attributes.
    maximise maximise(panel?: string) => Promise<void> Fills the workspace with the group holding a panel; no panel restores.
    movePanel movePanel(panel: string, target: BmxDockTarget) => Promise<void> Moves a panel into a group ({ stack, position: 'center', index }) or beside one, or to an edge of the workspace (no stack).
    openPanel openPanel(panel: string, target?: BmxDockTarget) => Promise<void> Shows a panel: brings it to the front of its group, slides out its folded group, or reopens it where it was.
    resetLayout resetLayout() => Promise<void> Goes back to the layout the page last set through layout (its default), whatever the user or storage changed since.
    restore restore() => Promise<void> Puts a maximised group back among the others.
    setFocus setFocus(panel?: string) => Promise<void> Puts keyboard focus on a panel's tab (default: the first group's showing panel).
    setLayout setLayout(layout: BmxDockArrangement | Record<string, unknown> | unknown[] | string) => Promise<void> Replaces the layout (the full form or the shorthand). resetLayout still goes back to the first one.

    Slots

    SlotDescription
    empty Shown when every panel is closed.

    CSS shadow parts

    PartDescription
    group A group of tabbed panels; also group-floating, group-flyout.
    splitter The bar between two groups.
    strip An edge strip of auto-hidden panels.
    tab A panel's tab; also tab-active.
    window A floating window.

    CSS custom properties

    PropertyDescription
    --bmx-dock-layout-accent The active tab's marker, the drop preview and the guides.
    --bmx-dock-layout-background Behind the groups (the gaps between them).
    --bmx-dock-layout-gap The width of a splitter.
    --bmx-dock-layout-group-background A group's body.
    --bmx-dock-layout-header-background A group's tab row.
    --bmx-dock-layout-height The workspace's height.
    --bmx-dock-layout-radius Corner radius of the groups and windows.
    --bmx-dock-layout-tab-height The height of a tab row.