@drivenets/design-system 0.17.0 → 0.18.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 (98) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.cjs +1797 -805
  3. package/dist/index.d.cts +117 -53
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +117 -53
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1799 -809
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +3 -3
  11. package/src/components/ds-alert-banner/ds-alert-banner-global.figma.ts +49 -0
  12. package/src/components/ds-alert-banner/ds-alert-banner.figma.ts +51 -0
  13. package/src/components/ds-autocomplete/ds-autocomplete.module.scss +15 -4
  14. package/src/components/ds-autocomplete/ds-autocomplete.tsx +10 -3
  15. package/src/components/ds-autocomplete/ds-autocomplete.types.ts +8 -0
  16. package/src/components/ds-button-v3/ds-button-v3.figma.ts +5 -3
  17. package/src/components/ds-button-v3/ds-button-v3.module.scss +84 -17
  18. package/src/components/ds-button-v3/ds-button-v3.tsx +2 -0
  19. package/src/components/ds-button-v3/ds-button-v3.types.ts +11 -2
  20. package/src/components/ds-button-v3/ds-button-v4.figma.ts +121 -0
  21. package/src/components/ds-comment-bubble/ds-comment-bubble.figma.ts +52 -0
  22. package/src/components/ds-comment-card/ds-comment-card.figma.ts +36 -0
  23. package/src/components/ds-comment-indicator/ds-comment-indicator.figma.ts +32 -0
  24. package/src/components/ds-comments-drawer/ds-comments-drawer.figma.ts +19 -0
  25. package/src/components/ds-select/ds-select.figma.ts +2 -1
  26. package/src/components/ds-select/ds-select.tsx +6 -1
  27. package/src/components/ds-select/ds-select.types.ts +2 -1
  28. package/src/components/ds-slider/ds-slider.figma.ts +29 -0
  29. package/src/components/ds-slider/ds-slider.module.scss +104 -0
  30. package/src/components/ds-slider/ds-slider.tsx +118 -0
  31. package/src/components/ds-slider/ds-slider.types.ts +102 -0
  32. package/src/components/ds-slider/ds-slider.utils.ts +14 -0
  33. package/src/components/ds-slider/index.ts +11 -0
  34. package/src/components/ds-smart-tabs/figma/ds-smart-tabs-tab.figma.ts +83 -0
  35. package/src/components/ds-smart-tabs/figma/ds-smart-tabs.figma.ts +23 -0
  36. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +5 -1
  37. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +15 -3
  38. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +15 -1
  39. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +29 -6
  40. package/src/components/ds-table/components/ds-table-resize-handle/ds-table-resize-handle.module.scss +23 -0
  41. package/src/components/ds-table/components/ds-table-resize-handle/ds-table-resize-handle.tsx +218 -0
  42. package/src/components/ds-table/components/ds-table-resize-handle/ds-table-resize-handle.types.ts +14 -0
  43. package/src/components/ds-table/components/ds-table-resize-handle/index.ts +1 -0
  44. package/src/components/ds-table/components/ds-table-resize-overlay/ds-table-resize-overlay.module.scss +18 -0
  45. package/src/components/ds-table/components/ds-table-resize-overlay/ds-table-resize-overlay.tsx +16 -0
  46. package/src/components/ds-table/components/ds-table-resize-overlay/ds-table-resize-overlay.types.ts +13 -0
  47. package/src/components/ds-table/components/ds-table-resize-overlay/index.ts +1 -0
  48. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +1 -1
  49. package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +9 -2
  50. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +2 -1
  51. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +9 -2
  52. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-checkbox.tsx +2 -2
  53. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-date.tsx +2 -1
  54. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-number.tsx +2 -1
  55. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-select.tsx +2 -1
  56. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-text.tsx +2 -1
  57. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.tsx +12 -3
  58. package/src/components/ds-table/context/ds-table-context.tsx +208 -0
  59. package/src/components/ds-table/ds-table-defaults.ts +42 -0
  60. package/src/components/ds-table/ds-table.module.scss +4 -1
  61. package/src/components/ds-table/ds-table.tsx +139 -214
  62. package/src/components/ds-table/ds-table.types.ts +53 -47
  63. package/src/components/ds-table/grouping/utils/column-group.ts +7 -5
  64. package/src/components/ds-table/hooks/use-cell-editor.ts +5 -1
  65. package/src/components/ds-table/hooks/use-column-resize.ts +385 -0
  66. package/src/components/ds-table/hooks/use-editing-state.ts +190 -0
  67. package/src/components/ds-table/index.ts +1 -0
  68. package/src/components/ds-table/styles/_variables.scss +2 -1
  69. package/src/components/ds-table/utils/column-size-meta.augmentation.ts +16 -0
  70. package/src/components/ds-table/utils/column-size.ts +213 -10
  71. package/src/components/ds-table/utils/column-size.unit.test.ts +213 -0
  72. package/src/components/ds-table/utils/constants.ts +28 -0
  73. package/src/components/ds-table/utils/frozen-body.ts +33 -0
  74. package/src/components/ds-table/utils/frozen-body.unit.test.ts +44 -0
  75. package/src/components/ds-table/utils/table-api.ts +42 -0
  76. package/src/components/ds-table/utils/table-api.unit.test.ts +82 -0
  77. package/src/components/ds-table/utils/table-columns.tsx +119 -0
  78. package/src/components/ds-table/utils/table-columns.unit.test.ts +199 -0
  79. package/src/components/ds-tag/ds-tag.figma.ts +51 -0
  80. package/src/components/ds-tag-filter/ds-tag-filter.figma.ts +16 -0
  81. package/src/components/ds-tag-filter/ds-tag-filter.module.scss +7 -35
  82. package/src/components/ds-tag-filter/ds-tag-filter.tsx +34 -57
  83. package/src/components/ds-tag-filter/ds-tag-filter.types.ts +2 -1
  84. package/src/components/ds-tag-filter/hooks/use-tag-overflow-calculation.ts +13 -18
  85. package/src/components/ds-text-input/ds-input-field-v02.figma.ts +29 -4
  86. package/src/components/ds-text-input/ds-text-input.figma.ts +3 -2
  87. package/src/components/ds-text-input/ds-text-input.tsx +1 -0
  88. package/src/components/ds-text-input/ds-text-input.types.ts +1 -1
  89. package/src/components/ds-tree/ds-tree.figma.ts +54 -0
  90. package/src/components/ds-workspace-layout/ds-workspace-layout.figma.ts +99 -0
  91. package/src/components/ds-workspace-layout/ds-workspace-layout.module.scss +1 -8
  92. package/src/index.ts +1 -0
  93. package/src/styles/_gradients.scss +19 -0
  94. package/src/styles/_root.scss +39 -15
  95. package/src/styles/_typography.scss +1 -0
  96. package/src/styles/shared/_input.scss +10 -0
  97. package/src/styles/styles.scss +1 -0
  98. package/src/components/ds-table/context/ds-table-context.ts +0 -112
@@ -1,7 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { useImperativeHandle, useMemo } from 'react';
3
3
  import {
4
- type ColumnDef,
5
4
  type RowSelectionState,
6
5
  type ColumnFiltersState,
7
6
  getCoreRowModel,
@@ -9,6 +8,7 @@ import {
9
8
  getFilteredRowModel,
10
9
  getSortedRowModel,
11
10
  type SortingState,
11
+ type Table as TanstackTable,
12
12
  useReactTable,
13
13
  type VisibilityState,
14
14
  } from '@tanstack/react-table';
@@ -16,27 +16,20 @@ import classnames from 'classnames';
16
16
  import { Table, TableBody, TableCell, TableRow } from './components/core-table';
17
17
  import { DsTableBulkActions } from './components/ds-table-bulk-actions';
18
18
  import { DsTableHeader } from './components/ds-table-header';
19
+ import { DsTableResizeOverlay } from './components/ds-table-resize-overlay';
19
20
  import styles from './ds-table.module.scss';
20
21
  import type { DsDataTableProps, DsTableRowSize } from './ds-table.types';
22
+ import { applyDsTableDefaults } from './ds-table-defaults';
21
23
  import { DsTableRow } from './components/ds-table-row';
22
- import { DsTableRowExpandableCell } from './components/ds-table-row-expandable-cell';
23
- import { DsTableRowSelectableCell } from './components/ds-table-row-selectable-cell';
24
- import { DsTableHeaderSelectableCell } from './components/ds-table-header-selectable-cell';
25
24
  import { useDragAndDrop } from './hooks/use-drag-and-drop';
26
- import { type DsTableContextType, DsTableContext, useEditingState } from './context/ds-table-context';
25
+ import { useColumnResize } from './hooks/use-column-resize';
26
+ import { DsTableContextProvider } from './context/ds-table-context';
27
27
  import { DsTableBodyVirtualized } from './components/ds-table-body-virtualized';
28
28
  import { useColumnGroups } from './grouping';
29
- import { DsSkeletonText } from '../ds-skeleton';
30
- import {
31
- EMPTY_TABLE_STATE_TEXT,
32
- EXPANDER_COLUMN_ID,
33
- EXPANDER_COLUMN_WIDTH,
34
- REORDER_COLUMN_ID,
35
- REORDER_COLUMN_WIDTH,
36
- SELECT_COLUMN_ID,
37
- SELECT_COLUMN_WIDTH,
38
- SKELETON_ROW_COUNT,
39
- } from './utils/constants';
29
+ import { EMPTY_TABLE_STATE_TEXT } from './utils/constants';
30
+ import { createSkeletonRows, getAugmentedColumns, toSkeletonColumns } from './utils/table-columns';
31
+ import { createTableApi } from './utils/table-api';
32
+ import { areBodiesFrozen } from './utils/frozen-body';
40
33
 
41
34
  // Row size to pixel height mapping (matches CSS variables)
42
35
  const ROW_SIZE_HEIGHT_MAP: Record<DsTableRowSize, number> = {
@@ -48,46 +41,41 @@ const ROW_SIZE_HEIGHT_MAP: Record<DsTableRowSize, number> = {
48
41
  /**
49
42
  * Design system Table component
50
43
  */
51
- const DsTable = <TData extends { id: string }, TValue>({
52
- ref,
53
- columns: columnsProp,
54
- data: tableData,
55
- controls,
56
- virtualized = false,
57
- virtualizedOptions,
58
- className,
59
- onRowClick,
60
- emptyState,
61
- stickyHeader = true,
62
- bordered = true,
63
- fullWidth = true,
64
- rowSize = 'medium',
65
- loading = false,
66
- expandable = false,
67
- renderExpandedRow,
68
- selectable = false,
69
- showSelectAllCheckbox = true,
70
- onSelectionChange,
71
- onSortingChange,
72
- onScroll,
73
- actions = [],
74
- onRowDoubleClick,
75
- primaryRowActions = [],
76
- secondaryRowActions,
77
- reorderable = false,
78
- onOrderChange,
79
- columnFilters: externalColumnFilters,
80
- onColumnFiltersChange,
81
- columnVisibility: externalColumnVisibility,
82
- onColumnVisibilityChange,
83
- collapsedColumnGroups: externalCollapsedColumnGroups,
84
- onCollapsedColumnGroupsChange,
85
- locale,
86
- activeRowId,
87
- infiniteScroll,
88
- onCellEdit,
89
- onCellValidate,
90
- }: DsDataTableProps<TData, TValue>) => {
44
+ const DsTable = <TData extends { id: string }, TValue>(props: DsDataTableProps<TData, TValue>) => {
45
+ const tableProps = applyDsTableDefaults(props);
46
+ const {
47
+ ref,
48
+ columns: columnsProp,
49
+ data: tableData,
50
+ controls,
51
+ virtualized,
52
+ virtualizedOptions,
53
+ className,
54
+ emptyState,
55
+ bordered,
56
+ fullWidth,
57
+ rowSize,
58
+ loading,
59
+ expandable,
60
+ selectable,
61
+ showSelectAllCheckbox,
62
+ onSelectionChange,
63
+ onSortingChange,
64
+ onScroll,
65
+ actions,
66
+ reorderable,
67
+ onOrderChange,
68
+ columnFilters: externalColumnFilters,
69
+ onColumnFiltersChange,
70
+ columnVisibility: externalColumnVisibility,
71
+ onColumnVisibilityChange,
72
+ collapsedColumnGroups: externalCollapsedColumnGroups,
73
+ onCollapsedColumnGroupsChange,
74
+ infiniteScroll,
75
+ resizableColumns,
76
+ columnSizing,
77
+ onColumnSizingChange,
78
+ } = tableProps;
91
79
  const [data, setData] = React.useState(tableData);
92
80
  const [sorting, setSorting] = React.useState<SortingState>([]);
93
81
  const [internalColumnFilters, setInternalColumnFilters] = React.useState<ColumnFiltersState>([]);
@@ -103,8 +91,6 @@ const DsTable = <TData extends { id: string }, TValue>({
103
91
  items: data,
104
92
  });
105
93
 
106
- const tableContainerRef = React.useRef<HTMLDivElement>(null);
107
-
108
94
  const columnFilters = externalColumnFilters ?? internalColumnFilters;
109
95
  const handleColumnFiltersChange = (
110
96
  updaterOrValue: ColumnFiltersState | ((old: ColumnFiltersState) => ColumnFiltersState),
@@ -157,74 +143,33 @@ const DsTable = <TData extends { id: string }, TValue>({
157
143
  onSelectionChange?.(newRowSelection);
158
144
  };
159
145
 
160
- const hasExpanderColumn = !!expandable;
161
- const hasSelectColumn = !!selectable;
162
- const hasReorderColumn = reorderable && !virtualized;
163
-
164
- const columns = useMemo<ColumnDef<TData, TValue>[]>(() => {
165
- const augmentedColumns: ColumnDef<TData, TValue>[] = [...columnsProp];
166
-
167
- if (hasSelectColumn) {
168
- const selectColumn: ColumnDef<TData, TValue> = {
169
- id: SELECT_COLUMN_ID,
170
- size: SELECT_COLUMN_WIDTH,
171
- enableSorting: false,
172
- enableResizing: false,
173
- header: ({ table }) => (showSelectAllCheckbox ? <DsTableHeaderSelectableCell table={table} /> : null),
174
- cell: ({ row }) => <DsTableRowSelectableCell row={row} />,
175
- };
176
- augmentedColumns.unshift(selectColumn);
177
- }
178
-
179
- if (hasExpanderColumn) {
180
- const expanderColumn: ColumnDef<TData, TValue> = {
181
- id: EXPANDER_COLUMN_ID,
182
- size: EXPANDER_COLUMN_WIDTH,
183
- enableSorting: false,
184
- enableResizing: false,
185
- header: () => null,
186
- cell: ({ row }) => (row.getCanExpand() ? <DsTableRowExpandableCell row={row} /> : null),
187
- };
188
- augmentedColumns.unshift(expanderColumn);
189
- }
190
-
191
- if (hasReorderColumn) {
192
- // Cell is rendered inline by DsTableRow when it encounters REORDER_COLUMN_ID,
193
- // since the drag handle needs row-level useSortable state.
194
- const reorderColumn: ColumnDef<TData, TValue> = {
195
- id: REORDER_COLUMN_ID,
196
- size: REORDER_COLUMN_WIDTH,
197
- enableSorting: false,
198
- enableResizing: false,
199
- header: 'Order',
200
- cell: () => null,
201
- };
202
- augmentedColumns.unshift(reorderColumn);
203
- }
204
-
205
- return augmentedColumns;
206
- }, [columnsProp, hasExpanderColumn, hasReorderColumn, hasSelectColumn, showSelectAllCheckbox]);
207
-
208
- const skeletonColumns = useMemo<ColumnDef<TData, TValue>[]>(() => {
209
- const toSkeleton = (cols: ColumnDef<TData, TValue>[]): ColumnDef<TData, TValue>[] =>
210
- cols.map((column) =>
211
- 'columns' in column && column.columns
212
- ? { ...column, columns: toSkeleton(column.columns) }
213
- : { ...column, cell: column.loadingCell ?? (() => <DsSkeletonText width="60%" />) },
214
- );
215
-
216
- return toSkeleton(columns);
217
- }, [columns]);
218
-
219
- const skeletonData = useMemo<TData[]>(
146
+ // Boolean flags only — an inline `selectable` / `expandable` callback must
147
+ // not rebuild column defs (and remount cells) on every parent render.
148
+ const hasSelectColumn = Boolean(selectable);
149
+ const hasExpanderColumn = Boolean(expandable);
150
+ const columns = useMemo(
220
151
  () =>
221
- Array.from(
222
- { length: SKELETON_ROW_COUNT },
223
- (_, i) => ({ id: `ds-table-skeleton-${String(i)}` }) as TData,
224
- ),
225
- [],
152
+ getAugmentedColumns(columnsProp, {
153
+ selectable: hasSelectColumn,
154
+ expandable: hasExpanderColumn,
155
+ reorderable,
156
+ virtualized,
157
+ showSelectAllCheckbox,
158
+ }),
159
+ [columnsProp, hasSelectColumn, hasExpanderColumn, reorderable, virtualized, showSelectAllCheckbox],
226
160
  );
227
161
 
162
+ const skeletonColumns = useMemo(() => toSkeletonColumns(columns), [columns]);
163
+ const skeletonData = useMemo(() => createSkeletonRows<TData>(), []);
164
+
165
+ const resize = useColumnResize({
166
+ enabled: resizableColumns,
167
+ columnSizing,
168
+ onColumnSizingChange,
169
+ columns,
170
+ columnVisibility,
171
+ });
172
+
228
173
  const table = useReactTable({
229
174
  data: loading ? skeletonData : reorderable ? data : tableData,
230
175
  columns: loading ? skeletonColumns : columns,
@@ -238,68 +183,22 @@ const DsTable = <TData extends { id: string }, TValue>({
238
183
  getRowId: (row) => row.id,
239
184
  getExpandedRowModel: getExpandedRowModel(),
240
185
  getRowCanExpand: typeof expandable === 'function' ? (row) => expandable(row.original) : () => expandable,
186
+ ...resize.tableOptions,
241
187
  state: {
242
188
  sorting,
243
189
  columnFilters,
244
190
  columnVisibility,
245
191
  rowSelection,
192
+ ...resize.state,
246
193
  },
247
194
  enableRowSelection: typeof selectable === 'function' ? (row) => selectable(row.original) : selectable,
248
195
  });
249
196
 
250
- useImperativeHandle(
251
- ref,
252
- () => ({
253
- selectRow: (rowId: string) => {
254
- table.getRow(rowId).toggleSelected(true);
255
- },
256
- deselectRow: (rowId: string) => {
257
- table.getRow(rowId).toggleSelected(false);
258
- },
259
- selectAllRows: () => {
260
- table.toggleAllRowsSelected(true);
261
- },
262
- deselectAllRows: () => {
263
- table.toggleAllRowsSelected(false);
264
- },
265
- selectRows: (rowIds: string[]) => {
266
- const selection: Record<string, boolean> = {};
267
- rowIds.forEach((id) => (selection[id] = true));
268
- table.setRowSelection(selection);
269
- },
270
- getSelectedRows: () => {
271
- return table.getFilteredSelectedRowModel().rows.map((r) => r.original);
272
- },
273
- expandRow: (rowId: string) => {
274
- table.getRow(rowId).toggleExpanded(true);
275
- },
276
- collapseRow: (rowId: string) => {
277
- table.getRow(rowId).toggleExpanded(false);
278
- },
279
- expandAllRows: () => {
280
- table.toggleAllRowsExpanded(true);
281
- },
282
- collapseAllRows: () => {
283
- table.toggleAllRowsExpanded(false);
284
- },
285
- expandRows: (rowIds: string[]) => {
286
- const expansion: Record<string, boolean> = {};
287
- rowIds.forEach((id) => (expansion[id] = true));
288
- table.setExpanded(expansion);
289
- },
290
- }),
291
- [table],
292
- );
197
+ const { columnSizeVars, activeResize, context: resizeContext } = resize.bind(table);
293
198
 
294
- const { rows, rowsById } = table.getRowModel();
199
+ useImperativeHandle(ref, () => createTableApi(table), [table]);
295
200
 
296
- const renderEmptyState = () => (
297
- <TableRow>
298
- <TableCell colSpan={columns.length} className={styles.emptyState}>
299
- {emptyState || EMPTY_TABLE_STATE_TEXT}
300
- </TableCell>
301
- </TableRow>
302
- );
201
+ const { rowsById } = table.getRowModel();
303
202
 
304
203
  const selectedRows = Object.entries(rowSelection)
305
204
  .filter(([, selected]) => selected)
@@ -308,50 +207,36 @@ const DsTable = <TData extends { id: string }, TValue>({
308
207
 
309
208
  const isBulkActionsVisible = selectable && actions.length > 0 && selectedRows.length > 0;
310
209
 
311
- const editingState = useEditingState<TData, TValue>(onCellEdit, onCellValidate);
312
-
313
- const contextValue: DsTableContextType<TData, TValue> = {
314
- stickyHeader,
315
- bordered,
316
- fullWidth,
317
- rowSize,
318
- expandable,
319
- selectable,
320
- reorderable,
321
- showSelectAllCheckbox,
322
- loading,
323
- onRowClick: loading ? undefined : onRowClick,
324
- onRowDoubleClick: loading ? undefined : onRowDoubleClick,
325
- primaryRowActions,
326
- secondaryRowActions,
327
- renderExpandedRow,
328
- virtualized,
329
- activeRowId,
330
- collapsedColumnGroups,
331
- onToggleColumnGroup: toggleColumnGroup,
332
- locale,
333
- onCellEdit,
334
- ...editingState,
335
- };
336
-
337
210
  return (
338
- <DsTableContext.Provider value={contextValue}>
211
+ <DsTableContextProvider
212
+ tableProps={tableProps}
213
+ derived={{
214
+ grouping: {
215
+ collapsedColumnGroups,
216
+ onToggleColumnGroup: toggleColumnGroup,
217
+ },
218
+ resize: resizeContext,
219
+ }}
220
+ >
339
221
  <div
340
222
  className={classnames(styles.container, isBulkActionsVisible && styles.bulkActionsVisible, className)}
341
223
  >
342
224
  {controls && <div className={styles.controls}>{controls}</div>}
343
225
  <div
344
- ref={tableContainerRef}
226
+ ref={resizeContext.resizeContainerRef}
345
227
  className={classnames(
346
228
  !virtualized && styles.dataTableContainer,
347
229
  virtualized && styles.virtualizedContainer,
348
230
  )}
349
231
  >
350
232
  <DragWrapper>
351
- <Table className={classnames(fullWidth && styles.fullWidth, !bordered && styles.tableNoBorder)}>
233
+ <Table
234
+ className={classnames(fullWidth && styles.fullWidth, !bordered && styles.tableNoBorder)}
235
+ style={columnSizeVars}
236
+ >
352
237
  <DsTableHeader table={table} />
353
238
  {virtualized ? (
354
- <DsTableBodyVirtualized
239
+ <MemoizedDsTableBodyVirtualized
355
240
  table={table}
356
241
  emptyState={emptyState}
357
242
  estimateSize={virtualizedOptions?.estimateSize || ROW_SIZE_HEIGHT_MAP[rowSize]}
@@ -361,18 +246,18 @@ const DsTable = <TData extends { id: string }, TValue>({
361
246
  infiniteScroll={infiniteScroll}
362
247
  />
363
248
  ) : (
364
- <TableBody>
365
- <SortableWrapper>
366
- {rows.length
367
- ? rows.map((row) => (
368
- <DsTableRow key={row.id} row={row} isSelected={!!rowSelection[row.id]} />
369
- ))
370
- : renderEmptyState()}
371
- </SortableWrapper>
372
- </TableBody>
249
+ <MemoizedDsTableRowsBody
250
+ table={table}
251
+ rowSelection={rowSelection}
252
+ emptyState={emptyState}
253
+ SortableWrapper={SortableWrapper}
254
+ />
373
255
  )}
374
256
  </Table>
375
257
  </DragWrapper>
258
+ {activeResize ? (
259
+ <DsTableResizeOverlay offset={activeResize.offset} phase={activeResize.phase} />
260
+ ) : null}
376
261
  </div>
377
262
  {selectable && actions.length > 0 && (
378
263
  <DsTableBulkActions
@@ -385,10 +270,50 @@ const DsTable = <TData extends { id: string }, TValue>({
385
270
  />
386
271
  )}
387
272
  </div>
388
- </DsTableContext.Provider>
273
+ </DsTableContextProvider>
274
+ );
275
+ };
276
+
277
+ interface DsTableRowsBodyProps<TData extends { id: string }> {
278
+ table: TanstackTable<TData>;
279
+ rowSelection: RowSelectionState;
280
+ emptyState?: React.ReactNode;
281
+ SortableWrapper: React.ComponentType<React.PropsWithChildren>;
282
+ }
283
+
284
+ const DsTableRowsBody = <TData extends { id: string }>({
285
+ table,
286
+ rowSelection,
287
+ emptyState,
288
+ SortableWrapper,
289
+ }: DsTableRowsBodyProps<TData>) => {
290
+ const { rows } = table.getRowModel();
291
+ const columnCount = table.getVisibleLeafColumns().length;
292
+
293
+ return (
294
+ <TableBody>
295
+ <SortableWrapper>
296
+ {rows.length ? (
297
+ rows.map((row) => <DsTableRow key={row.id} row={row} isSelected={!!rowSelection[row.id]} />)
298
+ ) : (
299
+ <TableRow>
300
+ <TableCell colSpan={columnCount} className={styles.emptyState}>
301
+ {emptyState || EMPTY_TABLE_STATE_TEXT}
302
+ </TableCell>
303
+ </TableRow>
304
+ )}
305
+ </SortableWrapper>
306
+ </TableBody>
389
307
  );
390
308
  };
391
309
 
310
+ const MemoizedDsTableRowsBody = React.memo(DsTableRowsBody, areBodiesFrozen) as typeof DsTableRowsBody;
311
+
312
+ const MemoizedDsTableBodyVirtualized = React.memo(
313
+ DsTableBodyVirtualized,
314
+ areBodiesFrozen,
315
+ ) as typeof DsTableBodyVirtualized;
316
+
392
317
  DsTable.displayName = 'DsTable';
393
318
 
394
319
  export default DsTable;
@@ -31,6 +31,11 @@ export interface DsTableLocale {
31
31
  * Accessible label for the toggle that expands a column group.
32
32
  */
33
33
  expandColumnGroup: string;
34
+
35
+ /**
36
+ * Accessible label for the column resize handle.
37
+ */
38
+ resizeColumn: string;
34
39
  }
35
40
 
36
41
  /**
@@ -39,6 +44,7 @@ export interface DsTableLocale {
39
44
  export const defaultDsTableLocale: DsTableLocale = Object.freeze({
40
45
  collapseColumnGroup: 'Collapse column group',
41
46
  expandColumnGroup: 'Expand column group',
47
+ resizeColumn: 'Resize column',
42
48
  });
43
49
 
44
50
  /**
@@ -320,6 +326,31 @@ export interface DsDataTableProps<TData, TValue> {
320
326
  */
321
327
  rowSize?: DsTableRowSize;
322
328
 
329
+ /**
330
+ * Enables resizing columns by dragging the right edge of a header cell.
331
+ * Double-click a handle to restore the column's original width. Opt out per
332
+ * column with `columnDef.enableResizing: false`. Leaf columns may set
333
+ * `minSize` and `maxSize`. Those bounds on a column group are ignored.
334
+ * @default false
335
+ */
336
+ resizableColumns?: boolean;
337
+
338
+ /**
339
+ * Persisted column widths as a column id → width (px) map, e.g. a map saved
340
+ * from {@link onColumnSizingChange}. Pass it (typically on the next mount) to
341
+ * restore widths; columns absent from the map are measured and sized
342
+ * automatically. This seeds widths — live resizing still updates them
343
+ * internally — so it is not a fully controlled value.
344
+ */
345
+ columnSizing?: Record<string, number>;
346
+
347
+ /**
348
+ * Called with the next column id → width (px) map when a resize finishes
349
+ * (drag end, double-click restore, or keyboard nudge). Use this to persist
350
+ * widths across sessions.
351
+ */
352
+ onColumnSizingChange?: (columnSizing: Record<string, number>) => void;
353
+
323
354
  /**
324
355
  * When `true`, renders placeholder skeleton rows instead of the data. Column
325
356
  * headers stay intact and row interactions are disabled while loading.
@@ -511,53 +542,28 @@ export interface DsDataTableProps<TData, TValue> {
511
542
  locale?: Partial<DsTableLocale>;
512
543
 
513
544
  /**
514
- * Callback fired when an editable cell commits a new value via one of the
515
- * `DsTableEditCell*` wrappers or the `useCellEditor` hook. The parent is
516
- * expected to update the row data in response.
517
- *
518
- * Commits occur on confirm (check button or Enter). Opening another cell
519
- * discards the previous draft without calling this callback.
520
- *
521
- * @param row - The row data of the cell that was edited
522
- * @param columnId - The id of the column whose cell was edited
523
- * @param value - The new committed value
524
- *
525
- * @example
526
- * ```tsx
527
- * const [people, setPeople] = useState(initialPeople);
528
- *
529
- * <DsTable
530
- * data={people}
531
- * columns={columns}
532
- * onCellEdit={(row, columnId, value) => {
533
- * setPeople((rows) =>
534
- * rows.map((r) => (r.id === row.id ? { ...r, [columnId]: value } : r)),
535
- * );
536
- * }}
537
- * />
538
- * ```
539
- */
540
- onCellEdit?: (row: TData, columnId: string, value: TValue) => void;
541
-
542
- /**
543
- * Validates a cell value before it is committed. Return `null` to allow the
544
- * commit (which fires `onCellEdit`), or an error message string to reject it
545
- * and keep the cell in edit mode. Synchronous only.
546
- *
547
- *
548
- * @param row - The row data of the cell being edited
549
- * @param columnId - The id of the column whose cell is being edited
550
- * @param value - The candidate value to validate
551
- *
552
- * @example
553
- * ```tsx
554
- * <DsTable
555
- * data={people}
556
- * columns={columns}
557
- * onCellValidate={(row, columnId, value) => validateField(columnId)(value, row)}
558
- * onCellEdit={...}
559
- * />
560
- * ```
545
+ * Commits an edited cell value. Runs when the user confirms (check button or
546
+ * Enter) after `onCellValidate` passes. This is the authoritative commit: use it
547
+ * for server-side validation and persistence. Return `void`/`null` to accept
548
+ * (closes the editor) or an error `string` to reject (keeps the cell open with
549
+ * the message); may be async. While a returned Promise is pending the editor is
550
+ * locked. `signal` is aborted when the edit is cancelled (Cancel/Escape) or
551
+ * superseded by opening another cell, so in-flight requests can be cancelled.
552
+ */
553
+ onCellEdit?: (
554
+ row: TData,
555
+ columnId: string,
556
+ value: TValue,
557
+ signal: AbortSignal,
558
+ ) => string | null | undefined | Promise<string | null | undefined>;
559
+
560
+ /**
561
+ * Synchronous, per-keystroke validation for the active editor. Runs live on
562
+ * every draft change and once more as a pre-commit gate before `onCellEdit`.
563
+ * Return `null` to allow or an error `string` to reject. A live error is shown
564
+ * inline and disables the Confirm button; `onCellEdit` never runs while the
565
+ * value is invalid. Keep it pure and cheap — for async or server-side checks,
566
+ * use `onCellEdit` instead.
561
567
  */
562
568
  onCellValidate?: (row: TData, columnId: string, value: TValue) => string | null;
563
569
  }
@@ -2,27 +2,29 @@ import type { CSSProperties } from 'react';
2
2
  import {
3
3
  type Column,
4
4
  type ColumnDef,
5
+ type ColumnSizingState,
5
6
  type VisibilityState,
6
- defaultColumnSizing,
7
7
  } from '@tanstack/react-table';
8
+ import { isExplicitColumnWidth } from '../../utils/column-size';
8
9
 
9
10
  /**
10
11
  * Builds the flex style for a group header cell so it lines up with the combined
11
12
  * width of its visible leaf columns.
12
13
  *
13
- * Fixed-width leaves contribute to `flexBasis`; default-sized leaves each add one
14
- * unit of `flexGrow`, mirroring how {@link getColumnSizeStyle} sizes leaf cells so
15
- * both header rows distribute free space identically.
14
+ * Fixed-width leaves contribute to `flexBasis`; fill leaves each add one unit of
15
+ * `flexGrow`, mirroring how {@link getColumnSizeStyle} sizes leaf cells so both
16
+ * header rows distribute free space identically.
16
17
  */
17
18
  export const getGroupColumnSizeStyle = <TData, TValue>(
18
19
  leafColumns: Column<TData, TValue>[],
20
+ columnSizing: ColumnSizingState,
19
21
  ): CSSProperties => {
20
22
  let fixedWidth = 0;
21
23
  let flexCount = 0;
22
24
 
23
25
  for (const leaf of leafColumns) {
24
26
  const size = leaf.getSize();
25
- if (size !== defaultColumnSizing.size) {
27
+ if (isExplicitColumnWidth(leaf, columnSizing)) {
26
28
  fixedWidth += size;
27
29
  } else {
28
30
  flexCount += 1;
@@ -19,8 +19,10 @@ export interface UseCellEditorContainerProps {
19
19
  export interface UseCellEditorResult<TValue> {
20
20
  /** The in-progress draft value for the active editor. */
21
21
  value: TValue;
22
- /** Validation error from the most recent commit attempt, if any. */
22
+ /** Current validation error — from live `onCellValidate` or the most recent commit attempt, if any. */
23
23
  error: string | null;
24
+ /** True while an async `onCellEdit` commit Promise is pending for this cell. */
25
+ isPending: boolean;
24
26
  /** Update the in-progress draft value. */
25
27
  setValue: (value: TValue) => void;
26
28
  /** Validate and commit the draft. */
@@ -48,6 +50,7 @@ export const useCellEditor = <TData extends RowData, TValue>({
48
50
  ctx.editing !== null && ctx.editing.cell.row.id === row.id && ctx.editing.cell.column.id === column.id;
49
51
  const value = isActiveCell && ctx.editing ? ctx.editing.draftValue : getValue();
50
52
  const error = isActiveCell && ctx.editing ? ctx.editing.error : null;
53
+ const isPending = isActiveCell && ctx.editing ? ctx.editing.pending : false;
51
54
 
52
55
  const setValue = (next: TValue) => {
53
56
  ctx.setDraft(next);
@@ -68,6 +71,7 @@ export const useCellEditor = <TData extends RowData, TValue>({
68
71
  return {
69
72
  value,
70
73
  error,
74
+ isPending,
71
75
  setValue,
72
76
  commit,
73
77
  cancel,