Merge pull request #32339 from backstage/bui-docs-migration

Bui docs migration
This commit is contained in:
Charles de Dreuille
2026-01-16 11:36:31 +00:00
committed by GitHub
161 changed files with 5729 additions and 4483 deletions
+1 -3
View File
@@ -26,7 +26,6 @@
"@mdx-js/react": "^3.1.0",
"@next/mdx": "15.5.7",
"@remixicon/react": "^4.6.0",
"@storybook/react": "^8.6.12",
"@uiw/codemirror-themes": "^4.23.7",
"@uiw/react-codemirror": "^4.23.7",
"clsx": "^2.1.1",
@@ -37,8 +36,7 @@
"prop-types": "^15.8.1",
"react": "19.1.1",
"react-dom": "19.1.1",
"shiki": "^3.13.0",
"storybook": "^8.6.15"
"shiki": "^3.13.0"
},
"devDependencies": {
"@octokit/rest": "^22.0.1",
@@ -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,130 @@
'use client';
import {
Accordion,
AccordionTrigger,
AccordionPanel,
AccordionGroup,
} from '../../../../../packages/ui/src/components/Accordion/Accordion';
import { Box } from '../../../../../packages/ui/src/components/Box/Box';
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
const Content = () => (
<Box>
<Text as="p">
It&apos;s the edge of the world and all of Western civilization
</Text>
<Text as="p">
The sun may rise in the East, at least it settled in a final location
</Text>
<Text as="p">
It&apos;s understood that Hollywood sells Californication
</Text>
</Box>
);
export const Default = () => {
return (
<Accordion>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
);
};
export const WithSubtitle = () => {
return (
<Accordion>
<AccordionTrigger
title="Advanced Settings"
subtitle="Configure additional options"
/>
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
);
};
export const CustomTrigger = () => {
return (
<Accordion>
<AccordionTrigger>
<Box>
<Text as="div" variant="body-large" weight="bold">
Custom Multi-line Trigger
</Text>
<Text as="div" variant="body-medium" color="secondary">
Click to expand additional details and configuration options
</Text>
</Box>
</AccordionTrigger>
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
);
};
export const DefaultExpanded = () => {
return (
<Accordion defaultExpanded>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
);
};
export const GroupSingleOpen = () => {
return (
<AccordionGroup>
<Accordion>
<AccordionTrigger title="First Panel" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
<Accordion>
<AccordionTrigger title="Second Panel" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
<Accordion>
<AccordionTrigger title="Third Panel" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
</AccordionGroup>
);
};
export const GroupMultipleOpen = () => {
return (
<AccordionGroup allowsMultiple>
<Accordion>
<AccordionTrigger title="First Panel" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
<Accordion>
<AccordionTrigger title="Second Panel" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
<Accordion>
<AccordionTrigger title="Third Panel" />
<AccordionPanel>
<Content />
</AccordionPanel>
</Accordion>
</AccordionGroup>
);
};
@@ -1,22 +1,32 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { AccordionSnippet } from '@/snippets/stories-snippets';
import {
accordionPropDefs,
accordionTriggerPropDefs,
accordionPanelPropDefs,
accordionGroupPropDefs,
} from './props-definition';
import {
accordionUsageSnippet,
accordionWithSubtitleSnippet,
accordionCustomTriggerSnippet,
accordionDefaultExpandedSnippet,
accordionGroupSingleOpenSnippet,
accordionGroupMultipleOpenSnippet,
} from './accordion.props';
defaultSnippet,
withSubtitleSnippet,
customTriggerSnippet,
defaultExpandedSnippet,
groupSingleOpenSnippet,
groupMultipleOpenSnippet,
} from './snippets';
import {
Default,
WithSubtitle,
CustomTrigger,
DefaultExpanded,
GroupSingleOpen,
GroupMultipleOpen,
} from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { AccordionDefinition } from '../utils/definitions';
import { AccordionDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -28,8 +38,8 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
align="center"
py={4}
height={240}
preview={<AccordionSnippet story="Default" />}
code={accordionUsageSnippet}
preview={<Default />}
code={defaultSnippet}
/>
## Usage
@@ -72,8 +82,8 @@ Here's a view when using both title and subtitle props.
align="center"
py={4}
height={240}
preview={<AccordionSnippet story="WithSubtitle" />}
code={accordionWithSubtitleSnippet}
preview={<WithSubtitle />}
code={withSubtitleSnippet}
/>
### Custom Trigger
@@ -84,8 +94,8 @@ Here's a view when providing custom multi-line content as children.
align="center"
py={4}
height={280}
preview={<AccordionSnippet story="CustomTrigger" />}
code={accordionCustomTriggerSnippet}
preview={<CustomTrigger />}
code={customTriggerSnippet}
/>
### Default Expanded
@@ -96,8 +106,8 @@ Here's a view when the panel is expanded by default.
align="center"
py={4}
height={280}
preview={<AccordionSnippet story="DefaultExpanded" />}
code={accordionDefaultExpandedSnippet}
preview={<DefaultExpanded />}
code={defaultExpandedSnippet}
/>
### Group Single Open
@@ -108,8 +118,8 @@ Here's a view when only one accordion can be open at a time.
align="center"
py={4}
height={280}
preview={<AccordionSnippet story="GroupSingleOpen" />}
code={accordionGroupSingleOpenSnippet}
preview={<GroupSingleOpen />}
code={groupSingleOpenSnippet}
/>
### Group Multiple Open
@@ -120,8 +130,8 @@ Here's a view when multiple accordions can be open simultaneously.
align="center"
py={4}
height={280}
preview={<AccordionSnippet story="GroupMultipleOpen" />}
code={accordionGroupMultipleOpenSnippet}
preview={<GroupMultipleOpen />}
code={groupMultipleOpenSnippet}
/>
<Theming definition={AccordionDefinition} />
@@ -0,0 +1,59 @@
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,
};
@@ -0,0 +1,68 @@
export const accordionUsageSnippet = `import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
<Accordion>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>Your content</AccordionPanel>
</Accordion>`;
export const defaultSnippet = `<Accordion>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const withSubtitleSnippet = `<Accordion>
<AccordionTrigger
title="Advanced Settings"
subtitle="Configure additional options"
/>
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const customTriggerSnippet = `<Accordion>
<AccordionTrigger>
<Box>
<Text as="div" variant="body-large" weight="bold">
Custom Multi-line Trigger
</Text>
<Text as="div" variant="body-medium" color="secondary">
Click to expand additional details and configuration options
</Text>
</Box>
</AccordionTrigger>
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const defaultExpandedSnippet = `<Accordion defaultExpanded>
<AccordionTrigger title="Toggle Panel" />
<AccordionPanel>
<Text>Your content here</Text>
</AccordionPanel>
</Accordion>`;
export const groupSingleOpenSnippet = `<AccordionGroup>
<Accordion>
<AccordionTrigger title="First Panel" />
<AccordionPanel>Content 1</AccordionPanel>
</Accordion>
<Accordion>
<AccordionTrigger title="Second Panel" />
<AccordionPanel>Content 2</AccordionPanel>
</Accordion>
</AccordionGroup>`;
export const groupMultipleOpenSnippet = `<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,100 @@
'use client';
import { Avatar } from '../../../../../packages/ui/src/components/Avatar/Avatar';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
export const Default = () => {
return (
<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille"
/>
);
};
export const Fallback = () => {
return (
<Avatar
src="https://avatars.githubusercontent.com/u/15406AAAAAAAAA"
name="Charles de Dreuille"
/>
);
};
export const Sizes = () => {
return (
<Flex direction="column" gap="6">
<Flex>
<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>
<Flex>
<Avatar name="Charles de Dreuille" size="x-small" src="" />
<Avatar name="Charles de Dreuille" size="small" src="" />
<Avatar name="Charles de Dreuille" size="medium" src="" />
<Avatar name="Charles de Dreuille" size="large" src="" />
<Avatar name="Charles de Dreuille" size="x-large" src="" />
</Flex>
</Flex>
);
};
export const Purpose = () => {
return (
<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 &quot;Charles de
Dreuille&quot; 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 avatar appears with adjacent text. Hidden from screen
readers:
</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>
);
};
@@ -1,17 +1,18 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { AvatarSnippet } from '@/snippets/stories-snippets';
import { avatarPropDefs } from './props-definition';
import {
avatarPropDefs,
snippetUsage,
snippetSizes,
snippetFallback,
snippetPurpose,
} from './avatar.props';
avatarUsageSnippet,
defaultSnippet,
sizesSnippet,
fallbackSnippet,
purposeSnippet,
} from './snippets';
import { Default, Sizes, Fallback, Purpose } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { AvatarDefinition } from '../utils/definitions';
import { AvatarDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -19,16 +20,11 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
description="An avatar component with a fallback for initials."
/>
<Snippet
align="center"
py={4}
preview={<AvatarSnippet story="Default" />}
code={`<Avatar src="https://avatars.githubusercontent.com/u/1540635?v=4" name="Charles de Dreuille" />`}
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
## Usage
<CodeBlock code={snippetUsage} />
<CodeBlock code={avatarUsageSnippet} />
## API reference
@@ -40,13 +36,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
Avatar sizes can be set using the `size` prop.
<Snippet
align="center"
py={4}
open
preview={<AvatarSnippet story="Sizes" />}
code={snippetSizes}
/>
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
### Fallback
@@ -56,20 +46,15 @@ If the image is not available, the avatar will show the initials of the name.
align="center"
py={4}
open
preview={<AvatarSnippet story="Fallback" />}
code={snippetFallback}
preview={<Fallback />}
code={fallbackSnippet}
/>
### The `purpose` prop
Control how the avatar is announced to screen readers using the `purpose` prop.
<Snippet
align="center"
py={4}
preview={<AvatarSnippet story="Purpose" />}
code={snippetPurpose}
/>
<Snippet align="center" py={4} preview={<Purpose />} code={purposeSnippet} />
<Theming definition={AvatarDefinition} />
@@ -0,0 +1,28 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const avatarPropDefs: Record<string, PropDef> = {
src: {
type: 'string',
},
name: {
type: 'string',
required: true,
},
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,
};
@@ -0,0 +1,52 @@
export const avatarUsageSnippet = `import { Avatar } from '@backstage/ui';
<Avatar />`;
export const defaultSnippet = `<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
name="Charles de Dreuille"
/>`;
export const fallbackSnippet = `<Avatar
src="https://avatars.githubusercontent.com/u/15406AAAAAAAAA"
name="Charles de Dreuille"
/>`;
export const sizesSnippet = `<Flex direction="column" gap="6">
<Flex>
<Avatar src="..." name="Charles de Dreuille" size="x-small" />
<Avatar src="..." name="Charles de Dreuille" size="small" />
<Avatar src="..." name="Charles de Dreuille" size="medium" />
<Avatar src="..." name="Charles de Dreuille" size="large" />
<Avatar src="..." name="Charles de Dreuille" size="x-large" />
</Flex>
<Flex>
<Avatar name="Charles de Dreuille" size="x-small" src="" />
<Avatar name="Charles de Dreuille" size="small" src="" />
<Avatar name="Charles de Dreuille" size="medium" src="" />
<Avatar name="Charles de Dreuille" size="large" src="" />
<Avatar name="Charles de Dreuille" size="x-large" src="" />
</Flex>
</Flex>`;
export const purposeSnippet = `<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="..." 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 avatar appears with adjacent text. Hidden from screen readers:
</Text>
<Flex gap="2" align="center">
<Avatar src="..." name="Charles de Dreuille" purpose="decoration" />
<Text>Charles de Dreuille</Text>
</Flex>
</Flex>
</Flex>`;
@@ -0,0 +1,34 @@
'use client';
import { Box } from '../../../../../packages/ui/src/components/Box/Box';
const diagonalStripePattern = (() => {
const svg = `
<svg width="6" height="6" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
<g fill="#2563eb" fill-opacity="0.6" fill-rule="evenodd">
<path d="M5 0h1L0 6V5zM6 5v1H5z"/>
</g>
</svg>
`.trim();
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
})();
export const Default = () => {
return (
<Box
width="64px"
height="64px"
style={{
background: '#eaf2fd',
borderRadius: '4px',
border: '1px solid #2563eb',
backgroundImage: `url("${diagonalStripePattern}")`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
color: '#2563eb',
}}
/>
);
};
@@ -1,18 +1,18 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { BoxSnippet } from '@/snippets/stories-snippets';
import { boxPropDefs } from './props-definition';
import {
boxPropDefs,
snippetUsage,
boxPreviewSnippet,
defaultSnippet,
boxSimpleSnippet,
boxResponsiveSnippet,
} from './box.props';
} from './snippets';
import { Default } from './components';
import { spacingPropDefs } from '@/utils/propDefs';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { BoxDefinition } from '../utils/definitions';
import { BoxDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -21,12 +21,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
description="Box is the lowest-level component in Backstage UI. It provides a consistent API for styling and layout."
/>
<Snippet
py={4}
preview={<BoxSnippet story="Default" />}
code={boxPreviewSnippet}
align="center"
/>
<Snippet py={4} preview={<Default />} code={defaultSnippet} align="center" />
## Usage
@@ -22,19 +22,3 @@ export const boxPropDefs: Record<string, PropDef> = {
...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>`;
@@ -0,0 +1,21 @@
export const snippetUsage = `import { Box } from '@backstage/ui';
<Box />`;
export const defaultSnippet = `<Box
width="64px"
height="64px"
style={{
background: '#eaf2fd',
borderRadius: '4px',
border: '1px solid #2563eb',
}}
/>`;
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>`;
@@ -0,0 +1,68 @@
'use client';
import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { RiCloudLine } from '@remixicon/react';
export const Variants = () => {
return (
<Flex align="center" gap="2">
<ButtonIcon icon={<RiCloudLine />} variant="primary" aria-label="Cloud" />
<ButtonIcon
icon={<RiCloudLine />}
variant="secondary"
aria-label="Cloud"
/>
<ButtonIcon
icon={<RiCloudLine />}
variant="tertiary"
aria-label="Cloud"
/>
</Flex>
);
};
export const Sizes = () => {
return (
<Flex align="center" gap="2">
<ButtonIcon icon={<RiCloudLine />} size="small" aria-label="Cloud" />
<ButtonIcon icon={<RiCloudLine />} size="medium" aria-label="Cloud" />
</Flex>
);
};
export const Disabled = () => {
return (
<Flex direction="row" gap="2">
<ButtonIcon
isDisabled
icon={<RiCloudLine />}
variant="primary"
aria-label="Cloud"
/>
<ButtonIcon
isDisabled
icon={<RiCloudLine />}
variant="secondary"
aria-label="Cloud"
/>
<ButtonIcon
isDisabled
icon={<RiCloudLine />}
variant="tertiary"
aria-label="Cloud"
/>
</Flex>
);
};
export const Loading = () => {
return (
<ButtonIcon
icon={<RiCloudLine />}
variant="primary"
loading
aria-label="Cloud"
/>
);
};
@@ -1,21 +1,18 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ButtonIconSnippet } from '@/snippets/stories-snippets';
import { buttonIconPropDefs } from './props-definition';
import {
buttonIconPropDefs,
buttonIconUsageSnippet,
buttonIconDefaultSnippet,
buttonIconVariantsSnippet,
buttonIconSizesSnippet,
buttonIconDisabledSnippet,
buttonIconLoadingSnippet,
buttonIconResponsiveSnippet,
buttonIconAsLinkSnippet,
} from './button-icon.props';
variantsSnippet,
sizesSnippet,
disabledSnippet,
loadingSnippet,
} from './snippets';
import { Variants, Sizes, Disabled, Loading } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ButtonIconDefinition } from '../utils/definitions';
import { ButtonIconDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -23,12 +20,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
description="A button component with a single icon that can be used to trigger actions."
/>
<Snippet
align="center"
py={4}
preview={<ButtonIconSnippet story="Variants" />}
code={buttonIconDefaultSnippet}
/>
<Snippet align="center" py={4} preview={<Variants />} code={variantsSnippet} />
## Usage
@@ -48,21 +40,15 @@ Here's a view when buttons have different variants.
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Variants" />}
code={buttonIconVariantsSnippet}
preview={<Variants />}
code={variantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Sizes" />}
code={buttonIconSizesSnippet}
/>
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
### Disabled
@@ -72,8 +58,8 @@ Here's a view when buttons are disabled.
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Disabled" />}
code={buttonIconDisabledSnippet}
preview={<Disabled />}
code={disabledSnippet}
/>
### Loading
@@ -84,16 +70,10 @@ Here's a view when buttons are in a loading state.
align="center"
py={4}
open
preview={<ButtonIconSnippet story="Loading" />}
code={buttonIconLoadingSnippet}
preview={<Loading />}
code={loadingSnippet}
/>
### Responsive
Here's a view when buttons are responsive.
<CodeBlock code={buttonIconResponsiveSnippet} />
<Theming definition={ButtonIconDefinition} />
<ChangelogComponent component="button-icon" />
@@ -0,0 +1,31 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const buttonIconPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary', 'tertiary'],
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,
};
@@ -0,0 +1,22 @@
export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/ui';
<ButtonIcon />`;
export const variantsSnippet = `<Flex align="center" gap="2">
<ButtonIcon icon={<RiCloudLine />} variant="primary" aria-label="Cloud" />
<ButtonIcon icon={<RiCloudLine />} variant="secondary" aria-label="Cloud" />
<ButtonIcon icon={<RiCloudLine />} variant="tertiary" aria-label="Cloud" />
</Flex>`;
export const sizesSnippet = `<Flex align="center" gap="2">
<ButtonIcon icon={<RiCloudLine />} size="small" aria-label="Cloud" />
<ButtonIcon icon={<RiCloudLine />} size="medium" aria-label="Cloud" />
</Flex>`;
export const disabledSnippet = `<Flex direction="row" gap="2">
<ButtonIcon isDisabled icon={<RiCloudLine />} variant="primary" aria-label="Cloud" />
<ButtonIcon isDisabled icon={<RiCloudLine />} variant="secondary" aria-label="Cloud" />
<ButtonIcon isDisabled icon={<RiCloudLine />} variant="tertiary" aria-label="Cloud" />
</Flex>`;
export const loadingSnippet = `<ButtonIcon icon={<RiCloudLine />} variant="primary" loading aria-label="Cloud" />`;
@@ -0,0 +1,122 @@
'use client';
import { ButtonLink } from '../../../../../packages/ui/src/components/ButtonLink/ButtonLink';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { MemoryRouter } from 'react-router-dom';
import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react';
export const Variants = () => {
return (
<MemoryRouter>
<Flex align="center">
<ButtonLink
iconStart={<RiCloudLine />}
variant="primary"
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
<ButtonLink
iconStart={<RiCloudLine />}
variant="secondary"
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
<ButtonLink
iconStart={<RiCloudLine />}
variant="tertiary"
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
</Flex>
</MemoryRouter>
);
};
export const Sizes = () => {
return (
<MemoryRouter>
<Flex align="center">
<ButtonLink size="small" href="https://ui.backstage.io" target="_blank">
Small
</ButtonLink>
<ButtonLink
size="medium"
href="https://ui.backstage.io"
target="_blank"
>
Medium
</ButtonLink>
</Flex>
</MemoryRouter>
);
};
export const WithIcons = () => {
return (
<MemoryRouter>
<Flex align="center">
<ButtonLink
iconStart={<RiCloudLine />}
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
<ButtonLink
iconEnd={<RiArrowRightSLine />}
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
<ButtonLink
iconStart={<RiCloudLine />}
iconEnd={<RiArrowRightSLine />}
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
</Flex>
</MemoryRouter>
);
};
export const Disabled = () => {
return (
<MemoryRouter>
<Flex gap="4">
<ButtonLink
variant="primary"
isDisabled
href="https://ui.backstage.io"
target="_blank"
>
Primary
</ButtonLink>
<ButtonLink
variant="secondary"
isDisabled
href="https://ui.backstage.io"
target="_blank"
>
Secondary
</ButtonLink>
<ButtonLink
variant="tertiary"
isDisabled
href="https://ui.backstage.io"
target="_blank"
>
Tertiary
</ButtonLink>
</Flex>
</MemoryRouter>
);
};
@@ -0,0 +1,79 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { buttonLinkPropDefs } from './props-definition';
import {
buttonLinkUsageSnippet,
variantsSnippet,
sizesSnippet,
withIconsSnippet,
disabledSnippet,
} from './snippets';
import { Variants, Sizes, WithIcons, Disabled } from './components';
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={variantsSnippet} />
## Usage
<CodeBlock code={buttonLinkUsageSnippet} />
## 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={variantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
### With Icons
Here's a view when buttons have icons.
<Snippet
align="center"
py={4}
open
preview={<WithIcons />}
code={withIconsSnippet}
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={disabledSnippet}
/>
<Theming definition={ButtonLinkDefinition} />
<ChangelogComponent component="button-link" />
@@ -0,0 +1,32 @@
import {
classNamePropDefs,
stylePropDefs,
childrenPropDefs,
type PropDef,
} from '@/utils/propDefs';
export const buttonLinkPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary', 'tertiary'],
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', required: true },
target: {
type: 'enum',
values: ['_self', '_blank', '_parent', '_top'],
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,68 @@
export const buttonLinkUsageSnippet = `import { ButtonLink } from '@backstage/ui';
<ButtonLink href="https://ui.backstage.io">Button</ButtonLink>`;
export const variantsSnippet = `<Flex align="center">
<ButtonLink
iconStart={<RiCloudLine />}
variant="primary"
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
<ButtonLink
iconStart={<RiCloudLine />}
variant="secondary"
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
<ButtonLink
iconStart={<RiCloudLine />}
variant="tertiary"
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
</Flex>`;
export const sizesSnippet = `<Flex align="center">
<ButtonLink size="small" href="https://ui.backstage.io" target="_blank">
Small
</ButtonLink>
<ButtonLink size="medium" href="https://ui.backstage.io" target="_blank">
Medium
</ButtonLink>
</Flex>`;
export const withIconsSnippet = `<Flex align="center">
<ButtonLink iconStart={<RiCloudLine />} href="https://ui.backstage.io" target="_blank">
Button
</ButtonLink>
<ButtonLink iconEnd={<RiArrowRightSLine />} href="https://ui.backstage.io" target="_blank">
Button
</ButtonLink>
<ButtonLink
iconStart={<RiCloudLine />}
iconEnd={<RiArrowRightSLine />}
href="https://ui.backstage.io"
target="_blank"
>
Button
</ButtonLink>
</Flex>`;
export const disabledSnippet = `<Flex gap="4">
<ButtonLink variant="primary" isDisabled href="https://ui.backstage.io" target="_blank">
Primary
</ButtonLink>
<ButtonLink variant="secondary" isDisabled href="https://ui.backstage.io" target="_blank">
Secondary
</ButtonLink>
<ButtonLink variant="tertiary" isDisabled href="https://ui.backstage.io" target="_blank">
Tertiary
</ButtonLink>
</Flex>`;
@@ -0,0 +1,78 @@
'use client';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
import { ButtonLink } from '../../../../../packages/ui/src/components/ButtonLink/ButtonLink';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react';
import { MemoryRouter } from 'react-router-dom';
export const Variants = () => {
return (
<Flex>
<Button variant="primary" iconStart={<RiCloudLine />}>
Button
</Button>
<Button variant="secondary" iconStart={<RiCloudLine />}>
Button
</Button>
<Button variant="tertiary" iconStart={<RiCloudLine />}>
Button
</Button>
</Flex>
);
};
export const Sizes = () => {
return (
<Flex align="center">
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
</Flex>
);
};
export const WithIcons = () => {
return (
<Flex align="center">
<Button iconStart={<RiCloudLine />}>Button</Button>
<Button iconEnd={<RiArrowRightSLine />}>Button</Button>
<Button iconStart={<RiCloudLine />} iconEnd={<RiArrowRightSLine />}>
Button
</Button>
</Flex>
);
};
export const Disabled = () => {
return (
<Flex gap="4">
<Button variant="primary" isDisabled>
Primary
</Button>
<Button variant="secondary" isDisabled>
Secondary
</Button>
<Button variant="tertiary" isDisabled>
Tertiary
</Button>
</Flex>
);
};
export const Loading = () => {
return (
<Button variant="primary" loading={true}>
Load more items
</Button>
);
};
export const AsLink = () => {
return (
<MemoryRouter>
<ButtonLink href="https://ui.backstage.io" target="_blank">
Button
</ButtonLink>
</MemoryRouter>
);
};
@@ -1,34 +1,36 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ButtonSnippet, ButtonLinkSnippet } from '@/snippets/stories-snippets';
import { buttonPropDefs } from './props-definition';
import {
buttonPropDefs,
variantsSnippet,
sizesSnippet,
withIconsSnippet,
disabledSnippet,
loadingSnippet,
asLinkSnippet,
buttonSnippetUsage,
buttonVariantsSnippet,
buttonSizesSnippet,
buttonIconsSnippet,
buttonDisabledSnippet,
buttonLoadingSnippet,
buttonResponsiveSnippet,
buttonAsLinkSnippet,
} from './button.props';
} from './snippets';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ButtonDefinition } from '../utils/definitions';
import { ButtonDefinition } from '../../../utils/definitions';
import {
Variants,
Sizes,
WithIcons,
Disabled,
Loading,
AsLink,
} from './components';
<PageTitle
title="Button"
description="A button component that can be used to trigger actions."
/>
<Snippet
align="center"
py={4}
preview={<ButtonSnippet story="Variants" />}
code={buttonVariantsSnippet}
/>
<Snippet align="center" py={4} preview={<Variants />} code={variantsSnippet} />
## Usage
@@ -48,21 +50,15 @@ Here's a view when buttons have different variants.
align="center"
py={4}
open
preview={<ButtonSnippet story="Variants" />}
code={buttonVariantsSnippet}
preview={<Variants />}
code={variantsSnippet}
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet
align="center"
py={4}
open
preview={<ButtonSnippet story="Sizes" />}
code={buttonSizesSnippet}
/>
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
### With Icons
@@ -72,8 +68,8 @@ Here's a view when buttons have icons.
align="center"
py={4}
open
preview={<ButtonSnippet story="WithIcons" />}
code={buttonIconsSnippet}
preview={<WithIcons />}
code={withIconsSnippet}
/>
### Disabled
@@ -84,8 +80,8 @@ Here's a view when buttons are disabled.
align="center"
py={4}
open
preview={<ButtonSnippet story="Disabled" />}
code={buttonDisabledSnippet}
preview={<Disabled />}
code={disabledSnippet}
/>
### Loading
@@ -96,8 +92,8 @@ Here's a view when buttons are in a loading state.
align="center"
py={4}
open
preview={<ButtonSnippet story="Loading" />}
code={buttonLoadingSnippet}
preview={<Loading />}
code={loadingSnippet}
/>
### Responsive
@@ -110,13 +106,7 @@ Here's a view when buttons are responsive.
If you want to use a button as a link, please use the `ButtonLink` component.
<Snippet
align="center"
py={4}
open
preview={<ButtonLinkSnippet story="Variants" />}
code={buttonAsLinkSnippet}
/>
<Snippet align="center" py={4} open preview={<AsLink />} code={asLinkSnippet} />
<Theming definition={ButtonDefinition} />
@@ -0,0 +1,30 @@
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,
};
@@ -0,0 +1,54 @@
export const buttonSnippetUsage = `import { Button } from '@backstage/ui';
<Button />`;
export const buttonResponsiveSnippet = `<Button variant={{ initial: 'primary', lg: 'secondary' }}>
Responsive Button
</Button>`;
export const variantsSnippet = `<Flex>
<Button variant="primary" iconStart={<RiCloudLine />}>
Button
</Button>
<Button variant="secondary" iconStart={<RiCloudLine />}>
Button
</Button>
<Button variant="tertiary" iconStart={<RiCloudLine />}>
Button
</Button>
</Flex>`;
export const sizesSnippet = `<Flex align="center">
<Button size="small">Small</Button>
<Button size="medium">Medium</Button>
</Flex>`;
export const withIconsSnippet = `<Flex align="center">
<Button iconStart={<RiCloudLine />}>Button</Button>
<Button iconEnd={<RiArrowRightSLine />}>Button</Button>
<Button iconStart={<RiCloudLine />} iconEnd={<RiArrowRightSLine />}>
Button
</Button>
</Flex>`;
export const disabledSnippet = `<Flex gap="4">
<Button variant="primary" isDisabled>
Primary
</Button>
<Button variant="secondary" isDisabled>
Secondary
</Button>
<Button variant="tertiary" isDisabled>
Tertiary
</Button>
</Flex>`;
export const loadingSnippet = `<Button variant="primary" loading={true}>
Load more items
</Button>`;
export const asLinkSnippet = `<MemoryRouter>
<ButtonLink href="https://ui.backstage.io" target="_blank">
Button
</ButtonLink>
</MemoryRouter>`;
@@ -0,0 +1,66 @@
'use client';
import {
Card,
CardHeader,
CardBody,
CardFooter,
} from '../../../../../packages/ui/src/components/Card/Card';
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
export const Default = () => {
return (
<Card style={{ width: '300px', height: '200px' }}>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>
);
};
export const CustomSize = () => {
return (
<Card
style={{
width: '300px',
height: '200px',
}}
>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>
);
};
export const WithLongBody = () => {
return (
<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&apos;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>
);
};
@@ -1,20 +1,22 @@
import { PropsTable } from '@/components/PropsTable';
import { CardSnippet } from '@/snippets/stories-snippets';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
cardPropDefs,
cardUsageSnippet,
cardDefaultSnippet,
cardHeaderPropDefs,
cardBodyPropDefs,
cardFooterPropDefs,
cardLongBodySnippet,
cardListRowSnippet,
} from './card.props';
} from './props-definition';
import {
cardUsageSnippet,
defaultSnippet,
customSizeSnippet,
withLongBodySnippet,
} from './snippets';
import { Default, CustomSize, WithLongBody } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { CardDefinition } from '../utils/definitions';
import { CardDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -22,12 +24,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
description="A card component that can be used to display content in a box."
/>
<Snippet
align="center"
py={4}
preview={<CardSnippet story="CustomSize" />}
code={cardDefaultSnippet}
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
## Usage
@@ -61,6 +58,18 @@ To display a footer in a card, use the `CardFooter` component. This will be fixe
## Examples
### Custom size
Here's a view when card has a custom size.
<Snippet
align="center"
py={4}
preview={<CustomSize />}
code={customSizeSnippet}
open
/>
### With long body
Here's a view when card has a long body.
@@ -68,20 +77,8 @@ Here's a view when card has a long body.
<Snippet
align="center"
py={4}
preview={<CardSnippet story="WithLongBody" />}
code={cardLongBodySnippet}
open
/>
### With list
Here's a view when card has a list.
<Snippet
align="center"
py={4}
preview={<CardSnippet story="WithListRow" />}
code={cardListRowSnippet}
preview={<WithLongBody />}
code={withLongBodySnippet}
open
/>
@@ -0,0 +1,30 @@
import {
classNamePropDefs,
stylePropDefs,
childrenPropDefs,
type PropDef,
} from '@/utils/propDefs';
export const cardPropDefs: Record<string, PropDef> = {
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const cardHeaderPropDefs: Record<string, PropDef> = {
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const cardBodyPropDefs: Record<string, PropDef> = {
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const cardFooterPropDefs: Record<string, PropDef> = {
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,41 @@
export const cardUsageSnippet = `import { Card, CardHeader, CardBody, CardFooter } from '@backstage/ui';
<Card>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>`;
export const defaultSnippet = `<Card>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>`;
export const customSizeSnippet = `<Card style={{ width: '300px', height: '200px' }}>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>`;
export const withLongBodySnippet = `<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.
</Text>
<Text>
Here's a second paragraph that adds more content to our card body.
</Text>
<Text>
This third paragraph continues to add more text to ensure we have a
proper demonstration of a card with significant content.
</Text>
</CardBody>
<CardFooter>
<Text>Footer</Text>
</CardFooter>
</Card>`;
@@ -0,0 +1,21 @@
'use client';
import { Checkbox } from '../../../../../packages/ui/src/components/Checkbox/Checkbox';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
export const Default = () => {
return <Checkbox>Accept terms and conditions</Checkbox>;
};
export const AllVariants = () => {
return (
<Flex direction="column" gap="2">
<Checkbox>Unchecked</Checkbox>
<Checkbox isSelected>Checked</Checkbox>
<Checkbox isDisabled>Disabled</Checkbox>
<Checkbox isSelected isDisabled>
Checked & Disabled
</Checkbox>
</Flex>
);
};
@@ -1,16 +1,16 @@
import { PropsTable } from '@/components/PropsTable';
import { CheckboxSnippet } from '@/snippets/stories-snippets';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { checkboxPropDefs } from './props-definition';
import {
checkboxPropDefs,
checkboxUsageSnippet,
checkboxDefaultSnippet,
checkboxVariantsSnippet,
} from './checkbox.props';
defaultSnippet,
allVariantsSnippet,
} from './snippets';
import { Default, AllVariants } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { CheckboxDefinition } from '../utils/definitions';
import { CheckboxDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -18,12 +18,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
description="A checkbox component that can be used to trigger actions."
/>
<Snippet
align="center"
py={4}
preview={<CheckboxSnippet story="Default" />}
code={checkboxDefaultSnippet}
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
## Usage
@@ -43,8 +38,8 @@ Here's a view when checkboxes have different variants.
align="center"
py={4}
open
preview={<CheckboxSnippet story="AllVariants" />}
code={checkboxVariantsSnippet}
preview={<AllVariants />}
code={allVariantsSnippet}
/>
<Theming definition={CheckboxDefinition} />
@@ -46,16 +46,3 @@ export const checkboxPropDefs: Record<string, PropDef> = {
...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,12 @@
export const checkboxUsageSnippet = `import { Checkbox } from '@backstage/ui';
<Checkbox>Accept terms</Checkbox>`;
export const defaultSnippet = `<Checkbox>Accept terms and conditions</Checkbox>`;
export const allVariantsSnippet = `<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,24 @@
'use client';
import { Box } from '../../../../../packages/ui/src/components/Box/Box';
const DecorativeBox = () => (
<Box
style={{
height: '64px',
background: '#eaf2fd',
borderRadius: '4px',
border: '1px solid #2563eb',
backgroundImage:
'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")',
}}
/>
);
export const Preview = () => {
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<DecorativeBox />
</div>
);
};
@@ -1,17 +1,17 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { ContainerSnippet } from '@/snippets/stories-snippets';
import { containerPropDefs } from './props-definition';
import {
containerPropDefs,
containerUsageSnippet,
containerDefaultSnippet,
previewSnippet,
containerSimpleSnippet,
containerResponsiveSnippet,
} from './container.props';
} from './snippets';
import { Preview } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ContainerDefinition } from '../utils/definitions';
import { ContainerDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -20,11 +20,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
description="The container component let you use our default max-width and center the content on the page."
/>
<Snippet
py={4}
preview={<ContainerSnippet story="Preview" />}
code={containerDefaultSnippet}
/>
<Snippet py={4} preview={<Preview />} code={previewSnippet} />
## Usage
@@ -0,0 +1,12 @@
import {
classNamePropDefs,
stylePropDefs,
gapPropDefs,
type PropDef,
} from '@/utils/propDefs';
export const containerPropDefs: Record<string, PropDef> = {
...gapPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -1,23 +1,10 @@
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>
export const previewSnippet = `<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<DecorativeBox />
</Container>`;
</div>`;
export const containerSimpleSnippet = `<Container>
<Box>Hello World</Box>
@@ -0,0 +1,104 @@
'use client';
import {
Dialog,
DialogTrigger,
DialogHeader,
DialogBody,
DialogFooter,
} from '../../../../../packages/ui/src/components/Dialog/Dialog';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
import { TextField } from '../../../../../packages/ui/src/components/TextField/TextField';
import { Select } from '../../../../../packages/ui/src/components/Select/Select';
export const Default = () => (
<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 PreviewFixedWidthAndHeight = () => (
<DialogTrigger>
<Button variant="secondary">Scrollable Dialog</Button>
<Dialog width={600} height={400}>
<DialogHeader>Long Content Dialog</DialogHeader>
<DialogBody>
<Flex direction="column" gap="3">
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</Text>
<Text>
Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit
anim id est laborum.
</Text>
<Text>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
quae ab illo inventore veritatis et quasi architecto beatae vitae
dicta sunt explicabo.
</Text>
</Flex>
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">
Cancel
</Button>
<Button variant="primary" slot="close">
Accept
</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>
);
export const PreviewWithForm = () => (
<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"
options={[
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' },
{ value: 'viewer', label: 'Viewer' },
]}
/>
</Flex>
</DialogBody>
<DialogFooter>
<Button variant="secondary" slot="close">
Cancel
</Button>
<Button variant="primary" slot="close">
Create User
</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>
);
@@ -1,7 +1,11 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { DialogSnippet } from '@/snippets/stories-snippets';
import {
Default,
PreviewFixedWidthAndHeight,
PreviewWithForm,
} from './components';
import {
dialogPropDefs,
dialogTriggerPropDefs,
@@ -9,16 +13,18 @@ import {
dialogBodyPropDefs,
dialogFooterPropDefs,
dialogClosePropDefs,
} from './props-definition';
import {
dialogUsageSnippet,
dialogDefaultSnippet,
dialogFixedWidthAndHeightSnippet,
dialogWithFormSnippet,
dialogWithNoTriggerSnippet,
dialogCloseSnippet,
} from './dialog.props';
} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { DialogDefinition } from '../utils/definitions';
import { DialogDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -29,7 +35,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<Snippet
align="center"
py={4}
preview={<DialogSnippet story="Default" />}
preview={<Default />}
code={dialogDefaultSnippet}
/>
@@ -82,7 +88,7 @@ Dialog with a fixed height body that scrolls when content overflows.
<Snippet
align="center"
py={4}
preview={<DialogSnippet story="PreviewFixedWidthAndHeight" />}
preview={<PreviewFixedWidthAndHeight />}
code={dialogFixedWidthAndHeightSnippet}
/>
@@ -93,7 +99,7 @@ Dialog containing form elements for user input.
<Snippet
align="center"
py={4}
preview={<DialogSnippet story="PreviewWithForm" />}
preview={<PreviewWithForm />}
code={dialogWithFormSnippet}
/>
@@ -0,0 +1,88 @@
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,
};
@@ -1,92 +1,3 @@
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,
@@ -122,9 +33,9 @@ export const dialogDefaultSnippet = `<DialogTrigger>
export const dialogFixedWidthAndHeightSnippet = `<DialogTrigger>
<Button variant="secondary">Scrollable Dialog</Button>
<Dialog>
<Dialog width={600} height={400}>
<DialogHeader>Long Content Dialog</DialogHeader>
<DialogBody width={600} height={400}>
<DialogBody>
...
</DialogBody>
<DialogFooter>
@@ -0,0 +1,38 @@
'use client';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { Box } from '../../../../../packages/ui/src/components/Box/Box';
export const Default = () => {
return (
<Flex gap="4">
<Box
style={{
width: '64px',
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
}}
/>
<Box
style={{
width: '64px',
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
}}
/>
<Box
style={{
width: '64px',
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
}}
/>
</Flex>
);
};
@@ -1,20 +1,19 @@
import { PropsTable } from '@/components/PropsTable';
import { CodeBlock } from '@/components/CodeBlock';
import { Snippet } from '@/components/Snippet';
import { FlexSnippet } from '@/snippets/stories-snippets';
import { flexPropDefs } from './props-definition';
import {
flexPropDefs,
flexUsageSnippet,
flexDefaultSnippet,
flexFAQ1Snippet,
defaultSnippet,
flexSimpleSnippet,
flexResponsiveSnippet,
flexAlignSnippet,
} from './flex.props';
} from './snippets';
import { Default } from './components';
import { spacingPropDefs } from '@/utils/propDefs';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { FlexDefinition } from '../utils/definitions';
import { FlexDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -23,12 +22,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
description="A responsive flex container component for vertical stacking with customizable gaps."
/>
<Snippet
py={4}
align="center"
preview={<FlexSnippet story="Default" />}
code={flexDefaultSnippet}
/>
<Snippet py={4} align="center" preview={<Default />} code={defaultSnippet} />
## Usage
@@ -0,0 +1,27 @@
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,
};
@@ -0,0 +1,27 @@
export const flexUsageSnippet = `import { Flex } from '@backstage/ui';
<Flex />`;
export const defaultSnippet = `<Flex gap="4">
<Box style={{ width: '64px', height: '64px' }} />
<Box style={{ width: '64px', height: '64px' }} />
<Box style={{ width: '64px', height: '64px' }} />
</Flex>`;
export const flexSimpleSnippet = `<Flex gap="4">
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Flex>`;
export const flexResponsiveSnippet = `<Flex gap={{ initial: '2', md: '4' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Flex>`;
export const flexAlignSnippet = `<Flex align={{ initial: 'start', md: 'center' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Flex>`;
@@ -0,0 +1,59 @@
'use client';
import { Grid } from '../../../../../packages/ui/src/components/Grid/Grid';
import { Box } from '../../../../../packages/ui/src/components/Box/Box';
export const Default = () => {
return (
<Grid.Root columns="3" gap="4">
<Box
style={{
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
}}
/>
<Box
style={{
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
}}
/>
<Box
style={{
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
}}
/>
<Box
style={{
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
}}
/>
<Box
style={{
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
}}
/>
<Box
style={{
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
}}
/>
</Grid.Root>
);
};
+41
View File
@@ -0,0 +1,41 @@
import { CodeBlock } from '@/components/CodeBlock';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { gridPropDefs } from './props-definition';
import { gridUsageSnippet, defaultSnippet } from './snippets';
import { Default } from './components';
import { spacingPropDefs } from '@/utils/propDefs';
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={defaultSnippet} />
## 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} />
<Theming definition={GridDefinition} />
<ChangelogComponent component="grid" />
@@ -0,0 +1,22 @@
import {
childrenPropDefs,
classNamePropDefs,
gapPropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const gridPropDefs: Record<string, PropDef> = {
columns: {
type: 'string',
responsive: true,
},
rows: {
type: 'string',
responsive: true,
},
...gapPropDefs,
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,12 @@
export const gridUsageSnippet = `import { Grid } from '@backstage/ui';
<Grid />`;
export const defaultSnippet = `<Grid.Root columns="3" gap="4">
<Box style={{ height: '64px' }} />
<Box style={{ height: '64px' }} />
<Box style={{ height: '64px' }} />
<Box style={{ height: '64px' }} />
<Box style={{ height: '64px' }} />
<Box style={{ height: '64px' }} />
</Grid.Root>`;
@@ -0,0 +1,107 @@
'use client';
import { HeaderPage } from '../../../../../packages/ui/src/components/HeaderPage/HeaderPage';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
import {
MenuTrigger,
Menu,
MenuItem,
} from '../../../../../packages/ui/src/components/Menu/Menu';
import { MemoryRouter } from 'react-router-dom';
import { RiMore2Line } from '@remixicon/react';
const 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' },
];
const menuItems = [
{ label: 'Settings', value: 'settings', href: '/settings' },
{
label: 'Invite new members',
value: 'invite-new-members',
href: '/invite-new-members',
},
{
label: 'Logout',
value: 'logout',
onClick: () => {
alert('logout');
},
},
];
const breadcrumbs = [
{ label: 'Home', href: '/' },
{ label: 'Long Breadcrumb Name', href: '/long-breadcrumb' },
{ label: 'Another Long Breadcrumb', href: '/another-long-breadcrumb' },
{
label: 'Yet Another Long Breadcrumb',
href: '/yet-another-long-breadcrumb',
},
];
export const WithEverything = () => (
<MemoryRouter>
<HeaderPage
title="Page Title"
tabs={tabs}
breadcrumbs={breadcrumbs}
customActions={
<>
<Button variant="secondary">Secondary</Button>
<Button variant="primary">Primary</Button>
</>
}
/>
</MemoryRouter>
);
export const WithLongBreadcrumbs = () => (
<MemoryRouter>
<HeaderPage title="Page Title" breadcrumbs={breadcrumbs} />
</MemoryRouter>
);
export const WithTabs = () => (
<MemoryRouter>
<HeaderPage title="Page Title" tabs={tabs} />
</MemoryRouter>
);
export const WithCustomActions = () => (
<MemoryRouter>
<HeaderPage
title="Page Title"
customActions={<Button>Custom action</Button>}
/>
</MemoryRouter>
);
export const WithMenuItems = () => (
<MemoryRouter>
<HeaderPage
title="Page Title"
customActions={
<MenuTrigger>
<ButtonIcon variant="tertiary" icon={<RiMore2Line />} />
<Menu placement="bottom end">
{menuItems.map(option => (
<MenuItem
key={option.value}
onAction={option.onClick}
href={option.href}
>
{option.label}
</MenuItem>
))}
</Menu>
</MenuTrigger>
}
/>
</MemoryRouter>
);
@@ -1,20 +1,25 @@
import { PropsTable } from '@/components/PropsTable';
import { CodeBlock } from '@/components/CodeBlock';
import { Snippet } from '@/components/Snippet';
import { HeaderPageSnippet } from '@/snippets/stories-snippets';
import {
propDefs,
WithEverything,
WithLongBreadcrumbs,
WithTabs,
WithCustomActions,
WithMenuItems,
} from './components';
import { propDefs } from './props-definition';
import {
usage,
simple,
defaultSnippet,
withTabs,
withBreadcrumbs,
withCustomActions,
withMenuItems,
} from './header-page.props';
} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { HeaderPageDefinition } from '../utils/definitions';
import { HeaderPageDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -23,11 +28,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
description="A header page component for the plugin that should sit under the Header component."
/>
<Snippet
py={4}
preview={<HeaderPageSnippet story="WithEverything" />}
code={defaultSnippet}
/>
<Snippet py={4} preview={<WithEverything />} code={defaultSnippet} />
## Usage
@@ -44,44 +45,28 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
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={<HeaderPageSnippet story="WithLongBreadcrumbs" />}
code={withBreadcrumbs}
/>
<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={<HeaderPageSnippet story="WithTabs" />}
code={withTabs}
/>
<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={<HeaderPageSnippet story="WithCustomActions" />}
code={withCustomActions}
/>
<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={<HeaderPageSnippet story="WithMenuItems" />}
code={withMenuItems}
/>
<Snippet open preview={<WithMenuItems />} code={withMenuItems} />
<Theming definition={HeaderPageDefinition} />
@@ -87,55 +87,3 @@ export const propDefs: Record<string, PropDef> = {
...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,51 @@
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,90 @@
'use client';
import { Header } from '../../../../../packages/ui/src/components/Header/Header';
import { HeaderPage } from '../../../../../packages/ui/src/components/HeaderPage/HeaderPage';
import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
import { MemoryRouter } from 'react-router-dom';
import {
RiHeartLine,
RiEmotionHappyLine,
RiCloudy2Line,
} from '@remixicon/react';
const 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' },
];
const tabs2 = [
{ id: 'Banana', label: 'Banana', href: '/banana' },
{ id: 'Apple', label: 'Apple', href: '/apple' },
{ id: 'Orange', label: 'Orange', href: '/orange' },
];
const breadcrumbs = [
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Settings', href: '/settings' },
];
export const WithAllOptionsAndTabs = () => (
<MemoryRouter>
<Header
tabs={tabs}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
/>
</MemoryRouter>
);
export const WithAllOptions = () => (
<MemoryRouter>
<Header
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
/>
</MemoryRouter>
);
export const WithBreadcrumbs = () => (
<MemoryRouter>
<Header tabs={tabs} />
</MemoryRouter>
);
export const WithHeaderPage = () => (
<MemoryRouter>
<>
<Header
tabs={tabs}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
/>
<HeaderPage
title="Page title"
tabs={tabs2}
customActions={<Button>Custom action</Button>}
breadcrumbs={breadcrumbs}
/>
</>
</MemoryRouter>
);
@@ -1,19 +1,24 @@
import { PropsTable } from '@/components/PropsTable';
import { CodeBlock } from '@/components/CodeBlock';
import { Snippet } from '@/components/Snippet';
import { HeaderSnippet } from '@/snippets/stories-snippets';
import {
propDefs,
WithAllOptionsAndTabs,
WithAllOptions,
WithBreadcrumbs,
WithHeaderPage,
} from './components';
import { propDefs } from './props-definition';
import {
usage,
simple,
defaultSnippet,
withTabs,
withBreadcrumbs,
withHeaderPage,
} from './header.props';
} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { HeaderDefinition } from '../utils/definitions';
import { HeaderDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
@@ -22,11 +27,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
description="A header component for the plugin."
/>
<Snippet
py={4}
preview={<HeaderSnippet story="WithAllOptionsAndTabs" />}
code={defaultSnippet}
/>
<Snippet py={4} preview={<WithAllOptionsAndTabs />} code={defaultSnippet} />
## Usage
@@ -42,12 +43,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
A simple example of how to use the Header component.
<Snippet
py={4}
preview={<HeaderSnippet story="WithAllOptions" />}
code={simple}
open
/>
<Snippet py={4} preview={<WithAllOptions />} code={simple} open />
### Header with tabs
@@ -56,7 +52,7 @@ under the hood and will be active when you are on the corresponding page.
<Snippet
preview={<HeaderSnippet story="WithAllOptionsAndTabs" />}
preview={<WithAllOptionsAndTabs />}
code={withTabs}
open
/>
@@ -65,21 +61,13 @@ open
Breacrumbs should appear when you scroll down (and not directly visible as it is in the demo below).
<Snippet
preview={<HeaderSnippet story="WithBreadcrumbs" />}
code={withBreadcrumbs}
open
/>
<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={<HeaderSnippet story="WithHeaderPage" />}
code={withHeaderPage}
open
/>
<Snippet preview={<WithHeaderPage />} code={withHeaderPage} open />
<Theming definition={HeaderDefinition} />
@@ -0,0 +1,83 @@
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,
};
@@ -1,87 +1,3 @@
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 />`;
@@ -0,0 +1,111 @@
'use client';
import { Link } from '../../../../../packages/ui/src/components/Link/Link';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { MemoryRouter } from 'react-router-dom';
export const Default = () => {
return (
<MemoryRouter>
<Link href="/">Sign up for Backstage</Link>
</MemoryRouter>
);
};
export const ExternalLink = () => {
return (
<MemoryRouter>
<Link href="https://backstage.io" target="_blank">
Sign up for Backstage
</Link>
</MemoryRouter>
);
};
export const AllVariants = () => {
return (
<MemoryRouter>
<Flex gap="4" direction="column">
<Link href="https://ui.backstage.io" variant="title-large">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="title-medium">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="title-small">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="title-x-small">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="body-large">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="body-medium">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="body-small">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="body-x-small">
Sign up for Backstage
</Link>
</Flex>
</MemoryRouter>
);
};
export const AllColors = () => {
return (
<MemoryRouter>
<Flex gap="4" direction="column">
<Link
href="https://ui.backstage.io"
variant="title-small"
color="primary"
>
I am primary
</Link>
<Link
href="https://ui.backstage.io"
variant="title-small"
color="secondary"
>
I am secondary
</Link>
<Link
href="https://ui.backstage.io"
variant="title-small"
color="tertiary"
>
I am tertiary
</Link>
<Link
href="https://ui.backstage.io"
variant="title-small"
color="inherit"
>
I am inherit
</Link>
</Flex>
</MemoryRouter>
);
};
export const Underline = () => {
return (
<MemoryRouter>
<Flex gap="4" direction="column">
<Link href="https://ui.backstage.io" underline="always">
Always underlined
</Link>
<Link href="https://ui.backstage.io" underline="hover">
Underlined on hover
</Link>
<Link href="https://ui.backstage.io" underline="none">
Never underlined
</Link>
</Flex>
</MemoryRouter>
);
};
+92
View File
@@ -0,0 +1,92 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { linkPropDefs } from './props-definition';
import {
linkUsageSnippet,
defaultSnippet,
externalLinkSnippet,
allVariantsSnippet,
allColorsSnippet,
underlineSnippet,
} from './snippets';
import {
Default,
ExternalLink,
AllVariants,
AllColors,
Underline,
} from './components';
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={defaultSnippet} />
## Usage
<CodeBlock code={linkUsageSnippet} />
## API reference
<PropsTable data={linkPropDefs} />
## Examples
### External Link
Here's how to create an external link that opens in a new tab.
<Snippet
align="center"
py={4}
open
preview={<ExternalLink />}
code={externalLinkSnippet}
/>
### Variants
Here's a view when links have different variants.
<Snippet
align="center"
py={4}
open
preview={<AllVariants />}
code={allVariantsSnippet}
/>
### Colors
Here's a view when links have different colors.
<Snippet
align="center"
py={4}
open
preview={<AllColors />}
code={allColorsSnippet}
/>
### Underline
Here's a view when links have different underline styles.
<Snippet
align="center"
py={4}
open
preview={<Underline />}
code={underlineSnippet}
/>
<Theming definition={LinkDefinition} />
<ChangelogComponent component="link" />
@@ -0,0 +1,45 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const linkPropDefs: Record<string, PropDef> = {
href: {
type: 'string',
required: true,
},
target: {
type: 'enum',
values: ['_self', '_blank', '_parent', '_top'],
},
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,
},
color: {
type: 'enum',
values: ['primary', 'secondary', 'tertiary', 'inherit'],
default: 'primary',
},
underline: {
type: 'enum',
values: ['always', 'hover', 'none'],
default: 'hover',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,54 @@
export const linkUsageSnippet = `import { Link } from '@backstage/ui';
<Link href="/">Sign up for Backstage</Link>`;
export const defaultSnippet = `<Link href="/">Sign up for Backstage</Link>`;
export const externalLinkSnippet = `<Link href="https://backstage.io" target="_blank">
Sign up for Backstage
</Link>`;
export const allVariantsSnippet = `<Flex gap="4" direction="column">
<Link href="https://ui.backstage.io" variant="title-large">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="title-medium">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="title-small">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="body-large">
Sign up for Backstage
</Link>
<Link href="https://ui.backstage.io" variant="body-medium">
Sign up for Backstage
</Link>
</Flex>`;
export const allColorsSnippet = `<Flex gap="4" direction="column">
<Link href="https://ui.backstage.io" variant="title-small" color="primary">
I am primary
</Link>
<Link href="https://ui.backstage.io" variant="title-small" color="secondary">
I am secondary
</Link>
<Link href="https://ui.backstage.io" variant="title-small" color="tertiary">
I am tertiary
</Link>
<Link href="https://ui.backstage.io" variant="title-small" color="inherit">
I am inherit
</Link>
</Flex>`;
export const underlineSnippet = `<Flex gap="4" direction="column">
<Link href="https://ui.backstage.io" underline="always">
Always underlined
</Link>
<Link href="https://ui.backstage.io" underline="hover">
Underlined on hover
</Link>
<Link href="https://ui.backstage.io" underline="none">
Never underlined
</Link>
</Flex>`;
@@ -0,0 +1,157 @@
'use client';
import {
MenuTrigger,
Menu,
MenuItem,
MenuSection,
MenuSeparator,
SubmenuTrigger,
MenuAutocomplete,
MenuListBoxItem,
} from '../../../../../packages/ui/src/components/Menu/Menu';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
import { MemoryRouter } from 'react-router-dom';
import {
RiChat1Line,
RiFileLine,
RiFolderLine,
RiImageLine,
RiSettingsLine,
RiShareBoxLine,
} from '@remixicon/react';
export const Preview = () => (
<MemoryRouter>
<MenuTrigger>
<Button aria-label="Menu">Menu</Button>
<Menu>
<MenuItem>Edit</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Rename</MenuItem>
<MenuSeparator />
<MenuItem iconStart={<RiShareBoxLine />}>Share</MenuItem>
<MenuItem iconStart={<RiChat1Line />}>Feedback</MenuItem>
<MenuSeparator />
<SubmenuTrigger>
<MenuItem iconStart={<RiSettingsLine />}>Settings</MenuItem>
<Menu placement="right top">
<MenuItem>Edit</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Rename</MenuItem>
</Menu>
</SubmenuTrigger>
</Menu>
</MenuTrigger>
</MemoryRouter>
);
export const PreviewSubmenu = () => (
<MemoryRouter>
<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuItem>New File</MenuItem>
<SubmenuTrigger>
<MenuItem>Open Recent</MenuItem>
<Menu>
<MenuItem>File 1.txt</MenuItem>
<MenuItem>File 2.txt</MenuItem>
<MenuItem>File 3.txt</MenuItem>
</Menu>
</SubmenuTrigger>
<MenuItem>Save</MenuItem>
</Menu>
</MenuTrigger>
</MemoryRouter>
);
export const PreviewIcons = () => (
<MemoryRouter>
<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuItem iconStart={<RiFileLine />}>New File</MenuItem>
<MenuItem iconStart={<RiFolderLine />}>New Folder</MenuItem>
<MenuItem iconStart={<RiImageLine />}>New Image</MenuItem>
</Menu>
</MenuTrigger>
</MemoryRouter>
);
export const PreviewLinks = () => (
<MemoryRouter>
<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuItem href="/home">Home</MenuItem>
<MenuItem href="/about">About</MenuItem>
<MenuItem href="/contact">Contact</MenuItem>
</Menu>
</MenuTrigger>
</MemoryRouter>
);
export const PreviewSections = () => (
<MemoryRouter>
<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuSection title="File">
<MenuItem>New</MenuItem>
<MenuItem>Open</MenuItem>
</MenuSection>
<MenuSection title="Edit">
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</MenuSection>
</Menu>
</MenuTrigger>
</MemoryRouter>
);
export const PreviewSeparators = () => (
<MemoryRouter>
<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuItem>New</MenuItem>
<MenuItem>Open</MenuItem>
<MenuSeparator />
<MenuItem>Save</MenuItem>
<MenuItem>Save As...</MenuItem>
</Menu>
</MenuTrigger>
</MemoryRouter>
);
export const PreviewAutocompleteMenu = () => (
<MemoryRouter>
<MenuAutocomplete placeholder="Type to search...">
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
<MenuItem>Option 3</MenuItem>
</MenuAutocomplete>
</MemoryRouter>
);
export const PreviewAutocompleteListbox = () => (
<MemoryRouter>
<MenuAutocomplete placeholder="Type to filter...">
<MenuListBoxItem>Option 1</MenuListBoxItem>
<MenuListBoxItem>Option 2</MenuListBoxItem>
<MenuListBoxItem>Option 3</MenuListBoxItem>
</MenuAutocomplete>
</MemoryRouter>
);
export const PreviewAutocompleteListboxMultiple = () => (
<MemoryRouter>
<MenuAutocomplete placeholder="Type to filter..." selectionMode="multiple">
<MenuListBoxItem>Option 1</MenuListBoxItem>
<MenuListBoxItem>Option 2</MenuListBoxItem>
<MenuListBoxItem>Option 3</MenuListBoxItem>
</MenuAutocomplete>
</MemoryRouter>
);
@@ -1,7 +1,29 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { MenuSnippet } from '@/snippets/stories-snippets';
import {
Preview,
PreviewSubmenu,
PreviewIcons,
PreviewLinks,
PreviewSections,
PreviewSeparators,
PreviewAutocompleteMenu,
PreviewAutocompleteListbox,
PreviewAutocompleteListboxMultiple,
} from './components';
import {
menuTriggerPropDefs,
submenuTriggerPropDefs,
menuPropDefs,
menuListBoxPropDefs,
menuAutocompletePropDefs,
menuAutocompleteListboxPropDefs,
menuItemPropDefs,
menuListBoxItemPropDefs,
menuSectionPropDefs,
menuSeparatorPropDefs,
} from './props-definition';
import {
usage,
preview,
@@ -13,33 +35,18 @@ import {
autocomplete,
autocompleteListbox,
autocompleteListboxMultiple,
menuTriggerPropDefs,
submenuTriggerPropDefs,
menuPropDefs,
menuListBoxPropDefs,
menuAutocompletePropDefs,
menuAutocompleteListboxPropDefs,
menuItemPropDefs,
menuListBoxItemPropDefs,
menuSectionPropDefs,
menuSeparatorPropDefs,
} from './menu.props';
} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { MenuDefinition } from '../utils/definitions';
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={<MenuSnippet story="Preview" />}
code={preview}
/>
<Snippet align="center" py={4} preview={<Preview />} code={preview} />
## Usage
@@ -142,7 +149,7 @@ the submenu is displayed in the correct position. The best practice is to use th
align="center"
py={4}
open
preview={<MenuSnippet story="PreviewSubmenu" />}
preview={<PreviewSubmenu />}
code={submenu}
/>
@@ -150,26 +157,14 @@ the submenu is displayed in the correct position. The best practice is to use th
You can use the `iconStart` prop to add an icon to the menu item.
<Snippet
align="center"
py={4}
open
preview={<MenuSnippet story="PreviewIcons" />}
code={icons}
/>
<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={<MenuSnippet story="PreviewLinks" />}
code={links}
/>
<Snippet align="center" py={4} open preview={<PreviewLinks />} code={links} />
### With sections
@@ -179,7 +174,7 @@ You can use the `MenuSection` component to add a section to the menu.
align="center"
py={4}
open
preview={<MenuSnippet story="PreviewSections" />}
preview={<PreviewSections />}
code={sections}
/>
@@ -191,7 +186,7 @@ You can use the `MenuSeparator` component to add a separator to the menu.
align="center"
py={4}
open
preview={<MenuSnippet story="PreviewSeparators" />}
preview={<PreviewSeparators />}
code={separators}
/>
@@ -203,7 +198,7 @@ You can use the `MenuAutocomplete` component to add a autocomplete to the menu.
align="center"
py={4}
open
preview={<MenuSnippet story="PreviewAutocompleteMenu" />}
preview={<PreviewAutocompleteMenu />}
code={autocomplete}
/>
@@ -215,7 +210,7 @@ You can use the `MenuListBox` component to add a list box to the menu.
align="center"
py={4}
open
preview={<MenuSnippet story="PreviewAutocompleteListbox" />}
preview={<PreviewAutocompleteListbox />}
code={autocompleteListbox}
/>
@@ -228,7 +223,7 @@ allow multiple selection.
align="center"
py={4}
open
preview={<MenuSnippet story="PreviewAutocompleteListboxMultiple" />}
preview={<PreviewAutocompleteListboxMultiple />}
code={autocompleteListboxMultiple}
/>
@@ -0,0 +1,241 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
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: [
'top',
'bottom',
'left',
'right',
'top start',
'top end',
'bottom start',
'bottom end',
'left start',
'left end',
'right start',
'right end',
],
},
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: [
'top',
'bottom',
'left',
'right',
'top start',
'top end',
'bottom start',
'bottom end',
'left start',
'left end',
'right start',
'right end',
],
},
virtualized: {
type: 'boolean',
default: 'false',
},
maxWidth: {
type: 'number',
},
maxHeight: {
type: 'number',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuAutocompletePropDefs: Record<string, PropDef> = {
placement: {
type: 'enum',
values: [
'top',
'bottom',
'left',
'right',
'top start',
'top end',
'bottom start',
'bottom end',
'left start',
'left end',
'right start',
'right end',
],
},
virtualized: {
type: 'boolean',
default: 'false',
},
maxWidth: {
type: 'number',
},
maxHeight: {
type: 'number',
},
...classNamePropDefs,
...stylePropDefs,
};
export const menuAutocompleteListboxPropDefs: Record<string, PropDef> = {
placement: {
type: 'enum',
values: [
'top',
'bottom',
'left',
'right',
'top start',
'top end',
'bottom start',
'bottom end',
'left start',
'left end',
'right start',
'right end',
],
},
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,
};
+100
View File
@@ -0,0 +1,100 @@
export const usage = `import { MenuTrigger, Menu, MenuItem } from '@backstage/ui';
<MenuTrigger>
<Button>Menu</Button>
<Menu>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
</Menu>
</MenuTrigger>`;
export const preview = `<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuItem>New File</MenuItem>
<MenuItem>Open File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem>Save As...</MenuItem>
</Menu>
</MenuTrigger>`;
export const submenu = `<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuItem>New File</MenuItem>
<SubmenuTrigger>
<MenuItem>Open Recent</MenuItem>
<Menu>
<MenuItem>File 1.txt</MenuItem>
<MenuItem>File 2.txt</MenuItem>
</Menu>
</SubmenuTrigger>
<MenuItem>Save</MenuItem>
</Menu>
</MenuTrigger>`;
export const icons = `<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuItem icon={<RiFileLine />}>New File</MenuItem>
<MenuItem icon={<RiFolderLine />}>New Folder</MenuItem>
<MenuItem icon={<RiImageLine />}>New Image</MenuItem>
</Menu>
</MenuTrigger>`;
export const sections = `<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuSection title="File">
<MenuItem>New</MenuItem>
<MenuItem>Open</MenuItem>
</MenuSection>
<MenuSection title="Edit">
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</MenuSection>
</Menu>
</MenuTrigger>`;
export const separators = `<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuItem>New</MenuItem>
<MenuItem>Open</MenuItem>
<MenuSeparator />
<MenuItem>Save</MenuItem>
<MenuItem>Save As...</MenuItem>
</Menu>
</MenuTrigger>`;
export const links = `<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Menu>
<MenuItem href="/home">Home</MenuItem>
<MenuItem href="/about">About</MenuItem>
<MenuItem href="/contact">Contact</MenuItem>
</Menu>
</MenuTrigger>`;
export const autocomplete = `<MenuAutocomplete label="Search" placeholder="Type to search...">
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
<MenuItem>Option 3</MenuItem>
</MenuAutocomplete>`;
export const autocompleteListbox = `<MenuAutocomplete label="Select an option" placeholder="Type to filter...">
<MenuListBoxItem>Option 1</MenuListBoxItem>
<MenuListBoxItem>Option 2</MenuListBoxItem>
<MenuListBoxItem>Option 3</MenuListBoxItem>
</MenuAutocomplete>`;
export const autocompleteListboxMultiple = `<MenuAutocomplete
label="Select multiple options"
placeholder="Type to filter..."
selectionMode="multiple"
>
<MenuListBoxItem>Option 1</MenuListBoxItem>
<MenuListBoxItem>Option 2</MenuListBoxItem>
<MenuListBoxItem>Option 3</MenuListBoxItem>
</MenuAutocomplete>`;
@@ -0,0 +1,34 @@
'use client';
import { PasswordField } from '../../../../../packages/ui/src/components/PasswordField/PasswordField';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
export const WithLabel = () => {
return (
<PasswordField
name="password"
label="Password"
style={{ maxWidth: '300px' }}
/>
);
};
export const Sizes = () => {
return (
<Flex direction="row" gap="4" style={{ width: '100%', maxWidth: '600px' }}>
<PasswordField name="password" label="Password" size="small" />
<PasswordField name="password" label="Password" size="medium" />
</Flex>
);
};
export const WithDescription = () => {
return (
<PasswordField
name="password"
label="Password"
description="Enter your password"
style={{ maxWidth: '300px' }}
/>
);
};
@@ -1,18 +1,18 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { PasswordFieldSnippet } from '@/snippets/stories-snippets';
import { inputPropDefs } from './props-definition';
import {
inputPropDefs,
passwordFieldUsageSnippet,
passwordFieldDefaultSnippet,
passwordFieldSizesSnippet,
passwordFieldDescriptionSnippet,
} from './password-field.props';
withLabelSnippet,
sizesSnippet,
withDescriptionSnippet,
} from './snippets';
import { WithLabel, Sizes, WithDescription } from './components';
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 { PasswordFieldDefinition } from '../../../utils/definitions';
<PageTitle
title="PasswordField"
@@ -22,8 +22,8 @@ import { PasswordFieldDefinition } from '../utils/definitions';
<Snippet
align="center"
py={4}
preview={<PasswordFieldSnippet story="WithLabel" />}
code={passwordFieldDefaultSnippet}
preview={<WithLabel />}
code={withLabelSnippet}
/>
## Usage
@@ -40,13 +40,7 @@ import { PasswordFieldDefinition } from '../utils/definitions';
We support two different sizes: `small`, `medium`.
<Snippet
align="center"
py={4}
open
preview={<PasswordFieldSnippet story="Sizes" />}
code={passwordFieldSizesSnippet}
/>
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
### With description
@@ -56,8 +50,8 @@ Here's a simple PasswordField with a description.
align="center"
py={4}
open
preview={<PasswordFieldSnippet story="WithDescription" />}
code={passwordFieldDescriptionSnippet}
preview={<WithDescription />}
code={withDescriptionSnippet}
/>
<Theming definition={PasswordFieldDefinition} />
@@ -0,0 +1,26 @@
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',
},
description: {
type: 'string',
},
name: {
type: 'string',
required: true,
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,19 @@
export const passwordFieldUsageSnippet = `import { PasswordField } from '@backstage/ui';
<PasswordField />`;
export const withLabelSnippet = `<PasswordField
name="password"
label="Password"
/>`;
export const sizesSnippet = `<Flex direction="row" gap="4">
<PasswordField name="password" label="Password" size="small" />
<PasswordField name="password" label="Password" size="medium" />
</Flex>`;
export const withDescriptionSnippet = `<PasswordField
name="password"
label="Password"
description="Enter your password"
/>`;
@@ -0,0 +1,18 @@
'use client';
import { Popover } from '../../../../../packages/ui/src/components/Popover/Popover';
import { DialogTrigger } from '../../../../../packages/ui/src/components/Dialog/Dialog';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
export const Default = () => {
return (
<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<div style={{ padding: '16px' }}>
<p>This is a popover</p>
</div>
</Popover>
</DialogTrigger>
);
};
@@ -0,0 +1,39 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { dialogTriggerPropDefs, popoverPropDefs } from './props-definition';
import { popoverUsageSnippet, defaultSnippet } from './snippets';
import { Default } from './components';
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={defaultSnippet} />
## Usage
<CodeBlock code={popoverUsageSnippet} />
## API reference
### DialogTrigger
The trigger component that wraps both the trigger button and the popover content.
<PropsTable data={dialogTriggerPropDefs} />
### Popover
The main popover component that displays floating content with automatic positioning.
<PropsTable data={popoverPropDefs} />
<Theming definition={PopoverDefinition} />
<ChangelogComponent component="popover" />
@@ -0,0 +1,55 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
} from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const dialogTriggerPropDefs: Record<string, PropDef> = {
isDisabled: {
type: 'boolean',
},
delay: {
type: 'number',
default: '0',
},
closeDelay: {
type: 'number',
default: '0',
},
isOpen: {
type: 'boolean',
},
defaultOpen: {
type: 'boolean',
},
...childrenPropDefs,
};
export const popoverPropDefs: 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: '8',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,19 @@
export const popoverUsageSnippet = `import { DialogTrigger, Popover, Button } from '@backstage/ui';
<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<div style={{ padding: '16px' }}>
<p>This is a popover</p>
</div>
</Popover>
</DialogTrigger>`;
export const defaultSnippet = `<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<div style={{ padding: '16px' }}>
<p>This is a popover</p>
</div>
</Popover>
</DialogTrigger>`;
@@ -0,0 +1,78 @@
'use client';
import {
RadioGroup,
Radio,
} from '../../../../../packages/ui/src/components/RadioGroup/RadioGroup';
export const Default = () => {
return (
<RadioGroup label="What is your favorite pokemon?">
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>
);
};
export const Horizontal = () => {
return (
<RadioGroup label="What is your favorite pokemon?" orientation="horizontal">
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>
);
};
export const Disabled = () => {
return (
<RadioGroup label="What is your favorite pokemon?" isDisabled>
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>
);
};
export const DisabledSingle = () => {
return (
<RadioGroup label="What is your favorite pokemon?">
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander" isDisabled>
Charmander
</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>
);
};
export const Validation = () => {
return (
<RadioGroup
label="What is your favorite pokemon?"
name="pokemon"
defaultValue="charmander"
validationBehavior="aria"
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 ReadOnly = () => {
return (
<RadioGroup
label="What is your favorite pokemon?"
isReadOnly
defaultValue="charmander"
>
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>
);
};
@@ -1,34 +1,35 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { RadioGroupSnippet } from '@/snippets/stories-snippets';
import { radioGroupPropDefs } from './props-definition';
import {
radioGroupPropDefs,
radioGroupUsageSnippet,
radioGroupDefaultSnippet,
radioGroupDescriptionSnippet,
radioGroupHorizontalSnippet,
radioGroupDisabledSnippet,
radioGroupDisabledSingleSnippet,
radioGroupValidationSnippet,
radioGroupReadOnlySnippet,
} from './radio-group.props';
defaultSnippet,
horizontalSnippet,
disabledSnippet,
disabledSingleSnippet,
validationSnippet,
readOnlySnippet,
} from './snippets';
import {
Default,
Horizontal,
Disabled,
DisabledSingle,
Validation,
ReadOnly,
} from './components';
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';
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={<RadioGroupSnippet story="Default" />}
code={radioGroupDefaultSnippet}
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
## Usage
@@ -48,8 +49,8 @@ Here's a simple TextField with a description.
align="center"
py={4}
open
preview={<RadioGroupSnippet story="Horizontal" />}
code={radioGroupHorizontalSnippet}
preview={<Horizontal />}
code={horizontalSnippet}
/>
### Disabled
@@ -60,8 +61,8 @@ You can disable the entire radio group by adding the `isDisabled` prop to the `R
align="center"
py={4}
open
preview={<RadioGroupSnippet story="Disabled" />}
code={radioGroupDisabledSnippet}
preview={<Disabled />}
code={disabledSnippet}
/>
### Disabled Single radio
@@ -72,8 +73,8 @@ You can disable a single radio by adding the `isDisabled` prop to the `Radio` co
align="center"
py={4}
open
preview={<RadioGroupSnippet story="DisabledSingle" />}
code={radioGroupDisabledSingleSnippet}
preview={<DisabledSingle />}
code={disabledSingleSnippet}
/>
### Validation
@@ -84,8 +85,8 @@ Here's an example of a radio group with errors.
align="center"
py={4}
open
preview={<RadioGroupSnippet story="Validation" />}
code={radioGroupValidationSnippet}
preview={<Validation />}
code={validationSnippet}
/>
### Read only
@@ -96,8 +97,8 @@ You can make the radio group read only by adding the `isReadOnly` prop to the `R
align="center"
py={4}
open
preview={<RadioGroupSnippet story="ReadOnly" />}
code={radioGroupReadOnlySnippet}
preview={<ReadOnly />}
code={readOnlySnippet}
/>
<Theming definition={RadioGroupDefinition} />
@@ -0,0 +1,30 @@
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,
};
@@ -0,0 +1,54 @@
export const radioGroupUsageSnippet = `import { RadioGroup } from '@backstage/ui';
<RadioGroup />`;
export const defaultSnippet = `<RadioGroup label="What is your favorite pokemon?">
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>`;
export const horizontalSnippet = `<RadioGroup
label="What is your favorite pokemon?"
orientation="horizontal"
>
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>`;
export const disabledSnippet = `<RadioGroup label="What is your favorite pokemon?" isDisabled>
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>`;
export const disabledSingleSnippet = `<RadioGroup label="What is your favorite pokemon?">
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander" isDisabled>
Charmander
</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>`;
export const validationSnippet = `<RadioGroup
label="What is your favorite pokemon?"
name="pokemon"
defaultValue="charmander"
validationBehavior="aria"
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 readOnlySnippet = `<RadioGroup
label="What is your favorite pokemon?"
isReadOnly
defaultValue="charmander"
>
<Radio value="bulbasaur">Bulbasaur</Radio>
<Radio value="charmander">Charmander</Radio>
<Radio value="squirtle">Squirtle</Radio>
</RadioGroup>`;
@@ -0,0 +1,57 @@
'use client';
import { SearchField } from '../../../../../packages/ui/src/components/SearchField/SearchField';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
export const WithLabel = () => {
return (
<SearchField
name="url"
label="Label"
aria-label="Search"
style={{ width: '100%', maxWidth: '300px' }}
/>
);
};
export const Sizes = () => {
return (
<Flex direction="row" gap="4" style={{ width: '100%', maxWidth: '600px' }}>
<SearchField name="url" aria-label="Search" size="small" />
<SearchField name="url" aria-label="Search" size="medium" />
</Flex>
);
};
export const WithDescription = () => {
return (
<SearchField
name="url"
label="Label"
description="Description"
aria-label="Search"
style={{ width: '100%', maxWidth: '300px' }}
/>
);
};
export const StartCollapsed = () => {
return (
<Flex direction="column" gap="4">
<Flex direction="row" gap="4">
<SearchField
name="url"
aria-label="Search"
size="small"
startCollapsed
/>
<SearchField
name="url"
aria-label="Search"
size="medium"
startCollapsed
/>
</Flex>
</Flex>
);
};
@@ -1,19 +1,24 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { SearchFieldSnippet } from '@/snippets/stories-snippets';
import { searchFieldPropDefs } from './props-definition';
import {
searchFieldPropDefs,
searchFieldUsageSnippet,
searchFieldDefaultSnippet,
searchFieldSizesSnippet,
searchFieldDescriptionSnippet,
searchFieldCollapsibleSnippet,
} from './search-field.props';
withLabelSnippet,
sizesSnippet,
withDescriptionSnippet,
startCollapsedSnippet,
} from './snippets';
import {
WithLabel,
Sizes,
WithDescription,
StartCollapsed,
} from './components';
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';
import { SearchFieldDefinition } from '../../../utils/definitions';
<PageTitle
title="SearchField"
@@ -23,8 +28,8 @@ import { SearchFieldDefinition } from '../utils/definitions';
<Snippet
align="center"
py={4}
preview={<SearchFieldSnippet story="WithLabel" />}
code={searchFieldDefaultSnippet}
preview={<WithLabel />}
code={withLabelSnippet}
/>
## Usage
@@ -41,13 +46,7 @@ import { SearchFieldDefinition } from '../utils/definitions';
We support two different sizes: `small`, `medium`.
<Snippet
align="center"
py={4}
open
preview={<SearchFieldSnippet story="Sizes" />}
code={searchFieldSizesSnippet}
/>
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
### With description
@@ -57,8 +56,8 @@ Here's a simple SearchField with a description.
align="center"
py={4}
open
preview={<SearchFieldSnippet story="WithDescription" />}
code={searchFieldDescriptionSnippet}
preview={<WithDescription />}
code={withDescriptionSnippet}
/>
### Collapsible
@@ -69,8 +68,8 @@ You can make the SearchField collapsible by setting the `startCollapsed` prop to
align="center"
py={4}
open
preview={<SearchFieldSnippet story="StartCollapsed" />}
code={searchFieldCollapsibleSnippet}
preview={<StartCollapsed />}
code={startCollapsedSnippet}
/>
<Theming definition={SearchFieldDefinition} />
@@ -0,0 +1,34 @@
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,
};
@@ -0,0 +1,35 @@
export const searchFieldUsageSnippet = `import { SearchField } from '@backstage/ui';
<SearchField />`;
export const withLabelSnippet = `<SearchField name="url" label="Label" aria-label="Search" />`;
export const sizesSnippet = `<Flex direction="row" gap="4">
<SearchField name="url" aria-label="Search" size="small" />
<SearchField name="url" aria-label="Search" size="medium" />
</Flex>`;
export const withDescriptionSnippet = `<SearchField
name="url"
label="Label"
description="Description"
aria-label="Search"
/>`;
export const startCollapsedSnippet = `<Flex direction="column" gap="4">
<Flex direction="row" gap="4">
<SearchField
name="url"
aria-label="Search"
size="small"
startCollapsed
/>
<SearchField
name="url"
aria-label="Search"
size="medium"
startCollapsed
/>
</Flex>
<SearchField name="url" aria-label="Search" size="small" startCollapsed />
</Flex>`;
@@ -0,0 +1,150 @@
'use client';
import { Select } from '../../../../../packages/ui/src/components/Select/Select';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { RiCloudLine } from '@remixicon/react';
const fontOptions = [
{ value: 'sans', label: 'Sans-serif' },
{ value: 'serif', label: 'Serif' },
{ value: 'mono', label: 'Monospace' },
{ value: 'cursive', label: 'Cursive' },
];
const countries = [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'mx', label: 'Mexico' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
{ value: 'it', label: 'Italy' },
{ value: 'es', label: 'Spain' },
{ value: 'jp', label: 'Japan' },
{ value: 'cn', label: 'China' },
{ value: 'in', label: 'India' },
{ value: 'br', label: 'Brazil' },
{ value: 'au', label: 'Australia' },
];
const skills = [
{ value: 'react', label: 'React' },
{ value: 'typescript', label: 'TypeScript' },
{ value: 'javascript', label: 'JavaScript' },
{ value: 'python', label: 'Python' },
{ value: 'java', label: 'Java' },
{ value: 'csharp', label: 'C#' },
{ value: 'go', label: 'Go' },
{ value: 'rust', label: 'Rust' },
{ value: 'kotlin', label: 'Kotlin' },
{ value: 'swift', label: 'Swift' },
];
export const Preview = () => (
<Select
label="Font Family"
options={fontOptions}
placeholder="Select a font"
name="font"
style={{ maxWidth: 260 }}
/>
);
export const WithLabelAndDescription = () => (
<Select
label="Font Family"
description="Choose a font family for your document"
options={fontOptions}
name="font"
style={{ width: 300 }}
/>
);
export const Sizes = () => (
<Flex direction="row" gap="2">
<Select
label="Small"
size="small"
options={fontOptions}
name="font-small"
placeholder="Select a font"
style={{ maxWidth: 260 }}
/>
<Select
label="Medium"
size="medium"
options={fontOptions}
name="font-medium"
placeholder="Select a font"
style={{ maxWidth: 260 }}
/>
</Flex>
);
export const WithIcon = () => (
<Select
label="Font Family"
options={fontOptions}
name="font"
icon={<RiCloudLine />}
style={{ width: 300 }}
/>
);
export const Disabled = () => (
<Select
label="Font Family"
options={fontOptions}
name="font"
isDisabled
style={{ width: 300 }}
/>
);
export const DisabledOption = () => (
<Select
label="Select an option"
options={fontOptions}
name="font"
disabledKeys={['serif']}
style={{ width: 300 }}
/>
);
export const Searchable = () => (
<Select
label="Country"
searchable
searchPlaceholder="Search countries..."
options={countries}
name="country"
style={{ width: 300 }}
/>
);
export const MultipleSelection = () => (
<Select
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' },
]}
name="multiple"
style={{ width: 300 }}
/>
);
export const SearchableMultiple = () => (
<Select
label="Skills"
searchable
selectionMode="multiple"
searchPlaceholder="Filter skills..."
options={skills}
name="skills"
style={{ width: 300 }}
/>
);
@@ -1,9 +1,19 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { SelectSnippet } from '@/snippets/stories-snippets';
import {
selectPropDefs,
Preview,
WithLabelAndDescription,
Sizes,
WithIcon,
Disabled,
DisabledOption,
Searchable,
MultipleSelection,
SearchableMultiple,
} from './components';
import { selectPropDefs } from './props-definition';
import {
selectUsageSnippet,
selectDefaultSnippet,
selectDescriptionSnippet,
@@ -15,11 +25,11 @@ import {
selectMultipleSnippet,
selectSearchableMultipleSnippet,
selectDisabledOptionsSnippet,
} from './select.props';
} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SelectDefinition } from '../utils/definitions';
import { SelectDefinition } from '../../../utils/definitions';
<PageTitle
title="Select"
@@ -29,7 +39,7 @@ import { SelectDefinition } from '../utils/definitions';
<Snippet
align="center"
py={4}
preview={<SelectSnippet story="Preview" />}
preview={<Preview />}
code={selectDefaultSnippet}
/>
@@ -51,7 +61,7 @@ Select component with label and description.
align="center"
py={4}
open
preview={<SelectSnippet story="WithLabelAndDescription" />}
preview={<WithLabelAndDescription />}
code={selectDescriptionSnippet}
/>
@@ -63,7 +73,7 @@ Here's a view when the selects have different sizes.
align="center"
py={4}
open
preview={<SelectSnippet story="Sizes" />}
preview={<Sizes />}
code={selectSizesSnippet}
/>
@@ -76,7 +86,7 @@ Here's a view when the select has an icon.
align="center"
py={4}
open
preview={<SelectSnippet story="WithIcon" />}
preview={<WithIcon />}
code={selectIconSnippet}
/>
@@ -88,7 +98,7 @@ Here's a view when the select is disabled.
align="center"
py={4}
open
preview={<SelectSnippet story="Disabled" />}
preview={<Disabled />}
code={selectDisabledSnippet}
/>
@@ -100,7 +110,7 @@ You can disable specific options within the Select component using `disabledKeys
align="center"
py={4}
open
preview={<SelectSnippet story="DisabledOption" />}
preview={<DisabledOption />}
code={selectDisabledOptionsSnippet}
/>
@@ -112,7 +122,7 @@ Here's a view when the select has searchable filtering.
align="center"
py={4}
open
preview={<SelectSnippet story="Searchable" />}
preview={<Searchable />}
code={selectSearchableSnippet}
/>
@@ -124,7 +134,7 @@ Here's a view when the select allows multiple selections.
align="center"
py={4}
open
preview={<SelectSnippet story="MultipleSelection" />}
preview={<MultipleSelection />}
code={selectMultipleSnippet}
/>
@@ -136,7 +146,7 @@ Here's a view when the select combines search and multiple selection.
align="center"
py={4}
open
preview={<SelectSnippet story="SearchableMultiple" />}
preview={<SearchableMultiple />}
code={selectSearchableMultipleSnippet}
/>
@@ -0,0 +1,111 @@
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,
};
@@ -1,115 +1,3 @@
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
@@ -0,0 +1,31 @@
'use client';
import { Skeleton } from '../../../../../packages/ui/src/components/Skeleton/Skeleton';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
export const Demo1 = () => {
return (
<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 Demo2 = () => {
return (
<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>
);
};
@@ -1,30 +1,20 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { SkeletonSnippet } from '@/snippets/stories-snippets';
import {
skeletonPropDefs,
skeletonUsageSnippet,
skeletonDefaultSnippet,
skeletonDemo1Snippet,
skeletonDemo2Snippet,
} from './skeleton.props';
import { skeletonPropDefs } from './props-definition';
import { skeletonUsageSnippet, demo1Snippet, demo2Snippet } from './snippets';
import { Demo1, Demo2 } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SkeletonDefinition } from '../utils/definitions';
import { SkeletonDefinition } from '../../../utils/definitions';
<PageTitle
title="Skeleton"
description="Use to show a placeholder while content is loading."
/>
<Snippet
align="center"
py={4}
preview={<SkeletonSnippet story="Demo2" />}
code={skeletonDefaultSnippet}
/>
<Snippet align="center" py={4} preview={<Demo2 />} code={demo2Snippet} />
## Usage
@@ -40,25 +30,13 @@ import { SkeletonDefinition } from '../utils/definitions';
You can use a mix of different sizes to create a more complex skeleton.
<Snippet
align="center"
py={4}
preview={<SkeletonSnippet story="Demo1" />}
code={skeletonDemo1Snippet}
open
/>
<Snippet align="center" py={4} preview={<Demo1 />} code={demo1Snippet} open />
### Demo 2
You can use a mix of different sizes to create a more complex skeleton.
<Snippet
align="center"
py={4}
preview={<SkeletonSnippet story="Demo2" />}
code={skeletonDemo2Snippet}
open
/>
<Snippet align="center" py={4} preview={<Demo2 />} code={demo2Snippet} open />
<Theming definition={SkeletonDefinition} />
@@ -0,0 +1,25 @@
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,
};
@@ -0,0 +1,26 @@
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 demo1Snippet = `<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 demo2Snippet = `<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>`;
@@ -0,0 +1,11 @@
'use client';
import { Switch } from '../../../../../packages/ui/src/components/Switch/Switch';
export const Default = () => {
return <Switch label="Switch" />;
};
export const Disabled = () => {
return <Switch label="Switch" isDisabled />;
};
@@ -1,24 +1,20 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { SwitchSnippet } from '@/snippets/stories-snippets';
import { switchPropDefs, snippetUsage } from './switch.props';
import { switchPropDefs } from './props-definition';
import { snippetUsage, defaultSnippet, disabledSnippet } from './snippets';
import { Default, Disabled } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SwitchDefinition } from '../utils/definitions';
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={<SwitchSnippet story="Default" />}
code={`<Switch />`}
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
## Usage
@@ -38,8 +34,8 @@ A switch can be disabled using the `isDisabled` prop.
align="center"
py={4}
open
preview={<SwitchSnippet story="Disabled" />}
code={`<Switch isDisabled />`}
preview={<Disabled />}
code={disabledSnippet}
/>
<Theming definition={SwitchDefinition} />
@@ -65,7 +65,3 @@ export const switchPropDefs: Record<string, PropDef> = {
type: 'string',
},
};
export const snippetUsage = `import { Switch } from '@backstage/ui';
<Switch />`;
@@ -0,0 +1,7 @@
export const snippetUsage = `import { Switch } from '@backstage/ui';
<Switch />`;
export const defaultSnippet = `<Switch label="Switch" />`;
export const disabledSnippet = `<Switch label="Switch" isDisabled />`;
@@ -0,0 +1,188 @@
'use client';
import {
Table,
CellProfile,
CellText,
type ColumnConfig,
useTable,
} from '../../../../../packages/ui/src/components/Table';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
import { RadioGroup } from '../../../../../packages/ui/src/components/RadioGroup/RadioGroup';
import { Radio } from '../../../../../packages/ui/src/components/RadioGroup';
import { data as data4 } from '../../../../../packages/ui/src/components/Table/stories/mocked-data4';
import { useState } from 'react';
import {
selectionData,
selectionColumns,
} from '../../../../../packages/ui/src/components/Table/stories/utils';
import { MemoryRouter } from 'react-router-dom';
type Data4Item = (typeof data4)[0];
const columns: ColumnConfig<Data4Item>[] = [
{
id: 'name',
label: 'Band name',
isRowHeader: true,
cell: item => (
<CellProfile name={item.name} src={item.image} href={item.website} />
),
},
{
id: 'genre',
label: 'Genre',
cell: item => <CellText title={item.genre} />,
},
{
id: 'yearFormed',
label: 'Year formed',
cell: item => <CellText title={item.yearFormed.toString()} />,
},
{
id: 'albums',
label: 'Albums',
cell: item => <CellText title={item.albums.toString()} />,
},
];
export const TableRockBand = () => {
const { tableProps } = useTable({
mode: 'complete',
getData: () => data4,
paginationOptions: { pageSize: 5 },
});
return (
<MemoryRouter>
<Table columnConfig={columns} {...tableProps} />
</MemoryRouter>
);
};
export const SelectionToggleWithActions = () => {
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
new Set(),
);
const { tableProps } = useTable({
mode: 'complete',
getData: () => selectionData,
paginationOptions: { pageSize: 10 },
});
return (
<MemoryRouter>
<Table
{...tableProps}
columnConfig={selectionColumns}
selection={{
mode: 'multiple',
behavior: 'toggle',
selected,
onSelectionChange: setSelected,
}}
rowConfig={{ onClick: item => alert(`Clicked: ${item.name}`) }}
/>
</MemoryRouter>
);
};
export const SelectionModePlayground = () => {
const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>(
'multiple',
);
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
new Set(),
);
const { tableProps } = useTable({
mode: 'complete',
getData: () => selectionData,
paginationOptions: { pageSize: 10 },
});
return (
<MemoryRouter>
<Flex direction="column" gap="8">
<Table
{...tableProps}
columnConfig={selectionColumns}
selection={{
mode: selectionMode,
behavior: 'toggle',
selected,
onSelectionChange: setSelected,
}}
/>
<div>
<Text as="h4" style={{ marginBottom: 'var(--bui-space-2)' }}>
Selection mode:
</Text>
<RadioGroup
aria-label="Selection mode"
orientation="horizontal"
value={selectionMode}
onChange={value => {
setSelectionMode(value as 'single' | 'multiple');
setSelected(new Set());
}}
>
<Radio value="single">single</Radio>
<Radio value="multiple">multiple</Radio>
</RadioGroup>
</div>
</Flex>
</MemoryRouter>
);
};
export const SelectionBehaviorPlayground = () => {
const [selectionBehavior, setSelectionBehavior] = useState<
'toggle' | 'replace'
>('toggle');
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
new Set(),
);
const { tableProps } = useTable({
mode: 'complete',
getData: () => selectionData,
paginationOptions: { pageSize: 10 },
});
return (
<MemoryRouter>
<Flex direction="column" gap="8">
<Table
{...tableProps}
columnConfig={selectionColumns}
selection={{
mode: 'multiple',
behavior: selectionBehavior,
selected,
onSelectionChange: setSelected,
}}
/>
<div>
<Text as="h4" style={{ marginBottom: 'var(--bui-space-2)' }}>
Selection behavior:
</Text>
<RadioGroup
aria-label="Selection behavior"
orientation="horizontal"
value={selectionBehavior}
onChange={value => {
setSelectionBehavior(value as 'toggle' | 'replace');
setSelected(new Set());
}}
>
<Radio value="toggle">toggle</Radio>
<Radio value="replace">replace</Radio>
</RadioGroup>
</div>
</Flex>
</MemoryRouter>
);
};
@@ -1,41 +1,39 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { TableSnippet } from '@/snippets/stories-snippets';
import {
TableRockBand,
SelectionModePlayground,
SelectionBehaviorPlayground,
SelectionToggleWithActions,
} from './components';
import {
tablePropDefs,
tableHeaderPropDefs,
tableBodyPropDefs,
tablePaginationPropDefs,
tableUsageSnippet,
tableBasicSnippet,
tableRowClickSnippet,
tableHybridSnippet,
tableCellInteractionsSnippet,
tablePaginationSnippet,
tableSelectionActionsSnippet,
tableSelectionModeSnippet,
tableSelectionBehaviorSnippet,
tableSortingSnippet,
columnPropDefs,
rowPropDefs,
cellPropDefs,
} from './table.props';
} from './props-definition';
import {
tableUsageSnippet,
tableBasicSnippet,
tableSelectionActionsSnippet,
tableSelectionModeSnippet,
tableSelectionBehaviorSnippet,
} from './snippets';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { TableDefinition } from '../utils/definitions';
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={<TableSnippet story="TableRockBand" />}
code={tableBasicSnippet}
/>
<Snippet py={4} preview={<TableRockBand />} code={tableBasicSnippet} />
## Usage
@@ -100,7 +98,7 @@ Tables support row selection with two configuration options: selection mode and
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={<TableSnippet story="SelectionModePlayground" />}
preview={<SelectionModePlayground />}
code={tableSelectionModeSnippet}
/>
@@ -109,7 +107,7 @@ Use `selectionMode` to control how many rows can be selected. With `single`, onl
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={<TableSnippet story="SelectionBehaviorPlayground" />}
preview={<SelectionBehaviorPlayground />}
code={tableSelectionBehaviorSnippet}
/>
@@ -120,7 +118,7 @@ With toggle behavior, clicking a row triggers its action when nothing is selecte
With replace behavior, clicking selects the row and double-clicking triggers the action.
<Snippet
preview={<TableSnippet story="SelectionToggleWithActions" />}
preview={<SelectionToggleWithActions />}
code={tableSelectionActionsSnippet}
/>
@@ -266,112 +266,3 @@ export const tablePaginationPropDefs: Record<string, PropDef> = {
...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>`;
@@ -0,0 +1,108 @@
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>`;
@@ -0,0 +1,104 @@
'use client';
import {
Tabs,
TabList,
Tab,
TabPanel,
} from '../../../../../packages/ui/src/components/Tabs';
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
import { MemoryRouter } from 'react-router-dom';
export const Default = () => {
return (
<MemoryRouter>
<Tabs>
<TabList>
<Tab id="tab1">Tab 1</Tab>
<Tab id="tab2">Tab 2</Tab>
<Tab id="tab3">Tab 3</Tab>
</TabList>
<TabPanel id="tab1">
<Text>Content for Tab 1</Text>
</TabPanel>
<TabPanel id="tab2">
<Text>Content for Tab 2</Text>
</TabPanel>
<TabPanel id="tab3">
<Text>Content for Tab 3</Text>
</TabPanel>
</Tabs>
</MemoryRouter>
);
};
export const DefaultSelectedKey = () => {
return (
<MemoryRouter>
<Tabs defaultSelectedKey="tab2">
<TabList>
<Tab id="tab1">Tab 1</Tab>
<Tab id="tab2">Tab 2</Tab>
<Tab id="tab3">Tab 3</Tab>
</TabList>
<TabPanel id="tab1">
<Text>Content for Tab 1</Text>
</TabPanel>
<TabPanel id="tab2">
<Text>Content for Tab 2</Text>
</TabPanel>
<TabPanel id="tab3">
<Text>Content for Tab 3</Text>
</TabPanel>
</Tabs>
</MemoryRouter>
);
};
export const DisabledTabs = () => {
return (
<MemoryRouter>
<Tabs>
<TabList>
<Tab id="tab1">Tab 1</Tab>
<Tab id="tab2" isDisabled>
Tab 2 (Disabled)
</Tab>
<Tab id="tab3">Tab 3</Tab>
</TabList>
<TabPanel id="tab1">
<Text>Content for Tab 1</Text>
</TabPanel>
<TabPanel id="tab2">
<Text>Content for Tab 2</Text>
</TabPanel>
<TabPanel id="tab3">
<Text>Content for Tab 3</Text>
</TabPanel>
</Tabs>
</MemoryRouter>
);
};
export const Orientation = () => {
return (
<MemoryRouter>
<Tabs orientation="vertical">
<TabList>
<Tab id="tab1">Tab 1</Tab>
<Tab id="tab2">Tab 2</Tab>
<Tab id="tab3">Tab 3</Tab>
</TabList>
<TabPanel id="tab1">
<Text>Content for Tab 1</Text>
</TabPanel>
<TabPanel id="tab2">
<Text>Content for Tab 2</Text>
</TabPanel>
<TabPanel id="tab3">
<Text>Content for Tab 3</Text>
</TabPanel>
</Tabs>
</MemoryRouter>
);
};
+80
View File
@@ -0,0 +1,80 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
tabsPropDefs,
tabListPropDefs,
tabPropDefs,
tabPanelPropDefs,
} from './props-definition';
import {
tabsUsageSnippet,
defaultSnippet,
defaultSelectedKeySnippet,
disabledTabsSnippet,
orientationSnippet,
} from './snippets';
import {
Default,
DefaultSelectedKey,
DisabledTabs,
Orientation,
} from './components';
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={defaultSnippet} />
## 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
### Default Selected Key
Here's how to set which tab is selected by default.
<Snippet
py={4}
preview={<DefaultSelectedKey />}
code={defaultSelectedKeySnippet}
open
/>
### Disabled Tabs
Here's how to disable specific tabs.
<Snippet py={4} preview={<DisabledTabs />} code={disabledTabsSnippet} open />
### Orientation
Here's how to display tabs vertically.
<Snippet py={4} preview={<Orientation />} code={orientationSnippet} open />
<Theming definition={TabsDefinition} />
<ChangelogComponent component="tabs" />

Some files were not shown because too many files have changed in this diff Show More