{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-table-01",
  "title": "数据表格",
  "description": "支持筛选、选择、编辑与虚拟滚动的数据集表格。",
  "dependencies": [
    "@tanstack/react-table@^8.21.3",
    "@tanstack/react-virtual@^3.14.24",
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "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": [
    {
      "path": "src/registry/blocks/data-table-01/data-table.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport {\n  ArrowUpDown,\n  ArrowUp,\n  ArrowDown,\n  Minus,\n  Inbox,\n  SearchX,\n  AlertCircle,\n  X,\n} from \"lucide-react\"\nimport {\n  flexRender,\n  getCoreRowModel,\n  getFilteredRowModel,\n  getSortedRowModel,\n  getPaginationRowModel,\n  useReactTable,\n  type VisibilityState,\n} from \"@tanstack/react-table\"\nimport { useVirtualizer } from \"@tanstack/react-virtual\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport {\n  Sheet,\n  SheetContent,\n  SheetTitle,\n  SheetDescription,\n} from \"@/components/ui/sheet\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  Empty,\n  EmptyTitle,\n  EmptyMedia,\n  EmptyHeader,\n  EmptyDescription,\n} from \"@/components/ui/empty\"\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\nimport { DataTableCell } from \"./data-table-cell\"\nimport { DataTableToolbar } from \"./data-table-toolbar\"\nimport { DataTablePagination } from \"./data-table-pagination\"\nimport { DATA_TABLE_LABELS } from \"./config\"\nimport type { DataTableProps, DataTableMeta } from \"./types\"\n\n/** 虚拟滚动模式下窄屏卡片列表每批显示的条数 */\nconst MOBILE_LIST_BATCH = 30\n\nfunction DataTable<T>({\n  apiRef,\n  columns,\n  data,\n  getRowId,\n  mode = \"client\",\n  rowCount,\n  loading,\n  error,\n  onRetry,\n  filtered = false,\n  onClearFilters,\n  ariaLabel,\n  sorting: sortingProp,\n  defaultSorting = [],\n  onSortingChange,\n  pagination: paginationProp,\n  defaultPagination = { pageIndex: 0, pageSize: 10 },\n  onPaginationChange,\n  globalFilter: filterProp,\n  defaultGlobalFilter = \"\",\n  onGlobalFilterChange,\n  columnFilters: filtersProp,\n  defaultColumnFilters = [],\n  onColumnFiltersChange,\n  rowSelection: selectionProp,\n  defaultRowSelection = {},\n  onRowSelectionChange,\n  onSelectAll,\n  bulkActions,\n  rowActions,\n  onCellCommit,\n  searchLabel,\n  virtualization = false,\n  toolbar,\n  header,\n  counts,\n  statusFilter,\n  labels: overrides,\n  className,\n  ...props\n}: DataTableProps<T>) {\n  \"use no memo\"\n  // Table v8 的稳定可变对象不能交由 React Compiler 缓存读取。\n  const reducedMotion = useReducedMotion()\n  const labels = { ...DATA_TABLE_LABELS, ...overrides }\n  const [sorting, setSorting] = useControllableState({\n    prop: sortingProp,\n    defaultProp: defaultSorting,\n    onChange: onSortingChange,\n  })\n  const [pagination, setPagination] = useControllableState({\n    prop: paginationProp,\n    defaultProp: defaultPagination,\n    onChange: onPaginationChange,\n  })\n  const [globalFilter, setGlobalFilter] = useControllableState({\n    prop: filterProp,\n    defaultProp: defaultGlobalFilter,\n    onChange: onGlobalFilterChange,\n  })\n  const [columnFilters, setColumnFilters] = useControllableState({\n    prop: filtersProp,\n    defaultProp: defaultColumnFilters,\n    onChange: onColumnFiltersChange,\n  })\n  const [rowSelection, setRowSelection] = useControllableState({\n    prop: selectionProp,\n    defaultProp: defaultRowSelection,\n    onChange: onRowSelectionChange,\n  })\n  const [visibility, setVisibility] = React.useState<VisibilityState>({})\n  const [density, setDensity] = React.useState(\"52\")\n  // 虚拟滚动模式下，窄屏卡片列表分批显示：一次铺开上万行会让页面（含预渲染的 HTML）过大\n  const [listLimit, setListLimit] = React.useState(MOBILE_LIST_BATCH)\n  const [detailId, setDetailId] = React.useState<string | null>(null)\n  const longPress = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n  const [scrolled, setScrolled] = React.useState(false)\n  const [patches, setPatches] = React.useState<\n    Record<string, Record<string, unknown>>\n  >({})\n  const scrollRef = React.useRef<HTMLDivElement>(null)\n  const containerRef = React.useRef<HTMLDivElement>(null)\n  React.useImperativeHandle(apiRef, () => ({\n    startEditing(rowId, columnId) {\n      if ((containerRef.current?.getBoundingClientRect().width ?? 0) < 640) {\n        setDetailId(rowId)\n        requestAnimationFrame(() =>\n          requestAnimationFrame(() => {\n            const cell = Array.from(\n              document.querySelectorAll<HTMLElement>(\n                '[data-slot=\"sheet-content\"] [data-column]',\n              ),\n            ).find((node) => node.dataset.column === columnId)\n            cell?.dispatchEvent(new MouseEvent(\"dblclick\", { bubbles: true }))\n          }),\n        )\n        return\n      }\n      const row = Array.from(\n        containerRef.current?.querySelectorAll<HTMLElement>(\"[data-row-id]\") ??\n          [],\n      ).find((node) => node.dataset.rowId === rowId)\n      const cell = Array.from(\n        row?.querySelectorAll<HTMLElement>(\"[data-column]\") ?? [],\n      ).find((node) => node.dataset.column === columnId)\n      // 菜单关闭并恢复焦点后再启动已有的单元格编辑流程。\n      requestAnimationFrame(() =>\n        cell?.dispatchEvent(new MouseEvent(\"dblclick\", { bubbles: true })),\n      )\n    },\n    openDetails: setDetailId,\n  }))\n  const [desktop, setDesktop] = React.useState(false)\n  React.useEffect(() => {\n    const query = window.matchMedia(\"(min-width: 640px)\")\n    const update = () => setDesktop(query.matches)\n    update()\n    query.addEventListener(\"change\", update)\n    return () => query.removeEventListener(\"change\", update)\n  }, [])\n  const touchedVisibility = React.useRef(false)\n  React.useEffect(() => {\n    const observer = new ResizeObserver(([entry]) => {\n      if (!entry || touchedVisibility.current) return\n      setVisibility(\n        Object.fromEntries(\n          columns.map((column) => [\n            column.id ??\n              (\"accessorKey\" in column ? String(column.accessorKey) : \"\"),\n            !(column.meta?.hideOnMobile && entry.contentRect.width < 640),\n          ]),\n        ),\n      )\n    })\n    if (containerRef.current) observer.observe(containerRef.current)\n    return () => observer.disconnect()\n  }, [columns])\n  const displayData = React.useMemo(\n    () => data.map((row) => ({ ...row, ...patches[getRowId(row)] })),\n    [data, patches, getRowId],\n  )\n  const sizedColumns = React.useMemo(\n    () =>\n      columns.map((column) => ({\n        ...column,\n        size: column.meta?.width ?? column.size,\n      })),\n    [columns],\n  )\n  // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Table v8 返回可变实例，组件已用 \"use no memo\" 退出编译器优化\n  const table = useReactTable({\n    data: displayData,\n    columns: sizedColumns,\n    getRowId,\n    state: {\n      sorting,\n      pagination,\n      globalFilter,\n      columnFilters,\n      rowSelection,\n      columnVisibility: visibility,\n    },\n    onSortingChange: setSorting,\n    onPaginationChange: setPagination,\n    onGlobalFilterChange: setGlobalFilter,\n    onColumnFiltersChange: setColumnFilters,\n    onRowSelectionChange: setRowSelection,\n    onColumnVisibilityChange: (updater) => {\n      touchedVisibility.current = true\n      setVisibility(updater)\n    },\n    getCoreRowModel: getCoreRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getPaginationRowModel: virtualization ? undefined : getPaginationRowModel(),\n    manualSorting: mode === \"manual\",\n    manualFiltering: mode === \"manual\",\n    manualPagination: mode === \"manual\",\n    rowCount: mode === \"manual\" ? rowCount : undefined,\n    autoResetPageIndex: false,\n    columnResizeMode: \"onChange\",\n    defaultColumn: { size: 160, minSize: 80, maxSize: 600 },\n  })\n  const rows = table.getRowModel().rows\n  const virtualizer = useVirtualizer({\n    count: rows.length,\n    getScrollElement: () => scrollRef.current,\n    estimateSize: () =>\n      (virtualization && virtualization.estimateRowHeight) || Number(density),\n    overscan: 8,\n    enabled: !!virtualization,\n  })\n  const items = virtualizer.getVirtualItems()\n  const shown = virtualization\n    ? items.flatMap((item) => {\n        const row = rows[item.index]\n        return row ? [{ row, index: item.index }] : []\n      })\n    : rows.map((row, index) => ({ row, index }))\n  const selected = Object.values(rowSelection).filter(Boolean).length\n  const count =\n    mode === \"manual\" && rowCount !== undefined\n      ? rowCount\n      : table.getFilteredRowModel().rows.length\n  const span = table.getVisibleLeafColumns().length + 1 + Number(!!rowActions)\n  const pinned = table\n    .getVisibleLeafColumns()\n    .find((column) => (column.columnDef.meta as DataTableMeta<T>)?.pinned)?.id\n  async function commitCell(rowId: string, columnId: string, value: unknown) {\n    const row = rows.find((row) => row.id === rowId)!\n    const column = table.getColumn(columnId)!\n    const key =\n      \"accessorKey\" in column.columnDef\n        ? String(column.columnDef.accessorKey)\n        : columnId\n    const previous =\n      patches[rowId]?.[key] ?? (row.original as Record<string, unknown>)[key]\n    setPatches((old) => ({ ...old, [rowId]: { ...old[rowId], [key]: value } }))\n    try {\n      await onCellCommit?.({ rowId, columnId, value, row: row.original })\n    } catch (reason) {\n      setPatches((old) => ({\n        ...old,\n        [rowId]: { ...old[rowId], [key]: previous },\n      }))\n      throw reason\n    }\n  }\n  const detail = rows.find((row) => row.id === detailId)\n  // 外部筛选（工具栏插槽里的、页面持有的）表格看不到，由 filtered 告知\n  const hasFilter = !!(globalFilter || columnFilters.length || filtered)\n  const listLabel = ariaLabel ?? labels.dataset\n  function clearFilters() {\n    // 先重置表格自己的搜索、列筛选和页码，再通知外部：回调里触发的副作用不会和旧的内部状态交错\n    setGlobalFilter(\"\")\n    setColumnFilters([])\n    table.setPageIndex(0)\n    onClearFilters?.()\n  }\n  return (\n    <div\n      ref={containerRef}\n      data-slot=\"data-table\"\n      className={cn(\n        \"@container w-full min-w-0 bg-background text-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {header}\n      <DataTableToolbar\n        table={table}\n        labels={labels}\n        density={density}\n        onDensityChange={setDensity}\n        statusFilter={statusFilter}\n        searchLabel={searchLabel}\n        counts={\n          mode === \"manual\"\n            ? counts\n            : statusFilter\n              ? Object.fromEntries(\n                  statusFilter.options.map((option) => [\n                    option.value,\n                    option.value\n                      ? displayData.filter(\n                          (row) =>\n                            String(\n                              (row as Record<string, unknown>)[\n                                statusFilter.columnId\n                              ],\n                            ) === option.value,\n                        ).length\n                      : displayData.length,\n                  ]),\n                )\n              : undefined\n        }\n      >\n        {typeof toolbar === \"function\" ? toolbar(table) : toolbar}\n      </DataTableToolbar>\n      <div\n        ref={scrollRef}\n        data-slot=\"data-table-scroll\"\n        data-scrolled={scrolled}\n        onScroll={(event) => setScrolled(event.currentTarget.scrollLeft > 0)}\n        className=\"relative hidden w-full max-w-full min-w-0 overflow-auto border-t border-edge @min-[40rem]:block\"\n        style={{\n          height: virtualization ? (virtualization.height ?? 480) : undefined,\n          maxHeight: virtualization ? undefined : 600,\n        }}\n      >\n        <table\n          aria-label={listLabel}\n          aria-rowcount={count + 1}\n          // 最后一行不画虚线，底边由分页栏的实线收口，避免虚线 + 实线叠成两道\n          className=\"table-fixed border-separate border-spacing-0 text-sm [&>tbody>tr:last-child>td]:border-b-0\"\n          style={\n            {\n              width: \"100%\",\n              minWidth: table.getTotalSize() + 40 + (rowActions ? 64 : 0),\n              \"--data-table-row-height\": density + \"px\",\n            } as React.CSSProperties\n          }\n        >\n          <colgroup>\n            <col style={{ width: 40 }} />\n            {table.getVisibleLeafColumns().map((column) => (\n              <col\n                key={column.id}\n                style={{\n                  width:\n                    column.id === table.getVisibleLeafColumns()[0]?.id\n                      ? undefined\n                      : column.getSize(),\n                }}\n              />\n            ))}\n            {rowActions && <col style={{ width: 64 }} />}\n          </colgroup>\n          <thead className=\"sticky top-0 z-20 bg-muted/50 [&_th]:border-b [&_th]:border-edge\">\n            <tr>\n              <th className=\"sticky left-0 z-30 w-12 bg-muted/50 px-3\">\n                <span className=\"relative flex size-4\">\n                  <Checkbox\n                    className={\n                      table.getIsSomePageRowsSelected()\n                        ? \"border-primary bg-primary dark:bg-primary [&_[data-slot=checkbox-indicator]]:invisible\"\n                        : undefined\n                    }\n                    aria-label={labels.selectPage}\n                    checked={table.getIsAllPageRowsSelected()}\n                    indeterminate={table.getIsSomePageRowsSelected()}\n                    onCheckedChange={(value) =>\n                      table.toggleAllPageRowsSelected(!!value)\n                    }\n                  />\n                  {table.getIsSomePageRowsSelected() && (\n                    <Minus\n                      aria-hidden=\"true\"\n                      className=\"pointer-events-none absolute inset-0 size-4 text-primary-foreground\"\n                    />\n                  )}\n                </span>\n              </th>\n              {table.getHeaderGroups()[0]?.headers.map((header, i, all) => (\n                <th\n                  key={header.id}\n                  aria-sort={\n                    header.column.getIsSorted() === \"asc\"\n                      ? \"ascending\"\n                      : header.column.getIsSorted() === \"desc\"\n                        ? \"descending\"\n                        : \"none\"\n                  }\n                  className={cn(\n                    \"relative bg-muted/50 px-3 text-left\",\n                    // 与表体一致：操作列前的分隔线画在最后一个数据列的右边\n                    rowActions &&\n                      i === all.length - 1 &&\n                      \"border-r [border-right-style:dashed] border-edge\",\n                    header.id === pinned && \"sticky left-10 z-30\",\n                    header.id === pinned &&\n                      scrolled &&\n                      \"shadow-[4px_0_6px_-4px_var(--border)]\",\n                  )}\n                  style={{\n                    width: header.index === 0 ? undefined : header.getSize(),\n                    minWidth: header.index === 0 ? undefined : header.getSize(),\n                  }}\n                >\n                  <Button\n                    variant=\"ghost\"\n                    className={cn(\n                      \"w-full justify-start px-0\",\n                      (header.column.columnDef.meta as DataTableMeta<T>)\n                        ?.align === \"right\" && \"justify-end\",\n                    )}\n                    disabled={!header.column.getCanSort()}\n                    onClick={header.column.getToggleSortingHandler()}\n                  >\n                    {flexRender(\n                      header.column.columnDef.header,\n                      header.getContext(),\n                    )}{\" \"}\n                    {header.column.getCanSort() &&\n                      (header.column.getIsSorted() === \"asc\" ? (\n                        <ArrowUp aria-hidden=\"true\" className=\"size-3.5\" />\n                      ) : header.column.getIsSorted() === \"desc\" ? (\n                        <ArrowDown aria-hidden=\"true\" className=\"size-3.5\" />\n                      ) : (\n                        <ArrowUpDown\n                          aria-hidden=\"true\"\n                          className=\"size-3.5 text-muted-foreground\"\n                        />\n                      ))}\n                  </Button>\n                  <div\n                    role=\"separator\"\n                    aria-label={String(header.column.columnDef.header)}\n                    aria-orientation=\"vertical\"\n                    aria-valuenow={header.getSize()}\n                    aria-valuemin={80}\n                    aria-valuemax={600}\n                    tabIndex={0}\n                    onMouseDown={header.getResizeHandler()}\n                    onKeyDown={(event) => {\n                      if ([\"ArrowLeft\", \"ArrowRight\"].includes(event.key)) {\n                        event.preventDefault()\n                        table.setColumnSizing((old) => ({\n                          ...old,\n                          [header.id]: Math.max(\n                            80,\n                            Math.min(\n                              600,\n                              header.getSize() +\n                                (event.key === \"ArrowRight\" ? 16 : -16),\n                            ),\n                          ),\n                        }))\n                      }\n                    }}\n                    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\"\n                  />\n                </th>\n              ))}\n              {rowActions && (\n                <th className=\"w-16 bg-muted text-center text-xs font-medium\">\n                  {labels.actions}\n                </th>\n              )}\n            </tr>\n          </thead>\n          <tbody>\n            {loading ? (\n              Array.from({ length: pagination.pageSize }, (_, i) => (\n                <tr key={i}>\n                  {Array.from({ length: span }, (_, j) => (\n                    <td key={j} className=\"p-3\">\n                      {j === 0 ? (\n                        <span className=\"font-mono text-muted-foreground\">\n                          –\n                        </span>\n                      ) : (\n                        <Skeleton className=\"h-4 w-full\" />\n                      )}\n                    </td>\n                  ))}\n                </tr>\n              ))\n            ) : error ? (\n              <tr>\n                <td colSpan={span}>\n                  <Empty className=\"sticky left-0 min-h-60 w-[100cqw] max-w-full bg-dots\">\n                    <EmptyHeader>\n                      <EmptyMedia variant=\"icon\">\n                        <AlertCircle aria-hidden=\"true\" />\n                      </EmptyMedia>\n                      <EmptyTitle>{error}</EmptyTitle>\n                      <EmptyDescription>\n                        {labels.errorDescription}\n                      </EmptyDescription>\n                    </EmptyHeader>\n                    {onRetry && (\n                      <Button onClick={onRetry}>{labels.retry}</Button>\n                    )}\n                  </Empty>\n                </td>\n              </tr>\n            ) : !rows.length ? (\n              <tr>\n                <td colSpan={span}>\n                  <Empty className=\"sticky left-0 min-h-60 w-[100cqw] max-w-full bg-dots\">\n                    <EmptyHeader>\n                      <EmptyMedia variant=\"icon\">\n                        {hasFilter ? (\n                          <SearchX aria-hidden=\"true\" />\n                        ) : (\n                          <Inbox aria-hidden=\"true\" />\n                        )}\n                      </EmptyMedia>\n                      <EmptyTitle>\n                        {hasFilter ? labels.noResults : labels.empty}\n                      </EmptyTitle>\n                      <EmptyDescription>\n                        {hasFilter\n                          ? labels.noResultsDescription\n                          : labels.emptyDescription}\n                      </EmptyDescription>\n                    </EmptyHeader>\n                    {hasFilter && (\n                      <Button variant=\"outline\" onClick={clearFilters}>\n                        {labels.clearFilters}\n                      </Button>\n                    )}\n                  </Empty>\n                </td>\n              </tr>\n            ) : (\n              <>\n                {!!virtualization && items.length > 0 && (\n                  <tr aria-hidden=\"true\">\n                    <td colSpan={span} style={{ height: items[0]?.start }} />\n                  </tr>\n                )}\n                {shown.map(({ row, index }) => (\n                  <tr\n                    key={row.id}\n                    data-row-id={row.id}\n                    onKeyDown={(event) => {\n                      if (\n                        (event.shiftKey && event.key === \"F10\") ||\n                        event.key === \"ContextMenu\"\n                      ) {\n                        const trigger =\n                          event.currentTarget.querySelector<HTMLButtonElement>(\n                            '[data-slot=\"data-table-row-actions\"] button',\n                          )\n                        if (trigger) {\n                          event.preventDefault()\n                          trigger.click()\n                        }\n                      }\n                    }}\n                    aria-rowindex={\n                      virtualization\n                        ? index + 2\n                        : pagination.pageIndex * pagination.pageSize + index + 2\n                    }\n                    className=\"group/row\"\n                    data-state={row.getIsSelected() ? \"selected\" : undefined}\n                  >\n                    <td\n                      className={cn(\n                        \"group/number sticky left-0 z-10 w-10 border-r border-b border-dashed border-edge px-3\",\n                        row.getIsSelected()\n                          ? \"bg-primary/[0.04]\"\n                          : \"bg-background\",\n                      )}\n                    >\n                      <span\n                        aria-hidden=\"true\"\n                        className={cn(\n                          \"pointer-events-none absolute inset-0\",\n                          row.getIsSelected()\n                            ? \"border-l-2 border-primary bg-hatch\"\n                            : \"group-focus-within/number:bg-dots group-hover/row:bg-dots\",\n                        )}\n                      />\n                      <span\n                        aria-hidden=\"true\"\n                        className={cn(\n                          \"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\",\n                          row.getIsSelected() && \"opacity-0\",\n                        )}\n                      >\n                        {String(index + 1).padStart(2, \"0\")}\n                      </span>\n                      <Checkbox\n                        className={cn(\n                          \"relative opacity-0 group-focus-within/number:opacity-100 group-hover/row:opacity-100\",\n                          row.getIsSelected() && \"opacity-100\",\n                        )}\n                        aria-label={labels.selectRow + \" \" + (index + 1)}\n                        checked={row.getIsSelected()}\n                        onCheckedChange={(value) => row.toggleSelected(!!value)}\n                      />\n                    </td>\n                    {row.getVisibleCells().map((cell) => (\n                      <td\n                        key={cell.id}\n                        style={{\n                          height: Number(density),\n                          width:\n                            cell.column.id ===\n                            table.getVisibleLeafColumns()[0]?.id\n                              ? undefined\n                              : cell.column.getSize(),\n                          maxWidth:\n                            cell.column.id ===\n                            table.getVisibleLeafColumns()[0]?.id\n                              ? undefined\n                              : cell.column.getSize(),\n                          textAlign: (\n                            cell.column.columnDef.meta as DataTableMeta<T>\n                          )?.align,\n                        }}\n                        className={cn(\n                          \"border-r border-b border-dashed border-edge px-3 py-0\",\n                          (cell.column.columnDef.meta as DataTableMeta<T>)\n                            ?.align === \"right\" && \"font-mono tabular-nums\",\n                          row.getIsSelected()\n                            ? \"bg-primary/[0.04]\"\n                            : \"bg-background\",\n                          cell.column.id === pinned && \"sticky left-10 z-10\",\n                          cell.column.id === pinned &&\n                            scrolled &&\n                            \"shadow-[4px_0_6px_-4px_var(--border)]\",\n                        )}\n                      >\n                        <DataTableCell\n                          value={cell.getValue()}\n                          row={row.original}\n                          meta={cell.column.columnDef.meta as DataTableMeta<T>}\n                          title={String(cell.column.columnDef.header)}\n                          labels={labels}\n                          cellKey={cell.column.id}\n                          rowHeight={Number(density)}\n                          onCommit={(value) =>\n                            commitCell(row.id, cell.column.id, value)\n                          }\n                        >\n                          {flexRender(\n                            cell.column.columnDef.cell,\n                            cell.getContext(),\n                          )}\n                          {(cell.column.columnDef.meta as DataTableMeta<T>)\n                            ?.bar && (\n                            <span\n                              aria-hidden=\"true\"\n                              className=\"mt-2 block h-0.5 bg-edge\"\n                            >\n                              <span\n                                className=\"block h-full bg-primary/70\"\n                                style={{\n                                  width:\n                                    Math.max(\n                                      0,\n                                      Math.min(\n                                        100,\n                                        (Number(cell.getValue()) /\n                                          Math.max(\n                                            1,\n                                            (\n                                              cell.column.columnDef\n                                                .meta as DataTableMeta<T>\n                                            )?.max ??\n                                              Math.max(\n                                                ...displayData.map(\n                                                  (row) =>\n                                                    Number(\n                                                      (\n                                                        row as Record<\n                                                          string,\n                                                          unknown\n                                                        >\n                                                      )[cell.column.id],\n                                                    ) || 0,\n                                                ),\n                                              ),\n                                          )) *\n                                          100,\n                                      ),\n                                    ) + \"%\",\n                                }}\n                              />\n                            </span>\n                          )}\n                        </DataTableCell>\n                      </td>\n                    ))}\n                    {rowActions && (\n                      <td className=\"w-16 border-b border-dashed border-edge text-center\">\n                        {/* 左侧分隔线由前一格的 border-r 提供（表头同理），这里不再画 */}\n                        {/* 操作按钮常驻显示（淡色），悬停行时加深 */}\n                        {rowActions(row.original)}\n                      </td>\n                    )}\n                  </tr>\n                ))}\n                {/* 分页模式下最后一页不足一页时用空白补齐高度，翻页时分页条不会上下跳 */}\n                {!virtualization &&\n                  table.getPageCount() > 1 &&\n                  rows.length < pagination.pageSize && (\n                    <tr aria-hidden=\"true\" data-slot=\"data-table-filler\">\n                      <td\n                        colSpan={span}\n                        style={{\n                          height:\n                            (pagination.pageSize - rows.length) *\n                            Number(density),\n                        }}\n                      />\n                    </tr>\n                  )}\n                {!!virtualization && items.length > 0 && (\n                  <tr aria-hidden=\"true\">\n                    <td\n                      colSpan={span}\n                      style={{\n                        height:\n                          virtualizer.getTotalSize() - (items.at(-1)?.end ?? 0),\n                      }}\n                    />\n                  </tr>\n                )}\n              </>\n            )}\n          </tbody>\n        </table>\n      </div>\n      <ul\n        data-slot=\"data-table-list\"\n        aria-label={listLabel}\n        className=\"border-t border-edge @min-[40rem]:hidden\"\n      >\n        {loading ? (\n          Array.from({ length: pagination.pageSize }, (_, index) => (\n            <li\n              key={index}\n              className=\"flex gap-4 border-b border-dashed border-edge p-4\"\n            >\n              <span className=\"font-mono\">–</span>\n              <Skeleton className=\"h-10 flex-1\" />\n            </li>\n          ))\n        ) : !rows.length || error ? (\n          <li className=\"bg-dots p-10 text-center\">\n            <Inbox aria-hidden=\"true\" className=\"mx-auto mb-3 size-6\" />\n            <p>{error || (hasFilter ? labels.noResults : labels.empty)}</p>\n            {(error ? onRetry : hasFilter) && (\n              <Button\n                variant=\"outline\"\n                onClick={error ? onRetry : clearFilters}\n              >\n                {error ? labels.retry : labels.clearFilters}\n              </Button>\n            )}\n          </li>\n        ) : (\n          (virtualization ? rows.slice(0, listLimit) : rows).map(\n            (row, index) => {\n              const cells = row.getAllCells()\n              const name = cells[0]\n              const status = cells.find(\n                (cell) => cell.column.id === statusFilter?.columnId,\n              )\n              const rest = cells\n                .filter((cell) => cell !== name && cell !== status)\n                .slice(0, 3)\n              return (\n                <li\n                  key={row.id}\n                  className={cn(\n                    \"group/row flex border-b border-dashed border-edge last:border-b-0\",\n                    row.getIsSelected() && \"bg-primary/[0.04]\",\n                  )}\n                >\n                  <div\n                    className={cn(\n                      \"group/number relative flex w-10 shrink-0 items-center justify-center border-r border-dashed border-edge\",\n                      row.getIsSelected()\n                        ? \"bg-hatch\"\n                        : \"focus-within:bg-dots hover:bg-dots\",\n                    )}\n                    onPointerDown={() => {\n                      longPress.current = setTimeout(\n                        () => row.toggleSelected(true),\n                        500,\n                      )\n                    }}\n                    onPointerUp={() => {\n                      if (longPress.current) clearTimeout(longPress.current)\n                    }}\n                    onPointerCancel={() => {\n                      if (longPress.current) clearTimeout(longPress.current)\n                    }}\n                    onPointerLeave={() => {\n                      if (longPress.current) clearTimeout(longPress.current)\n                    }}\n                  >\n                    {row.getIsSelected() && (\n                      <span\n                        aria-hidden=\"true\"\n                        className=\"pointer-events-none absolute inset-y-0 left-0 w-0.5 bg-primary\"\n                      />\n                    )}\n                    <span\n                      aria-hidden=\"true\"\n                      className={cn(\n                        \"font-mono text-[10px] text-muted-foreground group-focus-within/number:opacity-0 group-hover/number:opacity-0\",\n                        row.getIsSelected() && \"opacity-0\",\n                      )}\n                    >\n                      {String(index + 1).padStart(2, \"0\")}\n                    </span>\n                    <Checkbox\n                      aria-label={labels.selectRow + \" \" + (index + 1)}\n                      checked={row.getIsSelected()}\n                      onCheckedChange={(value) => row.toggleSelected(!!value)}\n                      className={cn(\n                        \"absolute opacity-0 group-focus-within/number:opacity-100 group-hover/number:opacity-100\",\n                        row.getIsSelected() && \"opacity-100\",\n                      )}\n                    />\n                  </div>\n                  <button\n                    type=\"button\"\n                    className=\"min-w-0 flex-1 p-3 text-left focus-visible:outline-2 focus-visible:outline-ring\"\n                    onClick={() => setDetailId(row.id)}\n                  >\n                    <span className=\"flex items-center justify-between gap-2\">\n                      <span className=\"truncate font-medium\">\n                        {String(name?.getValue() ?? \"\")}\n                      </span>\n                      <span className=\"shrink-0 text-xs\">\n                        {status &&\n                          flexRender(\n                            status.column.columnDef.cell,\n                            status.getContext(),\n                          )}\n                      </span>\n                    </span>\n                    <span className=\"mt-2 flex items-center gap-3 font-mono text-[10px] text-muted-foreground\">\n                      {rest.map((cell, index) => (\n                        <span\n                          key={cell.id}\n                          className={index === rest.length - 1 ? \"ml-auto\" : \"\"}\n                        >\n                          {flexRender(\n                            cell.column.columnDef.cell,\n                            cell.getContext(),\n                          )}\n                        </span>\n                      ))}\n                    </span>\n                  </button>\n                  {rowActions && (\n                    <div className=\"flex shrink-0 items-center pr-2\">\n                      {rowActions(row.original)}\n                    </div>\n                  )}\n                </li>\n              )\n            },\n          )\n        )}\n        {virtualization && !loading && !error && rows.length > listLimit && (\n          <li className=\"p-4\">\n            <Button\n              variant=\"outline\"\n              size=\"lg\"\n              className=\"w-full\"\n              onClick={() => setListLimit((limit) => limit + MOBILE_LIST_BATCH)}\n            >\n              {labels.showMore(rows.length - listLimit)}\n            </Button>\n          </li>\n        )}\n      </ul>\n      <Sheet\n        open={!!detail}\n        onOpenChange={(open) => {\n          if (!open) setDetailId(null)\n        }}\n      >\n        <SheetContent\n          side={desktop ? \"right\" : \"bottom\"}\n          className=\"max-h-[85dvh] overflow-auto p-6 sm:max-h-none\"\n        >\n          <SheetTitle>\n            {String(detail?.getAllCells()[0]?.getValue() ?? \"\")}\n          </SheetTitle>\n          <SheetDescription>{labels.detailDescription}</SheetDescription>\n          <div className=\"divide-y divide-dashed divide-edge\">\n            {detail?.getAllCells().map((cell) => (\n              <div key={cell.id} className=\"py-3\">\n                <p className=\"mb-1 text-xs text-muted-foreground\">\n                  {String(cell.column.columnDef.header)}\n                </p>\n                <DataTableCell\n                  value={cell.getValue()}\n                  row={detail.original}\n                  meta={cell.column.columnDef.meta as DataTableMeta<T>}\n                  title={String(cell.column.columnDef.header)}\n                  labels={labels}\n                  cellKey={cell.column.id}\n                  rowHeight={52}\n                  onCommit={(value) =>\n                    commitCell(detail.id, cell.column.id, value)\n                  }\n                >\n                  {(\n                    cell.column.columnDef.meta as DataTableMeta<T>\n                  )?.detailCell?.(cell.getValue(), detail.original) ??\n                    flexRender(cell.column.columnDef.cell, cell.getContext())}\n                </DataTableCell>\n              </div>\n            ))}\n          </div>\n        </SheetContent>\n      </Sheet>\n      <AnimatePresence>\n        {selected > 0 && (\n          <motion.div\n            role=\"toolbar\"\n            aria-label=\"批量操作\"\n            initial={{ opacity: 0, y: reducedMotion ? 0 : 12 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0, y: reducedMotion ? 0 : 12 }}\n            transition={{ duration: 0.18 }}\n            // 高度 2.75rem，用负的上边距抵消占位：浮在表格底部，不把分页栏往下推\n            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\"\n          >\n            <span aria-live=\"polite\" className=\"shrink-0 font-mono\">\n              {labels.selected} {selected}\n              <span className=\"hidden @min-[40rem]:inline\">\n                {\" \"}\n                / {rowCount ?? data.length}\n              </span>\n            </span>\n            {selected < (rowCount ?? data.length) && (\n              <Button\n                variant=\"ghost\"\n                className=\"shrink-0 px-2 font-normal text-background/70 hover:bg-background/15 hover:text-background\"\n                onClick={() =>\n                  onSelectAll\n                    ? onSelectAll()\n                    : table.toggleAllRowsSelected(true)\n                }\n              >\n                <span className=\"hidden @min-[40rem]:inline\">\n                  {labels.selectAll} {rowCount ?? data.length} {labels.items}\n                </span>\n                <span className=\"@min-[40rem]:hidden\">\n                  {labels.selectAllCompact} {rowCount ?? data.length}\n                </span>\n              </Button>\n            )}\n            <span\n              aria-hidden=\"true\"\n              className=\"mx-1 h-5 w-px bg-background/20\"\n            />\n            {/* 反色操作坞：覆盖按钮配色保持对比；窄容器只留图标（文字放在 data-label 里） */}\n            <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\">\n              {bulkActions?.(\n                table.getSelectedRowModel().rows.map((row) => row.original),\n              )}\n            </div>\n            <span\n              aria-hidden=\"true\"\n              className=\"mx-1 h-5 w-px bg-background/20\"\n            />\n            <Button\n              size=\"icon\"\n              variant=\"ghost\"\n              aria-label={labels.clear}\n              className=\"shrink-0 text-background/70 hover:bg-background/15 hover:text-background\"\n              onClick={() => setRowSelection({})}\n            >\n              <X />\n            </Button>\n          </motion.div>\n        )}\n      </AnimatePresence>\n      <DataTablePagination\n        table={table}\n        labels={labels}\n        virtual={!!virtualization}\n        count={count}\n      />\n    </div>\n  )\n}\nexport { DataTable }\nexport type { DataTableProps, DataTableColumn, DataTableLabels } from \"./types\"\n",
      "type": "registry:component",
      "target": "@components/data-table-01/data-table.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/data-table-toolbar.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport type { Table } from \"@tanstack/react-table\"\nimport { ArrowUpDown, Search, Settings2 } from \"lucide-react\"\nimport { BarField } from \"@/components/ui/bar-field/bar-field\"\nimport { BarSelect } from \"@/components/ui/bar-select/bar-select\"\nimport { SegmentedControl } from \"@/components/ui/segmented-control/segmented-control\"\nimport {\n  GridToolbar,\n  GridToolbarItem,\n} from \"@/components/ui/grid-toolbar/grid-toolbar\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuCheckboxItem,\n  DropdownMenuSeparator,\n  DropdownMenuItem,\n} from \"@/components/ui/dropdown-menu\"\nimport type { DataTableLabels, DataTableProps } from \"./types\"\n\n/**\n * 密度图标：同样高度里画几条横线，线越多越紧凑。\n * 不带外框，免得读成数据表格。\n */\nfunction DensityIcon({ lines }: { lines: number }) {\n  const top = 5\n  const bottom = 19\n  const step = (bottom - top) / (lines - 1)\n  return (\n    <svg\n      data-slot=\"data-table-density-icon\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      aria-hidden=\"true\"\n      className=\"size-4\"\n    >\n      {Array.from({ length: lines }, (_, i) => (\n        <path key={i} d={`M4 ${top + step * i}h16`} />\n      ))}\n    </svg>\n  )\n}\n\nfunction DataTableToolbar<T>({\n  table,\n  labels,\n  density,\n  onDensityChange,\n  statusFilter,\n  children,\n  searchLabel,\n  counts,\n}: {\n  table: Table<T>\n  labels: DataTableLabels\n  density: string\n  onDensityChange: (value: string) => void\n  statusFilter?: DataTableProps<T>[\"statusFilter\"]\n  counts?: Record<string, number>\n  searchLabel?: React.ReactNode\n  children?: React.ReactNode\n}) {\n  \"use no memo\"\n  // Table v8 的对象引用稳定，子组件同样需要读取当前状态。\n  const searchRef = React.useRef<HTMLInputElement>(null)\n  React.useEffect(() => {\n    function focusSearch(event: KeyboardEvent) {\n      if (\n        event.key !== \"/\" ||\n        event.ctrlKey ||\n        event.metaKey ||\n        event.altKey ||\n        (event.target instanceof HTMLElement &&\n          (event.target.isContentEditable ||\n            event.target.closest(\"input,textarea,select\")))\n      )\n        return\n      event.preventDefault()\n      searchRef.current?.focus()\n    }\n    window.addEventListener(\"keydown\", focusSearch)\n    return () => window.removeEventListener(\"keydown\", focusSearch)\n  }, [])\n  const statusColumn = statusFilter\n    ? table.getColumn(statusFilter.columnId)\n    : undefined\n  const status = String(statusColumn?.getFilterValue() ?? \"\")\n  // 有第二行内容（状态筛选或插槽）时，窄容器里搜索格独占一行，其余格在下一行平分；\n  // 只有搜索和两个图标按钮时仍保持一行，不多占高度\n  const stacked = !!statusFilter || !!children\n  return (\n    <GridToolbar\n      bordered={false}\n      // 表格工具栏统一 12px；触屏输入框保持 16px，避免 iOS 聚焦时页面放大\n      className=\"[&_*]:text-xs! pointer-coarse:[&_input]:text-base!\"\n    >\n      {/* 搜索：宽容器里定宽；窄容器里有第二行内容时独占第一行，否则和图标按钮同行 */}\n      <GridToolbarItem\n        grow\n        fullOnNarrow={stacked}\n        className=\"@min-[40rem]:w-64 @min-[40rem]:flex-none\"\n      >\n        <BarField\n          label={searchLabel}\n          aria-label={searchLabel ? undefined : labels.searchField}\n          icon={<Search />}\n          inputRef={searchRef}\n          type=\"search\"\n          placeholder={labels.search}\n          value={table.getState().globalFilter ?? \"\"}\n          onValueChange={(value) => {\n            table.setGlobalFilter(value)\n            table.setPageIndex(0)\n          }}\n          suffix={\n            <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\">\n              /\n            </kbd>\n          }\n        />\n      </GridToolbarItem>\n      {statusFilter && (\n        // 状态筛选用方条下拉：清除即「全部」；窄容器里占满第二行剩余宽度，右边是列设置与排序\n        <GridToolbarItem\n          grow\n          className=\"@min-[40rem]:w-36 @min-[40rem]:flex-none\"\n        >\n          <BarSelect\n            aria-label={labels.status}\n            placeholder={\n              statusFilter.options.find((option) => option.value === \"\")\n                ?.label ?? labels.all\n            }\n            value={status || null}\n            contentClassName=\"[&_*]:text-xs\"\n            options={statusFilter.options\n              .filter((option) => option.value !== \"\")\n              .map((option) => ({\n                value: option.value,\n                textValue: option.label,\n                count: counts?.[option.value] ?? 0,\n                label: (\n                  <span className=\"flex items-center gap-2\">\n                    {option.tone && (\n                      <span\n                        aria-hidden=\"true\"\n                        className=\"size-1.5 shrink-0 rounded-full\"\n                        style={{ background: option.tone }}\n                      />\n                    )}\n                    {option.label}\n                  </span>\n                ),\n              }))}\n            onValueChange={(value) => {\n              statusColumn?.setFilterValue(value || undefined)\n              table.setPageIndex(0)\n            }}\n          />\n        </GridToolbarItem>\n      )}\n      {children}\n      {/* 空白格：吃掉剩余宽度，让列设置与密度靠右 */}\n      <GridToolbarItem\n        grow\n        aria-hidden=\"true\"\n        className=\"hidden @min-[40rem]:flex\"\n      />\n      <GridToolbarItem>\n        <DropdownMenu>\n          <Tooltip>\n            <TooltipTrigger\n              render={\n                <DropdownMenuTrigger\n                  render={\n                    <Button\n                      variant=\"ghost\"\n                      size=\"icon-lg\"\n                      aria-label={labels.columns}\n                    />\n                  }\n                />\n              }\n            >\n              <Settings2 aria-hidden=\"true\" />\n            </TooltipTrigger>\n            <TooltipContent>{labels.columns}</TooltipContent>\n          </Tooltip>\n          <DropdownMenuContent align=\"end\" className=\"min-w-44 *:text-xs!\">\n            {table.getAllLeafColumns().map((column) => (\n              <DropdownMenuCheckboxItem\n                key={column.id}\n                checked={column.getIsVisible()}\n                onCheckedChange={(value) => column.toggleVisibility(value)}\n                closeOnClick={false}\n              >\n                {String(column.columnDef.header ?? column.id)}\n              </DropdownMenuCheckboxItem>\n            ))}\n            <DropdownMenuSeparator />\n            <DropdownMenuItem onClick={() => table.resetColumnVisibility()}>\n              {labels.reset}\n            </DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </GridToolbarItem>\n      {/* 密度是另一组：用实线与列设置分开；只用图标，文字在提示气泡和读屏里 */}\n      <GridToolbarItem separator=\"solid\" className=\"hidden @min-[40rem]:flex\">\n        <SegmentedControl\n          aria-label={labels.density}\n          iconOnly\n          value={density}\n          options={[\n            {\n              value: \"40\",\n              label: labels.compact,\n              icon: <DensityIcon lines={4} />,\n            },\n            {\n              value: \"52\",\n              label: labels.standard,\n              icon: <DensityIcon lines={3} />,\n            },\n            {\n              value: \"64\",\n              label: labels.comfortable,\n              icon: <DensityIcon lines={2} />,\n            },\n          ]}\n          onValueChange={onDensityChange}\n        />\n      </GridToolbarItem>\n      <GridToolbarItem className=\"@min-[40rem]:hidden\">\n        <DropdownMenu>\n          <Tooltip>\n            <TooltipTrigger\n              render={\n                <DropdownMenuTrigger\n                  render={\n                    <Button\n                      size=\"icon-lg\"\n                      variant=\"ghost\"\n                      aria-label={labels.sort}\n                    />\n                  }\n                />\n              }\n            >\n              <ArrowUpDown aria-hidden=\"true\" />\n            </TooltipTrigger>\n            <TooltipContent>{labels.sort}</TooltipContent>\n          </Tooltip>\n          <DropdownMenuContent align=\"end\" className=\"*:text-xs!\">\n            {table\n              .getAllLeafColumns()\n              .filter((column) => column.getCanSort())\n              .map((column) => (\n                <React.Fragment key={column.id}>\n                  {[false, true].map((desc) => (\n                    <DropdownMenuItem\n                      key={String(desc)}\n                      onClick={() => column.toggleSorting(desc)}\n                    >\n                      {String(column.columnDef.header)} ·{\" \"}\n                      {desc ? labels.descending : labels.ascending}\n                    </DropdownMenuItem>\n                  ))}\n                </React.Fragment>\n              ))}\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </GridToolbarItem>\n    </GridToolbar>\n  )\n}\nexport { DataTableToolbar }\n",
      "type": "registry:component",
      "target": "@components/data-table-01/data-table-toolbar.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/data-table-pagination.tsx",
      "content": "\"use client\"\nimport type { Table } from \"@tanstack/react-table\"\nimport { GridPagination } from \"@/components/ui/grid-pagination/grid-pagination\"\nimport type { DataTableLabels } from \"./types\"\nfunction DataTablePagination<T>({\n  table,\n  labels,\n  virtual,\n  count,\n}: {\n  table: Table<T>\n  labels: DataTableLabels\n  virtual: boolean\n  count: number\n}) {\n  \"use no memo\"\n  const { pageIndex, pageSize } = table.getState().pagination\n  return (\n    // 翻页贴在表格底部：先隔一条点阵小带，再是翻页行；左右与底边沿用表格外框\n    <div data-slot=\"data-table-pagination\" className=\"border-t border-edge\">\n      <div\n        aria-hidden=\"true\"\n        data-slot=\"data-table-pagination-band\"\n        className=\"h-4 border-b border-edge bg-dots\"\n      />\n      {virtual ? (\n        <p className=\"flex h-9 items-center px-3 font-mono text-xs text-muted-foreground tabular-nums pointer-coarse:h-11\">\n          {labels.rowTotal(count)}\n        </p>\n      ) : (\n        <GridPagination\n          bordered={false}\n          page={pageIndex + 1}\n          pageCount={table.getPageCount()}\n          total={count}\n          pageSize={pageSize}\n          pageSizes={[10, 20, 50, 100]}\n          jumper\n          siblings={1}\n          labels={{\n            first: labels.first,\n            previous: labels.previous,\n            next: labels.next,\n            last: labels.last,\n            perPage: labels.perPage,\n            jump: labels.jump,\n            jumpInput: labels.jumpInput,\n            jumpTrigger: labels.jumpTrigger,\n          }}\n          onPageChange={(page) => table.setPageIndex(page - 1)}\n          onPageSizeChange={(size) => {\n            table.setPageSize(size)\n            table.setPageIndex(0)\n          }}\n        />\n      )}\n    </div>\n  )\n}\nexport { DataTablePagination }\n",
      "type": "registry:component",
      "target": "@components/data-table-01/data-table-pagination.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/data-table-cell.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, CircleAlert, LoaderCircle, Pencil } from \"lucide-react\"\nimport { createPortal } from \"react-dom\"\nimport { cn } from \"@/lib/utils\"\nimport { Input } from \"@/components/ui/input\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Sheet,\n  SheetContent,\n  SheetTitle,\n  SheetDescription,\n} from \"@/components/ui/sheet\"\nimport type { DataTableMeta, DataTableLabels } from \"./types\"\n\nconst invertedKey =\n  \"border-primary-foreground/25 bg-primary-foreground/10 text-primary-foreground\"\n\ntype Phase = \"idle\" | \"saving\" | \"saved\" | \"failed\"\n\nfunction Key({\n  className,\n  children,\n}: {\n  className?: string\n  children: React.ReactNode\n}) {\n  return (\n    <kbd\n      data-slot=\"data-table-cell-key\"\n      className={cn(\n        \"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\",\n        className,\n      )}\n    >\n      {children}\n    </kbd>\n  )\n}\n\n/**\n * 可编辑单元格。编辑采用表格软件的习惯：\n * - 编辑框就地替换内容，整格描主色实线框，下方贴一条提示条（快捷键 / 校验错误）\n * - Enter、Tab、点击别处都会保存；值没变直接退出；Esc 放弃\n * - 保存是乐观的：编辑框立刻关闭，格子右上角依次显示 保存中 → 已保存（淡出）\n * - 保存失败时值回滚，格子角标变红并保留草稿，再次编辑会带回草稿和错误\n */\nfunction DataTableCell<T>({\n  value,\n  row,\n  meta,\n  title,\n  labels,\n  onCommit,\n  children,\n  cellKey,\n  rowHeight,\n}: {\n  value: unknown\n  row: T\n  meta?: DataTableMeta<T>\n  title: string\n  labels: DataTableLabels\n  onCommit: (value: unknown) => void | Promise<void>\n  children: React.ReactNode\n  cellKey: string\n  rowHeight: number\n}) {\n  const [editing, setEditing] = React.useState(false)\n  const [mobile, setMobile] = React.useState(false)\n  const [draft, setDraft] = React.useState(\"\")\n  const [error, setError] = React.useState(\"\")\n  const [phase, setPhase] = React.useState<Phase>(\"idle\")\n  const [failed, setFailed] = React.useState<{\n    draft: string\n    message: string\n  } | null>(null)\n  const [hovered, setHovered] = React.useState(false)\n  const [focused, setFocused] = React.useState(false)\n  const [position, setPosition] = React.useState({ left: 0, top: 0 })\n  const root = React.useRef<HTMLDivElement>(null)\n  // 关闭编辑框时输入框卸载会触发 blur，用它避免重复保存\n  const closing = React.useRef(false)\n  const id = React.useId()\n  const numeric = meta?.editable === \"number\"\n  const peek = hovered || focused\n  const hint = editing || (phase === \"failed\" && peek)\n\n  React.useLayoutEffect(() => {\n    if (!hint || mobile) return\n    function update() {\n      // 提示条贴住整格（td）的左下角，而不是内容区\n      const bounds = (\n        root.current?.closest(\"td\") ?? root.current\n      )?.getBoundingClientRect()\n      if (bounds)\n        setPosition({\n          left: Math.max(8, Math.min(bounds.left, window.innerWidth - 328)),\n          top: bounds.bottom - 1,\n        })\n    }\n    update()\n    window.addEventListener(\"scroll\", update, true)\n    window.addEventListener(\"resize\", update)\n    return () => {\n      window.removeEventListener(\"scroll\", update, true)\n      window.removeEventListener(\"resize\", update)\n    }\n  }, [hint, mobile])\n\n  React.useEffect(() => {\n    if (phase !== \"saved\") return\n    const timer = setTimeout(() => setPhase(\"idle\"), 1600)\n    return () => clearTimeout(timer)\n  }, [phase])\n\n  function start() {\n    closing.current = false\n    setDraft(failed?.draft ?? String(value ?? \"\"))\n    setError(failed?.message ?? \"\")\n    setMobile(window.matchMedia(\"(pointer: coarse)\").matches)\n    setEditing(true)\n  }\n\n  function close(direction?: number) {\n    closing.current = true\n    setEditing(false)\n    setError(\"\")\n    if (mobile) return\n    const cells = Array.from(\n      root.current\n        ?.closest(\"table\")\n        ?.querySelectorAll<HTMLElement>(\"[data-editable=true]\") ?? [],\n    )\n    const index = cells.indexOf(root.current!)\n    const target =\n      direction === undefined\n        ? undefined\n        : direction === 0\n          ? cells.slice(index + 1).find((el) => el.dataset.column === cellKey)\n          : cells[index + direction]\n    requestAnimationFrame(() => (target ?? root.current)?.focus())\n  }\n\n  function cancel() {\n    if (failed) {\n      setFailed(null)\n      setPhase(\"idle\")\n    }\n    close()\n  }\n\n  async function commit(direction?: number, blur = false) {\n    if (closing.current) return\n    const text = draft.trim()\n    const next = numeric\n      ? text === \"\"\n        ? Number.NaN\n        : Number(text.replace(/,/g, \"\"))\n      : draft\n    // 值没变：直接退出，不发请求\n    if (!failed && String(next) === String(value ?? \"\")) {\n      if (blur) {\n        closing.current = true\n        setEditing(false)\n      } else close(direction)\n      return\n    }\n    const message = meta?.validate?.(next, row)\n    if (message) {\n      setError(message)\n      return\n    }\n    if (blur) {\n      closing.current = true\n      setEditing(false)\n    } else close(direction)\n    setFailed(null)\n    setPhase(\"saving\")\n    try {\n      await onCommit(next)\n      setPhase(\"saved\")\n    } catch (reason) {\n      setFailed({\n        draft,\n        message: reason instanceof Error ? reason.message : labels.saveError,\n      })\n      setPhase(\"failed\")\n    }\n  }\n\n  const input = (\n    <Input\n      autoFocus\n      aria-label={title}\n      aria-invalid={!!error}\n      aria-describedby={error ? id : undefined}\n      type=\"text\"\n      inputMode={numeric ? \"decimal\" : undefined}\n      autoComplete=\"off\"\n      value={draft}\n      className={\n        mobile\n          ? cn(\"min-w-20 pointer-coarse:text-base\", numeric && \"font-mono\")\n          : cn(\n              \"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\",\n              numeric && \"text-right font-mono tabular-nums\",\n            )\n      }\n      onFocus={(event) => event.currentTarget.select()}\n      onChange={(event) => {\n        setDraft(event.target.value)\n        setError(\"\")\n      }}\n      onBlur={() => {\n        if (!mobile) void commit(undefined, true)\n      }}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\") {\n          event.preventDefault()\n          cancel()\n        }\n        if (!mobile && (event.key === \"Enter\" || event.key === \"Tab\")) {\n          event.preventDefault()\n          void commit(event.key === \"Enter\" ? 0 : event.shiftKey ? -1 : 1)\n        }\n        if (mobile && event.key === \"Enter\") {\n          event.preventDefault()\n          void commit()\n        }\n      }}\n    />\n  )\n\n  const message = error || (!editing && failed?.message)\n\n  return (\n    <div\n      ref={root}\n      style={{ height: rowHeight - 1 }}\n      data-slot=\"data-table-cell\"\n      data-editable={!!meta?.editable}\n      data-editing={(editing && !mobile) || undefined}\n      data-state={phase === \"idle\" ? undefined : phase}\n      data-column={cellKey}\n      tabIndex={meta?.editable ? 0 : undefined}\n      aria-describedby={phase === \"failed\" && peek ? id : undefined}\n      className={cn(\n        \"group/cell relative flex h-full min-h-0 flex-col justify-center outline-none\",\n      )}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={(event) => setFocused(event.target === event.currentTarget)}\n      onBlur={() => setFocused(false)}\n      onDoubleClick={(event) => {\n        if (\n          event.currentTarget.contains(event.target as Node) &&\n          meta?.editable &&\n          !editing\n        )\n          start()\n      }}\n      onClick={(event) => {\n        // Sheet 通过 Portal 渲染，面板事件不能冒泡成表格单元格的再次编辑。\n        if (!event.currentTarget.contains(event.target as Node)) return\n        if (\n          !editing &&\n          meta?.editable &&\n          window.matchMedia(\"(pointer: coarse)\").matches\n        )\n          start()\n      }}\n      onKeyDown={(event) => {\n        if (event.target !== event.currentTarget) return\n        if (!editing && meta?.editable && [\"Enter\", \"F2\"].includes(event.key)) {\n          event.preventDefault()\n          start()\n        }\n      }}\n    >\n      {/* 整格框：所有状态都画在 td 的边界上（-inset-x-3 抵消 td 的 px-3），和下方提示条左缘对齐。\n          悬停 = 浅底；键盘焦点 = 实线框；失败 = 红框 + 红底；编辑 = 主色粗框 */}\n      {meta?.editable && (\n        <span\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none absolute -inset-x-3 inset-y-0 ring-inset\",\n            editing && !mobile\n              ? cn(\n                  \"z-10 bg-background ring-2\",\n                  error ? \"ring-destructive\" : \"ring-primary\",\n                )\n              : phase === \"failed\"\n                ? \"bg-destructive/5 ring-1 ring-destructive/60\"\n                : \"group-hover/cell:bg-muted/60 group-focus-visible/cell:ring-2 group-focus-visible/cell:ring-ring\",\n          )}\n        />\n      )}\n      {editing && !mobile ? (\n        <span className=\"absolute inset-0 z-10\">{input}</span>\n      ) : (\n        <div\n          className={cn(\n            \"relative min-w-0 transition-opacity\",\n            phase === \"saving\" && \"opacity-50\",\n          )}\n        >\n          {children}\n        </div>\n      )}\n\n      {/* 右上角状态：悬停可编辑 / 保存中 / 已保存（淡出）/ 失败 */}\n      {!editing && (\n        <span\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute top-1.5 -right-1.5 flex size-3 items-center justify-center\"\n        >\n          {phase === \"saving\" ? (\n            <LoaderCircle className=\"size-3 animate-spin text-muted-foreground motion-reduce:animate-none\" />\n          ) : phase === \"saved\" ? (\n            <Check className=\"size-3 animate-out text-(--flow-green) delay-1000 duration-500 fade-out-0 fill-mode-forwards\" />\n          ) : phase === \"failed\" ? (\n            <CircleAlert className=\"size-3 text-destructive\" />\n          ) : meta?.editable ? (\n            <Pencil className=\"size-3 text-muted-foreground opacity-0 group-hover/cell:opacity-100\" />\n          ) : null}\n        </span>\n      )}\n      <span className=\"sr-only\" aria-live=\"polite\">\n        {phase === \"saving\"\n          ? labels.saving\n          : phase === \"saved\"\n            ? labels.saved\n            : \"\"}\n      </span>\n\n      {hint &&\n        !mobile &&\n        createPortal(\n          <div\n            id={id}\n            role={message ? \"alert\" : undefined}\n            style={{ left: position.left, top: position.top }}\n            className={cn(\n              // 提示条像挂在格子框下沿的标签：左缘、边框粗细与颜色都和格子框一致\n              \"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\",\n              message\n                ? cn(\n                    \"bg-popover text-destructive\",\n                    editing\n                      ? \"border-2 border-destructive\"\n                      : \"border border-destructive/60\",\n                  )\n                : \"border-2 border-primary bg-primary text-primary-foreground/70\",\n            )}\n          >\n            {message ? (\n              <>\n                <CircleAlert aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n                <span className=\"min-w-0 truncate\">{message}</span>\n                {!editing && (\n                  <span className=\"shrink-0 text-muted-foreground\">\n                    · <Key>↵</Key> {labels.editAgain}\n                  </span>\n                )}\n              </>\n            ) : (\n              <span className=\"flex items-center gap-3 whitespace-nowrap\">\n                <span className=\"flex items-center gap-1\">\n                  <Key className={invertedKey}>↵</Key>\n                  {labels.save}\n                </span>\n                <span className=\"flex items-center gap-1\">\n                  <Key className={invertedKey}>Tab</Key>\n                  {labels.nextCell}\n                </span>\n                <span className=\"flex items-center gap-1\">\n                  <Key className={invertedKey}>Esc</Key>\n                  {labels.cancel}\n                </span>\n              </span>\n            )}\n          </div>,\n          document.body,\n        )}\n\n      <Sheet\n        open={editing && mobile}\n        onOpenChange={(open) => {\n          if (!open) cancel()\n        }}\n      >\n        <SheetContent\n          side=\"bottom\"\n          className=\"space-y-4 p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))]\"\n        >\n          <SheetTitle>{title}</SheetTitle>\n          <SheetDescription>\n            {labels.editDescription(String(value ?? \"\"), title)}\n          </SheetDescription>\n          {editing && mobile && input}\n          {error && mobile && (\n            <p\n              id={id}\n              role=\"alert\"\n              className=\"flex items-center gap-1.5 text-sm text-destructive\"\n            >\n              <CircleAlert aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n              {error}\n            </p>\n          )}\n          <div className=\"flex gap-3\">\n            <Button\n              size=\"lg\"\n              variant=\"outline\"\n              className=\"flex-1\"\n              onClick={cancel}\n            >\n              {labels.cancel}\n            </Button>\n            <Button size=\"lg\" className=\"flex-1\" onClick={() => void commit()}>\n              {labels.save}\n            </Button>\n          </div>\n        </SheetContent>\n      </Sheet>\n    </div>\n  )\n}\nexport { DataTableCell }\n",
      "type": "registry:component",
      "target": "@components/data-table-01/data-table-cell.tsx"
    },
    {
      "path": "src/registry/blocks/data-table-01/types.ts",
      "content": "import type * as React from \"react\"\nimport type {\n  Table,\n  ColumnDef,\n  SortingState,\n  PaginationState,\n  ColumnFiltersState,\n  RowSelectionState,\n} from \"@tanstack/react-table\"\n\ntype DataTableMeta<T> = {\n  detailCell?: (value: unknown, row: T) => React.ReactNode\n  bar?: boolean\n  max?: number\n  align?: \"left\" | \"center\" | \"right\"\n  editable?: \"text\" | \"number\"\n  validate?: (value: unknown, row: T) => string | undefined\n  hideOnMobile?: boolean\n  pinned?: boolean\n  width?: number\n}\ntype DataTableColumn<T> = ColumnDef<T, unknown> & { meta?: DataTableMeta<T> }\ntype DataTableLabels = {\n  sort: string\n  ascending: string\n  descending: string\n  detailDescription: string\n  emptyDescription: string\n  noResultsDescription: string\n  errorDescription: string\n  items: string\n  page: string\n  editDescription: (value: string, column: string) => string\n  actions: string\n  rowTotal: (count: number) => string\n  /** 虚拟滚动模式下窄屏列表的「显示更多」 */\n  showMore: (rest: number) => string\n  search: string\n  /** 列表（桌面 table、手机 ul）的读屏名称；`ariaLabel` 优先 */\n  dataset: string\n  /** 行操作按钮的读屏名称模板，`{name}` 替换为行名称 */\n  rowActions: string\n  /** 搜索框的读屏名称（未传 searchLabel 时使用） */\n  searchField: string\n  status: string\n  columns: string\n  reset: string\n  density: string\n  compact: string\n  standard: string\n  comfortable: string\n  selected: string\n  selectAllCompact: string\n  selectAll: string\n  clear: string\n  retry: string\n  empty: string\n  noResults: string\n  clearFilters: string\n  /** 状态筛选未选时的占位（statusFilter 里没有空值选项时使用） */\n  all: string\n  first: string\n  previous: string\n  next: string\n  last: string\n  total: string\n  perPage: string\n  jump: string\n  /** 跳页输入框的读屏名称 */\n  jumpInput: string\n  /** 窄容器里「当前页 / 总页数」入口的读屏说明 */\n  jumpTrigger: string\n  cancel: string\n  save: string\n  saveError: string\n  saving: string\n  saved: string\n  nextCell: string\n  editAgain: string\n  selectPage: string\n  selectRow: string\n}\ntype DataTableHandle = {\n  startEditing: (rowId: string, columnId: string) => void\n  openDetails: (rowId: string) => void\n}\ntype DataTableProps<T> = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  apiRef?: React.Ref<DataTableHandle>\n  /**\n   * 列表的读屏名称，用于桌面 table 与手机 ul；不传时取 `labels.dataset`（默认「数据列表」）\n   */\n  ariaLabel?: string\n  columns: DataTableColumn<T>[]\n  data: T[]\n  getRowId: (row: T) => string\n  mode?: \"client\" | \"manual\"\n  rowCount?: number\n  loading?: boolean\n  error?: string | null\n  onRetry?: () => void\n  /**\n   * 外部筛选是否生效。服务端分页时筛选状态通常放在页面里（比如 `toolbar` 插槽里的下拉），\n   * 表格感知不到；传 true 后无结果的空状态会显示「没有符合条件的结果」和「清除筛选」\n   * @default false\n   */\n  filtered?: boolean\n  /** 点「清除筛选」时调用：表格先清掉自己的搜索和列筛选，再调用它，用来重置外部筛选 */\n  onClearFilters?: () => void\n  sorting?: SortingState\n  defaultSorting?: SortingState\n  onSortingChange?: (value: SortingState) => void\n  pagination?: PaginationState\n  defaultPagination?: PaginationState\n  onPaginationChange?: (value: PaginationState) => void\n  globalFilter?: string\n  defaultGlobalFilter?: string\n  onGlobalFilterChange?: (value: string) => void\n  columnFilters?: ColumnFiltersState\n  defaultColumnFilters?: ColumnFiltersState\n  onColumnFiltersChange?: (value: ColumnFiltersState) => void\n  rowSelection?: RowSelectionState\n  defaultRowSelection?: RowSelectionState\n  onRowSelectionChange?: (value: RowSelectionState) => void\n  onSelectAll?: () => void\n  bulkActions?: (selected: T[]) => React.ReactNode\n  rowActions?: (row: T) => React.ReactNode\n  /** 搜索框条内标签，默认不显示；传入如「搜索」时显示在图标前 */\n  searchLabel?: React.ReactNode\n  onCellCommit?: (change: {\n    rowId: string\n    columnId: string\n    value: unknown\n    row: T\n  }) => void | Promise<void>\n  virtualization?: false | { estimateRowHeight?: number; height?: number }\n  header?: React.ReactNode\n  counts?: Record<string, number>\n  /** 工具栏插槽：工具栏是网格工具栏，每个控件包一层 `GridToolbarItem` 成为一格 */\n  toolbar?: React.ReactNode | ((table: Table<T>) => React.ReactNode)\n  statusFilter?: {\n    columnId: string\n    /** tone：状态色点的颜色，填 CSS 颜色或主题变量，如 \"var(--flow-green)\" */\n    options: { value: string; label: string; tone?: string }[]\n  }\n  labels?: Partial<DataTableLabels>\n}\nexport type {\n  DataTableHandle,\n  DataTableProps,\n  DataTableColumn,\n  DataTableMeta,\n  DataTableLabels,\n}\n",
      "type": "registry:component",
      "target": "@components/data-table-01/types.ts"
    },
    {
      "path": "src/registry/blocks/data-table-01/config.ts",
      "content": "import type { DataTableLabels } from \"./types\"\nconst DATA_TABLE_LABELS: DataTableLabels = {\n  sort: \"排序\",\n  ascending: \"升序\",\n  descending: \"降序\",\n  detailDescription: \"查看全部字段，点击可编辑字段进行修改。\",\n  emptyDescription: \"添加第一份数据集，开始整理团队的数据资源。\",\n  noResultsDescription: \"试试其他关键词，或清除筛选条件。\",\n  errorDescription: \"暂时无法读取数据，请稍后重试。\",\n  items: \"条\",\n  page: \"页\",\n  editDescription: (value, column) => \"编辑「\" + value + \"」的\" + column,\n  actions: \"操作\",\n  rowTotal: (count: number) => `共 ${count} 行`,\n  showMore: (rest: number) => `显示更多（还有 ${rest} 行）`,\n  search: \"搜索数据集…\",\n  dataset: \"数据列表\",\n  rowActions: \"{name} 的操作\",\n  searchField: \"搜索\",\n  status: \"状态\",\n  columns: \"列设置\",\n  reset: \"恢复默认\",\n  density: \"密度\",\n  compact: \"紧凑\",\n  standard: \"标准\",\n  comfortable: \"舒适\",\n  selected: \"已选\",\n  selectAllCompact: \"全选\",\n  selectAll: \"选择全部\",\n  clear: \"清除选择\",\n  retry: \"重试\",\n  empty: \"暂无数据\",\n  noResults: \"没有符合条件的结果\",\n  clearFilters: \"清除筛选\",\n  all: \"全部状态\",\n  first: \"首页\",\n  previous: \"上一页\",\n  next: \"下一页\",\n  last: \"尾页\",\n  total: \"共\",\n  perPage: \"每页\",\n  jump: \"跳至\",\n  jumpInput: \"跳转页码\",\n  jumpTrigger: \"跳转到指定页\",\n  cancel: \"取消\",\n  save: \"保存\",\n  saveError: \"保存失败，请重试\",\n  saving: \"保存中\",\n  saved: \"已保存\",\n  nextCell: \"下一格\",\n  editAgain: \"重新编辑\",\n  selectPage: \"选择当前页\",\n  selectRow: \"选择行\",\n}\nconst numberFormat = new Intl.NumberFormat(\"zh-CN\")\nexport { DATA_TABLE_LABELS, numberFormat }\n",
      "type": "registry:component",
      "target": "@components/data-table-01/config.ts"
    },
    {
      "path": "src/registry/blocks/data-table-01/page.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport {\n  Plus,\n  Upload,\n  Eye,\n  Pencil,\n  Copy,\n  Archive,\n  Download,\n  Trash2,\n} from \"lucide-react\"\nimport { DataTableRowActions } from \"@/components/data-table-01/data-table-row-actions\"\nimport type { DataTableHandle } from \"@/components/data-table-01/types\"\nimport {\n  AlertDialog,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogCancel,\n  AlertDialogAction,\n} from \"@/components/ui/alert-dialog\"\nimport { BarSelect } from \"@/components/ui/bar-select/bar-select\"\nimport { GridToolbarItem } from \"@/components/ui/grid-toolbar/grid-toolbar\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  GridContainer,\n  GridSection,\n  GridSeparator,\n} from \"@/components/ui/grid-layout/grid-layout\"\nimport { numberFormat } from \"@/components/data-table-01/config\"\nimport { DataTable } from \"@/components/data-table-01/data-table\"\nimport type { DataTableColumn } from \"@/components/data-table-01/types\"\ntype Dataset = {\n  code: string\n  id: string\n  name: string\n  owner: string\n  status: string\n  samples: number\n  updatedAt: string\n}\nconst datasets: Dataset[] = Array.from({ length: 666 }, (_, i) => ({\n  code: \"DS-\" + String(i + 1).padStart(4, \"0\"),\n  id: String(i + 1),\n  name:\n    [\"城市声景\", \"植物图谱\", \"手写字迹\", \"街道影像\"][i % 4] +\n    \" \" +\n    String(i + 1).padStart(2, \"0\"),\n  owner: [\"林小满\", \"陈青\", \"周远\"][i % 3] ?? \"林小满\",\n  status: [\"进行中\", \"已归档\", \"待审核\"][i % 3] ?? \"进行中\",\n  samples: 1200 + i * 137,\n  updatedAt: new Date(\n    Date.UTC(2026, 9, 31, 0) - (i < 6 ? (i + 1) * 3600000 : (i - 4) * 86400000),\n  ).toISOString(),\n}))\nconst columns: DataTableColumn<Dataset>[] = [\n  {\n    accessorKey: \"name\",\n    header: \"名称\",\n    size: 240,\n    cell: ({ row, getValue }) => (\n      <span>\n        <span className=\"font-medium\">{String(getValue())}</span>\n        <span className=\"mt-1 block font-mono text-[10px] text-muted-foreground\">\n          {row.original.code}\n        </span>\n      </span>\n    ),\n    meta: {\n      pinned: true,\n      editable: \"text\",\n      validate: (value) => (String(value).trim() ? undefined : \"请输入名称\"),\n    },\n  },\n  {\n    accessorKey: \"owner\",\n    filterFn: (row, columnId, values: string[]) =>\n      !values.length || values.includes(String(row.getValue(columnId))),\n    header: \"负责人\",\n    size: 140,\n    cell: ({ getValue }) => (\n      <span className=\"inline-flex items-center gap-2\">\n        <span className=\"flex size-5 items-center justify-center rounded-sm border border-edge bg-muted text-[10px]\">\n          {String(getValue()).slice(0, 1)}\n        </span>\n        {String(getValue())}\n      </span>\n    ),\n  },\n  {\n    accessorKey: \"status\",\n    header: \"状态\",\n    size: 140,\n    cell: ({ getValue }) => (\n      <span className=\"inline-flex items-center gap-1.5 text-xs\">\n        <span\n          className={\n            String(getValue()) === \"进行中\"\n              ? \"size-1.5 rounded-full bg-(--flow-green)\"\n              : String(getValue()) === \"待审核\"\n                ? \"size-1.5 rounded-full bg-(--flow-amber)\"\n                : \"size-1.5 rounded-full bg-muted-foreground\"\n          }\n        />\n        {String(getValue())}\n      </span>\n    ),\n  },\n  {\n    accessorKey: \"samples\",\n    header: \"样本数\",\n    cell: ({ getValue }) => numberFormat.format(Number(getValue())),\n    size: 140,\n    meta: {\n      bar: true,\n      align: \"right\",\n      editable: \"number\",\n      validate: (value) =>\n        Number.isFinite(value) && Number(value) >= 0\n          ? undefined\n          : \"样本数不能小于零\",\n    },\n  },\n  {\n    accessorKey: \"updatedAt\",\n    cell: ({ getValue }) => <RelativeDate value={String(getValue())} />,\n    header: \"更新时间\",\n    size: 180,\n    meta: {\n      hideOnMobile: true,\n      detailCell: (value) => <RelativeDate value={String(value)} exact />,\n    },\n  },\n]\n// 全表共用一个分钟级时钟：新挂载的单元格（翻页、排序）直接读到当前时间，不会先显示日期再跳成「几小时前」；\n// 只有首次水合时用 null，与预渲染的日期一致\nlet clock = 0\nlet clockTimer: ReturnType<typeof setInterval> | undefined\nconst clockListeners = new Set<() => void>()\nfunction subscribeClock(listener: () => void) {\n  clockListeners.add(listener)\n  if (clockListeners.size === 1)\n    clockTimer = setInterval(() => {\n      clock = Date.now()\n      clockListeners.forEach((notify) => notify())\n    }, 60000)\n  return () => {\n    clockListeners.delete(listener)\n    if (!clockListeners.size) clearInterval(clockTimer)\n  }\n}\nfunction getClock() {\n  if (!clock) clock = Date.now()\n  return clock\n}\nfunction RelativeDate({\n  value,\n  exact = false,\n}: {\n  value: string\n  exact?: boolean\n}) {\n  const now = React.useSyncExternalStore(subscribeClock, getClock, () => null)\n  const date = new Date(value)\n  const days = now === null ? 100 : (now - date.getTime()) / 86400000\n  const relative = new Intl.RelativeTimeFormat(\"zh-CN\", { numeric: \"auto\" })\n  const text =\n    days > 7\n      ? value.slice(5, 10)\n      : days < 1\n        ? relative.format(-Math.max(1, Math.floor(days * 24)), \"hour\")\n        : relative.format(-Math.floor(days), \"day\")\n  return (\n    <time\n      data-slot=\"data-table-date\"\n      dateTime={value}\n      title={new Intl.DateTimeFormat(\"zh-CN\", {\n        dateStyle: \"full\",\n        timeStyle: \"long\",\n        timeZone: \"Asia/Shanghai\",\n      }).format(date)}\n      className=\"font-mono text-xs text-muted-foreground\"\n    >\n      {exact\n        ? new Intl.DateTimeFormat(\"sv-SE\", {\n            timeZone: \"Asia/Shanghai\",\n            year: \"numeric\",\n            month: \"2-digit\",\n            day: \"2-digit\",\n            hour: \"2-digit\",\n            minute: \"2-digit\",\n          }).format(date) +\n          \"（\" +\n          text +\n          \"）\"\n        : text}\n    </time>\n  )\n}\nconst getRowId = (row: Dataset) => row.id\nfunction DataTablePage() {\n  const apiRef = React.useRef<DataTableHandle>(null)\n  const [deleting, setDeleting] = React.useState<Dataset[]>([])\n  const [data, setData] = React.useState(datasets)\n  const [selection, setSelection] = React.useState<Record<string, boolean>>({})\n  const [notice, setNotice] = React.useState(\"\")\n  function exportRows(rows: Dataset[]) {\n    const url = URL.createObjectURL(\n      new Blob(\n        [\n          \"\\ufeff名称,负责人,状态,样本数,更新时间\\n\" +\n            rows\n              .map((row) =>\n                [\n                  row.name,\n                  row.owner,\n                  row.status,\n                  row.samples,\n                  row.updatedAt,\n                ].join(\",\"),\n              )\n              .join(\"\\n\"),\n        ],\n        { type: \"text/csv;charset=utf-8\" },\n      ),\n    )\n    const link = document.createElement(\"a\")\n    link.href = url\n    link.download = \"数据集.csv\"\n    link.click()\n    URL.revokeObjectURL(url)\n    setNotice(\"已导出 \" + rows.length + \" 条\")\n  }\n  return (\n    // 线条网格外框：竖线容器 + 全屏横线；表格贴着两条竖线铺满，工具栏和分页用 --data-table-gutter 与竖线留出间距\n    <main\n      data-slot=\"data-table-page\"\n      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]\"\n    >\n      <GridContainer>\n        <GridSection\n          topLine\n          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\"\n        >\n          <div>\n            <h1 className=\"text-2xl font-semibold tracking-tight\">数据集</h1>\n            <p className=\"mt-1.5 text-sm text-muted-foreground\">\n              整理、审核与维护团队的数据资源。\n            </p>\n          </div>\n          <div className=\"flex gap-2\">\n            <Button\n              variant=\"outline\"\n              onClick={() => setNotice(\"请选择 CSV 文件导入\")}\n            >\n              <Upload />\n              导入\n            </Button>\n            <Button\n              onClick={() => {\n                const first = datasets[0]\n                if (!first) return\n                const id = String(\n                  Math.max(0, ...data.map((row) => Number(row.id))) + 1,\n                )\n                setData((previous) => [\n                  {\n                    ...first,\n                    id,\n                    code: \"DS-\" + id.padStart(4, \"0\"),\n                    name: \"新建数据集\",\n                  },\n                  ...previous,\n                ])\n                setNotice(\"已新建数据集\")\n              }}\n            >\n              <Plus />\n              新建数据集\n            </Button>\n          </div>\n        </GridSection>\n        <GridSeparator label={\"DATASETS / \" + data.length} />\n        <GridSection>\n          <DataTable\n            apiRef={apiRef}\n            toolbar={(table) => (\n              // 工具栏是网格工具栏：插槽里每个控件包一层 GridToolbarItem 成为一格\n              <GridToolbarItem className=\"hidden w-40 @min-[64rem]:flex\">\n                <BarSelect\n                  multiple\n                  aria-label=\"负责人\"\n                  placeholder=\"全部负责人\"\n                  contentClassName=\"[&_[data-slot=combobox-item]]:text-xs [&_*]:text-xs\"\n                  value={\n                    (table.getColumn(\"owner\")?.getFilterValue() as string[]) ??\n                    []\n                  }\n                  options={[\"林小满\", \"陈青\", \"周远\"].map((owner) => ({\n                    value: owner,\n                    label: owner,\n                    count: data.filter((row) => row.owner === owner).length,\n                  }))}\n                  onValueChange={(values) => {\n                    table\n                      .getColumn(\"owner\")\n                      ?.setFilterValue(values.length ? values : undefined)\n                    table.setPageIndex(0)\n                  }}\n                />\n              </GridToolbarItem>\n            )}\n            rowActions={(row) => (\n              <DataTableRowActions\n                label={row.name}\n                actions={[\n                  {\n                    label: \"查看详情\",\n                    icon: <Eye />,\n                    onSelect: () => apiRef.current?.openDetails(row.id),\n                  },\n                  {\n                    label: \"编辑名称\",\n                    icon: <Pencil />,\n                    onSelect: () =>\n                      apiRef.current?.startEditing(row.id, \"name\"),\n                  },\n                  {\n                    label: \"复制编号\",\n                    icon: <Copy />,\n                    shortcut: row.code,\n                    onSelect: () => {\n                      void navigator.clipboard.writeText(row.code).then(\n                        () => setNotice(\"已复制\"),\n                        () => setNotice(\"复制失败\"),\n                      )\n                    },\n                  },\n                  \"separator\",\n                  {\n                    label: \"导出 CSV\",\n                    icon: <Download />,\n                    onSelect: () => exportRows([row]),\n                  },\n                  {\n                    label: \"归档\",\n                    icon: <Archive />,\n                    onSelect: () =>\n                      setData((previous) =>\n                        previous.map((item) =>\n                          item.id === row.id\n                            ? { ...item, status: \"已归档\" }\n                            : item,\n                        ),\n                      ),\n                  },\n                  \"separator\",\n                  {\n                    label: \"删除…\",\n                    icon: <Trash2 />,\n                    destructive: true,\n                    onSelect: () => setDeleting([row]),\n                  },\n                ]}\n              />\n            )}\n            columns={columns}\n            data={data}\n            rowCount={data.length}\n            rowSelection={selection}\n            onRowSelectionChange={setSelection}\n            onSelectAll={() =>\n              setSelection(\n                Object.fromEntries(data.map((row) => [row.id, true])),\n              )\n            }\n            bulkActions={(rows) => (\n              <>\n                <Button\n                  variant=\"ghost\"\n                  size=\"default\"\n                  aria-label=\"导出\"\n                  onClick={() => exportRows(rows)}\n                >\n                  <Download />\n                  <span data-label>导出</span>\n                </Button>\n                <Button\n                  variant=\"ghost\"\n                  size=\"default\"\n                  aria-label=\"归档\"\n                  onClick={() =>\n                    setData((previous) =>\n                      previous.map((row) =>\n                        selection[row.id] ? { ...row, status: \"已归档\" } : row,\n                      ),\n                    )\n                  }\n                >\n                  <Archive />\n                  <span data-label>归档</span>\n                </Button>\n                {/* 反色坞里删除用红色文字区分，混入背景色保证亮暗模式都清楚 */}\n                <Button\n                  variant=\"ghost\"\n                  size=\"default\"\n                  aria-label=\"删除\"\n                  className=\"text-[color-mix(in_oklab,var(--destructive)_75%,var(--background))]!\"\n                  onClick={() => setDeleting(rows)}\n                >\n                  <Trash2 />\n                  <span data-label>删除</span>\n                </Button>\n              </>\n            )}\n            onCellCommit={async ({ rowId, columnId, value }) => {\n              // 模拟请求：等待 600ms；重名由“服务端”拒绝，演示保存失败与回滚\n              await new Promise((resolve) => setTimeout(resolve, 600))\n              if (\n                columnId === \"name\" &&\n                data.some(\n                  (row) =>\n                    row.id !== rowId && row.name === String(value).trim(),\n                )\n              )\n                throw new Error(\"已有同名数据集\")\n              setData((previous) =>\n                previous.map((row) =>\n                  row.id === rowId ? { ...row, [columnId]: value } : row,\n                ),\n              )\n            }}\n\n            getRowId={getRowId}\n            statusFilter={{\n              columnId: \"status\",\n              options: [\n                { value: \"\", label: \"全部状态\" },\n                { value: \"进行中\", label: \"进行中\", tone: \"var(--flow-green)\" },\n                { value: \"待审核\", label: \"待审核\", tone: \"var(--flow-amber)\" },\n                {\n                  value: \"已归档\",\n                  label: \"已归档\",\n                  tone: \"var(--muted-foreground)\",\n                },\n              ],\n            }}\n          />\n        </GridSection>\n      </GridContainer>\n      <AlertDialog\n        open={deleting.length > 0}\n        onOpenChange={(open) => {\n          if (!open) setDeleting([])\n        }}\n      >\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogTitle>删除数据集？</AlertDialogTitle>\n            <AlertDialogDescription>\n              {deleting.length === 1\n                ? \"将删除 1 个数据集：「\" +\n                  deleting[0]?.name +\n                  \"」，此操作无法撤销。\"\n                : \"将删除 \" + deleting.length + \" 个数据集，此操作无法撤销。\"}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel size=\"default\">取消</AlertDialogCancel>\n            <AlertDialogAction\n              variant=\"destructive\"\n              size=\"default\"\n              onClick={() => {\n                const ids = new Set(deleting.map((row) => row.id))\n                setData((previous) =>\n                  previous.filter((row) => !ids.has(row.id)),\n                )\n                setSelection({})\n                setDeleting([])\n                setNotice(\"已删除数据集\")\n              }}\n            >\n              删除\n            </AlertDialogAction>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n      <p role=\"status\" className=\"sr-only\">\n        {notice}\n      </p>\n    </main>\n  )\n}\nexport default DataTablePage\nexport { datasets, columns, getRowId }\nexport type { Dataset }\n",
      "type": "registry:page",
      "target": "app/data-table/page.tsx"
    },
    {
      "path": "src/registry/hooks/use-controllable-state.ts",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\ntype UseControllableStateParams<T> = {\n  /** 受控值；传入后组件不再维护内部状态 */\n  prop?: T\n  /** 非受控时的初始值 */\n  defaultProp: T\n  /** 值变化时回调（受控与非受控都会触发） */\n  onChange?: (value: T) => void\n}\n\n/**\n * 同时支持受控与非受控的状态 hook。\n * 约定见 component-conventions 技能第 4 节：open/defaultOpen/onOpenChange 等成对 props 都用它实现。\n */\nfunction useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange,\n}: UseControllableStateParams<T>) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultProp)\n  const isControlled = prop !== undefined\n  const value = isControlled ? prop : uncontrolled\n\n  // 用 ref 保存最新的回调和值，保证 setValue 引用稳定\n  const onChangeRef = React.useRef(onChange)\n  const valueRef = React.useRef(value)\n  React.useEffect(() => {\n    onChangeRef.current = onChange\n    valueRef.current = value\n  })\n\n  const setValue = React.useCallback(\n    (next: React.SetStateAction<T>) => {\n      const resolved =\n        typeof next === \"function\"\n          ? (next as (prev: T) => T)(valueRef.current)\n          : next\n      if (Object.is(resolved, valueRef.current)) return\n      valueRef.current = resolved\n      if (!isControlled) setUncontrolled(resolved)\n      onChangeRef.current?.(resolved)\n    },\n    [isControlled],\n  )\n\n  return [value, setValue] as const\n}\n\nexport { useControllableState }\n",
      "type": "registry:hook",
      "target": "@hooks/use-controllable-state.ts"
    },
    {
      "path": "src/registry/blocks/data-table-01/data-table-row-actions.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { MoreHorizontal } from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n} from \"@/components/ui/dropdown-menu\"\nimport { DATA_TABLE_LABELS } from \"./config\"\nimport type { DataTableLabels } from \"./types\"\n\ntype DataTableRowAction =\n  | \"separator\"\n  | {\n      label: string\n      icon?: React.ReactNode\n      shortcut?: React.ReactNode\n      destructive?: boolean\n      onSelect: () => void\n    }\nfunction DataTableRowActions({\n  label,\n  ariaLabel,\n  labels: overrides,\n  actions,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  /** 行名称，用于生成按钮的读屏名称 */\n  label: string\n  /**\n   * 完整的读屏名称，优先于 `labels.rowActions`。传函数时参数是 `label`，方便按行生成\n   */\n  ariaLabel?: string | ((label: string) => string)\n  /** 只用到 `rowActions`（模板，`{name}` 替换为 `label`，默认「{name} 的操作」） */\n  labels?: Partial<Pick<DataTableLabels, \"rowActions\">>\n  actions: DataTableRowAction[]\n}) {\n  const template = overrides?.rowActions ?? DATA_TABLE_LABELS.rowActions\n  const triggerLabel =\n    typeof ariaLabel === \"function\"\n      ? ariaLabel(label)\n      : (ariaLabel ?? template.replaceAll(\"{name}\", label))\n  return (\n    <div {...props} data-slot=\"data-table-row-actions\">\n      <DropdownMenu>\n        <DropdownMenuTrigger\n          render={\n            <Button\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              className=\"text-muted-foreground group-hover/row:text-foreground hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground\"\n              aria-label={triggerLabel}\n            />\n          }\n        >\n          <MoreHorizontal aria-hidden=\"true\" />\n        </DropdownMenuTrigger>\n        <DropdownMenuContent\n          align=\"end\"\n          className=\"w-auto min-w-40 *:text-xs! [&_[role=menuitem]_svg]:size-3.5!\"\n        >\n          {actions.map((action, index) =>\n            action === \"separator\" ? (\n              <DropdownMenuSeparator key={index} />\n            ) : (\n              <DropdownMenuItem\n                key={index}\n                variant={action.destructive ? \"destructive\" : \"default\"}\n                onClick={action.onSelect}\n              >\n                {action.icon}\n                <span className=\"whitespace-nowrap\">{action.label}</span>\n                {action.shortcut && (\n                  // 右侧附注（如编号）不换行，菜单宽度随内容撑开\n                  <DropdownMenuShortcut className=\"ps-4 font-mono whitespace-nowrap\">\n                    {action.shortcut}\n                  </DropdownMenuShortcut>\n                )}\n              </DropdownMenuItem>\n            ),\n          )}\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  )\n}\nexport { DataTableRowActions }\nexport type { DataTableRowAction }\n",
      "type": "registry:component",
      "target": "@components/data-table-01/data-table-row-actions.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-edge": "var(--edge)"
    },
    "light": {
      "flow-green": "oklch(0.62 0.16 150)",
      "flow-amber": "oklch(0.7 0.16 70)",
      "edge": "color-mix(in oklch, var(--foreground) 12%, transparent)",
      "flow-blue": "oklch(0.56 0.2 258)",
      "flow-rose": "oklch(0.62 0.21 5)",
      "flow-violet": "oklch(0.56 0.22 292)",
      "flow-cyan": "oklch(0.66 0.12 215)"
    },
    "dark": {
      "flow-green": "oklch(0.77 0.17 152)",
      "flow-amber": "oklch(0.82 0.15 82)",
      "edge": "color-mix(in oklch, var(--foreground) 18%, transparent)",
      "flow-blue": "oklch(0.72 0.15 255)",
      "flow-rose": "oklch(0.74 0.16 8)",
      "flow-violet": "oklch(0.74 0.15 295)",
      "flow-cyan": "oklch(0.82 0.11 208)"
    }
  },
  "meta": {
    "iframeHeight": "820px"
  },
  "type": "registry:block"
}
