@scalepad/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai/rules/date-handling.md +39 -0
- package/.ai/rules/figma-design-system.md +372 -0
- package/.ai/rules/figma-lm-design-system-keys.md +680 -0
- package/.ai/rules/file-extensions.md +13 -0
- package/.ai/rules/modal-confirmation-mutation.md +56 -0
- package/.ai/rules/react-hooks.md +29 -0
- package/.ai/rules/styling.md +83 -0
- package/AGENTS.md +37 -0
- package/README.md +125 -0
- package/figma.config.json +9 -0
- package/package.json +127 -0
- package/scripts/install-ai-rules.mjs +136 -0
- package/src/ThemeProvider.tsx +57 -0
- package/src/charts.ts +32 -0
- package/src/components/ActionCard/ActionCard.css.ts +60 -0
- package/src/components/ActionCard/ActionCard.tsx +154 -0
- package/src/components/ActionCard/index.ts +2 -0
- package/src/components/Anchor/Anchor.tsx +47 -0
- package/src/components/Anchor/index.ts +2 -0
- package/src/components/AppliedFiltersManagerBar/AppliedFiltersManagerBar.tsx +105 -0
- package/src/components/AppliedFiltersManagerBar/FilterBadge.css.ts +23 -0
- package/src/components/AppliedFiltersManagerBar/FilterBadge.tsx +50 -0
- package/src/components/AppliedFiltersManagerBar/index.ts +5 -0
- package/src/components/Badge/Badge.css.ts +72 -0
- package/src/components/Badge/Badge.figma.tsx +43 -0
- package/src/components/Badge/Badge.tsx +159 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/BreadCrumb/BreadCrumb.tsx +62 -0
- package/src/components/BreadCrumb/index.ts +2 -0
- package/src/components/BulkActionBar/BulkActionBar.css.ts +26 -0
- package/src/components/BulkActionBar/BulkActionBar.tsx +164 -0
- package/src/components/BulkActionBar/index.ts +2 -0
- package/src/components/Button/Button.css.ts +272 -0
- package/src/components/Button/Button.figma.tsx +74 -0
- package/src/components/Button/Button.tsx +84 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Charts/ChartTooltip.figma.tsx +33 -0
- package/src/components/Charts/ChartTooltip.tsx +101 -0
- package/src/components/Charts/MiniBarSparkline.tsx +75 -0
- package/src/components/Charts/StackedPatternBarChart.tsx +494 -0
- package/src/components/Charts/TrendAreaChart.css.ts +23 -0
- package/src/components/Charts/TrendAreaChart.tsx +210 -0
- package/src/components/Charts/index.ts +12 -0
- package/src/components/CodePanel/CodePanel.css.ts +113 -0
- package/src/components/CodePanel/CodePanel.tsx +121 -0
- package/src/components/CodePanel/index.ts +2 -0
- package/src/components/CommentComposer/CommentComposer.css.ts +60 -0
- package/src/components/CommentComposer/CommentComposer.tsx +181 -0
- package/src/components/CommentComposer/index.ts +2 -0
- package/src/components/ConfirmationModal/ConfirmationModal.tsx +149 -0
- package/src/components/ConfirmationModal/index.ts +2 -0
- package/src/components/ConfirmationTooltip/ConfirmationTooltip.tsx +132 -0
- package/src/components/ConfirmationTooltip/index.ts +2 -0
- package/src/components/DataDialog.figma.tsx +33 -0
- package/src/components/DataDialog.tsx +46 -0
- package/src/components/DataTable/DataTable.tsx +1042 -0
- package/src/components/DataTable/RowExpandToggle.tsx +105 -0
- package/src/components/DataTable/RowGroupHeader.tsx +190 -0
- package/src/components/DataTable/createActionsColumn.tsx +86 -0
- package/src/components/DataTable/index.ts +25 -0
- package/src/components/DatePicker/CustomRangePicker.tsx +59 -0
- package/src/components/DatePicker/DateInput.tsx +329 -0
- package/src/components/DatePicker/DateNavigator.tsx +486 -0
- package/src/components/DatePicker/DatePicker.tsx +242 -0
- package/src/components/DatePicker/MonthlyRangePicker.tsx +231 -0
- package/src/components/DatePicker/QuarterlyRangePicker.tsx +224 -0
- package/src/components/DatePicker/QuickPicksSidebar.tsx +242 -0
- package/src/components/DatePicker/YearlyRangePicker.tsx +171 -0
- package/src/components/DatePicker/index.ts +7 -0
- package/src/components/DatePicker/types.ts +12 -0
- package/src/components/DesignSystemPrimitives/FluidGrid.tsx +44 -0
- package/src/components/DesignSystemPrimitives/InteractivePrimitives.tsx +177 -0
- package/src/components/DesignSystemPrimitives/LayoutPrimitives.tsx +220 -0
- package/src/components/DesignSystemPrimitives/LayoutPrimitives.types.tsx +15 -0
- package/src/components/DesignSystemPrimitives/SurfacePrimitives.tsx +46 -0
- package/src/components/DesignSystemPrimitives/index.ts +55 -0
- package/src/components/Details/Details.css.ts +74 -0
- package/src/components/Details/Details.tsx +140 -0
- package/src/components/Details/index.ts +2 -0
- package/src/components/DownloadCard/DownloadCard.css.ts +22 -0
- package/src/components/DownloadCard/DownloadCard.tsx +63 -0
- package/src/components/DownloadCard/index.ts +2 -0
- package/src/components/Drawer/Drawer.css.ts +32 -0
- package/src/components/Drawer/Drawer.tsx +236 -0
- package/src/components/Drawer/hooks/useDetailDrawer.ts +61 -0
- package/src/components/Drawer/hooks/useDetailDrawerNavigation.ts +125 -0
- package/src/components/Drawer/hooks/useDetailDrawerNavigationContext.ts +66 -0
- package/src/components/EditableRichText/EditableRichText.css.ts +72 -0
- package/src/components/EditableRichText/EditableRichText.tsx +324 -0
- package/src/components/EditableRichText/index.ts +2 -0
- package/src/components/EditableSelect/EditableSelect.css.ts +62 -0
- package/src/components/EditableSelect/EditableSelect.tsx +224 -0
- package/src/components/EditableSelect/index.ts +2 -0
- package/src/components/EditableText/EditableText.tsx +377 -0
- package/src/components/EditableText/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.figma.tsx +33 -0
- package/src/components/EmptyState/EmptyState.tsx +230 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/ErrorBoundary.tsx +135 -0
- package/src/components/ErrorState/ErrorState.tsx +197 -0
- package/src/components/ErrorState/index.ts +2 -0
- package/src/components/FeatureCard.tsx +42 -0
- package/src/components/FilterMenu/FilterMenu.figma.tsx +30 -0
- package/src/components/FilterMenu/FilterMenu.tsx +198 -0
- package/src/components/FilterMenu/FilterSubMenuTypes/BooleanFilterSubmenu.tsx +46 -0
- package/src/components/FilterMenu/FilterSubMenuTypes/SearchableFilterSubmenu.tsx +239 -0
- package/src/components/FilterMenu/FilterSubMenuTypes/index.ts +8 -0
- package/src/components/FilterMenu/defaultFilterSchemas.ts +63 -0
- package/src/components/FilterMenu/helpers.ts +115 -0
- package/src/components/FilterMenu/index.ts +35 -0
- package/src/components/FilterMenu/types.ts +101 -0
- package/src/components/IconButton/IconButton.css.ts +272 -0
- package/src/components/IconButton/IconButton.figma.tsx +47 -0
- package/src/components/IconButton/IconButton.tsx +72 -0
- package/src/components/IconButton/README.md +230 -0
- package/src/components/IconButton/index.ts +2 -0
- package/src/components/InfiniteScrollSentinel.tsx +86 -0
- package/src/components/InfiniteScrollTrigger.tsx +78 -0
- package/src/components/InfoCard.figma.tsx +47 -0
- package/src/components/InfoCard.tsx +216 -0
- package/src/components/KbdHint/KbdHint.tsx +23 -0
- package/src/components/KbdHint/index.ts +2 -0
- package/src/components/LabeledField/LabeledField.tsx +21 -0
- package/src/components/LabeledField/index.ts +2 -0
- package/src/components/LookupSelect/LookupSelect.css.ts +149 -0
- package/src/components/LookupSelect/LookupSelect.tsx +325 -0
- package/src/components/LookupSelect/index.ts +2 -0
- package/src/components/Menu/Menu.css.ts +89 -0
- package/src/components/Menu/Menu.tsx +105 -0
- package/src/components/Menu/index.ts +2 -0
- package/src/components/MessageBox/MessageBox.tsx +168 -0
- package/src/components/MessageBox/index.ts +2 -0
- package/src/components/MetricDisplay/MetricDisplay.tsx +55 -0
- package/src/components/MetricDisplay/index.ts +1 -0
- package/src/components/MultiSelect/MultiSelect.tsx +278 -0
- package/src/components/MultiSelect/index.ts +2 -0
- package/src/components/Notifications/Notifications.tsx +12 -0
- package/src/components/Notifications/README.md +93 -0
- package/src/components/Notifications/index.ts +4 -0
- package/src/components/Notifications/showToast.tsx +100 -0
- package/src/components/PropertyRow/PropertyRow.tsx +96 -0
- package/src/components/PropertyRow/index.ts +2 -0
- package/src/components/RadioTile/RadioTile.tsx +253 -0
- package/src/components/RadioTile/index.ts +2 -0
- package/src/components/RichText/FormattingToolbar.css.ts +69 -0
- package/src/components/RichText/FormattingToolbar.tsx +112 -0
- package/src/components/RichText/RichTextInline.css.ts +54 -0
- package/src/components/RichText/RichTextInline.tsx +318 -0
- package/src/components/RichText/formattingCommands.ts +181 -0
- package/src/components/RichText/formattingTypes.ts +34 -0
- package/src/components/RichText/index.ts +49 -0
- package/src/components/RichText/richTextExtensions.ts +111 -0
- package/src/components/RichText/richTextHelpers.ts +65 -0
- package/src/components/RichText/richTextImage.ts +253 -0
- package/src/components/RichText/richTextImageHandlers.ts +244 -0
- package/src/components/RichText/richTextProse.css.ts +261 -0
- package/src/components/RichTextEditor/RichTextEditor.css.ts +82 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +204 -0
- package/src/components/RichTextEditor/index.ts +2 -0
- package/src/components/RichTextView/RichTextView.css.ts +11 -0
- package/src/components/RichTextView/RichTextView.tsx +114 -0
- package/src/components/RichTextView/index.ts +2 -0
- package/src/components/Schedule/Schedule.tsx +35 -0
- package/src/components/SchedulePicker/SchedulePicker.css.ts +42 -0
- package/src/components/SchedulePicker/SchedulePicker.tsx +130 -0
- package/src/components/SchedulePicker/index.ts +2 -0
- package/src/components/SearchableList/types.ts +30 -0
- package/src/components/SearchableSubMenu/SearchableSubMenu.css.ts +25 -0
- package/src/components/SearchableSubMenu/SearchableSubMenu.tsx +139 -0
- package/src/components/SearchableSubMenu/index.ts +2 -0
- package/src/components/Select/README.md +114 -0
- package/src/components/Select/Select.css.ts +110 -0
- package/src/components/Select/Select.tsx +133 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/SelectCreatable/SelectCreatable.css.ts +16 -0
- package/src/components/SelectCreatable/SelectCreatable.tsx +203 -0
- package/src/components/SelectCreatable/index.ts +2 -0
- package/src/components/SettingsCard/SettingsCard.tsx +98 -0
- package/src/components/SettingsCard/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.css.ts +91 -0
- package/src/components/Sidebar/Sidebar.tsx +129 -0
- package/src/components/Sidebar/index.ts +5 -0
- package/src/components/SimpleList/SimpleList.css.ts +12 -0
- package/src/components/SimpleList/SimpleList.tsx +44 -0
- package/src/components/SimpleList/index.ts +2 -0
- package/src/components/SimpleTable/SimpleTable.tsx +296 -0
- package/src/components/SimpleTable/index.ts +2 -0
- package/src/components/SlashRichTextEditor/SelectionBubbleMenu.css.ts +62 -0
- package/src/components/SlashRichTextEditor/SelectionBubbleMenu.tsx +85 -0
- package/src/components/SlashRichTextEditor/SlashCommandMenu.css.ts +124 -0
- package/src/components/SlashRichTextEditor/SlashCommandMenu.tsx +168 -0
- package/src/components/SlashRichTextEditor/SlashRichTextEditor.css.ts +81 -0
- package/src/components/SlashRichTextEditor/SlashRichTextEditor.tsx +538 -0
- package/src/components/SlashRichTextEditor/SlashSuggestionExtension.ts +48 -0
- package/src/components/SlashRichTextEditor/index.ts +13 -0
- package/src/components/SlashRichTextEditor/types.ts +48 -0
- package/src/components/StatCard/StatCard.css.ts +70 -0
- package/src/components/StatCard/StatCard.tsx +201 -0
- package/src/components/StatCard/index.ts +1 -0
- package/src/components/StatusBadge/StatusBadge.tsx +70 -0
- package/src/components/StatusBadge/index.ts +2 -0
- package/src/components/StatusIndicator/StatusIndicator.tsx +67 -0
- package/src/components/StatusIndicator/index.ts +6 -0
- package/src/components/SubNavigation/SubNavigation.css.ts +72 -0
- package/src/components/SubNavigation/SubNavigation.tsx +104 -0
- package/src/components/SubNavigation/index.ts +2 -0
- package/src/components/SuspenseLoader.tsx +22 -0
- package/src/components/Table/SortableColumnHeader.tsx +99 -0
- package/src/components/Table/TableSkeletonRows.figma.tsx +22 -0
- package/src/components/Table/TableSkeletonRows.tsx +113 -0
- package/src/components/Table/index.ts +9 -0
- package/src/components/TableActionsMenu.tsx +58 -0
- package/src/components/TableCard.tsx +29 -0
- package/src/components/TableContainer/TableContainer.tsx +86 -0
- package/src/components/TableContainer/index.ts +2 -0
- package/src/components/TableControlBar/TableControlBar.tsx +156 -0
- package/src/components/TableControlBar/TableSelectionButton.tsx +57 -0
- package/src/components/TableControlBar/index.ts +13 -0
- package/src/components/TableControlBar/useTableControlBar.tsx +314 -0
- package/src/components/TableSelection/TableSelection.tsx +43 -0
- package/src/components/TableSelection/index.ts +5 -0
- package/src/components/Tabs/README.md +76 -0
- package/src/components/Tabs/Tabs.css.ts +54 -0
- package/src/components/Tabs/Tabs.figma.tsx +47 -0
- package/src/components/Tabs/Tabs.tsx +96 -0
- package/src/components/Tabs/index.ts +8 -0
- package/src/components/TextInput/README.md +98 -0
- package/src/components/TextInput/SearchTextInput.figma.tsx +22 -0
- package/src/components/TextInput/SearchTextInput.tsx +150 -0
- package/src/components/TextInput/TextInput.figma.tsx +44 -0
- package/src/components/TextInput/TextInput.tsx +42 -0
- package/src/components/TextInput/index.ts +4 -0
- package/src/components/ThemeSwitcher.figma.tsx +28 -0
- package/src/components/ThemeSwitcher.tsx +69 -0
- package/src/components/TrendBadge/TrendBadge.tsx +76 -0
- package/src/components/TrendBadge/index.ts +2 -0
- package/src/components/TruncatedText.tsx +115 -0
- package/src/components/Typography/Text.tsx +74 -0
- package/src/components/Typography/Title.tsx +100 -0
- package/src/components/Typography/index.ts +4 -0
- package/src/geist-fonts.ts +48 -0
- package/src/hooks/index.ts +31 -0
- package/src/hooks/useFilters.ts +152 -0
- package/src/hooks/useInfiniteScroll.ts +62 -0
- package/src/hooks/usePlatform.ts +33 -0
- package/src/hooks/useServerTable.ts +495 -0
- package/src/hooks/useTableSelection.ts +102 -0
- package/src/hooks/useTableSort.ts +259 -0
- package/src/index.ts +483 -0
- package/src/mantine.ts +25 -0
- package/src/theme/mantineVars.ts +12 -0
- package/src/theme/themeContract.css.ts +131 -0
- package/src/theme/themeVars.ts +31 -0
- package/src/theme.ts +168 -0
- package/src/tokens/color-types.ts +107 -0
- package/src/tokens/colors.ts +243 -0
- package/src/tokens/index.ts +14 -0
- package/src/tokens/radius.ts +17 -0
- package/src/tokens/semantic-colors.ts +224 -0
- package/src/tokens/semantic-tokens-css.ts +53 -0
- package/src/tokens/shadows.ts +11 -0
- package/src/tokens/spacing.ts +20 -0
- package/src/tokens/text-styles.ts +179 -0
- package/src/tokens/typography.ts +40 -0
- package/src/tokens/zIndex.ts +27 -0
- package/src/types/mantine-theme.d.ts +17 -0
- package/src/types/tanstack-table.d.ts +22 -0
- package/src/utils/avatar.ts +150 -0
- package/src/utils/chartHelpers.ts +53 -0
- package/src/utils/color-props.ts +77 -0
- package/src/utils/createDesignComponent.tsx +104 -0
- package/src/utils/nestFlatRows.ts +111 -0
- package/src/utils/withStaticComponents.ts +6 -0
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memo,
|
|
3
|
+
useMemo,
|
|
4
|
+
useState,
|
|
5
|
+
type CSSProperties,
|
|
6
|
+
type MouseEvent as ReactMouseEvent,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
flexRender,
|
|
12
|
+
type Header,
|
|
13
|
+
type Row,
|
|
14
|
+
type Table,
|
|
15
|
+
} from '@tanstack/react-table';
|
|
16
|
+
import { clsx } from 'clsx';
|
|
17
|
+
import { Settings2 } from 'lucide-react';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
Box,
|
|
21
|
+
Checkbox,
|
|
22
|
+
Flex,
|
|
23
|
+
Table as MantineTable,
|
|
24
|
+
Menu,
|
|
25
|
+
} from '@mantine/core';
|
|
26
|
+
|
|
27
|
+
import { tokens } from '../../theme/themeContract.css';
|
|
28
|
+
import { EmptyState } from '../EmptyState';
|
|
29
|
+
import { ErrorState } from '../ErrorState';
|
|
30
|
+
import { IconButton } from '../IconButton';
|
|
31
|
+
import { RowExpandToggle } from './RowExpandToggle';
|
|
32
|
+
import {
|
|
33
|
+
InfiniteScrollTrigger,
|
|
34
|
+
type InfiniteScrollState,
|
|
35
|
+
} from '../InfiniteScrollTrigger';
|
|
36
|
+
import { SortableColumnHeader } from '../Table/SortableColumnHeader';
|
|
37
|
+
import {
|
|
38
|
+
TableSkeletonRows,
|
|
39
|
+
type SkeletonColumnConfig,
|
|
40
|
+
} from '../Table/TableSkeletonRows';
|
|
41
|
+
import { TableCard } from '../TableCard';
|
|
42
|
+
import { Text } from '../Typography';
|
|
43
|
+
|
|
44
|
+
export interface DataTableColumnConfiguratorItem {
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
defaultVisible?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DataTableColumnConfigurator {
|
|
51
|
+
columns: DataTableColumnConfiguratorItem[];
|
|
52
|
+
buttonAriaLabel?: string;
|
|
53
|
+
menuLabel?: string;
|
|
54
|
+
onColumnToggle?: (columnId: string, isVisible: boolean) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DataTableProps<TData> {
|
|
58
|
+
/**
|
|
59
|
+
* TanStack Table instance from useServerTable
|
|
60
|
+
*/
|
|
61
|
+
table: Table<TData>;
|
|
62
|
+
/**
|
|
63
|
+
* True ONLY on first load (no cached data) -> show skeleton
|
|
64
|
+
*/
|
|
65
|
+
isInitialLoading: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* True when data exists but refetching (sort/filter change) -> show subtle indicator
|
|
68
|
+
*/
|
|
69
|
+
isRefetching: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* True when query failed
|
|
72
|
+
*/
|
|
73
|
+
isError: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Pagination state for InfiniteScrollTrigger
|
|
76
|
+
*/
|
|
77
|
+
pagination: InfiniteScrollState;
|
|
78
|
+
/**
|
|
79
|
+
* Custom empty state component to render when no data
|
|
80
|
+
* Receives hasData boolean to determine if showing filtered empty vs no data
|
|
81
|
+
* If not provided, will use a default EmptyState component
|
|
82
|
+
*/
|
|
83
|
+
emptyState?: ReactNode;
|
|
84
|
+
/**
|
|
85
|
+
* Custom error state component to render on query failure
|
|
86
|
+
* If not provided, will use a default ErrorState component
|
|
87
|
+
*/
|
|
88
|
+
errorState?: ReactNode;
|
|
89
|
+
/**
|
|
90
|
+
* Callback when a row is clicked.
|
|
91
|
+
*
|
|
92
|
+
* The second argument is the underlying TanStack `Row<TData>` for callers
|
|
93
|
+
* that need access to row-model state (depth, canExpand, selection, etc.).
|
|
94
|
+
* Existing one-argument callers remain compatible.
|
|
95
|
+
*
|
|
96
|
+
* For group rows (`row.getCanExpand() === true`) this callback is NOT
|
|
97
|
+
* invoked by default — clicking a group row toggles expansion instead.
|
|
98
|
+
* See `disableRowClickForGroupRows` to make group-row body-clicks inert.
|
|
99
|
+
*/
|
|
100
|
+
onRowClick?: (row: TData, tanstackRow: Row<TData>) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Callback when hovering over a row (for prefetching)
|
|
103
|
+
*/
|
|
104
|
+
onRowHover?: (row: TData) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Custom class names for table rows
|
|
107
|
+
*/
|
|
108
|
+
rowClassName?: string | ((row: TData) => string);
|
|
109
|
+
/**
|
|
110
|
+
* Row id that should render as active/highlighted.
|
|
111
|
+
*/
|
|
112
|
+
activeRowId?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Minimum width for horizontal scroll
|
|
115
|
+
* @default 800
|
|
116
|
+
*/
|
|
117
|
+
minWidth?: number;
|
|
118
|
+
/**
|
|
119
|
+
* Whether to show a border around the table card
|
|
120
|
+
* @default true
|
|
121
|
+
*/
|
|
122
|
+
withBorder?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Number of skeleton rows to show during initial load
|
|
125
|
+
* @default 8
|
|
126
|
+
*/
|
|
127
|
+
skeletonRowCount?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Opacity when refetching (0-1)
|
|
130
|
+
* @default 0.6
|
|
131
|
+
*/
|
|
132
|
+
refetchingOpacity?: number;
|
|
133
|
+
/**
|
|
134
|
+
* Loading message for infinite scroll
|
|
135
|
+
* @default "Loading more..."
|
|
136
|
+
*/
|
|
137
|
+
infiniteScrollMessage?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Message to show when all data has been loaded via infinite scroll
|
|
140
|
+
* Only shown if multiple pages were fetched.
|
|
141
|
+
* @default "All items loaded"
|
|
142
|
+
* @example "All apps loaded"
|
|
143
|
+
*/
|
|
144
|
+
infiniteScrollEndMessage?: string;
|
|
145
|
+
/**
|
|
146
|
+
* Whether to show row selection checkboxes
|
|
147
|
+
* When true, renders a checkbox column and changes row click to toggle selection
|
|
148
|
+
* @default false
|
|
149
|
+
*/
|
|
150
|
+
showRowSelection?: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Opt in to row expansion (tree rows). When true, `DataTable` assumes the
|
|
153
|
+
* consumer's table instance was built with `getSubRows` +
|
|
154
|
+
* `getExpandedRowModel()` and owns the `expanded` state. In auto mode this
|
|
155
|
+
* also injects a leading chevron column.
|
|
156
|
+
*
|
|
157
|
+
* @default false
|
|
158
|
+
*/
|
|
159
|
+
enableRowExpansion?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Optional custom renderer for group (expandable) rows. When provided and
|
|
162
|
+
* the row is a group (`row.getCanExpand() === true`), the row is rendered
|
|
163
|
+
* as a single `<td>` with `colSpan` covering every column — consumer fully
|
|
164
|
+
* owns the layout (chevron, label, aggregates, etc.). Pair with
|
|
165
|
+
* `RowGroupHeader` for the design-system default.
|
|
166
|
+
*
|
|
167
|
+
* Return `null`/`undefined` to fall back to per-column rendering for that
|
|
168
|
+
* specific row.
|
|
169
|
+
*
|
|
170
|
+
* When this prop is set, group rows ignore the selection checkbox cell
|
|
171
|
+
* and the auto-injected chevron cell — both are subsumed by the spanning
|
|
172
|
+
* TD. Leaf rows keep standard column-by-column rendering.
|
|
173
|
+
*/
|
|
174
|
+
renderGroupRow?: (row: Row<TData>) => ReactNode;
|
|
175
|
+
/**
|
|
176
|
+
* Background color applied to the `<tr>` of custom-rendered group rows.
|
|
177
|
+
* Accepts any CSS color value (typically a design-system token value).
|
|
178
|
+
*
|
|
179
|
+
* @default tokens.color.background.subduedUltralight
|
|
180
|
+
*/
|
|
181
|
+
groupRowBackground?: string;
|
|
182
|
+
/**
|
|
183
|
+
* How the expand/collapse chevron is rendered.
|
|
184
|
+
* - `"auto"` (default): `DataTable` prepends a dedicated chevron column.
|
|
185
|
+
* - `"custom"`: consumer composes `<RowExpandToggle row={row}>` inside one
|
|
186
|
+
* of their own cells; no extra column is rendered.
|
|
187
|
+
*
|
|
188
|
+
* Only honored when `enableRowExpansion` is true.
|
|
189
|
+
* @default "auto"
|
|
190
|
+
*/
|
|
191
|
+
renderExpandToggle?: 'auto' | 'custom';
|
|
192
|
+
/**
|
|
193
|
+
* When true, clicking a group row's body does nothing. The chevron button
|
|
194
|
+
* and (if present) the selection checkbox still work. Useful when the
|
|
195
|
+
* only meaningful interaction on a group row should be the explicit
|
|
196
|
+
* affordances.
|
|
197
|
+
* @default false
|
|
198
|
+
*/
|
|
199
|
+
disableRowClickForGroupRows?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Optional header content for the auto-injected chevron column.
|
|
202
|
+
* Ignored when `renderExpandToggle` is `"custom"`.
|
|
203
|
+
*/
|
|
204
|
+
expandColumnHeader?: ReactNode;
|
|
205
|
+
/**
|
|
206
|
+
* Width (px) of the auto-injected chevron column.
|
|
207
|
+
* Ignored when `renderExpandToggle` is `"custom"`.
|
|
208
|
+
* @default 40
|
|
209
|
+
*/
|
|
210
|
+
expandColumnWidth?: number;
|
|
211
|
+
/**
|
|
212
|
+
* Table layout algorithm
|
|
213
|
+
* - 'auto': Browser calculates column widths based on content (default)
|
|
214
|
+
* - 'fixed': Columns use exact widths from column definitions, content may truncate
|
|
215
|
+
* Use 'fixed' when you need precise column sizing with text truncation
|
|
216
|
+
* @default undefined (browser default is 'auto')
|
|
217
|
+
*/
|
|
218
|
+
tableLayout?: 'auto' | 'fixed';
|
|
219
|
+
/**
|
|
220
|
+
* Optional column configurator menu.
|
|
221
|
+
* Lets consumers toggle visibility of selected columns.
|
|
222
|
+
*/
|
|
223
|
+
columnConfigurator?: DataTableColumnConfigurator;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Any element matching this selector is treated as an "interactive
|
|
227
|
+
// descendant" — a click on it (or on one of its children) must not toggle
|
|
228
|
+
// group-row expansion. This matters for rows rendered through
|
|
229
|
+
// `renderGroupRow`, where consumers frequently embed buttons, links, inputs,
|
|
230
|
+
// or other controls inside the row body.
|
|
231
|
+
const INTERACTIVE_SELECTOR =
|
|
232
|
+
'button, a, input, textarea, select, label, [role="button"], [contenteditable=""], [contenteditable="true"]';
|
|
233
|
+
|
|
234
|
+
function isClickFromInteractiveDescendant(
|
|
235
|
+
event: ReactMouseEvent<HTMLElement>,
|
|
236
|
+
): boolean {
|
|
237
|
+
if (event.defaultPrevented) return true;
|
|
238
|
+
const target = event.target as Element | null;
|
|
239
|
+
if (!target || typeof target.closest !== 'function') return false;
|
|
240
|
+
const match = target.closest(INTERACTIVE_SELECTOR);
|
|
241
|
+
if (!match) return false;
|
|
242
|
+
// Only bail when the interactive element is inside this row, not an
|
|
243
|
+
// ancestor that happens to match (e.g. the surrounding page <main>).
|
|
244
|
+
return event.currentTarget.contains(match);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Resolve the aria-sort attribute from TanStack Table's sort state
|
|
249
|
+
*/
|
|
250
|
+
function getAriaSortValue(
|
|
251
|
+
sortState: false | 'asc' | 'desc',
|
|
252
|
+
): 'ascending' | 'descending' | 'none' {
|
|
253
|
+
if (sortState === 'asc') return 'ascending';
|
|
254
|
+
if (sortState === 'desc') return 'descending';
|
|
255
|
+
return 'none';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Render a single header cell based on sort state and placeholder status
|
|
260
|
+
*/
|
|
261
|
+
function renderHeaderCell<TData>(header: Header<TData, unknown>) {
|
|
262
|
+
if (header.isPlaceholder) return null;
|
|
263
|
+
|
|
264
|
+
if (header.column.getCanSort()) {
|
|
265
|
+
// Resolve label: prefer string header, fall back to column ID
|
|
266
|
+
const label =
|
|
267
|
+
typeof header.column.columnDef.header === 'string'
|
|
268
|
+
? header.column.columnDef.header
|
|
269
|
+
: header.column.id;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<SortableColumnHeader
|
|
273
|
+
label={label}
|
|
274
|
+
sortProps={{
|
|
275
|
+
onClick: () => header.column.toggleSorting(),
|
|
276
|
+
'aria-sort': getAriaSortValue(header.column.getIsSorted()),
|
|
277
|
+
}}
|
|
278
|
+
align={header.column.columnDef.meta?.align}
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<Text variant="caption2.stronger">
|
|
285
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
286
|
+
</Text>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Memoized row component ---
|
|
291
|
+
// Prevents re-rendering all rows when a single row's selection changes.
|
|
292
|
+
// Custom comparison: rowData reference covers data identity; row reference covers
|
|
293
|
+
// cell/handler freshness; callbacks and primitives are compared directly.
|
|
294
|
+
|
|
295
|
+
interface DataTableRowProps<TData> {
|
|
296
|
+
row: Row<TData>;
|
|
297
|
+
rowData: TData;
|
|
298
|
+
isSelected: boolean;
|
|
299
|
+
showRowSelection: boolean;
|
|
300
|
+
isClickable: boolean;
|
|
301
|
+
onRowClick?: (row: TData, tanstackRow: Row<TData>) => void;
|
|
302
|
+
onRowHover?: (row: TData) => void;
|
|
303
|
+
rowClassName?: string | ((row: TData) => string);
|
|
304
|
+
activeRowId?: string;
|
|
305
|
+
showSyntheticActionsColumn?: boolean;
|
|
306
|
+
showExpandCell?: boolean;
|
|
307
|
+
disableRowClickForGroupRows?: boolean;
|
|
308
|
+
// Snapshotted expansion state read at the parent level. Passing these as
|
|
309
|
+
// primitives lets the memo comparator detect expand/collapse transitions —
|
|
310
|
+
// calling `prev.row.getIsExpanded()` inside the comparator always reads the
|
|
311
|
+
// live table state, so it can't distinguish between two consecutive renders.
|
|
312
|
+
isExpanded: boolean;
|
|
313
|
+
canExpand: boolean;
|
|
314
|
+
// Subtle background applied to group rows rendered through the standard
|
|
315
|
+
// per-column path (no `renderGroupRow`). Leaf rows always ignore this.
|
|
316
|
+
groupRowBackground: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function DataTableRowInner<TData>({
|
|
320
|
+
row,
|
|
321
|
+
rowData,
|
|
322
|
+
isSelected,
|
|
323
|
+
showRowSelection,
|
|
324
|
+
isClickable,
|
|
325
|
+
onRowClick,
|
|
326
|
+
onRowHover,
|
|
327
|
+
rowClassName,
|
|
328
|
+
activeRowId,
|
|
329
|
+
showSyntheticActionsColumn = false,
|
|
330
|
+
showExpandCell = false,
|
|
331
|
+
disableRowClickForGroupRows = false,
|
|
332
|
+
isExpanded,
|
|
333
|
+
canExpand,
|
|
334
|
+
groupRowBackground,
|
|
335
|
+
}: DataTableRowProps<TData>) {
|
|
336
|
+
const rowClass =
|
|
337
|
+
typeof rowClassName === 'function' ? rowClassName(rowData) : rowClassName;
|
|
338
|
+
const isActive = row.id === activeRowId;
|
|
339
|
+
const isGroup = canExpand;
|
|
340
|
+
// Active/selected states always win over the group-row background so that
|
|
341
|
+
// hover/selection highlighting remains visible on group rows.
|
|
342
|
+
const rowBackgroundColor =
|
|
343
|
+
isSelected || isActive
|
|
344
|
+
? tokens.color.background.primaryLight
|
|
345
|
+
: isGroup
|
|
346
|
+
? groupRowBackground
|
|
347
|
+
: undefined;
|
|
348
|
+
|
|
349
|
+
const handleClick = (event: ReactMouseEvent<HTMLTableRowElement>) => {
|
|
350
|
+
// Group rows: body-click toggles expand (unless disabled). The chevron
|
|
351
|
+
// button and the selection checkbox stop propagation, so they still work
|
|
352
|
+
// independently — users get both affordances on a grouped + selectable
|
|
353
|
+
// table. Clicks that bubble up from an interactive descendant (e.g. a
|
|
354
|
+
// button or link inside a cell) must NOT toggle expansion.
|
|
355
|
+
if (isGroup) {
|
|
356
|
+
if (
|
|
357
|
+
!disableRowClickForGroupRows &&
|
|
358
|
+
!isClickFromInteractiveDescendant(event)
|
|
359
|
+
) {
|
|
360
|
+
row.getToggleExpandedHandler()();
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Leaf rows: selection wins over onRowClick when enabled (preserves
|
|
365
|
+
// existing behavior for flat tables).
|
|
366
|
+
if (showRowSelection) {
|
|
367
|
+
row.toggleSelected();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (onRowClick) {
|
|
371
|
+
onRowClick(rowData, row);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<MantineTable.Tr
|
|
377
|
+
onClick={isClickable ? handleClick : undefined}
|
|
378
|
+
onMouseEnter={onRowHover ? () => onRowHover(rowData) : undefined}
|
|
379
|
+
className={clsx(rowClass)}
|
|
380
|
+
data-active={isActive ? 'true' : undefined}
|
|
381
|
+
data-expanded={isGroup ? (isExpanded ? 'true' : 'false') : undefined}
|
|
382
|
+
style={{
|
|
383
|
+
cursor: isClickable ? 'pointer' : undefined,
|
|
384
|
+
backgroundColor: rowBackgroundColor,
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
{showRowSelection && (
|
|
388
|
+
<MantineTable.Td onClick={e => e.stopPropagation()}>
|
|
389
|
+
<Checkbox
|
|
390
|
+
checked={isSelected}
|
|
391
|
+
onChange={row.getToggleSelectedHandler()}
|
|
392
|
+
aria-label="Select row"
|
|
393
|
+
/>
|
|
394
|
+
</MantineTable.Td>
|
|
395
|
+
)}
|
|
396
|
+
{showExpandCell && (
|
|
397
|
+
<MantineTable.Td onClick={e => e.stopPropagation()}>
|
|
398
|
+
<RowExpandToggle row={row} />
|
|
399
|
+
</MantineTable.Td>
|
|
400
|
+
)}
|
|
401
|
+
{row.getVisibleCells().map(cell => {
|
|
402
|
+
const meta = cell.column.columnDef.meta as
|
|
403
|
+
| { align?: string }
|
|
404
|
+
| undefined;
|
|
405
|
+
const isPinned = cell.column.getIsPinned();
|
|
406
|
+
const pinnedStyle: CSSProperties | undefined = isPinned
|
|
407
|
+
? {
|
|
408
|
+
position: 'sticky',
|
|
409
|
+
left:
|
|
410
|
+
cell.column.getIsPinned() === 'left'
|
|
411
|
+
? cell.column.getStart()
|
|
412
|
+
: undefined,
|
|
413
|
+
right:
|
|
414
|
+
cell.column.getIsPinned() === 'right'
|
|
415
|
+
? cell.column.getAfter()
|
|
416
|
+
: undefined,
|
|
417
|
+
zIndex: 1,
|
|
418
|
+
background: rowBackgroundColor ?? tokens.color.background.default,
|
|
419
|
+
boxShadow:
|
|
420
|
+
cell.column.getIsPinned() === 'right'
|
|
421
|
+
? tokens.shadow.sm
|
|
422
|
+
: undefined,
|
|
423
|
+
}
|
|
424
|
+
: undefined;
|
|
425
|
+
const cellStyle: CSSProperties = {
|
|
426
|
+
...(meta?.align && {
|
|
427
|
+
textAlign: meta.align as CSSProperties['textAlign'],
|
|
428
|
+
}),
|
|
429
|
+
...pinnedStyle,
|
|
430
|
+
};
|
|
431
|
+
return (
|
|
432
|
+
<MantineTable.Td key={cell.id} style={cellStyle}>
|
|
433
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
434
|
+
</MantineTable.Td>
|
|
435
|
+
);
|
|
436
|
+
})}
|
|
437
|
+
{showSyntheticActionsColumn && (
|
|
438
|
+
<MantineTable.Td
|
|
439
|
+
style={{
|
|
440
|
+
position: 'sticky',
|
|
441
|
+
right: 0,
|
|
442
|
+
zIndex: 1,
|
|
443
|
+
width: 48,
|
|
444
|
+
background: rowBackgroundColor ?? tokens.color.background.default,
|
|
445
|
+
boxShadow: tokens.shadow.sm,
|
|
446
|
+
}}
|
|
447
|
+
/>
|
|
448
|
+
)}
|
|
449
|
+
</MantineTable.Tr>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const DataTableRow = memo(
|
|
454
|
+
DataTableRowInner,
|
|
455
|
+
(prev, next) =>
|
|
456
|
+
prev.row === next.row &&
|
|
457
|
+
prev.rowData === next.rowData &&
|
|
458
|
+
prev.isSelected === next.isSelected &&
|
|
459
|
+
prev.showRowSelection === next.showRowSelection &&
|
|
460
|
+
prev.isClickable === next.isClickable &&
|
|
461
|
+
prev.onRowClick === next.onRowClick &&
|
|
462
|
+
prev.onRowHover === next.onRowHover &&
|
|
463
|
+
prev.rowClassName === next.rowClassName &&
|
|
464
|
+
prev.activeRowId === next.activeRowId &&
|
|
465
|
+
prev.showSyntheticActionsColumn === next.showSyntheticActionsColumn &&
|
|
466
|
+
prev.showExpandCell === next.showExpandCell &&
|
|
467
|
+
prev.disableRowClickForGroupRows === next.disableRowClickForGroupRows &&
|
|
468
|
+
// Expansion state must be compared via snapshotted primitives, not
|
|
469
|
+
// `prev.row.getIsExpanded()`: TanStack caches the Row object across
|
|
470
|
+
// renders, and `getIsExpanded()` reads the live table state, so calling
|
|
471
|
+
// it on both `prev.row` and `next.row` (same reference) always returns
|
|
472
|
+
// the same value and would never invalidate the memo on expand/collapse.
|
|
473
|
+
prev.isExpanded === next.isExpanded &&
|
|
474
|
+
prev.canExpand === next.canExpand &&
|
|
475
|
+
prev.groupRowBackground === next.groupRowBackground,
|
|
476
|
+
) as typeof DataTableRowInner;
|
|
477
|
+
|
|
478
|
+
// --- Custom group-row renderer ---
|
|
479
|
+
// Used when `renderGroupRow` is provided and the row is a group. Renders a
|
|
480
|
+
// single <td colSpan> instead of the per-column layout, so the consumer can
|
|
481
|
+
// emit a row that looks nothing like a leaf row (different fields,
|
|
482
|
+
// background, aggregates, etc.).
|
|
483
|
+
|
|
484
|
+
interface DataTableGroupRowProps<TData> {
|
|
485
|
+
row: Row<TData>;
|
|
486
|
+
content: ReactNode;
|
|
487
|
+
colSpan: number;
|
|
488
|
+
background: string;
|
|
489
|
+
disableRowClick: boolean;
|
|
490
|
+
// Snapshotted at parent level — see DataTableRow for why `row.getIsExpanded()`
|
|
491
|
+
// inside the memo comparator can't detect expand/collapse transitions.
|
|
492
|
+
isExpanded: boolean;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function DataTableGroupRowInner<TData>({
|
|
496
|
+
row,
|
|
497
|
+
content,
|
|
498
|
+
colSpan,
|
|
499
|
+
background,
|
|
500
|
+
disableRowClick,
|
|
501
|
+
isExpanded,
|
|
502
|
+
}: DataTableGroupRowProps<TData>) {
|
|
503
|
+
// Custom-rendered group rows (`renderGroupRow`) often contain interactive
|
|
504
|
+
// descendants — buttons, links, inputs — that the consumer expects to
|
|
505
|
+
// behave independently. Swallow the row-level toggle when the original
|
|
506
|
+
// click originated from one of those controls so the control's own
|
|
507
|
+
// handler runs without also collapsing/expanding the group.
|
|
508
|
+
const handleClick = disableRowClick
|
|
509
|
+
? undefined
|
|
510
|
+
: (event: ReactMouseEvent<HTMLTableRowElement>) => {
|
|
511
|
+
if (isClickFromInteractiveDescendant(event)) return;
|
|
512
|
+
row.getToggleExpandedHandler()();
|
|
513
|
+
};
|
|
514
|
+
return (
|
|
515
|
+
<MantineTable.Tr
|
|
516
|
+
onClick={handleClick}
|
|
517
|
+
data-expanded={isExpanded ? 'true' : 'false'}
|
|
518
|
+
style={{
|
|
519
|
+
cursor: handleClick ? 'pointer' : undefined,
|
|
520
|
+
backgroundColor: background,
|
|
521
|
+
}}
|
|
522
|
+
>
|
|
523
|
+
<MantineTable.Td
|
|
524
|
+
colSpan={colSpan}
|
|
525
|
+
style={{
|
|
526
|
+
backgroundColor: background,
|
|
527
|
+
padding: '0 16px',
|
|
528
|
+
height: 40,
|
|
529
|
+
}}
|
|
530
|
+
>
|
|
531
|
+
{content}
|
|
532
|
+
</MantineTable.Td>
|
|
533
|
+
</MantineTable.Tr>
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const DataTableGroupRow = memo(
|
|
538
|
+
DataTableGroupRowInner,
|
|
539
|
+
(prev, next) =>
|
|
540
|
+
prev.row === next.row &&
|
|
541
|
+
prev.content === next.content &&
|
|
542
|
+
prev.colSpan === next.colSpan &&
|
|
543
|
+
prev.background === next.background &&
|
|
544
|
+
prev.disableRowClick === next.disableRowClick &&
|
|
545
|
+
prev.isExpanded === next.isExpanded,
|
|
546
|
+
) as typeof DataTableGroupRowInner;
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* DataTable component - renders a TanStack Table with automatic state handling
|
|
550
|
+
*
|
|
551
|
+
* Handles all loading/error/empty states automatically:
|
|
552
|
+
* - Initial load: Shows skeleton
|
|
553
|
+
* - Refetching: Shows table with opacity overlay
|
|
554
|
+
* - Error: Shows custom error state (or default ErrorState if not provided)
|
|
555
|
+
* - No data: Shows custom empty state (or default EmptyState if not provided)
|
|
556
|
+
* - Has data: Renders table from column definitions
|
|
557
|
+
*
|
|
558
|
+
* @example Basic usage
|
|
559
|
+
* ```tsx
|
|
560
|
+
* const { table, ...states } = useServerTable({ columns, queryOptions, filters });
|
|
561
|
+
*
|
|
562
|
+
* <DataTable
|
|
563
|
+
* {...states}
|
|
564
|
+
* table={table}
|
|
565
|
+
* emptyState={
|
|
566
|
+
* <TableEmptyState
|
|
567
|
+
* entityName="apps"
|
|
568
|
+
* noDataDescription="No apps found"
|
|
569
|
+
* hasActiveFilters={hasActiveFilters}
|
|
570
|
+
* onClearFilters={clearFilters}
|
|
571
|
+
* />
|
|
572
|
+
* }
|
|
573
|
+
* onRowClick={(app) => openDrawer(app.id)}
|
|
574
|
+
* />
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
export function DataTable<TData>({
|
|
578
|
+
table,
|
|
579
|
+
isInitialLoading,
|
|
580
|
+
isRefetching,
|
|
581
|
+
isError,
|
|
582
|
+
pagination,
|
|
583
|
+
emptyState,
|
|
584
|
+
errorState,
|
|
585
|
+
onRowClick,
|
|
586
|
+
onRowHover,
|
|
587
|
+
rowClassName,
|
|
588
|
+
activeRowId,
|
|
589
|
+
minWidth = 800,
|
|
590
|
+
withBorder = true,
|
|
591
|
+
skeletonRowCount = 8,
|
|
592
|
+
refetchingOpacity = 0.6,
|
|
593
|
+
infiniteScrollMessage = 'Loading more...',
|
|
594
|
+
infiniteScrollEndMessage,
|
|
595
|
+
showRowSelection = false,
|
|
596
|
+
enableRowExpansion = false,
|
|
597
|
+
renderExpandToggle = 'auto',
|
|
598
|
+
disableRowClickForGroupRows = false,
|
|
599
|
+
expandColumnHeader,
|
|
600
|
+
expandColumnWidth = 40,
|
|
601
|
+
renderGroupRow,
|
|
602
|
+
groupRowBackground,
|
|
603
|
+
tableLayout,
|
|
604
|
+
columnConfigurator,
|
|
605
|
+
}: DataTableProps<TData>) {
|
|
606
|
+
const rows = table.getRowModel().rows;
|
|
607
|
+
const hasData = rows.length > 0;
|
|
608
|
+
const showExpandCell = enableRowExpansion && renderExpandToggle !== 'custom';
|
|
609
|
+
const resolvedGroupRowBackground =
|
|
610
|
+
groupRowBackground ?? tokens.color.background.subduedUltralight;
|
|
611
|
+
|
|
612
|
+
// Footer rendering is auto-detected: if any column has a `footer` defined,
|
|
613
|
+
// we emit a <tfoot>. This keeps the API surface small while covering the
|
|
614
|
+
// common "grand total" pattern.
|
|
615
|
+
const footerGroups = table.getFooterGroups();
|
|
616
|
+
const hasFooter = footerGroups.some(group =>
|
|
617
|
+
group.headers.some(
|
|
618
|
+
header => !header.isPlaceholder && header.column.columnDef.footer != null,
|
|
619
|
+
),
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
if (
|
|
623
|
+
import.meta.env.DEV &&
|
|
624
|
+
enableRowExpansion &&
|
|
625
|
+
table.getState().expanded === undefined
|
|
626
|
+
) {
|
|
627
|
+
// Warn once per render but only during development. In production we stay
|
|
628
|
+
// silent — expansion simply won't do anything useful, which is immediately
|
|
629
|
+
// visible to the integrator.
|
|
630
|
+
console.warn(
|
|
631
|
+
'DataTable: enableRowExpansion is true but the table instance has no expanded state. ' +
|
|
632
|
+
'Pass getExpandedRowModel() + state.expanded when constructing the table (e.g. via useServerTable with the tree option).',
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
const [isConfiguratorOpen, setIsConfiguratorOpen] = useState(false);
|
|
636
|
+
const configurableColumns = useMemo(
|
|
637
|
+
() =>
|
|
638
|
+
(columnConfigurator?.columns ?? []).filter(
|
|
639
|
+
({ id }) => typeof table.getColumn(id)?.getIsVisible === 'function',
|
|
640
|
+
),
|
|
641
|
+
[columnConfigurator?.columns, table],
|
|
642
|
+
);
|
|
643
|
+
const hasActionsColumn = useMemo(
|
|
644
|
+
() =>
|
|
645
|
+
table
|
|
646
|
+
.getHeaderGroups()
|
|
647
|
+
.some(group =>
|
|
648
|
+
group.headers.some(header => header.column.id === 'actions'),
|
|
649
|
+
),
|
|
650
|
+
[table],
|
|
651
|
+
);
|
|
652
|
+
const shouldRenderSyntheticActionsColumn =
|
|
653
|
+
configurableColumns.length > 0 && !hasActionsColumn;
|
|
654
|
+
|
|
655
|
+
const handleToggleColumn = (columnId: string) => {
|
|
656
|
+
const column = table.getColumn(columnId);
|
|
657
|
+
if (!column) return;
|
|
658
|
+
|
|
659
|
+
const nextVisibility = !column.getIsVisible();
|
|
660
|
+
column.toggleVisibility(nextVisibility);
|
|
661
|
+
columnConfigurator?.onColumnToggle?.(columnId, nextVisibility);
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const renderColumnConfiguratorMenu = () => (
|
|
665
|
+
<Menu
|
|
666
|
+
opened={isConfiguratorOpen}
|
|
667
|
+
onChange={setIsConfiguratorOpen}
|
|
668
|
+
withArrow
|
|
669
|
+
withinPortal
|
|
670
|
+
closeOnItemClick={false}
|
|
671
|
+
>
|
|
672
|
+
<Menu.Target>
|
|
673
|
+
<IconButton
|
|
674
|
+
variant="ghost"
|
|
675
|
+
size="xs"
|
|
676
|
+
aria-label={
|
|
677
|
+
columnConfigurator?.buttonAriaLabel ?? 'Configure table columns'
|
|
678
|
+
}
|
|
679
|
+
>
|
|
680
|
+
<Settings2 size={16} />
|
|
681
|
+
</IconButton>
|
|
682
|
+
</Menu.Target>
|
|
683
|
+
<Menu.Dropdown>
|
|
684
|
+
{columnConfigurator?.menuLabel && (
|
|
685
|
+
<Menu.Label>{columnConfigurator.menuLabel}</Menu.Label>
|
|
686
|
+
)}
|
|
687
|
+
{configurableColumns.map(column => {
|
|
688
|
+
const isVisible = table.getColumn(column.id)?.getIsVisible() ?? false;
|
|
689
|
+
return (
|
|
690
|
+
<Menu.Item
|
|
691
|
+
key={column.id}
|
|
692
|
+
onClick={() => handleToggleColumn(column.id)}
|
|
693
|
+
>
|
|
694
|
+
<Flex gap="xs" align="center">
|
|
695
|
+
<Checkbox checked={isVisible} readOnly tabIndex={-1} />
|
|
696
|
+
<Text variant="caption1">{column.label}</Text>
|
|
697
|
+
</Flex>
|
|
698
|
+
</Menu.Item>
|
|
699
|
+
);
|
|
700
|
+
})}
|
|
701
|
+
</Menu.Dropdown>
|
|
702
|
+
</Menu>
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
const renderContent = () => {
|
|
706
|
+
// Initial loading: show skeleton rows (no headers)
|
|
707
|
+
if (isInitialLoading) {
|
|
708
|
+
const firstHeaderGroup = table.getHeaderGroups()[0];
|
|
709
|
+
const skeletonColumns: SkeletonColumnConfig[] =
|
|
710
|
+
firstHeaderGroup?.headers.map(h => ({
|
|
711
|
+
width: Math.min(Math.round(h.getSize() * 0.65), 150),
|
|
712
|
+
})) ?? [{ width: 100 }];
|
|
713
|
+
return (
|
|
714
|
+
<TableCard withBorder={withBorder}>
|
|
715
|
+
<Box style={{ overflow: 'hidden', borderRadius: 'inherit' }}>
|
|
716
|
+
<MantineTable.ScrollContainer minWidth={minWidth}>
|
|
717
|
+
<MantineTable verticalSpacing="sm">
|
|
718
|
+
<MantineTable.Tbody>
|
|
719
|
+
<TableSkeletonRows
|
|
720
|
+
columns={skeletonColumns}
|
|
721
|
+
rowCount={skeletonRowCount}
|
|
722
|
+
/>
|
|
723
|
+
</MantineTable.Tbody>
|
|
724
|
+
</MantineTable>
|
|
725
|
+
</MantineTable.ScrollContainer>
|
|
726
|
+
</Box>
|
|
727
|
+
</TableCard>
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Error: show error state
|
|
732
|
+
if (isError) {
|
|
733
|
+
return (
|
|
734
|
+
errorState ?? (
|
|
735
|
+
<ErrorState
|
|
736
|
+
title="Failed to load data"
|
|
737
|
+
description="An error occurred while fetching the data. Please try again."
|
|
738
|
+
/>
|
|
739
|
+
)
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// No data: show empty state
|
|
744
|
+
if (!hasData) {
|
|
745
|
+
return (
|
|
746
|
+
emptyState ?? (
|
|
747
|
+
<EmptyState
|
|
748
|
+
title="No data"
|
|
749
|
+
description="No records found to display."
|
|
750
|
+
/>
|
|
751
|
+
)
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Has data: render table
|
|
756
|
+
return (
|
|
757
|
+
<Box
|
|
758
|
+
style={
|
|
759
|
+
isRefetching
|
|
760
|
+
? {
|
|
761
|
+
opacity: refetchingOpacity,
|
|
762
|
+
pointerEvents: 'none',
|
|
763
|
+
transition: 'opacity 150ms',
|
|
764
|
+
}
|
|
765
|
+
: undefined
|
|
766
|
+
}
|
|
767
|
+
>
|
|
768
|
+
<TableCard withBorder={withBorder}>
|
|
769
|
+
<Box style={{ overflow: 'hidden', borderRadius: 'inherit' }}>
|
|
770
|
+
<MantineTable.ScrollContainer minWidth={minWidth}>
|
|
771
|
+
<MantineTable
|
|
772
|
+
highlightOnHover
|
|
773
|
+
verticalSpacing="sm"
|
|
774
|
+
layout={tableLayout}
|
|
775
|
+
>
|
|
776
|
+
{/* Table Headers */}
|
|
777
|
+
<MantineTable.Thead
|
|
778
|
+
bg={tokens.color.background.subduedUltralight}
|
|
779
|
+
>
|
|
780
|
+
{table.getHeaderGroups().map(headerGroup => (
|
|
781
|
+
<MantineTable.Tr key={headerGroup.id}>
|
|
782
|
+
{showRowSelection && (
|
|
783
|
+
<MantineTable.Th w={40}>
|
|
784
|
+
<Checkbox
|
|
785
|
+
checked={table.getIsAllRowsSelected()}
|
|
786
|
+
indeterminate={
|
|
787
|
+
table.getIsSomeRowsSelected() &&
|
|
788
|
+
!table.getIsAllRowsSelected()
|
|
789
|
+
}
|
|
790
|
+
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
791
|
+
aria-label="Select all rows"
|
|
792
|
+
/>
|
|
793
|
+
</MantineTable.Th>
|
|
794
|
+
)}
|
|
795
|
+
{showExpandCell && (
|
|
796
|
+
<MantineTable.Th w={expandColumnWidth}>
|
|
797
|
+
{expandColumnHeader ?? null}
|
|
798
|
+
</MantineTable.Th>
|
|
799
|
+
)}
|
|
800
|
+
{headerGroup.headers.map(header => {
|
|
801
|
+
const isPinned = header.column.getIsPinned();
|
|
802
|
+
const isConfiguratorHost =
|
|
803
|
+
configurableColumns.length > 0 &&
|
|
804
|
+
header.column.id === 'actions';
|
|
805
|
+
const stickyConfiguratorStyle:
|
|
806
|
+
| CSSProperties
|
|
807
|
+
| undefined = isConfiguratorHost
|
|
808
|
+
? {
|
|
809
|
+
position: 'sticky',
|
|
810
|
+
right:
|
|
811
|
+
isPinned === 'right'
|
|
812
|
+
? header.column.getAfter()
|
|
813
|
+
: 0,
|
|
814
|
+
zIndex: 3,
|
|
815
|
+
background:
|
|
816
|
+
tokens.color.background.subduedUltralight,
|
|
817
|
+
boxShadow: tokens.shadow.sm,
|
|
818
|
+
}
|
|
819
|
+
: undefined;
|
|
820
|
+
const pinnedStyle: CSSProperties | undefined = isPinned
|
|
821
|
+
? {
|
|
822
|
+
position: 'sticky',
|
|
823
|
+
left:
|
|
824
|
+
header.column.getIsPinned() === 'left'
|
|
825
|
+
? header.column.getStart()
|
|
826
|
+
: undefined,
|
|
827
|
+
right:
|
|
828
|
+
header.column.getIsPinned() === 'right'
|
|
829
|
+
? header.column.getAfter()
|
|
830
|
+
: undefined,
|
|
831
|
+
zIndex: 2,
|
|
832
|
+
background:
|
|
833
|
+
tokens.color.background.subduedUltralight,
|
|
834
|
+
boxShadow:
|
|
835
|
+
header.column.getIsPinned() === 'right'
|
|
836
|
+
? tokens.shadow.sm
|
|
837
|
+
: undefined,
|
|
838
|
+
}
|
|
839
|
+
: undefined;
|
|
840
|
+
const thStyle = stickyConfiguratorStyle ?? pinnedStyle;
|
|
841
|
+
return (
|
|
842
|
+
<MantineTable.Th
|
|
843
|
+
key={header.id}
|
|
844
|
+
w={header.getSize()}
|
|
845
|
+
style={thStyle}
|
|
846
|
+
>
|
|
847
|
+
{isConfiguratorHost ? (
|
|
848
|
+
<Flex justify="flex-end">
|
|
849
|
+
{renderColumnConfiguratorMenu()}
|
|
850
|
+
</Flex>
|
|
851
|
+
) : (
|
|
852
|
+
renderHeaderCell(header)
|
|
853
|
+
)}
|
|
854
|
+
</MantineTable.Th>
|
|
855
|
+
);
|
|
856
|
+
})}
|
|
857
|
+
{shouldRenderSyntheticActionsColumn && (
|
|
858
|
+
<MantineTable.Th
|
|
859
|
+
w={48}
|
|
860
|
+
ta="right"
|
|
861
|
+
style={{
|
|
862
|
+
position: 'sticky',
|
|
863
|
+
right: 0,
|
|
864
|
+
zIndex: 3,
|
|
865
|
+
background:
|
|
866
|
+
tokens.color.background.subduedUltralight,
|
|
867
|
+
boxShadow: tokens.shadow.sm,
|
|
868
|
+
}}
|
|
869
|
+
>
|
|
870
|
+
<Flex justify="flex-end">
|
|
871
|
+
{renderColumnConfiguratorMenu()}
|
|
872
|
+
</Flex>
|
|
873
|
+
</MantineTable.Th>
|
|
874
|
+
)}
|
|
875
|
+
</MantineTable.Tr>
|
|
876
|
+
))}
|
|
877
|
+
</MantineTable.Thead>
|
|
878
|
+
|
|
879
|
+
{/* Table Body */}
|
|
880
|
+
<MantineTable.Tbody>
|
|
881
|
+
{rows.map(row => {
|
|
882
|
+
// Snapshot expansion state here so the memoized row
|
|
883
|
+
// components receive stable primitive props — see the
|
|
884
|
+
// memo comparator comments on DataTableRow for why
|
|
885
|
+
// calling `row.getIsExpanded()` inside the comparator
|
|
886
|
+
// wouldn't detect transitions.
|
|
887
|
+
const canExpand = row.getCanExpand();
|
|
888
|
+
const isExpanded = row.getIsExpanded();
|
|
889
|
+
const isGroupRow = canExpand;
|
|
890
|
+
// Custom group row: render a single spanning TD. This
|
|
891
|
+
// lets a group row look nothing like a leaf row (e.g.
|
|
892
|
+
// different label, aggregates, background).
|
|
893
|
+
if (isGroupRow && renderGroupRow) {
|
|
894
|
+
const content = renderGroupRow(row);
|
|
895
|
+
if (content != null) {
|
|
896
|
+
const visibleCellCount = row.getVisibleCells().length;
|
|
897
|
+
const colSpan =
|
|
898
|
+
(showRowSelection ? 1 : 0) +
|
|
899
|
+
(showExpandCell ? 1 : 0) +
|
|
900
|
+
visibleCellCount +
|
|
901
|
+
(shouldRenderSyntheticActionsColumn ? 1 : 0);
|
|
902
|
+
return (
|
|
903
|
+
<DataTableGroupRow
|
|
904
|
+
key={row.id}
|
|
905
|
+
row={row}
|
|
906
|
+
content={content}
|
|
907
|
+
colSpan={colSpan}
|
|
908
|
+
background={resolvedGroupRowBackground}
|
|
909
|
+
disableRowClick={disableRowClickForGroupRows}
|
|
910
|
+
isExpanded={isExpanded}
|
|
911
|
+
/>
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// Group rows are clickable whenever group-row clicking
|
|
916
|
+
// isn't suppressed. Leaf rows keep the historical rule:
|
|
917
|
+
// clickable when selection is on or when onRowClick is
|
|
918
|
+
// provided.
|
|
919
|
+
const isClickable = isGroupRow
|
|
920
|
+
? !disableRowClickForGroupRows
|
|
921
|
+
: showRowSelection || !!onRowClick;
|
|
922
|
+
return (
|
|
923
|
+
<DataTableRow
|
|
924
|
+
key={row.id}
|
|
925
|
+
row={row}
|
|
926
|
+
rowData={row.original}
|
|
927
|
+
isSelected={showRowSelection && row.getIsSelected()}
|
|
928
|
+
showRowSelection={showRowSelection}
|
|
929
|
+
isClickable={isClickable}
|
|
930
|
+
onRowClick={onRowClick}
|
|
931
|
+
onRowHover={onRowHover}
|
|
932
|
+
rowClassName={rowClassName}
|
|
933
|
+
activeRowId={activeRowId}
|
|
934
|
+
showSyntheticActionsColumn={
|
|
935
|
+
shouldRenderSyntheticActionsColumn
|
|
936
|
+
}
|
|
937
|
+
showExpandCell={showExpandCell}
|
|
938
|
+
disableRowClickForGroupRows={
|
|
939
|
+
disableRowClickForGroupRows
|
|
940
|
+
}
|
|
941
|
+
isExpanded={isExpanded}
|
|
942
|
+
canExpand={canExpand}
|
|
943
|
+
groupRowBackground={resolvedGroupRowBackground}
|
|
944
|
+
/>
|
|
945
|
+
);
|
|
946
|
+
})}
|
|
947
|
+
</MantineTable.Tbody>
|
|
948
|
+
|
|
949
|
+
{/* Table Footer — emitted only when at least one column
|
|
950
|
+
defines a `footer` template. Mirrors the header layout:
|
|
951
|
+
leading selection/expand spacer cells, then per-column
|
|
952
|
+
footer cells, then optional synthetic actions spacer. */}
|
|
953
|
+
{hasFooter && (
|
|
954
|
+
<MantineTable.Tfoot
|
|
955
|
+
bg={tokens.color.background.subduedUltralight}
|
|
956
|
+
>
|
|
957
|
+
{footerGroups.map(footerGroup => (
|
|
958
|
+
<MantineTable.Tr key={footerGroup.id}>
|
|
959
|
+
{showRowSelection && <MantineTable.Td />}
|
|
960
|
+
{showExpandCell && <MantineTable.Td />}
|
|
961
|
+
{footerGroup.headers.map(header => {
|
|
962
|
+
const meta = header.column.columnDef.meta as
|
|
963
|
+
| { align?: string }
|
|
964
|
+
| undefined;
|
|
965
|
+
const footerTemplate = header.column.columnDef.footer;
|
|
966
|
+
const isPinned = header.column.getIsPinned();
|
|
967
|
+
const pinnedStyle: CSSProperties | undefined =
|
|
968
|
+
isPinned
|
|
969
|
+
? {
|
|
970
|
+
position: 'sticky',
|
|
971
|
+
left:
|
|
972
|
+
isPinned === 'left'
|
|
973
|
+
? header.column.getStart()
|
|
974
|
+
: undefined,
|
|
975
|
+
right:
|
|
976
|
+
isPinned === 'right'
|
|
977
|
+
? header.column.getAfter()
|
|
978
|
+
: undefined,
|
|
979
|
+
zIndex: 2,
|
|
980
|
+
background:
|
|
981
|
+
tokens.color.background.subduedUltralight,
|
|
982
|
+
boxShadow:
|
|
983
|
+
isPinned === 'right'
|
|
984
|
+
? tokens.shadow.sm
|
|
985
|
+
: undefined,
|
|
986
|
+
}
|
|
987
|
+
: undefined;
|
|
988
|
+
const cellStyle: CSSProperties = {
|
|
989
|
+
...(meta?.align && {
|
|
990
|
+
textAlign:
|
|
991
|
+
meta.align as CSSProperties['textAlign'],
|
|
992
|
+
}),
|
|
993
|
+
...pinnedStyle,
|
|
994
|
+
};
|
|
995
|
+
return (
|
|
996
|
+
<MantineTable.Td key={header.id} style={cellStyle}>
|
|
997
|
+
{header.isPlaceholder || footerTemplate == null
|
|
998
|
+
? null
|
|
999
|
+
: flexRender(
|
|
1000
|
+
footerTemplate,
|
|
1001
|
+
header.getContext(),
|
|
1002
|
+
)}
|
|
1003
|
+
</MantineTable.Td>
|
|
1004
|
+
);
|
|
1005
|
+
})}
|
|
1006
|
+
{shouldRenderSyntheticActionsColumn && (
|
|
1007
|
+
<MantineTable.Td
|
|
1008
|
+
style={{
|
|
1009
|
+
position: 'sticky',
|
|
1010
|
+
right: 0,
|
|
1011
|
+
zIndex: 2,
|
|
1012
|
+
background:
|
|
1013
|
+
tokens.color.background.subduedUltralight,
|
|
1014
|
+
boxShadow: tokens.shadow.sm,
|
|
1015
|
+
}}
|
|
1016
|
+
/>
|
|
1017
|
+
)}
|
|
1018
|
+
</MantineTable.Tr>
|
|
1019
|
+
))}
|
|
1020
|
+
</MantineTable.Tfoot>
|
|
1021
|
+
)}
|
|
1022
|
+
</MantineTable>
|
|
1023
|
+
</MantineTable.ScrollContainer>
|
|
1024
|
+
</Box>
|
|
1025
|
+
</TableCard>
|
|
1026
|
+
</Box>
|
|
1027
|
+
);
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
return (
|
|
1031
|
+
<>
|
|
1032
|
+
{renderContent()}
|
|
1033
|
+
{hasData && (
|
|
1034
|
+
<InfiniteScrollTrigger
|
|
1035
|
+
{...pagination}
|
|
1036
|
+
loadingMessage={infiniteScrollMessage}
|
|
1037
|
+
endMessage={infiniteScrollEndMessage}
|
|
1038
|
+
/>
|
|
1039
|
+
)}
|
|
1040
|
+
</>
|
|
1041
|
+
);
|
|
1042
|
+
}
|