shadimage
uicapsule
blockfilter-barsource: ok · hit

Filter Bar

npx shadcn add "https://uicapsule.com/r/filter-bar.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@base-ui/react
commandcmdk
iconslucide-react

Files22 · source cached 10/8/2026, 8:17:00 PM

/_components/active-filters.tsx
import { Button, Separator } from "./ui";
import { useCallback, useEffect, useRef, useState } from "react";
import type { ReactNode } from "react";
import { X } from "lucide-react";

import type {
  Column,
  FilterBinding,
  DataTableFilterActions,
  FiltersState,
  FilterStrategy,
} from "../filter-package";
import { getColumn, bindFilter } from "../filter-package";
import { FilterOperator } from "./filter-operator";
import { FilterSubject } from "./filter-subject";
import { FilterValue } from "./filter-value";

interface ActiveFiltersProps {
  columns: Column[];
  filters: FiltersState;
  actions: DataTableFilterActions;
  strategy: FilterStrategy;
  entityName?: string;
  aiGenerating?: boolean;
}

const ActiveFilterSkeleton = () => (
  <div className="border-(--border) bg-(--muted)/60 text-(--muted-foreground) flex h-7 items-center gap-2 rounded-2xl border px-3 text-xs shadow-xs">
    <div className="flex items-center gap-2">
      <span className="bg-(--muted-foreground)/60 block h-2 w-10 animate-pulse rounded" />
      <span className="bg-(--muted-foreground)/40 block h-2 w-6 animate-pulse rounded" />
    </div>
  </div>
);

interface ActiveFilterProps {
  binding: FilterBinding;
  actions: DataTableFilterActions;
  strategy: FilterStrategy;
  entityName?: string;
}

export const ActiveFilter = ({ binding, actions, strategy, entityName }: ActiveFilterProps) => {
  const { column, filter } = binding;
  if (!filter) {
    return null;
  }
  return (
    <div className="border-(--border) bg-(--background) flex h-7 items-center rounded-2xl border text-xs shadow-xs">
      <FilterSubject column={column} entityName={entityName} />
      <Separator orientation="vertical" />
      <FilterOperator filter={filter} actions={actions} />
      <Separator orientation="vertical" />
      <FilterValue
        binding={binding}
        actions={actions}
        strategy={strategy}
        entityName={entityName}
      />
      <Separator orientation="vertical" />
      <Button
        variant="ghost"
        className="text-(--muted-foreground) hover:text-(--primary) h-full w-7 rounded-none rounded-r-2xl text-xs"
        aria-label={`Remove ${column.displayName} filter`}
        onClick={() => actions.removeFilter(filter.columnId)}
      >
        <X className="size-4 -translate-x-0.5" />
      </Button>
    </div>
  );
};

export const ActiveFilters = ({
  columns,
  filters,
  actions,
  strategy,
  entityName,
  aiGenerating,
}: ActiveFiltersProps) => (
  <>
    {filters.map((filter) => {
      const id = filter.columnId;

      const column = getColumn(columns, id);

      return (
        <ActiveFilter
          key={`active-filter-${filter.columnId}`}
          binding={bindFilter(column, filter)}
          actions={actions}
          strategy={strategy}
          entityName={entityName}
        />
      );
    })}
    {aiGenerating && <ActiveFilterSkeleton />}
  </>
);

export const ActiveFiltersContainer = ({ children }: { children: ReactNode }) => {
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const [showLeftBlur, setShowLeftBlur] = useState(false);
  const [showRightBlur, setShowRightBlur] = useState(true);

  const checkScroll = useCallback(() => {
    const el = scrollContainerRef.current;
    if (!el) {
      return;
    }

    const { scrollLeft, scrollWidth, clientWidth } = el;

    setShowLeftBlur(scrollLeft > 0);
    // 1px buffer absorbs subpixel rounding so the right blur clears at the end.
    setShowRightBlur(scrollLeft + clientWidth < scrollWidth - 1);
  }, []);

  useEffect(() => {
    const el = scrollContainerRef.current;
    if (!el) {
      return;
    }

    const resizeObserver = new ResizeObserver(checkScroll);
    resizeObserver.observe(el);
    const mutationObserver = new MutationObserver(checkScroll);
    mutationObserver.observe(el, { characterData: true, childList: true, subtree: true });
    return () => {
      resizeObserver.disconnect();
      mutationObserver.disconnect();
    };
  }, [checkScroll]);

  return (
    <div className="relative min-w-0 flex-1 overflow-x-hidden md:overflow-visible">
      {showLeftBlur && (
        <div className="from-(--background) animate-in fade-in-0 pointer-events-none absolute top-0 bottom-0 left-0 z-10 md:hidden w-16 bg-gradient-to-r to-transparent" />
      )}

      <div
        ref={scrollContainerRef}
        className="flex gap-2 overflow-x-auto md:flex-wrap md:overflow-visible"
        onScroll={checkScroll}
      >
        {children}
      </div>

      {showRightBlur && (
        <div className="from-(--background) animate-in fade-in-0 pointer-events-none absolute top-0 right-0 bottom-0 z-10 md:hidden w-16 bg-gradient-to-l to-transparent" />
      )}
    </div>
  );
};
/_components/data-table-filter.tsx
"use client";

import type {
  Column,
  DataTableFilterActions,
  FiltersState,
  FilterStrategy,
} from "../filter-package";
import { ActiveFilters, ActiveFiltersContainer } from "./active-filters";
import { FilterActions } from "./filter-actions";
import { FilterSelector } from "./filter-selector";
import { useAiFilterSimulation } from "./use-ai-filter-simulation";

interface DataTableFilterProps {
  columns: Column[];
  filters: FiltersState;
  actions: DataTableFilterActions;
  strategy: FilterStrategy;
  entityName?: string;
}

export const DataTableFilter = ({
  columns,
  filters,
  actions,
  strategy,
  entityName,
}: DataTableFilterProps) => {
  const { aiGenerating, handleAiFilterSubmit } = useAiFilterSimulation({
    actions,
    columns,
  });

  const selectorProps = {
    actions,
    aiGenerating,
    columns,
    filters,
    onAIFilterSubmit: handleAiFilterSubmit,
    strategy,
  } as const;

  return (
    <div className="filter-bar flex w-full items-start gap-2">
      <FilterSelector {...selectorProps} />
      <ActiveFiltersContainer>
        <ActiveFilters
          columns={columns}
          filters={filters}
          actions={actions}
          strategy={strategy}
          entityName={entityName}
          aiGenerating={aiGenerating}
        />
        <FilterActions hasFilters={filters.length > 0} actions={actions} />
      </ActiveFiltersContainer>
    </div>
  );
};
/_components/filter-actions.tsx
import { Button } from "./ui";
import { cn } from "cn";

import type { DataTableFilterActions } from "../filter-package";

interface FilterActionsProps {
  hasFilters: boolean;
  actions?: DataTableFilterActions;
}

export const FilterActions = ({ hasFilters, actions }: FilterActionsProps) => (
  <Button
    className={cn("text-(--muted-foreground) h-7 px-2 font-normal", !hasFilters && "hidden")}
    variant="ghost"
    onClick={() => actions?.removeAllFilters()}
  >
    clear
  </Button>
);
/_components/filter-operator.tsx
import { useState } from "react";
import {
  Button,
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "./ui";
import type {
  ColumnDataType,
  DataTableFilterActions,
  FilterModel,
  FilterOperatorDetails,
  FilterOperators,
  FilterOperatorUpdate,
} from "../filter-package";
import { filterTypeOperatorDetails } from "../filter-package";

const operatorDetails = <K extends ColumnDataType>(
  type: K,
  operator: FilterOperators[K],
): { key: string; target: "single" | "multiple" } => filterTypeOperatorDetails[type][operator];

const relatedOperators = <K extends ColumnDataType>(type: K, operator: FilterOperators[K]) => {
  const current = operatorDetails(type, operator);
  const details: FilterOperatorDetails<FilterOperators[K], K>[] = Object.values(
    filterTypeOperatorDetails[type],
  );
  return details.filter((detail) => detail.target === current.target);
};

const operatorChoices = (
  filter: FilterModel,
): { label: string; update: FilterOperatorUpdate }[] => {
  const { columnId } = filter;
  switch (filter.type) {
    case "text": {
      return relatedOperators("text", filter.operator).map((detail) => ({
        label: detail.key,
        update: { columnId, operator: detail.value, type: "text" },
      }));
    }
    case "number": {
      return relatedOperators("number", filter.operator).map((detail) => ({
        label: detail.key,
        update: { columnId, operator: detail.value, type: "number" },
      }));
    }
    case "date": {
      return relatedOperators("date", filter.operator).map((detail) => ({
        label: detail.key,
        update: { columnId, operator: detail.value, type: "date" },
      }));
    }
    case "boolean": {
      return relatedOperators("boolean", filter.operator).map((detail) => ({
        label: detail.key,
        update: { columnId, operator: detail.value, type: "boolean" },
      }));
    }
    case "option": {
      return relatedOperators("option", filter.operator).map((detail) => ({
        label: detail.key,
        update: { columnId, operator: detail.value, type: "option" },
      }));
    }
    case "multiOption": {
      return relatedOperators("multiOption", filter.operator).map((detail) => ({
        label: detail.key,
        update: { columnId, operator: detail.value, type: "multiOption" },
      }));
    }
    default: {
      return [];
    }
  }
};

export const FilterOperator = ({
  filter,
  actions,
}: {
  filter: FilterModel;
  actions: DataTableFilterActions;
}) => {
  const [open, setOpen] = useState(false);
  const current = operatorDetails(filter.type, filter.operator);
  return (
    <Popover
      open={open}
      onOpenChange={(next) => {
        if (filter.type !== "boolean") {
          setOpen(next);
        }
      }}
    >
      <PopoverTrigger
        render={
          <Button
            variant="ghost"
            className="m-0 h-full w-fit rounded-none p-0 px-2 text-xs whitespace-nowrap"
            onClick={() => {
              if (filter.type !== "boolean") {
                return;
              }
              actions.setFilterOperator({
                columnId: filter.columnId,
                operator: filter.operator === "is" ? "is not" : "is",
                type: "boolean",
              });
            }}
          />
        }
      >
        <span className="text-(--muted-foreground)">{current.key}</span>
      </PopoverTrigger>
      <PopoverContent align="start" className="w-fit origin-(--transform-origin) p-0">
        <Command loop>
          <CommandInput placeholder="search" />
          <CommandEmpty>No results</CommandEmpty>
          <CommandList className="max-h-fit">
            <CommandGroup heading={filter.type === "date" ? undefined : "operators"}>
              {operatorChoices(filter).map(({ label, update }) => (
                <CommandItem
                  key={update.operator}
                  value={update.operator}
                  onSelect={() => {
                    actions.setFilterOperator(update);
                    setOpen(false);
                  }}
                >
                  {label}
                </CommandItem>
              ))}
            </CommandGroup>
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
};
/_components/filter-selector.tsx
import {
  Button,
  Checkbox,
  Command,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "./ui";
import { Fragment, isValidElement, useEffect, useMemo, useRef, useState } from "react";
import type { ElementType as ReactElementType, ReactElement } from "react";
import {
  ArrowLeftIcon,
  ArrowRightIcon,
  ChevronRightIcon,
  ListFilterIcon,
  SparklesIcon,
} from "lucide-react";

import type {
  Column,
  ColumnDataType,
  DataTableFilterActions,
  FiltersState,
  FilterStrategy,
} from "../filter-package";
import { getColumn, bindFilter } from "../filter-package";
import { FilterValueController } from "./filter-value";

const renderColumnIcon = (icon: ReactElement | ReactElementType, className: string) => {
  if (isValidElement(icon)) {
    return icon;
  }
  const IconComp = icon;
  return <IconComp className={className} />;
};

export const FilterableColumn = <TType extends ColumnDataType>({
  column,
  setProperty,
  actions,
  filters,
}: {
  column: Column<TType>;
  setProperty: (value: string) => void;
  actions: DataTableFilterActions;
  filters: FiltersState;
}) => {
  const { icon: Icon } = column;
  const isFiltered = filters.some(
    (filter) => filter.columnId === column.id && filter.values.length > 0,
  );

  const handleSelect = () => {
    if (column.type === "boolean") {
      actions.setFilterValue({ columnId: column.id, type: "boolean", values: [true] });
      return;
    }

    setProperty(column.id);
  };

  return (
    <CommandItem
      value={column.id}
      keywords={[column.displayName]}
      onSelect={handleSelect}
      className="group"
    >
      <div className="flex w-full items-center justify-between">
        <div className="inline-flex items-center gap-1.5">
          {Icon && (
            <div className="relative">
              {renderColumnIcon(Icon, "size-4 stroke-[2.25px]")}
              {isFiltered && (
                <div className="absolute -right-0.5 -bottom-0.5 h-2 w-2 rounded-full bg-green-500" />
              )}
            </div>
          )}
          {!Icon && isFiltered && <div className="h-2 w-2 rounded-full bg-green-500" />}
          <span>{column.displayName}</span>
        </div>
        {column.type !== "boolean" && (
          <ArrowRightIcon className="size-4 opacity-0 group-aria-selected:opacity-100" />
        )}
      </div>
    </CommandItem>
  );
};

interface QuickSearchFiltersProps {
  search?: string;
  filters: FiltersState;
  columns: Column[];
  actions: DataTableFilterActions;
}

export const QuickSearchFilters = ({
  search,
  filters,
  columns,
  actions,
}: QuickSearchFiltersProps) => {
  const cols = useMemo(
    () => columns.filter((column) => column.type === "option" || column.type === "multiOption"),
    [columns],
  );

  if (!search || search.trim().length < 2) {
    return null;
  }

  return (
    <>
      {cols.map((column) => {
        const filter = filters.find((f) => f.columnId === column.id);
        const { options } = column;

        const handleOptionSelect = (value: string, check: boolean) => {
          if (check) {
            actions.addFilterValue(column, [value]);
          } else {
            actions.removeFilterValue(column, [value]);
          }
        };

        return (
          <Fragment key={column.id}>
            {options.map((v) => {
              const checked =
                (filter?.type === "option" || filter?.type === "multiOption") &&
                filter.values.includes(v.value);

              return (
                <CommandItem
                  key={v.value}
                  value={v.value}
                  keywords={[v.label, v.value]}
                  onSelect={() => {
                    handleOptionSelect(v.value, !checked);
                  }}
                  className="group"
                >
                  <div className="group flex items-center gap-1.5">
                    <Checkbox
                      checked={checked}
                      className="dark:border-(--ring) mr-1 opacity-0 group-data-[selected=true]:opacity-100 data-checked:opacity-100"
                    />
                    <div className="flex w-4 items-center justify-center">
                      {v.icon && renderColumnIcon(v.icon, "text-(--primary) size-4")}
                    </div>
                    <div className="flex items-center gap-0.5">
                      <span className="text-(--muted-foreground)">{column.displayName}</span>
                      <ChevronRightIcon className="text-(--muted-foreground)/75 size-3.5" />
                      <span>{v.label}</span>
                    </div>
                  </div>
                </CommandItem>
              );
            })}
          </Fragment>
        );
      })}
    </>
  );
};
interface FilterSelectorProps {
  filters: FiltersState;
  columns: Column[];
  actions: DataTableFilterActions;
  strategy: FilterStrategy;
  onAIFilterSubmit?: (prompt: string) => void;
  aiGenerating?: boolean;
}

export const FilterSelector = ({
  filters,
  columns,
  actions,
  strategy,
  onAIFilterSubmit,
  aiGenerating,
}: FilterSelectorProps) => {
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState("");
  const [property, setProperty] = useState<string | undefined>();
  const inputRef = useRef<HTMLInputElement>(null);

  const visibleColumns = useMemo(() => columns.filter((c) => !c.hidden), [columns]);

  const visibleFilters = useMemo(
    () => filters.filter((f) => visibleColumns.find((c) => c.id === f.columnId)),
    [filters, visibleColumns],
  );

  const column = property ? getColumn(visibleColumns, property) : undefined;
  const filter = property ? visibleFilters.find((f) => f.columnId === property) : undefined;

  useEffect(() => {
    if (!property) {
      return;
    }
    inputRef.current?.focus();
  }, [property]);

  useEffect(() => {
    if (open) {
      return;
    }

    const timeoutId = setTimeout(() => setValue(""), 150);
    return () => clearTimeout(timeoutId);
  }, [open]);

  const content = useMemo(
    () =>
      property && column && column.type !== "boolean" ? (
        <div className="flex flex-col">
          <div className="flex h-9 items-center gap-2 border-b px-2">
            <Button
              variant="ghost"
              size="icon"
              onClick={() => setProperty(undefined)}
              className="h-6 w-6 p-0"
            >
              <ArrowLeftIcon className="size-4" />
              <span className="sr-only">Back</span>
            </Button>
            <div className="flex items-center gap-1.5 text-sm">
              {column.icon && renderColumnIcon(column.icon, "size-4 stroke-[2.25px]")}
              <span className="font-medium">{column.displayName}</span>
            </div>
          </div>
          <FilterValueController
            binding={bindFilter(column, filter)}
            actions={actions}
            strategy={strategy}
          />
        </div>
      ) : (
        <Command
          loop
          filter={(itemValue, search, keywords) => {
            const extendValue = `${itemValue} ${keywords?.join(" ")}`;
            return extendValue.toLowerCase().includes(search.toLowerCase()) ? 1 : 0;
          }}
        >
          <CommandInput
            value={value}
            onValueChange={setValue}
            ref={inputRef}
            placeholder="Search or ask AI"
          />
          <CommandList className="max-h-fit">
            <CommandGroup>
              <CommandItem
                value={value}
                disabled={!onAIFilterSubmit || aiGenerating}
                onSelect={() => {
                  const prompt = value.trim();
                  if (!prompt || !onAIFilterSubmit) {
                    return;
                  }

                  onAIFilterSubmit(prompt);
                  setOpen(false);
                  setTimeout(() => {
                    setValue("");
                    setProperty(undefined);
                  }, 100);
                }}
              >
                <div className="flex w-full items-center justify-between gap-2">
                  <div className="flex items-center gap-2">
                    <SparklesIcon className="size-4" />
                    <span>Ask AI</span>
                  </div>
                  <span className="text-(--muted-foreground) truncate text-xs">
                    {value.trim().length > 0 ? `"${value.trim()}"` : "Type a prompt"}
                  </span>
                </div>
              </CommandItem>
              {visibleColumns.map((visibleColumn) => (
                <FilterableColumn
                  key={visibleColumn.id}
                  column={visibleColumn}
                  setProperty={(id) => {
                    setValue("");
                    setProperty(id);
                  }}
                  actions={actions}
                  filters={visibleFilters}
                />
              ))}
              <QuickSearchFilters
                search={value}
                filters={visibleFilters}
                columns={visibleColumns}
                actions={actions}
              />
            </CommandGroup>
          </CommandList>
        </Command>
      ),
    [
      aiGenerating,
      property,
      column,
      filter,
      visibleFilters,
      visibleColumns,
      actions,
      value,
      strategy,
      onAIFilterSubmit,
    ],
  );

  return (
    <Popover
      open={open}
      onOpenChange={(next) => {
        setOpen(next);
        if (!next) {
          setTimeout(() => setProperty(undefined), 100);
        }
      }}
    >
      <PopoverTrigger
        aria-label="Add filter"
        render={<Button variant="ghost" size="icon" className="size-7 p-0" />}
      >
        <ListFilterIcon className="size-4" />
      </PopoverTrigger>
      <PopoverContent align="start" side="bottom" className="w-fit origin-(--transform-origin) p-0">
        {content}
      </PopoverContent>
    </Popover>
  );
};
/_components/filter-subject.tsx
import { isValidElement } from "react";
import type { Column, ColumnDataType } from "../filter-package";

interface FilterSubjectProps<TType extends ColumnDataType> {
  column: Column<TType>;
  entityName?: string;
}

export const FilterSubject = <TType extends ColumnDataType>({
  column,
  entityName,
}: FilterSubjectProps<TType>) => {
  const subject = column.type === "boolean" ? entityName : column.displayName;

  const { icon: Icon } = column;

  return (
    <span className="flex items-center gap-1 px-2 font-medium whitespace-nowrap select-none">
      {Icon &&
        (isValidElement(Icon)
          ? Icon
          : (() => {
              const IconComp = Icon;
              return <IconComp className="text-(--primary) size-4 stroke-[2.25px]" />;
            })())}

      <span>{subject}</span>
    </span>
  );
};
/_components/filter-value.tsx
import {
  Button,
  Calendar,
  Checkbox,
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
  Input,
  Popover,
  PopoverContent,
  PopoverTrigger,
  Slider,
  Tabs,
  TabsContent,
  TabsList,
  TabsTrigger,
} from "./ui";
import {
  Fragment,
  cloneElement,
  isValidElement,
  memo,
  useCallback,
  useEffect,
  useMemo,
  useState,
} from "react";
import type {
  ChangeEvent,
  ElementType as ReactElementType,
  InputHTMLAttributes,
  Key,
  ReactElement,
} from "react";
import type { DateRange } from "react-day-picker";
import { cn } from "cn";
import { format, isEqual } from "date-fns";
import { Ellipsis } from "lucide-react";

import type {
  Column,
  FilterBinding,
  ColumnDataType,
  ColumnOptionExtended,
  DataTableFilterActions,
  FilterModel,
  FilterStrategy,
} from "../filter-package";
import { createNumberRange, numberFilterOperators } from "../filter-package";
import { useDebouncedCallback } from "./use-debounced-callback";

interface IconProps {
  className?: string;
  key?: Key;
}
type IconLike = ReactElement | ReactElementType;

const renderIcon = (icon: IconLike, props: IconProps = {}) => {
  if (isValidElement(icon)) {
    // oxlint-disable-next-line react/no-clone-element -- applies the caller's className and key to an element the config supplied; wrapping it would change the DOM
    return cloneElement(icon, props);
  }
  const IconComp = icon;
  const { key, ...iconProps } = props;
  return <IconComp key={key} {...iconProps} />;
};

export const DebouncedInput = ({
  value: initialValue,
  onChange,
  debounceMs = 500,
  onBlur,
  onKeyDown,
  ...props
}: {
  value: string | number;
  onChange: (value: string | number) => void;
  debounceMs?: number;
} & Omit<InputHTMLAttributes<HTMLInputElement>, "onChange">) => {
  const [value, setValue] = useState(initialValue);
  const [prevInitialValue, setPrevInitialValue] = useState(initialValue);

  if (prevInitialValue !== initialValue) {
    setPrevInitialValue(initialValue);
    setValue(initialValue);
  }

  const { schedule: debouncedOnChange, cancel, flush } = useDebouncedCallback(onChange, debounceMs);
  useEffect(cancel, [initialValue, cancel]);

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    const newValue = e.target.value;
    setValue(newValue);
    debouncedOnChange(newValue);
  };

  return (
    <Input
      {...props}
      value={value}
      onChange={handleChange}
      onBlur={(event) => {
        flush();
        onBlur?.(event);
      }}
      onKeyDown={(event) => {
        onKeyDown?.(event);
        if (!event.defaultPrevented && (event.key === "Enter" || event.key === "Escape")) {
          flush();
        }
      }}
    />
  );
};

interface FilterValueProps {
  binding: FilterBinding;
  actions: DataTableFilterActions;
  strategy: FilterStrategy;
  entityName?: string;
}

interface FilterValueDisplayProps<K extends ColumnDataType> {
  filter: FilterModel<K>;
  column: Column<K>;
  actions: DataTableFilterActions;
  entityName?: string;
}

export const FilterValueOptionDisplay = ({ filter, column }: FilterValueDisplayProps<"option">) => {
  const { options } = column;
  const selected = options.filter((o) => filter?.values.includes(o.value));

  const [only] = selected;
  if (selected.length === 1 && only) {
    const { label, icon: Icon } = only;
    const hasIcon = !!Icon;
    return (
      <span className="inline-flex items-center gap-1">
        {hasIcon && renderIcon(Icon, { className: "text-(--primary) size-4" })}
        <span>{label}</span>
      </span>
    );
  }
  const name = column.displayName.toLowerCase();
  const pluralName = name.endsWith("s") ? `${name}es` : `${name}s`;

  const hasOptionIcons = !options?.some((o) => !o.icon);

  return (
    <div className="inline-flex items-center gap-0.5">
      {hasOptionIcons &&
        selected.slice(0, 3).map(({ value, icon }) => {
          const Icon = icon;
          if (!Icon) {
            return null;
          }
          return renderIcon(Icon, { className: "size-4", key: value });
        })}
      <span className={cn(hasOptionIcons && "ml-1.5")}>
        {selected.length} {pluralName}
      </span>
    </div>
  );
};

export const FilterValueMultiOptionDisplay = ({
  filter,
  column,
}: FilterValueDisplayProps<"multiOption">) => {
  const { options } = column;
  const selected = options.filter((o) => filter.values.includes(o.value));

  const [only] = selected;
  if (selected.length === 1 && only) {
    const { label, icon: Icon } = only;
    const hasIcon = !!Icon;
    return (
      <span className="inline-flex items-center gap-1.5">
        {hasIcon && renderIcon(Icon, { className: "text-(--primary) size-4" })}

        <span>{label}</span>
      </span>
    );
  }

  const name = column.displayName.toLowerCase();

  const hasOptionIcons = !options?.some((o) => !o.icon);

  return (
    <div className="inline-flex items-center gap-1.5">
      {hasOptionIcons && (
        <div key="icons" className="inline-flex items-center gap-0.5">
          {selected.slice(0, 3).map(({ value, icon }) => {
            const Icon = icon;
            if (!Icon) {
              return null;
            }
            return isValidElement(Icon) ? (
              <Fragment key={value}>{Icon}</Fragment>
            ) : (
              renderIcon(Icon, { className: "size-4", key: value })
            );
          })}
        </div>
      )}
      <span>
        {selected.length} {name}
      </span>
    </div>
  );
};

const formatDateRange = (start: Date, end: Date) => {
  const sameMonth = start.getMonth() === end.getMonth();
  const sameYear = start.getFullYear() === end.getFullYear();

  if (sameMonth && sameYear) {
    return `${format(start, "MMM d")} - ${format(end, "d, yyyy")}`;
  }

  if (sameYear) {
    return `${format(start, "MMM d")} - ${format(end, "MMM d, yyyy")}`;
  }

  return `${format(start, "MMM d, yyyy")} - ${format(end, "MMM d, yyyy")}`;
};

export const FilterValueDateDisplay = ({ filter }: FilterValueDisplayProps<"date">) => {
  if (!filter) {
    return null;
  }
  if (filter.values.length === 0) {
    return <Ellipsis className="size-4" />;
  }
  const [first, second] = filter.values;
  if (filter.values.length === 1 && first) {
    const formattedDateStr = format(first, "MMM d, yyyy");

    return <span>{formattedDateStr}</span>;
  }
  if (filter.values.length === 2 && first && second) {
    const formattedRangeStr = formatDateRange(first, second);

    return <span>{formattedRangeStr}</span>;
  }

  return null;
};

export const FilterValueTextDisplay = ({ filter }: FilterValueDisplayProps<"text">) => {
  if (!filter) {
    return null;
  }
  if (filter.values.length === 0 || (filter.values[0] && filter.values[0].trim() === "")) {
    return <Ellipsis className="size-4" />;
  }

  const [value] = filter.values;

  return <span>{value}</span>;
};

export const FilterValueNumberDisplay = ({ filter }: FilterValueDisplayProps<"number">) => {
  if (!filter || !filter.values || filter.values.length === 0) {
    return null;
  }

  if (filter.operator === "is between" || filter.operator === "is not between") {
    const [minValue, maxValue] = filter.values;

    return (
      <span className="tracking-tight tabular-nums">
        {minValue} and {maxValue}
      </span>
    );
  }

  const [value] = filter.values;
  return <span className="tracking-tight tabular-nums">{value}</span>;
};

export const FilterValueBooleanDisplay = ({
  filter,
  column,
}: FilterValueDisplayProps<"boolean">) => {
  if (!filter || filter.values.length === 0) {
    return null;
  }
  return <span>{column.toggledStateName}</span>;
};

export const FilterValueDisplay = ({
  binding,
  actions,
  entityName,
}: Omit<FilterValueProps, "strategy">) => {
  switch (binding.type) {
    case "option": {
      return (
        binding.filter && (
          <FilterValueOptionDisplay
            filter={binding.filter}
            column={binding.column}
            actions={actions}
            entityName={entityName}
          />
        )
      );
    }
    case "multiOption": {
      return (
        binding.filter && (
          <FilterValueMultiOptionDisplay
            filter={binding.filter}
            column={binding.column}
            actions={actions}
            entityName={entityName}
          />
        )
      );
    }
    case "date": {
      return (
        binding.filter && (
          <FilterValueDateDisplay
            filter={binding.filter}
            column={binding.column}
            actions={actions}
            entityName={entityName}
          />
        )
      );
    }
    case "text": {
      return (
        binding.filter && (
          <FilterValueTextDisplay
            filter={binding.filter}
            column={binding.column}
            actions={actions}
            entityName={entityName}
          />
        )
      );
    }
    case "number": {
      return (
        binding.filter && (
          <FilterValueNumberDisplay
            filter={binding.filter}
            column={binding.column}
            actions={actions}
            entityName={entityName}
          />
        )
      );
    }
    case "boolean": {
      return (
        binding.filter && (
          <FilterValueBooleanDisplay
            filter={binding.filter}
            column={binding.column}
            actions={actions}
            entityName={entityName}
          />
        )
      );
    }
    default: {
      return null;
    }
  }
};

interface FilterValueControllerProps<K extends ColumnDataType> {
  filter?: FilterModel<K>;
  column: Column<K>;
  actions: DataTableFilterActions;
  strategy: FilterStrategy;
}

interface OptionItemProps {
  option: ColumnOptionExtended;
  onToggle: (value: string, checked: boolean) => void;
}

const OptionItemBase = ({ option, onToggle }: OptionItemProps) => {
  const { value, label, icon: Icon, selected } = option;
  const handleSelect = useCallback(() => {
    onToggle(value, !selected);
  }, [onToggle, value, selected]);

  return (
    <CommandItem
      key={value}
      onSelect={handleSelect}
      className="group flex items-center justify-between gap-4"
    >
      <div className="flex min-w-0 flex-1 items-center gap-1.5">
        <Checkbox
          checked={selected}
          className="dark:border-(--ring) mr-1 shrink-0 opacity-0 group-data-[selected=true]:opacity-100 data-checked:opacity-100"
        />
        <div className="shrink-0">
          {Icon && renderIcon(Icon, { className: "text-(--primary) size-4" })}
        </div>
        <span className="overflow-x-hidden overflow-ellipsis whitespace-nowrap">{label}</span>
      </div>
    </CommandItem>
  );
};

const OptionItem = memo(OptionItemBase);

export const FilterValueOptionController = ({
  filter,
  column,
  actions,
}: FilterValueControllerProps<"option" | "multiOption">) => {
  // oxlint-disable-next-line react/hook-use-state -- a one-time snapshot of the selection when the controller opens, so items stay in their group while toggling; there is deliberately no setter
  const [initialSelectedValues] = useState(() => new Set(filter?.values || []));

  const { selectedOptions, unselectedOptions } = useMemo(() => {
    const allOptions = column.options.map((o) => {
      const currentlySelected = filter?.values.includes(o.value) ?? false;
      return {
        count: o.count ?? 0,
        icon: o.icon,
        label: o.label,
        selected: currentlySelected,
        value: o.value,
      };
    });

    const selected = allOptions.filter((o) => initialSelectedValues.has(o.value));
    const unselected = allOptions.filter((o) => !initialSelectedValues.has(o.value));
    return { selectedOptions: selected, unselectedOptions: unselected };
  }, [column, filter?.values, initialSelectedValues]);

  const handleToggle = useCallback(
    (value: string, checked: boolean) => {
      if (checked) {
        actions.addFilterValue(column, [value]);
      } else {
        actions.removeFilterValue(column, [value]);
      }
    },
    [actions, column],
  );

  return (
    <Command className="max-w-[300px]" loop>
      <CommandInput autoFocus placeholder="search" />
      <CommandEmpty>No results</CommandEmpty>
      <CommandList>
        <CommandGroup className={cn(selectedOptions.length === 0 && "hidden")}>
          {selectedOptions.map((option) => (
            <OptionItem key={option.value} option={option} onToggle={handleToggle} />
          ))}
        </CommandGroup>
        <CommandSeparator
          className={cn(
            (unselectedOptions.length === 0 || selectedOptions.length === 0) && "hidden",
          )}
        />
        <CommandGroup className={cn(unselectedOptions.length === 0 && "hidden")}>
          {unselectedOptions.map((option) => (
            <OptionItem key={option.value} option={option} onToggle={handleToggle} />
          ))}
        </CommandGroup>
      </CommandList>
    </Command>
  );
};

export const FilterValueDateController = ({
  filter,
  column,
  actions,
}: FilterValueControllerProps<"date">) => {
  const start = filter?.values[0];
  const date: DateRange | undefined = start ? { from: start, to: filter?.values[1] } : undefined;

  const changeDateRange = (value: DateRange | undefined) => {
    const from = value?.from;
    const to = from && value?.to && !isEqual(from, value.to) ? value.to : undefined;

    let newValues: Date[] = [];
    if (from && to) {
      newValues = [from, to];
    } else if (from) {
      newValues = [from];
    }

    actions.setFilterValue({ columnId: column.id, type: column.type, values: newValues });
  };

  return (
    <Command>
      <CommandList className="max-h-fit">
        <CommandGroup>
          <div>
            <Calendar
              mode="range"
              defaultMonth={date?.from}
              selected={date}
              onSelect={changeDateRange}
              numberOfMonths={1}
            />
          </div>
        </CommandGroup>
      </CommandList>
    </Command>
  );
};

export const FilterValueTextController = ({
  filter,
  column,
  actions,
}: FilterValueControllerProps<"text">) => {
  const changeText = (value: string | number) => {
    actions.setFilterValue({ columnId: column.id, type: "text", values: [String(value)] });
  };

  return (
    <Command>
      <CommandList className="max-h-fit">
        <CommandGroup>
          <CommandItem>
            <DebouncedInput
              placeholder="search"
              autoFocus
              value={filter?.values[0] ?? ""}
              onChange={changeText}
            />
          </CommandItem>
        </CommandGroup>
      </CommandList>
    </Command>
  );
};

export const FilterValueNumberController = ({
  filter,
  column,
  actions,
}: FilterValueControllerProps<"number">) => {
  const minMax = useMemo<[number, number]>(
    () => [column.min, column.max],
    [column.min, column.max],
  );
  const [sliderMin, sliderMax] = [minMax ? minMax[0] : 0, minMax ? minMax[1] : 0];

  const [values, setValues] = useState(filter?.values ?? [0, 0]);
  const [previousFilterValues, setPreviousFilterValues] = useState(filter?.values);

  if (previousFilterValues !== filter?.values) {
    setPreviousFilterValues(filter?.values);
    setValues(filter?.values ?? [0, 0]);
  }

  const isNumberRange = filter && numberFilterOperators[filter.operator].target === "multiple";

  const setNumberFilterValue = useCallback(
    (newValues: number[]) =>
      actions.setFilterValue({ columnId: column.id, type: "number", values: newValues }),
    [actions, column],
  );
  const {
    schedule: setFilterValueDebounced,
    cancel: cancelFilterValueUpdate,
    flush: flushFilterValueUpdate,
  } = useDebouncedCallback(setNumberFilterValue, 500);
  useEffect(cancelFilterValueUpdate, [filter?.values, cancelFilterValueUpdate]);

  const changeNumber = (value: number[]) => {
    cancelFilterValueUpdate();
    setValues(value);
    setNumberFilterValue(value);
  };

  const changeSlider = (value: number | readonly number[]) => {
    const nextValues = Array.isArray(value) ? [...value] : [value];
    setValues(nextValues);
    setFilterValueDebounced(nextValues);
  };

  const changeMinNumber = (value: number) => {
    const newValues = createNumberRange([value, values[1] ?? 0]);
    changeNumber(newValues);
  };

  const changeMaxNumber = (value: number) => {
    const newValues = createNumberRange([values[0] ?? 0, value]);
    changeNumber(newValues);
  };

  const changeType = useCallback(
    (type: "single" | "range") => {
      let newValues: number[] = [];
      if (type === "single") {
        newValues = [values[0] ?? 0];
      } else if (minMax) {
        const value = values[0] ?? 0;
        newValues =
          value - minMax[0] < minMax[1] - value
            ? createNumberRange([value, minMax[1]])
            : createNumberRange([minMax[0], value]);
      } else {
        newValues = createNumberRange([values[0] ?? 0, values[1] ?? 0]);
      }

      const newOperator = type === "single" ? "is" : "is between";

      setValues(newValues);

      // Cancel the old value before changing operators.
      cancelFilterValueUpdate();

      actions.setFilterOperator({ columnId: column.id, operator: newOperator, type: "number" });
      actions.setFilterValue({ columnId: column.id, type: column.type, values: newValues });
    },
    [values, column, actions, minMax, cancelFilterValueUpdate],
  );

  return (
    <Command
      onBlur={flushFilterValueUpdate}
      onKeyDown={(event) => {
        if (event.key === "Escape") {
          flushFilterValueUpdate();
        }
      }}
    >
      <CommandList className="w-[300px] px-2 py-2">
        <CommandGroup>
          <div className="flex w-full flex-col">
            <Tabs
              value={isNumberRange ? "range" : "single"}
              onValueChange={(v) => changeType(v === "range" ? "range" : "single")}
            >
              <TabsList className="w-full *:text-xs">
                <TabsTrigger value="single">single</TabsTrigger>
                <TabsTrigger value="range">range</TabsTrigger>
              </TabsList>
              <TabsContent value="single" className="mt-4 flex flex-col gap-4">
                {minMax && (
                  <Slider
                    value={[values[0] ?? 0]}
                    onValueChange={changeSlider}
                    onValueCommitted={flushFilterValueUpdate}
                    min={sliderMin}
                    max={sliderMax}
                    step={1}
                    aria-orientation="horizontal"
                  />
                )}
                <div className="flex items-center gap-2">
                  <span className="text-xs font-medium">value</span>
                  <DebouncedInput
                    id="single"
                    type="number"
                    value={(values[0] ?? 0).toString()}
                    onChange={(v) => changeNumber([Number(v)])}
                  />
                </div>
              </TabsContent>
              <TabsContent value="range" className="mt-4 flex flex-col gap-4">
                {minMax && (
                  <Slider
                    value={values}
                    onValueChange={changeSlider}
                    onValueCommitted={flushFilterValueUpdate}
                    min={sliderMin}
                    max={sliderMax}
                    step={1}
                    aria-orientation="horizontal"
                  />
                )}
                <div className="grid grid-cols-2 gap-4">
                  <div className="flex items-center gap-2">
                    <span className="text-xs font-medium">min</span>
                    <DebouncedInput
                      type="number"
                      value={values[0] ?? 0}
                      onChange={(v) => changeMinNumber(Number(v))}
                    />
                  </div>
                  <div className="flex items-center gap-2">
                    <span className="text-xs font-medium">max</span>
                    <DebouncedInput
                      type="number"
                      value={values[1] ?? 0}
                      onChange={(v) => changeMaxNumber(Number(v))}
                    />
                  </div>
                </div>
              </TabsContent>
            </Tabs>
          </div>
        </CommandGroup>
      </CommandList>
    </Command>
  );
};

export const FilterValueController = ({
  binding,
  actions,
  strategy,
}: Omit<FilterValueProps, "entityName">) => {
  switch (binding.type) {
    case "option": {
      return (
        <FilterValueOptionController
          filter={binding.filter}
          column={binding.column}
          actions={actions}
          strategy={strategy}
        />
      );
    }
    case "multiOption": {
      return (
        <FilterValueOptionController
          filter={binding.filter}
          column={binding.column}
          actions={actions}
          strategy={strategy}
        />
      );
    }
    case "date": {
      return (
        <FilterValueDateController
          filter={binding.filter}
          column={binding.column}
          actions={actions}
          strategy={strategy}
        />
      );
    }
    case "text": {
      return (
        <FilterValueTextController
          filter={binding.filter}
          column={binding.column}
          actions={actions}
          strategy={strategy}
        />
      );
    }
    case "number": {
      return (
        <FilterValueNumberController
          filter={binding.filter}
          column={binding.column}
          actions={actions}
          strategy={strategy}
        />
      );
    }
    case "boolean": {
      return null;
    }
    default: {
      return null;
    }
  }
};

export const FilterValue = ({ binding, actions, strategy, entityName }: FilterValueProps) => {
  const [open, setOpen] = useState(false);
  return (
    <Popover
      open={open}
      onOpenChange={(next) => {
        if (binding.type !== "boolean") {
          setOpen(next);
        }
      }}
    >
      <PopoverTrigger
        render={
          <Button
            variant="ghost"
            className={cn(
              "m-0 h-full w-fit rounded-none p-0 px-2 text-xs whitespace-nowrap",
              binding.type === "boolean" && "hover:bg-inherit",
            )}
          />
        }
      >
        <FilterValueDisplay binding={binding} actions={actions} entityName={entityName} />
      </PopoverTrigger>
      <PopoverContent align="start" side="bottom" className="w-fit origin-(--transform-origin) p-0">
        <FilterValueController binding={binding} actions={actions} strategy={strategy} />
      </PopoverContent>
    </Popover>
  );
};
/_components/ui.tsx
"use client";

import type { ComponentProps } from "react";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import { Slider as SliderPrimitive } from "@base-ui/react/slider";
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
import { Command as CommandPrimitive } from "cmdk";
import { cn } from "cn";
import { CheckIcon, SearchIcon } from "lucide-react";
import { DayPicker } from "react-day-picker";
import "react-day-picker/style.css";
import "../filter-bar.css";

type Styled<Props> = Omit<Props, "className"> & { className?: string };

export const Button = ({
  className,
  variant = "default",
  size = "default",
  ...props
}: Styled<ButtonPrimitive.Props> & {
  variant?: "default" | "outline" | "ghost";
  size?: "default" | "sm" | "icon";
}) => (
  <ButtonPrimitive
    className={cn(
      "inline-flex shrink-0 items-center justify-center gap-2 rounded-full text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-(--ring) disabled:pointer-events-none disabled:opacity-50 [&_svg]:shrink-0",
      variant === "default" && "bg-(--primary) text-(--primary-foreground) hover:bg-(--primary)/80",
      variant === "outline" && "border border-(--border) bg-(--background) hover:bg-(--muted)",
      variant === "ghost" && "hover:bg-(--muted) aria-expanded:bg-(--muted)",
      size === "default" && "h-9 px-4 py-2",
      size === "sm" && "h-8 px-2.5",
      size === "icon" && "size-9",
      className,
    )}
    {...props}
  />
);

export const Input = ({ className, ...props }: ComponentProps<"input">) => (
  <input
    className={cn(
      "h-9 w-full min-w-0 rounded-md border border-(--input) bg-transparent px-2.5 py-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-(--ring) disabled:opacity-50",
      className,
    )}
    {...props}
  />
);

export const Popover = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;

export const PopoverContent = ({
  className,
  align = "center",
  side = "bottom",
  ...props
}: Styled<PopoverPrimitive.Popup.Props> &
  Pick<PopoverPrimitive.Positioner.Props, "align" | "side">) => (
  <PopoverPrimitive.Portal>
    <PopoverPrimitive.Positioner
      align={align}
      side={side}
      sideOffset={4}
      className="filter-bar isolate z-50"
    >
      <PopoverPrimitive.Popup
        className={cn(
          "flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-md bg-(--popover) p-4 text-sm text-(--popover-foreground) shadow-md ring-1 ring-(--foreground)/10 outline-none",
          className,
        )}
        {...props}
      />
    </PopoverPrimitive.Positioner>
  </PopoverPrimitive.Portal>
);

export const Checkbox = ({ className, ...props }: Styled<CheckboxPrimitive.Root.Props>) => (
  <CheckboxPrimitive.Root
    className={cn(
      "relative size-[15px] shrink-0 rounded-[5px] border-[1.5px] border-(--input) outline-none focus-visible:ring-2 focus-visible:ring-(--ring) data-checked:border-transparent",
      className,
    )}
    {...props}
  >
    <CheckboxPrimitive.Indicator className="absolute inset-0 grid place-content-center text-(--primary)">
      <CheckIcon className="size-4" strokeWidth={2.5} />
    </CheckboxPrimitive.Indicator>
  </CheckboxPrimitive.Root>
);

export const Separator = ({
  className,
  orientation = "horizontal",
  ...props
}: Styled<SeparatorPrimitive.Props>) => (
  <SeparatorPrimitive
    orientation={orientation}
    className={cn(
      "shrink-0 bg-(--border) data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch",
      className,
    )}
    {...props}
  />
);

export const Command = ({ className, ...props }: ComponentProps<typeof CommandPrimitive>) => (
  <CommandPrimitive
    className={cn(
      "flex size-full flex-col overflow-hidden rounded-xl bg-(--popover) p-1 text-(--popover-foreground)",
      className,
    )}
    {...props}
  />
);

export const CommandInput = ({
  className,
  ...props
}: ComponentProps<typeof CommandPrimitive.Input>) => (
  <div className="relative m-1 flex h-8 items-center gap-2 rounded-lg border border-(--input)/30 bg-(--input)/30 px-2">
    <SearchIcon className="size-4 shrink-0 opacity-50" />
    <CommandPrimitive.Input
      aria-label="Search filters"
      className={cn("w-full bg-transparent text-sm outline-none disabled:opacity-50", className)}
      {...props}
    />
  </div>
);

export const CommandList = ({
  className,
  ...props
}: ComponentProps<typeof CommandPrimitive.List>) => (
  <CommandPrimitive.List
    className={cn("max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none", className)}
    {...props}
  />
);

export const CommandEmpty = ({
  className,
  ...props
}: ComponentProps<typeof CommandPrimitive.Empty>) => (
  <CommandPrimitive.Empty className={cn("py-6 text-center text-sm", className)} {...props} />
);

export const CommandGroup = ({
  className,
  ...props
}: ComponentProps<typeof CommandPrimitive.Group>) => (
  <CommandPrimitive.Group
    className={cn(
      "overflow-hidden p-1 text-(--foreground) **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:text-(--muted-foreground)",
      className,
    )}
    {...props}
  />
);

export const CommandItem = ({
  className,
  ...props
}: ComponentProps<typeof CommandPrimitive.Item>) => (
  <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-none select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-(--muted) [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
      className,
    )}
    {...props}
  />
);

export const CommandSeparator = ({
  className,
  ...props
}: ComponentProps<typeof CommandPrimitive.Separator>) => (
  <CommandPrimitive.Separator className={cn("-mx-1 h-px bg-(--border)", className)} {...props} />
);

export const Slider = ({ className, value, ...props }: Styled<SliderPrimitive.Root.Props>) => {
  const count = Array.isArray(value) ? value.length : 1;
  return (
    <SliderPrimitive.Root
      value={value}
      className={cn("relative flex w-full touch-none items-center select-none", className)}
      {...props}
    >
      <SliderPrimitive.Control className="relative flex w-full items-center">
        <SliderPrimitive.Track className="relative h-1.5 w-full rounded-full bg-(--muted)">
          <SliderPrimitive.Indicator className="rounded-full bg-(--primary)" />
        </SliderPrimitive.Track>
        {Array.from({ length: count }, (_, index) => (
          <SliderPrimitive.Thumb
            key={index}
            index={index}
            className="block size-4 rounded-full border border-(--primary) bg-(--background) shadow-sm outline-none focus-visible:ring-4 focus-visible:ring-(--primary)/30"
          />
        ))}
      </SliderPrimitive.Control>
    </SliderPrimitive.Root>
  );
};

export const Tabs = ({ className, ...props }: Styled<TabsPrimitive.Root.Props>) => (
  <TabsPrimitive.Root className={cn("flex flex-col gap-2", className)} {...props} />
);

export const TabsList = ({ className, ...props }: Styled<TabsPrimitive.List.Props>) => (
  <TabsPrimitive.List
    className={cn(
      "inline-flex h-9 w-fit items-center justify-center rounded-lg bg-(--muted) p-[3px] text-(--muted-foreground)",
      className,
    )}
    {...props}
  />
);

export const TabsTrigger = ({ className, ...props }: Styled<TabsPrimitive.Tab.Props>) => (
  <TabsPrimitive.Tab
    className={cn(
      "inline-flex h-full flex-1 items-center justify-center rounded-md px-2 py-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-(--ring) data-active:bg-(--background) data-active:text-(--foreground)",
      className,
    )}
    {...props}
  />
);

export const TabsContent = ({ className, ...props }: Styled<TabsPrimitive.Panel.Props>) => (
  <TabsPrimitive.Panel className={cn("flex-1 outline-none", className)} {...props} />
);

export const Calendar = (props: ComponentProps<typeof DayPicker>) => (
  <DayPicker showOutsideDays className="filter-bar-calendar p-3" {...props} />
);
/_components/use-ai-filter-simulation.ts
import { useCallback } from "react";
import { useChat } from "@ai-sdk/react";
import { StaticChatTransport } from "@loremllm/transport";

import type { Column, DataTableFilterActions } from "../filter-package";
import type { UIMessage } from "@ai-sdk/react";

type FilterUIMessage = UIMessage<
  unknown,
  {
    setFilter: {
      columnId: string;
      values: string[];
    };
  }
>;

const transport = new StaticChatTransport<FilterUIMessage>({
  chunkDelayMs: [600, 1400],
  async *mockResponse() {
    yield {
      data: {
        columnId: "skills",
        values: ["javascript", "typescript", "react", "nodejs"],
      },
      type: "data-setFilter",
    };

    yield {
      data: {
        columnId: "department",
        values: ["design"],
      },
      type: "data-setFilter",
    };
  },
});

interface UseAiFilterSimulationParams {
  columns: Column[];
  actions: DataTableFilterActions;
}

export const useAiFilterSimulation = ({ columns, actions }: UseAiFilterSimulationParams) => {
  const { sendMessage, status } = useChat<FilterUIMessage>({
    onData: (dataPart) => {
      if (dataPart.type === "data-setFilter") {
        const { columnId, values } = dataPart.data;
        const column = columns.find((col) => col.id === columnId);
        if (column?.type === "option" || column?.type === "multiOption") {
          actions.setFilterValue({ columnId, type: column.type, values });
        }
      }
    },
    transport,
  });

  const handleAiFilterSubmit = useCallback(
    (prompt: string) => {
      actions.removeAllFilters();
      void sendMessage({ text: prompt });
    },
    [sendMessage, actions],
  );

  const aiGenerating = status === "streaming" || status === "submitted";

  return {
    aiGenerating,
    handleAiFilterSubmit,
  };
};
/_components/use-debounced-callback.ts
import { useCallback, useEffect, useRef } from "react";

export const useDebouncedCallback = <TArgs extends unknown[]>(
  callback: (...args: TArgs) => void,
  delay: number,
) => {
  const callbackRef = useRef(callback);
  const pendingRef = useRef<{ timeout: ReturnType<typeof setTimeout>; args: TArgs } | null>(null);

  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  const cancel = useCallback(() => {
    if (pendingRef.current !== null) {
      clearTimeout(pendingRef.current.timeout);
    }
    pendingRef.current = null;
  }, []);

  useEffect(() => cancel, [cancel]);

  const flush = useCallback(() => {
    const pending = pendingRef.current;
    if (!pending) {
      return;
    }
    cancel();
    callbackRef.current(...pending.args);
  }, [cancel]);

  const schedule = useCallback(
    (...args: TArgs) => {
      cancel();
      pendingRef.current = { args, timeout: setTimeout(flush, delay) };
    },
    [cancel, delay, flush],
  );

  return { cancel, flush, schedule };
};
/filter-bar.css
.filter-bar {
  --background: #fff;
  --foreground: #18181b;
  --primary: #f66b2c;
  --primary-foreground: #fff;
  --secondary: #f4f4f5;
  --secondary-foreground: #27272a;
  --muted: #f4f4f5;
  --muted-foreground: #71717a;
  --accent: #f4f4f5;
  --accent-foreground: #27272a;
  --popover: #fff;
  --popover-foreground: #18181b;
  --border: #e4e4e7;
  --input: #e4e4e7;
  --ring: #a1a1aa;
  --destructive: #dc2626;
  color: var(--foreground);
}

.dark .filter-bar {
  --background: #18181b;
  --foreground: #fafafa;
  --primary-foreground: #27272a;
  --secondary: #27272a;
  --secondary-foreground: #fafafa;
  --muted: #27272a;
  --muted-foreground: #a1a1aa;
  --accent: #27272a;
  --accent-foreground: #fafafa;
  --popover: #27272a;
  --popover-foreground: #fafafa;
  --border: #ffffff1a;
  --input: #ffffff26;
  --ring: #71717a;
  --destructive: #f87171;
}

.filter-bar-calendar {
  --rdp-accent-color: var(--primary);
  --rdp-accent-background-color: var(--accent);
  --rdp-day-height: 32px;
  --rdp-day-width: 32px;
  --rdp-day_button-height: 32px;
  --rdp-day_button-width: 32px;
  font-size: 0.875rem;
}

@layer base {
  :where(.filter-bar, .filter-bar *) {
    border-color: var(--border);
  }
}
/filter-package/core/columns/column-factory.ts
import type { Column, ColumnConfig, ColumnOption, FilterStrategy } from "../types";

const isPresent = <T>(value: T | null | undefined): value is T =>
  value !== null && value !== undefined;

const prepareOptions = (
  values: (string | ColumnOption)[],
  configured: ColumnOption[] | undefined,
  strategy: FilterStrategy,
) => {
  if (strategy === "server" && !configured) {
    throw new Error("Server filters require explicit column options");
  }
  const entries = values.map((value) =>
    // eslint-disable-next-line anti-slop/no-runtime-typeof -- Discriminates the typed string | ColumnOption config union.
    typeof value === "string" ? { label: value, value } : value,
  );
  const unique = new Map<string, ColumnOption>();
  for (const option of configured ?? entries) {
    if (!unique.has(option.value)) {
      unique.set(option.value, option);
    }
  }
  const counts = new Map<string, number>();
  for (const entry of entries) {
    counts.set(entry.value, (counts.get(entry.value) ?? 0) + 1);
  }
  return {
    options: Array.from(unique.values(), (option) => ({
      ...option,
      count: strategy === "server" ? option.count : (counts.get(option.value) ?? 0),
    })),
    values: entries.filter((entry) => unique.has(entry.value)).map((entry) => entry.value),
  };
};

const createColumn = <Row>(
  data: Row[],
  config: ColumnConfig<Row>,
  strategy: FilterStrategy,
): Column => {
  const common = {
    displayName: config.displayName,
    hidden: config.hidden,
    icon: config.icon,
    id: config.id,
  };
  switch (config.type) {
    case "text": {
      return {
        ...common,
        type: "text",
        values: data.map(config.accessor).filter(isPresent),
      };
    }
    case "date": {
      return {
        ...common,
        type: "date",
        values: data.map(config.accessor).filter(isPresent),
      };
    }
    case "boolean": {
      return {
        ...common,
        toggledStateName: config.toggledStateName,
        type: "boolean",
        values: data.map(config.accessor).filter(isPresent),
      };
    }
    case "number": {
      const values = data.map(config.accessor).filter(isPresent).filter(Number.isFinite);
      let min = values[0] ?? 0;
      let max = min;
      for (const value of values) {
        min = Math.min(min, value);
        max = Math.max(max, value);
      }
      return {
        ...common,
        max: config.max ?? max,
        min: config.min ?? min,
        type: "number",
        values,
      };
    }
    case "option": {
      return {
        ...common,
        type: "option",
        ...prepareOptions(data.map(config.accessor).filter(isPresent), config.options, strategy),
      };
    }
    case "multiOption": {
      return {
        ...common,
        type: "multiOption",
        ...prepareOptions(
          data.flatMap((row) => config.accessor(row) ?? []),
          config.options,
          strategy,
        ),
      };
    }
    default: {
      throw new Error(`Unsupported column type: ${String(config satisfies never)}`);
    }
  }
};

export const createColumns = <Row>(
  data: Row[],
  configs: readonly ColumnConfig<Row>[],
  strategy: FilterStrategy,
): Column[] => configs.map((config) => createColumn(data, config, strategy));
/filter-package/core/filters.ts
import type {
  FilterModel,
  FiltersState,
  FilterValueUpdate,
  FilterOperatorUpdate,
  Column,
  OptionBasedColumnDataType,
} from "./types";
import { DEFAULT_OPERATORS, determineNewOperator } from "./operators";

const replaceFilter = (filters: FiltersState, next: FilterModel): FiltersState => {
  if (next.values.length === 0) {
    return filters.filter((filter) => filter.columnId !== next.columnId);
  }
  return filters.some((filter) => filter.columnId === next.columnId)
    ? filters.map((filter) => (filter.columnId === next.columnId ? next : filter))
    : [...filters, next];
};

const defaultOperator = <Operator>(
  count: number,
  operators: { single: Operator; multiple: Operator },
) => (count > 1 ? operators.multiple : operators.single);

export const setFilterValue = (filters: FiltersState, update: FilterValueUpdate): FiltersState => {
  const current = filters.find((filter) => filter.columnId === update.columnId);
  const { columnId } = update;
  switch (update.type) {
    case "text": {
      const values = [...new Set(update.values)].filter((value) => value.trim().length > 0);
      return replaceFilter(filters, {
        columnId,
        operator: current?.type === "text" ? current.operator : "contains",
        type: "text",
        values,
      });
    }
    case "boolean": {
      const values = update.values.slice(0, 1);
      return replaceFilter(filters, {
        columnId,
        operator: current?.type === "boolean" ? current.operator : "is",
        type: "boolean",
        values,
      });
    }
    case "number": {
      const values = update.values
        .filter(Number.isFinite)
        .slice(0, 2)
        .toSorted((a, b) => a - b);
      const operator =
        current?.type === "number"
          ? determineNewOperator("number", current.values, values, current.operator)
          : defaultOperator(values.length, DEFAULT_OPERATORS.number);
      return replaceFilter(filters, { columnId, operator, type: "number", values });
    }
    case "date": {
      const values = update.values
        .filter((value) => Number.isFinite(value.getTime()))
        .slice(0, 2)
        .toSorted((a, b) => a.getTime() - b.getTime());
      const operator =
        current?.type === "date"
          ? determineNewOperator("date", current.values, values, current.operator)
          : defaultOperator(values.length, DEFAULT_OPERATORS.date);
      return replaceFilter(filters, { columnId, operator, type: "date", values });
    }
    case "option": {
      const values = [...new Set(update.values)];
      const operator =
        current?.type === "option"
          ? determineNewOperator("option", current.values, values, current.operator)
          : defaultOperator(values.length, DEFAULT_OPERATORS.option);
      return replaceFilter(filters, { columnId, operator, type: "option", values });
    }
    case "multiOption": {
      const values = [...new Set(update.values)];
      const operator =
        current?.type === "multiOption"
          ? determineNewOperator("multiOption", current.values, values, current.operator)
          : defaultOperator(values.length, DEFAULT_OPERATORS.multiOption);
      return replaceFilter(filters, { columnId, operator, type: "multiOption", values });
    }
    default: {
      throw new Error(`Unsupported filter type: ${String(update satisfies never)}`);
    }
  }
};

export const setFilterOperator = (
  filters: FiltersState,
  update: FilterOperatorUpdate,
): FiltersState =>
  filters.map((filter) => {
    if (filter.columnId !== update.columnId) {
      return filter;
    }
    switch (update.type) {
      case "text": {
        return filter.type === "text" ? { ...filter, operator: update.operator } : filter;
      }
      case "number": {
        return filter.type === "number" ? { ...filter, operator: update.operator } : filter;
      }
      case "date": {
        return filter.type === "date" ? { ...filter, operator: update.operator } : filter;
      }
      case "boolean": {
        return filter.type === "boolean" ? { ...filter, operator: update.operator } : filter;
      }
      case "option": {
        return filter.type === "option" ? { ...filter, operator: update.operator } : filter;
      }
      case "multiOption": {
        return filter.type === "multiOption" ? { ...filter, operator: update.operator } : filter;
      }
      default: {
        return filter;
      }
    }
  });

export const toggleFilterValues = (
  filters: FiltersState,
  column: Column<OptionBasedColumnDataType>,
  values: string[],
  add: boolean,
): FiltersState => {
  const current = filters.find((filter) => filter.columnId === column.id);
  const existing =
    current?.type === "option" || current?.type === "multiOption" ? current.values : [];
  return setFilterValue(filters, {
    columnId: column.id,
    type: column.type,
    values: add ? [...existing, ...values] : existing.filter((value) => !values.includes(value)),
  });
};
/filter-package/core/operators.ts
import type {
  ColumnDataType,
  FilterDetails,
  FilterOperators,
  FilterOperatorTarget,
  FilterTypeOperatorDetails,
  FilterValues,
} from "./types";

export const DEFAULT_OPERATORS = {
  boolean: {
    multiple: "is",
    single: "is",
  },
  date: {
    multiple: "is between",
    single: "is",
  },
  multiOption: {
    multiple: "include any of",
    single: "include",
  },
  number: {
    multiple: "is between",
    single: "is",
  },
  option: {
    multiple: "is any of",
    single: "is",
  },
  text: {
    multiple: "contains",
    single: "contains",
  },
} satisfies Record<ColumnDataType, Record<FilterOperatorTarget, FilterOperators[ColumnDataType]>>;

export const optionFilterOperators = {
  is: {
    key: "is",
    multiple: "is any of",
    target: "single",
    value: "is",
  },
  "is any of": {
    key: "is any of",
    single: "is",
    target: "multiple",
    value: "is any of",
  },
  "is none of": {
    key: "is none of",
    single: "is not",
    target: "multiple",
    value: "is none of",
  },
  "is not": {
    key: "is not",
    multiple: "is none of",
    target: "single",
    value: "is not",
  },
} as const satisfies FilterDetails<"option">;

export const multiOptionFilterOperators = {
  exclude: {
    key: "excludes",
    multiple: "exclude if any of",
    target: "single",
    value: "exclude",
  },
  "exclude if all": {
    key: "excludes all of",
    single: "exclude",
    target: "multiple",
    value: "exclude if all",
  },
  "exclude if any of": {
    key: "excludes if any of",
    single: "exclude",
    target: "multiple",
    value: "exclude if any of",
  },
  include: {
    key: "includes",
    multiple: "include any of",
    target: "single",
    value: "include",
  },
  "include all of": {
    key: "includes all of",
    single: "include",
    target: "multiple",
    value: "include all of",
  },
  "include any of": {
    key: "includes any of",
    single: "include",
    target: "multiple",
    value: "include any of",
  },
} as const satisfies FilterDetails<"multiOption">;

export const dateFilterOperators = {
  is: {
    key: "is",
    multiple: "is between",
    target: "single",
    value: "is",
  },
  "is after": {
    key: "is after",
    multiple: "is between",
    target: "single",
    value: "is after",
  },
  "is before": {
    key: "is before",
    multiple: "is between",
    target: "single",
    value: "is before",
  },
  "is between": {
    key: "is between",
    single: "is",
    target: "multiple",
    value: "is between",
  },
  "is not": {
    key: "is not",
    multiple: "is not between",
    target: "single",
    value: "is not",
  },
  "is not between": {
    key: "is not between",
    single: "is not",
    target: "multiple",
    value: "is not between",
  },
  "is on or after": {
    key: "is on or after",
    multiple: "is between",
    target: "single",
    value: "is on or after",
  },
  "is on or before": {
    key: "is on or before",
    multiple: "is between",
    target: "single",
    value: "is on or before",
  },
} as const satisfies FilterDetails<"date">;

export const textFilterOperators = {
  contains: {
    key: "contains",
    target: "single",
    value: "contains",
  },
  "does not contain": {
    key: "does not contain",
    target: "single",
    value: "does not contain",
  },
} as const satisfies FilterDetails<"text">;

export const numberFilterOperators = {
  is: {
    key: "is",
    multiple: "is between",
    target: "single",
    value: "is",
  },
  "is between": {
    key: "is between",
    single: "is",
    target: "multiple",
    value: "is between",
  },
  "is greater than": {
    key: "greater than",
    multiple: "is between",
    target: "single",
    value: "is greater than",
  },
  "is greater than or equal to": {
    key: "greater than or equal",
    multiple: "is between",
    target: "single",
    value: "is greater than or equal to",
  },
  "is less than": {
    key: "less than",
    multiple: "is between",
    target: "single",
    value: "is less than",
  },
  "is less than or equal to": {
    key: "less than or equal",
    multiple: "is between",
    target: "single",
    value: "is less than or equal to",
  },
  "is not": {
    key: "is not",
    multiple: "is not between",
    target: "single",
    value: "is not",
  },
  "is not between": {
    key: "is not between",
    single: "is not",
    target: "multiple",
    value: "is not between",
  },
} as const satisfies FilterDetails<"number">;

export const booleanFilterOperators = {
  is: {
    key: "is",
    multiple: "is not",
    target: "single",
    value: "is",
  },
  "is not": {
    key: "is not",
    multiple: "is",
    target: "single",
    value: "is not",
  },
} as const satisfies FilterDetails<"boolean">;

export const filterTypeOperatorDetails: FilterTypeOperatorDetails = {
  boolean: booleanFilterOperators,
  date: dateFilterOperators,
  multiOption: multiOptionFilterOperators,
  number: numberFilterOperators,
  option: optionFilterOperators,
  text: textFilterOperators,
};

// Preserve negation when the value count switches between one and many.
export const determineNewOperator = <TType extends ColumnDataType>(
  type: TType,
  oldVals: FilterValues<TType>,
  nextVals: FilterValues<TType>,
  currentOperator: FilterOperators[TType],
): FilterOperators[TType] => {
  const a = oldVals.length;
  const b = nextVals.length;

  if (a === b || (a >= 2 && b >= 2) || (a <= 1 && b <= 1)) {
    return currentOperator;
  }

  const opDetails = filterTypeOperatorDetails[type][currentOperator];

  if (a < b && b >= 2) {
    return opDetails.multiple ?? currentOperator;
  }
  if (a > b && b <= 1) {
    return opDetails.single ?? currentOperator;
  }
  return currentOperator;
};
/filter-package/core/types.ts
import type { ElementType, ReactElement, Dispatch, SetStateAction } from "react";

export interface ColumnOption {
  label: string;
  value: string;
  icon?: ReactElement | ElementType;
  count?: number;
}

export interface ColumnOptionExtended extends ColumnOption {
  selected?: boolean;
}

export type ColumnDataType = "text" | "number" | "date" | "boolean" | "option" | "multiOption";
export type OptionBasedColumnDataType = "option" | "multiOption";
export type FilterStrategy = "client" | "server";

interface ColumnBase {
  id: string;
  displayName: string;
  icon?: ReactElement | ElementType;
  hidden?: boolean;
}

interface ColumnValues {
  text: string[];
  number: number[];
  date: Date[];
  boolean: boolean[];
  option: string[];
  multiOption: string[];
}

interface ColumnDetails {
  text: object;
  number: { min: number; max: number };
  date: object;
  boolean: { toggledStateName?: string };
  option: { options: ColumnOption[] };
  multiOption: { options: ColumnOption[] };
}

export type Column<K extends ColumnDataType = ColumnDataType> = {
  [Type in K]: ColumnBase & { type: Type; values: ColumnValues[Type] } & ColumnDetails[Type];
}[K];

type OptionValue = string | ColumnOption;

interface ColumnInputs<Row> {
  text: { accessor: (row: Row) => string | null | undefined };
  number: { accessor: (row: Row) => number | null | undefined; min?: number; max?: number };
  date: { accessor: (row: Row) => Date | null | undefined };
  boolean: { accessor: (row: Row) => boolean | null | undefined; toggledStateName?: string };
  option: { accessor: (row: Row) => OptionValue | null | undefined; options?: ColumnOption[] };
  multiOption: {
    accessor: (row: Row) => readonly OptionValue[] | null | undefined;
    options?: ColumnOption[];
  };
}

export type ColumnConfig<Row> = {
  [Type in ColumnDataType]: ColumnBase & { type: Type } & ColumnInputs<Row>[Type];
}[ColumnDataType];

export type TextFilterOperator = "contains" | "does not contain";

export type NumberFilterOperator =
  | "is"
  | "is not"
  | "is less than"
  | "is greater than or equal to"
  | "is greater than"
  | "is less than or equal to"
  | "is between"
  | "is not between";

export type DateFilterOperator =
  | "is"
  | "is not"
  | "is before"
  | "is on or after"
  | "is after"
  | "is on or before"
  | "is between"
  | "is not between";

export type BooleanFilterOperator = "is" | "is not";

export type OptionFilterOperator = "is" | "is not" | "is any of" | "is none of";

export type MultiOptionFilterOperator =
  | "include"
  | "exclude"
  | "include any of"
  | "include all of"
  | "exclude if any of"
  | "exclude if all";

export interface FilterOperators {
  text: TextFilterOperator;
  number: NumberFilterOperator;
  date: DateFilterOperator;
  boolean: BooleanFilterOperator;
  option: OptionFilterOperator;
  multiOption: MultiOptionFilterOperator;
}

export type FilterValues<K extends ColumnDataType> = ColumnValues[K];

export type FilterModel<K extends ColumnDataType = ColumnDataType> = {
  [Type in K]: {
    columnId: string;
    type: Type;
    operator: FilterOperators[Type];
    values: FilterValues<Type>;
  };
}[K];

export type FiltersState = FilterModel[];

export type FilterValueUpdate = {
  [Type in ColumnDataType]: Pick<FilterModel<Type>, "columnId" | "type" | "values">;
}[ColumnDataType];

export type FilterOperatorUpdate = {
  [Type in ColumnDataType]: Pick<FilterModel<Type>, "columnId" | "type" | "operator">;
}[ColumnDataType];

export interface DataTableFilterActions {
  setFilterValue: (update: FilterValueUpdate) => void;
  setFilterOperator: (update: FilterOperatorUpdate) => void;
  addFilterValue: (column: Column<OptionBasedColumnDataType>, values: string[]) => void;
  removeFilterValue: (column: Column<OptionBasedColumnDataType>, values: string[]) => void;
  removeFilter: (columnId: string) => void;
  removeAllFilters: () => void;
}

export type FilterOperatorTarget = "single" | "multiple";
export interface FilterOperatorDetails<Operator, K extends ColumnDataType> {
  key: string;
  value: Operator;
  target: FilterOperatorTarget;
  multiple?: FilterOperators[K];
  single?: FilterOperators[K];
}

export type FilterDetails<K extends ColumnDataType> = {
  [Operator in FilterOperators[K]]: FilterOperatorDetails<Operator, K>;
};
export type FilterTypeOperatorDetails = { [K in ColumnDataType]: FilterDetails<K> };

export type DataTableFiltersOptions<Row> = {
  data: Row[];
  columnsConfig: readonly ColumnConfig<Row>[];
  strategy?: FilterStrategy;
  entityName?: string;
} & (
  | {
      filters: FiltersState;
      onFiltersChange: Dispatch<SetStateAction<FiltersState>>;
      defaultFilters?: never;
    }
  | { filters?: never; onFiltersChange?: never; defaultFilters?: FiltersState }
);

export type FilterBinding = {
  [K in ColumnDataType]: { type: K; column: Column<K>; filter?: FilterModel<K> };
}[ColumnDataType];
/filter-package/hooks/use-data-table-filters.ts
"use client";

import { useMemo, useState } from "react";
import type { DataTableFilterActions, DataTableFiltersOptions, FiltersState } from "../core/types";
import { createColumns } from "../core/columns/column-factory";
import { setFilterOperator, setFilterValue, toggleFilterValues } from "../core/filters";

export const useDataTableFilters = <Row>({
  data,
  columnsConfig,
  filters: controlledFilters,
  onFiltersChange,
  defaultFilters = [],
  strategy = "client",
  entityName,
}: DataTableFiltersOptions<Row>) => {
  const [internalFilters, setInternalFilters] = useState<FiltersState>(defaultFilters);
  const filters = controlledFilters ?? internalFilters;
  const setFilters = onFiltersChange ?? setInternalFilters;
  const columns = useMemo(
    () => createColumns(data, columnsConfig, strategy),
    [data, columnsConfig, strategy],
  );
  const actions = useMemo<DataTableFilterActions>(
    () => ({
      addFilterValue: (column, values) =>
        setFilters((current) => toggleFilterValues(current, column, values, true)),
      removeAllFilters: () => setFilters([]),
      removeFilter: (columnId) =>
        setFilters((current) => current.filter((filter) => filter.columnId !== columnId)),
      removeFilterValue: (column, values) =>
        setFilters((current) => toggleFilterValues(current, column, values, false)),
      setFilterOperator: (update) => setFilters((current) => setFilterOperator(current, update)),
      setFilterValue: (update) => setFilters((current) => setFilterValue(current, update)),
    }),
    [setFilters],
  );
  return { actions, columns, entityName, filters, strategy };
};
/filter-package/index.ts
export * from "./core/types";
export * from "./core/operators";
export { useDataTableFilters } from "./hooks/use-data-table-filters";
export { getColumn, bindFilter, createNumberRange } from "./lib/helpers";
/filter-package/lib/helpers.ts
import type { Column, FilterModel, FilterBinding } from "../core/types";

export const getColumn = (columns: Column[], id: string) => {
  const column = columns.find((candidate) => candidate.id === id);
  if (!column) {
    throw new Error(`Column with id ${id} not found`);
  }
  return column;
};

export const createNumberRange = (values: number[]): number[] => {
  const [a = 0, b = 0] = values;
  return a < b ? [a, b] : [b, a];
};

export const bindFilter = (column: Column, filter?: FilterModel): FilterBinding => {
  switch (column.type) {
    case "text": {
      return { column, filter: filter?.type === "text" ? filter : undefined, type: "text" };
    }
    case "number": {
      return { column, filter: filter?.type === "number" ? filter : undefined, type: "number" };
    }
    case "date": {
      return { column, filter: filter?.type === "date" ? filter : undefined, type: "date" };
    }
    case "boolean": {
      return { column, filter: filter?.type === "boolean" ? filter : undefined, type: "boolean" };
    }
    case "option": {
      return { column, filter: filter?.type === "option" ? filter : undefined, type: "option" };
    }
    case "multiOption": {
      return {
        column,
        filter: filter?.type === "multiOption" ? filter : undefined,
        type: "multiOption",
      };
    }
    default: {
      throw new Error(`Unsupported column type: ${String(column satisfies never)}`);
    }
  }
};
/filters.tsx
import { cn } from "cn";
import {
  BuildingIcon,
  CalendarArrowUpIcon,
  DollarSignIcon,
  TargetIcon,
  UserCheckIcon,
  UserIcon,
} from "lucide-react";

import type { Person, PersonDepartment, PersonSkill } from "./types";
import type { ColumnConfig } from "./filter-package";

export const PERSON_DEPARTMENTS: PersonDepartment[] = [
  { color: "blue", id: "engineering", name: "Engineering" },
  { color: "purple", id: "design", name: "Design" },
  { color: "green", id: "product", name: "Product" },
  { color: "orange", id: "marketing", name: "Marketing" },
  { color: "red", id: "sales", name: "Sales" },
  { color: "pink", id: "hr", name: "Human Resources" },
  { color: "teal", id: "finance", name: "Finance" },
  { color: "indigo", id: "operations", name: "Operations" },
  { color: "emerald", id: "customer-success", name: "Customer Success" },
  { color: "cyan", id: "data", name: "Data & Analytics" },
  { color: "rose", id: "security", name: "Security" },
  { color: "violet", id: "legal", name: "Legal" },
] as const;

export const PERSON_SKILLS = [
  { color: "yellow", id: "javascript", level: "expert", name: "JavaScript" },
  { color: "blue", id: "typescript", level: "advanced", name: "TypeScript" },
  { color: "cyan", id: "react", level: "expert", name: "React" },
  { color: "green", id: "nodejs", level: "advanced", name: "Node.js" },
  { color: "yellow", id: "python", level: "intermediate", name: "Python" },
  { color: "red", id: "java", level: "advanced", name: "Java" },
  { color: "blue", id: "go", level: "beginner", name: "Go" },
  { color: "orange", id: "rust", level: "intermediate", name: "Rust" },
  { color: "indigo", id: "sql", level: "advanced", name: "SQL" },
  { color: "orange", id: "aws", level: "intermediate", name: "AWS" },
  { color: "blue", id: "docker", level: "advanced", name: "Docker" },
  {
    color: "blue",
    id: "kubernetes",
    level: "intermediate",
    name: "Kubernetes",
  },
  { color: "purple", id: "figma", level: "expert", name: "Figma" },
  { color: "yellow", id: "sketch", level: "advanced", name: "Sketch" },
  { color: "blue", id: "photoshop", level: "intermediate", name: "Photoshop" },
  {
    color: "orange",
    id: "illustrator",
    level: "beginner",
    name: "Illustrator",
  },
  { color: "green", id: "leadership", level: "expert", name: "Leadership" },
  {
    color: "pink",
    id: "communication",
    level: "advanced",
    name: "Communication",
  },
  {
    color: "teal",
    id: "project-management",
    level: "intermediate",
    name: "Project Management",
  },
  {
    color: "cyan",
    id: "data-analysis",
    level: "advanced",
    name: "Data Analysis",
  },
  {
    color: "purple",
    id: "machine-learning",
    level: "beginner",
    name: "Machine Learning",
  },
  { color: "rose", id: "ui-design", level: "expert", name: "UI Design" },
  {
    color: "indigo",
    id: "ux-research",
    level: "advanced",
    name: "UX Research",
  },
  {
    color: "emerald",
    id: "content-strategy",
    level: "intermediate",
    name: "Content Strategy",
  },
  { color: "lime", id: "seo", level: "beginner", name: "SEO" },
  {
    color: "pink",
    id: "social-media",
    level: "advanced",
    name: "Social Media",
  },
  {
    color: "blue",
    id: "email-marketing",
    level: "intermediate",
    name: "Email Marketing",
  },
  { color: "blue", id: "salesforce", level: "expert", name: "Salesforce" },
  { color: "orange", id: "hubspot", level: "advanced", name: "HubSpot" },
  {
    color: "green",
    id: "recruiting",
    level: "intermediate",
    name: "Recruiting",
  },
  {
    color: "purple",
    id: "training",
    level: "beginner",
    name: "Training & Development",
  },
  { color: "teal", id: "accounting", level: "advanced", name: "Accounting" },
  {
    color: "indigo",
    id: "budgeting",
    level: "intermediate",
    name: "Budgeting",
  },
  { color: "red", id: "compliance", level: "expert", name: "Compliance" },
  {
    color: "orange",
    id: "risk-management",
    level: "advanced",
    name: "Risk Management",
  },
  {
    color: "green",
    id: "customer-support",
    level: "intermediate",
    name: "Customer Support",
  },
  {
    color: "blue",
    id: "technical-writing",
    level: "beginner",
    name: "Technical Writing",
  },
  {
    color: "purple",
    id: "public-speaking",
    level: "advanced",
    name: "Public Speaking",
  },
  {
    color: "pink",
    id: "negotiation",
    level: "intermediate",
    name: "Negotiation",
  },
  {
    color: "emerald",
    id: "strategic-planning",
    level: "expert",
    name: "Strategic Planning",
  },
  {
    color: "cyan",
    id: "change-management",
    level: "advanced",
    name: "Change Management",
  },
  {
    color: "lime",
    id: "team-building",
    level: "intermediate",
    name: "Team Building",
  },
  { color: "rose", id: "mentoring", level: "beginner", name: "Mentoring" },
  {
    color: "violet",
    id: "conflict-resolution",
    level: "advanced",
    name: "Conflict Resolution",
  },
  {
    color: "amber",
    id: "time-management",
    level: "expert",
    name: "Time Management",
  },
  {
    color: "sky",
    id: "problem-solving",
    level: "expert",
    name: "Problem Solving",
  },
  {
    color: "indigo",
    id: "critical-thinking",
    level: "advanced",
    name: "Critical Thinking",
  },
  {
    color: "fuchsia",
    id: "creativity",
    level: "intermediate",
    name: "Creativity",
  },
  { color: "teal", id: "adaptability", level: "expert", name: "Adaptability" },
  {
    color: "pink",
    id: "emotional-intelligence",
    level: "advanced",
    name: "Emotional Intelligence",
  },
  {
    color: "green",
    id: "cross-cultural",
    level: "intermediate",
    name: "Cross-cultural Communication",
  },
  {
    color: "blue",
    id: "multilingual",
    level: "beginner",
    name: "Multilingual",
  },
] as const satisfies readonly PersonSkill[];

export const LABEL_STYLES_BG = {
  amber: "bg-amber-500",
  blue: "bg-blue-500",
  cyan: "bg-cyan-500",
  emerald: "bg-emerald-500",
  fuchsia: "bg-fuchsia-500",
  green: "bg-green-500",
  indigo: "bg-indigo-500",
  lime: "bg-lime-500",
  neutral: "bg-neutral-500",
  orange: "bg-orange-500",
  pink: "bg-pink-500",
  purple: "bg-purple-500",
  red: "bg-red-500",
  rose: "bg-rose-500",
  sky: "bg-sky-500",
  teal: "bg-teal-500",
  violet: "bg-violet-500",
  yellow: "bg-yellow-500",
} as const;

export type TW_COLOR = keyof typeof LABEL_STYLES_BG;

export const columnsConfig: readonly ColumnConfig<Person>[] = [
  {
    accessor: (row) => `${row.firstName} ${row.lastName}`,
    displayName: "Name",
    icon: UserIcon,
    id: "name",
    type: "text",
  },
  {
    accessor: (row) => row.department.id,
    displayName: "Department",
    icon: BuildingIcon,
    id: "department",
    options: PERSON_DEPARTMENTS.map((department) => ({
      icon: BuildingIcon,
      label: department.name,
      value: department.id,
    })),
    type: "option",
  },
  {
    accessor: (row) => row.salary,
    displayName: "Salary",
    icon: DollarSignIcon,
    id: "salary",
    max: 300_000,
    min: 20_000,
    type: "number",
  },
  {
    accessor: (row) => row.skills?.map((skill) => skill.id),
    displayName: "Skills",
    icon: TargetIcon,
    id: "skills",
    options: PERSON_SKILLS.map((skill) => ({
      icon: <div className={cn("size-2.5 rounded-full", LABEL_STYLES_BG[skill.color])} />,
      label: skill.name,
      value: skill.id,
    })),
    type: "multiOption",
  },
  {
    accessor: (row) => row.startDate,
    displayName: "Start Date",
    icon: CalendarArrowUpIcon,
    id: "startDate",
    type: "date",
  },
  {
    accessor: (row) => row.isActive,
    displayName: "Active employees",
    icon: UserCheckIcon,
    id: "isActive",
    toggledStateName: "active",
    type: "boolean",
  },
];
/preview.tsx
"use client";

import { useState } from "react";

import type { Person } from "./types";
import type { FiltersState } from "./filter-package/core/types";
import { DataTableFilter } from "./_components/data-table-filter";
import { useDataTableFilters } from "./filter-package";
import { columnsConfig } from "./filters";

const people: Person[] = [];

const Preview = () => {
  const [filtersState, setFiltersState] = useState<FiltersState>([]);

  const { columns, filters, actions, strategy, entityName } = useDataTableFilters({
    columnsConfig,
    data: people,
    entityName: "Person",
    filters: filtersState,
    onFiltersChange: setFiltersState,
    strategy: "client",
  });

  return (
    <div className="flex h-screen items-center bg-stone-100 p-10 dark:bg-stone-900">
      <DataTableFilter
        filters={filters}
        columns={columns}
        actions={actions}
        strategy={strategy}
        entityName={entityName}
      />
    </div>
  );
};

export default Preview;
/styles.d.ts
declare module "*.css" {}
/types.ts
import type { LucideIcon } from "lucide-react";

export interface Person {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  phone?: string;
  age: number;
  gender: PersonGender;
  department: PersonDepartment;
  position: string;
  salary: number;
  startDate: Date;
  endDate?: Date;
  isActive: boolean;
  skills?: PersonSkill[];
  location: PersonLocation;
  manager?: Person;
  isRemote: boolean;
  experience: number;
  education: PersonEducation;
  performance: PersonPerformance;
}

export interface PersonGender {
  id: "male" | "female" | "non-binary" | "other";
  name: string;
  order: number;
  icon: LucideIcon;
}

export interface PersonDepartment {
  id: string;
  name: string;
  color: string;
}

export interface PersonSkill {
  id: string;
  name: string;
  level: "beginner" | "intermediate" | "advanced" | "expert";
  color: string;
}

export interface PersonLocation {
  city: string;
  state: string;
  country: string;
  timezone: string;
}

export interface PersonEducation {
  degree: string;
  field: string;
  institution: string;
  graduationYear: number;
}

export interface PersonPerformance {
  // 1-5
  rating: number;
  lastReview: Date;
  goals: string[];
}

More from uicapsule

Browse all components in this library →