@drivenets/design-system 0.13.0 → 0.15.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 (84) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/dist/index.cjs +1155 -381
  4. package/dist/index.d.cts +371 -21
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +371 -21
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1151 -381
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.min.css +1 -1
  11. package/package.json +16 -13
  12. package/src/components/ds-button-v3/ds-button-v3.figma.ts +99 -0
  13. package/src/components/ds-button-v3/ds-button-v3.module.scss +22 -22
  14. package/src/components/ds-card/figma/ds-card-content.figma.ts +15 -0
  15. package/src/components/ds-card/figma/ds-card-footer.figma.ts +15 -0
  16. package/src/components/ds-card/figma/ds-card-header.figma.ts +15 -0
  17. package/src/components/ds-card/figma/ds-card.figma.ts +72 -0
  18. package/src/components/ds-date-picker/ds-date-picker.figma.ts +19 -0
  19. package/src/components/ds-drawer/ds-drawer.module.scss +14 -0
  20. package/src/components/ds-drawer/ds-drawer.tsx +20 -1
  21. package/src/components/ds-drawer/ds-drawer.types.ts +7 -0
  22. package/src/components/ds-file-upload/hooks/use-file-upload.ts +7 -0
  23. package/src/components/ds-form-control/ds-form-control.figma.ts +76 -0
  24. package/src/components/ds-icon/figma/ds-icon-material.figma.batch.ts +10 -0
  25. package/src/components/ds-icon/figma/ds-icon-special.figma.batch.ts +18 -0
  26. package/src/components/ds-icon/figma/ds-icon.figma.batch.json +2873 -0
  27. package/src/components/ds-modal/figma/ds-modal-content.figma.ts +18 -0
  28. package/src/components/ds-modal/figma/ds-modal-footer.figma.ts +19 -0
  29. package/src/components/ds-modal/figma/ds-modal-header.figma.ts +21 -0
  30. package/src/components/ds-modal/figma/ds-modal.figma.ts +36 -0
  31. package/src/components/ds-popover/ds-popover.module.scss +58 -16
  32. package/src/components/ds-popover/ds-popover.tsx +148 -24
  33. package/src/components/ds-popover/ds-popover.types.ts +95 -12
  34. package/src/components/ds-popover/index.ts +1 -1
  35. package/src/components/ds-progress-task-bar/ds-progress-task-bar.figma.ts +29 -0
  36. package/src/components/ds-progress-task-bar/ds-progress-task-bar.module.scss +39 -0
  37. package/src/components/ds-progress-task-bar/ds-progress-task-bar.tsx +122 -0
  38. package/src/components/ds-progress-task-bar/ds-progress-task-bar.types.ts +65 -0
  39. package/src/components/ds-progress-task-bar/index.ts +2 -0
  40. package/src/components/ds-progress-task-bar/utils.ts +51 -0
  41. package/src/components/ds-select/ds-select.figma.ts +53 -0
  42. package/src/components/ds-split-button/ds-split-button.figma.ts +95 -0
  43. package/src/components/ds-status-badge-v2/ds-status-badge-v2.figma.ts +41 -0
  44. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +8 -3
  45. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +31 -0
  46. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +48 -0
  47. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.types.ts +12 -0
  48. package/src/components/ds-table/components/ds-table-group-header-cell/index.ts +2 -0
  49. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +4 -0
  50. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +55 -39
  51. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +5 -1
  52. package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +4 -0
  53. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +5 -1
  54. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +4 -0
  55. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.module.scss +14 -2
  56. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.tsx +19 -0
  57. package/src/components/ds-table/context/ds-table-context.ts +4 -0
  58. package/src/components/ds-table/ds-table-column.types.ts +24 -0
  59. package/src/components/ds-table/ds-table.tsx +48 -6
  60. package/src/components/ds-table/ds-table.types.ts +62 -0
  61. package/src/components/ds-table/figma/ds-table-expandable.figma.ts +17 -0
  62. package/src/components/ds-table/figma/ds-table.figma.ts +16 -0
  63. package/src/components/ds-table/grouping/hooks/use-column-groups.ts +73 -0
  64. package/src/components/ds-table/grouping/index.ts +5 -0
  65. package/src/components/ds-table/grouping/types/column-group-meta.augmentation.ts +26 -0
  66. package/src/components/ds-table/grouping/types/column-group.types.ts +35 -0
  67. package/src/components/ds-table/grouping/utils/column-group.ts +119 -0
  68. package/src/components/ds-table/index.ts +4 -0
  69. package/src/components/ds-table/styles/_variables.scss +1 -0
  70. package/src/components/ds-table/utils/constants.ts +5 -0
  71. package/src/components/ds-tabs/figma/ds-tabs-tab-vertical.figma.ts +78 -0
  72. package/src/components/ds-tabs/figma/ds-tabs-tab.figma.ts +89 -0
  73. package/src/components/ds-tabs/figma/ds-tabs-vertical.figma.ts +31 -0
  74. package/src/components/ds-tabs/figma/ds-tabs.figma.ts +29 -0
  75. package/src/components/ds-tag/ds-tag.module.scss +22 -1
  76. package/src/components/ds-tag/ds-tag.tsx +16 -3
  77. package/src/components/ds-tag/ds-tag.types.ts +22 -8
  78. package/src/components/ds-tag/ds-tag.unit.test.ts +32 -0
  79. package/src/components/ds-tag-filter/ds-tag-filter.tsx +14 -22
  80. package/src/components/ds-text-input/ds-text-input.figma.ts +60 -0
  81. package/src/components/ds-time-picker/ds-time-picker.figma.ts +18 -0
  82. package/src/index.ts +1 -0
  83. package/src/styles/_root_new.scss +2 -0
  84. package/src/styles/_scrollbars.scss +27 -50
@@ -2,6 +2,7 @@ import classnames from 'classnames';
2
2
  import styles from './ds-table-row-virtualized.module.scss';
3
3
  import { useDsTableContext } from '../../context/ds-table-context';
4
4
  import { getColumnSizeStyle } from '../../utils/column-size';
5
+ import { isFirstLeafColumnOfGroup } from '../../grouping';
5
6
  import { TableCell, TableRow } from '../core-table';
6
7
  import type { DsTableRowVirtualizedProps } from './ds-table-row-virtualized.types';
7
8
  import { DsTableCell } from '../ds-table-cell';
@@ -25,6 +26,7 @@ export const DsTableRowVirtualized = <TData,>({
25
26
  renderExpandedRow,
26
27
  onRowClick,
27
28
  onRowDoubleClick,
29
+ loading,
28
30
  } = useDsTableContext<TData, unknown>();
29
31
  const isExpanded = row.getIsExpanded();
30
32
  const isActive = activeRowId === row.id;
@@ -57,6 +59,7 @@ export const DsTableRowVirtualized = <TData,>({
57
59
  onRowClick && styles.clickableRow,
58
60
  !bordered && styles.rowNoBorder,
59
61
  isExpanded && styles.expanded,
62
+ loading && styles.loadingRow,
60
63
  )}
61
64
  onClick={isExpandedRowContent ? undefined : () => onRowClick?.(row.original)}
62
65
  onDoubleClick={isExpandedRowContent ? undefined : () => onRowDoubleClick?.(row.original)}
@@ -77,6 +80,7 @@ export const DsTableRowVirtualized = <TData,>({
77
80
  styles.cell,
78
81
  cell.column.id === EXPANDER_COLUMN_ID && styles.expandableCell,
79
82
  cell.column.id === SELECT_COLUMN_ID && styles.selectableCell,
83
+ isFirstLeafColumnOfGroup(cell.column) && styles.groupStart,
80
84
  typeof cell.column.columnDef.editCell === 'function' && styles.editableCell,
81
85
  )}
82
86
  >
@@ -11,18 +11,30 @@
11
11
  outline: none;
12
12
  }
13
13
 
14
- .editButton {
14
+ .editButton,
15
+ .lockIcon {
15
16
  flex-shrink: 0;
16
17
  opacity: 0;
17
18
  pointer-events: none;
18
19
  }
19
20
 
21
+ .lockIcon {
22
+ display: inline-flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ }
26
+
20
27
  &:hover,
21
28
  &:focus-within,
22
29
  &[data-active] {
23
- .editButton {
30
+ .editButton,
31
+ .lockIcon {
24
32
  opacity: 1;
25
33
  pointer-events: auto;
26
34
  }
27
35
  }
36
+
37
+ &[data-locked] {
38
+ cursor: default;
39
+ }
28
40
  }
@@ -1,5 +1,7 @@
1
1
  import type { MouseEvent } from 'react';
2
2
  import { DsButtonV3 } from '../../../../ds-button-v3';
3
+ import { DsIcon } from '../../../../ds-icon';
4
+ import { DsTooltip } from '../../../../ds-tooltip';
3
5
  import styles from './ds-table-editable-cell.module.scss';
4
6
  import type { DsTableEditableCellProps } from './ds-table-editable-cell.types';
5
7
  import { useDsTableContext } from '../../../context/ds-table-context';
@@ -16,6 +18,23 @@ export const DsTableEditableCell = <TData, TValue>({
16
18
  beginEdit(cell);
17
19
  };
18
20
 
21
+ const editDisabled = cell.column.columnDef.editDisabled?.(cell.getContext());
22
+ const locked = editDisabled === true || typeof editDisabled === 'object';
23
+ const reason = typeof editDisabled === 'object' ? editDisabled.reason : undefined;
24
+
25
+ if (locked) {
26
+ return (
27
+ <div className={styles.root} data-locked="">
28
+ {children}
29
+ <DsTooltip content={reason}>
30
+ <span className={styles.lockIcon} aria-label="Editing disabled" role="img">
31
+ <DsIcon icon="lock" size="small" color="secondary" />
32
+ </span>
33
+ </DsTooltip>
34
+ </div>
35
+ );
36
+ }
37
+
19
38
  return (
20
39
  <div
21
40
  className={styles.root}
@@ -23,6 +23,10 @@ export interface DsTableContextType<TData, TValue> extends Partial<DsDataTablePr
23
23
  * ID of the currently active row
24
24
  */
25
25
  activeRowId?: string | null;
26
+ /**
27
+ * Toggles the collapsed state of a column group by its id.
28
+ */
29
+ onToggleColumnGroup?: (groupId: string) => void;
26
30
  /** The cell currently in edit mode, if any. */
27
31
  editing: EditingState<TData, TValue> | null;
28
32
  beginEdit: (cell: Cell<TData, TValue>) => void;
@@ -1,6 +1,15 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { CellContext, RowData } from '@tanstack/react-table';
3
3
 
4
+ /**
5
+ * Result of a column's `editDisabled` resolver for a single cell.
6
+ *
7
+ * - `false` (or `undefined`) keeps the cell editable.
8
+ * - `true` locks the cell with no tooltip.
9
+ * - `{ reason }` locks the cell and shows `reason` in a tooltip on hover.
10
+ */
11
+ export type DsTableEditDisabledResult = boolean | { reason?: ReactNode };
12
+
4
13
  declare module '@tanstack/react-table' {
5
14
  interface ColumnDefBase<TData extends RowData, TValue> {
6
15
  /**
@@ -8,5 +17,20 @@ declare module '@tanstack/react-table' {
8
17
  * When omitted, the column is not editable.
9
18
  */
10
19
  editCell?: (context: CellContext<TData, TValue>) => ReactNode;
20
+
21
+ /**
22
+ * Locks individual cells of an otherwise editable column (one that defines
23
+ * `editCell`). When this returns a truthy value for a cell, that cell shows a
24
+ * lock icon instead of the edit affordance and cannot enter edit mode. Return
25
+ * `{ reason }` to surface an optional tooltip explaining why. Has no effect on
26
+ * columns without `editCell`.
27
+ */
28
+ editDisabled?: (context: CellContext<TData, TValue>) => DsTableEditDisabledResult;
29
+
30
+ /**
31
+ * Renders this column's cell content while the table is `loading`.
32
+ * When omitted, a default text skeleton is rendered.
33
+ */
34
+ loadingCell?: (context: CellContext<TData, TValue>) => ReactNode;
11
35
  }
12
36
  }
@@ -25,6 +25,8 @@ import { DsTableHeaderSelectableCell } from './components/ds-table-header-select
25
25
  import { useDragAndDrop } from './hooks/use-drag-and-drop';
26
26
  import { type DsTableContextType, DsTableContext, useEditingState } from './context/ds-table-context';
27
27
  import { DsTableBodyVirtualized } from './components/ds-table-body-virtualized';
28
+ import { useColumnGroups } from './grouping';
29
+ import { DsSkeletonText } from '../ds-skeleton';
28
30
  import {
29
31
  EMPTY_TABLE_STATE_TEXT,
30
32
  EXPANDER_COLUMN_ID,
@@ -33,6 +35,7 @@ import {
33
35
  REORDER_COLUMN_WIDTH,
34
36
  SELECT_COLUMN_ID,
35
37
  SELECT_COLUMN_WIDTH,
38
+ SKELETON_ROW_COUNT,
36
39
  } from './utils/constants';
37
40
 
38
41
  // Row size to pixel height mapping (matches CSS variables)
@@ -58,6 +61,7 @@ const DsTable = <TData extends { id: string }, TValue>({
58
61
  bordered = true,
59
62
  fullWidth = true,
60
63
  rowSize = 'medium',
64
+ loading = false,
61
65
  expandable = false,
62
66
  renderExpandedRow,
63
67
  selectable = false,
@@ -75,6 +79,9 @@ const DsTable = <TData extends { id: string }, TValue>({
75
79
  onColumnFiltersChange,
76
80
  columnVisibility: externalColumnVisibility,
77
81
  onColumnVisibilityChange,
82
+ collapsedColumnGroups: externalCollapsedColumnGroups,
83
+ onCollapsedColumnGroupsChange,
84
+ locale,
78
85
  activeRowId,
79
86
  infiniteScroll,
80
87
  onCellEdit,
@@ -109,12 +116,12 @@ const DsTable = <TData extends { id: string }, TValue>({
109
116
  }
110
117
  };
111
118
 
112
- const columnVisibility = externalColumnVisibility ?? internalColumnVisibility;
119
+ const baseColumnVisibility = externalColumnVisibility ?? internalColumnVisibility;
113
120
  const handleColumnVisibilityChange = (
114
121
  updaterOrValue: VisibilityState | ((old: VisibilityState) => VisibilityState),
115
122
  ) => {
116
123
  const newVisibility =
117
- typeof updaterOrValue === 'function' ? updaterOrValue(columnVisibility) : updaterOrValue;
124
+ typeof updaterOrValue === 'function' ? updaterOrValue(baseColumnVisibility) : updaterOrValue;
118
125
 
119
126
  if (onColumnVisibilityChange) {
120
127
  onColumnVisibilityChange(newVisibility);
@@ -123,6 +130,17 @@ const DsTable = <TData extends { id: string }, TValue>({
123
130
  }
124
131
  };
125
132
 
133
+ const { collapsedColumnGroups, toggleColumnGroup, collapsedVisibility } = useColumnGroups({
134
+ columns: columnsProp,
135
+ collapsedColumnGroups: externalCollapsedColumnGroups,
136
+ onCollapsedColumnGroupsChange,
137
+ });
138
+
139
+ const columnVisibility = useMemo(
140
+ () => ({ ...baseColumnVisibility, ...collapsedVisibility }),
141
+ [baseColumnVisibility, collapsedVisibility],
142
+ );
143
+
126
144
  const handleSortingChange = (updaterOrValue: SortingState | ((old: SortingState) => SortingState)) => {
127
145
  const newSorting = typeof updaterOrValue === 'function' ? updaterOrValue(sorting) : updaterOrValue;
128
146
  setSorting(newSorting);
@@ -186,9 +204,29 @@ const DsTable = <TData extends { id: string }, TValue>({
186
204
  return augmentedColumns;
187
205
  }, [columnsProp, hasExpanderColumn, hasReorderColumn, hasSelectColumn, showSelectAllCheckbox]);
188
206
 
207
+ const skeletonColumns = useMemo<ColumnDef<TData, TValue>[]>(() => {
208
+ const toSkeleton = (cols: ColumnDef<TData, TValue>[]): ColumnDef<TData, TValue>[] =>
209
+ cols.map((column) =>
210
+ 'columns' in column && column.columns
211
+ ? { ...column, columns: toSkeleton(column.columns) }
212
+ : { ...column, cell: column.loadingCell ?? (() => <DsSkeletonText width="60%" />) },
213
+ );
214
+
215
+ return toSkeleton(columns);
216
+ }, [columns]);
217
+
218
+ const skeletonData = useMemo<TData[]>(
219
+ () =>
220
+ Array.from(
221
+ { length: SKELETON_ROW_COUNT },
222
+ (_, i) => ({ id: `ds-table-skeleton-${String(i)}` }) as TData,
223
+ ),
224
+ [],
225
+ );
226
+
189
227
  const table = useReactTable({
190
- data: reorderable ? data : tableData,
191
- columns,
228
+ data: loading ? skeletonData : reorderable ? data : tableData,
229
+ columns: loading ? skeletonColumns : columns,
192
230
  getCoreRowModel: getCoreRowModel(),
193
231
  onSortingChange: handleSortingChange,
194
232
  getSortedRowModel: getSortedRowModel(),
@@ -280,13 +318,17 @@ const DsTable = <TData extends { id: string }, TValue>({
280
318
  selectable,
281
319
  reorderable,
282
320
  showSelectAllCheckbox,
283
- onRowClick,
284
- onRowDoubleClick,
321
+ loading,
322
+ onRowClick: loading ? undefined : onRowClick,
323
+ onRowDoubleClick: loading ? undefined : onRowDoubleClick,
285
324
  primaryRowActions,
286
325
  secondaryRowActions,
287
326
  renderExpandedRow,
288
327
  virtualized,
289
328
  activeRowId,
329
+ collapsedColumnGroups,
330
+ onToggleColumnGroup: toggleColumnGroup,
331
+ locale,
290
332
  onCellEdit,
291
333
  ...editingState,
292
334
  };
@@ -16,6 +16,31 @@ import type { InfiniteScrollConfig, ScrollParams } from './components/ds-table-b
16
16
  */
17
17
  export type DsTableRowSize = 'small' | 'medium' | 'large';
18
18
 
19
+ /**
20
+ * Overridable, user-facing strings used by the table (e.g. accessible labels for
21
+ * the column-group collapse toggle). Pass a partial object via the `locale` prop
22
+ * to localize.
23
+ */
24
+ export interface DsTableLocale {
25
+ /**
26
+ * Accessible label for the toggle that collapses a column group.
27
+ */
28
+ collapseColumnGroup: string;
29
+
30
+ /**
31
+ * Accessible label for the toggle that expands a column group.
32
+ */
33
+ expandColumnGroup: string;
34
+ }
35
+
36
+ /**
37
+ * Default English strings for {@link DsTableLocale}.
38
+ */
39
+ export const defaultDsTableLocale: DsTableLocale = Object.freeze({
40
+ collapseColumnGroup: 'Collapse column group',
41
+ expandColumnGroup: 'Expand column group',
42
+ });
43
+
19
44
  /**
20
45
  * API for programmatically controlling a DsTable component.
21
46
  * This interface provides methods to interact with the table's selection, sorting, filtering, and pagination features.
@@ -289,6 +314,13 @@ export interface DsDataTableProps<TData, TValue> {
289
314
  */
290
315
  rowSize?: DsTableRowSize;
291
316
 
317
+ /**
318
+ * When `true`, renders placeholder skeleton rows instead of the data. Column
319
+ * headers stay intact and row interactions are disabled while loading.
320
+ * @default false
321
+ */
322
+ loading?: boolean;
323
+
292
324
  /**
293
325
  * Whether the table is expandable or if an individual row should be expandable
294
326
  */
@@ -442,6 +474,36 @@ export interface DsDataTableProps<TData, TValue> {
442
474
  */
443
475
  onColumnVisibilityChange?: (visibility: VisibilityState) => void;
444
476
 
477
+ /**
478
+ * Controlled list of collapsed column-group ids. A group is collapsible when its
479
+ * column def sets `meta.group.collapsible`; collapsing hides every leaf column in
480
+ * the group except those marked `meta.keepVisibleWhenCollapsed`. Omit for
481
+ * uncontrolled behavior (seeded from `meta.group.defaultCollapsed`).
482
+ *
483
+ * @example
484
+ * ```tsx
485
+ * const [collapsed, setCollapsed] = useState<string[]>(['equipment']);
486
+ *
487
+ * <DsTable
488
+ * collapsedColumnGroups={collapsed}
489
+ * onCollapsedColumnGroupsChange={setCollapsed}
490
+ * />
491
+ * ```
492
+ */
493
+ collapsedColumnGroups?: string[];
494
+
495
+ /**
496
+ * Callback fired with the next list of collapsed group ids when a group's
497
+ * collapse toggle is clicked.
498
+ */
499
+ onCollapsedColumnGroupsChange?: (collapsedGroupIds: string[]) => void;
500
+
501
+ /**
502
+ * Overrides for user-facing strings (e.g. column-group toggle labels). Merged
503
+ * over the built-in English defaults.
504
+ */
505
+ locale?: Partial<DsTableLocale>;
506
+
445
507
  /**
446
508
  * Callback fired when an editable cell commits a new value via one of the
447
509
  * `DsTableEditCell*` wrappers or the `useCellEditor` hook. The parent is
@@ -0,0 +1,17 @@
1
+ // url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=36535-44213
2
+ // source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-table
3
+ // component=DsTable
4
+ //
5
+ // `DAP_Table Expandable_v03` is the expandable table on the "Table New (Data grid)"
6
+ // page. It maps to the same `DsTable` code component as the simple table, with the
7
+ // `expandable` prop enabled and a `renderExpandedRow` callback for the nested content.
8
+ // The Simple vs Expandable split exists only because of a Figma restriction (a
9
+ // component can't contain an instance of itself); in code it's one `DsTable`.
10
+ import figma from 'figma';
11
+
12
+ export default {
13
+ example: figma.code`<DsTable columns={columns} data={data} expandable renderExpandedRow={(row) => null} />`,
14
+ imports: ["import { DsTable } from '@drivenets/design-system'"],
15
+ id: 'ds-table-expandable',
16
+ metadata: { nestable: false },
17
+ };
@@ -0,0 +1,16 @@
1
+ // url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=36354-1965
2
+ // source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-table
3
+ // component=DsTable
4
+ //
5
+ // `DAP_Table Simple_v03` is the non-expandable table on the "Table New (Data grid)"
6
+ // page. Figma models the table as separate components (header/row/cell),
7
+ // while code exposes a single declarative `DsTable` driven by `columns`/`data`,
8
+ // so the snippet is a representative usage that references those consts.
9
+ import figma from 'figma';
10
+
11
+ export default {
12
+ example: figma.code`<DsTable columns={columns} data={data} />`,
13
+ imports: ["import { DsTable } from '@drivenets/design-system'"],
14
+ id: 'ds-table',
15
+ metadata: { nestable: false },
16
+ };
@@ -0,0 +1,73 @@
1
+ import { useMemo } from 'react';
2
+ import type { ColumnDef, VisibilityState } from '@tanstack/react-table';
3
+ import { useControlled } from '../../../../utils/use-controlled';
4
+ import { getCollapsedColumnVisibility, getInitialCollapsedGroups } from '../utils/column-group';
5
+
6
+ interface UseColumnGroupsParams<TData, TValue> {
7
+ /**
8
+ * Consumer-provided columns (before utility columns are injected). Group defs
9
+ * nest child columns via `columns`.
10
+ */
11
+ columns: ColumnDef<TData, TValue>[];
12
+
13
+ /**
14
+ * Controlled list of collapsed group ids. When provided, the table no longer
15
+ * manages collapse state internally.
16
+ */
17
+ collapsedColumnGroups?: string[];
18
+
19
+ /**
20
+ * Called with the next list of collapsed group ids whenever a group is toggled.
21
+ */
22
+ onCollapsedColumnGroupsChange?: (collapsedGroupIds: string[]) => void;
23
+ }
24
+
25
+ interface UseColumnGroupsResult {
26
+ /**
27
+ * The effective list of collapsed group ids (controlled or internal).
28
+ */
29
+ collapsedColumnGroups: string[];
30
+
31
+ /**
32
+ * Toggles a single group's collapsed state.
33
+ */
34
+ toggleColumnGroup: (groupId: string) => void;
35
+
36
+ /**
37
+ * Visibility overrides derived from the collapsed groups, to be layered on top
38
+ * of the consumer's own column visibility.
39
+ */
40
+ collapsedVisibility: VisibilityState;
41
+ }
42
+
43
+ /**
44
+ * Manages column-group collapse state. Supports both controlled
45
+ * (`collapsedColumnGroups` + `onCollapsedColumnGroupsChange`) and uncontrolled
46
+ * usage (seeded from `meta.group.defaultCollapsed`).
47
+ */
48
+ export const useColumnGroups = <TData, TValue>({
49
+ columns,
50
+ collapsedColumnGroups,
51
+ onCollapsedColumnGroupsChange,
52
+ }: UseColumnGroupsParams<TData, TValue>): UseColumnGroupsResult => {
53
+ const [collapsed, setCollapsed] = useControlled(
54
+ collapsedColumnGroups,
55
+ onCollapsedColumnGroupsChange,
56
+ getInitialCollapsedGroups(columns),
57
+ );
58
+
59
+ const toggleColumnGroup = (groupId: string) => {
60
+ const next = collapsed.includes(groupId)
61
+ ? collapsed.filter((id) => id !== groupId)
62
+ : [...collapsed, groupId];
63
+
64
+ setCollapsed(next);
65
+ };
66
+
67
+ const collapsedVisibility = useMemo(
68
+ () => getCollapsedColumnVisibility(columns, collapsed),
69
+ [columns, collapsed],
70
+ );
71
+
72
+ return { collapsedColumnGroups: collapsed, toggleColumnGroup, collapsedVisibility };
73
+ };
@@ -0,0 +1,5 @@
1
+ import './types/column-group-meta.augmentation';
2
+
3
+ export type { DsColumnGroupMeta } from './types/column-group.types';
4
+ export { useColumnGroups } from './hooks/use-column-groups';
5
+ export { getGroupColumnSizeStyle, isFirstLeafColumnOfGroup } from './utils/column-group';
@@ -0,0 +1,26 @@
1
+ import '@tanstack/react-table';
2
+ import type { DsColumnGroupMeta } from './column-group.types';
3
+
4
+ declare module '@tanstack/react-table' {
5
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
6
+ interface ColumnMeta<TData, TValue> {
7
+ /**
8
+ * Set on a grouping column def (one that nests child `columns`) to configure
9
+ * the group header and its collapse behavior.
10
+ */
11
+ group?: DsColumnGroupMeta;
12
+
13
+ /**
14
+ * Set on a leaf column to keep it visible while its parent group is collapsed
15
+ * (e.g. a key identifier column that should always stay on screen).
16
+ *
17
+ * Only affects the collapsed state. When `false` or omitted (the default),
18
+ * the column is hidden while the group is collapsed and shown again when the
19
+ * group is expanded — it is never permanently hidden by this flag.
20
+ * @default false
21
+ */
22
+ keepVisibleWhenCollapsed?: boolean;
23
+ }
24
+ }
25
+
26
+ export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Configuration for a grouping column, set on a column definition that nests
3
+ * child columns via TanStack's `columns` array. Lives under `meta.group`.
4
+ *
5
+ * @example
6
+ * ```tsx
7
+ * const columns: ColumnDef<Row>[] = [
8
+ * {
9
+ * id: 'equipment',
10
+ * header: 'Equipment',
11
+ * meta: { group: { collapsible: true } },
12
+ * columns: [
13
+ * { accessorKey: 'cabinetType', header: 'Cabinet Type', meta: { keepVisibleWhenCollapsed: true } },
14
+ * { accessorKey: 'shelf', header: 'Shelf#' },
15
+ * ],
16
+ * },
17
+ * ];
18
+ * ```
19
+ */
20
+ export interface DsColumnGroupMeta {
21
+ /**
22
+ * Renders a collapse/expand toggle in the group header. When collapsed, every
23
+ * leaf column in the group is hidden except those marked
24
+ * `meta.keepVisibleWhenCollapsed`.
25
+ * @default false
26
+ */
27
+ collapsible?: boolean;
28
+
29
+ /**
30
+ * When the table manages collapse state internally (uncontrolled), start this
31
+ * group collapsed. Ignored once `collapsedColumnGroups` is provided.
32
+ * @default false
33
+ */
34
+ defaultCollapsed?: boolean;
35
+ }
@@ -0,0 +1,119 @@
1
+ import type { CSSProperties } from 'react';
2
+ import {
3
+ type Column,
4
+ type ColumnDef,
5
+ type VisibilityState,
6
+ defaultColumnSizing,
7
+ } from '@tanstack/react-table';
8
+
9
+ /**
10
+ * Builds the flex style for a group header cell so it lines up with the combined
11
+ * width of its visible leaf columns.
12
+ *
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.
16
+ */
17
+ export const getGroupColumnSizeStyle = <TData, TValue>(
18
+ leafColumns: Column<TData, TValue>[],
19
+ ): CSSProperties => {
20
+ let fixedWidth = 0;
21
+ let flexCount = 0;
22
+
23
+ for (const leaf of leafColumns) {
24
+ const size = leaf.getSize();
25
+ if (size !== defaultColumnSizing.size) {
26
+ fixedWidth += size;
27
+ } else {
28
+ flexCount += 1;
29
+ }
30
+ }
31
+
32
+ if (flexCount === 0) {
33
+ return { width: fixedWidth, minWidth: fixedWidth, flexShrink: 0 };
34
+ }
35
+
36
+ return { flexGrow: flexCount, flexShrink: 0, flexBasis: fixedWidth, minWidth: fixedWidth };
37
+ };
38
+
39
+ /**
40
+ * Whether a column is the first visible leaf of its parent group. Used to draw a
41
+ * single side border at the start of each group in both the header and body.
42
+ */
43
+ export const isFirstLeafColumnOfGroup = <TData, TValue>(column: Column<TData, TValue>): boolean => {
44
+ const parent = column.parent;
45
+ if (!parent) {
46
+ return false;
47
+ }
48
+
49
+ const visibleLeaves = parent.getLeafColumns().filter((leaf) => leaf.getIsVisible());
50
+
51
+ return visibleLeaves[0]?.id === column.id;
52
+ };
53
+
54
+ type GroupColumnDef<TData, TValue> = ColumnDef<TData, TValue> & {
55
+ id?: string;
56
+ accessorKey?: string;
57
+ columns?: ColumnDef<TData, TValue>[];
58
+ };
59
+
60
+ const getLeafColumnId = <TData, TValue>(def: GroupColumnDef<TData, TValue>): string | undefined =>
61
+ def.id ?? def.accessorKey;
62
+
63
+ /**
64
+ * Collects the ids of groups declared with `meta.group.defaultCollapsed`, used to
65
+ * seed the uncontrolled collapse state.
66
+ */
67
+ export const getInitialCollapsedGroups = <TData, TValue>(columns: ColumnDef<TData, TValue>[]): string[] => {
68
+ const collapsed: string[] = [];
69
+
70
+ const visit = (defs: ColumnDef<TData, TValue>[]) => {
71
+ for (const def of defs as GroupColumnDef<TData, TValue>[]) {
72
+ if (def.columns) {
73
+ if (def.id && def.meta?.group?.defaultCollapsed) {
74
+ collapsed.push(def.id);
75
+ }
76
+ visit(def.columns);
77
+ }
78
+ }
79
+ };
80
+
81
+ visit(columns);
82
+
83
+ return collapsed;
84
+ };
85
+
86
+ /**
87
+ * Maps collapsed groups to a {@link VisibilityState} that hides every leaf column
88
+ * inside a collapsed group except those flagged `meta.keepVisibleWhenCollapsed`.
89
+ * Layered on top of the consumer's own visibility so collapse never mutates it.
90
+ */
91
+ export const getCollapsedColumnVisibility = <TData, TValue>(
92
+ columns: ColumnDef<TData, TValue>[],
93
+ collapsedGroupIds: string[],
94
+ ): VisibilityState => {
95
+ const visibility: VisibilityState = {};
96
+
97
+ const visit = (defs: ColumnDef<TData, TValue>[], isInsideCollapsedGroup: boolean) => {
98
+ for (const def of defs as GroupColumnDef<TData, TValue>[]) {
99
+ if (def.columns) {
100
+ const isGroupCollapsed = isInsideCollapsedGroup || (!!def.id && collapsedGroupIds.includes(def.id));
101
+ visit(def.columns, isGroupCollapsed);
102
+ continue;
103
+ }
104
+
105
+ if (!isInsideCollapsedGroup || def.meta?.keepVisibleWhenCollapsed) {
106
+ continue;
107
+ }
108
+
109
+ const columnId = getLeafColumnId(def);
110
+ if (columnId) {
111
+ visibility[columnId] = false;
112
+ }
113
+ }
114
+ };
115
+
116
+ visit(columns, false);
117
+
118
+ return visibility;
119
+ };
@@ -1,7 +1,11 @@
1
+ import './grouping/types/column-group-meta.augmentation';
2
+
1
3
  export type { SecondaryRowAction, RowAction } from './components/ds-table-cell';
2
4
  export type { ScrollParams } from './components/ds-table-body-virtualized';
3
5
 
4
6
  export * from './filters';
7
+ export type { DsColumnGroupMeta } from './grouping';
8
+ export type { DsTableEditDisabledResult } from './ds-table-column.types';
5
9
 
6
10
  export { default as DsTable } from './ds-table';
7
11
  export * from './ds-table.types';
@@ -1,5 +1,6 @@
1
1
  $border-radius: 4px;
2
2
  $border-width: 1px;
3
+ $group-border-width: 2px;
3
4
  $virtualized-height: 600px;
4
5
  $reorder-column-width: 60px;
5
6
  $container-margin-vertical: 1rem;
@@ -33,3 +33,8 @@ export const REORDER_COLUMN_ID = 'reorder';
33
33
  * Width (in px) of the reorder column.
34
34
  */
35
35
  export const REORDER_COLUMN_WIDTH = 60;
36
+
37
+ /**
38
+ * Number of placeholder skeleton rows rendered while the table is `loading`.
39
+ */
40
+ export const SKELETON_ROW_COUNT = 5;