Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ next-env.d.ts

# test directory in app
app/test
package-lock.json
128 changes: 128 additions & 0 deletions components/ui/navigation-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";

import { cn } from "@/lib/utils";

const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;

const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;

const NavigationMenuItem = NavigationMenuPrimitive.Item;

const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);

const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;

const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;

const NavigationMenuLink = NavigationMenuPrimitive.Link;

const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;

const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;

export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};
117 changes: 117 additions & 0 deletions stories/NavigationMenu/NavigationMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { Meta, StoryObj } from "@storybook/react";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";

const meta = {
title: "Navigation/NavigationMenu",
component: NavigationMenu,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof NavigationMenu>;

export default meta;
type Story = StoryObj<typeof meta>;

const drop1 = [
{ title: "Content 1", href: "#", description: "Short description 1" },
{ title: "Content 2", href: "#", description: "Short description 2" },
{ title: "Content 3", href: "#", description: "Short description 3" },
{ title: "Content 4", href: "#", description: "Short description 4" },
{ title: "Content 5", href: "#", description: "Short description 5" },
];

const drop2 = [
{ title: "Content 1", href: "#", description: "Short description 1" },
{ title: "Content 2", href: "#", description: "Short description 2" },
{ title: "Content 3", href: "#", description: "Short description 3" },
{ title: "Content 4", href: "#", description: "Short description 4" },
{ title: "Content 5", href: "#", description: "Short description 5" },
];

const drop3 = [
{ title: "Content 1", href: "#", description: "Short description 1" },
{ title: "Content 2", href: "#", description: "Short description 2" },
{ title: "Content 3", href: "#", description: "Short description 3" },
{ title: "Content 4", href: "#", description: "Short description 4" },
{ title: "Content 5", href: "#", description: "Short description 5" },
];

export const Default: Story = {
render: () => (
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Drop 1</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
{drop1.map((item) => (
<ListItem key={item.title} title={item.title} href={item.href}>
{item.description}
</ListItem>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>

<NavigationMenuItem>
<NavigationMenuTrigger>Drop 2</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
{drop2.map((item) => (
<ListItem key={item.title} title={item.title} href={item.href}>
{item.description}
</ListItem>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>

<NavigationMenuItem>
<NavigationMenuTrigger>Drop 3</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
{drop3.map((item) => (
<ListItem key={item.title} title={item.title} href={item.href}>
{item.description}
</ListItem>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
),
};

const ListItem = ({
title,
children,
href,
}: {
title: string;
children: React.ReactNode;
href: string;
}) => {
return (
<li>
<NavigationMenuLink asChild>
<a
href={href}
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
{children}
</p>
</a>
</NavigationMenuLink>
</li>
);
};