@altruist03/ui 0.1.0 → 0.2.1

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 (207) hide show
  1. package/assets/images/noData.webp +0 -0
  2. package/dist/index.js +15237 -650
  3. package/dist/index.js.map +1 -1
  4. package/dist/noData-XPRHKMFR.webp +0 -0
  5. package/package.json +21 -7
  6. package/src/ThemeProvider/ThemeContext.tsx +75 -0
  7. package/src/ThemeProvider/index.ts +7 -0
  8. package/src/assets/images/noData.webp +0 -0
  9. package/src/assets.d.ts +10 -0
  10. package/src/components/Accordion/Accordion.stories.tsx +64 -0
  11. package/src/components/Accordion/Accordion.tsx +219 -0
  12. package/src/components/Accordion/Accordion.types.ts +16 -0
  13. package/src/components/Avatar/Avatar.stories.tsx +304 -0
  14. package/src/components/Avatar/Avatar.tsx +211 -0
  15. package/src/components/Avatar/Avatar.types.ts +33 -0
  16. package/src/components/Badge/Badge.stories.tsx +143 -0
  17. package/src/components/Badge/Badge.tsx +138 -0
  18. package/src/components/Badge/Badge.types.ts +24 -0
  19. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
  20. package/src/components/Breadcrumb/Breadcrumb.tsx +120 -0
  21. package/src/components/Breadcrumb/Breadcrumb.types.ts +22 -0
  22. package/src/components/Button/Button.stories.tsx +457 -0
  23. package/src/components/Button/Button.tsx +202 -0
  24. package/src/components/Button/Button.types.ts +18 -0
  25. package/src/components/Checkbox/Checkbox.stories.tsx +254 -0
  26. package/src/components/Checkbox/Checkbox.tsx +352 -0
  27. package/src/components/Checkbox/Checkbox.types.ts +123 -0
  28. package/src/components/CloseButton/CloseButton.stories.tsx +100 -0
  29. package/src/components/CloseButton/CloseButton.tsx +117 -0
  30. package/src/components/CloseButton/CloseButton.types.ts +15 -0
  31. package/src/components/CollapsibleSearch/CollapsibleSearch.tsx +120 -0
  32. package/src/components/DataTable/DataTable.stories.tsx +233 -0
  33. package/src/components/DataTable/DataTable.tsx +968 -0
  34. package/src/components/DataTable/DataTable.types.ts +49 -0
  35. package/src/components/DatePicker/DatePicker.stories.tsx +341 -0
  36. package/src/components/DatePicker/DatePicker.tsx +669 -0
  37. package/src/components/DatePicker/DatePicker.types.ts +105 -0
  38. package/src/components/DatePicker/DateTimePicker.tsx +35 -0
  39. package/src/components/DateRangePicker/DateRangePicker.tsx +179 -0
  40. package/src/components/DateRangePicker/DateRangePicker.types.ts +34 -0
  41. package/src/components/Drawer/Drawer.stories.tsx +243 -0
  42. package/src/components/Drawer/Drawer.tsx +196 -0
  43. package/src/components/Drawer/Drawer.types.ts +60 -0
  44. package/src/components/FileUpload/FileUpload.stories.tsx +162 -0
  45. package/src/components/FileUpload/FileUpload.tsx +216 -0
  46. package/src/components/FileUpload/FileUpload.types.ts +80 -0
  47. package/src/components/FilterPopover/FilterPopover.stories.tsx +173 -0
  48. package/src/components/FilterPopover/FilterPopover.tsx +310 -0
  49. package/src/components/FilterPopover/FilterPopover.types.ts +26 -0
  50. package/src/components/ImageViewer/ImageViewerToolbar.jsx +81 -0
  51. package/src/components/ImageViewer/useImagePanRotate.js +102 -0
  52. package/src/components/IpWhitelistField/IpWhitelistField.stories.tsx +138 -0
  53. package/src/components/IpWhitelistField/IpWhitelistField.tsx +164 -0
  54. package/src/components/IpWhitelistField/IpWhitelistField.types.ts +42 -0
  55. package/src/components/Loader/Loader.stories.tsx +95 -0
  56. package/src/components/Loader/Loader.tsx +132 -0
  57. package/src/components/Loader/Loader.types.ts +6 -0
  58. package/src/components/Menu/Menu.tsx +143 -0
  59. package/src/components/Menu/Menu.types.ts +28 -0
  60. package/src/components/MenuButton/MenuButton.stories.tsx +140 -0
  61. package/src/components/MenuButton/MenuButton.tsx +45 -0
  62. package/src/components/MenuButton/MenuButton.types.ts +21 -0
  63. package/src/components/Modal/Modal.stories.tsx +266 -0
  64. package/src/components/Modal/Modal.tsx +399 -0
  65. package/src/components/Modal/index.ts +2 -0
  66. package/src/components/Modal/types.ts +126 -0
  67. package/src/components/Pagination/Pagination.stories.tsx +167 -0
  68. package/src/components/Pagination/Pagination.tsx +266 -0
  69. package/src/components/Pagination/Pagination.types.ts +20 -0
  70. package/src/components/PeriodSelector/PeriodSelector.stories.tsx +50 -0
  71. package/src/components/PeriodSelector/PeriodSelector.tsx +47 -0
  72. package/src/components/PeriodSelector/PeriodSelector.types.ts +10 -0
  73. package/src/components/PhoneField/PhoneField.stories.tsx +141 -0
  74. package/src/components/PhoneField/PhoneField.tsx +284 -0
  75. package/src/components/PhoneField/PhoneField.types.ts +89 -0
  76. package/src/components/PhoneField/countryCodes.ts +236 -0
  77. package/src/components/Progress/Progress.stories.tsx +83 -0
  78. package/src/components/Progress/Progress.tsx +91 -0
  79. package/src/components/Progress/Progress.types.ts +22 -0
  80. package/src/components/RadioButton/RadioButton.stories.tsx +332 -0
  81. package/src/components/RadioButton/RadioButton.tsx +427 -0
  82. package/src/components/RadioButton/RadioButton.types.ts +117 -0
  83. package/src/components/ReportCard/ReportCard.tsx +183 -0
  84. package/src/components/ReportCard/ReportCard.types.ts +17 -0
  85. package/src/components/SectionIcon/SectionIcon.tsx +87 -0
  86. package/src/components/SectionIcon/SectionIcon.types.ts +23 -0
  87. package/src/components/SegmentedInputField/SegmentedInputField.stories.tsx +165 -0
  88. package/src/components/SegmentedInputField/SegmentedInputField.tsx +174 -0
  89. package/src/components/SegmentedInputField/SegmentedInputField.types.ts +51 -0
  90. package/src/components/SelectScroll/SelectScroll.stories.tsx +336 -0
  91. package/src/components/SelectScroll/SelectScroll.tsx +1119 -0
  92. package/src/components/SelectScroll/SelectScroll.types.ts +60 -0
  93. package/src/components/Selector/Selector.stories.tsx +382 -0
  94. package/src/components/Selector/Selector.tsx +1310 -0
  95. package/src/components/Selector/Selector.types.ts +71 -0
  96. package/src/components/Skeleton/Skeleton.stories.tsx +133 -0
  97. package/src/components/Skeleton/Skeleton.tsx +20 -0
  98. package/src/components/Skeleton/Skeleton.types.ts +3 -0
  99. package/src/components/Spinner/Spinner.stories.tsx +84 -0
  100. package/src/components/Spinner/Spinner.tsx +79 -0
  101. package/src/components/Spinner/Spinner.types.ts +8 -0
  102. package/src/components/StatCard/StatCard.stories.tsx +125 -0
  103. package/src/components/StatCard/StatCard.tsx +167 -0
  104. package/src/components/StatCard/StatCard.types.ts +20 -0
  105. package/src/components/StatusBadge/StatusBadge.stories.tsx +179 -0
  106. package/src/components/StatusBadge/StatusBadge.tsx +74 -0
  107. package/src/components/StatusBadge/StatusBadge.types.ts +23 -0
  108. package/src/components/Stepper/Stepper.stories.tsx +85 -0
  109. package/src/components/Stepper/Stepper.tsx +138 -0
  110. package/src/components/Stepper/Stepper.types.ts +16 -0
  111. package/src/components/SubTabs/SubTabs.stories.tsx +128 -0
  112. package/src/components/SubTabs/SubTabs.tsx +279 -0
  113. package/src/components/SubTabs/SubTabs.types.ts +44 -0
  114. package/src/components/SummaryCard/SummaryCard.stories.tsx +100 -0
  115. package/src/components/SummaryCard/SummaryCard.tsx +141 -0
  116. package/src/components/SummaryCard/SummaryCard.types.ts +22 -0
  117. package/src/components/Switch/Switch.stories.tsx +59 -0
  118. package/src/components/Switch/Switch.tsx +217 -0
  119. package/src/components/Switch/Switch.types.ts +22 -0
  120. package/src/components/Tabs/Tabs.stories.tsx +178 -0
  121. package/src/components/Tabs/Tabs.tsx +197 -0
  122. package/src/components/Tabs/Tabs.types.ts +34 -0
  123. package/src/components/TextField/TextField.stories.tsx +249 -0
  124. package/src/components/TextField/TextField.tsx +462 -0
  125. package/src/components/TextField/TextField.types.ts +141 -0
  126. package/src/components/Toast/Toast.stories.tsx +237 -0
  127. package/src/components/Toast/Toast.tsx +246 -0
  128. package/src/components/Toast/Toast.types.ts +31 -0
  129. package/src/components/ToggleButton/ToggleButton.stories.tsx +105 -0
  130. package/src/components/ToggleButton/ToggleButton.tsx +267 -0
  131. package/src/components/ToggleButton/ToggleButton.types.ts +19 -0
  132. package/src/components/UploadDropzone/UploadDropzone.jsx +78 -0
  133. package/src/components/UploadDropzone2/UploadDropzone.stories.tsx +94 -0
  134. package/src/components/UploadDropzone2/UploadDropzone.tsx +222 -0
  135. package/src/components/UploadDropzone2/UploadDropzone.types.ts +54 -0
  136. package/src/components/ViewEditToggle/ViewEditToggle.stories.tsx +40 -0
  137. package/src/components/ViewEditToggle/ViewEditToggle.tsx +239 -0
  138. package/src/components/ViewEditToggle/ViewEditToggle.types.ts +27 -0
  139. package/src/components/index.ts +157 -0
  140. package/src/icons/ReportDollarIcon.tsx +71 -0
  141. package/src/icons/index.ts +27 -0
  142. package/src/index.ts +214 -0
  143. package/src/storybook/preview.tsx +64 -0
  144. package/src/theme/index.ts +4 -0
  145. package/src/theme/types.d.ts +1 -0
  146. package/src/themes/cpc.ts +4 -0
  147. package/src/themes/firebird.ts +4 -0
  148. package/src/tokens/brands/cpc/themes/dark.json +82 -0
  149. package/src/tokens/brands/cpc/themes/light.json +1 -0
  150. package/src/tokens/brands/default/brand.json +20 -0
  151. package/src/tokens/brands/firebird/themes/dark.json +28 -0
  152. package/src/tokens/brands/firebird/themes/light.json +20 -0
  153. package/src/tokens/button.ts +24 -0
  154. package/src/tokens/components/accordion.json +52 -0
  155. package/src/tokens/components/avatar.json +40 -0
  156. package/src/tokens/components/badge.json +41 -0
  157. package/src/tokens/components/breadcrumb.json +13 -0
  158. package/src/tokens/components/button.json +67 -0
  159. package/src/tokens/components/card.json +39 -0
  160. package/src/tokens/components/checkbox.json +64 -0
  161. package/src/tokens/components/closebutton.json +55 -0
  162. package/src/tokens/components/datatable.json +40 -0
  163. package/src/tokens/components/datepicker.json +99 -0
  164. package/src/tokens/components/drawer.json +23 -0
  165. package/src/tokens/components/field.json +29 -0
  166. package/src/tokens/components/fileupload.json +57 -0
  167. package/src/tokens/components/filterpopover.json +29 -0
  168. package/src/tokens/components/input.json +31 -0
  169. package/src/tokens/components/ipwhitelistfield.json +16 -0
  170. package/src/tokens/components/loader.json +15 -0
  171. package/src/tokens/components/menu.json +21 -0
  172. package/src/tokens/components/modal.json +25 -0
  173. package/src/tokens/components/pagination.json +45 -0
  174. package/src/tokens/components/phonefield.json +41 -0
  175. package/src/tokens/components/progress.json +12 -0
  176. package/src/tokens/components/radio.json +87 -0
  177. package/src/tokens/components/reportcard.json +17 -0
  178. package/src/tokens/components/selector.json +87 -0
  179. package/src/tokens/components/selectscroll.json +65 -0
  180. package/src/tokens/components/skeleton.json +6 -0
  181. package/src/tokens/components/stepper.json +38 -0
  182. package/src/tokens/components/subtabs.json +45 -0
  183. package/src/tokens/components/switch.json +55 -0
  184. package/src/tokens/components/tabs.json +38 -0
  185. package/src/tokens/components/textfield.json +59 -0
  186. package/src/tokens/components/togglebutton.json +48 -0
  187. package/src/tokens/components/uploaddropzone.json +61 -0
  188. package/src/tokens/drawer.ts +37 -0
  189. package/src/tokens/foundation/breakpoints.json +10 -0
  190. package/src/tokens/foundation/colors.json +122 -0
  191. package/src/tokens/foundation/opacity.json +16 -0
  192. package/src/tokens/foundation/radius.json +14 -0
  193. package/src/tokens/foundation/size.json +38 -0
  194. package/src/tokens/foundation/spacing.json +19 -0
  195. package/src/tokens/foundation/typography.json +43 -0
  196. package/src/tokens/index.ts +3665 -0
  197. package/src/tokens/modal.ts +42 -0
  198. package/src/tokens/semantic/color.json +148 -0
  199. package/src/tokens/semantic/field.json +28 -0
  200. package/src/tokens/semantic/opacity.json +5 -0
  201. package/src/tokens/semantic/size.json +722 -0
  202. package/src/tokens/semantic/spacing.json +27 -0
  203. package/src/tokens/semantic/typography.json +87 -0
  204. package/src/utils/scrollLock.ts +44 -0
  205. package/dist/index.css +0 -514
  206. package/dist/index.css.map +0 -1
  207. package/dist/index.d.ts +0 -289
@@ -0,0 +1,968 @@
1
+ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { getDataTableTokensFromResolved } from '../../tokens';
3
+ import { useTokens } from '../../ThemeProvider/ThemeContext';
4
+ import type { DataTableProps } from './DataTable.types';
5
+ import NoDataIm from '../../../assets/images/noData.webp';
6
+
7
+ const SKELETON_KEYFRAMES_ID = 'ds-datatable-skeleton-keyframes';
8
+ const MOBILE_MAX_WIDTH = 767;
9
+
10
+ function useIsMobileScreen() {
11
+ const [isMobile, setIsMobile] = useState(
12
+ () => typeof window !== 'undefined' && window.innerWidth <= MOBILE_MAX_WIDTH
13
+ );
14
+
15
+ useEffect(() => {
16
+ const mediaQuery = window.matchMedia(`(max-width: ${MOBILE_MAX_WIDTH}px)`);
17
+ const update = () => setIsMobile(mediaQuery.matches);
18
+ update();
19
+ mediaQuery.addEventListener('change', update);
20
+ return () => mediaQuery.removeEventListener('change', update);
21
+ }, []);
22
+
23
+ return isMobile;
24
+ }
25
+
26
+ const COLUMN_DIVIDER_WIDTH = '1px';
27
+ const EXPANDABLE_TEXT_PREVIEW_LENGTH = 80;
28
+ /** Keeps Read More columns from growing to the full string width under table-layout:auto. */
29
+ const EXPANDABLE_CELL_MAX_WIDTH = 280;
30
+ /** Comfortable min width for month headers like "Jun 2026" when auto-fitting. */
31
+ const DEFAULT_MIN_SCROLL_COLUMN_WIDTH = 104;
32
+ /** Soft cap so ultra-wide screens don't show an excessive number of months at once. */
33
+ const DEFAULT_MAX_SCROLL_COLUMNS = 6;
34
+
35
+ /** Return-from-list row highlight (green, slightly darker for visibility). */
36
+ const HIGHLIGHT_ROW_BACKGROUND = '#C8E6C9';
37
+ const HIGHLIGHT_ROW_BACKGROUND_HOVER = '#A5D6A7';
38
+ const HIGHLIGHT_ROW_TRANSITION = 'background-color 2s ease';
39
+ /** Hover uses a short transition so row feedback stays snappy; highlight fade-out uses HIGHLIGHT_ROW_TRANSITION from React state. */
40
+ const ROW_HOVER_TRANSITION = 'background-color 0.15s ease';
41
+
42
+ /** Align headers/cells from explicit meta, falling back to API columnType. */
43
+ function textAlignForColumnType(column: { meta?: { columnType?: string } } | null | undefined) {
44
+ const raw = String(column?.meta?.columnType ?? '')
45
+ .trim()
46
+ .toLowerCase();
47
+ const numericTypes = new Set([
48
+ 'integer',
49
+ 'long',
50
+ 'int',
51
+ 'double',
52
+ 'float',
53
+ 'number',
54
+ 'bigint',
55
+ 'decimal',
56
+ ]);
57
+ if (numericTypes.has(raw)) return 'right' as const;
58
+ if (raw === 'string') return 'left' as const;
59
+ return 'left' as const;
60
+ }
61
+
62
+ function resolveExplicitAlign(value: unknown): 'left' | 'right' | 'center' | undefined {
63
+ const raw = String(value ?? '')
64
+ .trim()
65
+ .toLowerCase();
66
+ if (raw === 'left' || raw === 'right' || raw === 'center') return raw;
67
+ return undefined;
68
+ }
69
+
70
+ function headerAlignForColumn(column: { meta?: { headerAlign?: string } } | null | undefined) {
71
+ return resolveExplicitAlign(column?.meta?.headerAlign) ?? ('center' as const);
72
+ }
73
+
74
+ function cellAlignForColumn(
75
+ column: { meta?: { cellAlign?: string; columnType?: string } } | null | undefined
76
+ ) {
77
+ return resolveExplicitAlign(column?.meta?.cellAlign) ?? textAlignForColumnType(column);
78
+ }
79
+
80
+ function stickyPositionForColumn(
81
+ column: { meta?: { sticky?: string } } | null | undefined
82
+ ): 'left' | 'right' | undefined {
83
+ const raw = String(column?.meta?.sticky ?? '')
84
+ .trim()
85
+ .toLowerCase();
86
+ if (raw === 'left' || raw === 'right') return raw;
87
+ return undefined;
88
+ }
89
+
90
+ function extraCellStyleFromColumn(cell: any): React.CSSProperties {
91
+ const getCellStyle = cell?.column?.meta?.getCellStyle;
92
+ if (typeof getCellStyle !== 'function') return {};
93
+ const style = getCellStyle(cell);
94
+ return style && typeof style === 'object' ? (style as React.CSSProperties) : {};
95
+ }
96
+
97
+ function columnMinWidthStyle(column: any): React.CSSProperties {
98
+ const minWidth = column?.meta?.minWidth;
99
+ if (minWidth == null || minWidth === '') return {};
100
+ return { minWidth };
101
+ }
102
+
103
+ function columnHeaderExtraStyle(column: any): React.CSSProperties {
104
+ if (!column?.meta?.nowrapHeader) return {};
105
+ return { whiteSpace: 'nowrap' };
106
+ }
107
+
108
+ /**
109
+ * Match highlighted row when API uses userId, clientId, clientMasterId, positionId, deviceId, or id
110
+ * on row.original. positionId is checked before deviceId because GPS position rows carry both
111
+ * (deviceId there is the device's IMEI, not a row identifier) and positionId is the one that's
112
+ * actually unique per row.
113
+ */
114
+ function getRowClientIdForHighlight(original: unknown): string {
115
+ if (!original || typeof original !== 'object') return '';
116
+ const o = original as Record<string, unknown>;
117
+ const v =
118
+ o.selectionKey ??
119
+ o.otherDeductionTransactionId ??
120
+ o.loanId ??
121
+ o.userId ??
122
+ o.clientId ??
123
+ o.clientMasterId ??
124
+ o.positionId ??
125
+ o.deviceId ??
126
+ o.id;
127
+ return v != null ? String(v) : '';
128
+ }
129
+
130
+ function ensureSkeletonKeyframes() {
131
+ if (typeof document === 'undefined') return;
132
+ if (document.getElementById(SKELETON_KEYFRAMES_ID)) return;
133
+
134
+ const css = `
135
+ @keyframes dsSkeletonShimmer {
136
+ 0% { background-position: -200px 0; }
137
+ 100% { background-position: calc(200px + 100%) 0; }
138
+ }`;
139
+
140
+ const style = document.createElement('style');
141
+ style.id = SKELETON_KEYFRAMES_ID;
142
+ style.textContent = css;
143
+ document.head.appendChild(style);
144
+ }
145
+
146
+ /* Sort icon – uses the project's sorting_icon.svg asset */
147
+ // import ArrowUpIcon from "@/assets/images/sorting_icon.svg";
148
+
149
+ function isHtmlColumn(column: { meta?: { columnType?: string } } | null | undefined) {
150
+ return String(column?.meta?.columnType ?? '').toLowerCase() === 'html';
151
+ }
152
+
153
+ function isExpandableTextValue(
154
+ value: unknown,
155
+ column?: { meta?: { columnType?: string; disableExpandableCell?: boolean } } | null
156
+ ) {
157
+ if (isHtmlColumn(column) || column?.meta?.disableExpandableCell) return false;
158
+ if (typeof value !== 'string') return false;
159
+ if (/^\d{4}-\d{2}-\d{2}T/.test(value)) return false;
160
+ return value.length > EXPANDABLE_TEXT_PREVIEW_LENGTH;
161
+ }
162
+
163
+ /** Status / badge columns must stay on one line; squeeze → horizontal scroll instead of a broken badge. */
164
+ function isStatusBadgeColumn(
165
+ column:
166
+ | {
167
+ id?: string;
168
+ Header?: unknown;
169
+ accessor?: unknown;
170
+ meta?: { columnType?: string; wrapText?: boolean };
171
+ }
172
+ | null
173
+ | undefined
174
+ ) {
175
+ if (isHtmlColumn(column)) return true;
176
+ const key = String(column?.id ?? column?.accessor ?? '')
177
+ .trim()
178
+ .toLowerCase();
179
+ const header = String(column?.Header ?? '')
180
+ .trim()
181
+ .toLowerCase();
182
+ return key === 'status' || key === 'statusname' || key === 'status_name' || header === 'status';
183
+ }
184
+
185
+ const ExpandableCell = ({
186
+ value,
187
+ renderedCell,
188
+ column,
189
+ }: {
190
+ value: any;
191
+ renderedCell: React.ReactNode;
192
+ column?: { meta?: { columnType?: string; disableExpandableCell?: boolean } } | null;
193
+ }) => {
194
+ const [isExpanded, setIsExpanded] = React.useState(false);
195
+
196
+ if (!isExpandableTextValue(value, column)) {
197
+ return <>{renderedCell}</>;
198
+ }
199
+
200
+ return (
201
+ <div
202
+ style={{
203
+ display: 'block',
204
+ boxSizing: 'border-box',
205
+ width: '100%',
206
+ maxWidth: EXPANDABLE_CELL_MAX_WIDTH,
207
+ whiteSpace: 'normal',
208
+ overflowWrap: 'anywhere',
209
+ wordBreak: 'break-word',
210
+ }}
211
+ >
212
+ {isExpanded ?
213
+ renderedCell
214
+ : <span>{`${value.slice(0, EXPANDABLE_TEXT_PREVIEW_LENGTH).trimEnd()}...`}</span>}
215
+ <button
216
+ type="button"
217
+ onClick={(e) => {
218
+ e.stopPropagation();
219
+ setIsExpanded(!isExpanded);
220
+ }}
221
+ style={{
222
+ color: '#2563eb',
223
+ background: 'none',
224
+ border: 'none',
225
+ cursor: 'pointer',
226
+ padding: 0,
227
+ marginLeft: 2,
228
+ fontSize: '0.85em',
229
+ textDecoration: 'underline',
230
+ fontWeight: 500,
231
+ }}
232
+ >
233
+ {isExpanded ? 'Read Less' : 'Read More'}
234
+ </button>
235
+ </div>
236
+ );
237
+ };
238
+
239
+ function shouldWrapTableCell(
240
+ column:
241
+ | {
242
+ id?: string;
243
+ Header?: unknown;
244
+ accessor?: unknown;
245
+ meta?: { wrapText?: boolean; columnType?: string };
246
+ }
247
+ | null
248
+ | undefined,
249
+ cellValue: unknown,
250
+ preventHorizontalScroll: boolean
251
+ ) {
252
+ // Status badges / HTML pills must never wrap mid-word; prefer horizontal scroll.
253
+ if (isStatusBadgeColumn(column) && !column?.meta?.wrapText) return false;
254
+ if (preventHorizontalScroll) return true;
255
+ if (column?.meta?.wrapText) return true;
256
+ return (
257
+ typeof cellValue === 'string' &&
258
+ cellValue.length > EXPANDABLE_TEXT_PREVIEW_LENGTH &&
259
+ !/^\d{4}-\d{2}-\d{2}T/.test(cellValue)
260
+ );
261
+ }
262
+
263
+ export function DataTable({
264
+ getTableProps,
265
+ getTableBodyProps,
266
+ headerGroups,
267
+ page,
268
+ rows,
269
+ prepareRow,
270
+ loading = false,
271
+ Status,
272
+ length,
273
+ emptyStateHeight = 300,
274
+ stickyHeader = true,
275
+ className,
276
+ highlightRowClientId = null,
277
+ getHighlightRowId,
278
+ preventHorizontalScroll = false,
279
+ frozenPaneMaxWidth = '42%',
280
+ visibleScrollColumns,
281
+ minScrollColumnWidth = DEFAULT_MIN_SCROLL_COLUMN_WIDTH,
282
+ maxScrollColumns = DEFAULT_MAX_SCROLL_COLUMNS,
283
+ }: DataTableProps) {
284
+ const resolvedTokens = useTokens();
285
+ const isMobileScreen = useIsMobileScreen();
286
+ const tokens = getDataTableTokensFromResolved(resolvedTokens);
287
+ const injectedRef = useRef(false);
288
+ const frozenTableRef = useRef<HTMLTableElement>(null);
289
+ const scrollTableRef = useRef<HTMLTableElement>(null);
290
+ const frozenPaneRef = useRef<HTMLDivElement>(null);
291
+ const scrollPaneRef = useRef<HTMLDivElement>(null);
292
+ const [hoveredRowIndex, setHoveredRowIndex] = useState<number | null>(null);
293
+ const tableBorderRadius = resolvedTokens.foundation?.radius?.lg ?? '8px';
294
+ const bodyColumnDividerColor =
295
+ resolvedTokens.semantic?.color?.secondary?.default ?? tokens.cellBorderBottom;
296
+
297
+ // function SortIcon({ direction }: { direction: 'asc' | 'desc' | null; color?: string }) {
298
+ // return (
299
+ // <img
300
+ // // src={ArrowUpIcon}
301
+ // src={''}
302
+ // alt={
303
+ // direction === 'asc' ? 'Sort ascending'
304
+ // : direction === 'desc' ?
305
+ // 'Sort descending'
306
+ // : 'Sortable'
307
+ // }
308
+ // style={{
309
+ // marginLeft: 8,
310
+ // width: tokens.sortIconWidth,
311
+ // height: tokens.sortIconHeight,
312
+ // verticalAlign: 'middle',
313
+ // opacity: direction ? 1 : 0.4,
314
+ // transform: direction === 'desc' ? 'rotate(180deg)' : 'none',
315
+ // transition: 'transform 0.2s, opacity 0.2s',
316
+ // }}
317
+ // />
318
+ // );
319
+ // }
320
+
321
+ useEffect(() => {
322
+ if (!injectedRef.current) {
323
+ ensureSkeletonKeyframes();
324
+ injectedRef.current = true;
325
+ }
326
+ }, []);
327
+
328
+ const skelCols = headerGroups?.length > 0 ? headerGroups[0].headers.length : 3;
329
+ const skeletonRows = Status?.pageSize || length || 4;
330
+ const renderRows = page || rows || [];
331
+ const hasRows = renderRows.length > 0;
332
+ const allHeaders = headerGroups?.[0]?.headers ?? [];
333
+ const frozenHeaders = allHeaders.filter(
334
+ (header: any) => stickyPositionForColumn(header) === 'left'
335
+ );
336
+ const scrollHeaders = allHeaders.filter(
337
+ (header: any) => stickyPositionForColumn(header) !== 'left'
338
+ );
339
+ const hasFrozenSplit =
340
+ !isMobileScreen &&
341
+ !preventHorizontalScroll &&
342
+ frozenHeaders.length > 0 &&
343
+ scrollHeaders.length > 0 &&
344
+ (loading || hasRows);
345
+ const useEqualWidthScroll =
346
+ visibleScrollColumns === 'auto' ||
347
+ (typeof visibleScrollColumns === 'number' && visibleScrollColumns > 0);
348
+ const resolvedMinScrollColumnWidth =
349
+ typeof minScrollColumnWidth === 'number' && minScrollColumnWidth > 0 ?
350
+ minScrollColumnWidth
351
+ : DEFAULT_MIN_SCROLL_COLUMN_WIDTH;
352
+ const resolvedMaxScrollColumns =
353
+ typeof maxScrollColumns === 'number' && maxScrollColumns > 0 ?
354
+ maxScrollColumns
355
+ : DEFAULT_MAX_SCROLL_COLUMNS;
356
+
357
+ const resolveColumnsInView = (paneWidth: number, columnCount: number) => {
358
+ if (!useEqualWidthScroll || columnCount === 0 || paneWidth <= 0) return null;
359
+ if (visibleScrollColumns === 'auto') {
360
+ const fitted = Math.max(1, Math.floor(paneWidth / resolvedMinScrollColumnWidth));
361
+ return Math.min(fitted, resolvedMaxScrollColumns, columnCount);
362
+ }
363
+ return Math.min(visibleScrollColumns as number, columnCount);
364
+ };
365
+
366
+ useLayoutEffect(() => {
367
+ if (!hasFrozenSplit) return;
368
+ const leftTable = frozenTableRef.current;
369
+ const rightTable = scrollTableRef.current;
370
+ const leftPane = frozenPaneRef.current;
371
+ const rightPane = scrollPaneRef.current;
372
+ if (!leftTable || !rightTable) return;
373
+
374
+ let rafId = 0;
375
+ let running = false;
376
+
377
+ const applyEqualScrollColumnWidths = () => {
378
+ if (!useEqualWidthScroll || !rightPane) return;
379
+ const columnCount = scrollHeaders.length;
380
+ if (columnCount === 0) return;
381
+
382
+ const paneWidth = rightPane.clientWidth;
383
+ if (paneWidth <= 0) return;
384
+
385
+ const columnsInView = resolveColumnsInView(paneWidth, columnCount);
386
+ if (!columnsInView) return;
387
+
388
+ const colWidth = paneWidth / columnsInView;
389
+ const totalWidth = colWidth * columnCount;
390
+
391
+ rightTable.style.tableLayout = 'fixed';
392
+ rightTable.style.width = `${totalWidth}px`;
393
+ rightTable.style.minWidth = `${totalWidth}px`;
394
+
395
+ rightTable.querySelectorAll('thead th').forEach((th) => {
396
+ const el = th as HTMLElement;
397
+ el.style.width = `${colWidth}px`;
398
+ el.style.minWidth = `${colWidth}px`;
399
+ el.style.maxWidth = `${colWidth}px`;
400
+ el.style.boxSizing = 'border-box';
401
+ el.style.overflow = 'hidden';
402
+ });
403
+ rightTable.querySelectorAll('tbody td').forEach((td) => {
404
+ const el = td as HTMLElement;
405
+ el.style.width = `${colWidth}px`;
406
+ el.style.minWidth = `${colWidth}px`;
407
+ el.style.maxWidth = `${colWidth}px`;
408
+ el.style.boxSizing = 'border-box';
409
+ el.style.overflow = 'hidden';
410
+ });
411
+ };
412
+
413
+ const sync = () => {
414
+ if (running) return;
415
+ running = true;
416
+ rafId = requestAnimationFrame(() => {
417
+ try {
418
+ applyEqualScrollColumnWidths();
419
+
420
+ const leftRows = leftTable.querySelectorAll('tr');
421
+ const rightRows = rightTable.querySelectorAll('tr');
422
+ const count = Math.min(leftRows.length, rightRows.length);
423
+ for (let i = 0; i < count; i += 1) {
424
+ const leftRow = leftRows[i] as HTMLElement;
425
+ const rightRow = rightRows[i] as HTMLElement;
426
+ leftRow.style.height = 'auto';
427
+ rightRow.style.height = 'auto';
428
+ const height = Math.max(
429
+ leftRow.getBoundingClientRect().height,
430
+ rightRow.getBoundingClientRect().height
431
+ );
432
+ leftRow.style.height = `${height}px`;
433
+ rightRow.style.height = `${height}px`;
434
+ }
435
+ if (leftPane && rightPane) {
436
+ const scrollbarHeight = Math.max(0, rightPane.offsetHeight - rightPane.clientHeight);
437
+ leftPane.style.paddingBottom = `${scrollbarHeight}px`;
438
+ }
439
+ } finally {
440
+ running = false;
441
+ }
442
+ });
443
+ };
444
+
445
+ sync();
446
+ window.addEventListener('resize', sync);
447
+
448
+ let resizeObserver: ResizeObserver | null = null;
449
+ if (typeof ResizeObserver !== 'undefined' && rightPane) {
450
+ resizeObserver = new ResizeObserver(sync);
451
+ resizeObserver.observe(rightPane);
452
+ }
453
+
454
+ return () => {
455
+ running = false;
456
+ cancelAnimationFrame(rafId);
457
+ window.removeEventListener('resize', sync);
458
+ resizeObserver?.disconnect();
459
+ };
460
+ }, [
461
+ hasFrozenSplit,
462
+ loading,
463
+ renderRows.length,
464
+ allHeaders.length,
465
+ scrollHeaders.length,
466
+ useEqualWidthScroll,
467
+ visibleScrollColumns,
468
+ resolvedMinScrollColumnWidth,
469
+ resolvedMaxScrollColumns,
470
+ highlightRowClientId,
471
+ getHighlightRowId,
472
+ ]);
473
+
474
+ /* -------- Styles -------- */
475
+ const tableStyle: React.CSSProperties = {
476
+ width: '100%',
477
+ minWidth: '100%',
478
+ borderCollapse: 'collapse',
479
+ tableLayout: preventHorizontalScroll ? 'fixed' : 'auto',
480
+ };
481
+
482
+ const containerStyle: React.CSSProperties = {
483
+ width: '100%',
484
+ overflowX: preventHorizontalScroll ? 'hidden' : 'auto',
485
+ overflowY: 'auto',
486
+ border: `1px solid ${tokens.tableBorder}`,
487
+ borderRadius: tableBorderRadius,
488
+ };
489
+
490
+ const thStyle = (
491
+ align: string = 'center',
492
+ isLastColumn: boolean = false
493
+ ): React.CSSProperties => ({
494
+ padding: tokens.headerPadding,
495
+ fontSize: tokens.headerFontSize,
496
+ fontWeight: Number(tokens.headerFontWeight) || 600,
497
+ color: tokens.headerColor,
498
+ backgroundColor: tokens.headerBackground,
499
+ borderBottom: `1px solid ${tokens.headerBorderBottom}`,
500
+ borderRight: isLastColumn ? 'none' : `1px solid ${tokens.headerBorderRight}`,
501
+ textAlign: align as any,
502
+ whiteSpace: 'pre-line',
503
+ overflowWrap: 'break-word',
504
+ verticalAlign: 'middle',
505
+ cursor: 'default',
506
+ position: stickyHeader ? 'sticky' : 'relative',
507
+ top: stickyHeader ? 0 : undefined,
508
+ zIndex: stickyHeader ? 1 : undefined,
509
+ });
510
+
511
+ const tdStyle = (
512
+ isLastRow: boolean = false,
513
+ textAlign: 'left' | 'right' | 'center' = 'center',
514
+ isLastColumn: boolean = false
515
+ ): React.CSSProperties => ({
516
+ padding: tokens.cellPadding,
517
+ fontSize: tokens.cellFontSize,
518
+ color: tokens.cellColor,
519
+ borderBottom: isLastRow ? 'none' : `1px solid ${tokens.cellBorderBottom}`,
520
+ borderRight: isLastColumn ? 'none' : `${COLUMN_DIVIDER_WIDTH} solid ${bodyColumnDividerColor}`,
521
+ textAlign,
522
+ textTransform: 'none',
523
+ verticalAlign: 'middle',
524
+ whiteSpace: 'nowrap',
525
+ });
526
+
527
+ const stickyStyle = (
528
+ stickyPosition: 'left' | 'right' | undefined,
529
+ isHeader: boolean
530
+ ): React.CSSProperties => {
531
+ if (!stickyPosition || isMobileScreen) return {};
532
+ return {
533
+ position: 'sticky',
534
+ [stickyPosition]: 0,
535
+ backgroundColor: isHeader ? tokens.headerBackground : 'inherit',
536
+ zIndex: isHeader ? 3 : 2,
537
+ boxShadow:
538
+ stickyPosition === 'right' ?
539
+ '-1px 0 0 rgba(229, 231, 235, 0.9)'
540
+ : '1px 0 0 rgba(229, 231, 235, 0.9)',
541
+ };
542
+ };
543
+
544
+ const skeletonBarStyle: React.CSSProperties = {
545
+ height: tokens.skeletonHeight,
546
+ borderRadius: tokens.skeletonRadius,
547
+ background: `linear-gradient(90deg, ${tokens.skeletonBaseColor} 25%, transparent 50%, ${tokens.skeletonBaseColor} 75%)`,
548
+ backgroundSize: '400px 100%',
549
+ animation: 'dsSkeletonShimmer 1.5s ease-in-out infinite',
550
+ };
551
+
552
+ const splitContainerStyle: React.CSSProperties = {
553
+ width: '100%',
554
+ display: 'flex',
555
+ alignItems: 'stretch',
556
+ overflow: 'hidden',
557
+ border: `1px solid ${tokens.tableBorder}`,
558
+ borderRadius: tableBorderRadius,
559
+ };
560
+
561
+ const frozenPaneStyle: React.CSSProperties = {
562
+ // Hug content so the right pane starts flush against the last frozen column.
563
+ // Do not shrink or scroll horizontally — frozen columns stay fully visible.
564
+ flex: '0 0 auto',
565
+ width: 'max-content',
566
+ maxWidth: frozenPaneMaxWidth,
567
+ overflowX: 'hidden',
568
+ overflowY: 'hidden',
569
+ boxShadow: '8px 0 14px -8px rgba(15, 23, 42, 0.28)',
570
+ zIndex: 2,
571
+ backgroundColor: tokens.rowBackground,
572
+ };
573
+
574
+ const scrollPaneStyle: React.CSSProperties = {
575
+ flex: '1 1 0%',
576
+ minWidth: 0,
577
+ overflowX: 'auto',
578
+ overflowY: 'hidden',
579
+ };
580
+
581
+ const frozenTableStyle: React.CSSProperties = {
582
+ borderCollapse: 'collapse',
583
+ tableLayout: 'auto',
584
+ width: 'max-content',
585
+ };
586
+
587
+ const scrollTableStyle: React.CSSProperties = {
588
+ borderCollapse: 'collapse',
589
+ tableLayout: useEqualWidthScroll ? 'fixed' : 'auto',
590
+ width: useEqualWidthScroll ? undefined : 'max-content',
591
+ minWidth: useEqualWidthScroll ? undefined : '100%',
592
+ };
593
+
594
+ const renderHeaderRow = (
595
+ headers: any[],
596
+ {
597
+ headerLoading = false,
598
+ disableSticky = false,
599
+ keepLastDivider = false,
600
+ }: { headerLoading?: boolean; disableSticky?: boolean; keepLastDivider?: boolean } = {}
601
+ ) => (
602
+ <tr>
603
+ {headerLoading ?
604
+ headers.map((col: any, i: number) => (
605
+ <th
606
+ key={`skel-h-${col?.id ?? i}`}
607
+ style={{
608
+ ...thStyle(
609
+ headerAlignForColumn(col),
610
+ keepLastDivider ? false : i === headers.length - 1
611
+ ),
612
+ ...columnMinWidthStyle(col),
613
+ }}
614
+ >
615
+ <div style={skeletonBarStyle} />
616
+ </th>
617
+ ))
618
+ : headers.map((col: any, colIdx: number) => {
619
+ const isLast = keepLastDivider ? false : colIdx === headers.length - 1;
620
+ const headerAlign = headerAlignForColumn(col);
621
+ const stickyPosition = disableSticky ? undefined : stickyPositionForColumn(col);
622
+ const headerProps = col.getHeaderProps(
623
+ col.getSortByToggleProps ? col.getSortByToggleProps() : undefined
624
+ );
625
+ return (
626
+ <th
627
+ {...headerProps}
628
+ style={{
629
+ ...thStyle(headerAlign, isLast),
630
+ ...stickyStyle(stickyPosition, true),
631
+ ...columnMinWidthStyle(col),
632
+ ...columnHeaderExtraStyle(col),
633
+ ...(col.canSort ? { cursor: 'pointer', userSelect: 'none' } : {}),
634
+ }}
635
+ >
636
+ {col.render('Header')}
637
+ </th>
638
+ );
639
+ })
640
+ }
641
+ </tr>
642
+ );
643
+
644
+ const renderBodyCell = (
645
+ cell: any,
646
+ rowIdx: number,
647
+ cellIdx: number,
648
+ cellCount: number,
649
+ {
650
+ disableSticky = false,
651
+ totalRows = renderRows.length,
652
+ keepLastDivider = false,
653
+ }: { disableSticky?: boolean; totalRows?: number; keepLastDivider?: boolean } = {}
654
+ ) => {
655
+ const align = cellAlignForColumn(cell.column);
656
+ const stickyPosition = disableSticky ? undefined : stickyPositionForColumn(cell.column);
657
+ const isStatusBadge = isStatusBadgeColumn(cell.column);
658
+ const shouldWrap = shouldWrapTableCell(cell.column, cell.value, preventHorizontalScroll);
659
+ const isExpandableText = isExpandableTextValue(cell.value, cell.column);
660
+ const cellProps = cell.getCellProps();
661
+ const cellClassName = [cellProps.className, isStatusBadge ? 'ds-datatable-cell--status' : '']
662
+ .filter(Boolean)
663
+ .join(' ');
664
+ return (
665
+ <td
666
+ {...cellProps}
667
+ className={cellClassName || undefined}
668
+ style={{
669
+ ...tdStyle(
670
+ rowIdx === totalRows - 1,
671
+ align,
672
+ keepLastDivider ? false : cellIdx === cellCount - 1
673
+ ),
674
+ ...stickyStyle(stickyPosition, false),
675
+ ...columnMinWidthStyle(cell.column),
676
+ ...(shouldWrap && {
677
+ whiteSpace: 'normal',
678
+ wordBreak: 'break-word',
679
+ overflowWrap: 'anywhere',
680
+ }),
681
+ ...(isExpandableText && {
682
+ width: EXPANDABLE_CELL_MAX_WIDTH,
683
+ maxWidth: EXPANDABLE_CELL_MAX_WIDTH,
684
+ }),
685
+ ...(isStatusBadge &&
686
+ !shouldWrap && {
687
+ whiteSpace: 'nowrap',
688
+ wordBreak: 'normal',
689
+ overflowWrap: 'normal',
690
+ }),
691
+ ...extraCellStyleFromColumn(cell),
692
+ }}
693
+ >
694
+ <ExpandableCell
695
+ value={cell.value}
696
+ renderedCell={cell.render('Cell')}
697
+ column={cell.column}
698
+ />
699
+ </td>
700
+ );
701
+ };
702
+
703
+ const renderSkeletonRows = (headers: any[], { keepLastDivider = false } = {}) =>
704
+ Array.from({ length: skeletonRows }).map((_, rowIdx) => {
705
+ const stripeBg = rowIdx % 2 === 0 ? tokens.rowBackground : tokens.rowStripeBackground;
706
+ return (
707
+ <tr key={`skel-r-${rowIdx}`} style={{ backgroundColor: stripeBg }}>
708
+ {headers.map((headerCol: any, colIdx: number) => (
709
+ <td
710
+ key={`skel-c-${rowIdx}-${headerCol?.id ?? colIdx}`}
711
+ style={{
712
+ ...tdStyle(
713
+ rowIdx === skeletonRows - 1,
714
+ cellAlignForColumn(headerCol),
715
+ keepLastDivider ? false : colIdx === headers.length - 1
716
+ ),
717
+ ...columnMinWidthStyle(headerCol),
718
+ }}
719
+ >
720
+ <div
721
+ style={{
722
+ ...skeletonBarStyle,
723
+ width: '100%',
724
+ }}
725
+ />
726
+ </td>
727
+ ))}
728
+ </tr>
729
+ );
730
+ });
731
+
732
+ const renderDataRows = (
733
+ headers: any[],
734
+ {
735
+ disableSticky = false,
736
+ keyPrefix = 'row',
737
+ syncHover = false,
738
+ keepLastDivider = false,
739
+ }: {
740
+ disableSticky?: boolean;
741
+ keyPrefix?: string;
742
+ syncHover?: boolean;
743
+ keepLastDivider?: boolean;
744
+ } = {}
745
+ ) => {
746
+ const headerIds = new Set(headers.map((header: any) => header.id));
747
+ return renderRows.map((row: any, rowIdx: number) => {
748
+ if (prepareRow) prepareRow(row);
749
+ const stripeBg = rowIdx % 2 === 0 ? tokens.rowBackground : tokens.rowStripeBackground;
750
+ const isHighlightedRow =
751
+ highlightRowClientId != null &&
752
+ (getHighlightRowId ?
753
+ String(getHighlightRowId(row?.original) ?? '')
754
+ : getRowClientIdForHighlight(row?.original)) === String(highlightRowClientId);
755
+ const baseBg = isHighlightedRow ? HIGHLIGHT_ROW_BACKGROUND : stripeBg;
756
+ const hoverBg = isHighlightedRow ? HIGHLIGHT_ROW_BACKGROUND_HOVER : tokens.rowHoverBackground;
757
+ const isHovered = syncHover && hoveredRowIndex === rowIdx;
758
+ const rowProps = typeof row.getRowProps === 'function' ? row.getRowProps() : {};
759
+ const restRowProps = { ...(rowProps ?? {}) };
760
+ delete (restRowProps as { key?: unknown }).key;
761
+ const cells = row.cells.filter((cell: any) => headerIds.has(cell.column.id));
762
+ return (
763
+ <tr
764
+ {...restRowProps}
765
+ key={`${keyPrefix}-${row.id ?? rowIdx}`}
766
+ style={{
767
+ ...(restRowProps.style as React.CSSProperties | undefined),
768
+ backgroundColor: isHovered ? hoverBg : baseBg,
769
+ transition: HIGHLIGHT_ROW_TRANSITION,
770
+ }}
771
+ onMouseEnter={(e) => {
772
+ if (syncHover) {
773
+ setHoveredRowIndex(rowIdx);
774
+ return;
775
+ }
776
+ e.currentTarget.style.transition = ROW_HOVER_TRANSITION;
777
+ e.currentTarget.style.backgroundColor = hoverBg;
778
+ }}
779
+ onMouseLeave={(e) => {
780
+ if (syncHover) {
781
+ setHoveredRowIndex(null);
782
+ return;
783
+ }
784
+ e.currentTarget.style.transition = ROW_HOVER_TRANSITION;
785
+ e.currentTarget.style.backgroundColor = baseBg;
786
+ }}
787
+ >
788
+ {cells.map((cell: any, cellIdx: number) =>
789
+ renderBodyCell(cell, rowIdx, cellIdx, cells.length, { disableSticky, keepLastDivider })
790
+ )}
791
+ </tr>
792
+ );
793
+ });
794
+ };
795
+
796
+ const renderEmptyRow = (colSpan: number) => (
797
+ <tr>
798
+ <td
799
+ colSpan={colSpan || 1}
800
+ style={{
801
+ textAlign: 'center',
802
+ color: tokens.emptyStateColor,
803
+ fontSize: tokens.emptyStateFontSize,
804
+ }}
805
+ >
806
+ <div style={{ width: '100%', height: emptyStateHeight }}>
807
+ <img
808
+ src={NoDataIm}
809
+ alt="No data"
810
+ style={{
811
+ width: emptyStateHeight,
812
+ height: emptyStateHeight,
813
+ objectFit: 'cover',
814
+ placeSelf: 'center',
815
+ }}
816
+ />
817
+ </div>
818
+ </td>
819
+ </tr>
820
+ );
821
+
822
+ /* -------- Render -------- */
823
+ if (hasFrozenSplit) {
824
+ return (
825
+ <div className={className} style={splitContainerStyle}>
826
+ <div ref={frozenPaneRef} style={frozenPaneStyle}>
827
+ <table ref={frozenTableRef} style={frozenTableStyle}>
828
+ {headerGroups && headerGroups.length > 0 && (
829
+ <thead>
830
+ {renderHeaderRow(frozenHeaders, {
831
+ headerLoading: loading,
832
+ disableSticky: true,
833
+ keepLastDivider: true,
834
+ })}
835
+ </thead>
836
+ )}
837
+ <tbody>
838
+ {loading ?
839
+ renderSkeletonRows(frozenHeaders, { keepLastDivider: true })
840
+ : renderDataRows(frozenHeaders, {
841
+ disableSticky: true,
842
+ keyPrefix: 'frozen',
843
+ syncHover: true,
844
+ keepLastDivider: true,
845
+ })
846
+ }
847
+ </tbody>
848
+ </table>
849
+ </div>
850
+ <div ref={scrollPaneRef} style={scrollPaneStyle}>
851
+ <table ref={scrollTableRef} style={scrollTableStyle}>
852
+ {headerGroups && headerGroups.length > 0 && (
853
+ <thead>
854
+ {renderHeaderRow(scrollHeaders, {
855
+ headerLoading: loading,
856
+ disableSticky: false,
857
+ })}
858
+ </thead>
859
+ )}
860
+ <tbody>
861
+ {loading ?
862
+ renderSkeletonRows(scrollHeaders)
863
+ : renderDataRows(scrollHeaders, {
864
+ disableSticky: false,
865
+ keyPrefix: 'scroll',
866
+ syncHover: true,
867
+ })
868
+ }
869
+ </tbody>
870
+ </table>
871
+ </div>
872
+ </div>
873
+ );
874
+ }
875
+
876
+ return (
877
+ <div className={className} style={containerStyle}>
878
+ <table style={tableStyle} {...(getTableProps ? getTableProps() : {})}>
879
+ {headerGroups && headerGroups.length > 0 && (loading || hasRows) && (
880
+ <thead>
881
+ {headerGroups.map((headerGroup: any) => (
882
+ <tr {...headerGroup.getHeaderGroupProps()}>
883
+ {loading ?
884
+ Array.from({ length: skelCols }).map((_, i) => (
885
+ <th
886
+ key={`skel-h-${i}`}
887
+ style={{
888
+ ...thStyle(
889
+ headerAlignForColumn(
890
+ headerGroups?.[0]?.headers?.[i]?.column ??
891
+ headerGroups?.[0]?.headers?.[i]
892
+ ),
893
+ i === skelCols - 1
894
+ ),
895
+ ...columnMinWidthStyle(
896
+ headerGroups?.[0]?.headers?.[i]?.column ?? headerGroups?.[0]?.headers?.[i]
897
+ ),
898
+ }}
899
+ >
900
+ <div style={skeletonBarStyle} />
901
+ </th>
902
+ ))
903
+ : headerGroup.headers.map((col: any, colIdx: number) => {
904
+ const isLast = colIdx === headerGroup.headers.length - 1;
905
+ const headerAlign = headerAlignForColumn(col);
906
+ const stickyPosition = stickyPositionForColumn(col);
907
+ return (
908
+ <th
909
+ {...col.getHeaderProps(
910
+ col.getSortByToggleProps ? col.getSortByToggleProps() : undefined
911
+ )}
912
+ style={{
913
+ ...thStyle(headerAlign, isLast),
914
+ ...stickyStyle(stickyPosition, true),
915
+ ...columnMinWidthStyle(col),
916
+ ...columnHeaderExtraStyle(col),
917
+ ...(col.canSort ? { cursor: 'pointer', userSelect: 'none' } : {}),
918
+ }}
919
+ >
920
+ {col.render('Header')}
921
+ </th>
922
+ );
923
+ })
924
+ }
925
+ </tr>
926
+ ))}
927
+ </thead>
928
+ )}
929
+
930
+ <tbody {...(getTableBodyProps ? getTableBodyProps() : {})}>
931
+ {loading ?
932
+ Array.from({ length: skeletonRows }).map((_, rowIdx) => {
933
+ const stripeBg = rowIdx % 2 === 0 ? tokens.rowBackground : tokens.rowStripeBackground;
934
+ return (
935
+ <tr key={`skel-r-${rowIdx}`} style={{ backgroundColor: stripeBg }}>
936
+ {Array.from({ length: skelCols }).map((_, colIdx) => {
937
+ const headerCol =
938
+ headerGroups?.[0]?.headers?.[colIdx]?.column ??
939
+ headerGroups?.[0]?.headers?.[colIdx];
940
+ const align = cellAlignForColumn(headerCol);
941
+ return (
942
+ <td
943
+ key={`skel-c-${rowIdx}-${colIdx}`}
944
+ style={{
945
+ ...tdStyle(rowIdx === skeletonRows - 1, align, colIdx === skelCols - 1),
946
+ ...columnMinWidthStyle(headerCol),
947
+ }}
948
+ >
949
+ <div
950
+ style={{
951
+ ...skeletonBarStyle,
952
+ width: '100%',
953
+ }}
954
+ />
955
+ </td>
956
+ );
957
+ })}
958
+ </tr>
959
+ );
960
+ })
961
+ : renderRows.length > 0 ?
962
+ renderDataRows(allHeaders.length ? allHeaders : (headerGroups?.[0]?.headers ?? []))
963
+ : renderEmptyRow(skelCols || 1)}
964
+ </tbody>
965
+ </table>
966
+ </div>
967
+ );
968
+ }