@drivenets/design-system 0.16.0 → 0.18.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 (176) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/index.cjs +1893 -824
  3. package/dist/index.d.cts +363 -247
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +363 -247
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1896 -829
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +25 -25
  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-alert-banner/ds-alert-banner.tsx +7 -6
  14. package/src/components/ds-autocomplete/ds-autocomplete.figma.ts +2 -2
  15. package/src/components/ds-autocomplete/ds-autocomplete.module.scss +15 -4
  16. package/src/components/ds-autocomplete/ds-autocomplete.tsx +10 -3
  17. package/src/components/ds-autocomplete/ds-autocomplete.types.ts +8 -0
  18. package/src/components/ds-avatar/ds-avatar.figma.ts +2 -2
  19. package/src/components/ds-avatar-group/ds-avatar-group.figma.ts +2 -2
  20. package/src/components/ds-breadcrumb/ds-breadcrumb.figma.ts +2 -2
  21. package/src/components/ds-button-v3/ds-button-v3.figma.ts +7 -5
  22. package/src/components/ds-button-v3/ds-button-v3.module.scss +84 -17
  23. package/src/components/ds-button-v3/ds-button-v3.tsx +2 -0
  24. package/src/components/ds-button-v3/ds-button-v3.types.ts +11 -2
  25. package/src/components/ds-button-v3/ds-button-v4.figma.ts +121 -0
  26. package/src/components/ds-card/figma/ds-card-content.figma.ts +2 -2
  27. package/src/components/ds-card/figma/ds-card-footer.figma.ts +2 -2
  28. package/src/components/ds-card/figma/ds-card-header.figma.ts +2 -2
  29. package/src/components/ds-card/figma/ds-card.figma.ts +2 -2
  30. package/src/components/ds-catalog-layout/ds-catalog-layout.figma.ts +51 -0
  31. package/src/components/ds-catalog-layout/ds-catalog-layout.tsx +9 -8
  32. package/src/components/ds-checkbox/ds-checkbox-level-2.figma.ts +2 -2
  33. package/src/components/ds-checkbox/ds-checkbox.figma.ts +2 -2
  34. package/src/components/ds-checkbox-group/ds-checkbox-group.figma.ts +2 -2
  35. package/src/components/ds-comment-bubble/ds-comment-bubble.figma.ts +52 -0
  36. package/src/components/ds-comment-card/ds-comment-card.figma.ts +36 -0
  37. package/src/components/ds-comment-indicator/ds-comment-indicator.figma.ts +32 -0
  38. package/src/components/ds-comments-drawer/components/comments-filter-modal/index.ts +0 -1
  39. package/src/components/ds-comments-drawer/ds-comments-drawer.figma.ts +19 -0
  40. package/src/components/ds-confirmation/ds-confirmation.tsx +13 -11
  41. package/src/components/ds-date-picker/ds-date-picker.figma.ts +2 -2
  42. package/src/components/ds-divider/ds-divider.figma.ts +2 -2
  43. package/src/components/ds-drawer/ds-drawer.tsx +11 -10
  44. package/src/components/ds-drawer/figma/ds-drawer-content.figma.ts +2 -2
  45. package/src/components/ds-drawer/figma/ds-drawer-footer.figma.ts +2 -2
  46. package/src/components/ds-drawer/figma/ds-drawer-header.figma.ts +2 -2
  47. package/src/components/ds-drawer/figma/ds-drawer.figma.ts +2 -2
  48. package/src/components/ds-dropdown-menu/figma/ds-dropdown-menu-footer.figma.ts +2 -2
  49. package/src/components/ds-dropdown-menu/figma/ds-dropdown-menu-group.figma.ts +2 -2
  50. package/src/components/ds-dropdown-menu/figma/ds-dropdown-menu-item.figma.ts +2 -2
  51. package/src/components/ds-dropdown-menu/figma/ds-dropdown-menu-separator.figma.ts +2 -2
  52. package/src/components/ds-dropdown-menu/figma/ds-dropdown-menu.figma.ts +2 -2
  53. package/src/components/ds-expandable-text-input/ds-expandable-text-input.figma.ts +2 -2
  54. package/src/components/ds-file-upload/components/file-upload-item/index.ts +0 -1
  55. package/src/components/ds-file-upload/ds-file-upload.figma.ts +2 -2
  56. package/src/components/ds-filter-status-icon/ds-filter-status-icon.figma.ts +2 -2
  57. package/src/components/ds-form-control/ds-form-control.figma.ts +2 -2
  58. package/src/components/ds-form-control/ds-form-control.tsx +15 -13
  59. package/src/components/ds-icon/figma/ds-icon-material.figma.batch.ts +1 -1
  60. package/src/components/ds-icon/figma/ds-icon-special.figma.batch.ts +1 -1
  61. package/src/components/ds-key-value-pair/ds-key-value-pair.figma.ts +2 -2
  62. package/src/components/ds-loader/ds-loader.figma.ts +2 -2
  63. package/src/components/ds-modal/ds-modal.tsx +10 -9
  64. package/src/components/ds-modal/figma/ds-modal-content.figma.ts +2 -2
  65. package/src/components/ds-modal/figma/ds-modal-footer.figma.ts +2 -2
  66. package/src/components/ds-modal/figma/ds-modal-header.figma.ts +2 -2
  67. package/src/components/ds-modal/figma/ds-modal.figma.ts +2 -2
  68. package/src/components/ds-number-input/ds-number-input.tsx +1 -0
  69. package/src/components/ds-panel/ds-panel.figma.ts +2 -2
  70. package/src/components/ds-popover/figma/ds-popover-content-item.figma.ts +2 -2
  71. package/src/components/ds-popover/figma/ds-popover-content.figma.ts +2 -2
  72. package/src/components/ds-popover/figma/ds-popover-footer.figma.ts +2 -2
  73. package/src/components/ds-popover/figma/ds-popover-header.figma.ts +2 -2
  74. package/src/components/ds-popover/figma/ds-popover.figma.ts +2 -2
  75. package/src/components/ds-progress-arc/ds-progress-arc.figma.ts +2 -2
  76. package/src/components/ds-progress-donut/ds-progress-donut.figma.ts +2 -2
  77. package/src/components/ds-progress-linear/ds-progress-linear.figma.ts +2 -2
  78. package/src/components/ds-progress-task-bar/ds-progress-task-bar.figma.ts +2 -2
  79. package/src/components/ds-radio-group/figma/ds-radio-group-item.figma.ts +2 -2
  80. package/src/components/ds-radio-group/figma/ds-radio-group.figma.ts +2 -2
  81. package/src/components/ds-segment-group/figma/ds-segment-group-item.figma.ts +2 -2
  82. package/src/components/ds-segment-group/figma/ds-segment-group.figma.ts +2 -2
  83. package/src/components/ds-select/ds-select.figma.ts +4 -3
  84. package/src/components/ds-select/ds-select.tsx +6 -1
  85. package/src/components/ds-select/ds-select.types.ts +2 -1
  86. package/src/components/ds-site-node/ds-site-node.figma.ts +2 -2
  87. package/src/components/ds-slider/ds-slider.figma.ts +29 -0
  88. package/src/components/ds-slider/ds-slider.module.scss +104 -0
  89. package/src/components/ds-slider/ds-slider.tsx +118 -0
  90. package/src/components/ds-slider/ds-slider.types.ts +102 -0
  91. package/src/components/ds-slider/ds-slider.utils.ts +14 -0
  92. package/src/components/ds-slider/index.ts +11 -0
  93. package/src/components/ds-smart-tabs/ds-smart-tabs.tsx +5 -4
  94. package/src/components/ds-smart-tabs/figma/ds-smart-tabs-tab.figma.ts +83 -0
  95. package/src/components/ds-smart-tabs/figma/ds-smart-tabs.figma.ts +23 -0
  96. package/src/components/ds-spinner/ds-spinner.figma.ts +2 -2
  97. package/src/components/ds-split-button/ds-split-button.figma.ts +2 -2
  98. package/src/components/ds-status-badge-v2/ds-status-badge-v2.figma.ts +2 -2
  99. package/src/components/ds-stepper/ds-stepper.figma.ts +2 -2
  100. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +5 -1
  101. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +15 -3
  102. package/src/components/ds-table/components/ds-table-group-header-cell/index.ts +0 -1
  103. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +17 -3
  104. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +30 -9
  105. package/src/components/ds-table/components/ds-table-resize-handle/ds-table-resize-handle.module.scss +23 -0
  106. package/src/components/ds-table/components/ds-table-resize-handle/ds-table-resize-handle.tsx +218 -0
  107. package/src/components/ds-table/components/ds-table-resize-handle/ds-table-resize-handle.types.ts +14 -0
  108. package/src/components/ds-table/components/ds-table-resize-handle/index.ts +1 -0
  109. package/src/components/ds-table/components/ds-table-resize-overlay/ds-table-resize-overlay.module.scss +18 -0
  110. package/src/components/ds-table/components/ds-table-resize-overlay/ds-table-resize-overlay.tsx +16 -0
  111. package/src/components/ds-table/components/ds-table-resize-overlay/ds-table-resize-overlay.types.ts +13 -0
  112. package/src/components/ds-table/components/ds-table-resize-overlay/index.ts +1 -0
  113. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +1 -1
  114. package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +9 -2
  115. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +2 -1
  116. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +9 -2
  117. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-checkbox.tsx +2 -2
  118. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-date.tsx +2 -1
  119. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-number.tsx +2 -1
  120. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-select.tsx +2 -1
  121. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-text.tsx +2 -1
  122. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.tsx +12 -3
  123. package/src/components/ds-table/context/ds-table-context.tsx +208 -0
  124. package/src/components/ds-table/ds-table-defaults.ts +42 -0
  125. package/src/components/ds-table/ds-table.module.scss +19 -1
  126. package/src/components/ds-table/ds-table.tsx +163 -235
  127. package/src/components/ds-table/ds-table.types.ts +59 -47
  128. package/src/components/ds-table/figma/ds-table-expandable.figma.ts +2 -2
  129. package/src/components/ds-table/figma/ds-table.figma.ts +2 -2
  130. package/src/components/ds-table/grouping/utils/column-group.ts +7 -5
  131. package/src/components/ds-table/hooks/use-cell-editor.ts +5 -1
  132. package/src/components/ds-table/hooks/use-column-resize.ts +385 -0
  133. package/src/components/ds-table/hooks/use-editing-state.ts +190 -0
  134. package/src/components/ds-table/index.ts +1 -0
  135. package/src/components/ds-table/styles/_variables.scss +2 -1
  136. package/src/components/ds-table/utils/column-size-meta.augmentation.ts +16 -0
  137. package/src/components/ds-table/utils/column-size.ts +213 -10
  138. package/src/components/ds-table/utils/column-size.unit.test.ts +213 -0
  139. package/src/components/ds-table/utils/constants.ts +28 -0
  140. package/src/components/ds-table/utils/frozen-body.ts +33 -0
  141. package/src/components/ds-table/utils/frozen-body.unit.test.ts +44 -0
  142. package/src/components/ds-table/utils/table-api.ts +42 -0
  143. package/src/components/ds-table/utils/table-api.unit.test.ts +82 -0
  144. package/src/components/ds-table/utils/table-columns.tsx +119 -0
  145. package/src/components/ds-table/utils/table-columns.unit.test.ts +199 -0
  146. package/src/components/ds-tabs/figma/ds-tabs-tab-vertical.figma.ts +2 -2
  147. package/src/components/ds-tabs/figma/ds-tabs-tab.figma.ts +2 -2
  148. package/src/components/ds-tabs/figma/ds-tabs-vertical.figma.ts +2 -2
  149. package/src/components/ds-tabs/figma/ds-tabs.figma.ts +2 -2
  150. package/src/components/ds-tag/ds-tag.figma.ts +51 -0
  151. package/src/components/ds-tag-filter/ds-tag-filter.figma.ts +16 -0
  152. package/src/components/ds-text-input/ds-input-field-v02.figma.ts +31 -6
  153. package/src/components/ds-text-input/ds-text-input.figma.ts +5 -4
  154. package/src/components/ds-text-input/ds-text-input.tsx +1 -0
  155. package/src/components/ds-text-input/ds-text-input.types.ts +1 -1
  156. package/src/components/ds-time-picker/ds-time-picker.figma.ts +2 -2
  157. package/src/components/ds-toast/ds-toast.figma.ts +3 -3
  158. package/src/components/ds-toggle/ds-toggle.figma.ts +2 -2
  159. package/src/components/ds-tooltip/ds-tooltip.figma.ts +2 -2
  160. package/src/components/ds-tree/ds-tree.figma.ts +54 -0
  161. package/src/components/ds-tree/ds-tree.module.scss +2 -2
  162. package/src/components/ds-tree/ds-tree.tsx +6 -0
  163. package/src/components/ds-tree/ds-tree.types.ts +16 -0
  164. package/src/components/ds-typography/ds-typography.figma.ts +2 -2
  165. package/src/components/ds-user-card/ds-user-card.figma.ts +2 -2
  166. package/src/components/ds-vertical-tabs/ds-vertical-tabs.tsx +13 -6
  167. package/src/components/ds-workspace-layout/ds-workspace-layout.figma.ts +99 -0
  168. package/src/components/ds-workspace-layout/ds-workspace-layout.module.scss +1 -8
  169. package/src/components/ds-workspace-layout/ds-workspace-layout.tsx +11 -10
  170. package/src/index.ts +1 -0
  171. package/src/styles/_gradients.scss +19 -0
  172. package/src/styles/_root.scss +39 -15
  173. package/src/styles/_typography.scss +1 -0
  174. package/src/styles/shared/_input.scss +10 -0
  175. package/src/styles/styles.scss +1 -0
  176. package/src/components/ds-table/context/ds-table-context.ts +0 -112
@@ -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,
@@ -0,0 +1,385 @@
1
+ import {
2
+ useCallback,
3
+ useLayoutEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type CSSProperties,
8
+ type RefObject,
9
+ } from 'react';
10
+ import type {
11
+ ColumnSizingInfoState,
12
+ ColumnSizingState,
13
+ OnChangeFn,
14
+ Table,
15
+ VisibilityState,
16
+ } from '@tanstack/react-table';
17
+ import {
18
+ clampColumnSizing,
19
+ collectMissingLeafSizing,
20
+ getColumnSizeCssVars,
21
+ getResizeOriginSize,
22
+ measureLeafHeaderWidths,
23
+ omitBuiltinColumnSizing,
24
+ type ColumnSizeBoundsSource,
25
+ } from '../utils/column-size';
26
+ import { RESIZE_MIN_COLUMN_WIDTH } from '../utils/constants';
27
+ import type { DsTableResizePhase } from '../context/ds-table-context';
28
+
29
+ const INITIAL_COLUMN_SIZING_INFO: ColumnSizingInfoState = {
30
+ startOffset: null,
31
+ startSize: null,
32
+ deltaOffset: null,
33
+ deltaPercentage: null,
34
+ isResizingColumn: false,
35
+ columnSizingStart: [],
36
+ };
37
+
38
+ export interface UseColumnResizeOptions {
39
+ enabled: boolean;
40
+ /**
41
+ * Persisted widths to restore. Merged under live internal state so seed
42
+ * measurement fills leaves absent from the map and drags still take effect.
43
+ */
44
+ columnSizing?: Record<string, number>;
45
+ onColumnSizingChange?: (columnSizing: Record<string, number>) => void;
46
+ /**
47
+ * Invalidates the leaf-width seed measurement (same role as in the former
48
+ * `DsTable` layout effect). Read as a dependency only.
49
+ */
50
+ columns: unknown;
51
+ columnVisibility: VisibilityState;
52
+ }
53
+
54
+ export interface DsTableResizeContextSlice {
55
+ resizableColumns: boolean;
56
+ resizeSizingReady: boolean;
57
+ resizeContainerRef: RefObject<HTMLDivElement | null>;
58
+ onResizeHover: (columnId: string | null, offset: number | null) => void;
59
+ onResizeDragStart: (offset: number) => void;
60
+ onResizeReset: (columnIds: string[]) => void;
61
+ /**
62
+ * Keyboard resize for a focused handle. `columnId` is the boundary header's
63
+ * column (leaf or group); `delta` is the signed px change. Clamps and fires
64
+ * the persist callback, mirroring drag end.
65
+ */
66
+ onResizeKeyboardNudge: (columnId: string, delta: number) => void;
67
+ }
68
+
69
+ export interface UseColumnResizeActiveResize {
70
+ offset: number;
71
+ phase: DsTableResizePhase;
72
+ }
73
+
74
+ export interface UseColumnResizeBindResult {
75
+ columnSizeVars: CSSProperties | undefined;
76
+ activeResize: UseColumnResizeActiveResize | null;
77
+ context: DsTableResizeContextSlice;
78
+ }
79
+
80
+ export interface UseColumnResizeTableOptions {
81
+ enableColumnResizing: boolean;
82
+ columnResizeMode: 'onChange';
83
+ defaultColumn?: { minSize: number };
84
+ onColumnSizingChange: OnChangeFn<ColumnSizingState>;
85
+ onColumnSizingInfoChange: OnChangeFn<ColumnSizingInfoState>;
86
+ }
87
+
88
+ export interface UseColumnResizeResult {
89
+ tableOptions: UseColumnResizeTableOptions;
90
+ state: {
91
+ columnSizing: ColumnSizingState;
92
+ columnSizingInfo: ColumnSizingInfoState;
93
+ };
94
+ bind: <TData>(table: Table<TData>) => UseColumnResizeBindResult;
95
+ }
96
+
97
+ /**
98
+ * Owns column-resize state, TanStack sizing handlers, origin snapshots, seed
99
+ * measurement, hover/drag overlay model, and the context slice consumed by
100
+ * resize handles.
101
+ *
102
+ * Call before `useReactTable`, spread `tableOptions` / `state` into it, then
103
+ * `bind(table)` during render for CSS vars, overlay, and context.
104
+ */
105
+ export const useColumnResize = ({
106
+ enabled,
107
+ columnSizing: columnSizingProp,
108
+ onColumnSizingChange,
109
+ columns,
110
+ columnVisibility,
111
+ }: UseColumnResizeOptions): UseColumnResizeResult => {
112
+ const [columnResizeState, setColumnResizeState] = useState<{
113
+ columnSizing: ColumnSizingState;
114
+ columnSizingInfo: ColumnSizingInfoState;
115
+ }>({
116
+ columnSizing: {},
117
+ columnSizingInfo: INITIAL_COLUMN_SIZING_INFO,
118
+ });
119
+ // One useState so TanStack's mousemove updaters (info then sizing) run in
120
+ // call order against the same queue. Split hooks apply sizing first, while
121
+ // the next-width map is still empty.
122
+ const { columnSizing: internalColumnSizing, columnSizingInfo } = columnResizeState;
123
+
124
+ // Persisted widths seed the effective map, but internal state wins so a live
125
+ // drag (or seed measurement) of a persisted column still takes effect. Passed
126
+ // to TanStack and read for CSS vars / persist so restore, drag, and persist
127
+ // all see the same widths.
128
+ const columnSizing = useMemo<ColumnSizingState>(
129
+ () => (columnSizingProp ? { ...columnSizingProp, ...internalColumnSizing } : internalColumnSizing),
130
+ [columnSizingProp, internalColumnSizing],
131
+ );
132
+ const [resizeHover, setResizeHover] = useState<{
133
+ columnId: string;
134
+ offset: number;
135
+ } | null>(null);
136
+ const dragStartOffsetRef = useRef<number | null>(null);
137
+ const resizeOriginSizingRef = useRef<ColumnSizingState>({});
138
+ const containerRef = useRef<HTMLDivElement>(null);
139
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- bind is generic; the ref only calls sizing APIs
140
+ const tableRef = useRef<Table<any> | null>(null);
141
+
142
+ const columnSizingRef = useRef(columnSizing);
143
+ columnSizingRef.current = columnSizing;
144
+ const columnSizingInfoRef = useRef(columnSizingInfo);
145
+ columnSizingInfoRef.current = columnSizingInfo;
146
+ const onColumnSizingChangeRef = useRef(onColumnSizingChange);
147
+ onColumnSizingChangeRef.current = onColumnSizingChange;
148
+ const leafSizeBoundsRef = useRef<ColumnSizeBoundsSource[]>([]);
149
+
150
+ const handleColumnSizingChange: OnChangeFn<ColumnSizingState> = useCallback((updaterOrValue) => {
151
+ // Resolve against the ref and write it synchronously (same pattern as
152
+ // columnSizingInfoRef), before queuing the React state update. TanStack's
153
+ // mouseup applies the final sizing and then fires the persist callback in
154
+ // the same tick, so the ref must already hold the final widths — the state
155
+ // updater runs later (at render), which would leave persist reading a stale
156
+ // last-move value. Resolving against the effective map also keeps persisted
157
+ // (prop-provided) leaves in the reported map.
158
+ const next =
159
+ typeof updaterOrValue === 'function' ? updaterOrValue(columnSizingRef.current) : updaterOrValue;
160
+ const clamped = clampColumnSizing(next, leafSizeBoundsRef.current);
161
+ columnSizingRef.current = clamped;
162
+ setColumnResizeState((prev) => ({ ...prev, columnSizing: clamped }));
163
+ }, []);
164
+
165
+ const handleColumnSizingInfoChange: OnChangeFn<ColumnSizingInfoState> = useCallback((updaterOrValue) => {
166
+ const prevInfo = columnSizingInfoRef.current;
167
+ const nextInfo = typeof updaterOrValue === 'function' ? updaterOrValue(prevInfo) : updaterOrValue;
168
+ const ended = Boolean(prevInfo.isResizingColumn) && nextInfo.isResizingColumn === false;
169
+
170
+ columnSizingInfoRef.current = nextInfo;
171
+ setColumnResizeState((prev) => ({
172
+ ...prev,
173
+ columnSizingInfo: nextInfo,
174
+ }));
175
+
176
+ if (ended) {
177
+ onColumnSizingChangeRef.current?.(omitBuiltinColumnSizing(columnSizingRef.current));
178
+ }
179
+ }, []);
180
+
181
+ const handleResizeHover = useCallback((columnId: string | null, offset: number | null) => {
182
+ setResizeHover(columnId === null || offset === null ? null : { columnId, offset });
183
+ }, []);
184
+
185
+ const handleResizeDragStart = useCallback((offset: number) => {
186
+ dragStartOffsetRef.current = offset;
187
+ }, []);
188
+
189
+ const handleResizeReset = useCallback(
190
+ (columnIds: string[]) => {
191
+ const next = { ...columnSizingRef.current };
192
+
193
+ for (const id of columnIds) {
194
+ const origin = resizeOriginSizingRef.current[id];
195
+ if (origin !== undefined) {
196
+ next[id] = origin;
197
+ }
198
+ }
199
+
200
+ const clamped = clampColumnSizing(next, leafSizeBoundsRef.current);
201
+ handleColumnSizingChange(clamped);
202
+ onColumnSizingChangeRef.current?.(omitBuiltinColumnSizing(clamped));
203
+ },
204
+ [handleColumnSizingChange],
205
+ );
206
+
207
+ const handleResizeKeyboardNudge = useCallback(
208
+ (columnId: string, delta: number) => {
209
+ const table = tableRef.current;
210
+ const column = table?.getColumn(columnId);
211
+ if (!table || !column) {
212
+ return;
213
+ }
214
+
215
+ const leaves = column.getLeafColumns().filter((leaf) => leaf.getIsVisible() && leaf.getCanResize());
216
+ const totalSize = leaves.reduce((sum, leaf) => sum + leaf.getSize(), 0);
217
+ if (leaves.length === 0 || totalSize <= 0) {
218
+ return;
219
+ }
220
+
221
+ // Distribute the delta across the boundary's leaves by their current
222
+ // share. This mirrors TanStack's group-drag scaling and, for a single
223
+ // leaf, applies the whole delta to it.
224
+ const next = { ...columnSizingRef.current };
225
+ for (const leaf of leaves) {
226
+ const size = leaf.getSize();
227
+ next[leaf.id] = Math.round(size + delta * (size / totalSize));
228
+ }
229
+
230
+ const clamped = clampColumnSizing(next, leafSizeBoundsRef.current);
231
+ handleColumnSizingChange(clamped);
232
+ onColumnSizingChangeRef.current?.(omitBuiltinColumnSizing(clamped));
233
+ },
234
+ [handleColumnSizingChange],
235
+ );
236
+
237
+ useLayoutEffect(() => {
238
+ if (!enabled) {
239
+ return;
240
+ }
241
+
242
+ const container = containerRef.current;
243
+ const table = tableRef.current;
244
+ if (!container || !table) {
245
+ return;
246
+ }
247
+
248
+ const seedMissingLeaves = (): boolean => {
249
+ if (container.getBoundingClientRect().width === 0) {
250
+ return false;
251
+ }
252
+
253
+ const tableEl = container.querySelector('table');
254
+ if (!(tableEl instanceof HTMLElement)) {
255
+ return false;
256
+ }
257
+
258
+ const leaves = table.getVisibleLeafColumns();
259
+ const leafIds = leaves.map((column) => column.id);
260
+ const measured = measureLeafHeaderWidths(tableEl, leafIds);
261
+
262
+ for (const column of leaves) {
263
+ const measuredWidth = measured[column.id];
264
+ if (measuredWidth === undefined || resizeOriginSizingRef.current[column.id] !== undefined) {
265
+ continue;
266
+ }
267
+
268
+ resizeOriginSizingRef.current[column.id] = getResizeOriginSize(column, Math.round(measuredWidth));
269
+ }
270
+
271
+ const missing = collectMissingLeafSizing(measured, columnSizingRef.current);
272
+ if (Object.keys(missing).length > 0) {
273
+ handleColumnSizingChange((prev) => ({
274
+ ...clampColumnSizing(missing, leafSizeBoundsRef.current),
275
+ ...prev,
276
+ }));
277
+ }
278
+
279
+ return true;
280
+ };
281
+
282
+ if (seedMissingLeaves()) {
283
+ return;
284
+ }
285
+
286
+ const observer = new ResizeObserver(() => {
287
+ if (seedMissingLeaves()) {
288
+ observer.disconnect();
289
+ }
290
+ });
291
+ observer.observe(container);
292
+
293
+ return () => observer.disconnect();
294
+ }, [enabled, columns, columnVisibility, handleColumnSizingChange]);
295
+
296
+ const tableOptions = useMemo<UseColumnResizeTableOptions>(
297
+ () => ({
298
+ enableColumnResizing: enabled,
299
+ columnResizeMode: 'onChange',
300
+ // Only constrain the minimum width while resizing is on. TanStack merges
301
+ // `defaultColumn` onto every column, so spreading this when resizing is
302
+ // off would clamp unrelated columns (e.g. 36px utility columns) up to the
303
+ // resize minimum and break existing layouts.
304
+ ...(enabled ? { defaultColumn: { minSize: RESIZE_MIN_COLUMN_WIDTH } } : {}),
305
+ onColumnSizingChange: handleColumnSizingChange,
306
+ onColumnSizingInfoChange: handleColumnSizingInfoChange,
307
+ }),
308
+ [enabled, handleColumnSizingChange, handleColumnSizingInfoChange],
309
+ );
310
+
311
+ const bind = <TData>(table: Table<TData>): UseColumnResizeBindResult => {
312
+ tableRef.current = table;
313
+ leafSizeBoundsRef.current = table.getAllLeafColumns();
314
+
315
+ const visibleLeafIds = table.getVisibleLeafColumns().map((column) => column.id);
316
+ const resizeSizingReady =
317
+ enabled && visibleLeafIds.length > 0 && visibleLeafIds.every((id) => columnSizing[id] !== undefined);
318
+
319
+ const columnSizeVars: CSSProperties | undefined = resizeSizingReady
320
+ ? {
321
+ ...getColumnSizeCssVars(table.getFlatHeaders()),
322
+ width: table.getTotalSize(),
323
+ }
324
+ : undefined;
325
+
326
+ if (!enabled) {
327
+ return {
328
+ columnSizeVars: undefined,
329
+ activeResize: null,
330
+ context: {
331
+ resizableColumns: false,
332
+ resizeSizingReady: false,
333
+ resizeContainerRef: containerRef,
334
+ onResizeHover: handleResizeHover,
335
+ onResizeDragStart: handleResizeDragStart,
336
+ onResizeReset: handleResizeReset,
337
+ onResizeKeyboardNudge: handleResizeKeyboardNudge,
338
+ },
339
+ };
340
+ }
341
+
342
+ const isResizingColumn = columnSizingInfo.isResizingColumn;
343
+ const resizingHeader =
344
+ typeof isResizingColumn === 'string'
345
+ ? table.getFlatHeaders().find((header) => header.column.id === isResizingColumn)
346
+ : undefined;
347
+ const startSize = columnSizingInfo.startSize;
348
+ const clampedResizeDelta =
349
+ resizingHeader && startSize !== null
350
+ ? resizingHeader.getSize() - startSize
351
+ : (columnSizingInfo.deltaOffset ?? 0);
352
+ const activeResize: UseColumnResizeActiveResize | null =
353
+ isResizingColumn && dragStartOffsetRef.current !== null
354
+ ? {
355
+ offset: dragStartOffsetRef.current + clampedResizeDelta,
356
+ phase: 'dragging',
357
+ }
358
+ : resizeHover
359
+ ? { offset: resizeHover.offset, phase: 'hover' }
360
+ : null;
361
+
362
+ return {
363
+ columnSizeVars,
364
+ activeResize,
365
+ context: {
366
+ resizableColumns: enabled,
367
+ resizeSizingReady,
368
+ resizeContainerRef: containerRef,
369
+ onResizeHover: handleResizeHover,
370
+ onResizeDragStart: handleResizeDragStart,
371
+ onResizeReset: handleResizeReset,
372
+ onResizeKeyboardNudge: handleResizeKeyboardNudge,
373
+ },
374
+ };
375
+ };
376
+
377
+ return {
378
+ tableOptions,
379
+ state: {
380
+ columnSizing,
381
+ columnSizingInfo,
382
+ },
383
+ bind,
384
+ };
385
+ };
@@ -0,0 +1,190 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+ import type { Cell } from '@tanstack/react-table';
3
+ import type { DsDataTableProps } from '../ds-table.types';
4
+
5
+ export interface EditingState<TData, TValue> {
6
+ cell: Cell<TData, TValue>;
7
+ draftValue: TValue;
8
+ error: string | null;
9
+ /** True while an async `onCellEdit` commit Promise is pending for this cell. */
10
+ pending: boolean;
11
+ }
12
+
13
+ export interface UseEditingStateResult<TData, TValue> {
14
+ /** The cell currently in edit mode, if any. */
15
+ editing: EditingState<TData, TValue> | null;
16
+ beginEdit: (cell: Cell<TData, TValue>) => void;
17
+ setDraft: (value: TValue) => void;
18
+ commit: (overrideValue?: TValue) => void;
19
+ cancel: () => void;
20
+ }
21
+
22
+ /** Shown when an async `onCellEdit` commit rejects with a non-`Error` reason. */
23
+ const EDIT_FALLBACK_ERROR = 'Save failed';
24
+
25
+ const isThenable = (value: unknown): value is Promise<unknown> =>
26
+ typeof value === 'object' && value !== null && typeof (value as { then?: unknown }).then === 'function';
27
+
28
+ /** Prefer a thrown `Error`'s message; fall back to a generic string otherwise. */
29
+ const extractErrorMessage = (reason: unknown, fallback: string): string =>
30
+ reason instanceof Error && reason.message ? reason.message : fallback;
31
+
32
+ /**
33
+ * Hoisted editing state for single-cell-at-a-time inline editing.
34
+ * Surfaced on DsTableContext so cell renderers and editors share one provider.
35
+ */
36
+ export const useEditingState = <TData, TValue>(
37
+ onCellEdit: DsDataTableProps<TData, TValue>['onCellEdit'],
38
+ onCellValidate: DsDataTableProps<TData, TValue>['onCellValidate'],
39
+ ): UseEditingStateResult<TData, TValue> => {
40
+ const [editing, setEditing] = useState<EditingState<TData, TValue> | null>(null);
41
+
42
+ const onCellEditRef = useRef(onCellEdit);
43
+ onCellEditRef.current = onCellEdit;
44
+
45
+ const onCellValidateRef = useRef(onCellValidate);
46
+ onCellValidateRef.current = onCellValidate;
47
+
48
+ const editingRef = useRef(editing);
49
+ editingRef.current = editing;
50
+
51
+ // Bumped whenever the active editing session changes (begin/cancel/commit).
52
+ // A pending async commit captures the current generation token and is discarded
53
+ // on resolve if it no longer matches — guards against stale writes and repeat
54
+ // commits.
55
+ const generationRef = useRef(0);
56
+
57
+ // AbortController for the in-flight commit attempt. Aborted when the attempt is
58
+ // superseded (Cancel/Escape or switching cells) so consumers can cancel requests.
59
+ const abortRef = useRef<AbortController | null>(null);
60
+
61
+ const abortPending = useCallback(() => {
62
+ abortRef.current?.abort();
63
+ abortRef.current = null;
64
+ }, []);
65
+
66
+ const beginEdit = useCallback(
67
+ (cell: Cell<TData, TValue>) => {
68
+ const current = editingRef.current;
69
+ if (current) {
70
+ const isSameCell = current.cell.row.id === cell.row.id && current.cell.column.id === cell.column.id;
71
+ if (isSameCell) {
72
+ return;
73
+ }
74
+ }
75
+ abortPending();
76
+ generationRef.current += 1;
77
+ setEditing({
78
+ cell,
79
+ draftValue: cell.getValue(),
80
+ error: null,
81
+ pending: false,
82
+ });
83
+ },
84
+ [abortPending],
85
+ );
86
+
87
+ const setDraft = useCallback((value: TValue) => {
88
+ const current = editingRef.current;
89
+ if (!current) {
90
+ return;
91
+ }
92
+ // Live, per-keystroke validation: surface (or clear) the inline error as the
93
+ // user types so the Confirm button can gate on it.
94
+ const error =
95
+ onCellValidateRef.current?.(current.cell.row.original, current.cell.column.id, value) ?? null;
96
+ setEditing({ ...current, draftValue: value, error });
97
+ }, []);
98
+
99
+ const commit = useCallback(
100
+ (overrideValue?: TValue) => {
101
+ const current = editingRef.current;
102
+ if (!current || current.pending) {
103
+ return;
104
+ }
105
+
106
+ const valueToCommit = overrideValue !== undefined ? overrideValue : current.draftValue;
107
+
108
+ // Synchronous pre-commit gate. Also covers immediate-commit editors
109
+ // (checkbox/select) that pass an override value without going through
110
+ // setDraft, so the live error never had a chance to run.
111
+ const validationError =
112
+ onCellValidateRef.current?.(current.cell.row.original, current.cell.column.id, valueToCommit) ?? null;
113
+ if (validationError !== null) {
114
+ setEditing({ ...current, draftValue: valueToCommit, error: validationError, pending: false });
115
+ return;
116
+ }
117
+
118
+ generationRef.current += 1;
119
+ const token = generationRef.current;
120
+
121
+ abortPending();
122
+ const controller = new AbortController();
123
+ abortRef.current = controller;
124
+ const { signal } = controller;
125
+
126
+ const isStale = () => generationRef.current !== token;
127
+
128
+ // Release the controller once this attempt reaches a terminal state, unless a
129
+ // newer attempt has already replaced it.
130
+ const settle = () => {
131
+ if (abortRef.current === controller) {
132
+ abortRef.current = null;
133
+ }
134
+ };
135
+
136
+ const showError = (error: string) => {
137
+ settle();
138
+ if (isStale()) {
139
+ return;
140
+ }
141
+ const live = editingRef.current;
142
+ setEditing(live ? { ...live, draftValue: valueToCommit, error, pending: false } : null);
143
+ };
144
+
145
+ // Persist via the fallible/async commit handler.
146
+ const editResult = onCellEditRef.current?.(
147
+ current.cell.row.original,
148
+ current.cell.column.id,
149
+ valueToCommit,
150
+ signal,
151
+ );
152
+
153
+ const applyEditResult = (error: string | null) => {
154
+ if (error !== null) {
155
+ showError(error);
156
+ return;
157
+ }
158
+ settle();
159
+ if (isStale()) {
160
+ return;
161
+ }
162
+ setEditing(null);
163
+ };
164
+
165
+ if (!isThenable(editResult)) {
166
+ applyEditResult(editResult ?? null);
167
+ return;
168
+ }
169
+
170
+ setEditing({ ...current, draftValue: valueToCommit, error: null, pending: true });
171
+ editResult.then(
172
+ (value) => {
173
+ applyEditResult(value ?? null);
174
+ },
175
+ (reason: unknown) => {
176
+ applyEditResult(extractErrorMessage(reason, EDIT_FALLBACK_ERROR));
177
+ },
178
+ );
179
+ },
180
+ [abortPending],
181
+ );
182
+
183
+ const cancel = useCallback(() => {
184
+ abortPending();
185
+ generationRef.current += 1;
186
+ setEditing(null);
187
+ }, [abortPending]);
188
+
189
+ return { editing, beginEdit, setDraft, commit, cancel };
190
+ };
@@ -1,4 +1,5 @@
1
1
  import './grouping/types/column-group-meta.augmentation';
2
+ import './utils/column-size-meta.augmentation';
2
3
 
3
4
  export type { SecondaryRowAction, RowAction } from './components/ds-table-cell';
4
5
  export type { ScrollParams } from './components/ds-table-body-virtualized';
@@ -1,13 +1,14 @@
1
1
  $border-radius: 4px;
2
2
  $border-width: 1px;
3
3
  $group-border-width: 2px;
4
- $virtualized-height: 600px;
5
4
  $reorder-column-width: 60px;
6
5
  $container-margin-vertical: 1rem;
7
6
  $filter-input-gap: 0.5rem;
8
7
  $filter-input-max-width: 24rem;
9
8
  $header-sort-gap: 0.5rem;
10
9
  $empty-state-height: 6rem;
10
+ $resize-handle-hit-width: 12px;
11
+ $resize-divider-width: 2px;
11
12
 
12
13
  $hover-bg-color: var(--background-secondary);
13
14
  $hover-border-color: var(--border);
@@ -0,0 +1,16 @@
1
+ import '@tanstack/react-table';
2
+
3
+ declare module '@tanstack/react-table' {
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ interface ColumnMeta<TData, TValue> {
6
+ /**
7
+ * Internal: set by `getAugmentedColumns` on leaf defs whose `size` was
8
+ * authored on the raw `columns` prop, so sizing pins a fixed width instead
9
+ * of filling. Not part of the public API.
10
+ * @internal
11
+ */
12
+ hasExplicitSize?: boolean;
13
+ }
14
+ }
15
+
16
+ export {};