@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.
- package/.eslintignore +4 -0
- package/.eslintrc.cjs +33 -0
- package/.prettierignore +1 -0
- package/.prettierrc +17 -0
- package/.storybook/DocumentationTemplate.mdx +25 -0
- package/.storybook/main.ts +29 -0
- package/.storybook/preview.ts +53 -0
- package/CHANGELOG.md +7 -0
- package/package.json +91 -0
- package/playwright/index.html +12 -0
- package/playwright/index.ts +3 -0
- package/playwright.config.ts +29 -0
- package/postcss.config.cjs +8 -0
- package/scripts/createNewComponent.ts +42 -0
- package/scripts/templates/__tests__/component.ct.tsx +25 -0
- package/scripts/templates/__tests__/component.spec.tsx +24 -0
- package/scripts/templates/component.stories.ts +35 -0
- package/scripts/templates/component.ts +25 -0
- package/scripts/templates/index.ts +15 -0
- package/scripts/templates/styles/componentStyles.tsx +16 -0
- package/scripts/transforms.ts +13 -0
- package/scripts/types.ts +7 -0
- package/src/components/Button/Button.stories.tsx +57 -0
- package/src/components/Button/Button.tsx +111 -0
- package/src/components/Button/styles/buttonStyles.ts +175 -0
- package/src/components/Button/styles/iconStyles.ts +152 -0
- package/src/components/Button/styles/textStyles.ts +149 -0
- package/src/components/Button/tests/Button.ct.tsx +61 -0
- package/src/components/Button/tests/Button.spec.tsx +34 -0
- package/src/components/Divider/Divider.stories.ts +47 -0
- package/src/components/Divider/Divider.tsx +69 -0
- package/src/components/Divider/__tests__/Divider.spec.tsx +88 -0
- package/src/components/LoadingBar/LoadingBar.stories.tsx +32 -0
- package/src/components/LoadingBar/LoadingBar.tsx +68 -0
- package/src/components/LoadingBar/styles/loadingBarStyles.ts +38 -0
- package/src/components/LoadingBar/tests/LoadingBar.ct.tsx +39 -0
- package/src/components/Tag/Tag.ct.tsx +16 -0
- package/src/components/Tag/Tag.stories.ts +29 -0
- package/src/components/Tag/Tag.tsx +18 -0
- package/src/components/Tag/__tests__/tag.spec.tsx +13 -0
- package/src/index.ts +8 -0
- package/src/setupTests.ts +19 -0
- package/src/styles.css +35 -0
- package/src/utilities/focusStyle.ts +12 -0
- package/src/utilities/styleUtilities.ts +19 -0
- package/src/utilities/tests/styleUtilities.spec.ts +114 -0
- package/tailwind.config.ts +148 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +21 -0
- 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
|
+
});
|