@fabio.caffarello/react-design-system 1.6.0 → 1.7.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/dist/index.cjs +29 -4
- package/dist/index.js +1866 -716
- package/dist/ui/atoms/Button/Button.d.ts +28 -5
- package/dist/ui/atoms/Button/Button.stories.d.ts +11 -3
- package/dist/ui/atoms/Checkbox/Checkbox.d.ts +24 -0
- package/dist/ui/atoms/Checkbox/Checkbox.stories.d.ts +10 -0
- package/dist/ui/atoms/Checkbox/Checkbox.test.d.ts +1 -0
- package/dist/ui/atoms/Collapsible/Collapsible.d.ts +29 -0
- package/dist/ui/atoms/Collapsible/Collapsible.stories.d.ts +9 -0
- package/dist/ui/atoms/Collapsible/Collapsible.test.d.ts +1 -0
- package/dist/ui/atoms/Input/Input.d.ts +28 -4
- package/dist/ui/atoms/Input/Input.stories.d.ts +8 -3
- package/dist/ui/atoms/Radio/Radio.d.ts +26 -0
- package/dist/ui/atoms/Radio/Radio.stories.d.ts +10 -0
- package/dist/ui/atoms/Radio/Radio.test.d.ts +1 -0
- package/dist/ui/atoms/SidebarItem/SidebarItem.d.ts +3 -1
- package/dist/ui/atoms/SidebarItem/SidebarItem.stories.d.ts +3 -0
- package/dist/ui/atoms/index.d.ts +7 -0
- package/dist/ui/hooks/useCollapsible.d.ts +27 -0
- package/dist/ui/index.d.ts +13 -0
- package/dist/ui/molecules/InputWithLabel/InputWithLabel.d.ts +4 -2
- package/dist/ui/molecules/SidebarGroup/SidebarGroup.d.ts +8 -1
- package/dist/ui/molecules/SidebarGroup/SidebarGroup.stories.d.ts +11 -0
- package/dist/ui/molecules/SidebarGroup/SidebarGroup.test.d.ts +1 -0
- package/dist/ui/providers/ThemeProvider.d.ts +34 -0
- package/dist/ui/tokens/breakpoints.d.ts +36 -0
- package/dist/ui/tokens/colors.d.ts +89 -0
- package/dist/ui/tokens/sidebar.d.ts +48 -0
- package/dist/ui/tokens/spacing.d.ts +53 -0
- package/dist/ui/tokens/themes/dark.d.ts +38 -0
- package/dist/ui/tokens/themes/light.d.ts +38 -0
- package/dist/ui/tokens/tokens.factory.d.ts +57 -0
- package/dist/ui/tokens/typography.d.ts +90 -0
- package/package.json +3 -2
- package/src/ui/atoms/Button/Button.stories.tsx +77 -7
- package/src/ui/atoms/Button/Button.tsx +176 -28
- package/src/ui/atoms/Checkbox/Checkbox.stories.tsx +61 -0
- package/src/ui/atoms/Checkbox/Checkbox.test.tsx +32 -0
- package/src/ui/atoms/Checkbox/Checkbox.tsx +103 -0
- package/src/ui/atoms/Collapsible/Collapsible.tsx +2 -2
- package/src/ui/atoms/Input/Input.stories.tsx +67 -6
- package/src/ui/atoms/Input/Input.tsx +117 -14
- package/src/ui/atoms/Radio/Radio.stories.tsx +72 -0
- package/src/ui/atoms/Radio/Radio.test.tsx +32 -0
- package/src/ui/atoms/Radio/Radio.tsx +104 -0
- package/src/ui/atoms/index.ts +7 -0
- package/src/ui/index.ts +14 -0
- package/src/ui/molecules/InputWithLabel/InputWithLabel.tsx +5 -4
- package/src/ui/molecules/SidebarGroup/SidebarGroup.tsx +30 -38
- package/src/ui/providers/ThemeProvider.tsx +105 -0
- package/src/ui/tokens/breakpoints.ts +71 -0
- package/src/ui/tokens/colors.ts +250 -0
- package/src/ui/tokens/sidebar.ts +9 -3
- package/src/ui/tokens/spacing.ts +127 -0
- package/src/ui/tokens/themes/dark.ts +18 -0
- package/src/ui/tokens/themes/light.ts +18 -0
- package/src/ui/tokens/tokens.factory.ts +117 -0
- package/src/ui/tokens/typography.ts +191 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { InputHTMLAttributes, ReactNode } from 'react';
|
|
4
|
+
import { getTypographyClasses } from '../../tokens/typography';
|
|
5
|
+
|
|
6
|
+
export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
7
|
+
label?: ReactNode;
|
|
8
|
+
error?: boolean;
|
|
9
|
+
helperText?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checkbox Component
|
|
14
|
+
*
|
|
15
|
+
* A styled checkbox input component.
|
|
16
|
+
* Follows Atomic Design principles as an Atom component.
|
|
17
|
+
* Uses Composite Pattern when combined with Label and ErrorMessage.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <Checkbox
|
|
22
|
+
* id="terms"
|
|
23
|
+
* label="I agree to the terms"
|
|
24
|
+
* checked={checked}
|
|
25
|
+
* onChange={handleChange}
|
|
26
|
+
* />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export default function Checkbox({
|
|
30
|
+
id,
|
|
31
|
+
label,
|
|
32
|
+
error = false,
|
|
33
|
+
helperText,
|
|
34
|
+
className = '',
|
|
35
|
+
disabled = false,
|
|
36
|
+
...props
|
|
37
|
+
}: CheckboxProps) {
|
|
38
|
+
const checkboxId = id || `checkbox-${Math.random().toString(36).substr(2, 9)}`;
|
|
39
|
+
const errorId = error ? `${checkboxId}-error` : undefined;
|
|
40
|
+
const helperId = helperText ? `${checkboxId}-helper` : undefined;
|
|
41
|
+
|
|
42
|
+
const baseClasses = [
|
|
43
|
+
'h-4',
|
|
44
|
+
'w-4',
|
|
45
|
+
'rounded',
|
|
46
|
+
'border-gray-300',
|
|
47
|
+
'text-indigo-600',
|
|
48
|
+
'focus:ring-2',
|
|
49
|
+
'focus:ring-indigo-500',
|
|
50
|
+
'focus:ring-offset-2',
|
|
51
|
+
'disabled:opacity-50',
|
|
52
|
+
'disabled:cursor-not-allowed',
|
|
53
|
+
'cursor-pointer',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const errorClasses = error
|
|
57
|
+
? 'border-red-500 focus:ring-red-500'
|
|
58
|
+
: '';
|
|
59
|
+
|
|
60
|
+
const checkboxClasses = [
|
|
61
|
+
...baseClasses,
|
|
62
|
+
errorClasses,
|
|
63
|
+
].filter(Boolean).join(' ');
|
|
64
|
+
|
|
65
|
+
const labelClasses = [
|
|
66
|
+
getTypographyClasses('label'),
|
|
67
|
+
'ml-2',
|
|
68
|
+
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
|
69
|
+
].filter(Boolean).join(' ');
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className={`flex flex-col my-2 ${className}`}>
|
|
73
|
+
<div className="flex items-center">
|
|
74
|
+
<input
|
|
75
|
+
type="checkbox"
|
|
76
|
+
id={checkboxId}
|
|
77
|
+
className={checkboxClasses}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
aria-invalid={error}
|
|
80
|
+
aria-describedby={errorId || helperId}
|
|
81
|
+
{...props}
|
|
82
|
+
/>
|
|
83
|
+
{label && (
|
|
84
|
+
<label
|
|
85
|
+
htmlFor={checkboxId}
|
|
86
|
+
className={labelClasses}
|
|
87
|
+
>
|
|
88
|
+
{label}
|
|
89
|
+
</label>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
{(error || helperText) && (
|
|
93
|
+
<div
|
|
94
|
+
id={errorId || helperId}
|
|
95
|
+
className={`mt-1 ${getTypographyClasses('caption')} ${error ? 'text-red-600' : 'text-gray-500'}`}
|
|
96
|
+
role={error ? 'alert' : undefined}
|
|
97
|
+
>
|
|
98
|
+
{error || helperText}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -43,7 +43,7 @@ export default function Collapsible({
|
|
|
43
43
|
className = '',
|
|
44
44
|
...props
|
|
45
45
|
}: CollapsibleProps) {
|
|
46
|
-
const { isOpen, toggle
|
|
46
|
+
const { isOpen, toggle } = useCollapsible({
|
|
47
47
|
defaultOpen,
|
|
48
48
|
open,
|
|
49
49
|
onOpenChange,
|
|
@@ -94,7 +94,7 @@ export default function Collapsible({
|
|
|
94
94
|
aria-expanded={isOpen}
|
|
95
95
|
aria-controls={contentId}
|
|
96
96
|
aria-disabled={disabled}
|
|
97
|
-
className="w-full text-left"
|
|
97
|
+
className="w-full text-left focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 rounded-md"
|
|
98
98
|
>
|
|
99
99
|
{trigger}
|
|
100
100
|
</button>
|
|
@@ -1,15 +1,76 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from
|
|
2
|
-
import Input from
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import Input from './Input';
|
|
3
3
|
|
|
4
4
|
const meta: Meta<typeof Input> = {
|
|
5
|
-
title:
|
|
5
|
+
title: 'Atoms/Input',
|
|
6
6
|
component: Input,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
label: {
|
|
10
|
+
control: 'text',
|
|
11
|
+
},
|
|
12
|
+
error: {
|
|
13
|
+
control: 'boolean',
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: ['sm', 'md', 'lg'],
|
|
18
|
+
},
|
|
19
|
+
variant: {
|
|
20
|
+
control: 'select',
|
|
21
|
+
options: ['default', 'outlined', 'filled'],
|
|
22
|
+
},
|
|
23
|
+
disabled: {
|
|
24
|
+
control: 'boolean',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
7
27
|
};
|
|
8
28
|
|
|
9
|
-
export
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<typeof Input>;
|
|
31
|
+
|
|
32
|
+
export const Default: Story = {
|
|
10
33
|
args: {
|
|
11
|
-
|
|
34
|
+
label: 'Email',
|
|
35
|
+
type: 'email',
|
|
36
|
+
placeholder: 'Enter your email',
|
|
12
37
|
},
|
|
13
38
|
};
|
|
14
39
|
|
|
15
|
-
export
|
|
40
|
+
export const WithError: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
label: 'Email',
|
|
43
|
+
type: 'email',
|
|
44
|
+
placeholder: 'Enter your email',
|
|
45
|
+
error: true,
|
|
46
|
+
helperText: 'Please enter a valid email address',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Sizes: Story = {
|
|
51
|
+
render: () => (
|
|
52
|
+
<div className="space-y-4">
|
|
53
|
+
<Input label="Small" size="sm" placeholder="Small input" />
|
|
54
|
+
<Input label="Medium" size="md" placeholder="Medium input" />
|
|
55
|
+
<Input label="Large" size="lg" placeholder="Large input" />
|
|
56
|
+
</div>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Variants: Story = {
|
|
61
|
+
render: () => (
|
|
62
|
+
<div className="space-y-4">
|
|
63
|
+
<Input label="Default" variant="default" placeholder="Default variant" />
|
|
64
|
+
<Input label="Outlined" variant="outlined" placeholder="Outlined variant" />
|
|
65
|
+
<Input label="Filled" variant="filled" placeholder="Filled variant" />
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Disabled: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
label: 'Disabled Input',
|
|
73
|
+
placeholder: 'This input is disabled',
|
|
74
|
+
disabled: true,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -1,19 +1,122 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
|
|
3
|
-
type
|
|
3
|
+
import type { InputHTMLAttributes, ReactNode } from 'react';
|
|
4
|
+
import { getTypographyClasses } from '../../tokens/typography';
|
|
5
|
+
import { getColorClass } from '../../tokens/colors';
|
|
4
6
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
8
|
+
label?: ReactNode;
|
|
9
|
+
error?: boolean;
|
|
10
|
+
helperText?: string;
|
|
11
|
+
size?: 'sm' | 'md' | 'lg';
|
|
12
|
+
variant?: 'default' | 'outlined' | 'filled';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Input Component
|
|
17
|
+
*
|
|
18
|
+
* A styled text input component with label and error support.
|
|
19
|
+
* Follows Atomic Design principles as an Atom component.
|
|
20
|
+
* Uses Composite Pattern when combined with Label and ErrorMessage.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <Input
|
|
25
|
+
* id="email"
|
|
26
|
+
* label="Email"
|
|
27
|
+
* type="email"
|
|
28
|
+
* placeholder="Enter your email"
|
|
29
|
+
* error={hasError}
|
|
30
|
+
* helperText={errorMessage}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export default function Input({
|
|
35
|
+
id,
|
|
36
|
+
label,
|
|
37
|
+
error = false,
|
|
38
|
+
helperText,
|
|
39
|
+
size = 'md',
|
|
40
|
+
variant = 'outlined',
|
|
41
|
+
className = '',
|
|
42
|
+
disabled = false,
|
|
43
|
+
...props
|
|
44
|
+
}: InputProps) {
|
|
45
|
+
const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`;
|
|
46
|
+
const errorId = error ? `${inputId}-error` : undefined;
|
|
47
|
+
const helperId = helperText ? `${inputId}-helper` : undefined;
|
|
48
|
+
|
|
49
|
+
// Size classes
|
|
50
|
+
const sizeClasses = {
|
|
51
|
+
sm: 'h-8 text-sm px-3',
|
|
52
|
+
md: 'h-10 text-base px-4',
|
|
53
|
+
lg: 'h-12 text-lg px-5',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Variant classes
|
|
57
|
+
const variantClasses = {
|
|
58
|
+
default: 'border-0 border-b-2 border-gray-300 focus:border-indigo-500',
|
|
59
|
+
outlined: 'border border-gray-300 focus:border-indigo-500',
|
|
60
|
+
filled: 'bg-gray-100 border-0 focus:bg-white focus:ring-2 focus:ring-indigo-500',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const baseClasses = [
|
|
64
|
+
'w-full',
|
|
65
|
+
'rounded-md',
|
|
66
|
+
'transition-colors',
|
|
67
|
+
'focus:outline-none',
|
|
68
|
+
'focus:ring-2',
|
|
69
|
+
'focus:ring-offset-2',
|
|
70
|
+
'disabled:opacity-50',
|
|
71
|
+
'disabled:cursor-not-allowed',
|
|
72
|
+
sizeClasses[size],
|
|
73
|
+
variantClasses[variant],
|
|
16
74
|
];
|
|
17
75
|
|
|
18
|
-
|
|
76
|
+
const errorClasses = error
|
|
77
|
+
? 'border-red-500 focus:border-red-500 focus:ring-red-500'
|
|
78
|
+
: '';
|
|
79
|
+
|
|
80
|
+
const inputClasses = [
|
|
81
|
+
...baseClasses,
|
|
82
|
+
errorClasses,
|
|
83
|
+
className,
|
|
84
|
+
].filter(Boolean).join(' ');
|
|
85
|
+
|
|
86
|
+
const labelClasses = [
|
|
87
|
+
'block',
|
|
88
|
+
getTypographyClasses('label'),
|
|
89
|
+
'mb-1',
|
|
90
|
+
disabled ? 'opacity-50' : '',
|
|
91
|
+
].filter(Boolean).join(' ');
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="w-full">
|
|
95
|
+
{label && (
|
|
96
|
+
<label
|
|
97
|
+
htmlFor={inputId}
|
|
98
|
+
className={labelClasses}
|
|
99
|
+
>
|
|
100
|
+
{label}
|
|
101
|
+
</label>
|
|
102
|
+
)}
|
|
103
|
+
<input
|
|
104
|
+
id={inputId}
|
|
105
|
+
className={inputClasses}
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
aria-invalid={error}
|
|
108
|
+
aria-describedby={errorId || helperId}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
{(error || helperText) && (
|
|
112
|
+
<div
|
|
113
|
+
id={errorId || helperId}
|
|
114
|
+
className={`mt-1 ${getTypographyClasses('caption')} ${error ? getColorClass('error', 'DEFAULT', 'text') : 'text-gray-500'}`}
|
|
115
|
+
role={error ? 'alert' : undefined}
|
|
116
|
+
>
|
|
117
|
+
{error || helperText}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
19
122
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import Radio from './Radio';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Radio> = {
|
|
5
|
+
title: 'Atoms/Radio',
|
|
6
|
+
component: Radio,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
label: {
|
|
10
|
+
control: 'text',
|
|
11
|
+
},
|
|
12
|
+
error: {
|
|
13
|
+
control: 'boolean',
|
|
14
|
+
},
|
|
15
|
+
disabled: {
|
|
16
|
+
control: 'boolean',
|
|
17
|
+
},
|
|
18
|
+
checked: {
|
|
19
|
+
control: 'boolean',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof Radio>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
name: 'option',
|
|
30
|
+
label: 'Option 1',
|
|
31
|
+
value: '1',
|
|
32
|
+
checked: false,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Checked: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
name: 'option',
|
|
39
|
+
label: 'Selected option',
|
|
40
|
+
value: '1',
|
|
41
|
+
checked: true,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const WithError: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
name: 'option',
|
|
48
|
+
label: 'Option with error',
|
|
49
|
+
value: '1',
|
|
50
|
+
error: true,
|
|
51
|
+
helperText: 'Please select an option',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Disabled: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
name: 'option',
|
|
58
|
+
label: 'Disabled option',
|
|
59
|
+
value: '1',
|
|
60
|
+
disabled: true,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const RadioGroup: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<div className="space-y-2">
|
|
67
|
+
<Radio name="group" label="Option 1" value="1" />
|
|
68
|
+
<Radio name="group" label="Option 2" value="2" checked />
|
|
69
|
+
<Radio name="group" label="Option 3" value="3" />
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { vi } from 'vitest';
|
|
4
|
+
import Radio from './Radio';
|
|
5
|
+
|
|
6
|
+
describe('Radio', () => {
|
|
7
|
+
it('renders radio with label', () => {
|
|
8
|
+
render(<Radio name="test" label="Test radio" value="test" />);
|
|
9
|
+
expect(screen.getByLabelText('Test radio')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('handles click events', async () => {
|
|
13
|
+
const handleChange = vi.fn();
|
|
14
|
+
render(<Radio name="test" label="Test" value="test" onChange={handleChange} />);
|
|
15
|
+
|
|
16
|
+
const radio = screen.getByLabelText('Test');
|
|
17
|
+
await userEvent.click(radio);
|
|
18
|
+
|
|
19
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('shows error state', () => {
|
|
23
|
+
render(<Radio name="test" label="Test" value="test" error helperText="Error message" />);
|
|
24
|
+
expect(screen.getByText('Error message')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByRole('radio')).toHaveAttribute('aria-invalid', 'true');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('is disabled when disabled prop is true', () => {
|
|
29
|
+
render(<Radio name="test" label="Test" value="test" disabled />);
|
|
30
|
+
expect(screen.getByRole('radio')).toBeDisabled();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { InputHTMLAttributes, ReactNode } from 'react';
|
|
4
|
+
import { getTypographyClasses } from '../../tokens/typography';
|
|
5
|
+
|
|
6
|
+
export interface RadioProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
7
|
+
label?: ReactNode;
|
|
8
|
+
error?: boolean;
|
|
9
|
+
helperText?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Radio Component
|
|
14
|
+
*
|
|
15
|
+
* A styled radio input component.
|
|
16
|
+
* Follows Atomic Design principles as an Atom component.
|
|
17
|
+
* Uses Composite Pattern when combined with Label and ErrorMessage.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <Radio
|
|
22
|
+
* id="option1"
|
|
23
|
+
* name="options"
|
|
24
|
+
* label="Option 1"
|
|
25
|
+
* value="1"
|
|
26
|
+
* checked={selected === "1"}
|
|
27
|
+
* onChange={handleChange}
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export default function Radio({
|
|
32
|
+
id,
|
|
33
|
+
label,
|
|
34
|
+
error = false,
|
|
35
|
+
helperText,
|
|
36
|
+
className = '',
|
|
37
|
+
disabled = false,
|
|
38
|
+
...props
|
|
39
|
+
}: RadioProps) {
|
|
40
|
+
const radioId = id || `radio-${Math.random().toString(36).substr(2, 9)}`;
|
|
41
|
+
const errorId = error ? `${radioId}-error` : undefined;
|
|
42
|
+
const helperId = helperText ? `${radioId}-helper` : undefined;
|
|
43
|
+
|
|
44
|
+
const baseClasses = [
|
|
45
|
+
'h-4',
|
|
46
|
+
'w-4',
|
|
47
|
+
'border-gray-300',
|
|
48
|
+
'text-indigo-600',
|
|
49
|
+
'focus:ring-2',
|
|
50
|
+
'focus:ring-indigo-500',
|
|
51
|
+
'focus:ring-offset-2',
|
|
52
|
+
'disabled:opacity-50',
|
|
53
|
+
'disabled:cursor-not-allowed',
|
|
54
|
+
'cursor-pointer',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const errorClasses = error
|
|
58
|
+
? 'border-red-500 focus:ring-red-500'
|
|
59
|
+
: '';
|
|
60
|
+
|
|
61
|
+
const radioClasses = [
|
|
62
|
+
...baseClasses,
|
|
63
|
+
errorClasses,
|
|
64
|
+
].filter(Boolean).join(' ');
|
|
65
|
+
|
|
66
|
+
const labelClasses = [
|
|
67
|
+
getTypographyClasses('label'),
|
|
68
|
+
'ml-2',
|
|
69
|
+
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
|
70
|
+
].filter(Boolean).join(' ');
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className={`flex flex-col my-2 ${className}`}>
|
|
74
|
+
<div className="flex items-center">
|
|
75
|
+
<input
|
|
76
|
+
type="radio"
|
|
77
|
+
id={radioId}
|
|
78
|
+
className={radioClasses}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
aria-invalid={error}
|
|
81
|
+
aria-describedby={errorId || helperId}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
{label && (
|
|
85
|
+
<label
|
|
86
|
+
htmlFor={radioId}
|
|
87
|
+
className={labelClasses}
|
|
88
|
+
>
|
|
89
|
+
{label}
|
|
90
|
+
</label>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
{(error || helperText) && (
|
|
94
|
+
<div
|
|
95
|
+
id={errorId || helperId}
|
|
96
|
+
className={`mt-1 ${getTypographyClasses('caption')} ${error ? 'text-red-600' : 'text-gray-500'}`}
|
|
97
|
+
role={error ? 'alert' : undefined}
|
|
98
|
+
>
|
|
99
|
+
{error || helperText}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
package/src/ui/atoms/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Info } from "./Info/Info";
|
|
|
3
3
|
export { default as Text } from "./Text/Text";
|
|
4
4
|
|
|
5
5
|
export { default as Input } from "./Input/Input";
|
|
6
|
+
export type { InputProps } from "./Input/Input";
|
|
6
7
|
|
|
7
8
|
export { default as Button } from "./Button/Button";
|
|
8
9
|
|
|
@@ -31,3 +32,9 @@ export type { SidebarItemProps } from "./SidebarItem/SidebarItem";
|
|
|
31
32
|
|
|
32
33
|
export { default as Collapsible } from "./Collapsible/Collapsible";
|
|
33
34
|
export type { CollapsibleProps } from "./Collapsible/Collapsible";
|
|
35
|
+
|
|
36
|
+
export { default as Checkbox } from "./Checkbox/Checkbox";
|
|
37
|
+
export type { CheckboxProps } from "./Checkbox/Checkbox";
|
|
38
|
+
|
|
39
|
+
export { default as Radio } from "./Radio/Radio";
|
|
40
|
+
export type { RadioProps } from "./Radio/Radio";
|
package/src/ui/index.ts
CHANGED
|
@@ -2,3 +2,17 @@ export * from "./atoms";
|
|
|
2
2
|
export * from "./molecules";
|
|
3
3
|
export * from "./organisms";
|
|
4
4
|
export * from "./tokens/sidebar";
|
|
5
|
+
export * from "./tokens/spacing";
|
|
6
|
+
export * from "./tokens/typography";
|
|
7
|
+
export * from "./tokens/colors";
|
|
8
|
+
export * from "./tokens/breakpoints";
|
|
9
|
+
export * from "./tokens/tokens.factory";
|
|
10
|
+
export * from "./tokens/themes/light";
|
|
11
|
+
export * from "./tokens/themes/dark";
|
|
12
|
+
export * from "./providers/ThemeProvider";
|
|
13
|
+
|
|
14
|
+
// Export helper functions for convenience
|
|
15
|
+
export { getSpacingClass, getSpacing } from "./tokens/spacing";
|
|
16
|
+
export { getTypographyClasses, getTypography } from "./tokens/typography";
|
|
17
|
+
export { getColorClass, getColor } from "./tokens/colors";
|
|
18
|
+
export { getBreakpoint, getMediaQuery } from "./tokens/breakpoints";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { HTMLProps } from "react";
|
|
2
|
-
import { Text, Input } from "../../atoms";
|
|
2
|
+
import { Text, Input, type InputProps } from "../../atoms";
|
|
3
3
|
|
|
4
|
-
interface Props extends HTMLProps<HTMLInputElement> {
|
|
4
|
+
interface Props extends Omit<HTMLProps<HTMLInputElement>, 'size'> {
|
|
5
5
|
label: string;
|
|
6
|
+
size?: InputProps['size'];
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
export default function InputWithLabel({ label, ...props }: Props) {
|
|
9
|
+
export default function InputWithLabel({ label, size, ...props }: Props) {
|
|
9
10
|
if (!props.id) {
|
|
10
11
|
console.error("InputWithLabel component requires an id prop");
|
|
11
12
|
}
|
|
@@ -15,7 +16,7 @@ export default function InputWithLabel({ label, ...props }: Props) {
|
|
|
15
16
|
<Text as="label" htmlFor={props.id} className="cursor-pointer">
|
|
16
17
|
{label}
|
|
17
18
|
</Text>
|
|
18
|
-
<Input {...props} />
|
|
19
|
+
<Input {...props} size={size} />
|
|
19
20
|
</div>
|
|
20
21
|
);
|
|
21
22
|
}
|