A table puts a set of records into columns so they can be compared down a line rather than
read one record at a time. `Table` draws the element itself, a `<table>` with header
cells, a body, rows that can be chosen and rows that can be pressed, and takes its columns
as data: a header and a cell renderer per column.

What separates it from `DataTable` is the scope of the job. There is no toolbar, no search,
no paging and no sort state: `Table` renders the records it is given, in the order it is
given them, which is what makes it the primitive `DataTable` composes, the column shape is
the same `ColumnDef`, and every visual decision resolves through the same cascade.

```tsx demo="data-display/table-preview"
```

## Usage

```tsx
import { Table, type ColumnDef } from "@asheeui/web";

type Invoice = { id: string; client: string; amount: string };

const columns: ColumnDef<Invoice>[] = [
  { id: "number", header: "Invoice", cell: (row) => row.id },
  { id: "client", header: "Client", cell: (row) => row.client },
  { id: "amount", header: "Amount", cell: (row) => row.amount },
];

<Table data={invoices} columns={columns} rowKeyAccessor={(row) => row.id} />
```

A column is `{ id?, header, cell }`. `header` is a node rendered in the `<th>`, so it can
be text or a control; `cell` receives the row and returns the cell's content, which is how
one column renders a badge and the next a link. `id` is the column's key, without one the
key is the column's **position**, never its header, so a table whose columns can be reordered
should state `id` on every column it cares about.

`rowKeyAccessor` is the row's identity, and it is optional: without it a row is keyed by its
index, which is right for a list that never reorders and wrong for one that does. The same
key is what `selectedRowKey` is compared against and what `handleClick` receives as its
second argument.

## Selection and interactive rows

Two things decide whether a row can be pressed. `handleClick` and `handleDoubleClick` make
rows interactive on their own, and `isClickable` states it explicitly: `isClickable={false}`
with a handler attached leaves the rows inert, and the handler is then never called.

`selectedRowKey` is the selection, one key, compared with the row's key, and a matching
row is filled with the accent that `color` names. It is a single row rather than a set, and
the page owns it, because `Table` reports presses and changes nothing itself.

```tsx demo="data-display/table-selection"
```

## Naming and order

A table is a reading structure, so it can name itself. `caption` is rendered as the table's own
`<caption>`, placed below the rows; a table already named by the heading above it can point
`aria-labelledby` at that heading instead, which names the same table without repeating the words.

There is no sort state, here or on `DataTable`, because an order is a decision about the data and
the page already has one. What a column can do is *state* the order its rows are already in:
`sortDirection` puts `aria-sort` on that column's header cell, so a reader is told which column
ordered the rows and which way. A column that is not sorted states nothing at all, rather than
claiming to be unsorted.

```tsx demo="data-display/table-naming"
```

## Variants and the empty state

Four variants decide the surface. `grid` rules the columns apart; `striped` bands alternate
rows; `bordered` keeps the outer border and drops the rules between columns; `ghost` draws
no surface at all, no border, no radius, no shadow, and no rule under the last row. The
header row is sticky in every variant, so the column names stay in place while the page
scrolls.

`size` sets the cell padding and the type scale, in the same three steps the fields use.
`emptyMessage` is what a table with no rows shows: one cell spanning every column, centred
in a fixed-height block, so an empty result is a sentence rather than a bare border. It
takes a node, which is how an empty state component stands in it.

```tsx demo="data-display/table-variants"
```

## Props

```props Table
```

`size` (`md`) and `variant` (`grid`) are registered, so `components.table` states the
density and the treatment once for the application.

`color` is not registered, so it resolves from the application's `defaultColor` and then
`primary`; `radius` is not registered either, so it resolves from `defaultRadius` and then
`md`. An unconfigured application therefore draws a `grid` table at `md`, accented
`primary` and rounded `xs`, the framework's own corner. `full` is read as `xl` here: a
table cannot be a circle.

The four class-name overrides exist at two tiers, `components.table` for the application and
the prop for one table, and the instance's is applied last, so it wins, `gridClasses` included,
and it lands on the `<table>` itself, so a consumer's own width beats the default `w-full`.
`caption` names the table and is rendered as the `<caption>` the layout has always reserved.
`data`, `columns`, `rowKeyAccessor`, `handleClick`, `handleDoubleClick`, `selectedRowKey`,
`emptyMessage` and `isClickable` are instance-only: the records, their columns and the chosen row
are statements about one screen. A column's `sortDirection` states itself to assistive technology
as `aria-sort` on its header cell; ordering the rows is still the consumer's job.

## Accessibility

- The element is a real `<table>`: `<th scope="col">` in a `<thead>`, one `<td>` per column
  in a `<tbody>`. A reader hears a cell with its column's name, which is the whole reason
  the element is not a grid of divs.
- The header row is sticky, so the column names stay the same nodes while the page scrolls
  rather than being duplicated above a scrolled body.
- A chosen row carries `aria-current="true"` and the accent fill, so the choice is announced
  rather than only coloured. `aria-selected` is deliberately not used: on a plain `<table>` it is
  not part of a row's exposed state, because it belongs to a grid.
- An interactive row is focusable and takes <kbd>Enter</kbd> and <kbd>Space</kbd>, but it is
  announced as a row rather than as a control, and there is no keyboard path to
  `handleDoubleClick`. Whatever the double press does needs another route.
- The table can name itself: pass `caption`, or point `aria-labelledby` at a heading above it.
  Those attributes land on the `<table>` rather than on the scrolling wrapper, a name on the
  wrapper names nothing a reader navigates to. An empty table still announces the empty message,
  because it is rendered as a cell in the body.

## Related

```related pagination, badge, empty-state
```
