feat: Add paper-kit/Separator component
This commit is contained in:
2 files changed
+486
No files matched your search
@@ -0,0 +1,331 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Separator } from './Separator';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* A newspaper-style separator component for dividing content sections.
|
||||
*
|
||||
* Features:
|
||||
* - Single or double line variants inspired by newspaper layout traditions
|
||||
* - Multiple color options (black, outline, outline-light) with theme support
|
||||
* - Horizontal and vertical orientations
|
||||
* - Optional thinned gradient fade effect at line ends (10% fade length)
|
||||
* - 2px stroke width following print design standards
|
||||
* - Semantic div elements with proper ARIA separator role for accessibility
|
||||
*/
|
||||
const meta: Meta<typeof Separator> = {
|
||||
title: 'components/paper-kit/Separator',
|
||||
component: Separator,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'A newspaper-style separator following traditional print design patterns for content division.',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['single', 'double'],
|
||||
description: 'Type of separator line styling',
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['black', 'outline', 'outline-light'],
|
||||
description: 'Color variant following newspaper design patterns',
|
||||
},
|
||||
direction: {
|
||||
control: 'select',
|
||||
options: ['horizontal', 'vertical'],
|
||||
description: 'Orientation of the separator',
|
||||
},
|
||||
thinned: {
|
||||
control: 'boolean',
|
||||
description: 'Whether line ends should be thinned with gradient fade (10% fade length)',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
type: 'single',
|
||||
direction: 'horizontal',
|
||||
thinned: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
/**
|
||||
* Story type for Separator component stories.
|
||||
*/
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* The default single-line separator with light outline color.
|
||||
* Uses the newspaper-standard 2px stroke with thinned gradient ends.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-96 p-8">
|
||||
<div className="text-center text-sm text-muted-foreground mb-4">
|
||||
Content Above
|
||||
</div>
|
||||
<Story />
|
||||
<div className="text-center text-sm text-muted-foreground mt-4">
|
||||
Content Below
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Double-line separator with standard outline color.
|
||||
* Features 4px spacing between the two parallel lines.
|
||||
*/
|
||||
export const Double: Story = {
|
||||
args: {
|
||||
type: 'double',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-96 p-8">
|
||||
<div className="text-center text-sm text-muted-foreground mb-4">
|
||||
Content Above
|
||||
</div>
|
||||
<Story />
|
||||
<div className="text-center text-sm text-muted-foreground mt-4">
|
||||
Content Below
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Black color variant for high contrast content separation.
|
||||
* Most prominent separator option following newspaper header styling.
|
||||
*/
|
||||
export const Black: Story = {
|
||||
args: {
|
||||
color: 'black',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-96 p-8">
|
||||
<div className="text-center text-sm text-muted-foreground mb-4">
|
||||
Content Above
|
||||
</div>
|
||||
<Story />
|
||||
<div className="text-center text-sm text-muted-foreground mt-4">
|
||||
Content Below
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Light outline color for subtle content divisions.
|
||||
* Ideal for separating related content sections.
|
||||
*/
|
||||
export const OutlineLight: Story = {
|
||||
args: {
|
||||
color: 'outline-light',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-96 p-8">
|
||||
<div className="text-center text-sm text-muted-foreground mb-4">
|
||||
Content Above
|
||||
</div>
|
||||
<Story />
|
||||
<div className="text-center text-sm text-muted-foreground mt-4">
|
||||
Content Below
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Vertical separator for side-by-side content division.
|
||||
* Fills the height of its container.
|
||||
*/
|
||||
export const Vertical: Story = {
|
||||
args: {
|
||||
direction: 'vertical',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex h-48 p-8">
|
||||
<div className="flex-1 flex items-center justify-center text-sm text-muted-foreground">
|
||||
Left Content
|
||||
</div>
|
||||
<div className="px-4">
|
||||
<Story />
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center text-sm text-muted-foreground">
|
||||
Right Content
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Double vertical separator for enhanced visual separation.
|
||||
*/
|
||||
export const VerticalDouble: Story = {
|
||||
args: {
|
||||
type: 'double',
|
||||
direction: 'vertical',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex h-48 p-8">
|
||||
<div className="flex-1 flex items-center justify-center text-sm text-muted-foreground">
|
||||
Left Content
|
||||
</div>
|
||||
<div className="px-4">
|
||||
<Story />
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center text-sm text-muted-foreground">
|
||||
Right Content
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Separator without thinned ends showing clean, sharp line edges.
|
||||
* Compare with default to see the gradient fade difference.
|
||||
*/
|
||||
export const NonThinned: Story = {
|
||||
args: {
|
||||
thinned: false,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-96 p-8">
|
||||
<div className="text-center text-sm text-muted-foreground mb-4">
|
||||
Content Above (Sharp Edges)
|
||||
</div>
|
||||
<Story />
|
||||
<div className="text-center text-sm text-muted-foreground mt-4">
|
||||
Content Below (Sharp Edges)
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Interactive demonstration showing all color variants side by side.
|
||||
* Useful for comparing color intensity and theme compatibility.
|
||||
*/
|
||||
export const AllColors: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-8 w-96">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">Black</h3>
|
||||
<Separator color="black" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">Outline (Default for Double)</h3>
|
||||
<Separator color="outline" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">Outline Light (Default for Single)</h3>
|
||||
<Separator color="outline-light" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* Demonstration of all type variants with their default colors.
|
||||
* Shows the automatic color selection behavior.
|
||||
*/
|
||||
export const AllTypes: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-8 w-96">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">Single (Default: Outline Light)</h3>
|
||||
<Separator type="single" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">Double (Default: Outline)</h3>
|
||||
<Separator type="double" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* Comparison between thinned and non-thinned effects.
|
||||
* Demonstrates the 10% gradient fade at line ends.
|
||||
*/
|
||||
export const ThinnedComparison: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-8 w-96">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">With Thinned Ends (Default)</h3>
|
||||
<Separator thinned />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">Without Thinned Ends</h3>
|
||||
<Separator thinned={false} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* Comprehensive showcase of vertical separators in different contexts.
|
||||
* Demonstrates container height adaptation and spacing.
|
||||
*/
|
||||
export const VerticalShowcase: Story = {
|
||||
render: () => (
|
||||
<div className="flex space-x-8">
|
||||
<div className="flex h-32">
|
||||
<div className="flex items-center justify-center w-20 text-xs text-muted-foreground">
|
||||
Short
|
||||
</div>
|
||||
<div className="px-2">
|
||||
<Separator direction="vertical" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-20 text-xs text-muted-foreground">
|
||||
Content
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-48">
|
||||
<div className="flex items-center justify-center w-20 text-xs text-muted-foreground">
|
||||
Medium
|
||||
</div>
|
||||
<div className="px-2">
|
||||
<Separator direction="vertical" type="double" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-20 text-xs text-muted-foreground">
|
||||
Content
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-64">
|
||||
<div className="flex items-center justify-center w-20 text-xs text-muted-foreground">
|
||||
Tall
|
||||
</div>
|
||||
<div className="px-2">
|
||||
<Separator color="black" direction="vertical" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center w-20 text-xs text-muted-foreground">
|
||||
Content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import type { ClassNameProps, TestIdProps } from '@/shared/types';
|
||||
|
||||
import { VStack } from '@/components/shared/VStack/VStack';
|
||||
import { cn } from '@/shared/helpers';
|
||||
|
||||
/**
|
||||
* Props for the newspaper-style Separator component.
|
||||
*/
|
||||
export interface SeparatorProps extends ClassNameProps, TestIdProps {
|
||||
/** Type of separator line styling */
|
||||
type?: 'single' | 'double';
|
||||
/** Color variant following newspaper design patterns */
|
||||
color?: 'black' | 'outline' | 'outline-light';
|
||||
/** Orientation of the separator */
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
/** Whether line ends should be thinned with gradient fade */
|
||||
thinned?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A newspaper-style separator component for dividing content sections.
|
||||
*
|
||||
* Features:
|
||||
* - Single or double line variants inspired by newspaper layout traditions
|
||||
* - Multiple color options (black, outline, outline-light) with theme support
|
||||
* - Horizontal and vertical orientations
|
||||
* - Optional thinned gradient fade effect at line ends (10% fade length)
|
||||
* - 2px stroke width following print design standards
|
||||
* - Semantic HTML with proper ARIA attributes for accessibility
|
||||
*
|
||||
* The component uses CSS mask-image for the thinned effect and CSS custom properties
|
||||
* for theme-aware colors. Double separators have 4px spacing between lines.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Basic horizontal separator
|
||||
* <Separator />
|
||||
*
|
||||
* // Double-line separator with black color
|
||||
* <Separator type="double" color="black" />
|
||||
*
|
||||
* // Vertical separator without thinned ends
|
||||
* <Separator direction="vertical" thinned={false} />
|
||||
*
|
||||
* // Light separator for subtle content division
|
||||
* <Separator color="outline-light" />
|
||||
* ```
|
||||
*/
|
||||
export const Separator = memo(function Separator({
|
||||
type = 'single',
|
||||
color,
|
||||
direction = 'horizontal',
|
||||
thinned = true,
|
||||
className,
|
||||
'data-testid': testId,
|
||||
}: SeparatorProps) {
|
||||
// Default color based on type as specified
|
||||
const defaultColor = type === 'single' ? 'outline-light' : 'outline';
|
||||
const finalColor = color ?? defaultColor;
|
||||
|
||||
// Color mapping to CSS custom properties
|
||||
const colorClasses = {
|
||||
black: 'border-foreground',
|
||||
outline: 'border-border',
|
||||
'outline-light': 'border-muted',
|
||||
};
|
||||
|
||||
// Base line styles
|
||||
const lineBaseClasses = [
|
||||
'border-0',
|
||||
'border-solid',
|
||||
colorClasses[finalColor],
|
||||
// 2px stroke as specified
|
||||
direction === 'horizontal' ? 'border-t-2' : 'border-l-2',
|
||||
];
|
||||
|
||||
// Thinned effect using CSS mask
|
||||
const thinnedClasses = thinned ? [
|
||||
direction === 'horizontal'
|
||||
? '[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]'
|
||||
: '[mask-image:linear-gradient(to_bottom,transparent,black_10%,black_90%,transparent)]',
|
||||
] : [];
|
||||
|
||||
// Size classes - fill container by default
|
||||
const sizeClasses = direction === 'horizontal'
|
||||
? ['w-full', 'h-0'] // Full width, no height (border provides the visual)
|
||||
: ['h-full', 'w-0']; // Full height, no width (border provides the visual)
|
||||
|
||||
// Single line implementation
|
||||
if (type === 'single') {
|
||||
const lineClasses = cn(
|
||||
...lineBaseClasses,
|
||||
...thinnedClasses,
|
||||
...sizeClasses,
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-orientation={direction}
|
||||
className={lineClasses}
|
||||
data-testid={testId}
|
||||
role="separator"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Double line implementation
|
||||
const singleLineClasses = cn(
|
||||
...lineBaseClasses,
|
||||
...thinnedClasses,
|
||||
...sizeClasses
|
||||
);
|
||||
|
||||
const containerClasses = cn(
|
||||
direction === 'horizontal' ? 'w-full' : 'h-full',
|
||||
className
|
||||
);
|
||||
|
||||
if (direction === 'horizontal') {
|
||||
return (
|
||||
<div
|
||||
aria-orientation="horizontal"
|
||||
className={containerClasses}
|
||||
data-testid={testId}
|
||||
role="separator"
|
||||
>
|
||||
<VStack gap={1}>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={singleLineClasses}
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={singleLineClasses}
|
||||
/>
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
aria-orientation="vertical"
|
||||
className={cn('flex flex-row gap-1', containerClasses)}
|
||||
data-testid={testId}
|
||||
role="separator"
|
||||
>
|
||||
<div aria-hidden="true" className={singleLineClasses} />
|
||||
<div aria-hidden="true" className={singleLineClasses} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user