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/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..6b0db7e4fe 100644 --- a/docs-ui/src/components/Sidebar/index.tsx +++ b/docs-ui/src/components/Sidebar/index.tsx @@ -36,26 +36,12 @@ 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..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 ( -
+
- - - -
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/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; +}