# Demos

> Full-featured demo applications showcasing @toolbox-web/grid with 15+ plugins, custom editors, master-detail, and more.

## Reference Implementation — Employee Management (advanced, end-to-end)

This is the canonical advanced grid in this project: ~15 columns mixing text,
number, date, select and boolean; custom renderers and editors; master-detail;
multi-sort; per-column filtering; row grouping; CSV/Excel export; clipboard;
context menu; undo/redo; pinned rows; and side tool panels. In the cross-framework
`llms-full.txt` (and this page's `.md`) the source below is the
**vanilla, framework-agnostic implementation** — the `GridConfig` object is
portable verbatim to the React, Angular and Vue adapters (per RULE 0 in the
[Introduction](/grid/introduction.md): one `gridConfig` over fragmented props). In a
per-framework `llms-full-{react,vue,angular}.txt` corpus the same reference is
shown as that framework's **native implementation** (adapter components plus
JSX / SFC / component renderers and editors), drawn from
`demos/{angular,react,vue}/src/demos/employee-management/`. Only the row type and
seed data (`demos/shared/employee-management/`) are shared across all of them.

Read the files in this order: the domain model, then the grid config (columns +
`features` + manual `plugins`), then the custom renderers, editors and tool
panels, then the factory or component that wires them together.

```ts
// demos/shared/employee-management/types.ts
/**
 * Shared Data Models for Employee Management Demo
 *
 * These type definitions are shared across all framework implementations
 * (Vanilla, React, Angular, Vue) of the Employee Management demo.
 */

// Re-export grid element type for easier imports in demos
export type { TbwGrid as GridElement } from '@toolbox-web/grid';

/**
 * Represents a project that an employee is working on or has completed.
 */
export interface Project {
  id: string;
  name: string;
  role: string;
  hoursLogged: number;
  status: 'active' | 'completed' | 'on-hold';
}

/**
 * Represents a quarterly performance review for an employee.
 */
export interface PerformanceReview {
  year: number;
  quarter: string;
  score: number;
  notes: string;
}

/**
 * Represents an employee record in the HR management system.
 */
export interface Employee {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  department: string;
  team: string;
  title: string;
  level: 'Junior' | 'Mid' | 'Senior' | 'Lead' | 'Principal' | 'Director';
  salary: number;
  bonus: number;
  status: 'Active' | 'On Leave' | 'Remote' | 'Contract' | 'Terminated';
  hireDate: string;
  lastPromotion: string | null;
  manager: string | null;
  location: string;
  timezone: string;
  skills: string[];
  rating: number;
  completedProjects: number;
  activeProjects: Project[];
  performanceReviews: PerformanceReview[];
  isTopPerformer: boolean;
}

/**
 * Configuration options for the demo (used by story controls).
 */
export interface DemoConfig {
  rowCount: number;
  enableSelection: boolean;
  enableFiltering: boolean;
  enableSorting: boolean;
  enableEditing: boolean;
  enableMasterDetail: boolean;
  enableRowGrouping: boolean;
}
```

```ts
// demos/vanilla/src/demos/employee-management/grid-config.ts
/**
 * Grid Configuration for Employee Management Demo
 *
 * This file demonstrates the `features` configuration API for @toolbox-web/grid.
 * Most plugins are configured declaratively via `features: { ... }` on the grid config.
 * The PinnedRowsPlugin is configured manually via `plugins: [...]` to show both approaches.
 */

// Feature side-effect imports — register feature factories in the core registry.
// Each import is tiny (~200-300 bytes) and only includes the factory + type augmentation.
import '@toolbox-web/grid/features/clipboard';
import '@toolbox-web/grid/features/column-virtualization';
import '@toolbox-web/grid/features/context-menu';
import '@toolbox-web/grid/features/editing';
import '@toolbox-web/grid/features/export';
import '@toolbox-web/grid/features/filtering';
import '@toolbox-web/grid/features/grouping-columns';
import '@toolbox-web/grid/features/grouping-rows';
import '@toolbox-web/grid/features/master-detail';
import '@toolbox-web/grid/features/multi-sort';
import '@toolbox-web/grid/features/pinned-columns';
import '@toolbox-web/grid/features/pinned-rows';
import '@toolbox-web/grid/features/reorder-columns';
import '@toolbox-web/grid/features/responsive';
import '@toolbox-web/grid/features/selection';
import '@toolbox-web/grid/features/shell';
import '@toolbox-web/grid/features/undo-redo';
import '@toolbox-web/grid/features/visibility';

// PinnedRowsPlugin is imported directly to demonstrate the manual plugins approach.
// Built-in panel renderers are imported alongside it for the new slots[] API.
import type { GridConfig } from '@toolbox-web/grid';
import { filteredCountPanel, PinnedRowsPlugin, rowCountPanel } from '@toolbox-web/grid/plugins/pinned-rows';

import { DEPARTMENTS, type Employee } from '@demo/shared/employee-management';

import { bonusSliderEditor, dateEditor, starRatingEditor, statusSelectEditor } from './editors';
import {
  createDetailRenderer,
  createResponsiveCardRenderer,
  ratingRenderer,
  statusViewRenderer,
  topPerformerRenderer,
} from './renderers';

// =============================================================================
// COLUMN GROUPS
// =============================================================================

/**
 * Column groups for the employee grid.
 * Used by GroupingColumnsPlugin to create grouped headers.
 * Also used by column-move handler to enforce group constraints.
 */
export const COLUMN_GROUPS = [
  { id: 'employee', header: 'Employee Info', children: ['firstName', 'lastName', 'email'] },
  { id: 'organization', header: 'Organization', children: ['department', 'team', 'title', 'level'] },
  { id: 'compensation', header: 'Compensation', children: ['salary', 'bonus'] },
  {
    id: 'status',
    header: 'Status & Performance',
    children: ['status', 'hireDate', 'rating', 'isTopPerformer', 'location'],
  },
];

// =============================================================================
// CONFIGURATION OPTIONS
// =============================================================================

/**
 * Options for configuring the grid.
 * Toggle features on/off based on demo requirements.
 */
export interface GridConfigOptions {
  enableSelection: boolean;
  enableFiltering: boolean;
  enableSorting: boolean;
  enableEditing: boolean;
  enableMasterDetail: boolean;
  enableRowGrouping?: boolean;
}

// =============================================================================
// GRID CONFIGURATION FACTORY
// =============================================================================

/**
 * Creates a complete grid configuration for the employee management demo.
 *
 * This configuration includes:
 * - 15 columns with various types (text, number, date, select, boolean)
 * - Custom editors (star rating, bonus slider, status select, date picker)
 * - Custom renderers (status badges, rating colors, top performer badge)
 * - Shell header with title
 * - Multiple plugins for advanced features
 *
 * @example
 * ```ts
 * const config = createGridConfig({
 *   enableSelection: true,
 *   enableFiltering: true,
 *   enableSorting: true,
 *   enableEditing: true,
 *   enableMasterDetail: true,
 * });
 *
 * grid.gridConfig = config;
 * ```
 */
export function createGridConfig(options: GridConfigOptions): GridConfig<Employee> {
  const {
    enableSelection,
    enableFiltering,
    enableSorting,
    enableEditing,
    enableMasterDetail,
    enableRowGrouping = false,
  } = options;

  return {
    fitMode: 'fixed',

    // Column groups for grouped headers
    columnGroups: COLUMN_GROUPS,

    // Column definitions
    columns: [
      { field: 'id', header: 'ID', type: 'number', width: 70, sortable: true },
      {
        field: 'firstName',
        header: 'First Name',
        minWidth: 100,
        editable: enableEditing,
        sortable: true,
        resizable: true,
      },
      {
        field: 'lastName',
        header: 'Last Name',
        minWidth: 100,
        editable: enableEditing,
        sortable: true,
        resizable: true,
      },
      { field: 'email', header: 'Email', minWidth: 200, resizable: true },
      {
        field: 'department',
        header: 'Dept',
        width: 120,
        sortable: true,
        editable: enableEditing,
        type: 'select',
        options: DEPARTMENTS.map((d) => ({ label: d, value: d })),
      },
      { field: 'team', header: 'Team', width: 110, sortable: true },
      { field: 'title', header: 'Title', minWidth: 160, editable: enableEditing, resizable: true },
      {
        field: 'level',
        header: 'Level',
        width: 90,
        sortable: true,
        editable: enableEditing,
        type: 'select',
        options: ['Junior', 'Mid', 'Senior', 'Lead', 'Principal', 'Director'].map((l) => ({ label: l, value: l })),
      },
      {
        field: 'salary',
        header: 'Salary',
        type: 'number',
        width: 110,
        editable: enableEditing,
        sortable: true,
        resizable: true,
        format: (v: number) =>
          v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }),
      },
      {
        field: 'bonus',
        header: 'Bonus',
        type: 'number',
        width: 180,
        sortable: true,
        editable: enableEditing,
        editor: bonusSliderEditor,
        format: (v: number) =>
          v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }),
      },
      {
        field: 'status',
        header: 'Status',
        width: 140,
        sortable: true,
        editable: enableEditing,
        editor: statusSelectEditor,
        renderer: statusViewRenderer,
      },
      {
        field: 'hireDate',
        header: 'Hire Date',
        type: 'date',
        width: 130,
        sortable: true,
        editable: enableEditing,
        editor: dateEditor,
      },
      {
        field: 'rating',
        header: 'Rating',
        type: 'number',
        width: 120,
        sortable: true,
        editable: enableEditing,
        editor: starRatingEditor,
        renderer: ratingRenderer,
      },
      {
        field: 'isTopPerformer',
        header: '⭐',
        type: 'boolean',
        width: 50,
        sortable: false,
        renderer: topPerformerRenderer,
      },
      { field: 'location', header: 'Location', width: 110, sortable: true },
    ],

    // Grid-wide feature toggles (used by plugins that support enable/disable)
    sortable: enableSorting,
    filterable: enableFiltering,
    selectable: enableSelection,

    // Declarative feature configuration — the recommended approach.
    // Each key corresponds to a feature side-effect import above.
    // The grid creates plugin instances from these configs automatically.
    features: {
      // Shell (header, tool panels) — best-practice feature opt-in.
      shell: {
        header: {
          title: 'Employee Management System (JS)',
        },
        toolPanel: { position: 'right' as const, width: 300 },
      },
      selection: 'range',
      multiSort: true,
      filtering: { debounceMs: 200 },
      // EditingPlugin always loaded; toggle via editOn to avoid validation errors
      // when columns have `editable: true`
      editing: enableEditing ? 'dblclick' : { editOn: false },
      clipboard: true,
      contextMenu: true,
      reorderColumns: true,
      groupingColumns: true,
      pinnedColumns: true,
      columnVirtualization: true,
      visibility: true,
      // Responsive plugin for mobile/narrow layouts
      responsive: {
        breakpoint: 700,
        cardRenderer: (row: Employee) => createResponsiveCardRenderer(row),
        cardRowHeight: 80,
        hiddenColumns: ['id', 'email', 'team', 'level', 'bonus', 'hireDate', 'isTopPerformer', 'location'],
      },
      // Row grouping (works alongside master-detail)
      ...(enableRowGrouping
        ? {
            groupingRows: {
              groupOn: (row: unknown) => (row as Employee).department,
              defaultExpanded: false,
              showRowCount: true,
              aggregators: {
                salary: 'sum',
                rating: (rows: Record<string, unknown>[], field: string) => {
                  const sum = rows.reduce((acc, row) => acc + (Number(row[field]) || 0), 0);
                  return rows.length ? (sum / rows.length).toFixed(1) : '';
                },
              },
            },
          }
        : {}),
      // Master-detail (works alongside row grouping — details appear on data rows within groups)
      ...(enableMasterDetail
        ? {
            masterDetail: {
              detailRenderer: (row: unknown) => createDetailRenderer(row as Employee),
              showExpandColumn: true,
              animation: 'slide' as const,
            },
          }
        : {}),
      undoRedo: { maxHistorySize: 100 },
      export: true,
    },

    // Manual plugins array — PinnedRowsPlugin kept here to demonstrate
    // that `features` and `plugins` can be mixed in the same config.
    plugins: [
      new PinnedRowsPlugin({
        slots: [
          {
            id: 'totals',
            position: 'bottom',
            label: 'Summary:',
            cells: {
              salary: (rows: unknown[]) =>
                (rows as Employee[])
                  .reduce((acc, r) => acc + (r.salary || 0), 0)
                  .toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }),
              bonus: (rows: unknown[]) =>
                (rows as Employee[])
                  .reduce((acc, r) => acc + (r.bonus || 0), 0)
                  .toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }),
              rating: (rows: unknown[]) => {
                const vals = (rows as Employee[]).map((r) => r.rating).filter(Boolean);
                return vals.length ? `Avg: ${(vals.reduce((a, b) => a + b, 0) / vals.length).toFixed(2)}` : '';
              },
            },
          },
          { id: 'count', position: 'bottom', render: rowCountPanel() },
          { id: 'filtered', position: 'bottom', render: filteredCountPanel() },
        ],
      }),
    ],
  };
}
```

```ts
// demos/vanilla/src/demos/employee-management/renderers.ts
/**
 * View Renderers for the Vanilla Employee Management Demo
 *
 * View renderers customize how cell values are displayed in read mode.
 */

import type { Employee } from '@demo/shared/employee-management';

/**
 * Renders a status badge with color-coded styling.
 */
export const statusViewRenderer = ({ value }: { value: string }): string => {
  const statusClass = value.toLowerCase().replace(/\s+/g, '-');
  return `<span class="status-badge status-badge--${statusClass}">${value}</span>`;
};

/**
 * Renders a star indicator for top performer status.
 */
export const topPerformerRenderer = ({ value }: { value: boolean }): string => {
  return value
    ? '<span class="top-performer-star top-performer-star--active">★</span>'
    : '<span class="top-performer-star top-performer-star--inactive">☆</span>';
};

/**
 * Renders a rating value with color coding based on score.
 */
export const ratingRenderer = ({ value }: { value: number }): string => {
  const level = value >= 4.5 ? 'high' : value >= 3.5 ? 'medium' : 'low';
  return `<span class="rating-display rating-display--${level}">${value.toFixed(1)} ★</span>`;
};

/**
 * Creates a detail panel element for master-detail view.
 * Shows employee's active projects, performance reviews, and skills.
 */
export const createDetailRenderer = (employee: Employee): HTMLElement => {
  const container = document.createElement('div');
  container.className = 'detail-panel';

  const projectsHtml = employee.activeProjects
    .map(
      (p) =>
        `<tr class="detail-table__row">` +
        `<td class="detail-table__cell">${p.id}</td>` +
        `<td class="detail-table__cell">${p.name}</td>` +
        `<td class="detail-table__cell">${p.role}</td>` +
        `<td class="detail-table__cell">${p.hoursLogged}h</td>` +
        `<td class="detail-table__cell">` +
        `<span class="project-status project-status--${p.status}">${p.status}</span>` +
        `</td></tr>`,
    )
    .join('');

  const reviewsHtml = employee.performanceReviews
    .slice(-4)
    .map((r) => {
      const scoreLevel = r.score >= 4 ? 'high' : r.score >= 3 ? 'medium' : 'low';
      return (
        `<div class="review-card">` +
        `<div class="review-card__period">${r.quarter} ${r.year}</div>` +
        `<div class="review-card__score review-card__score--${scoreLevel}">${r.score.toFixed(1)}</div>` +
        `<div class="review-card__notes">${r.notes}</div>` +
        `</div>`
      );
    })
    .join('');

  const skillsHtml = employee.skills.map((s) => `<span class="skill-tag">${s}</span>`).join('');

  container.innerHTML =
    `<div class="detail-grid">` +
    `<div class="detail-section">` +
    `<h4 class="detail-section__title">Active Projects</h4>` +
    `<table class="detail-table">` +
    `<thead><tr class="detail-table__header">` +
    `<th class="detail-table__header-cell">ID</th>` +
    `<th class="detail-table__header-cell">Project</th>` +
    `<th class="detail-table__header-cell">Role</th>` +
    `<th class="detail-table__header-cell">Hours</th>` +
    `<th class="detail-table__header-cell">Status</th>` +
    `</tr></thead>` +
    `<tbody>${projectsHtml}</tbody>` +
    `</table>` +
    `</div>` +
    `<div class="detail-section">` +
    `<h4 class="detail-section__title">Performance Reviews</h4>` +
    `<div class="reviews-grid">${reviewsHtml}</div>` +
    `<div class="skills-container">` +
    `<h4 class="detail-section__title">Skills</h4>` +
    `${skillsHtml}` +
    `</div>` +
    `</div>` +
    `</div>`;

  return container;
};

/**
 * Creates a responsive card element for mobile/narrow layouts.
 * Shows compact employee info with avatar placeholder, status badge, and key metrics.
 */
export const createResponsiveCardRenderer = (employee: Employee): HTMLElement => {
  const card = document.createElement('div');
  card.className = 'responsive-employee-card';

  // Get initials for avatar placeholder
  const initials = `${employee.firstName.charAt(0)}${employee.lastName.charAt(0)}`;

  // Determine department color
  const deptColors: Record<string, string> = {
    Engineering: '#3b82f6',
    Marketing: '#ec4899',
    Sales: '#f59e0b',
    HR: '#10b981',
    Finance: '#6366f1',
    Legal: '#8b5cf6',
    Operations: '#14b8a6',
    'Customer Support': '#f97316',
  };
  const deptColor = deptColors[employee.department] ?? '#6b7280';

  // Format salary
  const salary = employee.salary.toLocaleString('en-US', {
    style: 'currency',
    currency: 'USD',
    maximumFractionDigits: 0,
  });

  // Status class
  const statusClass = employee.status.toLowerCase().replace(/\s+/g, '-');

  card.innerHTML =
    `<div class="responsive-employee-card__avatar" style="background-color: ${deptColor}">` +
    `${initials}` +
    `</div>` +
    `<div class="responsive-employee-card__content">` +
    `<div class="responsive-employee-card__header">` +
    `<span class="responsive-employee-card__name">${employee.firstName} ${employee.lastName}</span>` +
    `<span class="status-badge status-badge--${statusClass}">${employee.status}</span>` +
    `</div>` +
    `<div class="responsive-employee-card__title">${employee.title}</div>` +
    `<div class="responsive-employee-card__meta">` +
    `<span class="responsive-employee-card__dept" style="color: ${deptColor}">${employee.department}</span>` +
    `<span class="responsive-employee-card__separator">•</span>` +
    `<span class="responsive-employee-card__salary">${salary}</span>` +
    `<span class="responsive-employee-card__separator">•</span>` +
    `<span class="responsive-employee-card__rating">${employee.rating.toFixed(1)} ★</span>` +
    `${employee.isTopPerformer ? '<span class="responsive-employee-card__top-performer">⭐</span>' : ''}` +
    `</div>` +
    `</div>`;

  return card;
};
```

```ts
// demos/vanilla/src/demos/employee-management/editors.ts
/**
 * Custom Cell Editors for the Vanilla Employee Management Demo
 *
 * Each editor demonstrates different interaction patterns for inline editing.
 */

import type { Employee } from '@demo/shared/employee-management';
import type { ColumnEditorContext } from '@toolbox-web/grid';

/**
 * Interactive 5-star rating editor with keyboard support.
 */
export const starRatingEditor = (ctx: ColumnEditorContext<Employee, number>): HTMLElement => {
  const container = document.createElement('div');
  container.className = 'star-rating-editor';
  container.setAttribute('tabindex', '0');

  let currentValue = ctx.value ?? 3;

  const renderStars = () => {
    container.innerHTML = '';
    for (let i = 1; i <= 5; i++) {
      const star = document.createElement('span');
      const filled = i <= Math.round(currentValue);
      star.textContent = filled ? '★' : '☆';
      star.className = `star-rating-editor__star ${filled ? 'star-rating-editor__star--filled' : 'star-rating-editor__star--empty'}`;
      star.dataset.value = String(i);

      star.addEventListener('click', (e) => {
        e.stopPropagation();
        currentValue = i;
        renderStars();
        ctx.commit(i);
      });
      container.appendChild(star);
    }
    const label = document.createElement('span');
    label.textContent = ` ${currentValue.toFixed(1)}`;
    label.className = 'star-rating-editor__label';
    container.appendChild(label);
  };

  container.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft' && currentValue > 1) {
      currentValue = Math.max(1, currentValue - 0.5);
      renderStars();
    } else if (e.key === 'ArrowRight' && currentValue < 5) {
      currentValue = Math.min(5, currentValue + 0.5);
      renderStars();
    } else if (e.key === 'Enter') {
      ctx.commit(currentValue);
    } else if (e.key === 'Escape') {
      ctx.cancel();
    }
  });

  renderStars();
  // Note: Don't auto-focus here - the grid handles focus via beginBulkEdit/inlineEnterEdit
  // Auto-focusing here would cause all editors to fight for focus, with the last one winning
  return container;
};

/**
 * Bonus slider editor with percentage display.
 */
export const bonusSliderEditor = (ctx: ColumnEditorContext<Employee, number>): HTMLElement => {
  const container = document.createElement('div');
  container.className = 'bonus-slider-editor';

  const salary = ctx.row.salary || 100000;
  const minBonus = Math.round(salary * 0.02);
  const maxBonus = Math.round(salary * 0.25);
  let currentValue = ctx.value ?? Math.round(salary * 0.1);

  const slider = document.createElement('input');
  slider.type = 'range';
  slider.min = String(minBonus);
  slider.max = String(maxBonus);
  slider.value = String(currentValue);
  slider.className = 'bonus-slider-editor__slider';

  const display = document.createElement('span');
  const updateDisplay = () => {
    const percent = ((currentValue / salary) * 100).toFixed(1);
    const colorClass =
      parseFloat(percent) >= 15
        ? 'bonus-slider-editor__value--high'
        : parseFloat(percent) >= 10
          ? 'bonus-slider-editor__value--medium'
          : 'bonus-slider-editor__value--low';
    display.innerHTML = `<strong class="${colorClass}">$${currentValue.toLocaleString()}</strong> <small class="bonus-slider-editor__percent">(${percent}%)</small>`;
  };
  display.className = 'bonus-slider-editor__display';
  updateDisplay();

  slider.addEventListener('input', () => {
    currentValue = parseInt(slider.value, 10);
    updateDisplay();
  });

  slider.addEventListener('change', () => ctx.commit(currentValue));
  slider.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') ctx.commit(currentValue);
    else if (e.key === 'Escape') ctx.cancel();
  });

  container.appendChild(slider);
  container.appendChild(display);
  // Note: Don't auto-focus here - the grid handles focus via beginBulkEdit/inlineEnterEdit
  return container;
};

/**
 * Status selection editor with colored badges.
 */
export const statusSelectEditor = (ctx: ColumnEditorContext<Employee, string>): HTMLElement => {
  const container = document.createElement('div');
  container.className = 'status-select-editor';

  const statusConfig: Record<string, { bg: string; text: string; icon: string }> = {
    Active: { bg: '#d4edda', text: '#155724', icon: '✓' },
    Remote: { bg: '#cce5ff', text: '#004085', icon: '🏠' },
    'On Leave': { bg: '#fff3cd', text: '#856404', icon: '🌴' },
    Contract: { bg: '#e2e3e5', text: '#383d41', icon: '📄' },
    Terminated: { bg: '#f8d7da', text: '#721c24', icon: '✗' },
  };

  const select = document.createElement('select');
  select.className = 'status-select-editor__select';

  Object.entries(statusConfig).forEach(([status, config]) => {
    const option = document.createElement('option');
    option.value = status;
    option.textContent = `${config.icon} ${status}`;
    option.selected = status === ctx.value;
    select.appendChild(option);
  });

  container.appendChild(select);

  select.addEventListener('change', () => ctx.commit(select.value));
  select.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      e.preventDefault();
      ctx.cancel();
    }
  });

  // Note: Don't auto-focus here - the grid handles focus via beginBulkEdit/inlineEnterEdit
  return container;
};

/**
 * Native HTML5 date input editor.
 */
export const dateEditor = (ctx: ColumnEditorContext<Employee, string>): HTMLElement => {
  const input = document.createElement('input');
  input.type = 'date';
  input.value = ctx.value || '';
  input.className = 'date-editor';

  input.addEventListener('change', () => ctx.commit(input.value));
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') ctx.commit(input.value);
    else if (e.key === 'Escape') ctx.cancel();
  });

  // Note: Don't auto-focus here - the grid handles focus via beginBulkEdit/inlineEnterEdit
  return input;
};
```

```ts
// demos/vanilla/src/demos/employee-management/tool-panels.ts
/**
 * Tool Panels for the Vanilla Employee Management Demo
 *
 * Tool panels are registered with the grid's shell and appear in a sidebar.
 * This demonstrates how clean the API is - no type casting needed!
 */

import { DEPARTMENTS, type Employee, type GridElement } from '@demo/shared/employee-management';
import { shadowDomStyles } from '@demo/shared/employee-management/styles';
import { FilteringPlugin } from '@toolbox-web/grid/all';

/**
 * Injects all demo styles into the grid.
 * Uses the grid's registerStyles() API.
 */
export function injectToolPanelStyles(grid: GridElement<Employee>): void {
  grid.registerStyles?.('demo-styles', shadowDomStyles);
}

/**
 * Registers the Quick Filters tool panel.
 */
export function registerQuickFiltersPanel(grid: GridElement<Employee>): void {
  grid.getPluginByName('shell')?.registerToolPanel({
    id: 'quick-filters',
    title: 'Quick Filters',
    icon: '🔍',
    tooltip: 'Apply quick filters to the data',
    order: 10,
    render: (container: HTMLElement) => {
      const content = document.createElement('div');
      content.className = 'tool-panel-content';
      content.innerHTML = `
        <div class="filter-section">
          <label class="filter-label">Department</label>
          <select id="dept-filter" class="filter-select">
            <option value="">All Departments</option>
            ${DEPARTMENTS.map((d) => `<option value="${d}">${d}</option>`).join('')}
          </select>
        </div>
        <div class="filter-section">
          <label class="filter-label">Level</label>
          <div class="filter-pills">
            ${['Junior', 'Mid', 'Senior', 'Lead', 'Principal', 'Director']
              .map(
                (l) => `
              <label class="filter-pill"><input type="checkbox" value="${l}" class="level-filter"><span>${l}</span></label>
            `,
              )
              .join('')}
          </div>
        </div>
        <div class="filter-section">
          <label class="filter-label">Status</label>
          <div class="filter-pills">
            ${['Active', 'Remote', 'On Leave', 'Contract', 'Terminated']
              .map(
                (s) => `
              <label class="filter-pill"><input type="checkbox" value="${s}" class="status-filter"><span>${s}</span></label>
            `,
              )
              .join('')}
          </div>
        </div>
        <div class="filter-section">
          <label class="filter-label">Rating</label>
          <div class="filter-range">
            <input type="range" id="rating-filter" min="0" max="5" step="0.5" value="0">
            <span id="rating-value" class="filter-range__value">≥ 0</span>
          </div>
        </div>
        <div class="filter-section">
          <label class="filter-checkbox"><input type="checkbox" id="top-performer-filter"><span>⭐ Top Performers Only</span></label>
        </div>
        <div class="filter-actions">
          <button id="apply-filters" class="btn-primary">Apply Filters</button>
          <button id="clear-filters" class="btn-secondary">Clear</button>
        </div>
      `;
      container.appendChild(content);

      // Pill toggle styling
      content.querySelectorAll('.level-filter, .status-filter').forEach((input) => {
        input.addEventListener('change', (e) => {
          const cb = e.target as HTMLInputElement;
          cb.closest('.filter-pill')?.classList.toggle('filter-pill--active', cb.checked);
        });
      });

      // Rating slider
      const ratingSlider = content.querySelector('#rating-filter') as HTMLInputElement;
      const ratingValue = content.querySelector('#rating-value') as HTMLElement;
      ratingSlider?.addEventListener('input', () => (ratingValue.textContent = `≥ ${ratingSlider.value}`));

      // Apply filters - clean API, no type casting needed!
      content.querySelector('#apply-filters')?.addEventListener('click', () => {
        const plugin = grid.getPlugin?.(FilteringPlugin);
        if (!plugin) return;
        plugin.clearAllFilters();

        const dept = (content.querySelector('#dept-filter') as HTMLSelectElement).value;
        if (dept) plugin.setFilter('department', { type: 'text', operator: 'equals', value: dept });

        const levels = Array.from(content.querySelectorAll('.level-filter:checked')).map(
          (el) => (el as HTMLInputElement).value,
        );
        if (levels.length) plugin.setFilter('level', { type: 'set', operator: 'in', value: levels });

        const statuses = Array.from(content.querySelectorAll('.status-filter:checked')).map(
          (el) => (el as HTMLInputElement).value,
        );
        if (statuses.length) plugin.setFilter('status', { type: 'set', operator: 'in', value: statuses });

        const minRating = parseFloat(ratingSlider.value);
        if (minRating > 0)
          plugin.setFilter('rating', { type: 'number', operator: 'greaterThanOrEqual', value: minRating });

        if ((content.querySelector('#top-performer-filter') as HTMLInputElement).checked) {
          plugin.setFilter('isTopPerformer', { type: 'boolean', operator: 'equals', value: true });
        }
      });

      // Clear filters
      content.querySelector('#clear-filters')?.addEventListener('click', () => {
        grid.getPlugin?.(FilteringPlugin)?.clearAllFilters();
        (content.querySelector('#dept-filter') as HTMLSelectElement).value = '';
        content.querySelectorAll('.level-filter, .status-filter').forEach((input) => {
          (input as HTMLInputElement).checked = false;
          input.closest('.filter-pill')?.classList.remove('filter-pill--active');
        });
        ratingSlider.value = '0';
        ratingValue.textContent = '≥ 0';
        (content.querySelector('#top-performer-filter') as HTMLInputElement).checked = false;
      });

      return () => content.remove();
    },
  });
}

/**
 * Registers the Analytics tool panel.
 */
export function registerAnalyticsPanel(grid: GridElement<Employee>): void {
  grid.getPluginByName('shell')?.registerToolPanel({
    id: 'analytics',
    title: 'Analytics',
    icon: '📈',
    tooltip: 'View data analytics and insights',
    order: 20,
    render: (container: HTMLElement) => {
      const rows = grid.rows || [];
      const totalSalary = rows.reduce((sum, r) => sum + r.salary, 0);
      const avgSalary = totalSalary / rows.length;
      const avgRating = rows.reduce((sum, r) => sum + r.rating, 0) / rows.length;
      const topPerformers = rows.filter((r) => r.isTopPerformer).length;
      const deptCounts = rows.reduce(
        (acc, r) => ({ ...acc, [r.department]: (acc[r.department] || 0) + 1 }),
        {} as Record<string, number>,
      );
      const sortedDepts = Object.entries(deptCounts).sort((a, b) => b[1] - a[1]);
      const largestDept = sortedDepts[0];

      const formatCurrency = (v: number) =>
        v.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });

      const content = document.createElement('div');
      content.className = 'analytics-content';
      content.innerHTML = `
        <div class="stat-cards">
          <div class="stat-card stat-card--payroll">
            <div class="stat-card__label">Total Payroll</div>
            <div class="stat-card__value">${formatCurrency(totalSalary)}</div>
          </div>
          <div class="stat-card stat-card--salary">
            <div class="stat-card__label">Avg Salary</div>
            <div class="stat-card__value">${formatCurrency(avgSalary)}</div>
          </div>
          <div class="stat-card stat-card--rating">
            <div class="stat-card__label">Avg Rating</div>
            <div class="stat-card__value">${avgRating.toFixed(1)} ★</div>
          </div>
          <div class="stat-card stat-card--performers">
            <div class="stat-card__label">Top Performers</div>
            <div class="stat-card__value">${topPerformers}</div>
          </div>
        </div>
        <div class="dept-distribution">
          <h4 class="dept-distribution__title">Department Distribution</h4>
          <div class="dept-bars">
            ${sortedDepts
              .slice(0, 6)
              .map(
                ([dept, count]) => `
              <div class="dept-bar">
                <span class="dept-bar__name" title="${dept}">${dept}</span>
                <div class="dept-bar__track"><div class="dept-bar__fill" style="width: ${(count / rows.length) * 100}%"></div></div>
                <span class="dept-bar__count">${count}</span>
              </div>
            `,
              )
              .join('')}
          </div>
        </div>
        <div class="largest-dept">
          <div class="largest-dept__label">Largest Department</div>
          <div class="largest-dept__value">${largestDept?.[0] || 'N/A'} <span class="largest-dept__count">(${largestDept?.[1] || 0} employees)</span></div>
        </div>
      `;
      container.appendChild(content);
      return () => content.remove();
    },
  });
}
```

```ts
// demos/vanilla/src/demos/employee-management/grid-factory.ts
/**
 * Employee Management Grid Factory
 *
 * Pure factory function that creates a fully configured employee grid element.
 * Decoupled from the route shell so the docs site can call `createEmployeeGrid()`
 * directly via the `@demo/vanilla` alias.
 */

// Import shared demo styles (applies to document)
import '@demo/shared/employee-management/demo-styles.css';

// Import the grid component (registers <tbw-grid> custom element)
import '@toolbox-web/grid';

// Import grid factory and plugins
import { createGrid, type DataGridElement } from '@toolbox-web/grid/all';

// Import shared data generators and types
import { generateEmployees, type Employee } from '@demo/shared/employee-management';

// Import grid configuration from separate file
import { createGridConfig, type GridConfigOptions } from './grid-config';

// Import tool panel registration
import { injectToolPanelStyles, registerAnalyticsPanel, registerQuickFiltersPanel } from './tool-panels';

/**
 * Options for creating an employee grid.
 * Extends GridConfigOptions with row count.
 */
export interface EmployeeGridOptions extends GridConfigOptions {
  rowCount: number;
}

/**
 * Creates a fully configured employee management grid.
 *
 * @param options - Configuration options for the grid
 * @returns The configured grid element
 */
export function createEmployeeGrid(options: EmployeeGridOptions): DataGridElement<Employee> {
  const { rowCount, ...configOptions } = options;

  // Create the grid element using the typed factory function
  const grid = createGrid<Employee>();
  grid.id = 'employee-grid';
  grid.className = 'demo-grid';

  // Create toolbar buttons container (users have full control over button HTML)
  const toolButtons = document.createElement('tbw-grid-tool-buttons');

  const exportCsvBtn = document.createElement('button');
  exportCsvBtn.className = 'tbw-toolbar-btn';
  exportCsvBtn.setAttribute('title', 'Export CSV');
  exportCsvBtn.setAttribute('aria-label', 'Export CSV');
  exportCsvBtn.textContent = '📄';
  exportCsvBtn.onclick = () => grid.getPluginByName?.('export')?.exportCsv?.({ fileName: 'employees' });

  const exportExcelBtn = document.createElement('button');
  exportExcelBtn.className = 'tbw-toolbar-btn';
  exportExcelBtn.setAttribute('title', 'Export Excel');
  exportExcelBtn.setAttribute('aria-label', 'Export Excel');
  exportExcelBtn.textContent = '📊';
  exportExcelBtn.onclick = () => grid.getPluginByName?.('export')?.exportExcel?.({ fileName: 'employees' });

  toolButtons.appendChild(exportCsvBtn);
  toolButtons.appendChild(exportExcelBtn);
  grid.appendChild(toolButtons);

  // Apply configuration
  grid.gridConfig = createGridConfig(configOptions);

  // Set initial data
  grid.rows = generateEmployees(rowCount);

  // Register tool panels and inject styles after grid is ready
  grid.ready?.().then(() => {
    registerQuickFiltersPanel(grid);
    registerAnalyticsPanel(grid);
    grid.refreshShellHeader?.();
    injectToolPanelStyles(grid);
  });

  return grid;
}

// Re-export config helpers so consumers can build their own grid variants
export { createGridConfig, type GridConfigOptions } from './grid-config';
```
