Merge pull request 'style(storybook): refine foundation color chips and typography layout' (#44) from style/storybook-ui-refinements into main
Deploy Storybook / deploy (push) Successful in 1m25s

Reviewed-on: #44
This commit was merged in pull request #44.
This commit is contained in:
shkim4 committed 2026-11-12 16:44:28 +09:00
commit 3eedd898df
7 files changed
+213 -101

No files matched your search

+12
View File
@@ -1,5 +1,17 @@
# Working Context # Working Context
## 2026-11-12 — Foundation 한맥·가족사 컬러칩, 폰트 미니멀 타이포그래피 레이아웃 (브랜치 `style/storybook-ui-refinements`)
- 범위: `src/foundation/`(`color.stories.tsx`·`company-colors.stories.tsx`·`typography.stories.tsx`·`reference.tsx`·`foundation.css`)과 `docs/guidelines/brand-color-steps.md`. 토큰·HEX·`themes/*.json`·컴포넌트·패키지 공개 API·`reference-data.ts`·Story ID(`foundation-color--all`·`foundation-company-colors--all`·`foundation-typography--all`) 불변. 폰트 섹션 순서(Text Colors → Font Family → Font Weight → Typography Specimens) 불변.
- 공통 로직(`reference.tsx`): `colorHex()`(CSS 값 → HEX)와 `textTone()`(견본 글자색: `--hm-sem-fg-primary` → 흰색 `--hm-color-text-inverse` → 검정 `--hm-sem-palette-black` 중 배경 대비 4.5:1을 처음 충족하는 토큰, `data-text-tone`)을 한맥·가족사·Text Colors가 공유.
- 한맥 컬러: 3그룹 24칩을 연속형 컬러 띠로 표시(팔레트 섹션 폭 기준 container query 1040px 이상 8열, 520px 이상 4열, 미만 2열, 칩 간격 0, 줄 양 끝만 `--hm-radius-control-md`). 견본 120px·Padding 12 안에 단계명(14/700)·용도(Default `기본색` 12/500, Light 4 `보더 하이라이트용` 12/400) 왼쪽 위, HEX 12 오른쪽 아래. 어두운 글자 칩은 용도·HEX 500. 이전 Default 강조 테두리(`data-base`)·칩 외곽 박스·Light 4 구분선 제거. 24색 모두 4.5:1 이상(최저 브라운 Light 2 4.52), 검정 글자는 그린 Light 3·오렌지 Default만.
- 가족사 컬러: 회사 배치(3 → 2 → 1열, 2/2/3 줄)·`companyColorPresets` 데이터 유지. 회사마다 3칸 Grid(간격 12px)에 독립 칩(120px·Padding 12·Radius 4), 이름(메인·서브 N) 왼쪽 위·HEX 오른쪽 아래, 글자색은 `textTone()`. 메인 강조 테두리 없음(이름으로만 구분). 최저 대비 4.89.
- Text Colors: 가족사 컬러와 같은 개별 칩으로 변경. 행에 `data-text-colors`를 붙여 한맥·가족사 공통 견본 규칙(`:is([data-palette], [data-company-id], [data-text-colors])`: 120px·Padding 12·Radius 4, 이름 + 용도 12/400 왼쪽 위, HEX 12 오른쪽 아래)을 재사용하고(이름은 Text Colors만 14/500: 어두운 회색 위 흰 Bold가 퍼져 보여 사용자 결정, 한맥·가족사는 700 유지), 글자색은 공통 `textTone()`(fg-primary → 흰색 → 검정 중 4.5:1 첫 번째). 4개 칩 모두 흰색(대비 16.1 / 6.19 / 4.61 / 6.19). HEX는 공통 `colorHex()`로 통일(로컬 `toHex` 삭제). 그리드는 기존 4열·최소 150px 유지, 간격 12px.
- 공통 행 `.hm-typo-row`(Font Family 1행, Font Weight 3행, Specimens 10행): 정보(왼쪽) | 미리보기(오른쪽), 세로 중앙 정렬. 행 전환은 viewport가 아니라 자기 열 너비 기준(container query, `.hm-typo-rows`·`.hm-typo-group`): 440px 이상 정보 열 `min(240px, 40%)` + 24px, 미만 1열·미리보기 먼저(`order`). 1440px에서 Family·Specimens 왼쪽 열 미리보기 x=340, Weight·Specimens 오른쪽 열 x=1012로 일치(Specimens 열 간격 40px = 섹션 간격 24 + 들여쓰기 16).
- Font Family·Font Weight는 공통 `StorySectionGrid`(`.hm-typo-type-grid`)로 720px 이상 2열, 미만 1열(800px에서는 각 열 304px라 내부 1열). Font Family: Primary Typeface(12 보조)·Noto Sans KR(700) | `--hm-font-family-base` 미리보기(28/36) → CSS 변수(보조, 값에 대체 폰트 순서 포함; 별도 Fallback 문구는 사용자 요청으로 삭제). Font Weight: 이름+값·토큰 | 20/28 미리보기, 행 위·아래 12px, 행 사이에만 얇은 구분선.
- Typography Specimens: 표 2개(`.hm-typo-table`)를 컴포넌트 그룹(Page Header·Card·Label / Input·Button Standard) 목록으로 대체. 정보는 항목명(12/500)·계산값 `Size / Line / Weight · letter-spacing` 한 줄(모두 보조색), 샘플은 실제 크기(Primary). 구분선은 행마다 윗선 1줄(`--hm-sem-stroke-input-default`, 첫 줄이 그룹 제목 밑줄, 마지막 행 아래 없음), 그룹 제목 → 8px, 그룹 간 24px. 1100px 이상 텍스트/입력 2열. `data-typography-sample`(전체 라벨)·`data-typography-metrics` 유지. 그룹화로 표시 순서만 Page Header description이 Card title 앞으로 이동.
- 검증 메모: 값 표시(`useResolvedProperties`)는 `requestAnimationFrame`을 쓰므로 Storybook 미리보기 패널이 숨겨진 상태에서는 HEX·굵기 값·토큰 값이 비어 보인다(코드 회귀 아님).
## 2026-11-11 — Storybook 섹션 간격·정보 영역 들여쓰기 공통화, Foundation 폰트 위계 (main 기준, 미커밋) ## 2026-11-11 — Storybook 섹션 간격·정보 영역 들여쓰기 공통화, Foundation 폰트 위계 (main 기준, 미커밋)
- 공통 프레임 `src/components/story-page.css`: H2 16/24/700 → 12px → 구분선 → 16px → 콘텐츠(섹션 헤더 padding-bottom 8→12, margin-bottom 12→16), 섹션 사이 `--sb-page-section-gap` 24→28px. `StorySectionGrid`는 row gap만 이 변수를 따르고 column gap 24px은 유지(Chart 2열 미리보기 폭 불변). SW `sw-story-stack`·Footer 너비 행, Foundation Layout도 변수로 28px. - 공통 프레임 `src/components/story-page.css`: H2 16/24/700 → 12px → 구분선 → 16px → 콘텐츠(섹션 헤더 padding-bottom 8→12, margin-bottom 12→16), 섹션 사이 `--sb-page-section-gap` 24→28px. `StorySectionGrid`는 row gap만 이 변수를 따르고 column gap 24px은 유지(Chart 2열 미리보기 폭 불변). SW `sw-story-stack`·Footer 너비 행, Foundation Layout도 변수로 28px.
+2 -2
View File
@@ -35,6 +35,6 @@ Orange Dark 1·2의 원본 Needs Review 상태와 [이전 검수 보고서](../r
페이지 제목은 `Hanmac Color`(메뉴 `Foundation > Hanmac Color`)이며 별도 소개 문구는 표시하지 않는다. 기존 Story ID `foundation-color--all`은 `meta.id`로 유지한다. 과거 `한맥 브랜드 컬러` 소개 문구는 당시 이력이다. 그룹 설명의 단계 순서 나열 문구는 제공하지 않으며, 개별 컬러칩의 단계명과 HEX는 유지한다. 페이지 제목은 `Hanmac Color`(메뉴 `Foundation > Hanmac Color`)이며 별도 소개 문구는 표시하지 않는다. 기존 Story ID `foundation-color--all`은 `meta.id`로 유지한다. 과거 `한맥 브랜드 컬러` 소개 문구는 당시 이력이다. 그룹 설명의 단계 순서 나열 문구는 제공하지 않으며, 개별 컬러칩의 단계명과 HEX는 유지한다.
[Hanmac Color 대표 Story](../../src/foundation/color.stories.tsx)는 한맥그린·한맥브라운·한맥오렌지 3개 그룹에 각 8개, 총 24개 컬러칩만 표시한다. Green 8/8·Brown 8/8·Orange 8/8 확정이며 단계명·현재 HEX와 Default 기본색 강조를 유지한다. 2026-10-11 최종 표시 요청에 따라 Brown Deep, Orange Light·Mid·Dark·Deep의 별도 표시, Accent·Semantic·Gradients 섹션과 개발용 Token·CSS Variable·Runtime Key 설명은 제거한다. Legacy Button 색상과 Accent·Semantic·Gradient의 실제 Key·값·소비처는 보존한다. 기존 Component mapping을 새 값에 연결하지 않으며 public Key·Property·type·Default·API·schemaVersion 1과 서비스 partial JSON 호환성을 유지한다. [Runtime Theme](runtime-theme.md)의 geometry/상태 권한과 Designer의 기존 17개 상위 역할 입력 UI는 확대하지 않는다. 한맥오렌지의 미확정 브랜드 단계는 없다. [표시 정리 검증 기록](../reports/foundation-color-display-cleanup-2026-10-11.md). [Hanmac Color 대표 Story](../../src/foundation/color.stories.tsx)는 한맥그린·한맥브라운·한맥오렌지 3개 그룹에 각 8개, 총 24개 컬러칩만 표시한다. Green 8/8·Brown 8/8·Orange 8/8 확정이며 단계명·현재 HEX와 Default 기본색 표시를 유지한다. 2026-11-12 표시 정리로 세 그룹은 같은 Grid(팔레트 섹션 폭 1040px 이상 8열, 520px 이상 4열×2줄, 그 미만 2열×4줄, 콘텐츠 영역 전체 너비를 열 수만큼 균등 분할)를 쓰며 견본 사이 간격 없이 이어진 연속형 컬러 띠로 표시한다. 줄마다 첫 견본 왼쪽과 마지막 견본 오른쪽에만 `--hm-radius-control-md`를 적용하고 내부 경계는 직각이며 칩 외곽 박스·Default 테두리·구분선·그림자는 없다. 견본 높이는 120px이며 단계명(14px Bold)·용도 설명·HEX(12px)를 견본 내부(Padding 12px)에 표시한다. 단계명과 용도 설명은 왼쪽 상단, HEX는 오른쪽 하단이다. Default는 단계명 아래 `기본색`(12px Medium), Light 4는 `보더 하이라이트용`(12px Regular)을 표시하며 Badge·배경·테두리는 없다. 어두운 글자(`--hm-sem-fg-primary`·검정)를 쓰는 칩은 용도 설명과 HEX를 Medium(500)으로 표시해 채도 높은 중간 명도 배경에서 획이 흐려 보이지 않게 한다. 글자색은 `--hm-sem-fg-primary` → 흰색(`--hm-color-text-inverse`) → 검정(`--hm-sem-palette-black`) 순으로 견본 배경 대비 4.5:1을 처음 충족하는 토큰을 쓰며(검정은 한맥그린 Light 3·한맥오렌지 Default만 해당) 24색 모두 WCAG AA 4.5:1 이상이다(최저 브라운 Light 2 흰색 4.52:1). 2026-10-11 최종 표시 요청에 따라 Brown Deep, Orange Light·Mid·Dark·Deep의 별도 표시, Accent·Semantic·Gradients 섹션과 개발용 Token·CSS Variable·Runtime Key 설명은 제거한다. Legacy Button 색상과 Accent·Semantic·Gradient의 실제 Key·값·소비처는 보존한다. 기존 Component mapping을 새 값에 연결하지 않으며 public Key·Property·type·Default·API·schemaVersion 1과 서비스 partial JSON 호환성을 유지한다. [Runtime Theme](runtime-theme.md)의 geometry/상태 권한과 Designer의 기존 17개 상위 역할 입력 UI는 확대하지 않는다. 한맥오렌지의 미확정 브랜드 단계는 없다. [표시 정리 검증 기록](../reports/foundation-color-display-cleanup-2026-10-11.md).
[Company Colors Story](../../src/foundation/company-colors.stories.tsx)는 Foundation > Hanmac Color와 별도 메뉴(Hanmac Color 바로 아래)이며 7개 계열사·14개 컬러를 회사별로 표시한다(별도 소개 문구 없음). 데이터는 OrgChart의 기존 `companyColorPresets`를 그대로 읽고(메인 → 서브 순서, 메인은 Default 강조 재사용) HEX를 중복 정의하지 않는다. Theme 토큰·`themes/*.json`·공개 계약은 변경하지 않으며 Foundation > Hanmac Color의 3그룹 24칩은 그대로다. [Company Colors Story](../../src/foundation/company-colors.stories.tsx)는 Foundation > Hanmac Color와 별도 메뉴(Hanmac Color 바로 아래)이며 7개 계열사·14개 컬러를 회사별로 표시한다(별도 소개 문구 없음). 데이터는 OrgChart의 기존 `companyColorPresets`를 그대로 읽고(메인 → 서브 순서) HEX를 중복 정의하지 않는다. 회사 그룹은 콘텐츠 폭에 따라 3열(2/2/3 줄 배치, 장헌·장헌산업은 1열 시작) → 2열 → 1열이며, 컬러칩은 회사마다 3칸 Grid(간격 12px)에 독립된 직사각형(`--hm-radius-control-md`, 높이 120px, Padding 12px)으로 놓여 컬러가 적은 회사도 칩 폭은 1/3이다. 칩 안에는 Hanmac Color와 같은 방식으로 이름(`메인`·`서브 1`·`서브 2`, 14px Bold)을 왼쪽 상단, HEX(12px)를 오른쪽 하단에 두고, 글자색도 같은 대비 계산(`--hm-sem-fg-primary` → 흰색 → 검정, 4.5:1 첫 충족)과 어두운 글자일 때 HEX Medium을 쓴다. 메인 외곽 테두리·Badge·배경 강조는 없으며 메인·서브는 이름으로만 구분한다. Theme 토큰·`themes/*.json`·공개 계약은 변경하지 않으며 Foundation > Hanmac Color의 3그룹 24칩은 그대로다.
+16 -16
View File
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { runtimeThemeContract, type RuntimeThemeToken } from "@hm-design/ui/theme" import { runtimeThemeContract, type RuntimeThemeToken } from "@hm-design/ui/theme"
import { ReferencePage, ReferenceSection, useResolvedProperties } from "./reference" import { colorHex, ReferencePage, ReferenceSection, textTone, textToneProperties, useResolvedProperties } from "./reference"
// Keep the confirmed eight stages; presentation changes never alter Theme values. // Keep the confirmed eight stages; presentation changes never alter Theme values.
const stages = ["light-4", "light-3", "light-2", "light-1", "default", "dark-1", "dark-2", "dark-3"] as const const stages = ["light-4", "light-3", "light-2", "light-1", "default", "dark-1", "dark-2", "dark-3"] as const
@@ -11,25 +11,25 @@ const palettes = [
] as const ] as const
type ColorStage = typeof stages[number] type ColorStage = typeof stages[number]
type PalettePrefix = typeof palettes[number]["prefix"] type PalettePrefix = typeof palettes[number]["prefix"]
const stageLabel = (stage: ColorStage) => stage === "default" ? "Default · 기본색" : stage === "light-4" ? "Light 4 · 보더 하이라이트용" : stage.replaceAll("-", " ").replace(/^./, letter => letter.toUpperCase()) // Stage names stay plain; Default and Light 4 add a role line under the name (ColorChip).
const stageLabel = (stage: ColorStage) => stage.replaceAll("-", " ").replace(/^./, letter => letter.toUpperCase())
const stageNote: Partial<Record<ColorStage, string>> = { "light-4": "보더 하이라이트용", default: "기본색" }
const paletteToken = (prefix: PalettePrefix, stage: ColorStage) => `color.palette.${prefix}-${stage}` as RuntimeThemeToken const paletteToken = (prefix: PalettePrefix, stage: ColorStage) => `color.palette.${prefix}-${stage}` as RuntimeThemeToken
const colorProperties = palettes.flatMap(palette => stages.map(stage => runtimeThemeContract[paletteToken(palette.prefix, stage)].property)) // Palette values plus the swatch text candidates (textTone, reference.tsx).
const colorProperties = [...palettes.flatMap(palette => stages.map(stage => runtimeThemeContract[paletteToken(palette.prefix, stage)].property)), ...textToneProperties]
// Read the current CSS value rather than duplicating Theme HEX values.
function colorHex(value: string | undefined) {
if (value === undefined) return "확인 중"
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
}
function ColorChip({ prefix, stage, values }: { prefix: PalettePrefix; stage: ColorStage; values: Record<string, string> }) { function ColorChip({ prefix, stage, values }: { prefix: PalettePrefix; stage: ColorStage; values: Record<string, string> }) {
const token = paletteToken(prefix, stage) const token = paletteToken(prefix, stage)
const definition = runtimeThemeContract[token] const definition = runtimeThemeContract[token]
return <article className="hm-brand-color-chip" data-color-stage={stage} data-color-token={token} data-base={stage === "default" ? "" : undefined}> const hex = colorHex(values[definition.property])
<div className="hm-foundation-swatch" style={{ background: `var(${definition.property})` }} /> return <article className="hm-brand-color-chip" data-color-stage={stage} data-color-token={token} data-text-tone={textTone(hex, values)}>
<h3>{stageLabel(stage)}</h3><p data-color-value>{colorHex(values[definition.property])}</p> <div className="hm-foundation-swatch" style={{ background: `var(${definition.property})` }}>
<div className="hm-color-name">
<h3>{stageLabel(stage)}</h3>
{stageNote[stage] && <p className="hm-color-note">{stageNote[stage]}</p>}
</div>
<p data-color-value>{hex}</p>
</div>
</article> </article>
} }
@@ -37,7 +37,7 @@ function ColorReferencePage() {
const values = useResolvedProperties(colorProperties) const values = useResolvedProperties(colorProperties)
return <ReferencePage title="한맥 컬러"> return <ReferencePage title="한맥 컬러">
{palettes.map(palette => <ReferenceSection key={palette.name} title={palette.name} note=""> {palettes.map(palette => <ReferenceSection key={palette.name} title={palette.name} note="">
<div className="hm-brand-color-row">{stages.map(stage => <ColorChip key={stage} prefix={palette.prefix} stage={stage} values={values} />)}</div> <div className="hm-brand-color-row" data-palette={palette.prefix}>{stages.map(stage => <ColorChip key={stage} prefix={palette.prefix} stage={stage} values={values} />)}</div>
</ReferenceSection>)} </ReferenceSection>)}
</ReferencePage> </ReferencePage>
} }
+9 -6
View File
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { companyColorPresets } from "@hm-design/org-chart" import { companyColorPresets } from "@hm-design/org-chart"
import { ReferencePage, ReferenceSection } from "./reference" import { ReferencePage, ReferenceSection, textTone, textToneProperties, useResolvedProperties } from "./reference"
// Company colors come from the existing OrgChart presets; HEX values are never duplicated here. // Company colors come from the existing OrgChart presets; HEX values are never duplicated here.
// The chip markup and classes are shared with Foundation/Color (foundation.css). // The chip markup and classes are shared with Foundation/Color (foundation.css).
@@ -14,20 +14,23 @@ const companies = [
...presetCompanies.filter(company => !listedIds.includes(company.companyId)), ...presetCompanies.filter(company => !listedIds.includes(company.companyId)),
] ]
function CompanyColorChip({ label, hex, main }: { label: string; hex: string; main: boolean }) { // Same swatch content as Hanmac Color (name top left, HEX bottom right, text tone by contrast); main and sub differ only by the name.
return <article className="hm-brand-color-chip" data-company-color={hex} data-base={main ? "" : undefined}> function CompanyColorChip({ label, hex, values }: { label: string; hex: string; values: Record<string, string> }) {
<div className="hm-foundation-swatch" style={{ background: hex }} /> return <article className="hm-brand-color-chip" data-company-color={hex} data-text-tone={textTone(hex.toUpperCase(), values)}>
<div className="hm-foundation-swatch" style={{ background: hex }}>
<h3>{label}</h3><p data-color-value>{hex.toUpperCase()}</p> <h3>{label}</h3><p data-color-value>{hex.toUpperCase()}</p>
</div>
</article> </article>
} }
function CompanyColorsPage() { function CompanyColorsPage() {
const values = useResolvedProperties(textToneProperties)
return <ReferencePage title="가족사 컬러"> return <ReferencePage title="가족사 컬러">
<div className="hm-company-grid"> <div className="hm-company-grid">
{companies.map(company => <ReferenceSection key={company.companyId} title={company.companyName} note="" id={`company-${company.companyId}`}> {companies.map(company => <ReferenceSection key={company.companyId} title={company.companyName} note="" id={`company-${company.companyId}`}>
<div className="hm-brand-color-row" data-company-id={company.companyId}> <div className="hm-brand-color-row" data-company-id={company.companyId}>
<CompanyColorChip label="메인" hex={company.primary} main /> <CompanyColorChip label="메인" hex={company.primary} values={values} />
{company.secondary.map((hex, index) => <CompanyColorChip key={hex} label={`서브 ${index + 1}`} hex={hex} main={false} />)} {company.secondary.map((hex, index) => <CompanyColorChip key={hex} label={`서브 ${index + 1}`} hex={hex} values={values} />)}
</div> </div>
</ReferenceSection>)} </ReferenceSection>)}
</div> </div>
+95 -46
View File
@@ -26,13 +26,45 @@
.hm-foundation-radius { width: 100%; height: 64px; margin-bottom: 16px; border: 1px solid var(--hm-sem-stroke-default); background: var(--hm-sem-bg-content); } .hm-foundation-radius { width: 100%; height: 64px; margin-bottom: 16px; border: 1px solid var(--hm-sem-stroke-default); background: var(--hm-sem-bg-content); }
.hm-brand-color-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 8px; } .hm-brand-color-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 8px; }
.hm-brand-color-chip { flex: 1 0 110px; min-width: 0; padding: 8px; border: 1px solid var(--hm-sem-stroke-default); } .hm-brand-color-chip { flex: 1 0 110px; min-width: 0; padding: 8px; border: 1px solid var(--hm-sem-stroke-default); }
.hm-brand-color-chip[data-base] { border: 2px solid var(--hm-sem-fg-primary); font-weight: var(--hm-font-weight-bold); }
/* Hanmac Color only (data-color-stage): wider gap (8px + 16px extra = 24px) between Light 4 and Light 3 with a divider centered in it. */
.hm-brand-color-chip[data-color-stage="light-3"] { --hm-color-divider-extra: 16px; position: relative; margin-left: var(--hm-color-divider-extra); }
.hm-brand-color-chip[data-color-stage="light-3"]::before { content: ""; position: absolute; top: 0; bottom: 0; left: calc(-1px - 0.5px - (8px + var(--hm-color-divider-extra)) / 2); width: 1px; background: var(--hm-sem-stroke-default); pointer-events: none; }
.hm-brand-color-chip h3, .hm-brand-color-chip p { margin: 8px 0 0; font-size: 13px; } .hm-brand-color-chip h3, .hm-brand-color-chip p { margin: 8px 0 0; font-size: 13px; }
/* Color reference additions only; preserve the existing palette row and Default emphasis. */ /* Hanmac Color (rows with data-palette). The swatch content rules (chip reset, swatch text, text tone) are shared with Company Colors
(data-company-id) and Typography Text Colors (data-text-colors).
Continuous spectrum: the swatches touch (column gap 0) across the full section width and only each line's first/last swatch
gets the outer radius. Columns follow the section width (container query): 8 from 1040px, 4 x 2 rows from 520px, otherwise
2 x 4 rows, so a chip stays at least ~130px wide and every text line fits inside the 120px swatch. Each line is its own band. */
.hm-foundation .sb-page-section-body:has(> .hm-brand-color-row[data-palette]) { container-type: inline-size; }
.hm-foundation .hm-brand-color-row[data-palette] { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 0; overflow: visible; padding-bottom: 0; }
.hm-foundation .hm-brand-color-row:is([data-palette], [data-company-id], [data-text-colors]) .hm-brand-color-chip { min-width: 0; padding: 0; border: 0; border-radius: 0; font-weight: inherit; word-break: keep-all; }
/* Name and role line at the top left, HEX at the bottom right, all inside the swatch. Text tone comes from the Story
(data-text-tone: fg-primary first, then white, then black, whichever first reaches 4.5:1); no shadows or backing plates. */
.hm-foundation .hm-brand-color-row:is([data-palette], [data-company-id], [data-text-colors]) .hm-foundation-swatch { display: flex; flex-direction: column; justify-content: space-between; height: 120px; margin: 0; padding: 12px; border: 0; border-radius: 0; color: var(--hm-color-text-inverse); }
.hm-foundation .hm-brand-color-row:is([data-palette], [data-company-id], [data-text-colors]) .hm-brand-color-chip[data-text-tone="primary"] .hm-foundation-swatch { color: var(--hm-sem-fg-primary); }
.hm-foundation .hm-brand-color-row:is([data-palette], [data-company-id], [data-text-colors]) .hm-brand-color-chip[data-text-tone="black"] .hm-foundation-swatch { color: var(--hm-sem-palette-black); }
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip:nth-child(2n+1) .hm-foundation-swatch { border-radius: var(--hm-radius-control-md) 0 0 var(--hm-radius-control-md); }
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip:nth-child(2n) .hm-foundation-swatch { border-radius: 0 var(--hm-radius-control-md) var(--hm-radius-control-md) 0; }
@container (min-width: 520px) {
.hm-foundation .hm-brand-color-row[data-palette] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip:nth-child(n) .hm-foundation-swatch { border-radius: 0; }
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip:nth-child(4n+1) .hm-foundation-swatch { border-radius: var(--hm-radius-control-md) 0 0 var(--hm-radius-control-md); }
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip:nth-child(4n) .hm-foundation-swatch { border-radius: 0 var(--hm-radius-control-md) var(--hm-radius-control-md) 0; }
}
@container (min-width: 1040px) {
.hm-foundation .hm-brand-color-row[data-palette] { grid-template-columns: repeat(8, minmax(0, 1fr)); }
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip:nth-child(4n+1) .hm-foundation-swatch,
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip:nth-child(4n) .hm-foundation-swatch { border-radius: 0; }
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip:first-child .hm-foundation-swatch { border-radius: var(--hm-radius-control-md) 0 0 var(--hm-radius-control-md); }
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip:last-child .hm-foundation-swatch { border-radius: 0 var(--hm-radius-control-md) var(--hm-radius-control-md) 0; }
}
/* Name 14 Bold; role line 12 Regular (Default "기본색" 12 Medium); HEX 12. Dark text on a swatch reads thinner than white,
so its role line and HEX use Medium. */
.hm-foundation .hm-brand-color-row:is([data-palette], [data-company-id], [data-text-colors]) h3 { margin: 0; font-size: var(--hm-label-font-size); line-height: var(--hm-label-line-height); font-weight: var(--hm-font-weight-bold); color: inherit; }
.hm-foundation .hm-brand-color-row:is([data-palette], [data-text-colors]) .hm-color-note { margin: 0; font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); font-weight: var(--hm-input-font-weight); color: inherit; }
.hm-foundation .hm-brand-color-row[data-palette] .hm-brand-color-chip[data-color-stage="default"] .hm-color-note,
.hm-foundation .hm-brand-color-row:is([data-palette], [data-company-id], [data-text-colors]) .hm-brand-color-chip:is([data-text-tone="primary"], [data-text-tone="black"]) :is(.hm-color-note, p[data-color-value]) { font-weight: var(--hm-font-weight-medium); }
.hm-foundation .hm-brand-color-row:is([data-palette], [data-company-id], [data-text-colors]) p[data-color-value] { align-self: flex-end; margin: 0; font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); color: inherit; font-variant-numeric: tabular-nums; }
/* Color reference additions only; preserve the existing palette row. */
.hm-foundation .hm-color-gradient-preview { height: 112px; } .hm-foundation .hm-color-gradient-preview { height: 112px; }
.hm-foundation .hm-color-gradient-stops { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; } .hm-foundation .hm-color-gradient-stops { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.hm-foundation .hm-color-gradient-stops .hm-brand-color-chip { flex: 1 1 110px; } .hm-foundation .hm-color-gradient-stops .hm-brand-color-chip { flex: 1 1 110px; }
@@ -41,54 +73,71 @@
.hm-foundation .hm-color-foreground-sample { padding: 16px; background: var(--hm-sem-bg-surface); } .hm-foundation .hm-color-foreground-sample { padding: 16px; background: var(--hm-sem-bg-surface); }
/* Company Colors: two-level grid. Columns follow the Story content width (not the viewport): companies 3 -> 2 -> 1 (min 316px each), /* Company Colors: two-level grid. Columns follow the Story content width (not the viewport): companies 3 -> 2 -> 1 (min 316px each),
chips up to 3 per row (min 100px each, 3 -> 2 -> 1). Chip size stays at one third even when a company has fewer chips. */ chips always in three equal tracks 12px apart, so a chip stays one third wide even when a company has fewer chips (min ~97px).
Swatch content comes from the shared Hanmac rules; each chip is a separate rectangle with the outer radius and no border. */
.hm-foundation .hm-company-grid { display: grid; gap: 48px 32px; grid-template-columns: repeat(auto-fill, minmax(max(316px, (100% - 64px) / 3), 1fr)); } .hm-foundation .hm-company-grid { display: grid; gap: 48px 32px; grid-template-columns: repeat(auto-fill, minmax(max(316px, (100% - 64px) / 3), 1fr)); }
.hm-foundation .hm-company-grid > .sb-page-section { min-width: 0; margin-top: 0; } .hm-foundation .hm-company-grid > .sb-page-section { min-width: 0; margin-top: 0; }
/* Row starts for the 2 / 2 / 3 company layout (한맥·삼안 / 장헌·피티씨 / 장헌산업·한라산업개발·바론컨설턴트). */ /* Row starts for the 2 / 2 / 3 company layout (한맥·삼안 / 장헌·피티씨 / 장헌산업·한라산업개발·바론컨설턴트). */
.hm-foundation .hm-company-grid > #company-jangheon, .hm-foundation .hm-company-grid > #company-jangheon,
.hm-foundation .hm-company-grid > #company-jangheon-industry { grid-column-start: 1; } .hm-foundation .hm-company-grid > #company-jangheon-industry { grid-column-start: 1; }
.hm-foundation .hm-brand-color-row[data-company-id] { display: grid; gap: 8px; overflow: visible; padding-bottom: 0; grid-template-columns: repeat(auto-fill, minmax(max(100px, (100% - 16px) / 3), 1fr)); } .hm-foundation .hm-brand-color-row[data-company-id] { display: grid; gap: 12px; overflow: visible; padding-bottom: 0; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.hm-foundation .hm-brand-color-row:is([data-company-id], [data-text-colors]) .hm-foundation-swatch { border-radius: var(--hm-radius-control-md); }
/* Typography: compact specification page. Font Family as family + fallback over the CSS variable, Font Weight as three plain columns (no card box; /* Typography: minimal type specimen page (presentation only, existing tokens; no card box, outer border or shadow).
the section divider already frames them), Specimens as comparison tables (two columns on wide screens, stacked rows on narrow screens). Section titles, rhythm (title → 12px → divider → 16px → content, sections 28px apart) and the content indent (.sb-page-indent on
Document text levels (presentation only, existing tokens): item name 14/700 primary, description 14/400 secondary, Font Family, Font Weight and Specimens; Text Colors chips stay on the edge) come from the common Story page (story-page.css).
token / HEX / extra info 12/18/400 secondary (.hm-typo-token). Section titles stay on the common Story page H2 (16/24/700). */ Text levels: label / token 12/18/400 secondary, family and weight names 14/700 primary, specimen name and metrics 12/18 secondary;
/* Section rhythm (title → 12px → divider → 16px → content, sections 28px apart) and the content indent (.sb-page-indent on Font Family, previews and samples carry the largest type. */
Font Weight and Specimens; Text Colors chips stay on the edge) come from the common Story page (story-page.css). */ /* Font Family and Font Weight side by side (StorySectionGrid) from 720px, stacked below. */
.hm-foundation .hm-typo-family { display: grid; gap: 4px; margin: 0; } .hm-foundation .hm-typo-type-grid { grid-template-columns: minmax(0, 1fr); }
.hm-foundation .hm-typo-family p { margin: 0; } @media (min-width: 720px) { .hm-foundation .hm-typo-type-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.hm-foundation .hm-typo-family strong { font-weight: var(--hm-font-weight-bold); } /* Shared row (Font Family, each Font Weight item, each Specimen): information on the left, preview on the right, both vertically
.hm-foundation .hm-typo-token { font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); } centered. The row follows the width of its own column (container query on the Family / Weight content and on each Specimen
.hm-foundation .hm-typo-weights { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 12px 24px; } group), not the viewport: from 440px the information column is min(240px, 40%), so it never squeezes the preview; below that
.hm-foundation .hm-typo-weight { min-width: 0; } the row is one column with the preview first. */
.hm-foundation .hm-typo-weight p { margin: 0; } .hm-foundation :is(.hm-typo-rows, .hm-typo-group) { container-type: inline-size; }
.hm-foundation .hm-typo-weight-preview { margin: 6px 0 4px !important; font-size: var(--hm-card-title-font-size); line-height: var(--hm-card-title-line-height); } .hm-foundation .hm-typo-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px 24px; align-items: center; }
.hm-foundation .hm-typo-tables { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px 32px; align-items: start; } .hm-foundation .hm-typo-row :is(p, code) { margin: 0; }
.hm-foundation .hm-typo-table { width: 100%; border-collapse: collapse; } .hm-foundation :is(.hm-typo-row-meta, .hm-typo-row-preview) { display: grid; gap: 2px; min-width: 0; }
.hm-foundation .hm-typo-table th, .hm-foundation .hm-typo-table td { padding: 12px 16px 12px 0; border-bottom: 1px solid var(--hm-sem-stroke-input-default); text-align: left; vertical-align: baseline; font-weight: var(--hm-input-font-weight); } .hm-foundation .hm-typo-row-preview { order: -1; }
.hm-foundation .hm-typo-table thead th { color: var(--hm-sem-fg-secondary); font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); } .hm-foundation .hm-typo-token, .hm-foundation .hm-typo-kicker { font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); }
.hm-foundation .hm-typo-table tbody th { width: 27%; font-weight: var(--hm-font-weight-bold); } @container (min-width: 440px) {
.hm-foundation .hm-typo-table td:last-child { width: 26%; padding-right: 0; } .hm-foundation .hm-typo-row { grid-template-columns: min(240px, 40%) minmax(0, 1fr); row-gap: 4px; }
.hm-foundation .hm-typo-metrics span, .hm-foundation .hm-typo-metrics small { display: block; font-variant-numeric: tabular-nums; } .hm-foundation .hm-typo-row-preview { order: 0; }
.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 reuse the Hanmac color chips (.hm-brand-color-chip). Four equal tracks on wide screens;
narrow screens fall back to 150px tracks (2 columns at 390px) and wrap the text inside the chip instead of scrolling. */
.hm-foundation .hm-text-color-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(max(150px, (100% - 24px) / 4), 1fr)); overflow: visible; padding-bottom: 0; }
/* Text Colors chip content only (the shared 13px chip text of the color pages is untouched): role name → usage → HEX as three levels.
Grid rows stretch, so the four chips keep one height; keep-all wraps the long Readonly usage between words. */
.hm-foundation .hm-text-color-row { grid-auto-rows: 1fr; }
.hm-foundation .hm-text-color-row .hm-brand-color-chip { padding: 12px; word-break: keep-all; }
.hm-foundation .hm-text-color-row .hm-foundation-swatch { margin-bottom: 12px; }
.hm-foundation .hm-text-color-row h3 { margin: 0; font-size: var(--hm-label-font-size); line-height: var(--hm-label-line-height); font-weight: var(--hm-font-weight-bold); color: var(--hm-sem-fg-primary); }
.hm-foundation .hm-text-color-row p { margin: 4px 0 0; font-size: var(--hm-label-font-size); line-height: var(--hm-label-line-height); color: var(--hm-sem-fg-secondary); }
.hm-foundation .hm-text-color-row p[data-color-value] { margin-top: 4px; font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); font-variant-numeric: tabular-nums; }
@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; }
.hm-foundation .hm-typo-table tr { padding: 12px 0; border-bottom: 1px solid var(--hm-sem-stroke-input-default); }
.hm-foundation .hm-typo-table th, .hm-foundation .hm-typo-table td { padding: 2px 0; border-bottom: 0; }
} }
/* Font Family: Primary Typeface · family name | preview in --hm-font-family-base → CSS variable (its value lists the fallbacks). */
.hm-foundation .hm-typo-kicker { color: var(--hm-sem-fg-secondary); }
.hm-foundation .hm-typo-family-name { font-weight: var(--hm-font-weight-bold); }
.hm-foundation .hm-typo-row .hm-typo-family-preview { margin-bottom: 4px; font-size: var(--hm-page-header-title-font-size); line-height: var(--hm-page-header-title-line-height); }
/* Font Weight: one row per weight (name + value · token | preview), 12px above and below with a thin rule between rows only
(the section divider sits above the first row). */
.hm-foundation .hm-typo-weight { padding-block: 12px; }
.hm-foundation .hm-typo-weight:first-child { padding-top: 0; }
.hm-foundation .hm-typo-weight:last-child { padding-bottom: 0; }
.hm-foundation .hm-typo-weight + .hm-typo-weight { border-top: 1px solid var(--hm-sem-stroke-input-default); }
.hm-foundation .hm-typo-weight-preview { font-size: var(--hm-card-title-font-size); line-height: var(--hm-card-title-line-height); }
/* Specimens: text styles and form controls as two columns from 1100px, groups 24px apart. The 40px column gap makes both columns
exactly as wide as the Font Family / Font Weight content (section gap 24px + indent 16px), so the previews line up across sections.
Each group is its title, then rows that each carry one thin top rule (the first one doubles as the title underline; no rule after
the last row). Row: item name / computed Size / Line / Weight · letter-spacing (both secondary) | the sample at its real size. */
.hm-foundation .hm-typo-specimens { display: grid; grid-template-columns: minmax(0, 1fr); gap: 24px 40px; align-items: start; }
@media (min-width: 1100px) { .hm-foundation .hm-typo-specimens { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.hm-foundation .hm-typo-groups { display: grid; gap: 24px; }
.hm-foundation .hm-typo-group h3 { margin: 0 0 8px; }
.hm-foundation .hm-typo-group ul { margin: 0; padding: 0; list-style: none; }
.hm-foundation .hm-typo-specimen { padding-block: 12px; border-top: 1px solid var(--hm-sem-stroke-input-default); }
.hm-foundation .hm-typo-specimen-meta { gap: 0; color: var(--hm-sem-fg-secondary); font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); }
.hm-foundation .hm-typo-specimen-name { font-weight: var(--hm-font-weight-medium); }
.hm-foundation .hm-typo-metrics { display: flex; flex-wrap: wrap; font-variant-numeric: tabular-nums; }
.hm-foundation .hm-typo-metrics > span + span::before { content: "\00a0·\00a0"; }
.hm-foundation .hm-typo-sample { color: var(--hm-sem-fg-primary); }
/* Typography Text Colors (data-text-colors): separate chips like Company Colors (shared swatch rules above: 120px, 12px padding,
radius, name and usage top left, HEX bottom right, text tone by contrast). Four equal tracks 12px apart on wide screens;
narrow screens fall back to 150px tracks (2 columns at 375px) and wrap the text inside the chip instead of scrolling. */
.hm-foundation .hm-text-color-row { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(max(150px, (100% - 36px) / 4), 1fr)); overflow: visible; padding-bottom: 0; }
/* The four text colors are dark grays, so every chip uses white text; white Bold reads spread out on them, so the name uses Medium
(user decision 2026-11-12; Hanmac and Company names stay Bold). */
.hm-foundation .hm-brand-color-row[data-text-colors] h3 { font-weight: var(--hm-font-weight-medium); }
/* Layout: compact specification page (PC: three bands; narrow screens stack and scroll naturally). /* Layout: compact specification page (PC: three bands; narrow screens stack and scroll naturally).
Section header spacing and the section gap come from the common Story page; the bands only own their grid. No fixed heights or clipping. */ Section header spacing and the section gap come from the common Story page; the bands only own their grid. No fixed heights or clipping. */
+30
View File
@@ -23,6 +23,36 @@ export function useResolvedProperties(properties: readonly string[]) {
return values return values
} }
// Swatch text candidates in order of preference (see textTone); read from the Theme with useResolvedProperties.
export const textTones = [["primary", "--hm-sem-fg-primary"], ["inverse", "--hm-color-text-inverse"], ["black", "--hm-sem-palette-black"]] as const
export const textToneProperties = textTones.map(([, property]) => property)
// Read the current CSS value rather than duplicating Theme HEX values.
export function colorHex(value: string | undefined) {
if (value === undefined) return "확인 중"
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 relative luminance of a #RRGGBB value.
function luminance(hex: string) {
const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255).map(c => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
// Swatch text takes the first tone that reaches WCAG AA 4.5:1: fg-primary, then white, then black (data-text-tone in foundation.css).
export function textTone(hex: string, values: Record<string, string>) {
if (!/^#[\dA-F]{6}$/.test(hex)) return undefined
const background = luminance(hex)
return textTones.find(([, property]) => {
const text = colorHex(values[property])
if (!/^#[\dA-F]{6}$/.test(text)) return false
const [light, dark] = [luminance(text), background].sort((a, b) => b - a)
return (light + 0.05) / (dark + 0.05) >= 4.5
})?.[0]
}
// The page frame, title and sections are the common Story page (src/components/story-page.tsx); .hm-foundation styles the reference content. // The page frame, title and sections are the common Story page (src/components/story-page.tsx); .hm-foundation styles the reference content.
export function ReferencePage({ title, intro, children }: { title: string; intro?: string; children: ReactNode }) { export function ReferencePage({ title, intro, children }: { title: string; intro?: string; children: ReactNode }) {
return <StoryPage className="hm-foundation"><StoryPageHeader title={title} description={intro} />{children}</StoryPage> return <StoryPage className="hm-foundation"><StoryPageHeader title={title} description={intro} />{children}</StoryPage>
+49 -31
View File
@@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { typographyProperties, typographyReferences } from "./reference-data" import { typographyProperties, typographyReferences } from "./reference-data"
import { ReferencePage, ReferenceSection, useResolvedProperties } from "./reference" import { StorySectionGrid } from "../components/story-page"
import { colorHex, ReferencePage, ReferenceSection, textTone, textToneProperties, useResolvedProperties } from "./reference"
type TypographyReference = (typeof typographyReferences)[number] type TypographyReference = (typeof typographyReferences)[number]
@@ -10,7 +11,7 @@ const weights = [
{ property: "--hm-font-weight-medium", label: "Medium" }, { property: "--hm-font-weight-medium", label: "Medium" },
{ property: "--hm-font-weight-bold", label: "Bold" }, { property: "--hm-font-weight-bold", label: "Bold" },
] ]
// Left table: text styles. Right table: form controls. Together they cover every entry in typographyReferences. // Left column: text styles. Right column: form controls. Together they cover every entry in typographyReferences.
const textLabels = ["Page Header · title", "Card · title", "Page Header · description", "Label"] const textLabels = ["Page Header · title", "Card · title", "Page Header · description", "Label"]
const textRows = typographyReferences.filter(item => textLabels.includes(item.label)) const textRows = typographyReferences.filter(item => textLabels.includes(item.label))
const controlRows = typographyReferences.filter(item => !textLabels.includes(item.label)) const controlRows = typographyReferences.filter(item => !textLabels.includes(item.label))
@@ -23,22 +24,24 @@ const textColorRoles = [
// Transparent readonly Input on the beige surface uses Secondary (Placeholder is below 4.5:1 there). // Transparent readonly Input on the beige surface uses Secondary (Placeholder is below 4.5:1 there).
{ label: "Readonly Placeholder", name: "읽기 전용 입력 안내", token: "--hm-sem-fg-secondary" }, { label: "Readonly Placeholder", name: "읽기 전용 입력 안내", token: "--hm-sem-fg-secondary" },
] ]
const textColorProperties = [...new Set(textColorRoles.map(item => item.token))] // Chip colors plus the swatch text candidates (textTone, reference.tsx).
const textColorProperties = [...new Set([...textColorRoles.map(item => item.token), ...textToneProperties])]
function toHex(value: string | undefined) { // Same chip as Foundation/가족사 컬러 (separate chips; shared swatch rules in foundation.css via data-text-colors): role name and
if (!value) return "" // usage at the top left, HEX at the bottom right, text tone by contrast.
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
}
// Same chip classes as Foundation/한맥 컬러 and 가족사 컬러 (foundation.css): swatch, then role name / usage / HEX on separate lines
// so the item name, its description and the HEX value read as three levels (styles scoped to .hm-text-color-row).
function TextColorChips({ values }: { values: Record<string, string> }) { function TextColorChips({ values }: { values: Record<string, string> }) {
return <div className="hm-brand-color-row hm-text-color-row">{textColorRoles.map(item => <article className="hm-brand-color-chip" key={item.label} data-text-color={item.label} data-color-token={item.token}> return <div className="hm-brand-color-row hm-text-color-row" data-text-colors>{textColorRoles.map(item => {
<div className="hm-foundation-swatch" style={{ background: `var(${item.token})` }} /> const hex = colorHex(values[item.token])
<h3>{item.label}</h3><p data-color-name>{item.name}</p><p data-color-value>{toHex(values[item.token])}</p> return <article className="hm-brand-color-chip" key={item.label} data-text-color={item.label} data-color-token={item.token} data-text-tone={textTone(hex, values)}>
</article>)}</div> <div className="hm-foundation-swatch" style={{ background: `var(${item.token})` }}>
<div className="hm-color-name">
<h3>{item.label}</h3>
<p className="hm-color-note" data-color-name>{item.name}</p>
</div>
<p data-color-value>{hex}</p>
</div>
</article>
})}</div>
} }
function SpecimenRow({ label, size, line, weight }: TypographyReference & { weight?: string }) { function SpecimenRow({ label, size, line, weight }: TypographyReference & { weight?: string }) {
@@ -53,18 +56,23 @@ function SpecimenRow({ label, size, line, weight }: TypographyReference & { weig
}) })
return () => { mounted = false } return () => { mounted = false }
}, []) }, [])
return <tr> // The sample at its real component size leads; the item name and the computed metrics sit beside it as secondary text.
<th scope="row">{label}</th> return <li className="hm-typo-row hm-typo-specimen">
<td><div ref={ref} className="hm-typo-sample" data-typography-sample={label} lang="ko" style={{ fontSize: `var(${size})`, lineHeight: `var(${line})`, fontWeight: weight ? `var(${weight})` : "normal" }}>한맥 Hanmac 0123</div></td> <div className="hm-typo-row-meta hm-typo-specimen-meta">
<td className="hm-typo-metrics" data-typography-metrics><span>{metrics.main}</span><small className="hm-foundation-note">{metrics.spacing}</small></td> <span className="hm-typo-specimen-name">{label.split(" · ")[1] ?? label}</span>
</tr> <span className="hm-typo-metrics" data-typography-metrics><span>{metrics.main}</span><span>{metrics.spacing}</span></span>
</div>
<div ref={ref} className="hm-typo-row-preview hm-typo-sample" data-typography-sample={label} lang="ko" style={{ fontSize: `var(${size})`, lineHeight: `var(${line})`, fontWeight: weight ? `var(${weight})` : "normal" }}>한맥 Hanmac 0123</div>
</li>
} }
function SpecimenTable({ rows }: { rows: readonly TypographyReference[] }) { // Rows grouped by component, the label part before " · " (Page Header, Card, Label, Input, Button Standard).
return <table className="hm-typo-table"> const groupOf = (item: TypographyReference) => item.label.split(" · ")[0]
<thead><tr><th scope="col">Component</th><th scope="col">Sample</th><th scope="col">Size / Line / Weight</th></tr></thead> function SpecimenGroups({ rows }: { rows: readonly TypographyReference[] }) {
<tbody>{rows.map(item => <SpecimenRow key={item.label} {...item} />)}</tbody> return <div className="hm-typo-groups">{[...new Set(rows.map(groupOf))].map(group => <section className="hm-typo-group" key={group} aria-label={group}>
</table> <h3>{group}</h3>
<ul>{rows.filter(item => groupOf(item) === group).map(item => <SpecimenRow key={item.label} {...item} />)}</ul>
</section>)}</div>
} }
function TypographyReferencePage() { function TypographyReferencePage() {
@@ -75,21 +83,31 @@ function TypographyReferencePage() {
<ReferenceSection title="Text Colors" note=""> <ReferenceSection title="Text Colors" note="">
<TextColorChips values={colorValues} /> <TextColorChips values={colorValues} />
</ReferenceSection> </ReferenceSection>
<StorySectionGrid className="hm-typo-type-grid">
<ReferenceSection title="Font Family"> <ReferenceSection title="Font Family">
<div className="hm-typo-family sb-page-indent"> <div className="hm-typo-rows sb-page-indent"><div className="hm-typo-row hm-typo-family">
<p><strong>Noto Sans KR</strong> <span className="hm-foundation-note">→ 맑은 고딕 → 시스템 sans-serif</span></p> <div className="hm-typo-row-meta">
<p className="hm-typo-kicker">Primary Typeface</p>
<p className="hm-typo-family-name">Noto Sans KR</p>
</div>
<div className="hm-typo-row-preview">
<p className="hm-typo-family-preview" lang="ko" style={{ fontFamily: "var(--hm-font-family-base)" }}>한맥 Hanmac 0123</p>
<code className="hm-foundation-note hm-typo-token">--hm-font-family-base: {values["--hm-font-family-base"]}</code> <code className="hm-foundation-note hm-typo-token">--hm-font-family-base: {values["--hm-font-family-base"]}</code>
</div> </div>
</div></div>
</ReferenceSection> </ReferenceSection>
<ReferenceSection title="Font Weight"> <ReferenceSection title="Font Weight">
<div className="hm-typo-weights sb-page-indent">{weights.map(({ property, label }) => <article className="hm-typo-weight" key={property}> <div className="hm-typo-rows hm-typo-weights sb-page-indent">{weights.map(({ property, label }) => <article className="hm-typo-row hm-typo-weight" key={property}>
<div className="hm-typo-row-meta">
<p className="hm-typo-weight-meta"><strong>{label}</strong> <span className="hm-foundation-note">{values[property]}</span></p> <p className="hm-typo-weight-meta"><strong>{label}</strong> <span className="hm-foundation-note">{values[property]}</span></p>
<p className="hm-typo-weight-preview" style={{ fontWeight: `var(${property})` }}>한맥 Hanmac 0123</p>
<code className="hm-foundation-note hm-typo-token">{property}</code> <code className="hm-foundation-note hm-typo-token">{property}</code>
</div>
<p className="hm-typo-row-preview hm-typo-weight-preview" style={{ fontWeight: `var(${property})` }}>한맥 Hanmac 0123</p>
</article>)}</div> </article>)}</div>
</ReferenceSection> </ReferenceSection>
</StorySectionGrid>
<ReferenceSection title="Typography Specimens"> <ReferenceSection title="Typography Specimens">
<div className="hm-typo-tables sb-page-indent"><SpecimenTable rows={textRows} /><SpecimenTable rows={controlRows} /></div> <div className="hm-typo-specimens sb-page-indent"><SpecimenGroups rows={textRows} /><SpecimenGroups rows={controlRows} /></div>
</ReferenceSection> </ReferenceSection>
</ReferencePage> </ReferencePage>
} }