feat: integrate latest HMDesign design system updates

This commit is contained in:
DESKTOP-KPCP6AL\ㅅㅎ committed 2026-10-11 11:09:03 +09:00
1 parent 99faec189e
commit 1c4e0bb9a1
209 files changed
+8845 -552

No files matched your search

+25 -18
View File
@@ -1,10 +1,12 @@
import { useEffect, useRef, useState, useSyncExternalStore } from "react"
import { flushSync } from "react-dom"
import type { Meta, StoryObj } from "@storybook/react-vite"
import { defaultRuntimeTheme, runtimeThemeContract, fetchRuntimeTheme, type RuntimeThemeToken } from "@hm-design/ui/theme"
import { designerThemeStore } from "./designer-preview"
import { Specimens } from "./specimens"
import { DesignerExtras } from "./designer-extras"
import { StoryPage } from "@/components/story-page"
import { VerificationStyles, waitForThemeState } from "./verification-state"
import "./designer-theme.css"
const roles = [
@@ -12,7 +14,7 @@ const roles = [
["color.state.active", "Active / Selected", "Checkbox · Radio · Switch · Calendar"],
["color.on.state.active", "On Active", "선택된 컨트롤의 아이콘과 글자"],
["color.stroke.input.focus", "Focus", "입력 테두리 · Checkbox / Calendar 포커스 링"],
["color.accent", "Accent", "메뉴 · Calendar · Table · Pagination의 45% 강조 면"],
["color.accent", "Accent", "메뉴 · Calendar · Pagination 45%, Table 행 20% 강조 면"],
["color.on.accent", "On Accent", "강조 면의 글자"],
["color.action.destructive", "Destructive", "오류 · Invalid · Destructive Button / Badge"],
["color.bg.surface", "Surface", "입력 · Card · Popup"],
@@ -110,7 +112,7 @@ function ThemeEditor() {
return <StoryPage><main className="hm-designer">
<header className="hm-designer-header">
<div><p>HM DESIGN SYSTEM / COLOR THEME</p><h1>디자인팀 Theme Playground</h1>
<p>컬러를 바꾸면 이 화면과 다른 Story의 UI에 즉시 반영됩니다. 현재 탭의 세션 동안 유지됩니다.</p></div>
<p>임시 컬러는 이 편집기와 Live Preview에만 적용됩니다. 편집값은 현재 탭의 세션 동안 보관하며 다른 Story는 HM Default로 검수합니다.</p></div>
<button type="button" onClick={reset}>Reset to HM Default</button>
</header>
<div className="hm-designer-layout">
@@ -156,21 +158,20 @@ function ThemeEditor() {
</main></StoryPage>
}
const meta = { title: "Theme/Designer Playground", parameters: { layout: "fullscreen" } } satisfies Meta
const meta = { title: "Theme/Designer Playground", tags: ["internal"], parameters: { layout: "fullscreen" } } satisfies Meta
export default meta
type Story = StoryObj<typeof meta>
export const Editor: Story = { name: "Designer Playground", render: () => <ThemeEditor /> }
export const LiveVerification: Story = {
tags: ["verification"],
render: () => <><output data-testid="designer-result">Ready</output><ThemeEditor /></>,
render: () => <div data-hm-theme-verification><VerificationStyles /><output data-testid="designer-result">Ready</output><ThemeEditor /></div>,
play: async ({ canvasElement }) => {
const result = canvasElement.querySelector<HTMLOutputElement>('[data-testid="designer-result"]')!
const preview = canvasElement.querySelector<HTMLElement>('[aria-label="Live component preview"]')!
const origin = performance.timeOrigin
const checks: string[] = []
const assert = (ok: boolean, label: string) => { if (!ok) throw Error(label); checks.push(label) }
const settle = () => new Promise(resolve => setTimeout(resolve, 250))
const variable = (property: string) => getComputedStyle(document.documentElement).getPropertyValue(property).trim().toLowerCase()
const style = (selector: string, property: "backgroundColor" | "color" | "borderTopColor") => getComputedStyle(preview.querySelector<HTMLElement>(selector)!)[property]
const geometry = () => Array.from(preview.querySelectorAll<HTMLElement>("[data-slot]")).map(element => {
@@ -180,13 +181,14 @@ export const LiveVerification: Story = {
const setInput = (label: string, value: string) => {
const input = canvasElement.querySelector<HTMLInputElement>(`[aria-label="${label}"]`)!
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, value)
input.dispatchEvent(new Event("input", { bubbles: true }))
flushSync(() => input.dispatchEvent(new Event("input", { bubbles: true })))
}
const click = (label: string) => Array.from(canvasElement.querySelectorAll<HTMLButtonElement>("button")).find(button => button.textContent === label)!.click()
const click = (label: string) => flushSync(() => Array.from(canvasElement.querySelectorAll<HTMLButtonElement>("button")).find(button => button.textContent === label)!.click())
try {
result.textContent = "Running designer workflow verification…"
await document.fonts.ready
click("Reset to HM Default"); await settle()
click("Reset to HM Default")
await waitForThemeState(() => roles.every(([token, label]) => canvasElement.querySelector<HTMLInputElement>(`[aria-label="${label} HEX"]`)?.value === defaultRuntimeTheme.tokens[token]), "Designer default inputs")
const initial = JSON.stringify(geometry())
for (const [token, label] of roles) {
assert(canvasElement.querySelector<HTMLInputElement>(`[aria-label="${label} HEX"]`)!.value === defaultRuntimeTheme.tokens[token], `${label}: auto-loaded HM Default`)
@@ -200,14 +202,17 @@ export const LiveVerification: Story = {
] as const
for (const [label, property, value] of core) {
const others = core.filter(row => row[1] !== property).map(row => [row[1], variable(row[1])])
setInput(`${label} HEX`, value); await settle()
setInput(`${label} HEX`, value)
await waitForThemeState(() => variable(property) === value, `${label}: live input`)
assert(variable(property) === value, `${label}: input applies live`)
if (label === "Primary / Brand") assert(canvasElement.querySelector<HTMLInputElement>('[aria-label="On Accent HEX"]')!.value === variable("--hm-sem-on-accent"), "On Accent displays its actual resolved role")
for (const [key, previous] of others) assert(variable(key) === previous, `${label}: leaves ${key} independent`)
assert(JSON.stringify(geometry()) === initial, `${label}: component geometry unchanged`)
assert(performance.timeOrigin === origin && preview.isConnected, `${label}: same document, reload 0`)
}
assert(style('[data-slot="checkbox"][aria-label="Checked"]', "backgroundColor") === "rgb(0, 127, 115)", "Checkbox fill uses Active")
const checked = preview.querySelector<HTMLElement>('[data-slot="checkbox"][aria-label="Checked"]')!
await waitForThemeState(() => checked.dataset.state === "checked" && checked.getAttribute("aria-checked") === "true" && style('[data-slot="checkbox"][aria-label="Checked"]', "backgroundColor") === "rgb(0, 127, 115)", "Designer checked Checkbox fill")
assert(checked.dataset.state === "checked" && checked.getAttribute("aria-checked") === "true" && style('[data-slot="checkbox"][aria-label="Checked"]', "backgroundColor") === "rgb(0, 127, 115)", "Checkbox fill uses Active")
assert(style('[data-slot="radio-group-item"]', "borderTopColor") === "rgb(0, 127, 115)", "Radio uses Active")
assert(style('[data-slot="switch"]', "backgroundColor") === "rgb(0, 127, 115)", "Switch uses Active")
assert(style('[data-slot="slider-range"]', "backgroundColor") === "rgb(0, 127, 115)", "Slider uses Active")
@@ -215,29 +220,31 @@ export const LiveVerification: Story = {
assert(style('[data-slot="badge"]', "backgroundColor") === "rgb(104, 55, 168)", "Badge uses Primary")
assert(style('[data-slot="pagination-link"][data-active="true"]', "backgroundColor") === "rgb(104, 55, 168)", "Pagination current uses Primary")
for (const property of ["--hm-select-option-state-background", "--hm-calendar-day-hover-background", "--hm-calendar-range-middle-background", "--hm-pagination-hover-background", "--hm-table-row-hover-background", "--hm-table-row-selected-background", "--hm-command-highlight-background", "--hm-dropdown-menu-highlight-background", "--hm-context-menu-highlight-background", "--hm-menubar-highlight-background"])
assert(variable(property).includes("#ffd36d 45%"), `${property}: live Accent 45%`)
assert(variable(property).includes(`#ffd36d ${property.startsWith("--hm-table-row") ? 20 : 45}%`), `${property}: live Accent at its component opacity`)
click("Show JSON")
await settle()
const transfer = canvasElement.querySelector<HTMLTextAreaElement>('[aria-label="Theme JSON"]')!
const exported = transfer.value
assert(JSON.parse(exported).tokens["color.state.active"] === "#007f73", "Export is official partial contract JSON")
click("Reset to HM Default"); await settle()
click("Reset to HM Default")
assert(JSON.stringify(geometry()) === initial && variable("--hm-sem-state-active") === "#266651", "Reset restores HM Default immediately")
const fillJson = (value: string) => {
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")!.set!.call(transfer, value)
transfer.dispatchEvent(new Event("input", { bubbles: true }))
flushSync(() => transfer.dispatchEvent(new Event("input", { bubbles: true })))
}
fillJson(exported); await settle(); click("Import JSON"); await settle()
fillJson(exported); click("Import JSON")
await waitForThemeState(() => variable("--hm-sem-state-active") === "#007f73", "Designer JSON import")
assert(variable("--hm-sem-state-active") === "#007f73" && variable("--hm-sem-stroke-input-focus") === "#c75b00", "Export -> Import round trip applies immediately")
for (const invalid of ["not json", '{"schemaVersion":1,"tokens":{"color.action.primary":"red"}}', '{"schemaVersion":1,"tokens":{"radius.input.default":"7px"}}', '{"schemaVersion":1,"tokens":{"--hm-checkbox-checked-background":"#123456"}}']) {
fillJson(invalid); await settle(); click("Import JSON"); await settle()
fillJson(invalid); click("Import JSON")
assert(variable("--hm-sem-state-active") === "#007f73", "Invalid/color-only import preserves last valid Theme")
}
fillJson('{"schemaVersion":1,"tokens":{"color.state.active":"#345678"}}'); await settle(); click("Import JSON"); await settle()
fillJson('{"schemaVersion":1,"tokens":{"color.state.active":"#345678"}}'); click("Import JSON")
await waitForThemeState(() => variable("--hm-sem-state-active") === "#345678", "Designer partial import")
assert(variable("--hm-sem-state-active") === "#345678" && variable("--hm-sem-action-primary") === "#123328" && variable("--hm-sem-stroke-input-focus") === "#266651", "Partial import replaces previous keys; unspecified roles use Default")
for (const [index, [token, label]] of roles.entries()) {
const value = `#3456${(120 + index).toString(16)}`
setInput(`${label} HEX`, value); await settle()
setInput(`${label} HEX`, value)
await waitForThemeState(() => designerThemeStore.getSnapshot().tokens[token] === value, `${label}: editor role`)
assert(designerThemeStore.getSnapshot().tokens[token] === value, `${label}: editor role works`)
}
assert(JSON.stringify(geometry()) === initial, "All color roles leave component layout unchanged")