feat(web): Replace bespoke navigation menu with shadcn navigation menu (#11117)

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
roomote[bot] 2026-01-30 09:38:46 -08:00 committed by GitHub
parent d8f65b655d
commit 0cd257af89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 293 additions and 100 deletions

View file

@ -13,6 +13,7 @@
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-slot": "^1.2.4",
"@roo-code/evals": "workspace:^",
"@roo-code/types": "^1.108.0",

View file

@ -13,7 +13,17 @@ import { EXTERNAL_LINKS } from "@/lib/constants"
import { useLogoSrc } from "@/lib/hooks/use-logo-src"
import { ScrollButton } from "@/components/ui"
import ThemeToggle from "@/components/chromes/theme-toggle"
import { Brain, ChevronDown, Cloud, Puzzle, Slack, X } from "lucide-react"
import { Brain, Cloud, Puzzle, Slack, X } from "lucide-react"
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import { cn } from "@/lib/utils"
function LinearIcon({ className }: { className?: string }) {
return (
@ -30,115 +40,142 @@ interface NavBarProps {
export function NavBar({ stars, downloads }: NavBarProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [openDropdown, setOpenDropdown] = useState<string | null>(null)
const logoSrc = useLogoSrc()
return (
<header className="sticky font-light top-0 z-50 border-b border-border bg-background/80 backdrop-blur-md">
<div className="container flex h-16 items-center justify-between px-4 sm:px-6 lg:px-8">
<div className="flex items-center">
<div className="flex items-center flex-shrink-0">
<Link href="/" className="flex items-center">
<Image src={logoSrc} alt="Roo Code Logo" width={130} height={24} className="h-[24px] w-auto" />
</Link>
</div>
{/* Desktop Navigation */}
<nav className="grow ml-6 hidden text-sm md:flex md:items-center">
{/* Product Dropdown */}
<div
className="relative"
onMouseEnter={() => setOpenDropdown("product")}
onMouseLeave={() => setOpenDropdown(null)}>
<button className="flex items-center px-4 py-6 gap-1 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Product
<ChevronDown className="size-3 ml-1 mt-0.5" />
</button>
<div
className={`absolute left-0 top-12 mt-2 w-[260px] rounded-md border border-border bg-background py-1 shadow-lg transition-all duration-200 ${openDropdown === "product" ? "opacity-100 translate-y-0 pointer-events-auto" : "opacity-0 -translate-y-2 pointer-events-none"}`}>
<Link
href="/extension"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setOpenDropdown(null)}>
<Puzzle className="size-3 inline mr-2 -mt-0.5" />
Roo Code VS Code Extension
</Link>
<Link
href="/cloud"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setOpenDropdown(null)}>
<Cloud className="size-3 inline mr-2 -mt-0.5" />
Roo Code Cloud
</Link>
<Link
href="/slack"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setOpenDropdown(null)}>
<Slack className="size-3 inline mr-2 -mt-0.5" />
Roo Code for Slack
</Link>
<Link
href="/linear"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setOpenDropdown(null)}>
<LinearIcon className="size-3 inline mr-2 -mt-0.5" />
Roo Code for Linear
</Link>
<Link
href="/provider"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setOpenDropdown(null)}>
<Brain className="size-3 inline mr-2 -mt-0.5" />
Roo Code Router
</Link>
</div>
</div>
{/* Resources Dropdown */}
<div
className="relative"
onMouseEnter={() => setOpenDropdown("resources")}
onMouseLeave={() => setOpenDropdown(null)}>
<button className="flex items-center px-4 py-6 gap-1 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Resources
<ChevronDown className="size-3 ml-1 mt-0.5" />
</button>
<div
className={`absolute left-0 top-12 mt-2 w-40 rounded-md border border-border bg-background py-1 shadow-lg transition-all duration-200 ${openDropdown === "resources" ? "opacity-100 translate-y-0 pointer-events-auto" : "opacity-0 -translate-y-2 pointer-events-none"}`}>
<Link
href="/evals"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setOpenDropdown(null)}>
Evals
</Link>
<a
href={EXTERNAL_LINKS.DISCORD}
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setOpenDropdown(null)}>
Discord
</a>
<a
href={EXTERNAL_LINKS.SECURITY}
target="_blank"
rel="noopener noreferrer"
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setOpenDropdown(null)}>
Trust Center
</a>
</div>
</div>
<a
href={EXTERNAL_LINKS.DOCUMENTATION}
target="_blank"
className="px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Docs
</a>
<Link
href="/pricing"
className="px-4 py-6 transition-transform duration-200 hover:scale-105 hover:text-foreground">
Pricing
</Link>
</nav>
<NavigationMenu className="grow ml-6 hidden text-sm md:flex">
<NavigationMenuList>
{/* Product Dropdown */}
<NavigationMenuItem>
<NavigationMenuTrigger className="bg-transparent font-light">Product</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid min-w-[260px] gap-1 p-2">
<li>
<NavigationMenuLink asChild>
<Link
href="/extension"
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
<Puzzle className="size-3 mr-2" />
Roo Code VS Code Extension
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link
href="/cloud"
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
<Cloud className="size-3 mr-2" />
Roo Code Cloud
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link
href="/slack"
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
<Slack className="size-3 mr-2" />
Roo Code for Slack
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link
href="/linear"
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
<LinearIcon className="size-3 mr-2" />
Roo Code for Linear
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<Link
href="/provider"
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
<Brain className="size-3 mr-2" />
Roo Code Router
</Link>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
{/* Resources Dropdown */}
<NavigationMenuItem>
<NavigationMenuTrigger className="bg-transparent font-light">
Resources
</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid min-w-[260px] gap-1 p-2">
<li>
<NavigationMenuLink asChild>
<Link
href="/evals"
className="block select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
Evals
</Link>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<a
href={EXTERNAL_LINKS.DISCORD}
target="_blank"
rel="noopener noreferrer"
className="block select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
Discord
</a>
</NavigationMenuLink>
</li>
<li>
<NavigationMenuLink asChild>
<a
href={EXTERNAL_LINKS.SECURITY}
target="_blank"
rel="noopener noreferrer"
className="block select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
Trust Center
</a>
</NavigationMenuLink>
</li>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
{/* Docs Link */}
<NavigationMenuItem>
<NavigationMenuLink
asChild
className={cn(navigationMenuTriggerStyle(), "bg-transparent font-light")}>
<a href={EXTERNAL_LINKS.DOCUMENTATION} target="_blank">
Docs
</a>
</NavigationMenuLink>
</NavigationMenuItem>
{/* Pricing Link */}
<NavigationMenuItem>
<NavigationMenuLink
asChild
className={cn(navigationMenuTriggerStyle(), "bg-transparent font-light")}>
<Link href="/pricing">Pricing</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
<div className="hidden md:flex md:items-center md:space-x-4 flex-shrink-0 font-medium">
<div className="flex flex-row space-x-2 flex-shrink-0">

View file

@ -0,0 +1,117 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn("relative z-10 flex max-w-max flex-1 items-center", className)}
{...props}>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn("group flex flex-1 list-none items-center space-x-1", className)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent",
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className,
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

40
pnpm-lock.yaml generated
View file

@ -340,6 +340,9 @@ importers:
'@radix-ui/react-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-navigation-menu':
specifier: ^1.2.14
version: 1.2.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@18.3.23)(react@18.3.1)
@ -3190,6 +3193,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-navigation-menu@1.2.14':
resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==}
peerDependencies:
'@types/react': ^18.3.23
'@types/react-dom': ^18.3.5
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-popover@1.1.13':
resolution: {integrity: sha512-84uqQV3omKDR076izYgcha6gdpN8m3z6w/AeJ83MSBJYVG/AbOHdLjAgsPZkeC/kt+k64moXFCnio8BbqXszlw==}
peerDependencies:
@ -13259,6 +13275,28 @@ snapshots:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1)
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.23
'@types/react-dom': 18.3.7(@types/react@18.3.23)
'@radix-ui/react-popover@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.2
@ -15032,7 +15070,7 @@ snapshots:
sirv: 3.0.1
tinyglobby: 0.2.14
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
'@vitest/utils@3.2.4':
dependencies: