docs(ui): comprehensive documentation improvements for all components

Improves documentation quality and consistency across all UI components.

Props documentation:
- Added descriptions to all props across all components
- Fixed prop types to match source implementations
- Added missing props (onAction, isInvalid, aria-label, etc.)
- Corrected default values and removed non-existent props
- Converted props-definition files to .tsx for Chip component usage

Examples and snippets:
- Added side-by-side layout option to Snippet component
- Fixed snippet/preview mismatches
- Improved example code indentation and formatting
- Added live previews to Container, Box, Grid examples
- Wrapped Skeleton examples in themed Box for visibility

API reference:
- Added ReactAriaLinks to all components
- Updated React Aria URLs to react-aria.adobe.com format
- Cleaned up verbose prose and standardized descriptions

Other:
- Updated PageTitle descriptions to 5-12 word format
- Varied example intro phrasing for consistency

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-01-27 17:19:08 +01:00
parent 268888e656
commit 2a57064226
131 changed files with 3561 additions and 2296 deletions
+20 -9
View File
@@ -28,10 +28,16 @@ import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { AccordionDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ReactAriaLink } from '@/components/ReactAriaLink';
export const reactAriaUrls = {
disclosure: 'https://react-aria.adobe.com/Disclosure',
disclosureGroup: 'https://react-aria.adobe.com/DisclosureGroup',
};
<PageTitle
title="Accordion"
description="A component for showing and hiding content with animation."
description="A collapsible panel with title and subtitle for organizing content into expandable sections."
/>
<Snippet
@@ -54,6 +60,8 @@ Root container for the accordion. Renders a `<div>` element.
<PropsTable data={accordionPropDefs} />
<ReactAriaLink component="Disclosure" href={reactAriaUrls.disclosure} />
### AccordionTrigger
Trigger component with built-in animated chevron icon. Renders a heading element (defaults to `<h3>`, configurable via `level` prop) wrapping a `<button>`.
@@ -72,47 +80,50 @@ Container for managing multiple accordions. Renders a `<div>` element.
<PropsTable data={accordionGroupPropDefs} />
<ReactAriaLink
component="DisclosureGroup"
href={reactAriaUrls.disclosureGroup}
/>
## Examples
### With Subtitle
Here's a view when using both title and subtitle props.
Accordions can display a subtitle below the title.
<Snippet
align="center"
py={4}
height={240}
preview={<WithSubtitle />}
code={withSubtitleSnippet}
layout="side-by-side"
/>
### Custom Trigger
Here's a view when providing custom multi-line content as children.
Pass custom content as children instead of using the title prop.
<Snippet
align="center"
py={4}
height={280}
preview={<CustomTrigger />}
code={customTriggerSnippet}
/>
### Default Expanded
Here's a view when the panel is expanded by default.
<Snippet
align="center"
py={4}
height={280}
preview={<DefaultExpanded />}
code={defaultExpandedSnippet}
layout="side-by-side"
/>
### Group Single Open
Here's a view when only one accordion can be open at a time.
Use `AccordionGroup` to allow only one accordion open at a time.
<Snippet
align="center"
@@ -124,7 +135,7 @@ Here's a view when only one accordion can be open at a time.
### Group Multiple Open
Here's a view when multiple accordions can be open simultaneously.
Allows multiple panels to be open simultaneously.
<Snippet
align="center"
@@ -8,17 +8,22 @@ export const accordionPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['ReactNode', '(state: { isExpanded: boolean }) => ReactNode'],
description:
'Content of the accordion. Can be a render function to access expanded state.',
},
defaultExpanded: {
type: 'boolean',
default: 'false',
description: 'Whether the accordion is expanded on initial render.',
},
isExpanded: {
type: 'boolean',
description: 'Controls the expanded state (controlled mode).',
},
onExpandedChange: {
type: 'enum',
values: ['(isExpanded: boolean) => void'],
description: 'Called when the expanded state changes.',
},
...classNamePropDefs,
...stylePropDefs,
@@ -29,22 +34,33 @@ export const accordionTriggerPropDefs: Record<string, PropDef> = {
type: 'enum',
values: ['1', '2', '3', '4', '5', '6'],
default: '3',
description:
'Heading level for accessibility (renders h1-h6). Match your page hierarchy.',
},
title: {
type: 'string',
description: 'Primary text displayed in the trigger.',
},
subtitle: {
type: 'string',
description: 'Secondary text displayed next to the title.',
},
children: {
type: 'enum',
values: ['ReactNode'],
description:
'Custom trigger content. When provided, title and subtitle are ignored.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const accordionPanelPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['ReactNode'],
description: 'Content displayed when the accordion is expanded.',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -53,6 +69,13 @@ export const accordionGroupPropDefs: Record<string, PropDef> = {
allowsMultiple: {
type: 'boolean',
default: 'false',
description:
'Whether multiple accordions can be expanded at the same time.',
},
children: {
type: 'enum',
values: ['ReactNode'],
description: 'Accordion components to group together.',
},
...classNamePropDefs,
...stylePropDefs,
+12 -2
View File
@@ -17,7 +17,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Avatar"
description="An avatar component with a fallback for initials."
description="A user avatar with multiple sizes and initials fallback when image unavailable."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
@@ -30,13 +30,22 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PropsTable data={avatarPropDefs} />
Avatar also accepts all standard HTML div attributes (`onClick`, `onMouseEnter`, etc.) since it extends `React.ComponentPropsWithoutRef<'div'>`.
## Examples
### Sizes
Avatar sizes can be set using the `size` prop.
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={sizesSnippet}
layout="side-by-side"
/>
### Fallback
@@ -48,6 +57,7 @@ If the image is not available, the avatar will show the initials of the name.
open
preview={<Fallback />}
code={fallbackSnippet}
layout="side-by-side"
/>
### The `purpose` prop
@@ -1,28 +0,0 @@
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,41 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const avatarPropDefs: Record<string, PropDef> = {
src: {
type: 'string',
description:
'URL of the image to display. Pass an empty string to show initials fallback. Falls back to initials if the image fails to load.',
},
name: {
type: 'string',
required: true,
description:
'Name of the person. Used for generating initials fallback and accessibility label.',
},
size: {
type: 'enum',
values: ['x-small', 'small', 'medium', 'large', 'x-large'],
default: 'medium',
responsive: true,
description:
'Visual size. Smaller sizes show 1 initial, larger sizes show 2.',
},
purpose: {
type: 'enum',
values: ['informative', 'decoration'],
default: 'informative',
description: (
<>
Accessibility behavior. Use <Chip>decoration</Chip> when name appears in
adjacent text.
</>
),
},
...classNamePropDefs,
...stylePropDefs,
};
+11 -11
View File
@@ -1,6 +1,6 @@
export const avatarUsageSnippet = `import { Avatar } from '@backstage/ui';
<Avatar />`;
<Avatar src="https://example.com/user.jpg" name="Jane Doe" />`;
export const defaultSnippet = `<Avatar
src="https://avatars.githubusercontent.com/u/1540635?v=4"
@@ -14,18 +14,18 @@ export const fallbackSnippet = `<Avatar
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" />
<Avatar size="x-small" src="..." name="Charles de Dreuille" />
<Avatar size="small" src="..." name="Charles de Dreuille" />
<Avatar size="medium" src="..." name="Charles de Dreuille" />
<Avatar size="large" src="..." name="Charles de Dreuille" />
<Avatar size="x-large" src="..." name="Charles de Dreuille" />
</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="" />
<Avatar size="x-small" src="" name="Charles de Dreuille" />
<Avatar size="small" src="" name="Charles de Dreuille" />
<Avatar size="medium" src="" name="Charles de Dreuille" />
<Avatar size="large" src="" name="Charles de Dreuille" />
<Avatar size="x-large" src="" name="Charles de Dreuille" />
</Flex>
</Flex>`;
+34 -26
View File
@@ -1,34 +1,42 @@
'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)}`;
})();
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
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',
}}
/>
<Box p="4" surface="1">
Hello World
</Box>
);
};
export const Surface = () => {
return (
<Flex direction="column" gap="4">
<Box p="4" surface="0">
Surface 0
</Box>
<Box p="4" surface="1">
Surface 1
</Box>
<Box p="4" surface="2">
Surface 2
</Box>
<Box p="4" surface="3">
Surface 3
</Box>
</Flex>
);
};
export const Responsive = () => {
return (
<Box
p={{ initial: '2', md: '4' }}
display={{ initial: 'block', md: 'flex' }}
>
Hello World
</Box>
);
};
+14 -15
View File
@@ -5,10 +5,10 @@ import { boxPropDefs } from './props-definition';
import {
snippetUsage,
defaultSnippet,
boxSimpleSnippet,
boxSurfaceSnippet,
boxResponsiveSnippet,
} from './snippets';
import { Default } from './components';
import { Default, Surface, Responsive } from './components';
import { spacingPropDefs } from '@/utils/propDefs';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -18,10 +18,10 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Layout"
title="Box"
description="Box is the lowest-level component in Backstage UI. It provides a consistent API for styling and layout."
description="The lowest-level component in Backstage UI with spacing, sizing, and display props."
/>
<Snippet py={4} preview={<Default />} code={defaultSnippet} align="center" />
<Snippet preview={<Default />} code={defaultSnippet} align="center" />
## Usage
@@ -29,11 +29,6 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## API reference
### Box
This is the Box component, our lowest-level component. Here are all the
available properties.
<PropsTable data={boxPropDefs} />
Padding and margin are used to create space around your component using our
@@ -44,17 +39,21 @@ avoid collapsing margins but both are available.
## Examples
### Simple example
### Surface
A simple example of how to use the Box component.
Here's a view when boxes have different surface levels.
<CodeBlock code={boxSimpleSnippet} />
<Snippet preview={<Surface />} code={boxSurfaceSnippet} layout="side-by-side" />
### Responsive
### Responsive props
Here's a view when buttons are responsive.
Props can accept breakpoint objects for responsive behavior.
<CodeBlock code={boxResponsiveSnippet} />
<Snippet
preview={<Responsive />}
code={boxResponsiveSnippet}
layout="side-by-side"
/>
<Theming definition={BoxDefinition} />
@@ -1,24 +0,0 @@
import {
classNamePropDefs,
displayPropDefs,
heightPropDefs,
positionPropDefs,
stylePropDefs,
widthPropDefs,
type PropDef,
} from '@/utils/propDefs';
export const boxPropDefs: Record<string, PropDef> = {
as: {
type: 'enum',
values: ['div', 'span'],
default: 'div',
responsive: true,
},
...widthPropDefs,
...heightPropDefs,
...positionPropDefs,
...displayPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,46 @@
import {
classNamePropDefs,
heightPropDefs,
positionPropDefs,
stylePropDefs,
widthPropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const boxPropDefs: Record<string, PropDef> = {
as: {
type: 'string',
default: 'div',
description:
'HTML element to render. Accepts any valid HTML tag (div, span, section, etc.).',
},
surface: {
type: 'enum',
values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
responsive: true,
description:
'Background surface level for visual hierarchy. Higher numbers create elevation.',
},
children: {
type: 'enum',
values: ['ReactNode'],
description: 'Content to render inside the box.',
},
...widthPropDefs,
...heightPropDefs,
...positionPropDefs,
display: {
type: 'enum',
values: ['none', 'flex', 'block', 'inline'],
responsive: true,
description: (
<>
Controls layout behavior. Use <Chip>flex</Chip> for flexbox layouts,{' '}
<Chip>none</Chip> to hide.
</>
),
},
...classNamePropDefs,
...stylePropDefs,
};
+18 -16
View File
@@ -1,21 +1,23 @@
export const snippetUsage = `import { Box } from '@backstage/ui';
<Box />`;
<Box p="4" surface="1">
Content with padding and background
</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' }}>
export const defaultSnippet = `<Box p="4" surface="1">
Hello World
</Box>`;
export const boxSurfaceSnippet = `<Flex direction="column" gap="4">
<Box p="4" surface="0">Surface 0</Box>
<Box p="4" surface="1">Surface 1</Box>
<Box p="4" surface="2">Surface 2</Box>
<Box p="4" surface="3">Surface 3</Box>
</Flex>`;
export const boxResponsiveSnippet = `<Box
p={{ initial: '2', md: '4' }}
display={{ initial: 'block', md: 'flex' }}
>
Hello World
</Box>`;
@@ -14,10 +14,15 @@ import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ButtonIconDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ReactAriaLink } from '@/components/ReactAriaLink';
export const reactAriaUrls = {
button: 'https://react-aria.adobe.com/Button',
};
<PageTitle
title="ButtonIcon"
description="A button component with a single icon that can be used to trigger actions."
description="An icon-only button with primary, secondary, and tertiary variants."
/>
<Snippet align="center" py={4} preview={<Variants />} code={variantsSnippet} />
@@ -30,12 +35,12 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PropsTable data={buttonIconPropDefs} />
<ReactAriaLink component="Button" href={reactAriaUrls.button} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
@@ -46,14 +51,10 @@ Here's a view when buttons have different variants.
### Sizes
Here's a view when buttons have different sizes.
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
@@ -64,7 +65,7 @@ Here's a view when buttons are disabled.
### Loading
Here's a view when buttons are in a loading state.
Shows a spinner during async operations.
<Snippet
align="center"
@@ -1,31 +0,0 @@
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,64 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const buttonIconPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary', 'tertiary'],
default: 'primary',
responsive: true,
description: (
<>
Visual style. Use <Chip>primary</Chip> for main actions,{' '}
<Chip>secondary</Chip> for alternatives, <Chip>tertiary</Chip> for
low-emphasis.
</>
),
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
description: (
<>
Button size. Use <Chip>small</Chip> for toolbars, <Chip>medium</Chip>{' '}
for standalone actions.
</>
),
},
icon: {
type: 'enum',
values: ['ReactElement'],
description:
'Icon element to display. Required for accessibility via aria-label.',
},
isDisabled: {
type: 'boolean',
default: 'false',
description: 'Prevents interaction and applies disabled styling.',
},
loading: {
type: 'boolean',
default: 'false',
description: 'Shows a spinner and disables the button.',
},
type: {
type: 'enum',
values: ['button', 'submit', 'reset'],
default: 'button',
description: 'HTML button type attribute.',
},
onSurface: {
type: 'enum',
values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
responsive: true,
description: 'Surface context for correct color contrast.',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -1,6 +1,7 @@
export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/ui';
import { RiCloseLine } from '@remixicon/react';
<ButtonIcon />`;
<ButtonIcon icon={<RiCloseLine />} aria-label="Close" />`;
export const variantsSnippet = `<Flex align="center" gap="2">
<ButtonIcon icon={<RiCloudLine />} variant="primary" aria-label="Cloud" />
@@ -14,10 +14,15 @@ import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ButtonLinkDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ReactAriaLink } from '@/components/ReactAriaLink';
export const reactAriaUrls = {
link: 'https://react-aria.adobe.com/Link',
};
<PageTitle
title="ButtonLink"
description="A button component that can be used as a link."
description="A button-styled link with primary, secondary, and tertiary variants."
/>
<Snippet align="center" py={4} preview={<Variants />} code={variantsSnippet} />
@@ -30,30 +35,16 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PropsTable data={buttonLinkPropDefs} />
<ReactAriaLink component="Link" href={reactAriaUrls.link} />
## 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}
@@ -64,8 +55,6 @@ Here's a view when buttons have icons.
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
@@ -1,32 +0,0 @@
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,74 @@
import {
classNamePropDefs,
stylePropDefs,
childrenPropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const buttonLinkPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary', 'tertiary'],
default: 'primary',
responsive: true,
description: (
<>
Visual style. Use <Chip>primary</Chip> for main actions,{' '}
<Chip>secondary</Chip> for alternatives, <Chip>tertiary</Chip> for
low-emphasis.
</>
),
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
description: (
<>
Link size. Use <Chip>small</Chip> for inline contexts,{' '}
<Chip>medium</Chip> for standalone.
</>
),
},
iconStart: {
type: 'enum',
values: ['ReactElement'],
description: 'Icon displayed before the link text.',
},
iconEnd: {
type: 'enum',
values: ['ReactElement'],
description: 'Icon displayed after the link text.',
},
isDisabled: {
type: 'boolean',
default: 'false',
description: 'Prevents interaction and applies disabled styling.',
},
href: {
type: 'string',
required: true,
description: 'URL the link navigates to.',
},
target: {
type: 'enum',
values: ['_self', '_blank', '_parent', '_top'],
description: (
<>
Where to open the linked URL. Use <Chip>_blank</Chip> for external
links.
</>
),
},
onSurface: {
type: 'enum',
values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
responsive: true,
description: 'Surface context for correct color contrast.',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
+31 -12
View File
@@ -16,6 +16,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ButtonDefinition } from '../../../utils/definitions';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import {
Variants,
Sizes,
@@ -25,9 +26,13 @@ import {
AsLink,
} from './components';
export const reactAriaUrls = {
button: 'https://react-spectrum.adobe.com/react-aria/Button.html',
};
<PageTitle
title="Button"
description="A button component that can be used to trigger actions."
description="A button with primary, secondary, and tertiary variants and loading state."
/>
<Snippet align="center" py={4} preview={<Variants />} code={variantsSnippet} />
@@ -40,29 +45,35 @@ import {
<PropsTable data={buttonPropDefs} />
<ReactAriaLink component="Button" href={reactAriaUrls.button} />
## Examples
### Variants
Here's a view when buttons have different variants.
<Snippet
align="center"
py={4}
open
preview={<Variants />}
code={variantsSnippet}
layout="side-by-side"
/>
### Sizes
Here's a view when buttons have different sizes.
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={sizesSnippet}
layout="side-by-side"
/>
### With Icons
Here's a view when buttons have icons.
Icons can appear before or after the label.
<Snippet
align="center"
@@ -70,23 +81,23 @@ Here's a view when buttons have icons.
open
preview={<WithIcons />}
code={withIconsSnippet}
layout="side-by-side"
/>
### Disabled
Here's a view when buttons are disabled.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={disabledSnippet}
layout="side-by-side"
/>
### Loading
Here's a view when buttons are in a loading state.
Shows a spinner and disables interaction during async operations.
<Snippet
align="center"
@@ -94,11 +105,12 @@ Here's a view when buttons are in a loading state.
open
preview={<Loading />}
code={loadingSnippet}
layout="side-by-side"
/>
### Responsive
Here's a view when buttons are responsive.
Button props accept responsive breakpoint objects.
<CodeBlock code={buttonResponsiveSnippet} />
@@ -106,7 +118,14 @@ 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={<AsLink />} code={asLinkSnippet} />
<Snippet
align="center"
py={4}
open
preview={<AsLink />}
code={asLinkSnippet}
layout="side-by-side"
/>
<Theming definition={ButtonDefinition} />
@@ -1,30 +0,0 @@
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,69 @@
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const buttonPropDefs: Record<string, PropDef> = {
variant: {
type: 'enum',
values: ['primary', 'secondary', 'tertiary'],
default: 'primary',
responsive: true,
description: (
<>
Visual style. Use <Chip>primary</Chip> for main actions,{' '}
<Chip>secondary</Chip> for alternatives, <Chip>tertiary</Chip> for
low-emphasis.
</>
),
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
description: (
<>
Button size. Use <Chip>small</Chip> for dense layouts.
</>
),
},
iconStart: {
type: 'enum',
values: ['ReactElement'],
description: 'Icon displayed before the button text.',
},
iconEnd: {
type: 'enum',
values: ['ReactElement'],
description: 'Icon displayed after the button text.',
},
isDisabled: {
type: 'boolean',
default: 'false',
description: 'Prevents interaction and applies disabled styling.',
},
loading: {
type: 'boolean',
default: 'false',
description: 'Shows a spinner and disables the button.',
},
children: {
type: 'enum',
values: ['ReactNode'],
description: 'Button label text or content.',
},
type: {
type: 'enum',
values: ['button', 'submit', 'reset'],
default: 'button',
description: 'HTML button type attribute.',
},
onSurface: {
type: 'enum',
values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
responsive: true,
description: 'Surface context for correct color contrast.',
},
...classNamePropDefs,
...stylePropDefs,
};
+36 -36
View File
@@ -1,54 +1,54 @@
export const buttonSnippetUsage = `import { Button } from '@backstage/ui';
<Button />`;
<Button>Click me</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>`;
<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>`;
<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>`;
<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>`;
<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>`;
Load more items
</Button>`;
export const asLinkSnippet = `<MemoryRouter>
<ButtonLink href="https://ui.backstage.io" target="_blank">
Button
</ButtonLink>
</MemoryRouter>`;
<ButtonLink href="https://ui.backstage.io" target="_blank">
Button
</ButtonLink>
</MemoryRouter>`;
@@ -18,7 +18,7 @@ export const Default = () => {
);
};
export const CustomSize = () => {
export const HeaderAndBody = () => {
return (
<Card
style={{
@@ -27,8 +27,7 @@ export const CustomSize = () => {
}}
>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
<CardBody>Body content without a footer</CardBody>
</Card>
);
};
+15 -16
View File
@@ -10,10 +10,10 @@ import {
import {
cardUsageSnippet,
defaultSnippet,
customSizeSnippet,
headerAndBodySnippet,
withLongBodySnippet,
} from './snippets';
import { Default, CustomSize, WithLongBody } from './components';
import { Default, HeaderAndBody, WithLongBody } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { CardDefinition } from '../../../utils/definitions';
@@ -21,7 +21,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Card"
description="A card component that can be used to display content in a box."
description="A container with header, body, and footer sections for grouped content."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
@@ -32,54 +32,53 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## API reference
### Card
All Card components extend `HTMLDivElement` attributes.
A card component that can be used to display content in a box.
### Card
<PropsTable data={cardPropDefs} />
### CardHeader
To display a header in a card, use the `CardHeader` component. This will be fixed at the top of the card.
Fixed at the top of the card.
<PropsTable data={cardHeaderPropDefs} />
### CardBody
To display content in a card, use the `CardBody` component. This will automatically fill the card.
Scrollable content area that fills available space.
<PropsTable data={cardBodyPropDefs} />
### CardFooter
To display a footer in a card, use the `CardFooter` component. This will be fixed at the bottom of the card.
Fixed at the bottom of the card.
<PropsTable data={cardFooterPropDefs} />
## Examples
### Custom size
### Header and body only
Here's a view when card has a custom size.
Cards can omit the footer section.
<Snippet
align="center"
py={4}
preview={<CustomSize />}
code={customSizeSnippet}
open
layout="side-by-side"
preview={<HeaderAndBody />}
code={headerAndBodySnippet}
/>
### With long body
### Scrollable body
Here's a view when card has a long body.
When body content exceeds the available height, CardBody scrolls while header and footer remain fixed.
<Snippet
align="center"
py={4}
preview={<WithLongBody />}
code={withLongBodySnippet}
open
/>
<Theming definition={CardDefinition} />
@@ -1,30 +1,38 @@
import {
classNamePropDefs,
stylePropDefs,
childrenPropDefs,
type PropDef,
} from '@/utils/propDefs';
const optionalChildrenPropDef: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['ReactNode'],
responsive: false,
description: 'Content to display inside the component.',
},
};
export const cardPropDefs: Record<string, PropDef> = {
...childrenPropDefs,
...optionalChildrenPropDef,
...classNamePropDefs,
...stylePropDefs,
};
export const cardHeaderPropDefs: Record<string, PropDef> = {
...childrenPropDefs,
...optionalChildrenPropDef,
...classNamePropDefs,
...stylePropDefs,
};
export const cardBodyPropDefs: Record<string, PropDef> = {
...childrenPropDefs,
...optionalChildrenPropDef,
...classNamePropDefs,
...stylePropDefs,
};
export const cardFooterPropDefs: Record<string, PropDef> = {
...childrenPropDefs,
...optionalChildrenPropDef,
...classNamePropDefs,
...stylePropDefs,
};
+14 -7
View File
@@ -6,33 +6,40 @@ export const cardUsageSnippet = `import { Card, CardHeader, CardBody, CardFooter
<CardFooter>Footer</CardFooter>
</Card>`;
export const defaultSnippet = `<Card>
export const defaultSnippet = `<Card style={{ width: '300px', height: '200px' }}>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
</Card>`;
export const customSizeSnippet = `<Card style={{ width: '300px', height: '200px' }}>
export const headerAndBodySnippet = `<Card style={{ width: '300px', height: '200px' }}>
<CardHeader>Header</CardHeader>
<CardBody>Body</CardBody>
<CardFooter>Footer</CardFooter>
<CardBody>Body content without a footer</CardBody>
</Card>`;
export const withLongBodySnippet = `<Card style={{ width: '300px', height: '200px' }}>
export const withLongBodySnippet = `import { Text } from '@backstage/ui';
<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 component handles extensive content. The card should adjust
accordingly to display all the text properly while maintaining its
structure.
</Text>
<Text>
Here's a second paragraph that adds more content to our card body.
Having multiple paragraphs helps to visualize how spacing works within
the card component.
</Text>
<Text>
This third paragraph continues to add more text to ensure we have a
proper demonstration of a card with significant content.
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>
+8 -3
View File
@@ -12,10 +12,15 @@ import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { CheckboxDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ReactAriaLink } from '@/components/ReactAriaLink';
export const reactAriaUrls = {
checkbox: 'https://react-aria.adobe.com/Checkbox',
};
<PageTitle
title="Checkbox"
description="A checkbox component that can be used to trigger actions."
description="A selectable checkbox with label for boolean choices and form input."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
@@ -28,12 +33,12 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PropsTable data={checkboxPropDefs} />
<ReactAriaLink component="Checkbox" href={reactAriaUrls.checkbox} />
## Examples
### All variants
Here's a view when checkboxes have different variants.
<Snippet
align="center"
py={4}
@@ -7,41 +7,42 @@ import {
export const checkboxPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['React.ReactNode'],
responsive: false,
values: ['ReactNode'],
required: true,
description: 'Label displayed next to the checkbox.',
},
isSelected: {
type: 'enum',
values: ['boolean'],
responsive: false,
type: 'boolean',
description: 'Controls checked state (controlled mode).',
},
defaultSelected: {
type: 'enum',
values: ['boolean'],
responsive: false,
type: 'boolean',
description: 'Initial checked state (uncontrolled mode).',
},
onChange: {
type: 'enum',
values: ['(isSelected: boolean) => void'],
responsive: false,
description: 'Called when the checked state changes.',
},
isDisabled: {
type: 'enum',
values: ['boolean'],
responsive: false,
type: 'boolean',
description: 'Prevents interaction and applies disabled styling.',
},
isRequired: {
type: 'enum',
values: ['boolean'],
responsive: false,
type: 'boolean',
description: 'Marks the checkbox as required for form validation.',
},
isIndeterminate: {
type: 'boolean',
description: 'Shows a mixed state, typically for "select all" checkboxes.',
},
name: {
type: 'string',
responsive: false,
description: 'Name attribute for form submission.',
},
value: {
type: 'string',
responsive: false,
description: 'Value attribute for form submission.',
},
...classNamePropDefs,
...stylePropDefs,
@@ -1,24 +1,33 @@
'use client';
import { Container } from '../../../../../packages/ui/src/components/Container/Container';
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")',
}}
/>
);
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
export const Preview = () => {
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<DecorativeBox />
</div>
<Container py="4">
<Text>Content is centered with max-width</Text>
</Container>
);
};
export const BasicUsagePreview = () => {
return (
<Container>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Container>
);
};
export const ResponsiveSpacingPreview = () => {
return (
<Container py={{ initial: '2', md: '4' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
</Container>
);
};
+19 -10
View File
@@ -8,7 +8,11 @@ import {
containerSimpleSnippet,
containerResponsiveSnippet,
} from './snippets';
import { Preview } from './components';
import {
Preview,
BasicUsagePreview,
ResponsiveSpacingPreview,
} from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ContainerDefinition } from '../../../utils/definitions';
@@ -17,7 +21,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Layout"
title="Container"
description="The container component let you use our default max-width and center the content on the page."
description="A centered layout wrapper with max-width and vertical spacing props."
/>
<Snippet py={4} preview={<Preview />} code={previewSnippet} />
@@ -32,18 +36,23 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## Examples
### Simple
### Basic Usage
A simple example of how to use the Container component.
<Snippet
preview={<BasicUsagePreview />}
code={containerSimpleSnippet}
layout="side-by-side"
/>
<CodeBlock code={containerSimpleSnippet} />
### Responsive Spacing
### Responsive padding & margin
Spacing props accept breakpoint objects for responsive behavior.
The Container component also supports responsive values, making it easy to
create responsive designs.
<CodeBlock code={containerResponsiveSnippet} />
<Snippet
preview={<ResponsiveSpacingPreview />}
code={containerResponsiveSnippet}
layout="side-by-side"
/>
<Theming definition={ContainerDefinition} />
@@ -1,12 +1,52 @@
import {
classNamePropDefs,
stylePropDefs,
gapPropDefs,
spacingValues,
type PropDef,
} from '@/utils/propDefs';
export const containerPropDefs: Record<string, PropDef> = {
...gapPropDefs,
children: {
type: 'enum',
values: ['ReactNode'],
description: 'Content to render inside the container.',
},
my: {
type: 'enum | string',
values: spacingValues,
responsive: true,
description: 'Vertical margin (top and bottom).',
},
mt: {
type: 'enum | string',
values: spacingValues,
responsive: true,
description: 'Top margin.',
},
mb: {
type: 'enum | string',
values: spacingValues,
responsive: true,
description: 'Bottom margin.',
},
py: {
type: 'enum | string',
values: spacingValues,
responsive: true,
description: 'Vertical padding (top and bottom).',
},
pt: {
type: 'enum | string',
values: spacingValues,
responsive: true,
description: 'Top padding.',
},
pb: {
type: 'enum | string',
values: spacingValues,
responsive: true,
description: 'Bottom padding.',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -2,9 +2,9 @@ export const containerUsageSnippet = `import { Container } from "@backstage/ui";
<Container>Hello World!</Container>`;
export const previewSnippet = `<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<DecorativeBox />
</div>`;
export const previewSnippet = `<Container py="4">
Content is centered with max-width
</Container>`;
export const containerSimpleSnippet = `<Container>
<Box>Hello World</Box>
@@ -12,7 +12,7 @@ export const containerSimpleSnippet = `<Container>
<Box>Hello World</Box>
</Container>`;
export const containerResponsiveSnippet = `<Container paddingY={{ xs: 'sm', md: 'md' }}>
export const containerResponsiveSnippet = `<Container py={{ initial: '2', md: '4' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
@@ -58,6 +58,20 @@ export const PreviewFixedWidthAndHeight = () => (
quae ab illo inventore veritatis et quasi architecto beatae vitae
dicta sunt explicabo.
</Text>
<Text>
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut
fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt.
</Text>
<Text>
Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet,
consectetur, adipisci velit, sed quia non numquam eius modi tempora
incidunt ut labore et dolore magnam aliquam quaerat voluptatem.
</Text>
<Text>
Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis
suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur.
</Text>
</Flex>
</DialogBody>
<DialogFooter>
+17 -3
View File
@@ -12,7 +12,6 @@ import {
dialogHeaderPropDefs,
dialogBodyPropDefs,
dialogFooterPropDefs,
dialogClosePropDefs,
} from './props-definition';
import {
dialogUsageSnippet,
@@ -26,10 +25,16 @@ import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { DialogDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ReactAriaLink } from '@/components/ReactAriaLink';
export const reactAriaUrls = {
dialogTrigger: 'https://react-aria.adobe.com/Modal#dialogtrigger',
modal: 'https://react-aria.adobe.com/Modal',
};
<PageTitle
title="Dialog"
description="A modal dialog component that displays content in an overlay window."
description="A modal dialog with header, body, and footer sections supporting controlled state."
/>
<Snippet
@@ -51,18 +56,27 @@ Wraps a trigger element and the dialog content to handle open/close state.
<PropsTable data={dialogTriggerPropDefs} />
<ReactAriaLink component="DialogTrigger" href={reactAriaUrls.dialogTrigger} />
### Dialog
The main dialog container that renders as a modal overlay.
<PropsTable data={dialogPropDefs} />
<ReactAriaLink component="Modal" href={reactAriaUrls.modal} />
### DialogHeader
Displays the dialog title with a built-in close button.
<PropsTable data={dialogHeaderPropDefs} />
<ReactAriaLink
component="Heading"
href="https://react-aria.adobe.com/Heading"
/>
### DialogBody
The main content area of the dialog with optional scrolling.
@@ -94,7 +108,7 @@ Dialog with a fixed height body that scrolls when content overflows.
### Dialog with Form
Dialog containing form elements for user input.
Forms can be embedded in the dialog body.
<Snippet
align="center"
@@ -5,7 +5,11 @@ import {
} from '@/utils/propDefs';
export const dialogTriggerPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
children: {
type: 'enum',
values: ['ReactNode'],
description: 'Trigger element and dialog content.',
},
isOpen: {
type: 'boolean',
description: 'Whether the overlay is open by default (controlled).',
@@ -23,66 +27,66 @@ export const dialogTriggerPropDefs: Record<string, PropDef> = {
};
export const dialogPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
children: {
type: 'enum',
values: ['ReactNode'],
description: 'Dialog content (DialogHeader, DialogBody, DialogFooter).',
},
isOpen: {
type: 'boolean',
description: 'Whether the overlay is open by default (controlled).',
description: 'Whether the overlay is open (controlled mode).',
},
defaultOpen: {
type: 'boolean',
description: 'Whether the overlay is open by default (uncontrolled).',
description: 'Initial open state (uncontrolled mode).',
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
description:
"Handler that is called when the overlay's open state changes.",
description: 'Called when the open state changes.',
},
width: {
type: 'enum',
values: ['number', 'string'],
responsive: false,
default: '400',
description: 'Fixed width in pixels (number) or CSS units (string).',
},
height: {
type: 'enum',
values: ['number', 'string'],
responsive: false,
default: 'auto',
description: 'Fixed height in pixels (number) or CSS units (string).',
},
...classNamePropDefs,
...stylePropDefs,
};
export const dialogHeaderPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
children: {
type: 'enum',
values: ['ReactNode'],
description: 'Dialog title text.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const dialogBodyPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
height: {
children: {
type: 'enum',
values: ['number', 'string'],
responsive: false,
values: ['ReactNode'],
description: 'Main content of the dialog.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const dialogFooterPropDefs: Record<string, PropDef> = {
children: { type: 'enum', values: ['ReactNode'], responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
export const dialogClosePropDefs: Record<string, PropDef> = {
variant: {
children: {
type: 'enum',
values: ['primary', 'secondary', 'tertiary'],
default: 'secondary',
responsive: false,
values: ['ReactNode'],
description: 'Action buttons or footer content.',
},
children: { type: 'enum', values: ['ReactNode'], responsive: false },
...classNamePropDefs,
...stylePropDefs,
};
@@ -53,11 +53,14 @@ export const dialogWithFormSnippet = `<DialogTrigger>
<Flex direction="column" gap="3">
<TextField label="Name" placeholder="Enter full name" />
<TextField label="Email" placeholder="Enter email address" />
<Select label="Role">
<SelectItem>Admin</SelectItem>
<SelectItem>User</SelectItem>
<SelectItem>Viewer</SelectItem>
</Select>
<Select
label="Role"
options={[
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' },
{ value: 'viewer', label: 'Viewer' },
]}
/>
</Flex>
</DialogBody>
<DialogFooter>
+48 -27
View File
@@ -3,36 +3,57 @@
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { Box } from '../../../../../packages/ui/src/components/Box/Box';
const boxStyle = {
width: '64px',
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
};
const smallBoxStyle = {
padding: '8px 12px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
};
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',
}}
/>
<Box style={boxStyle} />
<Box style={boxStyle} />
<Box style={boxStyle} />
</Flex>
);
};
export const DirectionExample = () => {
return (
<Flex direction="column" gap="2">
<Box style={smallBoxStyle}>First</Box>
<Box style={smallBoxStyle}>Second</Box>
<Box style={smallBoxStyle}>Third</Box>
</Flex>
);
};
export const ResponsiveExample = () => {
return (
<Flex gap={{ initial: '2', md: '4' }}>
<Box style={smallBoxStyle}>Item 1</Box>
<Box style={smallBoxStyle}>Item 2</Box>
<Box style={smallBoxStyle}>Item 3</Box>
</Flex>
);
};
export const AlignExample = () => {
return (
<Flex align="center" justify="between" gap="4" style={{ width: '300px' }}>
<Box style={smallBoxStyle}>Start</Box>
<Box style={smallBoxStyle}>Middle</Box>
<Box style={smallBoxStyle}>End</Box>
</Flex>
);
};
+34 -17
View File
@@ -5,11 +5,16 @@ import { flexPropDefs } from './props-definition';
import {
flexUsageSnippet,
defaultSnippet,
flexSimpleSnippet,
flexResponsiveSnippet,
flexAlignSnippet,
flexDirectionSnippet,
} from './snippets';
import { Default } from './components';
import {
Default,
ResponsiveExample,
AlignExample,
DirectionExample,
} from './components';
import { spacingPropDefs } from '@/utils/propDefs';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -19,7 +24,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Layout"
title="Flex"
description="A responsive flex container component for vertical stacking with customizable gaps."
description="A flex container with gap, alignment, and direction props."
/>
<Snippet py={4} align="center" preview={<Default />} code={defaultSnippet} />
@@ -32,31 +37,43 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PropsTable data={flexPropDefs} />
The grid component also accepts all the spacing props from the Box component.
The Flex component also accepts all the spacing props from the Box component.
<PropsTable data={spacingPropDefs} />
## Examples
### Simple
### Direction
A simple example of how to use the Flex component.
<Snippet
py={4}
align="center"
preview={<DirectionExample />}
code={flexDirectionSnippet}
layout="side-by-side"
/>
<CodeBlock code={flexSimpleSnippet} />
### Responsive gap
### Responsive
Gap values can be responsive using breakpoint objects.
The Flex component also supports responsive values, making it easy to create
responsive designs.
<Snippet
py={4}
align="center"
preview={<ResponsiveExample />}
code={flexResponsiveSnippet}
layout="side-by-side"
/>
<CodeBlock code={flexResponsiveSnippet} />
### Alignment
### Align
The Flex component also supports responsive alignment, making it easy to
create responsive designs.
<CodeBlock code={flexAlignSnippet} />
<Snippet
py={4}
align="center"
preview={<AlignExample />}
code={flexAlignSnippet}
layout="side-by-side"
/>
<Theming definition={FlexDefinition} />
@@ -1,27 +0,0 @@
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,64 @@
import {
classNamePropDefs,
stylePropDefs,
gapPropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const flexPropDefs: Record<string, PropDef> = {
direction: {
type: 'enum',
values: ['row', 'column', 'row-reverse', 'column-reverse'],
responsive: true,
description: (
<>
Main axis direction. Use <Chip>row</Chip> for horizontal,{' '}
<Chip>column</Chip> for vertical layouts.
</>
),
},
align: {
type: 'enum',
values: ['start', 'center', 'end', 'baseline', 'stretch'],
responsive: true,
description:
'Cross-axis alignment. Controls how children align perpendicular to the main axis.',
},
justify: {
type: 'enum',
values: ['start', 'center', 'end', 'between'],
responsive: true,
description: (
<>
Main-axis distribution. Use <Chip>between</Chip> to space children
evenly with no edge gaps.
</>
),
},
gap: {
...gapPropDefs.gap,
default: '4',
description:
'Space between children. Accepts spacing scale values or responsive objects.',
},
surface: {
type: 'enum',
values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
responsive: true,
description: (
<>
Surface level for theming. Use <Chip>auto</Chip> to increment from
parent context.
</>
),
},
children: {
type: 'enum',
values: ['ReactNode'],
responsive: false,
description: 'Content to render inside the flex container.',
},
...classNamePropDefs,
...stylePropDefs,
};
+16 -12
View File
@@ -1,6 +1,10 @@
export const flexUsageSnippet = `import { Flex } from '@backstage/ui';
<Flex />`;
<Flex gap="4">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Flex>`;
export const defaultSnippet = `<Flex gap="4">
<Box style={{ width: '64px', height: '64px' }} />
@@ -8,20 +12,20 @@ export const defaultSnippet = `<Flex gap="4">
<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>
export const flexDirectionSnippet = `<Flex direction="column" gap="2">
<Box>First</Box>
<Box>Second</Box>
<Box>Third</Box>
</Flex>`;
export const flexResponsiveSnippet = `<Flex gap={{ initial: '2', md: '4' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Item 1</Box>
<Box>Item 2</Box>
<Box>Item 3</Box>
</Flex>`;
export const flexAlignSnippet = `<Flex align={{ initial: 'start', md: 'center' }}>
<Box>Hello World</Box>
<Box>Hello World</Box>
<Box>Hello World</Box>
export const flexAlignSnippet = `<Flex align="center" justify="between" gap="4">
<Box>Start</Box>
<Box>Middle</Box>
<Box>End</Box>
</Flex>`;
+43 -48
View File
@@ -3,57 +3,52 @@
import { Grid } from '../../../../../packages/ui/src/components/Grid/Grid';
import { Box } from '../../../../../packages/ui/src/components/Box/Box';
const boxStyle = {
height: '64px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
};
const labelBoxStyle = {
padding: '8px 12px',
background: '#eaf2fd',
border: '1px solid #2563eb',
borderRadius: '4px',
};
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',
}}
/>
<Box style={boxStyle} />
<Box style={boxStyle} />
<Box style={boxStyle} />
<Box style={boxStyle} />
<Box style={boxStyle} />
<Box style={boxStyle} />
</Grid.Root>
);
};
export const ResponsiveExample = () => {
return (
<Grid.Root columns={{ initial: '2', md: '4' }} gap="4">
<Box style={labelBoxStyle}>1</Box>
<Box style={labelBoxStyle}>2</Box>
<Box style={labelBoxStyle}>3</Box>
<Box style={labelBoxStyle}>4</Box>
</Grid.Root>
);
};
export const GridItemExample = () => {
return (
<Grid.Root columns="4" gap="4">
<Grid.Item colSpan="2" style={labelBoxStyle}>
Spans 2 columns
</Grid.Item>
<Box style={labelBoxStyle}>1 column</Box>
<Box style={labelBoxStyle}>1 column</Box>
</Grid.Root>
);
};
+36 -8
View File
@@ -1,9 +1,14 @@
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 { gridPropDefs, gridItemPropDefs } from './props-definition';
import {
gridUsageSnippet,
defaultSnippet,
gridResponsiveSnippet,
gridItemSnippet,
} from './snippets';
import { Default, ResponsiveExample, GridItemExample } from './components';
import { spacingPropDefs } from '@/utils/propDefs';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -13,7 +18,7 @@ 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."
description="A CSS grid container with columns, gap, and item spanning."
/>
<Snippet py={4} preview={<Default />} code={defaultSnippet} />
@@ -26,16 +31,39 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
### 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.
The grid container. Defines column count and gap between items.
<PropsTable data={gridPropDefs} />
The grid component also accepts all the spacing props from the Box component.
Grid.Root also accepts all spacing props from the Box component.
<PropsTable data={spacingPropDefs} />
### Grid.Item
A grid child with column and row spanning control.
<PropsTable data={gridItemPropDefs} />
## Examples
### Responsive columns
Column count can change at different breakpoints.
<Snippet
py={4}
preview={<ResponsiveExample />}
code={gridResponsiveSnippet}
open
/>
### Column spanning
Use Grid.Item to span multiple columns.
<Snippet py={4} preview={<GridItemExample />} code={gridItemSnippet} open />
<Theming definition={GridDefinition} />
<ChangelogComponent component="grid" />
@@ -1,22 +0,0 @@
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,113 @@
import {
childrenPropDefs,
classNamePropDefs,
gapPropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
const columnValues = [
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12',
'auto',
];
const surfaceValues = [
'0',
'1',
'2',
'3',
'danger',
'warning',
'success',
'auto',
];
export const gridPropDefs: Record<string, PropDef> = {
columns: {
type: 'enum',
values: columnValues,
default: 'auto',
responsive: true,
description: (
<>
Number of columns. Use 1-12 for fixed layouts, <Chip>auto</Chip> to fit
content.
</>
),
},
gap: {
...gapPropDefs.gap,
default: '4',
description:
'Space between items. Use higher values for separated layouts, lower for compact.',
},
surface: {
type: 'enum',
values: surfaceValues,
responsive: true,
description: (
<>
Surface level for theming. Use <Chip>auto</Chip> to increment from
parent context.
</>
),
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const gridItemPropDefs: Record<string, PropDef> = {
colSpan: {
type: 'enum',
values: columnValues,
responsive: true,
description: 'Number of columns the item spans across.',
},
colStart: {
type: 'enum',
values: columnValues,
responsive: true,
description:
'Starting column position. Use with colEnd for explicit placement.',
},
colEnd: {
type: 'enum',
values: columnValues,
responsive: true,
description:
'Ending column position. Use with colStart for explicit placement.',
},
rowSpan: {
type: 'enum',
values: columnValues,
responsive: true,
description: 'Number of rows the item spans. Useful for tall content.',
},
surface: {
type: 'enum',
values: surfaceValues,
responsive: true,
description: (
<>
Surface level for theming. Use <Chip>auto</Chip> to increment from
parent context.
</>
),
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
+18 -1
View File
@@ -1,6 +1,10 @@
export const gridUsageSnippet = `import { Grid } from '@backstage/ui';
<Grid />`;
<Grid.Root columns="3" gap="4">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Grid.Root>`;
export const defaultSnippet = `<Grid.Root columns="3" gap="4">
<Box style={{ height: '64px' }} />
@@ -10,3 +14,16 @@ export const defaultSnippet = `<Grid.Root columns="3" gap="4">
<Box style={{ height: '64px' }} />
<Box style={{ height: '64px' }} />
</Grid.Root>`;
export const gridResponsiveSnippet = `<Grid.Root columns={{ initial: '2', md: '4' }} gap="4">
<Box>1</Box>
<Box>2</Box>
<Box>3</Box>
<Box>4</Box>
</Grid.Root>`;
export const gridItemSnippet = `<Grid.Root columns="4" gap="4">
<Grid.Item colSpan="2">Spans 2 columns</Grid.Item>
<Grid.Item>1 column</Grid.Item>
<Grid.Item>1 column</Grid.Item>
</Grid.Root>`;
@@ -19,22 +19,6 @@ const tabs = [
{ 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' },
@@ -49,8 +33,8 @@ export const WithEverything = () => (
<MemoryRouter>
<HeaderPage
title="Page Title"
tabs={tabs}
breadcrumbs={breadcrumbs}
tabs={tabs.slice(0, 2)}
breadcrumbs={breadcrumbs.slice(0, 2)}
customActions={
<>
<Button variant="secondary">Secondary</Button>
@@ -63,13 +47,13 @@ export const WithEverything = () => (
export const WithLongBreadcrumbs = () => (
<MemoryRouter>
<HeaderPage title="Page Title" breadcrumbs={breadcrumbs} />
<HeaderPage title="Page Title" breadcrumbs={breadcrumbs.slice(0, 2)} />
</MemoryRouter>
);
export const WithTabs = () => (
<MemoryRouter>
<HeaderPage title="Page Title" tabs={tabs} />
<HeaderPage title="Page Title" tabs={tabs.slice(0, 3)} />
</MemoryRouter>
);
@@ -82,7 +66,7 @@ export const WithCustomActions = () => (
</MemoryRouter>
);
export const WithMenuItems = () => (
export const WithMenu = () => (
<MemoryRouter>
<HeaderPage
title="Page Title"
@@ -90,15 +74,8 @@ export const WithMenuItems = () => (
<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>
))}
<MenuItem href="/settings">Settings</MenuItem>
<MenuItem onAction={() => alert('logout')}>Logout</MenuItem>
</Menu>
</MenuTrigger>
}
+14 -21
View File
@@ -6,16 +6,16 @@ import {
WithLongBreadcrumbs,
WithTabs,
WithCustomActions,
WithMenuItems,
WithMenu,
} from './components';
import { propDefs } from './props-definition';
import { headerPagePropDefs } from './props-definition';
import {
usage,
defaultSnippet,
withTabs,
withBreadcrumbs,
withCustomActions,
withMenuItems,
withMenu,
} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -23,9 +23,8 @@ import { HeaderPageDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Component"
title="HeaderPage"
description="A header page component for the plugin that should sit under the Header component."
description="A secondary header with title, breadcrumbs, tabs, and actions."
/>
<Snippet py={4} preview={<WithEverything />} code={defaultSnippet} />
@@ -36,38 +35,32 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## API reference
<PropsTable data={propDefs} />
<PropsTable data={headerPagePropDefs} />
## Examples
### With Breadcrumbs
### Breadcrumbs
You can add breadcrumbs to the header page to help users navigate to the previous page. The `breadcrumbs`
prop is an array of objects with a `label` and `href` property. By default we truncate the breadcrumb label to 240px.
Labels are truncated at 240px.
<Snippet open preview={<WithLongBreadcrumbs />} code={withBreadcrumbs} />
### With Tabs
### 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.
Tabs use React Router and highlight based on the current route.
<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.
### Custom actions
<Snippet open preview={<WithCustomActions />} code={withCustomActions} />
### With Menu Items
### With menu
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.
Use `customActions` to add a dropdown menu.
<Snippet open preview={<WithMenuItems />} code={withMenuItems} />
<Snippet open preview={<WithMenu />} code={withMenu} />
<Theming definition={HeaderPageDefinition} />
<ChangelogComponent component="Header" />
<ChangelogComponent component="header-page" />
@@ -1,89 +0,0 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const propDefs: Record<string, PropDef> = {
title: {
type: 'string',
default: 'Your plugin',
},
customActions: {
type: 'enum',
values: ['ReactNode'],
},
menuItems: {
type: 'complex',
complexType: {
name: 'MenuItem[]',
properties: {
label: {
type: 'string',
required: true,
description: 'Display text for the menu item',
},
value: {
type: 'string',
required: true,
description: 'Unique value for the menu item',
},
onClick: {
type: '() => void',
required: false,
description: 'Callback function when menu item is clicked',
},
},
},
},
tabs: {
type: 'complex',
complexType: {
name: 'HeaderTab[]',
properties: {
id: {
type: 'string',
required: true,
description: 'Unique identifier for the tab',
},
label: {
type: 'string',
required: true,
description: 'Display text for the tab',
},
href: {
type: 'string',
required: false,
description: 'URL to navigate to when tab is clicked',
},
matchStrategy: {
type: "'exact' | 'prefix'",
required: false,
description: 'How to match the current route to highlight the tab',
},
},
},
},
breadcrumbs: {
type: 'complex',
complexType: {
name: 'Breadcrumb[]',
properties: {
label: {
type: 'string',
required: true,
description: 'Display text for the breadcrumb',
},
href: {
type: 'string',
required: true,
description: 'URL for the breadcrumb link',
},
},
},
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,69 @@
import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const headerPagePropDefs: Record<string, PropDef> = {
title: {
type: 'string',
description: 'Page heading displayed in the header.',
},
customActions: {
type: 'enum',
values: ['ReactNode'],
description: 'Custom elements rendered in the actions area.',
},
tabs: {
type: 'complex',
description: 'Navigation tabs displayed below the title.',
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: true,
description: 'URL to navigate to when tab is clicked.',
},
matchStrategy: {
type: "'exact' | 'prefix'",
required: false,
default: "'exact'",
description: (
<>
Route matching strategy. Use <Chip>exact</Chip> for exact path
match, <Chip>prefix</Chip> if pathname starts with href.
</>
),
},
},
},
},
breadcrumbs: {
type: 'complex',
description: 'Breadcrumb trail displayed above the title.',
complexType: {
name: 'HeaderPageBreadcrumb[]',
properties: {
label: {
type: 'string',
required: true,
description: 'Display text for the breadcrumb. Truncated at 240px.',
},
href: {
type: 'string',
required: true,
description: 'URL for the breadcrumb link.',
},
},
},
},
...classNamePropDefs,
};
@@ -1,21 +1,23 @@
export const usage = `import { HeaderPage } from '@backstage/ui';
<HeaderPage />`;
<HeaderPage title="Page Title" />`;
export const defaultSnippet = `<HeaderPage
title="Page Title"
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
]}
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'settings', label: 'Settings', href: '/settings' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
]}
customActions={<Button>Custom action</Button>}
customActions={
<>
<Button variant="secondary">Secondary</Button>
<Button variant="primary">Primary</Button>
</>
}
/>`;
export const withBreadcrumbs = `<HeaderPage
@@ -32,8 +34,6 @@ export const withTabs = `<HeaderPage
{ 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' },
]}
/>`;
@@ -42,10 +42,15 @@ export const withCustomActions = `<HeaderPage
customActions={<Button>Custom action</Button>}
/>`;
export const withMenuItems = `<HeaderPage
export const withMenu = `<HeaderPage
title="Page Title"
menuItems={[
{ label: 'Settings', value: 'settings', onClick: () => {} },
{ label: 'Invite new members', value: 'invite-new-members', onClick: () => {} },
]}
customActions={
<MenuTrigger>
<ButtonIcon variant="tertiary" icon={<RiMore2Line />} />
<Menu placement="bottom end">
<MenuItem href="/settings">Settings</MenuItem>
<MenuItem onAction={() => {}}>Logout</MenuItem>
</Menu>
</MenuTrigger>
}
/>`;
@@ -20,20 +20,16 @@ const tabs = [
];
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' },
{ id: 'banana', label: 'Banana', href: '/banana' },
{ id: 'apple', label: 'Apple', href: '/apple' },
{ id: 'orange', label: 'Orange', href: '/orange' },
];
export const WithAllOptionsAndTabs = () => (
<MemoryRouter>
<Header
title="My plugin"
titleLink="/"
tabs={tabs}
customActions={
<>
@@ -49,6 +45,8 @@ export const WithAllOptionsAndTabs = () => (
export const WithAllOptions = () => (
<MemoryRouter>
<Header
title="My plugin"
titleLink="/"
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
@@ -60,30 +58,14 @@ export const WithAllOptions = () => (
</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 />} />
</>
}
/>
<Header title="My plugin" titleLink="/" tabs={tabs.slice(0, 2)} />
<HeaderPage
title="Page title"
tabs={tabs2}
customActions={<Button>Custom action</Button>}
breadcrumbs={breadcrumbs}
/>
</>
</MemoryRouter>
+7 -24
View File
@@ -4,16 +4,14 @@ import { Snippet } from '@/components/Snippet';
import {
WithAllOptionsAndTabs,
WithAllOptions,
WithBreadcrumbs,
WithHeaderPage,
} from './components';
import { propDefs } from './props-definition';
import { headerPropDefs } from './props-definition';
import {
usage,
simple,
defaultSnippet,
withTabs,
withBreadcrumbs,
withHeaderPage,
} from './snippets';
import { PageTitle } from '@/components/PageTitle';
@@ -22,9 +20,8 @@ import { HeaderDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Component"
title="Header"
description="A header component for the plugin."
description="A plugin header with icon, title, custom actions, and navigation tabs."
/>
<Snippet py={4} preview={<WithAllOptionsAndTabs />} code={defaultSnippet} />
@@ -35,40 +32,26 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## API reference
<PropsTable data={propDefs} />
<PropsTable data={headerPropDefs} />
## Examples
### Simple header
A simple example of how to use the Header component.
<Snippet py={4} preview={<WithAllOptions />} code={simple} open />
### Header with tabs
A simple example of how to use the Header component with tabs. All links are using React Router
under the hood and will be active when you are on the corresponding page.
Tabs use React Router and highlight automatically based on the current route.
<Snippet
preview={<WithAllOptionsAndTabs />}
code={withTabs}
open
/>
### Header with breadcrumbs
Breacrumbs should appear when you scroll down (and not directly visible as it is in the demo below).
<Snippet preview={<WithBreadcrumbs />} code={withBreadcrumbs} open />
<Snippet preview={<WithAllOptionsAndTabs />} code={withTabs} open />
### Header with HeaderPage
You can use the `Header` component inside the [HeaderPage](/components/header-page) component to compose your multi-level navigation.
Combine with [HeaderPage](/components/header-page) for multi-level navigation.
<Snippet preview={<WithHeaderPage />} code={withHeaderPage} open />
<Theming definition={HeaderDefinition} />
<ChangelogComponent component="Header" />
<ChangelogComponent component="header" />
@@ -1,83 +0,0 @@
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,
};
@@ -0,0 +1,63 @@
import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const headerPropDefs: Record<string, PropDef> = {
icon: {
type: 'enum',
values: ['ReactNode'],
description: 'Icon displayed before the title.',
},
title: {
type: 'string',
description: 'Main heading text for the header.',
},
titleLink: {
type: 'string',
description: 'URL the title links to when clicked.',
},
customActions: {
type: 'enum',
values: ['ReactNode'],
description: 'Custom elements rendered in the toolbar area.',
},
tabs: {
type: 'complex',
description: 'Navigation tabs displayed below the toolbar.',
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: true,
description: 'URL to navigate to when tab is clicked.',
},
matchStrategy: {
type: "'exact' | 'prefix'",
required: false,
description: (
<>
Route matching strategy. Use <Chip>exact</Chip> for exact path
match, <Chip>prefix</Chip> if pathname starts with href.
</>
),
},
},
},
},
onTabSelectionChange: {
type: 'enum',
values: ['(key: Key) => void'],
description: 'Handler called when the selected tab changes.',
},
...classNamePropDefs,
};
+18 -73
View File
@@ -1,25 +1,14 @@
export const usage = `import { Header } from '@backstage/ui';
<Header />`;
<Header title="My plugin" />`;
export const defaultSnippet = `<Header
title="My plugin"
titleLink="/"
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Settings', href: '/settings' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'checks', label: 'Checks', href: '/checks' },
{ id: 'tracks', label: 'Tracks', href: '/tracks' },
]}
customActions={
<>
@@ -33,17 +22,6 @@ export const defaultSnippet = `<Header
export const simple = `<Header
title="My plugin"
titleLink="/"
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
]}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
@@ -53,64 +31,31 @@ export const simple = `<Header
}
/>`;
export const withTabs = `
<Header
export const withTabs = `<Header
title="My plugin"
titleLink="/"
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
]}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
/>
`;
export const withBreadcrumbs = `<Header
title="My plugin"
titleLink="/"
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Settings', href: '/settings' },
]}
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
{ 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 withHeaderPage = `<Header
title="My plugin"
titleLink="/"
breadcrumbs={...}
tabs={...}
tabs={[
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'checks', label: 'Checks', href: '/checks' },
]}
/>
<HeaderPage
title="Page title"
menuItems={...}
tabs={...}
tabs={[
{ id: 'banana', label: 'Banana', href: '/banana' },
{ id: 'apple', label: 'Apple', href: '/apple' },
]}
customActions={<Button>Custom action</Button>}
/>`;
+33 -49
View File
@@ -26,29 +26,29 @@ export const AllVariants = () => {
return (
<MemoryRouter>
<Flex gap="4" direction="column">
<Link href="https://ui.backstage.io" variant="title-large">
Sign up for Backstage
<Link href="#" variant="title-large">
title-large
</Link>
<Link href="https://ui.backstage.io" variant="title-medium">
Sign up for Backstage
<Link href="#" variant="title-medium">
title-medium
</Link>
<Link href="https://ui.backstage.io" variant="title-small">
Sign up for Backstage
<Link href="#" variant="title-small">
title-small
</Link>
<Link href="https://ui.backstage.io" variant="title-x-small">
Sign up for Backstage
<Link href="#" variant="title-x-small">
title-x-small
</Link>
<Link href="https://ui.backstage.io" variant="body-large">
Sign up for Backstage
<Link href="#" variant="body-large">
body-large
</Link>
<Link href="https://ui.backstage.io" variant="body-medium">
Sign up for Backstage
<Link href="#" variant="body-medium">
body-medium
</Link>
<Link href="https://ui.backstage.io" variant="body-small">
Sign up for Backstage
<Link href="#" variant="body-small">
body-small
</Link>
<Link href="https://ui.backstage.io" variant="body-x-small">
Sign up for Backstage
<Link href="#" variant="body-x-small">
body-x-small
</Link>
</Flex>
</MemoryRouter>
@@ -59,51 +59,35 @@ 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 href="#" color="primary">
Primary
</Link>
<Link
href="https://ui.backstage.io"
variant="title-small"
color="secondary"
>
I am secondary
<Link href="#" color="secondary">
Secondary
</Link>
<Link
href="https://ui.backstage.io"
variant="title-small"
color="tertiary"
>
I am tertiary
<Link href="#" color="danger">
Danger
</Link>
<Link
href="https://ui.backstage.io"
variant="title-small"
color="inherit"
>
I am inherit
<Link href="#" color="warning">
Warning
</Link>
<Link href="#" color="success">
Success
</Link>
</Flex>
</MemoryRouter>
);
};
export const Underline = () => {
export const Weight = () => {
return (
<MemoryRouter>
<Flex gap="4" direction="column">
<Link href="https://ui.backstage.io" underline="always">
Always underlined
<Flex gap="4">
<Link href="#" weight="regular">
Regular
</Link>
<Link href="https://ui.backstage.io" underline="hover">
Underlined on hover
</Link>
<Link href="https://ui.backstage.io" underline="none">
Never underlined
<Link href="#" weight="bold">
Bold
</Link>
</Flex>
</MemoryRouter>
+15 -21
View File
@@ -1,6 +1,7 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import { linkPropDefs } from './props-definition';
import {
linkUsageSnippet,
@@ -8,23 +9,27 @@ import {
externalLinkSnippet,
allVariantsSnippet,
allColorsSnippet,
underlineSnippet,
weightSnippet,
} from './snippets';
import {
Default,
ExternalLink,
AllVariants,
AllColors,
Underline,
Weight,
} from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { LinkDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
link: 'https://react-aria.adobe.com/Link',
};
<PageTitle
title="Link"
description="A link component that renders a `<a>` element."
description="A styled anchor with typography variants, colors, and weights."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
@@ -37,55 +42,44 @@ import { LinkDefinition } from '../../../utils/definitions';
<PropsTable data={linkPropDefs} />
<ReactAriaLink component="Link" href={reactAriaUrls.link} />
## Examples
### External Link
### External link
Here's how to create an external link that opens in a new tab.
Use `target="_blank"` to open links 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.
Status colors for contextual links.
<Snippet
align="center"
py={4}
open
preview={<AllColors />}
code={allColorsSnippet}
/>
### Underline
### Weight
Here's a view when links have different underline styles.
<Snippet
align="center"
py={4}
open
preview={<Underline />}
code={underlineSnippet}
/>
<Snippet align="center" py={4} preview={<Weight />} code={weightSnippet} />
<Theming definition={LinkDefinition} />
@@ -1,45 +0,0 @@
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,73 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const linkPropDefs: Record<string, PropDef> = {
href: {
type: 'string',
description:
'URL the link navigates to. Supports internal and external URLs.',
},
target: {
type: 'enum',
values: ['_self', '_blank', '_parent', '_top'],
description: (
<>
Where to open the link. Use <Chip>_blank</Chip> for external links that
open in new tabs.
</>
),
},
title: {
type: 'string',
description: 'Tooltip text shown on hover. Useful for accessibility.',
},
variant: {
type: 'enum',
values: [
'title-large',
'title-medium',
'title-small',
'title-x-small',
'body-large',
'body-medium',
'body-small',
'body-x-small',
],
default: 'body',
responsive: true,
description:
'Typography style. Title variants for headings, body for paragraph text.',
},
weight: {
type: 'enum',
values: ['regular', 'bold'],
default: 'regular',
responsive: true,
description: (
<>
Font weight. Use <Chip>bold</Chip> for emphasis.
</>
),
},
color: {
type: 'enum',
values: ['primary', 'secondary', 'danger', 'warning', 'success'],
default: 'primary',
responsive: true,
description:
'Text color. Status colors (danger, warning, success) for contextual links.',
},
truncate: {
type: 'boolean',
description:
'Truncates text with ellipsis when it overflows its container.',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
+16 -37
View File
@@ -9,46 +9,25 @@ export const externalLinkSnippet = `<Link href="https://backstage.io" target="_b
</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>
<Link href="#" variant="title-large">title-large</Link>
<Link href="#" variant="title-medium">title-medium</Link>
<Link href="#" variant="title-small">title-small</Link>
<Link href="#" variant="title-x-small">title-x-small</Link>
<Link href="#" variant="body-large">body-large</Link>
<Link href="#" variant="body-medium">body-medium</Link>
<Link href="#" variant="body-small">body-small</Link>
<Link href="#" variant="body-x-small">body-x-small</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>
<Link href="#" color="primary">Primary</Link>
<Link href="#" color="secondary">Secondary</Link>
<Link href="#" color="danger">Danger</Link>
<Link href="#" color="warning">Warning</Link>
<Link href="#" color="success">Success</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>
export const weightSnippet = `<Flex gap="4">
<Link href="#" weight="regular">Regular</Link>
<Link href="#" weight="bold">Bold</Link>
</Flex>`;
+29 -17
View File
@@ -8,6 +8,7 @@ import {
MenuSeparator,
SubmenuTrigger,
MenuAutocomplete,
MenuAutocompleteListbox,
MenuListBoxItem,
} from '../../../../../packages/ui/src/components/Menu/Menu';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
@@ -54,10 +55,9 @@ export const PreviewSubmenu = () => (
<MenuItem>New File</MenuItem>
<SubmenuTrigger>
<MenuItem>Open Recent</MenuItem>
<Menu>
<Menu placement="right top">
<MenuItem>File 1.txt</MenuItem>
<MenuItem>File 2.txt</MenuItem>
<MenuItem>File 3.txt</MenuItem>
</Menu>
</SubmenuTrigger>
<MenuItem>Save</MenuItem>
@@ -128,30 +128,42 @@ export const PreviewSeparators = () => (
export const PreviewAutocompleteMenu = () => (
<MemoryRouter>
<MenuAutocomplete placeholder="Type to search...">
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
<MenuItem>Option 3</MenuItem>
</MenuAutocomplete>
<MenuTrigger>
<Button variant="secondary">Search</Button>
<MenuAutocomplete placeholder="Type to search...">
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
<MenuItem>Option 3</MenuItem>
</MenuAutocomplete>
</MenuTrigger>
</MemoryRouter>
);
export const PreviewAutocompleteListbox = () => (
<MemoryRouter>
<MenuAutocomplete placeholder="Type to filter...">
<MenuListBoxItem>Option 1</MenuListBoxItem>
<MenuListBoxItem>Option 2</MenuListBoxItem>
<MenuListBoxItem>Option 3</MenuListBoxItem>
</MenuAutocomplete>
<MenuTrigger>
<Button variant="secondary">Select</Button>
<MenuAutocompleteListbox placeholder="Type to filter...">
<MenuListBoxItem>Option 1</MenuListBoxItem>
<MenuListBoxItem>Option 2</MenuListBoxItem>
<MenuListBoxItem>Option 3</MenuListBoxItem>
</MenuAutocompleteListbox>
</MenuTrigger>
</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>
<MenuTrigger>
<Button variant="secondary">Multi-select</Button>
<MenuAutocompleteListbox
placeholder="Type to filter..."
selectionMode="multiple"
>
<MenuListBoxItem>Option 1</MenuListBoxItem>
<MenuListBoxItem>Option 2</MenuListBoxItem>
<MenuListBoxItem>Option 3</MenuListBoxItem>
</MenuAutocompleteListbox>
</MenuTrigger>
</MemoryRouter>
);
+60 -63
View File
@@ -40,10 +40,21 @@ import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { MenuDefinition } from '../../../utils/definitions';
import { ReactAriaLink } from '@/components/ReactAriaLink';
export const reactAriaUrls = {
menuTrigger: 'https://react-aria.adobe.com/Menu#menutrigger',
submenuTrigger: 'https://react-aria.adobe.com/Menu#submenutrigger',
menu: 'https://react-aria.adobe.com/Menu',
menuItem: 'https://react-aria.adobe.com/Menu#menuitem',
menuSection: 'https://react-aria.adobe.com/Menu#menusection',
listBox: 'https://react-aria.adobe.com/ListBox',
listBoxItem: 'https://react-aria.adobe.com/ListBox#listboxitem',
};
<PageTitle
title="Menu"
description="A list of actions in a dropdown, enhanced with keyboard navigation."
description="A dropdown menu with actions, submenus, sections, and keyboard navigation."
/>
<Snippet align="center" py={4} preview={<Preview />} code={preview} />
@@ -54,100 +65,103 @@ import { MenuDefinition } from '../../../utils/definitions';
### Triggers
- `MenuTrigger` is a wrapper component that combines a button or other trigger element with a menu displayed in a popover.
- `SubmenuTrigger` is a wrapper component that combines a `MenuItem` with a menu displayed in a popover.
- `MenuTrigger` combines a trigger element with a menu popover.
- `SubmenuTrigger` combines a `MenuItem` with a nested submenu.
### Containers
- `Menu` is a container component that contains a list of menu items or sections.
- `MenuListBox` is a container component that contains a list of menu items or sections.
- `MenuAutocomplete` is a container component that contains a list of menu items or sections.
- `MenuAutocompleteListbox` is a container component that contains a list of menu items or sections.
- `Menu` contains menu items or sections.
- `MenuListBox` supports selection with checkmarks.
- `MenuAutocomplete` adds a search input to filter items.
- `MenuAutocompleteListbox` combines search with selection.
### Items
- `MenuItem` is an individual interactive item in the menu.
- `MenuListBoxItem` is an individual interactive item in the menu list box.
- `MenuItem` is an interactive action in the menu.
- `MenuListBoxItem` is a selectable item in a list box.
### Separators
### Grouping
- `MenuSeparator` is a component that renders a horizontal line to separate menu items.
- `MenuSection` is a component that renders a section in the menu.
- `MenuSection` groups items with a title.
- `MenuSeparator` adds a horizontal divider.
## API reference
### MenuTrigger
`MenuTrigger` accepts exactly two children: the first child should be the trigger element, and second child should be
one of the menu containers containing the menu.
Accepts two children: the trigger element and a menu container.
<PropsTable data={menuTriggerPropDefs} />
<ReactAriaLink component="MenuTrigger" href={reactAriaUrls.menuTrigger} />
### SubmenuTrigger
The `SubmenuTrigger` accepts exactly two children: the first child should be the `MenuItem` which triggers opening
of the submenu, and second child should be one of the menu containers containing the submenu.
Accepts two children: a `MenuItem` and a menu container.
<PropsTable data={submenuTriggerPropDefs} />
### Menu
<ReactAriaLink component="SubmenuTrigger" href={reactAriaUrls.submenuTrigger} />
`Menu` is a container component that contains a list of menu items or sections.
### Menu
<PropsTable data={menuPropDefs} />
### MenuListBox
<ReactAriaLink component="Menu" href={reactAriaUrls.menu} />
`MenuListBox` is a container component that contains a list of menu items or sections.
### MenuListBox
<PropsTable data={menuListBoxPropDefs} />
### MenuAutocomplete
<ReactAriaLink component="ListBox" href={reactAriaUrls.listBox} />
`MenuAutocomplete` is a container component that contains a list of menu items or sections.
### MenuAutocomplete
<PropsTable data={menuAutocompletePropDefs} />
### MenuAutocompleteListbox
<ReactAriaLink component="Menu" href={reactAriaUrls.menu} />
`MenuAutocompleteListbox` is a container component that contains a list of menu items or sections.
### MenuAutocompleteListbox
<PropsTable data={menuAutocompleteListboxPropDefs} />
### MenuItem
<ReactAriaLink component="ListBox" href={reactAriaUrls.listBox} />
`MenuItem` is an individual interactive item in the menu.
### MenuItem
<PropsTable data={menuItemPropDefs} />
### MenuListBoxItem
<ReactAriaLink component="MenuItem" href={reactAriaUrls.menuItem} />
`MenuListBoxItem` is an individual interactive item in the menu list box.
### MenuListBoxItem
<PropsTable data={menuListBoxItemPropDefs} />
### MenuSection
<ReactAriaLink component="ListBoxItem" href={reactAriaUrls.listBoxItem} />
`MenuSection` is a component that renders a section in the menu.
### MenuSection
<PropsTable data={menuSectionPropDefs} />
<ReactAriaLink component="MenuSection" href={reactAriaUrls.menuSection} />
### MenuSeparator
`MenuSeparator` is a component that renders a horizontal line to separate menu items.
<PropsTable data={menuSeparatorPropDefs} />
<ReactAriaLink
component="Separator"
href="https://react-aria.adobe.com/Menu#separator"
/>
## Examples
### Nested navigation
You can nest menus to create a more complex navigation structure. It is important to use the `placement` prop to ensure
the submenu is displayed in the correct position. The best practice is to use the `right top` placement for the submenu.
Submenus open to the right of their parent item.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<PreviewSubmenu />}
code={submenu}
@@ -155,24 +169,18 @@ the submenu is displayed in the correct position. The best practice is to use th
### With icons
You can use the `iconStart` prop to add an icon to the menu item.
<Snippet align="center" py={4} open preview={<PreviewIcons />} code={icons} />
<Snippet layout="side-by-side" 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.
The `href` prop works with both internal and external links.
<Snippet align="center" py={4} open preview={<PreviewLinks />} code={links} />
<Snippet layout="side-by-side" open preview={<PreviewLinks />} code={links} />
### With sections
You can use the `MenuSection` component to add a section to the menu.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<PreviewSections />}
code={sections}
@@ -180,11 +188,8 @@ You can use the `MenuSection` component to add a section to the menu.
### With separators
You can use the `MenuSeparator` component to add a separator to the menu.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<PreviewSeparators />}
code={separators}
@@ -192,11 +197,8 @@ You can use the `MenuSeparator` component to add a separator to the menu.
### With autocomplete
You can use the `MenuAutocomplete` component to add a autocomplete to the menu.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<PreviewAutocompleteMenu />}
code={autocomplete}
@@ -204,24 +206,19 @@ You can use the `MenuAutocomplete` component to add a autocomplete to the menu.
### With list box
You can use the `MenuListBox` component to add a list box to the menu.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<PreviewAutocompleteListbox />}
code={autocompleteListbox}
/>
### With list box with multiple selection
### Multiple selection
You can use the `MenuListBox` component to add a list box to the menu. You can also use the `selectionMode` prop to
allow multiple selection.
Set `selectionMode="multiple"` to allow multiple selections.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<PreviewAutocompleteListboxMultiple />}
code={autocompleteListboxMultiple}
@@ -1,241 +0,0 @@
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,
};
@@ -0,0 +1,364 @@
import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const menuTriggerPropDefs: Record<string, PropDef> = {
isOpen: {
type: 'boolean',
description: 'Controlled open state of the menu.',
},
defaultOpen: {
type: 'boolean',
description: 'Whether the menu is open by default.',
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
description: 'Handler called when the open state changes.',
},
};
export const submenuTriggerPropDefs: Record<string, PropDef> = {
delay: {
type: 'number',
default: '200',
description: 'Delay in milliseconds before the submenu opens on hover.',
},
};
export const menuPropDefs: 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',
],
description: 'Position of the menu relative to the trigger.',
},
onAction: {
type: 'enum',
values: ['(key: Key) => void'],
description:
'Handler called when an item is activated. Receives the item key.',
},
selectionMode: {
type: 'enum',
values: ['none', 'single', 'multiple'],
description: 'How items can be selected.',
},
selectedKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Controlled selected keys.',
},
defaultSelectedKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Default selected keys for uncontrolled usage.',
},
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Keys of items that are disabled.',
},
virtualized: {
type: 'boolean',
default: false,
description: 'Enable virtualization for large lists.',
},
maxWidth: {
type: 'string',
description: 'Maximum width of the menu popover.',
},
maxHeight: {
type: 'string',
description: 'Maximum height of the menu popover.',
},
...classNamePropDefs,
};
export const menuListBoxPropDefs: 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',
],
description: 'Position of the list box relative to the trigger.',
},
selectionMode: {
type: 'enum',
values: ['none', 'single', 'multiple'],
description: 'How items can be selected.',
},
selectedKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Controlled selected keys.',
},
defaultSelectedKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Default selected keys for uncontrolled usage.',
},
onSelectionChange: {
type: 'enum',
values: ['(keys: Selection) => void'],
description: 'Handler called when selection changes.',
},
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Keys of items that are disabled.',
},
virtualized: {
type: 'boolean',
default: false,
description: 'Enable virtualization for large lists.',
},
maxWidth: {
type: 'string',
description: 'Maximum width of the list box popover.',
},
maxHeight: {
type: 'string',
description: 'Maximum height of the list box popover.',
},
...classNamePropDefs,
};
export const menuAutocompletePropDefs: Record<string, PropDef> = {
placeholder: {
type: 'string',
description: 'Placeholder text for the search input.',
},
placement: {
type: 'enum',
values: [
'top',
'bottom',
'left',
'right',
'top start',
'top end',
'bottom start',
'bottom end',
'left start',
'left end',
'right start',
'right end',
],
description: 'Position of the menu relative to the trigger.',
},
onAction: {
type: 'enum',
values: ['(key: Key) => void'],
description:
'Handler called when an item is activated. Receives the item key.',
},
selectionMode: {
type: 'enum',
values: ['none', 'single', 'multiple'],
description: 'How items can be selected.',
},
selectedKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Controlled selected keys.',
},
defaultSelectedKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Default selected keys for uncontrolled usage.',
},
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Keys of items that are disabled.',
},
onSelectionChange: {
type: 'enum',
values: ['(keys: Selection) => void'],
description: 'Handler called when selection changes.',
},
virtualized: {
type: 'boolean',
default: false,
description: 'Enable virtualization for large lists.',
},
maxWidth: {
type: 'string',
description: 'Maximum width of the menu popover.',
},
maxHeight: {
type: 'string',
description: 'Maximum height of the menu popover.',
},
...classNamePropDefs,
};
export const menuAutocompleteListboxPropDefs: Record<string, PropDef> = {
placeholder: {
type: 'string',
description: 'Placeholder text for the search input.',
},
placement: {
type: 'enum',
values: [
'top',
'bottom',
'left',
'right',
'top start',
'top end',
'bottom start',
'bottom end',
'left start',
'left end',
'right start',
'right end',
],
description: 'Position of the list box relative to the trigger.',
},
selectionMode: {
type: 'enum',
values: ['none', 'single', 'multiple'],
description: 'How items can be selected.',
},
selectedKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Controlled selected keys.',
},
defaultSelectedKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Default selected keys for uncontrolled usage.',
},
onSelectionChange: {
type: 'enum',
values: ['(keys: Selection) => void'],
description: 'Handler called when selection changes.',
},
virtualized: {
type: 'boolean',
default: false,
description: 'Enable virtualization for large lists.',
},
maxWidth: {
type: 'string',
description: 'Maximum width of the list box popover.',
},
maxHeight: {
type: 'string',
description: 'Maximum height of the list box popover.',
},
...classNamePropDefs,
};
export const menuItemPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['ReactNode'],
required: true,
description: 'Content displayed in the menu item.',
},
id: {
type: 'enum',
values: ['Key'],
description: 'Unique key for the item.',
},
iconStart: {
type: 'enum',
values: ['ReactNode'],
description: 'Icon displayed before the item content.',
},
color: {
type: 'enum',
values: ['primary', 'danger'],
description: (
<>
Color variant. Use <Chip>danger</Chip> for destructive actions.
</>
),
},
href: {
type: 'string',
description: 'URL to navigate to when the item is clicked.',
},
isDisabled: {
type: 'boolean',
description: 'Whether the item is disabled.',
},
textValue: {
type: 'string',
description: 'Text used for typeahead and accessibility.',
},
onAction: {
type: 'enum',
values: ['() => void'],
description: 'Handler called when the item is activated.',
},
...classNamePropDefs,
};
export const menuListBoxItemPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['ReactNode'],
required: true,
description: 'Content displayed in the list box item.',
},
id: {
type: 'enum',
values: ['Key'],
description: 'Unique key for the item.',
},
textValue: {
type: 'string',
description: 'Text used for typeahead and accessibility.',
},
isDisabled: {
type: 'boolean',
description: 'Whether the item is disabled.',
},
...classNamePropDefs,
};
export const menuSectionPropDefs: Record<string, PropDef> = {
title: {
type: 'string',
required: true,
description: 'Heading displayed above the section.',
},
children: {
type: 'enum',
values: ['ReactNode'],
required: true,
description: 'Menu items within the section.',
},
...classNamePropDefs,
};
export const menuSeparatorPropDefs: Record<string, PropDef> = {
...classNamePropDefs,
};
+47 -28
View File
@@ -9,12 +9,23 @@ export const usage = `import { MenuTrigger, Menu, MenuItem } from '@backstage/ui
</MenuTrigger>`;
export const preview = `<MenuTrigger>
<Button variant="secondary">Menu</Button>
<Button>Menu</Button>
<Menu>
<MenuItem>New File</MenuItem>
<MenuItem>Open File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem>Save As...</MenuItem>
<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>`;
@@ -24,7 +35,7 @@ export const submenu = `<MenuTrigger>
<MenuItem>New File</MenuItem>
<SubmenuTrigger>
<MenuItem>Open Recent</MenuItem>
<Menu>
<Menu placement="right top">
<MenuItem>File 1.txt</MenuItem>
<MenuItem>File 2.txt</MenuItem>
</Menu>
@@ -36,9 +47,9 @@ export const submenu = `<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>
<MenuItem iconStart={<RiFileLine />}>New File</MenuItem>
<MenuItem iconStart={<RiFolderLine />}>New Folder</MenuItem>
<MenuItem iconStart={<RiImageLine />}>New Image</MenuItem>
</Menu>
</MenuTrigger>`;
@@ -77,24 +88,32 @@ export const links = `<MenuTrigger>
</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 autocomplete = `<MenuTrigger>
<Button variant="secondary">Search</Button>
<MenuAutocomplete placeholder="Type to search...">
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
<MenuItem>Option 3</MenuItem>
</MenuAutocomplete>
</MenuTrigger>`;
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 autocompleteListbox = `<MenuTrigger>
<Button variant="secondary">Select</Button>
<MenuAutocompleteListbox placeholder="Type to filter...">
<MenuListBoxItem>Option 1</MenuListBoxItem>
<MenuListBoxItem>Option 2</MenuListBoxItem>
<MenuListBoxItem>Option 3</MenuListBoxItem>
</MenuAutocompleteListbox>
</MenuTrigger>`;
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>`;
export const autocompleteListboxMultiple = `<MenuTrigger>
<Button variant="secondary">Multi-select</Button>
<MenuAutocompleteListbox
placeholder="Type to filter..."
selectionMode="multiple"
>
<MenuListBoxItem>Option 1</MenuListBoxItem>
<MenuListBoxItem>Option 2</MenuListBoxItem>
<MenuListBoxItem>Option 3</MenuListBoxItem>
</MenuAutocompleteListbox>
</MenuTrigger>`;
@@ -2,6 +2,7 @@
import { PasswordField } from '../../../../../packages/ui/src/components/PasswordField/PasswordField';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { RiLockLine } from '@remixicon/react';
export const WithLabel = () => {
return (
@@ -15,7 +16,11 @@ export const WithLabel = () => {
export const Sizes = () => {
return (
<Flex direction="row" gap="4" style={{ width: '100%', maxWidth: '600px' }}>
<Flex
direction="column"
gap="4"
style={{ width: '100%', maxWidth: '300px' }}
>
<PasswordField name="password" label="Password" size="small" />
<PasswordField name="password" label="Password" size="medium" />
</Flex>
@@ -32,3 +37,27 @@ export const WithDescription = () => {
/>
);
};
export const WithIcon = () => {
return (
<PasswordField
name="password"
label="Password"
icon={<RiLockLine />}
style={{ maxWidth: '300px' }}
/>
);
};
export const Validation = () => {
return (
<PasswordField
name="password"
label="Password"
isRequired
isInvalid
description="Password must be at least 8 characters"
style={{ maxWidth: '300px' }}
/>
);
};
@@ -1,22 +1,35 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { inputPropDefs } from './props-definition';
import { passwordFieldPropDefs } from './props-definition';
import {
passwordFieldUsageSnippet,
withLabelSnippet,
sizesSnippet,
withDescriptionSnippet,
withIconSnippet,
validationSnippet,
} from './snippets';
import { WithLabel, Sizes, WithDescription } from './components';
import {
WithLabel,
Sizes,
WithDescription,
WithIcon,
Validation,
} 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 { ReactAriaLink } from '@/components/ReactAriaLink';
export const reactAriaUrls = {
textField: 'https://react-aria.adobe.com/TextField',
};
<PageTitle
title="PasswordField"
description="A password field component for your forms."
description="A password input with visibility toggle, icon, and validation support."
/>
<Snippet
@@ -32,28 +45,43 @@ import { PasswordFieldDefinition } from '../../../utils/definitions';
## API reference
<PropsTable data={inputPropDefs} />
<PropsTable data={passwordFieldPropDefs} />
<ReactAriaLink component="TextField" href={reactAriaUrls.textField} />
## Examples
### Sizes
We support two different sizes: `small`, `medium`.
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
<Snippet layout="side-by-side" open preview={<Sizes />} code={sizesSnippet} />
### With description
Here's a simple PasswordField with a description.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<WithDescription />}
code={withDescriptionSnippet}
/>
### With icon
<Snippet
layout="side-by-side"
open
preview={<WithIcon />}
code={withIconSnippet}
/>
### Validation
<Snippet
layout="side-by-side"
open
preview={<Validation />}
code={validationSnippet}
/>
<Theming definition={PasswordFieldDefinition} />
<ChangelogComponent component="password-field" />
@@ -1,26 +0,0 @@
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,77 @@
import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const passwordFieldPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
description: (
<>
Visual size of the input. Use <Chip>small</Chip> for dense layouts,{' '}
<Chip>medium</Chip> for prominent fields.
</>
),
},
label: {
type: 'string',
description: 'Visible label displayed above the input.',
},
secondaryLabel: {
type: 'string',
description: (
<>
Secondary text shown next to the label. If not provided and isRequired
is true, displays <Chip>Required</Chip>.
</>
),
},
description: {
type: 'string',
description: 'Help text displayed below the label.',
},
icon: {
type: 'enum',
values: ['ReactNode'],
description: 'Icon rendered before the input.',
},
placeholder: {
type: 'string',
description: 'Text displayed when the input is empty.',
},
name: {
type: 'string',
description: 'Form field name for submission.',
},
isRequired: {
type: 'boolean',
description: 'Whether the field is required for form submission.',
},
isDisabled: {
type: 'boolean',
description: 'Whether the input is disabled.',
},
isReadOnly: {
type: 'boolean',
description: 'Whether the input is read-only.',
},
value: {
type: 'string',
description: 'Controlled value of the input.',
},
defaultValue: {
type: 'string',
description: 'Default value for uncontrolled usage.',
},
onChange: {
type: 'enum',
values: ['(value: string) => void'],
description: 'Handler called when the input value changes.',
},
isInvalid: {
type: 'boolean',
description: 'Whether the field is in an invalid state.',
},
...classNamePropDefs,
};
@@ -1,15 +1,15 @@
export const passwordFieldUsageSnippet = `import { PasswordField } from '@backstage/ui';
<PasswordField />`;
<PasswordField label="Password" />`;
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" />
export const sizesSnippet = `<Flex direction="column" gap="4">
<PasswordField size="small" name="password" label="Password" />
<PasswordField size="medium" name="password" label="Password" />
</Flex>`;
export const withDescriptionSnippet = `<PasswordField
@@ -17,3 +17,17 @@ export const withDescriptionSnippet = `<PasswordField
label="Password"
description="Enter your password"
/>`;
export const withIconSnippet = `<PasswordField
name="password"
label="Password"
icon={<RiLockLine />}
/>`;
export const validationSnippet = `<PasswordField
name="password"
label="Password"
isRequired
isInvalid
description="Password must be at least 8 characters"
/>`;
@@ -3,15 +3,57 @@
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';
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
export const Default = () => {
return (
<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<div style={{ padding: '16px' }}>
<p>This is a popover</p>
</div>
<Text>Popover content</Text>
</Popover>
</DialogTrigger>
);
};
export const Placement = () => {
return (
<Flex gap="4">
<DialogTrigger>
<Button>Top</Button>
<Popover placement="top">
<Text>Content above trigger</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Right</Button>
<Popover placement="right">
<Text>Content to the right</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Bottom</Button>
<Popover placement="bottom">
<Text>Content below trigger</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Left</Button>
<Popover placement="left">
<Text>Content to the left</Text>
</Popover>
</DialogTrigger>
</Flex>
);
};
export const HideArrow = () => {
return (
<DialogTrigger>
<Button>Open Popover</Button>
<Popover hideArrow>
<Text>Popover without arrow</Text>
</Popover>
</DialogTrigger>
);
+40 -5
View File
@@ -1,17 +1,28 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import { dialogTriggerPropDefs, popoverPropDefs } from './props-definition';
import { popoverUsageSnippet, defaultSnippet } from './snippets';
import { Default } from './components';
import {
popoverUsageSnippet,
defaultSnippet,
placementSnippet,
hideArrowSnippet,
} from './snippets';
import { Default, Placement, HideArrow } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { PopoverDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
export const reactAriaUrls = {
dialogTrigger: 'https://react-aria.adobe.com/Modal#dialogtrigger',
popover: 'https://react-aria.adobe.com/Popover',
};
<PageTitle
title="Popover"
description="A popover displays floating content anchored to a trigger element with automatic positioning and collision detection."
description="A floating panel anchored to a trigger with automatic positioning."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
@@ -24,16 +35,40 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
### DialogTrigger
The trigger component that wraps both the trigger button and the popover content.
Wraps the trigger button and popover content.
<PropsTable data={dialogTriggerPropDefs} />
<ReactAriaLink component="DialogTrigger" href={reactAriaUrls.dialogTrigger} />
### Popover
The main popover component that displays floating content with automatic positioning.
Displays floating content with automatic positioning.
<PropsTable data={popoverPropDefs} />
<ReactAriaLink component="Popover" href={reactAriaUrls.popover} />
## Examples
### Placement
<Snippet
align="center"
py={4}
preview={<Placement />}
code={placementSnippet}
/>
### Hidden arrow
<Snippet
align="center"
py={4}
preview={<HideArrow />}
code={hideArrowSnippet}
/>
<Theming definition={PopoverDefinition} />
<ChangelogComponent component="popover" />
@@ -1,55 +1,47 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
} from '@/utils/propDefs';
import { childrenPropDefs, classNamePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const dialogTriggerPropDefs: Record<string, PropDef> = {
isDisabled: {
defaultOpen: {
type: 'boolean',
},
delay: {
type: 'number',
default: '0',
},
closeDelay: {
type: 'number',
default: '0',
description: 'Whether the popover is open by default (uncontrolled).',
},
isOpen: {
type: 'boolean',
description: 'Whether the popover is open (controlled).',
},
defaultOpen: {
type: 'boolean',
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
description: 'Handler called when the popover open state changes.',
},
...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',
description:
'The placement of the popover relative to the trigger element.',
},
offset: {
type: 'number',
default: '8',
description:
'The distance in pixels between the popover and the trigger element.',
},
containerPadding: {
type: 'number',
default: '12',
description:
'The minimum distance in pixels from the edge of the viewport.',
},
hideArrow: {
type: 'boolean',
default: 'false',
description: 'Whether to hide the arrow pointing to the trigger element.',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
+37 -7
View File
@@ -1,19 +1,49 @@
export const popoverUsageSnippet = `import { DialogTrigger, Popover, Button } from '@backstage/ui';
export const popoverUsageSnippet = `import { DialogTrigger, Popover, Button, Text } from '@backstage/ui';
<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<div style={{ padding: '16px' }}>
<p>This is a popover</p>
</div>
<Text>Popover content</Text>
</Popover>
</DialogTrigger>`;
export const defaultSnippet = `<DialogTrigger>
<Button>Open Popover</Button>
<Popover>
<div style={{ padding: '16px' }}>
<p>This is a popover</p>
</div>
<Text>Popover content</Text>
</Popover>
</DialogTrigger>`;
export const placementSnippet = `<Flex gap="4">
<DialogTrigger>
<Button>Top</Button>
<Popover placement="top">
<Text>Content above trigger</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Right</Button>
<Popover placement="right">
<Text>Content to the right</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Bottom</Button>
<Popover placement="bottom">
<Text>Content below trigger</Text>
</Popover>
</DialogTrigger>
<DialogTrigger>
<Button>Left</Button>
<Popover placement="left">
<Text>Content to the left</Text>
</Popover>
</DialogTrigger>
</Flex>`;
export const hideArrowSnippet = `<DialogTrigger>
<Button>Open Popover</Button>
<Popover hideArrow>
<Text>Popover without arrow</Text>
</Popover>
</DialogTrigger>`;
+25 -13
View File
@@ -1,6 +1,7 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { radioGroupPropDefs } from './props-definition';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import { radioGroupPropDefs, radioPropDefs } from './props-definition';
import {
radioGroupUsageSnippet,
defaultSnippet,
@@ -24,9 +25,13 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
import { RadioGroupDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
radioGroup: 'https://react-aria.adobe.com/RadioGroup',
};
<PageTitle
title="RadioGroup"
description="A radio group allows a user to select a single item from a list of mutually exclusive options."
description="A group of radio buttons for selecting one option from a list."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
@@ -37,66 +42,73 @@ import { RadioGroupDefinition } from '../../../utils/definitions';
## API reference
### RadioGroup
<PropsTable data={radioGroupPropDefs} />
<ReactAriaLink component="RadioGroup" href={reactAriaUrls.radioGroup} />
### Radio
Individual radio button within a group.
<PropsTable data={radioPropDefs} />
<ReactAriaLink component="Radio" href={reactAriaUrls.radioGroup} />
## Examples
### Horizontal
Here's a simple TextField with a description.
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<Horizontal />}
code={horizontalSnippet}
/>
### Disabled
You can disable the entire radio group by adding the `isDisabled` prop to the `RadioGroup` component.
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<Disabled />}
code={disabledSnippet}
/>
### Disabled Single radio
You can disable a single radio by adding the `isDisabled` prop to the `Radio` component.
### Disabled single radio
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<DisabledSingle />}
code={disabledSingleSnippet}
/>
### Validation
Here's an example of a radio group with errors.
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<Validation />}
code={validationSnippet}
/>
### Read only
You can make the radio group read only by adding the `isReadOnly` prop to the `RadioGroup` component.
<Snippet
align="center"
py={4}
open
layout="side-by-side"
preview={<ReadOnly />}
code={readOnlySnippet}
/>
@@ -1,30 +0,0 @@
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,91 @@
import {
classNamePropDefs,
childrenPropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const radioGroupPropDefs: Record<string, PropDef> = {
label: {
type: 'string',
description: 'The visible label for the radio group.',
},
'aria-label': {
type: 'string',
description:
'Accessible label when a visible label is not provided. Either label, aria-label, or aria-labelledby is required.',
},
'aria-labelledby': {
type: 'string',
description:
'ID of an element that labels the radio group. Either label, aria-label, or aria-labelledby is required.',
},
secondaryLabel: {
type: 'string',
description: (
<>
Secondary label text. Defaults to <Chip>Required</Chip> when isRequired
is true.
</>
),
},
description: {
type: 'string',
description: 'Helper text displayed below the label.',
},
orientation: {
type: 'enum',
values: ['horizontal', 'vertical'],
default: 'vertical',
description: 'The axis the radio buttons should align with.',
},
value: {
type: 'string',
description: 'The current value (controlled).',
},
defaultValue: {
type: 'string',
description: 'The default value (uncontrolled).',
},
onChange: {
type: 'enum',
values: ['(value: string) => void'],
description: 'Handler called when the value changes.',
},
name: {
type: 'string',
description: 'The name of the radio group for form submission.',
},
isDisabled: {
type: 'boolean',
description: 'Whether all radio buttons in the group are disabled.',
},
isReadOnly: {
type: 'boolean',
description: 'Whether the radio group is read-only.',
},
isRequired: {
type: 'boolean',
description: 'Whether a selection is required before form submission.',
},
isInvalid: {
type: 'boolean',
description: 'Whether the radio group is in an invalid state.',
},
...childrenPropDefs,
...classNamePropDefs,
};
export const radioPropDefs: Record<string, PropDef> = {
value: {
type: 'string',
required: true,
description: 'The value of the radio button.',
},
isDisabled: {
type: 'boolean',
description: 'Whether this radio button is disabled.',
},
...childrenPropDefs,
...classNamePropDefs,
};
@@ -1,6 +1,9 @@
export const radioGroupUsageSnippet = `import { RadioGroup } from '@backstage/ui';
export const radioGroupUsageSnippet = `import { RadioGroup, Radio } from '@backstage/ui';
<RadioGroup />`;
<RadioGroup label="Select an option">
<Radio value="option1">Option 1</Radio>
<Radio value="option2">Option 2</Radio>
</RadioGroup>`;
export const defaultSnippet = `<RadioGroup label="What is your favorite pokemon?">
<Radio value="bulbasaur">Bulbasaur</Radio>
@@ -5,20 +5,15 @@ 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' }}
/>
<SearchField 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" />
<SearchField aria-label="Search" size="small" />
<SearchField aria-label="Search" size="medium" />
</Flex>
);
};
@@ -26,10 +21,8 @@ export const Sizes = () => {
export const WithDescription = () => {
return (
<SearchField
name="url"
label="Label"
description="Description"
aria-label="Search"
label="Search"
description="Enter a search term to filter results"
style={{ width: '100%', maxWidth: '300px' }}
/>
);
@@ -37,21 +30,9 @@ export const WithDescription = () => {
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 direction="row" gap="4">
<SearchField aria-label="Search" size="small" startCollapsed />
<SearchField aria-label="Search" size="medium" startCollapsed />
</Flex>
);
};
@@ -1,5 +1,6 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import { searchFieldPropDefs } from './props-definition';
import {
searchFieldUsageSnippet,
@@ -20,9 +21,13 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
import { SearchFieldDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
searchField: 'https://react-aria.adobe.com/SearchField',
};
<PageTitle
title="SearchField"
description="A SearchField is a text field designed for searches."
description="A text input for search queries with collapsible and icon support."
/>
<Snippet
@@ -40,17 +45,24 @@ import { SearchFieldDefinition } from '../../../utils/definitions';
<PropsTable data={searchFieldPropDefs} />
<ReactAriaLink component="SearchField" href={reactAriaUrls.searchField} />
## Examples
### Sizes
We support two different sizes: `small`, `medium`.
<Snippet align="center" py={4} open preview={<Sizes />} code={sizesSnippet} />
<Snippet
align="center"
py={4}
open
preview={<Sizes />}
code={sizesSnippet}
layout="side-by-side"
/>
### With description
Here's a simple SearchField with a description.
Add context below the input with the `description` prop.
<Snippet
align="center"
@@ -58,11 +70,12 @@ Here's a simple SearchField with a description.
open
preview={<WithDescription />}
code={withDescriptionSnippet}
layout="side-by-side"
/>
### Collapsible
You can make the SearchField collapsible by setting the `startCollapsed` prop to `true`.
Use `startCollapsed` for space-constrained layouts where the field expands on focus.
<Snippet
align="center"
@@ -70,6 +83,7 @@ You can make the SearchField collapsible by setting the `startCollapsed` prop to
open
preview={<StartCollapsed />}
code={startCollapsedSnippet}
layout="side-by-side"
/>
<Theming definition={SearchFieldDefinition} />
@@ -1,34 +0,0 @@
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,85 @@
import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const searchFieldPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
description: (
<>
Visual size of the input. Use <Chip>small</Chip> for inline or dense
layouts, <Chip>medium</Chip> for standalone fields.
</>
),
},
label: {
type: 'string',
description: 'The visible label for the search field.',
},
secondaryLabel: {
type: 'string',
description: (
<>
Secondary label text. Defaults to <Chip>Required</Chip> when isRequired
is true.
</>
),
},
description: {
type: 'string',
description: 'Helper text displayed below the label.',
},
placeholder: {
type: 'string',
default: 'Search',
description: 'Placeholder text shown when the field is empty.',
},
icon: {
type: 'enum',
values: ['ReactNode', 'false'],
description:
'Icon displayed before the input. Set to false to hide the icon.',
},
startCollapsed: {
type: 'boolean',
default: 'false',
description:
'Whether the search field starts in a collapsed state. Expands on focus.',
},
name: {
type: 'string',
description: 'The name of the input for form submission.',
},
value: {
type: 'string',
description: 'The current value (controlled).',
},
defaultValue: {
type: 'string',
description: 'The default value (uncontrolled).',
},
onChange: {
type: 'enum',
values: ['(value: string) => void'],
description: 'Handler called when the value changes.',
},
isDisabled: {
type: 'boolean',
description: 'Whether the search field is disabled.',
},
isReadOnly: {
type: 'boolean',
description: 'Whether the search field is read-only.',
},
isRequired: {
type: 'boolean',
description: 'Whether a value is required before form submission.',
},
isInvalid: {
type: 'boolean',
description: 'Whether the search field is in an invalid state.',
},
...classNamePropDefs,
};
@@ -2,34 +2,19 @@ export const searchFieldUsageSnippet = `import { SearchField } from '@backstage/
<SearchField />`;
export const withLabelSnippet = `<SearchField name="url" label="Label" aria-label="Search" />`;
export const withLabelSnippet = `<SearchField 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" />
<SearchField aria-label="Search" size="small" />
<SearchField aria-label="Search" size="medium" />
</Flex>`;
export const withDescriptionSnippet = `<SearchField
name="url"
label="Label"
description="Description"
aria-label="Search"
label="Search"
description="Enter a search term to filter results"
/>`;
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 />
export const startCollapsedSnippet = `<Flex direction="row" gap="4">
<SearchField aria-label="Search" size="small" startCollapsed />
<SearchField aria-label="Search" size="medium" startCollapsed />
</Flex>`;
+26 -40
View File
@@ -1,6 +1,7 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import {
Preview,
WithLabelAndDescription,
@@ -31,9 +32,13 @@ import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SelectDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
select: 'https://react-aria.adobe.com/Select',
};
<PageTitle
title="Select"
description="A common form component for choosing a predefined value in a dropdown menu."
description="A dropdown for selecting from predefined options with search support."
/>
<Snippet
@@ -51,15 +56,14 @@ import { SelectDefinition } from '../../../utils/definitions';
<PropsTable data={selectPropDefs} />
<ReactAriaLink component="Select" href={reactAriaUrls.select} />
## Examples
### With Label and description
Select component with label and description.
### Label and description
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<WithLabelAndDescription />}
code={selectDescriptionSnippet}
@@ -67,36 +71,26 @@ Select component with label and description.
### Sizes
Here's a view when the selects have different sizes.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<Sizes />}
code={selectSizesSnippet}
/>
### With Icon
Here's a view when the select has an icon.
### With icon
<Snippet
align="center"
py={4}
open
preview={<WithIcon />}
code={selectIconSnippet}
layout="side-by-side"
open
preview={<WithIcon />}
code={selectIconSnippet}
/>
### Disabled
Here's a view when the select is disabled.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<Disabled />}
code={selectDisabledSnippet}
@@ -104,11 +98,8 @@ Here's a view when the select is disabled.
### Disabled options
You can disable specific options within the Select component using `disabledKeys`.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<DisabledOption />}
code={selectDisabledOptionsSnippet}
@@ -116,35 +107,30 @@ You can disable specific options within the Select component using `disabledKeys
### Searchable
Here's a view when the select has searchable filtering.
Enable filtering with the `searchable` prop.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<Searchable />}
code={selectSearchableSnippet}
/>
### Multiple Selection
Here's a view when the select allows multiple selections.
### Multiple selection
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<MultipleSelection />}
code={selectMultipleSnippet}
/>
### Searchable with Multiple Selection
### Searchable multiple
Here's a view when the select combines search and multiple selection.
Combine search and multiple selection.
<Snippet
align="center"
py={4}
layout="side-by-side"
open
preview={<SearchableMultiple />}
code={selectSearchableMultipleSnippet}
@@ -152,7 +138,7 @@ Here's a view when the select combines search and multiple selection.
### Responsive
Here's a view when the select is responsive.
Size can change at different breakpoints.
<CodeBlock code={selectResponsiveSnippet} />
@@ -1,111 +0,0 @@
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,
};
@@ -0,0 +1,137 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
export const selectPropDefs: Record<string, PropDef> = {
options: {
type: 'complex',
description: 'Array of options to display in the dropdown.',
complexType: {
name: 'SelectOption[]',
properties: {
value: {
type: 'string',
required: true,
description: 'Unique value for the option.',
},
label: {
type: 'string',
required: true,
description: 'Display text for the option.',
},
disabled: {
type: 'boolean',
required: false,
description: 'Whether the option is disabled.',
},
},
},
},
selectionMode: {
type: 'enum',
values: ['single', 'multiple'],
default: 'single',
description: 'Single or multiple selection mode.',
},
value: {
type: 'enum',
values: ['string', 'string[]'],
description:
'Controlled selected value. String for single, array for multiple.',
},
defaultValue: {
type: 'enum',
values: ['string', 'string[]'],
description:
'Initial value for uncontrolled usage. String for single, array for multiple.',
},
onSelectionChange: {
type: 'enum',
values: ['(key: Key | null) => void', '(keys: Selection) => void'],
description: 'Called when selection changes.',
},
label: {
type: 'string',
description: 'Visible label above the select.',
},
secondaryLabel: {
type: 'string',
description: (
<>
Secondary text shown next to the label. If not provided and isRequired
is true, displays <Chip>Required</Chip>.
</>
),
},
description: {
type: 'string',
description: 'Helper text displayed below the label.',
},
placeholder: {
type: 'string',
default: 'Select an option',
description: 'Text shown when no option is selected.',
},
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
description: 'Visual size of the select field.',
},
icon: {
type: 'enum',
values: ['ReactNode'],
description: 'Icon displayed before the selected value.',
},
searchable: {
type: 'boolean',
default: false,
description: 'Enables search/filter functionality in the dropdown.',
},
searchPlaceholder: {
type: 'string',
default: 'Search...',
description:
'Placeholder text for the search input when searchable is true.',
},
isOpen: {
type: 'boolean',
description: 'Controlled open state. Use with onOpenChange.',
},
defaultOpen: {
type: 'boolean',
description: 'Initial open state for uncontrolled usage.',
},
onOpenChange: {
type: 'enum',
values: ['(isOpen: boolean) => void'],
description: 'Called when the dropdown opens or closes.',
},
isDisabled: {
type: 'boolean',
description: 'Prevents user interaction when true.',
},
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Keys of options that should be disabled.',
},
isRequired: {
type: 'boolean',
description: 'Marks the field as required for form validation.',
},
isInvalid: {
type: 'boolean',
description: 'Displays the select in an error state.',
},
name: {
type: 'string',
description: 'Form field name for form submission.',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -45,7 +45,7 @@ export const selectSizesSnippet = `<Flex>
</Flex>`;
export const selectDisabledSnippet = `<Select
disabled
isDisabled
label="Font family"
options={[ ... ]}
/>`;
@@ -2,30 +2,43 @@
import { Skeleton } from '../../../../../packages/ui/src/components/Skeleton/Skeleton';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { Box } from '../../../../../packages/ui/src/components/Box/Box';
export const Demo1 = () => {
export const CardPlaceholder = () => {
return (
<Flex gap="4">
<Skeleton rounded width={48} height={48} />
<Box surface="3" p="4">
<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} />
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>
</Box>
);
};
export const AvatarWithText = () => {
return (
<Box surface="3" p="4">
<Flex direction="column" 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>
</Flex>
</Flex>
</Box>
);
};
export const Demo2 = () => {
export const Rounded = () => {
return (
<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>
<Box surface="3" p="4">
<Flex direction="column" gap="4">
<Skeleton rounded width={32} height={32} />
<Skeleton rounded width={48} height={48} />
<Skeleton rounded width={64} height={64} />
</Flex>
</Box>
);
};
+29 -11
View File
@@ -2,8 +2,13 @@ import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { skeletonPropDefs } from './props-definition';
import { skeletonUsageSnippet, demo1Snippet, demo2Snippet } from './snippets';
import { Demo1, Demo2 } from './components';
import {
skeletonUsageSnippet,
cardPlaceholderSnippet,
avatarWithTextSnippet,
roundedSnippet,
} from './snippets';
import { CardPlaceholder, AvatarWithText, Rounded } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
@@ -14,7 +19,12 @@ import { SkeletonDefinition } from '../../../utils/definitions';
description="Use to show a placeholder while content is loading."
/>
<Snippet align="center" py={4} preview={<Demo2 />} code={demo2Snippet} />
<Snippet
align="center"
py={4}
preview={<CardPlaceholder />}
code={cardPlaceholderSnippet}
/>
## Usage
@@ -22,21 +32,29 @@ import { SkeletonDefinition } from '../../../utils/definitions';
## API reference
Skeleton extends standard HTML div attributes.
<PropsTable data={skeletonPropDefs} />
## Examples
### Demo 1
### Rounded
You can use a mix of different sizes to create a more complex skeleton.
<Snippet
layout="side-by-side"
open
preview={<Rounded />}
code={roundedSnippet}
/>
<Snippet align="center" py={4} preview={<Demo1 />} code={demo1Snippet} open />
### Avatar with text
### Demo 2
You can use a mix of different sizes to create a more complex skeleton.
<Snippet align="center" py={4} preview={<Demo2 />} code={demo2Snippet} open />
<Snippet
layout="side-by-side"
open
preview={<AvatarWithText />}
code={avatarWithTextSnippet}
/>
<Theming definition={SkeletonDefinition} />
@@ -6,19 +6,22 @@ import {
export const skeletonPropDefs: Record<string, PropDef> = {
width: {
type: 'number',
type: 'string',
default: '80',
responsive: false,
description:
'The width of the skeleton. Accepts a number (pixels) or CSS string value.',
},
height: {
type: 'number',
type: 'string',
default: '24',
responsive: false,
description:
'The height of the skeleton. Accepts a number (pixels) or CSS string value.',
},
rounded: {
type: 'boolean',
default: 'false',
responsive: false,
description:
'Whether to apply fully rounded corners (for circular shapes).',
},
...classNamePropDefs,
...stylePropDefs,
+24 -20
View File
@@ -1,26 +1,30 @@
export const skeletonUsageSnippet = `import { Flex, Skeleton } from '@backstage/ui';
export const skeletonUsageSnippet = `import { 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>`;
<Skeleton width={200} height={24} />`;
export const demo1Snippet = `<Flex gap="4">
<Skeleton rounded width={48} height={48} />
export const cardPlaceholderSnippet = `<Box surface="3" p="4">
<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} />
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>
</Box>`;
export const avatarWithTextSnippet = `<Box surface="3" p="4">
<Flex direction="column" 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>
</Flex>
</Flex>`;
</Box>`;
export const demo2Snippet = `<Flex direction="column" gap="4">
<Skeleton width={400} height={160} />
<Skeleton width={400} height={12} />
<Skeleton width={240} height={12} />
</Flex>`;
export const roundedSnippet = `<Box surface="3" p="4">
<Flex direction="column" gap="4">
<Skeleton rounded width={32} height={32} />
<Skeleton rounded width={48} height={48} />
<Skeleton rounded width={64} height={64} />
</Flex>
</Box>`;
+9 -10
View File
@@ -1,6 +1,7 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import { switchPropDefs } from './props-definition';
import { snippetUsage, defaultSnippet, disabledSnippet } from './snippets';
import { Default, Disabled } from './components';
@@ -9,9 +10,13 @@ import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { SwitchDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
switch: 'https://react-aria.adobe.com/Switch',
};
<PageTitle
title="Switch"
description="A control that indicates whether a setting is on or off."
description="A toggle control for on/off settings with label support."
/>
<Snippet align="center" py={4} preview={<Default />} code={defaultSnippet} />
@@ -24,19 +29,13 @@ import { SwitchDefinition } from '../../../utils/definitions';
<PropsTable data={switchPropDefs} />
<ReactAriaLink component="Switch" href={reactAriaUrls.switch} />
## Examples
### Disabled
A switch can be disabled using the `isDisabled` prop.
<Snippet
align="center"
py={4}
open
preview={<Disabled />}
code={disabledSnippet}
/>
<Snippet align="center" py={4} preview={<Disabled />} code={disabledSnippet} />
<Theming definition={SwitchDefinition} />
@@ -2,66 +2,44 @@ import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const switchPropDefs: Record<string, PropDef> = {
autoFocus: {
type: 'boolean',
},
defaultSelected: {
type: 'boolean',
},
...classNamePropDefs,
isDisabled: {
type: 'boolean',
},
isReadOnly: {
type: 'boolean',
label: {
type: 'string',
description: 'Text label displayed next to the switch.',
},
isSelected: {
type: 'boolean',
description:
'Controlled selected state. Use with onChange for controlled behavior.',
},
label: {
type: 'string',
},
name: {
type: 'string',
defaultSelected: {
type: 'boolean',
description: 'Initial selected state for uncontrolled usage.',
},
onChange: {
type: 'enum',
values: ['(isSelected: boolean) => void'],
description: 'Called when the switch state changes.',
},
onFocus: {
type: 'enum',
values: ['(e: FocusEvent<Target>) => void'],
isDisabled: {
type: 'boolean',
description: 'Prevents user interaction when true.',
},
onBlur: {
type: 'enum',
values: ['(e: FocusEvent<Target>) => void'],
isReadOnly: {
type: 'boolean',
description: 'Makes the switch non-interactive but still focusable.',
},
onFocusChange: {
type: 'enum',
values: ['(isFocused: boolean) => void'],
name: {
type: 'string',
description: 'Form field name for form submission.',
},
onKeyDown: {
type: 'enum',
values: ['(e: KeyboardEvent) => void'],
},
onKeyUp: {
type: 'enum',
values: ['(e: KeyboardEvent) => void'],
},
onHoverStart: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
},
onHoverEnd: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
},
onHoverChange: {
type: 'enum',
values: ['(isHovered: boolean) => void'],
},
...stylePropDefs,
value: {
type: 'string',
description: 'Form field value submitted when selected.',
},
autoFocus: {
type: 'boolean',
description: 'Focuses the switch on mount.',
},
...classNamePropDefs,
...stylePropDefs,
};
+10 -11
View File
@@ -13,7 +13,6 @@ import {
PrimitivesExample,
} from './components';
import {
tablePropsColumns,
tableReturnColumns,
useTableOptionsPropDefs,
useTableReturnPropDefs,
@@ -163,7 +162,7 @@ You can also disable specific rows from being clicked using `getIsDisabled`:
<CodeBlock code={tableRowActionsDisabledSnippet} />
<Snippet preview={<RowActionsExample />} />
<Snippet preview={<RowActionsExample />} code={tableRowActionsHrefSnippet} />
### Empty State
@@ -225,7 +224,7 @@ The `useTable` hook manages data fetching, pagination, sorting, and filtering.
**Options**
<PropsTable data={useTableOptionsPropDefs} columns={tablePropsColumns} />
<PropsTable data={useTableOptionsPropDefs} />
**Return Value**
@@ -235,27 +234,27 @@ The `useTable` hook manages data fetching, pagination, sorting, and filtering.
The main table component.
<PropsTable data={tablePropDefs} columns={tablePropsColumns} />
<PropsTable data={tablePropDefs} />
### ColumnConfig
<PropsTable data={columnConfigPropDefs} columns={tablePropsColumns} />
<PropsTable data={columnConfigPropDefs} />
### CellText
<PropsTable data={cellTextPropDefs} columns={tablePropsColumns} />
<PropsTable data={cellTextPropDefs} />
<ReactAriaLink component="Cell" href={reactAriaUrls.cell} />
### CellProfile
<PropsTable data={cellProfilePropDefs} columns={tablePropsColumns} />
<PropsTable data={cellProfilePropDefs} />
<ReactAriaLink component="Cell" href={reactAriaUrls.cell} />
### TablePagination
<PropsTable data={tablePaginationPropDefs} columns={tablePropsColumns} />
<PropsTable data={tablePaginationPropDefs} />
### Primitives
@@ -263,7 +262,7 @@ Low-level components for building custom table layouts.
#### TableRoot
<PropsTable data={tableRootPropDefs} columns={tablePropsColumns} />
<PropsTable data={tableRootPropDefs} />
<ReactAriaLink component="Table" href={reactAriaUrls.table} />
@@ -277,13 +276,13 @@ Low-level components for building custom table layouts.
#### Column
<PropsTable data={columnPropDefs} columns={tablePropsColumns} />
<PropsTable data={columnPropDefs} />
<ReactAriaLink component="Column" href={reactAriaUrls.column} />
#### Row
<PropsTable data={rowPropDefs} columns={tablePropsColumns} />
<PropsTable data={rowPropDefs} />
<ReactAriaLink component="Row" href={reactAriaUrls.row} />
@@ -9,13 +9,6 @@ import { Chip } from '@/components/Chip';
// PropsTable Column Configuration (Table docs use description instead of responsive)
// =============================================================================
export const tablePropsColumns = [
{ key: 'prop' as const, width: '15%' },
{ key: 'type' as const, width: '25%' },
{ key: 'default' as const, width: '15%' },
{ key: 'description' as const, width: '45%' },
];
// For return values (no default column)
export const tableReturnColumns = [
{ key: 'prop' as const, width: '15%' },
+50 -14
View File
@@ -1,6 +1,7 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import {
tabsPropDefs,
tabListPropDefs,
@@ -13,6 +14,7 @@ import {
defaultSelectedKeySnippet,
disabledTabsSnippet,
orientationSnippet,
urlNavigationSnippet,
} from './snippets';
import {
Default,
@@ -25,9 +27,13 @@ import { Theming } from '@/components/Theming';
import { TabsDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
export const reactAriaUrls = {
tabs: 'https://react-aria.adobe.com/Tabs',
};
<PageTitle
title="Tabs"
description="A component for toggling between related panels on the same page."
description="A tabbed interface supporting local state and URL-based navigation."
/>
<Snippet py={4} preview={<Default />} code={defaultSnippet} />
@@ -40,40 +46,70 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
### Tabs
Groups the tabs and the corresponding panels. Renders a `<div>` element.
Container that groups the tab list and panels.
<PropsTable data={tabsPropDefs} />
<ReactAriaLink component="Tabs" href={reactAriaUrls.tabs} />
### TabList
Container for the tab buttons.
<PropsTable data={tabListPropDefs} />
<ReactAriaLink component="TabList" href={reactAriaUrls.tabs} />
### Tab
An individual interactive tab button that toggles the corresponding panel. Renders a `<button>` element.
Individual tab button. Add `href` to enable URL-based tab selection.
<PropsTable data={tabPropDefs} />
<ReactAriaLink component="Tab" href={reactAriaUrls.tabs} />
### TabPanel
Content panel associated with a tab.
<PropsTable data={tabPanelPropDefs} />
<ReactAriaLink component="TabPanel" href={reactAriaUrls.tabs} />
## Examples
### Default Selected Key
Here's how to set which tab is selected by default.
### Default selected
<Snippet
py={4}
layout="side-by-side"
open
preview={<DefaultSelectedKey />}
code={defaultSelectedKeySnippet}
open
/>
### Disabled Tabs
### Disabled tabs
Here's how to disable specific tabs.
<Snippet
layout="side-by-side"
open
preview={<DisabledTabs />}
code={disabledTabsSnippet}
/>
<Snippet py={4} preview={<DisabledTabs />} code={disabledTabsSnippet} open />
### Vertical orientation
### Orientation
<Snippet
layout="side-by-side"
open
preview={<Orientation />}
code={orientationSnippet}
/>
Here's how to display tabs vertically.
### URL-based navigation
<Snippet py={4} preview={<Orientation />} code={orientationSnippet} open />
Add `href` to Tab components to enable URL-based tab selection. The active tab is determined by the current route.
<CodeBlock code={urlNavigationSnippet} />
<Theming definition={TabsDefinition} />
@@ -1,7 +1,6 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
@@ -10,56 +9,75 @@ export const tabsPropDefs: Record<string, PropDef> = {
type: 'enum',
values: ['horizontal', 'vertical'],
default: 'horizontal',
description:
'Layout direction. Use horizontal for top navigation, vertical for sidebar-style.',
},
selectedKey: {
type: 'string',
description: 'The currently selected tab key (controlled).',
},
defaultSelectedKey: {
type: 'string',
description: 'The default selected tab key (uncontrolled).',
},
onSelectionChange: {
type: 'enum',
values: ['(key: Key) => void'],
description: 'Handler called when the selected tab changes.',
},
isDisabled: {
type: 'boolean',
default: 'false',
description:
'Disables all tabs. Use when an entire section is unavailable.',
},
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'Keys of tabs that should be disabled.',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabListPropDefs: Record<string, PropDef> = {
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabPropDefs: Record<string, PropDef> = {
id: {
type: 'string',
required: true,
description: 'Unique identifier matching the corresponding TabPanel.',
},
href: {
type: 'string',
description:
'URL to navigate to. When set, tab selection is controlled by the current route.',
},
matchStrategy: {
type: 'enum',
values: ['exact', 'prefix'],
default: 'exact',
description:
'URL matching strategy. Use exact for leaf routes, prefix for parent routes.',
},
isDisabled: {
type: 'boolean',
default: 'false',
description: 'Disables this tab. Use for temporarily unavailable options.',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabPanelPropDefs: Record<string, PropDef> = {
id: {
type: 'string',
required: true,
description: 'Unique identifier matching the corresponding Tab.',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
@@ -58,3 +58,14 @@ export const orientationSnippet = `<Tabs orientation="vertical">
<TabPanel id="tab2">Content 2</TabPanel>
<TabPanel id="tab3">Content 3</TabPanel>
</Tabs>`;
export const urlNavigationSnippet = `<Tabs>
<TabList>
<Tab id="overview" href="/settings/overview">Overview</Tab>
<Tab id="profile" href="/settings/profile">Profile</Tab>
<Tab id="security" href="/settings/security">Security</Tab>
</TabList>
<TabPanel id="overview">Overview content</TabPanel>
<TabPanel id="profile">Profile content</TabPanel>
<TabPanel id="security">Security content</TabPanel>
</Tabs>`;
+16 -17
View File
@@ -1,6 +1,7 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import {
Default,
WithLink,
@@ -24,9 +25,13 @@ import { Theming } from '@/components/Theming';
import { TagGroupDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
export const reactAriaUrls = {
tagGroup: 'https://react-aria.adobe.com/TagGroup',
};
<PageTitle
title="TagGroup"
description="A tag group is a list of labels, categories, keywords, filters, or other items, that can be used to group and filter content."
description="A collection of tags for labeling, categorizing, or filtering content."
/>
<Snippet align="center" py={4} preview={<Default />} code={preview} />
@@ -39,39 +44,35 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
### TagGroup
A tag group is a list of tags.
Container for a collection of tags.
<PropsTable data={tagGroupPropDefs} />
<ReactAriaLink component="TagGroup" href={reactAriaUrls.tagGroup} />
### Tag
A tag is a single item in a tag group.
Individual tag item within a group.
<PropsTable data={tagPropDefs} />
<ReactAriaLink component="Tag" href={reactAriaUrls.tagGroup} />
## Examples
### With Links
A tag can be a link by passing a `href` prop.
### With links
<Snippet align="center" py={4} open preview={<WithLink />} code={withLink} />
### With Icons
A tag can have an icon by passing a `icon` prop.
### With icons
<Snippet align="center" py={4} open preview={<WithIcon />} code={withIcons} />
### Sizes
A tag can have a size by passing a `size` prop. It could be `small` or `medium`.
<Snippet align="center" py={4} open preview={<Sizes />} code={sizes} />
### Removing tags
A tag can be removed by passing a `onRemove` prop.
### Removable
<Snippet
align="center"
@@ -83,10 +84,8 @@ A tag can be removed by passing a `onRemove` prop.
### Disabled
A switch can be disabled using the `isDisabled` prop.
<Snippet align="center" py={4} open preview={<Disabled />} code={disabled} />
<Theming definition={TagGroupDefinition} />
<ChangelogComponent component="switch" />
<ChangelogComponent component="tag-group" />

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