first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
# DataTable
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - data-table.variants — `dataTableVariants`, `dataTab…
- [data-table.variants](./data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
- data-table.variants — `dataTableVariants`, `dataTab…
- [data-table.variants](./data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
- data-table.variants — `dataTableVariants`, `dataTab…
- [data-table.variants](./data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
- data-table.variants — `dataTableVariants`, `dataTab…
- [data-table.variants](./data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
- [data-table.variants](./data-table.variants.md) — `dataTableVariants`, `dataTablePaginationVariants`, and `dataTableSortHeaderVariants` drive the chassis `<DataTable>` o…
## Purpose
`<DataTable>` is the chassis headless data-table built on `@tanstack/react-table` (v8) and the chassis `<Table>` family. It is a **suaveplan-only addition** — shadcn does not ship `data-table` as a registry item upstream (per Wave 3a's reconciliation finding #7); shadcn's docs treat data-tables as recipes consumers build on top of the `<Table>` primitive. The chassis exposes `<DataTable>` as a discrete component so consumers do not hand-roll sorting / pagination / selection / empty-state treatment per surface.
The component is generic over `TData` (the row shape) and `TValue` (the column value union, defaults to `unknown`). Consumers build column definitions with the re-exported `createColumnHelper` from `@tanstack/react-table`. Every chassis-rendered string (no-results, pagination, rows-per-page, selection summary) flows through the active i18n via `t()` calls keyed on bundled `react-ui.dataTable.*` translation keys. The `selected` key uses ICU plural so the count agrees with the active locale's plural rules — Welsh's six-form scale is bundled.
## Features
- Built on TanStack Table v8 — the de-facto headless table for React.
- Sortable columns — click any sortable column header to toggle asc / desc / none; the header button shows a chevron icon indicating the active sort direction.
- Pagination — `pageSize` defaults to 10 rows; previous / next buttons disable at the boundaries.
- Row selection (opt-in via `enableRowSelection`) — chassis renders a selection summary using ICU plural translation.
- Empty state — when no rows match, the chassis renders the bundled `react-ui.dataTable.noResults` message in a single full-width row.
- Stable row keys via `getRowId` (recommended) — without it, TanStack uses array-index keys which collide on re-order.
- `data-state="selected"` on selected rows so chassis `<TableRow>` hover / selected styling activates automatically.
- Re-exports `createColumnHelper` so consumers don't need to add `@tanstack/react-table` as a direct dep.
## Usage: Basic Example
```tsx
import { createColumnHelper, DataTable } from "@suaveplan/react-ui";
interface Invoice {
readonly id: string;
readonly invoice: string;
readonly status: string;
readonly amount: number;
}
const columnHelper = createColumnHelper<Invoice>();
const COLUMNS = [
columnHelper.accessor("invoice", { header: "Invoice" }),
columnHelper.accessor("status", { header: "Status" }),
columnHelper.accessor("amount", {
header: "Amount",
cell: (info) => `$${info.getValue().toFixed(2)}`,
}),
];
export function InvoiceDataTable({ data }: { data: Invoice[] }) {
return (
<DataTable<Invoice>
columns={COLUMNS}
data={data}
getRowId={(row) => row.id}
/>
);
}
```
## Usage: Advanced Example
```tsx
import { createColumnHelper, DataTable } from "@suaveplan/react-ui";
interface User {
readonly id: string;
readonly name: string;
readonly email: string;
readonly role: "admin" | "member";
}
const columnHelper = createColumnHelper<User>();
const COLUMNS = [
columnHelper.display({
id: "select",
header: "",
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
aria-label="select-row"
/>
),
}),
columnHelper.accessor("name", { header: "Name", enableSorting: true }),
columnHelper.accessor("email", { header: "Email", enableSorting: false }),
columnHelper.accessor("role", { header: "Role", enableSorting: true }),
];
export function UserDataTable({ users }: { users: User[] }) {
return (
<DataTable<User>
columns={COLUMNS}
data={users}
getRowId={(row) => row.id}
enableRowSelection
pageSize={5}
initialSorting={[{ id: "name", desc: false }]}
/>
);
}
```
## API Reference
### `<DataTable<TData, TValue = unknown>>`
| Prop | Type | Default | Description |
|---|---|---|---|
| `columns` | `ReadonlyArray<ColumnDef<TData, TValue>>` | — | Column definitions; build with `createColumnHelper<TData>()`. |
| `data` | `ReadonlyArray<TData>` | — | The rows to render. Must be stable identity-wise (use `useMemo` on the consumer side). |
| `getRowId` | `(row: TData, index: number) => string` | _undefined_ | Stable row id for selection / keying. Recommended. |
| `enableRowSelection` | `boolean` | `false` | When true, renders a selection summary using ICU plural translation. |
| `pageSize` | `number` | `10` | Rows per page. |
| `initialSorting` | `SortingState` | `[]` | Initial sort state. |
| `noResultsKey` | `TranslationKey` | `react-ui.dataTable.noResults` | Empty-state message. |
| `rowsPerPageKey` | `TranslationKey` | `react-ui.dataTable.rowsPerPage` | Pagination footer label. |
| `selectedKey` | `TranslationKey` | `react-ui.dataTable.selected` | ICU-plural selection summary; receives `{ count }`. |
| `previousKey` | `TranslationKey` | `react-ui.dataTable.previous` | Previous-page button label. |
| `nextKey` | `TranslationKey` | `react-ui.dataTable.next` | Next-page button label. |
### `createColumnHelper<TData>()`
Re-exported from `@tanstack/react-table`. Returns a builder with `accessor`, `display`, and `group` methods for typed column definitions.
#### Errors
The DataTable component calls `useTranslation()` to resolve the chassis-rendered translation keys. It MUST render inside a `<TranslationProvider>` / `<ReactUiProvider>`; otherwise `useTranslation` throws.
## Implementation Notes
- The component is generic over `TData` and `TValue` so consumers get full type-safety on column definitions and cell renderers. The cast to `ColumnDef<TData, TValue>[]` inside `useReactTable` is a TanStack ergonomics workaround — TanStack's `data` accepts `TData[]` but its `columns` slot accepts a value-erased shape, so the chassis re-asserts the typed shape at the boundary.
- The `selected` plural translation includes the Welsh six-form scale (`zero | one | two | few | many | other`) so the count agrees with the active locale's plural rules. Other locales (`en-GB`, `zh-CN`, `ar-AE`, `ru-RU`) use their respective plural categories from the bundled JSON.
- `getRowId` is strongly recommended even when `data` items contain an `id` field — without a stable getter, TanStack uses array index keys, which collide when consumers reorder the data array. The chassis does NOT default `getRowId` to `(row) => row.id` because not every `TData` shape has an `id` field.
- The pagination controls are intentionally bare `<button>` elements with `border px-2 py-1` — this keeps the data-table self-contained without forcing a `<Button>` import. Consumers wanting fancier pagination compose their own footer instead of using the built-in one (read pagination state from `table.getState().pagination` and call `table.setPageIndex` / `table.setPageSize` directly).
- The sort indicator chevron icon is `aria-hidden="true"` because the column header text already conveys the sort target; screen-readers should not double-announce.
- Empty state spans `colSpan={columns.length}` so the no-results message centres across the full table width regardless of column count.