From 3682ddd00b39dafe40bed1596dcdf1e56999e41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=8D=EC=84=9C=ED=98=84?= Date: Thu, 10 Oct 2024 15:53:11 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20Avatar=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#163)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 아바타 이미지 wowds-icons에 추가 * rename: 스피너 관련 폴더 네이밍 변경 * fix: svg 이미지 관련 스토리북 이슈 해결 * feat: Avatar 컴포넌트 구현 * feat: Avatar 컴포넌트 스토리 작성 * chore: git 캐시된 부분 삭제 * refactor: Polymorphic 타입 리팩토링 * docs: jsdoc 및 스토리 문서화 추가 * chore: tsconfig 속성 변경 * fix: Avatar 컴포넌트 타입 추론 안 되는 문제 해결 * chore: orientation props default 값 right로 지정 * feat: ImageComponent도 props로 받을 수 있게 수정 * chore: Avatar 컴포넌트 changeset 작성 --- .changeset/clever-lizards-tell.md | 6 + .../wow-icons/src/component/BlueAvatar.tsx | 83 +++++++++ .../wow-icons/src/component/GreenAvatar.tsx | 87 ++++++++++ .../wow-icons/src/component/RedAvatar.tsx | 84 +++++++++ .../wow-icons/src/component/YellowAvatar.tsx | 91 ++++++++++ packages/wow-icons/src/component/index.ts | 4 + packages/wow-icons/src/svg/blue-avatar.svg | 18 ++ packages/wow-icons/src/svg/green-avatar.svg | 18 ++ packages/wow-icons/src/svg/red-avatar.svg | 20 +++ packages/wow-icons/src/svg/yellow-avatar.svg | 19 ++ packages/wow-ui/.storybook/main.ts | 22 +++ packages/wow-ui/package.json | 5 + packages/wow-ui/rollup.config.js | 1 + .../{lottie => lotties}/blueSpinner.json | 0 .../{lottie => lotties}/rainbowSpinner.json | 0 .../src/components/Avatar/Avatar.stories.tsx | 119 +++++++++++++ .../wow-ui/src/components/Avatar/index.tsx | 163 ++++++++++++++++++ .../src/components/Spinner/BlueSpinner.tsx | 2 +- .../src/components/Spinner/RainbowSpinner.tsx | 2 +- packages/wow-ui/src/types/index.ts | 4 +- .../types/{Polymorphic.ts => polymorphic.ts} | 8 +- 21 files changed, 748 insertions(+), 8 deletions(-) create mode 100644 .changeset/clever-lizards-tell.md create mode 100644 packages/wow-icons/src/component/BlueAvatar.tsx create mode 100644 packages/wow-icons/src/component/GreenAvatar.tsx create mode 100644 packages/wow-icons/src/component/RedAvatar.tsx create mode 100644 packages/wow-icons/src/component/YellowAvatar.tsx create mode 100644 packages/wow-icons/src/svg/blue-avatar.svg create mode 100644 packages/wow-icons/src/svg/green-avatar.svg create mode 100644 packages/wow-icons/src/svg/red-avatar.svg create mode 100644 packages/wow-icons/src/svg/yellow-avatar.svg rename packages/wow-ui/src/assets/{lottie => lotties}/blueSpinner.json (100%) rename packages/wow-ui/src/assets/{lottie => lotties}/rainbowSpinner.json (100%) create mode 100644 packages/wow-ui/src/components/Avatar/Avatar.stories.tsx create mode 100644 packages/wow-ui/src/components/Avatar/index.tsx rename packages/wow-ui/src/types/{Polymorphic.ts => polymorphic.ts} (74%) diff --git a/.changeset/clever-lizards-tell.md b/.changeset/clever-lizards-tell.md new file mode 100644 index 00000000..ca856a46 --- /dev/null +++ b/.changeset/clever-lizards-tell.md @@ -0,0 +1,6 @@ +--- +"wowds-icons": patch +"wowds-ui": patch +--- + +Avatar 컴포넌트를 추가합니다. diff --git a/packages/wow-icons/src/component/BlueAvatar.tsx b/packages/wow-icons/src/component/BlueAvatar.tsx new file mode 100644 index 00000000..6aee0b4d --- /dev/null +++ b/packages/wow-icons/src/component/BlueAvatar.tsx @@ -0,0 +1,83 @@ +import { forwardRef } from "react"; + +import type { IconProps } from "@/types/Icon.ts"; + +const BlueAvatar = forwardRef( + ( + { + className, + width = "100", + height = "100", + viewBox = "0 0 100 100", + ...rest + }, + ref + ) => { + return ( + + + + + + + + + + + + + + + + + + + ); + } +); + +BlueAvatar.displayName = "BlueAvatar"; +export default BlueAvatar; diff --git a/packages/wow-icons/src/component/GreenAvatar.tsx b/packages/wow-icons/src/component/GreenAvatar.tsx new file mode 100644 index 00000000..b3a465b5 --- /dev/null +++ b/packages/wow-icons/src/component/GreenAvatar.tsx @@ -0,0 +1,87 @@ +import { forwardRef } from "react"; + +import type { IconProps } from "@/types/Icon.ts"; + +const GreenAvatar = forwardRef( + ( + { + className, + width = "100", + height = "100", + viewBox = "0 0 100 100", + ...rest + }, + ref + ) => { + return ( + + + + + + + + + + + + + + + + + + + ); + } +); + +GreenAvatar.displayName = "GreenAvatar"; +export default GreenAvatar; diff --git a/packages/wow-icons/src/component/RedAvatar.tsx b/packages/wow-icons/src/component/RedAvatar.tsx new file mode 100644 index 00000000..542a5ffd --- /dev/null +++ b/packages/wow-icons/src/component/RedAvatar.tsx @@ -0,0 +1,84 @@ +import { forwardRef } from "react"; + +import type { IconProps } from "@/types/Icon.ts"; + +const RedAvatar = forwardRef( + ( + { + className, + width = "100", + height = "100", + viewBox = "0 0 100 100", + ...rest + }, + ref + ) => { + return ( + + + + + + + + + + + + + + + + + + + + + ); + } +); + +RedAvatar.displayName = "RedAvatar"; +export default RedAvatar; diff --git a/packages/wow-icons/src/component/YellowAvatar.tsx b/packages/wow-icons/src/component/YellowAvatar.tsx new file mode 100644 index 00000000..606be0bb --- /dev/null +++ b/packages/wow-icons/src/component/YellowAvatar.tsx @@ -0,0 +1,91 @@ +import { forwardRef } from "react"; + +import type { IconProps } from "@/types/Icon.ts"; + +const YellowAvatar = forwardRef( + ( + { + className, + width = "100", + height = "100", + viewBox = "0 0 100 100", + ...rest + }, + ref + ) => { + return ( + + + + + + + + + + + + + + + + + + + + ); + } +); + +YellowAvatar.displayName = "YellowAvatar"; +export default YellowAvatar; diff --git a/packages/wow-icons/src/component/index.ts b/packages/wow-icons/src/component/index.ts index 11ab8e7f..6e0a57b6 100644 --- a/packages/wow-icons/src/component/index.ts +++ b/packages/wow-icons/src/component/index.ts @@ -1,14 +1,18 @@ +export { default as BlueAvatar } from "./BlueAvatar.tsx"; export { default as Calendar } from "./Calendar.tsx"; export { default as Check } from "./Check.tsx"; export { default as Close } from "./Close.tsx"; export { default as DownArrow } from "./DownArrow.tsx"; export { default as Edit } from "./Edit.tsx"; +export { default as GreenAvatar } from "./GreenAvatar.tsx"; export { default as Help } from "./Help.tsx"; export { default as LeftArrow } from "./LeftArrow.tsx"; export { default as Link } from "./Link.tsx"; export { default as Plus } from "./Plus.tsx"; +export { default as RedAvatar } from "./RedAvatar.tsx"; export { default as Reload } from "./Reload.tsx"; export { default as RightArrow } from "./RightArrow.tsx"; export { default as Search } from "./Search.tsx"; export { default as Trash } from "./Trash.tsx"; export { default as Warn } from "./Warn.tsx"; +export { default as YellowAvatar } from "./YellowAvatar.tsx"; diff --git a/packages/wow-icons/src/svg/blue-avatar.svg b/packages/wow-icons/src/svg/blue-avatar.svg new file mode 100644 index 00000000..49658c71 --- /dev/null +++ b/packages/wow-icons/src/svg/blue-avatar.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/wow-icons/src/svg/green-avatar.svg b/packages/wow-icons/src/svg/green-avatar.svg new file mode 100644 index 00000000..5e0cfaf4 --- /dev/null +++ b/packages/wow-icons/src/svg/green-avatar.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/wow-icons/src/svg/red-avatar.svg b/packages/wow-icons/src/svg/red-avatar.svg new file mode 100644 index 00000000..26bf9bb3 --- /dev/null +++ b/packages/wow-icons/src/svg/red-avatar.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/wow-icons/src/svg/yellow-avatar.svg b/packages/wow-icons/src/svg/yellow-avatar.svg new file mode 100644 index 00000000..0dd1a239 --- /dev/null +++ b/packages/wow-icons/src/svg/yellow-avatar.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/wow-ui/.storybook/main.ts b/packages/wow-ui/.storybook/main.ts index 35682223..e6c80f20 100644 --- a/packages/wow-ui/.storybook/main.ts +++ b/packages/wow-ui/.storybook/main.ts @@ -36,6 +36,28 @@ const config: StorybookConfig = { "@styled-system": path.resolve(__dirname, "../styled-system"), }; } + + if (!config.module || !config.module.rules) { + return config; + } + + config.module.rules = [ + ...config.module.rules.map((rule) => { + if (!rule || rule === "...") { + return rule; + } + + if (rule.test && /svg/.test(String(rule.test))) { + return { ...rule, exclude: /\.svg$/i }; + } + return rule; + }), + { + test: /\.svg$/, + use: ["@svgr/webpack"], + }, + ]; + return config; }, }; diff --git a/packages/wow-ui/package.json b/packages/wow-ui/package.json index 488d13fc..a1278468 100644 --- a/packages/wow-ui/package.json +++ b/packages/wow-ui/package.json @@ -134,6 +134,11 @@ "types": "./dist/components/Box/index.d.ts", "require": "./dist/Box.cjs", "import": "./dist/Box.js" + }, + "./Avatar": { + "types": "./dist/components/Avatar/index.d.ts", + "require": "./dist/Avatar.cjs", + "import": "./dist/Avatar.js" } }, "keywords": [], diff --git a/packages/wow-ui/rollup.config.js b/packages/wow-ui/rollup.config.js index 2113035a..28c0c666 100644 --- a/packages/wow-ui/rollup.config.js +++ b/packages/wow-ui/rollup.config.js @@ -43,6 +43,7 @@ export default { Checkbox: "./src/components/Checkbox", Button: "./src/components/Button", Box: "./src/components/Box", + Avatar: "./src/components/Avatar", }, output: [ { diff --git a/packages/wow-ui/src/assets/lottie/blueSpinner.json b/packages/wow-ui/src/assets/lotties/blueSpinner.json similarity index 100% rename from packages/wow-ui/src/assets/lottie/blueSpinner.json rename to packages/wow-ui/src/assets/lotties/blueSpinner.json diff --git a/packages/wow-ui/src/assets/lottie/rainbowSpinner.json b/packages/wow-ui/src/assets/lotties/rainbowSpinner.json similarity index 100% rename from packages/wow-ui/src/assets/lottie/rainbowSpinner.json rename to packages/wow-ui/src/assets/lotties/rainbowSpinner.json diff --git a/packages/wow-ui/src/components/Avatar/Avatar.stories.tsx b/packages/wow-ui/src/components/Avatar/Avatar.stories.tsx new file mode 100644 index 00000000..bcedd31c --- /dev/null +++ b/packages/wow-ui/src/components/Avatar/Avatar.stories.tsx @@ -0,0 +1,119 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import Avatar from "@/components/Avatar/index.tsx"; + +const meta = { + title: "UI/Avatar", + component: Avatar, + parameters: { + componentSubtitle: "아바타 컴포넌트", + }, + tags: ["autodocs"], + argTypes: { + size: { + control: { type: "radio" }, + options: ["sm", "lg"], + description: "아바타의 크기입니다.", + table: { + defaultValue: { summary: "lg" }, + type: { + summary: "sm | lg", + }, + }, + }, + variant: { + control: { type: "radio" }, + options: ["blue", "green", "yellow", "red"], + description: "아바타의 색상입니다.", + table: { + defaultValue: { summary: "blue" }, + type: { + summary: "blue | green | yellow | red", + }, + }, + }, + ImageComponent: { + control: false, + description: "아바타에 표시할 이미지 컴포넌트입니다.", + table: { + type: { summary: "ElementType" }, + }, + }, + imageUrl: { + control: { type: "text" }, + description: "아바타에 표시할 이미지의 URL입니다.", + table: { + type: { summary: "string" }, + }, + }, + username: { + control: { type: "text" }, + description: "아바타 옆에 표시할 사용자 이름입니다.", + table: { + type: { summary: "string" }, + }, + }, + orientation: { + control: { type: "radio" }, + options: ["left", "right"], + description: + "사용자 이름 레이블의 방향입니다. size가 'sm'인 경우 지정할 수 있습니다.", + table: { + defaultValue: { summary: "right" }, + type: { summary: "left | right" }, + }, + }, + asProp: { + control: false, + description: "렌더링할 HTML 요소나 React 컴포넌트입니다.", + table: { + defaultValue: { summary: "div" }, + type: { summary: "React.ElementType" }, + }, + }, + ref: { + description: "'ref'를 사용하여 컴포넌트에 직접 접근할 수 있습니다.", + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +export const Small: Story = { + args: { + size: "sm", + }, +}; + +export const WithImage: Story = { + args: { + imageUrl: "https://i.ibb.co/2hz6hNP/2024-06-06-11-52-22.png", + }, +}; + +export const LargeWithUsername: Story = { + args: { + username: "김홍익 님", + }, +}; + +export const SmallWithUsernameOrientationLeft: Story = { + args: { + username: "김홍익 님", + size: "sm", + orientation: "left", + }, +}; + +export const SmallWithUsernameOrientationRight: Story = { + args: { + username: "김홍익 님", + size: "sm", + }, +}; diff --git a/packages/wow-ui/src/components/Avatar/index.tsx b/packages/wow-ui/src/components/Avatar/index.tsx new file mode 100644 index 00000000..018c6134 --- /dev/null +++ b/packages/wow-ui/src/components/Avatar/index.tsx @@ -0,0 +1,163 @@ +import { cva } from "@styled-system/css"; +import type { ElementType, PropsWithChildren, ReactNode } from "react"; +import { forwardRef } from "react"; +import { BlueAvatar, GreenAvatar, RedAvatar, YellowAvatar } from "wowds-icons"; + +import type { + PolymorphicComponentProps, + PolymorphicComponentPropsWithRef, + PolymorphicRef, +} from "@/types/polymorphic.ts"; + +/** + * @description Avatar 컴포넌트는 사용자 프로필을 나타내는 컴포넌트입니다. + * + * @param {("sm" | "lg")} [size="lg"] - 아바타의 크기. + * @param {("blue" | "green" | "yellow" | "red")} [variant="blue"] - 아바타의 색상. + * @param {ElementType} [ImageComponent] - 아바타에 표시할 이미지 컴포넌트. + * @param {string} [imageUrl] - 아바타에 표시할 이미지의 URL. + * @param {string} [username] - 아바타 옆에 표시할 사용자 이름. 사용자 이름이 제공되면 레이블로 표시됨. + * @param {("left" | "right")} [orientation="right"] - 사용자 이름 레이블의 방향. size가 'sm'인 경우 지정 가능. + * @param {React.ElementType} [asProp="div"] - 렌더링할 HTML 요소나 React 컴포넌트. 기본값은 "div". + */ + +type AvatarSizeType = "sm" | "lg"; + +export interface _AvatarProps { + size?: AvatarSizeType; + variant?: "blue" | "green" | "yellow" | "red"; + ImageComponent?: ElementType; + imageUrl?: string; + username?: string; + orientation?: "left" | "right"; +} + +type AvatarProps = PolymorphicComponentProps< + T, + _AvatarProps +> & + PropsWithChildren; + +type AvatarComponent = ( + props: PolymorphicComponentPropsWithRef> +) => ReactNode; + +const Avatar: AvatarComponent & { displayName?: string } = forwardRef( + ( + { + asProp, + size = "lg", + variant = "blue", + ImageComponent, + imageUrl, + username, + orientation = "right", + ...rest + }: AvatarProps, + ref?: PolymorphicRef + ) => { + const Component = asProp || "div"; + const AvatarComponent = avatarMap[variant]; + + return ( + + {ImageComponent ? ( + + ) : imageUrl ? ( + avatar + ) : ( + + )} + + + ); + } +); + +Avatar.displayName = "Avatar"; +export default Avatar; + +const avatarMap = { + blue: BlueAvatar, + green: GreenAvatar, + red: RedAvatar, + yellow: YellowAvatar, +}; + +const avatarContainerStyle = cva({ + base: { + height: "fit-content", + width: "fit-content", + display: "flex", + justifyContent: "center", + alignItems: "center", + }, + variants: { + size: { + sm: { + gap: 8, + }, + lg: { + gap: 12, + }, + }, + orientation: { + default: { + flexDirection: "column", + }, + left: { + flexDirection: "row-reverse", + }, + right: { + flexDirection: "row", + }, + }, + }, +}); + +const avatarSizeStyle = cva({ + base: { + borderRadius: "50%", + }, + variants: { + size: { + sm: { + width: 32, + height: 32, + }, + lg: { + width: 100, + height: 100, + }, + }, + }, +}); + +const avatarLabelStyle = cva({ + variants: { + size: { + sm: { + textStyle: "label1", + }, + lg: { + textStyle: "h1", + }, + }, + }, +}); diff --git a/packages/wow-ui/src/components/Spinner/BlueSpinner.tsx b/packages/wow-ui/src/components/Spinner/BlueSpinner.tsx index 354d26b6..c2be21f5 100644 --- a/packages/wow-ui/src/components/Spinner/BlueSpinner.tsx +++ b/packages/wow-ui/src/components/Spinner/BlueSpinner.tsx @@ -2,7 +2,7 @@ import type { LottieComponentProps } from "lottie-react"; import Lottie from "lottie-react"; import type { CSSProperties } from "react"; -import blueSpinner from "@/assets/lottie/blueSpinner.json"; +import blueSpinner from "@/assets/lotties/blueSpinner.json"; /** * @description 블루 스피너 컴포넌트입니다. diff --git a/packages/wow-ui/src/components/Spinner/RainbowSpinner.tsx b/packages/wow-ui/src/components/Spinner/RainbowSpinner.tsx index 5020d788..e1731fca 100644 --- a/packages/wow-ui/src/components/Spinner/RainbowSpinner.tsx +++ b/packages/wow-ui/src/components/Spinner/RainbowSpinner.tsx @@ -2,7 +2,7 @@ import type { LottieComponentProps } from "lottie-react"; import Lottie from "lottie-react"; import type { CSSProperties } from "react"; -import rainbowSpinner from "@/assets/lottie/rainbowSpinner.json"; +import rainbowSpinner from "@/assets/lotties/rainbowSpinner.json"; /** * @description 레인보우 스피너 컴포넌트입니다. Lottie 애니메이션을 사용하여 스피너를 표시합니다. diff --git a/packages/wow-ui/src/types/index.ts b/packages/wow-ui/src/types/index.ts index 2d701f74..e904c893 100644 --- a/packages/wow-ui/src/types/index.ts +++ b/packages/wow-ui/src/types/index.ts @@ -3,9 +3,9 @@ export type { ButtonElementType, MenuButtonProps, ToggleButtonProps, -} from "./Button"; +} from "./button.ts"; export type { PolymorphicComponentProps, PolymorphicComponentPropsWithRef, PolymorphicRef, -} from "./Polymorphic"; +} from "./polymorphic.ts"; diff --git a/packages/wow-ui/src/types/Polymorphic.ts b/packages/wow-ui/src/types/polymorphic.ts similarity index 74% rename from packages/wow-ui/src/types/Polymorphic.ts rename to packages/wow-ui/src/types/polymorphic.ts index 72c7681c..f9dedd89 100644 --- a/packages/wow-ui/src/types/Polymorphic.ts +++ b/packages/wow-ui/src/types/polymorphic.ts @@ -13,12 +13,12 @@ export type PolymorphicRef = export type PolymorphicComponentPropsWithRef< C extends ElementType, - Props = {}, -> = Props & { ref?: PolymorphicRef }; + ComponentProps = {}, +> = ComponentProps & { ref?: PolymorphicRef }; export type PolymorphicComponentProps< T extends ElementType, - Props = {}, + ComponentProps = {}, > = AsProps & ComponentPropsWithoutRef & - PolymorphicComponentPropsWithRef; + PolymorphicComponentPropsWithRef;