dropdown-menu.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. 'use client';
  2. import * as React from 'react';
  3. import { Menu as MenuPrimitive } from '@base-ui/react/menu';
  4. import { cn } from '@/lib/utils';
  5. import { ChevronRightIcon, CheckIcon } from 'lucide-react';
  6. function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
  7. return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
  8. }
  9. function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
  10. return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
  11. }
  12. function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
  13. return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
  14. }
  15. function DropdownMenuContent({
  16. align = 'start',
  17. alignOffset = 0,
  18. side = 'bottom',
  19. sideOffset = 4,
  20. className,
  21. ...props
  22. }: MenuPrimitive.Popup.Props &
  23. Pick<
  24. MenuPrimitive.Positioner.Props,
  25. 'align' | 'alignOffset' | 'side' | 'sideOffset'
  26. >) {
  27. return (
  28. <MenuPrimitive.Portal>
  29. <MenuPrimitive.Positioner
  30. className="isolate z-50 outline-none"
  31. align={align}
  32. alignOffset={alignOffset}
  33. side={side}
  34. sideOffset={sideOffset}
  35. >
  36. <MenuPrimitive.Popup
  37. data-slot="dropdown-menu-content"
  38. className={cn(
  39. 'z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95',
  40. className,
  41. )}
  42. {...props}
  43. />
  44. </MenuPrimitive.Positioner>
  45. </MenuPrimitive.Portal>
  46. );
  47. }
  48. function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
  49. return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
  50. }
  51. function DropdownMenuLabel({
  52. className,
  53. inset,
  54. ...props
  55. }: MenuPrimitive.GroupLabel.Props & {
  56. inset?: boolean;
  57. }) {
  58. return (
  59. <MenuPrimitive.GroupLabel
  60. data-slot="dropdown-menu-label"
  61. data-inset={inset}
  62. className={cn(
  63. 'px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7',
  64. className,
  65. )}
  66. {...props}
  67. />
  68. );
  69. }
  70. function DropdownMenuItem({
  71. className,
  72. inset,
  73. variant = 'default',
  74. ...props
  75. }: MenuPrimitive.Item.Props & {
  76. inset?: boolean;
  77. variant?: 'default' | 'destructive';
  78. }) {
  79. return (
  80. <MenuPrimitive.Item
  81. data-slot="dropdown-menu-item"
  82. data-inset={inset}
  83. data-variant={variant}
  84. className={cn(
  85. "group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
  86. className,
  87. )}
  88. {...props}
  89. />
  90. );
  91. }
  92. function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
  93. return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />;
  94. }
  95. function DropdownMenuSubTrigger({
  96. className,
  97. inset,
  98. children,
  99. ...props
  100. }: MenuPrimitive.SubmenuTrigger.Props & {
  101. inset?: boolean;
  102. }) {
  103. return (
  104. <MenuPrimitive.SubmenuTrigger
  105. data-slot="dropdown-menu-sub-trigger"
  106. data-inset={inset}
  107. className={cn(
  108. "flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  109. className,
  110. )}
  111. {...props}
  112. >
  113. {children}
  114. <ChevronRightIcon className="ml-auto" />
  115. </MenuPrimitive.SubmenuTrigger>
  116. );
  117. }
  118. function DropdownMenuSubContent({
  119. align = 'start',
  120. alignOffset = -3,
  121. side = 'right',
  122. sideOffset = 0,
  123. className,
  124. ...props
  125. }: React.ComponentProps<typeof DropdownMenuContent>) {
  126. return (
  127. <DropdownMenuContent
  128. data-slot="dropdown-menu-sub-content"
  129. className={cn(
  130. 'w-auto min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
  131. className,
  132. )}
  133. align={align}
  134. alignOffset={alignOffset}
  135. side={side}
  136. sideOffset={sideOffset}
  137. {...props}
  138. />
  139. );
  140. }
  141. function DropdownMenuCheckboxItem({
  142. className,
  143. children,
  144. checked,
  145. inset,
  146. ...props
  147. }: MenuPrimitive.CheckboxItem.Props & {
  148. inset?: boolean;
  149. }) {
  150. return (
  151. <MenuPrimitive.CheckboxItem
  152. data-slot="dropdown-menu-checkbox-item"
  153. data-inset={inset}
  154. className={cn(
  155. "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  156. className,
  157. )}
  158. checked={checked}
  159. {...props}
  160. >
  161. <span
  162. className="pointer-events-none absolute right-2 flex items-center justify-center"
  163. data-slot="dropdown-menu-checkbox-item-indicator"
  164. >
  165. <MenuPrimitive.CheckboxItemIndicator>
  166. <CheckIcon />
  167. </MenuPrimitive.CheckboxItemIndicator>
  168. </span>
  169. {children}
  170. </MenuPrimitive.CheckboxItem>
  171. );
  172. }
  173. function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
  174. return (
  175. <MenuPrimitive.RadioGroup
  176. data-slot="dropdown-menu-radio-group"
  177. {...props}
  178. />
  179. );
  180. }
  181. function DropdownMenuRadioItem({
  182. className,
  183. children,
  184. inset,
  185. ...props
  186. }: MenuPrimitive.RadioItem.Props & {
  187. inset?: boolean;
  188. }) {
  189. return (
  190. <MenuPrimitive.RadioItem
  191. data-slot="dropdown-menu-radio-item"
  192. data-inset={inset}
  193. className={cn(
  194. "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  195. className,
  196. )}
  197. {...props}
  198. >
  199. <span
  200. className="pointer-events-none absolute right-2 flex items-center justify-center"
  201. data-slot="dropdown-menu-radio-item-indicator"
  202. >
  203. <MenuPrimitive.RadioItemIndicator>
  204. <CheckIcon />
  205. </MenuPrimitive.RadioItemIndicator>
  206. </span>
  207. {children}
  208. </MenuPrimitive.RadioItem>
  209. );
  210. }
  211. function DropdownMenuSeparator({
  212. className,
  213. ...props
  214. }: MenuPrimitive.Separator.Props) {
  215. return (
  216. <MenuPrimitive.Separator
  217. data-slot="dropdown-menu-separator"
  218. className={cn('-mx-1 my-1 h-px bg-border', className)}
  219. {...props}
  220. />
  221. );
  222. }
  223. function DropdownMenuShortcut({
  224. className,
  225. ...props
  226. }: React.ComponentProps<'span'>) {
  227. return (
  228. <span
  229. data-slot="dropdown-menu-shortcut"
  230. className={cn(
  231. 'ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground',
  232. className,
  233. )}
  234. {...props}
  235. />
  236. );
  237. }
  238. export {
  239. DropdownMenu,
  240. DropdownMenuPortal,
  241. DropdownMenuTrigger,
  242. DropdownMenuContent,
  243. DropdownMenuGroup,
  244. DropdownMenuLabel,
  245. DropdownMenuItem,
  246. DropdownMenuCheckboxItem,
  247. DropdownMenuRadioGroup,
  248. DropdownMenuRadioItem,
  249. DropdownMenuSeparator,
  250. DropdownMenuShortcut,
  251. DropdownMenuSub,
  252. DropdownMenuSubTrigger,
  253. DropdownMenuSubContent,
  254. };