Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { Combobox } from '@signozhq/ui';
import { Command } from '@signozhq/ui';
import { DatePicker } from '@signozhq/ui';
import { Dialog } from '@signozhq/ui';
import { Divider } from '@signozhq/ui';
import { Drawer } from '@signozhq/ui';
import { DropdownMenu } from '@signozhq/ui';
import { Input } from '@signozhq/ui';
Expand Down
30 changes: 30 additions & 0 deletions apps/docs/stories/divider.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Meta, Controls, Primary } from '@storybook/addon-docs/blocks';
import * as DividerStories from './divider.stories';

<Meta of={DividerStories} />

# Divider

The divider component is a separator for visually dividing content. It supports horizontal and vertical orientations, dashed lines, and optional text labels.

## How to use

```tsx
import { Divider } from '@signozhq/ui';

export default function MyComponent() {
return (
<div>
<p>Content above</p>
<Divider />
<p>Content below</p>
</div>
);
}
```

This code above will render the following divider:

<Primary />

<Controls of={DividerStories.Playground} />
162 changes: 162 additions & 0 deletions apps/docs/stories/divider.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Divider } from '@signozhq/ui';
import type { Meta, StoryObj } from '@storybook/react-vite';

const meta: Meta<typeof Divider> = {
title: 'Components/Divider',
component: Divider,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component:
'A separator component for visually dividing content. Supports horizontal and vertical orientations, dashed lines, and optional text labels between the lines.',
},
},
},
argTypes: {
type: {
control: 'inline-radio',
options: ['horizontal', 'vertical'],
description: 'The orientation of the divider.',
table: { category: 'Appearance', defaultValue: { summary: 'horizontal' } },
},
dashed: {
control: 'boolean',
description: 'Whether the divider line is dashed.',
table: { category: 'Appearance', defaultValue: { summary: 'false' } },
},
plain: {
control: 'boolean',
description:
'Use plain style for the divider text (normal font weight instead of bold). Only relevant when children are provided.',
table: { category: 'Appearance', defaultValue: { summary: 'false' } },
},
children: {
control: 'text',
description: 'Optional text to display within a horizontal divider.',
table: { category: 'Content' },
},
testId: {
control: 'text',
description: 'Test ID for the divider.',
table: { category: 'Testing', type: { summary: 'string' } },
},
className: {
control: 'text',
description: 'Additional CSS classes.',
table: { category: 'Styling', type: { summary: 'string' } },
},
},
};

export default meta;

type Story = StoryObj<typeof Divider>;

export const Playground: Story = {
args: {
type: 'horizontal',
dashed: false,
plain: false,
},
render: (props) => (
<div style={{ width: '100%', padding: '1rem' }}>
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">Content above</p>
<Divider {...props} />
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">Content below</p>
</div>
),
};

export const Horizontal: Story = {
parameters: {
docs: {
description: {
story: 'A simple horizontal divider separating content sections.',
},
},
},
render: () => (
<div style={{ width: '100%', padding: '1rem' }}>
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">Section A</p>
<Divider />
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">Section B</p>
<Divider />
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">Section C</p>
</div>
),
};

export const Vertical: Story = {
parameters: {
docs: {
description: {
story:
'A vertical divider for inline separation, commonly used in toolbars and action groups.',
},
},
},
render: () => (
<div className="flex items-center gap-2" style={{ padding: '1rem' }}>
<span className="text-sm text-vanilla-800 dark:text-vanilla-200">Edit</span>
<Divider type="vertical" />
<span className="text-sm text-vanilla-800 dark:text-vanilla-200">Copy</span>
<Divider type="vertical" />
<span className="text-sm text-vanilla-800 dark:text-vanilla-200">Delete</span>
</div>
),
};

export const Dashed: Story = {
parameters: {
docs: {
description: {
story: 'Use the `dashed` prop for a dashed line style.',
},
},
},
render: () => (
<div style={{ width: '100%', padding: '1rem' }}>
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">Solid (default)</p>
<Divider />
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">Dashed</p>
<Divider dashed />
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">End</p>
</div>
),
};

export const WithText: Story = {
parameters: {
docs: {
description: {
story:
'Pass children to render text between the divider lines. Useful for "OR" separators and section labels.',
},
},
},
render: () => (
<div style={{ width: '100%', padding: '1rem' }}>
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">Login with email</p>
<Divider>OR</Divider>
<p className="text-sm text-vanilla-800 dark:text-vanilla-200">Login with SSO</p>
</div>
),
};

export const PlainText: Story = {
parameters: {
docs: {
description: {
story: 'The `plain` prop renders divider text with normal font weight instead of medium.',
},
},
},
render: () => (
<div style={{ width: '100%', padding: '1rem' }}>
<Divider>Default weight</Divider>
<div style={{ height: '1rem' }} />
<Divider plain>Plain weight</Divider>
</div>
),
};
1 change: 1 addition & 0 deletions apps/docs/stories/intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { Combobox } from '@signozhq/ui';
import { Command } from '@signozhq/ui';
import { DatePicker } from '@signozhq/ui';
import { Dialog } from '@signozhq/ui';
import { Divider } from '@signozhq/ui';
import { Drawer } from '@signozhq/ui';
import { DropdownMenu } from '@signozhq/ui';
import { Input } from '@signozhq/ui';
Expand Down
10 changes: 10 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@
"require": "./dist/dialog/index.cjs"
}
},
"./divider": {
"import": {
"types": "./dist/divider/index.d.ts",
"default": "./dist/divider/index.mjs"
},
"require": {
"types": "./dist/divider/index.d.cts",
"default": "./dist/divider/index.cjs"
}
},
"./drawer": {
"import": {
"types": "./dist/drawer/index.d.ts",
Expand Down
14 changes: 14 additions & 0 deletions packages/ui/src/divider/divider.forward-ref.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { render } from '@testing-library/react';
import { createRef } from 'react';
import { describe, expect, it } from 'vitest';

import { Divider } from './index.js';

describe('Divider forwardRef', () => {
it('forwards ref', () => {
const ref = createRef<HTMLDivElement>();
render(<Divider ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLDivElement);
expect(ref.current).toHaveAttribute('data-slot', 'divider');
});
});
66 changes: 66 additions & 0 deletions packages/ui/src/divider/divider.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.divider {
margin: var(--divider-margin, 0);
padding: 0;
border: none;
font-variant: none;
flex-shrink: 0;
}

/* Horizontal */
.divider[data-type="horizontal"] {
display: flex;
align-items: center;
width: 100%;
min-width: 100%;
border-block-start: var(--divider-border-width, 1px) solid var(--divider-color, var(--l2-border, #e5e7eb));
}

.divider[data-type="horizontal"][data-dashed] {
border-block-start-style: dashed;
}

/* Horizontal with text */
.divider[data-type="horizontal"][data-with-text] {
border-block-start: none;
gap: var(--divider-text-gap, 1em);
}

.divider[data-type="horizontal"][data-with-text]::before,
.divider[data-type="horizontal"][data-with-text]::after {
content: '';
flex: 1;
border-block-start: var(--divider-border-width, 1px) solid var(--divider-color, var(--l2-border, #e5e7eb));
}

.divider[data-type="horizontal"][data-with-text][data-dashed]::before,
.divider[data-type="horizontal"][data-with-text][data-dashed]::after {
border-block-start-style: dashed;
}

/* Vertical */
.divider[data-type="vertical"] {
display: inline-block;
position: relative;
top: -0.06em;
vertical-align: middle;
height: 0.9em;
margin-inline: var(--divider-vertical-margin, 8px);
border-inline-start: var(--divider-border-width, 1px) solid var(--divider-color, var(--l2-border, #e5e7eb));
}

.divider[data-type="vertical"][data-dashed] {
border-inline-start-style: dashed;
}

/* Text styling */
.text {
display: inline-block;
font-weight: var(--divider-text-font-weight, 500);
font-size: var(--divider-text-font-size, 14px);
white-space: nowrap;
color: var(--divider-text-color, var(--l2-foreground, inherit));
}

.divider[data-plain] .text {
font-weight: var(--divider-plain-text-font-weight, 400);
}
41 changes: 41 additions & 0 deletions packages/ui/src/divider/divider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { forwardRef } from 'react';
import { cn } from '../lib/utils.js';
import styles from './divider.module.scss';

export interface DividerProps
extends Pick<React.ComponentProps<'div'>, 'className' | 'children' | 'id' | 'style'> {
type?: 'horizontal' | 'vertical';
dashed?: boolean;
plain?: boolean;
testId?: string;
}

export const Divider = forwardRef<HTMLDivElement, DividerProps>(
(
{ className, type = 'horizontal', dashed = false, plain = false, testId, children, ...props },
ref
) => {
const hasChildren = children != null;
const separatorProps = hasChildren
? {}
: ({ role: 'separator', 'aria-orientation': type } as const);

return (
<div
ref={ref}
{...separatorProps}
data-slot="divider"
data-type={type}
data-dashed={dashed || undefined}
data-plain={plain || undefined}
data-with-text={hasChildren || undefined}
data-testid={testId}
className={cn(styles.divider, className)}
{...props}
>
{hasChildren && <span className={styles.text}>{children}</span>}
</div>
);
}
);
Divider.displayName = 'Divider';
2 changes: 2 additions & 0 deletions packages/ui/src/divider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type * from './divider.js';
export { Divider } from './divider.js';
1 change: 1 addition & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './combobox/index.js';
export * from './command/index.js';
export * from './date-picker/index.js';
export * from './dialog/index.js';
export * from './divider/index.js';
export * from './drawer/index.js';
export * from './dropdown-menu/index.js';
export * from './input/index.js';
Expand Down
1 change: 1 addition & 0 deletions packages/ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const entries: Record<string, string> = {
'command/index': 'src/command/index.ts',
'date-picker/index': 'src/date-picker/index.ts',
'dialog/index': 'src/dialog/index.ts',
'divider/index': 'src/divider/index.ts',
'drawer/index': 'src/drawer/index.ts',
'dropdown-menu/index': 'src/dropdown-menu/index.ts',
'input/index': 'src/input/index.ts',
Expand Down
Loading