diff --git a/src/App.tsx b/src/App.tsx index 330418d..0bf4866 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import logo from "/logo.svg"; import { css } from "../styled-system/css"; import { Button } from "./components/Button"; import { Heading } from "./components/Heading"; +import { Text } from "./components/Text"; function App() { return ( @@ -56,7 +57,9 @@ function App() { - + ); } diff --git a/src/components/Heading/Heading.stories.tsx b/src/components/Heading/Heading.stories.tsx index 211e369..2f5fb0b 100644 --- a/src/components/Heading/Heading.stories.tsx +++ b/src/components/Heading/Heading.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { vstack } from "../../../styled-system/patterns"; import { Heading } from "./Heading"; -const meta = { +export default { component: Heading, parameters: { layout: "centered", @@ -13,11 +13,9 @@ const meta = { }, } satisfies Meta; -export default meta; +export const Basic: StoryObj = {}; -export const Basic: StoryObj = {}; - -export const Levels: StoryObj = { +export const Levels: StoryObj = { render: (args) => { return (
@@ -52,7 +50,7 @@ export const Levels: StoryObj = { }, }; -export const Contrasts: StoryObj = { +export const Contrasts: StoryObj = { render: (args) => { return (
diff --git a/src/components/Heading/Heading.tsx b/src/components/Heading/Heading.tsx index b7b3a94..f8f28ac 100644 --- a/src/components/Heading/Heading.tsx +++ b/src/components/Heading/Heading.tsx @@ -13,7 +13,7 @@ export interface HeadingProps extends HTMLAttributes { size?: FontSize; /** 굵기 */ weight?: FontWeight; - /** 명암비 */ + /** 명암비 낮출지 */ muted?: boolean; } diff --git a/src/components/Text/Text.stories.tsx b/src/components/Text/Text.stories.tsx new file mode 100644 index 0000000..5f19d4e --- /dev/null +++ b/src/components/Text/Text.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { vstack } from "../../../styled-system/patterns"; +import { Text } from "./Text"; + +export default { + component: Text, + parameters: { + layout: "centered", + }, + args: { + children: "본문", + }, +} satisfies Meta; + +export const Basic: StoryObj = {}; + +export const Tones: StoryObj = { + render: (args) => { + return ( +
+ + 중립 색조 + + + 강조 색조 + + + 위험 색조 + + + 경고 색조 + +
+ ); + }, + argTypes: { + children: { + control: false, + }, + tone: { + control: false, + }, + }, +}; + +export const Contrasts: StoryObj = { + render: (args) => { + return ( +
+ + 낮은 명암비 + + 높은 명암비 +
+ ); + }, + argTypes: { + children: { + control: false, + }, + muted: { + control: false, + }, + }, +}; diff --git a/src/components/Text/Text.test.tsx b/src/components/Text/Text.test.tsx new file mode 100644 index 0000000..253cf9e --- /dev/null +++ b/src/components/Text/Text.test.tsx @@ -0,0 +1,54 @@ +import { faker } from "@faker-js/faker"; +import { composeStories } from "@storybook/react"; +import { render, screen } from "@testing-library/react"; +import { expect, test } from "vitest"; +import { fontSizes, fontWeights } from "../../tokens/typography"; +import * as stories from "./Text.stories"; + +const { Basic, Tones, Contrasts } = composeStories(stories); + +test("renders the heading with the correct text content", () => { + render(테스트); + + expect(screen.getByText("테스트")); +}); + +test("applies the correct font weight class based on the weight prop", () => { + const weight = faker.helpers.arrayElement( + Object.keys(fontWeights) + ) as keyof typeof fontWeights; + + render(); + + expect(screen.getByText("본문")).toHaveClass(`fw_${weight}`); +}); + +test("applies the correct font size class based on the size prop", () => { + const size = faker.helpers.arrayElement( + Object.keys(fontSizes) + ) as keyof typeof fontSizes; + + render(); + + expect(screen.getByText("본문")).toHaveClass(`fs_${size}`); +}); + +test("applies the correct color based on the tone", () => { + render(); + + expect(screen.getByText("중립 색조")).toHaveClass("c_text"); + + expect(screen.getByText("강조 색조")).toHaveClass("c_text.accent"); + + expect(screen.getByText("위험 색조")).toHaveClass("c_text.danger"); + + expect(screen.getByText("경고 색조")).toHaveClass("c_text.warning"); +}); + +test("applies the correct color for low and high contrast", () => { + render(); + + expect(screen.getByText("낮은 명암비")).toHaveClass("c_text.muted"); + + expect(screen.getByText("높은 명암비")).toHaveClass("c_text"); +}); diff --git a/src/components/Text/Text.tsx b/src/components/Text/Text.tsx new file mode 100644 index 0000000..84112c4 --- /dev/null +++ b/src/components/Text/Text.tsx @@ -0,0 +1,93 @@ +import type { ReactNode, HTMLAttributes } from "react"; +import { css, cva } from "../../../styled-system/css"; +import type { Tone } from "../../tokens/colors"; +import type { FontSize, FontWeight } from "../../tokens/typography"; + +export interface TextProps extends HTMLAttributes { + /** 텍스트 */ + children: ReactNode; + /** HTML 태그 */ + as?: "span" | "div" | "p" | "strong" | "em" | "small"; + /** 색조 */ + tone?: Tone; + /** 크기 */ + size?: FontSize; + /** 굵기 */ + weight?: FontWeight; + /** 명암비 낮출지 */ + muted?: boolean; +} + +/** + * - `as` 속성으로 어떤 HTML 태그를 사용할지 지정할 수 있습니다. + * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요. + */ +export const Text = ({ + children, + as: Tag = "span", + tone = "neutral", + size, + weight, + muted = false, + ...rest +}: TextProps) => { + return ( + + {children} + + ); +}; + +const styles = 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/Text/index.tsx b/src/components/Text/index.tsx new file mode 100644 index 0000000..7afe56f --- /dev/null +++ b/src/components/Text/index.tsx @@ -0,0 +1 @@ +export { Text } from "./Text"; diff --git a/src/tokens/colors.ts b/src/tokens/colors.ts index 2aff6eb..6ea04a2 100644 --- a/src/tokens/colors.ts +++ b/src/tokens/colors.ts @@ -1,5 +1,7 @@ import type { Tokens, SemanticTokens } from "@pandacss/types"; +export type Tone = "neutral" | "accent" | "danger" | "warning"; + export const semanticColors: SemanticTokens["colors"] = { current: { value: "currentColor" }, transparent: { value: "rgb(0 0 0 / 0)" },