Merge pull request #31622 from backstage/bui-docs-updates

BUI Docs updates
This commit is contained in:
Charles de Dreuille
2025-11-05 11:00:00 +00:00
committed by GitHub
112 changed files with 1791 additions and 1427 deletions
+6
View File
@@ -1,4 +1,6 @@
import createMDX from '@next/mdx';
import path from 'path';
import { fileURLToPath } from 'url';
const nextConfig = {
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
@@ -13,6 +15,10 @@ const nextConfig = {
// These are type-level conflicts that don't affect runtime behavior
ignoreBuildErrors: true,
},
outputFileTracingRoot: path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
),
};
const withMDX = createMDX({});
+2 -2
View File
@@ -7,7 +7,7 @@
"build": "next build",
"lint": "next lint",
"prestart": "yarn sync:css",
"start": "concurrently \"yarn sync:css:watch\" \"next dev\"",
"start": "next dev",
"sync:css": "node scripts/sync-css.js",
"sync:css:watch": "node scripts/sync-css.js --watch"
},
@@ -26,6 +26,7 @@
"@storybook/react": "^8.6.12",
"@uiw/codemirror-themes": "^4.23.7",
"@uiw/react-codemirror": "^4.23.7",
"clsx": "^2.1.1",
"html-react-parser": "^5.2.5",
"motion": "^12.4.1",
"next": "15.4.7",
@@ -43,7 +44,6 @@
"@types/react": "19.1.9",
"@types/react-dom": "19.1.7",
"chokidar": "^3.6.0",
"concurrently": "^8.2.2",
"eslint": "^8",
"eslint-config-next": "15.3.4",
"lightningcss": "^1.28.2",
-32
View File
@@ -1,32 +0,0 @@
# About Backstage UI
Backstage UI is a design system created specifically for Backstage, built with React, TypeScript, and vanilla CSS.
This open-source library is hosted in the Backstage monorepo. While it can be used in other projects, Backstage UI
is designed to deliver a consistent, accessible, and extensible experience tailored to Backstage users.
## Philosophy
Backstage empowers product teams to build software faster and with greater quality. Its extensibility,
however, required us to rethink how to deliver a consistent and accessible user experience. Our goal is
to enable plugin creators to design plugins that seamlessly integrate with Backstage's look and feel while
still allowing customization to match individual brands.
Instead of reinventing the wheel, we chose to focus on layout and styling while leveraging existing headless
component libraries for functionality. This approach allows us to dedicate our efforts to creating a cohesive
and flexible theming system.
## Team
Backstage UI is designed and maintained primarily by Spotify's Backstage team, leveraging Spotify's expertise in
crafting high-quality design and technology. Drawing from our experience in building reliable and intuitive
user experiences for the music industry, we've created a design system that looks great and works seamlessly.
## Community
Backstage UI is an open-source project and we welcome contributions from the community. If you are interested in
contributing to Backstage UI, please read our [contributing guide](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)
and our [code of conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md).
## License
Backstage UI is licensed under the Apache 2.0 license. See the [LICENSE](https://github.com/backstage/backstage/blob/master/LICENSE) file for more details.
+1 -3
View File
@@ -7,9 +7,7 @@ export default async function Page({
}) {
const { slug } = await params;
const { default: Component } = await import(
`@/content/components/${slug}.mdx`
);
const { default: Component } = await import(`@/content/${slug}.mdx`);
return <Component />;
}
+173
View File
@@ -0,0 +1,173 @@
import { ComponentCards, ComponentCard } from '@/components/ComponentCards';
import { LayoutComponents } from '@/components/LayoutComponents';
import { CodeBlock } from '@/components/CodeBlock';
# Components
## Layout Components
We built a couple of layout components to help you build responsive elements
that will be consistent with the rest of your Backstage instance. These
components are opinionated and use TypeScript to ensure that the props you
provide are the ones coming from the theme.
<CodeBlock
title="Layout components"
code={`<Flex direction="column" gap="4">
<Box>Hello World</Box>
<Inline gap="sm">
<Box>Project 1</Box>
<Box>Project 2</Box>
</Inline>
</Flex>
`}
/>
<LayoutComponents />
## Components
### Actions
<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>
### Content display
<ComponentCards>
<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>
### Selection and inputs
<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="PasswordField"
description="A password field component to help you input passwords."
href="/components/password-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>
### Navigation
<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>
### Images and icons
<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>
### Feedback indicators
<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>
-52
View File
@@ -1,52 +0,0 @@
import { CodeBlock } from '@/components/CodeBlock';
import { Banner } from '@/components/Banner';
import { snippet } from './snippets';
# How to install Backstage UI
## How to import BUI's global styles
Backstage UI works by importing a global CSS file at the root of your application. This file includes all the default styles for the components.
First, you'll need to install the package using a package manager. For example, if you're using Yarn:
<CodeBlock
lang="shell"
title="Run this command in your `packages/app` directory"
code={`yarn add @backstage/ui`}
/>
<CodeBlock
lang="tsx"
title="Add this line to `packages/app/src/index.tsx`"
code={`import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@backstage/ui/css/styles.css'; // [!code ++]
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);`}
/>
<Banner
text="Import these styles only once at your application root. Plugin developers should skip this step to avoid conflicts."
variant="warning"
/>
## How to use BUI components
As a plugin maintainer, you can use BUI components in your plugin. As mentioned above, you should not import the styles
again in your plugin as this will be handled at the root of your application. To get started, just add the library to
your plugin and import the components you need.
<CodeBlock
lang="shell"
title="Run this command in your `packages/[your-plugin]` directory"
code={`yarn add @backstage/ui`}
/>
<CodeBlock lang="tsx" title="Let's get started 🚀" code={snippet} />
## 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).
+32 -14
View File
@@ -1,28 +1,46 @@
.container {
position: relative;
z-index: 20;
width: calc(100% - 64px);
margin-inline: 0 16px;
margin-left: 32px;
margin-right: 32px;
margin-top: 112px;
margin-bottom: 48px;
padding-inline: 40px;
padding-block: 48px;
background-color: var(--panel);
border-radius: 0.5rem;
padding-inline: 24px;
}
.contentWrapper {
display: flex;
width: 100%;
max-width: 1200px;
margin: 0 auto;
justify-content: center;
flex-direction: column;
}
.content {
width: 100%;
max-width: 960px;
margin: 0 auto;
display: flex;
gap: 40px;
}
.contentInner {
flex: 1;
min-width: 0;
overflow-x: hidden;
}
.toc {
display: none;
}
@media (min-width: 768px) {
.container {
width: calc(100% - 332px - 40px);
margin-left: 332px;
width: calc(100% - 260px);
margin-left: 260px;
margin-right: 40px;
}
}
@media (min-width: 1280px) {
.toc {
display: block;
width: 240px;
flex-shrink: 0;
}
}
+13 -5
View File
@@ -1,9 +1,10 @@
import type { Metadata } from 'next';
import { Sidebar } from '../components/Sidebar';
import { Sidebar } from '@/components/Sidebar/Sidebar';
import { Toolbar } from '@/components/Toolbar';
import { StickyHeader } from '../components/StickyHeader/StickyHeader';
import { Providers } from './providers';
import { CustomTheme } from '@/components/CustomTheme';
import { TableOfContents } from '@/components/TableOfContents';
import { MobileBottomNav } from '@/components/MobileBottomNav';
import styles from './layout.module.css';
import '../css/globals.css';
@@ -51,11 +52,18 @@ export default async function RootLayout({
<body>
<Providers>
<Sidebar />
<Toolbar version={packageVersion} />
<StickyHeader />
<div className={styles.container}>
<div className={styles.content}>{children}</div>
<div className={styles.contentWrapper}>
<Toolbar version={packageVersion} />
<div className={styles.content}>
<div className={styles.contentInner}>{children}</div>
<aside className={styles.toc}>
<TableOfContents />
</aside>
</div>
</div>
</div>
<MobileBottomNav />
<CustomTheme />
</Providers>
</body>
-42
View File
@@ -1,42 +0,0 @@
import { LayoutComponents } from '@/components/LayoutComponents';
import { CodeBlock } from '@/components/CodeBlock';
# Layout
Backstage UI is made for extensibility. We built this library to make it easy for any
Backstage plugin creator to be able to build their ideas at speed ensuring
consistency across the rest of your ecosystem. Each component is designed to
be editable to match your need but sometimes you want to have more control
over the layout of your page. To help you with that, we created a set of
layout components that you can use to build your own layouts. All of these
components are built to extend on our theming system, making it easy for you
to build your own layouts. Sometimes these components are not enough so we
created a set of helpers to be used with any CSS-in-JS library.
## Layout Components
We built a couple of layout components to help you build responsive elements
that will be consistent with the rest of your Backstage instance. These
components are opinionated and use TypeScript to ensure that the props you
provide are the ones coming from the theme.
<CodeBlock
title="Layout components"
code={`<Flex direction="column" gap="4">
<Box>Hello World</Box>
<Inline gap="sm">
<Box>Project 1</Box>
<Box>Project 2</Box>
</Inline>
</Flex>
`}
/>
<LayoutComponents />
## Layout Helpers
Sometimes you want to use global tokens dynamically outside of React
components. To help you with that we would like to provide a set of helpers
that you can use in your code. These helpers are not available just yet but we
are working on it.
+65 -161
View File
@@ -1,179 +1,83 @@
import { ComponentCards, ComponentCard } from '@/components/ComponentCards';
import { CodeBlock } from '@/components/CodeBlock';
import { Banner } from '@/components/Banner';
import { snippet } from './snippets';
## Welcome to Backstage UI, the new design library for Backstage plugins.
# Welcome to Backstage UI
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.
Backstage UI is a design system created specifically for Backstage, built with React, TypeScript, and vanilla CSS.
This open-source library is hosted in the Backstage monorepo. While it can be used in other projects, Backstage UI
is designed to deliver a consistent, accessible, and extensible experience tailored to Backstage users.
### Actions
## Import BUI's global styles
<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>
Backstage UI works by importing a global CSS file at the root of your application. This file includes all the default styles for the components.
First, you'll need to install the package using a package manager. For example, if you're using Yarn:
### Layout
<CodeBlock
lang="shell"
title="Run this command in your `packages/app` directory"
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>
<CodeBlock
lang="tsx"
title="Add this line to `packages/app/src/index.tsx`"
code={`import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@backstage/ui/css/styles.css'; // [!code ++]
### Selection and inputs
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);`}
/>
<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="PasswordField"
description="A password field component to help you input passwords."
href="/components/password-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>
<Banner
text="Import these styles only once at your application root. Plugin developers should skip this step to avoid conflicts."
variant="warning"
/>
### Navigation
## Use BUI components
<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>
As a plugin maintainer, you can use BUI components in your plugin. As mentioned above, you should not import the styles
again in your plugin as this will be handled at the root of your application. To get started, just add the library to
your plugin and import the components you need.
### Images and icons
<CodeBlock
lang="shell"
title="Run this command in your `packages/[your-plugin]` directory"
code={`yarn add @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>
### Feedback indicators
<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>
<CodeBlock lang="tsx" title="Let's get started 🚀" code={snippet} />
## Support
Now that you have the basics down, you can start building your plugin using the new design system.
Please familiarise yourself first with our theming principles. This will help you understand the core concepts of the design system.
If you have any questions, please reach out to us on [Discord](https://discord.gg/MUpMjP2).
## Philosophy
Backstage empowers product teams to build software faster and with greater quality. Its extensibility,
however, required us to rethink how to deliver a consistent and accessible user experience. Our goal is
to enable plugin creators to design plugins that seamlessly integrate with Backstage&apos;s look and feel while
still allowing customization to match individual brands.
Instead of reinventing the wheel, we chose to focus on layout and styling while leveraging existing headless
component libraries for functionality. This approach allows us to dedicate our efforts to creating a cohesive
and flexible theming system.
## Team
Backstage UI is designed and maintained primarily by Spotify&apos;s Backstage team, leveraging Spotify&apos;s expertise in
crafting high-quality design and technology. Drawing from our experience in building reliable and intuitive
user experiences for the music industry, we&apos;ve created a design system that looks great and works seamlessly.
## Community
Backstage UI is an open-source project and we welcome contributions from the community. If you are interested in
contributing to Backstage UI, please read our [contributing guide](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md)
and our [code of conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md).
## License
Backstage UI is licensed under the Apache 2.0 license. See the [LICENSE](https://github.com/backstage/backstage/blob/master/LICENSE) file for more details.
-107
View File
@@ -1,107 +0,0 @@
import * as Table from '@/components/Table';
import { Chip } from '@/components/Chip';
import { CodeBlock } from '@/components/CodeBlock';
# Responsive
Backstage UI is built on a responsive design system, meaning that the components are
designed to adapt to different screen sizes. By default we offer a set of
breakpoints that you can use to create responsive components.
## Breakpoints
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Breakpoint prefix</Table.HeaderCell>
<Table.HeaderCell>Minimum width</Table.HeaderCell>
<Table.HeaderCell>CSS</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
<Chip head>xs</Chip>
</Table.Cell>
<Table.Cell>
<Chip>0px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`{ ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>sm</Chip>
</Table.Cell>
<Table.Cell>
<Chip>640px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 640px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>md</Chip>
</Table.Cell>
<Table.Cell>
<Chip>768px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 768px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>lg</Chip>
</Table.Cell>
<Table.Cell>
<Chip>1024px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 1024px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>xl</Chip>
</Table.Cell>
<Table.Cell>
<Chip>1280px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 1280px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>2xl</Chip>
</Table.Cell>
<Table.Cell>
<Chip>1536px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 1536px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
## Responsive components
Backstage UI components are designed to be responsive, meaning that they will adapt
to different screen sizes. Not every component is responsive, but the ones
that are will have a prop to control the responsive behavior.
The behaviour is the same for each component. For each prop, instead of adding
the value, you add an object with the value and the breakpoint prefix.
<CodeBlock
code={`// Fixed value
<Button size="small">Button</Button>
// Responsive value
<Button size={{ xs: 'small', md: 'medium' }}>Button</Button>`} />
@@ -3,48 +3,109 @@ import * as Table from '@/components/Table';
import { Chip } from '@/components/Chip';
import { customTheme } from '@/snippets/code-snippets';
# Theming
# Tokens
Backstage UI theming is built entirely on CSS, without relying on any CSS-in-JS libraries.
At its core, it provides a solid default theme that is easily customizable using a
comprehensive set of CSS variables. Additionally, it enables anyone to adapt the design
to their specific needs. Each component comes with fixed class names, making customization
even more straightforward.
## Responsive breakpoints
## Light & Dark modes
Backstage UI is built on a responsive design system, meaning that the components are
designed to adapt to different screen sizes. By default we offer a set of
breakpoints that you can use to create responsive components.
By default, Backstage UI supports both light and dark modes using the `data-theme-mode` attribute.
The light theme is applied by default if no `data-theme-mode` attribute is specified. To create
a custom theme, you'll need to define both light and dark modes as outlined below. If
only one mode is defined, the other will fall back to the default theme.
<Table.Root>
<Table.Header>
<Table.HeaderRow>
<Table.HeaderCell>Breakpoint prefix</Table.HeaderCell>
<Table.HeaderCell>Minimum width</Table.HeaderCell>
<Table.HeaderCell>CSS</Table.HeaderCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
<Chip head>xs</Chip>
</Table.Cell>
<Table.Cell>
<Chip>0px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`{ ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>sm</Chip>
</Table.Cell>
<Table.Cell>
<Chip>640px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 640px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>md</Chip>
</Table.Cell>
<Table.Cell>
<Chip>768px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 768px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>lg</Chip>
</Table.Cell>
<Table.Cell>
<Chip>1024px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 1024px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>xl</Chip>
</Table.Cell>
<Table.Cell>
<Chip>1280px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 1280px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<Chip head>2xl</Chip>
</Table.Cell>
<Table.Cell>
<Chip>1536px</Chip>
</Table.Cell>
<Table.Cell>
<Chip>{`@media (min-width: 1536px) { ... }`}</Chip>
</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
## How to create your own theme
Backstage UI components are designed to be responsive, meaning that they will adapt
to different screen sizes. Not every component is responsive, but the ones
that are will have a prop to control the responsive behavior.
In our [started guide](/), we ask you to import two css files. The `core.css` file includes
the default set of variables. We recommend to keep this file in place and add your own theme
on top of it. `core.css` also include an opinionated reset. If you decided to remove `core.css`
you will have to provide your own reset css.
The behaviour is the same for each component. For each prop, instead of adding
the value, you add an object with the value and the breakpoint prefix.
Here's an example of how your theme.css file should look like:
<CodeBlock
code={`// Fixed value
<CodeBlock lang="css" code={customTheme} />
<Button size="small">Button</Button>
## CSS class name structure
// Responsive value
All Backstage UI components come with a set of CSS classes that you can use to style them. To make it
easier to identify the class name you can use, we use a specific structure for the class names.
<Button size={{ xs: 'small', md: 'medium' }}>Button</Button>`} />
<img
src="/css-classname-structure.png"
style={{ width: '100%', marginBottom: '32px', marginTop: '16px' }}
/>
Every component has a unique prefix `.bui-` followed by the component name. Component props
are represented using the `data-` attribute. That way, class names are easily identifiable.
## Available CSS variables
### Base colors
## Base colors
These colors are used for special purposes like ring, scrollbar, ...
@@ -123,7 +184,7 @@ These colors are used for special purposes like ring, scrollbar, ...
</Table.Body>
</Table.Root>
### Core background colors
## Core background colors
These colors are used for the background of your application. We are mostly using for now a
single elevated background for panels. `--bui-bg` should mostly use as the main background
@@ -224,7 +285,7 @@ color of your app.
</Table.Body>
</Table.Root>
### Foreground colors
## Foreground colors
Foreground colours are meant to work in pair with a background colours. Typically this would work
for icons, texts, shapes, ... Use a matching name to know what foreground color to use. These colors
@@ -329,7 +390,7 @@ are prefixed with `fg` to make it easier to identify.
</Table.Body>
</Table.Root>
### Border colors
## Border colors
These border colors are mostly meant to be used as borders on top of any components with
low contrast to help as a separator with the different background colors.
@@ -391,7 +452,7 @@ low contrast to help as a separator with the different background colors.
</Table.Body>
</Table.Root>
### Special colors
## Special colors
These colors are used for special purposes like ring, scrollbar, ...
@@ -424,7 +485,7 @@ These colors are used for special purposes like ring, scrollbar, ...
</Table.Body>
</Table.Root>
### Font families
## Font families
We have two fonts that we use across Backstage UI. The first one is the sans-serif
font that we use for the body of the application. The second one is the
@@ -453,7 +514,7 @@ monospace font that we use for code blocks and tables.
</Table.Body>
</Table.Root>
### Font weights
## Font weights
We have two font weights that we use across Backstage UI. Regular or Bold.
@@ -480,7 +541,7 @@ We have two font weights that we use across Backstage UI. Regular or Bold.
</Table.Body>
</Table.Root>
### Spacing
## Spacing
We built a spacing system based on a single value `--bui-space`. This value is
used to calculate the spacing for all the components. By default if you would like to
@@ -619,7 +680,7 @@ tokens for pretty much each spacing properties like padding, margin, gaps, ...
</Table.Body>
</Table.Root>
### Radius
## Radius
We use a radius system to make sure that the components have a consistent look and feel.
+14 -5
View File
@@ -9,15 +9,24 @@ export interface CodeBlockProps {
code?: string;
}
export async function CodeBlock({ lang = 'tsx', title, code }: CodeBlockProps) {
const out = await codeToHtml(code || '', {
lang: lang,
export async function CodeBlock(props: CodeBlockProps) {
const { lang = 'tsx', title, code } = props;
let out = await codeToHtml(code || '', {
lang,
transformers: [transformerNotationDiff({ matchAlgorithm: 'v3' })],
themes: {
light: 'min-light',
light: 'github-dark',
dark: 'min-dark',
},
transformers: [transformerNotationDiff({ matchAlgorithm: 'v3' })],
});
// Remove background-color from the pre tag to use our theme colors
out = out.replace(
/style="([^"]*?)background-color:[^;]+;?([^"]*?)"/g,
'style="$1$2"',
);
// Clean up empty style attributes
out = out.replace(/style=""\s?/g, '');
return <CodeBlockClient out={out} title={title} />;
}
@@ -1,33 +1,29 @@
.codeBlock {
border-radius: 4px;
border: 1px solid var(--border);
border-radius: 8px;
position: relative;
background: transparent;
overflow-x: auto;
font-family: var(--font-mono);
background-color: #fff;
background-color: var(--code-bg);
margin-bottom: 1rem;
}
[data-theme-mode='dark'] .codeBlock {
background-color: #121212;
}
.title {
border-bottom: 1px solid var(--border);
padding: 12px 20px;
font-size: 0.8125rem;
color: var(--primary);
color: #fff;
font-family: var(--font-regular);
font-weight: var(--font-weight-bold);
background-color: var(--code-title);
}
.title code {
background-color: var(--surface-1);
background-color: rgba(255, 255, 255, 0.1);
padding: 0.2rem 0.375rem;
border-radius: 0.25rem;
color: var(--secondary);
font-size: 0.8125rem;
color: #fff;
font-size: 0.75rem;
font-weight: var(--font-weight-bold);
}
.code {
@@ -28,7 +28,7 @@
display: flex;
flex-direction: column;
justify-content: flex-end;
background-color: var(--panel);
background-color: var(--bg);
border-radius: 8px;
border: 1px solid var(--border);
padding: 16px;
@@ -36,7 +36,7 @@
min-height: 120px;
&:hover {
background-color: var(--panel-hover);
background-color: var(--bg-hover);
}
}
@@ -5,7 +5,7 @@
right: 16px;
width: 240px;
height: 47px;
background-color: var(--panel);
background-color: var(--bg);
border-radius: 0.375rem;
border: 1px solid var(--border);
display: flex;
@@ -33,7 +33,7 @@
height: 46px;
flex-shrink: 0;
border-bottom: 1px solid var(--border);
background-color: var(--panel);
background-color: var(--bg);
display: flex;
justify-content: space-between;
align-items: center;
@@ -1,6 +1,6 @@
.container {
display: flex;
background-color: var(--panel);
background-color: var(--bg);
padding: 1.5rem;
border-radius: 0.25rem;
border: 1px solid var(--border);
@@ -19,7 +19,7 @@
& .content {
flex: none;
background-color: var(--panel);
background-color: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
width: 100%;
@@ -0,0 +1,40 @@
.icon {
display: block;
}
.topLine,
.middleLine,
.bottomLine {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Menu state (default) */
.topLine {
transform-origin: 10px 5px;
transform: rotate(0deg);
}
.middleLine {
opacity: 1;
transform-origin: 10px 10px;
}
.bottomLine {
transform-origin: 10px 15px;
transform: rotate(0deg);
}
/* Close state (when open) */
.icon[data-open='true'] .topLine {
transform-origin: 10px 5px;
transform: translateY(5px) rotate(45deg);
}
.icon[data-open='true'] .middleLine {
opacity: 0;
}
.icon[data-open='true'] .bottomLine {
transform-origin: 10px 15px;
transform: translateY(-5px) rotate(-45deg);
}
@@ -0,0 +1,56 @@
import styles from './AnimatedMenuIcon.module.css';
interface AnimatedMenuIconProps {
isOpen: boolean;
size?: number;
}
export const AnimatedMenuIcon = ({
isOpen,
size = 20,
}: AnimatedMenuIconProps) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={styles.icon}
data-open={isOpen}
>
<g className={styles.lines}>
<line
x1="3"
y1="5"
x2="17"
y2="5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
className={styles.topLine}
/>
<line
x1="3"
y1="10"
x2="17"
y2="10"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
className={styles.middleLine}
/>
<line
x1="3"
y1="15"
x2="17"
y2="15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
className={styles.bottomLine}
/>
</g>
</svg>
);
};
@@ -0,0 +1,205 @@
/* Mobile Bottom Navigation Island */
.mobileBottomNav {
display: block;
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
z-index: 300;
pointer-events: none;
@media (min-width: 768px) {
display: none;
}
}
.island {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 0 auto;
width: 50vw;
padding: 8px 12px 8px 12px;
border-radius: 32px;
pointer-events: auto;
background-color: #000;
}
:global([data-theme-mode='dark']) .island {
background-color: #fff;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
}
.menuButton {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background-color 0.2s ease-in-out;
/* White icons on light mode (black island) */
color: white;
&:hover {
background-color: rgba(255, 255, 255, 0.1);
}
}
:global([data-theme-mode='dark']) .menuButton {
/* Black icons on dark mode (white island) */
color: black;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
.buttonGroup {
display: flex;
align-items: center;
gap: 4px;
padding: 4px;
border-radius: 24px;
background-color: rgba(255, 255, 255, 0.2);
}
:global([data-theme-mode='dark']) .buttonGroup {
background-color: rgba(0, 0, 0, 0.08);
}
.buttonGroup button {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
padding: 6px 12px;
border-radius: 20px;
transition: background-color 0.2s ease-in-out;
/* White icons on light mode */
color: white;
&[data-selected] {
background-color: rgba(255, 255, 255, 0.2);
}
&:hover {
background-color: rgba(255, 255, 255, 0.15);
}
}
:global([data-theme-mode='dark']) .buttonGroup button {
/* Black icons on dark mode */
color: black;
&[data-selected] {
background-color: rgba(0, 0, 0, 0.2);
}
&:hover {
background-color: rgba(0, 0, 0, 0.15);
}
}
/* Mobile Menu Bottom Sheet */
.overlay {
position: fixed;
inset: 0;
z-index: 200;
background-color: rgba(38, 38, 38, 0.7);
display: flex;
align-items: flex-end;
justify-content: center;
&[data-entering] {
animation: overlayFadeIn 200ms ease-out;
}
&[data-exiting] {
animation: overlayFadeOut 200ms ease-in;
}
}
@keyframes overlayFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes overlayFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.modal {
width: calc(100% - 48px);
max-height: 50vh;
background-color: var(--bg);
border-radius: 16px;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
outline: none;
margin-bottom: 96px;
&[data-entering] {
animation: slideUp 300ms cubic-bezier(0.32, 0.72, 0, 1);
}
&[data-exiting] {
animation: slideDown 200ms cubic-bezier(0.32, 0.72, 0, 1);
}
}
@keyframes slideUp {
from {
transform: translateY(8%);
}
to {
transform: translateY(0);
}
}
@keyframes slideDown {
from {
transform: translateY(0);
}
to {
transform: translateY(8%);
}
}
.dialog {
display: flex;
flex-direction: column;
height: 100%;
max-height: 70vh;
outline: none;
overflow: hidden;
}
.menuContent {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 16px;
display: flex;
flex-direction: column;
gap: 2px;
min-height: 0;
}
@@ -0,0 +1,44 @@
'use client';
import { useState } from 'react';
import { RiMoonLine, RiSunLine } from '@remixicon/react';
import { Button, ToggleButton, ToggleButtonGroup } from 'react-aria-components';
import styles from './MobileBottomNav.module.css';
import { usePlayground } from '@/utils/playground-context';
import { MobileMenu } from './MobileMenu';
import { AnimatedMenuIcon } from './AnimatedMenuIcon';
export const MobileBottomNav = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { selectedTheme, setSelectedTheme } = usePlayground();
return (
<>
<div className={styles.mobileBottomNav}>
<div className={styles.island}>
<Button
className={styles.menuButton}
onPress={() => setIsMenuOpen(!isMenuOpen)}
aria-label={isMenuOpen ? 'Close menu' : 'Open menu'}
>
<AnimatedMenuIcon isOpen={isMenuOpen} size={20} />
</Button>
<ToggleButtonGroup
selectedKeys={selectedTheme}
onSelectionChange={setSelectedTheme}
disallowEmptySelection
className={styles.buttonGroup}
>
<ToggleButton id="light" aria-label="Light mode">
<RiSunLine size={20} />
</ToggleButton>
<ToggleButton id="dark" aria-label="Dark mode">
<RiMoonLine size={20} />
</ToggleButton>
</ToggleButtonGroup>
</div>
</div>
<MobileMenu isOpen={isMenuOpen} onClose={() => setIsMenuOpen(false)} />
</>
);
};
@@ -0,0 +1,32 @@
'use client';
import { Button, Dialog, Modal, ModalOverlay } from 'react-aria-components';
import { RiCloseLine } from '@remixicon/react';
import styles from './MobileBottomNav.module.css';
import { Navigation } from '@/components/Navigation';
interface MobileMenuProps {
isOpen: boolean;
onClose: () => void;
}
export const MobileMenu = ({ isOpen, onClose }: MobileMenuProps) => {
return (
<ModalOverlay
isOpen={isOpen}
onOpenChange={onClose}
className={styles.overlay}
isDismissable
>
<Modal className={styles.modal}>
<Dialog className={styles.dialog} aria-label="Navigation menu">
{({ close }) => (
<div className={styles.menuContent}>
<Navigation onLinkClick={close} />
</div>
)}
</Dialog>
</Modal>
</ModalOverlay>
);
};
@@ -0,0 +1 @@
export { MobileBottomNav } from './MobileBottomNav';
@@ -0,0 +1,100 @@
.topNav {
& ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 2px;
}
& li {
margin: 0;
padding: 0;
list-style: none;
}
& li div,
& li a {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
color: var(--primary);
text-decoration: none;
&:hover {
background-color: var(--action);
&[data-disabled='true'] {
background-color: transparent;
}
}
&[data-active='true'] {
background-color: var(--action);
&[data-disabled='true'] {
background-color: transparent;
}
}
&[data-disabled='true'] {
opacity: 0.5;
cursor: not-allowed;
}
}
}
.sectionTitle {
font-size: 0.6875rem;
font-weight: 500;
padding: 0 12px 4px;
color: var(--secondary);
margin-top: 40px;
text-transform: uppercase;
&:first-child {
margin-top: 12px;
}
}
.line {
text-decoration: none;
align-items: center;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 28px;
padding: 0 12px;
border-radius: 4px;
color: var(--primary);
flex-shrink: 0;
&:hover {
background-color: var(--action);
}
&.active {
background-color: var(--action);
}
&.active .lineTitle {
color: var(--primary);
}
}
.lineTitle {
font-size: 14px;
font-weight: 400;
color: var(--primary);
}
.lineStatus {
font-size: 14px;
color: var(--secondary);
}
@@ -0,0 +1,125 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Fragment } from 'react';
import clsx from 'clsx';
import {
RiCollageLine,
RiFileHistoryLine,
RiHazeLine,
RiPaletteLine,
RiServiceLine,
RiStackLine,
} from '@remixicon/react';
import { components, layoutComponents } from '@/utils/data';
import styles from './Navigation.module.css';
interface NavigationProps {
onLinkClick?: () => void;
}
const data = [
{
title: 'Layout Components',
content: layoutComponents,
url: '/components',
},
{
title: 'Components',
content: components,
url: '/components',
},
];
export const Navigation = ({ onLinkClick }: NavigationProps) => {
const pathname = usePathname();
return (
<>
<nav className={styles.topNav}>
<ul>
<li>
<Link href="/" data-active={pathname === '/'} onClick={onLinkClick}>
<RiHazeLine size={20} />
Get Started
</Link>
</li>
<li>
<Link
href="/tokens"
data-active={pathname === '/tokens'}
onClick={onLinkClick}
>
<RiPaletteLine size={20} />
Tokens
</Link>
</li>
<li>
<Link
href="/components"
data-active={pathname.startsWith('/components')}
onClick={onLinkClick}
>
<RiCollageLine size={20} />
Components
</Link>
</li>
<li>
<div data-disabled={true}>
<RiStackLine size={20} />
Recipes (Soon)
</div>
</li>
<li>
<div data-disabled={true}>
<RiServiceLine size={20} />
Guides (Soon)
</div>
</li>
<li>
<Link
href="/changelog"
data-active={pathname === '/changelog'}
onClick={onLinkClick}
>
<RiFileHistoryLine size={20} />
Changelog
</Link>
</li>
</ul>
</nav>
{data.map(section => {
return (
<Fragment key={section.title}>
<div className={styles.sectionTitle}>{section.title}</div>
{section.content.map(item => {
const isActive = pathname === `${section.url}/${item.slug}`;
return (
<Link
href={`${section.url}/${item.slug}`}
key={item.slug}
className={clsx(styles.line, {
[styles.active]: isActive,
})}
onClick={onLinkClick}
>
<div className={styles.lineTitle}>{item.title}</div>
<div className={styles.lineStatus}>
{item.status === 'alpha' && 'Alpha'}
{item.status === 'beta' && 'Beta'}
{item.status === 'inProgress' && 'In Progress'}
{item.status === 'stable' && 'Stable'}
{item.status === 'deprecated' && 'Deprecated'}
</div>
</Link>
);
})}
</Fragment>
);
})}
</>
);
};
@@ -0,0 +1 @@
export { Navigation } from './Navigation';
@@ -5,15 +5,12 @@ import styles from './PageTitle.module.css';
export const PageTitle = ({
title,
description,
type = 'component',
}: {
title: string;
type?: string;
description: string;
}) => {
return (
<div className={styles.container}>
<div className={styles.component}>{type}</div>
<MDXRemote
components={formattedMDXComponents}
source={`# ${title}\n\n${description}`}
@@ -25,7 +25,7 @@
border: 1px solid var(--border);
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
border-radius: 6px;
background: var(--panel);
background: var(--bg);
color: var(--primary);
outline: none;
/* max-width: 400px; */
@@ -43,7 +43,7 @@
.arrow svg {
display: block;
fill: var(--panel);
fill: var(--bg);
stroke: var(--border);
stroke-width: 1px;
transform: rotate(180deg);
@@ -6,17 +6,24 @@
.sidebar {
display: block;
position: fixed;
top: 112px;
top: 0;
left: 0;
width: 332px;
height: calc(100vh - 112px);
width: 260px;
height: 100vh;
color: var(--primary);
overflow: hidden;
}
}
.logoContainer {
height: 80px;
display: flex;
align-items: center;
padding-left: 32px;
}
.root {
height: 100%;
height: calc(100% - 80px);
}
.scrollbar {
@@ -56,67 +63,13 @@
}
.content {
padding: 0 20px 64px;
padding: 32px 20px 64px;
}
.menu {
display: flex;
flex-direction: row;
position: relative;
}
.section {
width: 100%;
display: flex;
flex-direction: column;
gap: 2px;
}
.sectionTitle {
font-size: 0.6875rem;
font-weight: 500;
padding: 0 12px 4px;
color: var(--secondary);
margin-top: 40px;
text-transform: uppercase;
&:first-child {
margin-top: 12px;
}
}
.line {
text-decoration: none;
align-items: center;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 26px;
padding: 0 12px;
border-radius: 4px;
&:hover {
background-color: var(--action);
}
}
.line.active {
background-color: var(--action);
}
.line.active .lineTitle {
color: var(--primary);
}
.lineTitle {
font-size: 14px;
font-weight: 400;
color: var(--primary);
}
.lineStatus {
font-size: 14px;
color: var(--secondary);
position: relative;
}
@@ -0,0 +1,28 @@
'use client';
import styles from './Sidebar.module.css';
import { ScrollArea } from '@base-ui-components/react/scroll-area';
import { Logo } from './Logo';
import { Navigation } from '@/components/Navigation';
export const Sidebar = () => {
return (
<div className={styles.sidebar}>
<div className={styles.logoContainer}>
<Logo />
</div>
<ScrollArea.Root className={styles.root}>
<ScrollArea.Viewport className={styles.viewport}>
<div className={styles.content}>
<div className={styles.menu}>
<Navigation />
</div>
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className={styles.scrollbar}>
<ScrollArea.Thumb className={styles.thumb} />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</div>
);
};
-104
View File
@@ -1,104 +0,0 @@
'use client';
import styles from './Sidebar.module.css';
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';
const data = [
{
title: 'Overview',
content: overview,
url: '',
},
{
title: 'Core Concepts',
content: coreConcepts,
url: '',
},
{
title: 'Layout Components',
content: layoutComponents,
url: '/components',
},
{
title: 'Components',
content: components,
url: '/components',
},
];
export const Sidebar = () => {
const pathname = usePathname();
const isPlayground = pathname.includes('/playground');
return (
<div className={styles.sidebar}>
<ScrollArea.Root className={styles.root}>
<ScrollArea.Viewport className={styles.viewport}>
<div className={styles.content}>
<div className={styles.menu}>
<motion.div
className={styles.section}
animate={{
x: isPlayground ? -10 : 0,
opacity: isPlayground ? 0 : 1,
visibility: isPlayground ? 'hidden' : 'visible',
}}
initial={{
x: isPlayground ? -10 : 0,
opacity: isPlayground ? 0 : 1,
visibility: isPlayground ? 'hidden' : 'visible',
}}
transition={{ duration: 0.2 }}
>
{data.map(section => {
return (
<Fragment key={section.title}>
<div className={styles.sectionTitle}>{section.title}</div>
{section.content.map(item => {
const isActive =
pathname === `${section.url}/${item.slug}`;
return (
<Link
href={`${section.url}/${item.slug}`}
key={item.slug}
className={`${styles.line} ${
isActive ? styles.active : ''
}`}
>
<div className={styles.lineTitle}>{item.title}</div>
<div className={styles.lineStatus}>
{item.status === 'alpha' && 'Alpha'}
{item.status === 'beta' && 'Beta'}
{item.status === 'inProgress' && 'In Progress'}
{item.status === 'stable' && 'Stable'}
{item.status === 'deprecated' && 'Deprecated'}
</div>
</Link>
);
})}
</Fragment>
);
})}
</motion.div>
</div>
</div>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar className={styles.scrollbar}>
<ScrollArea.Thumb className={styles.thumb} />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</div>
);
};
@@ -5,9 +5,9 @@
}
.preview {
border-radius: 4px;
border-radius: 8px;
box-shadow: inset 0 0 0 1px var(--border);
background-color: var(--bg);
background-color: var(--bui-bg);
padding: 1px;
position: relative;
}
@@ -1,98 +0,0 @@
.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;
}
}
@@ -1,50 +0,0 @@
'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>
);
};
@@ -17,7 +17,7 @@
padding: 12px 16px !important;
border: none !important;
text-align: left;
background-color: var(--panel) !important;
background-color: var(--bg) !important;
font-size: 16px;
& p {
@@ -0,0 +1,93 @@
.container {
position: sticky;
top: 24px;
max-height: calc(100vh - 48px);
overflow-y: auto;
padding: 16px 0;
}
.list {
list-style: none;
padding: 0;
margin: 0;
position: relative;
}
.list::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background-color: var(--border);
}
.indicator {
position: absolute;
left: 0;
width: 2px;
background-color: var(--primary);
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 1;
}
.item {
position: relative;
margin: 0;
}
.itemNested {
padding-left: 16px;
}
.link {
display: block;
width: 100%;
text-align: left;
background: none;
border: none;
padding: 6px 16px;
font-size: 0.875rem;
color: var(--text-secondary);
cursor: pointer;
transition: color 0.2s ease;
line-height: 1.4;
position: relative;
}
.link:hover {
color: var(--text);
}
.itemActive .link {
color: var(--primary);
font-weight: 500;
}
/* Hide scrollbar for webkit browsers */
.container::-webkit-scrollbar {
width: 4px;
}
.container::-webkit-scrollbar-track {
background: transparent;
}
.container::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}
.container::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
/* Responsive: hide on smaller screens */
@media (max-width: 1280px) {
.container {
display: none;
}
}
@@ -0,0 +1,132 @@
'use client';
import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
import styles from './TableOfContents.module.css';
interface Heading {
id: string;
text: string;
level: number;
}
export function TableOfContents() {
const [headings, setHeadings] = useState<Heading[]>([]);
const [activeId, setActiveId] = useState<string>('');
const [indicatorTop, setIndicatorTop] = useState<number>(0);
const [indicatorHeight, setIndicatorHeight] = useState<number>(0);
const pathname = usePathname();
useEffect(() => {
// Extract all H2 and H3 headings from the document
const elements = Array.from(
document.querySelectorAll('h2[id], h3[id]'),
) as HTMLHeadingElement[];
const headingData: Heading[] = elements.map(element => ({
id: element.id,
text: (element.textContent || '').replace('#', '').trim(),
level: parseInt(element.tagName.substring(1)),
}));
setHeadings(headingData);
// Set initial active heading (first visible heading or first heading)
if (headingData.length > 0) {
const viewportTop = window.scrollY + 100; // offset for header
const visibleHeading = elements.find(element => {
const rect = element.getBoundingClientRect();
return rect.top + window.scrollY >= viewportTop - 200;
});
setActiveId(visibleHeading?.id || headingData[0].id);
}
// Set up IntersectionObserver to track visible headings
const observerOptions = {
rootMargin: '-80px 0px -80% 0px',
threshold: 1,
};
const observerCallback = (entries: IntersectionObserverEntry[]) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
};
const observer = new IntersectionObserver(
observerCallback,
observerOptions,
);
elements.forEach(element => observer.observe(element));
return () => {
elements.forEach(element => observer.unobserve(element));
};
}, [pathname]);
// Update indicator position when activeId changes
useEffect(() => {
if (activeId) {
const activeElement = document.querySelector(
`[data-toc-id="${activeId}"]`,
) as HTMLElement;
if (activeElement) {
const list = activeElement.closest('ul');
if (list) {
const listRect = list.getBoundingClientRect();
const elementRect = activeElement.getBoundingClientRect();
setIndicatorTop(elementRect.top - listRect.top);
setIndicatorHeight(elementRect.height);
}
}
}
}, [activeId, headings]);
const handleClick = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Update URL without scrolling (already handled above)
window.history.pushState(null, '', `#${id}`);
}
};
if (headings.length === 0) {
return null;
}
return (
<nav className={styles.container}>
<ul className={styles.list}>
{indicatorHeight > 0 && (
<div
className={styles.indicator}
style={{
top: `${indicatorTop}px`,
height: `${indicatorHeight}px`,
}}
/>
)}
{headings.map((heading, index) => (
<li
key={`${heading.id}-${index}`}
data-toc-id={heading.id}
className={`${styles.item} ${
heading.level === 3 ? styles.itemNested : ''
} ${activeId === heading.id ? styles.itemActive : ''}`}
>
<button
onClick={() => handleClick(heading.id)}
className={styles.link}
>
{heading.text}
</button>
</li>
))}
</ul>
</nav>
);
}
@@ -0,0 +1 @@
export { TableOfContents } from './TableOfContents';
+220 -73
View File
@@ -1,76 +1,77 @@
.toolbar {
position: fixed;
display: flex;
top: 0;
left: 32px;
right: 32px;
height: 112px;
z-index: 10;
height: 80px;
align-items: center;
justify-content: space-between;
padding-left: 0.25rem;
}
margin-bottom: 24px;
.left {
/* width: 296px; */
display: flex;
align-items: center;
gap: 0.5rem;
padding-right: 20px;
}
.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;
align-items: center;
gap: 1rem;
}
}
@media (min-width: 768px) {
.toolbar {
@media (min-width: 768px) {
right: 40px;
}
}
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
}
.logoMobile {
display: block;
@media (min-width: 768px) {
display: none;
}
}
.breadcrumbDesktop {
display: none;
@media (min-width: 768px) {
display: flex;
align-items: center;
gap: 0.5rem;
}
}
.breadcrumbLink {
color: var(--secondary);
text-decoration: none;
cursor: pointer;
&:hover {
color: var(--primary);
text-decoration: underline;
transition: color 0.2s ease-in-out;
text-decoration-thickness: 1px;
text-underline-offset: 4px;
}
}
.breadcrumbSeparator {
color: var(--secondary);
flex-shrink: 0;
}
.breadcrumbCurrent {
color: var(--primary);
}
.actions {
display: none;
@media (min-width: 320px) {
display: flex;
display: flex;
align-items: center;
gap: 0.5rem;
}
}
@media (min-width: 820px) {
.right {
.content {
justify-content: space-between;
}
@@ -79,18 +80,164 @@
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;
.bubble {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg);
border: 1px solid var(--border);
border-radius: 32px;
padding-inline: 16px;
color: var(--primary);
font-size: 0.875rem;
font-weight: 500;
height: 32px;
cursor: pointer;
gap: 4px;
&[data-selected] {
background-color: var(--action);
color: var(--primary);
}
&:hover {
background-color: var(--action);
transition: background-color 0.2s ease-in-out;
}
&[data-hide-tablet] {
display: none;
}
@media (min-width: 1024px) {
&[data-hide-tablet] {
display: flex;
}
}
}
.buttonGroup {
display: flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
border-radius: 32px;
padding-inline: 4px;
height: 32px;
display: none;
@media (min-width: 480px) {
display: flex;
}
}
.buttonGroup button {
height: 24px;
background-color: var(--bg);
border: none;
border-radius: 28px;
padding-inline: 16px;
color: var(--primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&[data-selected] {
background-color: var(--action);
color: var(--primary);
}
&:hover {
background-color: var(--action);
transition: background-color 0.2s ease-in-out;
}
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: var(--bg);
color: var(--color-gray-900);
border: 1px solid var(--border);
padding-inline: 0.25rem;
transform-origin: var(--transform-origin);
transition: transform 150ms, opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-side='none'] {
transition: none;
transform: none;
opacity: 1;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow: 0px 10px 15px -3px var(--color-gray-200),
0px 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Popup[data-trigger='Select'] {
min-width: var(--trigger-width);
.ListBox {
display: block;
width: unset;
max-height: inherit;
min-height: unset;
border: none;
.react-aria-Header {
padding-left: 1.571rem;
}
}
.Item {
position: relative;
padding: 0 0.571rem 0 1.571rem;
height: 2rem;
display: flex;
align-items: center;
&[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;
}
}
}
+153 -37
View File
@@ -1,52 +1,168 @@
'use client';
import { RiGithubLine, RiNpmjsLine } from '@remixicon/react';
import { motion, useScroll, useTransform } from 'framer-motion';
import { useRef } from 'react';
import { Logo } from './Logo';
import { ThemeSelector } from './theme';
import { ThemeNameSelector } from './theme-name';
import {
RiArrowDownSLine,
RiArrowRightSLine,
RiGithubLine,
RiMoonLine,
RiSunLine,
} from '@remixicon/react';
import {
Button,
ListBox,
ListBoxItem,
Popover,
Select,
SelectValue,
ToggleButton,
ToggleButtonGroup,
} from 'react-aria-components';
import styles from './Toolbar.module.css';
import { usePlayground } from '@/utils/playground-context';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { components, layoutComponents } from '@/utils/data';
import { Logo } from '@/components/Sidebar/Logo';
interface ToolbarProps {
version: string;
}
export const Toolbar = ({ version }: ToolbarProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollY } = useScroll();
const themes = [
{ name: 'Backstage', value: 'backstage' },
{ name: 'Spotify', value: 'spotify' },
{ name: 'Custom theme', value: 'custom' },
];
// Transform scroll velocity to vertical movement
const y = useTransform(scrollY, [0, 100], [0, -20], {
clamp: false,
});
export const Toolbar = ({ version }: ToolbarProps) => {
const {
selectedTheme,
setSelectedTheme,
selectedThemeName,
setSelectedThemeName,
} = usePlayground();
const pathname = usePathname();
// Determine breadcrumb content based on current path
const getBreadcrumb = () => {
const allComponents = [...components, ...layoutComponents];
// Root page
if (pathname === '/') {
return { section: null, title: 'Getting Started' };
}
// Components index page
if (pathname === '/components') {
return { section: null, title: 'Components' };
}
// Component detail pages
if (pathname?.startsWith('/components/')) {
const slug = pathname.split('/components/')[1];
const component = allComponents.find(c => c.slug === slug);
return {
section: 'Components',
sectionLink: '/components',
title: component?.title || slug,
};
}
// Tokens page
if (pathname === '/tokens') {
return { section: null, title: 'Tokens' };
}
// Changelog page
if (pathname === '/changelog') {
return { section: null, title: 'Changelog' };
}
return { section: null, title: '' };
};
const breadcrumb = getBreadcrumb();
return (
<div className={styles.toolbar} ref={containerRef}>
<div className={styles.left}>
<Logo />
</div>
<motion.div className={styles.right} style={{ y }}>
<div className={styles.version}>Version {version} - Alpha</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 className={styles.toolbar}>
<div className={styles.breadcrumb}>
<div className={styles.logoMobile}>
<Logo />
</div>
</motion.div>
<div className={styles.breadcrumbDesktop}>
{breadcrumb.section && breadcrumb.sectionLink ? (
<>
<Link
href={breadcrumb.sectionLink}
className={styles.breadcrumbLink}
>
{breadcrumb.section}
</Link>
<RiArrowRightSLine
size={16}
className={styles.breadcrumbSeparator}
/>
<span className={styles.breadcrumbCurrent}>
{breadcrumb.title}
</span>
</>
) : (
<span className={styles.breadcrumbCurrent}>{breadcrumb.title}</span>
)}
</div>
</div>
<div className={styles.actions}>
<Select
defaultValue="backstage"
value={selectedThemeName}
onChange={setSelectedThemeName}
>
<Button className={styles.bubble}>
<SelectValue />
<RiArrowDownSLine aria-hidden="true" size={16} />
</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>
<a
href="https://www.npmjs.com/package/@backstage/ui"
target="_blank"
className={styles.bubble}
data-hide-tablet
>
Version {version}
</a>
<a
href="https://github.com/backstage/backstage/tree/master/packages/ui"
target="_blank"
className={styles.bubble}
data-hide-tablet
>
<RiGithubLine size={16} />
</a>
<ToggleButtonGroup
defaultSelectedKeys={['light']}
selectedKeys={selectedTheme}
onSelectionChange={setSelectedTheme}
disallowEmptySelection
className={styles.buttonGroup}
>
<ToggleButton id="light">
<RiSunLine aria-hidden="true" size={16} />
</ToggleButton>
<ToggleButton id="dark">
<RiMoonLine aria-hidden="true" size={16} />
</ToggleButton>
</ToggleButtonGroup>
</div>
</div>
);
};
@@ -1,110 +0,0 @@
.Select {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
height: 3rem;
padding-left: 1.25rem;
padding-right: 1rem;
margin: 0;
outline: 0;
border: 1px solid var(--color-gray-200);
border-radius: 3rem;
font-family: inherit;
font-size: 1rem;
line-height: 1.5rem;
color: var(--color-gray-900);
cursor: pointer;
user-select: none;
background-color: var(--action);
&:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
}
.Popup {
box-sizing: border-box;
padding-block: 0.25rem;
border-radius: 0.375rem;
background-color: var(--panel);
color: var(--color-gray-900);
border: 1px solid var(--border);
padding-inline: 0.25rem;
transform-origin: var(--transform-origin);
transition: transform 150ms, opacity 150ms;
&[data-starting-style],
&[data-ending-style] {
opacity: 0;
transform: scale(0.9);
}
&[data-side='none'] {
transition: none;
transform: none;
opacity: 1;
}
@media (prefers-color-scheme: light) {
outline: 1px solid var(--color-gray-200);
box-shadow: 0px 10px 15px -3px var(--color-gray-200),
0px 4px 6px -4px var(--color-gray-200);
}
@media (prefers-color-scheme: dark) {
outline: 1px solid var(--color-gray-300);
outline-offset: -1px;
}
}
.Popup[data-trigger='Select'] {
min-width: var(--trigger-width);
.ListBox {
display: block;
width: unset;
max-height: inherit;
min-height: unset;
border: none;
.react-aria-Header {
padding-left: 1.571rem;
}
}
.Item {
position: relative;
padding: 0 0.571rem 0 1.571rem;
height: 2rem;
display: flex;
align-items: center;
&[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;
}
}
}
@@ -1,44 +0,0 @@
'use client';
import {
Button,
ListBox,
ListBoxItem,
Popover,
Select,
SelectValue,
} from 'react-aria-components';
import styles from './theme-name.module.css';
import { usePlayground } from '@/utils/playground-context';
import { RiArrowDownSLine } from '@remixicon/react';
const themes = [
{ name: 'Backstage', value: 'backstage' },
{ name: 'Spotify', value: 'spotify' },
{ name: 'Custom theme', value: 'custom' },
];
export const ThemeNameSelector = () => {
const { selectedThemeName, setSelectedThemeName } = usePlayground();
return (
<Select
selectedKey={selectedThemeName || 'backstage'}
onSelectionChange={setSelectedThemeName}
>
<Button className={styles.Select}>
<SelectValue />
<RiArrowDownSLine aria-hidden="true" />
</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>
);
};
@@ -1,79 +0,0 @@
.tabs {
border-radius: 0.375rem;
width: 100%;
}
.tabsTheme {
width: 100px;
border-radius: 0.375rem;
}
.list {
display: flex;
position: relative;
z-index: 0;
gap: 0.25rem;
}
.tab {
display: flex;
align-items: center;
justify-content: center;
border: 0;
margin: 0;
outline: 0;
background: none;
appearance: none;
color: var(--secondary);
user-select: none;
flex: 1;
cursor: pointer;
width: 3rem;
height: 3rem;
&[data-selected] {
color: var(--primary);
& p {
color: var(--primary);
}
}
@media (hover: hover) {
&:hover {
color: var(--primary);
}
}
&:focus-visible {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0.25rem 0;
border-radius: 0.25rem;
outline: 2px solid var(--action);
outline-offset: -1px;
}
}
}
.tab p {
color: var(--secondary) !important;
}
.indicator {
position: absolute;
z-index: -1;
left: 0;
top: 50%;
translate: var(--active-tab-left) -50%;
width: var(--active-tab-width);
height: 3rem;
border-radius: 3rem;
background-color: var(--action);
transition-property: translate, width;
transition-duration: 200ms;
transition-timing-function: ease-in-out;
}
-28
View File
@@ -1,28 +0,0 @@
'use client';
import { Tabs } from '@base-ui-components/react/tabs';
import { usePlayground } from '@/utils/playground-context';
import styles from './theme.module.css';
import { RiMoonLine, RiSunLine } from '@remixicon/react';
export const ThemeSelector = () => {
const { selectedTheme, setSelectedTheme } = usePlayground();
return (
<Tabs.Root
className={styles.tabsTheme}
onValueChange={setSelectedTheme}
value={selectedTheme}
>
<Tabs.List className={styles.list}>
<Tabs.Tab className={styles.tab} value="light">
<RiSunLine aria-hidden="true" size={16} />
</Tabs.Tab>
<Tabs.Tab className={styles.tab} value="dark">
<RiMoonLine aria-hidden="true" size={16} />
</Tabs.Tab>
<Tabs.Indicator className={styles.indicator} />
</Tabs.List>
</Tabs.Root>
);
};
@@ -1,5 +1,5 @@
import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs';
import type { PropDef } from '../../utils/propDefs';
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const buttonLinkPropDefs: Record<string, PropDef> = {
variant: {
@@ -1,5 +1,5 @@
import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs';
import type { PropDef } from '../../utils/propDefs';
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
import type { PropDef } from '@/utils/propDefs';
export const buttonPropDefs: Record<string, PropDef> = {
variant: {
@@ -2,7 +2,7 @@ import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '../../utils/propDefs';
} from '@/utils/propDefs';
export const tablePropDefs: Record<string, PropDef> = {
selectionBehavior: {

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