Skip to content

Commit

Permalink
feat: implement heading component
Browse files Browse the repository at this point in the history
  • Loading branch information
DaleSeo committed Jan 7, 2025
1 parent 010e541 commit c27c441
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 29 deletions.
8 changes: 6 additions & 2 deletions panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@ export default defineConfig({
tokens: {
colors,
fonts,
fontWeights,
fontSizes,
fontWeights: Object.fromEntries(
Object.entries(fontWeights).map(([key, value]) => [key, { value }])
),
fontSizes: Object.fromEntries(
Object.entries(fontSizes).map(([key, value]) => [key, { value }])
),
letterSpacings,
lineHeights,
},
Expand Down
66 changes: 66 additions & 0 deletions src/components/Heading/Heading.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Meta, StoryObj } from "@storybook/react";
import { vstack } from "../../../styled-system/patterns";
import { fontSizes } from "../../tokens/typography";
import { Heading } from "./Heading";

const meta = {
component: Heading,
parameters: {
layout: "centered",
},
args: {
children: "제목",
level: 2,
},
} satisfies Meta<typeof Heading>;

export default meta;

export const Basic: StoryObj<typeof meta> = {};

export const Levels: StoryObj<typeof meta> = {
render: (args) => {
return (
<div className={vstack({ gap: "6" })}>
<Heading {...args} level={1}>
1 단계 제목
</Heading>
<Heading {...args} level={2}>
2 단계 제목
</Heading>
<Heading {...args} level={3}>
3 단계 제목
</Heading>
<Heading {...args} level={4}>
4 단계 제목
</Heading>
<Heading {...args} level={5}>
5 단계 제목
</Heading>
<Heading {...args} level={6}>
6 단계 제목
</Heading>
</div>
);
},
parameters: {
controls: { disable: true },
},
};

export const Sizes: StoryObj<typeof meta> = {
render: (args) => {
return (
<div className={vstack({ gap: "1" })}>
{Object.keys(fontSizes).map((size) => (
<Heading {...args} size={size as keyof typeof fontSizes}>
{size} 제목
</Heading>
))}
</div>
);
},
parameters: {
controls: { disable: true },
},
};
20 changes: 20 additions & 0 deletions src/components/Heading/Heading.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { composeStories } from "@storybook/react";
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
import * as stories from "./Heading.stories";

const { Basic } = composeStories(stories);

test("renders the text content", () => {
render(<Basic>제목</Basic>);

const heading = screen.getByRole("heading");

expect(heading).toHaveTextContent("제목");
});

test.each([1, 2, 3, 4, 5, 6] as const)("use the correct level %s", (level) => {
render(<Basic level={level} />);

expect(screen.getByRole("heading", { level })).toBeInTheDocument();
});
50 changes: 50 additions & 0 deletions src/components/Heading/Heading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { cva } from "../../../styled-system/css";
import { token } from "../../../styled-system/tokens";
import { fontSizes, fontWeights } from "../../tokens/typography";

export interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
/** 텍스트 */
children: React.ReactNode | string;
/** 단계 */
level?: 1 | 2 | 3 | 4 | 5 | 6;
/** 크기 */
size?: keyof typeof fontSizes;
/** 굵기 */
weight?: keyof typeof fontWeights;
/** 명암비 */
// contrast?: "low" | "high";
}

export const Heading = ({
children,
level,
size = "3xl",
weight = "bold",
// contrast = "low",
...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 (
<Tag className={style({ size, weight })} {...rest}>
{children}
</Tag>
);
};

const style = cva({
variants: {
size: Object.fromEntries(
Object.keys(fontSizes).map((key) => [key, { fontSize: key }])
),
weight: Object.fromEntries(
Object.keys(fontWeights).map((key) => [key, { fontWeight: key }])
),
},
});
2 changes: 1 addition & 1 deletion src/index.css
Original file line number Diff line number Diff line change
@@ -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);
4 changes: 2 additions & 2 deletions src/tokens/typography.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { fonts, fontWeights, fontSizes } from "./typography.ts";

<p>
{Object.entries(fontSizes)
.map(([name, { value }]) => `${name}(${value})`)
.map(([name, value]) => `${name}(${value})`)
.join(", ")}
</p>

Expand All @@ -22,7 +22,7 @@ import { fonts, fontWeights, fontSizes } from "./typography.ts";

## 글꼴 굵기

{Object.entries(fontWeights).map(([name, {value}]) => (
{Object.entries(fontWeights).map(([name, value]) => (

<>
<h3>
Expand Down
50 changes: 27 additions & 23 deletions src/tokens/typography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,30 +86,34 @@ export const fonts: Tokens["fonts"] = {
// TODO customize serif and mono font styles when needed
};

export const fontWeights: Tokens["fontWeights"] = {
thin: { value: "100" },
light: { value: "300" },
normal: { value: "400" },
medium: { value: "500" },
bold: { value: "700" },
};
export const fontWeights = {
thin: "100",
light: "300",
normal: "400",
medium: "500",
bold: "700",
} as const;

export const fontSizes: Tokens["fontSizes"] = {
"2xs": { value: "0.5rem" },
xs: { value: "0.75rem" },
sm: { value: "0.875rem" },
md: { value: "1rem" },
lg: { value: "1.125rem" },
xl: { value: "1.25rem" },
"2xl": { value: "1.5rem" },
"3xl": { value: "1.875rem" },
"4xl": { value: "2.25rem" },
"5xl": { value: "3rem" },
"6xl": { value: "3.75rem" },
"7xl": { value: "4.5rem" },
"8xl": { value: "6rem" },
"9xl": { value: "8rem" },
};
export type FontWeight = keyof typeof fontWeights;

export const fontSizes = {
"2xs": "0.5rem",
xs: "0.75rem",
sm: "0.875rem",
md: "1rem",
lg: "1.125rem",
xl: "1.25rem",
"2xl": "1.5rem",
"3xl": "1.875rem",
"4xl": "2.25rem",
"5xl": "3rem",
"6xl": "3.75rem",
"7xl": "4.5rem",
"8xl": "6rem",
"9xl": "8rem",
} as const;

export type FontSize = keyof typeof fontSizes;

export const letterSpacings: Tokens["letterSpacings"] = {
tighter: { value: "-0.05em" },
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"verbatimModuleSyntax": true
},
"include": ["src", "styled-system"]
}
3 changes: 3 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ export default defineConfig({
environment: "happy-dom",
setupFiles: ["./src/setupTests.tsx"],
},
optimizeDeps: {
exclude: ["node_modules/.cache/storybook"],
},
});

0 comments on commit c27c441

Please sign in to comment.