返回 AiToEarn
dropdown-menu.tsx
根目录 / project / aitoearn-web / src / components / ui / dropdown-menu.tsx
1 /**
2 * DropdownMenu - 下拉菜单组件
3 * 基于 @radix-ui/react-dropdown-menu 封装
4 */
5
6 'use client'
7
8 import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
9 import { Check, ChevronRight, Circle } from 'lucide-react'
10 import * as React from 'react'
11
12 import { cn } from '@/utils/className'
13
14 const DropdownMenu = DropdownMenuPrimitive.Root
15
16 const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
17
18 const DropdownMenuGroup = DropdownMenuPrimitive.Group
19
20 const DropdownMenuPortal = DropdownMenuPrimitive.Portal
21
22 const DropdownMenuSub = DropdownMenuPrimitive.Sub
23
24 const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
25
26 const DropdownMenuSubTrigger = React.forwardRef<
27 React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
28 React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
29 inset?: boolean
30 }
31 >(({ className, inset, children, ...props }, ref) => (
32 <DropdownMenuPrimitive.SubTrigger
33 ref={ref}
34 className={cn(
35 'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
36 inset && 'pl-8',
37 className,
38 )}
39 {...props}
40 >
41 {children}
42 <ChevronRight className="ml-auto" />
43 </DropdownMenuPrimitive.SubTrigger>
44 ))
45 DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
46
47 const DropdownMenuSubContent = React.forwardRef<
48 React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
49 React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
50 >(({ className, ...props }, ref) => (
51 <DropdownMenuPrimitive.SubContent
52 ref={ref}
53 className={cn(
54 'z-50 min-w-32 overflow-hidden rounded-md border border-border bg-background p-1 text-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
55 className,
56 )}
57 {...props}
58 />
59 ))
60 DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
61
62 const DropdownMenuContent = React.forwardRef<
63 React.ElementRef<typeof DropdownMenuPrimitive.Content>,
64 React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
65 >(({ className, sideOffset = 4, ...props }, ref) => (
66 <DropdownMenuPrimitive.Portal>
67 <DropdownMenuPrimitive.Content
68 ref={ref}
69 sideOffset={sideOffset}
70 className={cn(
71 'z-50 min-w-32 overflow-hidden rounded-md border border-border bg-background p-1 text-foreground shadow-md',
72 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
73 className,
74 )}
75 {...props}
76 />
77 </DropdownMenuPrimitive.Portal>
78 ))
79 DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
80
81 const DropdownMenuItem = React.forwardRef<
82 React.ElementRef<typeof DropdownMenuPrimitive.Item>,
83 React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
84 inset?: boolean
85 }
86 >(({ className, inset, ...props }, ref) => (
87 <DropdownMenuPrimitive.Item
88 ref={ref}
89 className={cn(
90 'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
91 inset && 'pl-8',
92 className,
93 )}
94 {...props}
95 />
96 ))
97 DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
98
99 const DropdownMenuCheckboxItem = React.forwardRef<
100 React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
101 React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
102 >(({ className, children, checked, ...props }, ref) => (
103 <DropdownMenuPrimitive.CheckboxItem
104 ref={ref}
105 className={cn(
106 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
107 className,
108 )}
109 checked={checked}
110 {...props}
111 >
112 <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
113 <DropdownMenuPrimitive.ItemIndicator>
114 <Check className="h-4 w-4" />
115 </DropdownMenuPrimitive.ItemIndicator>
116 </span>
117 {children}
118 </DropdownMenuPrimitive.CheckboxItem>
119 ))
120 DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
121
122 const DropdownMenuRadioItem = React.forwardRef<
123 React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
124 React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
125 >(({ className, children, ...props }, ref) => (
126 <DropdownMenuPrimitive.RadioItem
127 ref={ref}
128 className={cn(
129 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
130 className,
131 )}
132 {...props}
133 >
134 <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
135 <DropdownMenuPrimitive.ItemIndicator>
136 <Circle className="h-2 w-2 fill-current" />
137 </DropdownMenuPrimitive.ItemIndicator>
138 </span>
139 {children}
140 </DropdownMenuPrimitive.RadioItem>
141 ))
142 DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
143
144 const DropdownMenuLabel = React.forwardRef<
145 React.ElementRef<typeof DropdownMenuPrimitive.Label>,
146 React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
147 inset?: boolean
148 }
149 >(({ className, inset, ...props }, ref) => (
150 <DropdownMenuPrimitive.Label
151 ref={ref}
152 className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
153 {...props}
154 />
155 ))
156 DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
157
158 const DropdownMenuSeparator = React.forwardRef<
159 React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
160 React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
161 >(({ className, ...props }, ref) => (
162 <DropdownMenuPrimitive.Separator
163 ref={ref}
164 className={cn('-mx-1 my-1 h-px bg-muted', className)}
165 {...props}
166 />
167 ))
168 DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
169
170 function DropdownMenuShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
171 return <span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />
172 }
173 DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
174
175 export {
176 DropdownMenu,
177 DropdownMenuCheckboxItem,
178 DropdownMenuContent,
179 DropdownMenuGroup,
180 DropdownMenuItem,
181 DropdownMenuLabel,
182 DropdownMenuPortal,
183 DropdownMenuRadioGroup,
184 DropdownMenuRadioItem,
185 DropdownMenuSeparator,
186 DropdownMenuShortcut,
187 DropdownMenuSub,
188 DropdownMenuSubContent,
189 DropdownMenuSubTrigger,
190 DropdownMenuTrigger,
191 }
192
192 lines Plain Text