UNPKG

mantine-datatable

Version:

The lightweight, dependency-free, dark-theme aware table component for your Mantine UI data-rich applications, featuring asynchronous data loading support, pagination, intuitive Gmail-style additive batch rows selection, column sorting, custom cell data r

1 lines 209 kB
{"version":3,"sources":["../package/DataTable.tsx","../package/DataTableDragToggleProvider.tsx","../package/DataTableColumns.context.ts","../package/DataTableEmptyRow.tsx","../package/DataTableEmptyState.tsx","../package/icons/IconDatabaseOff.tsx","../package/DataTableFooter.tsx","../package/DataTableFooterCell.tsx","../package/hooks/useColumnResize.ts","../package/hooks/useDataTableColumnReorder.ts","../package/hooks/useDataTableColumnResize.ts","../package/hooks/useDataTableColumns.ts","../package/hooks/useDataTableColumnToggle.ts","../package/hooks/useDataTableInjectCssVariables.ts","../package/hooks/useIsomorphicLayoutEffect.ts","../package/hooks/useStableValue.ts","../package/hooks/useLastSelectionChangeIndex.ts","../package/hooks/useMediaQueries.ts","../package/hooks/useMediaQueriesStringOrFunction.ts","../package/hooks/useMediaQueryStringOrFunction.ts","../package/hooks/useRowExpansion.ts","../package/utils.ts","../package/hooks/useRowExpansionStatus.ts","../package/utilityClasses.ts","../package/DataTableFooterSelectorPlaceholderCell.tsx","../package/DataTableHeader.tsx","../package/DataTableColumnGroupHeaderCell.tsx","../package/DataTableHeaderCell.tsx","../package/DataTableHeaderCellFilter.tsx","../package/icons/IconFilter.tsx","../package/icons/IconFilterFilled.tsx","../package/DataTableResizableHeaderHandle.tsx","../package/icons/IconArrowUp.tsx","../package/icons/IconArrowsVertical.tsx","../package/icons/IconGripVertical.tsx","../package/icons/IconX.tsx","../package/DataTableHeaderSelectorCell.tsx","../package/DataTableLoader.tsx","../package/DataTablePagination.tsx","../package/DataTablePageSizeSelector.tsx","../package/cssVariables.ts","../package/icons/IconSelector.tsx","../package/DataTableRow.tsx","../package/DataTableRowCell.tsx","../package/DataTableRowExpansion.tsx","../package/DataTableRowSelectorCell.tsx","../package/DataTableScrollArea.tsx","../package/DataTableDraggableRow.tsx"],"sourcesContent":["import { Box, Table, type MantineSize } from '@mantine/core';\nimport { useMergedRef } from '@mantine/hooks';\nimport clsx from 'clsx';\nimport type { RefObject } from 'react';\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { DataTableColumnsProvider } from './DataTableDragToggleProvider';\nimport { DataTableEmptyRow } from './DataTableEmptyRow';\nimport { DataTableEmptyState } from './DataTableEmptyState';\nimport { DataTableFooter } from './DataTableFooter';\nimport { DataTableHeader } from './DataTableHeader';\nimport { DataTableLoader } from './DataTableLoader';\nimport { DataTablePagination } from './DataTablePagination';\nimport { DataTableRow } from './DataTableRow';\nimport { DataTableScrollArea } from './DataTableScrollArea';\nimport { getTableCssVariables } from './cssVariables';\nimport {\n useDataTableColumns,\n useDataTableInjectCssVariables,\n useLastSelectionChangeIndex,\n useRowExpansion,\n} from './hooks';\nimport type { DataTableProps } from './types';\nimport { TEXT_SELECTION_DISABLED } from './utilityClasses';\nimport { differenceBy, flattenColumns, getRecordId, uniqBy } from './utils';\n\nexport function DataTable<T>({\n withTableBorder,\n borderRadius,\n textSelectionDisabled,\n height = '100%',\n minHeight,\n maxHeight,\n shadow,\n verticalAlign = 'center',\n fetching,\n columns,\n storeColumnsKey,\n groups,\n pinFirstColumn,\n pinLastColumn,\n defaultColumnProps,\n defaultColumnRender,\n idAccessor = 'id',\n records,\n selectionTrigger = 'checkbox',\n selectedRecords,\n onSelectedRecordsChange,\n selectionColumnClassName,\n selectionColumnStyle,\n isRecordSelectable,\n selectionCheckboxProps,\n allRecordsSelectionCheckboxProps = { 'aria-label': 'Select all records' },\n getRecordSelectionCheckboxProps = (_, index) => ({ 'aria-label': `Select record ${index + 1}` }),\n sortStatus,\n sortIcons,\n onSortStatusChange,\n horizontalSpacing,\n page,\n onPageChange,\n totalRecords,\n recordsPerPage,\n onRecordsPerPageChange,\n recordsPerPageOptions,\n recordsPerPageLabel = 'Records per page',\n paginationWithEdges,\n paginationWithControls,\n paginationActiveTextColor,\n paginationActiveBackgroundColor,\n paginationSize = 'sm',\n paginationText = ({ from, to, totalRecords }) => `${from} - ${to} / ${totalRecords}`,\n paginationWrapBreakpoint = 'sm',\n getPaginationControlProps = (control) => {\n if (control === 'previous') {\n return { 'aria-label': 'Previous page' };\n } else if (control === 'next') {\n return { 'aria-label': 'Next page' };\n }\n return {};\n },\n getPaginationItemProps,\n renderPagination,\n loaderBackgroundBlur,\n customLoader,\n loaderSize,\n loaderType,\n loaderColor,\n loadingText = '...',\n emptyState,\n noRecordsText = 'No records',\n noRecordsIcon,\n highlightOnHover,\n striped,\n noHeader,\n onRowClick,\n onRowDoubleClick,\n onRowContextMenu,\n onCellClick,\n onCellDoubleClick,\n onCellContextMenu,\n onScroll,\n onScrollToTop,\n onScrollToBottom,\n onScrollToLeft,\n onScrollToRight,\n c,\n backgroundColor,\n borderColor,\n rowBorderColor,\n stripedColor,\n highlightOnHoverColor,\n rowColor,\n rowBackgroundColor,\n rowExpansion,\n rowClassName,\n rowStyle,\n customRowAttributes,\n scrollViewportRef,\n scrollAreaProps,\n tableRef,\n bodyRef,\n m,\n my,\n mx,\n mt,\n mb,\n ml,\n mr,\n className,\n classNames,\n style,\n styles,\n rowFactory,\n tableWrapper,\n ...otherProps\n}: DataTableProps<T>) {\n const effectiveColumns = useMemo(() => {\n return groups ? flattenColumns(groups) : columns!;\n }, [columns, groups]);\n\n // When columns are resizable, start with auto layout to let the browser\n // compute natural widths, then capture them and switch to fixed layout.\n const [fixedLayoutEnabled, setFixedLayoutEnabled] = useState(false);\n\n const { refs, onScroll: handleScrollPositionChange } = useDataTableInjectCssVariables({\n scrollCallbacks: {\n onScroll,\n onScrollToTop,\n onScrollToBottom,\n onScrollToLeft,\n onScrollToRight,\n },\n withRowBorders: otherProps.withRowBorders,\n });\n\n const dragToggle = useDataTableColumns({\n key: storeColumnsKey,\n columns: effectiveColumns,\n headerRef: refs.header as RefObject<HTMLTableSectionElement | null>,\n scrollViewportRef: refs.scrollViewport as RefObject<HTMLElement | null>,\n onFixedLayoutChange: setFixedLayoutEnabled,\n });\n\n const mergedTableRef = useMergedRef(refs.table, tableRef);\n const mergedViewportRef = useMergedRef(refs.scrollViewport, scrollViewportRef);\n const rowExpansionInfo = useRowExpansion<T>({ rowExpansion, records, idAccessor });\n\n // Track when we should reset scroll due to pagination, but defer until data is rendered\n const resetScrollPending = useRef(false);\n const prevPageRef = useRef(page);\n const recordsAtPageChangeRef = useRef<typeof records | undefined>(records);\n\n const handlePageChange = useCallback(\n (newPage: number) => {\n resetScrollPending.current = true;\n recordsAtPageChangeRef.current = records;\n onPageChange!(newPage);\n },\n [onPageChange, records]\n );\n\n // Handle externally-driven page changes\n useEffect(() => {\n if (prevPageRef.current !== page) {\n resetScrollPending.current = true;\n recordsAtPageChangeRef.current = records;\n prevPageRef.current = page;\n }\n }, [page, records]);\n\n const recordsLength = records?.length;\n\n // Reset scroll position when changing pages (sync) or when records change (async)\n useLayoutEffect(() => {\n if (!resetScrollPending.current) return;\n if (fetching) return;\n if (records === recordsAtPageChangeRef.current) return;\n\n const viewport = refs.scrollViewport.current;\n if (!viewport) return;\n\n const raf = requestAnimationFrame(() => {\n viewport.scrollTo({ top: 0, left: 0 });\n resetScrollPending.current = false;\n });\n\n return () => cancelAnimationFrame(raf);\n }, [fetching, records, refs.scrollViewport]);\n\n const recordIds = records?.map((record) => getRecordId(record, idAccessor));\n const selectionColumnVisible = !!selectedRecords;\n const selectedRecordIds = selectedRecords?.map((record) => getRecordId(record, idAccessor));\n const hasRecordsAndSelectedRecords =\n recordIds !== undefined && selectedRecordIds !== undefined && selectedRecordIds.length > 0;\n\n const selectableRecords = isRecordSelectable ? records?.filter(isRecordSelectable) : records;\n const selectableRecordIds = selectableRecords?.map((record) => getRecordId(record, idAccessor));\n\n const allSelectableRecordsSelected =\n hasRecordsAndSelectedRecords && selectableRecordIds!.every((id) => selectedRecordIds.includes(id));\n const someRecordsSelected =\n hasRecordsAndSelectedRecords && selectableRecordIds!.some((id) => selectedRecordIds.includes(id));\n\n const handleHeaderSelectionChange = useCallback(() => {\n if (selectedRecords && onSelectedRecordsChange) {\n onSelectedRecordsChange(\n allSelectableRecordsSelected\n ? selectedRecords.filter((record) => !selectableRecordIds!.includes(getRecordId(record, idAccessor)))\n : uniqBy([...selectedRecords, ...selectableRecords!], (record) => getRecordId(record, idAccessor))\n );\n }\n }, [\n allSelectableRecordsSelected,\n idAccessor,\n onSelectedRecordsChange,\n selectableRecordIds,\n selectableRecords,\n selectedRecords,\n ]);\n\n const { lastSelectionChangeIndex, setLastSelectionChangeIndex } = useLastSelectionChangeIndex(recordIds);\n const selectorCellShadowVisible = selectionColumnVisible && !pinFirstColumn;\n\n const marginProperties = { m, my, mx, mt, mb, ml, mr };\n\n const TableWrapper = useCallback(\n ({ children }: { children: React.ReactNode }) => {\n if (tableWrapper) return tableWrapper({ children });\n return children;\n },\n [tableWrapper]\n );\n\n return (\n <DataTableColumnsProvider {...dragToggle}>\n <Box\n ref={refs.root}\n {...marginProperties}\n className={clsx(\n 'mantine-datatable',\n { 'mantine-datatable-with-border': withTableBorder },\n className,\n classNames?.root\n )}\n style={[\n (theme) => ({\n ...getTableCssVariables({\n theme,\n c,\n backgroundColor,\n borderColor,\n rowBorderColor,\n stripedColor,\n highlightOnHoverColor,\n }),\n borderRadius: theme.radius[borderRadius as MantineSize] || borderRadius,\n boxShadow: theme.shadows[shadow as MantineSize] || shadow,\n height,\n minHeight,\n maxHeight,\n }),\n style,\n styles?.root,\n {\n position: 'relative',\n },\n ]}\n >\n <DataTableScrollArea\n viewportRef={mergedViewportRef}\n leftShadowBehind={selectionColumnVisible || !!pinFirstColumn}\n rightShadowBehind={pinLastColumn}\n onScrollPositionChange={handleScrollPositionChange}\n scrollAreaProps={scrollAreaProps}\n >\n <TableWrapper>\n <Table\n ref={mergedTableRef}\n horizontalSpacing={horizontalSpacing}\n className={clsx(\n 'mantine-datatable-table',\n {\n [TEXT_SELECTION_DISABLED]: textSelectionDisabled,\n 'mantine-datatable-vertical-align-top': verticalAlign === 'top',\n 'mantine-datatable-vertical-align-bottom': verticalAlign === 'bottom',\n 'mantine-datatable-pin-last-column': pinLastColumn,\n 'mantine-datatable-selection-column-visible': selectionColumnVisible,\n 'mantine-datatable-pin-first-column': pinFirstColumn,\n 'mantine-datatable-resizable-columns': dragToggle.hasResizableColumns && fixedLayoutEnabled,\n },\n classNames?.table\n )}\n style={{\n ...styles?.table,\n }}\n data-striped={(recordsLength && striped) || undefined}\n data-highlight-on-hover={highlightOnHover || undefined}\n {...otherProps}\n >\n {noHeader ? null : (\n <DataTableColumnsProvider {...dragToggle}>\n <DataTableHeader<T>\n ref={refs.header}\n selectionColumnHeaderRef={refs.selectionColumnHeader}\n className={classNames?.header}\n style={styles?.header}\n columns={effectiveColumns}\n defaultColumnProps={defaultColumnProps}\n groups={groups}\n sortStatus={sortStatus}\n sortIcons={sortIcons}\n onSortStatusChange={onSortStatusChange}\n selectionTrigger={selectionTrigger}\n selectionVisible={selectionColumnVisible}\n selectionChecked={allSelectableRecordsSelected}\n selectionIndeterminate={someRecordsSelected && !allSelectableRecordsSelected}\n onSelectionChange={handleHeaderSelectionChange}\n selectionCheckboxProps={{ ...selectionCheckboxProps, ...allRecordsSelectionCheckboxProps }}\n selectorCellShadowVisible={selectorCellShadowVisible}\n selectionColumnClassName={selectionColumnClassName}\n selectionColumnStyle={selectionColumnStyle}\n withColumnBorders={otherProps.withColumnBorders}\n />\n </DataTableColumnsProvider>\n )}\n <tbody ref={bodyRef}>\n {recordsLength ? (\n records.map((record, index) => {\n const recordId = getRecordId(record, idAccessor);\n const isSelected = selectedRecordIds?.includes(recordId) || false;\n\n let handleSelectionChange: React.MouseEventHandler | undefined;\n\n if (onSelectedRecordsChange && selectedRecords) {\n handleSelectionChange = (e) => {\n if (e.nativeEvent.shiftKey && lastSelectionChangeIndex !== null) {\n const targetRecords = records.filter(\n index > lastSelectionChangeIndex\n ? (rec, idx) =>\n idx >= lastSelectionChangeIndex &&\n idx <= index &&\n (isRecordSelectable ? isRecordSelectable(rec, idx) : true)\n : (rec, idx) =>\n idx >= index &&\n idx <= lastSelectionChangeIndex &&\n (isRecordSelectable ? isRecordSelectable(rec, idx) : true)\n );\n onSelectedRecordsChange(\n isSelected\n ? differenceBy(selectedRecords, targetRecords, (r) => getRecordId(r, idAccessor))\n : uniqBy([...selectedRecords, ...targetRecords], (r) => getRecordId(r, idAccessor))\n );\n } else {\n onSelectedRecordsChange(\n isSelected\n ? selectedRecords.filter((rec) => getRecordId(rec, idAccessor) !== recordId)\n : uniqBy([...selectedRecords, record], (rec) => getRecordId(rec, idAccessor))\n );\n }\n setLastSelectionChangeIndex(index);\n };\n }\n\n return (\n <DataTableRow<T>\n key={recordId as React.Key}\n record={record}\n index={index}\n columns={effectiveColumns}\n defaultColumnProps={defaultColumnProps}\n defaultColumnRender={defaultColumnRender}\n selectionTrigger={selectionTrigger}\n selectionVisible={selectionColumnVisible}\n selectionChecked={isSelected}\n onSelectionChange={handleSelectionChange}\n isRecordSelectable={isRecordSelectable}\n selectionCheckboxProps={selectionCheckboxProps}\n getSelectionCheckboxProps={getRecordSelectionCheckboxProps}\n onClick={onRowClick}\n onDoubleClick={onRowDoubleClick}\n onCellClick={onCellClick}\n onCellDoubleClick={onCellDoubleClick}\n onContextMenu={onRowContextMenu}\n onCellContextMenu={onCellContextMenu}\n expansion={rowExpansionInfo}\n color={rowColor}\n backgroundColor={rowBackgroundColor}\n className={rowClassName}\n style={rowStyle}\n customAttributes={customRowAttributes}\n selectorCellShadowVisible={selectorCellShadowVisible}\n selectionColumnClassName={selectionColumnClassName}\n selectionColumnStyle={selectionColumnStyle}\n idAccessor={idAccessor as string}\n rowFactory={rowFactory}\n />\n );\n })\n ) : (\n <DataTableEmptyRow />\n )}\n </tbody>\n\n {effectiveColumns.some(({ footer }) => footer) && (\n <DataTableFooter<T>\n ref={refs.footer}\n className={classNames?.footer}\n style={styles?.footer}\n columns={effectiveColumns}\n defaultColumnProps={defaultColumnProps}\n selectionVisible={selectionColumnVisible}\n selectorCellShadowVisible={selectorCellShadowVisible}\n />\n )}\n </Table>\n </TableWrapper>\n </DataTableScrollArea>\n {!!(page && recordsLength) && (\n <DataTablePagination\n className={classNames?.pagination}\n style={styles?.pagination}\n horizontalSpacing={horizontalSpacing}\n fetching={fetching}\n page={page}\n onPageChange={handlePageChange}\n totalRecords={totalRecords}\n recordsPerPage={recordsPerPage}\n onRecordsPerPageChange={onRecordsPerPageChange}\n recordsPerPageOptions={recordsPerPageOptions}\n recordsPerPageLabel={recordsPerPageLabel}\n paginationWithEdges={paginationWithEdges}\n paginationWithControls={paginationWithControls}\n paginationActiveTextColor={paginationActiveTextColor}\n paginationActiveBackgroundColor={paginationActiveBackgroundColor}\n paginationSize={paginationSize}\n paginationText={paginationText}\n paginationWrapBreakpoint={paginationWrapBreakpoint}\n getPaginationControlProps={getPaginationControlProps}\n getPaginationItemProps={getPaginationItemProps}\n noRecordsText={noRecordsText}\n loadingText={loadingText}\n recordsLength={recordsLength}\n renderPagination={renderPagination}\n />\n )}\n <DataTableLoader\n fetching={fetching}\n backgroundBlur={loaderBackgroundBlur}\n customContent={customLoader}\n size={loaderSize}\n type={loaderType}\n color={loaderColor}\n />\n <DataTableEmptyState icon={noRecordsIcon} text={noRecordsText} active={!fetching && !recordsLength}>\n {emptyState}\n </DataTableEmptyState>\n </Box>\n </DataTableColumnsProvider>\n );\n}\n","'use client';\n\nimport { useState, type Dispatch, type PropsWithChildren, type SetStateAction } from 'react';\nimport { DataTableColumnsContextProvider } from './DataTableColumns.context';\nimport type { DataTableColumnToggle } from './hooks';\n\ntype DataTableColumnsProviderProps = PropsWithChildren<{\n columnsOrder: string[];\n setColumnsOrder: Dispatch<SetStateAction<string[]>>;\n resetColumnsOrder: () => void;\n\n columnsToggle: DataTableColumnToggle[];\n setColumnsToggle: Dispatch<SetStateAction<DataTableColumnToggle[]>>;\n resetColumnsToggle: () => void;\n\n setColumnWidth: (accessor: string, width: string | number) => void;\n setMultipleColumnWidths: (updates: Array<{ accessor: string; width: string | number }>) => void;\n resetColumnsWidth: () => void;\n}>;\n\nexport const DataTableColumnsProvider = (props: DataTableColumnsProviderProps) => {\n const {\n children,\n columnsOrder,\n setColumnsOrder,\n columnsToggle,\n setColumnsToggle,\n\n resetColumnsOrder,\n resetColumnsToggle,\n\n setColumnWidth,\n setMultipleColumnWidths,\n resetColumnsWidth,\n } = props;\n\n const [sourceColumn, setSourceColumn] = useState('');\n const [targetColumn, setTargetColumn] = useState('');\n\n const swapColumns = () => {\n if (!columnsOrder || !setColumnsOrder || !sourceColumn || !targetColumn) {\n return;\n }\n const sourceIndex = columnsOrder.indexOf(sourceColumn);\n const targetIndex = columnsOrder.indexOf(targetColumn);\n\n if (sourceIndex !== -1 && targetIndex !== -1) {\n const removedColumn = columnsOrder.splice(sourceIndex, 1)[0];\n\n columnsOrder.splice(targetIndex, 0, removedColumn);\n\n // update the columns order\n setColumnsOrder([...columnsOrder]);\n }\n };\n\n return (\n <DataTableColumnsContextProvider\n value={{\n sourceColumn,\n setSourceColumn,\n targetColumn,\n setTargetColumn,\n columnsToggle,\n setColumnsToggle,\n swapColumns,\n resetColumnsOrder,\n resetColumnsToggle,\n\n setColumnWidth,\n setMultipleColumnWidths,\n resetColumnsWidth,\n }}\n >\n {children}\n </DataTableColumnsContextProvider>\n );\n};\n","import { createSafeContext } from '@mantine/core';\nimport type { Dispatch, SetStateAction } from 'react';\nimport type { DataTableColumnToggle } from './hooks';\n\ninterface DataTableColumnsContext {\n // accessor of the column which is currently dragged\n sourceColumn: string;\n setSourceColumn: Dispatch<SetStateAction<string>>;\n\n // accessor of the column which is currently hovered\n targetColumn: string;\n setTargetColumn: Dispatch<SetStateAction<string>>;\n\n // swap the source column with the target column\n swapColumns: () => void;\n\n // reset to the default columns order\n resetColumnsOrder: () => void;\n\n columnsToggle: DataTableColumnToggle[];\n setColumnsToggle: Dispatch<SetStateAction<DataTableColumnToggle[]>>;\n resetColumnsToggle: () => void;\n\n setColumnWidth: (accessor: string, width: string | number) => void;\n setMultipleColumnWidths: (updates: Array<{ accessor: string; width: string | number }>) => void;\n resetColumnsWidth: () => void;\n}\n\nexport const [DataTableColumnsContextProvider, useDataTableColumnsContext] = createSafeContext<DataTableColumnsContext>(\n 'useDataTableColumnsContext must be used within DataTableColumnProvider'\n);\n","export function DataTableEmptyRow() {\n return (\n <tr className=\"mantine-datatable-empty-row\">\n <td />\n </tr>\n );\n}\n","import { Center, Text } from '@mantine/core';\nimport { IconDatabaseOff } from './icons/IconDatabaseOff';\n\ntype DataTableEmptyStateProps = React.PropsWithChildren<{\n icon: React.ReactNode | undefined;\n text: string;\n active: boolean;\n}>;\n\nexport function DataTableEmptyState({ icon, text, active, children }: DataTableEmptyStateProps) {\n return (\n <Center className=\"mantine-datatable-empty-state\" data-active={active || undefined}>\n {children || (\n <>\n {icon || (\n <div className=\"mantine-datatable-empty-state-icon\">\n <IconDatabaseOff />\n </div>\n )}\n <Text component=\"div\" size=\"sm\" c=\"dimmed\">\n {text}\n </Text>\n </>\n )}\n </Center>\n );\n}\n","export function IconDatabaseOff() {\n return (\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n strokeWidth=\"2\"\n stroke=\"currentColor\"\n fill=\"none\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n <path d=\"M12.983 8.978c3.955 -.182 7.017 -1.446 7.017 -2.978c0 -1.657 -3.582 -3 -8 -3c-1.661 0 -3.204 .19 -4.483 .515m-2.783 1.228c-.471 .382 -.734 .808 -.734 1.257c0 1.22 1.944 2.271 4.734 2.74\" />\n <path d=\"M4 6v6c0 1.657 3.582 3 8 3c.986 0 1.93 -.067 2.802 -.19m3.187 -.82c1.251 -.53 2.011 -1.228 2.011 -1.99v-6\" />\n <path d=\"M4 12v6c0 1.657 3.582 3 8 3c3.217 0 5.991 -.712 7.261 -1.74m.739 -3.26v-4\" />\n <path d=\"M3 3l18 18\" />\n </svg>\n );\n}\n","import { TableTfoot, TableTr, type MantineStyleProp } from '@mantine/core';\nimport clsx from 'clsx';\nimport { DataTableFooterCell } from './DataTableFooterCell';\nimport { DataTableFooterSelectorPlaceholderCell } from './DataTableFooterSelectorPlaceholderCell';\nimport type { DataTableColumn, DataTableDefaultColumnProps } from './types';\n\ntype DataTableFooterProps<T> = {\n className: string | undefined;\n style: MantineStyleProp | undefined;\n columns: DataTableColumn<T>[];\n defaultColumnProps: DataTableDefaultColumnProps<T> | undefined;\n selectionVisible: boolean;\n selectorCellShadowVisible: boolean;\n ref: React.Ref<HTMLTableSectionElement>;\n};\n\nexport function DataTableFooter<T>({\n className,\n style,\n columns,\n defaultColumnProps,\n selectionVisible,\n selectorCellShadowVisible,\n ref,\n}: DataTableFooterProps<T>) {\n return (\n <TableTfoot ref={ref} className={clsx('mantine-datatable-footer', className)} style={style}>\n <TableTr>\n {selectionVisible && <DataTableFooterSelectorPlaceholderCell shadowVisible={selectorCellShadowVisible} />}\n {columns.map(({ hidden, ...columnProps }) => {\n if (hidden) return null;\n\n const {\n accessor,\n visibleMediaQuery,\n textAlign,\n width,\n footer,\n footerClassName,\n footerStyle,\n noWrap,\n ellipsis,\n } = { ...defaultColumnProps, ...columnProps };\n\n return (\n <DataTableFooterCell<T>\n key={accessor as React.Key}\n className={footerClassName}\n style={footerStyle}\n visibleMediaQuery={visibleMediaQuery}\n textAlign={textAlign}\n width={width}\n title={footer}\n noWrap={noWrap}\n ellipsis={ellipsis}\n />\n );\n })}\n </TableTr>\n </TableTfoot>\n );\n}\n","import { TableTh, type MantineStyleProp, type MantineTheme } from '@mantine/core';\nimport clsx from 'clsx';\nimport { useMediaQueryStringOrFunction } from './hooks';\nimport type { DataTableColumn } from './types';\nimport { ELLIPSIS, NOWRAP, TEXT_ALIGN_CENTER, TEXT_ALIGN_LEFT, TEXT_ALIGN_RIGHT } from './utilityClasses';\n\ntype DataTableFooterCellProps<T> = {\n className: string | undefined;\n style: MantineStyleProp | undefined;\n visibleMediaQuery: string | ((theme: MantineTheme) => string) | undefined;\n title: React.ReactNode | undefined;\n} & Pick<DataTableColumn<T>, 'noWrap' | 'ellipsis' | 'textAlign' | 'width'>;\n\nexport function DataTableFooterCell<T>({\n className,\n style,\n visibleMediaQuery,\n title,\n noWrap,\n ellipsis,\n textAlign,\n width,\n}: DataTableFooterCellProps<T>) {\n if (!useMediaQueryStringOrFunction(visibleMediaQuery)) return null;\n return (\n <TableTh\n className={clsx(\n {\n [NOWRAP]: noWrap || ellipsis,\n [ELLIPSIS]: ellipsis,\n [TEXT_ALIGN_LEFT]: textAlign === 'left',\n [TEXT_ALIGN_CENTER]: textAlign === 'center',\n [TEXT_ALIGN_RIGHT]: textAlign === 'right',\n },\n className\n )}\n style={[\n {\n width,\n minWidth: width,\n maxWidth: width,\n },\n style,\n ]}\n >\n {title}\n </TableTh>\n );\n}\n","import { useCallback, useRef, useState } from 'react';\n\ninterface UseColumnResizeProps {\n onColumnResize: (updates: Array<{ accessor: string; width: string | number }>) => void;\n minColumnWidth?: number;\n}\n\ninterface ResizeState {\n isResizing: boolean;\n startX: number;\n originalWidths: { current: number; next: number };\n currentAccessor: string;\n nextAccessor: string | null;\n}\n\nexport const useColumnResize = ({ onColumnResize, minColumnWidth = 50 }: UseColumnResizeProps) => {\n const [resizeState, setResizeState] = useState<ResizeState | null>(null);\n const currentColumnRef = useRef<HTMLTableCellElement | null>(null);\n const nextColumnRef = useRef<HTMLTableCellElement | null>(null);\n\n const startResize = useCallback(\n (event: React.MouseEvent, currentColumn: HTMLTableCellElement, currentAccessor: string) => {\n event.preventDefault();\n event.stopPropagation();\n\n const nextColumn = currentColumn.nextElementSibling as HTMLTableCellElement | null;\n if (!nextColumn) {\n return false;\n }\n\n const nextAccessor = nextColumn.getAttribute('data-accessor');\n if (!nextAccessor) {\n return false;\n }\n\n const currentWidth = currentColumn.getBoundingClientRect().width;\n const nextWidth = nextColumn.getBoundingClientRect().width;\n\n currentColumnRef.current = currentColumn;\n nextColumnRef.current = nextColumn;\n\n setResizeState({\n isResizing: true,\n startX: event.clientX,\n originalWidths: { current: currentWidth, next: nextWidth },\n currentAccessor,\n nextAccessor,\n });\n\n // Global styles for better UX\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n\n return true;\n },\n []\n );\n\n const updateResize = useCallback(\n (clientX: number) => {\n if (!resizeState || !currentColumnRef.current || !nextColumnRef.current) return;\n\n const deltaX = clientX - resizeState.startX;\n\n // Calculate the actual delta we can apply based on constraints\n const actualDelta = Math.min(\n deltaX,\n resizeState.originalWidths.next - minColumnWidth // Don't shrink next below minimum\n );\n\n const finalCurrentWidth = resizeState.originalWidths.current + actualDelta;\n const finalNextWidth = resizeState.originalWidths.next - actualDelta;\n\n // Apply to DOM for immediate visual feedback\n currentColumnRef.current.style.width = `${finalCurrentWidth}px`;\n nextColumnRef.current.style.width = `${finalNextWidth}px`;\n\n return { finalCurrentWidth, finalNextWidth };\n },\n [resizeState, minColumnWidth]\n );\n\n const endResize = useCallback(() => {\n if (!resizeState || !currentColumnRef.current || !nextColumnRef.current) return;\n\n // Reset global styles\n document.body.style.cursor = 'initial';\n document.body.style.userSelect = 'initial';\n\n // Get final widths\n const currentWidth = currentColumnRef.current.getBoundingClientRect().width;\n const nextWidth = nextColumnRef.current.getBoundingClientRect().width;\n\n // Update through callback\n const updates = [{ accessor: resizeState.currentAccessor, width: `${currentWidth}px` }];\n\n if (resizeState.nextAccessor) {\n updates.push({ accessor: resizeState.nextAccessor, width: `${nextWidth}px` });\n }\n\n onColumnResize(updates);\n\n // Clean up\n setResizeState(null);\n currentColumnRef.current = null;\n nextColumnRef.current = null;\n }, [resizeState, onColumnResize]);\n\n const cancelResize = useCallback(() => {\n if (!resizeState) return;\n\n // Reset global styles\n document.body.style.cursor = 'initial';\n document.body.style.userSelect = 'initial';\n\n // Reset column widths to original values\n if (currentColumnRef.current) {\n currentColumnRef.current.style.width = `${resizeState.originalWidths.current}px`;\n }\n if (nextColumnRef.current) {\n nextColumnRef.current.style.width = `${resizeState.originalWidths.next}px`;\n }\n\n // Clean up\n setResizeState(null);\n currentColumnRef.current = null;\n nextColumnRef.current = null;\n }, [resizeState]);\n\n const resetColumnWidths = useCallback(\n (currentAccessor: string, nextAccessor?: string) => {\n const updates = [{ accessor: currentAccessor, width: 'initial' }];\n if (nextAccessor) {\n updates.push({ accessor: nextAccessor, width: 'initial' });\n }\n onColumnResize(updates);\n },\n [onColumnResize]\n );\n\n return {\n isResizing: !!resizeState,\n startResize,\n updateResize,\n endResize,\n cancelResize,\n resetColumnWidths,\n };\n};\n","import { useLocalStorage } from '@mantine/hooks';\nimport type { DataTableColumn } from '../types/DataTableColumn';\n\n/**\n * Hook to handle column reordering with localStorage persistence.\n * @see https://icflorescu.github.io/mantine-datatable/examples/column-dragging-and-toggling/\n */\nexport function useDataTableColumnReorder<T>({\n key,\n columns = [],\n getInitialValueInEffect = true,\n}: {\n /**\n * The key to use in localStorage to store the columns order.\n */\n key: string | undefined;\n /**\n * Columns definitions.\n */\n columns: DataTableColumn<T>[];\n /**\n * If set to true, value will be updated in useEffect after mount.\n * @default true\n */\n getInitialValueInEffect?: boolean;\n}) {\n // Align order with current columns definition\n function alignColumnsOrder<T>(columnsOrder: string[], columns: DataTableColumn<T>[]) {\n const updatedColumnsOrder: string[] = [];\n\n // Keep existing order for columns that still exist\n columnsOrder.forEach((col) => {\n if (columns.find((c) => c.accessor === col)) {\n updatedColumnsOrder.push(col);\n }\n });\n\n // Add new columns to the end\n columns.forEach((col) => {\n if (!updatedColumnsOrder.includes(col.accessor as string)) {\n updatedColumnsOrder.push(col.accessor as string);\n }\n });\n\n return updatedColumnsOrder;\n }\n\n // Default columns order is the order of the columns in the array\n const defaultColumnsOrder = (columns && columns.map((column) => column.accessor)) || [];\n\n const [columnsOrder, _setColumnsOrder] = useLocalStorage<string[]>({\n key: key ? `${key}-columns-order` : '',\n defaultValue: key ? (defaultColumnsOrder as string[]) : undefined,\n getInitialValueInEffect,\n });\n\n function setColumnsOrder(order: string[] | ((prev: string[]) => string[])) {\n if (key) {\n _setColumnsOrder(order);\n }\n }\n\n const resetColumnsOrder = () => {\n setColumnsOrder(defaultColumnsOrder as string[]);\n };\n\n // If no key is provided, return unmanaged state\n if (!key) {\n return {\n columnsOrder: columnsOrder as string[],\n setColumnsOrder,\n resetColumnsOrder,\n } as const;\n }\n\n // Align order with current columns\n const alignedColumnsOrder = alignColumnsOrder(columnsOrder, columns);\n const prevColumnsOrder = JSON.stringify(columnsOrder);\n\n if (JSON.stringify(alignedColumnsOrder) !== prevColumnsOrder) {\n setColumnsOrder(alignedColumnsOrder);\n }\n\n return {\n columnsOrder: alignedColumnsOrder,\n setColumnsOrder,\n resetColumnsOrder,\n } as const;\n}\n","import { useLocalStorage } from '@mantine/hooks';\nimport { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from 'react';\nimport type { DataTableColumn } from '../types/DataTableColumn';\n\ntype DataTableColumnWidth = Record<string, string | number>;\n\n/**\n * Hook to handle column resizing with localStorage persistence and auto-resize calculation.\n * @see https://icflorescu.github.io/mantine-datatable/examples/column-resizing/\n */\nexport function useDataTableColumnResize<T>({\n key,\n columns = [],\n getInitialValueInEffect = true,\n headerRef,\n onFixedLayoutChange,\n}: {\n /**\n * The key to use in localStorage to store the columns width.\n */\n key: string | undefined;\n /**\n * Columns definitions.\n */\n columns: DataTableColumn<T>[];\n /**\n * If set to true, value will be updated in useEffect after mount.\n * @default true\n */\n getInitialValueInEffect?: boolean;\n /**\n * Reference to the table header element for measuring column widths.\n */\n headerRef?: RefObject<HTMLTableSectionElement | null>;\n /**\n * Reference to the scroll viewport for calculating overflow.\n */\n scrollViewportRef?: RefObject<HTMLElement | null>;\n /**\n * Callback to control fixed layout state in the parent component.\n */\n onFixedLayoutChange?: (enabled: boolean) => void;\n}) {\n const isInitializedRef = useRef(false);\n const naturalWidthsRef = useRef<Record<string, number>>({});\n const [isSSR, setIsSSR] = useState(true);\n\n // Check if columns have resizable feature\n const hasResizableColumns = useMemo(() => {\n return columns.some((c) => c.resizable && !c.hidden && c.accessor !== '__selection__');\n }, [columns]);\n\n // Get resizable columns\n const resizableColumns = useMemo(() => {\n return columns.filter((c) => c.resizable && !c.hidden && c.accessor !== '__selection__');\n }, [columns]);\n\n // Check if we need to measure natural widths (columns without explicit width)\n const needsNaturalMeasurement = useMemo(() => {\n return resizableColumns.some((c) => c.width === undefined || c.width === '' || c.width === 'initial');\n }, [resizableColumns]);\n\n // Create default column widths - use explicit widths or 'auto' for natural sizing\n // Exclude selection column from width management\n const getDefaultColumnsWidth = useCallback(() => {\n return columns\n .filter((column) => column.accessor !== '__selection__')\n .map((column) => ({\n [column.accessor]: column.width ?? 'auto',\n }));\n }, [columns]);\n\n const [storedColumnsWidth, setStoredColumnsWidth] = useLocalStorage<DataTableColumnWidth[]>({\n key: key ? `${key}-columns-width` : '',\n defaultValue: key ? getDefaultColumnsWidth() : undefined,\n getInitialValueInEffect: false, // We'll handle initialization manually\n });\n\n // Current effective column widths (combines stored + measured natural widths)\n const [effectiveColumnsWidth, setEffectiveColumnsWidth] = useState<DataTableColumnWidth[]>(() =>\n getDefaultColumnsWidth()\n );\n\n // Handle SSR\n useEffect(() => {\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setIsSSR(false);\n }, []);\n\n // Measure natural widths of columns\n const measureNaturalWidths = useCallback(() => {\n if (!headerRef?.current || isSSR) return {};\n\n const thead = headerRef.current;\n const headerCells = Array.from(thead.querySelectorAll<HTMLTableCellElement>('th[data-accessor]'));\n const naturalWidths: Record<string, number> = {};\n\n headerCells.forEach((cell) => {\n const accessor = cell.getAttribute('data-accessor');\n if (!accessor || accessor === '__selection__') return;\n\n const column = resizableColumns.find((c) => c.accessor === accessor);\n if (!column) return;\n\n // Only measure if column doesn't have explicit width\n if (column.width === undefined || column.width === '' || column.width === 'initial') {\n const rect = cell.getBoundingClientRect();\n naturalWidths[accessor] = Math.round(rect.width);\n }\n });\n\n return naturalWidths;\n }, [headerRef, resizableColumns, isSSR]);\n\n // Update column widths (both stored and effective)\n // Filter out selection column from updates\n const updateColumnWidths = useCallback(\n (updates: Array<{ accessor: string; width: string | number }>) => {\n // Filter out any updates to the selection column\n const filteredUpdates = updates.filter((update) => update.accessor !== '__selection__');\n\n const newWidths = effectiveColumnsWidth.map((column) => {\n const accessor = Object.keys(column)[0];\n const update = filteredUpdates.find((u) => u.accessor === accessor);\n\n if (update) {\n return { [accessor]: update.width };\n }\n return column;\n });\n\n setEffectiveColumnsWidth(newWidths);\n\n // Also update stored widths if we have a key\n if (key) {\n setStoredColumnsWidth(newWidths);\n }\n },\n [effectiveColumnsWidth, key, setStoredColumnsWidth]\n );\n\n const setMultipleColumnWidths = useCallback(\n (updates: Array<{ accessor: string; width: string | number }>) => {\n updateColumnWidths(updates);\n },\n [updateColumnWidths]\n );\n\n // Initialize column widths (measure natural widths and apply stored widths)\n const initializeColumnWidths = useCallback(() => {\n if (!headerRef?.current || !onFixedLayoutChange || isSSR) return;\n\n // First, measure natural widths if needed\n if (needsNaturalMeasurement) {\n // Temporarily use auto layout to get natural widths\n onFixedLayoutChange(false);\n\n // Wait for layout to settle, then measure\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n const naturalWidths = measureNaturalWidths();\n naturalWidthsRef.current = { ...naturalWidthsRef.current, ...naturalWidths };\n\n // Create effective widths combining stored and natural widths\n // Exclude selection column from width management\n const newEffectiveWidths = columns\n .filter((column) => column.accessor !== '__selection__')\n .map((column) => {\n const accessor = column.accessor as string;\n\n // Check if we have a stored width for this column\n const storedWidth = storedColumnsWidth?.find((w) => Object.keys(w)[0] === accessor);\n if (storedWidth && storedWidth[accessor] !== 'auto') {\n return { [accessor]: storedWidth[accessor] };\n }\n\n // Use natural width if available, otherwise use column definition or auto\n const naturalWidth = naturalWidths[accessor];\n if (naturalWidth) {\n return { [accessor]: `${naturalWidth}px` };\n }\n\n return { [accessor]: column.width ?? 'auto' };\n });\n\n setEffectiveColumnsWidth(newEffectiveWidths);\n\n // Switch to fixed layout for resizing\n setTimeout(() => {\n onFixedLayoutChange(true);\n isInitializedRef.current = true;\n }, 10);\n });\n });\n } else {\n // All columns have explicit widths, use them directly\n // Exclude selection column from width management\n const explicitWidths = columns\n .filter((column) => column.accessor !== '__selection__')\n .map((column) => ({\n [column.accessor]: column.width ?? 'auto',\n }));\n\n setEffectiveColumnsWidth(explicitWidths);\n onFixedLayoutChange(true);\n isInitializedRef.current = true;\n }\n }, [\n headerRef,\n onFixedLayoutChange,\n isSSR,\n needsNaturalMeasurement,\n measureNaturalWidths,\n columns,\n storedColumnsWidth,\n ]);\n\n const measureAndSetColumnWidths = initializeColumnWidths;\n\n // Initialize on mount and when columns change\n useEffect(() => {\n if (!hasResizableColumns || !onFixedLayoutChange || isSSR) {\n onFixedLayoutChange?.(false);\n return;\n }\n\n // Reset initialization flag when columns change\n isInitializedRef.current = false;\n\n // Initialize after a short delay to ensure DOM is ready\n const timeoutId = setTimeout(() => {\n initializeColumnWidths();\n }, 50);\n\n return () => clearTimeout(timeoutId);\n }, [hasResizableColumns, onFixedLayoutChange, isSSR, initializeColumnWidths]);\n\n // Load stored widths on client-side hydration\n useEffect(() => {\n if (isSSR || !key || !getInitialValueInEffect) return;\n\n // Apply stored widths if available\n if (storedColumnsWidth && storedColumnsWidth.length > 0) {\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setEffectiveColumnsWidth(storedColumnsWidth);\n }\n }, [isSSR, key, getInitialValueInEffect, storedColumnsWidth]);\n // Reset all columns to their natural/initial widths\n const resetColumnsWidth = useCallback(() => {\n // Clear stored widths\n if (key) {\n setStoredColumnsWidth(getDefaultColumnsWidth());\n }\n\n // Reset to natural widths\n naturalWidthsRef.current = {};\n isInitializedRef.current = false;\n\n // Re-initialize to measure natural widths\n if (onFixedLayoutChange) {\n onFixedLayoutChange(false);\n setTimeout(() => {\n initializeColumnWidths();\n }, 10);\n }\n }, [key, setStoredColumnsWidth, getDefaultColumnsWidth, onFixedLayoutChange, initializeColumnWidths]);\n\n // Set width for a single column\n const setColumnWidth = useCallback(\n (accessor: string, width: string | number) => {\n updateColumnWidths([{ accessor, width }]);\n },\n [updateColumnWidths]\n );\n\n // Check if all resizable columns are using auto/natural widths\n const allResizableWidthsInitial = useMemo(() => {\n if (!hasResizableColumns) return false;\n return effectiveColumnsWidth\n .filter((colWidth) => {\n const accessor = Object.keys(colWidth)[0];\n return resizableColumns.some((c) => c.accessor === accessor);\n })\n .every((colWidth) => {\n const width = Object.values(colWidth)[0];\n return width === 'auto' || width === 'initial';\n });\n }, [hasResizableColumns, effectiveColumnsWidth, resizableColumns]);\n\n return {\n columnsWidth: effectiveColumnsWidth,\n setColumnsWidth: updateColumnWidths,\n setColumnWidth,\n setMultipleColumnWidths,\n resetColumnsWidth,\n hasResizableColumns,\n allResizableWidthsInitial,\n measureAndSetColumnWidths,\n } as const;\n}\n","import { useMemo, type RefObject } from 'react';\nimport type { DataTableColumn } from '../types/DataTableColumn';\nimport { useDataTableColumnReorder } from './useDataTableColumnReorder';\nimport { useDataTableColumnResize } from './useDataTableColumnResize';\nimport { useDataTableColumnToggle, type DataTableColumnToggle } from './useDataTableColumnToggle';\n\nexport type { DataTableColumnToggle };\n\n/**\n * Hook to handle column features such as drag-and-drop reordering, visibility toggling and resizing.\n * @see https://icflorescu.github.io/mantine-datatable/examples/column-dragging-and-toggling/\n */\nexport const useDataTableColumns = <T>({\n key,\n columns = [],\n getInitialValueInEffect = true,\n headerRef,\n scrollViewportRef,\n onFixedLayoutChange,\n}: {\n /**\n * The key to use in localStorage to store the columns order and toggle state.\n */\n key: string | undefined;\n /**\n * Columns definitions.\n */\n columns: DataTableColumn<T>[];\n /**\n * If set to true, value will be updated in useEffect after mount.\n * @default true\n */\n getInitialValueInEffect?: boolean;\n /**\n * Reference to the table header element for measuring column widths.\n */\n headerRef?: RefObject<HTMLTableSectionElement | null>;\n /**\n * Reference to the scroll viewport for calculating overflow.\n */\n scrollViewportRef?: RefObject<HTMLElement | null>;\n /**\n * Callback to control fixed layout state in the parent component.\n */\n onFixedLayoutChange?: (enabled: boolean) => void;\n}) => {\n // Use specialized hooks for each feature\n const { columnsOrder, setColumnsOrder, resetColumnsOrder } = useDataTableColumnReorder({\n key,\n columns,\n getInitialValueInEffect,\n });\n\n const { columnsToggle, setColumnsToggle, resetColumnsToggle } = useDataTableColumnToggle({\n key,\n columns,\n getInitialValueInEffect,\n });\n\n const {\n columnsWidth,\n setColumnsWidth,\n setColumnWidth,\n setMultipleColumnWidths,\n resetColumnsWidth,\n hasResizableColumns,\n allResizableWidthsInitial,\n measureAndSetColumnWidths,\n } = useDataTableColumnResize({\n key,\n columns,\n getInitialValueInEffect,\n headerRef,\n scrollViewportRef,\n onFixedLayoutChange,\n });\n\n // Compute effective columns based on order, toggle, and width\n const effectiveColumns = useMemo(() => {\n if (!columnsOrder) {\n return columns;\n }\n\n const result = columnsOrder\n .map((order) => columns.find((column) => column.accessor === order))\n .map((column) => {\n return {\n ...column,\n hidden:\n column?.hidden ||\n !columnsToggle.find((toggle) => {\n return toggle.accessor === column?.accessor;\n })?.toggled,\n };\n }) as DataT