# Data Grid

Data Grid is one of the heaviest workhorse components in the system, so the showcase should cover both the simple presets and the more operational states. Start from a clear column model, then layer in selection, filters, pagination, expandable details, and virtualization only when the workflow needs them.

## Import
```ts
import { DataGridComponent, createDataGridConfig, DataGridColumnFactory } from 'ui';
```

## Basic preset
```ts
import { Component, computed } from '@angular/core';
import { createDataGridConfig, DataGridColumnFactory, DataGridComponent } from 'ui';
import { Observable, of } from 'rxjs';
import { QueryParams, QueryResult } from '@shared/api/models/query-params.model';

interface FileRow {
  id: string;
  name: string;
  type: string;
  modified: string;
}

@Component({
  selector: 'app-data-grid-basic-preset-demo',
  imports: [DataGridComponent],
  template: ` <ui-data-grid [config]="config()" /> `,
})
export class DataGridBasicPresetDemoComponent {
  private rows: FileRow[] = [
    { id: '1', name: 'Roadmap.docx', type: 'Word Document', modified: '2026-04-12' },
    { id: '2', name: 'Launch Plan.pptx', type: 'PowerPoint', modified: '2026-04-10' },
    { id: '3', name: 'Budget.xlsx', type: 'Excel', modified: '2026-04-07' },
    { id: '4', name: 'Notes.pdf', type: 'PDF Document', modified: '2026-04-03' },
  ];

  config = computed(() =>
    createDataGridConfig<FileRow>({
      columns: [
        DataGridColumnFactory.text('name', 'Name', 'name'),
        DataGridColumnFactory.text('type', 'Type', 'type'),
        DataGridColumnFactory.text('modified', 'Modified', 'modified'),
      ],
      dataSource: this.createStaticDataSource(this.rows),
      styling: {
        size: 'medium',
        hoverable: true,
      },
    }),
  );

  private createStaticDataSource<T extends { id?: string }>(
    data: T[],
  ): (params: QueryParams<T>) => Observable<QueryResult<T>> {
    return () =>
      of({
        items: [...data],
        totalCount: data.length,
        hasNextPage: false,
        hasPreviousPage: false,
      });
  }
}
```

## Column factory
```ts
import { Component, computed } from '@angular/core';
import { createDataGridConfig, DataGridColumnFactory, DataGridComponent } from 'ui';
import { Observable, of } from 'rxjs';
import { QueryParams, QueryResult } from '@shared/api/models/query-params.model';

interface AssetRow {
  id: string;
  name: string;
  owner: string;
  modified: string;
  status: string;
  views: number;
}

@Component({
  selector: 'app-data-grid-column-factory-demo',
  imports: [DataGridComponent],
  template: ` <ui-data-grid [config]="config()" /> `,
})
export class DataGridColumnFactoryDemoComponent {
  private rows: AssetRow[] = [
    {
      id: '1',
      name: 'Homepage hero',
      owner: 'Ava Patel',
      modified: '2026-04-14',
      status: 'Approved',
      views: 182,
    },
    {
      id: '2',
      name: 'Pricing table',
      owner: 'Noah Kim',
      modified: '2026-04-12',
      status: 'Review',
      views: 73,
    },
    {
      id: '3',
      name: 'Retention report',
      owner: 'Mila Brooks',
      modified: '2026-04-09',
      status: 'Draft',
      views: 29,
    },
  ];

  config = computed(() =>
    createDataGridConfig<AssetRow>({
      columns: [
        DataGridColumnFactory.text('name', 'Name', 'name', { width: '220px' }),
        DataGridColumnFactory.text('owner', 'Owner', 'owner', { width: '180px' }),
        DataGridColumnFactory.number('views', 'Views', 'views', { width: '100px' }),
        DataGridColumnFactory.date('modified', 'Modified', 'modified', { width: '160px' }),
        DataGridColumnFactory.select(
          'status',
          'Status',
          'status',
          [
            { label: 'Approved', value: 'Approved' },
            { label: 'Review', value: 'Review' },
            { label: 'Draft', value: 'Draft' },
          ],
          { width: '140px' },
        ),
      ],
      dataSource: this.createStaticDataSource(this.rows),
      styling: {
        size: 'medium',
        hoverable: true,
      },
    }),
  );

  private createStaticDataSource<T extends { id?: string }>(
    data: T[],
  ): (params: QueryParams<T>) => Observable<QueryResult<T>> {
    return () =>
      of({
        items: [...data],
        totalCount: data.length,
        hasNextPage: false,
        hasPreviousPage: false,
      });
  }
}
```

## Selection
```ts
import { Component, computed, signal } from '@angular/core';
import { createDataGridConfig, DataGridColumnFactory, DataGridComponent, DataGridRow } from 'ui';
import { Observable, of } from 'rxjs';
import { QueryParams, QueryResult } from '@shared/api/models/query-params.model';

interface QueueRow {
  id: string;
  ticket: string;
  priority: string;
  owner: string;
}

@Component({
  selector: 'app-data-grid-selection-demo',
  imports: [DataGridComponent],
  template: `
    <ui-data-grid [config]="config()" />

    <div
      style="display:flex; flex-wrap:wrap; gap:12px; margin-top:16px; padding:12px 14px; border:1px dashed var(--color-neutral-stroke-2, #c8c6c4); border-radius:12px; background:var(--color-neutral-background-2, #f7f7f7);"
    >
      <div style="font-weight:600;">Selected rows</div>
      <div>{{ selectedCount() }}</div>
    </div>
  `,
})
export class DataGridSelectionDemoComponent {
  selectedCount = signal(0);

  private rows: QueueRow[] = [
    { id: '1', ticket: 'INC-2411', priority: 'High', owner: 'Mia Chen' },
    { id: '2', ticket: 'INC-2412', priority: 'Medium', owner: 'Luca Rossi' },
    { id: '3', ticket: 'INC-2413', priority: 'Low', owner: 'Sofia Reed' },
    { id: '4', ticket: 'INC-2414', priority: 'High', owner: 'Ethan Hall' },
  ];

  config = computed(() =>
    createDataGridConfig<QueueRow>({
      columns: [
        DataGridColumnFactory.text('ticket', 'Ticket', 'ticket'),
        DataGridColumnFactory.text('priority', 'Priority', 'priority'),
        DataGridColumnFactory.text('owner', 'Owner', 'owner'),
      ],
      dataSource: this.createStaticDataSource(this.rows),
      selection: 'multi',
      styling: {
        striped: true,
        hoverable: true,
      },
      callbacks: {
        onSelectionChange: (rows: DataGridRow<QueueRow>[]) => this.selectedCount.set(rows.length),
      },
    }),
  );

  private createStaticDataSource<T extends { id?: string }>(
    data: T[],
  ): (params: QueryParams<T>) => Observable<QueryResult<T>> {
    return () =>
      of({
        items: [...data],
        totalCount: data.length,
        hasNextPage: false,
        hasPreviousPage: false,
      });
  }
}
```

## Server-side features
```ts
import { Component, computed, signal } from '@angular/core';
import { createDataGridConfig, DataGridColumnFactory, DataGridComponent } from 'ui';
import { Observable, of } from 'rxjs';
import { QueryParams, QueryResult } from '@shared/api/models/query-params.model';

interface ApiRow {
  id: string;
  endpoint: string;
  region: string;
  latency: number;
}

function compareUnknownValues(aVal: unknown, bVal: unknown): number {
  if (typeof aVal === 'number' && typeof bVal === 'number') {
    return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
  }

  const aText = String(aVal ?? '').toLowerCase();
  const bText = String(bVal ?? '').toLowerCase();
  return aText < bText ? -1 : aText > bText ? 1 : 0;
}

@Component({
  selector: 'app-data-grid-server-side-features-demo',
  imports: [DataGridComponent],
  template: `
    <ui-data-grid [config]="config()" />

    <div
      style="display:flex; flex-wrap:wrap; gap:12px; margin-top:16px; padding:12px 14px; border:1px dashed var(--color-neutral-stroke-2, #c8c6c4); border-radius:12px; background:var(--color-neutral-background-2, #f7f7f7);"
    >
      <div style="font-weight:600;">Last callback</div>
      <div>{{ info() }}</div>
    </div>
  `,
})
export class DataGridServerSideFeaturesDemoComponent {
  info = signal('Waiting for pagination, sort, or filter changes.');

  private rows: ApiRow[] = [
    { id: '1', endpoint: '/users', region: 'EU', latency: 129 },
    { id: '2', endpoint: '/projects', region: 'US', latency: 212 },
    { id: '3', endpoint: '/billing', region: 'EU', latency: 164 },
    { id: '4', endpoint: '/events', region: 'APAC', latency: 248 },
    { id: '5', endpoint: '/search', region: 'US', latency: 186 },
    { id: '6', endpoint: '/health', region: 'EU', latency: 95 },
  ];

  config = computed(() =>
    createDataGridConfig<ApiRow>({
      columns: [
        DataGridColumnFactory.text('endpoint', 'Endpoint', 'endpoint', { sortable: true }),
        DataGridColumnFactory.select('region', 'Region', 'region', [
          { label: 'EU', value: 'EU' },
          { label: 'US', value: 'US' },
          { label: 'APAC', value: 'APAC' },
        ]),
        DataGridColumnFactory.number('latency', 'Latency', 'latency', { sortable: true }),
      ],
      dataSource: this.createStaticDataSource(this.rows),
      pagination: {
        enabled: true,
        pageSize: 2,
        pageSizeOptions: [2, 4, 6],
        showPageSizeSelector: true,
        showPageNumbers: true,
      },
      sorting: { enabled: true },
      filtering: { enabled: true, debounceMs: 250 },
      styling: {
        striped: true,
        stickyHeaders: true,
      },
      callbacks: {
        onPageChange: page => this.info.set(`Page changed to ${page}.`),
        onSortChange: sort => this.info.set(`Sorted by ${sort.field} (${sort.direction}).`),
      },
    }),
  );

  private createStaticDataSource<T extends { id?: string }>(
    data: T[],
  ): (params: QueryParams<T>) => Observable<QueryResult<T>> {
    return params => {
      let items: T[] = [...data];

      if (params.filters?.length) {
        items = items.filter(item =>
          params.filters!.every(filter => {
            const value = (item as Record<string, unknown>)[filter.columnName as string];
            const filterValue = filter.value;
            switch (filter.filterType) {
              case 'contains':
                return String(value ?? '')
                  .toLowerCase()
                  .includes(String(filterValue ?? '').toLowerCase());
              case 'equals':
                return value === filterValue;
              default:
                return true;
            }
          }),
        );
      }

      if (params.orders?.length) {
        const order = params.orders[0];
        items.sort((a, b) => {
          const aVal = (a as Record<string, unknown>)[order.columnName as string];
          const bVal = (b as Record<string, unknown>)[order.columnName as string];
          const comparison = compareUnknownValues(aVal, bVal);
          return order.order === 'asc' ? comparison : -comparison;
        });
      }

      const totalCount = items.length;
      if (params.page && params.pageSize) {
        const start = (params.page - 1) * params.pageSize;
        items = items.slice(start, start + params.pageSize);
      }

      return of({
        items,
        totalCount,
        hasNextPage: !!(
          params.page &&
          params.pageSize &&
          params.page * params.pageSize < totalCount
        ),
        hasPreviousPage: !!(params.page && params.page > 1),
      });
    };
  }
}
```

## Virtualization
```ts
import { Component, computed } from '@angular/core';
import { createDataGridConfig, DataGridColumnFactory, DataGridComponent } from 'ui';
import { Observable, of } from 'rxjs';
import { QueryParams, QueryResult } from '@shared/api/models/query-params.model';

interface ActivityRow {
  id: string;
  user: string;
  action: string;
  timestamp: string;
  target: string;
}

@Component({
  selector: 'app-data-grid-virtualization-demo',
  imports: [DataGridComponent],
  template: `
    <div
      style="display:flex;flex-direction:column;height:420px;min-height:0;width:100%;box-sizing:border-box;"
    >
      <ui-data-grid [config]="config()" />
    </div>

    <div style="margin-top:12px; color:var(--color-neutral-foreground-3, #605e5c);">
      Dataset size: {{ rows.length }} rows
    </div>
  `,
})
export class DataGridVirtualizationDemoComponent {
  rows: ActivityRow[] = Array.from({ length: 1000 }, (_, index) => ({
    id: `activity-${index + 1}`,
    user: `User ${index + 1}`,
    action: index % 2 === 0 ? 'Updated file' : 'Shared link',
    timestamp: `2026-04-${String((index % 28) + 1).padStart(2, '0')}`,
    target: `Record ${index + 1}`,
  }));

  config = computed(() =>
    createDataGridConfig<ActivityRow>({
      columns: [
        DataGridColumnFactory.text('user', 'User', 'user', { sortable: true }),
        DataGridColumnFactory.text('action', 'Action', 'action'),
        DataGridColumnFactory.text('target', 'Target', 'target'),
        DataGridColumnFactory.text('timestamp', 'Timestamp', 'timestamp', { sortable: true }),
      ],
      dataSource: this.createStaticDataSource(this.rows),
      pagination: { enabled: false },
      virtualization: {
        enabled: true,
        itemHeight: 48,
        bufferSize: 3,
      },
      styling: {
        hoverable: true,
        stickyHeaders: true,
      },
    }),
  );

  private createStaticDataSource<T extends { id?: string }>(
    data: T[],
  ): (params: QueryParams<T>) => Observable<QueryResult<T>> {
    return () =>
      of({
        items: [...data],
        totalCount: data.length,
        hasNextPage: false,
        hasPreviousPage: false,
      });
  }
}
```

## Filtering
```ts
import { Component, computed, signal } from '@angular/core';
import {
  createDataGridConfig,
  DataGridActiveFilter,
  DataGridColumnFactory,
  DataGridComponent,
} from 'ui';
import { Observable, of } from 'rxjs';
import { QueryParams, QueryResult } from '@shared/api/models/query-params.model';

interface FileRow {
  id: string;
  name: string;
  type: string;
  status: string;
  modified: string;
}

@Component({
  selector: 'app-data-grid-filtering-demo',
  imports: [DataGridComponent],
  template: `
    <ui-data-grid [config]="config()" />

    @if (activeFilters().length > 0) {
      <div
        style="display:flex; flex-wrap:wrap; gap:8px; margin-top:16px; padding:12px 14px; border:1px dashed var(--color-neutral-stroke-2, #c8c6c4); border-radius:12px; background:var(--color-neutral-background-2, #f7f7f7);"
      >
        @for (filter of activeFilters(); track filter.columnId) {
          <span
            style="display:inline-flex; align-items:center; padding:4px 8px; border-radius:999px; background:var(--color-brand-background-2, #ebf3fc); color:var(--color-brand-foreground-1, #0f6cbd);"
          >
            {{ filter.displayText }}
          </span>
        }
      </div>
    }
  `,
})
export class DataGridFilteringDemoComponent {
  activeFilters = signal<DataGridActiveFilter[]>([]);

  private rows: FileRow[] = [
    {
      id: '1',
      name: 'Roadmap.docx',
      type: 'Word Document',
      status: 'Active',
      modified: '2026-04-12',
    },
    { id: '2', name: 'Budget.xlsx', type: 'Excel', status: 'Draft', modified: '2026-04-10' },
    { id: '3', name: 'Deck.pptx', type: 'PowerPoint', status: 'Published', modified: '2026-04-08' },
    {
      id: '4',
      name: 'Notes.pdf',
      type: 'PDF Document',
      status: 'Archived',
      modified: '2026-04-04',
    },
    { id: '5', name: 'Metrics.xlsx', type: 'Excel', status: 'Active', modified: '2026-04-01' },
  ];

  config = computed(() =>
    createDataGridConfig<FileRow>({
      columns: [
        DataGridColumnFactory.text('name', 'Name', 'name'),
        DataGridColumnFactory.select('type', 'Type', 'type', [
          { label: 'Word Document', value: 'Word Document' },
          { label: 'Excel', value: 'Excel' },
          { label: 'PowerPoint', value: 'PowerPoint' },
          { label: 'PDF Document', value: 'PDF Document' },
        ]),
        DataGridColumnFactory.multiSelect('status', 'Status', 'status', [
          { label: 'Active', value: 'Active' },
          { label: 'Draft', value: 'Draft' },
          { label: 'Published', value: 'Published' },
          { label: 'Archived', value: 'Archived' },
        ]),
        DataGridColumnFactory.date('modified', 'Modified', 'modified'),
      ],
      dataSource: this.createStaticDataSource(this.rows),
      filtering: { enabled: true, debounceMs: 300 },
      styling: {
        striped: true,
      },
      callbacks: {
        onFilterChange: filters => this.activeFilters.set(filters),
      },
    }),
  );

  private createStaticDataSource<T extends { id?: string }>(
    data: T[],
  ): (params: QueryParams<T>) => Observable<QueryResult<T>> {
    return () =>
      of({
        items: [...data],
        totalCount: data.length,
        hasNextPage: false,
        hasPreviousPage: false,
      });
  }
}
```

## Expandable rows
```ts
import { Component, computed } from '@angular/core';
import { createDataGridConfig, DataGridColumnFactory, DataGridComponent } from 'ui';
import { Observable, of } from 'rxjs';
import { QueryParams, QueryResult } from '@shared/api/models/query-params.model';

interface ReleaseRow {
  id: string;
  feature: string;
  owner: string;
  status: string;
  summary: string;
}

@Component({
  selector: 'app-data-grid-expandable-rows-demo',
  imports: [DataGridComponent],
  template: `
    <ui-data-grid [config]="config()">
      <ng-template #rowDetailsTemplate let-row>
        <div style="display:grid; gap:10px;">
          <div style="display:grid; grid-template-columns:120px 1fr; gap:8px;">
            <strong>Owner</strong>
            <span>{{ row.data.owner }}</span>
          </div>
          <div style="display:grid; grid-template-columns:120px 1fr; gap:8px;">
            <strong>Status</strong>
            <span>{{ row.data.status }}</span>
          </div>
          <div style="display:grid; grid-template-columns:120px 1fr; gap:8px;">
            <strong>Summary</strong>
            <span>{{ row.data.summary }}</span>
          </div>
        </div>
      </ng-template>
    </ui-data-grid>
  `,
})
export class DataGridExpandableRowsDemoComponent {
  private rows: ReleaseRow[] = [
    {
      id: '1',
      feature: 'Approvals workflow',
      owner: 'Marta Lee',
      status: 'Ready for QA',
      summary: 'Adds staged approvals and audit history for finance requests.',
    },
    {
      id: '2',
      feature: 'Bulk invite',
      owner: 'Nolan Price',
      status: 'In progress',
      summary: 'Lets admins upload CSV files to invite and map new members.',
    },
    {
      id: '3',
      feature: 'Retention dashboard',
      owner: 'Iris Cole',
      status: 'Planned',
      summary: 'Introduces weekly churn snapshots and cohort trend cards.',
    },
  ];

  config = computed(() =>
    createDataGridConfig<ReleaseRow>({
      columns: [
        DataGridColumnFactory.text('feature', 'Feature', 'feature'),
        DataGridColumnFactory.text('owner', 'Owner', 'owner'),
        DataGridColumnFactory.text('status', 'Status', 'status'),
      ],
      dataSource: this.createStaticDataSource(this.rows),
      expandable: true,
      styling: {
        hoverable: true,
      },
    }),
  );

  private createStaticDataSource<T extends { id?: string }>(
    data: T[],
  ): (params: QueryParams<T>) => Observable<QueryResult<T>> {
    return () =>
      of({
        items: [...data],
        totalCount: data.length,
        hasNextPage: false,
        hasPreviousPage: false,
      });
  }
}
```

## Full featured
```ts
import { Component, computed } from '@angular/core';
import { createDataGridConfig, DataGridColumnFactory, DataGridComponent } from 'ui';
import { Observable, of } from 'rxjs';
import { QueryParams, QueryResult } from '@shared/api/models/query-params.model';

interface WorkspaceRow {
  id: string;
  title: string;
  area: string;
  owner: string;
  status: string;
  modified: string;
}

@Component({
  selector: 'app-data-grid-full-featured-demo',
  imports: [DataGridComponent],
  template: `
    <ui-data-grid [config]="config()">
      <ng-template #rowDetailsTemplate let-row>
        <div style="display:grid; gap:10px;">
          <div style="display:grid; grid-template-columns:120px 1fr; gap:8px;">
            <strong>Owner</strong>
            <span>{{ row.data.owner }}</span>
          </div>
          <div style="display:grid; grid-template-columns:120px 1fr; gap:8px;">
            <strong>Status</strong>
            <span>{{ row.data.status }}</span>
          </div>
          <div style="display:grid; grid-template-columns:120px 1fr; gap:8px;">
            <strong>Area</strong>
            <span>{{ row.data.area }}</span>
          </div>
        </div>
      </ng-template>
    </ui-data-grid>
  `,
})
export class DataGridFullFeaturedDemoComponent {
  private rows: WorkspaceRow[] = [
    {
      id: '1',
      title: 'Billing migration',
      area: 'Finance',
      owner: 'Ava Patel',
      status: 'Active',
      modified: '2026-04-12',
    },
    {
      id: '2',
      title: 'Access review',
      area: 'Security',
      owner: 'Owen Baker',
      status: 'Draft',
      modified: '2026-04-10',
    },
    {
      id: '3',
      title: 'Retention survey',
      area: 'Research',
      owner: 'Mia Chen',
      status: 'Published',
      modified: '2026-04-08',
    },
    {
      id: '4',
      title: 'Support macros',
      area: 'Operations',
      owner: 'Noah Kim',
      status: 'Active',
      modified: '2026-04-05',
    },
  ];

  config = computed(() =>
    createDataGridConfig<WorkspaceRow>({
      columns: [
        DataGridColumnFactory.text('title', 'Title', 'title', { sortable: true }),
        DataGridColumnFactory.text('area', 'Area', 'area', { sortable: true }),
        DataGridColumnFactory.text('owner', 'Owner', 'owner'),
        DataGridColumnFactory.text('modified', 'Modified', 'modified', { sortable: true }),
      ],
      dataSource: this.createStaticDataSource(this.rows),
      selection: 'multi',
      pagination: {
        enabled: true,
        pageSize: 5,
        pageSizeOptions: [5, 10, 20],
        showPageNumbers: true,
        showPageSizeSelector: true,
      },
      sorting: { enabled: true },
      filtering: { enabled: true, debounceMs: 250 },
      expandable: true,
      styling: {
        striped: true,
        bordered: true,
        stickyHeaders: true,
        hoverable: true,
      },
    }),
  );

  private createStaticDataSource<T extends { id?: string }>(
    data: T[],
  ): (params: QueryParams<T>) => Observable<QueryResult<T>> {
    return () =>
      of({
        items: [...data],
        totalCount: data.length,
        hasNextPage: false,
        hasPreviousPage: false,
      });
  }
}
```

## Advanced configuration
```ts
import { Component, computed, signal } from '@angular/core';
import {
  createDataGridConfig,
  DataGridActiveFilter,
  DataGridColumnFactory,
  DataGridComponent,
  DataGridRow,
} from 'ui';
import { Observable, of } from 'rxjs';
import { QueryParams, QueryResult } from '@shared/api/models/query-params.model';

interface FileRow {
  id: string;
  name: string;
  type: string;
  size: number;
  modified: string;
}

@Component({
  selector: 'app-data-grid-advanced-configuration-demo',
  imports: [DataGridComponent],
  template: `
    <ui-data-grid [config]="config()" />

    <div
      style="display:flex; flex-wrap:wrap; gap:12px; margin-top:16px; padding:12px 14px; border:1px dashed var(--color-neutral-stroke-2, #c8c6c4); border-radius:12px; background:var(--color-neutral-background-2, #f7f7f7);"
    >
      <div style="font-weight:600;">Event feedback</div>
      <div>{{ info() }}</div>
    </div>
  `,
})
export class DataGridAdvancedConfigurationDemoComponent {
  info = signal('Click rows, sort columns, or filter the grid.');

  private rows: FileRow[] = [
    { id: '1', name: 'Roadmap.docx', type: 'Word Document', size: 25, modified: '2026-04-12' },
    { id: '2', name: 'Metrics.xlsx', type: 'Excel', size: 14, modified: '2026-04-10' },
    { id: '3', name: 'Launch deck.pptx', type: 'PowerPoint', size: 58, modified: '2026-04-08' },
    { id: '4', name: 'Brief.pdf', type: 'PDF Document', size: 11, modified: '2026-04-06' },
  ];

  config = computed(() =>
    createDataGridConfig<FileRow>({
      columns: [
        DataGridColumnFactory.text('name', 'Name', 'name', { sortable: true }),
        DataGridColumnFactory.text('type', 'Type', 'type', { sortable: true }),
        DataGridColumnFactory.number('size', 'Size (MB)', 'size', { sortable: true }),
        DataGridColumnFactory.date('modified', 'Modified', 'modified', { sortable: true }),
      ],
      dataSource: this.createStaticDataSource(this.rows),
      selection: 'multi',
      pagination: {
        enabled: true,
        pageSize: 5,
        pageSizeOptions: [5, 10, 20],
        showPageNumbers: true,
        showPageSizeSelector: true,
        showInfo: true,
      },
      sorting: {
        enabled: true,
        defaultSort: { field: 'modified', direction: 'desc' },
      },
      filtering: { enabled: true, debounceMs: 250 },
      styling: {
        striped: true,
        bordered: true,
        stickyHeaders: true,
      },
      loading: {
        title: 'Loading documents...',
        description: 'Please wait',
      },
      empty: {
        title: 'No documents found',
        description: 'Try another filter set or upload new files.',
        icon: 'folder_open',
      },
      callbacks: {
        onRowClick: (row: DataGridRow<FileRow>) => this.info.set(`Clicked ${row.data.name}.`),
        onSortChange: sort => this.info.set(`Sorted by ${sort.field} (${sort.direction}).`),
        onFilterChange: (filters: DataGridActiveFilter[]) =>
          this.info.set(`Active filters: ${filters.length}.`),
        onSelectionChange: rows => this.info.set(`Selected rows: ${rows.length}.`),
      },
    }),
  );

  private createStaticDataSource<T extends { id?: string }>(
    data: T[],
  ): (params: QueryParams<T>) => Observable<QueryResult<T>> {
    return () =>
      of({
        items: [...data],
        totalCount: data.length,
        hasNextPage: false,
        hasPreviousPage: false,
      });
  }
}
```

## Accessibility

### Grid semantics
Treat Data Grid as a high-density interaction surface, not just a styled table. Headers, rows, selection controls, filter fields, expansion toggles, and pagination all contribute to the accessibility model.

| Area | Accessibility behavior |
| --- | --- |
| column headers | expose header semantics and optional sortable interaction |
| selection controls | rely on the checkbox or radio semantics used by the configured selection mode |
| filter inputs | inherit accessible behavior from the embedded field components |
| expandable rows | should expose a clear expand or collapse affordance and state |
| pagination and page size | inherit behavior from pagination and dropdown components |

### Keyboard
Keyboard behavior is a combination of normal interactive descendants rather than one custom keystroke model layered over the whole surface.

| Element | Keyboard behavior |
| --- | --- |
| sortable header actions | standard button-like activation behavior |
| filter controls | follow the embedded input, dropdown, or date control behavior |
| selection controls | follow checkbox or radio keyboard behavior |
| expandable toggles | follow standard button behavior |
| pagination controls | follow the shared pagination keyboard model |

### Usage guidance
Only enable the heavy features the workflow really needs. Dense tables become harder to scan and operate when selection, filtering, sticky headers, expansion, pagination, and virtualization are all turned on without a clear task behind them.
