docs(storybook): expose semantic colors and gradients
This commit is contained in:
1 parent
91c8e5e14e
commit
3ecc8c7402
3 files changed
+199
-6
No files matched your search
@@ -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<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>{definition ? definition.defaultValue.toUpperCase() : "미확정"}</p>
|
||||
<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>}
|
||||
</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, values }: { name: string; property: string; values: Record<string, string> }) {
|
||||
const value = values[property]
|
||||
const parts = gradientParts(value)
|
||||
return <>
|
||||
<div className="hm-foundation-swatch hm-color-gradient-preview" data-gradient-property={property} style={{ backgroundImage: `var(${property})` }} aria-label={`${name} Gradient 미리보기`} />
|
||||
<dl><dt>현재 CSS Variable</dt><dd><code>{property}</code></dd><dt>현재 Theme 적용 결과 · Computed CSS</dt><dd><code data-gradient-value>{value ?? "확인 중"}</code></dd><dt>방향</dt><dd>{parts?.direction ?? "NOT VERIFIED"}</dd></dl>
|
||||
{parts ? <div className="hm-color-gradient-stops">{parts.stops.map((stop, index) => <article className="hm-brand-color-chip" key={index} data-gradient-stop={stop.position}>
|
||||
<div className="hm-foundation-swatch" style={{ background: stop.color }} /><h3>{stop.position}</h3><p data-stop-value>{colorHex(stop.color)}</p>
|
||||
</article>)}</div> : <p>{value === undefined ? "Stop 확인 중" : "Stop 미정의 또는 해석 불가 · NOT VERIFIED"}</p>}
|
||||
</>
|
||||
}
|
||||
function ColorReferencePage() {
|
||||
return <ReferencePage title="Color" intro="한맥 브랜드 컬러의 확정된 8단계를 비교합니다. 한맥오렌지 Dark 1·2·3은 최신 사용자 확정값을 반영하며 기본색을 강조합니다.">
|
||||
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)} />)}</div>
|
||||
<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} />)}</div>
|
||||
<div className="hm-brand-color-row">{palette.legacy.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="실제 CSS Gradient를 가로형 카드에서 비교합니다. Stop과 방향은 현재 선언의 적용값에서 읽습니다. 공통 배경과 Button 전용 배경은 별도 역할입니다.">
|
||||
<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>
|
||||
{gradient.property ? <GradientPreview name={gradient.name} property={gradient.property} values={values} /> : <><p>공통 Background: 미정의 · Canonical Token / CSS Variable 없음</p>
|
||||
{gradient.componentProperty && <div className="hm-foundation-specimen"><h3>기존 Button 전용 Background · Confirmed</h3><p>현행 Component CSS만 표시합니다. 공통 5-stop 배경의 대체 또는 확정이 아닙니다. 전체 Gradient canonical은 없으며 stop 색상은 공개 Palette를 소비합니다.</p><GradientPreview name={`${gradient.name} Button`} property={gradient.componentProperty} values={values} /></div>}
|
||||
</>}
|
||||
</article>)}</div>
|
||||
</ReferenceSection>
|
||||
</ReferencePage>
|
||||
}
|
||||
const meta = { title: "Foundation/Color", parameters: { layout: "fullscreen" } } satisfies Meta
|
||||
|
||||
Vendored
+6
@@ -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); }
|
||||
Reference in new issue
Block a user