110 lines
6.2 KiB
TypeScript
110 lines
6.2 KiB
TypeScript
/**
|
||
* # 组件 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 };
|