From 275137cdbb352dadf86faef9dbf8bd2bff89ad01 Mon Sep 17 00:00:00 2001 From: Bas van Rossem Date: Thu, 19 Feb 2026 16:41:35 +0100 Subject: [PATCH] feat(web): make stepper values tappable for direct number input Tap the number to type a value directly instead of clicking +/- repeatedly. Input auto-selects, commits on Enter/blur, cancels on Escape, and clamps to min/max constraints. Co-Authored-By: Claude Opus 4.6 --- web/src/components/ui/NumericStepper.tsx | 51 +++++++++++++++++++++++- web/src/index.css | 17 ++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/web/src/components/ui/NumericStepper.tsx b/web/src/components/ui/NumericStepper.tsx index 0c99759..006b422 100644 --- a/web/src/components/ui/NumericStepper.tsx +++ b/web/src/components/ui/NumericStepper.tsx @@ -1,3 +1,5 @@ +import { useState, useRef, useEffect } from 'react'; + interface Props { label: string; value: number; @@ -8,6 +10,17 @@ interface Props { } export default function NumericStepper({ label, value, onChange, min = 0, max, step = 1 }: Props) { + const [editing, setEditing] = useState(false); + const [editValue, setEditValue] = useState(''); + const inputRef = useRef(null); + + useEffect(() => { + if (editing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [editing]); + const decrement = () => { const next = value - step; onChange(min !== undefined ? Math.max(min, next) : next); @@ -18,6 +31,26 @@ export default function NumericStepper({ label, value, onChange, min = 0, max, s onChange(max !== undefined ? Math.min(max, next) : next); }; + const startEditing = () => { + setEditValue(String(value)); + setEditing(true); + }; + + const commitEdit = () => { + setEditing(false); + const parsed = parseInt(editValue, 10); + if (isNaN(parsed)) return; // discard invalid input + let clamped = parsed; + if (min !== undefined) clamped = Math.max(min, clamped); + if (max !== undefined) clamped = Math.min(max, clamped); + if (clamped !== value) onChange(clamped); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') commitEdit(); + if (e.key === 'Escape') setEditing(false); + }; + return (
{label} @@ -30,7 +63,23 @@ export default function NumericStepper({ label, value, onChange, min = 0, max, s > - - {value} + {editing ? ( + setEditValue(e.target.value)} + onBlur={commitEdit} + onKeyDown={handleKeyDown} + min={min} + max={max} + /> + ) : ( + + {value} + + )}