From 232fa1b3852270bfe58072550239ae767618ec43 Mon Sep 17 00:00:00 2001 From: manika-signoz Date: Mon, 25 May 2026 14:45:49 +0530 Subject: [PATCH 1/2] feat(divider): add divider component --- README.md | 1 + apps/docs/stories/divider.mdx | 30 ++++ apps/docs/stories/divider.stories.tsx | 162 ++++++++++++++++++ apps/docs/stories/intro.mdx | 1 + packages/ui/package.json | 10 ++ .../src/divider/divider.forward-ref.test.tsx | 14 ++ packages/ui/src/divider/divider.module.css | 66 +++++++ packages/ui/src/divider/divider.tsx | 41 +++++ packages/ui/src/divider/index.ts | 2 + packages/ui/src/index.ts | 1 + packages/ui/vite.config.ts | 1 + 11 files changed, 329 insertions(+) create mode 100644 apps/docs/stories/divider.mdx create mode 100644 apps/docs/stories/divider.stories.tsx create mode 100644 packages/ui/src/divider/divider.forward-ref.test.tsx create mode 100644 packages/ui/src/divider/divider.module.css create mode 100644 packages/ui/src/divider/divider.tsx create mode 100644 packages/ui/src/divider/index.ts diff --git a/README.md b/README.md index f8b97416..e84c0166 100644 --- a/README.md +++ b/README.md @@ -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'; diff --git a/apps/docs/stories/divider.mdx b/apps/docs/stories/divider.mdx new file mode 100644 index 00000000..467ec2a1 --- /dev/null +++ b/apps/docs/stories/divider.mdx @@ -0,0 +1,30 @@ +import { Meta, Controls, Primary } from '@storybook/addon-docs/blocks'; +import * as DividerStories from './divider.stories'; + + + +# 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 ( +
+

Content above

+ +

Content below

+
+ ); +} +``` + +This code above will render the following divider: + + + + diff --git a/apps/docs/stories/divider.stories.tsx b/apps/docs/stories/divider.stories.tsx new file mode 100644 index 00000000..51bb5546 --- /dev/null +++ b/apps/docs/stories/divider.stories.tsx @@ -0,0 +1,162 @@ +import { Divider } from '@signozhq/ui'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +const meta: Meta = { + 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; + +export const Playground: Story = { + args: { + type: 'horizontal', + dashed: false, + plain: false, + }, + render: (props) => ( +
+

Content above

+ +

Content below

+
+ ), +}; + +export const Horizontal: Story = { + parameters: { + docs: { + description: { + story: 'A simple horizontal divider separating content sections.', + }, + }, + }, + render: () => ( +
+

Section A

+ +

Section B

+ +

Section C

+
+ ), +}; + +export const Vertical: Story = { + parameters: { + docs: { + description: { + story: + 'A vertical divider for inline separation, commonly used in toolbars and action groups.', + }, + }, + }, + render: () => ( +
+ Edit + + Copy + + Delete +
+ ), +}; + +export const Dashed: Story = { + parameters: { + docs: { + description: { + story: 'Use the `dashed` prop for a dashed line style.', + }, + }, + }, + render: () => ( +
+

Solid (default)

+ +

Dashed

+ +

End

+
+ ), +}; + +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: () => ( +
+

Login with email

+ OR +

Login with SSO

+
+ ), +}; + +export const PlainText: Story = { + parameters: { + docs: { + description: { + story: 'The `plain` prop renders divider text with normal font weight instead of medium.', + }, + }, + }, + render: () => ( +
+ Default weight +
+ Plain weight +
+ ), +}; diff --git a/apps/docs/stories/intro.mdx b/apps/docs/stories/intro.mdx index df8970eb..e6265ce8 100644 --- a/apps/docs/stories/intro.mdx +++ b/apps/docs/stories/intro.mdx @@ -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'; diff --git a/packages/ui/package.json b/packages/ui/package.json index 6f4522a6..c907dda1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -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", diff --git a/packages/ui/src/divider/divider.forward-ref.test.tsx b/packages/ui/src/divider/divider.forward-ref.test.tsx new file mode 100644 index 00000000..e17e932f --- /dev/null +++ b/packages/ui/src/divider/divider.forward-ref.test.tsx @@ -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(); + render(); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + expect(ref.current).toHaveAttribute('data-slot', 'divider'); + }); +}); diff --git a/packages/ui/src/divider/divider.module.css b/packages/ui/src/divider/divider.module.css new file mode 100644 index 00000000..8e30c3c0 --- /dev/null +++ b/packages/ui/src/divider/divider.module.css @@ -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); +} diff --git a/packages/ui/src/divider/divider.tsx b/packages/ui/src/divider/divider.tsx new file mode 100644 index 00000000..8a487c3c --- /dev/null +++ b/packages/ui/src/divider/divider.tsx @@ -0,0 +1,41 @@ +import { forwardRef } from 'react'; +import { cn } from '../lib/utils.js'; +import styles from './divider.module.css'; + +export interface DividerProps + extends Pick, 'className' | 'children' | 'id' | 'style'> { + type?: 'horizontal' | 'vertical'; + dashed?: boolean; + plain?: boolean; + testId?: string; +} + +export const Divider = forwardRef( + ( + { 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 ( +
+ {hasChildren && {children}} +
+ ); + } +); +Divider.displayName = 'Divider'; diff --git a/packages/ui/src/divider/index.ts b/packages/ui/src/divider/index.ts new file mode 100644 index 00000000..2c3fdede --- /dev/null +++ b/packages/ui/src/divider/index.ts @@ -0,0 +1,2 @@ +export type * from './divider.js'; +export { Divider } from './divider.js'; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index b0cf178d..a824ece9 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -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'; diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index b0bbf88a..5eca5eff 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -16,6 +16,7 @@ const entries: Record = { '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', From d0587c31ffbeba292c3bbcd0626b639092c4b101 Mon Sep 17 00:00:00 2001 From: manika-signoz Date: Mon, 25 May 2026 16:57:17 +0530 Subject: [PATCH 2/2] chore: remove .css and move to .scss --- .../ui/src/divider/{divider.module.css => divider.module.scss} | 0 packages/ui/src/divider/divider.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/ui/src/divider/{divider.module.css => divider.module.scss} (100%) diff --git a/packages/ui/src/divider/divider.module.css b/packages/ui/src/divider/divider.module.scss similarity index 100% rename from packages/ui/src/divider/divider.module.css rename to packages/ui/src/divider/divider.module.scss diff --git a/packages/ui/src/divider/divider.tsx b/packages/ui/src/divider/divider.tsx index 8a487c3c..3ad1d382 100644 --- a/packages/ui/src/divider/divider.tsx +++ b/packages/ui/src/divider/divider.tsx @@ -1,6 +1,6 @@ import { forwardRef } from 'react'; import { cn } from '../lib/utils.js'; -import styles from './divider.module.css'; +import styles from './divider.module.scss'; export interface DividerProps extends Pick, 'className' | 'children' | 'id' | 'style'> {