From 72c6474dbb77e5ae43f47f4fff19c5fd52a8a536 Mon Sep 17 00:00:00 2001 From: Vladimir Eremin Date: Tue, 13 Jan 2026 23:22:43 +0300 Subject: [PATCH] feat: Add paper-kit/Separator component --- .../paper-kit/Separator/Separator.stories.tsx | 331 ++++++++++++++++++ .../paper-kit/Separator/Separator.tsx | 155 ++++++++ 2 files changed, 486 insertions(+) create mode 100644 src/components/paper-kit/Separator/Separator.stories.tsx create mode 100644 src/components/paper-kit/Separator/Separator.tsx diff --git a/src/components/paper-kit/Separator/Separator.stories.tsx b/src/components/paper-kit/Separator/Separator.stories.tsx new file mode 100644 index 0000000..7dadd67 --- /dev/null +++ b/src/components/paper-kit/Separator/Separator.stories.tsx @@ -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 = { + 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; + +/** + * 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) => ( +
+
+ Content Above +
+ +
+ Content Below +
+
+ ), + ], +}; + +/** + * Double-line separator with standard outline color. + * Features 4px spacing between the two parallel lines. + */ +export const Double: Story = { + args: { + type: 'double', + }, + decorators: [ + (Story) => ( +
+
+ Content Above +
+ +
+ Content Below +
+
+ ), + ], +}; + +/** + * 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) => ( +
+
+ Content Above +
+ +
+ Content Below +
+
+ ), + ], +}; + +/** + * Light outline color for subtle content divisions. + * Ideal for separating related content sections. + */ +export const OutlineLight: Story = { + args: { + color: 'outline-light', + }, + decorators: [ + (Story) => ( +
+
+ Content Above +
+ +
+ Content Below +
+
+ ), + ], +}; + +/** + * Vertical separator for side-by-side content division. + * Fills the height of its container. + */ +export const Vertical: Story = { + args: { + direction: 'vertical', + }, + decorators: [ + (Story) => ( +
+
+ Left Content +
+
+ +
+
+ Right Content +
+
+ ), + ], +}; + +/** + * Double vertical separator for enhanced visual separation. + */ +export const VerticalDouble: Story = { + args: { + type: 'double', + direction: 'vertical', + }, + decorators: [ + (Story) => ( +
+
+ Left Content +
+
+ +
+
+ Right Content +
+
+ ), + ], +}; + +/** + * 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) => ( +
+
+ Content Above (Sharp Edges) +
+ +
+ Content Below (Sharp Edges) +
+
+ ), + ], +}; + +/** + * Interactive demonstration showing all color variants side by side. + * Useful for comparing color intensity and theme compatibility. + */ +export const AllColors: Story = { + render: () => ( +
+
+

Black

+ +
+
+

Outline (Default for Double)

+ +
+
+

Outline Light (Default for Single)

+ +
+
+ ), +}; + +/** + * Demonstration of all type variants with their default colors. + * Shows the automatic color selection behavior. + */ +export const AllTypes: Story = { + render: () => ( +
+
+

Single (Default: Outline Light)

+ +
+
+

Double (Default: Outline)

+ +
+
+ ), +}; + +/** + * Comparison between thinned and non-thinned effects. + * Demonstrates the 10% gradient fade at line ends. + */ +export const ThinnedComparison: Story = { + render: () => ( +
+
+

With Thinned Ends (Default)

+ +
+
+

Without Thinned Ends

+ +
+
+ ), +}; + +/** + * Comprehensive showcase of vertical separators in different contexts. + * Demonstrates container height adaptation and spacing. + */ +export const VerticalShowcase: Story = { + render: () => ( +
+
+
+ Short +
+
+ +
+
+ Content +
+
+ +
+
+ Medium +
+
+ +
+
+ Content +
+
+ +
+
+ Tall +
+
+ +
+
+ Content +
+
+
+ ), +}; diff --git a/src/components/paper-kit/Separator/Separator.tsx b/src/components/paper-kit/Separator/Separator.tsx new file mode 100644 index 0000000..fdd19e6 --- /dev/null +++ b/src/components/paper-kit/Separator/Separator.tsx @@ -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 + * + * + * // Double-line separator with black color + * + * + * // Vertical separator without thinned ends + * + * + * // Light separator for subtle content division + * + * ``` + */ +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 ( +
+ ); + } + + // Double line implementation + const singleLineClasses = cn( + ...lineBaseClasses, + ...thinnedClasses, + ...sizeClasses + ); + + const containerClasses = cn( + direction === 'horizontal' ? 'w-full' : 'h-full', + className + ); + + if (direction === 'horizontal') { + return ( +
+ +