docs(storybook): simplify Foundation Color to brand palettes

This commit is contained in:
shkim4 committed 2026-10-11 15:20:13 +09:00
1 parent d6d6eb53af
commit 09321e7d14
6 files changed
+85 -121

No files matched your search

+20 -117
View File
@@ -1,49 +1,21 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { useEffect, useRef, useState } from "react"
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).
// 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 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", token: "color.status.error", status: "Provisional", note: "일반 상태 오류를 위한 독립 Semantic Token입니다. 사용 가능 상태만 승인되었으며 기존 Input / Alert / Toast / Badge 오류색과 Action Destructive mapping은 유지합니다. 실제 Component 적용은 별도 사용자 결정이 필요합니다." },
] },
{ 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", stopPrefix: undefined, legacy: "gradient.green.background", note: "현행 공통 브랜드 배경 · Confirmed. 공개 stop 계약은 color.bg.brand.stop.*이며 방향·위치는 현행 CSS에서 읽습니다. Dashboard의 Header / Compact 배경은 별도 정의입니다." },
{ name: "HM Brown Background", property: "--hm-sem-bg-brown", stopPrefix: "color.bg.brown.stop", legacy: "gradient.brown.background", note: "공통 Brown Background · Confirmed. 확정 Brown Palette를 참조하는 독립 5-stop 배경입니다. Runtime JSON은 5개 stop 전체를 지정하며 Button·Dashboard와 별도 역할입니다." },
{ name: "Orange Background", property: "--hm-sem-bg-orange", stopPrefix: "color.bg.orange.stop", legacy: "gradient.orange.background", note: "공통 Orange Background · Confirmed. 확정 Orange Palette를 참조하는 독립 5-stop 배경입니다. Runtime JSON은 5개 stop 전체를 지정하며 Button·Dashboard와 별도 역할입니다." },
]
const colorProperties = [...new Set([
...Object.values(runtimeThemeContract).filter(definition => definition.type === "color").map(definition => definition.property),
"--hm-input-error-border-color",
...gradients.map(gradient => gradient.property),
])]
{ name: "한맥그린", prefix: "green" },
{ name: "한맥브라운", prefix: "brown" },
{ name: "한맥오렌지", prefix: "orange" },
] as const
type ColorStage = typeof stages[number]
type PalettePrefix = typeof palettes[number]["prefix"]
const stageLabel = (stage: ColorStage) => stage === "default" ? "Default · 기본색" : stage.replaceAll("-", " ").replace(/^./, letter => letter.toUpperCase())
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, never the legacy document HEX. Keep unsupported values visible.
// Read the current CSS value rather than duplicating Theme HEX values.
function colorHex(value: string | undefined) {
if (value === undefined) return "확인 중"
if (!value) return "미정의"
@@ -51,91 +23,22 @@ function colorHex(value: string | undefined) {
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<string[]>((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<string, string> }) {
const token = `color.palette.${prefix}-${stage}` as RuntimeThemeToken
const definition = confirmed ? runtimeThemeContract[token] : undefined
return <article className="hm-brand-color-chip" data-color-stage={stage} data-color-token={definition ? token : undefined} data-base={stage === "default" ? "" : undefined}>
<div className="hm-foundation-swatch" style={definition ? { background: `var(${definition.property})` } : undefined} />
<h3>{stageLabel(stage)}</h3><p data-color-value>{definition ? colorHex(values[definition.property]) : "미확정"}</p>
{definition && <details><summary>{paletteStatus(stage)} · Token</summary><dl><dt>Canonical Token</dt><dd><code>{token}</code></dd><dt>CSS Variable</dt><dd><code>{definition.property}</code></dd></dl></details>}
function ColorChip({ prefix, stage, values }: { prefix: PalettePrefix; stage: ColorStage; values: Record<string, string> }) {
const token = paletteToken(prefix, stage)
const definition = runtimeThemeContract[token]
return <article className="hm-brand-color-chip" data-color-stage={stage} data-color-token={token} data-base={stage === "default" ? "" : undefined}>
<div className="hm-foundation-swatch" style={{ background: `var(${definition.property})` }} />
<h3>{stageLabel(stage)}</h3><p data-color-value>{colorHex(values[definition.property])}</p>
</article>
}
const paletteStatus = (stage: string) => stages.includes(stage as typeof stages[number]) ? "Confirmed" : "Confirmed · 기존 Button 전용"
function SemanticColor({ item, values }: { item: ColorReference; values: Record<string, string> }) {
const definition = item.token ? runtimeThemeContract[item.token] : undefined
const property = item.property ?? definition?.property
return <article className="hm-foundation-card" data-color-name={item.name} data-color-token={item.token}>
{property && <div className="hm-foundation-swatch" data-color-property={property} style={{ background: `var(${property})` }} />}
<h3>{item.name}</h3><p data-color-value>{property ? colorHex(values[property]) : "미정의 · HEX 없음"}</p>
<p>{item.status}</p><p className="hm-foundation-note">{item.note}</p>
{item.foreground && property && <p className="hm-color-foreground-sample" style={{ color: `var(${property})` }}>한맥 프로젝트 · Hanmac Project · 0123456789</p>}
<dl><dt>Canonical Token</dt><dd><code>{item.token ?? "미정의"}</code></dd><dt>CSS Variable</dt><dd><code>{property ?? "미정의"}</code></dd>
{item.property && definition && <><dt>현재 Semantic 연결</dt><dd><code>{definition.property}</code></dd></>}
{item.legacy && <><dt>기존 참고 명칭</dt><dd><code>{item.legacy}</code></dd></>}
</dl>
</article>
}
function GradientPreview({ name, property, stopPrefix, values }: { name: string; property: string; stopPrefix?: string; values: Record<string, string> }) {
const preview = useRef<HTMLDivElement>(null)
const [backgroundImage, setBackgroundImage] = useState<string>()
useEffect(() => {
if (preview.current) setBackgroundImage(getComputedStyle(preview.current).backgroundImage)
}, [property, values])
const value = values[property]
const parts = gradientParts(value)
return <>
<div ref={preview} className="hm-foundation-swatch hm-color-gradient-preview" data-gradient-property={property} style={{ backgroundImage: `var(${property})` }} aria-label={`${name} Gradient 미리보기`} />
<dl><dt>현재 CSS Variable · Derived Gradient</dt><dd><code>{property}</code></dd><dt>현재 Theme 적용 결과 · Computed CSS Variable</dt><dd><code data-gradient-value>{value ?? "확인 중"}</code></dd><dt>실제 Computed background-image</dt><dd><code data-gradient-background-image>{backgroundImage ?? "확인 중"}</code></dd><dt>방향</dt><dd>{parts?.direction ?? "NOT VERIFIED"}</dd></dl>
{parts ? <div className="hm-color-gradient-stops">{parts.stops.map((stop, index) => {
const token = stopPrefix ? `${stopPrefix}.${stop.position.replace("%", "")}` as RuntimeThemeToken : undefined
const definition = token ? runtimeThemeContract[token] : undefined
return <article className="hm-brand-color-chip" key={index} data-gradient-stop={stop.position} data-gradient-stop-token={token}>
<div className="hm-foundation-swatch" style={{ background: stop.color }} /><h3>{stop.position}</h3><p data-stop-value>{colorHex(stop.color)}</p>
{definition && <dl><dt>Canonical Token</dt><dd><code>{token}</code></dd><dt>CSS Variable</dt><dd><code>{definition.property}</code></dd></dl>}
</article>
})}</div> : <p>{value === undefined ? "Stop 확인 중" : "Stop 미정의 또는 해석 불가 · NOT VERIFIED"}</p>}
</>
}
function ColorReferencePage() {
const values = useResolvedProperties(colorProperties)
return <ReferencePage title="Color" intro="브랜드 8단계와 Accent · Semantic · Gradient의 현재 CSS 값을 비교합니다. Default Theme를 기본으로 실제 적용값을 읽으며 미정의·잠정 항목은 확정하거나 대체하지 않습니다.">
{palettes.map(palette => <ReferenceSection key={palette.name} title={palette.name} note="Light 4 → Light 3 → Light 2 → Light 1 → Default → Dark 1 → Dark 2 → Dark 3.">
<div className="hm-brand-color-row">{stages.map(stage => <ColorChip key={stage} prefix={palette.prefix} stage={stage} confirmed={palette.confirmed.includes(stage)} values={values} />)}</div>
{palette.legacy.length > 0 && <>
<p>기존 버튼 그라데이션 색상 · 브랜드 단계와 별도 유지</p>
<div className="hm-brand-color-row">{palette.legacy.map(stage => <ColorChip key={stage} prefix={palette.prefix} stage={stage} values={values} />)}</div>
</>}
return <ReferencePage title="Color" intro="한맥 브랜드 컬러">
{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>
</ReferenceSection>)}
<ReferenceSection title="Accent Colors" note="Yellow는 color.palette.yellow-accent입니다. color.accent(Green Tint)와 별도 Token으로 유지합니다.">
<div className="hm-foundation-grid"><SemanticColor values={values} item={{ name: "Yellow", token: "color.palette.yellow-accent", status: "Confirmed", legacy: "hm-accent-yellow / --hm-accent-yellow", note: "현재 --hm-yellow-accent Primitive에 연결된 Yellow. Green 맥락 강조 및 LNB 그룹 선택 텍스트에 사용합니다." }} /></div>
</ReferenceSection>
<ReferenceSection title="Semantic Colors" note="역할별 현행 공개 계약·Component mapping을 표시합니다. 같은 HEX라도 별도 역할을 임의로 합치지 않습니다.">
{semanticGroups.map(group => <div className="hm-foundation-specimen" key={group.name}><h3>{group.name}</h3><div className="hm-foundation-grid">{group.colors.map(item => <SemanticColor key={item.name} item={item} values={values} />)}</div></div>)}
</ReferenceSection>
<ReferenceSection title="Gradients" note="공통 Background Gradient를 가로형 카드에서 비교합니다. Stop과 방향은 현재 선언의 적용값에서 읽습니다.">
<div className="hm-foundation-specimens">{gradients.map(gradient => <article className="hm-foundation-panel" key={gradient.name}>
<h3>{gradient.name}</h3><p className="hm-foundation-note">{gradient.note}</p><p>기존 참고 명칭: <code>{gradient.legacy}</code></p>
<GradientPreview name={gradient.name} property={gradient.property} stopPrefix={gradient.stopPrefix} values={values} />
</article>)}</div>
</ReferenceSection>
</ReferencePage>
}
const meta = { title: "Foundation/Color", parameters: { layout: "fullscreen" } } satisfies Meta