@camunda/camunda-composite-components 0.23.2 → 0.23.4

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 (22) hide show
  1. package/lib/esm/package.json +26 -26
  2. package/lib/esm/src/api/endpoints.const.js +2 -2
  3. package/lib/esm/src/components/c3-data-table/c3-data-table.js +1 -1
  4. package/lib/esm/src/components/c3-help-center/c3-help-center-provider.d.ts +2 -1
  5. package/lib/esm/src/components/c3-help-center/c3-help-center-provider.js +4 -1
  6. package/lib/esm/src/components/c3-help-center/c3-help-center.d.ts +2 -1
  7. package/lib/esm/src/components/c3-help-center/c3-help-center.js +4 -3
  8. package/lib/esm/src/components/c3-navigation/c3-navigation-appbar/c3-navigation-appbar.js +6 -6
  9. package/lib/esm/src/components/c3-navigation/c3-navigation-sidebar/c3-navigation-sidebar-element.js +3 -0
  10. package/lib/esm/src/components/c3-navigation/c3-navigation-sidebar/c3-navigation-sidebar.js +10 -1
  11. package/lib/esm/src/components/c3-navigation/helpers.js +12 -0
  12. package/lib/esm/src/components/c3-navigation-v2/c3-breadcrumb-bar.js +33 -31
  13. package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.js +6 -0
  14. package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.types.d.ts +4 -0
  15. package/lib/esm/src/components/c3-navigation-v2/c3-sidebar.js +25 -42
  16. package/lib/esm/src/components/c3-navigation-v2/c3-tools-area.js +18 -5
  17. package/lib/esm/src/components/c3-navigation-v2/stories/story-templates.d.ts +1 -0
  18. package/lib/esm/src/components/c3-navigation-v2/stories/story-templates.js +112 -0
  19. package/lib/esm/src/components/c3-navigation-v2/use-camunda-tools.d.ts +21 -4
  20. package/lib/esm/src/components/c3-navigation-v2/use-camunda-tools.js +39 -20
  21. package/lib/esm/src/components/c3-navigation-v2/use-cluster-sidebar-entries.js +10 -8
  22. package/package.json +27 -27
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/camunda-composite-components",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
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",
@@ -23,8 +23,8 @@ export const CONSOLE = {
23
23
  };
24
24
  export const STATUS = {
25
25
  id: 'status',
26
- dev: 'https://camundaultrawombat.statuspage.io/',
27
- int: 'https://camundaultrawombat.statuspage.io/',
26
+ dev: 'https://camundaultrawombat.statuspage.io',
27
+ int: 'https://camundaultrawombat.statuspage.io',
28
28
  prod: 'https://status.camunda.io',
29
29
  };
30
30
  export function getEndpoint(stage, endpoint) {
@@ -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
  };
@@ -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;
@@ -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;
@@ -7,7 +7,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  import { Popover, PopoverContent } from '@carbon/react';
8
8
  import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, } from '@carbon/react/icons/index.esm.js';
9
9
  import { useEffect, useRef, useState } from 'react';
10
- import styled from 'styled-components';
10
+ import styled, { css } from 'styled-components';
11
11
  const SidebarNav = styled.nav `
12
12
  position: fixed;
13
13
  top: 3rem;
@@ -76,20 +76,33 @@ const NavLabel = styled.span `
76
76
  text-overflow: ellipsis;
77
77
  white-space: nowrap;
78
78
  `;
79
+ /**
80
+ * Container row for `group` (pure section) and `group-item` (clickable +
81
+ * expandable) variants. `$clickable` adds the navigable affordance:
82
+ * hover background + transition. `$isActive` flips the selected styling.
83
+ */
79
84
  const GroupHeader = styled.div `
80
85
  display: flex;
81
86
  align-items: center;
82
87
  flex-wrap: nowrap;
83
88
  width: 100%;
84
89
  min-height: 2.5rem;
90
+ padding-right: var(--cds-spacing-04);
91
+ gap: var(--cds-spacing-03);
85
92
  overflow: hidden;
86
93
  background: ${(p) => (p.$isActive ? 'var(--cds-layer-selected)' : 'transparent')};
87
94
  border-left: ${(p) => (p.$isActive ? '3px solid var(--cds-border-interactive)' : '3px solid transparent')};
88
- transition: background 0.15s, color 0.15s;
89
95
 
90
- &:hover:not(:has(button[data-expand]:hover)) {
91
- background: ${(p) => (p.$isActive ? 'var(--cds-layer-selected)' : 'var(--cds-layer-hover)')};
92
- }
96
+ ${(p) => p.$clickable &&
97
+ css `
98
+ transition: background 0.15s, color 0.15s;
99
+
100
+ &:hover:not(:has(button[data-expand]:hover)) {
101
+ background: ${p.$isActive
102
+ ? 'var(--cds-layer-selected)'
103
+ : 'var(--cds-layer-hover)'};
104
+ }
105
+ `}
93
106
  `;
94
107
  const GroupLabelButton = styled.button `
95
108
  display: flex;
@@ -127,7 +140,6 @@ const ExpandButton = styled.button `
127
140
  align-items: center;
128
141
  justify-content: center;
129
142
  padding: var(--cds-spacing-02);
130
- margin-right: var(--cds-spacing-04);
131
143
  background: transparent;
132
144
  border: none;
133
145
  border-radius: 4px;
@@ -144,15 +156,6 @@ const ExpandButton = styled.button `
144
156
  outline-offset: -2px;
145
157
  }
146
158
  `;
147
- const PlainGroupHeader = styled.div `
148
- display: flex;
149
- align-items: center;
150
- flex-wrap: nowrap;
151
- width: 100%;
152
- min-height: 2.5rem;
153
- overflow: hidden;
154
- border-left: 3px solid transparent;
155
- `;
156
159
  const PlainGroupLabel = styled.span `
157
160
  display: flex;
158
161
  align-items: center;
@@ -168,28 +171,6 @@ const PlainGroupLabel = styled.span `
168
171
  font-size: 0.875rem;
169
172
  font-weight: 400;
170
173
  `;
171
- const PlainGroupExpandButton = styled.button `
172
- display: flex;
173
- align-items: center;
174
- justify-content: center;
175
- padding: var(--cds-spacing-02);
176
- margin-right: var(--cds-spacing-04);
177
- background: transparent;
178
- border: none;
179
- border-radius: 4px;
180
- cursor: pointer;
181
- color: var(--cds-icon-secondary);
182
- transition: background 0.15s;
183
-
184
- &:hover {
185
- background: var(--cds-layer-hover);
186
- }
187
-
188
- &:focus-visible {
189
- outline: 2px solid var(--cds-focus);
190
- outline-offset: -2px;
191
- }
192
- `;
193
174
  const StyledPopover = styled(Popover) `
194
175
  display: block;
195
176
 
@@ -200,7 +181,9 @@ const StyledPopover = styled(Popover) `
200
181
  white-space: nowrap;
201
182
  }
202
183
  `;
203
- const CollapsedItemTooltip = ({ label, children, }) => {
184
+ // `enabled={false}` keeps the wrapper but skips the popover. Lets a parent
185
+ // button keep its focus across state toggles (see the collapse button below).
186
+ const CollapsedItemTooltip = ({ label, enabled = true, children, }) => {
204
187
  const [open, setOpen] = useState(false);
205
188
  const timerRef = useRef(null);
206
189
  const handleMouseEnter = () => {
@@ -217,7 +200,7 @@ const CollapsedItemTooltip = ({ label, children, }) => {
217
200
  if (timerRef.current)
218
201
  clearTimeout(timerRef.current);
219
202
  }, []);
220
- return (_jsxs(StyledPopover, { open: open, align: 'right', highContrast: true, dropShadow: false, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [children, _jsx(PopoverContent, { children: label })] }));
203
+ return (_jsxs(StyledPopover, { open: enabled && open, align: 'right', highContrast: true, dropShadow: false, onMouseEnter: enabled ? handleMouseEnter : undefined, onMouseLeave: enabled ? handleMouseLeave : undefined, children: [children, _jsx(PopoverContent, { children: label })] }));
221
204
  };
222
205
  const SectionDivider = styled.div `
223
206
  border-top: ${(p) => (p.$hideTopDivider ? 'none' : '1px solid var(--cds-border-subtle-01)')};
@@ -323,7 +306,7 @@ const GroupItemNode = ({ node, sidebarExpanded, depth, linkComponent, }) => {
323
306
  const collapsed = (_jsx(NavButton, { as: resolveLinkAs(node.linkProps, linkComponent), ...(node.linkProps ?? {}), "$isActive": isActive, "$isExpanded": false, "$depth": 0, onClick: node.onClick ?? node.onToggleExpand, "aria-current": isActive ? 'page' : undefined, children: _jsx(Icon, { size: 20, style: { flexShrink: 0 } }) }));
324
307
  return (_jsx(CollapsedItemTooltip, { label: node.label, children: collapsed }));
325
308
  }
326
- return (_jsxs("div", { children: [_jsxs(GroupHeader, { "$isActive": isActive, "$depth": depth, children: [_jsxs(GroupLabelButton, { as: resolveLinkAs(node.linkProps, linkComponent), ...(node.linkProps ?? {}), "$isActive": isActive, "$isClickable": true, "$depth": depth, onClick: node.onClick, "aria-current": isActive ? 'page' : undefined, children: [_jsx(Icon, { size: 20, style: { flexShrink: 0 } }), _jsx(NavLabel, { children: node.label }), node.trailingElement] }), node.onToggleExpand && node.children.length > 0 && (_jsx(ExpandButton, { "data-expand": true, onClick: (e) => {
309
+ return (_jsxs("div", { children: [_jsxs(GroupHeader, { "$isActive": isActive, "$clickable": true, children: [_jsxs(GroupLabelButton, { as: resolveLinkAs(node.linkProps, linkComponent), ...(node.linkProps ?? {}), "$isActive": isActive, "$isClickable": true, "$depth": depth, onClick: node.onClick, "aria-current": isActive ? 'page' : undefined, children: [_jsx(Icon, { size: 20, style: { flexShrink: 0 } }), _jsx(NavLabel, { children: node.label }), node.trailingElement] }), node.onToggleExpand && node.children.length > 0 && (_jsx(ExpandButton, { "data-expand": true, onClick: (e) => {
327
310
  e.stopPropagation();
328
311
  node.onToggleExpand?.();
329
312
  }, "aria-label": node.isExpanded
@@ -337,7 +320,7 @@ const GroupNode = ({ node, sidebarExpanded, depth, linkComponent, }) => {
337
320
  const collapsed = (_jsx(NavButton, { "$isActive": false, "$isExpanded": false, "$depth": 0, onClick: node.onToggleExpand, children: _jsx(Icon, { size: 20, style: { flexShrink: 0 } }) }));
338
321
  return (_jsx(CollapsedItemTooltip, { label: node.label, children: collapsed }));
339
322
  }
340
- return (_jsxs("div", { children: [_jsxs(PlainGroupHeader, { "$depth": depth, children: [_jsxs(PlainGroupLabel, { "$depth": depth, children: [_jsx(Icon, { size: 20, style: { flexShrink: 0 } }), _jsx(NavLabel, { children: node.label }), node.trailingElement] }), node.onToggleExpand && (_jsx(PlainGroupExpandButton, { onClick: node.onToggleExpand, "aria-label": node.isExpanded
323
+ return (_jsxs("div", { children: [_jsxs(GroupHeader, { children: [_jsxs(PlainGroupLabel, { "$depth": depth, children: [_jsx(Icon, { size: 20, style: { flexShrink: 0 } }), _jsx(NavLabel, { children: node.label }), node.trailingElement] }), node.onToggleExpand && (_jsx(ExpandButton, { "data-expand": true, onClick: node.onToggleExpand, "aria-label": node.isExpanded
341
324
  ? `Collapse ${node.label}`
342
325
  : `Expand ${node.label}`, "aria-expanded": !!node.isExpanded, children: node.isExpanded ? (_jsx(ChevronUp, { size: 16 })) : (_jsx(ChevronDown, { size: 16 })) }))] }), node.isExpanded &&
343
326
  node.children.map((child) => (_jsx(SidebarNodeComponent, { node: child, sidebarExpanded: sidebarExpanded, depth: depth + 1, linkComponent: linkComponent }, child.key)))] }));
@@ -390,5 +373,5 @@ export const C3Sidebar = ({ ariaLabel, children: nodes, isExpanded = true, onTog
390
373
  }
391
374
  return (_jsx(SidebarNodeComponent, { node: node, sidebarExpanded: isExpanded, depth: 0, linkComponent: linkComponent, hideTopDivider: hideTopDivider, eatScrollPadding: eatScrollPadding, tight: tight }, node.key));
392
375
  });
393
- })() }), onToggleExpanded && (_jsx(CollapseToggleArea, { children: isExpanded ? (_jsxs(CollapseButton, { "$isExpanded": true, onClick: onToggleExpanded, "aria-label": 'Collapse sidebar', "aria-expanded": true, children: [_jsx(ChevronLeft, { size: 20, style: { flexShrink: 0 } }), _jsx(NavLabel, { children: "Collapse" })] })) : (_jsx(CollapsedItemTooltip, { label: 'Expand', children: _jsx(CollapseButton, { "$isExpanded": false, onClick: onToggleExpanded, "aria-label": 'Expand sidebar', "aria-expanded": false, children: _jsx(ChevronRight, { size: 20, style: { flexShrink: 0 } }) }) })) }))] }));
376
+ })() }), onToggleExpanded && (_jsx(CollapseToggleArea, { children: _jsx(CollapsedItemTooltip, { label: 'Expand', enabled: !isExpanded, children: _jsxs(CollapseButton, { "$isExpanded": isExpanded, onClick: onToggleExpanded, "aria-label": isExpanded ? 'Collapse sidebar' : 'Expand sidebar', "aria-expanded": isExpanded, children: [isExpanded ? (_jsx(ChevronLeft, { size: 20, style: { flexShrink: 0 } })) : (_jsx(ChevronRight, { size: 20, style: { flexShrink: 0 } })), isExpanded && _jsx(NavLabel, { children: "Collapse" })] }) }) }))] }));
394
377
  };
@@ -97,9 +97,22 @@ export const C3ToolsArea = ({ tools, activeToolKey: controlledKey, onActiveToolC
97
97
  return;
98
98
  setActive(null);
99
99
  }, [setActive]);
100
- return (_jsxs(_Fragment, { children: [tools.map((tool) => (_jsx(Fragment, { children: tool.renderButton({
101
- onClick: () => handleToolClick(tool.key, Boolean(tool.panel)),
102
- isActive: activeKey === tool.key,
103
- }) }, tool.key))), activeTool?.panel &&
104
- createPortal(_jsx(ToolsPanel, { ref: panelRef, role: 'complementary', "aria-label": activeTool.label, tabIndex: -1, onBlur: handlePanelBlur, children: activeTool.panel }), document.body)] }));
100
+ // Shift+Tab at the panel's first focusable: close the panel and return
101
+ // focus to the trigger button, instead of letting focus escape to whatever
102
+ // sits before the portaled panel in DOM order.
103
+ const handlePanelKeyDown = useCallback((event) => {
104
+ if (event.key !== 'Tab' || !event.shiftKey)
105
+ return;
106
+ const first = panelRef.current?.querySelector(FOCUSABLE_SELECTOR);
107
+ if (!first || event.target !== first)
108
+ return;
109
+ event.preventDefault();
110
+ setActive(null);
111
+ openerRef.current?.focus();
112
+ }, [setActive]);
113
+ return (_jsxs(_Fragment, { children: [tools.map((tool) => {
114
+ const RenderButton = tool.renderButton;
115
+ return (_jsx(Fragment, { children: _jsx(RenderButton, { onClick: () => handleToolClick(tool.key, Boolean(tool.panel)), isActive: activeKey === tool.key }) }, tool.key));
116
+ }), activeTool?.panel &&
117
+ createPortal(_jsx(ToolsPanel, { ref: panelRef, role: 'complementary', "aria-label": activeTool.label, tabIndex: -1, onBlur: handlePanelBlur, onKeyDown: handlePanelKeyDown, children: activeTool.panel }), document.body)] }));
105
118
  };
@@ -12,3 +12,4 @@ export declare const PruningTemplate: FC;
12
12
  export declare const BuildClusterSidebarEntriesTemplate: FC;
13
13
  export declare const CompactSectionTemplate: FC;
14
14
  export declare const GlobalActionWithCustomElementTemplate: FC;
15
+ export declare const LongBreadcrumbsTemplate: FC;
@@ -1379,3 +1379,115 @@ export const GlobalActionWithCustomElementTemplate = () => {
1379
1379
  });
1380
1380
  return (_jsxs(_Fragment, { children: [_jsx(C3NavigationV2, { ...navProps }), _jsxs(MainContent, { sidebarExpanded: isSidebarExpanded, children: [_jsx("h1", { children: "Global Action with Custom Element" }), _jsxs("p", { style: { marginTop: '1rem', color: 'var(--cds-text-secondary)' }, children: ["When a ", _jsx("code", { children: "globalActions" }), " entry supplies an", ' ', _jsx("code", { children: "element" }), ", the navigation renders it as a direct child of the header bar (no wrapper) so popups and inputs that rely on a stable positioning context (e.g. C4Search) work correctly. Click the search icon to expand the input and verify the popup positions against the search field, not against an unrelated parent."] })] })] }));
1381
1381
  };
1382
+ // ─── Long breadcrumbs (header space allocation) ─────────────────────────────
1383
+ export const LongBreadcrumbsTemplate = () => {
1384
+ const { navProps, isSidebarExpanded } = useC3NavigationV2({
1385
+ app: { ariaLabel: 'Camunda Modeler', linkProps: { href: '#' } },
1386
+ skipToContentTargetId: 'main-content',
1387
+ activeItemKey: 'readme',
1388
+ breadcrumbs: [
1389
+ {
1390
+ key: 'org',
1391
+ label: 'Globex Megacorp International Holdings GmbH & Co. KG',
1392
+ icon: Building,
1393
+ dropdownTitle: 'Switch organization',
1394
+ dropdownItems: [
1395
+ {
1396
+ key: 'org-globex',
1397
+ label: 'Globex Megacorp International Holdings GmbH & Co. KG',
1398
+ icon: Building,
1399
+ isSelected: true,
1400
+ },
1401
+ { key: 'org-acme', label: 'Acme Corp', icon: Building },
1402
+ { key: 'org-beta', label: 'Beta Inc', icon: Building },
1403
+ ],
1404
+ },
1405
+ {
1406
+ key: 'cluster',
1407
+ label: 'eu-west-3 production cluster (long-lived, customer-facing)',
1408
+ icon: CloudApp,
1409
+ dropdownTitle: 'Switch cluster',
1410
+ dropdownItems: [
1411
+ {
1412
+ key: 'cluster-prod-eu',
1413
+ label: 'eu-west-3 production cluster (long-lived, customer-facing)',
1414
+ icon: CloudApp,
1415
+ isSelected: true,
1416
+ },
1417
+ {
1418
+ key: 'cluster-staging',
1419
+ label: 'staging cluster',
1420
+ icon: CloudApp,
1421
+ },
1422
+ ],
1423
+ actions: [
1424
+ {
1425
+ key: 'pause',
1426
+ label: 'Pause cluster',
1427
+ onClick: () => console.log('pause cluster'),
1428
+ },
1429
+ {
1430
+ key: 'rename-cluster',
1431
+ label: 'Rename cluster',
1432
+ onClick: () => console.log('rename cluster'),
1433
+ },
1434
+ {
1435
+ key: 'delete-cluster',
1436
+ label: 'Delete cluster',
1437
+ isDanger: true,
1438
+ hasDivider: true,
1439
+ onClick: () => console.log('delete cluster'),
1440
+ },
1441
+ ],
1442
+ },
1443
+ {
1444
+ key: 'project',
1445
+ label: 'Customer Onboarding & KYC Verification Pipeline (rev. 2026)',
1446
+ icon: Folder,
1447
+ actions: [
1448
+ {
1449
+ key: 'rename',
1450
+ label: 'Rename project',
1451
+ onClick: () => console.log('rename project'),
1452
+ },
1453
+ {
1454
+ key: 'duplicate',
1455
+ label: 'Duplicate project',
1456
+ onClick: () => console.log('duplicate project'),
1457
+ },
1458
+ {
1459
+ key: 'delete',
1460
+ label: 'Delete project',
1461
+ isDanger: true,
1462
+ hasDivider: true,
1463
+ onClick: () => console.log('delete project'),
1464
+ },
1465
+ ],
1466
+ },
1467
+ {
1468
+ key: 'file',
1469
+ label: 'customer-onboarding-kyc-verification-pipeline.bpmn',
1470
+ icon: Diagram,
1471
+ actions: [
1472
+ {
1473
+ key: 'rename-file',
1474
+ label: 'Rename file',
1475
+ onClick: () => console.log('rename file'),
1476
+ },
1477
+ {
1478
+ key: 'download',
1479
+ label: 'Download BPMN',
1480
+ onClick: () => console.log('download'),
1481
+ },
1482
+ ],
1483
+ },
1484
+ ],
1485
+ sidebarChildren: [],
1486
+ globalActions: [
1487
+ { key: 'notifications', label: 'Notifications', icon: Notification },
1488
+ { key: 'help', label: 'Help', icon: Help },
1489
+ { key: 'user', label: 'Account', icon: UserAvatar },
1490
+ ],
1491
+ });
1492
+ return (_jsxs(_Fragment, { children: [_jsx(C3NavigationV2, { ...navProps }), _jsxs(MainContent, { sidebarExpanded: isSidebarExpanded, hasSidebar: false, children: [_jsx("h1", { children: "Long Breadcrumbs" }), _jsx("p", { style: { marginTop: '1rem', color: 'var(--cds-text-secondary)' }, children: "Demonstrates header space allocation with long org / cluster / project / file names. The tools area on the right renders its intrinsic content; the breadcrumb row claims the remaining width. Resize the viewport to see how the row reacts. Segment-level truncation (ellipsis + overflow collapse chip) is a separate follow-up; today the row scrolls horizontally when content exceeds the budget." })] })] }));
1493
+ };
@@ -1,3 +1,4 @@
1
+ import { type FC, type ReactNode } from 'react';
1
2
  import type { Notification } from '../../api/notifications';
2
3
  import type { ToolDescriptor } from './c3-navigation-v2.types';
3
4
  import { type InfoPanelElement } from './tools/c3-info-panel';
@@ -39,11 +40,27 @@ export interface UseCamundaToolsOptions {
39
40
  */
40
41
  help?: HelpToolOptions;
41
42
  }
43
+ export interface UseCamundaToolsReturn {
44
+ tools: ToolDescriptor[];
45
+ /**
46
+ * Wraps the consumer's nav subtree with the providers the requested tools
47
+ * depend on. Currently only notifications needs a provider; when omitted,
48
+ * `ToolsProvider` passes children through unchanged. Wrap the
49
+ * `<C3NavigationV2>` render with this so the tool components can read their
50
+ * context inside the JSX they're rendered into.
51
+ */
52
+ ToolsProvider: FC<{
53
+ children: ReactNode;
54
+ }>;
55
+ }
42
56
  /**
43
57
  * Single helper entry point for the standard Camunda tool set.
44
- * Returns a ToolDescriptor[] ready to pass into C3NavigationV2's `tools` prop.
45
- * Custom tools can be appended to the returned array.
58
+ * Returns a `ToolDescriptor[]` for `C3NavigationV2`'s `tools` prop plus a
59
+ * `ToolsProvider` that conditionally mounts the providers each requested tool
60
+ * depends on. Custom tools can be appended to the returned array; if they
61
+ * have their own provider needs, layer a separate wrapper outside
62
+ * `ToolsProvider`.
46
63
  *
47
- * Must be called within a C3UserConfigurationProvider tree (SaaS).
64
+ * Must be called within a `C3UserConfigurationProvider` tree (SaaS).
48
65
  */
49
- export declare const useCamundaTools: (options: UseCamundaToolsOptions) => ToolDescriptor[];
66
+ export declare const useCamundaTools: (options: UseCamundaToolsOptions) => UseCamundaToolsReturn;
@@ -1,75 +1,94 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, 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.
5
5
  * You may not use this file except in compliance with the commercial license.
6
6
  */
7
- import { HeaderGlobalAction } from '@carbon/react';
7
+ import { HeaderGlobalAction as HeaderGlobalActionBase } from '@carbon/react';
8
8
  import { Help as HelpIcon, Information, Notification as NotificationIcon, UserAvatar, } from '@carbon/react/icons/index.esm.js';
9
9
  import { useContext, useMemo } from 'react';
10
10
  import { C3NotificationsUnreadIcon } from '../../assets/c3-icons.js';
11
- import { C3NotificationContext } from '../c3-navigation/c3-notification-provider/c3-notification-provider.js';
11
+ import { useC3HelpCenter } from '../c3-help-center/c3-help-center-provider.js';
12
+ import C3NotificationProvider, { C3NotificationContext, } from '../c3-navigation/c3-notification-provider/c3-notification-provider.js';
12
13
  import { C3InfoPanel } from './tools/c3-info-panel.js';
13
14
  import { C3NotificationsPanel } from './tools/c3-notifications-panel.js';
14
15
  import { C3UserPanel } from './tools/c3-user-panel.js';
16
+ /**
17
+ * Carbon's `HeaderGlobalAction` is typed as `React.FC` and omits both `ref`
18
+ * (the component uses `forwardRef` internally) and `leaveDelayMs`. Both work
19
+ * at runtime, so we widen the type at the import site — same pattern as
20
+ * `c3-navigation-appbar` / `-sidebar`.
21
+ */
22
+ const HeaderGlobalAction = HeaderGlobalActionBase;
15
23
  /**
16
24
  * Single helper entry point for the standard Camunda tool set.
17
- * Returns a ToolDescriptor[] ready to pass into C3NavigationV2's `tools` prop.
18
- * Custom tools can be appended to the returned array.
25
+ * Returns a `ToolDescriptor[]` for `C3NavigationV2`'s `tools` prop plus a
26
+ * `ToolsProvider` that conditionally mounts the providers each requested tool
27
+ * depends on. Custom tools can be appended to the returned array; if they
28
+ * have their own provider needs, layer a separate wrapper outside
29
+ * `ToolsProvider`.
19
30
  *
20
- * Must be called within a C3UserConfigurationProvider tree (SaaS).
31
+ * Must be called within a `C3UserConfigurationProvider` tree (SaaS).
21
32
  */
22
33
  export const useCamundaTools = (options) => {
23
- const { notifications, isFetching } = useContext(C3NotificationContext);
24
- const hasUnread = !isFetching && notifications.some((n) => n.state === 'new');
25
- return useMemo(() => {
26
- const tools = [];
34
+ const { launcherButtonRef: helpButtonRef } = useC3HelpCenter();
35
+ const tools = useMemo(() => {
36
+ const result = [];
27
37
  if (options.notifications !== undefined) {
28
38
  const notifOptions = options.notifications;
29
39
  const label = notifOptions.ariaLabel ?? 'Notifications';
30
- tools.push({
40
+ result.push({
31
41
  key: 'notifications',
32
42
  label,
33
- renderButton: ({ onClick, isActive }) => (_jsx(HeaderGlobalAction, { "aria-label": label, onClick: onClick, isActive: isActive, tooltipAlignment: 'center', ...{ leaveDelayMs: 100 }, children: hasUnread ? (_jsx(C3NotificationsUnreadIcon, { size: 20 })) : (_jsx(NotificationIcon, { size: 20 })) })),
43
+ renderButton: ({ onClick, isActive }) => {
44
+ const { notifications, isFetching } = useContext(C3NotificationContext);
45
+ const hasUnread = !isFetching && notifications.some((n) => n.state === 'new');
46
+ return (_jsx(HeaderGlobalAction, { "aria-label": label, onClick: onClick, isActive: isActive, tooltipAlignment: 'center', leaveDelayMs: 100, children: hasUnread ? (_jsx(C3NotificationsUnreadIcon, { size: 20 })) : (_jsx(NotificationIcon, { size: 20 })) }));
47
+ },
34
48
  panel: (_jsx(C3NotificationsPanel, { onLinkClick: notifOptions.onLinkClick })),
35
49
  });
36
50
  }
37
51
  if (options.info !== undefined) {
38
52
  const { elements, title, ariaLabel } = options.info;
39
53
  const label = ariaLabel ?? 'Info';
40
- tools.push({
54
+ result.push({
41
55
  key: 'info',
42
56
  label,
43
- renderButton: ({ onClick, isActive }) => (_jsx(HeaderGlobalAction, { "aria-label": label, onClick: onClick, isActive: isActive, tooltipAlignment: 'center', ...{ leaveDelayMs: 100 }, children: _jsx(Information, { size: 20 }) })),
57
+ renderButton: ({ onClick, isActive }) => (_jsx(HeaderGlobalAction, { "aria-label": label, onClick: onClick, isActive: isActive, tooltipAlignment: 'center', leaveDelayMs: 100, children: _jsx(Information, { size: 20 }) })),
44
58
  panel: (_jsx(C3InfoPanel, { elements: elements, title: title })),
45
59
  });
46
60
  }
47
61
  if (options.help !== undefined) {
48
62
  const { onClick, ariaLabel } = options.help;
49
63
  const label = ariaLabel ?? 'Help';
50
- tools.push({
64
+ result.push({
51
65
  key: 'help',
52
66
  label,
53
- renderButton: ({ isActive: _isActive }) => (_jsx(HeaderGlobalAction, { "aria-label": label, onClick: onClick, tooltipAlignment: 'center', ...{ leaveDelayMs: 100 }, children: _jsx(HelpIcon, { size: 20 }) })),
67
+ renderButton: ({ isActive: _isActive }) => (_jsx(HeaderGlobalAction, { ref: helpButtonRef, "aria-label": label, onClick: onClick, tooltipAlignment: 'center', leaveDelayMs: 100, children: _jsx(HelpIcon, { size: 20 }) })),
54
68
  // no panel, help is a plain button
55
69
  });
56
70
  }
57
71
  if (options.user !== undefined) {
58
72
  const { ariaLabel, ...panelProps } = options.user;
59
73
  const label = ariaLabel ?? 'Account';
60
- tools.push({
74
+ result.push({
61
75
  key: 'user',
62
76
  label,
63
- renderButton: ({ onClick, isActive }) => (_jsx(HeaderGlobalAction, { "aria-label": label, onClick: onClick, isActive: isActive, tooltipAlignment: 'end', ...{ leaveDelayMs: 100 }, children: _jsx(UserAvatar, { size: 20 }) })),
77
+ renderButton: ({ onClick, isActive }) => (_jsx(HeaderGlobalAction, { "aria-label": label, onClick: onClick, isActive: isActive, tooltipAlignment: 'end', leaveDelayMs: 100, children: _jsx(UserAvatar, { size: 20 }) })),
64
78
  panel: (_jsx(C3UserPanel, { ...panelProps })),
65
79
  });
66
80
  }
67
- return tools;
81
+ return result;
68
82
  }, [
69
- hasUnread,
70
83
  options.notifications,
71
84
  options.info,
72
85
  options.help,
73
86
  options.user,
87
+ helpButtonRef,
74
88
  ]);
89
+ const ToolsProvider = useMemo(() => {
90
+ const needsNotifications = options.notifications !== undefined;
91
+ return ({ children }) => needsNotifications ? (_jsx(C3NotificationProvider, { children: children })) : (_jsx(_Fragment, { children: children }));
92
+ }, [options.notifications !== undefined]);
93
+ return { tools, ToolsProvider };
75
94
  };
@@ -24,15 +24,17 @@ function buildClusterSidebarEntries(clusters, { isAppVisible, resolveClusterLink
24
24
  if (!endpoint)
25
25
  return [];
26
26
  const teaser = appTeaserRoutes?.[resolvedApp] ?? appTeaserRoutes?.[app];
27
- // Admin has no dedicated status on the cluster DTO and doesn't depend
28
- // on zeebe being up (it manages the cluster itself). Always link to
29
- // the endpoint when present; consumers can still override via
30
- // `appTeaserRoutes.admin` / `.identity` if they want a teaser page.
31
- const linkProps = isAdminApp(app)
27
+ // Admin/identity has no dedicated status on the cluster DTO; it rides
28
+ // the same ingress as zeebe, so we gate its link on zeebe's health
29
+ // matching V1's "zeebe as admin health proxy" contract. Consumers can
30
+ // still surface a teaser via `appTeaserRoutes.admin` / `.identity`
31
+ // when the link is unavailable.
32
+ const healthKey = isAdminApp(app)
33
+ ? 'zeebe'
34
+ : resolvedApp;
35
+ const linkProps = cluster.status?.[healthKey] === 'Healthy'
32
36
  ? (teaser ?? { href: endpoint })
33
- : cluster.status?.[resolvedApp] === 'Healthy'
34
- ? { href: endpoint }
35
- : teaser;
37
+ : teaser;
36
38
  if (!linkProps)
37
39
  return [];
38
40
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/camunda-composite-components",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
4
4
  "description": "Camunda Composite Components",
5
5
  "bugs": {
6
6
  "url": "https://github.com/camunda/camunda-cloud-management-apps/issues"
@@ -26,41 +26,41 @@
26
26
  "jwt-decode": "4.0.0",
27
27
  "react-error-boundary": "6.1.1",
28
28
  "react-markdown": "10.1.0",
29
- "semver": "7.7.4"
29
+ "semver": "7.8.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@auth0/auth0-spa-js": "2.19.0",
33
- "@carbon/react": "1.103.0",
34
- "@chromatic-com/storybook": "5.1.1",
32
+ "@auth0/auth0-spa-js": "2.19.3",
33
+ "@carbon/react": "1.107.1",
34
+ "@chromatic-com/storybook": "5.2.1",
35
35
  "@mdx-js/react": "3.1.1",
36
- "@playwright/test": "1.59.1",
37
- "@storybook/addon-a11y": "10.3.5",
38
- "@storybook/addon-docs": "10.3.5",
39
- "@storybook/addon-links": "10.3.5",
40
- "@storybook/addon-vitest": "10.3.5",
41
- "@storybook/react": "10.3.5",
42
- "@storybook/react-vite": "10.3.5",
43
- "@vitest/browser": "4.1.4",
44
- "@vitest/browser-playwright": "4.1.4",
45
- "vitest": "4.1.4",
36
+ "@playwright/test": "1.60.0",
37
+ "@storybook/addon-a11y": "10.4.0",
38
+ "@storybook/addon-docs": "10.4.0",
39
+ "@storybook/addon-links": "10.4.0",
40
+ "@storybook/addon-vitest": "10.4.0",
41
+ "@storybook/react": "10.4.0",
42
+ "@storybook/react-vite": "10.4.0",
43
+ "@vitest/browser": "4.1.7",
44
+ "@vitest/browser-playwright": "4.1.7",
45
+ "vitest": "4.1.7",
46
46
  "conventional-changelog-conventionalcommits": "9.3.1",
47
47
  "eslint-import-resolver-typescript": "4.4.4",
48
48
  "eslint-plugin-react": "7.37.5",
49
- "eslint-plugin-react-hooks": "7.0.1",
50
- "eslint-plugin-storybook": "10.3.5",
49
+ "eslint-plugin-react-hooks": "7.1.1",
50
+ "eslint-plugin-storybook": "10.4.0",
51
51
  "event-source-polyfill": "1.0.31",
52
- "mixpanel-browser": "2.78.0",
53
- "playwright": "1.59.1",
54
- "react": "19.2.5",
55
- "react-dom": "19.2.5",
56
- "react-is": "19.2.5",
52
+ "mixpanel-browser": "2.79.0",
53
+ "playwright": "1.60.0",
54
+ "react": "19.2.6",
55
+ "react-dom": "19.2.6",
56
+ "react-is": "19.2.6",
57
57
  "rimraf": "6.1.3",
58
58
  "serve": "14.2.6",
59
- "storybook": "10.3.5",
60
- "styled-components": "6.4.0",
61
- "typescript-eslint": "8.58.1",
62
- "wait-on": "9.0.5",
63
- "@camunda/ccma-shared-types": "0.0.5"
59
+ "storybook": "10.4.0",
60
+ "styled-components": "6.4.2",
61
+ "typescript-eslint": "8.59.4",
62
+ "wait-on": "9.0.10",
63
+ "@camunda/ccma-shared-types": "0.1.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@carbon/react": "1.x",