feat: Добавить анимации и улучшения UX/UI в компоненты приложения, включая плавные переходы, эффекты при наведении и анимации для модальных окон. Обновить карточки и таблицы с новыми анимациями для улучшения визуального восприятия и доступности.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m42s

This commit is contained in:
2025-10-02 19:17:25 +07:00
parent cd5e169bc6
commit 8952c4a897
12 changed files with 1267 additions and 143 deletions
+147
View File
@@ -0,0 +1,147 @@
/**
* FormField - универсальное поле формы с валидацией
* Поддерживает иконки, подсказки, ошибки и success состояния
*/
function FormField({
label,
name,
type = 'text',
value,
onChange,
onBlur,
error,
success,
helpText,
required,
disabled,
placeholder,
icon: Icon,
className = '',
inputClassName = '',
rows, // для textarea
options, // для select
...inputProps
}) {
const inputId = `field-${name}`;
const hasError = !!error;
const hasSuccess = !!success && !error;
const isTextarea = type === 'textarea';
const isSelect = type === 'select';
const inputClasses = `form-control ${hasError ? 'is-invalid' : ''} ${hasSuccess ? 'is-valid' : ''} ${inputClassName}`;
const handleChange = (e) => {
onChange?.(e.target.value, e);
};
const handleBlur = (e) => {
onBlur?.(e);
};
const renderInput = () => {
if (isTextarea) {
return (
<textarea
id={inputId}
name={name}
className={inputClasses}
value={value}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
required={required}
disabled={disabled}
rows={rows || 3}
aria-invalid={hasError}
aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
{...inputProps}
/>
);
}
if (isSelect) {
return (
<select
id={inputId}
name={name}
className={inputClasses.replace('form-control', 'form-select')}
value={value}
onChange={handleChange}
onBlur={handleBlur}
required={required}
disabled={disabled}
aria-invalid={hasError}
aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
{...inputProps}
>
{options?.map((option) => (
<option key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</select>
);
}
return (
<input
id={inputId}
name={name}
type={type}
className={inputClasses}
value={value}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
required={required}
disabled={disabled}
aria-invalid={hasError}
aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
{...inputProps}
/>
);
};
return (
<div className={`mb-3 ${className}`}>
{label && (
<label htmlFor={inputId} className="form-label">
{label}
{required && <span className="text-danger ms-1" aria-label="обязательное поле">*</span>}
</label>
)}
{Icon ? (
<div className="input-icon">
<span className="input-icon-addon">
<Icon size={18} />
</span>
{renderInput()}
</div>
) : (
renderInput()
)}
{error && (
<div id={`${inputId}-error`} className="invalid-feedback d-block" role="alert">
{error}
</div>
)}
{hasSuccess && (
<div className="valid-feedback d-block">
{success}
</div>
)}
{helpText && !error && !success && (
<div id={`${inputId}-help`} className="form-text">
{helpText}
</div>
)}
</div>
);
}
export default FormField;