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..783b4a55be
--- /dev/null
+++ b/docs-ui/src/components/StickyHeader/StickyHeader.module.css
@@ -0,0 +1,92 @@
+.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;
+ gap: 12px;
+}
+
+.versionLinks a {
+ color: var(--text-secondary);
+ transition: color 0.2s ease;
+}
+
+.versionLinks a:hover {
+ color: var(--text-primary);
+}
+
+@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..5e3fe70808 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,34 @@
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-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/css/globals.css b/docs-ui/src/css/globals.css
index 334e6a870a..9193f49b5a 100644
--- a/docs-ui/src/css/globals.css
+++ b/docs-ui/src/css/globals.css
@@ -1,5 +1,5 @@
:root {
- --bg: #f8f8f8;
+ --bg: #f4f4f4;
--panel: #fff;
--primary: #000;
--secondary: #929292;
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/utils/getPageName.ts b/docs-ui/src/utils/getPageName.ts
new file mode 100644
index 0000000000..9fba42fbb4
--- /dev/null
+++ b/docs-ui/src/utils/getPageName.ts
@@ -0,0 +1,29 @@
+import { overview, theme, 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 theme pages
+ const themePage = theme.find(p => p.slug === slug);
+ if (themePage) {
+ return themePage.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;
+}