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 <TimeInput
value={dev.time} value={dev.time}
onChange={newTime => onDeviationChange(index, 'time', newTime)} onChange={newTime => onDeviationChange(index, 'time', newTime)}
errorMessage={t.timeRequired} errorMessage={t.errorTimeRequired}
/> />
<div className="w-32"> <div className="w-32">
<NumericInput <NumericInput

View File

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

View File

@@ -11,6 +11,8 @@ const NumericInput = ({
align = 'right', // 'left', 'center', 'right' align = 'right', // 'left', 'center', 'right'
allowEmpty = false, // Allow empty value (e.g., for "Auto" mode) allowEmpty = false, // Allow empty value (e.g., for "Auto" mode)
clearButton = false, // Show clear button (with allowEmpty=true) 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 errorMessage = 'This field is required' // Error message for mandatory empty fields
}) => { }) => {
const [hasError, setHasError] = React.useState(false); const [hasError, setHasError] = React.useState(false);
@@ -166,6 +168,7 @@ const NumericInput = ({
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
<div className="relative inline-flex items-center"> <div className="relative inline-flex items-center">
<div className={`relative inline-flex items-center ${hasError ? 'ring-2 ring-red-500 rounded-md pointer-events-auto' : ''}`}>
<button <button
onClick={() => updateValue(-1)} 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" 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"
@@ -182,7 +185,7 @@ const NumericInput = ({
onFocus={handleFocus} onFocus={handleFocus}
placeholder={placeholder} placeholder={placeholder}
style={{ width: '4em', minWidth: '4em', maxWidth: '8em' }} style={{ width: '4em', minWidth: '4em', maxWidth: '8em' }}
className={`p-2 border-t border-b text-sm ${getAlignmentClass()} ${hasError ? 'bg-red-50' : ''}`} className={`p-2 border-t border-b text-sm ${getAlignmentClass()} ${hasError ? 'bg-transparent' : ''}`}
/> />
<button <button
onClick={() => updateValue(1)} onClick={() => updateValue(1)}
@@ -196,21 +199,20 @@ const NumericInput = ({
onClick={handleClear} 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" 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} 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"> <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" /> <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> </svg>
</button>
)}
{hasError && (
<div className="absolute inset-0 border-2 border-red-500 rounded-md pointer-events-none" style={{ zIndex: 10 }} />
)} )}
</button>)}
</div>
{unit && <span className="ml-2 text-gray-500 text-sm whitespace-nowrap">{unit}</span>} {unit && <span className="ml-2 text-gray-500 text-sm whitespace-nowrap">{unit}</span>}
</div> </div>
{hasError && showErrorTooltip && ( {hasError && showErrorTooltip && (
<div <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.top ? 'bottom-full mb-1' : 'top-full mt-1'
} ${ } ${
tooltipPosition.left ? 'right-0' : 'left-0' tooltipPosition.left ? 'right-0' : 'left-0'

View File

@@ -12,6 +12,8 @@ const Settings = ({
t t
}) => { }) => {
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, simulationDays, displayedDays } = uiSettings; 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 ( return (
<div className="bg-white p-5 rounded-lg shadow-sm border"> <div className="bg-white p-5 rounded-lg shadow-sm border">
@@ -38,8 +40,9 @@ const Settings = ({
increment={'1'} increment={'1'}
min={2} min={2}
max={7} max={7}
placeholder="#"
unit={t.days} unit={t.days}
errorMessage={t.fieldRequired} errorMessage={t.errorNumberRequired}
/> />
</div> </div>
@@ -51,8 +54,9 @@ const Settings = ({
increment={'1'} increment={'1'}
min={1} min={1}
max={parseInt(simulationDays, 10) || 1} max={parseInt(simulationDays, 10) || 1}
placeholder="#"
unit={t.days} unit={t.days}
errorMessage={t.fieldRequired} errorMessage={t.errorNumberRequired}
/> />
</div> </div>
@@ -68,6 +72,8 @@ const Settings = ({
placeholder={t.auto} placeholder={t.auto}
allowEmpty={true} allowEmpty={true}
clearButton={true} clearButton={true}
clearButtonText={clearButtonSVG || t.yAxisRangeAutoButton}
clearButtonTitle={t.yAxisRangeAutoButtonTitle}
/> />
</div> </div>
<span className="text-gray-500 px-2">-</span> <span className="text-gray-500 px-2">-</span>
@@ -81,6 +87,8 @@ const Settings = ({
unit="ng/ml" unit="ng/ml"
allowEmpty={true} allowEmpty={true}
clearButton={true} clearButton={true}
clearButtonText={clearButtonSVG || t.yAxisRangeAutoButton}
clearButtonTitle={t.yAxisRangeAutoButtonTitle}
/> />
</div> </div>
</div> </div>
@@ -96,7 +104,7 @@ const Settings = ({
increment={'0.5'} increment={'0.5'}
min={0} min={0}
placeholder={t.min} placeholder={t.min}
errorMessage={t.fieldRequired} errorMessage={t.errorNumberRequired}
/> />
</div> </div>
<span className="text-gray-500 px-2">-</span> <span className="text-gray-500 px-2">-</span>
@@ -108,7 +116,7 @@ const Settings = ({
min={0} min={0}
placeholder={t.max} placeholder={t.max}
unit="ng/ml" unit="ng/ml"
errorMessage={t.fieldRequired} errorMessage={t.errorNumberRequired}
/> />
</div> </div>
</div> </div>
@@ -122,8 +130,9 @@ const Settings = ({
onChange={val => onUpdatePkParams('damph', { halfLife: val })} onChange={val => onUpdatePkParams('damph', { halfLife: val })}
increment={'0.5'} increment={'0.5'}
min={0.1} min={0.1}
placeholder="#.#"
unit="h" unit="h"
errorMessage={t.fieldRequired} errorMessage={t.errorNumberRequired}
/> />
</div> </div>
@@ -135,8 +144,9 @@ const Settings = ({
onChange={val => onUpdatePkParams('ldx', { halfLife: val })} onChange={val => onUpdatePkParams('ldx', { halfLife: val })}
increment={'0.1'} increment={'0.1'}
min={0.1} min={0.1}
placeholder="#.#"
unit="h" unit="h"
errorMessage={t.fieldRequired} errorMessage={t.errorNumberRequired}
/> />
</div> </div>
<div> <div>
@@ -146,8 +156,9 @@ const Settings = ({
onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })} onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })}
increment={'0.1'} increment={'0.1'}
min={0.1} min={0.1}
placeholder="#.#"
unit={t.faster} unit={t.faster}
errorMessage={t.fieldRequired} errorMessage={t.errorNumberRequired}
/> />
</div> </div>

View File

@@ -97,6 +97,25 @@ const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
setShowErrorTooltip(false); 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 ( return (
<div <div
ref={containerRef} ref={containerRef}
@@ -105,7 +124,7 @@ const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
<div className="flex items-center"> <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 <input
type="text" type="text"
value={displayValue} value={displayValue}
@@ -113,7 +132,7 @@ const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
placeholder="HH:MM" 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> </div>
<button <button
@@ -164,14 +183,21 @@ const TimeInput = ({ value, onChange, errorMessage = 'Time is required' }) => {
</div> </div>
)} )}
{hasError && showErrorTooltip && ( {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"> <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"> <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"/> <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"/> <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> </svg>
</div> </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>
)} )}
</div> </div>

View File

@@ -21,8 +21,8 @@ export const getDefaultState = () => ({
uiSettings: { uiSettings: {
showDayTimeOnXAxis: true, showDayTimeOnXAxis: true,
chartView: 'damph', chartView: 'damph',
yAxisMin: '', yAxisMin: '0',
yAxisMax: '', yAxisMax: '16',
simulationDays: '3', simulationDays: '3',
displayedDays: '2', displayedDays: '2',
} }

View File

@@ -51,6 +51,8 @@ export const de = {
days: "Tage", days: "Tage",
displayedDays: "Angezeigte Tage", displayedDays: "Angezeigte Tage",
yAxisRange: "Y-Achsen-Bereich", yAxisRange: "Y-Achsen-Bereich",
yAxisRangeAutoButton: "A",
yAxisRangeAutoButtonTitle: "Bereich automatisch anhand des Datenbereichs bestimmen",
auto: "Auto", auto: "Auto",
therapeuticRange: "Therapeutischer Bereich", therapeuticRange: "Therapeutischer Bereich",
dAmphetamineParameters: "d-Amphetamin Parameter", dAmphetamineParameters: "d-Amphetamin Parameter",
@@ -74,8 +76,8 @@ export const de = {
disclaimer: "Dieses Tool dient ausschließlich zu Illustrations- und Informationszwecken. Es ist kein medizinisches Gerät und ersetzt nicht die Beratung durch einen Arzt oder Apotheker. Alle Berechnungen sind Simulationen, die auf allgemeinen pharmakokinetischen Modellen basieren und von individuellen Faktoren erheblich abweichen können. Bitte konsultiere deinen behandelnden Arzt, bevor du Anpassungen an deiner Medikation vornimmst.", disclaimer: "Dieses Tool dient ausschließlich zu Illustrations- und Informationszwecken. Es ist kein medizinisches Gerät und ersetzt nicht die Beratung durch einen Arzt oder Apotheker. Alle Berechnungen sind Simulationen, die auf allgemeinen pharmakokinetischen Modellen basieren und von individuellen Faktoren erheblich abweichen können. Bitte konsultiere deinen behandelnden Arzt, bevor du Anpassungen an deiner Medikation vornimmst.",
// Field validation // Field validation
fieldRequired: "Dieses Feld ist erforderlich", errorNumberRequired: "Bitte gib eine gültige Zahl ein.",
timeRequired: "Zeitangabe ist erforderlich" errorTimeRequired: "Bitte gib eine gültige Zeitangabe ein."
}; };
export default de; export default de;

View File

@@ -51,6 +51,8 @@ export const en = {
days: "Days", days: "Days",
displayedDays: "Displayed Days", displayedDays: "Displayed Days",
yAxisRange: "Y-Axis Range", yAxisRange: "Y-Axis Range",
yAxisRangeAutoButton: "A",
yAxisRangeAutoButtonTitle: "Determine range automatically based on data range",
auto: "Auto", auto: "Auto",
therapeuticRange: "Therapeutic Range", therapeuticRange: "Therapeutic Range",
dAmphetamineParameters: "d-Amphetamine Parameters", dAmphetamineParameters: "d-Amphetamine Parameters",
@@ -74,8 +76,8 @@ export const en = {
disclaimer: "This tool is for illustration and information purposes only. It is not a medical device and does not replace consultation with a doctor or pharmacist. All calculations are simulations based on general pharmacokinetic models and may differ significantly from individual factors. Please consult your treating physician before making adjustments to your medication.", disclaimer: "This tool is for illustration and information purposes only. It is not a medical device and does not replace consultation with a doctor or pharmacist. All calculations are simulations based on general pharmacokinetic models and may differ significantly from individual factors. Please consult your treating physician before making adjustments to your medication.",
// Field validation // Field validation
fieldRequired: "This field is required", errorNumberRequired: "Please enter a valid number.",
timeRequired: "Time is required" errorTimeRequired: "Please enter a valid time."
}; };
export default en; export default en;