forked from cerc-io/snowballtools-base
⚡️ feat: implement Input Field component
This commit is contained in:
parent
636f68d7a4
commit
33b6191539
68
packages/frontend/src/components/shared/Input/Input.theme.ts
Normal file
68
packages/frontend/src/components/shared/Input/Input.theme.ts
Normal file
@ -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<typeof inputTheme>;
|
100
packages/frontend/src/components/shared/Input/Input.tsx
Normal file
100
packages/frontend/src/components/shared/Input/Input.tsx
Normal file
@ -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<ComponentPropsWithoutRef<'input'>, '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(
|
||||||
|
() => (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className={labelCls()}>{label}</p>
|
||||||
|
<p className={descriptionCls()}>{description}</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[labelCls, descriptionCls, label, description],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderLeftIcon = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<div className={iconContainerCls({ class: 'left-0 pl-4' })}>
|
||||||
|
{cloneIcon(leftIcon, { className: iconCls(), ariaHidden: true })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [iconCls, leftIcon]);
|
||||||
|
|
||||||
|
const renderRightIcon = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
|
||||||
|
{cloneIcon(rightIcon, { className: iconCls(), ariaHidden: true })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [rightIcon, iconCls]);
|
||||||
|
|
||||||
|
const renderHelperText = useMemo(
|
||||||
|
() => (
|
||||||
|
<div className={helperTextCls()}>
|
||||||
|
{state &&
|
||||||
|
cloneIcon(<WarningIcon className={helperIconCls()} />, {
|
||||||
|
ariaHidden: true,
|
||||||
|
})}
|
||||||
|
<p>{helperText}</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[state, helperText, helperTextCls],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{renderLabels}
|
||||||
|
<div className={containerCls({ class: className })}>
|
||||||
|
{leftIcon && renderLeftIcon}
|
||||||
|
<input
|
||||||
|
className={cn(inputCls({ class: 'w-80' }), {
|
||||||
|
'pl-10': leftIcon,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{rightIcon && renderRightIcon}
|
||||||
|
</div>
|
||||||
|
{renderHelperText}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
2
packages/frontend/src/components/shared/Input/index.ts
Normal file
2
packages/frontend/src/components/shared/Input/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './Input';
|
||||||
|
export * from './Input.theme';
|
Loading…
Reference in New Issue
Block a user