'use client'; import React, { forwardRef } from 'react'; import classNames from 'classnames'; import dynamic from 'next/dynamic'; import { CircleHelp } from 'lucide-react'; import { getDoc } from '@/docs'; import { openDoc } from '@/components/DocModal'; import { ConfigDoc, GroupedSelectOption, SelectOption } from '@/types'; const Select = dynamic(() => import('react-select'), { ssr: false }); const labelClasses = 'block text-xs mb-1 mt-2 text-gray-300'; const inputClasses = 'w-full text-sm px-3 py-1 bg-gray-800 border border-gray-700 rounded-sm focus:ring-2 focus:ring-gray-600 focus:border-transparent'; export interface InputProps { label?: string; docKey?: string | null; doc?: ConfigDoc | null; className?: string; placeholder?: string; required?: boolean; } export interface TextInputProps extends InputProps { value: string; onChange: (value: string) => void; type?: 'text' | 'password'; disabled?: boolean; } export const TextInput = forwardRef((props: TextInputProps, ref) => { const { label, value, onChange, placeholder, required, disabled, type = 'text', className, docKey = null } = props; let { doc } = props; if (!doc && docKey) { doc = getDoc(docKey); } return (
{label && ( )} { if (!disabled) onChange(e.target.value); }} className={`${inputClasses} ${disabled ? 'opacity-30 cursor-not-allowed' : ''}`} placeholder={placeholder} required={required} disabled={disabled} />
); }); // 👇 Helpful for debugging TextInput.displayName = 'TextInput'; export interface NumberInputProps extends InputProps { value: number; onChange: (value: number) => void; min?: number; max?: number; } export const NumberInput = (props: NumberInputProps) => { const { label, value, onChange, placeholder, required, min, max, docKey = null } = props; let { doc } = props; if (!doc && docKey) { doc = getDoc(docKey); } // Add controlled internal state to properly handle partial inputs const [inputValue, setInputValue] = React.useState(value ?? ''); // Sync internal state with prop value React.useEffect(() => { setInputValue(value ?? ''); }, [value]); return (
{label && ( )} { const rawValue = e.target.value; // Update the input display with the raw value setInputValue(rawValue); // Handle empty or partial inputs if (rawValue === '' || rawValue === '-') { // For empty or partial negative input, don't call onChange yet return; } const numValue = Number(rawValue); // Only apply constraints and call onChange when we have a valid number if (!isNaN(numValue)) { let constrainedValue = numValue; // Apply min/max constraints if they exist if (min !== undefined && constrainedValue < min) { constrainedValue = min; } if (max !== undefined && constrainedValue > max) { constrainedValue = max; } onChange(constrainedValue); } }} className={inputClasses} placeholder={placeholder} required={required} min={min} max={max} step="any" />
); }; export interface SelectInputProps extends InputProps { value: string; disabled?: boolean; onChange: (value: string) => void; options: GroupedSelectOption[] | SelectOption[]; } export const SelectInput = (props: SelectInputProps) => { const { label, value, onChange, options, docKey = null } = props; let { doc } = props; if (!doc && docKey) { doc = getDoc(docKey); } let selectedOption: SelectOption | undefined; if (options && options.length > 0) { // see if grouped options if ('options' in options[0]) { selectedOption = (options as GroupedSelectOption[]) .flatMap(group => group.options) .find(opt => opt.value === value); } else { selectedOption = (options as SelectOption[]).find(opt => opt.value === value); } } return (
{label && ( )}