Update input field style improvements

This commit is contained in:
2025-11-24 20:41:38 +00:00
parent b215188a39
commit d5938046a2
8 changed files with 96 additions and 53 deletions

View File

@@ -28,7 +28,7 @@ const DeviationList = ({
<TimeInput
value={dev.time}
onChange={newTime => onDeviationChange(index, 'time', newTime)}
errorMessage={t.timeRequired}
errorMessage={t.errorTimeRequired}
/>
<div className="w-32">
<NumericInput

View File

@@ -11,7 +11,7 @@ const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses, t }) => {
<TimeInput
value={dose.time}
onChange={newTime => onUpdateDoses(doses.map((d, i) => i === index ? {...d, time: newTime} : d))}
errorMessage={t.timeRequired}
errorMessage={t.errorTimeRequired}
/>
<div className="w-40">
<NumericInput

View File

@@ -11,6 +11,8 @@ const NumericInput = ({
align = 'right', // 'left', 'center', 'right'
allowEmpty = false, // Allow empty value (e.g., for "Auto" mode)
clearButton = false, // Show clear button (with allowEmpty=true)
clearButtonText, // Custom text or element for clear button
clearButtonTitle, // Custom title for clear button
errorMessage = 'This field is required' // Error message for mandatory empty fields
}) => {
const [hasError, setHasError] = React.useState(false);
@@ -166,51 +168,51 @@ const NumericInput = ({
onMouseLeave={handleMouseLeave}
>
<div className="relative inline-flex items-center">
<button
onClick={() => updateValue(-1)}
className="px-2 py-1 border rounded-l-md bg-gray-100 bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-800 text-lg font-bold"
tabIndex={-1}
>
-
</button>
<input
type="text"
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleInputBlurWrapper}
onFocus={handleFocus}
placeholder={placeholder}
style={{ width: '4em', minWidth: '4em', maxWidth: '8em' }}
className={`p-2 border-t border-b text-sm ${getAlignmentClass()} ${hasError ? 'bg-red-50' : ''}`}
/>
<button
onClick={() => updateValue(1)}
className={`px-2 py-1 border-t border-b ${showClearButton ? 'border-l' : 'border-l border-r rounded-r-md'} bg-gray-100 bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-800 text-lg font-bold`}
tabIndex={-1}
>
+
</button>
{showClearButton && (
<div className={`relative inline-flex items-center ${hasError ? 'ring-2 ring-red-500 rounded-md pointer-events-auto' : ''}`}>
<button
onClick={() => updateValue(-1)}
className="px-2 py-1 border rounded-l-md bg-gray-100 bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-800 text-lg font-bold"
tabIndex={-1}
>
-
</button>
<input
type="text"
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
onBlur={handleInputBlurWrapper}
onFocus={handleFocus}
placeholder={placeholder}
style={{ width: '4em', minWidth: '4em', maxWidth: '8em' }}
className={`p-2 border-t border-b text-sm ${getAlignmentClass()} ${hasError ? 'bg-transparent' : ''}`}
/>
<button
onClick={() => updateValue(1)}
className={`px-2 py-1 border-t border-b ${showClearButton ? 'border-l' : 'border-l border-r rounded-r-md'} bg-gray-100 bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-800 text-lg font-bold`}
tabIndex={-1}
>
+
</button>
{showClearButton && (
<button
onClick={handleClear}
className="px-2 py-1 border rounded-r-md bg-gray-100 bg-gray-100 hover:bg-gray-200 text-gray-600 hover:text-gray-800 text-gray-600 hover:text-gray-800"
tabIndex={-1}
title="Clear (set to Auto)"
title={clearButtonTitle || "Clear"}
>
{clearButtonText || (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
)}
{hasError && (
<div className="absolute inset-0 border-2 border-red-500 rounded-md pointer-events-none" style={{ zIndex: 10 }} />
)}
</svg>
)}
</button>)}
</div>
{unit && <span className="ml-2 text-gray-500 text-sm whitespace-nowrap">{unit}</span>}
</div>
{hasError && showErrorTooltip && (
<div
className={`absolute z-50 bg-white border-2 border-red-500 rounded-md shadow-lg p-2 flex items-start gap-2 ${
className={`absolute z-50 bg-white ring-2 ring-red-500 rounded-md shadow-lg p-2 flex items-start gap-2 ${
tooltipPosition.top ? 'bottom-full mb-1' : 'top-full mt-1'
} ${
tooltipPosition.left ? 'right-0' : 'left-0'

View File

@@ -12,6 +12,8 @@ const Settings = ({
t
}) => {
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, simulationDays, displayedDays } = uiSettings;
// https://www.svgrepo.com/svg/509013/actual-size
const clearButtonSVG = (<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" width="24px" height="24px" viewBox="0 0 31.812 31.906"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M23.263,9.969 L27.823,9.969 C28.372,9.969 28.817,10.415 28.817,10.966 C28.817,11.516 28.456,11.906 27.906,11.906 L21.154,11.906 C21.058,11.935 20.962,11.963 20.863,11.963 C20.608,11.963 20.354,11.865 20.160,11.671 C19.916,11.426 19.843,11.088 19.906,10.772 L19.906,3.906 C19.906,3.355 20.313,2.989 20.863,2.989 C21.412,2.989 21.857,3.435 21.857,3.986 L21.857,8.559 L30.103,0.289 C30.491,-0.100 31.120,-0.100 31.509,0.289 C31.897,0.679 31.897,1.310 31.509,1.699 L23.263,9.969 ZM11.914,27.917 C11.914,28.468 11.469,28.914 10.920,28.914 C10.370,28.914 9.926,28.468 9.926,27.917 L9.926,23.344 L1.680,31.613 C1.486,31.808 1.231,31.906 0.977,31.906 C0.723,31.906 0.468,31.808 0.274,31.613 C-0.114,31.224 -0.114,30.593 0.274,30.203 L8.520,21.934 L3.960,21.934 C3.410,21.934 2.966,21.488 2.966,20.937 C2.966,20.386 3.410,19.940 3.960,19.940 L10.920,19.940 C10.920,19.940 10.921,19.940 10.921,19.940 C11.050,19.940 11.179,19.967 11.300,20.017 C11.543,20.118 11.737,20.312 11.838,20.556 C11.888,20.678 11.914,20.807 11.914,20.937 L11.914,27.917 ZM10.920,11.963 C10.821,11.963 10.724,11.935 10.629,11.906 L3.906,11.906 C3.356,11.906 2.966,11.516 2.966,10.966 C2.966,10.415 3.410,9.969 3.960,9.969 L8.520,9.969 L0.274,1.699 C-0.114,1.310 -0.114,0.679 0.274,0.289 C0.662,-0.100 1.292,-0.100 1.680,0.289 L9.926,8.559 L9.926,3.986 C9.926,3.435 10.370,2.989 10.920,2.989 C11.469,2.989 11.914,3.435 11.914,3.986 L11.914,10.965 C11.914,11.221 11.817,11.476 11.623,11.671 C11.429,11.865 11.174,11.963 10.920,11.963 ZM20.174,20.222 C20.345,20.047 20.585,19.940 20.863,19.940 L27.823,19.940 C28.372,19.940 28.817,20.386 28.817,20.937 C28.817,21.488 28.372,21.934 27.823,21.934 L23.263,21.934 L31.509,30.203 C31.897,30.593 31.897,31.224 31.509,31.613 C31.314,31.808 31.060,31.906 30.806,31.906 C30.551,31.906 30.297,31.808 30.103,31.613 L21.857,23.344 L21.857,27.917 C21.857,28.468 21.412,28.914 20.863,28.914 C20.313,28.914 19.906,28.457 19.906,27.906 L19.906,21.130 C19.843,20.815 19.916,20.477 20.160,20.232 C20.164,20.228 20.170,20.227 20.174,20.222 Z"></path></g></svg>);
return (
<div className="bg-white p-5 rounded-lg shadow-sm border">
@@ -38,8 +40,9 @@ const Settings = ({
increment={'1'}
min={2}
max={7}
placeholder="#"
unit={t.days}
errorMessage={t.fieldRequired}
errorMessage={t.errorNumberRequired}
/>
</div>
@@ -51,8 +54,9 @@ const Settings = ({
increment={'1'}
min={1}
max={parseInt(simulationDays, 10) || 1}
placeholder="#"
unit={t.days}
errorMessage={t.fieldRequired}
errorMessage={t.errorNumberRequired}
/>
</div>
@@ -68,6 +72,8 @@ const Settings = ({
placeholder={t.auto}
allowEmpty={true}
clearButton={true}
clearButtonText={clearButtonSVG || t.yAxisRangeAutoButton}
clearButtonTitle={t.yAxisRangeAutoButtonTitle}
/>
</div>
<span className="text-gray-500 px-2">-</span>
@@ -81,6 +87,8 @@ const Settings = ({
unit="ng/ml"
allowEmpty={true}
clearButton={true}
clearButtonText={clearButtonSVG || t.yAxisRangeAutoButton}
clearButtonTitle={t.yAxisRangeAutoButtonTitle}
/>
</div>
</div>
@@ -96,7 +104,7 @@ const Settings = ({
increment={'0.5'}
min={0}
placeholder={t.min}
errorMessage={t.fieldRequired}
errorMessage={t.errorNumberRequired}
/>
</div>
<span className="text-gray-500 px-2">-</span>
@@ -108,7 +116,7 @@ const Settings = ({
min={0}
placeholder={t.max}
unit="ng/ml"
errorMessage={t.fieldRequired}
errorMessage={t.errorNumberRequired}
/>
</div>
</div>
@@ -122,8 +130,9 @@ const Settings = ({
onChange={val => onUpdatePkParams('damph', { halfLife: val })}
increment={'0.5'}
min={0.1}
placeholder="#.#"
unit="h"
errorMessage={t.fieldRequired}
errorMessage={t.errorNumberRequired}
/>
</div>
@@ -135,8 +144,9 @@ const Settings = ({
onChange={val => onUpdatePkParams('ldx', { halfLife: val })}
increment={'0.1'}
min={0.1}
placeholder="#.#"
unit="h"
errorMessage={t.fieldRequired}
errorMessage={t.errorNumberRequired}
/>
</div>
<div>
@@ -146,8 +156,9 @@ const Settings = ({
onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })}
increment={'0.1'}
min={0.1}
placeholder="#.#"
unit={t.faster}
errorMessage={t.fieldRequired}
errorMessage={t.errorNumberRequired}
/>
</div>

View File

@@ -97,6 +97,25 @@ const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
setShowErrorTooltip(false);
};
const [tooltipPosition, setTooltipPosition] = React.useState({ top: true, left: true });
React.useEffect(() => {
if (showErrorTooltip && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const tooltipWidth = 300; // Approximate width
const tooltipHeight = 60; // Approximate height
const spaceRight = window.innerWidth - rect.right;
const spaceBottom = window.innerHeight - rect.bottom;
setTooltipPosition({
top: spaceBottom < tooltipHeight + 10,
left: spaceRight < tooltipWidth
});
}
}, [showErrorTooltip]);
return (
<div
ref={containerRef}
@@ -105,7 +124,7 @@ const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
onMouseLeave={handleMouseLeave}
>
<div className="flex items-center">
<div className={`${hasError ? 'ring-2 ring-red-500 rounded-md' : ''}`}>
<div className={`${hasError ? 'ring-2 ring-red-500 rounded-md pointer-events-auto' : ''}`}>
<input
type="text"
value={displayValue}
@@ -113,7 +132,7 @@ const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
onBlur={handleBlur}
onFocus={handleFocus}
placeholder="HH:MM"
className={`p-2 border rounded-md w-24 text-sm text-center ${hasError ? 'bg-red-50 border-transparent' : ''}`}
className={`p-2 border rounded-md w-24 text-sm text-center ${hasError ? 'border-transparent' : ''}`}
/>
</div>
<button
@@ -164,14 +183,21 @@ const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
</div>
)}
{hasError && showErrorTooltip && (
<div className="absolute left-0 top-full mt-1 z-50 bg-white border-2 border-red-500 rounded-md shadow-lg p-2 flex items-start gap-2" style={{ maxWidth: '28rem' }}>
<div
className={`absolute z-50 bg-white ring-2 ring-red-500 rounded-md shadow-lg p-2 flex items-start gap-2 ${
tooltipPosition.top ? 'bottom-full mb-1' : 'top-full mt-1'
} ${
tooltipPosition.left ? 'right-0' : 'left-0'
}`}
style={{ minWidth: '200px', maxWidth: '400px' }}
>
<div className="flex-shrink-0 mt-0.5">
<svg className="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#fa5252"/>
<path d="M17 15.59l-1.41 1.41-3.59-3.59-3.59 3.59-1.41-1.41 3.59-3.59-3.59-3.59 1.41-1.41 3.59 3.59 3.59-3.59 1.41 1.41-3.59 3.59z" fill="white"/>
</svg>
</div>
<span className="text-xs text-red-700 break-words">{errorMessage}</span>
<span className="text-xs text-red-700 whitespace-nowrap">{errorMessage}</span>
</div>
)}
</div>