shadimage
Shadcn Table
componentdata-tablesource: ok · hit

Data Table

A feature-rich data table component with server-side filtering, sorting, and pagination

npx shadcn add "https://tablecn.com/r/data-table.json"
free

Where it goes

data-tablefeature-grid

Use cases

dashboard

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react
formszod

Registry dependencies

buttoncalendarcommanddropdown-menuinputpopoverselectseparatorslidertablelabelskeleton

Files19 · source cached 10/8/2026, 10:23:04 PM

src/registry/bases/base/components/data-table/data-table.tsx
"use client";

import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import {
  FlexRender,
  type Row,
  type RowData,
  Subscribe,
  type Table as TanstackTable,
} from "@tanstack/react-table";
import { cn } from "cn";
import * as React from "react";

import type { DataTableFeatures } from "@/lib/data-table-features";

import {
  getColumnPinningStyle,
  getColumnSizingStyle,
} from "@/lib/data-table-utils";
import { DataTablePagination } from "@/registry/bases/base/components/data-table/data-table-pagination";
import { useDirection } from "@/registry/bases/base/ui/direction";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/registry/bases/base/ui/table";

interface DataTableProps<
  TData extends RowData,
> extends React.ComponentProps<"div"> {
  table: TanstackTable<DataTableFeatures, TData>;
  actionBar?: React.ReactNode;
}

export function DataTable<TData extends RowData>({
  table,
  actionBar,
  children,
  className,
  ...props
}: DataTableProps<TData>) {
  const dir = useDirection();

  return (
    <div
      dir={dir}
      className={cn("flex w-full flex-col gap-2.5 overflow-auto", className)}
      {...props}
    >
      {children}
      <div className="overflow-hidden rounded-md border">
        <DataTableLayout table={table}>
          <DataTableHeader table={table} />
          <DataTableBody table={table} />
        </DataTableLayout>
      </div>
      <div className="flex flex-col gap-2.5">
        <DataTablePagination table={table} />
        {actionBar ? (
          <DataTableActionBar table={table} actionBar={actionBar} />
        ) : null}
      </div>
    </div>
  );
}

interface DataTableLayoutProps<TData extends RowData> {
  table: TanstackTable<DataTableFeatures, TData>;
  children: React.ReactNode;
}

function DataTableLayout<TData extends RowData>({
  table,
  children,
}: DataTableLayoutProps<TData>) {
  return (
    <Subscribe
      source={table.store}
      selector={(state) => ({
        columnOrder: state.columnOrder,
        columnPinning: state.columnPinning,
        columnSizing: state.columnSizing,
        columnVisibility: state.columnVisibility,
      })}
    >
      {() => (
        <Table className="table-fixed" style={getColumnSizingStyle(table)}>
          {children}
        </Table>
      )}
    </Subscribe>
  );
}

interface DataTableHeaderProps<TData extends RowData> {
  table: TanstackTable<DataTableFeatures, TData>;
}

function DataTableHeader<TData extends RowData>({
  table,
}: DataTableHeaderProps<TData>) {
  return (
    <Subscribe
      source={table.store}
      selector={(state) => ({
        columnOrder: state.columnOrder,
        columnPinning: state.columnPinning,
        columnVisibility: state.columnVisibility,
        rowSelection: state.rowSelection,
      })}
    >
      {() => (
        <TableHeader>
          {table.getHeaderGroups().map((headerGroup) => (
            <TableRow key={headerGroup.id} className="group/row">
              {headerGroup.headers.map((header) => (
                <DataTableCellSlot
                  key={header.id}
                  render={<TableHead colSpan={header.colSpan} />}
                  pinned={!!header.column.getIsPinned()}
                  style={getColumnPinningStyle(header.column)}
                >
                  {header.isPlaceholder ? null : <FlexRender header={header} />}
                </DataTableCellSlot>
              ))}
            </TableRow>
          ))}
        </TableHeader>
      )}
    </Subscribe>
  );
}

interface DataTableBodyProps<TData extends RowData> {
  table: TanstackTable<DataTableFeatures, TData>;
}

function DataTableBody<TData extends RowData>({
  table,
}: DataTableBodyProps<TData>) {
  const rows = table.getRowModel().rows;

  if (!rows.length) {
    return (
      <Subscribe source={table.atoms.columnVisibility}>
        {() => (
          <TableBody>
            <TableRow>
              <TableCell
                colSpan={table.getVisibleLeafColumns().length || 1}
                className="h-24 text-center"
              >
                No results.
              </TableCell>
            </TableRow>
          </TableBody>
        )}
      </Subscribe>
    );
  }

  return (
    <TableBody>
      {rows.map((row) => (
        <MemoizedDataTableRow key={row.id} row={row} />
      ))}
    </TableBody>
  );
}

interface DataTableRowProps<TData extends RowData> {
  row: Row<DataTableFeatures, TData>;
}

function DataTableRow<TData extends RowData>({
  row,
}: DataTableRowProps<TData>) {
  return (
    <Subscribe
      source={row.table.store}
      selector={(state) => ({
        columnOrder: state.columnOrder,
        columnPinning: state.columnPinning,
        columnVisibility: state.columnVisibility,
      })}
    >
      {() => {
        const cells = row.getVisibleCells().map((cell) => ({
          cell,
          pinned: !!cell.column.getIsPinned(),
          style: getColumnPinningStyle(cell.column),
        }));

        return (
          <Subscribe
            source={row.table.atoms.rowSelection}
            selector={(selection) => selection[row.id] === true}
          >
            {(isSelected) => (
              <TableRow
                data-state={isSelected ? "selected" : undefined}
                className="group/row"
              >
                {cells.map(({ cell, pinned, style }) => (
                  <DataTableCellSlot
                    key={cell.id}
                    render={<TableCell />}
                    pinned={pinned}
                    style={style}
                  >
                    <FlexRender cell={cell} />
                  </DataTableCellSlot>
                ))}
              </TableRow>
            )}
          </Subscribe>
        );
      }}
    </Subscribe>
  );
}

const MemoizedDataTableRow = React.memo(DataTableRow) as typeof DataTableRow;

interface DataTableActionBarProps<TData extends RowData> {
  table: TanstackTable<DataTableFeatures, TData>;
  actionBar: React.ReactNode;
}

function DataTableActionBar<TData extends RowData>({
  table,
  actionBar,
}: DataTableActionBarProps<TData>) {
  return (
    <Subscribe
      source={table.atoms.rowSelection}
      selector={() => table.getSelectedRowIds().length > 0}
    >
      {(hasSelectedRows) => (hasSelectedRows ? actionBar : null)}
    </Subscribe>
  );
}

interface DataTableCellSlotProps extends useRender.ComponentProps<"td"> {
  pinned?: boolean;
}

function DataTableCellSlot({
  pinned = false,
  className,
  render,
  ...props
}: DataTableCellSlotProps) {
  return useRender({
    defaultTagName: "td",
    props: mergeProps<"td">(
      {
        className: cn(
          "overflow-hidden",
          pinned &&
            "bg-background transition-colors group-hover/row:bg-[color-mix(in_srgb,var(--muted)_50%,var(--background))] group-has-aria-expanded/row:bg-[color-mix(in_srgb,var(--muted)_50%,var(--background))] group-data-[state=selected]/row:bg-muted",
          className,
        ),
      },
      props,
    ),
    render,
  });
}
src/registry/bases/base/components/data-table/data-table-column-header.tsx
"use client";

import {
  type Column,
  type RowData,
  type SortDirection,
  type SortingState,
  Subscribe,
} from "@tanstack/react-table";
import { cn } from "cn";
import * as React from "react";

import type { DataTableFeatures } from "@/lib/data-table-features";

import { useDirection } from "@/registry/bases/base/ui/direction";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/registry/bases/base/ui/dropdown-menu";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";

interface DataTableColumnHeaderProps<
  TData extends RowData,
  TValue,
> extends React.ComponentProps<typeof DropdownMenuTrigger> {
  column: Column<DataTableFeatures, TData, TValue>;
  label: string;
}

export function DataTableColumnHeader<TData extends RowData, TValue>({
  column,
  label,
  className,
  ...props
}: DataTableColumnHeaderProps<TData, TValue>) {
  if (!column.getCanSort() && !column.getCanHide()) {
    return <div className={cn(className)}>{label}</div>;
  }

  return (
    <Subscribe
      source={column.table.store}
      selector={(state) => ({
        isVisible: state.columnVisibility[column.id] !== false,
        sortDirection: getSortDirection(state.sorting, column.id),
      })}
    >
      {(headerState) => (
        <DataTableColumnHeaderMenu
          column={column}
          label={label}
          className={className}
          isVisible={headerState.isVisible}
          sortDirection={headerState.sortDirection}
          {...props}
        />
      )}
    </Subscribe>
  );
}

interface DataTableColumnHeaderMenuProps<
  TData extends RowData,
  TValue,
> extends DataTableColumnHeaderProps<TData, TValue> {
  isVisible: boolean;
  sortDirection: SortDirection | "none";
}

function DataTableColumnHeaderMenu<TData extends RowData, TValue>({
  column,
  label,
  className,
  isVisible,
  sortDirection,
  ...props
}: DataTableColumnHeaderMenuProps<TData, TValue>) {
  const dir = useDirection();

  function onSortDirectionChange(direction: SortDirection) {
    if (sortDirection === direction) {
      column.clearSorting();
      return;
    }

    column.toggleSorting(direction === "desc", true);
  }

  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        className={cn(
          "-ms-1.5 flex h-8 items-center gap-1.5 rounded-md px-2 py-1.5 hover:bg-accent focus:ring-1 focus:ring-ring focus:outline-none data-[state=open]:bg-accent rtl:flex-row-reverse [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground",
          className,
        )}
        {...props}
      >
        {label}
        {column.getCanSort() &&
          (sortDirection === "desc" ? (
            <IconPlaceholder
              lucide="ChevronDown"
              tabler="IconChevronDown"
              hugeicons="ArrowDown01Icon"
              phosphor="CaretDownIcon"
              remixicon="RiArrowDownSLine"
            />
          ) : sortDirection === "asc" ? (
            <IconPlaceholder
              lucide="ChevronUp"
              tabler="IconChevronUp"
              hugeicons="ArrowUp01Icon"
              phosphor="CaretUpIcon"
              remixicon="RiArrowUpSLine"
            />
          ) : (
            <IconPlaceholder
              lucide="ChevronsUpDown"
              tabler="IconSelector"
              hugeicons="UnfoldMoreIcon"
              phosphor="CaretUpDownIcon"
              remixicon="RiArrowUpDownLine"
            />
          ))}
      </DropdownMenuTrigger>
      <DropdownMenuContent
        dir={dir}
        align="start"
        className="w-28 data-closed:fill-mode-forwards"
      >
        {column.getCanSort() && (
          <>
            <DropdownMenuCheckboxItem
              className="[&_svg]:text-muted-foreground"
              checked={sortDirection === "asc"}
              onClick={() => onSortDirectionChange("asc")}
            >
              <IconPlaceholder
                lucide="ChevronUp"
                tabler="IconChevronUp"
                hugeicons="ArrowUp01Icon"
                phosphor="CaretUpIcon"
                remixicon="RiArrowUpSLine"
              />
              Asc
            </DropdownMenuCheckboxItem>
            <DropdownMenuCheckboxItem
              className="[&_svg]:text-muted-foreground"
              checked={sortDirection === "desc"}
              onClick={() => onSortDirectionChange("desc")}
            >
              <IconPlaceholder
                lucide="ChevronDown"
                tabler="IconChevronDown"
                hugeicons="ArrowDown01Icon"
                phosphor="CaretDownIcon"
                remixicon="RiArrowDownSLine"
              />
              Desc
            </DropdownMenuCheckboxItem>
          </>
        )}
        {column.getCanHide() && (
          <DropdownMenuCheckboxItem
            className="[&_svg]:text-muted-foreground"
            checked={!isVisible}
            onClick={() => column.toggleVisibility(false)}
          >
            <IconPlaceholder
              lucide="EyeOff"
              tabler="IconEyeClosed"
              hugeicons="ViewOffIcon"
              phosphor="EyeSlashIcon"
              remixicon="RiEyeOffLine"
            />
            Hide
          </DropdownMenuCheckboxItem>
        )}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

function getSortDirection(
  sorting: SortingState,
  columnId: string,
): SortDirection | "none" {
  const sort = sorting.find((sort) => sort.id === columnId);
  if (!sort) return "none";
  return sort.desc ? "desc" : "asc";
}
src/registry/bases/base/components/data-table/data-table-pagination.tsx
"use client";

import type * as React from "react";

import { type RowData, Subscribe, type Table } from "@tanstack/react-table";
import { cn } from "cn";

import type { DataTableFeatures } from "@/lib/data-table-features";

import { Button } from "@/registry/bases/base/ui/button";
import { useDirection } from "@/registry/bases/base/ui/direction";
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/registry/bases/base/ui/select";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";

interface DataTablePaginationProps<
  TData extends RowData,
> extends React.ComponentProps<"div"> {
  table: Table<DataTableFeatures, TData>;
  pageSizeOptions?: number[];
}

export function DataTablePagination<TData extends RowData>({
  table,
  pageSizeOptions = [10, 20, 30, 40, 50],
  className,
  ...props
}: DataTablePaginationProps<TData>) {
  return (
    <Subscribe
      source={table.store}
      selector={(state) => ({
        pageIndex: state.pagination.pageIndex,
        pageSize: state.pagination.pageSize,
        selectedRowCount: table.getSelectedRowIds().length,
      })}
    >
      {({ pageIndex, pageSize, selectedRowCount }) => (
        <DataTablePaginationContent
          table={table}
          pageIndex={pageIndex}
          pageSize={pageSize}
          selectedRowCount={selectedRowCount}
          pageSizeOptions={pageSizeOptions}
          className={className}
          {...props}
        />
      )}
    </Subscribe>
  );
}

interface DataTablePaginationContentProps<
  TData extends RowData,
> extends React.ComponentProps<"div"> {
  table: Table<DataTableFeatures, TData>;
  pageIndex: number;
  pageSize: number;
  selectedRowCount: number;
  pageSizeOptions: number[];
}

function DataTablePaginationContent<TData extends RowData>({
  table,
  pageIndex,
  pageSize,
  selectedRowCount,
  pageSizeOptions,
  className,
  ...props
}: DataTablePaginationContentProps<TData>) {
  const pageCount = table.getPageCount();
  const canPreviousPage = table.getCanPreviousPage();
  const canNextPage = table.getCanNextPage();
  const dir = useDirection();

  return (
    <div
      className={cn(
        "flex w-full flex-col-reverse items-center justify-between gap-4 overflow-auto p-1 sm:flex-row sm:gap-8",
        className,
      )}
      {...props}
    >
      <div className="flex-1 text-sm whitespace-nowrap text-muted-foreground">
        {selectedRowCount} {selectedRowCount === 1 ? "row" : "rows"} selected.
      </div>
      <div className="flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
        <div className="flex items-center gap-2">
          <p className="text-sm font-medium whitespace-nowrap">Rows per page</p>
          <Select
            value={`${pageSize}`}
            onValueChange={(value) => {
              if (value == null) return;
              table.setPageSize(Number(value));
            }}
          >
            <SelectTrigger className="w-18">
              <SelectValue placeholder={pageSize} />
            </SelectTrigger>
            <SelectContent side="top" dir={dir}>
              <SelectGroup>
                {pageSizeOptions.map((pageSize) => (
                  <SelectItem key={pageSize} value={`${pageSize}`}>
                    {pageSize}
                  </SelectItem>
                ))}
              </SelectGroup>
            </SelectContent>
          </Select>
        </div>
        <div className="flex items-center justify-center text-sm font-medium">
          Page {pageIndex + 1} of {pageCount}
        </div>
        <div className="flex items-center gap-2">
          <Button
            aria-label="Go to first page"
            variant="outline"
            size="icon"
            className="hidden lg:flex"
            onClick={() => table.setPageIndex(0)}
            disabled={!canPreviousPage}
          >
            <IconPlaceholder
              lucide="ChevronsLeft"
              tabler="IconChevronsLeft"
              hugeicons="ArrowLeftDoubleIcon"
              phosphor="CaretDoubleLeftIcon"
              remixicon="RiSkipLeftLine"
              className="rtl:rotate-180"
            />
          </Button>
          <Button
            aria-label="Go to previous page"
            variant="outline"
            size="icon"
            onClick={() => table.previousPage()}
            disabled={!canPreviousPage}
          >
            <IconPlaceholder
              lucide="ChevronLeft"
              tabler="IconChevronLeft"
              hugeicons="ArrowLeft01Icon"
              phosphor="CaretLeftIcon"
              remixicon="RiArrowLeftSLine"
              className="rtl:rotate-180"
            />
          </Button>
          <Button
            aria-label="Go to next page"
            variant="outline"
            size="icon"
            onClick={() => table.nextPage()}
            disabled={!canNextPage}
          >
            <IconPlaceholder
              lucide="ChevronRight"
              tabler="IconChevronRight"
              hugeicons="ArrowRight01Icon"
              phosphor="CaretRightIcon"
              remixicon="RiArrowRightSLine"
              className="rtl:rotate-180"
            />
          </Button>
          <Button
            aria-label="Go to last page"
            variant="outline"
            size="icon"
            className="hidden lg:flex"
            onClick={() => table.setPageIndex(pageCount - 1)}
            disabled={!canNextPage}
          >
            <IconPlaceholder
              lucide="ChevronsRight"
              tabler="IconChevronsRight"
              hugeicons="ArrowRightDoubleIcon"
              phosphor="CaretDoubleRightIcon"
              remixicon="RiSkipRightLine"
              className="rtl:rotate-180"
            />
          </Button>
        </div>
      </div>
    </div>
  );
}
src/registry/bases/base/components/data-table/data-table-view-options.tsx
"use client";

import { type RowData, Subscribe, type Table } from "@tanstack/react-table";
import { cn } from "cn";
import * as React from "react";

import type { DataTableFeatures } from "@/lib/data-table-features";

import { Button } from "@/registry/bases/base/ui/button";
import { useDirection } from "@/registry/bases/base/ui/direction";
import {
  Faceted,
  FacetedContent,
  FacetedEmpty,
  FacetedGroup,
  FacetedInput,
  FacetedItem,
  FacetedItemIndicator,
  FacetedList,
  FacetedTrigger,
} from "@/registry/bases/base/ui/faceted";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";

interface DataTableViewOptionsProps<
  TData extends RowData,
> extends React.ComponentProps<typeof FacetedContent> {
  table: Table<DataTableFeatures, TData>;
  disabled?: boolean;
}

export function DataTableViewOptions<TData extends RowData>({
  table,
  disabled,
  className,
  ...props
}: DataTableViewOptionsProps<TData>) {
  const dir = useDirection();
  const columns = React.useMemo(
    () =>
      table
        .getAllColumns()
        .filter(
          (column) =>
            typeof column.accessorFn !== "undefined" && column.getCanHide(),
        ),
    [table],
  );

  return (
    <Subscribe source={table.atoms.columnVisibility}>
      {(columnVisibility) => (
        <Faceted
          multiple
          value={columns
            .filter((column) => columnVisibility[column.id] !== false)
            .map((column) => column.id)}
        >
          <FacetedTrigger
            render={
              <Button
                aria-label="Toggle columns"
                role="combobox"
                variant="outline"
                className="ms-auto hidden lg:flex"
                disabled={disabled}
              />
            }
          >
            <IconPlaceholder
              lucide="Settings2"
              tabler="IconSettings"
              hugeicons="Settings05Icon"
              phosphor="GearIcon"
              remixicon="RiSettingsLine"
              className="text-muted-foreground"
            />
            View
          </FacetedTrigger>
          <FacetedContent
            dir={dir}
            align="center"
            className={cn("w-44", className)}
            {...props}
          >
            <FacetedInput placeholder="Search columns..." />
            <FacetedList>
              <FacetedEmpty>No columns found.</FacetedEmpty>
              <FacetedGroup>
                {columns.map((column) => (
                  <FacetedItem
                    key={column.id}
                    value={column.id}
                    onSelect={() => column.toggleVisibility()}
                  >
                    <span className="truncate">
                      {column.columnDef.meta?.label ?? column.id}
                    </span>
                    <FacetedItemIndicator />
                  </FacetedItem>
                ))}
              </FacetedGroup>
            </FacetedList>
          </FacetedContent>
        </Faceted>
      )}
    </Subscribe>
  );
}
src/registry/bases/base/components/data-table/data-table-faceted-filter.tsx
"use client";

import type * as React from "react";

import { type Column, type RowData, Subscribe } from "@tanstack/react-table";

import type { DataTableFeatures } from "@/lib/data-table-features";
import type { FilterOption } from "@/lib/data-table-types";

import { Button } from "@/registry/bases/base/ui/button";
import { useDirection } from "@/registry/bases/base/ui/direction";
import {
  Faceted,
  FacetedClear,
  FacetedContent,
  FacetedEmpty,
  FacetedGroup,
  FacetedInput,
  FacetedItem,
  FacetedItemIndicator,
  FacetedList,
  FacetedSeparator,
  FacetedTrigger,
  FacetedValue,
} from "@/registry/bases/base/ui/faceted";
import { Separator } from "@/registry/bases/base/ui/separator";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";

interface DataTableFacetedFilterProps<TData extends RowData, TValue> {
  column?: Column<DataTableFeatures, TData, TValue>;
  title?: string;
  options: FilterOption[];
  multiple?: boolean;
}

export function DataTableFacetedFilter<TData extends RowData, TValue>({
  column,
  ...props
}: DataTableFacetedFilterProps<TData, TValue>) {
  if (!column) {
    return <DataTableFacetedFilterContent column={column} {...props} />;
  }

  return (
    <Subscribe
      source={column.table.atoms.columnFilters}
      selector={() => column.getFilterValue()}
    >
      {(filterValue) => (
        <DataTableFacetedFilterContent
          column={column}
          columnFilterValue={filterValue}
          {...props}
        />
      )}
    </Subscribe>
  );
}

interface DataTableFacetedFilterContentProps<
  TData extends RowData,
  TValue,
> extends DataTableFacetedFilterProps<TData, TValue> {
  columnFilterValue?: unknown;
}

function DataTableFacetedFilterContent<TData extends RowData, TValue>({
  column,
  title,
  options,
  multiple = false,
  columnFilterValue,
}: DataTableFacetedFilterContentProps<TData, TValue>) {
  const dir = useDirection();
  const selectedValues = Array.isArray(columnFilterValue)
    ? columnFilterValue.filter(
        (value): value is string => typeof value === "string",
      )
    : [];
  const hasSelection = selectedValues.length > 0;

  function onValueChange(value: string | string[] | undefined) {
    const values = Array.isArray(value) ? value : value ? [value] : [];
    column?.setFilterValue(values.length > 0 ? values : undefined);
  }

  function onReset(event?: React.MouseEvent) {
    event?.stopPropagation();
    column?.setFilterValue(undefined);
  }

  return (
    <Faceted
      value={multiple ? selectedValues : selectedValues[0]}
      onValueChange={onValueChange}
      items={options}
      multiple={multiple}
    >
      <FacetedTrigger
        render={<Button variant="outline" className="border-dashed" />}
      >
        {hasSelection ? (
          <div
            role="button"
            aria-label={`Clear ${title} filter`}
            tabIndex={0}
            className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
            onClick={onReset}
          >
            <IconPlaceholder
              lucide="XCircle"
              tabler="IconCircleX"
              hugeicons="Cancel01Icon"
              phosphor="XCircleIcon"
              remixicon="RiCloseCircleLine"
            />
          </div>
        ) : (
          <IconPlaceholder
            lucide="PlusCircle"
            tabler="IconCirclePlus"
            hugeicons="PlusSignCircleIcon"
            phosphor="PlusCircleIcon"
            remixicon="RiAddCircleLine"
          />
        )}
        {title}
        {hasSelection && (
          <>
            <Separator
              orientation="vertical"
              className="mx-0.5 data-vertical:h-4 data-vertical:self-center"
            />
            <FacetedValue />
          </>
        )}
      </FacetedTrigger>
      <FacetedContent dir={dir} className="w-50">
        <FacetedInput placeholder={title} />
        <FacetedList className="max-h-full">
          <FacetedEmpty>No results found.</FacetedEmpty>
          <FacetedGroup className="max-h-75 scroll-py-1 overflow-x-hidden overflow-y-auto">
            {options.map((option) => (
              <FacetedItem key={option.value} value={option.value}>
                {option.icon && <option.icon />}
                <span className="flex-1 truncate">{option.label}</span>
                <FacetedItemIndicator>{option.count}</FacetedItemIndicator>
              </FacetedItem>
            ))}
          </FacetedGroup>
          {hasSelection && (
            <>
              <FacetedSeparator />
              <FacetedGroup>
                <FacetedClear>Clear filters</FacetedClear>
              </FacetedGroup>
            </>
          )}
        </FacetedList>
      </FacetedContent>
    </Faceted>
  );
}
src/registry/bases/base/components/data-table/data-table-toolbar.tsx
"use client";

import {
  type Column,
  type RowData,
  Subscribe,
  type Table,
} from "@tanstack/react-table";
import { cn } from "cn";
import * as React from "react";

import type { DataTableFeatures } from "@/lib/data-table-features";

import { DataTableDateFilter } from "@/registry/bases/base/components/data-table/data-table-date-filter";
import { DataTableFacetedFilter } from "@/registry/bases/base/components/data-table/data-table-faceted-filter";
import { DataTableSliderFilter } from "@/registry/bases/base/components/data-table/data-table-slider-filter";
import { DataTableViewOptions } from "@/registry/bases/base/components/data-table/data-table-view-options";
import { Button } from "@/registry/bases/base/ui/button";
import { Input } from "@/registry/bases/base/ui/input";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";

interface DataTableToolbarProps<
  TData extends RowData,
> extends React.ComponentProps<"div"> {
  table: Table<DataTableFeatures, TData>;
}

export function DataTableToolbar<TData extends RowData>({
  table,
  children,
  className,
  ...props
}: DataTableToolbarProps<TData>) {
  const columns = React.useMemo(
    () => table.getAllColumns().filter((column) => column.getCanFilter()),
    [table],
  );

  function onReset() {
    table.resetColumnFilters(true);
    table.resetJoinOperator(true);
  }

  return (
    <div
      role="toolbar"
      aria-orientation="horizontal"
      className={cn(
        "flex w-full items-start justify-between gap-2 p-1",
        className,
      )}
      {...props}
    >
      <div className="flex flex-1 flex-wrap items-center gap-2">
        {columns.map((column) => (
          <DataTableToolbarFilter key={column.id} column={column} />
        ))}
        <Subscribe
          source={table.atoms.columnFilters}
          selector={(filters) => filters.length > 0}
        >
          {(isFiltered) =>
            isFiltered && (
              <Button
                aria-label="Reset filters"
                variant="outline"
                className="border-dashed"
                onClick={onReset}
              >
                <IconPlaceholder
                  lucide="X"
                  tabler="IconX"
                  hugeicons="Cancel01Icon"
                  phosphor="XIcon"
                  remixicon="RiCloseLine"
                />
                Reset
              </Button>
            )
          }
        </Subscribe>
      </div>
      <div className="flex items-center gap-2">
        {children}
        <DataTableViewOptions table={table} align="end" />
      </div>
    </div>
  );
}
interface DataTableToolbarFilterProps<TData extends RowData> {
  column: Column<DataTableFeatures, TData>;
}

function DataTableToolbarFilter<TData extends RowData>({
  column,
}: DataTableToolbarFilterProps<TData>) {
  const columnMeta = column.columnDef.meta;
  if (!columnMeta?.variant) return null;

  const title = columnMeta.label ?? column.id;
  const placeholder = columnMeta.placeholder ?? columnMeta.label;

  switch (columnMeta.variant) {
    case "text":
      return (
        <DataTableFilterInput
          column={column}
          placeholder={placeholder}
          className="w-40 lg:w-56"
        />
      );

    case "number":
      return (
        <div className="relative">
          <DataTableFilterInput
            column={column}
            type="number"
            inputMode="numeric"
            placeholder={placeholder}
            className={cn("w-30", columnMeta.unit && "pe-8")}
          />
          {columnMeta.unit && (
            <span className="absolute inset-e-0 top-0 bottom-0 flex items-center rounded-e-md bg-accent px-2 text-sm text-muted-foreground">
              {columnMeta.unit}
            </span>
          )}
        </div>
      );

    case "range":
      return <DataTableSliderFilter column={column} title={title} />;

    case "date":
    case "dateRange":
      return (
        <DataTableDateFilter
          column={column}
          title={title}
          multiple={columnMeta.variant === "dateRange"}
        />
      );

    case "select":
    case "multiSelect":
      return (
        <DataTableFacetedFilter
          column={column}
          title={title}
          options={columnMeta.options ?? []}
          multiple={columnMeta.variant === "multiSelect"}
        />
      );

    default:
      return null;
  }
}

function readFilterInputValue(value: unknown) {
  if (typeof value === "string" || typeof value === "number") {
    return String(value);
  }

  if (Array.isArray(value)) {
    return value.join(",");
  }

  return "";
}

interface DataTableFilterInputProps<
  TData extends RowData,
> extends React.ComponentProps<"input"> {
  column: Column<DataTableFeatures, TData>;
}

function DataTableFilterInput<TData extends RowData>({
  column,
  type = "text",
  ...props
}: DataTableFilterInputProps<TData>) {
  return (
    <Subscribe
      source={column.table.atoms.columnFilters}
      selector={() => column.getFilterValue()}
    >
      {(filterValue) => (
        <Input
          type={type}
          {...props}
          value={readFilterInputValue(filterValue)}
          onChange={(event) =>
            column.setFilterValue(event.target.value || undefined)
          }
        />
      )}
    </Subscribe>
  );
}
src/registry/bases/base/components/data-table/data-table-slider-filter.tsx
"use client";

import { type Column, type RowData, Subscribe } from "@tanstack/react-table";
import { cn } from "cn";
import * as React from "react";

import type { DataTableFeatures } from "@/lib/data-table-features";

import { Button } from "@/registry/bases/base/ui/button";
import { useDirection } from "@/registry/bases/base/ui/direction";
import { Input } from "@/registry/bases/base/ui/input";
import { Label } from "@/registry/bases/base/ui/label";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/registry/bases/base/ui/popover";
import { Separator } from "@/registry/bases/base/ui/separator";
import { Slider } from "@/registry/bases/base/ui/slider";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";

interface Range {
  min: number;
  max: number;
}

type RangeValue = [number, number];

function getIsValidRange(value: unknown): value is RangeValue {
  return (
    Array.isArray(value) &&
    value.length === 2 &&
    typeof value[0] === "number" &&
    typeof value[1] === "number"
  );
}

function parseRangeBounds(
  value: unknown,
): [number | undefined, number | undefined] | undefined {
  if (!Array.isArray(value) || value.length !== 2) return undefined;

  const [start, end] = value.map(parseRangeBound);
  if (start === null || end === null) return undefined;
  if (start === undefined && end === undefined) return undefined;

  return [start, end];
}

function parseRangeBound(value: unknown) {
  if (value === undefined || value === "") return undefined;
  if (typeof value !== "number" && typeof value !== "string") return null;

  const number = Number(value);
  return Number.isNaN(number) ? null : number;
}

interface DataTableSliderFilterProps<TData extends RowData> {
  column: Column<DataTableFeatures, TData>;
  title?: string;
}

export function DataTableSliderFilter<TData extends RowData>({
  column,
  ...props
}: DataTableSliderFilterProps<TData>) {
  return (
    <Subscribe
      source={column.table.atoms.columnFilters}
      selector={() => column.getFilterValue()}
    >
      {(filterValue) => (
        <DataTableSliderFilterContent
          column={column}
          columnFilterValue={filterValue}
          {...props}
        />
      )}
    </Subscribe>
  );
}

function DataTableSliderFilterContent<TData extends RowData>({
  column,
  title,
  columnFilterValue: columnFilterValueProp,
}: DataTableSliderFilterProps<TData> & {
  columnFilterValue: unknown;
}) {
  const dir = useDirection();
  const id = React.useId();

  const defaultRange = column.columnDef.meta?.range;
  const unit = column.columnDef.meta?.unit;

  const { min, max, step } = React.useMemo<Range & { step: number }>(() => {
    let minValue = 0;
    let maxValue = 100;

    if (defaultRange && getIsValidRange(defaultRange)) {
      [minValue, maxValue] = defaultRange;
    } else {
      const values = column.getFacetedMinMaxValues();
      if (values && Array.isArray(values) && values.length === 2) {
        const [facetMinValue, facetMaxValue] = values;
        if (
          typeof facetMinValue === "number" &&
          typeof facetMaxValue === "number"
        ) {
          minValue = facetMinValue;
          maxValue = facetMaxValue;
        }
      }
    }

    const rangeSize = maxValue - minValue;
    const step =
      rangeSize <= 20
        ? 1
        : rangeSize <= 100
          ? Math.ceil(rangeSize / 20)
          : Math.ceil(rangeSize / 50);

    return { min: minValue, max: maxValue, step };
  }, [column, defaultRange]);

  // An open-ended bound, like `estimatedHours=,8`, shows as the column limit.
  const bounds = parseRangeBounds(columnFilterValueProp);
  const columnFilterValue: RangeValue | undefined = bounds
    ? [bounds[0] ?? min, bounds[1] ?? max]
    : undefined;
  const range: RangeValue = columnFilterValue ?? [min, max];

  const formatValue = React.useCallback((value: number) => {
    return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
  }, []);

  const onFromInputChange = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      const numValue = Number(event.target.value);
      if (!Number.isNaN(numValue) && numValue >= min && numValue <= range[1]) {
        column.setFilterValue([numValue, range[1]]);
      }
    },
    [column, min, range],
  );

  const onToInputChange = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      const numValue = Number(event.target.value);
      if (!Number.isNaN(numValue) && numValue <= max && numValue >= range[0]) {
        column.setFilterValue([range[0], numValue]);
      }
    },
    [column, max, range],
  );

  const onSliderValueChange = React.useCallback(
    (value: number | readonly number[]) => {
      if (Array.isArray(value) && value.length === 2) {
        column.setFilterValue(value);
      }
    },
    [column],
  );

  const onReset = React.useCallback(
    (event: React.MouseEvent) => {
      if (event.target instanceof HTMLDivElement) {
        event.stopPropagation();
      }
      column.setFilterValue(undefined);
    },
    [column],
  );

  return (
    <Popover>
      <PopoverTrigger
        render={<Button variant="outline" className="border-dashed" />}
      >
        {columnFilterValue ? (
          <div
            role="button"
            aria-label={`Clear ${title} filter`}
            tabIndex={0}
            className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
            onClick={onReset}
          >
            <IconPlaceholder
              lucide="XCircle"
              tabler="IconCircleX"
              hugeicons="Cancel01Icon"
              phosphor="XCircleIcon"
              remixicon="RiCloseCircleLine"
            />
          </div>
        ) : (
          <IconPlaceholder
            lucide="PlusCircle"
            tabler="IconCirclePlus"
            hugeicons="PlusSignCircleIcon"
            phosphor="PlusCircleIcon"
            remixicon="RiAddCircleLine"
          />
        )}
        <span>{title}</span>
        {columnFilterValue ? (
          <>
            <Separator
              orientation="vertical"
              className="mx-0.5 data-vertical:h-4 data-vertical:self-center"
            />
            {formatValue(columnFilterValue[0])} -{" "}
            {formatValue(columnFilterValue[1])}
            {unit ? ` ${unit}` : ""}
          </>
        ) : null}
      </PopoverTrigger>
      <PopoverContent
        dir={dir}
        align="start"
        className="flex w-auto flex-col gap-4"
      >
        <div className="flex flex-col gap-3">
          <p className="leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
            {title}
          </p>
          <div className="flex items-center gap-4">
            <Label htmlFor={`${id}-from`} className="sr-only">
              From
            </Label>
            <div className="relative">
              <Input
                id={`${id}-from`}
                type="number"
                aria-valuemin={min}
                aria-valuemax={max}
                inputMode="numeric"
                pattern="[0-9]*"
                placeholder={min.toString()}
                min={min}
                max={max}
                value={range[0]?.toString()}
                onChange={onFromInputChange}
                className={cn("w-24", unit && "pe-8")}
              />
              {unit && (
                <span className="absolute inset-e-0 top-0 bottom-0 flex items-center rounded-e-md bg-accent px-2 text-sm text-muted-foreground">
                  {unit}
                </span>
              )}
            </div>
            <Label htmlFor={`${id}-to`} className="sr-only">
              to
            </Label>
            <div className="relative">
              <Input
                id={`${id}-to`}
                type="number"
                aria-valuemin={min}
                aria-valuemax={max}
                inputMode="numeric"
                pattern="[0-9]*"
                placeholder={max.toString()}
                min={min}
                max={max}
                value={range[1]?.toString()}
                onChange={onToInputChange}
                className={cn("w-24", unit && "pe-8")}
              />
              {unit && (
                <span className="absolute inset-e-0 top-0 bottom-0 flex items-center rounded-e-md bg-accent px-2 text-sm text-muted-foreground">
                  {unit}
                </span>
              )}
            </div>
          </div>
          <Label htmlFor={`${id}-slider`} className="sr-only">
            {title} slider
          </Label>
          <Slider
            id={`${id}-slider`}
            min={min}
            max={max}
            step={step}
            value={range}
            onValueChange={onSliderValueChange}
          />
        </div>
        <Button
          aria-label={`Clear ${title} filter`}
          variant="outline"
          onClick={onReset}
        >
          Clear
        </Button>
      </PopoverContent>
    </Popover>
  );
}
src/registry/bases/base/components/data-table/data-table-date-filter.tsx
"use client";

import type { DateRange } from "react-day-picker";

import { type Column, type RowData, Subscribe } from "@tanstack/react-table";
import * as React from "react";

import type { DataTableFeatures } from "@/lib/data-table-features";

import { formatDate } from "@/lib/format";
import { Button } from "@/registry/bases/base/ui/button";
import { useDirection } from "@/registry/bases/base/ui/direction";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/registry/bases/base/ui/popover";
import { Separator } from "@/registry/bases/base/ui/separator";
import { Calendar } from "@/registry/bases/radix/ui/calendar";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";

type DateSelection = Date[] | DateRange;

function getIsDateRange(value: DateSelection): value is DateRange {
  return value && typeof value === "object" && !Array.isArray(value);
}

function parseAsDate(timestamp: number | string | undefined): Date | undefined {
  if (!timestamp) return undefined;
  const numericTimestamp =
    typeof timestamp === "string" ? Number(timestamp) : timestamp;
  const date = new Date(numericTimestamp);
  return !Number.isNaN(date.getTime()) ? date : undefined;
}

function parseColumnFilterValue(value: unknown) {
  if (value === null || value === undefined) {
    return [];
  }

  if (Array.isArray(value)) {
    return value.map((item) => {
      if (typeof item === "number" || typeof item === "string") {
        return item;
      }
      return undefined;
    });
  }

  if (typeof value === "string" || typeof value === "number") {
    return [value];
  }

  return [];
}

interface DataTableDateFilterProps<TData extends RowData> {
  column: Column<DataTableFeatures, TData>;
  title?: string;
  multiple?: boolean;
}

export function DataTableDateFilter<TData extends RowData>({
  column,
  ...props
}: DataTableDateFilterProps<TData>) {
  return (
    <Subscribe
      source={column.table.atoms.columnFilters}
      selector={() => column.getFilterValue()}
    >
      {(filterValue) => (
        <DataTableDateFilterContent
          column={column}
          columnFilterValue={filterValue}
          {...props}
        />
      )}
    </Subscribe>
  );
}

function DataTableDateFilterContent<TData extends RowData>({
  column,
  title,
  multiple,
  columnFilterValue,
}: DataTableDateFilterProps<TData> & {
  columnFilterValue: unknown;
}) {
  const dir = useDirection();
  const selectedDates = React.useMemo<DateSelection>(() => {
    if (!columnFilterValue) {
      return multiple ? { from: undefined, to: undefined } : [];
    }

    if (multiple) {
      const timestamps = parseColumnFilterValue(columnFilterValue);
      return {
        from: parseAsDate(timestamps[0]),
        to: parseAsDate(timestamps[1]),
      };
    }

    const timestamps = parseColumnFilterValue(columnFilterValue);
    const date = parseAsDate(timestamps[0]);
    return date ? [date] : [];
  }, [columnFilterValue, multiple]);

  const onSelect = React.useCallback(
    (date: Date | DateRange | undefined) => {
      if (!date) {
        column.setFilterValue(undefined);
        return;
      }

      if (multiple && !("getTime" in date)) {
        const from = date.from?.getTime();
        const to = date.to?.getTime();
        column.setFilterValue(from || to ? [from, to] : undefined);
      } else if (!multiple && "getTime" in date) {
        column.setFilterValue(date.getTime());
      }
    },
    [column, multiple],
  );

  const onReset = React.useCallback(
    (event: React.MouseEvent) => {
      event.stopPropagation();
      column.setFilterValue(undefined);
    },
    [column],
  );

  const hasValue = React.useMemo(() => {
    if (multiple) {
      if (!getIsDateRange(selectedDates)) return false;
      return selectedDates.from || selectedDates.to;
    }
    if (!Array.isArray(selectedDates)) return false;
    return selectedDates.length > 0;
  }, [multiple, selectedDates]);

  const formatDateRange = React.useCallback((range: DateRange) => {
    if (!range.from && !range.to) return "";
    if (range.from && range.to) {
      return `${formatDate(range.from)} - ${formatDate(range.to)}`;
    }
    return formatDate(range.from ?? range.to);
  }, []);

  const label = React.useMemo(() => {
    if (multiple) {
      if (!getIsDateRange(selectedDates)) return null;

      const hasSelectedDates = selectedDates.from || selectedDates.to;
      const dateText = hasSelectedDates
        ? formatDateRange(selectedDates)
        : "Select date range";

      return (
        <span className="flex items-center gap-2">
          <span>{title}</span>
          {hasSelectedDates && (
            <>
              <Separator
                orientation="vertical"
                className="mx-0.5 data-vertical:h-4 data-vertical:self-center"
              />
              <span>{dateText}</span>
            </>
          )}
        </span>
      );
    }

    if (getIsDateRange(selectedDates)) return null;

    const hasSelectedDate = selectedDates.length > 0;
    const dateText = hasSelectedDate
      ? formatDate(selectedDates[0])
      : "Select date";

    return (
      <span className="flex items-center gap-2">
        <span>{title}</span>
        {hasSelectedDate && (
          <>
            <Separator
              orientation="vertical"
              className="mx-0.5 data-vertical:h-4 data-vertical:self-center"
            />
            <span>{dateText}</span>
          </>
        )}
      </span>
    );
  }, [selectedDates, multiple, formatDateRange, title]);

  return (
    <Popover>
      <PopoverTrigger
        render={<Button variant="outline" className="border-dashed" />}
      >
        {hasValue ? (
          <div
            role="button"
            aria-label={`Clear ${title} filter`}
            tabIndex={0}
            onClick={onReset}
            className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
          >
            <IconPlaceholder
              lucide="XCircle"
              tabler="IconCircleX"
              hugeicons="Cancel01Icon"
              phosphor="XCircleIcon"
              remixicon="RiCloseCircleLine"
            />
          </div>
        ) : (
          <IconPlaceholder
            lucide="CalendarIcon"
            tabler="IconCalendar"
            hugeicons="CalendarIcon"
            phosphor="CalendarIcon"
            remixicon="RiCalendarLine"
          />
        )}
        {label}
      </PopoverTrigger>
      <PopoverContent dir={dir} className="w-auto p-0" align="start">
        {multiple ? (
          <Calendar
            autoFocus
            captionLayout="dropdown"
            mode="range"
            selected={
              getIsDateRange(selectedDates)
                ? selectedDates
                : { from: undefined, to: undefined }
            }
            onSelect={onSelect}
          />
        ) : (
          <Calendar
            captionLayout="dropdown"
            mode="single"
            selected={
              !getIsDateRange(selectedDates) ? selectedDates[0] : undefined
            }
            onSelect={onSelect}
          />
        )}
      </PopoverContent>
    </Popover>
  );
}
src/registry/bases/base/components/data-table/data-table-skeleton.tsx
import type * as React from "react";

import { cn } from "cn";

import { Skeleton } from "@/registry/bases/base/ui/skeleton";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/registry/bases/base/ui/table";

interface DataTableSkeletonProps extends React.ComponentProps<"div"> {
  columnCount: number;
  rowCount?: number;
  filterCount?: number;
  cellWidths?: string[];
  withViewOptions?: boolean;
  withPagination?: boolean;
  shrinkZero?: boolean;
}

export function DataTableSkeleton({
  columnCount,
  rowCount = 10,
  filterCount = 0,
  cellWidths = ["auto"],
  withViewOptions = true,
  withPagination = true,
  shrinkZero = false,
  className,
  ...props
}: DataTableSkeletonProps) {
  const cozyCellWidths = Array.from(
    { length: columnCount },
    (_, index) => cellWidths[index % cellWidths.length] ?? "auto",
  );

  return (
    <div
      className={cn("flex w-full flex-col gap-2.5 overflow-auto", className)}
      {...props}
    >
      <div className="flex w-full items-center justify-between gap-2 overflow-auto p-1">
        <div className="flex flex-1 items-center gap-2">
          {filterCount > 0
            ? Array.from({ length: filterCount }).map((_, i) => (
                <Skeleton
                  key={i}
                  className="h-8 w-18 rounded-lg border-dashed"
                />
              ))
            : null}
        </div>
        {withViewOptions ? (
          <Skeleton className="ms-auto hidden h-8 w-18 rounded-lg lg:flex" />
        ) : null}
      </div>
      <div className="rounded-md border">
        <Table>
          <TableHeader>
            {Array.from({ length: 1 }).map((_, i) => (
              <TableRow key={i} className="hover:bg-transparent">
                {Array.from({ length: columnCount }).map((_, j) => (
                  <TableHead
                    key={j}
                    style={{
                      width: cozyCellWidths[j],
                      minWidth: shrinkZero ? cozyCellWidths[j] : "auto",
                    }}
                  >
                    <Skeleton className="h-6 w-full" />
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {Array.from({ length: rowCount }).map((_, i) => (
              <TableRow key={i} className="hover:bg-transparent">
                {Array.from({ length: columnCount }).map((_, j) => (
                  <TableCell
                    key={j}
                    style={{
                      width: cozyCellWidths[j],
                      minWidth: shrinkZero ? cozyCellWidths[j] : "auto",
                    }}
                  >
                    <Skeleton className="h-6 w-full" />
                  </TableCell>
                ))}
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </div>
      {withPagination ? (
        <div className="flex w-full items-center justify-between gap-4 overflow-auto p-1 sm:gap-8">
          <Skeleton className="h-8 w-40 shrink-0 rounded-lg" />
          <div className="flex items-center gap-4 sm:gap-6 lg:gap-8">
            <div className="flex items-center gap-2">
              <Skeleton className="h-8 w-24 rounded-lg" />
              <Skeleton className="h-8 w-18 rounded-lg" />
            </div>
            <div className="flex items-center justify-center text-sm font-medium">
              <Skeleton className="h-8 w-20 rounded-lg" />
            </div>
            <div className="flex items-center gap-2">
              <Skeleton className="hidden size-8 rounded-lg lg:block" />
              <Skeleton className="size-8 rounded-lg" />
              <Skeleton className="size-8 rounded-lg" />
              <Skeleton className="hidden size-8 rounded-lg lg:block" />
            </div>
          </div>
        </div>
      ) : null}
    </div>
  );
}
src/registry/bases/base/ui/faceted.tsx
"use client";

import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "cn";
import * as React from "react";

import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandList,
  CommandSeparator,
} from "@/registry/bases/base/ui/command";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/registry/bases/base/ui/popover";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";

type FacetedSelectedValue<Multiple extends boolean> = Multiple extends true
  ? string[]
  : string;

interface FacetedOption {
  value: string;
  label: string;
}

const NO_ITEMS: FacetedOption[] = [];

interface FacetedState {
  value: string | string[] | undefined;
  open: boolean;
}

interface FacetedStore {
  subscribe: (callback: () => void) => () => void;
  getState: () => FacetedState;
  getProps: () => FacetedController;
  notify: () => void;
  setOpen: (open: boolean) => void;
  selectItem: (value: string) => void;
  clear: () => void;
}

interface FacetedController {
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  value?: string | string[];
  onValueChange?: (value: string | string[] | undefined) => void;
  isControlled: boolean;
  multiple: boolean;
  items: FacetedOption[];
}

function getSelection(
  state: FacetedState,
  props: FacetedController,
): string | string[] | undefined {
  return props.isControlled ? props.value : state.value;
}

function getHasSelection(value: string | string[] | undefined) {
  if (Array.isArray(value)) return value.length > 0;
  return !!value;
}

function getIsValueSelected(
  value: string | string[] | undefined,
  multiple: boolean,
  itemValue: string,
) {
  if (multiple) return Array.isArray(value) && value.includes(itemValue);
  return value === itemValue;
}

function getSelectedItems(
  value: string | string[] | undefined,
  items: FacetedOption[],
) {
  const values = Array.isArray(value) ? value : value ? [value] : [];

  return values.map((itemValue) => ({
    value: itemValue,
    label: items.find((item) => item.value === itemValue)?.label ?? itemValue,
  }));
}

function getIsFacetedSelectedValue<Multiple extends boolean>(
  value: string | string[] | undefined,
  multiple: Multiple,
): value is FacetedSelectedValue<Multiple> | undefined {
  if (value === undefined) return true;
  return multiple ? Array.isArray(value) : typeof value === "string";
}

function createFacetedStore(
  propsRef: React.RefObject<FacetedController>,
  state: FacetedState,
): FacetedStore {
  const listeners = new Set<() => void>();

  function commitValue(nextValue: string | string[] | undefined) {
    const props = propsRef.current;
    if (!props.isControlled && !Object.is(state.value, nextValue)) {
      state.value = nextValue;
      store.notify();
    }
    props.onValueChange?.(nextValue);
  }

  const store: FacetedStore = {
    subscribe: (callback) => {
      listeners.add(callback);
      return () => listeners.delete(callback);
    },
    getState: () => state,
    getProps: () => propsRef.current,
    notify: () => {
      for (const callback of listeners) {
        callback();
      }
    },
    setOpen: (open) => {
      const { open: openProp, onOpenChange } = propsRef.current;
      if (openProp === undefined && state.open !== open) {
        state.open = open;
        store.notify();
      }
      onOpenChange?.(open);
    },
    selectItem: (selectedValue) => {
      const props = propsRef.current;
      const currentValue = getSelection(state, props);

      if (props.multiple) {
        const selected = Array.isArray(currentValue) ? currentValue : [];
        commitValue(
          selected.includes(selectedValue)
            ? selected.filter((item) => item !== selectedValue)
            : [...selected, selectedValue],
        );
        return;
      }

      commitValue(currentValue === selectedValue ? undefined : selectedValue);
      store.setOpen(false);
    },
    clear: () => {
      commitValue(propsRef.current.multiple ? [] : undefined);
    },
  };

  return store;
}

function useFacetedStoreSelector<T>(
  store: FacetedStore,
  selector: (state: FacetedState) => T,
): T {
  const getSnapshot = () => selector(store.getState());
  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
}

const FacetedStoreContext = React.createContext<FacetedStore | null>(null);

function useFacetedStore(name: string) {
  const store = React.useContext(FacetedStoreContext);
  if (!store) {
    throw new Error(`\`${name}\` must be used within \`Faceted\`.`);
  }
  return store;
}

interface FacetedProps<Multiple extends boolean = false> extends Omit<
  React.ComponentProps<typeof Popover>,
  "onOpenChange"
> {
  value?: FacetedSelectedValue<Multiple>;
  defaultValue?: FacetedSelectedValue<Multiple>;
  onValueChange?: (value: FacetedSelectedValue<Multiple> | undefined) => void;
  onOpenChange?: (open: boolean) => void;
  items?: FacetedOption[];
  multiple?: Multiple;
}

function Faceted<Multiple extends boolean = false>(
  props: FacetedProps<Multiple>,
) {
  const {
    open: openProp,
    defaultOpen = false,
    onOpenChange,
    value,
    defaultValue,
    onValueChange,
    items = NO_ITEMS,
    multiple = false as Multiple,
    ...popoverProps
  } = props;

  const controller: FacetedController = {
    open: openProp,
    onOpenChange,
    value,
    onValueChange(nextValue) {
      if (!getIsFacetedSelectedValue(nextValue, multiple)) return;
      onValueChange?.(nextValue);
    },
    isControlled: "value" in props,
    multiple,
    items,
  };
  const propsRef = React.useRef(controller);
  propsRef.current = controller;

  const storeRef = React.useRef<FacetedStore | null>(null);
  storeRef.current ??= createFacetedStore(propsRef, {
    value: defaultValue,
    open: defaultOpen,
  });
  const store = storeRef.current;
  const open = useFacetedStoreSelector(store, (state) => state.open);

  return (
    <FacetedStoreContext.Provider value={store}>
      <Popover
        data-slot="faceted"
        open={openProp ?? open}
        onOpenChange={store.setOpen}
        {...popoverProps}
      />
    </FacetedStoreContext.Provider>
  );
}

function FacetedTrigger({
  className,
  ...props
}: React.ComponentProps<typeof PopoverTrigger>) {
  return (
    <PopoverTrigger
      data-slot="faceted-trigger"
      className={cn("justify-between text-left", className)}
      {...props}
    />
  );
}

interface FacetedValueProps {
  placeholder?: React.ReactNode;
  children?: React.ReactNode | ((selected: FacetedOption[]) => React.ReactNode);
}

function FacetedValue({
  placeholder = "Select options...",
  children,
}: FacetedValueProps) {
  const store = useFacetedStore("FacetedValue");
  const value = useFacetedStoreSelector(store, (state) =>
    getSelection(state, store.getProps()),
  );
  const selected = getSelectedItems(value, store.getProps().items);

  if (typeof children === "function") return children(selected);
  if (children != null) return children;

  if (selected.length === 0) {
    return (
      <span
        data-slot="faceted-value"
        className="flex w-full items-center gap-1 text-muted-foreground"
      >
        {placeholder}
        <IconPlaceholder
          lucide="ChevronDown"
          tabler="IconChevronDown"
          hugeicons="ArrowDown01Icon"
          phosphor="CaretDownIcon"
          remixicon="RiArrowDownSLine"
          className="ml-auto size-4 shrink-0 text-muted-foreground"
        />
      </span>
    );
  }

  if (selected.length > 2) {
    return (
      <FacetedBadgeList>
        <FacetedBadge>{selected.length} selected</FacetedBadge>
      </FacetedBadgeList>
    );
  }

  return (
    <FacetedBadgeList>
      {selected.map((item) => (
        <FacetedBadge key={item.value}>
          <span className="truncate">{item.label}</span>
        </FacetedBadge>
      ))}
    </FacetedBadgeList>
  );
}

function FacetedBadgeList({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="faceted-badge-list"
      className={cn(
        "flex w-max max-w-full flex-wrap items-center gap-1",
        className,
      )}
      {...props}
    />
  );
}

function FacetedBadge({
  className,
  render,
  ...props
}: useRender.ComponentProps<"span">) {
  return useRender({
    defaultTagName: "span",
    render,
    state: {
      slot: "faceted-badge",
    },
    props: mergeProps<"span">(
      {
        className: cn(
          "flex h-[calc(--spacing(5.25))] w-fit max-w-full min-w-0 items-center justify-center gap-1 rounded-sm bg-input/60 px-1.5 text-xs font-medium whitespace-nowrap text-foreground [&_svg]:pointer-events-none [&_svg]:size-3 [&_svg]:shrink-0",
          className,
        ),
      },
      props,
    ),
  });
}

function FacetedContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof PopoverContent>) {
  return (
    <PopoverContent
      data-slot="faceted-content"
      align="start"
      className={cn("w-50 origin-(--transform-origin) p-0", className)}

      {...props}
    >
      <Command className="p-0.5">{children}</Command>
    </PopoverContent>
  );
}

function FacetedInput({
  ref,
  ...props
}: React.ComponentProps<typeof CommandInput>) {
  return <CommandInput data-slot="faceted-input" ref={ref} {...props} />;
}

function FacetedList(props: React.ComponentProps<typeof CommandList>) {
  return <CommandList data-slot="faceted-list" {...props} />;
}

function FacetedEmpty(props: React.ComponentProps<typeof CommandEmpty>) {
  return <CommandEmpty data-slot="faceted-empty" {...props} />;
}

function FacetedGroup(props: React.ComponentProps<typeof CommandGroup>) {
  return <CommandGroup data-slot="faceted-group" {...props} />;
}

function FacetedCommandItem({
  className,
  onPointerDownCapture,
  onMouseDown,
  onSelect,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
  return (
    <CommandPrimitive.Item
      className={cn(
        "group/command-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
        className,
      )}
      {...mergeProps<"div">(
        {
          onPointerDownCapture(event) {
            // A focusable item steals the input on pointerdown.
            event.preventDefault();
          },
          onMouseDown(event) {
            // iOS Safari can emit a synthetic mousedown without a pointerdown.
            event.preventDefault();
          },
        },
        { onPointerDownCapture, onMouseDown },
      )}
      onSelect={onSelect}
      {...props}
    />
  );
}

interface FacetedItemProps extends React.ComponentProps<
  typeof CommandPrimitive.Item
> {
  value: string;
}

function FacetedItem({ value, onSelect, ...props }: FacetedItemProps) {
  const store = useFacetedStore("FacetedItem");
  const isSelected = useFacetedStoreSelector(store, (state) => {
    const props = store.getProps();
    return getIsValueSelected(
      getSelection(state, props),
      props.multiple,
      value,
    );
  });

  return (
    <FacetedCommandItem
      data-slot="faceted-item"
      data-checked={isSelected || undefined}
      aria-checked={isSelected}
      onSelect={() => {
        if (onSelect) {
          onSelect(value);
          return;
        }
        store.selectItem(value);
      }}
      {...props}
    />
  );
}

function FacetedItemIndicator({
  className,
  children,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="faceted-item-indicator"
      className={cn(
        "ml-auto grid place-items-center *:col-start-1 *:row-start-1",
        className,
      )}
      {...props}
    >
      {children != null && (
        <span className="font-mono text-xs group-data-checked/command-item:invisible">
          {children}
        </span>
      )}
      <IconPlaceholder
        lucide="Check"
        tabler="IconCheck"
        hugeicons="Tick02Icon"
        phosphor="CheckIcon"
        remixicon="RiCheckLine"
        className="invisible group-data-checked/command-item:visible"
      />
    </span>
  );
}

function FacetedClear({
  className,
  children = "Clear",
  onSelect,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
  const store = useFacetedStore("FacetedClear");
  const hasSelection = useFacetedStoreSelector(store, (state) =>
    getHasSelection(getSelection(state, store.getProps())),
  );

  if (!hasSelection) return null;

  return (
    <FacetedCommandItem
      data-slot="faceted-clear"
      className={cn("justify-center text-center", className)}
      onSelect={(value) => {
        onSelect?.(value);
        store.clear();
      }}
      {...props}
    >
      {children}
    </FacetedCommandItem>
  );
}

function FacetedSeparator(
  props: React.ComponentProps<typeof CommandSeparator>,
) {
  return (
    <CommandSeparator
      data-slot="faceted-separator"
      className="mx-0 my-0.5 w-full"
      {...props}
    />
  );
}

export {
  Faceted,
  FacetedBadge,
  FacetedBadgeList,
  FacetedClear,
  FacetedContent,
  FacetedEmpty,
  FacetedGroup,
  FacetedInput,
  FacetedItem,
  FacetedItemIndicator,
  FacetedList,
  FacetedSeparator,
  FacetedTrigger,
  FacetedValue,
};
src/registry/bases/base/ui/direction.tsx
"use client";

export {
  DirectionProvider,
  useDirection,
} from "@base-ui/react/direction-provider";
src/registry/bases/base/hooks/use-data-table.ts
import {
  type ColumnFiltersState,
  functionalUpdate,
  type PaginationState,
  type RowData,
  type SortingState,
  type TableOptions,
  type Updater,
  useTable,
} from "@tanstack/react-table";
import {
  parseAsInteger,
  parseAsStringEnum,
  useQueryState,
  type UseQueryStateOptions,
  useQueryStates,
} from "nuqs";
import * as React from "react";

import type {
  ColumnFilterItem,
  FilterVariant,
  JoinOperator,
  DataTableQueryKeys,
} from "@/lib/data-table-types";

import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import {
  type DataTableFeatures,
  dataTableFeatures,
} from "@/lib/data-table-features";
import {
  getColumnFilterParser,
  getColumnFilters,
  getColumnFiltersKey,
  getSortingStateParser,
  sortColumnFiltersBySearch,
} from "@/lib/data-table-parsers";
import {
  getActiveFilters,
  JOIN_OPERATORS,
  normalizeColumnFilter,
} from "@/lib/data-table-utils";

const PAGE_KEY = "page";
const PER_PAGE_KEY = "perPage";
const SORT_KEY = "sort";
const JOIN_OPERATOR_KEY = "joinOperator";
const DEBOUNCE_MS = 300;
const THROTTLE_MS = 50;
const DEFAULT_PAGE_SIZE = 10;
const EMPTY_SORTING: SortingState = [];

interface FiltersDraft {
  filters: ColumnFiltersState;
  urlFiltersKey: string;
}

type UseDataTableProps<TData extends RowData> = Omit<
  TableOptions<DataTableFeatures, TData>,
  | "state"
  | "pageCount"
  | "features"
  | "manualFiltering"
  | "manualPagination"
  | "manualSorting"
> & {
  queryKeys?: Partial<DataTableQueryKeys>;
  history?: "push" | "replace";
  debounceMs?: number;
  throttleMs?: number;
  clearOnDefault?: boolean;
  scroll?: boolean;
  shallow?: boolean;
  startTransition?: React.TransitionStartFunction;
} & (
    | {
        mode?: "server";
        pageCount: number;
      }
    | {
        mode: "client";
        pageCount?: never;
      }
  );

function useDataTable<TData extends RowData>({
  columns,
  mode = "server",
  pageCount,
  initialState,
  queryKeys,
  history = "replace",
  debounceMs = DEBOUNCE_MS,
  throttleMs = THROTTLE_MS,
  clearOnDefault = false,
  scroll = false,
  shallow: shallowProp = true,
  startTransition,
  ...props
}: UseDataTableProps<TData>) {
  const isServer = mode === "server";
  const shallow = isServer ? shallowProp : true;

  const pageKey = queryKeys?.page ?? PAGE_KEY;
  const perPageKey = queryKeys?.perPage ?? PER_PAGE_KEY;
  const sortKey = queryKeys?.sort ?? SORT_KEY;
  const joinOperatorKey = queryKeys?.joinOperator ?? JOIN_OPERATOR_KEY;

  const queryStateOptions = React.useMemo<
    Omit<UseQueryStateOptions<string>, "parse">
  >(
    () => ({
      history,
      scroll,
      shallow,
      throttleMs,
      debounceMs,
      clearOnDefault,
      startTransition,
    }),
    [
      history,
      scroll,
      shallow,
      throttleMs,
      debounceMs,
      clearOnDefault,
      startTransition,
    ],
  );

  const [initialTableState] = React.useState(initialState);

  const pageParser = React.useMemo(
    () => parseAsInteger.withOptions(queryStateOptions).withDefault(1),
    [queryStateOptions],
  );
  const perPageParser = React.useMemo(
    () =>
      parseAsInteger
        .withOptions(queryStateOptions)
        .withDefault(
          initialTableState?.pagination?.pageSize ?? DEFAULT_PAGE_SIZE,
        ),
    [initialTableState, queryStateOptions],
  );

  const [page, setPage] = useQueryState(pageKey, pageParser);
  const [perPage, setPerPage] = useQueryState(perPageKey, perPageParser);

  const pagination = React.useMemo<PaginationState>(
    () => ({ pageIndex: page - 1, pageSize: perPage }),
    [page, perPage],
  );

  function onPaginationChange(updater: Updater<PaginationState>) {
    const next = functionalUpdate(updater, pagination);
    void setPage(next.pageIndex + 1);
    void setPerPage(next.pageSize);
  }

  const columnIndex = React.useMemo(() => {
    const sortableIds = new Set<string>();
    const filterableVariants = new Map<string, FilterVariant>();

    for (const column of columns) {
      if (!column.id) continue;
      const hasAccessor = "accessorKey" in column || "accessorFn" in column;
      if (hasAccessor && column.enableSorting !== false) {
        sortableIds.add(column.id);
      }
      if (!column.enableColumnFilter) continue;

      filterableVariants.set(column.id, column.meta?.variant ?? "text");
    }

    return {
      sortableIds,
      filterableIds: [...filterableVariants.keys()],
      variantById: Object.fromEntries(filterableVariants),
      normalizeColumnFilters: (filters: ColumnFiltersState) =>
        filters.map((filter) =>
          normalizeColumnFilter(
            filter,
            filterableVariants.get(filter.id) ?? "text",
          ),
        ),
    };
  }, [columns]);

  const sortingParser = React.useMemo(
    () =>
      getSortingStateParser(columnIndex.sortableIds)
        .withOptions(queryStateOptions)
        .withDefault(initialTableState?.sorting ?? EMPTY_SORTING),
    [columnIndex, initialTableState, queryStateOptions],
  );

  const [sorting, setSorting] = useQueryState(sortKey, sortingParser);

  function onSortingChange(updater: Updater<SortingState>) {
    void setSorting(functionalUpdate(updater, sorting));
  }

  const joinOperatorParser = React.useMemo(
    () =>
      parseAsStringEnum([...JOIN_OPERATORS])
        .withOptions(queryStateOptions)
        .withDefault(initialTableState?.joinOperator ?? "and"),
    [initialTableState, queryStateOptions],
  );

  const [joinOperator, setJoinOperator] = useQueryState(
    joinOperatorKey,
    joinOperatorParser,
  );

  function onJoinOperatorChange(updater: Updater<JoinOperator>) {
    void setJoinOperator(functionalUpdate(updater, joinOperator));
  }

  const filterParsers = React.useMemo(
    () =>
      Object.fromEntries(
        columnIndex.filterableIds.map((id) => [
          id,
          getColumnFilterParser(
            id,
            columnIndex.variantById[id] ?? "text",
          ).withOptions(queryStateOptions),
        ]),
      ),
    [columnIndex, queryStateOptions],
  );

  const [filterParams, setFilterParams] = useQueryStates(filterParsers);

  const search = React.useSyncExternalStore(
    subscribeToHistory,
    getLocationSearch,
    getServerLocationSearch,
  );

  const urlFilters = React.useMemo(
    () =>
      sortColumnFiltersBySearch(
        getColumnFilters(
          columnIndex.filterableIds,
          (id) => filterParams[id] ?? [],
        ),
        search,
      ),
    [columnIndex, filterParams, search],
  );
  const urlFiltersKey = getColumnFiltersKey(urlFilters);

  const [filtersDraft, setFiltersDraft] = React.useState<FiltersDraft | null>(
    () => {
      const initialFilters = initialTableState?.columnFilters;
      if (urlFilters.length > 0 || !initialFilters?.length) return null;

      return {
        filters: columnIndex.normalizeColumnFilters(initialFilters),
        urlFiltersKey,
      };
    },
  );

  const columnFilters =
    filtersDraft?.urlFiltersKey === urlFiltersKey
      ? filtersDraft.filters
      : urlFilters;

  const debouncedWriteFilters = useDebouncedCallback(
    (columnFilters: ColumnFiltersState, sourceUrlFiltersKey: string) => {
      if (sourceUrlFiltersKey !== urlFiltersKey) return;

      const filters = getActiveFilters(
        columnIndex.normalizeColumnFilters(columnFilters),
      ).filter((filter) => Object.hasOwn(columnIndex.variantById, filter.id));
      const params = new Map<string, ColumnFilterItem[] | null>();

      for (const filter of filters) {
        const group = params.get(filter.id);
        if (group) group.push(filter);
        else params.set(filter.id, [filter]);
      }
      for (const id of columnIndex.filterableIds) {
        if (!params.has(id)) params.set(id, null);
      }

      const nextUrlFiltersKey = getColumnFiltersKey(filters);
      setFiltersDraft(
        (prev) => prev && { ...prev, urlFiltersKey: nextUrlFiltersKey },
      );
      void setPage(1);
      void setFilterParams(Object.fromEntries(params));
    },
    debounceMs,
  );

  function onColumnFiltersChange(updater: Updater<ColumnFiltersState>) {
    const filters = functionalUpdate(updater, columnFilters);
    setFiltersDraft({ filters, urlFiltersKey });
    debouncedWriteFilters(filters, urlFiltersKey);
  }

  const table = useTable(
    {
      ...props,
      features: dataTableFeatures,
      columns,
      initialState: initialTableState,
      pageCount: isServer ? pageCount : undefined,
      state: {
        pagination,
        sorting,
        columnFilters,
        joinOperator,
      },
      onPaginationChange,
      onSortingChange,
      onColumnFiltersChange,
      onJoinOperatorChange,
      manualPagination: isServer,
      manualSorting: isServer,
      manualFiltering: isServer,
    },
    (state) => ({
      columnFilters: state.columnFilters,
      joinOperator: state.joinOperator,
      pagination: state.pagination,
      sorting: state.sorting,
    }),
  );

  return React.useMemo(() => ({ table }), [table]);
}

function subscribeToHistory(onChange: () => void) {
  window.addEventListener("popstate", onChange);
  return () => window.removeEventListener("popstate", onChange);
}

function getLocationSearch() {
  return window.location.search;
}

function getServerLocationSearch() {
  return "";
}

export { useDataTable, type UseDataTableProps };
src/hooks/use-debounced-callback.ts
import * as React from "react";

export function useDebouncedCallback<T extends (...args: never[]) => unknown>(
  callback: T,
  delay: number,
) {
  const callbackRef = React.useRef(callback);
  callbackRef.current = callback;

  const debounceTimerRef = React.useRef(0);

  React.useEffect(
    () => () => window.clearTimeout(debounceTimerRef.current),
    [],
  );

  return React.useCallback(
    (...args: Parameters<T>) => {
      window.clearTimeout(debounceTimerRef.current);
      debounceTimerRef.current = window.setTimeout(
        () => callbackRef.current(...args),
        delay,
      );
    },
    [delay],
  );
}
src/lib/data-table-utils.ts
import type {
  Column,
  ColumnFilter,
  RowData,
  Table,
} from "@tanstack/react-table";

import type { DataTableFeatures } from "@/lib/data-table-features";
import type {
  ColumnFilterItem,
  FilterOperator,
  FilterOperatorOption,
  FilterVariant,
} from "@/lib/data-table-types";

import { formatDate } from "@/lib/format";

export const FILTER_VARIANTS = [
  "text",
  "number",
  "range",
  "date",
  "dateRange",
  "boolean",
  "select",
  "multiSelect",
] as const;

export const FILTER_OPERATORS = {
  iLike: "ilike",
  notILike: "not.ilike",
  eq: "eq",
  ne: "neq",
  inArray: "in",
  notInArray: "not.in",
  isEmpty: "is.empty",
  isNotEmpty: "not.is.empty",
  lt: "lt",
  lte: "lte",
  gt: "gt",
  gte: "gte",
  isBetween: "between",
  isRelativeToToday: "rel",
} as const;

export const JOIN_OPERATORS = ["and", "or"] as const;

export const SORT_ORDERS = [
  { label: "Asc", value: "asc" },
  { label: "Desc", value: "desc" },
] as const;

const TEXT_OPERATORS = [
  { label: "Contains", value: "iLike" },
  { label: "Does not contain", value: "notILike" },
  { label: "Is", value: "eq" },
  { label: "Is not", value: "ne" },
  { label: "Is empty", value: "isEmpty" },
  { label: "Is not empty", value: "isNotEmpty" },
] satisfies FilterOperatorOption[];

const NUMERIC_OPERATORS = [
  { label: "Is", value: "eq" },
  { label: "Is not", value: "ne" },
  { label: "Is less than", value: "lt" },
  { label: "Is less than or equal to", value: "lte" },
  { label: "Is greater than", value: "gt" },
  { label: "Is greater than or equal to", value: "gte" },
  { label: "Is between", value: "isBetween" },
  { label: "Is empty", value: "isEmpty" },
  { label: "Is not empty", value: "isNotEmpty" },
] satisfies FilterOperatorOption[];

const DATE_OPERATORS = [
  { label: "Is", value: "eq" },
  { label: "Is not", value: "ne" },
  { label: "Is before", value: "lt" },
  { label: "Is after", value: "gt" },
  { label: "Is on or before", value: "lte" },
  { label: "Is on or after", value: "gte" },
  { label: "Is between", value: "isBetween" },
  { label: "Is relative to today", value: "isRelativeToToday" },
  { label: "Is empty", value: "isEmpty" },
  { label: "Is not empty", value: "isNotEmpty" },
] satisfies FilterOperatorOption[];

const SELECT_OPERATORS = [
  { label: "Is", value: "eq" },
  { label: "Is not", value: "ne" },
  { label: "Is empty", value: "isEmpty" },
  { label: "Is not empty", value: "isNotEmpty" },
] satisfies FilterOperatorOption[];

const MULTI_SELECT_OPERATORS = [
  { label: "Has any of", value: "inArray" },
  { label: "Has none of", value: "notInArray" },
  { label: "Is empty", value: "isEmpty" },
  { label: "Is not empty", value: "isNotEmpty" },
] satisfies FilterOperatorOption[];

const BOOLEAN_OPERATORS = [
  { label: "Is", value: "eq" },
  { label: "Is not", value: "ne" },
] satisfies FilterOperatorOption[];

const MULTI_VALUE_FILTER_VARIANTS = [
  "select",
  "multiSelect",
  "range",
  "dateRange",
] satisfies FilterVariant[];

const FILTER_OPERATORS_BY_VARIANT: Record<
  FilterVariant,
  FilterOperatorOption[]
> = {
  text: TEXT_OPERATORS,
  number: NUMERIC_OPERATORS,
  range: NUMERIC_OPERATORS,
  date: DATE_OPERATORS,
  dateRange: DATE_OPERATORS,
  boolean: BOOLEAN_OPERATORS,
  select: SELECT_OPERATORS,
  multiSelect: MULTI_SELECT_OPERATORS,
};

function getColumnVar(columnId: string, property: "size" | "offset") {
  const name = columnId.replace(
    /[^a-zA-Z0-9-]/g,
    (char) => `_${char.codePointAt(0)?.toString(16)}_`,
  );
  return `--column-${name}-${property}`;
}

export function getColumnPinningStyle<TData extends RowData>(
  column: Column<DataTableFeatures, TData>,
): React.CSSProperties {
  const isPinned = column.getIsPinned();

  return {
    insetInlineStart:
      isPinned === "start"
        ? `var(${getColumnVar(column.id, "offset")})`
        : undefined,
    insetInlineEnd:
      isPinned === "end"
        ? `var(${getColumnVar(column.id, "offset")})`
        : undefined,
    opacity: isPinned ? 0.97 : 1,
    position: isPinned ? "sticky" : "relative",
    width: `var(${getColumnVar(column.id, "size")})`,
    zIndex: isPinned ? 1 : undefined,
  };
}

export function getColumnSizingStyle<TData extends RowData>(
  table: Table<DataTableFeatures, TData>,
): React.CSSProperties {
  const style: Record<string, string> = {
    minWidth: `${table.getTotalSize()}px`,
  };

  for (const header of table.getFlatHeaders()) {
    style[getColumnVar(header.column.id, "size")] = `${header.getSize()}px`;
  }

  for (const { column } of table.getLeafHeaders()) {
    const isPinned = column.getIsPinned();
    if (!isPinned) continue;

    const offset =
      isPinned === "start" ? column.getStart("start") : column.getAfter("end");
    style[getColumnVar(column.id, "offset")] = `${offset}px`;
  }

  return style;
}

export function getFilterOperators(filterVariant: FilterVariant) {
  return FILTER_OPERATORS_BY_VARIANT[filterVariant] ?? [];
}

export function getDefaultFilterOperator(filterVariant: FilterVariant) {
  const operators = getFilterOperators(filterVariant);

  return operators[0]?.value ?? (filterVariant === "text" ? "iLike" : "eq");
}

export function getIsEditableTarget(target: EventTarget | null) {
  if (!(target instanceof HTMLElement)) return false;

  return (
    target.isContentEditable ||
    target instanceof HTMLInputElement ||
    target instanceof HTMLTextAreaElement ||
    target instanceof HTMLSelectElement
  );
}

export function getIsValuelessOperator(operator: FilterOperator) {
  return operator === "isEmpty" || operator === "isNotEmpty";
}

export function coerceFilterValue(
  operator: FilterOperator,
  value: string | string[],
) {
  if (getIsValuelessOperator(operator)) return "";

  if (operator === "inArray" || operator === "notInArray") {
    if (Array.isArray(value)) return value;
    return value ? [value] : [];
  }

  if (operator === "isBetween") {
    return Array.isArray(value) ? value : [value, ""];
  }

  if (Array.isArray(value)) return value.find((item) => item !== "") ?? "";

  return value;
}

export function getDefaultFilter<TData extends RowData>(
  column: Column<DataTableFeatures, TData>,
) {
  const variant = column.columnDef.meta?.variant ?? "text";

  return {
    id: column.id,
    variant,
    operator: getDefaultFilterOperator(variant),
    value: "",
  };
}

export function getSelectFilterValue(filter: ColumnFilterItem) {
  if (filter.variant === "multiSelect") {
    return Array.isArray(filter.value) ? filter.value : [];
  }

  return typeof filter.value === "string" ? filter.value : undefined;
}

const CALENDAR_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;

export function parseFilterDate(value: unknown): Date | undefined {
  if (value instanceof Date) return getValidDate(value);
  if (typeof value === "number") return getValidDate(new Date(value));
  if (typeof value !== "string" || value.trim() === "") return undefined;

  const text = value.trim();
  const match = CALENDAR_DATE_PATTERN.exec(text);
  if (match) {
    const [, year, month, day] = match.map(Number);
    if (year === undefined || month === undefined || day === undefined) {
      return undefined;
    }
    const date = new Date(year, month - 1, day);
    return date.getFullYear() === year &&
      date.getMonth() === month - 1 &&
      date.getDate() === day
      ? date
      : undefined;
  }

  const numeric = Number(text);
  return getValidDate(new Date(Number.isNaN(numeric) ? text : numeric));
}

export function formatFilterDate(date: Date) {
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${date.getFullYear()}-${month}-${day}`;
}

function getValidDate(date: Date) {
  return Number.isNaN(date.getTime()) ? undefined : date;
}

function stringifyFilterDate(value: unknown) {
  const date = parseFilterDate(value);
  return date ? formatFilterDate(date) : "";
}

export function getIsDateVariant(variant: FilterVariant) {
  return variant === "date" || variant === "dateRange";
}

function getFilterValueStringifier(
  variant: FilterVariant,
  operator: FilterOperator,
) {
  return getIsDateVariant(variant) && operator !== "isRelativeToToday"
    ? stringifyFilterDate
    : stringifyFilterValue;
}

export function getFilterDates(value: ColumnFilterItem["value"]) {
  return (Array.isArray(value) ? value : [value]).flatMap((item) => {
    const date = parseFilterDate(item);
    return date ? [date] : [];
  });
}

export function getFilterDateValue(date: Date | undefined) {
  return date ? formatFilterDate(date) : "";
}

export function getDateFilterLabel(filter: ColumnFilterItem) {
  const [startDate, endDate] = getFilterDates(filter.value);
  if (!startDate) return undefined;

  const start = formatDate(startDate, { month: "short" });
  if (
    filter.operator !== "isBetween" ||
    !endDate ||
    startDate.toDateString() === endDate.toDateString()
  ) {
    return start;
  }

  return `${start} - ${formatDate(endDate, { month: "short" })}`;
}

export function stringifyFilterValue(value: unknown): string {
  if (value == null) return "";
  if (value instanceof Date) return value.toISOString();
  if (typeof value === "object") return JSON.stringify(value);
  return String(value as string | number | boolean | bigint);
}

export function getIsMultiValueVariant(variant: FilterVariant) {
  return MULTI_VALUE_FILTER_VARIANTS.includes(variant);
}

export function getPlainFilterOperator(variant: FilterVariant): FilterOperator {
  if (variant === "select" || variant === "multiSelect") return "inArray";
  if (variant === "range" || variant === "dateRange") return "isBetween";
  return getDefaultFilterOperator(variant);
}

export function getIsPlainFilter(filter: ColumnFilterItem) {
  return (
    filter.operator === getPlainFilterOperator(filter.variant) &&
    Array.isArray(filter.value) === getIsMultiValueVariant(filter.variant)
  );
}

export function getPlainFilterId(columnId: string) {
  return `${columnId}-filter`;
}

export function createPlainFilter<TColumnId extends string>(
  id: TColumnId,
  variant: FilterVariant,
  value: unknown,
): ColumnFilterItem<TColumnId> | null {
  if (value === undefined || value === null || value === "") return null;

  const operator = getPlainFilterOperator(variant);
  const filterId = getPlainFilterId(id);
  const stringify = getFilterValueStringifier(variant, operator);

  if (getIsMultiValueVariant(variant)) {
    const values = (Array.isArray(value) ? value : [value]).map(stringify);
    if (values.every((item) => item === "")) return null;
    return { id, variant, operator, value: values, filterId };
  }

  if (Array.isArray(value)) return null;

  return { id, variant, operator, value: stringify(value), filterId };
}

export function normalizeColumnFilter(
  filter: ColumnFilter,
  variant: FilterVariant,
): ColumnFilterItem {
  const filterId = filter.filterId ?? getPlainFilterId(filter.id);

  if (filter.operator) {
    const filterVariant = filter.variant ?? variant;
    const stringify = getFilterValueStringifier(filterVariant, filter.operator);

    return {
      id: filter.id,
      variant: filterVariant,
      operator: filter.operator,
      value: Array.isArray(filter.value)
        ? filter.value.map(stringify)
        : stringify(filter.value),
      filterId,
    };
  }

  const item = createPlainFilter(filter.id, variant, filter.value);
  if (item) return { ...item, filterId };

  return {
    id: filter.id,
    variant,
    operator: getPlainFilterOperator(variant),
    value: getIsMultiValueVariant(variant) ? [] : "",
    filterId,
  };
}

export function getPlainFilterValue(filter: ColumnFilterItem): unknown {
  const { variant, value } = filter;

  if (variant === "select" || variant === "multiSelect") {
    return Array.isArray(value) ? value : [value];
  }

  if (variant === "range") {
    return Array.isArray(value)
      ? value.map((item) => (item === "" ? undefined : Number(item)))
      : value;
  }

  if (getIsDateVariant(variant)) {
    return Array.isArray(value)
      ? value.map((item) => parseFilterDate(item)?.getTime())
      : parseFilterDate(value)?.getTime();
  }

  return value;
}

export function getIsActiveFilter(filter: ColumnFilterItem) {
  return (
    getIsValuelessOperator(filter.operator) ||
    (Array.isArray(filter.value)
      ? filter.value.some((value) => value !== "")
      : filter.value !== "" &&
        filter.value !== null &&
        filter.value !== undefined)
  );
}

export function getActiveFilters<TFilterItem extends ColumnFilterItem>(
  filters: TFilterItem[],
): TFilterItem[] {
  return filters.filter(getIsActiveFilter);
}
src/lib/format.ts
export function formatDate(
  date: Date | string | number | undefined,
  opts: Intl.DateTimeFormatOptions = {},
) {
  if (!date) return "";

  try {
    return new Intl.DateTimeFormat("en-US", {
      month: opts.month ?? "long",
      day: opts.day ?? "numeric",
      year: opts.year ?? "numeric",
      ...opts,
    }).format(new Date(date));
  } catch {
    return "";
  }
}
src/lib/data-table-parsers.ts
import {
  createParser,
  parseAsInteger,
  parseAsNativeArrayOf,
  parseAsStringEnum,
} from "nuqs/server";
import { z } from "zod";

import type {
  ColumnFilterItem,
  ColumnSortItem,
  DataTableColumnConfig,
  DataTableColumnConfigs,
  DataTableColumnConfigsQuery,
  FilterableColumnId,
  FilterOperator,
  FilterVariant,
  JoinOperator,
  SortableColumnId,
} from "@/lib/data-table-types";

import {
  FILTER_OPERATORS,
  getFilterOperators,
  getIsActiveFilter,
  getIsValuelessOperator,
  getPlainFilterOperator,
  JOIN_OPERATORS,
  normalizeColumnFilter,
} from "@/lib/data-table-utils";

const sortingItemSchema = z.object({
  id: z.string(),
  desc: z.boolean(),
});

export const getSortingStateParser = <TColumnId extends string = string>(
  columnIds?: readonly TColumnId[] | Set<TColumnId>,
) => {
  const validIds = getIdSet(columnIds);

  return createParser<ColumnSortItem<TColumnId>[]>({
    parse: (value) => {
      const sorting = parseSorting(value);
      return sorting && getHasKnownIds(sorting, validIds) ? sorting : null;
    },
    serialize: (value) => {
      const compact = value
        .map((item) => `${item.id}.${item.desc ? "desc" : "asc"}`)
        .join(",");
      const parsed = parseSorting(compact);

      // Ids with a comma, or a first id starting with `[`, don't round-trip.
      return parsed && getIsSameSorting(parsed, value)
        ? compact
        : JSON.stringify(value);
    },
    eq: getIsSameSorting,
  });
};

const OPERATORS_BY_NAME = new Map(
  getKeys(FILTER_OPERATORS).flatMap((operator): [string, FilterOperator][] => [
    [FILTER_OPERATORS[operator], operator],
    [operator.toLowerCase(), operator],
  ]),
);

const MAX_OPERATOR_PART_COUNT = 3;

const LIST_OPERATORS = new Set<FilterOperator>([
  "inArray",
  "notInArray",
  "isBetween",
]);

export function parseColumnFilter<TColumnId extends string>(
  id: TColumnId,
  variant: FilterVariant,
  param: string,
): ColumnFilterItem<TColumnId> {
  const { operator, rawValue } = splitFilterOperator(param, variant);

  const filter = normalizeColumnFilter(
    {
      id,
      variant,
      operator,
      value: getIsValuelessOperator(operator)
        ? ""
        : LIST_OPERATORS.has(operator)
          ? parseListValue(rawValue, operator)
          : rawValue,
    },
    variant,
  );

  return { ...filter, id };
}

export function serializeColumnFilter(filter: ColumnFilterItem) {
  const name = FILTER_OPERATORS[filter.operator];
  if (getIsValuelessOperator(filter.operator)) return name;

  const rawValue = Array.isArray(filter.value)
    ? joinListValue(filter.value)
    : filter.value;

  if (filter.operator === getPlainFilterOperator(filter.variant)) {
    const implicit = parseColumnFilter(filter.id, filter.variant, rawValue);
    if (getIsSameFilter(implicit, filter)) return rawValue;
  }

  return `${name}.${rawValue}`;
}

export function getColumnFilterParser<TColumnId extends string>(
  id: TColumnId,
  variant: FilterVariant,
) {
  return parseAsNativeArrayOf(
    createParser<ColumnFilterItem<TColumnId>>({
      // nuqs drops items that parse to null, so `?title=` reads as no filter.
      parse: (param) => {
        const filter = parseColumnFilter(id, variant, param);
        return getIsActiveFilter(filter) ? filter : null;
      },
      serialize: serializeColumnFilter,
      eq: (a, b) => serializeColumnFilter(a) === serializeColumnFilter(b),
    }),
  );
}

export function getColumnFilters<TColumnId extends string>(
  columnIds: readonly TColumnId[],
  getFilters: (id: TColumnId) => ColumnFilterItem<TColumnId>[],
) {
  return columnIds.flatMap((id) =>
    getFilters(id).map((filter, index) => ({
      ...filter,
      filterId: `${id}-${index}`,
    })),
  );
}

export function sortColumnFiltersBySearch<TFilter extends ColumnFilterItem>(
  filters: TFilter[],
  search: string | URLSearchParams,
) {
  const positions = new Map<string, number>();
  for (const key of new URLSearchParams(search).keys()) {
    if (!positions.has(key)) positions.set(key, positions.size);
  }

  return [...filters].sort(
    (a, b) =>
      (positions.get(a.id) ?? Number.POSITIVE_INFINITY) -
      (positions.get(b.id) ?? Number.POSITIVE_INFINITY),
  );
}

export function getColumnFiltersKey(filters: ColumnFilterItem[]) {
  return filters
    .map((filter) => `${filter.id}=${serializeColumnFilter(filter)}`)
    .sort()
    .join("&");
}

export function getFilterableColumns<
  TColumnConfigs extends DataTableColumnConfigs,
>(columnConfigs: TColumnConfigs) {
  const entries = getKeys<keyof TColumnConfigs & string>(columnConfigs).flatMap(
    (id) => {
      const variant = columnConfigs[id]?.variant;
      return variant ? [[id, variant] as const] : [];
    },
  );

  return Object.fromEntries(entries) as Record<
    FilterableColumnId<TColumnConfigs>,
    FilterVariant
  >;
}

export function getSortableColumns<
  TColumnConfigs extends DataTableColumnConfigs,
>(columnConfigs: TColumnConfigs) {
  return getKeys<keyof TColumnConfigs & string>(columnConfigs).filter(
    (id): id is SortableColumnId<TColumnConfigs> =>
      columnConfigs[id]?.isSortable !== false,
  );
}

export function getColumnOptions(config: DataTableColumnConfig) {
  return {
    enableColumnFilter: config.variant !== undefined,
    enableSorting: config.isSortable !== false,
  };
}

interface DataTableSearchParamsOptions<
  TColumnConfigs extends DataTableColumnConfigs,
> {
  columnConfigs: TColumnConfigs;
  defaultSorting?: ColumnSortItem<NoInfer<SortableColumnId<TColumnConfigs>>>[];
  defaultPerPage?: number;
}

export function getDataTableSearchParams<
  TColumnConfigs extends DataTableColumnConfigs,
>({
  columnConfigs,
  defaultSorting = [],
  defaultPerPage = 10,
}: DataTableSearchParamsOptions<TColumnConfigs>) {
  return {
    page: parseAsInteger.withDefault(1),
    perPage: parseAsInteger.withDefault(defaultPerPage),
    sort: getSortingStateParser(getSortableColumns(columnConfigs)).withDefault(
      defaultSorting,
    ),
    joinOperator: parseAsStringEnum([...JOIN_OPERATORS]).withDefault("and"),
    ...getFilterParsers(getFilterableColumns(columnConfigs)),
  };
}

interface DataTableSearch<TSortColumnId extends string> {
  page: number;
  perPage: number;
  sort: ColumnSortItem<TSortColumnId>[];
  joinOperator: JoinOperator;
}

export function getDataTableQuery<
  TColumnConfigs extends DataTableColumnConfigs,
>(
  search: NoInfer<
    DataTableSearch<SortableColumnId<TColumnConfigs>> &
      Record<
        FilterableColumnId<TColumnConfigs>,
        ColumnFilterItem<FilterableColumnId<TColumnConfigs>>[]
      >
  >,
  columnConfigs: TColumnConfigs,
): DataTableColumnConfigsQuery<TColumnConfigs> {
  const filtersById: Record<
    FilterableColumnId<TColumnConfigs>,
    ColumnFilterItem<FilterableColumnId<TColumnConfigs>>[]
  > = search;

  return {
    page: search.page,
    perPage: search.perPage,
    sorting: search.sort,
    filters: getColumnFilters(
      getKeys(getFilterableColumns(columnConfigs)),
      (id) => filtersById[id],
    ),
    joinOperator: search.joinOperator,
  };
}

function getFilterParsers<TColumnId extends string>(
  columns: Record<TColumnId, FilterVariant>,
) {
  const parsers = getKeys(columns).map(
    (id) => [id, getColumnFilterParser(id, columns[id])] as const,
  );
  return Object.fromEntries(parsers) as Record<
    TColumnId,
    ReturnType<typeof getColumnFilterParser<TColumnId>>
  >;
}

function splitFilterOperator(param: string, variant: FilterVariant) {
  const parts = param.split(".");

  for (
    let count = Math.min(MAX_OPERATOR_PART_COUNT, parts.length);
    count > 0;
    count--
  ) {
    const operator = OPERATORS_BY_NAME.get(
      parts.slice(0, count).join(".").toLowerCase(),
    );
    if (!operator || !getIsVariantOperator(variant, operator)) continue;
    if (count === parts.length && !getIsValuelessOperator(operator)) continue;

    return { operator, rawValue: parts.slice(count).join(".") };
  }

  return { operator: getPlainFilterOperator(variant), rawValue: param };
}

function getIsVariantOperator(
  variant: FilterVariant,
  operator: FilterOperator,
) {
  return (
    operator === getPlainFilterOperator(variant) ||
    getFilterOperators(variant).some((option) => option.value === operator)
  );
}

function parseListValue(rawValue: string, operator: FilterOperator) {
  const items: string[] = [];
  let item = "";
  let isQuoted = false;
  let wasQuoted = false;

  for (let index = 0; index < rawValue.length; index++) {
    const char = rawValue[index];

    if (char === '"') {
      if (isQuoted && rawValue[index + 1] === '"') {
        item += char;
        index++;
      } else {
        isQuoted = !isQuoted;
        wasQuoted = true;
      }
    } else if (char === "," && !isQuoted) {
      items.push(wasQuoted ? item : item.trim());
      item = "";
      wasQuoted = false;
    } else {
      item += char;
    }
  }
  if (rawValue !== "") items.push(wasQuoted ? item : item.trim());

  if (operator === "isBetween") return [items[0] ?? "", items[1] ?? ""];
  return items.filter((value) => value !== "");
}

function joinListValue(values: string[]) {
  return values
    .map((value) =>
      /[,"]/.test(value) || value !== value.trim()
        ? `"${value.replaceAll('"', '""')}"`
        : value,
    )
    .join(",");
}

function getIsSameFilter(a: ColumnFilterItem, b: ColumnFilterItem) {
  if (a.operator !== b.operator) return false;
  if (!Array.isArray(a.value) || !Array.isArray(b.value)) {
    return a.value === b.value;
  }
  const otherValue = b.value;
  return (
    a.value.length === otherValue.length &&
    a.value.every((item, index) => item === otherValue[index])
  );
}

function getKeys<TKey extends string>(record: Record<TKey, unknown>) {
  return Object.keys(record).filter((key): key is TKey =>
    Object.hasOwn(record, key),
  );
}

function parseSorting(value: string): ColumnSortItem[] | null {
  if (value.startsWith("[")) {
    try {
      const result = z.array(sortingItemSchema).safeParse(JSON.parse(value));
      return result.success ? result.data : null;
    } catch {
      return null;
    }
  }

  const sorting: ColumnSortItem[] = [];

  for (const part of value.split(",")) {
    const index = part.lastIndexOf(".");
    const order = part.slice(index + 1);

    if (index <= 0 || (order !== "asc" && order !== "desc")) return null;

    sorting.push({ id: part.slice(0, index), desc: order === "desc" });
  }

  return sorting;
}

function getIsSameSorting(a: ColumnSortItem[], b: ColumnSortItem[]) {
  return (
    a.length === b.length &&
    a.every(
      (item, index) => item.id === b[index]?.id && item.desc === b[index]?.desc,
    )
  );
}

function getIdSet<TColumnId extends string>(
  ids?: readonly TColumnId[] | Set<TColumnId>,
) {
  if (!ids) return null;
  return ids instanceof Set ? ids : new Set(ids);
}

function getHasKnownIds<
  TColumnItem extends { id: string },
  TColumnId extends string,
>(
  items: TColumnItem[],
  ids: ReadonlySet<TColumnId> | null,
): items is (TColumnItem & { id: TColumnId })[] {
  const knownIds: ReadonlySet<string> | null = ids;
  return knownIds === null || items.every((item) => knownIds.has(item.id));
}
src/lib/data-table-types.ts
import type {
  ColumnFilter,
  ColumnSort,
  Row,
  RowData,
} from "@tanstack/react-table";

import type { DataTableFeatures } from "@/lib/data-table-features";
import type {
  FILTER_OPERATORS,
  FILTER_VARIANTS,
  JOIN_OPERATORS,
} from "@/lib/data-table-utils";

export interface DataTableColumnMeta {
  label?: string;
  placeholder?: string;
  variant?: FilterVariant;
  options?: FilterOption[];
  range?: [number, number];
  unit?: string;
  icon?: React.ComponentType<React.ComponentProps<"svg">>;
}

export interface DataTableQueryKeys {
  page: string;
  perPage: string;
  sort: string;
  joinOperator: string;
}

export interface FilterOption {
  label: string;
  value: string;
  count?: number;
  icon?: React.ComponentType<React.ComponentProps<"svg">>;
}

export type FilterOperator = keyof typeof FILTER_OPERATORS;
export type FilterVariant = (typeof FILTER_VARIANTS)[number];
export type JoinOperator = (typeof JOIN_OPERATORS)[number];

export interface FilterOperatorOption {
  label: string;
  value: FilterOperator;
}

export interface ColumnSortItem<
  TColumnId extends string = string,
> extends ColumnSort {
  id: TColumnId;
}

export interface ColumnFilterItem<
  TColumnId extends string = string,
> extends ColumnFilter {
  id: TColumnId;
  value: string | string[];
  variant: FilterVariant;
  operator: FilterOperator;
  filterId: string;
}

declare module "@tanstack/react-table" {
  interface ColumnFilter extends Partial<
    Pick<ColumnFilterItem, "operator" | "variant" | "filterId">
  > {}
}

export interface DataTableColumnConfig {
  variant?: FilterVariant;
  isSortable?: boolean;
}

export type DataTableColumnConfigs = Record<string, DataTableColumnConfig>;

export type FilterableColumnId<TColumnConfigs extends DataTableColumnConfigs> =
  {
    [K in keyof TColumnConfigs]: TColumnConfigs[K] extends {
      variant: FilterVariant;
    }
      ? K
      : never;
  }[keyof TColumnConfigs] &
    string;

export type SortableColumnId<TColumnConfigs extends DataTableColumnConfigs> = {
  [K in keyof TColumnConfigs]: TColumnConfigs[K] extends { isSortable: false }
    ? never
    : K;
}[keyof TColumnConfigs] &
  string;

export interface DataTableQuery<
  TFilterColumnId extends string = string,
  TSortColumnId extends string = TFilterColumnId,
> {
  page: number;
  perPage: number;
  sorting: ColumnSortItem<TSortColumnId>[];
  filters: ColumnFilterItem<TFilterColumnId>[];
  joinOperator: JoinOperator;
}

export type DataTableColumnConfigsQuery<
  TColumnConfigs extends DataTableColumnConfigs,
> = DataTableQuery<
  FilterableColumnId<TColumnConfigs>,
  SortableColumnId<TColumnConfigs>
>;

export interface DataTableRowAction<TData extends RowData> {
  row: Row<DataTableFeatures, TData>;
  variant: "update" | "delete";
}
src/lib/data-table-features.ts
import {
  assignPrototypeAPIs,
  assignTableAPIs,
  type ColumnFilter,
  columnFacetingFeature,
  columnFilteringFeature,
  columnOrderingFeature,
  columnPinningFeature,
  columnSizingFeature,
  columnVisibilityFeature,
  createFacetedMinMaxValues,
  createFacetedUniqueValues,
  createPaginatedRowModel,
  createSortedRowModel,
  functionalUpdate,
  makeStateUpdater,
  metaHelper,
  setStateSlice,
  type OnChangeFn,
  type RowData,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  type Table,
  type TableFeature,
  type TableFeatures,
  tableFeatures,
  type Updater,
} from "@tanstack/react-table";

import type {
  ColumnFilterItem,
  DataTableColumnMeta,
  JoinOperator,
} from "@/lib/data-table-types";

import {
  createDataTableFacetedRowModel,
  createDataTableFilteredRowModel,
  dataTableFilterFn,
} from "@/lib/data-table-filters";
import {
  getIsPlainFilter,
  normalizeColumnFilter,
  getPlainFilterValue,
} from "@/lib/data-table-utils";

interface TableState_DataTableFiltering {
  joinOperator: JoinOperator;
}

interface TableOptions_DataTableFiltering {
  onJoinOperatorChange?: OnChangeFn<JoinOperator>;
}

interface Table_DataTableFiltering {
  getColumnFilterItems: () => ColumnFilterItem[];
  addColumnFilter: (filter: ColumnFilterItem) => void;
  updateColumnFilter: (
    filterId: string,
    updates: Partial<Omit<ColumnFilterItem, "filterId">>,
  ) => void;
  removeColumnFilter: (filterId: string) => void;
  getJoinOperator: () => JoinOperator;
  setJoinOperator: (updater: Updater<JoinOperator>) => void;
  resetJoinOperator: (defaultState?: boolean) => void;
}

declare module "@tanstack/react-table" {
  interface Plugins {
    dataTableFilteringFeature: TableFeature;
  }

  interface TableState_FeatureMap {
    dataTableFilteringFeature: TableState_DataTableFiltering;
  }

  interface TableOptions_FeatureMap<
    in out TFeatures extends TableFeatures,
    in out TData extends RowData,
  > {
    dataTableFilteringFeature: TableOptions_DataTableFiltering;
  }

  interface Table_FeatureMap<
    in out TFeatures extends TableFeatures,
    in out TData extends RowData,
  > {
    dataTableFilteringFeature: Table_DataTableFiltering;
  }
}

type DataTableInstance = Table<DataTableFeatures, RowData>;

/**
 * Feature hooks get a `Table` generic over any features. This feature is only
 * registered in `dataTableFeatures`, so its hooks can use that table's types.
 */
function asDataTable(table: object) {
  return table as DataTableInstance;
}

const dataTableFilteringFeature: TableFeature = {
  getInitialState: (initialState) => ({
    joinOperator: "and",
    ...initialState,
  }),
  getDefaultColumnDef: () => ({
    enableColumnFilter: false,
    filterFn: dataTableFilterFn,
  }),
  getDefaultTableOptions: (table) => {
    const options: TableOptions_DataTableFiltering = {
      onJoinOperatorChange: makeStateUpdater("joinOperator", table),
    };
    return options;
  },
  assignColumnPrototype: (prototype, table) => {
    const instance = asDataTable(table);

    assignPrototypeAPIs("dataTableFilteringFeature", prototype, table, {
      column_getFilterValue: {
        fn: (column: { id: string }) =>
          getPlainFilter(
            instance,
            column.id,
            instance.atoms.columnFilters.get(),
          ).value,
        memoDeps: () => [instance.atoms.columnFilters.get()],
      },
      column_setFilterValue: {
        fn: (column: { id: string }, updater: Updater<unknown>) =>
          setPlainFilter(instance, column.id, updater),
      },
    });
  },
  constructTableAPIs: (table) => {
    const instance = asDataTable(table);
    const resolve = (filter: ColumnFilter) =>
      normalizeColumnFilter(
        filter,
        instance.getColumn(filter.id)?.columnDef.meta?.variant ?? "text",
      );

    const setJoinOperator = (updater: Updater<JoinOperator>) =>
      instance.options.onJoinOperatorChange?.((old) =>
        functionalUpdate(updater, old),
      );

    assignTableAPIs("dataTableFilteringFeature", table, {
      table_setColumnFilters: {
        fn: (updater: Updater<ColumnFilter[]>) =>
          setStateSlice(table, "columnFilters", (old: ColumnFilter[]) =>
            functionalUpdate(updater, old).filter(
              (filter) => !getShouldRemoveFilter(filter, instance),
            ),
          ),
      },
      table_getColumnFilterItems: {
        fn: () => instance.atoms.columnFilters.get().map(resolve),
        memoDeps: () => [instance.atoms.columnFilters.get()],
      },
      table_addColumnFilter: {
        fn: (filter: ColumnFilterItem) =>
          instance.setColumnFilters((old) => [...old, filter]),
      },
      table_updateColumnFilter: {
        fn: (
          filterId: string,
          updates: Partial<Omit<ColumnFilterItem, "filterId">>,
        ) =>
          instance.setColumnFilters((old) =>
            old.map((filter) => {
              const item = resolve(filter);
              return item.filterId === filterId
                ? { ...item, ...updates }
                : filter;
            }),
          ),
      },
      table_removeColumnFilter: {
        fn: (filterId: string) =>
          instance.setColumnFilters((old) =>
            old.filter((filter) => resolve(filter).filterId !== filterId),
          ),
      },
      table_getJoinOperator: {
        fn: () => instance.atoms.joinOperator.get(),
      },
      table_setJoinOperator: { fn: setJoinOperator },
      table_resetJoinOperator: {
        fn: (defaultState?: boolean) =>
          setJoinOperator(
            defaultState
              ? "and"
              : (instance.initialState.joinOperator ?? "and"),
          ),
      },
    });
  },
};

function getPlainFilter(
  instance: DataTableInstance,
  columnId: string,
  filters: ColumnFilter[],
) {
  const variant =
    instance.getColumn(columnId)?.columnDef.meta?.variant ?? "text";
  const index = filters.findIndex(
    (filter) =>
      filter.id === columnId &&
      getIsPlainFilter(normalizeColumnFilter(filter, variant)),
  );
  const filter = filters[index];
  const value = filter
    ? getPlainFilterValue(normalizeColumnFilter(filter, variant))
    : undefined;

  return { index, filter, value };
}

function setPlainFilter(
  instance: DataTableInstance,
  columnId: string,
  updater: Updater<unknown>,
) {
  instance.setColumnFilters((old) => {
    const {
      index,
      filter: previous,
      value: previousValue,
    } = getPlainFilter(instance, columnId, old);
    const value = functionalUpdate(updater, previousValue);
    const next: ColumnFilter = previous?.filterId
      ? { id: columnId, value, filterId: previous.filterId }
      : { id: columnId, value };

    if (index === -1) return [...old, next];
    return old.map((filter, filterIndex) =>
      filterIndex === index ? next : filter,
    );
  });
}

/**
 * TanStack's `autoRemove` rules for plain filters. Filters with an
 * `operator` are only removed by an `undefined` value, so empty drafts in the
 * filter list survive a column `filterFn` whose `autoRemove` would drop them.
 */
function getShouldRemoveFilter(
  filter: ColumnFilter,
  instance: DataTableInstance,
) {
  if (filter.value === undefined) return true;
  if (filter.operator) return false;

  const column = instance.getColumn(filter.id);
  if (!column) return false;

  const filterFn = column.getFilterFn();
  if (filterFn?.autoRemove) {
    return filterFn.autoRemove(filter.value, column);
  }
  return typeof filter.value === "string" && !filter.value;
}

export const dataTableFeatures = tableFeatures({
  columnFilteringFeature,
  columnFacetingFeature,
  columnOrderingFeature,
  columnPinningFeature,
  columnSizingFeature,
  columnVisibilityFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  dataTableFilteringFeature,
  filteredRowModel: createDataTableFilteredRowModel(),
  facetedRowModel: createDataTableFacetedRowModel(),
  facetedUniqueValues: createFacetedUniqueValues(),
  facetedMinMaxValues: createFacetedMinMaxValues(),
  paginatedRowModel: createPaginatedRowModel(),
  sortedRowModel: createSortedRowModel(),
  columnMeta: metaHelper<DataTableColumnMeta>(),
});

export type DataTableFeatures = typeof dataTableFeatures;
src/lib/data-table-filters.ts
import {
  type ColumnFilter,
  constructFilterFn,
  constructRow,
  type FilterFn,
  makeObjectMap,
  type Row,
  type RowData,
  type RowModel,
  skipFirstRun,
  type Table,
  type TableFeatures,
  tableMemo,
} from "@tanstack/react-table";

import type {
  ColumnFilterItem,
  FilterVariant,
  JoinOperator,
} from "@/lib/data-table-types";

import {
  createPlainFilter,
  FILTER_VARIANTS,
  getIsActiveFilter,
  normalizeColumnFilter,
  parseFilterDate,
  stringifyFilterValue,
} from "@/lib/data-table-utils";

const DAY_MS = 24 * 60 * 60 * 1000;
const GLOBAL_FILTER_ID = "__global__";

const filterResultsByRow = new WeakMap<object, Record<string, boolean>>();

export function matchesFilter(
  cellValue: unknown,
  filter: Pick<ColumnFilterItem, "operator" | "variant" | "value">,
): boolean {
  const { operator, variant, value } = filter;
  const isDate = variant === "date" || variant === "dateRange";
  const isNumeric = variant === "number" || variant === "range";

  switch (operator) {
    case "iLike":
      return typeof value === "string"
        ? getLowercaseText(cellValue).includes(value.toLowerCase())
        : true;

    case "notILike":
      return typeof value === "string"
        ? !getLowercaseText(cellValue).includes(value.toLowerCase())
        : true;

    case "eq":
      if (variant === "boolean") return getBooleanString(cellValue) === value;
      if (isDate) return getIsSameDay(cellValue, value);
      if (isNumeric) return parseNumber(cellValue) === parseNumber(value);
      return stringifyFilterValue(cellValue) === String(value);

    case "ne":
      if (variant === "boolean") return getBooleanString(cellValue) !== value;
      if (isDate) return !getIsSameDay(cellValue, value);
      if (isNumeric) return parseNumber(cellValue) !== parseNumber(value);
      return stringifyFilterValue(cellValue) !== String(value);

    case "inArray":
      return Array.isArray(value)
        ? value.includes(stringifyFilterValue(cellValue))
        : true;

    case "notInArray":
      return Array.isArray(value)
        ? !value.includes(stringifyFilterValue(cellValue))
        : true;

    case "lt":
    case "lte":
    case "gt":
    case "gte":
      return compare(cellValue, value, operator, isDate);

    case "isBetween":
      return getIsBetween(cellValue, value, isDate);

    case "isRelativeToToday":
      return getIsRelativeToToday(cellValue, value);

    case "isEmpty":
      return getIsEmptyValue(cellValue);

    case "isNotEmpty":
      return !getIsEmptyValue(cellValue);

    default:
      return true;
  }
}

export const dataTableFilterFn = constructFilterFn({
  filter: (dataValue, filterValue, row, columnId) => {
    const variant = getFilterVariant(
      row.table.getColumn(columnId)?.columnDef.meta,
    );
    const item = createPlainFilter(columnId, variant, filterValue);
    return !item || matchesFilter(dataValue, item);
  },
  autoRemove: (value) => value === undefined,
});

export function createDataTableFilteredRowModel<
  TFeatures extends TableFeatures,
  TData extends RowData,
>() {
  return (table: Table<TFeatures, TData>) => {
    const instance = asFilteringTable(table);

    return tableMemo({
      feature: "dataTableFilteringFeature",
      table,
      fnName: "table.getFilteredRowModel",
      memoDeps: () => [
        table.getPreFilteredRowModel(),
        instance.atoms.columnFilters?.get(),
        instance.atoms.joinOperator?.get(),
        instance.atoms.globalFilter?.get(),
      ],
      fn: () => getFilteredRowModel(table.getPreFilteredRowModel(), instance),
      onAfterUpdate: skipFirstRun(() => instance.autoResetPageIndex?.()),
    });
  };
}

export function createDataTableFacetedRowModel<
  TFeatures extends TableFeatures,
  TData extends RowData,
>() {
  return (table: Table<TFeatures, TData>, columnId: string) => {
    const instance = asFilteringTable(table);

    return tableMemo({
      feature: "columnFacetingFeature",
      table,
      fnName: "createDataTableFacetedRowModel",
      memoDeps: () => [
        table.getPreFilteredRowModel(),
        instance.atoms.columnFilters?.get(),
        instance.atoms.joinOperator?.get(),
        instance.atoms.globalFilter?.get(),
        table.getFilteredRowModel(),
      ],
      fn: () =>
        getFacetedRowModel(table.getPreFilteredRowModel(), columnId, instance),
    });
  };
}

type RowTest<TFeatures extends TableFeatures, TData extends RowData> = (
  row: Row<TFeatures, TData>,
) => boolean;

interface FilteringColumn<
  TFeatures extends TableFeatures,
  TData extends RowData,
> {
  id: string;
  columnDef: { meta?: unknown };
  getFilterFn?: () => FilterFn<TFeatures, TData> | undefined;
  getCanGlobalFilter?: () => boolean;
}

interface FilteringMembers<
  TFeatures extends TableFeatures,
  TData extends RowData,
> {
  atoms: {
    columnFilters?: { get: () => ColumnFilter[] };
    joinOperator?: { get: () => JoinOperator };
    globalFilter?: { get: () => unknown };
  };
  options: { filterFromLeafRows?: boolean; maxLeafRowFilterDepth?: number };
  getColumn: (
    columnId: string,
  ) => FilteringColumn<TFeatures, TData> | undefined;
  getAllLeafColumns: () => FilteringColumn<TFeatures, TData>[];
  getGlobalFilterFn?: () => FilterFn<TFeatures, TData> | undefined;
  autoResetPageIndex?: () => void;
}

type FilteringInstance<
  TFeatures extends TableFeatures,
  TData extends RowData,
> = FilteringMembers<TFeatures, TData> & Table<TFeatures, TData>;

function asFilteringTable<
  TFeatures extends TableFeatures,
  TData extends RowData,
>(table: Table<TFeatures, TData>) {
  return table as FilteringInstance<TFeatures, TData>;
}

function getFilteredRowModel<
  TFeatures extends TableFeatures,
  TData extends RowData,
>(
  rowModel: RowModel<TFeatures, TData>,
  instance: FilteringInstance<TFeatures, TData>,
): RowModel<TFeatures, TData> {
  const joinOperator = instance.atoms.joinOperator?.get() ?? "and";
  const testsByColumn = new Map<string, RowTest<TFeatures, TData>[]>();

  for (const filter of instance.atoms.columnFilters?.get() ?? []) {
    const column = instance.getColumn(filter.id);
    const test = column ? getFilterTest(column, filter) : null;
    if (!test) continue;
    testsByColumn.set(filter.id, [
      ...(testsByColumn.get(filter.id) ?? []),
      test,
    ]);
  }

  const globalTests = getGlobalFilterTests(instance);
  const hasFilters = testsByColumn.size > 0 || globalTests.length > 0;

  for (const row of rowModel.flatRows) {
    const results = makeObjectMap<boolean>();

    if (hasFilters) {
      for (const [columnId, tests] of testsByColumn) {
        results[columnId] =
          joinOperator === "or"
            ? tests.some((test) => test(row))
            : tests.every((test) => test(row));
      }
      if (globalTests.length > 0) {
        results[GLOBAL_FILTER_ID] = globalTests.some((test) => test(row));
      }
    }

    setFilterResults(row, results);
  }

  if (!hasFilters || rowModel.rows.length === 0) return rowModel;

  return filterRows(
    rowModel.rows,
    (row) => getRowPasses(row, joinOperator),
    instance,
  );
}

function getFacetedRowModel<
  TFeatures extends TableFeatures,
  TData extends RowData,
>(
  rowModel: RowModel<TFeatures, TData>,
  columnId: string,
  instance: FilteringInstance<TFeatures, TData>,
): RowModel<TFeatures, TData> {
  const globalFilter = instance.atoms.globalFilter?.get();
  const hasGlobalFilter =
    columnId !== GLOBAL_FILTER_ID &&
    globalFilter !== undefined &&
    globalFilter !== null &&
    globalFilter !== "";
  const hasOtherFilters =
    hasGlobalFilter ||
    (instance.atoms.columnFilters?.get() ?? []).some(
      (filter) => filter.id !== columnId,
    );

  if (!hasOtherFilters || rowModel.rows.length === 0) return rowModel;

  const joinOperator = instance.atoms.joinOperator?.get() ?? "and";

  return filterRows(
    rowModel.rows,
    (row) => getRowPasses(row, joinOperator, columnId),
    instance,
  );
}

function getRowPasses<TFeatures extends TableFeatures, TData extends RowData>(
  row: Row<TFeatures, TData>,
  joinOperator: JoinOperator,
  excludedColumnId?: string,
) {
  const results = filterResultsByRow.get(row) ?? {};

  if (
    excludedColumnId !== GLOBAL_FILTER_ID &&
    results[GLOBAL_FILTER_ID] === false
  ) {
    return false;
  }

  const columnIds = Object.keys(results).filter(
    (id) => id !== GLOBAL_FILTER_ID && id !== excludedColumnId,
  );
  if (columnIds.length === 0) return true;

  return joinOperator === "or"
    ? columnIds.some((id) => results[id])
    : columnIds.every((id) => results[id]);
}

function setFilterResults<
  TFeatures extends TableFeatures,
  TData extends RowData,
>(row: Row<TFeatures, TData>, results: Record<string, boolean> = {}) {
  filterResultsByRow.set(row, results);
  Object.assign(row, {
    columnFilters: results,
    columnFiltersMeta: makeObjectMap(),
  });
}

function getFilterVariant(meta: unknown): FilterVariant {
  const variant =
    typeof meta === "object" && meta && "variant" in meta
      ? meta.variant
      : undefined;
  return FILTER_VARIANTS.find((item) => item === variant) ?? "text";
}

function getFilterTest<TFeatures extends TableFeatures, TData extends RowData>(
  column: FilteringColumn<TFeatures, TData>,
  filter: ColumnFilter,
): RowTest<TFeatures, TData> | null {
  const filterFn = column.getFilterFn?.();

  if (!filter.operator && filterFn && !Object.is(filterFn, dataTableFilterFn)) {
    const value = filterFn.resolveFilterValue?.(filter.value) ?? filter.value;
    return (row) => filterFn(row, column.id, value);
  }

  const item = normalizeColumnFilter(
    filter,
    getFilterVariant(column.columnDef.meta),
  );
  if (!getIsActiveFilter(item)) return null;

  return (row) => matchesFilter(row.getValue(column.id), item);
}

function getGlobalFilterTests<
  TFeatures extends TableFeatures,
  TData extends RowData,
>(instance: FilteringInstance<TFeatures, TData>): RowTest<TFeatures, TData>[] {
  const globalFilter = instance.atoms.globalFilter?.get();
  const filterFn = instance.getGlobalFilterFn?.();

  if (
    globalFilter === undefined ||
    globalFilter === null ||
    globalFilter === "" ||
    !filterFn
  ) {
    return [];
  }

  const value = filterFn.resolveFilterValue?.(globalFilter) ?? globalFilter;

  return instance
    .getAllLeafColumns()
    .filter((column) => column.getCanGlobalFilter?.())
    .map((column) => (row) => filterFn(row, column.id, value));
}

function filterRows<TFeatures extends TableFeatures, TData extends RowData>(
  rows: Row<TFeatures, TData>[],
  passes: RowTest<TFeatures, TData>,
  instance: FilteringInstance<TFeatures, TData>,
): RowModel<TFeatures, TData> {
  const maxDepth = instance.options.maxLeafRowFilterDepth ?? 100;
  const flatRows: Row<TFeatures, TData>[] = [];
  const rowsById = makeObjectMap<Row<TFeatures, TData>>();

  function copyRow(row: Row<TFeatures, TData>) {
    const copy = constructRow(
      instance,
      row.id,
      row.original,
      row.index,
      row.depth,
      undefined,
      row.parentId,
    );
    setFilterResults(copy, filterResultsByRow.get(row));
    return copy;
  }

  function addToFlat(subRows: Row<TFeatures, TData>[]) {
    for (const subRow of subRows) {
      flatRows.push(subRow);
      rowsById[subRow.id] = subRow;
      if (subRow.subRows.length) addToFlat(subRow.subRows);
    }
  }

  function fromLeafs(
    rowsToFilter: Row<TFeatures, TData>[],
    depth: number,
  ): Row<TFeatures, TData>[] {
    const filtered: Row<TFeatures, TData>[] = [];

    for (const row of rowsToFilter) {
      const copy = copyRow(row);

      if (row.subRows.length && depth < maxDepth) {
        copy.subRows = fromLeafs(row.subRows, depth + 1);
        if (copy.subRows.length || passes(copy)) filtered.push(copy);
      } else if (passes(copy)) {
        copy.subRows = row.subRows;
        filtered.push(copy);
      }
    }

    return filtered;
  }

  function fromRoot(
    rowsToFilter: Row<TFeatures, TData>[],
    depth: number,
  ): Row<TFeatures, TData>[] {
    const filtered: Row<TFeatures, TData>[] = [];

    for (const row of rowsToFilter) {
      if (!passes(row)) continue;

      if (row.subRows.length && depth < maxDepth) {
        const copy = copyRow(row);
        filtered.push(copy);
        flatRows.push(copy);
        rowsById[copy.id] = copy;
        copy.subRows = fromRoot(row.subRows, depth + 1);
      } else {
        filtered.push(row);
        flatRows.push(row);
        rowsById[row.id] = row;
        if (row.subRows.length) addToFlat(row.subRows);
      }
    }

    return filtered;
  }

  if (instance.options.filterFromLeafRows) {
    const filtered = fromLeafs(rows, 0);
    addToFlat(filtered);
    return { rows: filtered, flatRows, rowsById };
  }

  return { rows: fromRoot(rows, 0), flatRows, rowsById };
}

function getLowercaseText(value: unknown) {
  return stringifyFilterValue(value).toLowerCase();
}

function getBooleanString(value: unknown) {
  return value === true || value === "true" ? "true" : "false";
}

function parseNumber(value: unknown) {
  if (value === "" || value == null) return Number.NaN;
  return typeof value === "number" ? value : Number(value);
}

function parseTime(value: unknown) {
  return parseFilterDate(value)?.getTime() ?? Number.NaN;
}

function startOfDay(time: number) {
  const date = new Date(time);
  date.setHours(0, 0, 0, 0);
  return date.getTime();
}

function endOfDay(time: number) {
  const date = new Date(time);
  date.setHours(23, 59, 59, 999);
  return date.getTime();
}

function getIsSameDay(cellValue: unknown, value: unknown) {
  const cell = parseTime(cellValue);
  const target = parseTime(value);
  if (Number.isNaN(cell) || Number.isNaN(target)) return false;
  return cell >= startOfDay(target) && cell <= endOfDay(target);
}

function compare(
  cellValue: unknown,
  value: unknown,
  operator: "lt" | "lte" | "gt" | "gte",
  isDate: boolean,
) {
  if (Array.isArray(value)) return true;

  let cell: number;
  let target: number;

  if (isDate) {
    cell = parseTime(cellValue);
    const time = parseTime(value);
    target =
      operator === "lt" || operator === "gte"
        ? startOfDay(time)
        : endOfDay(time);
  } else {
    cell = parseNumber(cellValue);
    target = parseNumber(value);
  }

  if (Number.isNaN(cell) || Number.isNaN(target)) return false;

  switch (operator) {
    case "lt":
      return cell < target;
    case "lte":
      return cell <= target;
    case "gt":
      return cell > target;
    default:
      return cell >= target;
  }
}

function getIsBetween(cellValue: unknown, value: unknown, isDate: boolean) {
  if (!Array.isArray(value) || value.length !== 2) return true;

  const [rawStart, rawEnd] = value;
  const hasStart = stringifyFilterValue(rawStart).trim() !== "";
  const hasEnd = stringifyFilterValue(rawEnd).trim() !== "";

  if (!hasStart && !hasEnd) return true;

  const cell = isDate ? parseTime(cellValue) : parseNumber(cellValue);
  if (Number.isNaN(cell)) return false;

  if (isDate) {
    const start = hasStart ? startOfDay(parseTime(rawStart)) : null;
    const end = hasEnd ? endOfDay(parseTime(rawEnd)) : null;
    return (start === null || cell >= start) && (end === null || cell <= end);
  }

  const start = hasStart ? parseNumber(rawStart) : null;
  const end = hasEnd ? parseNumber(rawEnd) : null;

  return (start === null || cell >= start) && (end === null || cell <= end);
}

function getIsRelativeToToday(cellValue: unknown, value: unknown) {
  if (typeof value !== "string") return true;

  const [amountRaw, unit] = value.split(" ");
  const amount = Number.parseInt(amountRaw ?? "", 10);
  if (Number.isNaN(amount) || !unit) return true;

  const today = Date.now();
  let start: number;
  let end: number;

  switch (unit) {
    case "days":
      start = startOfDay(today + amount * DAY_MS);
      end = endOfDay(start);
      break;
    case "weeks":
      start = startOfDay(today + amount * 7 * DAY_MS);
      end = endOfDay(start + 6 * DAY_MS);
      break;
    case "months":
      start = startOfDay(today + amount * 30 * DAY_MS);
      end = endOfDay(start + 29 * DAY_MS);
      break;
    default:
      return true;
  }

  const cell = parseTime(cellValue);
  if (Number.isNaN(cell)) return false;
  return cell >= start && cell <= end;
}

function getIsEmptyValue(value: unknown) {
  return (
    value === null ||
    value === undefined ||
    value === "" ||
    (Array.isArray(value) && value.length === 0)
  );
}

More from Shadcn Table

Browse all components in this library →