Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/components/ui/Popover.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Popover, PopoverTrigger, PopoverContent, PopoverTitle, PopoverDescription } from "./popover";
import { Button } from "./button";
import { Input } from "./input";
import { Label } from "./label";

const meta = {
title: "UI/Popover",
component: Popover,
parameters: { layout: "centered" },
tags: ["autodocs"],
} satisfies Meta<typeof Popover>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: () => (
<Popover>
<PopoverTrigger>
<Button variant="outline">Open Popover</Button>
</PopoverTrigger>
Comment on lines +21 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

base-ui react Popover Trigger component API documentation

💡 Result:

Base UI’s React Popover.Trigger API docs are on the Popover component page, under API reference → Trigger. [1]

<Popover.Trigger /> (renders a <button> by default) props: [1]

  • handle?: Popover.Handle<Payload> — links a (possibly detached) trigger to a popover handle
  • nativeButton?: boolean (default true) — when using render, keep native <button> semantics; set false if your rendered element isn’t a button
  • payload?: Payload — value passed to Popover.Root when opened (useful with multiple triggers)
  • openOnHover?: boolean (default false) — also open on hover
  • delay?: number (default 300) — hover-open delay (ms); requires openOnHover
  • closeDelay?: number (default 0) — hover-close delay (ms); requires openOnHover
  • id?: string — trigger ID (used for controlled mode via Popover.Root triggerId)
  • className?: string | (state) => string
  • style?: CSSProperties | (state) => CSSProperties
  • render?: ReactElement | (props, state) => ReactElement — replace/compose the rendered element

State/data attributes noted in the docs: data-popup-open, data-pressed. [1]

Source

  • [1] Base UI Popover docs (API reference → Trigger): (base-ui.com)

Citations:


🏁 Script executed:

# First, let's find and examine the Popover component implementation
find . -type f -name "*opover*" | head -20

Repository: getdokan/plugin-ui

Length of output: 133


🏁 Script executed:

# Let's also check the Button component
find . -type f -name "*utton*" | head -20

Repository: getdokan/plugin-ui

Length of output: 278


🏁 Script executed:

# Read the Popover component implementation
cat -n ./src/components/ui/popover.tsx

Repository: getdokan/plugin-ui

Length of output: 3527


🏁 Script executed:

# Read the Button component implementation
cat -n ./src/components/ui/button.tsx

Repository: getdokan/plugin-ui

Length of output: 4936


🏁 Script executed:

# Read the Popover stories file to see the exact code
cat -n ./src/components/ui/Popover.stories.tsx

Repository: getdokan/plugin-ui

Length of output: 2880


Nested <button> elements — PopoverTrigger renders a <button>, and Button is also a <button>.

This produces invalid HTML (<button><button>…</button></button>) and can cause accessibility and click-handling issues. Base UI's Trigger accepts a render prop to customize the rendered element. Use that instead, or pass the Button directly via render.

Possible fix using `render` prop
-      <PopoverTrigger>
-        <Button variant="outline">Open Popover</Button>
-      </PopoverTrigger>
+      <PopoverTrigger render={<Button variant="outline" />}>
+        Open Popover
+      </PopoverTrigger>
🤖 Prompt for AI Agents
In `@src/components/ui/Popover.stories.tsx` around lines 21 - 23, The story nests
a Button inside PopoverTrigger which causes invalid nested <button> elements;
update the PopoverTrigger usage (symbol: PopoverTrigger) to render a non-button
wrapper or use its render prop so it doesn't output a <button> around the Button
component (symbol: Button). Replace the current children usage with
PopoverTrigger's render prop (or pass Button directly via render) so the trigger
renders a div/span/fragment or returns the Button element itself, ensuring only
a single interactive element is output and preserving accessibility and click
behavior.

<PopoverContent className="w-80">
<div className="grid gap-4">
<div className="space-y-2">
<PopoverTitle className="font-medium leading-none">Dimensions</PopoverTitle>
<PopoverDescription className="text-muted-foreground text-sm">
Set the dimensions for the layer.
</PopoverDescription>
</div>
<div className="grid gap-2">
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="width">Width</Label>
<Input
id="width"
defaultValue="100%"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxWidth">Max. width</Label>
<Input
id="maxWidth"
defaultValue="300px"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="height">Height</Label>
<Input
id="height"
defaultValue="25px"
className="col-span-2 h-8"
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxHeight">Max. height</Label>
<Input
id="maxHeight"
defaultValue="none"
className="col-span-2 h-8"
/>
</div>
</div>
</div>
</PopoverContent>
</Popover>
),
};
70 changes: 70 additions & 0 deletions src/components/ui/ScrollArea.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Meta, StoryObj } from "@storybook/react";
import * as React from "react";
import { ScrollArea, ScrollBar } from "./scroll-area";
import { Separator } from "./separator";

const meta = {
title: "UI/ScrollArea",
component: ScrollArea,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof ScrollArea>;

export default meta;
type Story = StoryObj<typeof meta>;

const tags = Array.from({ length: 50 }).map(
(_, i, a) => `v1.2.0-beta.${a.length - i}`
);

export const Default: Story = {
render: () => (
<ScrollArea className="h-72 w-48 rounded-md border">
<div className="p-4">
<h4 className="mb-4 text-sm font-medium leading-none">Tags</h4>
{tags.map((tag) => (
<React.Fragment key={tag}>
<div className="text-sm">
{tag}
</div>
<Separator className="my-2" />
</React.Fragment>
))}
</div>
</ScrollArea>
),
};

export const Horizontal: Story = {
render: () => (
<ScrollArea className="w-96 whitespace-nowrap rounded-md border">
<div className="flex w-max space-x-4 p-4">
{Array.from({ length: 20 }).map((_, i) => (
<div
key={i}
className="bg-muted flex h-40 w-32 items-center justify-center rounded-md"
>
Item {i + 1}
</div>
))}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
),
};

export const Both: Story = {
render: () => (
<ScrollArea className="h-72 w-96 rounded-md border">
<div className="grid h-[500px] w-[600px] place-items-center bg-muted/20">
<div className="text-sm font-medium">
Large content area (600x500px)
</div>
</div>
<ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" />
</ScrollArea>
),
Comment on lines +40 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Horizontal and Both stories won't render functional scrollbars.

As noted in the scroll-area.tsx review, the <ScrollBar> components placed as children here end up inside ScrollAreaPrimitive.Content rather than at the Root level. These stories will need to be updated once the ScrollArea component API is revised to support configurable orientations.

🤖 Prompt for AI Agents
In `@src/components/ui/ScrollArea.stories.tsx` around lines 40 - 69, The
Horizontal and Both stories currently render <ScrollBar> as children inside
<ScrollArea> (ending up in ScrollAreaPrimitive.Content) so they won't show
functional scrollbars; update the stories (Horizontal, Both) to match the
revised ScrollArea API by moving or mounting the <ScrollBar orientation="...">
at the ScrollArea root/slot required by the new API (or use the new
prop/slot/component the ScrollArea exposes for scrollbars) so that the scrollbar
is attached at the Root level rather than inside Content; ensure you reference
ScrollArea and ScrollBar symbols when updating the render functions so
orientation ("horizontal"/"vertical") is applied per the new API.

};
59 changes: 59 additions & 0 deletions src/components/ui/Skeleton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Skeleton } from "./skeleton";

const meta = {
title: "UI/Skeleton",
component: Skeleton,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof Skeleton>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: () => <Skeleton className="h-4 w-[250px]" />,
};

export const Card: Story = {
render: () => (
<div className="flex items-center space-x-4">
<Skeleton className="h-12 w-12 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
),
};

export const List: Story = {
render: () => (
<div className="space-y-4 w-[300px]">
<div className="flex items-center space-x-4">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
</div>
<div className="flex items-center space-x-4">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
</div>
<div className="flex items-center space-x-4">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
</div>
</div>
),
};
18 changes: 18 additions & 0 deletions src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,24 @@ export {
DropdownMenuTrigger,
} from "./dropdown-menu";

// Popover component
export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverPortal,
PopoverDescription,
PopoverTitle,
PopoverClose,
PopoverArrow,
} from "./popover";

// Skeleton component
export { Skeleton } from "./skeleton";

// ScrollArea component
export { ScrollArea, ScrollBar } from "./scroll-area";

// AlertDialog component
export {
AlertDialog,
Expand Down
79 changes: 79 additions & 0 deletions src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import * as React from "react";

import { cn } from "@/lib/utils";
import { useTheme } from "@/providers";

function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}

function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}

function PopoverPortal({ ...props }: PopoverPrimitive.Portal.Props) {
return <PopoverPrimitive.Portal data-slot="popover-portal" {...props} />;
}

function PopoverContent({
className,
align = "center",
alignOffset = 0,
side = "bottom",
sideOffset = 4,
...props
}: PopoverPrimitive.Popup.Props &
Pick<
PopoverPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
const { mode } = useTheme();
return (
<PopoverPrimitive.Portal className={cn("pui-root", mode)}>
<PopoverPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
>
<PopoverPrimitive.Popup
data-slot="popover-content"
className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
className
)}
{...props}
/>
</PopoverPrimitive.Positioner>
</PopoverPrimitive.Portal>
);
}

function PopoverDescription({ ...props }: PopoverPrimitive.Description.Props) {
return <PopoverPrimitive.Description data-slot="popover-description" {...props} />;
}

function PopoverTitle({ ...props }: PopoverPrimitive.Title.Props) {
return <PopoverPrimitive.Title data-slot="popover-title" {...props} />;
}

function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
return <PopoverPrimitive.Close data-slot="popover-close" {...props} />;
}

function PopoverArrow({ ...props }: PopoverPrimitive.Arrow.Props) {
return <PopoverPrimitive.Arrow data-slot="popover-arrow" {...props} />;
}

export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverPortal,
PopoverDescription,
PopoverTitle,
PopoverClose,
PopoverArrow,
};
50 changes: 50 additions & 0 deletions src/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
import * as React from "react";

import { cn } from "@/lib/utils";

function ScrollArea({
className,
children,
...props
}: ScrollAreaPrimitive.Root.Props) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] outline-none">
<ScrollAreaPrimitive.Content>{children}</ScrollAreaPrimitive.Content>
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
Comment on lines +6 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

ScrollBar children passed to ScrollArea are rendered inside Content, not at Root level.

Children are placed inside ScrollAreaPrimitive.Content (Line 18), so any <ScrollBar> added as a child (as done in the Horizontal and Both stories) will be nested inside the scrollable content area rather than being a sibling of Viewport at the Root level. This means horizontal/additional scrollbars won't function correctly in those stories.

Consider either:

  1. Filtering ScrollBar instances out of children and rendering them alongside the hardcoded one, or
  2. Accepting a prop like orientation to control which scrollbars to render internally.
Option 2: Add orientation prop
+type ScrollAreaProps = ScrollAreaPrimitive.Root.Props & {
+  orientation?: "vertical" | "horizontal" | "both";
+};
+
 function ScrollArea({
   className,
   children,
+  orientation = "vertical",
   ...props
-}: ScrollAreaPrimitive.Root.Props) {
+}: ScrollAreaProps) {
   return (
     <ScrollAreaPrimitive.Root
       data-slot="scroll-area"
       className={cn("relative overflow-hidden", className)}
       {...props}
     >
       <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] outline-none">
         <ScrollAreaPrimitive.Content>{children}</ScrollAreaPrimitive.Content>
       </ScrollAreaPrimitive.Viewport>
-      <ScrollBar />
+      {(orientation === "vertical" || orientation === "both") && (
+        <ScrollBar orientation="vertical" />
+      )}
+      {(orientation === "horizontal" || orientation === "both") && (
+        <ScrollBar orientation="horizontal" />
+      )}
       <ScrollAreaPrimitive.Corner />
     </ScrollAreaPrimitive.Root>
   );
 }
🤖 Prompt for AI Agents
In `@src/components/ui/scroll-area.tsx` around lines 6 - 24, The ScrollBar child
is being rendered inside ScrollAreaPrimitive.Content instead of as a sibling of
Viewport, so update the ScrollArea component to extract any ScrollBar children
from the children prop and render them alongside the existing <ScrollBar /> at
the Root level (outside ScrollAreaPrimitive.Content); specifically, in the
ScrollArea function filter React.Children.toArray(children) for elements with
type === ScrollBar (or a distinguishing prop), keep non-scrollbar children to
render inside ScrollAreaPrimitive.Content and render the extracted ScrollBar
elements after the Viewport (alongside the hardcoded <ScrollBar />) so
horizontal/additional scrollbars from the Horizontal and Both stories function
correctly.


function ScrollBar({
className,
orientation = "vertical",
...props
}: ScrollAreaPrimitive.Scrollbar.Props) {
return (
<ScrollAreaPrimitive.Scrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.Thumb className="bg-border relative flex-1 rounded-full" />
</ScrollAreaPrimitive.Scrollbar>
);
}

export { ScrollArea, ScrollBar };
18 changes: 18 additions & 0 deletions src/components/ui/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from "react"

import { cn } from "@/lib/utils"

function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="skeleton"
className={cn("bg-muted animate-pulse rounded-md", className)}
{...props}
/>
)
}

export { Skeleton }
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,20 @@ export {
ModalFooter, ModalHeader, ModalOverlay, ModalTitle,
// Notice
Notice, NoticeAction, NoticeTitle,
// Popover
Popover,
PopoverArrow,
PopoverClose,
PopoverContent,
PopoverDescription,
PopoverPortal,
PopoverTitle,
PopoverTrigger,
// Skeleton
Skeleton,
// ScrollArea
ScrollArea,
ScrollBar,
// Progress
Progress, ProgressIndicator,
ProgressLabel, ProgressTrack, ProgressValue,
Expand Down
Loading