fix(nav): replace NavigationMenu submenus with DropdownMenu

The Radix NavigationMenu uses a single shared viewport centered under
the full nav list. Opening a right-side submenu (Analyzer, Admin)
positioned the dropdown panel far to the left, so moving the mouse to
reach it crossed other triggers and closed it mid-way.

Switch all items-with-children to DropdownMenu, which positions each
panel directly under its own trigger. Keeps the same visual style
(navigationMenuTriggerStyle, ChevronDown, item layout with icon +
description) and active-state highlighting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lorentz 2026-05-03 11:18:52 -04:00
parent 370125d249
commit 2d4a546a8f

View file

@ -17,16 +17,21 @@ import {
Brain, Brain,
Search, Search,
AlertTriangle, AlertTriangle,
ChevronDown,
} from 'lucide-react'; } from 'lucide-react';
import { import {
NavigationMenu, NavigationMenu,
NavigationMenuContent,
NavigationMenuItem, NavigationMenuItem,
NavigationMenuLink, NavigationMenuLink,
NavigationMenuList, NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle, navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu'; } from '@/components/ui/navigation-menu';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ThemeToggle } from '@/components/theme-toggle'; import { ThemeToggle } from '@/components/theme-toggle';
import { StatusIndicator } from '@/components/navigation/status-indicator'; import { StatusIndicator } from '@/components/navigation/status-indicator';
@ -242,57 +247,57 @@ export function AppNavigation() {
return ( return (
<NavigationMenuItem key={item.title}> <NavigationMenuItem key={item.title}>
{item.children ? ( {item.children ? (
<> <DropdownMenu>
<NavigationMenuTrigger <DropdownMenuTrigger asChild>
className={cn( <button
'h-9 px-4 py-2', className={cn(
childActive && cn(activeRule, 'text-foreground'), navigationMenuTriggerStyle(),
)} 'h-9 px-4 py-2 group',
> childActive && cn(activeRule, 'text-foreground'),
{item.icon && <item.icon className="w-4 h-4 mr-2" />} )}
{item.title} >
</NavigationMenuTrigger> {item.icon && <item.icon className="w-4 h-4 mr-2" />}
<NavigationMenuContent> {item.title}
<ul className="grid gap-1 p-2 min-w-[320px] max-w-[440px]"> <ChevronDown className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180" aria-hidden="true" />
{item.children.map((child) => { </button>
const active = isActive(child.href); </DropdownMenuTrigger>
return ( <DropdownMenuContent align="start" className="p-2 min-w-[320px] max-w-[440px]">
<li key={child.title}> {item.children.map((child) => {
<NavigationMenuLink asChild> const active = isActive(child.href);
<Link return (
href={child.href || '#'} <DropdownMenuItem key={child.title} asChild className="p-0 focus:bg-transparent">
<Link
href={child.href || '#'}
className={cn(
'flex w-full items-start gap-3 rounded-sm px-3 py-2 text-sm leading-none no-underline outline-none transition-colors cursor-pointer',
'hover:bg-accent/40 focus:bg-accent/40',
active && 'bg-primary/10 text-primary',
)}
>
{child.icon && (
<child.icon
className={cn( className={cn(
'flex items-start gap-3 rounded-sm px-3 py-2 leading-none no-underline outline-none transition-colors', 'w-4 h-4 mt-0.5 shrink-0',
'hover:bg-accent/40 focus:bg-accent/40', active ? 'text-primary' : 'text-muted-foreground',
active && 'bg-primary/10 text-primary',
)} )}
> />
{child.icon && ( )}
<child.icon <div className="min-w-0 flex-1">
className={cn( <div className={cn('font-medium leading-none', active && 'text-primary')}>
'w-4 h-4 mt-0.5 shrink-0', {child.title}
active ? 'text-primary' : 'text-muted-foreground', </div>
)} {child.description && (
/> <p className="mt-1 line-clamp-2 text-xs leading-snug text-muted-foreground">
)} {child.description}
<div className="min-w-0 flex-1"> </p>
<div className={cn('text-sm font-medium leading-none', active && 'text-primary')}> )}
{child.title} </div>
</div> </Link>
{child.description && ( </DropdownMenuItem>
<p className="mt-1 line-clamp-2 text-xs leading-snug text-muted-foreground"> );
{child.description} })}
</p> </DropdownMenuContent>
)} </DropdownMenu>
</div>
</Link>
</NavigationMenuLink>
</li>
);
})}
</ul>
</NavigationMenuContent>
</>
) : ( ) : (
<Link href={item.href || '#'} legacyBehavior passHref> <Link href={item.href || '#'} legacyBehavior passHref>
<NavigationMenuLink <NavigationMenuLink