diff --git a/src/assets/X.svg b/src/assets/X.svg deleted file mode 100644 index 26be660..0000000 --- a/src/assets/X.svg +++ /dev/null @@ -1 +0,0 @@ -X \ No newline at end of file diff --git a/src/components/Icon/Icon.stories.tsx b/src/components/Icon/Icon.stories.tsx new file mode 100644 index 0000000..0cd5bd1 --- /dev/null +++ b/src/components/Icon/Icon.stories.tsx @@ -0,0 +1,106 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { vstack } from "../../../styled-system/patterns"; +import { Heading } from "../Heading/Heading"; +import { Text } from "../Text/Text"; +import { Icon } from "./Icon"; + +export default { + component: Icon, + parameters: { + layout: "centered", + }, + args: { + name: "user", + }, +} satisfies Meta; + +export const Basic: StoryObj = { + args: { + tone: "accent", + muted: true, + size: "xl", + }, +}; + +export const Sizes: StoryObj = { + render: (args) => { + return ( +
+ + + + + +
+ ); + }, + argTypes: { + size: { + control: false, + }, + }, + args: { + tone: "accent", + muted: true, + }, +}; + +export const Tones: StoryObj = { + render: (args) => { + return ( +
+ + + + +
+ ); + }, + argTypes: { + tone: { + control: false, + }, + }, + args: { + muted: true, + }, +}; + +export const Contrasts: StoryObj = { + render: (args) => { + return ( +
+ + 낮은 명암비 + + + 높은 명암비 + +
+ ); + }, + argTypes: { + name: { + control: false, + }, + muted: { + control: false, + }, + }, +}; + +export const WithHeading: StoryObj = { + render: (args) => { + return ( + + + 프로필 + + ); + }, + argTypes: { + name: { + control: false, + }, + }, +}; diff --git a/src/components/Icon/Icon.test.tsx b/src/components/Icon/Icon.test.tsx new file mode 100644 index 0000000..4e6c210 --- /dev/null +++ b/src/components/Icon/Icon.test.tsx @@ -0,0 +1,44 @@ +import { composeStories } from "@storybook/react"; +import { render } from "@testing-library/react"; +import { expect, test } from "vitest"; +import * as stories from "./Icon.stories"; + +const { Basic } = composeStories(stories); + +test("renders an svg element", () => { + const { container } = render(); + + expect(container.querySelector("svg")).toBeInTheDocument(); +}); + +test.each([ + ["xs", "w_1em h_1em"], + ["sm", "w_1.25em h_1.25em"], + ["md", "w_1.5em h_1.5em"], + ["lg", "w_1.875em h_1.875em"], + ["xl", "w_2.25em h_2.25em"], +] as const)('applies the correct class for size="%s"', (size, className) => { + const { container } = render(); + + expect(container.querySelector("svg")).toHaveClass(className); +}); + +test.each([ + ["neutral", "c_text"], + ["accent", "c_text.accent"], + ["danger", "c_text.danger"], + ["warning", "c_text.warning"], +] as const)('applies the correct class for tone="%s"', (tone, className) => { + const { container } = render(); + + expect(container.querySelector("svg")).toHaveClass(className); +}); + +test.each([ + [false, "c_text"], + [true, "c_text.muted"], +] as const)("applies the correct class for muted={%s}", (muted, className) => { + const { container } = render(); + + expect(container.querySelector("svg")).toHaveClass(className); +}); diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx new file mode 100644 index 0000000..364a0ba --- /dev/null +++ b/src/components/Icon/Icon.tsx @@ -0,0 +1,116 @@ +import { css, cva } from "../../../styled-system/css"; +import type { Tone } from "../../tokens/colors"; +import { type IconName, icons } from "../../tokens/iconography"; +export interface IconProps { + /** 이름 */ + name: IconName; + /** 색조 */ + tone?: Tone; + /** 크기 */ + size?: "xs" | "sm" | "md" | "lg" | "xl"; + /** 명암비 낮출지 */ + muted?: boolean; +} + +/** + * - `name` 속성으로 어떤 모양의 아이콘을 사용할지 지정할 수 있습니다. + * - 아이콘의 기본 크기는 부모 요소에서 설정한 글자 크기의 1.5배이며, `size` 속성을 통해서 크기를 변경할 수 있습니다. + * - 아이콘의 기본 색상은 부모 요소에서 설정한 글자 색상과 동일하며, `tone` 속성과 `muted` 속성을 통해서 색상을 변경할 수 있습니다. + */ +export const Icon = ({ + name, + size, + tone, + muted = false, + ...rest +}: IconProps) => { + const Tag = icons[name]; + + return ( + + ); +}; + +const sizeStyles = cva({ + variants: { + size: { + xs: { + width: "1em", + height: "1em", + }, + sm: { + width: "1.25em", + height: "1.25em", + }, + md: { + width: "1.5em", + height: "1.5em", + }, + lg: { + width: "1.875em", + height: "1.875em", + }, + xl: { + width: "2.25em", + height: "2.25em", + }, + }, + }, + defaultVariants: { + size: "md", + }, +}); + +const colorStyles = cva({ + compoundVariants: [ + { + muted: false, + tone: "neutral", + css: { color: "text" }, + }, + { + muted: false, + tone: "accent", + css: { color: "text.accent" }, + }, + { + muted: false, + tone: "danger", + css: { color: "text.danger" }, + }, + { + muted: false, + tone: "warning", + css: { color: "text.warning" }, + }, + { + muted: true, + tone: "neutral", + css: { color: "text.muted" }, + }, + { + muted: true, + tone: "accent", + css: { color: "text.muted.accent" }, + }, + { + muted: true, + tone: "danger", + css: { color: "text.muted.danger" }, + }, + { + muted: true, + tone: "warning", + css: { color: "text.muted.warning" }, + }, + ], +}); diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx new file mode 100644 index 0000000..8968625 --- /dev/null +++ b/src/components/Icon/index.tsx @@ -0,0 +1 @@ +export { Icon as Text } from "./Icon"; diff --git a/src/tokens/iconography.tsx b/src/tokens/iconography.tsx index 75f5ff8..4dae8c5 100644 --- a/src/tokens/iconography.tsx +++ b/src/tokens/iconography.tsx @@ -1,4 +1,9 @@ import { + Check, + ChevronDown, + ChevronLeft, + ChevronRight, + CircleAlert, Clock, Info, MessageCircle, @@ -6,14 +11,15 @@ import { Moon, Search, Sun, + Star, User, + X, } from "lucide-react"; import type { FunctionComponent, ComponentProps, SVGProps } from "react"; import Discord from "../assets/Discord.svg?react"; import GitHub from "../assets/GitHub.svg?react"; import LinkedIn from "../assets/LinkedIn.svg?react"; import Medium from "../assets/Medium.svg?react"; -import X from "../assets/X.svg?react"; import YouTube from "../assets/YouTube.svg?react"; function createBrandIcon(Icon: FunctionComponent>) { @@ -23,6 +29,11 @@ function createBrandIcon(Icon: FunctionComponent>) { } export const icons = { + check: Check, + chevronDown: ChevronDown, + chevronLeft: ChevronLeft, + chevronRight: ChevronRight, + circleAlert: CircleAlert, clock: Clock, info: Info, chat: MessageCircle, @@ -30,12 +41,13 @@ export const icons = { moon: Moon, search: Search, sun: Sun, + star: Star, user: User, + x: X, Discord: createBrandIcon(Discord), GitHub: createBrandIcon(GitHub), LinkedIn: createBrandIcon(LinkedIn), Medium: createBrandIcon(Medium), - X: createBrandIcon(X), YouTube: createBrandIcon(YouTube), };