@drivenets/design-system 0.18.1 → 0.19.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 (122) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/index.cjs +17138 -12075
  3. package/dist/index.d.cts +783 -238
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +783 -238
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +17123 -12076
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +15 -15
  11. package/src/components/ds-bot-button/ds-bot-button.figma.ts +38 -0
  12. package/src/components/ds-bot-button/ds-bot-button.module.scss +41 -0
  13. package/src/components/ds-bot-button/ds-bot-button.tsx +48 -0
  14. package/src/components/ds-bot-button/ds-bot-button.types.ts +33 -0
  15. package/src/components/ds-bot-button/index.ts +2 -0
  16. package/src/components/ds-bot-button/net-gen-logo.tsx +46 -0
  17. package/src/components/ds-bulk-actions/components/action-button.tsx +42 -0
  18. package/src/components/ds-bulk-actions/components/menu-entries.tsx +76 -0
  19. package/src/components/ds-bulk-actions/components/overflow.tsx +75 -0
  20. package/src/components/ds-bulk-actions/ds-bulk-actions.figma.ts +28 -0
  21. package/src/components/ds-bulk-actions/ds-bulk-actions.module.scss +199 -0
  22. package/src/components/ds-bulk-actions/ds-bulk-actions.tsx +220 -0
  23. package/src/components/ds-bulk-actions/ds-bulk-actions.types.ts +170 -0
  24. package/src/components/ds-bulk-actions/ds-bulk-actions.unit.test.ts +195 -0
  25. package/src/components/ds-bulk-actions/ds-bulk-actions.utils.ts +91 -0
  26. package/src/components/ds-bulk-actions/ds-bulk-actions.utils.unit.test.ts +134 -0
  27. package/src/components/ds-bulk-actions/hooks/compute-bulk-actions-overflow.ts +85 -0
  28. package/src/components/ds-bulk-actions/hooks/use-bulk-actions-overflow.ts +153 -0
  29. package/src/components/ds-bulk-actions/index.ts +18 -0
  30. package/src/components/ds-button-v3/ds-button-v3.module.scss +2 -2
  31. package/src/components/ds-catalog-layout/ds-catalog-layout.figma.ts +1 -1
  32. package/src/components/ds-dropdown-menu/ds-dropdown-menu.tsx +2 -0
  33. package/src/components/ds-empty-state/ds-empty-state.module.scss +16 -0
  34. package/src/components/ds-empty-state/ds-empty-state.tsx +45 -0
  35. package/src/components/ds-empty-state/ds-empty-state.types.ts +38 -0
  36. package/src/components/ds-empty-state/index.ts +3 -0
  37. package/src/components/ds-file-upload/ds-file-upload.tsx +4 -1
  38. package/src/components/ds-form-control/ds-form-control.tsx +18 -10
  39. package/src/components/ds-illustration/ds-illustration.figma.ts +34 -0
  40. package/src/components/ds-illustration/ds-illustration.module.scss +6 -0
  41. package/src/components/ds-illustration/ds-illustration.tsx +29 -0
  42. package/src/components/ds-illustration/ds-illustration.types.ts +45 -0
  43. package/src/components/ds-illustration/illustrations/configuration.tsx +51 -0
  44. package/src/components/ds-illustration/illustrations/device.tsx +133 -0
  45. package/src/components/ds-illustration/illustrations/done.tsx +107 -0
  46. package/src/components/{ds-catalog-layout/catalog-layout-empty-illustration.tsx → ds-illustration/illustrations/empty-inbox.tsx} +10 -14
  47. package/src/components/ds-illustration/illustrations/error.tsx +86 -0
  48. package/src/components/ds-illustration/illustrations/go-filter.tsx +51 -0
  49. package/src/components/ds-illustration/illustrations/illustration-clouds.tsx +10 -0
  50. package/src/components/ds-illustration/illustrations/index.ts +39 -0
  51. package/src/components/ds-illustration/illustrations/no-connection.tsx +54 -0
  52. package/src/components/ds-illustration/illustrations/no-credit-card.tsx +48 -0
  53. package/src/components/ds-illustration/illustrations/no-device.tsx +61 -0
  54. package/src/components/ds-illustration/illustrations/no-documents.tsx +52 -0
  55. package/src/components/ds-illustration/illustrations/no-gps.tsx +57 -0
  56. package/src/components/ds-illustration/illustrations/no-images.tsx +82 -0
  57. package/src/components/ds-illustration/illustrations/no-items-cart.tsx +86 -0
  58. package/src/components/ds-illustration/illustrations/no-messages.tsx +51 -0
  59. package/src/components/ds-illustration/illustrations/no-search-result.tsx +76 -0
  60. package/src/components/ds-illustration/illustrations/no-tasks.tsx +61 -0
  61. package/src/components/ds-illustration/illustrations/site.tsx +80 -0
  62. package/src/components/ds-illustration/index.ts +3 -0
  63. package/src/components/ds-main-menu/components/expanded-item/expanded-item.tsx +108 -0
  64. package/src/components/ds-main-menu/components/expanded-item/expanded-item.types.ts +8 -0
  65. package/src/components/ds-main-menu/components/expanded-item/index.ts +1 -0
  66. package/src/components/ds-main-menu/components/menu-graphic/index.ts +1 -0
  67. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.tsx +16 -0
  68. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.types.ts +7 -0
  69. package/src/components/ds-main-menu/components/tile-link/index.ts +1 -0
  70. package/src/components/ds-main-menu/components/tile-link/tile-link.tsx +85 -0
  71. package/src/components/ds-main-menu/components/tile-link/tile-link.types.ts +8 -0
  72. package/src/components/ds-main-menu/components/utility-link/index.ts +1 -0
  73. package/src/components/ds-main-menu/components/utility-link/utility-link.tsx +37 -0
  74. package/src/components/ds-main-menu/components/utility-link/utility-link.types.ts +7 -0
  75. package/src/components/ds-main-menu/ds-main-menu.figma.ts +17 -0
  76. package/src/components/ds-main-menu/ds-main-menu.module.scss +349 -0
  77. package/src/components/ds-main-menu/ds-main-menu.tsx +102 -0
  78. package/src/components/ds-main-menu/ds-main-menu.types.ts +86 -0
  79. package/src/components/ds-main-menu/ds-main-menu.utils.ts +40 -0
  80. package/src/components/ds-main-menu/index.ts +9 -0
  81. package/src/components/ds-modal/figma/ds-modal-footer.figma.ts +1 -1
  82. package/src/components/ds-popover/ds-popover.tsx +2 -1
  83. package/src/components/ds-popover/ds-popover.types.ts +7 -0
  84. package/src/components/ds-site-node/ds-site-node.tsx +2 -2
  85. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +12 -2
  86. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +2 -3
  87. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +1 -6
  88. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +6 -3
  89. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +18 -7
  90. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +27 -21
  91. package/src/components/ds-table/components/ds-table-header-label.module.scss +12 -0
  92. package/src/components/ds-table/components/ds-table-header-label.tsx +28 -0
  93. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +0 -1
  94. package/src/components/ds-table/context/ds-table-context.tsx +8 -0
  95. package/src/components/ds-table/ds-table.module.scss +26 -16
  96. package/src/components/ds-table/ds-table.tsx +46 -14
  97. package/src/components/ds-table/ds-table.types.ts +33 -3
  98. package/src/components/ds-table/hooks/use-column-resize.ts +114 -11
  99. package/src/components/ds-table/hooks/use-scrollbar-spacer.ts +108 -0
  100. package/src/components/ds-table/hooks/use-scrollbar-spacer.unit.test.ts +26 -0
  101. package/src/components/ds-table/styles/_variables.scss +0 -2
  102. package/src/components/ds-table/utils/column-size.ts +274 -1
  103. package/src/components/ds-table/utils/column-size.unit.test.ts +157 -0
  104. package/src/components/ds-table/utils/constants.ts +13 -5
  105. package/src/components/ds-table/utils/table-columns.tsx +27 -13
  106. package/src/components/ds-table/utils/table-columns.unit.test.ts +44 -0
  107. package/src/components/ds-toggle/ds-toggle.module.scss +18 -0
  108. package/src/components/ds-toggle/ds-toggle.tsx +15 -13
  109. package/src/components/ds-tooltip/ds-tooltip.tsx +6 -2
  110. package/src/components/ds-tooltip/ds-tooltip.types.ts +17 -0
  111. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.figma.ts +35 -0
  112. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.module.scss +150 -0
  113. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.tsx +161 -0
  114. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.types.ts +117 -0
  115. package/src/components/ds-top-bar-navigation/index.ts +2 -0
  116. package/src/index.ts +6 -0
  117. package/src/utils/use-is-horizontally-overflowing.ts +35 -0
  118. package/src/components/ds-site-node/hooks/use-is-overflowing.ts +0 -31
  119. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +0 -112
  120. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.tsx +0 -67
  121. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.types.ts +0 -41
  122. package/src/components/ds-table/components/ds-table-bulk-actions/index.ts +0 -2
@@ -13,8 +13,13 @@ import {
13
13
  getResizableColumnStyle,
14
14
  getResizableHeaderStyle,
15
15
  getResizeOriginSize,
16
+ getUtilityColumnSizing,
17
+ growFillLeavesToContainer,
16
18
  isExplicitColumnWidth,
17
19
  omitBuiltinColumnSizing,
20
+ resolveUtilityColumnWidth,
21
+ shiftColumnTrack,
22
+ withUtilityColumnSizing,
18
23
  } from './column-size';
19
24
  import {
20
25
  EXPANDER_COLUMN_ID,
@@ -179,6 +184,115 @@ describe('collectMissingLeafSizing', () => {
179
184
  it('returns an empty map when every measured id already has a sizing entry', () => {
180
185
  expect(collectMissingLeafSizing({ a: 100 }, { a: 90 })).toEqual({});
181
186
  });
187
+
188
+ it('reconciles independent rounding so seeded widths match the measured total', () => {
189
+ const next = collectMissingLeafSizing({ a: 500.6, b: 500.6 }, {});
190
+
191
+ expect(next).toEqual({ a: 501, b: 500 });
192
+ });
193
+
194
+ it('absorbs rounding drift into missing ids and leaves existing widths unchanged', () => {
195
+ expect(collectMissingLeafSizing({ a: 500.6, b: 500.6 }, { b: 501 }, 1001)).toEqual({ a: 500 });
196
+ });
197
+
198
+ it('does not snap seeded widths to the container when measured columns overflow it', () => {
199
+ expect(collectMissingLeafSizing({ a: 1000, b: 1000 }, {}, 1116)).toEqual({ a: 1000, b: 1000 });
200
+ });
201
+
202
+ it('snaps a 1px measured overflow to the container width', () => {
203
+ const next = collectMissingLeafSizing({ a: 558.5, b: 558.5 }, {}, 1116);
204
+ const total = Object.values(next).reduce((sum, width) => sum + width, 0);
205
+
206
+ expect(total).toBe(1116);
207
+ });
208
+
209
+ it('distributes container underflow across fill leaves by their measured share', () => {
210
+ const next = collectMissingLeafSizing({ a: 200, b: 200 }, {}, 1200, ['a', 'b']);
211
+
212
+ expect(next).toEqual({ a: 600, b: 600 });
213
+ });
214
+
215
+ it('grows only fill leaves when the container is wider than the measured total', () => {
216
+ const next = collectMissingLeafSizing({ a: 200, b: 200, c: 200 }, {}, 1200, ['b', 'c']);
217
+
218
+ expect(next).toEqual({ a: 200, b: 500, c: 500 });
219
+ });
220
+ });
221
+
222
+ describe('growFillLeavesToContainer', () => {
223
+ it('moves leftover space onto uncapped fill leaves after a sibling hits maxSize', () => {
224
+ const next = growFillLeavesToContainer(
225
+ { a: 280, b: 360, c: 360 },
226
+ {},
227
+ ['a', 'b', 'c'],
228
+ 1200,
229
+ ['a', 'b', 'c'],
230
+ [
231
+ { id: 'a', columnDef: { maxSize: 280 } },
232
+ { id: 'b', columnDef: {} },
233
+ { id: 'c', columnDef: {} },
234
+ ],
235
+ );
236
+
237
+ expect(next.a).toBe(280);
238
+ expect(next.b).toBeGreaterThan(360);
239
+ expect(next.c).toBeGreaterThan(360);
240
+ expect((next.a ?? 0) + (next.b ?? 0) + (next.c ?? 0)).toBe(1200);
241
+ });
242
+
243
+ it('does not grow an explicit leaf when filling leftover space', () => {
244
+ const next = growFillLeavesToContainer(
245
+ { a: 280, b: 360, c: 360 },
246
+ {},
247
+ ['a', 'b', 'c'],
248
+ 1200,
249
+ ['b', 'c'],
250
+ [
251
+ { id: 'a', columnDef: { maxSize: 280 } },
252
+ { id: 'b', columnDef: {} },
253
+ { id: 'c', columnDef: {} },
254
+ ],
255
+ );
256
+
257
+ expect(next).toEqual({ a: 280, b: 460, c: 460 });
258
+ });
259
+ });
260
+
261
+ describe('shiftColumnTrack', () => {
262
+ const bounds = [
263
+ { id: 'a', columnDef: {} },
264
+ { id: 'b', columnDef: {} },
265
+ ];
266
+
267
+ it('returns the same map when delta is 0', () => {
268
+ const sizing = { a: 200, b: 200 };
269
+
270
+ expect(shiftColumnTrack(sizing, ['a', 'b'], 0, bounds)).toBe(sizing);
271
+ });
272
+
273
+ it('shrinks from the last leaf when the spacer appears', () => {
274
+ const next = shiftColumnTrack({ a: 500, b: 500 }, ['a', 'b'], -10, bounds);
275
+
276
+ expect(next.a).toBe(500);
277
+ expect(next.b).toBe(490);
278
+ expect((next.a ?? 0) + (next.b ?? 0)).toBe(990);
279
+ });
280
+
281
+ it('grows leaves when the spacer disappears', () => {
282
+ const next = shiftColumnTrack({ a: 500, b: 490 }, ['a', 'b'], 10, bounds);
283
+
284
+ expect((next.a ?? 0) + (next.b ?? 0)).toBe(1000);
285
+ });
286
+
287
+ it('does not shrink a leaf below minSize', () => {
288
+ const next = shiftColumnTrack({ a: 200, b: 52 }, ['a', 'b'], -10, [
289
+ { id: 'a', columnDef: {} },
290
+ { id: 'b', columnDef: { minSize: 52 } },
291
+ ]);
292
+
293
+ expect(next.b).toBe(52);
294
+ expect(next.a).toBe(190);
295
+ });
182
296
  });
183
297
 
184
298
  describe('omitBuiltinColumnSizing', () => {
@@ -211,3 +325,46 @@ describe('omitBuiltinColumnSizing', () => {
211
325
  expect(next).not.toBe(sizing);
212
326
  });
213
327
  });
328
+
329
+ describe('resolveUtilityColumnWidth', () => {
330
+ it('returns the fallback when the width is omitted, non-finite, or not positive', () => {
331
+ expect(resolveUtilityColumnWidth(undefined, 36)).toBe(36);
332
+ expect(resolveUtilityColumnWidth(0, 36)).toBe(36);
333
+ expect(resolveUtilityColumnWidth(-8, 36)).toBe(36);
334
+ expect(resolveUtilityColumnWidth(Number.NaN, 36)).toBe(36);
335
+ expect(resolveUtilityColumnWidth(Number.POSITIVE_INFINITY, 36)).toBe(36);
336
+ });
337
+
338
+ it('returns a positive finite width as-is', () => {
339
+ expect(resolveUtilityColumnWidth(48, 36)).toBe(48);
340
+ expect(resolveUtilityColumnWidth(1, 36)).toBe(1);
341
+ });
342
+ });
343
+
344
+ describe('withUtilityColumnSizing', () => {
345
+ it('strips inbound utility ids and overlays the authored widths', () => {
346
+ expect(
347
+ withUtilityColumnSizing(
348
+ { [SELECT_COLUMN_ID]: 80, firstName: 200 },
349
+ { [SELECT_COLUMN_ID]: 48, [EXPANDER_COLUMN_ID]: 40 },
350
+ ),
351
+ ).toEqual({ firstName: 200, [SELECT_COLUMN_ID]: 48, [EXPANDER_COLUMN_ID]: 40 });
352
+ });
353
+ });
354
+
355
+ describe('getUtilityColumnSizing', () => {
356
+ it('collects size entries for injected utility leaves only', () => {
357
+ expect(
358
+ getUtilityColumnSizing([
359
+ { id: REORDER_COLUMN_ID, size: 80 },
360
+ { id: EXPANDER_COLUMN_ID, size: 40 },
361
+ { id: SELECT_COLUMN_ID, size: 48 },
362
+ { id: 'firstName', size: 200 },
363
+ ]),
364
+ ).toEqual({
365
+ [REORDER_COLUMN_ID]: 80,
366
+ [EXPANDER_COLUMN_ID]: 40,
367
+ [SELECT_COLUMN_ID]: 48,
368
+ });
369
+ });
370
+ });
@@ -1,8 +1,3 @@
1
- /**
2
- * Default text for empty table state
3
- */
4
- export const EMPTY_TABLE_STATE_TEXT = 'No results.';
5
-
6
1
  /**
7
2
  * Column id used for the synthetic expander column injected when `expandable` is set.
8
3
  */
@@ -55,6 +50,19 @@ export const SKELETON_ROW_COUNT = 5;
55
50
  */
56
51
  export const RESIZE_MIN_COLUMN_WIDTH = 52;
57
52
 
53
+ /**
54
+ * Pixel width of the full-height resize overlay divider. Must match
55
+ * `$resize-divider-width` in `styles/_variables.scss`.
56
+ */
57
+ export const RESIZE_DIVIDER_WIDTH = 2;
58
+
59
+ /**
60
+ * Rest width of the header **Scrollbar spacer** when the body overflows
61
+ * vertically. Must match `$scrollbar-default-size` in `styles/_scrollbars.scss`.
62
+ * Hover (12px) is intentionally not tracked.
63
+ */
64
+ export const SCROLLBAR_SPACER_WIDTH = 10;
65
+
58
66
  /**
59
67
  * Width change (in px) applied per Arrow key press when a resize handle is
60
68
  * focused.
@@ -3,6 +3,7 @@ import { DsSkeletonText } from '../../ds-skeleton';
3
3
  import { DsTableHeaderSelectableCell } from '../components/ds-table-header-selectable-cell';
4
4
  import { DsTableRowExpandableCell } from '../components/ds-table-row-expandable-cell';
5
5
  import { DsTableRowSelectableCell } from '../components/ds-table-row-selectable-cell';
6
+ import { resolveUtilityColumnWidth } from './column-size';
6
7
  import {
7
8
  EXPANDER_COLUMN_ID,
8
9
  EXPANDER_COLUMN_WIDTH,
@@ -19,6 +20,9 @@ export type GetAugmentedColumnsOptions = {
19
20
  reorderable: boolean;
20
21
  virtualized: boolean;
21
22
  showSelectAllCheckbox: boolean;
23
+ selectableColumnWidth?: number;
24
+ expandableColumnWidth?: number;
25
+ reorderableColumnWidth?: number;
22
26
  };
23
27
 
24
28
  const EXPLICIT_SIZE_META = { hasExplicitSize: true } as const;
@@ -41,6 +45,13 @@ const stampExplicitSize = <TData, TValue>(columns: ColumnDef<TData, TValue>[]):
41
45
  return { ...column, meta: { ...column.meta, ...EXPLICIT_SIZE_META } };
42
46
  });
43
47
 
48
+ /** Pin min/max so the table-wide resize `minSize` cannot stretch this column. */
49
+ const pinUtilityColumnSize = (width: number) => ({
50
+ size: width,
51
+ minSize: width,
52
+ maxSize: width,
53
+ });
54
+
44
55
  /**
45
56
  * Prepends the builtin select / expander / reorder columns when those features
46
57
  * are on. Reorder is omitted for virtualized tables. Unshift order yields
@@ -48,19 +59,24 @@ const stampExplicitSize = <TData, TValue>(columns: ColumnDef<TData, TValue>[]):
48
59
  */
49
60
  export const getAugmentedColumns = <TData, TValue>(
50
61
  columns: ColumnDef<TData, TValue>[],
51
- { selectable, expandable, reorderable, virtualized, showSelectAllCheckbox }: GetAugmentedColumnsOptions,
62
+ {
63
+ selectable,
64
+ expandable,
65
+ reorderable,
66
+ virtualized,
67
+ showSelectAllCheckbox,
68
+ selectableColumnWidth,
69
+ expandableColumnWidth,
70
+ reorderableColumnWidth,
71
+ }: GetAugmentedColumnsOptions,
52
72
  ): ColumnDef<TData, TValue>[] => {
53
73
  const augmentedColumns: ColumnDef<TData, TValue>[] = stampExplicitSize(columns);
54
74
 
55
75
  if (selectable) {
76
+ const width = resolveUtilityColumnWidth(selectableColumnWidth, SELECT_COLUMN_WIDTH);
56
77
  augmentedColumns.unshift({
57
78
  id: SELECT_COLUMN_ID,
58
- size: SELECT_COLUMN_WIDTH,
59
- // Pin min/max to the fixed width: `enableResizing: false` blocks the
60
- // handle but not the table-wide resize `minSize`, which would otherwise
61
- // stretch this column.
62
- minSize: SELECT_COLUMN_WIDTH,
63
- maxSize: SELECT_COLUMN_WIDTH,
79
+ ...pinUtilityColumnSize(width),
64
80
  enableSorting: false,
65
81
  enableResizing: false,
66
82
  meta: EXPLICIT_SIZE_META,
@@ -70,11 +86,10 @@ export const getAugmentedColumns = <TData, TValue>(
70
86
  }
71
87
 
72
88
  if (expandable) {
89
+ const width = resolveUtilityColumnWidth(expandableColumnWidth, EXPANDER_COLUMN_WIDTH);
73
90
  augmentedColumns.unshift({
74
91
  id: EXPANDER_COLUMN_ID,
75
- size: EXPANDER_COLUMN_WIDTH,
76
- minSize: EXPANDER_COLUMN_WIDTH,
77
- maxSize: EXPANDER_COLUMN_WIDTH,
92
+ ...pinUtilityColumnSize(width),
78
93
  enableSorting: false,
79
94
  enableResizing: false,
80
95
  meta: EXPLICIT_SIZE_META,
@@ -84,13 +99,12 @@ export const getAugmentedColumns = <TData, TValue>(
84
99
  }
85
100
 
86
101
  if (reorderable && !virtualized) {
102
+ const width = resolveUtilityColumnWidth(reorderableColumnWidth, REORDER_COLUMN_WIDTH);
87
103
  // Cell is rendered inline by DsTableRow when it encounters REORDER_COLUMN_ID,
88
104
  // since the drag handle needs row-level useSortable state.
89
105
  augmentedColumns.unshift({
90
106
  id: REORDER_COLUMN_ID,
91
- size: REORDER_COLUMN_WIDTH,
92
- minSize: REORDER_COLUMN_WIDTH,
93
- maxSize: REORDER_COLUMN_WIDTH,
107
+ ...pinUtilityColumnSize(width),
94
108
  enableSorting: false,
95
109
  enableResizing: false,
96
110
  meta: EXPLICIT_SIZE_META,
@@ -100,6 +100,50 @@ describe('getAugmentedColumns', () => {
100
100
  }
101
101
  });
102
102
 
103
+ it('pins custom utility column widths and ignores them when the feature is off', () => {
104
+ const columns = getAugmentedColumns([nameColumn], {
105
+ ...noFeatures,
106
+ selectable: true,
107
+ expandable: true,
108
+ reorderable: true,
109
+ selectableColumnWidth: 48,
110
+ expandableColumnWidth: 40,
111
+ reorderableColumnWidth: 80,
112
+ });
113
+ const boundsById = new Map(columns.map((column) => [column.id, column]));
114
+
115
+ expect(boundsById.get(SELECT_COLUMN_ID)?.size).toBe(48);
116
+ expect(boundsById.get(SELECT_COLUMN_ID)?.minSize).toBe(48);
117
+ expect(boundsById.get(SELECT_COLUMN_ID)?.maxSize).toBe(48);
118
+ expect(boundsById.get(EXPANDER_COLUMN_ID)?.size).toBe(40);
119
+ expect(boundsById.get(EXPANDER_COLUMN_ID)?.minSize).toBe(40);
120
+ expect(boundsById.get(EXPANDER_COLUMN_ID)?.maxSize).toBe(40);
121
+ expect(boundsById.get(REORDER_COLUMN_ID)?.size).toBe(80);
122
+ expect(boundsById.get(REORDER_COLUMN_ID)?.minSize).toBe(80);
123
+ expect(boundsById.get(REORDER_COLUMN_ID)?.maxSize).toBe(80);
124
+
125
+ expect(
126
+ columnIds(getAugmentedColumns([nameColumn], { ...noFeatures, selectableColumnWidth: 48 })),
127
+ ).toEqual(['name']);
128
+ });
129
+
130
+ it('falls back to the default width when the custom width is invalid', () => {
131
+ const columns = getAugmentedColumns([nameColumn], {
132
+ ...noFeatures,
133
+ selectable: true,
134
+ expandable: true,
135
+ reorderable: true,
136
+ selectableColumnWidth: 0,
137
+ expandableColumnWidth: Number.NaN,
138
+ reorderableColumnWidth: -10,
139
+ });
140
+ const boundsById = new Map(columns.map((column) => [column.id, column]));
141
+
142
+ expect(boundsById.get(SELECT_COLUMN_ID)?.size).toBe(SELECT_COLUMN_WIDTH);
143
+ expect(boundsById.get(EXPANDER_COLUMN_ID)?.size).toBe(EXPANDER_COLUMN_WIDTH);
144
+ expect(boundsById.get(REORDER_COLUMN_ID)?.size).toBe(REORDER_COLUMN_WIDTH);
145
+ });
146
+
103
147
  it('stamps hasExplicitSize on authored leaves and builtins, not on unsized leaves', () => {
104
148
  const columns = getAugmentedColumns(
105
149
  [
@@ -144,6 +144,24 @@
144
144
  pointer-events: none;
145
145
  }
146
146
 
147
+ // Overlay the native input over the control so `getByRole('checkbox')` clicks
148
+ // land on a real interactive surface. Ark applies `visuallyHiddenStyle` inline
149
+ // (1x1, clipped, off-screen) which Playwright refuses to click; `!important`
150
+ // is required to beat inline-style specificity.
151
+ .hiddenInput {
152
+ position: absolute !important;
153
+ inset-block-start: 0 !important;
154
+ inset-inline-start: 0 !important;
155
+ width: calc(var(--switch-track-width) + var(--thumb-gap) * 2) !important;
156
+ height: calc(var(--thumb-size) + var(--thumb-gap) * 2) !important;
157
+ margin: 0 !important;
158
+ padding: 0 !important;
159
+ clip: auto !important;
160
+ overflow: visible !important;
161
+ opacity: 0 !important;
162
+ cursor: pointer !important;
163
+ }
164
+
147
165
  .iconCheck {
148
166
  left: var(--icon-spacing);
149
167
  }
@@ -58,19 +58,21 @@ const DsToggle = ({
58
58
  <DsIcon icon="close" size={iconSize} className={closeIconClass} />
59
59
  </Switch.Control>
60
60
 
61
- {label ? (
62
- <Switch.Label>
63
- <DsTypography variant={isSmall ? 'body-xs-md' : 'body-sm-md'}>{label}</DsTypography>
64
- {!!labelInfo && (
65
- <DsTypography variant="body-xs-reg" className={styles.labelInfo}>
66
- {labelInfo}
67
- </DsTypography>
68
- )}
69
- </Switch.Label>
70
- ) : (
71
- children
72
- )}
73
- <Switch.HiddenInput />
61
+ <Switch.Label>
62
+ {label ? (
63
+ <>
64
+ <DsTypography variant={isSmall ? 'body-xs-md' : 'body-sm-md'}>{label}</DsTypography>
65
+ {!!labelInfo && (
66
+ <DsTypography variant="body-xs-reg" className={styles.labelInfo}>
67
+ {labelInfo}
68
+ </DsTypography>
69
+ )}
70
+ </>
71
+ ) : (
72
+ children
73
+ )}
74
+ </Switch.Label>
75
+ <Switch.HiddenInput className={styles.hiddenInput} />
74
76
  </Switch.Root>
75
77
  );
76
78
  };
@@ -14,6 +14,9 @@ const DsTooltip: FC<DsTooltipProps> = ({
14
14
  children,
15
15
  placement = 'top',
16
16
  disabled = false,
17
+ interactive = false,
18
+ openDelay = OPEN_DELAY_MS,
19
+ closeDelay = CLOSE_DELAY_MS,
17
20
  getAnchorRect,
18
21
  slotProps,
19
22
  }) => {
@@ -23,8 +26,9 @@ const DsTooltip: FC<DsTooltipProps> = ({
23
26
 
24
27
  return (
25
28
  <Tooltip.Root
26
- openDelay={OPEN_DELAY_MS}
27
- closeDelay={CLOSE_DELAY_MS}
29
+ interactive={interactive}
30
+ openDelay={openDelay}
31
+ closeDelay={closeDelay}
28
32
  disabled={disabled}
29
33
  positioning={{ placement, gutter: TOOLTIP_GUTTER_PX, getAnchorRect: getAnchorRect ?? undefined }}
30
34
  lazyMount
@@ -44,6 +44,23 @@ export interface DsTooltipProps {
44
44
  * When true the trigger renders but the tooltip never opens.
45
45
  */
46
46
  disabled?: boolean;
47
+ /**
48
+ * When true, content is pointer-reachable and the tooltip stays open while the
49
+ * pointer is over it. Pair with a non-zero `closeDelay` (e.g. 150) so the pointer
50
+ * can travel from the trigger onto the content; `0` can lose that handoff.
51
+ * @default false
52
+ */
53
+ interactive?: boolean;
54
+ /**
55
+ * Milliseconds after pointer enter or focus before the tooltip opens.
56
+ * @default 200
57
+ */
58
+ openDelay?: number;
59
+ /**
60
+ * Milliseconds after pointer leave before the tooltip closes.
61
+ * @default 0
62
+ */
63
+ closeDelay?: number;
47
64
  /**
48
65
  * Anchors the tooltip to a virtual rect instead of the trigger, re-evaluated on
49
66
  * each reposition. Return `null` to fall back to the trigger.
@@ -0,0 +1,35 @@
1
+ // url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=9543-40942
2
+ // source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-top-bar-navigation
3
+ // component=DsTopBarNavigation
4
+ //
5
+ // `DsTopBarNavigationV1` maps to the `DsTopBarNavigation` compound component. Figma models the
6
+ // bar as a fixed 920px left frame with an absolutely-positioned breadcrumb; in code the zones
7
+ // are flex (`.Logo` fixed-basis, `.Breadcrumb` shrinkable, `.Center` reserved, `.Actions`
8
+ // pinned right), so none of that geometry maps to props.
9
+ //
10
+ // The brand glyph and wordmark are consumer slots — no brand asset ships with the design
11
+ // system — so they are emitted as placeholders. Figma's `DsBotButtonV1` maps to `DsBotButton`,
12
+ // which is connected separately in `ds-bot-button/ds-bot-button.figma.ts`.
13
+ import figma from 'figma';
14
+
15
+ export default {
16
+ example: figma.code`<DsTopBarNavigation>
17
+ <DsTopBarNavigation.Logo logo={<img src="/brand-mark.svg" alt="" />} wordmark="DAP" href="/" onAppsClick={() => {}} />
18
+ <DsTopBarNavigation.Breadcrumb>
19
+ <DsBreadcrumb items={[]} />
20
+ </DsTopBarNavigation.Breadcrumb>
21
+ <DsTopBarNavigation.Center />
22
+ <DsTopBarNavigation.Actions>
23
+ <DsTopBarNavigation.Notifications count={13} onClick={() => {}} />
24
+ <DsBotButton />
25
+ <DsTopBarNavigation.UserMenu name="Full Name">
26
+ <DsDropdownMenu.Item value="profile">Profile</DsDropdownMenu.Item>
27
+ </DsTopBarNavigation.UserMenu>
28
+ </DsTopBarNavigation.Actions>
29
+ </DsTopBarNavigation>`,
30
+ imports: [
31
+ "import { DsTopBarNavigation, DsBotButton, DsBreadcrumb, DsDropdownMenu } from '@drivenets/design-system';",
32
+ ],
33
+ id: 'ds-top-bar-navigation',
34
+ metadata: { nestable: false },
35
+ } satisfies figma.Template;
@@ -0,0 +1,150 @@
1
+ $focus-outline-width: 2px;
2
+ $disabled-avatar-opacity: 0.55;
3
+
4
+ .root {
5
+ --ds-top-bar-navigation-height: 54px;
6
+ --ds-top-bar-navigation-logo-min-width: 174px;
7
+ --ds-top-bar-navigation-logo-glyph-size: 32px;
8
+ --ds-top-bar-navigation-wordmark-font-size: 24px;
9
+ --ds-top-bar-navigation-breadcrumb-offset: var(--lg);
10
+ --ds-top-bar-navigation-zone-gap: var(--lg);
11
+ --ds-top-bar-navigation-badge-size: 16px;
12
+ --ds-top-bar-navigation-caret-size: 20px;
13
+
14
+ box-sizing: border-box;
15
+ display: flex;
16
+ align-items: center;
17
+ flex-shrink: 0;
18
+ height: var(--ds-top-bar-navigation-height);
19
+ padding-right: var(--md);
20
+ border-bottom: 1px solid var(--border);
21
+ background: var(--background);
22
+ }
23
+
24
+ .logo {
25
+ box-sizing: border-box;
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ flex-shrink: 0;
30
+ gap: var(--sm);
31
+ align-self: stretch;
32
+ min-width: var(--ds-top-bar-navigation-logo-min-width);
33
+ padding: 0 var(--lg);
34
+ background: var(--background-primary);
35
+ color: var(--font-on-action);
36
+ }
37
+
38
+ .logoLockup {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: var(--xs);
42
+ color: inherit;
43
+ text-decoration: none;
44
+ }
45
+
46
+ .logoGlyph {
47
+ display: flex;
48
+ flex-shrink: 0;
49
+ width: var(--ds-top-bar-navigation-logo-glyph-size);
50
+ height: var(--ds-top-bar-navigation-logo-glyph-size);
51
+ }
52
+
53
+ .logoWordmark {
54
+ font-family: var(--font-family-heading);
55
+ font-size: var(--ds-top-bar-navigation-wordmark-font-size);
56
+ font-weight: var(--font-weight-bold);
57
+ white-space: nowrap;
58
+ }
59
+
60
+ .breadcrumb {
61
+ display: flex;
62
+ align-items: center;
63
+ min-width: 0;
64
+ margin-left: var(--ds-top-bar-navigation-breadcrumb-offset);
65
+ overflow: hidden;
66
+ white-space: nowrap;
67
+ }
68
+
69
+ .center {
70
+ display: flex;
71
+ align-items: center;
72
+ flex: 1;
73
+ min-width: 0;
74
+ }
75
+
76
+ .actions {
77
+ display: flex;
78
+ align-items: center;
79
+ flex-shrink: 0;
80
+ gap: var(--standard);
81
+ margin-inline-start: auto;
82
+ padding-inline-start: var(--ds-top-bar-navigation-zone-gap);
83
+ }
84
+
85
+ .notificationsBadge {
86
+ box-sizing: border-box;
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ min-width: var(--ds-top-bar-navigation-badge-size);
91
+ height: var(--ds-top-bar-navigation-badge-size);
92
+ padding: 0 var(--3xs);
93
+ border-radius: 9999px;
94
+ background: var(--background-error);
95
+ color: var(--font-on-action);
96
+ font-size: var(--body-font-size-xs);
97
+ font-weight: var(--font-weight-medium);
98
+ line-height: var(--body-line-height-xs);
99
+
100
+ &[data-empty] {
101
+ display: none;
102
+ }
103
+ }
104
+
105
+ .userMenuTrigger {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: var(--3xs);
109
+ padding: 0;
110
+ border: none;
111
+ border-radius: var(--3xs);
112
+ background: transparent;
113
+ color: var(--font-main);
114
+ cursor: pointer;
115
+
116
+ // Nested for the extra class of specificity: `DsAvatar` paints its own `background-color`
117
+ // and `color` from a single-class rule, so a standalone `.userMenuAvatar` ties with it and
118
+ // bundle order picks the winner.
119
+ .userMenuAvatar {
120
+ background-color: var(--light-buttons-tertiary);
121
+ color: var(--font-main);
122
+ }
123
+
124
+ &:focus {
125
+ outline: none;
126
+ }
127
+
128
+ &:focus-visible {
129
+ outline: $focus-outline-width solid var(--border-action-primary);
130
+ outline-offset: $focus-outline-width;
131
+ }
132
+
133
+ &:disabled {
134
+ color: var(--font-disabled);
135
+ cursor: not-allowed;
136
+
137
+ // The caret follows `color`; the avatar sets its own fill and text color above, so
138
+ // neither inherits the disabled tone — dim the whole lockup instead.
139
+ .userMenuAvatar {
140
+ opacity: $disabled-avatar-opacity;
141
+ }
142
+ }
143
+ }
144
+
145
+ .userMenuCaret {
146
+ display: flex;
147
+ flex-shrink: 0;
148
+ width: var(--ds-top-bar-navigation-caret-size);
149
+ height: var(--ds-top-bar-navigation-caret-size);
150
+ }