diff --git a/apps/engineering/content/design/components/form-input.mdx b/apps/engineering/content/design/components/form-input.mdx new file mode 100644 index 0000000000..1ca274815b --- /dev/null +++ b/apps/engineering/content/design/components/form-input.mdx @@ -0,0 +1,116 @@ +--- +title: FormInput +description: A form input component with built-in label, description, and error handling capabilities. +--- +import { + DefaultFormInputVariant, + RequiredFormInputVariant, + SuccessFormInputVariant, + WarningFormInputVariant, + ErrorFormInputVariant, + DisabledFormInputVariant, + DefaultValueFormInputVariant, + ReadonlyFormInputVariant, + ComplexFormInputVariant +} from "./form/form-input.variants" + +# FormInput +A comprehensive form input component that combines labels, descriptions, and validation states. Perfect for creating accessible, user-friendly forms with proper labeling and helpful context. + +## Default +The default FormInput includes a label and optional description text, providing clear context for users. + + + +## Input States + +### Required Field +Use the required prop to indicate mandatory fields. This automatically adds an asterisk (*) to the label. + + + +### Success State +Indicates successful validation or acceptance of input value. The success icon and text provide positive feedback. + + + +### Warning State +Used for potentially problematic inputs that don't prevent form submission. Includes a warning icon and explanatory text. + + + +### Error State +Shows validation errors or other issues that need user attention. Features prominent error styling and message. + + + +### Disabled State +Apply when the field should be non-interactive, such as during form submission or based on other field values. + + + +### With Default Value +Pre-populated input with an initial value that users can modify. + + + +### Read-only State +For displaying non-editable information while maintaining form layout consistency. + + + +## Complex Usage +Example of a FormInput with multiple props configured for a specific use case. + + + +## Props +The FormInput component extends the standard Input component props with additional form-specific properties: + + + +## Accessibility +FormInput is built with accessibility in mind: +- Labels are properly associated with inputs using htmlFor/id +- Error messages are announced to screen readers using role="alert" +- Required fields are marked both visually and via aria-required +- Helper text is linked to inputs using aria-describedby +- Error states are indicated using aria-invalid + +## Best Practices +When using the FormInput component: +- Always provide clear, concise labels +- Use description text to provide additional context when needed +- Keep error messages specific and actionable +- Use required fields sparingly and logically +- Group related FormInputs using fieldset and legend when appropriate +- Consider the mobile experience when writing labels and descriptions +- Maintain consistent validation patterns across your form +- Use appropriate input types (email, tel, etc.) for better mobile keyboards +- Consider character/word limits in descriptions and error messages +- Test with screen readers to ensure accessibility + +## Layout Guidelines +- Labels should be clear and concise +- Error messages should appear immediately below the input +- Description text should be helpful but not too lengthy diff --git a/apps/engineering/content/design/components/form/form-input.variants.tsx b/apps/engineering/content/design/components/form/form-input.variants.tsx new file mode 100644 index 0000000000..2c5aec329e --- /dev/null +++ b/apps/engineering/content/design/components/form/form-input.variants.tsx @@ -0,0 +1,131 @@ +import { RenderComponentWithSnippet } from "@/app/components/render"; +import { FormInput } from "@unkey/ui"; + +export const DefaultFormInputVariant = () => { + return ( + + + + ); +}; + +// Required field variant +export const RequiredFormInputVariant = () => { + return ( + + + + ); +}; + +// Success variant +export const SuccessFormInputVariant = () => { + return ( + + + + ); +}; + +// Warning variant +export const WarningFormInputVariant = () => { + return ( + + + + ); +}; + +// Error variant +export const ErrorFormInputVariant = () => { + return ( + + + + ); +}; + +// Disabled variant +export const DisabledFormInputVariant = () => { + return ( + + + + ); +}; + +// With default value +export const DefaultValueFormInputVariant = () => { + return ( + + + + ); +}; + +// Readonly variant +export const ReadonlyFormInputVariant = () => { + return ( + + + + ); +}; + +// Complex example with multiple props +export const ComplexFormInputVariant = () => { + return ( + + + + ); +}; diff --git a/apps/engineering/content/design/components/input.mdx b/apps/engineering/content/design/components/input.mdx index 9edfe3b53b..a21e685d81 100644 --- a/apps/engineering/content/design/components/input.mdx +++ b/apps/engineering/content/design/components/input.mdx @@ -2,8 +2,6 @@ title: Input description: A text input field component with different states, validations, and icon support. --- - -import { Input } from "@unkey/ui" import { RenderComponentWithSnippet } from "@/app/components/render" import { InputDefaultVariant, diff --git a/internal/ui/src/components/form/form-input.tsx b/internal/ui/src/components/form/form-input.tsx new file mode 100644 index 0000000000..8733de2270 --- /dev/null +++ b/internal/ui/src/components/form/form-input.tsx @@ -0,0 +1,82 @@ +import { CircleInfo, TriangleWarning2 } from "@unkey/icons"; +import * as React from "react"; +import { cn } from "../../lib/utils"; +import { Input, type InputProps } from "../input"; + +export interface FormInputProps extends InputProps { + label?: string; + description?: string; + required?: boolean; + error?: string; +} + +export const FormInput = React.forwardRef( + ({ label, description, error, required, id, className, variant, ...props }, ref) => { + const inputVariant = error ? "error" : variant; + + const inputId = id || React.useId(); + const descriptionId = `${inputId}-helper`; + const errorId = `${inputId}-error`; + + return ( +
+ {label && ( + + )} + + + + {(description || error) && ( +
+ {error ? ( + + ) : description ? ( + + {variant === "warning" ? ( + + ) : null} +
+ )} +
+ ); + }, +); + +FormInput.displayName = "FormInput"; diff --git a/internal/ui/src/components/form/index.tsx b/internal/ui/src/components/form/index.tsx new file mode 100644 index 0000000000..d177cdb90a --- /dev/null +++ b/internal/ui/src/components/form/index.tsx @@ -0,0 +1 @@ +export * from "./form-input"; diff --git a/internal/ui/src/components/input.tsx b/internal/ui/src/components/input.tsx index 09a7d6d379..b692f54e02 100644 --- a/internal/ui/src/components/input.tsx +++ b/internal/ui/src/components/input.tsx @@ -13,19 +13,19 @@ const inputVariants = cva( "[&:not(:placeholder-shown)]:focus:ring-0", ], success: [ - "border border-success-6 hover:border-success-7 bg-gray-2", + "border border-success-9 hover:border-success-10 bg-gray-2", "focus:border-success-8 focus:ring-2 focus:ring-success-2 focus-visible:outline-none", - "[&:not(:placeholder-shown)]:focus:ring-success-3", + "[&:not(:placeholder-shown)]:focus:ring-success-0", ], warning: [ - "border border-warning-6 hover:border-warning-7 bg-gray-2", + "border border-warning-9 hover:border-warning-10 bg-gray-2", "focus:border-warning-8 focus:ring-2 focus:ring-warning-2 focus-visible:outline-none", - "[&:not(:placeholder-shown)]:focus:ring-warning-3", + "[&:not(:placeholder-shown)]:focus:ring-warning-0", ], error: [ - "border border-error-6 hover:border-error-7 bg-gray-2", + "border border-error-9 hover:border-error-10 bg-gray-2", "focus:border-error-8 focus:ring-2 focus:ring-error-2 focus-visible:outline-none", - "[&:not(:placeholder-shown)]:focus:ring-error-3", + "[&:not(:placeholder-shown)]:focus:ring-error-0", ], }, }, diff --git a/internal/ui/src/index.ts b/internal/ui/src/index.ts index 08defde55a..d1ad3cac3b 100644 --- a/internal/ui/src/index.ts +++ b/internal/ui/src/index.ts @@ -4,3 +4,4 @@ export * from "./components/tooltip"; export * from "./components/date-time/date-time"; export * from "./components/input"; export * from "./components/empty"; +export * from "./components/form";