Unify preset/object GUI as collapsible cards; add NumberField for reliable numeric editing

This commit is contained in:
2026-10-17 20:38:37 -04:00
parent e314623c10
commit 5b2bc25ef6
7 changed files with 353 additions and 209 deletions
+73
View File
@@ -0,0 +1,73 @@
import { useEffect, useState } from 'react';
interface NumberFieldProps {
value: number;
onChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
width?: number | string;
className?: string;
title?: string;
}
export function NumberField({ value, onChange, min, max, step, width, className, title }: NumberFieldProps) {
const [text, setText] = useState<string>(String(value));
useEffect(() => {
setText(String(value));
}, [value]);
const clamp = (n: number) => {
if (min !== undefined) n = Math.max(min, n);
if (max !== undefined) n = Math.min(max, n);
return n;
};
return (
<input
type="text"
inputMode="numeric"
className={className}
title={title}
style={width ? { width } : undefined}
value={text}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onChange={(e) => {
const raw = e.target.value;
if (!/^-?\d*$/.test(raw)) return;
setText(raw);
if (raw === '' || raw === '-') return;
const parsed = Number(raw);
if (!Number.isNaN(parsed)) {
const next = clamp(parsed);
if (next !== value) onChange(next);
}
}}
onBlur={() => {
if (text === '' || text === '-') {
const fallback = clamp(value);
onChange(fallback);
setText(String(fallback));
} else {
setText(String(value));
}
}}
onKeyDown={(e) => {
if (e.key === 'ArrowUp' && step) {
e.preventDefault();
const next = clamp(value + step);
onChange(next);
setText(String(next));
}
if (e.key === 'ArrowDown' && step) {
e.preventDefault();
const next = clamp(value - step);
onChange(next);
setText(String(next));
}
}}
/>
);
}