diff --git a/bun.lockb b/bun.lockb index 95b12f2..356b3bc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 42d378c..88c507c 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@chromatic-com/storybook": "^3.2.2", "@eslint/js": "^9.16.0", + "@faker-js/faker": "^9.3.0", "@pandacss/dev": "^0.48.1", "@storybook/addon-a11y": "^8.4.7", "@storybook/addon-essentials": "^8.4.7", diff --git a/panda.config.ts b/panda.config.ts index e62fdef..99cdd88 100644 --- a/panda.config.ts +++ b/panda.config.ts @@ -26,6 +26,18 @@ export default defineConfig({ "--font-spoqa": "Spoqa Han Sans Neo", }, + staticCss: { + css: [ + { + properties: { + textStyle: Object.keys(textStyles), + fontSize: Object.keys(fontSizes), + fontWeight: Object.keys(fontWeights), + }, + }, + ], + }, + // Useful for theme customization theme: { extend: { diff --git a/src/App.tsx b/src/App.tsx index adfe7bc..330418d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import logo from "/logo.svg"; import { css } from "../styled-system/css"; import { Button } from "./components/Button"; +import { Heading } from "./components/Heading"; function App() { return ( @@ -23,9 +24,9 @@ function App() {
-

Welcome Dale UI!

+ Welcome Dale UI!
-

유용한 링크

+ 유용한 링크
-

섹션 2

+ 섹션 2
diff --git a/src/components/Heading/Heading.stories.tsx b/src/components/Heading/Heading.stories.tsx new file mode 100644 index 0000000..211e369 --- /dev/null +++ b/src/components/Heading/Heading.stories.tsx @@ -0,0 +1,74 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { vstack } from "../../../styled-system/patterns"; +import { Heading } from "./Heading"; + +const meta = { + component: Heading, + parameters: { + layout: "centered", + }, + args: { + children: "제목", + level: 1, + }, +} satisfies Meta; + +export default meta; + +export const Basic: StoryObj = {}; + +export const Levels: StoryObj = { + 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"], + }, });