diff --git a/package-lock.json b/package-lock.json index 1604a4df..2a4e27d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@mui/material": "^5.15.13", "@mui/x-date-pickers": "^5.0.20", "@tanstack/react-table": "^8.13.2", - "@tedi-design-system/core": "3.0.1", + "@tedi-design-system/core": "3.2.0", "classnames": "^2.5.1", "draft-js": "^0.11.7", "draftjs-md-converter": "^1.5.2", @@ -7979,9 +7979,9 @@ } }, "node_modules/@tedi-design-system/core": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tedi-design-system/core/-/core-3.0.1.tgz", - "integrity": "sha512-ioet8RlFmWjg8fic4WUuYeavLiqUsKx3vFGZzzXkL91xNNjHexNVKhhtMLLkpCywzOc2tKXMx3AYdDhu2dsbwg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tedi-design-system/core/-/core-3.2.0.tgz", + "integrity": "sha512-R9gpmprRT8qCGeJ8Frhz2yiJQ9bJM1Yp3mvWHeX2a6600hWh3mKXvhARSLFAF6im83FEFmOoKwdIOr4nfy8Qbg==", "engines": { "node": ">=18.0.0", "npm": ">=8.0.0" diff --git a/package.json b/package.json index 6fcde260..3fba459c 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@mui/material": "^5.15.13", "@mui/x-date-pickers": "^5.0.20", "@tanstack/react-table": "^8.13.2", - "@tedi-design-system/core": "3.0.1", + "@tedi-design-system/core": "3.2.0", "classnames": "^2.5.1", "draft-js": "^0.11.7", "draftjs-md-converter": "^1.5.2", diff --git a/src/community/components/map-components/left-panel/left-panel-footer.tsx b/src/community/components/map-components/left-panel/left-panel-footer.tsx index 526f38fc..f3d725cb 100644 --- a/src/community/components/map-components/left-panel/left-panel-footer.tsx +++ b/src/community/components/map-components/left-panel/left-panel-footer.tsx @@ -7,7 +7,7 @@ const LeftPanelFooter = () => {
Tume režiim} /> - +
diff --git a/src/tedi/components/cards/card/card.stories.tsx b/src/tedi/components/cards/card/card.stories.tsx index 4d2c39ab..48ddc9f8 100644 --- a/src/tedi/components/cards/card/card.stories.tsx +++ b/src/tedi/components/cards/card/card.stories.tsx @@ -283,7 +283,7 @@ const Timeline: StoryFn = (args) => (

Card content

- +

Card content

diff --git a/src/tedi/components/misc/separator/separator.module.scss b/src/tedi/components/misc/separator/separator.module.scss index 0e17b456..6d648458 100644 --- a/src/tedi/components/misc/separator/separator.module.scss +++ b/src/tedi/components/misc/separator/separator.module.scss @@ -13,6 +13,15 @@ $sizes: ( '2-5': 2.5rem, '5': 5rem, ); +$thicknesses: ( + '1': 1px, + '2': 2px, +); +$dot-colors: ( + 'primary': var(--general-border-primary), + 'secondary': var(--general-border-secondary), + 'accent': var(--general-border-accent), +); .tedi-separator { --vertical-separator-height: 100%; @@ -30,12 +39,8 @@ $sizes: ( border-left: 1px solid var(--general-border-primary); } - &--secondary { - border-color: var(--general-border-secondary); - } - - &--accent { - border-color: var(--general-border-accent); + &--horizontal { + display: block; } &--block { @@ -45,84 +50,38 @@ $sizes: ( &--inline { display: inline; } -} - -.tedi-separator--dotted, -.tedi-separator--dotted-small { - &::before { - position: absolute; - top: 1.25rem; - width: var(--separator-dotted-dot-lg); - height: var(--separator-dotted-dot-lg); - content: ''; - background-color: var(--general-border-primary); - border-radius: 100%; - transform: translateX(-8px); - - @include mixins.print-grayscale; - } - &.tedi-separator--secondary::before { - background-color: var(--general-border-secondary); + &--secondary { + border-color: var(--general-border-secondary); } - &.tedi-separator--accent::before { - background-color: var(--general-border-accent); + &--accent { + border-color: var(--general-border-accent); } -} -.tedi-separator--dotted-small::before { - top: 1.5rem; - width: var(--separator-dotted-dot-md); - height: var(--separator-dotted-dot-md); - transform: translateX(-5px); -} - -.tedi-separator--is-stretched { - margin-right: calc(var(--card-content-padding-right) * -1); - margin-left: calc(var(--card-content-padding-left) * -1); + &--is-stretched { + margin-right: calc(var(--card-content-padding-right) * -1); + margin-left: calc(var(--card-content-padding-left) * -1); - &.tedi-separator--vertical { - height: calc(100% + (var(--card-content-padding-top) + var(--card-content-padding-bottom))); - margin: calc(var(--card-content-padding-top) * -1) 0 calc(var(--card-content-padding-bottom) * -1); + &.tedi-separator--vertical { + height: calc(100% + (var(--card-content-padding-top) + var(--card-content-padding-bottom))); + margin: calc(var(--card-content-padding-top) * -1) 0 calc(var(--card-content-padding-bottom) * -1); + } } } -@each $size, $value in $sizes { - .tedi-separator--horizontal.tedi-separator--top-#{$size} { - margin-top: #{$value}; - } - - .tedi-separator--horizontal.tedi-separator--bottom-#{$size} { - margin-bottom: #{$value}; - } - - .tedi-separator--horizontal.tedi-separator--spacing-#{$size}:not(.tedi-separator--dot-only) { - margin-top: #{$value}; - margin-bottom: #{$value}; - } +.tedi-separator--dotted { + &::before { + position: absolute; + content: ''; + background-color: var(--general-border-primary); + border-radius: 50%; - .tedi-separator--horizontal.tedi-separator--dot-only.tedi-separator--spacing-#{$size}, - .tedi-separator--vertical.tedi-separator--spacing-#{$size} { - margin-right: #{$value}; - margin-left: #{$value}; + @include mixins.print-grayscale; } -} - -$thicknesses: ( - '1': 1px, - '2': 2px, -); -@each $thickness, $value in $thicknesses { - .tedi-separator { - &.tedi-separator--thickness-#{$thickness} { - border-top-width: #{$value}; - } - - &--vertical.tedi-separator--thickness-#{$thickness} { - border-left-width: #{$value}; - } + &.tedi-separator--vertical { + min-height: 3rem; } } @@ -138,58 +97,139 @@ $thicknesses: ( width: var(--separator-dotted-dot-sm); height: var(--separator-dotted-dot-sm); content: ''; - border-radius: 100%; + border-radius: 50%; @include mixins.print-grayscale; } - &.tedi-separator--dot-only-extra-small::before { - width: var(--separator-dotted-dot-xs); - height: var(--separator-dotted-dot-xs); + &.tedi-separator--dot-style-outlined::before { + background: transparent; } +} - &.tedi-separator--dot-only-small::before { - width: var(--separator-dotted-dot-sm); - height: var(--separator-dotted-dot-sm); +@each $name, $color in $dot-colors { + .tedi-separator--#{$name} { + border-color: $color; + + &::before { + background-color: $color; + } + + &.tedi-separator--dot-style-outlined:not(.tedi-separator--dot-only)::before { + border-color: $color; + } } +} - &.tedi-separator--dot-only-medium::before { - width: var(--separator-dotted-dot-md); - height: var(--separator-dotted-dot-md); +@each $size, $var in (extra-small: xs, small: sm, medium: md, large: lg) { + .tedi-separator--dot-only-#{$size}::before, + .tedi-separator--dotted-#{$size}::before, + .tedi-separator--dot-style-outlined.tedi-separator--dotted-#{$size}::before { + width: var(--separator-dot-size-#{$var}); + height: var(--separator-dot-size-#{$var}); } +} - &.tedi-separator--dot-only-large::before { - width: var(--separator-dotted-dot-lg); - height: var(--separator-dotted-dot-lg); +@each $axis in (horizontal, vertical) { + @each $pos in (start, center, end) { + .tedi-separator--#{$axis}.tedi-separator--dot-position-#{$pos}::before { + @if $axis == horizontal { + @if $pos == start { + right: auto; + left: 0; + } @else if $pos == center { + right: 0; + left: 0; + margin: auto; + } @else { + right: 0; + left: auto; + } + } @else { + @if $pos == start { + top: 0; + bottom: auto; + } @else if $pos == center { + top: 0; + bottom: 0; + margin: auto 0; + } @else { + top: auto; + bottom: 0; + } + } + } } +} - &.tedi-separator--primary::before { - background-color: var(--general-border-primary); +.tedi-separator--horizontal.tedi-separator--dot-position-custom::before { + right: auto; + left: var(--separator-dot-position); +} + +.tedi-separator--vertical.tedi-separator--dot-position-custom::before { + top: var(--separator-dot-position); + bottom: auto; +} + +@each $size, $value in $sizes { + .tedi-separator--spacing-#{$size}:not(.tedi-separator--dot-only) { + margin: #{$value} 0; + } + + .tedi-separator--dot-only.tedi-separator--spacing-#{$size} { + &.tedi-separator--horizontal { + margin-right: $value; + margin-left: $value; + } + + &.tedi-separator--vertical { + margin-top: $value; + margin-bottom: $value; + } } - &.tedi-separator--secondary::before { - background-color: var(--general-border-secondary); + @each $side in (top, bottom, left, right) { + .tedi-separator--#{$side}-#{$size} { + margin-#{$side}: $value; + } } +} - &.tedi-separator--accent::before { - background-color: var(--general-border-accent); +@each $thickness, $value in $thicknesses { + .tedi-separator--thickness-#{$thickness} { + border-top-width: $value; + } + .tedi-separator--vertical.tedi-separator--thickness-#{$thickness} { + border-left-width: $value; } } -.tedi-separator--horizontal { - &.tedi-separator--dotted::before, - &.tedi-separator--dotted-small::before { - right: 0; - left: 0; - margin: 0 auto; - transform: initial; +@each $axis in (horizontal, vertical) { + @each $size, $var in (extra-small: xs, small: sm, medium: md, large: lg) { + .tedi-separator--#{$axis}.tedi-separator--dotted-#{$size}::before, + .tedi-separator--#{$axis}.tedi-separator--dot-style-outlined.tedi-separator--dotted-#{$size}::before { + @if $axis == horizontal { + top: calc((var(--separator-dot-size-#{$var}) / 2 + var(--separator-thickness)) * -1); + } @else { + left: calc((var(--separator-dot-size-#{$var}) / 2 + var(--separator-thickness) / 2) * -1); + } + } } +} - &.tedi-separator--dotted::before { - top: calc(var(--separator-dotted-dot-lg) / 2 * -1); +.tedi-separator--dot-style-outlined { + &::before { + background-color: var(--general-surface-primary); + border-style: solid; + border-width: var(--separator-thickness); } - &.tedi-separator--dotted-small::before { - top: calc(var(--separator-dotted-dot-md) / 2 * -1); + @each $name, $color in $dot-colors { + &.tedi-separator--#{$name}::before, + &.tedi-separator--dotted-#{$name}::before { + z-index: 10; + border-color: $color; + } } } diff --git a/src/tedi/components/misc/separator/separator.spec.tsx b/src/tedi/components/misc/separator/separator.spec.tsx index 65d9c7e2..694961d9 100644 --- a/src/tedi/components/misc/separator/separator.spec.tsx +++ b/src/tedi/components/misc/separator/separator.spec.tsx @@ -17,132 +17,141 @@ describe('Separator Component', () => { }); }); - const renderComponent = (props: SeparatorProps) => render(); + const renderComponent =

(props: P) => + render(); - it('should render a div element by default', () => { - const { getByTestId } = renderComponent({}); + it('renders a div by default', () => { + const { getByTestId } = renderComponent({ element: 'div' }); expect(getByTestId('separator').tagName).toBe('DIV'); }); - it('should render the specified element', () => { + it('renders the specified element', () => { const { getByTestId } = renderComponent({ element: 'hr' }); expect(getByTestId('separator').tagName).toBe('HR'); }); - it('should apply default classes', () => { - const { getByTestId } = renderComponent({}); - const separator = getByTestId('separator'); - expect(separator).toHaveClass(styles['tedi-separator']); - expect(separator).toHaveClass(styles['tedi-separator--thickness-1']); - expect(separator).toHaveClass(styles['tedi-separator--horizontal']); - }); - - it('should apply additional classes from className prop', () => { - const { getByTestId } = renderComponent({ className: 'custom-class' }); - expect(getByTestId('separator')).toHaveClass('custom-class'); - }); - - it('should apply correct spacing class', () => { - const { getByTestId } = renderComponent({ spacing: 1.25 }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--spacing-1-25']); - }); + it('applies base and default classes', () => { + const { getByTestId } = renderComponent({ axis: 'horizontal' }); + const el = getByTestId('separator'); - it('should apply correct top and bottom spacing classes', () => { - const { getByTestId } = renderComponent({ topSpacing: 0.5, bottomSpacing: 1 }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--top-0-5']); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--bottom-1']); + expect(el).toHaveClass(styles['tedi-separator']); + expect(el).toHaveClass(styles['tedi-separator--horizontal']); + expect(el).toHaveClass(styles['tedi-separator--thickness-1']); }); - it('should apply axis specific class', () => { - const { getByTestId } = renderComponent({ axis: 'vertical' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--vertical']); + it('applies custom className', () => { + const { getByTestId } = renderComponent({ className: 'my-extra-class' }); + expect(getByTestId('separator')).toHaveClass('my-extra-class'); }); - it('should apply color class', () => { + it('applies color modifier', () => { const { getByTestId } = renderComponent({ color: 'accent' }); expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--accent']); }); - it('should apply variant class', () => { - const { getByTestId } = renderComponent({ variant: 'dotted' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dotted']); + it('applies vertical axis class and allows height', () => { + const { getByTestId } = renderComponent({ axis: 'vertical', height: 5.5 }); + const el = getByTestId('separator'); + + expect(el).toHaveClass(styles['tedi-separator--vertical']); + expect(el).toHaveStyle('--vertical-separator-height: 5.5rem'); }); - it('should apply thickness class when no variant is used', () => { - const { getByTestId } = renderComponent({ thickness: 2 }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--thickness-2']); + it('applies spacing classes — number value', () => { + const { getByTestId } = renderComponent({ spacing: 1.5 }); + const el = getByTestId('separator'); + + expect(el).toHaveClass(styles['tedi-separator--top-1-5']); + expect(el).toHaveClass(styles['tedi-separator--bottom-1-5']); }); - it('should not apply thickness class when variant is used', () => { - const { getByTestId } = renderComponent({ variant: 'dotted', thickness: 2 }); - expect(getByTestId('separator')).not.toHaveClass(styles['tedi-separator--thickness-2']); + it('applies spacing classes — object value', () => { + const { getByTestId } = renderComponent({ + spacing: { top: 2, bottom: 0.5, left: 1 }, + axis: 'horizontal', + }); + const el = getByTestId('separator'); + + expect(el).toHaveClass(styles['tedi-separator--top-2']); + expect(el).toHaveClass(styles['tedi-separator--bottom-0-5']); + expect(el).toHaveClass(styles['tedi-separator--left-1']); }); - it('should apply isStretched class', () => { + it('applies isStretched class', () => { const { getByTestId } = renderComponent({ isStretched: true }); expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--is-stretched']); }); - it('should set height CSS variable for vertical separator', () => { - const { getByTestId } = renderComponent({ axis: 'vertical', height: 2 }); - expect(getByTestId('separator')).toHaveStyle('--vertical-separator-height: 2rem'); + it('applies thickness class when no variant', () => { + const { getByTestId } = renderComponent({ thickness: 2 }); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--thickness-2']); }); + it('applies dotted class and dot size', () => { + const { getByTestId } = renderComponent({ + variant: 'dotted', + dotSize: 'small', + dotPosition: 'center', + }); + const el = getByTestId('separator'); - it('should apply dot-only variant class', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only']); + expect(el).toHaveClass(styles['tedi-separator--dotted']); + expect(el).toHaveClass(styles['tedi-separator--dotted-small']); + expect(el).toHaveClass(styles['tedi-separator--dot-position-center']); }); - it('should apply correct dot size class when variant is dot-only and dotSize is extra small', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'extra-small' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-extra-small']); + it('applies custom dot position via CSS var', () => { + const { getByTestId } = renderComponent({ + variant: 'dotted', + dotPosition: 2.75, + }); + expect(getByTestId('separator')).toHaveStyle('--separator-dot-position: 2.75rem'); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-position-custom']); }); - it('should apply correct dot size class when variant is dot-only and dotSize is small', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'small' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-small']); - }); + it('applies dot-only and dot size class', () => { + const { getByTestId } = renderComponent({ + variant: 'dot-only', + dotSize: 'large', + dotStyle: 'filled', + }); + const el = getByTestId('separator'); - it('should apply correct dot size class when variant is dot-only and dotSize is medium', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'medium' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-medium']); + expect(el).toHaveClass(styles['tedi-separator--dot-only']); + expect(el).toHaveClass(styles['tedi-separator--dot-only-large']); + expect(el).not.toHaveClass(styles['tedi-separator--dot-style-outlined']); }); - it('should apply correct dot size class when variant is dot-only and dotSize is large', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'large' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-large']); - }); + it('applies outlined style and thickness var when outlined', () => { + const { getByTestId } = renderComponent({ + variant: 'dot-only', + dotSize: 'medium', + dotStyle: 'outlined', + thickness: 2, + }); + const el = getByTestId('separator'); - it('should not apply dot size class when variant is not dot-only', () => { - const { getByTestId } = renderComponent({ variant: 'dotted', dotSize: 'large' }); - expect(getByTestId('separator')).not.toHaveClass(styles['tedi-separator--dot-only-large']); + expect(el).toHaveClass(styles['tedi-separator--dot-style-outlined']); + expect(el).toHaveStyle('--separator-thickness: 2px'); + expect(el).toHaveClass(styles['tedi-separator--thickness-2']); }); - it('should default vertical separator display to block', () => { + it('requires dotSize (but test fallback/default)', () => { + const { getByTestId } = renderComponent({ + variant: 'dot-only', + dotSize: 'large', + }); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-large']); + }); + it('defaults to block display in vertical mode', () => { const { getByTestId } = renderComponent({ axis: 'vertical' }); expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--block']); }); - it('should apply inline display class to vertical separator when specified', () => { - const { getByTestId } = renderComponent({ axis: 'vertical', display: 'inline' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--inline']); - }); - - it('should not apply inline display class to horizontal separator', () => { - const { getByTestId } = renderComponent({ axis: 'horizontal', display: 'block' }); - expect(getByTestId('separator')).not.toHaveClass(styles['tedi-separator--inline']); - }); - it('should apply dotSize class when variant is dot-only', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'medium' }); - const separator = getByTestId('separator'); - expect(separator).toHaveClass(styles['tedi-separator--dot-only']); - expect(separator).toHaveClass(styles['tedi-separator--dot-only-medium']); - }); - - it('should ignore dotSize when variant is not dot-only', () => { - const { getByTestId } = renderComponent({ variant: 'dotted', dotSize: 'medium' }); - const separator = getByTestId('separator'); - expect(separator).toHaveClass(styles['tedi-separator--dotted']); - expect(separator).not.toHaveClass(styles['tedi-separator--dot-only-medium']); + it('applies inline display when specified (vertical)', () => { + const { getByTestId } = renderComponent({ + axis: 'vertical', + display: 'inline-block', + }); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--inline-block']); }); }); diff --git a/src/tedi/components/misc/separator/separator.stories.tsx b/src/tedi/components/misc/separator/separator.stories.tsx index 5d5a4f6f..385b2a01 100644 --- a/src/tedi/components/misc/separator/separator.stories.tsx +++ b/src/tedi/components/misc/separator/separator.stories.tsx @@ -1,11 +1,10 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react'; -import { Fragment } from 'react/jsx-runtime'; import { Text } from '../../base/typography/text/text'; import { Card, CardContent } from '../../cards/card'; import { Col, Row } from '../../layout/grid'; import { VerticalSpacing } from '../../layout/vertical-spacing'; -import Separator, { SeparatorProps } from './separator'; +import Separator, { DotSize, SeparatorProps } from './separator'; /** * Figma ↗
@@ -32,86 +31,254 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const colorArray: SeparatorProps['color'][] = ['primary', 'secondary', 'accent']; +const spacingArray: SeparatorProps['spacing'][] = [0, 0.5, 1, 1.5, 2, 2.5]; +const sizeArray: SeparatorProps['dotSize'][] = ['large', 'medium']; +type TemplateMultipleProps = SeparatorProps & { + array: Type[]; + property: keyof SeparatorProps; +}; +const Template: StoryFn = (args) => ; -const Template: StoryFn = (args) => ( - <> - Some content - - Other content - -); +const SizesTemplate: StoryFn = (args) => { + const { array } = args; -const ColorsAndThickness: StoryFn = (args) => ( - <> - {colorArray.map((color) => ( - - - - + return ( +

+ {array.map((value, key) => ( + + + {value === 'large' ? 'Large' : 'Medium'} - - - - + + + + + - - ))} - -); + ))} +
+ ); +}; -const VerticalColorTemplate: StoryFn = (args) => ( +const ColorsAndThickness: StoryFn = (args) => ( - {colorArray.map((color) => ( - - - - - - - - - - - ))} + + + + ); -const DotOnlyTemplate: StoryFn = (args) => ( +const SpacingHorizontal: StoryFn = (args) => ( - - - - + {spacingArray.map((spacing, index) => ( + + ))} ); +const SpacingVertical: StoryFn = (args) => ( + + {spacingArray.map((spacing, index) => ( + + + + ))} + +); + export const Default: Story = { render: Template, args: { spacing: 1 }, }; -export const HorizontalColors: Story = { +export const HorizontalSpacings: Story = { + render: SpacingHorizontal, + args: { + axis: 'horizontal', + }, +}; + +export const HorizontalThickness: Story = { render: ColorsAndThickness, - args: { spacing: 1 }, }; -export const VerticalColors: Story = { - render: VerticalColorTemplate, - args: { axis: 'vertical', height: 5 }, +export const Vertical: Story = { + render: Template, + args: { axis: 'vertical', height: 3 }, +}; + +export const VerticalSpacings: Story = { + render: SpacingVertical, + args: { + axis: 'vertical', + height: 3, + display: 'inline-block', + }, +}; + +export const VerticalThickness: Story = { + render: ColorsAndThickness, + args: { axis: 'vertical', height: 3, display: 'inline' }, }; -export const PaddedEven: Story = { +export const DottedLineHorizontal: Story = { render: Template, - args: { spacing: 1 }, + args: { axis: 'horizontal', variant: 'dotted', color: 'accent', dotPosition: 'center' }, }; -export const PaddedUneven: Story = { +export const DottedLineVertical: Story = { render: Template, - args: { topSpacing: 2.5, bottomSpacing: 0.5 }, + args: { axis: 'vertical', variant: 'dotted', color: 'accent', height: 5, dotPosition: 'center' }, +}; + +export const Sizes: StoryObj = { + render: SizesTemplate, + + args: { + property: 'dotSize', + array: sizeArray, + }, +}; + +export const SpacingTopDefault: Story = { + render: () => { + return ( + + + + + + + + + ); + }, +}; + +export const SpacingTopSmall: Story = { + render: () => { + return ( + + + + + + + + + ); + }, +}; + +export const Position: Story = { + render: () => { + return ( + <> + + + + Start + + +
+ +
+ + + + +
+
+ + + + Center + + + + + + + + + + + + + End + + + + + + + + + + +
+ + ); + }, }; const TemplateVertical: StoryFn = (args) => ( @@ -137,88 +304,132 @@ const TemplateVertical: StoryFn = (args) => ( ); -export const VerticalThick: Story = { - render: TemplateVertical, +export const DotFilled: Story = { + render: () => { + return ( + <> + + + ); + }, +}; + +export const DotOutlined: Story = { + render: () => { + return ( + <> + + + ); + }, +}; + +const dotSizeToPxMap: Record = { + xs: '2px', + sm: '4px', + md: '8px', + lg: '15px', +}; + +const DottedSizesTemplate: StoryFn = (args) => { + const { array } = args; + + return ( +
+ {array.map((value, key) => ( + + + {value !== undefined ? dotSizeToPxMap[value] || value : '—'} + + + + + + + + + ))} +
+ ); +}; + +export const DottedSizes: StoryObj = { + render: DottedSizesTemplate, args: { - axis: 'horizontal', - thickness: 1, - isStretched: true, - topSpacing: 1, - bottomSpacing: 1, - md: { axis: 'vertical', thickness: 1 }, + property: 'dotSize', + array: ['extra-small', 'small', 'medium', 'large'], }, }; -export const VerticalDotted: Story = { +const InlineSeparatorTemplate: StoryFn = (args) => { + const { dotPosition, ...safeArgs } = args; + + return ( + <> + + Lorem ipsum dolor sit, amet + + consectetur adipisicing elit. + + + Lorem ipsum dolor sit, amet + + consectetur adipisicing elit. + + + Lorem ipsum dolor sit, amet + + consectetur adipisicing elit. + + + Lorem ipsum dolor sit, amet + + consectetur adipisicing elit. + + + ); +}; + +export const InlineSeparatorUsage: Story = { + render: InlineSeparatorTemplate, + args: { axis: 'vertical', display: 'inline' }, +}; + +export const VerticalDottedCardExample: Story = { render: TemplateVertical, args: { axis: 'horizontal', variant: 'dotted', color: 'accent', - topSpacing: 1, - bottomSpacing: 1, + spacing: 1, isStretched: true, + dotPosition: 1.25, md: { axis: 'vertical' }, }, }; -export const VerticalDottedSmall: Story = { +export const VerticalDottedSmallCardExample: Story = { render: TemplateVertical, args: { axis: 'horizontal', - topSpacing: 1, - bottomSpacing: 1, - variant: 'dotted-small', + spacing: 1, + variant: 'dotted', + dotSize: 'medium', color: 'accent', isStretched: true, + dotPosition: 1.25, md: { axis: 'vertical' }, }, }; - -export const HorizontalDottedSeparator: Story = { - render: () => ( - - - - - - - - - ), -}; - -export const DotOnly: Story = { - render: DotOnlyTemplate, - args: { spacing: 0.5 }, -}; - -const InlineSeparatorTemplate: StoryFn = (args) => ( - - - Lorem ipsum dolor sit, amet - - consectetur adipisicing elit. - - - Lorem ipsum dolor sit, amet - - consectetur adipisicing elit. - - - Lorem ipsum dolor sit, amet - - consectetur adipisicing elit. - - - Lorem ipsum dolor sit, amet - - consectetur adipisicing elit. - - -); - -export const InlineSeparatorUsedInText: Story = { - render: InlineSeparatorTemplate, - args: { axis: 'vertical', display: 'inline' }, -}; diff --git a/src/tedi/components/misc/separator/separator.tsx b/src/tedi/components/misc/separator/separator.tsx index bcf4f768..cabe5bd3 100644 --- a/src/tedi/components/misc/separator/separator.tsx +++ b/src/tedi/components/misc/separator/separator.tsx @@ -4,130 +4,162 @@ import { CSSProperties } from 'react'; import { BreakpointSupport, useBreakpointProps } from '../../../helpers'; import styles from './separator.module.scss'; -export type SeparatorSpacing = 0 | 0.25 | 0.5 | 0.75 | 1 | 1.25 | 1.5 | 1.75 | 2 | 2.5 | 5; +export type SeparatorVariant = 'dotted' | 'dot-only'; +export type DotSize = 'large' | 'medium' | 'small' | 'extra-small'; +export type DotStyle = 'filled' | 'outlined'; +export type DotPosition = 'start' | 'center' | 'end' | number; + +/** + * Margin/padding-like spacing around the separator + * - number → uniform spacing on main axis + * - object → fine-grained control (top/bottom/left/right) + */ +export type SeparatorSpacing = + | number + | { + top?: number; + bottom?: number; + left?: number; + right?: number; + }; export interface SeparatorSharedProps { /** - * Additional class. + * Additional class names */ className?: string; /** - * Rendered HTML element. - * @default div + * HTML element to render — most common are 'hr', 'div', 'span' */ element?: 'hr' | 'div' | 'span'; /** - * Whether the separator should stretch to fill the full spacing inside cardContent. + * When true, the separator stretches to fill available space (100%) */ isStretched?: boolean; - /* - * Color of separator - * @default default + /** + * Semantic color token + * @default primary */ color?: 'primary' | 'secondary' | 'accent'; - /* - * Separator style variant. - */ - variant?: 'dotted' | 'dotted-small' | 'dot-only'; - /* - * Dot size. - * Only used when variant="dot-only" + /** + * Visual style — line with dots vs standalone centered dot(s) */ - dotSize?: 'large' | 'medium' | 'small' | 'extra-small'; - /* - * Thickness in pixels (ignored if variant is used). - * @default 1 + variant?: SeparatorVariant; + /** + * Line thickness in pixels (1 or 2) — affects outlined & solid lines */ thickness?: 1 | 2; /** - * Spacing applied based on the axis: - * - For horizontal axis, spacing is applied to top and bottom of the separator. - * - For vertical axis, spacing is applied to left and right of the separator. + * Spacing (margin) around the separator + * @example + * spacing={16} // 16px top & bottom (horizontal) or left & right (vertical) + * spacing={{ top: 24, bottom: 8 }} */ spacing?: SeparatorSpacing; } - export interface SeparatorVerticalProps extends SeparatorSharedProps { /** - * Height of separator. Use with vertical axis, when full-width separator is not needed. - * Height can be number in rem units. It's customizable to allow for more flexibility around X components. + * Must be set to 'vertical' + */ + axis: 'vertical'; + /** + * Height of the vertical separator in rem units */ height?: number; /** - * Axis of separator, vertical and horizontal separators support different props + * CSS display value — usually 'block' or 'inline-block' */ - axis: 'vertical'; - topSpacing?: undefined; - bottomSpacing?: undefined; - display?: 'block' | 'inline'; + display?: 'block' | 'inline' | 'inline-block'; } - export interface SeparatorHorizontalProps extends SeparatorSharedProps { /** - * Spacing on top of separator. Ignored when spacing is also used. Only for horizontal axis. + * Must be set to 'horizontal' or left undefined (defaults to horizontal) */ - topSpacing?: SeparatorSpacing; + axis?: 'horizontal'; /** - * Spacing on bottom of separator. Ignored when spacing is also used. Only for horizontal axis. - */ - bottomSpacing?: SeparatorSpacing; + Vertical height is not used in horizontal mode + */ + height?: undefined; /** - * Axis of separator, vertical and horizontal separators support different props + * Display is forced to 'block' in horizontal mode */ - axis?: 'horizontal'; - height?: undefined; display?: 'block'; } -export type SeparatorBreakpointProps = { +type DottedSeparatorProps = { + variant?: 'dotted'; + dotSize?: DotSize; + dotStyle?: DotStyle; /** - * Spacing values based on breakpoints. + * Position of the single dot + * @example + * 'center' | 'start' | 'end' | 2.5 // 2.5rem from start */ - spacing?: Omit; - /** - * Height values based on breakpoints (for vertical separators). - */ - height?: Omit; + dotPosition?: DotPosition; +}; + +type DotOnlySeparatorProps = { + variant: 'dot-only'; + dotSize: DotSize; + dotStyle?: DotStyle; + dotPosition?: never; +}; + +export type SeparatorBreakpointProps = { + spacing?: SeparatorSpacing; + height?: number; axis?: 'horizontal' | 'vertical'; }; export type SeparatorProps = BreakpointSupport< - | ( - | SeparatorHorizontalProps - | (SeparatorVerticalProps & { - variant?: 'dotted' | 'dotted-small'; - dotSize?: undefined; - }) - ) - | ( - | SeparatorHorizontalProps - | (SeparatorVerticalProps & { - variant: 'dot-only'; - dotSize: 'large' | 'medium' | 'small' | 'extra-small'; - }) - ) + | (SeparatorHorizontalProps & (DottedSeparatorProps | DotOnlySeparatorProps)) + | (SeparatorVerticalProps & (DottedSeparatorProps | DotOnlySeparatorProps)) > & SeparatorBreakpointProps; export const Separator = (props: SeparatorProps): JSX.Element => { const { getCurrentBreakpointProps } = useBreakpointProps(props.defaultServerBreakpoint); + const { className, element: Element = 'div', isStretched, spacing, - topSpacing, - bottomSpacing, axis = 'horizontal', - color = 'default', + color = 'primary', variant, thickness = 1, height, - dotSize, + dotSize = 'large', + dotStyle = 'filled', + dotPosition, display = 'block', ...rest } = getCurrentBreakpointProps(props); + const isNumericDotPosition = typeof dotPosition === 'number'; + const resolvedDotPosition = variant !== 'dot-only' && !isNumericDotPosition ? dotPosition : undefined; + + let top: number | undefined; + let bottom: number | undefined; + let left: number | undefined; + let right: number | undefined; + + if (typeof spacing === 'number') { + if (axis === 'horizontal') { + top = bottom = spacing; + left = right = 0; + } else { + left = right = spacing; + top = bottom = 0; + } + } else if (typeof spacing === 'object' && spacing !== null) { + top = spacing.top ?? (axis === 'horizontal' ? spacing.top ?? spacing.bottom ?? 0 : 0); + bottom = spacing.bottom ?? (axis === 'horizontal' ? spacing.top ?? spacing.bottom ?? 0 : 0); + left = spacing.left ?? (axis === 'vertical' ? spacing.left ?? spacing.right ?? 0 : 0); + right = spacing.right ?? (axis === 'vertical' ? spacing.left ?? spacing.right ?? 0 : 0); + } + const SeparatorBEM = cn( styles['tedi-separator'], className, @@ -135,19 +167,33 @@ export const Separator = (props: SeparatorProps): JSX.Element => { { [styles[`tedi-separator--${axis}`]]: axis }, { [styles[`tedi-separator--${variant}`]]: variant }, { [styles[`tedi-separator--${display}`]]: display }, - { [styles[`tedi-separator--${variant}-${dotSize}`]]: variant && dotSize }, - { [styles[`tedi-separator--thickness-${thickness}`]]: thickness && !variant }, + { [styles[`tedi-separator--${variant}-${dotSize}`]]: variant === 'dot-only' && dotSize }, + { [styles[`tedi-separator--dot-style-${dotStyle}`]]: variant && dotStyle }, + { [styles[`tedi-separator--dotted-${dotSize}`]]: variant === 'dotted' && dotSize }, + { [styles[`tedi-separator--dot-position-${resolvedDotPosition}`]]: resolvedDotPosition && variant !== 'dot-only' }, + { [styles['tedi-separator--dot-position-custom']]: isNumericDotPosition }, + { + [styles[`tedi-separator--thickness-${thickness}`]]: thickness || dotStyle === 'outlined' ? thickness : undefined, + }, { [styles['tedi-separator--is-stretched']]: isStretched }, - { [styles[`tedi-separator--spacing-${spacing}`.replace('.', '-')]]: spacing }, - { [styles[`tedi-separator--top-${topSpacing}`.replace('.', '-')]]: !spacing && topSpacing }, - { [styles[`tedi-separator--bottom-${bottomSpacing}`.replace('.', '-')]]: !spacing && bottomSpacing } + { [styles[`tedi-separator--top-${top}`.replace('.', '-')]]: top }, + { [styles[`tedi-separator--bottom-${bottom}`.replace('.', '-')]]: bottom }, + { [styles[`tedi-separator--left-${left}`.replace('.', '-')]]: left }, + { [styles[`tedi-separator--right-${right}`.replace('.', '-')]]: right } ); const getCssVars = () => { const cssvars: CSSProperties = {}; - if (height) cssvars['--vertical-separator-height'] = `${height}rem`; + if (thickness) { + cssvars['--separator-thickness'] = `${thickness}px`; + } + + if (variant === 'dotted' && isNumericDotPosition) { + cssvars['--separator-dot-position'] = `${dotPosition}rem`; + } + return cssvars; };