Drop the category kicker from the 83 sidebar Stories (kicker is now optional; hidden verification Stories keep it) and shorten or remove repeated, obvious or internal page/section descriptions. Required specs, limits, virtual-data and undecided-value notices stay. Story IDs, exports, fixtures, shared state notes and package code are unchanged. Record: docs/reports/storybook-ui-simplification-2026-10-22.md
207 lines
15 KiB
TypeScript
207 lines
15 KiB
TypeScript
import { useId, useLayoutEffect, useRef, useState, type CSSProperties } from "react"
|
|
import {
|
|
SWHeader, parseSWHeaderColor, type SWHeaderProps, type SWHeaderProperty, type SWHeaderPropertyGroup,
|
|
type SWHeaderPropertySelection, type SWHeaderPropertyMenus, type SWHeaderColorMode, type SWHeaderLayer, type SWHeaderLineWeight,
|
|
type SWHomeMenuSectionData,
|
|
} from "@hm-design/sw"
|
|
import { StoryPage, StoryPageHeader } from "@/components/story-page"
|
|
import { showVerificationControls } from "./sw-story-flags"
|
|
|
|
type Selection = "none" | "all" | "circle" | "expanded"
|
|
interface OverviewProps {
|
|
initial: SWHeaderProps
|
|
fixtures: Required<Pick<SWHeaderProps, "documents" | "layers" | "colors" | "userColors" | "lineTypes" | "lineWeights" | "textStyles" | "dimensionStyles">>
|
|
propertyGroups: SWHeaderPropertyGroup[]
|
|
propertyFixture: (selection: Selection) => SWHeaderProperty[]
|
|
propertySelection: (selection: Selection) => SWHeaderPropertySelection
|
|
barFixture: SWHeaderProperty[]
|
|
/** Object text of the property Bar (2026-10-21: the object type, e.g. 원). Default: the selection title. */
|
|
barTitle?: string
|
|
/** Logo (home) menu of the SW normal mode: the logo opens and closes it (SWHeader homeMenu). */
|
|
homeMenuSections?: readonly SWHomeMenuSectionData[]
|
|
}
|
|
|
|
/** Storybook host only; dropdowns, validation, editing and focus use the public SW components. */
|
|
export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFixture, propertySelection, barFixture, barTitle, homeMenuSections }: OverviewProps) {
|
|
const controlId = useId()
|
|
const slotRef = useRef<HTMLDivElement>(null)
|
|
const headerRef = useRef<HTMLElement>(null)
|
|
const [size, setSize] = useState({ width: 0, height: 0 })
|
|
const [panelLeft, setPanelLeft] = useState(4)
|
|
const [example, setExample] = useState<Selection>("circle")
|
|
const getProperties = (selection: Selection) => propertyFixture(selection).map(property => ({
|
|
...property, value: barFixture.find(item => item.id === property.id)?.value ?? property.value,
|
|
}))
|
|
const [properties, setProperties] = useState(() => getProperties("circle"))
|
|
const [documents, setDocuments] = useState(fixtures.documents)
|
|
const [documentId, setDocumentId] = useState(fixtures.documents[0]?.id)
|
|
const [layers, setLayers] = useState(fixtures.layers)
|
|
const [weights, setWeights] = useState(fixtures.lineWeights)
|
|
const [weightId, setWeightId] = useState("w-default")
|
|
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>(null)
|
|
const [textStyle, setTextStyle] = useState(fixtures.textStyles[0])
|
|
const [dimensionStyle, setDimensionStyle] = useState(fixtures.dimensionStyles[0])
|
|
const [barOpen, setBarOpen] = useState(true)
|
|
const [panelOpen, setPanelOpen] = useState(false)
|
|
const [expanded, setExpanded] = useState<string[]>([])
|
|
const [readOnly, setReadOnly] = useState(false)
|
|
const [disabled, setDisabled] = useState(false)
|
|
const [emptyShape, setEmptyShape] = useState(false)
|
|
const [callbackLog, setCallbackLog] = useState<string[]>([])
|
|
const counter = useRef(0)
|
|
const record = (line: string) => { setCallbackLog(lines => [...lines.slice(-5), line]); console.info(`[SWHeader Overview] ${line}`) }
|
|
const value = (id: string) => properties.find(property => property.id === id)?.value ?? ""
|
|
const change = (id: string, next: string, event = "onPropertyChange") => {
|
|
setProperties(items => items.map(property => property.id === id ? {
|
|
...property, value: next, mixed: false, ...(id === "color" ? { swatch: parseSWHeaderColor(next)?.hex } : {}),
|
|
} : property))
|
|
if (id === "color") setColorMode(null)
|
|
record(`${event}(${id}, ${next})`)
|
|
}
|
|
const onPropertyChange = (property: SWHeaderProperty, next: string) => change(property.id, next)
|
|
const changeExample = (next: Selection) => {
|
|
setExample(next); setProperties(getProperties(next)); setExpanded(next === "expanded" ? ["common"] : [])
|
|
record(`selection(${next})`)
|
|
}
|
|
|
|
useLayoutEffect(() => {
|
|
const slot = slotRef.current
|
|
if (!slot) return
|
|
const measure = () => {
|
|
const bounds = slot.getBoundingClientRect()
|
|
const width = Math.max(0, Math.min(bounds.width, bounds.height * 16 / 9))
|
|
setSize(previous => previous.width === width ? previous : { width, height: width * 9 / 16 })
|
|
}
|
|
measure()
|
|
const observer = new ResizeObserver(measure)
|
|
observer.observe(slot)
|
|
return () => observer.disconnect()
|
|
}, [])
|
|
|
|
useLayoutEffect(() => {
|
|
const header = headerRef.current
|
|
if (!header) return
|
|
const measure = () => {
|
|
const bounds = header.getBoundingClientRect()
|
|
const trigger = header.querySelector<HTMLElement>("[data-property-panel-trigger]")
|
|
const anchor = trigger?.getClientRects().length ? trigger : header.querySelector<HTMLElement>(".hm-sw-header__more-trigger")
|
|
setPanelLeft(Math.max(4, Math.min((anchor?.getBoundingClientRect().left ?? bounds.left) - bounds.left, bounds.width - 318)))
|
|
}
|
|
measure()
|
|
const observer = new ResizeObserver(measure)
|
|
observer.observe(header)
|
|
return () => observer.disconnect()
|
|
}, [])
|
|
|
|
const toggleLayer = (key: "visible" | "frozen" | "locked") => (layer: SWHeaderLayer, next: boolean) => {
|
|
setLayers(items => items.map(item => item.id === layer.id ? { ...item, [key]: next } : item))
|
|
record(`layer.${key}(${layer.id}, ${next})`)
|
|
}
|
|
const chooseWeight = (weight: SWHeaderLineWeight) => {
|
|
setWeightId(weight.id)
|
|
// ByLayer/ByBlock select a mode, not a numeric property value (same policy as usePropertyMenus).
|
|
if (weight.value !== undefined) change("weight", weight.value, "onLineWeightSelect")
|
|
else record(`onLineWeightSelect(${weight.id})`)
|
|
}
|
|
const changeWeight = (weight: SWHeaderLineWeight, next: string) => {
|
|
setWeights(items => items.map(item => item.id === weight.id ? { ...item, value: next } : item))
|
|
if (weightId === weight.id) change("weight", next, "onLineWeightValueChange")
|
|
else record(`onLineWeightValueChange(${weight.id}, ${next})`)
|
|
}
|
|
const changeMode = (mode: SWHeaderColorMode) => { setColorMode(mode); record(`onColorModeChange(${mode})`) }
|
|
const scale = value("scale")
|
|
const menus: SWHeaderPropertyMenus = {
|
|
layers, onLayerVisibilityChange: toggleLayer("visible"), onLayerFreezeChange: toggleLayer("frozen"), onLayerLockChange: toggleLayer("locked"),
|
|
onLayerDetail: () => record("onLayerDetail"), lineTypes: fixtures.lineTypes, lineWeights: weights, activeLineWeightId: weightId, lineScale: scale,
|
|
onLineWeightSelect: chooseWeight, onLineWeightValueChange: changeWeight,
|
|
onLineScaleChange: next => change("scale", Number(next).toFixed(4), "onLineScaleChange"), onLineStyleDetail: () => record("onLineStyleDetail"),
|
|
colors: fixtures.colors, userColors: fixtures.userColors, colorMode, onColorModeChange: changeMode,
|
|
onColorPick: () => record("onColorPick"), onColorDetail: () => record("onColorDetail"),
|
|
}
|
|
const guardedProperties = properties.map(property => ({ ...property, disabled: disabled || property.disabled, readOnly: readOnly || property.readOnly }))
|
|
// Bar order = barFixture order (2026-10-21: 반지름 / 지름 / 원주 / 면적), then the preview fields.
|
|
const barProperties = [
|
|
...barFixture.flatMap(bar => guardedProperties.filter(property => property.id === bar.id)
|
|
.map(property => ({ ...bar, value: property.value, disabled: property.disabled, readOnly: property.readOnly }))),
|
|
...guardedProperties.filter(property => property.id.startsWith("preview-extra-")),
|
|
]
|
|
const layer = layers.find(item => item.id === value("layer"))
|
|
const line = fixtures.lineTypes.find(item => item.id === value("line"))
|
|
const color = parseSWHeaderColor(value("color"))
|
|
const weight = weights.find(item => item.id === weightId)
|
|
|
|
return <StoryPage className="sw-story-overview">
|
|
<StoryPageHeader title="SW Header" />
|
|
{showVerificationControls() && <div className="sw-overview-toolbar" aria-label="Storybook 전용 미리보기 설정">
|
|
<label htmlFor={`${controlId}-selection`}>선택 상태</label>
|
|
<select id={`${controlId}-selection`} value={example} onChange={event => changeExample(event.target.value as Selection)}>
|
|
<option value="none">선택 없음</option><option value="all">전체 객체</option><option value="circle">원 선택</option><option value="expanded">전체 펼침</option>
|
|
</select>
|
|
<details><summary>시연 데이터</summary><div className="sw-overview-data">
|
|
<button onClick={() => setProperties(items => items.map(property => barFixture.some(item => item.id === property.id) ? { ...property, value: "12.5" } : property))}>외부 값 12.5</button>
|
|
<button onClick={() => {
|
|
const number = ++counter.current
|
|
setProperties(items => [...items, { id: `preview-extra-${number}`, name: `추가 속성 ${number}`, value: "10", type: "number", groupId: "shape", min: 0 }])
|
|
}}>필드 추가</button>
|
|
<button onClick={() => setProperties(items => items.filter(item => !item.id.startsWith("preview-extra-")))}>추가 필드 제거</button>
|
|
<label><input type="checkbox" checked={readOnly} onChange={event => setReadOnly(event.target.checked)} />속성 ReadOnly</label>
|
|
<label><input type="checkbox" checked={disabled} onChange={event => setDisabled(event.target.checked)} />속성 Disabled</label>
|
|
<label><input type="checkbox" checked={emptyShape} onChange={event => setEmptyShape(event.target.checked)} />형상 Slot 비우기</label>
|
|
</div></details>
|
|
</div>}
|
|
<div ref={slotRef} className="sw-overview-slot" data-preview-slot="">
|
|
<div className="sw-overview-stage" data-preview-stage="" role="region" tabIndex={0} aria-label="SW Header 16:9 미리보기, 작은 화면에서는 내부 스크롤"
|
|
style={{ ...size, "--sw-preview-panel-left": `${panelLeft}px`, "--sw-preview-panel-top": barOpen ? "77px" : "49px" } as CSSProperties}>
|
|
<div className="sw-overview-world">
|
|
<SWHeader ref={headerRef} {...initial}
|
|
documents={documents} activeDocumentId={documentId} documentTitle={documents.find(document => document.id === documentId)?.name ?? "열린 문서 없음"}
|
|
onDocumentClick={() => record("onDocumentClick")} onDocumentMenuOpenChange={open => record(`documentOpen(${open})`)}
|
|
onDocumentSelect={document => { setDocumentId(document.id); record(`onDocumentSelect(${document.id})`) }}
|
|
onDocumentClose={document => {
|
|
const next = documents.filter(item => item.id !== document.id); setDocuments(next)
|
|
if (documentId === document.id) setDocumentId(next[0]?.id)
|
|
record(`onDocumentClose(${document.id})`)
|
|
}}
|
|
onDocumentNew={() => {
|
|
const id = `preview-document-${++counter.current}`
|
|
setDocuments(items => [...items, { id, name: `새 문서 ${counter.current}.dwg` }]); setDocumentId(id); record("onDocumentNew")
|
|
}} onDocumentOpen={() => record("onDocumentOpen")}
|
|
layers={layers} layer={{ name: layer?.name ?? value("layer") }} activeLayerId={layer?.id}
|
|
onLayerSelect={item => change("layer", item.id, "onLayerSelect")}
|
|
onLayerVisibilityChange={menus.onLayerVisibilityChange} onLayerFreezeChange={menus.onLayerFreezeChange} onLayerLockChange={menus.onLayerLockChange}
|
|
onLayerOpen={() => record("onLayerOpen")} onLayerDetail={menus.onLayerDetail}
|
|
colors={fixtures.colors} userColors={fixtures.userColors}
|
|
color={{ value: colorMode === "byLayer" ? "ByLayer" : colorMode === "byBlock" ? "ByBlock" : value("color"), swatch: color?.hex }} colorMode={colorMode}
|
|
onColorSelect={item => change("color", item.value ?? parseSWHeaderColor(item.color)?.value ?? item.color, "onColorSelect")}
|
|
onColorInput={next => change("color", next.value, "onColorInput")} onColorModeChange={changeMode}
|
|
onColorOpen={() => record("onColorOpen")} onColorDetail={menus.onColorDetail} onColorPick={() => record("onColorPick")}
|
|
lineTypes={fixtures.lineTypes} lineWeights={weights} activeLineTypeId={line?.id} activeLineWeightId={weightId}
|
|
lineStyle={{ name: line?.name ?? value("line"), weight: weight && weight.value === undefined ? weight.name : value("weight"), scale }}
|
|
onLineTypeSelect={item => change("line", item.id, "onLineTypeSelect")} onLineWeightSelect={chooseWeight} onLineWeightValueChange={changeWeight}
|
|
onLineScaleChange={menus.onLineScaleChange} onLineStyleOpen={() => record("onLineStyleOpen")} onLineStyleDetail={menus.onLineStyleDetail}
|
|
textStyles={fixtures.textStyles} activeTextStyleId={textStyle?.id} textStyle={textStyle}
|
|
onTextStyleSelect={item => { setTextStyle(item); record(`onTextStyleSelect(${item.id})`) }}
|
|
onTextStyleOpen={() => record("onTextStyleOpen")} onTextStyleDetail={() => record("onTextStyleDetail")}
|
|
dimensionStyles={fixtures.dimensionStyles} activeDimensionStyleId={dimensionStyle?.id} dimensionStyle={dimensionStyle}
|
|
onDimensionStyleSelect={item => { setDimensionStyle(item); record(`onDimensionStyleSelect(${item.id})`) }}
|
|
onDimensionStyleOpen={() => record("onDimensionStyleOpen")} onDimensionStyleDetail={() => record("onDimensionStyleDetail")}
|
|
propertyBarOpen={barOpen} onPropertyBarOpenChange={next => { setBarOpen(next); record(`propertyBarOpen(${next})`) }}
|
|
propertyPanelOpen={panelOpen} onPropertyPanelOpenChange={next => { setPanelOpen(next); record(`propertyPanelOpen(${next})`) }}
|
|
propertyBar={{ selection: { ...propertySelection(example), title: (propertySelection(example).kind === "single" && barTitle) || propertySelection(example).title }, properties: barProperties, onPropertyChange, readOnly, disabled }}
|
|
propertyPanel={{ selection: propertySelection(example), properties: guardedProperties, groups: propertyGroups,
|
|
expandedGroupIds: expanded, onExpandedGroupsChange: setExpanded, onPropertyChange, menus,
|
|
shapeContent: emptyShape ? null : undefined, onOverviewClick: () => changeExample("all"),
|
|
}}
|
|
onHomeClick={() => record("onHomeClick")}
|
|
homeMenu={homeMenuSections && { sections: homeMenuSections, onItemSelect: event => record(`onItemSelect(${event.item.id}, ${event.active})`) }}
|
|
onHomeMenuOpenChange={(open, reason) => record(`homeMenuOpen(${open}, ${reason})`)} onPrint={() => record("onPrint")} onPopupGuide={() => record("onPopupGuide")}
|
|
onMinimize={() => record("onMinimize")} onClose={() => record("onClose")} onWindowMoveStart={() => record("onWindowMoveStart")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Callback record for the browser tests only; not shown on the page. */}
|
|
<output hidden data-preview-callback="">{callbackLog.join(" → ")}</output>
|
|
</StoryPage>
|
|
}
|