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';
import React from 'react';
import React, { useState } from 'react';
const Input = ({
type = 'text',
@@ -16,15 +16,37 @@ const Input = ({
min,
max,
step,
size = 'md',
variant = 'default',
onFocus,
onBlur,
...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 ${
error ? 'border-red-500/50 dark:border-red-500/50' : ''
} ${className}`;
const [focused, setFocused] = useState(false);
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) => {
let newValue = e.target.value;
// Handle number type conversions
if (type === 'number') {
// Convert empty string to 0 for numeric inputs to prevent database errors
@@ -38,11 +60,23 @@ const Input = ({
}
}
}
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 = () => {
return (
<div className="flex gap-2">
@@ -55,7 +89,7 @@ const Input = ({
disabled={disabled}
{...props}
/>
<div
<div
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'
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
@@ -64,7 +98,7 @@ const Input = ({
</div>
<input
type="text"
className={`${baseInputClassName} flex-1 min-w-0`}
className={`${colorBaseClassName} flex-1 min-w-0`}
value={value || ''}
onChange={handleChange}
placeholder={placeholder || 'Enter hex color'}
@@ -83,7 +117,7 @@ const Input = ({
{required && <span className="text-red-500 dark:text-red-400 ml-1">*</span>}
</label>
)}
{type === 'color' ? (
renderColorInput()
) : (
@@ -91,6 +125,8 @@ const Input = ({
type={type}
value={value}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={placeholder}
className={baseInputClassName}
disabled={disabled}
@@ -100,11 +136,11 @@ const Input = ({
{...props}
/>
)}
{error && (
<p className="text-red-600 dark:text-red-400 text-xs">{error}</p>
)}
{description && !error && (
<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;