-
-
Notifications
You must be signed in to change notification settings - Fork 45
added context menu components and documentation #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9e67ba5
273fbce
cb05fd1
34181ad
a1624cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,253 @@ | ||
| "use client"; | ||
|
|
||
| import * as React from "react"; | ||
| import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; | ||
| import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; | ||
|
|
||
| import { cn } from "@/lib/utils"; | ||
|
|
||
| function ContextMenu({ | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { | ||
| return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />; | ||
| } | ||
|
|
||
| function ContextMenuTrigger({ | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { | ||
| return ( | ||
| <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} /> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuGroup({ | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) { | ||
| return ( | ||
| <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} /> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuPortal({ | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) { | ||
| return ( | ||
| <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} /> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuSub({ | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) { | ||
| return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />; | ||
| } | ||
|
|
||
| function ContextMenuRadioGroup({ | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) { | ||
| return ( | ||
| <ContextMenuPrimitive.RadioGroup | ||
| data-slot="context-menu-radio-group" | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuSubTrigger({ | ||
| className, | ||
| inset, | ||
| children, | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { | ||
| inset?: boolean; | ||
| }) { | ||
| return ( | ||
| <ContextMenuPrimitive.SubTrigger | ||
| data-slot="context-menu-sub-trigger" | ||
| data-inset={inset} | ||
| className={cn( | ||
| "focus:bg-primary focus:text-primary-foreground data-[state=open]:bg-primary data-[state=open]:text-primary-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors", | ||
| className, | ||
| )} | ||
| {...props} | ||
| > | ||
| {children} | ||
| <ChevronRightIcon className="ml-auto" /> | ||
| </ContextMenuPrimitive.SubTrigger> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuSubContent({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) { | ||
| return ( | ||
| <ContextMenuPrimitive.SubContent | ||
| data-slot="context-menu-sub-content" | ||
| className={cn( | ||
| "bg-background text-foreground border-2 shadow-md 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 z-50 min-w-[8rem] origin-[--radix-context-menu-content-transform-origin] overflow-hidden rounded-sm p-1", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuContent({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) { | ||
| return ( | ||
| <ContextMenuPrimitive.Portal> | ||
| <ContextMenuPrimitive.Content | ||
| data-slot="context-menu-content" | ||
| className={cn( | ||
| "bg-background text-foreground border-2 shadow-md 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 z-50 max-h-[var(--radix-context-menu-content-available-height)] min-w-[8rem] origin-[--radix-context-menu-content-transform-origin] overflow-x-hidden overflow-y-auto rounded-sm p-1", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| </ContextMenuPrimitive.Portal> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuItem({ | ||
| className, | ||
| inset, | ||
| variant = "default", | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & { | ||
| inset?: boolean; | ||
| variant?: "default" | "destructive"; | ||
| }) { | ||
| return ( | ||
| <ContextMenuPrimitive.Item | ||
| data-slot="context-menu-item" | ||
| data-inset={inset} | ||
| data-variant={variant} | ||
| className={cn( | ||
| "focus:bg-primary focus:text-primary-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 [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuCheckboxItem({ | ||
| className, | ||
| children, | ||
| checked, | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) { | ||
| return ( | ||
| <ContextMenuPrimitive.CheckboxItem | ||
| data-slot="context-menu-checkbox-item" | ||
| className={cn( | ||
| "focus:bg-primary focus:text-primary-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors", | ||
| className, | ||
| )} | ||
| checked={checked} | ||
| {...props} | ||
| > | ||
| <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> | ||
| <ContextMenuPrimitive.ItemIndicator> | ||
| <CheckIcon className="size-4" /> | ||
| </ContextMenuPrimitive.ItemIndicator> | ||
| </span> | ||
| {children} | ||
| </ContextMenuPrimitive.CheckboxItem> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuRadioItem({ | ||
| className, | ||
| children, | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) { | ||
| return ( | ||
| <ContextMenuPrimitive.RadioItem | ||
| data-slot="context-menu-radio-item" | ||
| className={cn( | ||
| "focus:bg-primary focus:text-primary-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors", | ||
| className, | ||
| )} | ||
| {...props} | ||
| > | ||
| <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> | ||
| <ContextMenuPrimitive.ItemIndicator> | ||
| <CircleIcon className="size-2 fill-current" /> | ||
| </ContextMenuPrimitive.ItemIndicator> | ||
| </span> | ||
| {children} | ||
| </ContextMenuPrimitive.RadioItem> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuLabel({ | ||
| className, | ||
| inset, | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & { | ||
| inset?: boolean; | ||
| }) { | ||
| return ( | ||
| <ContextMenuPrimitive.Label | ||
| data-slot="context-menu-label" | ||
| data-inset={inset} | ||
| className={cn( | ||
| "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuSeparator({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) { | ||
| return ( | ||
| <ContextMenuPrimitive.Separator | ||
| data-slot="context-menu-separator" | ||
| className={cn("bg-border -mx-1 my-1 h-px", className)} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| function ContextMenuShortcut({ | ||
| className, | ||
| ...props | ||
| }: React.ComponentProps<"span">) { | ||
| return ( | ||
| <span | ||
| data-slot="context-menu-shortcut" | ||
| className={cn( | ||
| "text-muted-foreground ml-auto text-xs tracking-widest", | ||
| className, | ||
| )} | ||
| {...props} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| const ContextMenuComponent = Object.assign(ContextMenu, { | ||
| Trigger: ContextMenuTrigger, | ||
| Content: ContextMenuContent, | ||
| Item: ContextMenuItem, | ||
| CheckboxItem: ContextMenuCheckboxItem, | ||
| RadioItem: ContextMenuRadioItem, | ||
| Label: ContextMenuLabel, | ||
| Separator: ContextMenuSeparator, | ||
| Shortcut: ContextMenuShortcut, | ||
| Group: ContextMenuGroup, | ||
| Portal: ContextMenuPortal, | ||
| Sub: ContextMenuSub, | ||
| SubContent: ContextMenuSubContent, | ||
| SubTrigger: ContextMenuSubTrigger, | ||
| RadioGroup: ContextMenuRadioGroup, | ||
| }); | ||
|
|
||
| export { ContextMenuComponent as ContextMenu }; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||
| --- | ||||||
| title: Context Menu | ||||||
| description: Displays a menu to the user — such as a set of actions or functions — triggered by a button. | ||||||
| lastUpdated: 21 Aug, 2025 | ||||||
| links: | ||||||
| source: https://github.com/Logging-Stuff/RetroUI/blob/main/components/retroui/ContextMenu.tsx | ||||||
| --- | ||||||
|
|
||||||
| <ComponentShowcase name="context-menu-style-default" /> | ||||||
| <br /> | ||||||
| <br /> | ||||||
|
|
||||||
| <ComponentInstall> | ||||||
| <ComponentInstall.Cli npmCommand="npx shadcn@latest add 'https://retroui.dev/r/context-menu.json'" /> | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify CLI snippet resolves to a valid recipe URL Sanity‑check that the JSON is reachable to avoid broken “Add” flows. 🏁 Script executed: #!/bin/bash
# Expect: HTTP 200 and a small JSON body
curl -sI https://retroui.dev/r/context-menu.json | head -n 1
curl -s https://retroui.dev/r/context-menu.json | jq 'keys' | headLength of output: 199 🏁 Script executed: #!/bin/bash
# Follow redirects and check the HTTP status after redirects
curl -sI -L https://retroui.dev/r/context-menu.json | head -n 1
# Fetch the content and list top-level keys to verify valid JSON
curl -sL https://retroui.dev/r/context-menu.json | jq 'keys' | headLength of output: 204 🏁 Script executed: #!/bin/bash
# Show the full redirect chain and headers, dropping body
curl -sL -D - https://retroui.dev/r/context-menu.json -o /dev/null
echo "------- BODY PREVIEW -------"
# Print the first 20 lines of the response body
curl -sL https://retroui.dev/r/context-menu.json | head -n 20Length of output: 20098 Broken Recipe URL in CLI Snippet • Please update the snippet to reference a valid, reachable JSON recipe URL (or publish the recipe at 🤖 Prompt for AI Agents |
||||||
| <ComponentInstall.Manual> | ||||||
| #### 1. Install dependencies: | ||||||
|
|
||||||
| ```sh | ||||||
| npm install class-variance-authority | ||||||
| ``` | ||||||
|
Comment on lines
+19
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Manual install misses the required Radix dependency Copy‑pasting the component without installing - npm install class-variance-authority
+ npm install class-variance-authority @radix-ui/react-context-menu📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| <br /> | ||||||
|
|
||||||
| #### 2. Copy the code 👇 into your project: | ||||||
|
|
||||||
| <ComponentSource name="context-menu" /> | ||||||
|
|
||||||
| </ComponentInstall.Manual> | ||||||
| </ComponentInstall> | ||||||
|
|
||||||
| <br /> | ||||||
| <br /> | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| "@radix-ui/react-accordion": "^1.2.1", | ||
| "@radix-ui/react-avatar": "^1.1.1", | ||
| "@radix-ui/react-checkbox": "^1.1.4", | ||
| "@radix-ui/react-context-menu": "^2.2.16", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainVerify Radix context menu version and peer alignment The added dependency looks right, but please verify:
Run locally: 🏁 Script executed: #!/bin/bash
pnpm why @radix-ui/react-context-menu
pnpm ls @radix-ui/react-context-menu @radix-ui/react-dropdown-menu @radix-ui/react-slot
pnpm dedupe
pnpm -s exec tsc -p .Length of output: 6890 🏁 Script executed: #!/bin/bash
# Inspect peerDependencies of the newly added Radix context-menu package
grep -R '"peerDependencies"' -A5 node_modules/@radix-ui/react-context-menu/package.json
# Verify why this package is present and ensure no unexpected duplicates
pnpm why @radix-ui/react-context-menuLength of output: 492 Radix Context Menu OK – Project Peers & Build Are Broken Context-menu v2.2.16 itself aligns with React’s peer requirement (supports React ≥16.8 and ≤19.x) and pnpm only pulls in one version of @radix-ui/react-context-menu (2.2.16) as expected. However, two critical issues surfaced when verifying: • Next.js peer-dependency mismatch
• TypeScript build errors
Action items (critical fixes required):
🤖 Prompt for AI Agents |
||
| "@radix-ui/react-dialog": "^1.1.2", | ||
| "@radix-ui/react-dropdown-menu": "^2.1.2", | ||
| "@radix-ui/react-label": "^2.1.2", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix likely-invalid Tailwind selector for destructive icons + forwardRef Item
The selector
data-[variant=destructive]:*:[svg]:!text-destructiveis likely not recognized by Tailwind. Use the established arbitrary selector form with a descendant:data-[variant=destructive]:[&_svg]:!text-destructive. Also forward the ref.Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents