Improve Header component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-07-22 20:57:20 +01:00
parent f07b3fb0da
commit aa3133bb3b
12 changed files with 575 additions and 299 deletions
@@ -2,15 +2,13 @@
import * as Table from '../Table';
import { Chip } from '../Chip';
import { TypePopup } from './TypePopup';
import { icons } from '../../../../packages/ui';
// Define a more specific type for the data object
type PropData = {
values?: string | string[];
responsive?: boolean;
default?: string;
type?: string;
};
import { PropDef } from '@/utils/propDefs';
// Use the proper PropDef type
type PropData = PropDef;
// Modify the PropsTable component to use the new type
export const PropsTable = <T extends Record<string, PropData>>({
@@ -59,6 +57,12 @@ export const PropsTable = <T extends Record<string, PropData>>({
<Chip>string</Chip>
</>
)}
{data[n].type === 'complex' && data[n].complexType && (
<TypePopup
complexType={data[n].complexType}
name={data[n].complexType.name}
/>
)}
{data[n].type === 'enum | string' && (
<>
{enumValues}
@@ -0,0 +1,79 @@
.button {
display: inline-flex;
align-items: center;
font-family: monospace;
font-size: 13px;
border-radius: 6px;
padding: 0px 8px;
height: 24px;
margin-right: 4px;
background-color: #f0f0f0;
color: #5d5d5d;
cursor: pointer;
border: none;
outline: none;
box-shadow: none;
text-decoration: underline;
}
[data-theme='dark'] .button {
background-color: #2c2c2c;
color: #fff;
}
.popover {
border: 1px solid var(--border);
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
border-radius: 6px;
background: var(--panel);
color: var(--primary);
outline: none;
/* max-width: 400px; */
transition: transform 200ms, opacity 200ms;
padding: 1rem;
margin-top: 6px;
--origin: translateY(-8px);
&[data-entering],
&[data-exiting] {
transform: var(--origin);
opacity: 0;
}
}
.arrow svg {
display: block;
fill: var(--panel);
stroke: var(--border);
stroke-width: 1px;
transform: rotate(180deg);
}
.name {
font-size: 14px;
font-weight: 500;
margin-bottom: 1rem;
}
.grid {
display: grid;
grid-template-columns: auto auto 1fr;
gap: 0.5rem 1rem;
}
.col1 {
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
padding-top: 0.25rem;
}
.col2 {
font-size: 13px;
}
.col3 {
font-size: 13px;
color: var(--text-secondary);
padding-top: 0.25rem;
}
@@ -0,0 +1,49 @@
import { ComplexTypeDef } from '@/utils/propDefs';
import { Chip } from '../Chip';
import {
Button,
Dialog,
DialogTrigger,
OverlayArrow,
Popover,
} from 'react-aria-components';
import styles from './TypePopup.module.css';
interface TypePopupProps {
complexType: ComplexTypeDef;
name: string;
}
export const TypePopup = ({ complexType, name }: TypePopupProps) => {
return (
<DialogTrigger>
<Button className={styles.button}>{name}</Button>
<Popover className={styles.popover}>
<OverlayArrow className={styles.arrow}>
<svg width={12} height={12} viewBox="0 0 12 12">
<path d="M0 0 L6 6 L12 0" />
</svg>
</OverlayArrow>
<Dialog>
<div className={styles.name}>{complexType.name}</div>
<div className={styles.grid}>
{Object.entries(complexType.properties).map(
([propName, propDef]) => [
<div key={`${propName}-name`} className={styles.col1}>
{propName}
{propDef.required ? '' : '?'}
</div>,
<div key={`${propName}-type`} className={styles.col2}>
<Chip>{propDef.type}</Chip>
</div>,
<div key={`${propName}-desc`} className={styles.col3}>
{propDef.description}
</div>,
],
)}
</div>
</Dialog>
</Popover>
</DialogTrigger>
);
};
+69
View File
@@ -0,0 +1,69 @@
import { PropsTable } from '@/components/PropsTable';
import { CodeBlock } from '@/components/CodeBlock';
import { Snippet } from '@/components/Snippet';
import { HeaderSnippet } from '@/snippets/stories-snippets';
import {
propDefs,
flexUsageSnippet,
flexDefaultSnippet,
flexFAQ1Snippet,
flexSimpleSnippet,
flexResponsiveSnippet,
flexAlignSnippet,
usage,
defaultSnippet,
} from './header.props';
import { spacingPropDefs } from '@/utils/propDefs';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Component"
title="Header"
description="A header component for the plugin."
/>
<Snippet
py={4}
preview={<HeaderSnippet story="WithAllComponents" />}
code={defaultSnippet}
/>
## Usage
<CodeBlock code={usage} />
## API reference
<PropsTable data={propDefs} />
The grid component also accepts all the spacing props from the Box component.
<PropsTable data={spacingPropDefs} />
## Examples
### Simple
A simple example of how to use the Flex component.
<CodeBlock code={flexSimpleSnippet} />
### Responsive
The Flex component also supports responsive values, making it easy to create
responsive designs.
<CodeBlock code={flexResponsiveSnippet} />
### Align
The Flex component also supports responsive alignment, making it easy to
create responsive designs.
<CodeBlock code={flexAlignSnippet} />
<Theming component="Header" />
<ChangelogComponent component="Header" />
@@ -0,0 +1,147 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const propDefs: Record<string, PropDef> = {
icon: {
type: 'enum',
values: ['ReactNode'],
},
title: {
type: 'string',
default: 'Your plugin',
},
titleLink: {
type: 'string',
default: '/',
},
breadcrumbs: {
type: 'complex',
complexType: {
name: 'Breadcrumb[]',
properties: {
label: {
type: 'string',
required: true,
description: 'Display text for the breadcrumb',
},
href: {
type: 'string',
required: true,
description: 'URL for the breadcrumb link',
},
},
},
},
customActions: {
type: 'enum',
values: ['ReactNode'],
},
menuItems: {
type: 'complex',
complexType: {
name: 'MenuItem[]',
properties: {
label: {
type: 'string',
required: true,
description: 'Display text for the menu item',
},
value: {
type: 'string',
required: true,
description: 'Unique value for the menu item',
},
onClick: {
type: '() => void',
required: false,
description: 'Callback function when menu item is clicked',
},
},
},
},
tabs: {
type: 'complex',
complexType: {
name: 'HeaderTab[]',
properties: {
id: {
type: 'string',
required: true,
description: 'Unique identifier for the tab',
},
label: {
type: 'string',
required: true,
description: 'Display text for the tab',
},
href: {
type: 'string',
required: false,
description: 'URL to navigate to when tab is clicked',
},
matchStrategy: {
type: "'exact' | 'prefix'",
required: false,
description: 'How to match the current route to highlight the tab',
},
},
},
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const usage = `import { Header } from '@backstage/ui';
<Header />`;
export const defaultSnippet = `<Header
title="My plugin"
titleLink="/"
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Settings', href: '/settings' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
]}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
/>`;
export const flexSimpleSnippet = `<Flex gap="md">
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Flex>`;
export const flexResponsiveSnippet = `<Flex gap={{ xs: 'xs', md: 'md' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Flex>`;
export const flexAlignSnippet = `<Flex align={{ xs: 'start', md: 'center' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Flex>`;
+41 -17
View File
@@ -3,12 +3,13 @@ import { TabsSnippet } from '@/snippets/stories-snippets';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
tabsRootPropDefs,
tabsListPropDefs,
tabsTabPropDefs,
tabsPanelPropDefs,
tabsPropDefs,
tabPropDefs,
tabsUsageSnippet,
tabsDefaultSnippet,
tabsWithTabPanelsSnippet,
tabsWithLinksSnippet,
tabsWithDeeplyNestedRoutesSnippet,
} from './tabs.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -31,29 +32,52 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## API reference
### Tabs.Root
### Tabs
Groups the tabs and the corresponding panels. Renders a `<div>` element.
<PropsTable data={tabsRootPropDefs} />
<PropsTable data={tabsPropDefs} />
### Tabs.List
Groups the individual tab buttons. Renders a `<div>` element.
<PropsTable data={tabsListPropDefs} />
### Tabs.Tab
### Tab
An individual interactive tab button that toggles the corresponding panel. Renders a `<button>` element.
<PropsTable data={tabsTabPropDefs} />
<PropsTable data={tabPropDefs} />
### Tabs.Panel
## Examples
A panel displayed when the corresponding tab is active. Renders a `<div>` element.
### Simple tabs
<PropsTable data={tabsPanelPropDefs} />
To connect the tabs with the panels, you need to use the `id` prop on the tab and the tab panel.
<Snippet
py={4}
preview={<TabsSnippet story="WithTabPanels" />}
code={tabsWithTabPanelsSnippet}
open
/>
### Tabs as links
You can use the `href` prop on the tab to make it a link. This will use the `react-router` under the hood to navigate to the tab. We automatically detect if this is an external URL and render a `<a>` element instead of a `<button>`.
<Snippet
py={4}
preview={<TabsSnippet story="WithMockedURLTab2" />}
code={tabsWithLinksSnippet}
open
/>
### With deeply nested routes
You can use the `matchStrategy` prop on the tab to control how the tab is matched to the current URL.
<Snippet
py={4}
preview={<TabsSnippet story="PrefixMatchingDeepNesting" />}
code={tabsWithDeeplyNestedRoutesSnippet}
open
/>
<Theming component="Tabs" />
+92 -55
View File
@@ -5,55 +5,66 @@ import {
type PropDef,
} from '@/utils/propDefs';
export const tabsRootPropDefs: Record<string, PropDef> = {
defaultValue: {
type: 'enum',
values: ['any'],
default: '0',
export const tabsPropDefs: Record<string, PropDef> = {
isDisabled: {
type: 'boolean',
},
value: {
disabledKeys: {
type: 'enum',
values: ['any'],
values: ['string[]'],
},
onValueChange: {
selectedKey: {
type: 'enum',
values: [`((value) => void)`],
values: ['string', 'null'],
},
defaultSelectedKey: {
type: 'enum',
values: ['string'],
},
onSelectionChange: {
type: 'enum',
values: [`(key: string) => void`],
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabsListPropDefs: Record<string, PropDef> = {
activateOnFocus: {
type: 'boolean',
default: 'false',
},
loop: {
type: 'boolean',
default: 'false',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabsTabPropDefs: Record<string, PropDef> = {
value: {
export const tabPropDefs: Record<string, PropDef> = {
id: {
type: 'string',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabsPanelPropDefs: Record<string, PropDef> = {
value: {
isDisabled: {
type: 'boolean',
},
href: {
type: 'string',
},
keepMounted: {
type: 'boolean',
default: 'false',
hrefLang: {
type: 'string',
},
target: {
type: 'enum',
values: ['HTMLAttributeAnchorTarget'],
},
rel: {
type: 'string',
},
matchStrategy: {
type: 'enum',
values: ['exact', 'prefix'],
},
onHoverStart: {
type: 'enum',
values: [`(e: HoverEvent) => void`],
},
onHoverEnd: {
type: 'enum',
values: [`(e: HoverEvent) => void`],
},
onHoverChange: {
type: 'enum',
values: [`(isHovering: boolean) => void`],
},
...childrenPropDefs,
...classNamePropDefs,
@@ -62,24 +73,50 @@ export const tabsPanelPropDefs: Record<string, PropDef> = {
export const tabsUsageSnippet = `import { Tabs } from '@backstage/ui';
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Tab>Tab 3</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>Content for Tab 1</Tabs.Panel>
<Tabs.Panel>Content for Tab 2</Tabs.Panel>
<Tabs.Panel>Content for Tab 3</Tabs.Panel>
</Tabs.Root>`;
<Tabs>
<TabList>
<Tab id="tab-1">Tab 1</Tab>
<Tab id="tab-2">Tab 2</Tab>
<Tab id="tab-3">Tab 3</Tab>
</TabList>
<TabPanel id="tab-1">Content for Tab 1</TabPanel>
<TabPanel id="tab-2">Content for Tab 2</TabPanel>
<TabPanel id="tab-3">Content for Tab 3</TabPanel>
</Tabs>`;
export const tabsDefaultSnippet = `<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Tab>Tab 3 With long title</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>Content for Tab 1</Tabs.Panel>
<Tabs.Panel>Content for Tab 2</Tabs.Panel>
<Tabs.Panel>Content for Tab 3</Tabs.Panel>
</Tabs.Root>`;
export const tabsDefaultSnippet = `import { Tabs } from '@backstage/ui';
<Tabs>
<TabList>
<Tab id="tab-1">Tab 1</Tab>
<Tab id="tab-2">Tab 2</Tab>
<Tab id="tab-3">Tab 3 With long title</Tab>
</TabList>
</Tabs>`;
export const tabsWithTabPanelsSnippet = `<Tabs>
<TabList>
<Tab id="settings">Settings</Tab>
<Tab id="profile">Profile</Tab>
<Tab id="preferences">Preferences</Tab>
</TabList>
<TabPanel id="settings">Settings panel content goes here</TabPanel>
<TabPanel id="profile">Profile panel content goes here</TabPanel>
<TabPanel id="preferences">Preferences panel content goes here</TabPanel>
</Tabs>`;
export const tabsWithLinksSnippet = `<Tabs>
<TabList>
<Tab id="tab-1" href="/tab-1">Tab 1</Tab>
<Tab id="tab-2" href="/tab-2">Tab 2</Tab>
<Tab id="tab-3" href="/tab-3">Tab 3 With long title</Tab>
</TabList>
</Tabs>`;
export const tabsWithDeeplyNestedRoutesSnippet = `<Tabs>
<TabList>
<Tab id="home" href="/home">Home</Tab>
<Tab id="catalog" href="/catalog" matchStrategy="prefix">Catalog</Tab>
<Tab id="mentorship" href="/mentorship" matchStrategy="prefix">Mentorship</Tab>
</TabList>
</Tabs>`;
+37 -213
View File
@@ -24,220 +24,44 @@ import * as SearchFieldStories from '../../../packages/ui/src/components/SearchF
import * as TooltipStories from '../../../packages/ui/src/components/Tooltip/Tooltip.stories';
import * as SkeletonStories from '../../../packages/ui/src/components/Skeleton/Skeleton.stories';
import * as CardStories from '../../../packages/ui/src/components/Card/Card.stories';
import * as HeaderStories from '../../../packages/ui/src/components/Header/Header.stories';
import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories';
export const BoxSnippet = ({ story }: { story: keyof typeof BoxStories }) => {
const stories = composeStories(BoxStories);
const StoryComponent = stories[story as keyof typeof stories];
// Helper function to create snippet components
const createSnippetComponent = (stories: any) => {
return function SnippetComponent({ story }: { story: string }) {
const composedStories = composeStories(stories);
const StoryComponent = composedStories[
story as keyof typeof composedStories
] as any;
return StoryComponent ? <StoryComponent /> : null;
return StoryComponent ? <StoryComponent /> : null;
};
};
export const ButtonSnippet = ({
story,
}: {
story: keyof typeof ButtonStories;
}) => {
const stories = composeStories(ButtonStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const ButtonIconSnippet = ({
story,
}: {
story: keyof typeof ButtonIconStories;
}) => {
const stories = composeStories(ButtonIconStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const ButtonLinkSnippet = ({
story,
}: {
story: keyof typeof ButtonLinkStories;
}) => {
const stories = composeStories(ButtonLinkStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const CheckboxSnippet = ({
story,
}: {
story: keyof typeof CheckboxStories;
}) => {
const stories = composeStories(CheckboxStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const ContainerSnippet = ({
story,
}: {
story: keyof typeof ContainerStories;
}) => {
const stories = composeStories(ContainerStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const FlexSnippet = ({ story }: { story: keyof typeof FlexStories }) => {
const stories = composeStories(FlexStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const TextFieldSnippet = ({
story,
}: {
story: keyof typeof TextFieldStories;
}) => {
const stories = composeStories(TextFieldStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const GridSnippet = ({ story }: { story: keyof typeof GridStories }) => {
const stories = composeStories(GridStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const IconSnippet = ({ story }: { story: keyof typeof IconStories }) => {
const stories = composeStories(IconStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const TextSnippet = ({ story }: { story: keyof typeof TextStories }) => {
const stories = composeStories(TextStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const SelectSnippet = ({
story,
}: {
story: keyof typeof SelectStories;
}) => {
const stories = composeStories(SelectStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const MenuSnippet = ({ story }: { story: keyof typeof MenuStories }) => {
const stories = composeStories(MenuStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const LinkSnippet = ({ story }: { story: keyof typeof LinkStories }) => {
const stories = composeStories(LinkStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const AvatarSnippet = ({
story,
}: {
story: keyof typeof AvatarStories;
}) => {
const stories = composeStories(AvatarStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const CollapsibleSnippet = ({
story,
}: {
story: keyof typeof CollapsibleStories;
}) => {
const stories = composeStories(CollapsibleStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const TabsSnippet = ({ story }: { story: keyof typeof TabsStories }) => {
const stories = composeStories(TabsStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const SwitchSnippet = ({
story,
}: {
story: keyof typeof SwitchStories;
}) => {
const stories = composeStories(SwitchStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const RadioGroupSnippet = ({
story,
}: {
story: keyof typeof RadioGroupStories;
}) => {
const stories = composeStories(RadioGroupStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const SearchFieldSnippet = ({
story,
}: {
story: keyof typeof SearchFieldStories;
}) => {
const stories = composeStories(SearchFieldStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const TooltipSnippet = ({
story,
}: {
story: keyof typeof TooltipStories;
}) => {
const stories = composeStories(TooltipStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const SkeletonSnippet = ({
story,
}: {
story: keyof typeof SkeletonStories;
}) => {
const stories = composeStories(SkeletonStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const CardSnippet = ({ story }: { story: keyof typeof CardStories }) => {
const stories = composeStories(CardStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
// Create snippet components using the helper function
export const BoxSnippet = createSnippetComponent(BoxStories);
export const ButtonSnippet = createSnippetComponent(ButtonStories);
export const ButtonIconSnippet = createSnippetComponent(ButtonIconStories);
export const ButtonLinkSnippet = createSnippetComponent(ButtonLinkStories);
export const CheckboxSnippet = createSnippetComponent(CheckboxStories);
export const ContainerSnippet = createSnippetComponent(ContainerStories);
export const GridSnippet = createSnippetComponent(GridStories);
export const IconSnippet = createSnippetComponent(IconStories);
export const TextFieldSnippet = createSnippetComponent(TextFieldStories);
export const TextSnippet = createSnippetComponent(TextStories);
export const FlexSnippet = createSnippetComponent(FlexStories);
export const SelectSnippet = createSnippetComponent(SelectStories);
export const MenuSnippet = createSnippetComponent(MenuStories);
export const LinkSnippet = createSnippetComponent(LinkStories);
export const AvatarSnippet = createSnippetComponent(AvatarStories);
export const CollapsibleSnippet = createSnippetComponent(CollapsibleStories);
export const RadioGroupSnippet = createSnippetComponent(RadioGroupStories);
export const TabsSnippet = createSnippetComponent(TabsStories);
export const SwitchSnippet = createSnippetComponent(SwitchStories);
export const SearchFieldSnippet = createSnippetComponent(SearchFieldStories);
export const TooltipSnippet = createSnippetComponent(TooltipStories);
export const SkeletonSnippet = createSnippetComponent(SkeletonStories);
export const CardSnippet = createSnippetComponent(CardStories);
export const HeaderSnippet = createSnippetComponent(HeaderStories);
export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories);
+5
View File
@@ -101,6 +101,11 @@ export const components: Page[] = [
slug: 'collapsible',
status: 'alpha',
},
{
title: 'Header',
slug: 'header',
status: 'alpha',
},
{
title: 'Icon',
slug: 'icon',
+21 -1
View File
@@ -2,9 +2,29 @@ import type { Breakpoint } from '@backstage/ui/src/types';
const breakpoints = ['initial', 'xs', 'sm', 'md', 'lg', 'xl'] as Breakpoint[];
export type ComplexTypeDef = {
name: string;
properties: Record<
string,
{
type: string;
required?: boolean;
description?: string;
}
>;
};
export type PropDef = {
type: 'string' | 'enum' | 'enum | string' | 'number' | 'boolean';
type:
| 'string'
| 'enum'
| 'enum | string'
| 'number'
| 'boolean'
| 'spacing'
| 'complex';
values?: string | string[];
complexType?: ComplexTypeDef;
default?: string;
required?: boolean;
responsive?: boolean;
@@ -170,10 +170,12 @@ export const WithOptions: Story = {
export const WithCustomActions: Story = {
args: {
customActions: <Button>Custom action</Button>,
menuItems,
},
decorators: [withRouter],
render: args => (
<Header {...args} customActions={<Button>Custom action</Button>} />
),
};
export const WithBreadcrumbs: Story = {
@@ -185,11 +187,25 @@ export const WithBreadcrumbs: Story = {
export const WithAllComponents: Story = {
args: {
title: 'My plugin',
titleLink: '/',
menuItems,
tabs,
breadcrumbs,
},
decorators: [withRouter],
render: args => (
<Header
{...args}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
/>
),
};
export const WithLayout: Story = {
@@ -95,10 +95,10 @@ export const WithMockedURLTab2: Story = {
</TabList>
</Tabs>
<Box mt="6" pl="2">
<Text>
<Text as="p">
Current URL is mocked to be: <strong>/tab2</strong>
</Text>
<Text>
<Text as="p">
Notice how the "Tab 2" tab is selected (highlighted) because it
matches the current path.
</Text>
@@ -273,14 +273,16 @@ export const PrefixMatchingDeepNesting: Story = {
</TabList>
</Tabs>
<Box mt="6" pl="2">
<Text>
<Text as="p">
Current URL: <strong>/catalog/users/john/details</strong>
</Text>
<Text>
<Text as="p">
The "Catalog" tab is active because it uses prefix matching and the
URL starts with "/catalog".
</Text>
<Text>This works for any level of nesting under "/catalog".</Text>
<Text as="p">
This works for any level of nesting under "/catalog".
</Text>
</Box>
</MemoryRouter>
),