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

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

@@ -0,0 +1,202 @@
{
"mode": "before",
"base": "static",
"browser": "155.0.8059.80",
"checks": [],
"errors": [],
"data": {
"inventory": {
"total": 343,
"sw": 24,
"ids": [
[
"sw-header--composed-controls",
"Dropdown"
],
[
"sw-header--detail-buttons",
"Detail Buttons (Shared States)"
],
[
"sw-header--overview",
"Overview"
],
[
"sw-header--interactive-states",
"Interactive States"
],
[
"sw-header--window-move-motion",
"Window Move Motion"
],
[
"sw-header--logo-slot",
"Logo Slot"
],
[
"sw-header--document-dropdown",
"Document Dropdown"
],
[
"sw-header--responsive-more",
"Responsive More"
],
[
"sw-header--layer-detail-input",
"Layer Detail · Input"
],
[
"sw-header--property-panel",
"Property Panel"
],
[
"sw-header--property-panel-variants",
"Property Panel Variants"
],
[
"sw-header--k-guide",
"K Guide"
],
[
"sw-header--additional-hover",
"Additional Hover"
],
[
"sw-header--property-bar",
"Property Bar"
],
[
"sw-header--dynamic-controls",
"Dynamic Controls"
],
[
"sw-header--colorize",
"Colorize"
],
[
"sw-header--composition",
"Composition (Visibility · Order)"
],
[
"sw-header--property-panel-none",
"Property Panel · 선택 없음"
],
[
"sw-header--property-panel-all",
"Property Panel · 전체 선택"
],
[
"sw-header--property-panel-circle",
"Property Panel · 원 1개"
],
[
"sw-header--property-panel-expanded",
"Property Panel · 모두 펼침"
],
[
"sw-header--property-panel-compare",
"Property Panel · 4개 비교"
],
[
"sw-header--property-shape-slot",
"Property Panel · 형상 Slot"
],
[
"sw-header--property-fields",
"Property Panel · 필드 13개"
]
]
},
"compare": {
"cells": [
{
"id": "none",
"cell": {
"x": 0,
"y": 177,
"w": 314,
"h": 322,
"right": 314,
"bottom": 499
},
"panel": {
"x": 0,
"y": 243,
"w": 314,
"h": 256,
"right": 314,
"bottom": 499
},
"caption": "선택된 객체 없음"
},
{
"id": "all",
"cell": {
"x": 338,
"y": 177,
"w": 314,
"h": 427,
"right": 652,
"bottom": 604
},
"panel": {
"x": 338,
"y": 243,
"w": 314,
"h": 361,
"right": 652,
"bottom": 604
},
"caption": "전체 객체 선택"
},
{
"id": "circle",
"cell": {
"x": 676,
"y": 177,
"w": 314,
"h": 724,
"right": 990,
"bottom": 901
},
"panel": {
"x": 676,
"y": 243,
"w": 314,
"h": 658,
"right": 990,
"bottom": 901
},
"caption": "원 1개 선택"
},
{
"id": "expanded",
"cell": {
"x": 1014,
"y": 177,
"w": 314,
"h": 820,
"right": 1328,
"bottom": 997
},
"panel": {
"x": 1014,
"y": 243,
"w": 314,
"h": 754,
"right": 1328,
"bottom": 997
},
"caption": "모든 항목 펼침"
}
],
"scroller": null,
"doc": {
"scrollW": 1440,
"clientW": 1440,
"scrollH": 1037,
"clientH": 900
}
}
}
}
@@ -0,0 +1,246 @@
{
"total": 343,
"sw": [
{
"id": "sw-header--composed-controls",
"name": "Dropdown",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--detail-buttons",
"name": "Detail Buttons (Shared States)",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest",
"verification"
]
},
{
"id": "sw-header--overview",
"name": "Overview",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--interactive-states",
"name": "Interactive States",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--window-move-motion",
"name": "Window Move Motion",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--logo-slot",
"name": "Logo Slot",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--document-dropdown",
"name": "Document Dropdown",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--responsive-more",
"name": "Responsive More",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--layer-detail-input",
"name": "Layer Detail · Input",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-panel",
"name": "Property Panel",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-panel-variants",
"name": "Property Panel Variants",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--k-guide",
"name": "K Guide",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--additional-hover",
"name": "Additional Hover",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-bar",
"name": "Property Bar",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--dynamic-controls",
"name": "Dynamic Controls",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--colorize",
"name": "Colorize",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--composition",
"name": "Composition (Visibility · Order)",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-panel-none",
"name": "Property Panel · 선택 없음",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-panel-all",
"name": "Property Panel · 전체 선택",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-panel-circle",
"name": "Property Panel · 원 1개",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-panel-expanded",
"name": "Property Panel · 모두 펼침",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-panel-compare",
"name": "Property Panel · 4개 비교",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-shape-slot",
"name": "Property Panel · 형상 Slot",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
},
{
"id": "sw-header--property-fields",
"name": "Property Panel · 필드 13개",
"title": "SW/Header",
"tags": [
"dev",
"test",
"manifest"
]
}
]
}
@@ -0,0 +1,114 @@
{
"mode": "preservation",
"browser": "155.0.8059.80",
"checks": [
{
"name": "required responsive width 390",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "required responsive width 768",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "required responsive width 1280",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "required responsive width 1440",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "preserve property panel none",
"status": "PASS",
"data": {
"x": 360,
"y": 282.5,
"width": 314,
"height": 256
}
},
{
"name": "preserve property panel all",
"status": "PASS",
"data": {
"x": 698,
"y": 282.5,
"width": 314,
"height": 361
}
},
{
"name": "preserve property panel circle",
"status": "PASS",
"data": {
"x": 360,
"y": 733.5,
"width": 314,
"height": 658
}
},
{
"name": "preserve property panel expanded",
"status": "PASS",
"data": {
"x": 698,
"y": 733.5,
"width": 314,
"height": 754
}
},
{
"name": "preserve window motion 1400ms",
"status": "PASS"
},
{
"name": "window motion leave clears animations",
"status": "PASS"
},
{
"name": "preserve K guide animation",
"status": "PASS"
},
{
"name": "K guide leave closes",
"status": "PASS"
},
{
"name": "preserve Colorize size",
"status": "PASS"
},
{
"name": "preserve editable Input",
"status": "PASS"
},
{
"name": "no browser errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"styles": {},
"status": "PASS"
}
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,774 @@
{
"before": [
{
"type": "story",
"subtype": "story",
"id": "sw-header--detail-buttons",
"name": "Detail Buttons (Shared States)",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest",
"verification"
],
"exportName": "DetailButtons"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--overview",
"name": "Overview",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "Overview"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--interactive-states",
"name": "Interactive States",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "InteractiveStates"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--window-move-motion",
"name": "Window Move Motion",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "WindowMoveMotion"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--composed-controls",
"name": "Composed Controls",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "ComposedControls"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--logo-slot",
"name": "Logo Slot",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "LogoSlot"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--document-dropdown",
"name": "Document Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "DocumentDropdown"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--layer-dropdown",
"name": "Layer Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "LayerDropdown"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--color-dropdown",
"name": "Color Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "ColorDropdown"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--line-style-dropdown",
"name": "Line Style Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "LineStyleDropdown"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--text-dimension-style-dropdown",
"name": "Text · Dimension Style Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "TextDimensionStyleDropdown"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--responsive-more",
"name": "Responsive More",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "ResponsiveMore"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--layer-detail-input",
"name": "Layer Detail · Input",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "LayerDetailInput"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-panel",
"name": "Property Panel",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyPanel"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-panel-variants",
"name": "Property Panel Variants",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyPanelVariants"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--k-guide",
"name": "K Guide",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "KGuide"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--additional-hover",
"name": "Additional Hover",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "AdditionalHover"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-bar",
"name": "Property Bar",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyBar"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--dynamic-controls",
"name": "Dynamic Controls",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "DynamicControls"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--colorize",
"name": "Colorize",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "Colorize"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--composition",
"name": "Composition (Visibility · Order)",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "Composition"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-panel-none",
"name": "Property Panel · 선택 없음",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyPanelNone"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-panel-all",
"name": "Property Panel · 전체 선택",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyPanelAll"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-panel-circle",
"name": "Property Panel · 원 1개",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyPanelCircle"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-panel-expanded",
"name": "Property Panel · 모두 펼침",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyPanelExpanded"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-panel-compare",
"name": "Property Panel · 4개 비교",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyPanelCompare"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-shape-slot",
"name": "Property Panel · 형상 Slot",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyShapeSlot"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-fields",
"name": "Property Panel · 필드 13개",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyFields"
}
],
"after": [
{
"type": "story",
"subtype": "story",
"id": "sw-header--composed-controls",
"name": "Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header-dropdown.stories.tsx",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "ComposedControls"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--detail-buttons",
"name": "Detail Buttons (Shared States)",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest",
"verification"
],
"exportName": "DetailButtons"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--overview",
"name": "Overview",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "Overview"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--interactive-states",
"name": "Interactive States",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "InteractiveStates"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--window-move-motion",
"name": "Window Move Motion",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "WindowMoveMotion"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--logo-slot",
"name": "Logo Slot",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "LogoSlot"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--document-dropdown",
"name": "Document Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "DocumentDropdown"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--responsive-more",
"name": "Responsive More",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "ResponsiveMore"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--layer-detail-input",
"name": "Layer Detail · Input",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "LayerDetailInput"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-panel",
"name": "Property Panel",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyPanel"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--k-guide",
"name": "K Guide",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "KGuide"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--additional-hover",
"name": "Additional Hover",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "AdditionalHover"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-bar",
"name": "Property Bar",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyBar"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--dynamic-controls",
"name": "Dynamic Controls",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "DynamicControls"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--colorize",
"name": "Colorize",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "Colorize"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--composition",
"name": "Composition (Visibility · Order)",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "Composition"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-panel-compare",
"name": "Property Panel · 4개 비교",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyPanelCompare"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-shape-slot",
"name": "Property Panel · 형상 Slot",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyShapeSlot"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--property-fields",
"name": "Property Panel · 필드 13개",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "PropertyFields"
}
],
"removed": [
{
"type": "story",
"subtype": "story",
"id": "sw-header--layer-dropdown",
"name": "Layer Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "LayerDropdown"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--color-dropdown",
"name": "Color Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "ColorDropdown"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--line-style-dropdown",
"name": "Line Style Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "LineStyleDropdown"
},
{
"type": "story",
"subtype": "story",
"id": "sw-header--text-dimension-style-dropdown",
"name": "Text · Dimension Style Dropdown",
"title": "SW/Header",
"importPath": "./src/sw/sw-header.stories.tsx",
"componentPath": "./packages/sw/dist/index.js",
"tags": [
"dev",
"test",
"manifest"
],
"exportName": "TextDimensionStyleDropdown"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,81 @@
{
"label": "run",
"checks": [
{
"name": "size 44 x 30, top-right of the panel",
"status": "PASS",
"data": {
"w": 44,
"h": 30,
"right": 1,
"top": 1,
"imgs": [
[
"hm-sw-property__close-default",
44,
30
],
[
"hm-sw-property__close-hover",
44,
30
]
]
}
},
{
"name": "Default: no canvas-light pixels on the button edge / corner",
"status": "PASS",
"data": {
"bright": 0,
"where": []
}
},
{
"name": "Hover: hover asset shown, no canvas-light pixels on the edge / corner",
"status": "PASS",
"data": {
"vis": [
"hidden",
"visible"
],
"bright": 0,
"where": []
}
},
{
"name": "click closes the panel, focus back on 속성창 trigger",
"status": "PASS",
"data": {
"open": false,
"focusTrigger": true,
"log": "legacyToggle → open(true) → open(false)"
}
},
{
"name": "Escape closes the panel, focus back on 속성창 trigger",
"status": "PASS",
"data": {
"open": false,
"focusTrigger": true,
"log": "rue) → open(false) → legacyToggle → open(true) → open(false)"
}
},
{
"name": "no console / page errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"pixels": {
"def": {
"bright": 0,
"where": []
},
"hov": {
"bright": 0,
"where": []
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,813 @@
{
"label": "run",
"checks": [
{
"name": "Default layer-mixed: 200 x 28, radius 2, no border / surface",
"status": "PASS",
"data": {
"x": 97,
"y": 371.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"borderTopLeftRadius": "2px"
}
},
{
"name": "Default layer: 200 x 28, radius 2, no border / surface",
"status": "PASS",
"data": {
"x": 97,
"y": 403.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"borderTopLeftRadius": "2px"
}
},
{
"name": "Default color: 200 x 28, radius 2, no border / surface",
"status": "PASS",
"data": {
"x": 97,
"y": 435.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"borderTopLeftRadius": "2px"
}
},
{
"name": "Default line: 200 x 28, radius 2, no border / surface",
"status": "PASS",
"data": {
"x": 97,
"y": 467.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"borderTopLeftRadius": "2px"
}
},
{
"name": "Default scale: 200 x 28, radius 2, no border / surface",
"status": "PASS",
"data": {
"x": 97,
"y": 531.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"borderTopLeftRadius": "2px"
}
},
{
"name": "Default link: 200 x 28, radius 2, no border / surface",
"status": "PASS",
"data": {
"x": 97,
"y": 595.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"borderTopLeftRadius": "2px"
}
},
{
"name": "Default thickness: 200 x 28, radius 2, no border / surface",
"status": "PASS",
"data": {
"x": 97,
"y": 627.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"borderTopLeftRadius": "2px"
}
},
{
"name": "Default material: 200 x 28, radius 2, no border / surface",
"status": "PASS",
"data": {
"x": 97,
"y": 659.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"borderTopLeftRadius": "2px"
}
},
{
"name": "Default weight: compact 28px hug field (pl 6 / pr 4 incl. border), no border",
"status": "PASS",
"data": {
"x": 97,
"y": 499.5,
"w": 39.23,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"paddingLeft": "5px",
"paddingRight": "3px"
}
},
{
"name": "Default opacity: compact 28px hug field (pl 6 / pr 4 incl. border), no border",
"status": "PASS",
"data": {
"x": 97,
"y": 563.5,
"w": 39.23,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"paddingLeft": "5px",
"paddingRight": "3px"
}
},
{
"name": "Typography: value #E60403 14px, unit #000 12px",
"status": "PASS",
"data": {
"value": [
"rgb(230, 4, 3)",
"14px"
],
"unit": [
"rgb(0, 0, 0)",
"12px"
]
}
},
{
"name": "Hover layer-mixed: rgba(33,29,19,.4) border, 28px kept, value not moved",
"status": "PASS",
"data": {
"x": 97,
"y": 371.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"before": [
105,
377.5
],
"after": [
105,
377.5
]
}
},
{
"name": "Mouse Leave layer-mixed: back to Default",
"status": "PASS",
"data": {
"x": 97,
"y": 371.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Hover layer: rgba(33,29,19,.4) border, 28px kept, value not moved",
"status": "PASS",
"data": {
"x": 97,
"y": 403.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"before": [
105,
409.5
],
"after": [
105,
409.5
]
}
},
{
"name": "Mouse Leave layer: back to Default",
"status": "PASS",
"data": {
"x": 97,
"y": 403.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Hover color: rgba(33,29,19,.4) border, 28px kept, value not moved, divider shown",
"status": "PASS",
"data": {
"x": 98,
"y": 436.5,
"w": 114,
"h": 26,
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"before": [
105,
436.5
],
"after": [
105,
436.5
],
"borderRightColor": "rgba(33, 29, 19, 0.4)"
}
},
{
"name": "Mouse Leave color: back to Default",
"status": "PASS",
"data": {
"x": 97,
"y": 435.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Hover line: rgba(33,29,19,.4) border, 28px kept, value not moved",
"status": "PASS",
"data": {
"x": 97,
"y": 467.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"before": [
105,
473.5
],
"after": [
105,
473.5
]
}
},
{
"name": "Mouse Leave line: back to Default",
"status": "PASS",
"data": {
"x": 97,
"y": 467.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Hover scale: rgba(33,29,19,.4) border, 28px kept, value not moved",
"status": "PASS",
"data": {
"x": 97,
"y": 531.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"before": [
105,
532.5
],
"after": [
105,
532.5
]
}
},
{
"name": "Mouse Leave scale: back to Default",
"status": "PASS",
"data": {
"x": 97,
"y": 531.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Hover link: rgba(33,29,19,.4) border, 28px kept, value not moved",
"status": "PASS",
"data": {
"x": 97,
"y": 595.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"before": [
105,
596.5
],
"after": [
105,
596.5
]
}
},
{
"name": "Mouse Leave link: back to Default",
"status": "PASS",
"data": {
"x": 97,
"y": 595.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Hover thickness: rgba(33,29,19,.4) border, 28px kept, value not moved",
"status": "PASS",
"data": {
"x": 97,
"y": 627.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"before": [
105,
628.5
],
"after": [
105,
628.5
]
}
},
{
"name": "Mouse Leave thickness: back to Default",
"status": "PASS",
"data": {
"x": 97,
"y": 627.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Hover material: rgba(33,29,19,.4) border, 28px kept, value not moved",
"status": "PASS",
"data": {
"x": 97,
"y": 659.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"backgroundColor": "rgba(0, 0, 0, 0)",
"before": [
105,
660.5
],
"after": [
105,
660.5
]
}
},
{
"name": "Mouse Leave material: back to Default",
"status": "PASS",
"data": {
"x": 97,
"y": 659.5,
"w": 200,
"h": 28,
"borderTopColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Hover / Leave weight compact",
"status": "PASS",
"data": {
"hover": "rgba(33, 29, 19, 0.4)",
"leave": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Hover / Leave opacity compact",
"status": "PASS",
"data": {
"hover": "rgba(33, 29, 19, 0.4)",
"leave": "rgba(0, 0, 0, 0)"
}
},
{
"name": "On(편집) scale: #0F0D0A, border, unit #FFF, focused input with red caret + outline",
"status": "PASS",
"data": {
"x": 97,
"y": 531.5,
"w": 200,
"h": 28,
"backgroundColor": "rgb(15, 13, 10)",
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"unit": "rgb(255, 255, 255)",
"focused": true,
"caret": "rgb(230, 4, 3)",
"shadow": true
}
},
{
"name": "On(편집) thickness: #0F0D0A, border, unit #FFF, focused input with red caret + outline",
"status": "PASS",
"data": {
"x": 97,
"y": 627.5,
"w": 200,
"h": 28,
"backgroundColor": "rgb(15, 13, 10)",
"borderTopColor": "rgba(33, 29, 19, 0.4)",
"unit": "rgb(255, 255, 255)",
"focused": true,
"caret": "rgb(230, 4, 3)",
"shadow": true
}
},
{
"name": "Enter: 선축적 2 → validated, 4 decimals committed",
"status": "PASS",
"data": "property(scale, 2.0000)"
},
{
"name": "Escape: draft cancelled (2.0000 restored, no callback)",
"status": "PASS",
"data": "2.0000"
},
{
"name": "On(편집) 선가중치 compact: #0F0D0A",
"status": "PASS",
"data": {
"x": 97,
"y": 499.5,
"w": 43.03,
"h": 28,
"backgroundColor": "rgb(15, 13, 10)"
}
},
{
"name": "Invalid 선가중치 -1 (min 0): restored, no callback",
"status": "PASS",
"data": "0.25"
},
{
"name": "선가중치 0.5 committed",
"status": "PASS",
"data": "property(scale, 2.0000) → property(weight, 0.5)"
},
{
"name": "하이퍼링크 focus: no On surface (Figma has no On), editable",
"status": "PASS",
"data": {
"x": 97,
"y": 595.5,
"w": 200,
"h": 28,
"backgroundColor": "rgba(0, 0, 0, 0)",
"borderTopColor": "rgba(33, 29, 19, 0.4)"
}
},
{
"name": "하이퍼링크 commit via host callback",
"status": "PASS",
"data": "property(scale, 2.0000) → property(weight, 0.5) → property(link, http://a)"
},
{
"name": "재료: read-only, no On",
"status": "PASS",
"data": {
"x": 97,
"y": 659.5,
"w": 200,
"h": 28,
"backgroundColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "Layer On: #211D13 field, 234 wide list, right edges meet, starts at field bottom",
"status": "PASS",
"data": {
"f": {
"x": 97,
"y": 403.5,
"w": 200,
"h": 28,
"backgroundColor": "rgb(33, 29, 19)",
"borderTopColor": "rgba(33, 29, 19, 0.4)"
},
"d": {
"x": 63,
"y": 431.5,
"w": 234,
"h": 250
}
}
},
{
"name": "Layer list rows 30px, footer 40px, height = rows*30+40",
"status": "PASS",
"data": {
"rows": 7,
"row": 30,
"footer": 40,
"h": 250
}
},
{
"name": "Layer current row marked (0_Layer_Cludmark)",
"status": "PASS",
"data": null
},
{
"name": "Bulb toggle: callback only, list stays open, row not selected",
"status": "PASS",
"data": "property(scale, 2.0000) → property(weight, 0.5) → property(link, http://a) → layer.visible(Sample-Wall, true)"
},
{
"name": "Row select: property(layer, CC-D GNL), closes, field shows value",
"status": "PASS",
"data": "property(scale, 2.0000) → property(weight, 0.5) → property(link, http://a) → layer.visible(Sample-Wall, true) → property(layer, CC-D GNL)"
},
{
"name": "Close keeps the value and focus returns to the field",
"status": "PASS",
"data": null
},
{
"name": "다양함: text 다양함, no current row in list",
"status": "PASS",
"data": null
},
{
"name": "Escape closes the layer list; panel stays",
"status": "PASS",
"data": null
},
{
"name": "Host data: 다양함 → 단일 값 (0_Layer_Cludmark)",
"status": "PASS",
"data": "0_Layer_Cludmark"
},
{
"name": "External value change shown (CC-D GNL)",
"status": "PASS",
"data": null
},
{
"name": "Keyboard: ArrowDown opens + focuses list, Escape closes to trigger",
"status": "PASS",
"data": null
},
{
"name": "Outside click closes",
"status": "PASS",
"data": null
},
{
"name": "Line style On: #211D13, (ByL) #D4CBBD, 3 columns, right edges meet (or shifted to the 4px screen edge when no room)",
"status": "PASS",
"data": {
"f": {
"x": 97,
"y": 485.5,
"w": 200,
"h": 28,
"backgroundColor": "rgb(33, 29, 19)"
},
"d": {
"x": 4,
"y": 513.5,
"w": 471.19,
"h": 190
},
"cols": [
187.19,
142,
142
]
}
},
{
"name": "Line style drop 466 x 190 (type column grows only for wider font names)",
"status": "PASS",
"data": {
"w": 471.19,
"h": 190,
"cols": [
187.19,
142,
142
]
}
},
{
"name": "Weight column: callback only, stays open",
"status": "PASS",
"data": "property(scale, 2.0000) → property(weight, 0.5) → property(link, http://a) → layer.visible(Sample-Wall, true) → property(layer, CC-D GNL) → lineWeight(w-byblock)"
},
{
"name": "Line type select: property(line, CENTER), closes, preview = CENTER pattern (4 segments)",
"status": "PASS",
"data": {
"segs": 4
}
},
{
"name": "Figma sample 255,259,0 shown as host text, swatch from host",
"status": "PASS",
"data": null
},
{
"name": "Color On: dropdown 192 wide, left edges meet, RGB area #211D13 + divider",
"status": "PASS",
"data": {
"f": {
"x": 97,
"y": 435.5,
"w": 200,
"h": 28
},
"d": {
"x": 97,
"y": 463.5,
"w": 192,
"h": 158
},
"left": {
"x": 98,
"y": 436.5,
"w": 114,
"h": 26,
"backgroundColor": "rgb(33, 29, 19)",
"borderRightColor": "rgba(33, 29, 19, 0.4)"
}
}
},
{
"name": "Color dropdown height 158 (Figma)",
"status": "PASS",
"data": 158
},
{
"name": "ByBlock: callback, required single choice (ByBlock pressed, ByLayer not)",
"status": "PASS",
"data": null
},
{
"name": "Clicking the pressed mode is a no-op",
"status": "PASS",
"data": null
},
{
"name": "Chip: property(color, 241,4,4)",
"status": "PASS",
"data": "property(scale, 2.0000) → property(weight, 0.5) → property(link, http://a) → layer.visible(Sample-Wall, true) → property(layer, CC-D GNL) → lineWeight(w-byblock) → property(line, CENTER) → colorMode(byBlock) → property(color, 241,4,4)"
},
{
"name": "Out-of-range 255,259,0 rejected (no callback, restored)",
"status": "PASS",
"data": "241,4,4"
},
{
"name": "Valid RGB committed normalized (1,2,3)",
"status": "PASS",
"data": "property(scale, 2.0000) → property(weight, 0.5) → property(link, http://a) → layer.visible(Sample-Wall, true) → property(layer, CC-D GNL) → lineWeight(w-byblock) → property(line, CENTER) → colorMode(byBlock) → property(color, 241,4,4) → property(color, 1,2,3)"
},
{
"name": "btn_colorize: host callback, dropdown closes",
"status": "PASS",
"data": null
},
{
"name": "Field RGB 300,0,0 rejected",
"status": "PASS",
"data": "1,2,3"
},
{
"name": "Escape #1 cancels draft, dropdown still open",
"status": "PASS",
"data": null
},
{
"name": "Escape #2 closes the dropdown, panel stays",
"status": "PASS",
"data": null
},
{
"name": "ReadOnly 두께: no On; ReadOnly 레이어: no dropdown",
"status": "PASS",
"data": {
"x": 97,
"y": 647,
"w": 200,
"h": 28,
"backgroundColor": "rgba(0, 0, 0, 0)"
}
},
{
"name": "ReadOnly keeps Hover border",
"status": "PASS",
"data": null
},
{
"name": "Disabled 색상: no dropdown; Disabled 선축적: no hover border",
"status": "PASS",
"data": null
},
{
"name": "External value / unit props: 선축적 2.5000, 두께 cm",
"status": "PASS",
"data": null
},
{
"name": "Dynamic rows: added at the end, removed row gone, order kept",
"status": "PASS",
"data": [
"layer-mixed",
"layer",
"color",
"line",
"weight",
"scale",
"opacity",
"thickness",
"material",
"extra-10"
]
},
{
"name": "Without menus: line types from options only (1 column), no crash",
"status": "PASS",
"data": 1
},
{
"name": "Pinned Hover specimen uses the same border",
"status": "PASS",
"data": "rgba(33, 29, 19, 0.4)"
},
{
"name": "4 panel states: 314 x 256 / 361 / 658 / 754",
"status": "PASS",
"data": [
[
"none",
314,
256
],
[
"all",
314,
361
],
[
"circle",
314,
658
],
[
"expanded",
314,
754
]
]
},
{
"name": "Header-hosted panel: layer list attached; Escape closes list only",
"status": "PASS",
"data": {
"hosted": {
"right": 0,
"top": 0,
"panelOpen": true
},
"stillOpen": 1
}
},
{
"name": "no console / page errors",
"status": "PASS",
"data": []
}
],
"errors": []
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,118 @@
# SW Storybook — 속성창 4개 상태 비교 통합 (2026-10-19)
- 브랜치: `feature/sw-package` · 시작 HEAD `c705a1fc59fe8ecbce663bf4601638fde43553a1` · 시작 시 Working Tree Clean
- 범위: Storybook 전용 코드(`src/sw/sw-header.stories.tsx`, `src/sw/sw-stories.css`)와 검증 스크립트. SW 패키지 런타임·공개 API·AI 템플릿·Overview·Dropdown Story는 변경하지 않음.
- 증거: `docs/sw/property-panel-comparison-qa/` (기존 QA 폴더는 덮어쓰지 않음)
## 1. 수정 전 속성창 관련 Story 목록 (실제 선언 기준)
| Story ID | 이름 | 실제 기능·목적 | 분류 |
| --- | --- | --- | --- |
| `sw-header--property-panel-compare` | Property Panel · 4개 비교 | 같은 `SWHeaderPropertyPanel` 4개 상태를 한 화면에 실측 크기와 함께 표시 | **유지** |
| `sw-header--property-panel-variants` | Property Panel Variants | `PropertyPanelDemo standalone`: 상태 전환 버튼으로 **한 번에 1개** 단독 패널. 데이터 조작 버튼은 `Property Panel`과 동일 | **삭제** (중복) |
| `sw-header--property-panel-none` | Property Panel · 선택 없음 | 314×256 단독 패널 1개 | **삭제** (비교 화면과 중복) |
| `sw-header--property-panel-all` | Property Panel · 전체 선택 | 314×361 단독 패널 1개 | **삭제** (중복) |
| `sw-header--property-panel-circle` | Property Panel · 원 1개 | 314×658 단독 패널 1개 | **삭제** (중복) |
| `sw-header--property-panel-expanded` | Property Panel · 모두 펼침 | 314×754 단독 패널 1개 | **삭제** (중복) |
| `sw-header--property-panel` | Property Panel | Header 연결 열기/닫기, Controlled/Uncontrolled, 외부 Open, 폭 전환, 속성 추가·삭제·순서·제한 상태 | **보존** (Header 연동·API 검증) |
| `sw-header--property-shape-slot` | Property Panel · 형상 Slot | `shapeContent` 확장 계약(기본/null/ReactNode/Render Prop) | **보존** (API 검증) |
| `sw-header--property-fields` | Property Panel · 필드 13개 | 필드 Default/Hover 표본 + 실제 조작 | **보존** (필드 검증) |
| `sw-header--property-bar` | Property Bar | 속성Bar 독립 기능 | **보존** |
| `sw-header--layer-detail-input` | Layer Detail · Input | Input 독립 검증 | **보존** |
| `sw-header--interactive-states` | Interactive States | Header 공통 Hover/Selected(속성창 토글 포함) | **보존** |
| `sw-header--detail-buttons` | Detail Buttons (Shared States) | 공통 상세 버튼 상태 | **보존** |
삭제한 5개는 모두 비교 화면과 같은 `PropertyState`/`SWHeaderPropertyPanel` + 같은 fixture를 렌더링했고, 고유 기능 검증은 없었습니다(Variants의 데이터 조작 버튼은 `Property Panel`에 동일하게 남아 있음).
## 2. 유지한 4개 상태 비교 Story
- ID `sw-header--property-panel-compare`, 이름 `Property Panel · 4개 비교`, 페이지 제목 `속성창 4개 상태 비교` — 모두 유지.
- 상태명·순서 유지(코드 `PROPERTY_STATES`): ① 선택된 객체 없음 ② 전체 객체 선택 ③ 원 1개 선택 ④ 모든 항목 펼침. 각 카드 상단 캡션(상태명 · Figma 노드 · 크기 · 측정값)은 패널 밖 `figcaption`이라 제품 디자인에 영향 없음.
- 변경: 줄바꿈 flex → 2열 × 2행 grid(간격 24px, 패널 원래 크기). 페이지를 뷰포트 높이로 고정하고 Inner 스크롤 영역(`[data-compare-scroll]`)만 가로·세로 스크롤. 설명 문구에서 "폭이 부족하면 줄바꿈합니다" 등 불필요한 문장 제거. 삭제된 개별 Story가 보여주던 Callback 기록을 대신해 한 줄 로그(`상태: callback`)를 추가.
- Storybook 전용 배치 예외 1건: 제품 CSS의 `.hm-sw-property { max-height: calc(100dvh - 8px) }`가 낮은 뷰포트에서 754px 패널을 줄이지 않도록 비교 grid 안에서만 `max-height: none` (Overview Stage와 같은 방식). 패키지 CSS는 미변경.
## 3·4. 삭제한 Popup 시연 / 삭제된 Story ID
`sw-header--property-panel-variants`, `sw-header--property-panel-none`, `sw-header--property-panel-all`, `sw-header--property-panel-circle`, `sw-header--property-panel-expanded`
함께 정리한 Story 전용 코드: `PropertyStateStory`(삭제된 4개 전용), `PropertyPanelDemo`의 `standalone` 분기(Variants 전용). 제품 컴포넌트·타입·CSS·SVG·형상 Slot은 삭제하지 않음.
## 5. 최종 Story 개수
| | 수정 전 | 수정 후 |
| --- | --- | --- |
| SW Story | 24 | **19** |
| 전체 저장소 Story | 343 | **338** |
남은 19개의 ID·이름·title·tags는 수정 전과 동일(자동 비교 PASS). Dropdown(`sw-header--composed-controls`, 이름 `Dropdown`, title `SW/Header`) 동일.
## 6~8. 배치·크기·스크롤 검증 (실제 좌표, Chrome, 높이 900)
| 폭 | ①(x,y) | ②(x,y) | ③(x,y) | ④(x,y) | 열/열/행 간격 | Inner client→scroll (W / H) |
| --- | --- | --- | --- | --- | --- | --- |
| 390 | 16,213 | 354,213 | 16,664 | 354,664 | 24/24/24 | 358→652 / 672→1271 |
| 768 | 23,200 | 361,200 | 23,651 | 361,651 | 24/24/24 | 722→722 / 677→1271 |
| 1280 | 38,215 | 376,215 | 38,666 | 376,666 | 24/24/24 | 1203→1203 / 647→1271 |
| 1440 | 120,217 | 458,217 | 120,668 | 458,668 | 24/24/24 | 1200→1200 / 644→1271 |
- 같은 행 y 일치, 같은 열 x 일치, 2열이 1열 오른쪽·2행이 1행 아래 → 4개 폭 모두 **2열 × 2행 PASS**.
- 패널 크기(모든 폭): 314×256 / 314×361 / 314×658 / 314×754 **PASS**.
- 문서 자체는 가로·세로 스크롤 없음(페이지 높이 무제한 확장 없음). Inner 끝까지 스크롤 시 754px 패널 하단·우측이 스크롤 영역 안에 완전히 보임. 390px은 Inner 가로 스크롤로 2열 유지(1열 전환·축소 없음) **PASS**.
## 9. Dropdown / Input / Focus / 닫기
- Input: 원 1개 패널 반지름 Enter 확정 → Callback 기록, 모두 펼침 패널 값 불변(상태 독립). Escape 편집 취소, Focus 패널 내부 유지, Tab 이동 정상.
- Dropdown(레이어): 1행 패널·2행 패널 모두 열림·항목 표시·뷰포트 내부. 열린 채 Inner를 120px 스크롤해도 필드에 붙어 이동(fixed overlay가 분리되지 않음). Escape 닫힘 + 필드로 Focus 복귀. 항목 선택 시 해당 패널에만 Callback.
- 도구 토글 `aria-pressed` 패널별 독립, 그룹 펼침 후 접으면 256px 복귀(다른 패널 361px 유지), 닫기 버튼 → 해당 패널 `onClose` Callback.
## 10. 기존 Header / Overview 보존
- Overview: Header 49px·Stage 렌더 PASS. Dropdown Story: composed controls 렌더 PASS, 기존 기능 스크립트(`dropdown-consolidation.browser.mjs`) 223개 기능 체크 PASS.
- Window Move Motion·K Guide·Property Bar·Layer Detail · Input·형상 Slot·필드 13개 렌더 PASS. `Property Panel`(Header 연결) 열기/Escape 닫기 PASS.
## 11. 제품 런타임·API 미변경
`git diff HEAD -- packages/sw/src packages/sw/ai packages/sw/package.json packages/ui packages/org-chart packages/org-chart-react docs/sw/ai-template src/sw/sw-header-overview.tsx src/sw/sw-header-dropdown.stories.tsx .storybook` → 변경 없음.
## 12. 변경 파일
- `src/sw/sw-header.stories.tsx` — 5개 Story·전용 헬퍼 삭제, 비교 화면 grid·스크롤·로그
- `src/sw/sw-stories.css` — 비교 페이지 레이아웃(Story 전용)
- `packages/sw/tests/storybook-inventory.mjs` — `retiredPropertyPanelIds` 추가
- `packages/sw/tests/followup-four.browser.mjs`, `packages/sw/tests/detail-buttons.browser.mjs` — 삭제된 ID 참조를 비교 Story의 `[data-compare]` 범위로 교체, 기대 개수 24→19
- `packages/sw/tests/property-panel-comparison.browser.mjs` — 신규 검증 스크립트(`before` / `verify`)
- `docs/sw/property-panel-comparison-qa/` — 수정 전후 증거
- 과거 검토·검증 문서(`sw-first-pass-cleanup-review`, `sw-storybook-screen-review` 등)의 옛 ID는 당시 기록으로 보존.
## 13. Typecheck / Build
| 항목 | 결과 |
| --- | --- |
| `npm run typecheck:sw` | PASS |
| `npm run build:sw` | PASS |
| Storybook Typecheck `npx tsc -b` | PASS |
| `npm run build-storybook` | PASS |
| `git diff --check` | PASS |
## 14. 브라우저 검증 (Chrome headless, 새 static build)
| 스크립트 | 결과 | 증거 |
| --- | --- | --- |
| `property-panel-comparison.browser.mjs verify` | **59/59 PASS**, 콘솔 오류 0 | `verify/` |
| `followup-four.browser.mjs` (비교 Story로 이전) | 39/39 PASS | `legacy-followup-four/` |
| `detail-buttons.browser.mjs preservation` | 15/15 PASS | `legacy-detail-buttons-preservation/` |
| `property-fields.browser.mjs` | 77/77 PASS | `legacy-property-fields/` |
| `property-close.browser.mjs` | 6/6 PASS | `legacy-property-close/` |
| `dropdown-consolidation.browser.mjs` | 223 PASS / 4 FAIL (아래) | `legacy-dropdown/` |
`dropdown-consolidation`의 4개 FAIL은 이전 단계 기준선에 고정된 단언입니다: 28→24 / 24 개수 2건, "삭제 외 선언 동일" 1건(이번에 선언 삭제), "`src/sw/sw-stories.css` 미변경" 1건(이번에 Story CSS 수정). 이번 작업의 의도된 변경이며 Dropdown 기능 체크는 모두 PASS입니다. 이 스크립트는 당시 단계 기록이라 수정하지 않았습니다.
수정 전 스크린샷: `before/before-compare-1440.png`(4개가 한 줄), `before/before-property-panel-*.png`. 수정 후: `verify/compare-<폭>-top.png` / `-end.png`, `dropdown-*.png`.
## 15. NOT VERIFIED
- Firefox·Safari 등 Chrome 외 브라우저(이번 작업에서 실행하지 않음).
- 개발 서버(6024) 화면 — 다른 worktree의 서버일 수 있어 사용하지 않고 static build로만 검증.
- 실제 Storybook Manager UI에서의 사이드바 표시(index.json으로 목록만 검증).
- `overview.browser.mjs`, `overview-firefox.browser.mjs`, `storybook-cleanup-stage1.browser.mjs`, `ai-template-*` 스크립트는 재실행하지 않음(Overview는 신규 스크립트에서 렌더·Header 49px만 확인).
- 스크린리더 실사용.
+7 -5
View File
@@ -2,7 +2,7 @@ import { createRequire } from 'node:module'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import http from 'node:http'
import { swStoryEntries, retainedInventory } from './storybook-inventory.mjs'
import { swStoryEntries, retainedInventory, retiredPropertyPanelIds } from './storybook-inventory.mjs'
// Usage: node packages/sw/tests/detail-buttons.browser.mjs baseline|verify <evidence-dir> [base-url]
const mode = process.argv[2] ?? 'verify'
@@ -50,9 +50,9 @@ try {
check(`required responsive width ${width}`,layout.height===49&&layout.overflow===0&&layout.documentOverflow===0,layout)
}
await page.setViewportSize({width:1920,height:1000})
await goto('property-panel-compare')
for(const [id,height] of [['none',256],['all',361],['circle',658],['expanded',754]]){
await goto(`property-panel-${id}`)
const box=await page.locator('.hm-sw-property').first().boundingBox()
const box=await page.locator(`[data-compare="${id}"] .hm-sw-property`).boundingBox()
check(`preserve property panel ${id}`,box.width===314&&box.height===height,box)
}
await goto('composition')
@@ -100,8 +100,10 @@ try {
for(const key of Object.keys(snapshots).filter(k=>!k.startsWith('text'))) check(`unchanged header ${key} (capture a current baseline for replacement fixtures)`,JSON.stringify(before[key])===JSON.stringify(snapshots[key]))
const oldIds=JSON.parse(await readFile(path.join(output,'baseline-story-ids.json'),'utf8'))
const retiredIds=['default','controls','responsive','text-stroke','dynamic-data','layer-dropdown','color-dropdown','line-style-dropdown','text-dimension-style-dropdown'].map(id=>`sw-header--${id}`)
const retainedIds=retainedInventory(oldIds.map(id=>({id}))).map(e=>e.id)
check('retained stories remain; only approved retirements are absent',ids.length===24&&retainedIds.every(id=>ids.includes(id))&&retiredIds.every(id=>!ids.includes(id)),{before:oldIds.length,after:ids.length})
// 2026-10-19: the five duplicate property panel demos are retired as well (24 -> 19).
retiredIds.push(...retiredPropertyPanelIds)
const retainedIds=retainedInventory(oldIds.map(id=>({id}))).map(e=>e.id).filter(id=>!retiredPropertyPanelIds.includes(id))
check('retained stories remain; only approved retirements are absent',ids.length===19&&retainedIds.every(id=>ids.includes(id))&&retiredIds.every(id=>!ids.includes(id)),{before:oldIds.length,after:ids.length})
for(const id of ids){await goto(id.replace('sw-header--',''));check(`render ${id}`,!(await page.locator('.sb-errordisplay').isVisible()))}
await goto('composition')
const presets=['기본 전체','구성 A','구성 B (레이어·선스타일)','구성 C (로고만)','파일 목록 제거','레이어 제거','색상 제거','스타일 그룹 제거','여러 메뉴 제거','순서 변경','숨김 + 순서 변경']
+13 -11
View File
@@ -72,8 +72,9 @@ for (const w of [1440, 1080, 768]) {
}
// ---- B. Tool icon
await goto('property-panel-all')
const tool = page.locator('.hm-sw-property__tool').first()
// The four states share one Story since 2026-10-19; each check is scoped to its panel.
await goto('property-panel-compare')
const tool = page.locator('[data-compare="all"] .hm-sw-property__tool')
const toolInfo = await tool.evaluate(e => { const s = getComputedStyle(e), imgs = [...e.querySelectorAll('img')].map(i => ({ cls: i.className, w: i.getBoundingClientRect().width, h: i.getBoundingClientRect().height, src: i.getAttribute('src').slice(0, 60) })); return { border: s.borderTopWidth + ' ' + s.borderTopColor, outline: s.outlineStyle, bg: s.backgroundColor, w: e.getBoundingClientRect().width, h: e.getBoundingClientRect().height, imgs } })
report.data.tool = toolInfo
check('B1 no CSS border/outline/background on tool button', toolInfo.border.startsWith('0px') && toolInfo.outline === 'none' && toolInfo.bg === 'rgba(0, 0, 0, 0)', toolInfo)
@@ -147,8 +148,8 @@ for (const menu of ['layer', 'color', 'text-style', 'dimension-style', 'line-sty
}
// ---- E. Gauge
await goto('property-panel-circle')
const range = page.locator('.hm-sw-property input[type="range"]').first()
await goto('property-panel-compare')
const range = page.locator('[data-compare="circle"] .hm-sw-property input[type="range"]')
const gaugeState = () => range.evaluate(e => ({ value: e.value, fill: e.style.getPropertyValue('--hm-sw-range-fill'), track: getComputedStyle(e).getPropertyValue('--hm-sw-property-range-track').trim() }))
const gaugePixels = async name => {
const b = await range.boundingBox()
@@ -166,8 +167,8 @@ await range.focus(); for (let n = 0; n < 40; n++) await page.keyboard.press('Arr
gs = await gaugePixels('keyboard'); report.data.gaugeKeyboard = gs
check('E2 keyboard ArrowRight x40 -> 0.65, fill follows', gs.value === '0.65' && Math.abs(Number(gs.fill) - 0.65) < 1e-9 && isRed(gs.left) && isBeige(gs.right), gs)
await page.keyboard.press('Enter')
await page.waitForFunction(() => document.querySelector('.hm-sw-property [data-property-id="opacity"] .hm-sw-header__input')?.value === '0.65', null, { timeout: 2000 }).catch(() => {})
const committed = await page.locator('.hm-sw-property [data-property-id="opacity"] .hm-sw-header__input').first().inputValue().catch(() => null)
await page.waitForFunction(() => document.querySelector('[data-compare="circle"] .hm-sw-property [data-property-id="opacity"] .hm-sw-header__input')?.value === '0.65', null, { timeout: 2000 }).catch(() => {})
const committed = await page.locator('[data-compare="circle"] [data-property-id="opacity"] .hm-sw-header__input').inputValue().catch(() => null)
report.data.gaugeCommitted = committed
check('E3 commit updates the number input (callback path)', committed === '0.65', { committed })
await page.keyboard.press('End'); gs = await gaugePixels('max'); report.data.gaugeMax = gs
@@ -175,18 +176,19 @@ check('E4 max -> fill 1', gs.value === '1' && Number(gs.fill) === 1 && isRed(gs.
await page.keyboard.press('Home'); gs = await gaugePixels('min'); report.data.gaugeMin = gs
check('E5 min -> fill 0, beige right', gs.value === '0' && Number(gs.fill) === 0 && isBeige(gs.right), gs)
// External value: type into the numeric input and commit
const num = page.locator('.hm-sw-property [data-property-id="opacity"] .hm-sw-header__input').first()
const num = page.locator('[data-compare="circle"] [data-property-id="opacity"] .hm-sw-header__input')
await num.click(); await num.fill('0.5'); await num.press('Enter'); await page.mouse.move(1, 1)
gs = await gaugeState(); report.data.gaugeExternal = gs
check('E6 value changed elsewhere syncs gauge', gs.value === '0.5' && Number(gs.fill) === 0.5, gs)
await page.locator('.hm-sw-property').first().screenshot({ path: path.join(out, 'E-panel-circle.png') })
await page.locator('[data-compare="circle"] .hm-sw-property').screenshot({ path: path.join(out, 'E-panel-circle.png') })
// ---- F. Panel four states render
await goto('property-panel-compare')
for (const id of ['property-panel-none', 'property-panel-all', 'property-panel-circle', 'property-panel-expanded']) {
await goto(id)
const sz = await page.locator('.hm-sw-property').first().evaluate(e => { const r = e.getBoundingClientRect(); return [r.width, r.height] })
const panel = page.locator(`[data-compare="${id.replace('property-panel-', '')}"] .hm-sw-property`)
const sz = await panel.evaluate(e => { const r = e.getBoundingClientRect(); return [r.width, r.height] })
report.data[id] = sz
await page.locator('.hm-sw-property').first().screenshot({ path: path.join(out, `F-${id}.png`) })
await panel.screenshot({ path: path.join(out, `F-${id}.png`) })
check(`F ${id} renders`, sz[0] === 314 && sz[1] > 200, sz)
}
check('G no page/console errors', report.errors.length === 0, report.errors)
@@ -0,0 +1,196 @@
import { createRequire } from 'node:module'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import http from 'node:http'
import { swStoryEntries, retiredPropertyPanelIds } from './storybook-inventory.mjs'
// Property panel comparison consolidation (2026-10-19).
// Usage: node packages/sw/tests/property-panel-comparison.browser.mjs before|verify <evidence-dir> [base-url|static]
const mode = process.argv[2] ?? 'verify'
const out = path.resolve(process.argv[3] ?? 'docs/sw/property-panel-comparison-qa/' + mode)
let base = process.argv[4] ?? 'static'
let server
if (base === 'static') {
const root = path.resolve('storybook-static'), mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.gif': 'image/gif', '.ttf': 'font/ttf', '.woff2': 'font/woff2' }
server = http.createServer(async (req, res) => { try { const p = new URL(req.url, 'http://localhost').pathname, file = path.resolve(root, '.' + decodeURIComponent(p === '/' ? '/index.html' : p)); if (path.relative(root, file).startsWith('..')) return res.writeHead(403).end(); res.setHeader('Content-Type', mime[path.extname(file)] ?? 'application/octet-stream'); res.end(await readFile(file)) } catch { res.writeHead(404).end() } })
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); base = `http://127.0.0.1:${server.address().port}`
}
const { chromium } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
await mkdir(out, { recursive: true })
const browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
const report = { mode, base: process.argv[4] ?? 'static', browser: browser.version(), checks: [], errors: [], data: {} }
page.on('pageerror', e => report.errors.push(e.message))
page.on('console', m => { if (m.type() === 'error') report.errors.push(m.text()) })
const check = (name, ok, data) => { report.checks.push({ name, status: ok ? 'PASS' : 'FAIL', data }); if (!ok) console.error('FAIL', name, JSON.stringify(data)) }
const goto = async id => {
await page.goto(`${base}/iframe.html?id=sw-header--${id}&viewMode=story`)
await page.locator('.hm-sw-header, .hm-sw-theme').first().waitFor()
await page.evaluate(() => document.fonts.ready)
await page.mouse.move(1, 1)
}
const STATES = [['none', 256], ['all', 361], ['circle', 658], ['expanded', 754]]
const geometry = () => page.evaluate(() => {
const r = e => { const b = e.getBoundingClientRect(); return { x: b.x, y: b.y, w: b.width, h: b.height, right: b.right, bottom: b.bottom } }
const scroller = document.querySelector('[data-compare-scroll]')
return {
cells: [...document.querySelectorAll('[data-compare]')].map(c => ({ id: c.dataset.compare, cell: r(c), panel: r(c.querySelector('.hm-sw-property')), caption: c.querySelector('figcaption strong')?.textContent })),
scroller: scroller && { ...r(scroller), scrollW: scroller.scrollWidth, clientW: scroller.clientWidth, scrollH: scroller.scrollHeight, clientH: scroller.clientHeight },
doc: { scrollW: document.documentElement.scrollWidth, clientW: document.documentElement.clientWidth, scrollH: document.documentElement.scrollHeight, clientH: document.documentElement.clientHeight },
}
})
try {
const index = await (await fetch(`${base}/index.json`)).json()
const stories = Object.values(index.entries).filter(e => e.type === 'story')
const sw = swStoryEntries(index)
report.data.inventory = { total: stories.length, sw: sw.length, ids: sw.map(e => [e.id, e.name]) }
if (mode === 'before') {
await writeFile(path.join(out, 'story-inventory-before.json'), JSON.stringify({ total: stories.length, sw: sw.map(e => ({ id: e.id, name: e.name, title: e.title, tags: e.tags })) }, null, 2) + '\n')
await goto('property-panel-compare')
report.data.compare = await geometry()
await page.screenshot({ path: path.join(out, 'before-compare-1440.png'), fullPage: true })
for (const id of retiredPropertyPanelIds) {
await goto(id.replace('sw-header--', ''))
await page.screenshot({ path: path.join(out, `before-${id.replace('sw-header--', '')}.png`), fullPage: true })
}
} else {
// ---- Inventory
const before = JSON.parse(await readFile(path.resolve(process.env.SW_BEFORE_INVENTORY ?? 'docs/sw/property-panel-comparison-qa/before/story-inventory-before.json'), 'utf8'))
const expected = before.sw.filter(e => !retiredPropertyPanelIds.includes(e.id))
const signature = list => JSON.stringify(list.map(e => [e.id, e.name, e.title, e.tags]).sort((a, b) => a[0].localeCompare(b[0])))
check('inventory: 24 -> 19 SW Stories, 343 -> 338 total, only 5 approved retirements', before.sw.length === 24 && sw.length === 19 && before.total === 343 && stories.length === 338 && retiredPropertyPanelIds.every(id => !index.entries[id]), { before: [before.total, before.sw.length], after: [stories.length, sw.length] })
check('inventory: retained SW Story IDs, names, titles, tags unchanged', signature(expected) === signature(sw), { retained: sw.length })
check('inventory: compare Story keeps its ID and name', index.entries['sw-header--property-panel-compare']?.name === 'Property Panel · 4개 비교')
check('inventory: Dropdown Story keeps ID / name / title', index.entries['sw-header--composed-controls']?.name === 'Dropdown' && index.entries['sw-header--composed-controls']?.title === 'SW/Header')
// ---- Layout at four widths
for (const width of [390, 768, 1280, 1440]) {
await page.setViewportSize({ width, height: 900 })
await goto('property-panel-compare')
const g = await geometry(); report.data[`layout${width}`] = g
const [a, b, c, d] = g.cells
const sizes = g.cells.map(cell => [cell.id, cell.panel.w, cell.panel.h])
check(`${width}: four states in original order with captions`, JSON.stringify(g.cells.map(cell => cell.id)) === JSON.stringify(STATES.map(s => s[0])) && g.cells.every(cell => cell.caption), g.cells.map(cell => [cell.id, cell.caption]))
check(`${width}: original panel sizes 314 × 256 / 361 / 658 / 754`, STATES.every(([id, h], i) => sizes[i][0] === id && sizes[i][1] === 314 && sizes[i][2] === h), sizes)
const colGap = b.cell.x - a.cell.right, colGap2 = d.cell.x - c.cell.right, rowGap = c.cell.y - Math.max(a.cell.bottom, b.cell.bottom)
report.data[`gaps${width}`] = { colGap, colGap2, rowGap }
check(`${width}: 2 columns × 2 rows by coordinates`, Math.abs(a.cell.y - b.cell.y) < 0.5 && Math.abs(c.cell.y - d.cell.y) < 0.5 && Math.abs(a.cell.x - c.cell.x) < 0.5 && Math.abs(b.cell.x - d.cell.x) < 0.5 && b.cell.x >= a.cell.right && c.cell.y >= Math.max(a.cell.bottom, b.cell.bottom), { a: a.cell, b: b.cell, c: c.cell, d: d.cell })
check(`${width}: consistent 24px column and row gaps`, [colGap, colGap2, rowGap].every(v => Math.abs(v - 24) < 0.5), { colGap, colGap2, rowGap })
check(`${width}: page itself does not scroll (no document overflow)`, g.doc.scrollW <= g.doc.clientW && g.doc.scrollH <= g.doc.clientH, g.doc)
check(`${width}: Inner scroller scrolls vertically to the 754px panel`, g.scroller.scrollH > g.scroller.clientH, g.scroller)
await page.screenshot({ path: path.join(out, `compare-${width}-top.png`) })
// Scroll to the very end: every panel must be reachable with its bottom inside the scroller.
const end = await page.evaluate(() => {
const s = document.querySelector('[data-compare-scroll]'); s.scrollTop = s.scrollHeight; s.scrollLeft = s.scrollWidth
const sr = s.getBoundingClientRect()
return { scrollTop: s.scrollTop, scrollLeft: s.scrollLeft, panels: [...s.querySelectorAll('.hm-sw-property')].map(p => { const r = p.getBoundingClientRect(); return { bottom: r.bottom, right: r.right } }), sBottom: sr.bottom, sRight: sr.right }
})
report.data[`end${width}`] = end
const last = end.panels[3], right = end.panels[1]
check(`${width}: bottom of the 754px panel reachable, not clipped`, last.bottom <= end.sBottom + 0.5 && last.right <= end.sRight + 0.5, end)
if (g.scroller.scrollW > g.scroller.clientW) check(`${width}: narrow Inner scrolls horizontally, keeping 2 columns`, end.scrollLeft > 0 && right.right <= end.sRight + 0.5, { scrollW: g.scroller.scrollW, clientW: g.scroller.clientW, end })
else check(`${width}: both columns fit without horizontal scroll`, g.scroller.scrollW <= g.scroller.clientW && b.cell.right <= g.scroller.right, g.scroller)
await page.screenshot({ path: path.join(out, `compare-${width}-end.png`) })
}
// ---- Behaviour (1440 × 900)
await page.setViewportSize({ width: 1440, height: 900 })
await goto('property-panel-compare')
const cell = id => page.locator(`[data-compare="${id}"]`)
const log = () => page.locator('[data-property-log]').textContent()
const input = (id, prop) => cell(id).locator(`[data-property-id="${prop}"] input.hm-sw-header__input`).first()
// Input: Enter commits through onPropertyChange; Escape restores; other panels unaffected.
const radius = input('circle', 'radius')
await radius.scrollIntoViewIfNeeded(); await radius.click(); await radius.fill('5.5'); await radius.press('Enter')
const committed = { circle: await radius.inputValue(), expanded: await input('expanded', 'radius').inputValue(), log: await log() }
check('Input Enter commits via callback; only that panel changes', committed.circle.startsWith('5.5') && committed.expanded === '4.0526' && committed.log.includes('circle: property(radius, 5.5'), committed)
await radius.click(); await radius.fill('9'); await radius.press('Escape')
check('Input Escape cancels the edit', (await radius.inputValue()).startsWith('5.5'), await radius.inputValue())
const focusOk = await radius.evaluate(e => document.activeElement === e || !!e.closest('.hm-sw-property')?.contains(document.activeElement))
check('Focus stays inside the edited panel after Escape', focusOk)
await page.keyboard.press('Tab')
check('Tab moves focus within the page (no trap / loss to body)', await page.evaluate(() => document.activeElement !== document.body))
// Dropdown (layer picker) in a top panel and in a bottom-row panel after scrolling.
const openPicker = async id => {
const trigger = cell(id).locator('[data-property-id="layer"] button[aria-haspopup], [data-property-id="layer"] [aria-expanded]').first()
await trigger.scrollIntoViewIfNeeded(); await trigger.click()
const drop = page.locator('.hm-sw-property__drop').first(); await drop.waitFor({ timeout: 2000 })
const info = await drop.evaluate(e => { const r = e.getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height, bottom: r.bottom, right: r.right, vw: innerWidth, vh: innerHeight, items: e.querySelectorAll('button, [role="option"], li').length } })
return { trigger, drop, info }
}
for (const id of ['all', 'expanded']) {
const { trigger, drop, info } = await openPicker(id)
report.data[`drop-${id}`] = info
check(`Dropdown ${id}: opens, has items, fully inside the viewport`, info.items > 0 && info.x >= 0 && info.y >= 0 && info.right <= info.vw + 0.5 && info.bottom <= info.vh + 0.5, info)
await page.screenshot({ path: path.join(out, `dropdown-${id}.png`) })
if (id === 'expanded') {
// Scroll the Inner while open: the fixed overlay must stay attached to its field (below or flipped above).
const moved = await page.locator('[data-compare-scroll]').evaluate(s => { const t = s.scrollTop; s.scrollTop += 120; return s.scrollTop - t })
await page.waitForTimeout(150)
const after = await page.evaluate(() => {
const d = document.querySelector('.hm-sw-property__drop')?.getBoundingClientRect()
const f = document.querySelector('[data-compare="expanded"] [data-property-id="layer"] [aria-expanded="true"]')?.getBoundingClientRect()
return d && f ? { dTop: d.top, dBottom: d.bottom, fTop: f.top, fBottom: f.bottom } : null
})
report.data.dropScroll = { moved, before: info, after }
check('Dropdown follows the Inner scroller (stays attached to its field)', moved > 0 && after && (Math.abs(after.dTop - after.fBottom) < 1.5 || Math.abs(after.dBottom - after.fTop) < 1.5), report.data.dropScroll)
}
await page.keyboard.press('Escape')
check(`Dropdown ${id}: Escape closes and returns focus to its field`, await page.locator('.hm-sw-property__drop').count() === 0 && await trigger.evaluate(e => document.activeElement === e || e.contains(document.activeElement)))
}
// Select a layer value: callback updates only that panel.
{
const { drop } = await openPicker('all')
const option = drop.getByRole('button', { name: 'CC-D GNL', exact: true }).first()
if (await option.count()) {
await option.click()
check('Dropdown selection commits via callback in its own panel only', (await log()).includes('all: property(layer, CC-D GNL)'), await log())
} else check('Dropdown selection commits via callback in its own panel only', false, 'option not found')
if (await page.locator('.hm-sw-property__drop').count()) await page.keyboard.press('Escape')
}
// Tool toggle, group expand, close button — each panel keeps its own state.
const tool = cell('none').locator('.hm-sw-property__tool')
await tool.scrollIntoViewIfNeeded(); await tool.click()
check('Tool toggle in one panel does not affect the others', await tool.getAttribute('aria-pressed') === 'true' && await cell('all').locator('.hm-sw-property__tool').getAttribute('aria-pressed') !== 'true')
await tool.click()
const expand = cell('none').locator('.hm-sw-property__expand').first()
if (await expand.count()) {
const h0 = (await cell('none').locator('.hm-sw-property').boundingBox()).height
await expand.click(); const h1 = (await cell('none').locator('.hm-sw-property').boundingBox()).height
const allH = (await cell('all').locator('.hm-sw-property').boundingBox()).height
await expand.click(); const h2 = (await cell('none').locator('.hm-sw-property').boundingBox()).height
check('Group expand is per panel and collapses back to 256', h1 > h0 && allH === 361 && h2 === 256, { h0, h1, h2, allH })
}
const close = cell('circle').getByRole('button', { name: '속성창 닫기', exact: true })
await close.scrollIntoViewIfNeeded(); await close.click()
check('Close button calls onClose of that panel', (await log()).includes('circle: close'), await log())
await page.screenshot({ path: path.join(out, 'compare-after-interaction.png') })
// ---- Preservation of other Stories
await page.setViewportSize({ width: 1920, height: 1000 })
await goto('overview')
check('Overview: Header 49px and Stage render', await page.locator('header.hm-sw-header').first().evaluate(h => h.getBoundingClientRect().height) === 49 && await page.locator('.sw-overview-stage').count() === 1)
await goto('composed-controls')
check('Dropdown Story renders composed controls', await page.locator('.sw-composed-controls').count() > 0)
await goto('window-move-motion')
check('Window Move Motion Story renders', await page.locator('[data-sw-window-move]').count() > 0)
await goto('property-panel')
const trigger = page.locator('.hm-sw-header [data-property-panel-trigger]')
await trigger.click(); const opened = await page.locator('.hm-sw-property').count()
await page.keyboard.press('Escape'); await page.keyboard.press('Escape')
check('Property Panel (Header-connected) still opens; Escape closes', opened === 1 && await page.locator('.hm-sw-property').count() === 0, { opened })
for (const id of ['property-shape-slot', 'property-fields', 'property-bar', 'layer-detail-input', 'k-guide']) {
await goto(id); check(`render ${id}`, !(await page.locator('.sb-errordisplay').isVisible()))
}
check('no browser console or page errors', report.errors.length === 0, report.errors)
}
} finally {
await writeFile(path.join(out, 'result.json'), JSON.stringify(report, null, 2) + '\n')
await browser.close(); server?.close()
}
console.log('TOTAL', report.checks.length, 'FAIL', report.checks.filter(c => c.status === 'FAIL').length)
@@ -1,6 +1,8 @@
// Current SW inventory after the approved stage-1 and Dropdown consolidations.
export const retiredStage1Ids = ['responsive', 'text-stroke', 'dynamic-data', 'default', 'controls'].map(id => `sw-header--${id}`)
export const retiredDropdownIds = ['layer-dropdown', 'color-dropdown', 'line-style-dropdown', 'text-dimension-style-dropdown'].map(id => `sw-header--${id}`)
// 2026-10-19: duplicate property panel demos; the four states live on sw-header--property-panel-compare.
export const retiredPropertyPanelIds = ['property-panel-variants', 'property-panel-none', 'property-panel-all', 'property-panel-circle', 'property-panel-expanded'].map(id => `sw-header--${id}`)
// Include the old title when reading historical evidence; live hierarchy is asserted separately.
export const swStoryEntries = index => Object.values(index.entries).filter(entry => entry.type === 'story' && (entry.title === 'SW/Header' || entry.title === 'SW/Header/Dropdown'))
export const retainedInventory = entries => entries.filter(entry => ![...retiredStage1Ids, ...retiredDropdownIds].includes(entry.id))
+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; }