import { useEffect, useRef, useState } from 'react'; interface BaseProps { className?: string; disabled?: boolean; } interface NumberProps extends BaseProps { type?: 'number'; value: number; onSave: (value: number) => Promise; formatDisplay?: (value: number) => string; min?: number; step?: string; } interface TextProps extends BaseProps { type: 'text'; value: string; onSave: (value: string) => Promise; formatDisplay?: (value: string) => string; } type InlineEditInputProps = NumberProps | TextProps; type Status = 'idle' | 'editing' | 'saving' | 'success' | 'error'; export default function InlineEditInput(props: InlineEditInputProps) { const { className = '', disabled = false } = props; const isText = props.type === 'text'; const [status, setStatus] = useState('idle'); const [editValue, setEditValue] = useState(''); const inputRef = useRef(null); const savingRef = useRef(false); useEffect(() => { if (status === 'editing' && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [status]); const startEditing = () => { if (disabled) return; setEditValue(String(props.value)); setStatus('editing'); }; const cancel = () => { setStatus('idle'); }; const save = async () => { if (savingRef.current) return; let parsedValue: string | number; if (isText) { const trimmed = editValue.trim(); if (trimmed === '' || trimmed === props.value) { setStatus('idle'); return; } parsedValue = trimmed; } else { const parsed = Number(editValue); if (isNaN(parsed) || parsed === props.value) { setStatus('idle'); return; } parsedValue = parsed; } savingRef.current = true; setStatus('saving'); try { // Type assertion needed: TS can't correlate isText with the union branch await (props.onSave as (v: typeof parsedValue) => Promise)( parsedValue, ); setStatus('success'); setTimeout(() => setStatus('idle'), 1500); } catch { setStatus('error'); setTimeout(() => setStatus('idle'), 1500); } finally { savingRef.current = false; } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); save(); } else if (e.key === 'Escape') { cancel(); } }; const displayValue = isText ? (props.formatDisplay ?? String)(props.value) : (props.formatDisplay ?? ((v: number) => String(v)))(props.value); if (status === 'editing' || status === 'saving') { return ( setEditValue(e.target.value)} onBlur={save} onKeyDown={handleKeyDown} min={props.type !== 'text' ? props.min : undefined} step={props.type !== 'text' ? props.step : undefined} disabled={status === 'saving'} className={`border border-red-500 bg-black px-2 py-0.5 text-sm text-red-500 font-mono outline-none focus:ring-1 focus:ring-red-500 ${isText ? 'w-48' : 'w-24'} ${className}`} /> ); } return ( {displayValue} {status === 'success' && OK} {status === 'error' && ERR} ); }