ADD new frontend
This commit is contained in:
191
frontend/src/components/ui/Input/Input.tsx
Normal file
191
frontend/src/components/ui/Input/Input.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { forwardRef, InputHTMLAttributes } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
isRequired?: boolean;
|
||||
isInvalid?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'outline' | 'filled' | 'unstyled';
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
leftAddon?: React.ReactNode;
|
||||
rightAddon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(({
|
||||
label,
|
||||
error,
|
||||
helperText,
|
||||
isRequired = false,
|
||||
isInvalid = false,
|
||||
size = 'md',
|
||||
variant = 'outline',
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
leftAddon,
|
||||
rightAddon,
|
||||
className,
|
||||
id,
|
||||
...props
|
||||
}, ref) => {
|
||||
const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const hasError = isInvalid || !!error;
|
||||
|
||||
const baseInputClasses = [
|
||||
'w-full transition-colors duration-200',
|
||||
'focus:outline-none',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
'placeholder:text-[var(--text-tertiary)]'
|
||||
];
|
||||
|
||||
const variantClasses = {
|
||||
outline: [
|
||||
'bg-[var(--bg-primary)] border border-[var(--border-secondary)]',
|
||||
'focus:border-[var(--color-primary)] focus:ring-1 focus:ring-[var(--color-primary)]',
|
||||
hasError ? 'border-[var(--color-error)] focus:border-[var(--color-error)] focus:ring-[var(--color-error)]' : ''
|
||||
],
|
||||
filled: [
|
||||
'bg-[var(--bg-secondary)] border border-transparent',
|
||||
'focus:bg-[var(--bg-primary)] focus:border-[var(--color-primary)]',
|
||||
hasError ? 'border-[var(--color-error)]' : ''
|
||||
],
|
||||
unstyled: [
|
||||
'bg-transparent border-none',
|
||||
'focus:ring-0'
|
||||
]
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: leftIcon || rightIcon ? 'h-8 px-8 text-sm' : 'h-8 px-3 text-sm',
|
||||
md: leftIcon || rightIcon ? 'h-10 px-10 text-base' : 'h-10 px-4 text-base',
|
||||
lg: leftIcon || rightIcon ? 'h-12 px-12 text-lg' : 'h-12 px-5 text-lg'
|
||||
};
|
||||
|
||||
const iconSizeClasses = {
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-5 h-5',
|
||||
lg: 'w-6 h-6'
|
||||
};
|
||||
|
||||
const inputClasses = clsx(
|
||||
baseInputClasses,
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
'rounded-lg',
|
||||
{
|
||||
'pl-8': leftIcon && size === 'sm',
|
||||
'pl-10': leftIcon && size === 'md',
|
||||
'pl-12': leftIcon && size === 'lg',
|
||||
'pr-8': rightIcon && size === 'sm',
|
||||
'pr-10': rightIcon && size === 'md',
|
||||
'pr-12': rightIcon && size === 'lg',
|
||||
'rounded-l-none': leftAddon,
|
||||
'rounded-r-none': rightAddon,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
const addonClasses = clsx(
|
||||
'inline-flex items-center px-3 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-secondary)] text-[var(--text-secondary)]',
|
||||
{
|
||||
'border-r-0 rounded-l-lg': leftAddon,
|
||||
'border-l-0 rounded-r-lg': rightAddon,
|
||||
}
|
||||
);
|
||||
|
||||
const iconClasses = clsx(
|
||||
'absolute text-[var(--text-tertiary)] pointer-events-none',
|
||||
iconSizeClasses[size],
|
||||
{
|
||||
'left-2': leftIcon && size === 'sm',
|
||||
'left-2.5': leftIcon && size === 'md',
|
||||
'left-3': leftIcon && size === 'lg',
|
||||
'right-2': rightIcon && size === 'sm',
|
||||
'right-2.5': rightIcon && size === 'md',
|
||||
'right-3': rightIcon && size === 'lg',
|
||||
'top-1/2 -translate-y-1/2': true,
|
||||
}
|
||||
);
|
||||
|
||||
const inputElement = (
|
||||
<div className="relative">
|
||||
{leftIcon && (
|
||||
<div className={iconClasses}>
|
||||
{leftIcon}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
className={inputClasses}
|
||||
aria-invalid={hasError}
|
||||
aria-describedby={
|
||||
error ? `${inputId}-error` :
|
||||
helperText ? `${inputId}-helper` :
|
||||
undefined
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
{rightIcon && (
|
||||
<div className={iconClasses}>
|
||||
{rightIcon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const inputWithAddons = (
|
||||
<div className="flex">
|
||||
{leftAddon && (
|
||||
<span className={addonClasses}>{leftAddon}</span>
|
||||
)}
|
||||
{inputElement}
|
||||
{rightAddon && (
|
||||
<span className={addonClasses}>{rightAddon}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="block text-sm font-medium text-[var(--text-primary)] mb-2"
|
||||
>
|
||||
{label}
|
||||
{isRequired && (
|
||||
<span className="text-[var(--color-error)] ml-1">*</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{leftAddon || rightAddon ? inputWithAddons : inputElement}
|
||||
|
||||
{error && (
|
||||
<p
|
||||
id={`${inputId}-error`}
|
||||
className="mt-2 text-sm text-[var(--color-error)]"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{helperText && !error && (
|
||||
<p
|
||||
id={`${inputId}-helper`}
|
||||
className="mt-2 text-sm text-[var(--text-secondary)]"
|
||||
>
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export default Input;
|
||||
3
frontend/src/components/ui/Input/index.ts
Normal file
3
frontend/src/components/ui/Input/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default } from './Input';
|
||||
export { default as Input } from './Input';
|
||||
export type { InputProps } from './Input';
|
||||
Reference in New Issue
Block a user