@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
package/lib/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camunda/camunda-composite-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Camunda Composite Components",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/camunda/camunda-cloud-management-apps/issues"
|
|
@@ -55,38 +55,38 @@
|
|
|
55
55
|
"semver": "7.7.4"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@auth0/auth0-spa-js": "2.
|
|
58
|
+
"@auth0/auth0-spa-js": "2.19.0",
|
|
59
59
|
"@camunda/ccma-shared-types": "workspace:*",
|
|
60
60
|
"@carbon/react": "1.103.0",
|
|
61
61
|
"@chromatic-com/storybook": "5.1.1",
|
|
62
62
|
"@mdx-js/react": "3.1.1",
|
|
63
|
-
"@playwright/test": "1.
|
|
64
|
-
"@storybook/addon-a11y": "10.3.
|
|
65
|
-
"@storybook/addon-docs": "10.3.
|
|
66
|
-
"@storybook/addon-links": "10.3.
|
|
67
|
-
"@storybook/addon-vitest": "10.3.
|
|
68
|
-
"@storybook/react": "10.3.
|
|
69
|
-
"@storybook/react-vite": "10.3.
|
|
70
|
-
"@vitest/browser": "4.1.
|
|
71
|
-
"@vitest/browser-playwright": "4.1.
|
|
72
|
-
"vitest": "4.1.
|
|
73
|
-
"conventional-changelog-conventionalcommits": "9.3.
|
|
63
|
+
"@playwright/test": "1.59.1",
|
|
64
|
+
"@storybook/addon-a11y": "10.3.5",
|
|
65
|
+
"@storybook/addon-docs": "10.3.5",
|
|
66
|
+
"@storybook/addon-links": "10.3.5",
|
|
67
|
+
"@storybook/addon-vitest": "10.3.5",
|
|
68
|
+
"@storybook/react": "10.3.5",
|
|
69
|
+
"@storybook/react-vite": "10.3.5",
|
|
70
|
+
"@vitest/browser": "4.1.4",
|
|
71
|
+
"@vitest/browser-playwright": "4.1.4",
|
|
72
|
+
"vitest": "4.1.4",
|
|
73
|
+
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
74
74
|
"eslint-import-resolver-typescript": "4.4.4",
|
|
75
75
|
"eslint-plugin-react": "7.37.5",
|
|
76
76
|
"eslint-plugin-react-hooks": "7.0.1",
|
|
77
|
-
"eslint-plugin-storybook": "10.3.
|
|
77
|
+
"eslint-plugin-storybook": "10.3.5",
|
|
78
78
|
"event-source-polyfill": "1.0.31",
|
|
79
|
-
"mixpanel-browser": "2.
|
|
80
|
-
"playwright": "1.
|
|
81
|
-
"react": "19.2.
|
|
82
|
-
"react-dom": "19.2.
|
|
83
|
-
"react-is": "19.2.
|
|
79
|
+
"mixpanel-browser": "2.78.0",
|
|
80
|
+
"playwright": "1.59.1",
|
|
81
|
+
"react": "19.2.5",
|
|
82
|
+
"react-dom": "19.2.5",
|
|
83
|
+
"react-is": "19.2.5",
|
|
84
84
|
"rimraf": "6.1.3",
|
|
85
85
|
"serve": "14.2.6",
|
|
86
|
-
"storybook": "10.3.
|
|
87
|
-
"styled-components": "6.
|
|
88
|
-
"typescript-eslint": "8.
|
|
89
|
-
"wait-on": "9.0.
|
|
86
|
+
"storybook": "10.3.5",
|
|
87
|
+
"styled-components": "6.4.0",
|
|
88
|
+
"typescript-eslint": "8.58.1",
|
|
89
|
+
"wait-on": "9.0.5"
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
92
|
"@carbon/react": "1.x",
|
|
@@ -3,4 +3,5 @@ import type { C3ClusterTagProps, C3ClusterTagWithClusterNameProps, CamundaCluste
|
|
|
3
3
|
export type TagColor = 'green' | 'blue' | 'purple' | 'red' | 'cool-gray' | 'magenta' | 'cyan' | 'teal' | 'gray' | 'warm-gray' | 'high-contrast' | 'outline';
|
|
4
4
|
export declare const C3ClusterTagWithClusterName: FC<C3ClusterTagWithClusterNameProps>;
|
|
5
5
|
export declare const C3ClusterTag: FC<C3ClusterTagProps>;
|
|
6
|
+
export declare function getShortStageLabel(stage: CamundaClusterStage): string;
|
|
6
7
|
export declare function getColorForStage(stage: CamundaClusterStage): TagColor | undefined;
|
|
@@ -52,19 +52,40 @@ export const C3ClusterTagWithClusterName = (props) => {
|
|
|
52
52
|
};
|
|
53
53
|
export const C3ClusterTag = (props) => {
|
|
54
54
|
const { clusters } = useC3Profile();
|
|
55
|
-
|
|
55
|
+
const subtle = props.subtle ?? false;
|
|
56
|
+
return 'stage' in props ? (renderStageTag(props.stage, subtle)) : 'clusterUuid' in props ? (generateFromInput({
|
|
56
57
|
clusterUuid: props.clusterUuid,
|
|
57
58
|
allClusters: clusters,
|
|
58
59
|
conditionalRendering: props.conditionalRendering || (() => true),
|
|
60
|
+
subtle,
|
|
59
61
|
})) : (_jsx(_Fragment, {}));
|
|
60
62
|
};
|
|
63
|
+
function renderStageTag(stage, subtle) {
|
|
64
|
+
if (subtle) {
|
|
65
|
+
return (_jsx(Tag, { type: 'gray', size: 'sm', style: { color: 'var(--cds-text-secondary)' }, children: getShortStageLabel(stage) }));
|
|
66
|
+
}
|
|
67
|
+
return _jsx(Tag, { type: getColorForStage(stage), children: stage });
|
|
68
|
+
}
|
|
69
|
+
export function getShortStageLabel(stage) {
|
|
70
|
+
switch (stage) {
|
|
71
|
+
case 'dev':
|
|
72
|
+
return 'Dev';
|
|
73
|
+
case 'test':
|
|
74
|
+
return 'Test';
|
|
75
|
+
case 'stage':
|
|
76
|
+
return 'Stage';
|
|
77
|
+
case 'prod':
|
|
78
|
+
return 'Prod';
|
|
79
|
+
default:
|
|
80
|
+
return stage;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
61
83
|
function generateFromInput(props) {
|
|
62
84
|
const foundCluster = props.allClusters?.find((cluster) => cluster.uuid === props.clusterUuid);
|
|
63
85
|
if (foundCluster?.labels?.camunda &&
|
|
64
86
|
foundCluster?.labels?.camunda?.length > 0) {
|
|
65
87
|
const label = foundCluster.labels.camunda[0];
|
|
66
|
-
return getColorForStage(label) &&
|
|
67
|
-
props.conditionalRendering(label) ? (_jsx(Tag, { type: getColorForStage(label), children: label })) : (_jsx(_Fragment, {}));
|
|
88
|
+
return getColorForStage(label) && props.conditionalRendering(label) ? (renderStageTag(label, props.subtle)) : (_jsx(_Fragment, {}));
|
|
68
89
|
}
|
|
69
90
|
return _jsx(_Fragment, {});
|
|
70
91
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
export interface C3LicenseTagProps {
|
|
3
|
+
/** Whether the license is a production license. */
|
|
4
|
+
isProductionLicense: boolean;
|
|
5
|
+
/** Whether the license is commercial. When false, shows non-commercial variants. */
|
|
6
|
+
isCommercial?: boolean;
|
|
7
|
+
/** License expiry as a Unix timestamp (ms) or ISO date string. */
|
|
8
|
+
expiresAt?: number | string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Renders a license status tag for the Camunda header. Handles all license
|
|
12
|
+
* states: production, non-production (with tooltip), non-commercial,
|
|
13
|
+
* non-commercial expiring (with countdown), and non-commercial expired.
|
|
14
|
+
*
|
|
15
|
+
* Drop into `headerTrailingContent` of C3NavigationV2.
|
|
16
|
+
*/
|
|
17
|
+
export declare const C3LicenseTag: (props: C3LicenseTagProps) => JSX.Element | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { Link, Tag, Toggletip, ToggletipButton, ToggletipContent, } from '@carbon/react';
|
|
8
|
+
import { Time, Warning } from '@carbon/react/icons/index.esm.js';
|
|
9
|
+
const NON_PRODUCTION_TERMS_LINK = 'https://legal.camunda.com/#self-managed-non-production-terms';
|
|
10
|
+
const SALES_CONTACT_LINK = 'https://camunda.com/contact/';
|
|
11
|
+
const DAY = 1000 * 60 * 60 * 24;
|
|
12
|
+
const LICENSE_EXPIRY_THRESHOLD = DAY * 30;
|
|
13
|
+
function resolveExpiresAt(value) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (typeof value === 'string')
|
|
17
|
+
return Date.parse(value);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function getTagVariant(props) {
|
|
21
|
+
const { isProductionLicense, isCommercial, expiresAt: rawExpiresAt } = props;
|
|
22
|
+
const expiresAt = resolveExpiresAt(rawExpiresAt);
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
// Non-commercial expired
|
|
25
|
+
if (isCommercial === false && expiresAt !== undefined && expiresAt < now) {
|
|
26
|
+
return {
|
|
27
|
+
label: 'Non-commercial license - expired',
|
|
28
|
+
color: 'red',
|
|
29
|
+
icon: Warning,
|
|
30
|
+
tooltip: (_jsx("p", { children: "To continue using all features, please renew your license." })),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// Non-commercial about to expire (within 30 days)
|
|
34
|
+
if (isCommercial === false &&
|
|
35
|
+
expiresAt !== undefined &&
|
|
36
|
+
expiresAt - LICENSE_EXPIRY_THRESHOLD < now &&
|
|
37
|
+
expiresAt > now) {
|
|
38
|
+
const daysLeft = Math.floor((expiresAt - now) / DAY);
|
|
39
|
+
return {
|
|
40
|
+
label: `Non-commercial license - ${daysLeft} ${daysLeft > 1 ? 'days' : 'day'} left`,
|
|
41
|
+
color: 'blue',
|
|
42
|
+
icon: Time,
|
|
43
|
+
tooltip: (_jsx("p", { children: "Please renew and provide new license keys to continue production use of Camunda." })),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Non-commercial (not expiring soon or no expiry)
|
|
47
|
+
if (isCommercial === false &&
|
|
48
|
+
(expiresAt === undefined || expiresAt - LICENSE_EXPIRY_THRESHOLD >= now)) {
|
|
49
|
+
return {
|
|
50
|
+
label: 'Non-commercial license',
|
|
51
|
+
color: 'gray',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Production or non-production license
|
|
55
|
+
return {
|
|
56
|
+
label: isProductionLicense
|
|
57
|
+
? 'Production license'
|
|
58
|
+
: 'Non-production license',
|
|
59
|
+
color: 'gray',
|
|
60
|
+
tooltip: isProductionLicense ? undefined : (_jsxs("p", { children: ["Non-production license. For production usage details, visit our", ' ', _jsx(Link, { href: NON_PRODUCTION_TERMS_LINK, target: '_blank', rel: 'noopener noreferrer', style: { display: 'inline' }, children: "terms & conditions page" }), ' ', "or", ' ', _jsx(Link, { href: SALES_CONTACT_LINK, target: '_blank', rel: 'noopener noreferrer', style: { display: 'inline' }, children: "contact our sales team" }), "."] })),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Renders a license status tag for the Camunda header. Handles all license
|
|
65
|
+
* states: production, non-production (with tooltip), non-commercial,
|
|
66
|
+
* non-commercial expiring (with countdown), and non-commercial expired.
|
|
67
|
+
*
|
|
68
|
+
* Drop into `headerTrailingContent` of C3NavigationV2.
|
|
69
|
+
*/
|
|
70
|
+
export const C3LicenseTag = (props) => {
|
|
71
|
+
const variant = getTagVariant(props);
|
|
72
|
+
if (!variant)
|
|
73
|
+
return null;
|
|
74
|
+
const { label, color, icon, tooltip } = variant;
|
|
75
|
+
if (tooltip) {
|
|
76
|
+
return (_jsxs(Toggletip, { align: 'bottom', children: [_jsx(ToggletipButton, { as: 'span', label: label, children: _jsx(Tag, { type: color, renderIcon: icon, style: { margin: 0, cursor: 'pointer' }, children: label }) }), _jsx(ToggletipContent, { children: tooltip })] }));
|
|
77
|
+
}
|
|
78
|
+
return (_jsx(Tag, { type: color, renderIcon: icon, style: { margin: 0 }, children: label }));
|
|
79
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { C3LicenseTag, type C3LicenseTagProps } from './c3-license-tag';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
|
|
3
|
+
* under one or more contributor license agreements. Licensed under a commercial license.
|
|
4
|
+
* You may not use this file except in compliance with the commercial license.
|
|
5
|
+
*/
|
|
6
|
+
export { C3LicenseTag } from './c3-license-tag.js';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { FC } from 'react';
|
|
3
|
-
import type { Organization } from '../../../utils/camunda.types';
|
|
1
|
+
import { Dropdown } from '@carbon/react';
|
|
2
|
+
import type { ComponentProps, ComponentType, FC } from 'react';
|
|
4
3
|
import type { LoadingStatus } from '../c3-navigation-appbar/c3-navigation-appbar';
|
|
5
4
|
export declare const ActiveOrgWrapperCustom: any;
|
|
6
5
|
export declare const ActiveOrgWrapper: any;
|
|
@@ -23,7 +22,7 @@ export declare const ManageOrgOverflowMenu: FC<{
|
|
|
23
22
|
onManage: () => void;
|
|
24
23
|
onRequestToLeave: () => void;
|
|
25
24
|
}>;
|
|
26
|
-
export declare const StyledDropdown:
|
|
25
|
+
export declare const StyledDropdown: ComponentType<ComponentProps<typeof Dropdown> & {
|
|
27
26
|
$scrollBarWidth: number;
|
|
28
27
|
}>;
|
|
29
28
|
export declare const OrgListWrapper: any;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import type { BreadcrumbSegment, LinkComponent } from './c3-navigation-v2.types';
|
|
3
|
+
export interface C3BreadcrumbBarProps {
|
|
4
|
+
segments: BreadcrumbSegment[];
|
|
5
|
+
linkComponent?: LinkComponent;
|
|
6
|
+
}
|
|
7
|
+
export declare const C3BreadcrumbBar: ({ segments, linkComponent, }: C3BreadcrumbBarProps) => JSX.Element;
|
|
@@ -0,0 +1,371 @@
|
|
|
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 { Layer } from '@carbon/react';
|
|
8
|
+
import { Checkmark, ChevronDown, OverflowMenuVertical, } from '@carbon/react/icons/index.esm.js';
|
|
9
|
+
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
10
|
+
import { createPortal } from 'react-dom';
|
|
11
|
+
import styled from 'styled-components';
|
|
12
|
+
const SegmentWrapper = styled.div `
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
position: relative;
|
|
16
|
+
`;
|
|
17
|
+
const SegmentButton = styled.button `
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 0.5rem;
|
|
21
|
+
padding: 0.25rem 0.5rem;
|
|
22
|
+
background: transparent;
|
|
23
|
+
border: none;
|
|
24
|
+
border-radius: 4px;
|
|
25
|
+
cursor: ${(p) => (p.$isInteractive ? 'pointer' : 'default')};
|
|
26
|
+
color: ${(p) => (p.$isLast ? 'var(--cds-text-primary)' : 'var(--cds-text-secondary)')};
|
|
27
|
+
font-size: 0.875rem;
|
|
28
|
+
font-weight: ${(p) => (p.$isLast ? 500 : 400)};
|
|
29
|
+
text-decoration: none;
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
transition: background 0.15s, color 0.15s;
|
|
32
|
+
|
|
33
|
+
&:hover {
|
|
34
|
+
background: ${(p) => (p.$isInteractive ? 'var(--cds-layer-hover)' : 'transparent')};
|
|
35
|
+
color: var(--cds-text-primary);
|
|
36
|
+
text-decoration: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&:focus-visible {
|
|
40
|
+
outline: 2px solid var(--cds-focus);
|
|
41
|
+
outline-offset: -2px;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
const ChevronButton = styled.button `
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
padding: 0.25rem;
|
|
49
|
+
background: ${(p) => (p.$isOpen ? 'var(--cds-layer-01)' : 'transparent')};
|
|
50
|
+
border: none;
|
|
51
|
+
border-radius: 4px;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
color: var(--cds-icon-secondary);
|
|
54
|
+
transition: background 0.15s;
|
|
55
|
+
|
|
56
|
+
&:hover {
|
|
57
|
+
background: var(--cds-layer-hover);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&:focus-visible {
|
|
61
|
+
outline: 2px solid var(--cds-focus);
|
|
62
|
+
outline-offset: -2px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
svg {
|
|
66
|
+
transform: ${(p) => (p.$isOpen ? 'rotate(180deg)' : 'rotate(0deg)')};
|
|
67
|
+
transition: transform 0.15s;
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
const ActionMenuButton = styled.button `
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
padding: 0.25rem;
|
|
75
|
+
background: ${(p) => (p.$isOpen ? 'var(--cds-layer-01)' : 'transparent')};
|
|
76
|
+
border: none;
|
|
77
|
+
border-radius: 4px;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
color: var(--cds-icon-secondary);
|
|
80
|
+
transition: background 0.15s;
|
|
81
|
+
|
|
82
|
+
&:hover {
|
|
83
|
+
background: var(--cds-layer-hover);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&:focus-visible {
|
|
87
|
+
outline: 2px solid var(--cds-focus);
|
|
88
|
+
outline-offset: -2px;
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
const ActionMenuItem = styled.button `
|
|
92
|
+
width: 100%;
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
padding: 0.625rem 1rem;
|
|
96
|
+
background: transparent;
|
|
97
|
+
border: none;
|
|
98
|
+
border-top: ${(p) => (p.$hasDivider ? '1px solid var(--cds-border-subtle)' : 'none')};
|
|
99
|
+
cursor: ${(p) => (p.$isDisabled ? 'not-allowed' : 'pointer')};
|
|
100
|
+
text-align: left;
|
|
101
|
+
transition: background 0.15s;
|
|
102
|
+
color: ${(p) => p.$isDisabled
|
|
103
|
+
? 'var(--cds-text-disabled)'
|
|
104
|
+
: p.$isDanger
|
|
105
|
+
? 'var(--cds-text-error)'
|
|
106
|
+
: 'var(--cds-text-primary)'};
|
|
107
|
+
font-size: 0.875rem;
|
|
108
|
+
opacity: ${(p) => (p.$isDisabled ? 0.5 : 1)};
|
|
109
|
+
|
|
110
|
+
&:hover {
|
|
111
|
+
background: ${(p) => (p.$isDisabled ? 'transparent' : 'var(--cds-layer-hover)')};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
&:focus-visible {
|
|
115
|
+
outline: 2px solid var(--cds-focus);
|
|
116
|
+
outline-offset: -2px;
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
const DropdownPanel = styled.div `
|
|
120
|
+
position: fixed;
|
|
121
|
+
top: ${(p) => p.$top}px;
|
|
122
|
+
left: ${(p) => p.$left}px;
|
|
123
|
+
z-index: 9999;
|
|
124
|
+
background: var(--cds-layer-01);
|
|
125
|
+
border-radius: 4px;
|
|
126
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16);
|
|
127
|
+
border: 1px solid var(--cds-border-subtle);
|
|
128
|
+
min-width: ${(p) => p.$minWidth ?? '240px'};
|
|
129
|
+
max-height: 400px;
|
|
130
|
+
overflow-y: auto;
|
|
131
|
+
`;
|
|
132
|
+
const DropdownItem = styled.button `
|
|
133
|
+
width: 100%;
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 0.75rem;
|
|
137
|
+
padding: 0.625rem 1rem;
|
|
138
|
+
background: ${(p) => (p.$isSelected ? 'var(--cds-layer-selected)' : 'transparent')};
|
|
139
|
+
border: none;
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
text-align: left;
|
|
142
|
+
transition: background 0.15s;
|
|
143
|
+
color: var(--cds-text-primary);
|
|
144
|
+
font-size: 0.875rem;
|
|
145
|
+
text-decoration: none;
|
|
146
|
+
|
|
147
|
+
&:hover {
|
|
148
|
+
background: var(--cds-layer-hover);
|
|
149
|
+
text-decoration: none;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
&:focus-visible {
|
|
153
|
+
outline: 2px solid var(--cds-focus);
|
|
154
|
+
outline-offset: -2px;
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
const DropdownItemLabel = styled.span `
|
|
158
|
+
flex: 1;
|
|
159
|
+
overflow: hidden;
|
|
160
|
+
text-overflow: ellipsis;
|
|
161
|
+
white-space: nowrap;
|
|
162
|
+
`;
|
|
163
|
+
const Separator = styled.span `
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
padding: 0 0.25rem;
|
|
167
|
+
color: var(--cds-text-secondary);
|
|
168
|
+
font-size: 0.875rem;
|
|
169
|
+
user-select: none;
|
|
170
|
+
|
|
171
|
+
&::after {
|
|
172
|
+
content: '/';
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
const DropdownEntry = ({ item, onSelect, linkComponent, }) => {
|
|
176
|
+
const Icon = item.icon;
|
|
177
|
+
return (_jsxs(DropdownItem, { as: item.linkProps?.href !== undefined
|
|
178
|
+
? (linkComponent ?? 'a')
|
|
179
|
+
: item.linkProps
|
|
180
|
+
? linkComponent
|
|
181
|
+
: undefined, ...(item.linkProps ?? {}), "$isSelected": !!item.isSelected, onClick: () => onSelect(item), role: 'option', tabIndex: -1, "aria-selected": !!item.isSelected, children: [Icon && (_jsx(Icon, { size: 16, style: { color: 'var(--cds-icon-secondary)', flexShrink: 0 } })), _jsx(DropdownItemLabel, { children: item.label }), item.trailingElement, item.isSelected && (_jsx(Checkmark, { size: 16, style: { color: 'var(--cds-icon-primary)', flexShrink: 0 } }))] }));
|
|
182
|
+
};
|
|
183
|
+
const DropdownTitle = styled.div `
|
|
184
|
+
padding: 0.75rem 1rem 0.5rem;
|
|
185
|
+
font-size: 0.6875rem;
|
|
186
|
+
font-weight: 600;
|
|
187
|
+
color: var(--cds-text-secondary);
|
|
188
|
+
text-transform: uppercase;
|
|
189
|
+
letter-spacing: 0.1em;
|
|
190
|
+
border-bottom: 1px solid var(--cds-border-subtle);
|
|
191
|
+
`;
|
|
192
|
+
const CLOSE_EVENT = 'breadcrumb-dropdown-close';
|
|
193
|
+
function useBreadcrumbDropdown(id) {
|
|
194
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
195
|
+
const open = useCallback(() => {
|
|
196
|
+
document.dispatchEvent(new CustomEvent(CLOSE_EVENT, { detail: id }));
|
|
197
|
+
setIsOpen(true);
|
|
198
|
+
}, [id]);
|
|
199
|
+
const close = useCallback(() => setIsOpen(false), []);
|
|
200
|
+
const toggle = useCallback(() => {
|
|
201
|
+
if (isOpen) {
|
|
202
|
+
close();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
open();
|
|
206
|
+
}
|
|
207
|
+
}, [isOpen, open, close]);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const handler = (e) => {
|
|
210
|
+
if (e.detail !== id)
|
|
211
|
+
setIsOpen(false);
|
|
212
|
+
};
|
|
213
|
+
document.addEventListener(CLOSE_EVENT, handler);
|
|
214
|
+
return () => document.removeEventListener(CLOSE_EVENT, handler);
|
|
215
|
+
}, [id]);
|
|
216
|
+
return { isOpen, open, close, toggle };
|
|
217
|
+
}
|
|
218
|
+
function usePanelKeyboard(panelRef, close, returnFocusRef, isOpen, itemSelector) {
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (!isOpen || !panelRef.current)
|
|
221
|
+
return;
|
|
222
|
+
const items = panelRef.current.querySelectorAll(itemSelector);
|
|
223
|
+
const firstEnabled = Array.from(items).find((el) => !el.disabled);
|
|
224
|
+
firstEnabled?.focus();
|
|
225
|
+
}, [isOpen, panelRef, itemSelector]);
|
|
226
|
+
const handleKeyDown = useCallback((e) => {
|
|
227
|
+
const panel = panelRef.current;
|
|
228
|
+
if (!panel)
|
|
229
|
+
return;
|
|
230
|
+
const items = Array.from(panel.querySelectorAll(itemSelector)).filter((el) => !el.disabled);
|
|
231
|
+
const currentIndex = items.indexOf(document.activeElement);
|
|
232
|
+
switch (e.key) {
|
|
233
|
+
case 'ArrowDown': {
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
236
|
+
items[next]?.focus();
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'ArrowUp': {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
242
|
+
items[prev]?.focus();
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case 'Home':
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
items[0]?.focus();
|
|
248
|
+
break;
|
|
249
|
+
case 'End':
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
items[items.length - 1]?.focus();
|
|
252
|
+
break;
|
|
253
|
+
case 'Tab':
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
close();
|
|
256
|
+
returnFocusRef.current?.focus();
|
|
257
|
+
break;
|
|
258
|
+
case 'Escape':
|
|
259
|
+
close();
|
|
260
|
+
returnFocusRef.current?.focus();
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}, [panelRef, close, returnFocusRef, itemSelector]);
|
|
264
|
+
return handleKeyDown;
|
|
265
|
+
}
|
|
266
|
+
const ActionsMenu = ({ actions, ariaLabel, }) => {
|
|
267
|
+
const menuId = useRef(`actions-${Math.random().toString(36).slice(2, 8)}`).current;
|
|
268
|
+
const { isOpen, close, toggle } = useBreadcrumbDropdown(menuId);
|
|
269
|
+
const buttonRef = useRef(null);
|
|
270
|
+
const panelRef = useRef(null);
|
|
271
|
+
const [panelPos, setPanelPos] = useState(undefined);
|
|
272
|
+
const updatePosition = useCallback(() => {
|
|
273
|
+
if (buttonRef.current) {
|
|
274
|
+
const rect = buttonRef.current.getBoundingClientRect();
|
|
275
|
+
setPanelPos({ top: rect.bottom + 4, left: rect.left });
|
|
276
|
+
}
|
|
277
|
+
}, []);
|
|
278
|
+
const handleClickOutside = useCallback((event) => {
|
|
279
|
+
const target = event.target;
|
|
280
|
+
if (buttonRef.current?.contains(target))
|
|
281
|
+
return;
|
|
282
|
+
if (target.closest?.('[data-breadcrumb-actions]'))
|
|
283
|
+
return;
|
|
284
|
+
close();
|
|
285
|
+
}, [close]);
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
if (isOpen) {
|
|
288
|
+
updatePosition();
|
|
289
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
290
|
+
}
|
|
291
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
292
|
+
}, [isOpen, handleClickOutside, updatePosition]);
|
|
293
|
+
const handleAction = useCallback((action) => {
|
|
294
|
+
if (action.disabled)
|
|
295
|
+
return;
|
|
296
|
+
action.onClick?.();
|
|
297
|
+
close();
|
|
298
|
+
}, [close]);
|
|
299
|
+
const handleKeyDown = usePanelKeyboard(panelRef, close, buttonRef, isOpen, '[role="menuitem"]');
|
|
300
|
+
return (_jsxs(_Fragment, { children: [_jsx(ActionMenuButton, { ref: buttonRef, "$isOpen": isOpen, onClick: toggle, onKeyDown: (e) => {
|
|
301
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
302
|
+
if (!isOpen) {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
toggle();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}, "aria-label": ariaLabel, "aria-expanded": isOpen, "aria-haspopup": 'menu', children: _jsx(OverflowMenuVertical, { size: 16 }) }), isOpen &&
|
|
308
|
+
panelPos &&
|
|
309
|
+
createPortal(_jsx(Layer, { children: _jsx(DropdownPanel, { ref: panelRef, "data-breadcrumb-actions": true, "$top": panelPos.top, "$left": panelPos.left, "$minWidth": 'auto', role: 'menu', "aria-label": ariaLabel, onKeyDown: handleKeyDown, children: actions.map((action) => (_jsx(ActionMenuItem, { role: 'menuitem', tabIndex: -1, "$isDanger": action.isDanger, "$hasDivider": action.hasDivider, "$isDisabled": action.disabled, disabled: action.disabled, onClick: () => handleAction(action), children: action.label }, action.key))) }) }), document.body)] }));
|
|
310
|
+
};
|
|
311
|
+
const BreadcrumbSegmentComponent = ({ segment, isLast, linkComponent, }) => {
|
|
312
|
+
const dropdownId = useRef(`dropdown-${Math.random().toString(36).slice(2, 8)}`).current;
|
|
313
|
+
const { isOpen, close, toggle } = useBreadcrumbDropdown(dropdownId);
|
|
314
|
+
const wrapperRef = useRef(null);
|
|
315
|
+
const chevronRef = useRef(null);
|
|
316
|
+
const dropdownPanelRef = useRef(null);
|
|
317
|
+
const hasDropdown = segment.dropdownItems && segment.dropdownItems.length > 0;
|
|
318
|
+
const Icon = segment.icon;
|
|
319
|
+
const dropdownAriaLabel = segment.dropdownAriaLabel ?? `Switch ${segment.label}`;
|
|
320
|
+
const [dropdownPos, setDropdownPos] = useState(undefined);
|
|
321
|
+
const updateDropdownPosition = useCallback(() => {
|
|
322
|
+
if (wrapperRef.current) {
|
|
323
|
+
const rect = wrapperRef.current.getBoundingClientRect();
|
|
324
|
+
setDropdownPos({ top: rect.bottom + 4, left: rect.left });
|
|
325
|
+
}
|
|
326
|
+
}, []);
|
|
327
|
+
const handleClickOutside = useCallback((event) => {
|
|
328
|
+
const target = event.target;
|
|
329
|
+
if (wrapperRef.current && !wrapperRef.current.contains(target)) {
|
|
330
|
+
if (target.closest?.('[data-breadcrumb-dropdown]'))
|
|
331
|
+
return;
|
|
332
|
+
close();
|
|
333
|
+
}
|
|
334
|
+
}, [close]);
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (isOpen) {
|
|
337
|
+
updateDropdownPosition();
|
|
338
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
339
|
+
}
|
|
340
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
341
|
+
}, [isOpen, handleClickOutside, updateDropdownPosition]);
|
|
342
|
+
const handleSelect = useCallback((item) => {
|
|
343
|
+
item.onClick?.();
|
|
344
|
+
close();
|
|
345
|
+
}, [close]);
|
|
346
|
+
const handleDropdownKeyDown = usePanelKeyboard(dropdownPanelRef, close, chevronRef, isOpen, '[role="option"]');
|
|
347
|
+
return (_jsxs(_Fragment, { children: [_jsxs(SegmentWrapper, { ref: wrapperRef, children: [_jsxs(SegmentButton, { as: segment.linkProps?.href !== undefined
|
|
348
|
+
? (linkComponent ?? 'a')
|
|
349
|
+
: segment.linkProps
|
|
350
|
+
? linkComponent
|
|
351
|
+
: undefined, ...(segment.linkProps ?? {}), "$isInteractive": !!(segment.onClick || segment.linkProps), "$isLast": isLast, onClick: segment.onClick, "aria-label": segment.label, children: [Icon && _jsx(Icon, { size: 16, style: { flexShrink: 0 } }), _jsx("span", { children: segment.label }), segment.trailingElement] }), segment.menuElement, segment.actions && segment.actions.length > 0 && (_jsx(ActionsMenu, { actions: segment.actions, ariaLabel: `${segment.label} actions` })), hasDropdown && (_jsx(ChevronButton, { ref: chevronRef, "$isOpen": isOpen, onClick: toggle, onKeyDown: (e) => {
|
|
352
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
353
|
+
if (!isOpen) {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
toggle();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}, "aria-label": dropdownAriaLabel, "aria-expanded": isOpen, "aria-haspopup": 'listbox', children: _jsx(ChevronDown, { size: 16 }) }))] }), hasDropdown &&
|
|
359
|
+
isOpen &&
|
|
360
|
+
dropdownPos &&
|
|
361
|
+
createPortal(_jsx(Layer, { children: _jsxs(DropdownPanel, { ref: dropdownPanelRef, "data-breadcrumb-dropdown": true, "$top": dropdownPos.top, "$left": dropdownPos.left, role: 'listbox', "aria-label": dropdownAriaLabel, onKeyDown: handleDropdownKeyDown, children: [segment.dropdownTitle && (_jsx(DropdownTitle, { children: segment.dropdownTitle })), segment.dropdownItems?.map((item) => (_jsx(DropdownEntry, { item: item, onSelect: handleSelect, linkComponent: linkComponent }, item.key)))] }) }), document.body), !isLast && _jsx(Separator, { "aria-hidden": 'true' })] }));
|
|
362
|
+
};
|
|
363
|
+
const BreadcrumbBarWrapper = styled.div `
|
|
364
|
+
display: flex;
|
|
365
|
+
align-items: center;
|
|
366
|
+
flex: 1;
|
|
367
|
+
min-width: 0;
|
|
368
|
+
overflow-x: auto;
|
|
369
|
+
padding-left: 0.5rem;
|
|
370
|
+
`;
|
|
371
|
+
export const C3BreadcrumbBar = ({ segments, linkComponent, }) => (_jsx(BreadcrumbBarWrapper, { role: 'navigation', "aria-label": 'Breadcrumb', children: segments.map((segment, index) => (_jsx(BreadcrumbSegmentComponent, { segment: segment, isLast: index === segments.length - 1, linkComponent: linkComponent }, segment.key))) }));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import type { C3NavigationV2Props } from './c3-navigation-v2.types';
|
|
3
|
+
/**
|
|
4
|
+
* Sets CSS custom properties on :root so consumers can derive layout offsets
|
|
5
|
+
* without hard-coding pixel values:
|
|
6
|
+
* --c3-header-height: height of the fixed header (always 3rem)
|
|
7
|
+
* --c3-sidebar-width: current sidebar width (updates on expand/collapse)
|
|
8
|
+
*/
|
|
9
|
+
export declare const C3NavigationV2: ({ app, skipToContentTargetId, skipToContentLabel, headerAriaLabel, breadcrumbs, tools, globalActions, sidebar, linkComponent, headerTrailingContent, activeToolKey, onActiveToolChange, }: C3NavigationV2Props) => JSX.Element;
|