# Data Table (`data-table`) - SignalOS widget

> TanStack-table wrapper with sorting, pagination, column toggle, row selection, and widget-owned loading/empty/error states.

- **Version:** 0.2.1
- **Kind:** widget · **Category:** data-display
- **Install:** `npx shadcn@latest add @signalos/data-table`
- **Registry dependencies (pulled automatically):** @signalos/tokens, @signalos/utils, @signalos/button, @signalos/dropdown-menu, @signalos/select, @signalos/skeleton
- **npm dependencies:** @tanstack/react-table@^8.21.3, lucide-react@^1.7.0
- **Files installed:** `src/components/widgets/data-table/DataTable.tsx`, `src/components/widgets/data-table/DataTable.types.ts`

## Access

This is a private registry: pulling source requires a `SIGNALOS_REGISTRY_TOKEN`
(GitHub fine-grained PAT with read access to the signal-widgets repo) and an
`@signalos` entry in components.json `"registries"`. Previews and this document are public.

## Usage

```tsx
import { DataTable } from "@/components/widgets/data-table/DataTable"
```

## Example

```tsx
// Example: three data-table variants from the same widget.
// This file is consumer-side code - copy it into a screen and adapt.
import { useState } from "react"
import type { ColumnDef, RowSelectionState } from "@tanstack/react-table"

import { DataTable } from "@/components/widgets/data-table/DataTable"
import { StatusBadge } from "@/components/widgets/status-badge/StatusBadge"
import type { Severity } from "@/components/widgets/status-badge/StatusBadge.types"

interface Signal {
  id: string
  title: string
  owner: string
  severity: Severity
}

const columns: ColumnDef<Signal, string>[] = [
  { accessorKey: "id", header: "ID" },
  { accessorKey: "title", header: "Title" },
  { accessorKey: "owner", header: "Owner" },
  {
    accessorKey: "severity",
    header: "Severity",
    // Screens map domain values → widget props right in the column def:
    cell: ({ row }) => (
      <StatusBadge severity={row.original.severity} appearance="chip" />
    ),
  },
]

const signals: Signal[] = [
  {
    id: "SIG-1042",
    title: "Latency spike on ingest queue",
    owner: "M. Chen",
    severity: "critical",
  },
  {
    id: "SIG-1041",
    title: "Duplicate decision records",
    owner: "A. Okafor",
    severity: "high",
  },
  {
    id: "SIG-1038",
    title: "Stale cache on summary tiles",
    owner: "R. Patel",
    severity: "medium",
  },
]

/** Variant 1 - server-paginated table with column toggle. */
export function PaginatedSignalsTable() {
  const [page, setPage] = useState(1)
  const [pageSize, setPageSize] = useState(10)

  return (
    <DataTable
      columns={columns}
      data={signals}
      showColumnToggle
      pagination={{
        page,
        pageSize,
        total: 128,
        totalPages: Math.ceil(128 / pageSize),
        onPageChange: setPage,
        onRowsPerPageChange: setPageSize,
      }}
      onRowClick={(signal) => console.info("open", signal.id)}
    />
  )
}

/** Variant 2 - selectable rows (bulk actions). */
export function SelectableSignalsTable() {
  const [selection, setSelection] = useState<RowSelectionState>({})

  return (
    <DataTable
      columns={columns}
      data={signals}
      rowSelection={selection}
      onRowSelectionChange={setSelection}
      maxHeight="320px"
    />
  )
}

/** Variant 3 - compact embedded list (no pagination, capped height). */
export function CompactSignalsTable() {
  return (
    <DataTable
      columns={columns}
      data={signals}
      maxHeight="240px"
      emptyMessage="Nothing routed to you."
    />
  )
}

export default function Example() {
  return (
    <div className="flex flex-col gap-8">
      <PaginatedSignalsTable />
      <CompactSignalsTable />
    </div>
  )
}
```

## Props

### `PaginationConfig`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `page` | `number` | yes | - | Current page (1-indexed). |
| `pageSize` | `number` | yes | - |  |
| `total` | `number` | yes | - | Total record count across all pages. |
| `totalPages` | `number` | yes | - |  |
| `pageSizeOptions` | `number[] \| undefined` | no | `[5, 10, 25, 50]` |  |
| `onPageChange` | `(page: number) => void` | yes | - |  |
| `onRowsPerPageChange` | `(pageSize: number) => void` | yes | - |  |

### `DataTableErrorConfig`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `message` | `string \| undefined` | no | `"Something went wrong."` | Error message shown in the table body. |
| `onRetry` | `(() => void) \| undefined` | no | - | When provided, renders a Retry button. |

### `DataTableProps`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `columns` | `ColumnDef<TData, TValue>[]` | yes | - |  |
| `data` | `TData[]` | yes | - |  |
| `maxHeight` | `string \| undefined` | no | `"calc(100vh - 400px)"` | Max height of the scrollable body. |
| `isLoading` | `boolean \| undefined` | no | - | Renders skeleton rows while true. Takes precedence over error/empty. |
| `error` | `DataTableErrorConfig \| null \| undefined` | no | - | Error state owned by the widget. Rendered when set and not loading. |
| `showColumnToggle` | `boolean \| undefined` | no | - | Shows the column visibility toggle. |
| `onRowClick` | `((row: TData) => void) \| undefined` | no | - |  |
| `emptyMessage` | `string \| undefined` | no | `"No results found."` |  |
| `pagination` | `PaginationConfig \| undefined` | no | - |  |
| `rowSelection` | `RowSelectionState \| undefined` | no | - |  |
| `onRowSelectionChange` | `OnChangeFn<RowSelectionState> \| undefined` | no | - |  |
| `className` | `string \| undefined` | no | - | Merged with `cn()` onto the root element. |
| `data-testid` | `string \| undefined` | no | `"data-table"` | Test identifier rendered as `data-testid` on the root element. Internal parts (`-empty`, `-error`, `-pagination`, etc.) derive their ids from this value. |

### `DataTableSkeletonProps`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `columns` | `number \| undefined` | no | `6` |  |
| `rows` | `number \| undefined` | no | `5` |  |
| `showPagination` | `boolean \| undefined` | no | `true` |  |
| `className` | `string \| undefined` | no | - | Merged with `cn()` onto the root element. |
| `data-testid` | `string \| undefined` | no | `"data-table-skeleton"` | Test identifier rendered as `data-testid` on the root element. |

## Changelog

## 0.2.1

- Fix the record count rendering as `1-10 of 42records`. JSX drops the newline
  between an expression and text on the following line, so a prettier line-wrap
  silently removed the space. The test only asserted the `data-total` attribute,
  not the visible string; it now asserts both.

# data-table

## 0.2.0

- Add `className` (merged onto the root element) and a `"data-testid"` prop
  (`@default "data-table"`); all internal ids (`-empty`, `-error`, `-retry`,
  `-column-toggle`, `-pagination`, `-page-size-trigger`, `-record-count`,
  `-prev-page`, `-next-page`, `-page-{n}`, `-sort-{id}`) now derive from it.
  Default behavior is unchanged for existing consumers.
- Sortable column headers now render as a real `<button>` (keyboard-reachable,
  Enter/Space toggles sort) instead of a plain `<div onClick>`, and the `<th>`
  now sets `aria-sort="ascending" | "descending" | "none"` from the column's
  sort state.
- Error state div now has `role="alert"`.

## 0.1.2

- Use semantic status tokens (text-text-error, bg-bg-warning, border-border-*, bg-status-*) instead of raw palette scales so client theme overrides restyle status colors.


## 0.1.1

- Content sync fix to bring source in line with published registry payload.


## 0.1.0

- Initial release, lifted near-verbatim from `signal-core-ui` `common/data-table.tsx`
  (all `data-testid`s, skeleton rows, pagination, column toggle, row selection preserved).
- Added a widget-owned **error state** (`error: { message?, onRetry? }`) with render
  precedence loading → error → rows → empty, plus `data-table-empty` /
  `data-table-error` / `data-table-retry` test ids.
- Prop interfaces extracted to `DataTable.types.ts`.
