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