import * as React from "react"; import { cn } from "@core/utils/cn.ts"; import { cva, type VariantProps } from "class-variance-authority"; import { Check, Copy, Eye, EyeOff, type LucideIcon, X } from "lucide-react"; import { useCopyToClipboard } from "@core/hooks/useCopyToClipboard.ts"; import { usePasswordVisibilityToggle } from "@core/hooks/usePasswordVisibilityToggle.ts"; const inputVariants = cva( "flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:bg-transparet dark:text-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-600", { variants: { variant: { default: "border-slate-300 dark:border-slate-500", invalid: "border-red-500 dark:border-red-500 focus:ring-red-500 dark:focus:ring-red-500", }, }, defaultVariants: { variant: "default", }, }, ); type InputActionType = { id: string; icon: LucideIcon; onClick: (e: React.MouseEvent) => void; ariaLabel: string; tooltip?: string; condition?: boolean; }; export interface InputProps extends Omit, "prefix" | "suffix">, VariantProps { prefix?: React.ReactNode; suffix?: React.ReactNode; showPasswordToggle?: boolean; showCopyButton?: boolean; showClearButton?: boolean; containerClassName?: string; } const Input = React.forwardRef( ( { className, containerClassName, variant, type = "text", prefix, suffix, showPasswordToggle, showCopyButton, showClearButton, value, onChange, ...props }, ref, ) => { const { isVisible, toggleVisibility } = usePasswordVisibilityToggle(); const { copy, isCopied } = useCopyToClipboard({ timeout: 1500 }); const potentialActions: InputActionType[] = [ { id: "clear-input", icon: X, onClick: (e) => { e.stopPropagation(); if (onChange) { const event = { target: { value: "" }, currentTarget: { value: "" }, } as React.ChangeEvent; onChange(event); } if (ref && typeof ref !== "function" && ref.current) { ref.current.focus(); } }, ariaLabel: "Clear input", tooltip: "Clear input", condition: !!showClearButton && !!value, }, { id: "toggle-visibility", icon: isVisible ? EyeOff : Eye, onClick: (e) => { e.stopPropagation(); toggleVisibility(); }, ariaLabel: isVisible ? "Hide password" : "Show password", tooltip: isVisible ? "Hide password" : "Show password", condition: !!showPasswordToggle && type === "password", }, { id: "copy-value", icon: isCopied ? Check : Copy, onClick: (e) => { e.stopPropagation(); if (value !== undefined && value !== null) { copy(String(value)); } }, ariaLabel: isCopied ? "Copied!" : "Copy to clipboard", tooltip: isCopied ? "Copied!" : "Copy to clipboard", condition: !!showCopyButton, }, ]; const actions = potentialActions.filter((action) => action.condition); const inputType = showPasswordToggle ? (isVisible ? "text" : "password") : type; const hasPrefix = !!prefix; const hasSuffix = !!suffix; const hasActions = actions.length > 0; const inputClassName = cn( inputVariants({ variant }), hasActions && !hasSuffix && "pr-10", hasPrefix && "rounded-l-none", className, ); return (
{prefix && ( {prefix} )}
{suffix && ( {suffix} )} {hasActions && (
{actions.map((action) => ( ))}
)}
); }, ); Input.displayName = "Input"; export { Input, inputVariants };