# Custom Plugins

> Build custom plugins for @toolbox-web/grid — lifecycle hooks, communication, queries, manifests, testing, and complete examples.

Learn how to extend `@toolbox-web/grid` with your own plugins. Plugins can add new features, modify data, inject styles, and respond to user interactions.

## Plugin Architecture

All plugins extend [`BaseGridPlugin`](/grid/api/plugin-development/classes/basegridplugin.md) and implement lifecycle hooks:

```mermaid
flowchart TB
    A["attach()<br/><i>Called when plugin is added to grid</i>"]
    B["processColumns()<br/><i>Transform column definitions</i>"]
    C["processRows()<br/><i>Transform row data</i>"]

    subgraph render["For each visible row"]
        direction TB
        D["afterCellRender()<br/><i>Per-cell hook (called for each cell)</i>"]
        E["afterRowRender()<br/><i>Per-row hook (after all cells in row)</i>"]
        D --> E
    end

    F["afterRender()<br/><i>Grid-wide DOM manipulation</i>"]
    G["User Interactions<br/><i>onCellClick, onKeyDown, etc.</i>"]
    H["detach()<br/><i>Cleanup when plugin is removed</i>"]

    A --> B --> C --> render --> F --> G --> H
```

## Basic Plugin Structure

A typical plugin lives in a folder under `libs/grid/src/lib/plugins/`:

  - my-feature/
    - my-feature-plugin.ts — Plugin class extending [`BaseGridPlugin`](/grid/api/plugin-development/classes/basegridplugin.md)
    - my-feature-plugin.spec.ts — Co-located unit tests
    - my-feature.css — Plugin styles (optional)
    - types.ts — Public config/event types (optional)
    - index.ts — Barrel export

```typescript
import { BaseGridPlugin, type AfterCellRenderContext } from '@toolbox-web/grid';

// 1. Define your config interface
interface HighlightConfig {
  field: string;
  threshold: number;
  className?: string;
}

// 2. Extend BaseGridPlugin with your config type
export class HighlightPlugin extends BaseGridPlugin<HighlightConfig> {
  // Required: unique plugin name
  readonly name = 'highlight';

  // Optional: CSS styles to inject
  override readonly styles = `
    .highlight-cell {
      background: linear-gradient(135deg, #fff3cd, #ffeeba);
      font-weight: 600;
    }
  `;

  // Called for each cell during rendering
  override afterCellRender(context: AfterCellRenderContext): void {
    const { field, threshold, className = 'highlight-cell' } = this.config;
    const { column, value, cellElement } = context;

    if (column.field !== field) return;

    const numValue = typeof value === 'number' ? value : parseFloat(String(value));
    if (!isNaN(numValue) && numValue > threshold) {
      cellElement.classList.add(className);
    }
  }
}
```

## Using Your Plugin

```typescript
import { HighlightPlugin } from './highlight-plugin';

grid.gridConfig = {
  columns: [...],
  plugins: [
    new HighlightPlugin({
      field: 'salary',
      threshold: 100000,
      className: 'high-earner',
    }),
  ],
};
```

## Lifecycle Hooks

### `attach(grid)`

Called when the plugin is attached to the grid. Use for initial setup.

```typescript
import type { GridElement } from '@toolbox-web/grid';

override attach(grid: GridElement): void {
  super.attach(grid); // Always call super first!
  this.state = new Map();
  console.log('Attached to grid with', grid.rows.length, 'rows');
}
```

### `detach()`

Called when the plugin is removed. Clean up event listeners, timers, etc.

```typescript
override detach(): void {
  this.state.clear();
  clearInterval(this.timer);
  super.detach(); // Always call super last!
}
```

### `processColumns(columns)`

Transform column definitions before rendering. Return the modified array.

```typescript
override processColumns(columns: ColumnConfig[]): ColumnConfig[] {
  return [
    ...columns,
    {
      field: '__rowNumber',
      header: '#',
      width: 50,
      renderer: (ctx) => String(ctx.rowIndex + 1),
    },
  ];
}
```

### `processRows(rows)`

Transform row data before rendering. Return the modified array.

```typescript
override processRows(rows: T[]): T[] {
  return rows.filter(row => row.active);
}
```

### `afterRender()`

Called after each render cycle. Use for grid-wide DOM manipulation.

```typescript
override afterRender(): void {
  const gridEl = this.gridElement;
  const rows = gridEl.querySelectorAll('.data-grid-row');
  rows.forEach((row, i) => {
    if (i % 2 === 0) row.classList.add('even-row');
  });
}
```

### `afterCellRender(context)`

Called after each cell is rendered. More efficient than `afterRender` for per-cell modifications because you receive the cell context directly—no DOM queries needed.

```typescript
import type { AfterCellRenderContext } from '@toolbox-web/grid';

override afterCellRender(context: AfterCellRenderContext): void {
  const { row, rowIndex, colIndex, value, cellElement, column } = context;

  // Add selection class without DOM queries
  if (this.isSelected(rowIndex, colIndex)) {
    cellElement.classList.add('selected');
  }

  // Add validation error styling
  if (this.hasError(row, column.field)) {
    cellElement.classList.add('has-error');
  }
}
```

:::note[Performance]
`afterCellRender` is called for every visible cell during render and scroll. Keep implementation fast.
:::

### `afterRowRender(context)`

Called after a row is fully rendered (all cells complete). Use for row-level decorations, styling, or ARIA attributes.

```typescript
import type { AfterRowRenderContext } from '@toolbox-web/grid';

override afterRowRender(context: AfterRowRenderContext): void {
  const { row, rowIndex, rowElement } = context;

  // Add row selection class without DOM queries
  if (this.isRowSelected(rowIndex)) {
    rowElement.classList.add('selected', 'row-focus');
  }

  // Add validation error styling
  if (this.rowHasErrors(row)) {
    rowElement.classList.add('has-errors');
  }
}
```

:::note[Performance]
`afterRowRender` is called for every visible row during render and scroll. Keep implementation fast.
:::

## Built-in Plugin Helpers

[`BaseGridPlugin`](/grid/api/plugin-development/classes/basegridplugin.md) provides protected helpers:

| Helper | Description |
|--------|-------------|
| `this.grid` | Typed `GridElement` (extends `GridElementRef`) |
| `this.gridElement` | Grid as `HTMLElement` for DOM queries |
| `this.columns` | Current column configurations |
| `this.visibleColumns` | Only visible columns |
| `this.rows` | Processed rows (after filtering, grouping) |
| `this.sourceRows` | Original unfiltered rows |
| `this.disconnectSignal` | `AbortSignal` for auto-cleanup |
| `this.isAnimationEnabled` | Whether animations are enabled |
| `this.animationDuration` | Animation duration in ms |
| `this.gridIcons` | Merged icon configuration |
| `this.getPluginByName(name)` | Get another plugin by name (preferred) |
| `this.getPlugin(PluginClass)` | Get another plugin by class (alternative) |
| `this.emit(eventName, detail)` | Dispatch a DOM `CustomEvent` from the grid (external consumers) |
| `this.broadcast(eventType, detail)` | Emit on the internal event bus + DOM (other plugins + external consumers) |
| `this.on(eventType, callback)` | Subscribe to event-bus events (auto-cleaned on detach) |
| `this.requestRender()` | Request full re-render |
| `this.requestAfterRender()` | Request lightweight style update |
| `this.requestVirtualRefresh()` | Re-render visible rows without rebuilding the row model |
| `this.setIcon(el, iconKey)` | Set icon on element using CSS-first hybrid approach |

## Event Hooks

### `onCellClick(event)`

Handle cell click events. Return `true` to prevent default behavior.

```typescript
import type { CellClickEvent } from '@toolbox-web/grid';

override onCellClick(event: CellClickEvent): boolean | void {
  const { row, field, value, cellEl } = event;
  if (field === 'delete') {
    this.handleDelete(row);
    return true; // Prevent default click handling
  }
  return false; // Allow normal processing
}
```

### `onCellMouseDown(event)`

Handle mousedown for drag operations or selection.

```typescript
import type { CellMouseEvent } from '@toolbox-web/grid';

override onCellMouseDown(event: CellMouseEvent): boolean | void {
  const { row, field } = event;

  if (field === 'drag-handle') {
    this.startDrag(row);
    return true;
  }

  return false;
}
```

### `onKeyDown(event)`

Handle keyboard events. Return `true` to prevent default.

```typescript
override onKeyDown(event: KeyboardEvent): boolean | void {
  if (event.ctrlKey && event.key === 'd') {
    this.duplicateSelectedRow();
    return true;
  }
  return false;
}
```

### `onScroll(event)`

Respond to scroll events (use sparingly — can impact performance).

```typescript
import type { ScrollEvent } from '@toolbox-web/grid';

override onScroll(event: ScrollEvent): void {
  this.updateStickyElements(event.scrollTop);
}
```

### `renderRow(row, rowEl, rowIndex)`

Custom row rendering. Return `true` to skip default rendering.

```typescript
override renderRow(row: T, rowEl: HTMLElement, rowIndex: number): boolean | void {
  if (row.type === 'section-header') {
    rowEl.innerHTML = `<div class="section-header">${row.title}</div>`;
    return true;
  }
  return false;
}
```

## Injecting Styles

Plugins can inject CSS via the `styles` property (uses `adoptedStyleSheets`):

#### External CSS File

```typescript
// Import CSS as string (Vite)
import styles from './my-plugin.css?inline';

export class MyPlugin extends BaseGridPlugin {
  override readonly styles = styles;
}
```

```css
/* my-plugin.css */
.my-custom-class {
  background: #f0f0f0;
  border-left: 3px solid #1976d2;
}
```

#### Inline Styles

```typescript
export class MyPlugin extends BaseGridPlugin {
  override readonly styles = `
    .my-custom-class {
      background: #f0f0f0;
      border-left: 3px solid #1976d2;
    }

    .my-custom-class:hover {
      background: #e0e0e0;
    }
  `;
}
```

:::caution
Do not append `<style>` elements as **children of `<tbw-grid>`** — the grid calls `replaceChildren()` during renders, which removes child nodes. For plugin styles, use the `styles` property (shown above) or `this.gridElement.registerStyles(id, css)`. Note: external CSS (stylesheets, `<style>` in `<head>`) is unaffected and works normally since the grid uses light DOM.
:::

## Accessing Grid State

```typescript
override afterRender(): void {
  // Access current rows (after processing)
  const rows = this.grid.rows;

  // Access effective config
  const config = this.grid.gridConfig;

  // Access sort state
  const sortState = this.grid.sortState;

  // Access changed rows (editing)
  const changes = this.grid.changedRows;

  // Request a full re-render (internal API for plugins)
  this.grid.requestRender();

  // Request only afterRender hooks (lightweight update)
  this.grid.requestAfterRender();
}
```

## Plugin Manifest

The **Plugin Manifest** is a static property that declares metadata about your plugin's capabilities and requirements. The grid uses this metadata for:

- **Validation**: Detect missing plugins when their properties are used
- **Configuration rules**: Warn or error on invalid config combinations
- **Query routing**: Efficiently route queries only to plugins that handle them
- **Event discovery**: Document events that other plugins can subscribe to
- **Incompatibility detection**: Warn when conflicting plugins are loaded together

### Manifest Structure

```typescript
import { BaseGridPlugin, type PluginManifest } from '@toolbox-web/grid';

interface MyPluginConfig {
  optionA?: boolean;
  optionB?: boolean;
}

export class MyPlugin extends BaseGridPlugin<MyPluginConfig> {
  static override readonly manifest: PluginManifest<MyPluginConfig> = {
    // Properties this plugin owns (for validation)
    ownedProperties: [
      {
        property: 'myColumnProp',
        level: 'column',
        description: 'the "myColumnProp" column property',
      },
      {
        property: 'myGridOption',
        level: 'config',
        description: 'the "myGridOption" grid config option',
      },
    ],

    // Configuration validation rules
    configRules: [
      {
        id: 'my-plugin/invalid-combo',
        severity: 'warn',
        message: 'optionA and optionB cannot both be true',
        check: (config) => config.optionA === true && config.optionB === true,
      },
    ],

    // Queries this plugin handles
    queries: [
      { type: 'getMyState', description: 'Get the current plugin state' },
    ],

    // Events this plugin emits
    events: [
      { type: 'my-state-change', description: 'Emitted when state changes' },
    ],

    // Plugins that conflict with this one
    incompatibleWith: [
      { name: 'conflictingPlugin', reason: 'Both plugins modify the same DOM elements' },
    ],

    // Hook execution priority (lower = earlier, default 0)
    hookPriority: {
      processRows: 100,   // Run after most plugins
      afterRender: -50,   // Run before most plugins
    },
  };

  readonly name = 'myPlugin';
}
```

### Owned Properties

Declare properties your plugin adds to [`ColumnConfig`](/grid/api/core/interfaces/columnconfig.md) or [`GridConfig`](/grid/api/core/interfaces/gridconfig.md). If a user configures these properties without loading your plugin, the grid throws a helpful error:

```typescript
ownedProperties: [
  {
    property: 'editable',
    level: 'column',
    description: 'the "editable" column property',
    importHint: "import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';",
  },
],
```

Error shown to user:

```
[tbw-grid] Configuration error:
Column(s) [name, email] use the "editable" column property, but the required plugin is not loaded.
  → Add the plugin to your gridConfig.plugins array:
    import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';
    plugins: [new EditingPlugin(), ...]
```

### Configuration Rules

```typescript
configRules: [
  {
    id: 'selection/range-dblclick',
    severity: 'warn',
    message: '"triggerOn: dblclick" has no effect when mode is "range".',
    check: (config) => config.mode === 'range' && config.triggerOn === 'dblclick',
  },
],
```

- `severity: 'error'` — Throws an exception (always)
- `severity: 'warn'` — Logs to console (development only)

### Hook Priority

By default, hooks execute in plugin array order. Use `hookPriority` to override the execution order for specific hooks without changing the plugin array:

```typescript
static override readonly manifest: PluginManifest = {
  hookPriority: {
    processRows: 100,   // Higher value → runs later
    afterRender: -50,   // Negative value → runs earlier
  },
};
```

- Default priority is **0** for all hooks
- **Lower values** execute first; **higher values** execute later
- Plugins with equal priority preserve their original array order
- Priority is configured **per hook** — a plugin can run early for one hook and late for another

Available hooks: `processColumns`, `processRows`, `afterRender`, `afterCellRender`, `afterRowRender`, `onHeaderClick`, `onRowClick`, `onCellClick`, `onCellMouseDown`, `onCellMouseMove`, `onCellMouseUp`, `onKeyDown`, `onScroll`, `onScrollRender`.

### Plugin Dependencies

Declare required plugins via a static `dependencies` property:

```typescript
export class UndoRedoPlugin extends BaseGridPlugin<UndoRedoConfig> {
  static override readonly dependencies: PluginDependency[] = [
    { name: 'editing', required: true, reason: 'Tracks cell edit history' },
    { name: 'selection', required: false, reason: 'Enables selection-based undo' },
  ];

  readonly name = 'undoRedo';
}
```

:::danger
When using the manual `plugins` array, dependencies must be loaded *before* the dependent plugin. Wrong order **throws a runtime error**:

```typescript
// ✅ Correct order
plugins: [new EditingPlugin(), new UndoRedoPlugin()]

// ❌ Wrong order — throws error
plugins: [new UndoRedoPlugin(), new EditingPlugin()]
```
:::

:::tip
The **features API** handles dependency ordering automatically. When you use `features: { ... }`, the grid reorders plugins based on declared dependencies — no manual ordering required, and import order doesn't matter (safe from Prettier's alphabetical sorting).

```typescript
// Order doesn't matter — the grid resolves dependencies for you
import '@toolbox-web/grid/features/undo-redo';
import '@toolbox-web/grid/features/editing';

grid.gridConfig = {
  features: { editing: true, undoRedo: true },
};
```
:::

#### Conditional dependencies and severity

A dependency can be made **config-conditional** with a `when` predicate, and its missing-dependency behavior tuned with `severity`:

```typescript
export class PivotPlugin extends BaseGridPlugin<PivotConfig> {
  static override readonly dependencies: PluginDependency[] = [
    {
      name: 'shell',
      // Only needed when the tool panel is enabled
      required: false,
      when: (cfg) => (cfg as PivotConfig).showToolPanel === true,
      severity: 'warn',
      reason: 'PivotPlugin needs a tool-panel host when showToolPanel is enabled',
    },
  ];

  readonly name = 'pivot';
}
```

- **`when(pluginConfig)`** — receives the depending plugin's resolved config (defaults merged with user config) and is evaluated **before** the plugin attaches. Return `false` to skip the dependency entirely. When omitted, the dependency always applies.
- **`severity`** — overrides the default derived from `required`:
  - `'error'` — throws and halts grid setup (the default when `required` is not `false`).
  - `'warn'` — logs a `console.warn` and continues (development only).
  - `'info'` — logs a verbose `console.debug` and continues (development only).

When `severity` is omitted, a missing **hard** dependency (`required !== false`) throws and a missing **soft** dependency (`required: false`) stays silent — preserving backward-compatible behavior. Opt into a message by setting `severity` explicitly.

## Plugin Communication

### Event Bus (Plugin-to-Plugin)

Plugins can emit and subscribe to events using the built-in Event Bus. Events are automatically cleaned up when a plugin is detached.

```typescript
import { BaseGridPlugin, type PluginManifest } from '@toolbox-web/grid';

// Plugin A: Emit events
export class FilterPlugin extends BaseGridPlugin<FilterConfig> {
  readonly name = 'filtering';

  // Declare events in manifest for discoverability
  static override readonly manifest: PluginManifest = {
    events: [
      { type: 'filter-change', description: 'Emitted when filters change' },
    ],
  };

  applyFilter(criteria: FilterCriteria): void {
    // ... filter logic

    // Broadcast to both DOM consumers and other plugins
    this.broadcast('filter-change', { criteria, rowCount: this.rows.length });
  }
}

// Plugin B: Subscribe to events
export class SelectionPlugin extends BaseGridPlugin<SelectionConfig> {
  readonly name = 'selection';

  override attach(grid: GridElement): void {
    super.attach(grid);

    // Subscribe — auto-cleaned on detach
    this.on('filter-change', (detail) => {
      console.log('Filter changed, clearing selection');
      this.clearSelection();
    });
  }
}
```

### Query System (Synchronous State Retrieval)

Plugins can expose queryable state that other plugins can retrieve synchronously.

```typescript
import { BaseGridPlugin, type PluginManifest, type PluginQuery } from '@toolbox-web/grid';

// Plugin A: Handle queries
export class SelectionPlugin extends BaseGridPlugin<SelectionConfig> {
  readonly name = 'selection';

  // Declare queries in manifest for routing
  static override readonly manifest: PluginManifest = {
    queries: [
      { type: 'getSelection', description: 'Get current selection state' },
    ],
  };

  override handleQuery(query: PluginQuery): unknown {
    if (query.type === 'getSelection') {
      return this.getSelection();
    }
    return undefined;
  }
}

// Plugin B: Query other plugins
export class ClipboardPlugin extends BaseGridPlugin<ClipboardConfig> {
  readonly name = 'clipboard';

  copy(): void {
    const responses = this.grid.query<SelectionResult>('getSelection', undefined);
    const selection = responses[0];
    if (selection?.ranges.length > 0) {
      // Copy selected cells
    }
  }
}
```

### DOM Events (External Consumers)

For events that external code should listen to, use `emit()`:

```typescript
this.emit('copy', { text: copiedText, rowCount: 5 });

// External code:
grid.on('copy', (detail) => console.log(detail));
```

## TypeScript Generics

For type-safe row access, use generics:

```typescript
export class TypedPlugin<T extends { id: number }> extends BaseGridPlugin<{ idField: keyof T }> {
  override processRows(rows: T[]): T[] {
    const idField = this.config.idField;
    return rows.filter(row => row[idField] != null);
  }
}

// Usage
new TypedPlugin<Employee>({ idField: 'employeeId' });
```

## Complete Example: Row Numbering Plugin

This example adds a synthetic column that doesn't map to any field in the data. The `__rowNumber` field doesn't exist on row objects—the renderer uses `ctx.rowIndex` instead of `ctx.value`. This pattern is safe because:

- `processColumns` only modifies column definitions, not row data
- Renderers produce DOM output—they don't write back to the source data
- The `rows` array remains unchanged

```typescript
import { BaseGridPlugin, type ColumnConfig } from '@toolbox-web/grid';

interface RowNumberConfig {
  header?: string;
  width?: number;
  startFrom?: number;
}

export class RowNumberPlugin extends BaseGridPlugin<RowNumberConfig> {
  readonly name = 'rowNumber';

  override readonly styles = `
    .row-number-cell {
      color: #888;
      font-size: 0.85em;
      text-align: center;
    }
  `;

  override processColumns(columns: ColumnConfig[]): ColumnConfig[] {
    const { header = '#', width = 50, startFrom = 1 } = this.config;

    return [
      {
        field: '__rowNumber',
        header,
        width,
        minWidth: 40,
        maxWidth: 80,
        resizable: false,
        sortable: false,
        renderer: (ctx) => {
          const span = document.createElement('span');
          span.className = 'row-number-cell';
          span.textContent = String(ctx.rowIndex + startFrom);
          return span;
        },
      },
      ...columns,
    ];
  }
}
```

## Best Practices

1. **Always call `super`** — In `attach()` call it first, in `detach()` call it last
2. **Use unique names** — Plugin `name` should be unique across all plugins
3. **Clean up resources** — Remove event listeners and timers in `detach()`
4. **Return modified data** — `processColumns` and `processRows` must return arrays
5. **Use `ctx.signal`** — For automatic cleanup of event listeners in renderers
6. **Avoid heavy operations** — Keep hooks fast, especially `onScroll` and `afterRender`
7. **Document your config** — Use JSDoc comments for IDE support
8. **Version your plugins** — Increment version when making breaking changes

## Testing Plugins

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { RowNumberPlugin } from './row-number-plugin';

describe('RowNumberPlugin', () => {
  let plugin: RowNumberPlugin;

  beforeEach(() => {
    plugin = new RowNumberPlugin({ startFrom: 1 });
  });

  it('should add row number column', () => {
    const columns = plugin.processColumns([
      { field: 'name', header: 'Name' },
    ]);

    expect(columns).toHaveLength(2);
    expect(columns[0].field).toBe('__rowNumber');
  });
});
```

## Registering as a Feature

Plugins can be promoted to **features** so consumers can enable them via the declarative `features` config instead of manually instantiating classes. This also enables shorthand syntax and tree-shaking.

### 1. Create the Feature Module

Create a feature file that registers your plugin with a factory function:

```typescript
// my-feature.ts (e.g., libs/grid/src/lib/features/row-numbers.ts)
import { RowNumberPlugin, type RowNumberConfig } from '../plugins/row-numbers';
import { registerFeature } from './registry';

// Augment the FeatureConfig interface so TypeScript knows about your feature
declare module '../core/types' {
  interface FeatureConfig {
    /** Enable row numbering. Shorthand: `true` or a start number. */
    rowNumbers?: boolean | number | RowNumberConfig;
  }
}

// Register the factory — convert shorthands to full plugin config
registerFeature('rowNumbers', (config) => {
  if (config === true) {
    return new RowNumberPlugin(); // defaults
  }
  if (typeof config === 'number') {
    return new RowNumberPlugin({ startFrom: config }); // shorthand
  }
  return new RowNumberPlugin(config as RowNumberConfig); // full config
});
```

### 2. The Pattern

Every feature module follows the same three steps:

1. **Import** the plugin class and its config type
2. **Augment** `FeatureConfig` via `declare module` — this adds your shorthand types to the `features` object
3. **Call** `registerFeature(name, factory)` — the factory receives the raw config value and returns a plugin instance

The factory function is responsible for converting shorthand values (`true`, `'row'`, a number, etc.) into the full plugin constructor config.

### 3. How It Works at Runtime

Feature modules are **side-effect imports** — importing the file automatically registers the feature:

```typescript
// Consumer code
import '@toolbox-web/grid/features/row-numbers'; // ← registers the feature

grid.gridConfig = {
  features: {
    rowNumbers: 5, // shorthand: start from 5
  },
};
```

If no one imports the feature module, it doesn't exist in the bundle — this is how tree-shaking works.

### 4. Common Shorthand Patterns

| Shorthand Type | Example | Usage |
|---------------|---------|-------|
| `boolean` | `filtering: true` | Enable with defaults |
| String literal | `selection: 'row'` | Pick a mode |
| `number` | `rowNumbers: 5` | Single numeric option |
| Full config | `editing: { editOn: 'click', ... }` | Full control |

### 5. Declaring Dependencies

If your feature depends on other features, declare it in the `PLUGIN_DEPENDENCIES` map in `registry.ts`:

```typescript
// In registry.ts
const PLUGIN_DEPENDENCIES: Record<string, string[]> = {
  clipboard: ['selection'],   // clipboard needs selection
  rowNumbers: [],             // no dependencies
};
```

The grid reorders plugins automatically based on declared dependencies and warns if a dependency is missing.

### 6. Framework Adapter Integration

For the feature to work with framework adapters (React, Vue, Angular), each adapter needs to re-export or reference the feature. The typical pattern:

```typescript
// libs/grid-react/src/features/row-numbers.ts
import '@toolbox-web/grid/features/row-numbers';
```

This ensures the side-effect import is available when consumers import from the adapter package:

```tsx
import '@toolbox-web/grid-react/features/row-numbers';

<DataGrid rows={rows} gridConfig={{ features: { rowNumbers: true } }} />
```

## See Also

  - [Plugins Overview](/grid/plugins.md): Built-in plugin catalog with dependencies and import paths
  - [Architecture](/grid/architecture.md): How plugins fit into the grid
  - [Events Reference](/grid/api-reference.md#events): Complete list of all grid and plugin events
  - [API Reference](/grid/api-reference.md): getPluginByName(), getPlugin(), and plugin access methods
