@frontify/fondue-components 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.eslintignore +4 -0
  2. package/.eslintrc.cjs +33 -0
  3. package/.prettierignore +1 -0
  4. package/.prettierrc +17 -0
  5. package/.storybook/DocumentationTemplate.mdx +25 -0
  6. package/.storybook/main.ts +29 -0
  7. package/.storybook/preview.ts +53 -0
  8. package/CHANGELOG.md +7 -0
  9. package/package.json +91 -0
  10. package/playwright/index.html +12 -0
  11. package/playwright/index.ts +3 -0
  12. package/playwright.config.ts +29 -0
  13. package/postcss.config.cjs +8 -0
  14. package/scripts/createNewComponent.ts +42 -0
  15. package/scripts/templates/__tests__/component.ct.tsx +25 -0
  16. package/scripts/templates/__tests__/component.spec.tsx +24 -0
  17. package/scripts/templates/component.stories.ts +35 -0
  18. package/scripts/templates/component.ts +25 -0
  19. package/scripts/templates/index.ts +15 -0
  20. package/scripts/templates/styles/componentStyles.tsx +16 -0
  21. package/scripts/transforms.ts +13 -0
  22. package/scripts/types.ts +7 -0
  23. package/src/components/Button/Button.stories.tsx +57 -0
  24. package/src/components/Button/Button.tsx +111 -0
  25. package/src/components/Button/styles/buttonStyles.ts +175 -0
  26. package/src/components/Button/styles/iconStyles.ts +152 -0
  27. package/src/components/Button/styles/textStyles.ts +149 -0
  28. package/src/components/Button/tests/Button.ct.tsx +61 -0
  29. package/src/components/Button/tests/Button.spec.tsx +34 -0
  30. package/src/components/Divider/Divider.stories.ts +47 -0
  31. package/src/components/Divider/Divider.tsx +69 -0
  32. package/src/components/Divider/__tests__/Divider.spec.tsx +88 -0
  33. package/src/components/LoadingBar/LoadingBar.stories.tsx +32 -0
  34. package/src/components/LoadingBar/LoadingBar.tsx +68 -0
  35. package/src/components/LoadingBar/styles/loadingBarStyles.ts +38 -0
  36. package/src/components/LoadingBar/tests/LoadingBar.ct.tsx +39 -0
  37. package/src/components/Tag/Tag.ct.tsx +16 -0
  38. package/src/components/Tag/Tag.stories.ts +29 -0
  39. package/src/components/Tag/Tag.tsx +18 -0
  40. package/src/components/Tag/__tests__/tag.spec.tsx +13 -0
  41. package/src/index.ts +8 -0
  42. package/src/setupTests.ts +19 -0
  43. package/src/styles.css +35 -0
  44. package/src/utilities/focusStyle.ts +12 -0
  45. package/src/utilities/styleUtilities.ts +19 -0
  46. package/src/utilities/tests/styleUtilities.spec.ts +114 -0
  47. package/tailwind.config.ts +148 -0
  48. package/tsconfig.json +26 -0
  49. package/tsconfig.node.json +21 -0
  50. package/vite.config.ts +67 -0
@@ -0,0 +1,47 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { type Meta, type StoryObj } from '@storybook/react';
4
+
5
+ import { Divider } from './Divider';
6
+
7
+ type Story = StoryObj<typeof Divider>;
8
+ const meta: Meta<typeof Divider> = {
9
+ component: Divider,
10
+ tags: ['autodocs'],
11
+ parameters: {
12
+ status: {
13
+ type: 'released',
14
+ },
15
+ },
16
+ };
17
+ export default meta;
18
+
19
+ export const Primary: Story = {
20
+ args: {
21
+ color: '#FF0000',
22
+ style: 'dashed',
23
+ height: 'medium',
24
+ },
25
+ };
26
+
27
+ export const Minimal: Story = {
28
+ args: {},
29
+ };
30
+
31
+ export const CustomColor: Story = {
32
+ args: {
33
+ color: '#FF0000',
34
+ },
35
+ };
36
+
37
+ export const CustomStyle: Story = {
38
+ args: {
39
+ style: 'dotted',
40
+ },
41
+ };
42
+
43
+ export const CustomHeight: Story = {
44
+ args: {
45
+ height: 'large',
46
+ },
47
+ };
@@ -0,0 +1,69 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { type ReactElement } from 'react';
4
+
5
+ export type DividerStyle = 'noline' | 'dashed' | 'solid' | 'dotted';
6
+
7
+ export type DividerHeight = 'small' | 'medium' | 'large';
8
+
9
+ export type DividerProps = {
10
+ style?: DividerStyle;
11
+ height?: DividerHeight;
12
+ color?: string;
13
+ vertical?: boolean;
14
+ 'data-test-id'?: string;
15
+ };
16
+
17
+ const styleMap = {
18
+ noline: 'tw-border-none',
19
+ dashed: 'tw-border-dashed',
20
+ solid: 'tw-border-solid',
21
+ dotted: 'tw-border-dotted',
22
+ };
23
+
24
+ const heightMap = {
25
+ small: 36,
26
+ medium: 60,
27
+ large: 96,
28
+ };
29
+
30
+ const DIVIDER_TEST_ID = 'fondue-divider';
31
+
32
+ export const Divider = ({
33
+ vertical = false,
34
+ style = 'solid',
35
+ height = 'small',
36
+ 'data-test-id': dataTestId = DIVIDER_TEST_ID,
37
+ color = '#CCC',
38
+ }: DividerProps): ReactElement => {
39
+ return vertical ? (
40
+ <div
41
+ aria-hidden="true"
42
+ className="tw-flex tw-self-stretch tw-mt-0 tw-mb-0 tw-items-center tw-justify-center"
43
+ data-test-id={dataTestId}
44
+ style={{
45
+ marginLeft: heightMap[height] / 2,
46
+ marginRight: heightMap[height] / 2,
47
+ }}
48
+ >
49
+ <div
50
+ className={`tw-w-px tw-h-full tw-border-r tw-m-0 ${styleMap[style]}`}
51
+ style={{ borderRightColor: color }}
52
+ data-test-id="fondue-divider-line"
53
+ />
54
+ </div>
55
+ ) : (
56
+ <div
57
+ aria-hidden="true"
58
+ className="tw-flex tw-items-center tw-w-full"
59
+ style={{ height: heightMap[height] }}
60
+ data-test-id={dataTestId}
61
+ >
62
+ <hr
63
+ className={`tw-border-t tw-m-0 tw-w-full ${styleMap[style]}`}
64
+ style={{ borderTopColor: color }}
65
+ data-test-id="fondue-divider-line"
66
+ />
67
+ </div>
68
+ );
69
+ };
@@ -0,0 +1,88 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { render } from '@testing-library/react';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { Divider } from '../Divider';
7
+
8
+ const DEFAULT_COLOR_HEX = '#CCC';
9
+ const COLOR_HEX = '#4065AE';
10
+ const DIVIDER_SELECTOR = 'fondue-divider';
11
+ const DIVIDER_LINE_SELECTOR = 'fondue-divider-line';
12
+
13
+ describe('Divider component', () => {
14
+ it('should loads and displays divider', () => {
15
+ const { getByTestId } = render(<Divider />);
16
+ const divider = getByTestId(DIVIDER_LINE_SELECTOR);
17
+
18
+ expect(divider).toHaveStyle({ borderTopColor: DEFAULT_COLOR_HEX });
19
+ });
20
+ it('should have correct color', () => {
21
+ const { getByTestId } = render(<Divider color={COLOR_HEX} />);
22
+ const divider = getByTestId(DIVIDER_LINE_SELECTOR);
23
+
24
+ expect(divider).toHaveStyle({ borderTopColor: COLOR_HEX });
25
+ });
26
+ it('should allow for the height to be set to Small', () => {
27
+ const { getByTestId } = render(<Divider height={'small'} />);
28
+ const divider = getByTestId(DIVIDER_SELECTOR);
29
+
30
+ expect(divider).toHaveStyle({ height: '36px' });
31
+ });
32
+
33
+ it('should allow for the height to be set to Medium', () => {
34
+ const { getByTestId } = render(<Divider height={'medium'} />);
35
+ const divider = getByTestId(DIVIDER_SELECTOR);
36
+
37
+ expect(divider).toHaveStyle({ height: '60px' });
38
+ });
39
+
40
+ it('should allow for the height to be set to Large', () => {
41
+ const { getByTestId } = render(<Divider height={'large'} />);
42
+ const divider = getByTestId(DIVIDER_SELECTOR);
43
+
44
+ expect(divider).toHaveStyle({ height: '96px' });
45
+ });
46
+
47
+ it('should allow for the divider border style to be dashed', () => {
48
+ const { getByTestId } = render(<Divider style="dashed" />);
49
+ const divider = getByTestId(DIVIDER_LINE_SELECTOR);
50
+
51
+ expect(divider).toHaveClass('tw-border-dashed');
52
+ });
53
+
54
+ it('should allow for the divider to have no border', () => {
55
+ const { getByTestId } = render(<Divider style="noline" />);
56
+ const divider = getByTestId(DIVIDER_LINE_SELECTOR);
57
+
58
+ expect(divider).toHaveClass('tw-border-none');
59
+ });
60
+
61
+ it('should allow for the divider to have solid border', () => {
62
+ const { getByTestId } = render(<Divider style="solid" />);
63
+ const divider = getByTestId(DIVIDER_LINE_SELECTOR);
64
+
65
+ expect(divider).toHaveClass('tw-border-solid');
66
+ });
67
+
68
+ it('should allow for the divider to have dotted border', () => {
69
+ const { getByTestId } = render(<Divider style="dotted" />);
70
+ const divider = getByTestId(DIVIDER_LINE_SELECTOR);
71
+
72
+ expect(divider).toHaveClass('tw-border-dotted');
73
+ });
74
+
75
+ it('should allow for the divider to be vertical', () => {
76
+ const { getByTestId } = render(<Divider vertical={true} />);
77
+ const divider = getByTestId(DIVIDER_LINE_SELECTOR);
78
+
79
+ expect(divider).toHaveClass('tw-border-r');
80
+ });
81
+
82
+ it('should allow for the divider to have a custom test-id', () => {
83
+ const { getByTestId } = render(<Divider data-test-id="custom-divider-test-id" />);
84
+ const divider = getByTestId('custom-divider-test-id');
85
+
86
+ expect(divider);
87
+ });
88
+ });
@@ -0,0 +1,32 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { type Meta, type StoryObj } from '@storybook/react';
4
+
5
+ import { LoadingBar } from './LoadingBar';
6
+
7
+ type Story = StoryObj<typeof LoadingBar>;
8
+ const meta: Meta<typeof LoadingBar> = {
9
+ component: LoadingBar,
10
+ tags: ['autodocs'],
11
+ parameters: {
12
+ status: {
13
+ type: 'in_progress',
14
+ },
15
+ },
16
+ args: {
17
+ value: 42,
18
+ max: 100,
19
+ rounded: true,
20
+ 'aria-label': 'Fondue Loading Bar',
21
+ },
22
+ };
23
+
24
+ export default meta;
25
+
26
+ export const WithDefinedValue: Story = {};
27
+
28
+ export const Indeterminate: Story = {
29
+ args: {
30
+ value: null,
31
+ },
32
+ };
@@ -0,0 +1,68 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import * as ProgressRadixPrimitive from '@radix-ui/react-progress';
4
+ import { type CSSProperties, forwardRef, type ElementRef } from 'react';
5
+
6
+ import { loadingBarContainerStyles, loadingBarStyles } from './styles/loadingBarStyles';
7
+
8
+ export type LoadingBarProps = {
9
+ /**
10
+ * The current value of the loading bar. If `null`, the loading bar will be in an indeterminate state.
11
+ * @default null
12
+ */
13
+ value: number | null;
14
+ /**
15
+ * @default 100
16
+ */
17
+ max?: number;
18
+ /**
19
+ * @default 'fondue-loading-bar'
20
+ */
21
+ 'data-test-id'?: string;
22
+ /**
23
+ * @default true
24
+ */
25
+ rounded?: boolean;
26
+ /**
27
+ * @default 'default'
28
+ */
29
+ style?: 'default' | 'positive' | 'negative';
30
+ /**
31
+ * @default 'medium'
32
+ */
33
+ size?: 'small' | 'medium' | 'large' | 'x-large';
34
+ getValueLabel?: (value: number, max: number) => string;
35
+ } & ({ 'aria-label': string } | { 'aria-labelledby': string });
36
+
37
+ export const LoadingBar = forwardRef<ElementRef<typeof ProgressRadixPrimitive.Root>, LoadingBarProps>(
38
+ (
39
+ {
40
+ value,
41
+ max = 100,
42
+ rounded = true,
43
+ style = 'default',
44
+ size = 'medium',
45
+ 'data-test-id': dataTestId = 'fondue-loading-bar',
46
+ ...props
47
+ },
48
+ ref,
49
+ ) => {
50
+ return (
51
+ <ProgressRadixPrimitive.Root
52
+ ref={ref}
53
+ data-test-id={dataTestId}
54
+ className={loadingBarContainerStyles({ rounded, size, style })}
55
+ aria-busy={value !== max}
56
+ value={value}
57
+ max={max}
58
+ style={value ? ({ '--loading-bar-value': `${value}%` } as CSSProperties) : {}}
59
+ {...props}
60
+ >
61
+ <ProgressRadixPrimitive.Indicator
62
+ className={loadingBarStyles({ style, indeterminateState: value === null })}
63
+ />
64
+ </ProgressRadixPrimitive.Root>
65
+ );
66
+ },
67
+ );
68
+ LoadingBar.displayName = 'LoadingBar';
@@ -0,0 +1,38 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { sv } from '#/utilities/styleUtilities';
4
+
5
+ export const loadingBarContainerStyles = sv({
6
+ base: 'tw-relative tw-w-full tw-overflow-hidden',
7
+ variants: {
8
+ rounded: {
9
+ true: 'tw-rounded',
10
+ },
11
+ size: {
12
+ small: 'tw-h-1',
13
+ medium: 'tw-h-2',
14
+ large: 'tw-h-3',
15
+ 'x-large': 'tw-h-4',
16
+ },
17
+ style: {
18
+ default: 'tw-bg-box-selected',
19
+ positive: 'tw-bg-box-positive',
20
+ negative: 'tw-bg-box-negative',
21
+ },
22
+ },
23
+ });
24
+
25
+ export const loadingBarStyles = sv({
26
+ base: 'tw-h-full tw-w-full',
27
+ variants: {
28
+ style: {
29
+ default: 'tw-bg-text-interactive',
30
+ positive: 'tw-bg-text-positive',
31
+ negative: 'tw-bg-text-negative',
32
+ },
33
+ indeterminateState: {
34
+ true: 'tw-animate-loading-bar-infinite tw-origin-left-right',
35
+ false: 'tw-transition-all tw--translate-x-[calc(100%-var(--loading-bar-value))]',
36
+ },
37
+ },
38
+ });
@@ -0,0 +1,39 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { expect, test } from '@playwright/experimental-ct-react';
4
+
5
+ import { LoadingBar } from '../LoadingBar';
6
+
7
+ test('should render with the correct value', async ({ mount }) => {
8
+ const container = await mount(<LoadingBar value={60} max={120} aria-label="Fondue Loading Bar" />);
9
+
10
+ await expect(container).toHaveAttribute('role', 'progressbar');
11
+ await expect(container).toHaveAttribute('aria-valuenow', '60');
12
+ await expect(container).toHaveAttribute('aria-valuemax', '120');
13
+ await expect(container).toHaveAttribute('aria-valuetext', '50%');
14
+ });
15
+
16
+ test('should render with the correct computed label', async ({ mount }) => {
17
+ const container = await mount(
18
+ <LoadingBar
19
+ value={60}
20
+ max={120}
21
+ aria-label="Fondue Loading Bar"
22
+ getValueLabel={(value, max) => {
23
+ return `${value} of ${max}`;
24
+ }}
25
+ />,
26
+ );
27
+
28
+ await expect(container).toHaveAttribute('aria-label', 'Fondue Loading Bar');
29
+
30
+ // FIXME: Playwright somehow return `undefined` when the component executes `getValueLabel`, bug?
31
+ // await expect(container).toHaveAttribute('aria-valuetext', '60 of 120');
32
+ });
33
+
34
+ test('should render in indeterminate state', async ({ mount }) => {
35
+ const container = await mount(<LoadingBar value={null} aria-label="Fondue Loading Bar" />);
36
+
37
+ await expect(container).toHaveAttribute('aria-busy', 'true');
38
+ await expect(container).not.toHaveAttribute('aria-valuetext');
39
+ });
@@ -0,0 +1,16 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { test, expect } from '@playwright/experimental-ct-react';
4
+
5
+ import { Tag } from './Tag';
6
+
7
+ test('should render without error', async ({ mount }) => {
8
+ const component = await mount(<Tag>Test</Tag>);
9
+ await expect(component).toContainText('Test');
10
+ });
11
+
12
+ test('should allow for the tag to have a border color and background color', async ({ mount }) => {
13
+ const component = await mount(<Tag>Test</Tag>);
14
+ await expect(component).toHaveCSS('background-color', 'rgb(240, 234, 250)');
15
+ await expect(component).toHaveCSS('border-color', 'rgb(124, 87, 255)');
16
+ });
@@ -0,0 +1,29 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { type Meta, type StoryObj } from '@storybook/react';
4
+
5
+ import { Tag } from './Tag';
6
+
7
+ type Story = StoryObj<typeof Tag>;
8
+ const meta: Meta<typeof Tag> = {
9
+ component: Tag,
10
+ tags: ['autodocs'],
11
+ parameters: {
12
+ status: {
13
+ type: 'planned',
14
+ },
15
+ },
16
+ };
17
+ export default meta;
18
+
19
+ export const Primary: Story = {
20
+ args: {
21
+ children: 'Tag',
22
+ },
23
+ };
24
+
25
+ export const Secondary: Story = {
26
+ args: {
27
+ children: 'Tag',
28
+ },
29
+ };
@@ -0,0 +1,18 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { type ReactNode } from 'react';
4
+
5
+ type TagProps = {
6
+ children: ReactNode;
7
+ };
8
+
9
+ export const Tag = ({ children }: TagProps) => {
10
+ return (
11
+ <div
12
+ className="tw-text-text tw-bg-box-selected tw-border-box-selected-strong tw-border-2 tw-p-2 tw-rounded tw-w-fit"
13
+ data-test-id="fondue-tag"
14
+ >
15
+ {children}
16
+ </div>
17
+ );
18
+ };
@@ -0,0 +1,13 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { render } from '@testing-library/react';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { Tag } from '../Tag';
7
+
8
+ describe('Tag', () => {
9
+ it('should render', () => {
10
+ const { getByTestId } = render(<Tag>Tag</Tag>);
11
+ expect(getByTestId('fondue-tag')).toBeInTheDocument();
12
+ });
13
+ });
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import './styles.css';
4
+
5
+ export { Button } from './components/Button/Button';
6
+ export { Divider } from './components/Divider/Divider';
7
+ export { LoadingBar } from './components/LoadingBar/LoadingBar';
8
+ export { Tag } from './components/Tag/Tag';
@@ -0,0 +1,19 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { type TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';
4
+ import '@testing-library/jest-dom/vitest';
5
+ import { cleanup, configure } from '@testing-library/react';
6
+ import { afterEach, beforeAll } from 'vitest';
7
+
8
+ declare module 'vitest' {
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ interface Assertion<T = any> extends jest.Matchers<void, T>, TestingLibraryMatchers<T, void> {}
11
+ }
12
+
13
+ beforeAll(() => {
14
+ configure({ testIdAttribute: 'data-test-id' });
15
+ });
16
+
17
+ afterEach(() => {
18
+ cleanup();
19
+ });
package/src/styles.css ADDED
@@ -0,0 +1,35 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ @import '@frontify/fondue-tokens/styles';
4
+
5
+ @tailwind base;
6
+ @tailwind components;
7
+ @tailwind utilities;
8
+
9
+ @layer utilities {
10
+ input.tw-hide-input-arrows::-webkit-outer-spin-button,
11
+ input.tw-hide-input-arrows::-webkit-inner-spin-button {
12
+ -webkit-appearance: none;
13
+ margin: 0;
14
+ }
15
+
16
+ input[type='number'].tw-hide-input-arrows {
17
+ -moz-appearance: textfield;
18
+ }
19
+
20
+ .tw-popper-container[data-popper-placement^='top'] > .tw-popper-arrow {
21
+ bottom: -4px;
22
+ }
23
+
24
+ .tw-popper-container[data-popper-placement^='bottom'] > .tw-popper-arrow {
25
+ top: -4px;
26
+ }
27
+
28
+ .tw-popper-container[data-popper-placement^='left'] > .tw-popper-arrow {
29
+ right: -4px;
30
+ }
31
+
32
+ .tw-popper-container[data-popper-placement^='right'] > .tw-popper-arrow {
33
+ left: -4px;
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { cn } from './styleUtilities';
4
+
5
+ export const FOCUS_STYLE = 'tw-ring-4 tw-ring-blue tw-ring-offset-2 dark:tw-ring-offset-black tw-outline-none';
6
+ export const FOCUS_STYLE_NO_OFFSET = 'tw-ring-4 tw-ring-blue dark:tw-ring-offset-black tw-outline-none';
7
+ export const FOCUS_VISIBLE_STYLE =
8
+ 'focus-visible:tw-ring-4 focus-visible:tw-ring-blue focus-visible:tw-ring-offset-2 focus-visible:dark:tw-ring-offset-black focus-visible:tw-outline-none';
9
+ export const FOCUS_STYLE_INSET = cn([FOCUS_STYLE, 'tw-ring-inset']);
10
+ export const FOCUS_VISIBLE_STYLE_INSET = cn([FOCUS_VISIBLE_STYLE, 'tw-ring-inset']);
11
+ export const FOCUS_WITHIN_STYLE =
12
+ 'focus-within:tw-ring-4 focus-within:tw-ring-blue focus-within:tw-ring-offset-2 focus-within:dark:tw-ring-offset-black focus-within:tw-outline-none';
@@ -0,0 +1,19 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { extendTailwindMerge } from 'tailwind-merge';
4
+ import { type TV, tv } from 'tailwind-variants';
5
+
6
+ type ClassNameValue = ClassNameArray | string | null | undefined | 0 | false;
7
+ type ClassNameArray = ClassNameValue[];
8
+
9
+ const customTwMerge = extendTailwindMerge({
10
+ prefix: 'tw-',
11
+ });
12
+
13
+ export const cn = (...classLists: ClassNameValue[]): string => {
14
+ return customTwMerge(...classLists);
15
+ };
16
+
17
+ export const sv: TV = (variants) => {
18
+ return tv(variants);
19
+ };
@@ -0,0 +1,114 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { cn, sv } from '#/utilities/styleUtilities';
6
+
7
+ describe('class merging utility', () => {
8
+ it('concatinates strings from list', () => {
9
+ const className = cn('tw-flex', 'tw-p-8 tw-justiify-center', 'tw-items-start');
10
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
11
+ });
12
+
13
+ it('allows for dynamic inputs', () => {
14
+ // eslint-disable-next-line no-constant-condition
15
+ const className = cn('tw-flex', 'tw-p-8 tw-justiify-center', true ? 'tw-items-start' : 'kiwi');
16
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
17
+ });
18
+
19
+ it('removes whitespaces', () => {
20
+ const className = cn('tw-flex', 'tw-p-8 tw-justiify-center', ' tw-items-start');
21
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
22
+ });
23
+
24
+ it('supports arrays', () => {
25
+ const className = cn('tw-flex', ['tw-p-8', ' tw-justiify-center'], 'tw-items-start');
26
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
27
+ });
28
+
29
+ it('removes empty strings', () => {
30
+ const className = cn('tw-flex', 'tw-p-8', ' ', 'tw-justiify-center', 'tw-items-start');
31
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
32
+ });
33
+
34
+ it('removes null values', () => {
35
+ const className = cn('tw-flex', 'tw-p-8', null, 'tw-justiify-center', 'tw-items-start');
36
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
37
+ });
38
+
39
+ it('removes undefined values', () => {
40
+ const className = cn('tw-flex', 'tw-p-8', undefined, 'tw-justiify-center', 'tw-items-start');
41
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
42
+ });
43
+
44
+ it('removes false values', () => {
45
+ const className = cn('tw-flex', 'tw-p-8', false, 'tw-justiify-center', 'tw-items-start');
46
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
47
+ });
48
+
49
+ it('removes 0 values', () => {
50
+ const className = cn('tw-flex', 'tw-p-8', 0, 'tw-justiify-center', 'tw-items-start');
51
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
52
+ });
53
+
54
+ it('removes empty arrays', () => {
55
+ const className = cn('tw-flex', 'tw-p-8', [], 'tw-justiify-center', 'tw-items-start');
56
+ expect(className).toBe('tw-flex tw-p-8 tw-justiify-center tw-items-start');
57
+ });
58
+
59
+ it('removes overridden classes', () => {
60
+ const className = cn('tw-flex', 'tw-p-8', 'tw-justiify-center', 'tw-items-start', 'tw-flex', 'tw-p-2');
61
+ expect(className).toBe('tw-justiify-center tw-items-start tw-flex tw-p-2');
62
+ });
63
+ });
64
+
65
+ describe('tailwind variants utility', () => {
66
+ const styledDiv = sv({
67
+ base: 'tw-flex tw-flex-col',
68
+ variants: {
69
+ size: {
70
+ small: 'tw-px-2 tw-h-6',
71
+ medium: 'tw-px-4 tw-h-9',
72
+ large: 'tw-px-6 tw-h-12',
73
+ },
74
+ emphasis: {
75
+ default: '',
76
+ weak: '',
77
+ strong: '',
78
+ },
79
+ alignment: {
80
+ start: 'tw-items-start',
81
+ center: 'tw-items-center',
82
+ end: 'tw-items-end',
83
+ },
84
+ },
85
+ compoundVariants: [
86
+ {
87
+ size: 'large',
88
+ alignment: 'end',
89
+ class: 'tw-border-2 tw-border-black',
90
+ },
91
+ ],
92
+ });
93
+
94
+ it('returns base styles', () => {
95
+ const className = styledDiv();
96
+ expect(className).toBe('tw-flex tw-flex-col');
97
+ });
98
+
99
+ it('applies variants', () => {
100
+ const className = styledDiv({ alignment: 'start', size: 'medium' });
101
+ expect(className).toBe('tw-flex tw-flex-col tw-px-4 tw-h-9 tw-items-start');
102
+ });
103
+
104
+ it('ignores invalid variants', () => {
105
+ // @ts-expect-error Wrong value on purpose for the test
106
+ const className = styledDiv({ alignment: 'weird', size: 'medium' });
107
+ expect(className).toBe('tw-flex tw-flex-col tw-px-4 tw-h-9');
108
+ });
109
+
110
+ it('applies compound variants', () => {
111
+ const className = styledDiv({ alignment: 'end', size: 'large' });
112
+ expect(className).toBe('tw-flex tw-flex-col tw-px-6 tw-h-12 tw-items-end tw-border-2 tw-border-black');
113
+ });
114
+ });