-
-
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
Conversation
|
@ameybh is attempting to deploy a commit to the Retro UI Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughAdds a new ContextMenu React component (wrapping Radix UI primitives) with styled static subcomponents, re-exports it, registers the component and an example in configs and registry, adds docs and a preview demo, and adds the @radix-ui/react-context-menu dependency. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Trigger as ContextMenu.Trigger
participant Root as ContextMenu.Root (Radix)
participant Portal as Radix Portal
participant Content as ContextMenu.Content
participant Sub as ContextMenu.SubContent
User->>Trigger: right-click / open
Trigger->>Root: open request
Root->>Portal: mount Content
Portal->>Content: render & animate
User->>Content: select Item / toggle Checkbox / choose Radio
Content-->>User: action callback / state update
User->>Content: hover SubTrigger
Content->>Sub: open SubContent
Sub-->>User: show submenu
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
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.
Actionable comments posted: 4
🧹 Nitpick comments (12)
components/retroui/ContextMenu.tsx (6)
15-21: Forward refs to preserve focus management and type parity with RadixWithout forwarding refs, consumers can’t focus or measure these elements, and it’s inconsistent with patterns elsewhere (see Menu.Item). Recommend forwardRef on high-traffic primitives starting with Trigger.
Apply this diff:
-function ContextMenuTrigger({ - ...props -}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { - return ( - <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} /> - ); -} +const ContextMenuTrigger = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Trigger> +>(({ ...props }, ref) => ( + <ContextMenuPrimitive.Trigger + ref={ref} + data-slot="context-menu-trigger" + {...props} + /> +)); +ContextMenuTrigger.displayName = "ContextMenuTrigger";
56-79: SubTrigger: forwardRef + a11y for decorative icon
- Forward the ref to maintain keyboard focus alignment with Radix.
- Mark the chevron icon as aria-hidden to avoid it being read by screen readers.
Apply this diff:
-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> - ); -} +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + <ContextMenuPrimitive.SubTrigger + ref={ref} + 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 aria-hidden="true" className="ml-auto" /> + </ContextMenuPrimitive.SubTrigger> +)); +ContextMenuSubTrigger.displayName = "ContextMenuSubTrigger";
80-95: SubContent: forwardRef to allow measurements/imperative focusThis keeps parity with Radix and prevents ref loss in nested menus.
Apply this diff:
-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} - /> - ); -} +const ContextMenuSubContent = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.SubContent>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> +>(({ className, ...props }, ref) => ( + <ContextMenuPrimitive.SubContent + ref={ref} + 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} + /> +)); +ContextMenuSubContent.displayName = "ContextMenuSubContent";
96-112: Content: forwardRef and clarify Portal usage
- Forward the ref to preserve focus/positioning behaviors.
- Content already wraps itself in a Portal. If you also use ContextMenu.Portal externally, you’ll double-portal. Ensure docs mention that Content includes a Portal by default.
Apply this diff:
-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> - ); -} +const ContextMenuContent = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> +>(({ className, ...props }, ref) => ( + <ContextMenuPrimitive.Portal> + <ContextMenuPrimitive.Content + ref={ref} + 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> +)); +ContextMenuContent.displayName = "ContextMenuContent";
147-156: Mark the check icon as decorativeItemIndicator already conveys the state to assistive tech. Marking the icon decorative avoids redundancy.
Apply this diff:
- <ContextMenuPrimitive.ItemIndicator> - <CheckIcon className="size-4" /> - </ContextMenuPrimitive.ItemIndicator> + <ContextMenuPrimitive.ItemIndicator> + <CheckIcon aria-hidden="true" className="size-4" /> + </ContextMenuPrimitive.ItemIndicator>
177-180: Mark the radio dot icon as decorativeSame rationale as above.
Apply this diff:
- <ContextMenuPrimitive.ItemIndicator> - <CircleIcon className="size-2 fill-current" /> - </ContextMenuPrimitive.ItemIndicator> + <ContextMenuPrimitive.ItemIndicator> + <CircleIcon aria-hidden="true" className="size-2 fill-current" /> + </ContextMenuPrimitive.ItemIndicator>preview/components/context-menu-style-default.tsx (4)
10-11: Nit: Hyphenate “Right-click”Minor copy update for consistency.
Apply this diff:
- Right click here + Right-click here
13-15: Showcase keyboard shortcuts in the demoUse the Shortcut subcomponent so users can see how it’s intended.
Apply this diff:
- <ContextMenu.Item>Copy</ContextMenu.Item> - <ContextMenu.Item>Cut</ContextMenu.Item> - <ContextMenu.Item>Paste</ContextMenu.Item> + <ContextMenu.Item> + Copy <ContextMenu.Shortcut>Ctrl+C</ContextMenu.Shortcut> + </ContextMenu.Item> + <ContextMenu.Item> + Cut <ContextMenu.Shortcut>Ctrl+X</ContextMenu.Shortcut> + </ContextMenu.Item> + <ContextMenu.Item> + Paste <ContextMenu.Shortcut>Ctrl+V</ContextMenu.Shortcut> + </ContextMenu.Item>
22-22: Use the destructive variant for “Delete”Demonstrates the variant styling you added.
Apply this diff:
- <ContextMenu.Item>Delete</ContextMenu.Item> + <ContextMenu.Item variant="destructive">Delete</ContextMenu.Item>
26-33: Prefer uncontrolled state in interactive demosUse defaultChecked/defaultValue so users can toggle options during the preview.
Apply this diff:
- <ContextMenu.CheckboxItem checked> + <ContextMenu.CheckboxItem defaultChecked> Show hidden files </ContextMenu.CheckboxItem> - <ContextMenu.RadioGroup value="one"> + <ContextMenu.RadioGroup defaultValue="one"> <ContextMenu.Label>View mode</ContextMenu.Label> <ContextMenu.RadioItem value="one">List</ContextMenu.RadioItem> <ContextMenu.RadioItem value="two">Grid</ContextMenu.RadioItem> </ContextMenu.RadioGroup>config/components.ts (1)
136-139: Add Radix dependency to keep registry consistent and installers accurateSeveral Radix-based entries list their peer packages under
dependencies(e.g.,slider). Adding the Context Menu's Radix package here helps generators/installer UIs surface the right install hints."context-menu": { name: "context-menu", - filePath: "components/retroui/ContextMenu.tsx", + filePath: "components/retroui/ContextMenu.tsx", + dependencies: ["@radix-ui/react-context-menu"], },content/docs/components/context-menu.mdx (1)
3-3: Clarify trigger semantics for a context menuContext menus are typically opened via right‑click/long‑press (and optionally via a trigger element), not just “triggered by a button.”
-description: Displays a menu to the user — such as a set of actions or functions — triggered by a button. +description: Displays a context menu — a set of actions or functions — typically triggered by right‑click or long‑press (or via an explicit trigger element).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
components/retroui/ContextMenu.tsx(1 hunks)components/retroui/index.ts(1 hunks)config/components.ts(4 hunks)config/navigation.ts(2 hunks)content/docs/components/context-menu.mdx(1 hunks)package.json(1 hunks)preview/components/context-menu-style-default.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
content/docs/components/context-menu.mdx (4)
components/retroui/Menu.tsx (3)
IMenuContent(10-11)props(13-25)props(30-39)preview/components/menu-style-default.tsx (1)
MenuDefault(4-17)components/MDX.tsx (1)
type(16-109)components/ComponentInstall.tsx (1)
ComponentInstall(115-130)
package.json (1)
components/retroui/Menu.tsx (1)
IMenuContent(10-11)
config/navigation.ts (2)
components/SideNav.tsx (3)
SideNav(8-43)item(16-39)child(20-36)types/index.d.ts (1)
INavigationConfig(12-15)
components/retroui/ContextMenu.tsx (2)
lib/utils.ts (1)
cn(4-6)components/retroui/Menu.tsx (3)
IMenuContent(10-11)props(30-39)props(13-25)
config/components.ts (3)
preview/components/menu-style-default.tsx (1)
MenuDefault(4-17)components/retroui/Menu.tsx (1)
IMenuContent(10-11)components/ComponentShowcase.tsx (1)
ComponentShowcase(10-64)
🪛 LanguageTool
content/docs/components/context-menu.mdx
[grammar] ~24-~24: There might be a mistake here.
Context: ...# 2. Copy the code 👇 into your project: </ComponentInstall.Manual> </ComponentIns...
(QB_NEW_EN)
🔇 Additional comments (11)
components/retroui/ContextMenu.tsx (3)
9-13: Root wrapper looks goodSimple pass-through to Radix Root with a stable public surface and data-slot for theming. No issues.
129-129: Verify custom utility classoutline-hiddenexistsTailwind’s standard utility is
outline-none. Ifoutline-hiddenis a custom plugin/class, ignore; otherwise replace to avoid no-op classes.
236-251: Static composition via Object.assign looks goodThe exported API surface is ergonomic and type-inferred (properties retain their component types). After adopting forwardRef for select subcomponents, re-run typecheck to ensure inference stays intact.
components/retroui/index.ts (1)
27-27: Expose ContextMenu from the retroui barrelGood addition; matches the pattern used by other components.
config/navigation.ts (2)
40-41: Trailing-comma fix looks fineKeeps array syntax clean and consistent.
58-58: New “Context Menu” nav entry wired correctlyPath matches the componentsRoute convention. Assuming the MDX page exists at /docs/components/context-menu, this should render in the side nav as expected.
config/components.ts (3)
243-245: Formatting-only lazy() wrap looks goodNo semantic changes; consistent with nearby entries.
431-433: LGTM on multiline lazy importPurely stylistic and consistent with other examples.
603-609: Default export confirmed for ContextMenuStyleDefaultThe file
preview/components/context-menu-style-default.tsxcontains a default export on line 5 (export default function ContextMenuStyleDefault() { … }), so thelazy(() => import("@/preview/components/context-menu-style-default"))call will resolve correctly. No changes required.content/docs/components/context-menu.mdx (2)
26-26: ComponentSource name matches the registry key
"context-menu"aligns with the new registry entry and should render correctly.
4-4: Confirm lastUpdated date formatting and valueIf the site surfaces this field, ensure the date format matches other pages and reflects the actual update date for consistency.
| 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} | ||
| /> | ||
| ); | ||
| } |
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-destructive is 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:
-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}
- />
- );
-}
+const ContextMenuItem = React.forwardRef<
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
+ inset?: boolean;
+ variant?: "default" | "destructive";
+ }
+>(({ className, inset, variant = "default", ...props }, ref) => (
+ <ContextMenuPrimitive.Item
+ ref={ref}
+ 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}
+ />
+));
+ContextMenuItem.displayName = "ContextMenuItem";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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} | |
| /> | |
| ); | |
| } | |
| const ContextMenuItem = React.forwardRef< | |
| React.ElementRef<typeof ContextMenuPrimitive.Item>, | |
| React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { | |
| inset?: boolean; | |
| variant?: "default" | "destructive"; | |
| } | |
| >(({ className, inset, variant = "default", ...props }, ref) => ( | |
| <ContextMenuPrimitive.Item | |
| ref={ref} | |
| 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} | |
| /> | |
| )); | |
| ContextMenuItem.displayName = "ContextMenuItem"; |
🤖 Prompt for AI Agents
components/retroui/ContextMenu.tsx around lines 114 to 135: the Tailwind
selector `data-[variant=destructive]:*:[svg]:!text-destructive` is invalid and
the component doesn't forward refs; replace the selector with the correct
arbitrary descendant form `data-[variant=destructive]:[&_svg]:!text-destructive`
in the cn list, and convert ContextMenuItem to a forwardRef component that
accepts the same props and passes the ref through to ContextMenuPrimitive.Item
(ensure proper React.forwardRef typing and spread props).
| <br /> | ||
|
|
||
| <ComponentInstall> | ||
| <ComponentInstall.Cli npmCommand="npx shadcn@latest add 'https://retroui.dev/r/context-menu.json'" /> |
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.
💡 Verification agent
🧩 Analysis chain
Verify 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
The recipe URL in content/docs/components/context-menu.mdx (line 14) currently points to https://retroui.dev/r/context-menu.json, but following the redirect yields a 404, which will break the npx shadcn@latest add … flow:
• curl -sI -L https://retroui.dev/r/context-menu.json | head -n1
→ HTTP/2 307 (redirect)
• curl -sI -L https://www.retroui.dev/r/context-menu.json | head -n1
→ HTTP/2 404
Please update the snippet to reference a valid, reachable JSON recipe URL (or publish the recipe at /r/context-menu.json) so that the “Add” command succeeds.
🤖 Prompt for AI Agents
In content/docs/components/context-menu.mdx around line 14, the CLI snippet
references https://retroui.dev/r/context-menu.json which redirects to a 404;
update the ComponentInstall.Cli npmCommand to point to a valid, reachable JSON
recipe URL (or publish the recipe at /r/context-menu.json) so the npx
shadcn@latest add command succeeds — replace the broken URL with the correct
hosted recipe URL and verify with aHEAD request or curl that it returns 200
before committing.
| npm install class-variance-authority | ||
| ``` |
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.
Manual install misses the required Radix dependency
Copy‑pasting the component without installing @radix-ui/react-context-menu will break builds.
- npm install class-variance-authority
+ npm install class-variance-authority @radix-ui/react-context-menu📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| npm install class-variance-authority | |
| ``` |
🤖 Prompt for AI Agents
In content/docs/components/context-menu.mdx around lines 19 to 20, the docs list
installing class-variance-authority but omit the required Radix dependency;
update the installation instructions to also include
@radix-ui/react-context-menu so users install both packages (e.g., add a second
install command or a combined npm/yarn install line) and mention that
@radix-ui/react-context-menu is required for the context menu component to build
correctly.
| "@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", |
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.
💡 Verification agent
🧩 Analysis chain
Verify Radix context menu version and peer alignment
The added dependency looks right, but please verify:
- Version compatibility with Next 14.2.x and React 18.x (peers).
- No duplicate Radix versions pulled in (dedupe if needed).
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
- next@14.2.7 requires react@^18.2.0 and react-dom@^18.2.0, but package.json is pinned to 18.0.0 → fail pnpm dedupe & peer info
• TypeScript build errors
- Numerous “Cannot find module 'contentlayer/generated'” and implicit-
anyerrors indicate that contentlayer types aren’t generated beforetsc→ fail tsc output
Action items (critical fixes required):
- Upgrade react and react-dom to at least 18.2.0 in package.json so Next.js peers are satisfied.
- Ensure contentlayer’s generated types exist:
- Add or update the
contentlayer buildstep in your prebuild or CI scripts. - Confirm imports reference the correct output path.
- Add or update the
- Re-run:
to verify no peer warnings or type errors remain.
pnpm dedupe pnpm -s exec tsc -p .
🤖 Prompt for AI Agents
In package.json around line 17, the project currently pins react and react-dom
to 18.0.0 which conflicts with Next.js 14.2.7 peers and the build fails plus
Contentlayer types are missing before tsc runs; update package.json to bump
react and react-dom to at least 18.2.0, add/update scripts so contentlayer types
are generated before TypeScript compilation (e.g., run contentlayer build in
prebuild/prepare or CI before tsc), verify all imports reference the generated
output path (contentlayer/generated), and after changes run pnpm dedupe and pnpm
-s exec tsc -p . to confirm no peer warnings or type errors remain.
|
@ariflogs Do I need to run |
yes |
|
Ready for review. @ariflogs |
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.
Actionable comments posted: 4
🧹 Nitpick comments (11)
public/r/table.json (2)
can be surprising
10-10: Wrapper introduced + default border/shadow on
- Adding a mandatory wrapper div changes DOM and might conflict with external layout wrappers.
- Moving border/shadow to the table itself can cause double borders when users wrap Table with their own bordered containers (see preview/components/table-with-sticky-header.tsx).
Consider exposing wrapper customization and making borders opt-in.
Apply this refactor to add wrapper props and make table border/shadow opt-in via a variant:
-import * as React from "react" - -import { cn } from "@/lib/utils" - -const Table = React.forwardRef< - HTMLTableElement, - React.HTMLAttributes<HTMLTableElement> ->(({ className, ...props }, ref) => ( - <div className="relative w-full overflow-auto"> - <table - ref={ref} - className={cn("w-full caption-bottom text-sm border-2 shadow-lg", className)} - {...props} - /> - </div> -)) +import * as React from "react" +import { cn } from "@/lib/utils" + +type TableProps = React.HTMLAttributes<HTMLTableElement> & { + containerClassName?: string + containerProps?: React.HTMLAttributes<HTMLDivElement> + bordered?: boolean + elevated?: boolean +} + +const Table = React.forwardRef<HTMLTableElement, TableProps>( + ({ className, containerClassName, containerProps, bordered = true, elevated = true, ...props }, ref) => ( + <div className={cn("relative w-full overflow-auto", containerClassName)} {...containerProps}> + <table + ref={ref} + className={cn( + "w-full caption-bottom text-sm", + bordered && "border-2", + elevated && "shadow-lg", + className + )} + {...props} + /> + </div> + ) +)
10-10: Minor: header/cell density defaultsThe new h-10 and p-2 on small screens are sensible. If you expect dense data tables, consider an optional “density” variant (compact/comfortable) to avoid per-callsite overrides.
public/r/select.json (2)
14-14: Style placeholder correctly for Radix Select.Valueplaceholder:text-muted-foreground won’t affect Radix Value. Target the data-placeholder attribute instead so placeholders are visibly muted.
Add child selector on Trigger or style Value directly:
- className={cn( - "flex h-10 min-w-40 items-center shadow-md justify-between border-2 border-input border-border bg-transparent px-4 py-2 placeholder:text-muted-foreground outline-none focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", + className={cn( + "flex h-10 min-w-40 items-center justify-between px-4 py-2 bg-transparent shadow-md border-2 border-border", + // Muted placeholder inside Value + "[&_[data-placeholder]]:text-muted-foreground", + "outline-none focus-visible:outline outline-2 outline-offset-2 outline-primary disabled:cursor-not-allowed disabled:opacity-50", className, )}Alternatively:
-const SelectValue = SelectPrimitive.Value; +const SelectValue = (props: SelectPrimitive.SelectValueProps) => ( + <SelectPrimitive.Value className="[&_[data-placeholder]]:text-muted-foreground" {...props} /> +);
14-14: Improve selected item visibility, not just highlightedWhen navigating with keyboard, selected options aren’t visually distinct unless highlighted. Mirror highlighted styles on [data-state=checked].
Update SelectItem:
- className={cn( - "relative flex w-full cursor-default select-none items-center py-1.5 px-2 outline-none data-[highlighted]:bg-primary data-[highlighted]:text-primary-foreground focus:bg-primary focus:text-primary-foreground data-disabled:pointer-events-none data-disabled:opacity-50", + className={cn( + "relative flex w-full cursor-default select-none items-center py-1.5 px-2 outline-none", + "data-[highlighted]:bg-primary data-[highlighted]:text-primary-foreground", + "data-[state=checked]:bg-primary/80 data-[state=checked]:text-primary-foreground", + "focus:bg-primary focus:text-primary-foreground", + "data-disabled:pointer-events-none data-disabled:opacity-50", )}public/r/accordion.json (2)
14-14: Accordion.Trigger removes focus outline — add visible focus styleKeep keyboard navigation discoverable by adding a focus-visible treatment instead of hiding outlines.
- className={cn( - "flex flex-1 items-start justify-between px-4 py-2 font-head cursor-pointer focus:outline-hidden [&[data-state=open]>svg]:rotate-180", + className={cn( + "flex flex-1 items-start justify-between px-4 py-2 font-head cursor-pointer", + "focus-visible:outline outline-2 outline-offset-2 outline-primary", + "[&[data-state=open]>svg]:rotate-180", className, )}
14-14: Nit: transition classes duplicationItem has both
shadow-md hover:shadow-sm data-[state=open]:shadow-sm transition-all. You can drop one of hover/state shadow settings or simplify to reduce class churn.public/r/button.json (2)
13-13: Provide a visible focus style instead of hiding itCurrent base includes
outline-hidden. Offer a consistent focus-visible treatment used elsewhere.- "font-head transition-all outline-hidden cursor-pointer duration-200 font-medium flex items-center", + "font-head transition-all cursor-pointer duration-200 font-medium flex items-center", + "focus-visible:outline outline-2 outline-offset-2 outline-primary",
13-13: Optional: improve polymorphic typing when asChild=trueRef stays typed to HTMLButtonElement even when rendering non-button children via Slot. If you want stricter typing, consider a polymorphic type setup; otherwise this is acceptable for most cases.
Example direction (non-breaking pattern with overloads or a Polymorphic component type) can be provided if you want to pursue it.
public/r/context-menu.json (2)
14-14: Forward refs through key subcomponents to preserve Radix focus/keyboard behaviors (align with Menu implementation).Currently, none of the wrappers forward refs, which can hinder imperative focus and composeability with Radix. Recommend forwarding refs at least for Content, Item, and SubTrigger (optional for CheckboxItem/RadioItem/Label/Separator). This also aligns with components/retroui/Menu.tsx where Item uses forwardRef.
Apply this diff inside components/retroui/ContextMenu.tsx:
@@ -function ContextMenuSubTrigger({ - className, - inset, - children, - ...props -}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { - inset?: boolean; -}) { - return ( - <ContextMenuPrimitive.SubTrigger +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + <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, )} + ref={ref} {...props} - > - {children} - <ChevronRightIcon className="ml-auto" /> - </ContextMenuPrimitive.SubTrigger> - ); -} + > + {children} + <ChevronRightIcon className="ml-auto" /> + </ContextMenuPrimitive.SubTrigger> +)); +ContextMenuSubTrigger.displayName = "ContextMenuSubTrigger"; @@ -function ContextMenuSubContent({ - className, - ...props -}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) { - return ( - <ContextMenuPrimitive.SubContent +const ContextMenuSubContent = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.SubContent>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> +>(({ className, ...props }, ref) => ( + <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, )} + ref={ref} {...props} - /> - ); -} + /> +)); +ContextMenuSubContent.displayName = "ContextMenuSubContent"; @@ -function ContextMenuContent({ - className, - ...props -}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) { - return ( - <ContextMenuPrimitive.Portal> - <ContextMenuPrimitive.Content +const ContextMenuContent = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> +>(({ className, ...props }, ref) => ( + <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, )} + ref={ref} {...props} - /> - </ContextMenuPrimitive.Portal> - ); -} + /> + </ContextMenuPrimitive.Portal> +)); +ContextMenuContent.displayName = "ContextMenuContent"; @@ -function ContextMenuItem({ - className, - inset, - variant = "default", - ...props -}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & { - inset?: boolean; - variant?: "default" | "destructive"; -}) { - return ( - <ContextMenuPrimitive.Item +const ContextMenuItem = React.forwardRef< + React.ElementRef<typeof ContextMenuPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { + inset?: boolean; + variant?: "default" | "destructive"; + } +>(({ className, inset, variant = "default", ...props }, ref) => ( + <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, )} + ref={ref} {...props} - /> - ); -} + /> +)); +ContextMenuItem.displayName = "ContextMenuItem";Optional: apply the same pattern to CheckboxItem and RadioItem if you anticipate programmatic focus there (e.g., in wizards).
14-14: Add displayName on the root wrapper for better DevTools introspection.You already set clear subcomponent names via Object.assign. Setting a displayName on the root makes it easier to debug.
@@ function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />; } + +// Helpful in React DevTools +ContextMenu.displayName = "ContextMenu";registry.json (1)
5-5: Formatting normalization to inline arrays is fine, but consider scoping to touched items next time.Converting many dependency arrays to single-line increases diff noise. It’s okay to keep, just be mindful to limit unrelated churn when possible.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
public/r/accordion.json(1 hunks)public/r/badge-style-variants.json(1 hunks)public/r/button.json(1 hunks)public/r/context-menu.json(1 hunks)public/r/select.json(1 hunks)public/r/table.json(1 hunks)registry.json(84 hunks)🧰 Additional context used
🧬 Code graph analysis (6)
public/r/badge-style-variants.json (2)
components/retroui/Badge.tsx (2)
Badge(29-44)ButtonProps(25-27)config/components.ts (1)
() => import("@/preview/components/badge-style-variants")(209-209)public/r/table.json (3)
preview/components/table-with-sticky-header.tsx (1)
TableWithStickyHeader(92-140)preview/components/table-style-default.tsx (1)
TableStyleDefault(58-89)preview/components/table-with-checkbox.tsx (1)
TableWithCheckbox(58-131)public/r/accordion.json (1)
preview/components/accordion-style-default.tsx (1)
AccordionStyleDefault(5-28)public/r/select.json (1)
preview/components/select-style-default.tsx (1)
SelectStyleDefault(4-20)registry.json (1)
components/retroui/Menu.tsx (1)
IMenuContent(10-11)public/r/context-menu.json (2)
components/retroui/Menu.tsx (3)
IMenuContent(10-11)props(30-39)props(13-25)preview/components/menu-style-default.tsx (1)
MenuDefault(4-17)🔇 Additional comments (7)
public/r/badge-style-variants.json (1)
12-12: LGTM — responsive layout improvementSwitching to flex-wrap with gap makes the preview resilient to narrow widths without overflowing. Good call.
public/r/context-menu.json (2)
7-10: Dependencies look correct and minimal.Both @radix-ui/react-context-menu and lucide-react are used in the implementation. No extra deps.
13-17: Registry file path/target are consistent.The file path and target both point to components/retroui/ContextMenu.tsx and the type is correctly set to registry:component.
registry.json (4)
232-245: New registry entry for Context Menu is well-formed.Name, type, dependencies, and file mapping look consistent with other components and the public/r entry.
237-237: Good call including lucide-react here.Icons (ChevronRightIcon, CheckIcon, CircleIcon) are used by the implementation; dependency declared accordingly.
58-58: Consistent inline dependency formatting across the file.The updated single-line arrays are consistently applied wherever changed. No schema regressions spotted.
Also applies to: 71-71, 85-85, 99-99, 113-113, 127-127, 209-209, 223-223, 251-251, 266-266, 297-297, 311-311, 325-325, 339-339, 353-353, 381-381, 407-407, 446-446, 461-461, 475-475, 489-489, 503-503, 516-516, 529-529, 543-543, 569-569, 581-581, 607-607, 621-621, 633-633, 659-659, 673-673, 701-701, 713-713, 739-739, 753-753, 765-765, 779-779, 791-791, 805-805, 817-817, 869-869, 883-883, 895-895, 909-909, 921-921, 935-935, 947-947, 961-961, 973-973, 987-987, 1013-1013, 1025-1025, 1039-1039, 1051-1051, 1065-1065, 1077-1077, 1091-1091, 1117-1117, 1131-1131, 1145-1145, 1159-1159, 1176-1176, 1193-1193, 1210-1210, 1244-1244, 1261-1261, 1278-1278, 1295-1295, 1331-1331, 1350-1350
232-245: Sync check between registry.json and public/r/context-menu.json.Both entries reference components/retroui/ContextMenu.tsx and declare the same dependency set. Keep these in lockstep going forward. If you later add/remove icons or Radix subpackages, update both places.
| { | ||
| "path": "components/retroui/Accordion.tsx", | ||
| "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n <AccordionPrimitive.Item\n ref={ref}\n className={cn(\n \"border-2 bg-background text-foreground shadow-md hover:shadow-sm data-[state=open]:shadow-sm transition-all overflow-hidden\",\n className,\n )}\n {...props}\n />\n));\nAccordionItem.displayName = AccordionPrimitive.Item.displayName;\n\nconst AccordionHeader = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex flex-1 items-start justify-between px-4 py-2 font-head cursor-pointer focus:outline-hidden [&[data-state=open]>svg]:rotate-180\",\n className,\n )}\n {...props}\n >\n {children}\n <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n));\nAccordionHeader.displayName = AccordionPrimitive.Header.displayName;\n\nconst AccordionContent = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Content\n ref={ref}\n className=\"overflow-hidden px-4 py-2 font-body bg-white text-gray-700 transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\"\n {...props}\n >\n <div className={cn(\"pb-4 pt-0\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n));\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nconst AccordionComponent = Object.assign(Accordion, {\n Item: AccordionItem,\n Header: AccordionHeader,\n Content: AccordionContent,\n});\n\nexport { AccordionComponent as Accordion };\n", | ||
| "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n <AccordionPrimitive.Item\n ref={ref}\n className={cn(\n \"border-2 bg-background text-foreground shadow-md hover:shadow-sm data-[state=open]:shadow-sm transition-all overflow-hidden\",\n className,\n )}\n {...props}\n />\n));\nAccordionItem.displayName = AccordionPrimitive.Item.displayName;\n\nconst AccordionHeader = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex flex-1 items-start justify-between px-4 py-2 font-head cursor-pointer focus:outline-hidden [&[data-state=open]>svg]:rotate-180\",\n className,\n )}\n {...props}\n >\n {children}\n <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n));\nAccordionHeader.displayName = AccordionPrimitive.Header.displayName;\n\nconst AccordionContent = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Content\n ref={ref}\n className=\"overflow-hidden font-body bg-white text-gray-700 data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up\"\n {...props}\n >\n <div className={cn(\"px-4 pt-2 pb-4\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n));\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nconst AccordionComponent = Object.assign(Accordion, {\n Item: AccordionItem,\n Header: AccordionHeader,\n Content: AccordionContent,\n});\n\nexport { AccordionComponent as Accordion };\n", |
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
Use theme tokens instead of hard-coded colors to preserve theming
bg-white text-gray-700 will break dark mode and custom themes; other components use bg-background/text-foreground.
Update Accordion.Content:
- <AccordionPrimitive.Content
- ref={ref}
- className="overflow-hidden font-body bg-white text-gray-700 data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up"
+ <AccordionPrimitive.Content
+ ref={ref}
+ className="overflow-hidden font-body bg-background text-foreground data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up"
{...props}
>
- <div className={cn("px-4 pt-2 pb-4", className)}>{children}</div>
+ <div className={cn("px-4 pt-2 pb-4 text-muted-foreground", className)}>{children}</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Item>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n <AccordionPrimitive.Item\n ref={ref}\n className={cn(\n \"border-2 bg-background text-foreground shadow-md hover:shadow-sm data-[state=open]:shadow-sm transition-all overflow-hidden\",\n className,\n )}\n {...props}\n />\n));\nAccordionItem.displayName = AccordionPrimitive.Item.displayName;\n\nconst AccordionHeader = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Trigger>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n ref={ref}\n className={cn(\n \"flex flex-1 items-start justify-between px-4 py-2 font-head cursor-pointer focus:outline-hidden [&[data-state=open]>svg]:rotate-180\",\n className,\n )}\n {...props}\n >\n {children}\n <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n));\nAccordionHeader.displayName = AccordionPrimitive.Header.displayName;\n\nconst AccordionContent = React.forwardRef<\n React.ElementRef<typeof AccordionPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n <AccordionPrimitive.Content\n ref={ref}\n className=\"overflow-hidden font-body bg-white text-gray-700 data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up\"\n {...props}\n >\n <div className={cn(\"px-4 pt-2 pb-4\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n));\n\nAccordionContent.displayName = AccordionPrimitive.Content.displayName;\n\nconst AccordionComponent = Object.assign(Accordion, {\n Item: AccordionItem,\n Header: AccordionHeader,\n Content: AccordionContent,\n});\n\nexport { AccordionComponent as Accordion };\n", | |
| const AccordionContent = React.forwardRef< | |
| React.ElementRef<typeof AccordionPrimitive.Content>, | |
| React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> | |
| >(({ className, children, ...props }, ref) => ( | |
| <AccordionPrimitive.Content | |
| ref={ref} | |
| className="overflow-hidden font-body bg-background text-foreground data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up" | |
| {...props} | |
| > | |
| <div className={cn("px-4 pt-2 pb-4 text-muted-foreground", className)}> | |
| {children} | |
| </div> | |
| </AccordionPrimitive.Content> | |
| )); |
🤖 Prompt for AI Agents
In public/r/accordion.json around line 14, Accordion.Content uses hard-coded
colors ("bg-white text-gray-700") which break dark mode and custom themes;
replace those with theme tokens (e.g., "bg-background text-foreground") and
ensure the className is composed with cn so incoming className prop is preserved
(keep existing overflow, font-body and data-state animation classes and merge
them with cn and the passed className).
| { | ||
| "path": "components/retroui/Button.tsx", | ||
| "content": "import { cn } from \"@/lib/utils\";\nimport { cva, VariantProps } from \"class-variance-authority\";\nimport React, { ButtonHTMLAttributes } from \"react\";\n\nconst buttonVariants = cva(\n \"font-head transition-all outline-hidden cursor-pointer duration-200 font-medium flex items-center\",\n {\n variants: {\n variant: {\n default:\n \"shadow-md hover:shadow-none bg-primary text-black border-2 border-black transition hover:translate-y-1 hover:bg-primary-hover\",\n secondary:\n \"shadow-md hover:shadow-none bg-secondary shadow-primary text-secondary-foreground border-2 border-black transition hover:translate-y-1\",\n outline:\n \"shadow-md hover:shadow-none bg-transparent border-2 transition hover:translate-y-1\",\n link: \"bg-transparent hover:underline\",\n },\n size: {\n sm: \"px-3 py-1 text-sm shadow hover:shadow-none\",\n md: \"px-4 py-1.5 text-base\",\n lg: \"px-8 py-3 text-lg\",\n icon: \"p-2\",\n },\n },\n defaultVariants: {\n size: \"md\",\n variant: \"default\",\n },\n },\n);\n\nexport interface IButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {}\n\nexport const Button = React.forwardRef<HTMLButtonElement, IButtonProps>(\n (\n {\n children,\n size = \"md\",\n className = \"\",\n variant = \"default\",\n ...props\n }: IButtonProps,\n forwardedRef,\n ) => (\n <button\n ref={forwardedRef}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n >\n {children}\n </button>\n ),\n);\n\nButton.displayName = \"Button\";\n", | ||
| "content": "import { cn } from \"@/lib/utils\";\nimport { cva, VariantProps } from \"class-variance-authority\";\nimport React, { ButtonHTMLAttributes } from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\n\nconst buttonVariants = cva(\n \"font-head transition-all outline-hidden cursor-pointer duration-200 font-medium flex items-center\",\n {\n variants: {\n variant: {\n default:\n \"shadow-md hover:shadow-none bg-primary text-black border-2 border-black transition hover:translate-y-1 hover:bg-primary-hover\",\n secondary:\n \"shadow-md hover:shadow-none bg-secondary shadow-primary text-secondary-foreground border-2 border-black transition hover:translate-y-1\",\n outline:\n \"shadow-md hover:shadow-none bg-transparent border-2 transition hover:translate-y-1\",\n link: \"bg-transparent hover:underline\",\n },\n size: {\n sm: \"px-3 py-1 text-sm shadow hover:shadow-none\",\n md: \"px-4 py-1.5 text-base\",\n lg: \"px-8 py-3 text-lg\",\n icon: \"p-2\",\n },\n },\n defaultVariants: {\n size: \"md\",\n variant: \"default\",\n },\n },\n);\n\nexport interface IButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n}\n\nexport const Button = React.forwardRef<HTMLButtonElement, IButtonProps>(\n (\n {\n children,\n size = \"md\",\n className = \"\",\n variant = \"default\",\n asChild = false,\n ...props\n }: IButtonProps,\n forwardedRef,\n ) => {\n const Comp = asChild ? Slot : \"button\";\n return (\n <Comp\n ref={forwardedRef}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n >\n {children}\n </Comp>\n );\n },\n);\n\nButton.displayName = \"Button\";\n", |
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.
Default native button type to “button” to prevent unintended form submits
Without an explicit type, a button inside a form submits by default. Set type="button" when not using asChild (and allow override).
Apply this diff:
-export const Button = React.forwardRef<HTMLButtonElement, IButtonProps>(
- (
- {
- children,
- size = "md",
- className = "",
- variant = "default",
- asChild = false,
- ...props
- }: IButtonProps,
- forwardedRef,
- ) => {
- const Comp = asChild ? Slot : "button";
- return (
- <Comp
- ref={forwardedRef}
- className={cn(buttonVariants({ variant, size }), className)}
- {...props}
- >
- {children}
- </Comp>
- );
- },
-);
+export const Button = React.forwardRef<HTMLButtonElement, IButtonProps>(
+ (
+ { children, size = "md", className = "", variant = "default", asChild = false, ...props }: IButtonProps,
+ forwardedRef,
+ ) => {
+ const Comp = asChild ? Slot : ("button" as const);
+ const { type, ...rest } = props;
+ return (
+ <Comp
+ ref={forwardedRef}
+ // Only apply default type for native button
+ {...(!asChild && { type: (type as any) ?? "button" })}
+ className={cn(buttonVariants({ variant, size }), className)}
+ {...rest}
+ >
+ {children}
+ </Comp>
+ );
+ },
+);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In public/r/button.json around line 13, the Button lacks an explicit type so it
can unintentionally submit forms; update the component to default the native
button type to "button" when rendering a real <button> (i.e., when asChild is
false) while still allowing consumers to override type via props: ensure the
props destructuring sets no fixed type, but when choosing Comp === "button" add
type={props.type ?? "button"} (or equivalent) to the rendered element, and do
not add type when asChild is true so Slot consumers can control it.
| { | ||
| "$schema": "https://ui.shadcn.com/schema/registry-item.json", | ||
| "name": "context-menu", | ||
| "type": "registry:component", | ||
| "title": "Context Menu", | ||
| "description": "Show contextual actions on right-click.", | ||
| "dependencies": [ | ||
| "@radix-ui/react-context-menu", | ||
| "lucide-react" | ||
| ], | ||
| "files": [ | ||
| { | ||
| "path": "components/retroui/ContextMenu.tsx", | ||
| "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\";\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction ContextMenu({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {\n return <ContextMenuPrimitive.Root data-slot=\"context-menu\" {...props} />;\n}\n\nfunction ContextMenuTrigger({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {\n return (\n <ContextMenuPrimitive.Trigger data-slot=\"context-menu-trigger\" {...props} />\n );\n}\n\nfunction ContextMenuGroup({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {\n return (\n <ContextMenuPrimitive.Group data-slot=\"context-menu-group\" {...props} />\n );\n}\n\nfunction ContextMenuPortal({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {\n return (\n <ContextMenuPrimitive.Portal data-slot=\"context-menu-portal\" {...props} />\n );\n}\n\nfunction ContextMenuSub({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {\n return <ContextMenuPrimitive.Sub data-slot=\"context-menu-sub\" {...props} />;\n}\n\nfunction ContextMenuRadioGroup({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {\n return (\n <ContextMenuPrimitive.RadioGroup\n data-slot=\"context-menu-radio-group\"\n {...props}\n />\n );\n}\n\nfunction ContextMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {\n inset?: boolean;\n}) {\n return (\n <ContextMenuPrimitive.SubTrigger\n data-slot=\"context-menu-sub-trigger\"\n data-inset={inset}\n className={cn(\n \"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\",\n className,\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ml-auto\" />\n </ContextMenuPrimitive.SubTrigger>\n );\n}\n\nfunction ContextMenuSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {\n return (\n <ContextMenuPrimitive.SubContent\n data-slot=\"context-menu-sub-content\"\n className={cn(\n \"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\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction ContextMenuContent({\n className,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {\n return (\n <ContextMenuPrimitive.Portal>\n <ContextMenuPrimitive.Content\n data-slot=\"context-menu-content\"\n className={cn(\n \"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\",\n className,\n )}\n {...props}\n />\n </ContextMenuPrimitive.Portal>\n );\n}\n\nfunction ContextMenuItem({\n className,\n inset,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n}) {\n return (\n <ContextMenuPrimitive.Item\n data-slot=\"context-menu-item\"\n data-inset={inset}\n data-variant={variant}\n className={cn(\n \"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\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction ContextMenuCheckboxItem({\n className,\n children,\n checked,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {\n return (\n <ContextMenuPrimitive.CheckboxItem\n data-slot=\"context-menu-checkbox-item\"\n className={cn(\n \"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\",\n className,\n )}\n checked={checked}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <ContextMenuPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </ContextMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </ContextMenuPrimitive.CheckboxItem>\n );\n}\n\nfunction ContextMenuRadioItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {\n return (\n <ContextMenuPrimitive.RadioItem\n data-slot=\"context-menu-radio-item\"\n className={cn(\n \"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\",\n className,\n )}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <ContextMenuPrimitive.ItemIndicator>\n <CircleIcon className=\"size-2 fill-current\" />\n </ContextMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </ContextMenuPrimitive.RadioItem>\n );\n}\n\nfunction ContextMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {\n inset?: boolean;\n}) {\n return (\n <ContextMenuPrimitive.Label\n data-slot=\"context-menu-label\"\n data-inset={inset}\n className={cn(\n \"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction ContextMenuSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {\n return (\n <ContextMenuPrimitive.Separator\n data-slot=\"context-menu-separator\"\n className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n {...props}\n />\n );\n}\n\nfunction ContextMenuShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"context-menu-shortcut\"\n className={cn(\n \"text-muted-foreground ml-auto text-xs tracking-widest\",\n className,\n )}\n {...props}\n />\n );\n}\n\nconst ContextMenuComponent = Object.assign(ContextMenu, {\n Trigger: ContextMenuTrigger,\n Content: ContextMenuContent,\n Item: ContextMenuItem,\n CheckboxItem: ContextMenuCheckboxItem,\n RadioItem: ContextMenuRadioItem,\n Label: ContextMenuLabel,\n Separator: ContextMenuSeparator,\n Shortcut: ContextMenuShortcut,\n Group: ContextMenuGroup,\n Portal: ContextMenuPortal,\n Sub: ContextMenuSub,\n SubContent: ContextMenuSubContent,\n SubTrigger: ContextMenuSubTrigger,\n RadioGroup: ContextMenuRadioGroup,\n});\n\nexport { ContextMenuComponent as ContextMenu };\n", | ||
| "type": "registry:component", | ||
| "target": "components/retroui/ContextMenu.tsx" | ||
| } | ||
| ] | ||
| } No newline at end of file |
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.
💡 Verification agent
🧩 Analysis chain
Remember to rebuild the local registry and validate types.
Given this is a new registry item with inline content, please:
- Run pnpm run registry:build to regenerate the JSON registry.
- Ensure @radix-ui/react-context-menu is in package.json dependencies and installed.
- Run pnpm run typecheck to catch any missed generics after adding forwardRef.
Run this quick verification script from repo root:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Checking files exist and exports wired..."
fd -a "ContextMenu.tsx"
rg -n "export\s+\*\s+from\s+\"\.\/ContextMenu\"" -g "components/retroui/index.ts" || echo "Note: re-export missing in components/retroui/index.ts"
rg -n "\"@radix-ui/react-context-menu\"" package.json || (echo "Missing @radix-ui/react-context-menu in package.json" && exit 1)
echo "Cross-check registry entries..."
rg -n "\"name\":\s*\"context-menu\"" public/r/context-menu.json registry.json
rg -n "\"components/retroui/ContextMenu.tsx\"" public/r/context-menu.json registry.json
echo "Grepping references in config/docs/preview..."
rg -n "context-menu" config content preview || true
echo "OK. Now run:"
echo " pnpm i"
echo " pnpm run registry:build"
echo " pnpm run typecheck"Length of output: 2447
Add missing export, forward refs, then rebuild registry & typecheck
The core setup is correct (file exists, dependency present, registry entry and docs updated), but two refinements are needed before merging:
• components/retroui/index.ts
– Add a re-export so consumers can import from the module:
diff // at end of components/retroui/index.ts +export * from "./ContextMenu";
• components/retroui/ContextMenu.tsx
– Wrap the root component in React.forwardRef to forward refs and preserve proper type inference:
```diff
-function ContextMenu({
- ...props
-}: React.ComponentProps) { - return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
-}
+import React from "react";
+export const ContextMenu = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
+>((props, ref) => ( - <ContextMenuPrimitive.Root ref={ref} data-slot="context-menu" {...props} />
+));
Everything else (the @radix-ui/react-context-menu dependency, registry JSON, navigation/docs references) is wired correctly.
Next steps:
- pnpm install
- pnpm run registry:build
- pnpm run typecheck
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In components/retroui/ContextMenu.tsx (the component file in the registry entry)
and components/retroui/index.ts: add a forwardRef wrapper for the Root component
by converting the current ContextMenu function into a React.forwardRef that
forwards the ref to ContextMenuPrimitive.Root with the correct element/ref
generic (use React.ComponentPropsWithRef<typeof ContextMenuPrimitive.Root> for
props and forward the ref to the underlying Root element), export the resulting
component under the same name/shape so consumers keep the existing API; then add
a re-export in components/retroui/index.ts that exports ContextMenu from
./ContextMenu (so consumers can import from the module). After changes run pnpm
install (if needed), pnpm run registry:build and pnpm run typecheck.
| { | ||
| "path": "components/retroui/Select.tsx", | ||
| "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check, ChevronDown } from \"lucide-react\";\nimport React from \"react\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectTrigger = ({\n className,\n children,\n ...props\n}: SelectPrimitive.SelectTriggerProps) => {\n return (\n <SelectPrimitive.Trigger\n className={cn(\n \"flex h-10 min-w-40 items-center shadow-md justify-between border-2 border-input border-border bg-transparent px-4 py-2 ring-offset-background placeholder:text-muted-foreground focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50\",\n className,\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <ChevronDown className=\"ml-2 h-4 w-4\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n};\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectIcon = SelectPrimitive.Icon;\n\nconst SelectContent = ({\n className,\n children,\n position = \"popper\",\n ...props\n}: SelectPrimitive.SelectContentProps) => {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n className={cn(\n \"relative z-50 min-w-[8rem] overflow-hidden border border-border bg-background text-popover-foreground 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\",\n position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className,\n )}\n position={position}\n {...props}\n >\n <SelectPrimitive.Viewport\n className={cn(\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n};\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectItem = ({\n className,\n children,\n ...props\n}: SelectPrimitive.SelectItemProps) => (\n <SelectPrimitive.Item\n className={cn(\n \"relative flex w-full cursor-default select-none items-center py-1.5 px-2 outline-hidden hover:bg-primary hover:text-primary-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n className,\n )}\n {...props}\n >\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n\n <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n <SelectPrimitive.ItemIndicator>\n <Check className=\"h-4 w-4 text-foreground\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n </SelectPrimitive.Item>\n);\nconst SelectLabel = SelectPrimitive.Label;\nconst SelectSeparator = SelectPrimitive.Separator;\n\nconst SelectObj = Object.assign(Select, {\n Trigger: SelectTrigger,\n Value: SelectValue,\n Icon: SelectIcon,\n Content: SelectContent,\n Group: SelectGroup,\n Item: SelectItem,\n Label: SelectLabel,\n Separator: SelectSeparator,\n});\n\nexport { SelectObj as Select };\n", | ||
| "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\";\nimport React from \"react\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectTrigger = ({\n className,\n children,\n ...props\n}: SelectPrimitive.SelectTriggerProps) => {\n return (\n <SelectPrimitive.Trigger\n className={cn(\n \"flex h-10 min-w-40 items-center shadow-md justify-between border-2 border-input border-border bg-transparent px-4 py-2 placeholder:text-muted-foreground outline-none focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50\",\n className,\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <ChevronDown className=\"ml-2 h-4 w-4\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n};\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectIcon = SelectPrimitive.Icon;\n\nconst SelectContent = ({\n className,\n children,\n position = \"popper\",\n ...props\n}: SelectPrimitive.SelectContentProps) => {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n className={cn(\n \"relative z-50 min-w-[8rem] overflow-hidden border border-border bg-background text-popover-foreground 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\",\n position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className,\n )}\n position={position}\n {...props}\n >\n <SelectPrimitive.ScrollUpButton className=\"flex cursor-default items-center justify-center py-1 text-muted-foreground\">\n <ChevronUp className=\"h-4 w-4\" />\n </SelectPrimitive.ScrollUpButton>\n <SelectPrimitive.Viewport\n className={cn(\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n <SelectPrimitive.ScrollDownButton className=\"flex cursor-default items-center justify-center py-1 text-muted-foreground\">\n <ChevronDown className=\"h-4 w-4\" />\n </SelectPrimitive.ScrollDownButton>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n};\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectItem = ({\n className,\n children,\n ...props\n}: SelectPrimitive.SelectItemProps) => (\n <SelectPrimitive.Item\n className={cn(\n \"relative flex w-full cursor-default select-none items-center py-1.5 px-2 outline-none data-[highlighted]:bg-primary data-[highlighted]:text-primary-foreground focus:bg-primary focus:text-primary-foreground data-disabled:pointer-events-none data-disabled:opacity-50\",\n className,\n )}\n {...props}\n >\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n\n <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n <SelectPrimitive.ItemIndicator>\n <Check className=\"h-4 w-4 text-foreground\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n </SelectPrimitive.Item>\n);\nconst SelectLabel = SelectPrimitive.Label;\nconst SelectSeparator = SelectPrimitive.Separator;\n\nconst SelectObj = Object.assign(Select, {\n Trigger: SelectTrigger,\n Value: SelectValue,\n Icon: SelectIcon,\n Content: SelectContent,\n Group: SelectGroup,\n Item: SelectItem,\n Label: SelectLabel,\n Separator: SelectSeparator,\n});\n\nexport { SelectObj as Select };\n", |
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.
Select.Trigger removes visible focus — restore an accessible focus indicator
Current classes include outline-none/focus-visible:outline-none, resulting in no visible focus for keyboard users. Add an explicit focus-visible style.
Apply this diff to SelectTrigger:
- <SelectPrimitive.Trigger
- className={cn(
- "flex h-10 min-w-40 items-center shadow-md justify-between border-2 border-input border-border bg-transparent px-4 py-2 placeholder:text-muted-foreground outline-none focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
+ <SelectPrimitive.Trigger
+ className={cn(
+ "flex h-10 min-w-40 items-center justify-between px-4 py-2 bg-transparent shadow-md border-2 border-border",
+ // Accessible focus
+ "outline-none focus-visible:outline outline-2 outline-offset-2 outline-primary",
+ // State
+ "disabled:cursor-not-allowed disabled:opacity-50",
className,
)}Notes:
- Keep a single border token (border-border) to avoid conflicts with border-input. If border-input is intentional, prefer one source of truth.
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
const SelectTrigger = ({
className,
children,
...props
}: SelectPrimitive.SelectTriggerProps) => {
return (
<SelectPrimitive.Trigger
className={cn(
// Layout & spacing
"flex h-10 min-w-40 items-center justify-between px-4 py-2 bg-transparent shadow-md border-2 border-border",
// Accessible focus
"outline-none focus-visible:outline outline-2 outline-offset-2 outline-primary",
// Disabled state
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="ml-2 h-4 w-4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
};
🤖 Prompt for AI Agents
public/r/select.json around line 14: the SelectTrigger component currently
strips visible focus by using outline-none and focus-visible:outline-none and
also mixes border-input and border-border; restore an accessible focus indicator
by removing the outline-none and focus-visible:outline-none classes and add an
explicit focus-visible class (e.g., focus-visible:ring-2 focus-visible:ring-ring
or focus-visible:ring-primary with a contrasting ring color) so keyboard users
see focus, and ensure only a single border token is used (replace border-input
with border-border or vice versa) to keep one source of truth.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Add ContextMenu component
Added a ContextMenu component with retro UI styling, smooth animations, and full documentation. Includes all sub-components (Trigger, Content, Item, etc.) and follows the same export pattern as other components. Added to registry, navigation, and includes interactive preview example.
Summary by CodeRabbit
New Features
Documentation
Examples
Chores