diff --git a/app/layout.tsx b/app/layout.tsx index d67d803..0e50b37 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; import "./globals.css"; +import Blur from "@/components/common/Blur"; const pretendard = localFont({ src: "./fonts/PretendardVariable.woff2", @@ -38,9 +39,10 @@ export default function RootLayout({ return ( -
+
+ {children}
diff --git a/app/page.tsx b/app/page.tsx index b4c3e0b..718c073 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,63 +1,9 @@ +import Blur from "@/components/common/Blur"; + export default function Home() { return ( -
-
- {/* 토큰 테스트 섹션 */} -
-

- 🎨 Design Token Test -

- - {/* 1. Typography */} -
-

1. Typography

-
-

Display 24px (Bold 700)

-

Title 20px (SemiBold 600)

-

Body 18px (Medium 500)

-

Body 16px (Regular 400)

-

- Caption 14px (Disabled Color) -

-
-
- - {/* 2. Colors & Backgrounds */} -
-

2. Colors & Backgrounds

-
-
- Pink Secondary -
-
- Orange Primary -
-
- Surface Base -
-
- Disabled Area -
-
-
- - {/* 3. Buttons */} -
-

3. Buttons

-
- - - -
-
-
-
-
+
+ +
); } diff --git a/app/tokens.css b/app/tokens.css index e8566c8..02dc7fb 100644 --- a/app/tokens.css +++ b/app/tokens.css @@ -1,260 +1,411 @@ /* Auto-generated from token.json */ -/* Run: node scripts/generate-tokens.js to regenerate */ +/* Run: pnpm run token to regenerate */ +/* Total unique tokens: 126 */ @layer base { :root { - --color-gray-900: #1a1a1a; + --background-app-base: var(--color-surface-base); + --background-app-blur-bottom-left: var(--color-brand-secondary-pink); + --background-app-blur-bottom-right: var(--color-brand-primary-orange); + --background-app-opacity: var(--opacity-full); + --background-border-radius: var(--radius-none); + --background-glow-blur-radius: 280; + --background: var(--default); + --black: #000000; + --body: Roboto; + --bodybold: Bold; + --bodyregular: Regular; + --border-width-default: 1; + --border-width-thick: 2; + --border-width-thin: 0.8; + --borderradius: var(--lg); + --borderwidth: var(--sm); + --button-background-bold: var(--color-brand-black); + --button-background-disabled: var(--color-background-disabled); + --button-background-gradient-1-end: var(--color-brand-primary-orange); + --button-background-gradient-1-start: var(--color-brand-primary-flame); + --button-border-color: var(--color-border-light); + --button-border-radius: var(--radius-md); + --button-border-width: var(--border-width-thin); + --button-opacity-default: var(--opacity-full); + --button-primary-text-default: var(--color-text-white); + --button-primary-text-disabled: var(--color-text-disabled); + --button-slate-default: var(--color-gray-0-a30); + --color-background-disabled: var(--color-gray-300-a40); + --color-border-light: var(--color-gray-0-a30); + --color-brand-black: var(--color-gray-900); + --color-brand-primary-flame: var(--color-flame-700); + --color-brand-primary-orange: var(--color-orange-700); + --color-brand-primary-pink: var(--color-pink-700); + --color-brand-secondary-pink: var(--color-pink-100); + --color-flame-100: #ffc4cd; + --color-flame-300: #ff8a9b; + --color-flame-500: #ff667c; + --color-flame-50: #ffe5e9; + --color-flame-700: #ff4d61; + --color-flame-900: #ed1134; + --color-gray-0-a30: #ffffff4d; --color-gray-0: #ffffff; - --color-gray-500: #808080; - --color-gray-600: #666666; - --color-gray-300: #b3b3b3; - --color-gray-700: #4d4d4d; --color-gray-100: #e5e5e5; + --color-gray-200: #cccccc; + --color-gray-300-a40: #b3b3b366; + --color-gray-300: #b3b3b3; --color-gray-400: #999999; + --color-gray-500: #808080; --color-gray-50: #f5f5f5; - --color-gray-200: #cccccc; - --color-gray-800: #333333; + --color-gray-600: #666666; --color-gray-64: #efefef; + --color-gray-700: #4d4d4d; + --color-gray-800: #333333; + --color-gray-900: #1a1a1a; + --color-orange-100: #ffc7b9; + --color-orange-300: #ffa188; + --color-orange-500: #ff8a6f; + --color-orange-50: #ffe8e2; + --color-orange-700: #ff775e; + --color-orange-900: #dd4527; + --color-pink-100: #fbcde7; --color-pink-300: #f8a4cb; --color-pink-500: #f387be; - --color-pink-700: #f57db2; --color-pink-50: #fdeaf5; + --color-pink-700: #f57db2; --color-pink-900: #e3468b; - --color-pink-100: #fbcde7; - --color-flame-300: #ff8a9b; - --color-flame-700: #ff4d61; - --color-flame-500: #ff667c; - --color-flame-50: #ffe5e9; - --color-flame-900: #ed1134; - --color-flame-100: #ffc4cd; - --color-orange-500: #ff8a6f; - --color-orange-700: #ff775e; - --color-orange-100: #ffc7b9; - --color-orange-300: #ffa188; - --color-orange-900: #dd4527; - --color-orange-50: #ffe8e2; - --color-gray-0-a30: #ffffff4d; - --color-gray-300-a40: #b3b3b366; --color-surface-base: var(--color-gray-64); - --color-brand-secondary-pink: var(--color-pink-100); - --color-brand-primary-orange: var(--color-orange-700); - --color-brand-primary-pink: var(--color-pink-700); - --color-brand-primary-flame: var(--color-flame-700); - --color-border-light: var(--color-gray-0-a30); - --color-text-white: var(--color-gray-0); --color-text-disabled: var(--color-gray-300); - --color-background-disabled: var(--color-gray-300-a40); - --color-brand-black: var(--color-gray-900); - --radius-none: 0px; - --radius-xs: 8px; - --radius-sm: 12px; - --radius-md: 16px; - --radius-lg: 24px; - --radius-full: 99px; - --border-thin: 0.8px; - --border-default: 1px; - --border-thick: 2px; - --opacity-full: 100%; - --opacity-medium: 50%; - --opacity-subtle: 20%; - --opacity-subtle-2: 40%; + --color-text-white: var(--color-gray-0); + --decreased: -5%; + --default: 0; + --flame100: #ffc4cd; + --flame300: #ff8a9b; + --flame500: #ff667c; + --flame50: #ffe5e9; + --flame700: #ff4d61; + --flame900: #ed1134; + --font-size-10: 10; + --font-size-12: 12; + --font-size-14: 14; + --font-size-16: 16; + --font-size-18: 18; + --font-size-20: 20; + --font-size-24: 24; + --h1: 32; + --h2: 26; + --h6: var(--body); + --heading: Inter; + --headingbold: Bold; + --headingregular: Regular; + --high: 90%; + --increased: 150%; + --lg: var(--lg); + --low: 10%; + --md: var(--md); + --multi-value: var(--xl); + --onaccent: var(--white); + --opacity-full: 100; + --opacity-medium: 50; + --opacity-subtle-2: 40; + --opacity-subtle: 20; + --orange100: #ffc7b9; + --orange300: #ffa188; + --orange500: #ff8a6f; + --orange50: #ffe8e2; + --orange700: #ff775e; + --orange900: #dd4527; + --padding: var(--md); + --pink100: #fbcde7; + --pink300: #f8a4cb; + --pink500: #f387be; + --pink50: #fdeaf5; + --pink700: #f57db2; + --pink900: #e3468b; + --radius-full: 99; + --radius-lg: 24; + --radius-md: 16; + --radius-none: 0; + --radius-sm: 12; + --radius-xs: 8; + --scale: 2; + --sm: var(--sm); + --text: var(--onaccent); + --white: #ffffff; + --xl: var(--xl); + --xs: 4; } } @layer utilities { - /* Text utilities: text-{size}-{weight} */ -.text-16-400 { - font-size: 16px; - font-weight: 400; - line-height: 1.5; -} + /* Font utilities */ + .text-16-400 { + font-size: 16px; + font-weight: 400; + line-height: 1.5; + } -.text-16-500 { - font-size: 16px; - font-weight: 500; - line-height: 1.5; -} + .text-16-500 { + font-size: 16px; + font-weight: 500; + line-height: 1.5; + } -.text-16-600 { - font-size: 16px; - font-weight: 600; - line-height: 1.5; -} + .text-16-600 { + font-size: 16px; + font-weight: 600; + line-height: 1.5; + } -.text-16-700 { - font-size: 16px; - font-weight: 700; - line-height: 1.5; -} + .text-16-700 { + font-size: 16px; + font-weight: 700; + line-height: 1.5; + } -.text-20-400 { - font-size: 20px; - font-weight: 400; - line-height: 1.5; -} + .text-20-400 { + font-size: 20px; + font-weight: 400; + line-height: 1.5; + } -.text-20-500 { - font-size: 20px; - font-weight: 500; - line-height: 1.5; -} + .text-20-500 { + font-size: 20px; + font-weight: 500; + line-height: 1.5; + } -.text-20-600 { - font-size: 20px; - font-weight: 600; - line-height: 1.5; -} + .text-20-600 { + font-size: 20px; + font-weight: 600; + line-height: 1.5; + } -.text-20-700 { - font-size: 20px; - font-weight: 700; - line-height: 1.5; -} + .text-20-700 { + font-size: 20px; + font-weight: 700; + line-height: 1.5; + } -.text-24-400 { - font-size: 24px; - font-weight: 400; - line-height: 1.5; -} + .text-24-400 { + font-size: 24px; + font-weight: 400; + line-height: 1.5; + } -.text-24-500 { - font-size: 24px; - font-weight: 500; - line-height: 1.5; -} + .text-24-500 { + font-size: 24px; + font-weight: 500; + line-height: 1.5; + } -.text-24-600 { - font-size: 24px; - font-weight: 600; - line-height: 1.5; -} + .text-24-600 { + font-size: 24px; + font-weight: 600; + line-height: 1.5; + } -.text-24-700 { - font-size: 24px; - font-weight: 700; - line-height: 1.5; -} + .text-24-700 { + font-size: 24px; + font-weight: 700; + line-height: 1.5; + } -.text-12-400 { - font-size: 12px; - font-weight: 400; - line-height: 1.5; -} + .text-12-400 { + font-size: 12px; + font-weight: 400; + line-height: 1.5; + } -.text-12-500 { - font-size: 12px; - font-weight: 500; - line-height: 1.5; -} + .text-12-500 { + font-size: 12px; + font-weight: 500; + line-height: 1.5; + } -.text-12-600 { - font-size: 12px; - font-weight: 600; - line-height: 1.5; -} + .text-12-600 { + font-size: 12px; + font-weight: 600; + line-height: 1.5; + } -.text-12-700 { - font-size: 12px; - font-weight: 700; - line-height: 1.5; -} + .text-12-700 { + font-size: 12px; + font-weight: 700; + line-height: 1.5; + } -.text-14-400 { - font-size: 14px; - font-weight: 400; - line-height: 1.5; -} + .text-14-400 { + font-size: 14px; + font-weight: 400; + line-height: 1.5; + } -.text-14-500 { - font-size: 14px; - font-weight: 500; - line-height: 1.5; -} + .text-14-500 { + font-size: 14px; + font-weight: 500; + line-height: 1.5; + } -.text-14-600 { - font-size: 14px; - font-weight: 600; - line-height: 1.5; -} + .text-14-600 { + font-size: 14px; + font-weight: 600; + line-height: 1.5; + } -.text-14-700 { - font-size: 14px; - font-weight: 700; - line-height: 1.5; -} + .text-14-700 { + font-size: 14px; + font-weight: 700; + line-height: 1.5; + } -.text-10-400 { - font-size: 10px; - font-weight: 400; - line-height: 1.5; -} + .text-10-400 { + font-size: 10px; + font-weight: 400; + line-height: 1.5; + } -.text-10-500 { - font-size: 10px; - font-weight: 500; - line-height: 1.5; -} + .text-10-500 { + font-size: 10px; + font-weight: 500; + line-height: 1.5; + } -.text-10-600 { - font-size: 10px; - font-weight: 600; - line-height: 1.5; -} + .text-10-600 { + font-size: 10px; + font-weight: 600; + line-height: 1.5; + } -.text-10-700 { - font-size: 10px; - font-weight: 700; - line-height: 1.5; -} + .text-10-700 { + font-size: 10px; + font-weight: 700; + line-height: 1.5; + } -.text-18-400 { - font-size: 18px; - font-weight: 400; - line-height: 1.5; -} + .text-18-400 { + font-size: 18px; + font-weight: 400; + line-height: 1.5; + } -.text-18-500 { - font-size: 18px; - font-weight: 500; - line-height: 1.5; -} + .text-18-500 { + font-size: 18px; + font-weight: 500; + line-height: 1.5; + } -.text-18-600 { - font-size: 18px; - font-weight: 600; - line-height: 1.5; -} + .text-18-600 { + font-size: 18px; + font-weight: 600; + line-height: 1.5; + } -.text-18-700 { - font-size: 18px; - font-weight: 700; - line-height: 1.5; -} + .text-18-700 { + font-size: 18px; + font-weight: 700; + line-height: 1.5; + } - /* Background utilities */ - .bg-surface-base { background-color: var(--color-surface-base); } - .bg-brand-secondary-pink { background-color: var(--color-brand-secondary-pink); } - .bg-brand-primary-orange { background-color: var(--color-brand-primary-orange); } - .bg-disabled { background-color: var(--color-background-disabled); } - - /* Button utilities */ + /* Background color utilities */ + .bg-background-app-base { background-color: var(--background-app-base); } + .bg-background-app-blur-bottom-left { background-color: var(--background-app-blur-bottom-left); } + .bg-background-app-blur-bottom-right { background-color: var(--background-app-blur-bottom-right); } + .bg-button-background-bold { background-color: var(--button-background-bold); } + .bg-button-background-disabled { background-color: var(--button-background-disabled); } + .bg-button-background-gradient-1-end { background-color: var(--button-background-gradient-1-end); } + .bg-button-background-gradient-1-start { background-color: var(--button-background-gradient-1-start); } + .bg-color-background-disabled { background-color: var(--color-background-disabled); } + .bg-color-border-light { background-color: var(--color-border-light); } + .bg-color-brand-black { background-color: var(--color-brand-black); } + .bg-color-brand-primary-flame { background-color: var(--color-brand-primary-flame); } + .bg-color-brand-primary-orange { background-color: var(--color-brand-primary-orange); } + .bg-color-brand-primary-pink { background-color: var(--color-brand-primary-pink); } + .bg-color-brand-secondary-pink { background-color: var(--color-brand-secondary-pink); } + .bg-color-flame-100 { background-color: var(--color-flame-100); } + .bg-color-flame-300 { background-color: var(--color-flame-300); } + .bg-color-flame-50 { background-color: var(--color-flame-50); } + .bg-color-flame-500 { background-color: var(--color-flame-500); } + .bg-color-flame-700 { background-color: var(--color-flame-700); } + .bg-color-flame-900 { background-color: var(--color-flame-900); } + .bg-color-gray-0 { background-color: var(--color-gray-0); } + .bg-color-gray-0-a30 { background-color: var(--color-gray-0-a30); } + .bg-color-gray-100 { background-color: var(--color-gray-100); } + .bg-color-gray-200 { background-color: var(--color-gray-200); } + .bg-color-gray-300 { background-color: var(--color-gray-300); } + .bg-color-gray-300-a40 { background-color: var(--color-gray-300-a40); } + .bg-color-gray-400 { background-color: var(--color-gray-400); } + .bg-color-gray-50 { background-color: var(--color-gray-50); } + .bg-color-gray-500 { background-color: var(--color-gray-500); } + .bg-color-gray-600 { background-color: var(--color-gray-600); } + .bg-color-gray-64 { background-color: var(--color-gray-64); } + .bg-color-gray-700 { background-color: var(--color-gray-700); } + .bg-color-gray-800 { background-color: var(--color-gray-800); } + .bg-color-gray-900 { background-color: var(--color-gray-900); } + .bg-color-orange-100 { background-color: var(--color-orange-100); } + .bg-color-orange-300 { background-color: var(--color-orange-300); } + .bg-color-orange-50 { background-color: var(--color-orange-50); } + .bg-color-orange-500 { background-color: var(--color-orange-500); } + .bg-color-orange-700 { background-color: var(--color-orange-700); } + .bg-color-orange-900 { background-color: var(--color-orange-900); } + .bg-color-pink-100 { background-color: var(--color-pink-100); } + .bg-color-pink-300 { background-color: var(--color-pink-300); } + .bg-color-pink-50 { background-color: var(--color-pink-50); } + .bg-color-pink-500 { background-color: var(--color-pink-500); } + .bg-color-pink-700 { background-color: var(--color-pink-700); } + .bg-color-pink-900 { background-color: var(--color-pink-900); } + .bg-color-surface-base { background-color: var(--color-surface-base); } + .bg-color-text-disabled { background-color: var(--color-text-disabled); } + .bg-color-text-white { background-color: var(--color-text-white); } + + /* Text color utilities */ + .text-button-primary-text-default { color: var(--button-primary-text-default); } + .text-button-primary-text-disabled { color: var(--button-primary-text-disabled); } + .text-color-background-disabled { color: var(--color-background-disabled); } + .text-color-border-light { color: var(--color-border-light); } + .text-color-brand-black { color: var(--color-brand-black); } + .text-color-brand-primary-flame { color: var(--color-brand-primary-flame); } + .text-color-brand-primary-orange { color: var(--color-brand-primary-orange); } + .text-color-brand-primary-pink { color: var(--color-brand-primary-pink); } + .text-color-brand-secondary-pink { color: var(--color-brand-secondary-pink); } + .text-color-flame-100 { color: var(--color-flame-100); } + .text-color-flame-300 { color: var(--color-flame-300); } + .text-color-flame-50 { color: var(--color-flame-50); } + .text-color-flame-500 { color: var(--color-flame-500); } + .text-color-flame-700 { color: var(--color-flame-700); } + .text-color-flame-900 { color: var(--color-flame-900); } + .text-color-gray-0 { color: var(--color-gray-0); } + .text-color-gray-0-a30 { color: var(--color-gray-0-a30); } + .text-color-gray-100 { color: var(--color-gray-100); } + .text-color-gray-200 { color: var(--color-gray-200); } + .text-color-gray-300 { color: var(--color-gray-300); } + .text-color-gray-300-a40 { color: var(--color-gray-300-a40); } + .text-color-gray-400 { color: var(--color-gray-400); } + .text-color-gray-50 { color: var(--color-gray-50); } + .text-color-gray-500 { color: var(--color-gray-500); } + .text-color-gray-600 { color: var(--color-gray-600); } + .text-color-gray-64 { color: var(--color-gray-64); } + .text-color-gray-700 { color: var(--color-gray-700); } + .text-color-gray-800 { color: var(--color-gray-800); } + .text-color-gray-900 { color: var(--color-gray-900); } + .text-color-orange-100 { color: var(--color-orange-100); } + .text-color-orange-300 { color: var(--color-orange-300); } + .text-color-orange-50 { color: var(--color-orange-50); } + .text-color-orange-500 { color: var(--color-orange-500); } + .text-color-orange-700 { color: var(--color-orange-700); } + .text-color-orange-900 { color: var(--color-orange-900); } + .text-color-pink-100 { color: var(--color-pink-100); } + .text-color-pink-300 { color: var(--color-pink-300); } + .text-color-pink-50 { color: var(--color-pink-50); } + .text-color-pink-500 { color: var(--color-pink-500); } + .text-color-pink-700 { color: var(--color-pink-700); } + .text-color-pink-900 { color: var(--color-pink-900); } + .text-color-surface-base { color: var(--color-surface-base); } + .text-color-text-disabled { color: var(--color-text-disabled); } + .text-color-text-white { color: var(--color-text-white); } + + /* Border utilities */ + .border-button-border-color { border-color: var(--button-border-color); } + .border-color-border-light { border-color: var(--color-border-light); } + + /* Custom gradient buttons */ .bg-button-primary { background: linear-gradient(135deg, var(--color-brand-primary-flame), var(--color-brand-primary-orange)); } - - .bg-button-disabled { - background-color: var(--color-background-disabled); - } - - .bg-button-slate { - background-color: var(--color-gray-0-a30); - } - - /* Border utilities */ - .border-light { border-color: var(--color-border-light); } - - /* Text color utilities */ - .text-white { color: var(--color-text-white); } - .text-disabled { color: var(--color-text-disabled); } - .text-brand-black { color: var(--color-brand-black); } } diff --git a/components/common/Blur.tsx b/components/common/Blur.tsx new file mode 100644 index 0000000..edee4b5 --- /dev/null +++ b/components/common/Blur.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +const Blur = () => { + return ( +
+ {/* orange blur - right */} +
+ {/* pink blur - left */} +
+
+ ); +}; + +export default Blur; diff --git a/package.json b/package.json index ff07740..b770268 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "lint": "eslint", "format": "prettier --write .", "format:check": "prettier --check .", - "prepare": "husky" + "prepare": "husky", + "token": "node scripts/generate-tokens.js" }, "dependencies": { "@tanstack/react-query": "^5.90.20", diff --git a/scripts/generate-tokens.js b/scripts/generate-tokens.js index 188f534..c047395 100644 --- a/scripts/generate-tokens.js +++ b/scripts/generate-tokens.js @@ -9,93 +9,88 @@ const __dirname = path.dirname(__filename); const tokensPath = path.join(__dirname, "../app/token.json"); const tokens = JSON.parse(fs.readFileSync(tokensPath, "utf-8")); -// Semantic tokens에서 실제 색상값 추출 -const semanticMode1 = tokens["Sementic/Mode 1"] || {}; -const systemMode1 = tokens["System/Mode 1"] || {}; - -// Grayscale, Colors, Transparent 값 추출 -const grayscale = semanticMode1.Grayscale || {}; -const colors = semanticMode1.Colors || {}; -const transparent = semanticMode1.Transparent || {}; - -// CSS 변수 생성 -let cssVariables = []; - -// 키 정제 함수 (공백을 하이픈으로 변경) +// 키 정제 함수 - 더 엄격하게 function sanitizeKey(key) { - return key.replace(/\s+/g, "-").toLowerCase(); + return key + .replace(/\(.*?\)/g, "") // 괄호와 내용 제거 + .replace(/[^\w-]/g, "-") // 특수문자를 하이픈으로 + .replace(/^-+|-+$/g, "") // 앞뒤 하이픈 제거 + .replace(/-+/g, "-") // 연속 하이픈을 하나로 + .toLowerCase(); } -// Grayscale 색상 -Object.entries(grayscale).forEach(([key, value]) => { - if (key.startsWith("color-gray-")) { - cssVariables.push(` --${sanitizeKey(key)}: ${value.$value};`); - } -}); - -// Colors (Pink, Flame, Orange) -if (colors.Pink) { - Object.entries(colors.Pink).forEach(([key, value]) => { - if (key.startsWith("color-pink-")) { - cssVariables.push(` --${sanitizeKey(key)}: ${value.$value};`); - } - }); +// 유효한 CSS 변수명인지 확인 +function isValidCSSVarName(name) { + // 숫자로만 구성되거나, 숫자로 시작하면 안됨 + return !/^\d+$/.test(name) && !/^-?\d/.test(name) && name.length > 0; } -if (colors.Flame) { - Object.entries(colors.Flame).forEach(([key, value]) => { - if (key.startsWith("color-flame-")) { - cssVariables.push(` --${sanitizeKey(key)}: ${value.$value};`); - } - }); + +// 유효한 CSS 값인지 확인 +function isValidCSSValue(value) { + if (typeof value === "number") return true; + if (typeof value === "string" && value.trim().length > 0) { + // [object Object] 같은 잘못된 값 제외 + if (value.includes("[object")) return false; + // roundTo, 수식 같은 함수 호출 제외 + if (value.includes("roundTo(") || /\*|\^/.test(value)) return false; + // 참조는 나중에 해석되므로 허용 + return true; + } + return false; } -if (colors.Orange) { - Object.entries(colors.Orange).forEach(([key, value]) => { - if (key.startsWith("color-orange-")) { - cssVariables.push(` --${sanitizeKey(key)}: ${value.$value};`); - } - }); + +// 해석된 CSS 값이 유효한지 확인 (최종 검증) +function isValidResolvedValue(value) { + if (typeof value === "number") return true; + if (typeof value === "string" && value.trim().length > 0) { + // 해석되지 않은 참조가 남아있으면 제외 + if (value.includes("{") && value.includes("}")) return false; + // var(...) 참조는 유효 + if (value.startsWith("var(--")) return true; + // 일반 CSS 값 (색상, px 등) + return true; + } + return false; } -// Transparent -Object.entries(transparent).forEach(([key, value]) => { - cssVariables.push(` --${sanitizeKey(key)}: ${value.$value};`); -}); +// 토큰 저장소 (중복 방지를 위해 Map 사용) +const tokenMap = new Map(); // key -> { type, value, path } -// System tokens -if (systemMode1.Colors) { - Object.entries(systemMode1.Colors).forEach(([key, value]) => { - if (value.$type === "color") { - const finalValue = resolveValue(value.$value); - cssVariables.push(` --${sanitizeKey(key)}: ${finalValue};`); - } - }); -} +// 재귀적으로 token.json 탐색 +function traverseTokens(obj, path = []) { + for (const [key, value] of Object.entries(obj)) { + if (value && typeof value === "object") { + // $type과 $value가 있으면 토큰임 + if (value.$type && value.$value !== undefined) { + const tokenName = sanitizeKey(key); -// Radius -if (systemMode1.Radius) { - Object.entries(systemMode1.Radius).forEach(([key, value]) => { - const varName = key.replace("radius-", ""); - cssVariables.push(` --radius-${sanitizeKey(varName)}: ${value.$value}px;`); - }); -} + // 유효성 검사 + if (!isValidCSSVarName(tokenName)) continue; + if (!isValidCSSValue(value.$value)) continue; -// Border -if (systemMode1.Border) { - Object.entries(systemMode1.Border).forEach(([key, value]) => { - const varName = key.replace("border-width-", ""); - cssVariables.push(` --border-${sanitizeKey(varName)}: ${value.$value}px;`); - }); -} + // 경로를 포함한 고유 키 생성 (중복 방지) + const fullPath = [...path, key].map(sanitizeKey).join("-"); + const uniqueKey = fullPath || tokenName; -// Opacity -if (systemMode1.Opacity) { - Object.entries(systemMode1.Opacity).forEach(([key, value]) => { - const varName = key.replace("opacity-", ""); - cssVariables.push(` --opacity-${sanitizeKey(varName)}: ${value.$value}%;`); - }); + // 이미 존재하지 않으면 추가 + if (!tokenMap.has(uniqueKey)) { + tokenMap.set(uniqueKey, { + name: tokenName, + type: value.$type, + value: value.$value, + path: [...path, key], + fullPath: uniqueKey, + }); + } + } else { + // 재귀적으로 탐색 + traverseTokens(value, [...path, key]); + } + } + } } -// 토큰 참조 해석 +// 토큰 참조 해석 (범용) function resolveValue(value, depth = 0) { if (depth > 10 || typeof value !== "string") return value; @@ -103,64 +98,117 @@ function resolveValue(value, depth = 0) { if (!match) return value; const ref = match[1]; + const parts = ref.split("."); + const lastPart = parts[parts.length - 1]; + const tokenName = sanitizeKey(lastPart); - // Grayscale 참조 - if (ref.startsWith("Grayscale.")) { - const key = ref.replace("Grayscale.", ""); - return `var(--${key})`; + // tokenMap에서 찾기 + for (const [key, token] of tokenMap.entries()) { + if (token.name === tokenName) { + return `var(--${token.name})`; + } } - // Colors 참조 - if (ref.startsWith("Colors.Pink.")) { - const key = ref.replace("Colors.Pink.", ""); - return `var(--${key})`; - } - if (ref.startsWith("Colors.Flame.")) { - const key = ref.replace("Colors.Flame.", ""); - return `var(--${key})`; + // 못 찾으면 그대로 반환 + return value; +} + +// 1단계: 모든 토큰 수집 +traverseTokens(tokens); +const allTokens = Array.from(tokenMap.values()); + +console.log(`\n🔍 Collected ${allTokens.length} unique tokens`); + +// 2단계: CSS 변수 생성 (중복 제거 및 정렬) +const uniqueVars = new Map(); + +allTokens.forEach((token) => { + let cssValue = token.value; + + // 참조 해석 + if (typeof cssValue === "string" && cssValue.includes("{")) { + cssValue = resolveValue(cssValue); } - if (ref.startsWith("Colors.Orange.")) { - const key = ref.replace("Colors.Orange.", ""); - return `var(--${key})`; + + // 타입에 따라 단위 추가 + if (token.type === "dimension" && typeof cssValue === "number") { + cssValue = `${cssValue}px`; + } else if (token.type === "spacing" && typeof cssValue === "number") { + cssValue = `${cssValue}px`; } - // Transparent 참조 - if (ref.startsWith("Transparent.")) { - const key = ref.replace("Transparent.", ""); - return `var(--${key})`; + // 최종 해석된 값이 유효한지 확인 + if (!isValidResolvedValue(cssValue)) { + return; // 유효하지 않으면 건너뛰기 } - // System/Mode 1의 Colors 참조 - if (ref.startsWith("Colors.")) { - const key = ref.replace("Colors.", ""); - return `var(--${key})`; + // 중복 방지: 같은 이름이 있으면 더 구체적인 경로를 우선 + if (!uniqueVars.has(token.name)) { + uniqueVars.set(token.name, cssValue); } +}); - return value; -} +const cssVariables = Array.from(uniqueVars.entries()) + .map(([name, value]) => ` --${name}: ${value};`) + .sort(); // 알파벳 순 정렬 -// Font Size - text-{size}-{weight} 형태로 생성 -const fontSizes = systemMode1?.Font || {}; -const weights = ["400", "500", "600", "700"]; -let tailwindUtilities = []; +// 3단계: Tailwind 유틸리티 클래스 생성 +const colorTokens = allTokens.filter((t) => t.type === "color"); +const fontTokens = allTokens.filter((t) => t.name.startsWith("font-size-")); -Object.entries(fontSizes).forEach(([key, value]) => { - if (key.startsWith("font-size-")) { - const size = value.$value; +// 배경색 유틸리티 (중복 제거) +const bgUtilitiesSet = new Set(); +colorTokens + .filter((t) => t.name.includes("color-") || t.name.includes("background-")) + .forEach((t) => { + bgUtilitiesSet.add( + ` .bg-${t.name} { background-color: var(--${t.name}); }`, + ); + }); +const bgUtilities = Array.from(bgUtilitiesSet).sort().join("\n"); + +// 텍스트 색상 유틸리티 (중복 제거) +const textColorUtilitiesSet = new Set(); +colorTokens + .filter((t) => t.name.includes("color-") || t.name.includes("text-")) + .forEach((t) => { + textColorUtilitiesSet.add(` .text-${t.name} { color: var(--${t.name}); }`); + }); +const textColorUtilities = Array.from(textColorUtilitiesSet).sort().join("\n"); + +// 폰트 사이즈 유틸리티 (text-{size}-{weight}) +const weights = ["400", "500", "600", "700"]; +const fontUtilitiesSet = new Set(); +fontTokens.forEach((t) => { + const size = t.value; + if (typeof size === "number") { weights.forEach((weight) => { - const className = `.text-${size}-${weight}`; - tailwindUtilities.push(`${className} { - font-size: ${size}px; - font-weight: ${weight}; - line-height: 1.5; -}`); + fontUtilitiesSet.add(` .text-${size}-${weight} { + font-size: ${size}px; + font-weight: ${weight}; + line-height: 1.5; + }`); }); } }); +const fontUtilities = Array.from(fontUtilitiesSet); + +// Border 유틸리티 (중복 제거) +const borderUtilitiesSet = new Set(); +allTokens + .filter((t) => t.name.includes("border") && t.type === "color") + .forEach((t) => { + const className = t.name.replace(/^border-/, ""); + borderUtilitiesSet.add( + ` .border-${className} { border-color: var(--${t.name}); }`, + ); + }); +const borderUtilities = Array.from(borderUtilitiesSet).sort().join("\n"); -// CSS 파일 생성 +// 4단계: CSS 파일 생성 const cssContent = `/* Auto-generated from token.json */ -/* Run: node scripts/generate-tokens.js to regenerate */ +/* Run: pnpm run token to regenerate */ +/* Total unique tokens: ${uniqueVars.size} */ @layer base { :root { @@ -169,35 +217,22 @@ ${cssVariables.join("\n")} } @layer utilities { - /* Text utilities: text-{size}-{weight} */ -${tailwindUtilities.join("\n\n")} - - /* Background utilities */ - .bg-surface-base { background-color: var(--color-surface-base); } - .bg-brand-secondary-pink { background-color: var(--color-brand-secondary-pink); } - .bg-brand-primary-orange { background-color: var(--color-brand-primary-orange); } - .bg-disabled { background-color: var(--color-background-disabled); } - - /* Button utilities */ + /* Font utilities */ +${fontUtilities.join("\n\n")} + + /* Background color utilities */ +${bgUtilities} + + /* Text color utilities */ +${textColorUtilities} + + /* Border utilities */ +${borderUtilities} + + /* Custom gradient buttons */ .bg-button-primary { background: linear-gradient(135deg, var(--color-brand-primary-flame), var(--color-brand-primary-orange)); } - - .bg-button-disabled { - background-color: var(--color-background-disabled); - } - - .bg-button-slate { - background-color: var(--color-gray-0-a30); - } - - /* Border utilities */ - .border-light { border-color: var(--color-border-light); } - - /* Text color utilities */ - .text-white { color: var(--color-text-white); } - .text-disabled { color: var(--color-text-disabled); } - .text-brand-black { color: var(--color-brand-black); } } `; @@ -207,8 +242,23 @@ fs.writeFileSync(outputPath, cssContent, "utf-8"); console.log("✅ Tokens generated successfully!"); console.log(`📝 Output: ${outputPath}`); +console.log(`📊 Total unique tokens: ${uniqueVars.size}`); +console.log( + ` - Colors: ${colorTokens.length}, Fonts: ${fontTokens.length}, Others: ${ + allTokens.length - colorTokens.length - fontTokens.length + }`, +); +console.log("\n✨ Improvements:"); +console.log(" - Removed duplicate variable names"); +console.log(" - Filtered out invalid CSS values ([object Object])"); +console.log( + " - Fixed variable names (removed parentheses, numbers-only names)", +); +console.log(" - Sorted variables alphabetically"); console.log("\nNext steps:"); console.log("1. Import tokens.css in your globals.css:"); console.log(' @import "./tokens.css";'); console.log("\n2. Use utilities in your components:"); -console.log('
Hello
'); +console.log( + '
Hello
', +);