Improve toolbar
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/canon': patch
|
||||
---
|
||||
|
||||
Add new Toolbar component to Canon.
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.canon-MenuPositioner {
|
||||
outline: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.canon-MenuPopup {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const tabRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
@@ -49,7 +55,34 @@ export const Toolbar = (props: ToolbarProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames.toolbarOptions}>
|
||||
<ButtonIcon icon={<RiMore2Line />} variant="secondary" />
|
||||
{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>
|
||||
@@ -67,7 +100,7 @@ export const Toolbar = (props: ToolbarProps) => {
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
<Indicators
|
||||
<ToolbarTabsIndicators
|
||||
tabRefs={tabRefs}
|
||||
tabsRef={tabsRef}
|
||||
hoveredKey={hoveredKey}
|
||||
@@ -79,6 +112,11 @@ 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();
|
||||
|
||||
@@ -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) => (
|
||||
<>
|
||||
<div
|
||||
@@ -86,19 +97,25 @@ export const WithTabs: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLayout: Story = {
|
||||
args: {},
|
||||
decorators: withLayoutDecorator,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
export const WithOptions: Story = {
|
||||
args: {
|
||||
options,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithTabsInLayout: Story = {
|
||||
export const WithOptionsAndTabs: Story = {
|
||||
args: {
|
||||
options,
|
||||
tabs,
|
||||
},
|
||||
decorators: withLayoutDecorator,
|
||||
};
|
||||
|
||||
export const WithLayout: Story = {
|
||||
args: {
|
||||
options,
|
||||
tabs,
|
||||
},
|
||||
decorators: layoutDecorator,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
|
||||
@@ -15,3 +15,5 @@
|
||||
*/
|
||||
|
||||
export * from './Toolbar';
|
||||
export * from './Indicators';
|
||||
export * from './types';
|
||||
|
||||
@@ -16,22 +16,54 @@
|
||||
|
||||
import { MutableRefObject } from 'react';
|
||||
|
||||
/**
|
||||
* Represents a tab item in the toolbar navigation.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ToolbarTab {
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an option item in the toolbar dropdown menu.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface ToolbarOption {
|
||||
label: string;
|
||||
value: string;
|
||||
onClick?: () => 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<Map<string, HTMLDivElement>>;
|
||||
tabsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
|
||||
Reference in New Issue
Block a user