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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>`;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>}
|
||||
/>`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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%' },
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user