Merge pull request #31622 from backstage/bui-docs-updates
BUI Docs updates
This commit is contained in:
@@ -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({});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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).
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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'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,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.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -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: {
|
||||
+2
-2
@@ -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
Reference in New Issue
Block a user