← All proposals

LabeledDivider

Net-new LabeledDivider: a horizontal rule with a pill-shaped label centered on it.

Net-new componentOpen in Storybook

proposals/LabeledDivider/LabeledDivider.stories.tsx209 lines · View on GitHub
import { useState } from 'react';

import { Meta, StoryObj } from '@storybook/react-vite';

import { Button } from '../_base/Button/Button';

import { LabeledDivider } from './LabeledDivider';

import './labeled-divider-story.css';

const timeMenuItems = [
	{ label: 'Jump to...', isHeading: true },
	{ label: 'Today', callback: () => {} },
	{ label: 'Yesterday', callback: () => {} },
	{ label: 'Last week', callback: () => {} },
	{ label: 'Last month', callback: () => {} },
	{ label: 'The very beginning', callback: () => {} },
];

const datePicker = {
	maxDate: new Date(),
	menuLabel: 'Jump to a specific date',
	modalTitle: 'Jump to a date',
	onChange: () => {},
	pickerLabel: 'Date',
};

const meta: Meta<typeof LabeledDivider> = {
	tags: ['new-pattern', 'ready-for-dev'],
	title: 'Component Proposals/Labeled Divider',
	component: LabeledDivider,
	parameters: {
		docs: {
			description: {
				component:
					'**New pattern:** not in PDS v2 today. A horizontal rule with a pill label centered on it, to mark a point in an event log, a thread or a timeline: a date, a jump in time or a state change.',
			},
		},
	},
	argTypes: {
		className: { table: { category: 'Base attributes' } },
		datePicker: { control: false, table: { category: 'Content' } },
		id: { table: { category: 'Base attributes' } },
		label: { control: 'text', table: { category: 'Content' } },
		menuItems: { control: false, table: { category: 'Content' } },
		prefixIcon: {
			control: 'select',
			options: [undefined, 'circle', 'zappy', 'sparklesSolid'],
			table: { category: 'Content' },
		},
		size: {
			control: 'select',
			options: ['xs', 's'],
			table: { category: 'Component configuration' },
		},
		suffixIcon: {
			control: 'select',
			options: [undefined, 'circle', 'zappy', 'sparklesSolid'],
			table: { category: 'Content' },
		},
		tone: {
			control: 'select',
			options: ['neutral', 'agentic'],
			table: { category: 'Component configuration' },
		},
	},
	args: {
		label: 'Text',
		prefixIcon: 'circle',
		suffixIcon: 'circle',
		size: 's',
		tone: 'neutral',
	},
};

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const WithTimeSelector: Story = {
	args: {
		id: 'labeled-divider-time',
		label: 'Yesterday',
		datePicker,
		menuItems: timeMenuItems,
		prefixIcon: undefined,
		suffixIcon: undefined,
	},
	parameters: {
		docs: {
			description: {
				story:
					'Pass `menuItems` (and an `id`) to turn the label into a selector with a chevron, for jumping to another point in time. The consumer supplies the options and what each one does.',
			},
		},
	},
};

export const WithAction: Story = {
	args: {
		label: 'Show more',
	},
	parameters: {
		docs: {
			description: {
				story:
					'Pass `action` to swap the label for any control, centered on the line. Here it is a "Show more" Button that unfolds more content under the divider. The screen owns the control, `aria-expanded` and the content it reveals; the divider only places it. `tone` still colors the lines.',
			},
		},
	},
	render: (args) => {
		const [isOpen, setIsOpen] = useState(false);
		const events = [
			'Updated the build cache',
			'Ran a dry run of the deployment',
			'Checked the dependency versions',
		];

		return (
			<div className='labeled-divider-story'>
				<LabeledDivider
					{...args}
					action={
						<Button
							aria-controls='labeled-divider-more'
							aria-expanded={isOpen}
							displayType='icon-end'
							iconName={isOpen ? 'angleUp' : 'angleDown'}
							label={isOpen ? 'Show less' : 'Show 3 more'}
							onClick={() => setIsOpen((open) => !open)}
							size='xs'
							variant='secondary'
						/>
					}
				/>
				<ul
					className='labeled-divider-story__more'
					hidden={!isOpen}
					id='labeled-divider-more'
				>
					{events.map((event) => (
						<li key={event}>{event}</li>
					))}
				</ul>
			</div>
		);
	},
};

export const Agentic: Story = {
	render: (args) => (
		<div className='labeled-divider-story'>
			<LabeledDivider
				{...args}
				label='Zappy engaged'
				prefixIcon='zappy'
				suffixIcon={undefined}
				tone='agentic'
			/>
		</div>
	),
	parameters: {
		docs: {
			description: {
				story:
					'The `agentic` tone: the AI gradient as the label border and on the lines, with the shimmer, and the Zappy bolt in the plain foreground color. Use for a state change in the stream, such as an agent joining.',
			},
		},
	},
};

export const AllSizesAndVariants: Story = {
	name: 'All sizes and variants',
	render: (args) => (
		<div className='labeled-divider-story'>
			{(['s', 'xs'] as const).flatMap((size) => [
				<LabeledDivider
					key={`${size}-neutral`}
					{...args}
					label={`Neutral (${size})`}
					size={size}
				/>,
				<LabeledDivider
					key={`${size}-selector`}
					{...args}
					datePicker={datePicker}
					id={`labeled-divider-sizes-${size}`}
					label={`Time selector (${size})`}
					menuItems={timeMenuItems}
					prefixIcon={undefined}
					size={size}
					suffixIcon={undefined}
				/>,
				<LabeledDivider
					key={`${size}-agentic`}
					{...args}
					label={`Agentic (${size})`}
					prefixIcon='zappy'
					size={size}
					suffixIcon={undefined}
					tone='agentic'
				/>,
			])}
		</div>
	),
};