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 {
  ActionIcon,
  Box,
  Button,
  Container,
  Group,
  MantineProvider,
  Table as MantineTable,
  Menu,
  Paper,
  Stack,
  TextInput,
  Tooltip,
  useComputedColorScheme,
  useMantineColorScheme,
} from '@mantine/core'
import '@mantine/core/styles.css'
import {
  IconDeviceDesktop,
  IconMoon,
  IconSearch,
  IconSun,
} from '@tabler/icons-react'
import {
  tableDevtoolsPlugin,
  useTanStackTableDevtools,
} from '@tanstack/react-table-devtools'
import type {
  Cell,
  Column,
  Header,
  HeaderGroup,
  Row,
} from '@tanstack/react-table'
import type { ExtendedColumnFilter } from '@/types'
import type { Person } from '@/lib/make-data'
import type { features } from '@/hooks/features'
import { makeData } from '@/lib/make-data'
import { useAppTable } from '@/hooks/table'
import { columns } from '@/columns'
import './styles/globals.css'

type AppColumn = Column<typeof features, Person, any>
type AppHeader = Header<typeof features, Person, any>
type AppRow = Row<typeof features, Person>
type AppCell = Cell<typeof features, Person, any>
type AppHeaderGroup = HeaderGroup<typeof features, Person>

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 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 [columnFilters, setColumnFilters] = React.useState<
    Array<ExtendedColumnFilter>
  >([])
  const [globalFilter, setGlobalFilter] = React.useState('')
  const [data, setData] = React.useState(() => makeData(1_000))

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

  const table = useAppTable(
    {
      key: 'kitchen-sink-mantine',
      columns,
      data,
      debugTable: true,
      state: {
        columnFilters,
        globalFilter,
      },
      onColumnFiltersChange: setColumnFilters,
      onGlobalFilterChange: setGlobalFilter,
      initialState: {
        columnPinning: { left: ['select'], right: ['actions'] },
        columnOrder: columns.map((c) => c.id ?? ''),
      },
    },
    (state) => state,
  )

  useTanStackTableDevtools(table)

  return (
    <table.AppTable>
      <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 (1M rows)
              </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 }}
            />
            <table.FilterList />
            <table.SortList />
            <table.ViewOptions />
          </Group>

          <Paper withBorder>
            <MantineTable.ScrollContainer minWidth={0} maxHeight={680}>
              <MantineTable
                stickyHeader
                highlightOnHover
                withColumnBorders
                withRowBorders
                withTableBorder
                style={{
                  display: 'grid',
                }}
              >
                <MantineTable.Thead
                  style={{
                    display: 'grid',
                    position: 'sticky',
                    top: 0,
                    zIndex: 3,
                  }}
                >
                  {table
                    .getHeaderGroups()
                    .map((headerGroup: AppHeaderGroup) => (
                      <MantineTable.Tr
                        key={headerGroup.id}
                        style={{ display: 'flex', width: '100%' }}
                      >
                        {headerGroup.headers
                          .filter((header: AppHeader) =>
                            header.column.getIsVisible(),
                          )
                          .map((header: AppHeader) => (
                            <table.AppHeader header={header} key={header.id}>
                              {(h) => (
                                <MantineTable.Th
                                  colSpan={h.colSpan}
                                  style={{
                                    display: 'flex',
                                    flexShrink: 0,
                                    alignItems: 'center',
                                    overflow: 'clip',
                                    whiteSpace: 'nowrap',
                                    textOverflow: 'ellipsis',
                                    flexGrow: h.getSize(),
                                    width: h.getSize(),
                                    padding: 8,
                                    position: 'relative',
                                    ...getCommonPinningStyles(h.column),
                                  }}
                                >
                                  <Box
                                    style={{
                                      position: 'relative',
                                      flex: 1,
                                      minWidth: 0,
                                      paddingRight: h.column.getCanResize()
                                        ? 8
                                        : 0,
                                    }}
                                  >
                                    {h.isPlaceholder ? null : <h.FlexRender />}
                                  </Box>
                                  <h.ResizeHandle />
                                </MantineTable.Th>
                              )}
                            </table.AppHeader>
                          ))}
                      </MantineTable.Tr>
                    ))}
                </MantineTable.Thead>
                <MantineTable.Tbody style={{ display: 'grid' }}>
                  {table.getRowModel().rows.map((row: AppRow) => (
                    <MantineTable.Tr
                      key={row.id}
                      data-selected={row.getIsSelected() || undefined}
                      aria-selected={row.getIsSelected()}
                      style={{
                        display: 'flex',
                        width: '100%',
                        background: row.getIsSelected()
                          ? 'var(--mantine-color-blue-light)'
                          : undefined,
                      }}
                    >
                      {row.getVisibleCells().map((cell: AppCell) => (
                        <table.AppCell cell={cell} key={cell.id}>
                          {(c) => (
                            <MantineTable.Td
                              style={{
                                display: 'flex',
                                flexShrink: 0,
                                alignItems: 'center',
                                overflow: 'hidden',
                                flexGrow: c.column.getSize(),
                                width: c.column.getSize(),
                                ...getCommonPinningStyles(
                                  c.column,
                                  row.getIsSelected(),
                                ),
                              }}
                            >
                              {c.getIsGrouped() ? (
                                <c.GroupedCell />
                              ) : (
                                <c.FlexRender />
                              )}
                            </MantineTable.Td>
                          )}
                        </table.AppCell>
                      ))}
                    </MantineTable.Tr>
                  ))}
                </MantineTable.Tbody>
              </MantineTable>
            </MantineTable.ScrollContainer>
            <table.Pagination />
          </Paper>
        </Stack>
      </Container>
    </table.AppTable>
  )
}

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

ReactDOM.createRoot(rootElement).render(
  <React.StrictMode>
    <MantineProvider defaultColorScheme="auto">
      <App />
      <TanStackDevtools plugins={[tableDevtoolsPlugin()]} />
    </MantineProvider>
  </React.StrictMode>,
)