-
Notifications
You must be signed in to change notification settings - Fork 10
feat: add Popover, Skeleton, and ScrollArea components with Storybook stories #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
199f60f
ab34a20
fd21924
732f74d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,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> | ||
| <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> | ||
| ), | ||
| }; | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Horizontal and Both stories won't render functional scrollbars. As noted in the 🤖 Prompt for AI Agents |
||
| }; | ||
| 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> | ||
| ), | ||
| }; |
| 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, | ||
| }; |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ScrollBar children passed to ScrollArea are rendered inside Content, not at Root level. Children are placed inside Consider either:
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 |
||
|
|
||
| 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 }; | ||
| 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 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
base-ui react Popover Trigger component API documentation💡 Result:
Base UI’s React
Popover.TriggerAPI 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 handlenativeButton?: boolean(defaulttrue) — when usingrender, keep native<button>semantics; setfalseif your rendered element isn’t a buttonpayload?: Payload— value passed toPopover.Rootwhen opened (useful with multiple triggers)openOnHover?: boolean(defaultfalse) — also open on hoverdelay?: number(default300) — hover-open delay (ms); requiresopenOnHovercloseDelay?: number(default0) — hover-close delay (ms); requiresopenOnHoverid?: string— trigger ID (used for controlled mode viaPopover.Root triggerId)className?: string | (state) => stringstyle?: CSSProperties | (state) => CSSPropertiesrender?: ReactElement | (props, state) => ReactElement— replace/compose the rendered elementState/data attributes noted in the docs:
data-popup-open,data-pressed. [1]Source
Citations:
🏁 Script executed:
Repository: getdokan/plugin-ui
Length of output: 133
🏁 Script executed:
Repository: getdokan/plugin-ui
Length of output: 278
🏁 Script executed:
# Read the Popover component implementation cat -n ./src/components/ui/popover.tsxRepository: getdokan/plugin-ui
Length of output: 3527
🏁 Script executed:
# Read the Button component implementation cat -n ./src/components/ui/button.tsxRepository: 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.tsxRepository: getdokan/plugin-ui
Length of output: 2880
Nested
<button>elements —PopoverTriggerrenders a<button>, andButtonis also a<button>.This produces invalid HTML (
<button><button>…</button></button>) and can cause accessibility and click-handling issues. Base UI's Trigger accepts arenderprop to customize the rendered element. Use that instead, or pass the Button directly viarender.Possible fix using `render` prop
🤖 Prompt for AI Agents