@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
@@ -0,0 +1,65 @@
1
+ import type { CSSProperties, Ref } from 'react';
2
+
3
+ export const taskBarStatuses = ['completed', 'running', 'failed'] as const;
4
+ export type TaskBarStatus = (typeof taskBarStatuses)[number];
5
+
6
+ export interface DsProgressTaskBarLocale {
7
+ /** @default 'Completed' */
8
+ completed?: string;
9
+ /** @default 'Running' */
10
+ running?: string;
11
+ /** @default 'Failed' */
12
+ failed?: string;
13
+ /**
14
+ * Builds the trailing total label from the already-formatted total value.
15
+ * @default (total) => `of ${total}`
16
+ */
17
+ total?: (total: string) => string;
18
+ }
19
+
20
+ export interface DsProgressTaskBarProps {
21
+ /**
22
+ * Number of completed tasks.
23
+ * @default 0
24
+ */
25
+ completed?: number;
26
+
27
+ /**
28
+ * Number of running tasks.
29
+ * @default 0
30
+ */
31
+ running?: number;
32
+
33
+ /**
34
+ * Number of failed tasks.
35
+ * @default 0
36
+ */
37
+ failed?: number;
38
+
39
+ /**
40
+ * Total task count used as the denominator for segment widths and shown as
41
+ * the trailing "of {total}" label. Defaults to the sum of the status counts
42
+ * (fully filled bar).
43
+ */
44
+ total?: number;
45
+
46
+ /**
47
+ * Localized strings (status tooltips and total label).
48
+ */
49
+ locale?: DsProgressTaskBarLocale;
50
+
51
+ /**
52
+ * Additional CSS class names
53
+ */
54
+ className?: string;
55
+
56
+ /**
57
+ * Additional styles to apply to the component
58
+ */
59
+ style?: CSSProperties;
60
+
61
+ /**
62
+ * Ref to the root element
63
+ */
64
+ ref?: Ref<HTMLDivElement>;
65
+ }
@@ -0,0 +1,2 @@
1
+ export { default as DsProgressTaskBar } from './ds-progress-task-bar';
2
+ export * from './ds-progress-task-bar.types';
@@ -0,0 +1,51 @@
1
+ interface CountUnit {
2
+ value: number;
3
+ suffix: string;
4
+ }
5
+
6
+ const COUNT_UNITS: readonly CountUnit[] = [
7
+ { value: 1_000_000_000, suffix: 'B' },
8
+ { value: 1_000_000, suffix: 'M' },
9
+ { value: 1_000, suffix: 'K' },
10
+ ];
11
+
12
+ // Counts below this are shown verbatim; at or above it they are abbreviated.
13
+ const ABBREVIATION_FLOOR = 1_000;
14
+
15
+ // Below this multiple of a unit, show one decimal place (e.g. 1.1K); at or
16
+ // above it, show a whole number (e.g. 87K).
17
+ const DECIMAL_THRESHOLD = 10;
18
+
19
+ const stripTrailingZero = (value: number): string => (value % 1 === 0 ? String(value) : value.toFixed(1));
20
+
21
+ /**
22
+ * Format a task count for the legend, following the design's abbreviation rules:
23
+ * - `< 1000` → exact number (e.g. `999`)
24
+ * - single-digit unit → one decimal (e.g. `1.1K`, `1.2M`)
25
+ * - multi-digit unit → whole number, truncated so `999,999 → 999K`
26
+ */
27
+ export const formatCount = (count: number): string => {
28
+ if (!Number.isFinite(count)) {
29
+ return '0';
30
+ }
31
+
32
+ const rounded = Math.max(0, Math.round(count));
33
+
34
+ if (rounded < ABBREVIATION_FLOOR) {
35
+ return String(rounded);
36
+ }
37
+
38
+ const unit = COUNT_UNITS.find(({ value }) => rounded >= value);
39
+
40
+ if (unit === undefined) {
41
+ return String(rounded);
42
+ }
43
+
44
+ const scaled = rounded / unit.value;
45
+
46
+ if (scaled < DECIMAL_THRESHOLD) {
47
+ return `${stripTrailingZero(Math.round(scaled * 10) / 10)}${unit.suffix}`;
48
+ }
49
+
50
+ return `${String(Math.floor(scaled))}${unit.suffix}`;
51
+ };
@@ -0,0 +1,53 @@
1
+ // url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=13773-1189
2
+ // source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-select
3
+ // component=DsSelect
4
+ import figma from 'figma';
5
+
6
+ const instance = figma.selectedInstance;
7
+
8
+ const size = instance.getEnum('Size', {
9
+ Default: 'default',
10
+ Small: 'small',
11
+ });
12
+
13
+ const state = instance.getEnum('State', {
14
+ Default: 'default',
15
+ Hover: 'hover',
16
+ Focus: 'focus',
17
+ Active: 'active',
18
+ Disable: 'disable',
19
+ Error: 'error',
20
+ 'Error - Focus': 'errorFocus',
21
+ });
22
+
23
+ const disabled = state === 'disable';
24
+
25
+ const sizeAttr = size === 'small' ? 'size="small"' : '';
26
+
27
+ // Multi-select is driven by the nested dropdown menu's `Type` (Checkbox = multi). The menu
28
+ // lives inside the select tree even when collapsed, so the designer's intent is read in any
29
+ // state; an absent menu (ErrorHandle) safely defaults to single-select.
30
+ const menu = instance.findInstance('DAP_dropdown menu_v01', { traverseInstances: true });
31
+ const multiple =
32
+ menu.type === 'INSTANCE' && menu.getEnum('Type', { default: false, Checkbox: true, Radio: false }) === true;
33
+
34
+ // `value` and `options` are required by `DsSelectProps` but are data, not derivable from
35
+ // Figma, so emit placeholders for the developer to fill in (the discriminated union needs
36
+ // an array `value` when `multiple`). The same string is surfaced as `fcProps` so a parent
37
+ // `DsFormControl` template can emit `<DsFormControl.Select ${fcProps} />` uniformly.
38
+ const attrs = [
39
+ sizeAttr,
40
+ disabled ? 'disabled' : '',
41
+ multiple ? 'multiple' : '',
42
+ 'options={[]}',
43
+ multiple ? 'value={[]}' : 'value=""',
44
+ ]
45
+ .filter(Boolean)
46
+ .join(' ');
47
+
48
+ export default {
49
+ example: figma.code`<DsSelect ${attrs} />`,
50
+ imports: ["import { DsSelect } from '@drivenets/design-system'"],
51
+ id: 'ds-select',
52
+ metadata: { nestable: true, props: { fcMember: 'Select', fcProps: attrs } },
53
+ };
@@ -0,0 +1,95 @@
1
+ // url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=32830-9950
2
+ // source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-split-button
3
+ // component=DsSplitButton
4
+ import figma from 'figma';
5
+
6
+ const instance = figma.selectedInstance;
7
+
8
+ const state = instance.getEnum('state', {
9
+ default: 'default',
10
+ hover: 'hover',
11
+ select: 'select',
12
+ focus: 'focus',
13
+ disabled: 'disabled',
14
+ running: 'running',
15
+ });
16
+ const disabled = state === 'disabled';
17
+ const loading = state === 'running';
18
+
19
+ const button = instance.findInstance('DAP_Button_v03', { traverseInstances: true });
20
+ const size =
21
+ button.type === 'INSTANCE'
22
+ ? button.getEnum('Size', { tiny: 'small', small: 'small', medium: 'medium', large: 'medium' })
23
+ : 'medium';
24
+
25
+ const structure = instance.findInstance('.button-structure', { traverseInstances: true });
26
+ const showIcon = structure.type === 'INSTANCE' ? structure.getBoolean('Show icon') : false;
27
+
28
+ // `DsButtonV3.icon` is an icon-name string. Swapped icons keep the placeholder layer
29
+ // name (e.g. `DAP_GM_O_check_circle`), so resolve via Code Connect ID or metadata.
30
+ const toIconName = (name: string): string =>
31
+ name
32
+ .replace(/^DAP_DAP_/, 'DAP_')
33
+ .replace(/^DAP_GM_[OS]_/, '')
34
+ .replace(/^DAP_outline_icon\//, '')
35
+ .replace(/^DAP_/, '')
36
+ .replace(/^\/+/, '')
37
+ .replace(/[\s/]+/g, '_')
38
+ .toLowerCase();
39
+
40
+ const iconNameFromCodeConnectId = (id: string | null): string | undefined => {
41
+ if (!id) {
42
+ return undefined;
43
+ }
44
+ if (id.startsWith('ds-icon-o-')) {
45
+ return id.slice('ds-icon-o-'.length);
46
+ }
47
+ if (id.startsWith('ds-icon-s-')) {
48
+ return id.slice('ds-icon-s-'.length);
49
+ }
50
+ return undefined;
51
+ };
52
+
53
+ const resolveIcon = (iconInstance: figma.InstanceHandle): string | undefined => {
54
+ const fromCcId = iconNameFromCodeConnectId(iconInstance.codeConnectId());
55
+ if (fromCcId) {
56
+ return fromCcId;
57
+ }
58
+
59
+ const iconFromMetadata = iconInstance.executeTemplate().metadata?.props?.icon;
60
+ if (typeof iconFromMetadata === 'string') {
61
+ return iconFromMetadata;
62
+ }
63
+
64
+ return toIconName(iconInstance.name);
65
+ };
66
+
67
+ const leadingIcon = structure.type === 'INSTANCE' ? structure.getInstanceSwap('Leading icon') : undefined;
68
+ const iconFromSwap = leadingIcon?.type === 'INSTANCE' ? resolveIcon(leadingIcon) : undefined;
69
+ const iconFromChild =
70
+ structure.type === 'INSTANCE'
71
+ ? structure
72
+ .findConnectedInstances((node) => node.codeConnectId()?.startsWith('ds-icon-') ?? false, {
73
+ traverseInstances: true,
74
+ })
75
+ .find((node): node is figma.InstanceHandle => node.type === 'INSTANCE')
76
+ : undefined;
77
+ const icon = iconFromSwap ?? (iconFromChild ? resolveIcon(iconFromChild) : undefined);
78
+
79
+ // The split button's button slot is intended for an icon-only button, so emit
80
+ // `icon` (and `loading`) rather than a text `children` label.
81
+ // While loading, a spinner replaces the icon, so omit it
82
+ // (the leading icon is the component's default placeholder in that state).
83
+ const buttonProps = [
84
+ !loading && showIcon && icon ? `icon: '${icon}'` : undefined,
85
+ loading ? 'loading: true' : undefined,
86
+ ]
87
+ .filter(Boolean)
88
+ .join(', ');
89
+
90
+ export default {
91
+ example: figma.code`<DsSplitButton size="${size}"${disabled ? ' disabled' : ''} slotProps={{ button: { ${buttonProps} }, select: { options: [], value: '' } }} />`,
92
+ imports: ["import { DsSplitButton } from '@drivenets/design-system'"],
93
+ id: 'ds-split-button',
94
+ metadata: { nestable: false },
95
+ };
@@ -0,0 +1,41 @@
1
+ // url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=34428-16204
2
+ // source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-status-badge-v2
3
+ // component=DsStatusBadgeV2
4
+ import figma from 'figma';
5
+
6
+ const instance = figma.selectedInstance;
7
+
8
+ const phase = instance.getEnum('status', {
9
+ 'not-started': 'not-started',
10
+ temporary: 'temporary',
11
+ 'in-review': 'in-review',
12
+ pending: 'pending',
13
+ active: 'active',
14
+ execution: 'execution',
15
+ 'result-succeeded': 'result-succeeded',
16
+ 'result-warning': 'result-warning',
17
+ 'result-failed': 'result-failed',
18
+ deprecated: 'deprecated',
19
+ });
20
+ const variant = instance.getEnum('variant', { primary: 'primary', secondary: 'secondary' });
21
+ const size = instance.getEnum('Size', { medium: 'medium', small: 'small' });
22
+ const structure = instance.getEnum('structure', {
23
+ 'icon+text': 'icon+text',
24
+ 'icon-only': 'icon-only',
25
+ 'text-only': 'text-only',
26
+ });
27
+
28
+ const labelNode = instance.findText('Active', { traverseInstances: true });
29
+ const label = labelNode.type === 'TEXT' ? labelNode.textContent : 'Active';
30
+
31
+ export default {
32
+ example:
33
+ structure === 'icon-only'
34
+ ? figma.code`<DsStatusBadgeV2 phase="${phase}" label="${label}" variant="${variant}" size="${size}" iconOnly />`
35
+ : structure === 'text-only'
36
+ ? figma.code`<DsStatusBadgeV2 phase="${phase}" label="${label}" variant="${variant}" size="${size}" icon={null} />`
37
+ : figma.code`<DsStatusBadgeV2 phase="${phase}" label="${label}" variant="${variant}" size="${size}" />`,
38
+ imports: ["import { DsStatusBadgeV2 } from '@drivenets/design-system'"],
39
+ id: 'ds-status-badge-v2',
40
+ metadata: { nestable: true },
41
+ };
@@ -110,7 +110,7 @@ interface DsTableBodyProps<TData> {
110
110
  rowSelection: RowSelectionState;
111
111
  }
112
112
 
113
- function DsTableBody<TData>({
113
+ export function DsTableBody<TData>({
114
114
  ref,
115
115
  rowVirtualizer,
116
116
  rowsMapRef,
@@ -122,6 +122,11 @@ function DsTableBody<TData>({
122
122
  const virtualRows = rowVirtualizer.getVirtualItems();
123
123
  const totalSize = rowVirtualizer.getTotalSize();
124
124
 
125
+ // Gate the empty state on real data count, not on rendered virtual rows.
126
+ // getVirtualItems() transiently returns [] while the virtualizer re-measures
127
+ // after a `data` swap; rendering nothing for that frame avoids an empty-state flash.
128
+ const hasNoData = rowsAndExpandedRowContent.length === 0;
129
+
125
130
  return (
126
131
  <TableBody ref={ref} className={styles.body}>
127
132
  {virtualRows.length > 0 ? (
@@ -144,11 +149,11 @@ function DsTableBody<TData>({
144
149
  />
145
150
  );
146
151
  })
147
- ) : (
152
+ ) : hasNoData ? (
148
153
  <TableRow>
149
154
  <TableCell className={styles.emptyState}>{emptyState || EMPTY_TABLE_STATE_TEXT}</TableCell>
150
155
  </TableRow>
151
- )}
156
+ ) : null}
152
157
 
153
158
  <tr aria-hidden className={styles.sentinel} style={{ top: `${totalSize.toString()}px` }}>
154
159
  <td />
@@ -0,0 +1,31 @@
1
+ @use '../../../../styles/typography';
2
+ @use '../../styles/variables' as vars;
3
+
4
+ .groupHeaderCell {
5
+ @include typography.body-sm-md;
6
+ color: var(--font-main);
7
+ height: var(--ds-table-header-height);
8
+ padding: 0;
9
+ border-bottom: vars.$border-width solid var(--color-dap-gray-400);
10
+ border-left: vars.$group-border-width solid var(--border);
11
+ }
12
+
13
+ .groupHeaderContent {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ gap: vars.$header-sort-gap;
18
+ width: 100%;
19
+ height: 100%;
20
+ padding: 0 var(--standard);
21
+ }
22
+
23
+ .groupLabel {
24
+ overflow: hidden;
25
+ text-overflow: ellipsis;
26
+ white-space: nowrap;
27
+ }
28
+
29
+ .collapseToggle {
30
+ flex-shrink: 0;
31
+ }
@@ -0,0 +1,48 @@
1
+ import { flexRender } from '@tanstack/react-table';
2
+ import { DsButtonV3 } from '../../../ds-button-v3';
3
+ import { TableHead } from '../core-table';
4
+ import { useDsTableContext } from '../../context/ds-table-context';
5
+ import { getGroupColumnSizeStyle } from '../../grouping';
6
+ import { defaultDsTableLocale } from '../../ds-table.types';
7
+ import type { DsTableGroupHeaderCellProps } from './ds-table-group-header-cell.types';
8
+ import styles from './ds-table-group-header-cell.module.scss';
9
+
10
+ export const DsTableGroupHeaderCell = <TData, TValue>({
11
+ header,
12
+ }: DsTableGroupHeaderCellProps<TData, TValue>) => {
13
+ const { collapsedColumnGroups, onToggleColumnGroup, locale } = useDsTableContext<TData, TValue>();
14
+
15
+ const groupId = header.column.id;
16
+ const visibleLeafColumns = header.column.getLeafColumns().filter((leaf) => leaf.getIsVisible());
17
+ const groupStyle = getGroupColumnSizeStyle(visibleLeafColumns);
18
+
19
+ const isCollapsible = header.column.columnDef.meta?.group?.collapsible ?? false;
20
+ const isCollapsed = collapsedColumnGroups?.includes(groupId) ?? false;
21
+
22
+ const mergedLocale = { ...defaultDsTableLocale, ...locale };
23
+ const toggleLabel = isCollapsed ? mergedLocale.expandColumnGroup : mergedLocale.collapseColumnGroup;
24
+
25
+ const handleToggle = () => onToggleColumnGroup?.(groupId);
26
+
27
+ return (
28
+ <TableHead className={styles.groupHeaderCell} style={groupStyle} colSpan={header.colSpan}>
29
+ <div className={styles.groupHeaderContent}>
30
+ <span className={styles.groupLabel}>
31
+ {flexRender(header.column.columnDef.header, header.getContext())}
32
+ </span>
33
+ {isCollapsible && (
34
+ <DsButtonV3
35
+ variant="secondary"
36
+ size="tiny"
37
+ icon={isCollapsed ? 'chevron_right' : 'chevron_left'}
38
+ className={styles.collapseToggle}
39
+ aria-label={toggleLabel}
40
+ aria-expanded={!isCollapsed}
41
+ title={toggleLabel}
42
+ onClick={handleToggle}
43
+ />
44
+ )}
45
+ </div>
46
+ </TableHead>
47
+ );
48
+ };
@@ -0,0 +1,12 @@
1
+ import type { Header } from '@tanstack/react-table';
2
+
3
+ /**
4
+ * Props for the grouping column header cell.
5
+ */
6
+ export interface DsTableGroupHeaderCellProps<TData, TValue> {
7
+ /**
8
+ * The grouping header from `@tanstack/react-table` (a header whose column nests
9
+ * child columns).
10
+ */
11
+ header: Header<TData, TValue>;
12
+ }
@@ -0,0 +1,2 @@
1
+ export { DsTableGroupHeaderCell } from './ds-table-group-header-cell';
2
+ export type { DsTableGroupHeaderCellProps } from './ds-table-group-header-cell.types';
@@ -57,6 +57,10 @@
57
57
  justify-content: center;
58
58
  }
59
59
 
60
+ .groupStart {
61
+ border-left: vars.$group-border-width solid var(--border);
62
+ }
63
+
60
64
  .headerSortContainer {
61
65
  display: flex;
62
66
  align-items: center;
@@ -11,6 +11,8 @@ import { SELECT_COLUMN_ID } from '../../utils/constants';
11
11
  import { DsStack } from '../../../ds-stack';
12
12
  import { DsTableColumnFilterPopover } from '../../filters/components/column-filter-popover';
13
13
  import type { ResolvedColumnFilter } from '../../filters/types/filter-adapter.types';
14
+ import { DsTableGroupHeaderCell } from '../ds-table-group-header-cell';
15
+ import { isFirstLeafColumnOfGroup } from '../../grouping';
14
16
 
15
17
  const DsTableHeader = <TData,>({ table }: DsTableHeaderProps<TData>) => {
16
18
  const { stickyHeader, bordered, virtualized } = useDsTableContext<TData, unknown>();
@@ -29,9 +31,24 @@ const DsTableHeader = <TData,>({ table }: DsTableHeaderProps<TData>) => {
29
31
  )}
30
32
  >
31
33
  {headerGroup.headers.map((header) => {
34
+ if (header.isPlaceholder) {
35
+ return (
36
+ <TableHead
37
+ key={header.id}
38
+ className={styles.headerCell}
39
+ style={getColumnSizeStyle(header.getSize())}
40
+ />
41
+ );
42
+ }
43
+
44
+ if (header.subHeaders.length > 0) {
45
+ return <DsTableGroupHeaderCell key={header.id} header={header} />;
46
+ }
47
+
32
48
  const headerStyle = getColumnSizeStyle(header.column.getSize());
33
49
  const canSort = header.column.getCanSort();
34
50
  const isSelectColumn = header.column.id === SELECT_COLUMN_ID;
51
+ const isGroupStart = isFirstLeafColumnOfGroup(header.column);
35
52
 
36
53
  const metaFilter = header.column.columnDef.meta?.filter;
37
54
  const resolvedFilter: ResolvedColumnFilter | undefined =
@@ -47,49 +64,48 @@ const DsTableHeader = <TData,>({ table }: DsTableHeaderProps<TData>) => {
47
64
  isSelectColumn && styles.selectHeaderCell,
48
65
  resolvedFilter && styles.filterableHeader,
49
66
  hasActiveColumnFilter && styles.filterActive,
67
+ isGroupStart && styles.groupStart,
50
68
  )}
51
69
  onClick={header.column.getToggleSortingHandler()}
52
70
  style={headerStyle}
53
71
  >
54
- {header.isPlaceholder ? null : (
55
- <div className={styles.headerSortContainer}>
56
- <DsStack direction="column" justifyContent="center" width="100%">
57
- {flexRender(header.column.columnDef.header, header.getContext())}
58
- </DsStack>
59
- {canSort && (
60
- <div className={styles.pageButtonIconContainer}>
61
- {{
62
- asc: (
63
- <DsIcon
64
- icon="arrow_drop_up"
65
- className={classnames(styles.pageButtonIcon, stylesShared.pageButtonIcon)}
66
- />
67
- ),
68
- desc: (
69
- <DsIcon
70
- icon="arrow_drop_down"
71
- className={classnames(styles.pageButtonIcon, stylesShared.pageButtonIcon)}
72
- />
73
- ),
74
- }[header.column.getIsSorted() as string] ?? (
75
- <div className={classnames(styles.pageButtonIcon, stylesShared.pageButtonIcon)} />
76
- )}
77
- </div>
78
- )}
79
- {resolvedFilter && (
80
- <div className={styles.filterTriggerContainer}>
81
- <DsTableColumnFilterPopover
82
- hasActiveFilter={resolvedFilter.hasActiveFilter}
83
- onApply={resolvedFilter.onApply}
84
- onClear={resolvedFilter.onClear}
85
- onCancel={resolvedFilter.onCancel}
86
- >
87
- {resolvedFilter.render()}
88
- </DsTableColumnFilterPopover>
89
- </div>
90
- )}
91
- </div>
92
- )}
72
+ <div className={styles.headerSortContainer}>
73
+ <DsStack direction="column" justifyContent="center" width="100%">
74
+ {flexRender(header.column.columnDef.header, header.getContext())}
75
+ </DsStack>
76
+ {canSort && (
77
+ <div className={styles.pageButtonIconContainer}>
78
+ {{
79
+ asc: (
80
+ <DsIcon
81
+ icon="arrow_drop_up"
82
+ className={classnames(styles.pageButtonIcon, stylesShared.pageButtonIcon)}
83
+ />
84
+ ),
85
+ desc: (
86
+ <DsIcon
87
+ icon="arrow_drop_down"
88
+ className={classnames(styles.pageButtonIcon, stylesShared.pageButtonIcon)}
89
+ />
90
+ ),
91
+ }[header.column.getIsSorted() as string] ?? (
92
+ <div className={classnames(styles.pageButtonIcon, stylesShared.pageButtonIcon)} />
93
+ )}
94
+ </div>
95
+ )}
96
+ {resolvedFilter && (
97
+ <div className={styles.filterTriggerContainer}>
98
+ <DsTableColumnFilterPopover
99
+ hasActiveFilter={resolvedFilter.hasActiveFilter}
100
+ onApply={resolvedFilter.onApply}
101
+ onClear={resolvedFilter.onClear}
102
+ onCancel={resolvedFilter.onCancel}
103
+ >
104
+ {resolvedFilter.render()}
105
+ </DsTableColumnFilterPopover>
106
+ </div>
107
+ )}
108
+ </div>
93
109
  </TableHead>
94
110
  );
95
111
  })}
@@ -9,7 +9,7 @@
9
9
  }
10
10
  }
11
11
 
12
- &:hover {
12
+ &:not(.loadingRow):hover {
13
13
  background-color: var(--background-secondary);
14
14
  }
15
15
 
@@ -85,6 +85,10 @@
85
85
  justify-content: center;
86
86
  }
87
87
 
88
+ .groupStart {
89
+ border-left: vars.$group-border-width solid var(--border);
90
+ }
91
+
88
92
  .tableRow .editableCell {
89
93
  padding: 1px;
90
94
 
@@ -10,6 +10,7 @@ import styles from './ds-table-row.module.scss';
10
10
  import { useDsTableContext } from '../../context/ds-table-context';
11
11
  import { mergeRefs } from '../../../../utils/merge-refs';
12
12
  import { getColumnSizeStyle } from '../../utils/column-size';
13
+ import { isFirstLeafColumnOfGroup } from '../../grouping';
13
14
  import { EXPANDER_COLUMN_ID, REORDER_COLUMN_ID, SELECT_COLUMN_ID } from '../../utils/constants';
14
15
 
15
16
  interface DsRowDragHandleProps {
@@ -50,6 +51,7 @@ const DsTableRow = <TData,>({ ref, row, isSelected }: DsTableRowProps<TData>) =>
50
51
  primaryRowActions,
51
52
  secondaryRowActions,
52
53
  activeRowId,
54
+ loading,
53
55
  } = useDsTableContext<TData, unknown>();
54
56
  const isExpanded = row.getIsExpanded();
55
57
  const isActive = activeRowId === row.id;
@@ -89,6 +91,7 @@ const DsTableRow = <TData,>({ ref, row, isSelected }: DsTableRowProps<TData>) =>
89
91
  onRowClick && styles.clickableRow,
90
92
  !bordered && styles.rowNoBorder,
91
93
  isExpanded && styles.expanded,
94
+ loading && styles.loadingRow,
92
95
  )}
93
96
  style={rowStyle}
94
97
  onClick={() => onRowClick?.(row.original)}
@@ -118,6 +121,7 @@ const DsTableRow = <TData,>({ ref, row, isSelected }: DsTableRowProps<TData>) =>
118
121
  styles.tableCell,
119
122
  cell.column.id === EXPANDER_COLUMN_ID && styles.expandableCell,
120
123
  cell.column.id === SELECT_COLUMN_ID && styles.selectableCell,
124
+ isFirstLeafColumnOfGroup(cell.column) && styles.groupStart,
121
125
  typeof cell.column.columnDef.editCell === 'function' && styles.editableCell,
122
126
  )}
123
127
  style={cellStyle}
@@ -7,7 +7,7 @@
7
7
  position: absolute;
8
8
  width: 100%;
9
9
 
10
- &:hover {
10
+ &:not(.loadingRow):hover {
11
11
  background-color: var(--background-secondary);
12
12
  }
13
13
 
@@ -66,6 +66,10 @@
66
66
  justify-content: center;
67
67
  }
68
68
 
69
+ .groupStart {
70
+ border-left: vars.$group-border-width solid var(--border);
71
+ }
72
+
69
73
  .expandedRowContent {
70
74
  background-color: var(--background-secondary);
71
75