React Example: Kitchen Sink Mantine

'use client'

import * as React from 'react'
import { TanStackDevtools } from '@tanstack/react-devtools'
import * as ReactDOM from 'react-dom/client'
import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer'
import {
  DndContext,
  PointerSensor,
  closestCenter,
  useSensor,
  useSensors,
} from '@dnd-kit/core'
import {
  SortableContext,
  arrayMove,
  useSortable,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import {
  ActionIcon,
  Badge,
  Box,
  Button,
  Checkbox,
  Container,
  Group,
  Pagination as MantinePagination,
  MantineProvider,
  Table as MantineTable,
  Menu,
  MultiSelect,
  Paper,
  Popover,
  Progress,
  Select,
  Stack,
  Text,
  TextInput,
  Tooltip,
  UnstyledButton,
  useComputedColorScheme,
  useMantineColorScheme,
} from '@mantine/core'
import '@mantine/core/styles.css'
import {
  IconArrowDown,
  IconArrowUp,
  IconArrowsSort,
  IconBriefcase,
  IconBuildingStore,
  IconCategory,
  IconCheck,
  IconChevronDown,
  IconChevronLeft,
  IconChevronRight,
  IconChevronsLeft,
  IconChevronsRight,
  IconCode,
  IconCreditCard,
  IconDeviceDesktop,
  IconDotsVertical,
  IconEyeOff,
  IconFilter,
  IconGripVertical,
  IconMoon,
  IconPinned,
  IconSearch,
  IconSettings,
  IconSun,
  IconTrash,
  IconUsersGroup,
} from '@tabler/icons-react'
import {
  aggregationFns,
  columnFacetingFeature,
  columnFilteringFeature,
  columnGroupingFeature,
  columnOrderingFeature,
  columnPinningFeature,
  columnResizingFeature,
  columnSizingFeature,
  columnVisibilityFeature,
  createColumnHelper,
  createExpandedRowModel,
  createFacetedRowModel,
  createFacetedUniqueValues,
  createFilteredRowModel,
  createGroupedRowModel,
  createPaginatedRowModel,
  createSortedRowModel,
  filterFns,
  globalFilteringFeature,
  metaHelper,
  rowExpandingFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowSortingFeature,
  sortFns,
  tableFeatures,
  useTable,
} from '@tanstack/react-table'
import {
  tableDevtoolsPlugin,
  useTanStackTableDevtools,
} from '@tanstack/react-table-devtools'
import type { Person } from '@/lib/make-data'
import type { DragEndEvent } from '@dnd-kit/core'
import type {
  Column,
  ColumnPinningState,
  ColumnSizingState,
  ExpandedState,
  GroupingState,
  Header,
  ReactTable,
  SortingState,
} from '@tanstack/react-table'
import type { ExtendedColumnFilter } from '@/types'

import { dynamicFilterFn, getFilterOperators } from '@/lib/data-table'
import { rankItem } from '@tanstack/match-sorter-utils'
import { departments, makeData, statuses } from '@/lib/make-data'
import './styles/globals.css'

interface MyColumnMeta {
  label?: string
  variant?: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi-select'
  options?: Array<{ label: string; value: string; count?: number }>
}

// Local fuzzy filter implementation for the filterFns registry slot.
// Defined here to avoid a circular type dependency with data-table.ts.
const fuzzyFilterFn = (
  row: { getValue: (id: string) => unknown },
  columnId: string,
  value: unknown,
  addMeta?: (meta: object) => void,
) => {
  const itemRank = rankItem(row.getValue(columnId), value as string)
  addMeta?.({ itemRank })
  return itemRank.passed
}

export const features = tableFeatures({
  rowSortingFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowExpandingFeature,
  columnFilteringFeature,
  columnFacetingFeature,
  columnOrderingFeature,
  columnVisibilityFeature,
  columnSizingFeature,
  columnResizingFeature,
  columnPinningFeature,
  columnGroupingFeature,
  globalFilteringFeature,
  columnMeta: metaHelper<MyColumnMeta>(),
  filteredRowModel: createFilteredRowModel(),
  facetedRowModel: createFacetedRowModel(),
  facetedUniqueValues: createFacetedUniqueValues(),
  paginatedRowModel: createPaginatedRowModel(),
  sortedRowModel: createSortedRowModel(),
  groupedRowModel: createGroupedRowModel(),
  expandedRowModel: createExpandedRowModel(),
  filterFns: { ...filterFns, fuzzy: fuzzyFilterFn },
  sortFns,
  aggregationFns,
})

const columnHelper = createColumnHelper<typeof features, Person>()
type AppTable = ReactTable<typeof features, Person>
type AppColumn = Column<typeof features, Person, any>

function SortableFrame({
  id,
  children,
}: {
  id: string
  children: React.ReactNode
}) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id })

  return (
    <Box
      ref={setNodeRef}
      {...attributes}
      {...listeners}
      style={{
        opacity: isDragging ? 0.6 : 1,
        transform: CSS.Transform.toString(transform),
        transition,
        cursor: 'grab',
      }}
    >
      {children}
    </Box>
  )
}

function toSentenceCase(value: string) {
  return value
    .replace(/[-_]/g, ' ')
    .replace(/\w\S*/g, (word) => word[0].toUpperCase() + word.slice(1))
}

function formatDate(value: string) {
  return new Intl.DateTimeFormat('en-US', {
    month: 'short',
    day: 'numeric',
    year: 'numeric',
  }).format(new Date(value))
}

function toDateInputValue(value: unknown) {
  if (!value) return ''
  const date = new Date(String(value))
  return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)
}

function getAriaSort(sortDirection: false | 'asc' | 'desc') {
  if (sortDirection === 'asc') return 'ascending'
  if (sortDirection === 'desc') return 'descending'
  return 'none'
}

const SortingContext = React.createContext<SortingState>([])

function getSortDirection(sorting: SortingState, columnId: string) {
  const sort = sorting.find((sort) => sort.id === columnId)
  return sort ? (sort.desc ? 'desc' : 'asc') : undefined
}

function getCommonPinningStyles(
  column: AppColumn,
  isSelected = false,
): React.CSSProperties {
  const isPinned = column.getIsPinned()
  const isLastLeftPinnedColumn =
    isPinned === 'left' && column.getIsLastColumn('left')
  const isFirstRightPinnedColumn =
    isPinned === 'right' && column.getIsFirstColumn('right')

  return {
    boxShadow: isLastLeftPinnedColumn
      ? '-4px 0 4px -4px var(--mantine-color-default-border) inset'
      : isFirstRightPinnedColumn
        ? '4px 0 4px -4px var(--mantine-color-default-border) inset'
        : undefined,
    left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
    right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
    position: isPinned ? 'sticky' : 'relative',
    borderRight: isLastLeftPinnedColumn
      ? '1px solid var(--mantine-color-default-border)'
      : undefined,
    borderLeft: isFirstRightPinnedColumn
      ? '1px solid var(--mantine-color-default-border)'
      : undefined,
    background: isSelected
      ? 'var(--mantine-color-blue-light)'
      : isPinned
        ? 'var(--mantine-color-body)'
        : undefined,
    zIndex: isPinned ? 2 : 0,
  }
}

function DepartmentIcon({ department }: { department: Person['department'] }) {
  const icons: Record<Person['department'], React.ReactElement> = {
    engineering: <IconCode size={16} />,
    marketing: <IconBriefcase size={16} />,
    sales: <IconBuildingStore size={16} />,
    hr: <IconUsersGroup size={16} />,
    finance: <IconCreditCard size={16} />,
  }

  return icons[department]
}

function DepartmentPill({ department }: { department: Person['department'] }) {
  return (
    <Box
      component="span"
      style={{
        display: 'inline-flex',
        maxWidth: '100%',
        height: 24,
        minWidth: 0,
        alignItems: 'center',
        gap: 6,
        paddingInline: 10,
        borderRadius: 999,
        border: '1px solid var(--mantine-color-default-border)',
        fontSize: 'var(--mantine-font-size-sm)',
      }}
    >
      <Box
        component="span"
        style={{
          display: 'inline-flex',
          width: 16,
          height: 16,
          flex: '0 0 16px',
          alignItems: 'center',
          justifyContent: 'center',
          overflow: 'hidden',
        }}
      >
        <DepartmentIcon department={department} />
      </Box>
      <Box
        component="span"
        style={{
          minWidth: 0,
          overflow: 'hidden',
          textOverflow: 'ellipsis',
          whiteSpace: 'nowrap',
        }}
      >
        {toSentenceCase(department)}
      </Box>
    </Box>
  )
}

function EllipsisText({ children }: { children: React.ReactNode }) {
  return (
    <Box
      component="span"
      style={{
        display: 'block',
        minWidth: 0,
        overflow: 'hidden',
        textOverflow: 'ellipsis',
        whiteSpace: 'nowrap',
      }}
    >
      {children}
    </Box>
  )
}

function StatusBadge({ status }: { status: Person['status'] }) {
  const color: Record<Person['status'], string> = {
    active: 'green',
    inactive: 'red',
    pending: 'yellow',
  }

  return (
    <Badge
      color={color[status]}
      variant="light"
      leftSection={<IconCheck size={14} />}
    >
      {toSentenceCase(status)}
    </Badge>
  )
}

function RowActions({ person }: { person: Person }) {
  return (
    <Menu shadow="md" width={180}>
      <Menu.Target>
        <ActionIcon variant="subtle" aria-label="Open row actions">
          <IconDotsVertical size={18} />
        </ActionIcon>
      </Menu.Target>
      <Menu.Dropdown>
        <Menu.Item
          onClick={() => {
            void navigator.clipboard.writeText(person.id)
          }}
        >
          Copy ID
        </Menu.Item>
        <Menu.Divider />
        <Menu.Item>View details</Menu.Item>
        <Menu.Item>View profile</Menu.Item>
      </Menu.Dropdown>
    </Menu>
  )
}

function SortIcon({ direction }: { direction: 'asc' | 'desc' | undefined }) {
  if (direction === 'asc') return <IconArrowUp size={16} />
  if (direction === 'desc') return <IconArrowDown size={16} />
  return <IconArrowsSort className="sort-icon-unsorted" size={16} />
}

function ColumnHeaderMenu({
  column,
  title,
}: {
  column: AppColumn
  title: string
}) {
  const canSort = column.getCanSort()
  const canHide = column.getCanHide()
  const canPin = column.getCanPin()
  const canGroup = column.getCanGroup()
  const sorting = React.useContext(SortingContext)
  const direction = canSort ? getSortDirection(sorting, column.id) : undefined
  const pinned = canPin ? column.getIsPinned() : false
  const grouped = canGroup ? column.getIsGrouped() : false

  if (!canSort && !canHide && !canPin && !canGroup) {
    return <Text fw={600}>{title}</Text>
  }

  return (
    <Group gap={4} wrap="nowrap">
      {canSort ? (
        <UnstyledButton
          onClick={column.getToggleSortingHandler()}
          className="sort-trigger"
          style={{ minWidth: 0 }}
        >
          <Group gap={4} wrap="nowrap">
            <Text fw={600} truncate>
              {title}
            </Text>
            <SortIcon direction={direction} />
          </Group>
        </UnstyledButton>
      ) : (
        <Text fw={600}>{title}</Text>
      )}
      <Menu shadow="md" width={180}>
        <Menu.Target>
          <ActionIcon
            variant="subtle"
            size="sm"
            aria-label={`Open ${title} column menu`}
          >
            <IconChevronDown size={16} />
          </ActionIcon>
        </Menu.Target>
        <Menu.Dropdown>
          {canSort ? (
            <>
              <Menu.Item
                leftSection={<IconArrowUp size={16} />}
                onClick={() => column.toggleSorting(false)}
              >
                Asc
              </Menu.Item>
              <Menu.Item
                leftSection={<IconArrowDown size={16} />}
                onClick={() => column.toggleSorting(true)}
              >
                Desc
              </Menu.Item>
            </>
          ) : null}
          {canGroup ? (
            <Menu.Item
              leftSection={<IconCategory size={16} />}
              onClick={column.getToggleGroupingHandler()}
            >
              {grouped ? 'Ungroup' : 'Group by'}
            </Menu.Item>
          ) : null}
          {canPin ? (
            <>
              <Menu.Divider />
              <Menu.Item
                disabled={pinned === 'left'}
                leftSection={<IconPinned size={16} />}
                onClick={() => column.pin('left')}
              >
                Pin left
              </Menu.Item>
              <Menu.Item
                disabled={pinned === 'right'}
                leftSection={<IconPinned size={16} />}
                onClick={() => column.pin('right')}
              >
                Pin right
              </Menu.Item>
              {pinned ? (
                <Menu.Item
                  leftSection={<IconPinned size={16} opacity={0.45} />}
                  onClick={() => column.pin(false)}
                >
                  Unpin
                </Menu.Item>
              ) : null}
            </>
          ) : null}
          {canHide ? (
            <>
              <Menu.Divider />
              <Menu.Item
                leftSection={<IconEyeOff size={16} />}
                onClick={() => column.toggleVisibility(false)}
              >
                Hide
              </Menu.Item>
            </>
          ) : null}
        </Menu.Dropdown>
      </Menu>
    </Group>
  )
}

function ViewOptionsPopover({
  table,
  columnOrder,
  onColumnOrderChange,
}: {
  table: AppTable
  columnOrder: Array<string>
  onColumnOrderChange: React.Dispatch<React.SetStateAction<Array<string>>>
}) {
  const [opened, setOpened] = React.useState(false)
  const [query, setQuery] = React.useState('')
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
  )
  const columns = table
    .getAllColumns()
    .filter((column) => typeof column.accessorFn !== 'undefined')
    .sort((a, b) => columnOrder.indexOf(a.id) - columnOrder.indexOf(b.id))
    .filter((column) =>
      (column.columnDef.meta?.label ?? column.id)
        .toLowerCase()
        .includes(query.toLowerCase()),
    )

  const onDragEnd = (event: DragEndEvent) => {
    const { active, over } = event
    if (!over || active.id === over.id) return

    onColumnOrderChange((current) => {
      const oldIndex = current.indexOf(String(active.id))
      const newIndex = current.indexOf(String(over.id))
      return oldIndex >= 0 && newIndex >= 0
        ? arrayMove(current, oldIndex, newIndex)
        : current
    })
  }

  return (
    <Popover
      opened={opened}
      onChange={setOpened}
      position="bottom-end"
      shadow="md"
    >
      <Popover.Target>
        <Button
          variant="outline"
          size="sm"
          leftSection={<IconSettings size={16} />}
          onClick={() => setOpened((value) => !value)}
        >
          View
        </Button>
      </Popover.Target>
      <Popover.Dropdown w={320}>
        <Stack gap="sm">
          <TextInput
            label="Search columns"
            value={query}
            onChange={(event) => setQuery(event.currentTarget.value)}
          />
          <DndContext
            sensors={sensors}
            collisionDetection={closestCenter}
            onDragEnd={onDragEnd}
          >
            <SortableContext
              items={columns.map((column) => column.id)}
              strategy={verticalListSortingStrategy}
            >
              <Stack gap={4}>
                {columns.map((column) => (
                  <SortableFrame key={column.id} id={column.id}>
                    <Group justify="space-between" wrap="nowrap">
                      <Checkbox
                        checked={column.getIsVisible()}
                        label={column.columnDef.meta?.label ?? column.id}
                        onChange={() =>
                          column.toggleVisibility(!column.getIsVisible())
                        }
                      />
                      <IconGripVertical size={16} opacity={0.45} />
                    </Group>
                  </SortableFrame>
                ))}
              </Stack>
            </SortableContext>
          </DndContext>
        </Stack>
      </Popover.Dropdown>
    </Popover>
  )
}

function SortListPopover({
  table,
  sorting,
  onSortingChange,
}: {
  table: AppTable
  sorting: SortingState
  onSortingChange: React.Dispatch<React.SetStateAction<SortingState>>
}) {
  const [opened, setOpened] = React.useState(false)
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
  )
  const sortableColumns = table
    .getAllColumns()
    .filter((column) => column.getCanSort())
  const columnOptions = sortableColumns.map((column) => ({
    value: column.id,
    label: column.columnDef.meta?.label ?? column.id,
  }))

  const updateSort = (index: number, patch: Partial<SortingState[number]>) => {
    onSortingChange((current) =>
      current.map((sort, sortIndex) =>
        sortIndex === index ? { ...sort, ...patch } : sort,
      ),
    )
  }

  const addSort = () => {
    const nextColumn = sortableColumns.find(
      (column) => !sorting.some((sort) => sort.id === column.id),
    )
    if (nextColumn)
      onSortingChange((current) => [
        ...current,
        { id: nextColumn.id, desc: false },
      ])
  }

  const onDragEnd = (event: DragEndEvent) => {
    const { active, over } = event
    if (!over || active.id === over.id) return

    onSortingChange((current) => {
      const oldIndex = current.findIndex((sort) => sort.id === active.id)
      const newIndex = current.findIndex((sort) => sort.id === over.id)
      return oldIndex >= 0 && newIndex >= 0
        ? arrayMove(current, oldIndex, newIndex)
        : current
    })
  }

  return (
    <Popover opened={opened} onChange={setOpened} width={520} shadow="md">
      <Popover.Target>
        <Button
          variant="outline"
          size="sm"
          leftSection={<IconArrowsSort size={16} />}
          rightSection={
            sorting.length ? <Badge size="sm">{sorting.length}</Badge> : null
          }
          onClick={() => setOpened((value) => !value)}
        >
          Sort
        </Button>
      </Popover.Target>
      <Popover.Dropdown>
        <Stack gap="md">
          <Text fw={600}>
            {sorting.length ? 'Sort by' : 'No sorting applied'}
          </Text>
          <DndContext
            sensors={sensors}
            collisionDetection={closestCenter}
            onDragEnd={onDragEnd}
          >
            <SortableContext
              items={sorting.map((sort) => sort.id)}
              strategy={verticalListSortingStrategy}
            >
              <Stack gap="xs">
                {sorting.map((sort, index) => (
                  <SortableFrame key={sort.id} id={sort.id}>
                    <Group wrap="nowrap" align="flex-end">
                      <IconGripVertical size={18} opacity={0.45} />
                      <Select
                        label="Column"
                        searchable
                        data={columnOptions}
                        value={sort.id}
                        onChange={(value) => {
                          if (value) updateSort(index, { id: value })
                        }}
                        style={{ flex: 1 }}
                      />
                      <Select
                        label="Direction"
                        data={[
                          { value: 'asc', label: 'Asc' },
                          { value: 'desc', label: 'Desc' },
                        ]}
                        value={sort.desc ? 'desc' : 'asc'}
                        onChange={(value) =>
                          updateSort(index, { desc: value === 'desc' })
                        }
                        w={110}
                      />
                      <ActionIcon
                        variant="subtle"
                        color="red"
                        aria-label="Remove sort"
                        onClick={() =>
                          onSortingChange((current) =>
                            current.filter(
                              (_, sortIndex) => sortIndex !== index,
                            ),
                          )
                        }
                      >
                        <IconTrash size={16} />
                      </ActionIcon>
                    </Group>
                  </SortableFrame>
                ))}
              </Stack>
            </SortableContext>
          </DndContext>
          <Group>
            <Button
              size="sm"
              onClick={addSort}
              disabled={sorting.length >= sortableColumns.length}
            >
              Add sort
            </Button>
            <Button
              size="sm"
              variant="subtle"
              onClick={() => table.resetSorting()}
            >
              Reset
            </Button>
          </Group>
        </Stack>
      </Popover.Dropdown>
    </Popover>
  )
}

function FilterValueInput({
  column,
  filter,
  onFilterUpdate,
}: {
  column: AppColumn
  filter: ExtendedColumnFilter
  onFilterUpdate: (
    filterId: string,
    patch: Partial<ExtendedColumnFilter>,
  ) => void
}) {
  if (!filter.filterId) return null
  const variant = column.columnDef.meta?.variant ?? 'text'
  const operator = filter.operator ?? 'includesString'
  const disabled = operator === 'isEmpty' || operator === 'isNotEmpty'

  if (disabled) {
    return <Text c="dimmed">No value required</Text>
  }

  if (variant === 'select') {
    const options = column.columnDef.meta?.options ?? []
    return (
      <Select
        label="Value"
        data={options}
        value={typeof filter.value === 'string' ? filter.value : null}
        onChange={(value) => onFilterUpdate(filter.filterId!, { value })}
      />
    )
  }

  if (variant === 'multi-select') {
    const options = column.columnDef.meta?.options ?? []
    return (
      <MultiSelect
        label="Value"
        data={options}
        value={Array.isArray(filter.value) ? filter.value : []}
        onChange={(value) => onFilterUpdate(filter.filterId!, { value })}
      />
    )
  }

  if (variant === 'date') {
    if (operator === 'inRange') {
      const value = Array.isArray(filter.value) ? filter.value : []
      return (
        <Group grow>
          <TextInput
            label="From"
            type="date"
            value={toDateInputValue(value[0])}
            onChange={(event) =>
              onFilterUpdate(filter.filterId!, {
                value: [
                  event.currentTarget.value
                    ? new Date(event.currentTarget.value).toISOString()
                    : undefined,
                  value[1],
                ],
              })
            }
          />
          <TextInput
            label="To"
            type="date"
            value={toDateInputValue(value[1])}
            onChange={(event) =>
              onFilterUpdate(filter.filterId!, {
                value: [
                  value[0],
                  event.currentTarget.value
                    ? new Date(event.currentTarget.value).toISOString()
                    : undefined,
                ],
              })
            }
          />
        </Group>
      )
    }

    return (
      <TextInput
        label="Value"
        type="date"
        value={toDateInputValue(filter.value)}
        onChange={(event) =>
          onFilterUpdate(filter.filterId!, {
            value: event.currentTarget.value
              ? new Date(event.currentTarget.value).toISOString()
              : undefined,
          })
        }
      />
    )
  }

  if (variant === 'number') {
    return (
      <TextInput
        label="Value"
        type="number"
        value={
          typeof filter.value === 'number' || typeof filter.value === 'string'
            ? filter.value
            : ''
        }
        onChange={(event) =>
          onFilterUpdate(filter.filterId!, {
            value:
              event.currentTarget.value === ''
                ? ''
                : Number(event.currentTarget.value),
          })
        }
      />
    )
  }

  return (
    <TextInput
      label="Value"
      value={typeof filter.value === 'string' ? filter.value : ''}
      onChange={(event) =>
        onFilterUpdate(filter.filterId!, { value: event.currentTarget.value })
      }
    />
  )
}

function FilterListPopover({
  table,
  columnFilters,
  onColumnFiltersChange,
}: {
  table: AppTable
  columnFilters: Array<ExtendedColumnFilter>
  onColumnFiltersChange: React.Dispatch<
    React.SetStateAction<Array<ExtendedColumnFilter>>
  >
}) {
  const [opened, setOpened] = React.useState(false)
  const filterableColumns = table
    .getAllColumns()
    .filter((column) => column.getCanFilter())
  const fieldOptions = filterableColumns.map((column) => ({
    value: column.id,
    label: column.columnDef.meta?.label ?? column.id,
  }))

  const updateFilter = (
    filterId: string,
    patch: Partial<ExtendedColumnFilter>,
  ) => {
    onColumnFiltersChange((current) =>
      current.map((filter) =>
        filter.filterId === filterId ? { ...filter, ...patch } : filter,
      ),
    )
  }

  const addFilter = () => {
    if (filterableColumns.length === 0) return
    const [column] = filterableColumns
    onColumnFiltersChange((current) => [
      ...current,
      {
        id: column.id,
        filterId: crypto.randomUUID(),
        value: '',
        operator: 'includesString',
        joinOperator: current[0]?.joinOperator ?? 'and',
      },
    ])
  }

  return (
    <Popover opened={opened} onChange={setOpened} width={760} shadow="md">
      <Popover.Target>
        <Button
          variant="outline"
          size="sm"
          leftSection={<IconFilter size={16} />}
          rightSection={
            columnFilters.length ? (
              <Badge size="sm">{columnFilters.length}</Badge>
            ) : null
          }
          onClick={() => setOpened((value) => !value)}
        >
          Filter
        </Button>
      </Popover.Target>
      <Popover.Dropdown>
        <Stack gap="md">
          <Text fw={600}>Filters</Text>
          {columnFilters.map((filter, index) => {
            const column = table.getColumn(filter.id)
            if (!column || !filter.filterId) return null
            const variant = column.columnDef.meta?.variant ?? 'text'
            const operators = getFilterOperators(variant)
            return (
              <Group key={filter.filterId} align="flex-end" wrap="nowrap">
                {index === 0 ? (
                  <Text w={70} pb={8}>
                    Where
                  </Text>
                ) : index === 1 ? (
                  <Select
                    data={[
                      { value: 'and', label: 'and' },
                      { value: 'or', label: 'or' },
                    ]}
                    value={filter.joinOperator ?? 'and'}
                    onChange={(joinOperator) => {
                      if (!joinOperator) return
                      onColumnFiltersChange((current) =>
                        current.map((item) => ({ ...item, joinOperator })),
                      )
                    }}
                    w={90}
                  />
                ) : (
                  <Text w={70} pb={8}>
                    {filter.joinOperator ?? 'and'}
                  </Text>
                )}
                <Select
                  label="Field"
                  searchable
                  data={fieldOptions}
                  value={column.id}
                  onChange={(nextColumnId) => {
                    const nextColumn = nextColumnId
                      ? table.getColumn(nextColumnId)
                      : undefined
                    if (nextColumn) {
                      updateFilter(filter.filterId!, {
                        id: nextColumn.id,
                        operator: getFilterOperators(
                          nextColumn.columnDef.meta?.variant ?? 'text',
                        )[0].value,
                        value: '',
                      })
                    }
                  }}
                  w={190}
                />
                <Select
                  label="Operator"
                  data={operators.map((operator) => ({
                    value: operator.value,
                    label: operator.label,
                  }))}
                  value={filter.operator ?? operators[0].value}
                  onChange={(operator) => {
                    if (!operator) return
                    updateFilter(filter.filterId!, {
                      operator,
                      value: '',
                    })
                  }}
                  w={180}
                />
                <Box style={{ flex: 1 }}>
                  <FilterValueInput
                    column={column}
                    filter={filter}
                    onFilterUpdate={updateFilter}
                  />
                </Box>
                <ActionIcon
                  variant="subtle"
                  color="red"
                  aria-label="Remove filter"
                  onClick={() =>
                    onColumnFiltersChange((current) =>
                      current.filter(
                        (item) => item.filterId !== filter.filterId,
                      ),
                    )
                  }
                >
                  <IconTrash size={16} />
                </ActionIcon>
              </Group>
            )
          })}
          <Group>
            <Button size="sm" onClick={addFilter}>
              Add filter
            </Button>
            <Button
              size="sm"
              variant="subtle"
              onClick={() => onColumnFiltersChange([])}
            >
              Reset
            </Button>
          </Group>
        </Stack>
      </Popover.Dropdown>
    </Popover>
  )
}

function Pagination({ table }: { table: AppTable }) {
  const pageIndex = table.state.pagination.pageIndex
  const pageSize = table.state.pagination.pageSize

  return (
    <Group justify="space-between" p="sm">
      <Text size="sm" c="dimmed">
        {table.getFilteredSelectedRowModel().rows.length.toLocaleString()} of{' '}
        {table.getFilteredRowModel().rows.length.toLocaleString()} row(s)
        selected.
      </Text>
      <Group gap="xs">
        <Text size="sm">Rows per page:</Text>
        <Select
          aria-label="Rows per page"
          data={['10', '20', '30', '40', '50']}
          value={String(pageSize)}
          onChange={(value) => {
            table.setPageSize(Number(value))
            table.setPageIndex(0)
          }}
          w={90}
        />
        <ActionIcon
          variant="subtle"
          aria-label="First page"
          onClick={() => table.setPageIndex(0)}
          disabled={!table.getCanPreviousPage()}
        >
          <IconChevronsLeft size={18} />
        </ActionIcon>
        <ActionIcon
          variant="subtle"
          aria-label="Previous page"
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
        >
          <IconChevronLeft size={18} />
        </ActionIcon>
        <MantinePagination
          value={pageIndex + 1}
          total={table.getPageCount()}
          onChange={(page) => table.setPageIndex(page - 1)}
          withEdges={false}
          siblings={1}
          boundaries={1}
        />
        <ActionIcon
          variant="subtle"
          aria-label="Next page"
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
        >
          <IconChevronRight size={18} />
        </ActionIcon>
        <ActionIcon
          variant="subtle"
          aria-label="Last page"
          onClick={() => table.setPageIndex(table.getPageCount() - 1)}
          disabled={!table.getCanNextPage()}
        >
          <IconChevronsRight size={18} />
        </ActionIcon>
      </Group>
    </Group>
  )
}

function ModeMenu() {
  const { colorScheme, setColorScheme } = useMantineColorScheme()
  const computedColorScheme = useComputedColorScheme('light')
  const icon =
    computedColorScheme === 'dark' ? (
      <IconMoon size={18} />
    ) : (
      <IconSun size={18} />
    )

  return (
    <Menu shadow="md" width={150}>
      <Menu.Target>
        <Tooltip label="Theme">
          <ActionIcon variant="subtle" aria-label="Theme">
            {icon}
          </ActionIcon>
        </Tooltip>
      </Menu.Target>
      <Menu.Dropdown>
        {(
          [
            { value: 'light', label: 'Light', icon: <IconSun size={16} /> },
            { value: 'dark', label: 'Dark', icon: <IconMoon size={16} /> },
            {
              value: 'auto',
              label: 'Auto',
              icon: <IconDeviceDesktop size={16} />,
            },
          ] satisfies Array<{
            value: 'light' | 'dark' | 'auto'
            label: string
            icon: React.ReactNode
          }>
        ).map((item) => (
          <Menu.Item
            key={item.value}
            leftSection={item.icon}
            color={colorScheme === item.value ? 'blue' : undefined}
            onClick={() => setColorScheme(item.value)}
          >
            {item.label}
          </Menu.Item>
        ))}
      </Menu.Dropdown>
    </Menu>
  )
}

function DebouncedTextInput({
  value: initialValue,
  onChange,
  debounce = 300,
  ...props
}: {
  value: string | number
  onChange: (value: string | number) => void
  debounce?: number
} & Omit<React.ComponentProps<typeof TextInput>, 'onChange'>) {
  const [value, setValue] = React.useState(initialValue)

  React.useEffect(() => {
    setValue(initialValue)
  }, [initialValue])

  const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce })

  return (
    <TextInput
      {...props}
      value={value}
      onChange={(event) => {
        setValue(event.currentTarget.value)
        debouncedOnChange(event.currentTarget.value)
      }}
    />
  )
}

function App() {
  const rerender = React.useReducer(() => ({}), {})[1]
  const [rowSelection, setRowSelection] = React.useState({})
  const [sorting, setSorting] = React.useState<SortingState>([])
  const [columnFilters, setColumnFilters] = React.useState<
    Array<ExtendedColumnFilter>
  >([])
  const [columnVisibility, setColumnVisibility] = React.useState({})
  const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({})
  const [globalFilter, setGlobalFilter] = React.useState('')
  const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({
    left: ['select'],
    right: ['actions'],
  })
  const [grouping, setGrouping] = React.useState<GroupingState>([])
  const [expanded, setExpanded] = React.useState<ExpandedState>({})
  const [data, setData] = React.useState(() => makeData(1_000))

  const columns = React.useMemo(
    () =>
      columnHelper.columns([
        columnHelper.display({
          id: 'select',
          header: ({ table }) => (
            <Checkbox
              checked={table.getIsAllPageRowsSelected()}
              indeterminate={
                !table.getIsAllPageRowsSelected() &&
                table.getIsSomePageRowsSelected()
              }
              onChange={(event) =>
                table.toggleAllPageRowsSelected(event.currentTarget.checked)
              }
              aria-label="Select all"
            />
          ),
          cell: ({ row }) => (
            <Checkbox
              checked={row.getIsSelected()}
              onChange={(event) =>
                row.toggleSelected(event.currentTarget.checked)
              }
              aria-label="Select row"
            />
          ),
          maxSize: 48,
          enableSorting: false,
          enableHiding: false,
          enableResizing: false,
        }),
        columnHelper.accessor('firstName', {
          id: 'firstName',
          header: ({ column }) => (
            <ColumnHeaderMenu column={column} title="First Name" />
          ),
          cell: (info) => (
            <EllipsisText>{String(info.getValue())}</EllipsisText>
          ),
          meta: { label: 'First Name', variant: 'text' },
        }),
        columnHelper.accessor((row) => row.lastName, {
          id: 'lastName',
          header: ({ column }) => (
            <ColumnHeaderMenu column={column} title="Last Name" />
          ),
          cell: (info) => (
            <EllipsisText>{String(info.getValue())}</EllipsisText>
          ),
          meta: { label: 'Last Name', variant: 'text' },
        }),
        columnHelper.accessor('age', {
          id: 'age',
          header: ({ column }) => (
            <ColumnHeaderMenu column={column} title="Age" />
          ),
          cell: (info) => <Text size="sm">{String(info.getValue())}</Text>,
          aggregationFn: 'mean',
          aggregatedCell: ({ getValue }) => (
            <Text size="sm" c="dimmed">
              Avg: {Math.round(Number(getValue()) * 10) / 10}
            </Text>
          ),
          meta: { label: 'Age', variant: 'number' },
        }),
        columnHelper.accessor('email', {
          id: 'email',
          header: ({ column }) => (
            <ColumnHeaderMenu column={column} title="Email" />
          ),
          cell: (info) => (
            <EllipsisText>{info.cell.getValue<string>()}</EllipsisText>
          ),
          meta: { label: 'Email', variant: 'text' },
        }),
        columnHelper.accessor('status', {
          id: 'status',
          header: ({ column }) => (
            <ColumnHeaderMenu column={column} title="Status" />
          ),
          cell: (info) => {
            const status = info.getValue<Person['status'] | undefined>()
            return status ? <StatusBadge status={status} /> : null
          },
          aggregatedCell: () => null,
          meta: {
            label: 'Status',
            variant: 'select',
            options: statuses.map((status) => ({
              label: toSentenceCase(status),
              value: status,
            })),
          },
        }),
        columnHelper.accessor('department', {
          id: 'department',
          header: ({ column }) => (
            <ColumnHeaderMenu column={column} title="Department" />
          ),
          cell: (info) => {
            const department = info.getValue<Person['department'] | undefined>()
            return department ? (
              <DepartmentPill department={department} />
            ) : null
          },
          aggregatedCell: () => null,
          meta: {
            label: 'Department',
            variant: 'multi-select',
            options: departments.map((department) => ({
              label: toSentenceCase(department),
              value: department,
            })),
          },
        }),
        columnHelper.accessor('joinDate', {
          id: 'joinDate',
          header: ({ column }) => (
            <ColumnHeaderMenu column={column} title="Join Date" />
          ),
          cell: (info) => formatDate(info.getValue<string>()),
          aggregationFn: 'min',
          aggregatedCell: ({ getValue }) => {
            const earliest = getValue<string>()
            return (
              <Text size="sm" c="dimmed">
                Earliest: {earliest ? formatDate(earliest) : '-'}
              </Text>
            )
          },
          meta: { label: 'Join Date', variant: 'date' },
        }),
        columnHelper.display({
          id: 'actions',
          enableHiding: false,
          cell: ({ row }) => <RowActions person={row.original} />,
          maxSize: 44,
          enableResizing: false,
        }),
      ]),
    [],
  )

  const [columnOrder, setColumnOrder] = React.useState<Array<string>>(() =>
    columns.map((column) => column.id ?? ''),
  )

  const table = useTable(
    {
      key: 'kitchen-sink-mantine', // needed for devtools
      features,
      columns,
      data,
      defaultColumn: {
        minSize: 60,
        maxSize: 800,
        filterFn: dynamicFilterFn,
      },
      globalFilterFn: 'fuzzy',
      state: {
        rowSelection,
        sorting,
        columnVisibility,
        columnOrder,
        columnSizing,
        columnFilters,
        globalFilter,
        columnPinning,
        grouping,
        expanded,
      },
      onSortingChange: setSorting,
      onColumnVisibilityChange: setColumnVisibility,
      onColumnOrderChange: setColumnOrder,
      onColumnSizingChange: setColumnSizing,
      onColumnFiltersChange: setColumnFilters,
      onGlobalFilterChange: setGlobalFilter,
      onColumnPinningChange: setColumnPinning,
      onGroupingChange: setGrouping,
      onExpandedChange: setExpanded,
      getRowId: (row) => row.id,
      enableRowSelection: true,
      onRowSelectionChange: setRowSelection,
      columnResizeMode: 'onChange',
      debugTable: true,
    },
    (state) => state, // default selector
  )

  useTanStackTableDevtools(table)

  const columnSizeVars = React.useMemo(() => {
    const headers = table.getFlatHeaders()
    const colSizes: Record<string, number> = {}
    for (const header of headers) {
      colSizes[`--header-${header.id}-size`] = header.getSize()
      colSizes[`--col-${header.column.id}-size`] = header.column.getSize()
    }
    return colSizes
  }, [table.state.columnSizing])

  const refreshData = () => setData(makeData(1_000))
  const stressTest = () => setData(makeData(200_000))

  return (
    <SortingContext.Provider value={sorting}>
      <Container fluid py="md">
        <Stack gap="md">
          <Paper withBorder p="sm">
            <Group justify="flex-end" gap="xs">
              <ModeMenu />
              <Button variant="outline" size="sm" onClick={refreshData}>
                Regenerate Data
              </Button>
              <Button variant="outline" size="sm" onClick={stressTest}>
                Stress Test (200k rows)
              </Button>
              <Button variant="outline" size="sm" onClick={() => rerender()}>
                Force Rerender
              </Button>
              <Button
                variant="outline"
                size="sm"
                onClick={() =>
                  console.info(
                    'table.getSelectedRowModel().flatRows',
                    table.getSelectedRowModel().flatRows,
                  )
                }
              >
                Log Selected Rows
              </Button>
            </Group>
          </Paper>

          <Group align="center" gap="xs">
            <DebouncedTextInput
              value={globalFilter}
              onChange={(value) => setGlobalFilter(String(value))}
              placeholder="Search all columns..."
              leftSection={<IconSearch size={16} />}
              w={{ base: '100%', md: 360 }}
            />
            <FilterListPopover
              table={table}
              columnFilters={columnFilters}
              onColumnFiltersChange={setColumnFilters}
            />
            <SortListPopover
              table={table}
              sorting={sorting}
              onSortingChange={setSorting}
            />
            <ViewOptionsPopover
              table={table}
              columnOrder={columnOrder}
              onColumnOrderChange={setColumnOrder}
            />
          </Group>

          <Paper withBorder>
            <MantineTable.ScrollContainer minWidth={1200} maxHeight={680}>
              <MantineTable
                stickyHeader
                highlightOnHover
                withColumnBorders
                withRowBorders
                withTableBorder
                style={{
                  width: `max(100%, ${table.getTotalSize()}px)`,
                  tableLayout: 'fixed',
                  ...columnSizeVars,
                }}
              >
                <colgroup>
                  {table.getVisibleLeafColumns().map((column) => (
                    <col
                      key={column.id}
                      style={{
                        width: `calc(var(--col-${column.id}-size) * 1px)`,
                      }}
                    />
                  ))}
                </colgroup>
                <MantineTable.Thead>
                  {table.getHeaderGroups().map((headerGroup) => (
                    <MantineTable.Tr key={headerGroup.id}>
                      {headerGroup.headers
                        .filter((header) => header.column.getIsVisible())
                        .map((header) => (
                          <ResizableHeaderCell
                            key={header.id}
                            header={header}
                            table={table}
                          />
                        ))}
                    </MantineTable.Tr>
                  ))}
                </MantineTable.Thead>
                <MantineTable.Tbody>
                  {table.getRowModel().rows.map((row) => {
                    const selected = row.getIsSelected()
                    return (
                      <MantineTable.Tr
                        key={row.id}
                        aria-selected={selected}
                        data-selected={selected || undefined}
                        bg={
                          selected
                            ? 'var(--mantine-color-blue-light)'
                            : undefined
                        }
                      >
                        {row.getVisibleCells().map((cell) => (
                          <MantineTable.Td
                            key={cell.id}
                            align={
                              cell.column.id === 'select' ? 'center' : undefined
                            }
                            style={{
                              width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
                              overflow: 'hidden',
                              ...getCommonPinningStyles(cell.column, selected),
                            }}
                          >
                            {cell.getIsGrouped() ? (
                              <Button
                                size="xs"
                                variant="subtle"
                                leftSection={
                                  row.getIsExpanded() ? (
                                    <IconChevronDown size={16} />
                                  ) : (
                                    <IconChevronRight size={16} />
                                  )
                                }
                                onClick={row.getToggleExpandedHandler()}
                                disabled={!row.getCanExpand()}
                                style={{
                                  paddingLeft: `calc(${row.depth} * 1.5rem + 0.5rem)`,
                                }}
                              >
                                <table.FlexRender cell={cell} />
                                <Text span c="dimmed" ml={4}>
                                  ({row.subRows.length})
                                </Text>
                              </Button>
                            ) : cell.column.id === 'progress' ? (
                              <Stack gap={4}>
                                <Text size="sm">
                                  {String(cell.getValue())}%
                                </Text>
                                <Progress value={Number(cell.getValue())} />
                              </Stack>
                            ) : (
                              <table.FlexRender cell={cell} />
                            )}
                          </MantineTable.Td>
                        ))}
                      </MantineTable.Tr>
                    )
                  })}
                </MantineTable.Tbody>
              </MantineTable>
            </MantineTable.ScrollContainer>
            <Pagination table={table} />
          </Paper>
        </Stack>
      </Container>
    </SortingContext.Provider>
  )
}

function ResizableHeaderCell({
  header,
  table,
}: {
  header: Header<typeof features, Person>
  table: {
    FlexRender: React.ComponentType<{
      header: Header<typeof features, Person>
    }>
  }
}) {
  const sorting = React.useContext(SortingContext)
  const sortDirection = getSortDirection(sorting, header.column.id)

  return (
    <MantineTable.Th
      colSpan={header.colSpan}
      align={header.column.id === 'select' ? 'center' : undefined}
      aria-sort={getAriaSort(sortDirection || false)}
      data-sort={sortDirection}
      style={{
        width: `calc(var(--header-${header.id}-size) * 1px)`,
        padding: 8,
        ...getCommonPinningStyles(header.column),
      }}
    >
      <Box
        style={{
          position: 'relative',
          paddingRight: header.column.getCanResize() ? 8 : 0,
        }}
      >
        {header.isPlaceholder ? null : <table.FlexRender header={header} />}
        {header.column.getCanResize() ? (
          <Box
            onDoubleClick={() => header.column.resetSize()}
            onMouseDown={header.getResizeHandler()}
            onTouchStart={header.getResizeHandler()}
            style={{
              position: 'absolute',
              top: 0,
              right: -6,
              width: 6,
              height: '100%',
              cursor: 'col-resize',
              touchAction: 'none',
              background: header.column.getIsResizing()
                ? 'var(--mantine-primary-color-filled)'
                : 'transparent',
            }}
          />
        ) : null}
      </Box>
    </MantineTable.Th>
  )
}

function Root() {
  return (
    <MantineProvider defaultColorScheme="auto">
      <App />
      <TanStackDevtools plugins={[tableDevtoolsPlugin()]} />
    </MantineProvider>
  )
}

const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <Root />
  </React.StrictMode>,
)