Skip to content

Conversation

@ameybh
Copy link
Contributor

@ameybh ameybh commented Aug 20, 2025

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

    • Added a fully featured Context Menu with submenus, checkable items, radio groups, labels, separators, keyboard shortcut display, inset/variant styling, animations, and portal behavior.
  • Documentation

    • New docs page with CLI and manual install flows, source reference, and a default-style showcase; navigation updated to include Context Menu.
  • Examples

    • Added a preview/example demonstrating Context Menu usage and styling.
  • Chores

    • Added Radix context-menu dependency, registry/config entries, and registry formatting updates.

@vercel
Copy link

vercel bot commented Aug 20, 2025

@ameybh is attempting to deploy a commit to the Retro UI Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 20, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Context Menu component
components/retroui/ContextMenu.tsx
New styled wrapper around Radix Context Menu primitives exporting ContextMenu with static subcomponents (Trigger, Content, Item, CheckboxItem, RadioItem, Label, Separator, Shortcut, Group, Portal, Sub, SubContent, SubTrigger, RadioGroup). Includes variants, inset handling, indicators, submenu support, and portal-based animations.
Public exports
components/retroui/index.ts
Re-exports the ContextMenu module via export * from "./ContextMenu".
Configs (components & navigation)
config/components.ts, config/navigation.ts
Adds context-menu to componentConfig and a context-menu-style-default example; inserts "Context Menu" into side navigation; minor formatting fix.
Documentation & Preview
content/docs/components/context-menu.mdx, preview/components/context-menu-style-default.tsx
Adds MDX documentation page with install/source info and a client preview demo component showcasing the ContextMenu.
Package dependency
package.json
Adds dependency @radix-ui/react-context-menu@^2.2.16.
Registry & public previews
registry.json, public/r/*.json
Adds registry entry context-menu (dependencies include @radix-ui/react-context-menu, lucide-react); normalizes dependency-array formatting; updates multiple public preview JSONs for styling/layout tweaks (accordion, badge, button, select, table, etc.).

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

I nibble bytes and hop with glee,
A menu sprouts where right-clicks be.
Submenu whispers, checkmarks shine,
Portal petals fold in line.
A rabbit cheers — the UI's fine! 🐇✨

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 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 34181ad and a1624cf.

📒 Files selected for processing (1)
  • config/components.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/components.ts
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 Radix

Without 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 focus

This 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 decorative

ItemIndicator 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 decorative

Same 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 demo

Use 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 demos

Use 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 accurate

Several 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 menu

Context 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 82ce613 and 9e67ba5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is 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 good

Simple pass-through to Radix Root with a stable public surface and data-slot for theming. No issues.


129-129: Verify custom utility class outline-hidden exists

Tailwind’s standard utility is outline-none. If outline-hidden is a custom plugin/class, ignore; otherwise replace to avoid no-op classes.


236-251: Static composition via Object.assign looks good

The 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 barrel

Good addition; matches the pattern used by other components.

config/navigation.ts (2)

40-41: Trailing-comma fix looks fine

Keeps array syntax clean and consistent.


58-58: New “Context Menu” nav entry wired correctly

Path 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 good

No semantic changes; consistent with nearby entries.


431-433: LGTM on multiline lazy import

Purely stylistic and consistent with other examples.


603-609: Default export confirmed for ContextMenuStyleDefault

The file preview/components/context-menu-style-default.tsx contains a default export on line 5 (export default function ContextMenuStyleDefault() { … }), so the lazy(() => 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 value

If the site surfaces this field, ensure the date format matches other pages and reflects the actual update date for consistency.

Comment on lines +114 to +135
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}
/>
);
}
Copy link
Contributor

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.

Suggested change
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'" />
Copy link
Contributor

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' | head

Length 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' | head

Length 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 20

Length 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.

Comment on lines +19 to +20
npm install class-variance-authority
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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",
Copy link
Contributor

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-menu

Length 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-any errors indicate that contentlayer types aren’t generated before tscfail 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 build step in your prebuild or CI scripts.
    • Confirm imports reference the correct output path.
  • Re-run:
    pnpm dedupe
    pnpm -s exec tsc -p .
    to verify no peer warnings or type errors remain.
🤖 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.

@ameybh
Copy link
Contributor Author

ameybh commented Aug 20, 2025

@ariflogs Do I need to run pnpm run registry:build to create the json registry for new components?

@ariflogs
Copy link
Member

@ariflogs Do I need to run pnpm run registry:build to create the json registry for new components?

yes

@ameybh
Copy link
Contributor Author

ameybh commented Aug 21, 2025

Ready for review. @ariflogs

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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)

10-10: Wrapper introduced + default border/shadow on

can be surprising

  • 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 defaults

The 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.Value

placeholder: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 highlighted

When 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 style

Keep 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 duplication

Item 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 it

Current 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=true

Ref 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 273fbce and cb05fd1.

📒 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 improvement

Switching 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",
Copy link
Contributor

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.

Suggested change
"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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines +1 to +19
{
"$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
Copy link
Contributor

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:

  1. pnpm install
  2. pnpm run registry:build
  3. 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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

@vercel
Copy link

vercel bot commented Aug 24, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
retro-ui Ready Ready Preview Comment Aug 24, 2025 5:19pm

@ariflogs ariflogs merged commit 2b8825e into Logging-Studio:main Aug 24, 2025
3 checks passed
@ameybh ameybh deleted the context-menu branch August 25, 2025 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants