diff --git a/.github/release.yml b/.github/release.yml index d7fd83a..a8f6cf1 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,37 +1,36 @@ # 이 파일은 release note 작성에 사용되는 파일입니다. -name-template: '@yourssu/design-system-react@$RESOLVED_VERSION' -tag-template: 'v$RESOLVED_VERSION' -categories: - - title: '🆕 새로운 기능이 추가되었어요!' - label: 'feat' - - title: '🐞 자잘한 버그를 수정했습니다.' - labels: - - 'bug' - - 'fix' - - title: '🫶🏻 사용성 개선에 힘썼습니다.' - label: 'docs' - - title: '🛠️ 더 나은 코드를 위해 노력하고 있습니다.' - labels: - - 'refactor' - - 'chore' - - title: 'ETC' - labels: - - '*' -change-template: '* $TITLE (#$NUMBER) by @$AUTHOR' -change-title-escapes: '\<*_&#@`' -exclude-labels: - - 'Main' -version-resolver: - major: - labels: - - 'Major' - minor: - labels: - - 'Minor' - patch: - labels: - - 'Patch' - default: patch -template: | - $CHANGES +changelog: + name-template: '@Yourssu Design/design-system-react@$RESOLVED_VERSION' + tag-template: 'v$RESOLVED_VERSION' + categories: + - title: ':new: Exciting New Features!' + label: 'feat' + - title: ':ladybug: Fixed a Bug' + labels: + - 'bug' + - 'fix' + - title: ':heart_hands::skin-tone-2: Improve User Experience' + label: 'docs' + - title: ':hammer_and_wrench: Strive for Better Code' + label: 'refactor' + - title: 'ETC' + labels: + - '*' + change-template: '* $TITLE (#$NUMBER) by @$AUTHOR' + change-title-escapes: '\<*_&#@`' + exclude-labels: + - 'Main' + version-resolver: + major: + labels: + - 'Major' + minor: + labels: + - 'Minor' + patch: + labels: + - 'Patch' + default: patch + template: | + $CHANGES diff --git a/.storybook/main.ts b/.storybook/main.ts index 998bbf4..d51b64d 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -2,12 +2,26 @@ import type { StorybookConfig } from '@storybook/react-vite'; const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + typescript: { + reactDocgen: 'react-docgen-typescript', + reactDocgenTypescriptOptions: { + shouldExtractLiteralValuesFromEnum: true, + shouldRemoveUndefinedFromOptional: true, + propFilter: (prop) => { + if (prop.parent) { + return !prop.parent.fileName.includes('node_modules'); + } + return true; + }, + }, + check: false, + }, addons: [ '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-onboarding', '@storybook/addon-interactions', - '@storybook/addon-mdx-gfm' + '@storybook/addon-mdx-gfm', ], framework: { name: '@storybook/react-vite', diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..593b87f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "jpoissonnier.vscode-styled-components" + ] +} diff --git a/package.json b/package.json index f66306c..d28c4ac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@yourssu/design-system-react", "private": false, - "version": "1.0.2", + "version": "1.1.0", "description": "Yourssu Design System for React", "keywords": [ "yourssu", diff --git a/src/components/Badge/Badge.type.ts b/src/components/Badge/Badge.type.ts index 62a5bf9..4ac49af 100644 --- a/src/components/Badge/Badge.type.ts +++ b/src/components/Badge/Badge.type.ts @@ -1,8 +1,8 @@ -import { SemanticBGColor } from '@/style'; +import { SemanticItemBGColor } from '@/style'; export interface BadgeProps extends React.HTMLAttributes { /** 배경 색상 */ - color?: SemanticBGColor; + color?: SemanticItemBGColor; /** Badge 안에 들어갈 텍스트 */ children?: React.ReactNode; /** 텍스트 왼쪽에 들어갈 아이콘 */ diff --git a/src/components/TextField/PasswordTextField/PasswordTextField.stories.tsx b/src/components/TextField/PasswordTextField/PasswordTextField.stories.tsx new file mode 100644 index 0000000..334e63e --- /dev/null +++ b/src/components/TextField/PasswordTextField/PasswordTextField.stories.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; + +import { Meta, StoryObj } from '@storybook/react'; + +import { PasswordTextField } from './PasswordTextField'; + +const meta: Meta = { + title: 'Atoms/TextField/PasswordTextField', + component: PasswordTextField, + parameters: { + layout: 'centered', + }, +}; +export default meta; + +const TextFieldStory = ({ ...textFieldProps }) => { + const [value, setValue] = useState(''); + const onChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + }; + + const newProps = { ...textFieldProps, value, onChange }; + return ; +}; + +type Story = StoryObj; +export const Primary: Story = { + args: { + fieldLabel: '필드 라벨', + helperLabel: '도움말 텍스트', + placeholder: '플레이스 홀더', + disabled: false, + isPositive: false, + isNegative: false, + isMarked: true, + width: '350px', + }, + render: TextFieldStory, +}; + +export const Disabled: Story = { + args: { + fieldLabel: '필드 라벨', + helperLabel: '도움말 텍스트', + placeholder: '플레이스 홀더', + disabled: true, + width: '350px', + }, + render: TextFieldStory, +}; + +export const Positive: Story = { + args: { + fieldLabel: '필드 라벨', + helperLabel: '도움말 텍스트', + placeholder: '플레이스 홀더', + disabled: false, + isPositive: true, + isMarked: false, + width: '350px', + }, + render: TextFieldStory, +}; + +export const Negative: Story = { + args: { + fieldLabel: '필드 라벨', + helperLabel: '도움말 텍스트', + placeholder: '플레이스 홀더', + disabled: false, + isNegative: true, + isMarked: true, + width: '350px', + }, + render: TextFieldStory, +}; diff --git a/src/components/TextField/PasswordTextField/PasswordTextField.tsx b/src/components/TextField/PasswordTextField/PasswordTextField.tsx new file mode 100644 index 0000000..6b8c4aa --- /dev/null +++ b/src/components/TextField/PasswordTextField/PasswordTextField.tsx @@ -0,0 +1,34 @@ +import { useState } from 'react'; + +import { useTheme } from 'styled-components'; + +import { IcEyeclosedLine, IcEyeopenLine, IconContext } from '@/style'; + +import { TextField } from '../TextField'; + +import { PasswordTextFieldProps } from './PasswordTextField.type'; + +export const PasswordTextField = ({ isMarked, ...props }: PasswordTextFieldProps) => { + const [isMarkedValue, setIsMarkedValue] = useState(isMarked); + const onClickEyeButton = () => { + setIsMarkedValue((prev) => !prev); + }; + return ( + +
+ {isMarkedValue ? : } +
+ + } + {...props} + >
+ ); +}; diff --git a/src/components/TextField/PasswordTextField/PasswordTextField.type.ts b/src/components/TextField/PasswordTextField/PasswordTextField.type.ts new file mode 100644 index 0000000..879bc71 --- /dev/null +++ b/src/components/TextField/PasswordTextField/PasswordTextField.type.ts @@ -0,0 +1,6 @@ +import { TextFieldProps } from '../TextField.type'; + +export interface PasswordTextFieldProps extends Omit { + /** 입력된 내용을 보지 못하게 할 것인지 나타내는 속성 */ + isMarked?: boolean; +} diff --git a/src/components/TextField/SearchTextField/SearchTextField.stories.tsx b/src/components/TextField/SearchTextField/SearchTextField.stories.tsx new file mode 100644 index 0000000..9f1a8b8 --- /dev/null +++ b/src/components/TextField/SearchTextField/SearchTextField.stories.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; + +import { Meta, StoryObj } from '@storybook/react'; + +import { SearchTextField } from './SearchTextField'; + +const meta: Meta = { + title: 'Atoms/TextField/SearchTextField', + component: SearchTextField, + parameters: { + layout: 'centered', + }, +}; +export default meta; + +const TextFieldStory = ({ ...textFieldProps }) => { + const [value, setValue] = useState(''); + const onChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + }; + const onClickClearButton = () => { + setValue(''); + }; + + const newProps = { ...textFieldProps, value, onChange, onClickClearButton }; + return ; +}; + +type Story = StoryObj; +export const Primary: Story = { + args: { + isFocused: false, + isTyping: false, + placeholder: '플레이스 홀더', + disabled: false, + width: '350px', + }, + render: TextFieldStory, +}; + +export const Disabled: Story = { + args: { + placeholder: '플레이스 홀더', + disabled: true, + width: '350px', + }, + render: TextFieldStory, +}; + +export const Focused: Story = { + args: { + isFocused: true, + placeholder: '플레이스 홀더', + disabled: false, + width: '350px', + }, + render: TextFieldStory, +}; diff --git a/src/components/TextField/SearchTextField/SearchTextField.tsx b/src/components/TextField/SearchTextField/SearchTextField.tsx new file mode 100644 index 0000000..3a15227 --- /dev/null +++ b/src/components/TextField/SearchTextField/SearchTextField.tsx @@ -0,0 +1,39 @@ +import { useTheme } from 'styled-components'; + +import { IcSearchLine, IcXLine, IconContext } from '@/style'; + +import { TextField } from '../TextField'; + +import { SearchTextFieldProps } from './SearchTextField.type'; + +export const SearchTextField = ({ onClickClearButton, ...props }: SearchTextFieldProps) => { + const theme = useTheme(); + + return ( + +
+ +
+ + } + searchPrefix={ + + + + } + {...props} + /> + ); +}; diff --git a/src/components/TextField/SearchTextField/SearchTextField.type.ts b/src/components/TextField/SearchTextField/SearchTextField.type.ts new file mode 100644 index 0000000..1ca448f --- /dev/null +++ b/src/components/TextField/SearchTextField/SearchTextField.type.ts @@ -0,0 +1,10 @@ +import { TextFieldProps } from '../TextField.type'; + +export interface SearchTextFieldProps + extends Omit< + TextFieldProps, + 'isNegative' | 'isPositive' | 'fieldLabel' | 'helperLabel' | 'suffix' | 'searchPrefix' + > { + /** x 버튼을 클릭했을 때 이벤트 핸들러 */ + onClickClearButton?: () => void; +} diff --git a/src/components/TextField/SimpleTextField/SimpleTextField.stories.tsx b/src/components/TextField/SimpleTextField/SimpleTextField.stories.tsx index 1d65986..d3f5172 100644 --- a/src/components/TextField/SimpleTextField/SimpleTextField.stories.tsx +++ b/src/components/TextField/SimpleTextField/SimpleTextField.stories.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; -import { Stories, Primary as PrimaryBlock, Controls, Title } from '@storybook/blocks'; import { Meta, StoryObj } from '@storybook/react'; import { SimpleTextField } from './SimpleTextField'; @@ -10,58 +9,6 @@ const meta: Meta = { component: SimpleTextField, parameters: { layout: 'centered', - docs: { - page: () => ( - <> - - <PrimaryBlock /> - <Controls /> - <h2> 주의사항 </h2> - <ol> - <li> - TextField의 종류에 따라 suffix, searchPrefix 속성 값이 일부 정해져 있습니다. - <br /> - <br /> - <table> - <tr> - <th>종류</th> - <th>suffix</th> - <th>searchPrefix</th> - </tr> - <tr> - <td>SimpleTextField</td> - <td>IcXLine</td> - <td>설정 불가</td> - </tr> - <tr> - <td>SuffixTextField</td> - <td>사용자가 설정한 값</td> - <td>설정 불가</td> - </tr> - <tr> - <td>SearchTextField</td> - <td>IcXLine</td> - <td>IcSearchLine</td> - </tr> - <tr> - <td>PasswordTextField</td> - <td>IcEyeclosedLine 또는 IcEyeopenLine</td> - <td>설정 불가</td> - </tr> - </table> - </li> - <br /> - <li> - boolean 타입 속성의 우선순위는 아래와 같습니다. - <br /> - disabled > isNegative > isPositive - </li> - </ol> - <br /> - <Stories /> - </> - ), - }, }, }; export default meta; diff --git a/src/components/TextField/SimpleTextField/SimpleTextField.tsx b/src/components/TextField/SimpleTextField/SimpleTextField.tsx index a49bf1c..ea2fe6a 100644 --- a/src/components/TextField/SimpleTextField/SimpleTextField.tsx +++ b/src/components/TextField/SimpleTextField/SimpleTextField.tsx @@ -1,3 +1,5 @@ +import { useTheme } from 'styled-components'; + import { IcXLine, IconContext } from '@/style'; import { TextField } from '../TextField'; @@ -10,7 +12,7 @@ export const SimpleTextField = ({ onClickClearButton, ...props }: SimpleTextFiel suffix={ <IconContext.Provider value={{ - color: '#0f0f0f', + color: useTheme().color.buttonNormal, size: '1rem', }} > diff --git a/src/components/TextField/SimpleTextField/SimpleTextField.type.ts b/src/components/TextField/SimpleTextField/SimpleTextField.type.ts index bb0ec8d..bb6d7fe 100644 --- a/src/components/TextField/SimpleTextField/SimpleTextField.type.ts +++ b/src/components/TextField/SimpleTextField/SimpleTextField.type.ts @@ -1,6 +1,6 @@ import { TextFieldProps } from '../TextField.type'; -export interface SimpleTextFieldProps extends TextFieldProps { +export interface SimpleTextFieldProps extends Omit<TextFieldProps, 'suffix' | 'searchPrefix'> { /** x 버튼을 클릭했을 때 이벤트 핸들러 */ onClickClearButton?: () => void; } diff --git a/src/components/TextField/SuffixTextField/SuffixTextField.stories.tsx b/src/components/TextField/SuffixTextField/SuffixTextField.stories.tsx new file mode 100644 index 0000000..2757995 --- /dev/null +++ b/src/components/TextField/SuffixTextField/SuffixTextField.stories.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react'; + +import { Meta, StoryObj } from '@storybook/react'; + +import { SuffixTextField } from './SuffixTextField'; + +const meta: Meta<typeof SuffixTextField> = { + title: 'Atoms/TextField/SuffixTextField', + component: SuffixTextField, + parameters: { + layout: 'centered', + }, +}; +export default meta; + +const TextFieldStory = ({ ...textFieldProps }) => { + const [value, setValue] = useState(''); + const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setValue(e.target.value); + }; + + const newProps = { ...textFieldProps, value, onChange }; + return <SuffixTextField {...newProps} />; +}; + +type Story = StoryObj<typeof SuffixTextField>; +export const Primary: Story = { + args: { + fieldLabel: '필드 라벨', + helperLabel: '도움말 텍스트', + placeholder: '플레이스 홀더', + disabled: false, + isPositive: false, + isNegative: false, + width: '350px', + suffix: '@soongsil.ac.kr', + }, + render: TextFieldStory, +}; + +export const Disabled: Story = { + args: { + fieldLabel: '필드 라벨', + helperLabel: '도움말 텍스트', + placeholder: '플레이스 홀더', + disabled: true, + width: '350px', + suffix: '@soongsil.ac.kr', + }, + render: TextFieldStory, +}; + +export const Positive: Story = { + args: { + fieldLabel: '필드 라벨', + helperLabel: '도움말 텍스트', + placeholder: '플레이스 홀더', + disabled: false, + isPositive: true, + width: '350px', + suffix: '@soongsil.ac.kr', + }, + render: TextFieldStory, +}; + +export const Negative: Story = { + args: { + fieldLabel: '필드 라벨', + helperLabel: '도움말 텍스트', + placeholder: '플레이스 홀더', + disabled: false, + isNegative: true, + width: '350px', + suffix: '@soongsil.ac.kr', + }, + render: TextFieldStory, +}; diff --git a/src/components/TextField/SuffixTextField/SuffixTextField.tsx b/src/components/TextField/SuffixTextField/SuffixTextField.tsx new file mode 100644 index 0000000..5984f1e --- /dev/null +++ b/src/components/TextField/SuffixTextField/SuffixTextField.tsx @@ -0,0 +1,7 @@ +import { TextField } from '../TextField'; + +import { SuffixTextFieldProps } from './SuffixTextField.type'; + +export const SuffixTextField = ({ suffix, ...props }: SuffixTextFieldProps) => { + return <TextField suffix={suffix} {...props} />; +}; diff --git a/src/components/TextField/SuffixTextField/SuffixTextField.type.ts b/src/components/TextField/SuffixTextField/SuffixTextField.type.ts new file mode 100644 index 0000000..4667363 --- /dev/null +++ b/src/components/TextField/SuffixTextField/SuffixTextField.type.ts @@ -0,0 +1,6 @@ +import { TextFieldProps } from '../TextField.type'; + +export interface SuffixTextFieldProps extends Omit<TextFieldProps, 'searchPrefix'> { + /** TextField 오른쪽에 들어갈 텍스트 */ + suffix?: string; +} diff --git a/src/components/TextField/TextField.stories.tsx b/src/components/TextField/TextField.stories.tsx new file mode 100644 index 0000000..0165025 --- /dev/null +++ b/src/components/TextField/TextField.stories.tsx @@ -0,0 +1,72 @@ +import { Controls, Title } from '@storybook/blocks'; +import { Meta } from '@storybook/react'; + +import { TextField } from './TextField'; + +const meta: Meta = { + title: 'Atoms/TextField', + component: TextField, + parameters: { + layout: 'centered', + docs: { + page: () => ( + <> + <Title /> + <h2>주의사항</h2> + <ol> + <li> + TextField의 종류에 따라 suffix, searchPrefix 속성 값이 일부 정해져 있습니다. + <br /> + <br /> + <table> + <tbody> + <tr> + <th>종류</th> + <th>suffix</th> + <th>searchPrefix</th> + </tr> + <tr> + <td>SimpleTextField</td> + <td>IcXLine</td> + <td>설정 불가</td> + </tr> + <tr> + <td>SuffixTextField</td> + <td>사용자가 설정한 값</td> + <td>설정 불가</td> + </tr> + <tr> + <td>SearchTextField</td> + <td>IcXLine</td> + <td>IcSearchLine</td> + </tr> + <tr> + <td>PasswordTextField</td> + <td>IcEyeclosedLine 또는 IcEyeopenLine</td> + <td>설정 불가</td> + </tr> + </tbody> + </table> + </li> + <br /> + <li> + boolean 타입 속성의 우선순위는 아래와 같습니다. + <br /> + disabled > isNegative > isPositive + </li> + </ol> + <br /> + <h2>TextField 속성</h2> + <Controls /> + </> + ), + }, + }, +}; +export default meta; + +const TextFieldStory = () => {}; + +export const Primary = { + render: TextFieldStory, +}; diff --git a/src/components/TextField/TextField.style.ts b/src/components/TextField/TextField.style.ts index 7ad4bf5..2f11e7a 100644 --- a/src/components/TextField/TextField.style.ts +++ b/src/components/TextField/TextField.style.ts @@ -25,6 +25,7 @@ export const StyledTextFieldWrapper = styled.div<StyledTextFieldProps>` margin: 8px 0 0 0; padding: 12px 16px; + gap: 4px; .suffix-icon { visibility: hidden; @@ -79,6 +80,16 @@ export const StyledTextField = styled.input<StyledTextFieldProps>` color: ${({ theme, disabled }) => disabled ? theme.color.textDisabled : theme.color.textTertiary}; } + + &::-ms-reveal { + display: none; + } +`; + +export const StyledSuffixText = styled.span<StyledTextFieldProps>` + ${({ theme }) => theme.typo.body2}; + color: ${({ theme, $isDisabled }) => + $isDisabled ? theme.color.textDisabled : theme.color.textTertiary}; `; export const StyledFieldLabel = styled.label<StyledTextFieldProps>` diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index 25c50ae..befe079 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -1,6 +1,7 @@ import { StyledFieldLabel, StyledHelperLabel, + StyledSuffixText, StyledTextField, StyledTextFieldWrapper, } from './TextField.style'; @@ -31,7 +32,11 @@ export const TextField = ({ > {searchPrefix} <StyledTextField {...props} /> - {suffix} + {typeof suffix === 'string' ? ( + <StyledSuffixText $isDisabled={props.disabled}>{suffix}</StyledSuffixText> + ) : ( + suffix + )} </StyledTextFieldWrapper> {helperLabel && ( <StyledHelperLabel $isNegative={isNegative} $isDisabled={props.disabled}> diff --git a/src/components/TextField/index.ts b/src/components/TextField/index.ts index a2cc4f0..1aeb192 100644 --- a/src/components/TextField/index.ts +++ b/src/components/TextField/index.ts @@ -1,2 +1,11 @@ export { SimpleTextField } from './SimpleTextField/SimpleTextField'; export type { SimpleTextFieldProps } from './SimpleTextField/SimpleTextField.type'; + +export { SuffixTextField } from './SuffixTextField/SuffixTextField'; +export type { SuffixTextFieldProps } from './SuffixTextField/SuffixTextField.type'; + +export { PasswordTextField } from './PasswordTextField/PasswordTextField'; +export type { PasswordTextFieldProps } from './PasswordTextField/PasswordTextField.type'; + +export { SearchTextField } from './SearchTextField/SearchTextField'; +export type { SearchTextFieldProps } from './SearchTextField/SearchTextField.type'; diff --git a/src/components/Toast/HookSource.md b/src/components/Toast/HookSource.md index 680d789..1a1a9a7 100644 --- a/src/components/Toast/HookSource.md +++ b/src/components/Toast/HookSource.md @@ -1,4 +1,4 @@ -```typescript +```tsx import { ToastDuration, useToast, Toast } from '@yourssu/design-system-react'; const ToastWrapper = () => { @@ -11,7 +11,14 @@ const ToastWrapper = () => { return ( <div> - <button onClick={() => { showToast(toastProps.duration); }}> 버튼 </button> + <button + onClick={() => { + showToast(toastProps.duration); + }} + > + {' '} + 버튼{' '} + </button> {isShowToast && <Toast {...toastProps} />} </div> ); diff --git a/src/components/Toast/Toast.stories.tsx b/src/components/Toast/Toast.stories.tsx index c478976..3824a7e 100644 --- a/src/components/Toast/Toast.stories.tsx +++ b/src/components/Toast/Toast.stories.tsx @@ -1,11 +1,10 @@ -import { Stories, Primary as PrimaryBlock, Controls, Title, Markdown } from '@storybook/blocks'; +import { Primary as PrimaryBlock, Controls, Title, Markdown } from '@storybook/blocks'; import { Meta, StoryObj } from '@storybook/react'; import { useToast } from '@/hooks/useToast'; import HookSource from './HookSource.md?raw'; import { Toast } from './Toast'; -import { ToastDuration } from './Toast.type'; const meta: Meta<typeof Toast> = { title: 'Component/Toast', @@ -20,18 +19,12 @@ const meta: Meta<typeof Toast> = { <Controls /> <h2> 주의사항 </h2> <ol> - <li>Toast의 width는 Toast 를 감싸는 컴포넌트의 width에 영향을 받습니다.</li> - <li> - Toast의 위치는 position: relative 속성이 설정된 가장 가까운 부모 컴포넌트에 의해 - 결정됩니다. - </li> + <li>width props 값이 fit-content보다 작을 경우 적용되지 않습니다.</li> </ol> <br /> <Title>useToast Toast 컴포넌트를 사용하기 위한 Custom Hook입니다. {HookSource} -
- ), }, @@ -41,48 +34,36 @@ export default meta; const ToastStory = ({ ...toastProps }) => { return ( -
-
- short duration toast (1.5s) - -
-
- long duration toast (3s) - -
+
+
); }; -const HookTest = () => { - const toastProps = { - children: 'useToast를 사용한 토스트 메시지', - duration: 'long' as ToastDuration, - }; +const HookTest = ({ ...toastProps }) => { const { showToast, isShowToast } = useToast(); return (
{isShowToast && }
@@ -90,18 +71,25 @@ const HookTest = () => { }; type Story = StoryObj; +export const ToastHook: Story = { + render: HookTest, + args: { + children: 'useToast를 사용한 토스트 메시지', + duration: 'long', + }, +}; export const SingleLine: Story = { args: { children: '토스트 메시지', + duration: 'short', + width: '300px', }, render: ToastStory, }; export const MultiLine: Story = { args: { - children: '줄 수가 두 줄 이상이 되는 토스트 메시지입니다. 좌측 정렬을 해주세요.', + children: '줄 수가 두 줄 이상이 되는 토스트 메시지입니다.\n좌측 정렬을 해주세요.', + duration: 'short', }, render: ToastStory, }; -export const ToastHook: Story = { - render: HookTest, -}; diff --git a/src/components/Toast/Toast.style.ts b/src/components/Toast/Toast.style.ts index 15a0433..2c3a926 100644 --- a/src/components/Toast/Toast.style.ts +++ b/src/components/Toast/Toast.style.ts @@ -1,9 +1,10 @@ import { css, keyframes, styled } from 'styled-components'; -import { ToastDuration } from './Toast.type'; +import { ToastDuration, ToastProps } from './Toast.type'; interface StyledToastProps { $duration: ToastDuration; + $width: ToastProps['width']; } const SHORT_DURATION = 1.5; @@ -26,34 +27,49 @@ const setToastAnimation = ($duration: ToastDuration) => { switch ($duration) { case 'short': return css` - ${ToastFadeIn} ${FADE_DURATION}s ease-in forwards, - ${ToastFadeOut} ${FADE_DURATION}s ${SHORT_DURATION + FADE_DURATION}s ease-out forwards + animation: + ${ToastFadeIn} ${FADE_DURATION}s ease-in forwards, + ${ToastFadeOut} ${FADE_DURATION}s ${SHORT_DURATION + FADE_DURATION}s ease-out forwards; `; case 'long': return css` - ${ToastFadeIn} ${FADE_DURATION}s ease-in forwards, - ${ToastFadeOut} ${FADE_DURATION}s ${LONG_DURATION + FADE_DURATION}s ease-out forwards + animation: + ${ToastFadeIn} ${FADE_DURATION}s ease-in forwards, + ${ToastFadeOut} ${FADE_DURATION}s ${LONG_DURATION + FADE_DURATION}s ease-out forwards; `; } }; export const StyledToastWrapper = styled.div` - position: absolute; - bottom: 66px; + position: fixed; + inset: 0px; width: 100%; + height: 100%; padding: 0px 8px; + + display: flex; + justify-content: center; + + pointer-events: none; `; export const StyledToast = styled.div` - opacity: 0; - border-radius: 8px; - width: 100%; - padding: 16px 24px; + position: absolute; + bottom: 66px; + min-width: fit-content; + width: ${({ $width }) => $width}; + max-width: 100%; + display: flex; justify-content: center; + padding: 16px 24px; + opacity: 0; + background-color: ${({ theme }) => theme.color.toastBG}; + border-radius: 8px; color: ${({ theme }) => theme.color.textBright}; ${({ theme }) => theme.typo.body2}; + white-space: pre-line; - animation: ${({ $duration }) => setToastAnimation($duration)}; + ${({ $duration }) => setToastAnimation($duration)}; `; diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx index 3f7a519..3065180 100644 --- a/src/components/Toast/Toast.tsx +++ b/src/components/Toast/Toast.tsx @@ -1,12 +1,12 @@ import { StyledToast, StyledToastWrapper } from './Toast.style'; import { ToastProps } from './Toast.type'; -export const Toast = ({ children, duration = 'short', ...props }: ToastProps) => { +export const Toast = ({ children, duration = 'short', width, ...props }: ToastProps) => { if (!children) return; return ( - + {children} diff --git a/src/components/Toast/Toast.type.ts b/src/components/Toast/Toast.type.ts index 3d3b12d..aec2e12 100644 --- a/src/components/Toast/Toast.type.ts +++ b/src/components/Toast/Toast.type.ts @@ -5,4 +5,6 @@ export interface ToastProps extends React.HTMLAttributes { children?: React.ReactNode; /** 지속 시간 (1.5s | 3s)*/ duration?: ToastDuration; + /** Toast의 width */ + width?: string; } diff --git a/src/components/index.ts b/src/components/index.ts index e3524fc..a173ddf 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -27,3 +27,12 @@ export type { ToastProps, ToastDuration } from './Toast'; export { SimpleTextField } from './TextField'; export type { SimpleTextFieldProps } from './TextField'; + +export { SuffixTextField } from './TextField'; +export type { SuffixTextFieldProps } from './TextField'; + +export { PasswordTextField } from './TextField'; +export type { PasswordTextFieldProps } from './TextField'; + +export { SearchTextField } from './TextField'; +export type { SearchTextFieldProps } from './TextField'; diff --git a/src/style/foundation/color/semanticColor/semanticColor.type.ts b/src/style/foundation/color/semanticColor/semanticColor.type.ts index 0ec4d27..5302054 100644 --- a/src/style/foundation/color/semanticColor/semanticColor.type.ts +++ b/src/style/foundation/color/semanticColor/semanticColor.type.ts @@ -99,5 +99,5 @@ export type SemanticColor = | SemanticItemColor; // Utility Types -type OnlyBGColor = T extends `${string}BG` ? T : never; -export type SemanticBGColor = OnlyBGColor; +type OnlyItemBGColor = T extends `${string}ItemBG` ? T : never; +export type SemanticItemBGColor = OnlyItemBGColor;