` element.
+
+
## Examples
### With Subtitle
-Here's a view when using both title and subtitle props.
+Accordions can display a subtitle below the title.
}
- code={accordionWithSubtitleSnippet}
+ preview={
}
+ 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.
}
- code={accordionCustomTriggerSnippet}
+ preview={
}
+ code={customTriggerSnippet}
/>
### Default Expanded
-Here's a view when the panel is expanded by default.
-
}
- code={accordionDefaultExpandedSnippet}
+ preview={
}
+ 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.
}
- code={accordionGroupSingleOpenSnippet}
+ preview={
}
+ code={groupSingleOpenSnippet}
/>
### Group Multiple Open
-Here's a view when multiple accordions can be open simultaneously.
+Allows multiple panels to be open simultaneously.
}
- code={accordionGroupMultipleOpenSnippet}
+ preview={
}
+ code={groupMultipleOpenSnippet}
/>
diff --git a/docs-ui/src/app/components/accordion/props-definition.ts b/docs-ui/src/app/components/accordion/props-definition.ts
new file mode 100644
index 0000000000..b465526632
--- /dev/null
+++ b/docs-ui/src/app/components/accordion/props-definition.ts
@@ -0,0 +1,82 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const accordionPropDefs: Record
= {
+ 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,
+};
+
+export const accordionTriggerPropDefs: Record = {
+ level: {
+ 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 = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Content displayed when the accordion is expanded.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const accordionGroupPropDefs: Record = {
+ 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,
+};
diff --git a/docs-ui/src/app/components/accordion/snippets.ts b/docs-ui/src/app/components/accordion/snippets.ts
new file mode 100644
index 0000000000..83e89d1b64
--- /dev/null
+++ b/docs-ui/src/app/components/accordion/snippets.ts
@@ -0,0 +1,68 @@
+export const accordionUsageSnippet = `import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
+
+
+
+ Your content
+ `;
+
+export const defaultSnippet = `
+
+
+ Your content here
+
+ `;
+
+export const withSubtitleSnippet = `
+
+
+ Your content here
+
+ `;
+
+export const customTriggerSnippet = `
+
+
+
+ Custom Multi-line Trigger
+
+
+ Click to expand additional details and configuration options
+
+
+
+
+ Your content here
+
+ `;
+
+export const defaultExpandedSnippet = `
+
+
+ Your content here
+
+ `;
+
+export const groupSingleOpenSnippet = `
+
+
+ Content 1
+
+
+
+ Content 2
+
+ `;
+
+export const groupMultipleOpenSnippet = `
+
+
+ Content 1
+
+
+
+ Content 2
+
+ `;
diff --git a/docs-ui/src/app/components/alert/components.tsx b/docs-ui/src/app/components/alert/components.tsx
new file mode 100644
index 0000000000..afc5eb654c
--- /dev/null
+++ b/docs-ui/src/app/components/alert/components.tsx
@@ -0,0 +1,168 @@
+'use client';
+
+import { Alert } from '../../../../../packages/ui/src/components/Alert/Alert';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
+import { RiCloseLine, RiCloudLine } from '@remixicon/react';
+
+export const Default = () => {
+ return ;
+};
+
+export const StatusVariants = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const WithDescription = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+export const WithActions = () => {
+ return (
+
+
+ Dismiss
+
+ }
+ />
+ }
+ aria-label="Close"
+ />
+ }
+ />
+
+ );
+};
+
+export const WithActionsAndDescriptions = () => {
+ return (
+
+
+ Later
+
+
+ Update Now
+
+ >
+ }
+ />
+ );
+};
+
+export const LoadingStates = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export const WithoutIcons = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const CustomIcon = () => {
+ return (
+ }
+ title="This alert uses a custom cloud icon instead of the default info icon."
+ />
+ );
+};
diff --git a/docs-ui/src/app/components/alert/page.mdx b/docs-ui/src/app/components/alert/page.mdx
new file mode 100644
index 0000000000..cdfdf184e0
--- /dev/null
+++ b/docs-ui/src/app/components/alert/page.mdx
@@ -0,0 +1,120 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { alertPropDefs } from './props-definition';
+import {
+ alertUsageSnippet,
+ defaultSnippet,
+ statusVariantsSnippet,
+ withDescriptionSnippet,
+ withActionsSnippet,
+ loadingStatesSnippet,
+ withoutIconsSnippet,
+ customIconSnippet,
+} from './snippets';
+import {
+ Default,
+ StatusVariants,
+ WithDescription,
+ WithActions,
+ LoadingStates,
+ WithoutIcons,
+ CustomIcon,
+} from './components';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { AlertDefinition } from '../../../utils/definitions';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Status Variants
+
+The Alert component supports four status variants, each with its own color theme.
+
+ }
+ code={statusVariantsSnippet}
+/>
+
+### With Description
+
+Add a description to provide additional context or details.
+
+ }
+ code={withDescriptionSnippet}
+/>
+
+### With Actions
+
+Include custom actions like buttons for interactive alerts.
+
+ }
+ code={withActionsSnippet}
+/>
+
+### Loading States
+
+The loading spinner replaces the icon to indicate an ongoing process.
+
+ }
+ code={loadingStatesSnippet}
+/>
+
+### Without Icons
+
+Disable icons for a simpler appearance.
+
+ }
+ code={withoutIconsSnippet}
+/>
+
+### Custom Icon
+
+Provide a custom icon element instead of the default status icon.
+
+ }
+ code={customIconSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/alert/props-definition.ts b/docs-ui/src/app/components/alert/props-definition.ts
new file mode 100644
index 0000000000..6f2650fe42
--- /dev/null
+++ b/docs-ui/src/app/components/alert/props-definition.ts
@@ -0,0 +1,76 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const alertPropDefs: Record = {
+ status: {
+ type: 'enum',
+ values: ['info', 'success', 'warning', 'danger'],
+ responsive: true,
+ default: 'info',
+ },
+ icon: {
+ type: 'enum',
+ values: ['boolean', 'React.ReactElement'],
+ responsive: false,
+ },
+ loading: {
+ type: 'enum',
+ values: ['boolean'],
+ responsive: false,
+ },
+ title: {
+ type: 'enum',
+ values: ['React.ReactNode'],
+ responsive: false,
+ },
+ description: {
+ type: 'enum',
+ values: ['React.ReactNode'],
+ responsive: false,
+ },
+ customActions: {
+ type: 'enum',
+ values: ['React.ReactNode'],
+ responsive: false,
+ },
+ m: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ mx: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ my: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ mt: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ mb: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ ml: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ mr: {
+ type: 'enum',
+ values: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ responsive: true,
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/alert/snippets.ts b/docs-ui/src/app/components/alert/snippets.ts
new file mode 100644
index 0000000000..739dfde9db
--- /dev/null
+++ b/docs-ui/src/app/components/alert/snippets.ts
@@ -0,0 +1,131 @@
+export const alertUsageSnippet = `import { Alert } from '@backstage/ui';
+
+ `;
+
+export const defaultSnippet = ` `;
+
+export const statusVariantsSnippet = `
+
+
+
+
+ `;
+
+export const withDescriptionSnippet = `
+
+
+
+
+ `;
+
+export const withActionsSnippet = `
+
+ Dismiss
+
+ }
+ />
+ }
+ aria-label="Close"
+ />
+ }
+ />
+ `;
+
+export const withActionsAndDescriptionsSnippet = `
+
+ Later
+
+
+ Update Now
+
+ >
+ }
+/>`;
+
+export const loadingStatesSnippet = `
+
+
+
+ `;
+
+export const withoutIconsSnippet = `
+
+
+ `;
+
+export const customIconSnippet = `import { RiCloudLine } from '@remixicon/react';
+
+ }
+ title="This alert uses a custom cloud icon instead of the default info icon."
+/>`;
diff --git a/docs-ui/src/app/components/avatar/components.tsx b/docs-ui/src/app/components/avatar/components.tsx
new file mode 100644
index 0000000000..da257f13b9
--- /dev/null
+++ b/docs-ui/src/app/components/avatar/components.tsx
@@ -0,0 +1,100 @@
+'use client';
+
+import { Avatar } from '../../../../../packages/ui/src/components/Avatar/Avatar';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+
+export const Default = () => {
+ return (
+
+ );
+};
+
+export const Fallback = () => {
+ return (
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const Purpose = () => {
+ return (
+
+
+ Informative (default)
+
+ Use when avatar appears alone. Announced as "Charles de
+ Dreuille" to screen readers:
+
+
+
+
+
+
+ Decoration
+
+ Use when avatar appears with adjacent text. Hidden from screen
+ readers:
+
+
+
+ Charles de Dreuille
+
+
+
+ );
+};
diff --git a/docs-ui/src/content/avatar.mdx b/docs-ui/src/app/components/avatar/page.mdx
similarity index 50%
rename from docs-ui/src/content/avatar.mdx
rename to docs-ui/src/app/components/avatar/page.mdx
index 047485c2cc..7020eb90c4 100644
--- a/docs-ui/src/content/avatar.mdx
+++ b/docs-ui/src/app/components/avatar/page.mdx
@@ -1,39 +1,37 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
-import { AvatarSnippet } from '@/snippets/stories-snippets';
+import { avatarPropDefs } from './props-definition';
import {
- avatarPropDefs,
- snippetUsage,
- snippetSizes,
- snippetFallback,
- snippetPurpose,
-} from './avatar.props';
+ avatarUsageSnippet,
+ defaultSnippet,
+ sizesSnippet,
+ fallbackSnippet,
+ purposeSnippet,
+} from './snippets';
+import { Default, Sizes, Fallback, Purpose } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
-import { AvatarDefinition } from '../utils/definitions';
+import { AvatarDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
- }
- code={` `}
-/>
+ } code={defaultSnippet} />
## Usage
-
+
## API reference
+Avatar also accepts all standard HTML div attributes (`onClick`, `onMouseEnter`, etc.) since it extends `React.ComponentPropsWithoutRef<'div'>`.
+
## Examples
### Sizes
@@ -44,8 +42,9 @@ Avatar sizes can be set using the `size` prop.
align="center"
py={4}
open
- preview={ }
- code={snippetSizes}
+ preview={ }
+ code={sizesSnippet}
+ layout="side-by-side"
/>
### Fallback
@@ -56,20 +55,16 @@ If the image is not available, the avatar will show the initials of the name.
align="center"
py={4}
open
- preview={ }
- code={snippetFallback}
+ preview={ }
+ code={fallbackSnippet}
+ layout="side-by-side"
/>
### The `purpose` prop
Control how the avatar is announced to screen readers using the `purpose` prop.
- }
- code={snippetPurpose}
-/>
+ } code={purposeSnippet} />
diff --git a/docs-ui/src/app/components/avatar/props-definition.tsx b/docs-ui/src/app/components/avatar/props-definition.tsx
new file mode 100644
index 0000000000..4b56b94be5
--- /dev/null
+++ b/docs-ui/src/app/components/avatar/props-definition.tsx
@@ -0,0 +1,41 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const avatarPropDefs: Record = {
+ 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 decoration when name appears in
+ adjacent text.
+ >
+ ),
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/avatar/snippets.ts b/docs-ui/src/app/components/avatar/snippets.ts
new file mode 100644
index 0000000000..2deb8037a6
--- /dev/null
+++ b/docs-ui/src/app/components/avatar/snippets.ts
@@ -0,0 +1,52 @@
+export const avatarUsageSnippet = `import { Avatar } from '@backstage/ui';
+
+ `;
+
+export const defaultSnippet = ` `;
+
+export const fallbackSnippet = ` `;
+
+export const sizesSnippet = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+export const purposeSnippet = `
+
+ Informative (default)
+
+ Use when avatar appears alone. Announced as "Charles de Dreuille" to screen readers:
+
+
+
+
+
+
+ Decoration
+
+ Use when avatar appears with adjacent text. Hidden from screen readers:
+
+
+
+ Charles de Dreuille
+
+
+ `;
diff --git a/docs-ui/src/app/components/box/components.tsx b/docs-ui/src/app/components/box/components.tsx
new file mode 100644
index 0000000000..dac66f1f59
--- /dev/null
+++ b/docs-ui/src/app/components/box/components.tsx
@@ -0,0 +1,43 @@
+'use client';
+
+import { Box } from '../../../../../packages/ui/src/components/Box/Box';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { DecorativeBox } from '@/components/DecorativeBox';
+
+export const Default = () => {
+ return (
+
+
+
+ );
+};
+
+export const Surface = () => {
+ return (
+
+
+ Surface 0
+
+
+ Surface 1
+
+
+ Surface 2
+
+
+ Surface 3
+
+
+ );
+};
+
+export const Responsive = () => {
+ return (
+
+ Resize to see change
+
+ );
+};
diff --git a/docs-ui/src/app/components/box/page.mdx b/docs-ui/src/app/components/box/page.mdx
new file mode 100644
index 0000000000..9b60e5244a
--- /dev/null
+++ b/docs-ui/src/app/components/box/page.mdx
@@ -0,0 +1,53 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { boxPropDefs } from './props-definition';
+import {
+ snippetUsage,
+ defaultSnippet,
+ boxSurfaceSnippet,
+ boxResponsiveSnippet,
+} from './snippets';
+import { Default, Surface, Responsive } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { BoxDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} align="center" />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Surface
+
+Use surface levels to create visual hierarchy.
+
+ } code={boxSurfaceSnippet} layout="side-by-side" />
+
+### Responsive props
+
+Props can accept breakpoint objects for responsive behavior.
+
+ }
+ code={boxResponsiveSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/box/props-definition.tsx b/docs-ui/src/app/components/box/props-definition.tsx
new file mode 100644
index 0000000000..3809bb9993
--- /dev/null
+++ b/docs-ui/src/app/components/box/props-definition.tsx
@@ -0,0 +1,48 @@
+import {
+ classNamePropDefs,
+ heightPropDefs,
+ positionPropDefs,
+ stylePropDefs,
+ widthPropDefs,
+ spacingGroupAll,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const boxPropDefs: Record = {
+ 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 flex for flexbox layouts,{' '}
+ none to hide.
+ >
+ ),
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+ spacing: spacingGroupAll,
+};
diff --git a/docs-ui/src/app/components/box/snippets.ts b/docs-ui/src/app/components/box/snippets.ts
new file mode 100644
index 0000000000..e9af07dec5
--- /dev/null
+++ b/docs-ui/src/app/components/box/snippets.ts
@@ -0,0 +1,23 @@
+export const snippetUsage = `import { Box } from '@backstage/ui';
+
+
+ Content with padding and background
+ `;
+
+export const defaultSnippet = `
+
+ `;
+
+export const boxSurfaceSnippet = `
+ Surface 0
+ Surface 1
+ Surface 2
+ Surface 3
+ `;
+
+export const boxResponsiveSnippet = `
+ Content
+ `;
diff --git a/docs-ui/src/app/components/button-icon/components.tsx b/docs-ui/src/app/components/button-icon/components.tsx
new file mode 100644
index 0000000000..3ed421289c
--- /dev/null
+++ b/docs-ui/src/app/components/button-icon/components.tsx
@@ -0,0 +1,68 @@
+'use client';
+
+import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiCloudLine } from '@remixicon/react';
+
+export const Variants = () => {
+ return (
+
+ } variant="primary" aria-label="Cloud" />
+ }
+ variant="secondary"
+ aria-label="Cloud"
+ />
+ }
+ variant="tertiary"
+ aria-label="Cloud"
+ />
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+ } size="small" aria-label="Cloud" />
+ } size="medium" aria-label="Cloud" />
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+ }
+ variant="primary"
+ aria-label="Cloud"
+ />
+ }
+ variant="secondary"
+ aria-label="Cloud"
+ />
+ }
+ variant="tertiary"
+ aria-label="Cloud"
+ />
+
+ );
+};
+
+export const Loading = () => {
+ return (
+ }
+ variant="primary"
+ loading
+ aria-label="Cloud"
+ />
+ );
+};
diff --git a/docs-ui/src/app/components/button-icon/page.mdx b/docs-ui/src/app/components/button-icon/page.mdx
new file mode 100644
index 0000000000..51ff0ea5a0
--- /dev/null
+++ b/docs-ui/src/app/components/button-icon/page.mdx
@@ -0,0 +1,80 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { buttonIconPropDefs } from './props-definition';
+import {
+ buttonIconUsageSnippet,
+ variantsSnippet,
+ sizesSnippet,
+ disabledSnippet,
+ loadingSnippet,
+} from './snippets';
+import { Variants, Sizes, Disabled, Loading } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ButtonIconDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ button: 'https://react-aria.adobe.com/Button',
+};
+
+
+
+ } code={variantsSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Variants
+
+ }
+ code={variantsSnippet}
+/>
+
+### Sizes
+
+ } code={sizesSnippet} />
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+/>
+
+### Loading
+
+Shows a spinner during async operations.
+
+ }
+ code={loadingSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/button-icon/props-definition.tsx b/docs-ui/src/app/components/button-icon/props-definition.tsx
new file mode 100644
index 0000000000..05917cc2b4
--- /dev/null
+++ b/docs-ui/src/app/components/button-icon/props-definition.tsx
@@ -0,0 +1,64 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const buttonIconPropDefs: Record = {
+ variant: {
+ type: 'enum',
+ values: ['primary', 'secondary', 'tertiary'],
+ default: 'primary',
+ responsive: true,
+ description: (
+ <>
+ Visual style. Use primary for main actions,{' '}
+ secondary for alternatives, tertiary for
+ low-emphasis.
+ >
+ ),
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Button size. Use small for toolbars, medium {' '}
+ 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,
+};
diff --git a/docs-ui/src/app/components/button-icon/snippets.ts b/docs-ui/src/app/components/button-icon/snippets.ts
new file mode 100644
index 0000000000..b1ab27b77b
--- /dev/null
+++ b/docs-ui/src/app/components/button-icon/snippets.ts
@@ -0,0 +1,23 @@
+export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/ui';
+import { RiCloseLine } from '@remixicon/react';
+
+ } aria-label="Close" />`;
+
+export const variantsSnippet = `
+ } variant="primary" aria-label="Cloud" />
+ } variant="secondary" aria-label="Cloud" />
+ } variant="tertiary" aria-label="Cloud" />
+ `;
+
+export const sizesSnippet = `
+ } size="small" aria-label="Cloud" />
+ } size="medium" aria-label="Cloud" />
+ `;
+
+export const disabledSnippet = `
+ } variant="primary" aria-label="Cloud" />
+ } variant="secondary" aria-label="Cloud" />
+ } variant="tertiary" aria-label="Cloud" />
+ `;
+
+export const loadingSnippet = ` } variant="primary" loading aria-label="Cloud" />`;
diff --git a/docs-ui/src/app/components/button-link/components.tsx b/docs-ui/src/app/components/button-link/components.tsx
new file mode 100644
index 0000000000..4282ebf7dd
--- /dev/null
+++ b/docs-ui/src/app/components/button-link/components.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import { ButtonLink } from '../../../../../packages/ui/src/components/ButtonLink/ButtonLink';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { MemoryRouter } from 'react-router-dom';
+import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react';
+
+export const Variants = () => {
+ return (
+
+
+ }
+ variant="primary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ variant="secondary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ variant="tertiary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+
+
+ Small
+
+
+ Medium
+
+
+
+ );
+};
+
+export const WithIcons = () => {
+ return (
+
+
+ }
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ iconEnd={ }
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/button-link/page.mdx b/docs-ui/src/app/components/button-link/page.mdx
new file mode 100644
index 0000000000..35e4de68c5
--- /dev/null
+++ b/docs-ui/src/app/components/button-link/page.mdx
@@ -0,0 +1,68 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { buttonLinkPropDefs } from './props-definition';
+import {
+ buttonLinkUsageSnippet,
+ variantsSnippet,
+ sizesSnippet,
+ withIconsSnippet,
+ disabledSnippet,
+} from './snippets';
+import { Variants, Sizes, WithIcons, Disabled } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ButtonLinkDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ link: 'https://react-aria.adobe.com/Link',
+};
+
+
+
+ } code={variantsSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Sizes
+
+ } code={sizesSnippet} />
+
+### With Icons
+
+ }
+ code={withIconsSnippet}
+/>
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/button-link/props-definition.tsx b/docs-ui/src/app/components/button-link/props-definition.tsx
new file mode 100644
index 0000000000..deae5364df
--- /dev/null
+++ b/docs-ui/src/app/components/button-link/props-definition.tsx
@@ -0,0 +1,74 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ childrenPropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const buttonLinkPropDefs: Record = {
+ variant: {
+ type: 'enum',
+ values: ['primary', 'secondary', 'tertiary'],
+ default: 'primary',
+ responsive: true,
+ description: (
+ <>
+ Visual style. Use primary for main actions,{' '}
+ secondary for alternatives, tertiary for
+ low-emphasis.
+ >
+ ),
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Link size. Use small for inline contexts,{' '}
+ medium 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 _blank 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,
+};
diff --git a/docs-ui/src/app/components/button-link/snippets.ts b/docs-ui/src/app/components/button-link/snippets.ts
new file mode 100644
index 0000000000..7f1a69027c
--- /dev/null
+++ b/docs-ui/src/app/components/button-link/snippets.ts
@@ -0,0 +1,68 @@
+export const buttonLinkUsageSnippet = `import { ButtonLink } from '@backstage/ui';
+
+Button `;
+
+export const variantsSnippet = `
+ }
+ variant="primary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ variant="secondary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ }
+ variant="tertiary"
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ `;
+
+export const sizesSnippet = `
+
+ Small
+
+
+ Medium
+
+ `;
+
+export const withIconsSnippet = `
+ } href="https://ui.backstage.io" target="_blank">
+ Button
+
+ } href="https://ui.backstage.io" target="_blank">
+ Button
+
+ }
+ iconEnd={ }
+ href="https://ui.backstage.io"
+ target="_blank"
+ >
+ Button
+
+ `;
+
+export const disabledSnippet = `
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+ `;
diff --git a/docs-ui/src/app/components/button/components.tsx b/docs-ui/src/app/components/button/components.tsx
new file mode 100644
index 0000000000..1fe13f6630
--- /dev/null
+++ b/docs-ui/src/app/components/button/components.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { ButtonLink } from '../../../../../packages/ui/src/components/ButtonLink/ButtonLink';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiArrowRightSLine, RiCloudLine } from '@remixicon/react';
+import { MemoryRouter } from 'react-router-dom';
+
+export const Variants = () => {
+ return (
+
+ }>
+ Button
+
+ }>
+ Button
+
+ }>
+ Button
+
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+ Small
+ Medium
+
+ );
+};
+
+export const WithIcons = () => {
+ return (
+
+ }>Button
+ }>Button
+ } iconEnd={ }>
+ Button
+
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+
+ );
+};
+
+export const Destructive = () => {
+ return (
+
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+
+ );
+};
+
+export const Loading = () => {
+ return (
+
+ Load more items
+
+ );
+};
+
+export const AsLink = () => {
+ return (
+
+
+ Button
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/button/page.mdx b/docs-ui/src/app/components/button/page.mdx
new file mode 100644
index 0000000000..6434a7f4a4
--- /dev/null
+++ b/docs-ui/src/app/components/button/page.mdx
@@ -0,0 +1,147 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { buttonPropDefs } from './props-definition';
+import {
+ variantsSnippet,
+ sizesSnippet,
+ withIconsSnippet,
+ disabledSnippet,
+ destructiveSnippet,
+ loadingSnippet,
+ asLinkSnippet,
+ buttonSnippetUsage,
+ buttonResponsiveSnippet,
+} from './snippets';
+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,
+ WithIcons,
+ Disabled,
+ Destructive,
+ Loading,
+ AsLink,
+} from './components';
+
+export const reactAriaUrls = {
+ button: 'https://react-spectrum.adobe.com/react-aria/Button.html',
+};
+
+
+
+ } code={variantsSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Variants
+
+ }
+ code={variantsSnippet}
+ layout="side-by-side"
+/>
+
+### Sizes
+
+ }
+ code={sizesSnippet}
+ layout="side-by-side"
+/>
+
+### With Icons
+
+Icons can appear before or after the label.
+
+ }
+ code={withIconsSnippet}
+ layout="side-by-side"
+/>
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+ layout="side-by-side"
+/>
+
+### Destructive
+
+Use the `destructive` prop for dangerous actions like delete or remove.
+
+ }
+ code={destructiveSnippet}
+ layout="side-by-side"
+/>
+
+### Loading
+
+Shows a spinner and disables interaction during async operations.
+
+ }
+ code={loadingSnippet}
+ layout="side-by-side"
+/>
+
+### Responsive
+
+Button props accept responsive breakpoint objects.
+
+
+
+### As Link
+
+If you want to use a button as a link, please use the `ButtonLink` component.
+
+ }
+ code={asLinkSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/button/props-definition.tsx b/docs-ui/src/app/components/button/props-definition.tsx
new file mode 100644
index 0000000000..0f94fcffaf
--- /dev/null
+++ b/docs-ui/src/app/components/button/props-definition.tsx
@@ -0,0 +1,75 @@
+import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const buttonPropDefs: Record = {
+ variant: {
+ type: 'enum',
+ values: ['primary', 'secondary', 'tertiary'],
+ default: 'primary',
+ responsive: true,
+ description: (
+ <>
+ Visual style. Use primary for main actions,{' '}
+ secondary for alternatives, tertiary for
+ low-emphasis.
+ >
+ ),
+ },
+ destructive: {
+ type: 'boolean',
+ default: 'false',
+ description:
+ 'Applies destructive styling for dangerous actions like delete or remove.',
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Button size. Use small 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,
+};
diff --git a/docs-ui/src/app/components/button/snippets.ts b/docs-ui/src/app/components/button/snippets.ts
new file mode 100644
index 0000000000..6b8aba7e26
--- /dev/null
+++ b/docs-ui/src/app/components/button/snippets.ts
@@ -0,0 +1,66 @@
+export const buttonSnippetUsage = `import { Button } from '@backstage/ui';
+
+Click me `;
+
+export const buttonResponsiveSnippet = `
+ Responsive Button
+ `;
+
+export const variantsSnippet = `
+ }>
+ Button
+
+ }>
+ Button
+
+ }>
+ Button
+
+ `;
+
+export const sizesSnippet = `
+ Small
+ Medium
+ `;
+
+export const withIconsSnippet = `
+ }>Button
+ }>Button
+ } iconEnd={ }>
+ Button
+
+ `;
+
+export const disabledSnippet = `
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+ `;
+
+export const destructiveSnippet = `
+
+ Primary
+
+
+ Secondary
+
+
+ Tertiary
+
+ `;
+
+export const loadingSnippet = `
+ Load more items
+ `;
+
+export const asLinkSnippet = `
+
+ Button
+
+ `;
diff --git a/docs-ui/src/app/components/card/components.tsx b/docs-ui/src/app/components/card/components.tsx
new file mode 100644
index 0000000000..d10e4d7bce
--- /dev/null
+++ b/docs-ui/src/app/components/card/components.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import {
+ Card,
+ CardHeader,
+ CardBody,
+ CardFooter,
+} from '../../../../../packages/ui/src/components/Card/Card';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+
+export const Default = () => {
+ return (
+
+ Header
+ Body
+ Footer
+
+ );
+};
+
+export const HeaderAndBody = () => {
+ return (
+
+ Header
+ Body content without a footer
+
+ );
+};
+
+export const WithLongBody = () => {
+ return (
+
+
+ Header
+
+
+
+ This is the first paragraph of a long body text that demonstrates how
+ the Card component handles extensive content. The card should adjust
+ accordingly to display all the text properly while maintaining its
+ structure.
+
+
+ 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.
+
+
+ This third paragraph continues to add more text to ensure we have a
+ proper demonstration of a card with significant content. This makes it
+ easier to test scrolling behavior and overall layout when content
+ exceeds the initial view.
+
+
+
+ Footer
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/card/page.mdx b/docs-ui/src/app/components/card/page.mdx
new file mode 100644
index 0000000000..6f410fcd97
--- /dev/null
+++ b/docs-ui/src/app/components/card/page.mdx
@@ -0,0 +1,86 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import {
+ cardPropDefs,
+ cardHeaderPropDefs,
+ cardBodyPropDefs,
+ cardFooterPropDefs,
+} from './props-definition';
+import {
+ cardUsageSnippet,
+ defaultSnippet,
+ headerAndBodySnippet,
+ withLongBodySnippet,
+} from './snippets';
+import { Default, HeaderAndBody, WithLongBody } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { CardDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+All Card components extend `HTMLDivElement` attributes.
+
+### Card
+
+
+
+### CardHeader
+
+Fixed at the top of the card.
+
+
+
+### CardBody
+
+Scrollable content area that fills available space.
+
+
+
+### CardFooter
+
+Fixed at the bottom of the card.
+
+
+
+## Examples
+
+### Header and body only
+
+Cards can omit the footer section.
+
+ }
+ code={headerAndBodySnippet}
+/>
+
+### Scrollable body
+
+When body content exceeds the available height, CardBody scrolls while header and footer remain fixed.
+
+ }
+ code={withLongBodySnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/card/props-definition.ts b/docs-ui/src/app/components/card/props-definition.ts
new file mode 100644
index 0000000000..b3e3776648
--- /dev/null
+++ b/docs-ui/src/app/components/card/props-definition.ts
@@ -0,0 +1,38 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+const optionalChildrenPropDef: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ responsive: false,
+ description: 'Content to display inside the component.',
+ },
+};
+
+export const cardPropDefs: Record = {
+ ...optionalChildrenPropDef,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const cardHeaderPropDefs: Record = {
+ ...optionalChildrenPropDef,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const cardBodyPropDefs: Record = {
+ ...optionalChildrenPropDef,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const cardFooterPropDefs: Record = {
+ ...optionalChildrenPropDef,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/content/card.props.ts b/docs-ui/src/app/components/card/snippets.ts
similarity index 50%
rename from docs-ui/src/content/card.props.ts
rename to docs-ui/src/app/components/card/snippets.ts
index 70c71bdf56..6f956e5518 100644
--- a/docs-ui/src/content/card.props.ts
+++ b/docs-ui/src/app/components/card/snippets.ts
@@ -1,30 +1,4 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const cardPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const cardHeaderPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const cardBodyPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const cardFooterPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const cardUsageSnippet = `import { card } from '@backstage/ui';
+export const cardUsageSnippet = `import { Card, CardHeader, CardBody, CardFooter } from '@backstage/ui';
Header
@@ -32,13 +6,20 @@ export const cardUsageSnippet = `import { card } from '@backstage/ui';
Footer
`;
-export const cardDefaultSnippet = `
+export const defaultSnippet = `
Header
Body
Footer
`;
-export const cardLongBodySnippet = `
+export const headerAndBodySnippet = `
+ Header
+ Body content without a footer
+ `;
+
+export const withLongBodySnippet = `import { Text } from '@backstage/ui';
+
+
Header
@@ -65,19 +46,3 @@ export const cardLongBodySnippet = `Footer
`;
-
-export const cardListRowSnippet = `
-
- Header
-
-
- Hello world
- Hello world
- Hello world
- Hello world
- ...
-
-
- Footer
-
- `;
diff --git a/docs-ui/src/app/components/checkbox/components.tsx b/docs-ui/src/app/components/checkbox/components.tsx
new file mode 100644
index 0000000000..a826c9e02f
--- /dev/null
+++ b/docs-ui/src/app/components/checkbox/components.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { Checkbox } from '../../../../../packages/ui/src/components/Checkbox/Checkbox';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return Accept terms and conditions ;
+};
+
+export const AllVariants = () => {
+ return (
+
+ Unchecked
+ Checked
+ Disabled
+
+ Checked & Disabled
+
+
+ );
+};
diff --git a/docs-ui/src/content/checkbox.mdx b/docs-ui/src/app/components/checkbox/page.mdx
similarity index 50%
rename from docs-ui/src/content/checkbox.mdx
rename to docs-ui/src/app/components/checkbox/page.mdx
index c574e8fed4..9657b471da 100644
--- a/docs-ui/src/content/checkbox.mdx
+++ b/docs-ui/src/app/components/checkbox/page.mdx
@@ -1,29 +1,29 @@
import { PropsTable } from '@/components/PropsTable';
-import { CheckboxSnippet } from '@/snippets/stories-snippets';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
+import { checkboxPropDefs } from './props-definition';
import {
- checkboxPropDefs,
checkboxUsageSnippet,
- checkboxDefaultSnippet,
- checkboxVariantsSnippet,
-} from './checkbox.props';
+ defaultSnippet,
+ allVariantsSnippet,
+} from './snippets';
+import { Default, AllVariants } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
-import { CheckboxDefinition } from '../utils/definitions';
+import { CheckboxDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ checkbox: 'https://react-aria.adobe.com/Checkbox',
+};
- }
- code={checkboxDefaultSnippet}
-/>
+ } code={defaultSnippet} />
## Usage
@@ -33,18 +33,18 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
## Examples
### All variants
-Here's a view when checkboxes have different variants.
-
}
- code={checkboxVariantsSnippet}
+ preview={ }
+ code={allVariantsSnippet}
/>
diff --git a/docs-ui/src/app/components/checkbox/props-definition.ts b/docs-ui/src/app/components/checkbox/props-definition.ts
new file mode 100644
index 0000000000..fd4b47ff8d
--- /dev/null
+++ b/docs-ui/src/app/components/checkbox/props-definition.ts
@@ -0,0 +1,49 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const checkboxPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ required: true,
+ description: 'Label displayed next to the checkbox.',
+ },
+ isSelected: {
+ type: 'boolean',
+ description: 'Controls checked state (controlled mode).',
+ },
+ defaultSelected: {
+ type: 'boolean',
+ description: 'Initial checked state (uncontrolled mode).',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(isSelected: boolean) => void'],
+ description: 'Called when the checked state changes.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Prevents interaction and applies disabled styling.',
+ },
+ isRequired: {
+ 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',
+ description: 'Name attribute for form submission.',
+ },
+ value: {
+ type: 'string',
+ description: 'Value attribute for form submission.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/checkbox/snippets.ts b/docs-ui/src/app/components/checkbox/snippets.ts
new file mode 100644
index 0000000000..bef496195f
--- /dev/null
+++ b/docs-ui/src/app/components/checkbox/snippets.ts
@@ -0,0 +1,12 @@
+export const checkboxUsageSnippet = `import { Checkbox } from '@backstage/ui';
+
+Accept terms `;
+
+export const defaultSnippet = `Accept terms and conditions `;
+
+export const allVariantsSnippet = `
+ Unchecked
+ Checked
+ Disabled
+ Checked & Disabled
+ `;
diff --git a/docs-ui/src/app/components/container/components.tsx b/docs-ui/src/app/components/container/components.tsx
new file mode 100644
index 0000000000..a11bbd9463
--- /dev/null
+++ b/docs-ui/src/app/components/container/components.tsx
@@ -0,0 +1,20 @@
+'use client';
+
+import { Container } from '../../../../../packages/ui/src/components/Container/Container';
+import { DecorativeBox } from '@/components/DecorativeBox';
+
+export const Default = () => {
+ return (
+
+ Page content goes here
+
+ );
+};
+
+export const ResponsiveSpacing = () => {
+ return (
+
+ Content with vertical spacing
+
+ );
+};
diff --git a/docs-ui/src/app/components/container/page.mdx b/docs-ui/src/app/components/container/page.mdx
new file mode 100644
index 0000000000..e0f4014a07
--- /dev/null
+++ b/docs-ui/src/app/components/container/page.mdx
@@ -0,0 +1,46 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { containerPropDefs } from './props-definition';
+import {
+ containerUsageSnippet,
+ defaultSnippet,
+ containerResponsiveSnippet,
+} from './snippets';
+import { Default, ResponsiveSpacing } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ContainerDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## Core Concepts
+
+Container provides the standard page layout for plugin content. It constrains content to a maximum width, centers it horizontally, and adds consistent horizontal gutters. Use it once per page to wrap your main content area.
+
+## API Reference
+
+
+
+## Examples
+
+### Responsive spacing
+
+Vertical spacing props accept breakpoint objects.
+
+ } code={containerResponsiveSnippet} />
+
+
+
+
diff --git a/docs-ui/src/app/components/container/props-definition.ts b/docs-ui/src/app/components/container/props-definition.ts
new file mode 100644
index 0000000000..89a51eac5c
--- /dev/null
+++ b/docs-ui/src/app/components/container/props-definition.ts
@@ -0,0 +1,20 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ createSpacingGroup,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const containerPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Content to render inside the container.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+ spacing: createSpacingGroup(
+ ['my', 'mt', 'mb', 'py', 'pt', 'pb'],
+ 'Vertical spacing properties for controlling margin and padding.',
+ ),
+};
diff --git a/docs-ui/src/app/components/container/snippets.ts b/docs-ui/src/app/components/container/snippets.ts
new file mode 100644
index 0000000000..a314b52387
--- /dev/null
+++ b/docs-ui/src/app/components/container/snippets.ts
@@ -0,0 +1,13 @@
+export const containerUsageSnippet = `import { Container } from "@backstage/ui";
+
+
+ {/* Your plugin's main content */}
+ `;
+
+export const defaultSnippet = `
+ Page content goes here
+ `;
+
+export const containerResponsiveSnippet = `
+ Content with vertical spacing
+ `;
diff --git a/docs-ui/src/app/components/dialog/components.tsx b/docs-ui/src/app/components/dialog/components.tsx
new file mode 100644
index 0000000000..18ab1bf9c5
--- /dev/null
+++ b/docs-ui/src/app/components/dialog/components.tsx
@@ -0,0 +1,118 @@
+'use client';
+
+import {
+ Dialog,
+ DialogTrigger,
+ DialogHeader,
+ DialogBody,
+ DialogFooter,
+} from '../../../../../packages/ui/src/components/Dialog/Dialog';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { TextField } from '../../../../../packages/ui/src/components/TextField/TextField';
+import { Select } from '../../../../../packages/ui/src/components/Select/Select';
+
+export const Default = () => (
+
+ Open Dialog
+
+ Example Dialog
+
+ This is a basic dialog example.
+
+
+
+ Close
+
+
+ Save
+
+
+
+
+);
+
+export const PreviewFixedWidthAndHeight = () => (
+
+ Scrollable Dialog
+
+ Long Content Dialog
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
+ ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
+ aliquip ex ea commodo consequat.
+
+
+ Duis aute irure dolor in reprehenderit in voluptate velit esse
+ cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
+ cupidatat non proident, sunt in culpa qui officia deserunt mollit
+ anim id est laborum.
+
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem
+ accusantium doloremque laudantium, totam rem aperiam, eaque ipsa
+ quae ab illo inventore veritatis et quasi architecto beatae vitae
+ dicta sunt explicabo.
+
+
+ Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut
+ fugit, sed quia consequuntur magni dolores eos qui ratione
+ voluptatem sequi nesciunt.
+
+
+ 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.
+
+
+ Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis
+ suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur.
+
+
+
+
+
+ Cancel
+
+
+ Accept
+
+
+
+
+);
+
+export const PreviewWithForm = () => (
+
+ Create User
+
+ Create New User
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Create User
+
+
+
+
+);
diff --git a/docs-ui/src/content/dialog.mdx b/docs-ui/src/app/components/dialog/page.mdx
similarity index 69%
rename from docs-ui/src/content/dialog.mdx
rename to docs-ui/src/app/components/dialog/page.mdx
index e1e4ec436e..12ce2d4135 100644
--- a/docs-ui/src/content/dialog.mdx
+++ b/docs-ui/src/app/components/dialog/page.mdx
@@ -1,35 +1,46 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
-import { DialogSnippet } from '@/snippets/stories-snippets';
+import {
+ Default,
+ PreviewFixedWidthAndHeight,
+ PreviewWithForm,
+} from './components';
import {
dialogPropDefs,
dialogTriggerPropDefs,
dialogHeaderPropDefs,
dialogBodyPropDefs,
dialogFooterPropDefs,
- dialogClosePropDefs,
+} from './props-definition';
+import {
dialogUsageSnippet,
dialogDefaultSnippet,
dialogFixedWidthAndHeightSnippet,
dialogWithFormSnippet,
dialogWithNoTriggerSnippet,
dialogCloseSnippet,
-} from './dialog.props';
+} from './snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
-import { DialogDefinition } from '../utils/definitions';
+import { DialogDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ dialogTrigger: 'https://react-aria.adobe.com/Modal#dialogtrigger',
+ modal: 'https://react-aria.adobe.com/Modal',
+};
}
+ preview={ }
code={dialogDefaultSnippet}
/>
@@ -45,18 +56,27 @@ Wraps a trigger element and the dialog content to handle open/close state.
+
+
### Dialog
The main dialog container that renders as a modal overlay.
+
+
### DialogHeader
Displays the dialog title with a built-in close button.
+
+
### DialogBody
The main content area of the dialog with optional scrolling.
@@ -82,18 +102,18 @@ Dialog with a fixed height body that scrolls when content overflows.
}
+ preview={ }
code={dialogFixedWidthAndHeightSnippet}
/>
### Dialog with Form
-Dialog containing form elements for user input.
+Forms can be embedded in the dialog body.
}
+ preview={ }
code={dialogWithFormSnippet}
/>
diff --git a/docs-ui/src/app/components/dialog/props-definition.ts b/docs-ui/src/app/components/dialog/props-definition.ts
new file mode 100644
index 0000000000..d5f2037f22
--- /dev/null
+++ b/docs-ui/src/app/components/dialog/props-definition.ts
@@ -0,0 +1,92 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const dialogTriggerPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Trigger element and dialog content.',
+ },
+ isOpen: {
+ type: 'boolean',
+ description: 'Whether the overlay is open by default (controlled).',
+ },
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Whether the overlay is open by default (uncontrolled).',
+ },
+ onOpenChange: {
+ type: 'enum',
+ values: ['(isOpen: boolean) => void'],
+ description:
+ "Handler that is called when the overlay's open state changes.",
+ },
+};
+
+export const dialogPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Dialog content (DialogHeader, DialogBody, DialogFooter).',
+ },
+ isOpen: {
+ type: 'boolean',
+ description: 'Whether the overlay is open (controlled mode).',
+ },
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Initial open state (uncontrolled mode).',
+ },
+ onOpenChange: {
+ type: 'enum',
+ values: ['(isOpen: boolean) => void'],
+ description: 'Called when the open state changes.',
+ },
+ width: {
+ type: 'enum',
+ values: ['number', 'string'],
+ default: '400',
+ description: 'Fixed width in pixels (number) or CSS units (string).',
+ },
+ height: {
+ type: 'enum',
+ values: ['number', 'string'],
+ default: 'auto',
+ description: 'Fixed height in pixels (number) or CSS units (string).',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const dialogHeaderPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Dialog title text.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const dialogBodyPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Main content of the dialog.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+export const dialogFooterPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Action buttons or footer content.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/dialog/snippets.ts b/docs-ui/src/app/components/dialog/snippets.ts
new file mode 100644
index 0000000000..440ff417e8
--- /dev/null
+++ b/docs-ui/src/app/components/dialog/snippets.ts
@@ -0,0 +1,86 @@
+export const dialogUsageSnippet = `import {
+ Dialog,
+ DialogTrigger,
+ DialogHeader,
+ DialogBody,
+ DialogFooter,
+} from '@backstage/ui';
+
+
+ Open Dialog
+
+ Title
+ Content
+
+ Close
+
+
+ `;
+
+export const dialogDefaultSnippet = `
+ Open Dialog
+
+ Example Dialog
+
+ This is a basic dialog example.
+
+
+ Close
+ Save
+
+
+ `;
+
+export const dialogFixedWidthAndHeightSnippet = `
+ Scrollable Dialog
+
+ Long Content Dialog
+
+ ...
+
+
+ Cancel
+ Accept
+
+
+ `;
+
+export const dialogWithFormSnippet = `
+ Create User
+
+ Create New User
+
+
+
+
+
+
+
+
+ Cancel
+ Create User
+
+
+ `;
+
+export const dialogWithNoTriggerSnippet = `const [isOpen, setIsOpen] = useState(false);
+
+
+ Create New User
+
+ Your content
+
+
+ Cancel
+ Create User
+
+ `;
+
+export const dialogCloseSnippet = `Close `;
diff --git a/docs-ui/src/app/components/flex/components.tsx b/docs-ui/src/app/components/flex/components.tsx
new file mode 100644
index 0000000000..ce6cc8c05f
--- /dev/null
+++ b/docs-ui/src/app/components/flex/components.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { DecorativeBox } from '@/components/DecorativeBox';
+
+export const Default = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export const DirectionExample = () => {
+ return (
+
+ First
+ Second
+ Third
+
+ );
+};
+
+export const ResponsiveExample = () => {
+ return (
+
+ 1
+ 2
+ 3
+
+ );
+};
+
+export const AlignExample = () => {
+ return (
+
+ Start
+ Middle
+ End
+
+ );
+};
diff --git a/docs-ui/src/app/components/flex/page.mdx b/docs-ui/src/app/components/flex/page.mdx
new file mode 100644
index 0000000000..f321b54583
--- /dev/null
+++ b/docs-ui/src/app/components/flex/page.mdx
@@ -0,0 +1,75 @@
+import { PropsTable } from '@/components/PropsTable';
+import { CodeBlock } from '@/components/CodeBlock';
+import { Snippet } from '@/components/Snippet';
+import { flexPropDefs } from './props-definition';
+import {
+ flexUsageSnippet,
+ defaultSnippet,
+ flexResponsiveSnippet,
+ flexAlignSnippet,
+ flexDirectionSnippet,
+} from './snippets';
+import {
+ Default,
+ ResponsiveExample,
+ AlignExample,
+ DirectionExample,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { FlexDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Direction
+
+ }
+ code={flexDirectionSnippet}
+ layout="side-by-side"
+/>
+
+### Responsive gap
+
+Gap values can be responsive using breakpoint objects.
+
+ }
+ code={flexResponsiveSnippet}
+ layout="side-by-side"
+/>
+
+### Alignment
+
+ }
+ code={flexAlignSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/flex/props-definition.tsx b/docs-ui/src/app/components/flex/props-definition.tsx
new file mode 100644
index 0000000000..2136dd0498
--- /dev/null
+++ b/docs-ui/src/app/components/flex/props-definition.tsx
@@ -0,0 +1,66 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ gapPropDefs,
+ spacingGroupAll,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const flexPropDefs: Record = {
+ direction: {
+ type: 'enum',
+ values: ['row', 'column', 'row-reverse', 'column-reverse'],
+ responsive: true,
+ description: (
+ <>
+ Main axis direction. Use row for horizontal,{' '}
+ column 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 between 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 auto to increment from
+ parent context.
+ >
+ ),
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ responsive: false,
+ description: 'Content to render inside the flex container.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+ spacing: spacingGroupAll,
+};
diff --git a/docs-ui/src/app/components/flex/snippets.ts b/docs-ui/src/app/components/flex/snippets.ts
new file mode 100644
index 0000000000..8770880c79
--- /dev/null
+++ b/docs-ui/src/app/components/flex/snippets.ts
@@ -0,0 +1,31 @@
+export const flexUsageSnippet = `import { Flex } from '@backstage/ui';
+
+
+ Item 1
+ Item 2
+ Item 3
+ `;
+
+export const defaultSnippet = `
+
+
+
+ `;
+
+export const flexDirectionSnippet = `
+ First
+ Second
+ Third
+ `;
+
+export const flexResponsiveSnippet = `
+ 1
+ 2
+ 3
+ `;
+
+export const flexAlignSnippet = `
+ Start
+ Middle
+ End
+ `;
diff --git a/docs-ui/src/app/components/grid/components.tsx b/docs-ui/src/app/components/grid/components.tsx
new file mode 100644
index 0000000000..0c0e5a0546
--- /dev/null
+++ b/docs-ui/src/app/components/grid/components.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { Grid } from '../../../../../packages/ui/src/components/Grid/Grid';
+import { DecorativeBox } from '@/components/DecorativeBox';
+
+export const Default = () => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export const ResponsiveExample = () => {
+ return (
+
+ 1
+ 2
+ 3
+ 4
+
+ );
+};
+
+export const GridItemExample = () => {
+ return (
+
+
+ Spans 2 columns
+
+ 1 column
+ 1 column
+
+ );
+};
diff --git a/docs-ui/src/app/components/grid/page.mdx b/docs-ui/src/app/components/grid/page.mdx
new file mode 100644
index 0000000000..8862586830
--- /dev/null
+++ b/docs-ui/src/app/components/grid/page.mdx
@@ -0,0 +1,64 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { gridPropDefs, gridItemPropDefs } from './props-definition';
+import {
+ gridUsageSnippet,
+ defaultSnippet,
+ gridResponsiveSnippet,
+ gridItemSnippet,
+} from './snippets';
+import { Default, ResponsiveExample, GridItemExample } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { GridDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+### Grid.Root
+
+The grid container. Defines column count and gap between items.
+
+
+
+### Grid.Item
+
+A grid child with column and row spanning control.
+
+
+
+## Examples
+
+### Responsive columns
+
+Column count can change at different breakpoints.
+
+ }
+ code={gridResponsiveSnippet}
+ open
+/>
+
+### Column spanning
+
+Use Grid.Item to span multiple columns.
+
+ } code={gridItemSnippet} open />
+
+
+
+
diff --git a/docs-ui/src/app/components/grid/props-definition.tsx b/docs-ui/src/app/components/grid/props-definition.tsx
new file mode 100644
index 0000000000..4622f8f614
--- /dev/null
+++ b/docs-ui/src/app/components/grid/props-definition.tsx
@@ -0,0 +1,115 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ gapPropDefs,
+ stylePropDefs,
+ spacingGroupAll,
+ 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 = {
+ columns: {
+ type: 'enum',
+ values: columnValues,
+ default: 'auto',
+ responsive: true,
+ description: (
+ <>
+ Number of columns. Use 1-12 for fixed layouts, auto 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 auto to increment from
+ parent context.
+ >
+ ),
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+ spacing: spacingGroupAll,
+};
+
+export const gridItemPropDefs: Record = {
+ 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 auto to increment from
+ parent context.
+ >
+ ),
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/grid/snippets.ts b/docs-ui/src/app/components/grid/snippets.ts
new file mode 100644
index 0000000000..2915a2ba4b
--- /dev/null
+++ b/docs-ui/src/app/components/grid/snippets.ts
@@ -0,0 +1,31 @@
+export const gridUsageSnippet = `import { Grid } from '@backstage/ui';
+
+
+ Item 1
+ Item 2
+ Item 3
+ `;
+
+export const defaultSnippet = `
+
+
+
+
+
+
+ `;
+
+export const gridResponsiveSnippet = `
+ 1
+ 2
+ 3
+ 4
+ `;
+
+export const gridItemSnippet = `
+
+ Spans 2 columns
+
+ 1 column
+ 1 column
+ `;
diff --git a/docs-ui/src/app/components/header-page/components.tsx b/docs-ui/src/app/components/header-page/components.tsx
new file mode 100644
index 0000000000..791afc5bbe
--- /dev/null
+++ b/docs-ui/src/app/components/header-page/components.tsx
@@ -0,0 +1,84 @@
+'use client';
+
+import { HeaderPage } from '../../../../../packages/ui/src/components/HeaderPage/HeaderPage';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
+import {
+ MenuTrigger,
+ Menu,
+ MenuItem,
+} from '../../../../../packages/ui/src/components/Menu/Menu';
+import { MemoryRouter } from 'react-router-dom';
+import { RiMore2Line } from '@remixicon/react';
+
+const tabs = [
+ { id: 'overview', label: 'Overview', href: '/overview' },
+ { id: 'checks', label: 'Checks', href: '/checks' },
+ { id: 'tracks', label: 'Tracks', href: '/tracks' },
+ { id: 'campaigns', label: 'Campaigns', href: '/campaigns' },
+ { id: 'integrations', label: 'Integrations', href: '/integrations' },
+];
+
+const breadcrumbs = [
+ { label: 'Home', href: '/' },
+ { label: 'Long Breadcrumb Name', href: '/long-breadcrumb' },
+ { label: 'Another Long Breadcrumb', href: '/another-long-breadcrumb' },
+ {
+ label: 'Yet Another Long Breadcrumb',
+ href: '/yet-another-long-breadcrumb',
+ },
+];
+
+export const WithEverything = () => (
+
+
+ Secondary
+ Primary
+ >
+ }
+ />
+
+);
+
+export const WithLongBreadcrumbs = () => (
+
+
+
+);
+
+export const WithTabs = () => (
+
+
+
+);
+
+export const WithCustomActions = () => (
+
+ Custom action}
+ />
+
+);
+
+export const WithMenu = () => (
+
+
+ } />
+
+ Settings
+ alert('logout')}>Logout
+
+
+ }
+ />
+
+);
diff --git a/docs-ui/src/app/components/header-page/page.mdx b/docs-ui/src/app/components/header-page/page.mdx
new file mode 100644
index 0000000000..ac99fb1af0
--- /dev/null
+++ b/docs-ui/src/app/components/header-page/page.mdx
@@ -0,0 +1,66 @@
+import { PropsTable } from '@/components/PropsTable';
+import { CodeBlock } from '@/components/CodeBlock';
+import { Snippet } from '@/components/Snippet';
+import {
+ WithEverything,
+ WithLongBreadcrumbs,
+ WithTabs,
+ WithCustomActions,
+ WithMenu,
+} from './components';
+import { headerPagePropDefs } from './props-definition';
+import {
+ usage,
+ defaultSnippet,
+ withTabs,
+ withBreadcrumbs,
+ withCustomActions,
+ withMenu,
+} from './snippets';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { HeaderPageDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Breadcrumbs
+
+Labels are truncated at 240px.
+
+ } code={withBreadcrumbs} />
+
+### Tabs
+
+Tabs use React Router and highlight based on the current route.
+
+ } code={withTabs} />
+
+### Custom actions
+
+ } code={withCustomActions} />
+
+### With menu
+
+Use `customActions` to add a dropdown menu.
+
+ } code={withMenu} />
+
+
+
+
diff --git a/docs-ui/src/app/components/header-page/props-definition.tsx b/docs-ui/src/app/components/header-page/props-definition.tsx
new file mode 100644
index 0000000000..393141870f
--- /dev/null
+++ b/docs-ui/src/app/components/header-page/props-definition.tsx
@@ -0,0 +1,69 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const headerPagePropDefs: Record = {
+ 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 exact for exact path
+ match, prefix 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,
+};
diff --git a/docs-ui/src/app/components/header-page/snippets.ts b/docs-ui/src/app/components/header-page/snippets.ts
new file mode 100644
index 0000000000..607675717c
--- /dev/null
+++ b/docs-ui/src/app/components/header-page/snippets.ts
@@ -0,0 +1,56 @@
+export const usage = `import { HeaderPage } from '@backstage/ui';
+
+ `;
+
+export const defaultSnippet = `
+ Secondary
+ Primary
+ >
+ }
+/>`;
+
+export const withBreadcrumbs = ` `;
+
+export const withTabs = ` `;
+
+export const withCustomActions = `Custom action}
+/>`;
+
+export const withMenu = `
+ } />
+
+ Settings
+ {}}>Logout
+
+
+ }
+/>`;
diff --git a/docs-ui/src/app/components/header/components.tsx b/docs-ui/src/app/components/header/components.tsx
new file mode 100644
index 0000000000..80a4d1dd6e
--- /dev/null
+++ b/docs-ui/src/app/components/header/components.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import { Header } from '../../../../../packages/ui/src/components/Header/Header';
+import { HeaderPage } from '../../../../../packages/ui/src/components/HeaderPage/HeaderPage';
+import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { MemoryRouter } from 'react-router-dom';
+import {
+ RiHeartLine,
+ RiEmotionHappyLine,
+ RiCloudy2Line,
+} from '@remixicon/react';
+
+const tabs = [
+ { id: 'overview', label: 'Overview', href: '/overview' },
+ { id: 'checks', label: 'Checks', href: '/checks' },
+ { id: 'tracks', label: 'Tracks', href: '/tracks' },
+ { id: 'campaigns', label: 'Campaigns', href: '/campaigns' },
+ { id: 'integrations', label: 'Integrations', href: '/integrations' },
+];
+
+const tabs2 = [
+ { id: 'banana', label: 'Banana', href: '/banana' },
+ { id: 'apple', label: 'Apple', href: '/apple' },
+ { id: 'orange', label: 'Orange', href: '/orange' },
+];
+
+export const WithAllOptionsAndTabs = () => (
+
+
+ } />
+ } />
+ } />
+ >
+ }
+ />
+
+);
+
+export const WithAllOptions = () => (
+
+
+ } />
+ } />
+ } />
+ >
+ }
+ />
+
+);
+
+export const WithHeaderPage = () => (
+
+ <>
+
+ Custom action}
+ />
+ >
+
+);
diff --git a/docs-ui/src/app/components/header/page.mdx b/docs-ui/src/app/components/header/page.mdx
new file mode 100644
index 0000000000..71e9950933
--- /dev/null
+++ b/docs-ui/src/app/components/header/page.mdx
@@ -0,0 +1,57 @@
+import { PropsTable } from '@/components/PropsTable';
+import { CodeBlock } from '@/components/CodeBlock';
+import { Snippet } from '@/components/Snippet';
+import {
+ WithAllOptionsAndTabs,
+ WithAllOptions,
+ WithHeaderPage,
+} from './components';
+import { headerPropDefs } from './props-definition';
+import {
+ usage,
+ simple,
+ defaultSnippet,
+ withTabs,
+ withHeaderPage,
+} from './snippets';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { HeaderDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Simple header
+
+ } code={simple} open />
+
+### Header with tabs
+
+Tabs use React Router and highlight automatically based on the current route.
+
+ } code={withTabs} open />
+
+### Header with HeaderPage
+
+Combine with [HeaderPage](/components/header-page) for multi-level navigation.
+
+ } code={withHeaderPage} open />
+
+
+
+
diff --git a/docs-ui/src/app/components/header/props-definition.tsx b/docs-ui/src/app/components/header/props-definition.tsx
new file mode 100644
index 0000000000..4a651cdb72
--- /dev/null
+++ b/docs-ui/src/app/components/header/props-definition.tsx
@@ -0,0 +1,63 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const headerPropDefs: Record = {
+ 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 exact for exact path
+ match, prefix if pathname starts with href.
+ >
+ ),
+ },
+ },
+ },
+ },
+ onTabSelectionChange: {
+ type: 'enum',
+ values: ['(key: Key) => void'],
+ description: 'Handler called when the selected tab changes.',
+ },
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/header/snippets.ts b/docs-ui/src/app/components/header/snippets.ts
new file mode 100644
index 0000000000..b83f1b68b2
--- /dev/null
+++ b/docs-ui/src/app/components/header/snippets.ts
@@ -0,0 +1,61 @@
+export const usage = `import { Header } from '@backstage/ui';
+
+`;
+
+export const defaultSnippet = `
+ } />
+ } />
+ } />
+ >
+ }
+/>`;
+
+export const simple = `
+ } />
+ } />
+ } />
+ >
+ }
+/>`;
+
+export const withTabs = ``;
+
+export const withHeaderPage = `
+Custom action}
+/>`;
diff --git a/docs-ui/src/app/components/link/components.tsx b/docs-ui/src/app/components/link/components.tsx
new file mode 100644
index 0000000000..320dd52517
--- /dev/null
+++ b/docs-ui/src/app/components/link/components.tsx
@@ -0,0 +1,115 @@
+'use client';
+
+import { Link } from '../../../../../packages/ui/src/components/Link/Link';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { MemoryRouter } from 'react-router-dom';
+
+export const Default = () => {
+ return (
+
+
+ Sign up for Backstage
+
+
+ );
+};
+
+export const ExternalLink = () => {
+ return (
+
+
+ Sign up for Backstage
+
+
+ );
+};
+
+export const AllVariants = () => {
+ return (
+
+
+
+ title-large
+
+
+ title-medium
+
+
+ title-small
+
+
+ title-x-small
+
+
+ body-large
+
+
+ body-medium
+
+
+ body-small
+
+
+ body-x-small
+
+
+
+ );
+};
+
+export const AllColors = () => {
+ return (
+
+
+
+ Primary
+
+
+ Secondary
+
+
+ Danger
+
+
+ Warning
+
+
+ Success
+
+
+ Info
+
+
+
+ );
+};
+
+export const Weight = () => {
+ return (
+
+
+
+ Regular
+
+
+ Bold
+
+
+
+ );
+};
+
+export const Standalone = () => {
+ return (
+
+
+
+ Default link
+
+
+ Standalone link
+
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/link/page.mdx b/docs-ui/src/app/components/link/page.mdx
new file mode 100644
index 0000000000..8715bb270b
--- /dev/null
+++ b/docs-ui/src/app/components/link/page.mdx
@@ -0,0 +1,107 @@
+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,
+ defaultSnippet,
+ externalLinkSnippet,
+ allVariantsSnippet,
+ allColorsSnippet,
+ weightSnippet,
+ standaloneSnippet,
+} from './snippets';
+import {
+ Default,
+ ExternalLink,
+ AllVariants,
+ AllColors,
+ Weight,
+ Standalone,
+} 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',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### External link
+
+Use `target="_blank"` to open links in a new tab.
+
+ }
+ code={externalLinkSnippet}
+ layout="side-by-side"
+/>
+
+### Variants
+
+ }
+ code={allVariantsSnippet}
+/>
+
+### Colors
+
+Status colors for contextual links.
+
+ }
+ code={allColorsSnippet}
+/>
+
+### Weight
+
+ }
+ code={weightSnippet}
+ layout="side-by-side"
+/>
+
+### Standalone
+
+Use `standalone` to remove the underline by default. The underline will appear on hover.
+
+ }
+ code={standaloneSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/link/props-definition.tsx b/docs-ui/src/app/components/link/props-definition.tsx
new file mode 100644
index 0000000000..f0aa95114e
--- /dev/null
+++ b/docs-ui/src/app/components/link/props-definition.tsx
@@ -0,0 +1,79 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const linkPropDefs: Record = {
+ 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 _blank 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 bold 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.',
+ default: 'false',
+ },
+ standalone: {
+ type: 'boolean',
+ description: 'Removes underline by default. Underline appears on hover.',
+ default: 'false',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/link/snippets.ts b/docs-ui/src/app/components/link/snippets.ts
new file mode 100644
index 0000000000..386ad9f1a0
--- /dev/null
+++ b/docs-ui/src/app/components/link/snippets.ts
@@ -0,0 +1,39 @@
+export const linkUsageSnippet = `import { Link } from '@backstage/ui';
+
+ Sign up for Backstage`;
+
+export const defaultSnippet = ` Sign up for Backstage`;
+
+export const externalLinkSnippet = `
+ Sign up for Backstage
+`;
+
+export const allVariantsSnippet = `
+ title-large
+ title-medium
+ title-small
+ title-x-small
+ body-large
+ body-medium
+ body-small
+ body-x-small
+ `;
+
+export const allColorsSnippet = `
+ Primary
+ Secondary
+ Danger
+ Warning
+ Success
+ Info
+ `;
+
+export const weightSnippet = `
+ Regular
+ Bold
+ `;
+
+export const standaloneSnippet = `
+ Default link
+ Standalone link
+ `;
diff --git a/docs-ui/src/app/components/menu/components.tsx b/docs-ui/src/app/components/menu/components.tsx
new file mode 100644
index 0000000000..fbb481f5ef
--- /dev/null
+++ b/docs-ui/src/app/components/menu/components.tsx
@@ -0,0 +1,169 @@
+'use client';
+
+import {
+ MenuTrigger,
+ Menu,
+ MenuItem,
+ MenuSection,
+ MenuSeparator,
+ SubmenuTrigger,
+ MenuAutocomplete,
+ MenuAutocompleteListbox,
+ MenuListBoxItem,
+} from '../../../../../packages/ui/src/components/Menu/Menu';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { MemoryRouter } from 'react-router-dom';
+import {
+ RiChat1Line,
+ RiFileLine,
+ RiFolderLine,
+ RiImageLine,
+ RiSettingsLine,
+ RiShareBoxLine,
+} from '@remixicon/react';
+
+export const Preview = () => (
+
+
+ Menu
+
+ Edit
+ Duplicate
+ Rename
+
+ }>Share
+ }>Feedback
+
+
+ }>Settings
+
+ Edit
+ Duplicate
+ Rename
+
+
+
+
+
+);
+
+export const PreviewSubmenu = () => (
+
+
+ Menu
+
+ New File
+
+ Open Recent
+
+ File 1.txt
+ File 2.txt
+
+
+ Save
+
+
+
+);
+
+export const PreviewIcons = () => (
+
+
+ Menu
+
+ }>New File
+ }>New Folder
+ }>New Image
+
+
+
+);
+
+export const PreviewLinks = () => (
+
+
+ Menu
+
+ Home
+ About
+ Contact
+
+
+
+);
+
+export const PreviewSections = () => (
+
+
+ Menu
+
+
+ New
+ Open
+
+
+ Cut
+ Copy
+ Paste
+
+
+
+
+);
+
+export const PreviewSeparators = () => (
+
+
+ Menu
+
+ New
+ Open
+
+ Save
+ Save As...
+
+
+
+);
+
+export const PreviewAutocompleteMenu = () => (
+
+
+ Search
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+);
+
+export const PreviewAutocompleteListbox = () => (
+
+
+ Select
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+);
+
+export const PreviewAutocompleteListboxMultiple = () => (
+
+
+ Multi-select
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+);
diff --git a/docs-ui/src/app/components/menu/page.mdx b/docs-ui/src/app/components/menu/page.mdx
new file mode 100644
index 0000000000..f1de264d73
--- /dev/null
+++ b/docs-ui/src/app/components/menu/page.mdx
@@ -0,0 +1,229 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import {
+ Preview,
+ PreviewSubmenu,
+ PreviewIcons,
+ PreviewLinks,
+ PreviewSections,
+ PreviewSeparators,
+ PreviewAutocompleteMenu,
+ PreviewAutocompleteListbox,
+ PreviewAutocompleteListboxMultiple,
+} from './components';
+import {
+ menuTriggerPropDefs,
+ submenuTriggerPropDefs,
+ menuPropDefs,
+ menuListBoxPropDefs,
+ menuAutocompletePropDefs,
+ menuAutocompleteListboxPropDefs,
+ menuItemPropDefs,
+ menuListBoxItemPropDefs,
+ menuSectionPropDefs,
+ menuSeparatorPropDefs,
+} from './props-definition';
+import {
+ usage,
+ preview,
+ submenu,
+ icons,
+ sections,
+ separators,
+ links,
+ autocomplete,
+ autocompleteListbox,
+ autocompleteListboxMultiple,
+} from './snippets';
+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',
+};
+
+
+
+ } code={preview} />
+
+## Usage
+
+
+
+### Triggers
+
+- `MenuTrigger` combines a trigger element with a menu popover.
+- `SubmenuTrigger` combines a `MenuItem` with a nested submenu.
+
+### Containers
+
+- `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 interactive action in the menu.
+- `MenuListBoxItem` is a selectable item in a list box.
+
+### Grouping
+
+- `MenuSection` groups items with a title.
+- `MenuSeparator` adds a horizontal divider.
+
+## API reference
+
+### MenuTrigger
+
+Accepts two children: the trigger element and a menu container.
+
+
+
+
+
+### SubmenuTrigger
+
+Accepts two children: a `MenuItem` and a menu container.
+
+
+
+
+
+### Menu
+
+
+
+
+
+### MenuListBox
+
+
+
+
+
+### MenuAutocomplete
+
+
+
+
+
+### MenuAutocompleteListbox
+
+
+
+
+
+### MenuItem
+
+
+
+
+
+### MenuListBoxItem
+
+
+
+
+
+### MenuSection
+
+
+
+
+
+### MenuSeparator
+
+
+
+
+
+## Examples
+
+### Nested navigation
+
+Submenus open to the right of their parent item.
+
+ }
+ code={submenu}
+/>
+
+### With icons
+
+ } code={icons} />
+
+### With links
+
+The `href` prop works with both internal and external links.
+
+ } code={links} />
+
+### With sections
+
+ }
+ code={sections}
+/>
+
+### With separators
+
+ }
+ code={separators}
+/>
+
+### With autocomplete
+
+ }
+ code={autocomplete}
+/>
+
+### With list box
+
+ }
+ code={autocompleteListbox}
+/>
+
+### Multiple selection
+
+Set `selectionMode="multiple"` to allow multiple selections.
+
+ }
+ code={autocompleteListboxMultiple}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/menu/props-definition.tsx b/docs-ui/src/app/components/menu/props-definition.tsx
new file mode 100644
index 0000000000..5e11b22737
--- /dev/null
+++ b/docs-ui/src/app/components/menu/props-definition.tsx
@@ -0,0 +1,364 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const menuTriggerPropDefs: Record = {
+ 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 = {
+ delay: {
+ type: 'number',
+ default: '200',
+ description: 'Delay in milliseconds before the submenu opens on hover.',
+ },
+};
+
+export const menuPropDefs: Record = {
+ 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'],
+ description: 'Controlled selected keys.',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Default selected keys for uncontrolled usage.',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ 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 = {
+ 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'],
+ description: 'Controlled selected keys.',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ 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'],
+ 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 = {
+ 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'],
+ description: 'Controlled selected keys.',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Default selected keys for uncontrolled usage.',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ 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 = {
+ 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'],
+ description: 'Controlled selected keys.',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ 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 = {
+ 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 danger 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 = {
+ 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 = {
+ 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 = {
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/menu/snippets.ts b/docs-ui/src/app/components/menu/snippets.ts
new file mode 100644
index 0000000000..ee7e77e8c2
--- /dev/null
+++ b/docs-ui/src/app/components/menu/snippets.ts
@@ -0,0 +1,119 @@
+export const usage = `import { MenuTrigger, Menu, MenuItem } from '@backstage/ui';
+
+
+ Menu
+
+ Item 1
+ Item 2
+
+ `;
+
+export const preview = `
+ Menu
+
+ Edit
+ Duplicate
+ Rename
+
+ }>Share
+ }>Feedback
+
+
+ }>Settings
+
+ Edit
+ Duplicate
+ Rename
+
+
+
+ `;
+
+export const submenu = `
+ Menu
+
+ New File
+
+ Open Recent
+
+ File 1.txt
+ File 2.txt
+
+
+ Save
+
+ `;
+
+export const icons = `
+ Menu
+
+ }>New File
+ }>New Folder
+ }>New Image
+
+ `;
+
+export const sections = `
+ Menu
+
+
+ New
+ Open
+
+
+ Cut
+ Copy
+ Paste
+
+
+ `;
+
+export const separators = `
+ Menu
+
+ New
+ Open
+
+ Save
+ Save As...
+
+ `;
+
+export const links = `
+ Menu
+
+ Home
+ About
+ Contact
+
+ `;
+
+export const autocomplete = `
+ Search
+
+ Option 1
+ Option 2
+ Option 3
+
+ `;
+
+export const autocompleteListbox = `
+ Select
+
+ Option 1
+ Option 2
+ Option 3
+
+ `;
+
+export const autocompleteListboxMultiple = `
+ Multi-select
+
+ Option 1
+ Option 2
+ Option 3
+
+ `;
diff --git a/docs-ui/src/app/components/page.mdx b/docs-ui/src/app/components/page.mdx
index bd18005d19..399d9aafbf 100644
--- a/docs-ui/src/app/components/page.mdx
+++ b/docs-ui/src/app/components/page.mdx
@@ -1,173 +1,9 @@
-import { ComponentCards, ComponentCard } from '@/components/ComponentCards';
-import { LayoutComponents } from '@/components/LayoutComponents';
-import { CodeBlock } from '@/components/CodeBlock';
+import { ComponentGrid } from '@/components/ComponentGrid';
# Components
-## Layout Components
+Below is the full list of components available in the library. Each component is designed to be flexible, accessible,
+and easy to integrate into your plugins. We are actively working on adding more components, so check back regularly
+for updates.
-We built a couple of layout components to help you build responsive elements
-that will be consistent with the rest of your Backstage instance. These
-components are opinionated and use TypeScript to ensure that the props you
-provide are the ones coming from the theme.
-
-
- Hello World
-
- Project 1
- Project 2
-
-
-`}
-/>
-
-
-
-## Components
-
-### Actions
-
-
-
-
-
-
-
-
-### Content display
-
-
-
-
-
-
-### Selection and inputs
-
-
-
-
-
-
-
-
-
-
-
-### Navigation
-
-
-
-
-
-
-
-
-### Images and icons
-
-
-
-
-
-
-### Feedback indicators
-
-
-
-
-
-
-### Typography
-
-
-
-
+
diff --git a/docs-ui/src/app/components/password-field/components.tsx b/docs-ui/src/app/components/password-field/components.tsx
new file mode 100644
index 0000000000..a9f96b9267
--- /dev/null
+++ b/docs-ui/src/app/components/password-field/components.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+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 (
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const WithDescription = () => {
+ return (
+
+ );
+};
+
+export const WithIcon = () => {
+ return (
+ }
+ style={{ maxWidth: '300px' }}
+ />
+ );
+};
+
+export const Validation = () => {
+ return (
+
+ );
+};
diff --git a/docs-ui/src/app/components/password-field/page.mdx b/docs-ui/src/app/components/password-field/page.mdx
new file mode 100644
index 0000000000..2b79425453
--- /dev/null
+++ b/docs-ui/src/app/components/password-field/page.mdx
@@ -0,0 +1,87 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { passwordFieldPropDefs } from './props-definition';
+import {
+ passwordFieldUsageSnippet,
+ withLabelSnippet,
+ sizesSnippet,
+ withDescriptionSnippet,
+ withIconSnippet,
+ validationSnippet,
+} from './snippets';
+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',
+};
+
+
+
+ }
+ code={withLabelSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Sizes
+
+ } code={sizesSnippet} />
+
+### With description
+
+ }
+ code={withDescriptionSnippet}
+/>
+
+### With icon
+
+ }
+ code={withIconSnippet}
+/>
+
+### Validation
+
+ }
+ code={validationSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/password-field/props-definition.tsx b/docs-ui/src/app/components/password-field/props-definition.tsx
new file mode 100644
index 0000000000..63f0f56d25
--- /dev/null
+++ b/docs-ui/src/app/components/password-field/props-definition.tsx
@@ -0,0 +1,77 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const passwordFieldPropDefs: Record = {
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Visual size of the input. Use small for dense layouts,{' '}
+ medium 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 Required .
+ >
+ ),
+ },
+ 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,
+};
diff --git a/docs-ui/src/app/components/password-field/snippets.ts b/docs-ui/src/app/components/password-field/snippets.ts
new file mode 100644
index 0000000000..6bc4b9760b
--- /dev/null
+++ b/docs-ui/src/app/components/password-field/snippets.ts
@@ -0,0 +1,33 @@
+export const passwordFieldUsageSnippet = `import { PasswordField } from '@backstage/ui';
+
+ `;
+
+export const withLabelSnippet = ` `;
+
+export const sizesSnippet = `
+
+
+ `;
+
+export const withDescriptionSnippet = ` `;
+
+export const withIconSnippet = ` }
+/>`;
+
+export const validationSnippet = ` `;
diff --git a/docs-ui/src/app/components/popover/components.tsx b/docs-ui/src/app/components/popover/components.tsx
new file mode 100644
index 0000000000..4135d88059
--- /dev/null
+++ b/docs-ui/src/app/components/popover/components.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import { Popover } from '../../../../../packages/ui/src/components/Popover/Popover';
+import { DialogTrigger } from '../../../../../packages/ui/src/components/Dialog/Dialog';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return (
+
+ Open Popover
+
+ Popover content
+
+
+ );
+};
+
+export const Placement = () => {
+ return (
+
+
+ Top
+
+ Content above trigger
+
+
+
+ Right
+
+ Content to the right
+
+
+
+ Bottom
+
+ Content below trigger
+
+
+
+ Left
+
+ Content to the left
+
+
+
+ );
+};
+
+export const HideArrow = () => {
+ return (
+
+ Open Popover
+
+ Popover without arrow
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/popover/page.mdx b/docs-ui/src/app/components/popover/page.mdx
new file mode 100644
index 0000000000..0d546cdc05
--- /dev/null
+++ b/docs-ui/src/app/components/popover/page.mdx
@@ -0,0 +1,74 @@
+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,
+ 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',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+### DialogTrigger
+
+Wraps the trigger button and popover content.
+
+
+
+
+
+### Popover
+
+Displays floating content with automatic positioning.
+
+
+
+
+
+## Examples
+
+### Placement
+
+ }
+ code={placementSnippet}
+/>
+
+### Hidden arrow
+
+ }
+ code={hideArrowSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/popover/props-definition.ts b/docs-ui/src/app/components/popover/props-definition.ts
new file mode 100644
index 0000000000..563bfa7d66
--- /dev/null
+++ b/docs-ui/src/app/components/popover/props-definition.ts
@@ -0,0 +1,47 @@
+import { childrenPropDefs, classNamePropDefs } from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+
+export const dialogTriggerPropDefs: Record = {
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Whether the popover is open by default (uncontrolled).',
+ },
+ isOpen: {
+ type: 'boolean',
+ description: 'Whether the popover is open (controlled).',
+ },
+ onOpenChange: {
+ type: 'enum',
+ values: ['(isOpen: boolean) => void'],
+ description: 'Handler called when the popover open state changes.',
+ },
+ ...childrenPropDefs,
+};
+
+export const popoverPropDefs: Record = {
+ placement: {
+ type: 'enum',
+ values: ['top', 'right', 'bottom', 'left'],
+ 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,
+};
diff --git a/docs-ui/src/app/components/popover/snippets.ts b/docs-ui/src/app/components/popover/snippets.ts
new file mode 100644
index 0000000000..986b33c770
--- /dev/null
+++ b/docs-ui/src/app/components/popover/snippets.ts
@@ -0,0 +1,49 @@
+export const popoverUsageSnippet = `import { DialogTrigger, Popover, Button, Text } from '@backstage/ui';
+
+
+ Open Popover
+
+ Popover content
+
+ `;
+
+export const defaultSnippet = `
+ Open Popover
+
+ Popover content
+
+ `;
+
+export const placementSnippet = `
+
+ Top
+
+ Content above trigger
+
+
+
+ Right
+
+ Content to the right
+
+
+
+ Bottom
+
+ Content below trigger
+
+
+
+ Left
+
+ Content to the left
+
+
+ `;
+
+export const hideArrowSnippet = `
+ Open Popover
+
+ Popover without arrow
+
+ `;
diff --git a/docs-ui/src/app/components/radio-group/components.tsx b/docs-ui/src/app/components/radio-group/components.tsx
new file mode 100644
index 0000000000..56870c0166
--- /dev/null
+++ b/docs-ui/src/app/components/radio-group/components.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import {
+ RadioGroup,
+ Radio,
+} from '../../../../../packages/ui/src/components/RadioGroup/RadioGroup';
+
+export const Default = () => {
+ return (
+
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
+
+export const Horizontal = () => {
+ return (
+
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
+
+export const DisabledSingle = () => {
+ return (
+
+ Bulbasaur
+
+ Charmander
+
+ Squirtle
+
+ );
+};
+
+export const Validation = () => {
+ return (
+ (value === 'charmander' ? 'Nice try!' : null)}
+ >
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
+
+export const ReadOnly = () => {
+ return (
+
+ Bulbasaur
+ Charmander
+ Squirtle
+
+ );
+};
diff --git a/docs-ui/src/app/components/radio-group/page.mdx b/docs-ui/src/app/components/radio-group/page.mdx
new file mode 100644
index 0000000000..5cc296b959
--- /dev/null
+++ b/docs-ui/src/app/components/radio-group/page.mdx
@@ -0,0 +1,118 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { radioGroupPropDefs, radioPropDefs } from './props-definition';
+import {
+ radioGroupUsageSnippet,
+ defaultSnippet,
+ horizontalSnippet,
+ disabledSnippet,
+ disabledSingleSnippet,
+ validationSnippet,
+ readOnlySnippet,
+} from './snippets';
+import {
+ Default,
+ Horizontal,
+ Disabled,
+ DisabledSingle,
+ Validation,
+ ReadOnly,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { RadioGroupDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ radioGroup: 'https://react-aria.adobe.com/RadioGroup',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+### RadioGroup
+
+
+
+
+
+### Radio
+
+Individual radio button within a group.
+
+
+
+
+
+## Examples
+
+### Horizontal
+
+ }
+ code={horizontalSnippet}
+/>
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+/>
+
+### Disabled single radio
+
+ }
+ code={disabledSingleSnippet}
+/>
+
+### Validation
+
+ }
+ code={validationSnippet}
+/>
+
+### Read only
+
+ }
+ code={readOnlySnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/radio-group/props-definition.tsx b/docs-ui/src/app/components/radio-group/props-definition.tsx
new file mode 100644
index 0000000000..54da42d93a
--- /dev/null
+++ b/docs-ui/src/app/components/radio-group/props-definition.tsx
@@ -0,0 +1,91 @@
+import {
+ classNamePropDefs,
+ childrenPropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const radioGroupPropDefs: Record = {
+ 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 Required 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 = {
+ value: {
+ type: 'string',
+ required: true,
+ description: 'The value of the radio button.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether this radio button is disabled.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/radio-group/snippets.ts b/docs-ui/src/app/components/radio-group/snippets.ts
new file mode 100644
index 0000000000..d0b14ef5c4
--- /dev/null
+++ b/docs-ui/src/app/components/radio-group/snippets.ts
@@ -0,0 +1,57 @@
+export const radioGroupUsageSnippet = `import { RadioGroup, Radio } from '@backstage/ui';
+
+
+ Option 1
+ Option 2
+ `;
+
+export const defaultSnippet = `
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
+
+export const horizontalSnippet = `
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
+
+export const disabledSnippet = `
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
+
+export const disabledSingleSnippet = `
+ Bulbasaur
+
+ Charmander
+
+ Squirtle
+ `;
+
+export const validationSnippet = ` (value === 'charmander' ? 'Nice try!' : null)}
+>
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
+
+export const readOnlySnippet = `
+ Bulbasaur
+ Charmander
+ Squirtle
+ `;
diff --git a/docs-ui/src/app/components/search-field/components.tsx b/docs-ui/src/app/components/search-field/components.tsx
new file mode 100644
index 0000000000..84a4c6018d
--- /dev/null
+++ b/docs-ui/src/app/components/search-field/components.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { SearchField } from '../../../../../packages/ui/src/components/SearchField/SearchField';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const WithLabel = () => {
+ return (
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+
+
+
+ );
+};
+
+export const WithDescription = () => {
+ return (
+
+ );
+};
+
+export const StartCollapsed = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/search-field/page.mdx b/docs-ui/src/app/components/search-field/page.mdx
new file mode 100644
index 0000000000..425dbc8f62
--- /dev/null
+++ b/docs-ui/src/app/components/search-field/page.mdx
@@ -0,0 +1,91 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { searchFieldPropDefs } from './props-definition';
+import {
+ searchFieldUsageSnippet,
+ withLabelSnippet,
+ sizesSnippet,
+ withDescriptionSnippet,
+ startCollapsedSnippet,
+} from './snippets';
+import {
+ WithLabel,
+ Sizes,
+ WithDescription,
+ StartCollapsed,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { SearchFieldDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ searchField: 'https://react-aria.adobe.com/SearchField',
+};
+
+
+
+ }
+ code={withLabelSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Sizes
+
+ }
+ code={sizesSnippet}
+ layout="side-by-side"
+/>
+
+### With description
+
+Add context below the input with the `description` prop.
+
+ }
+ code={withDescriptionSnippet}
+ layout="side-by-side"
+/>
+
+### Collapsible
+
+Use `startCollapsed` for space-constrained layouts where the field expands on focus.
+
+ }
+ code={startCollapsedSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/search-field/props-definition.tsx b/docs-ui/src/app/components/search-field/props-definition.tsx
new file mode 100644
index 0000000000..58d2cb0988
--- /dev/null
+++ b/docs-ui/src/app/components/search-field/props-definition.tsx
@@ -0,0 +1,85 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const searchFieldPropDefs: Record = {
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Visual size of the input. Use small for inline or dense
+ layouts, medium for standalone fields.
+ >
+ ),
+ },
+ label: {
+ type: 'string',
+ description: 'The visible label for the search field.',
+ },
+ secondaryLabel: {
+ type: 'string',
+ description: (
+ <>
+ Secondary label text. Defaults to Required 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,
+};
diff --git a/docs-ui/src/app/components/search-field/snippets.ts b/docs-ui/src/app/components/search-field/snippets.ts
new file mode 100644
index 0000000000..e54bca1bf4
--- /dev/null
+++ b/docs-ui/src/app/components/search-field/snippets.ts
@@ -0,0 +1,20 @@
+export const searchFieldUsageSnippet = `import { SearchField } from '@backstage/ui';
+
+ `;
+
+export const withLabelSnippet = ` `;
+
+export const sizesSnippet = `
+
+
+ `;
+
+export const withDescriptionSnippet = ` `;
+
+export const startCollapsedSnippet = `
+
+
+ `;
diff --git a/docs-ui/src/app/components/select/components.tsx b/docs-ui/src/app/components/select/components.tsx
new file mode 100644
index 0000000000..1cbc323b84
--- /dev/null
+++ b/docs-ui/src/app/components/select/components.tsx
@@ -0,0 +1,150 @@
+'use client';
+
+import { Select } from '../../../../../packages/ui/src/components/Select/Select';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiCloudLine } from '@remixicon/react';
+
+const fontOptions = [
+ { value: 'sans', label: 'Sans-serif' },
+ { value: 'serif', label: 'Serif' },
+ { value: 'mono', label: 'Monospace' },
+ { value: 'cursive', label: 'Cursive' },
+];
+
+const countries = [
+ { value: 'us', label: 'United States' },
+ { value: 'ca', label: 'Canada' },
+ { value: 'mx', label: 'Mexico' },
+ { value: 'uk', label: 'United Kingdom' },
+ { value: 'fr', label: 'France' },
+ { value: 'de', label: 'Germany' },
+ { value: 'it', label: 'Italy' },
+ { value: 'es', label: 'Spain' },
+ { value: 'jp', label: 'Japan' },
+ { value: 'cn', label: 'China' },
+ { value: 'in', label: 'India' },
+ { value: 'br', label: 'Brazil' },
+ { value: 'au', label: 'Australia' },
+];
+
+const skills = [
+ { value: 'react', label: 'React' },
+ { value: 'typescript', label: 'TypeScript' },
+ { value: 'javascript', label: 'JavaScript' },
+ { value: 'python', label: 'Python' },
+ { value: 'java', label: 'Java' },
+ { value: 'csharp', label: 'C#' },
+ { value: 'go', label: 'Go' },
+ { value: 'rust', label: 'Rust' },
+ { value: 'kotlin', label: 'Kotlin' },
+ { value: 'swift', label: 'Swift' },
+];
+
+export const Preview = () => (
+
+);
+
+export const WithLabelAndDescription = () => (
+
+);
+
+export const Sizes = () => (
+
+
+
+
+);
+
+export const WithIcon = () => (
+ }
+ style={{ width: 300 }}
+ />
+);
+
+export const Disabled = () => (
+
+);
+
+export const DisabledOption = () => (
+
+);
+
+export const Searchable = () => (
+
+);
+
+export const MultipleSelection = () => (
+
+);
+
+export const SearchableMultiple = () => (
+
+);
diff --git a/docs-ui/src/app/components/select/page.mdx b/docs-ui/src/app/components/select/page.mdx
new file mode 100644
index 0000000000..8e7df65ef9
--- /dev/null
+++ b/docs-ui/src/app/components/select/page.mdx
@@ -0,0 +1,147 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import {
+ Preview,
+ WithLabelAndDescription,
+ Sizes,
+ WithIcon,
+ Disabled,
+ DisabledOption,
+ Searchable,
+ MultipleSelection,
+ SearchableMultiple,
+} from './components';
+import { selectPropDefs } from './props-definition';
+import {
+ selectUsageSnippet,
+ selectDefaultSnippet,
+ selectDescriptionSnippet,
+ selectSizesSnippet,
+ selectDisabledSnippet,
+ selectResponsiveSnippet,
+ selectIconSnippet,
+ selectSearchableSnippet,
+ selectMultipleSnippet,
+ selectSearchableMultipleSnippet,
+ selectDisabledOptionsSnippet,
+} from './snippets';
+import { PageTitle } from '@/components/PageTitle';
+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',
+};
+
+
+
+ }
+ code={selectDefaultSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Label and description
+
+ }
+ code={selectDescriptionSnippet}
+/>
+
+### Sizes
+
+ }
+ code={selectSizesSnippet}
+/>
+
+### With icon
+
+ }
+ code={selectIconSnippet}
+/>
+
+### Disabled
+
+ }
+ code={selectDisabledSnippet}
+/>
+
+### Disabled options
+
+ }
+ code={selectDisabledOptionsSnippet}
+/>
+
+### Searchable
+
+Enable filtering with the `searchable` prop.
+
+ }
+ code={selectSearchableSnippet}
+/>
+
+### Multiple selection
+
+ }
+ code={selectMultipleSnippet}
+/>
+
+### Searchable multiple
+
+Combine search and multiple selection.
+
+ }
+ code={selectSearchableMultipleSnippet}
+/>
+
+### Responsive
+
+Size can change at different breakpoints.
+
+
+
+
+
+
diff --git a/docs-ui/src/app/components/select/props-definition.tsx b/docs-ui/src/app/components/select/props-definition.tsx
new file mode 100644
index 0000000000..0d1ab22202
--- /dev/null
+++ b/docs-ui/src/app/components/select/props-definition.tsx
@@ -0,0 +1,137 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const selectPropDefs: Record = {
+ 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 Required .
+ >
+ ),
+ },
+ 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'],
+ 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,
+};
diff --git a/docs-ui/src/content/select.props.ts b/docs-ui/src/app/components/select/snippets.ts
similarity index 51%
rename from docs-ui/src/content/select.props.ts
rename to docs-ui/src/app/components/select/snippets.ts
index 05f0d895c9..ca16481eaa 100644
--- a/docs-ui/src/content/select.props.ts
+++ b/docs-ui/src/app/components/select/snippets.ts
@@ -1,115 +1,3 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const selectPropDefs: Record = {
- 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'],
- responsive: false,
- },
- isDisabled: {
- type: 'boolean',
- responsive: false,
- },
- isRequired: {
- type: 'boolean',
- responsive: false,
- },
- isInvalid: {
- type: 'boolean',
- responsive: false,
- },
- onOpenChange: {
- type: 'enum',
- values: ['(isOpen: boolean) => void'],
- responsive: false,
- },
- onSelectionChange: {
- type: 'enum',
- values: ['(key: Key | null) => void', '(keys: Selection) => void'],
- responsive: false,
- description:
- 'Handler called when selection changes. Single mode: receives Key | null. Multiple mode: receives Selection.',
- },
- searchable: {
- type: 'boolean',
- default: 'false',
- responsive: false,
- },
- searchPlaceholder: {
- type: 'string',
- default: 'Search...',
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
export const selectUsageSnippet = `import { Select } from '@backstage/ui';
`;
export const selectDisabledSnippet = ` `;
@@ -209,3 +97,16 @@ export const selectSearchableMultipleSnippet = ` `;
+
+export const selectDisabledOptionsSnippet = ` `;
diff --git a/docs-ui/src/app/components/skeleton/components.tsx b/docs-ui/src/app/components/skeleton/components.tsx
new file mode 100644
index 0000000000..9f83a013db
--- /dev/null
+++ b/docs-ui/src/app/components/skeleton/components.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+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 CardPlaceholder = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export const AvatarWithText = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const Rounded = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/docs-ui/src/content/skeleton.mdx b/docs-ui/src/app/components/skeleton/page.mdx
similarity index 52%
rename from docs-ui/src/content/skeleton.mdx
rename to docs-ui/src/app/components/skeleton/page.mdx
index 07cd4b3332..b7d218af89 100644
--- a/docs-ui/src/content/skeleton.mdx
+++ b/docs-ui/src/app/components/skeleton/page.mdx
@@ -1,18 +1,18 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
-import { SkeletonSnippet } from '@/snippets/stories-snippets';
+import { skeletonPropDefs } from './props-definition';
import {
- skeletonPropDefs,
skeletonUsageSnippet,
- skeletonDefaultSnippet,
- skeletonDemo1Snippet,
- skeletonDemo2Snippet,
-} from './skeleton.props';
+ 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';
-import { SkeletonDefinition } from '../utils/definitions';
+import { SkeletonDefinition } from '../../../utils/definitions';
}
- code={skeletonDefaultSnippet}
+ preview={ }
+ code={cardPlaceholderSnippet}
/>
## Usage
@@ -32,32 +32,28 @@ import { SkeletonDefinition } from '../utils/definitions';
## API reference
+Skeleton extends standard HTML div attributes.
+
## Examples
-### Demo 1
-
-You can use a mix of different sizes to create a more complex skeleton.
+### Rounded
}
- code={skeletonDemo1Snippet}
+ layout="side-by-side"
open
+ preview={ }
+ code={roundedSnippet}
/>
-### Demo 2
-
-You can use a mix of different sizes to create a more complex skeleton.
+### Avatar with text
}
- code={skeletonDemo2Snippet}
+ layout="side-by-side"
open
+ preview={ }
+ code={avatarWithTextSnippet}
/>
diff --git a/docs-ui/src/app/components/skeleton/props-definition.ts b/docs-ui/src/app/components/skeleton/props-definition.ts
new file mode 100644
index 0000000000..8c14c4c0ec
--- /dev/null
+++ b/docs-ui/src/app/components/skeleton/props-definition.ts
@@ -0,0 +1,28 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const skeletonPropDefs: Record = {
+ width: {
+ type: 'string',
+ default: '80',
+ description:
+ 'The width of the skeleton. Accepts a number (pixels) or CSS string value.',
+ },
+ height: {
+ type: 'string',
+ default: '24',
+ description:
+ 'The height of the skeleton. Accepts a number (pixels) or CSS string value.',
+ },
+ rounded: {
+ type: 'boolean',
+ default: 'false',
+ description:
+ 'Whether to apply fully rounded corners (for circular shapes).',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/skeleton/snippets.ts b/docs-ui/src/app/components/skeleton/snippets.ts
new file mode 100644
index 0000000000..84bbec1616
--- /dev/null
+++ b/docs-ui/src/app/components/skeleton/snippets.ts
@@ -0,0 +1,30 @@
+export const skeletonUsageSnippet = `import { Skeleton } from '@backstage/ui';
+
+ `;
+
+export const cardPlaceholderSnippet = `
+
+
+
+
+
+ `;
+
+export const avatarWithTextSnippet = `
+
+
+
+
+
+
+
+
+ `;
+
+export const roundedSnippet = `
+
+
+
+
+
+ `;
diff --git a/docs-ui/src/app/components/switch/components.tsx b/docs-ui/src/app/components/switch/components.tsx
new file mode 100644
index 0000000000..604d9d68b1
--- /dev/null
+++ b/docs-ui/src/app/components/switch/components.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import { Switch } from '../../../../../packages/ui/src/components/Switch/Switch';
+
+export const Default = () => {
+ return ;
+};
+
+export const Disabled = () => {
+ return ;
+};
diff --git a/docs-ui/src/app/components/switch/page.mdx b/docs-ui/src/app/components/switch/page.mdx
new file mode 100644
index 0000000000..1f893aea36
--- /dev/null
+++ b/docs-ui/src/app/components/switch/page.mdx
@@ -0,0 +1,42 @@
+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';
+import { PageTitle } from '@/components/PageTitle';
+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',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Disabled
+
+ } code={disabledSnippet} />
+
+
+
+
diff --git a/docs-ui/src/app/components/switch/props-definition.ts b/docs-ui/src/app/components/switch/props-definition.ts
new file mode 100644
index 0000000000..eeab3e64b8
--- /dev/null
+++ b/docs-ui/src/app/components/switch/props-definition.ts
@@ -0,0 +1,45 @@
+import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+
+export const switchPropDefs: Record = {
+ label: {
+ type: 'string',
+ description: 'Text label displayed next to the switch.',
+ },
+ isSelected: {
+ type: 'boolean',
+ description:
+ 'Controlled selected state. Use with onChange for controlled behavior.',
+ },
+ defaultSelected: {
+ type: 'boolean',
+ description: 'Initial selected state for uncontrolled usage.',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(isSelected: boolean) => void'],
+ description: 'Called when the switch state changes.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Prevents user interaction when true.',
+ },
+ isReadOnly: {
+ type: 'boolean',
+ description: 'Makes the switch non-interactive but still focusable.',
+ },
+ name: {
+ type: 'string',
+ description: 'Form field name for form submission.',
+ },
+ value: {
+ type: 'string',
+ description: 'Form field value submitted when selected.',
+ },
+ autoFocus: {
+ type: 'boolean',
+ description: 'Focuses the switch on mount.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/switch/snippets.ts b/docs-ui/src/app/components/switch/snippets.ts
new file mode 100644
index 0000000000..694f15106c
--- /dev/null
+++ b/docs-ui/src/app/components/switch/snippets.ts
@@ -0,0 +1,7 @@
+export const snippetUsage = `import { Switch } from '@backstage/ui';
+
+ `;
+
+export const defaultSnippet = ` `;
+
+export const disabledSnippet = ` `;
diff --git a/docs-ui/src/app/components/table/components.tsx b/docs-ui/src/app/components/table/components.tsx
new file mode 100644
index 0000000000..d26deeb5d3
--- /dev/null
+++ b/docs-ui/src/app/components/table/components.tsx
@@ -0,0 +1,554 @@
+'use client';
+
+import { useState } from 'react';
+import {
+ Table,
+ TableRoot,
+ TableHeader,
+ TableBody,
+ Column,
+ Row,
+ CellProfile,
+ CellText,
+ useTable,
+ type ColumnConfig,
+} from '../../../../../packages/ui/src/components/Table';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { SearchField } from '../../../../../packages/ui/src/components/SearchField/SearchField';
+import {
+ RadioGroup,
+ Radio,
+} from '../../../../../packages/ui/src/components/RadioGroup';
+import { data as rockBandData } from '../../../../../packages/ui/src/components/Table/stories/mocked-data4';
+import { data as catalogData } from '../../../../../packages/ui/src/components/Table/stories/mocked-data1';
+import { MemoryRouter } from 'react-router-dom';
+
+// =============================================================================
+// Types
+// =============================================================================
+
+type RockBandItem = (typeof rockBandData)[0];
+type CatalogItem = (typeof catalogData)[0];
+
+// =============================================================================
+// Hero Example
+// =============================================================================
+
+const heroColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Band name',
+ isRowHeader: true,
+ defaultWidth: '3fr',
+ cell: item => (
+
+ ),
+ },
+ {
+ id: 'genre',
+ label: 'Genre',
+ defaultWidth: '3fr',
+ cell: item => ,
+ },
+ {
+ id: 'yearFormed',
+ label: 'Year formed',
+ defaultWidth: '1fr',
+ cell: item => ,
+ },
+ {
+ id: 'albums',
+ label: 'Albums',
+ defaultWidth: '1fr',
+ cell: item => ,
+ },
+];
+
+export function HeroExample() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => rockBandData,
+ paginationOptions: { pageSize: 5 },
+ });
+
+ return (
+
+
+
+ );
+}
+
+// =============================================================================
+// Sorting Example
+// =============================================================================
+
+const sortingColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'lifecycle',
+ label: 'Lifecycle',
+ isSortable: true,
+ cell: item => ,
+ },
+];
+
+export function SortingExample() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => catalogData,
+ paginationOptions: { pageSize: 5 },
+ initialSort: { column: 'name', direction: 'ascending' },
+ sortFn: (items, { column, direction }) => {
+ return [...items].sort((a, b) => {
+ let aVal: string;
+ let bVal: string;
+ if (column === 'owner') {
+ aVal = a.owner.name;
+ bVal = b.owner.name;
+ } else {
+ aVal = String(a[column as keyof CatalogItem]);
+ bVal = String(b[column as keyof CatalogItem]);
+ }
+ const cmp = aVal.localeCompare(bVal);
+ return direction === 'descending' ? -cmp : cmp;
+ });
+ },
+ });
+
+ return (
+
+
+
+ );
+}
+
+// =============================================================================
+// Search Example
+// =============================================================================
+
+const searchColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+export function SearchExample() {
+ const { tableProps, search } = useTable({
+ mode: 'complete',
+ getData: () => catalogData,
+ paginationOptions: { pageSize: 5 },
+ searchFn: (items, query) => {
+ const lowerQuery = query.toLocaleLowerCase('en-US');
+ return items.filter(
+ item =>
+ item.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
+ item.owner.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
+ item.type.toLocaleLowerCase('en-US').includes(lowerQuery),
+ );
+ },
+ });
+
+ return (
+
+
+
+ No results found for "{search.value}"
+ ) : (
+ No data available
+ )
+ }
+ {...tableProps}
+ />
+
+
+ );
+}
+
+// =============================================================================
+// Selection Example
+// =============================================================================
+
+const selectionColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+export function SelectionExample() {
+ const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>(
+ 'multiple',
+ );
+ const [selectionBehavior, setSelectionBehavior] = useState<
+ 'toggle' | 'replace'
+ >('toggle');
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => catalogData.slice(0, 5),
+ });
+
+ return (
+
+
+
+
+
+
+ Selection mode:
+
+ {
+ setSelectionMode(value as 'single' | 'multiple');
+ setSelected(new Set());
+ }}
+ >
+ single
+ multiple
+
+
+
+
+ Selection behavior:
+
+ {
+ setSelectionBehavior(value as 'toggle' | 'replace');
+ setSelected(new Set());
+ }}
+ >
+ toggle
+ replace
+
+
+
+
+
+ );
+}
+
+// =============================================================================
+// Row Actions Example
+// =============================================================================
+
+export function RowActionsExample() {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => catalogData.slice(0, 5),
+ });
+
+ return (
+
+ alert(`Clicked: ${item.name}`),
+ }}
+ selection={{
+ mode: 'multiple',
+ behavior: 'toggle',
+ selected,
+ onSelectionChange: setSelected,
+ }}
+ {...tableProps}
+ />
+
+ );
+}
+
+// =============================================================================
+// Empty State Example
+// =============================================================================
+
+const emptyStateColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+export function EmptyStateExample() {
+ const emptyData: CatalogItem[] = [];
+
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => emptyData,
+ });
+
+ return (
+
+ No items yet. Create one to get started.}
+ {...tableProps}
+ />
+
+ );
+}
+
+// =============================================================================
+// Combined Example
+// =============================================================================
+
+const combinedColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ isSortable: true,
+ cell: item => ,
+ },
+ {
+ id: 'type',
+ label: 'Type',
+ isSortable: true,
+ cell: item => ,
+ },
+];
+
+export function CombinedExample() {
+ const [selected, setSelected] = useState | 'all'>(
+ new Set(),
+ );
+
+ const { tableProps, search } = useTable({
+ mode: 'offset',
+ getData: async ({ offset, pageSize, search: query, sort }) => {
+ // Simulate network delay
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Filter data
+ let filtered = catalogData;
+ if (query) {
+ const lowerQuery = query.toLocaleLowerCase('en-US');
+ filtered = filtered.filter(
+ item =>
+ item.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
+ item.owner.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
+ item.type.toLocaleLowerCase('en-US').includes(lowerQuery),
+ );
+ }
+
+ // Sort data
+ if (sort?.column) {
+ filtered = [...filtered].sort((a, b) => {
+ let aVal: string;
+ let bVal: string;
+ if (sort.column === 'owner') {
+ aVal = a.owner.name;
+ bVal = b.owner.name;
+ } else {
+ aVal = String(a[sort.column as keyof CatalogItem]);
+ bVal = String(b[sort.column as keyof CatalogItem]);
+ }
+ const cmp = aVal.localeCompare(bVal);
+ return sort.direction === 'descending' ? -cmp : cmp;
+ });
+ }
+
+ // Paginate
+ const data = filtered.slice(offset, offset + pageSize);
+
+ return { data, totalCount: filtered.length };
+ },
+ paginationOptions: { pageSize: 5, pageSizeOptions: [5, 10, 20] },
+ initialSort: { column: 'name', direction: 'ascending' },
+ });
+
+ return (
+
+
+
+ alert(`Clicked: ${item.name}`),
+ }}
+ selection={{
+ mode: 'multiple',
+ behavior: 'toggle',
+ selected,
+ onSelectionChange: setSelected,
+ }}
+ emptyState={
+ search.value ? (
+ No results match your search
+ ) : (
+ No items available
+ )
+ }
+ {...tableProps}
+ />
+
+
+ );
+}
+
+// =============================================================================
+// Custom Row Example
+// =============================================================================
+
+const customRowColumns: ColumnConfig[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ cell: item => ,
+ },
+ {
+ id: 'owner',
+ label: 'Owner',
+ cell: item => ,
+ },
+ {
+ id: 'lifecycle',
+ label: 'Lifecycle',
+ cell: item => ,
+ },
+];
+
+export function CustomRowExample() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => catalogData.slice(0, 5),
+ });
+
+ return (
+
+ (
+
+ {column => column.cell(item)}
+
+ )}
+ {...tableProps}
+ />
+
+ );
+}
+
+// =============================================================================
+// Primitives Example
+// =============================================================================
+
+export function PrimitivesExample() {
+ const items = catalogData.slice(0, 5);
+
+ return (
+
+
+
+ Name
+ Owner
+ Type
+
+
+ {items.map(item => (
+
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/docs-ui/src/app/components/table/page.mdx b/docs-ui/src/app/components/table/page.mdx
new file mode 100644
index 0000000000..6eb280cbe2
--- /dev/null
+++ b/docs-ui/src/app/components/table/page.mdx
@@ -0,0 +1,295 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import {
+ HeroExample,
+ SortingExample,
+ SearchExample,
+ SelectionExample,
+ RowActionsExample,
+ EmptyStateExample,
+ CombinedExample,
+ CustomRowExample,
+ PrimitivesExample,
+} from './components';
+import {
+ tableReturnColumns,
+ useTableOptionsPropDefs,
+ useTableReturnPropDefs,
+ tablePropDefs,
+ tablePaginationPropDefs,
+ columnConfigPropDefs,
+ cellTextPropDefs,
+ cellProfilePropDefs,
+ tableRootPropDefs,
+ columnPropDefs,
+ rowPropDefs,
+} from './props-definition';
+import {
+ tableUsageSnippet,
+ tableHeroSnippet,
+ tableConceptsSnippet,
+ tableSortingSnippet,
+ tablePaginationSnippet,
+ tableSearchSnippet,
+ tableSelectionSnippet,
+ tableRowActionsHrefSnippet,
+ tableRowActionsClickSnippet,
+ tableRowActionsDisabledSnippet,
+ tableEmptyStateSnippet,
+ tableOffsetPaginationSnippet,
+ tableCursorPaginationSnippet,
+ tableCombinedSnippet,
+ tableCustomRowSnippet,
+ tablePrimitivesSnippet,
+} from './snippets';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { TableDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ table: 'https://react-aria.adobe.com/Table#table',
+ tableHeader: 'https://react-aria.adobe.com/Table#tableheader',
+ tableBody: 'https://react-aria.adobe.com/Table#tablebody',
+ column: 'https://react-aria.adobe.com/Table#column',
+ row: 'https://react-aria.adobe.com/Table#row',
+ cell: 'https://react-aria.adobe.com/Table#cell',
+};
+
+
+
+ } code={tableHeroSnippet} />
+
+## Usage
+
+
+
+## Core Concepts
+
+The Table component is designed around a hook + component pattern:
+
+- **`useTable`** manages data fetching, pagination state, sorting, and filtering. It returns `tableProps` that you spread onto the Table component.
+- **`Table`** handles rendering - columns, rows, cells, and interactions.
+
+The hook supports three modes for different data scenarios:
+
+- **`complete`** - You have all data available upfront (client-side)
+- **`offset`** - Your API uses offset/limit pagination
+- **`cursor`** - Your API uses cursor-based pagination
+
+
+
+**Controlled vs uncontrolled state**
+
+By default, `useTable` manages sort, search, and filter state internally. Pass `initialSort`, `initialSearch`, or `initialFilter` to set starting values.
+
+For full control over state, use the controlled props instead:
+
+- `sort` / `onSortChange`
+- `search` / `onSearchChange`
+- `filter` / `onFilterChange`
+
+## Common Patterns
+
+### Sorting
+
+Columns can be made sortable by adding `isSortable: true` to the column configuration. When sortable, clicking a column header cycles through ascending, descending, and unsorted states. A visual indicator shows the current sort direction.
+
+With `mode: 'complete'`, sorting happens client-side. Provide a `sortFn` that receives the full dataset and the current sort descriptor, and returns the sorted array. You can also set an `initialSort` to define the default sort when the table first renders. For server-side sorting with `offset` or `cursor` modes, see [Server-Side Data](#server-side-data).
+
+ } code={tableSortingSnippet} />
+
+### Pagination
+
+Configure page size and available options through `paginationOptions`. The table displays navigation controls automatically.
+
+
+
+### Search
+
+The `useTable` hook returns a `search` object with `value` and `onChange` properties, ready to connect to a search input. With `mode: 'complete'`, provide a `searchFn` that filters the dataset based on the search query.
+
+The search state is debounced internally, so rapid typing doesn't trigger excessive re-filtering. When the search query changes, pagination resets to the first page automatically.
+
+For server-side search with `offset` or `cursor` modes, the search query is passed to your `getData` function. See [Server-Side Data](#server-side-data).
+
+ } code={tableSearchSnippet} />
+
+### Row Selection
+
+Tables support row selection with two configuration options: `mode` and `behavior`.
+
+Selection mode controls how many rows can be selected:
+
+- `single` - Only one row can be selected at a time
+- `multiple` - Any number of rows can be selected, with a header checkbox for select-all
+
+Selection behavior controls the interaction style:
+
+- `toggle` - Checkboxes appear for selection. Click a checkbox to select/deselect.
+- `replace` - No checkboxes. Click a row to select it (replacing previous selection). Use Cmd/Ctrl+click to select multiple rows.
+
+Selection state can be managed in two ways:
+
+- **Controlled** - Pass both `selected` and `onSelectionChange` to fully manage state externally
+- **Uncontrolled** - Pass only `onSelectionChange` to let the Table manage state while receiving change notifications
+
+ } code={tableSelectionSnippet} />
+
+### Row Actions
+
+Rows can respond to user interaction in two ways: navigating to a URL or triggering a callback.
+
+Use `getHref` when rows should link to detail pages. The entire row becomes clickable and navigates on click. Links within cells remain independently clickable.
+
+
+
+Use `onClick` for custom actions like opening a panel or triggering a dialog.
+
+
+
+When combining row actions with selection, the interaction depends on selection behavior:
+
+- **`toggle`**: Clicking a row triggers its action when nothing is selected. Once any row is selected, clicking anywhere on the row toggles selection instead.
+- **`replace`**: Single click selects the row. Double-click triggers the row action.
+
+You can also disable specific rows from being clicked using `getIsDisabled`:
+
+
+
+ } code={tableRowActionsHrefSnippet} />
+
+### Empty State
+
+When the table has no data to display, provide an `emptyState` to show a helpful message instead of an empty table body. Consider providing different messages depending on context - an empty search result is different from a table with no data at all.
+
+ } code={tableEmptyStateSnippet} />
+
+## Server-Side Data
+
+When your data comes from an API with server-side pagination, use `offset` or `cursor` mode instead of `complete`. The key difference: instead of providing all data upfront, you provide a `getData` function that fetches data for the current page.
+
+### Offset Pagination
+
+Use `mode: 'offset'` when your API accepts `offset` and `limit` (or `skip` and `take`) parameters.
+
+
+
+The `signal` parameter is an `AbortSignal` - pass it to `fetch` so in-flight requests are cancelled when the user navigates away or triggers a new query.
+
+### Cursor Pagination
+
+Use `mode: 'cursor'` when your API uses cursor-based pagination (common with GraphQL or APIs that return `nextCursor`/`prevCursor` tokens).
+
+
+
+### Loading States
+
+When fetching data, the table shows a loading state. If the user triggers a new query (by paginating, sorting, or searching) while previous data is displayed, the table enters a "stale" state where it continues showing the previous data until new data arrives. This prevents jarring layout shifts.
+
+You can access these states via `tableProps.loading` and `tableProps.isStale` if you need to render additional loading indicators.
+
+## Combining Features
+
+Real-world tables often combine multiple features. Here's an example with search, sorting, pagination, and row selection working together.
+
+ } code={tableCombinedSnippet} />
+
+## Custom Tables
+
+For most use cases, `useTable` + `Table` provides everything you need. When you need more control, there are several ways you can customize your table.
+
+### Custom Row and Header Rendering
+
+Use a render function for `rowConfig` when you need to customize individual rows based on their data - for example, highlighting rows that require attention. Use `header` in column config to customize header cell content.
+
+ } code={tableCustomRowSnippet} />
+
+### Using Primitives Directly
+
+When the `Table` component doesn't support your use case, you can compose with the lower-level primitives: `TableRoot`, `TableHeader`, `TableBody`, `Column`, and `Row`.
+
+ } code={tablePrimitivesSnippet} />
+
+## API Reference
+
+### useTable
+
+The `useTable` hook manages data fetching, pagination, sorting, and filtering.
+
+**Options**
+
+
+
+**Return Value**
+
+
+
+### Table
+
+The main table component.
+
+
+
+### ColumnConfig
+
+
+
+### CellText
+
+
+
+
+
+### CellProfile
+
+
+
+
+
+### TablePagination
+
+
+
+### Primitives
+
+Low-level components for building custom table layouts.
+
+#### TableRoot
+
+
+
+
+
+#### TableHeader
+
+
+
+#### TableBody
+
+
+
+#### Column
+
+
+
+
+
+#### Row
+
+
+
+
+
+#### Cell
+
+
+
+
+
+
diff --git a/docs-ui/src/app/components/table/props-definition.tsx b/docs-ui/src/app/components/table/props-definition.tsx
new file mode 100644
index 0000000000..be24e86bf0
--- /dev/null
+++ b/docs-ui/src/app/components/table/props-definition.tsx
@@ -0,0 +1,461 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+// =============================================================================
+// PropsTable Column Configuration (Table docs use description instead of responsive)
+// =============================================================================
+
+// For return values (no default column)
+export const tableReturnColumns = [
+ { key: 'prop' as const, width: '15%' },
+ { key: 'type' as const, width: '25%' },
+ { key: 'description' as const, width: '60%' },
+];
+
+// =============================================================================
+// useTable Hook
+// =============================================================================
+
+export const useTableOptionsPropDefs: Record = {
+ mode: {
+ type: 'enum',
+ values: ['complete', 'offset', 'cursor'],
+ description: (
+ <>
+ Data fetching strategy (required). Use complete for
+ client-side data, offset for offset/limit APIs,{' '}
+ cursor for cursor-based pagination.
+ >
+ ),
+ },
+ getData: {
+ type: 'enum',
+ values: ['function'],
+ description:
+ 'Function that returns or fetches data (required for "offset" and "cursor" modes). For the "complete" mode, either this or `data` must be provided. Signature varies by mode.',
+ },
+ data: {
+ type: 'enum',
+ values: ['T[]'],
+ description:
+ 'The data for the table. Only applicable for "complete" mode, and either this or `getData` must be provided.',
+ },
+ paginationOptions: {
+ type: 'enum',
+ values: ['object'],
+ description: (
+ <>
+ Pagination configuration including pageSize ,{' '}
+ pageSizeOptions , and initialOffset .
+ >
+ ),
+ },
+ // Uncontrolled state
+ initialSort: {
+ type: 'enum',
+ values: ['SortDescriptor'],
+ description: 'Default sort configuration on first render (uncontrolled).',
+ },
+ initialSearch: {
+ type: 'string',
+ description: 'Default search value on first render (uncontrolled).',
+ },
+ initialFilter: {
+ type: 'enum',
+ values: ['TFilter'],
+ description: 'Default filter value on first render (uncontrolled).',
+ },
+ // Controlled state
+ sort: {
+ type: 'enum',
+ values: ['SortDescriptor'],
+ description: 'Current sort state (controlled).',
+ },
+ onSortChange: {
+ type: 'enum',
+ values: ['(sort: SortDescriptor) => void'],
+ description: 'Sort change handler (controlled).',
+ },
+ search: {
+ type: 'string',
+ description: 'Current search value (controlled).',
+ },
+ onSearchChange: {
+ type: 'enum',
+ values: ['(search: string) => void'],
+ description: 'Search change handler (controlled).',
+ },
+ filter: {
+ type: 'enum',
+ values: ['TFilter'],
+ description: 'Current filter value (controlled).',
+ },
+ onFilterChange: {
+ type: 'enum',
+ values: ['(filter: TFilter) => void'],
+ description: 'Filter change handler (controlled).',
+ },
+ // Client-side functions
+ sortFn: {
+ type: 'enum',
+ values: ['(data, sort) => data'],
+ description: (
+ <>
+ Client-side sort function. Only used with complete mode.
+ >
+ ),
+ },
+ searchFn: {
+ type: 'enum',
+ values: ['(data, query) => data'],
+ description: (
+ <>
+ Client-side search function. Only used with complete mode.
+ >
+ ),
+ },
+ filterFn: {
+ type: 'enum',
+ values: ['(data, filter) => data'],
+ description: (
+ <>
+ Client-side filter function. Only used with complete mode.
+ >
+ ),
+ },
+};
+
+export const useTableReturnPropDefs: Record = {
+ tableProps: {
+ type: 'enum',
+ values: ['object'],
+ description: (
+ <>
+ Props to spread onto the Table component. Includes data,
+ loading, error, pagination, and sort state.
+ >
+ ),
+ },
+ reload: {
+ type: 'enum',
+ values: ['() => void'],
+ description: 'Function to trigger a data refetch.',
+ },
+ search: {
+ type: 'enum',
+ values: ['{ value, onChange }'],
+ description: (
+ <>
+ Search state object for binding to a SearchField component.
+ >
+ ),
+ },
+ filter: {
+ type: 'enum',
+ values: ['{ value, onChange }'],
+ description: 'Filter state object for binding to filter controls.',
+ },
+};
+
+// =============================================================================
+// Table Component
+// =============================================================================
+
+export const tablePropDefs: Record = {
+ columnConfig: {
+ type: 'enum',
+ values: ['ColumnConfig[]'],
+ description:
+ 'Array of column configurations defining how each column renders.',
+ },
+ data: {
+ type: 'enum',
+ values: ['T[]'],
+ description: 'Array of data items to display in the table.',
+ },
+ loading: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether the table is in a loading state.',
+ },
+ isStale: {
+ type: 'boolean',
+ default: 'false',
+ description:
+ 'Whether the displayed data is stale while new data is loading.',
+ },
+ error: {
+ type: 'enum',
+ values: ['Error'],
+ description: 'Error object if data fetching failed.',
+ },
+ pagination: {
+ type: 'enum',
+ values: ['TablePaginationType'],
+ description: (
+ <>
+ Pagination configuration (required). Use{' '}
+ {'{ type: "none" }'} to disable or{' '}
+ {'{ type: "page", ...props }'} for pagination.
+ >
+ ),
+ },
+ sort: {
+ type: 'enum',
+ values: ['SortState'],
+ description: 'Sort state including current descriptor and change handler.',
+ },
+ rowConfig: {
+ type: 'enum',
+ values: ['RowConfig | RowRenderFn'],
+ description: (
+ <>
+ Row configuration object with getHref , onClick
+ , getIsDisabled , or a render function for custom rows.
+ >
+ ),
+ },
+ selection: {
+ type: 'enum',
+ values: ['TableSelection'],
+ description:
+ 'Selection configuration including mode, behavior, selected keys, and change handler.',
+ },
+ emptyState: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Content to display when the table has no data.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
+
+// =============================================================================
+// ColumnConfig
+// =============================================================================
+
+export const columnConfigPropDefs: Record = {
+ id: {
+ type: 'string',
+ description: 'Unique identifier for the column.',
+ },
+ label: {
+ type: 'string',
+ description: 'Display label for the column header.',
+ },
+ cell: {
+ type: 'enum',
+ values: ['(item) => ReactNode'],
+ description: 'Render function for cell content.',
+ },
+ header: {
+ type: 'enum',
+ values: ['() => ReactNode'],
+ description: 'Optional custom render function for the header cell.',
+ },
+ isSortable: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether the column supports sorting.',
+ },
+ isHidden: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether the column is hidden.',
+ },
+ isRowHeader: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Whether this column is the row header for accessibility.',
+ },
+ width: {
+ type: 'enum',
+ values: ['ColumnSize'],
+ description: 'Current width of the column.',
+ },
+ defaultWidth: {
+ type: 'enum',
+ values: ['ColumnSize'],
+ description: (
+ <>
+ Default width of the column (e.g., 1fr , 200px
+ ).
+ >
+ ),
+ },
+ minWidth: {
+ type: 'enum',
+ values: ['ColumnStaticSize'],
+ description: 'Minimum width of the column.',
+ },
+ maxWidth: {
+ type: 'enum',
+ values: ['ColumnStaticSize'],
+ description: 'Maximum width of the column.',
+ },
+};
+
+// =============================================================================
+// CellText
+// Note: Extends React Aria Cell props
+// =============================================================================
+
+export const cellTextPropDefs: Record = {
+ title: {
+ type: 'string',
+ description: 'Primary text content of the cell.',
+ },
+ description: {
+ type: 'string',
+ description: 'Secondary description text displayed below the title.',
+ },
+ color: {
+ type: 'enum',
+ values: ['TextColors'],
+ description: 'Text color variant.',
+ },
+ leadingIcon: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon displayed before the text content.',
+ },
+ href: {
+ type: 'string',
+ description: 'URL to navigate to when the cell is clicked.',
+ },
+};
+
+// =============================================================================
+// CellProfile
+// Note: Extends React Aria Cell props
+// =============================================================================
+
+export const cellProfilePropDefs: Record = {
+ name: {
+ type: 'string',
+ description: 'Name to display.',
+ },
+ src: {
+ type: 'string',
+ description: 'URL of the avatar image.',
+ },
+ description: {
+ type: 'string',
+ description: 'Secondary text displayed below the name.',
+ },
+ href: {
+ type: 'string',
+ description: 'URL to navigate to when clicked.',
+ },
+ color: {
+ type: 'enum',
+ values: ['TextColors'],
+ description: 'Text color variant.',
+ },
+};
+
+// =============================================================================
+// TablePagination
+// =============================================================================
+
+export const tablePaginationPropDefs: Record = {
+ pageSize: {
+ type: 'number',
+ description: 'Number of items per page.',
+ },
+ pageSizeOptions: {
+ type: 'enum',
+ values: ['number[]'],
+ description: 'Available page size options for the dropdown.',
+ },
+ offset: {
+ type: 'number',
+ description: 'Current offset (starting index) in the data.',
+ },
+ totalCount: {
+ type: 'number',
+ description: 'Total number of items across all pages.',
+ },
+ hasNextPage: {
+ type: 'boolean',
+ description: 'Whether there is a next page available.',
+ },
+ hasPreviousPage: {
+ type: 'boolean',
+ description: 'Whether there is a previous page available.',
+ },
+ onNextPage: {
+ type: 'enum',
+ values: ['() => void'],
+ description: 'Handler called when navigating to the next page.',
+ },
+ onPreviousPage: {
+ type: 'enum',
+ values: ['() => void'],
+ description: 'Handler called when navigating to the previous page.',
+ },
+ onPageSizeChange: {
+ type: 'enum',
+ values: ['(size: number) => void'],
+ description: 'Handler called when the page size changes.',
+ },
+ showPageSizeOptions: {
+ type: 'boolean',
+ default: 'true',
+ description: 'Whether to show the page size dropdown.',
+ },
+ getLabel: {
+ type: 'enum',
+ values: ['(props) => string'],
+ description: 'Custom function to generate the pagination label text.',
+ },
+};
+
+// =============================================================================
+// Primitives
+// =============================================================================
+
+export const tableRootPropDefs: Record = {
+ stale: {
+ type: 'boolean',
+ default: 'false',
+ description: (
+ <>
+ Whether the table data is stale (e.g., while fetching new data). Adds{' '}
+ aria-busy attribute.
+ >
+ ),
+ },
+};
+
+export const columnPropDefs: Record = {
+ isRowHeader: {
+ type: 'boolean',
+ description: 'Whether this column is a row header for accessibility.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Column header content.',
+ },
+};
+
+export const rowPropDefs: Record = {
+ id: {
+ type: 'enum',
+ values: ['string | number'],
+ description: 'Unique identifier for the row.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode | ((column) => ReactNode)'],
+ description:
+ 'Row content. Can be a render function receiving column config.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/table/snippets.ts b/docs-ui/src/app/components/table/snippets.ts
new file mode 100644
index 0000000000..ab4bf53a58
--- /dev/null
+++ b/docs-ui/src/app/components/table/snippets.ts
@@ -0,0 +1,359 @@
+// =============================================================================
+// Usage
+// =============================================================================
+
+export const tableUsageSnippet = `import { Table, useTable, CellText, type ColumnConfig } from '@backstage/ui';
+
+const columns: ColumnConfig[] = [
+ { id: 'name', label: 'Name', isRowHeader: true, cell: item => },
+ { id: 'owner', label: 'Owner', cell: item => },
+];
+
+function MyTable() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+ });
+
+ return ;
+}`;
+
+// =============================================================================
+// Hero / Quick Start
+// =============================================================================
+
+export const tableHeroSnippet = `import { Table, CellText, useTable, type ColumnConfig } from '@backstage/ui';
+
+const data = [
+ { id: 1, name: 'Service A', owner: 'Team Alpha', type: 'service' },
+ { id: 2, name: 'Service B', owner: 'Team Beta', type: 'website' },
+ { id: 3, name: 'Library C', owner: 'Team Gamma', type: 'library' },
+];
+
+const columns: ColumnConfig[] = [
+ { id: 'name', label: 'Name', isRowHeader: true, cell: item => },
+ { id: 'owner', label: 'Owner', cell: item => },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+function MyTable() {
+ const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+ });
+
+ return ;
+}`;
+
+// =============================================================================
+// Core Concepts
+// =============================================================================
+
+export const tableConceptsSnippet = `// What useTable returns
+const {
+ tableProps, // Spread onto
+ reload, // Trigger data refetch
+ search, // { value, onChange } for search input
+ filter, // { value, onChange } for filters
+} = useTable({ ... });`;
+
+// =============================================================================
+// Common Patterns
+// =============================================================================
+
+export const tableSortingSnippet = `const columns: ColumnConfig- [] = [
+ { id: 'name', label: 'Name', isSortable: true, cell: item =>
},
+ { id: 'owner', label: 'Owner', isSortable: true, cell: item => },
+ { id: 'type', label: 'Type', cell: item => },
+];
+
+const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+ initialSort: { column: 'name', direction: 'ascending' },
+ sortFn: (items, { column, direction }) => {
+ return [...items].sort((a, b) => {
+ const aVal = String(a[column]);
+ const bVal = String(b[column]);
+ const cmp = aVal.localeCompare(bVal);
+ return direction === 'descending' ? -cmp : cmp;
+ });
+ },
+});
+
+return ;`;
+
+export const tablePaginationSnippet = `const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+ paginationOptions: {
+ pageSize: 10,
+ pageSizeOptions: [10, 25, 50],
+ },
+});`;
+
+export const tableSearchSnippet = `const { tableProps, search } = useTable({
+ mode: 'complete',
+ data,
+ searchFn: (items, query) => {
+ const lowerQuery = query.toLowerCase();
+ return items.filter(item =>
+ item.name.toLowerCase().includes(lowerQuery) ||
+ item.owner.toLowerCase().includes(lowerQuery)
+ );
+ },
+});
+
+return (
+ <>
+
+
+ >
+);`;
+
+export const tableSelectionSnippet = `const [selected, setSelected] = useState | 'all'>(new Set());
+
+const { tableProps } = useTable({
+ mode: 'complete',
+ data,
+});
+
+return (
+
+);`;
+
+export const tableRowActionsHrefSnippet = ` \`/catalog/\${item.namespace}/\${item.name}\`
+ }}
+ {...tableProps}
+/>`;
+
+export const tableRowActionsClickSnippet = ` openDetailPanel(item)
+ }}
+ {...tableProps}
+/>`;
+
+export const tableRowActionsDisabledSnippet = ` openDetailPanel(item),
+ getIsDisabled: item => item.status === 'archived',
+ }}
+ {...tableProps}
+/>`;
+
+export const tableEmptyStateSnippet = `const { tableProps, search } = useTable({
+ mode: 'complete',
+ data,
+ searchFn: (items, query) => { /* ... */ },
+});
+
+return (
+ No results match "{search.value}"
+ : No items yet. Create one to get started.
+ }
+ {...tableProps}
+ />
+);`;
+
+// =============================================================================
+// Server-Side Data
+// =============================================================================
+
+export const tableOffsetPaginationSnippet = `const { tableProps } = useTable({
+ mode: 'offset',
+ getData: async ({ offset, pageSize, sort, search, filter, signal }) => {
+ const response = await fetch(
+ \`/api/items?offset=\${offset}&limit=\${pageSize}&q=\${search}\`,
+ { signal }
+ );
+ const { items, totalCount } = await response.json();
+
+ return {
+ data: items,
+ totalCount,
+ };
+ },
+ paginationOptions: {
+ pageSize: 20,
+ pageSizeOptions: [20, 50, 100],
+ },
+});`;
+
+export const tableCursorPaginationSnippet = `const { tableProps } = useTable({
+ mode: 'cursor',
+ getData: async ({ cursor, pageSize, sort, search, signal }) => {
+ const response = await fetch(
+ \`/api/items?cursor=\${cursor ?? ''}&limit=\${pageSize}&q=\${search}\`,
+ { signal }
+ );
+ const { items, nextCursor, prevCursor, totalCount } = await response.json();
+
+ return {
+ data: items,
+ nextCursor,
+ prevCursor,
+ totalCount, // optional - enables "X of Y" display
+ };
+ },
+ paginationOptions: {
+ pageSize: 20,
+ pageSizeOptions: [20, 50, 100],
+ },
+});`;
+
+// =============================================================================
+// Combining Features
+// =============================================================================
+
+export const tableCombinedSnippet = `interface TypeFilter {
+ type: string | null;
+}
+
+const columns: ColumnConfig- [] = [
+ { id: 'name', label: 'Name', isRowHeader: true, isSortable: true,
+ cell: item =>
},
+ { id: 'owner', label: 'Owner', isSortable: true,
+ cell: item => },
+ { id: 'type', label: 'Type', isSortable: true,
+ cell: item => },
+];
+
+function ItemsTable() {
+ const [selected, setSelected] = useState | 'all'>(new Set());
+
+ const { tableProps, search, filter } = useTable- ({
+ mode: 'offset',
+ initialSort: { column: 'name', direction: 'ascending' },
+ getData: async ({ offset, pageSize, sort, search, filter, signal }) => {
+ const params = new URLSearchParams({
+ offset: String(offset),
+ limit: String(pageSize),
+ q: search,
+ ...(sort && { sortBy: sort.column, sortDir: sort.direction }),
+ ...(filter?.type && { type: filter.type }),
+ });
+
+ const response = await fetch(\`/api/items?\${params}\`, { signal });
+ const { items, totalCount } = await response.json();
+
+ return { data: items, totalCount };
+ },
+ });
+
+ return (
+
+
+
+ filter.onChange({ type: value || null })}
+ />
+
+ openDetailPanel(item),
+ }}
+ selection={{
+ mode: 'multiple',
+ behavior: 'toggle',
+ selected,
+ onSelectionChange: setSelected,
+ }}
+ emptyState={
+ search.value || filter.value?.type
+ ? No results match your filters
+ : No items available
+ }
+ {...tableProps}
+ />
+
+ );
+}`;
+
+// =============================================================================
+// Custom Tables
+// =============================================================================
+
+export const tableCustomRowSnippet = `import { Fragment } from 'react';
+
+const columns: ColumnConfig- [] = [
+ {
+ id: 'name',
+ label: 'Name',
+ isRowHeader: true,
+ header: () =>
Name (required) ,
+ cell: item => ,
+ },
+ { id: 'owner', label: 'Owner', cell: item => },
+ { id: 'lifecycle', label: 'Lifecycle', cell: item => },
+];
+
+const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => data,
+});
+
+ (
+
+ {column => (
+ {column.cell(item)}
+ )}
+
+ )}
+ {...tableProps}
+/>`;
+
+export const tablePrimitivesSnippet = `
+
+ Name
+ Owner
+ Type
+
+
+ {items.map(item => (
+
+
+
+
+
+ ))}
+
+ `;
diff --git a/docs-ui/src/app/components/tabs/components.tsx b/docs-ui/src/app/components/tabs/components.tsx
new file mode 100644
index 0000000000..8fbf3d4924
--- /dev/null
+++ b/docs-ui/src/app/components/tabs/components.tsx
@@ -0,0 +1,104 @@
+'use client';
+
+import {
+ Tabs,
+ TabList,
+ Tab,
+ TabPanel,
+} from '../../../../../packages/ui/src/components/Tabs';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { MemoryRouter } from 'react-router-dom';
+
+export const Default = () => {
+ return (
+
+
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+
+
+ );
+};
+
+export const DefaultSelectedKey = () => {
+ return (
+
+
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+
+
+ );
+};
+
+export const DisabledTabs = () => {
+ return (
+
+
+
+ Tab 1
+
+ Tab 2 (Disabled)
+
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+
+
+ );
+};
+
+export const Orientation = () => {
+ return (
+
+
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/tabs/page.mdx b/docs-ui/src/app/components/tabs/page.mdx
new file mode 100644
index 0000000000..6925127943
--- /dev/null
+++ b/docs-ui/src/app/components/tabs/page.mdx
@@ -0,0 +1,116 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import {
+ tabsPropDefs,
+ tabListPropDefs,
+ tabPropDefs,
+ tabPanelPropDefs,
+} from './props-definition';
+import {
+ tabsUsageSnippet,
+ defaultSnippet,
+ defaultSelectedKeySnippet,
+ disabledTabsSnippet,
+ orientationSnippet,
+ urlNavigationSnippet,
+} from './snippets';
+import {
+ Default,
+ DefaultSelectedKey,
+ DisabledTabs,
+ Orientation,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { TabsDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+export const reactAriaUrls = {
+ tabs: 'https://react-aria.adobe.com/Tabs',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+### Tabs
+
+Container that groups the tab list and panels.
+
+
+
+
+
+### TabList
+
+Container for the tab buttons.
+
+
+
+
+
+### Tab
+
+Individual tab button. Add `href` to enable URL-based tab selection.
+
+
+
+
+
+### TabPanel
+
+Content panel associated with a tab.
+
+
+
+
+
+## Examples
+
+### Default selected
+
+ }
+ code={defaultSelectedKeySnippet}
+/>
+
+### Disabled tabs
+
+ }
+ code={disabledTabsSnippet}
+/>
+
+### Vertical orientation
+
+ }
+ code={orientationSnippet}
+/>
+
+### URL-based navigation
+
+Add `href` to Tab components to enable URL-based tab selection. The active tab is determined by the current route.
+
+
+
+
+
+
diff --git a/docs-ui/src/app/components/tabs/props-definition.ts b/docs-ui/src/app/components/tabs/props-definition.ts
new file mode 100644
index 0000000000..e1c1f056cb
--- /dev/null
+++ b/docs-ui/src/app/components/tabs/props-definition.ts
@@ -0,0 +1,83 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const tabsPropDefs: Record = {
+ orientation: {
+ 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'],
+ description: 'Keys of tabs that should be disabled.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
+
+export const tabListPropDefs: Record = {
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
+
+export const tabPropDefs: Record = {
+ 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,
+};
+
+export const tabPanelPropDefs: Record = {
+ id: {
+ type: 'string',
+ required: true,
+ description: 'Unique identifier matching the corresponding Tab.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/tabs/snippets.ts b/docs-ui/src/app/components/tabs/snippets.ts
new file mode 100644
index 0000000000..651a0a89af
--- /dev/null
+++ b/docs-ui/src/app/components/tabs/snippets.ts
@@ -0,0 +1,71 @@
+export const tabsUsageSnippet = `import { Tabs, TabList, Tab, TabPanel } from '@backstage/ui';
+
+
+
+ Tab 1
+ Tab 2
+
+ Content 1
+ Content 2
+ `;
+
+export const defaultSnippet = `
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+
+ Content for Tab 1
+
+
+ Content for Tab 2
+
+
+ Content for Tab 3
+
+ `;
+
+export const defaultSelectedKeySnippet = `
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ Content 1
+ Content 2
+ Content 3
+ `;
+
+export const disabledTabsSnippet = `
+
+ Tab 1
+ Tab 2 (Disabled)
+ Tab 3
+
+ Content 1
+ Content 2
+ Content 3
+ `;
+
+export const orientationSnippet = `
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ Content 1
+ Content 2
+ Content 3
+ `;
+
+export const urlNavigationSnippet = `
+
+ Overview
+ Profile
+ Security
+
+ Overview content
+ Profile content
+ Security content
+ `;
diff --git a/docs-ui/src/app/components/tag-group/components.tsx b/docs-ui/src/app/components/tag-group/components.tsx
new file mode 100644
index 0000000000..a8455c1d1f
--- /dev/null
+++ b/docs-ui/src/app/components/tag-group/components.tsx
@@ -0,0 +1,127 @@
+'use client';
+
+import {
+ TagGroup,
+ Tag,
+} from '../../../../../packages/ui/src/components/TagGroup/TagGroup';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { MemoryRouter } from 'react-router-dom';
+import { useState } from 'react';
+import { useListData } from 'react-stately';
+import type { Selection } from 'react-aria-components';
+import {
+ RiAccountCircleLine,
+ RiBugLine,
+ RiEyeLine,
+ RiHeartLine,
+} from '@remixicon/react';
+
+interface ListItem {
+ id: string;
+ name: string;
+ icon: React.ReactNode;
+ isDisabled?: boolean;
+}
+
+const initialList: ListItem[] = [
+ { id: 'banana', name: 'Banana', icon: },
+ {
+ id: 'apple',
+ name: 'Apple',
+ icon: ,
+ isDisabled: true,
+ },
+ { id: 'orange', name: 'Orange', icon: , isDisabled: true },
+ { id: 'pear', name: 'Pear', icon: },
+ { id: 'grape', name: 'Grape', icon: },
+ { id: 'pineapple', name: 'Pineapple', icon: },
+ { id: 'strawberry', name: 'Strawberry', icon: },
+];
+
+export const Default = () => (
+
+
+ {initialList.map(item => (
+ {item.name}
+ ))}
+
+
+);
+
+export const WithLink = () => (
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+);
+
+export const WithIcon = () => (
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+);
+
+export const Sizes = () => (
+
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+
+);
+
+export const RemovingTags = () => {
+ const [selected, setSelected] = useState(new Set(['travel']));
+ const list = useListData({
+ initialItems: initialList,
+ });
+
+ return (
+
+
+ aria-label="Tag Group"
+ items={list.items}
+ onRemove={keys => list.remove(...keys)}
+ selectedKeys={selected}
+ onSelectionChange={setSelected}
+ >
+ {item => {item.name} }
+
+
+ );
+};
+
+export const Disabled = () => (
+
+
+ {initialList.map(item => (
+
+ {item.name}
+
+ ))}
+
+
+);
diff --git a/docs-ui/src/app/components/tag-group/page.mdx b/docs-ui/src/app/components/tag-group/page.mdx
new file mode 100644
index 0000000000..1b283a8170
--- /dev/null
+++ b/docs-ui/src/app/components/tag-group/page.mdx
@@ -0,0 +1,91 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import {
+ Default,
+ WithLink,
+ WithIcon,
+ Sizes,
+ RemovingTags,
+ Disabled,
+} from './components';
+import { tagGroupPropDefs, tagPropDefs } from './props-definition';
+import {
+ usage,
+ preview,
+ withLink,
+ disabled,
+ withIcons,
+ sizes,
+ removingTags,
+} from './snippets';
+import { PageTitle } from '@/components/PageTitle';
+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',
+};
+
+
+
+ } code={preview} />
+
+## Usage
+
+
+
+## API reference
+
+### TagGroup
+
+Container for a collection of tags.
+
+
+
+
+
+### Tag
+
+Individual tag item within a group.
+
+
+
+
+
+## Examples
+
+### With links
+
+ } code={withLink} />
+
+### With icons
+
+ } code={withIcons} />
+
+### Sizes
+
+ } code={sizes} />
+
+### Removable
+
+ }
+ code={removingTags}
+/>
+
+### Disabled
+
+ } code={disabled} />
+
+
+
+
diff --git a/docs-ui/src/app/components/tag-group/props-definition.tsx b/docs-ui/src/app/components/tag-group/props-definition.tsx
new file mode 100644
index 0000000000..3f64b03e8c
--- /dev/null
+++ b/docs-ui/src/app/components/tag-group/props-definition.tsx
@@ -0,0 +1,89 @@
+import {
+ classNamePropDefs,
+ childrenPropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const tagGroupPropDefs: Record = {
+ items: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Item objects in the collection.',
+ },
+ renderEmptyState: {
+ type: 'enum',
+ values: ['() => ReactNode'],
+ description: 'Content to display when the collection is empty.',
+ },
+ selectionMode: {
+ type: 'enum',
+ values: ['none', 'single', 'multiple'],
+ description: 'The type of selection allowed.',
+ },
+ selectedKeys: {
+ type: 'enum',
+ values: ['all', 'Iterable'],
+ description: 'The currently selected keys (controlled).',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['all', 'Iterable'],
+ description: 'The initial selected keys (uncontrolled).',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Keys of tags that should be disabled.',
+ },
+ onRemove: {
+ type: 'enum',
+ values: ['(keys: Set) => void'],
+ description: 'Handler called when a tag is removed.',
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: ['(keys: Selection) => void'],
+ description: 'Handler called when the selection changes.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
+
+export const tagPropDefs: Record = {
+ id: {
+ type: 'string',
+ description: 'Unique identifier for the tag.',
+ },
+ textValue: {
+ type: 'string',
+ description:
+ 'Text value for accessibility. Derived from children if string.',
+ },
+ href: {
+ type: 'string',
+ description: 'URL to navigate to when the tag is clicked.',
+ },
+ icon: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon displayed before the tag text.',
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ description: (
+ <>
+ Visual size of the tag. Use small for inline or dense
+ layouts, medium for standalone tags.
+ >
+ ),
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Whether the tag is disabled.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/tag-group/snippets.ts b/docs-ui/src/app/components/tag-group/snippets.ts
new file mode 100644
index 0000000000..c939c69d14
--- /dev/null
+++ b/docs-ui/src/app/components/tag-group/snippets.ts
@@ -0,0 +1,62 @@
+export const usage = `import { TagGroup, Tag } from '@backstage/ui';
+
+
+ Tag 1
+ Tag 2
+ `;
+
+export const preview = `
+ Banana
+ Apple
+ Orange
+ Pear
+ Grape
+ Pineapple
+ Strawberry
+ `;
+
+export const withLink = `
+ Banana
+ Apple
+ Orange
+ `;
+
+export const disabled = `
+ Banana
+ Apple
+ Orange
+ Pear
+ `;
+
+export const withIcons = `
+ }>Banana
+ }>Apple
+ }>Orange
+ }>Pear
+ `;
+
+export const sizes = `
+
+ }>Banana
+ }>Apple
+
+
+ }>Banana
+ }>Apple
+
+ `;
+
+export const removingTags = `const list = useListData({
+ initialItems: [
+ { id: 'banana', name: 'Banana' },
+ { id: 'apple', name: 'Apple' },
+ { id: 'orange', name: 'Orange' },
+ ],
+});
+
+ list.remove(...keys)}
+>
+ {item => {item.name} }
+ `;
diff --git a/docs-ui/src/app/components/text-field/components.tsx b/docs-ui/src/app/components/text-field/components.tsx
new file mode 100644
index 0000000000..6f6c8019ea
--- /dev/null
+++ b/docs-ui/src/app/components/text-field/components.tsx
@@ -0,0 +1,51 @@
+'use client';
+
+import { TextField } from '../../../../../packages/ui/src/components/TextField/TextField';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiSparklingLine } from '@remixicon/react';
+
+export const WithLabel = () => {
+ return (
+
+ );
+};
+
+export const Sizes = () => {
+ return (
+
+ }
+ />
+ }
+ />
+
+ );
+};
+
+export const WithDescription = () => {
+ return (
+
+ );
+};
diff --git a/docs-ui/src/app/components/text-field/page.mdx b/docs-ui/src/app/components/text-field/page.mdx
new file mode 100644
index 0000000000..87d1087814
--- /dev/null
+++ b/docs-ui/src/app/components/text-field/page.mdx
@@ -0,0 +1,70 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { textFieldPropDefs } from './props-definition';
+import {
+ textFieldUsageSnippet,
+ withLabelSnippet,
+ sizesSnippet,
+ withDescriptionSnippet,
+} from './snippets';
+import { WithLabel, Sizes, WithDescription } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { TextFieldDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+
+export const reactAriaUrls = {
+ textField: 'https://react-aria.adobe.com/TextField',
+};
+
+
+
+ }
+ code={withLabelSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Sizes
+
+ }
+ code={sizesSnippet}
+ layout="side-by-side"
+/>
+
+### With description
+
+ }
+ code={withDescriptionSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/text-field/props-definition.tsx b/docs-ui/src/app/components/text-field/props-definition.tsx
new file mode 100644
index 0000000000..54589e1dab
--- /dev/null
+++ b/docs-ui/src/app/components/text-field/props-definition.tsx
@@ -0,0 +1,85 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const textFieldPropDefs: Record = {
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description: (
+ <>
+ Visual size of the input. Use small for dense layouts,{' '}
+ medium 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 Required .
+ >
+ ),
+ },
+ description: {
+ type: 'string',
+ description: 'Help text displayed below the label.',
+ },
+ icon: {
+ type: 'enum',
+ values: ['ReactNode'],
+ description: 'Icon rendered before the input.',
+ },
+ type: {
+ type: 'enum',
+ values: ['text', 'email', 'tel', 'url'],
+ default: 'text',
+ description:
+ 'HTML input type. Use SearchField for search inputs and PasswordField for passwords.',
+ },
+ 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.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/text-field/snippets.ts b/docs-ui/src/app/components/text-field/snippets.ts
new file mode 100644
index 0000000000..617884dcfe
--- /dev/null
+++ b/docs-ui/src/app/components/text-field/snippets.ts
@@ -0,0 +1,31 @@
+export const textFieldUsageSnippet = `import { TextField } from '@backstage/ui';
+
+ `;
+
+export const withLabelSnippet = ` `;
+
+export const sizesSnippet = `
+ }
+ />
+ }
+ />
+ `;
+
+export const withDescriptionSnippet = ` `;
diff --git a/docs-ui/src/app/components/text/components.tsx b/docs-ui/src/app/components/text/components.tsx
new file mode 100644
index 0000000000..8021a1dde6
--- /dev/null
+++ b/docs-ui/src/app/components/text/components.tsx
@@ -0,0 +1,66 @@
+'use client';
+
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return This is a text component ;
+};
+
+export const Variants = () => {
+ return (
+
+ Title Large
+ Title Medium
+ Title Small
+ Title X-Small
+ Body Large
+ Body Medium
+ Body Small
+ Body X-Small
+
+ );
+};
+
+export const Colors = () => {
+ return (
+
+ Primary
+ Secondary
+ Danger
+ Warning
+ Success
+ Info
+
+ );
+};
+
+export const Weights = () => {
+ return (
+
+ Regular weight
+ Bold weight
+
+ );
+};
+
+export const AsElement = () => {
+ return (
+
+ Paragraph element
+ Span element
+ Div element
+
+ );
+};
+
+export const Truncate = () => {
+ return (
+
+
+ This is a very long text that will be truncated when it exceeds the
+ container width
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/text/page.mdx b/docs-ui/src/app/components/text/page.mdx
new file mode 100644
index 0000000000..9a93f38160
--- /dev/null
+++ b/docs-ui/src/app/components/text/page.mdx
@@ -0,0 +1,56 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { textPropDefs } from './props-definition';
+import {
+ textUsageSnippet,
+ defaultSnippet,
+ variantsSnippet,
+ weightsSnippet,
+ colorsSnippet,
+ truncateSnippet,
+} from './snippets';
+import { Default, Variants, Weights, Colors, Truncate } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { TextDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+## Examples
+
+### Variants
+
+ } code={variantsSnippet} />
+
+### Weights
+
+ } code={weightsSnippet} />
+
+### Colors
+
+Status colors for contextual messaging.
+
+ } code={colorsSnippet} />
+
+### Truncate
+
+ } code={truncateSnippet} />
+
+
+
+
diff --git a/docs-ui/src/app/components/text/props-definition.tsx b/docs-ui/src/app/components/text/props-definition.tsx
new file mode 100644
index 0000000000..8a25af2de7
--- /dev/null
+++ b/docs-ui/src/app/components/text/props-definition.tsx
@@ -0,0 +1,81 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const textPropDefs: Record = {
+ 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,
+ 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 bold 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 messaging.',
+ },
+ as: {
+ type: 'enum',
+ values: [
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'p',
+ 'span',
+ 'label',
+ 'div',
+ 'strong',
+ 'em',
+ 'small',
+ 'legend',
+ ],
+ default: 'span',
+ description:
+ 'HTML element to render. Use heading tags for semantic structure.',
+ },
+ truncate: {
+ type: 'boolean',
+ default: 'false',
+ description: (
+ <>
+ Truncates text with ellipsis when it overflows its container. Requires{' '}
+ display: block to work.
+ >
+ ),
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/text/snippets.ts b/docs-ui/src/app/components/text/snippets.ts
new file mode 100644
index 0000000000..b8cc26bd6b
--- /dev/null
+++ b/docs-ui/src/app/components/text/snippets.ts
@@ -0,0 +1,43 @@
+export const textUsageSnippet = `import { Text } from '@backstage/ui';
+
+This is a text component `;
+
+export const defaultSnippet = `This is a text component `;
+
+export const variantsSnippet = `
+ Title Large
+ Title Medium
+ Title Small
+ Title X-Small
+ Body Large
+ Body Medium
+ Body Small
+ Body X-Small
+ `;
+
+export const colorsSnippet = `
+ Primary
+ Secondary
+ Danger
+ Warning
+ Success
+ Info
+ `;
+
+export const weightsSnippet = `
+ Regular weight
+ Bold weight
+ `;
+
+export const asElementSnippet = `
+ Paragraph element
+ Span element
+ Div element
+ `;
+
+export const truncateSnippet = `
+
+ This is a very long text that will be truncated when it exceeds the
+ container width
+
+
`;
diff --git a/docs-ui/src/app/components/toggle-button-group/components.tsx b/docs-ui/src/app/components/toggle-button-group/components.tsx
new file mode 100644
index 0000000000..a6da51f862
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button-group/components.tsx
@@ -0,0 +1,154 @@
+'use client';
+
+import { ToggleButtonGroup } from '../../../../../packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup';
+import { ToggleButton } from '../../../../../packages/ui/src/components/ToggleButton/ToggleButton';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import {
+ RiCloudLine,
+ RiStarFill,
+ RiStarLine,
+ RiArrowRightSLine,
+} from '@remixicon/react';
+
+export const ToggleButtonGroupSurfaces = () => (
+
+
+ Default
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 0
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 1
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 2
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 3
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+);
+
+export const ToggleButtonGroupSingle = () => (
+
+ Dogs
+ Cats
+ Birds
+
+);
+
+export const ToggleButtonGroupMultiple = () => (
+
+ Frontend
+ Backend
+ Platform
+
+);
+
+export const ToggleButtonGroupVertical = () => (
+
+ Morning
+ Afternoon
+ Evening
+
+);
+
+export const ToggleButtonGroupDisabled = () => (
+
+ Cat
+ Dog
+ Bird
+
+);
+
+export const ToggleButtonGroupDisallowEmpty = () => (
+
+ One
+ Two
+ Three
+
+);
+
+export const ToggleButtonGroupIcons = () => (
+
+ } />
+ }
+ />
+ }>
+ Star
+
+ }>
+ Next
+
+
+);
+
+export const ToggleButtonGroupIconsOnly = () => (
+
+ } />
+ } />
+ } />
+
+);
diff --git a/docs-ui/src/app/components/toggle-button-group/page.mdx b/docs-ui/src/app/components/toggle-button-group/page.mdx
new file mode 100644
index 0000000000..462ee8680b
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button-group/page.mdx
@@ -0,0 +1,152 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { toggleButtonGroupPropDefs } from './props-definition';
+import {
+ toggleButtonGroupHeroSnippet,
+ toggleButtonGroupUsageSnippet,
+ toggleButtonGroupSurfacesSnippet,
+ toggleButtonGroupSingleSnippet,
+ toggleButtonGroupMultipleSnippet,
+ toggleButtonGroupIconsSnippet,
+ toggleButtonGroupIconsOnlySnippet,
+ toggleButtonGroupDisallowEmptySnippet,
+ toggleButtonGroupVerticalSnippet,
+ toggleButtonGroupDisabledSnippet,
+} from './snippets';
+import {
+ ToggleButtonGroupSingle,
+ ToggleButtonGroupMultiple,
+ ToggleButtonGroupVertical,
+ ToggleButtonGroupDisabled,
+ ToggleButtonGroupDisallowEmpty,
+ ToggleButtonGroupIcons,
+ ToggleButtonGroupIconsOnly,
+ ToggleButtonGroupSurfaces,
+} from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ToggleButtonGroupDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ toggleButtonGroup: 'https://react-aria.adobe.com/ToggleButtonGroup',
+};
+
+
+
+ }
+ code={toggleButtonGroupHeroSnippet}
+/>
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### Surfaces
+
+ }
+ code={toggleButtonGroupSurfacesSnippet}
+/>
+
+### Single selection
+
+ }
+ code={toggleButtonGroupSingleSnippet}
+ layout="side-by-side"
+/>
+
+### Multiple selection
+
+ }
+ code={toggleButtonGroupMultipleSnippet}
+ layout="side-by-side"
+/>
+
+### Icons and text
+
+ }
+ code={toggleButtonGroupIconsSnippet}
+ layout="side-by-side"
+/>
+
+### Icons only
+
+ }
+ code={toggleButtonGroupIconsOnlySnippet}
+ layout="side-by-side"
+/>
+
+### Disallow empty selection
+
+ }
+ code={toggleButtonGroupDisallowEmptySnippet}
+ layout="side-by-side"
+/>
+
+### Vertical orientation
+
+ }
+ code={toggleButtonGroupVerticalSnippet}
+ layout="side-by-side"
+/>
+
+### Disabled
+
+ }
+ code={toggleButtonGroupDisabledSnippet}
+ layout="side-by-side"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/toggle-button-group/props-definition.ts b/docs-ui/src/app/components/toggle-button-group/props-definition.ts
new file mode 100644
index 0000000000..8c8632709e
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button-group/props-definition.ts
@@ -0,0 +1,51 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const toggleButtonGroupPropDefs: Record = {
+ selectionMode: {
+ type: 'enum',
+ values: ['single', 'multiple'],
+ description: 'Whether to allow single or multiple selection.',
+ },
+ selectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'The currently selected keys (controlled).',
+ },
+ defaultSelectedKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'The initial selected keys (uncontrolled).',
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: ['(keys: Selection) => void'],
+ description: 'Handler called when the selection changes.',
+ },
+ orientation: {
+ type: 'enum',
+ values: ['horizontal', 'vertical'],
+ default: 'horizontal',
+ description: 'The layout orientation of the button group.',
+ },
+ disallowEmptySelection: {
+ type: 'boolean',
+ description:
+ 'Whether to prevent deselecting when at least one button is selected.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Disables all buttons in the group.',
+ },
+ disabledKeys: {
+ type: 'enum',
+ values: ['Iterable'],
+ description: 'Keys of buttons that should be disabled.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/toggle-button-group/snippets.ts b/docs-ui/src/app/components/toggle-button-group/snippets.ts
new file mode 100644
index 0000000000..ce5d70784a
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button-group/snippets.ts
@@ -0,0 +1,121 @@
+export const toggleButtonGroupHeroSnippet = `
+ Dogs
+ Cats
+ Birds
+ `;
+
+export const toggleButtonGroupUsageSnippet = `import { ToggleButtonGroup, ToggleButton } from '@backstage/ui';
+
+
+ Dogs
+ Cats
+ Birds
+ `;
+
+export const toggleButtonGroupSingleSnippet = `
+ Dogs
+ Cats
+ Birds
+ `;
+
+export const toggleButtonGroupMultipleSnippet = `
+ Frontend
+ Backend
+ Platform
+ `;
+
+export const toggleButtonGroupVerticalSnippet = `
+ Morning
+ Afternoon
+ Evening
+ `;
+
+export const toggleButtonGroupDisabledSnippet = `
+ Cat
+ Dog
+ Bird
+ `;
+
+export const toggleButtonGroupDisallowEmptySnippet = `
+ One
+ Two
+ Three
+ `;
+
+export const toggleButtonGroupIconsSnippet = `import { RiCloudLine, RiStarFill, RiStarLine, RiArrowRightSLine } from '@remixicon/react';
+
+
+ } />
+ }
+ />
+ }>
+ Star
+
+ }>
+ Next
+
+ `;
+
+export const toggleButtonGroupIconsOnlySnippet = `import { RiCloudLine, RiStarLine, RiArrowRightSLine } from '@remixicon/react';
+
+
+ } />
+ } />
+ } />
+ `;
+
+export const toggleButtonGroupSurfacesSnippet = `
+
+ Default
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 0
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 1
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 2
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+
+ On Surface 3
+
+
+ Option 1
+ Option 2
+ Option 3
+
+
+
+ `;
diff --git a/docs-ui/src/app/components/toggle-button/components.tsx b/docs-ui/src/app/components/toggle-button/components.tsx
new file mode 100644
index 0000000000..6fa5aada98
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button/components.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import { ToggleButton } from '../../../../../packages/ui/src/components/ToggleButton/ToggleButton';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+import { RiCheckLine } from '@remixicon/react';
+import { useState } from 'react';
+
+export const Default = () => {
+ const [isSelected, setIsSelected] = useState(false);
+ return (
+
+ Toggle
+
+ );
+};
+
+export const WithIcon = () => {
+ const [isSelected, setIsSelected] = useState(false);
+ return (
+ }
+ >
+ With Icon
+
+ );
+};
+
+export const Sizes = () => {
+ const [small, setSmall] = useState(false);
+ const [medium, setMedium] = useState(false);
+
+ return (
+
+
+ Small
+
+
+ Medium
+
+
+ );
+};
+
+export const Disabled = () => {
+ return (
+
+ {}}>
+ Disabled Off
+
+ {}}>
+ Disabled On
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/toggle-button/page.mdx b/docs-ui/src/app/components/toggle-button/page.mdx
new file mode 100644
index 0000000000..4b6aad90d1
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button/page.mdx
@@ -0,0 +1,68 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { toggleButtonPropDefs } from './props-definition';
+import {
+ toggleButtonUsageSnippet,
+ defaultSnippet,
+ withIconSnippet,
+ sizesSnippet,
+ disabledSnippet,
+} from './snippets';
+import { Default, WithIcon, Sizes, Disabled } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { ToggleButtonDefinition } from '../../../utils/definitions';
+
+export const reactAriaUrls = {
+ toggleButton: 'https://react-aria.adobe.com/ToggleButton',
+};
+
+
+
+ } code={defaultSnippet} />
+
+## Usage
+
+
+
+## API reference
+
+
+
+
+
+## Examples
+
+### With icon
+
+ }
+ code={withIconSnippet}
+/>
+
+### Sizes
+
+ } code={sizesSnippet} />
+
+### Disabled
+
+ }
+ code={disabledSnippet}
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/toggle-button/props-definition.ts b/docs-ui/src/app/components/toggle-button/props-definition.ts
new file mode 100644
index 0000000000..7af4521bcb
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button/props-definition.ts
@@ -0,0 +1,55 @@
+import { classNamePropDefs, type PropDef } from '@/utils/propDefs';
+
+export const toggleButtonPropDefs: Record = {
+ isSelected: {
+ type: 'boolean',
+ description:
+ 'Whether the button is selected. Controls the visual pressed state.',
+ },
+ defaultSelected: {
+ type: 'boolean',
+ description: 'The default selected state (uncontrolled).',
+ },
+ onChange: {
+ type: 'enum',
+ values: ['(isSelected: boolean) => void'],
+ description: 'Handler called when the button is pressed.',
+ },
+ size: {
+ type: 'enum',
+ values: ['small', 'medium'],
+ default: 'small',
+ responsive: true,
+ description:
+ 'Visual weight. Use small for compact layouts, medium for emphasis.',
+ },
+ iconStart: {
+ type: 'enum',
+ values: ['ReactElement'],
+ description:
+ 'Icon displayed before the button text. Sized to match button.',
+ },
+ iconEnd: {
+ type: 'enum',
+ values: ['ReactElement'],
+ description: 'Icon displayed after the button text. Sized to match button.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ default: 'false',
+ description: 'Prevents interaction and dims the button.',
+ },
+ onSurface: {
+ type: 'enum',
+ values: ['Surface'],
+ responsive: true,
+ description:
+ 'Surface the button is placed on. Defaults to context surface.',
+ },
+ children: {
+ type: 'enum',
+ values: ['ReactNode', '(renderProps) => ReactNode'],
+ description: 'Button label. Can be a function for dynamic content.',
+ },
+ ...classNamePropDefs,
+};
diff --git a/docs-ui/src/app/components/toggle-button/snippets.ts b/docs-ui/src/app/components/toggle-button/snippets.ts
new file mode 100644
index 0000000000..d913e0702e
--- /dev/null
+++ b/docs-ui/src/app/components/toggle-button/snippets.ts
@@ -0,0 +1,42 @@
+export const toggleButtonUsageSnippet = `import { ToggleButton } from '@backstage/ui';
+import { useState } from 'react';
+
+const [isSelected, setIsSelected] = useState(false);
+
+
+ Toggle
+ `;
+
+export const defaultSnippet = `const [isSelected, setIsSelected] = useState(false);
+
+
+ Toggle
+ `;
+
+export const withIconSnippet = `const [isSelected, setIsSelected] = useState(false);
+
+ }
+>
+ With Icon
+`;
+
+export const sizesSnippet = `
+
+ Small
+
+
+ Medium
+
+ `;
+
+export const disabledSnippet = `
+ {}}>
+ Disabled Off
+
+ {}}>
+ Disabled On
+
+ `;
diff --git a/docs-ui/src/app/components/tooltip/components.tsx b/docs-ui/src/app/components/tooltip/components.tsx
new file mode 100644
index 0000000000..67fc8c8d30
--- /dev/null
+++ b/docs-ui/src/app/components/tooltip/components.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import {
+ TooltipTrigger,
+ Tooltip,
+} from '../../../../../packages/ui/src/components/Tooltip/Tooltip';
+import { Button } from '../../../../../packages/ui/src/components/Button/Button';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return (
+
+ Hover me
+ Helpful information
+
+ );
+};
+
+export const Placement = () => {
+ return (
+
+
+ Top
+ Top tooltip
+
+
+ Right
+ Right tooltip
+
+
+ Bottom
+ Bottom tooltip
+
+
+ Left
+ Left tooltip
+
+
+ );
+};
diff --git a/docs-ui/src/app/components/tooltip/page.mdx b/docs-ui/src/app/components/tooltip/page.mdx
new file mode 100644
index 0000000000..8a9711d56c
--- /dev/null
+++ b/docs-ui/src/app/components/tooltip/page.mdx
@@ -0,0 +1,65 @@
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { CodeBlock } from '@/components/CodeBlock';
+import { ReactAriaLink } from '@/components/ReactAriaLink';
+import { tooltipTriggerPropDefs, tooltipPropDefs } from './props-definition';
+import {
+ tooltipUsageSnippet,
+ defaultSnippet,
+ placementSnippet,
+} from './snippets';
+import { Default, Placement } from './components';
+import { PageTitle } from '@/components/PageTitle';
+import { Theming } from '@/components/Theming';
+import { TooltipDefinition } from '../../../utils/definitions';
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+
+export const reactAriaUrls = {
+ tooltip: 'https://react-aria.adobe.com/Tooltip',
+};
+
+
+
+ } code={defaultSnippet} align="center" />
+
+## Usage
+
+
+
+## API reference
+
+### TooltipTrigger
+
+Wraps the trigger element and tooltip content.
+
+
+
+
+
+### Tooltip
+
+The tooltip content that appears on hover or focus.
+
+
+
+
+
+## Examples
+
+### Placement
+
+The tooltip appears in the specified direction relative to the trigger.
+
+ }
+ code={placementSnippet}
+ align="center"
+/>
+
+
+
+
diff --git a/docs-ui/src/app/components/tooltip/props-definition.tsx b/docs-ui/src/app/components/tooltip/props-definition.tsx
new file mode 100644
index 0000000000..16dc9133dd
--- /dev/null
+++ b/docs-ui/src/app/components/tooltip/props-definition.tsx
@@ -0,0 +1,79 @@
+import {
+ childrenPropDefs,
+ classNamePropDefs,
+ stylePropDefs,
+} from '@/utils/propDefs';
+import type { PropDef } from '@/utils/propDefs';
+import { Chip } from '@/components/Chip';
+
+export const tooltipTriggerPropDefs: Record = {
+ delay: {
+ type: 'number',
+ default: 600,
+ description: 'Milliseconds before tooltip appears on hover.',
+ },
+ closeDelay: {
+ type: 'number',
+ default: 0,
+ description: 'Milliseconds before tooltip hides after leaving trigger.',
+ },
+ trigger: {
+ type: 'enum',
+ values: ['focus'],
+ description: (
+ <>
+ Set to focus for focus-only tooltips that do not appear on
+ hover.
+ >
+ ),
+ },
+ isOpen: {
+ type: 'boolean',
+ description: 'Controlled open state. Use with onOpenChange.',
+ },
+ defaultOpen: {
+ type: 'boolean',
+ description: 'Initial open state for uncontrolled usage.',
+ },
+ isDisabled: {
+ type: 'boolean',
+ description: 'Prevents the tooltip from appearing.',
+ },
+ ...childrenPropDefs,
+};
+
+export const tooltipPropDefs: Record = {
+ placement: {
+ type: 'enum',
+ values: [
+ 'top',
+ 'top start',
+ 'top end',
+ 'bottom',
+ 'bottom start',
+ 'bottom end',
+ 'left',
+ 'left top',
+ 'left bottom',
+ 'right',
+ 'right top',
+ 'right bottom',
+ ],
+ default: 'top',
+ description:
+ 'Position relative to the trigger element. Compound placements include alignment.',
+ },
+ offset: {
+ type: 'number',
+ default: 4,
+ description: 'Distance in pixels from the trigger element.',
+ },
+ containerPadding: {
+ type: 'number',
+ default: 12,
+ description: 'Padding from viewport edge when repositioning.',
+ },
+ ...childrenPropDefs,
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/app/components/tooltip/snippets.ts b/docs-ui/src/app/components/tooltip/snippets.ts
new file mode 100644
index 0000000000..68b7d1b0c7
--- /dev/null
+++ b/docs-ui/src/app/components/tooltip/snippets.ts
@@ -0,0 +1,30 @@
+export const tooltipUsageSnippet = `import { TooltipTrigger, Tooltip, Button } from '@backstage/ui';
+
+
+ Hover me
+ Helpful information
+ `;
+
+export const defaultSnippet = `
+ Hover me
+ Helpful information
+ `;
+
+export const placementSnippet = `
+
+ Top
+ Top tooltip
+
+
+ Right
+ Right tooltip
+
+
+ Bottom
+ Bottom tooltip
+
+
+ Left
+ Left tooltip
+
+ `;
diff --git a/docs-ui/src/app/components/visually-hidden/components.tsx b/docs-ui/src/app/components/visually-hidden/components.tsx
new file mode 100644
index 0000000000..8298465240
--- /dev/null
+++ b/docs-ui/src/app/components/visually-hidden/components.tsx
@@ -0,0 +1,39 @@
+'use client';
+
+import { VisuallyHidden } from '../../../../../packages/ui/src/components/VisuallyHidden/VisuallyHidden';
+import { Text } from '../../../../../packages/ui/src/components/Text/Text';
+import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
+
+export const Default = () => {
+ return (
+
+
+ This text is followed by a paragraph that is visually hidden but
+ accessible to screen readers. Try using a screen reader to hear it, or
+ inspect the DOM to see it's there.
+
+
+ This content is visually hidden but accessible to screen readers
+
+
+ );
+};
+
+export const ExampleUsage = () => {
+ return (
+
+
+ Footer links
+
+
+ About us
+
+
+ Jobs
+
+
+ Terms and Conditions
+
+
+ );
+};
diff --git a/docs-ui/src/content/visually-hidden.mdx b/docs-ui/src/app/components/visually-hidden/page.mdx
similarity index 56%
rename from docs-ui/src/content/visually-hidden.mdx
rename to docs-ui/src/app/components/visually-hidden/page.mdx
index 72ffc7a1d4..c72250231f 100644
--- a/docs-ui/src/content/visually-hidden.mdx
+++ b/docs-ui/src/app/components/visually-hidden/page.mdx
@@ -1,16 +1,16 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
-import { VisuallyHiddenSnippet } from '@/snippets/stories-snippets';
+import { visuallyHiddenPropDefs } from './props-definition';
import {
- visuallyHiddenPropDefs,
visuallyHiddenUsageSnippet,
- visuallyHiddenDefaultSnippet,
- visuallyHiddenExampleUsageSnippet,
-} from './visually-hidden.props';
+ defaultSnippet,
+ exampleUsageSnippet,
+} from './snippets';
+import { Default, ExampleUsage } from './components';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
-import { VisuallyHiddenDefinition } from '../utils/definitions';
+import { VisuallyHiddenDefinition } from '../../../utils/definitions';
import { ChangelogComponent } from '@/components/ChangelogComponent';
- }
- code={visuallyHiddenDefaultSnippet}
-/>
+ } code={defaultSnippet} />
## Usage
@@ -33,17 +28,17 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
+This component also accepts all standard HTML `div` attributes.
+
## Examples
-### Example Usage
-
-Here's an example of providing screen reader context for a list of links in a footer.
+### Hidden headings
}
- code={visuallyHiddenExampleUsageSnippet}
+ preview={ }
+ code={exampleUsageSnippet}
open
/>
diff --git a/docs-ui/src/app/components/visually-hidden/props-definition.ts b/docs-ui/src/app/components/visually-hidden/props-definition.ts
new file mode 100644
index 0000000000..140ed45fa4
--- /dev/null
+++ b/docs-ui/src/app/components/visually-hidden/props-definition.ts
@@ -0,0 +1,17 @@
+import {
+ classNamePropDefs,
+ stylePropDefs,
+ type PropDef,
+} from '@/utils/propDefs';
+
+export const visuallyHiddenPropDefs: Record = {
+ children: {
+ type: 'enum',
+ values: ['ReactNode'],
+ responsive: false,
+ description:
+ 'Content to hide visually while remaining accessible to screen readers.',
+ },
+ ...classNamePropDefs,
+ ...stylePropDefs,
+};
diff --git a/docs-ui/src/content/visually-hidden.props.ts b/docs-ui/src/app/components/visually-hidden/snippets.ts
similarity index 63%
rename from docs-ui/src/content/visually-hidden.props.ts
rename to docs-ui/src/app/components/visually-hidden/snippets.ts
index 18a1c503d1..21ce6a04de 100644
--- a/docs-ui/src/content/visually-hidden.props.ts
+++ b/docs-ui/src/app/components/visually-hidden/snippets.ts
@@ -1,26 +1,10 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const visuallyHiddenPropDefs: Record = {
- children: {
- type: 'enum',
- values: ['ReactNode'],
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
export const visuallyHiddenUsageSnippet = `import { VisuallyHidden } from '@backstage/ui';
This content is visually hidden but accessible to screen readers
`;
-export const visuallyHiddenDefaultSnippet = `
+export const defaultSnippet = `
This text is followed by a paragraph that is visually hidden but
accessible to screen readers. Try using a screen reader to hear it, or
@@ -31,7 +15,7 @@ export const visuallyHiddenDefaultSnippet = `
`;
-export const visuallyHiddenExampleUsageSnippet = `
+export const exampleUsageSnippet = `
Footer links
diff --git a/docs-ui/src/app/get-started/installation/page.mdx b/docs-ui/src/app/get-started/installation/page.mdx
new file mode 100644
index 0000000000..cdb14d5924
--- /dev/null
+++ b/docs-ui/src/app/get-started/installation/page.mdx
@@ -0,0 +1,46 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { Banner } from '@/components/Banner';
+import { snippet } from './snippets';
+
+# Installation
+
+## Import BUI's global styles
+
+Backstage UI works by importing a global CSS file at the root of your application. This file includes all the default styles for the components.
+First, you'll need to install the package using a package manager. For example, if you're using Yarn:
+
+
+
+ );`}
+/>
+
+
+
+## Use BUI components
+
+As a plugin maintainer, you can use BUI components in your plugin. As mentioned above, you should not import the styles
+again in your plugin as this will be handled at the root of your application. To get started, just add the library to
+your plugin and import the components you need.
+
+
+
+
diff --git a/docs-ui/src/app/get-started/installation/snippets.ts b/docs-ui/src/app/get-started/installation/snippets.ts
new file mode 100644
index 0000000000..7e63005172
--- /dev/null
+++ b/docs-ui/src/app/get-started/installation/snippets.ts
@@ -0,0 +1,6 @@
+export const snippet = `import { Flex, Button, Text } from '@backstage/ui';
+
+
+ Hello World
+ Click me
+ ;`;
diff --git a/docs-ui/src/app/icon.svg b/docs-ui/src/app/icon.svg
index b50d29e902..18824daaa7 100644
--- a/docs-ui/src/app/icon.svg
+++ b/docs-ui/src/app/icon.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/docs-ui/src/app/layout.tsx b/docs-ui/src/app/layout.tsx
index ad056626d8..a0f8ae5816 100644
--- a/docs-ui/src/app/layout.tsx
+++ b/docs-ui/src/app/layout.tsx
@@ -8,8 +8,8 @@ import { MobileBottomNav } from '@/components/MobileBottomNav';
import styles from './layout.module.css';
import '../css/globals.css';
-import '/public/theme-backstage.css';
-import '/public/theme-spotify.css';
+import '../css/theme-backstage.css';
+import '../css/theme-spotify.css';
export const metadata: Metadata = {
title: 'Backstage UI',
@@ -49,6 +49,10 @@ export default async function RootLayout({
data-theme-name="backstage"
suppressHydrationWarning
>
+
+
+
+
diff --git a/docs-ui/src/app/page.mdx b/docs-ui/src/app/page.mdx
index 9a796b401b..712a5ac5c6 100644
--- a/docs-ui/src/app/page.mdx
+++ b/docs-ui/src/app/page.mdx
@@ -1,59 +1,78 @@
+import { ComponentGrid } from '@/components/ComponentGrid';
import { CodeBlock } from '@/components/CodeBlock';
-import { Banner } from '@/components/Banner';
-import { snippet } from './snippets';
+import { ColorFamily } from '@/components/ColorFamily';
+import {
+ surfacesSnippet,
+ adaptiveSnippet,
+ customCardSnippet,
+ customTokensSnippet,
+ colorPickerSnippet,
+} from './snippets';
-# Welcome to Backstage UI
+# Get Started with BUI
Backstage UI is a design system created specifically for Backstage, built with React, TypeScript, and vanilla CSS.
This open-source library is hosted in the Backstage monorepo. While it can be used in other projects, Backstage UI
is designed to deliver a consistent, accessible, and extensible experience tailored to Backstage users.
-## Import BUI's global styles
+Backstage UI is installed by default on every instance of Backstage, so you can start using it right away.
+If your setup doesn't include it yet, follow the [installation guide](/get-started/installation) to get started.
-Backstage UI works by importing a global CSS file at the root of your application. This file includes all the default styles for the components.
-First, you'll need to install the package using a package manager. For example, if you're using Yarn:
+## Layout containers
-
+[`Box`](/components/box), [`Flex`](/components/flex), [`Grid`](/components/grid), and [`Card`](/components/card) are the foundation of every layout in Backstage UI.
+Each one offers a set of utility props that map directly to our design tokens, so you can build consistent
+layouts without writing any CSS. When nested, they also act as surfaces and automatically increment the
+background depth so visual hierarchy is handled for you.
);`}
+ title="Nested surfaces with automatic styling"
+ code={surfacesSnippet}
/>
-
+## Adaptive components
-## Use BUI components
-
-As a plugin maintainer, you can use BUI components in your plugin. As mentioned above, you should not import the styles
-again in your plugin as this will be handled at the root of your application. To get started, just add the library to
-your plugin and import the components you need.
+Components like [`Card`](/components/card), [`Button`](/components/button), [`Text`](/components/text), and others are **adaptive components**. They
+automatically adjust their colors, borders, and backgrounds to match the surface they live on. Drop a
+[`Button`](/components/button) inside a [`Card`](/components/card) inside a [`Box`](/components/box) and each component styles itself appropriately
+without any extra configuration.
-
+## The neutral scale background colors
-## Support
+
-Now that you have the basics down, you can start building your plugin using the new design system.
-Please familiarise yourself first with our theming principles. This will help you understand the core concepts of the design system.
-If you have any questions, please reach out to us on [Discord](https://discord.gg/MUpMjP2).
+## Creating custom components
+
+As much as possible we would like you to use components directly without creating custom components. If you need to create a custom component, you should use the components provided by Backstage UI.
+
+
+
+If you need to build custom components outside of BUI, you can use our [design tokens](/tokens) as CSS variables to ensure your styles stay consistent with the rest of the system.
+
+
+
+When building custom interactive components, we strongly recommend using [React Aria](https://react-spectrum.adobe.com/react-aria/) as your foundation. React Aria provides all the necessary accessibility features out of the box — keyboard navigation, focus management, ARIA attributes, and screen reader support — so you can focus on styling and logic without worrying about compliance.
+
+
## Philosophy
diff --git a/docs-ui/src/app/snippets.ts b/docs-ui/src/app/snippets.ts
index 7e63005172..e16463a085 100644
--- a/docs-ui/src/app/snippets.ts
+++ b/docs-ui/src/app/snippets.ts
@@ -1,6 +1,39 @@
-export const snippet = `import { Flex, Button, Text } from '@backstage/ui';
+export const surfacesSnippet = `
+
+ Hello World
+
+
+ Hello World
+
+ `;
-
+export const adaptiveSnippet = `
+ {/* automatically set background to neutral-2 */}
+ Button with background set to neutral-3
+
+ `;
+
+export const customCardSnippet = `
Hello World
- Click me
- ;`;
+`;
+
+export const customTokensSnippet = ``;
+
+export const colorPickerSnippet = `import { ColorPicker, ColorArea, ColorSlider, ColorField } from 'react-aria-components';
+
+function MyColorPicker() {
+ return (
+
+
+
+
+
+ );
+}`;
diff --git a/docs-ui/src/app/tokens/page.mdx b/docs-ui/src/app/tokens/page.mdx
index 8e9031c81c..f359c10525 100644
--- a/docs-ui/src/app/tokens/page.mdx
+++ b/docs-ui/src/app/tokens/page.mdx
@@ -107,8 +107,6 @@ the value, you add an object with the value and the breakpoint prefix.
## Base colors
-These colors are used for special purposes like ring, scrollbar, ...
-
@@ -133,62 +131,15 @@ These colors are used for special purposes like ring, scrollbar, ...
Pure white color. This one should be the same in light and dark themes.
-
-
- --bui-gray-1
-
- You can use these mostly for backgrounds colors.
-
-
-
- --bui-gray-2
-
- You can use these mostly for backgrounds colors.
-
-
-
- --bui-gray-3
-
- You can use these mostly for backgrounds colors.
-
-
-
- --bui-gray-4
-
- You can use these mostly for backgrounds colors.
-
-
-
- --bui-gray-5
-
- You can use these mostly for backgrounds colors.
-
-
-
- --bui-gray-6
-
- You can use these mostly for backgrounds colors.
-
-
-
- --bui-gray-7
-
- You can use these mostly for backgrounds colors.
-
-
-
- --bui-gray-8
-
- You can use these mostly for backgrounds colors.
-
-## Core background colors
+## Neutral background colors
-These colors are used for the background of your application. We are mostly using for now a
-single elevated background for panels. `--bui-bg` should mostly use as the main background
-color of your app.
+These colors form a layered neutral scale for your application backgrounds.
+`--bui-bg-app` is the base background color of your app. Each subsequent level
+(1 through 4) represents an elevated layer on top of the previous one, with hover,
+pressed, and disabled variants for interactive states.
@@ -200,22 +151,129 @@ color of your app.
- --bui-bg
+ --bui-bg-app
+
+
+ The base background color of your Backstage instance.
- The background color of your Backstage instance.
- --bui-bg-surface-1
+ --bui-bg-neutral-1
+
+
+ First elevated layer. Use for cards, dialogs, and panels.
- Use for any panels or elevated surfaces.
- --bui-bg-surface-2
+ --bui-bg-neutral-1-hover
- Use for any panels or elevated surfaces.
+ Hover state for elements on neutral-1.
+
+
+ --bui-bg-neutral-1-pressed
+
+ Pressed state for elements on neutral-1.
+
+
+
+ --bui-bg-neutral-1-disabled
+
+ Disabled state for elements on neutral-1.
+
+
+
+ --bui-bg-neutral-2
+
+
+ Second elevated layer. Use for elements on top of neutral-1.
+
+
+
+
+ --bui-bg-neutral-2-hover
+
+ Hover state for elements on neutral-2.
+
+
+
+ --bui-bg-neutral-2-pressed
+
+ Pressed state for elements on neutral-2.
+
+
+
+ --bui-bg-neutral-2-disabled
+
+ Disabled state for elements on neutral-2.
+
+
+
+ --bui-bg-neutral-3
+
+
+ Third elevated layer. Use for elements on top of neutral-2.
+
+
+
+
+ --bui-bg-neutral-3-hover
+
+ Hover state for elements on neutral-3.
+
+
+
+ --bui-bg-neutral-3-pressed
+
+ Pressed state for elements on neutral-3.
+
+
+
+ --bui-bg-neutral-3-disabled
+
+ Disabled state for elements on neutral-3.
+
+
+
+ --bui-bg-neutral-4
+
+
+ Fourth elevated layer. Use for elements on top of neutral-3.
+
+
+
+
+ --bui-bg-neutral-4-hover
+
+ Hover state for elements on neutral-4.
+
+
+
+ --bui-bg-neutral-4-pressed
+
+ Pressed state for elements on neutral-4.
+
+
+
+ --bui-bg-neutral-4-disabled
+
+ Disabled state for elements on neutral-4.
+
+
+
+
+## Solid background colors
+
+
+
+
+ Prop
+ Description
+
+
+
--bui-bg-solid
@@ -240,30 +298,19 @@ color of your app.
Used for solid background colors when disabled.
-
-
- --bui-bg-tint
-
- Used for tint background colors.
-
-
-
- --bui-bg-tint-hover
-
- Used for tint background colors when hovered.
-
-
-
- --bui-bg-tint-focus
-
- Used for tint background colors when active.
-
-
-
- --bui-bg-tint-disabled
-
- Used for tint background colors when disabled.
-
+
+
+
+## Status background colors
+
+
+
+
+ Prop
+ Description
+
+
+
--bui-bg-danger
@@ -282,6 +329,12 @@ color of your app.
Used to show success information.
+
+
+ --bui-bg-info
+
+ Used to show informational content.
+
@@ -315,22 +368,6 @@ are prefixed with `fg` to make it easier to identify.
It should be used on top of main background surfaces.
-
-
- --bui-fg-link
-
-
- It should be used on top of main background surfaces.
-
-
-
-
- --bui-fg-link-hover
-
-
- It should be used on top of main background surfaces.
-
-
--bui-fg-disabled
@@ -347,33 +384,45 @@ are prefixed with `fg` to make it easier to identify.
It should be used on top of solid background colors.
-
-
- --bui-fg-tint
-
-
- It should be used on top of tint background colors.
-
-
-
-
- --bui-fg-tint-disabled
-
-
- It should be used on top of tint background colors when disabled.
-
-
--bui-fg-danger
+ Used for error states and destructive actions.
+
+
+
+ --bui-fg-warning
+
+
+ Used for warning states and cautionary information.
+
+
+
+
+ --bui-fg-success
+
+ Used for success states and positive feedback.
+
+
+
+ --bui-fg-info
+
+
+ Used for informational content and neutral status.
+
+
+
+
+ --bui-fg-danger-on-bg
+
It should be used on top of danger background colors.
- --bui-fg-warning
+ --bui-fg-warning-on-bg
It should be used on top of warning background colors.
@@ -381,12 +430,20 @@ are prefixed with `fg` to make it easier to identify.
- --bui-fg-success
+ --bui-fg-success-on-bg
It should be used on top of success background colors.
+
+
+ --bui-fg-info-on-bg
+
+
+ It should be used on top of info background colors.
+
+
@@ -405,31 +462,15 @@ low contrast to help as a separator with the different background colors.
- --bui-border
+ --bui-border-1
- It should be used on top of `--bui-bg-surface-1`.
+ Subtle border for low-contrast separators.
- --bui-border-hover
+ --bui-border-2
-
- Used when the component is interactive and hovered.
-
-
-
-
- --bui-border-pressed
-
-
- Used when the component is interactive and focused.
-
-
-
-
- --bui-border-disabled
-
- Used when the component is disabled.
+ It should be used on top of `--bui-bg-neutral-1`.
@@ -449,6 +490,12 @@ low contrast to help as a separator with the different background colors.
It should be used on top of `--bui-bg-success`.
+
+
+ --bui-border-info
+
+ It should be used on top of `--bui-bg-info`.
+
diff --git a/docs-ui/src/components/Changelog/index.tsx b/docs-ui/src/components/Changelog/index.tsx
index d7a547b901..c1f3a5660b 100644
--- a/docs-ui/src/components/Changelog/index.tsx
+++ b/docs-ui/src/components/Changelog/index.tsx
@@ -1,76 +1,21 @@
import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
+import { Badge, BreakingBadge, generateChangelogMarkdown } from './utils';
export function Changelog() {
- // Group changelog entries by version
- const groupedChangelog = changelog.reduce((acc, entry) => {
- if (!acc[entry.version]) {
- acc[entry.version] = [];
- }
- acc[entry.version].push(entry);
- return acc;
- }, {} as Record);
+ const content = generateChangelogMarkdown(changelog, {
+ showComponentBadges: true,
+ });
- // Sort versions in descending order
- const sortedVersions = Object.keys(groupedChangelog).sort((a, b) =>
- b.localeCompare(a),
+ return (
+
);
-
- const content = sortedVersions
- .map(version => {
- const entries = groupedChangelog[version];
-
- // Group entries by bump type
- const groupedByBump = entries.reduce((acc, entry) => {
- const bumpType = entry.type || 'other';
- if (!acc[bumpType]) {
- acc[bumpType] = [];
- }
- acc[bumpType].push(entry);
- return acc;
- }, {} as Record);
-
- // Define the order of bump types
- const bumpOrder = ['breaking', 'new', 'fix', 'other'];
-
- const bumpSections = bumpOrder
- .filter(bumpType => groupedByBump[bumpType]?.length > 0)
- .map(bumpType => {
- const bumpEntries = groupedByBump[bumpType];
- let sectionTitle = 'Other Changes';
- if (bumpType === 'breaking') {
- sectionTitle = 'Breaking Changes';
- } else if (bumpType === 'new') {
- sectionTitle = 'New Features';
- } else if (bumpType === 'fix') {
- sectionTitle = 'Bug Fixes';
- }
-
- return `### ${sectionTitle}
-
- ${bumpEntries
- .map(e => {
- const prs =
- e.prs.length > 0
- ? e.prs
- .map(
- pr =>
- `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
- )
- .join(', ')
- : '';
- return `- ${e.description}${prs ? ` ${prs}` : ''}`;
- })
- .join('\n')}`;
- })
- .join('\n\n');
-
- return `## Version ${version}
-
- ${bumpSections}`;
- })
- .join('\n');
-
- return ;
}
diff --git a/docs-ui/src/components/Changelog/utils.tsx b/docs-ui/src/components/Changelog/utils.tsx
new file mode 100644
index 0000000000..d2daed1176
--- /dev/null
+++ b/docs-ui/src/components/Changelog/utils.tsx
@@ -0,0 +1,185 @@
+import type { ChangelogProps } from '@/utils/types';
+
+// Badge Components
+export const Badge = ({
+ children,
+ variant = 'gray',
+}: {
+ children: React.ReactNode;
+ variant?: 'red' | 'gray';
+}) => {
+ const colors = {
+ red: {
+ backgroundColor: 'var(--badge-red-bg)',
+ color: 'var(--badge-red-color)',
+ },
+ gray: {
+ backgroundColor: 'var(--badge-gray-bg)',
+ color: 'var(--badge-gray-color)',
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const BreakingBadge = () => Breaking ;
+
+// Utility Functions
+export const toTitleCase = (kebabCase: string): string => {
+ return kebabCase
+ .split('-')
+ .map(word => word.charAt(0).toLocaleUpperCase('en-US') + word.slice(1))
+ .join(' ');
+};
+
+export const groupByVersion = (
+ entries: ChangelogProps[],
+): Record => {
+ return entries.reduce((acc, entry) => {
+ if (!acc[entry.version]) {
+ acc[entry.version] = [];
+ }
+ acc[entry.version].push(entry);
+ return acc;
+ }, {} as Record);
+};
+
+export const sortVersions = (versions: string[]): string[] => {
+ return versions.sort((a, b) => {
+ const aParts = a.split('.').map(Number);
+ const bParts = b.split('.').map(Number);
+
+ for (let i = 0; i < 3; i++) {
+ if (bParts[i] !== aParts[i]) {
+ return bParts[i] - aParts[i]; // Descending order
+ }
+ }
+ return 0;
+ });
+};
+
+export const formatPRLinks = (prs: string[]): string => {
+ if (prs.length === 0) return '';
+
+ return prs
+ .map(pr => `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`)
+ .join(', ');
+};
+
+export const generateEntryMarkdown = (
+ entry: ChangelogProps,
+ options: { showComponentBadges?: boolean } = {},
+): string => {
+ const { showComponentBadges = true } = options;
+ const prs = formatPRLinks(entry.prs);
+
+ // Prepend component names as badges if available and requested
+ const componentBadges =
+ showComponentBadges && entry.components.length > 0
+ ? entry.components
+ .map(c => `${toTitleCase(c)} `)
+ .join(' ') + ' '
+ : '';
+
+ // Add breaking badge if this is a breaking change
+ const breakingBadge = entry.breaking ? ' ' : '';
+
+ // Remove **BREAKING**: text from description since we show it as a badge
+ const description = entry.description.replace(/\*\*BREAKING\*\*:?\s*/, '');
+
+ // Build the entry
+ let entryMarkdown = `- ${componentBadges}${breakingBadge}${description}`;
+ if (prs) {
+ entryMarkdown += ` ${prs}`;
+ }
+
+ // Add migration if present (should already be in description, but check)
+ if (entry.migration && !entry.description.includes('**Migration:**')) {
+ entryMarkdown += `\n\n Migration Guide:\n\n ${entry.migration
+ .split('\n')
+ .join('\n ')}`;
+ }
+
+ return entryMarkdown;
+};
+
+export interface GenerateChangelogOptions {
+ showComponentBadges?: boolean;
+ headingLevel?: number;
+}
+
+export const generateChangelogMarkdown = (
+ entries: ChangelogProps[],
+ options: GenerateChangelogOptions = {},
+): string => {
+ const { showComponentBadges = true, headingLevel = 2 } = options;
+
+ // Group changelog entries by version
+ const groupedChangelog = groupByVersion(entries);
+
+ // Sort versions in descending order (semantic versioning)
+ const sortedVersions = sortVersions(Object.keys(groupedChangelog));
+
+ // Generate heading prefix based on level (e.g., "##" for level 2, "###" for level 3)
+ const versionHeading = '#'.repeat(headingLevel);
+ const sectionHeading = '#'.repeat(headingLevel + 1);
+
+ const content = sortedVersions
+ .map(version => {
+ const versionEntries = groupedChangelog[version];
+
+ // Group entries: Breaking vs Everything Else
+ const breakingChanges = versionEntries.filter(e => e.breaking);
+ const otherChanges = versionEntries.filter(e => !e.breaking);
+
+ const sections = [];
+
+ // Breaking changes section
+ if (breakingChanges.length > 0) {
+ sections.push({
+ title: 'Breaking Changes',
+ entries: breakingChanges,
+ });
+ }
+
+ // All other changes section
+ if (otherChanges.length > 0) {
+ sections.push({
+ title: 'Changes',
+ entries: otherChanges,
+ });
+ }
+
+ const bumpSections = sections
+ .map(({ title, entries: sectionEntries }) => {
+ const entriesMarkdown = sectionEntries
+ .map(e => generateEntryMarkdown(e, { showComponentBadges }))
+ .join('\n\n');
+
+ return `${sectionHeading} ${title}\n\n${entriesMarkdown}`;
+ })
+ .join('\n\n');
+
+ return `${versionHeading} Version ${version}
+
+ ${bumpSections}`;
+ })
+ .join('\n');
+
+ return content;
+};
diff --git a/docs-ui/src/components/ChangelogComponent/index.tsx b/docs-ui/src/components/ChangelogComponent/index.tsx
index ff86433268..92068d04f8 100644
--- a/docs-ui/src/components/ChangelogComponent/index.tsx
+++ b/docs-ui/src/components/ChangelogComponent/index.tsx
@@ -2,34 +2,32 @@ import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
import type { Component } from '@/utils/changelog';
+import {
+ Badge,
+ BreakingBadge,
+ generateChangelogMarkdown,
+} from '../Changelog/utils';
export const ChangelogComponent = ({ component }: { component: Component }) => {
const componentChangelog = changelog.filter(c =>
c.components.includes(component),
);
+ const content = `## Changelog
+
+${generateChangelogMarkdown(componentChangelog, {
+ showComponentBadges: false,
+ headingLevel: 3,
+})}`;
+
return (
{
- const prs =
- change.prs.length > 0
- ? change.prs
- .map(
- pr =>
- `[#${pr}](https://github.com/backstage/backstage/pull/${pr})`,
- )
- .join(', ')
- : '';
- return `- \`${change.version}\` - ${change.description}${
- prs ? ` ${prs}` : ''
- }`;
- })
- .join('\n')}`}
+ components={{
+ ...formattedMDXComponents,
+ Badge,
+ BreakingBadge,
+ }}
+ source={content}
/>
);
};
diff --git a/docs-ui/src/components/Chip/styles.module.css b/docs-ui/src/components/Chip/styles.module.css
index 01ee29311d..d752ae2602 100644
--- a/docs-ui/src/components/Chip/styles.module.css
+++ b/docs-ui/src/components/Chip/styles.module.css
@@ -1,11 +1,9 @@
.chip {
- display: inline-flex;
- align-items: center;
+ display: inline-block;
font-family: monospace;
font-size: 13px;
border-radius: 6px;
- padding: 0px 8px;
- height: 24px;
+ padding: 4px 8px;
margin-right: 4px;
background-color: #f0f0f0;
color: #5d5d5d;
diff --git a/docs-ui/src/components/ColorFamily/ColorFamily.module.css b/docs-ui/src/components/ColorFamily/ColorFamily.module.css
new file mode 100644
index 0000000000..f38e481383
--- /dev/null
+++ b/docs-ui/src/components/ColorFamily/ColorFamily.module.css
@@ -0,0 +1,94 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ margin-top: 1rem;
+ margin-bottom: 2rem;
+}
+
+@media (min-width: 768px) {
+ .wrapper {
+ flex-direction: row;
+ align-items: flex-start;
+ }
+}
+
+.visual {
+ flex: 1;
+ min-width: 0;
+}
+
+.text {
+ flex: 1;
+ min-width: 0;
+}
+
+.base {
+ border-radius: 12px;
+ padding: 16px;
+}
+
+.baseLabel {
+ display: block;
+ font-size: 13px;
+ color: var(--bui-fg-primary);
+ margin-bottom: 12px;
+}
+
+.level {
+ border-radius: 10px;
+ padding: 12px;
+}
+
+.levelHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.level > .level > .levelHeader:last-child,
+.level:not(:has(> .level)) > .levelHeader {
+ margin-bottom: 0;
+}
+
+.levelLabel {
+ font-size: 13px;
+ color: var(--bui-fg-primary);
+ white-space: nowrap;
+}
+
+.chips {
+ display: flex;
+ gap: 6px;
+}
+
+.chip {
+ padding: 6px 10px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--bui-fg-primary);
+ white-space: nowrap;
+}
+
+.description {
+ color: var(--primary);
+ margin-top: 0;
+ margin-bottom: 1rem;
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+.description:last-child {
+ margin-bottom: 0;
+}
+
+.link {
+ color: var(--link);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
diff --git a/docs-ui/src/components/ColorFamily/ColorFamily.tsx b/docs-ui/src/components/ColorFamily/ColorFamily.tsx
new file mode 100644
index 0000000000..07b8779503
--- /dev/null
+++ b/docs-ui/src/components/ColorFamily/ColorFamily.tsx
@@ -0,0 +1,97 @@
+'use client';
+
+import styles from './ColorFamily.module.css';
+
+const StateChip = ({
+ level,
+ state,
+ label,
+}: {
+ level: number;
+ state: string;
+ label: string;
+}) => (
+
+ {label}
+
+);
+
+const NeutralLevel = ({
+ level,
+ children,
+ height,
+}: {
+ level: number;
+ children?: React.ReactNode;
+ height?: number;
+}) => (
+
+
+
Neutral {level}
+
+
+
+
+
+
+ {children}
+
+);
+
+export const ColorFamily = () => {
+ return (
+
+
+
+ Neutral 0
+
+
+
+
+
+
+
+
+
+
+
+ BUI uses a layered neutral scale from 0 to 4. Each level nests inside
+ the previous one, automatically incrementing the background depth.
+ This creates clear visual hierarchy without manually picking colors.
+
+
+ Neutral 0 is the application background and should only be used once,
+ at the root of your app. All other surfaces build on top of it.
+
+
+ Each level can be interactive or{' '}
+ non-interactive . A Card, for example, can be flat
+ (just a surface) or fully clickable with hover and pressed states. The
+ three interaction states (hover, pressed, disabled) are built into
+ every neutral level.
+
+
+ Explore the full list of color tokens on the{' '}
+
+ tokens page
+
+ .
+
+
+
+ );
+};
diff --git a/docs-ui/src/components/ColorFamily/index.ts b/docs-ui/src/components/ColorFamily/index.ts
new file mode 100644
index 0000000000..12f7fb4008
--- /dev/null
+++ b/docs-ui/src/components/ColorFamily/index.ts
@@ -0,0 +1 @@
+export { ColorFamily } from './ColorFamily';
diff --git a/docs-ui/src/components/CommandPalette/CommandPalette.module.css b/docs-ui/src/components/CommandPalette/CommandPalette.module.css
new file mode 100644
index 0000000000..dbbdb72ceb
--- /dev/null
+++ b/docs-ui/src/components/CommandPalette/CommandPalette.module.css
@@ -0,0 +1,154 @@
+.overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding-top: 15vh;
+
+ &[data-entering] {
+ animation: overlayFadeIn 200ms ease-out;
+ }
+
+ &[data-exiting] {
+ animation: overlayFadeOut 150ms ease-in;
+ }
+}
+
+@keyframes overlayFadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes overlayFadeOut {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+.modal {
+ width: min(90vw, 500px);
+ max-height: min(70vh, 400px);
+ background-color: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
+ display: flex;
+ flex-direction: column;
+ outline: none;
+ overflow: hidden;
+
+ &[data-entering] {
+ animation: modalScaleIn 200ms ease-out;
+ }
+
+ &[data-exiting] {
+ animation: modalScaleOut 150ms ease-in;
+ }
+}
+
+@keyframes modalScaleIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95) translateY(-8px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+@keyframes modalScaleOut {
+ from {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: scale(0.95) translateY(-8px);
+ }
+}
+
+.dialog {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ max-height: min(70vh, 400px);
+ outline: none;
+ overflow: hidden;
+}
+
+.searchField {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.srOnly {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
+.input {
+ width: 100%;
+ background: transparent;
+ border: none;
+ outline: none;
+ font-size: 1rem;
+ color: var(--primary);
+ font-family: inherit;
+
+ &::placeholder {
+ color: var(--secondary);
+ }
+}
+
+.menu {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+ outline: none;
+ border: none;
+}
+
+.menuItem {
+ padding: 8px 12px;
+ border-radius: 8px;
+ color: var(--primary);
+ cursor: pointer;
+ font-size: 0.875rem;
+ outline: none;
+
+ &[data-focused] {
+ background-color: var(--action);
+ }
+
+ &[data-pressed] {
+ background-color: var(--action);
+ }
+}
+
+.empty {
+ padding: 24px 12px;
+ text-align: center;
+ color: var(--secondary);
+ font-size: 0.875rem;
+}
diff --git a/docs-ui/src/components/CommandPalette/CommandPalette.tsx b/docs-ui/src/components/CommandPalette/CommandPalette.tsx
new file mode 100644
index 0000000000..bcc534372a
--- /dev/null
+++ b/docs-ui/src/components/CommandPalette/CommandPalette.tsx
@@ -0,0 +1,76 @@
+'use client';
+
+import {
+ Autocomplete,
+ Dialog,
+ Input,
+ Label,
+ Menu,
+ MenuItem,
+ Modal,
+ ModalOverlay,
+ SearchField,
+ useFilter,
+} from 'react-aria-components';
+import { useRouter } from 'next/navigation';
+import { components } from '@/utils/data';
+import styles from './CommandPalette.module.css';
+
+interface CommandPaletteProps {
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+}
+
+const items = components.map(c => ({ id: c.slug, ...c }));
+
+export const CommandPalette = ({
+ isOpen,
+ onOpenChange,
+}: CommandPaletteProps) => {
+ const router = useRouter();
+ const { contains } = useFilter({ sensitivity: 'base' });
+
+ return (
+
+
+
+
+
+ Search
+
+
+ {
+ router.push(`/components/${key}`);
+ onOpenChange(false);
+ }}
+ renderEmptyState={() => (
+ No components found.
+ )}
+ >
+ {item => (
+
+ {item.title}
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/docs-ui/src/components/CommandPalette/index.ts b/docs-ui/src/components/CommandPalette/index.ts
new file mode 100644
index 0000000000..95c1305982
--- /dev/null
+++ b/docs-ui/src/components/CommandPalette/index.ts
@@ -0,0 +1 @@
+export { CommandPalette } from './CommandPalette';
diff --git a/docs-ui/src/components/ComponentCards/ComponentCards.module.css b/docs-ui/src/components/ComponentCards/ComponentCards.module.css
deleted file mode 100644
index 8eb9dbd3d0..0000000000
--- a/docs-ui/src/components/ComponentCards/ComponentCards.module.css
+++ /dev/null
@@ -1,54 +0,0 @@
-.grid {
- display: grid;
- grid-template-columns: repeat(1, 1fr);
- gap: 1rem;
- margin-top: 1rem;
- margin-bottom: 3rem;
-}
-
-@media (min-width: 768px) {
- .grid {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-@media (min-width: 1024px) {
- .grid {
- grid-template-columns: repeat(3, 1fr);
- }
-}
-
-@media (min-width: 1280px) {
- .grid {
- grid-template-columns: repeat(4, 1fr);
- }
-}
-
-.card {
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- background-color: var(--bg);
- border-radius: 8px;
- border: 1px solid var(--border);
- padding: 16px;
- gap: 4px;
- min-height: 120px;
-
- &:hover {
- background-color: var(--bg-hover);
- }
-}
-
-.title {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
- color: var(--primary);
-}
-
-.description {
- margin: 0;
- font-size: 14px;
- color: var(--secondary);
-}
diff --git a/docs-ui/src/components/ComponentCards/ComponentCards.tsx b/docs-ui/src/components/ComponentCards/ComponentCards.tsx
deleted file mode 100644
index 0e4d22c02a..0000000000
--- a/docs-ui/src/components/ComponentCards/ComponentCards.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import Link from 'next/link';
-import styles from './ComponentCards.module.css';
-
-export const ComponentCards = ({ children }: { children: React.ReactNode }) => {
- return {children}
;
-};
-
-export const ComponentCard = ({
- title,
- description,
- href,
-}: {
- title: string;
- description: string;
- href: string;
-}) => {
- return (
-
- {title}
- {description}
-
- );
-};
diff --git a/docs-ui/src/components/ComponentCards/index.ts b/docs-ui/src/components/ComponentCards/index.ts
deleted file mode 100644
index 4bc4beaae3..0000000000
--- a/docs-ui/src/components/ComponentCards/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './ComponentCards';
diff --git a/docs-ui/src/components/ComponentGrid/ComponentGrid.module.css b/docs-ui/src/components/ComponentGrid/ComponentGrid.module.css
new file mode 100644
index 0000000000..a6dd54a864
--- /dev/null
+++ b/docs-ui/src/components/ComponentGrid/ComponentGrid.module.css
@@ -0,0 +1,29 @@
+.grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ margin-top: 1rem;
+ margin-bottom: 2rem;
+}
+
+@media (min-width: 768px) {
+ .grid {
+ grid-template-columns: repeat(4, 1fr);
+ }
+}
+
+.item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px 0;
+ font-size: 14px;
+ font-weight: 400;
+ color: var(--primary);
+ text-decoration: none;
+
+ &:hover {
+ color: var(--link);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ }
+}
diff --git a/docs-ui/src/components/ComponentGrid/ComponentGrid.tsx b/docs-ui/src/components/ComponentGrid/ComponentGrid.tsx
new file mode 100644
index 0000000000..eb0f043420
--- /dev/null
+++ b/docs-ui/src/components/ComponentGrid/ComponentGrid.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import Link from 'next/link';
+import { components } from '@/utils/data';
+import styles from './ComponentGrid.module.css';
+
+export const ComponentGrid = () => {
+ return (
+
+ {components.map(item => (
+
+ {item.title}
+
+ ))}
+
+ );
+};
diff --git a/docs-ui/src/components/ComponentGrid/index.ts b/docs-ui/src/components/ComponentGrid/index.ts
new file mode 100644
index 0000000000..f0fe06477a
--- /dev/null
+++ b/docs-ui/src/components/ComponentGrid/index.ts
@@ -0,0 +1 @@
+export * from './ComponentGrid';
diff --git a/docs-ui/src/components/CustomTheme/customTheme.tsx b/docs-ui/src/components/CustomTheme/customTheme.tsx
index ae765d7d88..539113140a 100644
--- a/docs-ui/src/components/CustomTheme/customTheme.tsx
+++ b/docs-ui/src/components/CustomTheme/customTheme.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useState, useCallback } from 'react';
+import { useEffect, useState, useCallback, useSyncExternalStore } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { sass } from '@codemirror/lang-sass';
import styles from './styles.module.css';
@@ -14,6 +14,10 @@ const defaultTheme = `:root {
--bui-bg-solid: #000;
}`;
+// Stable server snapshots for useSyncExternalStore
+const serverIsClient = false;
+const serverDefaultTheme = defaultTheme;
+
const myTheme = createTheme({
theme: 'light',
settings: {
@@ -46,12 +50,41 @@ const myTheme = createTheme({
});
export const CustomTheme = () => {
- const [isClient, setIsClient] = useState(false);
const [open, setOpen] = useState(true);
- const [customTheme, setCustomTheme] = useState(undefined);
const { selectedThemeName } = usePlayground();
const [savedMessage, setSavedMessage] = useState('Save');
+ // SSR-safe client detection
+ const isClient = useSyncExternalStore(
+ () => () => {},
+ () => true,
+ () => serverIsClient,
+ );
+
+ // SSR-safe localStorage access for custom theme
+ const customThemeFromStorage = useSyncExternalStore(
+ callback => {
+ window.addEventListener('storage', callback);
+ return () => window.removeEventListener('storage', callback);
+ },
+ () => {
+ const stored = localStorage.getItem('customThemeCss');
+ if (!stored) {
+ localStorage.setItem('customThemeCss', defaultTheme);
+ return defaultTheme;
+ }
+ return stored;
+ },
+ () => serverDefaultTheme,
+ );
+
+ const [customTheme, setCustomTheme] = useState(customThemeFromStorage);
+
+ // Sync from storage when it changes
+ useEffect(() => {
+ setCustomTheme(customThemeFromStorage);
+ }, [customThemeFromStorage]);
+
const updateStyleElement = (theme: string) => {
let styleElement = document.getElementById(
'custom-theme-style',
@@ -66,16 +99,11 @@ export const CustomTheme = () => {
styleElement.textContent = theme;
};
+ // Apply custom theme to DOM
useEffect(() => {
- if (selectedThemeName === 'custom') {
- let storedTheme = localStorage.getItem('customThemeCss');
- if (!storedTheme) {
- storedTheme = defaultTheme;
- localStorage.setItem('customThemeCss', storedTheme);
- }
- setCustomTheme(storedTheme);
- updateStyleElement(storedTheme);
- } else {
+ if (selectedThemeName === 'custom' && customTheme && isClient) {
+ updateStyleElement(customTheme);
+ } else if (isClient) {
const styleElement = document.getElementById(
'custom-theme-style',
) as HTMLStyleElement;
@@ -83,11 +111,7 @@ export const CustomTheme = () => {
styleElement.remove();
}
}
- }, [selectedThemeName]);
-
- useEffect(() => {
- setIsClient(true);
- }, []);
+ }, [selectedThemeName, customTheme, isClient]);
const handleSave = () => {
if (customTheme) {
diff --git a/docs-ui/src/components/DecorativeBox/index.tsx b/docs-ui/src/components/DecorativeBox/index.tsx
index a5c20725bc..8af067459e 100644
--- a/docs-ui/src/components/DecorativeBox/index.tsx
+++ b/docs-ui/src/components/DecorativeBox/index.tsx
@@ -1,5 +1,17 @@
+'use client';
+
+import { CSSProperties, ReactNode } from 'react';
import styles from './styles.module.css';
-export const DecorativeBox = () => {
- return
;
+interface DecorativeBoxProps {
+ children?: ReactNode;
+ style?: CSSProperties;
+}
+
+export const DecorativeBox = ({ children, style }: DecorativeBoxProps) => {
+ return (
+
+ {children}
+
+ );
};
diff --git a/docs-ui/src/components/DecorativeBox/styles.module.css b/docs-ui/src/components/DecorativeBox/styles.module.css
index 84a6b579a0..a2a5c9407a 100644
--- a/docs-ui/src/components/DecorativeBox/styles.module.css
+++ b/docs-ui/src/components/DecorativeBox/styles.module.css
@@ -1,8 +1,14 @@
.box {
min-width: 64px;
min-height: 64px;
+ padding: 8px 12px;
background-color: #eaf2fd;
border-radius: 4px;
box-shadow: 0 0 0 1px #2563eb;
background-image: url("data:image/svg+xml,%3Csvg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%232563eb' fill-opacity='0.3' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E");
+ color: #2563eb;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
diff --git a/docs-ui/src/components/LayoutComponents/LayoutComponents.module.css b/docs-ui/src/components/LayoutComponents/LayoutComponents.module.css
deleted file mode 100644
index 9009cdb6a4..0000000000
--- a/docs-ui/src/components/LayoutComponents/LayoutComponents.module.css
+++ /dev/null
@@ -1,48 +0,0 @@
-.layoutComponents {
- display: flex;
- justify-content: flex-start;
- gap: 1rem;
- flex-wrap: wrap;
- margin-top: 2rem;
-
- & svg rect {
- transition: fill 0.2s ease-in-out;
- }
-
- & .box {
- display: flex;
- flex-direction: column;
- width: calc(50% - 0.5rem);
- margin-bottom: 1rem;
- align-items: flex-start;
- }
-
- & .content {
- flex: none;
- background-color: var(--bg);
- border: 1px solid var(--border);
- border-radius: 4px;
- width: 100%;
- height: 180px;
- transition: all 0.2s ease-in-out;
- margin-bottom: 0.75rem;
- display: flex;
- align-items: center;
- justify-content: center;
-
- &:hover {
- transform: translateY(-4px);
- }
- }
-
- & .title {
- font-size: 16px;
- transition: color 0.2s ease-in-out;
- margin-bottom: 0.25rem;
- }
-
- & .description {
- font-size: 16px;
- color: var(--secondary);
- }
-}
diff --git a/docs-ui/src/components/LayoutComponents/LayoutComponents.tsx b/docs-ui/src/components/LayoutComponents/LayoutComponents.tsx
deleted file mode 100644
index 0bf822526e..0000000000
--- a/docs-ui/src/components/LayoutComponents/LayoutComponents.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { BoxSvg } from './svgs/box';
-import { FlexSvg } from './svgs/flex';
-import { GridSvg } from './svgs/grid';
-import { ContainerSvg } from './svgs/container';
-import styles from './LayoutComponents.module.css';
-import Link from 'next/link';
-
-export const LayoutComponents = () => {
- return (
-
-
-
-
-
-
Box
-
- The most basic layout component
-
-
-
-
-
-
-
Flex
-
- Arrange your components vertically
-
-
-
-
-
-
-
Grid
-
- Arrange your components in a grid
-
-
-
-
-
-
-
Container
-
- A container for your components
-
-
-
- );
-};
diff --git a/docs-ui/src/components/LayoutComponents/index.ts b/docs-ui/src/components/LayoutComponents/index.ts
deleted file mode 100644
index 8efb793bbd..0000000000
--- a/docs-ui/src/components/LayoutComponents/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { LayoutComponents } from './LayoutComponents';
diff --git a/docs-ui/src/components/Navigation/Navigation.module.css b/docs-ui/src/components/Navigation/Navigation.module.css
index f5e1312ab1..efba109f97 100644
--- a/docs-ui/src/components/Navigation/Navigation.module.css
+++ b/docs-ui/src/components/Navigation/Navigation.module.css
@@ -49,16 +49,13 @@
}
.sectionTitle {
- font-size: 0.6875rem;
- font-weight: 500;
- padding: 0 12px 4px;
- color: var(--secondary);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--primary);
margin-top: 40px;
- text-transform: uppercase;
-
- &:first-child {
- margin-top: 12px;
- }
+ padding: 8px 12px;
+ margin-bottom: 8px;
}
.line {
diff --git a/docs-ui/src/components/Navigation/Navigation.tsx b/docs-ui/src/components/Navigation/Navigation.tsx
index 13fa87661f..bad77e942e 100644
--- a/docs-ui/src/components/Navigation/Navigation.tsx
+++ b/docs-ui/src/components/Navigation/Navigation.tsx
@@ -2,7 +2,6 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
-import { Fragment } from 'react';
import clsx from 'clsx';
import {
RiCollageLine,
@@ -12,26 +11,13 @@ import {
RiServiceLine,
RiStackLine,
} from '@remixicon/react';
-import { components, layoutComponents } from '@/utils/data';
+import { components } from '@/utils/data';
import styles from './Navigation.module.css';
interface NavigationProps {
onLinkClick?: () => void;
}
-const data = [
- {
- title: 'Layout Components',
- content: layoutComponents,
- url: '/components',
- },
- {
- title: 'Components',
- content: components,
- url: '/components',
- },
-];
-
export const Navigation = ({ onLinkClick }: NavigationProps) => {
const pathname = usePathname();
@@ -55,16 +41,6 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => {
Tokens
-
-
-
- Components
-
-
@@ -89,35 +65,31 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => {
- {data.map(section => {
+
+
+ Components
+
+ {components.map(item => {
+ const isActive = pathname === `/components/${item.slug}`;
+
return (
-
- {section.title}
-
- {section.content.map(item => {
- const isActive = pathname === `${section.url}/${item.slug}`;
-
- return (
-
- {item.title}
-
- {item.status === 'alpha' && 'Alpha'}
- {item.status === 'beta' && 'Beta'}
- {item.status === 'inProgress' && 'In Progress'}
- {item.status === 'stable' && 'Stable'}
- {item.status === 'deprecated' && 'Deprecated'}
-
-
- );
+
+ onClick={onLinkClick}
+ >
+ {item.title}
+
+ {item.status === 'alpha' && 'Alpha'}
+ {item.status === 'beta' && 'Beta'}
+ {item.status === 'inProgress' && 'In Progress'}
+ {item.status === 'stable' && 'Stable'}
+ {item.status === 'deprecated' && 'Deprecated'}
+
+
);
})}
>
diff --git a/docs-ui/src/components/PropsTable/PropsTable.tsx b/docs-ui/src/components/PropsTable/PropsTable.tsx
index 86b7b274ef..a8af2ad116 100644
--- a/docs-ui/src/components/PropsTable/PropsTable.tsx
+++ b/docs-ui/src/components/PropsTable/PropsTable.tsx
@@ -3,77 +3,133 @@
import * as Table from '../Table';
import { Chip } from '../Chip';
import { TypePopup } from './TypePopup';
+import { SpacingPopup } from './SpacingPopup';
+import { SpacingGroupRow } from './SpacingGroupRow';
import { PropDef } from '@/utils/propDefs';
-// Use the proper PropDef type
type PropData = PropDef;
-// Modify the PropsTable component to use the new type
+type ColumnType = 'prop' | 'type' | 'default' | 'description' | 'responsive';
+
+interface ColumnConfig {
+ key: ColumnType;
+ width: string;
+}
+
+const defaultColumns: ColumnConfig[] = [
+ { key: 'prop' as const, width: '15%' },
+ { key: 'type' as const, width: '25%' },
+ { key: 'default' as const, width: '15%' },
+ { key: 'description' as const, width: '45%' },
+];
+
+const columnLabels: Record = {
+ prop: 'Prop',
+ type: 'Type',
+ default: 'Default',
+ description: 'Description',
+ responsive: 'Responsive',
+};
+
export const PropsTable = >({
data,
+ columns = defaultColumns,
}: {
data: T;
+ columns?: ColumnConfig[];
}) => {
if (!data) return null;
+ const renderCell = (
+ propName: string,
+ propData: PropData,
+ column: ColumnType,
+ ) => {
+ const enumValues =
+ Array.isArray(propData.values) &&
+ propData.values.map(t => {t} );
+
+ switch (column) {
+ case 'prop':
+ return {propName} ;
+
+ case 'type':
+ return (
+
+ {propData.type === 'string' && string }
+ {propData.type === 'number' && number }
+ {propData.type === 'boolean' && boolean }
+ {propData.type === 'enum' && enumValues}
+ {propData.type === 'spacing' && Array.isArray(propData.values) && (
+
+ )}
+ {propData.type === 'complex' && propData.complexType && (
+
+ )}
+ {propData.type === 'enum | string' && (
+ <>
+ {enumValues}
+ string
+ >
+ )}
+
+ );
+
+ case 'default':
+ return propData.default ? {propData.default} : '-';
+
+ case 'description':
+ return propData.description ? (
+ {propData.description}
+ ) : null;
+
+ case 'responsive':
+ return {propData.responsive ? 'Yes' : 'No'} ;
+
+ default:
+ return null;
+ }
+ };
+
return (
- Prop
- Type
- Default
-
- Responsive
-
+ {columns.map(col => (
+
+ {columnLabels[col.key]}
+
+ ))}
{Object.keys(data).map(n => {
- const enumValues =
- Array.isArray(data[n].values) &&
- data[n].values.map(t => {t} );
+ const propData = data[n];
+ // Handle spacing-group type
+ if (propData.type === 'spacing-group' && propData.spacingGroup) {
+ return (
+
+ );
+ }
+
+ // Handle regular props
return (
-
- {n}
-
-
-
- {data[n].type === 'string' && string }
- {data[n].type === 'number' && number }
- {data[n].type === 'boolean' && boolean }
- {data[n].type === 'enum' && enumValues}
- {data[n].type === 'spacing' && (
- <>
- 0.5, 1, 1.5, 2, 3, ..., 14
- string
- >
- )}
- {data[n].type === 'complex' && data[n].complexType && (
-
- )}
- {data[n].type === 'enum | string' && (
- <>
- {enumValues}
- string
- >
- )}
-
-
-
- {data[n].default ? data[n].default : '-'}
-
-
- {data[n].responsive ? 'Yes' : 'No'}
-
+ {columns.map(col => (
+
+ {renderCell(n, propData, col.key)}
+
+ ))}
);
})}
diff --git a/docs-ui/src/components/PropsTable/SpacingGroupRow.module.css b/docs-ui/src/components/PropsTable/SpacingGroupRow.module.css
new file mode 100644
index 0000000000..71ade80b47
--- /dev/null
+++ b/docs-ui/src/components/PropsTable/SpacingGroupRow.module.css
@@ -0,0 +1,96 @@
+.expandButton {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ font-family: inherit;
+ color: inherit;
+ text-align: left;
+ gap: 0.5rem;
+}
+
+.expandButton:hover {
+ opacity: 0.8;
+}
+
+.propName {
+ font-family: monospace;
+ font-size: 13px;
+ font-weight: 500;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.count {
+ font-size: 12px;
+ color: var(--text-secondary);
+ font-weight: normal;
+}
+
+.icon {
+ color: var(--text-secondary);
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+}
+
+.expandedContent {
+ padding: 0.5rem 0;
+}
+
+.twoColumnLayout {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+}
+
+.column {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.columnTitle {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 0.25rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.propsGrid {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.propRow {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ gap: 1rem;
+ align-items: start;
+}
+
+.propRowName {
+ min-width: 80px;
+}
+
+.propRowDescription {
+ font-size: 14px;
+ color: var(--text-secondary);
+ line-height: 1.5;
+}
+
+.propRowDefault {
+ font-size: 12px;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
diff --git a/docs-ui/src/components/PropsTable/SpacingGroupRow.tsx b/docs-ui/src/components/PropsTable/SpacingGroupRow.tsx
new file mode 100644
index 0000000000..b3cc8de26d
--- /dev/null
+++ b/docs-ui/src/components/PropsTable/SpacingGroupRow.tsx
@@ -0,0 +1,142 @@
+'use client';
+
+import { useState } from 'react';
+import * as Table from '../Table';
+import { Chip } from '../Chip';
+import { SpacingPopup } from './SpacingPopup';
+import type { SpacingGroupDef } from '@/utils/propDefs';
+import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react';
+import styles from './SpacingGroupRow.module.css';
+
+interface SpacingGroupRowProps {
+ spacingGroup: SpacingGroupDef;
+ description?: React.ReactNode;
+ columns: Array<{ key: string; width: string }>;
+}
+
+export const SpacingGroupRow = ({
+ spacingGroup,
+ description,
+ columns,
+}: SpacingGroupRowProps) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ // Separate padding and margin props
+ const paddingProps = spacingGroup.props.filter(prop =>
+ prop.name.startsWith('p'),
+ );
+ const marginProps = spacingGroup.props.filter(prop =>
+ prop.name.startsWith('m'),
+ );
+
+ const renderCell = (columnKey: string) => {
+ switch (columnKey) {
+ case 'prop':
+ return (
+ setIsExpanded(!isExpanded)}
+ aria-expanded={isExpanded}
+ >
+
+ Spacing props{' '}
+
+ ({spacingGroup.props.length})
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ );
+
+ case 'type':
+ return ;
+
+ case 'default':
+ return '-';
+
+ case 'description':
+ return description ? (
+ {description}
+ ) : null;
+
+ case 'responsive':
+ return {spacingGroup.responsive ? 'Yes' : 'No'} ;
+
+ default:
+ return null;
+ }
+ };
+
+ return (
+ <>
+
+ {columns.map(col => (
+
+ {renderCell(col.key)}
+
+ ))}
+
+ {isExpanded && (
+
+
+
+
+ {/* Padding Column */}
+ {paddingProps.length > 0 && (
+
+
Padding
+
+ {paddingProps.map(prop => (
+
+
+ {prop.name}
+
+
+ {prop.description}
+
+ {prop.default && (
+
+ Default: {prop.default}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* Margin Column */}
+ {marginProps.length > 0 && (
+
+
Margin
+
+ {marginProps.map(prop => (
+
+
+ {prop.name}
+
+
+ {prop.description}
+
+ {prop.default && (
+
+ Default: {prop.default}
+
+ )}
+
+ ))}
+
+
+ )}
+
+
+
+
+ )}
+ >
+ );
+};
diff --git a/docs-ui/src/components/PropsTable/SpacingPopup.module.css b/docs-ui/src/components/PropsTable/SpacingPopup.module.css
new file mode 100644
index 0000000000..892bc14bc6
--- /dev/null
+++ b/docs-ui/src/components/PropsTable/SpacingPopup.module.css
@@ -0,0 +1,77 @@
+.button {
+ display: inline-flex;
+ align-items: center;
+ font-family: monospace;
+ font-size: 13px;
+ border-radius: 6px;
+ padding: 0px 8px;
+ height: 24px;
+ margin-right: 4px;
+ background-color: #f0f0f0;
+ color: #5d5d5d;
+ cursor: pointer;
+ border: none;
+ outline: none;
+ box-shadow: none;
+ transition: background-color 150ms ease;
+}
+
+.button:hover {
+ background-color: #e0e0e0;
+}
+
+[data-theme-mode='dark'] .button {
+ background-color: #2c2c2c;
+ color: #fff;
+}
+
+[data-theme-mode='dark'] .button:hover {
+ background-color: #3c3c3c;
+}
+
+.popover {
+ border: 1px solid var(--border);
+ box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
+ border-radius: 6px;
+ background: var(--bg);
+ color: var(--primary);
+ outline: none;
+ transition: transform 200ms, opacity 200ms;
+ padding: 1rem;
+ margin-top: 6px;
+ max-width: 400px;
+ --origin: translateY(-8px);
+
+ &[data-entering],
+ &[data-exiting] {
+ transform: var(--origin);
+ opacity: 0;
+ }
+}
+
+.arrow svg {
+ display: block;
+ fill: var(--bg);
+ stroke: var(--border);
+ stroke-width: 1px;
+ transform: rotate(180deg);
+}
+
+.title {
+ font-size: 14px;
+ font-weight: 500;
+ margin-bottom: 0.75rem;
+}
+
+.grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.375rem;
+ margin-bottom: 0.75rem;
+}
+
+.note {
+ font-size: 12px;
+ color: var(--text-secondary);
+ font-style: italic;
+}
diff --git a/docs-ui/src/components/PropsTable/SpacingPopup.tsx b/docs-ui/src/components/PropsTable/SpacingPopup.tsx
new file mode 100644
index 0000000000..9515ec4e5a
--- /dev/null
+++ b/docs-ui/src/components/PropsTable/SpacingPopup.tsx
@@ -0,0 +1,47 @@
+import { Chip } from '../Chip';
+import {
+ Button,
+ Dialog,
+ DialogTrigger,
+ OverlayArrow,
+ Popover,
+} from 'react-aria-components';
+import styles from './SpacingPopup.module.css';
+
+interface SpacingPopupProps {
+ values: string[];
+}
+
+export const SpacingPopup = ({ values }: SpacingPopupProps) => {
+ // Display abbreviated list: first, second, ..., last
+ const firstValue = values[0];
+ const secondValue = values[1];
+ const lastValue = values[values.length - 1];
+
+ return (
+
+
{firstValue}
+
{secondValue}
+
+ ...
+
+
+
+
+
+
+
+ All spacing values
+
+ {values.map(value => (
+ {value}
+ ))}
+
+ Also accepts custom string values
+
+
+
+
{lastValue}
+
+ );
+};
diff --git a/docs-ui/src/components/ReactAriaLink/ReactAriaLink.tsx b/docs-ui/src/components/ReactAriaLink/ReactAriaLink.tsx
new file mode 100644
index 0000000000..37656c482d
--- /dev/null
+++ b/docs-ui/src/components/ReactAriaLink/ReactAriaLink.tsx
@@ -0,0 +1,26 @@
+import styles from './styles.module.css';
+
+interface ReactAriaLinkProps {
+ /** The React Aria component name (e.g., "Cell", "Table") */
+ component: string;
+ /** The documentation URL */
+ href: string;
+}
+
+/**
+ * Displays a standardized note linking to React Aria documentation.
+ *
+ * Usage:
+ *
+ */
+export function ReactAriaLink({ component, href }: ReactAriaLinkProps) {
+ return (
+
+ Inherits all{' '}
+
+ React Aria {component}
+ {' '}
+ props.
+
+ );
+}
diff --git a/docs-ui/src/components/ReactAriaLink/index.ts b/docs-ui/src/components/ReactAriaLink/index.ts
new file mode 100644
index 0000000000..8fa7b70e73
--- /dev/null
+++ b/docs-ui/src/components/ReactAriaLink/index.ts
@@ -0,0 +1 @@
+export { ReactAriaLink } from './ReactAriaLink';
diff --git a/docs-ui/src/components/ReactAriaLink/styles.module.css b/docs-ui/src/components/ReactAriaLink/styles.module.css
new file mode 100644
index 0000000000..b74436fee3
--- /dev/null
+++ b/docs-ui/src/components/ReactAriaLink/styles.module.css
@@ -0,0 +1,15 @@
+.note {
+ font-size: 1rem;
+ color: var(--secondary);
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.note a {
+ color: var(--link);
+ text-decoration: none;
+}
+
+.note a:hover {
+ text-decoration: underline;
+}
diff --git a/docs-ui/src/components/Roadmap/Roadmap.tsx b/docs-ui/src/components/Roadmap/Roadmap.tsx
deleted file mode 100644
index 52f96e0f76..0000000000
--- a/docs-ui/src/components/Roadmap/Roadmap.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { RoadmapItem } from './list';
-
-export const Roadmap = ({ list }: { list: RoadmapItem[] }) => {
- const orderList = ['inProgress', 'notStarted', 'completed'];
- return (
-
- {list
- .sort(
- (a, b) => orderList.indexOf(a.status) - orderList.indexOf(b.status),
- )
- .map(Item)}
-
- );
-};
-
-const Item = ({
- title,
- status = 'notStarted',
-}: {
- title: string;
- status: 'notStarted' | 'inProgress' | 'inReview' | 'completed';
-}) => {
- return (
-
-
-
- {status === 'notStarted' && 'Not Started'}
- {status === 'inProgress' && 'In Progress'}
- {status === 'inReview' && 'Ready for Review'}
- {status === 'completed' && 'Completed'}
-
-
- );
-};
diff --git a/docs-ui/src/components/Roadmap/index.ts b/docs-ui/src/components/Roadmap/index.ts
deleted file mode 100644
index c7006073d6..0000000000
--- a/docs-ui/src/components/Roadmap/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { Roadmap } from './Roadmap';
diff --git a/docs-ui/src/components/Roadmap/list.ts b/docs-ui/src/components/Roadmap/list.ts
deleted file mode 100644
index 24b9197cf5..0000000000
--- a/docs-ui/src/components/Roadmap/list.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-export type RoadmapItem = {
- title: string;
- status: 'notStarted' | 'inProgress' | 'inReview' | 'completed';
-};
-
-export const list: RoadmapItem[] = [
- {
- title: 'Remove Vanilla Extract and use pure CSS instead',
- status: 'inProgress',
- },
- {
- title: 'Add collapsing across breakpoints for the Inline component',
- status: 'notStarted',
- },
- {
- title: 'Add reversing the order for the Inline component',
- status: 'notStarted',
- },
- {
- title: 'Set up Storybook',
- status: 'completed',
- },
- {
- title: 'Set up iconography',
- status: 'completed',
- },
- {
- title: 'Set up global tokens',
- status: 'inProgress',
- },
- {
- title: 'Set up theming system',
- status: 'inProgress',
- },
- {
- title: 'Create first pass at box component',
- status: 'completed',
- },
- {
- title: 'Create first pass at stack component',
- status: 'completed',
- },
- {
- title: 'Create first pass at inline component',
- status: 'completed',
- },
-];
diff --git a/docs-ui/src/components/Roadmap/styles.css b/docs-ui/src/components/Roadmap/styles.css
deleted file mode 100644
index 63f32312e7..0000000000
--- a/docs-ui/src/components/Roadmap/styles.css
+++ /dev/null
@@ -1,100 +0,0 @@
-.roadmap {
- display: flex;
- flex-direction: column;
-}
-
-.roadmap .roadmap-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- border-bottom: 1px solid #e0e0e0;
- padding: 8px 0px;
-}
-
-.roadmap .roadmap-item .left {
- display: flex;
- align-items: center;
- padding-left: 12px;
- gap: 12px;
-}
-
-.roadmap .roadmap-item .dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background-color: #e0e0e0;
-}
-
-.roadmap .roadmap-item.notStarted {
- color: #000;
-}
-
-.roadmap .roadmap-item.inProgress {
- color: #000;
-}
-
-.roadmap .roadmap-item.inReview {
- color: #000;
-}
-
-.roadmap .roadmap-item.completed .title {
- color: #a2a2a2;
- text-decoration: line-through;
-}
-
-.roadmap .roadmap-item.notStarted .dot {
- background-color: #d1d1d1;
-}
-
-.roadmap .roadmap-item.inProgress .dot {
- background-color: #ffd000;
-}
-
-.roadmap .roadmap-item.inReview .dot {
- background-color: #4ed14a;
-}
-
-.roadmap .roadmap-item.completed .dot {
- background-color: #4ed14a;
-}
-
-.roadmap .roadmap-item .title {
- font-size: 16px;
-}
-
-.roadmap .roadmap-item .pill {
- display: inline-flex;
- align-items: center;
- height: 24px;
- padding: 0px 8px;
- border-radius: 40px;
- font-size: 12px;
- font-weight: 600;
- margin-left: 8px;
- border-style: solid;
- border-width: 1px;
-}
-
-.roadmap .roadmap-item.notStarted .pill {
- background-color: #f2f2f2;
- border-color: #cdcdcd;
- color: #888888;
-}
-
-.roadmap .roadmap-item.inProgress .pill {
- background-color: #fff2b9;
- border-color: #ffd000;
- color: #d79927;
-}
-
-.roadmap .roadmap-item.inReview .pill {
- background-color: #d7f9d7;
- border-color: #4ed14a;
- color: #3a9837;
-}
-
-.roadmap .roadmap-item.completed .pill {
- background-color: #d7f9d7;
- border-color: #4ed14a;
- color: #3a9837;
-}
diff --git a/docs-ui/src/components/Snippet/client.tsx b/docs-ui/src/components/Snippet/client.tsx
index 04284c3249..2041218a12 100644
--- a/docs-ui/src/components/Snippet/client.tsx
+++ b/docs-ui/src/components/Snippet/client.tsx
@@ -12,6 +12,7 @@ interface SnippetProps {
py?: number;
open?: boolean;
height?: string | number;
+ layout?: 'stacked' | 'side-by-side';
}
export const SnippetClient = ({
@@ -22,9 +23,28 @@ export const SnippetClient = ({
py = 2,
open = false,
height = 'auto',
+ layout = 'stacked',
}: SnippetProps) => {
const [isOpen, setIsOpen] = useState(open);
+ if (layout === 'side-by-side') {
+ return (
+
+ );
+ }
+
return (
{
return (
);
};
diff --git a/docs-ui/src/components/Snippet/styles.module.css b/docs-ui/src/components/Snippet/styles.module.css
index 3141564293..c6270670a7 100644
--- a/docs-ui/src/components/Snippet/styles.module.css
+++ b/docs-ui/src/components/Snippet/styles.module.css
@@ -7,7 +7,7 @@
.preview {
border-radius: 8px;
box-shadow: inset 0 0 0 1px var(--border);
- background-color: var(--bui-bg);
+ background-color: var(--bui-bg-app);
padding: 1px;
position: relative;
}
@@ -60,3 +60,43 @@
opacity: 1;
}
}
+
+/* Side-by-side layout */
+.sideBySide {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+ align-items: stretch;
+}
+
+.sideBySideCode {
+ min-width: 0;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.codeWrapper {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.codeWrapper > div {
+ flex: 1;
+ margin-bottom: 0;
+}
+
+.sideBySidePreview {
+ border-radius: 8px;
+ box-shadow: inset 0 0 0 1px var(--border);
+ background-color: var(--bui-bg-app);
+ padding: 1px;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.sideBySidePreview > .previewContent {
+ flex: 1;
+}
diff --git a/docs-ui/src/components/Table/Table.tsx b/docs-ui/src/components/Table/Table.tsx
index 4d0e48dbce..a43bcd766a 100644
--- a/docs-ui/src/components/Table/Table.tsx
+++ b/docs-ui/src/components/Table/Table.tsx
@@ -45,12 +45,14 @@ export const Row = ({ children }: { children: ReactNode }) => {
export const Cell = ({
children,
style,
+ colSpan,
}: {
children: ReactNode;
style?: CSSProperties;
+ colSpan?: number;
}) => {
return (
-
+
{children}
);
diff --git a/docs-ui/src/components/TableOfContents/TableOfContents.tsx b/docs-ui/src/components/TableOfContents/TableOfContents.tsx
index d18265415b..429d677b80 100644
--- a/docs-ui/src/components/TableOfContents/TableOfContents.tsx
+++ b/docs-ui/src/components/TableOfContents/TableOfContents.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useLayoutEffect } from 'react';
import { usePathname } from 'next/navigation';
import styles from './TableOfContents.module.css';
@@ -17,6 +17,29 @@ export function TableOfContents() {
const [indicatorHeight, setIndicatorHeight] = useState(0);
const pathname = usePathname();
+ // Update indicator position when activeId changes
+ useLayoutEffect(() => {
+ if (!activeId) return;
+
+ // Use requestAnimationFrame to defer setState call
+ const rafId = requestAnimationFrame(() => {
+ const activeElement = document.querySelector(
+ `[data-toc-id="${activeId}"]`,
+ ) as HTMLElement;
+ if (activeElement) {
+ const list = activeElement.closest('ul');
+ if (list) {
+ const listRect = list.getBoundingClientRect();
+ const elementRect = activeElement.getBoundingClientRect();
+ setIndicatorTop(elementRect.top - listRect.top);
+ setIndicatorHeight(elementRect.height);
+ }
+ }
+ });
+
+ return () => cancelAnimationFrame(rafId);
+ }, [activeId, headings]);
+
useEffect(() => {
// Extract all H2 and H3 headings from the document
const elements = Array.from(
@@ -29,18 +52,6 @@ export function TableOfContents() {
level: parseInt(element.tagName.substring(1)),
}));
- setHeadings(headingData);
-
- // Set initial active heading (first visible heading or first heading)
- if (headingData.length > 0) {
- const viewportTop = window.scrollY + 100; // offset for header
- const visibleHeading = elements.find(element => {
- const rect = element.getBoundingClientRect();
- return rect.top + window.scrollY >= viewportTop - 200;
- });
- setActiveId(visibleHeading?.id || headingData[0].id);
- }
-
// Set up IntersectionObserver to track visible headings
const observerOptions = {
rootMargin: '-80px 0px -80% 0px',
@@ -62,29 +73,26 @@ export function TableOfContents() {
elements.forEach(element => observer.observe(element));
+ // Initialize headings and active ID after observer is set up
+ requestAnimationFrame(() => {
+ setHeadings(headingData);
+
+ // Set initial active heading (first visible heading or first heading)
+ if (headingData.length > 0) {
+ const viewportTop = window.scrollY + 100; // offset for header
+ const visibleHeading = elements.find(element => {
+ const rect = element.getBoundingClientRect();
+ return rect.top + window.scrollY >= viewportTop - 200;
+ });
+ setActiveId(visibleHeading?.id || headingData[0].id);
+ }
+ });
+
return () => {
elements.forEach(element => observer.unobserve(element));
};
}, [pathname]);
- // Update indicator position when activeId changes
- useEffect(() => {
- if (activeId) {
- const activeElement = document.querySelector(
- `[data-toc-id="${activeId}"]`,
- ) as HTMLElement;
- if (activeElement) {
- const list = activeElement.closest('ul');
- if (list) {
- const listRect = list.getBoundingClientRect();
- const elementRect = activeElement.getBoundingClientRect();
- setIndicatorTop(elementRect.top - listRect.top);
- setIndicatorHeight(elementRect.height);
- }
- }
- }
- }, [activeId, headings]);
-
const handleClick = (id: string) => {
const element = document.getElementById(id);
if (element) {
diff --git a/docs-ui/src/components/Theming/index.tsx b/docs-ui/src/components/Theming/index.tsx
index d06f51348b..9f67c3a111 100644
--- a/docs-ui/src/components/Theming/index.tsx
+++ b/docs-ui/src/components/Theming/index.tsx
@@ -1,11 +1,16 @@
-import { MDXRemote } from 'next-mdx-remote-client/rsc';
-import { formattedMDXComponents } from '@/mdx-components';
-import type {
- ComponentDefinition,
- DataAttributeValues,
-} from '../../../../packages/ui/src/types';
+'use client';
-export function Theming({ definition }: { definition: ComponentDefinition }) {
+import { formattedMDXComponents } from '@/mdx-components';
+import type { DataAttributeValues } from '../../../../packages/ui/src/types';
+
+interface ThemingProps {
+ definition: {
+ classNames: Record;
+ dataAttributes?: Record;
+ };
+}
+
+export function Theming({ definition }: ThemingProps) {
const classNames = definition.classNames;
const dataAttributes = definition.dataAttributes;
@@ -33,15 +38,28 @@ export function Theming({ definition }: { definition: ComponentDefinition }) {
...Object.values(classNames).slice(1),
];
+ // Use the same styled components from MDX, with fallbacks to HTML elements
+ const H2 = formattedMDXComponents.h2 || 'h2';
+ const P = formattedMDXComponents.p || 'p';
+ const Ul = formattedMDXComponents.ul || 'ul';
+ const Li = formattedMDXComponents.li || 'li';
+ const Code = formattedMDXComponents.code || 'code';
+
return (
- `- \`${selector}\``).join('\n')}
- `}
- />
+
+
Theming
+
+ Our theming system is based on a mix between CSS classes, CSS variables
+ and data attributes. If you want to customise this component, you can
+ use one of these class names below.
+
+
+ {classNamesArray.map((selector, index) => (
+
+ {selector}
+
+ ))}
+
+
);
}
diff --git a/docs-ui/src/components/Toolbar/Toolbar.module.css b/docs-ui/src/components/Toolbar/Toolbar.module.css
index 581fc599b3..4f067bd7bd 100644
--- a/docs-ui/src/components/Toolbar/Toolbar.module.css
+++ b/docs-ui/src/components/Toolbar/Toolbar.module.css
@@ -10,12 +10,10 @@
}
}
-.breadcrumb {
+.left {
display: flex;
align-items: center;
gap: 0.5rem;
- font-size: 0.875rem;
- font-weight: 500;
}
.logoMobile {
@@ -26,39 +24,6 @@
}
}
-.breadcrumbDesktop {
- display: none;
-
- @media (min-width: 768px) {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
-}
-
-.breadcrumbLink {
- color: var(--secondary);
- text-decoration: none;
- cursor: pointer;
-
- &:hover {
- color: var(--primary);
- text-decoration: underline;
- transition: color 0.2s ease-in-out;
- text-decoration-thickness: 1px;
- text-underline-offset: 4px;
- }
-}
-
-.breadcrumbSeparator {
- color: var(--secondary);
- flex-shrink: 0;
-}
-
-.breadcrumbCurrent {
- color: var(--primary);
-}
-
.actions {
display: none;
@@ -119,6 +84,51 @@
}
}
+.searchButton {
+ display: none;
+ align-items: center;
+ gap: 6px;
+ background-color: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 32px;
+ padding-inline: 16px 12px;
+ min-width: 180px;
+ color: var(--secondary);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ height: 32px;
+ cursor: pointer;
+ font-family: inherit;
+ white-space: nowrap;
+
+ &:hover {
+ background-color: var(--action);
+ color: var(--primary);
+ transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
+ }
+
+ @media (min-width: 768px) {
+ display: flex;
+ }
+}
+
+.searchLabel {
+ display: inline;
+ flex: 1;
+ text-align: left;
+}
+
+.searchKbd {
+ display: inline;
+ font-family: inherit;
+ font-size: 0.6875rem;
+ font-weight: 500;
+ background-color: var(--action);
+ border-radius: 4px;
+ padding: 2px 5px;
+ color: var(--secondary);
+}
+
.buttonGroup {
display: flex;
align-items: center;
diff --git a/docs-ui/src/components/Toolbar/Toolbar.tsx b/docs-ui/src/components/Toolbar/Toolbar.tsx
index e7defd5e8a..4a071bf45e 100644
--- a/docs-ui/src/components/Toolbar/Toolbar.tsx
+++ b/docs-ui/src/components/Toolbar/Toolbar.tsx
@@ -1,10 +1,11 @@
'use client';
+import { useState, useEffect } from 'react';
import {
RiArrowDownSLine,
- RiArrowRightSLine,
RiGithubLine,
RiMoonLine,
+ RiSearchLine,
RiSunLine,
} from '@remixicon/react';
import {
@@ -19,10 +20,8 @@ import {
} from 'react-aria-components';
import styles from './Toolbar.module.css';
import { usePlayground } from '@/utils/playground-context';
-import { usePathname } from 'next/navigation';
-import Link from 'next/link';
-import { components, layoutComponents } from '@/utils/data';
import { Logo } from '@/components/Sidebar/Logo';
+import { CommandPalette } from '@/components/CommandPalette';
interface ToolbarProps {
version: string;
@@ -42,75 +41,36 @@ export const Toolbar = ({ version }: ToolbarProps) => {
setSelectedThemeName,
} = usePlayground();
- const pathname = usePathname();
+ const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
- // Determine breadcrumb content based on current path
- const getBreadcrumb = () => {
- const allComponents = [...components, ...layoutComponents];
+ useEffect(() => {
+ const isMac = /mac(os|intosh)/i.test(navigator.userAgent);
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey)) {
+ e.preventDefault();
+ setIsCommandPaletteOpen(prev => !prev);
+ }
+ };
- // Root page
- if (pathname === '/') {
- return { section: null, title: 'Getting Started' };
- }
-
- // Components index page
- if (pathname === '/components') {
- return { section: null, title: 'Components' };
- }
-
- // Component detail pages
- if (pathname?.startsWith('/components/')) {
- const slug = pathname.split('/components/')[1];
- const component = allComponents.find(c => c.slug === slug);
- return {
- section: 'Components',
- sectionLink: '/components',
- title: component?.title || slug,
- };
- }
-
- // Tokens page
- if (pathname === '/tokens') {
- return { section: null, title: 'Tokens' };
- }
-
- // Changelog page
- if (pathname === '/changelog') {
- return { section: null, title: 'Changelog' };
- }
-
- return { section: null, title: '' };
- };
-
- const breadcrumb = getBreadcrumb();
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, []);
return (
-
+
-
- {breadcrumb.section && breadcrumb.sectionLink ? (
- <>
-
- {breadcrumb.section}
-
-
-
- {breadcrumb.title}
-
- >
- ) : (
- {breadcrumb.title}
- )}
-
+
setIsCommandPaletteOpen(true)}
+ >
+
+ Search
+ ⌘K
+
{
+
);
};
diff --git a/docs-ui/src/content/accordion.props.ts b/docs-ui/src/content/accordion.props.ts
deleted file mode 100644
index aac983b0c3..0000000000
--- a/docs-ui/src/content/accordion.props.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const accordionPropDefs: Record
= {
- children: {
- type: 'enum',
- values: ['ReactNode', '(state: { isExpanded: boolean }) => ReactNode'],
- },
- defaultExpanded: {
- type: 'boolean',
- default: 'false',
- },
- isExpanded: {
- type: 'boolean',
- },
- onExpandedChange: {
- type: 'enum',
- values: ['(isExpanded: boolean) => void'],
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const accordionTriggerPropDefs: Record = {
- level: {
- type: 'enum',
- values: ['1', '2', '3', '4', '5', '6'],
- default: '3',
- },
- title: {
- type: 'string',
- },
- subtitle: {
- type: 'string',
- },
- children: {
- type: 'enum',
- values: ['ReactNode'],
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const accordionPanelPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const accordionGroupPropDefs: Record = {
- allowsMultiple: {
- type: 'boolean',
- default: 'false',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const accordionUsageSnippet = `import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
-
-
-
- Your content
- `;
-
-export const accordionWithSubtitleSnippet = `
-
-
- Your content here
-
- `;
-
-export const accordionCustomTriggerSnippet = `
-
-
- Custom Multi-line Trigger
-
- Click to expand additional details
-
-
-
-
- Your content here
-
- `;
-
-export const accordionDefaultExpandedSnippet = `
-
-
- Your content here
-
- `;
-
-export const accordionGroupSingleOpenSnippet = `
-
-
- Content 1
-
-
-
- Content 2
-
- `;
-
-export const accordionGroupMultipleOpenSnippet = `
-
-
- Content 1
-
-
-
- Content 2
-
- `;
diff --git a/docs-ui/src/content/avatar.props.ts b/docs-ui/src/content/avatar.props.ts
deleted file mode 100644
index 80621cdb89..0000000000
--- a/docs-ui/src/content/avatar.props.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
-import type { PropDef } from '@/utils/propDefs';
-
-export const avatarPropDefs: Record = {
- src: {
- type: 'string',
- },
- name: {
- type: 'string',
- },
- size: {
- type: 'enum',
- values: ['x-small', 'small', 'medium', 'large', 'x-large'],
- default: 'medium',
- responsive: true,
- },
- purpose: {
- type: 'enum',
- values: ['informative', 'decoration'],
- default: 'informative',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const snippetUsage = `import { Avatar } from '@backstage/ui';
-
- `;
-
-export const snippetSizes = `
-
-
-
-
-
- `;
-
-export const snippetFallback = ` `;
-
-export const snippetPurpose = `
-
- Informative (default)
-
- Use when avatar appears alone. Announced as "Charles de Dreuille" to screen readers:
-
-
-
-
-
-
- Decoration
-
- Use when name appears adjacent to avatar. Hidden from screen readers to avoid redundancy:
-
-
-
- Charles de Dreuille
-
-
- `;
diff --git a/docs-ui/src/content/box.mdx b/docs-ui/src/content/box.mdx
deleted file mode 100644
index 66e99ec19a..0000000000
--- a/docs-ui/src/content/box.mdx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { CodeBlock } from '@/components/CodeBlock';
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { BoxSnippet } from '@/snippets/stories-snippets';
-import {
- boxPropDefs,
- snippetUsage,
- boxPreviewSnippet,
- boxSimpleSnippet,
- boxResponsiveSnippet,
-} from './box.props';
-import { spacingPropDefs } from '@/utils/propDefs';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { BoxDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={boxPreviewSnippet}
- align="center"
-/>
-
-## Usage
-
-
-
-## API reference
-
-### Box
-
-This is the Box component, our lowest-level component. Here are all the
-available properties.
-
-
-
-Padding and margin are used to create space around your component using our
-predefined spacing tokens. We would recommend to use padding over margin to
-avoid collapsing margins but both are available.
-
-
-
-## Examples
-
-### Simple example
-
-A simple example of how to use the Box component.
-
-
-
-### Responsive
-
-Here's a view when buttons are responsive.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/box.props.ts b/docs-ui/src/content/box.props.ts
deleted file mode 100644
index 0ee45c80f4..0000000000
--- a/docs-ui/src/content/box.props.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import {
- classNamePropDefs,
- displayPropDefs,
- heightPropDefs,
- positionPropDefs,
- stylePropDefs,
- widthPropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const boxPropDefs: Record = {
- as: {
- type: 'enum',
- values: ['div', 'span'],
- default: 'div',
- responsive: true,
- },
- ...widthPropDefs,
- ...heightPropDefs,
- ...positionPropDefs,
- ...displayPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const snippetUsage = `import { Box } from '@backstage/ui';
-
- `;
-
-export const boxPreviewSnippet = `
-
- `;
-
-export const boxSimpleSnippet = `Hello World `;
-
-export const boxResponsiveSnippet = `
- Hello World
- `;
diff --git a/docs-ui/src/content/button-icon.mdx b/docs-ui/src/content/button-icon.mdx
deleted file mode 100644
index b0595cc9bc..0000000000
--- a/docs-ui/src/content/button-icon.mdx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { ButtonIconSnippet } from '@/snippets/stories-snippets';
-import {
- buttonIconPropDefs,
- buttonIconUsageSnippet,
- buttonIconDefaultSnippet,
- buttonIconVariantsSnippet,
- buttonIconSizesSnippet,
- buttonIconDisabledSnippet,
- buttonIconLoadingSnippet,
- buttonIconResponsiveSnippet,
- buttonIconAsLinkSnippet,
-} from './button-icon.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ButtonIconDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={buttonIconDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Variants
-
-Here's a view when buttons have different variants.
-
- }
- code={buttonIconVariantsSnippet}
-/>
-
-### Sizes
-
-Here's a view when buttons have different sizes.
-
- }
- code={buttonIconSizesSnippet}
-/>
-
-### Disabled
-
-Here's a view when buttons are disabled.
-
- }
- code={buttonIconDisabledSnippet}
-/>
-
-### Loading
-
-Here's a view when buttons are in a loading state.
-
- }
- code={buttonIconLoadingSnippet}
-/>
-
-### Responsive
-
-Here's a view when buttons are responsive.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/button-icon.props.ts b/docs-ui/src/content/button-icon.props.ts
deleted file mode 100644
index b063484d27..0000000000
--- a/docs-ui/src/content/button-icon.props.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const buttonIconPropDefs: Record = {
- variant: {
- type: 'enum',
- values: ['primary', 'secondary'],
- default: 'primary',
- responsive: true,
- },
- size: {
- type: 'enum',
- values: ['small', 'medium'],
- default: 'medium',
- responsive: true,
- },
- icon: { type: 'enum', values: ['ReactNode'], responsive: false },
- isDisabled: { type: 'boolean', default: 'false', responsive: false },
- loading: { type: 'boolean', default: 'false', responsive: false },
- type: {
- type: 'enum',
- values: ['button', 'submit', 'reset'],
- default: 'button',
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/ui';
-
- `;
-
-export const buttonIconDefaultSnippet = `
- } variant="primary" />
- } variant="secondary" />
- `;
-
-export const buttonIconVariantsSnippet = `
- } variant="primary" />
- } variant="secondary" />
- `;
-
-export const buttonIconSizesSnippet = `
- } size="small" />
- } size="medium" />
- `;
-
-export const buttonIconDisabledSnippet = ` } isDisabled />`;
-
-export const buttonIconLoadingSnippet = ` } variant="primary" loading={isLoading} onPress={handleClick} />`;
-
-export const buttonIconResponsiveSnippet = ` } variant={{ initial: 'primary', lg: 'secondary' }} />`;
-
-export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/ui';
-
-
- Button
- `;
diff --git a/docs-ui/src/content/button-link.mdx b/docs-ui/src/content/button-link.mdx
deleted file mode 100644
index 4f3f376aee..0000000000
--- a/docs-ui/src/content/button-link.mdx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { ButtonLinkSnippet } from '@/snippets/stories-snippets';
-import {
- buttonLinkPropDefs,
- buttonLinkSnippetUsage,
- buttonLinkVariantsSnippet,
- buttonLinkSizesSnippet,
- buttonLinkIconsSnippet,
- buttonLinkDisabledSnippet,
- buttonLinkResponsiveSnippet,
-} from './button-link.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ButtonLinkDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={buttonLinkVariantsSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Variants
-
-Here's a view when buttons have different variants.
-
- }
- code={buttonLinkVariantsSnippet}
-/>
-
-### Sizes
-
-Here's a view when buttons have different sizes.
-
- }
- code={buttonLinkSizesSnippet}
-/>
-
-### With Icons
-
-Here's a view when buttons have icons.
-
- }
- code={buttonLinkIconsSnippet}
-/>
-
-### Disabled
-
-Here's a view when buttons are disabled.
-
- }
- code={buttonLinkDisabledSnippet}
-/>
-
-### Responsive
-
-Here's a view when buttons are responsive.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/button-link.props.ts b/docs-ui/src/content/button-link.props.ts
deleted file mode 100644
index 2bd4e2196a..0000000000
--- a/docs-ui/src/content/button-link.props.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
-import type { PropDef } from '@/utils/propDefs';
-
-export const buttonLinkPropDefs: Record = {
- variant: {
- type: 'enum',
- values: ['primary', 'secondary'],
- default: 'primary',
- responsive: true,
- },
- size: {
- type: 'enum',
- values: ['small', 'medium'],
- default: 'medium',
- responsive: true,
- },
- iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
- iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
- isDisabled: { type: 'boolean', default: 'false', responsive: false },
- href: { type: 'string', responsive: false },
- hrefLang: { type: 'string', responsive: false },
- target: {
- type: 'enum',
- values: ['HTMLAttributeAnchorTarget'],
- default: '_self',
- responsive: false,
- },
- rel: { type: 'string', responsive: false },
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/ui';
-
- `;
-
-export const buttonLinkVariantsSnippet = `
- } variant="primary">
- Button
-
- } variant="secondary">
- Button
-
- `;
-
-export const buttonLinkSizesSnippet = `
- Small
- Medium
- `;
-
-export const buttonLinkIconsSnippet = `
- }>Button
- }>Button
- }
- iconEnd={ }>
- Button
-
- `;
-
-export const buttonLinkDisabledSnippet = `
-
- Primary
-
-
- Secondary
-
- `;
-
-export const buttonLinkResponsiveSnippet = `
- Responsive Button
- `;
diff --git a/docs-ui/src/content/button.mdx b/docs-ui/src/content/button.mdx
deleted file mode 100644
index d161c97edf..0000000000
--- a/docs-ui/src/content/button.mdx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { ButtonSnippet, ButtonLinkSnippet } from '@/snippets/stories-snippets';
-import {
- buttonPropDefs,
- buttonSnippetUsage,
- buttonVariantsSnippet,
- buttonSizesSnippet,
- buttonIconsSnippet,
- buttonDisabledSnippet,
- buttonLoadingSnippet,
- buttonResponsiveSnippet,
- buttonAsLinkSnippet,
-} from './button.props';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ButtonDefinition } from '../utils/definitions';
-
-
-
- }
- code={buttonVariantsSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Variants
-
-Here's a view when buttons have different variants.
-
- }
- code={buttonVariantsSnippet}
-/>
-
-### Sizes
-
-Here's a view when buttons have different sizes.
-
- }
- code={buttonSizesSnippet}
-/>
-
-### With Icons
-
-Here's a view when buttons have icons.
-
- }
- code={buttonIconsSnippet}
-/>
-
-### Disabled
-
-Here's a view when buttons are disabled.
-
- }
- code={buttonDisabledSnippet}
-/>
-
-### Loading
-
-Here's a view when buttons are in a loading state.
-
- }
- code={buttonLoadingSnippet}
-/>
-
-### Responsive
-
-Here's a view when buttons are responsive.
-
-
-
-### As Link
-
-If you want to use a button as a link, please use the `ButtonLink` component.
-
- }
- code={buttonAsLinkSnippet}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/button.props.ts b/docs-ui/src/content/button.props.ts
deleted file mode 100644
index e650a9f2e9..0000000000
--- a/docs-ui/src/content/button.props.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
-import type { PropDef } from '@/utils/propDefs';
-
-export const buttonPropDefs: Record = {
- variant: {
- type: 'enum',
- values: ['primary', 'secondary'],
- default: 'primary',
- responsive: true,
- },
- size: {
- type: 'enum',
- values: ['small', 'medium'],
- default: 'medium',
- responsive: true,
- },
- iconStart: { type: 'enum', values: ['ReactNode'], responsive: false },
- iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false },
- isDisabled: { type: 'boolean', default: 'false', responsive: false },
- loading: { type: 'boolean', default: 'false', responsive: false },
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- type: {
- type: 'enum',
- values: ['button', 'submit', 'reset'],
- default: 'button',
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const buttonSnippetUsage = `import { Button } from '@backstage/ui';
-
- `;
-
-export const buttonVariantsSnippet = `
-
- Button
-
-
- Button
-
- `;
-
-export const buttonSizesSnippet = `
- Small
- Medium
- `;
-
-export const buttonIconsSnippet = `
- }>Button
- }>Button
- } iconEnd={ }>Button
- `;
-
-export const buttonDisabledSnippet = `
-
- Primary
-
-
- Secondary
-
- `;
-
-export const buttonResponsiveSnippet = `
- Responsive Button
- `;
-
-export const buttonLoadingSnippet = `
- Load more items
- `;
-
-export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/ui';
-
-
- Button
- `;
diff --git a/docs-ui/src/content/card.mdx b/docs-ui/src/content/card.mdx
deleted file mode 100644
index c38a07e920..0000000000
--- a/docs-ui/src/content/card.mdx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { CardSnippet } from '@/snippets/stories-snippets';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import {
- cardPropDefs,
- cardUsageSnippet,
- cardDefaultSnippet,
- cardHeaderPropDefs,
- cardBodyPropDefs,
- cardFooterPropDefs,
- cardLongBodySnippet,
- cardListRowSnippet,
-} from './card.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { CardDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={cardDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-### Card
-
-A card component that can be used to display content in a box.
-
-
-
-### CardHeader
-
-To display a header in a card, use the `CardHeader` component. This will be fixed at the top of the card.
-
-
-
-### CardBody
-
-To display content in a card, use the `CardBody` component. This will automatically fill the card.
-
-
-
-### CardFooter
-
-To display a footer in a card, use the `CardFooter` component. This will be fixed at the bottom of the card.
-
-
-
-## Examples
-
-### With long body
-
-Here's a view when card has a long body.
-
- }
- code={cardLongBodySnippet}
- open
-/>
-
-### With list
-
-Here's a view when card has a list.
-
- }
- code={cardListRowSnippet}
- open
-/>
-
-
-
-
diff --git a/docs-ui/src/content/checkbox.props.ts b/docs-ui/src/content/checkbox.props.ts
deleted file mode 100644
index ce0f1033ff..0000000000
--- a/docs-ui/src/content/checkbox.props.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const checkboxPropDefs: Record = {
- children: {
- type: 'enum',
- values: ['React.ReactNode'],
- responsive: false,
- },
- isSelected: {
- type: 'enum',
- values: ['boolean'],
- responsive: false,
- },
- defaultSelected: {
- type: 'enum',
- values: ['boolean'],
- responsive: false,
- },
- onChange: {
- type: 'enum',
- values: ['(isSelected: boolean) => void'],
- responsive: false,
- },
- isDisabled: {
- type: 'enum',
- values: ['boolean'],
- responsive: false,
- },
- isRequired: {
- type: 'enum',
- values: ['boolean'],
- responsive: false,
- },
- name: {
- type: 'string',
- responsive: false,
- },
- value: {
- type: 'string',
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const checkboxUsageSnippet = `import { Checkbox } from '@backstage/ui';
-
-Accept terms `;
-
-export const checkboxDefaultSnippet = `Accept terms and conditions `;
-
-export const checkboxVariantsSnippet = `
- Unchecked
- Checked
- Disabled
- Checked & Disabled
- `;
diff --git a/docs-ui/src/content/container.mdx b/docs-ui/src/content/container.mdx
deleted file mode 100644
index 56c2d5d405..0000000000
--- a/docs-ui/src/content/container.mdx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { CodeBlock } from '@/components/CodeBlock';
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { ContainerSnippet } from '@/snippets/stories-snippets';
-import {
- containerPropDefs,
- containerUsageSnippet,
- containerDefaultSnippet,
- containerSimpleSnippet,
- containerResponsiveSnippet,
-} from './container.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ContainerDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={containerDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Simple
-
-A simple example of how to use the Container component.
-
-
-
-### Responsive padding & margin
-
-The Container component also supports responsive values, making it easy to
-create responsive designs.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/container.props.ts b/docs-ui/src/content/container.props.ts
deleted file mode 100644
index a4d23a8495..0000000000
--- a/docs-ui/src/content/container.props.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- gapPropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const containerPropDefs: Record = {
- ...gapPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const containerUsageSnippet = `import { Container } from "@backstage/ui";
-
-Hello World! `;
-
-export const containerDefaultSnippet = `
-
- `;
-
-export const containerSimpleSnippet = `
- Hello World
- Hello World
- Hello World
- `;
-
-export const containerResponsiveSnippet = `
- Hello World
- Hello World
- Hello World
- `;
diff --git a/docs-ui/src/content/dialog.props.ts b/docs-ui/src/content/dialog.props.ts
deleted file mode 100644
index 162ec307dc..0000000000
--- a/docs-ui/src/content/dialog.props.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const dialogTriggerPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- isOpen: {
- type: 'boolean',
- description: 'Whether the overlay is open by default (controlled).',
- },
- defaultOpen: {
- type: 'boolean',
- description: 'Whether the overlay is open by default (uncontrolled).',
- },
- onOpenChange: {
- type: 'enum',
- values: ['(isOpen: boolean) => void'],
- description:
- "Handler that is called when the overlay's open state changes.",
- },
-};
-
-export const dialogPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- isOpen: {
- type: 'boolean',
- description: 'Whether the overlay is open by default (controlled).',
- },
- defaultOpen: {
- type: 'boolean',
- description: 'Whether the overlay is open by default (uncontrolled).',
- },
- onOpenChange: {
- type: 'enum',
- values: ['(isOpen: boolean) => void'],
- description:
- "Handler that is called when the overlay's open state changes.",
- },
- width: {
- type: 'enum',
- values: ['number', 'string'],
- responsive: false,
- },
- height: {
- type: 'enum',
- values: ['number', 'string'],
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogHeaderPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogBodyPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- height: {
- type: 'enum',
- values: ['number', 'string'],
- responsive: false,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogFooterPropDefs: Record = {
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogClosePropDefs: Record = {
- variant: {
- type: 'enum',
- values: ['primary', 'secondary', 'tertiary'],
- default: 'secondary',
- responsive: false,
- },
- children: { type: 'enum', values: ['ReactNode'], responsive: false },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const dialogUsageSnippet = `import {
- Dialog,
- DialogTrigger,
- DialogHeader,
- DialogBody,
- DialogFooter,
-} from '@backstage/ui';
-
-
- Open Dialog
-
- Title
- Content
-
- Close
-
-
- `;
-
-export const dialogDefaultSnippet = `
- Open Dialog
-
- Example Dialog
-
- This is a basic dialog example.
-
-
- Close
- Save
-
-
- `;
-
-export const dialogFixedWidthAndHeightSnippet = `
- Scrollable Dialog
-
- Long Content Dialog
-
- ...
-
-
- Cancel
- Accept
-
-
- `;
-
-export const dialogWithFormSnippet = `
- Create User
-
- Create New User
-
-
-
-
-
- Admin
- User
- Viewer
-
-
-
-
- Cancel
- Create User
-
-
- `;
-
-export const dialogWithNoTriggerSnippet = `const [isOpen, setIsOpen] = useState(false);
-
-
- Create New User
-
- Your content
-
-
- Cancel
- Create User
-
- `;
-
-export const dialogCloseSnippet = `Close `;
diff --git a/docs-ui/src/content/flex.mdx b/docs-ui/src/content/flex.mdx
deleted file mode 100644
index 36284f1f0b..0000000000
--- a/docs-ui/src/content/flex.mdx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { CodeBlock } from '@/components/CodeBlock';
-import { Snippet } from '@/components/Snippet';
-import { FlexSnippet } from '@/snippets/stories-snippets';
-import {
- flexPropDefs,
- flexUsageSnippet,
- flexDefaultSnippet,
- flexFAQ1Snippet,
- flexSimpleSnippet,
- flexResponsiveSnippet,
- flexAlignSnippet,
-} from './flex.props';
-import { spacingPropDefs } from '@/utils/propDefs';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { FlexDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={flexDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-The grid component also accepts all the spacing props from the Box component.
-
-
-
-## Examples
-
-### Simple
-
-A simple example of how to use the Flex component.
-
-
-
-### Responsive
-
-The Flex component also supports responsive values, making it easy to create
-responsive designs.
-
-
-
-### Align
-
-The Flex component also supports responsive alignment, making it easy to
-create responsive designs.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/flex.props.ts b/docs-ui/src/content/flex.props.ts
deleted file mode 100644
index cc4c2d5a75..0000000000
--- a/docs-ui/src/content/flex.props.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- childrenPropDefs,
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const flexPropDefs: Record = {
- align: {
- type: 'enum',
- values: ['start', 'center', 'end', 'baseline', 'stretch'],
- responsive: true,
- },
- direction: {
- type: 'enum',
- values: ['row', 'column', 'row-reverse', 'column-reverse'],
- responsive: true,
- },
- justify: {
- type: 'enum',
- values: ['start', 'center', 'end', 'between'],
- responsive: true,
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const flexUsageSnippet = `import { Flex } from '@backstage/ui';
-
- `;
-
-export const flexDefaultSnippet = `
-
-
-
- `;
-
-export const flexSimpleSnippet = `
- Hello World
- Hello World
- Hello World
- `;
-
-export const flexResponsiveSnippet = `
- Hello World
- Hello World
- Hello World
- `;
-
-export const flexAlignSnippet = `
- Hello World
- Hello World
- Hello World
- `;
diff --git a/docs-ui/src/content/grid.mdx b/docs-ui/src/content/grid.mdx
deleted file mode 100644
index 5f179b9e14..0000000000
--- a/docs-ui/src/content/grid.mdx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { CodeBlock } from '@/components/CodeBlock';
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { GridSnippet } from '@/snippets/stories-snippets';
-import { spacingPropDefs } from '@/utils/propDefs';
-import {
- gridPropDefs,
- gridItemPropDefs,
- gridUsageSnippet,
- gridDefaultSnippet,
- gridSimpleSnippet,
- gridComplexSnippet,
- gridMixingRowsSnippet,
- gridResponsiveSnippet,
- gridStartEndSnippet,
-} from './grid.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { GridDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={gridDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-### Grid.Root
-
-This is the grid container component. It will help to define the number of
-columns that will be used in the grid. You can also define the gap between the
-columns. All values are responsive.
-
-
-
-The grid component also accepts all the spacing props from the Box component.
-
-
-
-### Grid.Item
-
-If you need more control over the columns, you can use the grid item
-component. This will give you access to `rowSpan`, `colSpan`, `start` and
-`end`. All values are responsive. This component is optional, you can use any
-elements directly if you prefer.
-
-
-
-## Examples
-
-### Simple grid
-
-A simple grid with 3 columns and a gap of md.
-
-
-
-### Complex grid
-
-You can also use the grid item to create more complex layouts. In this example
-the first column will span 1 column and the second column will span 2 columns.
-
-
-
-### Mixing rows and columns
-
-The grid item component also supports the `rowSpan` prop, which allows you to
-span multiple rows within the grid layout. In this example, the first item
-will span 2 rows to achieve a dynamic and flexible grid structure.
-
-
-
-### Responsive grid
-
-The grid component also supports responsive values, making it easy to create
-responsive designs.
-
-
-
-### Start and End
-
-The start and end props can be used to position the item in the grid.
-
-
-
-
-
-
diff --git a/docs-ui/src/content/grid.props.ts b/docs-ui/src/content/grid.props.ts
deleted file mode 100644
index 6f037cd64f..0000000000
--- a/docs-ui/src/content/grid.props.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import {
- childrenPropDefs,
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-const columnsValues = [
- '1',
- '2',
- '3',
- '4',
- '5',
- '6',
- '7',
- '8',
- '9',
- '10',
- '11',
- '12',
-];
-
-export const gridPropDefs: Record = {
- columns: {
- type: 'enum | string',
- values: [...columnsValues, 'auto'],
- responsive: true,
- default: 'auto',
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const gridItemPropDefs: Record = {
- colSpan: {
- type: 'enum | string',
- values: [...columnsValues, 'full'],
- responsive: true,
- },
- rowSpan: {
- type: 'enum | string',
- values: [...columnsValues, 'full'],
- responsive: true,
- },
- colStart: {
- type: 'enum | string',
- values: [...columnsValues, 'auto'],
- responsive: true,
- },
- colEnd: {
- type: 'enum | string',
- values: [...columnsValues, 'auto'],
- responsive: true,
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const gridUsageSnippet = `import { Grid } from '@backstage/ui';
-
- `;
-
-export const gridDefaultSnippet = `
-
-
-
- `;
-
-export const gridSimpleSnippet = `
- Hello World
- Hello World
- Hello World
- `;
-
-export const gridComplexSnippet = `
-
- Hello World
-
-
- Hello World
-
- `;
-
-export const gridMixingRowsSnippet = `
-
- Hello World
-
-
- Hello World
-
-
- Hello World
-
- `;
-
-export const gridResponsiveSnippet = `
-
-
-
- Hello World
-
- `;
-
-export const gridStartEndSnippet = `
-
- Hello World
-
- `;
diff --git a/docs-ui/src/content/header-page.mdx b/docs-ui/src/content/header-page.mdx
deleted file mode 100644
index 12ef2c32ee..0000000000
--- a/docs-ui/src/content/header-page.mdx
+++ /dev/null
@@ -1,88 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { CodeBlock } from '@/components/CodeBlock';
-import { Snippet } from '@/components/Snippet';
-import { HeaderPageSnippet } from '@/snippets/stories-snippets';
-import {
- propDefs,
- usage,
- simple,
- defaultSnippet,
- withTabs,
- withBreadcrumbs,
- withCustomActions,
- withMenuItems,
-} from './header-page.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { HeaderPageDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={defaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### With Breadcrumbs
-
-You can add breadcrumbs to the header page to help users navigate to the previous page. The `breadcrumbs`
-prop is an array of objects with a `label` and `href` property. By default we truncate the breadcrumb label to 240px.
-
- }
- code={withBreadcrumbs}
-/>
-
-### With Tabs
-
-You can add tabs to the header page to help users navigate to the different sections of the page. The `tabs`
-prop is an array of objects with a `label` and `href` property.
-
- }
- 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.
-
- }
- code={withCustomActions}
-/>
-
-### With Menu Items
-
-You can add menu items to the header page to help users navigate to the different sections of the page. The `menuItems`
-prop is an array of objects with a `label`, `value` and `onClick` property.
-
- }
- code={withMenuItems}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/header-page.props.ts b/docs-ui/src/content/header-page.props.ts
deleted file mode 100644
index fca9497b58..0000000000
--- a/docs-ui/src/content/header-page.props.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import {
- childrenPropDefs,
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const propDefs: Record = {
- title: {
- type: 'string',
- default: 'Your plugin',
- },
- customActions: {
- type: 'enum',
- values: ['ReactNode'],
- },
- menuItems: {
- type: 'complex',
- complexType: {
- name: 'MenuItem[]',
- properties: {
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the menu item',
- },
- value: {
- type: 'string',
- required: true,
- description: 'Unique value for the menu item',
- },
- onClick: {
- type: '() => void',
- required: false,
- description: 'Callback function when menu item is clicked',
- },
- },
- },
- },
- tabs: {
- type: 'complex',
- complexType: {
- name: 'HeaderTab[]',
- properties: {
- id: {
- type: 'string',
- required: true,
- description: 'Unique identifier for the tab',
- },
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the tab',
- },
- href: {
- type: 'string',
- required: false,
- description: 'URL to navigate to when tab is clicked',
- },
- matchStrategy: {
- type: "'exact' | 'prefix'",
- required: false,
- description: 'How to match the current route to highlight the tab',
- },
- },
- },
- },
- breadcrumbs: {
- type: 'complex',
- complexType: {
- name: 'Breadcrumb[]',
- properties: {
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the breadcrumb',
- },
- href: {
- type: 'string',
- required: true,
- description: 'URL for the breadcrumb link',
- },
- },
- },
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const usage = `import { HeaderPage } from '@backstage/ui';
-
- `;
-
-export const defaultSnippet = `Custom action}
-/>`;
-
-export const withBreadcrumbs = ` `;
-
-export const withTabs = ` `;
-
-export const withCustomActions = `Custom action}
-/>`;
-
-export const withMenuItems = ` {} },
- { label: 'Invite new members', value: 'invite-new-members', onClick: () => {} },
- ]}
-/>`;
diff --git a/docs-ui/src/content/header.mdx b/docs-ui/src/content/header.mdx
deleted file mode 100644
index eb505d7a62..0000000000
--- a/docs-ui/src/content/header.mdx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { CodeBlock } from '@/components/CodeBlock';
-import { Snippet } from '@/components/Snippet';
-import { HeaderSnippet } from '@/snippets/stories-snippets';
-import {
- propDefs,
- usage,
- simple,
- defaultSnippet,
- withTabs,
- withBreadcrumbs,
- withHeaderPage,
-} from './header.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { HeaderDefinition } from '../utils/definitions';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-
-
-
- }
- code={defaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Examples
-
-### Simple header
-
-A simple example of how to use the Header component.
-
- }
- 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.
-
- }
-code={withTabs}
-open
-/>
-
-### Header with breadcrumbs
-
-Breacrumbs should appear when you scroll down (and not directly visible as it is in the demo below).
-
- }
- code={withBreadcrumbs}
- open
-/>
-
-### Header with HeaderPage
-
-You can use the `Header` component inside the [HeaderPage](/components/header-page) component to compose your multi-level navigation.
-
- }
- code={withHeaderPage}
- open
-/>
-
-
-
-
diff --git a/docs-ui/src/content/header.props.ts b/docs-ui/src/content/header.props.ts
deleted file mode 100644
index af9a745fc5..0000000000
--- a/docs-ui/src/content/header.props.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import {
- childrenPropDefs,
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const propDefs: Record = {
- icon: {
- type: 'enum',
- values: ['ReactNode'],
- },
- title: {
- type: 'string',
- default: 'Your plugin',
- },
- titleLink: {
- type: 'string',
- default: '/',
- },
- customActions: {
- type: 'enum',
- values: ['ReactNode'],
- },
- menuItems: {
- type: 'complex',
- complexType: {
- name: 'MenuItem[]',
- properties: {
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the menu item',
- },
- value: {
- type: 'string',
- required: true,
- description: 'Unique value for the menu item',
- },
- onClick: {
- type: '() => void',
- required: false,
- description: 'Callback function when menu item is clicked',
- },
- },
- },
- },
- tabs: {
- type: 'complex',
- complexType: {
- name: 'HeaderTab[]',
- properties: {
- id: {
- type: 'string',
- required: true,
- description: 'Unique identifier for the tab',
- },
- label: {
- type: 'string',
- required: true,
- description: 'Display text for the tab',
- },
- href: {
- type: 'string',
- required: false,
- description: 'URL to navigate to when tab is clicked',
- },
- matchStrategy: {
- type: "'exact' | 'prefix'",
- required: false,
- description: 'How to match the current route to highlight the tab',
- },
- },
- },
- },
- onTabSelectionChange: {
- type: 'enum',
- values: ['(key: string) => void'],
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const usage = `import { Header } from '@backstage/ui';
-
-`;
-
-export const defaultSnippet = `
- } />
- } />
- } />
- >
- }
-/>`;
-
-export const simple = `
- } />
- } />
- } />
- >
- }
-/>`;
-
-export const withTabs = `
-
- } />
- } />
- } />
- >
- }
- tabs={[
- { id: 'overview', label: 'Overview' },
- { id: 'checks', label: 'Checks' },
- { id: 'tracks', label: 'Tracks' },
- { id: 'campaigns', label: 'Campaigns' },
- { id: 'integrations', label: 'Integrations' },
- ]}
-/>
-`;
-
-export const withBreadcrumbs = ``;
-
-export const withHeaderPage = `
-Custom action}
-/>`;
diff --git a/docs-ui/src/content/link.mdx b/docs-ui/src/content/link.mdx
deleted file mode 100644
index 486c06b298..0000000000
--- a/docs-ui/src/content/link.mdx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import {
- MenuSnippet,
- ButtonSnippet,
- LinkSnippet,
-} from '@/snippets/stories-snippets';
-import {
- linkPropDefs,
- linkUsageSnippet,
- linkDefaultSnippet,
- linkVariantsSnippet,
- linkWeightsSnippet,
- linkColorsSnippet,
- linkRouterSnippet,
- linkTruncateSnippet,
-} from './link.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { LinkDefinition } from '../utils/definitions';
-
-
-
- }
- code={linkDefaultSnippet}
-/>
-
-## Usage
-
-
-
-## API reference
-
-
-
-## Router Integration
-
-The `Link` component handles both internal and external navigation. It automatically detects whether the provided URL is internal (relative path) or external (absolute URL with protocol) and renders the appropriate element:
-
-- **Internal routes**: Uses `react-router-dom`'s `Link` component for client-side navigation
-- **External URLs**: Renders a standard `` element for traditional navigation
-
-
-
-## Examples
-
-### Variants
-
-Here's a view when links have different variants.
-
- }
- code={linkVariantsSnippet}
-/>
-
-### Weights
-
-Here's a view when links have different weights.
-
- }
- code={linkWeightsSnippet}
-/>
-
-### Colors
-
-Here's a view when links have different colors.
-
- }
- code={linkColorsSnippet}
-/>
-
-### Truncate
-
-The `Link` component has a `truncate` prop that can be used to truncate the text.
-
- }
- code={linkTruncateSnippet}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/link.props.ts b/docs-ui/src/content/link.props.ts
deleted file mode 100644
index 748e64cc15..0000000000
--- a/docs-ui/src/content/link.props.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-export const linkPropDefs: Record = {
- href: {
- type: 'string',
- },
- variant: {
- type: 'enum',
- values: [
- 'title-large',
- 'title-medium',
- 'title-small',
- 'title-x-small',
- 'body-large',
- 'body-medium',
- 'body-small',
- 'body-x-small',
- ],
- default: 'body-medium',
- responsive: true,
- },
- weight: {
- type: 'enum',
- values: ['regular', 'bold'],
- default: 'regular',
- responsive: true,
- },
- color: {
- type: 'enum',
- values: ['primary', 'secondary', 'danger', 'warning', 'success'],
- default: 'primary',
- responsive: true,
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const linkUsageSnippet = `import { Link } from '@backstage/ui';
-
- Sign up for Backstage`;
-
-export const linkDefaultSnippet = ` Sign up for Backstage`;
-
-export const linkVariantsSnippet = `
- ...
- ...
- ...
- ...
- ...
- ...
- ...
- ...
- `;
-
-export const linkWeightsSnippet = `
-
-
- `;
-
-export const linkColorsSnippet = `
- I am primary
- I am secondary
- I am danger
- I am warning
- I am success
- `;
-
-export const linkRouterSnippet = `import { Link } from '@backstage/ui';
-
-// Internal route
- Home
-
-// External URL
- Backstage
-`;
-
-export const linkTruncateSnippet = ` ...`;
diff --git a/docs-ui/src/content/menu.mdx b/docs-ui/src/content/menu.mdx
deleted file mode 100644
index 9ede0885e2..0000000000
--- a/docs-ui/src/content/menu.mdx
+++ /dev/null
@@ -1,237 +0,0 @@
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { CodeBlock } from '@/components/CodeBlock';
-import { MenuSnippet } from '@/snippets/stories-snippets';
-import {
- usage,
- preview,
- submenu,
- icons,
- sections,
- separators,
- links,
- autocomplete,
- autocompleteListbox,
- autocompleteListboxMultiple,
- menuTriggerPropDefs,
- submenuTriggerPropDefs,
- menuPropDefs,
- menuListBoxPropDefs,
- menuAutocompletePropDefs,
- menuAutocompleteListboxPropDefs,
- menuItemPropDefs,
- menuListBoxItemPropDefs,
- menuSectionPropDefs,
- menuSeparatorPropDefs,
-} from './menu.props';
-import { PageTitle } from '@/components/PageTitle';
-import { Theming } from '@/components/Theming';
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { MenuDefinition } from '../utils/definitions';
-
-
-
- }
- code={preview}
-/>
-
-## Usage
-
-
-
-### Triggers
-
-- `MenuTrigger` is a wrapper component that combines a button or other trigger element with a menu displayed in a popover.
-- `SubmenuTrigger` is a wrapper component that combines a `MenuItem` with a menu displayed in a popover.
-
-### Containers
-
-- `Menu` is a container component that contains a list of menu items or sections.
-- `MenuListBox` is a container component that contains a list of menu items or sections.
-- `MenuAutocomplete` is a container component that contains a list of menu items or sections.
-- `MenuAutocompleteListbox` is a container component that contains a list of menu items or sections.
-
-### Items
-
-- `MenuItem` is an individual interactive item in the menu.
-- `MenuListBoxItem` is an individual interactive item in the menu list box.
-
-### Separators
-
-- `MenuSeparator` is a component that renders a horizontal line to separate menu items.
-- `MenuSection` is a component that renders a section in the menu.
-
-## API reference
-
-### MenuTrigger
-
-`MenuTrigger` accepts exactly two children: the first child should be the trigger element, and second child should be
-one of the menu containers containing the menu.
-
-
-
-### 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.
-
-
-
-### Menu
-
-`Menu` is a container component that contains a list of menu items or sections.
-
-
-
-### MenuListBox
-
-`MenuListBox` is a container component that contains a list of menu items or sections.
-
-
-
-### MenuAutocomplete
-
-`MenuAutocomplete` is a container component that contains a list of menu items or sections.
-
-
-
-### MenuAutocompleteListbox
-
-`MenuAutocompleteListbox` is a container component that contains a list of menu items or sections.
-
-
-
-### MenuItem
-
-`MenuItem` is an individual interactive item in the menu.
-
-
-
-### MenuListBoxItem
-
-`MenuListBoxItem` is an individual interactive item in the menu list box.
-
-
-
-### MenuSection
-
-`MenuSection` is a component that renders a section in the menu.
-
-
-
-### MenuSeparator
-
-`MenuSeparator` is a component that renders a horizontal line to separate menu items.
-
-
-
-## 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.
-
- }
- code={submenu}
-/>
-
-### With icons
-
-You can use the `iconStart` prop to add an icon to the menu item.
-
- }
- 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.
-
- }
- code={links}
-/>
-
-### With sections
-
-You can use the `MenuSection` component to add a section to the menu.
-
- }
- code={sections}
-/>
-
-### With separators
-
-You can use the `MenuSeparator` component to add a separator to the menu.
-
- }
- code={separators}
-/>
-
-### With autocomplete
-
-You can use the `MenuAutocomplete` component to add a autocomplete to the menu.
-
- }
- code={autocomplete}
-/>
-
-### With list box
-
-You can use the `MenuListBox` component to add a list box to the menu.
-
- }
- code={autocompleteListbox}
-/>
-
-### With list box with multiple selection
-
-You can use the `MenuListBox` component to add a list box to the menu. You can also use the `selectionMode` prop to
-allow multiple selection.
-
- }
- code={autocompleteListboxMultiple}
-/>
-
-
-
-
diff --git a/docs-ui/src/content/menu.props.ts b/docs-ui/src/content/menu.props.ts
deleted file mode 100644
index 2c7d76dd5d..0000000000
--- a/docs-ui/src/content/menu.props.ts
+++ /dev/null
@@ -1,372 +0,0 @@
-import {
- classNamePropDefs,
- stylePropDefs,
- type PropDef,
-} from '@/utils/propDefs';
-
-const placementValues = [
- 'bottom',
- 'bottom left',
- 'bottom right',
- 'bottom start',
- 'bottom end',
- 'top',
- 'top left',
- 'top right',
- 'top start',
- 'top end',
- 'left',
- 'left top',
- 'left bottom',
- 'start',
- 'start top',
- 'start bottom',
- 'right',
- 'right top',
- 'right bottom',
- 'end',
- 'end top',
- 'end bottom',
-];
-
-export const menuTriggerPropDefs: Record = {
- isOpen: {
- type: 'boolean',
- },
- defaultOpen: {
- type: 'boolean',
- },
- onOpenChange: {
- type: 'enum',
- values: ['(isOpen: boolean) => void'],
- },
-};
-
-export const submenuTriggerPropDefs: Record = {
- delay: {
- type: 'number',
- default: '200',
- },
-};
-
-export const menuPropDefs: Record = {
- disabledKeys: {
- type: 'enum',
- values: ['Iterable'],
- },
- selectionMode: {
- type: 'enum',
- values: ['none', 'single', 'multiple'],
- },
- selectedKeys: {
- type: 'enum',
- values: ['all', 'Iterable'],
- },
- defaultSelectedKeys: {
- type: 'enum',
- values: ['all', 'Iterable'],
- },
- placement: {
- type: 'enum',
- values: placementValues,
- },
- virtualized: {
- type: 'boolean',
- default: 'false',
- },
- maxWidth: {
- type: 'number',
- },
- maxHeight: {
- type: 'number',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuListBoxPropDefs: Record = {
- disabledKeys: {
- type: 'enum',
- values: ['Iterable'],
- },
- selectionMode: {
- type: 'enum',
- values: ['none', 'single', 'multiple'],
- },
- selectedKeys: {
- type: 'enum',
- values: ['all', 'Iterable'],
- },
- defaultSelectedKeys: {
- type: 'enum',
- values: ['all', 'Iterable'],
- },
- placement: {
- type: 'enum',
- values: placementValues,
- },
- virtualized: {
- type: 'boolean',
- default: 'false',
- },
- maxWidth: {
- type: 'number',
- },
- maxHeight: {
- type: 'number',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuAutocompletePropDefs: Record = {
- placement: {
- type: 'enum',
- values: placementValues,
- },
- virtualized: {
- type: 'boolean',
- default: 'false',
- },
- maxWidth: {
- type: 'number',
- },
- maxHeight: {
- type: 'number',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuAutocompleteListboxPropDefs: Record = {
- placement: {
- type: 'enum',
- values: placementValues,
- },
- virtualized: {
- type: 'boolean',
- default: 'false',
- },
- maxWidth: {
- type: 'number',
- },
- maxHeight: {
- type: 'number',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuItemPropDefs: Record = {
- 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 = {
- id: {
- type: 'enum',
- values: ['Key'],
- },
- value: {
- type: 'string',
- },
- textValue: {
- type: 'string',
- },
- isDisabled: {
- type: 'boolean',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuSectionPropDefs: Record = {
- title: {
- type: 'string',
- },
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const menuSeparatorPropDefs: Record = {
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const usage = `import { MenuTrigger, Menu, MenuItem } from '@backstage/ui';
-
-
- Menu
-
- Apple
- Banana
- Blueberry
-
-
- Vegetables
-
- Carrot
- Tomato
- Potato
-
-
-
- `;
-
-export const preview = `
- Menu
-
- {options.map(option => (
- {option.label}
- ))}
-
- `;
-
-export const submenu = `
- Menu
-
- Edit
- Duplicate
-
- Submenu
-
- Edit
- Duplicate
- Rename
-
- Share
- Move
-
- }>Feedback
-
-
-
- `;
-
-export const icons = `
- Menu
-
- }>Copy
- }>Rename
- }>Send feedback
-
- `;
-
-export const sections = `
- Menu
-
-
- }>Profile
- }>Settings
-
-
- }>Help Center
- }>
- Contact Support
-
- }>Feedback
-
-
- `;
-
-export const separators = `
- Menu
-
- Edit
- Duplicate
- Rename
-
- Share
- Move
-
- }>Feedback
-
- `;
-
-export const links = `
- Menu
-
- Internal link
-
- External link
-
- Email link
-
- `;
-
-export const autocomplete = `
- Menu
-
- Create new file...
- Create new folder...
- Assign to...
- Assign to me
- Change status...
- Change priority...
- Add label...
- Remove label...
-
- `;
-
-export const autocompleteListbox = `const [selected, setSelected] = useState(
- new Set(['blueberry']),
-);
-
-
- Selected: {Array.from(selected).join(', ')}
-
- Menu
-
- {options.map(option => (
-
- {option.label}
-
- ))}
-
-
- `;
-
-export const autocompleteListboxMultiple = `const [selected, setSelected] = useState(
- new Set(['blueberry', 'cherry']),
-);
-
-
- Selected: {Array.from(selected).join(', ')}
-
- Menu
-
- {options.map(option => (
-