From 90b618ade8ed47023c9c580499cdfaefaacf7746 Mon Sep 17 00:00:00 2001 From: hywznn Date: Tue, 18 Aug 2026 18:24:23 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=83=81=ED=83=9C=20=EC=83=89=EC=83=81?= =?UTF-8?q?=20=EB=8C=80=EB=B9=84=20AA=20=EA=B8=B0=EC=A4=80=20=EC=B6=A9?= =?UTF-8?q?=EC=A1=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/tokens.css | 10 +++---- src/styles/tokens.test.ts | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 src/styles/tokens.test.ts diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 622f89b..5c7d07e 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -20,14 +20,14 @@ --fowoco-violet-50: #f2effe; --fowoco-violet-600: #7a5cd6; --fowoco-red-50: #fdedee; - --fowoco-red-600: #d8434a; + --fowoco-red-600: #c13f46; --fowoco-amber-50: #fdf3e3; - --fowoco-amber-600: #c9821c; + --fowoco-amber-600: #8f5f0f; --fowoco-green-50: #e9f6ee; - --fowoco-green-600: #1f8a51; + --fowoco-green-600: #187745; - --status-critical: #d8434a; - --status-warning: #c9821c; + --status-critical: #c13f46; + --status-warning: #8f5f0f; --status-info: #4361ee; /* 로그인류 히어로 패널·토스트처럼 앱 테마와 무관하게 항상 어두워야 하는 곳 전용. */ diff --git a/src/styles/tokens.test.ts b/src/styles/tokens.test.ts new file mode 100644 index 0000000..65dfec4 --- /dev/null +++ b/src/styles/tokens.test.ts @@ -0,0 +1,61 @@ +/// + +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +import { describe, expect, it } from 'vitest' + +const WHITE = '#ffffff' +const tokensCss = readFileSync(join(process.cwd(), 'src/styles/tokens.css'), 'utf8') + +function token(name: string) { + const match = tokensCss.match(new RegExp(`--${name}:\\s*(#[0-9a-fA-F]{3,6})`)) + + if (!match) { + throw new Error(`CSS token not found: ${name}`) + } + + return match[1] +} + +function relativeLuminance(hex: string) { + const normalized = + hex.length === 4 + ? hex + .slice(1) + .split('') + .map((digit) => digit.repeat(2)) + .join('') + : hex.slice(1) + const channels = normalized.match(/.{2}/g)?.map((value) => Number.parseInt(value, 16) / 255) + + if (!channels || channels.length !== 3) { + throw new Error(`Invalid hex color: ${hex}`) + } + + const [red, green, blue] = channels.map((value) => + value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4, + ) + + return 0.2126 * red + 0.7152 * green + 0.0722 * blue +} + +function contrastRatio(foreground: string, background: string) { + const brighter = Math.max(relativeLuminance(foreground), relativeLuminance(background)) + const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background)) + + return (brighter + 0.05) / (darker + 0.05) +} + +describe('상태 색상 토큰', () => { + it.each(['red', 'amber', 'green'])( + '%s 상태 텍스트가 흰색과 옅은 상태 배경에서 WCAG AA 대비를 충족한다', + (color) => { + const foreground = token(`fowoco-${color}-600`) + const tint = token(`fowoco-${color}-50`) + + expect(contrastRatio(foreground, WHITE)).toBeGreaterThanOrEqual(4.5) + expect(contrastRatio(foreground, tint)).toBeGreaterThanOrEqual(4.5) + }, + ) +})