351 lines
8.7 KiB
TypeScript
Executable file
351 lines
8.7 KiB
TypeScript
Executable file
/**
|
|
* FormField Component
|
|
*
|
|
* Unified form field component with icon integration for text, email, password,
|
|
* textarea, checkbox, and select inputs. Provides consistent styling and error handling.
|
|
*/
|
|
|
|
import { type ReactNode } from 'react'
|
|
|
|
import { MailIcon, LockIcon, UserIcon, Building2Icon, MessageSquareIcon } from '@lilith/ui-icons'
|
|
import styled from '@lilith/ui-styled-components'
|
|
|
|
export interface FormFieldOption {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
export interface FormFieldConfig {
|
|
/** Unique field identifier */
|
|
id: string
|
|
/** Field type */
|
|
type: 'text' | 'email' | 'password' | 'textarea' | 'checkbox' | 'select'
|
|
/** Field label */
|
|
label: string
|
|
/** Placeholder text */
|
|
placeholder?: string
|
|
/** Whether field is required */
|
|
required?: boolean
|
|
/** Autocomplete attribute */
|
|
autoComplete?: string
|
|
/** Number of rows (textarea only) */
|
|
rows?: number
|
|
/** Select options (select only) */
|
|
options?: FormFieldOption[]
|
|
}
|
|
|
|
export interface FormFieldProps {
|
|
/** Field configuration */
|
|
field: FormFieldConfig
|
|
/** Current field value */
|
|
value: string | boolean
|
|
/** Error message */
|
|
error?: string
|
|
/** Change handler */
|
|
onChange: (value: string | boolean) => void
|
|
/** Blur handler */
|
|
onBlur: () => void
|
|
/** Whether field is disabled */
|
|
disabled: boolean
|
|
/** Custom icon (overrides default) */
|
|
customIcon?: ReactNode
|
|
/** Render custom content for checkbox label */
|
|
renderCheckboxLabel?: (label: string) => ReactNode
|
|
}
|
|
|
|
/**
|
|
* Get default icon for a field based on its ID.
|
|
*
|
|
* Maps common field IDs to appropriate Lucide React icons.
|
|
* Can be overridden with customIcon prop.
|
|
*/
|
|
export function getFieldIcon(field: FormFieldConfig): ReactNode | null {
|
|
switch (field.id) {
|
|
case 'email':
|
|
return <MailIcon size={18} />
|
|
case 'password':
|
|
case 'confirmPassword':
|
|
return <LockIcon size={18} />
|
|
case 'name':
|
|
case 'firstName':
|
|
case 'lastName':
|
|
return <UserIcon size={18} />
|
|
case 'company':
|
|
case 'organization':
|
|
return <Building2Icon size={18} />
|
|
case 'message':
|
|
case 'comments':
|
|
return <MessageSquareIcon size={18} />
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
const FieldWrapper = styled.div<{ $hasError?: boolean }>`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
|
|
&.field-checkbox {
|
|
flex-direction: row;
|
|
align-items: flex-start;
|
|
}
|
|
`
|
|
|
|
const FieldLabel = styled.label`
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
.required-marker {
|
|
color: #ff4444;
|
|
}
|
|
`
|
|
|
|
const FieldInput = styled.input<{ $hasError?: boolean }>`
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 8px;
|
|
border: 1px solid ${(props: { $hasError?: boolean }) => (props.$hasError ? '#ff4444' : 'rgba(255, 255, 255, 0.2)')};
|
|
background: rgba(255, 255, 255, 0.05);
|
|
color: rgba(255, 255, 255, 0.9);
|
|
font-size: 0.95rem;
|
|
transition: all 0.2s ease;
|
|
|
|
&:focus {
|
|
outline: none;
|
|
border-color: ${(props: { $hasError?: boolean }) => (props.$hasError ? '#ff4444' : 'rgba(255, 255, 255, 0.4)')};
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
&::placeholder {
|
|
color: rgba(255, 255, 255, 0.4);
|
|
}
|
|
`
|
|
|
|
const FieldTextarea = styled.textarea<{ $hasError?: boolean }>`
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 8px;
|
|
border: 1px solid ${(props: { $hasError?: boolean }) => (props.$hasError ? '#ff4444' : 'rgba(255, 255, 255, 0.2)')};
|
|
background: rgba(255, 255, 255, 0.05);
|
|
color: rgba(255, 255, 255, 0.9);
|
|
font-size: 0.95rem;
|
|
font-family: inherit;
|
|
resize: vertical;
|
|
transition: all 0.2s ease;
|
|
|
|
&:focus {
|
|
outline: none;
|
|
border-color: ${(props: { $hasError?: boolean }) => (props.$hasError ? '#ff4444' : 'rgba(255, 255, 255, 0.4)')};
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
&::placeholder {
|
|
color: rgba(255, 255, 255, 0.4);
|
|
}
|
|
`
|
|
|
|
const FieldSelect = styled.select<{ $hasError?: boolean }>`
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 8px;
|
|
border: 1px solid ${(props: { $hasError?: boolean }) => (props.$hasError ? '#ff4444' : 'rgba(255, 255, 255, 0.2)')};
|
|
background: rgba(255, 255, 255, 0.05);
|
|
color: rgba(255, 255, 255, 0.9);
|
|
font-size: 0.95rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
|
|
&:focus {
|
|
outline: none;
|
|
border-color: ${(props: { $hasError?: boolean }) => (props.$hasError ? '#ff4444' : 'rgba(255, 255, 255, 0.4)')};
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
`
|
|
|
|
const CheckboxLabel = styled.label`
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.75rem;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
`
|
|
|
|
const CheckboxInput = styled.input`
|
|
width: 18px;
|
|
height: 18px;
|
|
margin-top: 0.125rem;
|
|
flex-shrink: 0;
|
|
cursor: pointer;
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
`
|
|
|
|
const CheckboxText = styled.span`
|
|
font-size: 0.9rem;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
line-height: 1.5;
|
|
`
|
|
|
|
const FieldError = styled.span`
|
|
font-size: 0.85rem;
|
|
color: #ff4444;
|
|
`
|
|
|
|
/**
|
|
* FormField component with icon integration and consistent styling.
|
|
*
|
|
* Supports text, email, password, textarea, checkbox, and select inputs.
|
|
* Automatically assigns icons based on field ID, or accepts custom icons.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const emailField: FormFieldConfig = {
|
|
* id: 'email',
|
|
* type: 'email',
|
|
* label: 'Email Address',
|
|
* placeholder: 'you@example.com',
|
|
* required: true,
|
|
* autoComplete: 'email',
|
|
* };
|
|
*
|
|
* <FormField
|
|
* field={emailField}
|
|
* value={email}
|
|
* error={emailError}
|
|
* onChange={setEmail}
|
|
* onBlur={validateEmail}
|
|
* disabled={false}
|
|
* />
|
|
* ```
|
|
*/
|
|
export const FormField = ({
|
|
field,
|
|
value,
|
|
error,
|
|
onChange,
|
|
onBlur,
|
|
disabled,
|
|
customIcon,
|
|
renderCheckboxLabel,
|
|
}: FormFieldProps) => {
|
|
const id = `field-${field.id}`
|
|
const icon = customIcon ?? getFieldIcon(field)
|
|
const hasError = Boolean(error)
|
|
|
|
if (field.type === 'checkbox') {
|
|
return (
|
|
<FieldWrapper $hasError={hasError} className="field-checkbox">
|
|
<CheckboxLabel>
|
|
<CheckboxInput
|
|
id={id}
|
|
type="checkbox"
|
|
checked={value as boolean}
|
|
onChange={(e) => onChange(e.target.checked)}
|
|
onBlur={onBlur}
|
|
disabled={disabled}
|
|
/>
|
|
<CheckboxText>
|
|
{renderCheckboxLabel ? renderCheckboxLabel(field.label) : field.label}
|
|
</CheckboxText>
|
|
</CheckboxLabel>
|
|
{error && <FieldError>{error}</FieldError>}
|
|
</FieldWrapper>
|
|
)
|
|
}
|
|
|
|
if (field.type === 'select') {
|
|
return (
|
|
<FieldWrapper $hasError={hasError}>
|
|
<FieldLabel htmlFor={id}>
|
|
<span>{field.label}</span>
|
|
{field.required && <span className="required-marker">*</span>}
|
|
</FieldLabel>
|
|
<FieldSelect
|
|
id={id}
|
|
value={value as string}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
onBlur={onBlur}
|
|
disabled={disabled}
|
|
required={field.required}
|
|
$hasError={hasError}
|
|
>
|
|
{field.options?.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</FieldSelect>
|
|
{error && <FieldError>{error}</FieldError>}
|
|
</FieldWrapper>
|
|
)
|
|
}
|
|
|
|
if (field.type === 'textarea') {
|
|
return (
|
|
<FieldWrapper $hasError={hasError}>
|
|
<FieldLabel htmlFor={id}>
|
|
{icon}
|
|
<span>{field.label}</span>
|
|
{field.required && <span className="required-marker">*</span>}
|
|
</FieldLabel>
|
|
<FieldTextarea
|
|
id={id}
|
|
value={value as string}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
onBlur={onBlur}
|
|
placeholder={field.placeholder}
|
|
disabled={disabled}
|
|
rows={field.rows || 4}
|
|
required={field.required}
|
|
$hasError={hasError}
|
|
/>
|
|
{error && <FieldError>{error}</FieldError>}
|
|
</FieldWrapper>
|
|
)
|
|
}
|
|
|
|
// Text, email, password inputs
|
|
return (
|
|
<FieldWrapper $hasError={hasError}>
|
|
<FieldLabel htmlFor={id}>
|
|
{icon}
|
|
<span>{field.label}</span>
|
|
{field.required && <span className="required-marker">*</span>}
|
|
</FieldLabel>
|
|
<FieldInput
|
|
id={id}
|
|
type={field.type}
|
|
value={value as string}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
onBlur={onBlur}
|
|
placeholder={field.placeholder}
|
|
disabled={disabled}
|
|
autoComplete={field.autoComplete}
|
|
required={field.required}
|
|
$hasError={hasError}
|
|
/>
|
|
{error && <FieldError>{error}</FieldError>}
|
|
</FieldWrapper>
|
|
)
|
|
}
|