diff --git a/src/app/(auth)/login/_styles/Login.css.ts b/src/app/(auth)/login/_styles/Login.css.ts index 260a4b8a..938211be 100644 --- a/src/app/(auth)/login/_styles/Login.css.ts +++ b/src/app/(auth)/login/_styles/Login.css.ts @@ -1,10 +1,10 @@ import { style } from "@vanilla-extract/css"; -import { semantic, typography } from "@/styles"; +import { colors, semantic, typography } from "@/styles"; export const wrapper = style({ width: "100%", - height: "100vh", + height: "100dvh", overflow: "hidden", }); @@ -39,6 +39,7 @@ export const logoWrapper = style({ export const logoIcon = style({ width: "7.7rem", height: "4rem", + color: colors.redOrange[50], }); export const gradientOverlay = style({ diff --git a/src/app/member/onboarding/layout.css.ts b/src/app/member/onboarding/layout.css.ts index 9309cfd2..0a204b49 100644 --- a/src/app/member/onboarding/layout.css.ts +++ b/src/app/member/onboarding/layout.css.ts @@ -3,7 +3,7 @@ import { style } from "@vanilla-extract/css"; export const wrapper = style({ width: "100%", maxWidth: "480px", - height: "100vh", + height: "100dvh", display: "flex", flexDirection: "column", }); diff --git a/src/app/member/profile/_components/Banner/Banner.tsx b/src/app/member/profile/_components/Banner/Banner.tsx index 814acf46..f140e47f 100644 --- a/src/app/member/profile/_components/Banner/Banner.tsx +++ b/src/app/member/profile/_components/Banner/Banner.tsx @@ -26,7 +26,7 @@ export const Banner = () => { {/* TODO: [가게 등록하기] url로 변경 */} - + 가게 등록하기 diff --git a/src/assets/logo-wordmark.svg b/src/assets/logo-wordmark.svg index 576dcf2b..2f960815 100644 --- a/src/assets/logo-wordmark.svg +++ b/src/assets/logo-wordmark.svg @@ -1,12 +1,12 @@ - - - + + + - + diff --git a/src/components/ui/BottomSheet/BottomSheet.css.ts b/src/components/ui/BottomSheet/BottomSheet.css.ts index 3d19fb9d..9304170c 100644 --- a/src/components/ui/BottomSheet/BottomSheet.css.ts +++ b/src/components/ui/BottomSheet/BottomSheet.css.ts @@ -1,11 +1,13 @@ import { style } from "@vanilla-extract/css"; -import { colors, radius, semantic, typography } from "@/styles"; +import { colors, semantic } 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({ @@ -15,25 +17,19 @@ export const content = style({ right: 0, maxWidth: "48rem", margin: "0 auto", - display: "flex", - justifyContent: "center", backgroundColor: colors.common[100], - borderTopLeftRadius: radius[120], - borderTopRightRadius: radius[120], + borderRadius: "2.8rem 2.8rem 0 0", + zIndex: zIndex.modal, }); export const innerContent = style({ width: "100%", - minHeight: "32.6rem", - maxHeight: "100vh", + minHeight: "37.5rem", + maxHeight: "calc(100dvh - 52px)", display: "flex", flexDirection: "column", }); -export const handleContainer = style({ - padding: "1.2rem 16.2rem 1rem", -}); - export const handle = style({ width: "5.1rem", height: "0.4rem", @@ -43,29 +39,16 @@ export const handle = style({ }); export const title = style({ - display: "flex", - gap: "1rem", + width: "100%", padding: "1.4rem 2rem", - ...typography.title2Sb, - color: semantic.text.normal, }); export const sheetBody = style({ display: "flex", + flex: 1, flexDirection: "column", gap: "0.8rem", - padding: "1.4rem 2rem 6rem", - overflowY: "auto", -}); - -export const sheetBodyTitle = style({ - ...typography.title3Sb, - color: semantic.text.normal, -}); - -export const sheetBodyDescription = style({ - ...typography.body2Rg, - color: semantic.text.alternative, + padding: "2rem", }); export const buttonContainer = style({ diff --git a/src/components/ui/BottomSheet/BottomSheet.stories.tsx b/src/components/ui/BottomSheet/BottomSheet.stories.tsx index 5b5e8b1c..9f6a9fc5 100644 --- a/src/components/ui/BottomSheet/BottomSheet.stories.tsx +++ b/src/components/ui/BottomSheet/BottomSheet.stories.tsx @@ -2,137 +2,96 @@ import type { Meta, StoryObj } from "@storybook/nextjs"; import { useState } from "react"; import { Button } from "../Button"; +import { Text } from "../Text"; import { BottomSheet } from "./BottomSheet"; -import * as styles from "./BottomSheet.css"; -const meta: Meta = { +const meta: Meta = { title: "Components/BottomSheet", - component: BottomSheet, + component: BottomSheet.Root, tags: ["autodocs"], - argTypes: { - open: { table: { disable: true } }, - title: { control: "text" }, - trigger: { control: false }, - footer: { control: false }, - content: { control: false }, - }, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; -const BottomSheetWrapper = ( - args: React.ComponentProps & { - content?: React.ReactNode; - footer?: React.ReactNode; - } -) => { - const [isOpen, setIsOpen] = useState(args.open || false); +const BottomSheetWrapper = ({ + defaultOpen = false, + title = "서비스 필수 이용 약관", + bodyText = "회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 있습니다.", + confirmText = "동의하고 계속하기", +}: { + defaultOpen?: boolean; + title?: string; + bodyText?: string; + confirmText?: string; +}) => { + const [isOpen, setIsOpen] = useState(defaultOpen); return ( - + - } - content={args.content} - /> + + + + + {title} + + + + {bodyText} + + + + + + + ); }; export const Default: Story = { - render: args => ( + render: () => ( alert("동의합니다")}> - 동의하고 계속하기 - - } - content={ - <> -

비밀번호를 변경해 안내 설명

-

- 회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 경우 - 비밀번호 변경을 안내해 드리고 있습니다. 회원님의 개인정보 보호를 - 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 - 있습니다.회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 - 경우 비밀번호 변경을 안내해 드리고 있습니다.회원님의 개인정보 보호를 - 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 - 있습니다.회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 - 경우 비밀번호 변경을 안내해 드리고 있습니다.회원님의 개인정보 보호를 - 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 - 있습니다.회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 - 경우 비밀번호 변경을 안내해 드리고 있습니다.회원님의 개인정보 보호를 - 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 - 있습니다.회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 - 경우 비밀번호 변경을 안내해 드리고 있습니다.회원님의 개인정보 보호를 - 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 - 있습니다.회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 - 경우 비밀번호 변경을 안내해 드리고 있습니다.회원님의 개인정보 보호를 - 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 - 있습니다.회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 - 경우 비밀번호 변경을 안내해 드리고 있습니다.회원님의 개인정보 보호를 - 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 - 있습니다.회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 - 경우 비밀번호 변경을 안내해 드리고 있습니다.회원님의 개인정보 보호를 - 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 - 있습니다. -

- - } + defaultOpen={false} + title='서비스 필수 이용 약관' + bodyText={`회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 있습니다. +동일한 설명이 반복되어 내용이 길어지면, Body 영역에 scroll이 적용됩니다.`} + confirmText='동의하고 계속하기' /> ), parameters: { docs: { description: { story: - "버튼을 클릭하면 바텀시트가 열리고, 하단에 footer 버튼이 렌더링됩니다.", + "Trigger 버튼을 클릭하면 BottomSheet가 열리고 Title/Body/Footer가 표시됩니다.", }, }, }, }; export const Opened: Story = { - render: (args, { viewMode }) => { + render: (_, { viewMode }) => { const forceClosedInDocs = viewMode === "docs"; + return ( alert("닫기")}> - 확인 - - } - content={ - <> -

- 비밀번호를 변경해 안내 설명 -

-

- 회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 경우 - 비밀번호 변경을 안내해 드리고 있습니다. -

- - } + bodyText='회원님의 개인정보 보호를 위해 장기간 비밀번호를 유지 중인 경우 비밀번호 변경을 안내해 드리고 있습니다.' + confirmText='확인' /> ); }, - args: { - open: true, - }, parameters: { docs: { description: { story: - "Canvas에선 열린 상태로 시작되며, 하단에 버튼이 함께 표시됩니다.", + "Canvas에선 열린 상태로 시작되며, 닫기 버튼을 통해 닫을 수 있습니다.", }, }, }, diff --git a/src/components/ui/BottomSheet/BottomSheet.tsx b/src/components/ui/BottomSheet/BottomSheet.tsx index 59dcbe95..4e058e3d 100644 --- a/src/components/ui/BottomSheet/BottomSheet.tsx +++ b/src/components/ui/BottomSheet/BottomSheet.tsx @@ -1,38 +1,79 @@ -import { type ReactNode } from "react"; -import { type DialogProps,Drawer } from "vaul"; - -import * as styles from "./BottomSheet.css"; - -export type BottomSheetProps = { - title: string; - trigger?: ReactNode; - footer?: ReactNode; - content?: ReactNode; -} & DialogProps; - -export const BottomSheet = ({ - title, - trigger, - footer, - content, - ...props -}: BottomSheetProps) => { - return ( - - {trigger && {trigger}} - - - -
-
-
-
- {title} -
{content}
- {footer &&
{footer}
} -
-
-
-
- ); +"use client"; + +import { BottomSheetBody } from "./BottomSheetBody"; +import { BottomSheetContent } from "./BottomSheetContent"; +import { BottomSheetFooter } from "./BottomSheetFooter"; +import { BottomSheetRoot } from "./BottomSheetRoot"; +import { BottomSheetTitle } from "./BottomSheetTitle"; +import { BottomSheetTrigger } from "./BottomSheetTrigger"; + +type BottomSheetComposition = { + /** + * 바텀시트의 상태 및 컨텍스트를 관리하는 최상위 컴포넌트 (`vaul`의 Drawer.Root 래핑) + */ + Root: typeof BottomSheetRoot; + + /** + * 바텀시트를 열기 위한 트리거 (버튼 등) + * `asChild`로 감싸면 외부 요소를 그대로 트리거로 사용할 수 있음 + */ + Trigger: typeof BottomSheetTrigger; + + /** + * 바텀시트의 콘텐츠를 감싸는 영역. Portal을 통해 렌더링되며 Overlay와 Content를 포함 + */ + Content: typeof BottomSheetContent; + + /** + * 바텀시트의 제목 영역. 상단 핸들바와 함께 사용 + */ + Title: typeof BottomSheetTitle; + + /** + * 바텀시트의 메인 콘텐츠 영역 + */ + Body: typeof BottomSheetBody; + + /** + * 바텀시트의 하단 영역. 주로 버튼/액션을 배치하는 데 사용 + */ + Footer: typeof BottomSheetFooter; +}; + +/** + * BottomSheet 컴포넌트 + * @description Compound Component Pattern을 사용하여 화면 하단에서 나타나는 패널을 구현한 컴포넌트입니다. + * + * @see vaul 라이브러리를 기반으로 구현되었습니다. {@link https://vaul.emilkowal.ski/} + * + * @example + * ```tsx + * + * + * + * + * + * + * + * 제목 + * + * + * + *

여기에 컨텐츠가 들어갑니다.

+ *
+ * + * + * + * + *
+ *
+ * ``` + */ +export const BottomSheet: BottomSheetComposition = { + Root: BottomSheetRoot, + Title: BottomSheetTitle, + Trigger: BottomSheetTrigger, + Content: BottomSheetContent, + Body: BottomSheetBody, + Footer: BottomSheetFooter, }; diff --git a/src/components/ui/BottomSheet/BottomSheetBody.tsx b/src/components/ui/BottomSheet/BottomSheetBody.tsx new file mode 100644 index 00000000..41993765 --- /dev/null +++ b/src/components/ui/BottomSheet/BottomSheetBody.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { type ReactNode } from "react"; + +import * as styles from "./BottomSheet.css"; + +export type BottomSheetBodyProps = { + children: ReactNode; + className?: string; +}; + +export function BottomSheetBody({ children, className }: BottomSheetBodyProps) { + return ( +
+ {children} +
+ ); +} + +BottomSheetBody.displayName = "BottomSheet.Body"; diff --git a/src/components/ui/BottomSheet/BottomSheetContent.tsx b/src/components/ui/BottomSheet/BottomSheetContent.tsx new file mode 100644 index 00000000..f634dd95 --- /dev/null +++ b/src/components/ui/BottomSheet/BottomSheetContent.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { type ReactNode } from "react"; +import { Drawer } from "vaul"; + +import * as styles from "./BottomSheet.css"; + +export type BottomSheetContentProps = { + children: ReactNode; + className?: string; +}; + +export function BottomSheetContent({ + children, + className, +}: BottomSheetContentProps) { + return ( + + + +
+ {children} +
+
+
+ ); +} + +BottomSheetContent.displayName = "BottomSheet.Content"; diff --git a/src/components/ui/BottomSheet/BottomSheetFooter.tsx b/src/components/ui/BottomSheet/BottomSheetFooter.tsx new file mode 100644 index 00000000..1b2a7cbc --- /dev/null +++ b/src/components/ui/BottomSheet/BottomSheetFooter.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ReactNode } from "react"; + +import * as styles from "./BottomSheet.css"; + +export type BottomSheetFooterProps = { + children: ReactNode; + className?: string; +}; + +export function BottomSheetFooter({ + children, + className, + ...props +}: BottomSheetFooterProps) { + return ( +
+ {children} +
+ ); +} + +BottomSheetFooter.displayName = "BottomSheet.Footer"; diff --git a/src/components/ui/BottomSheet/BottomSheetRoot.tsx b/src/components/ui/BottomSheet/BottomSheetRoot.tsx new file mode 100644 index 00000000..15daba02 --- /dev/null +++ b/src/components/ui/BottomSheet/BottomSheetRoot.tsx @@ -0,0 +1,12 @@ +import { type ReactNode } from "react"; +import { type DialogProps, Drawer } from "vaul"; + +export type BottomSheetRootProps = { + children: ReactNode; +} & DialogProps; + +export function BottomSheetRoot({ children, ...props }: BottomSheetRootProps) { + return {children}; +} + +BottomSheetRoot.displayName = "BottomSheet.Root"; diff --git a/src/components/ui/BottomSheet/BottomSheetTitle.tsx b/src/components/ui/BottomSheet/BottomSheetTitle.tsx new file mode 100644 index 00000000..e905f7be --- /dev/null +++ b/src/components/ui/BottomSheet/BottomSheetTitle.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { type ReactNode } from "react"; +import { Drawer } from "vaul"; + +import * as styles from "./BottomSheet.css"; + +export type BottomSheetTitleProps = { + children: ReactNode; + className?: string; +}; + +export function BottomSheetTitle({ + children, + className, + ...props +}: BottomSheetTitleProps) { + return ( +
+
+ + {children} + +
+ ); +} + +BottomSheetTitle.displayName = "BottomSheet.Title"; diff --git a/src/components/ui/BottomSheet/BottomSheetTrigger.tsx b/src/components/ui/BottomSheet/BottomSheetTrigger.tsx new file mode 100644 index 00000000..2236972d --- /dev/null +++ b/src/components/ui/BottomSheet/BottomSheetTrigger.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { type ReactNode } from "react"; +import { Drawer } from "vaul"; + +export type BottomSheetTriggerProps = { + children: ReactNode; +}; + +export function BottomSheetTrigger({ children }: BottomSheetTriggerProps) { + return {children}; +} + +BottomSheetTrigger.displayName = "BottomSheet.Trigger"; diff --git a/src/components/ui/BottomSheet/index.ts b/src/components/ui/BottomSheet/index.ts index 3f8a2c81..98d85e1b 100644 --- a/src/components/ui/BottomSheet/index.ts +++ b/src/components/ui/BottomSheet/index.ts @@ -1 +1,7 @@ export { BottomSheet } from "./BottomSheet"; +export { BottomSheetBody } from "./BottomSheetBody"; +export { BottomSheetContent } from "./BottomSheetContent"; +export { BottomSheetFooter } from "./BottomSheetFooter"; +export { BottomSheetRoot } from "./BottomSheetRoot"; +export { BottomSheetTitle } from "./BottomSheetTitle"; +export { BottomSheetTrigger } from "./BottomSheetTrigger"; diff --git a/src/components/ui/Button/Button.css.ts b/src/components/ui/Button/Button.css.ts index d63faef3..b7d155f9 100644 --- a/src/components/ui/Button/Button.css.ts +++ b/src/components/ui/Button/Button.css.ts @@ -43,7 +43,7 @@ export const button = recipe({ }, size: { small: { - ...typography.label2, + ...typography.label2Sb, padding: "0.7rem 2rem", borderRadius: radius[80], }, diff --git a/src/components/ui/TextButton/TextButton.css.ts b/src/components/ui/TextButton/TextButton.css.ts index 2844cf6f..728275b5 100644 --- a/src/components/ui/TextButton/TextButton.css.ts +++ b/src/components/ui/TextButton/TextButton.css.ts @@ -35,7 +35,7 @@ export const textButton = recipe({ ...typography.body1Sb, }, small: { - ...typography.label1, + ...typography.label1Md, }, }, }, diff --git a/src/components/ui/TextField/TextField.css.ts b/src/components/ui/TextField/TextField.css.ts index 032afa79..e6117e9a 100644 --- a/src/components/ui/TextField/TextField.css.ts +++ b/src/components/ui/TextField/TextField.css.ts @@ -10,7 +10,7 @@ export const wrapper = style({ }); export const label = style({ - ...typography.label1, + ...typography.label1Sb, color: semantic.text.alternative, }); diff --git a/src/components/ui/TextField/TextField.tsx b/src/components/ui/TextField/TextField.tsx index 6e945683..cb123409 100644 --- a/src/components/ui/TextField/TextField.tsx +++ b/src/components/ui/TextField/TextField.tsx @@ -1,23 +1,42 @@ -import { type ComponentProps, useId } from "react"; +import { type ElementType, type ReactNode, useId } from "react"; + +import { type PolymorphicComponentPropsWithRef } from "@/types/polymorphic.types"; import * as styles from "./TextField.css"; type Status = "inactive" | "negative"; -export type TextFieldProps = { - label?: string; - helperText?: string; - status?: Status; - rightAddon?: React.ReactNode; -} & ComponentProps<"input">; +export type TextFieldProps = + PolymorphicComponentPropsWithRef< + T, + { + label?: string | ReactNode; + helperText?: string; + status?: Status; + rightAddon?: ReactNode; + } + >; -export const TextField = ({ +/** + * TextField 컴포넌트 + * @description 기본은 input이지만, as="textarea"처럼 다른 태그로도 변경 가능 + * @example + * ```tsx + * + * + * ``` + */ +export const TextField = ({ + as, + className, label, helperText, status = "inactive", rightAddon, + ref, ...props -}: TextFieldProps) => { +}: TextFieldProps) => { + const Component = as || "input"; const inputId = useId(); const helperId = `${inputId}-helper`; @@ -30,10 +49,11 @@ export const TextField = ({ )}
- diff --git a/src/components/ui/TextField/index.ts b/src/components/ui/TextField/index.ts index aa463ea6..1fb214bf 100644 --- a/src/components/ui/TextField/index.ts +++ b/src/components/ui/TextField/index.ts @@ -1 +1 @@ -export { TextField } from "./TextField"; +export { TextField, type TextFieldProps } from "./TextField"; diff --git a/src/styles/reset.css.ts b/src/styles/reset.css.ts index 8cc2f777..d75711ef 100644 --- a/src/styles/reset.css.ts +++ b/src/styles/reset.css.ts @@ -61,6 +61,9 @@ globalStyle("button", { outline: "none", boxShadow: "none", cursor: "pointer", + WebkitAppearance: "none", + MozAppearance: "none", + appearance: "none", }, }, }); diff --git a/src/styles/typography.css.ts b/src/styles/typography.css.ts index 71d06420..a2053ae7 100644 --- a/src/styles/typography.css.ts +++ b/src/styles/typography.css.ts @@ -109,18 +109,42 @@ export const typography = createGlobalTheme(":root", { letterSpacing: "0.0096em", fontWeight: "400", }, - label1: { + label1Sb: { fontSize: "1.4rem", lineHeight: "2rem", letterSpacing: "0.0145em", fontWeight: "600", }, - label2: { + label1Md: { + fontSize: "1.4rem", + lineHeight: "2rem", + letterSpacing: "0.0145em", + fontWeight: "500", + }, + label1Rg: { + fontSize: "1.4rem", + lineHeight: "2rem", + letterSpacing: "0.0145em", + fontWeight: "400", + }, + label2Sb: { fontSize: "1.3rem", lineHeight: "1.8rem", letterSpacing: "0.0194em", fontWeight: "600", }, + label2Md: { + fontSize: "1.3rem", + lineHeight: "1.8rem", + letterSpacing: "0.0194em", + fontWeight: "500", + }, + label2Rg: { + fontSize: "1.3rem", + lineHeight: "1.8rem", + letterSpacing: "0.0194em", + fontWeight: "400", + }, caption1Sb: { fontSize: "1.2rem", lineHeight: "1.6rem",