diff --git a/docs/reports/foundation-color-storybook-2026-10-11.md b/docs/reports/foundation-color-storybook-2026-10-11.md new file mode 100644 index 0000000..146e919 --- /dev/null +++ b/docs/reports/foundation-color-storybook-2026-10-11.md @@ -0,0 +1,87 @@ +# Foundation Color 현행 Token 표시 확장 — 2026-10-11 + +RESULT: PARTIAL. 요청한 표시 확장과 선택 검증은 완료했다. 현행 프로젝트에 없는 Generic Status Error와 Brown/Orange 공통 Background Gradient는 누락 상태로 표시한다. 기존 자료의 HEX·5-stop 구조를 새 Theme 정의로 만들지 않았다. Guard의 동적 값 미검증도 보존한다. + +## 작업 기준과 범위 + +- PROJECT_PATH: `D:\디자인시스템\HMDesign-Gitea-Transfer` +- BRANCH: `main` +- ORIGIN: `https://gitea.hmac.kr/b25027/HMDesign.git` +- BEFORE_SHA: `91c8e5e14ef9f162d364909df9a4b481ef3146e4` +- 조사 시작 때 보인 Disabled Input Label의 Stage된 5개 파일은 작업 A가 Commit·Push를 완료했다. 이후 로컬/원격 일치와 깨끗한 Working Tree를 확인하고 작업 B의 기준으로 삼았다. 해당 Commit을 보존한다. +- Scope: STORYBOOK_ONLY / React. 기존 `Foundation/Color`의 `All` export, `foundation-color--all` ID, Storybook Navigation을 유지한다. 신규 업무 화면·Navigation·Pattern·control은 없으므로 신규 화면 선택 계약과 Form 검증은 해당하지 않는다. +- 변경: [Color Story](../../src/foundation/color.stories.tsx), [Foundation 전용 CSS](../../src/foundation/foundation.css), 이 보고서. 기존 ReferencePage / ReferenceSection / 카드·Swatch·가로 Palette 행과 [Computed Property Hook](../../src/foundation/reference.tsx)을 재사용한다. Palette의 Token 상세는 native details/summary로 제공한다. + +## Foundation → Theme → Contract → CSS → Storybook + +[Default JSON](../../themes/default.json) → Theme 생성 스크립트 → [생성 Default 값](../../packages/ui/src/theme/default-values.generated.ts) / [공개 Runtime 계약](../../packages/ui/src/theme/contract.ts) 및 [default.css](../../packages/ui/src/theme/default.css) → Component mapping → [Storybook preview](../../.storybook/preview.tsx)의 index.css / hm-theme.css → Color Story. + +Story는 `@hm-design/ui/theme` 공개 export를 사용한다. Swatch는 현재 CSS 변수를 직접 소비하고 표시 HEX는 documentElement의 computed custom property를 읽는다. Root style/class/data-hm-theme 변경과 resize를 기존 Hook이 관찰한다. Gradient 방향·Stop 위치·색상은 현재 변수의 해석된 linear-gradient에서 추출한다. 과거 자료 HEX나 Stop 위치를 Story에 하드코딩하지 않는다. 해석되지 않는 형식은 NOT VERIFIED로 표시한다. + +상태는 현행 [브랜드 단계 기준](../guidelines/brand-color-steps.md), [Focus 시험 적용 기록](../guidelines/테마-토큰-가이드.md), Component CSS의 Confirmed 근거와 읽기 전용 ui_viz token-inventory의 해당 역할을 대조했다. 값·이름은 현재 HMDesign 구현이 우선하며 ui_viz는 수정하지 않았다. + +## STORYBOOK / TOKEN_CONSISTENCY + +FOUNDATION_COLOR: 렌더 PASS. EXISTING_PALETTE: Green/Brown/Orange 각 8개 및 기존 Button 전용 5개 보존. 모두 현재 CSS 배경색과 표시 HEX가 일치한다. 단계 순서·HEX·기존 Orange Dark 1/2/3의 확정 상태를 보존한다. Orange Default의 border 2px / font-weight 700을 브라우저에서 확인했다. + +| 표시 항목 | 실제 Canonical Token | 실제 CSS Variable | Default Theme HEX | 상태 / 차이 | +|---|---|---|---|---| +| ACCENT_YELLOW | color.palette.yellow-accent | --hm-sem-palette-yellow-accent | #FFC600 | Confirmed. 이전 hm-accent-yellow / --hm-accent-yellow → 현행 --hm-yellow-accent Primitive. color.accent(#BDDFD4 Green Tint)와 별도 | +| INPUT_OUTLINE_DEFAULT | color.stroke.input.default | --hm-sem-stroke-input-default | #DEDBD2 | Confirmed. 동일 색의 다른 역할과 통합하지 않음 | +| INPUT_OUTLINE_FOCUS | color.stroke.input.focus | --hm-sem-stroke-input-focus | #266651 | Provisional · 현재 시험 적용. 참고 #123328로 되돌리지 않음 | +| INPUT_OUTLINE_ERROR | color.action.destructive | --hm-input-error-border-color → --hm-sem-action-destructive | #D32F2F | Confirmed 현행 mapping. 참고 color.stroke.input.error는 현재 공개 계약에 없음을 표시 | +| GENERIC_STATUS_ERROR | 미정의 | 미정의 | 미정의 | 참고 color.status.error는 Provisional. Alert destructive의 Action Token으로 대체하지 않음 | +| PRIMARY_ACTION | color.action.primary | --hm-sem-action-primary | #123328 | Confirmed | +| CONTENT_BACKGROUND | color.bg.content | --hm-sem-bg-content | #F5F0E5 | Application Context Confirmed / Marketing TBD | +| SURFACE_BACKGROUND | color.bg.surface | --hm-sem-bg-surface | #FFFFFF | Confirmed | +| PRIMARY_FOREGROUND | color.fg.primary | --hm-sem-fg-primary | #0A221D | Confirmed. White 배경 텍스트 예시 제공 | + +Input/Textarea의 error-border-color 및 Select invalid border는 현재 Action Destructive에 연결된다. Generic Status Error의 독립 역할과 구분하여 설명했으며 mapping을 변경하지 않았다. Yellow의 실제 LNB 소비는 그룹 선택 텍스트다. + +MATCHED: 위 정의된 8개 컬러와 Palette 29개의 HEX / computed backgroundColor 일치. PRIMARY_FOREGROUND의 computed color는 rgb(10, 34, 29), 예시 backgroundColor는 rgb(255, 255, 255). + +MISSING: color.status.error, 독립 color.stroke.input.error 공개 입력, Brown/Orange 공통 Background 역할. LEGACY_NAME_DIFFERENCES와 PROVISIONAL_ITEMS는 위 표에 기록했다. + +## GRADIENTS + +| 구분 | 현재 CSS 변수 | 방향 | STOP_VALUES: 위치 / 현재 HEX | +|---|---|---|---| +| GREEN: 공통 HM Green Background | --hm-sem-bg-brand | to bottom | 0% #0A221D / 12% #071E18 / 42% #123328 / 78% #266651 / 100% #205241 | +| BROWN: 공통 Background 미정의. 별도 기존 Button Background 표시 | --hm-button-brand-brown-background | 180deg | 0% #897445 / 30% #604F32 / 65% #352D1D / 100% #221D13 | +| ORANGE: 공통 Background 미정의. 별도 기존 Button Background 표시 | --hm-button-brand-orange-background | 180deg | 0% #D35A00 / 30% #CE5600 / 65% #B44B00 / 100% #783402 | + +참고 명칭 gradient.green.background는 현재 공통 derived CSS와 color.bg.brand.stop.* 공개 입력으로 연결된다. 전체 Gradient 자체의 Runtime 입력은 없다. gradient.brown.background / gradient.orange.background의 공통 5-stop 계약은 현재 HMDesign에 없으므로 재구성하지 않았다. 현행 Button 전용 4-stop은 [button.css](../../packages/ui/src/theme/button.css)의 정의를 그대로 소비하며 공통 배경으로 승격하지 않는다. + +COMPUTED_STYLE: 실제 3개 preview의 backgroundImage가 모두 linear-gradient이고 위 모든 색상·Stop 위치와 일치했다. 각 Stop Swatch도 실제 적용값에서 생성한다. 전체 opacity나 Solid 색으로 Gradient를 대체하지 않았다. + +[Dashboard CSS](../../packages/ui/src/theme/dashboard-layout.css)의 Header는 별도 Action Surface 4-stop이며 Compact는 독립적인 2-layer Gradient다. 이번 공통 Green 미리보기를 Dashboard Compact와 동일한 Gradient라고 주장하지 않으며 Dashboard 원본을 변경하지 않았다. + +## VALIDATION + +| 항목 | 결과 | +|---|---| +| STORYBOOK_RENDER | Default Theme 실제 React 렌더 PASS. Accent/Semantic/Gradient 및 텍스트 예시 확인. 브라우저 error 로그 0건 | +| TYPECHECK | npm run typecheck PASS(root/UI/CENTER, 패키지 생성 및 Theme defaults 검사 포함). 표시 문구·details 변경 후 영향받는 root tsc -b PASS | +| BUILD | Storybook build PASS: `_local/foundation-color-storybook-20261011`. 최초 기존 storybook-static의 폰트 쓰기가 Access Denied로 실패하여 별도 출력으로 해결. 500kB chunk·plugin timing 경고는 남음 | +| STORY_ID | build index에 foundation-color--all / Foundation/Color / Color / All 유지. 다른 Story export와 Navigation 구성 변경 없음 | +| DOC_LINKS | 내부 링크 검사 PASS. 관련 문서 및 Source 연결 유효 | +| RESPONSIVE | 390/768/1280/1440px 실제 브라우저에서 document scrollWidth = clientWidth(375/753/1265/1425). Palette 행만 좁은 폭에서 overflow-x:auto. 카드 외부 넘침 0, Stop wrap 후 내부 넘침 0 | +| KEYBOARD | Palette Token details를 Enter로 열기·닫기 PASS, SUMMARY focus 유지. 읽기 전용 참조 화면이며 Form ID / disabled / readOnly / popup focus 복귀는 해당 없음. 실제 SR 발화는 NOT VERIFIED | +| GUARD | PARTIAL / STORYBOOK 1회. FAIL 0, 원본 status 및 staticStatus NOT VERIFIED. 동적 Token template/스타일·기존 reference/story-page의 해석 한계 보존 | + +Guard 원본은 `_local/foundation-color-guard-20261011.json`에 보존한다. styleEvidence: STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED. Guard 판정을 computed 결과로 덮어쓰지 않는다. 별도 Runtime 근거는 위 Swatch backgroundColor, Foreground color, Gradient backgroundImage와 네 폭 측정이다. 동적 ColorChip 후보 29개, SemanticColor 후보 8개, GradientPreview 후보 3개, Stop 후보 13개는 모두 실제 소스의 유한 집합을 소비한다. CSS 추가 4개 규칙은 Foundation 콘텐츠에만 적용하며 HM Component override는 없다. Storybook 밖 production UI의 전체 검증과 전체 Component 반복 검증은 실행하지 않았다. + +## PRESERVED / GIT + +PALETTE / THEME / COMPONENT_API / EXISTING_STORIES / DASHBOARD_GRADIENTS: 값·정의·소비처 변경 없음. 기존 Story의 레이아웃 기준과 Swatch·Palette 행을 유지하면서 Token 상세와 요청한 섹션만 확장했다. 다른 Foundation 화면에 새 CSS selector가 적용되지 않는다. + +CHANGED_FILES: 위 Story, 전용 CSS, 이 보고서의 3개만 Stage/검토한다. Commit 메시지는 `docs(storybook): expose semantic colors and gradients`. Commit·Push 전 최신 원격 main과 작업 브랜치/기준 SHA 및 Working Tree를 비교하며 일반 Push만 수행한다. Commit SHA / Push / 최종 Local-Remote 일치 / Working Tree 결과는 최종 응답에 기록한다. + +## FOLLOW_UP + +- Generic Status Error의 Provisional 역할·값과 공개 계약 추가 여부는 후속 승인 작업이다. +- Input Error의 독립 canonical이 필요한지 판단한다. 이번에는 기존 Action Destructive mapping을 유지했다. +- Focus #266651 시험 적용값의 최종 확정이 필요하다. +- Brown/Orange 공통 Background Gradient를 도입할지, 현행 Button 전용 Gradient를 계속 별도로 유지할지 판단한다. 이번에는 공통 Token을 만들지 않았다. + +Estimated Duration: 최초 약 35분. 실제 시작·완료 시각과 소요 시간은 Commit·Push 및 최종 상태 확인을 마친 응답에 KST로 기록한다. diff --git a/src/foundation/color.stories.tsx b/src/foundation/color.stories.tsx index 33fcb54..15874a3 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 } from "./reference" +import { ReferencePage, ReferenceSection, useResolvedProperties } from "./reference" // Orange Dark 1/2/3 use the latest user-confirmed values (2026-10-11). const stages = ["light-4", "light-3", "light-2", "light-1", "default", "dark-1", "dark-2", "dark-3"] as const @@ -10,23 +10,123 @@ const palettes = [ { name: "한맥오렌지", prefix: "orange", confirmed: [...stages], legacy: ["light", "mid", "dark", "deep"] }, ] const stageLabel = (stage: string) => stage === "default" ? "Default · 기본색" : stage.replaceAll("-", " ").replace(/^./, letter => letter.toUpperCase()) -function ColorChip({ prefix, stage, confirmed = true }: { prefix: string; stage: string; confirmed?: boolean }) { +const semanticGroups = [ + { name: "Input-family Outline", colors: [ + { name: "Input Default Outline", token: "color.stroke.input.default", status: "Confirmed", note: "Input / Textarea / Select / Search Form 기본·Disabled 윤곽. 다른 역할의 동일 HEX와 통합하지 않습니다." }, + { name: "Input Focus / Open Outline", token: "color.stroke.input.focus", status: "Provisional · 시험 적용", note: "현재 Light 2 시험 적용값. 과거 Green Default로 되돌리지 않으며 사용자 최종 확정 전입니다." }, + { name: "Input Error Outline", token: "color.action.destructive", property: "--hm-input-error-border-color", status: "Confirmed · 현행 mapping", legacy: "color.stroke.input.error", note: "현행 Input / Textarea / Select 오류 윤곽은 Action Destructive를 소비합니다. 독립 Input Error canonical은 공개 계약에 없습니다. Generic Status Error와 별도 역할입니다." }, + ] }, + { name: "Status", colors: [ + { name: "Generic Status Error", status: "Provisional · 미정의", legacy: "color.status.error", note: "현재 계약·Theme에 해당 역할이 없습니다. Alert destructive는 color.action.destructive를 소비하지만 Generic Status Error로 대체하지 않습니다." }, + ] }, + { name: "Action", colors: [ + { name: "Primary Action", token: "color.action.primary", status: "Confirmed", note: "확인·저장 등 주요 행동. Gradient 배경과는 별도 역할입니다." }, + ] }, + { name: "Background", colors: [ + { name: "Content Background", token: "color.bg.content", status: "Confirmed · Application Context", note: "업무 콘텐츠 배경. Marketing Context는 TBD입니다." }, + { name: "Surface Background", token: "color.bg.surface", status: "Confirmed", note: "Card / Panel / Input / Select / Textarea / Table 표면." }, + ] }, + { name: "Foreground", colors: [ + { name: "Primary Foreground", token: "color.fg.primary", status: "Confirmed", foreground: true, note: "본문·제목·입력값에 사용하는 주요 텍스트." }, + ] }, +] satisfies { name: string; colors: ColorReference[] }[] +type ColorReference = { name: string; token?: RuntimeThemeToken; property?: string; status: string; note: string; legacy?: string; foreground?: boolean } +const gradients = [ + { name: "HM Green Background", property: "--hm-sem-bg-brand", legacy: "gradient.green.background", note: "현행 공통 브랜드 배경 · Confirmed. 공개 stop 계약은 color.bg.brand.stop.*이며 방향·위치는 현행 CSS에서 읽습니다. Dashboard의 Header / Compact 배경은 별도 정의입니다." }, + { name: "HM Brown Background", legacy: "gradient.brown.background", note: "공통 Brown Background Token / CSS 변수 미정의. 참고 자료의 5-stop 배경을 재구성하지 않습니다.", componentProperty: "--hm-button-brand-brown-background" }, + { name: "Orange Background", legacy: "gradient.orange.background", note: "공통 Orange Background Token / CSS 변수 미정의. 참고 자료의 5-stop 배경을 재구성하지 않습니다.", componentProperty: "--hm-button-brand-orange-background" }, +] +const colorProperties = [...new Set([ + ...Object.values(runtimeThemeContract).filter(definition => definition.type === "color").map(definition => definition.property), + "--hm-input-error-border-color", + ...gradients.flatMap(gradient => [gradient.property, gradient.componentProperty].filter((property): property is string => !!property)), +])] + +// Read the current CSS value, never the legacy document HEX. Keep unsupported values visible. +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 gradientParts(value: string | undefined) { + // Split only top-level commas: rgb()/color() channels must stay together. + if (!value?.startsWith("linear-gradient(") || !value.endsWith(")")) return undefined + let depth = 0 + const parts = value.slice("linear-gradient(".length, -1).split("").reduce((result, char) => { + if (char === "(") depth++ + if (char === ")") depth-- + if (char === "," && depth === 0) result.push("") + else result[result.length - 1] += char + return result + }, [""]).map(part => part.trim()) + const [direction, ...stops] = parts + const parsed = stops.map(stop => stop.match(/^(.*)\s+(-?[\d.]+%)$/)) + if (!parsed.length || parsed.some(stop => !stop)) return undefined + return { direction, stops: parsed.map(stop => ({ color: stop![1], position: stop![2] })) } +} + +function ColorChip({ prefix, stage, confirmed = true, values }: { prefix: string; stage: string; confirmed?: boolean; values: Record }) { const token = `color.palette.${prefix}-${stage}` as RuntimeThemeToken const definition = confirmed ? runtimeThemeContract[token] : undefined return
-

{stageLabel(stage)}

{definition ? definition.defaultValue.toUpperCase() : "미확정"}

+

{stageLabel(stage)}

{definition ? colorHex(values[definition.property]) : "미확정"}

+ {definition &&
{paletteStatus(stage)} · Token
Canonical Token
{token}
CSS Variable
{definition.property}
}
} +const paletteStatus = (stage: string) => stages.includes(stage as typeof stages[number]) ? "Confirmed" : "Confirmed · 기존 Button 전용" + +function SemanticColor({ item, values }: { item: ColorReference; values: Record }) { + const definition = item.token ? runtimeThemeContract[item.token] : undefined + const property = item.property ?? definition?.property + return
+ {property &&
} +

{item.name}

{property ? colorHex(values[property]) : "미정의 · HEX 없음"}

+

{item.status}

{item.note}

+ {item.foreground && property &&

한맥 프로젝트 · Hanmac Project · 0123456789

} +
Canonical Token
{item.token ?? "미정의"}
CSS Variable
{property ?? "미정의"}
+ {item.property && definition && <>
현재 Semantic 연결
{definition.property}
} + {item.legacy && <>
기존 참고 명칭
{item.legacy}
} +
+
+} +function GradientPreview({ name, property, values }: { name: string; property: string; values: Record }) { + const value = values[property] + const parts = gradientParts(value) + return <> +
+
현재 CSS Variable
{property}
현재 Theme 적용 결과 · Computed CSS
{value ?? "확인 중"}
방향
{parts?.direction ?? "NOT VERIFIED"}
+ {parts ?
{parts.stops.map((stop, index) =>
+

{stop.position}

{colorHex(stop.color)}

+
)}
:

{value === undefined ? "Stop 확인 중" : "Stop 미정의 또는 해석 불가 · NOT VERIFIED"}

} + +} function ColorReferencePage() { - return + const values = useResolvedProperties(colorProperties) + return {palettes.map(palette => -
{stages.map(stage => )}
+
{stages.map(stage => )}
{palette.legacy.length > 0 && <>

기존 버튼 그라데이션 색상 · 브랜드 단계와 별도 유지

-
{palette.legacy.map(stage => )}
+
{palette.legacy.map(stage => )}
}
)} + +
+
+ + {semanticGroups.map(group =>

{group.name}

{group.colors.map(item => )}
)} +
+ +
{gradients.map(gradient =>
+

{gradient.name}

{gradient.note}

기존 참고 명칭: {gradient.legacy}

+ {gradient.property ? : <>

공통 Background: 미정의 · Canonical Token / CSS Variable 없음

+ {gradient.componentProperty &&

기존 Button 전용 Background · Confirmed

현행 Component CSS만 표시합니다. 공통 5-stop 배경의 대체 또는 확정이 아닙니다. 전체 Gradient canonical은 없으며 stop 색상은 공개 Palette를 소비합니다.

} + } +
)}
+
} const meta = { title: "Foundation/Color", parameters: { layout: "fullscreen" } } satisfies Meta diff --git a/src/foundation/foundation.css b/src/foundation/foundation.css index 9340e70..274c2f4 100644 --- a/src/foundation/foundation.css +++ b/src/foundation/foundation.css @@ -30,3 +30,9 @@ .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); } .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. */ +.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; } +.hm-foundation .hm-color-foreground-sample { padding: 16px; background: var(--hm-sem-bg-surface); }