Improve Header component
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>`;
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -101,6 +101,11 @@ export const components: Page[] = [
|
||||
slug: 'collapsible',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Header',
|
||||
slug: 'header',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Icon',
|
||||
slug: 'icon',
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user