feat: Добавить новые компоненты и улучшения UX/UI, включая Breadcrumbs с динамическими фильтрами, ToastContainer для уведомлений и CommandPalette для глобального поиска. Обновить TableSkeleton с анимацией и добавить новые skeleton компоненты для карточек, списков и форм. Обновить документацию по новым компонентам и улучшениям.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s

This commit is contained in:
2025-10-02 18:02:22 +07:00
parent 495063a510
commit 339144fb11
12 changed files with 1950 additions and 24 deletions
+119
View File
@@ -0,0 +1,119 @@
import { useState, useEffect, useRef } from 'react'
import { IconCheck, IconX, IconAlertCircle } from '@tabler/icons-react'
/**
* Компонент input с inline валидацией и подсказками (Tabler UI стили)
*/
function ValidatedInput({
type = 'text',
value,
onChange,
onValidate, // функция валидации: (value) => { valid: boolean, message: string }
placeholder = '',
label = '',
required = false,
disabled = false,
className = '',
helpText = '',
debounceMs = 300,
showSuccessIcon = true,
autoFocus = false,
...rest
}) {
const [localValue, setLocalValue] = useState(value || '')
const [validation, setValidation] = useState({ valid: null, message: '' })
const [isDirty, setIsDirty] = useState(false)
const timerRef = useRef(null)
// Синхронизация с внешним value
useEffect(() => {
setLocalValue(value || '')
}, [value])
// Валидация с debounce
useEffect(() => {
if (!isDirty || !onValidate) return
if (timerRef.current) {
clearTimeout(timerRef.current)
}
timerRef.current = setTimeout(() => {
const result = onValidate(localValue)
setValidation(result)
}, debounceMs)
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [localValue, isDirty, onValidate, debounceMs])
const handleChange = (e) => {
const newValue = e.target.value
setLocalValue(newValue)
setIsDirty(true)
onChange?.(newValue)
}
const getInputClass = () => {
if (!isDirty) return ''
if (validation.valid === true) return 'is-valid'
if (validation.valid === false) return 'is-invalid'
return ''
}
return (
<div className={`mb-3 ${className}`}>
{/* Label */}
{label && (
<label className="form-label">
{label}
{required && <span className="text-danger ms-1">*</span>}
</label>
)}
{/* Input с иконкой валидации */}
<div className="input-icon">
<input
type={type}
className={`form-control ${getInputClass()}`}
value={localValue}
onChange={handleChange}
placeholder={placeholder}
disabled={disabled}
required={required}
autoFocus={autoFocus}
{...rest}
/>
{/* Иконка статуса валидации */}
{isDirty && validation.valid !== null && (
<span className="input-icon-addon">
{validation.valid === true && showSuccessIcon ? (
<IconCheck size={20} className="text-success" />
) : validation.valid === false ? (
<IconX size={20} className="text-danger" />
) : null}
</span>
)}
</div>
{/* Help text или сообщение валидации */}
{isDirty && validation.message ? (
<div className={`form-hint ${validation.valid === false ? 'text-danger' : 'text-success'}`}>
{validation.message}
</div>
) : helpText ? (
<div className="form-hint text-muted">
<IconAlertCircle size={14} className="me-1" />
{helpText}
</div>
) : null}
</div>
)
}
export default ValidatedInput