diff --git a/docs-ui/src/app/components/page.mdx b/docs-ui/src/app/components/page.mdx index bd18005d19..399d9aafbf 100644 --- a/docs-ui/src/app/components/page.mdx +++ b/docs-ui/src/app/components/page.mdx @@ -1,173 +1,9 @@ -import { ComponentCards, ComponentCard } from '@/components/ComponentCards'; -import { LayoutComponents } from '@/components/LayoutComponents'; -import { CodeBlock } from '@/components/CodeBlock'; +import { ComponentGrid } from '@/components/ComponentGrid'; # Components -## Layout Components +Below is the full list of components available in the library. Each component is designed to be flexible, accessible, +and easy to integrate into your plugins. We are actively working on adding more components, so check back regularly +for updates. -We built a couple of layout components to help you build responsive elements -that will be consistent with the rest of your Backstage instance. These -components are opinionated and use TypeScript to ensure that the props you -provide are the ones coming from the theme. - - - Hello World - - Project 1 - Project 2 - - -`} -/> - - - -## Components - -### Actions - - - - - - - - -### Content display - - - - - - -### Selection and inputs - - - - - - - - - - - -### Navigation - - - - - - - - -### Images and icons - - - - - - -### Feedback indicators - - - - - - -### Typography - - - - + diff --git a/docs-ui/src/app/get-started/installation/page.mdx b/docs-ui/src/app/get-started/installation/page.mdx new file mode 100644 index 0000000000..cdb14d5924 --- /dev/null +++ b/docs-ui/src/app/get-started/installation/page.mdx @@ -0,0 +1,46 @@ +import { CodeBlock } from '@/components/CodeBlock'; +import { Banner } from '@/components/Banner'; +import { snippet } from './snippets'; + +# Installation + +## Import BUI's global styles + +Backstage UI works by importing a global CSS file at the root of your application. This file includes all the default styles for the components. +First, you'll need to install the package using a package manager. For example, if you're using Yarn: + + + +);`} +/> + + + +## Use BUI components + +As a plugin maintainer, you can use BUI components in your plugin. As mentioned above, you should not import the styles +again in your plugin as this will be handled at the root of your application. To get started, just add the library to +your plugin and import the components you need. + + + + diff --git a/docs-ui/src/app/get-started/installation/snippets.ts b/docs-ui/src/app/get-started/installation/snippets.ts new file mode 100644 index 0000000000..7e63005172 --- /dev/null +++ b/docs-ui/src/app/get-started/installation/snippets.ts @@ -0,0 +1,6 @@ +export const snippet = `import { Flex, Button, Text } from '@backstage/ui'; + + + Hello World + +;`; diff --git a/docs-ui/src/app/page.mdx b/docs-ui/src/app/page.mdx index 9a796b401b..712a5ac5c6 100644 --- a/docs-ui/src/app/page.mdx +++ b/docs-ui/src/app/page.mdx @@ -1,59 +1,78 @@ +import { ComponentGrid } from '@/components/ComponentGrid'; import { CodeBlock } from '@/components/CodeBlock'; -import { Banner } from '@/components/Banner'; -import { snippet } from './snippets'; +import { ColorFamily } from '@/components/ColorFamily'; +import { + surfacesSnippet, + adaptiveSnippet, + customCardSnippet, + customTokensSnippet, + colorPickerSnippet, +} from './snippets'; -# Welcome to Backstage UI +# Get Started with BUI Backstage UI is a design system created specifically for Backstage, built with React, TypeScript, and vanilla CSS. This open-source library is hosted in the Backstage monorepo. While it can be used in other projects, Backstage UI is designed to deliver a consistent, accessible, and extensible experience tailored to Backstage users. -## Import BUI's global styles +Backstage UI is installed by default on every instance of Backstage, so you can start using it right away. +If your setup doesn't include it yet, follow the [installation guide](/get-started/installation) to get started. -Backstage UI works by importing a global CSS file at the root of your application. This file includes all the default styles for the components. -First, you'll need to install the package using a package manager. For example, if you're using Yarn: +## Layout containers - +[`Box`](/components/box), [`Flex`](/components/flex), [`Grid`](/components/grid), and [`Card`](/components/card) are the foundation of every layout in Backstage UI. +Each one offers a set of utility props that map directly to our design tokens, so you can build consistent +layouts without writing any CSS. When nested, they also act as surfaces and automatically increment the +background depth so visual hierarchy is handled for you. );`} + title="Nested surfaces with automatic styling" + code={surfacesSnippet} /> - +## Adaptive components -## Use BUI components - -As a plugin maintainer, you can use BUI components in your plugin. As mentioned above, you should not import the styles -again in your plugin as this will be handled at the root of your application. To get started, just add the library to -your plugin and import the components you need. +Components like [`Card`](/components/card), [`Button`](/components/button), [`Text`](/components/text), and others are **adaptive components**. They +automatically adjust their colors, borders, and backgrounds to match the surface they live on. Drop a +[`Button`](/components/button) inside a [`Card`](/components/card) inside a [`Box`](/components/box) and each component styles itself appropriately +without any extra configuration. - +## The neutral scale background colors -## Support + -Now that you have the basics down, you can start building your plugin using the new design system. -Please familiarise yourself first with our theming principles. This will help you understand the core concepts of the design system. -If you have any questions, please reach out to us on [Discord](https://discord.gg/MUpMjP2). +## Creating custom components + +As much as possible we would like you to use components directly without creating custom components. If you need to create a custom component, you should use the components provided by Backstage UI. + + + +If you need to build custom components outside of BUI, you can use our [design tokens](/tokens) as CSS variables to ensure your styles stay consistent with the rest of the system. + + + +When building custom interactive components, we strongly recommend using [React Aria](https://react-spectrum.adobe.com/react-aria/) as your foundation. React Aria provides all the necessary accessibility features out of the box — keyboard navigation, focus management, ARIA attributes, and screen reader support — so you can focus on styling and logic without worrying about compliance. + + ## Philosophy diff --git a/docs-ui/src/app/snippets.ts b/docs-ui/src/app/snippets.ts index 7e63005172..e16463a085 100644 --- a/docs-ui/src/app/snippets.ts +++ b/docs-ui/src/app/snippets.ts @@ -1,6 +1,39 @@ -export const snippet = `import { Flex, Button, Text } from '@backstage/ui'; +export const surfacesSnippet = ` + + + + + + +`; - +export const adaptiveSnippet = ` + {/* automatically set background to neutral-2 */} + + +`; + +export const customCardSnippet = ` Hello World - -;`; +`; + +export const customTokensSnippet = `
+
Hello World
+
`; + +export const colorPickerSnippet = `import { ColorPicker, ColorArea, ColorSlider, ColorField } from 'react-aria-components'; + +function MyColorPicker() { + return ( + + + + + + ); +}`; diff --git a/docs-ui/src/components/ColorFamily/ColorFamily.module.css b/docs-ui/src/components/ColorFamily/ColorFamily.module.css new file mode 100644 index 0000000000..f38e481383 --- /dev/null +++ b/docs-ui/src/components/ColorFamily/ColorFamily.module.css @@ -0,0 +1,94 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: 2rem; + margin-top: 1rem; + margin-bottom: 2rem; +} + +@media (min-width: 768px) { + .wrapper { + flex-direction: row; + align-items: flex-start; + } +} + +.visual { + flex: 1; + min-width: 0; +} + +.text { + flex: 1; + min-width: 0; +} + +.base { + border-radius: 12px; + padding: 16px; +} + +.baseLabel { + display: block; + font-size: 13px; + color: var(--bui-fg-primary); + margin-bottom: 12px; +} + +.level { + border-radius: 10px; + padding: 12px; +} + +.levelHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.level > .level > .levelHeader:last-child, +.level:not(:has(> .level)) > .levelHeader { + margin-bottom: 0; +} + +.levelLabel { + font-size: 13px; + color: var(--bui-fg-primary); + white-space: nowrap; +} + +.chips { + display: flex; + gap: 6px; +} + +.chip { + padding: 6px 10px; + border-radius: 6px; + font-size: 11px; + font-weight: 500; + color: var(--bui-fg-primary); + white-space: nowrap; +} + +.description { + color: var(--primary); + margin-top: 0; + margin-bottom: 1rem; + font-size: 1rem; + line-height: 1.5; +} + +.description:last-child { + margin-bottom: 0; +} + +.link { + color: var(--link); + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} diff --git a/docs-ui/src/components/ColorFamily/ColorFamily.tsx b/docs-ui/src/components/ColorFamily/ColorFamily.tsx new file mode 100644 index 0000000000..0e168f0bbe --- /dev/null +++ b/docs-ui/src/components/ColorFamily/ColorFamily.tsx @@ -0,0 +1,97 @@ +'use client'; + +import styles from './ColorFamily.module.css'; + +const StateChip = ({ + level, + state, + label, +}: { + level: number; + state: string; + label: string; +}) => ( +
+ {label} +
+); + +const NeutralLevel = ({ + level, + children, + height, +}: { + level: number; + children?: React.ReactNode; + height?: number; +}) => ( +
+
+ Neutral {level} +
+ + + +
+
+ {children} +
+); + +export const ColorFamily = () => { + return ( +
+
+
+ Neutral 0 + + + + + + + +
+
+
+

+ BUI uses a layered neutral scale from 0 to 4. Each level nests inside + the previous one, automatically incrementing the background depth. + This creates clear visual hierarchy without manually picking colors. +

+

+ Neutral 0 is the application background and should only be used once, + at the root of your app. All other surfaces build on top of it. +

+

+ Each level can be interactive or{' '} + non-interactive. A Card, for example, can be flat + (just a surface) or fully clickable with hover and pressed states. The + three interaction states (hover, pressed, disabled) are built into + every neutral level. +

+

+ Explore the full list of color tokens on the{' '} + + tokens page + + . +

+
+
+ ); +}; diff --git a/docs-ui/src/components/ColorFamily/index.ts b/docs-ui/src/components/ColorFamily/index.ts new file mode 100644 index 0000000000..12f7fb4008 --- /dev/null +++ b/docs-ui/src/components/ColorFamily/index.ts @@ -0,0 +1 @@ +export { ColorFamily } from './ColorFamily'; diff --git a/docs-ui/src/components/CommandPalette/CommandPalette.module.css b/docs-ui/src/components/CommandPalette/CommandPalette.module.css new file mode 100644 index 0000000000..dbbdb72ceb --- /dev/null +++ b/docs-ui/src/components/CommandPalette/CommandPalette.module.css @@ -0,0 +1,154 @@ +.overlay { + position: fixed; + inset: 0; + z-index: 200; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 15vh; + + &[data-entering] { + animation: overlayFadeIn 200ms ease-out; + } + + &[data-exiting] { + animation: overlayFadeOut 150ms ease-in; + } +} + +@keyframes overlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes overlayFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +.modal { + width: min(90vw, 500px); + max-height: min(70vh, 400px); + background-color: var(--bg); + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2); + display: flex; + flex-direction: column; + outline: none; + overflow: hidden; + + &[data-entering] { + animation: modalScaleIn 200ms ease-out; + } + + &[data-exiting] { + animation: modalScaleOut 150ms ease-in; + } +} + +@keyframes modalScaleIn { + from { + opacity: 0; + transform: scale(0.95) translateY(-8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +@keyframes modalScaleOut { + from { + opacity: 1; + transform: scale(1) translateY(0); + } + to { + opacity: 0; + transform: scale(0.95) translateY(-8px); + } +} + +.dialog { + display: flex; + flex-direction: column; + height: 100%; + max-height: min(70vh, 400px); + outline: none; + overflow: hidden; +} + +.searchField { + display: flex; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} + +.srOnly { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.input { + width: 100%; + background: transparent; + border: none; + outline: none; + font-size: 1rem; + color: var(--primary); + font-family: inherit; + + &::placeholder { + color: var(--secondary); + } +} + +.menu { + flex: 1; + overflow-y: auto; + padding: 8px; + outline: none; + border: none; +} + +.menuItem { + padding: 8px 12px; + border-radius: 8px; + color: var(--primary); + cursor: pointer; + font-size: 0.875rem; + outline: none; + + &[data-focused] { + background-color: var(--action); + } + + &[data-pressed] { + background-color: var(--action); + } +} + +.empty { + padding: 24px 12px; + text-align: center; + color: var(--secondary); + font-size: 0.875rem; +} diff --git a/docs-ui/src/components/CommandPalette/CommandPalette.tsx b/docs-ui/src/components/CommandPalette/CommandPalette.tsx new file mode 100644 index 0000000000..bcc534372a --- /dev/null +++ b/docs-ui/src/components/CommandPalette/CommandPalette.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { + Autocomplete, + Dialog, + Input, + Label, + Menu, + MenuItem, + Modal, + ModalOverlay, + SearchField, + useFilter, +} from 'react-aria-components'; +import { useRouter } from 'next/navigation'; +import { components } from '@/utils/data'; +import styles from './CommandPalette.module.css'; + +interface CommandPaletteProps { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +} + +const items = components.map(c => ({ id: c.slug, ...c })); + +export const CommandPalette = ({ + isOpen, + onOpenChange, +}: CommandPaletteProps) => { + const router = useRouter(); + const { contains } = useFilter({ sensitivity: 'base' }); + + return ( + + + + + + + + + { + router.push(`/components/${key}`); + onOpenChange(false); + }} + renderEmptyState={() => ( +
No components found.
+ )} + > + {item => ( + + {item.title} + + )} +
+
+
+
+
+ ); +}; diff --git a/docs-ui/src/components/CommandPalette/index.ts b/docs-ui/src/components/CommandPalette/index.ts new file mode 100644 index 0000000000..95c1305982 --- /dev/null +++ b/docs-ui/src/components/CommandPalette/index.ts @@ -0,0 +1 @@ +export { CommandPalette } from './CommandPalette'; diff --git a/docs-ui/src/components/ComponentCards/ComponentCards.module.css b/docs-ui/src/components/ComponentCards/ComponentCards.module.css deleted file mode 100644 index 8eb9dbd3d0..0000000000 --- a/docs-ui/src/components/ComponentCards/ComponentCards.module.css +++ /dev/null @@ -1,54 +0,0 @@ -.grid { - display: grid; - grid-template-columns: repeat(1, 1fr); - gap: 1rem; - margin-top: 1rem; - margin-bottom: 3rem; -} - -@media (min-width: 768px) { - .grid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (min-width: 1024px) { - .grid { - grid-template-columns: repeat(3, 1fr); - } -} - -@media (min-width: 1280px) { - .grid { - grid-template-columns: repeat(4, 1fr); - } -} - -.card { - display: flex; - flex-direction: column; - justify-content: flex-end; - background-color: var(--bg); - border-radius: 8px; - border: 1px solid var(--border); - padding: 16px; - gap: 4px; - min-height: 120px; - - &:hover { - background-color: var(--bg-hover); - } -} - -.title { - margin: 0; - font-size: 16px; - font-weight: 600; - color: var(--primary); -} - -.description { - margin: 0; - font-size: 14px; - color: var(--secondary); -} diff --git a/docs-ui/src/components/ComponentCards/ComponentCards.tsx b/docs-ui/src/components/ComponentCards/ComponentCards.tsx deleted file mode 100644 index 0e4d22c02a..0000000000 --- a/docs-ui/src/components/ComponentCards/ComponentCards.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Link from 'next/link'; -import styles from './ComponentCards.module.css'; - -export const ComponentCards = ({ children }: { children: React.ReactNode }) => { - return
{children}
; -}; - -export const ComponentCard = ({ - title, - description, - href, -}: { - title: string; - description: string; - href: string; -}) => { - return ( - -

{title}

-

{description}

- - ); -}; diff --git a/docs-ui/src/components/ComponentCards/index.ts b/docs-ui/src/components/ComponentCards/index.ts deleted file mode 100644 index 4bc4beaae3..0000000000 --- a/docs-ui/src/components/ComponentCards/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ComponentCards'; diff --git a/docs-ui/src/components/ComponentGrid/ComponentGrid.module.css b/docs-ui/src/components/ComponentGrid/ComponentGrid.module.css new file mode 100644 index 0000000000..a6dd54a864 --- /dev/null +++ b/docs-ui/src/components/ComponentGrid/ComponentGrid.module.css @@ -0,0 +1,29 @@ +.grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + margin-top: 1rem; + margin-bottom: 2rem; +} + +@media (min-width: 768px) { + .grid { + grid-template-columns: repeat(4, 1fr); + } +} + +.item { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 0; + font-size: 14px; + font-weight: 400; + color: var(--primary); + text-decoration: none; + + &:hover { + color: var(--link); + text-decoration: underline; + text-underline-offset: 2px; + } +} diff --git a/docs-ui/src/components/ComponentGrid/ComponentGrid.tsx b/docs-ui/src/components/ComponentGrid/ComponentGrid.tsx new file mode 100644 index 0000000000..eb0f043420 --- /dev/null +++ b/docs-ui/src/components/ComponentGrid/ComponentGrid.tsx @@ -0,0 +1,21 @@ +'use client'; + +import Link from 'next/link'; +import { components } from '@/utils/data'; +import styles from './ComponentGrid.module.css'; + +export const ComponentGrid = () => { + return ( +
+ {components.map(item => ( + + {item.title} + + ))} +
+ ); +}; diff --git a/docs-ui/src/components/ComponentGrid/index.ts b/docs-ui/src/components/ComponentGrid/index.ts new file mode 100644 index 0000000000..f0fe06477a --- /dev/null +++ b/docs-ui/src/components/ComponentGrid/index.ts @@ -0,0 +1 @@ +export * from './ComponentGrid'; diff --git a/docs-ui/src/components/LayoutComponents/LayoutComponents.module.css b/docs-ui/src/components/LayoutComponents/LayoutComponents.module.css deleted file mode 100644 index 9009cdb6a4..0000000000 --- a/docs-ui/src/components/LayoutComponents/LayoutComponents.module.css +++ /dev/null @@ -1,48 +0,0 @@ -.layoutComponents { - display: flex; - justify-content: flex-start; - gap: 1rem; - flex-wrap: wrap; - margin-top: 2rem; - - & svg rect { - transition: fill 0.2s ease-in-out; - } - - & .box { - display: flex; - flex-direction: column; - width: calc(50% - 0.5rem); - margin-bottom: 1rem; - align-items: flex-start; - } - - & .content { - flex: none; - background-color: var(--bg); - border: 1px solid var(--border); - border-radius: 4px; - width: 100%; - height: 180px; - transition: all 0.2s ease-in-out; - margin-bottom: 0.75rem; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - transform: translateY(-4px); - } - } - - & .title { - font-size: 16px; - transition: color 0.2s ease-in-out; - margin-bottom: 0.25rem; - } - - & .description { - font-size: 16px; - color: var(--secondary); - } -} diff --git a/docs-ui/src/components/LayoutComponents/LayoutComponents.tsx b/docs-ui/src/components/LayoutComponents/LayoutComponents.tsx deleted file mode 100644 index 0bf822526e..0000000000 --- a/docs-ui/src/components/LayoutComponents/LayoutComponents.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { BoxSvg } from './svgs/box'; -import { FlexSvg } from './svgs/flex'; -import { GridSvg } from './svgs/grid'; -import { ContainerSvg } from './svgs/container'; -import styles from './LayoutComponents.module.css'; -import Link from 'next/link'; - -export const LayoutComponents = () => { - return ( -
-
- - - -
Box
-
- The most basic layout component -
-
-
- - - -
Flex
-
- Arrange your components vertically -
-
-
- - - -
Grid
-
- Arrange your components in a grid -
-
-
- - - -
Container
-
- A container for your components -
-
-
- ); -}; diff --git a/docs-ui/src/components/LayoutComponents/index.ts b/docs-ui/src/components/LayoutComponents/index.ts deleted file mode 100644 index 8efb793bbd..0000000000 --- a/docs-ui/src/components/LayoutComponents/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LayoutComponents } from './LayoutComponents'; diff --git a/docs-ui/src/components/Navigation/Navigation.module.css b/docs-ui/src/components/Navigation/Navigation.module.css index f5e1312ab1..efba109f97 100644 --- a/docs-ui/src/components/Navigation/Navigation.module.css +++ b/docs-ui/src/components/Navigation/Navigation.module.css @@ -49,16 +49,13 @@ } .sectionTitle { - font-size: 0.6875rem; - font-weight: 500; - padding: 0 12px 4px; - color: var(--secondary); + display: flex; + align-items: center; + gap: 8px; + color: var(--primary); margin-top: 40px; - text-transform: uppercase; - - &:first-child { - margin-top: 12px; - } + padding: 8px 12px; + margin-bottom: 8px; } .line { diff --git a/docs-ui/src/components/Navigation/Navigation.tsx b/docs-ui/src/components/Navigation/Navigation.tsx index 13fa87661f..bad77e942e 100644 --- a/docs-ui/src/components/Navigation/Navigation.tsx +++ b/docs-ui/src/components/Navigation/Navigation.tsx @@ -2,7 +2,6 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { Fragment } from 'react'; import clsx from 'clsx'; import { RiCollageLine, @@ -12,26 +11,13 @@ import { RiServiceLine, RiStackLine, } from '@remixicon/react'; -import { components, layoutComponents } from '@/utils/data'; +import { components } from '@/utils/data'; import styles from './Navigation.module.css'; interface NavigationProps { onLinkClick?: () => void; } -const data = [ - { - title: 'Layout Components', - content: layoutComponents, - url: '/components', - }, - { - title: 'Components', - content: components, - url: '/components', - }, -]; - export const Navigation = ({ onLinkClick }: NavigationProps) => { const pathname = usePathname(); @@ -55,16 +41,6 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => { Tokens -
  • - - - Components - -
  • @@ -89,35 +65,31 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => {
  • - {data.map(section => { +
    + + Components +
    + {components.map(item => { + const isActive = pathname === `/components/${item.slug}`; + return ( - -
    {section.title}
    - - {section.content.map(item => { - const isActive = pathname === `${section.url}/${item.slug}`; - - return ( - -
    {item.title}
    -
    - {item.status === 'alpha' && 'Alpha'} - {item.status === 'beta' && 'Beta'} - {item.status === 'inProgress' && 'In Progress'} - {item.status === 'stable' && 'Stable'} - {item.status === 'deprecated' && 'Deprecated'} -
    - - ); + + onClick={onLinkClick} + > +
    {item.title}
    +
    + {item.status === 'alpha' && 'Alpha'} + {item.status === 'beta' && 'Beta'} + {item.status === 'inProgress' && 'In Progress'} + {item.status === 'stable' && 'Stable'} + {item.status === 'deprecated' && 'Deprecated'} +
    + ); })} diff --git a/docs-ui/src/components/Toolbar/Toolbar.module.css b/docs-ui/src/components/Toolbar/Toolbar.module.css index 581fc599b3..4f067bd7bd 100644 --- a/docs-ui/src/components/Toolbar/Toolbar.module.css +++ b/docs-ui/src/components/Toolbar/Toolbar.module.css @@ -10,12 +10,10 @@ } } -.breadcrumb { +.left { display: flex; align-items: center; gap: 0.5rem; - font-size: 0.875rem; - font-weight: 500; } .logoMobile { @@ -26,39 +24,6 @@ } } -.breadcrumbDesktop { - display: none; - - @media (min-width: 768px) { - display: flex; - align-items: center; - gap: 0.5rem; - } -} - -.breadcrumbLink { - color: var(--secondary); - text-decoration: none; - cursor: pointer; - - &:hover { - color: var(--primary); - text-decoration: underline; - transition: color 0.2s ease-in-out; - text-decoration-thickness: 1px; - text-underline-offset: 4px; - } -} - -.breadcrumbSeparator { - color: var(--secondary); - flex-shrink: 0; -} - -.breadcrumbCurrent { - color: var(--primary); -} - .actions { display: none; @@ -119,6 +84,51 @@ } } +.searchButton { + display: none; + align-items: center; + gap: 6px; + background-color: var(--bg); + border: 1px solid var(--border); + border-radius: 32px; + padding-inline: 16px 12px; + min-width: 180px; + color: var(--secondary); + font-size: 0.8125rem; + font-weight: 500; + height: 32px; + cursor: pointer; + font-family: inherit; + white-space: nowrap; + + &:hover { + background-color: var(--action); + color: var(--primary); + transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; + } + + @media (min-width: 768px) { + display: flex; + } +} + +.searchLabel { + display: inline; + flex: 1; + text-align: left; +} + +.searchKbd { + display: inline; + font-family: inherit; + font-size: 0.6875rem; + font-weight: 500; + background-color: var(--action); + border-radius: 4px; + padding: 2px 5px; + color: var(--secondary); +} + .buttonGroup { display: flex; align-items: center; diff --git a/docs-ui/src/components/Toolbar/Toolbar.tsx b/docs-ui/src/components/Toolbar/Toolbar.tsx index e7defd5e8a..4a071bf45e 100644 --- a/docs-ui/src/components/Toolbar/Toolbar.tsx +++ b/docs-ui/src/components/Toolbar/Toolbar.tsx @@ -1,10 +1,11 @@ 'use client'; +import { useState, useEffect } from 'react'; import { RiArrowDownSLine, - RiArrowRightSLine, RiGithubLine, RiMoonLine, + RiSearchLine, RiSunLine, } from '@remixicon/react'; import { @@ -19,10 +20,8 @@ import { } from 'react-aria-components'; import styles from './Toolbar.module.css'; import { usePlayground } from '@/utils/playground-context'; -import { usePathname } from 'next/navigation'; -import Link from 'next/link'; -import { components, layoutComponents } from '@/utils/data'; import { Logo } from '@/components/Sidebar/Logo'; +import { CommandPalette } from '@/components/CommandPalette'; interface ToolbarProps { version: string; @@ -42,75 +41,36 @@ export const Toolbar = ({ version }: ToolbarProps) => { setSelectedThemeName, } = usePlayground(); - const pathname = usePathname(); + const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); - // Determine breadcrumb content based on current path - const getBreadcrumb = () => { - const allComponents = [...components, ...layoutComponents]; + useEffect(() => { + const isMac = /mac(os|intosh)/i.test(navigator.userAgent); + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey)) { + e.preventDefault(); + setIsCommandPaletteOpen(prev => !prev); + } + }; - // Root page - if (pathname === '/') { - return { section: null, title: 'Getting Started' }; - } - - // Components index page - if (pathname === '/components') { - return { section: null, title: 'Components' }; - } - - // Component detail pages - if (pathname?.startsWith('/components/')) { - const slug = pathname.split('/components/')[1]; - const component = allComponents.find(c => c.slug === slug); - return { - section: 'Components', - sectionLink: '/components', - title: component?.title || slug, - }; - } - - // Tokens page - if (pathname === '/tokens') { - return { section: null, title: 'Tokens' }; - } - - // Changelog page - if (pathname === '/changelog') { - return { section: null, title: 'Changelog' }; - } - - return { section: null, title: '' }; - }; - - const breadcrumb = getBreadcrumb(); + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, []); return (
    -
    +
    -
    - {breadcrumb.section && breadcrumb.sectionLink ? ( - <> - - {breadcrumb.section} - - - - {breadcrumb.title} - - - ) : ( - {breadcrumb.title} - )} -
    +