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 (
-
+
-
-
-
+
+
+ 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 `
` element.
-
+
-### Tabs.Panel
+## Examples
-A panel displayed when the corresponding tab is active. Renders a `` element.
+### Simple tabs
-
+To connect the tabs with the panels, you need to use the `id` prop on the tab and the tab panel.
+
+
}
+ code={tabsWithTabPanelsSnippet}
+ open
+/>
+
+### Tabs as links
+
+You can use the `href` prop on the tab to make it a link. This will use the `react-router` under the hood to navigate to the tab. We automatically detect if this is an external URL and render a `
` element instead of a ``.
+
+ }
+ code={tabsWithLinksSnippet}
+ open
+/>
+
+### With deeply nested routes
+
+You can use the `matchStrategy` prop on the tab to control how the tab is matched to the current URL.
+
+ }
+ code={tabsWithDeeplyNestedRoutesSnippet}
+ open
+/>
diff --git a/docs-ui/src/content/components/tabs.props.ts b/docs-ui/src/content/components/tabs.props.ts
index f231a85283..eb9f4b97de 100644
--- a/docs-ui/src/content/components/tabs.props.ts
+++ b/docs-ui/src/content/components/tabs.props.ts
@@ -5,55 +5,66 @@ import {
type PropDef,
} from '@/utils/propDefs';
-export const tabsRootPropDefs: Record = {
- defaultValue: {
- type: 'enum',
- values: ['any'],
- default: '0',
+export const tabsPropDefs: Record = {
+ isDisabled: {
+ type: 'boolean',
},
- value: {
+ disabledKeys: {
type: 'enum',
- values: ['any'],
+ values: ['string[]'],
},
- onValueChange: {
+ selectedKey: {
type: 'enum',
- values: [`((value) => void)`],
+ values: ['string', 'null'],
+ },
+ defaultSelectedKey: {
+ type: 'enum',
+ values: ['string'],
+ },
+ onSelectionChange: {
+ type: 'enum',
+ values: [`(key: string) => void`],
},
...childrenPropDefs,
...classNamePropDefs,
...stylePropDefs,
};
-export const tabsListPropDefs: Record = {
- activateOnFocus: {
- type: 'boolean',
- default: 'false',
- },
- loop: {
- type: 'boolean',
- default: 'false',
- },
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const tabsTabPropDefs: Record = {
- value: {
+export const tabPropDefs: Record = {
+ id: {
type: 'string',
},
- ...childrenPropDefs,
- ...classNamePropDefs,
- ...stylePropDefs,
-};
-
-export const tabsPanelPropDefs: Record = {
- value: {
+ isDisabled: {
+ type: 'boolean',
+ },
+ href: {
type: 'string',
},
- keepMounted: {
- type: 'boolean',
- default: 'false',
+ hrefLang: {
+ type: 'string',
+ },
+ target: {
+ type: 'enum',
+ values: ['HTMLAttributeAnchorTarget'],
+ },
+ rel: {
+ type: 'string',
+ },
+ matchStrategy: {
+ type: 'enum',
+ values: ['exact', 'prefix'],
+ },
+ onHoverStart: {
+ type: 'enum',
+ values: [`(e: HoverEvent) => void`],
+ },
+ onHoverEnd: {
+ type: 'enum',
+ values: [`(e: HoverEvent) => void`],
+ },
+ onHoverChange: {
+ type: 'enum',
+ values: [`(isHovering: boolean) => void`],
},
...childrenPropDefs,
...classNamePropDefs,
@@ -62,24 +73,50 @@ export const tabsPanelPropDefs: Record = {
export const tabsUsageSnippet = `import { Tabs } from '@backstage/ui';
-
-
- Tab 1
- Tab 2
- Tab 3
-
- Content for Tab 1
- Content for Tab 2
- Content for Tab 3
- `;
+
+
+ Tab 1
+ Tab 2
+ Tab 3
+
+ Content for Tab 1
+ Content for Tab 2
+ Content for Tab 3
+ `;
-export const tabsDefaultSnippet = `
-
- Tab 1
- Tab 2
- Tab 3 With long title
-
- Content for Tab 1
- Content for Tab 2
- Content for Tab 3
- `;
+export const tabsDefaultSnippet = `import { Tabs } from '@backstage/ui';
+
+
+
+ Tab 1
+ Tab 2
+ Tab 3 With long title
+
+ `;
+
+export const tabsWithTabPanelsSnippet = `
+
+ Settings
+ Profile
+ Preferences
+
+ Settings panel content goes here
+ Profile panel content goes here
+ Preferences panel content goes here
+ `;
+
+export const tabsWithLinksSnippet = `
+
+ Tab 1
+ Tab 2
+ Tab 3 With long title
+
+ `;
+
+export const tabsWithDeeplyNestedRoutesSnippet = `
+
+ Home
+ Catalog
+ Mentorship
+
+ `;
diff --git a/docs-ui/src/css/globals.css b/docs-ui/src/css/globals.css
index 334e6a870a..5d8422bd30 100644
--- a/docs-ui/src/css/globals.css
+++ b/docs-ui/src/css/globals.css
@@ -1,6 +1,7 @@
:root {
- --bg: #f8f8f8;
+ --bg: #f4f4f4;
--panel: #fff;
+ --panel-hover: #fafafa;
--primary: #000;
--secondary: #929292;
--action: #fff;
@@ -13,17 +14,20 @@
--font-weight-regular: 400;
--font-weight-bold: 600;
--border: #e5e5e5;
+ --border2: #cdcdcd;
--surface-1: #f4f4f4;
}
[data-theme='dark'] {
--bg: #000;
--panel: #181818;
+ --panel-hover: #202020;
--primary: #fff;
--secondary: #818181;
--action: #202020;
--link: #4856e8;
--border: #2a2a2a;
+ --border2: #2a2a2a;
--surface-1: #282828;
}
diff --git a/docs-ui/src/hooks/useCurrentPage.ts b/docs-ui/src/hooks/useCurrentPage.ts
new file mode 100644
index 0000000000..1f5f978846
--- /dev/null
+++ b/docs-ui/src/hooks/useCurrentPage.ts
@@ -0,0 +1,31 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+import { getPageName } from '@/utils/getPageName';
+
+export function useCurrentPage(): string | null {
+ const pathname = usePathname();
+
+ // Handle root path
+ if (pathname === '/') {
+ return 'Getting Started';
+ }
+
+ // Extract slug from various path patterns
+ const patterns = [
+ /^\/components\/(.+)$/, // /components/button
+ /^\/theme\/(.+)$/, // /theme/typography
+ /^\/about$/, // /about
+ /^\/changelog$/, // /changelog
+ ];
+
+ for (const pattern of patterns) {
+ const match = pathname.match(pattern);
+ if (match) {
+ const slug = match[1] || pathname.slice(1); // Use full path for exact matches
+ return getPageName(slug);
+ }
+ }
+
+ return null;
+}
diff --git a/docs-ui/src/snippets/stories-snippets.tsx b/docs-ui/src/snippets/stories-snippets.tsx
index dea8351778..92d4362d98 100644
--- a/docs-ui/src/snippets/stories-snippets.tsx
+++ b/docs-ui/src/snippets/stories-snippets.tsx
@@ -24,220 +24,46 @@ import * as SearchFieldStories from '../../../packages/ui/src/components/SearchF
import * as TooltipStories from '../../../packages/ui/src/components/Tooltip/Tooltip.stories';
import * as SkeletonStories from '../../../packages/ui/src/components/Skeleton/Skeleton.stories';
import * as CardStories from '../../../packages/ui/src/components/Card/Card.stories';
+import * as HeaderStories from '../../../packages/ui/src/components/Header/Header.stories';
+import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories';
-export const BoxSnippet = ({ story }: { story: keyof typeof BoxStories }) => {
- const stories = composeStories(BoxStories);
- const StoryComponent = stories[story as keyof typeof stories];
+// Helper function to create snippet components
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const createSnippetComponent = (stories: any) => {
+ return function SnippetComponent({ story }: { story: string }) {
+ const composedStories = composeStories(stories);
+ const StoryComponent = composedStories[
+ story as keyof typeof composedStories
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ] as any;
- return StoryComponent ? : null;
+ return StoryComponent ? : null;
+ };
};
-export const ButtonSnippet = ({
- story,
-}: {
- story: keyof typeof ButtonStories;
-}) => {
- const stories = composeStories(ButtonStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const ButtonIconSnippet = ({
- story,
-}: {
- story: keyof typeof ButtonIconStories;
-}) => {
- const stories = composeStories(ButtonIconStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const ButtonLinkSnippet = ({
- story,
-}: {
- story: keyof typeof ButtonLinkStories;
-}) => {
- const stories = composeStories(ButtonLinkStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const CheckboxSnippet = ({
- story,
-}: {
- story: keyof typeof CheckboxStories;
-}) => {
- const stories = composeStories(CheckboxStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const ContainerSnippet = ({
- story,
-}: {
- story: keyof typeof ContainerStories;
-}) => {
- const stories = composeStories(ContainerStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const FlexSnippet = ({ story }: { story: keyof typeof FlexStories }) => {
- const stories = composeStories(FlexStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const TextFieldSnippet = ({
- story,
-}: {
- story: keyof typeof TextFieldStories;
-}) => {
- const stories = composeStories(TextFieldStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const GridSnippet = ({ story }: { story: keyof typeof GridStories }) => {
- const stories = composeStories(GridStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const IconSnippet = ({ story }: { story: keyof typeof IconStories }) => {
- const stories = composeStories(IconStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const TextSnippet = ({ story }: { story: keyof typeof TextStories }) => {
- const stories = composeStories(TextStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const SelectSnippet = ({
- story,
-}: {
- story: keyof typeof SelectStories;
-}) => {
- const stories = composeStories(SelectStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const MenuSnippet = ({ story }: { story: keyof typeof MenuStories }) => {
- const stories = composeStories(MenuStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const LinkSnippet = ({ story }: { story: keyof typeof LinkStories }) => {
- const stories = composeStories(LinkStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const AvatarSnippet = ({
- story,
-}: {
- story: keyof typeof AvatarStories;
-}) => {
- const stories = composeStories(AvatarStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const CollapsibleSnippet = ({
- story,
-}: {
- story: keyof typeof CollapsibleStories;
-}) => {
- const stories = composeStories(CollapsibleStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const TabsSnippet = ({ story }: { story: keyof typeof TabsStories }) => {
- const stories = composeStories(TabsStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const SwitchSnippet = ({
- story,
-}: {
- story: keyof typeof SwitchStories;
-}) => {
- const stories = composeStories(SwitchStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const RadioGroupSnippet = ({
- story,
-}: {
- story: keyof typeof RadioGroupStories;
-}) => {
- const stories = composeStories(RadioGroupStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const SearchFieldSnippet = ({
- story,
-}: {
- story: keyof typeof SearchFieldStories;
-}) => {
- const stories = composeStories(SearchFieldStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const TooltipSnippet = ({
- story,
-}: {
- story: keyof typeof TooltipStories;
-}) => {
- const stories = composeStories(TooltipStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const SkeletonSnippet = ({
- story,
-}: {
- story: keyof typeof SkeletonStories;
-}) => {
- const stories = composeStories(SkeletonStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
-
-export const CardSnippet = ({ story }: { story: keyof typeof CardStories }) => {
- const stories = composeStories(CardStories);
- const StoryComponent = stories[story as keyof typeof stories];
-
- return StoryComponent ? : null;
-};
+// Create snippet components using the helper function
+export const BoxSnippet = createSnippetComponent(BoxStories);
+export const ButtonSnippet = createSnippetComponent(ButtonStories);
+export const ButtonIconSnippet = createSnippetComponent(ButtonIconStories);
+export const ButtonLinkSnippet = createSnippetComponent(ButtonLinkStories);
+export const CheckboxSnippet = createSnippetComponent(CheckboxStories);
+export const ContainerSnippet = createSnippetComponent(ContainerStories);
+export const GridSnippet = createSnippetComponent(GridStories);
+export const IconSnippet = createSnippetComponent(IconStories);
+export const TextFieldSnippet = createSnippetComponent(TextFieldStories);
+export const TextSnippet = createSnippetComponent(TextStories);
+export const FlexSnippet = createSnippetComponent(FlexStories);
+export const SelectSnippet = createSnippetComponent(SelectStories);
+export const MenuSnippet = createSnippetComponent(MenuStories);
+export const LinkSnippet = createSnippetComponent(LinkStories);
+export const AvatarSnippet = createSnippetComponent(AvatarStories);
+export const CollapsibleSnippet = createSnippetComponent(CollapsibleStories);
+export const RadioGroupSnippet = createSnippetComponent(RadioGroupStories);
+export const TabsSnippet = createSnippetComponent(TabsStories);
+export const SwitchSnippet = createSnippetComponent(SwitchStories);
+export const SearchFieldSnippet = createSnippetComponent(SearchFieldStories);
+export const TooltipSnippet = createSnippetComponent(TooltipStories);
+export const SkeletonSnippet = createSnippetComponent(SkeletonStories);
+export const CardSnippet = createSnippetComponent(CardStories);
+export const HeaderSnippet = createSnippetComponent(HeaderStories);
+export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories);
diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts
index 618bfb3832..752f28c2c5 100644
--- a/docs-ui/src/utils/data.ts
+++ b/docs-ui/src/utils/data.ts
@@ -9,6 +9,10 @@ export const overview: Page[] = [
title: 'Getting Started',
slug: '',
},
+ {
+ title: 'Installation',
+ slug: 'install',
+ },
{
title: 'About',
slug: 'about',
@@ -19,7 +23,7 @@ export const overview: Page[] = [
},
];
-export const theme: Page[] = [
+export const coreConcepts: Page[] = [
{
title: 'Iconography',
slug: 'iconography',
@@ -36,10 +40,6 @@ export const theme: Page[] = [
title: 'Theming',
slug: 'theming',
},
- {
- title: 'Typography',
- slug: 'typography',
- },
];
export const layoutComponents: Page[] = [
@@ -101,6 +101,16 @@ export const components: Page[] = [
slug: 'collapsible',
status: 'alpha',
},
+ {
+ title: 'Header',
+ slug: 'header',
+ status: 'alpha',
+ },
+ {
+ title: 'HeaderPage',
+ slug: 'header-page',
+ status: 'alpha',
+ },
{
title: 'Icon',
slug: 'icon',
diff --git a/docs-ui/src/utils/getPageName.ts b/docs-ui/src/utils/getPageName.ts
new file mode 100644
index 0000000000..a3298bc9b7
--- /dev/null
+++ b/docs-ui/src/utils/getPageName.ts
@@ -0,0 +1,29 @@
+import { overview, coreConcepts, components, layoutComponents } from './data';
+
+export function getPageName(slug: string): string | null {
+ // Search in overview pages
+ const overviewPage = overview.find(p => p.slug === slug);
+ if (overviewPage) {
+ return overviewPage.title;
+ }
+
+ // Search in core concepts array
+ const coreConcept = coreConcepts.find(c => c.slug === slug);
+ if (coreConcept) {
+ return coreConcept.title;
+ }
+
+ // Search in components array
+ const component = components.find(c => c.slug === slug);
+ if (component) {
+ return component.title;
+ }
+
+ // Search in layoutComponents array
+ const layoutComponent = layoutComponents.find(c => c.slug === slug);
+ if (layoutComponent) {
+ return layoutComponent.title;
+ }
+
+ return null;
+}
diff --git a/docs-ui/src/utils/propDefs.ts b/docs-ui/src/utils/propDefs.ts
index 27cba2c2de..c26a321d0b 100644
--- a/docs-ui/src/utils/propDefs.ts
+++ b/docs-ui/src/utils/propDefs.ts
@@ -2,9 +2,29 @@ import type { Breakpoint } from '@backstage/ui/src/types';
const breakpoints = ['initial', 'xs', 'sm', 'md', 'lg', 'xl'] as Breakpoint[];
+export type ComplexTypeDef = {
+ name: string;
+ properties: Record<
+ string,
+ {
+ type: string;
+ required?: boolean;
+ description?: string;
+ }
+ >;
+};
+
export type PropDef = {
- type: 'string' | 'enum' | 'enum | string' | 'number' | 'boolean';
+ type:
+ | 'string'
+ | 'enum'
+ | 'enum | string'
+ | 'number'
+ | 'boolean'
+ | 'spacing'
+ | 'complex';
values?: string | string[];
+ complexType?: ComplexTypeDef;
default?: string;
required?: boolean;
responsive?: boolean;
diff --git a/packages/ui/src/components/Header/Header.stories.tsx b/packages/ui/src/components/Header/Header.stories.tsx
index d09309ff3e..302aef9765 100644
--- a/packages/ui/src/components/Header/Header.stories.tsx
+++ b/packages/ui/src/components/Header/Header.stories.tsx
@@ -169,27 +169,74 @@ export const WithOptions: Story = {
};
export const WithCustomActions: Story = {
+ args: {},
+ decorators: [withRouter],
+ render: args => (
+
+ } />
+ } />
+ } />
+ >
+ }
+ />
+ ),
+};
+
+export const WithAllOptions: Story = {
args: {
- customActions: Custom action ,
+ 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".
+
),