diff --git a/packages/frontend/src/components/shared/Input/Input.theme.ts b/packages/frontend/src/components/shared/Input/Input.theme.ts new file mode 100644 index 00000000..e0ea2c5f --- /dev/null +++ b/packages/frontend/src/components/shared/Input/Input.theme.ts @@ -0,0 +1,68 @@ +import { VariantProps, tv } from 'tailwind-variants'; + +export const inputTheme = tv( + { + slots: { + container: [ + 'flex items-center rounded-lg relative', + 'placeholder:text-elements-disabled', + 'disabled:cursor-not-allowed disabled:bg-controls-disabled', + ], + label: ['text-sm', 'text-elements-high-em'], + description: ['text-xs', 'text-elements-low-em'], + input: [ + 'focus-ring', + 'block w-full h-full rounded-lg text-elements-mid-em', + 'shadow-sm', + 'border border-border-interactive', + 'disabled:shadow-none', + 'disabled:border-none', + ], + icon: ['text-elements-mid-em'], + iconContainer: [ + 'absolute inset-y-0 flex items-center z-10 cursor-pointer', + ], + helperIcon: [], + helperText: ['flex gap-2 items-center text-elements-danger'], + }, + variants: { + state: { + default: { + input: '', + }, + error: { + input: [ + 'outline outline-offset-0 outline-border-danger', + 'shadow-none', + ], + helperText: ['text-elements-danger'], + }, + }, + size: { + md: { + container: 'h-11', + input: 'text-sm pl-4 pr-4', + icon: 'h-[18px] w-[18px]', + helperText: 'text-sm', + helperIcon: 'h-5 w-5', + }, + sm: { + container: 'h-8', + input: 'text-xs pl-3 pr-3', + icon: 'h-4 w-4', + helperText: 'text-xs', + helperIcon: 'h-4 w-4', + }, + }, + }, + defaultVariants: { + size: 'md', + state: 'default', + }, + }, + { + responsiveVariants: true, + }, +); + +export type InputTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Input/Input.tsx b/packages/frontend/src/components/shared/Input/Input.tsx new file mode 100644 index 00000000..52b398e1 --- /dev/null +++ b/packages/frontend/src/components/shared/Input/Input.tsx @@ -0,0 +1,100 @@ +import React, { ReactNode, useMemo } from 'react'; +import { ComponentPropsWithoutRef } from 'react'; +import { InputTheme, inputTheme } from './Input.theme'; +import { cloneIcon } from 'utils/cloneIcon'; +import { cn } from 'utils/classnames'; +import { WarningIcon } from '../CustomIcon'; + +export interface InputProps + extends InputTheme, + Omit, 'size'> { + label?: string; + description?: string; + leftIcon?: ReactNode; + rightIcon?: ReactNode; + helperText?: string; +} + +export const Input = ({ + className, + label, + description, + leftIcon, + rightIcon, + helperText, + size, + state, + ...props +}: InputProps) => { + const styleProps = (({ size = 'md', state }) => ({ + size, + state, + }))({ size, state }); + + const { + container: containerCls, + label: labelCls, + description: descriptionCls, + input: inputCls, + icon: iconCls, + iconContainer: iconContainerCls, + helperText: helperTextCls, + helperIcon: helperIconCls, + } = inputTheme({ ...styleProps }); + + const renderLabels = useMemo( + () => ( +
+

{label}

+

{description}

+
+ ), + [labelCls, descriptionCls, label, description], + ); + + const renderLeftIcon = useMemo(() => { + return ( +
+ {cloneIcon(leftIcon, { className: iconCls(), ariaHidden: true })} +
+ ); + }, [iconCls, leftIcon]); + + const renderRightIcon = useMemo(() => { + return ( +
+ {cloneIcon(rightIcon, { className: iconCls(), ariaHidden: true })} +
+ ); + }, [rightIcon, iconCls]); + + const renderHelperText = useMemo( + () => ( +
+ {state && + cloneIcon(, { + ariaHidden: true, + })} +

{helperText}

+
+ ), + [state, helperText, helperTextCls], + ); + + return ( +
+ {renderLabels} +
+ {leftIcon && renderLeftIcon} + + {rightIcon && renderRightIcon} +
+ {renderHelperText} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/Input/index.ts b/packages/frontend/src/components/shared/Input/index.ts new file mode 100644 index 00000000..9ffcc229 --- /dev/null +++ b/packages/frontend/src/components/shared/Input/index.ts @@ -0,0 +1,2 @@ +export * from './Input'; +export * from './Input.theme';