Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Toast 컴포넌트 추가 #52

Merged
merged 10 commits into from
Jan 12, 2025
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
"@repo/theme": "workspace:^",
"@repo/ui": "workspace:^",
"next": "14.2.21",
"overlay-kit": "^1.4.1",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@repo/theme": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@repo/ui": "workspace:*",
"@types/node": "^20.11.24",
"@types/react": "18.3.0",
Expand Down
56 changes: 44 additions & 12 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,49 @@
import { Icon } from '@repo/ui';
'use client';

import { Icon, Toast } from '@repo/ui';
import { tokens } from '@repo/theme';
import { overlay, OverlayProvider } from 'overlay-kit';

export default function Home() {
const notify1 = () =>
overlay.open(({ isOpen, close, unmount }) => (
<Toast
open={isOpen}
onClose={close}
leftAddon={<Toast.Icon toastType="success" />}
onExited={unmount}
>
생성된 본문이 업데이트 됐어요!
</Toast>
));

minseong0324 marked this conversation as resolved.
Show resolved Hide resolved
const notify2 = () =>
overlay.open(({ isOpen, close, unmount }) => (
<Toast
open={isOpen}
onClose={close}
leftAddon={<Toast.Icon toastType="error" />}
onExited={unmount}
>
1개 이상의 게시물을 선택해주세요
</Toast>
));

return (
<div>
웹 1팀 파이팅!
<Icon size={24} name="stack" type="stroke" />
<Icon size={24} name="stack" type="fill" />
<Icon
size={24}
name="stack"
type="stroke"
color={tokens.colors.warning300}
/>
</div>
<OverlayProvider>
<div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overlay-kit 문서 확인해 보니 <OverlayProvider /> 컴포넌트는 지금부터 바로 layout.tsx에 넣어 둬도 좋지 않을까요?.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다 수정할게요

웹 1팀 파이팅!
<Icon size={24} name="stack" type="stroke" />
<Icon size={24} name="stack" type="fill" />
<Icon
size={24}
name="stack"
type="stroke"
color={tokens.colors.warning300}
/>
<button onClick={notify1}>success 토스트 열기</button>
<button onClick={notify2}>warning 토스트 열기</button>
</div>
minseong0324 marked this conversation as resolved.
Show resolved Hide resolved
</OverlayProvider>
);
}
1 change: 1 addition & 0 deletions packages/ui/esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const buildOptions = {
}),
],
loader: { '.css': 'file' },
allowOverwrite: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 없으니까 빌드가 갑자기 터지더라구요..? 이유가 뭘까요..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 esbuild에서 빌드 후 파일 덮어쓰기를 할 때 해당 옵션이 없으면 에러가 발생할 수 있다는데... 그럼 지금까지는 왜 됐을까요? 기기괴괴...
저 옵션을 쓰고 싶지 않으면 빌드 전마다 dist 파일을 비우고 빌드를 시작하는 방법으로 해결할 수 있대요! 저희는 그대로 둬도 될 것 같긴 합니다,,,

outdir,
external: ['react'],
};
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
},
"dependencies": {
"@repo/theme": "workspace:^",
"@types/react-dom": "18.3.1",
"@vanilla-extract/css": "^1.17.0",
"@vanilla-extract/recipes": "^0.5.5",
"motion": "^11.17.0",
"react": "^18"
},
"devDependencies": {
Expand All @@ -36,7 +38,6 @@
"ts-node": "^10.9.2"
},
"peerDependencies": {
"react": "^18",
"react-dom": "^18"
"react": "^18"
minseong0324 marked this conversation as resolved.
Show resolved Hide resolved
}
}
24 changes: 24 additions & 0 deletions packages/ui/src/components/Toast/Toast.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { style } from '@vanilla-extract/css';
import { tokens } from '@repo/theme';

export const container = style({
position: 'fixed',
bottom: 40,
right: 40,
padding: `${tokens.spacing[20]} ${tokens.spacing[32]}`,
borderRadius: 100,
backgroundColor: tokens.colors.grey700,
color: tokens.colors.grey0,
});

export const content = style({
display: 'flex',
alignItems: 'center',
gap: tokens.spacing[8],
});

export const message = style({
fontSize: tokens.typography.fontSize[20],
fontWeight: tokens.typography.fontWeight.semibold,
lineHeight: '24px',
});
124 changes: 124 additions & 0 deletions packages/ui/src/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { motion, AnimatePresence, HTMLMotionProps } from 'motion/react';
import {
ForwardRefExoticComponent,
ReactNode,
forwardRef,
useEffect,
} from 'react';
import { ToastIcon } from './compounds/Icon/Icon';
import * as styles from './Toast.css';
import { useTimer } from './hooks/useTimer';

export type ToastType = 'default' | 'success' | 'error';

export type ToastProps = {
/**
* 토스트 타입
* @default 'default'
*/
toastType?: ToastType;
/**
* 왼쪽 추가 요소 (아이콘 등)
*/
leftAddon?: ReactNode;
/**
* 토스트 지속 시간
* @default 2000
*/
duration?: number;
/**
* 자식 요소
*/
children?: ReactNode;
/**
* 토스트 열기 여부
*/
open: boolean;
/**
* 토스트가 열릴 때 호출되는 함수
*/
onOpen?: VoidFunction;
/**
* 토스트가 닫힐 때 호출되는 함수
*/
onClose?: VoidFunction;
/**
* 토스트가 완전히 닫힌 후 호출되는 함수
*/
onExited?: VoidFunction;
} & Omit<HTMLMotionProps<'div'>, 'children'>;

const ToastComponent = forwardRef<HTMLDivElement, ToastProps>(
(
{
toastType = 'default',
leftAddon,
duration = 2000,
children,
open,
onOpen,
onClose,
onExited,
style: toastStyle,
...restProps
},
ref
) => {
minseong0324 marked this conversation as resolved.
Show resolved Hide resolved
const { startCurrentTimer, clearCurrentTimeout } = useTimer({
onTimerEnd: onClose,
timeoutSecond: duration,
});
kongnayeon marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
if (open) {
onOpen?.();
startCurrentTimer();
}
}, [open, onOpen, startCurrentTimer]);

return (
<AnimatePresence onExitComplete={onExited}>
{open && (
<motion.div
ref={ref}
className={styles.container}
initial={{ y: '120%', opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '120%', opacity: 0 }}
transition={{
type: 'spring',
damping: 25,
stiffness: 400,
opacity: {
duration: 0.15,
ease: 'easeInOut',
},
}}
onPointerEnter={clearCurrentTimeout}
onPointerLeave={startCurrentTimer}
style={{
...toastStyle,
}}
{...restProps}
>
<div className={styles.content}>
{leftAddon ?? <ToastIcon toastType={toastType} />}
<span className={styles.message}>{children}</span>
</div>
minseong0324 marked this conversation as resolved.
Show resolved Hide resolved
</motion.div>
)}
</AnimatePresence>
);
}
);

type ToastComposition = {
Icon: typeof ToastIcon;
};

export const Toast: ForwardRefExoticComponent<ToastProps> & ToastComposition =
Object.assign(ToastComponent, {
Icon: ToastIcon,
});

Toast.displayName = 'Toast';
39 changes: 39 additions & 0 deletions packages/ui/src/components/Toast/compounds/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Icon } from '@repo/ui';
import type { IconProps } from '@repo/ui';
import { tokens } from '@repo/theme';
import { ToastType } from '../../Toast';

export type ToastIconProps = Omit<IconProps, 'name'> & {
toastType?: ToastType;
};

export function ToastIcon({
toastType = 'default',
...restProps
}: ToastIconProps) {
const iconName = (() => {
switch (toastType) {
case 'success':
return 'check';
case 'error':
return 'notice';
default:
return null;
}
})();

if (!iconName) {
return null;
}

const iconColor = (() => {
switch (toastType) {
case 'success':
return tokens.colors.green200;
case 'error':
return tokens.colors.warning300;
}
})();
minseong0324 marked this conversation as resolved.
Show resolved Hide resolved

return <Icon type="fill" name={iconName} color={iconColor} {...restProps} />;
}
42 changes: 42 additions & 0 deletions packages/ui/src/components/Toast/hooks/useTimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useRef, useCallback, useEffect } from 'react';

type UseTimerParameters = {
onTimerEnd?: () => void;
timeoutSecond?: number;
};

export function useTimer({
onTimerEnd,
timeoutSecond = 3000,
}: UseTimerParameters) {
const timerRef = useRef<ReturnType<typeof setTimeout>>();

const startCurrentTimer = useCallback(() => {
if (!onTimerEnd) {
return;
}

timerRef.current = setTimeout(() => {
onTimerEnd();
}, timeoutSecond);
}, [timeoutSecond, onTimerEnd]);

const clearCurrentTimeout = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
}, []);

useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [timerRef]);

return {
startCurrentTimer,
clearCurrentTimeout,
};
}
4 changes: 4 additions & 0 deletions packages/ui/src/components/Toast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { Toast } from './Toast';
export type { ToastProps, ToastType } from './Toast';
export { ToastIcon } from './compounds/Icon/Icon';
export type { ToastIconProps } from './compounds/Icon/Icon';
2 changes: 2 additions & 0 deletions packages/ui/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './Spacing/Spacing';
export { Icon } from './Icon/Icon';
export type { IconName, IconProps } from './Icon/Icon';
export { Toast } from './Toast';
export type { ToastProps, ToastType, ToastIconProps } from './Toast';
Loading
Loading