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:
2026-04-26 11:37:42 -04:00
parent 649c69f408
commit 3fea89dbd4
+49 -13
View File
@@ -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;