/** * SchemaFieldFactory.jsx * ======================= * Renders a single form field based on its FieldMetadata. * Maps component types to actual React input elements. * * Supported components: * segmented → Segmented radio-group (Male=1 / Female=0) * toggle → Styled checkbox/switch (binary 0/1) * select → Native with min/max/step * text → (fallback) */ import { useController } from 'react-hook-form'; import MedicalTooltip from './MedicalTooltip'; const SLIDER_CLASS = `flex-1 h-2 rounded-full appearance-none cursor-pointer bg-gray-200 accent-blue-600 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-600 [&::-webkit-slider-thumb]:shadow-md`; function ValueBadge({ value }) { return ( {value} ); } /** * Toggle/Switch — binary 0/1 field rendered as a styled checkbox. */ function ToggleField({ field, meta, error }) { const isOn = field.value === 1 || field.value === true; return (
{error &&

{error}

}
); } /** * Segmented Button Group — premium radio-group for distinct binary states (e.g., Sex: Male/Female). * Active state uses bg-blue-600 with shadow for clear visual distinction. * Smooth 200ms transitions on all interactive elements. */ function SegmentedField({ field, meta, error }) { const val = field.value ?? 0; return (
{error &&

{error}

}
); } /** * Dropdown — renders a native field.onChange(e.target.value)} > {options.map((opt) => ( ))} {error &&

{error}

}
); } /** * Slider — range input for small-range integer fields. * Renders the current value as a prominent live badge next to the label. */ function SliderField({ field, meta, error }) { const min = meta.validation.minimum ?? 0; const max = meta.validation.maximum ?? 10; const val = field.value ?? min; return (
{min} field.onChange(parseInt(e.target.value, 10))} className={SLIDER_CLASS} /> {val}
{error &&

{error}

}
); } /** * Number Input — for float or wide-range integer fields. * Shows a twin-bound slider alongside the number input when min/max bounds exist. * Both controls share the exact same field.value — moving the slider updates the * number box instantly, and typing in the box moves the slider instantly. * When slider is visible, a live value badge is shown next to the label. */ function NumberField({ field, meta, error }) { const min = meta.validation.minimum; const max = meta.validation.maximum; const step = meta.validation.step ?? (meta.type === 'number' ? 0.1 : 1); // Show a slider when we have finite numeric bounds and the range isn't excessive const showSlider = min !== undefined && max !== undefined && (max - min) <= 500; const currentVal = field.value ?? min ?? 0; return (
{showSlider && ( field.onChange(parseFloat(e.target.value))} className={SLIDER_CLASS} /> )} { const raw = e.target.value; if (raw === '') { field.onChange(''); return; } const parsed = Number(raw); if (isNaN(parsed)) return; // Clamp within bounds to match slider behaviour let clamped = parsed; if (min !== undefined) clamped = Math.max(min, clamped); if (max !== undefined) clamped = Math.min(max, clamped); field.onChange(clamped); }} className={`input-field w-24 ${error ? 'border-red-400 ring-1 ring-red-400' : ''}`} />
{error &&

{error}

} {(min !== undefined || max !== undefined) && (

{min !== undefined && `Min: ${min}`} {min !== undefined && max !== undefined && ' | '} {max !== undefined && `Max: ${max}`}

)}
); } /** * Text Input — fallback for string fields without enum. */ function TextField({ field, meta, error }) { return (
{error &&

{error}

}
); } // ── Component Registry ── const COMPONENT_MAP = { segmented: SegmentedField, toggle: ToggleField, select: SelectField, slider: SliderField, number: NumberField, text: TextField, }; /** * SchemaFieldFactory — renders a single dynamic form field. * * @param {{ * meta: import('../utils/schemaFieldParser').FieldMetadata, * control: import('react-hook-form').Control, * errors: object * }} props */ export default function SchemaFieldFactory({ meta, control, errors }) { const { field, fieldState: { error }, } = useController({ name: meta.name, control, defaultValue: meta.default ?? (meta.type === 'integer' || meta.type === 'number' ? 0 : ''), rules: { required: meta.validation.required ? `${meta.title} is required` : false, min: meta.validation.minimum, max: meta.validation.maximum, }, }); const FieldComponent = COMPONENT_MAP[meta.component] || TextField; const errMsg = error?.message || errors?.[meta.name]; return ( ); }