Files
dusuniot/src/components/ui/navigation-menu.tsx
T

110 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 组件 shadcn/ui NavigationMenu
* 基于 @radix-ui/react-navigation-menu,用于顶部主导航(含巨幕面板)。
* 样式对齐原站 Max Mega Menu(megamenu 内联 CSS 实测值):
* - 顶级项:高 68px、16px/400/#222,hover/展开仅文字变橙(背景不变);
* 指示器为实心三角 ▾(dashicons arrow-down 等价物),展开不旋转、颜色随文字。
* - 巨幕/飞出面板:#fcfbfa 底 + 顶部 2px #f5b279 橙线 + 0 10px 30px 阴影,直角。
* 切换动效保留 shadcn/ui(tw-animate-css 的 fade/zoom/slide + Radix motion 属性)。
*/
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cn } from "~/lib/utils";
/** # 组件 导航菜单根容器 */
function NavigationMenu({ className, children, viewportPosition = "inline", ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & { viewportPosition?: "inline" | "full" }) {
return (
<NavigationMenuPrimitive.Root data-viewport={viewportPosition} className={cn("relative z-[90] flex max-w-max items-center justify-center", className)} {...props}>
{children}
{viewportPosition === "full" ? <ViewportFullWidth /> : <ViewportCentered />}
</NavigationMenuPrimitive.Root>
);
}
/** # 组件 导航菜单列表(顶级项容器)
* p-0:本项目不引入 Tailwind preflight,需重置 UL 的浏览器默认左内边距。
*/
function NavigationMenuList({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return <NavigationMenuPrimitive.List className={cn("m-0 flex flex-1 list-none items-center justify-center gap-0.5 p-0", className)} {...props} />;
}
/** # 组件 导航菜单项 */
function NavigationMenuItem({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return <NavigationMenuPrimitive.Item className={cn("relative", className)} {...props} />;
}
/** # 类型 NavigationMenuTrigger 属性 */
const triggerStyle =
"inline-flex h-[68px] w-max cursor-pointer items-center justify-center gap-[6px] rounded-none border-0 bg-transparent py-0 pr-[6px] pl-2 text-[16px] font-normal whitespace-nowrap text-[#222] transition-colors hover:text-brand focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-brand";
/** # 组件 导航菜单触发器(顶级可展开项)
* chevron=false 时子元素原样透传(asChild 场景:Radix Slot 要求 children 是
* 单个 React 元素,不能被 JSX 双槽包装成数组)。
*/
function NavigationMenuTrigger({ className, children, chevron = true, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger> & { chevron?: boolean }) {
const content = chevron ? (
<>
{children}
<CaretDown />
</>
) : (
children
);
return (
<NavigationMenuPrimitive.Trigger className={cn(triggerStyle, "group", className)} {...props}>
{content}
</NavigationMenuPrimitive.Trigger>
);
}
/** # 组件 导航菜单内容面板(下拉/巨幕内容)
* data-[motion] 动画类依赖 tw-animate-css(app.css 已引入)。
*/
function NavigationMenuContent({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return <NavigationMenuPrimitive.Content className={cn("data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion=from-end]:fade-in data-[motion=from-start]:fade-in data-[motion=to-end]:fade-out data-[motion=to-start]:fade-out", className)} {...props} />;
}
/** # 组件 导航菜单链接(无下级的普通链接项) */
function NavigationMenuLink({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return <NavigationMenuPrimitive.Link className={cn(triggerStyle, className)} {...props} />;
}
/** # 组件 巨幕面板标题(面板内小标题/副标题) */
function NavigationMenuHeading({ className, ...props }: React.ComponentProps<"div">) {
return <div className={cn("text-[16px] leading-relaxed font-bold text-[#222]", className)} {...props} />;
}
/** # 组件 实心下三角指示器(等价原站 dashicons arrow-down,展开不旋转,颜色随文字) */
function CaretDown() {
return (
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="shrink-0">
<path d="M4.5 7.5h11L10 13.5 4.5 7.5Z" />
</svg>
);
}
/** # 组件 居中式视口(小尺寸下拉用)
* 面板 chrome 对齐原站 flyout:#fcfbfa 底 + 顶部 2px 橙线 + 直角 + 大阴影。
*/
function ViewportCentered() {
return (
<div className="absolute top-full left-0 flex w-max justify-center">
<NavigationMenuPrimitive.Viewport className="relative mt-0 h-[var(--radix-navigation-menu-viewport-height)] w-[var(--radix-navigation-menu-viewport-width)] origin-top overflow-hidden rounded-none border-t-2 border-panel-line bg-panel shadow-[0_10px_30px_rgba(13,21,46,0.1)] transition-[width,height] duration-300 data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:zoom-in-95" />
</div>
);
}
/** # 组件 全宽视口(巨幕面板用:固定在导航条下方、水平居中)
* 导航条(topbar 40px + 主导航 68px)sticky 置顶,故固定定位 top-108px 与其底边对齐;
* 面板宽度按原站实测 1340px(1436 视口),窄视口收窄到留 16px 边距。
*/
function ViewportFullWidth() {
return (
<div className="fixed inset-x-0 top-[108px] z-[90] flex justify-center">
<NavigationMenuPrimitive.Viewport className="relative h-[var(--radix-navigation-menu-viewport-height)] w-[calc(100%-32px)] max-w-[1340px] origin-top overflow-visible rounded-none border-t-2 border-panel-line bg-panel shadow-[0_10px_30px_rgba(13,21,46,0.1)] transition-[height] duration-300 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:pointer-events-none data-[state=closed]:slide-out-to-top-2 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-top-2" />
</div>
);
}
export { NavigationMenu, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuHeading, CaretDown };