test(sw): align regression tests with current components

This commit is contained in:
shkim4 committed 2026-11-10 14:53:06 +09:00
1 parent 3cbe879910
commit b01ccaca01
5 files changed
+282 -31

No files matched your search

+198
View File
@@ -0,0 +1,198 @@
// Test-only host for property-fields.browser.mjs (2026-11-10). Not a Storybook Story and not shipped (package "files" is
// dist + ai). It restores the host data and host controls of the two demos removed in 5faa1f1 that the field checks
// need: SW/Header › Property Fields (data-fields live / default / pinned hover) and the Header-hosted Property Panel
// (원 1개 selection). Records and controls are copied from src/sw/sw-header.stories.tsx at 5faa1f1^ without changes.
// Uses only the public @hm-design/sw API, like a consuming project. The pinned Hover column uses the Storybook pseudo-state
// mirror (.storybook/pseudo-states.ts) exactly as the retired Story did.
import { useEffect, useRef, useState, type ReactNode } from "react"
import { createRoot } from "react-dom/client"
import {
SWHeader, SWHeaderPropertyPanel, SWTheme, swHeaderLinePatterns,
type SWHeaderColorMode, type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderLineType, type SWHeaderLineWeight,
type SWHeaderProperty, type SWHeaderPropertyGroup, type SWHeaderPropertyMenus, type SWHeaderPropertySelection,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
import { ensurePseudoStates } from "../../../../.storybook/pseudo-states"
const FIGMA_COLORS: SWHeaderColorOption[] = [
{ id: "white", color: "#ffffff", label: "흰색" },
{ id: "red", color: "#f10404", label: "빨강" },
{ id: "green", color: "#48ff08", label: "초록" },
{ id: "yellow", color: "#ffff00", label: "노랑" },
{ id: "blue", color: "#0012ff", label: "파랑" },
{ id: "magenta", color: "#ff00f6", label: "자홍" },
{ id: "cyan", color: "#00f6ff", label: "청록" },
]
const FIGMA_LINE_TYPES: SWHeaderLineType[] = [
{ id: "byblock", name: "ByBlock" },
{ id: "bylayer", name: "ByLayer" },
{ id: "continuous", name: "Continuous" },
{ id: "center", name: "CENTER", pattern: swHeaderLinePatterns.center },
{ id: "hidden", name: "HIDDEN", pattern: swHeaderLinePatterns.hidden },
]
const FIGMA_LINE_WEIGHTS: SWHeaderLineWeight[] = [
{ id: "w-bylayer", name: "ByLayer" },
{ id: "w-byblock", name: "ByBlock" },
{ id: "w-default", name: "ByDefault", value: "0.25" },
{ id: "w-user", name: "User", value: "0.25", editable: true },
]
const PROPERTY_GROUPS: SWHeaderPropertyGroup[] = [
{ id: "common", collapsedCount: 6 },
{ id: "shape", name: "형상", layout: "circle" },
{ id: "position", name: "위치", layout: "position", positionLabel: "중간점" },
]
const PROPERTY_FIXTURE: SWHeaderProperty[] = [
{ id: "layer", name: "레이어", value: "0_Layer_Cludmark", type: "text", groupId: "common", picker: "layer", validate: draft => draft.length <= 40 },
{ id: "color", name: "색상", value: "255,229,0", type: "color", groupId: "common" },
{ id: "line", name: "선스타일", value: "Continuous", type: "select", groupId: "common", unit: "(ByL)", linePreview: true, options: [{ id: "Continuous", name: "Continuous" }, { id: "CENTER", name: "CENTER" }, { id: "HIDDEN", name: "HIDDEN" }] },
{ id: "weight", name: "선가중치", value: "0.25", type: "number", groupId: "common", min: 0 },
{ id: "scale", name: "선축적", value: "1.0000", type: "number", groupId: "common", min: .0001, decimals: 4, unit: "(LTscale:1)" },
{ id: "opacity", name: "투명도", value: "0.25", type: "slider", groupId: "common", min: 0, max: 1, step: .01, annotation: "(ByL)", unit: "%" },
{ id: "link", name: "하이퍼링크", value: "--", type: "text", groupId: "common" },
{ id: "thickness", name: "두께", value: "0.25", type: "number", groupId: "common", unit: "mm", min: 0 },
{ id: "material", name: "재료", value: "-", type: "computed", groupId: "common", unit: "(ByL)" },
{ id: "circumference", name: "원주", value: "0.25", type: "computed", groupId: "shape", graphicSlot: "circumference" },
{ id: "diameter", name: "지름", value: "8.1502", type: "number", groupId: "shape", graphicSlot: "diameter", min: 0 },
{ id: "radius", name: "반지름", value: "4.0526", type: "number", groupId: "shape", graphicSlot: "radius", min: 0 },
{ id: "area", name: "면적", value: "51.5962", type: "computed", groupId: "shape", graphicSlot: "area" },
{ id: "x", name: "X", value: "48.0808", type: "number", groupId: "position" },
{ id: "y", name: "Y", value: "14.9160", type: "number", groupId: "position" },
{ id: "z", name: "Z", value: "0.0000", type: "number", groupId: "position" },
]
const PROPERTY_COUNTS = [
{ id: "shape", name: "도형", count: 10, row: 1, column: 1 }, { id: "line", name: "직선", count: 10, row: 1, column: 2 },
{ id: "circle", name: "원", count: 1, row: 1, column: 3 }, { id: "ray", name: "광선", count: 1, row: 1, column: 4 },
{ id: "curve", name: "곡선", count: 10, row: 2, column: 3 }, { id: "arc", name: "호", count: 1, row: 2, column: 4 },
{ id: "polyline", name: "폴리선", count: 10, row: 3, column: 3 }, { id: "polyline2", name: "폴리선", count: 2, row: 3, column: 4 },
{ id: "text1", name: "문자", count: 1, row: 4, column: 1 }, { id: "text2", name: "문자", count: 1, row: 4, column: 2 },
{ id: "text3", name: "문자", count: 1, row: 4, column: 3 },
]
type PropertyExample = "none" | "all" | "circle" | "expanded"
function propertySelection(example: PropertyExample): SWHeaderPropertySelection {
return example === "none" ? { id: "none", title: "선택된 객체 - 없음", kind: "none" }
: example === "all" ? { id: "all", title: "선택된 객체 - 전체 11", kind: "all", counts: PROPERTY_COUNTS }
: { id: "circle", title: "선택된 객체 - 원1", kind: "single", overviewTitle: "선택된 객체 - 전체 11" }
}
function propertyFixture(example: PropertyExample) {
return PROPERTY_FIXTURE.filter(item => (example === "none" || example === "all") ? item.groupId === "common" : true).map(item => ({
...item, ...(example === "none" && item.id === "layer" ? { value: "0" } : {}),
...(example === "none" && item.id === "color" ? { value: "138,138,138" } : {}),
...(example === "all" && item.id === "layer" ? { mixed: true } : {}),
...((example === "none" || example === "all") && item.id === "opacity" ? { value: "0" } : {}),
}))
}
const PROPERTY_LAYERS: SWHeaderLayer[] = [
{ id: "0", name: "0" },
{ id: "0_Layer_Cludmark", name: "0_Layer_Cludmark", color: "#ff0000" },
{ id: "CC-D GNL", name: "CC-D GNL" },
{ id: "Sample-Wall", name: "Sample-Wall", color: "#ffe500", visible: false },
{ id: "Sample-Door", name: "Sample-Door", color: "#00ff00", frozen: true },
{ id: "Sample-Text", name: "Sample-Text", color: "#00ffff", locked: true },
{ id: "Sample-Defpoints", name: "Sample-Defpoints", color: "#808080", disabled: true },
]
const PROPERTY_LINE_TYPES: SWHeaderLineType[] = FIGMA_LINE_TYPES.map(item => ({ ...item, id: item.name }))
const linePatternOf = (value: string) => PROPERTY_LINE_TYPES.find(item => item.id === value)?.pattern
function usePropertyMenus(record: (line: string) => void, setProperties: (update: (items: SWHeaderProperty[]) => SWHeaderProperty[]) => void): SWHeaderPropertyMenus {
const [layers, setLayers] = useState(PROPERTY_LAYERS)
const [weights, setWeights] = useState(FIGMA_LINE_WEIGHTS)
const [weightId, setWeightId] = useState("w-default")
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>(null)
const setValue = (id: string, value: string) => setProperties(items => items.map(item => item.id === id ? { ...item, value, mixed: false } : item))
const scale = "1.0000"
const toggle = (key: "visible" | "frozen" | "locked") => (layer: SWHeaderLayer, next: boolean) => {
record(`layer.${key}(${layer.id}, ${next})`)
setLayers(items => items.map(item => item.id === layer.id ? { ...item, [key]: next } : item))
}
return {
layers, onLayerVisibilityChange: toggle("visible"), onLayerFreezeChange: toggle("frozen"), onLayerLockChange: toggle("locked"),
onLayerDetail: () => record("layerDetail"),
lineTypes: PROPERTY_LINE_TYPES, lineWeights: weights, activeLineWeightId: weightId, lineScale: scale,
onLineWeightSelect: weight => { record(`lineWeight(${weight.id})`); setWeightId(weight.id); if (weight.value) setValue("weight", weight.value) },
onLineWeightValueChange: (weight, value) => { record(`lineWeightValue(${weight.id}, ${value})`); setWeights(items => items.map(item => item.id === weight.id ? { ...item, value } : item)) },
onLineScaleChange: value => { record(`lineScale(${value})`); setValue("scale", Number(value).toFixed(4)) },
onLineStyleDetail: () => record("lineStyleDetail"),
colors: FIGMA_COLORS, colorMode,
onColorModeChange: mode => { record(`colorMode(${mode})`); setColorMode(mode) },
onColorPick: property => record(`colorPick(${property.id})`),
onColorDetail: () => record("colorDetail"),
}
}
function PinHover({ selectors, children }: { selectors: string[]; children: ReactNode }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
ensurePseudoStates()
ref.current?.querySelectorAll(selectors.join(",")).forEach(element => element.setAttribute("data-sb-hover", ""))
}, [selectors])
return <div ref={ref}>{children}</div>
}
/* SW/Header › Property Fields (retired in 5faa1f1): the common rows of PROPERTY_FIXTURE plus a mixed layer row. Figma
sample text 255,259,0 is out of range: shown as host text only, never accepted as input. */
const FIELD_FIXTURE: SWHeaderProperty[] = PROPERTY_FIXTURE.filter(item => item.groupId === "common").flatMap(item =>
item.id === "layer" ? [{ ...item, id: "layer-mixed", mixed: true }, item]
: item.id === "color" ? [{ ...item, value: "255,259,0", swatch: "#ffe500" }]
: [item])
const FIELD_HOVER = [".hm-sw-property__value[data-field]", ".hm-sw-property__compact"]
function FieldPanel({ title, properties, onChange, menus }: { title: string; properties: SWHeaderProperty[]; onChange?: (property: SWHeaderProperty, value: string) => void; menus?: SWHeaderPropertyMenus }) {
return <SWTheme><SWHeaderPropertyPanel selection={{ id: title, title, kind: "none" }} properties={properties} groups={[{ id: "common" }]} onPropertyChange={onChange} menus={menus} /></SWTheme>
}
function PropertyFieldsHost() {
const [properties, setProperties] = useState(FIELD_FIXTURE)
const [log, setLog] = useState<string[]>([])
const record = (line: string) => setLog(lines => [...lines.slice(-11), line])
const menus = usePropertyMenus(record, setProperties as (update: (items: SWHeaderProperty[]) => SWHeaderProperty[]) => void)
const [noMenus, setNoMenus] = useState(false)
const patch = (id: string, change: Partial<SWHeaderProperty>) => setProperties(items => items.map(item => item.id === id ? { ...item, ...change } : item))
const change = (property: SWHeaderProperty, value: string) => {
record(`property(${property.id}, ${value})`)
patch(property.id, { value, mixed: false, ...(property.id === "line" ? { linePattern: linePatternOf(value) } : {}), ...(property.id === "color" ? { swatch: undefined } : {}) })
}
return <div data-fields-demo="">
<div><strong>외부 데이터</strong>
<button data-action="layer-mixed" onClick={() => patch("layer-mixed", { mixed: !properties.find(item => item.id === "layer-mixed")?.mixed })}>레이어 다양함 ↔ 단일</button>
<button data-action="layer-external" onClick={() => patch("layer", { value: "CC-D GNL" })}>레이어 외부 값 변경</button>
<button data-action="scale-external" onClick={() => patch("scale", { value: "2.5000" })}>선축적 외부 값 변경</button>
<button data-action="thickness-unit" onClick={() => patch("thickness", { unit: properties.find(item => item.id === "thickness")?.unit === "mm" ? "cm" : "mm" })}>두께 단위 변경</button>
<button data-action="line-center" onClick={() => patch("line", { value: "CENTER", linePattern: linePatternOf("CENTER") })}>선스타일 외부 CENTER</button>
<button data-action="readonly" onClick={() => setProperties(items => items.map(item => item.id === "thickness" || item.id === "layer" ? { ...item, readOnly: !item.readOnly } : item))}>두께·레이어 ReadOnly</button>
<button data-action="disabled" onClick={() => setProperties(items => items.map(item => item.id === "scale" || item.id === "color" ? { ...item, disabled: !item.disabled } : item))}>선축적·색상 Disabled</button>
<button data-action="add-row" onClick={() => setProperties(items => [...items, { id: `extra-${items.length}`, name: "추가 속성", value: "호스트 값", type: "text", groupId: "common" }])}>속성 추가</button>
<button data-action="remove-row" onClick={() => setProperties(items => items.filter(item => item.id !== "link"))}>하이퍼링크 삭제</button>
<button data-action="no-menus" onClick={() => setNoMenus(value => !value)}>menus 없음(options 사용): {String(noMenus)}</button>
</div>
<p data-property-log="" aria-live="polite">{log.join(" → ") || "Callback 없음"}</p>
<div style={{ display: "flex", gap: 24, alignItems: "flex-start" }}>
<figure style={{ margin: 0 }} data-fields="live"><FieldPanel title="실제 조작" properties={properties} onChange={change} menus={noMenus ? undefined : menus} /></figure>
<figure style={{ margin: 0 }} data-fields="default"><FieldPanel title="Default" properties={FIELD_FIXTURE} /></figure>
<figure style={{ margin: 0 }} data-fields="hover"><PinHover selectors={FIELD_HOVER}><FieldPanel title="Hover" properties={FIELD_FIXTURE} /></PinHover></figure>
</div>
</div>
}
/* SW/Header › Property Panel (retired in 5faa1f1): SWHeader with the propertyPanel prop and the selection buttons. */
function HeaderPanelHost() {
const [selection, setSelection] = useState(propertySelection("none"))
const [properties, setProperties] = useState(propertyFixture("none"))
const [expanded, setExpanded] = useState<string[]>([])
const menus = usePropertyMenus(() => {}, setProperties)
const changeExample = (next: PropertyExample) => {
setSelection(propertySelection(next)); setProperties(propertyFixture(next))
setExpanded(next === "expanded" ? ["common"] : [])
}
const panel = {
selection, properties, groups: PROPERTY_GROUPS, expandedGroupIds: expanded, menus,
onExpandedGroupsChange: setExpanded,
onPropertyChange: (property: SWHeaderProperty, value: string) => setProperties(items => items.map(item => item.id === property.id ? { ...item, value, mixed: false } : item)),
onOverviewClick: () => changeExample("all"),
}
return <div data-property-demo="">
<div>{(["none", "all", "circle", "expanded"] as const).map((name, index) => <button key={name} data-example-button={name} onClick={() => changeExample(name)}>{["선택 없음", "전체 선택", "원 1개", "모두 펼침"][index]}</button>)}</div>
<div style={{ width: 1920, minHeight: 820 }}>
<SWHeader documentTitle="C0020101-006-평면및종단면도(06)" drawingModeLabel="그리기모드" layer={{ name: "0_Layer_Cludmark" }} color={{ value: "255,229,0", swatch: "#ffe500" }}
lineStyle={{ name: "Continuous", weight: "0.25", scale: "1.00" }} textStyle={{ font: "Arial", name: "Standard" }} dimensionStyle={{ name: "Standard" }} propertyPanel={panel} />
</div>
</div>
}
const view = new URLSearchParams(location.search).get("view")
createRoot(document.getElementById("root")!).render(view === "header" ? <HeaderPanelHost /> : <PropertyFieldsHost />)