@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.
Files changed (273) hide show
  1. package/.ai/rules/date-handling.md +39 -0
  2. package/.ai/rules/figma-design-system.md +372 -0
  3. package/.ai/rules/figma-lm-design-system-keys.md +680 -0
  4. package/.ai/rules/file-extensions.md +13 -0
  5. package/.ai/rules/modal-confirmation-mutation.md +56 -0
  6. package/.ai/rules/react-hooks.md +29 -0
  7. package/.ai/rules/styling.md +83 -0
  8. package/AGENTS.md +37 -0
  9. package/README.md +125 -0
  10. package/figma.config.json +9 -0
  11. package/package.json +127 -0
  12. package/scripts/install-ai-rules.mjs +136 -0
  13. package/src/ThemeProvider.tsx +57 -0
  14. package/src/charts.ts +32 -0
  15. package/src/components/ActionCard/ActionCard.css.ts +60 -0
  16. package/src/components/ActionCard/ActionCard.tsx +154 -0
  17. package/src/components/ActionCard/index.ts +2 -0
  18. package/src/components/Anchor/Anchor.tsx +47 -0
  19. package/src/components/Anchor/index.ts +2 -0
  20. package/src/components/AppliedFiltersManagerBar/AppliedFiltersManagerBar.tsx +105 -0
  21. package/src/components/AppliedFiltersManagerBar/FilterBadge.css.ts +23 -0
  22. package/src/components/AppliedFiltersManagerBar/FilterBadge.tsx +50 -0
  23. package/src/components/AppliedFiltersManagerBar/index.ts +5 -0
  24. package/src/components/Badge/Badge.css.ts +72 -0
  25. package/src/components/Badge/Badge.figma.tsx +43 -0
  26. package/src/components/Badge/Badge.tsx +159 -0
  27. package/src/components/Badge/index.ts +2 -0
  28. package/src/components/BreadCrumb/BreadCrumb.tsx +62 -0
  29. package/src/components/BreadCrumb/index.ts +2 -0
  30. package/src/components/BulkActionBar/BulkActionBar.css.ts +26 -0
  31. package/src/components/BulkActionBar/BulkActionBar.tsx +164 -0
  32. package/src/components/BulkActionBar/index.ts +2 -0
  33. package/src/components/Button/Button.css.ts +272 -0
  34. package/src/components/Button/Button.figma.tsx +74 -0
  35. package/src/components/Button/Button.tsx +84 -0
  36. package/src/components/Button/index.ts +2 -0
  37. package/src/components/Charts/ChartTooltip.figma.tsx +33 -0
  38. package/src/components/Charts/ChartTooltip.tsx +101 -0
  39. package/src/components/Charts/MiniBarSparkline.tsx +75 -0
  40. package/src/components/Charts/StackedPatternBarChart.tsx +494 -0
  41. package/src/components/Charts/TrendAreaChart.css.ts +23 -0
  42. package/src/components/Charts/TrendAreaChart.tsx +210 -0
  43. package/src/components/Charts/index.ts +12 -0
  44. package/src/components/CodePanel/CodePanel.css.ts +113 -0
  45. package/src/components/CodePanel/CodePanel.tsx +121 -0
  46. package/src/components/CodePanel/index.ts +2 -0
  47. package/src/components/CommentComposer/CommentComposer.css.ts +60 -0
  48. package/src/components/CommentComposer/CommentComposer.tsx +181 -0
  49. package/src/components/CommentComposer/index.ts +2 -0
  50. package/src/components/ConfirmationModal/ConfirmationModal.tsx +149 -0
  51. package/src/components/ConfirmationModal/index.ts +2 -0
  52. package/src/components/ConfirmationTooltip/ConfirmationTooltip.tsx +132 -0
  53. package/src/components/ConfirmationTooltip/index.ts +2 -0
  54. package/src/components/DataDialog.figma.tsx +33 -0
  55. package/src/components/DataDialog.tsx +46 -0
  56. package/src/components/DataTable/DataTable.tsx +1042 -0
  57. package/src/components/DataTable/RowExpandToggle.tsx +105 -0
  58. package/src/components/DataTable/RowGroupHeader.tsx +190 -0
  59. package/src/components/DataTable/createActionsColumn.tsx +86 -0
  60. package/src/components/DataTable/index.ts +25 -0
  61. package/src/components/DatePicker/CustomRangePicker.tsx +59 -0
  62. package/src/components/DatePicker/DateInput.tsx +329 -0
  63. package/src/components/DatePicker/DateNavigator.tsx +486 -0
  64. package/src/components/DatePicker/DatePicker.tsx +242 -0
  65. package/src/components/DatePicker/MonthlyRangePicker.tsx +231 -0
  66. package/src/components/DatePicker/QuarterlyRangePicker.tsx +224 -0
  67. package/src/components/DatePicker/QuickPicksSidebar.tsx +242 -0
  68. package/src/components/DatePicker/YearlyRangePicker.tsx +171 -0
  69. package/src/components/DatePicker/index.ts +7 -0
  70. package/src/components/DatePicker/types.ts +12 -0
  71. package/src/components/DesignSystemPrimitives/FluidGrid.tsx +44 -0
  72. package/src/components/DesignSystemPrimitives/InteractivePrimitives.tsx +177 -0
  73. package/src/components/DesignSystemPrimitives/LayoutPrimitives.tsx +220 -0
  74. package/src/components/DesignSystemPrimitives/LayoutPrimitives.types.tsx +15 -0
  75. package/src/components/DesignSystemPrimitives/SurfacePrimitives.tsx +46 -0
  76. package/src/components/DesignSystemPrimitives/index.ts +55 -0
  77. package/src/components/Details/Details.css.ts +74 -0
  78. package/src/components/Details/Details.tsx +140 -0
  79. package/src/components/Details/index.ts +2 -0
  80. package/src/components/DownloadCard/DownloadCard.css.ts +22 -0
  81. package/src/components/DownloadCard/DownloadCard.tsx +63 -0
  82. package/src/components/DownloadCard/index.ts +2 -0
  83. package/src/components/Drawer/Drawer.css.ts +32 -0
  84. package/src/components/Drawer/Drawer.tsx +236 -0
  85. package/src/components/Drawer/hooks/useDetailDrawer.ts +61 -0
  86. package/src/components/Drawer/hooks/useDetailDrawerNavigation.ts +125 -0
  87. package/src/components/Drawer/hooks/useDetailDrawerNavigationContext.ts +66 -0
  88. package/src/components/EditableRichText/EditableRichText.css.ts +72 -0
  89. package/src/components/EditableRichText/EditableRichText.tsx +324 -0
  90. package/src/components/EditableRichText/index.ts +2 -0
  91. package/src/components/EditableSelect/EditableSelect.css.ts +62 -0
  92. package/src/components/EditableSelect/EditableSelect.tsx +224 -0
  93. package/src/components/EditableSelect/index.ts +2 -0
  94. package/src/components/EditableText/EditableText.tsx +377 -0
  95. package/src/components/EditableText/index.ts +2 -0
  96. package/src/components/EmptyState/EmptyState.figma.tsx +33 -0
  97. package/src/components/EmptyState/EmptyState.tsx +230 -0
  98. package/src/components/EmptyState/index.ts +2 -0
  99. package/src/components/ErrorBoundary.tsx +135 -0
  100. package/src/components/ErrorState/ErrorState.tsx +197 -0
  101. package/src/components/ErrorState/index.ts +2 -0
  102. package/src/components/FeatureCard.tsx +42 -0
  103. package/src/components/FilterMenu/FilterMenu.figma.tsx +30 -0
  104. package/src/components/FilterMenu/FilterMenu.tsx +198 -0
  105. package/src/components/FilterMenu/FilterSubMenuTypes/BooleanFilterSubmenu.tsx +46 -0
  106. package/src/components/FilterMenu/FilterSubMenuTypes/SearchableFilterSubmenu.tsx +239 -0
  107. package/src/components/FilterMenu/FilterSubMenuTypes/index.ts +8 -0
  108. package/src/components/FilterMenu/defaultFilterSchemas.ts +63 -0
  109. package/src/components/FilterMenu/helpers.ts +115 -0
  110. package/src/components/FilterMenu/index.ts +35 -0
  111. package/src/components/FilterMenu/types.ts +101 -0
  112. package/src/components/IconButton/IconButton.css.ts +272 -0
  113. package/src/components/IconButton/IconButton.figma.tsx +47 -0
  114. package/src/components/IconButton/IconButton.tsx +72 -0
  115. package/src/components/IconButton/README.md +230 -0
  116. package/src/components/IconButton/index.ts +2 -0
  117. package/src/components/InfiniteScrollSentinel.tsx +86 -0
  118. package/src/components/InfiniteScrollTrigger.tsx +78 -0
  119. package/src/components/InfoCard.figma.tsx +47 -0
  120. package/src/components/InfoCard.tsx +216 -0
  121. package/src/components/KbdHint/KbdHint.tsx +23 -0
  122. package/src/components/KbdHint/index.ts +2 -0
  123. package/src/components/LabeledField/LabeledField.tsx +21 -0
  124. package/src/components/LabeledField/index.ts +2 -0
  125. package/src/components/LookupSelect/LookupSelect.css.ts +149 -0
  126. package/src/components/LookupSelect/LookupSelect.tsx +325 -0
  127. package/src/components/LookupSelect/index.ts +2 -0
  128. package/src/components/Menu/Menu.css.ts +89 -0
  129. package/src/components/Menu/Menu.tsx +105 -0
  130. package/src/components/Menu/index.ts +2 -0
  131. package/src/components/MessageBox/MessageBox.tsx +168 -0
  132. package/src/components/MessageBox/index.ts +2 -0
  133. package/src/components/MetricDisplay/MetricDisplay.tsx +55 -0
  134. package/src/components/MetricDisplay/index.ts +1 -0
  135. package/src/components/MultiSelect/MultiSelect.tsx +278 -0
  136. package/src/components/MultiSelect/index.ts +2 -0
  137. package/src/components/Notifications/Notifications.tsx +12 -0
  138. package/src/components/Notifications/README.md +93 -0
  139. package/src/components/Notifications/index.ts +4 -0
  140. package/src/components/Notifications/showToast.tsx +100 -0
  141. package/src/components/PropertyRow/PropertyRow.tsx +96 -0
  142. package/src/components/PropertyRow/index.ts +2 -0
  143. package/src/components/RadioTile/RadioTile.tsx +253 -0
  144. package/src/components/RadioTile/index.ts +2 -0
  145. package/src/components/RichText/FormattingToolbar.css.ts +69 -0
  146. package/src/components/RichText/FormattingToolbar.tsx +112 -0
  147. package/src/components/RichText/RichTextInline.css.ts +54 -0
  148. package/src/components/RichText/RichTextInline.tsx +318 -0
  149. package/src/components/RichText/formattingCommands.ts +181 -0
  150. package/src/components/RichText/formattingTypes.ts +34 -0
  151. package/src/components/RichText/index.ts +49 -0
  152. package/src/components/RichText/richTextExtensions.ts +111 -0
  153. package/src/components/RichText/richTextHelpers.ts +65 -0
  154. package/src/components/RichText/richTextImage.ts +253 -0
  155. package/src/components/RichText/richTextImageHandlers.ts +244 -0
  156. package/src/components/RichText/richTextProse.css.ts +261 -0
  157. package/src/components/RichTextEditor/RichTextEditor.css.ts +82 -0
  158. package/src/components/RichTextEditor/RichTextEditor.tsx +204 -0
  159. package/src/components/RichTextEditor/index.ts +2 -0
  160. package/src/components/RichTextView/RichTextView.css.ts +11 -0
  161. package/src/components/RichTextView/RichTextView.tsx +114 -0
  162. package/src/components/RichTextView/index.ts +2 -0
  163. package/src/components/Schedule/Schedule.tsx +35 -0
  164. package/src/components/SchedulePicker/SchedulePicker.css.ts +42 -0
  165. package/src/components/SchedulePicker/SchedulePicker.tsx +130 -0
  166. package/src/components/SchedulePicker/index.ts +2 -0
  167. package/src/components/SearchableList/types.ts +30 -0
  168. package/src/components/SearchableSubMenu/SearchableSubMenu.css.ts +25 -0
  169. package/src/components/SearchableSubMenu/SearchableSubMenu.tsx +139 -0
  170. package/src/components/SearchableSubMenu/index.ts +2 -0
  171. package/src/components/Select/README.md +114 -0
  172. package/src/components/Select/Select.css.ts +110 -0
  173. package/src/components/Select/Select.tsx +133 -0
  174. package/src/components/Select/index.ts +2 -0
  175. package/src/components/SelectCreatable/SelectCreatable.css.ts +16 -0
  176. package/src/components/SelectCreatable/SelectCreatable.tsx +203 -0
  177. package/src/components/SelectCreatable/index.ts +2 -0
  178. package/src/components/SettingsCard/SettingsCard.tsx +98 -0
  179. package/src/components/SettingsCard/index.ts +2 -0
  180. package/src/components/Sidebar/Sidebar.css.ts +91 -0
  181. package/src/components/Sidebar/Sidebar.tsx +129 -0
  182. package/src/components/Sidebar/index.ts +5 -0
  183. package/src/components/SimpleList/SimpleList.css.ts +12 -0
  184. package/src/components/SimpleList/SimpleList.tsx +44 -0
  185. package/src/components/SimpleList/index.ts +2 -0
  186. package/src/components/SimpleTable/SimpleTable.tsx +296 -0
  187. package/src/components/SimpleTable/index.ts +2 -0
  188. package/src/components/SlashRichTextEditor/SelectionBubbleMenu.css.ts +62 -0
  189. package/src/components/SlashRichTextEditor/SelectionBubbleMenu.tsx +85 -0
  190. package/src/components/SlashRichTextEditor/SlashCommandMenu.css.ts +124 -0
  191. package/src/components/SlashRichTextEditor/SlashCommandMenu.tsx +168 -0
  192. package/src/components/SlashRichTextEditor/SlashRichTextEditor.css.ts +81 -0
  193. package/src/components/SlashRichTextEditor/SlashRichTextEditor.tsx +538 -0
  194. package/src/components/SlashRichTextEditor/SlashSuggestionExtension.ts +48 -0
  195. package/src/components/SlashRichTextEditor/index.ts +13 -0
  196. package/src/components/SlashRichTextEditor/types.ts +48 -0
  197. package/src/components/StatCard/StatCard.css.ts +70 -0
  198. package/src/components/StatCard/StatCard.tsx +201 -0
  199. package/src/components/StatCard/index.ts +1 -0
  200. package/src/components/StatusBadge/StatusBadge.tsx +70 -0
  201. package/src/components/StatusBadge/index.ts +2 -0
  202. package/src/components/StatusIndicator/StatusIndicator.tsx +67 -0
  203. package/src/components/StatusIndicator/index.ts +6 -0
  204. package/src/components/SubNavigation/SubNavigation.css.ts +72 -0
  205. package/src/components/SubNavigation/SubNavigation.tsx +104 -0
  206. package/src/components/SubNavigation/index.ts +2 -0
  207. package/src/components/SuspenseLoader.tsx +22 -0
  208. package/src/components/Table/SortableColumnHeader.tsx +99 -0
  209. package/src/components/Table/TableSkeletonRows.figma.tsx +22 -0
  210. package/src/components/Table/TableSkeletonRows.tsx +113 -0
  211. package/src/components/Table/index.ts +9 -0
  212. package/src/components/TableActionsMenu.tsx +58 -0
  213. package/src/components/TableCard.tsx +29 -0
  214. package/src/components/TableContainer/TableContainer.tsx +86 -0
  215. package/src/components/TableContainer/index.ts +2 -0
  216. package/src/components/TableControlBar/TableControlBar.tsx +156 -0
  217. package/src/components/TableControlBar/TableSelectionButton.tsx +57 -0
  218. package/src/components/TableControlBar/index.ts +13 -0
  219. package/src/components/TableControlBar/useTableControlBar.tsx +314 -0
  220. package/src/components/TableSelection/TableSelection.tsx +43 -0
  221. package/src/components/TableSelection/index.ts +5 -0
  222. package/src/components/Tabs/README.md +76 -0
  223. package/src/components/Tabs/Tabs.css.ts +54 -0
  224. package/src/components/Tabs/Tabs.figma.tsx +47 -0
  225. package/src/components/Tabs/Tabs.tsx +96 -0
  226. package/src/components/Tabs/index.ts +8 -0
  227. package/src/components/TextInput/README.md +98 -0
  228. package/src/components/TextInput/SearchTextInput.figma.tsx +22 -0
  229. package/src/components/TextInput/SearchTextInput.tsx +150 -0
  230. package/src/components/TextInput/TextInput.figma.tsx +44 -0
  231. package/src/components/TextInput/TextInput.tsx +42 -0
  232. package/src/components/TextInput/index.ts +4 -0
  233. package/src/components/ThemeSwitcher.figma.tsx +28 -0
  234. package/src/components/ThemeSwitcher.tsx +69 -0
  235. package/src/components/TrendBadge/TrendBadge.tsx +76 -0
  236. package/src/components/TrendBadge/index.ts +2 -0
  237. package/src/components/TruncatedText.tsx +115 -0
  238. package/src/components/Typography/Text.tsx +74 -0
  239. package/src/components/Typography/Title.tsx +100 -0
  240. package/src/components/Typography/index.ts +4 -0
  241. package/src/geist-fonts.ts +48 -0
  242. package/src/hooks/index.ts +31 -0
  243. package/src/hooks/useFilters.ts +152 -0
  244. package/src/hooks/useInfiniteScroll.ts +62 -0
  245. package/src/hooks/usePlatform.ts +33 -0
  246. package/src/hooks/useServerTable.ts +495 -0
  247. package/src/hooks/useTableSelection.ts +102 -0
  248. package/src/hooks/useTableSort.ts +259 -0
  249. package/src/index.ts +483 -0
  250. package/src/mantine.ts +25 -0
  251. package/src/theme/mantineVars.ts +12 -0
  252. package/src/theme/themeContract.css.ts +131 -0
  253. package/src/theme/themeVars.ts +31 -0
  254. package/src/theme.ts +168 -0
  255. package/src/tokens/color-types.ts +107 -0
  256. package/src/tokens/colors.ts +243 -0
  257. package/src/tokens/index.ts +14 -0
  258. package/src/tokens/radius.ts +17 -0
  259. package/src/tokens/semantic-colors.ts +224 -0
  260. package/src/tokens/semantic-tokens-css.ts +53 -0
  261. package/src/tokens/shadows.ts +11 -0
  262. package/src/tokens/spacing.ts +20 -0
  263. package/src/tokens/text-styles.ts +179 -0
  264. package/src/tokens/typography.ts +40 -0
  265. package/src/tokens/zIndex.ts +27 -0
  266. package/src/types/mantine-theme.d.ts +17 -0
  267. package/src/types/tanstack-table.d.ts +22 -0
  268. package/src/utils/avatar.ts +150 -0
  269. package/src/utils/chartHelpers.ts +53 -0
  270. package/src/utils/color-props.ts +77 -0
  271. package/src/utils/createDesignComponent.tsx +104 -0
  272. package/src/utils/nestFlatRows.ts +111 -0
  273. 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
+ }