Просмотр исходного кода

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

Co-authored-by: Roo Code <[email protected]>
roomote[bot] 1 неделя назад
Родитель
Сommit
0cd257af89

+ 1 - 0
apps/web-roo-code/package.json

@@ -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",

+ 136 - 99
apps/web-roo-code/src/components/chromes/nav-bar.tsx

@@ -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">

+ 117 - 0
apps/web-roo-code/src/components/ui/navigation-menu.tsx

@@ -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,
+}

+ 39 - 1
pnpm-lock.yaml

@@ -340,6 +340,9 @@ importers:
       '@radix-ui/react-dialog':
         specifier: ^1.1.15
         version: 1.1.15(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-navigation-menu':
+        specifier: ^1.2.14
+        version: 1.2.14(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
       '@radix-ui/react-slot':
         specifier: ^1.2.4
         version: 1.2.4(@types/[email protected])([email protected])
@@ -3190,6 +3193,19 @@ packages:
       '@types/react-dom':
         optional: true
 
+  '@radix-ui/[email protected]':
+    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/[email protected]':
     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/[email protected])
 
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-collection': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-direction': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-presence': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-previous': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-visually-hidden': 1.2.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 18.3.1
+      react-dom: 18.3.1([email protected])
+    optionalDependencies:
+      '@types/react': 18.3.23
+      '@types/react-dom': 18.3.7(@types/[email protected])
+
   '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
     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/[email protected])(@types/node@24.2.1)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
+      vitest: 3.2.4(@types/[email protected])(@types/node@20.17.57)(@vitest/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
 
   '@vitest/[email protected]':
     dependencies: