import React, { forwardRef } from 'react'; import classNames from 'classnames'; const labelClasses = 'block text-xs mb-1 mt-2 text-gray-300'; const inputClasses = 'w-full text-sm px-3 py-1 bg-gray-800 border border-gray-700 rounded-sm focus:ring-2 focus:ring-gray-600 focus:border-transparent'; export interface InputProps { label?: string; className?: string; placeholder?: string; required?: boolean; } export interface TextInputProps extends InputProps { value: string; onChange: (value: string) => void; type?: 'text' | 'password'; disabled?: boolean; } export const TextInput = forwardRef( ({ label, value, onChange, placeholder, required, disabled, type = 'text', className }, ref) => { return (
{label && } { if (!disabled) onChange(e.target.value); }} className={`${inputClasses} ${disabled ? 'opacity-30 cursor-not-allowed' : ''}`} placeholder={placeholder} required={required} disabled={disabled} />
); } ); // 👇 Helpful for debugging TextInput.displayName = 'TextInput'; export interface NumberInputProps extends InputProps { value: number; onChange: (value: number) => void; min?: number; max?: number; } export const NumberInput = (props: NumberInputProps) => { const { label, value, onChange, placeholder, required, min, max } = props; // Add controlled internal state to properly handle partial inputs const [inputValue, setInputValue] = React.useState(value ?? ''); // Sync internal state with prop value React.useEffect(() => { setInputValue(value ?? ''); }, [value]); return (
{label && } { const rawValue = e.target.value; // Update the input display with the raw value setInputValue(rawValue); // Handle empty or partial inputs if (rawValue === '' || rawValue === '-') { // For empty or partial negative input, don't call onChange yet return; } const numValue = Number(rawValue); // Only apply constraints and call onChange when we have a valid number if (!isNaN(numValue)) { let constrainedValue = numValue; // Apply min/max constraints if they exist if (min !== undefined && constrainedValue < min) { constrainedValue = min; } if (max !== undefined && constrainedValue > max) { constrainedValue = max; } onChange(constrainedValue); } }} className={inputClasses} placeholder={placeholder} required={required} min={min} max={max} step="any" />
); }; export interface SelectInputProps extends InputProps { value: string; onChange: (value: string) => void; options: { value: string; label: string }[]; } export const SelectInput = (props: SelectInputProps) => { const { label, value, onChange, options } = props; return (
{label && }
); }; export interface CheckboxProps { label?: string; checked: boolean; onChange: (checked: boolean) => void; className?: string; required?: boolean; disabled?: boolean; } export const Checkbox = (props: CheckboxProps) => { const { label, checked, onChange, required, disabled } = props; const id = React.useId(); return (
{label && ( )}
); }; interface FormGroupProps { label?: string; className?: string; children: React.ReactNode; } export const FormGroup: React.FC = ({ label, className, children }) => { return (
{label && }
{children}
); };