From 912bd5dd31af2e02ee542ddcda11b809ee1113c7 Mon Sep 17 00:00:00 2001 From: Clawd Date: Mon, 16 Feb 2026 08:03:04 +0100 Subject: [PATCH] feat(01-input-ux-01): create StepperInput controlled component - Reusable stepper with +/- buttons flanking a number input - Handles min clamping, max constraint, decimal steps - Controlled component (no internal state): value/onChange props - 44px touch targets, 16px font, aria-labels present - Rejects non-numeric input silently --- frontend/src/components/StepperInput.jsx | 89 ++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 frontend/src/components/StepperInput.jsx diff --git a/frontend/src/components/StepperInput.jsx b/frontend/src/components/StepperInput.jsx new file mode 100644 index 0000000..b46bd4f --- /dev/null +++ b/frontend/src/components/StepperInput.jsx @@ -0,0 +1,89 @@ +function StepperInput({ + value = '', + onChange, + step = 1, + min = 0, + max = null, + label = 'Value', + suffix = '', + disabled = false, +}) { + const numValue = parseFloat(value) || 0; + + function handleInputChange(e) { + const raw = e.target.value; + if (raw === '') { + onChange(''); + return; + } + const parsed = parseFloat(raw); + if (isNaN(parsed)) return; + if (parsed < min) { + onChange(String(min)); + } else if (max !== null && parsed > max) { + onChange(String(max)); + } else { + onChange(String(parsed)); + } + } + + function handleDecrement() { + if (disabled) return; + const newVal = Math.max(min, numValue - step); + onChange(String(newVal)); + } + + function handleIncrement() { + if (disabled) return; + const newVal = numValue + step; + if (max === null || newVal <= max) { + onChange(String(newVal)); + } + } + + const canDecrement = numValue > min; + const canIncrement = max === null || numValue < max; + + return ( +
+ +
+ +
+ + {suffix && {suffix}} +
+ +
+
+ ); +} + +export default StepperInput;