@camunda/camunda-composite-components 0.23.3 → 0.24.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 (40) hide show
  1. package/lib/esm/package.json +26 -26
  2. package/lib/esm/src/components/c3-data-table/c3-data-table.js +1 -1
  3. package/lib/esm/src/components/c3-help-center/c3-help-center-provider.d.ts +2 -1
  4. package/lib/esm/src/components/c3-help-center/c3-help-center-provider.js +4 -1
  5. package/lib/esm/src/components/c3-help-center/c3-help-center.d.ts +2 -1
  6. package/lib/esm/src/components/c3-help-center/c3-help-center.js +4 -3
  7. package/lib/esm/src/components/c3-license-tag/c3-license-tag.d.ts +4 -5
  8. package/lib/esm/src/components/c3-license-tag/c3-license-tag.js +54 -47
  9. package/lib/esm/src/components/c3-navigation/c3-navigation-appbar/c3-navigation-appbar.js +6 -6
  10. package/lib/esm/src/components/c3-navigation/c3-navigation-sidebar/c3-navigation-sidebar-element.js +3 -0
  11. package/lib/esm/src/components/c3-navigation/c3-navigation-sidebar/c3-navigation-sidebar.js +10 -1
  12. package/lib/esm/src/components/c3-navigation/helpers.js +12 -0
  13. package/lib/esm/src/components/c3-navigation-v2/c3-breadcrumb-bar.js +33 -31
  14. package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.js +7 -1
  15. package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.types.d.ts +18 -0
  16. package/lib/esm/src/components/c3-navigation-v2/c3-sidebar.d.ts +1 -1
  17. package/lib/esm/src/components/c3-navigation-v2/c3-sidebar.js +82 -84
  18. package/lib/esm/src/components/c3-navigation-v2/c3-tools-area.js +18 -5
  19. package/lib/esm/src/components/c3-navigation-v2/index.d.ts +5 -3
  20. package/lib/esm/src/components/c3-navigation-v2/index.js +1 -0
  21. package/lib/esm/src/components/c3-navigation-v2/stories/story-templates.d.ts +1 -0
  22. package/lib/esm/src/components/c3-navigation-v2/stories/story-templates.js +112 -0
  23. package/lib/esm/src/components/c3-navigation-v2/tools/c3-info-panel.d.ts +2 -1
  24. package/lib/esm/src/components/c3-navigation-v2/tools/c3-info-panel.js +1 -1
  25. package/lib/esm/src/components/c3-navigation-v2/tools/c3-notifications-panel.d.ts +11 -0
  26. package/lib/esm/src/components/c3-navigation-v2/tools/c3-notifications-panel.js +9 -5
  27. package/lib/esm/src/components/c3-navigation-v2/tools/c3-theme-selector.d.ts +25 -0
  28. package/lib/esm/src/components/c3-navigation-v2/tools/c3-theme-selector.js +15 -0
  29. package/lib/esm/src/components/c3-navigation-v2/tools/c3-user-panel.d.ts +15 -0
  30. package/lib/esm/src/components/c3-navigation-v2/tools/c3-user-panel.js +10 -17
  31. package/lib/esm/src/components/c3-navigation-v2/use-c3-navigation-v2.d.ts +3 -1
  32. package/lib/esm/src/components/c3-navigation-v2/use-c3-navigation-v2.js +2 -1
  33. package/lib/esm/src/components/c3-navigation-v2/use-camunda-tools.d.ts +28 -5
  34. package/lib/esm/src/components/c3-navigation-v2/use-camunda-tools.js +42 -23
  35. package/lib/esm/src/components/c3-navigation-v2/use-cluster-sidebar-entries.js +10 -8
  36. package/lib/esm/src/components/c3-navigation-v2/use-cluster-webapp-breadcrumbs.d.ts +16 -18
  37. package/lib/esm/src/components/c3-navigation-v2/use-cluster-webapp-breadcrumbs.js +146 -36
  38. package/lib/esm/src/index.d.ts +2 -2
  39. package/lib/esm/src/index.js +1 -1
  40. package/package.json +27 -27
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/camunda-composite-components",
3
- "version": "0.23.3",
3
+ "version": "0.24.0",
4
4
  "description": "Camunda Composite Components",
5
5
  "bugs": {
6
6
  "url": "https://github.com/camunda/camunda-cloud-management-apps/issues"
@@ -52,41 +52,41 @@
52
52
  "jwt-decode": "4.0.0",
53
53
  "react-error-boundary": "6.1.1",
54
54
  "react-markdown": "10.1.0",
55
- "semver": "7.7.4"
55
+ "semver": "7.8.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@auth0/auth0-spa-js": "2.19.0",
58
+ "@auth0/auth0-spa-js": "2.19.3",
59
59
  "@camunda/ccma-shared-types": "workspace:*",
60
- "@carbon/react": "1.103.0",
61
- "@chromatic-com/storybook": "5.1.1",
60
+ "@carbon/react": "1.107.1",
61
+ "@chromatic-com/storybook": "5.2.1",
62
62
  "@mdx-js/react": "3.1.1",
63
- "@playwright/test": "1.59.1",
64
- "@storybook/addon-a11y": "10.3.5",
65
- "@storybook/addon-docs": "10.3.5",
66
- "@storybook/addon-links": "10.3.5",
67
- "@storybook/addon-vitest": "10.3.5",
68
- "@storybook/react": "10.3.5",
69
- "@storybook/react-vite": "10.3.5",
70
- "@vitest/browser": "4.1.4",
71
- "@vitest/browser-playwright": "4.1.4",
72
- "vitest": "4.1.4",
63
+ "@playwright/test": "1.60.0",
64
+ "@storybook/addon-a11y": "10.4.0",
65
+ "@storybook/addon-docs": "10.4.0",
66
+ "@storybook/addon-links": "10.4.0",
67
+ "@storybook/addon-vitest": "10.4.0",
68
+ "@storybook/react": "10.4.0",
69
+ "@storybook/react-vite": "10.4.0",
70
+ "@vitest/browser": "4.1.7",
71
+ "@vitest/browser-playwright": "4.1.7",
72
+ "vitest": "4.1.7",
73
73
  "conventional-changelog-conventionalcommits": "9.3.1",
74
74
  "eslint-import-resolver-typescript": "4.4.4",
75
75
  "eslint-plugin-react": "7.37.5",
76
- "eslint-plugin-react-hooks": "7.0.1",
77
- "eslint-plugin-storybook": "10.3.5",
76
+ "eslint-plugin-react-hooks": "7.1.1",
77
+ "eslint-plugin-storybook": "10.4.0",
78
78
  "event-source-polyfill": "1.0.31",
79
- "mixpanel-browser": "2.78.0",
80
- "playwright": "1.59.1",
81
- "react": "19.2.5",
82
- "react-dom": "19.2.5",
83
- "react-is": "19.2.5",
79
+ "mixpanel-browser": "2.79.0",
80
+ "playwright": "1.60.0",
81
+ "react": "19.2.6",
82
+ "react-dom": "19.2.6",
83
+ "react-is": "19.2.6",
84
84
  "rimraf": "6.1.3",
85
85
  "serve": "14.2.6",
86
- "storybook": "10.3.5",
87
- "styled-components": "6.4.0",
88
- "typescript-eslint": "8.58.1",
89
- "wait-on": "9.0.5"
86
+ "storybook": "10.4.0",
87
+ "styled-components": "6.4.2",
88
+ "typescript-eslint": "8.59.4",
89
+ "wait-on": "9.0.10"
90
90
  },
91
91
  "peerDependencies": {
92
92
  "@carbon/react": "1.x",
@@ -371,7 +371,7 @@ export const C3DataTable = ({ data, headers, options, toolbar: singleToolbar, to
371
371
  }
372
372
  }
373
373
  }
374
- if (toolbarComponents.length > 0 || !!searchComponent) {
374
+ if (toolbarComponents.length > 0 || searchComponent) {
375
375
  toolbarComponent = (_jsx(TableToolbar, { "aria-label": 'data table toolbar', children: _jsxs(ToolbarWrapper, { style: {
376
376
  justifyContent: toolbarComponents.length > 5 ? 'normal' : 'end',
377
377
  flexWrap: toolbarComponents.length > 5 ? 'wrap' : undefined,
@@ -1,4 +1,4 @@
1
- import React, { type FC, type PropsWithChildren } from 'react';
1
+ import React, { type FC, type PropsWithChildren, type RefObject } from 'react';
2
2
  export declare enum HelpCenterHintType {
3
3
  HelpCenter = "help-center",
4
4
  Onboarding = "onboarding"
@@ -13,6 +13,7 @@ export type C3HelpCenterContextValue = {
13
13
  setShowHintOnClose: (showHintOnClose: boolean) => void;
14
14
  hintType: HelpCenterHintType;
15
15
  setHintType: (hintType: HelpCenterHintType) => void;
16
+ launcherButtonRef: RefObject<HTMLButtonElement | null>;
16
17
  };
17
18
  export declare const C3HelpCenterContext: React.Context<C3HelpCenterContextValue>;
18
19
  export declare const C3HelpCenterProvider: FC<PropsWithChildren>;
@@ -4,7 +4,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  * under one or more contributor license agreements. Licensed under a commercial license.
5
5
  * You may not use this file except in compliance with the commercial license.
6
6
  */
7
- import React, { useEffect, useState, } from 'react';
7
+ import React, { useEffect, useRef, useState, } from 'react';
8
8
  export var HelpCenterHintType;
9
9
  (function (HelpCenterHintType) {
10
10
  HelpCenterHintType["HelpCenter"] = "help-center";
@@ -19,6 +19,7 @@ export const C3HelpCenterContext = React.createContext({
19
19
  setShowHintOnClose: () => undefined,
20
20
  hintType: HelpCenterHintType.Onboarding,
21
21
  setHintType: () => undefined,
22
+ launcherButtonRef: { current: null },
22
23
  });
23
24
  export const C3HelpCenterProvider = ({ children }) => {
24
25
  const [isHelpCenterOpen, setIsHelpCenterOpen] = useState(false);
@@ -26,6 +27,7 @@ export const C3HelpCenterProvider = ({ children }) => {
26
27
  const [showHint, setShowHint] = useState(false);
27
28
  const [showHintOnClose, setShowHintOnClose] = useState(false);
28
29
  const [hintType, setHintType] = useState(HelpCenterHintType.Onboarding);
30
+ const launcherButtonRef = useRef(null);
29
31
  const openHelpCenter = (showTabId) => {
30
32
  if (!isHelpCenterOpen) {
31
33
  setIsHelpCenterOpen(true);
@@ -53,6 +55,7 @@ export const C3HelpCenterProvider = ({ children }) => {
53
55
  setShowHintOnClose,
54
56
  hintType,
55
57
  setHintType,
58
+ launcherButtonRef,
56
59
  }, children: children }));
57
60
  };
58
61
  export const useC3HelpCenter = () => React.useContext(C3HelpCenterContext);
@@ -1,5 +1,5 @@
1
1
  import type { Dict } from 'mixpanel-browser';
2
- import { type FC } from 'react';
2
+ import React, { type FC } from 'react';
3
3
  import { type Theme } from '../c3-user-configuration/c3-profile-provider/c3-profile-provider';
4
4
  import type { Persona } from './c3-help-center.types';
5
5
  export interface C3HelpCenterProps {
@@ -12,5 +12,6 @@ export interface C3HelpCenterProps {
12
12
  onRequestClose?: () => void;
13
13
  onRequestOpen?: () => void;
14
14
  mixpanelTrack?: (event: string, data: Dict | undefined) => void;
15
+ launcherButtonRef?: React.RefObject<HTMLButtonElement | null>;
15
16
  }
16
17
  export declare const C3HelpCenter: FC<C3HelpCenterProps>;
@@ -21,8 +21,9 @@ const StyledComposedModal = styled(ComposedModal) `
21
21
  mask-image: none;
22
22
  }
23
23
  `;
24
- export const C3HelpCenter = ({ autoStartSurvey, origin, flags, onRequestClose, mixpanelTrack: customMixpanelTrack, onRequestOpen, theme, onPersonaChange, activeTab, }) => {
25
- const { isHelpCenterOpen: isOpen, setIsHelpCenterOpen, setShowHintOnClose, } = useC3HelpCenter();
24
+ export const C3HelpCenter = ({ autoStartSurvey, origin, flags, onRequestClose, mixpanelTrack: customMixpanelTrack, onRequestOpen, theme, onPersonaChange, activeTab, launcherButtonRef, }) => {
25
+ const { isHelpCenterOpen: isOpen, setIsHelpCenterOpen, setShowHintOnClose, launcherButtonRef: contextLauncherButtonRef, } = useC3HelpCenter();
26
+ const effectiveLauncherButtonRef = launcherButtonRef ?? contextLauncherButtonRef;
26
27
  const { userToken, decodedToken, activeOrganizationId, handleTheme, decodedAudience, analyticsTrack, currentApp, } = useC3UserConfiguration();
27
28
  const { theme: themeConfig, isEnabled, reloadClusters } = useC3Profile();
28
29
  const themeHandlingEnabled = isEnabled && !!handleTheme && !!themeConfig;
@@ -212,7 +213,7 @@ export const C3HelpCenter = ({ autoStartSurvey, origin, flags, onRequestClose, m
212
213
  setShowSurvey(false);
213
214
  }, 400);
214
215
  };
215
- return (_jsx(Layer, { children: _jsx(StyledComposedModal, { open: isOpen, size: 'lg', onClose: closeFn, className: 'help-center', "aria-label": 'HelpCenter', preventCloseOnClickOutside: true, style: showSurvey || !persona?.wasShown
216
+ return (_jsx(Layer, { children: _jsx(StyledComposedModal, { open: isOpen, size: 'lg', onClose: closeFn, launcherButtonRef: effectiveLauncherButtonRef, className: 'help-center', "aria-label": 'HelpCenter', preventCloseOnClickOutside: true, style: showSurvey || !persona?.wasShown
216
217
  ? { backgroundColor: 'rgba(22,22,22, 0.8)' }
217
218
  : {}, children: _jsx(ErrorBoundary, { fallbackRender: () => (_jsxs(_Fragment, { children: [_jsx(ModalHeader, { title: 'Help Center', closeModal: closeFn }), _jsx(ModalBody, { children: _jsx(ActionableNotification, { inline: true, hideCloseButton: true, lowContrast: true, kind: 'error', title: 'Something went wrong.', subtitle: 'Try reloading the page.', actionButtonLabel: 'Reload', onActionButtonClick: () => window.location.reload() }) })] })), children: showSurvey || !persona?.wasShown ? (_jsx(C3OnboardingSurvey, { personaCallback: personaCallback, persona: persona, mixpanelTrack: mixpanelTrack, onRequestClose: closeFn, onRequestSkip: onRequestSkipSurvey, theme: resolvedTheme, origin: hostApp, modal: true })) : (_jsx(HelpCenter, { configuration: helpCenterConfig, persona: persona, audience: decodedAudience || '', flags: flags, onRequestResumeSurvey: onRequestResumeSurvey, onRequestRetakeSurvey: onRequestRetakeSurvey, onRequestClose: closeFn, mixpanelTrack: mixpanelTrack, email: email, theme: resolvedTheme, origin: hostApp, initialTab: activeTab })) }) }) }));
218
219
  };
@@ -8,10 +8,9 @@ export interface C3LicenseTagProps {
8
8
  expiresAt?: number | string;
9
9
  }
10
10
  /**
11
- * Renders a license status tag for the Camunda header. Handles all license
12
- * states: production, non-production (with tooltip), non-commercial,
13
- * non-commercial expiring (with countdown), and non-commercial expired.
14
- *
15
- * Drop into `headerTrailingContent` of C3NavigationV2.
11
+ * Renders license status tags for the Camunda header. Mirrors V1 c3-navigation:
12
+ * always renders a production-status tag, plus a non-commercial tag (or its
13
+ * expiring/expired variant) when isCommercial === false. Drop the element into
14
+ * `headerTrailingContent` of C3NavigationV2.
16
15
  */
17
16
  export declare const C3LicenseTag: (props: C3LicenseTagProps) => JSX.Element | null;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /*
3
3
  * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
4
4
  * under one or more contributor license agreements. Licensed under a commercial license.
@@ -17,63 +17,70 @@ function resolveExpiresAt(value) {
17
17
  return Date.parse(value);
18
18
  return value;
19
19
  }
20
- function getTagVariant(props) {
20
+ // Matches V1 c3-navigation: production-status tag is always shown, and a
21
+ // non-commercial tag is shown additionally when isCommercial === false. They
22
+ // stack rather than override each other.
23
+ function getTagVariants(props) {
21
24
  const { isProductionLicense, isCommercial, expiresAt: rawExpiresAt } = props;
22
25
  const expiresAt = resolveExpiresAt(rawExpiresAt);
23
26
  const now = Date.now();
24
- // Non-commercial expired
25
- if (isCommercial === false && expiresAt !== undefined && expiresAt < now) {
26
- return {
27
- label: 'Non-commercial license - expired',
28
- color: 'red',
29
- icon: Warning,
30
- tooltip: (_jsx("p", { children: "To continue using all features, please renew your license." })),
31
- };
32
- }
33
- // Non-commercial about to expire (within 30 days)
34
- if (isCommercial === false &&
35
- expiresAt !== undefined &&
36
- expiresAt - LICENSE_EXPIRY_THRESHOLD < now &&
37
- expiresAt > now) {
38
- const daysLeft = Math.floor((expiresAt - now) / DAY);
39
- return {
40
- label: `Non-commercial license - ${daysLeft} ${daysLeft > 1 ? 'days' : 'day'} left`,
41
- color: 'blue',
42
- icon: Time,
43
- tooltip: (_jsx("p", { children: "Please renew and provide new license keys to continue production use of Camunda." })),
44
- };
45
- }
46
- // Non-commercial (not expiring soon or no expiry)
47
- if (isCommercial === false &&
48
- (expiresAt === undefined || expiresAt - LICENSE_EXPIRY_THRESHOLD >= now)) {
49
- return {
50
- label: 'Non-commercial license',
51
- color: 'gray',
52
- };
53
- }
54
- // Production or non-production license
55
- return {
27
+ const variants = [];
28
+ variants.push({
29
+ key: 'license-tag',
56
30
  label: isProductionLicense
57
31
  ? 'Production license'
58
32
  : 'Non-production license',
59
33
  color: 'gray',
60
34
  tooltip: isProductionLicense ? undefined : (_jsxs("p", { children: ["Non-production license. For production usage details, visit our", ' ', _jsx(Link, { href: NON_PRODUCTION_TERMS_LINK, target: '_blank', rel: 'noopener noreferrer', style: { display: 'inline' }, children: "terms & conditions page" }), ' ', "or", ' ', _jsx(Link, { href: SALES_CONTACT_LINK, target: '_blank', rel: 'noopener noreferrer', style: { display: 'inline' }, children: "contact our sales team" }), "."] })),
61
- };
35
+ });
36
+ if (isCommercial === false) {
37
+ if (expiresAt !== undefined && expiresAt < now) {
38
+ variants.push({
39
+ key: 'non-commercial-license-tag-is-expired',
40
+ label: 'Non-commercial license - expired',
41
+ color: 'red',
42
+ icon: Warning,
43
+ tooltip: (_jsx("p", { children: "To continue using all features, please renew your license." })),
44
+ });
45
+ }
46
+ else if (expiresAt !== undefined &&
47
+ expiresAt - LICENSE_EXPIRY_THRESHOLD < now &&
48
+ expiresAt > now) {
49
+ const daysLeft = Math.floor((expiresAt - now) / DAY);
50
+ variants.push({
51
+ key: 'non-commercial-license-tag-about-to-expire',
52
+ label: `Non-commercial license - ${daysLeft} ${daysLeft > 1 ? 'days' : 'day'} left`,
53
+ color: 'blue',
54
+ icon: Time,
55
+ tooltip: (_jsx("p", { children: "Please renew and provide new license keys to continue production use of Camunda." })),
56
+ });
57
+ }
58
+ else {
59
+ variants.push({
60
+ key: 'non-commercial-license-tag',
61
+ label: 'Non-commercial license',
62
+ color: 'gray',
63
+ });
64
+ }
65
+ }
66
+ return variants;
67
+ }
68
+ function renderVariant(variant) {
69
+ const { key, label, color, icon, tooltip } = variant;
70
+ if (tooltip) {
71
+ return (_jsxs(Toggletip, { align: 'bottom', children: [_jsx(ToggletipButton, { as: 'span', label: label, children: _jsx(Tag, { type: color, renderIcon: icon, style: { margin: 0, cursor: 'pointer' }, children: label }) }), _jsx(ToggletipContent, { children: tooltip })] }, key));
72
+ }
73
+ return (_jsx(Tag, { type: color, renderIcon: icon, style: { margin: 0 }, children: label }, key));
62
74
  }
63
75
  /**
64
- * Renders a license status tag for the Camunda header. Handles all license
65
- * states: production, non-production (with tooltip), non-commercial,
66
- * non-commercial expiring (with countdown), and non-commercial expired.
67
- *
68
- * Drop into `headerTrailingContent` of C3NavigationV2.
76
+ * Renders license status tags for the Camunda header. Mirrors V1 c3-navigation:
77
+ * always renders a production-status tag, plus a non-commercial tag (or its
78
+ * expiring/expired variant) when isCommercial === false. Drop the element into
79
+ * `headerTrailingContent` of C3NavigationV2.
69
80
  */
70
81
  export const C3LicenseTag = (props) => {
71
- const variant = getTagVariant(props);
72
- if (!variant)
82
+ const variants = getTagVariants(props);
83
+ if (variants.length === 0)
73
84
  return null;
74
- const { label, color, icon, tooltip } = variant;
75
- if (tooltip) {
76
- return (_jsxs(Toggletip, { align: 'bottom', children: [_jsx(ToggletipButton, { as: 'span', label: label, children: _jsx(Tag, { type: color, renderIcon: icon, style: { margin: 0, cursor: 'pointer' }, children: label }) }), _jsx(ToggletipContent, { children: tooltip })] }));
77
- }
78
- return (_jsx(Tag, { type: color, renderIcon: icon, style: { margin: 0 }, children: label }));
85
+ return _jsx(_Fragment, { children: variants.map(renderVariant) });
79
86
  };
@@ -138,18 +138,18 @@ export const C3NavigationAppBar = ({ app: appProps, appBar, forwardRef, navbar,
138
138
  const clusterDto = data.payload;
139
139
  const status = clusterDto.status;
140
140
  const endpoints = {
141
- tasklist: status?.tasklistUrl,
142
- operate: status?.operateUrl,
143
- optimize: status?.optimizeUrl,
141
+ tasklist: status?.tasklistUrl ?? '',
142
+ operate: status?.operateUrl ?? '',
143
+ optimize: status?.optimizeUrl ?? '',
144
144
  console: '',
145
145
  modeler: '',
146
146
  identity: status?.identityUrl ?? '',
147
147
  admin: status?.identityUrl ?? '',
148
148
  };
149
149
  const expectedStatus = {
150
- tasklist: status?.tasklistStatus,
151
- operate: status?.operateStatus,
152
- optimize: status?.optimizeStatus,
150
+ tasklist: status?.tasklistStatus ?? '',
151
+ operate: status?.operateStatus ?? '',
152
+ optimize: status?.optimizeStatus ?? '',
153
153
  console: '',
154
154
  modeler: '',
155
155
  identity: '',
@@ -39,6 +39,9 @@ const C3NavigationSidebarElement = (props) => {
39
39
  setIsOverflown(element ? element.offsetWidth < element.scrollWidth : false);
40
40
  };
41
41
  useEffect(() => {
42
+ if (typeof window === 'undefined') {
43
+ return;
44
+ }
42
45
  window.addEventListener('resize', handleSetIsOverflown);
43
46
  handleSetIsOverflown();
44
47
  return () => window.removeEventListener('resize', handleSetIsOverflown);
@@ -60,16 +60,25 @@ const C3NavigationSideBar = (props) => {
60
60
  setScrollBarWidth(scrollbarWidth);
61
61
  };
62
62
  useEffect(() => {
63
+ if (typeof window === 'undefined') {
64
+ return;
65
+ }
63
66
  window.addEventListener('resize', handleResize);
64
67
  handleResize();
65
68
  return () => window.removeEventListener('resize', handleResize);
66
69
  }, []);
67
70
  useEffect(() => {
71
+ let timeoutId;
68
72
  if (isOpen) {
69
- setTimeout(() => {
73
+ timeoutId = setTimeout(() => {
70
74
  handleResize();
71
75
  }, 120);
72
76
  }
77
+ return () => {
78
+ if (timeoutId !== undefined) {
79
+ clearTimeout(timeoutId);
80
+ }
81
+ };
73
82
  }, [isOpen]);
74
83
  return (_jsxs(Wrapper, { children: [_jsx(HeaderGlobalAction, { ref: setIconRef, "aria-label": sideBar.tooltip ?? `Open ${sideBar.ariaLabel}`, "aria-expanded": isOpen, "aria-controls": id, onClick: () => {
75
84
  setIsOpen(!isOpen);
@@ -40,6 +40,18 @@ export function useOnClickOutside(handler) {
40
40
  return { panelRef, setPanelRef, iconRef, setIconRef };
41
41
  }
42
42
  export function executeMediaQuery(mediaQuery) {
43
+ if (typeof window === 'undefined') {
44
+ return {
45
+ matches: false,
46
+ media: mediaQuery,
47
+ onchange: null,
48
+ addEventListener: () => { },
49
+ removeEventListener: () => { },
50
+ addListener: () => { },
51
+ removeListener: () => { },
52
+ dispatchEvent: () => false,
53
+ };
54
+ }
43
55
  return window.matchMedia(mediaQuery);
44
56
  }
45
57
  export function useMediaQuery(mediaQuery) {
@@ -9,16 +9,16 @@ import { Checkmark, ChevronDown, OverflowMenuVertical, } from '@carbon/react/ico
9
9
  import { useCallback, useEffect, useRef, useState, } from 'react';
10
10
  import { createPortal } from 'react-dom';
11
11
  import styled from 'styled-components';
12
+ // `display: contents` so nested flex doesn't trap min-content; label
13
+ // shrinkage propagates to the breadcrumb row directly.
12
14
  const SegmentWrapper = styled.div `
13
- display: flex;
14
- align-items: center;
15
- position: relative;
15
+ display: contents;
16
16
  `;
17
17
  const SegmentButton = styled.button `
18
18
  display: flex;
19
19
  align-items: center;
20
20
  gap: var(--cds-spacing-03);
21
- padding: var(--cds-spacing-02) var(--cds-spacing-03);
21
+ padding: var(--cds-spacing-02) var(--cds-spacing-04);
22
22
  background: transparent;
23
23
  border: none;
24
24
  border-radius: 4px;
@@ -29,6 +29,8 @@ const SegmentButton = styled.button `
29
29
  text-decoration: none;
30
30
  white-space: nowrap;
31
31
  transition: background 0.15s, color 0.15s;
32
+ min-width: 0;
33
+ flex-shrink: ${(p) => (p.$isLast ? 0 : 1)};
32
34
 
33
35
  &:hover {
34
36
  background: ${(p) => (p.$isInteractive ? 'var(--cds-layer-hover)' : 'transparent')};
@@ -41,7 +43,13 @@ const SegmentButton = styled.button `
41
43
  outline-offset: -2px;
42
44
  }
43
45
  `;
44
- const ChevronButton = styled.button `
46
+ const SegmentLabel = styled.span `
47
+ overflow: hidden;
48
+ text-overflow: ellipsis;
49
+ white-space: nowrap;
50
+ min-width: 0;
51
+ `;
52
+ const ControlButton = styled.button `
45
53
  display: flex;
46
54
  align-items: center;
47
55
  justify-content: center;
@@ -52,6 +60,7 @@ const ChevronButton = styled.button `
52
60
  cursor: pointer;
53
61
  color: var(--cds-icon-secondary);
54
62
  transition: background 0.15s;
63
+ flex-shrink: 0;
55
64
 
56
65
  &:hover {
57
66
  background: var(--cds-layer-hover);
@@ -62,32 +71,20 @@ const ChevronButton = styled.button `
62
71
  outline-offset: -2px;
63
72
  }
64
73
 
74
+ ${SegmentButton} + & {
75
+ margin-left: -0.125rem;
76
+ }
77
+ & + & {
78
+ margin-left: 0.375rem;
79
+ }
80
+ `;
81
+ const ChevronButton = styled(ControlButton) `
65
82
  svg {
66
83
  transform: ${(p) => (p.$isOpen ? 'rotate(180deg)' : 'rotate(0deg)')};
67
84
  transition: transform 0.15s;
68
85
  }
69
86
  `;
70
- const ActionMenuButton = styled.button `
71
- display: flex;
72
- align-items: center;
73
- justify-content: center;
74
- padding: var(--cds-spacing-02);
75
- background: ${(p) => (p.$isOpen ? 'var(--cds-layer-01)' : 'transparent')};
76
- border: none;
77
- border-radius: 4px;
78
- cursor: pointer;
79
- color: var(--cds-icon-secondary);
80
- transition: background 0.15s;
81
-
82
- &:hover {
83
- background: var(--cds-layer-hover);
84
- }
85
-
86
- &:focus-visible {
87
- outline: 2px solid var(--cds-focus);
88
- outline-offset: -2px;
89
- }
90
- `;
87
+ const ActionMenuButton = styled(ControlButton) ``;
91
88
  const ActionMenuItem = styled.button `
92
89
  width: 100%;
93
90
  display: flex;
@@ -167,6 +164,7 @@ const Separator = styled.span `
167
164
  color: var(--cds-text-secondary);
168
165
  font-size: 0.875rem;
169
166
  user-select: none;
167
+ flex-shrink: 0;
170
168
 
171
169
  &::after {
172
170
  content: '/';
@@ -312,6 +310,7 @@ const BreadcrumbSegmentComponent = ({ segment, isLast, linkComponent, }) => {
312
310
  const dropdownId = useRef(`dropdown-${Math.random().toString(36).slice(2, 8)}`).current;
313
311
  const { isOpen, close, toggle } = useBreadcrumbDropdown(dropdownId);
314
312
  const wrapperRef = useRef(null);
313
+ const segmentButtonRef = useRef(null);
315
314
  const chevronRef = useRef(null);
316
315
  const dropdownPanelRef = useRef(null);
317
316
  const hasDropdown = segment.dropdownItems && segment.dropdownItems.length > 0;
@@ -319,8 +318,9 @@ const BreadcrumbSegmentComponent = ({ segment, isLast, linkComponent, }) => {
319
318
  const dropdownAriaLabel = segment.dropdownAriaLabel ?? `Switch ${segment.label}`;
320
319
  const [dropdownPos, setDropdownPos] = useState(undefined);
321
320
  const updateDropdownPosition = useCallback(() => {
322
- if (wrapperRef.current) {
323
- const rect = wrapperRef.current.getBoundingClientRect();
321
+ // SegmentWrapper has no box (display: contents); anchor on the button.
322
+ if (segmentButtonRef.current) {
323
+ const rect = segmentButtonRef.current.getBoundingClientRect();
324
324
  setDropdownPos({ top: rect.bottom + 4, left: rect.left });
325
325
  }
326
326
  }, []);
@@ -344,11 +344,11 @@ const BreadcrumbSegmentComponent = ({ segment, isLast, linkComponent, }) => {
344
344
  close();
345
345
  }, [close]);
346
346
  const handleDropdownKeyDown = usePanelKeyboard(dropdownPanelRef, close, chevronRef, isOpen, '[role="option"]');
347
- return (_jsxs(_Fragment, { children: [_jsxs(SegmentWrapper, { ref: wrapperRef, children: [_jsxs(SegmentButton, { as: segment.linkProps?.href !== undefined
347
+ return (_jsxs(_Fragment, { children: [_jsxs(SegmentWrapper, { ref: wrapperRef, children: [_jsxs(SegmentButton, { ref: segmentButtonRef, as: segment.linkProps?.href !== undefined
348
348
  ? (linkComponent ?? 'a')
349
349
  : segment.linkProps
350
350
  ? linkComponent
351
- : undefined, ...(segment.linkProps ?? {}), "$isInteractive": !!(segment.onClick || segment.linkProps), "$isLast": isLast, onClick: segment.onClick, "aria-label": segment.label, children: [Icon && _jsx(Icon, { size: 16, style: { flexShrink: 0 } }), _jsx("span", { children: segment.label }), segment.trailingElement] }), segment.menuElement, segment.actions && segment.actions.length > 0 && (_jsx(ActionsMenu, { actions: segment.actions, ariaLabel: `${segment.label} actions` })), hasDropdown && (_jsx(ChevronButton, { ref: chevronRef, "$isOpen": isOpen, onClick: toggle, onKeyDown: (e) => {
351
+ : undefined, ...(segment.linkProps ?? {}), "$isInteractive": !!(segment.onClick || segment.linkProps), "$isLast": isLast, onClick: segment.onClick, "aria-label": segment.label, title: segment.label, children: [Icon && _jsx(Icon, { size: 16, style: { flexShrink: 0 } }), _jsx(SegmentLabel, { children: segment.label }), segment.trailingElement] }), segment.menuElement, segment.actions && segment.actions.length > 0 && (_jsx(ActionsMenu, { actions: segment.actions, ariaLabel: `${segment.label} actions` })), hasDropdown && (_jsx(ChevronButton, { ref: chevronRef, "$isOpen": isOpen, onClick: toggle, onKeyDown: (e) => {
352
352
  if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
353
353
  if (!isOpen) {
354
354
  e.preventDefault();
@@ -365,7 +365,9 @@ const BreadcrumbBarWrapper = styled.div `
365
365
  align-items: center;
366
366
  flex: 1;
367
367
  min-width: 0;
368
+ height: 100%;
368
369
  overflow-x: auto;
369
- padding-left: var(--cds-spacing-03);
370
+ margin-left: var(--cds-spacing-03);
371
+ margin-right: var(--cds-spacing-03);
370
372
  `;
371
373
  export const C3BreadcrumbBar = ({ segments, linkComponent, }) => (_jsx(BreadcrumbBarWrapper, { role: 'navigation', "aria-label": 'Breadcrumb', children: segments.map((segment, index) => (_jsx(BreadcrumbSegmentComponent, { segment: segment, isLast: index === segments.length - 1, linkComponent: linkComponent }, segment.key))) }));
@@ -16,6 +16,12 @@ const StyledHeader = styled(Header) `
16
16
  border-bottom: 1px solid var(--cds-border-subtle) !important;
17
17
  box-shadow: none !important;
18
18
  z-index: 8001 !important;
19
+
20
+ // Carbon defaults the tools area to flex: 1, which splits the row
21
+ // 50/50 with breadcrumbs. Make it intrinsic instead.
22
+ .cds--header__global {
23
+ flex: 0 0 auto;
24
+ }
19
25
  `;
20
26
  const LogoSection = styled.a `
21
27
  display: flex;
@@ -57,7 +63,7 @@ export const C3NavigationV2 = ({ app, skipToContentTargetId, skipToContentLabel
57
63
  root.style.removeProperty('--c3-sidebar-width');
58
64
  };
59
65
  }, [sidebarWidth]);
60
- return (_jsxs(_Fragment, { children: [_jsxs(StyledHeader, { "aria-label": ariaLabel, children: [_jsx(SkipToContent, { href: `#${skipToContentTargetId}`, children: skipToContentLabel }), _jsxs(LogoSection, { as: app.linkProps ? LinkEl : 'div', ...(app.linkProps ?? {}), children: [_jsx(CamundaLogo, { "aria-label": 'Camunda' }), app.name && _jsx("span", { children: app.name })] }), breadcrumbs && breadcrumbs.length > 0 && (_jsx(C3BreadcrumbBar, { segments: breadcrumbs, linkComponent: LinkEl })), _jsxs(HeaderGlobalBar, { children: [headerTrailingContent && (_jsx("span", { style: {
66
+ return (_jsxs(_Fragment, { children: [_jsxs(StyledHeader, { "aria-label": ariaLabel, children: [_jsx(SkipToContent, { href: `#${skipToContentTargetId}`, children: skipToContentLabel }), _jsx(LogoSection, { as: app.linkProps ? LinkEl : 'div', "aria-label": app.name ? `Camunda ${app.name}` : 'Camunda', ...(app.linkProps ?? {}), children: _jsx(CamundaLogo, {}) }), breadcrumbs && breadcrumbs.length > 0 && (_jsx(C3BreadcrumbBar, { segments: breadcrumbs, linkComponent: LinkEl })), _jsxs(HeaderGlobalBar, { children: [headerTrailingContent && (_jsx("span", { style: {
61
67
  display: 'flex',
62
68
  alignItems: 'center',
63
69
  padding: '0 var(--cds-spacing-03)',
@@ -2,6 +2,10 @@ import type { ReactNode } from 'react';
2
2
  /**
3
3
  * Describes a tool button in the header. If `panel` is provided, clicking the
4
4
  * button toggles a slide-over panel. Without `panel`, the button is standalone.
5
+ *
6
+ * `renderButton` is rendered via JSX (`<tool.renderButton ... />`), so it
7
+ * may call hooks internally — e.g. reading `C3NotificationContext` to drive
8
+ * an unread-state badge.
5
9
  */
6
10
  export interface ToolDescriptor {
7
11
  key: string;
@@ -143,6 +147,19 @@ export interface SidebarSection {
143
147
  compact?: boolean;
144
148
  }
145
149
  export type SidebarNode = SidebarItem | SidebarGroupItem | SidebarGroup | SidebarSection;
150
+ export interface SidebarLabels {
151
+ /** Toggle button text when expanded. Defaults to `'Collapse'`. */
152
+ collapse?: string;
153
+ /** Tooltip on the collapsed toggle button. Defaults to `'Expand'`. */
154
+ expand?: string;
155
+ /** Aria-label on the toggle button. Receives `isExpanded`. Defaults to `'Collapse sidebar'` / `'Expand sidebar'`. */
156
+ toggleAriaLabel?: (isExpanded: boolean) => string;
157
+ /** Aria-label on a group's chevron. Receives `{label, isExpanded}`. Defaults to `'Collapse {label}'` / `'Expand {label}'`. */
158
+ groupToggleAriaLabel?: (args: {
159
+ label: string;
160
+ isExpanded: boolean;
161
+ }) => string;
162
+ }
146
163
  export interface SidebarProps {
147
164
  ariaLabel: string;
148
165
  children: SidebarNode[];
@@ -151,6 +168,7 @@ export interface SidebarProps {
151
168
  expandedWidth?: string;
152
169
  collapsedWidth?: string;
153
170
  linkComponent?: LinkComponent;
171
+ labels?: SidebarLabels;
154
172
  }
155
173
  export interface AppProps {
156
174
  name?: string;
@@ -5,4 +5,4 @@ import type { SidebarProps } from './c3-navigation-v2.types';
5
5
  * Loose items (not wrapped in a section) are valid and behave as an implicit
6
6
  * untitled group at the top.
7
7
  */
8
- export declare const C3Sidebar: ({ ariaLabel, children: nodes, isExpanded, onToggleExpanded, expandedWidth, collapsedWidth, linkComponent, }: SidebarProps) => JSX.Element;
8
+ export declare const C3Sidebar: ({ ariaLabel, children: nodes, isExpanded, onToggleExpanded, expandedWidth, collapsedWidth, linkComponent, labels, }: SidebarProps) => JSX.Element;