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';
/**