Migrate all mdx files

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-01-14 23:53:30 +01:00
parent 3d98ee3f97
commit ca2e767204
100 changed files with 741 additions and 339 deletions
@@ -1,23 +0,0 @@
import { components, layoutComponents } from '@/utils/data';
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const { default: Component } = await import(`@/content/${slug}.mdx`);
return <Component />;
}
export function generateStaticParams() {
const list = [...components, ...layoutComponents];
return list.map(component => ({
slug: component.slug,
}));
}
export const dynamicParams = false;
@@ -0,0 +1,119 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const accordionPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['ReactNode', '(state: { isExpanded: boolean }) => ReactNode'],
},
defaultExpanded: {
type: 'boolean',
default: 'false',
},
isExpanded: {
type: 'boolean',
},
onExpandedChange: {
type: 'enum',
values: ['(isExpanded: boolean) => void'],
},
...classNamePropDefs,
...stylePropDefs,
};
export const accordionTriggerPropDefs: Record<string, PropDef> = {
level: {
type: 'enum',
values: ['1', '2', '3', '4', '5', '6'],
default: '3',
},
title: {
type: 'string',
},
subtitle: {
type: 'string',
},
children: {
type: 'enum',
values: ['ReactNode'],
},
...classNamePropDefs,
...stylePropDefs,
};
export const accordionPanelPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
...stylePropDefs,
};
export const accordionGroupPropDefs: Record<string, PropDef> = {
allowsMultiple: {
type: 'boolean',
default: 'false',
},
...classNamePropDefs,
...stylePropDefs,
};
export const accordionUsageSnippet = `import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
<Accordion>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>Your content</AccordionPanel>
</Accordion>`;
export const accordionWithSubtitleSnippet = `<Accordion>
<AccordionTrigger
title="Advanced Settings"
subtitle="Configure additional options"
/>
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const accordionCustomTriggerSnippet = `<Accordion>
<AccordionTrigger>
<Box>
<Text as="div" weight="bold">Custom Multi-line Trigger</Text>
<Text as="div" size="small" color="secondary">
Click to expand additional details
</Text>
</Box>
</AccordionTrigger>
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const accordionDefaultExpandedSnippet = `<Accordion defaultExpanded>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const accordionGroupSingleOpenSnippet = `<AccordionGroup>
<Accordion>
<AccordionTrigger title="First Panel" />
<AccordionPanel>Content 1</AccordionPanel>
</Accordion>
<Accordion>
<AccordionTrigger title="Second Panel" />
<AccordionPanel>Content 2</AccordionPanel>
</Accordion>
</AccordionGroup>`;
export const accordionGroupMultipleOpenSnippet = `<AccordionGroup allowsMultiple>
<Accordion>
<AccordionTrigger title="First Panel" />
<AccordionPanel>Content 1</AccordionPanel>
</Accordion>
<Accordion>
<AccordionTrigger title="Second Panel" />
<AccordionPanel>Content 2</AccordionPanel>
</Accordion>
</AccordionGroup>`;
@@ -0,0 +1,136 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
Default,
WithSubtitle,
CustomTrigger,
DefaultExpanded,
GroupSingleOpen,
GroupMultipleOpen,
} from './stories';
import {
accordionPropDefs,
accordionTriggerPropDefs,
accordionPanelPropDefs,
accordionGroupPropDefs,
accordionUsageSnippet,
accordionWithSubtitleSnippet,
accordionCustomTriggerSnippet,
accordionDefaultExpandedSnippet,
accordionGroupSingleOpenSnippet,
accordionGroupMultipleOpenSnippet,
} from './accordion.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { AccordionDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Accordion"
description="A component for showing and hiding content with animation."
/>
<Snippet
align="center"
py={4}
height={240}
preview={<Default />}
code={accordionUsageSnippet}
/>
## Usage
<CodeBlock code={accordionUsageSnippet} />
## API reference
### Accordion
Root container for the accordion. Renders a `<div>` element.
<PropsTable data={accordionPropDefs} />
### AccordionTrigger
Trigger component with built-in animated chevron icon. Renders a heading element (defaults to `<h3>`, configurable via `level` prop) wrapping a `<button>`.
<PropsTable data={accordionTriggerPropDefs} />
### AccordionPanel
Panel with the accordion content. Renders a `<div>` element.
<PropsTable data={accordionPanelPropDefs} />
### AccordionGroup
Container for managing multiple accordions. Renders a `<div>` element.
<PropsTable data={accordionGroupPropDefs} />
## Examples
### With Subtitle
Here's a view when using both title and subtitle props.
<Snippet
align="center"
py={4}
height={240}
preview={<WithSubtitle />}
code={accordionWithSubtitleSnippet}
/>
### Custom Trigger
Here's a view when providing custom multi-line content as children.
<Snippet
align="center"
py={4}
height={280}
preview={<CustomTrigger />}
code={accordionCustomTriggerSnippet}
/>
### Default Expanded
Here's a view when the panel is expanded by default.
<Snippet
align="center"
py={4}
height={280}
preview={<DefaultExpanded />}
code={accordionDefaultExpandedSnippet}
/>
### Group Single Open
Here's a view when only one accordion can be open at a time.
<Snippet
align="center"
py={4}
height={280}
preview={<GroupSingleOpen />}
code={accordionGroupSingleOpenSnippet}
/>
### Group Multiple Open
Here's a view when multiple accordions can be open simultaneously.
<Snippet
align="center"
py={4}
height={280}
preview={<GroupMultipleOpen />}
code={accordionGroupMultipleOpenSnippet}
/>
<Theming definition={AccordionDefinition} />
<ChangelogComponent component="accordion" />
@@ -0,0 +1,19 @@
'use client';
import * as stories from '@backstage/ui/src/components/Accordion/Accordion.stories';
const {
Default: DefaultStory,
WithSubtitle: WithSubtitleStory,
CustomTrigger: CustomTriggerStory,
DefaultExpanded: DefaultExpandedStory,
GroupSingleOpen: GroupSingleOpenStory,
GroupMultipleOpen: GroupMultipleOpenStory,
} = stories;
export const Default = () => <DefaultStory.Component />;
export const WithSubtitle = () => <WithSubtitleStory.Component />;
export const CustomTrigger = () => <CustomTriggerStory.Component />;
export const DefaultExpanded = () => <DefaultExpandedStory.Component />;
export const GroupSingleOpen = () => <GroupSingleOpenStory.Component />;
export const GroupMultipleOpen = () => <GroupMultipleOpenStory.Component />;
@@ -0,0 +1,89 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const avatarPropDefs: Record<string, PropDef> = {
src: {
type: 'string',
},
name: {
type: 'string',
},
size: {
type: 'enum',
values: ['x-small', 'small', 'medium', 'large', 'x-large'],
default: 'medium',
responsive: true,
},
purpose: {
type: 'enum',
values: ['informative', 'decoration'],
default: 'informative',
},
...classNamePropDefs,
...stylePropDefs,
};
export const snippetUsage = `import { Avatar } from '@backstage/ui';
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille"
/>`;
export const snippetSizes = `<Flex gap="4" direction="column">
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille" size="x-small"
/>
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille" size="small"
/>
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille" size="medium"
/>
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille" size="large"
/>
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille" size="x-large"
/>
</Flex>`;
export const snippetFallback = `<Avatar
src="https://avatars.githubusercontent.com/u/15406AAAAAAAAA"
name="Charles de Dreuille"
/>`;
export const snippetPurpose = `<Flex direction="column" gap="4">
<Flex direction="column" gap="1">
<Text variant="title-x-small">Informative (default)</Text>
<Text variant="body-medium">
Use when avatar appears alone. Announced as "Charles de Dreuille" to screen readers:
</Text>
<Flex gap="2" align="center">
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille"
purpose="informative"
/>
</Flex>
</Flex>
<Flex direction="column" gap="1">
<Text variant="title-x-small">Decoration</Text>
<Text variant="body-medium">
Use when name appears adjacent to avatar. Hidden from screen readers to avoid redundancy:
</Text>
<Flex gap="2" align="center">
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille"
purpose="decoration"
/>
<Text>Charles de Dreuille</Text>
</Flex>
</Flex>
</Flex>`;
@@ -0,0 +1,76 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Default, Sizes, Fallback, Purpose } from './stories';
import {
avatarPropDefs,
snippetUsage,
snippetSizes,
snippetFallback,
snippetPurpose,
} from './avatar.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { AvatarDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Avatar"
description="An avatar component with a fallback for initials."
/>
<Snippet
align="center"
py={4}
preview={<Default />}
code={`<Avatar src="https://avatars.githubusercontent.com/u/1540635?v=4" name="Charles de Dreuille" />`}
/>
## Usage
<CodeBlock code={snippetUsage} />
## API reference
<PropsTable data={avatarPropDefs} />
## Examples
### Sizes
Avatar sizes can be set using the `size` prop.
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={snippetSizes}
/>
### Fallback
If the image is not available, the avatar will show the initials of the name.
<Snippet
align="center"
py={4}
open
preview={<Fallback />}
code={snippetFallback}
/>
### The `purpose` prop
Control how the avatar is announced to screen readers using the `purpose` prop.
<Snippet
align="center"
py={4}
preview={<Purpose />}
code={snippetPurpose}
/>
<Theming definition={AvatarDefinition} />
<ChangelogComponent component="avatar" />
@@ -0,0 +1,15 @@
'use client';
import * as stories from '@backstage/ui/src/components/Avatar/Avatar.stories';
const {
Default: DefaultStory,
Sizes: SizesStory,
Fallback: FallbackStory,
Purpose: PurposeStory,
} = stories;
export const Default = () => <DefaultStory.Component />;
export const Sizes = () => <SizesStory.Component />;
export const Fallback = () => <FallbackStory.Component />;
export const Purpose = () => <PurposeStory.Component />;
@@ -0,0 +1,40 @@
import {
classNamePropDefs,
displayPropDefs,
heightPropDefs,
positionPropDefs,
stylePropDefs,
widthPropDefs,
type PropDef,
} from '@/utils/propDefs';
export const boxPropDefs: Record<string, PropDef> = {
as: {
type: 'enum',
values: ['div', 'span'],
default: 'div',
responsive: true,
},
...widthPropDefs,
...heightPropDefs,
...positionPropDefs,
...displayPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const snippetUsage = `import { Box } from '@backstage/ui';
<Box />`;
export const boxPreviewSnippet = `<Box>
<DecorativeBox />
</Box>`;
export const boxSimpleSnippet = `<Box padding="md" borderRadius="md">Hello World</Box>`;
export const boxResponsiveSnippet = `<Box
padding={{ xs: 'sm', md: 'md' }}
borderRadius={{ xs: 'sm', md: 'md' }}>
Hello World
</Box>`;
+66
View File
@@ -0,0 +1,66 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Default } from './stories';
import {
boxPropDefs,
snippetUsage,
boxPreviewSnippet,
boxSimpleSnippet,
boxResponsiveSnippet,
} from './box.props';
import { spacingPropDefs } from '@/utils/propDefs';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { BoxDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Layout"
title="Box"
description="Box is the lowest-level component in Backstage UI. It provides a consistent API for styling and layout."
/>
<Snippet
py={4}
preview={<Default />}
code={boxPreviewSnippet}
align="center"
/>
## Usage
<CodeBlock code={snippetUsage} />
## API reference
### Box
This is the Box component, our lowest-level component. Here are all the
available properties.
<PropsTable data={boxPropDefs} />
Padding and margin are used to create space around your component using our
predefined spacing tokens. We would recommend to use padding over margin to
avoid collapsing margins but both are available.
<PropsTable data={spacingPropDefs} />
## Examples
### Simple example
A simple example of how to use the Box component.
<CodeBlock code={boxSimpleSnippet} />
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={boxResponsiveSnippet} />
<Theming definition={BoxDefinition} />
<ChangelogComponent component="box" />
@@ -0,0 +1,7 @@
'use client';
import * as stories from '@backstage/ui/src/components/Box/Box.stories';
const { Default: DefaultStory } = stories;
export const Default = () => <DefaultStory.Component />;
@@ -0,0 +1,62 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const buttonIconPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: true,
},
icon: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
loading: { type: 'boolean', default: 'false', responsive: false },
type: {
type: 'enum',
values: ['button', 'submit', 'reset'],
default: 'button',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/ui';
<ButtonIcon />`;
export const buttonIconDefaultSnippet = `<Flex align="center">
<ButtonIcon icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="secondary" />
</Flex>`;
export const buttonIconVariantsSnippet = `<Flex align="center">
<ButtonIcon icon={<Icon name="cloud" />} variant="primary" />
<ButtonIcon icon={<Icon name="cloud" />} variant="secondary" />
</Flex>`;
export const buttonIconSizesSnippet = `<Flex align="center">
<ButtonIcon icon={<Icon name="cloud" />} size="small" />
<ButtonIcon icon={<Icon name="cloud" />} size="medium" />
</Flex>`;
export const buttonIconDisabledSnippet = `<ButtonIcon icon={<Icon name="cloud" />} isDisabled />`;
export const buttonIconLoadingSnippet = `<ButtonIcon icon={<Icon name="cloud" />} variant="primary" loading={isLoading} onPress={handleClick} />`;
export const buttonIconResponsiveSnippet = `<ButtonIcon icon={<Icon name="cloud" />} variant={{ initial: 'primary', lg: 'secondary' }} />`;
export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/ui';
<ButtonLink href="https://ui.backstage.io" target="_blank">
Button
</ButtonLink>`;
@@ -0,0 +1,99 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Variants, Sizes, Disabled, Loading } from './stories';
import {
buttonIconPropDefs,
buttonIconUsageSnippet,
buttonIconDefaultSnippet,
buttonIconVariantsSnippet,
buttonIconSizesSnippet,
buttonIconDisabledSnippet,
buttonIconLoadingSnippet,
buttonIconResponsiveSnippet,
buttonIconAsLinkSnippet,
} from './button-icon.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ButtonIconDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="ButtonIcon"
description="A button component with a single icon that can be used to trigger actions."
/>
<Snippet
align="center"
py={4}
preview={<Variants />}
code={buttonIconDefaultSnippet}
/>
## Usage
<CodeBlock code={buttonIconUsageSnippet} />
## API reference
<PropsTable data={buttonIconPropDefs} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<Variants />}
code={buttonIconVariantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={buttonIconSizesSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={buttonIconDisabledSnippet}
/>
### Loading
Here's a view when buttons are in a loading state.
<Snippet
align="center"
py={4}
open
preview={<Loading />}
code={buttonIconLoadingSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={buttonIconResponsiveSnippet} />
<Theming definition={ButtonIconDefinition} />
<ChangelogComponent component="button-icon" />
@@ -0,0 +1,15 @@
'use client';
import * as stories from '@backstage/ui/src/components/ButtonIcon/ButtonIcon.stories';
const {
Variants: VariantsStory,
Sizes: SizesStory,
Disabled: DisabledStory,
Loading: LoadingStory,
} = stories;
export const Variants = () => <VariantsStory.Component />;
export const Sizes = () => <SizesStory.Component />;
export const Disabled = () => <DisabledStory.Component />;
export const Loading = () => <LoadingStory.Component />;
@@ -0,0 +1,73 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const buttonLinkPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: true,
},
iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
href: { type: 'string', responsive: false },
hrefLang: { type: 'string', responsive: false },
target: {
type: 'enum',
values: ['HTMLAttributeAnchorTarget'],
default: '_self',
responsive: false,
},
rel: { type: 'string', responsive: false },
children: { type: 'enum', values: ['ReactNode'], responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/ui';
<ButtonLink />`;
export const buttonLinkVariantsSnippet = `<Flex align="center">
<ButtonLink iconStart={<Icon name="cloud" />} variant="primary">
Button
</ButtonLink>
<ButtonLink iconStart={<Icon name="cloud" />} variant="secondary">
Button
</ButtonLink>
</Flex>`;
export const buttonLinkSizesSnippet = `<Flex align="center">
<ButtonLink size="small">Small</ButtonLink>
<ButtonLink size="medium">Medium</ButtonLink>
</Flex>`;
export const buttonLinkIconsSnippet = `<Flex align="center">
<ButtonLink iconStart={<Icon name="cloud" />}>Button</ButtonLink>
<ButtonLink iconEnd={<Icon name="chevronRight" />}>Button</ButtonLink>
<ButtonLink
iconStart={<Icon name="cloud" />}
iconEnd={<Icon name="chevronRight" />}>
Button
</ButtonLink>
</Flex>`;
export const buttonLinkDisabledSnippet = `<Flex gap="4">
<ButtonLink variant="primary" isDisabled>
Primary
</ButtonLink>
<ButtonLink variant="secondary" isDisabled>
Secondary
</ButtonLink>
</Flex>`;
export const buttonLinkResponsiveSnippet = `<ButtonLink variant={{ initial: 'primary', lg: 'secondary' }}>
Responsive Button
</ButtonLink>`;
@@ -0,0 +1,97 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Variants, Sizes, WithIcons, Disabled } from './stories';
import {
buttonLinkPropDefs,
buttonLinkSnippetUsage,
buttonLinkVariantsSnippet,
buttonLinkSizesSnippet,
buttonLinkIconsSnippet,
buttonLinkDisabledSnippet,
buttonLinkResponsiveSnippet,
} from './button-link.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ButtonLinkDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="ButtonLink"
description="A button component that can be used as a link."
/>
<Snippet
align="center"
py={4}
preview={<Variants />}
code={buttonLinkVariantsSnippet}
/>
## Usage
<CodeBlock code={buttonLinkSnippetUsage} />
## API reference
<PropsTable data={buttonLinkPropDefs} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<Variants />}
code={buttonLinkVariantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={buttonLinkSizesSnippet}
/>
### With Icons
Here's a view when buttons have icons.
<Snippet
align="center"
py={4}
open
preview={<WithIcons />}
code={buttonLinkIconsSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={buttonLinkDisabledSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={buttonLinkResponsiveSnippet} />
<Theming definition={ButtonLinkDefinition} />
<ChangelogComponent component="button-link" />
@@ -0,0 +1,15 @@
'use client';
import * as stories from '@backstage/ui/src/components/ButtonLink/ButtonLink.stories';
const {
Variants: VariantsStory,
Sizes: SizesStory,
WithIcons: WithIconsStory,
Disabled: DisabledStory,
} = stories;
export const Variants = () => <VariantsStory.Component />;
export const Sizes = () => <SizesStory.Component />;
export const WithIcons = () => <WithIconsStory.Component />;
export const Disabled = () => <DisabledStory.Component />;
@@ -0,0 +1,77 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const buttonPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary'],
default: 'primary',
responsive: true,
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'medium',
responsive: true,
},
iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
isDisabled: { type: 'boolean', default: 'false', responsive: false },
loading: { type: 'boolean', default: 'false', responsive: false },
children: { type: 'enum', values: ['ReactNode'], responsive: false },
type: {
type: 'enum',
values: ['button', 'submit', 'reset'],
default: 'button',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const buttonSnippetUsage = `import { Button } from '@backstage/ui';
<Button />`;
export const buttonVariantsSnippet = `<Flex align="center">
<Button iconStart="cloud" variant="primary">
Button
</Button>
<Button iconStart="cloud" variant="secondary">
Button
</Button>
</Flex>`;
export const buttonSizesSnippet = `<Flex align="center">
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
</Flex>`;
export const buttonIconsSnippet = `<Flex align="center">
<Button iconStart={<Icon name="cloud" />}>Button</Button>
<Button iconEnd={<Icon name="chevronRight" />}>Button</Button>
<Button iconStart={<Icon name="cloud" />} iconEnd={<Icon name="chevronRight" />}>Button</Button>
</Flex>`;
export const buttonDisabledSnippet = `<Flex gap="4">
<Button variant="primary" isDisabled>
Primary
</Button>
<Button variant="secondary" isDisabled>
Secondary
</Button>
</Flex>`;
export const buttonResponsiveSnippet = `<Button variant={{ initial: 'primary', lg: 'secondary' }}>
Responsive Button
</Button>`;
export const buttonLoadingSnippet = `<Button variant="primary" loading={isLoading} onPress={handleClick}>
Load more items
</Button>`;
export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/ui';
<ButtonLink href="https://ui.backstage.io" target="_blank">
Button
</ButtonLink>`;
+124
View File
@@ -0,0 +1,124 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Variants, Sizes, WithIcons, Disabled, Loading } from './stories';
import { Variants as ButtonLinkVariants } from '../button-link/stories';
import {
buttonPropDefs,
buttonSnippetUsage,
buttonVariantsSnippet,
buttonSizesSnippet,
buttonIconsSnippet,
buttonDisabledSnippet,
buttonLoadingSnippet,
buttonResponsiveSnippet,
buttonAsLinkSnippet,
} from './button.props';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ButtonDefinition } from '../../../utils/definitions';
<PageTitle
title="Button"
description="A button component that can be used to trigger actions."
/>
<Snippet
align="center"
py={4}
preview={<Variants />}
code={buttonVariantsSnippet}
/>
## Usage
<CodeBlock code={buttonSnippetUsage} />
## API reference
<PropsTable data={buttonPropDefs} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<Variants />}
code={buttonVariantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={buttonSizesSnippet}
/>
### With Icons
Here's a view when buttons have icons.
<Snippet
align="center"
py={4}
open
preview={<WithIcons />}
code={buttonIconsSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={buttonDisabledSnippet}
/>
### Loading
Here's a view when buttons are in a loading state.
<Snippet
align="center"
py={4}
open
preview={<Loading />}
code={buttonLoadingSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={buttonResponsiveSnippet} />
### As Link
If you want to use a button as a link, please use the `ButtonLink` component.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkVariants />}
code={buttonAsLinkSnippet}
/>
<Theming definition={ButtonDefinition} />
<ChangelogComponent component="button" />
@@ -0,0 +1,17 @@
'use client';
import * as stories from '@backstage/ui/src/components/Button/Button.stories';
const {
Variants: VariantsStory,
Sizes: SizesStory,
WithIcons: WithIconsStory,
Disabled: DisabledStory,
Loading: LoadingStory,
} = stories;
export const Variants = () => <VariantsStory.Component />;
export const Sizes = () => <SizesStory.Component />;
export const WithIcons = () => <WithIconsStory.Component />;
export const Disabled = () => <DisabledStory.Component />;
export const Loading = () => <LoadingStory.Component />;
@@ -0,0 +1,83 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const cardPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
...stylePropDefs,
};
export const cardHeaderPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
...stylePropDefs,
};
export const cardBodyPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
...stylePropDefs,
};
export const cardFooterPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
...stylePropDefs,
};
export const cardUsageSnippet = `import { card } from '@backstage/ui';
<Card>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>`;
export const cardDefaultSnippet = `<Card>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>`;
export const cardLongBodySnippet = `<Card style={{ width: '300px', height: '200px' }}>
<CardHeader>
<Text>Header</Text>
</CardHeader>
<CardBody>
<Text>
This is the first paragraph of a long body text that demonstrates how
the Card component handles extensive content. The card should adjust
accordingly to display all the text properly while maintaining its
structure.
</Text>
<Text>
Here's a second paragraph that adds more content to our card body.
Having multiple paragraphs helps to visualize how spacing works within
the card component.
</Text>
<Text>
This third paragraph continues to add more text to ensure we have a
proper demonstration of a card with significant content. This makes it
easier to test scrolling behavior and overall layout when content
exceeds the initial view.
</Text>
</CardBody>
<CardFooter>
<Text>Footer</Text>
</CardFooter>
</Card>`;
export const cardListRowSnippet = `<Card style={{ width: '300px', height: '200px' }}>
<CardHeader>
<Text>Header</Text>
</CardHeader>
<CardBody>
<ListRow>Hello world</ListRow>
<ListRow>Hello world</ListRow>
<ListRow>Hello world</ListRow>
<ListRow>Hello world</ListRow>
...
</CardBody>
<CardFooter>
<Text>Footer</Text>
</CardFooter>
</Card>`;
+90
View File
@@ -0,0 +1,90 @@
import { PropsTable } from '@/components/PropsTable';
import { CustomSize, WithLongBody, WithListRow } from './stories';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
cardPropDefs,
cardUsageSnippet,
cardDefaultSnippet,
cardHeaderPropDefs,
cardBodyPropDefs,
cardFooterPropDefs,
cardLongBodySnippet,
cardListRowSnippet,
} from './card.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { CardDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Card"
description="A card component that can be used to display content in a box."
/>
<Snippet
align="center"
py={4}
preview={<CustomSize />}
code={cardDefaultSnippet}
/>
## Usage
<CodeBlock code={cardUsageSnippet} />
## API reference
### Card
A card component that can be used to display content in a box.
<PropsTable data={cardPropDefs} />
### CardHeader
To display a header in a card, use the `CardHeader` component. This will be fixed at the top of the card.
<PropsTable data={cardHeaderPropDefs} />
### CardBody
To display content in a card, use the `CardBody` component. This will automatically fill the card.
<PropsTable data={cardBodyPropDefs} />
### CardFooter
To display a footer in a card, use the `CardFooter` component. This will be fixed at the bottom of the card.
<PropsTable data={cardFooterPropDefs} />
## Examples
### With long body
Here's a view when card has a long body.
<Snippet
align="center"
py={4}
preview={<WithLongBody />}
code={cardLongBodySnippet}
open
/>
### With list
Here's a view when card has a list.
<Snippet
align="center"
py={4}
preview={<WithListRow />}
code={cardListRowSnippet}
open
/>
<Theming definition={CardDefinition} />
<ChangelogComponent component="card" />
@@ -0,0 +1,13 @@
'use client';
import * as stories from '@backstage/ui/src/components/Card/Card.stories';
const {
CustomSize: CustomSizeStory,
WithLongBody: WithLongBodyStory,
WithListRow: WithListRowStory,
} = stories;
export const CustomSize = () => <CustomSizeStory.Component />;
export const WithLongBody = () => <WithLongBodyStory.Component />;
export const WithListRow = () => <WithListRowStory.Component />;
@@ -0,0 +1,61 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const checkboxPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['React.ReactNode'],
responsive: false,
},
isSelected: {
type: 'enum',
values: ['boolean'],
responsive: false,
},
defaultSelected: {
type: 'enum',
values: ['boolean'],
responsive: false,
},
onChange: {
type: 'enum',
values: ['(isSelected: boolean) => void'],
responsive: false,
},
isDisabled: {
type: 'enum',
values: ['boolean'],
responsive: false,
},
isRequired: {
type: 'enum',
values: ['boolean'],
responsive: false,
},
name: {
type: 'string',
responsive: false,
},
value: {
type: 'string',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const checkboxUsageSnippet = `import { Checkbox } from '@backstage/ui';
<Checkbox>Accept terms</Checkbox>`;
export const checkboxDefaultSnippet = `<Checkbox>Accept terms and conditions</Checkbox>`;
export const checkboxVariantsSnippet = `<Flex direction="column" gap="2">
<Checkbox>Unchecked</Checkbox>
<Checkbox isSelected>Checked</Checkbox>
<Checkbox isDisabled>Disabled</Checkbox>
<Checkbox isSelected isDisabled>Checked & Disabled</Checkbox>
</Flex>`;
@@ -0,0 +1,52 @@
import { PropsTable } from '@/components/PropsTable';
import { Default, AllVariants } from './stories';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
checkboxPropDefs,
checkboxUsageSnippet,
checkboxDefaultSnippet,
checkboxVariantsSnippet,
} from './checkbox.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { CheckboxDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Checkbox"
description="A checkbox component that can be used to trigger actions."
/>
<Snippet
align="center"
py={4}
preview={<Default />}
code={checkboxDefaultSnippet}
/>
## Usage
<CodeBlock code={checkboxUsageSnippet} />
## API reference
<PropsTable data={checkboxPropDefs} />
## Examples
### All variants
Here's a view when checkboxes have different variants.
<Snippet
align="center"
py={4}
open
preview={<AllVariants />}
code={checkboxVariantsSnippet}
/>
<Theming definition={CheckboxDefinition} />
<ChangelogComponent component="checkbox" />
@@ -0,0 +1,8 @@
'use client';
import * as stories from '@backstage/ui/src/components/Checkbox/Checkbox.stories';
const { Default: DefaultStory, AllVariants: AllVariantsStory } = stories;
export const Default = () => <DefaultStory.Component />;
export const AllVariants = () => <AllVariantsStory.Component />;
@@ -0,0 +1,32 @@
import {
classNamePropDefs,
stylePropDefs,
gapPropDefs,
type PropDef,
} from '@/utils/propDefs';
export const containerPropDefs: Record<string, PropDef> = {
...gapPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const containerUsageSnippet = `import { Container } from "@backstage/ui";
<Container>Hello World!</Container>`;
export const containerDefaultSnippet = `<Container>
<DecorativeBox />
</Container>`;
export const containerSimpleSnippet = `<Container>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Container>`;
export const containerResponsiveSnippet = `<Container paddingY={{ xs: 'sm', md: 'md' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Container>`;
@@ -0,0 +1,54 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Preview } from './stories';
import {
containerPropDefs,
containerUsageSnippet,
containerDefaultSnippet,
containerSimpleSnippet,
containerResponsiveSnippet,
} from './container.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ContainerDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Layout"
title="Container"
description="The container component let you use our default max-width and center the content on the page."
/>
<Snippet
py={4}
preview={<Preview />}
code={containerDefaultSnippet}
/>
## Usage
<CodeBlock code={containerUsageSnippet} />
## API reference
<PropsTable data={containerPropDefs} />
## Examples
### Simple
A simple example of how to use the Container component.
<CodeBlock code={containerSimpleSnippet} />
### Responsive padding & margin
The Container component also supports responsive values, making it easy to
create responsive designs.
<CodeBlock code={containerResponsiveSnippet} />
<Theming definition={ContainerDefinition} />
<ChangelogComponent component="container" />
@@ -0,0 +1,7 @@
'use client';
import * as stories from '@backstage/ui/src/components/Container/Container.stories';
const { Preview: PreviewStory } = stories;
export const Preview = () => <PreviewStory.Component />;
@@ -0,0 +1,172 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const dialogTriggerPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
isOpen: {
type: 'boolean',
description: 'Whether the overlay is open by default (controlled).',
},
defaultOpen: {
type: 'boolean',
description: 'Whether the overlay is open by default (uncontrolled).',
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
description:
"Handler that is called when the overlay's open state changes.",
},
};
export const dialogPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
isOpen: {
type: 'boolean',
description: 'Whether the overlay is open by default (controlled).',
},
defaultOpen: {
type: 'boolean',
description: 'Whether the overlay is open by default (uncontrolled).',
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
description:
"Handler that is called when the overlay's open state changes.",
},
width: {
type: 'enum',
values: ['number', 'string'],
responsive: false,
},
height: {
type: 'enum',
values: ['number', 'string'],
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const dialogHeaderPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
export const dialogBodyPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
height: {
type: 'enum',
values: ['number', 'string'],
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const dialogFooterPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
export const dialogClosePropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary', 'tertiary'],
default: 'secondary',
responsive: false,
},
children: { type: 'enum', values: ['ReactNode'], responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
export const dialogUsageSnippet = `import {
Dialog,
DialogTrigger,
DialogHeader,
DialogBody,
DialogFooter,
} from '@backstage/ui';
<DialogTrigger>
<Button>Open Dialog</Button>
<Dialog>
<DialogHeader>Title</DialogHeader>
<DialogBody>Content</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">Close</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>`;
export const dialogDefaultSnippet = `<DialogTrigger>
<Button variant="secondary">Open Dialog</Button>
<Dialog>
<DialogHeader>Example Dialog</DialogHeader>
<DialogBody>
<Text>This is a basic dialog example.</Text>
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">Close</Button>
<Button variant="primary" slot="close">Save</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>`;
export const dialogFixedWidthAndHeightSnippet = `<DialogTrigger>
<Button variant="secondary">Scrollable Dialog</Button>
<Dialog>
<DialogHeader>Long Content Dialog</DialogHeader>
<DialogBody width={600} height={400}>
...
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">Cancel</Button>
<Button variant="primary" slot="close">Accept</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>`;
export const dialogWithFormSnippet = `<DialogTrigger>
<Button variant="secondary">Create User</Button>
<Dialog>
<DialogHeader>Create New User</DialogHeader>
<DialogBody>
<Flex direction="column" gap="3">
<TextField label="Name" placeholder="Enter full name" />
<TextField label="Email" placeholder="Enter email address" />
<Select label="Role">
<SelectItem>Admin</SelectItem>
<SelectItem>User</SelectItem>
<SelectItem>Viewer</SelectItem>
</Select>
</Flex>
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">Cancel</Button>
<Button variant="primary" slot="close">Create User</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>`;
export const dialogWithNoTriggerSnippet = `const [isOpen, setIsOpen] = useState(false);
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<DialogHeader>Create New User</DialogHeader>
<DialogBody>
Your content
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">Cancel</Button>
<Button variant="primary" slot="close">Create User</Button>
</DialogFooter>
</Dialog>`;
export const dialogCloseSnippet = `<Button slot="close">Close</Button>`;
+108
View File
@@ -0,0 +1,108 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Default, PreviewFixedWidthAndHeight, PreviewWithForm } from './stories';
import {
dialogPropDefs,
dialogTriggerPropDefs,
dialogHeaderPropDefs,
dialogBodyPropDefs,
dialogFooterPropDefs,
dialogClosePropDefs,
dialogUsageSnippet,
dialogDefaultSnippet,
dialogFixedWidthAndHeightSnippet,
dialogWithFormSnippet,
dialogWithNoTriggerSnippet,
dialogCloseSnippet,
} from './dialog.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { DialogDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Dialog"
description="A modal dialog component that displays content in an overlay window."
/>
<Snippet
align="center"
py={4}
preview={<Default />}
code={dialogDefaultSnippet}
/>
## Usage
<CodeBlock code={dialogUsageSnippet} />
## API reference
### DialogTrigger
Wraps a trigger element and the dialog content to handle open/close state.
<PropsTable data={dialogTriggerPropDefs} />
### Dialog
The main dialog container that renders as a modal overlay.
<PropsTable data={dialogPropDefs} />
### DialogHeader
Displays the dialog title with a built-in close button.
<PropsTable data={dialogHeaderPropDefs} />
### DialogBody
The main content area of the dialog with optional scrolling.
<PropsTable data={dialogBodyPropDefs} />
### DialogFooter
Contains action buttons or other footer content.
<PropsTable data={dialogFooterPropDefs} />
If you want to close the dialog while pressing a button, you can use the `slot="close"` prop on the button.
<CodeBlock code={dialogCloseSnippet} />
## Examples
### Fixed Width and Height
Dialog with a fixed height body that scrolls when content overflows.
<Snippet
align="center"
py={4}
preview={<PreviewFixedWidthAndHeight />}
code={dialogFixedWidthAndHeightSnippet}
/>
### Dialog with Form
Dialog containing form elements for user input.
<Snippet
align="center"
py={4}
preview={<PreviewWithForm />}
code={dialogWithFormSnippet}
/>
### Dialog with no trigger and controlled by props
You can also control the dialog using your own states.
<CodeBlock code={dialogWithNoTriggerSnippet} />
<Theming definition={DialogDefinition} />
<ChangelogComponent component="dialog" />
@@ -0,0 +1,15 @@
'use client';
import * as stories from '@backstage/ui/src/components/Dialog/Dialog.stories';
const {
Default: DefaultStory,
PreviewFixedWidthAndHeight: PreviewFixedWidthAndHeightStory,
PreviewWithForm: PreviewWithFormStory,
} = stories;
export const Default = () => <DefaultStory.Component />;
export const PreviewFixedWidthAndHeight = () => (
<PreviewFixedWidthAndHeightStory.Component />
);
export const PreviewWithForm = () => <PreviewWithFormStory.Component />;
@@ -0,0 +1,55 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const flexPropDefs: Record<string, PropDef> = {
align: {
type: 'enum',
values: ['start', 'center', 'end', 'baseline', 'stretch'],
responsive: true,
},
direction: {
type: 'enum',
values: ['row', 'column', 'row-reverse', 'column-reverse'],
responsive: true,
},
justify: {
type: 'enum',
values: ['start', 'center', 'end', 'between'],
responsive: true,
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const flexUsageSnippet = `import { Flex } from '@backstage/ui';
<Flex />`;
export const flexDefaultSnippet = `<Flex>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
</Flex>`;
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>`;
+69
View File
@@ -0,0 +1,69 @@
import { PropsTable } from '@/components/PropsTable';
import { CodeBlock } from '@/components/CodeBlock';
import { Snippet } from '@/components/Snippet';
import { Default } from './stories';
import {
flexPropDefs,
flexUsageSnippet,
flexDefaultSnippet,
flexFAQ1Snippet,
flexSimpleSnippet,
flexResponsiveSnippet,
flexAlignSnippet,
} from './flex.props';
import { spacingPropDefs } from '@/utils/propDefs';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { FlexDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Layout"
title="Flex"
description="A responsive flex container component for vertical stacking with customizable gaps."
/>
<Snippet
py={4}
align="center"
preview={<Default />}
code={flexDefaultSnippet}
/>
## Usage
<CodeBlock code={flexUsageSnippet} />
## API reference
<PropsTable data={flexPropDefs} />
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 definition={FlexDefinition} />
<ChangelogComponent component="flex" />
@@ -0,0 +1,7 @@
'use client';
import * as stories from '@backstage/ui/src/components/Flex/Flex.stories';
const { Default: DefaultStory } = stories;
export const Default = () => <DefaultStory.Component />;
@@ -0,0 +1,111 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
const columnsValues = [
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12',
];
export const gridPropDefs: Record<string, PropDef> = {
columns: {
type: 'enum | string',
values: [...columnsValues, 'auto'],
responsive: true,
default: 'auto',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const gridItemPropDefs: Record<string, PropDef> = {
colSpan: {
type: 'enum | string',
values: [...columnsValues, 'full'],
responsive: true,
},
rowSpan: {
type: 'enum | string',
values: [...columnsValues, 'full'],
responsive: true,
},
colStart: {
type: 'enum | string',
values: [...columnsValues, 'auto'],
responsive: true,
},
colEnd: {
type: 'enum | string',
values: [...columnsValues, 'auto'],
responsive: true,
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const gridUsageSnippet = `import { Grid } from '@backstage/ui';
<Grid.Root />`;
export const gridDefaultSnippet = `<Grid.Root>
<DecorativeBox />
<DecorativeBox />
<DecorativeBox />
</Grid.Root>`;
export const gridSimpleSnippet = `<Grid.Root columns="3" gap="md">
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Grid.Root>`;
export const gridComplexSnippet = `<Grid.Root columns="3" gap="md">
<Grid.Item colSpan="1">
Hello World
</Grid.Item>
<Grid.Item colSpan="2">
Hello World
</Grid.Item>
</Grid.Root>`;
export const gridMixingRowsSnippet = `<Grid.Root columns="3" gap="md">
<Grid.Item colSpan="1" rowSpan="2">
Hello World
</Grid.Item>
<Grid.Item colSpan="2">
Hello World
</Grid.Item>
<Grid.Item colSpan="2">
Hello World
</Grid.Item>
</Grid.Root>`;
export const gridResponsiveSnippet = `<Grid.Root columns={{ xs: 1, md: 3 }} gap={{ xs: 'xs', md: 'md' }}>
<Grid.Item colSpan={{ xs: 1, md: 2 }}>
<Hello World
</Grid.Item>
<Grid.Item colSpan={{ xs: 1, md: 1 }}>
Hello World
</Grid.Item>
</Grid.Root>`;
export const gridStartEndSnippet = `<Grid.Root columns="3" gap="md">
<Grid.Item colStart="2" colEnd="4">
Hello World
</Grid.Item>
</Grid.Root>`;
+99
View File
@@ -0,0 +1,99 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Default } from './stories';
import { spacingPropDefs } from '@/utils/propDefs';
import {
gridPropDefs,
gridItemPropDefs,
gridUsageSnippet,
gridDefaultSnippet,
gridSimpleSnippet,
gridComplexSnippet,
gridMixingRowsSnippet,
gridResponsiveSnippet,
gridStartEndSnippet,
} from './grid.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { GridDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Layout"
title="Grid"
description="A layout component that helps to create simple column-based layouts as well as more complex ones."
/>
<Snippet
py={4}
preview={<Default />}
code={gridDefaultSnippet}
/>
## Usage
<CodeBlock code={gridUsageSnippet} />
## API reference
### Grid.Root
This is the grid container component. It will help to define the number of
columns that will be used in the grid. You can also define the gap between the
columns. All values are responsive.
<PropsTable data={gridPropDefs} />
The grid component also accepts all the spacing props from the Box component.
<PropsTable data={spacingPropDefs} />
### Grid.Item
If you need more control over the columns, you can use the grid item
component. This will give you access to `rowSpan`, `colSpan`, `start` and
`end`. All values are responsive. This component is optional, you can use any
elements directly if you prefer.
<PropsTable data={gridItemPropDefs} />
## Examples
### Simple grid
A simple grid with 3 columns and a gap of md.
<CodeBlock code={gridSimpleSnippet} />
### Complex grid
You can also use the grid item to create more complex layouts. In this example
the first column will span 1 column and the second column will span 2 columns.
<CodeBlock code={gridComplexSnippet} />
### Mixing rows and columns
The grid item component also supports the `rowSpan` prop, which allows you to
span multiple rows within the grid layout. In this example, the first item
will span 2 rows to achieve a dynamic and flexible grid structure.
<CodeBlock code={gridMixingRowsSnippet} />
### Responsive grid
The grid component also supports responsive values, making it easy to create
responsive designs.
<CodeBlock code={gridResponsiveSnippet} />
### Start and End
The start and end props can be used to position the item in the grid.
<CodeBlock code={gridStartEndSnippet} />
<Theming definition={GridDefinition} />
<ChangelogComponent component="grid" />
@@ -0,0 +1,7 @@
'use client';
import * as stories from '@backstage/ui/src/components/Grid/Grid.stories';
const { Default: DefaultStory } = stories;
export const Default = () => <DefaultStory.Component />;
@@ -0,0 +1,141 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const propDefs: Record<string, PropDef> = {
title: {
type: 'string',
default: 'Your plugin',
},
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',
},
},
},
},
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',
},
},
},
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const usage = `import { HeaderPage } from '@backstage/ui';
<HeaderPage />`;
export const defaultSnippet = `<HeaderPage
title="Page Title"
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
]}
customActions={<Button>Custom action</Button>}
/>`;
export const withBreadcrumbs = `<HeaderPage
title="Page Title"
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Long Breadcrumb Name', href: '/long-breadcrumb' },
]}
/>`;
export const withTabs = `<HeaderPage
title="Page Title"
tabs={[
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'checks', label: 'Checks', href: '/checks' },
{ id: 'tracks', label: 'Tracks', href: '/tracks' },
{ id: 'campaigns', label: 'Campaigns', href: '/campaigns' },
{ id: 'integrations', label: 'Integrations', href: '/integrations' },
]}
/>`;
export const withCustomActions = `<HeaderPage
title="Page Title"
customActions={<Button>Custom action</Button>}
/>`;
export const withMenuItems = `<HeaderPage
title="Page Title"
menuItems={[
{ label: 'Settings', value: 'settings', onClick: () => {} },
{ label: 'Invite new members', value: 'invite-new-members', onClick: () => {} },
]}
/>`;
@@ -0,0 +1,88 @@
import { PropsTable } from '@/components/PropsTable';
import { CodeBlock } from '@/components/CodeBlock';
import { Snippet } from '@/components/Snippet';
import { WithEverything, WithLongBreadcrumbs, WithTabs, WithCustomActions, WithMenuItems } from './stories';
import {
propDefs,
usage,
simple,
defaultSnippet,
withTabs,
withBreadcrumbs,
withCustomActions,
withMenuItems,
} from './header-page.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { HeaderPageDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Component"
title="HeaderPage"
description="A header page component for the plugin that should sit under the Header component."
/>
<Snippet
py={4}
preview={<WithEverything />}
code={defaultSnippet}
/>
## Usage
<CodeBlock code={usage} />
## API reference
<PropsTable data={propDefs} />
## Examples
### With Breadcrumbs
You can add breadcrumbs to the header page to help users navigate to the previous page. The `breadcrumbs`
prop is an array of objects with a `label` and `href` property. By default we truncate the breadcrumb label to 240px.
<Snippet
open
preview={<WithLongBreadcrumbs />}
code={withBreadcrumbs}
/>
### With Tabs
You can add tabs to the header page to help users navigate to the different sections of the page. The `tabs`
prop is an array of objects with a `label` and `href` property.
<Snippet
open
preview={<WithTabs />}
code={withTabs}
/>
### With Custom Actions
You can add custom actions to the header page to help users navigate to the different sections of the page. The `customActions`
prop is a React node.
<Snippet
open
preview={<WithCustomActions />}
code={withCustomActions}
/>
### With Menu Items
You can add menu items to the header page to help users navigate to the different sections of the page. The `menuItems`
prop is an array of objects with a `label`, `value` and `onClick` property.
<Snippet
open
preview={<WithMenuItems />}
code={withMenuItems}
/>
<Theming definition={HeaderPageDefinition} />
<ChangelogComponent component="Header" />
@@ -0,0 +1,17 @@
'use client';
import * as stories from '@backstage/ui/src/components/HeaderPage/HeaderPage.stories';
const {
WithEverything: WithEverythingStory,
WithLongBreadcrumbs: WithLongBreadcrumbsStory,
WithTabs: WithTabsStory,
WithCustomActions: WithCustomActionsStory,
WithMenuItems: WithMenuItemsStory,
} = stories;
export const WithEverything = () => <WithEverythingStory.Component />;
export const WithLongBreadcrumbs = () => <WithLongBreadcrumbsStory.Component />;
export const WithTabs = () => <WithTabsStory.Component />;
export const WithCustomActions = () => <WithCustomActionsStory.Component />;
export const WithMenuItems = () => <WithMenuItemsStory.Component />;
@@ -0,0 +1,200 @@
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: '/',
},
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',
},
},
},
},
onTabSelectionChange: {
type: 'enum',
values: ['(key: string) => void'],
},
...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 simple = `<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' },
]}
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 withTabs = `
<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' },
]}
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 />} />
</>
}
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
/>
`;
export const withBreadcrumbs = `<Header
title="My plugin"
titleLink="/"
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Settings', href: '/settings' },
]}
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
/>`;
export const withHeaderPage = `<Header
title="My plugin"
titleLink="/"
breadcrumbs={...}
tabs={...}
/>
<HeaderPage
title="Page title"
menuItems={...}
tabs={...}
customActions={<Button>Custom action</Button>}
/>`;
@@ -0,0 +1,86 @@
import { PropsTable } from '@/components/PropsTable';
import { CodeBlock } from '@/components/CodeBlock';
import { Snippet } from '@/components/Snippet';
import { WithAllOptionsAndTabs, WithAllOptions, WithBreadcrumbs, WithHeaderPage } from './stories';
import {
propDefs,
usage,
simple,
defaultSnippet,
withTabs,
withBreadcrumbs,
withHeaderPage,
} from './header.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { HeaderDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Component"
title="Header"
description="A header component for the plugin."
/>
<Snippet
py={4}
preview={<WithAllOptionsAndTabs />}
code={defaultSnippet}
/>
## Usage
<CodeBlock code={usage} />
## API reference
<PropsTable data={propDefs} />
## Examples
### Simple header
A simple example of how to use the Header component.
<Snippet
py={4}
preview={<WithAllOptions />}
code={simple}
open
/>
### Header with tabs
A simple example of how to use the Header component with tabs. All links are using React Router
under the hood and will be active when you are on the corresponding page.
<Snippet
preview={<WithAllOptionsAndTabs />}
code={withTabs}
open
/>
### Header with breadcrumbs
Breacrumbs should appear when you scroll down (and not directly visible as it is in the demo below).
<Snippet
preview={<WithBreadcrumbs />}
code={withBreadcrumbs}
open
/>
### Header with HeaderPage
You can use the `Header` component inside the [HeaderPage](/components/header-page) component to compose your multi-level navigation.
<Snippet
preview={<WithHeaderPage />}
code={withHeaderPage}
open
/>
<Theming definition={HeaderDefinition} />
<ChangelogComponent component="Header" />
@@ -0,0 +1,17 @@
'use client';
import * as stories from '@backstage/ui/src/components/Header/Header.stories';
const {
WithAllOptionsAndTabs: WithAllOptionsAndTabsStory,
WithAllOptions: WithAllOptionsStory,
WithBreadcrumbs: WithBreadcrumbsStory,
WithHeaderPage: WithHeaderPageStory,
} = stories;
export const WithAllOptionsAndTabs = () => (
<WithAllOptionsAndTabsStory.Component />
);
export const WithAllOptions = () => <WithAllOptionsStory.Component />;
export const WithBreadcrumbs = () => <WithBreadcrumbsStory.Component />;
export const WithHeaderPage = () => <WithHeaderPageStory.Component />;
@@ -0,0 +1,81 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const linkPropDefs: Record<string, PropDef> = {
href: {
type: 'string',
},
variant: {
type: 'enum',
values: [
'title-large',
'title-medium',
'title-small',
'title-x-small',
'body-large',
'body-medium',
'body-small',
'body-x-small',
],
default: 'body-medium',
responsive: true,
},
weight: {
type: 'enum',
values: ['regular', 'bold'],
default: 'regular',
responsive: true,
},
color: {
type: 'enum',
values: ['primary', 'secondary', 'danger', 'warning', 'success'],
default: 'primary',
responsive: true,
},
...classNamePropDefs,
...stylePropDefs,
};
export const linkUsageSnippet = `import { Link } from '@backstage/ui';
<Link href="/sign-up">Sign up for Backstage</Link>`;
export const linkDefaultSnippet = `<Link href="/">Sign up for Backstage</Link>`;
export const linkVariantsSnippet = `<Flex gap="4" direction="column">
<Link href="/" variant="title-large">...</Link>
<Link href="/" variant="title-medium">...</Link>
<Link href="/" variant="title-small">...</Link>
<Link href="/" variant="title-x-small">...</Link>
<Link href="/" variant="body-large">...</Link>
<Link href="/" variant="body-medium">...</Link>
<Link href="/" variant="body-small">...</Link>
<Link href="/" variant="body-x-small">...</Link>
</Flex>`;
export const linkWeightsSnippet = `<Flex gap="4" direction="column">
<Link href="/" weight="regular" />
<Link href="/" weight="bold" />
</Flex>`;
export const linkColorsSnippet = `<Flex gap="4" direction="column">
<Link href="/" color="primary">I am primary</Link>
<Link href="/" color="secondary">I am secondary</Link>
<Link href="/" color="danger">I am danger</Link>
<Link href="/" color="warning">I am warning</Link>
<Link href="/" color="success">I am success</Link>
</Flex>`;
export const linkRouterSnippet = `import { Link } from '@backstage/ui';
// Internal route
<Link href="/home">Home</Link>
// External URL
<Link href="https://backstage.io">Backstage</Link>
`;
export const linkTruncateSnippet = `<Link href="/" truncate>...</Link>`;
+99
View File
@@ -0,0 +1,99 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Default, AllVariants, AllWeights, AllColors, Truncate } from './stories';
import {
linkPropDefs,
linkUsageSnippet,
linkDefaultSnippet,
linkVariantsSnippet,
linkWeightsSnippet,
linkColorsSnippet,
linkRouterSnippet,
linkTruncateSnippet,
} from './link.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { LinkDefinition } from '../../../utils/definitions';
<PageTitle
title="Link"
description="A link component that renders a `<a>` element."
/>
<Snippet
align="center"
py={4}
preview={<Default />}
code={linkDefaultSnippet}
/>
## Usage
<CodeBlock code={linkUsageSnippet} />
## API reference
<PropsTable data={linkPropDefs} />
## Router Integration
The `Link` component handles both internal and external navigation. It automatically detects whether the provided URL is internal (relative path) or external (absolute URL with protocol) and renders the appropriate element:
- **Internal routes**: Uses `react-router-dom`'s `Link` component for client-side navigation
- **External URLs**: Renders a standard `<a>` element for traditional navigation
<CodeBlock language="tsx" code={linkRouterSnippet} />
## Examples
### Variants
Here's a view when links have different variants.
<Snippet
align="center"
py={4}
open
preview={<AllVariants />}
code={linkVariantsSnippet}
/>
### Weights
Here's a view when links have different weights.
<Snippet
align="center"
py={4}
open
preview={<AllWeights />}
code={linkWeightsSnippet}
/>
### Colors
Here's a view when links have different colors.
<Snippet
align="center"
py={4}
open
preview={<AllColors />}
code={linkColorsSnippet}
/>
### Truncate
The `Link` component has a `truncate` prop that can be used to truncate the text.
<Snippet
open
preview={<Truncate />}
code={linkTruncateSnippet}
/>
<Theming definition={LinkDefinition} />
<ChangelogComponent component="link" />
@@ -0,0 +1,17 @@
'use client';
import * as stories from '@backstage/ui/src/components/Link/Link.stories';
const {
Default: DefaultStory,
AllVariants: AllVariantsStory,
AllWeights: AllWeightsStory,
AllColors: AllColorsStory,
Truncate: TruncateStory,
} = stories;
export const Default = () => <DefaultStory.Component />;
export const AllVariants = () => <AllVariantsStory.Component />;
export const AllWeights = () => <AllWeightsStory.Component />;
export const AllColors = () => <AllColorsStory.Component />;
export const Truncate = () => <TruncateStory.Component />;
@@ -0,0 +1,372 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
const placementValues = [
'bottom',
'bottom left',
'bottom right',
'bottom start',
'bottom end',
'top',
'top left',
'top right',
'top start',
'top end',
'left',
'left top',
'left bottom',
'start',
'start top',
'start bottom',
'right',
'right top',
'right bottom',
'end',
'end top',
'end bottom',
];
export const menuTriggerPropDefs: Record<string, PropDef> = {
isOpen: {
type: 'boolean',
},
defaultOpen: {
type: 'boolean',
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
},
};
export const submenuTriggerPropDefs: Record<string, PropDef> = {
delay: {
type: 'number',
default: '200',
},
};
export const menuPropDefs: Record<string, PropDef> = {
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
},
selectionMode: {
type: 'enum',
values: ['none', 'single', 'multiple'],
},
selectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
},
defaultSelectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
},
placement: {
type: 'enum',
values: placementValues,
},
virtualized: {
type: 'boolean',
default: 'false',
},
maxWidth: {
type: 'number',
},
maxHeight: {
type: 'number',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuListBoxPropDefs: Record<string, PropDef> = {
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
},
selectionMode: {
type: 'enum',
values: ['none', 'single', 'multiple'],
},
selectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
},
defaultSelectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
},
placement: {
type: 'enum',
values: placementValues,
},
virtualized: {
type: 'boolean',
default: 'false',
},
maxWidth: {
type: 'number',
},
maxHeight: {
type: 'number',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuAutocompletePropDefs: Record<string, PropDef> = {
placement: {
type: 'enum',
values: placementValues,
},
virtualized: {
type: 'boolean',
default: 'false',
},
maxWidth: {
type: 'number',
},
maxHeight: {
type: 'number',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuAutocompleteListboxPropDefs: Record<string, PropDef> = {
placement: {
type: 'enum',
values: placementValues,
},
virtualized: {
type: 'boolean',
default: 'false',
},
maxWidth: {
type: 'number',
},
maxHeight: {
type: 'number',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuItemPropDefs: Record<string, PropDef> = {
id: {
type: 'enum',
values: ['Key'],
},
value: {
type: 'string',
},
textValue: {
type: 'string',
},
isDisabled: {
type: 'boolean',
},
href: {
type: 'string',
},
onAction: {
type: 'enum',
values: ['(event) => void'],
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuListBoxItemPropDefs: Record<string, PropDef> = {
id: {
type: 'enum',
values: ['Key'],
},
value: {
type: 'string',
},
textValue: {
type: 'string',
},
isDisabled: {
type: 'boolean',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuSectionPropDefs: Record<string, PropDef> = {
title: {
type: 'string',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuSeparatorPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
...stylePropDefs,
};
export const usage = `import { MenuTrigger, Menu, MenuItem } from '@backstage/ui';
<MenuTrigger>
<Button>Menu</Button>
<Menu>
<MenuItem id="apple">Apple</MenuItem>
<MenuItem id="banana">Banana</MenuItem>
<MenuItem id="blueberry">Blueberry</MenuItem>
<MenuSeparator />
<SubmenuTrigger>
<MenuItem>Vegetables</MenuItem>
<Menu>
<MenuItem id="carrot">Carrot</MenuItem>
<MenuItem id="tomato">Tomato</MenuItem>
<MenuItem id="potato">Potato</MenuItem>
</Menu>
</SubmenuTrigger>
</Menu>
</MenuTrigger>`;
export const preview = `<MenuTrigger>
<Button>Menu</Button>
<Menu>
{options.map(option => (
<MenuItem key={option.value}>{option.label}</MenuItem>
))}
</Menu>
</MenuTrigger>`;
export const submenu = `<MenuTrigger>
<Button aria-label="Menu">Menu</Button>
<Menu>
<MenuItem>Edit</MenuItem>
<MenuItem>Duplicate</MenuItem>
<SubmenuTrigger>
<MenuItem>Submenu</MenuItem>
<Menu placement="right top">
<MenuItem>Edit</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Rename</MenuItem>
<MenuSeparator />
<MenuItem>Share</MenuItem>
<MenuItem>Move</MenuItem>
<MenuSeparator />
<MenuItem iconStart={<RiChat1Line />}>Feedback</MenuItem>
</Menu>
</SubmenuTrigger>
</Menu>
</MenuTrigger>`;
export const icons = `<MenuTrigger>
<Button aria-label="Menu">Menu</Button>
<Menu>
<MenuItem iconStart={<RiFileCopyLine />}>Copy</MenuItem>
<MenuItem iconStart={<RiEdit2Line />}>Rename</MenuItem>
<MenuItem iconStart={<RiChat1Line />}>Send feedback</MenuItem>
</Menu>
</MenuTrigger>`;
export const sections = `<MenuTrigger>
<Button aria-label="Menu">Menu</Button>
<Menu>
<MenuSection title="My Account">
<MenuItem iconStart={<RiUserLine />}>Profile</MenuItem>
<MenuItem iconStart={<RiSettingsLine />}>Settings</MenuItem>
</MenuSection>
<MenuSection title="Support">
<MenuItem iconStart={<RiQuestionLine />}>Help Center</MenuItem>
<MenuItem iconStart={<RiCustomerService2Line />}>
Contact Support
</MenuItem>
<MenuItem iconStart={<RiChat1Line />}>Feedback</MenuItem>
</MenuSection>
</Menu>
</MenuTrigger>`;
export const separators = `<MenuTrigger>
<Button aria-label="Menu">Menu</Button>
<Menu>
<MenuItem>Edit</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Rename</MenuItem>
<MenuSeparator />
<MenuItem>Share</MenuItem>
<MenuItem>Move</MenuItem>
<MenuSeparator />
<MenuItem iconStart={<RiChat1Line />}>Feedback</MenuItem>
</Menu>
</MenuTrigger>`;
export const links = `<MenuTrigger>
<Button aria-label="Menu">Menu</Button>
<Menu>
<MenuItem href="/home">Internal link</MenuItem>
<MenuItem href="https://www.google.com" target="_blank">
External link
</MenuItem>
<MenuItem href="mailto:test@test.com">Email link</MenuItem>
</Menu>
</MenuTrigger>`;
export const autocomplete = `<MenuTrigger isOpen>
<Button aria-label="Menu">Menu</Button>
<MenuAutocomplete placeholder="Filter">
<MenuItem>Create new file...</MenuItem>
<MenuItem>Create new folder...</MenuItem>
<MenuItem>Assign to...</MenuItem>
<MenuItem>Assign to me</MenuItem>
<MenuItem>Change status...</MenuItem>
<MenuItem>Change priority...</MenuItem>
<MenuItem>Add label...</MenuItem>
<MenuItem>Remove label...</MenuItem>
</MenuAutocomplete>
</MenuTrigger>`;
export const autocompleteListbox = `const [selected, setSelected] = useState<Selection>(
new Set(['blueberry']),
);
<Flex direction="column" gap="2" align="start">
<Text>Selected: {Array.from(selected).join(', ')}</Text>
<MenuTrigger>
<Button aria-label="Menu">Menu</Button>
<MenuAutocompleteListbox
selectedKeys={selected}
onSelectionChange={setSelected}
>
{options.map(option => (
<MenuListBoxItem key={option.value} id={option.value}>
{option.label}
</MenuListBoxItem>
))}
</MenuAutocompleteListbox>
</MenuTrigger>
</Flex>`;
export const autocompleteListboxMultiple = `const [selected, setSelected] = useState<Selection>(
new Set(['blueberry', 'cherry']),
);
<Flex direction="column" gap="2" align="start">
<Text>Selected: {Array.from(selected).join(', ')}</Text>
<MenuTrigger>
<Button aria-label="Menu">Menu</Button>
<MenuAutocompleteListbox
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected}
>
{options.map(option => (
<MenuListBoxItem key={option.value} id={option.value}>
{option.label}
</MenuListBoxItem>
))}
</MenuAutocompleteListbox>
</MenuTrigger>
</Flex>`;
+237
View File
@@ -0,0 +1,237 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Preview, PreviewSubmenu, PreviewIcons, PreviewLinks, PreviewSections, PreviewSeparators, PreviewAutocompleteMenu, PreviewAutocompleteListbox, PreviewAutocompleteListboxMultiple } from './stories';
import {
usage,
preview,
submenu,
icons,
sections,
separators,
links,
autocomplete,
autocompleteListbox,
autocompleteListboxMultiple,
menuTriggerPropDefs,
submenuTriggerPropDefs,
menuPropDefs,
menuListBoxPropDefs,
menuAutocompletePropDefs,
menuAutocompleteListboxPropDefs,
menuItemPropDefs,
menuListBoxItemPropDefs,
menuSectionPropDefs,
menuSeparatorPropDefs,
} from './menu.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { MenuDefinition } from '../../../utils/definitions';
<PageTitle
title="Menu"
description="A list of actions in a dropdown, enhanced with keyboard navigation."
/>
<Snippet
align="center"
py={4}
preview={<Preview />}
code={preview}
/>
## Usage
<CodeBlock code={usage} />
### Triggers
- `MenuTrigger` is a wrapper component that combines a button or other trigger element with a menu displayed in a popover.
- `SubmenuTrigger` is a wrapper component that combines a `MenuItem` with a menu displayed in a popover.
### Containers
- `Menu` is a container component that contains a list of menu items or sections.
- `MenuListBox` is a container component that contains a list of menu items or sections.
- `MenuAutocomplete` is a container component that contains a list of menu items or sections.
- `MenuAutocompleteListbox` is a container component that contains a list of menu items or sections.
### Items
- `MenuItem` is an individual interactive item in the menu.
- `MenuListBoxItem` is an individual interactive item in the menu list box.
### Separators
- `MenuSeparator` is a component that renders a horizontal line to separate menu items.
- `MenuSection` is a component that renders a section in the menu.
## API reference
### MenuTrigger
`MenuTrigger` accepts exactly two children: the first child should be the trigger element, and second child should be
one of the menu containers containing the menu.
<PropsTable data={menuTriggerPropDefs} />
### SubmenuTrigger
The `SubmenuTrigger` accepts exactly two children: the first child should be the `MenuItem` which triggers opening
of the submenu, and second child should be one of the menu containers containing the submenu.
<PropsTable data={submenuTriggerPropDefs} />
### Menu
`Menu` is a container component that contains a list of menu items or sections.
<PropsTable data={menuPropDefs} />
### MenuListBox
`MenuListBox` is a container component that contains a list of menu items or sections.
<PropsTable data={menuListBoxPropDefs} />
### MenuAutocomplete
`MenuAutocomplete` is a container component that contains a list of menu items or sections.
<PropsTable data={menuAutocompletePropDefs} />
### MenuAutocompleteListbox
`MenuAutocompleteListbox` is a container component that contains a list of menu items or sections.
<PropsTable data={menuAutocompleteListboxPropDefs} />
### MenuItem
`MenuItem` is an individual interactive item in the menu.
<PropsTable data={menuItemPropDefs} />
### MenuListBoxItem
`MenuListBoxItem` is an individual interactive item in the menu list box.
<PropsTable data={menuListBoxItemPropDefs} />
### MenuSection
`MenuSection` is a component that renders a section in the menu.
<PropsTable data={menuSectionPropDefs} />
### MenuSeparator
`MenuSeparator` is a component that renders a horizontal line to separate menu items.
<PropsTable data={menuSeparatorPropDefs} />
## Examples
### Nested navigation
You can nest menus to create a more complex navigation structure. It is important to use the `placement` prop to ensure
the submenu is displayed in the correct position. The best practice is to use the `right top` placement for the submenu.
<Snippet
align="center"
py={4}
open
preview={<PreviewSubmenu />}
code={submenu}
/>
### With icons
You can use the `iconStart` prop to add an icon to the menu item.
<Snippet
align="center"
py={4}
open
preview={<PreviewIcons />}
code={icons}
/>
### With links
You can use the `href` prop to add a link to the menu item. This is using our router provider under the hood
to work for both internal and external links.
<Snippet
align="center"
py={4}
open
preview={<PreviewLinks />}
code={links}
/>
### With sections
You can use the `MenuSection` component to add a section to the menu.
<Snippet
align="center"
py={4}
open
preview={<PreviewSections />}
code={sections}
/>
### With separators
You can use the `MenuSeparator` component to add a separator to the menu.
<Snippet
align="center"
py={4}
open
preview={<PreviewSeparators />}
code={separators}
/>
### With autocomplete
You can use the `MenuAutocomplete` component to add a autocomplete to the menu.
<Snippet
align="center"
py={4}
open
preview={<PreviewAutocompleteMenu />}
code={autocomplete}
/>
### With list box
You can use the `MenuListBox` component to add a list box to the menu.
<Snippet
align="center"
py={4}
open
preview={<PreviewAutocompleteListbox />}
code={autocompleteListbox}
/>
### With list box with multiple selection
You can use the `MenuListBox` component to add a list box to the menu. You can also use the `selectionMode` prop to
allow multiple selection.
<Snippet
align="center"
py={4}
open
preview={<PreviewAutocompleteListboxMultiple />}
code={autocompleteListboxMultiple}
/>
<Theming definition={MenuDefinition} />
<ChangelogComponent component="menu" />
@@ -0,0 +1,31 @@
'use client';
import * as stories from '@backstage/ui/src/components/Menu/Menu.stories';
const {
Preview: PreviewStory,
PreviewSubmenu: PreviewSubmenuStory,
PreviewIcons: PreviewIconsStory,
PreviewLinks: PreviewLinksStory,
PreviewSections: PreviewSectionsStory,
PreviewSeparators: PreviewSeparatorsStory,
PreviewAutocompleteMenu: PreviewAutocompleteMenuStory,
PreviewAutocompleteListbox: PreviewAutocompleteListboxStory,
PreviewAutocompleteListboxMultiple: PreviewAutocompleteListboxMultipleStory,
} = stories;
export const Preview = () => <PreviewStory.Component />;
export const PreviewSubmenu = () => <PreviewSubmenuStory.Component />;
export const PreviewIcons = () => <PreviewIconsStory.Component />;
export const PreviewLinks = () => <PreviewLinksStory.Component />;
export const PreviewSections = () => <PreviewSectionsStory.Component />;
export const PreviewSeparators = () => <PreviewSeparatorsStory.Component />;
export const PreviewAutocompleteMenu = () => (
<PreviewAutocompleteMenuStory.Component />
);
export const PreviewAutocompleteListbox = () => (
<PreviewAutocompleteListboxStory.Component />
);
export const PreviewAutocompleteListboxMultiple = () => (
<PreviewAutocompleteListboxMultipleStory.Component />
);
@@ -0,0 +1,65 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import {
inputPropDefs,
passwordFieldUsageSnippet,
passwordFieldDefaultSnippet,
passwordFieldSizesSnippet,
passwordFieldDescriptionSnippet,
} from './password-field.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
import { PasswordFieldDefinition } from '../../../utils/definitions';
import { WithLabel, Sizes, WithDescription } from './stories';
<PageTitle
title="PasswordField"
description="A password field component for your forms."
/>
<Snippet
align="center"
py={4}
preview={<WithLabel />}
code={passwordFieldDefaultSnippet}
/>
## Usage
<CodeBlock code={passwordFieldUsageSnippet} />
## API reference
<PropsTable data={inputPropDefs} />
## Examples
### Sizes
We support two different sizes: `small`, `medium`.
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={passwordFieldSizesSnippet}
/>
### With description
Here's a simple PasswordField with a description.
<Snippet
align="center"
py={4}
open
preview={<WithDescription />}
code={passwordFieldDescriptionSnippet}
/>
<Theming definition={PasswordFieldDefinition} />
<ChangelogComponent component="password-field" />
@@ -0,0 +1,43 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const inputPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
},
label: {
type: 'string',
},
icon: {
type: 'enum',
values: ['ReactNode'],
},
description: {
type: 'string',
},
name: {
type: 'string',
required: true,
},
...classNamePropDefs,
...stylePropDefs,
};
export const passwordFieldUsageSnippet = `import { PasswordField } from '@backstage/ui';
<PasswordField />`;
export const passwordFieldDefaultSnippet = `<PasswordField label="Label" placeholder="Enter a secret" />`;
export const passwordFieldSizesSnippet = `<Flex direction="row" gap="4">
<PasswordField size="small" placeholder="Small" icon={<Icon name="sparkling" />} />
<PasswordField size="medium" placeholder="Medium" icon={<Icon name="sparkling" />} />
</Flex>`;
export const passwordFieldDescriptionSnippet = `<PasswordField label="Label" description="Description" placeholder="Enter a secret" />`;
@@ -0,0 +1,13 @@
'use client';
import * as stories from '@backstage/ui/src/components/PasswordField/PasswordField.stories';
const {
WithLabel: WithLabelStory,
Sizes: SizesStory,
WithDescription: WithDescriptionStory,
} = stories;
export const WithLabel = () => <WithLabelStory.Component />;
export const Sizes = () => <SizesStory.Component />;
export const WithDescription = () => <WithDescriptionStory.Component />;
+102
View File
@@ -0,0 +1,102 @@
import { PropsTable } from '@/components/PropsTable';
import { Default } from './stories';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
popoverPropDefs,
popoverUsageSnippet,
popoverDefaultSnippet,
popoverPlacementsSnippet,
popoverNoArrowSnippet,
popoverRichContentSnippet,
popoverNonModalSnippet,
} from './popover.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { PopoverDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Popover"
description="A popover displays floating content anchored to a trigger element with automatic positioning and collision detection."
/>
<Snippet
align="center"
py={4}
preview={<Default />}
code={popoverDefaultSnippet}
/>
## Usage
<CodeBlock code={popoverUsageSnippet} />
## API reference
### Popover
The main popover component that displays floating content with automatic positioning.
<PropsTable data={popoverPropDefs} />
## Examples
### Placements
Popover supports multiple placement options to position the content relative to the trigger.
<Snippet
align="center"
py={4}
preview={<Default />}
code={popoverPlacementsSnippet}
/>
### Without Arrow
You can hide the arrow by setting the `hideArrow` prop.
<Snippet
align="center"
py={4}
preview={<Default />}
code={popoverNoArrowSnippet}
/>
### Rich Content
Popovers can contain complex content including buttons, forms, and other interactive elements.
<Snippet
align="center"
py={4}
preview={<Default />}
code={popoverRichContentSnippet}
/>
### Non-Modal
Non-modal popovers allow interaction with other elements on the page while the popover is open.
<Snippet
align="center"
py={4}
preview={<Default />}
code={popoverNonModalSnippet}
/>
### Long Content
Popovers automatically handle scrolling when content exceeds the available viewport space.
<Snippet
align="center"
py={4}
preview={<Default />}
code={popoverDefaultSnippet}
/>
<Theming definition={PopoverDefinition} />
<ChangelogComponent component="popover" />
@@ -0,0 +1,174 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const popoverPropDefs: Record<string, PropDef> = {
hideArrow: {
type: 'boolean',
default: 'false',
description:
'Whether to hide the arrow pointing to the trigger element. Arrow is also automatically hidden for MenuTrigger and SubmenuTrigger contexts.',
},
placement: {
type: 'enum',
values: [
'top',
'top start',
'top end',
'bottom',
'bottom start',
'bottom end',
'left',
'left start',
'left end',
'right',
'right start',
'right end',
],
default: 'bottom',
description: 'The placement of the popover relative to the trigger.',
},
containerPadding: {
type: 'number',
default: '12',
description:
'The padding between the popover and the edge of the viewport.',
},
offset: {
type: 'number',
default: '0',
description: 'The offset from the trigger element.',
},
crossOffset: {
type: 'number',
default: '0',
description: 'The cross-axis offset from the trigger element.',
},
shouldFlip: {
type: 'boolean',
default: 'true',
description:
'Whether the popover should flip to the opposite side when there is not enough space.',
},
isOpen: {
type: 'boolean',
description: 'Whether the popover is open (controlled).',
},
defaultOpen: {
type: 'boolean',
description: 'Whether the popover is open by default (uncontrolled).',
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
description:
"Handler that is called when the popover's open state changes.",
},
isNonModal: {
type: 'boolean',
default: 'false',
description:
'Whether the popover is non-modal. Non-modal popovers do not block interaction with the rest of the page.',
},
isKeyboardDismissDisabled: {
type: 'boolean',
default: 'false',
description:
'Whether pressing the escape key to close the popover should be disabled.',
},
shouldCloseOnBlur: {
type: 'boolean',
default: 'true',
description:
'Whether to close the popover when the user interacts outside it.',
},
triggerRef: {
type: 'enum',
values: ['RefObject<Element | null>'],
description: 'A ref for the trigger element.',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const popoverUsageSnippet = `import { Popover, DialogTrigger, Button } from '@backstage/ui';
<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<Text>This is a popover</Text>
</Popover>
</DialogTrigger>`;
export const popoverDefaultSnippet = `<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<Text>This is a popover</Text>
</Popover>
</DialogTrigger>`;
export const popoverPlacementsSnippet = `<DialogTrigger>
<Button>Top</Button>
<Popover placement="top">
<Text>Top placement</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Right</Button>
<Popover placement="right">
<Text>Right placement</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Bottom</Button>
<Popover placement="bottom">
<Text>Bottom placement</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Left</Button>
<Popover placement="left">
<Text>Left placement</Text>
</Popover>
</DialogTrigger>`;
export const popoverNoArrowSnippet = `<DialogTrigger>
<Button>Open Popover</Button>
<Popover hideArrow>
<Text>Popover without arrow</Text>
</Popover>
</DialogTrigger>`;
export const popoverRichContentSnippet = `<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<Flex direction="column" gap="3">
<Text style={{ fontWeight: 'bold' }}>Popover Title</Text>
<Text>
This is a popover with rich content. It can contain multiple
elements and formatted text.
</Text>
<Flex gap="2" justify="end">
<Button variant="tertiary" size="small">Cancel</Button>
<Button variant="primary" size="small">Confirm</Button>
</Flex>
</Flex>
</Popover>
</DialogTrigger>`;
export const popoverNonModalSnippet = `<DialogTrigger>
<Button>Open Non-Modal Popover</Button>
<Popover isNonModal>
<Text>
This is a non-modal popover. You can interact with other
elements on the page while it's open.
</Text>
</Popover>
</DialogTrigger>`;
@@ -0,0 +1,7 @@
'use client';
import * as stories from '@backstage/ui/src/components/Popover/Popover.stories';
const { Default: DefaultStory } = stories;
export const Default = () => <DefaultStory.Component />;
@@ -0,0 +1,105 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Default, Horizontal, Disabled, DisabledSingle, Validation, ReadOnly } from './stories';
import {
radioGroupPropDefs,
radioGroupUsageSnippet,
radioGroupDefaultSnippet,
radioGroupDescriptionSnippet,
radioGroupHorizontalSnippet,
radioGroupDisabledSnippet,
radioGroupDisabledSingleSnippet,
radioGroupValidationSnippet,
radioGroupReadOnlySnippet,
} from './radio-group.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
import { RadioGroupDefinition } from '../../../utils/definitions';
<PageTitle
title="RadioGroup"
description="A radio group allows a user to select a single item from a list of mutually exclusive options."
/>
<Snippet
align="center"
py={4}
preview={<Default />}
code={radioGroupDefaultSnippet}
/>
## Usage
<CodeBlock code={radioGroupUsageSnippet} />
## API reference
<PropsTable data={radioGroupPropDefs} />
## Examples
### Horizontal
Here's a simple TextField with a description.
<Snippet
align="center"
py={4}
open
preview={<Horizontal />}
code={radioGroupHorizontalSnippet}
/>
### Disabled
You can disable the entire radio group by adding the `isDisabled` prop to the `RadioGroup` component.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={radioGroupDisabledSnippet}
/>
### Disabled Single radio
You can disable a single radio by adding the `isDisabled` prop to the `Radio` component.
<Snippet
align="center"
py={4}
open
preview={<DisabledSingle />}
code={radioGroupDisabledSingleSnippet}
/>
### Validation
Here's an example of a radio group with errors.
<Snippet
align="center"
py={4}
open
preview={<Validation />}
code={radioGroupValidationSnippet}
/>
### Read only
You can make the radio group read only by adding the `isReadOnly` prop to the `RadioGroup` component.
<Snippet
align="center"
py={4}
open
preview={<ReadOnly />}
code={radioGroupReadOnlySnippet}
/>
<Theming definition={RadioGroupDefinition} />
<ChangelogComponent component="radio-group" />
@@ -0,0 +1,64 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const radioGroupPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
},
label: {
type: 'string',
},
icon: {
type: 'enum',
values: ['ReactNode'],
},
description: {
type: 'string',
},
name: {
type: 'string',
required: true,
},
...classNamePropDefs,
...stylePropDefs,
};
export const radioGroupUsageSnippet = `import { RadioGroup } from '@backstage/ui';
<RadioGroup />`;
export const radioGroupDefaultSnippet = `<RadioGroup label="Label" />`;
export const radioGroupDescriptionSnippet = `<TextField label="Label" description="Description" placeholder="Enter a URL" />`;
export const radioGroupHorizontalSnippet = `<RadioGroup label="Label" orientation="horizontal" />`;
export const radioGroupDisabledSnippet = `<RadioGroup label="Label" isDisabled>
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>`;
export const radioGroupDisabledSingleSnippet = `<RadioGroup label="Label">
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander" isDisabled>Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>`;
export const radioGroupValidationSnippet = `<RadioGroup validate: value => (value === \'charmander\' ? \'Nice try!\' : null)>
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>`;
export const radioGroupReadOnlySnippet = `<RadioGroup label="Label" isReadOnly>
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>`;
@@ -0,0 +1,19 @@
'use client';
import * as stories from '@backstage/ui/src/components/RadioGroup/RadioGroup.stories';
const {
Default: DefaultStory,
Horizontal: HorizontalStory,
Disabled: DisabledStory,
DisabledSingle: DisabledSingleStory,
Validation: ValidationStory,
ReadOnly: ReadOnlyStory,
} = stories;
export const Default = () => <DefaultStory.Component />;
export const Horizontal = () => <HorizontalStory.Component />;
export const Disabled = () => <DisabledStory.Component />;
export const DisabledSingle = () => <DisabledSingleStory.Component />;
export const Validation = () => <ValidationStory.Component />;
export const ReadOnly = () => <ReadOnlyStory.Component />;
@@ -0,0 +1,78 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { WithLabel, Sizes, WithDescription, StartCollapsed } from './stories';
import {
searchFieldPropDefs,
searchFieldUsageSnippet,
searchFieldDefaultSnippet,
searchFieldSizesSnippet,
searchFieldDescriptionSnippet,
searchFieldCollapsibleSnippet,
} from './search-field.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
import { SearchFieldDefinition } from '../../../utils/definitions';
<PageTitle
title="SearchField"
description="A SearchField is a text field designed for searches."
/>
<Snippet
align="center"
py={4}
preview={<WithLabel />}
code={searchFieldDefaultSnippet}
/>
## Usage
<CodeBlock code={searchFieldUsageSnippet} />
## API reference
<PropsTable data={searchFieldPropDefs} />
## Examples
### Sizes
We support two different sizes: `small`, `medium`.
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={searchFieldSizesSnippet}
/>
### With description
Here's a simple SearchField with a description.
<Snippet
align="center"
py={4}
open
preview={<WithDescription />}
code={searchFieldDescriptionSnippet}
/>
### Collapsible
You can make the SearchField collapsible by setting the `startCollapsed` prop to `true`.
<Snippet
align="center"
py={4}
open
preview={<StartCollapsed />}
code={searchFieldCollapsibleSnippet}
/>
<Theming definition={SearchFieldDefinition} />
<ChangelogComponent component="search-field" />
@@ -0,0 +1,49 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const searchFieldPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
},
label: {
type: 'string',
},
icon: {
type: 'enum',
values: ['ReactNode'],
},
description: {
type: 'string',
},
name: {
type: 'string',
required: true,
},
startCollapsed: {
type: 'boolean',
default: 'false',
},
...classNamePropDefs,
...stylePropDefs,
};
export const searchFieldUsageSnippet = `import { SearchField } from '@backstage/ui';
<SearchField />`;
export const searchFieldDefaultSnippet = `<SearchField label="Label" placeholder="Enter a URL" />`;
export const searchFieldSizesSnippet = `<Flex direction="row" gap="4">
<SearchField size="small" placeholder="Small" icon={<Icon name="sparkling" />} />
<SearchField size="medium" placeholder="Medium" icon={<Icon name="sparkling" />} />
</Flex>`;
export const searchFieldDescriptionSnippet = `<SearchField label="Label" description="Description" placeholder="Enter a URL" />`;
export const searchFieldCollapsibleSnippet = `<SearchField startCollapsed />`;
@@ -0,0 +1,15 @@
'use client';
import * as stories from '@backstage/ui/src/components/SearchField/SearchField.stories';
const {
WithLabel: WithLabelStory,
Sizes: SizesStory,
WithDescription: WithDescriptionStory,
StartCollapsed: StartCollapsedStory,
} = stories;
export const WithLabel = () => <WithLabelStory.Component />;
export const Sizes = () => <SizesStory.Component />;
export const WithDescription = () => <WithDescriptionStory.Component />;
export const StartCollapsed = () => <StartCollapsedStory.Component />;
+151
View File
@@ -0,0 +1,151 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Preview, WithLabelAndDescription, Sizes, WithIcon, Disabled, DisabledOption, Searchable, MultipleSelection, SearchableMultiple } from './stories';
import {
selectPropDefs,
selectUsageSnippet,
selectDefaultSnippet,
selectDescriptionSnippet,
selectSizesSnippet,
selectDisabledSnippet,
selectResponsiveSnippet,
selectIconSnippet,
selectSearchableSnippet,
selectMultipleSnippet,
selectSearchableMultipleSnippet,
selectDisabledOptionsSnippet,
} from './select.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SelectDefinition } from '../../../utils/definitions';
<PageTitle
title="Select"
description="A common form component for choosing a predefined value in a dropdown menu."
/>
<Snippet
align="center"
py={4}
preview={<Preview />}
code={selectDefaultSnippet}
/>
## Usage
<CodeBlock code={selectUsageSnippet} />
## API reference
<PropsTable data={selectPropDefs} />
## Examples
### With Label and description
Select component with label and description.
<Snippet
align="center"
py={4}
open
preview={<WithLabelAndDescription />}
code={selectDescriptionSnippet}
/>
### Sizes
Here's a view when the selects have different sizes.
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={selectSizesSnippet}
/>
### With Icon
Here's a view when the select has an icon.
<Snippet
align="center"
py={4}
open
preview={<WithIcon />}
code={selectIconSnippet}
/>
### Disabled
Here's a view when the select is disabled.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={selectDisabledSnippet}
/>
### Disabled options
You can disable specific options within the Select component using `disabledKeys`.
<Snippet
align="center"
py={4}
open
preview={<DisabledOption />}
code={selectDisabledOptionsSnippet}
/>
### Searchable
Here's a view when the select has searchable filtering.
<Snippet
align="center"
py={4}
open
preview={<Searchable />}
code={selectSearchableSnippet}
/>
### Multiple Selection
Here's a view when the select allows multiple selections.
<Snippet
align="center"
py={4}
open
preview={<MultipleSelection />}
code={selectMultipleSnippet}
/>
### Searchable with Multiple Selection
Here's a view when the select combines search and multiple selection.
<Snippet
align="center"
py={4}
open
preview={<SearchableMultiple />}
code={selectSearchableMultipleSnippet}
/>
### Responsive
Here's a view when the select is responsive.
<CodeBlock code={selectResponsiveSnippet} />
<Theming definition={SelectDefinition} />
<ChangelogComponent component="select" />
@@ -0,0 +1,224 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const selectPropDefs: Record<string, PropDef> = {
label: {
type: 'string',
responsive: false,
},
description: {
type: 'string',
responsive: false,
},
name: {
type: 'string',
responsive: false,
required: true,
},
options: {
type: 'enum',
values: ['Array<{ value: string, label: string }>'],
required: true,
},
selectionMode: {
type: 'enum',
values: ['single', 'multiple'],
default: 'single',
responsive: false,
},
placeholder: {
type: 'string',
default: 'Select an item',
responsive: false,
},
icon: {
type: 'enum',
values: ['ReactNode'],
responsive: false,
},
value: {
type: 'enum',
values: ['string', 'string[]'],
responsive: false,
description:
'Selected value (controlled). String for single selection, array for multiple.',
},
defaultValue: {
type: 'enum',
values: ['string', 'string[]'],
responsive: false,
description:
'Initial value (uncontrolled). String for single selection, array for multiple.',
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
},
isOpen: {
type: 'boolean',
responsive: false,
},
defaultOpen: {
type: 'boolean',
responsive: false,
},
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
responsive: false,
},
isDisabled: {
type: 'boolean',
responsive: false,
},
isRequired: {
type: 'boolean',
responsive: false,
},
isInvalid: {
type: 'boolean',
responsive: false,
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
responsive: false,
},
onSelectionChange: {
type: 'enum',
values: ['(key: Key | null) => void', '(keys: Selection) => void'],
responsive: false,
description:
'Handler called when selection changes. Single mode: receives Key | null. Multiple mode: receives Selection.',
},
searchable: {
type: 'boolean',
default: 'false',
responsive: false,
},
searchPlaceholder: {
type: 'string',
default: 'Search...',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const selectUsageSnippet = `import { Select } from '@backstage/ui';
<Select
name="font"
options={[
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
{ value: 'cursive', label: 'Cursive' },
]}
/>`;
export const selectDefaultSnippet = `<Select name="font" label="Font Family" options={[
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
{ value: 'cursive', label: 'Cursive' },
]} />`;
export const selectDescriptionSnippet = `<Select
name="font"
label="Font Family"
description="Choose a font family for your document"
options={[ ... ]}
/>`;
export const selectIconSnippet = `<Select
name="font"
label="Font Family"
icon={<RiCloudLine />}
options={[ ... ]}
/>`;
export const selectSizesSnippet = `<Flex>
<Select
size="small"
label="Font family"
options={[ ... ]}
/>
<Select
size="medium"
label="Font family"
options={[ ... ]}
/>
</Flex>`;
export const selectDisabledSnippet = `<Select
disabled
label="Font family"
options={[ ... ]}
/>`;
export const selectResponsiveSnippet = `<Select
size={{ initial: 'small', lg: 'medium' }}
label="Font family"
options={[ ... ]}
/>`;
export const selectSearchableSnippet = `<Select
name="country"
label="Country"
searchable
searchPlaceholder="Search countries..."
options={[
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
// ... more options
]}
/>`;
export const selectMultipleSnippet = `<Select
name="options"
label="Select multiple options"
selectionMode="multiple"
options={[
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' },
{ value: 'option4', label: 'Option 4' },
]}
/>`;
export const selectSearchableMultipleSnippet = `<Select
name="skills"
label="Skills"
searchable
selectionMode="multiple"
searchPlaceholder="Filter skills..."
options={[
{ value: 'react', label: 'React' },
{ value: 'typescript', label: 'TypeScript' },
{ value: 'javascript', label: 'JavaScript' },
{ value: 'python', label: 'Python' },
// ... more options
]}
/>`;
export const selectDisabledOptionsSnippet = `<Select
name="font"
label="Font Family"
placeholder="Select a font"
disabledKeys={['cursive', 'serif']}
options={[
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
{ value: 'cursive', label: 'Cursive' },
]}
/>`;
@@ -0,0 +1,27 @@
'use client';
import * as stories from '@backstage/ui/src/components/Select/Select.stories';
const {
Preview: PreviewStory,
WithLabelAndDescription: WithLabelAndDescriptionStory,
Sizes: SizesStory,
WithIcon: WithIconStory,
Disabled: DisabledStory,
DisabledOption: DisabledOptionStory,
Searchable: SearchableStory,
MultipleSelection: MultipleSelectionStory,
SearchableMultiple: SearchableMultipleStory,
} = stories;
export const Preview = () => <PreviewStory.Component />;
export const WithLabelAndDescription = () => (
<WithLabelAndDescriptionStory.Component />
);
export const Sizes = () => <SizesStory.Component />;
export const WithIcon = () => <WithIconStory.Component />;
export const Disabled = () => <DisabledStory.Component />;
export const DisabledOption = () => <DisabledOptionStory.Component />;
export const Searchable = () => <SearchableStory.Component />;
export const MultipleSelection = () => <MultipleSelectionStory.Component />;
export const SearchableMultiple = () => <SearchableMultipleStory.Component />;
@@ -0,0 +1,65 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Demo1, Demo2 } from './stories';
import {
skeletonPropDefs,
skeletonUsageSnippet,
skeletonDefaultSnippet,
skeletonDemo1Snippet,
skeletonDemo2Snippet,
} from './skeleton.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SkeletonDefinition } from '../../../utils/definitions';
<PageTitle
title="Skeleton"
description="Use to show a placeholder while content is loading."
/>
<Snippet
align="center"
py={4}
preview={<Demo2 />}
code={skeletonDefaultSnippet}
/>
## Usage
<CodeBlock code={skeletonUsageSnippet} />
## API reference
<PropsTable data={skeletonPropDefs} />
## Examples
### Demo 1
You can use a mix of different sizes to create a more complex skeleton.
<Snippet
align="center"
py={4}
preview={<Demo1 />}
code={skeletonDemo1Snippet}
open
/>
### Demo 2
You can use a mix of different sizes to create a more complex skeleton.
<Snippet
align="center"
py={4}
preview={<Demo2 />}
code={skeletonDemo2Snippet}
open
/>
<Theming definition={SkeletonDefinition} />
<ChangelogComponent component="skeleton" />
@@ -0,0 +1,58 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const skeletonPropDefs: Record<string, PropDef> = {
width: {
type: 'number',
default: '80',
responsive: false,
},
height: {
type: 'number',
default: '24',
responsive: false,
},
rounded: {
type: 'boolean',
default: 'false',
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const skeletonUsageSnippet = `import { Flex, Skeleton } from '@backstage/ui';
<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>`;
export const skeletonDefaultSnippet = `<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>`;
export const skeletonDemo1Snippet = `<Flex gap="4">
<Skeleton rounded width={48} height={48} />
<Flex direction="column" gap="4">
<Skeleton width={200} height={8} />
<Skeleton width={200} height={8} />
<Skeleton width={200} height={8} />
<Flex gap="4">
<Skeleton width="100%" height={8} />
<Skeleton width="100%" height={8} />
</Flex>
</Flex>
</Flex>`;
export const skeletonDemo2Snippet = `<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>`;
@@ -0,0 +1,8 @@
'use client';
import * as stories from '@backstage/ui/src/components/Skeleton/Skeleton.stories';
const { Demo1: Demo1Story, Demo2: Demo2Story } = stories;
export const Demo1 = () => <Demo1Story.Component />;
export const Demo2 = () => <Demo2Story.Component />;
@@ -0,0 +1,47 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Default, Disabled } from './stories';
import { switchPropDefs, snippetUsage } from './switch.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SwitchDefinition } from '../../../utils/definitions';
<PageTitle
title="Switch"
description="A control that indicates whether a setting is on or off."
/>
<Snippet
align="center"
py={4}
preview={<Default />}
code={`<Switch />`}
/>
## Usage
<CodeBlock code={snippetUsage} />
## API reference
<PropsTable data={switchPropDefs} />
## Examples
### Disabled
A switch can be disabled using the `isDisabled` prop.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={`<Switch isDisabled />`}
/>
<Theming definition={SwitchDefinition} />
<ChangelogComponent component="switch" />
@@ -0,0 +1,8 @@
'use client';
import * as stories from '@backstage/ui/src/components/Switch/Switch.stories';
const { Default: DefaultStory, Disabled: DisabledStory } = stories;
export const Default = () => <DefaultStory.Component />;
export const Disabled = () => <DisabledStory.Component />;
@@ -0,0 +1,71 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const switchPropDefs: Record<string, PropDef> = {
autoFocus: {
type: 'boolean',
},
defaultSelected: {
type: 'boolean',
},
...classNamePropDefs,
isDisabled: {
type: 'boolean',
},
isReadOnly: {
type: 'boolean',
},
isSelected: {
type: 'boolean',
},
label: {
type: 'string',
},
name: {
type: 'string',
},
onChange: {
type: 'enum',
values: ['(isSelected: boolean) => void'],
},
onFocus: {
type: 'enum',
values: ['(e: FocusEvent<Target>) => void'],
},
onBlur: {
type: 'enum',
values: ['(e: FocusEvent<Target>) => void'],
},
onFocusChange: {
type: 'enum',
values: ['(isFocused: boolean) => void'],
},
onKeyDown: {
type: 'enum',
values: ['(e: KeyboardEvent) => void'],
},
onKeyUp: {
type: 'enum',
values: ['(e: KeyboardEvent) => void'],
},
onHoverStart: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
},
onHoverEnd: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
},
onHoverChange: {
type: 'enum',
values: ['(isHovered: boolean) => void'],
},
...stylePropDefs,
value: {
type: 'string',
},
};
export const snippetUsage = `import { Switch } from '@backstage/ui';
<Switch />`;
+165
View File
@@ -0,0 +1,165 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { TableRockBand, SelectionModePlayground, SelectionBehaviorPlayground, SelectionToggleWithActions } from './stories';
import {
tablePropDefs,
tableHeaderPropDefs,
tableBodyPropDefs,
tablePaginationPropDefs,
tableUsageSnippet,
tableBasicSnippet,
tableRowClickSnippet,
tableHybridSnippet,
tableCellInteractionsSnippet,
tablePaginationSnippet,
tableSelectionActionsSnippet,
tableSelectionModeSnippet,
tableSelectionBehaviorSnippet,
tableSortingSnippet,
columnPropDefs,
rowPropDefs,
cellPropDefs,
} from './table.props';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { TableDefinition } from '../../../utils/definitions';
<PageTitle
title="Table"
description="A flexible table component built on top of TanStack Table with built-in styling, interactions, and pagination support."
/>
<Snippet
py={4}
preview={<TableRockBand />}
code={tableBasicSnippet}
/>
## Usage
<CodeBlock code={tableUsageSnippet} />
## API reference
### Table
The main table component that renders data in a structured grid format.
<PropsTable data={tablePropDefs} />
### TableHeader
The header section of the table that contains the column definitions.
<PropsTable data={tableHeaderPropDefs} />
### Column
A column definition that describes how a column should be rendered.
<PropsTable data={columnPropDefs} />
### TableBody
The body section of the table that contains the rows.
<PropsTable data={tableBodyPropDefs} />
### Row
A row definition that describes how a row should be rendered.
<PropsTable data={rowPropDefs} />
### Cell
A cell definition that describes how a cell should be rendered.
<PropsTable data={cellPropDefs} />
### TablePagination
A pagination component designed to work with the Table component.
<PropsTable data={tablePaginationPropDefs} />
## Examples
### Basic Table
Coming soon.
### Row selection
Tables support row selection with two configuration options: selection mode and selection behavior.
#### Selection mode
Use `selectionMode` to control how many rows can be selected. With `single`, only one row can be selected at a time. With `multiple`, any number of rows can be selected, and a header checkbox provides select-all functionality.
<Snippet
preview={<SelectionModePlayground />}
code={tableSelectionModeSnippet}
/>
#### Selection behavior
Use `selectionBehavior` to control how selection is indicated and interacted with. With `toggle`, checkboxes appear for selection. With `replace`, selection is indicated by row background color—click to select, Cmd/Ctrl+click for multiple.
<Snippet
preview={<SelectionBehaviorPlayground />}
code={tableSelectionBehaviorSnippet}
/>
#### With row actions
With toggle behavior, clicking a row triggers its action when nothing is selected. Once any row is selected, clicking toggles selection instead.
With replace behavior, clicking selects the row and double-clicking triggers the action.
<Snippet
preview={<SelectionToggleWithActions />}
code={tableSelectionActionsSnippet}
/>
### Row Clicks
Coming soon.
### Pagination
Coming soon.
### Sorting
Coming soon.
### Asynchronous loading
Coming soon.
### Empty state
Coming soon.
### Column resizing
This feature is not available yet — let us know if you'd like us to explore it!
### Column reordering
This feature is not available yet — let us know if you'd like us to explore it!
### Column pinning
This feature is not available yet — let us know if you'd like us to explore it!
### Column visibility
This feature is not available yet — let us know if you'd like us to explore it!
<Theming definition={TableDefinition} />
<ChangelogComponent component="table" />
@@ -0,0 +1,21 @@
'use client';
import * as stories from '@backstage/ui/src/components/Table/stories/Table.docs.stories';
const {
TableRockBand: TableRockBandStory,
SelectionModePlayground: SelectionModePlaygroundStory,
SelectionBehaviorPlayground: SelectionBehaviorPlaygroundStory,
SelectionToggleWithActions: SelectionToggleWithActionsStory,
} = stories;
export const TableRockBand = () => <TableRockBandStory.Component />;
export const SelectionModePlayground = () => (
<SelectionModePlaygroundStory.Component />
);
export const SelectionBehaviorPlayground = () => (
<SelectionBehaviorPlaygroundStory.Component />
);
export const SelectionToggleWithActions = () => (
<SelectionToggleWithActionsStory.Component />
);
@@ -0,0 +1,377 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const tablePropDefs: Record<string, PropDef> = {
selectionBehavior: {
type: 'enum',
values: ['toggle', 'replace'],
default: 'toggle',
description: 'How multiple selection should behave in the collection.',
},
disabledBehavior: {
type: 'enum',
values: ['selection', 'all'],
default: 'selection',
description:
'Whether disabledKeys applies to all interactions, or only selection.',
},
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'A list of row keys to disable.',
},
selectionMode: {
type: 'enum',
values: ['single', 'multiple'],
description: 'The type of selection that is allowed in the collection.',
},
selectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
description: 'The currently selected keys in the collection (controlled).',
},
defaultSelectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
description: 'The initial selected keys in the collection (uncontrolled).',
},
onRowAction: {
type: 'enum',
values: ['(key: Key) => void'],
description:
'Handler that is called when a user performs an action on the row.',
},
onSelectionChange: {
type: 'enum',
values: ['(keys: Selection) => void'],
description: 'Handler that is called when the selection changes.',
},
onSortChange: {
type: 'enum',
values: ['(descriptor: SortDescriptor) => any'],
description:
'Handler that is called when the sorted column or direction changes.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const tableHeaderPropDefs: Record<string, PropDef> = {
onHoverStart: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
description: 'Handler that is called when a hover interaction starts.',
},
onHoverEnd: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
description: 'Handler that is called when a hover interaction ends.',
},
onHoverChange: {
type: 'enum',
values: ['(isHovering: boolean) => void'],
description: 'Handler that is called when the hover state changes.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const columnPropDefs: Record<string, PropDef> = {
id: {
type: 'enum',
values: ['Key'],
description: 'The unique id of the column.',
},
allowsSorting: {
type: 'boolean',
description: 'Whether the column allows sorting.',
},
isRowHeader: {
type: 'boolean',
description:
'Whether a column is a row header and should be announced by assistive technology during row navigation.',
},
textValue: {
type: 'string',
description:
"A string representation of the column's contents, used for accessibility announcements.",
},
...classNamePropDefs,
...stylePropDefs,
};
export const tableBodyPropDefs: Record<string, PropDef> = {
renderEmptyState: {
type: 'enum',
values: ['(props) => ReactNode'],
description:
'Provides content to display when there are no rows in the table.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const rowPropDefs: Record<string, PropDef> = {
textValue: {
type: 'string',
description:
"A string representation of the row's contents, used for accessibility announcements.",
},
isDisabled: {
type: 'boolean',
description: 'Whether the row is disabled.',
},
id: {
type: 'enum',
values: ['Key'],
description: 'The unique id of the row.',
},
href: {
type: 'string',
description: 'The URL to navigate to when the row is clicked.',
},
hrefLang: {
type: 'string',
description:
'The language of the URL to navigate to when the row is clicked.',
},
target: {
type: 'string',
description:
'The target of the URL to navigate to when the row is clicked.',
},
rel: {
type: 'string',
description:
'The relationship of the URL to navigate to when the row is clicked.',
},
onAction: {
type: 'enum',
values: ['() => void'],
description:
"Handler that is called when a user performs an action on the row. The exact user event depends on the collection's selectionBehavior prop and the interaction modality.",
},
onHoverStart: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
description: 'Handler that is called when a hover interaction starts.',
},
onHoverEnd: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
description: 'Handler that is called when a hover interaction ends.',
},
onHoverChange: {
type: 'enum',
values: ['(isHovering: boolean) => void'],
description: 'Handler that is called when the hover state changes.',
},
onPress: {
type: 'enum',
values: ['(e: PressEvent) => void'],
description:
'Handler that is called when the press is released over the target.',
},
onPressStart: {
type: 'enum',
values: ['(e: PressEvent) => void'],
description: 'Handler that is called when a press interaction starts.',
},
onPressEnd: {
type: 'enum',
values: ['(e: PressEvent) => void'],
description:
'Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target.',
},
onPressChange: {
type: 'enum',
values: ['(isPressed: boolean) => void'],
description: 'Handler that is called when the press state changes.',
},
onPressUp: {
type: 'enum',
values: ['(e: PressEvent) => void'],
description:
'Handler that is called when a press is released over the target, regardless of whether it started on the target or not.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const cellPropDefs: Record<string, PropDef> = {
id: {
type: 'enum',
values: ['Key'],
description: 'The unique id of the cell.',
},
textValue: {
type: 'string',
description:
"A string representation of the cell's contents, used for features like typeahead.",
},
leadingIcon: {
type: 'enum',
values: ['ReactNode'],
description: 'Optional icon to display before the cell content.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const tablePaginationPropDefs: Record<string, PropDef> = {
offset: {
type: 'number',
description: 'The current offset (starting index) for pagination.',
},
pageSize: {
type: 'number',
description: 'The number of items per page.',
},
setOffset: {
type: 'enum',
values: ['(offset: number) => void'],
description: 'Handler that is called when the offset changes.',
},
setPageSize: {
type: 'enum',
values: ['(pageSize: number) => void'],
description: 'Handler that is called when the page size changes.',
},
rowCount: {
type: 'number',
description: 'The total number of rows in the table.',
},
onNextPage: {
type: 'enum',
values: ['() => void'],
description: 'Handler that is called when the next page is requested.',
},
onPreviousPage: {
type: 'enum',
values: ['() => void'],
description: 'Handler that is called when the previous page is requested.',
},
onPageSizeChange: {
type: 'enum',
values: ['(pageSize: number) => void'],
description: 'Handler that is called when the page size changes.',
},
showPageSizeOptions: {
type: 'boolean',
description: 'Whether to show the page size options.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const tableUsageSnippet = `import { Cell, CellText, ..., TableHeader, TablePagination } from '@backstage/ui';
<Table>
<TableHeader>
<Column />
</TableHeader>
<TableBody>
<Row>
<CellText title="Example" />
<CellProfile />
</Row>
</TableBody>
</Table>
<TablePagination />`;
export const tableBasicSnippet = `import { Table, TableHeader, Column, TableBody, Row, CellText, CellProfile, TablePagination, useTable } from '@backstage/ui';
const data = [
{
name: 'The Beatles',
image: 'https://upload.wikimedia.org/wikipedia/en/thumb/4/42/Beatles_-...jpg',
genre: 'Rock, Pop, Psychedelic Rock',
yearFormed: 1960,
albums: 13
},
// ... more data
];
// Uncontrolled pagination (easiest)
const { data: paginatedData, paginationProps } = useTable({
data,
pagination: {
defaultPageSize: 5,
},
});
<Table>
<TableHeader>
<Column isRowHeader>Band name</Column>
<Column>Genre</Column>
<Column>Year formed</Column>
<Column>Albums</Column>
</TableHeader>
<TableBody>
{paginatedData?.map(item => (
<Row key={item.name}>
<CellProfile
name={item.name}
src={item.image}
href={item.website}
/>
<CellText title={item.genre} />
<CellText title={item.yearFormed.toString()} />
<CellText title={item.albums.toString()} />
</Row>
))}
</TableBody>
</Table>
<TablePagination {...paginationProps} />`;
export const tableSelectionActionsSnippet = `import { Table, TableHeader, TableBody, Column, Row, CellText } from '@backstage/ui';
function MyTable() {
const [selectedKeys, setSelectedKeys] = React.useState(new Set([]));
return (
<Table
selectionMode="multiple"
selectionBehavior="toggle"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
onRowAction={(key) => console.log('Opening', key)}
>
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Status</Column>
</TableHeader>
<TableBody>
<Row id="1">
<CellText title="Component A" />
<CellText title="Active" />
</Row>
<Row id="2">
<CellText title="Component B" />
<CellText title="Inactive" />
</Row>
</TableBody>
</Table>
);
}`;
export const tableSelectionModeSnippet = `<Table
selectionMode="multiple" // or "single"
selectionBehavior="toggle"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
>
{/* ... */}
</Table>`;
export const tableSelectionBehaviorSnippet = `<Table
selectionMode="multiple"
selectionBehavior="toggle" // or "replace"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
>
{/* ... */}
</Table>`;
+85
View File
@@ -0,0 +1,85 @@
import { PropsTable } from '@/components/PropsTable';
import { Default, WithTabPanels, WithMockedURLTab2, PrefixMatchingDeepNesting } from './stories';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
tabsPropDefs,
tabPropDefs,
tabsUsageSnippet,
tabsDefaultSnippet,
tabsWithTabPanelsSnippet,
tabsWithLinksSnippet,
tabsWithDeeplyNestedRoutesSnippet,
} from './tabs.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { TabsDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Tabs"
description="A component for toggling between related panels on the same page."
/>
<Snippet
py={4}
preview={<Default />}
code={tabsDefaultSnippet}
/>
## Usage
<CodeBlock code={tabsUsageSnippet} />
## API reference
### Tabs
Groups the tabs and the corresponding panels. Renders a `<div>` element.
<PropsTable data={tabsPropDefs} />
### Tab
An individual interactive tab button that toggles the corresponding panel. Renders a `<button>` element.
<PropsTable data={tabPropDefs} />
## Examples
### Simple tabs
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={<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={<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={<PrefixMatchingDeepNesting />}
code={tabsWithDeeplyNestedRoutesSnippet}
open
/>
<Theming definition={TabsDefinition} />
<ChangelogComponent component="tabs" />
@@ -0,0 +1,17 @@
'use client';
import * as stories from '@backstage/ui/src/components/Tabs/Tabs.stories';
const {
Default: DefaultStory,
WithTabPanels: WithTabPanelsStory,
WithMockedURLTab2: WithMockedURLTab2Story,
PrefixMatchingDeepNesting: PrefixMatchingDeepNestingStory,
} = stories;
export const Default = () => <DefaultStory.Component />;
export const WithTabPanels = () => <WithTabPanelsStory.Component />;
export const WithMockedURLTab2 = () => <WithMockedURLTab2Story.Component />;
export const PrefixMatchingDeepNesting = () => (
<PrefixMatchingDeepNestingStory.Component />
);
@@ -0,0 +1,122 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const tabsPropDefs: Record<string, PropDef> = {
isDisabled: {
type: 'boolean',
},
disabledKeys: {
type: 'enum',
values: ['string[]'],
},
selectedKey: {
type: 'enum',
values: ['string', 'null'],
},
defaultSelectedKey: {
type: 'enum',
values: ['string'],
},
onSelectionChange: {
type: 'enum',
values: [`(key: string) => void`],
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabPropDefs: Record<string, PropDef> = {
id: {
type: 'string',
},
isDisabled: {
type: 'boolean',
},
href: {
type: 'string',
},
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,
...stylePropDefs,
};
export const tabsUsageSnippet = `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</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 = `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>`;
@@ -0,0 +1,115 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Default, WithLink, WithIcon, Sizes, RemovingTags, Disabled } from './stories';
import {
tagGroupPropDefs,
tagPropDefs,
usage,
preview,
withLink,
disabled,
withIcons,
sizes,
removingTags,
} from './tag-group.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { TagGroupDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="TagGroup"
description="A tag group is a list of labels, categories, keywords, filters, or other items, that can be used to group and filter content."
/>
<Snippet
align="center"
py={4}
preview={<Default />}
code={preview}
/>
## Usage
<CodeBlock code={usage} />
## API reference
### TagGroup
A tag group is a list of tags.
<PropsTable data={tagGroupPropDefs} />
### Tag
A tag is a single item in a tag group.
<PropsTable data={tagPropDefs} />
## Examples
### With Links
A tag can be a link by passing a `href` prop.
<Snippet
align="center"
py={4}
open
preview={<WithLink />}
code={withLink}
/>
### With Icons
A tag can have an icon by passing a `icon` prop.
<Snippet
align="center"
py={4}
open
preview={<WithIcon />}
code={withIcons}
/>
### Sizes
A tag can have a size by passing a `size` prop. It could be `small` or `medium`.
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={sizes}
/>
### Removing tags
A tag can be removed by passing a `onRemove` prop.
<Snippet
align="center"
py={4}
open
preview={<RemovingTags />}
code={removingTags}
/>
### Disabled
A switch can be disabled using the `isDisabled` prop.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={disabled}
/>
<Theming definition={TagGroupDefinition} />
<ChangelogComponent component="switch" />
@@ -0,0 +1,19 @@
'use client';
import * as stories from '@backstage/ui/src/components/TagGroup/TagGroup.stories';
const {
Default: DefaultStory,
WithLink: WithLinkStory,
WithIcon: WithIconStory,
Sizes: SizesStory,
RemovingTags: RemovingTagsStory,
Disabled: DisabledStory,
} = stories;
export const Default = () => <DefaultStory.Component />;
export const WithLink = () => <WithLinkStory.Component />;
export const WithIcon = () => <WithIconStory.Component />;
export const Sizes = () => <SizesStory.Component />;
export const RemovingTags = () => <RemovingTagsStory.Component />;
export const Disabled = () => <DisabledStory.Component />;
@@ -0,0 +1,161 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const tagGroupPropDefs: Record<string, PropDef> = {
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'A list of row keys to disable.',
},
selectionMode: {
type: 'enum',
values: ['none', 'single', 'multiple'],
description: 'The type of selection that is allowed in the collection.',
},
selectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
description: 'The currently selected keys in the collection (controlled).',
},
defaultSelectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
description: 'The initial selected keys in the collection (uncontrolled).',
},
onRemove: {
type: 'enum',
values: ['(keys: Set<Key>) => void'],
description: 'Handler that is called when a tag is removed.',
},
onSelectionChange: {
type: 'enum',
values: ['(keys: Selection) => void'],
description: 'Handler that is called when the selection changes.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const tagPropDefs: Record<string, PropDef> = {
id: {
type: 'string',
description: 'The id of the tag.',
},
textValue: {
type: 'string',
description: 'The text value of the tag.',
},
isDisabled: {
type: 'boolean',
description: 'Whether the tag is disabled.',
},
href: {
type: 'string',
description: 'The href of the tag.',
},
icon: {
type: 'enum',
values: ['ReactNode'],
description: 'The icon of the tag.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const preview = `import { TagGroup, Tag } from '@backstage/ui';
<TagGroup aria-label="Tag Group">
<Tag>Banana</Tag>
<Tag>Apple</Tag>
<Tag>Orange</Tag>
<Tag>Pear</Tag>
<Tag>Grape</Tag>
<Tag>Pineapple</Tag>
<Tag>Strawberry</Tag>
</TagGroup>
`;
export const usage = `import { TagGroup, Tag } from '@backstage/ui';
// Basic usage
<TagGroup aria-label="Tag Group">
<Tag>Tag 1</Tag>
<Tag>Tag 2</Tag>
<Tag>Tag 3</Tag>
</TagGroup>
// With the items prop
const list = [
{ id: '1', name: 'Tag 1' },
{ id: '2', name: 'Tag 2' },
{ id: '3', name: 'Tag 3' },
];
<TagGroup aria-label="Tag Group" items={list}>
{item => <Tag>{item.name}</Tag>}
</TagGroup>`;
export const withLink = `import { TagGroup, Tag } from '@backstage/ui';
<TagGroup aria-label="Tag Group">
<Tag href="/items/banana">Banana</Tag>
<Tag href="/items/apple">Apple</Tag>
<Tag href="/items/orange">Orange</Tag>
...
</TagGroup>`;
export const disabled = `import { TagGroup, Tag } from '@backstage/ui';
<TagGroup aria-label="Tag Group">
<Tag>Banana</Tag>
<Tag isDisabled>Apple</Tag>
<Tag isDisabled>Orange</Tag>
<Tag>Pear</Tag>
...
</TagGroup>`;
export const withIcons = `import { TagGroup, Tag } from '@backstage/ui';
<TagGroup aria-label="Tag Group">
<Tag icon={<RiBugLine />}>Banana</Tag>
<Tag icon={<RiAccountCircleLine />}>Apple</Tag>
<Tag icon={<RiEyeLine />}>Orange</Tag>
...
</TagGroup>`;
export const sizes = `import { TagGroup, Tag, Flex } from '@backstage/ui';
<Flex direction="column">
<TagGroup aria-label="Tag Group">
<Tag size="small">Banana</Tag>
<Tag size="small">Apple</Tag>
<Tag size="small">Orange</Tag>
...
</TagGroup>
<TagGroup aria-label="Tag Group">
<Tag size="medium">Banana</Tag>
<Tag size="medium">Apple</Tag>
<Tag size="medium">Orange</Tag>
...
</TagGroup>
</Flex>`;
export const removingTags = `import { TagGroup, Tag } from '@backstage/ui';
import type { Selection } from 'react-aria-components';
import { useListData } from 'react-stately';
const [selected, setSelected] = useState<Selection>(new Set(['travel']));
const list = useListData({
initialItems: initialList,
});
<TagGroup
items={list.items}
onRemove={keys => list.remove(...keys)}
selectedKeys={selected}
onSelectionChange={setSelected}
{...args}
>
{item => <Tag>{item.name}</Tag>}
</TagGroup>`;
@@ -0,0 +1,65 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { WithLabel, Sizes, WithDescription } from './stories';
import {
inputPropDefs,
textFieldUsageSnippet,
textFieldDefaultSnippet,
textFieldSizesSnippet,
textFieldDescriptionSnippet,
} from './text-field.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { TextFieldDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
<PageTitle
title="TextField"
description="A text field component for your forms."
/>
<Snippet
align="center"
py={4}
preview={<WithLabel />}
code={textFieldDefaultSnippet}
/>
## Usage
<CodeBlock code={textFieldUsageSnippet} />
## API reference
<PropsTable data={inputPropDefs} />
## Examples
### Sizes
We support two different sizes: `small`, `medium`.
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={textFieldSizesSnippet}
/>
### With description
Here's a simple TextField with a description.
<Snippet
align="center"
py={4}
open
preview={<WithDescription />}
code={textFieldDescriptionSnippet}
/>
<Theming definition={TextFieldDefinition} />
<ChangelogComponent component="text-field" />
@@ -0,0 +1,13 @@
'use client';
import * as stories from '@backstage/ui/src/components/TextField/TextField.stories';
const {
WithLabel: WithLabelStory,
Sizes: SizesStory,
WithDescription: WithDescriptionStory,
} = stories;
export const WithLabel = () => <WithLabelStory.Component />;
export const Sizes = () => <SizesStory.Component />;
export const WithDescription = () => <WithDescriptionStory.Component />;
@@ -0,0 +1,43 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const inputPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
},
label: {
type: 'string',
},
icon: {
type: 'enum',
values: ['ReactNode'],
},
description: {
type: 'string',
},
name: {
type: 'string',
required: true,
},
...classNamePropDefs,
...stylePropDefs,
};
export const textFieldUsageSnippet = `import { TextField } from '@backstage/ui';
<TextField />`;
export const textFieldDefaultSnippet = `<TextField label="Label" placeholder="Enter a URL" />`;
export const textFieldSizesSnippet = `<Flex direction="row" gap="4">
<TextField size="small" placeholder="Small" icon={<Icon name="sparkling" />} />
<TextField size="medium" placeholder="Medium" icon={<Icon name="sparkling" />} />
</Flex>`;
export const textFieldDescriptionSnippet = `<TextField label="Label" description="Description" placeholder="Enter a URL" />`;
+93
View File
@@ -0,0 +1,93 @@
import { PropsTable } from '@/components/PropsTable';
import { Default, AllVariants, AllWeights, AllColors, Truncate } from './stories';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
textPropDefs,
textUsageSnippet,
textDefaultSnippet,
textVariantsSnippet,
textWeightsSnippet,
textTruncateSnippet,
textResponsiveSnippet,
textColorsSnippet,
} from './text.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { TextDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Text"
description="The `Text` component is used to display content on your page."
/>
<Snippet
py={4}
preview={<Default />}
code={textDefaultSnippet}
/>
## Usage
<CodeBlock code={textUsageSnippet} />
## API reference
<PropsTable data={textPropDefs} />
## Examples
### All variants
The `Text` component has a `variant` prop that can be used to change the
appearance of the text.
<Snippet
open
preview={<AllVariants />}
code={textVariantsSnippet}
/>
### All weights
The `Text` component has a `weight` prop that can be used to change the
appearance of the text.
<Snippet
open
preview={<AllWeights />}
code={textWeightsSnippet}
/>
### All colors
The `Text` component has a `color` prop that can be used to change the
appearance of the text.
<Snippet
open
preview={<AllColors />}
code={textColorsSnippet}
/>
### Truncate
The `Text` component has a `truncate` prop that can be used to truncate the text.
<Snippet
open
preview={<Truncate />}
code={textTruncateSnippet}
/>
### Responsive
You can also use the `variant` prop to change the appearance of the text based
on the screen size.
<CodeBlock code={textResponsiveSnippet} />
<Theming definition={TextDefinition} />
<ChangelogComponent component="text" />
@@ -0,0 +1,17 @@
'use client';
import * as stories from '@backstage/ui/src/components/Text/Text.stories';
const {
Default: DefaultStory,
AllVariants: AllVariantsStory,
AllWeights: AllWeightsStory,
AllColors: AllColorsStory,
Truncate: TruncateStory,
} = stories;
export const Default = () => <DefaultStory.Component />;
export const AllVariants = () => <AllVariantsStory.Component />;
export const AllWeights = () => <AllWeightsStory.Component />;
export const AllColors = () => <AllColorsStory.Component />;
export const Truncate = () => <TruncateStory.Component />;
@@ -0,0 +1,131 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
} from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const textPropDefs: Record<string, PropDef> = {
as: {
type: 'enum',
values: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'p',
'span',
'label',
'div',
'strong',
'em',
'small',
],
default: 'span',
responsive: true,
},
variant: {
type: 'enum',
values: [
'title-large',
'title-medium',
'title-small',
'title-x-small',
'body-large',
'body-medium',
'body-small',
'body-x-small',
],
responsive: true,
},
weight: {
type: 'enum',
values: ['regular', 'bold'],
responsive: true,
},
color: {
type: 'enum',
values: ['primary', 'secondary', 'danger', 'warning', 'success'],
default: 'primary',
responsive: true,
},
truncate: {
type: 'boolean',
default: 'false',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const textUsageSnippet = `import { Text } from '@backstage/ui';
<Text />`;
export const textDefaultSnippet = `<Text style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, "Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son." Who is
in the painting?
</Text>`;
export const textVariantsSnippet = `<Flex direction="column" gap="4">
<Text variant="title-large">...</Text>
<Text variant="title-medium">...</Text>
<Text variant="title-small">...</Text>
<Text variant="title-x-small">...</Text>
<Text variant="body-large">...</Text>
<Text variant="body-medium">...</Text>
<Text variant="body-small">...</Text>
<Text variant="body-x-small">...</Text>
</Flex>`;
export const textWeightsSnippet = `<Flex direction="column" gap="4">
<Flex>
<Text variant="title-large" weight="regular">A fox</Text>
<Text variant="title-large" weight="bold">A turtle</Text>
</Flex>
<Flex>
<Text variant="title-medium" weight="regular">A fox</Text>
<Text variant="title-medium" weight="bold">A turtle</Text>
</Flex>
<Flex>
<Text variant="title-small" weight="regular">A fox</Text>
<Text variant="title-small" weight="bold">A turtle</Text>
</Flex>
<Flex>
<Text variant="title-x-small" weight="regular">A fox</Text>
<Text variant="title-x-small" weight="bold">A turtle</Text>
</Flex>
<Flex>
<Text variant="body-large" weight="regular">A fox</Text>
<Text variant="body-large" weight="bold">A turtle</Text>
</Flex>
<Flex>
<Text variant="body-medium" weight="regular">A fox</Text>
<Text variant="body-medium" weight="bold">A turtle</Text>
</Flex>
<Flex>
<Text variant="body-small" weight="regular">A fox</Text>
<Text variant="body-small" weight="bold">A turtle</Text>
</Flex>
<Flex>
<Text variant="body-x-small" weight="regular">A fox</Text>
<Text variant="body-x-small" weight="bold">A turtle</Text>
</Flex>
</Flex>`;
export const textColorsSnippet = `<Flex direction="column" gap="4">
<Text color="primary">I am primary</Text>
<Text color="secondary">I am secondary</Text>
<Text color="danger">I am danger</Text>
<Text color="warning">I am warning</Text>
<Text color="success">I am success</Text>
</Flex>`;
export const textTruncateSnippet = `<Text as="p" truncate>...</Text>`;
export const textResponsiveSnippet = `<Text variant={{ initial: 'body', lg: 'subtitle' }}>
Responsive text
</Text>`;
@@ -0,0 +1,126 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
toggleButtonGroupPropDefs,
toggleButtonGroupUsageSnippet,
toggleButtonGroupSingleSnippet,
toggleButtonGroupMultipleSnippet,
toggleButtonGroupVerticalSnippet,
toggleButtonGroupDisabledSnippet,
toggleButtonGroupDisallowEmptySnippet,
toggleButtonGroupIconsSnippet,
toggleButtonGroupIconsOnlySnippet,
toggleButtonGroupSurfacesSnippet,
} from './toggle-button-group.props';
import { SingleSelection, Surfaces, MultipleSelection, WithIcons, IconsOnly, DisallowEmptySelection, Orientation, DisabledGroup } from './stories';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ToggleButtonGroupDefinition } from '../../../utils/definitions';
<PageTitle
title="ToggleButtonGroup"
description="A grouping container for related [ToggleButtons](./toggle-button) supporting single or multiple selection."
/>
<Snippet
align="center"
py={4}
preview={<SingleSelection />}
code={toggleButtonGroupSingleSnippet}
/>
## Usage
<CodeBlock code={toggleButtonGroupUsageSnippet} />
## API reference
<PropsTable data={toggleButtonGroupPropDefs} />
## Examples
### Surfaces
<Snippet
align="center"
py={4}
open
preview={<Surfaces />}
code={toggleButtonGroupSurfacesSnippet}
/>
### Single Selection
<Snippet
align="center"
py={4}
open
preview={<SingleSelection />}
code={toggleButtonGroupSingleSnippet}
/>
### Multiple Selection
<Snippet
align="center"
py={4}
open
preview={<MultipleSelection />}
code={toggleButtonGroupMultipleSnippet}
/>
### Icons and Text
<Snippet
align="center"
py={4}
open
preview={<WithIcons />}
code={toggleButtonGroupIconsSnippet}
/>
### Icons Only
<Snippet
align="center"
py={4}
open
preview={<IconsOnly />}
code={toggleButtonGroupIconsOnlySnippet}
/>
### Disallow Empty Selection
<Snippet
align="center"
py={4}
open
preview={<DisallowEmptySelection />}
code={toggleButtonGroupDisallowEmptySnippet}
/>
### Vertical Orientation
<Snippet
align="center"
py={4}
open
preview={<Orientation />}
code={toggleButtonGroupVerticalSnippet}
/>
### Disabled
<Snippet
align="center"
py={4}
open
preview={<DisabledGroup />}
code={toggleButtonGroupDisabledSnippet}
/>
<Theming definition={ToggleButtonGroupDefinition} />
<ChangelogComponent component="toggle-button-group" />
@@ -0,0 +1,25 @@
'use client';
import * as stories from '@backstage/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories';
const {
SingleSelection: SingleSelectionStory,
Surfaces: SurfacesStory,
MultipleSelection: MultipleSelectionStory,
WithIcons: WithIconsStory,
IconsOnly: IconsOnlyStory,
DisallowEmptySelection: DisallowEmptySelectionStory,
Orientation: OrientationStory,
DisabledGroup: DisabledGroupStory,
} = stories;
export const SingleSelection = () => <SingleSelectionStory.Component />;
export const Surfaces = () => <SurfacesStory.Component />;
export const MultipleSelection = () => <MultipleSelectionStory.Component />;
export const WithIcons = () => <WithIconsStory.Component />;
export const IconsOnly = () => <IconsOnlyStory.Component />;
export const DisallowEmptySelection = () => (
<DisallowEmptySelectionStory.Component />
);
export const Orientation = () => <OrientationStory.Component />;
export const DisabledGroup = () => <DisabledGroupStory.Component />;
@@ -0,0 +1,142 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const toggleButtonGroupPropDefs: Record<string, PropDef> = {
selectionMode: {
type: 'enum',
values: ['single', 'multiple'],
default: 'single',
},
orientation: {
type: 'enum',
values: ['horizontal', 'vertical'],
default: 'horizontal',
responsive: true,
},
selectedKeys: { type: 'enum', values: ['Iterable<Key>'] },
defaultSelectedKeys: { type: 'enum', values: ['Iterable<Key>'] },
onSelectionChange: { type: 'enum', values: ['(keys) => void'] },
isDisabled: { type: 'boolean', default: 'false' },
disallowEmptySelection: { type: 'boolean', default: 'false' },
...classNamePropDefs,
...stylePropDefs,
};
export const toggleButtonGroupUsageSnippet = `import { ToggleButtonGroup, ToggleButton } from '@backstage/ui';
<ToggleButtonGroup selectionMode="single">
<ToggleButton id="dogs">Dogs</ToggleButton>
<ToggleButton id="cats">Cats</ToggleButton>
<ToggleButton id="birds">Birds</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupSingleSnippet = `<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['dogs']}>
<ToggleButton id="dogs">Dogs</ToggleButton>
<ToggleButton id="cat">Cats</ToggleButton>
<ToggleButton id="bird">Birds</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupMultipleSnippet = `<ToggleButtonGroup selectionMode="multiple" defaultSelectedKeys={['frontend']}>
<ToggleButton id="frontend">Frontend</ToggleButton>
<ToggleButton id="backend">Backend</ToggleButton>
<ToggleButton id="platform">Platform</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupVerticalSnippet = `<ToggleButtonGroup selectionMode="single" orientation="vertical">
<ToggleButton id="morning">Morning</ToggleButton>
<ToggleButton id="afternoon">Afternoon</ToggleButton>
<ToggleButton id="evening">Evening</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupDisabledSnippet = `<ToggleButtonGroup selectionMode="single" isDisabled>
<ToggleButton id="cat">Cat</ToggleButton>
<ToggleButton id="dog">Dog</ToggleButton>
<ToggleButton id="bird">Bird</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupDisallowEmptySnippet = `<ToggleButtonGroup selectionMode="single" disallowEmptySelection defaultSelectedKeys={['one']}>
<ToggleButton id="one">One</ToggleButton>
<ToggleButton id="two">Two</ToggleButton>
<ToggleButton id="three">Three</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupIconsSnippet = `import { RiCloudLine, RiStarFill, RiStarLine, RiArrowRightSLine } from '@remixicon/react';
<ToggleButtonGroup selectionMode="multiple" defaultSelectedKeys={['cloud']}>
<ToggleButton id="cloud" aria-label="Cloud" iconStart={<RiCloudLine />} />
<ToggleButton
id="starred"
aria-label="Starred"
iconStart={<RiStarFill />}
/>
<ToggleButton id="star" iconStart={<RiStarLine />}>
Star
</ToggleButton>
<ToggleButton id="next" iconEnd={<RiArrowRightSLine />}>
Next
</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupIconsOnlySnippet = `import { RiCloudLine, RiStarLine, RiArrowRightSLine } from '@remixicon/react';
<ToggleButtonGroup selectionMode="multiple" defaultSelectedKeys={['cloud']}>
<ToggleButton id="cloud" iconStart={<RiCloudLine />} />
<ToggleButton id="star" iconStart={<RiStarLine />} />
<ToggleButton id="next" iconEnd={<RiArrowRightSLine />} />
</ToggleButtonGroup>`;
export const toggleButtonGroupSurfacesSnippet = `<Flex direction="column" gap="4">
<Flex direction="column" gap="4">
<Text>Default</Text>
<Flex align="center" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 0</Text>
<Flex align="center" surface="0" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex align="center" surface="1" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 2</Text>
<Flex align="center" surface="2" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex align="center" surface="3" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
</Flex>`;
@@ -0,0 +1,106 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Default, Surfaces, Sizes, WithIcons, Disabled, Controlled, DynamicContent } from './stories';
import {
toggleButtonPropDefs,
toggleButtonUsageSnippet,
toggleButtonSurfacesSnippet,
toggleButtonSizesSnippet,
toggleButtonIconsSnippet,
toggleButtonDisabledSnippet,
toggleButtonControlledSnippet,
toggleButtonFunctionChildrenSnippet,
} from './toggle-button.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ToggleButtonDefinition } from '../../../utils/definitions';
<PageTitle
title="ToggleButton"
description="A button that toggles between selected and unselected states. Can be used as a single toggle or as part of a [ToggleButtonGroup](./toggle-button-group)."
/>
<Snippet
align="center"
py={4}
preview={<Default />}
code={toggleButtonUsageSnippet}
/>
## Usage
<CodeBlock code={toggleButtonUsageSnippet} />
## API reference
<PropsTable data={toggleButtonPropDefs} />
## Examples
### Surfaces
<Snippet
align="center"
py={4}
open
preview={<Surfaces />}
code={toggleButtonSurfacesSnippet}
/>
### Sizes
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={toggleButtonSizesSnippet}
/>
### With Icons
<Snippet
align="center"
py={4}
open
preview={<WithIcons />}
code={toggleButtonIconsSnippet}
/>
### Disabled
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={toggleButtonDisabledSnippet}
/>
### Controlled
<Snippet
align="center"
py={4}
open
preview={<Controlled />}
code={toggleButtonControlledSnippet}
/>
### Dynamic Content with Function Children
The `children` prop can be a function that receives render props, allowing you to dynamically customize the button content based on component state (such as `isSelected`, `isDisabled`, `isHovered`, etc.).
<Snippet
align="center"
py={4}
open
preview={<DynamicContent />}
code={toggleButtonFunctionChildrenSnippet}
/>
<Theming definition={ToggleButtonDefinition} />
<ChangelogComponent component="toggle-button" />
@@ -0,0 +1,21 @@
'use client';
import * as stories from '@backstage/ui/src/components/ToggleButton/ToggleButton.stories';
const {
Default: DefaultStory,
Surfaces: SurfacesStory,
Sizes: SizesStory,
WithIcons: WithIconsStory,
Disabled: DisabledStory,
Controlled: ControlledStory,
DynamicContent: DynamicContentStory,
} = stories;
export const Default = () => <DefaultStory.Component />;
export const Surfaces = () => <SurfacesStory.Component />;
export const Sizes = () => <SizesStory.Component />;
export const WithIcons = () => <WithIconsStory.Component />;
export const Disabled = () => <DisabledStory.Component />;
export const Controlled = () => <ControlledStory.Component />;
export const DynamicContent = () => <DynamicContentStory.Component />;
@@ -0,0 +1,195 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const toggleButtonPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
},
onSurface: {
type: 'enum',
values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
description: 'Surface level this toggle is placed on',
responsive: true,
},
iconStart: { type: 'enum', values: ['ReactNode'] },
iconEnd: { type: 'enum', values: ['ReactNode'] },
isSelected: { type: 'boolean' },
defaultSelected: { type: 'boolean' },
onChange: { type: 'enum', values: ['(isSelected: boolean) => void'] },
isDisabled: { type: 'boolean', default: 'false' },
children: {
type: 'enum',
values: ['ReactNode', '(values: ToggleButtonRenderProps) => ReactNode'],
description:
'The children of the component. A function may be provided to alter the children based on component state (such as `isSelected`, `isDisabled`, `isHovered`, etc.).',
},
...classNamePropDefs,
...stylePropDefs,
};
export const toggleButtonUsageSnippet = `import { ToggleButton } from '@backstage/ui';
<ToggleButton>Toggle</ToggleButton>`;
export const toggleButtonSurfacesSnippet = `<Flex direction="column" gap="4">
<Flex direction="column" gap="4">
<Text>Default</Text>
<Flex align="center" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 0</Text>
<Flex align="center" surface="0" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex align="center" surface="1" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 2</Text>
<Flex align="center" surface="2" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex align="center" surface="3" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
</Flex>`;
export const toggleButtonSizesSnippet = `<Flex align="center">
<ToggleButton size="small">Small</ToggleButton>
<ToggleButton size="medium">Medium</ToggleButton>
</Flex>`;
export const toggleButtonIconsSnippet = `import { RiStarLine, RiStarFill, RiCheckLine } from '@remixicon/react';
<Flex align="center">
<ToggleButton iconStart={<RiStarLine />}>Favorite</ToggleButton>
<ToggleButton iconStart={<RiStarFill />} defaultSelected>Starred</ToggleButton>
<ToggleButton iconEnd={<RiCheckLine />}>Confirm</ToggleButton>
</Flex>`;
export const toggleButtonDisabledSnippet = `<Flex align="center">
<ToggleButton isDisabled>Disabled</ToggleButton>
<ToggleButton defaultSelected isDisabled>Selected</ToggleButton>
</Flex>`;
export const toggleButtonControlledSnippet = `import { useState } from 'react';
import { RiStarFill, RiStarLine } from '@remixicon/react';
const [selected, setSelected] = useState(false);
<ToggleButton
isSelected={selected}
onChange={setSelected}
iconStart={selected ? <RiStarFill /> : <RiStarLine />}
>
{selected ? 'Starred' : 'Not starred'}
</ToggleButton>`;
export const toggleButtonFunctionChildrenSnippet = `import { RiStarFill, RiStarLine } from '@remixicon/react';
<Flex direction="column" gap="4">
<Flex direction="column" gap="2">
<Text weight="bold">Example 1: Selection State</Text>
<Flex align="center" gap="2">
<ToggleButton defaultSelected>
{({ isSelected }) => (isSelected ? '✓ Selected' : 'Not Selected')}
</ToggleButton>
<ToggleButton>
{({ isSelected }) => (isSelected ? '✓ Selected' : 'Not Selected')}
</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="2">
<Text weight="bold">Example 2: Multiple States</Text>
<Flex align="center" gap="2">
<ToggleButton defaultSelected>
{({ isSelected, isHovered }) => {
const states = [];
if (isSelected) states.push('on');
else states.push('off');
if (isHovered) states.push('hovered');
return \`Email (\${states.join(', ')})\`;
}}
</ToggleButton>
<ToggleButton>
{({ isSelected, isHovered }) => {
const states = [];
if (isSelected) states.push('on');
else states.push('off');
if (isHovered) states.push('hovered');
return \`Push (\${states.join(', ')})\`;
}}
</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="2">
<Text weight="bold">Example 3: Conditional Icons</Text>
<Flex align="center" gap="2">
<ToggleButton>
{({ isSelected }) => (
<>
{isSelected ? <RiStarFill /> : <RiStarLine />}
<span>{isSelected ? 'Starred' : 'Star'}</span>
</>
)}
</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="2">
<Text weight="bold">Example 4: Status Indicators</Text>
<Flex align="center" gap="2">
<ToggleButton defaultSelected>
{({ isSelected }) => (
<Flex align="center" gap="2">
<span
style={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: isSelected
? 'var(--bui-fg-success)'
: 'var(--bui-fg-secondary)',
}}
/>
<span>Active</span>
</Flex>
)}
</ToggleButton>
<ToggleButton>
{({ isSelected }) => (
<Flex align="center" gap="2">
<span
style={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: isSelected
? 'var(--bui-fg-danger)'
: 'var(--bui-fg-secondary)',
}}
/>
<span>Inactive</span>
</Flex>
)}
</ToggleButton>
</Flex>
</Flex>
</Flex>`;
@@ -0,0 +1,48 @@
import { PropsTable } from '@/components/PropsTable';
import { Default } from './stories';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
tooltipDefaultSnippet,
tooltipUsageSnippet,
tooltipTriggerPropDefs,
tooltipPropDefs,
} from './tooltip.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { TooltipDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Tooltip"
description="A tooltip displays a description of an element on hover or focus."
/>
<Snippet
py={4}
preview={<Default />}
code={tooltipDefaultSnippet}
align="center"
/>
## Usage
<CodeBlock code={tooltipUsageSnippet} />
## API reference
### TooltipTrigger
The trigger will wrap both the trigger and the tooltip.
<PropsTable data={tooltipTriggerPropDefs} />
### Tooltip
The tooltip will wrap the content of the tooltip.
<PropsTable data={tooltipPropDefs} />
<Theming definition={TooltipDefinition} />
<ChangelogComponent component="tooltip" />
@@ -0,0 +1,7 @@
'use client';
import * as stories from '@backstage/ui/src/components/Tooltip/Tooltip.stories';
const { Default: DefaultStory } = stories;
export const Default = () => <DefaultStory.Component />;
@@ -0,0 +1,67 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
} from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const tooltipTriggerPropDefs: Record<string, PropDef> = {
isDisabled: {
type: 'boolean',
},
delay: {
type: 'number',
default: '600',
},
closeDelay: {
type: 'number',
default: '500',
},
isOpen: {
type: 'boolean',
},
defaultOpen: {
type: 'boolean',
},
...childrenPropDefs,
};
export const tooltipPropDefs: Record<string, PropDef> = {
triggerRef: {
type: 'enum',
values: ['RefObject<Element | null>'],
},
isEntering: {
type: 'boolean',
},
isExiting: {
type: 'boolean',
},
placement: {
type: 'enum',
values: ['top', 'right', 'bottom', 'left'],
},
containerPadding: {
type: 'number',
default: '12',
},
offset: {
type: 'number',
default: '0',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tooltipUsageSnippet = `import { TooltipTrigger, Tooltip, Button } from '@backstage/ui';
<TooltipTrigger>
<Button>Button</Button>
<Tooltip>I am a tooltip</Tooltip>
</TooltipTrigger>`;
export const tooltipDefaultSnippet = `<TooltipTrigger>
<Button>Button</Button>
<Tooltip>I am a tooltip</Tooltip>
</TooltipTrigger>`;
@@ -0,0 +1,52 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { Default, ExampleUsage } from './stories';
import {
visuallyHiddenPropDefs,
visuallyHiddenUsageSnippet,
visuallyHiddenDefaultSnippet,
visuallyHiddenExampleUsageSnippet,
} from './visually-hidden.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { VisuallyHiddenDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="VisuallyHidden"
description="Visually hides content while keeping it accessible to screen readers."
/>
<Snippet
align="start"
py={4}
preview={<Default />}
code={visuallyHiddenDefaultSnippet}
/>
## Usage
<CodeBlock code={visuallyHiddenUsageSnippet} />
## API reference
<PropsTable data={visuallyHiddenPropDefs} />
## Examples
### Example Usage
Here's an example of providing screen reader context for a list of links in a footer.
<Snippet
align="start"
py={4}
preview={<ExampleUsage />}
code={visuallyHiddenExampleUsageSnippet}
open
/>
<Theming definition={VisuallyHiddenDefinition} />
<ChangelogComponent component="visually-hidden" />
@@ -0,0 +1,8 @@
'use client';
import * as stories from '@backstage/ui/src/components/VisuallyHidden/VisuallyHidden.stories';
const { Default: DefaultStory, ExampleUsage: ExampleUsageStory } = stories;
export const Default = () => <DefaultStory.Component />;
export const ExampleUsage = () => <ExampleUsageStory.Component />;
@@ -0,0 +1,47 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const visuallyHiddenPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['ReactNode'],
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const visuallyHiddenUsageSnippet = `import { VisuallyHidden } from '@backstage/ui';
<VisuallyHidden>
This content is visually hidden but accessible to screen readers
</VisuallyHidden>`;
export const visuallyHiddenDefaultSnippet = `<Flex direction="column" gap="4">
<Text as="p">
This text is followed by a paragraph that is visually hidden but
accessible to screen readers. Try using a screen reader to hear it, or
inspect the DOM to see it's there.
</Text>
<VisuallyHidden>
This content is visually hidden but accessible to screen readers
</VisuallyHidden>
</Flex>`;
export const visuallyHiddenExampleUsageSnippet = `<Flex direction="column" gap="4">
<VisuallyHidden>
<Text as="h2">Footer links</Text>
</VisuallyHidden>
<Text as="p">
<a href="#">About us</a>
</Text>
<Text as="p">
<a href="#">Jobs</a>
</Text>
<Text as="p">
<a href="#">Terms and Conditions</a>
</Text>
</Flex>`;