diff --git a/src/assets/icons/add.svg b/src/assets/icons/add.svg new file mode 100644 index 00000000..88b48132 --- /dev/null +++ b/src/assets/icons/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/logout.svg b/src/assets/icons/logout.svg new file mode 100644 index 00000000..cb87385a --- /dev/null +++ b/src/assets/icons/logout.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/profileDialog.svg b/src/assets/icons/profileDialog.svg new file mode 100644 index 00000000..57914426 --- /dev/null +++ b/src/assets/icons/profileDialog.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/setting.svg b/src/assets/icons/setting.svg new file mode 100644 index 00000000..257a4a0e --- /dev/null +++ b/src/assets/icons/setting.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Dialog/Dialog.stories.tsx b/src/components/Dialog/Dialog.stories.tsx new file mode 100644 index 00000000..6980a584 --- /dev/null +++ b/src/components/Dialog/Dialog.stories.tsx @@ -0,0 +1,76 @@ +import Icon from '@components/Icon'; +import type { Meta, StoryObj } from '@storybook/react'; + +import Dialog from '.'; +import * as styles from './style.css'; + +const meta: Meta = { + title: 'Components/Dialog', + component: Dialog, + parameters: { + componentSubtitle: '다양한 액션을 제공하는 컴포넌트', + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Small: Story = { + render: () => ( + <> + + {}}>수정하기 + {}}>삭제하기 + + + ), +}; + +export const MediumFolder: Story = { + render: () => ( + <> + + {}}> + OOO님의 폴더기본 + + {}}>폴더 이름1 + {}}>폴더 이름2 + {}}> +
+ +
+ 새 폴더 만들기 +
+
+ + ), +}; + +export const MediumProfile: Story = { + render: () => ( + <> + + +
+ +
+
+ 바로가나다라마바님 + + {}}> +
+ +
+ 계정 설정 +
+ {}}> +
+ +
+ 로그아웃 +
+
+ + ), +}; diff --git a/src/components/Dialog/components/DialogButton.tsx b/src/components/Dialog/components/DialogButton.tsx new file mode 100644 index 00000000..c57bc153 --- /dev/null +++ b/src/components/Dialog/components/DialogButton.tsx @@ -0,0 +1,24 @@ +import { type PropsWithChildren } from 'react'; + +import Button from '@components/Button'; +import * as styles from '@components/Dialog/style.css'; +import { useDialogContext } from '@hooks/useDialogContext'; + +interface DialogButtonProps { + onClick: () => void; +} + +const DialogButton = ({ + children, + onClick, +}: PropsWithChildren) => { + const { type } = useDialogContext(); + + return ( + + ); +}; + +export default DialogButton; diff --git a/src/components/Dialog/components/DialogTitle.tsx b/src/components/Dialog/components/DialogTitle.tsx new file mode 100644 index 00000000..e4f05213 --- /dev/null +++ b/src/components/Dialog/components/DialogTitle.tsx @@ -0,0 +1,14 @@ +import { type PropsWithChildren } from 'react'; + +import * as styles from '@components/Dialog/style.css'; + +const DialogTitle = ({ children }: PropsWithChildren) => { + return ( + <> +
{children}
+
+ + ); +}; + +export default DialogTitle; diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx new file mode 100644 index 00000000..b57ae9d9 --- /dev/null +++ b/src/components/Dialog/index.tsx @@ -0,0 +1,32 @@ +import { createContext, type PropsWithChildren } from 'react'; + +import DialogButton from '@components/Dialog/components/DialogButton'; +import DialogTitle from '@components/Dialog/components/DialogTitle'; +import * as styles from '@components/Dialog/style.css'; + +interface DialogContextProps { + type: 'small' | 'medium'; +} + +type DialogRootProps = DialogContextProps & PropsWithChildren; + +export const DialogContext = createContext(null); + +const DialogRoot = ({ children, type }: DialogRootProps) => { + return ( + +
{children}
+
+ ); +}; + +const Dialog = Object.assign(DialogRoot, { + Title: DialogTitle, + Button: DialogButton, +}); + +export default Dialog; diff --git a/src/components/Dialog/style.css.ts b/src/components/Dialog/style.css.ts new file mode 100644 index 00000000..e5a1591d --- /dev/null +++ b/src/components/Dialog/style.css.ts @@ -0,0 +1,145 @@ +import { style } from '@vanilla-extract/css'; +import { recipe } from '@vanilla-extract/recipes'; + +import { sprinkles } from '@styles/sprinkles.css'; +import { COLORS } from '@styles/tokens'; + +export const dialogRoot = recipe({ + base: { + borderRadius: '12px', + boxShadow: '0px 8px 15px 0px rgba(28, 28, 28, 0.08);', + }, + variants: { + type: { + small: { + width: '100px', + padding: '8px', + }, + medium: { + width: '228px', + padding: '12px', + }, + }, + }, +}); + +export const dialogTitle = style({ + position: 'relative', + padding: '6px 12px 10px', + lineHeight: '40px', +}); + +export const line = style({ + height: '1px', + width: '196px', + backgroundColor: COLORS['Grey/150'], + margin: '4px 0', +}); + +export const dialogButton = recipe({ + base: [ + sprinkles({ + typography: '15/Body/Regular', + }), + { + color: COLORS['Grey/900'], + borderRadius: '4px', + display: 'block', + ':hover': { + backgroundColor: COLORS['Grey/100'], + }, + }, + ], + variants: { + type: { + small: { + padding: '8px', + width: '84px', + selectors: { + '& + &': { + marginTop: '8px', + }, + }, + }, + medium: { + padding: '8px 12px', + width: '204px', + textAlign: 'left', + selectors: { + '& + &': { + marginTop: '8px', + }, + }, + }, + }, + }, +}); + +export const badge = style([ + sprinkles({ + typography: '11/Caption/Medium', + }), + { + marginLeft: '4px', + backgroundColor: COLORS['Blue/Light'], + color: COLORS['Blue/Default'], + width: '32px', + height: '20px', + display: 'inline-block', + verticalAlign: 'middle', + marginTop: '-2px', + padding: '3px 6px', + borderRadius: '100px', + }, +]); + +export const iconTitleText = style([ + sprinkles({ + typography: '16/Title/Medium', + }), + { + color: COLORS['Grey/900'], + marginLeft: '48px', + }, +]); + +export const iconMediumText = style([ + sprinkles({ + typography: '15/Body/Medium', + }), + { + color: COLORS['Grey/400'], + marginLeft: '28px', + }, +]); + +export const iconRegularText = style([ + sprinkles({ + typography: '15/Body/Regular', + }), + { + color: COLORS['Grey/800'], + marginLeft: '28px', + }, +]); + +export const icon = style({ + position: 'absolute', + marginTop: '2px', +}); + +export const circle = style({ + width: '40px', + height: '40px', + backgroundColor: COLORS['Grey/100'], + position: 'absolute', + top: '6px', + borderRadius: '50%', + zIndex: -1, +}); + +export const profileIcon = style({ + position: 'absolute', + top: '13px', + left: '20px', +}); diff --git a/src/constants/icon.ts b/src/constants/icon.ts index 501e2eeb..9d171579 100644 --- a/src/constants/icon.ts +++ b/src/constants/icon.ts @@ -1,16 +1,24 @@ +import Add from '@assets/icons/add.svg'; import Archive from '@assets/icons/archive.svg'; import Close from '@assets/icons/close.svg'; import Copy from '@assets/icons/copy.svg'; +import Logout from '@assets/icons/logout.svg'; import Menu from '@assets/icons/menu.svg'; import Profle from '@assets/icons/profile.svg'; +import ProfileDialog from '@assets/icons/profileDialog.svg'; +import Setting from '@assets/icons/setting.svg'; import Submit from '@assets/icons/submit.svg'; export const iconFactory = { + add: Add, archive: Archive, close: Close, copy: Copy, + logout: Logout, menu: Menu, profile: Profle, + profileDialog: ProfileDialog, + setting: Setting, submit: Submit, }; diff --git a/src/hooks/useDialogContext.ts b/src/hooks/useDialogContext.ts new file mode 100644 index 00000000..23636c72 --- /dev/null +++ b/src/hooks/useDialogContext.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import { DialogContext } from '../components/Dialog'; + +export const useDialogContext = () => { + const ctx = useContext(DialogContext); + + if (!ctx) { + throw new Error( + 'useDialogContext hook must be used within a Dialog component', + ); + } + + return ctx; +};