import type { Meta, StoryObj } from "@storybook/react-vite" import { runtimeThemeContract, type RuntimeThemeToken } from "@hm-design/ui/theme" 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 const palettes = [ { name: "한맥그린", prefix: "green", confirmed: [...stages], legacy: [] }, { name: "한맥브라운", prefix: "brown", confirmed: [...stages], legacy: ["deep"] }, { name: "한맥오렌지", prefix: "orange", confirmed: [...stages], legacy: ["light", "mid", "dark", "deep"] }, ] const stageLabel = (stage: string) => stage === "default" ? "Default · 기본색" : stage.replaceAll("-", " ").replace(/^./, letter => letter.toUpperCase()) 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 ? 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() { const values = useResolvedProperties(colorProperties) return {palettes.map(palette =>
{stages.map(stage => )}
{palette.legacy.length > 0 && <>

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

{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 export default meta export const All: StoryObj = { name: "Color", render: () => }