= {
+ render: (args) => {
+ return (
+
+
+ 1 단계
+
+
+ 2 단계
+
+
+ 3 단계
+
+
+ 4 단계
+
+
+ 5 단계
+
+
+ 6 단계
+
+
+ );
+ },
+ argTypes: {
+ children: {
+ control: false,
+ },
+ level: {
+ control: false,
+ },
+ },
+};
+
+export const Contrasts: StoryObj = {
+ render: (args) => {
+ return (
+
+
+ 낮은 명암비
+
+ 높은 명암비
+
+ );
+ },
+ argTypes: {
+ children: {
+ control: false,
+ },
+ muted: {
+ control: false,
+ },
+ },
+};
diff --git a/src/components/Heading/Heading.test.tsx b/src/components/Heading/Heading.test.tsx
new file mode 100644
index 0000000..4e0f57e
--- /dev/null
+++ b/src/components/Heading/Heading.test.tsx
@@ -0,0 +1,55 @@
+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 "./Heading.stories";
+
+const { Basic, Contrasts } = composeStories(stories);
+
+test("renders the heading with the correct text content", () => {
+ render(제목);
+
+ expect(screen.getByRole("heading")).toHaveTextContent("제목");
+});
+
+test.each([1, 2, 3, 4, 5, 6] as const)(
+ "renders the correct HTML heading element for level %i",
+ (level) => {
+ render();
+
+ expect(screen.getByRole("heading", { level })).toBeInTheDocument();
+ }
+);
+
+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.getByRole("heading")).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.getByRole("heading")).toHaveClass(`fs_${size}`);
+});
+
+test("applies the correct color for low and high contrast", () => {
+ render();
+
+ expect(screen.getByRole("heading", { name: "낮은 명암비" })).toHaveClass(
+ "c_text.muted"
+ );
+
+ expect(screen.getByRole("heading", { name: "높은 명암비" })).toHaveClass(
+ "c_text"
+ );
+});
diff --git a/src/components/Heading/Heading.tsx b/src/components/Heading/Heading.tsx
new file mode 100644
index 0000000..b7b3a94
--- /dev/null
+++ b/src/components/Heading/Heading.tsx
@@ -0,0 +1,73 @@
+import type { ReactNode, HTMLAttributes } from "react";
+import { css, cva } from "../../../styled-system/css";
+import type { FontSize, FontWeight } from "../../tokens/typography";
+
+type Level = 1 | 2 | 3 | 4 | 5 | 6;
+
+export interface HeadingProps extends HTMLAttributes {
+ /** 텍스트 */
+ children: ReactNode;
+ /** 단계 */
+ level: Level;
+ /** 크기 */
+ size?: FontSize;
+ /** 굵기 */
+ weight?: FontWeight;
+ /** 명암비 */
+ muted?: boolean;
+}
+
+/**
+ * - `level` 속성을 통해서 ``, ``, ``, ``, ``, `` 요소 중 하나를 선택할 수 있습니다.
+ * - `level` 속성은 단계 별 기본 텍스트 스타일을 제공합니다.
+ * - `size` 속성과 `weight` 속성을 통해서 기본 스타일을 변경할 수 있습니다.
+ * - `muted` 속성을 주시면 글자색이 옅어집니다. 명암비가 낮아지므로 접근성 측면에서 주의해서 사용하세요.
+ */
+export const Heading = ({
+ children,
+ level,
+ size,
+ weight,
+ muted = false,
+ ...rest
+}: HeadingProps) => {
+ if (!level) {
+ throw new Error(
+ "The level prop is required and you can cause accessibility issues."
+ );
+ }
+
+ const Tag = `h${level}` as const;
+
+ return (
+
+ {children}
+
+ );
+};
+
+const styles = cva({
+ variants: {
+ level: {
+ 1: { textStyle: "4xl" },
+ 2: { textStyle: "3xl" },
+ 3: { textStyle: "2xl" },
+ 4: { textStyle: "xl" },
+ 5: { textStyle: "lg" },
+ 6: { textStyle: "md" },
+ },
+ muted: {
+ true: { color: "text.muted" },
+ false: { color: "text" },
+ },
+ },
+});
diff --git a/src/components/Heading/index.tsx b/src/components/Heading/index.tsx
new file mode 100644
index 0000000..0776e15
--- /dev/null
+++ b/src/components/Heading/index.tsx
@@ -0,0 +1 @@
+export { Heading } from "./Heading";
diff --git a/src/index.css b/src/index.css
index 0f6cb42..14b5d9c 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,2 +1,2 @@
@layer reset, base, tokens, recipes, utilities;
-@import url(//spoqa.github.io/spoqa-han-sans/css/SpoqaHanSansNeo.css);
+@import url(https://spoqa.github.io/spoqa-han-sans/css/SpoqaHanSansNeo.css);
diff --git a/src/tokens/colors.ts b/src/tokens/colors.ts
index 200da7c..2aff6eb 100644
--- a/src/tokens/colors.ts
+++ b/src/tokens/colors.ts
@@ -111,7 +111,7 @@ export const semanticColors: SemanticTokens["colors"] = {
},
},
text: {
- DEFAULT: {
+ muted: {
DEFAULT: {
value: { base: "{colors.gray.11}", _dark: "{colors.grayDark.11}" },
},
@@ -125,7 +125,7 @@ export const semanticColors: SemanticTokens["colors"] = {
value: { base: "{colors.yellow.11}", _dark: "{colors.yellowDark.11}" },
},
},
- contrast: {
+ DEFAULT: {
DEFAULT: {
value: { base: "{colors.gray.12}", _dark: "{colors.grayDark.12}" },
},
diff --git a/src/tokens/typography.ts b/src/tokens/typography.ts
index 91c171d..f547789 100644
--- a/src/tokens/typography.ts
+++ b/src/tokens/typography.ts
@@ -1,6 +1,4 @@
-import type { TextStyles, Tokens } from "@pandacss/types";
-
-export const textStyles: TextStyles = {
+export const textStyles = {
xs: {
value: {
fontSize: "0.75rem",
@@ -81,12 +79,12 @@ export const textStyles: TextStyles = {
},
};
-export const fonts: Tokens["fonts"] = {
+export const fonts = {
sans: { value: '"Spoqa Han Sans Neo", "Noto Sans KR", sans-serif' },
// TODO customize serif and mono font styles when needed
};
-export const fontWeights: Tokens["fontWeights"] = {
+export const fontWeights = {
thin: { value: "100" },
light: { value: "300" },
normal: { value: "400" },
@@ -94,7 +92,9 @@ export const fontWeights: Tokens["fontWeights"] = {
bold: { value: "700" },
};
-export const fontSizes: Tokens["fontSizes"] = {
+export type FontWeight = keyof typeof fontWeights;
+
+export const fontSizes = {
"2xs": { value: "0.5rem" },
xs: { value: "0.75rem" },
sm: { value: "0.875rem" },
@@ -111,7 +111,9 @@ export const fontSizes: Tokens["fontSizes"] = {
"9xl": { value: "8rem" },
};
-export const letterSpacings: Tokens["letterSpacings"] = {
+export type FontSize = keyof typeof fontSizes;
+
+export const letterSpacings = {
tighter: { value: "-0.05em" },
tight: { value: "-0.025em" },
normal: { value: "0em" },
@@ -120,7 +122,7 @@ export const letterSpacings: Tokens["letterSpacings"] = {
widest: { value: "0.1em" },
};
-export const lineHeights: Tokens["lineHeights"] = {
+export const lineHeights = {
none: { value: "1" },
tight: { value: "1.25" },
snug: { value: "1.375" },
diff --git a/tsconfig.app.json b/tsconfig.app.json
index c7bcde8..07afee5 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -18,7 +18,8 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
+ "noFallthroughCasesInSwitch": true,
+ "verbatimModuleSyntax": true
},
"include": ["src", "styled-system"]
}
diff --git a/vite.config.ts b/vite.config.ts
index 3e9a2f0..741c3ad 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -9,4 +9,7 @@ export default defineConfig({
environment: "happy-dom",
setupFiles: ["./src/setupTests.tsx"],
},
+ optimizeDeps: {
+ exclude: ["node_modules/.cache/storybook"],
+ },
});