Merge pull request #30618 from backstage/docs-ui

Docs UI - Improve layout and add missing pages
This commit is contained in:
Charles de Dreuille
2025-07-23 09:34:22 +01:00
committed by GitHub
38 changed files with 1527 additions and 578 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

+36
View File
@@ -0,0 +1,36 @@
import { CodeBlock } from '@/components/CodeBlock';
# How to install Backstage UI
## 1. Import the package
Import the package using a package manager.
<CodeBlock lang="shell" code={`yarn add @backstage/ui`} />
## 2. Import the css files
Import the global CSS file at the root of your application.
```tsx
import '@backstage/ui/css/styles.css';
```
## 3. Start building ✨
Now you can start building your plugin using the new design system.
```tsx
import { Flex, Button, Text } from '@backstage/ui';
<Flex>
<Text>Hello World</Text>
<Button>Click me</Button>
</Flex>;
```
## Next steps
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).
+4 -2
View File
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { Sidebar } from '../components/Sidebar';
import { Toolbar } from '@/components/Toolbar';
import { StickyHeader } from '../components/StickyHeader/StickyHeader';
import { Providers } from './providers';
import { CustomTheme } from '@/components/CustomTheme';
import styles from './layout.module.css';
@@ -49,8 +50,9 @@ export default async function RootLayout({
>
<body>
<Providers>
<Sidebar version={packageVersion} />
<Toolbar />
<Sidebar />
<Toolbar version={packageVersion} />
<StickyHeader />
<div className={styles.container}>
<div className={styles.content}>{children}</div>
</div>
+153 -23
View File
@@ -1,43 +1,173 @@
import { CodeBlock } from '@/components/CodeBlock';
import { ComponentCards, ComponentCard } from '@/components/ComponentCards';
# Backstage UI
## Welcome to Backstage UI, the new design library for Backstage plugins.
Welcome to the Backstage UI, the new design library for Backstage plugins. This
project is still under active development but we will make sure to document
This project is still under active development but we will make sure to document
the API as we go. We are aiming to improve the general UI of Backstage and
plugins across Backstage. This new library will take time to build but we are
building it incrementally with not conflict with the existing theming system.
## Installation
### Actions
### 1. Install Backstage UI
<ComponentCards>
<ComponentCard
title="Button"
description="A button component to help you trigger actions."
href="/components/button"
/>
<ComponentCard
title="ButtonLink"
description="A button component to help you trigger actions."
href="/components/button-link"
/>
<ComponentCard
title="ButtonIcon"
description="A button for actions with an icon."
href="/components/button-icon"
/>
<ComponentCard
title="Link"
description="A link component to help you navigate to other pages."
href="/components/link"
/>
</ComponentCards>
Install Backstage UI using a package manager.
### Layout
<CodeBlock lang="shell" code={`yarn add @backstage/ui`} />
<ComponentCards>
<ComponentCard
title="Box"
description="The simplest component to help you style your content."
href="/components/box"
/>
<ComponentCard
title="Flex"
description="A flex container for your layout."
href="/components/flex"
/>
<ComponentCard
title="Grid"
description="A grid container for your layout."
href="/components/grid"
/>
<ComponentCard
title="Container"
description="Helps you center your content."
href="/components/container"
/>
<ComponentCard
title="Card"
description="A component to separate your content on the page."
href="/components/card"
/>
<ComponentCard
title="Collapsible"
description="A collapsible component for expandable content."
href="/components/collapsible"
/>
</ComponentCards>
### 2. Import the css files
### Selection and inputs
Import the global CSS file at the root of your application.
<ComponentCards>
<ComponentCard
title="Checkbox"
description="A checkbox component to help you select items."
href="/components/checkbox"
/>
<ComponentCard
title="TextField"
description="A text field component to help you input text."
href="/components/text-field"
/>
<ComponentCard
title="SearchField"
description="A search field component to help you search for items."
href="/components/search-field"
/>
<ComponentCard
title="Select"
description="A select component to help you select items."
href="/components/select"
/>
<ComponentCard
title="Switch"
description="A switch component to help you toggle items."
href="/components/switch"
/>
<ComponentCard
title="RadioGroup"
description="A radio group component to help you select items."
href="/components/radio-group"
/>
</ComponentCards>
```tsx
import '@backstage/ui/css/styles.css';
```
### Navigation
### 3. Start building ✨
<ComponentCards>
<ComponentCard
title="Header"
description="A header component to help you display a header."
href="/components/header"
/>
<ComponentCard
title="HeaderPage"
description="A header to complement the Header component."
href="/components/header-page"
/>
<ComponentCard
title="Menu"
description="A menu component to help you display a menu."
href="/components/menu"
/>
<ComponentCard
title="Tabs"
description="A tabs component to help you display a tabs."
href="/components/tabs"
/>
</ComponentCards>
Now you can start building your plugin using the new design system.
### Images and icons
```tsx
import { Flex, Button, Text } from '@backstage/ui';
<ComponentCards>
<ComponentCard
title="Avatar"
description="A avatar component to help you display user avatars."
href="/components/avatar"
/>
<ComponentCard
title="Icon"
description="A icon component to help you display icons."
href="/components/icon"
/>
</ComponentCards>
<Flex>
<Text>Hello World</Text>
<Button>Click me</Button>
</Flex>;
```
### Feedback indicators
## Next steps
<ComponentCards>
<ComponentCard
title="Skeleton"
description="A skeleton component to help you display loading states."
href="/components/skeleton"
/>
<ComponentCard
title="Tooltip"
description="A tooltip component to help you display a tooltip."
href="/components/tooltip"
/>
</ComponentCards>
### Typography
<ComponentCards>
<ComponentCard
title="Text"
description="A text component to help you style your text."
href="/components/text"
/>
</ComponentCards>
## 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.
-65
View File
@@ -1,65 +0,0 @@
import { HeadingSnippet, TextSnippet } from '@/snippets/stories-snippets';
import { Snippet } from '@/components/Snippet';
# Typography
Backstage UI offers a suite of typography components designed to seamlessly align
with the rest of your Backstage instance. While you can customize their
appearance to match your brand, the underlying API remains consistent and
unchanged. Each component is built on a responsive structure, allowing you to
define different typography values for various breakpoints.
## Headings
Headings are used to structure the content of your page. They are used to
create a hierarchy of information and to make the content more readable. The
best way to use add these headings to your page is to import the [Heading
component](?path=/docs/components-heading--docs).
<Snippet
py={2}
open
preview={<HeadingSnippet story="AllVariants" />}
code={`<Flex direction="column" gap="4">
<Heading variant="display">Display</Heading>
<Heading variant="title1">Title 1</Heading>
<Heading variant="title2">Title 2</Heading>
<Heading variant="title3">Title 3</Heading>
<Heading variant="title4">Title 4</Heading>
</Flex>`}
/>
## Text
Backstage UI provides four distinct text variants, each offering different font sizes
carefully designed to cover the majority of use cases. These variants are
versatile and can be paired with regular and bold of font weights. You can use
the [Text component](?path=/docs/components-text--docs) to add text to your
page.
<Snippet
open
preview={<TextSnippet story="AllVariants" />}
code={`<Flex direction="column" gap="4">
<Text variant="subtitle" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text variant="body" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text variant="caption" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
<Text variant="label" style={{ maxWidth: '600px' }}>
A man looks at a painting in a museum and says, “Brothers and sisters I
have none, but that man&apos;s father is my father&apos;s son.” Who is
in the painting?
</Text>
</Flex>`}
/>
@@ -0,0 +1,54 @@
.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(--panel);
border-radius: 8px;
border: 1px solid var(--border);
padding: 16px;
gap: 4px;
min-height: 120px;
&:hover {
background-color: var(--panel-hover);
}
}
.title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--primary);
}
.description {
margin: 0;
font-size: 14px;
color: var(--secondary);
}
@@ -0,0 +1,23 @@
import Link from 'next/link';
import styles from './ComponentCards.module.css';
export const ComponentCards = ({ children }: { children: React.ReactNode }) => {
return <div className={styles.grid}>{children}</div>;
};
export const ComponentCard = ({
title,
description,
href,
}: {
title: string;
description: string;
href: string;
}) => {
return (
<Link href={href} className={styles.card}>
<h3 className={styles.title}>{title}</h3>
<p className={styles.description}>{description}</p>
</Link>
);
};
@@ -0,0 +1 @@
export * from './ComponentCards';
@@ -2,15 +2,13 @@
import * as Table from '../Table';
import { Chip } from '../Chip';
import { TypePopup } from './TypePopup';
import { icons } from '../../../../packages/ui';
// Define a more specific type for the data object
type PropData = {
values?: string | string[];
responsive?: boolean;
default?: string;
type?: string;
};
import { PropDef } from '@/utils/propDefs';
// Use the proper PropDef type
type PropData = PropDef;
// Modify the PropsTable component to use the new type
export const PropsTable = <T extends Record<string, PropData>>({
@@ -59,6 +57,12 @@ export const PropsTable = <T extends Record<string, PropData>>({
<Chip>string</Chip>
</>
)}
{data[n].type === 'complex' && data[n].complexType && (
<TypePopup
complexType={data[n].complexType}
name={data[n].complexType.name}
/>
)}
{data[n].type === 'enum | string' && (
<>
{enumValues}
@@ -0,0 +1,79 @@
.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;
text-decoration: underline;
}
[data-theme='dark'] .button {
background-color: #2c2c2c;
color: #fff;
}
.popover {
border: 1px solid var(--border);
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
border-radius: 6px;
background: var(--panel);
color: var(--primary);
outline: none;
/* max-width: 400px; */
transition: transform 200ms, opacity 200ms;
padding: 1rem;
margin-top: 6px;
--origin: translateY(-8px);
&[data-entering],
&[data-exiting] {
transform: var(--origin);
opacity: 0;
}
}
.arrow svg {
display: block;
fill: var(--panel);
stroke: var(--border);
stroke-width: 1px;
transform: rotate(180deg);
}
.name {
font-size: 14px;
font-weight: 500;
margin-bottom: 1rem;
}
.grid {
display: grid;
grid-template-columns: auto auto 1fr;
gap: 0.5rem 1rem;
}
.col1 {
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
padding-top: 0.25rem;
}
.col2 {
font-size: 13px;
}
.col3 {
font-size: 13px;
color: var(--text-secondary);
padding-top: 0.25rem;
}
@@ -0,0 +1,49 @@
import { ComplexTypeDef } from '@/utils/propDefs';
import { Chip } from '../Chip';
import {
Button,
Dialog,
DialogTrigger,
OverlayArrow,
Popover,
} from 'react-aria-components';
import styles from './TypePopup.module.css';
interface TypePopupProps {
complexType: ComplexTypeDef;
name: string;
}
export const TypePopup = ({ complexType, name }: TypePopupProps) => {
return (
<DialogTrigger>
<Button className={styles.button}>{name}</Button>
<Popover className={styles.popover}>
<OverlayArrow className={styles.arrow}>
<svg width={12} height={12} viewBox="0 0 12 12">
<path d="M0 0 L6 6 L12 0" />
</svg>
</OverlayArrow>
<Dialog>
<div className={styles.name}>{complexType.name}</div>
<div className={styles.grid}>
{Object.entries(complexType.properties).map(
([propName, propDef]) => [
<div key={`${propName}-name`} className={styles.col1}>
{propName}
{propDef.required ? '' : '?'}
</div>,
<div key={`${propName}-type`} className={styles.col2}>
<Chip>{propDef.type}</Chip>
</div>,
<div key={`${propName}-desc`} className={styles.col3}>
{propDef.description}
</div>,
],
)}
</div>
</Dialog>
</Popover>
</DialogTrigger>
);
};
@@ -15,33 +15,6 @@
}
}
.version {
font-size: 0.875rem;
font-weight: 500;
color: var(--secondary);
margin-inline: 32px;
border-bottom: 1px solid var(--border);
padding-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
}
.versionLinks {
display: flex;
align-items: center;
gap: 8px;
a {
color: var(--secondary);
transition: color 0.2s ease-in-out;
&:hover {
color: var(--primary);
}
}
}
.root {
height: 100%;
}
@@ -106,6 +79,10 @@
color: var(--secondary);
margin-top: 40px;
text-transform: uppercase;
&:first-child {
margin-top: 12px;
}
}
.line {
+10 -24
View File
@@ -1,13 +1,17 @@
'use client';
import styles from './Sidebar.module.css';
import { components, overview, layoutComponents, theme } from '@/utils/data';
import {
components,
overview,
layoutComponents,
coreConcepts,
} from '@/utils/data';
import { ScrollArea } from '@base-ui-components/react/scroll-area';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { motion } from 'motion/react';
import { Fragment } from 'react';
import { RiGithubLine, RiNpmjsLine } from '@remixicon/react';
const data = [
{
@@ -16,9 +20,9 @@ const data = [
url: '',
},
{
title: 'Theme',
content: theme,
url: '/theme',
title: 'Core Concepts',
content: coreConcepts,
url: '',
},
{
title: 'Layout Components',
@@ -32,30 +36,12 @@ const data = [
},
];
interface SidebarProps {
version: string;
}
export const Sidebar = ({ version }: SidebarProps) => {
export const Sidebar = () => {
const pathname = usePathname();
const isPlayground = pathname.includes('/playground');
return (
<div className={styles.sidebar}>
<div className={styles.version}>
Version {version}
<div className={styles.versionLinks}>
<a
href="https://github.com/backstage/backstage/tree/master/packages/ui"
target="_blank"
>
<RiGithubLine size={16} />
</a>
<a href="https://www.npmjs.com/package/@backstage/ui" target="_blank">
<RiNpmjsLine size={16} />
</a>
</div>
</div>
<ScrollArea.Root className={styles.root}>
<ScrollArea.Viewport className={styles.viewport}>
<div className={styles.content}>
@@ -0,0 +1,98 @@
.stickyHeader {
position: fixed;
top: 0px;
left: 32px;
right: 32px;
z-index: 99999;
background-color: var(--background);
padding: 16px 20px 16px 40px;
display: flex;
justify-content: space-between;
align-items: center;
backdrop-filter: blur(10px);
pointer-events: auto;
isolation: isolate;
transform: translateZ(0);
opacity: 0;
width: calc(100% - 64px);
mask-image: linear-gradient(to bottom, black 0%, black 80%, transparent 100%);
-webkit-mask-image: linear-gradient(
to bottom,
black 0%,
black 80%,
transparent 100%
);
}
@media (max-width: 768px) {
.stickyHeader {
display: none;
}
}
.right {
display: flex;
align-items: center;
gap: 24px;
}
.name {
font-size: 24px;
color: var(--text-primary);
font-weight: 400;
}
.version {
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}
.actions {
display: flex;
align-items: center;
gap: 16px;
}
.versionLinks {
display: flex;
align-items: center;
a {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
color: var(--secondary);
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
border-radius: 48px;
&:hover {
color: var(--primary);
background-color: var(--action);
}
}
}
@media (max-width: 768px) {
.stickyHeader {
padding: 12px 16px;
}
.right {
gap: 16px;
}
.version {
font-size: 12px;
}
}
@media (min-width: 768px) {
.stickyHeader {
width: calc(100% - 332px - 40px);
left: 332px;
right: 40px;
}
}
@@ -0,0 +1,50 @@
'use client';
import { motion, useScroll, useTransform, circOut } from 'framer-motion';
import { RiGithubLine, RiNpmjsLine } from '@remixicon/react';
import { ThemeSelector } from '../Toolbar/theme';
import { ThemeNameSelector } from '../Toolbar/theme-name';
import { useCurrentPage } from '@/hooks/useCurrentPage';
import styles from './StickyHeader.module.css';
export const StickyHeader = () => {
const { scrollY } = useScroll();
const currentPage = useCurrentPage();
// Transform scroll position to opacity only
const opacity = useTransform(scrollY, [100, 200], [0, 1], {
clamp: false,
});
const yPos = useTransform(scrollY, [200, 500], [-60, 0], {
clamp: true,
ease: circOut,
});
return (
<motion.div
className={styles.stickyHeader}
style={{
opacity,
y: yPos,
}}
>
<div className={styles.name}>{currentPage || 'Backstage UI'}</div>
<div className={styles.actions}>
<div className={styles.versionLinks}>
<a
href="https://github.com/backstage/backstage/tree/master/packages/ui"
target="_blank"
>
<RiGithubLine size={16} />
</a>
<a href="https://www.npmjs.com/package/@backstage/ui" target="_blank">
<RiNpmjsLine size={16} />
</a>
</div>
<ThemeNameSelector />
<ThemeSelector />
</div>
</motion.div>
);
};
File diff suppressed because one or more lines are too long
@@ -11,30 +11,50 @@
padding-left: 0.25rem;
}
@media (min-width: 960px) {
.toolbar {
left: 332px;
right: 40px;
}
}
.left {
/* width: 296px; */
display: flex;
align-items: center;
gap: 0.5rem;
padding-right: 20px;
}
.name {
margin-top: 3px;
font-size: 28px;
color: #26c9ad;
font-weight: 500;
.right {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
}
.actions {
display: none;
}
.version {
display: none;
}
.versionLinks {
display: flex;
align-items: center;
a {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
color: var(--secondary);
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
border-radius: 48px;
&:hover {
color: var(--primary);
background-color: var(--action);
}
}
}
@media (min-width: 600px) {
.actions {
display: flex;
@@ -42,3 +62,35 @@
gap: 1rem;
}
}
@media (min-width: 768px) {
.toolbar {
right: 40px;
}
}
@media (min-width: 820px) {
.right {
justify-content: space-between;
}
.version {
display: flex;
align-items: center;
justify-content: center;
/* background-color: var(--action); */
border: 1px solid var(--border2);
border-radius: 48px;
padding-inline: 20px;
color: var(--primary);
font-size: 0.875rem;
font-weight: 500;
height: 48px;
}
}
@media (min-width: 960px) {
.left {
width: 296px;
}
}
File diff suppressed because one or more lines are too long
@@ -25,19 +25,6 @@
}
}
.SelectIcon {
display: flex;
}
.SelectValue {
font-size: 0.875rem;
font-weight: 400;
}
.Positioner {
z-index: 20;
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
@@ -73,65 +60,51 @@
}
}
.Item {
box-sizing: border-box;
outline: 0;
font-size: 0.875rem;
line-height: 1rem;
padding-block: 0.5rem;
padding-left: 0.625rem;
padding-right: 1rem;
font-size: 0.875rem;
font-weight: 600;
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 0.75rem 1fr;
cursor: default;
user-select: none;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
.Popup[data-trigger='Select'] {
min-width: var(--trigger-width);
[data-side='none'] & {
font-size: 1rem;
padding-right: 3rem;
min-width: calc(var(--anchor-width) + 1rem);
.ListBox {
display: block;
width: unset;
max-height: inherit;
min-height: unset;
border: none;
.react-aria-Header {
padding-left: 1.571rem;
}
}
&[data-highlighted] {
z-index: 0;
.Item {
position: relative;
color: var(--color-gray-50);
background-color: var(--bg);
}
padding: 0 0.571rem 0 1.571rem;
height: 2rem;
display: flex;
align-items: center;
&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0.25rem;
border-radius: 0.25rem;
background-color: var(--color-gray-900);
&[data-focus-visible] {
outline: none;
}
&[data-selected] {
font-weight: 600;
background: unset;
color: var(--text-color);
&::before {
content: '✓';
content: '✓' / '';
alt: ' ';
position: absolute;
left: 4px;
}
}
&[data-focused],
&[data-pressed] {
background: var(--bg);
color: var(--primary);
cursor: pointer;
}
}
}
.ItemIndicator {
grid-column-start: 1;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
}
.ItemIndicatorIcon {
display: block;
width: 0.75rem;
height: 0.75rem;
}
.ItemText {
grid-column-start: 2;
font-size: 14px;
}
+25 -30
View File
@@ -1,6 +1,13 @@
'use client';
import { Select } from '@base-ui-components/react/select';
import {
Button,
ListBox,
ListBoxItem,
Popover,
Select,
SelectValue,
} from 'react-aria-components';
import styles from './theme-name.module.css';
import { Icon } from '@backstage/ui';
import { usePlayground } from '@/utils/playground-context';
@@ -15,35 +22,23 @@ export const ThemeNameSelector = () => {
const { selectedThemeName, setSelectedThemeName } = usePlayground();
return (
<Select.Root
value={selectedThemeName || 'default'}
onValueChange={setSelectedThemeName}
<Select
selectedKey={selectedThemeName || 'default'}
onSelectionChange={setSelectedThemeName}
>
<Select.Trigger className={styles.Select}>
<Select.Value
className={styles.SelectValue}
placeholder="Select a theme"
/>
<Select.Icon className={styles.SelectIcon}>
<Icon name="chevron-down" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Positioner className={styles.Positioner} sideOffset={8}>
<Select.Popup className={styles.Popup}>
{themes.map(({ name, value }) => (
<Select.Item className={styles.Item} value={value} key={value}>
<Select.ItemIndicator className={styles.ItemIndicator}>
<Icon name="check" />
</Select.ItemIndicator>
<Select.ItemText className={styles.ItemText}>
{name}
</Select.ItemText>
</Select.Item>
))}
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
<Button className={styles.Select}>
<SelectValue />
<Icon name="chevron-down" />
</Button>
<Popover className={styles.Popup}>
<ListBox className={styles.ListBox}>
{themes.map(({ name, value }) => (
<ListBoxItem key={value} id={value} className={styles.Item}>
{name}
</ListBoxItem>
))}
</ListBox>
</Popover>
</Select>
);
};
@@ -0,0 +1,40 @@
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,
withHeaderPage,
} from './header-page.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Component"
title="HeaderPage"
description="A header page component for the plugin that should sit under the Header component."
/>
<Snippet
py={4}
preview={<HeaderPageSnippet story="WithEverything" />}
code={defaultSnippet}
/>
## Usage
<CodeBlock code={usage} />
## API reference
<PropsTable data={propDefs} />
<Theming component="Header" />
<ChangelogComponent component="Header" />
@@ -0,0 +1,91 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const propDefs: Record<string, PropDef> = {
title: {
type: 'string',
default: 'Your plugin',
},
customActions: {
type: 'enum',
values: ['ReactNode'],
},
menuItems: {
type: 'complex',
complexType: {
name: 'MenuItem[]',
properties: {
label: {
type: 'string',
required: true,
description: 'Display text for the menu item',
},
value: {
type: 'string',
required: true,
description: 'Unique value for the menu item',
},
onClick: {
type: '() => void',
required: false,
description: 'Callback function when menu item is clicked',
},
},
},
},
tabs: {
type: 'complex',
complexType: {
name: 'HeaderTab[]',
properties: {
id: {
type: 'string',
required: true,
description: 'Unique identifier for the tab',
},
label: {
type: 'string',
required: true,
description: 'Display text for the tab',
},
href: {
type: 'string',
required: false,
description: 'URL to navigate to when tab is clicked',
},
matchStrategy: {
type: "'exact' | 'prefix'",
required: false,
description: 'How to match the current route to highlight the tab',
},
},
},
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const usage = `import { HeaderPage } from '@backstage/ui';
<HeaderPage />`;
export const defaultSnippet = `<HeaderPage
title="My plugin"
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
]}
customActions={<Button>Custom action</Button>}
/>`;
+85
View File
@@ -0,0 +1,85 @@
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 { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
type="Component"
title="Header"
description="A header component for the plugin."
/>
<Snippet
py={4}
preview={<HeaderSnippet story="WithAllOptionsAndTabs" />}
code={defaultSnippet}
/>
## Usage
<CodeBlock code={usage} />
## API reference
<PropsTable data={propDefs} />
## Examples
### Simple header
A simple example of how to use the Header component.
<Snippet
py={4}
preview={<HeaderSnippet story="WithAllOptions" />}
code={simple}
open
/>
### Header with tabs
A simple example of how to use the Header component with tabs. All links are using React Router
under the hood and will be active when you are on the corresponding page.
<Snippet
preview={<HeaderSnippet story="WithAllOptionsAndTabs" />}
code={withTabs}
open
/>
### Header with breadcrumbs
Breacrumbs should appear when you scroll down (and not directly visible as it is in the demo below).
<Snippet
preview={<HeaderSnippet story="WithBreadcrumbs" />}
code={withBreadcrumbs}
open
/>
### Header with HeaderPage
You can use the `Header` component inside the [HeaderPage](/components/header-page) component to compose your multi-level navigation.
<Snippet
preview={<HeaderSnippet story="WithHeaderPage" />}
code={withHeaderPage}
open
/>
<Theming component="Header" />
<ChangelogComponent component="Header" />
@@ -0,0 +1,218 @@
import {
childrenPropDefs,
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const propDefs: Record<string, PropDef> = {
icon: {
type: 'enum',
values: ['ReactNode'],
},
title: {
type: 'string',
default: 'Your plugin',
},
titleLink: {
type: 'string',
default: '/',
},
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',
},
},
},
},
customActions: {
type: 'enum',
values: ['ReactNode'],
},
menuItems: {
type: 'complex',
complexType: {
name: 'MenuItem[]',
properties: {
label: {
type: 'string',
required: true,
description: 'Display text for the menu item',
},
value: {
type: 'string',
required: true,
description: 'Unique value for the menu item',
},
onClick: {
type: '() => void',
required: false,
description: 'Callback function when menu item is clicked',
},
},
},
},
tabs: {
type: 'complex',
complexType: {
name: 'HeaderTab[]',
properties: {
id: {
type: 'string',
required: true,
description: 'Unique identifier for the tab',
},
label: {
type: 'string',
required: true,
description: 'Display text for the tab',
},
href: {
type: 'string',
required: false,
description: 'URL to navigate to when tab is clicked',
},
matchStrategy: {
type: "'exact' | 'prefix'",
required: false,
description: 'How to match the current route to highlight the tab',
},
},
},
},
onTabSelectionChange: {
type: 'enum',
values: ['(key: string) => void'],
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const usage = `import { Header } from '@backstage/ui';
<Header />`;
export const defaultSnippet = `<Header
title="My plugin"
titleLink="/"
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Settings', href: '/settings' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
]}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
/>`;
export const simple = `<Header
title="My plugin"
titleLink="/"
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
]}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
/>`;
export const withTabs = `
<Header
title="My plugin"
titleLink="/"
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
menuItems={[
{ label: 'Settings', value: 'settings' },
{ label: 'Invite new members', value: 'invite-new-members' },
]}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
/>
`;
export const withBreadcrumbs = `<Header
title="My plugin"
titleLink="/"
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'Settings', href: '/settings' },
]}
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'checks', label: 'Checks' },
{ id: 'tracks', label: 'Tracks' },
{ id: 'campaigns', label: 'Campaigns' },
{ id: 'integrations', label: 'Integrations' },
]}
/>`;
export const withHeaderPage = `<Header
title="My plugin"
titleLink="/"
breadcrumbs={...}
tabs={...}
/>
<HeaderPage
title="Page title"
menuItems={...}
tabs={...}
customActions={<Button>Custom action</Button>}
/>`;
+41 -17
View File
@@ -3,12 +3,13 @@ import { TabsSnippet } from '@/snippets/stories-snippets';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
tabsRootPropDefs,
tabsListPropDefs,
tabsTabPropDefs,
tabsPanelPropDefs,
tabsPropDefs,
tabPropDefs,
tabsUsageSnippet,
tabsDefaultSnippet,
tabsWithTabPanelsSnippet,
tabsWithLinksSnippet,
tabsWithDeeplyNestedRoutesSnippet,
} from './tabs.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -31,29 +32,52 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## API reference
### Tabs.Root
### Tabs
Groups the tabs and the corresponding panels. Renders a `<div>` element.
<PropsTable data={tabsRootPropDefs} />
<PropsTable data={tabsPropDefs} />
### Tabs.List
Groups the individual tab buttons. Renders a `<div>` element.
<PropsTable data={tabsListPropDefs} />
### Tabs.Tab
### Tab
An individual interactive tab button that toggles the corresponding panel. Renders a `<button>` element.
<PropsTable data={tabsTabPropDefs} />
<PropsTable data={tabPropDefs} />
### Tabs.Panel
## Examples
A panel displayed when the corresponding tab is active. Renders a `<div>` element.
### Simple tabs
<PropsTable data={tabsPanelPropDefs} />
To connect the tabs with the panels, you need to use the `id` prop on the tab and the tab panel.
<Snippet
py={4}
preview={<TabsSnippet story="WithTabPanels" />}
code={tabsWithTabPanelsSnippet}
open
/>
### Tabs as links
You can use the `href` prop on the tab to make it a link. This will use the `react-router` under the hood to navigate to the tab. We automatically detect if this is an external URL and render a `<a>` element instead of a `<button>`.
<Snippet
py={4}
preview={<TabsSnippet story="WithMockedURLTab2" />}
code={tabsWithLinksSnippet}
open
/>
### With deeply nested routes
You can use the `matchStrategy` prop on the tab to control how the tab is matched to the current URL.
<Snippet
py={4}
preview={<TabsSnippet story="PrefixMatchingDeepNesting" />}
code={tabsWithDeeplyNestedRoutesSnippet}
open
/>
<Theming component="Tabs" />
+92 -55
View File
@@ -5,55 +5,66 @@ import {
type PropDef,
} from '@/utils/propDefs';
export const tabsRootPropDefs: Record<string, PropDef> = {
defaultValue: {
type: 'enum',
values: ['any'],
default: '0',
export const tabsPropDefs: Record<string, PropDef> = {
isDisabled: {
type: 'boolean',
},
value: {
disabledKeys: {
type: 'enum',
values: ['any'],
values: ['string[]'],
},
onValueChange: {
selectedKey: {
type: 'enum',
values: [`((value) => void)`],
values: ['string', 'null'],
},
defaultSelectedKey: {
type: 'enum',
values: ['string'],
},
onSelectionChange: {
type: 'enum',
values: [`(key: string) => void`],
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabsListPropDefs: Record<string, PropDef> = {
activateOnFocus: {
type: 'boolean',
default: 'false',
},
loop: {
type: 'boolean',
default: 'false',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabsTabPropDefs: Record<string, PropDef> = {
value: {
export const tabPropDefs: Record<string, PropDef> = {
id: {
type: 'string',
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
export const tabsPanelPropDefs: Record<string, PropDef> = {
value: {
isDisabled: {
type: 'boolean',
},
href: {
type: 'string',
},
keepMounted: {
type: 'boolean',
default: 'false',
hrefLang: {
type: 'string',
},
target: {
type: 'enum',
values: ['HTMLAttributeAnchorTarget'],
},
rel: {
type: 'string',
},
matchStrategy: {
type: 'enum',
values: ['exact', 'prefix'],
},
onHoverStart: {
type: 'enum',
values: [`(e: HoverEvent) => void`],
},
onHoverEnd: {
type: 'enum',
values: [`(e: HoverEvent) => void`],
},
onHoverChange: {
type: 'enum',
values: [`(isHovering: boolean) => void`],
},
...childrenPropDefs,
...classNamePropDefs,
@@ -62,24 +73,50 @@ export const tabsPanelPropDefs: Record<string, PropDef> = {
export const tabsUsageSnippet = `import { Tabs } from '@backstage/ui';
<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Tab>Tab 3</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>Content for Tab 1</Tabs.Panel>
<Tabs.Panel>Content for Tab 2</Tabs.Panel>
<Tabs.Panel>Content for Tab 3</Tabs.Panel>
</Tabs.Root>`;
<Tabs>
<TabList>
<Tab id="tab-1">Tab 1</Tab>
<Tab id="tab-2">Tab 2</Tab>
<Tab id="tab-3">Tab 3</Tab>
</TabList>
<TabPanel id="tab-1">Content for Tab 1</TabPanel>
<TabPanel id="tab-2">Content for Tab 2</TabPanel>
<TabPanel id="tab-3">Content for Tab 3</TabPanel>
</Tabs>`;
export const tabsDefaultSnippet = `<Tabs.Root>
<Tabs.List>
<Tabs.Tab>Tab 1</Tabs.Tab>
<Tabs.Tab>Tab 2</Tabs.Tab>
<Tabs.Tab>Tab 3 With long title</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>Content for Tab 1</Tabs.Panel>
<Tabs.Panel>Content for Tab 2</Tabs.Panel>
<Tabs.Panel>Content for Tab 3</Tabs.Panel>
</Tabs.Root>`;
export const tabsDefaultSnippet = `import { Tabs } from '@backstage/ui';
<Tabs>
<TabList>
<Tab id="tab-1">Tab 1</Tab>
<Tab id="tab-2">Tab 2</Tab>
<Tab id="tab-3">Tab 3 With long title</Tab>
</TabList>
</Tabs>`;
export const tabsWithTabPanelsSnippet = `<Tabs>
<TabList>
<Tab id="settings">Settings</Tab>
<Tab id="profile">Profile</Tab>
<Tab id="preferences">Preferences</Tab>
</TabList>
<TabPanel id="settings">Settings panel content goes here</TabPanel>
<TabPanel id="profile">Profile panel content goes here</TabPanel>
<TabPanel id="preferences">Preferences panel content goes here</TabPanel>
</Tabs>`;
export const tabsWithLinksSnippet = `<Tabs>
<TabList>
<Tab id="tab-1" href="/tab-1">Tab 1</Tab>
<Tab id="tab-2" href="/tab-2">Tab 2</Tab>
<Tab id="tab-3" href="/tab-3">Tab 3 With long title</Tab>
</TabList>
</Tabs>`;
export const tabsWithDeeplyNestedRoutesSnippet = `<Tabs>
<TabList>
<Tab id="home" href="/home">Home</Tab>
<Tab id="catalog" href="/catalog" matchStrategy="prefix">Catalog</Tab>
<Tab id="mentorship" href="/mentorship" matchStrategy="prefix">Mentorship</Tab>
</TabList>
</Tabs>`;
+5 -1
View File
@@ -1,6 +1,7 @@
:root {
--bg: #f8f8f8;
--bg: #f4f4f4;
--panel: #fff;
--panel-hover: #fafafa;
--primary: #000;
--secondary: #929292;
--action: #fff;
@@ -13,17 +14,20 @@
--font-weight-regular: 400;
--font-weight-bold: 600;
--border: #e5e5e5;
--border2: #cdcdcd;
--surface-1: #f4f4f4;
}
[data-theme='dark'] {
--bg: #000;
--panel: #181818;
--panel-hover: #202020;
--primary: #fff;
--secondary: #818181;
--action: #202020;
--link: #4856e8;
--border: #2a2a2a;
--border2: #2a2a2a;
--surface-1: #282828;
}
+31
View File
@@ -0,0 +1,31 @@
'use client';
import { usePathname } from 'next/navigation';
import { getPageName } from '@/utils/getPageName';
export function useCurrentPage(): string | null {
const pathname = usePathname();
// Handle root path
if (pathname === '/') {
return 'Getting Started';
}
// Extract slug from various path patterns
const patterns = [
/^\/components\/(.+)$/, // /components/button
/^\/theme\/(.+)$/, // /theme/typography
/^\/about$/, // /about
/^\/changelog$/, // /changelog
];
for (const pattern of patterns) {
const match = pathname.match(pattern);
if (match) {
const slug = match[1] || pathname.slice(1); // Use full path for exact matches
return getPageName(slug);
}
}
return null;
}
+39 -213
View File
@@ -24,220 +24,46 @@ import * as SearchFieldStories from '../../../packages/ui/src/components/SearchF
import * as TooltipStories from '../../../packages/ui/src/components/Tooltip/Tooltip.stories';
import * as SkeletonStories from '../../../packages/ui/src/components/Skeleton/Skeleton.stories';
import * as CardStories from '../../../packages/ui/src/components/Card/Card.stories';
import * as HeaderStories from '../../../packages/ui/src/components/Header/Header.stories';
import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories';
export const BoxSnippet = ({ story }: { story: keyof typeof BoxStories }) => {
const stories = composeStories(BoxStories);
const StoryComponent = stories[story as keyof typeof stories];
// Helper function to create snippet components
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createSnippetComponent = (stories: any) => {
return function SnippetComponent({ story }: { story: string }) {
const composedStories = composeStories(stories);
const StoryComponent = composedStories[
story as keyof typeof composedStories
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any;
return StoryComponent ? <StoryComponent /> : null;
return StoryComponent ? <StoryComponent /> : null;
};
};
export const ButtonSnippet = ({
story,
}: {
story: keyof typeof ButtonStories;
}) => {
const stories = composeStories(ButtonStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const ButtonIconSnippet = ({
story,
}: {
story: keyof typeof ButtonIconStories;
}) => {
const stories = composeStories(ButtonIconStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const ButtonLinkSnippet = ({
story,
}: {
story: keyof typeof ButtonLinkStories;
}) => {
const stories = composeStories(ButtonLinkStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const CheckboxSnippet = ({
story,
}: {
story: keyof typeof CheckboxStories;
}) => {
const stories = composeStories(CheckboxStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const ContainerSnippet = ({
story,
}: {
story: keyof typeof ContainerStories;
}) => {
const stories = composeStories(ContainerStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const FlexSnippet = ({ story }: { story: keyof typeof FlexStories }) => {
const stories = composeStories(FlexStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const TextFieldSnippet = ({
story,
}: {
story: keyof typeof TextFieldStories;
}) => {
const stories = composeStories(TextFieldStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const GridSnippet = ({ story }: { story: keyof typeof GridStories }) => {
const stories = composeStories(GridStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const IconSnippet = ({ story }: { story: keyof typeof IconStories }) => {
const stories = composeStories(IconStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const TextSnippet = ({ story }: { story: keyof typeof TextStories }) => {
const stories = composeStories(TextStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const SelectSnippet = ({
story,
}: {
story: keyof typeof SelectStories;
}) => {
const stories = composeStories(SelectStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const MenuSnippet = ({ story }: { story: keyof typeof MenuStories }) => {
const stories = composeStories(MenuStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const LinkSnippet = ({ story }: { story: keyof typeof LinkStories }) => {
const stories = composeStories(LinkStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const AvatarSnippet = ({
story,
}: {
story: keyof typeof AvatarStories;
}) => {
const stories = composeStories(AvatarStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const CollapsibleSnippet = ({
story,
}: {
story: keyof typeof CollapsibleStories;
}) => {
const stories = composeStories(CollapsibleStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const TabsSnippet = ({ story }: { story: keyof typeof TabsStories }) => {
const stories = composeStories(TabsStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const SwitchSnippet = ({
story,
}: {
story: keyof typeof SwitchStories;
}) => {
const stories = composeStories(SwitchStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const RadioGroupSnippet = ({
story,
}: {
story: keyof typeof RadioGroupStories;
}) => {
const stories = composeStories(RadioGroupStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const SearchFieldSnippet = ({
story,
}: {
story: keyof typeof SearchFieldStories;
}) => {
const stories = composeStories(SearchFieldStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const TooltipSnippet = ({
story,
}: {
story: keyof typeof TooltipStories;
}) => {
const stories = composeStories(TooltipStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const SkeletonSnippet = ({
story,
}: {
story: keyof typeof SkeletonStories;
}) => {
const stories = composeStories(SkeletonStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
export const CardSnippet = ({ story }: { story: keyof typeof CardStories }) => {
const stories = composeStories(CardStories);
const StoryComponent = stories[story as keyof typeof stories];
return StoryComponent ? <StoryComponent /> : null;
};
// Create snippet components using the helper function
export const BoxSnippet = createSnippetComponent(BoxStories);
export const ButtonSnippet = createSnippetComponent(ButtonStories);
export const ButtonIconSnippet = createSnippetComponent(ButtonIconStories);
export const ButtonLinkSnippet = createSnippetComponent(ButtonLinkStories);
export const CheckboxSnippet = createSnippetComponent(CheckboxStories);
export const ContainerSnippet = createSnippetComponent(ContainerStories);
export const GridSnippet = createSnippetComponent(GridStories);
export const IconSnippet = createSnippetComponent(IconStories);
export const TextFieldSnippet = createSnippetComponent(TextFieldStories);
export const TextSnippet = createSnippetComponent(TextStories);
export const FlexSnippet = createSnippetComponent(FlexStories);
export const SelectSnippet = createSnippetComponent(SelectStories);
export const MenuSnippet = createSnippetComponent(MenuStories);
export const LinkSnippet = createSnippetComponent(LinkStories);
export const AvatarSnippet = createSnippetComponent(AvatarStories);
export const CollapsibleSnippet = createSnippetComponent(CollapsibleStories);
export const RadioGroupSnippet = createSnippetComponent(RadioGroupStories);
export const TabsSnippet = createSnippetComponent(TabsStories);
export const SwitchSnippet = createSnippetComponent(SwitchStories);
export const SearchFieldSnippet = createSnippetComponent(SearchFieldStories);
export const TooltipSnippet = createSnippetComponent(TooltipStories);
export const SkeletonSnippet = createSnippetComponent(SkeletonStories);
export const CardSnippet = createSnippetComponent(CardStories);
export const HeaderSnippet = createSnippetComponent(HeaderStories);
export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories);
+15 -5
View File
@@ -9,6 +9,10 @@ export const overview: Page[] = [
title: 'Getting Started',
slug: '',
},
{
title: 'Installation',
slug: 'install',
},
{
title: 'About',
slug: 'about',
@@ -19,7 +23,7 @@ export const overview: Page[] = [
},
];
export const theme: Page[] = [
export const coreConcepts: Page[] = [
{
title: 'Iconography',
slug: 'iconography',
@@ -36,10 +40,6 @@ export const theme: Page[] = [
title: 'Theming',
slug: 'theming',
},
{
title: 'Typography',
slug: 'typography',
},
];
export const layoutComponents: Page[] = [
@@ -101,6 +101,16 @@ export const components: Page[] = [
slug: 'collapsible',
status: 'alpha',
},
{
title: 'Header',
slug: 'header',
status: 'alpha',
},
{
title: 'HeaderPage',
slug: 'header-page',
status: 'alpha',
},
{
title: 'Icon',
slug: 'icon',
+29
View File
@@ -0,0 +1,29 @@
import { overview, coreConcepts, components, layoutComponents } from './data';
export function getPageName(slug: string): string | null {
// Search in overview pages
const overviewPage = overview.find(p => p.slug === slug);
if (overviewPage) {
return overviewPage.title;
}
// Search in core concepts array
const coreConcept = coreConcepts.find(c => c.slug === slug);
if (coreConcept) {
return coreConcept.title;
}
// Search in components array
const component = components.find(c => c.slug === slug);
if (component) {
return component.title;
}
// Search in layoutComponents array
const layoutComponent = layoutComponents.find(c => c.slug === slug);
if (layoutComponent) {
return layoutComponent.title;
}
return null;
}
+21 -1
View File
@@ -2,9 +2,29 @@ import type { Breakpoint } from '@backstage/ui/src/types';
const breakpoints = ['initial', 'xs', 'sm', 'md', 'lg', 'xl'] as Breakpoint[];
export type ComplexTypeDef = {
name: string;
properties: Record<
string,
{
type: string;
required?: boolean;
description?: string;
}
>;
};
export type PropDef = {
type: 'string' | 'enum' | 'enum | string' | 'number' | 'boolean';
type:
| 'string'
| 'enum'
| 'enum | string'
| 'number'
| 'boolean'
| 'spacing'
| 'complex';
values?: string | string[];
complexType?: ComplexTypeDef;
default?: string;
required?: boolean;
responsive?: boolean;
@@ -169,27 +169,74 @@ export const WithOptions: Story = {
};
export const WithCustomActions: Story = {
args: {},
decorators: [withRouter],
render: args => (
<Header
{...args}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
/>
),
};
export const WithAllOptions: Story = {
args: {
customActions: <Button>Custom action</Button>,
title: 'My plugin',
titleLink: '/',
menuItems,
},
decorators: [withRouter],
render: WithCustomActions.render,
};
export const WithBreadcrumbs: Story = {
args: {
breadcrumbs,
tabs,
},
decorators: [withRouter],
};
export const WithAllComponents: Story = {
export const WithAllOptionsAndTabs: Story = {
args: {
menuItems,
...WithAllOptions.args,
tabs,
breadcrumbs,
},
decorators: [withRouter],
render: WithAllOptions.render,
};
export const WithHeaderPage: Story = {
args: {
...WithAllOptionsAndTabs.args,
},
decorators: [withRouter],
render: args => (
<>
<Header
{...args}
customActions={
<>
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
</>
}
/>
<HeaderPage
title="Page title"
menuItems={args.menuItems}
tabs={tabs2}
customActions={<Button>Custom action</Button>}
/>
</>
),
};
export const WithLayout: Story = {
@@ -95,10 +95,10 @@ export const WithMockedURLTab2: Story = {
</TabList>
</Tabs>
<Box mt="6" pl="2">
<Text>
<Text as="p">
Current URL is mocked to be: <strong>/tab2</strong>
</Text>
<Text>
<Text as="p">
Notice how the "Tab 2" tab is selected (highlighted) because it
matches the current path.
</Text>
@@ -273,14 +273,16 @@ export const PrefixMatchingDeepNesting: Story = {
</TabList>
</Tabs>
<Box mt="6" pl="2">
<Text>
<Text as="p">
Current URL: <strong>/catalog/users/john/details</strong>
</Text>
<Text>
<Text as="p">
The "Catalog" tab is active because it uses prefix matching and the
URL starts with "/catalog".
</Text>
<Text>This works for any level of nesting under "/catalog".</Text>
<Text as="p">
This works for any level of nesting under "/catalog".
</Text>
</Box>
</MemoryRouter>
),