= {
+ 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.mdx b/src/tokens/iconography.mdx
new file mode 100644
index 0000000..97a3bc8
--- /dev/null
+++ b/src/tokens/iconography.mdx
@@ -0,0 +1,15 @@
+import { IconGallery, IconItem } from "@storybook/blocks";
+import { icons } from "./iconography";
+
+# Iconography
+
+> 달레 UI는 [Lucide](https://lucide.dev/)와 [Simple Icons](https://simpleicons.org/)의 아이콘을 선별적으로 사용하고 있습니다.
+> 번들 크기를 최적화하기 위해서 아이콘은 필요할 때 마다 요청을 받아서 추가됩니다.
+
+
+ {Object.entries(icons).map(([name, Icon]) => (
+
+
+
+ ))}
+
diff --git a/src/tokens/iconography.tsx b/src/tokens/iconography.tsx
new file mode 100644
index 0000000..4dae8c5
--- /dev/null
+++ b/src/tokens/iconography.tsx
@@ -0,0 +1,54 @@
+import {
+ Check,
+ ChevronDown,
+ ChevronLeft,
+ ChevronRight,
+ CircleAlert,
+ Clock,
+ Info,
+ MessageCircle,
+ Menu,
+ 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 YouTube from "../assets/YouTube.svg?react";
+
+function createBrandIcon(Icon: FunctionComponent>) {
+ return (args: ComponentProps) => (
+
+ );
+}
+
+export const icons = {
+ check: Check,
+ chevronDown: ChevronDown,
+ chevronLeft: ChevronLeft,
+ chevronRight: ChevronRight,
+ circleAlert: CircleAlert,
+ clock: Clock,
+ info: Info,
+ chat: MessageCircle,
+ menu: Menu,
+ moon: Moon,
+ search: Search,
+ sun: Sun,
+ star: Star,
+ user: User,
+ x: X,
+ Discord: createBrandIcon(Discord),
+ GitHub: createBrandIcon(GitHub),
+ LinkedIn: createBrandIcon(LinkedIn),
+ Medium: createBrandIcon(Medium),
+ YouTube: createBrandIcon(YouTube),
+};
+
+export type IconName = keyof typeof icons;
diff --git a/src/tokens/typography.mdx b/src/tokens/typography.mdx
index 8a2556c..3ea46d0 100644
--- a/src/tokens/typography.mdx
+++ b/src/tokens/typography.mdx
@@ -1,5 +1,5 @@
import { Typeset } from "@storybook/blocks";
-import { fonts, fontWeights, fontSizes } from "./typography.ts";
+import { fonts, fontWeights, fontSizes } from "./typography";
# Typography
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 11f02fe..b1f45c7 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1 +1,2 @@
///
+///
diff --git a/vite.config.ts b/vite.config.ts
index 741c3ad..d809f0f 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,10 +1,11 @@
///
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
+import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), svgr()],
test: {
environment: "happy-dom",
setupFiles: ["./src/setupTests.tsx"],