From 65cc256cfd08f57752f0c472513c58104e427bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Thu, 12 Nov 2026 14:50:34 +0900 Subject: [PATCH] style(storybook): refine foundation color chips and typography layout - Hanmac Color: continuous spectrum band (8/4/2 columns by width), 120px swatches with name, role and HEX inside, Default/Light 4 role lines - Company Colors: separate 120px chips (12px padding, 4px radius), name top left and HEX bottom right; company layout and data unchanged - Typography: Font Family/Weight in two columns with info | preview rows, vertically centered specimens grouped by component, Text Colors chips with 14px Medium names - Shared colorHex/textTone helpers pick the first text tone reaching 4.5:1 - Update brand color guide and WORKING_CONTEXT --- WORKING_CONTEXT.md | 12 ++ docs/guidelines/brand-color-steps.md | 4 +- src/foundation/color.stories.tsx | 32 ++--- src/foundation/company-colors.stories.tsx | 17 +-- src/foundation/foundation.css | 141 +++++++++++++++------- src/foundation/reference.tsx | 30 +++++ src/foundation/typography.stories.tsx | 98 +++++++++------ 7 files changed, 223 insertions(+), 111 deletions(-) diff --git a/WORKING_CONTEXT.md b/WORKING_CONTEXT.md index 215734a..cc5ebe3 100644 --- a/WORKING_CONTEXT.md +++ b/WORKING_CONTEXT.md @@ -1,5 +1,17 @@ # 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 기준, 미커밋) - 공통 프레임 `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. diff --git a/docs/guidelines/brand-color-steps.md b/docs/guidelines/brand-color-steps.md index b7e25fe..2f76d29 100644 --- a/docs/guidelines/brand-color-steps.md +++ b/docs/guidelines/brand-color-steps.md @@ -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 대표 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칩은 그대로다. diff --git a/src/foundation/color.stories.tsx b/src/foundation/color.stories.tsx index 3658a39..b728567 100644 --- a/src/foundation/color.stories.tsx +++ b/src/foundation/color.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react-vite" 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. 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 type ColorStage = typeof stages[number] 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> = { "light-4": "보더 하이라이트용", default: "기본색" } 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)) - -// 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 -} +// 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] function ColorChip({ prefix, stage, values }: { prefix: PalettePrefix; stage: ColorStage; values: Record }) { const token = paletteToken(prefix, stage) const definition = runtimeThemeContract[token] - return
-
-

{stageLabel(stage)}

{colorHex(values[definition.property])}

+ const hex = colorHex(values[definition.property]) + return
+
+
+

{stageLabel(stage)}

+ {stageNote[stage] &&

{stageNote[stage]}

} +
+

{hex}

+
} @@ -37,7 +37,7 @@ function ColorReferencePage() { const values = useResolvedProperties(colorProperties) return {palettes.map(palette => -
{stages.map(stage => )}
+
{stages.map(stage => )}
)}
} diff --git a/src/foundation/company-colors.stories.tsx b/src/foundation/company-colors.stories.tsx index 9ff045d..df8ae1a 100644 --- a/src/foundation/company-colors.stories.tsx +++ b/src/foundation/company-colors.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react-vite" 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. // 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)), ] -function CompanyColorChip({ label, hex, main }: { label: string; hex: string; main: boolean }) { - return
-
-

{label}

{hex.toUpperCase()}

+// Same swatch content as Hanmac Color (name top left, HEX bottom right, text tone by contrast); main and sub differ only by the name. +function CompanyColorChip({ label, hex, values }: { label: string; hex: string; values: Record }) { + return
+
+

{label}

{hex.toUpperCase()}

+
} function CompanyColorsPage() { + const values = useResolvedProperties(textToneProperties) return
{companies.map(company =>
- - {company.secondary.map((hex, index) => )} + + {company.secondary.map((hex, index) => )}
)}
diff --git a/src/foundation/foundation.css b/src/foundation/foundation.css index 0e76cd1..e2d8329 100644 --- a/src/foundation/foundation.css +++ b/src/foundation/foundation.css @@ -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-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[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; } -/* 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-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; } @@ -41,54 +73,71 @@ .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), - 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 > .sb-page-section { min-width: 0; margin-top: 0; } /* Row starts for the 2 / 2 / 3 company layout (한맥·삼안 / 장헌·피티씨 / 장헌산업·한라산업개발·바론컨설턴트). */ .hm-foundation .hm-company-grid > #company-jangheon, .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; - the section divider already frames them), Specimens as comparison tables (two columns on wide screens, stacked rows on narrow screens). - Document text levels (presentation only, existing tokens): item name 14/700 primary, description 14/400 secondary, - token / HEX / extra info 12/18/400 secondary (.hm-typo-token). Section titles stay on the common Story page H2 (16/24/700). */ -/* Section rhythm (title → 12px → divider → 16px → content, sections 28px apart) and the content indent (.sb-page-indent on Font Family, - Font Weight and Specimens; Text Colors chips stay on the edge) come from the common Story page (story-page.css). */ -.hm-foundation .hm-typo-family { display: grid; gap: 4px; margin: 0; } -.hm-foundation .hm-typo-family p { margin: 0; } -.hm-foundation .hm-typo-family strong { font-weight: var(--hm-font-weight-bold); } -.hm-foundation .hm-typo-token { font-size: var(--hm-button-font-size-sm); line-height: var(--hm-button-line-height-sm); } -.hm-foundation .hm-typo-weights { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 12px 24px; } -.hm-foundation .hm-typo-weight { min-width: 0; } -.hm-foundation .hm-typo-weight p { margin: 0; } -.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-tables { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px 32px; align-items: start; } -.hm-foundation .hm-typo-table { width: 100%; border-collapse: collapse; } -.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-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-table tbody th { width: 27%; font-weight: var(--hm-font-weight-bold); } -.hm-foundation .hm-typo-table td:last-child { width: 26%; padding-right: 0; } -.hm-foundation .hm-typo-metrics span, .hm-foundation .hm-typo-metrics small { display: block; font-variant-numeric: tabular-nums; } -.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; } +/* Typography: minimal type specimen page (presentation only, existing tokens; no card box, outer border or shadow). + Section titles, rhythm (title → 12px → divider → 16px → content, sections 28px apart) and the content indent (.sb-page-indent on + Font Family, Font Weight and Specimens; Text Colors chips stay on the edge) come from the common Story page (story-page.css). + Text levels: label / token 12/18/400 secondary, family and weight names 14/700 primary, specimen name and metrics 12/18 secondary; + previews and samples carry the largest type. */ +/* Font Family and Font Weight side by side (StorySectionGrid) from 720px, stacked below. */ +.hm-foundation .hm-typo-type-grid { grid-template-columns: minmax(0, 1fr); } +@media (min-width: 720px) { .hm-foundation .hm-typo-type-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +/* Shared row (Font Family, each Font Weight item, each Specimen): information on the left, preview on the right, both vertically + centered. The row follows the width of its own column (container query on the Family / Weight content and on each Specimen + group), not the viewport: from 440px the information column is min(240px, 40%), so it never squeezes the preview; below that + the row is one column with the preview first. */ +.hm-foundation :is(.hm-typo-rows, .hm-typo-group) { container-type: inline-size; } +.hm-foundation .hm-typo-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px 24px; align-items: center; } +.hm-foundation .hm-typo-row :is(p, code) { margin: 0; } +.hm-foundation :is(.hm-typo-row-meta, .hm-typo-row-preview) { display: grid; gap: 2px; min-width: 0; } +.hm-foundation .hm-typo-row-preview { order: -1; } +.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); } +@container (min-width: 440px) { + .hm-foundation .hm-typo-row { grid-template-columns: min(240px, 40%) minmax(0, 1fr); row-gap: 4px; } + .hm-foundation .hm-typo-row-preview { order: 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). Section header spacing and the section gap come from the common Story page; the bands only own their grid. No fixed heights or clipping. */ diff --git a/src/foundation/reference.tsx b/src/foundation/reference.tsx index 972690a..426fb68 100644 --- a/src/foundation/reference.tsx +++ b/src/foundation/reference.tsx @@ -23,6 +23,36 @@ export function useResolvedProperties(properties: readonly string[]) { 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) { + 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. export function ReferencePage({ title, intro, children }: { title: string; intro?: string; children: ReactNode }) { return {children} diff --git a/src/foundation/typography.stories.tsx b/src/foundation/typography.stories.tsx index ae9c7e2..9e9fb31 100644 --- a/src/foundation/typography.stories.tsx +++ b/src/foundation/typography.stories.tsx @@ -1,7 +1,8 @@ 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" +import { StorySectionGrid } from "../components/story-page" +import { colorHex, ReferencePage, ReferenceSection, textTone, textToneProperties, useResolvedProperties } from "./reference" type TypographyReference = (typeof typographyReferences)[number] @@ -10,7 +11,7 @@ const weights = [ { 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. +// 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 textRows = 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). { 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) { - 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 -} - -// 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). +// Same chip as Foundation/가족사 컬러 (separate chips; shared swatch rules in foundation.css via data-text-colors): role name and +// usage at the top left, HEX at the bottom right, text tone by contrast. function TextColorChips({ values }: { values: Record }) { - return
{textColorRoles.map(item =>
-
-

{item.label}

{item.name}

{toHex(values[item.token])}

-
)}
+ return
{textColorRoles.map(item => { + const hex = colorHex(values[item.token]) + return
+
+
+

{item.label}

+

{item.name}

+
+

{hex}

+
+
+ })}
} 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 - {label} -
한맥 Hanmac 0123
- {metrics.main}{metrics.spacing} - + // The sample at its real component size leads; the item name and the computed metrics sit beside it as secondary text. + return
  • +
    + {label.split(" · ")[1] ?? label} + {metrics.main}{metrics.spacing} +
    +
    한맥 Hanmac 0123
    +
  • } -function SpecimenTable({ rows }: { rows: readonly TypographyReference[] }) { - return - - {rows.map(item => )} -
    ComponentSampleSize / Line / Weight
    +// Rows grouped by component, the label part before " · " (Page Header, Card, Label, Input, Button Standard). +const groupOf = (item: TypographyReference) => item.label.split(" · ")[0] +function SpecimenGroups({ rows }: { rows: readonly TypographyReference[] }) { + return
    {[...new Set(rows.map(groupOf))].map(group =>
    +

    {group}

    +
      {rows.filter(item => groupOf(item) === group).map(item => )}
    +
    )}
    } function TypographyReferencePage() { @@ -75,21 +83,31 @@ function TypographyReferencePage() { - -
    -

    Noto Sans KR → 맑은 고딕 → 시스템 sans-serif

    - --hm-font-family-base: {values["--hm-font-family-base"]} -
    -
    - -
    {weights.map(({ property, label }) =>
    -

    {label} {values[property]}

    -

    한맥 Hanmac 0123

    - {property} -
    )}
    -
    + + +
    +
    +

    Primary Typeface

    +

    Noto Sans KR

    +
    +
    +

    한맥 Hanmac 0123

    + --hm-font-family-base: {values["--hm-font-family-base"]} +
    +
    +
    + +
    {weights.map(({ property, label }) =>
    +
    +

    {label} {values[property]}

    + {property} +
    +

    한맥 Hanmac 0123

    +
    )}
    +
    +
    -
    +
    }