import React, { useMemo, useState } from 'react'; import { Adapt, Button, Input, Paragraph, Select, Separator, Sheet, Switch, Text, TextArea, XStack, YStack } from 'tamagui'; import { getIcon } from './IconMapper.jsx'; import { ColorPickerPopup, DateTimePickerPopup } from './FieldControlPickers.jsx'; import { getTypographyRoleProps } from '../styles/index.js'; import { pickFile } from '../../platform/compat.js'; const ChevronDown = getIcon('chevron-down'); const ChevronUp = getIcon('chevron-up'); const CheckIcon = getIcon('check'); function helperTextColor(error) { return error ? '$danger' : '$textMuted'; } function fieldChrome(orientation, error, border) { const embedded = orientation === 'embedded'; const borderless = border === 0; return { position: embedded ? 'relative' : 'static', borderWidth: embedded && !borderless ? 1 : 0, borderColor: error ? '$danger' : '$lineSubtle', borderRadius: embedded ? '$radiusMd' : '$0', backgroundColor: embedded && !borderless ? '$bgPanel' : 'transparent', paddingTop: embedded && !borderless ? '$3.5' : '$0', paddingRight: embedded && !borderless ? '$3' : '$0', paddingBottom: embedded && !borderless ? '$3' : '$0', paddingLeft: embedded && !borderless ? '$3' : '$0', gap: embedded ? '$2' : '$1.5' }; } function inputChrome(orientation, error, border) { const embedded = orientation === 'embedded'; const borderless = border === 0; return { backgroundColor: embedded || borderless ? 'transparent' : '$bgPanel', borderColor: embedded || borderless ? 'transparent' : (error ? '$danger' : '$lineSubtle'), borderWidth: embedded || borderless ? 0 : 1, paddingHorizontal: embedded ? '$0' : undefined, paddingVertical: embedded ? '$0' : undefined, focusStyle: embedded ? { borderColor: 'transparent', outlineWidth: 0 } : { borderColor: error ? '$danger' : '$accent' } }; } function renderHelper(error, hint) { if (!error && !hint) { return null; } return ( {error || hint} ); } function parseAreaPlacement(value, fallback = 'inside') { const raw = String(value || fallback) .split(',') .map((part) => part.trim()) .filter(Boolean); const hints = new Set(raw.length ? raw : [fallback]); return { mode: hints.has('outside') ? 'outside' : 'inside', hints, }; } function renderActionNode(action, context, fallbackColor = '$textMuted') { if (!action) { return null; } if (React.isValidElement(action)) { return action; } if (typeof action === 'function') { const rendered = action(context); if (React.isValidElement(rendered)) { return rendered; } const ActionComponent = action; return ; } if (typeof action === 'string') { const IconComponent = getIcon(action); return IconComponent ? : null; } if (action && typeof action === 'object' && action.icon) { const IconComponent = typeof action.icon === 'string' ? getIcon(action.icon) : action.icon; return IconComponent ? : null; } return null; } function chipProps(selected, disabled) { return selected ? { backgroundColor: '$accentBg', color: '$accent', borderColor: '$accent', borderWidth: 1, hoverStyle: { backgroundColor: '$accentBgHover' }, pressStyle: { backgroundColor: '$accentBgHover' }, disabled, } : { backgroundColor: '$bgPanel', color: '$textPrimary', borderColor: '$lineSubtle', borderWidth: 1, hoverStyle: { backgroundColor: '$bgPage', borderColor: '$lineStrong' }, pressStyle: { backgroundColor: '$bgPage' }, disabled, }; } function DropdownControl({ value, options, onValueChange, placeholder, disabled, error, inputProps = {}, fieldProps = {} }) { return ( ); } function ColorSwatch({ value }) { const normalized = String(value || '').trim(); const isHex = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(normalized); return ( ); } function EmbeddedLabel({ label, required = false }) { if (!label) return null; return ( {label}{required ? ' *' : ''} ); } function resolveOptionLabel(options, value) { const match = (options || []).find((option) => String(option.value) === String(value ?? '')); return match ? match.label : String(value ?? ''); } function StaticTextValue({ value, fieldProps = {}, multiline = false }) { const textValue = value == null || value === '' ? '—' : String(value); return ( {textValue} ); } async function handleFilePick(controlId, emitValue, props = {}) { const selection = await pickFile({ accept: props.accept || '*', readAs: props.readAs || null, }); if (!selection?.file) { return; } emitValue(selection.file, selection); } export function FieldControl({ name, id, label, value, onChange, onValueChange, placeholder = '', hint = '', helperText = '', error = '', orientation = 'vertical', type = 'text', format = '', options = [], rows = 4, cols = -1, readOnly = false, locked = false, disabled = false, active = true, visible = true, border, render = null, fieldProps = {}, required = false, children = null, accept, readAs, leftAction = null, rightAction = null, leftAreaPlacement = 'inside', rightAreaPlacement = 'inside', leftActionOnPress = null, rightActionOnPress = null, leftIcon = null, rightIcon = null, leftIconPlacement = 'inside', rightIconPlacement = 'inside', leftIconOnPress = null, rightIconOnPress = null }) { if (!visible) { return null; } if (type === 'select') { type = 'dropdown'; } const controlId = id || name || label || 'field'; const chrome = fieldChrome(orientation, error, border); const commonInputProps = inputChrome(orientation, error, border); const normalizedValue = value ?? ''; const embedded = orientation === 'embedded'; const borderless = border === 0; const staticMode = readOnly || locked || !active; const [showPassword, setShowPassword] = useState(false); const [autocompleteOpen, setAutocompleteOpen] = useState(false); const [colorPickerOpen, setColorPickerOpen] = useState(false); const [dateTimePickerOpen, setDateTimePickerOpen] = useState(false); const isDateLike = type === 'date' || type === 'time' || type === 'date-time'; const effectiveLeftAction = leftAction || leftIcon; const effectiveRightAction = type === 'password' ? (rightAction || rightIcon || (showPassword ? 'visibility-off' : 'visibility')) : (isDateLike ? (rightAction || rightIcon || (type === 'time' ? 'clock' : 'calendar')) : (rightAction || rightIcon)); const effectiveLeftActionOnPress = leftActionOnPress || leftIconOnPress; const effectiveRightActionOnPress = type === 'password' ? (rightActionOnPress || rightIconOnPress || (() => setShowPassword((state) => !state))) : (isDateLike ? (rightActionOnPress || rightIconOnPress || (() => setDateTimePickerOpen((open) => !open))) : (rightActionOnPress || rightIconOnPress)); const normalizedLeftPlacement = parseAreaPlacement(leftAreaPlacement || leftIconPlacement, 'inside'); const normalizedRightPlacement = parseAreaPlacement(rightAreaPlacement || rightIconPlacement, 'inside'); const hasInsideLeftAction = Boolean(effectiveLeftAction) && normalizedLeftPlacement.mode === 'inside'; const hasInsideRightAction = Boolean(effectiveRightAction) && normalizedRightPlacement.mode === 'inside'; const emitValue = (nextValue, eventLike = undefined) => { if (typeof onChange === 'function') { onChange(controlId, nextValue, eventLike); return; } if (typeof onValueChange === 'function') { onValueChange(nextValue, eventLike); } }; const decoratedInputProps = { ...commonInputProps, paddingLeft: hasInsideLeftAction ? '$8' : commonInputProps.paddingLeft, paddingRight: hasInsideRightAction ? '$8' : commonInputProps.paddingRight, }; const renderActionArea = (action, side, placement, onPress) => { const actionContext = { id: controlId, name: controlId, side, placement: placement.mode, placementHints: placement.hints, value: normalizedValue, disabled, readOnly, locked, active, error, orientation, onPress, showPassword, setShowPassword, }; const actionNode = renderActionNode(action, actionContext); if (!actionNode) { return null; } const button = typeof onPress === 'function' ? ( ) : actionNode; if (placement.mode === 'outside') { return button; } return ( {button} ); }; const wrapControlWithIcons = (node, multiline = false) => { const leftOutside = effectiveLeftAction && normalizedLeftPlacement.mode === 'outside' ? renderActionArea(effectiveLeftAction, 'left', normalizedLeftPlacement, effectiveLeftActionOnPress) : null; const rightOutside = effectiveRightAction && normalizedRightPlacement.mode === 'outside' ? renderActionArea(effectiveRightAction, 'right', normalizedRightPlacement, effectiveRightActionOnPress) : null; const leftInside = effectiveLeftAction && normalizedLeftPlacement.mode === 'inside' ? renderActionArea(effectiveLeftAction, 'left', normalizedLeftPlacement, effectiveLeftActionOnPress) : null; const rightInside = effectiveRightAction && normalizedRightPlacement.mode === 'inside' ? renderActionArea(effectiveRightAction, 'right', normalizedRightPlacement, effectiveRightActionOnPress) : null; const inner = leftInside || rightInside ? ( {node} {leftInside} {rightInside} ) : node; if (leftOutside || rightOutside) { return ( {leftOutside} {inner} {rightOutside} ); } return inner; }; if (type === 'divider') { return ; } if (type === 'title') { return ( {label} {hint ? ( {hint} ) : null} ); } const control = useMemo(() => { if (type === 'custom') { return children || null; } if (typeof render === 'function') { return render({ id: controlId, name: controlId, value: normalizedValue, placeholder, disabled, readOnly, locked, active, error, orientation, inputProps: commonInputProps, onChange: emitValue, onValueChange: emitValue }); } if (type === 'dropdown') { if (staticMode) { return ( ); } return ( ); } if (type === 'autocomplete') { if (staticMode) { return ; } const filteredOptions = String(normalizedValue || '').trim() ? options.filter((option) => option.label.toLowerCase().includes(String(normalizedValue).toLowerCase())) : options; return ( {wrapControlWithIcons( setAutocompleteOpen(true)} onBlur={() => { setTimeout(() => { setAutocompleteOpen(false); }, 120); }} onChangeText={emitValue} /> )} {autocompleteOpen && filteredOptions.length ? ( {filteredOptions.slice(0, 8).map((option) => ( ))} ) : null} ); } if (type === 'number') { if (staticMode) { return ; } return ( emitValue(nextValue === '' ? '' : Number(nextValue))} /> ); } if (type === 'email') { if (staticMode) { return ; } return ( ); } if (type === 'password') { if (staticMode) { return ; } return wrapControlWithIcons( ); } if (type === 'date' || type === 'time' || type === 'date-time') { if (staticMode) { return ; } const defaultPlaceholder = type === 'time' ? 'HH:MM' : (type === 'date-time' ? 'YYYY-MM-DDTHH:MM' : 'YYYY-MM-DD'); return ( {wrapControlWithIcons( )} {dateTimePickerOpen ? ( emitValue(nextValue)} onClose={() => setDateTimePickerOpen(false)} /> ) : null} ); } if (type === 'color') { if (staticMode) { return ( ); } return ( {colorPickerOpen ? ( emitValue(String(nextValue).toUpperCase())} onClose={() => setColorPickerOpen(false)} /> ) : null} ); } if (type === 'checkbox') { return ( emitValue(next)} backgroundColor={normalizedValue ? '$accent' : '$lineStrong'} borderColor={error ? '$danger' : 'transparent'} borderWidth={error ? 1 : 0} > {normalizedValue ? 'Enabled' : 'Disabled'} ); } if (type === 'radio') { const selectedValue = String(normalizedValue ?? ''); return ( {options.map((option) => { const selected = selectedValue === String(option.value); return ( ); })} ); } if (type === 'multiselect') { const selectedValues = Array.isArray(normalizedValue) ? normalizedValue.map(String) : []; return ( {options.map((option) => { const selected = selectedValues.includes(String(option.value)); return ( ); })} ); } if (type === 'file') { return ( {normalizedValue?.name || 'No file selected'} ); } if (type === 'textarea') { if (staticMode) { return ; } return (