import { useEffect, useRef, useState } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" import { typographyProperties, typographyReferences } from "./reference-data" import { ReferencePage, ReferenceSection, useResolvedProperties } from "./reference" type TypographyReference = (typeof typographyReferences)[number] const weights = [ { property: "--hm-input-font-weight", label: "Regular" }, { property: "--hm-font-weight-medium", label: "Medium" }, { property: "--hm-font-weight-bold", label: "Bold" }, ] // Left table: text styles. Right table: form controls. Together they cover every entry in typographyReferences. const textLabels = ["Page Header · title", "Card · title", "Page Header · description", "Label"] 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: "" }) useEffect(() => { let mounted = true void document.fonts.ready.then(() => { if (!mounted || !ref.current) return const style = getComputedStyle(ref.current) setMetrics({ main: `${style.fontSize} / ${style.lineHeight} / ${style.fontWeight}`, spacing: `letter-spacing: ${style.letterSpacing}` }) }) return () => { mounted = false } }, []) return {label}
한맥 Hanmac 0123
{metrics.main}{metrics.spacing} } function SpecimenTable({ rows }: { rows: readonly TypographyReference[] }) { return {rows.map(item => )}
ComponentSampleSize / Line / Weight
} 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"]}

{weights.map(({ property, label }) =>

{label} {values[property]}

한맥 Hanmac 0123

{property}
)}
} // The sidebar name is "폰트"; `id` keeps the existing Story ID (foundation-typography--all) stable. const meta = { title: "Foundation/폰트", id: "foundation-typography", parameters: { layout: "fullscreen" } } satisfies Meta export default meta export const All: StoryObj = { name: "폰트", render: () => }