feat(ui): add size and variant props to Input component
- add `size` prop (sm | md | lg) with corresponding tailwind size classes - add `variant` prop supporting `default` and `ghost` styles - add focus state tracking to enable ghost→default transition on focus - forward `onFocus` and `onBlur` callbacks with internal focus handling - isolate color input styling from size/variant logic
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
const Input = ({
|
const Input = ({
|
||||||
type = 'text',
|
type = 'text',
|
||||||
@@ -16,15 +16,37 @@ const Input = ({
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
step,
|
step,
|
||||||
|
size = 'md',
|
||||||
|
variant = 'default',
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const baseInputClassName = `w-full px-[10px] py-[7px] rounded-lg text-[13px] focus:outline-none transition-all duration-[120ms] ease-out disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900/60 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20 ${
|
const [focused, setFocused] = useState(false);
|
||||||
error ? 'border-red-500/50 dark:border-red-500/50' : ''
|
|
||||||
} ${className}`;
|
const sizeClasses = {
|
||||||
|
sm: 'px-[8px] py-[5px] text-[12px]',
|
||||||
|
md: 'px-[10px] py-[7px] text-[13px]',
|
||||||
|
lg: 'px-[14px] py-[10px] text-[20px] font-semibold',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultVariant = 'bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900/60 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20';
|
||||||
|
|
||||||
|
// Ghost : bordure transparente + texte gris au repos. Au focus, prend l'apparence default.
|
||||||
|
const ghostRest = 'bg-transparent border border-transparent text-neutral-500 placeholder-neutral-400 dark:text-neutral-400 dark:placeholder-neutral-600';
|
||||||
|
const ghostFocus = defaultVariant;
|
||||||
|
|
||||||
|
const variantClasses = variant === 'ghost'
|
||||||
|
? (focused ? ghostFocus : ghostRest)
|
||||||
|
: defaultVariant;
|
||||||
|
|
||||||
|
const errorClasses = error ? 'border-red-500/50 dark:border-red-500/50' : '';
|
||||||
|
|
||||||
|
const baseInputClassName = `w-full rounded-lg focus:outline-none transition-all duration-[120ms] ease-out disabled:opacity-50 disabled:cursor-not-allowed ${sizeClasses[size] || sizeClasses.md} ${variantClasses} ${errorClasses} ${className}`;
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
let newValue = e.target.value;
|
let newValue = e.target.value;
|
||||||
|
|
||||||
// Handle number type conversions
|
// Handle number type conversions
|
||||||
if (type === 'number') {
|
if (type === 'number') {
|
||||||
// Convert empty string to 0 for numeric inputs to prevent database errors
|
// Convert empty string to 0 for numeric inputs to prevent database errors
|
||||||
@@ -38,11 +60,23 @@ const Input = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange?.(newValue);
|
onChange?.(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enhanced color input renderer
|
const handleFocus = (e) => {
|
||||||
|
setFocused(true);
|
||||||
|
onFocus?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (e) => {
|
||||||
|
setFocused(false);
|
||||||
|
onBlur?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Color input — non concerné par size/variant, garde son apparence dédiée.
|
||||||
|
const colorBaseClassName = `w-full px-[10px] py-[7px] rounded-lg text-[13px] focus:outline-none transition-all duration-[120ms] ease-out disabled:opacity-50 disabled:cursor-not-allowed ${defaultVariant} ${errorClasses}`;
|
||||||
|
|
||||||
const renderColorInput = () => {
|
const renderColorInput = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -55,7 +89,7 @@ const Input = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`w-12 h-10 border rounded-lg cursor-pointer transition-all duration-[120ms] ease-out ${
|
className={`w-12 h-10 border rounded-lg cursor-pointer transition-all duration-[120ms] ease-out ${
|
||||||
error ? 'border-red-500/50' : 'border-neutral-300 dark:border-neutral-700/50 dark:hover:border-neutral-600'
|
error ? 'border-red-500/50' : 'border-neutral-300 dark:border-neutral-700/50 dark:hover:border-neutral-600'
|
||||||
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
@@ -64,7 +98,7 @@ const Input = ({
|
|||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={`${baseInputClassName} flex-1 min-w-0`}
|
className={`${colorBaseClassName} flex-1 min-w-0`}
|
||||||
value={value || ''}
|
value={value || ''}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={placeholder || 'Enter hex color'}
|
placeholder={placeholder || 'Enter hex color'}
|
||||||
@@ -83,7 +117,7 @@ const Input = ({
|
|||||||
{required && <span className="text-red-500 dark:text-red-400 ml-1">*</span>}
|
{required && <span className="text-red-500 dark:text-red-400 ml-1">*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === 'color' ? (
|
{type === 'color' ? (
|
||||||
renderColorInput()
|
renderColorInput()
|
||||||
) : (
|
) : (
|
||||||
@@ -91,6 +125,8 @@ const Input = ({
|
|||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={baseInputClassName}
|
className={baseInputClassName}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -100,11 +136,11 @@ const Input = ({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-red-600 dark:text-red-400 text-xs">{error}</p>
|
<p className="text-red-600 dark:text-red-400 text-xs">{error}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{description && !error && (
|
{description && !error && (
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 opacity-75">{description}</p>
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 opacity-75">{description}</p>
|
||||||
)}
|
)}
|
||||||
@@ -112,4 +148,4 @@ const Input = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
||||||
|
|||||||
Reference in New Issue
Block a user