From f75a2b21daab3b783ef683cf2d2efceb8caaa653 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:08:19 +0200 Subject: [PATCH 1/5] feat(separator): redesign with new spacing API, dotPosition and dotStyle support #30 BREAKING CHANGE: legacy spacing props removed, dotSize enum changed, variant values simplified --- package-lock.json | 8 +- package.json | 2 +- .../misc/separator/separator.module.scss | 251 +++++++---- .../misc/separator/separator.spec.tsx | 183 ++++---- .../misc/separator/separator.stories.tsx | 420 +++++++++++++----- .../components/misc/separator/separator.tsx | 186 +++++--- 6 files changed, 688 insertions(+), 362 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1604a4dfe..2a4e27d17 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 6fcde260d..3fba459cb 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/tedi/components/misc/separator/separator.module.scss b/src/tedi/components/misc/separator/separator.module.scss index 0e17b456e..41ee80ff4 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%; @@ -28,6 +37,72 @@ $sizes: ( height: var(--vertical-separator-height); border-top: 0; border-left: 1px solid var(--general-border-primary); + + &.tedi-separator--dot-position-start::before { + top: 0; + bottom: auto; + } + + &.tedi-separator--dot-position-center::before { + top: 0; + bottom: 0; + margin: auto 0; + } + + &.tedi-separator--dot-position-end::before { + top: auto; + bottom: 0; + } + + &.tedi-separator--dotted-xs::before { + left: calc(var(--separator-dot-size-xs) / 2 * -1); + } + + &.tedi-separator--dotted-sm::before { + left: calc(var(--separator-dot-size-sm) / 2 * -1); + } + + &.tedi-separator--dotted-md::before { + left: calc(var(--separator-dot-size-md) / 2 * -1); + } + + &.tedi-separator--dotted-lg::before { + left: calc(var(--separator-dot-size-lg) / 2 * -1); + } + } + + &--horizontal { + &.tedi-separator--dotted-xs::before { + top: calc(var(--separator-dot-size-xs) / 2 * -1); + } + + &.tedi-separator--dotted-sm::before { + top: calc(var(--separator-dot-size-sm) / 2 * -1); + } + + &.tedi-separator--dotted-md::before { + top: calc(var(--separator-dot-size-md) / 2 * -1); + } + + &.tedi-separator--dotted-lg::before { + top: calc(var(--separator-dot-size-lg) / 2 * -1); + } + + &.tedi-separator--dot-position-start::before { + right: auto; + left: 0; + } + + &.tedi-separator--dot-position-center::before { + right: 0; + left: 0; + margin: auto; + } + + &.tedi-separator--dot-position-end::before { + right: 0; + left: auto; + } } &--secondary { @@ -45,84 +120,82 @@ $sizes: ( &--inline { display: inline; } + + &--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--dotted, -.tedi-separator--dotted-small { +%separator-dotted-base { &::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); - } - - &.tedi-separator--accent::before { - background-color: var(--general-border-accent); + &.tedi-separator--vertical { + min-height: 3rem; } } -.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--dotted { + @extend %separator-dotted-base; -.tedi-separator--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); + @each $name, $color in $dot-colors { + &.tedi-separator--#{$name}::before { + background-color: $color; + } } } @each $size, $value in $sizes { - .tedi-separator--horizontal.tedi-separator--top-#{$size} { - margin-top: #{$value}; + .tedi-separator--spacing-#{$size}:not(.tedi-separator--dot-only) { + margin: #{$value} 0 #{$value} 0; } - .tedi-separator--horizontal.tedi-separator--bottom-#{$size} { - margin-bottom: #{$value}; - } + .tedi-separator--dot-only.tedi-separator--spacing-#{$size} { + &.tedi-separator--horizontal { + margin-right: #{$value}; + margin-left: #{$value}; + } - .tedi-separator--horizontal.tedi-separator--spacing-#{$size}:not(.tedi-separator--dot-only) { - margin-top: #{$value}; - margin-bottom: #{$value}; + &.tedi-separator--vertical { + margin-top: #{$value}; + margin-bottom: #{$value}; + } } - .tedi-separator--horizontal.tedi-separator--dot-only.tedi-separator--spacing-#{$size}, - .tedi-separator--vertical.tedi-separator--spacing-#{$size} { - margin-right: #{$value}; - margin-left: #{$value}; + @each $side in (top, bottom, left, right) { + .tedi-separator--#{$side}-#{$size} { + @if $side == top { + margin-top: #{$value}; + } @else if $side == bottom { + margin-bottom: #{$value}; + } @else if $side == left { + margin-left: #{$value}; + } @else if $side == right { + margin-right: #{$value}; + } + } } } -$thicknesses: ( - '1': 1px, - '2': 2px, -); - @each $thickness, $value in $thicknesses { - .tedi-separator { - &.tedi-separator--thickness-#{$thickness} { - border-top-width: #{$value}; - } + .tedi-separator.tedi-separator--thickness-#{$thickness} { + border-top-width: #{$value}; + } - &--vertical.tedi-separator--thickness-#{$thickness} { - border-left-width: #{$value}; - } + .tedi-separator--vertical.tedi-separator--thickness-#{$thickness} { + border-left-width: #{$value}; } } @@ -143,53 +216,57 @@ $thicknesses: ( @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-only-small::before { - width: var(--separator-dotted-dot-sm); - height: var(--separator-dotted-dot-sm); + @each $name, $color in $dot-colors { + &.tedi-separator--#{$name}::before { + background-color: $color; + } } - &.tedi-separator--dot-only-medium::before { - width: var(--separator-dotted-dot-md); - height: var(--separator-dotted-dot-md); - } + &.tedi-separator--dot-style-outlined { + &::before { + background: transparent; + border-style: solid; + border-width: var(--separator-thickness, 1px); + } - &.tedi-separator--dot-only-large::before { - width: var(--separator-dotted-dot-lg); - height: var(--separator-dotted-dot-lg); + @each $name, $color in $dot-colors { + &.tedi-separator--#{$name}::before { + border-color: $color; + } + } } +} - &.tedi-separator--primary::before { - background-color: var(--general-border-primary); - } +.tedi-separator--dot-only-xs::before, +.tedi-separator--dotted-xs::before { + width: var(--separator-dot-size-xs); + height: var(--separator-dot-size-xs); +} - &.tedi-separator--secondary::before { - background-color: var(--general-border-secondary); - } +.tedi-separator--dot-only-sm::before, +.tedi-separator--dotted-sm::before { + width: var(--separator-dot-size-sm); + height: var(--separator-dot-size-sm); +} - &.tedi-separator--accent::before { - background-color: var(--general-border-accent); - } +.tedi-separator--dot-only-md::before, +.tedi-separator--dotted-md::before { + width: var(--separator-dot-size-md); + height: var(--separator-dot-size-md); } -.tedi-separator--horizontal { - &.tedi-separator--dotted::before, - &.tedi-separator--dotted-small::before { - right: 0; - left: 0; - margin: 0 auto; - transform: initial; - } +.tedi-separator--dot-only-lg::before, +.tedi-separator--dotted-lg::before { + width: var(--separator-dot-size-lg); + height: var(--separator-dot-size-lg); +} - &.tedi-separator--dotted::before { - top: calc(var(--separator-dotted-dot-lg) / 2 * -1); - } +.tedi-separator--horizontal.tedi-separator--dot-position-custom::before { + right: auto; + left: var(--separator-dot-position); +} - &.tedi-separator--dotted-small::before { - top: calc(var(--separator-dotted-dot-md) / 2 * -1); - } +.tedi-separator--vertical.tedi-separator--dot-position-custom::before { + top: var(--separator-dot-position); + bottom: auto; } diff --git a/src/tedi/components/misc/separator/separator.spec.tsx b/src/tedi/components/misc/separator/separator.spec.tsx index 65d9c7e20..550221f05 100644 --- a/src/tedi/components/misc/separator/separator.spec.tsx +++ b/src/tedi/components/misc/separator/separator.spec.tsx @@ -17,132 +17,149 @@ 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('should apply dot-only variant class', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only']); + 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: 'sm', + dotPosition: 'center', + }); + const el = getByTestId('separator'); - 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']); + expect(el).toHaveClass(styles['tedi-separator--dotted']); + expect(el).toHaveClass(styles['tedi-separator--dotted-sm']); + expect(el).toHaveClass(styles['tedi-separator--dot-position-center']); }); - 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 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 medium', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'medium' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-medium']); + it('does not apply dot-style class when variant is dotted', () => { + const { getByTestId } = renderComponent({ + variant: 'dotted', + dotStyle: undefined, + }); + expect(getByTestId('separator')).not.toHaveClass(/dot-style/); }); - 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 dot-only and dot size class', () => { + const { getByTestId } = renderComponent({ + variant: 'dot-only', + dotSize: 'lg', + dotStyle: 'filled', + }); + 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-only']); + expect(el).toHaveClass(styles['tedi-separator--dot-only-lg']); + expect(el).not.toHaveClass(styles['tedi-separator--dot-style-outlined']); }); - it('should default vertical separator display to block', () => { - const { getByTestId } = renderComponent({ axis: 'vertical' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--block']); - }); + it('applies outlined style and thickness var when outlined', () => { + const { getByTestId } = renderComponent({ + variant: 'dot-only', + dotSize: 'md', + dotStyle: 'outlined', + thickness: 2, + }); + const el = getByTestId('separator'); - 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']); + 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 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('requires dotSize (but test fallback/default)', () => { + const { getByTestId } = renderComponent({ + variant: 'dot-only', + dotSize: 'lg', + }); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-lg']); }); - 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('defaults to block display in vertical mode', () => { + const { getByTestId } = renderComponent({ axis: 'vertical' }); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--block']); }); - 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 5d5a4f6f5..7ae362149 100644 --- a/src/tedi/components/misc/separator/separator.stories.tsx +++ b/src/tedi/components/misc/separator/separator.stories.tsx @@ -1,11 +1,9 @@ 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 +30,232 @@ 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'][] = ['lg', 'md']; +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 === 'lg' ? '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 +281,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: ['xs', 'sm', 'md', 'lg'], }, }; -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: 'md', 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 bcf4f7682..5ac08ebac 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 = 'lg' | 'md' | 'sm' | 'xs'; +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 = { - /** - * Spacing values based on breakpoints. - */ - spacing?: Omit; +type DottedSeparatorProps = { + variant?: 'dotted'; + dotSize?: DotSize; + dotStyle?: never; /** - * Height values based on breakpoints (for vertical separators). + * Position of the single dot + * @example + * 'center' | 'start' | 'end' | 2.5 // 2.5rem from start */ - 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 = 'lg', + 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,31 @@ 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 === 'dot-only' && 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 && (!variant || (variant === 'dot-only' && dotStyle === 'outlined')), + }, { [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 (variant === 'dot-only' && dotStyle === 'outlined' && thickness) { + cssvars['--separator-thickness'] = `${thickness}px`; + } + if (variant === 'dotted' && isNumericDotPosition) { + cssvars['--separator-dot-position'] = `${dotPosition}rem`; + } return cssvars; }; From 222eedb0461947109e7c7b8f4d8a91f32e52230c Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:18:54 +0200 Subject: [PATCH 2/5] chore: fix stories that use Separator #30 --- .../components/map-components/left-panel/left-panel-footer.tsx | 2 +- src/tedi/components/cards/card/card.stories.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 526f38fc9..f3d725cbc 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 4d2c39ab7..48ddc9f8e 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

From 8072aeda50cb18cc577e868307b2b002d2e9be8d Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:26:29 +0200 Subject: [PATCH 3/5] fix(separator): design review fixes #30 --- .../misc/separator/separator.module.scss | 272 ++++++++---------- .../misc/separator/separator.spec.tsx | 22 +- .../misc/separator/separator.stories.tsx | 155 +++++----- .../components/misc/separator/separator.tsx | 16 +- 4 files changed, 227 insertions(+), 238 deletions(-) diff --git a/src/tedi/components/misc/separator/separator.module.scss b/src/tedi/components/misc/separator/separator.module.scss index 41ee80ff4..cd5455a2a 100644 --- a/src/tedi/components/misc/separator/separator.module.scss +++ b/src/tedi/components/misc/separator/separator.module.scss @@ -37,72 +37,18 @@ $dot-colors: ( height: var(--vertical-separator-height); border-top: 0; border-left: 1px solid var(--general-border-primary); - - &.tedi-separator--dot-position-start::before { - top: 0; - bottom: auto; - } - - &.tedi-separator--dot-position-center::before { - top: 0; - bottom: 0; - margin: auto 0; - } - - &.tedi-separator--dot-position-end::before { - top: auto; - bottom: 0; - } - - &.tedi-separator--dotted-xs::before { - left: calc(var(--separator-dot-size-xs) / 2 * -1); - } - - &.tedi-separator--dotted-sm::before { - left: calc(var(--separator-dot-size-sm) / 2 * -1); - } - - &.tedi-separator--dotted-md::before { - left: calc(var(--separator-dot-size-md) / 2 * -1); - } - - &.tedi-separator--dotted-lg::before { - left: calc(var(--separator-dot-size-lg) / 2 * -1); - } } &--horizontal { - &.tedi-separator--dotted-xs::before { - top: calc(var(--separator-dot-size-xs) / 2 * -1); - } - - &.tedi-separator--dotted-sm::before { - top: calc(var(--separator-dot-size-sm) / 2 * -1); - } - - &.tedi-separator--dotted-md::before { - top: calc(var(--separator-dot-size-md) / 2 * -1); - } - - &.tedi-separator--dotted-lg::before { - top: calc(var(--separator-dot-size-lg) / 2 * -1); - } - - &.tedi-separator--dot-position-start::before { - right: auto; - left: 0; - } + display: block; + } - &.tedi-separator--dot-position-center::before { - right: 0; - left: 0; - margin: auto; - } + &--block { + display: block; + } - &.tedi-separator--dot-position-end::before { - right: 0; - left: auto; - } + &--inline { + display: inline; } &--secondary { @@ -113,14 +59,6 @@ $dot-colors: ( border-color: var(--general-border-accent); } - &--block { - display: block; - } - - &--inline { - display: inline; - } - &--is-stretched { margin-right: calc(var(--card-content-padding-right) * -1); margin-left: calc(var(--card-content-padding-left) * -1); @@ -137,7 +75,7 @@ $dot-colors: ( position: absolute; content: ''; background-color: var(--general-border-primary); - border-radius: 100%; + border-radius: 50%; @include mixins.print-grayscale; } @@ -149,124 +87,158 @@ $dot-colors: ( .tedi-separator--dotted { @extend %separator-dotted-base; +} - @each $name, $color in $dot-colors { - &.tedi-separator--#{$name}::before { +.tedi-separator--dot-only { + position: relative; + display: inline-block; + vertical-align: middle; + border: none; + + &::before { + position: relative; + display: block; + width: var(--separator-dotted-dot-sm); + height: var(--separator-dotted-dot-sm); + content: ''; + border-radius: 50%; + + @include mixins.print-grayscale; + } + + &.tedi-separator--dot-style-outlined::before { + background: transparent; + } +} + +@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 { + background-color: var(--tedi-neutral-100); + border-color: $color; + } + } +} + +@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}); + } +} + +@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 if $pos == end { + right: 0; + left: auto; + } + } @else { + @if $pos == start { + top: 0; + bottom: auto; + } @else if $pos == center { + top: 0; + bottom: 0; + margin: auto 0; + } @else if $pos == end { + top: auto; + bottom: 0; + } + } + } } } +.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 #{$value} 0; + margin: #{$value} 0; } .tedi-separator--dot-only.tedi-separator--spacing-#{$size} { &.tedi-separator--horizontal { - margin-right: #{$value}; - margin-left: #{$value}; + margin-right: $value; + margin-left: $value; } &.tedi-separator--vertical { - margin-top: #{$value}; - margin-bottom: #{$value}; + margin-top: $value; + margin-bottom: $value; } } @each $side in (top, bottom, left, right) { .tedi-separator--#{$side}-#{$size} { - @if $side == top { - margin-top: #{$value}; - } @else if $side == bottom { - margin-bottom: #{$value}; - } @else if $side == left { - margin-left: #{$value}; - } @else if $side == right { - margin-right: #{$value}; - } + margin-#{$side}: $value; } } } @each $thickness, $value in $thicknesses { - .tedi-separator.tedi-separator--thickness-#{$thickness} { - border-top-width: #{$value}; + .tedi-separator--thickness-#{$thickness} { + border-top-width: $value; } - .tedi-separator--vertical.tedi-separator--thickness-#{$thickness} { - border-left-width: #{$value}; + border-left-width: $value; } } -.tedi-separator--dot-only { - position: relative; - display: inline-block; - vertical-align: middle; - border: none; +@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--dot-style-outlined { &::before { - position: relative; - display: block; - width: var(--separator-dotted-dot-sm); - height: var(--separator-dotted-dot-sm); - content: ''; - border-radius: 100%; - - @include mixins.print-grayscale; + background-color: var(--tedi-neutral-100); + border-style: solid; + border-width: var(--separator-thickness, 1px); } @each $name, $color in $dot-colors { &.tedi-separator--#{$name}::before { - background-color: $color; - } - } - - &.tedi-separator--dot-style-outlined { - &::before { - background: transparent; - border-style: solid; - border-width: var(--separator-thickness, 1px); + z-index: 10; + border-color: $color; } - @each $name, $color in $dot-colors { - &.tedi-separator--#{$name}::before { - border-color: $color; - } + &.tedi-separator--dotted-#{$name}::before { + z-index: 10; + border-color: $color; } } } - -.tedi-separator--dot-only-xs::before, -.tedi-separator--dotted-xs::before { - width: var(--separator-dot-size-xs); - height: var(--separator-dot-size-xs); -} - -.tedi-separator--dot-only-sm::before, -.tedi-separator--dotted-sm::before { - width: var(--separator-dot-size-sm); - height: var(--separator-dot-size-sm); -} - -.tedi-separator--dot-only-md::before, -.tedi-separator--dotted-md::before { - width: var(--separator-dot-size-md); - height: var(--separator-dot-size-md); -} - -.tedi-separator--dot-only-lg::before, -.tedi-separator--dotted-lg::before { - width: var(--separator-dot-size-lg); - height: var(--separator-dot-size-lg); -} - -.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; -} diff --git a/src/tedi/components/misc/separator/separator.spec.tsx b/src/tedi/components/misc/separator/separator.spec.tsx index 550221f05..694961d9a 100644 --- a/src/tedi/components/misc/separator/separator.spec.tsx +++ b/src/tedi/components/misc/separator/separator.spec.tsx @@ -89,13 +89,13 @@ describe('Separator Component', () => { it('applies dotted class and dot size', () => { const { getByTestId } = renderComponent({ variant: 'dotted', - dotSize: 'sm', + dotSize: 'small', dotPosition: 'center', }); const el = getByTestId('separator'); expect(el).toHaveClass(styles['tedi-separator--dotted']); - expect(el).toHaveClass(styles['tedi-separator--dotted-sm']); + expect(el).toHaveClass(styles['tedi-separator--dotted-small']); expect(el).toHaveClass(styles['tedi-separator--dot-position-center']); }); @@ -108,31 +108,23 @@ describe('Separator Component', () => { expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-position-custom']); }); - it('does not apply dot-style class when variant is dotted', () => { - const { getByTestId } = renderComponent({ - variant: 'dotted', - dotStyle: undefined, - }); - expect(getByTestId('separator')).not.toHaveClass(/dot-style/); - }); - it('applies dot-only and dot size class', () => { const { getByTestId } = renderComponent({ variant: 'dot-only', - dotSize: 'lg', + dotSize: 'large', dotStyle: 'filled', }); const el = getByTestId('separator'); expect(el).toHaveClass(styles['tedi-separator--dot-only']); - expect(el).toHaveClass(styles['tedi-separator--dot-only-lg']); + expect(el).toHaveClass(styles['tedi-separator--dot-only-large']); expect(el).not.toHaveClass(styles['tedi-separator--dot-style-outlined']); }); it('applies outlined style and thickness var when outlined', () => { const { getByTestId } = renderComponent({ variant: 'dot-only', - dotSize: 'md', + dotSize: 'medium', dotStyle: 'outlined', thickness: 2, }); @@ -146,9 +138,9 @@ describe('Separator Component', () => { it('requires dotSize (but test fallback/default)', () => { const { getByTestId } = renderComponent({ variant: 'dot-only', - dotSize: 'lg', + dotSize: 'large', }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-lg']); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-large']); }); it('defaults to block display in vertical mode', () => { const { getByTestId } = renderComponent({ axis: 'vertical' }); diff --git a/src/tedi/components/misc/separator/separator.stories.tsx b/src/tedi/components/misc/separator/separator.stories.tsx index 7ae362149..385b2a01d 100644 --- a/src/tedi/components/misc/separator/separator.stories.tsx +++ b/src/tedi/components/misc/separator/separator.stories.tsx @@ -3,6 +3,7 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react'; 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, { DotSize, SeparatorProps } from './separator'; /** @@ -31,7 +32,7 @@ export default meta; type Story = StoryObj; const spacingArray: SeparatorProps['spacing'][] = [0, 0.5, 1, 1.5, 2, 2.5]; -const sizeArray: SeparatorProps['dotSize'][] = ['lg', 'md']; +const sizeArray: SeparatorProps['dotSize'][] = ['large', 'medium']; type TemplateMultipleProps = SeparatorProps & { array: Type[]; property: keyof SeparatorProps; @@ -46,7 +47,7 @@ const SizesTemplate: StoryFn = (args) => { {array.map((value, key) => ( - {value === 'lg' ? 'Large' : 'Medium'} + {value === 'large' ? 'Large' : 'Medium'} - + - + ); @@ -176,10 +177,10 @@ export const SpacingTopSmall: Story = { return ( - + - + ); @@ -192,65 +193,87 @@ export const Position: Story = { <> - Start - - - - - - - - + + Start + + +
+ +
+ + + + +
+
- Center - - - - - - - - + + Center + + + + + + + + + - End - - - - - - - - + + End + + + + + + + + +
@@ -285,7 +308,7 @@ export const DotFilled: Story = { render: () => { return ( <> - + ); }, @@ -295,7 +318,7 @@ export const DotOutlined: Story = { render: () => { return ( <> - + ); }, @@ -346,7 +369,7 @@ export const DottedSizes: StoryObj = { render: DottedSizesTemplate, args: { property: 'dotSize', - array: ['xs', 'sm', 'md', 'lg'], + array: ['extra-small', 'small', 'medium', 'large'], }, }; @@ -372,7 +395,7 @@ const InlineSeparatorTemplate: StoryFn = (args) => { Lorem ipsum dolor sit, amet - + consectetur adipisicing elit. @@ -403,7 +426,7 @@ export const VerticalDottedSmallCardExample: Story = { axis: 'horizontal', spacing: 1, variant: 'dotted', - dotSize: 'md', + dotSize: 'medium', color: 'accent', isStretched: true, dotPosition: 1.25, diff --git a/src/tedi/components/misc/separator/separator.tsx b/src/tedi/components/misc/separator/separator.tsx index 5ac08ebac..94424b898 100644 --- a/src/tedi/components/misc/separator/separator.tsx +++ b/src/tedi/components/misc/separator/separator.tsx @@ -5,7 +5,7 @@ import { BreakpointSupport, useBreakpointProps } from '../../../helpers'; import styles from './separator.module.scss'; export type SeparatorVariant = 'dotted' | 'dot-only'; -export type DotSize = 'lg' | 'md' | 'sm' | 'xs'; +export type DotSize = 'large' | 'medium' | 'small' | 'extra-small'; export type DotStyle = 'filled' | 'outlined'; export type DotPosition = 'start' | 'center' | 'end' | number; @@ -89,7 +89,7 @@ export interface SeparatorHorizontalProps extends SeparatorSharedProps { type DottedSeparatorProps = { variant?: 'dotted'; dotSize?: DotSize; - dotStyle?: never; + dotStyle?: DotStyle; /** * Position of the single dot * @example @@ -130,7 +130,7 @@ export const Separator = (props: SeparatorProps): JSX.Element => { variant, thickness = 1, height, - dotSize = 'lg', + dotSize = 'large', dotStyle = 'filled', dotPosition, display = 'block', @@ -168,13 +168,12 @@ export const Separator = (props: SeparatorProps): JSX.Element => { { [styles[`tedi-separator--${variant}`]]: variant }, { [styles[`tedi-separator--${display}`]]: display }, { [styles[`tedi-separator--${variant}-${dotSize}`]]: variant === 'dot-only' && dotSize }, - { [styles[`tedi-separator--dot-style-${dotStyle}`]]: variant === 'dot-only' && dotStyle }, + { [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 && (!variant || (variant === 'dot-only' && dotStyle === 'outlined')), + [styles[`tedi-separator--thickness-${thickness}`]]: thickness || dotStyle === 'outlined' ? thickness : undefined, }, { [styles['tedi-separator--is-stretched']]: isStretched }, { [styles[`tedi-separator--top-${top}`.replace('.', '-')]]: top }, @@ -186,12 +185,15 @@ export const Separator = (props: SeparatorProps): JSX.Element => { const getCssVars = () => { const cssvars: CSSProperties = {}; if (height) cssvars['--vertical-separator-height'] = `${height}rem`; - if (variant === 'dot-only' && dotStyle === 'outlined' && thickness) { + + if (thickness) { cssvars['--separator-thickness'] = `${thickness}px`; } + if (variant === 'dotted' && isNumericDotPosition) { cssvars['--separator-dot-position'] = `${dotPosition}rem`; } + return cssvars; }; From 9c3a379bdbfaed87ce680c99a8d525ad7a8b1720 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:37:39 +0200 Subject: [PATCH 4/5] fix(separator): change outlined dotted dot background color #30 --- src/tedi/components/misc/separator/separator.module.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tedi/components/misc/separator/separator.module.scss b/src/tedi/components/misc/separator/separator.module.scss index cd5455a2a..ca779cd01 100644 --- a/src/tedi/components/misc/separator/separator.module.scss +++ b/src/tedi/components/misc/separator/separator.module.scss @@ -120,7 +120,6 @@ $dot-colors: ( } &.tedi-separator--dot-style-outlined:not(.tedi-separator--dot-only)::before { - background-color: var(--tedi-neutral-100); border-color: $color; } } @@ -225,7 +224,7 @@ $dot-colors: ( .tedi-separator--dot-style-outlined { &::before { - background-color: var(--tedi-neutral-100); + background-color: var(--general-surface-primary); border-style: solid; border-width: var(--separator-thickness, 1px); } From 1a45a6cf1f974eef6236ff36d077ef5fa959c09b Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:59:12 +0200 Subject: [PATCH 5/5] fix(separator): improve css #30 --- .../misc/separator/separator.module.scss | 18 +++++------------- .../components/misc/separator/separator.tsx | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/tedi/components/misc/separator/separator.module.scss b/src/tedi/components/misc/separator/separator.module.scss index ca779cd01..6d648458d 100644 --- a/src/tedi/components/misc/separator/separator.module.scss +++ b/src/tedi/components/misc/separator/separator.module.scss @@ -70,7 +70,7 @@ $dot-colors: ( } } -%separator-dotted-base { +.tedi-separator--dotted { &::before { position: absolute; content: ''; @@ -85,10 +85,6 @@ $dot-colors: ( } } -.tedi-separator--dotted { - @extend %separator-dotted-base; -} - .tedi-separator--dot-only { position: relative; display: inline-block; @@ -145,7 +141,7 @@ $dot-colors: ( right: 0; left: 0; margin: auto; - } @else if $pos == end { + } @else { right: 0; left: auto; } @@ -157,7 +153,7 @@ $dot-colors: ( top: 0; bottom: 0; margin: auto 0; - } @else if $pos == end { + } @else { top: auto; bottom: 0; } @@ -226,15 +222,11 @@ $dot-colors: ( &::before { background-color: var(--general-surface-primary); border-style: solid; - border-width: var(--separator-thickness, 1px); + border-width: var(--separator-thickness); } @each $name, $color in $dot-colors { - &.tedi-separator--#{$name}::before { - z-index: 10; - border-color: $color; - } - + &.tedi-separator--#{$name}::before, &.tedi-separator--dotted-#{$name}::before { z-index: 10; border-color: $color; diff --git a/src/tedi/components/misc/separator/separator.tsx b/src/tedi/components/misc/separator/separator.tsx index 94424b898..cabe5bd3c 100644 --- a/src/tedi/components/misc/separator/separator.tsx +++ b/src/tedi/components/misc/separator/separator.tsx @@ -38,7 +38,7 @@ export interface SeparatorSharedProps { isStretched?: boolean; /** * Semantic color token - * @default 'primary' + * @default primary */ color?: 'primary' | 'secondary' | 'accent'; /**