feat: fine-tune user account dropdown menu with Radix UI and custom animations
This commit is contained in:
parent
7338a884f6
commit
d4c5dbfc57
@ -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">
|
||||||
|
|||||||
@ -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',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user