diff --git a/docs-ui/public/css-classname-structure.png b/docs-ui/public/css-classname-structure.png index df9894ff2b..c241b182cc 100644 Binary files a/docs-ui/public/css-classname-structure.png and b/docs-ui/public/css-classname-structure.png differ diff --git a/docs-ui/src/app/theme/iconography/page.mdx b/docs-ui/src/app/iconography/page.mdx similarity index 100% rename from docs-ui/src/app/theme/iconography/page.mdx rename to docs-ui/src/app/iconography/page.mdx diff --git a/docs-ui/src/app/install/page.mdx b/docs-ui/src/app/install/page.mdx new file mode 100644 index 0000000000..329d477eca --- /dev/null +++ b/docs-ui/src/app/install/page.mdx @@ -0,0 +1,36 @@ +import { CodeBlock } from '@/components/CodeBlock'; + +# How to install Backstage UI + +## 1. Import the package + +Import the package using a package manager. + + + +## 2. Import the css files + +Import the global CSS file at the root of your application. + +```tsx +import '@backstage/ui/css/styles.css'; +``` + +## 3. Start building ✨ + +Now you can start building your plugin using the new design system. + +```tsx +import { Flex, Button, Text } from '@backstage/ui'; + + + Hello World + +; +``` + +## 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). diff --git a/docs-ui/src/app/layout.tsx b/docs-ui/src/app/layout.tsx index 58decbf22b..d90f1c06a4 100644 --- a/docs-ui/src/app/layout.tsx +++ b/docs-ui/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import { Sidebar } from '../components/Sidebar'; import { Toolbar } from '@/components/Toolbar'; +import { StickyHeader } from '../components/StickyHeader/StickyHeader'; import { Providers } from './providers'; import { CustomTheme } from '@/components/CustomTheme'; import styles from './layout.module.css'; @@ -49,8 +50,9 @@ export default async function RootLayout({ > - - + + +
{children}
diff --git a/docs-ui/src/app/theme/layout/page.mdx b/docs-ui/src/app/layout/page.mdx similarity index 100% rename from docs-ui/src/app/theme/layout/page.mdx rename to docs-ui/src/app/layout/page.mdx diff --git a/docs-ui/src/app/page.mdx b/docs-ui/src/app/page.mdx index 27d7bc66d4..f31cf4524e 100644 --- a/docs-ui/src/app/page.mdx +++ b/docs-ui/src/app/page.mdx @@ -1,43 +1,173 @@ -import { CodeBlock } from '@/components/CodeBlock'; +import { ComponentCards, ComponentCard } from '@/components/ComponentCards'; -# Backstage UI +## Welcome to Backstage UI, the new design library for Backstage plugins. -Welcome to the Backstage UI, the new design library for Backstage plugins. This -project is still under active development but we will make sure to document +This project is still under active development but we will make sure to document the API as we go. We are aiming to improve the general UI of Backstage and plugins across Backstage. This new library will take time to build but we are building it incrementally with not conflict with the existing theming system. -## Installation +### Actions -### 1. Install Backstage UI + + + + + + -Install Backstage UI using a package manager. +### Layout - + + + + + + + + -### 2. Import the css files +### Selection and inputs -Import the global CSS file at the root of your application. + + + + + + + + -```tsx -import '@backstage/ui/css/styles.css'; -``` +### Navigation -### 3. Start building ✨ + + + + + + -Now you can start building your plugin using the new design system. +### Images and icons -```tsx -import { Flex, Button, Text } from '@backstage/ui'; + + + + - - Hello World - -; -``` +### Feedback indicators -## Next steps + + + + + +### Typography + + + + + +## 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. diff --git a/docs-ui/src/app/theme/responsive/page.mdx b/docs-ui/src/app/responsive/page.mdx similarity index 100% rename from docs-ui/src/app/theme/responsive/page.mdx rename to docs-ui/src/app/responsive/page.mdx diff --git a/docs-ui/src/app/theme/typography/page.mdx b/docs-ui/src/app/theme/typography/page.mdx deleted file mode 100644 index 31c55b8e25..0000000000 --- a/docs-ui/src/app/theme/typography/page.mdx +++ /dev/null @@ -1,65 +0,0 @@ -import { HeadingSnippet, TextSnippet } from '@/snippets/stories-snippets'; -import { Snippet } from '@/components/Snippet'; - -# Typography - -Backstage UI offers a suite of typography components designed to seamlessly align -with the rest of your Backstage instance. While you can customize their -appearance to match your brand, the underlying API remains consistent and -unchanged. Each component is built on a responsive structure, allowing you to -define different typography values for various breakpoints. - -## Headings - -Headings are used to structure the content of your page. They are used to -create a hierarchy of information and to make the content more readable. The -best way to use add these headings to your page is to import the [Heading -component](?path=/docs/components-heading--docs). - -} - code={` - Display - Title 1 - Title 2 - Title 3 - Title 4 -`} -/> - -## Text - -Backstage UI provides four distinct text variants, each offering different font sizes -carefully designed to cover the majority of use cases. These variants are -versatile and can be paired with regular and bold of font weights. You can use -the [Text component](?path=/docs/components-text--docs) to add text to your -page. - -} - code={` - - A man looks at a painting in a museum and says, “Brothers and sisters I - have none, but that man's father is my father's son.” Who is - in the painting? - - - A man looks at a painting in a museum and says, “Brothers and sisters I - have none, but that man's father is my father's son.” Who is - in the painting? - - - A man looks at a painting in a museum and says, “Brothers and sisters I - have none, but that man's father is my father's son.” Who is - in the painting? - - - A man looks at a painting in a museum and says, “Brothers and sisters I - have none, but that man's father is my father's son.” Who is - in the painting? - - `} -/> diff --git a/docs-ui/src/app/theme/theming/page.mdx b/docs-ui/src/app/theming/page.mdx similarity index 100% rename from docs-ui/src/app/theme/theming/page.mdx rename to docs-ui/src/app/theming/page.mdx diff --git a/docs-ui/src/components/ComponentCards/ComponentCards.module.css b/docs-ui/src/components/ComponentCards/ComponentCards.module.css new file mode 100644 index 0000000000..688010110c --- /dev/null +++ b/docs-ui/src/components/ComponentCards/ComponentCards.module.css @@ -0,0 +1,54 @@ +.grid { + display: grid; + grid-template-columns: repeat(1, 1fr); + gap: 1rem; + margin-top: 1rem; + margin-bottom: 3rem; +} + +@media (min-width: 768px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (min-width: 1280px) { + .grid { + grid-template-columns: repeat(4, 1fr); + } +} + +.card { + display: flex; + flex-direction: column; + justify-content: flex-end; + background-color: var(--panel); + border-radius: 8px; + border: 1px solid var(--border); + padding: 16px; + gap: 4px; + min-height: 120px; + + &:hover { + background-color: var(--panel-hover); + } +} + +.title { + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--primary); +} + +.description { + margin: 0; + font-size: 14px; + color: var(--secondary); +} diff --git a/docs-ui/src/components/ComponentCards/ComponentCards.tsx b/docs-ui/src/components/ComponentCards/ComponentCards.tsx new file mode 100644 index 0000000000..0e4d22c02a --- /dev/null +++ b/docs-ui/src/components/ComponentCards/ComponentCards.tsx @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000000..4bc4beaae3 --- /dev/null +++ b/docs-ui/src/components/ComponentCards/index.ts @@ -0,0 +1 @@ +export * from './ComponentCards'; diff --git a/docs-ui/src/components/PropsTable/PropsTable.tsx b/docs-ui/src/components/PropsTable/PropsTable.tsx index 8fedeb7c0c..bd54b1e245 100644 --- a/docs-ui/src/components/PropsTable/PropsTable.tsx +++ b/docs-ui/src/components/PropsTable/PropsTable.tsx @@ -2,15 +2,13 @@ import * as Table from '../Table'; import { Chip } from '../Chip'; +import { TypePopup } from './TypePopup'; import { icons } from '../../../../packages/ui'; -// Define a more specific type for the data object -type PropData = { - values?: string | string[]; - responsive?: boolean; - default?: string; - type?: string; -}; +import { PropDef } from '@/utils/propDefs'; + +// Use the proper PropDef type +type PropData = PropDef; // Modify the PropsTable component to use the new type export const PropsTable = >({ @@ -59,6 +57,12 @@ export const PropsTable = >({ string )} + {data[n].type === 'complex' && data[n].complexType && ( + + )} {data[n].type === 'enum | string' && ( <> {enumValues} diff --git a/docs-ui/src/components/PropsTable/TypePopup.module.css b/docs-ui/src/components/PropsTable/TypePopup.module.css new file mode 100644 index 0000000000..294df0b645 --- /dev/null +++ b/docs-ui/src/components/PropsTable/TypePopup.module.css @@ -0,0 +1,79 @@ +.button { + display: inline-flex; + align-items: center; + font-family: monospace; + font-size: 13px; + border-radius: 6px; + padding: 0px 8px; + height: 24px; + margin-right: 4px; + background-color: #f0f0f0; + color: #5d5d5d; + cursor: pointer; + border: none; + outline: none; + box-shadow: none; + text-decoration: underline; +} + +[data-theme='dark'] .button { + background-color: #2c2c2c; + color: #fff; +} + +.popover { + border: 1px solid var(--border); + box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); + border-radius: 6px; + background: var(--panel); + color: var(--primary); + outline: none; + /* max-width: 400px; */ + transition: transform 200ms, opacity 200ms; + padding: 1rem; + margin-top: 6px; + --origin: translateY(-8px); + + &[data-entering], + &[data-exiting] { + transform: var(--origin); + opacity: 0; + } +} + +.arrow svg { + display: block; + fill: var(--panel); + stroke: var(--border); + stroke-width: 1px; + transform: rotate(180deg); +} + +.name { + font-size: 14px; + font-weight: 500; + margin-bottom: 1rem; +} + +.grid { + display: grid; + grid-template-columns: auto auto 1fr; + gap: 0.5rem 1rem; +} + +.col1 { + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + padding-top: 0.25rem; +} + +.col2 { + font-size: 13px; +} + +.col3 { + font-size: 13px; + color: var(--text-secondary); + padding-top: 0.25rem; +} diff --git a/docs-ui/src/components/PropsTable/TypePopup.tsx b/docs-ui/src/components/PropsTable/TypePopup.tsx new file mode 100644 index 0000000000..bd0fc5dd76 --- /dev/null +++ b/docs-ui/src/components/PropsTable/TypePopup.tsx @@ -0,0 +1,49 @@ +import { ComplexTypeDef } from '@/utils/propDefs'; +import { Chip } from '../Chip'; +import { + Button, + Dialog, + DialogTrigger, + OverlayArrow, + Popover, +} from 'react-aria-components'; +import styles from './TypePopup.module.css'; + +interface TypePopupProps { + complexType: ComplexTypeDef; + name: string; +} + +export const TypePopup = ({ complexType, name }: TypePopupProps) => { + return ( + + + + + + + + + +
{complexType.name}
+
+ {Object.entries(complexType.properties).map( + ([propName, propDef]) => [ +
+ {propName} + {propDef.required ? '' : '?'} +
, +
+ {propDef.type} +
, +
+ {propDef.description} +
, + ], + )} +
+
+
+
+ ); +}; diff --git a/docs-ui/src/components/Sidebar/Sidebar.module.css b/docs-ui/src/components/Sidebar/Sidebar.module.css index 2b88945c47..75ac541d9f 100644 --- a/docs-ui/src/components/Sidebar/Sidebar.module.css +++ b/docs-ui/src/components/Sidebar/Sidebar.module.css @@ -15,33 +15,6 @@ } } -.version { - font-size: 0.875rem; - font-weight: 500; - color: var(--secondary); - margin-inline: 32px; - border-bottom: 1px solid var(--border); - padding-bottom: 12px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.versionLinks { - display: flex; - align-items: center; - gap: 8px; - - a { - color: var(--secondary); - transition: color 0.2s ease-in-out; - - &:hover { - color: var(--primary); - } - } -} - .root { height: 100%; } @@ -106,6 +79,10 @@ color: var(--secondary); margin-top: 40px; text-transform: uppercase; + + &:first-child { + margin-top: 12px; + } } .line { diff --git a/docs-ui/src/components/Sidebar/index.tsx b/docs-ui/src/components/Sidebar/index.tsx index 4d86398ef7..59e297e26e 100644 --- a/docs-ui/src/components/Sidebar/index.tsx +++ b/docs-ui/src/components/Sidebar/index.tsx @@ -1,13 +1,17 @@ 'use client'; import styles from './Sidebar.module.css'; -import { components, overview, layoutComponents, theme } from '@/utils/data'; +import { + components, + overview, + layoutComponents, + coreConcepts, +} from '@/utils/data'; import { ScrollArea } from '@base-ui-components/react/scroll-area'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { motion } from 'motion/react'; import { Fragment } from 'react'; -import { RiGithubLine, RiNpmjsLine } from '@remixicon/react'; const data = [ { @@ -16,9 +20,9 @@ const data = [ url: '', }, { - title: 'Theme', - content: theme, - url: '/theme', + title: 'Core Concepts', + content: coreConcepts, + url: '', }, { title: 'Layout Components', @@ -32,30 +36,12 @@ const data = [ }, ]; -interface SidebarProps { - version: string; -} - -export const Sidebar = ({ version }: SidebarProps) => { +export const Sidebar = () => { const pathname = usePathname(); const isPlayground = pathname.includes('/playground'); return (
-
- Version {version} - -
diff --git a/docs-ui/src/components/StickyHeader/StickyHeader.module.css b/docs-ui/src/components/StickyHeader/StickyHeader.module.css new file mode 100644 index 0000000000..7cfc197f83 --- /dev/null +++ b/docs-ui/src/components/StickyHeader/StickyHeader.module.css @@ -0,0 +1,98 @@ +.stickyHeader { + position: fixed; + top: 0px; + left: 32px; + right: 32px; + z-index: 99999; + background-color: var(--background); + padding: 16px 20px 16px 40px; + display: flex; + justify-content: space-between; + align-items: center; + backdrop-filter: blur(10px); + pointer-events: auto; + isolation: isolate; + transform: translateZ(0); + opacity: 0; + width: calc(100% - 64px); + mask-image: linear-gradient(to bottom, black 0%, black 80%, transparent 100%); + -webkit-mask-image: linear-gradient( + to bottom, + black 0%, + black 80%, + transparent 100% + ); +} + +@media (max-width: 768px) { + .stickyHeader { + display: none; + } +} + +.right { + display: flex; + align-items: center; + gap: 24px; +} + +.name { + font-size: 24px; + color: var(--text-primary); + font-weight: 400; +} + +.version { + font-size: 14px; + color: var(--text-secondary); + font-weight: 500; +} + +.actions { + display: flex; + align-items: center; + gap: 16px; +} + +.versionLinks { + display: flex; + align-items: center; + + a { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + color: var(--secondary); + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out; + border-radius: 48px; + + &:hover { + color: var(--primary); + background-color: var(--action); + } + } +} + +@media (max-width: 768px) { + .stickyHeader { + padding: 12px 16px; + } + + .right { + gap: 16px; + } + + .version { + font-size: 12px; + } +} + +@media (min-width: 768px) { + .stickyHeader { + width: calc(100% - 332px - 40px); + left: 332px; + right: 40px; + } +} diff --git a/docs-ui/src/components/StickyHeader/StickyHeader.tsx b/docs-ui/src/components/StickyHeader/StickyHeader.tsx new file mode 100644 index 0000000000..804d759715 --- /dev/null +++ b/docs-ui/src/components/StickyHeader/StickyHeader.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { motion, useScroll, useTransform, circOut } from 'framer-motion'; +import { RiGithubLine, RiNpmjsLine } from '@remixicon/react'; +import { ThemeSelector } from '../Toolbar/theme'; +import { ThemeNameSelector } from '../Toolbar/theme-name'; +import { useCurrentPage } from '@/hooks/useCurrentPage'; +import styles from './StickyHeader.module.css'; + +export const StickyHeader = () => { + const { scrollY } = useScroll(); + const currentPage = useCurrentPage(); + + // Transform scroll position to opacity only + const opacity = useTransform(scrollY, [100, 200], [0, 1], { + clamp: false, + }); + + const yPos = useTransform(scrollY, [200, 500], [-60, 0], { + clamp: true, + ease: circOut, + }); + + return ( + +
{currentPage || 'Backstage UI'}
+
+ + + +
+
+ ); +}; diff --git a/docs-ui/src/components/Toolbar/Logo.tsx b/docs-ui/src/components/Toolbar/Logo.tsx new file mode 100644 index 0000000000..159244ecc7 --- /dev/null +++ b/docs-ui/src/components/Toolbar/Logo.tsx @@ -0,0 +1,20 @@ +export const Logo = () => { + return ( + + + + + ); +}; diff --git a/docs-ui/src/components/Toolbar/Toolbar.module.css b/docs-ui/src/components/Toolbar/Toolbar.module.css index 8142c5464e..f0e58f2edb 100644 --- a/docs-ui/src/components/Toolbar/Toolbar.module.css +++ b/docs-ui/src/components/Toolbar/Toolbar.module.css @@ -11,30 +11,50 @@ padding-left: 0.25rem; } -@media (min-width: 960px) { - .toolbar { - left: 332px; - right: 40px; - } -} - .left { + /* width: 296px; */ display: flex; align-items: center; gap: 0.5rem; + padding-right: 20px; } -.name { - margin-top: 3px; - font-size: 28px; - color: #26c9ad; - font-weight: 500; +.right { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-end; } .actions { display: none; } +.version { + display: none; +} + +.versionLinks { + display: flex; + align-items: center; + + a { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + color: var(--secondary); + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out; + border-radius: 48px; + + &:hover { + color: var(--primary); + background-color: var(--action); + } + } +} + @media (min-width: 600px) { .actions { display: flex; @@ -42,3 +62,35 @@ gap: 1rem; } } + +@media (min-width: 768px) { + .toolbar { + right: 40px; + } +} + +@media (min-width: 820px) { + .right { + justify-content: space-between; + } + + .version { + display: flex; + align-items: center; + justify-content: center; + /* background-color: var(--action); */ + border: 1px solid var(--border2); + border-radius: 48px; + padding-inline: 20px; + color: var(--primary); + font-size: 0.875rem; + font-weight: 500; + height: 48px; + } +} + +@media (min-width: 960px) { + .left { + width: 296px; + } +} diff --git a/docs-ui/src/components/Toolbar/Toolbar.tsx b/docs-ui/src/components/Toolbar/Toolbar.tsx index 952b48f5ac..29570739f8 100644 --- a/docs-ui/src/components/Toolbar/Toolbar.tsx +++ b/docs-ui/src/components/Toolbar/Toolbar.tsx @@ -1,31 +1,52 @@ '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 styles from './Toolbar.module.css'; -export const Toolbar = () => { +interface ToolbarProps { + version: string; +} + +export const Toolbar = ({ version }: ToolbarProps) => { + const containerRef = useRef(null); + const { scrollY } = useScroll(); + + // Transform scroll velocity to vertical movement + const y = useTransform(scrollY, [0, 100], [0, -20], { + clamp: false, + }); + return ( -
+
- - - -
UI
-
-
- - +
+ +
Version {version} - Alpha
+
+ + + +
+
); }; diff --git a/docs-ui/src/components/Toolbar/theme-name.module.css b/docs-ui/src/components/Toolbar/theme-name.module.css index 58471b8fd7..1e1943b886 100644 --- a/docs-ui/src/components/Toolbar/theme-name.module.css +++ b/docs-ui/src/components/Toolbar/theme-name.module.css @@ -25,19 +25,6 @@ } } -.SelectIcon { - display: flex; -} - -.SelectValue { - font-size: 0.875rem; - font-weight: 400; -} - -.Positioner { - z-index: 20; -} - .Popup { box-sizing: border-box; padding-block: 0.25rem; @@ -73,65 +60,51 @@ } } -.Item { - box-sizing: border-box; - outline: 0; - font-size: 0.875rem; - line-height: 1rem; - padding-block: 0.5rem; - padding-left: 0.625rem; - padding-right: 1rem; - font-size: 0.875rem; - font-weight: 600; - display: grid; - gap: 0.5rem; - align-items: center; - grid-template-columns: 0.75rem 1fr; - cursor: default; - user-select: none; - border-radius: 0.25rem; - cursor: pointer; - transition: background-color 0.2s ease-in-out; +.Popup[data-trigger='Select'] { + min-width: var(--trigger-width); - [data-side='none'] & { - font-size: 1rem; - padding-right: 3rem; - min-width: calc(var(--anchor-width) + 1rem); + .ListBox { + display: block; + width: unset; + max-height: inherit; + min-height: unset; + border: none; + + .react-aria-Header { + padding-left: 1.571rem; + } } - &[data-highlighted] { - z-index: 0; + .Item { position: relative; - color: var(--color-gray-50); - background-color: var(--bg); - } + padding: 0 0.571rem 0 1.571rem; + height: 2rem; + display: flex; + align-items: center; - &[data-highlighted]::before { - content: ''; - z-index: -1; - position: absolute; - inset-block: 0; - inset-inline: 0.25rem; - border-radius: 0.25rem; - background-color: var(--color-gray-900); + &[data-focus-visible] { + outline: none; + } + + &[data-selected] { + font-weight: 600; + background: unset; + color: var(--text-color); + + &::before { + content: '✓'; + content: '✓' / ''; + alt: ' '; + position: absolute; + left: 4px; + } + } + + &[data-focused], + &[data-pressed] { + background: var(--bg); + color: var(--primary); + cursor: pointer; + } } } - -.ItemIndicator { - grid-column-start: 1; - width: 1rem; - height: 1rem; - display: flex; - align-items: center; -} - -.ItemIndicatorIcon { - display: block; - width: 0.75rem; - height: 0.75rem; -} - -.ItemText { - grid-column-start: 2; - font-size: 14px; -} diff --git a/docs-ui/src/components/Toolbar/theme-name.tsx b/docs-ui/src/components/Toolbar/theme-name.tsx index 3cf0d088ef..544c345f86 100644 --- a/docs-ui/src/components/Toolbar/theme-name.tsx +++ b/docs-ui/src/components/Toolbar/theme-name.tsx @@ -1,6 +1,13 @@ 'use client'; -import { Select } from '@base-ui-components/react/select'; +import { + Button, + ListBox, + ListBoxItem, + Popover, + Select, + SelectValue, +} from 'react-aria-components'; import styles from './theme-name.module.css'; import { Icon } from '@backstage/ui'; import { usePlayground } from '@/utils/playground-context'; @@ -15,35 +22,23 @@ export const ThemeNameSelector = () => { const { selectedThemeName, setSelectedThemeName } = usePlayground(); return ( - - - - - - - - - - - {themes.map(({ name, value }) => ( - - - - - - {name} - - - ))} - - - - + + + + {themes.map(({ name, value }) => ( + + {name} + + ))} + + + ); }; diff --git a/docs-ui/src/content/components/header-page.mdx b/docs-ui/src/content/components/header-page.mdx new file mode 100644 index 0000000000..4589e037a4 --- /dev/null +++ b/docs-ui/src/content/components/header-page.mdx @@ -0,0 +1,40 @@ +import { PropsTable } from '@/components/PropsTable'; +import { CodeBlock } from '@/components/CodeBlock'; +import { Snippet } from '@/components/Snippet'; +import { HeaderPageSnippet } from '@/snippets/stories-snippets'; +import { + propDefs, + usage, + simple, + defaultSnippet, + withTabs, + withBreadcrumbs, + withHeaderPage, +} from './header-page.props'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; + + + +} + code={defaultSnippet} +/> + +## Usage + + + +## API reference + + + + + + diff --git a/docs-ui/src/content/components/header-page.props.ts b/docs-ui/src/content/components/header-page.props.ts new file mode 100644 index 0000000000..d287c8baf8 --- /dev/null +++ b/docs-ui/src/content/components/header-page.props.ts @@ -0,0 +1,91 @@ +import { + childrenPropDefs, + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const propDefs: Record = { + title: { + type: 'string', + default: 'Your plugin', + }, + customActions: { + type: 'enum', + values: ['ReactNode'], + }, + menuItems: { + type: 'complex', + complexType: { + name: 'MenuItem[]', + properties: { + label: { + type: 'string', + required: true, + description: 'Display text for the menu item', + }, + value: { + type: 'string', + required: true, + description: 'Unique value for the menu item', + }, + onClick: { + type: '() => void', + required: false, + description: 'Callback function when menu item is clicked', + }, + }, + }, + }, + tabs: { + type: 'complex', + complexType: { + name: 'HeaderTab[]', + properties: { + id: { + type: 'string', + required: true, + description: 'Unique identifier for the tab', + }, + label: { + type: 'string', + required: true, + description: 'Display text for the tab', + }, + href: { + type: 'string', + required: false, + description: 'URL to navigate to when tab is clicked', + }, + matchStrategy: { + type: "'exact' | 'prefix'", + required: false, + description: 'How to match the current route to highlight the tab', + }, + }, + }, + }, + ...childrenPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const usage = `import { HeaderPage } from '@backstage/ui'; + +`; + +export const defaultSnippet = `Custom action} +/>`; diff --git a/docs-ui/src/content/components/header.mdx b/docs-ui/src/content/components/header.mdx new file mode 100644 index 0000000000..af4e8010c5 --- /dev/null +++ b/docs-ui/src/content/components/header.mdx @@ -0,0 +1,85 @@ +import { PropsTable } from '@/components/PropsTable'; +import { CodeBlock } from '@/components/CodeBlock'; +import { Snippet } from '@/components/Snippet'; +import { HeaderSnippet } from '@/snippets/stories-snippets'; +import { + propDefs, + usage, + simple, + defaultSnippet, + withTabs, + withBreadcrumbs, + withHeaderPage, +} from './header.props'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; + + + +} + code={defaultSnippet} +/> + +## Usage + + + +## API reference + + + +## Examples + +### Simple header + +A simple example of how to use the Header component. + +} + code={simple} + open +/> + +### Header with tabs + +A simple example of how to use the Header component with tabs. All links are using React Router +under the hood and will be active when you are on the corresponding page. + +} +code={withTabs} +open +/> + +### Header with breadcrumbs + +Breacrumbs should appear when you scroll down (and not directly visible as it is in the demo below). + +} + code={withBreadcrumbs} + open +/> + +### Header with HeaderPage + +You can use the `Header` component inside the [HeaderPage](/components/header-page) component to compose your multi-level navigation. + +} + code={withHeaderPage} + open +/> + + + + diff --git a/docs-ui/src/content/components/header.props.ts b/docs-ui/src/content/components/header.props.ts new file mode 100644 index 0000000000..50c5540153 --- /dev/null +++ b/docs-ui/src/content/components/header.props.ts @@ -0,0 +1,218 @@ +import { + childrenPropDefs, + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const propDefs: Record = { + icon: { + type: 'enum', + values: ['ReactNode'], + }, + title: { + type: 'string', + default: 'Your plugin', + }, + titleLink: { + type: 'string', + default: '/', + }, + breadcrumbs: { + type: 'complex', + complexType: { + name: 'Breadcrumb[]', + properties: { + label: { + type: 'string', + required: true, + description: 'Display text for the breadcrumb', + }, + href: { + type: 'string', + required: true, + description: 'URL for the breadcrumb link', + }, + }, + }, + }, + customActions: { + type: 'enum', + values: ['ReactNode'], + }, + menuItems: { + type: 'complex', + complexType: { + name: 'MenuItem[]', + properties: { + label: { + type: 'string', + required: true, + description: 'Display text for the menu item', + }, + value: { + type: 'string', + required: true, + description: 'Unique value for the menu item', + }, + onClick: { + type: '() => void', + required: false, + description: 'Callback function when menu item is clicked', + }, + }, + }, + }, + tabs: { + type: 'complex', + complexType: { + name: 'HeaderTab[]', + properties: { + id: { + type: 'string', + required: true, + description: 'Unique identifier for the tab', + }, + label: { + type: 'string', + required: true, + description: 'Display text for the tab', + }, + href: { + type: 'string', + required: false, + description: 'URL to navigate to when tab is clicked', + }, + matchStrategy: { + type: "'exact' | 'prefix'", + required: false, + description: 'How to match the current route to highlight the tab', + }, + }, + }, + }, + onTabSelectionChange: { + type: 'enum', + values: ['(key: string) => void'], + }, + ...childrenPropDefs, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const usage = `import { Header } from '@backstage/ui'; + +
`; + +export const defaultSnippet = `
+ } /> + } /> + } /> + + } +/>`; + +export const simple = `
+ } /> + } /> + } /> + + } +/>`; + +export const withTabs = ` +
+ } /> + } /> + } /> + + } + tabs={[ + { id: 'overview', label: 'Overview' }, + { id: 'checks', label: 'Checks' }, + { id: 'tracks', label: 'Tracks' }, + { id: 'campaigns', label: 'Campaigns' }, + { id: 'integrations', label: 'Integrations' }, + ]} +/> +`; + +export const withBreadcrumbs = `
`; + +export const withHeaderPage = `
+Custom action} +/>`; diff --git a/docs-ui/src/content/components/tabs.mdx b/docs-ui/src/content/components/tabs.mdx index dd0552b436..26905d4733 100644 --- a/docs-ui/src/content/components/tabs.mdx +++ b/docs-ui/src/content/components/tabs.mdx @@ -3,12 +3,13 @@ import { TabsSnippet } from '@/snippets/stories-snippets'; import { Snippet } from '@/components/Snippet'; import { CodeBlock } from '@/components/CodeBlock'; import { - tabsRootPropDefs, - tabsListPropDefs, - tabsTabPropDefs, - tabsPanelPropDefs, + tabsPropDefs, + tabPropDefs, tabsUsageSnippet, tabsDefaultSnippet, + tabsWithTabPanelsSnippet, + tabsWithLinksSnippet, + tabsWithDeeplyNestedRoutesSnippet, } from './tabs.props'; import { PageTitle } from '@/components/PageTitle'; import { Theming } from '@/components/Theming'; @@ -31,29 +32,52 @@ import { ChangelogComponent } from '@/components/ChangelogComponent'; ## API reference -### Tabs.Root +### Tabs Groups the tabs and the corresponding panels. Renders a `
` element. - + -### Tabs.List - -Groups the individual tab buttons. Renders a `
` element. - - - -### Tabs.Tab +### Tab An individual interactive tab button that toggles the corresponding panel. Renders a `, + title: 'My plugin', + titleLink: '/', menuItems, }, decorators: [withRouter], + render: WithCustomActions.render, }; export const WithBreadcrumbs: Story = { args: { breadcrumbs, + tabs, }, decorators: [withRouter], }; -export const WithAllComponents: Story = { +export const WithAllOptionsAndTabs: Story = { args: { - menuItems, + ...WithAllOptions.args, tabs, - breadcrumbs, }, decorators: [withRouter], + render: WithAllOptions.render, +}; + +export const WithHeaderPage: Story = { + args: { + ...WithAllOptionsAndTabs.args, + }, + decorators: [withRouter], + render: args => ( + <> +
+ } /> + } /> + } /> + + } + /> + Custom action} + /> + + ), }; export const WithLayout: Story = { diff --git a/packages/ui/src/components/Tabs/Tabs.stories.tsx b/packages/ui/src/components/Tabs/Tabs.stories.tsx index 9f6e05e289..a0a1fa75dc 100644 --- a/packages/ui/src/components/Tabs/Tabs.stories.tsx +++ b/packages/ui/src/components/Tabs/Tabs.stories.tsx @@ -95,10 +95,10 @@ export const WithMockedURLTab2: Story = { - + Current URL is mocked to be: /tab2 - + Notice how the "Tab 2" tab is selected (highlighted) because it matches the current path. @@ -273,14 +273,16 @@ export const PrefixMatchingDeepNesting: Story = { - + Current URL: /catalog/users/john/details - + The "Catalog" tab is active because it uses prefix matching and the URL starts with "/catalog". - This works for any level of nesting under "/catalog". + + This works for any level of nesting under "/catalog". + ),