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