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;