From a71ecbe0b203a2ddf8bb5849175dde052494c5a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Wed, 11 Nov 2026 14:37:19 +0900 Subject: [PATCH] docs(storybook): add admin text color swatches to typography --- src/foundation/foundation.css | 8 ++++ src/foundation/typography.stories.tsx | 67 +++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/foundation/foundation.css b/src/foundation/foundation.css index ef0f6aa..135394a 100644 --- a/src/foundation/foundation.css +++ b/src/foundation/foundation.css @@ -65,6 +65,14 @@ .hm-foundation .hm-typo-metrics span, .hm-foundation .hm-typo-metrics small { display: block; } .hm-foundation .hm-typo-metrics small { font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); } @media (min-width: 1100px) { .hm-foundation .hm-typo-tables { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +/* Typography Text Colors / Foundation Gray: same comparison table, a 28px swatch in the first column. Colors come from the row's token. */ +.hm-foundation .hm-text-color-table th, .hm-foundation .hm-text-color-table td { vertical-align: middle; } +.hm-foundation .hm-text-color-table tbody th { width: 20%; } +.hm-foundation .hm-text-color-table td:last-child { width: 30%; } +.hm-foundation .hm-text-color-table small { display: block; font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); font-weight: var(--hm-input-font-weight); } +.hm-foundation .hm-text-color-role { display: flex; align-items: center; gap: 12px; min-width: 0; } +.hm-foundation .hm-text-color-chip { flex: none; width: 28px; height: 28px; border: 1px solid var(--hm-sem-stroke-input-default); border-radius: var(--hm-radius-control-md); } +.hm-foundation .hm-text-color-sample { display: inline-block; max-width: 100%; padding: 2px 8px; border-radius: var(--hm-radius-control-md); } @media (max-width: 640px) { .hm-foundation .hm-typo-table thead { display: none; } .hm-foundation .hm-typo-table, .hm-foundation .hm-typo-table tbody, .hm-foundation .hm-typo-table tr, .hm-foundation .hm-typo-table th, .hm-foundation .hm-typo-table td { display: block; width: auto !important; } diff --git a/src/foundation/typography.stories.tsx b/src/foundation/typography.stories.tsx index 0dad925..66378b2 100644 --- a/src/foundation/typography.stories.tsx +++ b/src/foundation/typography.stories.tsx @@ -15,6 +15,66 @@ const textLabels = ["Page Header · title", "Card · title", "Page Header · des const textRows = typographyReferences.filter(item => textLabels.includes(item.label)) const controlRows = typographyReferences.filter(item => !textLabels.includes(item.label)) +// Admin text colors (Web / Chart / OrgChart; not SW). Swatches, samples and HEX read the current tokens; no color is hardcoded here. +const textColorRoles = [ + { role: "Primary", name: "기본 텍스트", token: "--hm-sem-fg-primary", alias: "--foreground", surface: "--hm-sem-bg-surface", surfaceLabel: "흰 면", sample: "제목과 본문 한맥 Hanmac 0123", usage: "페이지·카드·Dialog 제목, 본문, 입력값, 테이블 셀, 차트 값 · OrgChart 기본 글자(--hm-orgchart-fg-primary 기본값)" }, + { role: "Secondary", name: "보조 텍스트", token: "--hm-sem-fg-secondary", alias: "--muted-foreground", surface: "--hm-sem-bg-surface", surfaceLabel: "흰 면", sample: "설명과 캡션 한맥 Hanmac 0123", usage: "설명, Label, 캡션·요약·빈 상태, 차트 축·범례 · OrgChart 직위·보조 정보(--hm-orgchart-fg-secondary 기본값)" }, + { role: "Placeholder", name: "입력 안내", token: "--hm-sem-fg-placeholder", surface: "--hm-sem-bg-surface", surfaceLabel: "흰 면", sample: "검색어를 입력하세요", usage: "Input·Textarea·Select·Command·Combobox·Date Picker의 흰 입력 면 전용" }, + { role: "대비 예외", name: "베이지 면 Placeholder", token: "--hm-sem-fg-secondary", surface: "--hm-sem-bg-content", surfaceLabel: "베이지 면", sample: "읽기 전용 · 값 없음", usage: "투명 면 readonly Input의 Placeholder. 베이지 면에서는 Placeholder 색이 4.5:1 미만이라 Secondary를 사용" }, +] as const +// Palette steps: only some are wired to a text role. Strong Secondary / Tertiary Semantic tokens do not exist. +const grayPalette = [ + { step: "900", link: "Primary", linked: true }, + { step: "800", link: "팔레트만 · Semantic 없음(Strong Secondary 미정의)", linked: false }, + { step: "700", link: "Secondary · 대비 예외", linked: true }, + { step: "600", link: "Placeholder", linked: true }, + { step: "500", link: "팔레트만 · Disabled 텍스트 색 아님(Disabled는 opacity 방식)", linked: false }, +] as const +const grayToken = (step: string) => `--hm-sem-palette-gray-${step}` +const textColorProperties = [...new Set([...textColorRoles.flatMap(item => [item.token, item.surface]), ...grayPalette.map(item => grayToken(item.step))])] + +function toHex(value: string | undefined) { + if (!value) return "" + if (/^#[\da-f]{6}$/i.test(value)) return value.toUpperCase() + const rgb = value.match(/^rgb\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)\s*\)$/) + return rgb ? `#${rgb.slice(1).map(channel => Number(channel).toString(16).padStart(2, "0")).join("").toUpperCase()}` : value +} +// WCAG 2 contrast ratio of two #RRGGBB values; empty while a value is not resolved yet. +function contrast(foreground: string, background: string) { + if (!/^#[\da-f]{6}$/i.test(foreground) || !/^#[\da-f]{6}$/i.test(background)) return "" + const luminance = (hex: string) => [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16) / 255).map(channel => channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4).reduce((sum, channel, index) => sum + channel * [0.2126, 0.7152, 0.0722][index], 0) + const [light, dark] = [luminance(foreground), luminance(background)].sort((a, b) => b - a) + return `${((light + 0.05) / (dark + 0.05)).toFixed(2)}:1` +} + +function TextColorTable({ values }: { values: Record }) { + return + + {textColorRoles.map(item => { + const hex = toHex(values[item.token]) + return + + + + + + + })} +
RoleSampleHEX · ContrastTokenUsage
{item.sample}{hex}{item.surfaceLabel} {contrast(hex, toHex(values[item.surface]))}{item.token}{"alias" in item ? {item.alias} : null}{item.usage}
+} + +function GrayPaletteTable({ values }: { values: Record }) { + return + + {grayPalette.map(item => + + + + + )} +
StepHEXTokenText Role
{toHex(values[grayToken(item.step)])}{grayToken(item.step)}{item.link}
+} + function SpecimenRow({ label, size, line, weight }: TypographyReference & { weight?: string }) { const ref = useRef(null) const [metrics, setMetrics] = useState({ main: "", spacing: "" }) @@ -43,6 +103,7 @@ function SpecimenTable({ rows }: { rows: readonly TypographyReference[] }) { function TypographyReferencePage() { const values = useResolvedProperties(typographyProperties) + const colorValues = useResolvedProperties(textColorProperties) return

Noto Sans KR → 맑은 고딕 → 시스템 sans-serif--hm-font-family-base: {values["--hm-font-family-base"]}

@@ -57,6 +118,12 @@ function TypographyReferencePage() {
+ + + + + +
} // The sidebar name is "폰트"; `id` keeps the existing Story ID (foundation-typography--all) stable.