refactor(storybook): consolidate property panel comparison layout

Keep sw-header--property-panel-compare as the single property panel state
page: the four existing SWHeaderPropertyPanel states in 2 columns x 2 rows
at their original size, inside a scrolling Inner. Remove the duplicate
Property Panel Variants and the four single-state Stories (SW 24 -> 19).
Storybook-only change; SW runtime and public API untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shkim4andClaude Opus 5.5 committed 2026-10-19 12:52:29 +09:00
1 parent c705a1fc59
commit c8e5fbeebc
82 files changed
+12318 -49

No files matched your search

+21 -33
View File
@@ -1038,7 +1038,7 @@ function usePropertyMenus(record: (line: string) => void, setProperties: (update
}
}
function PropertyPanelDemo({ standalone = false }: { standalone?: boolean }) {
function PropertyPanelDemo() {
const [example, setExample] = useState<PropertyExample>("none")
const [selection, setSelection] = useState(propertySelection("none"))
const [properties, setProperties] = useState(propertyFixture("none"))
@@ -1087,12 +1087,11 @@ function PropertyPanelDemo({ standalone = false }: { standalone?: boolean }) {
</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>
{standalone ? <SWTheme><SWHeaderPropertyPanel {...panel} onClose={() => record("close")} /></SWTheme>
: <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 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>
)
}
@@ -1102,11 +1101,6 @@ export const PropertyPanel: Story = {
parameters: { controls: { disable: true } },
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="속성창" description="Figma 4개 상태를 외부 데이터·그룹 펼침으로 구성합니다. 속성창 버튼(좁은 화면: 더보기)으로 열고 닫으세요. Enter/Blur 확정, Escape 취소, 다음 Escape 닫기." /><PropertyPanelDemo /></StoryPage>,
}
export const PropertyPanelVariants: Story = {
name: "Property Panel Variants",
parameters: { controls: { disable: true } },
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="속성창 4개 상태" description="동일 컴포넌트와 호스트 데이터입니다. 측정용 독립 패널." /><PropertyPanelDemo standalone /></StoryPage>,
}
export const KGuide: Story = {
name: "K Guide",
parameters: { controls: { disable: true } },
@@ -1293,22 +1287,11 @@ function PropertyState({ example, shapeContent, onLog }: { example: PropertyExam
onPropertyChange={(property, value) => { onLog?.(`property(${property.id}, ${value})`); setProperties(items => items.map(item => item.id === property.id ? { ...item, value, mixed: false, ...(item.id === "line" ? { linePattern: linePatternOf(value) } : {}) } : item)) }}
onClose={() => onLog?.("close")} /></SWTheme>
}
function PropertyStateStory({ example }: { example: PropertyExample }) {
const state = PROPERTY_STATES.find(item => item.example === example)!
const [log, setLog] = useState<string[]>([])
return <StoryPage><StoryPageHeader kicker={KICKER} title={`속성창 · ${state.title}`} description={`Figma ${state.node} (${state.size}px). 기존 SWHeaderPropertyPanel 하나에 호스트 데이터만 바꿔 표시합니다. Enter/Blur 확정, Escape 취소.`} />
<p className="sw-story-log" data-property-log="">{log.join(" → ") || "Callback 없음"}</p>
<div className="sw-story-property-state" data-property-state={example}><PropertyState example={example} onLog={line => setLog(lines => [...lines.slice(-9), line])} /></div>
</StoryPage>
}
export const PropertyPanelNone: Story = { name: "Property Panel · 선택 없음", parameters: { controls: { disable: true } }, render: () => <PropertyStateStory example="none" /> }
export const PropertyPanelAll: Story = { name: "Property Panel · 전체 선택", parameters: { controls: { disable: true } }, render: () => <PropertyStateStory example="all" /> }
export const PropertyPanelCircle: Story = { name: "Property Panel · 원 1개", parameters: { controls: { disable: true } }, render: () => <PropertyStateStory example="circle" /> }
export const PropertyPanelExpanded: Story = { name: "Property Panel · 모두 펼침", parameters: { controls: { disable: true } }, render: () => <PropertyStateStory example="expanded" /> }
/** Design check only: a real header shows one panel for the current selection. */
/** Design check only: a real header shows one panel for the current selection.
2 columns × 2 rows at the panels' own size; the Inner scrolls instead of shrinking or wrapping them. */
function PropertyCompare() {
const [sizes, setSizes] = useState<Record<string, string>>({})
const [log, setLog] = useState<string[]>([])
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const measure = () => setSizes(Object.fromEntries(Array.from(ref.current?.querySelectorAll<HTMLElement>("[data-compare]") ?? []).map(cell => {
@@ -1320,17 +1303,22 @@ function PropertyCompare() {
ref.current?.querySelectorAll(".hm-sw-property").forEach(panel => observer.observe(panel))
return () => observer.disconnect()
}, [])
return <div ref={ref} className="sw-story-compare">
{PROPERTY_STATES.map(state => <figure key={state.example} className="sw-story-compare__cell" data-compare={state.example}>
<figcaption><strong>{state.title}</strong><span>Figma {state.node} · {state.size}</span><span data-measured="">측정 {sizes[state.example] ?? "-"}</span></figcaption>
<PropertyState example={state.example} />
</figure>)}
</div>
return <>
<p className="sw-story-log" data-property-log="" aria-live="polite">{log.join(" → ") || "Callback 없음"}</p>
<div className="sw-story-compare-scroll" data-compare-scroll="">
<div ref={ref} className="sw-story-compare sw-story-compare--grid">
{PROPERTY_STATES.map(state => <figure key={state.example} className="sw-story-compare__cell" data-compare={state.example}>
<figcaption><strong>{state.title}</strong><span>Figma {state.node} · {state.size}</span><span data-measured="">측정 {sizes[state.example] ?? "-"}</span></figcaption>
<PropertyState example={state.example} onLog={line => setLog(lines => [...lines.slice(-7), `${state.example}: ${line}`])} />
</figure>)}
</div>
</div>
</>
}
export const PropertyPanelCompare: Story = {
name: "Property Panel · 4개 비교",
parameters: { controls: { disable: true } },
render: () => <StoryPage className="sw-story-followup"><StoryPageHeader kicker={KICKER} title="속성창 4개 상태 비교" description="디자인 검증용 비교 화면입니다. 네 패널은 같은 SWHeaderPropertyPanel이며, 실제 Header에서는 현재 선택 객체의 패널 하나만 표시합니다. 폭이 부족하면 줄바꿈합니다." /><PropertyCompare /></StoryPage>,
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. */
+11
View File
@@ -45,6 +45,17 @@
.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; }
/* Property panel 4-state comparison (2026-10-19): 2 columns × 2 rows at the panels' own size inside a
viewport-high page. Only the Inner scroller scrolls (both axes); panels are never shrunk or wrapped. */
.sw-story-compare-page.sb-page { height: 100dvh; min-height: 0; }
.sw-story-compare-page > .sb-page-inner { height: 100%; display: flex; flex-direction: column; }
.sw-story-compare-page .sb-page-header { flex: 0 0 auto; }
.sw-story-compare-page .sw-story-log { flex: 0 0 auto; margin-top: 12px; }
.sw-story-compare-scroll { flex: 1 1 0; min-height: 0; overflow: auto; overscroll-behavior: contain; }
.sw-story-compare.sw-story-compare--grid { display: grid; grid-template-columns: repeat(2, max-content); align-items: start; gap: 24px; width: max-content; }
/* Placement-only exception, same as the Overview Stage: the product clamps a panel to the viewport
height; here the scroller provides the room so the 754px panel keeps its size. */
.sw-story-compare--grid .hm-sw-property { max-height: none; }
.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; }