@camunda/camunda-composite-components 0.23.4 → 0.25.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 (33) hide show
  1. package/lib/esm/package.json +22 -22
  2. package/lib/esm/src/assets/c3-icons.d.ts +4 -4
  3. package/lib/esm/src/components/c3-app-teaser/app-teaser-cards.d.ts +1 -1
  4. package/lib/esm/src/components/c3-help-center/help-center-hint.d.ts +1 -1
  5. package/lib/esm/src/components/c3-license-tag/c3-license-tag.d.ts +4 -5
  6. package/lib/esm/src/components/c3-license-tag/c3-license-tag.js +57 -46
  7. package/lib/esm/src/components/c3-navigation-v2/c3-breadcrumb-bar.js +2 -2
  8. package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.js +6 -2
  9. package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.types.d.ts +14 -0
  10. package/lib/esm/src/components/c3-navigation-v2/c3-sidebar.d.ts +1 -1
  11. package/lib/esm/src/components/c3-navigation-v2/c3-sidebar.js +62 -47
  12. package/lib/esm/src/components/c3-navigation-v2/index.d.ts +5 -3
  13. package/lib/esm/src/components/c3-navigation-v2/index.js +1 -0
  14. package/lib/esm/src/components/c3-navigation-v2/tools/c3-info-panel.d.ts +2 -1
  15. package/lib/esm/src/components/c3-navigation-v2/tools/c3-info-panel.js +1 -1
  16. package/lib/esm/src/components/c3-navigation-v2/tools/c3-notifications-panel.d.ts +11 -0
  17. package/lib/esm/src/components/c3-navigation-v2/tools/c3-notifications-panel.js +9 -5
  18. package/lib/esm/src/components/c3-navigation-v2/tools/c3-theme-selector.d.ts +25 -0
  19. package/lib/esm/src/components/c3-navigation-v2/tools/c3-theme-selector.js +15 -0
  20. package/lib/esm/src/components/c3-navigation-v2/tools/c3-user-panel.d.ts +15 -0
  21. package/lib/esm/src/components/c3-navigation-v2/tools/c3-user-panel.js +10 -17
  22. package/lib/esm/src/components/c3-navigation-v2/use-c3-navigation-v2.d.ts +3 -1
  23. package/lib/esm/src/components/c3-navigation-v2/use-c3-navigation-v2.js +2 -1
  24. package/lib/esm/src/components/c3-navigation-v2/use-camunda-tools.d.ts +7 -1
  25. package/lib/esm/src/components/c3-navigation-v2/use-camunda-tools.js +4 -4
  26. package/lib/esm/src/components/c3-navigation-v2/use-cluster-webapp-breadcrumbs.d.ts +20 -16
  27. package/lib/esm/src/components/c3-navigation-v2/use-cluster-webapp-breadcrumbs.js +156 -36
  28. package/lib/esm/src/components/c3-user-configuration/c3-profile-provider/c3-profile-provider.d.ts +1 -1
  29. package/lib/esm/src/contexts/c3-cluster-update-manager.d.ts +1 -1
  30. package/lib/esm/src/index.d.ts +2 -2
  31. package/lib/esm/src/index.js +1 -1
  32. package/lib/esm/src/utils/camunda.d.ts +1 -1
  33. package/package.json +23 -23
@@ -0,0 +1,25 @@
1
+ import type { FC } from 'react';
2
+ import type { Theme } from '../../c3-user-configuration/c3-profile-provider/c3-profile-provider';
3
+ export interface C3ThemeSelectorLabels {
4
+ /** Defaults to `'Theme'`. */
5
+ legend?: string;
6
+ /** Defaults to `'Light'`. */
7
+ light?: string;
8
+ /** Defaults to `'System'`. */
9
+ system?: string;
10
+ /** Defaults to `'Dark'`. */
11
+ dark?: string;
12
+ }
13
+ export interface C3ThemeSelectorProps {
14
+ currentTheme: Theme;
15
+ onChange: (theme: Theme) => void;
16
+ labels?: C3ThemeSelectorLabels;
17
+ }
18
+ /**
19
+ * Light / System / Dark radio group, matching the V1 user sidebar's
20
+ * built-in theme switcher. Consumers wire it into V2's
21
+ * `useCamundaTools.user.customSection` (or anywhere else) and pass
22
+ * their own state. SaaS consumers can read `theme` and
23
+ * `onThemeChange` from `useC3Profile()`; SM consumers pass local state.
24
+ */
25
+ export declare const C3ThemeSelector: FC<C3ThemeSelectorProps>;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
4
+ * under one or more contributor license agreements. Licensed under a commercial license.
5
+ * You may not use this file except in compliance with the commercial license.
6
+ */
7
+ import { Layer, RadioButton, RadioButtonGroup } from '@carbon/react';
8
+ /**
9
+ * Light / System / Dark radio group, matching the V1 user sidebar's
10
+ * built-in theme switcher. Consumers wire it into V2's
11
+ * `useCamundaTools.user.customSection` (or anywhere else) and pass
12
+ * their own state. SaaS consumers can read `theme` and
13
+ * `onThemeChange` from `useC3Profile()`; SM consumers pass local state.
14
+ */
15
+ export const C3ThemeSelector = ({ currentTheme, onChange, labels, }) => (_jsx(Layer, { children: _jsx("div", { style: { padding: '0.5rem 1rem' }, children: _jsxs(RadioButtonGroup, { name: 'theme-radio-group', legendText: labels?.legend ?? 'Theme', orientation: 'vertical', valueSelected: currentTheme, onChange: (value) => onChange(value), children: [_jsx(RadioButton, { id: 'theme-light', labelText: labels?.light ?? 'Light', value: 'light' }), _jsx(RadioButton, { id: 'theme-system', labelText: labels?.system ?? 'System', value: 'system' }), _jsx(RadioButton, { id: 'theme-dark', labelText: labels?.dark ?? 'Dark', value: 'dark' })] }) }) }));
@@ -26,5 +26,20 @@ export interface C3UserPanelProps {
26
26
  * a built-in link (`terms`, `privacy`, `imprint`) replace the default.
27
27
  */
28
28
  elements?: UserPanelElement[];
29
+ /** Defaults to `'Account'`. */
30
+ title?: string | null;
31
+ labels?: C3UserPanelLabels;
32
+ }
33
+ export interface C3UserPanelLabels {
34
+ /** Defaults to `'Terms of use'`. */
35
+ termsOfUse?: string;
36
+ /** Defaults to `'Privacy policy'`. */
37
+ privacyPolicy?: string;
38
+ /** Defaults to `'Imprint'`. */
39
+ imprint?: string;
40
+ /** Defaults to `'Log out'`. */
41
+ logOut?: string;
42
+ /** Footer copyright. Defaults to `'© Camunda Services GmbH {year}'`. Receives the current year. */
43
+ copyright?: (year: number) => string;
29
44
  }
30
45
  export declare const C3UserPanel: FC<C3UserPanelProps>;
@@ -4,11 +4,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
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 { Button, FormLabel, RadioButton, RadioButtonGroup, Stack, Toggle, } from '@carbon/react';
7
+ import { Button, Toggle } from '@carbon/react';
8
8
  import { ArrowRight } from '@carbon/react/icons/index.esm.js';
9
9
  import styled from 'styled-components';
10
- import { useC3Profile, } from '../../c3-user-configuration/c3-profile-provider/c3-profile-provider.js';
11
- import { useC3UserConfiguration } from '../../c3-user-configuration/c3-user-configuration-provider.js';
12
10
  import { PanelHeader, PanelTitle } from './panel-primitives.js';
13
11
  const ProfileSection = styled.div `
14
12
  padding: var(--cds-spacing-05);
@@ -26,10 +24,6 @@ const ProfileEmail = styled.span `
26
24
  font-size: var(--cds-label-01-font-size);
27
25
  color: var(--cds-text-secondary);
28
26
  `;
29
- const ThemeSection = styled.div `
30
- padding: var(--cds-spacing-05);
31
- border-bottom: 1px solid var(--cds-border-subtle-01);
32
- `;
33
27
  const CustomSection = styled.div `
34
28
  border-bottom: 1px solid var(--cds-border-subtle-01);
35
29
  `;
@@ -95,31 +89,30 @@ const Copyright = styled.p `
95
89
  line-height: var(--cds-label-01-line-height);
96
90
  letter-spacing: var(--cds-label-01-letter-spacing);
97
91
  `;
98
- const LEGAL_LINKS = [
92
+ const buildLegalLinks = (labels) => [
99
93
  {
100
94
  key: 'terms',
101
- label: 'Terms of use',
95
+ label: labels?.termsOfUse ?? 'Terms of use',
102
96
  onClick: () => window.open('https://camunda.com/legal/terms/camunda-platform/camunda-platform-8-saas-trial/', '_blank'),
103
97
  },
104
98
  {
105
99
  key: 'privacy',
106
- label: 'Privacy policy',
100
+ label: labels?.privacyPolicy ?? 'Privacy policy',
107
101
  onClick: () => window.open('https://camunda.com/legal/privacy/', '_blank'),
108
102
  },
109
103
  {
110
104
  key: 'imprint',
111
- label: 'Imprint',
105
+ label: labels?.imprint ?? 'Imprint',
112
106
  onClick: () => window.open('https://camunda.com/legal/imprint/', '_blank'),
113
107
  },
114
108
  ];
115
- export const C3UserPanel = ({ name, email, version, onLogout, stageToggle, customSection, elements, }) => {
116
- const { handleTheme } = useC3UserConfiguration();
117
- const { theme, onThemeChange } = useC3Profile();
109
+ export const C3UserPanel = ({ name, email, version, onLogout, stageToggle, customSection, elements, title = 'Account', labels, }) => {
118
110
  const consumerKeys = new Set((elements ?? []).map((e) => e.key));
119
- const defaults = LEGAL_LINKS.filter((l) => !consumerKeys.has(l.key));
111
+ const defaults = buildLegalLinks(labels).filter((l) => !consumerKeys.has(l.key));
120
112
  const allElements = [...(elements ?? []), ...defaults];
121
- return (_jsxs(_Fragment, { children: [_jsx(PanelHeader, { children: _jsx(PanelTitle, { children: "Account" }) }), (name || email) && (_jsxs(ProfileSection, { children: [name && _jsx(ProfileName, { children: name }), email && _jsx(ProfileEmail, { children: email })] })), customSection && _jsx(CustomSection, { children: customSection }), handleTheme && (_jsx(ThemeSection, { children: _jsxs(Stack, { gap: 3, children: [_jsx(FormLabel, { children: "Theme" }), _jsxs(RadioButtonGroup, { name: 'theme', valueSelected: theme, onChange: (value) => onThemeChange(value), orientation: 'vertical', children: [_jsx(RadioButton, { value: 'light', labelText: 'Light', id: 'theme-light' }), _jsx(RadioButton, { value: 'dark', labelText: 'Dark', id: 'theme-dark' }), _jsx(RadioButton, { value: 'system', labelText: 'System', id: 'theme-system' })] })] }) })), stageToggle && (_jsx("div", { style: {
113
+ return (_jsxs(_Fragment, { children: [title && (_jsx(PanelHeader, { children: _jsx(PanelTitle, { children: title }) })), (name || email) && (_jsxs(ProfileSection, { children: [name && _jsx(ProfileName, { children: name }), email && _jsx(ProfileEmail, { children: email })] })), customSection && _jsx(CustomSection, { children: customSection }), stageToggle && (_jsx("div", { style: {
122
114
  padding: 'var(--cds-spacing-05)',
123
115
  borderBottom: '1px solid var(--cds-border-subtle-01)',
124
- }, children: _jsx(Toggle, { id: 'stage-toggle', labelText: 'Production features', toggled: stageToggle.prodFeaturesEnabled, onToggle: stageToggle.toggle, size: 'sm' }) })), allElements.length > 0 && (_jsx(ElementList, { children: allElements.map((element) => (_jsx(ElementItem, { children: _jsx(ElementButton, { type: 'button', onClick: element.onClick, children: element.label }) }, element.key))) })), _jsxs(BottomPinned, { children: [version && (_jsxs(VersionWrapper, { children: [_jsx(VersionText, { children: version }), _jsxs(Copyright, { children: ["\u00A9 Camunda Services GmbH ", new Date().getFullYear()] })] })), onLogout && (_jsx(BottomActions, { children: _jsx(Button, { kind: 'ghost', size: 'lg', onClick: onLogout, renderIcon: ArrowRight, children: "Log out" }) }))] })] }));
116
+ }, children: _jsx(Toggle, { id: 'stage-toggle', labelText: 'Production features', toggled: stageToggle.prodFeaturesEnabled, onToggle: stageToggle.toggle, size: 'sm' }) })), allElements.length > 0 && (_jsx(ElementList, { children: allElements.map((element) => (_jsx(ElementItem, { children: _jsx(ElementButton, { type: 'button', onClick: element.onClick, children: element.label }) }, element.key))) })), _jsxs(BottomPinned, { children: [version && (_jsxs(VersionWrapper, { children: [_jsx(VersionText, { children: version }), _jsx(Copyright, { children: labels?.copyright?.(new Date().getFullYear()) ??
117
+ `© Camunda Services GmbH ${new Date().getFullYear()}` })] })), onLogout && (_jsx(BottomActions, { children: _jsx(Button, { kind: 'ghost', size: 'lg', onClick: onLogout, renderIcon: ArrowRight, children: labels?.logOut ?? 'Log out' }) }))] })] }));
125
118
  };
@@ -1,4 +1,4 @@
1
- import type { BreadcrumbAction, C3NavigationV2Props, GlobalActionButton, LinkComponent, LinkProps, ToolDescriptor } from './c3-navigation-v2.types';
1
+ import type { BreadcrumbAction, C3NavigationV2Props, GlobalActionButton, LinkComponent, LinkProps, SidebarLabels, ToolDescriptor } from './c3-navigation-v2.types';
2
2
  type ActiveMatcher = boolean | string | ((activeItemKey: string) => boolean);
3
3
  export interface ItemDescriptor {
4
4
  type: 'item';
@@ -82,6 +82,8 @@ export interface UseC3NavigationV2Options {
82
82
  skipToContentLabel?: string;
83
83
  headerAriaLabel?: string;
84
84
  sidebarAriaLabel?: string;
85
+ /** Forwarded to `C3Sidebar` `labels`. */
86
+ sidebarLabels?: SidebarLabels;
85
87
  /** Used to resolve `isActive` on sidebar items and `isSelected` on breadcrumb dropdown items. */
86
88
  activeItemKey: string;
87
89
  sidebarChildren?: SidebarNodeDescriptor[];
@@ -30,7 +30,7 @@ function collectDefaultExpanded(nodes, out) {
30
30
  * descriptors (isActive, label functions) into concrete props.
31
31
  */
32
32
  export function useC3NavigationV2(options) {
33
- const { app, skipToContentTargetId, skipToContentLabel, headerAriaLabel, sidebarAriaLabel = 'Main navigation', activeItemKey, sidebarChildren = [], breadcrumbs = [], tools, globalActions, sidebarExpandedWidth, sidebarCollapsedWidth, defaultSidebarExpanded = true, onSidebarToggle, linkComponent, headerTrailingContent, } = options;
33
+ const { app, skipToContentTargetId, skipToContentLabel, headerAriaLabel, sidebarAriaLabel = 'Main navigation', sidebarLabels, activeItemKey, sidebarChildren = [], breadcrumbs = [], tools, globalActions, sidebarExpandedWidth, sidebarCollapsedWidth, defaultSidebarExpanded = true, onSidebarToggle, linkComponent, headerTrailingContent, } = options;
34
34
  const [isSidebarExpanded, setIsSidebarExpanded] = useState(defaultSidebarExpanded);
35
35
  const setSidebarExpanded = useCallback((expanded) => {
36
36
  setIsSidebarExpanded(expanded);
@@ -173,6 +173,7 @@ export function useC3NavigationV2(options) {
173
173
  expandedWidth: sidebarExpandedWidth,
174
174
  collapsedWidth: sidebarCollapsedWidth,
175
175
  linkComponent,
176
+ labels: sidebarLabels,
176
177
  }
177
178
  : undefined;
178
179
  const navProps = useMemo(() => ({
@@ -2,10 +2,15 @@ import { type FC, type ReactNode } from 'react';
2
2
  import type { Notification } from '../../api/notifications';
3
3
  import type { ToolDescriptor } from './c3-navigation-v2.types';
4
4
  import { type InfoPanelElement } from './tools/c3-info-panel';
5
+ import { type C3NotificationsPanelLabels } from './tools/c3-notifications-panel';
5
6
  import { type C3UserPanelProps } from './tools/c3-user-panel';
6
7
  export interface NotificationsToolOptions {
7
8
  onLinkClick?: (meta: Notification['meta']) => void;
8
9
  ariaLabel?: string;
10
+ /** Forwarded to `C3NotificationsPanel`. */
11
+ title?: string | null;
12
+ /** Forwarded to `C3NotificationsPanel`. */
13
+ labels?: C3NotificationsPanelLabels;
9
14
  }
10
15
  export type UserToolOptions = C3UserPanelProps & {
11
16
  ariaLabel?: string;
@@ -16,7 +21,8 @@ export interface HelpToolOptions {
16
21
  }
17
22
  export interface InfoToolOptions {
18
23
  elements: InfoPanelElement[];
19
- title?: string;
24
+ /** Forwarded to `C3InfoPanel`. */
25
+ title?: string | null;
20
26
  ariaLabel?: string;
21
27
  }
22
28
  export interface UseCamundaToolsOptions {
@@ -5,13 +5,13 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
5
5
  * You may not use this file except in compliance with the commercial license.
6
6
  */
7
7
  import { HeaderGlobalAction as HeaderGlobalActionBase } from '@carbon/react';
8
- import { Help as HelpIcon, Information, Notification as NotificationIcon, UserAvatar, } from '@carbon/react/icons/index.esm.js';
8
+ import { Help as HelpIcon, 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
11
  import { useC3HelpCenter } from '../c3-help-center/c3-help-center-provider.js';
12
12
  import C3NotificationProvider, { C3NotificationContext, } from '../c3-navigation/c3-notification-provider/c3-notification-provider.js';
13
13
  import { C3InfoPanel } from './tools/c3-info-panel.js';
14
- import { C3NotificationsPanel } from './tools/c3-notifications-panel.js';
14
+ import { C3NotificationsPanel, } from './tools/c3-notifications-panel.js';
15
15
  import { C3UserPanel } from './tools/c3-user-panel.js';
16
16
  /**
17
17
  * Carbon's `HeaderGlobalAction` is typed as `React.FC` and omits both `ref`
@@ -45,7 +45,7 @@ export const useCamundaTools = (options) => {
45
45
  const hasUnread = !isFetching && notifications.some((n) => n.state === 'new');
46
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
47
  },
48
- panel: (_jsx(C3NotificationsPanel, { onLinkClick: notifOptions.onLinkClick })),
48
+ panel: (_jsx(C3NotificationsPanel, { onLinkClick: notifOptions.onLinkClick, title: notifOptions.title, labels: notifOptions.labels })),
49
49
  });
50
50
  }
51
51
  if (options.info !== undefined) {
@@ -54,7 +54,7 @@ export const useCamundaTools = (options) => {
54
54
  result.push({
55
55
  key: 'info',
56
56
  label,
57
- 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(HelpIcon, { size: 20 }) })),
58
58
  panel: (_jsx(C3InfoPanel, { elements: elements, title: title })),
59
59
  });
60
60
  }
@@ -4,29 +4,33 @@ export interface UseClusterWebappBreadcrumbsOptions {
4
4
  /** The app currently being viewed. */
5
5
  currentApp: CamundaApp;
6
6
  /**
7
- * Optional resolver to convert an endpoint URL into linkProps.
8
- * Defaults to `(url) => ({ href: url })`. Override to support
9
- * client-side routing (e.g. returning `{ to: path }` for internal links).
7
+ * Per-app URL map matching the shape of `Cluster['endpoints']`. Source for
8
+ * the modeler back-links on the org/cluster crumb labels, and the sole
9
+ * source for the app-switcher dropdown when no SaaS cluster is available.
10
10
  */
11
- resolveLinkProps?: (url: string, app: string) => {
12
- href?: string;
13
- to?: string;
14
- [key: string]: unknown;
15
- };
11
+ webappLinks?: Partial<Record<CamundaApp, string>>;
16
12
  }
17
13
  /**
18
14
  * Builds the standard breadcrumb chain for a cluster-scoped Camunda webapp:
19
15
  *
20
- * [Org] > [Cluster] > [App ▾]
16
+ * [Org] > [Cluster] > [App ▾]
21
17
  *
22
- * - **Org crumb**: shown when org data is available (SaaS). Links back to
23
- * web modeler at the org level.
24
- * - **Cluster crumb**: shown when cluster data is available. Links back to
25
- * web modeler / console at the cluster level.
26
- * - **App crumb**: always shown. Includes a dropdown to switch between
27
- * sibling apps on the same cluster (filtered to apps with endpoints).
18
+ * - **Org crumb**: shown when org data is available (SaaS). Label links back
19
+ * to web modeler for the current org. Dropdown switches orgs and lands on
20
+ * web modeler scoped to the selected org. Dropdown only renders when there
21
+ * is at least one sibling org and the current cluster exposes a modeler
22
+ * endpoint to derive sibling URLs from.
23
+ * - **Cluster crumb**: shown when cluster data is available. Label links back
24
+ * to web modeler / console at the cluster level. Dropdown switches clusters
25
+ * while staying on `currentApp`, filtered to clusters that expose an
26
+ * endpoint for that app (alias-aware via `getEndpointForApp`).
27
+ * - **App crumb**: always shown. Dropdown switches between sibling apps on
28
+ * the same cluster (filtered to apps with endpoints).
28
29
  *
29
- * Crumbs are omitted when the underlying data is unavailable, so the same
30
+ * Each dropdown follows the breadcrumb convention: clicking the label
31
+ * navigates, clicking the chevron opens the dropdown. Items are listed in
32
+ * source order with the current entry marked `isSelected`. Crumbs and
33
+ * dropdowns are omitted when the underlying data is unavailable, so the same
30
34
  * hook works for both SaaS (full chain) and Self-Managed (partial or single
31
35
  * crumb) without any environment flag.
32
36
  */
@@ -1,3 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  /*
2
3
  * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
3
4
  * under one or more contributor license agreements. Licensed under a commercial license.
@@ -5,7 +6,8 @@
5
6
  */
6
7
  import { Building, CloudApp } from '@carbon/react/icons/index.esm.js';
7
8
  import { useMemo } from 'react';
8
- import { APP_ICONS, getReadableAppName } from '../../utils/camunda.js';
9
+ import { APP_ICONS, getEndpointForApp, getReadableAppName, isAdminApp, resolveAdminAppName, } from '../../utils/camunda.js';
10
+ import { C3ClusterTag } from '../c3-cluster-tag/c3-cluster-tag.js';
9
11
  import { useC3Profile } from '../c3-user-configuration/c3-profile-provider/c3-profile-provider.js';
10
12
  import { useC3UserConfiguration } from '../c3-user-configuration/c3-user-configuration-provider.js';
11
13
  /**
@@ -20,77 +22,195 @@ const SWITCHABLE_APPS = [
20
22
  'optimize',
21
23
  'admin',
22
24
  ];
25
+ /** Healthy on the cluster, using zeebe as the admin/identity health proxy. */
26
+ function isAppHealthyOnCluster(cluster, app) {
27
+ const healthKey = isAdminApp(app) ? 'zeebe' : app;
28
+ return cluster.status?.[healthKey] === 'Healthy';
29
+ }
30
+ /**
31
+ * Replaces the path of `modelerEndpoint` with `/org/{orgId}`. Used to derive
32
+ * the modeler URL for sibling orgs from the current cluster's modeler
33
+ * endpoint. Assumes all modeler URLs share the same host across orgs (SaaS
34
+ * pattern).
35
+ */
36
+ function buildModelerOrgUrl(modelerEndpoint, orgId) {
37
+ try {
38
+ const url = new URL(modelerEndpoint);
39
+ url.pathname = `/org/${orgId}`;
40
+ url.search = '';
41
+ return url.toString();
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
23
47
  /**
24
48
  * Builds the standard breadcrumb chain for a cluster-scoped Camunda webapp:
25
49
  *
26
- * [Org] > [Cluster] > [App ▾]
50
+ * [Org] > [Cluster] > [App ▾]
27
51
  *
28
- * - **Org crumb**: shown when org data is available (SaaS). Links back to
29
- * web modeler at the org level.
30
- * - **Cluster crumb**: shown when cluster data is available. Links back to
31
- * web modeler / console at the cluster level.
32
- * - **App crumb**: always shown. Includes a dropdown to switch between
33
- * sibling apps on the same cluster (filtered to apps with endpoints).
52
+ * - **Org crumb**: shown when org data is available (SaaS). Label links back
53
+ * to web modeler for the current org. Dropdown switches orgs and lands on
54
+ * web modeler scoped to the selected org. Dropdown only renders when there
55
+ * is at least one sibling org and the current cluster exposes a modeler
56
+ * endpoint to derive sibling URLs from.
57
+ * - **Cluster crumb**: shown when cluster data is available. Label links back
58
+ * to web modeler / console at the cluster level. Dropdown switches clusters
59
+ * while staying on `currentApp`, filtered to clusters that expose an
60
+ * endpoint for that app (alias-aware via `getEndpointForApp`).
61
+ * - **App crumb**: always shown. Dropdown switches between sibling apps on
62
+ * the same cluster (filtered to apps with endpoints).
34
63
  *
35
- * Crumbs are omitted when the underlying data is unavailable, so the same
64
+ * Each dropdown follows the breadcrumb convention: clicking the label
65
+ * navigates, clicking the chevron opens the dropdown. Items are listed in
66
+ * source order with the current entry marked `isSelected`. Crumbs and
67
+ * dropdowns are omitted when the underlying data is unavailable, so the same
36
68
  * hook works for both SaaS (full chain) and Self-Managed (partial or single
37
69
  * crumb) without any environment flag.
38
70
  */
39
71
  export function useClusterWebappBreadcrumbs(options) {
40
- const { currentApp, resolveLinkProps = (url) => ({ href: url }) } = options;
41
- const { clusters, activeOrg } = useC3Profile();
72
+ const { currentApp, webappLinks } = options;
73
+ const { clusters, activeOrg, orgs } = useC3Profile();
42
74
  const config = useC3UserConfiguration();
43
75
  const currentClusterUuid = config.currentClusterUuid;
44
76
  const currentCluster = useMemo(() => clusters?.find((c) => c.uuid === currentClusterUuid) ?? null, [clusters, currentClusterUuid]);
45
77
  return useMemo(() => {
46
78
  const crumbs = [];
79
+ const modelerUrl = webappLinks?.modeler ?? currentCluster?.endpoints?.modeler;
47
80
  // ── Org crumb ──────────────────────────────────────────────────────────
48
81
  if (activeOrg) {
49
- const modelerUrl = currentCluster?.endpoints?.modeler;
82
+ const allOrgs = orgs ?? [];
83
+ const hasSiblings = allOrgs.filter((o) => o.uuid !== activeOrg.uuid).length > 0;
84
+ const orgDropdownItems = hasSiblings && modelerUrl
85
+ ? allOrgs.flatMap((o) => {
86
+ const isCurrent = o.uuid === activeOrg.uuid;
87
+ if (isCurrent) {
88
+ return [
89
+ {
90
+ key: o.uuid,
91
+ label: o.name,
92
+ icon: Building,
93
+ isSelected: true,
94
+ },
95
+ ];
96
+ }
97
+ const href = buildModelerOrgUrl(modelerUrl, o.uuid);
98
+ if (!href)
99
+ return [];
100
+ return [
101
+ {
102
+ key: o.uuid,
103
+ label: o.name,
104
+ icon: Building,
105
+ isSelected: false,
106
+ linkProps: { href },
107
+ },
108
+ ];
109
+ })
110
+ : undefined;
50
111
  crumbs.push({
51
112
  key: 'org',
52
113
  label: activeOrg.name,
53
114
  icon: Building,
54
- ...(modelerUrl
55
- ? { linkProps: resolveLinkProps(modelerUrl, 'org') }
115
+ ...(modelerUrl ? { linkProps: { href: modelerUrl } } : {}),
116
+ ...(orgDropdownItems
117
+ ? {
118
+ dropdownTitle: 'Switch organization',
119
+ dropdownAriaLabel: 'Switch organization',
120
+ dropdownItems: orgDropdownItems,
121
+ }
56
122
  : {}),
57
123
  });
58
124
  }
59
125
  // ── Cluster crumb ──────────────────────────────────────────────────────
60
126
  if (currentCluster) {
61
- const modelerUrl = currentCluster.endpoints?.modeler;
62
127
  const clusterUrl = modelerUrl
63
128
  ? `${modelerUrl}?fromCluster=${currentCluster.uuid}`
64
129
  : undefined;
130
+ const reachableClusters = (clusters ?? []).filter((c) => c.uuid === currentCluster.uuid ||
131
+ (getEndpointForApp(currentApp, c.endpoints) &&
132
+ isAppHealthyOnCluster(c, currentApp)));
133
+ const hasSiblings = reachableClusters.filter((c) => c.uuid !== currentCluster.uuid).length >
134
+ 0;
135
+ const clusterDropdownItems = hasSiblings
136
+ ? reachableClusters.map((c) => {
137
+ const isCurrent = c.uuid === currentCluster.uuid;
138
+ const stage = c.labels?.camunda?.[0];
139
+ return {
140
+ key: c.uuid,
141
+ label: c.name,
142
+ icon: CloudApp,
143
+ isSelected: isCurrent,
144
+ ...(isCurrent
145
+ ? {}
146
+ : {
147
+ linkProps: {
148
+ href: getEndpointForApp(currentApp, c.endpoints) ?? '',
149
+ },
150
+ }),
151
+ ...(stage
152
+ ? { trailingElement: _jsx(C3ClusterTag, { stage: stage, subtle: true }) }
153
+ : {}),
154
+ };
155
+ })
156
+ : undefined;
65
157
  crumbs.push({
66
158
  key: 'cluster',
67
159
  label: currentCluster.name,
68
160
  icon: CloudApp,
69
- ...(clusterUrl
70
- ? { linkProps: resolveLinkProps(clusterUrl, 'cluster') }
161
+ ...(clusterUrl ? { linkProps: { href: clusterUrl } } : {}),
162
+ ...(clusterDropdownItems
163
+ ? {
164
+ dropdownTitle: 'Switch cluster',
165
+ dropdownAriaLabel: 'Switch cluster',
166
+ dropdownItems: clusterDropdownItems,
167
+ }
71
168
  : {}),
72
169
  });
73
170
  }
74
171
  // ── App crumb ──────────────────────────────────────────────────────────
75
- const appLabel = getReadableAppName(currentApp);
76
- const appIcon = APP_ICONS[currentApp];
77
- const siblingApps = SWITCHABLE_APPS.filter((app) => app !== currentApp && currentCluster?.endpoints?.[app]);
78
- const dropdownItems = siblingApps.length > 0
79
- ? [
80
- {
81
- key: currentApp,
82
- label: appLabel,
83
- icon: appIcon,
84
- isSelected: true,
85
- },
86
- ...siblingApps.map((app) => ({
172
+ // Resolve label/icon via the same admin↔identity aliasing the sidebar
173
+ // entries use (`useClusterSidebarEntries`), so older clusters that expose
174
+ // only `identity` render as "Identity" instead of disappearing.
175
+ // `SWITCHABLE_APPS` carries only the canonical `admin` key; consumers
176
+ // that pass `currentApp: 'identity'` (legacy code path) still match the
177
+ // switcher row via this normalisation, otherwise the current entry
178
+ // would be missing from the dropdown and nothing would be selected.
179
+ const switcherKey = isAdminApp(currentApp)
180
+ ? 'admin'
181
+ : currentApp;
182
+ const resolvedCurrentApp = isAdminApp(currentApp) && currentCluster
183
+ ? resolveAdminAppName(currentCluster)
184
+ : currentApp;
185
+ const appLabel = getReadableAppName(resolvedCurrentApp);
186
+ const appIcon = APP_ICONS[resolvedCurrentApp];
187
+ const sourceEndpoints = currentCluster?.endpoints ?? webappLinks;
188
+ const reachableApps = sourceEndpoints
189
+ ? SWITCHABLE_APPS.filter((app) => app === switcherKey ||
190
+ (getEndpointForApp(app, sourceEndpoints) &&
191
+ (!currentCluster || isAppHealthyOnCluster(currentCluster, app))))
192
+ : [switcherKey];
193
+ const hasSiblingApps = reachableApps.filter((app) => app !== switcherKey).length > 0;
194
+ const dropdownItems = hasSiblingApps && sourceEndpoints
195
+ ? reachableApps.map((app) => {
196
+ const isCurrent = app === switcherKey;
197
+ const resolvedApp = isAdminApp(app) && currentCluster
198
+ ? resolveAdminAppName(currentCluster)
199
+ : app;
200
+ return {
87
201
  key: app,
88
- label: getReadableAppName(app),
89
- icon: APP_ICONS[app],
90
- isSelected: false,
91
- linkProps: resolveLinkProps(currentCluster?.endpoints[app] ?? '', app),
92
- })),
93
- ]
202
+ label: getReadableAppName(resolvedApp),
203
+ icon: APP_ICONS[resolvedApp],
204
+ isSelected: isCurrent,
205
+ ...(isCurrent
206
+ ? {}
207
+ : {
208
+ linkProps: {
209
+ href: getEndpointForApp(app, sourceEndpoints) ?? '',
210
+ },
211
+ }),
212
+ };
213
+ })
94
214
  : undefined;
95
215
  crumbs.push({
96
216
  key: 'app',
@@ -105,5 +225,5 @@ export function useClusterWebappBreadcrumbs(options) {
105
225
  : {}),
106
226
  });
107
227
  return crumbs;
108
- }, [activeOrg, currentCluster, currentApp, resolveLinkProps]);
228
+ }, [activeOrg, orgs, clusters, currentCluster, currentApp, webappLinks]);
109
229
  }
@@ -22,5 +22,5 @@ export type C3ProfileContextValue = {
22
22
  export declare const C3ProfileContext: React.Context<C3ProfileContextValue>;
23
23
  export declare const C3ProfileProvider: ({ children }: {
24
24
  children: JSX.Element;
25
- }) => import("react/jsx-runtime").JSX.Element;
25
+ }) => JSX.Element;
26
26
  export declare const useC3Profile: () => C3ProfileContextValue;
@@ -5,7 +5,7 @@ type C3ClusterUpdateManagerProps = {
5
5
  stage: Stage;
6
6
  userToken: string;
7
7
  };
8
- declare const C3ClusterUpdateManager: ({ children, stage, userToken, }: C3ClusterUpdateManagerProps) => import("react/jsx-runtime").JSX.Element;
8
+ declare const C3ClusterUpdateManager: ({ children, stage, userToken, }: C3ClusterUpdateManagerProps) => React.JSX.Element;
9
9
  declare const useClusterUpdate: ({ onUpdate }: {
10
10
  onUpdate: (data: any) => void;
11
11
  }) => void;
@@ -15,8 +15,8 @@ export { C3LicenseTag, type C3LicenseTagProps, } from './components/c3-license-t
15
15
  export { default as C3Navigation } from './components/c3-navigation';
16
16
  export { C3NavigationAppProps, C3NavigationElementProps, C3NavigationNavBarElement, C3NavigationNavBarProps, C3NavigationNavBarSubElement, C3NavigationProps, } from './components/c3-navigation/c3-navigation.types';
17
17
  export { C3NavigationSideBarBaseProps } from './components/c3-navigation/c3-navigation-sidebar/c3-navigation-sidebar.types';
18
- export type { AppProps as C3NavV2AppProps, BreadcrumbDescriptor, BreadcrumbDropdownItem, BreadcrumbSegment, C3NavigationV2Props, C3NotificationsPanelProps, C3UserPanelProps, GlobalActionButton, GroupDescriptor, GroupItemDescriptor, HelpToolOptions, ItemDescriptor, LinkComponent, LinkProps, NotificationsToolOptions, SectionDescriptor, SidebarGroup, SidebarGroupItem, SidebarItem, SidebarNode, SidebarNodeDescriptor, SidebarProps, SidebarSection, ToolDescriptor, UseC3NavigationV2Options, UseC3NavigationV2Return, UseCamundaToolsOptions, UseClusterSidebarEntriesOptions, UserToolOptions, } from './components/c3-navigation-v2';
19
- export { C3BreadcrumbBar as preview_C3BreadcrumbBar, C3NavigationV2 as preview_C3NavigationV2, C3NotificationsPanel as preview_C3NotificationsPanel, C3Sidebar as preview_C3Sidebar, C3ToolsArea as preview_C3ToolsArea, C3UserPanel as preview_C3UserPanel, useC3NavigationV2 as preview_useC3NavigationV2, useCamundaTools as preview_useCamundaTools, useClusterSidebarEntries as preview_useClusterSidebarEntries, useClusterWebappBreadcrumbs as preview_useClusterWebappBreadcrumbs, } from './components/c3-navigation-v2';
18
+ export type { AppProps as C3NavV2AppProps, BreadcrumbDescriptor, BreadcrumbDropdownItem, BreadcrumbSegment, C3NavigationV2Props, C3NotificationsPanelLabels, C3NotificationsPanelProps, C3ThemeSelectorLabels, C3ThemeSelectorProps, C3UserPanelLabels, C3UserPanelProps, GlobalActionButton, GroupDescriptor, GroupItemDescriptor, HelpToolOptions, ItemDescriptor, LinkComponent, LinkProps, NotificationsToolOptions, SectionDescriptor, SidebarGroup, SidebarGroupItem, SidebarItem, SidebarLabels, SidebarNode, SidebarNodeDescriptor, SidebarProps, SidebarSection, ToolDescriptor, UseC3NavigationV2Options, UseC3NavigationV2Return, UseCamundaToolsOptions, UseClusterSidebarEntriesOptions, UserToolOptions, } from './components/c3-navigation-v2';
19
+ export { C3BreadcrumbBar as preview_C3BreadcrumbBar, C3NavigationV2 as preview_C3NavigationV2, C3NotificationsPanel as preview_C3NotificationsPanel, C3Sidebar as preview_C3Sidebar, C3ThemeSelector, C3ToolsArea as preview_C3ToolsArea, C3UserPanel as preview_C3UserPanel, useC3NavigationV2 as preview_useC3NavigationV2, useCamundaTools as preview_useCamundaTools, useClusterSidebarEntries as preview_useClusterSidebarEntries, useClusterWebappBreadcrumbs as preview_useClusterWebappBreadcrumbs, } from './components/c3-navigation-v2';
20
20
  export { C3BreadcrumbProps } from './components/c3-page/c3-breadcrumb/c3-breadcrumb.types';
21
21
  export { C3Page } from './components/c3-page/c3-page';
22
22
  export { C3PageProps } from './components/c3-page/c3-page.types';
@@ -13,7 +13,7 @@ export { C3HelpCenter } from './components/c3-help-center/c3-help-center.js';
13
13
  export { useC3HelpCenter } from './components/c3-help-center/c3-help-center-provider.js';
14
14
  export { C3LicenseTag, } from './components/c3-license-tag/index.js';
15
15
  export { default as C3Navigation } from './components/c3-navigation/index.js';
16
- export { C3BreadcrumbBar as preview_C3BreadcrumbBar, C3NavigationV2 as preview_C3NavigationV2, C3NotificationsPanel as preview_C3NotificationsPanel, C3Sidebar as preview_C3Sidebar, C3ToolsArea as preview_C3ToolsArea, C3UserPanel as preview_C3UserPanel, useC3NavigationV2 as preview_useC3NavigationV2, useCamundaTools as preview_useCamundaTools, useClusterSidebarEntries as preview_useClusterSidebarEntries, useClusterWebappBreadcrumbs as preview_useClusterWebappBreadcrumbs, } from './components/c3-navigation-v2/index.js';
16
+ export { C3BreadcrumbBar as preview_C3BreadcrumbBar, C3NavigationV2 as preview_C3NavigationV2, C3NotificationsPanel as preview_C3NotificationsPanel, C3Sidebar as preview_C3Sidebar, C3ThemeSelector, C3ToolsArea as preview_C3ToolsArea, C3UserPanel as preview_C3UserPanel, useC3NavigationV2 as preview_useC3NavigationV2, useCamundaTools as preview_useCamundaTools, useClusterSidebarEntries as preview_useClusterSidebarEntries, useClusterWebappBreadcrumbs as preview_useClusterWebappBreadcrumbs, } from './components/c3-navigation-v2/index.js';
17
17
  export { C3Page } from './components/c3-page/c3-page.js';
18
18
  export { C3ResponsiveStack } from './components/c3-responsive-stack/c3-responsive-stack.js';
19
19
  export { useC3Profile } from './components/c3-user-configuration/c3-profile-provider/c3-profile-provider.js';
@@ -25,7 +25,7 @@ export declare const getReadableAppName: (appName: CamundaApp) => string;
25
25
  /**
26
26
  * Gets the endpoint URL for an app, handling identity/admin aliasing.
27
27
  */
28
- export declare const getEndpointForApp: (app: CamundaApp, endpoints: Cluster["endpoints"]) => string | undefined;
28
+ export declare const getEndpointForApp: (app: CamundaApp, endpoints: Partial<Cluster["endpoints"]>) => string | undefined;
29
29
  /**
30
30
  * Checks if user has read permission for an app, handling identity/admin aliasing.
31
31
  */