From b0a6c8e6e2eab72a7dfd8e0bcb143c2bd8d4b0e5 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 29 Jun 2025 20:43:38 +0100 Subject: [PATCH] Improve toolbar Signed-off-by: Charles de Dreuille --- .changeset/early-trains-relax.md | 5 + packages/canon/css/styles.css | 131 ++++++++++++++++++ packages/canon/report.api.md | 16 +++ .../canon/src/components/Menu/Menu.styles.css | 1 + .../src/components/Toolbar/Indicators.tsx | 7 +- .../canon/src/components/Toolbar/Toolbar.tsx | 46 +++++- .../components/Toolbar/Tooltip.stories.tsx | 33 +++-- .../canon/src/components/Toolbar/index.tsx | 2 + .../canon/src/components/Toolbar/types.ts | 32 +++++ 9 files changed, 260 insertions(+), 13 deletions(-) create mode 100644 .changeset/early-trains-relax.md diff --git a/.changeset/early-trains-relax.md b/.changeset/early-trains-relax.md new file mode 100644 index 0000000000..6e41593160 --- /dev/null +++ b/.changeset/early-trains-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': patch +--- + +Add new Toolbar component to Canon. diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 4fbd0cf035..9ae1899c52 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9644,6 +9644,7 @@ } .canon-MenuPositioner { + z-index: 100; outline: 0; } @@ -10264,6 +10265,136 @@ height: 1rem; } +.canon-Toolbar { + z-index: 10; + padding-top: 8px; + position: sticky; + top: 0; + + &:before { + content: ""; + background-color: var(--canon-bg); + z-index: 0; + height: 16px; + position: absolute; + top: 0; + left: 0; + right: 0; + } +} + +.canon-ToolbarContentWrapper { + z-index: 1; + background-color: var(--canon-bg-surface-1); + padding-inline: var(--canon-space-3); + border-radius: var(--canon-radius-2); + border: 1px solid var(--canon-border); + color: var(--canon-fg-primary); + flex-direction: row; + justify-content: space-between; + align-items: center; + height: 52px; + display: flex; + position: relative; +} + +.canon-ToolbarContent { + align-items: center; + gap: var(--canon-space-2); + flex-direction: row; + display: flex; +} + +.canon-ToolbarName { + align-items: center; + gap: var(--canon-space-2); + font-size: var(--canon-font-size-3); + font-weight: var(--canon-font-weight-regular); + flex-direction: row; + display: flex; +} + +.canon-ToolbarIcon { + width: 16px; + height: 16px; + color: var(--canon-fg-primary); + + & svg { + width: 100%; + height: 100%; + } +} + +.canon-ToolbarTabs { + background-color: var(--canon-bg-surface-1); + padding-inline: var(--canon-space-1); + border-radius: var(--canon-radius-2); + border: 1px solid var(--canon-border); + --active-tab-left: 0px; + --active-tab-right: 0px; + --active-tab-top: 0px; + --active-tab-bottom: 0px; + --active-tab-width: 0px; + --active-tab-height: 0px; + --hovered-tab-left: 0px; + --hovered-tab-right: 0px; + --hovered-tab-top: 0px; + --hovered-tab-bottom: 0px; + --hovered-tab-width: 0px; + --hovered-tab-height: 0px; + --hovered-tab-opacity: 0; + --hovered-transition-duration: 0s; + flex-direction: row; + align-items: center; + margin-top: 8px; + display: flex; + position: relative; +} + +.canon-ToolbarTabList { + flex-direction: row; + display: flex; +} + +.canon-ToolbarTab { + font-size: var(--canon-font-size-3); + color: var(--canon-fg-primary); + cursor: pointer; + z-index: 2; + height: 36px; + padding-inline: var(--canon-space-2); + justify-content: center; + align-items: center; + display: flex; + position: relative; +} + +.canon-ToolbarActiveIndicator { + content: ""; + left: calc(var(--active-tab-left) + var(--canon-space-2)); + width: calc(var(--active-tab-width) - var(--canon-space-4)); + background-color: var(--canon-fg-primary); + opacity: 1; + border-radius: 4px; + height: 1px; + transition: all .25s ease-out; + position: absolute; + bottom: -1px; +} + +.canon-ToolbarHoveredIndicator { + content: ""; + left: var(--hovered-tab-left); + top: calc(var(--hovered-tab-top) + 4px); + width: var(--hovered-tab-width); + height: calc(var(--hovered-tab-height) - 8px); + background-color: var(--canon-gray-2); + opacity: var(--hovered-tab-opacity); + transition: left var(--hovered-transition-duration) ease-out, top var(--hovered-transition-duration) ease-out, width var(--hovered-transition-duration) ease-out, height var(--hovered-transition-duration) ease-out, opacity .15s ease-out; + border-radius: 4px; + position: absolute; +} + .canon-TooltipPopup { box-sizing: border-box; transform-origin: var(--transform-origin); diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 372b82436b..49c647f9ea 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -508,6 +508,22 @@ export const componentDefinitions: { readonly disabled: readonly [true, false]; }; }; + readonly Toolbar: { + 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 Tooltip: { readonly classNames: { readonly trigger: 'canon-TooltipTrigger'; diff --git a/packages/canon/src/components/Menu/Menu.styles.css b/packages/canon/src/components/Menu/Menu.styles.css index 7ccaa7a634..37fa4731e5 100644 --- a/packages/canon/src/components/Menu/Menu.styles.css +++ b/packages/canon/src/components/Menu/Menu.styles.css @@ -1,5 +1,6 @@ .canon-MenuPositioner { outline: 0; + z-index: 100; } .canon-MenuPopup { diff --git a/packages/canon/src/components/Toolbar/Indicators.tsx b/packages/canon/src/components/Toolbar/Indicators.tsx index cc4c92ed5f..b9599833ff 100644 --- a/packages/canon/src/components/Toolbar/Indicators.tsx +++ b/packages/canon/src/components/Toolbar/Indicators.tsx @@ -19,7 +19,12 @@ import { useStyles } from '../../hooks/useStyles'; import { useContext, useEffect, useCallback } from 'react'; import type { ToolbarIndicatorsProps } from './types'; -export const Indicators = (props: ToolbarIndicatorsProps) => { +/** + * A component that renders the indicators for the toolbar. + * + * @public + */ +export const ToolbarTabsIndicators = (props: ToolbarIndicatorsProps) => { const { tabRefs, tabsRef, hoveredKey, prevHoveredKey } = props; const { classNames } = useStyles('Toolbar'); const state = useContext(TabListStateContext); diff --git a/packages/canon/src/components/Toolbar/Toolbar.tsx b/packages/canon/src/components/Toolbar/Toolbar.tsx index 9ef4d0edbd..e7d5fd855b 100644 --- a/packages/canon/src/components/Toolbar/Toolbar.tsx +++ b/packages/canon/src/components/Toolbar/Toolbar.tsx @@ -17,13 +17,19 @@ import { Tabs, TabList, Tab as AriaTab } from 'react-aria-components'; import { useStyles } from '../../hooks/useStyles'; import { useId, useRef, useState } from 'react'; -import { Indicators } from './Indicators'; +import { ToolbarTabsIndicators } from './Indicators'; import { RiMore2Line, RiShapesLine } from '@remixicon/react'; import type { ToolbarProps, ToolbarTabProps } from './types'; import { ButtonIcon } from '../ButtonIcon'; +import { Menu } from '../Menu'; +/** + * A component that renders a toolbar. + * + * @public + */ export const Toolbar = (props: ToolbarProps) => { - const { tabs, icon, name } = props; + const { tabs, icon, name, options } = props; const { classNames } = useStyles('Toolbar'); const tabsRef = useRef(null); const tabRefs = useRef>(new Map()); @@ -49,7 +55,34 @@ export const Toolbar = (props: ToolbarProps) => {
- } variant="secondary" /> + {options && ( + + ( + } + variant="secondary" + /> + )} + /> + + + + {options.map(option => ( + option.onClick?.()} + > + {option.label} + + ))} + + + + + )}
@@ -67,7 +100,7 @@ export const Toolbar = (props: ToolbarProps) => { ); })} - { ); }; +/** + * A component that renders a toolbar tab. + * + * @public + */ const Tab = (props: ToolbarTabProps) => { const { tab, setTabRef, setHoveredKey } = props; const id = useId(); diff --git a/packages/canon/src/components/Toolbar/Tooltip.stories.tsx b/packages/canon/src/components/Toolbar/Tooltip.stories.tsx index 67c02b3e9e..93efd7efc8 100644 --- a/packages/canon/src/components/Toolbar/Tooltip.stories.tsx +++ b/packages/canon/src/components/Toolbar/Tooltip.stories.tsx @@ -34,8 +34,19 @@ const tabs = [ }, ]; +const options = [ + { + label: 'Settings', + value: 'settings', + }, + { + label: 'Invite new members', + value: 'invite-new-members', + }, +]; + // Extract layout decorator as a reusable constant -const withLayoutDecorator = [ +const layoutDecorator = [ (Story: StoryFn) => ( <>
void; +} + +/** + * Props for the main Toolbar component. + * + * @public + */ export interface ToolbarProps { icon?: React.ReactNode; name?: string; tabs?: ToolbarTab[]; + options?: ToolbarOption[]; } +/** + * Props for individual toolbar tab components. + * + * @public + */ export interface ToolbarTabProps { tab: ToolbarTab; setTabRef: (key: string, element: HTMLDivElement | null) => void; setHoveredKey: (key: string | null) => void; } +/** + * Props for the toolbar indicators component that handles tab highlighting and animation. + * + * @public + */ export interface ToolbarIndicatorsProps { tabRefs: MutableRefObject>; tabsRef: MutableRefObject;