diff --git a/packages/design-system/packages/core/src/components/index.scss b/packages/design-system/packages/core/src/components/index.scss index e7173d26..d4eca685 100644 --- a/packages/design-system/packages/core/src/components/index.scss +++ b/packages/design-system/packages/core/src/components/index.scss @@ -3,3 +3,4 @@ @import 'badge'; @import 'switch'; @import 'chip'; +@import 'text-input'; diff --git a/packages/design-system/packages/core/src/components/index.ts b/packages/design-system/packages/core/src/components/index.ts index 921f6a02..573455f9 100644 --- a/packages/design-system/packages/core/src/components/index.ts +++ b/packages/design-system/packages/core/src/components/index.ts @@ -4,3 +4,4 @@ export * from './checkbox' export * from './badge' export * from './switch' export * from './chip' +export * from './text-input' diff --git a/packages/design-system/packages/core/src/components/text-input/TextInput.stories.tsx b/packages/design-system/packages/core/src/components/text-input/TextInput.stories.tsx new file mode 100644 index 00000000..6cb9a494 --- /dev/null +++ b/packages/design-system/packages/core/src/components/text-input/TextInput.stories.tsx @@ -0,0 +1,123 @@ +/* eslint-disable tailwindcss/no-custom-classname */ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import TextInput from './TextInput' +import { $variable } from '../../token' + +const meta: Meta = { + title: 'Inputs/TextInput', + component: TextInput, + tags: ['autodocs'], + parameters: { + componentSubtitle: 'Attraction에서 사용되는 인풋 컴포넌트입니다.', + }, + argTypes: { + state: { + description: '인풋의 상태를 지정합니다.', + control: 'select', + options: ['default', 'danger', 'warn', 'success'], + table: { + type: { + summary: ['default', 'danger', 'warn', 'success'].join(' | '), + }, + defaultValue: { summary: 'default' }, + }, + }, + size: { + description: '인풋의 크기를 지정합니다.', + control: 'select', + options: ['md', 'lg'], + table: { + type: { summary: ['md', 'lg'].join(' | ') }, + defaultValue: { summary: 'md' }, + }, + }, + round: { + description: '인풋의 모서리 형태를 지정합니다.', + control: 'select', + options: ['xs', 'sm', 'md', 'lg', 'full'], + table: { + type: { summary: ['xs', 'sm', 'md', 'lg', 'full'].join(' | ') }, + defaultValue: { summary: 'sm' }, + }, + }, + disabled: { + description: '인풋의 비활성화 상태를 지정합니다.', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'undefined' }, + }, + }, + withBackground: { + description: '인풋 배경색의 기본 상태를 지정합니다.', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'undefined' }, + }, + }, + required: { + description: '필수 인풋 여부를 지정합니다. label 옵션과 함께 사용됩니다.', + control: 'boolean', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'undefined' }, + }, + }, + label: { + description: '인풋의 label을 지정합니다.', + control: 'text', + table: { + type: { summary: 'ReactNode | string' }, + defaultValue: { summary: 'undefined' }, + }, + }, + placeholder: { + description: '인풋의 placeholder를 지정합니다.', + control: 'text', + table: { + type: { summary: 'string' }, + defaultValue: { summary: 'undefined' }, + }, + }, + description: { + description: '인풋의 설명(하단 텍스트)을 지정합니다.', + control: 'text', + table: { + type: { summary: 'ReactNode | string' }, + defaultValue: { summary: 'undefined' }, + }, + }, + }, +} + +export default meta + +type Story = StoryObj + +const style: React.CSSProperties = { + display: 'block', + padding: '30px', + width: '50%', +} + +export const TextInputDefault: Story = { + render: (props) => ( +
+
+ +
+
+ +
+
+ ), +} diff --git a/packages/design-system/packages/core/src/components/text-input/TextInput.style.ts b/packages/design-system/packages/core/src/components/text-input/TextInput.style.ts index 7c058a95..55e0db59 100644 --- a/packages/design-system/packages/core/src/components/text-input/TextInput.style.ts +++ b/packages/design-system/packages/core/src/components/text-input/TextInput.style.ts @@ -9,11 +9,30 @@ export const variants = { danger: getTextInputModifier('danger'), warn: getTextInputModifier('warn'), success: getTextInputModifier('success'), - info: getTextInputModifier('info'), + }, + size: { + md: '', + lg: getTextInputModifier('size-lg'), + }, + round: { + xs: getTextInputModifier('round-xs'), + sm: '', + md: getTextInputModifier('round-md'), + lg: getTextInputModifier('round-lg'), + full: getTextInputModifier('round-full'), + }, + background: { + none: '', + with: getTextInputModifier('background'), }, } export const textInputVariants = cva(textInputClassName, { variants, - defaultVariants: { state: 'default' }, + defaultVariants: { + state: 'default', + size: 'md', + round: 'sm', + background: 'none', + }, }) diff --git a/packages/design-system/packages/core/src/components/text-input/TextInput.tsx b/packages/design-system/packages/core/src/components/text-input/TextInput.tsx index 3ad6adcb..48d879ba 100644 --- a/packages/design-system/packages/core/src/components/text-input/TextInput.tsx +++ b/packages/design-system/packages/core/src/components/text-input/TextInput.tsx @@ -1,15 +1,65 @@ import React from 'react' +import { cn } from '@attraction/utils' +import { textInputVariants, variants } from './TextInput.style' + +type TextInputVariant = typeof variants interface TextProps - extends React.DetailedHTMLProps< - React.InputHTMLAttributes, - HTMLInputElement + extends Omit< + React.DetailedHTMLProps< + React.InputHTMLAttributes, + HTMLInputElement + >, + 'size' > { + state?: keyof TextInputVariant['state'] + size?: keyof TextInputVariant['size'] + round?: keyof TextInputVariant['round'] withBackground?: boolean + label?: React.ReactNode | string + required?: boolean + description?: React.ReactNode | string } const TextInput = React.forwardRef( - ({ withBackground, ...props }, ref) => , + ( + { + className, + id, + type, + label, + required, + state, + size, + round, + withBackground, + style, + description, + ...props + }, + ref, + ) => ( +
+ {label && ( + + )} + + {description &&

{description}

} +
+ ), ) export default TextInput diff --git a/packages/design-system/packages/core/src/components/text-input/_index.scss b/packages/design-system/packages/core/src/components/text-input/_index.scss new file mode 100644 index 00000000..b57a01ff --- /dev/null +++ b/packages/design-system/packages/core/src/components/text-input/_index.scss @@ -0,0 +1,196 @@ +@use '../../token/variable'; + +.ds-text-input { + display: block; + width: 100%; + height: auto; + & + & { + margin: 1.25rem 0px 0px; + } + + & > label { + position: relative; + display: inline-flex; + justify-content: flex-start; + align-items: center; + box-sizing: border-box; + margin: 0px 0px 0.25rem; + padding: 0px 0.25rem; + font-size: variable.$ds-font-size-200; + font-weight: variable.$ds-font-weight-medium; + color: variable.$ds-gray-500; + line-height: variable.$ds-leading-normal; + white-space: nowrap; + .dark & { + color: variable.$ds-gray-400; + } + & > span { + position: absolute; + top: 0.125rem; + bottom: 0rem; + right: -0.25rem; + font-size: variable.$ds-font-size-300; + font-weight: variable.$ds-font-weight-medium; + color: variable.$ds-red-400; + line-height: variable.$ds-leading-tight; + white-space: inherit; + .dark & { + color: variable.$ds-red-300; + } + } + } + + & > input { + display: block; + width: 100%; + height: 2.5rem; + box-sizing: border-box; + padding: 0.5rem 0.75rem; + border: 1px solid variable.$ds-gray-100; + font-size: variable.$ds-font-size-300; + font-weight: variable.$ds-font-weight-regular; + color: variable.$ds-gray-700; + border-radius: 0.5rem; + outline: none; + background-color: variable.$ds-gray-000; + @include variable.transition-colors(); + + .dark & { + color: variable.$ds-gray-050; + border-color: variable.$ds-gray-700; + background-color: variable.$ds-gray-800; + } + + &::placeholder { + color: variable.$ds-gray-500; + .dark & { + color: variable.$ds-gray-400; + } + } + &:disabled { + color: variable.$ds-gray-400 !important; + background-color: variable.$ds-gray-050 !important; + cursor: not-allowed; + .dark & { + color: variable.$ds-gray-500 !important; + background-color: variable.$ds-gray-700 !important; + } + } + + &:focus, + .dark &:focus { + border-color: variable.$ds-blue-400; + } + } + + &--background > input { + background-color: variable.$ds-gray-050; + .dark & { + background-color: variable.$ds-gray-700; + } + &:focus { + background-color: variable.$ds-gray-000; + .dark & { + background-color: variable.$ds-gray-800; + } + } + } + + &--round-xs > input { + border-radius: 0.25rem; + } + &--round-md > input { + border-radius: 0.75rem; + } + &--round-lg > input { + border-radius: 1rem; + } + &--round-full > input { + padding: 0.5rem 1rem; + border-radius: 9999px; + } + + & > p { + display: block; + width: 100%; + height: auto; + box-sizing: border-box; + margin: 0.75rem 0px 0px; + padding: 0px 0.25rem; + font-size: variable.$ds-font-size-300; + font-weight: variable.$ds-font-weight-regular; + color: variable.$ds-gray-500; + line-height: variable.$ds-leading-normal; + word-break: keep-all; + .dark & { + color: variable.$ds-gray-400; + } + } + + &--danger { + & > input { + &, + &:focus { + border-color: variable.$ds-red-400; + } + .dark &, + .dark &:focus { + border-color: variable.$ds-red-300; + } + } + & > p { + color: variable.$ds-red-400; + .dark & { + color: variable.$ds-red-300; + } + } + } + &--warn { + & > input { + &:focus { + border-color: variable.$ds-yellow-400; + } + .dark &:focus { + border-color: variable.$ds-yellow-300; + } + } + & > p { + color: variable.$ds-yellow-400; + .dark & { + color: variable.$ds-yellow-300; + } + } + } + &--success { + & > input { + &:focus { + border-color: variable.$ds-green-400; + } + .dark &:focus { + border-color: variable.$ds-green-300; + } + } + & > p { + color: variable.$ds-green-400; + .dark & { + color: variable.$ds-green-300; + } + } + } + + &--size-lg { + & + & { + margin: 1.5rem 0px 0px; + } + & > label { + margin: 0px 0px 0.5rem; + } + & > input { + height: 3rem; + font-size: variable.$ds-font-size-400; + } + & > p { + margin: 1rem 0px 0px; + } + } +}