@camunda/camunda-composite-components 0.22.7 → 0.23.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.
- package/lib/esm/package.json +23 -23
- package/lib/esm/src/components/c3-cluster-tag/c3-cluster-tag.d.ts +1 -0
- package/lib/esm/src/components/c3-cluster-tag/c3-cluster-tag.js +24 -3
- package/lib/esm/src/components/c3-cluster-tag/c3-cluster-tag.types.d.ts +1 -0
- package/lib/esm/src/components/c3-license-tag/c3-license-tag.d.ts +17 -0
- package/lib/esm/src/components/c3-license-tag/c3-license-tag.js +79 -0
- package/lib/esm/src/components/c3-license-tag/index.d.ts +1 -0
- package/lib/esm/src/components/c3-license-tag/index.js +6 -0
- package/lib/esm/src/components/c3-navigation/c3-org-sidebar/components.d.ts +3 -4
- package/lib/esm/src/components/c3-navigation-v2/c3-breadcrumb-bar.d.ts +7 -0
- package/lib/esm/src/components/c3-navigation-v2/c3-breadcrumb-bar.js +371 -0
- package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.d.ts +9 -0
- package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.js +72 -0
- package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.types.d.ts +180 -0
- package/lib/esm/src/components/c3-navigation-v2/c3-navigation-v2.types.js +6 -0
- package/lib/esm/src/components/c3-navigation-v2/c3-sidebar.d.ts +8 -0
- package/lib/esm/src/components/c3-navigation-v2/c3-sidebar.js +357 -0
- package/lib/esm/src/components/c3-navigation-v2/c3-tools-area.d.ts +16 -0
- package/lib/esm/src/components/c3-navigation-v2/c3-tools-area.js +99 -0
- package/lib/esm/src/components/c3-navigation-v2/index.d.ts +18 -0
- package/lib/esm/src/components/c3-navigation-v2/index.js +15 -0
- package/lib/esm/src/components/c3-navigation-v2/stories/story-helpers.d.ts +10 -0
- package/lib/esm/src/components/c3-navigation-v2/stories/story-helpers.js +231 -0
- package/lib/esm/src/components/c3-navigation-v2/stories/story-templates.d.ts +11 -0
- package/lib/esm/src/components/c3-navigation-v2/stories/story-templates.js +796 -0
- package/lib/esm/src/components/c3-navigation-v2/tools/c3-info-panel.d.ts +11 -0
- package/lib/esm/src/components/c3-navigation-v2/tools/c3-info-panel.js +33 -0
- package/lib/esm/src/components/c3-navigation-v2/tools/c3-notifications-panel.d.ts +6 -0
- package/lib/esm/src/components/c3-navigation-v2/tools/c3-notifications-panel.js +52 -0
- package/lib/esm/src/components/c3-navigation-v2/tools/c3-user-panel.d.ts +30 -0
- package/lib/esm/src/components/c3-navigation-v2/tools/c3-user-panel.js +125 -0
- package/lib/esm/src/components/c3-navigation-v2/tools/panel-primitives.d.ts +7 -0
- package/lib/esm/src/components/c3-navigation-v2/tools/panel-primitives.js +16 -0
- package/lib/esm/src/components/c3-navigation-v2/use-c3-navigation-v2.d.ts +115 -0
- package/lib/esm/src/components/c3-navigation-v2/use-c3-navigation-v2.js +216 -0
- package/lib/esm/src/components/c3-navigation-v2/use-camunda-tools.d.ts +49 -0
- package/lib/esm/src/components/c3-navigation-v2/use-camunda-tools.js +75 -0
- package/lib/esm/src/components/c3-navigation-v2/use-cluster-webapp-breadcrumbs.d.ts +33 -0
- package/lib/esm/src/components/c3-navigation-v2/use-cluster-webapp-breadcrumbs.js +126 -0
- package/lib/esm/src/index.d.ts +5 -0
- package/lib/esm/src/index.js +3 -0
- package/package.json +23 -23
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { Header, HeaderGlobalAction, HeaderGlobalBar, SkipToContent, } from '@carbon/react';
|
|
8
|
+
import { useEffect } from 'react';
|
|
9
|
+
import styled from 'styled-components';
|
|
10
|
+
import { CamundaLogo } from '../../assets/c3-icons.js';
|
|
11
|
+
import { C3BreadcrumbBar } from './c3-breadcrumb-bar.js';
|
|
12
|
+
import { C3Sidebar } from './c3-sidebar.js';
|
|
13
|
+
import { C3ToolsArea } from './c3-tools-area.js';
|
|
14
|
+
const StyledHeader = styled(Header) `
|
|
15
|
+
background-color: var(--cds-layer-01) !important;
|
|
16
|
+
border-bottom: 1px solid var(--cds-border-subtle) !important;
|
|
17
|
+
box-shadow: none !important;
|
|
18
|
+
z-index: 8001 !important;
|
|
19
|
+
`;
|
|
20
|
+
const LogoSection = styled.a `
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
width: 3rem;
|
|
25
|
+
flex-shrink: 0;
|
|
26
|
+
gap: 0.5rem;
|
|
27
|
+
border-right: 1px solid var(--cds-border-subtle);
|
|
28
|
+
height: 100%;
|
|
29
|
+
text-decoration: none;
|
|
30
|
+
color: var(--cds-text-primary);
|
|
31
|
+
font-size: 0.875rem;
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
`;
|
|
35
|
+
/**
|
|
36
|
+
* Sets CSS custom properties on :root so consumers can derive layout offsets
|
|
37
|
+
* without hard-coding pixel values:
|
|
38
|
+
* --c3-header-height: height of the fixed header (always 3rem)
|
|
39
|
+
* --c3-sidebar-width: current sidebar width (updates on expand/collapse)
|
|
40
|
+
*/
|
|
41
|
+
export const C3NavigationV2 = ({ app, skipToContentTargetId, skipToContentLabel = 'Skip to main content', headerAriaLabel, breadcrumbs, tools, globalActions, sidebar, linkComponent, headerTrailingContent, activeToolKey, onActiveToolChange, }) => {
|
|
42
|
+
const ariaLabel = headerAriaLabel ?? app.ariaLabel;
|
|
43
|
+
const LinkEl = linkComponent ?? 'a';
|
|
44
|
+
const sidebarWidth = sidebar
|
|
45
|
+
? sidebar.isExpanded
|
|
46
|
+
? (sidebar.expandedWidth ?? '16rem')
|
|
47
|
+
: (sidebar.collapsedWidth ?? '3rem')
|
|
48
|
+
: '0px';
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (typeof document === 'undefined')
|
|
51
|
+
return;
|
|
52
|
+
const root = document.documentElement;
|
|
53
|
+
root.style.setProperty('--c3-header-height', '3rem');
|
|
54
|
+
root.style.setProperty('--c3-sidebar-width', sidebarWidth);
|
|
55
|
+
return () => {
|
|
56
|
+
root.style.removeProperty('--c3-header-height');
|
|
57
|
+
root.style.removeProperty('--c3-sidebar-width');
|
|
58
|
+
};
|
|
59
|
+
}, [sidebarWidth]);
|
|
60
|
+
return (_jsxs(_Fragment, { children: [_jsxs(StyledHeader, { "aria-label": ariaLabel, children: [_jsx(SkipToContent, { href: `#${skipToContentTargetId}`, children: skipToContentLabel }), _jsxs(LogoSection, { as: app.linkProps ? LinkEl : 'div', ...(app.linkProps ?? {}), children: [_jsx(CamundaLogo, { "aria-label": 'Camunda' }), app.name && _jsx("span", { children: app.name })] }), breadcrumbs && breadcrumbs.length > 0 && (_jsx(C3BreadcrumbBar, { segments: breadcrumbs, linkComponent: LinkEl })), _jsxs(HeaderGlobalBar, { children: [headerTrailingContent && (_jsx("span", { style: {
|
|
61
|
+
display: 'flex',
|
|
62
|
+
alignItems: 'center',
|
|
63
|
+
padding: '0 0.5rem',
|
|
64
|
+
}, children: headerTrailingContent })), tools && tools.length > 0 && (_jsx(C3ToolsArea, { tools: tools, activeToolKey: activeToolKey, onActiveToolChange: onActiveToolChange })), globalActions?.map((action, index) => {
|
|
65
|
+
if (action.element) {
|
|
66
|
+
return _jsx("span", { children: action.element }, action.key);
|
|
67
|
+
}
|
|
68
|
+
const Icon = action.icon;
|
|
69
|
+
const isLast = index === (globalActions?.length ?? 0) - 1;
|
|
70
|
+
return (_jsx(HeaderGlobalAction, { "aria-label": action.label, onClick: action.onClick, tooltipAlignment: isLast ? 'end' : undefined, children: _jsx(Icon, { size: 20 }) }, action.key));
|
|
71
|
+
})] })] }), sidebar && _jsx(C3Sidebar, { ...sidebar })] }));
|
|
72
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Describes a tool button in the header. If `panel` is provided, clicking the
|
|
4
|
+
* button toggles a slide-over panel. Without `panel`, the button is standalone.
|
|
5
|
+
*/
|
|
6
|
+
export interface ToolDescriptor {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
renderButton: (props: {
|
|
10
|
+
onClick: () => void;
|
|
11
|
+
isActive: boolean;
|
|
12
|
+
}) => ReactNode;
|
|
13
|
+
panel?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export interface LinkProps {
|
|
16
|
+
/** Route path for client-side navigation (e.g. react-router `to`). */
|
|
17
|
+
to?: string;
|
|
18
|
+
/** Standard anchor href for external or full-page navigation. */
|
|
19
|
+
href?: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
type IconComponent = React.ComponentType<{
|
|
23
|
+
size?: number;
|
|
24
|
+
style?: React.CSSProperties;
|
|
25
|
+
}>;
|
|
26
|
+
export type LinkComponent = React.ComponentType<LinkProps & React.RefAttributes<HTMLAnchorElement>> | string;
|
|
27
|
+
export interface BreadcrumbDropdownItem {
|
|
28
|
+
key: string;
|
|
29
|
+
label: string;
|
|
30
|
+
icon?: IconComponent;
|
|
31
|
+
isSelected?: boolean;
|
|
32
|
+
onClick?: () => void;
|
|
33
|
+
linkProps?: LinkProps;
|
|
34
|
+
trailingElement?: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
export interface BreadcrumbAction {
|
|
37
|
+
key: string;
|
|
38
|
+
label: string;
|
|
39
|
+
onClick?: () => void;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
isDanger?: boolean;
|
|
42
|
+
hasDivider?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `linkProps` and `onClick` may coexist: the segment renders as a link
|
|
46
|
+
* (native navigation + middle-click / new tab support) and `onClick` fires
|
|
47
|
+
* on activation — useful for analytics or side-effects alongside navigation.
|
|
48
|
+
* When only `onClick` is set, the segment renders as a button.
|
|
49
|
+
*/
|
|
50
|
+
export interface BreadcrumbSegment {
|
|
51
|
+
key: string;
|
|
52
|
+
label: string;
|
|
53
|
+
icon?: IconComponent;
|
|
54
|
+
onClick?: () => void;
|
|
55
|
+
linkProps?: LinkProps;
|
|
56
|
+
trailingElement?: ReactNode;
|
|
57
|
+
/** Rendered as a sibling next to the segment button (not inside it). Use for overflow menus etc. */
|
|
58
|
+
menuElement?: ReactNode;
|
|
59
|
+
/**
|
|
60
|
+
* Action items rendered as a triple-dot overflow menu beside the segment.
|
|
61
|
+
* Can coexist with `dropdownItems`; both render side-by-side
|
|
62
|
+
* (actions as an overflow menu, dropdown as a chevron selector).
|
|
63
|
+
*/
|
|
64
|
+
actions?: BreadcrumbAction[];
|
|
65
|
+
/**
|
|
66
|
+
* Items for a chevron-triggered dropdown selector beside the segment.
|
|
67
|
+
* Can coexist with `actions`; both render side-by-side
|
|
68
|
+
* (dropdown as a chevron selector, actions as an overflow menu).
|
|
69
|
+
*/
|
|
70
|
+
dropdownItems?: BreadcrumbDropdownItem[];
|
|
71
|
+
dropdownTitle?: string;
|
|
72
|
+
dropdownAriaLabel?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface GlobalActionButton {
|
|
75
|
+
key: string;
|
|
76
|
+
label: string;
|
|
77
|
+
icon: React.ComponentType<{
|
|
78
|
+
size?: number;
|
|
79
|
+
}>;
|
|
80
|
+
onClick?: () => void;
|
|
81
|
+
element?: ReactNode;
|
|
82
|
+
}
|
|
83
|
+
export interface SidebarItem {
|
|
84
|
+
type: 'item';
|
|
85
|
+
key: string;
|
|
86
|
+
label: string;
|
|
87
|
+
icon: IconComponent;
|
|
88
|
+
isActive?: boolean;
|
|
89
|
+
onClick?: () => void;
|
|
90
|
+
linkProps?: LinkProps;
|
|
91
|
+
trailingElement?: ReactNode;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A sidebar node that is both navigable and expandable: clicking the label
|
|
95
|
+
* navigates (via `onClick` / `linkProps`), and a separate chevron toggles
|
|
96
|
+
* the nested `children`. Use when the parent itself has a destination —
|
|
97
|
+
* e.g. a "Projects" item that both navigates to the project list and
|
|
98
|
+
* expands to show recent projects.
|
|
99
|
+
*/
|
|
100
|
+
export interface SidebarGroupItem {
|
|
101
|
+
type: 'group-item';
|
|
102
|
+
key: string;
|
|
103
|
+
label: string;
|
|
104
|
+
icon: IconComponent;
|
|
105
|
+
isActive?: boolean;
|
|
106
|
+
onClick?: () => void;
|
|
107
|
+
linkProps?: LinkProps;
|
|
108
|
+
trailingElement?: ReactNode;
|
|
109
|
+
isExpanded?: boolean;
|
|
110
|
+
onToggleExpand?: () => void;
|
|
111
|
+
children: SidebarNode[];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A sidebar node that is only a container: clicking it expands/collapses
|
|
115
|
+
* its `children`, and it has no destination of its own. Use when the
|
|
116
|
+
* parent is a pure grouping label — e.g. "Administration" with child items
|
|
117
|
+
* for users, roles, settings.
|
|
118
|
+
*/
|
|
119
|
+
export interface SidebarGroup {
|
|
120
|
+
type: 'group';
|
|
121
|
+
key: string;
|
|
122
|
+
label: string;
|
|
123
|
+
icon: IconComponent;
|
|
124
|
+
trailingElement?: ReactNode;
|
|
125
|
+
isExpanded?: boolean;
|
|
126
|
+
onToggleExpand?: () => void;
|
|
127
|
+
children: SidebarNode[];
|
|
128
|
+
}
|
|
129
|
+
export interface SidebarSection {
|
|
130
|
+
type: 'section';
|
|
131
|
+
key: string;
|
|
132
|
+
title?: string;
|
|
133
|
+
children: SidebarNode[];
|
|
134
|
+
/**
|
|
135
|
+
* Compact sections render flush: no own divider or spacing. The next section
|
|
136
|
+
* still gets its divider but without the usual top margin.
|
|
137
|
+
* Use for action rows like "Back to home" at the top of the sidebar.
|
|
138
|
+
*/
|
|
139
|
+
compact?: boolean;
|
|
140
|
+
}
|
|
141
|
+
export type SidebarNode = SidebarItem | SidebarGroupItem | SidebarGroup | SidebarSection;
|
|
142
|
+
export interface SidebarProps {
|
|
143
|
+
ariaLabel: string;
|
|
144
|
+
children: SidebarNode[];
|
|
145
|
+
isExpanded?: boolean;
|
|
146
|
+
onToggleExpanded?: () => void;
|
|
147
|
+
expandedWidth?: string;
|
|
148
|
+
collapsedWidth?: string;
|
|
149
|
+
linkComponent?: LinkComponent;
|
|
150
|
+
}
|
|
151
|
+
export interface AppProps {
|
|
152
|
+
name?: string;
|
|
153
|
+
ariaLabel: string;
|
|
154
|
+
linkProps?: LinkProps;
|
|
155
|
+
}
|
|
156
|
+
export interface C3NavigationV2Props {
|
|
157
|
+
app: AppProps;
|
|
158
|
+
/**
|
|
159
|
+
* `id` of the page's main content element (e.g. `<main id="...">`). The skip
|
|
160
|
+
* link points at `#${skipToContentTargetId}`, so the element must exist and
|
|
161
|
+
* be focusable (`tabindex="-1"` if not natively focusable). Required — the
|
|
162
|
+
* consuming app is responsible for the target; a missing id makes the skip
|
|
163
|
+
* link a dead Tab stop.
|
|
164
|
+
*/
|
|
165
|
+
skipToContentTargetId: string;
|
|
166
|
+
skipToContentLabel?: string;
|
|
167
|
+
headerAriaLabel?: string;
|
|
168
|
+
breadcrumbs?: BreadcrumbSegment[];
|
|
169
|
+
tools?: ToolDescriptor[];
|
|
170
|
+
globalActions?: GlobalActionButton[];
|
|
171
|
+
sidebar?: SidebarProps;
|
|
172
|
+
linkComponent?: LinkComponent;
|
|
173
|
+
/** Content rendered in the right-aligned header area, before the tool buttons. Use for license tags, environment badges, etc. */
|
|
174
|
+
headerTrailingContent?: ReactNode;
|
|
175
|
+
/** Currently active tool panel key (controlled). When omitted, C3ToolsArea manages its own state. */
|
|
176
|
+
activeToolKey?: string | null;
|
|
177
|
+
/** Called when the active tool panel changes (including close via outside click). */
|
|
178
|
+
onActiveToolChange?: (key: string | null) => void;
|
|
179
|
+
}
|
|
180
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import type { SidebarProps } from './c3-navigation-v2.types';
|
|
3
|
+
/**
|
|
4
|
+
* Sidebar navigation panel. Children are `SidebarNode[]`, typically sections.
|
|
5
|
+
* Loose items (not wrapped in a section) are valid and behave as an implicit
|
|
6
|
+
* untitled group at the top.
|
|
7
|
+
*/
|
|
8
|
+
export declare const C3Sidebar: ({ ariaLabel, children: nodes, isExpanded, onToggleExpanded, expandedWidth, collapsedWidth, linkComponent, }: SidebarProps) => JSX.Element;
|
|
@@ -0,0 +1,357 @@
|
|
|
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 { Popover, PopoverContent } from '@carbon/react';
|
|
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';
|
|
11
|
+
const SidebarNav = styled.nav `
|
|
12
|
+
position: fixed;
|
|
13
|
+
top: 3rem;
|
|
14
|
+
left: 0;
|
|
15
|
+
bottom: 0;
|
|
16
|
+
width: ${(p) => p.$width};
|
|
17
|
+
background: var(--cds-layer-01);
|
|
18
|
+
border-right: 1px solid var(--cds-border-subtle);
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
transition: width 0.15s ease-out;
|
|
22
|
+
z-index: 8000;
|
|
23
|
+
`;
|
|
24
|
+
const ScrollArea = styled.div `
|
|
25
|
+
flex: 1;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
overflow-y: ${(p) => (p.$sidebarExpanded ? 'auto' : 'visible')};
|
|
29
|
+
overflow-x: ${(p) => (p.$sidebarExpanded ? 'hidden' : 'visible')};
|
|
30
|
+
padding-top: 0.5rem;
|
|
31
|
+
padding-bottom: 1rem;
|
|
32
|
+
`;
|
|
33
|
+
const NavButton = styled.button `
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
flex-wrap: nowrap;
|
|
37
|
+
gap: 0.75rem;
|
|
38
|
+
width: 100%;
|
|
39
|
+
min-height: 2.5rem;
|
|
40
|
+
padding: ${(p) => {
|
|
41
|
+
if (p.$depth > 0)
|
|
42
|
+
return `0.625rem 1rem 0.625rem ${0.75 + p.$depth * 1.75}rem`;
|
|
43
|
+
return p.$isExpanded ? '0.75rem 1rem' : '0.75rem';
|
|
44
|
+
}};
|
|
45
|
+
justify-content: ${(p) => (p.$isExpanded ? 'flex-start' : 'center')};
|
|
46
|
+
background: ${(p) => (p.$isActive ? 'var(--cds-layer-selected)' : 'transparent')};
|
|
47
|
+
border: none;
|
|
48
|
+
border-left: ${(p) => (p.$isActive ? '3px solid var(--cds-border-interactive)' : '3px solid transparent')};
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
color: ${(p) => (p.$isActive ? 'var(--cds-text-primary)' : 'var(--cds-text-secondary)')};
|
|
51
|
+
font-size: 0.875rem;
|
|
52
|
+
font-weight: ${(p) => (p.$isActive ? 500 : 400)};
|
|
53
|
+
text-align: left;
|
|
54
|
+
text-decoration: none;
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
transition: background 0.15s, color 0.15s;
|
|
58
|
+
|
|
59
|
+
&:hover {
|
|
60
|
+
background: ${(p) => (p.$isActive ? 'var(--cds-layer-selected)' : 'var(--cds-layer-hover)')};
|
|
61
|
+
color: var(--cds-text-primary);
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&:focus-visible {
|
|
66
|
+
outline: 2px solid var(--cds-focus);
|
|
67
|
+
outline-offset: -2px;
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
const NavLabel = styled.span `
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
text-overflow: ellipsis;
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
`;
|
|
75
|
+
const GroupHeader = styled.div `
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
flex-wrap: nowrap;
|
|
79
|
+
width: 100%;
|
|
80
|
+
min-height: 2.5rem;
|
|
81
|
+
overflow: hidden;
|
|
82
|
+
background: ${(p) => (p.$isActive ? 'var(--cds-layer-selected)' : 'transparent')};
|
|
83
|
+
border-left: ${(p) => (p.$isActive ? '3px solid var(--cds-border-interactive)' : '3px solid transparent')};
|
|
84
|
+
transition: background 0.15s, color 0.15s;
|
|
85
|
+
|
|
86
|
+
&:hover:not(:has(button[data-expand]:hover)) {
|
|
87
|
+
background: ${(p) => (p.$isActive ? 'var(--cds-layer-selected)' : 'var(--cds-layer-hover)')};
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
const GroupLabelButton = styled.button `
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
flex: 1;
|
|
94
|
+
padding: ${(p) => {
|
|
95
|
+
if (p.$depth > 0)
|
|
96
|
+
return `0.75rem 0 0.75rem ${0.75 + p.$depth * 1.75}rem`;
|
|
97
|
+
return '0.75rem 0 0.75rem 1rem';
|
|
98
|
+
}};
|
|
99
|
+
background: transparent;
|
|
100
|
+
border: none;
|
|
101
|
+
cursor: ${(p) => (p.$isClickable ? 'pointer' : 'default')};
|
|
102
|
+
color: ${(p) => (p.$isActive ? 'var(--cds-text-primary)' : 'var(--cds-text-secondary)')};
|
|
103
|
+
font-size: 0.875rem;
|
|
104
|
+
font-weight: ${(p) => (p.$isActive ? 500 : 400)};
|
|
105
|
+
text-align: left;
|
|
106
|
+
text-decoration: none;
|
|
107
|
+
transition: color 0.15s;
|
|
108
|
+
gap: 0.75rem;
|
|
109
|
+
|
|
110
|
+
&:hover {
|
|
111
|
+
color: var(--cds-text-primary);
|
|
112
|
+
text-decoration: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
&:focus-visible {
|
|
116
|
+
outline: 2px solid var(--cds-focus);
|
|
117
|
+
outline-offset: -2px;
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
const ExpandButton = styled.button `
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
padding: 0.25rem;
|
|
125
|
+
margin-right: 0.75rem;
|
|
126
|
+
background: transparent;
|
|
127
|
+
border: none;
|
|
128
|
+
border-radius: 4px;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
color: var(--cds-icon-secondary);
|
|
131
|
+
transition: background 0.15s;
|
|
132
|
+
|
|
133
|
+
&:hover {
|
|
134
|
+
background: var(--cds-layer-hover);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
&:focus-visible {
|
|
138
|
+
outline: 2px solid var(--cds-focus);
|
|
139
|
+
outline-offset: -2px;
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
const PlainGroupHeader = styled.div `
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
flex-wrap: nowrap;
|
|
146
|
+
width: 100%;
|
|
147
|
+
min-height: 2.5rem;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
border-left: 3px solid transparent;
|
|
150
|
+
`;
|
|
151
|
+
const PlainGroupLabel = styled.span `
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
flex: 1;
|
|
155
|
+
padding: ${(p) => {
|
|
156
|
+
if (p.$depth > 0)
|
|
157
|
+
return `0.75rem 0 0.75rem ${0.75 + p.$depth * 1.75}rem`;
|
|
158
|
+
return '0.75rem 0 0.75rem 1rem';
|
|
159
|
+
}};
|
|
160
|
+
gap: 0.75rem;
|
|
161
|
+
color: var(--cds-text-secondary);
|
|
162
|
+
font-size: 0.875rem;
|
|
163
|
+
font-weight: 400;
|
|
164
|
+
`;
|
|
165
|
+
const PlainGroupExpandButton = styled.button `
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
padding: 0.25rem;
|
|
170
|
+
margin-right: 0.75rem;
|
|
171
|
+
background: transparent;
|
|
172
|
+
border: none;
|
|
173
|
+
border-radius: 4px;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
color: var(--cds-icon-secondary);
|
|
176
|
+
transition: background 0.15s;
|
|
177
|
+
|
|
178
|
+
&:hover {
|
|
179
|
+
background: var(--cds-layer-hover);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
&:focus-visible {
|
|
183
|
+
outline: 2px solid var(--cds-focus);
|
|
184
|
+
outline-offset: -2px;
|
|
185
|
+
}
|
|
186
|
+
`;
|
|
187
|
+
const StyledPopover = styled(Popover) `
|
|
188
|
+
display: block;
|
|
189
|
+
|
|
190
|
+
.cds--popover-content {
|
|
191
|
+
pointer-events: none;
|
|
192
|
+
padding: 0.1875rem 0.5rem;
|
|
193
|
+
font-size: var(--cds-body-compact-01-font-size);
|
|
194
|
+
white-space: nowrap;
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
const CollapsedItemTooltip = ({ label, children, }) => {
|
|
198
|
+
const [open, setOpen] = useState(false);
|
|
199
|
+
const timerRef = useRef(null);
|
|
200
|
+
const handleMouseEnter = () => {
|
|
201
|
+
timerRef.current = setTimeout(() => setOpen(true), 150);
|
|
202
|
+
};
|
|
203
|
+
const handleMouseLeave = () => {
|
|
204
|
+
if (timerRef.current) {
|
|
205
|
+
clearTimeout(timerRef.current);
|
|
206
|
+
timerRef.current = null;
|
|
207
|
+
}
|
|
208
|
+
setOpen(false);
|
|
209
|
+
};
|
|
210
|
+
useEffect(() => () => {
|
|
211
|
+
if (timerRef.current)
|
|
212
|
+
clearTimeout(timerRef.current);
|
|
213
|
+
}, []);
|
|
214
|
+
return (_jsxs(StyledPopover, { open: open, align: 'right', highContrast: true, dropShadow: false, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [children, _jsx(PopoverContent, { children: label })] }));
|
|
215
|
+
};
|
|
216
|
+
const SectionDivider = styled.div `
|
|
217
|
+
border-top: ${(p) => (p.$hideTopDivider ? 'none' : '1px solid var(--cds-border-subtle)')};
|
|
218
|
+
margin-top: ${(p) => {
|
|
219
|
+
if (p.$eatScrollPadding)
|
|
220
|
+
return '-0.5rem';
|
|
221
|
+
if (p.$tight)
|
|
222
|
+
return '0';
|
|
223
|
+
return p.$hideTopDivider ? '0' : '0.5rem';
|
|
224
|
+
}};
|
|
225
|
+
padding-top: ${(p) => (p.$hideTopDivider ? '0' : '0.5rem')};
|
|
226
|
+
`;
|
|
227
|
+
const SectionTitle = styled.div `
|
|
228
|
+
padding: 0.5rem 1rem;
|
|
229
|
+
font-size: 0.6875rem;
|
|
230
|
+
font-weight: 600;
|
|
231
|
+
color: var(--cds-text-secondary);
|
|
232
|
+
text-transform: uppercase;
|
|
233
|
+
letter-spacing: 0.1em;
|
|
234
|
+
`;
|
|
235
|
+
const CollapseToggleArea = styled.div `
|
|
236
|
+
border-top: 1px solid var(--cds-border-subtle);
|
|
237
|
+
background: var(--cds-layer-01);
|
|
238
|
+
`;
|
|
239
|
+
const CollapseButton = styled.button `
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
flex-wrap: nowrap;
|
|
243
|
+
gap: 0.75rem;
|
|
244
|
+
width: 100%;
|
|
245
|
+
min-height: 2.5rem;
|
|
246
|
+
padding: ${(p) => (p.$isExpanded ? '0.75rem 1rem' : '0.75rem')};
|
|
247
|
+
justify-content: ${(p) => (p.$isExpanded ? 'flex-start' : 'center')};
|
|
248
|
+
background: transparent;
|
|
249
|
+
border: none;
|
|
250
|
+
border-left: 3px solid transparent;
|
|
251
|
+
cursor: pointer;
|
|
252
|
+
color: var(--cds-text-secondary);
|
|
253
|
+
font-size: 0.875rem;
|
|
254
|
+
white-space: nowrap;
|
|
255
|
+
overflow: hidden;
|
|
256
|
+
transition: background 0.15s, color 0.15s, padding 0.15s ease-out;
|
|
257
|
+
|
|
258
|
+
&:hover {
|
|
259
|
+
background: var(--cds-layer-hover);
|
|
260
|
+
color: var(--cds-text-primary);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
&:focus-visible {
|
|
264
|
+
outline: 2px solid var(--cds-focus);
|
|
265
|
+
outline-offset: -2px;
|
|
266
|
+
}
|
|
267
|
+
`;
|
|
268
|
+
const resolveLinkAs = (linkProps, linkComponent) => {
|
|
269
|
+
if (!linkProps)
|
|
270
|
+
return undefined;
|
|
271
|
+
if ('href' in linkProps)
|
|
272
|
+
return 'a';
|
|
273
|
+
return linkComponent;
|
|
274
|
+
};
|
|
275
|
+
const ItemNode = ({ node, sidebarExpanded, depth, linkComponent, }) => {
|
|
276
|
+
const Icon = node.icon;
|
|
277
|
+
const content = (_jsxs(NavButton, { as: resolveLinkAs(node.linkProps, linkComponent), ...(node.linkProps ?? {}), "$isActive": !!node.isActive, "$isExpanded": sidebarExpanded, "$depth": depth, onClick: node.onClick, "aria-current": node.isActive ? 'page' : undefined, children: [_jsx(Icon, { size: 20, style: { flexShrink: 0 } }), sidebarExpanded && _jsx(NavLabel, { children: node.label }), sidebarExpanded && node.trailingElement] }));
|
|
278
|
+
if (!sidebarExpanded) {
|
|
279
|
+
return (_jsx(CollapsedItemTooltip, { label: node.label, children: content }));
|
|
280
|
+
}
|
|
281
|
+
return content;
|
|
282
|
+
};
|
|
283
|
+
const GroupItemNode = ({ node, sidebarExpanded, depth, linkComponent, }) => {
|
|
284
|
+
const Icon = node.icon;
|
|
285
|
+
const isActive = !!node.isActive;
|
|
286
|
+
if (!sidebarExpanded) {
|
|
287
|
+
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 } }) }));
|
|
288
|
+
return (_jsx(CollapsedItemTooltip, { label: node.label, children: collapsed }));
|
|
289
|
+
}
|
|
290
|
+
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 && (_jsx(ExpandButton, { "data-expand": true, onClick: (e) => {
|
|
291
|
+
e.stopPropagation();
|
|
292
|
+
node.onToggleExpand?.();
|
|
293
|
+
}, "aria-label": node.isExpanded
|
|
294
|
+
? `Collapse ${node.label}`
|
|
295
|
+
: `Expand ${node.label}`, "aria-expanded": !!node.isExpanded, children: node.isExpanded ? (_jsx(ChevronUp, { size: 16 })) : (_jsx(ChevronDown, { size: 16 })) }))] }), node.isExpanded &&
|
|
296
|
+
node.children.map((child) => (_jsx(SidebarNodeComponent, { node: child, sidebarExpanded: sidebarExpanded, depth: depth + 1, linkComponent: linkComponent }, child.key)))] }));
|
|
297
|
+
};
|
|
298
|
+
const GroupNode = ({ node, sidebarExpanded, depth, linkComponent, }) => {
|
|
299
|
+
const Icon = node.icon;
|
|
300
|
+
if (!sidebarExpanded) {
|
|
301
|
+
const collapsed = (_jsx(NavButton, { "$isActive": false, "$isExpanded": false, "$depth": 0, onClick: node.onToggleExpand, children: _jsx(Icon, { size: 20, style: { flexShrink: 0 } }) }));
|
|
302
|
+
return (_jsx(CollapsedItemTooltip, { label: node.label, children: collapsed }));
|
|
303
|
+
}
|
|
304
|
+
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
|
|
305
|
+
? `Collapse ${node.label}`
|
|
306
|
+
: `Expand ${node.label}`, "aria-expanded": !!node.isExpanded, children: node.isExpanded ? (_jsx(ChevronUp, { size: 16 })) : (_jsx(ChevronDown, { size: 16 })) }))] }), node.isExpanded &&
|
|
307
|
+
node.children.map((child) => (_jsx(SidebarNodeComponent, { node: child, sidebarExpanded: sidebarExpanded, depth: depth + 1, linkComponent: linkComponent }, child.key)))] }));
|
|
308
|
+
};
|
|
309
|
+
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)))] }));
|
|
310
|
+
const SidebarNodeComponent = ({ node, sidebarExpanded, depth, linkComponent, hideTopDivider = false, eatScrollPadding = false, tight = false, }) => {
|
|
311
|
+
if (node.type === 'section') {
|
|
312
|
+
if (depth !== 0) {
|
|
313
|
+
console.error(`[C3Sidebar] Section "${node.key}" must be at the root level (depth 0), found at depth ${depth}.`);
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return (_jsx(SectionNode, { node: node, sidebarExpanded: sidebarExpanded, linkComponent: linkComponent, hideTopDivider: hideTopDivider, eatScrollPadding: eatScrollPadding, tight: tight }));
|
|
317
|
+
}
|
|
318
|
+
if (node.type === 'group-item') {
|
|
319
|
+
return (_jsx(GroupItemNode, { node: node, sidebarExpanded: sidebarExpanded, depth: depth, linkComponent: linkComponent }));
|
|
320
|
+
}
|
|
321
|
+
if (node.type === 'group') {
|
|
322
|
+
return (_jsx(GroupNode, { node: node, sidebarExpanded: sidebarExpanded, depth: depth, linkComponent: linkComponent }));
|
|
323
|
+
}
|
|
324
|
+
return (_jsx(ItemNode, { node: node, sidebarExpanded: sidebarExpanded, depth: depth, linkComponent: linkComponent }));
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Sidebar navigation panel. Children are `SidebarNode[]`, typically sections.
|
|
328
|
+
* Loose items (not wrapped in a section) are valid and behave as an implicit
|
|
329
|
+
* untitled group at the top.
|
|
330
|
+
*/
|
|
331
|
+
export const C3Sidebar = ({ ariaLabel, children: nodes, isExpanded = true, onToggleExpanded, expandedWidth = '16rem', collapsedWidth = '3rem', linkComponent, }) => {
|
|
332
|
+
const width = isExpanded ? expandedWidth : collapsedWidth;
|
|
333
|
+
return (_jsxs(SidebarNav, { "$width": width, "aria-label": ariaLabel, children: [_jsx(ScrollArea, { "$sidebarExpanded": isExpanded, children: (() => {
|
|
334
|
+
let sectionSeen = false;
|
|
335
|
+
let prevSectionCompact = false;
|
|
336
|
+
let hasNonSectionNodes = false;
|
|
337
|
+
return nodes.map((node) => {
|
|
338
|
+
let hideTopDivider = false;
|
|
339
|
+
let eatScrollPadding = false;
|
|
340
|
+
let tight = false;
|
|
341
|
+
if (node.type === 'section') {
|
|
342
|
+
const isFirst = !sectionSeen;
|
|
343
|
+
hideTopDivider =
|
|
344
|
+
(isFirst && !hasNonSectionNodes) || !!node.compact;
|
|
345
|
+
eatScrollPadding =
|
|
346
|
+
isFirst && !hasNonSectionNodes && !!node.compact;
|
|
347
|
+
tight = prevSectionCompact;
|
|
348
|
+
sectionSeen = true;
|
|
349
|
+
prevSectionCompact = !!node.compact;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
hasNonSectionNodes = true;
|
|
353
|
+
}
|
|
354
|
+
return (_jsx(SidebarNodeComponent, { node: node, sidebarExpanded: isExpanded, depth: 0, linkComponent: linkComponent, hideTopDivider: hideTopDivider, eatScrollPadding: eatScrollPadding, tight: tight }, node.key));
|
|
355
|
+
});
|
|
356
|
+
})() }), 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 } }) }) })) }))] }));
|
|
357
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import type { ToolDescriptor } from './c3-navigation-v2.types';
|
|
3
|
+
interface C3ToolsAreaProps {
|
|
4
|
+
tools: ToolDescriptor[];
|
|
5
|
+
/** Currently active tool key (controlled mode). When omitted, internal state is used. */
|
|
6
|
+
activeToolKey?: string | null;
|
|
7
|
+
/** Called when the active tool changes (controlled mode). */
|
|
8
|
+
onActiveToolChange?: (key: string | null) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Renders tool buttons inline (for HeaderGlobalBar) and manages a single shared
|
|
12
|
+
* right-side panel. Opening a tool with a panel implicitly closes whatever was active.
|
|
13
|
+
* Tools without a panel are plain header buttons; their renderButton owns the onClick action.
|
|
14
|
+
*/
|
|
15
|
+
export declare const C3ToolsArea: ({ tools, activeToolKey: controlledKey, onActiveToolChange, }: C3ToolsAreaProps) => JSX.Element;
|
|
16
|
+
export {};
|