实现响应式导航栏组件
This commit is contained in:
@@ -4,21 +4,22 @@ import {cva, VariantProps} from 'class-variance-authority'
|
||||
|
||||
export const buttonVariants = cva(
|
||||
[
|
||||
`transition-all duration-200 ease-in-out`,
|
||||
`h-10 px-4 rounded-md cursor-pointer whitespace-nowrap`,
|
||||
'inline-flex items-center justify-center gap-2',
|
||||
`transition-all duration-200 ease-in-out`, // 过渡动画
|
||||
`h-10 px-4 rounded-md cursor-pointer whitespace-nowrap`, // 样式
|
||||
'outline-none focus-visible:ring-4 ring-blue-200', // 焦点样式
|
||||
'disabled:pointer-events-none disabled:opacity-50 ', // 禁用样式
|
||||
'aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail', // 无效状态样式
|
||||
'inline-flex items-center justify-center gap-2', // 布局
|
||||
'[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0 ',
|
||||
'outline-none focus-visible:ring-4 ring-blue-200',
|
||||
'disabled:pointer-events-none disabled:opacity-50 ',
|
||||
'aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
theme: {
|
||||
gradient: 'bg-gradient-to-r from-blue-400 to-cyan-300 text-white ring-offset-2',
|
||||
gradient: 'bg-gradient-to-r from-blue-400 to-cyan-300 text-white ring-offset-2 hover:from-blue-500 hover:to-cyan-400',
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
outline: 'border bg-background hover:bg-secondary hover:text-secondary-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
ghost: 'text-foreground bg-transparent',
|
||||
text: '',
|
||||
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
||||
fail: 'bg-fail text-white hover:bg-fail/90',
|
||||
warn: '',
|
||||
@@ -70,6 +71,11 @@ export const buttonVariants = cva(
|
||||
color: 'fail',
|
||||
className: 'hover:bg-fail/10 text-fail',
|
||||
},
|
||||
{
|
||||
theme: 'text',
|
||||
color: 'primary',
|
||||
className: 'hover:text-primary',
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
theme: 'default',
|
||||
|
||||
126
src/components/ui/navigation-menu.tsx
Normal file
126
src/components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import {ComponentProps, ReactNode} from 'react'
|
||||
import Link from 'next/link'
|
||||
import * as Primitive from '@radix-ui/react-navigation-menu'
|
||||
import {ChevronDownIcon, ChevronUpIcon} from 'lucide-react'
|
||||
import {Button, buttonVariants} from './button'
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
const NavigationMenuItemStyle = buttonVariants({
|
||||
theme: 'text',
|
||||
color: `primary`,
|
||||
className: 'h-full rounded-none',
|
||||
})
|
||||
|
||||
function Navigation(props: ComponentProps<typeof Primitive.Root>) {
|
||||
return <Primitive.Root {...props} delayDuration={0} skipDelayDuration={0}/>
|
||||
}
|
||||
|
||||
function NavigationGroup(props: ComponentProps<typeof Primitive.List>) {
|
||||
return <Primitive.List {...props} className={merge('h-full flex items-stretch', props.className)}/>
|
||||
}
|
||||
|
||||
function NavigationItem(props: ComponentProps<typeof Primitive.Item>) {
|
||||
return <Primitive.Item {...props}/>
|
||||
}
|
||||
|
||||
function NavigationLink(props: {
|
||||
text: ReactNode
|
||||
href: string
|
||||
classNameOverride?: string
|
||||
}) {
|
||||
return (
|
||||
<Primitive.Link asChild className={merge(props.classNameOverride ?? NavigationMenuItemStyle, 'text-lg')}>
|
||||
<Link href={props.href}>
|
||||
{props.text}
|
||||
</Link>
|
||||
</Primitive.Link>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationTrigger(props: {
|
||||
suffix?: boolean
|
||||
text: ReactNode
|
||||
}) {
|
||||
const suffix = props.suffix ?? true
|
||||
|
||||
return (
|
||||
<Primitive.Trigger asChild>
|
||||
<Button theme="text" className="text-lg gap-2 h-full group/trigger">
|
||||
{props.text}
|
||||
{suffix && (
|
||||
<ChevronUpIcon
|
||||
className="size-4 transition-transform duration-150 ease-in-out rotate-0 group-data-[state=open]/trigger:rotate-180"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</Primitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationItemContent(props: ComponentProps<typeof Primitive.Content>) {
|
||||
return (
|
||||
<Primitive.Content {...props}/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationLinkItem(props: {
|
||||
href: string
|
||||
text: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Primitive.Item>
|
||||
<NavigationLink {...props}/>
|
||||
</Primitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationTriggerItem(props: {
|
||||
text: ReactNode
|
||||
suffix?: boolean
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<Primitive.Item>
|
||||
<NavigationTrigger text={props.text} suffix={props.suffix}/>
|
||||
<NavigationItemContent className={props.className}>
|
||||
{props.children}
|
||||
</NavigationItemContent>
|
||||
</Primitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationIndicator() {
|
||||
return (
|
||||
<Primitive.Indicator className={merge(
|
||||
'w-full h-1 rounded-xs bg-primary z-10 top-[calc(100%-4px)]',
|
||||
'transition-transform duration-150 ease-out',
|
||||
'data-[state=visible]:animate-fadein',
|
||||
'data-[state=hidden]:animate-fadeout',
|
||||
)}/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuViewport(props: ComponentProps<typeof Primitive.Viewport>) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<Primitive.Viewport
|
||||
{...props}
|
||||
className="data-[state=open]:animate-fadein data-[state=closed]:animate-fadeout"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Navigation,
|
||||
NavigationGroup,
|
||||
NavigationItem,
|
||||
NavigationItemContent,
|
||||
NavigationTrigger,
|
||||
NavigationLink,
|
||||
NavigationIndicator,
|
||||
NavigationMenuViewport,
|
||||
NavigationLinkItem,
|
||||
NavigationTriggerItem,
|
||||
}
|
||||
Reference in New Issue
Block a user