# Shell Plugin

> Wrap the grid with a header bar (title + toolbar) and a collapsible tool-panel sidebar.

The Shell plugin wraps the grid with an optional **header bar** (title + toolbar) and a
collapsible **tool panel** sidebar. Features like `visibility`, `filtering`, and `pivot`
register their tool panels into this shell.

:::note[On by default in v2.x — opt-in at v3]
In **v2.x** the shell still ships **on by default**, so existing code keeps working without
a code change. It is **deprecated** in that form: from **v3.0.0** the shell becomes opt-in and
you must enable the feature explicitly (see [Migrating to the feature API](#migrating-to-the-feature-api)).
New code should enable it now via `import '@toolbox-web/grid/features/shell'` + `features: { shell: true }`.
:::

## Installation

```ts
import '@toolbox-web/grid/features/shell';
```

The feature import registers the `ShellPlugin` and augments `features` with a typed `shell`
option. Shell configuration types (`ShellConfig`, `ToolPanelDefinition`, …) are exported from
`@toolbox-web/grid/plugins/shell`.

## Basic Usage

Enable the shell by setting `features: { shell }` with a `header.title`. Features like
`visibility` automatically register a tool panel when the shell is active.

#### TypeScript

```ts
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/shell';
import '@toolbox-web/grid/features/visibility';

const grid = queryGrid('tbw-grid');
grid.gridConfig = {
  features: {
    shell: { header: { title: 'Employee Data' } },
    visibility: true,
  },
};
```

#### React

```tsx
import '@toolbox-web/grid-react/features/visibility';
import { DataGrid } from '@toolbox-web/grid-react';

// Shell auto-registers in v2.x. For v3, opt in explicitly:
// import '@toolbox-web/grid-react/features/shell';
const config = { shell: { header: { title: 'Employee Data' } } };

function MyGrid({ data }) {
  return <DataGrid rows={data} gridConfig={config} visibility />;
}
```

#### Vue

```html
<script setup lang="ts">
import '@toolbox-web/grid-vue/features/visibility';
import { TbwGrid } from '@toolbox-web/grid-vue';

// Shell auto-registers in v2.x. For v3, opt in explicitly:
// import '@toolbox-web/grid-vue/features/shell';
const config = { shell: { header: { title: 'Employee Data' } } };
</script>

<template>
  <TbwGrid :rows="data" :grid-config="config" visibility />
</template>
```

#### Angular

```typescript
// Enables the [visibility] input; the shell auto-registers in v2.x.
// For v3, opt in explicitly: import '@toolbox-web/grid-angular/features/shell';
import { GridVisibilityDirective } from '@toolbox-web/grid-angular/features/visibility';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { GridConfig } from '@toolbox-web/grid';

@Component({
  selector: 'app-shell-grid',
  imports: [Grid, GridVisibilityDirective],
  template: `
    <tbw-grid
      [rows]="rows"
      [gridConfig]="config"
      [visibility]="true"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class ShellGridComponent {
  rows = [];
  config: GridConfig = { shell: { header: { title: 'Employee Data' } } };
}
```

```ts
// ShellBasicDemo.astro
  import '@toolbox-web/grid';
import type { ColumnConfig } from '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/shell';
import '@toolbox-web/grid/features/visibility';
import type { ShellConfig } from '@toolbox-web/grid/plugins/shell';

  const container = document.getElementById('shell-basic-demo-container');
  const grid = queryGrid('#demo-shell-basic');

  if (container && grid) {
    const departments = ['Engineering', 'Sales', 'Marketing', 'Support'];
    const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry'];
    function generateRows(count: number) {
      return Array.from({ length: count }, (_, i) => ({
        id: i + 1,
        name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1),
        department: departments[i % departments.length],
        salary: 50000 + Math.floor(Math.random() * 50000),
        active: i % 3 !== 0,
      }));
    }

    const shellColumns: ColumnConfig[] = [
      { field: 'id', header: 'ID', type: 'number', width: 80 },
      { field: 'name', header: 'Name', minWidth: 150 },
      { field: 'department', header: 'Department', width: 150 },
      { field: 'salary', header: 'Salary', type: 'number', width: 120 },
      { field: 'active', header: 'Active', type: 'boolean', width: 80 },
    ];
    const sampleData = generateRows(20);

    interface ShellValues {
      showTitle: boolean;
      showHeaderContent: boolean;
      showToolbarButton: boolean;
      showVisibilityPlugin: boolean;
      showCustomPanel: boolean;
      panelPosition: string;
      panelMode: string;
    }

    function rebuild(v: ShellValues) {
      const shellConfig: ShellConfig = {
        header: v.showTitle ? { title: 'Employee Data' } : {},
        toolPanel: {
          position: v.panelPosition as 'left' | 'right',
          mode: v.panelMode as 'overlay' | 'push' | 'dropdown',
        },
      };
      grid.gridConfig = {
        columns: shellColumns,
        features: {
          shell: shellConfig,
          ...(v.showVisibilityPlugin ? { visibility: true } : {}),
        },
      };
      grid.rows = sampleData;

      const shell = grid.getPluginByName('shell');
      if (v.showHeaderContent) {
        shell?.registerHeaderContent({
          id: 'row-count',
          order: 10,
          render: (el) => {
            const span = document.createElement('span');
            span.style.cssText = 'font-size:13px;color:var(--sl-color-gray-3);padding:4px 8px;background:var(--sl-color-gray-6);border-radius:4px;';
            const update = () => { span.textContent = `${grid.rows.length} rows`; };
            const unsub = grid.on('data-change', update);
            el.appendChild(span);
            return () => {
              unsub();
              span.remove();
            };
          },
        });
      } else {
        shell?.unregisterHeaderContent('row-count');
      }

      if (v.showCustomPanel) {
        shell?.registerToolPanel({
          id: 'custom-info',
          title: 'Info Panel',
          icon: 'ℹ',
          tooltip: 'Info Panel',
          render: (el) => {
            el.innerHTML = '<div style="padding:16px;"><h4 style="margin:0 0 8px;">Custom Panel</h4><p style="margin:0;font-size:13px;">This panel was added via registerToolPanel().</p></div>';
            return () => { el.innerHTML = ''; };
          },
        });
      } else {
        shell?.unregisterToolPanel('custom-info');
      }

      if (v.showToolbarButton) {
        shell?.registerToolbarContent({
          id: 'refresh-btn',
          render: (el) => {
            const btn = document.createElement('button');
            btn.className = 'tbw-toolbar-btn';
            btn.title = 'Refresh Data';
            btn.setAttribute('aria-label', 'Refresh Data');
            btn.textContent = '↻';
            btn.addEventListener('click', () => { grid.rows = generateRows(20); });
            el.appendChild(btn);
            return () => btn.remove();
          },
        });
      } else {
        shell?.unregisterToolbarContent('refresh-btn');
      }
    }

    rebuild({
      showTitle: true,
      showHeaderContent: true,
      showToolbarButton: true,
      showVisibilityPlugin: true,
      showCustomPanel: false,
      panelPosition: 'right',
      panelMode: 'overlay',
    });

    container.addEventListener('control-change', ((e: CustomEvent) => {
      rebuild(e.detail.allValues as ShellValues);
    }) as EventListener);
  }
```

## Light DOM Configuration

Configure the shell declaratively using `<tbw-grid-header>` and `<tbw-grid-header-content>`
elements. Still `import '@toolbox-web/grid/features/shell'` so the plugin is present:

```html
<tbw-grid>
  <tbw-grid-header title="Employee Directory">
    <tbw-grid-header-content>
      <span id="row-count">0 employees</span>
    </tbw-grid-header-content>
  </tbw-grid-header>
</tbw-grid>
```

```ts
// ShellLightDomDemo.astro
  import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/shell';

  const container = document.getElementById('shell-lightdom-wrap');
  if (container) {
    const grid = queryGrid('tbw-grid', container)!;
    const departments = ['Engineering', 'Sales', 'Marketing', 'Support'];
    const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry'];

    grid.columns = [
      { field: 'id', header: 'ID', type: 'number', width: 80 },
      { field: 'name', header: 'Name', minWidth: 150 },
      { field: 'department', header: 'Department', width: 150 },
      { field: 'salary', header: 'Salary', type: 'number', width: 120 },
      { field: 'active', header: 'Active', type: 'boolean', width: 80 },
    ];

    grid.rows = Array.from({ length: 20 }, (_, i) => ({
      id: i + 1,
      name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1),
      department: departments[i % departments.length],
      salary: 50000 + Math.floor(Math.random() * 50000),
      active: i % 3 !== 0,
    }));
  }
```

## Multiple Tool Panels

Register multiple tool panels — each gets a tab in the sidebar. Panels can be registered via
configuration, the framework wrapper components, or the plugin's runtime `registerToolPanel()`
API. Each panel **requires** a `title`.

#### TypeScript

```ts
import '@toolbox-web/grid/features/shell';

const shell = grid.getPluginByName('shell');
shell?.registerToolPanel({
  id: 'filter-panel',
  title: 'Filters',
  icon: '🔍',
  render(container) {
    container.innerHTML = '<p>Filter controls here</p>';
  },
});
```

#### React

```tsx
import { DataGrid, GridToolPanel } from '@toolbox-web/grid-react';

function MyGrid({ rows }) {
  return (
    <DataGrid rows={rows} gridConfig={config}>
      <GridToolPanel id="filter-panel" title="Filters" icon="🔍">
        {({ grid }) => <p>Filter controls here</p>}
      </GridToolPanel>
    </DataGrid>
  );
}
```

#### Vue

```html
<script setup lang="ts">
import { TbwGrid, TbwGridToolPanel } from '@toolbox-web/grid-vue';
</script>

<template>
  <TbwGrid :rows="rows" :grid-config="config">
    <TbwGridToolPanel id="filter-panel" label="Filters" icon="🔍">
      <template #default="{ grid }">
        <p>Filter controls here</p>
      </template>
    </TbwGridToolPanel>
  </TbwGrid>
</template>
```

#### Angular

```html
<tbw-grid [rows]="rows" [gridConfig]="config">
  <tbw-grid-tool-panel id="filter-panel" title="Filters" icon="🔍">
    <ng-template>
      <p>Filter controls here</p>
    </ng-template>
  </tbw-grid-tool-panel>
</tbw-grid>
```

:::tip[Adapters auto-register the shell (v2.x)]
While **v2.x** is current, the React, Vue, and Angular wrappers register the `ShellPlugin`
automatically when you use a shell wrapper component (`GridToolPanel`, `GridToolButtons`, …) or
pass `features.shell`, so adapter users do not need an explicit shell import. This
auto-registration is a v2.x convenience and is removed at **v3.0.0**, where the shell becomes
opt-in. Each adapter ships a side-effect feature import for the explicit opt-in:
`import '@toolbox-web/grid-react/features/shell'` (and the matching
`grid-vue` / `grid-angular` subpaths). Adding it now is forward-compatible and tree-shakeable.
:::

```ts
// ShellMultiPanelsDemo.astro
  import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/shell';
import '@toolbox-web/grid/features/visibility';

  const grid = queryGrid('#demo-shell-multi-panels');

  if (grid) {
    const departments = ['Engineering', 'Sales', 'Marketing', 'Support'];
    const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry'];

    grid.gridConfig = {
      columns: [
        { field: 'id', header: 'ID', type: 'number', width: 80 },
        { field: 'name', header: 'Name', minWidth: 150 },
        { field: 'department', header: 'Department', width: 150 },
        { field: 'salary', header: 'Salary', type: 'number', width: 120 },
        { field: 'active', header: 'Active', type: 'boolean', width: 80 },
      ],
      features: {
        shell: { header: { title: 'Multi-Panel Demo' } },
        visibility: true,
      },
    };

    grid.rows = Array.from({ length: 20 }, (_, i) => ({
      id: i + 1,
      name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1),
      department: departments[i % departments.length],
      salary: 50000 + Math.floor(Math.random() * 50000),
      active: i % 3 !== 0,
    }));

    const shell = grid.getPluginByName('shell');

    // Custom filter panel
    shell?.registerToolPanel({
      id: 'filter',
      title: 'Filter',
      icon: '🔍',
      tooltip: 'Filter data',
      order: 20,
      render: (el) => {
        el.innerHTML = `
          <div style="padding:0.75rem;">
            <div style="margin-bottom:16px;">
              <label style="display:block;margin-bottom:4px;font-size:12px;color:var(--sl-color-gray-3);">Name contains</label>
              <input type="text" placeholder="Search..." style="width:100%;padding:6px 8px;border:1px solid var(--sl-color-gray-5);border-radius:4px;box-sizing:border-box;background:var(--sl-color-gray-6);color:var(--sl-color-text);" />
            </div>
            <div style="margin-bottom:16px;">
              <label style="display:block;margin-bottom:4px;font-size:12px;color:var(--sl-color-gray-3);">Department</label>
              <select style="width:100%;padding:6px 8px;border:1px solid var(--sl-color-gray-5);border-radius:4px;box-sizing:border-box;background:var(--sl-color-gray-6);color:var(--sl-color-text);">
                <option value="">All</option>
                <option value="Engineering">Engineering</option>
                <option value="Sales">Sales</option>
                <option value="Marketing">Marketing</option>
                <option value="Support">Support</option>
              </select>
            </div>
          </div>
        `;
        return () => { el.innerHTML = ''; };
      },
    });

    // Custom settings panel
    shell?.registerToolPanel({
      id: 'settings',
      title: 'Settings',
      icon: '⚙',
      tooltip: 'Grid settings',
      order: 50,
      render: (el) => {
        el.innerHTML = `
          <div style="padding:0.75rem;">
            <label style="display:flex;align-items:center;gap:8px;margin-bottom:12px;"><input type="checkbox" checked /><span>Row hover effect</span></label>
            <label style="display:flex;align-items:center;gap:8px;margin-bottom:12px;"><input type="checkbox" checked /><span>Alternating row colors</span></label>
            <label style="display:flex;align-items:center;gap:8px;"><input type="checkbox" /><span>Compact mode</span></label>
          </div>
        `;
        return () => { el.innerHTML = ''; };
      },
    });
  }
```

## Toolbar Buttons

Add custom buttons to the shell toolbar using framework wrapper components or light-DOM
`<tbw-grid-tool-buttons>`:

#### TypeScript

```html
<tbw-grid>
  <tbw-grid-tool-buttons>
    <button onclick="exportData()">📥 Export</button>
    <button onclick="printGrid()">🖨️ Print</button>
  </tbw-grid-tool-buttons>
</tbw-grid>
```

#### React

```tsx
import { DataGrid, GridToolButtons } from '@toolbox-web/grid-react';

function MyGrid({ rows }) {
  return (
    <DataGrid rows={rows} gridConfig={config}>
      <GridToolButtons>
        <button onClick={exportData}>📥 Export</button>
        <button onClick={printGrid}>🖨️ Print</button>
      </GridToolButtons>
    </DataGrid>
  );
}
```

#### Vue

```html
<script setup lang="ts">
import { TbwGrid, TbwGridToolButtons } from '@toolbox-web/grid-vue';
</script>

<template>
  <TbwGrid :rows="rows" :grid-config="config">
    <TbwGridToolButtons>
      <button @click="exportData">📥 Export</button>
      <button @click="printGrid">🖨️ Print</button>
    </TbwGridToolButtons>
  </TbwGrid>
</template>
```

#### Angular

```html
<tbw-grid [rows]="rows" [gridConfig]="config">
  <tbw-grid-tool-buttons>
    <button (click)="exportData()">📥 Export</button>
    <button (click)="printGrid()">🖨️ Print</button>
  </tbw-grid-tool-buttons>
</tbw-grid>
```

```ts
// ShellToolbarButtonsDemo.astro
  import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/shell';

  const wrap = document.getElementById('shell-toolbar-wrap');
  const grid = queryGrid('tbw-grid', wrap!);

  if (grid && wrap) {
    const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry'];
    const depts = ['Engineering', 'Sales', 'Marketing', 'Support'];

    grid.columns = [
      { field: 'id', header: 'ID', type: 'number', width: 80 },
      { field: 'name', header: 'Name', minWidth: 150 },
      { field: 'department', header: 'Department', width: 150 },
      { field: 'salary', header: 'Salary', type: 'number', width: 120 },
    ];

    grid.rows = Array.from({ length: 15 }, (_, i) => ({
      id: i + 1,
      name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1),
      department: depts[i % depts.length],
      salary: 50000 + Math.floor(Math.random() * 50000),
    }));

    const exportBtn = wrap.querySelector('[title="Export"]');
    exportBtn?.addEventListener('click', () => alert('Export clicked!'));

    const printBtn = wrap.querySelector('[title="Print"]');
    printBtn?.addEventListener('click', () => alert('Print clicked!'));
  }
```

## Shell Configuration Reference

All options live under `features.shell` and are typed by `ShellConfig`
(from `@toolbox-web/grid/plugins/shell`):

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `header.title` | `string` | — | Title text in the header bar |
| `header.visible` | `boolean` | `true` | Render the header bar. Set `false` to suppress the entire `.tbw-shell-header` and own all chrome yourself |
| `header.toolPanelToggle` | `boolean` | `true` | Render the built-in panel toggle button + separator |
| `toolPanel.position` | `'left' \| 'right'` | `'right'` | Sidebar position |
| `toolPanel.width` | `number` | `280` | Sidebar width in pixels |
| `toolPanel.defaultOpen` | `string` | — | Accordion section to auto-expand on first open. **Deprecated:** in v2.x this also opens the sidebar; in v3.0.0 it will only pre-select the section (see [#259](https://github.com/OysteinAmundsen/toolbox/issues/259)). Combine with `initialState: 'open'` for forward-compatible behavior. |
| `toolPanel.initialState` | `'open' \| 'closed'` | `'closed'` | Whether the sidebar starts open on grid load. Overrides the legacy `defaultOpen` open-on-load behavior when set. |
| `toolPanel.locked` | `boolean` | `false` | Lock the sidebar open. Implies `initialState: 'open'`, makes `closeToolPanel()` a no-op, and hides the built-in toggle button. |
| `toolPanel.persistState` | `boolean` | `false` | Remember open/closed state |
| `toolPanel.closeOnClickOutside` | `boolean` | `false` | Close on outside click (ignored in `mode: 'push'` or when `locked: true`; `'dropdown'` always light-dismisses) |
| `toolPanel.mode` | `'overlay' \| 'push' \| 'dropdown'` | `'overlay'` | `'overlay'` floats over the grid; `'push'` reflows the grid sideways so all cells stay visible; `'dropdown'` renders the whole sidebar as an anchored popover |

## Tool-Panel Layout Modes

The tool panel supports three layout modes — pick whichever fits the viewport you're targeting.

- **`'overlay'`** (default): the panel is positioned **over** the grid content. Opening it does not change the grid's available width. Best for narrow viewports where shrinking the grid would leave too little room for the data.
- **`'push'`**: the panel is laid out as a **sibling** of the grid content in a flex row. Opening it shrinks the grid's available width; the grid's `ResizeObserver` re-runs column virtualization, so cells that an overlay panel would hide remain reachable. Best for desktop layouts.
- **`'dropdown'`**: the entire sidebar (the full accordion) renders as an **anchored popover** built on the native [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API). It floats above all other content in the top layer, is dismissed on `Escape` or click-outside, and is positioned via [CSS anchor positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) where supported (with a JS bounding-rect fallback). The anchor is resolved by priority: an explicit `anchor` passed to `openToolPanel()` → the built-in toggle button → the grid corner. Best for compact toolbars and bring-your-own trigger buttons (e.g. a "columns" button in a custom column header).

```ts
import '@toolbox-web/grid/features/shell';

grid.gridConfig = {
  features: {
    shell: {
      toolPanel: {
        position: 'right',
        mode: 'push', // grid reflows when the panel opens/closes
      },
    },
  },
};
```

In `'push'` mode the user-facing resize handle is hidden (the panel's width is part of the layout, so drag-to-resize would fight the flex container) and `closeOnClickOutside` is treated as a no-op (there is no overlap to dismiss against). Drive the width via the `--tbw-tool-panel-width` CSS variable instead.

### Dropdown mode + a custom column-header trigger

`'dropdown'` mode shines when you want the column chooser to hang off your **own** button rather than the built-in toolbar toggle. Pass that button as the `anchor` when you open the panel, and the popover positions itself directly below it:

```ts
const shell = grid.getPluginByName('shell');
// Mark your trigger so the popover can re-find it after a structural re-render.
columnsButton.setAttribute('data-panel-toggle', '');
columnsButton.addEventListener('click', () => {
  shell?.openToolPanel('columns', { anchor: columnsButton });
});
```

:::caution[Mark a custom trigger with `data-panel-toggle`]
A custom trigger button **must** carry the `data-panel-toggle` attribute. Toggling a column's visibility or reordering columns from inside the panel triggers a structural re-render that **rebuilds the trigger node** — so the element reference you passed as `anchor` is detached and can no longer be re-anchored. The shell re-resolves the anchor after every re-render via the `[data-panel-toggle]` selector; without it, an open dropdown falls back to the grid corner instead of staying under your button.
:::

The demo below renders a utility-type column whose **header** hosts a custom `▥` button. Clicking it opens the full tool panel as a dropdown anchored to that button — no toolbar required.

```ts
// ShellDropdownDemo.astro
  import '@toolbox-web/grid';
import type { ColumnConfig } from '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/shell';
import '@toolbox-web/grid/features/visibility';

  const grid = queryGrid('#demo-shell-dropdown');

  if (grid) {
    const departments = ['Engineering', 'Sales', 'Marketing', 'Support'];
    const names = ['Alice', 'Bob', 'Carol', 'Dan', 'Eve', 'Frank', 'Grace', 'Henry'];

    // A utility column: not bound to data — its header hosts the dropdown trigger.
    const columnsTrigger: ColumnConfig = {
      field: '__columns__',
      utility: true,
      header: '',
      width: 48,
      sortable: false,
      resizable: false,
      headerRenderer: (ctx) => {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'tbw-columns-trigger';
        btn.textContent = '▥';
        btn.title = 'Choose columns';
        btn.setAttribute('aria-label', 'Choose columns');
        btn.setAttribute('aria-haspopup', 'dialog');
        // INVARIANT: a custom dropdown trigger MUST carry `data-panel-toggle`.
        // Toggling a column from inside the panel rebuilds this header (and so
        // recreates this very button), detaching the element the popover was
        // anchored to. The shell re-resolves the anchor via the
        // `[data-panel-toggle]` selector, so without this attribute the dropdown
        // would re-anchor to the grid corner instead of back to this button.
        btn.setAttribute('data-panel-toggle', '');
        btn.addEventListener('click', (e) => {
          e.stopPropagation();
          const shell = grid.getPluginByName('shell');
          if (shell?.isToolPanelOpen) {
            shell.closeToolPanel();
          } else {
            // Anchor the dropdown popover directly to this header button.
            shell?.openToolPanel('columns', { anchor: btn });
          }
        });
        ctx.cellEl.style.justifyContent = 'center';
        return btn;
      },
    };

    grid.gridConfig = {
      columns: [
        { field: 'id', header: 'ID', type: 'number', width: 70 },
        { field: 'name', header: 'Name', minWidth: 140, lockVisible: true },
        { field: 'department', header: 'Department', width: 150 },
        { field: 'salary', header: 'Salary', type: 'number', width: 110 },
        { field: 'active', header: 'Active', type: 'boolean', width: 80 },
        columnsTrigger,
      ],
      features: {
        // No built-in toolbar toggle — the dropdown is driven entirely by the
        // custom column-header button.
        shell: { header: { title: 'Employees', toolPanelToggle: false }, toolPanel: { mode: 'dropdown' } },
        visibility: true,
      },
    };

    grid.rows = Array.from({ length: 20 }, (_, i) => ({
      id: i + 1,
      name: names[i % names.length] + ' ' + (Math.floor(i / names.length) + 1),
      department: departments[i % departments.length],
      salary: 50000 + Math.floor(Math.random() * 50000),
      active: i % 3 !== 0,
    }));
  }
```

In `'dropdown'` mode the resize handle is hidden, the popover is capped to the viewport (`max-height: min(70vh, 480px)`) with internal scrolling for tall accordions, and dismissal (Escape / click-outside) is always active regardless of `closeOnClickOutside`.

## Styling the Shell

`<tbw-grid>` renders into the **light DOM**, so the shell's internals are reachable from any outer stylesheet — no `::part()` needed. Theme-level overrides applied to a top-level CSS file affect every grid in the application uniformly.

**Stable selectors:**

| Selector | Element |
|----------|---------|
| `tbw-grid .tbw-shell-header` | Header bar (title + content + toolbar) |
| `tbw-grid .tbw-shell-title` | Title element |
| `tbw-grid .tbw-shell-toolbar` | Toolbar (flex container; custom content + toggle) |
| `tbw-grid .tbw-toolbar-btn` | Buttons inside the toolbar (incl. panel toggle) |
| `tbw-grid [data-panel-toggle]` | The single panel toggle button |
| `tbw-grid .tbw-toolbar-separator` | Separator between custom content and the toggle |
| `tbw-grid .tbw-tool-panel` | Tool-panel sidebar |
| `tbw-grid .tbw-accordion-header` | Tool-panel section header (one per registered panel) |

**Recipe — move the panel toggle to the start of the toolbar (left in LTR):**

The toolbar is a flex row, so reordering is purely a CSS concern — use `order` on both the toggle and its separator so they stay grouped together:

```css
/* App-level theme — affects every <tbw-grid> uniformly */
tbw-grid [data-panel-toggle],
tbw-grid .tbw-toolbar-separator {
  order: -1;
}
```

No grid configuration is required; this is the recommended way to standardize toolbar layout across an application's grids.

## Bring Your Own Tool-Panel Toggle

If you need to replace the built-in toggle button entirely — for example, to use a design-system button instead of styling `.tbw-toolbar-btn` — set `header.toolPanelToggle: false`. The grid will not render the built-in button **or** the auto-inserted separator next to it; tool panels themselves remain fully functional and can be toggled from any element via the plugin's `toggleToolPanel()`, opened directly on a specific section via `openToolPanel(panelId)`, or driven section-by-section with `toggleToolPanelSection(id)`.

```ts
import '@toolbox-web/grid/features/shell';

const shell = grid.getPluginByName('shell');

const config = {
  features: {
    shell: {
      header: {
        title: 'Employees',
        toolPanelToggle: false, // suppress built-in <button data-panel-toggle> + separator
        toolbarContents: [
          {
            id: 'my-buttons',
            render: (container) => {
              const filters = document.createElement('my-button');
              filters.textContent = 'Filters';
              filters.addEventListener('click', () => shell?.openToolPanel('filters'));

              const settings = document.createElement('my-button');
              settings.textContent = 'Settings';
              settings.addEventListener('click', () => shell?.openToolPanel('settings'));

              container.append(filters, settings);
            },
          },
        ],
      },
    },
  },
};
```

Passing a `panelId` to `openToolPanel()` jumps straight to that accordion section (one click, no double-tap). It takes precedence over `toolPanel.defaultOpen`; if the ID isn't registered, the call falls back to default behavior and emits a `TBW072` warning.

## Hide the Shell Header Bar Entirely

`toolPanelToggle: false` removes the built-in toggle button but keeps the header bar (with its title and any toolbar contents). To suppress the **entire** `.tbw-shell-header` bar — for example when you want to open tool panels from a custom column header icon and own all chrome yourself — set `header.visible: false`.

```ts
import '@toolbox-web/grid/features/shell';

const shell = grid.getPluginByName('shell');

const config = {
  columns: [
    // …
    {
      field: '__actions',
      width: 56,
      utility: true, // excluded from print, export, reorder, visibility, etc.
      resizable: false,
      sortable: false,
      headerRenderer: () => {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.textContent = '⚙';
        btn.setAttribute('aria-label', 'Open tool panel');
        btn.addEventListener('click', () => shell?.toggleToolPanel());
        return btn;
      },
    },
  ],
  features: {
    shell: {
      header: { visible: false },
      toolPanel: {
        closeOnClickOutside: true, // optional — see below
      },
    },
  },
};
```

When `header.visible: false`, tool panels remain fully functional and are opened with the plugin's `toggleToolPanel()` / `openToolPanel(panelId)` from any element you like. Because there is no header toggle to close the panel, the grid provides three additional dismissal affordances:

- **Close button** — a small `✕` button (exposed as the `tool-panel-close` CSS part) is rendered to dismiss the panel. With a single panel it sits inline on the first accordion header row; with multiple panels it gets its own row at the top of the panel (exposed as the `tool-panel-header` CSS part). It is omitted when `toolPanel.locked` is `true` (a locked panel cannot be closed) and in `dropdown` mode, where the popover already light-dismisses on <kbd>Esc</kbd> / click-outside.
- **Escape key** — pressing <kbd>Esc</kbd> closes an open **overlay** panel. It yields to more specific handlers (e.g. an active cell editor that has already handled the key), and is a no-op for `push`-mode panels, which are persistent sidebars.
- **Click-outside** — set `toolPanel.closeOnClickOutside: true` to dismiss an open overlay panel when clicking anywhere in the window outside the panel. This is opt-in and, like Esc, is a no-op in `push` mode.

`header.visible: false` keeps the shell active (the panel overlay still mounts); it only suppresses the header bar element. This is the supported alternative to hiding `.tbw-shell-header` with CSS.

## Migrating to the Feature API

The shell used to be part of grid **core**: auto-registered with no import, driven by
`grid.register*` / `openToolPanel` element delegates, with its config types exported from the
package root. Those surfaces are **deprecated** and will be **removed in v3.0.0**. The `shell`
configuration object itself is augmented onto `gridConfig` by the plugin and is **not** deprecated.
Migrate as follows:

| Deprecated (v2.x, removed at v3) | Use instead |
|----------------------------------|-------------|
| _Implicit_ shell (no import) | `import '@toolbox-web/grid/features/shell'` |
| `grid.registerToolPanel(panel)` | `grid.getPluginByName('shell')?.registerToolPanel(panel)` |
| `grid.unregisterToolPanel(id)` | `grid.getPluginByName('shell')?.unregisterToolPanel(id)` |
| `grid.registerHeaderContent(c)` | `grid.getPluginByName('shell')?.registerHeaderContent(c)` |
| `grid.registerToolbarContent(c)` | `grid.getPluginByName('shell')?.registerToolbarContent(c)` |
| `grid.openToolPanel(id)` / `closeToolPanel()` / `toggleToolPanel()` | `grid.getPluginByName('shell')?.openToolPanel(id)` (same method names) |
| Shell types from `@toolbox-web/grid` | Shell types from `@toolbox-web/grid/plugins/shell` |

Notes:

- In **v2.x** the shell is still auto-registered, so unmigrated code keeps working — the
  imperative `grid.*` shell delegates emit a one-time **TBW076** deprecation warning. The
  top-level `config.shell` field is a plugin augmentation (**not** deprecated) and
  `refreshShellHeader()` is merged/handled silently.
- For advanced/explicit control you can register the plugin directly:
  `plugins: [new ShellPlugin(shellConfig)]` (from `@toolbox-web/grid/plugins/shell`).
- **Adapters auto-register** the `ShellPlugin` in **v2.x**, so React/Vue/Angular users need no
  change; this auto-registration is removed at **v3.0.0** (opt-in). The forward-compatible opt-in
  is the adapter-specific side-effect import
  `import '@toolbox-web/grid-{react,vue,angular}/features/shell'`.
- From **v3.0.0** the shell is opt-in: shell usage without the feature/plugin throws a clear
  error with a migration link.

## How the Shell Wraps the Grid

The shell is unusual among plugins: most plugins **enrich** the grid (tagging rows, decorating
cells), but the shell **wraps** it — it consumes the freshly built grid DOM and renders its own
chrome (header bar + tool-panel sidebar) _around_ it. It does this from the
`afterStructuralRender()` lifecycle hook, relocating the existing `.tbw-grid-content` node into
the shell wrapper (preserving the subtree, its listeners, and the grid's cached refs) rather than
re-creating it.

If you want to build **another** grid-wrapping plugin, this pattern is documented as a reusable
recipe in [Plugin Architecture → Wrapping plugins](/grid/plugin-development/architecture.md#wrapping-plugins-host-dom-chrome).

## See Also
- **[Filtering](/grid/plugins/filtering.md)** — Registers a filter tool panel into the shell
- **[Pivot](/grid/plugins/pivot.md)** — Registers a pivot configuration tool panel
- **[Core Features](/grid/core.md)** — Columns, renderers, formatters, events, and methods
- **[Plugins Overview](/grid/plugins.md)** — Plugin compatibility and combinations
