From 8940144b5a988fa5c4ae33c7e72f6bc50ce80c5f Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sat, 18 Jan 2025 18:53:20 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20input=20container=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/input/input-container.styles.css.ts | 5 +++++ src/ui/input/input-container.tsx | 15 +++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/ui/input/input-container.styles.css.ts create mode 100644 src/ui/input/input-container.tsx diff --git a/src/ui/input/input-container.styles.css.ts b/src/ui/input/input-container.styles.css.ts new file mode 100644 index 000000000..beca5e809 --- /dev/null +++ b/src/ui/input/input-container.styles.css.ts @@ -0,0 +1,5 @@ +import { style } from '@vanilla-extract/css'; + +export const inputContainerBase = style({ + position: 'relative', +}); diff --git a/src/ui/input/input-container.tsx b/src/ui/input/input-container.tsx new file mode 100644 index 000000000..160eb3857 --- /dev/null +++ b/src/ui/input/input-container.tsx @@ -0,0 +1,15 @@ +import { type ComponentProps } from 'react'; +import { cx } from '../util.ts'; +import * as styles from './input-container.styles.css.ts'; + +export type InputContainerProps = ComponentProps<'div'>; + +export const InputContainer = (props: InputContainerProps) => { + const { children, className, ...restProps } = props; + + return ( +
+ {children} +
+ ); +}; From 259b12be54036605da8746330f1f89d607e8d500 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sat, 18 Jan 2025 18:53:47 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20input=20element=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/input/input-element.styles.css.ts | 21 +++++++++++++++++++++ src/ui/input/input-element.tsx | 20 ++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/ui/input/input-element.styles.css.ts create mode 100644 src/ui/input/input-element.tsx diff --git a/src/ui/input/input-element.styles.css.ts b/src/ui/input/input-element.styles.css.ts new file mode 100644 index 000000000..72cb6ebc8 --- /dev/null +++ b/src/ui/input/input-element.styles.css.ts @@ -0,0 +1,21 @@ +import { recipe } from '@vanilla-extract/recipes'; + +export const inputElement = recipe({ + base: { + width: 24, + position: 'absolute', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + + variants: { + direction: { + right: { + top: 0, + right: 12, + }, + }, + }, +}); diff --git a/src/ui/input/input-element.tsx b/src/ui/input/input-element.tsx new file mode 100644 index 000000000..48d1d436f --- /dev/null +++ b/src/ui/input/input-element.tsx @@ -0,0 +1,20 @@ +import { ComponentProps } from 'react'; +import { cx } from '../util.ts'; +import * as styles from './input-element.styles.css.ts'; + +type InputElementProps = ComponentProps<'div'>; + +export type InputRightElementProps = InputElementProps; + +export const InputRightElement = (props: InputRightElementProps) => { + const { children, className, ...restProps } = props; + + return ( +
+ {children} +
+ ); +}; From ce146ae62893ea319310ab676c3743990828b478 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sat, 18 Jan 2025 18:54:03 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20input=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/input/input.styles.css.ts | 60 ++++++++++++++++++++++++++++++++ src/ui/input/input.tsx | 20 +++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/ui/input/input.styles.css.ts create mode 100644 src/ui/input/input.tsx diff --git a/src/ui/input/input.styles.css.ts b/src/ui/input/input.styles.css.ts new file mode 100644 index 000000000..07da2c364 --- /dev/null +++ b/src/ui/input/input.styles.css.ts @@ -0,0 +1,60 @@ +import { style } from '@vanilla-extract/css'; +import { type RecipeVariants, recipe } from '@vanilla-extract/recipes'; +import { typography } from '../typography.css.ts'; +import { globalVars } from './../theme.css.ts'; +import { inputElement } from './input-element.styles.css.ts'; + +export const inputWithElement = style({ + selectors: { + [`&:has(~ ${inputElement.classNames.variants.direction.right})`]: { + paddingRight: 44, + }, + }, +}); + +const base = style([ + typography('head_3_16_r'), + { + color: globalVars.color.black, + backgroundColor: globalVars.color.white, + outline: 'none', + borderRadius: 6, + border: `1px solid ${globalVars.color.grey300}`, + width: '100%', + padding: '13px 16px', + boxSizing: 'border-box', + '::placeholder': { + color: globalVars.color.grey400, + }, + ':focus': { + borderColor: globalVars.color.mainblue500, + }, + ':disabled': { + backgroundColor: globalVars.color.grey100, + color: globalVars.color.grey300, + }, + }, +]); + +export const inputVariants = recipe({ + base, + variants: { + variant: { + default: {}, + error: { + borderColor: globalVars.color.redDanger, + ':focus': { + borderColor: globalVars.color.redDanger, + }, + }, + }, + }, + defaultVariants: { + variant: 'default', + }, +}); + +export type InputVariants = Exclude< + RecipeVariants, + undefined +>; diff --git a/src/ui/input/input.tsx b/src/ui/input/input.tsx new file mode 100644 index 000000000..744d349a1 --- /dev/null +++ b/src/ui/input/input.tsx @@ -0,0 +1,20 @@ +import { ComponentProps } from 'react'; +import { cx } from '../util.ts'; +import * as styles from './input.styles.css.ts'; + +type InputProps = ComponentProps<'input'> & styles.InputVariants; + +export const Input = (props: InputProps) => { + const { className, variant, ...restProps } = props; + + return ( + + ); +}; From c9092b10b20c5a79618223e947c247a17be8d198 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sat, 18 Jan 2025 18:54:26 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20input=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/input/input.stories.tsx | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/ui/input/input.stories.tsx diff --git a/src/ui/input/input.stories.tsx b/src/ui/input/input.stories.tsx new file mode 100644 index 000000000..2576ffa8d --- /dev/null +++ b/src/ui/input/input.stories.tsx @@ -0,0 +1,72 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { DeleteCir } from '../../assets/index.ts'; +import { InputContainer } from './input-container.tsx'; +import { InputRightElement } from './input-element.tsx'; +import { Input } from './input.tsx'; + +const meta: Meta = { + title: 'ui/Input', + component: Input, + parameters: { + layout: 'centered', + }, + argTypes: { + value: { + control: 'text', + }, + placeholder: { + control: 'text', + }, + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const OnlyInput: Story = { + render: (args) => ( + + + + ), +}; + +export const InputWithButton: Story = { + render: (args) => ( + + + + + + + ), +}; + +export const ErrorInput: Story = { + args: { + variant: 'error', + value: '에러 텍스트입니다', + }, + render: (args) => ( + + + + ), +}; + +export const DisabledInput: Story = { + args: { + value: 'disabled', + disabled: true, + }, + render: (args) => ( + + + + ), +}; From 38d2f86261ef850355c20fa91dec8ffdb6fcacef Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 19 Jan 2025 01:13:07 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20storybook=EC=97=90=EC=84=9C=20rese?= =?UTF-8?q?t.css=20=EC=A0=81=EC=9A=A9=20=EC=95=88=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/preview.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 37914b18f..41aecef3c 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,4 +1,5 @@ -import type { Preview } from "@storybook/react"; +import type { Preview } from '@storybook/react'; +import '../src/reset.css.ts'; const preview: Preview = { parameters: { From 6bcbac7c1a21f82d9c40c4e5810227ee5a8a2735 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 19 Jan 2025 01:13:26 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20form=20error=20message=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/form/form-error-message.styles.css.ts | 17 ++++++++++++ src/ui/form/form-error-message.tsx | 27 ++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/ui/form/form-error-message.styles.css.ts create mode 100644 src/ui/form/form-error-message.tsx diff --git a/src/ui/form/form-error-message.styles.css.ts b/src/ui/form/form-error-message.styles.css.ts new file mode 100644 index 000000000..cda79a33a --- /dev/null +++ b/src/ui/form/form-error-message.styles.css.ts @@ -0,0 +1,17 @@ +import { style } from '@vanilla-extract/css'; +import { globalVars } from '../theme.css.ts'; +import { typography } from '../typography.css.ts'; + +export const formErrorMessageBase = style([ + typography('body_5_12_r'), + { + color: globalVars.color.redDanger, + }, +]); + +export const formErrorMessageContainer = style({ + display: 'flex', + gap: 4, + padding: '1.5px 0', + alignItems: 'center', +}); diff --git a/src/ui/form/form-error-message.tsx b/src/ui/form/form-error-message.tsx new file mode 100644 index 000000000..5372aa50a --- /dev/null +++ b/src/ui/form/form-error-message.tsx @@ -0,0 +1,27 @@ +import { type ComponentProps } from 'react'; +import { ErrorCir } from '../../assets/index.ts'; +import { cx } from '../util.ts'; +import * as styles from './form-error-message.styles.css.ts'; +import { useFormField } from './form-field.tsx'; + +export type FormErrorMessageProps = ComponentProps<'p'>; + +export const FormErrorMessage = (props: FormErrorMessageProps) => { + const { children, className, ...restProps } = props; + + const { error, formMessageId } = useFormField(); + const body = error ? String(error?.message) : children; + + if (!body) { + return null; + } + + return ( +
+ +

+ {body} +

+
+ ); +}; From aa54fbf898b986470ff649bcd8edc6ed2dac7a9b Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 19 Jan 2025 01:13:44 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20form=20label=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/form/form-label.styles.css.ts | 10 ++++++++++ src/ui/form/form-label.tsx | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/ui/form/form-label.styles.css.ts create mode 100644 src/ui/form/form-label.tsx diff --git a/src/ui/form/form-label.styles.css.ts b/src/ui/form/form-label.styles.css.ts new file mode 100644 index 000000000..2bcdf8388 --- /dev/null +++ b/src/ui/form/form-label.styles.css.ts @@ -0,0 +1,10 @@ +import { style } from '@vanilla-extract/css'; +import { globalVars } from '../theme.css.ts'; +import { typography } from '../typography.css.ts'; + +export const formLabelBase = style([ + typography('body_2_14_sb'), + { + color: globalVars.color.grey800, + }, +]); diff --git a/src/ui/form/form-label.tsx b/src/ui/form/form-label.tsx new file mode 100644 index 000000000..f0121fffe --- /dev/null +++ b/src/ui/form/form-label.tsx @@ -0,0 +1,22 @@ +import { type ComponentProps } from 'react'; +import { cx } from '../util.ts'; +import { useFormField } from './form-field.tsx'; +import * as styles from './form-label.styles.css.ts'; + +export type FormLabelProps = ComponentProps<'label'>; + +// TODO required 속성 지원 필요 +export const FormLabel = (props: FormLabelProps) => { + const { children, className, ...restProps } = props; + + const { formItemId } = useFormField(); + return ( + + ); +}; From f64c291293a008d2991846646ea407628b769fe1 Mon Sep 17 00:00:00 2001 From: DongjaJ Date: Sun, 19 Jan 2025 01:14:11 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20form=20field=20=EB=B0=8F=20form?= =?UTF-8?q?=20label=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/form/form-field.tsx | 66 ++++++++++++ src/ui/form/form-item.stories.tsx | 151 ++++++++++++++++++++++++++++ src/ui/form/form-item.styles.css.ts | 7 ++ src/ui/form/form-item.tsx | 20 ++++ 4 files changed, 244 insertions(+) create mode 100644 src/ui/form/form-field.tsx create mode 100644 src/ui/form/form-item.stories.tsx create mode 100644 src/ui/form/form-item.styles.css.ts create mode 100644 src/ui/form/form-item.tsx diff --git a/src/ui/form/form-field.tsx b/src/ui/form/form-field.tsx new file mode 100644 index 000000000..0b85ade7c --- /dev/null +++ b/src/ui/form/form-field.tsx @@ -0,0 +1,66 @@ +import { createContext, useContext } from 'react'; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from 'react-hook-form'; + +export const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = createContext( + {} as FormFieldContextValue, +); + +export const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +type FormItemContextValue = { + id: string; +}; + +export const FormItemContext = createContext( + {} as FormItemContextValue, +); + +export const useFormField = () => { + const fieldContext = useContext(FormFieldContext); + const itemContext = useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; diff --git a/src/ui/form/form-item.stories.tsx b/src/ui/form/form-item.stories.tsx new file mode 100644 index 000000000..26f839ace --- /dev/null +++ b/src/ui/form/form-item.stories.tsx @@ -0,0 +1,151 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import type { Meta, StoryObj } from '@storybook/react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { DeleteCir } from '../../assets/index.ts'; +import { InputContainer } from '../input/input-container.tsx'; +import { InputRightElement } from '../input/input-element.tsx'; +import { Input } from '../input/input.tsx'; +import { FormErrorMessage } from './form-error-message.tsx'; +import { Form, FormField } from './form-field.tsx'; +import { FormItem } from './form-item.tsx'; +import { FormLabel } from './form-label.tsx'; + +const formSchema = z.object({ + username: z.string().min(2, { + message: 'Username must be at least 2 characters.', + }), +}); + +const meta: Meta = { + title: 'ui/FormField', + component: FormField, + parameters: { + layout: 'centered', + }, + argTypes: {}, + tags: ['autodocs'], + decorators: [ + (Story, context) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + username: '', + }, + }); + + function onSubmit(values: z.infer) { + console.log(values); + } + + return ( +
+ + + ( + + 폼 라밸 + + { + + } + {field.value && ( + + + + )} + + + + )} + /> +