# Responsive Plugin

> Automatically adapt the grid layout for different screen sizes.

The Responsive plugin transforms the grid from a tabular layout to a card/list layout when the grid width falls below a configurable breakpoint. This enables grids to work seamlessly in narrow containers like split-pane UIs, mobile viewports, and dashboard widgets.

## Installation

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

## Basic Usage

#### TypeScript

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

const grid = queryGrid('tbw-grid');
grid.gridConfig = {
  columns: [
    { field: 'id', header: 'ID' },
    { field: 'name', header: 'Name' },
    { field: 'email', header: 'Email' },
  ],
  features: {
    responsive: { breakpoint: 500 },
  },
};
grid.rows = data;
```

#### React

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

function MyGrid({ data }) {
  return (
    <DataGrid
      rows={data}
      columns={[
        { field: 'id', header: 'ID' },
        { field: 'name', header: 'Name' },
        { field: 'email', header: 'Email' },
      ]}
      responsive={{ breakpoint: 500 }}
      style={{ height: '400px' }}
    />
  );
}
```

#### Vue

```html
<script setup>
import '@toolbox-web/grid-vue/features/responsive';
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';

const data = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' },
];
</script>

<template>
  <TbwGrid :rows="data" :responsive="{ breakpoint: 500 }" style="height: 400px">
    <TbwGridColumn field="id" header="ID" />
    <TbwGridColumn field="name" header="Name" />
    <TbwGridColumn field="email" header="Email" />
  </TbwGrid>
</template>
```

#### Angular

```typescript
// Feature import - enables the [responsive] input
import { GridResponsiveDirective } from '@toolbox-web/grid-angular/features/responsive';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { ColumnConfig } from '@toolbox-web/grid';

@Component({
  selector: 'app-my-grid',
  imports: [Grid, GridResponsiveDirective],
  template: `
    <tbw-grid
      [rows]="rows"
      [columns]="columns"
      [responsive]="{ breakpoint: 500 }"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class MyGridComponent {
  rows = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
  ];

  columns: ColumnConfig[] = [
    { field: 'id', header: 'ID' },
    { field: 'name', header: 'Name' },
    { field: 'email', header: 'Email' },
  ];
}
```

## Demos

### Interactive Demo

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

const container = document.getElementById('responsive-default-demo');
if (container) {
  const sampleData = [
    { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, email: 'alice@example.com', startDate: '2020-03-15' },
    { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, email: 'bob@example.com', startDate: '2019-07-22' },
    { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, email: 'carol@example.com', startDate: '2018-11-01' },
    { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, email: 'dan@example.com', startDate: '2021-01-10' },
    { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, email: 'eve@example.com', startDate: '2022-05-03' },
    { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, email: 'frank@example.com', startDate: '2020-08-20' },
  ];
  const columns = [
    { field: 'id', header: 'ID', type: 'number', width: 60 },
    { field: 'name', header: 'Name', width: 150 },
    { field: 'department', header: 'Department', width: 120 },
    { field: 'salary', header: 'Salary', type: 'number', width: 100 },
    { field: 'email', header: 'Email', width: 200 },
    { field: 'startDate', header: 'Start Date', width: 120 },
  ];

  const grid = queryGrid('tbw-grid', container)!;
  const status = container.querySelector('.responsive-status')!;
  let currentBreakpoint = 500;

  function rebuild(breakpoint = 500, hideHeader = false, animate = true, hiddenColumns: string[] = [], debounceMs = 100) {
    currentBreakpoint = breakpoint;
    grid.gridConfig = {
      columns,
      features: { responsive: { breakpoint, hideHeader, animate, hiddenColumns, debounceMs } },
    };
    grid.rows = sampleData;
  }

  rebuild();

  container.addEventListener('control-change', ((e: CustomEvent) => {
    const v = e.detail.allValues;
    rebuild(v.breakpoint as number, v.hideHeader as boolean, v.animate as boolean, v.hiddenColumns as string[], v.debounceMs as number);
  }) as EventListener);

  grid.on('responsive-change', ({ isResponsive, width, breakpoint }) => {
    status.textContent = `Mode: ${isResponsive ? '📱 Card' : '📊 Table'} | Width: ${Math.round(width)}px | Breakpoint: ${breakpoint}px`;
  });

  requestAnimationFrame(() => {
    const width = grid.clientWidth;
    status.textContent = `Mode: ${width < currentBreakpoint ? '📱 Card' : '📊 Table'} | Width: ${Math.round(width)}px | Breakpoint: ${currentBreakpoint}px`;
  });
}
```

Resize the container to see the grid switch between table and card layouts.

### Manual Control

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

import '@toolbox-web/grid/features/responsive';

const container = document.getElementById('responsive-manual-control-demo');
if (container) {
  // Sample data
  const sampleData = [
    {
      id: 1,
      name: 'Alice Johnson',
      department: 'Engineering',
      salary: 95000,
      email: 'alice@example.com',
      startDate: '2020-03-15',
    },
    {
      id: 2,
      name: 'Bob Smith',
      department: 'Marketing',
      salary: 75000,
      email: 'bob@example.com',
      startDate: '2019-07-22',
    },
    {
      id: 3,
      name: 'Carol Williams',
      department: 'Engineering',
      salary: 105000,
      email: 'carol@example.com',
      startDate: '2018-11-01',
    },
    { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, email: 'dan@example.com', startDate: '2021-01-10' },
    {
      id: 5,
      name: 'Eve Davis',
      department: 'Marketing',
      salary: 72000,
      email: 'eve@example.com',
      startDate: '2022-05-03',
    },
    {
      id: 6,
      name: 'Frank Miller',
      department: 'Engineering',
      salary: 98000,
      email: 'frank@example.com',
      startDate: '2020-08-20',
    },
  ];
  const columns = [
    { field: 'id', header: 'ID', type: 'number', width: 60 },
    { field: 'name', header: 'Name', width: 150 },
    { field: 'department', header: 'Department', width: 120 },
    { field: 'salary', header: 'Salary', type: 'number', width: 100 },
    { field: 'email', header: 'Email', width: 200 },
    { field: 'startDate', header: 'Start Date', width: 120 },
  ];

  // Controls
      const controls = container.querySelector('[data-controls-id="responsive-manual-control-demo"]');
      controls.style.cssText = 'margin-bottom: 12px; display: flex; gap: 8px;';

      const tableBtn = document.createElement('button');
      tableBtn.textContent = '📊 Table Mode';
      tableBtn.style.cssText = 'padding: 8px 16px; cursor: pointer;';

      const cardBtn = document.createElement('button');
      cardBtn.textContent = '📱 Card Mode';
      cardBtn.style.cssText = 'padding: 8px 16px; cursor: pointer;';

      controls.appendChild(tableBtn);
      controls.appendChild(cardBtn);

      const grid = queryGrid('tbw-grid', container);

      grid.gridConfig = {
        columns: columns.slice(0, 4), // Fewer columns
        features: { responsive: { breakpoint: 0 } },
      };
      grid.rows = sampleData;

      tableBtn.addEventListener('click', () => grid.getPluginByName('responsive')?.setResponsive(false));
      cardBtn.addEventListener('click', () => grid.getPluginByName('responsive')?.setResponsive(true));
}
```

Use the plugin API to manually control responsive mode.

### Custom Card Renderer

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

const container = document.getElementById('responsive-custom-card-renderer-demo');
if (container) {
  const grid = queryGrid('tbw-grid', container);
  const status = container.querySelector('.responsive-status');
  if (!grid) throw new Error('Grid not found');

  interface Employee {
    id: number;
    name: string;
    department: string;
    salary: number;
    email: string;
    startDate: string;
  }

  grid.gridConfig = {
    columns: [
      { field: 'id', header: 'ID', type: 'number', width: 60 },
      { field: 'name', header: 'Name', width: 150 },
      { field: 'department', header: 'Department', width: 120 },
      { field: 'salary', header: 'Salary', type: 'number', width: 100 },
      { field: 'email', header: 'Email', width: 200 },
      { field: 'startDate', header: 'Start Date', width: 120 },
    ],
    features: {
      responsive: {
        breakpoint: 600,
        cardRenderer: (row: Employee) => {
          const card = document.createElement('div');
          card.className = 'employee-card';
          const deptClass = row.department.toLowerCase().replace(/\s+/g, '-');
          card.innerHTML = `
            <div class="avatar">${row.name[0]}</div>
            <div class="info">
              <div class="name">
                ${row.name}
                <span class="badge ${deptClass}">${row.department}</span>
              </div>
              <div class="meta">$${row.salary.toLocaleString()} · Started ${row.startDate}</div>
              <div class="email">${row.email}</div>
            </div>
          `;
          return card;
        },
      },
    },
  };

  grid.rows = [
    { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, email: 'alice@example.com', startDate: '2020-03-15' },
    { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, email: 'bob@example.com', startDate: '2019-07-22' },
    { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, email: 'carol@example.com', startDate: '2018-11-01' },
    { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, email: 'dan@example.com', startDate: '2021-01-10' },
    { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, email: 'eve@example.com', startDate: '2022-05-03' },
    { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, email: 'frank@example.com', startDate: '2020-08-20' },
  ];

  grid.on('responsive-change', ({ isResponsive, width }) => {
    if (!status) return;
    status.textContent = `Mode: ${isResponsive ? '📱 Custom Cards' : '📊 Table'} | Width: ${Math.round(width)}px`;
  });

  requestAnimationFrame(() => {
    if (!status) return;
    const width = grid.clientWidth;
    status.textContent = `Mode: ${width < 600 ? '📱 Custom Cards' : '📊 Table'} | Width: ${Math.round(width)}px`;
  });
}
```

Use `cardRenderer` for complete control over card layout - avatars, badges, custom layouts, etc.

### Fixed Card Height

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

const container = document.getElementById('responsive-fixed-card-height-demo');
if (container) {
  const grid = queryGrid('tbw-grid', container);
  if (!grid) throw new Error('Grid not found');

  interface Employee {
    id: number;
    name: string;
    department: string;
    salary: number;
    email: string;
    startDate: string;
  }

  grid.gridConfig = {
    columns: [
      { field: 'id', header: 'ID', type: 'number', width: 60 },
      { field: 'name', header: 'Name', width: 150 },
      { field: 'department', header: 'Department', width: 120 },
      { field: 'salary', header: 'Salary', type: 'number', width: 100 },
    ],
    features: {
      responsive: {
        breakpoint: 500,
        cardRowHeight: 60,
        cardRenderer: (row: Employee) => {
          const card = document.createElement('div');
          card.className = 'simple-card';
          card.innerHTML = `
            <div class="initial">${row.name[0]}</div>
            <div class="details">
              <div class="name">${row.name}</div>
              <div class="dept">${row.department}</div>
            </div>
          `;
          return card;
        },
      },
    },
  };

  grid.rows = [
    { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, email: 'alice@example.com', startDate: '2020-03-15' },
    { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, email: 'bob@example.com', startDate: '2019-07-22' },
    { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, email: 'carol@example.com', startDate: '2018-11-01' },
    { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, email: 'dan@example.com', startDate: '2021-01-10' },
    { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, email: 'eve@example.com', startDate: '2022-05-03' },
    { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, email: 'frank@example.com', startDate: '2020-08-20' },
  ];
}
```

Use `cardRowHeight` for consistent card heights, which helps virtualization performance.

### Progressive Degradation

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

const container = document.getElementById('responsive-progressive-degradation-demo');
if (container) {
  const grid = queryGrid('tbw-grid', container);
  const status = container.querySelector('.responsive-status');
  if (!grid) throw new Error('Grid not found');

  grid.gridConfig = {
    columns: [
      { field: 'id', header: 'ID', type: 'number', width: 60 },
      { field: 'name', header: 'Name', width: 150 },
      { field: 'department', header: 'Department', width: 120 },
      { field: 'salary', header: 'Salary', type: 'number', width: 100 },
      { field: 'email', header: 'Email', width: 200 },
      { field: 'startDate', header: 'Start Date', width: 120 },
    ],
    features: {
      responsive: {
        breakpoints: [
          { maxWidth: 900, hiddenColumns: ['startDate'] },
          { maxWidth: 700, hiddenColumns: ['startDate', 'email'] },
          { maxWidth: 500, hiddenColumns: ['startDate', 'email', 'salary'] },
          { maxWidth: 400, cardLayout: true },
        ],
      },
    },
  };

  grid.rows = [
    { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, email: 'alice@example.com', startDate: '2020-03-15' },
    { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, email: 'bob@example.com', startDate: '2019-07-22' },
    { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, email: 'carol@example.com', startDate: '2018-11-01' },
    { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, email: 'dan@example.com', startDate: '2021-01-10' },
    { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, email: 'eve@example.com', startDate: '2022-05-03' },
    { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, email: 'frank@example.com', startDate: '2020-08-20' },
  ];

  const updateStatus = () => {
    if (!status) return;
    const plugin = grid.getPluginByName('responsive');
    const bp = plugin?.getActiveBreakpoint();
    const isCard = plugin?.isResponsive();
    if (!bp) {
      status.textContent = '📊 Full Table (no columns hidden)';
    } else if (isCard) {
      status.textContent = `📱 Card Layout (≤${bp.maxWidth}px)`;
    } else {
      const hidden = bp.hiddenColumns?.length ?? 0;
      status.textContent = `📊 Table - ${hidden} column(s) hidden (≤${bp.maxWidth}px)`;
    }
  };

  grid.on('responsive-change', updateStatus);
  requestAnimationFrame(updateStatus);
}
```

Use multiple breakpoints to progressively hide columns before switching to card layout.

### Value-Only Columns

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

const container = document.getElementById('responsive-value-only-columns-demo');
if (container) {
  const grid = queryGrid('tbw-grid', container);
  if (!grid) throw new Error('Grid not found');

  grid.gridConfig = {
    columns: [
      { field: 'id', header: 'ID', type: 'number', width: 60 },
      { field: 'name', header: 'Name', width: 150 },
      { field: 'department', header: 'Department', width: 120 },
      { field: 'salary', header: 'Salary', type: 'number', width: 100 },
      { field: 'email', header: 'Email', width: 200 },
      { field: 'startDate', header: 'Start Date', width: 120 },
    ],
    features: {
      responsive: {
        breakpoint: 500,
        hiddenColumns: ['startDate', { field: 'email', showValue: true }],
      },
    },
  };

  grid.rows = [
    { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000, email: 'alice@example.com', startDate: '2020-03-15' },
    { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000, email: 'bob@example.com', startDate: '2019-07-22' },
    { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000, email: 'carol@example.com', startDate: '2018-11-01' },
    { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000, email: 'dan@example.com', startDate: '2021-01-10' },
    { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000, email: 'eve@example.com', startDate: '2022-05-03' },
    { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000, email: 'frank@example.com', startDate: '2020-08-20' },
  ];
}
```

Use enhanced `hiddenColumns` syntax to show values without labels.

### Animated Transitions

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

const container = document.getElementById('responsive-animated-transitions-demo');
if (container) {
  const grid = queryGrid('tbw-grid', container);
  const status = container.querySelector('.responsive-status');
  if (!grid) throw new Error('Grid not found');

  grid.gridConfig = {
    columns: [
      { field: 'id', header: 'ID', type: 'number', width: 60 },
      { field: 'name', header: 'Name', width: 150 },
      { field: 'department', header: 'Department', width: 120 },
      { field: 'salary', header: 'Salary', type: 'number', width: 100 },
    ],
    features: {
      responsive: {
        breakpoint: 500,
        animate: true,
        animationDuration: 300,
      },
    },
  };

  grid.rows = [
    { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 95000 },
    { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 75000 },
    { id: 3, name: 'Carol Williams', department: 'Engineering', salary: 105000 },
    { id: 4, name: 'Dan Brown', department: 'Sales', salary: 85000 },
    { id: 5, name: 'Eve Davis', department: 'Marketing', salary: 72000 },
    { id: 6, name: 'Frank Miller', department: 'Engineering', salary: 98000 },
  ];

  grid.on('responsive-change', ({ isResponsive }) => {
    if (status) status.textContent = isResponsive ? '📱 Card mode - watch the fade-in animation!' : '📊 Table mode';
  });
}
```

Smooth animations when transitioning between modes (enabled by default).

## Light DOM Configuration

The ResponsivePlugin supports declarative configuration via the `<tbw-grid-responsive-card>` element. Framework adapters provide wrapper components with idiomatic rendering patterns.

#### TypeScript

```html
<tbw-grid>
  <tbw-grid-responsive-card
    breakpoint="500"
    card-row-height="80"
    hidden-columns="createdAt, updatedAt"
    hide-header="true">
    <div class="custom-card">
      <strong>{{ row.name }}</strong>
      <span>{{ row.email }}</span>
    </div>
  </tbw-grid-responsive-card>
</tbw-grid>
```

In vanilla JS, the innerHTML of the element is used as a template. Use `{{ row.fieldName }}` syntax for value interpolation.

#### React

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

<DataGrid rows={data} columns={columns} responsive={{ breakpoint: 500 }}>
  <GridResponsiveCard<Employee> cardRowHeight={80}>
    {({ row, index }) => (
      <div className="custom-card">
        <strong>{row.name}</strong>
        <span>{row.email}</span>
      </div>
    )}
  </GridResponsiveCard>
</DataGrid>
```

React uses a render function as children, receiving `{ row, index }`.

#### Vue

```html
<script setup>
import { TbwGrid, TbwGridResponsiveCard } from '@toolbox-web/grid-vue';
</script>

<template>
  <TbwGrid :rows="data" :columns="columns" :responsive="{ breakpoint: 500 }">
    <TbwGridResponsiveCard>
      <template #default="{ row, rowIndex }">
        <div class="custom-card">
          <strong>{{ row.name }}</strong>
          <span>{{ row.email }}</span>
        </div>
      </template>
    </TbwGridResponsiveCard>
  </TbwGrid>
</template>
```

Vue uses a scoped slot, receiving `{ row, rowIndex }`.

#### Angular

```html
<tbw-grid [rows]="data" [columns]="columns" [responsive]="{ breakpoint: 500 }">
  <tbw-grid-responsive-card>
    <ng-template let-row let-index="index">
      <div class="custom-card">
        <strong>{{ row.name }}</strong>
        <span>{{ row.email }}</span>
      </div>
    </ng-template>
  </tbw-grid-responsive-card>
</tbw-grid>
```

Angular uses `<ng-template>` with implicit context binding (`let-row`, `let-index="index"`). Import `GridResponsiveCard` from `@toolbox-web/grid-angular`.

### Vanilla Attributes

| Attribute | Type | Description |
| --------- | ---- | ----------- |
| `breakpoint` | `number` | Width threshold in pixels for responsive mode |
| `card-row-height` | `number \| 'auto'` | Card height in pixels or auto |
| `hidden-columns` | `string` | Comma-separated field names to hide |
| `hide-header` | `'true' \| 'false'` | Hide the per-card field label (e.g. `Name:`). Default `'false'`. Does not affect the column header row, which is always hidden in card mode. |
| `debounce-ms` | `number` | Resize debounce delay in ms |

:::note
Light DOM attributes override constructor config when both are present.
:::

## Configuration Options

See [`ResponsivePluginConfig`](./Interfaces/ResponsivePluginConfig/) for the full list of options and defaults.

### BreakpointConfig

For progressive degradation, use the `breakpoints` array instead of a single `breakpoint`. See [`BreakpointConfig`](./Interfaces/BreakpointConfig/) for the full interface.

### HiddenColumnConfig

The enhanced `hiddenColumns` syntax supports both simple strings and objects. See [`HiddenColumnConfig`](./Types/HiddenColumnConfig/) for the full type definition.

**Example:**
```ts
hiddenColumns: [
  'startDate',                        // Fully hidden
  { field: 'email', showValue: true }, // Value shown without label
]
```

### Choosing a Breakpoint

The breakpoint should be based on your grid's column count and content:

| Grid Size | Suggested Breakpoint |
| --------- | -------------------- |
| 3-5 columns | 400-500px |
| 6-10 columns | 600-800px |
| 10+ columns | 900-1200px |

:::note
If no breakpoint is configured (or set to 0), a warning is logged and responsive mode is effectively disabled.
:::

## Events

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

const container = document.getElementById('responsive-events-demo');
if (container) {
  const grid = queryGrid('tbw-grid', container);

  grid.gridConfig = {
    columns: [
      { field: 'name', header: 'Name' },
      { field: 'department', header: 'Department' },
      { field: 'salary', header: 'Salary', type: 'number', align: 'right' },
    ],
    features: {
      responsive: { breakpoint: 600 },
    },
  };

  grid.rows = [
    { name: 'Alice Johnson', department: 'Engineering', salary: 95000 },
    { name: 'Bob Smith', department: 'Marketing', salary: 75000 },
    { name: 'Carol White', department: 'Sales', salary: 68000 },
    { name: 'David Brown', department: 'Engineering', salary: 92000 },
    { name: 'Eve Davis', department: 'HR', salary: 65000 },
  ];

  const log = container.querySelector('#responsive-events-log');
  const clearBtn = container.querySelector('#clear-responsive-events-log');

  function addLog(type: string, detail: string) {
    if (!log) return;
    const msg = document.createElement('div');
    msg.innerHTML = `<span class="event-type">[${type}]</span> ${detail}`;
    log.insertBefore(msg, log.firstChild);
    while (log.children.length > 15) log.lastChild?.remove();
  }

  clearBtn?.addEventListener('click', () => { if (log) log.innerHTML = ''; });

  grid.on('responsive-change', (d) => {
    addLog('responsive-change', `mode: ${d.mode}, width: ${d.width}px`);
  });
}
```

### responsive-change

Emitted when the grid crosses the breakpoint threshold:

```ts
grid.on('responsive-change', ({ isResponsive, width, breakpoint }) => {
  console.log(isResponsive ? 'Card mode' : 'Table mode');
  console.log(`Width: ${width}px, Breakpoint: ${breakpoint}px`);
});
```

## Programmatic API

Control responsive mode programmatically via the plugin instance:

```ts
// Get the plugin instance from the grid
const plugin = grid.getPluginByName('responsive');

// Check current mode
const isCardMode = plugin.isResponsive();

// Force responsive mode (regardless of width)
plugin.setResponsive(true);
plugin.setResponsive(false);

// Update breakpoint dynamically
plugin.setBreakpoint(600);

// Get current grid width
const width = plugin.getWidth();

// Get active breakpoint (multi-breakpoint mode)
const activeBreakpoint = plugin.getActiveBreakpoint();
// Returns: { maxWidth: 600, hiddenColumns: [...], cardLayout: false } or null
```

## How It Works

1. **ResizeObserver** monitors the grid element's width
2. When `width < breakpoint`, the plugin adds `data-responsive` attribute to the grid
3. **CSS transforms** cells from horizontal to vertical layout
4. Each cell displays "Header: Value" using the `::before` pseudo-element with `data-header` attribute

This CSS-only approach means:
- No DOM replacement or re-rendering needed
- Smooth transitions between modes
- Works with all other plugins (selection, editing, etc.)

## Styling

The responsive plugin uses the grid's existing CSS custom properties for theming.

### CSS Custom Properties

The responsive plugin uses the grid's built-in CSS variables:

| Property | Description |
| -------- | ----------- |
| `--tbw-cell-padding` | Padding inside card rows |
| `--tbw-color-border` | Card separator color |
| `--tbw-color-bg` | Card background color |
| `--tbw-color-row-alt` | Alternating card background |
| `--tbw-color-row-hover` | Card hover background |
| `--tbw-color-selection` | Selected card background |
| `--tbw-color-header-fg` | Label text color |
| `--tbw-color-accent` | Selection indicator color |

### Custom Card Styling

```css
/* Custom card styling */
tbw-grid[data-responsive] .data-grid-row {
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  margin-bottom: 8px;
}

/* Hide specific columns in card mode */
tbw-grid[data-responsive] .cell[data-field="createdAt"],
tbw-grid[data-responsive] .cell[data-field="updatedAt"] {
  display: none;
}
```

### Data Attributes

| Attribute | Element | Description |
| --------- | ------- | ----------- |
| `data-responsive` | `tbw-grid` | Present when in responsive (card) mode |
| `data-responsive-animate` | `tbw-grid` | Present when animations are enabled |
| `data-header` | `.cell` | Column header text for CSS `::before` |
| `data-responsive-hidden` | `.cell` | Marks cells hidden via `hiddenColumns` |
| `data-responsive-value-only` | `.cell` | Marks cells showing value only (no label) |

### CSS Custom Properties for Animation

| Property | Default | Description |
| -------- | ------- | ----------- |
| `--tbw-responsive-duration` | `200ms` | Animation duration for mode transitions |

## Keyboard Shortcuts

In responsive mode, the visual layout is inverted - cells are stacked vertically within each card. The plugin automatically adjusts keyboard navigation to match this layout:

| Key | Table Mode | Responsive Mode |
| --- | ---------- | --------------- |
| **↑** | Previous row | Previous field (within card), wraps to previous card |
| **↓** | Next row | Next field (within card), wraps to next card |
| **←** | Previous column | Previous card (same field) |
| **→** | Next column | Next card (same field) |
| **Tab** | Next cell, wraps to next row | Same behavior |
| **Enter** | Start editing | Same behavior |
| **Escape** | Cancel editing | Same behavior |

### Custom Card Renderers

When using `cardRenderer` (Phase 2), the grid's built-in keyboard navigation is disabled for arrow keys. Implementors should handle navigation within their custom card content via their own event handlers.

## Use Cases

### Split-Pane UI

```ts
// Grid in a resizable panel
features: {
  responsive: {
    breakpoint: 400,
    hiddenColumns: ['createdAt', 'updatedAt'], // Hide dates in card mode
  },
},
```

### Mobile-First Design

```ts
// Responsive grid for mobile/tablet
features: {
  responsive: {
    breakpoint: 768,
    hideHeader: true,
  },
},
```

### Dashboard Widget

```ts
// Small widget in dashboard
features: {
  responsive: {
    breakpoint: 300,
    hiddenColumns: ['email', 'phone', 'address'],
  },
},
```

### Progressive Degradation

```ts
// Gracefully degrade as container shrinks
features: {
  responsive: {
    breakpoints: [
      { maxWidth: 900, hiddenColumns: ['startDate'] },
      { maxWidth: 700, hiddenColumns: ['startDate', 'email'] },
      { maxWidth: 500, cardLayout: true },
    ],
  },
},
```

## Custom Card Renderer

For advanced card layouts with avatars, badges, or custom grouped fields, use the `cardRenderer` option:

```ts
features: {
  responsive: {
    breakpoint: 600,
    cardRenderer: (row, rowIndex) => {
      const card = document.createElement('div');
      card.className = 'employee-card';
      card.innerHTML = `
        <div class="avatar">${row.name[0]}</div>
        <div class="info">
          <div class="name">${row.name}</div>
          <div class="meta">${row.department} · $${row.salary.toLocaleString()}</div>
          <div class="email">${row.email}</div>
        </div>
      `;
      return card;
    },
  },
},
```

### cardRenderer Signature

```ts
cardRenderer: (row: T, rowIndex: number) => HTMLElement
```

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `row` | `T` | The row data object |
| `rowIndex` | `number` | Index of the row in the data array |
| **Returns** | `HTMLElement` | The element to render as the card content |

### cardRowHeight

When using `cardRenderer`, you can control card height:

```ts
// Fixed height (better for virtualization with large datasets)
features: {
  responsive: {
    breakpoint: 600,
    cardRowHeight: 80,  // 80px per card
    cardRenderer: (row) => { /* ... */ },
  },
},

// Auto height (default - cards size to content)
features: {
  responsive: {
    breakpoint: 600,
    cardRowHeight: 'auto',
    cardRenderer: (row) => { /* ... */ },
  },
},
```

### Keyboard Navigation with cardRenderer

When using a custom `cardRenderer`, the grid's built-in arrow key navigation is disabled. This allows you to implement your own navigation within the card content.

Standard keyboard shortcuts still work:
- **Tab** - Move between cards
- **Enter** - Triggers `cell-activate` event
- **Escape** - Standard escape handling

## See Also

- **[Selection](../selection/)** — Row and cell selection
- **[Theming](../../guides/theming/)** — CSS custom properties for styling
