# Table

Các hàng dữ liệu so sánh theo cột: thành viên, hóa đơn, triển khai. Hàng chạy sát hai mép, có đường chia dưới mỗi hàng, chữ header là secondary, và hàng body nhận overlay hover trung tính. Cell chứa bất kỳ component nào: một Avatar và một tên, một Badge, một Checkbox để chọn hàng, một DropdownMenu các hành động của hàng. Header có thể sắp xếp, một TableToolbar phía trên và phía dưới chứa tìm kiếm, hành động, số lượng và Pagination, và TableEmpty nói khi không có hàng. TableSelection thêm việc chọn hàng: một checkbox mỗi hàng, một checkbox ở header theo các hàng đó, và một thanh đếm rồi nêu tên các hàng đã chọn.

**Phụ thuộc:** npm: 2 trực tiếp, 6 tổng; registry: 4 trực tiếp, 6 tổng.

## Cách dùng

### Khi nào dùng

- Các hàng dữ liệu người dùng so sánh theo cột: thành viên, hóa đơn, triển khai.
- Các số thẳng hàng xuống một cột, chẳng hạn số tiền hoặc số đếm.
- Các hàng người dùng chọn rồi tác động cùng lúc, chẳng hạn xóa vài thành viên, với TableSelection.

### Khi nào không dùng

- Một danh sách đơn, mỗi mục một hoặc hai dòng. Dùng một danh sách các hàng.
- Một lưới card, chẳng hạn dự án có ảnh. Dùng Card.
- Layout. Dùng class flex hoặc grid.

## Biến thể

### align

Đặt trên TableHead và TableCell. Vị trí nội dung trong cell. end cho số, center chỉ cho dấu trạng thái ngắn.

Giá trị: `start`, `center`, `end`. Mặc định: `start`.

### sortDirection

Đặt trên TableHead có thể sắp xếp. Cách table đang sắp xếp theo cột này; đặt aria-sort và caret.

Giá trị: `none`, `ascending`, `descending`. Mặc định: `none`.

## Ví dụ

### Hóa đơn

```tsx
<Table className="max-w-full [&_th]:whitespace-normal [&_th]:wrap-anywhere [&_td]:wrap-anywhere [&_th]:px-sm [&_td]:px-sm">
  <TableCaption>Hóa đơn cho năm 2026.</TableCaption>
  <TableHeader>
    <TableRow>
      <TableHead>Hóa đơn</TableHead>
      <TableHead>Trạng thái</TableHead>
      <TableHead numeric>Số tiền</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>INV-001</TableCell>
      <TableCell>
        <Badge variant="success">Đã thanh toán</Badge>
      </TableCell>
      <TableCell numeric>$250.00</TableCell>
    </TableRow>
    <TableRow>
      <TableCell>INV-002</TableCell>
      <TableCell>
        <Badge variant="warning">Đến hạn</Badge>
      </TableCell>
      <TableCell numeric>$1,200.00</TableCell>
    </TableRow>
  </TableBody>
  <TableFooter>
    <TableRow>
      <TableCell>Tổng</TableCell>
      <TableCell />
      <TableCell numeric>$1,450.00</TableCell>
    </TableRow>
  </TableFooter>
</Table>
```

### Thành viên

Hàng được chọn bằng TableSelection: checkbox header theo các hàng, và thanh phía trên đếm rồi nêu tên hàng đã chọn, cùng các hành động trên chúng. Tìm kiếm và một hành động phía trên table, tên có thể sắp xếp, một Avatar và một email trong một cell, một Badge, hành động của hàng ở cuối, số lượng cùng Pagination phía dưới.

```tsx
<TableSelection defaultValue={["ada"]} className="min-w-0 max-w-full">
  <TableSelectionBar>
    <Button variant="secondary" size="sm">Xuất</Button>
    <Button variant="destructive-soft" size="sm">Gỡ</Button>
  </TableSelectionBar>
  <TableToolbar>
    <Input aria-label="Tìm kiếm thành viên" placeholder="Tìm kiếm thành viên" leadingIcon={<IconSearch size={16} />} className="max-w-xs" />
    <TableActions>
      <Button>Mời</Button>
    </TableActions>
  </TableToolbar>
  <Table aria-label="Thành viên" className="max-w-full [&_th]:whitespace-normal [&_th]:wrap-anywhere [&_td]:wrap-anywhere [&_th]:px-sm [&_td]:px-sm">
    <TableHeader>
      <TableRow>
        <TableHead fit>
          <TableSelectAll aria-label="Chọn tất cả thành viên" />
        </TableHead>
        <TableHead sortDirection="ascending" onSort={sortBy}>Tên</TableHead>
        <TableHead>Vai trò</TableHead>
        <TableHead fit>
          <span className="sr-only">Thao tác</span>
        </TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      <TableRow>
        <TableCell fit>
          <TableRowSelect value="ada" label="Ada Lovelace" />
        </TableCell>
        <TableCell>
          <div className="flex items-center gap-sm min-w-0 max-w-full flex-wrap">
            <Avatar alt="" color="blue">AL</Avatar>
            <div className="flex flex-col min-w-0 max-w-full flex-wrap">
              <span className="font-medium">Ada Lovelace</span>
              <span className="text-sm text-text-secondary">ada@example.com</span>
            </div>
          </div>
        </TableCell>
        <TableCell>
          <Badge>Chủ sở hữu</Badge>
        </TableCell>
        <TableCell fit>
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <Button variant="ghost" size="sm" iconOnly aria-label="Thao tác cho Ada Lovelace">
                <IconDots size={16} />
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end">
              <DropdownMenuItem>Đổi vai trò</DropdownMenuItem>
              <DropdownMenuItem destructive>Gỡ khỏi nhóm</DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </TableCell>
      </TableRow>
      <TableRow>
        <TableCell fit>
          <TableRowSelect value="grace" label="Grace Hopper" />
        </TableCell>
        <TableCell>
          <div className="flex items-center gap-sm min-w-0 max-w-full flex-wrap">
            <Avatar alt="" color="green">GH</Avatar>
            <div className="flex flex-col min-w-0 max-w-full flex-wrap">
              <span className="font-medium">Grace Hopper</span>
              <span className="text-sm text-text-secondary">grace@example.com</span>
            </div>
          </div>
        </TableCell>
        <TableCell>
          <Badge variant="outline">Biên tập</Badge>
        </TableCell>
        <TableCell fit>
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <Button variant="ghost" size="sm" iconOnly aria-label="Thao tác cho Grace Hopper">
                <IconDots size={16} />
              </Button>
            </DropdownMenuTrigger>
            <DropdownMenuContent align="end">
              <DropdownMenuItem>Đổi vai trò</DropdownMenuItem>
              <DropdownMenuItem destructive>Gỡ khỏi nhóm</DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </TableCell>
      </TableRow>
    </TableBody>
  </Table>
  <TableToolbar>
    <span className="text-sm text-text-secondary">2 of 24 members</span>
    <TableActions>
      <Pagination page={1} pageCount={12} onPageChange={goToPage} />
    </TableActions>
  </TableToolbar>
</TableSelection>
```

### Nhiều hàng được chọn

Quá ba tên thì thanh hiện phần còn lại dạng số. max trên TableSelectionBar đổi số tên được hiện.

```tsx
<TableSelection defaultValue={["ada", "grace", "alan", "katherine", "linus"]} className="min-w-0 max-w-full">
  <TableSelectionBar>
    <Button variant="destructive-soft" size="sm">Gỡ</Button>
  </TableSelectionBar>
  <Table aria-label="Thành viên" className="max-w-full [&_th]:whitespace-normal [&_th]:wrap-anywhere [&_td]:wrap-anywhere [&_th]:px-sm [&_td]:px-sm">
    <TableHeader>
      <TableRow>
        <TableHead fit>
          <TableSelectAll aria-label="Chọn tất cả thành viên" />
        </TableHead>
        <TableHead>Tên</TableHead>
        <TableHead>Vai trò</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      <TableRow>
        <TableCell fit>
          <TableRowSelect value="ada" label="Ada Lovelace" />
        </TableCell>
        <TableCell>Ada Lovelace</TableCell>
        <TableCell>Chủ sở hữu</TableCell>
      </TableRow>
      <TableRow>
        <TableCell fit>
          <TableRowSelect value="grace" label="Grace Hopper" />
        </TableCell>
        <TableCell>Grace Hopper</TableCell>
        <TableCell>Biên tập</TableCell>
      </TableRow>
      <TableRow>
        <TableCell fit>
          <TableRowSelect value="alan" label="Alan Turing" />
        </TableCell>
        <TableCell>Alan Turing</TableCell>
        <TableCell>Biên tập</TableCell>
      </TableRow>
      <TableRow>
        <TableCell fit>
          <TableRowSelect value="katherine" label="Katherine Johnson" />
        </TableCell>
        <TableCell>Katherine Johnson</TableCell>
        <TableCell>Người xem</TableCell>
      </TableRow>
      <TableRow>
        <TableCell fit>
          <TableRowSelect value="linus" label="Linus Torvalds" />
        </TableCell>
        <TableCell>Linus Torvalds</TableCell>
        <TableCell>Người xem</TableCell>
      </TableRow>
      <TableRow>
        <TableCell fit>
          <TableRowSelect value="margaret" label="Margaret Hamilton" />
        </TableCell>
        <TableCell>Margaret Hamilton</TableCell>
        <TableCell>Người xem</TableCell>
      </TableRow>
    </TableBody>
  </Table>
</TableSelection>
```

### Không có kết quả

```tsx
<Table aria-label="Thành viên" className="max-w-full [&_th]:whitespace-normal [&_th]:wrap-anywhere [&_td]:wrap-anywhere [&_th]:px-sm [&_td]:px-sm">
  <TableHeader>
    <TableRow>
      <TableHead>Tên</TableHead>
      <TableHead>Vai trò</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableEmpty colSpan={2}>Không có thành viên nào khớp với tìm kiếm của bạn.</TableEmpty>
  </TableBody>
</Table>
```

### Đang tải trang tiếp theo

Người dùng yêu cầu trang 2, trang này vẫn đang tải: trang 1 ở lại, mờ và được đánh dấu busy. Trong app, pending là isPending của transition đổi trang.

```tsx
<Table pending pendingLabel="Loading page 2" aria-label="Hóa đơn" className="max-w-full [&_th]:whitespace-normal [&_th]:wrap-anywhere [&_td]:wrap-anywhere [&_th]:px-sm [&_td]:px-sm">
  <TableHeader>
    <TableRow>
      <TableHead>Hóa đơn</TableHead>
      <TableHead numeric>Số tiền</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableCell>INV-001</TableCell>
      <TableCell numeric>$250.00</TableCell>
    </TableRow>
    <TableRow>
      <TableCell>INV-002</TableCell>
      <TableCell numeric>$150.00</TableCell>
    </TableRow>
  </TableBody>
</Table>
```

## Nên và không nên

### Nên

- Dùng numeric trên mọi cell của cột số, cả header.
- Đặt table trên một surface có mép riêng (một Card, một phần của trang); table không có mép.
- Đặt tên bằng caption, hoặc aria-label khi heading phía trên đã nói đó là gì.
- Đặt tìm kiếm và hành động chính trong một TableToolbar phía trên table, và số lượng cùng Pagination trong một toolbar phía dưới.
- Dùng fit cho cột Checkbox và cột hành động, để các cột dữ liệu lấy phần rộng.
- Với các hàng người dùng chọn rồi tác động cùng lúc, bọc table trong TableSelection; nó giữ checkbox header, các hàng và thanh khớp nhau.
- Đặt các hành động trên hàng đã chọn (Xuất, Xóa) trong TableSelectionBar, dạng button sm.
- Khi một trang, lần sắp xếp hoặc bộ lọc tải từ server, đổi nó trong một transition (useTransition) và truyền isPending làm pending: các hàng hiện tại ở lại đến khi hàng tiếp theo tới. Dùng hàng Skeleton chỉ cho lần tải đầu.

### Không nên

- Đừng bọc mỗi hàng trong một Card; hàng chính là layout.
- Đừng căn giữa cột chữ; căn start.
- Đừng dùng table cho layout hoặc cho một danh sách đơn.
- Đừng tự nối cell Checkbox khi TableSelection đã phù hợp; checkbox header và thanh sẽ lệch.
- Đừng làm trống table hoặc đổi hàng thành spinner khi trang tiếp theo đang tải; layout nhảy và người dùng mất vị trí.

## Props

| Tên | Kiểu | Giá trị | Mặc định | Mô tả |
| --- | --- | --- | --- | --- |
| `align` | `enum` | `start`, `center`, `end` | `start` | Đặt trên TableHead và TableCell. Vị trí nội dung trong cell. end cho số, center chỉ cho dấu trạng thái ngắn. |
| `numeric` | `boolean` |  | `false` | Đặt trên TableHead và TableCell. Cột số: căn end, mono, chữ số tabular, để các chữ số thẳng hàng. |
| `fit` | `boolean` |  | `false` | Đặt trên TableHead và TableCell. Cột chỉ chiếm chỗ nội dung cần: một Checkbox, các hành động của hàng. |
| `sortDirection` | `enum` | `none`, `ascending`, `descending` | `none` | Đặt trên TableHead có thể sắp xếp. Cách table đang sắp xếp theo cột này; đặt aria-sort và caret. |
| `onSort` | `function` |  |  | Đặt trên TableHead. Biến header thành button sắp xếp theo cột. Bạn tự sắp các hàng, rồi đặt sortDirection. |
| `selected` | `boolean` |  | `false` | Đặt trên TableRow. Hàng được chọn bằng Checkbox của nó: nền accent muted. |
| `colSpan` | `number` |  |  | Đặt trên TableEmpty. Số cột của table, để hàng trải hết các cột đó. |
| `defaultValue` | `string` |  |  | Đặt trên TableSelection. Value của các hàng được chọn ở lần render đầu, dạng mảng: ["ada"]. Mỗi value là value của một TableRowSelect. |
| `value` | `string` |  |  | Đặt trên TableSelection: các value đã chọn dạng mảng, khi bạn control chúng. Đi cùng onValueChange. TableRowSelect nhận một value, id của hàng đó. |
| `onValueChange` | `function` |  |  | Đặt trên TableSelection. Được gọi với mảng value đã chọn mới. |
| `label` | `string` |  |  | Đặt trên TableRowSelect, cùng value của nó. Tên của hàng: TableSelectionBar hiện tên đó, và nó đặt tên checkbox "Select <label>". |
| `max` | `number` |  | `3` | Đặt trên TableSelectionBar. Bao nhiêu tên hiện trước khi phần còn lại thành +N. |
| `pending` | `boolean` |  | `false` | Table: trang, lần sắp xếp hoặc bộ lọc tiếp theo vẫn đang tải. Đổi nó trong một transition (useTransition) để các hàng hiện tại ở lại, và truyền isPending: các hàng mờ xuống opacity-pending và table được đánh dấu aria-busy thay vì để trống. |
| `pendingLabel` | `string` |  | `Loading` | Table: trình đọc màn hình đọc trong một status polite khi pending. |

### Slot

| Tên | Bắt buộc | Mô tả |
| --- | --- | --- |
| `toolbar` | không | TableToolbar phía trên table (tìm kiếm, bộ lọc, TableActions với các button) và một cái phía dưới (số lượng, TableActions với Pagination). |
| `header` | không | TableHeader với một TableRow gồm các cell TableHead. |
| `body` | có | TableBody với một TableRow cho mỗi mục. |
| `caption` | không | TableCaption, hoặc aria-label trên Table. |
| `cells` | không | Bất kỳ component nào trong TableCell: Avatar kèm tên, Badge, Checkbox trong cell fit, DropdownMenu các hành động của hàng trong cell fit ở cuối. |
| `empty` | không | TableEmpty trong TableBody khi không có hàng. |
| `selection` | không | TableSelection bao quanh thanh, các toolbar và table, với TableSelectAll trong cell fit đầu của header, một TableRowSelect trong cell fit của mỗi hàng, và TableSelectionBar phía trên toolbar. Children của thanh là hành động của app trên các hàng đã chọn (Xuất, Xóa). |

## Cấu tạo

| Phần | Mô tả |
| --- | --- |
| `root` | Table: một table rộng hết chiều ngang, chữ primary 14px, trong một container cuộn ngang. |
| `header` | TableHeader: các hàng header. Chúng không nổi khi hover. |
| `row` | TableRow: một hàng có đường chia bên dưới và overlay hover trung tính. Hàng body cuối không có đường chia. selected phủ nền accent muted và màu chữ đậm đi kèm. |
| `head` | TableHead: một cell header, cao bằng control md, chữ secondary medium 14px trên một dòng. |
| `cell` | TableCell: một cell dữ liệu, px-md py-sm, chứa chữ hoặc bất kỳ component nào. align đặt start, center hoặc end; numeric đặt mono, chữ số tabular và end; fit thu cột về đúng nội dung. |
| `footer` | TableFooter: dòng tổng trên nền sunken, chữ medium, đường chia phía trên. |
| `caption` | TableCaption: tên của table, bên dưới table, chữ secondary 14px. |
| `sort` | Khi có onSort: một button trong cell header với chữ của header và một caret, chữ primary khi hover, vùng nhấn size-target-min. |
| `icon` | Caret sắp xếp: Tabler ChevronUpDown, CaretUp hoặc CaretDown, 16px, secondary. |
| `empty` | TableEmpty: một hàng trải colSpan cột, chữ secondary 14px, căn giữa, py-2xl. |
| `toolbar` | TableToolbar: một hàng phía trên hoặc phía dưới table, gap-sm, tự xuống dòng: một Input tìm kiếm, bộ lọc, số kết quả. |
| `actions` | TableActions: cuối một toolbar, gap-sm: các button tác động lên table, hoặc Pagination. |
| `selection` | TableSelection: một cột (gap-md) bao quanh thanh, các toolbar và table. Giữ các hàng đã chọn và thông báo số lượng trong một vùng status. |
| `selectAll` | TableSelectAll: Checkbox trong cell fit của header. Bật khi mọi hàng trên màn hình được chọn, bật một phần khi chỉ một số hàng được chọn. |
| `rowSelect` | TableRowSelect: Checkbox trong cell fit của hàng, tên "Select <label>". Checkbox đã check tô hàng bằng accent muted. |
| `selectionBar` | TableSelectionBar: chỉ hiện khi có hàng được chọn. Số lượng, tối đa max tên rồi +N, Select all, hành động của app, Clear selection. Viền, radius-container, nền surface. |
| `body` | Các hàng body. Khi pending, chúng ở lại trên màn hình và mờ xuống opacity-pending (duration-medium); container được đánh dấu aria-busy, với một status polite bên cạnh. |

## Khả năng truy cập

Role: `table`

| Phím | Hành động |
| --- | --- |
| `Tab` | Chuyển qua các button sắp xếp trong header và các control bên trong cell; bản thân table không focus được. |
| `Enter, Space` | Trên header có thể sắp xếp, sắp xếp theo cột đó. |

- Một table gốc: trình đọc màn hình đọc mỗi cell cùng header cột. TableHead đặt scope="col" theo mặc định; đặt scope="row" trên cell header đặt tên cho hàng.
- Đặt tên table bằng TableCaption, hoặc aria-label hay aria-labelledby khi tên là heading phía trên.
- Table rộng cuộn ngang trong container thay vì làm rộng trang.
- Header có thể sắp xếp là một button bên trong cell header; cell mang aria-sort (ascending hoặc descending) chỉ cho cột mà table đang sắp xếp theo.
- Chọn hàng bằng Checkbox trong cell fit, đặt tên theo hàng (Select Ada Lovelace); một Checkbox header chọn mọi hàng. selected chỉ tô hàng: Checkbox mới báo cho trình đọc màn hình.
- Đặt tên cho button hành động chỉ có icon, có tên hàng trong đó: Actions for Ada Lovelace.
- TableEmpty là một hàng trải mọi cột, nên table vẫn có header khi không có gì khớp.
- TableSelectAll là checkbox mixed: aria-checked là mixed khi một số hàng được chọn. Nhấn lúc đó thì chọn phần còn lại; nhấn khi tất cả đã chọn thì xóa chúng.
- TableSelection giữ một vùng status đọc số lượng ("2 of 5 rows selected") mỗi khi số đó đổi.
- Select all và Clear selection biến mất sau khi nhấn, nên focus chuyển trước: tới Clear selection sau Select all, tới checkbox header sau Clear selection.

## Cài đặt

```bash
npx werewolf-ui add table
```

Chạy `npx werewolf-ui init` một lần trong app trước khi thêm component. Init cài token, thêm CSS import và luật cho agent. Add cài component này cùng các dependency và giữ nguyên theme của bạn. Xem [Cài đặt](https://www.werewolf-ui.dev/vi/docs/installation.md) để biết yêu cầu và cách cài qua registry.
