From 912907e28e0d3d83dff7df981eca5487700cf6f9 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 30 Jun 2025 14:22:17 +0100 Subject: [PATCH] Breadcrumb responsive improvements. Signed-off-by: Charles de Dreuille --- .../src/components/Toolbar/Toolbar.styles.css | 41 ++++++- .../canon/src/components/Toolbar/Toolbar.tsx | 105 ++++++++++++++++-- .../components/Toolbar/Tooltip.stories.tsx | 30 ++++- .../canon/src/components/Toolbar/types.ts | 12 ++ .../canon/src/utils/componentDefinitions.ts | 4 + 5 files changed, 180 insertions(+), 12 deletions(-) diff --git a/packages/canon/src/components/Toolbar/Toolbar.styles.css b/packages/canon/src/components/Toolbar/Toolbar.styles.css index a69e698c84..ed48b008d0 100644 --- a/packages/canon/src/components/Toolbar/Toolbar.styles.css +++ b/packages/canon/src/components/Toolbar/Toolbar.styles.css @@ -61,6 +61,7 @@ gap: var(--canon-space-2); font-size: var(--canon-font-size-3); font-weight: var(--canon-font-weight-regular); + flex-shrink: 0; } .canon-ToolbarIcon { @@ -74,6 +75,45 @@ } } +.canon-ToolbarBreadcrumbs { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--canon-space-2); + font-size: var(--canon-font-size-3); + font-weight: var(--canon-font-weight-regular); +} + +.canon-ToolbarBreadcrumb { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--canon-space-2); +} + +.canon-ToolbarBreadcrumbLink { + color: var(--canon-fg-secondary); + text-decoration: none; + cursor: pointer; + + &[data-active='true'] { + color: var(--canon-fg-primary); + } +} + +.canon-ToolbarBreadcrumbSeparator { + width: 16px; + height: 16px; + color: var(--canon-fg-secondary); +} + +.canon-ToolbarOptions { + position: absolute; + right: var(--canon-space-2); + top: 50%; + transform: translateY(-50%); +} + .canon-ToolbarTabs { display: flex; flex-direction: row; @@ -84,7 +124,6 @@ border-radius: var(--canon-radius-2); border: 1px solid var(--canon-border); margin-top: 8px; - overflow: hidden; /* Initialize CSS variables */ --active-tab-left: 0px; diff --git a/packages/canon/src/components/Toolbar/Toolbar.tsx b/packages/canon/src/components/Toolbar/Toolbar.tsx index e7d5fd855b..4c83c64143 100644 --- a/packages/canon/src/components/Toolbar/Toolbar.tsx +++ b/packages/canon/src/components/Toolbar/Toolbar.tsx @@ -14,14 +14,15 @@ * limitations under the License. */ -import { Tabs, TabList, Tab as AriaTab } from 'react-aria-components'; +import { Tabs, TabList, Tab as AriaTab, Link } from 'react-aria-components'; import { useStyles } from '../../hooks/useStyles'; -import { useId, useRef, useState } from 'react'; +import { useId, useRef, useState, useEffect } from 'react'; import { ToolbarTabsIndicators } from './Indicators'; -import { RiMore2Line, RiShapesLine } from '@remixicon/react'; +import { RiArrowRightSLine, RiMore2Line, RiShapesLine } from '@remixicon/react'; import type { ToolbarProps, ToolbarTabProps } from './types'; import { ButtonIcon } from '../ButtonIcon'; import { Menu } from '../Menu'; +import { motion, useScroll, useTransform } from 'framer-motion'; /** * A component that renders a toolbar. @@ -29,12 +30,22 @@ import { Menu } from '../Menu'; * @public */ export const Toolbar = (props: ToolbarProps) => { - const { tabs, icon, name, options } = props; + const { tabs, icon, name, options, breadcrumbs } = props; const { classNames } = useStyles('Toolbar'); const tabsRef = useRef(null); const tabRefs = useRef>(new Map()); const [hoveredKey, setHoveredKey] = useState(null); const prevHoveredKey = useRef(null); + const { scrollY } = useScroll(); + const breadcrumbOpacity = useTransform(scrollY, [80, 120], [0, 1]); + + // Refs for collision detection + const toolbarWrapperRef = useRef(null); + const toolbarContentRef = useRef(null); + const toolbarOptionsRef = useRef(null); + + // State for breadcrumb visibility + const [showBreadcrumbs, setShowBreadcrumbs] = useState(true); const setTabRef = (key: string, element: HTMLDivElement | null) => { if (element) { @@ -44,17 +55,94 @@ export const Toolbar = (props: ToolbarProps) => { } }; + // 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 ( <>
-
-
+
+
{icon || }
{name || 'Your plugin'}
+ {breadcrumbs && ( + + + {breadcrumbs.map((breadcrumb, index) => ( +
+ + {breadcrumb.label} + + {index < breadcrumbs.length - 1 && ( + + )} +
+ ))} +
+ )}
-
+
{options && ( {
{tabs && ( - + {tabs.map((tab, index) => { return ( { ref={el => setTabRef(id, el as HTMLDivElement)} onHoverStart={() => setHoveredKey(id)} onHoverEnd={() => setHoveredKey(null)} + href={tab.href} > {tab.label} diff --git a/packages/canon/src/components/Toolbar/Tooltip.stories.tsx b/packages/canon/src/components/Toolbar/Tooltip.stories.tsx index 3e6ee1d7bb..5840179926 100644 --- a/packages/canon/src/components/Toolbar/Tooltip.stories.tsx +++ b/packages/canon/src/components/Toolbar/Tooltip.stories.tsx @@ -16,6 +16,7 @@ import type { Meta, StoryObj, StoryFn } from '@storybook/react'; import { Toolbar } from './Toolbar'; +import { ToolbarBreadcrumb, ToolbarOption, ToolbarTab } from './types'; const meta = { title: 'Components/Toolbar', @@ -25,7 +26,7 @@ const meta = { export default meta; type Story = StoryObj; -const tabs = [ +const tabs: ToolbarTab[] = [ { label: 'Overview', }, @@ -43,7 +44,22 @@ const tabs = [ }, ]; -const options = [ +const breadcrumbs: ToolbarBreadcrumb[] = [ + { + label: 'Home', + href: '/', + }, + { + label: 'Dashboard', + href: '/dashboard', + }, + { + label: 'Settings', + href: '/settings', + }, +]; + +const options: ToolbarOption[] = [ { label: 'Settings', value: 'settings', @@ -112,10 +128,17 @@ export const WithOptions: Story = { }, }; -export const WithOptionsAndTabs: Story = { +export const WithBreadcrumbs: Story = { + args: { + breadcrumbs, + }, +}; + +export const WithAllComponents: Story = { args: { options, tabs, + breadcrumbs, }, }; @@ -123,6 +146,7 @@ export const WithLayout: Story = { args: { options, tabs, + breadcrumbs, }, decorators: layoutDecorator, parameters: { diff --git a/packages/canon/src/components/Toolbar/types.ts b/packages/canon/src/components/Toolbar/types.ts index 4efd9bcb30..d2e0f336a3 100644 --- a/packages/canon/src/components/Toolbar/types.ts +++ b/packages/canon/src/components/Toolbar/types.ts @@ -23,6 +23,7 @@ import { MutableRefObject } from 'react'; */ export interface ToolbarTab { label: string; + href?: string; } /** @@ -36,6 +37,16 @@ export interface ToolbarOption { onClick?: () => void; } +/** + * Represents a breadcrumb item in the toolbar. + * + * @public + */ +export interface ToolbarBreadcrumb { + label: string; + href: string; +} + /** * Props for the main Toolbar component. * @@ -46,6 +57,7 @@ export interface ToolbarProps { name?: string; tabs?: ToolbarTab[]; options?: ToolbarOption[]; + breadcrumbs?: ToolbarBreadcrumb[]; } /** diff --git a/packages/canon/src/utils/componentDefinitions.ts b/packages/canon/src/utils/componentDefinitions.ts index 9aa18f09d0..6773aa5f30 100644 --- a/packages/canon/src/utils/componentDefinitions.ts +++ b/packages/canon/src/utils/componentDefinitions.ts @@ -244,6 +244,10 @@ export const componentDefinitions = { toolbarOptions: 'canon-ToolbarOptions', icon: 'canon-ToolbarIcon', name: 'canon-ToolbarName', + breadcrumbs: 'canon-ToolbarBreadcrumbs', + breadcrumb: 'canon-ToolbarBreadcrumb', + breadcrumbLink: 'canon-ToolbarBreadcrumbLink', + breadcrumbSeparator: 'canon-ToolbarBreadcrumbSeparator', tabs: 'canon-ToolbarTabs', tabList: 'canon-ToolbarTabList', tab: 'canon-ToolbarTab',