- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
47 lines
1.6 KiB
TypeScript
47 lines
1.6 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { formatEditableNumber } from './unitSystem';
|
|
|
|
interface NumberFieldProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type'> {
|
|
/** Value in display units. */
|
|
value: number;
|
|
/** Called with the parsed display-unit number on blur / Enter only. */
|
|
onCommit: (value: number) => void;
|
|
}
|
|
|
|
/**
|
|
* Numeric input for unit-converted values. A controlled <input type=number>
|
|
* whose value is display(model(typed)) gets rewritten mid-typing whenever the
|
|
* ft/m or flow-unit round trip isn't exact (typing "216.4" in a US model ended
|
|
* up as 3.9999999999999996). Keeping the typed text as a local draft and
|
|
* committing only on blur/Enter avoids that round trip while editing.
|
|
*/
|
|
export const NumberField: React.FC<NumberFieldProps> = ({ value, onCommit, onBlur, onKeyDown, ...rest }) => {
|
|
const [draft, setDraft] = useState<string | null>(null);
|
|
|
|
const commit = () => {
|
|
if (draft === null) return;
|
|
const parsed = Number(draft);
|
|
setDraft(null);
|
|
if (draft.trim() !== '' && Number.isFinite(parsed)) onCommit(parsed);
|
|
};
|
|
|
|
return (
|
|
<input
|
|
{...rest}
|
|
type="text"
|
|
inputMode="decimal"
|
|
value={draft ?? formatEditableNumber(value)}
|
|
onChange={(e) => setDraft(e.target.value)}
|
|
onBlur={(e) => {
|
|
commit();
|
|
onBlur?.(e);
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') commit();
|
|
if (e.key === 'Escape') setDraft(null);
|
|
onKeyDown?.(e);
|
|
}}
|
|
/>
|
|
);
|
|
};
|