Skip to content

Commit

Permalink
Merge pull request #40 from DaleStudy/37
Browse files Browse the repository at this point in the history
Icon 컴포넌트
  • Loading branch information
DaleSeo authored Feb 4, 2025
2 parents 0f64b90 + 6e88817 commit fe0d235
Show file tree
Hide file tree
Showing 17 changed files with 399 additions and 9 deletions.
58 changes: 52 additions & 6 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"axe-playwright": "^2.0.3",
"chromatic": "^11.25.1",
"lucide-react": "^0.474.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
Expand Down Expand Up @@ -52,6 +53,7 @@
"typescript": "^5.7.3",
"typescript-eslint": "^8.22.0",
"vite": "^6.0.11",
"vite-plugin-svgr": "^4.3.0",
"vitest": "^3.0.4"
},
"eslintConfig": {
Expand Down
1 change: 1 addition & 0 deletions src/assets/Discord.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/GitHub.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/LinkedIn.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/Medium.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/YouTube.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/assets/react.svg

This file was deleted.

106 changes: 106 additions & 0 deletions src/components/Icon/Icon.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Icon>;

export const Basic: StoryObj<typeof Icon> = {
args: {
tone: "accent",
muted: true,
size: "xl",
},
};

export const Sizes: StoryObj<typeof Icon> = {
render: (args) => {
return (
<div className={vstack({ gap: "6" })}>
<Icon {...args} size="xs" />
<Icon {...args} size="sm" />
<Icon {...args} size="md" />
<Icon {...args} size="lg" />
<Icon {...args} size="xl" />
</div>
);
},
argTypes: {
size: {
control: false,
},
},
args: {
tone: "accent",
muted: true,
},
};

export const Tones: StoryObj<typeof Icon> = {
render: (args) => {
return (
<div className={vstack({ gap: "6" })}>
<Icon {...args} tone="neutral" />
<Icon {...args} tone="accent" />
<Icon {...args} tone="danger" />
<Icon {...args} tone="warning" />
</div>
);
},
argTypes: {
tone: {
control: false,
},
},
args: {
muted: true,
},
};

export const Contrasts: StoryObj<typeof Icon> = {
render: (args) => {
return (
<div className={vstack({ gap: "6" })}>
<Text {...args} muted>
낮은 <Icon name="moon" /> 명암비
</Text>
<Text {...args}>
높은 <Icon name="sun" /> 명암비
</Text>
</div>
);
},
argTypes: {
name: {
control: false,
},
muted: {
control: false,
},
},
};

export const WithHeading: StoryObj<typeof Icon> = {
render: (args) => {
return (
<Heading level={2}>
<Icon {...args} name="user" />
프로필
</Heading>
);
},
argTypes: {
name: {
control: false,
},
},
};
44 changes: 44 additions & 0 deletions src/components/Icon/Icon.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Basic />);

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(<Basic size={size} />);

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(<Basic tone={tone} muted={false} />);

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(<Basic tone="neutral" muted={muted} />);

expect(container.querySelector("svg")).toHaveClass(className);
});
116 changes: 116 additions & 0 deletions src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Tag
className={css(
sizeStyles.raw({ size }),
colorStyles.raw({ tone, muted }),
css.raw({
display: "inline-block",
})
)}
{...rest}
/>
);
};

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" },
},
],
});
1 change: 1 addition & 0 deletions src/components/Icon/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Icon as Text } from "./Icon";
15 changes: 15 additions & 0 deletions src/tokens/iconography.mdx
Original file line number Diff line number Diff line change
@@ -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/)의 아이콘을 선별적으로 사용하고 있습니다.
> 번들 크기를 최적화하기 위해서 아이콘은 필요할 때 마다 요청을 받아서 추가됩니다.
<IconGallery>
{Object.entries(icons).map(([name, Icon]) => (
<IconItem name={name}>
<Icon />
</IconItem>
))}
</IconGallery>
54 changes: 54 additions & 0 deletions src/tokens/iconography.tsx
Original file line number Diff line number Diff line change
@@ -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<SVGProps<SVGSVGElement>>) {
return (args: ComponentProps<typeof Icon>) => (
<Icon {...args} fill="currentColor" />
);
}

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;
2 changes: 1 addition & 1 deletion src/tokens/typography.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Typeset } from "@storybook/blocks";
import { fonts, fontWeights, fontSizes } from "./typography.ts";
import { fonts, fontWeights, fontSizes } from "./typography";

# Typography

Expand Down
1 change: 1 addition & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
3 changes: 2 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/// <reference types="vitest" />
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"],
Expand Down

0 comments on commit fe0d235

Please sign in to comment.