/** * Label — text that names a form field. * * A string child is wrapped in `Label.Text` automatically, so the common case * stays one line; pass the parts explicitly when the label needs more than * text. `isRequired`, `isInvalid` and `isDisabled` are visual *and* semantic — * they recolour and they set the accessibility state, so the two cannot drift * apart. */ import { createContext, forwardRef, useContext, useMemo, type ReactNode, } from 'react'; import { View, type Text as RNText, type ViewProps } from 'react-native'; import { tv } from 'tailwind-variants'; import { Text, type TextProps, textChildren } from '../../primitives/text'; const labelVariants = tv({ slots: { root: 'flex-row items-center gap-1', text: 'text-sm font-medium text-foreground', asterisk: 'text-sm font-medium text-destructive', }, variants: { isDisabled: { true: { root: 'opacity-[0.64]', asterisk: 'text-muted-foreground', }, }, isInvalid: { true: { text: 'text-destructive' }, }, }, }); interface LabelState { isRequired: boolean; isInvalid: boolean; isDisabled: boolean; } const LabelContext = createContext({ isRequired: false, isInvalid: false, isDisabled: false, }); export interface LabelProps extends ViewProps { className?: string; /** Appends an asterisk marking the field as required. */ isRequired?: boolean; /** Recolours the label to signal a validation error. */ isInvalid?: boolean; isDisabled?: boolean; children?: ReactNode; } const LabelRoot = forwardRef( ( { className, isRequired = false, isInvalid = false, isDisabled = false, children, ...props }, ref ) => { const { root } = labelVariants({ isDisabled, isInvalid }); const state = useMemo( () => ({ isRequired, isInvalid, isDisabled }), [isRequired, isInvalid, isDisabled] ); return ( {/* Bare strings are the common case — wrap them so callers don't have to. */} {textChildren(children, (text) => {text})} ); } ); LabelRoot.displayName = 'Label'; export interface LabelTextProps extends TextProps { className?: string; /** Classes for the required asterisk. */ asteriskClassName?: string; } const LabelText = forwardRef( ({ className, asteriskClassName, children, ...props }, ref) => { const { isRequired, isInvalid, isDisabled } = useContext(LabelContext); const { text, asterisk } = labelVariants({ isDisabled, isInvalid }); return ( <> {children} {isRequired ? ( * ) : null} ); } ); LabelText.displayName = 'Label.Text'; export const Label = Object.assign(LabelRoot, { Text: LabelText, });