feat: Add paper-kit/Separator component

This commit is contained in:
deytenit committed 2026-01-13 23:22:43 +03:00
1 parent 0a31031965
commit 72c6474dbb
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>
);
}
});