'use client' import { Slot } from '@radix-ui/react-slot' import React from 'react' import { cn } from '../../utils/cn' import { ErrorMessage } from '../ErrorMessage/ErrorMessage' import { Helper } from '../Helper/Helper' import { Label } from '../Label/Label' export const FIELD_SIZE_OPTIONS = ['default', 'sm', 'xs'] as const export type FieldSize = (typeof FIELD_SIZE_OPTIONS)[number] type FieldContextValue = { id: string size: FieldSize fieldItemId: string fieldDescriptionId: string fieldMessageId: string } const FieldContext = React.createContext( undefined, ) const useField = () => { const context = React.useContext(FieldContext) return context } export type FieldProps = React.HTMLAttributes & { /** * Controls the density of all child field elements (label, helper, error, and control). * Propagated automatically via context — no need to set it on each sub-component. */ size?: FieldSize children: React.ReactNode ref?: React.Ref } /** * Accessibility wrapper for a single form field. Generates a shared ID via * `React.useId()` and wires it to the label (`htmlFor`), the control (`id`, * `aria-describedby`), and the error message (`aria-invalid`). Compose with * FieldLabel, FieldHelper, FieldControl, and FieldError. */ const Field = ({ children, className, size = 'default', ref, ...props }: FieldProps) => { const id = React.useId() const value = { id, size, fieldItemId: `${id}-field-item`, fieldDescriptionId: `${id}-field-item-description`, fieldMessageId: `${id}-field-item-message`, } return (
{children}
) } Field.displayName = 'Field' type FieldErrorProps = React.ComponentProps & { ref?: React.Ref> } /** * Error message for a field. Automatically linked to the control via * `aria-describedby`. Only renders visible text when children are provided. */ const FieldError = ({ ref, ...props }: FieldErrorProps) => { const fieldContext = useField() const fieldMessageId = fieldContext?.fieldMessageId return ( ) } FieldError.displayName = 'FieldError' type FieldLabelProps = React.ComponentProps & { ref?: React.Ref> } /** * Label for a field. Automatically linked to the control via `htmlFor`. * Use `asChild` when the label wraps a non-input element (e.g. a Checkbox) * to suppress the `htmlFor` link and avoid double-activation. */ const FieldLabel = ({ asChild, size: propSize, ref, ...props }: FieldLabelProps) => { const fieldContext = useField() const size = propSize || fieldContext?.size || 'default' const fieldItemId = fieldContext?.fieldItemId // If asChild is true, we assume it's being used as a non-semantic title (e.g., for a Checkbox). // We render the Label with `asChild` but do NOT pass the `htmlFor` prop. if (asChild) { return (