refactor(storybook): remove unused SW header demo stories

Remove nine SW/Header demo pages (property-fields, property-shape-slot,
composition, colorize, dynamic-controls, property-bar, additional-hover,
property-panel, layer-detail-input) and the demo code and Storybook-only
CSS used only by them. SW Stories 19 -> 10, total 338 -> 329.

The package runtime, public API, AI template and the retained Stories
(Overview, Dropdown, Property Panel 4-state comparison, Window Move
Motion and others) are unchanged. Add a current regression script and
mark the older scripts that open the retired IDs as historical evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shkim4andClaude Opus 5.5 committed 2026-10-19 13:10:34 +09:00
1 parent c8e5fbeebc
commit 5faa1f1ffe
58 files changed
+10577 -499

No files matched your search

+5 -481
View File
@@ -2,10 +2,10 @@ import { useEffect, useId, useRef, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import {
SWHeader, SWHeaderColorMenu, SWHeaderLineStyleMenu, SWHeaderTextStyleMenu, SWHeaderDimensionStyleMenu,
SWHeaderProperties, SWHeaderWindowControls, SWTheme, SWHeaderLayerMenu, SWHeaderInput, parseSWHeaderColor, swHeaderLinePatterns, SWHeaderPropertyPanel, SWHeaderPropertyBar,
SWHeaderProperties, SWHeaderWindowControls, SWTheme, SWHeaderLayerMenu, parseSWHeaderColor, swHeaderLinePatterns, SWHeaderPropertyPanel, SWHeaderPropertyBar,
type SWHeaderColorMode, type SWHeaderColorOption, type SWHeaderColorValue, type SWHeaderDimensionStyleOption,
type SWHeaderDocument, type SWHeaderLayer, type SWHeaderLineStyleValue, type SWHeaderLineType, type SWHeaderLineWeight,
type SWHeaderMenuId, type SWHeaderProps, type SWHeaderTextStyleOption, type SWHeaderProperty, type SWHeaderPropertyGroup, type SWHeaderPropertySelection, type SWHeaderPropertyMenus, type SWHeaderControlConfig, type SWHeaderHideableControlId, type SWHeaderPropertyShapeContent,
type SWHeaderMenuId, type SWHeaderProps, type SWHeaderTextStyleOption, type SWHeaderProperty, type SWHeaderPropertyGroup, type SWHeaderPropertySelection, type SWHeaderPropertyMenus, type SWHeaderPropertyShapeContent,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
import { SWHeaderMenuDetail } from "../../packages/sw/src/components/sw-header/components/SWHeaderDropdown"
@@ -13,8 +13,6 @@ import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-pag
import "./sw-stories.css"
import { SWHeaderOverview } from "./sw-header-overview"
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
import figmaLayerDetailReference from "./assets/figma-reference/btn-detail-layer-26-10767.png"
import figmaInputReference from "./assets/figma-reference/input-26-11656.png"
// Owner: docs/sw/sw-header-design-spec.md (Figma rj6wpsfk24lOWvWiVRAANP, node 26:8944 "header_green").
// The package is independent of @hm-design/ui: this file imports only @hm-design/sw for the specimen.
@@ -615,7 +613,6 @@ export const DocumentDropdown: Story = {
}
/** Test data only. The package never ships layer names; each SW program passes its own list. */
const FIGMA_LAYERS: SWHeaderLayer[] = Array.from({ length: 8 }, (_, index) => ({ id: `figma-${index + 1}`, name: "CC-D GNL" }))
const SAMPLE_LAYERS: SWHeaderLayer[] = [
{ id: "layer-0", name: "0" },
{ id: "layer-1", name: "Sample-Center", color: "#ff0000" },
@@ -640,56 +637,6 @@ const MANY_LAYERS: SWHeaderLayer[] = Array.from({ length: 20 }, (_, index) => ({
locked: index % 6 === 3,
}))
const noop = () => {}
type LayerHostProps = Partial<SWHeaderProps> & { onLog?: (line: string) => void; showState?: boolean }
const describeLayers = (layers: SWHeaderLayer[]) => layers.map(layer =>
`${layer.id}: visible=${layer.visible !== false} frozen=${layer.frozen === true} locked=${layer.locked === true}`).join("\n")
/**
* Host-owned layers: the SW program keeps the list, the current layer and each layer's state. The
* header only reports the next value; this example writes it back so the icons follow.
*/
function LayerHeader({ layers: initialLayers = SAMPLE_LAYERS, activeLayerId: initialActiveId, onLog, showState, ...props }: LayerHostProps) {
const [layers, setLayers] = useState(initialLayers)
const [activeId, setActiveId] = useState(initialActiveId)
const update = (id: string, patch: Partial<SWHeaderLayer>) => setLayers(list => list.map(item => (item.id === id ? { ...item, ...patch } : item)))
const header = (
<SWHeader
{...figmaArgs}
{...callbacks}
layers={layers}
activeLayerId={activeId}
onLayerSelect={layer => { console.info("[SWHeader] layerSelect", layer.id); onLog?.(`onLayerSelect(${layer.id})`); setActiveId(layer.id) }}
onLayerVisibilityChange={(layer, next) => { onLog?.(`onLayerVisibilityChange(${layer.id}, ${next})`); update(layer.id, { visible: next }) }}
onLayerFreezeChange={(layer, next) => { onLog?.(`onLayerFreezeChange(${layer.id}, ${next})`); update(layer.id, { frozen: next }) }}
onLayerLockChange={(layer, next) => { onLog?.(`onLayerLockChange(${layer.id}, ${next})`); update(layer.id, { locked: next }) }}
onLayerDetail={() => { console.info("[SWHeader] layerDetail"); onLog?.("onLayerDetail") }}
onLayerMenuOpenChange={open => onLog?.(`onLayerMenuOpenChange(${open})`)}
{...props}
/>
)
if (!showState) return header
return (
<>
<pre className="sw-story-layer-state" data-layer-state="">{`${describeLayers(layers)}\nactiveLayerId: ${activeId ?? "없음"}`}</pre>
{header}
</>
)
}
/** Interactive demo with a visible callback log, to check that selection and toggles never double-fire. */
function LayerDemo({ width, ...props }: LayerHostProps & { width: number }) {
const [log, setLog] = useState<string[]>([])
return (
<>
<p className="sw-story-log" aria-live="polite" data-callback-log="">Callback: {log.length ? log.join(" → ") : "없음"}</p>
<MenuFrame width={width}><LayerHeader showState {...props} onLog={line => setLog(lines => [...lines.slice(-9), line])} /></MenuFrame>
</>
)
}
/* ------------------------------------------------------------------------------------------------
Field dropdowns (2026-10-16): color 26:10986 / 26:11036, ByLayer 26:12666, ByBlock 26:12659,
line style 26:11104 (rows 26:10702), text style 26:11181, dimension style 26:11215.
@@ -735,16 +682,16 @@ const FIGMA_DIMENSION_STYLES: SWHeaderDimensionStyleOption[] = [
{ id: "iso-25", name: "ISO - 25" },
]
type StyleHostProps = Partial<SWHeaderProps> & { onLog?: (line: string) => void; showState?: boolean; initialColorMode?: SWHeaderColorMode | null }
type StyleHostProps = Partial<SWHeaderProps> & { onLog?: (line: string) => void; showState?: boolean }
/**
* Host-owned field data: the SW program keeps color, line / text / dimension style and the lists. The
* header only reports; this example writes each report back (an explicit color clears ByLayer / ByBlock,
* which is this host's choice) so the fields follow. Each property changes alone.
*/
function StyleHeader({ onLog, showState, initialColorMode = null, ...props }: StyleHostProps) {
function StyleHeader({ onLog, showState, ...props }: StyleHostProps) {
const [color, setColor] = useState<SWHeaderColorValue>(figmaArgs.color!)
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>(initialColorMode)
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>(null)
const [lineStyle, setLineStyle] = useState<SWHeaderLineStyleValue>(figmaArgs.lineStyle!)
const [lineWeights, setLineWeights] = useState(FIGMA_LINE_WEIGHTS)
const [weightId, setWeightId] = useState<string | undefined>("w-default")
@@ -857,101 +804,6 @@ export const ResponsiveMore: Story = {
),
}
/** Host-owned detail values for the editable input (the detail popup itself is not designed yet). */
function InputDemo() {
const [value, setValue] = useState("4.0526")
const [name, setName] = useState("벽체_외벽")
const [log, setLog] = useState<string[]>([])
const add = (line: string) => setLog(lines => [...lines.slice(-9), line])
return (
<>
<p className="sw-story-log" aria-live="polite" data-callback-log="">Callback: {log.length ? log.join(" → ") : "없음"}</p>
<pre className="sw-story-layer-state" data-input-state="">{`확정값: number=${value} · text=${name}`}</pre>
<div className="sw-story-controls">
<button type="button" onClick={() => { setValue("12.5"); add("host: value=12.5") }}>외부 값 변경 (12.5)</button>
<button type="button" onClick={() => { setName("레이어 이름 변경"); add("host: text 변경") }}>외부 텍스트 변경</button>
</div>
<div className="sw-story-zoom">
<SWTheme className="sw-story-composed sw-story-detail">
<SWHeaderInput
aria-label="숫자 속성"
valueType="number"
value={value}
onValueChange={draft => add(`onValueChange(${draft})`)}
onCommit={next => { add(`onCommit(${next})`); setValue(next) }}
onCancel={() => add("onCancel")}
/>
<SWHeaderInput
aria-label="텍스트 속성"
value={name}
onCommit={next => { add(`onCommit(${next})`); setName(next) }}
onCancel={() => add("onCancel")}
/>
</SWTheme>
</div>
</>
)
}
export const LayerDetailInput: Story = {
name: "Layer Detail · Input",
parameters: { controls: { disable: true } },
render: () => (
<StoryPage>
<StoryPageHeader
kicker={KICKER}
title="Layer Detail Button · Editable Input"
description="레이어 Dropdown 하단 '레이어 상세' 버튼(btn_상세/레이어 26:10767)과, 상세 영역에서 쓸 수정 가능한 Input(input 26:11656) · 커서(cursor 26:11664)입니다. 상세 팝업 전체는 Figma 전달 전이라 만들지 않으며, 버튼 Callback(onLayerDetail)과 SWHeaderInput 계약만 제공합니다."
/>
<StorySection title="Figma 원본 (비교용 이미지)" description="왼쪽: 레이어 상세 Default / Hover(배경 .25 · radius 25 · 아이콘 35%). 오른쪽: Input Default 58 × 20 / Hover 58 × 24 / On 58 × 24. 최신 요청에 따라 아래 실제 버튼에도 Hover를 적용합니다. 이미지는 Storybook 비교용이며 패키지에 포함하지 않습니다.">
<div className="sw-story-row sw-story-figma">
<img className="sw-story-reference" src={figmaLayerDetailReference} alt="Figma btn_상세/레이어 26:10767" width={180} height={108} />
<img className="sw-story-reference" src={figmaInputReference} alt="Figma input 26:11656" width={98} height={140} />
</div>
</StorySection>
<StorySection title="레이어 상세 — Default · Hover (고정) · Disabled (3배 확대)" description="왼쪽: Callback 없이 사용 가능(Default). 가운데: Hover 배경 .25·radius 25·Hover 아이콘. 오른쪽: 호스트가 fieldset disabled를 명시한 상태. Figma와 같은 113 × 24, 아이콘 14 · 간격 4 · padding 5 / 10 · 배경 rgba(197,177,158,.15) · radius 24 · 글자 35%. CAD 상세 실행은 호스트 Callback이 있을 때만 가능합니다.">
<div className="sw-story-row">
{[{ detail: undefined, hover: false, disabled: false }, { detail: noop, hover: true, disabled: false }, { detail: undefined, hover: false, disabled: true }].map((item, index) => {
const specimen = (
<div className="sw-story-zoom sw-story-layer-room" data-detail-case={index}>
<fieldset className="sw-story-explicit-disabled" disabled={item.disabled}><SWTheme className="sw-story-composed">
<SWHeaderLayerMenu value={{ name: "0" }} layers={FIGMA_LAYERS.slice(0, 2)} open onDetail={item.detail} />
</SWTheme></fieldset>
</div>
)
return item.hover ? <PinHover key={index} selectors={[".hm-sw-header__layer-detail"]}>{specimen}</PinHover> : <div key={index}>{specimen}</div>
})}
</div>
</StorySection>
<StorySection title="레이어 상세 Click Callback" description="필드를 열고 '레이어 상세'를 누르면 onLayerDetail 1회 후 목록이 닫힙니다(기존 동작). 레이어 선택·전구·눈꽃·자물쇠 매핑은 변경하지 않았습니다.">
<LayerDemo width={1920} layers={FIGMA_LAYERS} />
</StorySection>
<StorySection title="Input — Default · Hover (고정) · ReadOnly · Disabled (3배 확대)" description="Default: 배경·테두리 없음, #E60403 14px + 검정 1.3px Stroke. Hover: 1px rgba(212,203,189,.25), 24px(글자 위치 그대로). ReadOnly(onCommit 없음 또는 readOnly): Hover·편집 상태 없음. Disabled: 40%.">
<PinHover selectors={[".sw-story-pin-hover"]}>
<div className="sw-story-zoom">
<SWTheme className="sw-story-composed sw-story-detail">
<SWHeaderInput aria-label="Default" value="4.0526" onCommit={noop} />
<SWHeaderInput aria-label="Hover" className="sw-story-pin-hover" value="4.0526" onCommit={noop} />
<SWHeaderInput aria-label="ReadOnly" value="4.0526" onCommit={noop} readOnly />
<SWHeaderInput aria-label="Disabled" value="4.0526" onCommit={noop} disabled />
</SWTheme>
</div>
</PinHover>
</StorySection>
<StorySection title="Input — Editing (Focus)" description="편집 중(26:11661): 배경 #0F0D0A, 1px rgba(212,203,189,.3), 글자 #E60403 유지, 네이티브 커서 #E60403(caret-color, 깜박임 = Figma cursor Default / Variant2). 이 예제는 입력에 자동 Focus합니다.">
<div className="sw-story-zoom">
<SWTheme className="sw-story-composed sw-story-detail">
<SWHeaderInput aria-label="Editing" value="4.0526" onCommit={noop} autoFocus />
</SWTheme>
</div>
</StorySection>
<StorySection title="Input — 입력 · Enter 확정 · Escape 취소 · 외부 값 갱신" description="숫자 입력(valueType='number'): 유효한 소수만 onCommit, 잘못된 값은 복원. 텍스트 입력(한글 IME 포함): 조합 중 Enter는 확정하지 않습니다. Escape는 편집 취소(onCancel) — 확정값으로 복원. Focus 이동(Tab / 바깥 클릭)도 확정합니다. 위 버튼으로 호스트 값을 바꾸면 최신 값이 표시됩니다.">
<InputDemo />
</StorySection>
</StoryPage>
),
}
/* Additional review 2026-10-16: fixture values are host data, never package defaults. */
const PROPERTY_GROUPS: SWHeaderPropertyGroup[] = [
{ id: "common", collapsedCount: 6 },
@@ -1038,239 +890,17 @@ function usePropertyMenus(record: (line: string) => void, setProperties: (update
}
}
function PropertyPanelDemo() {
const [example, setExample] = useState<PropertyExample>("none")
const [selection, setSelection] = useState(propertySelection("none"))
const [properties, setProperties] = useState(propertyFixture("none"))
const [expanded, setExpanded] = useState<string[]>([])
const [controlled, setControlled] = useState(false)
const [open, setOpen] = useState(false)
const [width, setWidth] = useState(1920)
const [log, setLog] = useState<string[]>([])
const record = (text: string) => setLog(lines => [...lines.slice(-19), text])
const menus = usePropertyMenus(record, setProperties)
const changeExample = (next: PropertyExample) => {
setExample(next); 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) => {
record("property(" + property.id + ", " + value + ")")
setProperties(items => items.map(item => item.id === property.id ? { ...item, value, mixed: false, ...(item.id === "line" ? { linePattern: linePatternOf(value) } : {}) } : item))
},
onToolActiveChange: (next: boolean) => record("tool(" + next + ")"),
onOverviewClick: () => changeExample("all"),
}
return (
<div data-property-demo="" data-example={example}>
<div className="sw-story-controls">
{(["none", "all", "circle", "expanded"] as const).map((name, index) => <button key={name} data-example-button={name} onClick={() => changeExample(name)}>{["선택 없음", "전체 선택", "원 1개", "모두 펼침"][index]}</button>)}
{[1920, 1440, 1080, 900, 768, 640, 390].map(value => <button key={value} onClick={() => setWidth(value)}>{value}px</button>)}
<button onClick={() => { setControlled(value => !value); setOpen(false) }}>Controlled: {String(controlled)}</button>
<button onClick={() => setOpen(value => !value)}>외부 Open 변경</button>
</div>
<div className="sw-story-controls">
<button onClick={() => setProperties(items => [...items, { id: "extra-" + items.length, groupId: "common", name: "추가 속성", type: "text", value: "호스트 값" }])}>속성 추가</button>
<button onClick={() => setProperties(items => [...items, { id: "shape-extra-" + items.length, groupId: "shape", name: "추가 형상 속성", type: "text", value: "호스트 값" }])}>형상 속성 추가</button>
<button onClick={() => setProperties(items => items.map(item => item.id === "line" ? { ...item, options: item.options?.filter(option => option.id !== "CENTER") } : item))}>CENTER 옵션 삭제</button>
<button onClick={() => setProperties(items => items.filter(item => item.id !== "layer"))}>레이어 속성 삭제</button>
<button onClick={() => setProperties(items => items.map(item => item.id === "layer" ? { ...item, value: "외부 변경값" } : item))}>외부 값 변경</button>
<button onClick={() => setSelection(current => ({ ...current, id: current.id + "-changed" }))}>선택 객체 교체</button>
<button onClick={() => setProperties(items => [...items].reverse())}>속성 순서 뒤집기</button>
<button onClick={() => setProperties(items => items.map(item => item.id === "weight" ? { ...item, disabled: !item.disabled } : item))}>가중치 Disabled</button>
<button onClick={() => setProperties(items => items.map(item => item.id === "layer" ? { ...item, readOnly: !item.readOnly } : item))}>레이어 ReadOnly</button>
<button onClick={() => setProperties(items => items.map(item => item.id === "opacity" ? { ...item, value: "0.8" } : item))}>투명도 외부 값 변경</button>
<button onClick={() => setProperties(items => items.map(item => item.id === "opacity" ? { ...item, disabled: !item.disabled } : item))}>투명도 Disabled</button>
<button onClick={() => setProperties(items => items.map(item => item.id === "opacity" ? { ...item, readOnly: !item.readOnly } : item))}>투명도 ReadOnly</button>
</div>
<p className="sw-story-log" data-property-log="" aria-live="polite">{log.join(" → ") || "Callback 없음"}</p>
<pre className="sw-story-layer-state" data-property-state="">{JSON.stringify({ selection: selection.id, values: properties.map(item => [item.id, item.value]), expanded, open })}</pre>
<div className="sw-story-property-room"><Frame width={width}><SWHeader {...figmaArgs} propertyPanel={panel} propertyPanelOpen={controlled ? open : undefined}
onPropertyPanelOpenChange={next => { record("open(" + next + ")"); if (controlled) setOpen(next) }}
onPropertyPanelToggle={() => record("legacyToggle")}
layers={SAMPLE_LAYERS} activeLayerId="layer-0" colors={FIGMA_COLORS} lineTypes={FIGMA_LINE_TYPES} textStyles={FIGMA_TEXT_STYLES} dimensionStyles={FIGMA_DIMENSION_STYLES}
/></Frame></div>
</div>
)
}
export const PropertyPanel: Story = {
name: "Property Panel",
parameters: { controls: { disable: true } },
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="속성창" description="Figma 4개 상태를 외부 데이터·그룹 펼침으로 구성합니다. 속성창 버튼(좁은 화면: 더보기)으로 열고 닫으세요. Enter/Blur 확정, Escape 취소, 다음 Escape 닫기." /><PropertyPanelDemo /></StoryPage>,
}
export const KGuide: Story = {
name: "K Guide",
parameters: { controls: { disable: true } },
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="K 가이드 반복 애니메이션" description="기존 K 버튼 Hover/Focus → 원본 이미지 Pan·Zoom 반복. 영역을 벗어나면 정지, 재진입은 처음부터. Reduced Motion은 정지 이미지." /><div className="sw-story-property-room"><SWHeader {...figmaArgs} {...callbacks} /></div></StoryPage>,
}
function RequiredColorDemo() {
const [mode, setMode] = useState<SWHeaderColorMode | null>(null)
const [log, setLog] = useState<string[]>([])
return <div data-mode-demo=""><div className="sw-story-controls">
<button onClick={() => setMode("byLayer")}>외부 ByLayer</button><button onClick={() => setMode("byBlock")}>외부 ByBlock</button><button onClick={() => setMode(null)}>외부 직접 색상</button>
</div><p data-mode-log="">{log.join(",")}</p><MenuFrame width={1920}><SWHeader {...figmaArgs} colors={FIGMA_COLORS} colorMode={mode} onColorModeChange={next => { setMode(next); setLog(items => [...items, next]) }} onColorSelect={() => setMode(null)} /></MenuFrame></div>
}
export const AdditionalHover: Story = {
name: "Additional Hover",
parameters: { controls: { disable: true } },
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="추가 Hover 및 Selected" description="5개 상세 버튼 Hover와 목록 Selected 강조, 색상 기준 필수 선택. 실제 Hover·클릭으로 검증합니다." /><StyleDemo width={1920} onLayerDetail={() => console.info("[SWHeader] onLayerDetail")} initialColorMode="byLayer" /><RequiredColorDemo /></StoryPage>,
}
const BAR_FIXTURE: SWHeaderProperty[] = [
{ id: "radius", name: "반지름", value: "4.0526", type: "number", groupId: "shape", min: 0 },
{ id: "diameter", name: "지름", value: "8.1052", type: "number", groupId: "shape", min: 0 },
{ id: "circumference", name: "원주", value: "8.1052", type: "number", groupId: "shape", min: 0 },
{ id: "area", name: "면적", value: "51.5962", type: "number", groupId: "shape", min: 0 },
]
function PropertyBarDemo() {
const [selection, setSelection] = useState<SWHeaderPropertySelection>({ id: "circle", title: "원", kind: "single" })
const [properties, setProperties] = useState(BAR_FIXTURE)
const [open, setOpen] = useState(false)
const [controlled, setControlled] = useState(false)
const [readOnly, setReadOnly] = useState(false)
const [disabled, setDisabled] = useState(false)
const [log, setLog] = useState<string[]>([])
const change = (property: SWHeaderProperty, value: string) => {
setProperties(items => items.map(item => item.id === property.id ? { ...item, value } : item))
setLog(items => [...items, `${property.id}=${value}`])
}
const data = { selection, properties, onPropertyChange: change }
return <div data-bar-demo="">
<div className="sw-story-controls">
<button onClick={() => setControlled(value => !value)}>Controlled 전환</button>
<button onClick={() => setOpen(value => !value)}>외부 열림 변경</button>
<button onClick={() => setProperties(items => [...items, { id: `extra-${items.length}`, name: "추가 속성", value: "10", type: "number", groupId: "shape", min: 0 }])}>Bar 필드 추가</button>
<button onClick={() => setProperties(items => items.filter(item => item.id !== "radius"))}>Bar 반지름 삭제</button>
<button onClick={() => setProperties(items => [...items].reverse().map(item => ({ ...item, order: undefined })))}>Bar 순서 반전</button>
<button onClick={() => setProperties(items => items.map(item => ({ ...item, name: `${item.name} 변경`, value: "12.5" })))}>Bar 외부 값 변경</button>
<button onClick={() => { setSelection({ id: "line", title: "직선", kind: "single" }); setProperties([{ id: "length", name: "길이", value: "25", type: "number", groupId: "shape", min: 0 }]) }}>Bar 객체 교체</button>
<button onClick={() => setReadOnly(value => !value)}>Bar ReadOnly</button>
<button onClick={() => setDisabled(value => !value)}>Bar Disabled</button>
<button onClick={() => setProperties(items => items.map(item => ({ ...item, readOnly: item.id === "diameter", disabled: item.id === "area" })))}>Bar 개별 제한</button>
</div>
<p data-bar-log="">{log.join(" → ")}</p>
<div className="sw-story-property-room">
<StyleHeader propertyBar={{ ...data, readOnly, disabled }} propertyPanel={{ ...data, groups: [{ id: "shape", name: "형상" }] }}
propertyPanelOpen={undefined} propertyBarOpen={controlled ? open : undefined} onPropertyBarOpenChange={next => { setOpen(next); setLog(items => [...items, `open=${next}`]) }} />
</div>
</div>
}
export const PropertyBar: Story = {
name: "Property Bar",
parameters: { controls: { disable: true } },
render: () => <StoryPage className="sw-story-followup"><StoryPageHeader kicker={KICKER} title="속성Bar · 동적 편집" description="Figma 26:10492. 속성창과 독립 열림, 동일 호스트 데이터. 수치는 Figma 예시이며 CAD 계산은 수행하지 않습니다." /><PropertyBarDemo />
<StorySection title="콘텐츠 폭 기준 (부모 1222px, 높이 28)"><div className="sw-story-scroller"><SWTheme><div style={{ width: 1222 }}><SWHeaderPropertyBar selection={{ id: "reference", title: "원", kind: "single" }} properties={BAR_FIXTURE} /></div></SWTheme></div></StorySection>
</StoryPage>,
}
const CONTROL_FIXTURE: SWHeaderControlConfig[] = ["document", "drawingMode", "layer", "color", "lineStyle", "textStyle", "dimensionStyle", "details", "propertyBar", "propertyPanel", "print", "popupGuide"].map(id => ({ id: id as SWHeaderControlConfig["id"] }))
function DynamicControlsDemo() {
const [controls, setControls] = useState(CONTROL_FIXTURE)
const [log, setLog] = useState<string[]>([])
const [title, setTitle] = useState("현재 문서.dwg")
const [documents, setDocuments] = useState<SWHeaderDocument[]>([{ id: "current", name: "현재 문서.dwg" }])
return <div data-controls-demo="">
<div className="sw-story-controls">
{CONTROL_FIXTURE.map(item => <label key={item.id}><input type="checkbox" aria-label={`구성 ${item.id}`} checked={controls.some(control => control.id === item.id && control.visible !== false)} onChange={event => setControls(items => event.target.checked ? [...items.filter(control => control.id !== item.id), item] : items.filter(control => control.id !== item.id))} />{item.id}</label>)}
<button onClick={() => setControls(CONTROL_FIXTURE)}>구성 초기화</button>
<button onClick={() => setControls([])}>모두 제거</button>
<button onClick={() => setControls([{ id: "layer" }, { id: "lineStyle" }])}>레이어·선스타일만</button>
<button onClick={() => setControls(items => items.filter(item => item.id !== "document" && item.id !== "color"))}>파일·색상 제거</button>
<button onClick={() => setControls(items => [...items].reverse())}>구성 순서 반전</button>
<button onClick={() => setControls(items => items.map(item => ({ ...item, label: `${item.id} 변경` })))}>구성 라벨 변경</button>
<button onClick={() => setControls(items => items.map(item => item.id === "color" ? { ...item, visible: false } : item))}>색상 숨김</button>
<button onClick={() => setControls(items => items.map(item => ({ ...item, visible: true })))}>숨김 복원</button>
</div>
<p data-config-log="">{log.join(" → ")}</p>
<div className="sw-story-property-room"><StyleHeader controls={controls} onLog={line => setLog(items => [...items, line])}
propertyBarOpen={undefined} propertyPanelOpen={undefined}
documentTitle={title} documents={documents} activeDocumentId="current" onDocumentSelect={document => { setTitle(document.name); setLog(items => [...items, `document=${document.id}`]) }}
onDocumentNew={() => setDocuments(items => [...items, { id: `new-${items.length}`, name: `새 문서 ${items.length}` }])}
propertyBar={{ selection: { id: "circle", title: "원", kind: "single" }, properties: BAR_FIXTURE }}
propertyPanel={{ selection: { id: "circle", title: "원", kind: "single" }, properties: BAR_FIXTURE, groups: [{ id: "shape" }] }} />
</div>
</div>
}
export const DynamicControls: Story = {
name: "Dynamic Controls",
parameters: { controls: { disable: true } },
render: () => <StoryPage className="sw-story-followup"><StoryPageHeader kicker={KICKER} title="메뉴 컴포넌트 구성" description="Dropdown 내부 옵션과 별개인 controls allowlist. 항목 제거·재추가·숨김·순서·라벨, More 대표 슬롯과 값/Callback 보존." /><DynamicControlsDemo /></StoryPage>,
}
export const Colorize: Story = {
name: "Colorize",
parameters: { controls: { disable: true } },
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="btn_colorize" description="Figma 26:11302 / 26:11305의 원본 SVG, 24px 버튼과 18px 아이콘. 클릭은 기존 onColorPick 계약입니다. 색상 돋보기 Overlay 26:11338은 호스트 연결 대상입니다." /><StyleDemo width={1920} /></StoryPage>,
}
/* 2026-10-16 composition review: every part except the logo can be removed (hiddenControls), the five
similar menus can be reordered (menuOrder). Untouched props keep the approved default header. */
const HIDEABLE_PARTS: { id: SWHeaderHideableControlId; name: string }[] = [
{ id: "home", name: "홈" }, { id: "document", name: "파일 목록" }, { id: "drawingMode", name: "그리기모드" },
{ id: "layer", name: "레이어" }, { id: "color", name: "색상" }, { id: "lineStyle", name: "선스타일" },
{ id: "textStyle", name: "문자스타일" }, { id: "dimensionStyle", name: "치수스타일" }, { id: "details", name: "상세보기" },
{ id: "propertyBar", name: "속성Bar" }, { id: "propertyPanel", name: "속성창" }, { id: "print", name: "인쇄" },
{ id: "popupGuide", name: "K 가이드" }, { id: "windowMove", name: "창 이동" }, { id: "minimize", name: "최소화" }, { id: "close", name: "종료" },
]
const MENU_IDS: SWHeaderMenuId[] = ["layer", "color", "lineStyle", "textStyle", "dimensionStyle"]
const COMPOSITION_PRESETS: { name: string; hidden: SWHeaderHideableControlId[]; order?: SWHeaderMenuId[] }[] = [
{ name: "기본 전체", hidden: [] },
{ name: "구성 A", hidden: ["drawingMode", "textStyle", "dimensionStyle", "details", "propertyBar", "propertyPanel", "print", "popupGuide", "windowMove", "minimize", "close"] },
{ name: "구성 B (레이어·선스타일)", hidden: HIDEABLE_PARTS.map(part => part.id).filter(id => id !== "layer" && id !== "lineStyle") },
{ name: "구성 C (로고만)", hidden: HIDEABLE_PARTS.map(part => part.id) },
{ name: "파일 목록 제거", hidden: ["document"] },
{ name: "레이어 제거", hidden: ["layer"] },
{ name: "색상 제거", hidden: ["color"] },
{ name: "스타일 그룹 제거", hidden: ["lineStyle", "textStyle", "dimensionStyle"] },
{ name: "여러 메뉴 제거", hidden: ["document", "color", "textStyle", "print", "minimize"] },
{ name: "순서 변경", hidden: [], order: ["color", "layer", "dimensionStyle", "lineStyle", "textStyle"] },
{ name: "숨김 + 순서 변경", hidden: ["layer", "textStyle"], order: ["dimensionStyle", "textStyle", "color", "layer"] },
]
function CompositionDemo() {
const [hidden, setHidden] = useState<SWHeaderHideableControlId[]>([])
const [order, setOrder] = useState<SWHeaderMenuId[] | undefined>(undefined)
const [width, setWidth] = useState<number | null>(null)
const [log, setLog] = useState<string[]>([])
const record = (line: string) => setLog(lines => [...lines.slice(-11), line])
const toggle = (id: SWHeaderHideableControlId) => setHidden(list => list.includes(id) ? list.filter(item => item !== id) : [...list, id])
const header = <StyleHeader showState onLog={record} hiddenControls={hidden} menuOrder={order}
onHomeClick={() => record("onHomeClick")} onMinimize={() => record("onMinimize")} onClose={() => record("onClose")}
onPrint={() => record("onPrint")} onPopupGuide={() => record("onPopupGuide")}
onPropertyBarOpenChange={next => record(`onPropertyBarOpenChange(${next})`)} onPropertyPanelOpenChange={next => record(`onPropertyPanelOpenChange(${next})`)}
propertyBarOpen={undefined} propertyPanelOpen={undefined}
propertyBar={{ selection: { id: "circle", title: "원", kind: "single" }, properties: BAR_FIXTURE }}
propertyPanel={{ selection: { id: "circle", title: "원", kind: "single" }, properties: BAR_FIXTURE, groups: [{ id: "shape" }] }} />
return <div data-composition-demo="" data-hidden={hidden.join(",")} data-order={order?.join(",") ?? "default"}>
<div className="sw-story-controls"><strong>프리셋</strong>
{COMPOSITION_PRESETS.map(preset => <button key={preset.name} onClick={() => { setHidden(preset.hidden); setOrder(preset.order) }}>{preset.name}</button>)}
</div>
<div className="sw-story-controls"><strong>표시 (로고 고정)</strong>
{HIDEABLE_PARTS.map(part => <label key={part.id}><input type="checkbox" aria-label={`표시 ${part.id}`} checked={!hidden.includes(part.id)} onChange={() => toggle(part.id)} />{part.name}</label>)}
</div>
<div className="sw-story-controls"><strong>메뉴 순서</strong>
<button onClick={() => setOrder(undefined)}>기본 순서</button>
<button onClick={() => setOrder([...(order ?? MENU_IDS)].reverse())}>순서 반전</button>
<button onClick={() => setOrder(list => { const items = [...(list ?? MENU_IDS)]; items.push(items.shift()!); return items })}>첫 메뉴를 끝으로</button>
<button onClick={() => setOrder(["color", "color", "unknown" as SWHeaderMenuId, "layer"])}>중복·알 수 없는 ID</button>
<code data-order-state="">{(order ?? ["(default)"]).join(" → ")}</code>
</div>
<div className="sw-story-controls"><strong>너비</strong>
<button onClick={() => setWidth(null)}>실제 화면</button>
{[1920, 1440, 1080, 900, 768, 640, 390].map(value => <button key={value} onClick={() => setWidth(value)}>{value}px</button>)}
</div>
<p className="sw-story-log" aria-live="polite" data-callback-log="">Callback: {log.length ? log.join(" → ") : "없음"}</p>
<div className="sw-story-property-room">{width ? <Frame width={width}>{header}</Frame> : header}</div>
</div>
}
export const Composition: Story = {
name: "Composition (Visibility · Order)",
parameters: { controls: { disable: true } },
render: () => <StoryPage className="sw-story-followup"><StoryPageHeader kicker={KICKER} title="Header 구성 · 메뉴 순서" description="hiddenControls로 로고 외 모든 구성요소를 개별 제거·재표시하고, menuOrder로 레이어·색상·선/문자/치수스타일 순서를 바꿉니다. 아무것도 지정하지 않으면 승인된 기본 디자인입니다. 값은 호스트 상태라 재표시·순서 변경 후에도 유지되며, 순서 변경은 Callback을 호출하지 않습니다." /><CompositionDemo /></StoryPage>,
}
/* Property panel: the four Figma states with the same component and host data. */
const PROPERTY_STATES: { example: PropertyExample; title: string; node: string; size: string }[] = [
{ example: "none", title: "선택된 객체 없음", node: "26:11886", size: "314 × 256" },
@@ -1320,109 +950,3 @@ export const PropertyPanelCompare: Story = {
parameters: { controls: { disable: true } },
render: () => <StoryPage className="sw-story-compare-page"><StoryPageHeader kicker={KICKER} title="속성창 4개 상태 비교" description="네 패널은 같은 SWHeaderPropertyPanel입니다. 실제 Header에서는 현재 선택 객체의 패널 하나만 표시합니다." /><PropertyCompare /></StoryPage>,
}
/* Shape slot: replaceable area; the values stay in the property rows. */
function SampleShapeViewer({ label, kind }: { label: string; kind: "circle" | "rect" }) {
return <svg width="208" height="208" viewBox="0 0 208 208" role="img" aria-label={label} data-sample-shape={kind}>
{kind === "circle" ? <circle cx="104" cy="104" r="84" fill="none" stroke="#1d5a8c" strokeWidth="2" strokeDasharray="6 4" />
: <rect x="34" y="54" width="140" height="100" fill="none" stroke="#1d5a8c" strokeWidth="2" />}
<text x="104" y="200" textAnchor="middle" fontSize="11" fill="#1d5a8c">{label}</text>
</svg>
}
const SHAPE_MODES = ["default", "empty", "custom", "render"] as const
const SHAPE_MODE_LABELS = { default: "기본 Figma 형상", empty: "비움 (null)", custom: "외부 컴포넌트", render: "Render Prop" }
function ShapeSlotDemo() {
const [mode, setMode] = useState<(typeof SHAPE_MODES)[number]>("default")
const [kind, setKind] = useState<"circle" | "rect">("circle")
const [properties, setProperties] = useState(() => propertyFixture("circle"))
const [log, setLog] = useState<string[]>([])
const shapeContent: SWHeaderPropertyShapeContent = mode === "default" ? undefined : mode === "empty" ? null
: mode === "custom" ? <SampleShapeViewer label="외부 형상 컴포넌트" kind={kind} />
: context => <SampleShapeViewer label={`${context.selection.title} · ${context.properties.length}개 값`} kind={kind} />
return <div data-shape-demo="" data-mode={mode} data-kind={kind}>
<div className="sw-story-controls"><strong>형상 Slot</strong>
{SHAPE_MODES.map(name => <button key={name} data-shape-mode={name} onClick={() => setMode(name)}>{SHAPE_MODE_LABELS[name]}</button>)}
<button onClick={() => setKind(value => value === "circle" ? "rect" : "circle")}>형상 교체 (원 ↔ 사각형)</button>
</div>
<div className="sw-story-controls"><strong>속성값 (형상과 독립)</strong>
<button onClick={() => setProperties(items => items.map(item => item.id === "diameter" ? { ...item, value: "10.0000" } : item.id === "radius" ? { ...item, value: "5.0000" } : item))}>지름·반지름 외부 변경</button>
<button onClick={() => setProperties(items => [...items, { id: `shape-extra-${items.length}`, groupId: "shape", name: "추가 형상값", type: "text", value: "호스트 값" }])}>형상 그룹 항목 추가</button>
<button onClick={() => setProperties(items => items.filter(item => item.id !== "area"))}>면적 삭제</button>
</div>
<p className="sw-story-log" data-property-log="">{log.join(" → ") || "Callback 없음"}</p>
<div className="sw-story-property-state"><SWTheme><SWHeaderPropertyPanel selection={propertySelection("circle")} properties={properties} groups={PROPERTY_GROUPS} shapeContent={shapeContent}
onPropertyChange={(property, value) => { setLog(lines => [...lines.slice(-9), `property(${property.id}, ${value})`]); setProperties(items => items.map(item => item.id === property.id ? { ...item, value } : item)) }} /></SWTheme></div>
</div>
}
export const PropertyShapeSlot: Story = {
name: "Property Panel · 형상 Slot",
parameters: { controls: { disable: true } },
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="형상 영역 Slot" description="shapeContent: 미지정은 Figma 원형 그림, null은 208 × 208 빈 영역(치수 라벨·패널 높이 유지), ReactNode·Render Prop은 외부 형상 컴포넌트입니다. 예시 SVG는 Storybook 전용 샘플이며 패키지에 포함되지 않습니다. 원주·지름·반지름·면적·위치 값은 properties로만 표시됩니다." /><ShapeSlotDemo /></StoryPage>,
}
/* Property fields (2026-10-19): the 13 Figma field nodes in the real SWHeaderPropertyPanel rows.
Default / pinned Hover columns are specimens; the live panel is the one to click (On / dropdown). */
const FIELD_NODES = [
{ node: "26:12388 / 26:12404", name: "선스타일", states: "Default · Hover / On(Dropdown)" },
{ node: "26:12250 / 26:12265", name: "레이어 — 다양함", states: "Default · Hover / On(Dropdown)" },
{ node: "26:12260 / 26:12291", name: "레이어 — 단일 값", states: "Default · Hover / On(Dropdown)" },
{ node: "26:12314", name: "선축적", states: "Default · Hover · On(편집)" },
{ node: "26:12649", name: "선가중치 · 투명도", states: "Default · Hover · On(편집)" },
{ node: "26:12170 / 26:12184", name: "색상", states: "Default · Hover / On(Dropdown)" },
{ node: "26:12583", name: "하이퍼링크", states: "Default · Hover (Figma On 없음)" },
{ node: "26:12590", name: "두께", states: "Default · Hover · On(편집)" },
{ node: "26:12574", name: "재료", states: "Default · Hover (Figma On 없음)" },
]
// 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 PropertyFieldsDemo() {
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 className="sw-story-controls"><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 className="sw-story-log" data-property-log="" aria-live="polite">{log.join(" → ") || "Callback 없음"}</p>
<pre className="sw-story-layer-state" data-property-state="">{JSON.stringify(properties.map(item => [item.id, item.mixed ? "다양함" : item.value]))}</pre>
<div className="sw-story-compare">
<figure className="sw-story-compare__cell" data-fields="live"><figcaption><strong>실제 조작</strong><span>클릭 → On · Enter 확정 · Escape 취소/닫기</span></figcaption>
<FieldPanel title="실제 조작" properties={properties} onChange={change} menus={noMenus ? undefined : menus} /></figure>
<figure className="sw-story-compare__cell" data-fields="default"><figcaption><strong>Default</strong><span>Figma Default</span></figcaption>
<FieldPanel title="Default" properties={FIELD_FIXTURE} /></figure>
<figure className="sw-story-compare__cell" data-fields="hover"><figcaption><strong>Hover (고정)</strong><span>Figma Hover · 값 변경 없음</span></figcaption>
<PinHover selectors={FIELD_HOVER}><FieldPanel title="Hover" properties={FIELD_FIXTURE} /></PinHover></figure>
</div>
<table className="sw-story-node-table"><thead><tr><th>Figma</th><th>항목</th><th>상태</th></tr></thead>
<tbody>{FIELD_NODES.map(item => <tr key={item.node}><td>{item.node}</td><td>{item.name}</td><td>{item.states}</td></tr>)}</tbody></table>
</div>
}
export const PropertyFields: Story = {
name: "Property Panel · 필드 13개",
parameters: { controls: { disable: true } },
render: () => <StoryPage className="sw-story-followup"><StoryPageHeader kicker={KICKER} title="속성창 입력·선택 필드" description="Figma 13개 필드(26:12388 ~ 26:12649)를 기존 SWHeaderPropertyPanel 행으로 표시합니다. Default · Hover 고정 표본과 실제 조작 패널입니다. 숫자는 편집 On(#0F0D0A), 레이어·선스타일·색상은 Dropdown On(#211D13)이며 하이퍼링크·재료는 Figma에 On이 없어 Default · Hover만 있습니다. 색상 255,259,0은 Figma 표기 예시로, 입력 검증은 0~255를 유지합니다." /><PropertyFieldsDemo /></StoryPage>,
}
-18
View File
@@ -15,33 +15,18 @@
runtime dropdown still clamps to the real viewport. Scroll the specimen to see its right edge. */
.sw-story-fixed-dimension .hm-sw-header__list-menu--dimension-style { left: auto !important; right: 0; }
.sw-story-fixed-dimension .hm-sw-header__list-menu--dimension-style[data-shifted="true"]::before { top: calc(-1 * var(--hm-sw-header-field-height) - 1px); }
.sw-story-explicit-disabled { min-width: 0; margin: 0; padding: 0; border: 0; }
.sw-story-log { margin: 0 0 8px; font-size: 13px; color: #3d4a45; }
.sw-story-stack { display: flex; flex-direction: column; gap: 8px; }
/* Window Move Motion: window controls alone on the window-area surface, magnified for comparison. */
.sw-story-window { display: flex; width: 152px; height: 48px; background: var(--hm-sw-header-document-background); }
.sw-story-zoom { zoom: 3; width: max-content; }
.sw-story-row { display: flex; flex-wrap: wrap; gap: 24px; }
.sw-story-stages { display: grid; grid-template-columns: repeat(auto-fill, minmax(480px, 1fr)); gap: 16px; }
.sw-story-stage { margin: 0; }
.sw-story-stage figcaption { margin-top: 6px; font-size: 13px; color: #3d4a45; }
/* Layer Dropdown: room below a standalone layer field for its overlay list. */
.sw-story-layer-room { padding-bottom: 300px; }
.sw-story-layer-state { margin: 0 0 8px; font-size: 12px; color: #3d4a45; font-family: ui-monospace, monospace; white-space: pre-wrap; }
/* Field dropdowns / Dynamic Data: story-only controls and Figma reference images. */
.sw-story-reference { display: block; max-width: none; image-rendering: pixelated; }
.sw-story-figma { padding: 12px; background: #27241d; }
.sw-story-figma .sw-story-reference { zoom: 2; }
.sw-story-detail { background: var(--hm-sw-header-layer-menu-background); padding: 8px 12px; gap: 12px; }
.sw-story-dynamic { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
.sw-story-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 12px; color: #3d4a45; }
.sw-story-controls > strong { min-width: 110px; }
.sw-story-controls > button { padding: 2px 8px; border: 1px solid #b6c4bf; border-radius: 4px; background: #fff; color: #1f2a26; font-size: 12px; cursor: pointer; }
.sw-story-property-room { min-height: 820px; }
/* New responsive scenarios measure the Header at the actual viewport width. */
.sw-story-followup.sb-page > .sb-page-inner { width: 100%; }
/* Property panel states (2026-10-16): standalone panels at their natural size; the comparison wraps. */
.sw-story-property-state { display: inline-block; }
.sw-story-compare { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px; }
.sw-story-compare__cell { margin: 0; }
.sw-story-compare__cell figcaption { display: flex; flex-direction: column; gap: 2px; margin-bottom: 8px; font-size: 12px; color: #3d4a45; }
@@ -59,9 +44,6 @@
.sw-story-detail-comparison { overflow-x: auto; padding: 16px; background: var(--hm-sw-header-layer-menu-background); }
.sw-story-detail-row { display: grid; grid-template-columns: 140px repeat(4, 180px); align-items: center; gap: 16px; min-width: 924px; padding: 12px 0; }
.sw-story-detail-row > div { display: flex; align-items: center; gap: 8px; }
/* Property fields (2026-10-19): Figma node table under the specimens. */
.sw-story-node-table { margin-top: 16px; border-collapse: collapse; font-size: 12px; color: #3d4a45; }
.sw-story-node-table :is(th, td) { padding: 4px 10px; border-bottom: 1px solid #d5ddd9; text-align: left; }
/* Overview host layout only. Native controls below are Storybook data controls, outside the product. */
.sw-story-overview.sb-page { height: 100dvh; min-height: 0; padding-block: 16px; }