import * as React from 'react'; import { cva, type VariantProps } from '@/lib/cva'; import { cn } from '@/lib/utils'; import { disabledField, fieldHeights, focusRing, invalidState } from '@/lib/cva-presets'; /** * The box itself — shared by the bare input, the adornment wrapper and Select's * trigger, so the three can never drift apart. */ const fieldShape = { /** Boxed on all four sides. */ outline: 'rounded-xs border border-input shadow-xs', /** Underlined only — for dense forms and inline editing. */ flushed: 'rounded-none border-0 border-b border-input', } as const; /** * Shared between Input, Textarea and Select — CBAR draws all three on the same * ladder (32 / 36 / 44 / 56px, 4px corners, hairline neutral border) and gives * each the same two variants. */ export const inputVariants = cva( cn( 'w-full min-w-0 bg-transparent text-sm dark:bg-input/30', 'transition-[color,box-shadow,border-color] duration-(--ui-duration-fast) ease-(--ui-ease-standard)', 'placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground', focusRing, disabledField, invalidState ), { variants: { variant: fieldShape, size: { xs: `${fieldHeights.xs} px-2`, sm: `${fieldHeights.sm} px-2.5`, md: `${fieldHeights.md} px-3`, lg: `${fieldHeights.lg} px-4`, }, }, defaultVariants: { variant: 'outline', size: 'md' }, } ); /** * The same box, but as a container: the focus ring and the invalid treatment * move to `focus-within:` / `has-aria-invalid:` because the element that takes * focus is now a child. * * Same idea as InputNumber's shell, which is where this pattern already lives. */ const inputWrapperVariants = cva( cn( 'flex w-full min-w-0 items-center bg-transparent text-sm dark:bg-input/30', 'transition-[color,box-shadow,border-color] duration-(--ui-duration-fast) ease-(--ui-ease-standard)', 'focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50', 'has-aria-invalid:border-destructive has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40', 'has-disabled:cursor-not-allowed has-disabled:opacity-50' ), { variants: { variant: fieldShape, size: { xs: `${fieldHeights.xs} gap-1.5 px-2`, sm: `${fieldHeights.sm} gap-1.5 px-2.5`, md: `${fieldHeights.md} gap-2 px-3`, lg: `${fieldHeights.lg} gap-2 px-4`, }, }, defaultVariants: { variant: 'outline', size: 'md' }, } ); /** Icon sizing and muting for whatever sits in an adornment slot. */ const adornment = cn( 'flex shrink-0 items-center text-muted-foreground', "[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" ); export interface InputProps extends Omit, 'size'>, VariantProps { /** * Content pinned to the leading edge — CBAR's `.leftElement?`. Inert by * design: a click passes through to the field, which is what you want for the * icon or currency symbol this slot is for. Put anything interactive in * {@link InputProps.endElement} instead. */ startElement?: React.ReactNode; /** * Content pinned to the trailing edge — CBAR's `.rightElement?`. Stays * interactive, so a clear button or a reveal-password toggle works here. */ endElement?: React.ReactNode; /** * Classes for the inner `` when an adornment is present. * * With a slot filled, `className` lands on the wrapper — that is the visible * box, and `className="w-64"` should size it. Same split as InputNumber. */ classNames?: { input?: string }; } /** * Single-line text field. * * Set `aria-invalid` to switch it to the error treatment — the styling is * driven by that attribute rather than a prop, so it stays in sync with what * assistive technology reports. * * With neither adornment set it renders a bare `` and nothing else, so * existing markup and `[data-slot="input"]` selectors are unaffected. * * ```tsx * * } placeholder="Search" /> * ₼} defaultValue="1200" /> * ``` */ function Input({ className, classNames, type, variant, size, startElement, endElement, ...props }: InputProps) { const fileStyling = 'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground'; if (startElement === undefined && endElement === undefined) { return ( ); } return (
{startElement !== undefined ? ( {startElement} ) : null} {endElement !== undefined ? ( {endElement} ) : null}
); } export { Input, inputWrapperVariants };