forked from cerc-io/snowballtools-base
Merge pull request #85 from snowball-tools/andrehadianto/T-4863-input-field
feat: input field
This commit is contained in:
commit
2369f4498a
@ -16,6 +16,7 @@
|
|||||||
"@types/react": "^18.2.42",
|
"@types/react": "^18.2.42",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.17",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"downshift": "^8.2.3",
|
"downshift": "^8.2.3",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const CrossIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 5L19 19M19 5L5 19"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const SearchIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 20L16.05 16.05M18 11C18 14.866 14.866 18 11 18C7.13401 18 4 14.866 4 11C4 7.13401 7.13401 4 11 4C14.866 4 18 7.13401 18 11Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||||
|
|
||||||
|
export const WarningIcon = (props: CustomIconProps) => {
|
||||||
|
return (
|
||||||
|
<CustomIcon
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M10.4902 2.84406C11.1661 1.69 12.8343 1.69 13.5103 2.84406L22.0156 17.3654C22.699 18.5321 21.8576 19.9999 20.5056 19.9999H3.49483C2.14281 19.9999 1.30147 18.5321 1.98479 17.3654L10.4902 2.84406ZM12 9C12.4142 9 12.75 9.33579 12.75 9.75V13.25C12.75 13.6642 12.4142 14 12 14C11.5858 14 11.25 13.6642 11.25 13.25V9.75C11.25 9.33579 11.5858 9 12 9ZM13 15.75C13 16.3023 12.5523 16.75 12 16.75C11.4477 16.75 11 16.3023 11 15.75C11 15.1977 11.4477 14.75 12 14.75C12.5523 14.75 13 15.1977 13 15.75Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</CustomIcon>
|
||||||
|
);
|
||||||
|
};
|
@ -4,4 +4,7 @@ export * from './CheckIcon';
|
|||||||
export * from './ChevronGrabberHorizontal';
|
export * from './ChevronGrabberHorizontal';
|
||||||
export * from './ChevronLeft';
|
export * from './ChevronLeft';
|
||||||
export * from './ChevronRight';
|
export * from './ChevronRight';
|
||||||
|
export * from './WarningIcon';
|
||||||
|
export * from './SearchIcon';
|
||||||
|
export * from './CrossIcon';
|
||||||
export * from './GlobeIcon';
|
export * from './GlobeIcon';
|
||||||
|
84
packages/frontend/src/components/shared/Input/Input.theme.ts
Normal file
84
packages/frontend/src/components/shared/Input/Input.theme.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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 { WarningIcon } from 'components/shared/CustomIcon';
|
||||||
|
import { cloneIcon } from 'utils/cloneIcon';
|
||||||
|
import { cn } from 'utils/classnames';
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}, [cloneIcon, iconCls, iconContainerCls, leftIcon]);
|
||||||
|
|
||||||
|
const renderRightIcon = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<div className={iconContainerCls({ class: 'pr-4 right-0' })}>
|
||||||
|
{cloneIcon(rightIcon, { className: iconCls(), ariaHidden: true })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [cloneIcon, iconCls, iconContainerCls, rightIcon]);
|
||||||
|
|
||||||
|
const renderHelperText = useMemo(
|
||||||
|
() => (
|
||||||
|
<div className={helperTextCls()}>
|
||||||
|
{state &&
|
||||||
|
cloneIcon(<WarningIcon className={helperIconCls()} />, {
|
||||||
|
ariaHidden: true,
|
||||||
|
})}
|
||||||
|
<p>{helperText}</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[cloneIcon, state, helperIconCls, 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';
|
@ -17,6 +17,7 @@ import {
|
|||||||
renderTabs,
|
renderTabs,
|
||||||
renderVerticalTabs,
|
renderVerticalTabs,
|
||||||
} from './renders/tabs';
|
} from './renders/tabs';
|
||||||
|
import { renderInputs } from './renders/input';
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const [singleDate, setSingleDate] = useState<Value>();
|
const [singleDate, setSingleDate] = useState<Value>();
|
||||||
@ -37,6 +38,11 @@ const Page = () => {
|
|||||||
|
|
||||||
{/* Button */}
|
{/* Button */}
|
||||||
<div className="flex flex-col gap-10 items-center justify-between">
|
<div className="flex flex-col gap-10 items-center justify-between">
|
||||||
|
<h1 className="text-2xl font-bold">Input</h1>
|
||||||
|
<div className="flex w-full flex-col gap-10">{renderInputs()}</div>
|
||||||
|
|
||||||
|
<div className="w-full h border border-gray-200 px-20 my-10" />
|
||||||
|
|
||||||
<h1 className="text-2xl font-bold">Button</h1>
|
<h1 className="text-2xl font-bold">Button</h1>
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
{renderButtons()}
|
{renderButtons()}
|
||||||
|
57
packages/frontend/src/pages/components/renders/input.tsx
Normal file
57
packages/frontend/src/pages/components/renders/input.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Input } from 'components/shared/Input';
|
||||||
|
import { SearchIcon, CrossIcon } from 'components/shared/CustomIcon';
|
||||||
|
|
||||||
|
export const renderInputs = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex w-full gap-10">
|
||||||
|
<Input
|
||||||
|
label="Label"
|
||||||
|
description="Additional information or context"
|
||||||
|
leftIcon={<SearchIcon />}
|
||||||
|
rightIcon={<CrossIcon />}
|
||||||
|
placeholder="Placeholder text"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
disabled
|
||||||
|
label="Label"
|
||||||
|
description="Additional information or context"
|
||||||
|
placeholder="Placeholder text"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
state="error"
|
||||||
|
label="Label"
|
||||||
|
description="Additional information or context"
|
||||||
|
placeholder="Placeholder text"
|
||||||
|
helperText="The error goes here"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full gap-10">
|
||||||
|
<Input
|
||||||
|
label="Label"
|
||||||
|
leftIcon={<SearchIcon />}
|
||||||
|
rightIcon={<CrossIcon />}
|
||||||
|
description="Additional information or context"
|
||||||
|
placeholder="Placeholder text"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
disabled
|
||||||
|
label="Label"
|
||||||
|
description="Additional information or context"
|
||||||
|
placeholder="Placeholder text"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
state="error"
|
||||||
|
label="Label"
|
||||||
|
description="Additional information or context"
|
||||||
|
placeholder="Placeholder text"
|
||||||
|
size="sm"
|
||||||
|
helperText="The error goes here"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
13
packages/frontend/src/utils/classnames.ts
Normal file
13
packages/frontend/src/utils/classnames.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { clsx } from 'clsx';
|
||||||
|
import type { ClassValue } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a merged class name string by merging and processing multiple class names and Tailwind CSS styles.
|
||||||
|
*
|
||||||
|
* @param {...string[]} args - One or more class names and/or Tailwind CSS styles to be merged.
|
||||||
|
* @returns {string} - The merged class name string.
|
||||||
|
*/
|
||||||
|
export function cn(...args: ClassValue[]): string {
|
||||||
|
return twMerge(clsx(args));
|
||||||
|
}
|
@ -6327,7 +6327,7 @@ clone@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||||
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
||||||
|
|
||||||
clsx@^2.0.0:
|
clsx@^2.0.0, clsx@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
|
||||||
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
|
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
|
||||||
|
Loading…
Reference in New Issue
Block a user