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 • 162 kB
Source Map (JSON)
{"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/useDataTableColumns.ts","../package/hooks/useElementOuterSize.ts","../package/hooks/useIsomorphicLayoutEffect.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 { useDebouncedCallback, useMergedRef } from '@mantine/hooks';\nimport clsx from 'clsx';\nimport { useCallback, useMemo, 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 useElementOuterSize,\n useIsomorphicLayoutEffect,\n useLastSelectionChangeIndex,\n useRowExpansion,\n} from './hooks';\nimport type { DataTableProps } from './types';\nimport { TEXT_SELECTION_DISABLED } from './utilityClasses';\nimport { differenceBy, 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 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 {\n ref: localScrollViewportRef,\n width: scrollViewportWidth,\n height: scrollViewportHeight,\n } = useElementOuterSize<HTMLDivElement>();\n\n const effectiveColumns = useMemo(() => {\n return groups?.flatMap((group) => group.columns) ?? columns!;\n }, [columns, groups]);\n\n const dragToggle = useDataTableColumns({\n key: storeColumnsKey,\n columns: effectiveColumns,\n });\n\n const { ref: headerRef, height: headerHeight } = useElementOuterSize<HTMLTableSectionElement>();\n const { ref: localTableRef, width: tableWidth, height: tableHeight } = useElementOuterSize<HTMLTableElement>();\n const { ref: footerRef, height: footerHeight } = useElementOuterSize<HTMLTableSectionElement>();\n const { ref: paginationRef, height: paginationHeight } = useElementOuterSize<HTMLDivElement>();\n const { ref: selectionColumnHeaderRef, width: selectionColumnWidth } = useElementOuterSize<HTMLTableCellElement>();\n\n const mergedTableRef = useMergedRef(localTableRef, tableRef);\n const mergedViewportRef = useMergedRef(localScrollViewportRef, scrollViewportRef);\n\n const [scrolledToTop, setScrolledToTop] = useState(true);\n const [scrolledToBottom, setScrolledToBottom] = useState(true);\n const [scrolledToLeft, setScrolledToLeft] = useState(true);\n const [scrolledToRight, setScrolledToRight] = useState(true);\n\n const rowExpansionInfo = useRowExpansion<T>({ rowExpansion, records, idAccessor });\n\n const processScrolling = useCallback(() => {\n const scrollTop = localScrollViewportRef.current?.scrollTop ?? 0;\n const scrollLeft = localScrollViewportRef.current?.scrollLeft ?? 0;\n\n if (fetching || tableHeight <= scrollViewportHeight) {\n setScrolledToTop(true);\n setScrolledToBottom(true);\n } else {\n const newScrolledToTop = scrollTop === 0;\n const newScrolledToBottom = tableHeight - scrollTop - scrollViewportHeight < 1;\n setScrolledToTop(newScrolledToTop);\n setScrolledToBottom(newScrolledToBottom);\n if (newScrolledToTop && newScrolledToTop !== scrolledToTop) onScrollToTop?.();\n if (newScrolledToBottom && newScrolledToBottom !== scrolledToBottom) onScrollToBottom?.();\n }\n\n if (fetching || tableWidth === scrollViewportWidth) {\n setScrolledToLeft(true);\n setScrolledToRight(true);\n } else {\n const newScrolledToLeft = scrollLeft === 0;\n const newScrolledToRight = tableWidth - scrollLeft - scrollViewportWidth < 1;\n setScrolledToLeft(newScrolledToLeft);\n setScrolledToRight(newScrolledToRight);\n if (newScrolledToLeft && newScrolledToLeft !== scrolledToLeft) onScrollToLeft?.();\n if (newScrolledToRight && newScrolledToRight !== scrolledToRight) onScrollToRight?.();\n }\n }, [\n fetching,\n onScrollToBottom,\n onScrollToLeft,\n onScrollToRight,\n onScrollToTop,\n scrollViewportHeight,\n localScrollViewportRef,\n scrollViewportWidth,\n scrolledToBottom,\n scrolledToLeft,\n scrolledToRight,\n scrolledToTop,\n tableHeight,\n tableWidth,\n ]);\n\n useIsomorphicLayoutEffect(processScrolling, [processScrolling]);\n\n const debouncedProcessScrolling = useDebouncedCallback(processScrolling, 50);\n\n const handleScrollPositionChange = useCallback(\n (e: { x: number; y: number }) => {\n onScroll?.(e);\n debouncedProcessScrolling();\n },\n [debouncedProcessScrolling, onScroll]\n );\n\n const handlePageChange = useCallback(\n (page: number) => {\n localScrollViewportRef.current?.scrollTo({ top: 0, left: 0 });\n onPageChange!(page);\n },\n [onPageChange, localScrollViewportRef]\n );\n\n const recordsLength = records?.length;\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 && !scrolledToLeft && !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 {...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 topShadowVisible={!scrolledToTop}\n leftShadowVisible={!scrolledToLeft}\n leftShadowBehind={selectionColumnVisible || !!pinFirstColumn}\n rightShadowVisible={!scrolledToRight}\n rightShadowBehind={pinLastColumn}\n bottomShadowVisible={!scrolledToBottom}\n headerHeight={headerHeight}\n footerHeight={footerHeight}\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-last-row-border-bottom-visible':\n otherProps.withRowBorders && tableHeight < scrollViewportHeight,\n 'mantine-datatable-pin-last-column': pinLastColumn,\n 'mantine-datatable-pin-last-column-scrolled': !scrolledToRight && pinLastColumn,\n 'mantine-datatable-selection-column-visible': selectionColumnVisible,\n 'mantine-datatable-pin-first-column': pinFirstColumn,\n 'mantine-datatable-pin-first-column-scrolled': !scrolledToLeft && pinFirstColumn,\n },\n classNames?.table\n )}\n style={{\n ...styles?.table,\n '--mantine-datatable-selection-column-width': `${selectionColumnWidth}px`,\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={headerRef}\n selectionColumnHeaderRef={selectionColumnHeaderRef}\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 />\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={footerRef}\n className={classNames?.footer}\n style={styles?.footer}\n columns={effectiveColumns}\n defaultColumnProps={defaultColumnProps}\n selectionVisible={selectionColumnVisible}\n selectorCellShadowVisible={selectorCellShadowVisible}\n scrollDiff={tableHeight - scrollViewportHeight}\n />\n )}\n </Table>\n </TableWrapper>\n </DataTableScrollArea>\n\n {page && (\n <DataTablePagination\n ref={paginationRef}\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 noRecordsText={noRecordsText}\n loadingText={loadingText}\n recordsLength={recordsLength}\n />\n )}\n <DataTableLoader\n pt={headerHeight}\n pb={paginationHeight}\n fetching={fetching}\n backgroundBlur={loaderBackgroundBlur}\n customContent={customLoader}\n size={loaderSize}\n type={loaderType}\n color={loaderColor}\n />\n <DataTableEmptyState\n pt={headerHeight}\n pb={paginationHeight}\n icon={noRecordsIcon}\n text={noRecordsText}\n active={!fetching && !recordsLength}\n >\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 { 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 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 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 resetColumnsWidth,\n }}\n >\n {children}\n </DataTableColumnsContextProvider>\n );\n};\n","import { createSafeContext } from '@mantine/core';\nimport { Dispatch, SetStateAction } from 'react';\nimport { 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 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, type MantineSpacing, type StyleProp } from '@mantine/core';\nimport { IconDatabaseOff } from './icons/IconDatabaseOff';\n\ntype DataTableEmptyStateProps = React.PropsWithChildren<{\n icon: React.ReactNode | undefined;\n text: string;\n pt: StyleProp<MantineSpacing>;\n pb: StyleProp<MantineSpacing>;\n active: boolean;\n}>;\n\nexport function DataTableEmptyState({ icon, text, pt, pb, active, children }: DataTableEmptyStateProps) {\n return (\n <Center pt={pt} pb={pb} 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, rem, type MantineStyleProp } from '@mantine/core';\nimport clsx from 'clsx';\nimport { forwardRef, type JSX } from 'react';\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 scrollDiff: number;\n};\n\nexport const DataTableFooter = forwardRef(function DataTableFooter<T>(\n {\n className,\n style,\n columns,\n defaultColumnProps,\n selectionVisible,\n selectorCellShadowVisible,\n scrollDiff,\n }: DataTableFooterProps<T>,\n ref: React.ForwardedRef<HTMLTableSectionElement>\n) {\n const relative = scrollDiff < 0;\n return (\n <TableTfoot\n ref={ref}\n className={clsx('mantine-datatable-footer', className)}\n style={[\n {\n position: relative ? 'relative' : 'sticky',\n bottom: rem(relative ? scrollDiff : 0),\n },\n style,\n ]}\n >\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}) as <T>(props: DataTableFooterProps<T> & { ref: React.ForwardedRef<HTMLTableSectionElement> }) => JSX.Element;\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 { useLocalStorage } from '@mantine/hooks';\nimport { useMemo } from 'react';\nimport type { DataTableColumn } from '../types/DataTableColumn';\n\nexport type DataTableColumnToggle = {\n accessor: string;\n defaultToggle: boolean;\n toggleable: boolean;\n toggled: boolean;\n};\n\ntype DataTableColumnWidth = Record<string, string | number>;\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}: {\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 * Columns definitions.\n */\n /**\n * If set to true, value will be update is useEffect after mount.\n * @default true\n */\n getInitialValueInEffect?: boolean;\n}) => {\n // align order\n function alignColumnsOrder<T>(columnsOrder: string[], columns: DataTableColumn<T>[]) {\n const updatedColumnsOrder: string[] = [];\n columnsOrder.forEach((col) => {\n if (columns.find((c) => c.accessor === col)) {\n updatedColumnsOrder.push(col);\n }\n });\n columns.forEach((col) => {\n if (!updatedColumnsOrder.includes(col.accessor as string)) {\n updatedColumnsOrder.push(col.accessor as string);\n }\n });\n return updatedColumnsOrder;\n }\n\n // align toggle\n function alignColumnsToggle<T>(columnsToggle: DataTableColumnToggle[], columns: DataTableColumn<T>[]) {\n const updatedColumnsToggle: DataTableColumnToggle[] = [];\n columnsToggle.forEach((col) => {\n if (columns.find((c) => c.accessor === col.accessor)) {\n updatedColumnsToggle.push(col);\n }\n });\n columns.forEach((col) => {\n if (!updatedColumnsToggle.find((c) => c.accessor === col.accessor)) {\n updatedColumnsToggle.push({\n accessor: col.accessor as string,\n defaultToggle: col.defaultToggle || true,\n toggleable: col.toggleable as boolean,\n toggled: col.defaultToggle === undefined ? true : col.defaultToggle,\n });\n }\n });\n return updatedColumnsToggle as DataTableColumnToggle[];\n }\n\n // align width\n function alignColumnsWidth<T>(columnsWidth: DataTableColumnWidth[], columns: DataTableColumn<T>[]) {\n const updatedColumnsWidth: DataTableColumnWidth[] = [];\n\n columnsWidth.forEach((col) => {\n const accessor = Object.keys(col)[0];\n if (columns.find((c) => c.accessor === accessor)) {\n updatedColumnsWidth.push(col);\n }\n });\n\n columns.forEach((col) => {\n const accessor = col.accessor;\n if (!updatedColumnsWidth.find((c) => Object.keys(c)[0] === accessor)) {\n const widthObj: DataTableColumnWidth = {};\n widthObj[accessor as string] = '';\n updatedColumnsWidth.push(widthObj);\n }\n });\n\n return updatedColumnsWidth;\n }\n\n // align order\n function useAlignColumnsOrder() {\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 if (!key) {\n return [columnsOrder, setColumnsOrder] as const;\n }\n\n const alignedColumnsOrder = alignColumnsOrder(columnsOrder, columns);\n\n const prevColumnsOrder = JSON.stringify(columnsOrder);\n\n if (JSON.stringify(alignedColumnsOrder) !== prevColumnsOrder) {\n setColumnsOrder(alignedColumnsOrder);\n }\n\n return [alignedColumnsOrder, setColumnsOrder] as const;\n }\n\n function useAlignColumnsToggle() {\n const [columnsToggle, _setColumnsToggle] = useLocalStorage<DataTableColumnToggle[]>({\n key: key ? `${key}-columns-toggle` : '',\n defaultValue: key ? (defaultColumnsToggle as DataTableColumnToggle[]) : undefined,\n getInitialValueInEffect,\n });\n\n function setColumnsToggle(\n toggle: DataTableColumnToggle[] | ((prev: DataTableColumnToggle[]) => DataTableColumnToggle[])\n ) {\n if (key) {\n _setColumnsToggle(toggle);\n }\n }\n\n if (!key) {\n return [columnsToggle, setColumnsToggle] as const;\n }\n\n const alignedColumnsToggle = alignColumnsToggle(columnsToggle, columns);\n\n const prevColumnsToggle = JSON.stringify(columnsToggle);\n\n if (JSON.stringify(alignedColumnsToggle) !== prevColumnsToggle) {\n setColumnsToggle(alignedColumnsToggle);\n }\n\n return [alignColumnsToggle(columnsToggle, columns), setColumnsToggle] as const;\n }\n\n function useAlignColumnsWidth() {\n const [columnsWidth, _setColumnsWidth] = useLocalStorage<DataTableColumnWidth[]>({\n key: key ? `${key}-columns-width` : '',\n defaultValue: key ? (defaultColumnsWidth as DataTableColumnWidth[]) : undefined,\n getInitialValueInEffect,\n });\n\n function setColumnsWidth(\n width: DataTableColumnWidth[] | ((prev: DataTableColumnWidth[]) => DataTableColumnWidth[])\n ) {\n if (key) {\n _setColumnsWidth(width);\n }\n }\n\n if (!key) {\n return [columnsWidth, setColumnsWidth] as const;\n }\n\n const alignedColumnsWidth = alignColumnsWidth(columnsWidth, columns);\n\n const prevColumnsWidth = JSON.stringify(columnsWidth);\n\n if (JSON.stringify(alignedColumnsWidth) !== prevColumnsWidth) {\n setColumnsWidth(alignedColumnsWidth);\n }\n\n return [alignColumnsWidth(columnsWidth, columns), setColumnsWidth] as const;\n }\n\n // Default columns id ordered is the order of the columns in the array\n const defaultColumnsOrder = (columns && columns.map((column) => column.accessor)) || [];\n\n // create an array of object with key = accessor and value = width\n const defaultColumnsWidth =\n (columns && columns.map((column) => ({ [column.accessor]: column.width ?? 'initial' }))) || [];\n\n // Default columns id toggled is the array of columns which have the toggleable property set to true\n const defaultColumnsToggle =\n columns &&\n columns.map((column) => ({\n accessor: column.accessor,\n defaultToggle: column.defaultToggle || true,\n toggleable: column.toggleable,\n toggled: column.defaultToggle === undefined ? true : column.defaultToggle,\n }));\n\n // Store the columns order in localStorage\n const [columnsOrder, setColumnsOrder] = useAlignColumnsOrder();\n\n // Store the columns toggle in localStorage\n const [columnsToggle, setColumnsToggle] = useAlignColumnsToggle();\n\n // Store the columns widths in localStorage\n const [columnsWidth, setColumnsWidth] = useAlignColumnsWidth();\n\n // we won't use the \"remove\" function from useLocalStorage() because\n // we got issue with rendering\n const resetColumnsOrder = () => setColumnsOrder(defaultColumnsOrder as string[]);\n\n const resetColumnsToggle = () => {\n setColumnsToggle(defaultColumnsToggle as DataTableColumnToggle[]);\n };\n\n const resetColumnsWidth = () => setColumnsWidth(defaultColumnsWidth as DataTableColumnWidth[]);\n\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 DataTableColumn<T>[];\n\n const newWidths = result.map((column) => {\n return {\n ...column,\n width: columnsWidth.find((width) => {\n return width[column?.accessor as string];\n })?.[column?.accessor as string],\n };\n });\n\n return newWidths;\n }, [columns, columnsOrder, columnsToggle, columnsWidth]);\n\n const setColumnWidth = (accessor: string, width: string | number) => {\n const newColumnsWidth = columnsWidth.map((column) => {\n if (!column[accessor]) {\n return column;\n }\n return {\n [accessor]: width,\n };\n });\n\n setColumnsWidth(newColumnsWidth);\n };\n\n return {\n effectiveColumns: effectiveColumns as DataTableColumn<T>[],\n\n // Order handling\n setColumnsOrder,\n columnsOrder: columnsOrder as string[],\n resetColumnsOrder,\n\n // Toggle handling\n columnsToggle: columnsToggle as DataTableColumnToggle[],\n setColumnsToggle,\n resetColumnsToggle,\n\n // Resize handling\n columnsWidth,\n setColumnsWidth,\n setColumnWidth,\n resetColumnsWidth,\n } as const;\n};\n","import { useResizeObserver } from '@mantine/hooks';\n\nexport function useElementOuterSize<T extends HTMLElement>() {\n const [ref] = useResizeObserver<T>();\n const { width, height } = ref.current?.getBoundingClientRect() || { width: 0, height: 0 };\n return { ref, width, height };\n}\n","import { useEffect, useLayoutEffect } from 'react';\n\nexport const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n","import { useEffect, useState } from 'react';\n\nexport function useLastSelectionChangeIndex(recordIds: unknown[] | undefined) {\n const [lastSelectionChangeIndex, setLastSelectionChangeIndex] = useState<number | null>(null);\n const recordIdsString = recordIds?.join(':') || '';\n useEffect(() => {\n setLastSelectionChangeIndex(null);\n }, [recordIdsString]);\n\n return { lastSelectionChangeIndex, setLastSelectionChangeIndex };\n}\n","// Modified from https://github.com/mantinedev/mantine/blob/8c12a76c56da51af34213f18dd67c8b72a0ddb44/src/mantine-hooks/src/use-media-query/use-media-query.ts\n\nimport { useEffect, useRef, useState } from 'react';\n\nexport interface UseMediaQueryOptions {\n getInitialValueInEffect: boolean;\n}\n\n/**\n * Older versions of Safari (shipped with Catalina and before) do not support addEventListener on matchMedia\n * https://stackoverflow.com/questions/56466261/matchmedia-addlistener-marked-as-deprecated-addeventlistener-equivalent\n * */\nfunction attachMediaListeners(queries: MediaQueryList[], callback: (matches: boolean[]) => void) {\n const callbackWrapper = () => {\n callback(queries.map((query) => query.matches));\n };\n const subscriptions = queries.map((query) => {\n try {\n query.addEventListener('change', callbackWrapper);\n return () => query.removeEventListener('change', callbackWrapper);\n } catch {\n query.addListener(callbackWrapper);\n return () => query.removeListener(callbackWrapper);\n }\n });\n return () => {\n subscriptions.forEach((unsubscribe) => unsubscribe());\n };\n}\n\nfunction getInitialValue(queries: string[], initialValues?: boolean[]) {\n if (initialValues) {\n return initialValues;\n }\n\n if (typeof window !== 'undefined' && 'matchMedia' in window) {\n return queries.map((query) => window.matchMedia(query).matches);\n }\n\n return queries.map(() => false);\n}\n\nexport function useMediaQueries(\n queries: string[],\n initialValues?: boolean[],\n { getInitialValueInEffect }: UseMediaQueryOptions = {\n getInitialValueInEffect: true,\n }\n) {\n const [matches, setMatches] = useState(\n getInitialValueInEffect ? initialValues : getInitialValue(queries, initialValues)\n );\n const queryRef = useRef<MediaQueryList[]>(null);\n\n useEffect(() => {\n if ('matchMedia' in window) {\n queryRef.current = queries.map((query) => window.matchMedia(query));\n setMatches(queryRef.current.map((queryResult) => queryResult.matches));\n return attachMediaListeners(queryRef.current, (event) => {\n setMatches(event);\n });\n }\n\n return undefined;\n }, [queries]);\n\n return matches;\n}\n","import { useMantineTheme, type MantineTheme } from '@mantine/core';\nimport { useMemo } from 'react';\nimport { useMediaQueries } from './useMediaQueries';\n\nexport function useMediaQueriesStringOrFunction(queries: (string | ((theme: MantineTheme) => string) | undefined)[]) {\n const theme = useMantineTheme();\n const values = useMemo(\n () => queries.map((query) => (typeof query === 'function' ? query(theme) : query) ?? ''),\n [queries, theme]\n );\n const defaults = useMemo(() => queries.map(() => true), [queries]);\n return useMediaQueries(values, defaults);\n}\n","import { useMantineTheme, type MantineTheme } from '@mantine/core';\nimport { useMediaQuery } from '@mantine/hooks';\n\nexport function useMediaQueryStringOrFunction(mediaQuery: string | ((theme: MantineTheme) => string) | undefined) {\n const theme = useMantineTheme();\n const mediaQueryValue = typeof mediaQuery === 'function' ? mediaQuery(theme) : mediaQuery;\n return useMediaQuery(mediaQueryValue || '', true);\n}\n","import { useState } from 'react';\nimport { DataTableRowExpansionProps } from '../types/DataTableRowExpansionProps';\nimport { getRecordId } from '../utils';\n\nexport function useRowExpansion<T>({\n rowExpansion,\n records,\n idAccessor,\n}: {\n rowExpansion?: DataTableRowExpansionProps<T>;\n records: T[] | undefined;\n idAccessor: (keyof T | (string & NonNullable<unknown>)) | ((record: T) => React.Key);\n}) {\n let initiallyExpandedRecordIds: unknown[] = [];\n if (rowExpansion && records) {\n const { trigger, allowMultiple, initiallyExpanded } = rowExpansion;\n if (records && trigger === 'always') {\n initiallyExpandedRecordIds = records.map((r) => getRecordId(r, idAccessor));\n } else if (initiallyExpanded) {\n initiallyExpandedRecordIds = records\n .filter((record, index) => initiallyExpanded({ record, index }))\n .map((r) => getRecordId(r, idAccessor));\n if (!allowMultiple) {\n initiallyExpandedRecordIds = [initiallyExpandedRecordIds[0]];\n }\n }\n }\n\n let expandedRecordIds: unknown[];\n let setExpandedRecordIds: ((expandedRecordIds: unknown[]) => void) | undefined;\n const expandedRecordIdsState = useState<unknown[]>(initiallyExpandedRecordIds);\n\n if (rowExpansion) {\n const { expandable, trigger, allowMultiple, collapseProps, content } = rowExpansion;\n if (rowExpansion.expanded) {\n ({ recordIds: expandedRecordIds, onRecordIdsChange: setExpandedRecordIds } = rowExpansion.expanded);\n } else {\n [expandedRecordIds, setExpandedRecordIds] = expandedRecordIdsState;\n }\n\n const collapseRow = (record: T) =>\n setExpandedRecordIds?.(expandedRecordIds.filter((id) => id !== getRecordId(record, idAccessor)));\n\n return {\n expandOnClick: trigger !== 'always' && trigger !== 'never',\n isRowExpanded: (record: T) =>\n trigger === 'always' ? true : expandedRecordIds.includes(getRecordId(record, idAccessor)),\n isExpandable: ({ record, index }: { record: T; index: number }) => {\n if (!expandable) {\n return true;\n }\n return expandable({ record, index });\n },\n expandRow: (record: T) => {\n const recordId = getRecordId(record, idAccessor);\n setExpandedRecordIds?.(allowMultiple ? [...expandedRecordIds, recordId] : [recordId]);\n },\n collapseRow,\n collapseProps,\n content:\n ({ record, index }: { record: T; index: number }) =>\n () =>\n content({ record, index, collapse: () => collapseRow(record) }),\n };\n }\n}\n","import type { DropResult } from '@hello-pangea/dnd';\n\n/**\n * Utility function that returns a humanized version of a string, e.g. \"camelCase\" -> \"Camel Case\"\n */\nexport function humanize(value: string) {\n const str = value\n .replace(/([a-z\\d])([A-Z]+)/g, '$1 $2')\n .replace(/\\W|_/g, ' ')\n .trim()\n .toLowerCase();\n return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;\n}\n\n/**\n * Utility function that returns an array of values that are present in the first array but not in the second\n */\nexport function differenceBy<T>(arr1: T[], arr2: T[], iteratee: (value: T) => unknown) {\n return arr1.filter((c) => !arr2.map(iteratee).includes(iteratee(c)));\n}\n\n/**\n * Utility function that returns an array of unique values from a given array\n */\nexport function uniqBy<T>(arr: T[], iteratee: (value: T) => unknown) {\n return arr.filter((x, i, self) => i === self.findIndex((y) => iteratee(x) === iteratee(y)));\n}\n\n/**\n * Utility function that returns the value at a given path in an object\n */\nexport function getValueAtPath<T>(obj: T, path: keyof T | (string & NonNullable<unknown>)) {\n if (!path) return undefined;\n const pathArray = (path as string).match(/([^[.\\]])+/g) as string[];\n return pathArray.reduce((prevObj: unknown, key) => prevObj && (prevObj as Record<string, unknown>)[key], obj);\n}\n\n/**\n * Utility function that returns the record id using idAccessor\n */\nexport function getRecordId<T>(\n record: T,\n idAccessor: keyof T | (string & NonNullable<unknown>) | ((record: T) => React.Key)\n) {\n return typeof idAccessor === 'string'\n ? getValueAtPath(record, idAccessor)\n : (idAccessor as (record: T) => React.Key)(record);\n}\n\n/**\n * Utility function that reorders an array of records by a given field used for drag'n'drop functionality.\n * @see https://github.com/hello-pangea/dnd\n */\nexport function reorderRecords<T>(dropResult: DropResult, records: T[]): T[] {\n const draft = structuredClone(records);\n const prev = draft[dropResult.source.index];\n\n if (dropResult.destination) {\n draft.splice(dropResult.source.index, 1);\n draft.splice(dropResult.destination.index, 0, prev);\n }\n\n return draft;\n}\n\n/**\n * Utility function that swaps elements of an array, by a given result from drag'n'drop functionality.\n * @see https://github.com/hello-pangea/dnd\n */\nexport function swapRecords<T>(dropResult: DropResult, records: T[]): T[] {\n const draft = structuredClone(records);\n\n const destination = dropResult.destination;\n\n if (!destination) return draft;\n\n const sourceEl = draft[dropResult.source.index];\n const destEl = draft[destination.index];\n\n draft.splice(destination.index, 1, sourceEl);\n draft.splice(dropResult.source.index, 1, destEl);\n\n return draft;\n}\n","import { useTimeout } from '@mantine/hooks';\nimport { useEffect, useState } from 'react';\n\nexport function useRowExpansionStatus(open: boolean, transitionDuration?: number) {\n const [expanded, setExpanded] = useState(open);\n const [visible, setVisible] = useState(open);\n\n const expand = useTimeout(() => setExpanded(true), 0);\n const hide = useTimeout(() => setVisible(false), transitionDuration || 200);\n\n useEffect(() => {\n if (open) {\n hide.clear();\n se