# 数据表格

> 支持筛选、选择、编辑与虚拟滚动的数据集表格。

A shadcn/ui block from the [@yiui](https://yiui.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/yiui/data-table-01).

- Page, with a live preview: https://shadcn.exchange/r/yiui/data-table-01
- Screenshot: https://shadcn.exchange/shot/yiui/data-table-01/light.png
- Registry item: https://yiui.dev/r/data-table-01.json
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/yiui/data-table-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@tanstack/react-table`, `@tanstack/react-virtual`, `lucide-react`, `motion`
- Registry items: `button`, `input`, `checkbox`, `skeleton`, `empty`, `sheet`, `dropdown-menu`, `tooltip`, `https://yiui.dev/r/grid-layout.json`, `https://yiui.dev/r/bar-field.json`, `https://yiui.dev/r/grid-pagination.json`, `https://yiui.dev/r/grid-toolbar.json`, `https://yiui.dev/r/segmented-control.json`, `alert-dialog`, `https://yiui.dev/r/bar-select.json`

## Files

### `src/registry/blocks/data-table-01/data-table.tsx`

```tsx
"use client"
import * as React from "react"
import {
  ArrowUpDown,
  ArrowUp,
  ArrowDown,
  Minus,
  Inbox,
  SearchX,
  AlertCircle,
  X,
} from "lucide-react"
import {
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getSortedRowModel,
  getPaginationRowModel,
  useReactTable,
  type VisibilityState,
} from "@tanstack/react-table"
import { useVirtualizer } from "@tanstack/react-virtual"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Sheet,
  SheetContent,
  SheetTitle,
  SheetDescription,
} from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
  Empty,
  EmptyTitle,
  EmptyMedia,
  EmptyHeader,
  EmptyDescription,
} from "@/components/ui/empty"
import { useControllableState } from "@/hooks/use-controllable-state"
import { DataTableCell } from "./data-table-cell"
import { DataTableToolbar } from "./data-table-toolbar"
import { DataTablePagination } from "./data-table-pagination"
import { DATA_TABLE_LABELS } from "./config"
import type { DataTableProps, DataTableMeta } from "./types"

/** 虚拟滚动模式下窄屏卡片列表每批显示的条数 */
const MOBILE_LIST_BATCH = 30

function DataTable<T>({
  apiRef,
  columns,
  data,
  getRowId,
  mode = "client",
  rowCount,
  loading,
  error,
  onRetry,
  sorting: sortingProp,
  defaultSorting = [],
  onSortingChange,
  pagination: paginationProp,
  defaultPagination = { pageIndex: 0, pageSize: 10 },
  onPaginationChange,
  globalFilter: filterProp,
  defaultGlobalFilter = "",
  onGlobalFilterChange,
  columnFilters: filtersProp,
  defaultColumnFilters = [],
  onColumnFiltersChange,
  rowSelection: selectionProp,
  defaultRowSelection = {},
  onRowSelectionChange,
  onSelectAll,
  bulkActions,
  rowActions,
  onCellCommit,
  searchLabel,
  virtualization = false,
  toolbar,
  header,
  counts,
  statusFilter,
  labels: overrides,
  className,
  ...props
}: DataTableProps<T>) {
  "use no memo"
  // Table v8 的稳定可变对象不能交由 React Compiler 缓存读取。
  const reducedMotion = useReducedMotion()
  const labels = { ...DATA_TABLE_LABELS, ...overrides }
  const [sorting, setSorting] = useControllableState({
    prop: sortingProp,
    defaultProp: defaultSorting,
    onChange: onSortingChange,
  })
  const [pagination, setPagination] = useControllableState({
    prop: paginationProp,
    defaultProp: defaultPagination,
    onChange: onPaginationChange,
  })
  const [globalFilter, setGlobalFilter] = useControllableState({
    prop: filterProp,
    defaultProp: defaultGlobalFilter,
    onChange: onGlobalFilterChange,
  })
  const [columnFilters, setColumnFilters] = useControllableState({
    prop: filtersProp,
    defaultProp: defaultColumnFilters,
    onChange: onColumnFiltersChange,
  })
  const [rowSelection, setRowSelection] = useControllableState({
    prop: selectionProp,
    defaultProp: defaultRowSelection,
    onChange: onRowSelectionChange,
  })
  const [visibility, setVisibility] = React.useState<VisibilityState>({})
  const [density, setDensity] = React.useState("52")
  // 虚拟滚动模式下，窄屏卡片列表分批显示：一次铺开上万行会让页面（含预渲染的 HTML）过大
  const [listLimit, setListLimit] = React.useState(MOBILE_LIST_BATCH)
  const [detailId, setDetailId] = React.useState<string | null>(null)
  const longPress = React.useRef<ReturnType<typeof setTimeout> | null>(null)
  const [scrolled, setScrolled] = React.useState(false)
  const [patches, setPatches] = React.useState<
    Record<string, Record<string, unknown>>
  >({})
  const scrollRef = React.useRef<HTMLDivElement>(null)
  const containerRef = React.useRef<HTMLDivElement>(null)
  React.useImperativeHandle(apiRef, () => ({
    startEditing(rowId, columnId) {
      if ((containerRef.current?.getBoundingClientRect().width ?? 0) < 640) {
        setDetailId(rowId)
        requestAnimationFrame(() =>
          requestAnimationFrame(() => {
            const cell = Array.from(
              document.querySelectorAll<HTMLElement>(
                '[data-slot="sheet-content"] [data-column]',
              ),
            ).find((node) => node.dataset.column === columnId)
            cell?.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }))
          }),
        )
        return
      }
      const row = Array.from(
        containerRef.current?.querySelectorAll<HTMLElement>("[data-row-id]") ??
          [],
      ).find((node) => node.dataset.rowId === rowId)
      const cell = Array.from(
        row?.querySelectorAll<HTMLElement>("[data-column]") ?? [],
      ).find((node) => node.dataset.column === columnId)
      // 菜单关闭并恢复焦点后再启动已有的单元格编辑流程。
      requestAnimationFrame(() =>
        cell?.dispatchEvent(new MouseEvent("dblclick", { bubbles: true })),
      )
    },
    openDetails: setDetailId,
  }))
  const [desktop, setDesktop] = React.useState(false)
  React.useEffect(() => {
    const query = window.matchMedia("(min-width: 640px)")
    const update = () => setDesktop(query.matches)
    update()
    query.addEventListener("change", update)
    return () => query.removeEventListener("change", update)
  }, [])
  const touchedVisibility = React.useRef(false)
  React.useEffect(() => {
    const observer = new ResizeObserver(([entry]) => {
      if (touchedVisibility.current) return
      setVisibility(
        Object.fromEntries(
          columns.map((column) => [
            column.id ??
              ("accessorKey" in column ? String(column.accessorKey) : ""),
            !(column.meta?.hideOnMobile && entry.contentRect.width < 640),
          ]),
        ),
      )
    })
    if (containerRef.current) observer.observe(containerRef.current)
    return () => observer.disconnect()
  }, [columns])
  const displayData = React.useMemo(
    () => data.map((row) => ({ ...row, ...patches[getRowId(row)] })),
    [data, patches, getRowId],
  )
  const sizedColumns = React.useMemo(
    () =>
      columns.map((column) => ({
        ...column,
        size: column.meta?.width ?? column.size,
      })),
    [columns],
  )
  // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Table v8 返回可变实例，组件已用 "use no memo" 退出编译器优化
  const table = useReactTable({
    data: displayData,
    columns: sizedColumns,
    getRowId,
    state: {
      sorting,
      pagination,
      globalFilter,
      columnFilters,
      rowSelection,
      columnVisibility: visibility,
    },
    onSortingChange: setSorting,
    onPaginationChange: setPagination,
    onGlobalFilterChange: setGlobalFilter,
    onColumnFiltersChange: setColumnFilters,
    onRowSelectionChange: setRowSelection,
    onColumnVisibilityChange: (updater) => {
      touchedVisibility.current = true
      setVisibility(updater)
    },
    getCoreRowModel: getCoreRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getPaginationRowModel: virtualization ? undefined : getPaginationRowModel(),
    manualSorting: mode === "manual",
    manualFiltering: mode === "manual",
    manualPagination: mode === "manual",
    rowCount: mode === "manual" ? rowCount : undefined,
    autoResetPageIndex: false,
    columnResizeMode: "onChange",
    defaultColumn: { size: 160, minSize: 80, maxSize: 600 },
  })
  const rows = table.getRowModel().rows
  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => scrollRef.current,
    estimateSize: () =>
      (virtualization && virtualization.estimateRowHeight) || Number(density),
    overscan: 8,
    enabled: !!virtualization,
  })
  const items = virtualizer.getVirtualItems()
  const shown = virtualization
    ? items.map((item) => ({ row: rows[item.index], index: item.index }))
    : rows.map((row, index) => ({ row, index }))
  const selected = Object.values(rowSelection).filter(Boolean).length
  const count =
    mode === "manual" && rowCount !== undefined
      ? rowCount
      : table.getFilteredRowModel().rows.length
  const span = table.getVisibleLeafColumns().length + 1 + Number(!!rowActions)
  const pinned = table
    .getVisibleLeafColumns()
    .find((column) => (column.columnDef.meta as DataTableMeta<T>)?.pinned)?.id
  async function commitCell(rowId: string, columnId: string, value: unknown) {
    const row = rows.find((row) => row.id === rowId)!
    const column = table.getColumn(columnId)!
    const key =
      "accessorKey" in column.columnDef
        ? String(column.columnDef.accessorKey)
        : columnId
    const previous =
      patches[rowId]?.[key] ?? (row.original as Record<string, unknown>)[key]
    setPatches((old) => ({ ...old, [rowId]: { ...old[rowId], [key]: value } }))
    try {
      await onCellCommit?.({ rowId, columnId, value, row: row.original })
    } catch (reason) {
      setPatches((old) => ({
        ...old,
        [rowId]: { ...old[rowId], [key]: previous },
      }))
      throw reason
    }
  }
  const detail = rows.find((row) => row.id === detailId)
  function clearFilters() {
    setGlobalFilter("")
    setColumnFilters([])
    table.setPageIndex(0)
  }
  return (
    <div
      ref={containerRef}
      data-slot="data-table"
      className={cn(
        "@container w-full min-w-0 bg-background text-foreground",
        className,
      )}
      {...props}
    >
      {header}
      <DataTableToolbar
        table={table}
        labels={labels}
        density={density}
        onDensityChange={setDensity}
        statusFilter={statusFilter}
        searchLabel={searchLabel}
        counts={
          mode === "manual"
            ? counts
            : statusFilter
              ? Object.fromEntries(
                  statusFilter.options.map((option) => [
                    option.value,
                    option.value
                      ? displayData.filter(
                          (row) =>
                            String(
                              (row as Record<string, unknown>)[
                                statusFilter.columnId
                              ],
                            ) === option.value,
                        ).length
                      : displayData.length,
                  ]),
                )
              : undefined
        }
      >
        {typeof toolbar === "function" ? toolbar(table) : toolbar}
      </DataTableToolbar>
      <div
        ref={scrollRef}
        data-slot="data-table-scroll"
        data-scrolled={scrolled}
        onScroll={(event) => setScrolled(event.currentTarget.scrollLeft > 0)}
        className="relative hidden w-full max-w-full min-w-0 overflow-auto border-t border-edge @min-[40rem]:block"
        style={{
          height: virtualization ? (virtualization.height ?? 480) : undefined,
          maxHeight: virtualization ? undefined : 600,
        }}
      >
        <table
          aria-label={labels.search}
          aria-rowcount={count + 1}
          // 最后一行不画虚线，底边由分页栏的实线收口，避免虚线 + 实线叠成两道
          className="table-fixed border-separate border-spacing-0 text-sm [&>tbody>tr:last-child>td]:border-b-0"
          style={
            {
              width: "100%",
              minWidth: table.getTotalSize() + 40 + (rowActions ? 64 : 0),
              "--data-table-row-height": density + "px",
            } as React.CSSProperties
          }
        >
          <colgroup>
            <col style={{ width: 40 }} />
            {table.getVisibleLeafColumns().map((column) => (
              <col
                key={column.id}
                style={{
                  width:
                    column.id === table.getVisibleLeafColumns()[0]?.id
                      ? undefined
                      : column.getSize(),
                }}
              />
            ))}
            {rowActions && <col style={{ width: 64 }} />}
          </colgroup>
          <thead className="sticky top-0 z-20 bg-muted/50 [&_th]:border-b [&_th]:border-edge">
            <tr>
              <th className="sticky left-0 z-30 w-12 bg-muted/50 px-3">
                <span className="relative flex size-4">
                  <Checkbox
                    className={
                      table.getIsSomePageRowsSelected()
                        ? "border-primary bg-primary dark:bg-primary [&_[data-slot=checkbox-indicator]]:invisible"
                        : undefined
                    }
                    aria-label={labels.selectPage}
                    checked={table.getIsAllPageRowsSelected()}
                    indeterminate={table.getIsSomePageRowsSelected()}
                    onCheckedChange={(value) =>
                      table.toggleAllPageRowsSelected(!!value)
                    }
                  />
                  {table.getIsSomePageRowsSelected() && (
                    <Minus
                      aria-hidden="true"
                      className="pointer-events-none absolute inset-0 size-4 text-primary-foreground"
                    />
                  )}
                </span>
              </th>
              {table.getHeaderGroups()[0]?.headers.map((header, i, all) => (
                <th
                  key={header.id}
                  aria-sort={
                    header.column.getIsSorted() === "asc"
                      ? "ascending"
                      : header.column.getIsSorted() === "desc"
                        ? "descending"
                        : "none"
                  }
                  className={cn(
                    "relative bg-muted/50 px-3 text-left",
                    // 与表体一致：操作列前的分隔线画在最后一个数据列的右边
                    rowActions &&
                      i === all.length - 1 &&
                      "border-r [border-right-style:dashed] border-edge",
                    header.id === pinned && "sticky left-10 z-30",
                    header.id === pinned &&
                      scrolled &&
                      "shadow-[4px_0_6px_-4px_var(--border)]",
                  )}
                  style={{
                    width: header.index === 0 ? undefined : header.getSize(),
                    minWidth: header.index === 0 ? undefined : header.getSize(),
                  }}
                >
                  <Button
                    variant="ghost"
                    className={cn(
                      "w-full justify-start px-0",
                      (header.column.columnDef.meta as DataTableMeta<T>)
                        ?.align === "right" && "justify-end",
                    )}
                    disabled={!header.column.getCanSort()}
                    onClick={header.column.getToggleSortingHandler()}
                  >
                    {flexRender(
                      header.column.columnDef.header,
                      header.getContext(),
                    )}{" "}
                    {header.column.getCanSort() &&
                      (header.column.getIsSorted() === "asc" ? (
                        <ArrowUp aria-hidden="true" className="size-3.5" />
                      ) : header.column.getIsSorted() === "desc" ? (
                        <ArrowDown aria-hidden="true" className="size-3.5" />
                      ) : (
                        <ArrowUpDown
                          aria-hidden="true"
                          className="size-3.5 text-muted-foreground"
                        />
                      ))}
                  </Button>
                  <div
                    role="separator"
                    aria-label={String(header.column.columnDef.header)}
                    aria-orientation="vertical"
                    aria-valuenow={header.getSize()}
                    aria-valuemin={80}
                    aria-valuemax={600}
                    tabIndex={0}
                    onMouseDown={header.getResizeHandler()}
                    onKeyDown={(event) => {
                      if (["ArrowLeft", "ArrowRight"].includes(event.key)) {
                        event.preventDefault()
                        table.setColumnSizing((old) => ({
                          ...old,
                          [header.id]: Math.max(
                            80,
                            Math.min(
                              600,
                              header.getSize() +
                                (event.key === "ArrowRight" ? 16 : -16),
                            ),
                          ),
                        }))
                      }
                    }}
                    className="absolute inset-y-0 right-0 w-2 cursor-col-resize select-none focus-visible:bg-primary focus-visible:outline-2 pointer-coarse:hidden"
                  />
                </th>
              ))}
              {rowActions && (
                <th className="w-16 bg-muted text-center text-xs font-medium">
                  {labels.actions}
                </th>
              )}
            </tr>
          </thead>
          <tbody>
            {loading ? (
              Array.from({ length: pagination.pageSize }, (_, i) => (
                <tr key={i}>
                  {Array.from({ length: span }, (_, j) => (
                    <td key={j} className="p-3">
                      {j === 0 ? (
                        <span className="font-mono text-muted-foreground">
                          –
                        </span>
                      ) : (
                        <Skeleton className="h-4 w-full" />
                      )}
                    </td>
                  ))}
                </tr>
              ))
            ) : error ? (
              <tr>
                <td colSpan={span}>
                  <Empty className="sticky left-0 min-h-60 w-[100cqw] max-w-full bg-dots">
                    <EmptyHeader>
                      <EmptyMedia variant="icon">
                        <AlertCircle aria-hidden="true" />
                      </EmptyMedia>
                      <EmptyTitle>{error}</EmptyTitle>
                      <EmptyDescription>
                        {labels.errorDescription}
                      </EmptyDescription>
                    </EmptyHeader>
                    {onRetry && (
                      <Button onClick={onRetry}>{labels.retry}</Button>
                    )}
                  </Empty>
                </td>
              </tr>
            ) : !rows.length ? (
              <tr>
                <td colSpan={span}>
                  <Empty className="sticky left-0 min-h-60 w-[100cqw] max-w-full bg-dots">
                    <EmptyHeader>
                      <EmptyMedia variant="icon">
                        {globalFilter || columnFilters.length ? (
                          <SearchX aria-hidden="true" />
                        ) : (
                          <Inbox aria-hidden="true" />
                        )}
                      </EmptyMedia>
                      <EmptyTitle>
                        {globalFilter || columnFilters.length
                          ? labels.noResults
                          : labels.empty}
                      </EmptyTitle>
                      <EmptyDescription>
                        {globalFilter || columnFilters.length
                          ? labels.noResultsDescription
                          : labels.emptyDescription}
                      </EmptyDescription>
                    </EmptyHeader>
                    {(globalFilter || columnFilters.length > 0) && (
                      <Button variant="outline" onClick={clearFilters}>
                        {labels.clearFilters}
                      </Button>
                    )}
                  </Empty>
                </td>
              </tr>
            ) : (
              <>
                {!!virtualization && items.length > 0 && (
                  <tr aria-hidden="true">
                    <td colSpan={span} style={{ height: items[0].start }} />
                  </tr>
                )}
                {shown.map(({ row, index }) => (
                  <tr
                    key={row.id}
                    data-row-id={row.id}
                    onKeyDown={(event) => {
                      if (
                        (event.shiftKey && event.key === "F10") ||
                        event.key === "ContextMenu"
                      ) {
                        const trigger =
                          event.currentTarget.querySelector<HTMLButtonElement>(
                            '[data-slot="data-table-row-actions"] button',
                          )
                        if (trigger) {
                          event.preventDefault()
                          trigger.click()
                        }
                      }
                    }}
                    aria-rowindex={
                      virtualization
                        ? index + 2
                        : pagination.pageIndex * pagination.pageSize + index + 2
                    }
                    className="group/row"
                    data-state={row.getIsSelected() ? "selected" : undefined}
                  >
                    <td
                      className={cn(
                        "group/number sticky left-0 z-10 w-10 border-r border-b border-dashed border-edge px-3",
                        row.getIsSelected()
                          ? "bg-primary/[0.04]"
                          : "bg-background",
                      )}
                    >
                      <span
                        aria-hidden="true"
                        className={cn(
                          "pointer-events-none absolute inset-0",
                          row.getIsSelected()
                            ? "border-l-2 border-primary bg-hatch"
                            : "group-focus-within/number:bg-dots group-hover/row:bg-dots",
                        )}
                      />
                      <span
                        aria-hidden="true"
                        className={cn(
                          "absolute inset-0 flex items-center justify-center font-mono text-[10px] text-muted-foreground group-focus-within/number:opacity-0 group-hover/row:opacity-0",
                          row.getIsSelected() && "opacity-0",
                        )}
                      >
                        {String(index + 1).padStart(2, "0")}
                      </span>
                      <Checkbox
                        className={cn(
                          "relative opacity-0 group-focus-within/number:opacity-100 group-hover/row:opacity-100",
                          row.getIsSelected() && "opacity-100",
                        )}
                        aria-label={labels.selectRow + " " + (index + 1)}
                        checked={row.getIsSelected()}
                        onCheckedChange={(value) => row.toggleSelected(!!value)}
                      />
                    </td>
                    {row.getVisibleCells().map((cell) => (
                      <td
                        key={cell.id}
                        style={{
                          height: Number(density),
                          width:
                            cell.column.id ===
                            table.getVisibleLeafColumns()[0]?.id
                              ? undefined
                              : cell.column.getSize(),
                          maxWidth:
                            cell.column.id ===
                            table.getVisibleLeafColumns()[0]?.id
                              ? undefined
                              : cell.column.getSize(),
                          textAlign: (
                            cell.column.columnDef.meta as DataTableMeta<T>
                          )?.align,
                        }}
                        className={cn(
                          "border-r border-b border-dashed border-edge px-3 py-0",
                          (cell.column.columnDef.meta as DataTableMeta<T>)
                            ?.align === "right" && "font-mono tabular-nums",
                          row.getIsSelected()
                            ? "bg-primary/[0.04]"
                            : "bg-background",
                          cell.column.id === pinned && "sticky left-10 z-10",
                          cell.column.id === pinned &&
                            scrolled &&
                            "shadow-[4px_0_6px_-4px_var(--border)]",
                        )}
                      >
                        <DataTableCell
                          value={cell.getValue()}
                          row={row.original}
                          meta={cell.column.columnDef.meta as DataTableMeta<T>}
                          title={String(cell.column.columnDef.header)}
                          labels={labels}
                          cellKey={cell.column.id}
                          rowHeight={Number(density)}
                          onCommit={(value) =>
                            commitCell(row.id, cell.column.id, value)
                          }
                        >
                          {flexRender(
                            cell.column.columnDef.cell,
                            cell.getContext(),
                          )}
                          {(cell.column.columnDef.meta as DataTableMeta<T>)
                            ?.bar && (
                            <span
                              aria-hidden="true"
                              className="mt-2 block h-0.5 bg-edge"
                            >
                              <span
                                className="block h-full bg-primary/70"
                                style={{
                                  width:
                                    Math.max(
                                      0,
                                      Math.min(
                                        100,
                                        (Number(cell.getValue()) /
                                          Math.max(
                                            1,
                                            (
                                              cell.column.columnDef
                                                .meta as DataTableMeta<T>
                                            )?.max ??
                                              Math.max(
                                                ...displayData.map(
                                                  (row) =>
                                                    Number(
                                                      (
                                                        row as Record<
                                                          string,
                                                          unknown
                                                        >
                                                      )[cell.column.id],
                                                    ) || 0,
                                                ),
                                              ),
                                          )) *
                                          100,
                                      ),
                                    ) + "%",
                                }}
                              />
                            </span>
                          )}
                        </DataTableCell>
                      </td>
                    ))}
                    {rowActions && (
                      <td className="w-16 border-b border-dashed border-edge text-center">
                        {/* 左侧分隔线由前一格的 border-r 提供（表头同理），这里不再画 */}
                        {/* 操作按钮常驻显示（淡色），悬停行时加深 */}
                        {rowActions(row.original)}
                      </td>
                    )}
                  </tr>
                ))}
                {/* 分页模式下最后一页不足一页时用空白补齐高度，翻页时分页条不会上下跳 */}
                {!virtualization &&
                  table.getPageCount() > 1 &&
                  rows.length < pagination.pageSize && (
                    <tr aria-hidden="true" data-slot="data-table-filler">
                      <td
                        colSpan={span}
                        style={{
                          height:
                            (pagination.pageSize - rows.length) *
                            Number(density),
                        }}
                      />
                    </tr>
                  )}
                {!!virtualization && items.length > 0 && (
                  <tr aria-hidden="true">
                    <td
                      colSpan={span}
                      style={{
                        height:
                          virtualizer.getTotalSize() -
                          items[items.length - 1].end,
                      }}
                    />
                  </tr>
                )}
              </>
            )}
          </tbody>
        </table>
      </div>
      <ul
        data-slot="data-table-list"
        aria-label={labels.search}
        className="border-t border-edge @min-[40rem]:hidden"
      >
        {loading ? (
          Array.from({ length: pagination.pageSize }, (_, index) => (
            <li
              key={index}
              className="flex gap-4 border-b border-dashed border-edge p-4"
            >
              <span className="font-mono">–</span>
              <Skeleton className="h-10 flex-1" />
            </li>
          ))
        ) : !rows.length || error ? (
          <li className="bg-dots p-10 text-center">
            <Inbox className="mx-auto mb-3 size-6" />
            <p>{error || labels.noResults}</p>
            <Button variant="outline" onClick={error ? onRetry : clearFilters}>
              {error ? labels.retry : labels.clearFilters}
            </Button>
          </li>
        ) : (
          (virtualization ? rows.slice(0, listLimit) : rows).map(
            (row, index) => {
              const cells = row.getAllCells()
              const name = cells[0]
              const status = cells.find(
                (cell) => cell.column.id === statusFilter?.columnId,
              )
              const rest = cells
                .filter((cell) => cell !== name && cell !== status)
                .slice(0, 3)
              return (
                <li
                  key={row.id}
                  className={cn(
                    "group/row flex border-b border-dashed border-edge last:border-b-0",
                    row.getIsSelected() && "bg-primary/[0.04]",
                  )}
                >
                  <div
                    className={cn(
                      "group/number relative flex w-10 shrink-0 items-center justify-center border-r border-dashed border-edge",
                      row.getIsSelected()
                        ? "bg-hatch"
                        : "focus-within:bg-dots hover:bg-dots",
                    )}
                    onPointerDown={() => {
                      longPress.current = setTimeout(
                        () => row.toggleSelected(true),
                        500,
                      )
                    }}
                    onPointerUp={() => {
                      if (longPress.current) clearTimeout(longPress.current)
                    }}
                    onPointerCancel={() => {
                      if (longPress.current) clearTimeout(longPress.current)
                    }}
                    onPointerLeave={() => {
                      if (longPress.current) clearTimeout(longPress.current)
                    }}
                  >
                    {row.getIsSelected() && (
                      <span
                        aria-hidden="true"
                        className="pointer-events-none absolute inset-y-0 left-0 w-0.5 bg-primary"
                      />
                    )}
                    <span
                      aria-hidden="true"
                      className={cn(
                        "font-mono text-[10px] text-muted-foreground group-focus-within/number:opacity-0 group-hover/number:opacity-0",
                        row.getIsSelected() && "opacity-0",
                      )}
                    >
                      {String(index + 1).padStart(2, "0")}
                    </span>
                    <Checkbox
                      aria-label={labels.selectRow + " " + (index + 1)}
                      checked={row.getIsSelected()}
                      onCheckedChange={(value) => row.toggleSelected(!!value)}
                      className={cn(
                        "absolute opacity-0 group-focus-within/number:opacity-100 group-hover/number:opacity-100",
                        row.getIsSelected() && "opacity-100",
                      )}
                    />
                  </div>
                  <button
                    type="button"
                    className="min-w-0 flex-1 p-3 text-left focus-visible:outline-2 focus-visible:outline-ring"
                    onClick={() => setDetailId(row.id)}
                  >
                    <span className="flex items-center justify-between gap-2">
                      <span className="truncate font-medium">
                        {String(name?.getValue() ?? "")}
                      </span>
                      <span className="shrink-0 text-xs">
                        {status &&
                          flexRender(
                            status.column.columnDef.cell,
                            status.getContext(),
                          )}
                      </span>
                    </span>
                    <span className="mt-2 flex items-center gap-3 font-mono text-[10px] text-muted-foreground">
                      {rest.map((cell, index) => (
                        <span
                          key={cell.id}
                          className={index === rest.length - 1 ? "ml-auto" : ""}
                        >
                          {flexRender(
                            cell.column.columnDef.cell,
                            cell.getContext(),
                          )}
                        </span>
                      ))}
                    </span>
                  </button>
                  {rowActions && (
                    <div className="flex shrink-0 items-center pr-2">
                      {rowActions(row.original)}
                    </div>
                  )}
                </li>
              )
            },
          )
        )}
        {virtualization && !loading && !error && rows.length > listLimit && (
          <li className="p-4">
            <Button
              variant="outline"
              size="lg"
              className="w-full"
              onClick={() => setListLimit((limit) => limit + MOBILE_LIST_BATCH)}
            >
              {labels.showMore(rows.length - listLimit)}
            </Button>
          </li>
        )}
      </ul>
      <Sheet
        open={!!detail}
        onOpenChange={(open) => {
          if (!open) setDetailId(null)
        }}
      >
        <SheetContent
          side={desktop ? "right" : "bottom"}
          className="max-h-[85dvh] overflow-auto p-6 sm:max-h-none"
        >
          <SheetTitle>
            {String(detail?.getAllCells()[0]?.getValue() ?? "")}
          </SheetTitle>
          <SheetDescription>{labels.detailDescription}</SheetDescription>
          <div className="divide-y divide-dashed divide-edge">
            {detail?.getAllCells().map((cell) => (
              <div key={cell.id} className="py-3">
                <p className="mb-1 text-xs text-muted-foreground">
                  {String(cell.column.columnDef.header)}
                </p>
                <DataTableCell
                  value={cell.getValue()}
                  row={detail.original}
                  meta={cell.column.columnDef.meta as DataTableMeta<T>}
                  title={String(cell.column.columnDef.header)}
                  labels={labels}
                  cellKey={cell.column.id}
                  rowHeight={52}
                  onCommit={(value) =>
                    commitCell(detail.id, cell.column.id, value)
                  }
                >
                  {(
                    cell.column.columnDef.meta as DataTableMeta<T>
                  )?.detailCell?.(cell.getValue(), detail.original) ??
                    flexRender(cell.column.columnDef.cell, cell.getContext())}
                </DataTableCell>
              </div>
            ))}
          </div>
        </SheetContent>
      </Sheet>
      <AnimatePresence>
        {selected > 0 && (
          <motion.div
            role="toolbar"
            aria-label="批量操作"
            initial={{ opacity: 0, y: reducedMotion ? 0 : 12 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: reducedMotion ? 0 : 12 }}
            transition={{ duration: 0.18 }}
            // 高度 2.75rem，用负的上边距抵消占位：浮在表格底部，不把分页栏往下推
            className="sticky bottom-4 z-40 mx-auto -mt-14 mb-3 flex h-11 w-fit max-w-[calc(100%-2rem)] flex-nowrap items-center gap-1 rounded-lg bg-foreground p-1 pl-3 text-sm whitespace-nowrap text-background shadow-lg"
          >
            <span aria-live="polite" className="shrink-0 font-mono">
              {labels.selected} {selected}
              <span className="hidden @min-[40rem]:inline">
                {" "}
                / {rowCount ?? data.length}
              </span>
            </span>
            {selected < (rowCount ?? data.length) && (
              <Button
                variant="ghost"
                className="shrink-0 px-2 font-normal text-background/70 hover:bg-background/15 hover:text-background"
                onClick={() =>
                  onSelectAll
                    ? onSelectAll()
                    : table.toggleAllRowsSelected(true)
                }
              >
                <span className="hidden @min-[40rem]:inline">
                  {labels.selectAll} {rowCount ?? data.length} {labels.items}
                </span>
                <span className="@min-[40rem]:hidden">
                  {labels.selectAllCompact} {rowCount ?? data.length}
                </span>
              </Button>
            )}
            <span
              aria-hidden="true"
              className="mx-1 h-5 w-px bg-background/20"
            />
            {/* 反色操作坞：覆盖按钮配色保持对比；窄容器只留图标（文字放在 data-label 里） */}
            <div className="flex shrink-0 gap-1 [&_[data-label]]:hidden @min-[40rem]:[&_[data-label]]:inline [&_button]:text-background [&_button:hover]:bg-background/15 [&_button:hover]:text-background">
              {bulkActions?.(
                table.getSelectedRowModel().rows.map((row) => row.original),
              )}
            </div>
            <span
              aria-hidden="true"
              className="mx-1 h-5 w-px bg-background/20"
            />
            <Button
              size="icon"
              variant="ghost"
              aria-label={labels.clear}
              className="shrink-0 text-background/70 hover:bg-background/15 hover:text-background"
              onClick={() => setRowSelection({})}
            >
              <X />
            </Button>
          </motion.div>
        )}
      </AnimatePresence>
      <DataTablePagination
        table={table}
        labels={labels}
        virtual={!!virtualization}
        count={count}
      />
    </div>
  )
}
export { DataTable }
export type { DataTableProps, DataTableColumn, DataTableLabels } from "./types"
```

### `src/registry/blocks/data-table-01/data-table-toolbar.tsx`

```tsx
"use client"
import * as React from "react"
import type { Table } from "@tanstack/react-table"
import { ArrowUpDown, Search, Settings2 } from "lucide-react"
import { BarField } from "@/components/ui/bar-field/bar-field"
import { BarSelect } from "@/components/ui/bar-select/bar-select"
import { SegmentedControl } from "@/components/ui/segmented-control/segmented-control"
import {
  GridToolbar,
  GridToolbarItem,
} from "@/components/ui/grid-toolbar/grid-toolbar"
import { Button } from "@/components/ui/button"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"
import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuCheckboxItem,
  DropdownMenuSeparator,
  DropdownMenuItem,
} from "@/components/ui/dropdown-menu"
import type { DataTableLabels, DataTableProps } from "./types"

/**
 * 密度图标：同样高度里画几条横线，线越多越紧凑。
 * 不带外框，免得读成数据表格。
 */
function DensityIcon({ lines }: { lines: number }) {
  const top = 5
  const bottom = 19
  const step = (bottom - top) / (lines - 1)
  return (
    <svg
      data-slot="data-table-density-icon"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      aria-hidden="true"
      className="size-4"
    >
      {Array.from({ length: lines }, (_, i) => (
        <path key={i} d={`M4 ${top + step * i}h16`} />
      ))}
    </svg>
  )
}

function DataTableToolbar<T>({
  table,
  labels,
  density,
  onDensityChange,
  statusFilter,
  children,
  searchLabel,
  counts,
}: {
  table: Table<T>
  labels: DataTableLabels
  density: string
  onDensityChange: (value: string) => void
  statusFilter?: DataTableProps<T>["statusFilter"]
  counts?: Record<string, number>
  searchLabel?: React.ReactNode
  children?: React.ReactNode
}) {
  "use no memo"
  // Table v8 的对象引用稳定，子组件同样需要读取当前状态。
  const searchRef = React.useRef<HTMLInputElement>(null)
  React.useEffect(() => {
    function focusSearch(event: KeyboardEvent) {
      if (
        event.key !== "/" ||
        event.ctrlKey ||
        event.metaKey ||
        event.altKey ||
        (event.target instanceof HTMLElement &&
          (event.target.isContentEditable ||
            event.target.closest("input,textarea,select")))
      )
        return
      event.preventDefault()
      searchRef.current?.focus()
    }
    window.addEventListener("keydown", focusSearch)
    return () => window.removeEventListener("keydown", focusSearch)
  }, [])
  const statusColumn = statusFilter
    ? table.getColumn(statusFilter.columnId)
    : undefined
  const status = String(statusColumn?.getFilterValue() ?? "")
  // 格内的图标按钮：方形、无圆角，焦点环画在格子内侧
  const cellButton = "h-full rounded-none focus-visible:ring-inset"
  return (
    <GridToolbar
      bordered={false}
      // 表格工具栏统一 12px；触屏输入框保持 16px，避免 iOS 聚焦时页面放大
      className="[&_*]:text-xs! pointer-coarse:[&_input]:text-base!"
    >
      {/* 搜索：宽容器里定宽，窄容器里占满第一行 */}
      <GridToolbarItem
        grow
        className="@min-[40rem]:w-64 @min-[40rem]:flex-none"
      >
        <BarField
          label={searchLabel}
          aria-label={searchLabel ? undefined : labels.searchField}
          icon={<Search />}
          inputRef={searchRef}
          type="search"
          placeholder={labels.search}
          value={table.getState().globalFilter ?? ""}
          onValueChange={(value) => {
            table.setGlobalFilter(value)
            table.setPageIndex(0)
          }}
          suffix={
            <kbd className="pointer-events-none grid size-5 place-items-center rounded-sm border border-edge bg-muted font-mono text-[11px] text-muted-foreground pointer-coarse:hidden">
              /
            </kbd>
          }
        />
      </GridToolbarItem>
      {statusFilter && (
        // 状态筛选用方条下拉：清除即「全部」；窄容器里独占最后一行
        <GridToolbarItem className="order-last basis-full @min-[40rem]:order-none @min-[40rem]:w-36 @min-[40rem]:basis-auto">
          <BarSelect
            aria-label={labels.status}
            placeholder={
              statusFilter.options.find((option) => option.value === "")
                ?.label ?? labels.all
            }
            value={status || null}
            contentClassName="[&_*]:text-xs"
            options={statusFilter.options
              .filter((option) => option.value !== "")
              .map((option) => ({
                value: option.value,
                textValue: option.label,
                count: counts?.[option.value] ?? 0,
                label: (
                  <span className="flex items-center gap-2">
                    {option.tone && (
                      <span
                        aria-hidden="true"
                        className="size-1.5 shrink-0 rounded-full"
                        style={{ background: option.tone }}
                      />
                    )}
                    {option.label}
                  </span>
                ),
              }))}
            onValueChange={(value) => {
              statusColumn?.setFilterValue(value || undefined)
              table.setPageIndex(0)
            }}
          />
        </GridToolbarItem>
      )}
      {children}
      {/* 空白格：吃掉剩余宽度，让列设置与密度靠右 */}
      <GridToolbarItem
        grow
        aria-hidden="true"
        className="hidden @min-[40rem]:flex"
      />
      <GridToolbarItem>
        <DropdownMenu>
          <Tooltip>
            <TooltipTrigger
              render={
                <DropdownMenuTrigger
                  render={
                    <Button
                      variant="ghost"
                      size="icon-lg"
                      aria-label={labels.columns}
                      className={cellButton}
                    />
                  }
                />
              }
            >
              <Settings2 aria-hidden="true" />
            </TooltipTrigger>
            <TooltipContent>{labels.columns}</TooltipContent>
          </Tooltip>
          <DropdownMenuContent align="end" className="min-w-44 *:text-xs!">
            {table.getAllLeafColumns().map((column) => (
              <DropdownMenuCheckboxItem
                key={column.id}
                checked={column.getIsVisible()}
                onCheckedChange={(value) => column.toggleVisibility(value)}
                closeOnClick={false}
              >
                {String(column.columnDef.header ?? column.id)}
              </DropdownMenuCheckboxItem>
            ))}
            <DropdownMenuSeparator />
            <DropdownMenuItem onClick={() => table.resetColumnVisibility()}>
              {labels.reset}
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </GridToolbarItem>
      {/* 密度是另一组：用实线与列设置分开；只用图标，文字在提示气泡和读屏里 */}
      <GridToolbarItem separator="solid" className="hidden @min-[40rem]:flex">
        <SegmentedControl
          aria-label={labels.density}
          iconOnly
          value={density}
          options={[
            {
              value: "40",
              label: labels.compact,
              icon: <DensityIcon lines={4} />,
            },
            {
              value: "52",
              label: labels.standard,
              icon: <DensityIcon lines={3} />,
            },
            {
              value: "64",
              label: labels.comfortable,
              icon: <DensityIcon lines={2} />,
            },
          ]}
          onValueChange={onDensityChange}
        />
      </GridToolbarItem>
      <GridToolbarItem className="@min-[40rem]:hidden">
        <DropdownMenu>
          <Tooltip>
            <TooltipTrigger
              render={
                <DropdownMenuTrigger
                  render={
                    <Button
                      size="icon-lg"
                      variant="ghost"
                      aria-label={labels.sort}
                      className={cellButton}
                    />
                  }
                />
              }
            >
              <ArrowUpDown aria-hidden="true" />
            </TooltipTrigger>
            <TooltipContent>{labels.sort}</TooltipContent>
          </Tooltip>
          <DropdownMenuContent align="end" className="*:text-xs!">
            {table
              .getAllLeafColumns()
              .filter((column) => column.getCanSort())
              .map((column) => (
                <React.Fragment key={column.id}>
                  {[false, true].map((desc) => (
                    <DropdownMenuItem
                      key={String(desc)}
                      onClick={() => column.toggleSorting(desc)}
                    >
                      {String(column.columnDef.header)} ·{" "}
                      {desc ? labels.descending : labels.ascending}
                    </DropdownMenuItem>
                  ))}
                </React.Fragment>
              ))}
          </DropdownMenuContent>
        </DropdownMenu>
      </GridToolbarItem>
    </GridToolbar>
  )
}
export { DataTableToolbar }
```

### `src/registry/blocks/data-table-01/data-table-pagination.tsx`

```tsx
"use client"
import type { Table } from "@tanstack/react-table"
import { GridPagination } from "@/components/ui/grid-pagination/grid-pagination"
import type { DataTableLabels } from "./types"
function DataTablePagination<T>({
  table,
  labels,
  virtual,
  count,
}: {
  table: Table<T>
  labels: DataTableLabels
  virtual: boolean
  count: number
}) {
  "use no memo"
  const { pageIndex, pageSize } = table.getState().pagination
  return (
    // 翻页贴在表格底部：先隔一条点阵小带，再是翻页行；左右与底边沿用表格外框
    <div data-slot="data-table-pagination" className="border-t border-edge">
      <div
        aria-hidden="true"
        data-slot="data-table-pagination-band"
        className="h-4 border-b border-edge bg-dots"
      />
      {virtual ? (
        <p className="flex h-9 items-center px-3 font-mono text-xs text-muted-foreground tabular-nums pointer-coarse:h-11">
          {labels.rowTotal(count)}
        </p>
      ) : (
        <GridPagination
          bordered={false}
          page={pageIndex + 1}
          pageCount={table.getPageCount()}
          total={count}
          pageSize={pageSize}
          pageSizes={[10, 20, 50, 100]}
          jumper
          siblings={1}
          labels={{
            first: labels.first,
            previous: labels.previous,
            next: labels.next,
            last: labels.last,
            perPage: labels.perPage,
            jump: labels.jump,
          }}
          onPageChange={(page) => table.setPageIndex(page - 1)}
          onPageSizeChange={(size) => {
            table.setPageSize(size)
            table.setPageIndex(0)
          }}
        />
      )}
    </div>
  )
}
export { DataTablePagination }
```

### `src/registry/blocks/data-table-01/data-table-cell.tsx`

```tsx
"use client"

import * as React from "react"
import { Check, CircleAlert, LoaderCircle, Pencil } from "lucide-react"
import { createPortal } from "react-dom"
import { cn } from "@/lib/utils"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetContent,
  SheetTitle,
  SheetDescription,
} from "@/components/ui/sheet"
import type { DataTableMeta, DataTableLabels } from "./types"

const invertedKey =
  "border-primary-foreground/25 bg-primary-foreground/10 text-primary-foreground"

type Phase = "idle" | "saving" | "saved" | "failed"

function Key({
  className,
  children,
}: {
  className?: string
  children: React.ReactNode
}) {
  return (
    <kbd
      data-slot="data-table-cell-key"
      className={cn(
        "inline-flex h-4 min-w-4 items-center justify-center rounded-[3px] border border-b-2 border-edge bg-background px-1 font-mono text-[10px] leading-none text-foreground",
        className,
      )}
    >
      {children}
    </kbd>
  )
}

/**
 * 可编辑单元格。编辑采用表格软件的习惯：
 * - 编辑框就地替换内容，整格描主色实线框，下方贴一条提示条（快捷键 / 校验错误）
 * - Enter、Tab、点击别处都会保存；值没变直接退出；Esc 放弃
 * - 保存是乐观的：编辑框立刻关闭，格子右上角依次显示 保存中 → 已保存（淡出）
 * - 保存失败时值回滚，格子角标变红并保留草稿，再次编辑会带回草稿和错误
 */
function DataTableCell<T>({
  value,
  row,
  meta,
  title,
  labels,
  onCommit,
  children,
  cellKey,
  rowHeight,
}: {
  value: unknown
  row: T
  meta?: DataTableMeta<T>
  title: string
  labels: DataTableLabels
  onCommit: (value: unknown) => void | Promise<void>
  children: React.ReactNode
  cellKey: string
  rowHeight: number
}) {
  const [editing, setEditing] = React.useState(false)
  const [mobile, setMobile] = React.useState(false)
  const [draft, setDraft] = React.useState("")
  const [error, setError] = React.useState("")
  const [phase, setPhase] = React.useState<Phase>("idle")
  const [failed, setFailed] = React.useState<{
    draft: string
    message: string
  } | null>(null)
  const [hovered, setHovered] = React.useState(false)
  const [focused, setFocused] = React.useState(false)
  const [position, setPosition] = React.useState({ left: 0, top: 0 })
  const root = React.useRef<HTMLDivElement>(null)
  // 关闭编辑框时输入框卸载会触发 blur，用它避免重复保存
  const closing = React.useRef(false)
  const id = React.useId()
  const numeric = meta?.editable === "number"
  const peek = hovered || focused
  const hint = editing || (phase === "failed" && peek)

  React.useLayoutEffect(() => {
    if (!hint || mobile) return
    function update() {
      // 提示条贴住整格（td）的左下角，而不是内容区
      const bounds = (
        root.current?.closest("td") ?? root.current
      )?.getBoundingClientRect()
      if (bounds)
        setPosition({
          left: Math.max(8, Math.min(bounds.left, window.innerWidth - 328)),
          top: bounds.bottom - 1,
        })
    }
    update()
    window.addEventListener("scroll", update, true)
    window.addEventListener("resize", update)
    return () => {
      window.removeEventListener("scroll", update, true)
      window.removeEventListener("resize", update)
    }
  }, [hint, mobile])

  React.useEffect(() => {
    if (phase !== "saved") return
    const timer = setTimeout(() => setPhase("idle"), 1600)
    return () => clearTimeout(timer)
  }, [phase])

  function start() {
    closing.current = false
    setDraft(failed?.draft ?? String(value ?? ""))
    setError(failed?.message ?? "")
    setMobile(window.matchMedia("(pointer: coarse)").matches)
    setEditing(true)
  }

  function close(direction?: number) {
    closing.current = true
    setEditing(false)
    setError("")
    if (mobile) return
    const cells = Array.from(
      root.current
        ?.closest("table")
        ?.querySelectorAll<HTMLElement>("[data-editable=true]") ?? [],
    )
    const index = cells.indexOf(root.current!)
    const target =
      direction === undefined
        ? undefined
        : direction === 0
          ? cells.slice(index + 1).find((el) => el.dataset.column === cellKey)
          : cells[index + direction]
    requestAnimationFrame(() => (target ?? root.current)?.focus())
  }

  function cancel() {
    if (failed) {
      setFailed(null)
      setPhase("idle")
    }
    close()
  }

  async function commit(direction?: number, blur = false) {
    if (closing.current) return
    const text = draft.trim()
    const next = numeric
      ? text === ""
        ? Number.NaN
        : Number(text.replace(/,/g, ""))
      : draft
    // 值没变：直接退出，不发请求
    if (!failed && String(next) === String(value ?? "")) {
      if (blur) {
        closing.current = true
        setEditing(false)
      } else close(direction)
      return
    }
    const message = meta?.validate?.(next, row)
    if (message) {
      setError(message)
      return
    }
    if (blur) {
      closing.current = true
      setEditing(false)
    } else close(direction)
    setFailed(null)
    setPhase("saving")
    try {
      await onCommit(next)
      setPhase("saved")
    } catch (reason) {
      setFailed({
        draft,
        message: reason instanceof Error ? reason.message : labels.saveError,
      })
      setPhase("failed")
    }
  }

  const input = (
    <Input
      autoFocus
      aria-label={title}
      aria-invalid={!!error}
      aria-describedby={error ? id : undefined}
      type="text"
      inputMode={numeric ? "decimal" : undefined}
      autoComplete="off"
      value={draft}
      className={
        mobile
          ? cn("min-w-20 pointer-coarse:text-base", numeric && "font-mono")
          : cn(
              "absolute inset-0 h-full! min-w-0 rounded-none border-0 bg-transparent! px-0 py-0 shadow-none focus-visible:ring-0 aria-invalid:ring-0",
              numeric && "text-right font-mono tabular-nums",
            )
      }
      onFocus={(event) => event.currentTarget.select()}
      onChange={(event) => {
        setDraft(event.target.value)
        setError("")
      }}
      onBlur={() => {
        if (!mobile) void commit(undefined, true)
      }}
      onKeyDown={(event) => {
        if (event.key === "Escape") {
          event.preventDefault()
          cancel()
        }
        if (!mobile && (event.key === "Enter" || event.key === "Tab")) {
          event.preventDefault()
          void commit(event.key === "Enter" ? 0 : event.shiftKey ? -1 : 1)
        }
        if (mobile && event.key === "Enter") {
          event.preventDefault()
          void commit()
        }
      }}
    />
  )

  const message = error || (!editing && failed?.message)

  return (
    <div
      ref={root}
      style={{ height: rowHeight - 1 }}
      data-slot="data-table-cell"
      data-editable={!!meta?.editable}
      data-editing={(editing && !mobile) || undefined}
      data-state={phase === "idle" ? undefined : phase}
      data-column={cellKey}
      tabIndex={meta?.editable ? 0 : undefined}
      aria-describedby={phase === "failed" && peek ? id : undefined}
      className={cn(
        "group/cell relative flex h-full min-h-0 flex-col justify-center outline-none",
      )}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      onFocus={(event) => setFocused(event.target === event.currentTarget)}
      onBlur={() => setFocused(false)}
      onDoubleClick={(event) => {
        if (
          event.currentTarget.contains(event.target as Node) &&
          meta?.editable &&
          !editing
        )
          start()
      }}
      onClick={(event) => {
        // Sheet 通过 Portal 渲染，面板事件不能冒泡成表格单元格的再次编辑。
        if (!event.currentTarget.contains(event.target as Node)) return
        if (
          !editing &&
          meta?.editable &&
          window.matchMedia("(pointer: coarse)").matches
        )
          start()
      }}
      onKeyDown={(event) => {
        if (event.target !== event.currentTarget) return
        if (!editing && meta?.editable && ["Enter", "F2"].includes(event.key)) {
          event.preventDefault()
          start()
        }
      }}
    >
      {/* 整格框：所有状态都画在 td 的边界上（-inset-x-3 抵消 td 的 px-3），和下方提示条左缘对齐。
          悬停 = 浅底；键盘焦点 = 实线框；失败 = 红框 + 红底；编辑 = 主色粗框 */}
      {meta?.editable && (
        <span
          aria-hidden="true"
          className={cn(
            "pointer-events-none absolute -inset-x-3 inset-y-0 ring-inset",
            editing && !mobile
              ? cn(
                  "z-10 bg-background ring-2",
                  error ? "ring-destructive" : "ring-primary",
                )
              : phase === "failed"
                ? "bg-destructive/5 ring-1 ring-destructive/60"
                : "group-hover/cell:bg-muted/60 group-focus-visible/cell:ring-2 group-focus-visible/cell:ring-ring",
          )}
        />
      )}
      {editing && !mobile ? (
        <span className="absolute inset-0 z-10">{input}</span>
      ) : (
        <div
          className={cn(
            "relative min-w-0 transition-opacity",
            phase === "saving" && "opacity-50",
          )}
        >
          {children}
        </div>
      )}

      {/* 右上角状态：悬停可编辑 / 保存中 / 已保存（淡出）/ 失败 */}
      {!editing && (
        <span
          aria-hidden="true"
          className="pointer-events-none absolute top-1.5 -right-1.5 flex size-3 items-center justify-center"
        >
          {phase === "saving" ? (
            <LoaderCircle className="size-3 animate-spin text-muted-foreground motion-reduce:animate-none" />
          ) : phase === "saved" ? (
            <Check className="size-3 animate-out text-(--flow-green) delay-1000 duration-500 fade-out-0 fill-mode-forwards" />
          ) : phase === "failed" ? (
            <CircleAlert className="size-3 text-destructive" />
          ) : meta?.editable ? (
            <Pencil className="size-3 text-muted-foreground opacity-0 group-hover/cell:opacity-100" />
          ) : null}
        </span>
      )}
      <span className="sr-only" aria-live="polite">
        {phase === "saving"
          ? labels.saving
          : phase === "saved"
            ? labels.saved
            : ""}
      </span>

      {hint &&
        !mobile &&
        createPortal(
          <div
            id={id}
            role={message ? "alert" : undefined}
            style={{ left: position.left, top: position.top }}
            className={cn(
              // 提示条像挂在格子框下沿的标签：左缘、边框粗细与颜色都和格子框一致
              "pointer-events-none fixed z-50 flex max-w-80 items-center gap-2 rounded-b-md border-t-0 px-2 py-1 text-xs shadow-sm",
              message
                ? cn(
                    "bg-popover text-destructive",
                    editing
                      ? "border-2 border-destructive"
                      : "border border-destructive/60",
                  )
                : "border-2 border-primary bg-primary text-primary-foreground/70",
            )}
          >
            {message ? (
              <>
                <CircleAlert aria-hidden="true" className="size-3.5 shrink-0" />
                <span className="min-w-0 truncate">{message}</span>
                {!editing && (
                  <span className="shrink-0 text-muted-foreground">
                    · <Key>↵</Key> {labels.editAgain}
                  </span>
                )}
              </>
            ) : (
              <span className="flex items-center gap-3 whitespace-nowrap">
                <span className="flex items-center gap-1">
                  <Key className={invertedKey}>↵</Key>
                  {labels.save}
                </span>
                <span className="flex items-center gap-1">
                  <Key className={invertedKey}>Tab</Key>
                  {labels.nextCell}
                </span>
                <span className="flex items-center gap-1">
                  <Key className={invertedKey}>Esc</Key>
                  {labels.cancel}
                </span>
              </span>
            )}
          </div>,
          document.body,
        )}

      <Sheet
        open={editing && mobile}
        onOpenChange={(open) => {
          if (!open) cancel()
        }}
      >
        <SheetContent
          side="bottom"
          className="space-y-4 p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))]"
        >
          <SheetTitle>{title}</SheetTitle>
          <SheetDescription>
            {labels.editDescription(String(value ?? ""), title)}
          </SheetDescription>
          {editing && mobile && input}
          {error && mobile && (
            <p
              id={id}
              role="alert"
              className="flex items-center gap-1.5 text-sm text-destructive"
            >
              <CircleAlert aria-hidden="true" className="size-4 shrink-0" />
              {error}
            </p>
          )}
          <div className="flex gap-3">
            <Button
              size="lg"
              variant="outline"
              className="flex-1"
              onClick={cancel}
            >
              {labels.cancel}
            </Button>
            <Button size="lg" className="flex-1" onClick={() => void commit()}>
              {labels.save}
            </Button>
          </div>
        </SheetContent>
      </Sheet>
    </div>
  )
}
export { DataTableCell }
```

### `src/registry/blocks/data-table-01/types.ts`

```ts
import type * as React from "react"
import type {
  Table,
  ColumnDef,
  SortingState,
  PaginationState,
  ColumnFiltersState,
  RowSelectionState,
} from "@tanstack/react-table"

type DataTableMeta<T> = {
  detailCell?: (value: unknown, row: T) => React.ReactNode
  bar?: boolean
  max?: number
  align?: "left" | "center" | "right"
  editable?: "text" | "number"
  validate?: (value: unknown, row: T) => string | undefined
  hideOnMobile?: boolean
  pinned?: boolean
  width?: number
}
type DataTableColumn<T> = ColumnDef<T, unknown> & { meta?: DataTableMeta<T> }
type DataTableLabels = {
  sort: string
  ascending: string
  descending: string
  detailDescription: string
  emptyDescription: string
  noResultsDescription: string
  errorDescription: string
  items: string
  page: string
  editDescription: (value: string, column: string) => string
  actions: string
  rowTotal: (count: number) => string
  /** 虚拟滚动模式下窄屏列表的「显示更多」 */
  showMore: (rest: number) => string
  search: string
  /** 搜索框的读屏名称（未传 searchLabel 时使用） */
  searchField: string
  status: string
  columns: string
  reset: string
  density: string
  compact: string
  standard: string
  comfortable: string
  selected: string
  selectAllCompact: string
  selectAll: string
  clear: string
  retry: string
  empty: string
  noResults: string
  clearFilters: string
  /** 状态筛选未选时的占位（statusFilter 里没有空值选项时使用） */
  all: string
  first: string
  previous: string
  next: string
  last: string
  total: string
  perPage: string
  jump: string
  cancel: string
  save: string
  saveError: string
  saving: string
  saved: string
  nextCell: string
  editAgain: string
  selectPage: string
  selectRow: string
}
type DataTableHandle = {
  startEditing: (rowId: string, columnId: string) => void
  openDetails: (rowId: string) => void
}
type DataTableProps<T> = Omit<React.ComponentProps<"div">, "onChange"> & {
  apiRef?: React.Ref<DataTableHandle>
  columns: DataTableColumn<T>[]
  data: T[]
  getRowId: (row: T) => string
  mode?: "client" | "manual"
  rowCount?: number
  loading?: boolean
  error?: string | null
  onRetry?: () => void
  sorting?: SortingState
  defaultSorting?: SortingState
  onSortingChange?: (value: SortingState) => void
  pagination?: PaginationState
  defaultPagination?: PaginationState
  onPaginationChange?: (value: PaginationState) => void
  globalFilter?: string
  defaultGlobalFilter?: string
  onGlobalFilterChange?: (value: string) => void
  columnFilters?: ColumnFiltersState
  defaultColumnFilters?: ColumnFiltersState
  onColumnFiltersChange?: (value: ColumnFiltersState) => void
  rowSelection?: RowSelectionState
  defaultRowSelection?: RowSelectionState
  onRowSelectionChange?: (value: RowSelectionState) => void
  onSelectAll?: () => void
  bulkActions?: (selected: T[]) => React.ReactNode
  rowActions?: (row: T) => React.ReactNode
  /** 搜索框条内标签，默认不显示；传入如「搜索」时显示在图标前 */
  searchLabel?: React.ReactNode
  onCellCommit?: (change: {
    rowId: string
    columnId: string
    value: unknown
    row: T
  }) => void | Promise<void>
  virtualization?: false | { estimateRowHeight?: number; height?: number }
  header?: React.ReactNode
  counts?: Record<string, number>
  /** 工具栏插槽：工具栏是网格工具栏，每个控件包一层 `GridToolbarItem` 成为一格 */
  toolbar?: React.ReactNode | ((table: Table<T>) => React.ReactNode)
  statusFilter?: {
    columnId: string
    /** tone：状态色点的颜色，填 CSS 颜色或主题变量，如 "var(--flow-green)" */
    options: { value: string; label: string; tone?: string }[]
  }
  labels?: Partial<DataTableLabels>
}
export type {
  DataTableHandle,
  DataTableProps,
  DataTableColumn,
  DataTableMeta,
  DataTableLabels,
}
```

### `src/registry/blocks/data-table-01/config.ts`

```ts
import type { DataTableLabels } from "./types"
const DATA_TABLE_LABELS: DataTableLabels = {
  sort: "排序",
  ascending: "升序",
  descending: "降序",
  detailDescription: "查看全部字段，点击可编辑字段进行修改。",
  emptyDescription: "添加第一份数据集，开始整理团队的数据资源。",
  noResultsDescription: "试试其他关键词，或清除筛选条件。",
  errorDescription: "暂时无法读取数据，请稍后重试。",
  items: "条",
  page: "页",
  editDescription: (value, column) => "编辑「" + value + "」的" + column,
  actions: "操作",
  rowTotal: (count: number) => `共 ${count} 行`,
  showMore: (rest: number) => `显示更多（还有 ${rest} 行）`,
  search: "搜索数据集…",
  searchField: "搜索",
  status: "状态",
  columns: "列设置",
  reset: "恢复默认",
  density: "密度",
  compact: "紧凑",
  standard: "标准",
  comfortable: "舒适",
  selected: "已选",
  selectAllCompact: "全选",
  selectAll: "选择全部",
  clear: "清除选择",
  retry: "重试",
  empty: "暂无数据",
  noResults: "没有符合条件的结果",
  clearFilters: "清除筛选",
  all: "全部状态",
  first: "首页",
  previous: "上一页",
  next: "下一页",
  last: "尾页",
  total: "共",
  perPage: "每页",
  jump: "跳至",
  cancel: "取消",
  save: "保存",
  saveError: "保存失败，请重试",
  saving: "保存中",
  saved: "已保存",
  nextCell: "下一格",
  editAgain: "重新编辑",
  selectPage: "选择当前页",
  selectRow: "选择行",
}
const numberFormat = new Intl.NumberFormat("zh-CN")
export { DATA_TABLE_LABELS, numberFormat }
```

### `src/registry/blocks/data-table-01/page.tsx`

```tsx
"use client"
import * as React from "react"
import {
  Plus,
  Upload,
  Eye,
  Pencil,
  Copy,
  Archive,
  Download,
  Trash2,
} from "lucide-react"
import { DataTableRowActions } from "@/components/data-table-01/data-table-row-actions"
import type { DataTableHandle } from "@/components/data-table-01/types"
import {
  AlertDialog,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogCancel,
  AlertDialogAction,
} from "@/components/ui/alert-dialog"
import { BarSelect } from "@/components/ui/bar-select/bar-select"
import { GridToolbarItem } from "@/components/ui/grid-toolbar/grid-toolbar"
import { Button } from "@/components/ui/button"
import {
  GridContainer,
  GridSection,
  GridSeparator,
} from "@/components/ui/grid-layout/grid-layout"
import { numberFormat } from "@/components/data-table-01/config"
import { DataTable } from "@/components/data-table-01/data-table"
import type { DataTableColumn } from "@/components/data-table-01/types"
type Dataset = {
  code: string
  id: string
  name: string
  owner: string
  status: string
  samples: number
  updatedAt: string
}
const datasets: Dataset[] = Array.from({ length: 666 }, (_, i) => ({
  code: "DS-" + String(i + 1).padStart(4, "0"),
  id: String(i + 1),
  name:
    ["城市声景", "植物图谱", "手写字迹", "街道影像"][i % 4] +
    " " +
    String(i + 1).padStart(2, "0"),
  owner: ["林小满", "陈青", "周远"][i % 3],
  status: ["进行中", "已归档", "待审核"][i % 3],
  samples: 1200 + i * 137,
  updatedAt: new Date(
    Date.UTC(2026, 9, 31, 0) - (i < 6 ? (i + 1) * 3600000 : (i - 4) * 86400000),
  ).toISOString(),
}))
const columns: DataTableColumn<Dataset>[] = [
  {
    accessorKey: "name",
    header: "名称",
    size: 240,
    cell: ({ row, getValue }) => (
      <span>
        <span className="font-medium">{String(getValue())}</span>
        <span className="mt-1 block font-mono text-[10px] text-muted-foreground">
          {row.original.code}
        </span>
      </span>
    ),
    meta: {
      pinned: true,
      editable: "text",
      validate: (value) => (String(value).trim() ? undefined : "请输入名称"),
    },
  },
  {
    accessorKey: "owner",
    filterFn: (row, columnId, values: string[]) =>
      !values.length || values.includes(String(row.getValue(columnId))),
    header: "负责人",
    size: 140,
    cell: ({ getValue }) => (
      <span className="inline-flex items-center gap-2">
        <span className="flex size-5 items-center justify-center rounded-sm border border-edge bg-muted text-[10px]">
          {String(getValue()).slice(0, 1)}
        </span>
        {String(getValue())}
      </span>
    ),
  },
  {
    accessorKey: "status",
    header: "状态",
    size: 140,
    cell: ({ getValue }) => (
      <span className="inline-flex items-center gap-1.5 text-xs">
        <span
          className={
            String(getValue()) === "进行中"
              ? "size-1.5 rounded-full bg-(--flow-green)"
              : String(getValue()) === "待审核"
                ? "size-1.5 rounded-full bg-(--flow-amber)"
                : "size-1.5 rounded-full bg-muted-foreground"
          }
        />
        {String(getValue())}
      </span>
    ),
  },
  {
    accessorKey: "samples",
    header: "样本数",
    cell: ({ getValue }) => numberFormat.format(Number(getValue())),
    size: 140,
    meta: {
      bar: true,
      align: "right",
      editable: "number",
      validate: (value) =>
        Number.isFinite(value) && Number(value) >= 0
          ? undefined
          : "样本数不能小于零",
    },
  },
  {
    accessorKey: "updatedAt",
    cell: ({ getValue }) => <RelativeDate value={String(getValue())} />,
    header: "更新时间",
    size: 180,
    meta: {
      hideOnMobile: true,
      detailCell: (value) => <RelativeDate value={String(value)} exact />,
    },
  },
]
// 全表共用一个分钟级时钟：新挂载的单元格（翻页、排序）直接读到当前时间，不会先显示日期再跳成「几小时前」；
// 只有首次水合时用 null，与预渲染的日期一致
let clock = 0
let clockTimer: ReturnType<typeof setInterval> | undefined
const clockListeners = new Set<() => void>()
function subscribeClock(listener: () => void) {
  clockListeners.add(listener)
  if (clockListeners.size === 1)
    clockTimer = setInterval(() => {
      clock = Date.now()
      clockListeners.forEach((notify) => notify())
    }, 60000)
  return () => {
    clockListeners.delete(listener)
    if (!clockListeners.size) clearInterval(clockTimer)
  }
}
function getClock() {
  if (!clock) clock = Date.now()
  return clock
}
function RelativeDate({
  value,
  exact = false,
}: {
  value: string
  exact?: boolean
}) {
  const now = React.useSyncExternalStore(subscribeClock, getClock, () => null)
  const date = new Date(value)
  const days = now === null ? 100 : (now - date.getTime()) / 86400000
  const relative = new Intl.RelativeTimeFormat("zh-CN", { numeric: "auto" })
  const text =
    days > 7
      ? value.slice(5, 10)
      : days < 1
        ? relative.format(-Math.max(1, Math.floor(days * 24)), "hour")
        : relative.format(-Math.floor(days), "day")
  return (
    <time
      data-slot="data-table-date"
      dateTime={value}
      title={new Intl.DateTimeFormat("zh-CN", {
        dateStyle: "full",
        timeStyle: "long",
        timeZone: "Asia/Shanghai",
      }).format(date)}
      className="font-mono text-xs text-muted-foreground"
    >
      {exact
        ? new Intl.DateTimeFormat("sv-SE", {
            timeZone: "Asia/Shanghai",
            year: "numeric",
            month: "2-digit",
            day: "2-digit",
            hour: "2-digit",
            minute: "2-digit",
          }).format(date) +
          "（" +
          text +
          "）"
        : text}
    </time>
  )
}
const getRowId = (row: Dataset) => row.id
function DataTablePage() {
  const apiRef = React.useRef<DataTableHandle>(null)
  const [deleting, setDeleting] = React.useState<Dataset[]>([])
  const [data, setData] = React.useState(datasets)
  const [selection, setSelection] = React.useState<Record<string, boolean>>({})
  const [notice, setNotice] = React.useState("")
  function exportRows(rows: Dataset[]) {
    const url = URL.createObjectURL(
      new Blob(
        [
          "\ufeff名称,负责人,状态,样本数,更新时间\n" +
            rows
              .map((row) =>
                [
                  row.name,
                  row.owner,
                  row.status,
                  row.samples,
                  row.updatedAt,
                ].join(","),
              )
              .join("\n"),
        ],
        { type: "text/csv;charset=utf-8" },
      ),
    )
    const link = document.createElement("a")
    link.href = url
    link.download = "数据集.csv"
    link.click()
    URL.revokeObjectURL(url)
    setNotice("已导出 " + rows.length + " 条")
  }
  return (
    // 线条网格外框：竖线容器 + 全屏横线；表格贴着两条竖线铺满，工具栏和分页用 --data-table-gutter 与竖线留出间距
    <main
      data-slot="data-table-page"
      className="@container w-full min-w-0 overflow-x-clip bg-background py-6 [--data-table-gutter:1rem] [--grid-width:72rem] sm:py-10 sm:[--data-table-gutter:1.5rem]"
    >
      <GridContainer>
        <GridSection
          topLine
          className="flex flex-col items-start justify-between gap-4 px-4 py-6 sm:px-6 @min-[40rem]:flex-row @min-[40rem]:items-end"
        >
          <div>
            <h1 className="text-2xl font-semibold tracking-tight">数据集</h1>
            <p className="mt-1.5 text-sm text-muted-foreground">
              整理、审核与维护团队的数据资源。
            </p>
          </div>
          <div className="flex gap-2">
            <Button
              variant="outline"
              onClick={() => setNotice("请选择 CSV 文件导入")}
            >
              <Upload />
              导入
            </Button>
            <Button
              onClick={() => {
                const id = String(
                  Math.max(0, ...data.map((row) => Number(row.id))) + 1,
                )
                setData((previous) => [
                  {
                    ...datasets[0],
                    id,
                    code: "DS-" + id.padStart(4, "0"),
                    name: "新建数据集",
                  },
                  ...previous,
                ])
                setNotice("已新建数据集")
              }}
            >
              <Plus />
              新建数据集
            </Button>
          </div>
        </GridSection>
        <GridSeparator label={"DATASETS / " + data.length} />
        <GridSection>
          <DataTable
            apiRef={apiRef}
            toolbar={(table) => (
              // 工具栏是网格工具栏：插槽里每个控件包一层 GridToolbarItem 成为一格
              <GridToolbarItem className="hidden w-40 @min-[64rem]:flex">
                <BarSelect
                  multiple
                  aria-label="负责人"
                  placeholder="全部负责人"
                  contentClassName="[&_[data-slot=combobox-item]]:text-xs [&_*]:text-xs"
                  value={
                    (table.getColumn("owner")?.getFilterValue() as string[]) ??
                    []
                  }
                  options={["林小满", "陈青", "周远"].map((owner) => ({
                    value: owner,
                    label: owner,
                    count: data.filter((row) => row.owner === owner).length,
                  }))}
                  onValueChange={(values) => {
                    table
                      .getColumn("owner")
                      ?.setFilterValue(values.length ? values : undefined)
                    table.setPageIndex(0)
                  }}
                />
              </GridToolbarItem>
            )}
            rowActions={(row) => (
              <DataTableRowActions
                label={row.name}
                actions={[
                  {
                    label: "查看详情",
                    icon: <Eye />,
                    onSelect: () => apiRef.current?.openDetails(row.id),
                  },
                  {
                    label: "编辑名称",
                    icon: <Pencil />,
                    onSelect: () =>
                      apiRef.current?.startEditing(row.id, "name"),
                  },
                  {
                    label: "复制编号",
                    icon: <Copy />,
                    shortcut: row.code,
                    onSelect: () => {
                      void navigator.clipboard.writeText(row.code).then(
                        () => setNotice("已复制"),
                        () => setNotice("复制失败"),
                      )
                    },
                  },
                  "separator",
                  {
                    label: "导出 CSV",
                    icon: <Download />,
                    onSelect: () => exportRows([row]),
                  },
                  {
                    label: "归档",
                    icon: <Archive />,
                    onSelect: () =>
                      setData((previous) =>
                        previous.map((item) =>
                          item.id === row.id
                            ? { ...item, status: "已归档" }
                            : item,
                        ),
                      ),
                  },
                  "separator",
                  {
                    label: "删除…",
                    icon: <Trash2 />,
                    destructive: true,
                    onSelect: () => setDeleting([row]),
                  },
                ]}
              />
            )}
            columns={columns}
            data={data}
            rowCount={data.length}
            rowSelection={selection}
            onRowSelectionChange={setSelection}
            onSelectAll={() =>
              setSelection(
                Object.fromEntries(data.map((row) => [row.id, true])),
              )
            }
            bulkActions={(rows) => (
              <>
                <Button
                  variant="ghost"
                  size="default"
                  aria-label="导出"
                  onClick={() => exportRows(rows)}
                >
                  <Download />
                  <span data-label>导出</span>
                </Button>
                <Button
                  variant="ghost"
                  size="default"
                  aria-label="归档"
                  onClick={() =>
                    setData((previous) =>
                      previous.map((row) =>
                        selection[row.id] ? { ...row, status: "已归档" } : row,
                      ),
                    )
                  }
                >
                  <Archive />
                  <span data-label>归档</span>
                </Button>
                {/* 反色坞里删除用红色文字区分，混入背景色保证亮暗模式都清楚 */}
                <Button
                  variant="ghost"
                  size="default"
                  aria-label="删除"
                  className="text-[color-mix(in_oklab,var(--destructive)_75%,var(--background))]!"
                  onClick={() => setDeleting(rows)}
                >
                  <Trash2 />
                  <span data-label>删除</span>
                </Button>
              </>
            )}
            onCellCommit={async ({ rowId, columnId, value }) => {
              // 模拟请求：等待 600ms；重名由“服务端”拒绝，演示保存失败与回滚
              await new Promise((resolve) => setTimeout(resolve, 600))
              if (
                columnId === "name" &&
                data.some(
                  (row) =>
                    row.id !== rowId && row.name === String(value).trim(),
                )
              )
                throw new Error("已有同名数据集")
              setData((previous) =>
                previous.map((row) =>
                  row.id === rowId ? { ...row, [columnId]: value } : row,
                ),
              )
            }}

            getRowId={getRowId}
            statusFilter={{
              columnId: "status",
              options: [
                { value: "", label: "全部状态" },
                { value: "进行中", label: "进行中", tone: "var(--flow-green)" },
                { value: "待审核", label: "待审核", tone: "var(--flow-amber)" },
                {
                  value: "已归档",
                  label: "已归档",
                  tone: "var(--muted-foreground)",
                },
              ],
            }}
          />
        </GridSection>
      </GridContainer>
      <AlertDialog
        open={deleting.length > 0}
        onOpenChange={(open) => {
          if (!open) setDeleting([])
        }}
      >
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>删除数据集？</AlertDialogTitle>
            <AlertDialogDescription>
              {deleting.length === 1
                ? "将删除 1 个数据集：「" +
                  deleting[0]?.name +
                  "」，此操作无法撤销。"
                : "将删除 " + deleting.length + " 个数据集，此操作无法撤销。"}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel size="default">取消</AlertDialogCancel>
            <AlertDialogAction
              variant="destructive"
              size="default"
              onClick={() => {
                const ids = new Set(deleting.map((row) => row.id))
                setData((previous) =>
                  previous.filter((row) => !ids.has(row.id)),
                )
                setSelection({})
                setDeleting([])
                setNotice("已删除数据集")
              }}
            >
              删除
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      <p role="status" className="sr-only">
        {notice}
      </p>
    </main>
  )
}
export default DataTablePage
export { datasets, columns, getRowId }
export type { Dataset }
```

### `src/registry/hooks/use-controllable-state.ts`

```ts
"use client"

import * as React from "react"

type UseControllableStateParams<T> = {
  /** 受控值；传入后组件不再维护内部状态 */
  prop?: T
  /** 非受控时的初始值 */
  defaultProp: T
  /** 值变化时回调（受控与非受控都会触发） */
  onChange?: (value: T) => void
}

/**
 * 同时支持受控与非受控的状态 hook。
 * 约定见 component-conventions 技能第 4 节：open/defaultOpen/onOpenChange 等成对 props 都用它实现。
 */
function useControllableState<T>({
  prop,
  defaultProp,
  onChange,
}: UseControllableStateParams<T>) {
  const [uncontrolled, setUncontrolled] = React.useState(defaultProp)
  const isControlled = prop !== undefined
  const value = isControlled ? prop : uncontrolled

  // 用 ref 保存最新的回调和值，保证 setValue 引用稳定
  const onChangeRef = React.useRef(onChange)
  const valueRef = React.useRef(value)
  React.useEffect(() => {
    onChangeRef.current = onChange
    valueRef.current = value
  })

  const setValue = React.useCallback(
    (next: React.SetStateAction<T>) => {
      const resolved =
        typeof next === "function"
          ? (next as (prev: T) => T)(valueRef.current)
          : next
      if (Object.is(resolved, valueRef.current)) return
      valueRef.current = resolved
      if (!isControlled) setUncontrolled(resolved)
      onChangeRef.current?.(resolved)
    },
    [isControlled],
  )

  return [value, setValue] as const
}

export { useControllableState }
```

### `src/registry/blocks/data-table-01/data-table-row-actions.tsx`

```tsx
"use client"
import * as React from "react"
import { MoreHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
} from "@/components/ui/dropdown-menu"

type DataTableRowAction =
  | "separator"
  | {
      label: string
      icon?: React.ReactNode
      shortcut?: React.ReactNode
      destructive?: boolean
      onSelect: () => void
    }
function DataTableRowActions({
  label,
  actions,
  ...props
}: React.ComponentProps<"div"> & {
  label: string
  actions: DataTableRowAction[]
}) {
  return (
    <div {...props} data-slot="data-table-row-actions">
      <DropdownMenu>
        <DropdownMenuTrigger
          render={
            <Button
              variant="ghost"
              size="icon-sm"
              className="text-muted-foreground group-hover/row:text-foreground hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground"
              aria-label={label + " 的操作"}
            />
          }
        >
          <MoreHorizontal aria-hidden="true" />
        </DropdownMenuTrigger>
        <DropdownMenuContent
          align="end"
          className="w-auto min-w-40 *:text-xs! [&_[role=menuitem]_svg]:size-3.5!"
        >
          {actions.map((action, index) =>
            action === "separator" ? (
              <DropdownMenuSeparator key={index} />
            ) : (
              <DropdownMenuItem
                key={index}
                variant={action.destructive ? "destructive" : "default"}
                onClick={action.onSelect}
              >
                {action.icon}
                <span className="whitespace-nowrap">{action.label}</span>
                {action.shortcut && (
                  // 右侧附注（如编号）不换行，菜单宽度随内容撑开
                  <DropdownMenuShortcut className="ps-4 font-mono whitespace-nowrap">
                    {action.shortcut}
                  </DropdownMenuShortcut>
                )}
              </DropdownMenuItem>
            ),
          )}
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  )
}
export { DataTableRowActions }
export type { DataTableRowAction }
```
