Skip to content

feat: add Popover, Skeleton, and ScrollArea components with Storybook stories#50

Merged
Aunshon merged 4 commits intomainfrom
add/new-components
Feb 15, 2026
Merged

feat: add Popover, Skeleton, and ScrollArea components with Storybook stories#50
Aunshon merged 4 commits intomainfrom
add/new-components

Conversation

@Aunshon
Copy link
Collaborator

@Aunshon Aunshon commented Feb 10, 2026

Summary by CodeRabbit

  • New Features

    • Added Popover component (trigger, content, header/title/description, arrow/close)
    • Added ScrollArea with vertical/horizontal modes and configurable scrollbar
    • Added Skeleton component for loading placeholders
  • Documentation

    • Added Storybook stories demonstrating Popover, ScrollArea (Default/Horizontal/Both), and Skeleton usage and variants

… stories

- Introduced new `Popover`, `Skeleton`, and `ScrollArea` components in the UI library.
- Added Storybook stories for each component (`Popover.stories.tsx`, `Skeleton.stories.tsx`, `ScrollArea.stories.tsx`) showcasing their usage and variations.
- Exported new components in the `ui/index.ts` for external use.
@Aunshon Aunshon self-assigned this Feb 10, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

Adds three new UI components (Popover, ScrollArea, Skeleton), Storybook stories for each, and re-exports them from the UI barrel and main index. All changes are additive with no changes to existing runtime behavior.

Changes

Cohort / File(s) Summary
Component Implementations
src/components/ui/popover.tsx, src/components/ui/scroll-area.tsx, src/components/ui/skeleton.tsx
New components: Popover (wrapping Popover primitives: Trigger, Portal, Content, Title, Description, Header), ScrollArea (Root/Viewport/Content with ScrollBar and Corner, orientation support), and Skeleton (simple pulsing placeholder).
Storybook Stories
src/components/ui/Popover.stories.tsx, src/components/ui/ScrollArea.stories.tsx, src/components/ui/Skeleton.stories.tsx
Added Storybook stories: Popover default demo with labeled inputs; ScrollArea stories (Default, Horizontal, Both); Skeleton stories (Default, Card, List).
Barrel & Public Exports
src/components/ui/index.ts, src/index.ts
Re-exported new UI components and subcomponents from the UI barrel and main package index: Popover (+subparts), ScrollArea, ScrollBar, Skeleton.
Story/Meta Types
src/components/ui/*.stories.tsx
Added Storybook meta objects, Story type aliases, and exported story constants for each new story file.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant Trigger as PopoverTrigger
  participant Portal as PopoverPortal
  participant Content as PopoverContent

  User->>Trigger: click
  Trigger->>Portal: open portal
  Portal->>Content: mount content
  Content-->>User: render panel (Title, Description, Inputs)
  User->>Content: interact / close
  Content->>Portal: unmount/close
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • mrabbani

Poem

🐰 I hopped in with a tiny cheer,
Popovers, scrolls, and skeletons appear,
Stories bright as a carrot's gleam,
Exported neat — a dev's little dream,
Hop on, test — the rabbit's near.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding three UI components (Popover, Skeleton, ScrollArea) with their corresponding Storybook stories, which aligns with all file additions in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add/new-components

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

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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: 2

🤖 Fix all issues with AI agents
In `@src/components/ui/scroll-area.tsx`:
- Around line 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.

In `@src/components/ui/ScrollArea.stories.tsx`:
- Around line 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.
🧹 Nitpick comments (4)
src/components/ui/scroll-area.tsx (1)

1-2: Unused React import.

With the modern JSX transform, import * as React from "react" is unnecessary here since no React APIs (e.g., React.Fragment, React.useState) are used directly.

src/components/ui/Skeleton.stories.tsx (1)

33-58: Consider reducing duplication in the List story.

The three list items are identical. A .map() would reduce the boilerplate, consistent with how the Default story in ScrollArea.stories.tsx generates items.

Suggested refactor
 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>
+      {Array.from({ length: 3 }).map((_, i) => (
+        <div key={i} 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>
   ),
 };
src/components/ui/popover.tsx (2)

15-17: Exported PopoverPortal may confuse consumers since PopoverContent already renders its own Portal.

PopoverContent (line 33) internally wraps everything in PopoverPrimitive.Portal, so if a consumer also wraps PopoverContent in PopoverPortal, the content gets double-portaled. Consider either removing this export or documenting that it's only needed for advanced/custom composition outside of PopoverContent.


54-68: Simple pass-through wrappers look fine.

Minor nit: the { ...props } destructuring pattern (e.g., { ...props }: Type) creates an unnecessary shallow copy. You could use (props: Type) directly. This applies to all the simple wrappers in this file.

Comment on lines +6 to +24
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>
);
}
Copy link

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.

Comment on lines +40 to +69
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>
),
Copy link

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.

Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@src/components/ui/Popover.stories.tsx`:
- Around line 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.
🧹 Nitpick comments (1)
src/components/ui/popover.tsx (1)

15-17: PopoverPortal is dead code — defined but neither exported nor used internally.

PopoverContent creates its own PopoverPrimitive.Portal (line 33), so this standalone wrapper serves no purpose. Remove it to avoid confusion.

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

Comment on lines +21 to +23
<PopoverTrigger>
<Button variant="outline">Open Popover</Button>
</PopoverTrigger>
Copy link

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.

@Aunshon Aunshon merged commit 2c0e072 into main Feb 15, 2026
1 check failed
@Aunshon Aunshon deleted the add/new-components branch February 15, 2026 11:47
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.

1 participant

Comments