docs(storybook): add admin text color swatches to typography

This commit is contained in:
shkim4 committed 2026-11-11 14:37:19 +09:00
1 parent 0486cecd5c
commit a71ecbe0b2
2 files changed
+75

No files matched your search

+8
View File
@@ -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; }
+67
View File
@@ -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<string, string> }) {
return <table className="hm-typo-table hm-text-color-table">
<thead><tr><th scope="col">Role</th><th scope="col">Sample</th><th scope="col">HEX · Contrast</th><th scope="col">Token</th><th scope="col">Usage</th></tr></thead>
<tbody>{textColorRoles.map(item => {
const hex = toHex(values[item.token])
return <tr key={item.role} data-text-color-role={item.role}>
<th scope="row"><span className="hm-text-color-role"><span className="hm-text-color-chip" style={{ background: `var(${item.token})` }} aria-hidden="true" /><span><span>{item.role}</span><small className="hm-foundation-note">{item.name}</small></span></span></th>
<td><span className="hm-text-color-sample" lang="ko" style={{ color: `var(${item.token})`, background: `var(${item.surface})` }}>{item.sample}</span></td>
<td><span data-text-color-hex>{hex}</span><small className="hm-foundation-note">{item.surfaceLabel} {contrast(hex, toHex(values[item.surface]))}</small></td>
<td><code>{item.token}</code>{"alias" in item ? <small className="hm-foundation-note"><code>{item.alias}</code></small> : null}</td>
<td className="hm-foundation-note">{item.usage}</td>
</tr>
})}</tbody>
</table>
}
function GrayPaletteTable({ values }: { values: Record<string, string> }) {
return <table className="hm-typo-table hm-text-color-table">
<thead><tr><th scope="col">Step</th><th scope="col">HEX</th><th scope="col">Token</th><th scope="col">Text Role</th></tr></thead>
<tbody>{grayPalette.map(item => <tr key={item.step} data-gray-step={item.step} data-linked={item.linked || undefined}>
<th scope="row"><span className="hm-text-color-role"><span className="hm-text-color-chip" style={{ background: `var(${grayToken(item.step)})` }} aria-hidden="true" /><span>Gray {item.step}</span></span></th>
<td><span data-text-color-hex>{toHex(values[grayToken(item.step)])}</span></td>
<td><code>{grayToken(item.step)}</code></td>
<td className={item.linked ? undefined : "hm-foundation-note"}>{item.link}</td>
</tr>)}</tbody>
</table>
}
function SpecimenRow({ label, size, line, weight }: TypographyReference & { weight?: string }) {
const ref = useRef<HTMLDivElement>(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 <ReferencePage title="폰트">
<ReferenceSection title="Font Family">
<p className="hm-typo-family"><span>Noto Sans KR → 맑은 고딕 → 시스템 sans-serif</span><code className="hm-foundation-note">--hm-font-family-base: {values["--hm-font-family-base"]}</code></p>
@@ -57,6 +118,12 @@ function TypographyReferencePage() {
<ReferenceSection title="Typography Specimens">
<div className="hm-typo-tables"><SpecimenTable rows={textRows} /><SpecimenTable rows={controlRows} /></div>
</ReferenceSection>
<ReferenceSection title="Text Colors" note="Web · Chart · OrgChart 공통 텍스트 색입니다(SW 제외). 칩·샘플·HEX는 현재 토큰 값을 읽습니다.">
<TextColorTable values={colorValues} />
</ReferenceSection>
<ReferenceSection title="Foundation Gray" note="텍스트 역할에 연결된 단계와 팔레트에만 정의된 단계를 구분합니다.">
<GrayPaletteTable values={colorValues} />
</ReferenceSection>
</ReferencePage>
}
// The sidebar name is "폰트"; `id` keeps the existing Story ID (foundation-typography--all) stable.