Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
178 changes: 178 additions & 0 deletions packages/frappe-ui-react/src/components/divider/divider.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ChevronDown } from "lucide-react";

import Divider from "./divider";
import { Button } from "../button";
import { Badge } from "../badge";

const meta: Meta<typeof Divider> = {
title: "Components/Divider",
component: Divider,
parameters: { docs: { source: { type: "dynamic" } }, layout: "centered" },
tags: ["autodocs"],
argTypes: {
orientation: {
control: { type: "select" },
options: ["horizontal", "vertical"],
description: "Orientation of the divider",
},
slot: {
control: false,
description: "Element to render in the divider slot",
},
position: {
control: { type: "select" },
options: ["start", "center", "end"],
description: "Position of the slot element",
},
padding: {
control: "number",
description: "Padding around the divider in pixels",
},
flexItem: {
control: "boolean",
description: "If true, adapts to flex container",
},
className: {
control: "text",
description: "Additional CSS classes to apply to the divider",
},
},
};

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

export const Horizontal: Story = {
render: (args) => (
<div className="w-80">
<p className="text-sm text-ink-gray-7">Content above</p>
<Divider {...args} />
<p className="text-sm text-ink-gray-7">Content below</p>
</div>
),
args: {
orientation: "horizontal",
padding: 16,
},
};

export const Vertical: Story = {
render: (args) => (
<div className="flex items-center h-16">
<span className="text-sm text-ink-gray-7">Left</span>
<Divider {...args} />
<span className="text-sm text-ink-gray-7">Right</span>
</div>
),
args: {
orientation: "vertical",
flexItem: true,
padding: 16,
},
};

export const WithSlot: Story = {
render: (args) => (
<div className="w-80">
<p className="text-sm text-ink-gray-7">Content above</p>
<Divider {...args} />
<p className="text-sm text-ink-gray-7">Content below</p>
</div>
),
args: {
orientation: "horizontal",
position: "center",
padding: 6,
slot: () => <Button label="Load More" size="sm" variant="outline" />,
},
};

export const SlotPositionsHorizontal: Story = {
render: () => (
<div className="flex flex-col gap-14 w-80">
<Divider
orientation="horizontal"
position="start"
slot={() => <Button label="Start" size="sm" variant="outline" />}
/>
<Divider
orientation="horizontal"
position="center"
slot={() => <Button label="Center" size="sm" variant="outline" />}
/>
<Divider
orientation="horizontal"
position="end"
slot={() => <Button label="End" size="sm" variant="outline" />}
/>
</div>
),
};

export const SlotPositionsVertical: Story = {
render: () => (
<div className="flex items-center gap-20 h-48">
<Divider
orientation="vertical"
position="start"
flexItem
slot={() => <Button label="Start" size="sm" variant="outline" />}
/>
<Divider
orientation="vertical"
position="center"
flexItem
slot={() => <Button label="Center" size="sm" variant="outline" />}
/>
<Divider
orientation="vertical"
position="end"
flexItem
slot={() => <Button label="End" size="sm" variant="outline" />}
/>
</div>
),
};

export const Timeline: Story = {
render: () => (
<div className="w-125 flex flex-col gap-8 items-center">
<Divider
position="start"
slot={() => <Badge label="Jun" size="lg" variant="outline" />}
/>
<Divider
position="start"
slot={() => (
<Badge label="Today 2 hours ago" size="lg" variant="outline" />
)}
/>
<Divider
position="center"
slot={() => (
<Button
label="Today"
size="sm"
variant="outline"
iconRight={() => <ChevronDown className="w-4 h-4" />}
/>
)}
/>
<Divider
position="start"
slot={() => <Badge label="New messages" size="lg" theme="blue" />}
/>
<div className="w-80 flex flex-col gap-8 items-center">
<Divider
position="start"
slot={() => <Button label="Continue" size="sm" variant="outline" />}
/>
<Divider
position="center"
slot={() => <Button icon="message-circle" variant="ghost" />}
/>
</div>
</div>
),
};
99 changes: 53 additions & 46 deletions packages/frappe-ui-react/src/components/divider/divider.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,72 @@
import { useMemo } from "react";
/**
* External dependencies.
*/
import clsx from "clsx";

import { Button } from "../button";
/**
* Internal dependencies.
*/
import type { DividerProps } from "./types";

const Divider = ({
orientation = "horizontal",
slot,
position = "center",
padding = 0,
flexItem = false,
action,
className = "",
}: DividerProps) => {
const alignmentClasses = useMemo(() => {
const spacerDimensionClasses = {
horizontal: "border-t w-full",
vertical: "border-l",
}[orientation];
const isHorizontal = orientation === "horizontal";
const flexClasses = flexItem ? "self-stretch h-auto" : "";

const flexClasses = flexItem ? "self-stretch h-auto" : "h-full";
if (!slot) {
return (
<hr
className={clsx(
"border-0 border-outline-gray-1",
isHorizontal ? "border-t w-full" : "border-l h-full",
flexClasses,
className
)}
style={
isHorizontal
? { marginTop: padding, marginBottom: padding }
: { marginLeft: padding, marginRight: padding }
}
/>
);
}

return [spacerDimensionClasses, flexClasses];
}, [orientation, flexItem]);

const actionAlignmentClasses = useMemo(() => {
return {
horizontal: {
center: "left-1/2 top-0 -translate-y-1/2 -translate-x-1/2",
start: "left-0 top-0 -translate-y-1/2 ml-4",
end: "right-0 top-0 -translate-y-1/2 mr-4",
},
vertical: {
center: "-translate-x-1/2 top-1/2 left-0 -translate-y-1/2",
start: "-translate-x-1/2 top-0 mt-4 left-0",
end: "-translate-x-1/2 bottom-0 mb-4 left-0",
},
}[orientation][position];
}, [orientation, position]);

return action ? (
return (
<div
className={clsx(
"relative whitespace-nowrap border-0 border-outline-gray-2",
alignmentClasses
"flex",
isHorizontal ? "w-full items-center" : "h-full flex-col items-center",
flexClasses,
className
)}
style={
isHorizontal
? { paddingTop: padding, paddingBottom: padding }
: { paddingLeft: padding, paddingRight: padding }
}
>
<span className={clsx("absolute", actionAlignmentClasses)}>
<Button
label={action.label}
loading={action.loading}
size="sm"
variant="outline"
onClick={action.handler}
/>
</span>
<hr
className={clsx(
"border-0 border-outline-gray-1",
isHorizontal ? "border-t" : "border-l",
position === "start" ? (isHorizontal ? "w-4" : "h-4") : "flex-1"
)}
/>
<span>{slot?.()}</span>
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The slot function is called without any arguments, but the type signature suggests it could accept args?: any. This is inconsistent with the type definition. Either remove the parameter from the type definition, or if arguments are needed in the future, pass them here consistently.

Suggested change
<span>{slot?.()}</span>
<span>{slot?.(undefined)}</span>

Copilot uses AI. Check for mistakes.
<hr
className={clsx(
"border-0 border-outline-gray-1",
isHorizontal ? "border-t" : "border-l",
position === "end" ? (isHorizontal ? "w-4" : "h-4") : "flex-1"
)}
/>
</div>
) : (
<hr
className={clsx(
"relative whitespace-nowrap border-0 border-outline-gray-2",
alignmentClasses
)}
/>
);
};

Expand Down
Loading
Loading