shadimage
Shadcn Table
componentdata-table-filter-menusource: ok · hit

Data Table Filter Menu

A filter menu component for the data table

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

Where it goes

data-table

Interaction

dropdown

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

badgebuttoncalendarcommandinputpopoverselect

Files13 · source cached 10/14/2026, 1:22:16 PM

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

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

import { cn } from "cn";
import * as React from "react";

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

import {
  getDefaultFilter,
  getDateFilterLabel,
  getFilterDates,
  getFilterOperators,
  coerceFilterValue,
  getIsEditableTarget,
  getIsValuelessOperator,
  getSelectFilterValue,
  JOIN_OPERATORS,
  getFilterDateValue,
} from "@/lib/data-table-utils";
import { generateId } from "@/lib/id";
import { DataTableRangeFilter } from "@/registry/bases/base/components/data-table/data-table-range-filter";
import { Badge } from "@/registry/bases/base/ui/badge";
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,
  FacetedValue,
} from "@/registry/bases/base/ui/faceted";
import { Input } from "@/registry/bases/base/ui/input";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/registry/bases/base/ui/popover";
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/registry/bases/base/ui/select";
import {
  Sortable,
  SortableContent,
  SortableItem,
  SortableItemHandle,
  SortableOverlay,
} from "@/registry/bases/base/ui/sortable";
import { Calendar } from "@/registry/bases/radix/ui/calendar";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";

const FILTER_SHORTCUT_KEY = "f";
const REMOVE_FILTER_SHORTCUTS = ["backspace", "delete"];

function getIsRemoveKey(event: React.KeyboardEvent) {
  return REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase());
}

type FilterUpdate = Partial<Omit<ColumnFilterItem, "filterId">>;
type FilterSelector = "field" | "operator" | "value";

interface DataTableFilterMenuProps<
  TData extends RowData,
> extends React.ComponentProps<typeof PopoverContent> {
  table: ReactTable<DataTableFeatures, TData, unknown>;
  disabled?: boolean;
}

export function DataTableFilterMenu<TData extends RowData>({
  table,
  ...props
}: DataTableFilterMenuProps<TData>) {
  return (
    <table.Subscribe
      selector={(state) => ({
        filters: table.getColumnFilterItems(),
        joinOperator: state.joinOperator,
      })}
    >
      {({ filters, joinOperator }) => (
        <DataTableFilterMenuContent
          table={table}
          filters={filters}
          joinOperator={joinOperator}
          {...props}
        />
      )}
    </table.Subscribe>
  );
}

interface DataTableFilterMenuContentProps<
  TData extends RowData,
> extends DataTableFilterMenuProps<TData> {
  filters: ColumnFilterItem[];
  joinOperator: JoinOperator;
}

function DataTableFilterMenuContent<TData extends RowData>({
  table,
  filters,
  joinOperator,
  disabled,
  ...props
}: DataTableFilterMenuContentProps<TData>) {
  const dir = useDirection();
  const id = React.useId();
  const labelId = React.useId();
  const descriptionId = React.useId();
  const [open, setOpen] = React.useState(false);
  const addButtonRef = React.useRef<HTMLButtonElement>(null);

  React.useEffect(() => {
    function onKeyDown(event: KeyboardEvent) {
      if (getIsEditableTarget(event.target)) return;
      if (
        event.key.toLowerCase() !== FILTER_SHORTCUT_KEY ||
        !(event.ctrlKey || event.metaKey) ||
        !event.shiftKey
      ) {
        return;
      }

      event.preventDefault();
      setOpen((open) => !open);
    }

    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, []);

  const columns = React.useMemo(
    () => table.getAllColumns().filter((column) => column.getCanFilter()),
    [table],
  );

  const hasFilters = filters.length > 0;

  function onFilterAdd() {
    const column = columns[0];
    if (!column) return;

    table.addColumnFilter({
      ...getDefaultFilter(column),
      filterId: generateId({ length: 8 }),
    });
  }

  function onFilterUpdate(filterId: string, updates: FilterUpdate) {
    table.updateColumnFilter(filterId, updates);
  }

  function onFilterRemove(filterId: string) {
    table.removeColumnFilter(filterId);
    addButtonRef.current?.focus();
  }

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

  function onTriggerKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
    const lastFilter = filters.at(-1);
    if (!lastFilter || !getIsRemoveKey(event)) return;

    event.preventDefault();
    onFilterRemove(lastFilter.filterId);
  }

  return (
    <Sortable
      value={filters}
      onValueChange={(value) => table.setColumnFilters(value)}
      getItemValue={(item) => item.filterId}
    >
      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger
          render={
            <Button
              variant="outline"
              onKeyDown={onTriggerKeyDown}
              disabled={disabled}
            />
          }
        >
          <IconPlaceholder
            lucide="ListFilter"
            tabler="IconListDetails"
            hugeicons="LeftToRightListBulletIcon"
            phosphor="ListIcon"
            remixicon="RiListUnordered"
            className="text-muted-foreground"
          />
          Filter
          {hasFilters && (
            <Badge
              variant="secondary"
              className="h-[18.24px] px-[5.12px] font-mono text-[10.4px]"
            >
              {filters.length}
            </Badge>
          )}
        </PopoverTrigger>
        <PopoverContent
          dir={dir}
          aria-describedby={descriptionId}
          aria-labelledby={labelId}
          className="flex w-full max-w-(--available-width) flex-col gap-3.5 p-4 sm:min-w-95"
          {...props}
        >
          <div className="flex flex-col gap-1">
            <h4 id={labelId} className="leading-none font-medium">
              {hasFilters ? "Filters" : "No filters applied"}
            </h4>
            <p
              id={descriptionId}
              className={cn(
                "text-sm text-muted-foreground",
                hasFilters && "sr-only",
              )}
            >
              {hasFilters
                ? "Modify filters to refine your rows."
                : "Add filters to refine your rows."}
            </p>
          </div>
          {hasFilters && (
            <SortableContent
              render={
                <div
                  role="list"
                  className="grid max-h-75 grid-cols-[minmax(4.5rem,auto)_8rem_8rem_minmax(10rem,max-content)_auto_auto] gap-x-2 gap-y-2 overflow-y-auto p-1"
                />
              }
            >
              {filters.map((filter, index) => (
                <DataTableFilterItem<TData>
                  key={filter.filterId}
                  filter={filter}
                  index={index}
                  filterItemId={`${id}-filter-${filter.filterId}`}
                  joinOperator={joinOperator}
                  columns={columns}
                  onJoinOperatorChange={(value) => table.setJoinOperator(value)}
                  onFilterUpdate={onFilterUpdate}
                  onFilterRemove={onFilterRemove}
                />
              ))}
            </SortableContent>
          )}
          <div className="flex w-full items-center gap-2">
            <Button ref={addButtonRef} onClick={onFilterAdd}>
              Add filter
            </Button>
            {hasFilters && (
              <Button variant="outline" onClick={onFiltersReset}>
                Reset filters
              </Button>
            )}
          </div>
        </PopoverContent>
      </Popover>
      <SortableOverlay>
        <div
          dir={dir}
          className="grid size-full grid-cols-[minmax(4.5rem,auto)_8rem_8rem_minmax(10rem,1fr)_auto_auto] items-center gap-2"
        >
          <div className="h-8 rounded-lg bg-primary/10" />
          <div className="h-8 rounded-lg bg-primary/10" />
          <div className="h-8 rounded-lg bg-primary/10" />
          <div className="h-8 rounded-lg bg-primary/10" />
          <div className="size-8 rounded-lg bg-primary/10" />
          <div className="size-8 rounded-lg bg-primary/10" />
        </div>
      </SortableOverlay>
    </Sortable>
  );
}

interface DataTableFilterItemProps<TData extends RowData> {
  filter: ColumnFilterItem;
  index: number;
  filterItemId: string;
  joinOperator: JoinOperator;
  columns: Column<DataTableFeatures, TData>[];
  onJoinOperatorChange: (value: JoinOperator) => void;
  onFilterUpdate: (filterId: string, updates: FilterUpdate) => void;
  onFilterRemove: (filterId: string) => void;
}

function DataTableFilterItem<TData extends RowData>({
  filter,
  index,
  filterItemId,
  joinOperator,
  columns,
  onJoinOperatorChange,
  onFilterUpdate,
  onFilterRemove,
}: DataTableFilterItemProps<TData>) {
  const [activeSelector, setActiveSelector] =
    React.useState<FilterSelector | null>(null);

  const column = columns.find((column) => column.id === filter.id);
  if (!column) return null;

  const label = column.columnDef.meta?.label ?? column.id;

  function getSelectorProps(selector: FilterSelector) {
    return {
      open: activeSelector === selector,
      onOpenChange: (open: boolean) =>
        setActiveSelector(open ? selector : null),
    };
  }

  function onItemKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
    if (activeSelector || getIsEditableTarget(event.target)) return;
    if (!getIsRemoveKey(event)) return;

    event.preventDefault();
    onFilterRemove(filter.filterId);
  }

  return (
    <SortableItem
      id={filterItemId}
      value={filter.filterId}
      render={
        <div
          role="listitem"
          tabIndex={-1}
          className="col-span-full grid grid-cols-subgrid items-center"
          onKeyDown={onItemKeyDown}
        />
      }
    >
      <div className="min-w-18 text-center">
        <FilterJoinOperator
          index={index}
          joinOperator={joinOperator}
          listboxId={`${filterItemId}-join-operator-listbox`}
          onJoinOperatorChange={onJoinOperatorChange}
        />
      </div>
      <FilterFieldSelector
        filter={filter}
        column={column}
        columns={columns}
        listboxId={`${filterItemId}-field-listbox`}
        onFilterUpdate={onFilterUpdate}
        {...getSelectorProps("field")}
      />
      <FilterOperatorSelector
        filter={filter}
        label={label}
        listboxId={`${filterItemId}-operator-listbox`}
        onFilterUpdate={onFilterUpdate}
        {...getSelectorProps("operator")}
      />
      <div className="w-full min-w-0">
        <FilterValueInput
          filter={filter}
          column={column}
          inputId={`${filterItemId}-input`}
          onFilterUpdate={onFilterUpdate}
          {...getSelectorProps("value")}
        />
      </div>
      <Button
        aria-controls={filterItemId}

        aria-label={`Remove ${label} filter`}
        variant="outline"
        size="icon"
        onClick={() => onFilterRemove(filter.filterId)}
      >
        <IconPlaceholder
          lucide="Trash2"
          tabler="IconTrash"
          hugeicons="Delete02Icon"
          phosphor="TrashIcon"
          remixicon="RiDeleteBinLine"
        />
      </Button>
      <SortableItemHandle
        aria-label={`Reorder ${label} filter`}
        render={<Button variant="outline" size="icon" />}
      >
        <IconPlaceholder
          lucide="GripVertical"
          tabler="IconGripVertical"
          hugeicons="DragDropVerticalIcon"
          phosphor="DotsSixVerticalIcon"
          remixicon="RiDraggable"
        />
      </SortableItemHandle>
    </SortableItem>
  );
}

interface FilterJoinOperatorProps {
  index: number;
  joinOperator: JoinOperator;
  listboxId: string;
  onJoinOperatorChange: (value: JoinOperator) => void;
}

function FilterJoinOperator({
  index,
  joinOperator,
  listboxId,
  onJoinOperatorChange,
}: FilterJoinOperatorProps) {
  const dir = useDirection();
  if (index === 0) {
    return <span className="text-sm text-muted-foreground">Where</span>;
  }

  if (index > 1) {
    return (
      <span className="text-sm text-muted-foreground">{joinOperator}</span>
    );
  }

  return (
    <Select
      value={joinOperator}
      onValueChange={(value) => {
        if (value == null) return;
        onJoinOperatorChange(value);
      }}
    >
      <SelectTrigger
        aria-label="Select join operator"
        aria-controls={listboxId}
        className="lowercase"
      >
        <SelectValue placeholder={joinOperator} />
      </SelectTrigger>
      <SelectContent
        dir={dir}
        id={listboxId}
        alignItemWithTrigger={false}
        className="min-w-(--anchor-width) lowercase"
      >
        <SelectGroup>
          {JOIN_OPERATORS.map((joinOperator) => (
            <SelectItem key={joinOperator} value={joinOperator}>
              {joinOperator}
            </SelectItem>
          ))}
        </SelectGroup>
      </SelectContent>
    </Select>
  );
}

interface FilterSelectorProps {
  filter: ColumnFilterItem;
  open: boolean;
  onOpenChange: (open: boolean) => void;
  onFilterUpdate: (filterId: string, updates: FilterUpdate) => void;
}

interface FilterFieldSelectorProps<
  TData extends RowData,
> extends FilterSelectorProps {
  column: Column<DataTableFeatures, TData>;
  columns: Column<DataTableFeatures, TData>[];
  listboxId: string;
}

function FilterFieldSelector<TData extends RowData>({
  filter,
  column,
  columns,
  listboxId,
  open,
  onOpenChange,
  onFilterUpdate,
}: FilterFieldSelectorProps<TData>) {
  const dir = useDirection();
  return (
    <Faceted
      open={open}
      onOpenChange={onOpenChange}
      value={filter.id}
      onValueChange={(columnId) => {
        const column = columns.find((column) => column.id === columnId);
        if (column) {
          onFilterUpdate(filter.filterId, getDefaultFilter(column));
        }
      }}
    >
      <FacetedTrigger
        render={
          <Button
            aria-controls={listboxId}
            variant="outline"
            className="w-32"
          />
        }
      >
        <span className="truncate">
          {column.columnDef.meta?.label ?? "Select field"}
        </span>
        <IconPlaceholder
          lucide="ChevronsUpDown"
          tabler="IconSelector"
          hugeicons="UnfoldMoreIcon"
          phosphor="CaretUpDownIcon"
          remixicon="RiArrowUpDownLine"
          className="opacity-50"
        />
      </FacetedTrigger>
      <FacetedContent dir={dir} id={listboxId} className="w-40">
        <FacetedInput placeholder="Search fields..." />
        <FacetedList>
          <FacetedEmpty>No fields found.</FacetedEmpty>
          <FacetedGroup>
            {columns.map((column) => (
              <FacetedItem key={column.id} value={column.id}>
                <span className="truncate">{column.columnDef.meta?.label}</span>
                <FacetedItemIndicator />
              </FacetedItem>
            ))}
          </FacetedGroup>
        </FacetedList>
      </FacetedContent>
    </Faceted>
  );
}

interface FilterOperatorSelectorProps extends FilterSelectorProps {
  label: string;
  listboxId: string;
}

function FilterOperatorSelector({
  filter,
  label,
  listboxId,
  open,
  onOpenChange,
  onFilterUpdate,
}: FilterOperatorSelectorProps) {
  const dir = useDirection();
  return (
    <Select
      open={open}
      onOpenChange={onOpenChange}
      value={filter.operator}
      onValueChange={(operator) => {
        if (operator == null) return;
        onFilterUpdate(filter.filterId, {
          operator,
          value: coerceFilterValue(operator, filter.value),
        });
      }}
    >
      <SelectTrigger
        aria-controls={listboxId}
        aria-label={`${label} filter operator`}
        className="w-32 lowercase"
      >
        <div className="truncate">
          <SelectValue placeholder={filter.operator} />
        </div>
      </SelectTrigger>
      <SelectContent dir={dir} id={listboxId}>
        <SelectGroup>
          {getFilterOperators(filter.variant).map((operator) => (
            <SelectItem
              key={operator.value}
              value={operator.value}
              className="lowercase"
            >
              {operator.label}
            </SelectItem>
          ))}
        </SelectGroup>
      </SelectContent>
    </Select>
  );
}

interface FilterValueInputProps<
  TData extends RowData,
> extends FilterSelectorProps {
  column: Column<DataTableFeatures, TData>;
  inputId: string;
}

function FilterValueInput<TData extends RowData>(
  props: FilterValueInputProps<TData>,
) {
  const { filter, column, inputId, onFilterUpdate } = props;
  const columnMeta = column.columnDef.meta;

  if (getIsValuelessOperator(filter.operator)) {
    return (
      <div
        id={inputId}
        role="status"
        aria-label={`${columnMeta?.label} filter is ${
          filter.operator === "isEmpty" ? "empty" : "not empty"
        }`}
        aria-live="polite"
        className="h-8 w-full rounded-lg border border-dashed border-input bg-transparent dark:bg-input/30"
      />
    );
  }

  switch (filter.variant) {
    case "text":
    case "number":
    case "range":
      if (filter.operator === "isBetween") {
        return (
          <DataTableRangeFilter
            filter={filter}
            column={column}
            inputId={inputId}
            onFilterUpdate={onFilterUpdate}
          />
        );
      }

      return (
        <Input
          id={inputId}
          type={filter.variant === "text" ? "text" : "number"}
          aria-label={`${columnMeta?.label} filter value`}
          inputMode={filter.variant === "text" ? undefined : "numeric"}
          placeholder={columnMeta?.placeholder ?? "Enter a value..."}
          defaultValue={
            typeof filter.value === "string" ? filter.value : undefined
          }
          onChange={(event) =>
            onFilterUpdate(filter.filterId, { value: event.target.value })
          }
        />
      );

    case "boolean":
      return <BooleanFilterValue {...props} />;

    case "select":
    case "multiSelect":
      return <SelectFilterValue {...props} />;

    case "date":
    case "dateRange":
      return <DateFilterValue {...props} />;

    default:
      return null;
  }
}

function BooleanFilterValue<TData extends RowData>({
  filter,
  column,
  inputId,
  open,
  onOpenChange,
  onFilterUpdate,
}: FilterValueInputProps<TData>) {
  const dir = useDirection();
  const listboxId = `${inputId}-listbox`;

  return (
    <Select
      open={open}
      onOpenChange={onOpenChange}
      value={typeof filter.value === "string" ? filter.value : null}
      onValueChange={(value) => {
        if (value == null) return;
        onFilterUpdate(filter.filterId, { value });
      }}
    >
      <SelectTrigger
        id={inputId}
        aria-controls={listboxId}
        aria-label={`${column.columnDef.meta?.label} boolean filter`}
        className="w-full"
      >
        <SelectValue placeholder="Select value" />
      </SelectTrigger>
      <SelectContent dir={dir} id={listboxId}>
        <SelectGroup>
          <SelectItem value="true">True</SelectItem>
          <SelectItem value="false">False</SelectItem>
        </SelectGroup>
      </SelectContent>
    </Select>
  );
}

function SelectFilterValue<TData extends RowData>({
  filter,
  column,
  inputId,
  open,
  onOpenChange,
  onFilterUpdate,
}: FilterValueInputProps<TData>) {
  const dir = useDirection();
  const listboxId = `${inputId}-listbox`;
  const columnMeta = column.columnDef.meta;
  const multiple = filter.variant === "multiSelect";

  return (
    <Faceted
      open={open}
      onOpenChange={onOpenChange}
      value={getSelectFilterValue(filter)}
      onValueChange={(value) =>
        onFilterUpdate(filter.filterId, { value: value ?? "" })
      }
      items={columnMeta?.options}
      multiple={multiple}
    >
      <FacetedTrigger
        render={
          <Button
            id={inputId}
            aria-controls={listboxId}
            aria-label={`${columnMeta?.label} filter value${multiple ? "s" : ""}`}
            variant="outline"
            className="w-full hover:bg-muted/50 aria-expanded:bg-background dark:aria-expanded:bg-input/30"
          />
        }
      >
        <FacetedValue
          placeholder={
            columnMeta?.placeholder ?? `Select option${multiple ? "s" : ""}...`
          }
        />
      </FacetedTrigger>
      <FacetedContent dir={dir} id={listboxId} className="w-50">
        <FacetedInput
          aria-label={`Search ${columnMeta?.label} options`}
          placeholder={columnMeta?.placeholder ?? "Search options..."}
        />
        <FacetedList>
          <FacetedEmpty>No options found.</FacetedEmpty>
          <FacetedGroup>
            {columnMeta?.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>
        </FacetedList>
      </FacetedContent>
    </Faceted>
  );
}

function DateFilterValue<TData extends RowData>({
  filter,
  column,
  inputId,
  open,
  onOpenChange,
  onFilterUpdate,
}: FilterValueInputProps<TData>) {
  const dir = useDirection();
  const listboxId = `${inputId}-listbox`;
  const label = column.columnDef.meta?.label;
  const [startDate, endDate] = getFilterDates(filter.value);

  return (
    <Popover open={open} onOpenChange={onOpenChange}>
      <PopoverTrigger
        render={
          <Button
            id={inputId}
            aria-controls={listboxId}
            aria-label={`${label} date filter`}
            variant="outline"
            className={cn(
              "w-full justify-start text-start",
              !startDate && "text-muted-foreground",
            )}
          />
        }
      >
        <IconPlaceholder
          lucide="CalendarIcon"
          tabler="IconCalendar"
          hugeicons="CalendarIcon"
          phosphor="CalendarIcon"
          remixicon="RiCalendarLine"
        />
        <span className="truncate">
          {getDateFilterLabel(filter) ?? "Pick a date"}
        </span>
      </PopoverTrigger>
      <PopoverContent
        dir={dir}
        id={listboxId}
        align="start"
        className="w-auto p-0"
      >
        {filter.operator === "isBetween" ? (
          <Calendar
            aria-label={`Select ${label} date range`}
            autoFocus
            captionLayout="dropdown"
            mode="range"
            selected={startDate ? { from: startDate, to: endDate } : undefined}
            onSelect={(range) =>
              onFilterUpdate(filter.filterId, {
                value: range
                  ? [
                      getFilterDateValue(range.from),
                      getFilterDateValue(range.to),
                    ]
                  : [],
              })
            }
          />
        ) : (
          <Calendar
            aria-label={`Select ${label} date`}
            autoFocus
            captionLayout="dropdown"
            mode="single"
            selected={startDate}
            onSelect={(date) => {
              onFilterUpdate(filter.filterId, {
                value: getFilterDateValue(date),
              });
              onOpenChange(false);
            }}
          />
        )}
      </PopoverContent>
    </Popover>
  );
}
src/registry/bases/base/components/data-table/data-table-range-filter.tsx
"use client";

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

import { cn } from "cn";
import * as React from "react";

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

import { Input } from "@/registry/bases/base/ui/input";

function getRangeFilterValues(value: string | string[]): [string, string] {
  if (Array.isArray(value)) return [value[0] ?? "", value[1] ?? ""];
  return [value, ""];
}

/** Number inputs only accept plain numbers, so skip rounding and grouping. */
function getInputValue(value: string) {
  return value === "" || Number.isNaN(Number(value)) ? "" : value;
}

interface DataTableRangeFilterProps<
  TData extends RowData,
> extends React.ComponentProps<"div"> {
  filter: ColumnFilterItem;
  column: Column<DataTableFeatures, TData>;
  inputId: string;
  onFilterUpdate: (
    filterId: string,
    updates: Partial<Omit<ColumnFilterItem, "filterId">>,
  ) => void;
}

export function DataTableRangeFilter<TData extends RowData>({
  filter,
  column,
  inputId,
  onFilterUpdate,
  className,
  ...props
}: DataTableRangeFilterProps<TData>) {
  const meta = column.columnDef.meta;

  const [min, max] = React.useMemo(() => {
    const range = column.columnDef.meta?.range;
    if (range) return range;

    const values = column.getFacetedMinMaxValues();
    if (!values) return [0, 100];

    return [values[0], values[1]];
  }, [column]);

  const value = getRangeFilterValues(filter.value).map(getInputValue);

  const onRangeValueChange = React.useCallback(
    (value: string, isMin?: boolean) => {
      const numValue = Number(value);
      const currentValues = getRangeFilterValues(filter.value);
      const otherValue = isMin
        ? (currentValues[1] ?? "")
        : (currentValues[0] ?? "");

      if (
        value === "" ||
        (!Number.isNaN(numValue) &&
          (isMin
            ? numValue >= min && numValue <= (Number(otherValue) || max)
            : numValue <= max && numValue >= (Number(otherValue) || min)))
      ) {
        onFilterUpdate(filter.filterId, {
          value: isMin ? [value, otherValue] : [otherValue, value],
        });
      }
    },
    [filter.filterId, filter.value, min, max, onFilterUpdate],
  );

  return (
    <div
      data-slot="data-table-range-filter"
      className={cn("flex w-full items-center gap-2", className)}
      {...props}
    >
      <Input
        id={`${inputId}-min`}
        type="number"
        aria-label={`${meta?.label} minimum value`}
        aria-valuemin={min}
        aria-valuemax={max}
        data-slot="data-table-range-filter-min"
        inputMode="numeric"
        placeholder={min.toString()}
        min={min}
        max={max}
        className="w-full min-w-24"
        defaultValue={value[0]}
        onChange={(event) => onRangeValueChange(event.target.value, true)}
      />
      <span className="sr-only shrink-0 text-muted-foreground">to</span>
      <Input
        id={`${inputId}-max`}
        type="number"
        aria-label={`${meta?.label} maximum value`}
        aria-valuemin={min}
        aria-valuemax={max}
        data-slot="data-table-range-filter-max"
        inputMode="numeric"
        placeholder={max.toString()}
        min={min}
        max={max}
        className="w-full min-w-24"
        defaultValue={value[1]}
        onChange={(event) => onRangeValueChange(event.target.value)}
      />
    </div>
  );
}
src/registry/bases/base/components/data-table/data-table-advanced-toolbar.tsx
"use client";

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

import { cn } from "cn";

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

import { DataTableViewOptions } from "@/registry/bases/base/components/data-table/data-table-view-options";

interface DataTableAdvancedToolbarProps<
  TData extends RowData,
> extends React.ComponentProps<"div"> {
  table: ReactTable<DataTableFeatures, TData, unknown>;
}

export function DataTableAdvancedToolbar<TData extends RowData>({
  table,
  children,
  className,
  ...props
}: DataTableAdvancedToolbarProps<TData>) {
  return (
    <div
      data-slot="data-table-advanced-toolbar"
      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">{children}</div>
      <div className="flex items-center gap-2">
        <DataTableViewOptions table={table} align="end" />
      </div>
    </div>
  );
}
src/registry/bases/base/ui/sortable.tsx
"use client";

import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { useIsoLayoutEffect } from "@base-ui/utils/useIsoLayoutEffect";
import {
  type Announcements,
  closestCenter,
  closestCorners,
  defaultDropAnimationSideEffects,
  DndContext,
  type DndContextProps,
  type DragEndEvent,
  type DraggableAttributes,
  type DraggableSyntheticListeners,
  DragOverlay,
  type DragStartEvent,
  type DropAnimation,
  KeyboardSensor,
  MouseSensor,
  type ScreenReaderInstructions,
  TouchSensor,
  type UniqueIdentifier,
  useSensor,
  useSensors,
} from "@dnd-kit/core";
import {
  restrictToHorizontalAxis,
  restrictToParentElement,
  restrictToVerticalAxis,
} from "@dnd-kit/modifiers";
import {
  arrayMove,
  horizontalListSortingStrategy,
  SortableContext,
  type SortableContextProps,
  sortableKeyboardCoordinates,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { cn } from "cn";
import * as React from "react";
import * as ReactDOM from "react-dom";

const orientationConfig = {
  vertical: {
    modifiers: [restrictToVerticalAxis, restrictToParentElement],
    strategy: verticalListSortingStrategy,
    collisionDetection: closestCenter,
  },
  horizontal: {
    modifiers: [restrictToHorizontalAxis, restrictToParentElement],
    strategy: horizontalListSortingStrategy,
    collisionDetection: closestCenter,
  },
  mixed: {
    modifiers: [restrictToParentElement],
    strategy: undefined,
    collisionDetection: closestCorners,
  },
};

const ROOT_NAME = "Sortable";
const CONTENT_NAME = "SortableContent";
const ITEM_NAME = "SortableItem";
const ITEM_HANDLE_NAME = "SortableItemHandle";
const OVERLAY_NAME = "SortableOverlay";

interface SortableRootContextValue<T> {
  id: string;
  items: UniqueIdentifier[];
  modifiers: DndContextProps["modifiers"];
  strategy: SortableContextProps["strategy"];
  activeId: UniqueIdentifier | null;
  setActiveId: (id: UniqueIdentifier | null) => void;
  getItemValue: (item: T) => UniqueIdentifier;
  flatCursor: boolean;
}

const SortableRootContext =
  React.createContext<SortableRootContextValue<unknown> | null>(null);

function useSortableContext(consumerName: string) {
  const context = React.useContext(SortableRootContext);
  if (!context) {
    throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
  }
  return context;
}

interface GetItemValue<T> {
  /**
   * Callback that returns a unique identifier for each sortable item. Required for array of objects.
   * @example getItemValue={(item) => item.id}
   */
  getItemValue: (item: T) => UniqueIdentifier;
}

type SortableProps<T> = DndContextProps &
  (T extends object ? GetItemValue<T> : Partial<GetItemValue<T>>) & {
    value: T[];
    onValueChange?: (items: T[]) => void;
    onMove?: (
      event: DragEndEvent & { activeIndex: number; overIndex: number },
    ) => void;
    strategy?: SortableContextProps["strategy"];
    orientation?: "vertical" | "horizontal" | "mixed";
    flatCursor?: boolean;
  };

function Sortable<T>(props: SortableProps<T>) {
  const {
    value,
    onValueChange,
    collisionDetection,
    modifiers,
    strategy,
    onMove,
    orientation = "vertical",
    flatCursor = false,
    getItemValue: getItemValueProp,
    accessibility,
    ...sortableProps
  } = props;

  const id = React.useId();
  const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);

  const sensors = useSensors(
    useSensor(MouseSensor),
    useSensor(TouchSensor),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    }),
  );
  const config = React.useMemo(
    () => orientationConfig[orientation],
    [orientation],
  );

  const getItemValue = React.useCallback(
    (item: T): UniqueIdentifier => {
      if (typeof item === "object" && !getItemValueProp) {
        throw new Error(
          "`getItemValue` is required when using array of objects",
        );
      }
      return getItemValueProp
        ? getItemValueProp(item)
        : (item as UniqueIdentifier);
    },
    [getItemValueProp],
  );

  const items = React.useMemo(() => {
    return value.map((item) => getItemValue(item));
  }, [value, getItemValue]);

  const onDragStart = React.useCallback(
    (event: DragStartEvent) => {
      sortableProps.onDragStart?.(event);

      if (event.activatorEvent.defaultPrevented) return;

      setActiveId(event.active.id);
    },
    [sortableProps.onDragStart],
  );

  const onDragEnd = React.useCallback(
    (event: DragEndEvent) => {
      sortableProps.onDragEnd?.(event);

      if (event.activatorEvent.defaultPrevented) return;

      const { active, over } = event;
      if (over && active.id !== over?.id) {
        const activeIndex = value.findIndex(
          (item) => getItemValue(item) === active.id,
        );
        const overIndex = value.findIndex(
          (item) => getItemValue(item) === over.id,
        );

        if (onMove) {
          onMove({ ...event, activeIndex, overIndex });
        } else {
          onValueChange?.(arrayMove(value, activeIndex, overIndex));
        }
      }
      setActiveId(null);
    },
    [value, onValueChange, onMove, getItemValue, sortableProps.onDragEnd],
  );

  const onDragCancel = React.useCallback(
    (event: DragEndEvent) => {
      sortableProps.onDragCancel?.(event);

      if (event.activatorEvent.defaultPrevented) return;

      setActiveId(null);
    },
    [sortableProps.onDragCancel],
  );

  const announcements: Announcements = React.useMemo(
    () => ({
      onDragStart({ active }) {
        const activeValue = active.id.toString();
        return `Grabbed sortable item "${activeValue}". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;
      },
      onDragOver({ active, over }) {
        if (over) {
          const overIndex = over.data.current?.sortable.index ?? 0;
          const activeIndex = active.data.current?.sortable.index ?? 0;
          const moveDirection = overIndex > activeIndex ? "down" : "up";
          const activeValue = active.id.toString();
          return `Sortable item "${activeValue}" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;
        }
        return "Sortable item is no longer over a droppable area. Press escape to cancel.";
      },
      onDragEnd({ active, over }) {
        const activeValue = active.id.toString();
        if (over) {
          const overIndex = over.data.current?.sortable.index ?? 0;
          return `Sortable item "${activeValue}" dropped at position ${overIndex + 1} of ${value.length}.`;
        }
        return `Sortable item "${activeValue}" dropped. No changes were made.`;
      },
      onDragCancel({ active }) {
        const activeIndex = active.data.current?.sortable.index ?? 0;
        const activeValue = active.id.toString();
        return `Sorting cancelled. Sortable item "${activeValue}" returned to position ${activeIndex + 1} of ${value.length}.`;
      },
      onDragMove({ active, over }) {
        if (over) {
          const overIndex = over.data.current?.sortable.index ?? 0;
          const activeIndex = active.data.current?.sortable.index ?? 0;
          const moveDirection = overIndex > activeIndex ? "down" : "up";
          const activeValue = active.id.toString();
          return `Sortable item "${activeValue}" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;
        }
        return "Sortable item is no longer over a droppable area. Press escape to cancel.";
      },
    }),
    [value],
  );

  const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(
    () => ({
      draggable: `
        To pick up a sortable item, press space or enter.
        While dragging, use the ${orientation === "vertical" ? "up and down" : orientation === "horizontal" ? "left and right" : "arrow"} keys to move the item.
        Press space or enter again to drop the item in its new position, or press escape to cancel.
      `,
    }),
    [orientation],
  );

  const contextValue = React.useMemo(
    () => ({
      id,
      items,
      modifiers: modifiers ?? config.modifiers,
      strategy: strategy ?? config.strategy,
      activeId,
      setActiveId,
      getItemValue,
      flatCursor,
    }),
    [
      id,
      items,
      modifiers,
      strategy,
      config.modifiers,
      config.strategy,
      activeId,
      getItemValue,
      flatCursor,
    ],
  );

  return (
    <SortableRootContext.Provider
      value={contextValue as SortableRootContextValue<unknown>}
    >
      <DndContext
        collisionDetection={collisionDetection ?? config.collisionDetection}
        modifiers={modifiers ?? config.modifiers}
        sensors={sensors}
        {...sortableProps}
        id={id}
        onDragStart={onDragStart}
        onDragEnd={onDragEnd}
        onDragCancel={onDragCancel}
        accessibility={{
          announcements,
          screenReaderInstructions,
          ...accessibility,
        }}
      />
    </SortableRootContext.Provider>
  );
}

const SortableContentContext = React.createContext<boolean>(false);

interface SortableContentProps
  extends React.ComponentProps<"div">, useRender.ComponentProps<"div"> {
  strategy?: SortableContextProps["strategy"];
  children: React.ReactNode;
  withoutSlot?: boolean;
}

function SortableContent(props: SortableContentProps) {
  const {
    strategy: strategyProp,
    withoutSlot,
    children,
    className,
    render,
    ref,
    ...contentProps
  } = props;

  const context = useSortableContext(CONTENT_NAME);

  const element = useRender({
    defaultTagName: "div",
    props: mergeProps<"div">(
      {
        ref,
        className,
        children,
      },
      contentProps,
    ),
    render,
    state: {
      slot: "sortable-content",
    },
    enabled: !withoutSlot,
  });

  return (
    <SortableContentContext.Provider value={true}>
      <SortableContext
        items={context.items}
        strategy={strategyProp ?? context.strategy}
      >
        {withoutSlot ? children : element}
      </SortableContext>
    </SortableContentContext.Provider>
  );
}

interface SortableItemContextValue {
  id: string;
  attributes: DraggableAttributes;
  listeners: DraggableSyntheticListeners | undefined;
  setActivatorNodeRef: (node: HTMLElement | null) => void;
  isDragging?: boolean;
  disabled?: boolean;
}

const SortableItemContext =
  React.createContext<SortableItemContextValue | null>(null);

function useSortableItemContext(consumerName: string) {
  const context = React.useContext(SortableItemContext);
  if (!context) {
    throw new Error(`\`${consumerName}\` must be used within \`${ITEM_NAME}\``);
  }
  return context;
}

interface SortableItemProps
  extends React.ComponentProps<"div">, useRender.ComponentProps<"div"> {
  value: UniqueIdentifier;
  asHandle?: boolean;
  disabled?: boolean;
}

function SortableItem(props: SortableItemProps) {
  const {
    value,
    style,
    asHandle,
    disabled,
    className,
    render,
    ref,
    ...itemProps
  } = props;

  const inSortableContent = React.useContext(SortableContentContext);
  const inSortableOverlay = React.useContext(SortableOverlayContext);

  if (!inSortableContent && !inSortableOverlay) {
    throw new Error(
      `\`${ITEM_NAME}\` must be used within \`${CONTENT_NAME}\` or \`${OVERLAY_NAME}\``,
    );
  }

  if (value === "") {
    throw new Error(`\`${ITEM_NAME}\` value cannot be an empty string`);
  }

  const context = useSortableContext(ITEM_NAME);
  const instanceId = React.useId();
  const id = itemProps.id ?? instanceId;

  const {
    attributes,
    listeners,
    setNodeRef,
    setActivatorNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: value, disabled });

  const onNodeRefChange = React.useCallback(
    (node: HTMLElement | null) => {
      if (disabled) return;
      setNodeRef(node);
      if (asHandle) setActivatorNodeRef(node);
    },
    [disabled, asHandle, setNodeRef, setActivatorNodeRef],
  );

  const composedStyle = React.useMemo<React.CSSProperties>(() => {
    return {
      transform: CSS.Translate.toString(transform),
      transition,
      ...style,
    };
  }, [transform, transition, style]);

  const itemContext = React.useMemo<SortableItemContextValue>(
    () => ({
      id,
      attributes,
      listeners,
      setActivatorNodeRef,
      isDragging,
      disabled,
    }),
    [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],
  );

  const handleProps =
    asHandle && !disabled ? { ...attributes, ...listeners } : undefined;

  const element = useRender({
    defaultTagName: "div",
    ref: [ref ?? null, onNodeRefChange],
    props: mergeProps<"div">(
      {
        id,
        style: composedStyle,
        className: cn(
          "focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-hidden",
          {
            "touch-none select-none": asHandle,
            "cursor-default": context.flatCursor,
            "data-dragging:cursor-grabbing": !context.flatCursor,
            "cursor-grab": !isDragging && asHandle && !context.flatCursor,
            "opacity-50": isDragging,
            "pointer-events-none opacity-50": disabled,
          },
          className,
        ),
      },
      itemProps,
      handleProps,
    ),
    render,
    state: {
      slot: "sortable-item",
      ...(disabled && { disabled: "" }),
      ...(isDragging && { dragging: "" }),
    },
  });

  return (
    <SortableItemContext.Provider value={itemContext}>
      {element}
    </SortableItemContext.Provider>
  );
}

interface SortableItemHandleProps
  extends React.ComponentProps<"button">, useRender.ComponentProps<"button"> {}

function SortableItemHandle(props: SortableItemHandleProps) {
  const { disabled, className, render, ref, ...itemHandleProps } = props;

  const context = useSortableContext(ITEM_HANDLE_NAME);
  const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);

  const isDisabled = disabled ?? itemContext.disabled;

  const { setActivatorNodeRef } = itemContext;

  const onActivatorNodeRef = React.useCallback(
    (node: HTMLElement | null) => {
      if (isDisabled) return;
      setActivatorNodeRef(node);
    },
    [isDisabled, setActivatorNodeRef],
  );

  const handleProps = isDisabled
    ? undefined
    : { ...itemContext.attributes, ...itemContext.listeners };

  return useRender({
    defaultTagName: "button",
    ref: [ref ?? null, onActivatorNodeRef],
    props: mergeProps<"button">(
      {
        type: "button",
        "aria-controls": itemContext.id,
        disabled: isDisabled,
        className: cn(
          "select-none disabled:pointer-events-none disabled:opacity-50",
          context.flatCursor
            ? "cursor-default"
            : "cursor-grab data-dragging:cursor-grabbing",
          className,
        ),
      },
      itemHandleProps,
      handleProps,
    ),
    render,
    state: {
      slot: "sortable-item-handle",
      ...(isDisabled && { disabled: "" }),
      ...(itemContext.isDragging && { dragging: "" }),
    },
  });
}

const SortableOverlayContext = React.createContext(false);

const dropAnimation: DropAnimation = {
  sideEffects: defaultDropAnimationSideEffects({
    styles: {
      active: {
        opacity: "0.4",
      },
    },
  }),
};

interface SortableOverlayProps extends Omit<
  React.ComponentProps<typeof DragOverlay>,
  "children"
> {
  container?: Element | DocumentFragment | null;
  children?:
    | ((params: { value: UniqueIdentifier }) => React.ReactNode)
    | React.ReactNode;
}

function SortableOverlay(props: SortableOverlayProps) {
  const { container: containerProp, children, ...overlayProps } = props;

  const context = useSortableContext(OVERLAY_NAME);

  const [mounted, setMounted] = React.useState(false);

  useIsoLayoutEffect(() => setMounted(true), []);

  const container =
    containerProp ?? (mounted ? globalThis.document?.body : null);

  if (!container) return null;

  return ReactDOM.createPortal(
    <DragOverlay
      dropAnimation={dropAnimation}
      modifiers={context.modifiers}
      className={cn(!context.flatCursor && "cursor-grabbing")}
      {...overlayProps}
    >
      <SortableOverlayContext.Provider value={true}>
        {context.activeId
          ? typeof children === "function"
            ? children({ value: context.activeId })
            : children
          : null}
      </SortableOverlayContext.Provider>
    </DragOverlay>,
    container,
  );
}

export {
  Sortable,
  SortableContent,
  SortableItem,
  SortableItemHandle,
  SortableOverlay,
  type SortableProps,
};
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 { useIsoLayoutEffect } from "@base-ui/utils/useIsoLayoutEffect";
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 {
  label: string;
  value: string;
}

const NO_ITEMS: FacetedOption[] = [];

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

interface FacetedStore {
  subscribe: (callback: () => void) => () => void;
  getState: () => FacetedStoreState;
  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: FacetedStoreState,
  props: FacetedController,
): string | string[] | undefined {
  return props.isControlled ? props.value : state.value;
}

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

const labelsCache = new WeakMap<FacetedOption[], Map<string, string>>();

function getLabels(items: FacetedOption[]) {
  let labels = labelsCache.get(items);
  if (!labels) {
    labels = new Map(items.map((item) => [item.value, item.label]));
    labelsCache.set(items, labels);
  }
  return labels;
}

function getSelectedValues(value: string | string[] | undefined) {
  if (Array.isArray(value)) return value;
  return value ? [value] : [];
}

function getSelectedItems(
  values: string[],
  items: FacetedOption[],
): FacetedOption[] {
  const labels = getLabels(items);

  return values.map((value) => ({
    label: labels.get(value) ?? value,
    value,
  }));
}

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: FacetedStoreState,
): 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: FacetedStoreState) => 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>({
  open: openProp,
  defaultOpen = false,
  onOpenChange,
  value,
  defaultValue,
  onValueChange,
  items = NO_ITEMS,
  multiple = false as Multiple,
  ...props
}: FacetedProps<Multiple>) {
  const controller: FacetedController = {
    open: openProp,
    onOpenChange,
    value,
    onValueChange(nextValue) {
      if (!getIsFacetedSelectedValue(nextValue, multiple)) return;
      onValueChange?.(nextValue);
    },
    isControlled: value !== undefined,
    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);

  useIsoLayoutEffect(() => {
    if (value !== undefined) store.notify();
  }, [store, value]);

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

function FacetedTrigger({
  className,
  ...props
}: React.ComponentProps<typeof PopoverTrigger>) {
  return (
    <PopoverTrigger
      data-slot="faceted-trigger"
      className={cn(
        "justify-between text-left has-[>[data-slot=faceted-value]]:transition-[color,background-color,border-color,box-shadow] has-[>[data-slot=faceted-value]>[data-slot=faceted-badge-list]]:ps-1",
        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 { items, multiple } = store.getProps();
  const values = getSelectedValues(value);

  if (typeof children === "function") {
    return children(getSelectedItems(values, items));
  }
  if (children != null) return children;

  const selected =
    multiple && values.length > 2 ? [] : getSelectedItems(values, items);
  const [item] = selected;

  return (
    <span
      data-slot="faceted-value"
      className={cn(
        "flex w-full min-w-0 items-center gap-1",
        values.length === 0 && "text-muted-foreground",
      )}
    >
      {multiple && values.length > 0 ? (
        <FacetedBadgeList>
          {values.length > 2 ? (
            <FacetedBadge>{values.length} selected</FacetedBadge>
          ) : (
            selected.map((selectedItem) => (
              <FacetedBadge key={selectedItem.value}>
                <span className="truncate">{selectedItem.label}</span>
              </FacetedBadge>
            ))
          )}
        </FacetedBadgeList>
      ) : (
        <span className="truncate">{item ? item.label : placeholder}</span>
      )}
      <IconPlaceholder
        lucide="ChevronDown"
        tabler="IconChevronDown"
        hugeicons="ArrowDown01Icon"
        phosphor="CaretDownIcon"
        remixicon="RiArrowDownSLine"
        className="ml-auto size-4 shrink-0 text-muted-foreground"
      />
    </span>
  );
}

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,
    ),
  });
}

interface FacetedContentProps
  extends
    React.ComponentProps<typeof PopoverContent>,
    Pick<
      React.ComponentProps<typeof Command>,
      "shouldFilter" | "filter" | "loop"
    > {}

function FacetedContent({
  className,
  children,
  shouldFilter,
  filter,
  loop,
  ...props
}: FacetedContentProps) {
  return (
    <PopoverContent
      data-slot="faceted-content"
      align="start"
      className={cn("w-50 origin-(--transform-origin) p-0", className)}

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

function FacetedInput({ ...props }: React.ComponentProps<typeof CommandInput>) {
  return <CommandInput data-slot="faceted-input" {...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 FacetedLoading({
  className,
  ...props
}: React.ComponentProps<typeof CommandPrimitive.Loading>) {
  return (
    <CommandPrimitive.Loading
      data-slot="faceted-loading"
      className={cn(
        "py-6 text-center text-sm text-muted-foreground",
        className,
      )}
      {...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 facetedProps = store.getProps();
    return getIsValueSelected(
      getSelection(state, facetedProps),
      facetedProps.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) =>
      getSelectedValues(getSelection(state, store.getProps())).length > 0,
  );

  if (!hasSelection) return null;

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

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

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

export {
  DirectionProvider,
  useDirection,
} from "@base-ui/react/direction-provider";
src/lib/data-table-utils.ts
import type {
  Column,
  ColumnFilter,
  RowData,
  SortingState,
  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: FilterVariant[] = [
  "select",
  "multiSelect",
  "range",
  "dateRange",
];

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 getAriaSort(
  sorting: SortingState,
  columnId: string,
): React.AriaAttributes["aria-sort"] {
  const sort = sorting[0];
  if (sort?.id !== columnId) return undefined;
  return sort.desc ? "descending" : "ascending";
}

export function getVisibleSelectedRows<TData extends RowData>(
  table: Table<DataTableFeatures, TData>,
) {
  return table.getRowModel().rows.filter((row) => row.getIsSelected());
}

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 ? 20 : 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/id.ts
const DEFAULT_ALPHABET =
  "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

interface GenerateIdOptions {
  alphabet?: string;
  length?: number;
}

export function generateId(
  prefixOrOptions?: string | GenerateIdOptions,
  inputOptions: GenerateIdOptions = {},
): string {
  const options =
    typeof prefixOrOptions === "object" ? prefixOrOptions : inputOptions;

  const { alphabet = DEFAULT_ALPHABET, length = 12 } = options;

  return Array.from(
    { length },
    () => alphabet[(Math.random() * alphabet.length) | 0],
  ).join("");
}
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,
  sortFn_alphanumeric,
  sortFn_datetime,
  sortFn_text,
  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(),
  sortFns: {
    alphanumeric: sortFn_alphanumeric,
    datetime: sortFn_datetime,
    text: sortFn_text,
  },
  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)
  );
}
src/registry/bases/base/components/data-table/data-table-view-options.tsx
"use client";

import {
  type ReactTable,
  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 {
  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: ReactTable<DataTableFeatures, TData, unknown>;
  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="View columns"
                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
            aria-label="Columns"
            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>
  );
}

More from Shadcn Table

Browse all components in this library →