context-menu.tsx 8.0 KB

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