diff --git a/src/common/components/forms/FormNumberInput.tsx b/src/common/components/forms/FormNumberInput.tsx new file mode 100644 index 000000000..9fcb3c910 --- /dev/null +++ b/src/common/components/forms/FormNumberInput.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; + +import type { SxProps } from '@mui/joy/styles/types'; +import { Box, FormControl, IconButton, Input, Tooltip } from '@mui/joy'; +import ClearIcon from '@mui/icons-material/Clear'; + +import { FormLabelStart } from './FormLabelStart'; + + +const _style = { + control: { + flexWrap: 'wrap', + justifyContent: 'space-between', + alignItems: 'center', + }, + inputRow: { + display: 'flex', + alignItems: 'center', + gap: 0.5, + }, +} as const satisfies Record; + + +/** + * Compact numeric input field with label and optional description. + * Supports undefined (unset/default) state with a toggle button: + * - undefined: input disabled, X button pressed (soft) — input shows empty + * - number: input active, X button flat (plain) — pressing X clears to undefined + */ +export function FormNumberInput(props: { + title: string, + description?: string, + tooltip?: string, + disabled?: boolean, + size?: 'sm' | 'md' | 'lg', + min?: number, + max?: number, + initialValue: number, + value: undefined | number, + onChange: (value: number | undefined) => void, + inputSx?: SxProps, +}) { + + const { onChange, min, max, initialValue } = props; + const isSet = props.value !== undefined; + + const handleInputChange = React.useCallback((event: React.ChangeEvent) => { + const num = parseInt(event.target.value, 10); + if (!isNaN(num)) { + const clamped = Math.max(min ?? -Infinity, Math.min(max ?? Infinity, num)); + onChange(clamped); + } + }, [max, min, onChange]); + + const handleToggle = React.useCallback(() => { + if (isSet) + onChange(undefined); + else + onChange(initialValue); + }, [isSet, onChange, initialValue]); + + return ( + + + + + + + + + + + + ); +}