Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
},
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-dialog": "^1.1.14",
"@tanstack/react-query": "^5.77.0",
"@tanstack/react-query-devtools": "^5.77.0",
"@use-funnel/browser": "^0.0.15",
Expand Down
33 changes: 33 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 59 additions & 0 deletions src/components/ui/AlertModal/AlertModal.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { style } from "@vanilla-extract/css";

import { radius, semantic, typography } from "@/styles";
import { zIndex } from "@/styles/zIndex.css";

export const overlay = style({
position: "fixed",
inset: 0,
backgroundColor: semantic.background.dim,
zIndex: zIndex.overlay,
});

export const content = style({
maxHeight: "80vh",
display: "flex",
flexDirection: "column",
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "min(30rem, 90vw)",
background: semantic.background.white,
borderRadius: radius[120],
zIndex: zIndex.modal,
});

export const innerContent = style({
overflowY: "auto",
padding: "4.8rem 1.6rem",
display: "flex",
flexDirection: "column",
alignItems: "center",
});

export const title = style({
...typography.title3Sb,
color: semantic.text.normal,
});

export const description = style({
...typography.body2Rg,
color: semantic.text.alternative,
marginTop: "0.8rem",
width: "100%",
textAlign: "center",
});

export const footer = style({
width: "100%",
display: "flex",
});

export const cancelButton = style({
flex: 1,
});

export const confirmButton = style({
flex: 1,
});
119 changes: 119 additions & 0 deletions src/components/ui/AlertModal/AlertModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as AlertDialog from "@radix-ui/react-alert-dialog";
import type { Meta, StoryObj } from "@storybook/nextjs";

import { Button } from "../Button";
import { AlertModal } from "./AlertModal";
import * as styles from "./AlertModal.css";

const meta: Meta<typeof AlertModal> = {
title: "Components/AlertModal",
component: AlertModal,
tags: ["autodocs"],
argTypes: {
title: {
control: "text",
description: "모달의 제목",
},
trigger: {
control: false,
description: "모달을 열기 위한 트리거(버튼 등, ReactNode)",
},
content: {
control: false,
description: "본문(설명 등, ReactNode)",
},
footer: {
control: false,
description:
"하단 푸터(버튼 영역 등, ReactNode). Radix의 <AlertDialog.Cancel asChild> 또는 <AlertDialog.Action asChild>를 조합하여 사용.",
},
},
parameters: {
layout: "centered",
docs: {
description: {
component:
"AlertModal은 사용자에게 확인/취소 액션을 요청할 때 사용하는 모달입니다. title, trigger, content, footer props를 지원합니다.",
},
},
},
};

export default meta;
type Story = StoryObj<typeof AlertModal>;

export const Default: Story = {
render: args => <AlertModal {...args} />,
args: {
title: "정말 삭제하시겠어요?",
trigger: <Button>모달 열기</Button>,
content: <div>삭제하면 복구할 수 없습니다.</div>,
footer: (
<>
<AlertDialog.Cancel asChild>
<Button
variant='assistive'
className={styles.cancelButton}
style={{ borderRadius: "0 0 0 1.2rem" }}
>
취소
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
<Button
variant='primary'
className={styles.confirmButton}
style={{ borderRadius: "0 0 1.2rem 0" }}
>
삭제
</Button>
</AlertDialog.Action>
</>
),
},
parameters: {
docs: {
description: {
story:
"기본 AlertModal. 타이틀과 설명, 확인/취소 버튼을 모두 포함합니다.",
},
},
},
};

export const NoDescription: Story = {
render: args => <AlertModal {...args} />,
args: {
title: "약관을 동의하시겠습니까?",
trigger: <Button>모달 열기</Button>,
footer: (
<>
<AlertDialog.Cancel asChild>
<Button
variant='assistive'
className={styles.cancelButton}
style={{ borderRadius: "0 0 0 1.2rem" }}
>
취소
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
<Button
variant='primary'
className={styles.confirmButton}
style={{ borderRadius: "0 0 1.2rem 0" }}
>
동의
</Button>
</AlertDialog.Action>
</>
),
},
parameters: {
docs: {
description: {
story: "description 없이 타이틀과 버튼만 표시되는 AlertModal입니다.",
},
},
},
};
75 changes: 75 additions & 0 deletions src/components/ui/AlertModal/AlertModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as AlertDialog from "@radix-ui/react-alert-dialog";
import { type ReactNode } from "react";

import * as styles from "./AlertModal.css";

export type AlertModalProps = {
/** 모달의 제목 */
title?: string;

/** 모달을 열기 위한 트리거(버튼 등, 선택) */
trigger?: ReactNode;

/** 본문(설명 등, ReactNode로 자유롭게 구성) */
content?: ReactNode;

/**
* 하단 푸터(버튼 영역 등, ReactNode로 자유롭게 구성)
* Radix의 <AlertDialog.Cancel asChild> 또는 <AlertDialog.Action asChild>를 조합하여 사용 가능
* 예시:
* <>
* <AlertDialog.Cancel asChild>
* <Button>취소</Button>
* </AlertDialog.Cancel>
* <AlertDialog.Action asChild>
* <Button>확인</Button>
* </AlertDialog.Action>
* </>
*/
footer?: ReactNode;
};

/**
* AlertModal 컴포넌트
*
* @example
* ```tsx
* <AlertModal
* title="정말 삭제하시겠어요?"
* trigger={<Button>모달 열기</Button>}
* content={<div>삭제하면 복구할 수 없습니다.</div>}
* footer={
* <>
* <Button variant="assistive">취소</Button>
* <Button variant="primary">삭제</Button>
* </>
* }
* />
* ```
*/
export const AlertModal = ({
title,
trigger,
content,
footer,
}: AlertModalProps) => {
return (
<AlertDialog.Root>
{trigger && <AlertDialog.Trigger asChild>{trigger}</AlertDialog.Trigger>}
<AlertDialog.Portal>
<AlertDialog.Overlay className={styles.overlay} />
<AlertDialog.Content className={styles.content}>
<section className={styles.innerContent}>
{title && (
<AlertDialog.Title className={styles.title}>
{title}
</AlertDialog.Title>
)}
{content && <div className={styles.description}>{content}</div>}
</section>
{footer && <div className={styles.footer}>{footer}</div>}
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
);
};
1 change: 1 addition & 0 deletions src/components/ui/AlertModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AlertModal } from "./AlertModal";
3 changes: 2 additions & 1 deletion src/components/ui/GNB/GNB.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";

import { semantic, typography } from "@/styles";
import { zIndex } from "@/styles/zIndex.css";

export const wrapper = recipe({
base: {
Expand All @@ -12,7 +13,7 @@ export const wrapper = recipe({
width: "100%",
height: "5.6rem",
padding: "1.4rem 2rem",
zIndex: 999,
zIndex: zIndex.gnb,
},
variants: {
background: {
Expand Down
4 changes: 4 additions & 0 deletions src/styles/reset.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ globalStyle("html", {
fontSize: "62.5%",
});

globalStyle("html, body", {
height: "100%",
});

globalStyle(
"html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video",
{
Expand Down
9 changes: 9 additions & 0 deletions src/styles/zIndex.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createGlobalTheme } from "@vanilla-extract/css";

export const zIndex = createGlobalTheme(":root", {
base: "0",
gnb: "100",
overlay: "900",
modal: "1000",
toast: "1100",
});