Merge pull request #30618 from backstage/docs-ui
Docs UI - Improve layout and add missing pages
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
@@ -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).
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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's father is my father'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's father is my father'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's father is my father'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's father is my father'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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>}
|
||||
/>`;
|
||||
@@ -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>}
|
||||
/>`;
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user