@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
@@ -6,8 +6,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  */
7
7
  import { Popover, PopoverContent } from '@carbon/react';
8
8
  import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, } from '@carbon/react/icons/index.esm.js';
9
- import { useEffect, useRef, useState } from 'react';
10
- import styled from 'styled-components';
9
+ import { createContext, useContext, useEffect, useRef, useState, } from 'react';
10
+ import styled, { css } from 'styled-components';
11
+ const SidebarLabelsContext = createContext({
12
+ groupToggleAria: ({ label, isExpanded }) => isExpanded ? `Collapse ${label}` : `Expand ${label}`,
13
+ });
14
+ const useSidebarLabels = () => useContext(SidebarLabelsContext);
11
15
  const SidebarNav = styled.nav `
12
16
  position: fixed;
13
17
  top: 3rem;
@@ -36,13 +40,12 @@ const NavButton = styled.button `
36
40
  flex-wrap: nowrap;
37
41
  gap: var(--cds-spacing-04);
38
42
  width: 100%;
39
- min-height: 2.5rem;
43
+ height: var(--cds-spacing-09);
44
+ box-sizing: border-box;
40
45
  padding: ${(p) => {
41
46
  if (p.$depth > 0)
42
- return `0.625rem var(--cds-spacing-05) 0.625rem ${0.75 + p.$depth * 1.75}rem`;
43
- return p.$isExpanded
44
- ? 'var(--cds-spacing-04) var(--cds-spacing-05)'
45
- : 'var(--cds-spacing-04)';
47
+ return `0 var(--cds-spacing-05) 0 ${0.75 + p.$depth * 1.75}rem`;
48
+ return p.$isExpanded ? '0 var(--cds-spacing-05)' : '0';
46
49
  }};
47
50
  justify-content: ${(p) => (p.$isExpanded ? 'flex-start' : 'center')};
48
51
  background: ${(p) => (p.$isActive ? 'var(--cds-layer-selected)' : 'transparent')};
@@ -76,30 +79,43 @@ const NavLabel = styled.span `
76
79
  text-overflow: ellipsis;
77
80
  white-space: nowrap;
78
81
  `;
82
+ /**
83
+ * Container row for `group` (pure section) and `group-item` (clickable +
84
+ * expandable) variants. `$clickable` adds the navigable affordance:
85
+ * hover background + transition. `$isActive` flips the selected styling.
86
+ */
79
87
  const GroupHeader = styled.div `
80
88
  display: flex;
81
89
  align-items: center;
82
90
  flex-wrap: nowrap;
83
91
  width: 100%;
84
- min-height: 2.5rem;
92
+ height: var(--cds-spacing-09);
93
+ box-sizing: border-box;
94
+ padding-right: var(--cds-spacing-04);
95
+ gap: var(--cds-spacing-03);
85
96
  overflow: hidden;
86
97
  background: ${(p) => (p.$isActive ? 'var(--cds-layer-selected)' : 'transparent')};
87
98
  border-left: ${(p) => (p.$isActive ? '3px solid var(--cds-border-interactive)' : '3px solid transparent')};
88
- transition: background 0.15s, color 0.15s;
89
99
 
90
- &:hover:not(:has(button[data-expand]:hover)) {
91
- background: ${(p) => (p.$isActive ? 'var(--cds-layer-selected)' : 'var(--cds-layer-hover)')};
92
- }
100
+ ${(p) => p.$clickable &&
101
+ css `
102
+ transition: background 0.15s, color 0.15s;
103
+
104
+ &:hover:not(:has(button[data-expand]:hover)) {
105
+ background: ${p.$isActive ? 'var(--cds-layer-selected)' : 'var(--cds-layer-hover)'};
106
+ }
107
+ `}
93
108
  `;
94
109
  const GroupLabelButton = styled.button `
95
110
  display: flex;
96
111
  align-items: center;
112
+ align-self: stretch;
97
113
  flex: 1;
98
114
  min-width: 0;
99
115
  padding: ${(p) => {
100
116
  if (p.$depth > 0)
101
- return `var(--cds-spacing-04) 0 var(--cds-spacing-04) ${0.75 + p.$depth * 1.75}rem`;
102
- return 'var(--cds-spacing-04) 0 var(--cds-spacing-04) var(--cds-spacing-05)';
117
+ return `0 0 0 ${0.75 + p.$depth * 1.75}rem`;
118
+ return '0 0 0 var(--cds-spacing-05)';
103
119
  }};
104
120
  background: transparent;
105
121
  border: none;
@@ -126,8 +142,11 @@ const ExpandButton = styled.button `
126
142
  display: flex;
127
143
  align-items: center;
128
144
  justify-content: center;
129
- padding: var(--cds-spacing-02);
130
- margin-right: var(--cds-spacing-04);
145
+ flex-shrink: 0;
146
+ align-self: center;
147
+ width: var(--cds-spacing-07);
148
+ height: var(--cds-spacing-07);
149
+ padding: 0;
131
150
  background: transparent;
132
151
  border: none;
133
152
  border-radius: 4px;
@@ -144,52 +163,22 @@ const ExpandButton = styled.button `
144
163
  outline-offset: -2px;
145
164
  }
146
165
  `;
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
166
  const PlainGroupLabel = styled.span `
157
167
  display: flex;
158
168
  align-items: center;
169
+ align-self: stretch;
159
170
  flex: 1;
160
171
  min-width: 0;
161
172
  padding: ${(p) => {
162
173
  if (p.$depth > 0)
163
- return `var(--cds-spacing-04) 0 var(--cds-spacing-04) ${0.75 + p.$depth * 1.75}rem`;
164
- return 'var(--cds-spacing-04) 0 var(--cds-spacing-04) var(--cds-spacing-05)';
174
+ return `0 0 0 ${0.75 + p.$depth * 1.75}rem`;
175
+ return '0 0 0 var(--cds-spacing-05)';
165
176
  }};
166
177
  gap: var(--cds-spacing-04);
167
178
  color: var(--cds-text-secondary);
168
179
  font-size: 0.875rem;
169
180
  font-weight: 400;
170
181
  `;
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
182
  const StyledPopover = styled(Popover) `
194
183
  display: block;
195
184
 
@@ -200,7 +189,9 @@ const StyledPopover = styled(Popover) `
200
189
  white-space: nowrap;
201
190
  }
202
191
  `;
203
- const CollapsedItemTooltip = ({ label, children, }) => {
192
+ // `enabled={false}` keeps the wrapper but skips the popover. Lets a parent
193
+ // button keep its focus across state toggles (see the collapse button below).
194
+ const CollapsedItemTooltip = ({ label, enabled = true, children, }) => {
204
195
  const [open, setOpen] = useState(false);
205
196
  const timerRef = useRef(null);
206
197
  const handleMouseEnter = () => {
@@ -217,7 +208,7 @@ const CollapsedItemTooltip = ({ label, children, }) => {
217
208
  if (timerRef.current)
218
209
  clearTimeout(timerRef.current);
219
210
  }, []);
220
- return (_jsxs(StyledPopover, { open: open, align: 'right', highContrast: true, dropShadow: false, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [children, _jsx(PopoverContent, { children: label })] }));
211
+ 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
212
  };
222
213
  const SectionDivider = styled.div `
223
214
  border-top: ${(p) => (p.$hideTopDivider ? 'none' : '1px solid var(--cds-border-subtle-01)')};
@@ -248,10 +239,9 @@ const CollapseButton = styled.button `
248
239
  flex-wrap: nowrap;
249
240
  gap: var(--cds-spacing-04);
250
241
  width: 100%;
251
- min-height: 2.5rem;
252
- padding: ${(p) => p.$isExpanded
253
- ? 'var(--cds-spacing-04) var(--cds-spacing-05)'
254
- : 'var(--cds-spacing-04)'};
242
+ height: var(--cds-spacing-09);
243
+ box-sizing: border-box;
244
+ padding: ${(p) => (p.$isExpanded ? '0 var(--cds-spacing-05)' : '0')};
255
245
  justify-content: ${(p) => (p.$isExpanded ? 'flex-start' : 'center')};
256
246
  background: transparent;
257
247
  border: none;
@@ -323,7 +313,7 @@ const GroupItemNode = ({ node, sidebarExpanded, depth, linkComponent, }) => {
323
313
  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
314
  return (_jsx(CollapsedItemTooltip, { label: node.label, children: collapsed }));
325
315
  }
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) => {
316
+ 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
317
  e.stopPropagation();
328
318
  node.onToggleExpand?.();
329
319
  }, "aria-label": node.isExpanded
@@ -333,13 +323,15 @@ const GroupItemNode = ({ node, sidebarExpanded, depth, linkComponent, }) => {
333
323
  };
334
324
  const GroupNode = ({ node, sidebarExpanded, depth, linkComponent, }) => {
335
325
  const Icon = node.icon;
326
+ const { groupToggleAria } = useSidebarLabels();
336
327
  if (!sidebarExpanded) {
337
328
  const collapsed = (_jsx(NavButton, { "$isActive": false, "$isExpanded": false, "$depth": 0, onClick: node.onToggleExpand, children: _jsx(Icon, { size: 20, style: { flexShrink: 0 } }) }));
338
329
  return (_jsx(CollapsedItemTooltip, { label: node.label, children: collapsed }));
339
330
  }
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
341
- ? `Collapse ${node.label}`
342
- : `Expand ${node.label}`, "aria-expanded": !!node.isExpanded, children: node.isExpanded ? (_jsx(ChevronUp, { size: 16 })) : (_jsx(ChevronDown, { size: 16 })) }))] }), node.isExpanded &&
331
+ 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": groupToggleAria({
332
+ label: node.label,
333
+ isExpanded: !!node.isExpanded,
334
+ }), "aria-expanded": !!node.isExpanded, children: node.isExpanded ? (_jsx(ChevronUp, { size: 16 })) : (_jsx(ChevronDown, { size: 16 })) }))] }), node.isExpanded &&
343
335
  node.children.map((child) => (_jsx(SidebarNodeComponent, { node: child, sidebarExpanded: sidebarExpanded, depth: depth + 1, linkComponent: linkComponent }, child.key)))] }));
344
336
  };
345
337
  const SectionNode = ({ node, sidebarExpanded, linkComponent, hideTopDivider, eatScrollPadding, tight, }) => (_jsxs(SectionDivider, { "$hideTopDivider": hideTopDivider, "$eatScrollPadding": eatScrollPadding, "$tight": tight, children: [sidebarExpanded && node.title && _jsx(SectionTitle, { children: node.title }), node.children.map((child) => (_jsx(SidebarNodeComponent, { node: child, sidebarExpanded: sidebarExpanded, depth: 0, linkComponent: linkComponent }, child.key)))] }));
@@ -364,31 +356,37 @@ const SidebarNodeComponent = ({ node, sidebarExpanded, depth, linkComponent, hid
364
356
  * Loose items (not wrapped in a section) are valid and behave as an implicit
365
357
  * untitled group at the top.
366
358
  */
367
- export const C3Sidebar = ({ ariaLabel, children: nodes, isExpanded = true, onToggleExpanded, expandedWidth = '16rem', collapsedWidth = '3rem', linkComponent, }) => {
359
+ export const C3Sidebar = ({ ariaLabel, children: nodes, isExpanded = true, onToggleExpanded, expandedWidth = '16rem', collapsedWidth = '3rem', linkComponent, labels, }) => {
368
360
  const width = isExpanded ? expandedWidth : collapsedWidth;
369
361
  const prunedNodes = pruneChildren(nodes);
370
- return (_jsxs(SidebarNav, { "$width": width, "aria-label": ariaLabel, children: [_jsx(ScrollArea, { "$sidebarExpanded": isExpanded, children: (() => {
371
- let sectionSeen = false;
372
- let prevSectionCompact = false;
373
- let hasNonSectionNodes = false;
374
- return prunedNodes.map((node) => {
375
- let hideTopDivider = false;
376
- let eatScrollPadding = false;
377
- let tight = false;
378
- if (node.type === 'section') {
379
- const isFirst = !sectionSeen;
380
- hideTopDivider =
381
- (isFirst && !hasNonSectionNodes) || !!node.compact;
382
- eatScrollPadding =
383
- isFirst && !hasNonSectionNodes && !!node.compact;
384
- tight = prevSectionCompact;
385
- sectionSeen = true;
386
- prevSectionCompact = !!node.compact;
387
- }
388
- else {
389
- hasNonSectionNodes = true;
390
- }
391
- return (_jsx(SidebarNodeComponent, { node: node, sidebarExpanded: isExpanded, depth: 0, linkComponent: linkComponent, hideTopDivider: hideTopDivider, eatScrollPadding: eatScrollPadding, tight: tight }, node.key));
392
- });
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 } }) }) })) }))] }));
362
+ const collapseLabel = labels?.collapse ?? 'Collapse';
363
+ const expandLabel = labels?.expand ?? 'Expand';
364
+ const toggleAria = labels?.toggleAriaLabel ??
365
+ ((expanded) => (expanded ? 'Collapse sidebar' : 'Expand sidebar'));
366
+ const groupToggleAria = labels?.groupToggleAriaLabel ??
367
+ (({ label, isExpanded: e }) => e ? `Collapse ${label}` : `Expand ${label}`);
368
+ return (_jsx(SidebarLabelsContext.Provider, { value: { groupToggleAria }, children: _jsxs(SidebarNav, { "$width": width, "aria-label": ariaLabel, children: [_jsx(ScrollArea, { "$sidebarExpanded": isExpanded, children: (() => {
369
+ let sectionSeen = false;
370
+ let prevSectionCompact = false;
371
+ let hasNonSectionNodes = false;
372
+ return prunedNodes.map((node) => {
373
+ let hideTopDivider = false;
374
+ let eatScrollPadding = false;
375
+ let tight = false;
376
+ if (node.type === 'section') {
377
+ const isFirst = !sectionSeen;
378
+ hideTopDivider =
379
+ (isFirst && !hasNonSectionNodes) || !!node.compact;
380
+ eatScrollPadding =
381
+ isFirst && !hasNonSectionNodes && !!node.compact;
382
+ tight = prevSectionCompact;
383
+ sectionSeen = true;
384
+ prevSectionCompact = !!node.compact;
385
+ }
386
+ else {
387
+ hasNonSectionNodes = true;
388
+ }
389
+ return (_jsx(SidebarNodeComponent, { node: node, sidebarExpanded: isExpanded, depth: 0, linkComponent: linkComponent, hideTopDivider: hideTopDivider, eatScrollPadding: eatScrollPadding, tight: tight }, node.key));
390
+ });
391
+ })() }), onToggleExpanded && (_jsx(CollapseToggleArea, { children: _jsx(CollapsedItemTooltip, { label: expandLabel, enabled: !isExpanded, children: _jsxs(CollapseButton, { "$isExpanded": isExpanded, onClick: onToggleExpanded, "aria-label": toggleAria(isExpanded), "aria-expanded": isExpanded, children: [isExpanded ? (_jsx(ChevronLeft, { size: 20, style: { flexShrink: 0 } })) : (_jsx(ChevronRight, { size: 20, style: { flexShrink: 0 } })), isExpanded && _jsx(NavLabel, { children: collapseLabel })] }) }) }))] }) }));
394
392
  };
@@ -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
  };
@@ -1,14 +1,16 @@
1
1
  export type { CamundaApp } from '../../utils/camunda.types';
2
2
  export { C3BreadcrumbBar } from './c3-breadcrumb-bar';
3
3
  export { C3NavigationV2 } from './c3-navigation-v2';
4
- export type { AppProps, BreadcrumbAction, BreadcrumbDropdownItem, BreadcrumbSegment, C3NavigationV2Props, GlobalActionButton, LinkComponent, LinkProps, SidebarGroup, SidebarGroupItem, SidebarItem, SidebarNode, SidebarProps, SidebarSection, ToolDescriptor, } from './c3-navigation-v2.types';
4
+ export type { AppProps, BreadcrumbAction, BreadcrumbDropdownItem, BreadcrumbSegment, C3NavigationV2Props, GlobalActionButton, LinkComponent, LinkProps, SidebarGroup, SidebarGroupItem, SidebarItem, SidebarLabels, SidebarNode, SidebarProps, SidebarSection, ToolDescriptor, } from './c3-navigation-v2.types';
5
5
  export { C3Sidebar } from './c3-sidebar';
6
6
  export { C3ToolsArea } from './c3-tools-area';
7
7
  export type { C3InfoPanelProps, InfoPanelElement } from './tools/c3-info-panel';
8
8
  export { C3InfoPanel } from './tools/c3-info-panel';
9
- export type { C3NotificationsPanelProps } from './tools/c3-notifications-panel';
9
+ export type { C3NotificationsPanelLabels, C3NotificationsPanelProps, } from './tools/c3-notifications-panel';
10
10
  export { C3NotificationsPanel } from './tools/c3-notifications-panel';
11
- export type { C3UserPanelProps, UserPanelElement, } from './tools/c3-user-panel';
11
+ export type { C3ThemeSelectorLabels, C3ThemeSelectorProps, } from './tools/c3-theme-selector';
12
+ export { C3ThemeSelector } from './tools/c3-theme-selector';
13
+ export type { C3UserPanelLabels, C3UserPanelProps, UserPanelElement, } from './tools/c3-user-panel';
12
14
  export { C3UserPanel } from './tools/c3-user-panel';
13
15
  export type { BreadcrumbDescriptor, BreadcrumbDropdownItemDescriptor, GroupDescriptor, GroupItemDescriptor, ItemDescriptor, SectionDescriptor, SidebarNodeDescriptor, UseC3NavigationV2Options, UseC3NavigationV2Return, } from './use-c3-navigation-v2';
14
16
  export { useC3NavigationV2 } from './use-c3-navigation-v2';
@@ -9,6 +9,7 @@ export { C3Sidebar } from './c3-sidebar.js';
9
9
  export { C3ToolsArea } from './c3-tools-area.js';
10
10
  export { C3InfoPanel } from './tools/c3-info-panel.js';
11
11
  export { C3NotificationsPanel } from './tools/c3-notifications-panel.js';
12
+ export { C3ThemeSelector } from './tools/c3-theme-selector.js';
12
13
  export { C3UserPanel } from './tools/c3-user-panel.js';
13
14
  export { useC3NavigationV2 } from './use-c3-navigation-v2.js';
14
15
  export { useCamundaTools } from './use-camunda-tools.js';
@@ -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
+ };
@@ -6,6 +6,7 @@ export interface InfoPanelElement {
6
6
  }
7
7
  export interface C3InfoPanelProps {
8
8
  elements: InfoPanelElement[];
9
- title?: string;
9
+ /** Defaults to `'Info'`. */
10
+ title?: string | null;
10
11
  }
11
12
  export declare const C3InfoPanel: FC<C3InfoPanelProps>;
@@ -30,4 +30,4 @@ const LinkButton = styled.button `
30
30
  outline-offset: -2px;
31
31
  }
32
32
  `;
33
- export const C3InfoPanel = ({ elements, title = 'Help & Resources', }) => (_jsxs(_Fragment, { children: [_jsx(PanelHeader, { children: _jsx(PanelTitle, { children: title }) }), _jsx(LinkList, { children: elements.map((el) => (_jsx(LinkItem, { children: _jsx(LinkButton, { onClick: el.onClick, children: el.label }) }, el.key))) })] }));
33
+ export const C3InfoPanel = ({ elements, title = 'Info', }) => (_jsxs(_Fragment, { children: [title && (_jsx(PanelHeader, { children: _jsx(PanelTitle, { children: title }) })), _jsx(LinkList, { children: elements.map((el) => (_jsx(LinkItem, { children: _jsx(LinkButton, { onClick: el.onClick, children: el.label }) }, el.key))) })] }));
@@ -1,6 +1,17 @@
1
1
  import { type FC } from 'react';
2
2
  import type { Notification } from '../../../api/notifications';
3
+ export interface C3NotificationsPanelLabels {
4
+ /** Defaults to `'Dismiss all'`. */
5
+ dismissAll?: string;
6
+ /** Defaults to `'No notifications'`. */
7
+ emptyTitle?: string;
8
+ /** Defaults to `'New updates regarding your processes, clusters and more will appear here.'`. */
9
+ emptyDescription?: string;
10
+ }
3
11
  export interface C3NotificationsPanelProps {
4
12
  onLinkClick?: (meta: Notification['meta']) => void;
13
+ /** Defaults to `'Notifications'`. */
14
+ title?: string | null;
15
+ labels?: C3NotificationsPanelLabels;
5
16
  }
6
17
  export declare const C3NotificationsPanel: FC<C3NotificationsPanelProps>;
@@ -28,7 +28,7 @@ const EmptyStateDescription = styled(NotificationDescription) `
28
28
  margin-top: var(--cds-spacing-03);
29
29
  `;
30
30
  const sortDescending = (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
31
- export const C3NotificationsPanel = ({ onLinkClick, }) => {
31
+ export const C3NotificationsPanel = ({ onLinkClick, title = 'Notifications', labels, }) => {
32
32
  const { enabled, notifications, markAllAsRead, dismiss, dismissAll, analytics, } = useContext(C3NotificationContext);
33
33
  // Snapshot uuids that were unread when the panel opened so "new" dots
34
34
  // persist until the panel closes, even after mark-as-read runs.
@@ -41,17 +41,21 @@ export const C3NotificationsPanel = ({ onLinkClick, }) => {
41
41
  if (enabled)
42
42
  analytics('notification-panel-opened');
43
43
  }, []);
44
- return (_jsxs(_Fragment, { children: [_jsxs(PanelHeader, { style: {
44
+ return (_jsxs(_Fragment, { children: [(title || notifications.length > 0) && (_jsxs(PanelHeader, { style: {
45
45
  width: '100%',
46
46
  height: 60,
47
47
  display: 'flex',
48
48
  flexDirection: 'row',
49
- justifyContent: 'space-between',
49
+ // Without a title, the dismiss-all button is the sole row item;
50
+ // flex-end keeps it pinned right instead of drifting left under
51
+ // `space-between`.
52
+ justifyContent: title ? 'space-between' : 'flex-end',
50
53
  alignItems: 'center',
51
- }, children: [_jsx(PanelTitle, { children: "Notifications" }), notifications.length > 0 && (_jsx(DismissAllButton, { kind: 'ghost', size: 'sm', onClick: () => dismissAll(notifications), children: "Dismiss all" }))] }), notifications.length > 0 ? ([...notifications].sort(sortDescending).map((notification) => (_jsx(C3NotificationContainer, { onRead: () => undefined, onDismiss: () => dismiss(notification), originalOnLinkClick: onLinkClick, onLinkClick: () => {
54
+ }, children: [title && _jsx(PanelTitle, { children: title }), notifications.length > 0 && (_jsx(DismissAllButton, { kind: 'ghost', size: 'sm', onClick: () => dismissAll(notifications), children: labels?.dismissAll ?? 'Dismiss all' }))] })), notifications.length > 0 ? ([...notifications].sort(sortDescending).map((notification) => (_jsx(C3NotificationContainer, { onRead: () => undefined, onDismiss: () => dismiss(notification), originalOnLinkClick: onLinkClick, onLinkClick: () => {
52
55
  if (enabled) {
53
56
  analytics('notification-clicked-cta', notification.meta?.identifier);
54
57
  }
55
58
  onLinkClick?.(notification.meta);
56
- }, unread: unreadAtOpen.has(notification.uuid), ...notification }, notification.uuid)))) : (_jsxs(EmptyState, { children: [_jsx(C3BellIcon, { size: 56 }), _jsx(EmptyStateTitle, { children: "No notifications" }), _jsx(EmptyStateDescription, { children: "New updates regarding your processes, clusters and more will appear here." })] }))] }));
59
+ }, unread: unreadAtOpen.has(notification.uuid), ...notification }, notification.uuid)))) : (_jsxs(EmptyState, { children: [_jsx(C3BellIcon, { size: 56 }), _jsx(EmptyStateTitle, { children: labels?.emptyTitle ?? 'No notifications' }), _jsx(EmptyStateDescription, { children: labels?.emptyDescription ??
60
+ 'New updates regarding your processes, clusters and more will appear here.' })] }))] }));
57
61
  };
@@ -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>;