Add sub navigation
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
"@remixicon/react": "^4.6.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"clsx": "^2.1.1",
|
||||
"motion": "^12.20.1",
|
||||
"react-aria-components": "^1.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -508,20 +508,24 @@ export const componentDefinitions: {
|
||||
readonly disabled: readonly [true, false];
|
||||
};
|
||||
};
|
||||
readonly Toolbar: {
|
||||
readonly Header: {
|
||||
readonly classNames: {
|
||||
readonly toolbar: 'canon-Toolbar';
|
||||
readonly toolbarContentWrapper: 'canon-ToolbarContentWrapper';
|
||||
readonly toolbarOverlay: 'canon-ToolbarOverlay';
|
||||
readonly toolbarContent: 'canon-ToolbarContent';
|
||||
readonly toolbarOptions: 'canon-ToolbarOptions';
|
||||
readonly icon: 'canon-ToolbarIcon';
|
||||
readonly name: 'canon-ToolbarName';
|
||||
readonly tabs: 'canon-ToolbarTabs';
|
||||
readonly tabList: 'canon-ToolbarTabList';
|
||||
readonly tab: 'canon-ToolbarTab';
|
||||
readonly activeIndicator: 'canon-ToolbarActiveIndicator';
|
||||
readonly hoveredIndicator: 'canon-ToolbarHoveredIndicator';
|
||||
readonly toolbar: 'canon-HeaderToolbar';
|
||||
readonly toolbarWrapper: 'canon-HeaderToolbarWrapper';
|
||||
readonly toolbarContent: 'canon-HeaderToolbarContent';
|
||||
readonly toolbarOptions: 'canon-HeaderToolbarOptions';
|
||||
readonly toolbarIcon: 'canon-HeaderToolbarIcon';
|
||||
readonly toolbarName: 'canon-HeaderToolbarName';
|
||||
readonly breadcrumbs: 'canon-HeaderBreadcrumbs';
|
||||
readonly breadcrumb: 'canon-HeaderBreadcrumb';
|
||||
readonly breadcrumbLink: 'canon-HeaderBreadcrumbLink';
|
||||
readonly breadcrumbSeparator: 'canon-HeaderBreadcrumbSeparator';
|
||||
readonly tabs: 'canon-HeaderTabs';
|
||||
readonly tabList: 'canon-HeaderTabList';
|
||||
readonly tab: 'canon-HeaderTab';
|
||||
readonly tabActive: 'canon-HeaderTabActive';
|
||||
readonly tabHovered: 'canon-HeaderTabHovered';
|
||||
readonly subNav: 'canon-HeaderSubNav';
|
||||
};
|
||||
};
|
||||
readonly Tooltip: {
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
|
||||
import type { Meta, StoryObj, StoryFn } from '@storybook/react';
|
||||
import { Header } from './Header';
|
||||
import { HeaderBreadcrumb, HeaderOption, HeaderTab } from './types';
|
||||
import {
|
||||
HeaderBreadcrumb,
|
||||
HeaderOption,
|
||||
HeaderProps,
|
||||
HeaderTab,
|
||||
} from './types';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Header',
|
||||
@@ -70,6 +75,13 @@ const options: HeaderOption[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const subNavigation: HeaderProps['subNavigation'] = {
|
||||
name: 'Sub Navigation',
|
||||
description: 'Sub Navigation Description',
|
||||
tabs: tabs,
|
||||
options: options,
|
||||
};
|
||||
|
||||
// Extract layout decorator as a reusable constant
|
||||
const layoutDecorator = [
|
||||
(Story: StoryFn) => (
|
||||
@@ -134,11 +146,19 @@ export const WithBreadcrumbs: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithTabsAndSubNavigation: Story = {
|
||||
args: {
|
||||
tabs,
|
||||
subNavigation,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAllComponents: Story = {
|
||||
args: {
|
||||
options,
|
||||
tabs,
|
||||
breadcrumbs,
|
||||
subNavigation,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -153,3 +173,16 @@ export const WithLayout: Story = {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSubNavigation: Story = {
|
||||
args: {
|
||||
options,
|
||||
tabs,
|
||||
breadcrumbs,
|
||||
subNavigation,
|
||||
},
|
||||
decorators: layoutDecorator,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,15 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Tabs, TabList, Tab as AriaTab, Link } from 'react-aria-components';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { useId, useRef, useState, useEffect } from 'react';
|
||||
import { HeaderTabsIndicators } from './Indicators';
|
||||
import { RiArrowRightSLine, RiMore2Line, RiShapesLine } from '@remixicon/react';
|
||||
import type { HeaderProps, HeaderTabProps } from './types';
|
||||
import { ButtonIcon } from '../ButtonIcon';
|
||||
import { Menu } from '../Menu';
|
||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||
import type { HeaderProps } from './types';
|
||||
import { HeaderToolbar } from './HeaderToolbar';
|
||||
import { HeaderTabs } from './HeaderTabs';
|
||||
import { HeaderSubNav } from './HeaderSubNav';
|
||||
|
||||
/**
|
||||
* A component that renders a toolbar.
|
||||
@@ -30,199 +25,18 @@ import { motion, useScroll, useTransform } from 'framer-motion';
|
||||
* @public
|
||||
*/
|
||||
export const Header = (props: HeaderProps) => {
|
||||
const { tabs, icon, name, options, breadcrumbs } = props;
|
||||
const { classNames } = useStyles('Header');
|
||||
const tabsRef = useRef<HTMLDivElement>(null);
|
||||
const tabRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const [hoveredKey, setHoveredKey] = useState<string | null>(null);
|
||||
const prevHoveredKey = useRef<string | null>(null);
|
||||
const { scrollY } = useScroll();
|
||||
const breadcrumbOpacity = useTransform(scrollY, [80, 120], [0, 1]);
|
||||
|
||||
// Refs for collision detection
|
||||
const toolbarWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarContentRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarOptionsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// State for breadcrumb visibility
|
||||
const [showBreadcrumbs, setShowBreadcrumbs] = useState(true);
|
||||
|
||||
const setTabRef = (key: string, element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
tabRefs.current.set(key, element);
|
||||
} else {
|
||||
tabRefs.current.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
// Set up resize observer
|
||||
useEffect(() => {
|
||||
const wrapper = toolbarWrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const wrapper = toolbarWrapperRef.current;
|
||||
const content = toolbarContentRef.current;
|
||||
const options = toolbarOptionsRef.current;
|
||||
|
||||
if (!wrapper || !content) return;
|
||||
|
||||
// Get dimensions
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
const wrapperWidth = wrapperRect.width;
|
||||
const wrapperPadding = 24; // 12px on each side (var(--canon-space-3))
|
||||
const availableWidth = wrapperWidth - wrapperPadding;
|
||||
|
||||
// Calculate required width for content
|
||||
const contentRect = content.getBoundingClientRect();
|
||||
const contentWidth = contentRect?.width || 0;
|
||||
|
||||
// Calculate options width (if exists)
|
||||
const optionsRect = options?.getBoundingClientRect();
|
||||
const optionsWidth = optionsRect?.width || 0;
|
||||
|
||||
// Check if we need to hide breadcrumbs
|
||||
const shouldShowBreadcrumbs =
|
||||
contentWidth + optionsWidth + 32 <= availableWidth;
|
||||
|
||||
// Only update state if the value actually changed to prevent flickering
|
||||
setShowBreadcrumbs(prev =>
|
||||
prev !== shouldShowBreadcrumbs ? shouldShowBreadcrumbs : prev,
|
||||
);
|
||||
});
|
||||
|
||||
resizeObserver.observe(wrapper);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
const { tabs, icon, name, options, breadcrumbs, subNavigation } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames.toolbar}>
|
||||
<div className={classNames.toolbarWrapper} ref={toolbarWrapperRef}>
|
||||
<div className={classNames.toolbarContent} ref={toolbarContentRef}>
|
||||
<div className={classNames.toolbarName}>
|
||||
<div className={classNames.toolbarIcon}>
|
||||
{icon || <RiShapesLine />}
|
||||
</div>
|
||||
{name || 'Your plugin'}
|
||||
</div>
|
||||
{breadcrumbs && (
|
||||
<motion.div
|
||||
className={classNames.breadcrumbs}
|
||||
style={{
|
||||
opacity: breadcrumbOpacity,
|
||||
visibility: showBreadcrumbs ? 'visible' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<RiArrowRightSLine
|
||||
size={16}
|
||||
className={classNames.breadcrumbSeparator}
|
||||
/>
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<div key={breadcrumb.label} className={classNames.breadcrumb}>
|
||||
<Link
|
||||
href={breadcrumb.href}
|
||||
className={classNames.breadcrumbLink}
|
||||
data-active={index === breadcrumbs.length - 1}
|
||||
>
|
||||
{breadcrumb.label}
|
||||
</Link>
|
||||
{index < breadcrumbs.length - 1 && (
|
||||
<RiArrowRightSLine
|
||||
size={16}
|
||||
className={classNames.breadcrumbSeparator}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames.toolbarOptions} ref={toolbarOptionsRef}>
|
||||
{options && (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
render={props => (
|
||||
<ButtonIcon
|
||||
{...props}
|
||||
size="small"
|
||||
icon={<RiMore2Line />}
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Positioner sideOffset={4} align="end">
|
||||
<Menu.Popup>
|
||||
{options.map(option => (
|
||||
<Menu.Item
|
||||
key={option.value}
|
||||
onClick={() => option.onClick?.()}
|
||||
>
|
||||
{option.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
</Menu.Root>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{tabs && (
|
||||
<Tabs
|
||||
className={classNames.tabs}
|
||||
ref={tabsRef}
|
||||
keyboardActivation="manual"
|
||||
>
|
||||
<TabList className={classNames.tabList} aria-label="Toolbar tabs">
|
||||
{tabs.map((tab, index) => {
|
||||
return (
|
||||
<Tab
|
||||
key={index}
|
||||
tab={tab}
|
||||
setTabRef={setTabRef}
|
||||
setHoveredKey={setHoveredKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
<HeaderTabsIndicators
|
||||
tabRefs={tabRefs}
|
||||
tabsRef={tabsRef}
|
||||
hoveredKey={hoveredKey}
|
||||
prevHoveredKey={prevHoveredKey}
|
||||
/>
|
||||
</Tabs>
|
||||
)}
|
||||
<HeaderToolbar
|
||||
icon={icon}
|
||||
name={name}
|
||||
options={options}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
<HeaderTabs tabs={tabs} />
|
||||
<HeaderSubNav subNavigation={subNavigation} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A component that renders a toolbar tab.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
const Tab = (props: HeaderTabProps) => {
|
||||
const { tab, setTabRef, setHoveredKey } = props;
|
||||
const id = useId();
|
||||
const { classNames } = useStyles('Header');
|
||||
|
||||
return (
|
||||
<AriaTab
|
||||
id={id}
|
||||
className={classNames.tab}
|
||||
ref={el => setTabRef(id, el as HTMLDivElement)}
|
||||
onHoverStart={() => setHoveredKey(id)}
|
||||
onHoverEnd={() => setHoveredKey(null)}
|
||||
href={tab.href}
|
||||
>
|
||||
{tab.label}
|
||||
</AriaTab>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import type { HeaderProps } from './types';
|
||||
import { Heading } from '../Heading';
|
||||
import { Text } from '../Text';
|
||||
import { Flex } from '../Flex';
|
||||
import { Menu } from '../Menu';
|
||||
import { ButtonIcon } from '../ButtonIcon';
|
||||
import { RiMore2Line } from '@remixicon/react';
|
||||
|
||||
export const HeaderSubNav = (props: Pick<HeaderProps, 'subNavigation'>) => {
|
||||
const { subNavigation } = props;
|
||||
|
||||
if (!subNavigation) return null;
|
||||
|
||||
return (
|
||||
<Flex pl="4" pr="2" mt="6" justify="between">
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading variant="title4">{subNavigation.name}</Heading>
|
||||
<Text>{subNavigation.description}</Text>
|
||||
</Flex>
|
||||
<div>
|
||||
{subNavigation.options && (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
render={props => (
|
||||
<ButtonIcon
|
||||
{...props}
|
||||
size="small"
|
||||
icon={<RiMore2Line />}
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Positioner sideOffset={4} align="end">
|
||||
<Menu.Popup>
|
||||
{subNavigation.options.map(option => (
|
||||
<Menu.Item
|
||||
key={option.value}
|
||||
onClick={() => option.onClick?.()}
|
||||
>
|
||||
{option.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
</Menu.Root>
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Tabs, TabList, Tab as AriaTab } from 'react-aria-components';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { useId, useRef, useState } from 'react';
|
||||
import { HeaderTabsIndicators } from './HeaderTabsIndicators';
|
||||
import type { HeaderProps, HeaderTabProps } from './types';
|
||||
|
||||
/**
|
||||
* A component that renders header tabs.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const HeaderTabs = (props: HeaderProps) => {
|
||||
const { tabs } = props;
|
||||
const { classNames } = useStyles('Header');
|
||||
const tabsRef = useRef<HTMLDivElement>(null);
|
||||
const tabRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const [hoveredKey, setHoveredKey] = useState<string | null>(null);
|
||||
const prevHoveredKey = useRef<string | null>(null);
|
||||
|
||||
const setTabRef = (key: string, element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
tabRefs.current.set(key, element);
|
||||
} else {
|
||||
tabRefs.current.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
if (!tabs) return null;
|
||||
|
||||
return (
|
||||
<Tabs className={classNames.tabs} ref={tabsRef} keyboardActivation="manual">
|
||||
<TabList className={classNames.tabList} aria-label="Toolbar tabs">
|
||||
{tabs.map((tab, index) => {
|
||||
return (
|
||||
<Tab
|
||||
key={index}
|
||||
tab={tab}
|
||||
setTabRef={setTabRef}
|
||||
setHoveredKey={setHoveredKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
<HeaderTabsIndicators
|
||||
tabRefs={tabRefs}
|
||||
tabsRef={tabsRef}
|
||||
hoveredKey={hoveredKey}
|
||||
prevHoveredKey={prevHoveredKey}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A component that renders a toolbar tab.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
const Tab = (props: HeaderTabProps) => {
|
||||
const { tab, setTabRef, setHoveredKey } = props;
|
||||
const id = useId();
|
||||
const { classNames } = useStyles('Header');
|
||||
|
||||
return (
|
||||
<AriaTab
|
||||
id={id}
|
||||
className={classNames.tab}
|
||||
ref={el => setTabRef(id, el as HTMLDivElement)}
|
||||
onHoverStart={() => setHoveredKey(id)}
|
||||
onHoverEnd={() => setHoveredKey(null)}
|
||||
href={tab.href}
|
||||
>
|
||||
{tab.label}
|
||||
</AriaTab>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Link } from 'react-aria-components';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { RiArrowRightSLine, RiMore2Line, RiShapesLine } from '@remixicon/react';
|
||||
import type { HeaderProps } from './types';
|
||||
import { ButtonIcon } from '../ButtonIcon';
|
||||
import { Menu } from '../Menu';
|
||||
import { motion, useScroll, useTransform } from 'motion/react';
|
||||
|
||||
/**
|
||||
* A component that renders a toolbar.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const HeaderToolbar = (props: HeaderProps) => {
|
||||
const { icon, name, options, breadcrumbs } = props;
|
||||
const { classNames } = useStyles('Header');
|
||||
|
||||
const { scrollY } = useScroll();
|
||||
const breadcrumbOpacity = useTransform(scrollY, [80, 120], [0, 1]);
|
||||
|
||||
// Refs for collision detection
|
||||
const toolbarWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarContentRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarOptionsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// State for breadcrumb visibility
|
||||
const [showBreadcrumbs, setShowBreadcrumbs] = useState(true);
|
||||
|
||||
// Set up resize observer
|
||||
useEffect(() => {
|
||||
const wrapper = toolbarWrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const wrapper = toolbarWrapperRef.current;
|
||||
const content = toolbarContentRef.current;
|
||||
const options = toolbarOptionsRef.current;
|
||||
|
||||
if (!wrapper || !content) return;
|
||||
|
||||
// Get dimensions
|
||||
const wrapperRect = wrapper.getBoundingClientRect();
|
||||
const wrapperWidth = wrapperRect.width;
|
||||
const wrapperPadding = 24; // 12px on each side (var(--canon-space-3))
|
||||
const availableWidth = wrapperWidth - wrapperPadding;
|
||||
|
||||
// Calculate required width for content
|
||||
const contentRect = content.getBoundingClientRect();
|
||||
const contentWidth = contentRect?.width || 0;
|
||||
|
||||
// Calculate options width (if exists)
|
||||
const optionsRect = options?.getBoundingClientRect();
|
||||
const optionsWidth = optionsRect?.width || 0;
|
||||
|
||||
// Check if we need to hide breadcrumbs
|
||||
const shouldShowBreadcrumbs =
|
||||
contentWidth + optionsWidth + 32 <= availableWidth;
|
||||
|
||||
// Only update state if the value actually changed to prevent flickering
|
||||
setShowBreadcrumbs(prev =>
|
||||
prev !== shouldShowBreadcrumbs ? shouldShowBreadcrumbs : prev,
|
||||
);
|
||||
});
|
||||
|
||||
resizeObserver.observe(wrapper);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classNames.toolbar}>
|
||||
<div className={classNames.toolbarWrapper} ref={toolbarWrapperRef}>
|
||||
<div className={classNames.toolbarContent} ref={toolbarContentRef}>
|
||||
<div className={classNames.toolbarName}>
|
||||
<div className={classNames.toolbarIcon}>
|
||||
{icon || <RiShapesLine />}
|
||||
</div>
|
||||
{name || 'Your plugin'}
|
||||
</div>
|
||||
{breadcrumbs && (
|
||||
<motion.div
|
||||
className={classNames.breadcrumbs}
|
||||
style={{
|
||||
opacity: breadcrumbOpacity,
|
||||
visibility: showBreadcrumbs ? 'visible' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<RiArrowRightSLine
|
||||
size={16}
|
||||
className={classNames.breadcrumbSeparator}
|
||||
/>
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<div key={breadcrumb.label} className={classNames.breadcrumb}>
|
||||
<Link
|
||||
href={breadcrumb.href}
|
||||
className={classNames.breadcrumbLink}
|
||||
data-active={index === breadcrumbs.length - 1}
|
||||
>
|
||||
{breadcrumb.label}
|
||||
</Link>
|
||||
{index < breadcrumbs.length - 1 && (
|
||||
<RiArrowRightSLine
|
||||
size={16}
|
||||
className={classNames.breadcrumbSeparator}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
<div className={classNames.toolbarOptions} ref={toolbarOptionsRef}>
|
||||
{options && (
|
||||
<Menu.Root>
|
||||
<Menu.Trigger
|
||||
render={props => (
|
||||
<ButtonIcon
|
||||
{...props}
|
||||
size="small"
|
||||
icon={<RiMore2Line />}
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Positioner sideOffset={4} align="end">
|
||||
<Menu.Popup>
|
||||
{options.map(option => (
|
||||
<Menu.Item
|
||||
key={option.value}
|
||||
onClick={() => option.onClick?.()}
|
||||
>
|
||||
{option.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
</Menu.Root>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -15,5 +15,5 @@
|
||||
*/
|
||||
|
||||
export * from './Header';
|
||||
export * from './Indicators';
|
||||
export * from './HeaderTabsIndicators';
|
||||
export * from './types';
|
||||
|
||||
@@ -252,6 +252,7 @@ export const componentDefinitions = {
|
||||
tab: 'canon-HeaderTab',
|
||||
tabActive: 'canon-HeaderTabActive',
|
||||
tabHovered: 'canon-HeaderTabHovered',
|
||||
subNav: 'canon-HeaderSubNav',
|
||||
},
|
||||
},
|
||||
Tooltip: {
|
||||
|
||||
@@ -3815,6 +3815,7 @@ __metadata:
|
||||
globals: "npm:^15.11.0"
|
||||
lightningcss: "npm:^1.29.1"
|
||||
mini-css-extract-plugin: "npm:^2.9.2"
|
||||
motion: "npm:^12.20.1"
|
||||
react: "npm:^18.0.2"
|
||||
react-aria-components: "npm:^1.10.1"
|
||||
react-dom: "npm:^18.0.2"
|
||||
@@ -32756,6 +32757,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"framer-motion@npm:^12.20.1":
|
||||
version: 12.20.1
|
||||
resolution: "framer-motion@npm:12.20.1"
|
||||
dependencies:
|
||||
motion-dom: "npm:^12.20.1"
|
||||
motion-utils: "npm:^12.19.0"
|
||||
tslib: "npm:^2.4.0"
|
||||
peerDependencies:
|
||||
"@emotion/is-prop-valid": "*"
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@emotion/is-prop-valid":
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10/ba36e76dbf66c856f0c5d3cfb80973bb0ef95ca9169fc16b22560a7bc55fbedcf63205266dba7beecac5152c4cd140097ff504c5d529beffca86cd85f97bf5e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"framer-motion@npm:^6.5.1":
|
||||
version: 6.5.1
|
||||
resolution: "framer-motion@npm:6.5.1"
|
||||
@@ -40180,6 +40203,43 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"motion-dom@npm:^12.20.1":
|
||||
version: 12.20.1
|
||||
resolution: "motion-dom@npm:12.20.1"
|
||||
dependencies:
|
||||
motion-utils: "npm:^12.19.0"
|
||||
checksum: 10/b92d733fdfe3f66511f73c7f84f9483a0529eadf6ad2ab633cef584d161bcd1af786d257776e1fe7cd2b4e2c9c10912773f96ce9e2bd5b45c6c33ba6d9edfe86
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"motion-utils@npm:^12.19.0":
|
||||
version: 12.19.0
|
||||
resolution: "motion-utils@npm:12.19.0"
|
||||
checksum: 10/a6fe6b329bf4e2070bb95788218f36f0d7ea6cc3098e6ea2646ed68d5be88d25fa30e55284463ace78d19a5e19714875a7b70055bd86ae6e52a5fdee08438470
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"motion@npm:^12.20.1":
|
||||
version: 12.20.1
|
||||
resolution: "motion@npm:12.20.1"
|
||||
dependencies:
|
||||
framer-motion: "npm:^12.20.1"
|
||||
tslib: "npm:^2.4.0"
|
||||
peerDependencies:
|
||||
"@emotion/is-prop-valid": "*"
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@emotion/is-prop-valid":
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10/49057c8d5812207209ca078a67ee7467d2dca7abacdcf2db486cd28c3ca105a7fe2504c34407df201c85035f6df1c806954af94914417ee89d721133fc4e92ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mri@npm:1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "mri@npm:1.1.4"
|
||||
|
||||
Reference in New Issue
Block a user