@drivenets/design-system 0.18.0 → 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 (126) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/index.cjs +15850 -10821
  3. package/dist/index.d.cts +785 -239
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +785 -239
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +15835 -10822
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +13 -13
  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-tag-filter/ds-tag-filter.module.scss +7 -35
  108. package/src/components/ds-tag-filter/ds-tag-filter.tsx +34 -57
  109. package/src/components/ds-tag-filter/ds-tag-filter.types.ts +2 -1
  110. package/src/components/ds-tag-filter/hooks/use-tag-overflow-calculation.ts +13 -18
  111. package/src/components/ds-toggle/ds-toggle.module.scss +18 -0
  112. package/src/components/ds-toggle/ds-toggle.tsx +15 -13
  113. package/src/components/ds-tooltip/ds-tooltip.tsx +6 -2
  114. package/src/components/ds-tooltip/ds-tooltip.types.ts +17 -0
  115. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.figma.ts +35 -0
  116. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.module.scss +150 -0
  117. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.tsx +161 -0
  118. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.types.ts +117 -0
  119. package/src/components/ds-top-bar-navigation/index.ts +2 -0
  120. package/src/index.ts +6 -0
  121. package/src/utils/use-is-horizontally-overflowing.ts +35 -0
  122. package/src/components/ds-site-node/hooks/use-is-overflowing.ts +0 -31
  123. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +0 -112
  124. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.tsx +0 -67
  125. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.types.ts +0 -41
  126. package/src/components/ds-table/components/ds-table-bulk-actions/index.ts +0 -2
@@ -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
+ }
@@ -0,0 +1,161 @@
1
+ import classNames from 'classnames';
2
+ import styles from './ds-top-bar-navigation.module.scss';
3
+ import { DsAvatar } from '../ds-avatar';
4
+ import { DsButtonV3 } from '../ds-button-v3';
5
+ import { DsDropdownMenu } from '../ds-dropdown-menu';
6
+ import { DsIcon } from '../ds-icon';
7
+ import {
8
+ notificationCountCap,
9
+ type DsTopBarNavigationActionsProps,
10
+ type DsTopBarNavigationBreadcrumbProps,
11
+ type DsTopBarNavigationCenterProps,
12
+ type DsTopBarNavigationLogoProps,
13
+ type DsTopBarNavigationNotificationsProps,
14
+ type DsTopBarNavigationProps,
15
+ type DsTopBarNavigationUserMenuProps,
16
+ } from './ds-top-bar-navigation.types';
17
+
18
+ const DEFAULT_LOCALE = Object.freeze({
19
+ appsButtonLabel: 'Apps',
20
+ notificationsButtonLabel: 'Notifications',
21
+ notificationsButtonLabelWithCount: (count: number) => `Notifications, ${String(count)} unread`,
22
+ userMenuButtonLabel: (name: string) => `Account menu for ${name}`,
23
+ });
24
+
25
+ const DEFAULT_USER_MENU_POSITIONING = Object.freeze({ placement: 'bottom-end' } as const);
26
+
27
+ const DsTopBarNavigationRoot = ({ className, ...rest }: DsTopBarNavigationProps) => (
28
+ <div {...rest} className={classNames(styles.root, className)} />
29
+ );
30
+
31
+ const Logo = ({
32
+ className,
33
+ logo,
34
+ wordmark,
35
+ href,
36
+ locale,
37
+ onLogoClick,
38
+ onAppsClick,
39
+ appsMenu,
40
+ ...rest
41
+ }: DsTopBarNavigationLogoProps) => {
42
+ const lockup = (
43
+ <>
44
+ {logo ? <span className={styles.logoGlyph}>{logo}</span> : null}
45
+ {wordmark ? <span className={styles.logoWordmark}>{wordmark}</span> : null}
46
+ </>
47
+ );
48
+
49
+ return (
50
+ <div {...rest} className={classNames(styles.logo, className)}>
51
+ {href ? (
52
+ <a className={styles.logoLockup} href={href} aria-label={locale?.homeLinkLabel} onClick={onLogoClick}>
53
+ {lockup}
54
+ </a>
55
+ ) : (
56
+ <span className={styles.logoLockup}>{lockup}</span>
57
+ )}
58
+
59
+ {appsMenu}
60
+
61
+ {!appsMenu && onAppsClick ? (
62
+ <DsButtonV3
63
+ variant="tertiary"
64
+ color="light"
65
+ size="small"
66
+ icon="apps"
67
+ aria-label={locale?.appsButtonLabel ?? DEFAULT_LOCALE.appsButtonLabel}
68
+ onClick={onAppsClick}
69
+ />
70
+ ) : null}
71
+ </div>
72
+ );
73
+ };
74
+
75
+ const Breadcrumb = ({ className, ...rest }: DsTopBarNavigationBreadcrumbProps) => (
76
+ <div {...rest} className={classNames(styles.breadcrumb, className)} />
77
+ );
78
+
79
+ /** Reserved zone between the breadcrumb trail and the actions. Empty by design today. */
80
+ const Center = ({ className, ...rest }: DsTopBarNavigationCenterProps) => (
81
+ <div {...rest} className={classNames(styles.center, className)} />
82
+ );
83
+
84
+ const Actions = ({ className, ...rest }: DsTopBarNavigationActionsProps) => (
85
+ <div {...rest} className={classNames(styles.actions, className)} />
86
+ );
87
+
88
+ const Notifications = ({ count = 0, locale, ...rest }: DsTopBarNavigationNotificationsProps) => {
89
+ const hasCount = count > 0;
90
+ const label = hasCount
91
+ ? (locale?.notificationsButtonLabelWithCount ?? DEFAULT_LOCALE.notificationsButtonLabelWithCount)(count)
92
+ : (locale?.notificationsButtonLabel ?? DEFAULT_LOCALE.notificationsButtonLabel);
93
+
94
+ return (
95
+ <DsButtonV3 {...rest} variant="tertiary" size="small" icon="notifications_active" aria-label={label}>
96
+ {/*
97
+ Always rendered so DsButtonV3 never switches to its icon-only geometry,
98
+ which would re-pad the bell when the count clears.
99
+ */}
100
+ <span className={styles.notificationsBadge} aria-hidden data-empty={hasCount ? undefined : ''}>
101
+ {count > notificationCountCap ? `${String(notificationCountCap)}+` : count}
102
+ </span>
103
+ </DsButtonV3>
104
+ );
105
+ };
106
+
107
+ const UserMenu = ({
108
+ className,
109
+ name,
110
+ src,
111
+ children,
112
+ open,
113
+ positioning,
114
+ locale,
115
+ onOpenChange,
116
+ onSelect,
117
+ ...rest
118
+ }: DsTopBarNavigationUserMenuProps) => (
119
+ <DsDropdownMenu.Root
120
+ open={open}
121
+ // Merged, not replaced: a caller who only overrides `gutter` keeps `bottom-end`, which is
122
+ // what stops the menu from spilling past the right edge of the bar.
123
+ positioning={{ ...DEFAULT_USER_MENU_POSITIONING, ...positioning }}
124
+ onOpenChange={onOpenChange}
125
+ onSelect={onSelect}
126
+ >
127
+ <DsDropdownMenu.Trigger asChild>
128
+ <button
129
+ {...rest}
130
+ type="button"
131
+ className={classNames(styles.userMenuTrigger, className)}
132
+ aria-label={(locale?.userMenuButtonLabel ?? DEFAULT_LOCALE.userMenuButtonLabel)(name)}
133
+ >
134
+ <DsAvatar className={styles.userMenuAvatar} name={name} src={src} size="regular" type="circle" />
135
+ <span className={styles.userMenuCaret}>
136
+ <DsIcon icon="arrow_drop_down" size="small" aria-hidden />
137
+ </span>
138
+ </button>
139
+ </DsDropdownMenu.Trigger>
140
+ <DsDropdownMenu.Content>{children}</DsDropdownMenu.Content>
141
+ </DsDropdownMenu.Root>
142
+ );
143
+
144
+ DsTopBarNavigationRoot.displayName = 'DsTopBarNavigation';
145
+ Logo.displayName = 'DsTopBarNavigation.Logo';
146
+ Breadcrumb.displayName = 'DsTopBarNavigation.Breadcrumb';
147
+ Center.displayName = 'DsTopBarNavigation.Center';
148
+ Actions.displayName = 'DsTopBarNavigation.Actions';
149
+ Notifications.displayName = 'DsTopBarNavigation.Notifications';
150
+ UserMenu.displayName = 'DsTopBarNavigation.UserMenu';
151
+
152
+ const DsTopBarNavigation = Object.assign(DsTopBarNavigationRoot, {
153
+ Logo,
154
+ Breadcrumb,
155
+ Center,
156
+ Actions,
157
+ Notifications,
158
+ UserMenu,
159
+ });
160
+
161
+ export default DsTopBarNavigation;
@@ -0,0 +1,117 @@
1
+ import type { ComponentPropsWithRef, MouseEvent, ReactNode } from 'react';
2
+ import type { DsDropdownMenuPositioning } from '../ds-dropdown-menu';
3
+
4
+ /** Count above which the notification badge collapses to `<cap>+`. */
5
+ export const notificationCountCap = 99;
6
+
7
+ export type DsTopBarNavigationProps = ComponentPropsWithRef<'div'>;
8
+
9
+ export interface DsTopBarNavigationLogoProps extends Omit<ComponentPropsWithRef<'div'>, 'children'> {
10
+ /**
11
+ * Brand glyph rendered at 32px before the wordmark. Kept as a slot, so the bar stays
12
+ * tenant-neutral — no brand asset ships with the design system.
13
+ */
14
+ logo?: ReactNode;
15
+ /**
16
+ * Brand wordmark rendered beside the glyph. Styled with the heading font; pass a node
17
+ * when the mark needs custom kerning.
18
+ */
19
+ wordmark?: ReactNode;
20
+ /**
21
+ * Turns the glyph and wordmark into a link to the platform home.
22
+ */
23
+ href?: string;
24
+ locale?: {
25
+ /**
26
+ * Accessible name for the logo link. Only applied when `href` is set; useful when the
27
+ * wordmark is an image rather than text.
28
+ */
29
+ homeLinkLabel?: string;
30
+ /**
31
+ * @default 'Apps'
32
+ */
33
+ appsButtonLabel?: string;
34
+ };
35
+ /**
36
+ * Fires on the logo link itself, not the surrounding zone. Call `preventDefault` here to
37
+ * hand navigation to a client-side router instead of letting the browser reload the document.
38
+ */
39
+ onLogoClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
40
+ /**
41
+ * Renders the app-switcher button. Omit to hide the button entirely. Ignored when
42
+ * `appsMenu` is set.
43
+ */
44
+ onAppsClick?: () => void;
45
+ /**
46
+ * Takes over the app-switcher slot with your own control — pass a `DsMainMenu` to open the
47
+ * app grid from the brand zone. The node is rendered as-is, so it owns its own trigger,
48
+ * open state, and positioning.
49
+ */
50
+ appsMenu?: ReactNode;
51
+ }
52
+
53
+ export type DsTopBarNavigationBreadcrumbProps = ComponentPropsWithRef<'div'>;
54
+
55
+ export type DsTopBarNavigationCenterProps = ComponentPropsWithRef<'div'>;
56
+
57
+ export type DsTopBarNavigationActionsProps = ComponentPropsWithRef<'div'>;
58
+
59
+ export interface DsTopBarNavigationNotificationsProps extends Omit<
60
+ ComponentPropsWithRef<'button'>,
61
+ 'children' | 'aria-label' | 'color'
62
+ > {
63
+ /**
64
+ * Unread count. `0` or omitted renders the bell with no badge; anything above
65
+ * `notificationCountCap` collapses to a capped label.
66
+ */
67
+ count?: number;
68
+ locale?: {
69
+ /**
70
+ * Accessible name when there are no unread notifications.
71
+ * @default 'Notifications'
72
+ */
73
+ notificationsButtonLabel?: string;
74
+ /**
75
+ * Accessible name when unread notifications exist. Receives the uncapped count.
76
+ * @default (count) => `Notifications, ${count} unread`
77
+ */
78
+ notificationsButtonLabelWithCount?: (count: number) => string;
79
+ };
80
+ }
81
+
82
+ export interface DsTopBarNavigationUserMenuProps extends Omit<
83
+ ComponentPropsWithRef<'button'>,
84
+ 'children' | 'aria-label' | 'onSelect'
85
+ > {
86
+ /**
87
+ * Drives the avatar initials and its accessible name.
88
+ */
89
+ name: string;
90
+ /**
91
+ * Avatar photo URL. Omit, or let it fail to load, to fall back to initials.
92
+ */
93
+ src?: string;
94
+ /**
95
+ * Menu entries — `DsDropdownMenu.Item`, `.Separator`, `.ItemGroup` and friends.
96
+ */
97
+ children?: ReactNode;
98
+ /**
99
+ * Open state for controlled usage.
100
+ */
101
+ open?: boolean;
102
+ /**
103
+ * Placement and offset of the menu. Defaults to `bottom-end` so the menu stays inside the
104
+ * viewport when the trigger sits against the right edge of the bar.
105
+ */
106
+ positioning?: DsDropdownMenuPositioning;
107
+ locale?: {
108
+ /**
109
+ * Accessible name for the trigger. Receives `name`; the default distinguishes the
110
+ * control from the avatar's own name tooltip.
111
+ * @default (name) => `Account menu for ${name}`
112
+ */
113
+ userMenuButtonLabel?: (name: string) => string;
114
+ };
115
+ onOpenChange?: (open: boolean) => void;
116
+ onSelect?: (value: string) => void;
117
+ }
@@ -0,0 +1,2 @@
1
+ export { default as DsTopBarNavigation } from './ds-top-bar-navigation';
2
+ export * from './ds-top-bar-navigation.types';
package/src/index.ts CHANGED
@@ -2,7 +2,9 @@ export * from './components/ds-alert-banner';
2
2
  export * from './components/ds-autocomplete';
3
3
  export * from './components/ds-avatar';
4
4
  export * from './components/ds-avatar-group';
5
+ export * from './components/ds-bot-button';
5
6
  export * from './components/ds-breadcrumb';
7
+ export * from './components/ds-bulk-actions';
6
8
  export * from './components/ds-button';
7
9
  export * from './components/ds-button-v3';
8
10
  export * from './components/ds-card';
@@ -23,14 +25,17 @@ export * from './components/ds-dialog';
23
25
  export * from './components/ds-divider';
24
26
  export * from './components/ds-drawer';
25
27
  export * from './components/ds-dropdown-menu';
28
+ export * from './components/ds-empty-state';
26
29
  export * from './components/ds-expandable-text-input';
27
30
  export * from './components/ds-file-upload';
28
31
  export * from './components/ds-filter-status-icon';
29
32
  export * from './components/ds-form-control';
30
33
  export * from './components/ds-grid';
31
34
  export * from './components/ds-icon';
35
+ export * from './components/ds-illustration';
32
36
  export * from './components/ds-key-value-pair';
33
37
  export * from './components/ds-loader';
38
+ export * from './components/ds-main-menu';
34
39
  export * from './components/ds-modal';
35
40
  export * from './components/ds-number-input';
36
41
  export * from './components/ds-panel';
@@ -64,6 +69,7 @@ export * from './components/ds-time-picker';
64
69
  export * from './components/ds-toast';
65
70
  export * from './components/ds-toggle';
66
71
  export * from './components/ds-tooltip';
72
+ export * from './components/ds-top-bar-navigation';
67
73
  export * from './components/ds-tree';
68
74
  export * from './components/ds-typography';
69
75
  export * from './components/ds-user-card';
@@ -0,0 +1,35 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const useIsHorizontallyOverflowing = <T extends HTMLElement = HTMLElement>(content?: string) => {
4
+ const [element, setElement] = useState<T | null>(null);
5
+ const [measuredOverflow, setMeasuredOverflow] = useState(false);
6
+
7
+ useEffect(() => {
8
+ if (!element || !content) {
9
+ return;
10
+ }
11
+
12
+ const measure = () => {
13
+ setMeasuredOverflow(element.scrollWidth > element.clientWidth);
14
+ };
15
+
16
+ measure();
17
+
18
+ const resizeObserver = new ResizeObserver(measure);
19
+ resizeObserver.observe(element);
20
+
21
+ // document.fonts is missing in jsdom; `'fonts' in document` (not optional chaining) satisfies TS.
22
+ const fonts = 'fonts' in document ? document.fonts : undefined;
23
+ fonts?.addEventListener('loadingdone', measure);
24
+
25
+ return () => {
26
+ resizeObserver.disconnect();
27
+ fonts?.removeEventListener('loadingdone', measure);
28
+ };
29
+ }, [content, element]);
30
+
31
+ return {
32
+ ref: setElement,
33
+ isOverflowing: Boolean(content) && measuredOverflow,
34
+ };
35
+ };
@@ -1,31 +0,0 @@
1
- import { useLayoutEffect, useRef, useState } from 'react';
2
-
3
- export const useIsOverflowing = <T extends HTMLElement = HTMLElement>(text: string | undefined) => {
4
- const ref = useRef<T>(null);
5
- const [isOverflowing, setIsOverflowing] = useState(false);
6
-
7
- useLayoutEffect(() => {
8
- const element = ref.current;
9
- if (!element) {
10
- return;
11
- }
12
-
13
- const measure = () => setIsOverflowing(element.scrollWidth > element.clientWidth);
14
- measure();
15
-
16
- const observer = new ResizeObserver(measure);
17
- observer.observe(element);
18
-
19
- const frame = requestAnimationFrame(measure);
20
- document.fonts.ready.then(measure).catch(() => undefined);
21
- document.fonts.addEventListener('loadingdone', measure);
22
-
23
- return () => {
24
- cancelAnimationFrame(frame);
25
- observer.disconnect();
26
- document.fonts.removeEventListener('loadingdone', measure);
27
- };
28
- }, [text]);
29
-
30
- return { ref, isOverflowing };
31
- };