neogestify-ui-components 1.2.18 → 1.2.19
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/README.md +349 -342
- package/dist/components/alerts/index.js +1 -1
- package/dist/components/alerts/index.js.map +1 -1
- package/dist/components/alerts/index.mjs +1 -1
- package/dist/components/alerts/index.mjs.map +1 -1
- package/dist/components/html/index.js +1 -1
- package/dist/components/html/index.js.map +1 -1
- package/dist/components/html/index.mjs +1 -1
- package/dist/components/html/index.mjs.map +1 -1
- package/dist/components/icons/index.js.map +1 -1
- package/dist/components/icons/index.mjs.map +1 -1
- package/dist/context/theme/index.js +1 -1
- package/dist/context/theme/index.js.map +1 -1
- package/dist/context/theme/index.mjs +1 -1
- package/dist/context/theme/index.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/alerts/InfoAlert.tsx +25 -25
- package/src/components/alerts/alerta.ts +93 -93
- package/src/components/alerts/index.ts +1 -1
- package/src/components/html/Button.tsx +71 -71
- package/src/components/html/Form.tsx +39 -39
- package/src/components/html/Input.tsx +136 -136
- package/src/components/html/Loading.tsx +104 -104
- package/src/components/html/Modal.tsx +79 -79
- package/src/components/html/Select.tsx +81 -81
- package/src/components/html/Table.tsx +61 -61
- package/src/components/html/TextArea.tsx +70 -70
- package/src/components/html/index.ts +7 -7
- package/src/components/icons/icons.tsx +550 -550
- package/src/components/icons/index.ts +1 -1
- package/src/context/theme/ThemeContext.tsx +37 -37
- package/src/context/theme/ThemeToggle.tsx +23 -23
- package/src/context/theme/index.ts +3 -3
- package/src/context/theme/theme.types.ts +11 -11
- package/src/context/theme/useTheme.ts +10 -10
- package/src/index.ts +5 -5
- package/src/types/types.ts +3 -3
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import { type InputHTMLAttributes, type FC, type ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
4
|
-
label?: string | ReactNode;
|
|
5
|
-
error?: string;
|
|
6
|
-
helperText?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const Input: FC<InputProps> = ({
|
|
10
|
-
label,
|
|
11
|
-
error,
|
|
12
|
-
helperText,
|
|
13
|
-
className = '',
|
|
14
|
-
id,
|
|
15
|
-
type,
|
|
16
|
-
...props
|
|
17
|
-
}) => {
|
|
18
|
-
const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`;
|
|
19
|
-
|
|
20
|
-
// ── Default text input ────────────────────────────────────────────────────
|
|
21
|
-
const baseClasses = 'appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 focus:z-10 sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200';
|
|
22
|
-
const errorClasses = error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400 focus:border-red-500' : 'border-gray-300 dark:border-gray-600';
|
|
23
|
-
const classes = `${baseClasses} ${errorClasses} ${className}`;
|
|
24
|
-
|
|
25
|
-
// ── Checkbox ──────────────────────────────────────────────────────────────
|
|
26
|
-
const checkboxBaseClasses = 'h-4 w-4 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-indigo-600 dark:text-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200 cursor-pointer';
|
|
27
|
-
const checkboxErrorClasses = error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400' : '';
|
|
28
|
-
const checkboxClasses = `${checkboxBaseClasses} ${checkboxErrorClasses}`.trim();
|
|
29
|
-
|
|
30
|
-
// ── File input ────────────────────────────────────────────────────────────
|
|
31
|
-
const fileBaseClasses = 'block w-full sm:text-sm text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200 file:mr-4 file:py-2 file:px-4 file:rounded-l-md file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 dark:file:bg-indigo-900/50 dark:file:text-indigo-300 hover:file:bg-indigo-100 dark:hover:file:bg-indigo-800/50 file:transition-colors file:duration-200 file:cursor-pointer';
|
|
32
|
-
const fileErrorClasses = error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400' : 'border-gray-300 dark:border-gray-600';
|
|
33
|
-
const fileClasses = `${fileBaseClasses} ${fileErrorClasses} ${className}`.trim();
|
|
34
|
-
|
|
35
|
-
const hasHidden = Boolean(className && /\bhidden\b/.test(className));
|
|
36
|
-
const wrapperBase = 'space-y-1 w-full';
|
|
37
|
-
const wrapperClasses = hasHidden ? `${wrapperBase} hidden` : wrapperBase;
|
|
38
|
-
|
|
39
|
-
if (type === 'checkbox') {
|
|
40
|
-
return (
|
|
41
|
-
<div className={wrapperClasses}>
|
|
42
|
-
<div className="flex items-center space-x-2">
|
|
43
|
-
<input
|
|
44
|
-
id={inputId}
|
|
45
|
-
type="checkbox"
|
|
46
|
-
className={checkboxClasses}
|
|
47
|
-
{...props}
|
|
48
|
-
/>
|
|
49
|
-
{label && typeof label === 'string' ? (
|
|
50
|
-
<label
|
|
51
|
-
htmlFor={inputId}
|
|
52
|
-
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
53
|
-
>
|
|
54
|
-
{label}
|
|
55
|
-
</label>
|
|
56
|
-
) : (
|
|
57
|
-
label
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
{error && (
|
|
61
|
-
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
|
|
62
|
-
{error}
|
|
63
|
-
</p>
|
|
64
|
-
)}
|
|
65
|
-
{helperText && !error && (
|
|
66
|
-
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
67
|
-
{helperText}
|
|
68
|
-
</p>
|
|
69
|
-
)}
|
|
70
|
-
</div>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (type === 'file') {
|
|
75
|
-
return (
|
|
76
|
-
<div className={wrapperClasses}>
|
|
77
|
-
{label && typeof label === 'string' ? (
|
|
78
|
-
<label
|
|
79
|
-
htmlFor={inputId}
|
|
80
|
-
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
81
|
-
>
|
|
82
|
-
{label}
|
|
83
|
-
</label>
|
|
84
|
-
) : (
|
|
85
|
-
label
|
|
86
|
-
)}
|
|
87
|
-
<input
|
|
88
|
-
id={inputId}
|
|
89
|
-
type="file"
|
|
90
|
-
className={fileClasses}
|
|
91
|
-
{...props}
|
|
92
|
-
/>
|
|
93
|
-
{error && (
|
|
94
|
-
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
|
|
95
|
-
{error}
|
|
96
|
-
</p>
|
|
97
|
-
)}
|
|
98
|
-
{helperText && !error && (
|
|
99
|
-
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
100
|
-
{helperText}
|
|
101
|
-
</p>
|
|
102
|
-
)}
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<div className={wrapperClasses}>
|
|
109
|
-
{label && typeof label === 'string' ? (
|
|
110
|
-
<label
|
|
111
|
-
htmlFor={inputId}
|
|
112
|
-
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
113
|
-
>
|
|
114
|
-
{label}
|
|
115
|
-
</label>
|
|
116
|
-
) : (
|
|
117
|
-
label
|
|
118
|
-
)}
|
|
119
|
-
<input
|
|
120
|
-
id={inputId}
|
|
121
|
-
className={classes}
|
|
122
|
-
type={type}
|
|
123
|
-
{...props}
|
|
124
|
-
/>
|
|
125
|
-
{error && (
|
|
126
|
-
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
|
|
127
|
-
{error}
|
|
128
|
-
</p>
|
|
129
|
-
)}
|
|
130
|
-
{helperText && !error && (
|
|
131
|
-
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
132
|
-
{helperText}
|
|
133
|
-
</p>
|
|
134
|
-
)}
|
|
135
|
-
</div>
|
|
136
|
-
);
|
|
1
|
+
import { type InputHTMLAttributes, type FC, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
4
|
+
label?: string | ReactNode;
|
|
5
|
+
error?: string;
|
|
6
|
+
helperText?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Input: FC<InputProps> = ({
|
|
10
|
+
label,
|
|
11
|
+
error,
|
|
12
|
+
helperText,
|
|
13
|
+
className = '',
|
|
14
|
+
id,
|
|
15
|
+
type,
|
|
16
|
+
...props
|
|
17
|
+
}) => {
|
|
18
|
+
const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`;
|
|
19
|
+
|
|
20
|
+
// ── Default text input ────────────────────────────────────────────────────
|
|
21
|
+
const baseClasses = 'appearance-none relative block w-full px-3 py-2 border placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white bg-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 focus:z-10 sm:text-sm disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200';
|
|
22
|
+
const errorClasses = error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400 focus:border-red-500' : 'border-gray-300 dark:border-gray-600';
|
|
23
|
+
const classes = `${baseClasses} ${errorClasses} ${className}`;
|
|
24
|
+
|
|
25
|
+
// ── Checkbox ──────────────────────────────────────────────────────────────
|
|
26
|
+
const checkboxBaseClasses = 'h-4 w-4 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-indigo-600 dark:text-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200 cursor-pointer';
|
|
27
|
+
const checkboxErrorClasses = error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400' : '';
|
|
28
|
+
const checkboxClasses = `${checkboxBaseClasses} ${checkboxErrorClasses}`.trim();
|
|
29
|
+
|
|
30
|
+
// ── File input ────────────────────────────────────────────────────────────
|
|
31
|
+
const fileBaseClasses = 'block w-full sm:text-sm text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 border rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-400 focus:border-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200 file:mr-4 file:py-2 file:px-4 file:rounded-l-md file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 dark:file:bg-indigo-900/50 dark:file:text-indigo-300 hover:file:bg-indigo-100 dark:hover:file:bg-indigo-800/50 file:transition-colors file:duration-200 file:cursor-pointer';
|
|
32
|
+
const fileErrorClasses = error ? 'border-red-300 dark:border-red-600 focus:ring-red-500 dark:focus:ring-red-400' : 'border-gray-300 dark:border-gray-600';
|
|
33
|
+
const fileClasses = `${fileBaseClasses} ${fileErrorClasses} ${className}`.trim();
|
|
34
|
+
|
|
35
|
+
const hasHidden = Boolean(className && /\bhidden\b/.test(className));
|
|
36
|
+
const wrapperBase = 'space-y-1 w-full';
|
|
37
|
+
const wrapperClasses = hasHidden ? `${wrapperBase} hidden` : wrapperBase;
|
|
38
|
+
|
|
39
|
+
if (type === 'checkbox') {
|
|
40
|
+
return (
|
|
41
|
+
<div className={wrapperClasses}>
|
|
42
|
+
<div className="flex items-center space-x-2">
|
|
43
|
+
<input
|
|
44
|
+
id={inputId}
|
|
45
|
+
type="checkbox"
|
|
46
|
+
className={checkboxClasses}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
{label && typeof label === 'string' ? (
|
|
50
|
+
<label
|
|
51
|
+
htmlFor={inputId}
|
|
52
|
+
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
53
|
+
>
|
|
54
|
+
{label}
|
|
55
|
+
</label>
|
|
56
|
+
) : (
|
|
57
|
+
label
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
{error && (
|
|
61
|
+
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
|
|
62
|
+
{error}
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
{helperText && !error && (
|
|
66
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
67
|
+
{helperText}
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (type === 'file') {
|
|
75
|
+
return (
|
|
76
|
+
<div className={wrapperClasses}>
|
|
77
|
+
{label && typeof label === 'string' ? (
|
|
78
|
+
<label
|
|
79
|
+
htmlFor={inputId}
|
|
80
|
+
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
81
|
+
>
|
|
82
|
+
{label}
|
|
83
|
+
</label>
|
|
84
|
+
) : (
|
|
85
|
+
label
|
|
86
|
+
)}
|
|
87
|
+
<input
|
|
88
|
+
id={inputId}
|
|
89
|
+
type="file"
|
|
90
|
+
className={fileClasses}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
{error && (
|
|
94
|
+
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
|
|
95
|
+
{error}
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
{helperText && !error && (
|
|
99
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
100
|
+
{helperText}
|
|
101
|
+
</p>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className={wrapperClasses}>
|
|
109
|
+
{label && typeof label === 'string' ? (
|
|
110
|
+
<label
|
|
111
|
+
htmlFor={inputId}
|
|
112
|
+
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
113
|
+
>
|
|
114
|
+
{label}
|
|
115
|
+
</label>
|
|
116
|
+
) : (
|
|
117
|
+
label
|
|
118
|
+
)}
|
|
119
|
+
<input
|
|
120
|
+
id={inputId}
|
|
121
|
+
className={classes}
|
|
122
|
+
type={type}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
{error && (
|
|
126
|
+
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
|
|
127
|
+
{error}
|
|
128
|
+
</p>
|
|
129
|
+
)}
|
|
130
|
+
{helperText && !error && (
|
|
131
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
132
|
+
{helperText}
|
|
133
|
+
</p>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
137
|
};
|
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
import { type FC } from 'react';
|
|
2
|
-
|
|
3
|
-
interface LoadingProps {
|
|
4
|
-
variant?: 'spinner' | 'dots' | 'pulse' | 'bars' | 'ring' | 'cube';
|
|
5
|
-
size?: 'small' | 'medium' | 'large' | 'xl';
|
|
6
|
-
color?: 'primary' | 'white' | 'gray' | 'success' | 'danger' | 'warning';
|
|
7
|
-
label?: string;
|
|
8
|
-
className?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const Loading: FC<LoadingProps> = ({
|
|
12
|
-
variant = 'spinner',
|
|
13
|
-
size = 'medium',
|
|
14
|
-
color = 'primary',
|
|
15
|
-
label,
|
|
16
|
-
className = '',
|
|
17
|
-
}) => {
|
|
18
|
-
const sizeClasses = {
|
|
19
|
-
small: 'h-4 w-4',
|
|
20
|
-
medium: 'h-8 w-8',
|
|
21
|
-
large: 'h-12 w-12',
|
|
22
|
-
xl: 'h-16 w-16',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const colorClasses = {
|
|
26
|
-
primary: 'text-indigo-600 dark:text-indigo-400',
|
|
27
|
-
white: 'text-white',
|
|
28
|
-
gray: 'text-gray-500 dark:text-gray-400',
|
|
29
|
-
success: 'text-green-600 dark:text-green-400',
|
|
30
|
-
danger: 'text-red-600 dark:text-red-400',
|
|
31
|
-
warning: 'text-yellow-600 dark:text-yellow-400',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const renderIcon = () => {
|
|
35
|
-
const commonClass = `${sizeClasses[size]} ${colorClasses[color]}`;
|
|
36
|
-
|
|
37
|
-
switch (variant) {
|
|
38
|
-
case 'dots':
|
|
39
|
-
const dotSize = size === 'small' ? 'h-1 w-1' : size === 'medium' ? 'h-2 w-2' : size === 'large' ? 'h-3 w-3' : 'h-4 w-4';
|
|
40
|
-
return (
|
|
41
|
-
<div className={`flex space-x-1 ${colorClasses[color]}`}>
|
|
42
|
-
<div className={`rounded-full bg-current animate-bounce ${dotSize}`} style={{ animationDelay: '0s' }}></div>
|
|
43
|
-
<div className={`rounded-full bg-current animate-bounce ${dotSize}`} style={{ animationDelay: '0.15s' }}></div>
|
|
44
|
-
<div className={`rounded-full bg-current animate-bounce ${dotSize}`} style={{ animationDelay: '0.3s' }}></div>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
case 'bars':
|
|
49
|
-
return (
|
|
50
|
-
<div className={`flex items-end space-x-1 ${sizeClasses[size]} ${colorClasses[color]}`}>
|
|
51
|
-
<div className="w-1/4 bg-current animate-[pulse_1s_ease-in-out_infinite]" style={{ height: '60%', animationDelay: '0s' }}></div>
|
|
52
|
-
<div className="w-1/4 bg-current animate-[pulse_1s_ease-in-out_infinite]" style={{ height: '100%', animationDelay: '0.2s' }}></div>
|
|
53
|
-
<div className="w-1/4 bg-current animate-[pulse_1s_ease-in-out_infinite]" style={{ height: '60%', animationDelay: '0.4s' }}></div>
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
case 'pulse':
|
|
58
|
-
return (
|
|
59
|
-
<span className={`relative flex ${commonClass}`}>
|
|
60
|
-
<span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 bg-current"></span>
|
|
61
|
-
<span className="relative inline-flex rounded-full h-full w-full bg-current"></span>
|
|
62
|
-
</span>
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
case 'cube':
|
|
66
|
-
return (
|
|
67
|
-
<div className={`${commonClass} grid grid-cols-2 gap-1`}>
|
|
68
|
-
<div className="bg-current animate-[pulse_2s_ease-in-out_infinite]" style={{ animationDelay: '0s' }}></div>
|
|
69
|
-
<div className="bg-current animate-[pulse_2s_ease-in-out_infinite]" style={{ animationDelay: '0.5s' }}></div>
|
|
70
|
-
<div className="bg-current animate-[pulse_2s_ease-in-out_infinite]" style={{ animationDelay: '1.5s' }}></div>
|
|
71
|
-
<div className="bg-current animate-[pulse_2s_ease-in-out_infinite]" style={{ animationDelay: '1s' }}></div>
|
|
72
|
-
</div>
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
case 'ring':
|
|
76
|
-
return (
|
|
77
|
-
<svg className={`${commonClass} animate-spin`} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
78
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
79
|
-
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
80
|
-
</svg>
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
case 'spinner':
|
|
84
|
-
default:
|
|
85
|
-
return (
|
|
86
|
-
<svg className={`${commonClass} animate-spin`} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
87
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
88
|
-
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
|
89
|
-
</svg>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<div className={`flex flex-col items-center justify-center w-full h-full min-h-[inherit] ${className}`} role="status">
|
|
96
|
-
{renderIcon()}
|
|
97
|
-
{label && (
|
|
98
|
-
<span className={`mt-3 text-sm font-medium ${colorClasses[color]}`}>
|
|
99
|
-
{label}
|
|
100
|
-
</span>
|
|
101
|
-
)}
|
|
102
|
-
</div>
|
|
103
|
-
);
|
|
104
|
-
};
|
|
1
|
+
import { type FC } from 'react';
|
|
2
|
+
|
|
3
|
+
interface LoadingProps {
|
|
4
|
+
variant?: 'spinner' | 'dots' | 'pulse' | 'bars' | 'ring' | 'cube';
|
|
5
|
+
size?: 'small' | 'medium' | 'large' | 'xl';
|
|
6
|
+
color?: 'primary' | 'white' | 'gray' | 'success' | 'danger' | 'warning';
|
|
7
|
+
label?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Loading: FC<LoadingProps> = ({
|
|
12
|
+
variant = 'spinner',
|
|
13
|
+
size = 'medium',
|
|
14
|
+
color = 'primary',
|
|
15
|
+
label,
|
|
16
|
+
className = '',
|
|
17
|
+
}) => {
|
|
18
|
+
const sizeClasses = {
|
|
19
|
+
small: 'h-4 w-4',
|
|
20
|
+
medium: 'h-8 w-8',
|
|
21
|
+
large: 'h-12 w-12',
|
|
22
|
+
xl: 'h-16 w-16',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const colorClasses = {
|
|
26
|
+
primary: 'text-indigo-600 dark:text-indigo-400',
|
|
27
|
+
white: 'text-white',
|
|
28
|
+
gray: 'text-gray-500 dark:text-gray-400',
|
|
29
|
+
success: 'text-green-600 dark:text-green-400',
|
|
30
|
+
danger: 'text-red-600 dark:text-red-400',
|
|
31
|
+
warning: 'text-yellow-600 dark:text-yellow-400',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const renderIcon = () => {
|
|
35
|
+
const commonClass = `${sizeClasses[size]} ${colorClasses[color]}`;
|
|
36
|
+
|
|
37
|
+
switch (variant) {
|
|
38
|
+
case 'dots':
|
|
39
|
+
const dotSize = size === 'small' ? 'h-1 w-1' : size === 'medium' ? 'h-2 w-2' : size === 'large' ? 'h-3 w-3' : 'h-4 w-4';
|
|
40
|
+
return (
|
|
41
|
+
<div className={`flex space-x-1 ${colorClasses[color]}`}>
|
|
42
|
+
<div className={`rounded-full bg-current animate-bounce ${dotSize}`} style={{ animationDelay: '0s' }}></div>
|
|
43
|
+
<div className={`rounded-full bg-current animate-bounce ${dotSize}`} style={{ animationDelay: '0.15s' }}></div>
|
|
44
|
+
<div className={`rounded-full bg-current animate-bounce ${dotSize}`} style={{ animationDelay: '0.3s' }}></div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
case 'bars':
|
|
49
|
+
return (
|
|
50
|
+
<div className={`flex items-end space-x-1 ${sizeClasses[size]} ${colorClasses[color]}`}>
|
|
51
|
+
<div className="w-1/4 bg-current animate-[pulse_1s_ease-in-out_infinite]" style={{ height: '60%', animationDelay: '0s' }}></div>
|
|
52
|
+
<div className="w-1/4 bg-current animate-[pulse_1s_ease-in-out_infinite]" style={{ height: '100%', animationDelay: '0.2s' }}></div>
|
|
53
|
+
<div className="w-1/4 bg-current animate-[pulse_1s_ease-in-out_infinite]" style={{ height: '60%', animationDelay: '0.4s' }}></div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
case 'pulse':
|
|
58
|
+
return (
|
|
59
|
+
<span className={`relative flex ${commonClass}`}>
|
|
60
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 bg-current"></span>
|
|
61
|
+
<span className="relative inline-flex rounded-full h-full w-full bg-current"></span>
|
|
62
|
+
</span>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
case 'cube':
|
|
66
|
+
return (
|
|
67
|
+
<div className={`${commonClass} grid grid-cols-2 gap-1`}>
|
|
68
|
+
<div className="bg-current animate-[pulse_2s_ease-in-out_infinite]" style={{ animationDelay: '0s' }}></div>
|
|
69
|
+
<div className="bg-current animate-[pulse_2s_ease-in-out_infinite]" style={{ animationDelay: '0.5s' }}></div>
|
|
70
|
+
<div className="bg-current animate-[pulse_2s_ease-in-out_infinite]" style={{ animationDelay: '1.5s' }}></div>
|
|
71
|
+
<div className="bg-current animate-[pulse_2s_ease-in-out_infinite]" style={{ animationDelay: '1s' }}></div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
case 'ring':
|
|
76
|
+
return (
|
|
77
|
+
<svg className={`${commonClass} animate-spin`} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
78
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
79
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
80
|
+
</svg>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
case 'spinner':
|
|
84
|
+
default:
|
|
85
|
+
return (
|
|
86
|
+
<svg className={`${commonClass} animate-spin`} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
87
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
88
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
|
89
|
+
</svg>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className={`flex flex-col items-center justify-center w-full h-full min-h-[inherit] ${className}`} role="status">
|
|
96
|
+
{renderIcon()}
|
|
97
|
+
{label && (
|
|
98
|
+
<span className={`mt-3 text-sm font-medium ${colorClasses[color]}`}>
|
|
99
|
+
{label}
|
|
100
|
+
</span>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import { Button } from './Button';
|
|
2
|
-
import { CloseIcon } from '../icons/icons';
|
|
3
|
-
import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
|
|
4
|
-
|
|
5
|
-
interface ModalProps {
|
|
6
|
-
onClose: () => void;
|
|
7
|
-
title: string;
|
|
8
|
-
children: React.ReactNode;
|
|
9
|
-
footer?: React.ReactNode;
|
|
10
|
-
maxWidth?: string;
|
|
11
|
-
showCloseButton?: boolean;
|
|
12
|
-
zIndex?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ModalRef {
|
|
16
|
-
handleClose: () => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const Modal = forwardRef<ModalRef, ModalProps>(({
|
|
20
|
-
onClose,
|
|
21
|
-
title,
|
|
22
|
-
children,
|
|
23
|
-
footer,
|
|
24
|
-
maxWidth = 'max-w-2xl',
|
|
25
|
-
showCloseButton = true,
|
|
26
|
-
zIndex = 50
|
|
27
|
-
}, ref) => {
|
|
28
|
-
const [show, setShow] = useState(false);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
setShow(true);
|
|
32
|
-
}, []);
|
|
33
|
-
|
|
34
|
-
const handleClose = () => {
|
|
35
|
-
setShow(false);
|
|
36
|
-
setTimeout(() => {
|
|
37
|
-
onClose();
|
|
38
|
-
}, 300);
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
useImperativeHandle(ref, () => ({
|
|
42
|
-
handleClose
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<dialog
|
|
47
|
-
open={show}
|
|
48
|
-
className={`fixed inset-0 w-full h-full flex items-center justify-center p-4 ${show && 'opacity-100'} transition-opacity opacity-0 duration-300 bg-gray-900/60 backdrop-blur-sm`}
|
|
49
|
-
style={{ zIndex: zIndex - 10 }}
|
|
50
|
-
>
|
|
51
|
-
<article
|
|
52
|
-
className={`relative bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-2xl w-full ${maxWidth} max-h-[90vh] flex flex-col overflow-hidden`}
|
|
53
|
-
style={{ zIndex }}
|
|
54
|
-
>
|
|
55
|
-
<header className="shrink-0 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-between">
|
|
56
|
-
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{title}</h2>
|
|
57
|
-
{showCloseButton && (
|
|
58
|
-
<Button
|
|
59
|
-
variant='icon'
|
|
60
|
-
onClick={handleClose}
|
|
61
|
-
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
62
|
-
>
|
|
63
|
-
<CloseIcon className="w-5 h-5" />
|
|
64
|
-
</Button>
|
|
65
|
-
)}
|
|
66
|
-
</header>
|
|
67
|
-
<div className="flex-1 overflow-y-auto p-6">
|
|
68
|
-
{children}
|
|
69
|
-
</div>
|
|
70
|
-
{footer && (
|
|
71
|
-
<footer className="shrink-0 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 flex justify-end gap-3">
|
|
72
|
-
{footer}
|
|
73
|
-
</footer>
|
|
74
|
-
)}
|
|
75
|
-
</article>
|
|
76
|
-
</dialog>
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
|
|
1
|
+
import { Button } from './Button';
|
|
2
|
+
import { CloseIcon } from '../icons/icons';
|
|
3
|
+
import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react';
|
|
4
|
+
|
|
5
|
+
interface ModalProps {
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
title: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
footer?: React.ReactNode;
|
|
10
|
+
maxWidth?: string;
|
|
11
|
+
showCloseButton?: boolean;
|
|
12
|
+
zIndex?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ModalRef {
|
|
16
|
+
handleClose: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Modal = forwardRef<ModalRef, ModalProps>(({
|
|
20
|
+
onClose,
|
|
21
|
+
title,
|
|
22
|
+
children,
|
|
23
|
+
footer,
|
|
24
|
+
maxWidth = 'max-w-2xl',
|
|
25
|
+
showCloseButton = true,
|
|
26
|
+
zIndex = 50
|
|
27
|
+
}, ref) => {
|
|
28
|
+
const [show, setShow] = useState(false);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setShow(true);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const handleClose = () => {
|
|
35
|
+
setShow(false);
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
onClose();
|
|
38
|
+
}, 300);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
useImperativeHandle(ref, () => ({
|
|
42
|
+
handleClose
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<dialog
|
|
47
|
+
open={show}
|
|
48
|
+
className={`fixed inset-0 w-full h-full flex items-center justify-center p-4 ${show && 'opacity-100'} transition-opacity opacity-0 duration-300 bg-gray-900/60 backdrop-blur-sm`}
|
|
49
|
+
style={{ zIndex: zIndex - 10 }}
|
|
50
|
+
>
|
|
51
|
+
<article
|
|
52
|
+
className={`relative bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-2xl w-full ${maxWidth} max-h-[90vh] flex flex-col overflow-hidden`}
|
|
53
|
+
style={{ zIndex }}
|
|
54
|
+
>
|
|
55
|
+
<header className="shrink-0 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-between">
|
|
56
|
+
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{title}</h2>
|
|
57
|
+
{showCloseButton && (
|
|
58
|
+
<Button
|
|
59
|
+
variant='icon'
|
|
60
|
+
onClick={handleClose}
|
|
61
|
+
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
62
|
+
>
|
|
63
|
+
<CloseIcon className="w-5 h-5" />
|
|
64
|
+
</Button>
|
|
65
|
+
)}
|
|
66
|
+
</header>
|
|
67
|
+
<div className="flex-1 overflow-y-auto p-6">
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
{footer && (
|
|
71
|
+
<footer className="shrink-0 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 flex justify-end gap-3">
|
|
72
|
+
{footer}
|
|
73
|
+
</footer>
|
|
74
|
+
)}
|
|
75
|
+
</article>
|
|
76
|
+
</dialog>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
80
|
Modal.displayName = 'Modal';
|