feat: fine-tune user account dropdown menu with Radix UI and custom animations

This commit is contained in:
Haitao Pan 2026-02-02 12:39:17 +08:00
parent 7338a884f6
commit d4c5dbfc57
2 changed files with 86 additions and 36 deletions

View File

@ -4,13 +4,14 @@ import Image from "next/image";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useLanguage } from "../i18n/LanguageProvider"; import { useLanguage } from "../i18n/LanguageProvider";
import { Menu, X, Sun, Moon, Monitor, Plus } from "lucide-react"; import { Menu, X, Sun, Moon, Monitor, Plus, BarChart2 } from "lucide-react";
import { translations } from "../i18n/translations"; import { translations } from "../i18n/translations";
import LanguageToggle from "./LanguageToggle"; import LanguageToggle from "./LanguageToggle";
import { AskAIButton } from "./AskAIButton"; import { AskAIButton } from "./AskAIButton";
import ReleaseChannelSelector from "./ReleaseChannelSelector"; import ReleaseChannelSelector from "./ReleaseChannelSelector";
import { useUserStore } from "@lib/userStore"; import { useUserStore } from "@lib/userStore";
import { useMoltbotStore } from "@lib/moltbotStore"; import { useMoltbotStore } from "@lib/moltbotStore";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { import {
createNavConfig, createNavConfig,
filterNavItems, filterNavItems,
@ -272,42 +273,65 @@ export default function UnifiedNavigation() {
</nav> </nav>
</div> </div>
<div className="hidden flex-1 items-center justify-end gap-4 lg:flex"> <div className="hidden flex-1 items-center justify-end gap-3 lg:flex">
{user ? ( {user ? (
<div className="relative" ref={accountMenuRef}> <div className="flex items-center gap-2">
<button <DropdownMenu.Root open={accountMenuOpen} onOpenChange={setAccountMenuOpen}>
type="button" <DropdownMenu.Trigger asChild>
onClick={() => setAccountMenuOpen((prev) => !prev)} <button
className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-primary to-accent text-sm font-semibold text-white shadow-shadow-sm transition hover:from-primary-hover hover:to-accent focus:outline-none focus:ring-2 focus:ring-primary/60 focus:ring-offset-2 focus:ring-offset-background" type="button"
aria-haspopup="menu" className="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-primary to-accent text-sm font-semibold text-white shadow-shadow-sm transition hover:from-primary-hover hover:to-accent focus:outline-none focus:ring-2 focus:ring-primary/60 focus:ring-offset-2 focus:ring-offset-background outline-none ring-offset-background"
aria-expanded={accountMenuOpen} aria-label="User account menu"
> >
{accountInitial} {accountInitial}
</button> </button>
{accountMenuOpen ? ( </DropdownMenu.Trigger>
<div className="absolute right-0 mt-2 w-56 overflow-hidden rounded-xl border border-surface-border bg-surface/95 shadow-shadow-md">
<div className="border-b border-surface-border bg-surface-muted px-4 py-3"> <DropdownMenu.Portal>
<p className="text-sm font-semibold text-text"> <DropdownMenu.Content
{user.username} align="end"
</p> sideOffset={8}
<p className="text-xs text-text-muted">{user.email}</p> className="z-50 min-w-[220px] overflow-hidden rounded-[12px] border border-surface-border bg-surface/95 p-1 shadow-shadow-md backdrop-blur-sm animate-in fade-in zoom-in-95 duration-[120ms] data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:zoom-out-95 motion-reduce:animate-none"
</div> >
<div className="py-1 text-sm text-text"> <div className="px-4 py-3 border-b border-surface-border/50 mb-1">
{accountNav.map((item) => ( <p className="text-sm font-semibold text-text leading-none mb-1.5">
<Link {user.username}
key={item.key} </p>
href={item.href} <p className="text-[12px] text-text-muted leading-none">
className="block px-4 py-2 text-sm opacity-80 transition hover:bg-primary/10 hover:opacity-100" {user.email}
onClick={() => setAccountMenuOpen(false)} </p>
> </div>
{typeof item.label === "function"
? item.label(language) <div className="space-y-0.5">
: item.label} {accountNav.map((item) => (
</Link> <DropdownMenu.Item
))} key={item.key}
</div> asChild
</div> className="outline-none"
) : null} >
<Link
href={item.href}
className={`flex h-[38px] items-center gap-2.5 px-3 rounded-lg text-[13px] font-medium transition-all group select-none ${item.key === 'logout'
? "text-rose-500 hover:bg-rose-500/10 hover:text-rose-600 focus:bg-rose-500/10 focus:text-rose-600"
: "text-text-muted hover:bg-primary/10 hover:text-primary focus:bg-primary/10 focus:text-primary"
}`}
onClick={() => setAccountMenuOpen(false)}
>
{item.icon && (
<item.icon className={`w-4 h-4 opacity-70 group-hover:opacity-100 transition-opacity ${item.key === 'logout' ? 'text-rose-500' : 'text-current'}`} />
)}
<span>
{typeof item.label === "function"
? item.label(language)
: item.label}
</span>
</Link>
</DropdownMenu.Item>
))}
</div>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-3 text-sm font-medium text-text-muted"> <div className="flex items-center gap-3 text-sm font-medium text-text-muted">

View File

@ -85,6 +85,32 @@ const tailwindConfig = {
boxShadow: { boxShadow: {
soft: '0 35px 80px -45px rgba(37, 78, 219, 0.35), 0 25px 60px -40px rgba(15, 23, 42, 0.25)', soft: '0 35px 80px -45px rgba(37, 78, 219, 0.35), 0 25px 60px -40px rgba(15, 23, 42, 0.25)',
}, },
// 自定义动效
keyframes: {
'fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
'fade-out': {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
'scale-in': {
'0%': { transform: 'scale(0.95)' },
'100%': { transform: 'scale(1)' },
},
'scale-out': {
'0%': { transform: 'scale(1)' },
'100%': { transform: 'scale(0.95)' },
},
},
animation: {
'fade-in': 'fade-in 120ms ease-out',
'fade-out': 'fade-out 120ms ease-in',
'zoom-in-95': 'scale-in 120ms ease-out',
'zoom-out-95': 'scale-out 120ms ease-in',
},
}, },
}, },