style(storybook): tidy CENTER, chart, org chart and SW story pages
- CENTER plain page layout and shorter reference demo notes (spec values moved to the CENTER README); chart specimen descriptions trimmed - Org chart page names and previews; SW width rows follow the 30px frame margin (1980 / 1065 / 727px) and the SW tests match the current layout - Story inventory regenerated and WORKING_CONTEXT updated
This commit is contained in:
1 parent
bad2acfc49
commit
ed7e8790ec
40 files changed
+1488
-296
No files matched your search
@@ -6,16 +6,16 @@ This is a classification record. No Story IDs, exports or titles are renamed. HI
|
||||
|
||||
## Summary
|
||||
|
||||
- PRIMARY: 82
|
||||
- VERIFICATION: 256
|
||||
- PRIMARY: 73
|
||||
- VERIFICATION: 266
|
||||
- UTILITY: 49
|
||||
- LEGACY / DUPLICATE: 0
|
||||
- KEEP: 82
|
||||
- HIDE: 305
|
||||
- KEEP: 73
|
||||
- HIDE: 315
|
||||
- MERGE_CANDIDATE: 33
|
||||
- DELETE_CANDIDATE: 0
|
||||
- Story files: 89
|
||||
- Stories: 387
|
||||
- Story files: 90
|
||||
- Stories: 388
|
||||
- Hierarchy: Foundation → Components → Patterns → Chart → SW → 조직도 → 노선도 → Verification → Theme
|
||||
|
||||
## Full inventory
|
||||
@@ -29,7 +29,7 @@ Source: `src/center/center-facility.stories.tsx`; meta.tags: []
|
||||
- Intersection — 교차로; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Junction — 분기점; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Interchange — 나들목; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Roadside — 휴게소 · 졸음쉼터; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Roadside — 연계 노선; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- Linked — 연계 시설물; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- TunnelPlayground — 직접 테스트 (터널); tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
@@ -114,7 +114,7 @@ Source: `src/components/accordion.stories.tsx`; meta.tags: []
|
||||
|
||||
Source: `src/components/alert-dialog.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Alert Dialog; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Alert Dialog; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- ConfirmationOpen — Confirmation; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- DestructiveOpen — Destructive; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
@@ -179,7 +179,7 @@ Source: `src/components/button.stories.tsx`; meta.tags: []
|
||||
|
||||
Source: `src/components/calendar.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Calendar; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Calendar; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- SingleSelection — Single; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- DisabledSelection — Disabled Dates; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- RangeSelection — Range; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
@@ -207,7 +207,7 @@ Source: `src/components/carousel.stories.tsx`; meta.tags: []
|
||||
|
||||
Source: `src/components/checkbox.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Checkbox; tags: ["play-fn"]; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Checkbox; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Unchecked — Unchecked; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Checked — Checked; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Indeterminate — Indeterminate; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
@@ -220,7 +220,7 @@ Source: `src/components/checkbox.stories.tsx`; meta.tags: []
|
||||
|
||||
Source: `src/components/collapsible.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Collapsible; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Collapsible; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Closed — Closed; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
@@ -249,7 +249,7 @@ Source: `src/components/command.stories.tsx`; meta.tags: []
|
||||
|
||||
Source: `src/components/context-menu.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Context Menu; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Context Menu; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Open — Open; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Search & Date/Date Picker
|
||||
@@ -277,7 +277,7 @@ Source: `src/components/dialog.stories.tsx`; meta.tags: []
|
||||
|
||||
Source: `src/components/drawer.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Drawer; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Drawer; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Form — Form; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- LongContent — Long Content; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
@@ -293,7 +293,7 @@ Source: `src/components/dropdown-menu.stories.tsx`; meta.tags: []
|
||||
|
||||
Source: `src/components/hover-card.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Hover Card; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Hover Card; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Open — Open; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Input
|
||||
@@ -374,7 +374,7 @@ Source: `src/components/publisher-regression.stories.tsx`; meta.tags: ["verifica
|
||||
|
||||
Source: `src/components/radio-group.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Radio Group; tags: ["play-fn"]; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Radio Group; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Vertical — Vertical; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Horizontal — Horizontal; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
@@ -420,6 +420,12 @@ Source: `src/components/select.stories.tsx`; meta.tags: []
|
||||
- Scroll — Scroll; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Playground — Playground; tags: ["verification"]; UTILITY; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
### Components/Form/Selection Controls
|
||||
|
||||
Source: `src/components/selection-controls.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Selection Controls; tags: ["play-fn"]; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
|
||||
### Components/Data & Display/Separator
|
||||
|
||||
Source: `src/components/separator.stories.tsx`; meta.tags: []
|
||||
@@ -468,7 +474,7 @@ Source: `src/components/sonner.stories.tsx`; meta.tags: []
|
||||
|
||||
Source: `src/components/switch.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Switch; tags: ["play-fn"]; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Switch; tags: ["verification","play-fn"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Off — Off; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- On — On; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- Disabled — Disabled; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
@@ -532,7 +538,7 @@ Source: `src/components/toggle-group.stories.tsx`; meta.tags: ["internal"]
|
||||
|
||||
Source: `src/components/toggle.stories.tsx`; meta.tags: []
|
||||
|
||||
- All — Toggle; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- All — Toggle; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- States — States; tags: ["verification"]; VERIFICATION; HIDE; MERGE_CANDIDATE. Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.
|
||||
- IconOnly — Icon Only; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- DisabledToggle — Disabled; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
@@ -734,7 +740,7 @@ Source: `src/patterns/page-header.stories.tsx`; meta.tags: ["verification"]
|
||||
|
||||
Source: `src/sw/sw-footer.stories.tsx`; meta.tags: []
|
||||
|
||||
- FullFooter — SW 하단 명령어바; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- FullFooter — SW 명령어바; tags: []; PRIMARY; KEEP. Representative story or distinct reference page.
|
||||
- ToolGroups — 도구 그룹별 표시; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- FigmaVariants — Figma 디자인별 Variant (35 노드); tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
- CustomContent — 도구 및 메뉴 변경 예제; tags: ["verification"]; VERIFICATION; HIDE. Keep direct URL and tag-filter access; excluded from default sidebar.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"summary": {
|
||||
"storyFiles": 89,
|
||||
"stories": 387,
|
||||
"storyFiles": 90,
|
||||
"stories": 388,
|
||||
"categories": {
|
||||
"PRIMARY": 82,
|
||||
"VERIFICATION": 256,
|
||||
"PRIMARY": 73,
|
||||
"VERIFICATION": 266,
|
||||
"UTILITY": 49,
|
||||
"LEGACY / DUPLICATE": 0
|
||||
},
|
||||
"decisions": {
|
||||
"KEEP": 82,
|
||||
"HIDE": 305,
|
||||
"KEEP": 73,
|
||||
"HIDE": 315,
|
||||
"MERGE_CANDIDATE": 33,
|
||||
"DELETE_CANDIDATE": 0
|
||||
},
|
||||
@@ -90,7 +90,7 @@
|
||||
{
|
||||
"id": "center-graphic-facility--roadside",
|
||||
"exportName": "Roadside",
|
||||
"name": "휴게소 · 졸음쉼터",
|
||||
"name": "연계 노선",
|
||||
"storyTags": [],
|
||||
"effectiveTags": [],
|
||||
"category": "PRIMARY",
|
||||
@@ -430,12 +430,16 @@
|
||||
"id": "components-overlay-alert-dialog--all",
|
||||
"exportName": "All",
|
||||
"name": "Alert Dialog",
|
||||
"storyTags": [],
|
||||
"effectiveTags": [],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"storyTags": [
|
||||
"verification"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification"
|
||||
],
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-overlay-alert-dialog--confirmation-open",
|
||||
@@ -901,12 +905,16 @@
|
||||
"id": "components-search-date-calendar--all",
|
||||
"exportName": "All",
|
||||
"name": "Calendar",
|
||||
"storyTags": [],
|
||||
"effectiveTags": [],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"storyTags": [
|
||||
"verification"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification"
|
||||
],
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-search-date-calendar--single-selection",
|
||||
@@ -1106,15 +1114,17 @@
|
||||
"exportName": "All",
|
||||
"name": "Checkbox",
|
||||
"storyTags": [
|
||||
"verification",
|
||||
"play-fn"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification",
|
||||
"play-fn"
|
||||
],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-form-checkbox--unchecked",
|
||||
@@ -1232,12 +1242,16 @@
|
||||
"id": "components-disclosure-collapsible--all",
|
||||
"exportName": "All",
|
||||
"name": "Collapsible",
|
||||
"storyTags": [],
|
||||
"effectiveTags": [],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"storyTags": [
|
||||
"verification"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification"
|
||||
],
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-disclosure-collapsible--closed",
|
||||
@@ -1451,12 +1465,16 @@
|
||||
"id": "components-overlay-context-menu--all",
|
||||
"exportName": "All",
|
||||
"name": "Context Menu",
|
||||
"storyTags": [],
|
||||
"effectiveTags": [],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"storyTags": [
|
||||
"verification"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification"
|
||||
],
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-overlay-context-menu--open",
|
||||
@@ -1657,12 +1675,16 @@
|
||||
"id": "components-overlay-drawer--all",
|
||||
"exportName": "All",
|
||||
"name": "Drawer",
|
||||
"storyTags": [],
|
||||
"effectiveTags": [],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"storyTags": [
|
||||
"verification"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification"
|
||||
],
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-overlay-drawer--open",
|
||||
@@ -1753,12 +1775,16 @@
|
||||
"id": "components-overlay-hover-card--all",
|
||||
"exportName": "All",
|
||||
"name": "Hover Card",
|
||||
"storyTags": [],
|
||||
"effectiveTags": [],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"storyTags": [
|
||||
"verification"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification"
|
||||
],
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-overlay-hover-card--open",
|
||||
@@ -2346,15 +2372,17 @@
|
||||
"exportName": "All",
|
||||
"name": "Radio Group",
|
||||
"storyTags": [
|
||||
"verification",
|
||||
"play-fn"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification",
|
||||
"play-fn"
|
||||
],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-form-radio-group--vertical",
|
||||
@@ -2802,6 +2830,28 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/components/selection-controls.stories.tsx",
|
||||
"title": "Components/Form/Selection Controls",
|
||||
"metaTags": [],
|
||||
"stories": [
|
||||
{
|
||||
"id": "components-form-selection-controls--all",
|
||||
"exportName": "All",
|
||||
"name": "Selection Controls",
|
||||
"storyTags": [
|
||||
"play-fn"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"play-fn"
|
||||
],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/components/separator.stories.tsx",
|
||||
"title": "Components/Data & Display/Separator",
|
||||
@@ -3114,15 +3164,17 @@
|
||||
"exportName": "All",
|
||||
"name": "Switch",
|
||||
"storyTags": [
|
||||
"verification",
|
||||
"play-fn"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification",
|
||||
"play-fn"
|
||||
],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-form-switch--off",
|
||||
@@ -3736,12 +3788,16 @@
|
||||
"id": "components-form-toggle--all",
|
||||
"exportName": "All",
|
||||
"name": "Toggle",
|
||||
"storyTags": [],
|
||||
"effectiveTags": [],
|
||||
"category": "PRIMARY",
|
||||
"decision": "KEEP",
|
||||
"storyTags": [
|
||||
"verification"
|
||||
],
|
||||
"effectiveTags": [
|
||||
"verification"
|
||||
],
|
||||
"category": "VERIFICATION",
|
||||
"decision": "HIDE",
|
||||
"candidate": null,
|
||||
"reason": "Representative story or distinct reference page."
|
||||
"reason": "Keep direct URL and tag-filter access; excluded from default sidebar."
|
||||
},
|
||||
{
|
||||
"id": "components-form-toggle--states",
|
||||
@@ -5244,7 +5300,7 @@
|
||||
{
|
||||
"id": "sw-footer--full-footer",
|
||||
"exportName": "FullFooter",
|
||||
"name": "SW 하단 명령어바",
|
||||
"name": "SW 명령어바",
|
||||
"storyTags": [],
|
||||
"effectiveTags": [],
|
||||
"category": "PRIMARY",
|
||||
|
||||
@@ -1,5 +1,279 @@
|
||||
# Working Context
|
||||
|
||||
## 2026-11-13 — 접기·펼치기 대표 Story 정렬 (미커밋)
|
||||
|
||||
- Variants → States → Collapsible · States → Content · Usage 순서로 배치했다. 공통 StateBoard의 24px 열 간격과 라벨 스타일을 재사용하고 대표 페이지의 섹션 간격만 24px로 맞췄다. 긴 예제 2개와 Button trigger는 마지막 섹션에 배치했다. 공개 컴포넌트와 별도 검증 Story는 그대로다.
|
||||
- Chrome 6027에서 1920/1440/1024/760/390px 기준 3/3/2/1/1열, PC 열 시작선 일치, 긴 문단·영문 문자열 전체 표시 및 가로 스크롤 없음 확인. 클릭·키보드·Disabled·ARIA 연결, Story ID 10개, `tsc -b`와 관련 테스트 통과.
|
||||
|
||||
## 2026-11-13 — Combobox Usage 그룹 제목 제거 (미커밋)
|
||||
|
||||
- 대표 Story의 `Long option`·`Select / Combobox` 제목과 래퍼를 제거했다. 개별 라벨 5개, 기존 그리드·들여쓰기·Story ID를 유지했다. Chrome 6027에서 1440px 입력 3개 상단 일치, 1024/760/390px 기존 2/1/1열 및 가로 스크롤 없음 확인. Select 선택과 비활성 옵션, Combobox 검색·선택·포커스 복귀, `tsc -b`, 컴포넌트 테스트 9건 통과.
|
||||
|
||||
## 2026-11-13 — 날짜 선택: PC 열 정렬 및 반응형 정리 (미커밋)
|
||||
|
||||
- DatePicker 대표 Story 전용 4열 기준으로 Sizes·States·Usage와 Calendar의 시작선을 맞췄다. 섹션 간 24px, 열 간 16px. Usage 제목은 중복 라벨을 대신하며 입력창과 팝업의 접근성 이름으로 연결했다.
|
||||
- Calendar의 Variants/States 섹션과 기존 Story ID는 유지했다. Two months는 아래 왼쪽에 배치하며, 넓은 화면에서는 기존 544px 너비, 좁은 화면에서는 기존 월 줄바꿈을 유지한다.
|
||||
- Chrome 6027: 1920/1440에서 4열 기준선 일치, 1024에서 입력·달력 첫 행 2열, 760에서 입력 2열·달력 1열, 390에서 모두 1열. 전체 폭에서 가로 스크롤 없음. 1920×1080에서는 공통 열을 적용한 최종 콘텐츠 높이가 1134px로, Two months 하단까지 약 30px 스크롤 필요. 날짜 선택·비활성 조건·포커스 복귀·Story ID 확인, 타입 검사와 관련 테스트 통과.
|
||||
|
||||
## 2026-11-13 — 조직도 › 개요 → `가족사 현황`: 명칭 통일·로고 카드 테두리 제거 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 메뉴: `org-chart-p1.stories.tsx` title `조직도/가족사 현황`, Story name `가족사 현황`, `.storybook/preview.tsx` storySort 항목도 같은 이름(위치 첫 번째 유지). Story ID `orgchart--logo-personnel-summary`(`__id`)·export명 불변이라 기존 verify 스크립트·링크 영향 없음.
|
||||
- 텍스트(`org-chart-page-previews.tsx`): 페이지 제목 `가족사 현황`, 섹션 `가족사 로고`·`가족사별 인원 현황`. `조직·직위별 인원 현황` 유지. 패키지 내부 문구(`logo-legend.ts`의 h2는 이 페이지에서 제거됨, `affiliation-bar` aria-label, 직원 상세 Dialog `관계사` 항목)는 공용 Core라 미변경.
|
||||
- 테두리: `core-component-preview.tsx`의 `hideLogoHeading` 분기(사용처는 이 페이지 하나)에서 로고 `li`에 inline `border: 0`. padding 16px·로고 96×96·grid gap 16px 유지로 간격만으로 구분. 패키지 CSS·배경·그림자 미변경.
|
||||
- 검증(6024 iframe, 1400·1024·760·390): 화면 텍스트 `관계사` 0, 로고 7개(순서 동일) border 0, 로고·회사명 중앙 정렬 오차 0, 열 6/5/3/1, 가로 Overflow 0. `tsc -b` 통과. 스크린샷은 pane 렌더 시간 초과로 미첨부. `.storybook/qa` 인벤토리(title·name) 재생성은 미실행.
|
||||
|
||||
## 2026-11-13 — 노선도 › 시설물 › 연계 시설물: 카드 배치 재구성 (POSITION | ACTUAL, 2열 카드) (미커밋)
|
||||
|
||||
- 범위: `src/center/center-stories.css`만(선택자 `.sb-center-linked-page` 범위). 패키지 Demo DOM·공개 API·SVG·Props·색상·다른 페이지·Story ID·메뉴 불변. 아래 "상단 안내·중복 명칭 제거" 기록의 숨김(안내·READY·카드 첫 이름)은 유지.
|
||||
- 측정(17개 카드): POSITION 최대 240px(지방도 터널 분할), ACTUAL 최대 252px(교차로 계열) → 카드 내부 2열 240 + 24 + 252 = 516px, 카드 2열 516 × 2 + 24 = 1056px.
|
||||
- 구조: 목록 `.linked-facility`를 container `sb-linked-list`로. 카드 그리드 기본 1열, ≥ 1056px 2열(gap 24, 홀수 마지막 카드 왼쪽). 카드 = 제목(14/20/700) + 패키지 `__variant` 2개를 열로: ≥ 516px POSITION | ACTUAL 좌우(subgrid로 라벨·기본·분할 행 정렬, 열 간 24), 미만 세로(블록 사이 24). 열 라벨 `POSITION` / `ACTUAL` 11px(사용자 A안 선택), 라벨 → 기본 8px, 기본 → 분할 16px(같은 행의 실사화 기본이 2px 더 높아 위치도 쪽 측정 18px). 카드 프레임 없음.
|
||||
- 삭제: 기존 base `sb-linked-card` 좁은 카드 라벨 쌓기 규칙과 64px 라벨 열 규칙(연계 시설물 전용, 새 구조로 대체).
|
||||
- 검증(6024 HMR, iframe): 1400(목록 1309) 카드 2열·내부 2열, 1024(933)·760(669) 카드 1열·내부 2열, 390(331) 카드 1열·내부 세로. 그룹 5개(5·3·3·3·3)·카드 17·figure 68·깨짐 0, 제목 중복 0, 안내·READY 미표시, 등급 제목 16px x=46(390: 28) → 카드 12px, 섹션·카드 간격 24, 행 정렬 불일치 0, figure 너비 불변(239·202·252·218), 로컬 스크롤 0, 문서 가로 Overflow 0. `tsc -b` 통과, `storybook-inventory.mjs` 1/1.
|
||||
|
||||
## 2026-11-13 — 노선도 › 시설물 › 연계 시설물: 상단 안내·중복 명칭 제거, 시작 위치·라벨 통일 (미커밋)
|
||||
|
||||
- 범위: `center-facility.stories.tsx`(Linked Story만 `sb-page-indent` 래퍼 추가), `center-stories.css`(선택자 `.sb-center-linked-page` 범위). 패키지 Demo·공개 API·SVG·Props·색상·토큰·다른 페이지·Story ID·메뉴 불변.
|
||||
- CSS 숨김(실제 반복 표시만): 상단 안내 `.linked-facility__context`(문구 2개 + 회색 막대), 등급 제목 옆 `N READY 시설물 집합`(카드 개수 반복), 카드 첫 이름 `strong`(터널·교량·교차로는 한글 이름 반복, JC·IC는 그래픽에 표기). 카드 제목은 한글 이름 1회(14/20/700 primary).
|
||||
- 정렬: 등급 제목·카드 시작 x = 터널 첫 제목(1400·1024·760: 46, 390: 28), 첫 제목 top도 터널과 동일. 카드 그리드의 자체 들여쓰기 제거.
|
||||
- POSITION / ACTUAL: 다른 시설물 행 라벨과 동일(11px/700/.08em, 64px 열, 간격 16px). 좁은 카드 라벨 위로 쌓기 기준 365 → 331px(64 + 16 + 최대 252px 그래픽), 이 페이지 64px 규칙이 쌓기 규칙보다 우선하므로 scoped container 재정의 추가.
|
||||
- 검증(6024 HMR, iframe 1400·1024·760·390): 안내·READY 미표시, 카드 제목 터널/교량/교차로/분기점/나들목 1회, 그룹 5개·카드 17개(5+3+3+3+3)·figure 68, 등급 제목 16/24/700 → 카드 12px, 섹션 24px, 카드 제목 → 그래픽 12px(760·390은 라벨이 위로 쌓여 41px, 기존 동작), 그리드 3/2/2/1열, figure 너비 불변, 이미지 깨짐 0, 로컬 스크롤 0, 문서 가로 Overflow 0. 1400 페이지 높이 3082 → 2838. `tsc -b` 통과, `storybook-inventory.mjs` 1/1.
|
||||
|
||||
## 2026-11-13 — 노선도 › 시설물: 교량·교차로·분기점·나들목·연계 노선·연계 시설물을 터널 레이아웃으로 통일 (미커밋)
|
||||
|
||||
- 범위: `src/center/center-facility.stories.tsx`, `src/center/center-stories.css`만. 패키지·dist·SVG·토큰·Story ID·메뉴명·순서·터널 화면 불변.
|
||||
- 교량·교차로·분기점·나들목: 패키지 `FacilityReferenceDemo` 대신 공용 Story 컴포넌트 `FacilitySpecimens`(공개 API `LocalAssetFacility`·`FACILITY_CONFIGS`·`CompactFacilityConnection`, Demo와 같은 Props). Position Map | Actual / Detail 2열, 각 열 SEPARATED → SHORT/MEDIUM/LONG(ACTUAL SHORT/REF/LONG), 간략 적용 예제 하단 전체 너비(SOURCE 문구·bridge 구간 모양 유지). 상단 단독 위치도·실사 예제 = MEDIUM / ACTUAL REF 행(기본 이름·보조 문구 동일)이라 제거, Separated 예제는 각 열 첫 행으로 이동, `너비 동작` 제목 제거.
|
||||
- 연계 노선: `RoadsideSpecimens`(공개 API `RoadsideFacilityTag`). 행 라벨을 다른 시설물과 같은 1줄 64px 라벨(SHORT/MEDIUM/LONG, ACTUAL …)로 공통화(사용자 요청), 열 보조 설명은 `위치도`/`실사화`만(사용자 요청으로 `콘텐츠 기반 너비`·`고정 양 끝 + 가변 중앙` 삭제). 분할은 실사화 첫 행, 2열에서 위치도 첫 행은 빈 행 `.sb-center-roadside__blank`, 1열에서는 숨기고 그룹 span 4. 위치도 태그는 `div`로 감쌌다 — 태그가 `span`이라 패키지 행 라벨 규칙(`.facility-reference__width-tests > div > span`, 11px)이 태그 글자에 적용되던 문제 방지.
|
||||
- 연계 시설물: 패키지 Demo·카드 그대로. CSS만 — 등급 그룹 사이 28 → 24px, 그룹 제목 옆 설명 12/18 회색·간격 8px·왼쪽 정렬(기존 space-between 14px).
|
||||
- CSS 전환 기준: 터널 1180px 블록을 `.sb-center-tunnel:not([data-sb-compare-at])`로 한정, 교량만 `data-sb-compare-at="1210"`(LONG 511px: 64+16+511 ×2 + 24 = 1206) 별도 블록. 연계 노선은 라벨 공통화로 64+16+450 → 1180px 사용(승인 시 1230px 안은 라벨 변경으로 불필요해져 제외). 빈 행 `display:none`은 패키지 `> div { display:grid }`보다 우선하도록 `.sb-center-roadside__position > ` 선택자.
|
||||
- 검증(6024 HMR, 서버 재시작 없음, iframe 1400·1024·760·390): 7개 페이지 1400 2열·행 top 일치, 이하 1열(그룹 사이 24px, 겹침 없음), 제목 16/24/700 + 설명 12px 회색·간격 8px, 제목 → 첫 행 12px, 섹션 24px, 이미지 깨짐 0, figure 너비 변경 전과 동일, 390에서 stage 2개 로컬 스크롤(8px 규칙 유지), 문서 가로 Overflow 0. 1400 페이지 높이 교량 1299 → 900, 교차로·분기점·나들목 1046 → 900(뷰포트). `tsc -b` 통과, `packages/sw/tests/storybook-inventory.mjs` 1/1 통과. Storybook 전체 창(LNB 180px) 1400에서 교량은 콘텐츠 1210px 미만이면 1열.
|
||||
|
||||
## 2026-11-13 — 노선도 › 터널: `너비 동작` 제목 제거, 열 제목을 공통 섹션 타이틀로 (미커밋)
|
||||
|
||||
- `center-facility.stories.tsx`: `너비 동작 · CENTER만 수평 확장` 제목 행 삭제(stage는 `tunnel-reference__sample` 안에 유지). 열 제목을 `header > strong + span`으로 — `Position Map` + `위치도`, `Actual / Detail` + `실사화`.
|
||||
- `center-stories.css`: 공통 예제 제목 행 규칙 3개의 `:is()` 목록에 `.sb-center-tunnel__width-group` 추가(16/24/700 + 12px 회색, 같은 줄, 간격 8px). 예전 열 라벨 규칙 삭제. 제목이 없어진 stage는 상단 padding 0, 열 제목 → 첫 행은 subgrid 16px − 4px = 12px.
|
||||
- 검증(6024): 1400 두 제목 top·높이(24px) 동일, 4단계 행 정렬 유지. 1024·760·390 1열(그룹 사이 24px). 안내 문구 → 첫 제목 16px, 섹션 간 24px, 행 라벨 8개·figure 너비 불변, 390 stage 8px 스크롤바·끝까지 스크롤, 문서 가로 Overflow 0. `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — 노선도 › 터널: 상단 영역 제거, 너비 동작 한 섹션으로 통합 (아래 "상단 3열" 기록을 대체, 미커밋)
|
||||
|
||||
- `center-facility.stories.tsx` `TunnelSpecimens`: 상단 Position Map / Actual / Separated 영역 삭제. `너비 동작` 각 열에 SEPARATED 행 추가 — 왼쪽 Position Map: SEPARATED → SHORT → MEDIUM → LONG, 오른쪽 Actual / Detail: SEPARATED → ACTUAL SHORT → ACTUAL REF → ACTUAL LONG. 삭제한 단독 위치도·실사 예제는 MEDIUM / ACTUAL REF와 같은 그래픽(대산2터널 L=750m). 간략 적용 예제는 하단 유지.
|
||||
- `center-stories.css`: `.sb-center-tunnel__top`·`__stack`(3열 상단) 규칙 삭제, 그룹 subgrid 행 4 → 5. 1180px 2열/1열 전환·8px 스크롤바 규칙 유지.
|
||||
- 검증(6024): 1400(콘텐츠 1324) 2열·4단계 행 위치 일치, 1024·760·390 1열(그룹 사이 24px). 섹션 2개(너비 동작·간략 적용 예제), 행 라벨 8개, 제목 → 그래픽 12px, 섹션 간 24px, figure 크기 불변(분할 197×93·199×95 등), 390에서 stage 8px 스크롤바·끝까지 스크롤, 문서 가로 Overflow 0. `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — 노선도 › 터널: 상단 3열·너비 동작 2열 그룹 배치 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/center/center-facility.stories.tsx`: `Tunnel`만 패키지 `TunnelReferenceDemo` 대신 Story의 `TunnelSpecimens`(공개 API `TunnelFacility`·`CompactFacilityConnection`, 데모와 같은 Props·문구)로 구성. 패키지 클래스(`tunnel-reference__sample`/`__stage`/`__width-tests`)를 재사용해 공통 제목 행·12px·8px 스크롤바가 그대로 적용. Story ID·이름·순서·`TunnelPlayground`·다른 시설물 페이지 불변.
|
||||
- `src/center/center-stories.css` 끝에 `.sb-center-tunnel*` 규칙만 추가(container query, 기존 8px 스크롤바 규칙 보존): 상단 Position Map | Actual / Detail | Separated(분할 2개 세로 16px) 3열 ≥ 720px, 미만 1열. 너비 동작은 열 제목 `Position Map`/`Actual / Detail`(14/20/500 회색) + SHORT·MEDIUM·LONG | ACTUAL SHORT·REF·LONG, subgrid로 같은 길이 행 정렬, 2열 ≥ 1180px(행 최대 577px × 2 + 24), 미만 1열(그룹 사이 24px). 간략 적용 예제는 아래 전체 너비.
|
||||
- 검증(6024): 1400(콘텐츠 1309) 3열·2열·행 정렬 일치, 1024(933) 3열·1열, 760·390·320 1열·1열. 문구·라벨 누락 0, 제목 → 그래픽 12px, 예제 간 24px, figure 크기 모든 폭 동일(210×93 … 497×95), 390·320에서 너비 동작·간략 예제 stage 8px 스크롤바 + 끝까지 스크롤, 문서 가로 Overflow 0. `tsc -b` 통과.
|
||||
- 테스트: `verify-p9-4.mjs`는 실행하면 untracked 기준 파일 `test-results/org-chart-p9-4/browser.json`을 덮어쓰므로 실행하지 않고, 터널 비교 항목만 브라우저에서 재현. 터널 첫 예제 프레임·stage 스타일 ≠ OrgChart Department(투명·padding 12px 0 0 vs 카드형) — 2026-11-12 노선도 카드 프레임 제거(`--plain`) 이후의 기존 차이이며 이번 변경과 무관(같은 클래스·같은 규칙).
|
||||
|
||||
## 2026-11-13 — SW Viewer 로고·문서 메뉴 테스트 잔여 7건: 승인 규격 기대값 반영 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `packages/sw/tests/viewer-logo-document-menu.browser.mjs`만 수정(SW 컴포넌트·Story·CSS·Props 불변). 7건 모두 2026-10-21~23 승인 변경 이전의 기대값이었다: 파일 열기 아이콘 = 실제 아이콘 1 + 외곽선 1 SVG(caf4c32), 아이콘 영역 16 × 16·클릭 영역(버튼) ≥ 34px(0840d76, 실측 104 × 50 / 아이콘만 37 × 50), 모든 sweep 폭에서 같은 규격; 좁은 헤더 hover = 면 없음(투명)·글자 #FFC600(caf4c32); sweep은 "More 전 아이콘 + 패널 토글이 전체 보기보다 먼저(2026-10-23) + 아이콘이 된 버튼은 좁아질수록 라벨 복귀 없음(More 이동 허용)", 항목 수 단조 감소 대신 More가 보이는 21개 폭(650~400)마다 More를 실제로 열어 8개 명령이 헤더·More 중 정확히 한 곳(패널 토글은 More 금지, 메뉴는 화면 안, Escape 닫힘, 페이지 넘침 없음), 파일명 길이와 무관하게 같은 전환 폭(아이콘 1040 / More 650, caf4c32의 Header 폭 기준).
|
||||
- 결과: viewer-logo-document-menu 49/49(검사 1개 추가: ⑤를 순서·라벨 복귀 금지 두 검사로 분리), viewer 103/103, property-panel-comparison 59/59. 작업 중 새로 넣은 검사의 판정 오류 1건(More로 이동한 버튼을 라벨 복귀로 셈)은 라벨이 다시 보이는지로 정정.
|
||||
|
||||
## 2026-11-13 — 노선도 그래픽 내부 가로 스크롤 표시 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 점검: 10개 페이지 × 1400·1024·760·390. 레이아웃 컨테이너 잘림 0(시설물 탐지는 `visibility:hidden; position:fixed` 측정용 span), 인라인 SVG viewBox 초과 0, 깨진 이미지 0. "잘려 보임"의 원인은 실제 크기 그래픽이 `overflow-x: auto` stage 안에서 넘치는데 스크롤바가 overlay(높이 0)라 스크롤 표시가 없던 것.
|
||||
- 수정: `src/center/center-stories.css` 끝에만 규칙 추가. `.sb-center--plain` 안의 `.tunnel-reference__stage`·`.facility-reference__stage`·`.center-title-reference__stage`·`.cid-demo__stage`·`.linked-facility__variant`·`.csi-panel > div`에 `::-webkit-scrollbar` 높이 8px, Thumb는 HM Scroll Area 토큰(`--hm-scroll-area-thumb-background`, hover `…-hover-background`), 트랙 투명. 비 WebKit은 `@supports not selector(::-webkit-scrollbar)`로 `scrollbar-width: thin` + 같은 색. overflow 동작은 그대로라 넘칠 때만 표시. 패키지·dist·다른 메뉴 불변.
|
||||
- 검증(6024, Chromium 기반 앱 브라우저): 넘치는 stage 44개 전부 스크롤바 8px, 넘치지 않는 stage 스크롤바 0, 프로그램 스크롤로 끝까지 이동 44/44, 사업명 760에서 실제 마우스 가로 스크롤로 끝(205/205px)까지 이동·그래픽 오른쪽 끝 표시 확인. 제목 → 그래픽 12px·예제 간 24px 유지, figure 크기 모든 폭에서 동일, 문서 가로 Overflow 0. 미검증: Firefox·Safari(표준 thin 스크롤바 경로)와 OS overlay 설정이 다른 실제 사용자 브라우저.
|
||||
|
||||
## 2026-11-13 — SW 테스트 잔여 2건: SW 범위 Story 집합 검사, 로고 메뉴 More 정책 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `storybook-inventory.mjs`: 승인 Step Header Story 10개(`stepHeaderStoryIds`, adbb034)와 `swScopeIssues` 추가 — 제목이 `SW/`인 Story ID 집합이 "c8e5fbe 이후 유지된 SW/Header(승인 변경 반영) + 승인 추가 목록"과 정확히 같은지 검사(unexpected = 무단 추가·ID 변경, missing = 삭제·ID 변경). `property-panel-comparison.browser.mjs`: 전체 Story 수 공식(338 − 9 + 30)을 `swScope.ok`로 교체(그 1줄과 import만; 공식이 이 파일에 있어 함께 수정). SW 밖 Story 추가·삭제는 영향 없음, 태그(internal·verification) 검사는 기존 header 검사가 유지 — 메모리 시뮬레이션으로 SW 무단 추가·삭제·ID 변경·태그 제거 감지 확인.
|
||||
- `viewer-logo-document-menu.browser.mjs`: 2026-10-26 More 정책(약 520px 미만에서만 More)에 맞춰 More를 여는 세 곳을 400px로(520 행을 제자리에서 400px로 줄임 / 헤더 400px, viewer.browser.mjs와 같은 방식), 860·760·520 기대를 "아이콘만, More 없음"으로, 항목 1회 보유 검사는 400px 축소 상태에서 수행 후 복원.
|
||||
- 결과: property-panel-comparison 59/59(swScope 50 = 50), viewer 103/103, viewer-logo-document-menu 41/48(이전엔 1단계에서 중단). 남은 7건은 이번에 처음 실행된 검사의 옛 기대값: 파일 열기 아이콘 34px 기대 ↔ 현재 16px(3건), 좁은 헤더 hover 색(1건) — 2026-10-21 이후 Viewer Header 수정; sweep 3건 — 1050~1310px에서 패널 토글만 아이콘(전체 보기 라벨 유지, 2026-10-23 승인), 560px 미만 파일명 56px로 항목이 More에서 돌아옴(비단조), 파일명 폭 고정으로 길이와 무관(2026-10-26). 제품 코드 미변경, 판정 재정의는 별도 승인 필요.
|
||||
|
||||
## 2026-11-13 — SW 테스트 기존 실패 정리: 인벤토리 internal 태그·Dropdown 스크롤·로고 메뉴 뷰포트 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 테스트 파일 3개만 수정(SW 컴포넌트·Story·CSS·Story ID 불변). `packages/sw/tests/storybook-inventory.mjs`: 승인 commit 2378663(2026-11-09)이 `sw-header--logo-slot`에 붙인 `internal` 태그를 `internalTaggedIds`로 기대값에 반영(`withApprovedEntryChanges`). `property-panel-comparison.browser.mjs`: "Dropdown follows the page scroll" 검사를 반복문 밖 별도 단계로 옮겨 1440 × 600(페이지가 스크롤되는 높이)에서 `behavior: "instant"`로 실행 후 크기·스크롤 복원, 판정 조건(이동 > 0, 필드 부착 1.5px 이내) 그대로. 원인: 3열 배치 이후 1440 × 900에서 페이지가 화면보다 짧고, `src/hm-theme.css`의 `html { scroll-behavior: smooth }`로 직후 scrollY가 0. `viewer-logo-document-menu.browser.mjs`: 화면 너비 페이지를 여는 두 곳을 1920 → 2500px(1920 행은 1980px 이상 페이지에서만 표시, 정책 유지).
|
||||
- 결과: property-panel-comparison 58/59(이전 56/59, Dropdown 스크롤·인벤토리 태그 해결), viewer 103/103(회귀 없음). 남은 실패: ① 인벤토리 첫 검사의 전체 Story 수 조건(388 ≠ 338 − 9 + 30 = 359) — SW 변경만 반영하는 공식이라 2026-10-20 이후 추가된 비 SW Story(Chart·Selection Controls 등)를 세지 못함, 기존 문제·승인 범위 밖. ② viewer-logo-document-menu: 시간 초과는 해결됐지만 다음 단계가 520 행의 More(추가 도구)를 누르는데 2026-10-26 규칙(More는 약 520px 미만에서만, viewer.browser.mjs는 520 행을 400px로 줄여 검사)으로 520 행에 More가 없어 중단. 테스트 기대가 2026-10-21 규칙(860·760·520에서 More) 기준이라 판정 조건 갱신이 필요하며 이번에는 수정하지 않음. 이후 검사는 실행되지 못함. 정적 빌드가 필요한 테스트(dropdown-consolidation·overview·storybook-unused-removal)는 실행하지 않음.
|
||||
|
||||
## 2026-11-13 — 노선도 데모 설명 정리: 기술·제작 설명 제거, 규격은 문서 보존 (브랜치 `style/storybook-ui-refinements`, 미커밋, 패키지 수정 승인됨)
|
||||
|
||||
- 패키지 `packages/center-graphic/src/components`의 `*ReferenceDemo` 문자열만 수정(컴포넌트 API·Props·CSS·그래픽·`index.ts` 불변, CRLF 유지): `TunnelFacility.tsx`·`FacilityBatch.tsx`(로컬 SVG → 위치도/실사화, Separated 설명 제거, `Source 정의` 제거, 연계 노선 `재구성 Primitive`·`Source 작성 …`·`13 / 90 / 13` 제거, `태그 계약` → `태그`), `CenterInformationDisplay.tsx`(정의 목록 구조 설명·테이블 행 높이·강조형 1 겹침 설명 제거, `Source 팔레트` → `팔레트`, 동적 콘텐츠 → `한 줄 항목은 말줄임, 명시적 줄바꿈 유지`), `CenterSpatialInformation.tsx`(1·1b legend 제거, 2·3 legend는 사용 기준 문장만). 유지: `SOURCE_RECEIVED`/`SOURCE_PENDING` 설명, `KNOWN_DEVIATION`, `READY`, 너비 테스트 라벨, 4·5·6·7 사용 기준·미확정 표기, 노선·공간 상태·입력. `npm run build:center-graphic` 1회(dist는 Git 미추적).
|
||||
- 문서: `docs/center-graphic/README.md` §4에 화면에서 뺀 수치를 코드와 대조해 추가(윤곽 3.6u, 그림자 Blur 6u 70%, Dark/White Outline 분류, 공사중 32u/32u·계획 45° 빗금, 사업 노선 #D21203·150%, 노선명 54u/9u, 번호 64u, 방면 50u·80% 2u·7점, 정보 테이블 26.116/34.364u(=26.116 + 8.248), 강조형 1 4u 겹침, 연계 노선 간격 19/14). `13 / 90 / 13`은 코드 미확인 Source 표기로 비확정 명시. §7 공통 레이아웃 문단 갱신.
|
||||
- 검증(6024, 10개 페이지 × 1400·1024·760·390): 제거 대상 문구 23종 잔존 0, 필수 상태·비교 문구 누락 0, 제목 → 그래픽 12px, 노선·공간 h4 → 캔버스 12px(설명 있으면 4px), 안내 오른쪽 끝, 문서 가로 Overflow 0, 깨진 이미지 0. `tsc -b`·center-graphic `typecheck` 통과.
|
||||
|
||||
## 2026-11-13 — Storybook 미해결 사항: SW 화면 너비·390px 넘침, Input States, Selection Focus, 문서·주석 정합성 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- SW 화면 너비 4개 페이지(SW 헤더·뷰어·단계별 헤더·명령어바 › 화면 너비): 행 제목의 폭과 실제 폭이 같도록, Story 열보다 넓은 행은 숨기고(`data-sw-width-row`) 페이지의 가장 좁은 행(760 / 520 / 1100, `data-sw-width-min`)은 항상 표시하며 열이 더 좁으면 그 폭을 유지한 채 행 안에서 스크롤한다(`sw-stories.css` 컨테이너 `sw-story-rows`, `.sw-story-fit__scroll`). 최소 폭·overflow는 열이 좁을 때만 적용되어 넓은 화면의 열린 메뉴가 잘리지 않고 행을 제자리에서 줄이는 검사(viewer More)도 그대로 동작한다. 각 페이지의 너비 세트는 유지. 명령어바 화면 너비는 다른 세 페이지처럼 `sw-story-stack`(flex gap)으로 행 간격을 두고 형제 margin 규칙 2개를 삭제했다(`sw-footer-stories.css`).
|
||||
- SW 명령어바(760)·뷰어 로고(520): Story 열이 그보다 좁으면 예제가 그 폭을 유지하고 섹션 안에서 스크롤(`.sw-story-fit`), 페이지 가로 넘침 없음. SW 설정창: 액자 최소 520px(이미 있던 스크롤 영역 안)로 390px에서도 340px 옵션 팝업이 액자 안에 들어간다. SW 컴포넌트 크기·최소 폭·API는 변경 없음.
|
||||
- Input States: Disabled를 Focus·Validation과 같은 Medium(34px)으로. Readonly는 컴포넌트 규칙(텍스트만, 항상 Large 수치)이라 42px 유지.
|
||||
- Selection Controls Focus: `.storybook/pseudo-states.ts`에 `:focus-visible` → `data-sb-focus` 복제를 추가(부정형 `:not(:focus-visible)` 규칙은 복제하지 않음). Radio Group(Option A)·Switch Focus 칸에 링 고정, Checkbox는 기존 `play` 실제 포커스 유지. 실제 Tab 포커스 링 동작 동일.
|
||||
- 후속 수정(2026-11-13): 위 복제가 `:focus-visible:active` 같은 복합 규칙을 `[data-sb-focus][data-sb-active]`로만 만들어 Slider `Focus` 검증 Story의 `Focus + Drag`(실제 포커스 + `data-sb-active`)가 실패했다. 기존 복제본(`:focus-visible` 유지, hover/active만 교체)도 함께 만들도록 보완. 검증: Slider Focus play 통과, Selection Controls·Button·Input·Checkbox에서 복제 규칙 삭제 0·추가 9개(모두 기존 방식 복제본), Selection Controls 고정 포커스 링 동일, `tsc -b`·Storybook build 통과.
|
||||
- 문서: 프로젝트-지침 §6(구분선·16px 제거, 좌우 최대 30px), combobox·dialog·dropdown-menu·toggle·sonner·navigation-menu·lnb·checkbox·radio-group·switch·input, chart README 높이. 주석: foundation.css·center-stories.css(1줄)·chart-story·story-board·Story 공통 문구의 "divider", tabs 28px, carousel 중복 주석, org-chart-expanded의 적용되지 않던 `margin-top: 24px`(계산값 0px) 삭제.
|
||||
- SW 테스트: 인벤토리 기대 이름을 현재 Story 이름(속성창·화면 너비·설정창·로고·SW 명령어바)으로 갱신. property-panel-comparison 56/59(남은 실패: 인벤토리 2건 = `sw-header--logo-slot`의 `internal` 태그(258e0a4)가 승인 태그 목록에 없음, Dropdown 페이지 스크롤 1건 — 모두 기존), viewer 103/103. viewer-logo-document-menu는 1920px 화면에서 숨겨진 1920 행의 헤더를 기다리다 시간 초과(2026-10-23 이후 기존 문제).
|
||||
|
||||
## 2026-11-13 — 노선도 메뉴 전체에 사업명 페이지 레이아웃 공통 적용 (A안, 브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- Story: `center-facility.stories.tsx`(`page()` — 터널·교량·교차로·분기점·나들목·연계 노선), `center-information.stories.tsx`, `center-spatial.stories.tsx`에서 `StorySection title="개요"` 제거 → 공통 `sb-page-indent`로 기존 예제 들여쓰기 유지. 연계 시설물(원래 개요 없음, 들여쓰기 없음)·사업명은 불변. Story ID·이름·순서·Playground 불변.
|
||||
- CSS(`center-stories.css`, `.sb-center--plain` 범위): 사업명 전용 제목 행 규칙을 공통 규칙 하나로 통합 — 터널·시설물·사업명·정보 데모 헤더(`header > strong|h3 + span`)를 한 행(flex-wrap, baseline, 간격 8px)으로, 제목 16/24/700 primary, 설명·사이즈 12/18 secondary. 노선·공간 `h4`도 16/24/700(긴 설명 legend/note는 제목 아래 14px 그대로). 연계 시설물 카드 라벨은 기존 14/500 유지. 배율 안내 `.center-representation__note`는 독립 행 오른쪽 끝(flex-end, 줄바꿈), 사업명만 숨기고 첫 제목 행에 표시(숨김 선택자 특이도 상향). 섹션 제거로 사용처가 없어진 `.sb-page-section-body > .sb-center--plain` 들여쓰기와 사업명 헤더 `> div` gap 규칙 삭제. 패키지 문구(Source·SOURCE_RECEIVED/PENDING·번호)는 그대로.
|
||||
- 문서: `docs/center-graphic/README.md` §7에 공통 레이아웃 기준 한 단락.
|
||||
- 검증(6024, 페이지 10개 × 1400·1024·760·390·320): `개요` 0, 안내 오른쪽 끝 = Inner 오른쪽 끝(사업명은 제목 행), 모든 예제 헤더 16/24/700 + 12px·8px, 제목 → 그래픽 12px, 노선·공간 h4 16px/700, 문서 가로 Overflow 0. 1400·1024는 전 헤더 한 줄, 760 정보 1개, 390·320은 긴 설명(정보·연계 노선 등)이 다음 줄로 줄바꿈. 교량 1024 예제 간 24px·열 간 24px. `tsc -b` 통과. 그래픽·stage 규칙은 미변경.
|
||||
- 미해결: 안내 문구의 첫 제목 행 배치와 Source 문구 정리는 패키지 데모 공통화 작업에서 별도 검토.
|
||||
|
||||
## 2026-11-13 — 노선도 › 사업명: 배율 안내를 첫 예제 제목 행 오른쪽으로 이동 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 아래 "`개요` 제거·우측 정렬" 기록의 후속. 패키지 `CenterRepresentation`의 독립 안내(`.center-representation__note`)는 이 페이지에서만 `display: none`(`.sb-center-title-page`). 같은 문구를 `center-title.stories.tsx`의 첫 예제 제목 행에 `.sb-center-title-note`로 렌더(문구는 `centerTargetResolution()` 기준 100% · 7680 / 75% · 3840, 패키지와 같은 규칙).
|
||||
- `center-stories.css`: 이전 우측 정렬 규칙을 위 숨김 + `.sb-center-title-note { margin-left: auto; text-align: right }`로 교체. 줄바꿈되는 flex 행에서 "구성명 + 사이즈 | 안내" 양끝 정렬, 공간 부족 시 안내만 다음 줄 오른쪽으로 이동. 스타일은 사이즈와 같은 12px 회색.
|
||||
- 검증(6024): 1400·1024·760·390 모두 안내가 첫 제목 행과 같은 줄, 오른쪽 끝 = Inner 오른쪽 끝(1355 / 979 / 730 / 374). 별도 안내 영역 없음(첫 제목 행이 페이지 상단 gutter에서 시작). 예제 5개 제목→그래픽 12px·간격 8px·예제 간 24px·그래픽 크기 불변. 320px 추가 확인: 제목 + 사이즈는 한 줄, 안내만 다음 줄 오른쪽. 문서 가로 Overflow 없음, `tsc -b` 통과. 패키지·dist 미수정.
|
||||
|
||||
## 2026-11-13 — 노선도 › 사업명: `개요` 타이틀 제거·배율 안내 우측 정렬 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/center/center-title.stories.tsx`의 `Compositions`: `StorySection title="개요"` 제거. 예제는 공통 `sb-page-indent`(16px, ≤640px 12px) 안에 둬 기존 x 위치를 유지한다. `StoryPage className="sb-center-title-page"` 추가. Story ID·`Playground`·예제 5개 구성은 불변.
|
||||
- `src/center/center-stories.css` 끝에 `.sb-center-title-page .center-representation__note` 규칙만 추가(`display:flex; justify-content:flex-end; text-align:right; overflow-wrap:anywhere`). 패키지 `CenterRepresentation`·dist·다른 노선도 페이지는 불변(클래스가 이 Story에만 있음).
|
||||
- 검증(6024): 1400·1024·760·390 모두 H2 0개·`개요` 텍스트 없음. 안내 문구 오른쪽 끝 = Inner 오른쪽 끝(1355 / 979 / 730 / 374), 한 줄. 첫 예제 제목은 안내 문구 16px 아래(패키지 note margin), x = Inner + 16(390은 +12)로 이전과 같다. 예제 5개 한 줄·간격 8px·제목→그래픽 12px·예제 간 24px·그래픽 크기 불변(390의 긴 콘텐츠 설명 줄바꿈은 이전과 같음). 문서 가로 Overflow 없음. `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — 노선도 › 사업명: Type·Source 문구 제거, 구성명 + 사이즈 한 줄 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/center/center-title.stories.tsx`의 `Compositions`(Story ID `center-graphic-title--compositions` 유지)만 변경. 패키지 데모 `CenterTitleReferenceDemo` 대신 공개 API(`CenterTitle`, `CENTER_TITLE_COMPOSITIONS`)로 예제 5개를 Story에서 직접 구성한다. 제목 = 구성명(H3), 오른쪽 사이즈(`620 × 100` 등). 긴 콘텐츠는 제목 `노선구분 + 사업명 (긴 콘텐츠)` + 기존 설명 "같은 연결과 고정 외곽 지오메트리; 콘텐츠 기반 너비". `Type 1~4`·`Source 근거 뷰포트` 표시 제거. 패키지(`packages/center-graphic`)·dist·`Playground`·상단 `표시 배율 · 제작 대상` 안내는 불변.
|
||||
- `src/center/center-stories.css` 끝에 `.sb-center--plain … header.sb-center-title-row` 규칙만 추가: 제목은 공통 섹션 제목(16/24/700 primary), 사이즈는 12/18 secondary, 간격 8px, 모든 폭에서 한 줄(공간 부족 시에만 줄바꿈 — 패키지의 ≤720px 세로 배치를 이 페이지에서만 덮음). 제목 → 그래픽 12px(기존 stage 상단 padding), 예제 간 24px(기존) 유지. 그래픽 영역 클래스 `.center-title-reference__stage` 재사용으로 배경·크기 불변.
|
||||
- 검증(6024): 1400·1024·760에서 5개 모두 한 줄·간격 8px·제목→그래픽 12px·예제 간 24px, 그래픽 620×100 / 610×167 / 890×100 / 782×167 / 긴 콘텐츠 1695×100(기존처럼 stage 로컬 스크롤). 390에서 고정 사이즈 4개는 한 줄, 긴 콘텐츠 설명만 다음 줄로 넘어감(의도된 줄바꿈). 모든 폭에서 문서 가로 Overflow 없음, Type·Source 문구 0. `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — Storybook 전체 점검 후속: P1 4건·중복 UI 요소 제거 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 원칙(사용자 정정): 중복 정리는 문구 번역·재작성이 아니라 같은 정보를 반복하는 UI 요소 자체를 제거하는 것. 버튼·폼 Label·aria-label·필요한 H2/H3는 유지하고, 차트처럼 한 행에 놓인 예제는 정렬을 유지한다. 번역·`열기` 일괄 치환·Title Case·Invalid→Error·Usage/Default 명칭 변경은 원복했다.
|
||||
- P1: Toggle Button Sizes 라벨 Default→Medium·글자 토글의 prop 값(`sm/default/lg`)→예시 문구 "알림"(`toggle-examples.tsx`), Collapsible 대표 화면 디버그 출력 제거(`collapsible-examples.tsx`), Combobox·Date Picker Sizes `Default`→`Large`(기본 크기가 실제 large), SW 설정창 패널 배치를 Story 열 폭에 따라 3/2/1열로(1076px 이상 기존 24/368/712, 700px 이상 2열, 미만 1열; `sw-viewer.stories.tsx`, 1024px 겹침 해소, 390px에서 340px 옵션 팝업은 프레임보다 7px 넓음).
|
||||
- 중복 제거: 비활성 항목의 "(사용 불가)"(메뉴바·팝업 메뉴·컨텍스트 메뉴·명령어 검색), Breadcrumb "홈 (custom link)"→"홈", Toggle "Disabled off/on"→"Off/On", SW 뷰어 속성 "일반 정보 (General)"·"기하 속성 (Geometry)"의 괄호 영문, Date Picker Label "기본 형식 · " 접두어, 차트 설명 줄(H2 반복만 있는 Line 6·Area 곡선 6·Donut 4·Radar 4 전체 제거, Bar Horizontal "· 가로 막대"·Radial 내부 ID 접두어만 제거, 정보가 섞인 Pie·Area Stacked/Expanded는 정렬 때문에 유지), SW 속성창 비교 figcaption 4개(상태명은 figure `aria-label`).
|
||||
- 기타: 라디오 그룹 aria-label이 내부 id(`sc-d`)로 읽히던 문제 → "옵션 선택", slider 소스 공백 오타, alert-dialog 주석 정정. 테스트 `packages/sw/tests/property-panel-comparison.browser.mjs`: figcaption 대신 figure aria-label도 읽고, 1열/3열 기준을 현재 CSS(727 / 1065px, 좌우 여백 30px)에 맞춤.
|
||||
- Selection Controls H2("Selection Controls")와 Step Header H2는 섹션 구조(제목 위계)에 필요해 유지. Story ID 388개·Props·컴포넌트 불변. 문서: button.md·toggle.md·combobox.md·date-picker.md 해당 문장.
|
||||
|
||||
## 2026-11-13 — 이동 경로(Breadcrumb) Story: Custom 명칭·Variants 1/3열·Usage 전체 너비 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/components/breadcrumb.stories.tsx`의 대표 `All`만 수정. 예제 이름 `Custom link · separator` → `Custom`(id `custom` 유지). Variants 배치를 `auto-fill`(최소 240px) → `grid-cols-1` + `min-[1000px]:grid-cols-3`(Accordion·Collapsible과 같은 1 → 3열 패턴, 2 + 1 줄 배치 제거). Usage(Long path)의 `max-w-[900px]` 제거 → 섹션 전체 너비, `BreadcrumbList` `flex-wrap`으로 자연 줄바꿈. 공통 `StorySection` / `StateBoard` 재사용, 페이지 전용 CSS 없음. Story ID·예제 수·verification Story·Breadcrumb 컴포넌트·Props·Theme·토큰 불변. `docs/components/breadcrumb.md` §9 대표 Story 설명 갱신.
|
||||
- 검증(6027 임시 서버 `storybook-verify`): iframe 1400 = 3열(각 425px) / 1024 = 3열(각 300px), 760 / 390 = 1열. 세 예제 모두 제목 → Breadcrumb 12px, 시작 위치 같음, 섹션 간격 28px. Long path는 1400·1024에서 1줄, 760에서 2줄, 390에서 3줄이며 넘침·문서 가로 스크롤 없음. 마지막 경로 700·`rgb(33,33,33)` 유지. Ellipsis Dropdown은 열림(`문서`·`설정`) → Esc 닫힘 → 트리거로 Focus 복귀, 링크 href·`/` 구분자 유지, 콘솔 오류 없음. `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — 탭 Story: Variants 2열·Behavior(Overflow) 분리·Usage 반응형 2열 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/components/tabs.stories.tsx`의 대표 `All`만 재배치. Variants = Primary / Secondary 균등 2열, 새 H2 **Behavior** = 기존 Overflow 예제(20개 탭, 내용 불변)를 왼쪽 열에, Usage = 기존 두 예제(Card 흰 배경·페이지 배경·`max-w-lg`·입력 요소 불변) 균등 2열. `ThreeColumns` 제거, `TwoColumns`를 `@container` + `@min-[720px]` 2열(24px 간격)로 변경 — 기준은 섹션 본문 실제 너비(공통 board 들여쓰기 16px 제외). 공통 `StorySection` / `StateBoard` 재사용, 페이지 전용 CSS 없음. Story ID·`data-overview-state`·verification Story·Tabs 컴포넌트·Props·Theme·토큰 불변. meta 설명 문구와 `docs/components/tabs.md` 대표 Story 설명 갱신.
|
||||
- 검증(6027 임시 서버 `storybook-verify`, 6024는 다른 세션 서버로 `storybook-stories.js` 404): iframe 1400(컨테이너 1309) / 1024(933) = 2열(각 642 / 454px, 제목·탭 시작점 동일), 760(685) / 390(346) = 1열, 문서 가로 스크롤 없음. 탭 클릭 선택·패널 전환(Primary·Secondary·Overflow), Overflow 다음 버튼 `scrollLeft` 증가·이전 버튼 활성화·선택 유지. `tsc -p tsconfig.app.json --noEmit` 통과.
|
||||
|
||||
## 2026-11-13 — 선택 컨트롤 상태 비교 매트릭스 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/components/selection-controls.stories.tsx`만 재작성(+ `checkbox-examples.tsx`의 `Controlled` export). 반복 H2 6개("Checkbox · States" 등) → H2 `Selection Controls`(상태 매트릭스) + H2 `Variants`(Checkbox Indeterminate, Radio Group Vertical/Horizontal; Switch는 별도 Variant 없음).
|
||||
- 매트릭스: 행 Default / Hover / Focus / Disabled / Validation, 열 State / Checkbox / Radio Group / Switch. Radio Disabled는 캡션 `항목`·`그룹`으로 구분, Switch Validation은 `—`(상태 없음). Radio Error 시안은 선택 없음(`defaultValue=""`, 사용자 요청 — Error 행에 초록 선택 표시 없음). 컴포넌트·Props·Theme CSS 불변, Hover는 기존 `data-sb-hover`, Focus는 기존처럼 Checkbox만 play에서 자동 Focus.
|
||||
- 반응형: DOM 1회 렌더(컴포넌트 순서) + 컨테이너 쿼리(`@container`, 미리보기 Inner 실제 너비). Inner ≥ 720px이면 각 컴포넌트 블록이 `display: contents`로 4열 grid에 배치(열 `max-content` + 남는 폭 1fr 균등, 행 구분 `border-border` 1px, 셀 12px 상하·24px 열 간격). 미만이면 Checkbox → Radio Group → Switch, 각 Default → Validation 세로 그룹(상태명 12/16/400). 4열 콘텐츠 실측 합계 692px(State 92 + 232 + 217 + 151) → 여유 28px로 720px.
|
||||
- 검증(6026, 6024는 기존 `/index.json` 500 지속): iframe 1400·1024 = 4열, 760·390 = 세로 그룹, 문서 가로 스크롤·잘림·화면 밖 요소 0. 인스턴스 Checkbox 9·RadioGroup 8·Switch 7(중복 렌더 없음), `data-overview-state` 19개, Hover 시각(테두리 색) 유지, Manager 1024 창(LNB 180)에서도 4열. `tsc -p tsconfig.app.json --noEmit` 통과.
|
||||
- 남은 정리 후보: `*Sections`의 `titlePrefix`·`idPrefix`·`gridClassName`는 이제 사용처 없음(숨김 개별 페이지는 미사용) — 범위 밖이라 유지.
|
||||
|
||||
## 2026-11-13 — 통합 페이지 디자인 정합성: 버튼·Toggle Button 규칙 통일, 선택 컨트롤 공통 그리드 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 버튼 페이지 Toggle Button 재구성(같은 날 후속, 아래 `flat` 기록을 대체): `flat`이면 `ButtonPageToggleSection` — 독립 H2 "Toggle Button" 한 섹션(버튼 열과 36px: 섹션 간격 28px + `pt-2`), 그룹 H3 Variants / States(Off / On, Disabled) · Groups(단독, 단일 선택, 다중 선택 — 3차: 버튼 페이지만 `groupSpecimens(idPrefix, true)`로 "연결 그룹 · " 접두어 생략, 숨김 Toggle 페이지 라벨 유지) · Sizes(Small/Default/Large). 버튼과 통일(같은 날 2차): 1400px 이상 같은 너비 3열(버튼 열과 같은 x), 미만 1열·그룹 간 24px. 각 행은 States 매트릭스 형식(왼쪽 라벨 14/20/500 → 8px → 컨트롤, 행 간 8px, subgrid로 그룹 안 컨트롤 시작선 정렬). `ToggleSections`에 `stacked` 옵션 — 버튼 페이지가 `MatrixFit` 안의 `ToggleButtonSection`에서 넘겨, 매트릭스가 세로 배치되면(390px) Toggle도 라벨 위 12/16/400 → 4px → 컨트롤, 행 간 12px로 함께 바뀐다. 반복 제목 `Toggle Button · …` 제거, 예시 ID(`toggle-offon` 등) 유지. 숨김 Toggle 페이지 경로(4섹션·StateBoard·Sizes 들여쓰기)는 그대로이며 `groupSpecimens`·`SizeRow`만 공유. 검증 1440/1400/1399/1100/1099/1024/768/390 가로 스크롤·잘림·줄바꿈 0, 상호작용 정상, `tsc -p tsconfig.app.json` 통과.
|
||||
- (이전 기록) 버튼 페이지 Toggle Button 섹션: `ToggleSections`에 `flat` 옵션(버튼 페이지만). H3 그룹 라벨을 버튼 GroupBlock 규칙(들여쓰기 0, H3 → 예시 8px)으로 그리고 Sizes 들여쓰기도 뺀다. 1400px 이상에서는 Variants·States가 버튼 3열 트랙의 1·2열을 써서 단독/연결 그룹·Sizes와 함께 Brand / Standard Solid / Standard Outline 열에 맞는다(1000~1399px는 기존 324:234 비율 2열, 999px 이하 1열). 섹션 제목 `Toggle Button · 단독 · 연결 그룹` → `Toggle Button · 단독/연결 그룹`. 숨김 Toggle 페이지는 기존 StateBoard(16px·12px)·제목 그대로.
|
||||
- 버튼 상태 행렬 세로 배치(좁은 화면)의 상태 이름만 12/16/400(`text-xs font-normal`, 보조색)으로 H3 "States"(14/20/500)와 구분. 공통 H2/H3 스타일은 불변.
|
||||
- (이전 기록, 위 상태 매트릭스로 대체) 선택 컨트롤: `CheckboxSections`·`RadioGroupSections`·`SwitchSections`에 `gridClassName` 옵션. 통합 페이지만 공통 트랙(1000px 이상 4열, 미만은 최소 220px 자동 열 → 999px 3열·768px 2열·390px 1열)을 넘겨 모든 섹션의 예시 열이 맞는다. Radio Horizontal은 1000px 이상 2칸·미만 한 줄 전체를 써 한 줄 유지. 숨김 Checkbox(3열)·Radio Group(2/4/4열)·Switch(5열) 페이지는 그대로.
|
||||
- 주의: Tailwind 임의값 class 바로 뒤에 `${…}`를 붙이면(공백 없이) 소스 스캔이 그 class를 놓친다. 이번 작업 중 Toggle Variants/States 2열이 사라졌다가 공백으로 분리해 복구.
|
||||
- 검증(6026, 다른 세션 Storybook이 6025 사용 중): 버튼 1440/1400/1399/1024/768/390, 선택 컨트롤 1440/1024/1000/999/768/390 페이지·내부 가로 스크롤·잘림 0, 예시 수 불변(11/19), 상호작용·Focus·Disabled 정상, 숨김 페이지 레이아웃 불변, `tsc -p tsconfig.app.json` 통과. 인벤토리 재생성 결과 변화 없음(KEEP 73 / HIDE 315).
|
||||
- 6024: 계속 `/index.json` 500(`center-spatial.stories.tsx`를 MDX 문서 단계에서 파싱한 캐시 오류). 다른 세션 서버라 재시작하지 않음 — 그 세션에서 재시작 필요.
|
||||
|
||||
## 2026-11-13 — Storybook 유사 컴포넌트 6개 그룹 통합·메뉴명 2건 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 방식: 2026-11-12 대화상자·팝업 메뉴 통합과 같다. 흡수되는 컴포넌트의 예제를 `src/components/<name>-examples.tsx`(Storybook 전용)로 옮기고 `<Name>Sections`(섹션 묶음, `titlePrefix`/`title`·`idPrefix` 옵션) 하나로 대표 페이지와 기존 Story가 함께 쓴다. 흡수되는 쪽의 기존 대표 Story `All`은 `verification` 태그만 붙여 사이드바에서 숨기고 같은 화면·`play`·ID·직접 URL을 유지한다. Story 파일 이동·`title` 변경·Story 삭제 없음. 패키지 컴포넌트·Props·토큰 변경 없음.
|
||||
- 사이드바 결과: `버튼`(Button + "Toggle Button · …" 4섹션), `선택 컨트롤`(신규 `Components/Form/Selection Controls` → `components-form-selection-controls--all`, "Checkbox · …"·"Radio Group · …"·"Switch · States"), `시트·드로어`(Sheet + "Drawer"), `툴팁·호버 카드`(Tooltip + "Hover Card", 같은 5열), `접기·펼치기`(Accordion + "Collapsible · …"), `날짜 선택`(Date Picker + "Calendar · …"). 숨김 대표 8개(인벤토리 기준, 처음 보고의 "9개"는 오기): checkbox·radio-group·switch·toggle·drawer·hover-card·collapsible·calendar의 `--all`. 호스트 페이지 섹션 제목은 그대로, 흡수 섹션만 컴포넌트명을 붙였다(단일 섹션은 컴포넌트명만 — "Alert Dialog" 선례). 합친 페이지의 `data-overview-state`는 접두어(`checkbox-` 등)로 중복을 피한다.
|
||||
- 선택 컨트롤 `play`는 Checkbox Focus 예시 하나만 focus한다(한 번에 하나만 focus 가능). Radio Group·Switch Focus는 Tab 또는 각자의 숨김 대표 Story(로드 시 focus 유지)에서 확인.
|
||||
- 버튼 상태 매트릭스: 매트릭스가 자기 열보다 넓으면(390px에서 420px Standard Solid) 세 Treatment가 함께 상태별 세로 배치(상태 이름 → 버튼 줄 wrap)로 바뀐다(`MatrixFitProvider`·`data-state-matrix="table|stacked"`, ResizeObserver로 표 실제 폭과 열 폭 비교). 1400px 이상 3열·미만 1열, 버튼 크기·상태·색상은 그대로.
|
||||
- 메뉴명(`manager.js`만): `components-navigation-navigation-menu` `메뉴` → `내비게이션 메뉴`, `patterns-data-data-table` `테이블` → `데이터 테이블`(2026-11-12 `테이블` 결정을 이번 요청으로 대체). 통합 라벨: `components-overlay-sheet` → `시트·드로어`, `components-overlay-tooltip` → `툴팁·호버 카드`, `components-disclosure-accordion` → `접기·펼치기`, 신규 `components-form-selection-controls` → `선택 컨트롤`. 숨김 컴포넌트 라벨은 태그 필터용으로 유지. `preview.tsx` storySort Form에 `Selection Controls` 추가.
|
||||
- 문서: `docs/components/` 13개(button·toggle·checkbox·radio-group·switch·sheet·drawer·tooltip·hover-card·accordion·collapsible·calendar·date-picker) 대표 Story 행. 인벤토리 재생성: 파일 89 → 90, Story 387 → 388, PRIMARY 80 → 73, 삭제된 ID 0.
|
||||
- 검증: 6024는 다른 세션 서버가 `center-spatial.stories.tsx` acorn 파싱 오류로 index 500 상태여서(파일은 HEAD와 같고 tsc 통과) `.claude/launch.json`에 `storybook-verify`(6025)를 추가해 확인. 사이드바·숨김 URL 9개·1420/1400/1399/844/580/390px 6개 페이지 페이지/내부 가로 스크롤 없음, `tsc -p tsconfig.app.json` 통과.
|
||||
|
||||
## 2026-11-13 — 버튼 Story: Treatment별 3열 안에 Sizes·States 통합 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 바로 아래 항목(Sizes 섹션 + Treatment별 상태 섹션)을 대체한다. `src/components/button.stories.tsx`의 `All`만 변경했고 `SizeColumns`는 `GroupBlock`으로 바꿨다. 패키지 Button·API·CSS, verification Story, 다른 Story는 변경 없다.
|
||||
- 구조: `StorySectionGrid` 안에 Brand / Standard Solid / Standard Outline 공통 `StorySection`(H2 16/24/700, 제목 → 콘텐츠 12px, Border 없음)을 둔다. 각 섹션은 `GroupBlock` 두 개로 이루어진다. Sizes(H3 14/20/500 회색 → 8px → Small/Medium/Large)와 States(H3 → 8px → 기존 `StateMatrix`)이며 두 블록 사이는 24px다. 페이지 단위 Sizes / States 제목은 없다. 그룹은 섹션 가장자리에 둔다(들여쓰기 없음). 1400px 경계에서 Solid 매트릭스 420px가 1/3 열에 들어가게 하기 위해서다.
|
||||
- 반응형: 미리보기(iframe) 폭 1400px 이상에서 같은 너비 3열, 미만은 1열이며 섹션 간격은 28px이다.
|
||||
- 검증(6024, 미리보기 1420·1400·1399·844·580·390px): 1420에서 열 x 30 / 491 / 953, 너비 437px, 세 H2 상단 같음. 1400에서 3열(431px), 1399에서 1열로 전환. 모든 폭에서 H2 → Sizes 12px, H3 → 콘텐츠 8 / 8px, Sizes ↔ States 24px, 크기 행 높이 42px(줄바꿈 없음), 컬러(3 / 5 / 4)·상태(4)·버튼 수(12 / 20 / 16) 누락 없음, 페이지 가로 스크롤 0. 표 내부 스크롤은 390px의 Standard Solid(77px)만 있다(420px 매트릭스 > 343px 열, 버튼 크기·구조 유지 조건상 불가피). `docs/components/button.md` 갱신, `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — 버튼 Story: Variants 제거·Sizes와 상태 비교를 Brand / Standard Solid / Standard Outline 3열로 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/components/button.stories.tsx`의 대표 Story `All`만 변경했다. Variants 섹션과 그 전용 `GroupRows`, 비례 폭 `GroupColumns`를 제거했다. 패키지 Button, API, 스타일, verification Story(Roles·Sizes·Brand States·Standard States 등)는 변경 없다.
|
||||
- Sizes: `SizeColumns`로 Brand / Standard Solid / Standard Outline(Outline Small/Medium/Large 추가, 기존 `size` API 지원) 같은 너비 3열을 미리보기 840px 이상에서, 미만은 1열로 둔다. 그룹명 H3(14/20/500 회색) → 버튼 8px, 버튼 간격 8px. 상태 열과 시작 위치를 맞추려고 그룹은 섹션 가장자리에 둔다(`data-sb-board` 들여쓰기 미사용).
|
||||
- 상태 비교: "States" 제목을 지우고 `StorySectionGrid` 안에 Treatment별 공통 `StorySection`(제목 16/24/700, 제목 → 매트릭스 12px)을 둔다. 미리보기 1400px 이상에서 같은 너비 3열(Solid 매트릭스 420px 기준), 미만은 Brand → Solid → Outline 1열이다. 매트릭스 구조(행 Default/Hover/Pressed/Disabled × 색상), 버튼 수(12 / 20 / 16)는 그대로다.
|
||||
- 검증(6024, 미리보기 1420·844·580·390px = 화면 1600·1024·760·390): 1600에서 Sizes 열과 상태 열이 x 30 / 491 / 953, 너비 437px로 일치하고 매트릭스 내부 스크롤 0이다. 1024에서는 Sizes 3열(열 240px, 행 높이 42px로 줄바꿈 없음)·상태 1열, 760에서는 둘 다 1열이며 내부 스크롤 0이다. 페이지 가로 스크롤은 모든 폭에서 0이다. 미해결: 390px에서 Standard Solid 매트릭스(420px)가 열(343px)보다 넓어 표 영역 안에서만 77px 가로 스크롤된다(기존 동작, 버튼 크기·구조 유지 조건상 불가피). `docs/components/button.md` Story 구성 설명을 갱신했다. `tsc -b` 통과. `.storybook/qa` 인벤토리는 재생성하지 않았다.
|
||||
|
||||
## 2026-11-13 — 조직도 › 개요 첫 섹션 제목 → 콘텐츠 25 → 12px (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 원인: Storybook 미리보기 `src/org-chart/core-component-preview.tsx`가 만든 Shadow DOM에서 `hideLogoHeading`으로 패키지 로고 범례의 `h2`를 지우면, `ul.hm-orgchart-logo-list`의 브라우저 기본 `margin-top: 1em`(13px)이 섹션 제목 바로 아래 남았다. 패키지 CSS에는 이 margin 규칙이 없고, 패키지에는 공식 확장 지점(`::part`·slot·추가 스타일시트 옵션)이 없다.
|
||||
- 수정: 같은 `hideLogoHeading` 분기에서 남은 로고 목록의 `style.marginTop = '0'`만 지정했다. 사용처는 `org-chart-page-previews.tsx:44`(개요 첫 섹션) 하나다. 패키지 CSS·API·토큰·다른 Story는 변경 없다. 아래쪽 기본 margin 13px은 다음 섹션의 28px과 상쇄되어 보이지 않으므로 그대로 둔다.
|
||||
- 검증(6024, 1600·1024·760·390px): 개요 세 섹션 모두 제목 → 콘텐츠 12px, 섹션 사이 28px이다. 로고 7개, 목록 gap 16px, 열 수 7 / 4 / 2 / 1, 가로 스크롤 0. `tsc -b` 통과. 바로 아래 항목의 보류 B는 해결되었다.
|
||||
|
||||
## 2026-11-13 — Storybook 메뉴별 공통 스타일 정합성 점검·A/C 수정 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 점검: 사이드바에 보이는 80개 Story(verification·internal 태그 제외)를 미리보기 폭 1420·390px에서 측정했다. 모두 공통 틀(`story-page.css`)을 쓰고, 77개는 좌우 30/16px·제목 → 콘텐츠 12px·섹션 사이 28px·제목 Border 0으로 일치한다. 공통 섹션이 없는 단일 미리보기 Story(SW Overview·속성창 비교·Viewer 콘텐츠 변형·확장 조직도·연계 시설물)는 고유 구조로 유지했다.
|
||||
- A. SW › 뷰어 › 화면 너비: 숨김 콜백 기록 `<p hidden>`이 H1과 첫 블록 사이에 있어 공통 첫 블록 reset이 빠지고, 스택의 28px margin-top이 남았다(PC 52px / 모바일 44px 시작). `src/sw/sw-stories.css`에 `.sb-page > .sb-page-inner > .sb-page-a11y-title:first-child + [hidden] + .sw-story-stack { margin-top: 0; }`만 추가했다(공통 CSS 미변경). 이 구조는 이 Story에만 있다. 다른 Story는 모두 이미 시작 위치가 같았다. 결과: 1600·1024·760·390px에서 시작 24/17/16/16px(공통 gutter `clamp(16px, 2vw, 24px)`와 일치), 제목 → 콘텐츠 12px.
|
||||
- C. 노선도 › 연계 시설물: `.sb-center--plain .linked-facility__group` gap 16 → 0. 도로 등급 제목 → 카드 격자 28 → 12px(헤더 padding 12px). 카드 들여쓰기 16px, 그룹 사이 28px, 열 수(1600 3열·1024 2열·760/390 1열), 그래픽은 유지된다. 가로 스크롤·넘침 0.
|
||||
- 보류 B. 조직도 › 개요 › 관계사·회사 로고: 제목 → 콘텐츠 25px(+13px). 원인은 `@hm-design/org-chart`의 Shadow DOM 안 `.hm-orgchart-logo-list`(`packages/org-chart/src/styles/org-chart-content.css`)에 남은 브라우저 기본 `ul` margin(1em)이다. 패키지 CSS라 소비 프로젝트 영향을 검토한 뒤 결정하기로 하고 수정하지 않았다.
|
||||
- 기존 문제(패키지 컴포넌트 최소 폭, 이번 변경과 무관): SW 하단 명령어바 전체·화면 너비 Story는 390px에서 가로 넘침 203/218px(`.hm-sw-footer__command` min-width 280px). SW Viewer 로고·화면 너비 Story는 390px에서 2px(`.hm-sw-viewer-header__actions`).
|
||||
- `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — 공통 섹션 제목 하단 Margin 0 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/components/story-page.css`의 `.sb-page-section-header`: `margin-bottom` 16px → 0. `padding-bottom` 12px은 유지하고 Border는 없다. 그래서 섹션 제목(설명이 있으면 설명)에서 콘텐츠까지 28px → 12px이다. 4px 안은 사용자가 취소했다.
|
||||
- `src/foundation/foundation.css`: 바로 아래 항목에서 넣은 Font Family·Font Weight 전용 `margin-bottom: 10px` 예외를 지워 공통 기준(12px)을 따르게 했다. Specimens 그룹 간격(H3 → 첫 구분선 8px, 그룹 사이 24px)과 행 좌우 12px은 그대로다.
|
||||
- CENTER의 `.sb-center … > header`는 자체 margin 0·padding을 쓰는 Story 고유 구조라 변경하지 않았다. 다른 Story CSS에는 섹션 헤더 margin 재정의가 없다.
|
||||
- 검증(6024, 미리보기 폭 1420·844·580·390px): 한맥 컬러·가족사 컬러·폰트·Button·Input·데이터 테이블은 모든 섹션 12px, 막대 차트는 11~12px이다. SW Header Responsive More도 헤더 하단과 본문 시작이 일치해 12px이다. 가로 스크롤 0. 예외로 SW Footer 전체 Story는 390px에서 기존 Footer 명령 영역(280px) 때문에 가로 넘침이 있다. 세로 Margin 변경과 무관하다. `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — Storybook 콘텐츠 좌우 여백 60 → 30px·SW 기준점 재계산·폰트 섹션 상단 간격 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/components/story-page.css`의 `.sb-page-inner`: 좌우 여백(각각)을 `clamp(16px, 16px + (100vw − 480px) × 11/72, 60px)`에서 `clamp(16px, 16px + (100vw − 480px) × 7/144, 30px)`로 바꿨다. 최대값만 절반이고 모바일 16px은 유지한다. 실측(미리보기 폭 기준)은 1420·844px에서 60 → 30px, 580px에서 약 21px, 390px에서 16px이다. LNB(180px)·manager CSS는 변경하지 않았다.
|
||||
- SW Story 전용 기준점(Inner 좌우 60px 전제였음을 주석과 계산으로 확인): `sw-stories.css`의 1920 행 container 2039.98 → 1979.98px(1920 + 60), 속성창 비교 3열 1125 → 1065px(990 + 60 + 15), 1열 전환 786.98 → 726.98px(652 + 약 28 × 2 + 15 = 723). `sw-footer-stories.css`의 같은 1920 container 조건도 1979.98px. 관련 주석을 갱신했다. 패키지 SW 컴포넌트는 변경하지 않았다.
|
||||
- `src/foundation/foundation.css`: Font Family·Font Weight 섹션 제목 아래 여백 16 → 10px(`.sb-page.hm-foundation .hm-typo-type-grid .sb-page-section-header`)으로, 제목에서 첫 행까지 28 → 22px이다. Specimens는 독립 열 유지가 확정되었다. 이미 그룹 제목 → 첫 구분선 8px, 그룹 사이 24px로 통일되어 있어 수정하지 않았다.
|
||||
- 검증(6024): 폰트 Story를 1600·1024·760·390px에서 확인했다. 제목 → 첫 행 22/22px, 그룹 8/24px, 행 padding 12px, 구분선 유지, 섹션 Border 0, 가로 스크롤 0, 넘침은 화면 읽기용 H1뿐이다. 한맥 컬러·Button·Input·막대 차트·데이터 테이블의 4개 폭에서 여백 30/30/21/16px, 가로 스크롤 0, 렌더 오류 0이다(Button의 `sr-only` 3개는 기존). SW 속성창 비교는 1064px 2열 → 1065px 3열, 726px 1열 → 727px 2열이며 넘침 0이다. Header Responsive More는 1979px 숨김 → 1980px 1920 행 표시. Footer·Viewer는 세로 스크롤바 15px 때문에 1995px에서 표시(1990px 숨김)되며 이는 기존 container 기준 그대로다. Step Header Responsive는 1995px 표시, 가로 스크롤 0이다. `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — Foundation 폰트 목록 행 좌우 12px 안쪽 여백 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `src/foundation/foundation.css`에 `.hm-foundation .hm-typo-row { padding-inline: 12px; }` 한 줄만 추가했다. 이 클래스는 폰트 Story에서만 쓴다(Font Family·Font Weight·Specimens 모든 행).
|
||||
- 행 구분선은 각 행의 `border-top`이라 시작점과 너비가 그대로이고, 안쪽 텍스트만 12px 들어간다. 섹션 제목·그룹 H3 위치, 세로 간격, 폰트·토큰 값, 컬럼 규칙(`min(240px, 40%)` / `1fr`, 440px container 전환)은 유지한다. 공통 섹션 제목 Border는 다시 넣지 않았다.
|
||||
- 검증(6024, 미리보기 폭 1420·844·580·390px = 화면 1600·1024·760·390px): 14개 행 모두 padding 12px, 행 시작 x와 그룹 H3 x가 같고 텍스트는 +12px이다. 섹션 제목 위치는 변화 없다. 자식 요소가 좌우 12px 안쪽을 벗어나는 경우 0, 잘림 0, 가로 스크롤 0. 390px에서는 기존대로 1열(미리보기 → 정보)이다. `tsc -b` 통과.
|
||||
|
||||
## 2026-11-13 — Storybook LNB 조밀화·섹션 제목 구분선 제거·Default 컬러 구분 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 모두 Storybook 표시 전용이며 패키지 CSS·토큰·Story 파일은 바꾸지 않았다. 신규 토큰도 없다.
|
||||
- `.storybook/manager.css`(180px 매니저 사이드바): 좌우 여백 20 → 14px, 1뎁스·섹션 행 48 → 40px, 2뎁스 이상 42 → 36px, line-height 28 → 24px, 행 패딩 `8px 14px 8px 10px`(우측 32 → 14px), 아이콘·텍스트 간격 12 → 8px, 들여쓰기 32/44 → 26/36px(1뎁스 leaf 26px), 섹션 구분 간격 24 → 16px. 선택 상태 Radius 8 → 6px, 연결 곡선 `--hm-manager-join` 18 → 12px, 그룹 시작·끝 Radius·하단 여백 6px. 1뎁스 라벨은 chevron 유무와 관계없이 x=40에 정렬된다.
|
||||
- `managerHead`가 서버 시작 시 `manager.css`를 읽으므로 6024를 재시작해야 반영된다. 이번 검증은 현재 파일 내용을 `#hm-manager-style`에 주입해 확인했다.
|
||||
- `src/components/story-page.css`: 모든 Story 공통 섹션 제목(`.sb-page-section-header`)의 하단 구분선만 제거했다. 12px + 16px 간격은 유지한다. `src/center/center-stories.css`의 연계 시설물 그룹 제목(섹션 제목 모양)도 같은 기준으로 구분선을 제거했다.
|
||||
- `src/foundation/foundation.css`: 한맥 컬러의 Default 칩 좌우에 2px 페이지 배경색(`--hm-sem-bg-content`) 경계를 바깥 그림자로 겹쳐 그린다. 칩 너비(1024px에서 모두 177px)·값·순서·텍스트는 같고, 줄 시작에서는 경계가 페이지 배경에 묻힌다. 가족사 컬러에는 Default가 없어 영향이 없다.
|
||||
- 검증(6024): 1600(8칸)·1024(4칸)·760(4칸)·390(2칸)px 컬러 화면, 매니저 선택·펼침 그룹·Hover, 390px 모바일 메뉴 행 40px, Input Story의 섹션 제목 확인. `tsc -b` 통과.
|
||||
- 후속 검증(6024 재기동 후): 6024는 이미 내려가 있어 `.claude/launch.json`의 `storybook` 구성으로 새로 띄웠다. 주입 없이 `#hm-manager-style`에 새 CSS가 들어갔다(join 12px, 콘텐츠 여백 `20px 0 20px 14px`). 행 높이는 섹션·1뎁스 40px, 2뎁스 36px, 그룹 끝 42px(36 + 하단 6px)이고 라벨 x는 섹션 36·1뎁스 40·2뎁스 50이다. 선택(입력창)은 36px, Radius `6px 0 0 6px`, 곡선 12px, 배경 `#F5F0E5`이다. Hover는 Black 10%이고, 내비게이션 그룹 펼침(시작 Radius 6px·끝 6px)과 접힘도 정상이다. CENTER `노선도/시설물 › 연계 시설물`(`--plain`)의 그룹 제목 5개는 하단 Border 0, padding 12px 유지, 가로 스크롤 0이다. 공통 섹션 제목을 쓰는 한맥 컬러·가족사 컬러·폰트·입력창·막대 차트·데이터 테이블은 Border 0이고, 제목 → 본문 28px(차트는 설명 포함 52px, 기존 구성)이며 렌더 오류와 가로 스크롤이 없다. SW Header·Footer Overview는 공통 섹션 제목을 쓰지 않는다. 추가 수정은 없다.
|
||||
|
||||
## 2026-11-12 — 노선도 배치·간격을 공통 Storybook 기준으로 정렬 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 기준: Button·Badge 등이 쓰는 `story-page.css`(H2 → 12px + 구분선 → 16px, 섹션 28px, 들여쓰기 16px / 640px 이하 12px)와 `story-board.tsx`(H3 14/20/500 회색, 설명 14px 회색 4px 아래, 그래픽 12px 아래, 행·열 24px).
|
||||
- 변경은 `src/center/center-stories.css`의 `.sb-center.sb-center--plain` 규칙에만 있다. 바로 아래 항목의 카드 제거 블록 뒤에 이어 붙였다. 조직도(`.sb-center`만 사용)·Overview(`plain` 아님)·공통 `story-page.css`·패키지 CSS·Story 파일은 변경 없음.
|
||||
- 적용: `.sb-center` 위아래 8px과 연계 시설물 18px 여백 제거. `개요` 본문 안의 노선도 블록 16px(12px) 들여쓰기. 예제 제목은 500 회색, 설명은 12px → 14px. 공간 화면의 `h4`·`csi-legend`·`csi-note`도 같은 기준이며, 설명은 `-8px` 위쪽 여백으로 제목 아래 4px에 맞춘다. 설명 → 그래픽 16px → 12px. 예제 간격 32px → 24px.
|
||||
- 정렬: 그래픽을 가운데에서 제목과 같은 왼쪽 기준선으로 옮겼다. 데모 stage의 `min-height`(터널·시설물 154px, 사업명 184px, 연계 변형 122px)는 카드 시절 빈 프레임 안에서 그래픽을 가운데에 두던 값이라, plain 화면에서만 0으로 두어 그래픽 높이를 따른다. 그래픽 자체의 크기·색·구성, 로컬 가로 스크롤, 760px 이하 1열은 그대로다. `tunnel/facility-reference__row`의 `space-evenly`는 `flex-start`로 바꿨다.
|
||||
- 연계 시설물: 도로 등급 그룹 제목은 섹션 제목 모양(16/24/700, 12px + 구분선, 그룹 28px 간격, 헤더 → 격자 16px). 카드 격자는 16px(12px) 들여쓰기, `minmax(min(100%, 320px), 1fr)`를 써서 760px에서도 1열이다. 배율 안내·맥락 띠는 섹션 앞 블록이라 들여쓰지 않는다.
|
||||
- 검증(6024): 보이는 노선도 Story 10개와 숨김 Overview를 1024·760·390px에서 확인했다. 페이지 가로 스크롤 0, 넘친 텍스트 0, 콘텐츠 시작 x는 Button과 같다(76 / 75 / 28). 정보·사업명·공간·터널 계열의 그래픽 내부 로컬 스크롤은 변경 전과 같은 기존 동작이다. 조직도 Expanded·Department Colors에는 `--plain`이 없다. `npm run typecheck` 통과.
|
||||
|
||||
## 2026-11-12 — 노선도 Story 샘플 카드 박스 제거 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 대상: 노선도 Story 5개(시설물·노선·공간·사업명·정보·연계 시설물). 샘플 카드 프레임(배경·테두리·Radius), 카드 머리 띠(배경·하단 테두리), 연계 시설물 위치도/상세도 사이 가로선을 없앴다. 제목·설명 텍스트와 그래픽(시설물 SVG, 정보 카드, 사업명 박스, 노선명 배지, 범위 카드, 공간 단면 `#efefef` 캔버스)은 유지한다.
|
||||
- 방식: `src/center/center-shared.tsx`의 `CenterFrame`에 `plain` 옵션을 두고 `withRepresentation`만 `sb-center--plain`을 붙인다. `src/center/center-stories.css`의 새 규칙은 모두 `.sb-center.sb-center--plain` 아래에만 있다. 조직도 Department Colors·Expanded가 `.sb-center`·`.tunnel-reference__sample`·`--sb-center-card-*`를 재사용하므로 기존 공통 카드 규칙과 변수는 그대로 둔다. Overview(검증용)는 `CenterFrame`을 직접 써서 기존 카드가 유지된다.
|
||||
- 간격: 항목 32px, 2열·연계 카드 격자 24px, 제목↔설명 4px, 그래픽 위 16px(좌우·아래 0). 가운데 정렬·로컬 가로 스크롤·760px 이하 1열 전환은 기존 규칙 그대로. 패키지(`packages/center-graphic`) 변경 없음.
|
||||
|
||||
## 2026-11-12 — 사이드바 표시명 `팝업 메뉴`·`테이블` (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- `components-overlay-dropdown-menu` 표시명 `드롭다운·컨텍스트 메뉴` → `팝업 메뉴`(180px 사이드바 1줄). 화면 제목 "Dropdown Menu · Context Menu"·섹션·Story ID·정렬은 불변. 같은 이름을 쓰던 `docs/components/dropdown-menu.md`·`context-menu.md`와 Story 주석 3곳도 맞춤.
|
||||
- `patterns-data-data-table` 표시명 `데이터 테이블` → `테이블`(manager.js만). `컴포넌트 > 데이터 표시 > 테이블`(Table 컴포넌트)과 그룹은 다르지만 같은 이름이다(사용자 결정).
|
||||
- 인벤토리는 Story 이름이 아니라 manager 라벨만 바뀌어 재생성 불필요. manager.js 변경은 Storybook dev 서버 재시작 후 반영.
|
||||
|
||||
## 2026-11-12 — Storybook 대화상자·드롭다운/컨텍스트 메뉴 사이드바 통합, 데이터 테이블 메뉴명 (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 방식: Story 파일 4개(`src/components/dialog`·`alert-dialog`·`dropdown-menu`·`context-menu.stories.tsx`)와 모든 Story ID 유지. 통합되는 쪽의 기존 대표 Story `All`(`components-overlay-alert-dialog--all`, `components-overlay-context-menu--all`)에 `verification` 태그만 붙여 사이드바에서 숨긴다(URL 직접 접근·검증 Story·`play` 유지).
|
||||
- 공용 예제(Storybook 전용, 중복 구현 없음): `src/components/alert-dialog-examples.tsx`(`AlertDialogConfirmation`, `AlertDialogDestructive`), `src/components/context-menu-examples.tsx`(`ContextMenuFull`). 원래 Story 파일에서 옮겼고 두 Story 파일이 함께 import한다.
|
||||
- 대화상자: Dialog 대표 화면에 "Alert Dialog" 섹션(로그아웃·삭제 트리거) 추가 → 트리거 8개. 사이드바 `대화상자` 하나.
|
||||
- 드롭다운·컨텍스트 메뉴: Dropdown Menu 대표 화면 제목 "Dropdown Menu · Context Menu", 섹션 "Dropdown Menu"(트리거)·"Context Menu"(우클릭 영역). 사이드바 표시명 `드롭다운·컨텍스트 메뉴`(Navigation Menu의 `메뉴`와 구분, 180px 사이드바에서 2줄; 이후 `팝업 메뉴`로 변경, 위 항목). `manager.js`의 `components-overlay-alert-dialog`·`components-overlay-context-menu` 라벨은 태그 필터로 숨김 Story를 볼 때를 위해 유지.
|
||||
- `patterns-data-data-table` 표시명 `테이블 패턴` → `데이터 테이블`(manager.js만).
|
||||
- 문서: `docs/components/dialog.md`·`alert-dialog.md`·`dropdown-menu.md`·`context-menu.md`의 대표 Story 표와 §9. 인벤토리는 `node .storybook/qa/generate-inventory.mjs`로 재생성(PRIMARY 82 → 80, Story 수 387 불변).
|
||||
- 검증 메모: 브라우저 창이 화면에 그려지지 않을 때 닫힘 animation이 늦게 끝나 overlay가 1초 이상 DOM에 남는다. 닫힘 판정은 `data-state`나 충분한 대기 후 DOM 제거로 확인한다.
|
||||
|
||||
## 2026-11-12 — Storybook 메뉴명 6건·사이드바/LNB 1뎁스 왼쪽 Padding 12px (브랜치 `style/storybook-ui-refinements`, 미커밋)
|
||||
|
||||
- 메뉴명(사이드바 표시명만, Story ID·export·title·본문 제목·정렬 순서 불변): `.storybook/manager.js` 라벨 4건(내비게이션 메뉴 → 메뉴, 데이터 테이블 → 테이블 패턴, 전체 메뉴 (GNB) → 상단 메뉴, 좌측 메뉴 (LNB) → 좌측 메뉴), Story `name` 2건(`sw-footer--full-footer` SW 하단 명령어바 → SW 명령어바, `center-graphic-facility--roadside` 휴게소 · 졸음쉼터 → 연계 노선). SW 정렬은 storySort가 Story 이름을 쓰므로 `.storybook/preview.tsx`의 `["SW 명령어바", "*"]`도 함께 변경. 본문 `StoryPageHeader` 제목은 그대로(Roadside는 `page()` 첫 인자, Footer는 별도 literal).
|
||||
- Storybook 사이드바: `.storybook/manager.css` 기존 규칙 `#storybook-explorer-tree .sidebar-item > :is(a[id], button[id]:not([data-testid]))`의 padding을 `10px 32px 10px 12px`로(이전 왼쪽 18). 더 구체적인 기존 규칙은 유지: 2뎁스 이상 padding-block 7px, 하위 없는 1뎁스 leaf 왼쪽 32px.
|
||||
- LNB: `--hm-lnb-item-padding-start` 18 → 12px(`packages/ui/src/theme/lnb.css`, 기준값 `src/theme/verification-baseline.json`, 문서 `docs/patterns/lnb.md`). 1뎁스 행 padding `10 32 10 12`(오른쪽 = inset-end 20 + 12). 2뎁스 글자 x 88·중첩 그룹 trigger 58·3뎁스 108은 불변. 2뎁스 링크 배경 시작(`--hm-lnb-subitem-surface-inset`)은 이 토큰으로 계산되어 x 68 → 62(1뎁스 그룹 글자 시작과 정렬 유지).
|
||||
- Storybook은 `@hm-design/ui`를 `packages/ui/dist`로 읽으므로 LNB 반영에 `npm run build:ui`가 필요하다(dist는 gitignore). 소비 프로젝트는 dist 재빌드 + Submodule 갱신 후 반영.
|
||||
- manager.js/manager.css 변경은 Storybook dev 서버 재시작 후 반영된다. 인벤토리는 `node .storybook/qa/generate-inventory.mjs`로 재생성(차이는 이름 2건뿐).
|
||||
|
||||
## 2026-11-12 — Foundation 한맥·가족사 컬러칩, 폰트 미니멀 타이포그래피 레이아웃 (브랜치 `style/storybook-ui-refinements`)
|
||||
|
||||
- 범위: `src/foundation/`(`color.stories.tsx`·`company-colors.stories.tsx`·`typography.stories.tsx`·`reference.tsx`·`foundation.css`)과 `docs/guidelines/brand-color-steps.md`. 토큰·HEX·`themes/*.json`·컴포넌트·패키지 공개 API·`reference-data.ts`·Story ID(`foundation-color--all`·`foundation-company-colors--all`·`foundation-typography--all`) 불변. 폰트 섹션 순서(Text Colors → Font Family → Font Weight → Typography Specimens) 불변.
|
||||
|
||||
@@ -51,6 +51,11 @@ CENTER 도메인(제목 · 정보 표기 · 노선/공간 · 시설물)의 현
|
||||
|
||||
- 시설물 총 너비(기준 이름): 터널 위치도 210 · 실사화 220 (Source reference total과 일치).
|
||||
- 노선: 중심선 18u, 윤곽 중심선의 20%, 그림자 층 순서 Shadow → Outline → Primary Stroke(`CONFIRMED`).
|
||||
- (2026-11-13, Storybook 데모 설명에서 옮김 · `CenterSpatialInformation.css`/`.tsx`와 대조) 윤곽 3.6u. 그림자 X0 Y0 Blur 6u, Opacity 70%. 윤곽색은 지방도 · 구도·면도·이도가 Dark Outline(#000 / 30%), 나머지 분류는 White Outline. 상태 표현: 공사중 = 중심선에 흰 블록 32u / 간격 32u, 계획 = 45° 흰 빗금(+ 흰 워시).
|
||||
- 사업 노선: 중심선 #D21203, 끝 원형 표시 = 선 두께의 150%(18u 선에서 27u), 노선과 같은 윤곽·그림자.
|
||||
- 노선명 배지: 높이 54u, 좌우 padding 9u + 텍스트 너비. 노선번호 기호: 기본 64u. 방면 배지: 높이 50u의 완전히 둥근 캡슐, 흰색 80% 안쪽 윤곽 2u, 7점 화살표 다각형, 콘텐츠 기반 너비.
|
||||
- 정보(`CenterInformationDisplay.css`와 대조): 테이블형 행 높이 = 기본값 + 추가 행마다 26.116u + 추가 항목마다 34.364u(줄 높이 26.116u + 항목 간격 8.248u). 강조형 1의 패널은 카드 뒤로 4u 겹쳐 배치된다.
|
||||
- 휴게소·졸음쉼터 실사화 분리형: 예제 간격 19 / 14(`ROADSIDE_ACTUAL_CONFIG.gaps`). 데모 설명에 있던 조각 구조 13 / 90 / 13은 코드에서 확인하지 못한 Source 표기 값이며 확정값이 아니다.
|
||||
- 글꼴: Noto Sans KR(Canonical). 폰트 파일 배포와 fallback 순서는 `PLATFORM_MAPPING`/`TBD`이며 이 패키지는 폰트를 포함하지 않는다.
|
||||
- 상태: 노선만 상태(기본/공사중/계획)를 가진다. Facility는 정적이며 Selected/Hover가 없다.
|
||||
|
||||
@@ -75,6 +80,8 @@ import { CenterTitleReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
|
||||
`Center Graphic` 그룹(기본 sidebar): `Title`, `Information`, `Spatial`, `Facility`(터널 · 교량 · 교차로 · 분기점 · 나들목 · 휴게소·졸음쉼터 · 연계 시설물), verification 숨김: `Overview`(배율 규칙·상태 표, 직접 URL `center-graphic-overview--overview` 유지), Title `Playground`, Facility `직접 테스트 (터널)`. 제작 대상 해상도는 주소 `?centerResolution=3840x2160`으로 지정한다.
|
||||
|
||||
공통 페이지 레이아웃(2026-11-13, `src/center/center-stories.css`의 `.sb-center--plain` 범위): 별도 `개요` 섹션 제목 없이 예제를 한 번 들여쓰고(`sb-page-indent`), 예제 제목 16/24/700 · 설명(또는 사이즈) 12px 회색을 8px 간격의 한 행에 둔다(공간 부족 시 줄바꿈). 제목 → 그래픽 12px, 예제 사이 24px. 표시 배율 안내는 콘텐츠 오른쪽 끝 정렬이며, 사업명 페이지만 첫 예제 제목 행 오른쪽에 둔다. 데모 설명은 구성명·사용 기준·비교에 필요한 짧은 정보와 상태 표기(`SOURCE_RECEIVED`/`SOURCE_PENDING`, `KNOWN_DEVIATION`, `READY`, 너비 테스트 라벨)만 두고, 제작 수치·SVG 로딩·Source 작성 방식 설명은 화면에서 뺐다(필요한 수치는 §4, 2026-11-13). 예외: 연계 시설물 그룹·카드 구성, 노선·공간의 제목 아래 긴 설명.
|
||||
|
||||
## 8. 미확정 / 보류 (원본 기준)
|
||||
|
||||
center-domain.md §5 registry를 따른다: 표시 배율 Q2b·Q3 `HOLD`, Facility §12.24 `OWNER_REVIEW`, Information 일부 `TBD`/`Candidate`, Title Resolution B `TBD`, 전체 layer order·collision `TBD`, 철도 윤곽 두께·line join `TBD`, `symbol.md` 정확한 기하·팔레트 `TBD`, 폰트 binary 승인 `TBD`. Storybook `Overview`에 상태 표가 있다. 이 구현은 위 항목을 확정하지 않는다.
|
||||
|
||||
@@ -353,7 +353,6 @@ export function CenterInformationDisplayReferenceDemo() {
|
||||
<div className="cid-demo">
|
||||
<Block
|
||||
title="박스형 · 정의 목록 (박스형 설명1)"
|
||||
note="배지 + 그라데이션 레이블 칩 + 값 블록; 하위 불릿은 값 열 유지"
|
||||
>
|
||||
<CenterInformationBox
|
||||
layout="definition"
|
||||
@@ -370,7 +369,7 @@ export function CenterInformationDisplayReferenceDemo() {
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block title="박스형 · 목록 — Source 팔레트 4종" note="지오메트리 동일; 팔레트만 다름">
|
||||
<Block title="박스형 · 목록 — 팔레트 4종" note="지오메트리 동일; 팔레트만 다름">
|
||||
{['navy', 'teal', 'orange', 'violet'].map((palette) => (
|
||||
<CenterInformationBox
|
||||
key={palette}
|
||||
@@ -382,7 +381,7 @@ export function CenterInformationDisplayReferenceDemo() {
|
||||
))}
|
||||
</Block>
|
||||
|
||||
<Block title="테이블형" note="행 높이 = 기본값 + 추가 행마다 26.116 u + 추가 항목마다 34.364 u">
|
||||
<Block title="테이블형">
|
||||
<CenterInformationTable
|
||||
title="사업효과"
|
||||
width="643px"
|
||||
@@ -401,7 +400,7 @@ export function CenterInformationDisplayReferenceDemo() {
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block title="강조형 · 화살표 표시 (강조형 1)" note="패널은 카드 뒤에 4 u 겹침으로 배치되며 투명하게 사라짐">
|
||||
<Block title="강조형 · 화살표 표시 (강조형 1)">
|
||||
<CenterInformationEmphasis
|
||||
title="사업목적"
|
||||
arrow
|
||||
@@ -429,7 +428,7 @@ export function CenterInformationDisplayReferenceDemo() {
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block title="동적 콘텐츠 — 짧은 / 기준 / 긴 / 매우 긴" note="소유 단일행은 말줄임으로 클리핑; 명시적 줄바꿈 유지; 장문이 지오메트리를 늘리지 않음">
|
||||
<Block title="동적 콘텐츠 — 짧은 / 기준 / 긴 / 매우 긴" note="한 줄 항목은 말줄임, 명시적 줄바꿈 유지">
|
||||
{CONTENT_STATES.map((state) => (
|
||||
<div className="cid-demo__state" key={state.key} data-content-test={state.key}>
|
||||
<span className="cid-demo__tag">{state.label}</span>
|
||||
|
||||
@@ -452,11 +452,6 @@ export function CenterSpatialInformationReferenceDemo() {
|
||||
<div className="csi csi-demo">
|
||||
<Panel
|
||||
id="csi-area-route" title="1. 노선 — 노선, 상태, 윤곽"
|
||||
legend={
|
||||
"중심선 18u, 윤곽은 중심선의 20% (3.6u), 그림자 X0 Y0 Blur 6u Opacity 70%. "
|
||||
+ "지방도 · 구도 · 면도 · 이도는 Dark Outline (#000 / 30%), 나머지 분류는 White Outline 적용. "
|
||||
+ "Source SVG를 로드하지 않음: 외피는 spread box-shadow, 공사중은 반복 파선 그라데이션, 계획은 45° 빗금 그라데이션."
|
||||
}
|
||||
note={
|
||||
"KNOWN_DEVIATION — 구도,면도, 이도.svg는 흰색 윤곽으로 내보내며 명시된 Source 규칙과 시안 색상 견본에 모순"
|
||||
+ "여기서는 명시된 규칙 구현"
|
||||
@@ -479,7 +474,6 @@ export function CenterSpatialInformationReferenceDemo() {
|
||||
|
||||
<Panel
|
||||
id="csi-area-project-route" title="1b. 사업 노선 — 사업노선과 끝 표시"
|
||||
legend={"사업 노선 #D21203. 원형 표시 = 선의 150%인 원 (18u 선에서 27u), 노선 윤곽과 그림자 공유."}
|
||||
>
|
||||
<div className="csi-scale-50" style={{ padding: '10px 4px 4px' }}>
|
||||
<ProjectRoute />
|
||||
@@ -489,9 +483,7 @@ export function CenterSpatialInformationReferenceDemo() {
|
||||
<Panel
|
||||
id="csi-area-route-information" title="2. 노선 정보 표기 — 노선번호와 노선명 (동적 텍스트)"
|
||||
legend={
|
||||
'노선 식별에 필요한 경우 노선번호 또는 노선명을 선택하여 표기한다. '
|
||||
+ "아래 문자열은 모두 실제 DOM 텍스트다. Source SVG에는 윤곽 글리프 경로로 기록되어 있지만 해당 지오메트리는 런타임에서 사용하지 않음. "
|
||||
+ "노선명 너비 = padding 9u + 고유 텍스트 너비 (높이 54u 고정). 노선번호 기호는 배지 기본 64u로 고정."
|
||||
'노선 식별에 필요한 경우 노선번호 또는 노선명을 선택하여 표기한다.'
|
||||
}
|
||||
note={
|
||||
"KNOWN_DEVIATION — 고속도로 노선명.svg 높이는 48u이며 고속도로/특별시도 이름 표면 (#2E619A / #9729E5)은 선 색상과 다름"
|
||||
@@ -530,8 +522,7 @@ export function CenterSpatialInformationReferenceDemo() {
|
||||
<Panel
|
||||
id="csi-area-direction" title="3. 방면 표기 — 방면 배지"
|
||||
legend={
|
||||
'노선의 진행 방향을 안내할 경우 화살표와 방면명을 조합한 배지형으로 표기한다. 노선 유형과 관계없이 동일한 형태로 적용한다. '
|
||||
+ "완전히 둥근 50u 캡슐, 흰색 80%의 안쪽 윤곽 2u, 7점 화살표 다각형, 콘텐츠 기반 너비."
|
||||
'노선의 진행 방향을 안내할 경우 화살표와 방면명을 조합한 배지형으로 표기한다. 노선 유형과 관계없이 동일한 형태로 적용한다.'
|
||||
}
|
||||
>
|
||||
<div className="csi-scale-50" style={{ position: 'relative', padding: '4px 0 18px' }}>
|
||||
|
||||
@@ -422,8 +422,8 @@ function FacilityReferenceDemo({ config, alternateName }: { config: FacilityConf
|
||||
return (
|
||||
<div className="facility-reference" data-reference-facility={config.id}>
|
||||
<div className="facility-reference__variant-grid">
|
||||
<Sample title="Position Map" note="로컬 SVG · 위치도"><LocalAssetFacility config={config} /></Sample>
|
||||
<Sample title="Actual / Detail" note="로컬 SVG · 실사화"><LocalAssetFacility config={config} variant="actualDetail" /></Sample>
|
||||
<Sample title="Position Map" note="위치도"><LocalAssetFacility config={config} /></Sample>
|
||||
<Sample title="Actual / Detail" note="실사화"><LocalAssetFacility config={config} variant="actualDetail" /></Sample>
|
||||
</div>
|
||||
|
||||
{alternateName ? (
|
||||
@@ -435,14 +435,14 @@ function FacilityReferenceDemo({ config, alternateName }: { config: FacilityConf
|
||||
</Sample>
|
||||
) : null}
|
||||
|
||||
<Sample title="Separated" note="Source 작성 부분 간격">
|
||||
<Sample title="Separated">
|
||||
<div className="facility-reference__row">
|
||||
<LocalAssetFacility config={config} separated />
|
||||
<LocalAssetFacility config={config} variant="actualDetail" separated />
|
||||
</div>
|
||||
</Sample>
|
||||
|
||||
<Sample title="너비 동작" note="Source 정의 가변 영역만 확장">
|
||||
<Sample title="너비 동작" note="가변 영역만 확장">
|
||||
<div className="facility-reference__width-tests">
|
||||
{widthNames.map((name, index) => (
|
||||
<div key={name}><span>{['SHORT', 'MEDIUM', 'LONG'][index]}</span><LocalAssetFacility config={config} name={name} /></div>
|
||||
@@ -473,7 +473,7 @@ export function RoadsideReferenceDemo() {
|
||||
|
||||
return (
|
||||
<div className="facility-reference roadside-reference" data-reference-facility="roadside">
|
||||
<Sample title="Position Map" note="재구성 Primitive · 콘텐츠 기반 너비">
|
||||
<Sample title="Position Map" note="콘텐츠 기반 너비">
|
||||
<div className="roadside-tag-tests">
|
||||
{tests.map((test) => (
|
||||
<div key={test.size} className="roadside-tag-test" data-roadside-test={test.size.toLowerCase()}>
|
||||
@@ -484,7 +484,7 @@ export function RoadsideReferenceDemo() {
|
||||
</div>
|
||||
</Sample>
|
||||
|
||||
<Sample title="Actual / Detail" note="Source 작성 그라데이션/filter 그래픽 · 고정 양 끝 + 가변 중앙">
|
||||
<Sample title="Actual / Detail" note="고정 양 끝 + 가변 중앙">
|
||||
<div className="roadside-tag-tests">
|
||||
{tests.map((test) => (
|
||||
<div key={test.size} className="roadside-tag-test" data-roadside-actual-test={test.size.toLowerCase()}>
|
||||
@@ -495,11 +495,11 @@ export function RoadsideReferenceDemo() {
|
||||
</div>
|
||||
</Sample>
|
||||
|
||||
<Sample title="Actual / Detail · Separated" note="Source 작성 13 / 90 / 13 구조, 예제 간격 19 / 14">
|
||||
<Sample title="Actual / Detail · Separated">
|
||||
<RoadsideFacilityTag entity="drowsy-shelter" variant="actualDetail" separated />
|
||||
</Sample>
|
||||
|
||||
<Sample title="연계 / 노선 맥락" note="노선 + 앵커 + 연결선 + 같은 시설물 태그 계약">
|
||||
<Sample title="연계 / 노선 맥락" note="노선 + 앵커 + 연결선 + 시설물 태그">
|
||||
<RoadsideAppliedExample />
|
||||
</Sample>
|
||||
</div>
|
||||
|
||||
@@ -178,15 +178,15 @@ export function TunnelReferenceDemo() {
|
||||
return (
|
||||
<div className="tunnel-reference">
|
||||
<div className="tunnel-reference__variant-grid">
|
||||
<TunnelSample title="Position Map" note="로컬 SVG · 위치도">
|
||||
<TunnelSample title="Position Map" note="위치도">
|
||||
<TunnelFacility variant="positionMap" />
|
||||
</TunnelSample>
|
||||
<TunnelSample title="Actual / Detail" note="로컬 SVG · 실사화">
|
||||
<TunnelSample title="Actual / Detail" note="실사화">
|
||||
<TunnelFacility variant="actualDetail" />
|
||||
</TunnelSample>
|
||||
</div>
|
||||
|
||||
<TunnelSample title="Separated" note="같은 개체 · Source 작성 부분 간격">
|
||||
<TunnelSample title="Separated">
|
||||
<div className="tunnel-reference__row">
|
||||
<TunnelFacility variant="positionMap" separated />
|
||||
<TunnelFacility variant="actualDetail" separated />
|
||||
|
||||
@@ -61,7 +61,7 @@ type HMDonutDatum = { id: string; label: string; value: number; color?: string }
|
||||
- 색상을 지정하지 않으면 `--hm-chart-1`~`5`(HM 그린·브라운·웜 뉴트럴 팔레트 별칭)를 순서대로 사용합니다. 다른 색은 `color`에 CSS 색상/토큰을 전달합니다.
|
||||
- 범례 라벨은 짧게 유지합니다. Bar·Line·Radar·Area의 범례는 한 줄로 두며, 직접 `flex-wrap`으로 줄바꿈시키면 Recharts가 범례 높이를 과대 측정해 차트 아래에 빈 공간이 생길 수 있습니다. Donut·Pie·Stacked Pie는 컴포넌트 내부에서 줄바꿈 범례를 쓰도록 구성되어 있고, Radial 계열은 범례를 차트 박스 밖 아래에 따로 놓습니다.
|
||||
- 모든 차트의 `label`은 필수 접근성 이름(`role="group"`의 `aria-label`)입니다.
|
||||
- 차트는 부모 폭을 따라 줄어듭니다(`w-full`, 높이 `h-64` → `sm:h-80`). `className`으로 높이를 바꿀 수 있습니다. 부모에 `min-width: 0`이 없으면 flex·grid 안에서 넘칠 수 있습니다.
|
||||
- 차트는 부모 폭을 따라 줄어듭니다(`w-full`, 높이 `h-64` → `sm:h-80`; Donut·Pie·Radar는 `h-72` → `sm:h-80`, Radial은 `300px`). `className`으로 높이를 바꿀 수 있습니다. 부모에 `min-width: 0`이 없으면 flex·grid 안에서 넘칠 수 있습니다.
|
||||
|
||||
## 4. 렌더링
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
import http from 'node:http'
|
||||
import { swStoryEntries, retiredPropertyPanelIds, retiredUnusedDemoIds, approvedAddedStoryIds, headerInventoryIssues } from './storybook-inventory.mjs'
|
||||
import { swStoryEntries, retiredPropertyPanelIds, headerInventoryIssues, swScopeIssues } from './storybook-inventory.mjs'
|
||||
import { loadPlaywright } from './playwright-runtime.mjs'
|
||||
|
||||
// Property panel comparison consolidation (2026-10-19).
|
||||
@@ -39,7 +39,7 @@ 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 })),
|
||||
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 ?? c.getAttribute('aria-label') })),
|
||||
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 },
|
||||
}
|
||||
@@ -70,12 +70,15 @@ try {
|
||||
// approved additions, the verification tags and the Popup move).
|
||||
const recorded = JSON.parse(execFileSync('git', ['show', 'c8e5fbeebc26fba92998fc5ddcd28c6c110e452d:docs/sw/property-panel-comparison-qa/verify/result.json'], { encoding: 'utf8', maxBuffer: 1 << 26 })).data.inventory
|
||||
const now = headerInventoryIssues(before.sw, index)
|
||||
// 2026-11-13 (user request): the former whole-Storybook count (338 - retired + approved SW additions) failed whenever a
|
||||
// Story outside SW was added; the SW Story set is checked instead (swScopeIssues: no unapproved SW addition, deletion or ID change).
|
||||
const swScope = swScopeIssues(before.sw, index)
|
||||
report.data.inventory.recorded = { total: recorded.total, sw: recorded.sw }
|
||||
check('inventory: 24 -> 19 SW Stories, 343 -> 338 total, only 5 approved retirements (recorded at c8e5fbe); now only approved later changes', before.sw.length === 24 && recorded.sw === 19 && before.total === 343 && recorded.total === 338 &&
|
||||
JSON.stringify(recorded.ids.map(([id]) => id).sort()) === JSON.stringify(expected.map(e => e.id).sort()) && retiredPropertyPanelIds.every(id => !index.entries[id]) &&
|
||||
stories.length === 338 - retiredUnusedDemoIds.length + approvedAddedStoryIds.length && now.unexpected.length === 0 && now.missing.length === 0 && now.retiredPresent.length === 0, { before: [before.total, before.sw.length], recorded: [recorded.total, recorded.sw], after: [stories.length, sw.length], ...now })
|
||||
swScope.ok && now.unexpected.length === 0 && now.missing.length === 0 && now.retiredPresent.length === 0, { before: [before.total, before.sw.length], recorded: [recorded.total, recorded.sw], after: [stories.length, sw.length], ...now, swScope })
|
||||
check('inventory: retained SW Story IDs, names, titles, tags unchanged (recorded at c8e5fbe); now only the approved tags and Popup move', JSON.stringify(recorded.ids) === JSON.stringify(expected.map(e => [e.id, e.name])) && now.changed.length === 0, { retained: sw.length, changed: now.changed })
|
||||
check('inventory: compare Story keeps its ID and name', index.entries['sw-header--property-panel-compare']?.name === 'Property Panel')
|
||||
check('inventory: compare Story keeps its ID and name', index.entries['sw-header--property-panel-compare']?.name === '속성창')
|
||||
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
|
||||
@@ -93,7 +96,8 @@ try {
|
||||
// 2026-10-26 (user request): from 1080px, 3 columns — none/all stacked in column 1, circle and expanded beside them.
|
||||
// 2026-11-05: the Story page Inner keeps 60px on each side (story-page.css), so the steps moved to 787px / 1125px
|
||||
// (sw-stories.css .sw-story-compare--grid). It keeps 16px up to 480px (growing to 60px at 768px), so 390px has 358px.
|
||||
const oneColumn = width < 787, threeColumns = width >= 1125
|
||||
// 2026-11-13: the side margin maximum is 30px (story-page.css), so sw-stories.css moved the steps to 727px / 1065px.
|
||||
const oneColumn = width < 727, threeColumns = width >= 1065
|
||||
if (threeColumns) {
|
||||
const gaps = { stack: b.cell.y - a.cell.bottom, col1: c.cell.x - Math.max(a.cell.right, b.cell.right), col2: d.cell.x - c.cell.right }
|
||||
report.data[`gaps${width}`] = gaps
|
||||
@@ -141,20 +145,30 @@ try {
|
||||
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') {
|
||||
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)))
|
||||
}
|
||||
// Scroll the page while open (2026-10-22: no Inner scroller): the fixed overlay must stay attached to its field.
|
||||
const moved = await page.evaluate(() => { const t = scrollY; scrollBy(0, 120); return scrollY - t })
|
||||
// 2026-11-13: at 1440 × 900 the page (3-column layout since 2026-10-26) fits the viewport and cannot scroll, and the Story
|
||||
// preview sets `html { scroll-behavior: smooth }` (src/hm-theme.css), so scrollY read right after scrollBy stayed 0. The
|
||||
// check runs at 1440 × 600, where the page scrolls, with an instant scroll; the viewport and scroll position are restored.
|
||||
{
|
||||
const size = page.viewportSize()
|
||||
await page.setViewportSize({ width: size.width, height: 600 })
|
||||
await page.evaluate(() => scrollTo({ top: 0, behavior: 'instant' }))
|
||||
const { trigger, info } = await openPicker('expanded')
|
||||
const moved = await page.evaluate(() => { const t = scrollY; scrollBy({ top: 120, behavior: 'instant' }); return scrollY - 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 }
|
||||
report.data.dropScroll = { viewport: { width: size.width, height: 600 }, moved, before: info, after }
|
||||
check('Dropdown follows the page scroll (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)))
|
||||
await trigger.evaluate(() => scrollTo({ top: 0, behavior: 'instant' }))
|
||||
await page.setViewportSize(size)
|
||||
}
|
||||
// Select a layer value: callback updates only that panel.
|
||||
{
|
||||
|
||||
@@ -106,11 +106,16 @@ export const menuCleanupHiddenIds = ['sw-header--interactive-states', 'sw-header
|
||||
'sw-viewer--compare-with-sw-header',
|
||||
// 2026-10-22 (user request): SW/Popup keeps only Property Panel in the sidebar.
|
||||
'sw-popup--custom-content', 'sw-popup--multiple-popups']
|
||||
export const renamedEntries = { 'sw-header--overview': 'SW Header', 'sw-footer--full-footer': 'SW Footer',
|
||||
'sw-header--logo-slot': 'Logo', // 2026-10-22 (user request): sidebar "Logo", page title "SW Header — 로고"
|
||||
'sw-header--property-panel-compare': 'Property Panel', // 2026-10-22 (user request): was "Property Panel · 4개 비교"
|
||||
'sw-footer--responsive': 'Responsive More', 'sw-viewer--content-variants': 'Settings Popup',
|
||||
'sw-viewer--responsive': 'Responsive More' }
|
||||
// 2026-11-13 (user request): the expected names follow the current approved Story names (Korean menu names); IDs unchanged.
|
||||
export const renamedEntries = { 'sw-header--overview': 'SW Header', 'sw-footer--full-footer': 'SW 명령어바',
|
||||
'sw-header--logo-slot': '로고', // 2026-10-22 (user request): sidebar "Logo", page title "SW Header — 로고"; now 로고
|
||||
'sw-header--property-panel-compare': '속성창', // 2026-10-22 (user request): was "Property Panel · 4개 비교", then Property Panel
|
||||
'sw-header--responsive-more': '화면 너비',
|
||||
'sw-footer--responsive': '화면 너비', 'sw-viewer--content-variants': '설정창',
|
||||
'sw-viewer--responsive': '화면 너비' }
|
||||
// Approved commit 2378663 (2026-11-09, "localize sidebar menus and hide unused stories"): Logo Slot left the default sidebar
|
||||
// with the `internal` tag (Story ID and direct URL kept). Only that tag is added to the recorded entry.
|
||||
export const internalTaggedIds = ['sw-header--logo-slot']
|
||||
// Every approved SW Story ID added after the unused-demo removal, as the current index lists them.
|
||||
export const approvedAddedStoryIds = [...integratedStoryIds.filter(id => !retiredToolIds.includes(id)), ...homeMenuSidebarIds,
|
||||
...Object.values(movedHomeMenuIds).filter(id => !retiredHomeMenuCompareIds.includes(id)), ...footerStoryIds, ...viewerStoryIds, ...themeStoryIds]
|
||||
@@ -120,6 +125,7 @@ export const withApprovedOrder = entries => [...entries.filter(entry => entry.id
|
||||
// An earlier index entry with the approved tag / Popup move applied (2b0e1a2; the moved Story has no component path).
|
||||
export const withApprovedEntryChanges = entry => {
|
||||
if (renamedEntries[entry.id]) entry = { ...entry, name: renamedEntries[entry.id] }
|
||||
if (internalTaggedIds.includes(entry.id)) entry = { ...entry, tags: [...entry.tags, 'internal'] }
|
||||
if (verificationTaggedIds.includes(entry.id)) return { ...entry, tags: [...entry.tags, 'verification'] }
|
||||
if (!movedToPopupIds.includes(entry.id)) return entry
|
||||
const { componentPath, ...rest } = entry
|
||||
@@ -145,6 +151,20 @@ export const movedHeaderDeclarations = { PropertyPanelCompare: ['src/sw/sw-prope
|
||||
export const laterRetiredHeaderIds = [...retiredPropertyPanelIds, ...retiredUnusedDemoIds]
|
||||
// The approved additions that sit under SW/Header.
|
||||
export const approvedAddedHeaderIds = approvedAddedStoryIds.filter(id => id.startsWith('sw-header--'))
|
||||
// User-approved SW/Step Header Stories (adbb034, 2026-11-03; navigation labels b67a3ec), a SW group added after the lists above.
|
||||
export const stepHeaderStoryIds = ['process-header', 'responsive', 'split-open', 'multi-step', 'fixed-widths', 'process-bar',
|
||||
'multi-step-many-groups', 'multi-step-many-steps', 'states', 'theme-override'].map(id => `sw-step-header--${id}`)
|
||||
// 2026-11-13 (user request): the SW Story set, separate from the whole Storybook count. Every Story whose title starts with
|
||||
// "SW/" must be either a retained SW/Header Story of the earlier list (approved changes applied) or an approved addition, and
|
||||
// every one of those must exist: `unexpected` catches an unapproved SW addition or an ID change, `missing` a deletion or an
|
||||
// ID change. Stories outside SW may be added or removed without affecting it.
|
||||
export const swScopeIssues = (entries, index) => {
|
||||
const retained = retainedInventory(entries).filter(entry => !laterRetiredHeaderIds.includes(entry.id)).map(withApprovedEntryChanges).map(entry => entry.id)
|
||||
const approved = [...new Set([...retained, ...approvedAddedStoryIds, ...approvedAddedHeaderIds, ...stepHeaderStoryIds])]
|
||||
const current = Object.values(index.entries).filter(entry => entry.type === 'story' && entry.title.startsWith('SW/')).map(entry => entry.id)
|
||||
const issues = { actual: current.length, expected: approved.length, unexpected: current.filter(id => !approved.includes(id)), missing: approved.filter(id => !current.includes(id)) }
|
||||
return { ...issues, ok: issues.actual === issues.expected && issues.unexpected.length === 0 && issues.missing.length === 0 }
|
||||
}
|
||||
// An earlier SW/Header entry list against today's index: `changed` (ID, name, title or tags differ from the earlier entry
|
||||
// with the approved changes applied), `unexpected` (SW/Header Stories neither retained nor approved), `missing` (approved
|
||||
// additions not under SW/Header) and `retiredPresent` (an approved retirement back in the index).
|
||||
|
||||
@@ -44,7 +44,7 @@ const keys = Object.keys(afterIndex.entries), firstSW = keys.findIndex(id => id.
|
||||
const swOrder = { previous: keys[firstSW - 1], afterCenterGraphic: afterIndex.entries[keys[firstSW - 1]]?.title.startsWith('Center Graphic/') && !keys.slice(firstSW + 1).some(id => afterIndex.entries[id].title.startsWith('Center Graphic/')), contiguous: keys.filter(id => id.startsWith('sw-')).every((id, i) => keys[firstSW + i] === id) }
|
||||
check('inventory: every non-removed index entry identical (docs and other packages included) except the approved SW changes', outsideSW(others(beforeIndex)) === outsideSW(others(afterIndex)) && JSON.stringify(others(beforeIndex).filter(e => e.id.startsWith('sw-')).map(withApprovedEntryChanges)) === JSON.stringify(others(beforeIndex).filter(e => e.id.startsWith('sw-')).map(e => afterIndex.entries[e.id])) && swOrder.afterCenterGraphic && swOrder.contiguous, swOrder)
|
||||
check('inventory: Dropdown keeps sw-header--composed-controls / Dropdown / SW/Header', afterIndex.entries['sw-header--composed-controls']?.name === 'Dropdown' && afterIndex.entries['sw-header--composed-controls']?.title === 'SW/Header')
|
||||
check('inventory: comparison keeps its ID and name', afterIndex.entries['sw-header--property-panel-compare']?.name === 'Property Panel')
|
||||
check('inventory: comparison keeps its ID and name', afterIndex.entries['sw-header--property-panel-compare']?.name === '속성창')
|
||||
|
||||
// ---- Source: retained Story declarations byte-identical; product, AI template and other packages untouched.
|
||||
const sourcePath = 'src/sw/sw-header.stories.tsx'
|
||||
|
||||
@@ -89,8 +89,12 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const menu = header.getByRole('menu', { name: '추가 도구' })
|
||||
reference = await face(menu, MORE_ITEM)
|
||||
refTrigger = await more.evaluate(element => { const s = getComputedStyle(element); return `${s.borderTopWidth} ${s.borderTopStyle} ${s.borderTopColor} | ${s.borderTopLeftRadius} | ${s.backgroundImage !== 'none' ? s.backgroundImage : s.backgroundColor} | ${s.boxShadow}` })
|
||||
const responsive = await newPage(1920, 1500)
|
||||
// 2026-11-13: 2500px wide (like viewer.browser.mjs), so the 1920 row, shown only on a page of 1980px or more, is visible too.
|
||||
const responsive = await newPage(2500, 1500)
|
||||
await open(responsive, 'sw-viewer--responsive')
|
||||
// 2026-11-13: since 2026-10-26 More shows only once even the narrowest tabs do not fit (below about 520px), so the 520 row
|
||||
// is narrowed in place to 400px first (as viewer.browser.mjs does) to open the same More menu.
|
||||
await responsive.evaluate(async () => { document.querySelector('[data-width="520"]').style.maxWidth = '400px'; await new Promise(resolve => setTimeout(resolve, 400)) })
|
||||
await responsive.locator('[data-width="520"] .hm-sw-viewer-header').getByRole('button', { name: '추가 도구' }).click()
|
||||
const more520 = responsive.getByRole('menu', { name: '추가 도구' })
|
||||
moreMenu = await face(more520, VIEWER_ITEM, '.hm-sw-viewer-menu__label', '.hm-sw-viewer-menu__separator', '.hm-sw-viewer-menu__icon', '.hm-sw-viewer-menu__text')
|
||||
@@ -129,12 +133,17 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const layout = await header.evaluate(element => {
|
||||
const button = element.querySelector('[data-document-open]'), wrapper = button.parentElement
|
||||
return { surface: button.classList.contains('hm-sw-header__document-button') && wrapper.matches('.hm-sw-header__document[data-tool]'), afterLogo: wrapper.previousElementSibling?.classList.contains('hm-sw-viewer-header__brand'),
|
||||
beforeName: wrapper.nextElementSibling?.classList.contains('hm-sw-viewer-header__document'), label: button.querySelector('.hm-sw-viewer-header__open-label')?.textContent, icons: button.querySelectorAll('.hm-sw-viewer-header__open-icon svg').length }
|
||||
beforeName: wrapper.nextElementSibling?.classList.contains('hm-sw-viewer-header__document'), label: button.querySelector('.hm-sw-viewer-header__open-label')?.textContent,
|
||||
// 2026-11-13: the folder icon carries the viewer icon outline (SWViewerIcon, caf4c32 2026-10-21): one icon (glyph) SVG and
|
||||
// one outline copy behind it, nothing else.
|
||||
icons: button.querySelectorAll('.hm-sw-viewer-header__open-icon svg').length, glyphs: button.querySelectorAll('.hm-sw-viewer-header__open-icon .hm-sw-viewer-icon__glyph svg').length,
|
||||
outlines: button.querySelectorAll('.hm-sw-viewer-header__open-icon .hm-sw-viewer-icon__outline svg').length }
|
||||
})
|
||||
check('file open button after the logo: SWHeader document surface, icon + 파일 열기', layout.surface && layout.afterLogo && layout.beforeName && layout.label === '파일 열기' && layout.icons === 1, layout)
|
||||
const iconBox = await openButton.locator('.hm-sw-viewer-header__open-icon').evaluate(element => { const s = getComputedStyle(element), r = element.getBoundingClientRect(); return { style: `${s.borderTopWidth} ${s.borderTopStyle} ${s.borderTopColor} | ${s.borderTopLeftRadius} | ${s.backgroundImage !== 'none' ? s.backgroundImage : s.backgroundColor} | ${s.boxShadow}`, size: [Math.round(r.width), Math.round(r.height)] } })
|
||||
// 2026-10-21 user request: the icon has no border and no face (transparent); 34px slot kept.
|
||||
check('file open icon: no border, transparent, no shadow; 34px slot', iconBox.style.startsWith('0px none') && iconBox.style.includes('rgba(0, 0, 0, 0)') && iconBox.style.endsWith('none') && iconBox.size.join() === '34,34', iconBox)
|
||||
check('file open button after the logo: SWHeader document surface, icon (1 glyph + 1 outline SVG) + 파일 열기', layout.surface && layout.afterLogo && layout.beforeName && layout.label === '파일 열기' && layout.glyphs === 1 && layout.outlines === 1 && layout.icons === 2, layout)
|
||||
const iconBox = await openButton.locator('.hm-sw-viewer-header__open-icon').evaluate(element => { const s = getComputedStyle(element), r = element.getBoundingClientRect(), b = element.closest('[data-document-open]').getBoundingClientRect(); return { style: `${s.borderTopWidth} ${s.borderTopStyle} ${s.borderTopColor} | ${s.borderTopLeftRadius} | ${s.backgroundImage !== 'none' ? s.backgroundImage : s.backgroundColor} | ${s.boxShadow}`, size: [Math.round(r.width), Math.round(r.height)], button: [Math.round(b.width), Math.round(b.height)] } })
|
||||
// 2026-10-21 user request: the icon has no border and no face (transparent). 2026-11-13: the icon area is the 16 × 16 icon
|
||||
// itself with no margin (0840d76, DESIGN_RULES --hm-sw-viewer-icon-size); the click area is the button, at least 34px each way.
|
||||
check('file open icon: no border, transparent, no shadow; 16 × 16 icon, button click area ≥ 34px', iconBox.style.startsWith('0px none') && iconBox.style.includes('rgba(0, 0, 0, 0)') && iconBox.style.endsWith('none') && iconBox.size.join() === '16,16' && iconBox.button.every(side => side >= 34), iconBox)
|
||||
// File-name area (.hm-sw-viewer-header__document): no left padding, right after the file open button; the file-name
|
||||
// button inside keeps its own padding (user decision 2026-10-21).
|
||||
const nameStart = await header.evaluate(element => { const d = element.querySelector('.hm-sw-viewer-header__document'); return [getComputedStyle(d).paddingLeft, Math.round(d.getBoundingClientRect().left - element.querySelector('[data-document-open]').getBoundingClientRect().right)] })
|
||||
@@ -179,18 +188,21 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const openState = width => header.evaluate(async (element, width) => {
|
||||
element.style.width = `${width}px`
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
const button = element.querySelector('[data-document-open]'), icon = button.querySelector('.hm-sw-viewer-header__open-icon').getBoundingClientRect()
|
||||
return { width, label: !!button.querySelector('.hm-sw-viewer-header__open-label'), aria: button.getAttribute('aria-label'), title: button.title, icon: [Math.round(icon.width), Math.round(icon.height)], overflow: element.scrollWidth - element.clientWidth }
|
||||
const button = element.querySelector('[data-document-open]'), icon = button.querySelector('.hm-sw-viewer-header__open-icon').getBoundingClientRect(), area = button.getBoundingClientRect()
|
||||
return { width, label: !!button.querySelector('.hm-sw-viewer-header__open-label'), aria: button.getAttribute('aria-label'), title: button.title, icon: [Math.round(icon.width), Math.round(icon.height)], button: [Math.round(area.width), Math.round(area.height)], overflow: element.scrollWidth - element.clientWidth }
|
||||
}, width)
|
||||
const sweep = []
|
||||
for (let width = 1440; width >= 400; width -= 20) sweep.push(await openState(width))
|
||||
const back = []
|
||||
for (let width = 400; width <= 1440; width += 20) back.push(await openState(width))
|
||||
const firstIcon = sweep.find(item => !item.label)
|
||||
check('1440: icon + 파일 열기; narrower: icon only (tooltip / name 파일 열기, 34px icon slot kept)', sweep[0].label && firstIcon
|
||||
&& sweep.filter(item => !item.label).every(item => item.aria === '파일 열기' && item.title === '파일 열기' && item.icon.join() === '34,34'), { first: sweep[0], firstIcon })
|
||||
// 2026-11-13: at every width the icon stays the 16 × 16 icon area (0840d76) and the button keeps a click area of at least 34px.
|
||||
check('1440: icon + 파일 열기; narrower: icon only (tooltip / name 파일 열기); 16 × 16 icon and ≥ 34px click area at every width', sweep[0].label && firstIcon
|
||||
&& sweep.filter(item => !item.label).every(item => item.aria === '파일 열기' && item.title === '파일 열기')
|
||||
&& [...sweep, ...back].every(item => item.icon.join() === '16,16' && item.button.every(side => side >= 34)), { first: sweep[0], firstIcon, icons: [...new Set([...sweep, ...back].map(item => item.icon.join()))], minButton: [Math.min(...sweep.map(item => item.button[0])), Math.min(...sweep.map(item => item.button[1]))] })
|
||||
check('label comes back at the same widths when the header grows; no overflow', sweep.every(item => back.find(other => other.width === item.width).label === item.label) && [...sweep, ...back].every(item => item.overflow <= 0))
|
||||
await header.evaluate(element => { element.style.width = '520px' })
|
||||
// 2026-11-13: More shows only below about 520px since 2026-10-26 (viewer.browser.mjs), so the header is set to 400px.
|
||||
await header.evaluate(element => { element.style.width = '400px' })
|
||||
await page.waitForTimeout(60)
|
||||
const more = header.getByRole('button', { name: '추가 도구' })
|
||||
await more.click()
|
||||
@@ -223,7 +235,8 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
// ---------- Responsive ----------
|
||||
{
|
||||
// Tall enough that the 520 row and its popups are inside the viewport (the Story page does not scroll them in).
|
||||
const page = await newPage(1920, 1500)
|
||||
// 2026-11-13: 2500px wide so the 1920 row (shown only on a page of 1980px or more) is measured with the others.
|
||||
const page = await newPage(2500, 1500)
|
||||
await open(page, 'sw-viewer--responsive')
|
||||
const headers = page.locator('[data-width] .hm-sw-viewer-header')
|
||||
const report = []
|
||||
@@ -251,12 +264,21 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
check('layer / settings / properties always in the header', report.every(item => JSON.stringify(item.toggles.map(toggle => toggle.name)) === '["레이어","설정","속성"]'), report.map(item => item.toggles.map(toggle => toggle.name)))
|
||||
// 2026-10-21 measured steps: full → icons only → More, by the measured room (no fixed width). With the file open button
|
||||
// after the logo, this Story's content fits fully at 1920 / 1440, as icons at 1280 / 950, and needs More at 860 / 760 / 520.
|
||||
// 2026-11-13: since 2026-10-26 (user request; viewer.browser.mjs) the file name and the Model / Layout tabs give way first,
|
||||
// so 860 / 760 / 520 keep every item as icons without More; More shows only below about 520px (the 520 row narrowed to 400).
|
||||
check('1920 / 1440: full items with labels (전체 보기 shown), no More; 1280 / 950: icons only, no More', report.slice(0, 2).every(item => item.fitShown && !item.more && item.toggles.every(toggle => toggle.label))
|
||||
&& report.slice(2, 4).every(item => !item.more && item.fitShown && item.toggles.every(toggle => !toggle.label && toggle.title)), report.map(item => [item.width, item.fitShown, item.more]))
|
||||
check('860 / 760: icons only (tooltips kept) + More', report.slice(4, 6).every(item => item.more && item.toggles.every(toggle => !toggle.label && toggle.title)), report.slice(4, 6))
|
||||
check('520: icons only, then More for what still does not fit', report[6].more && report[6].toggles.every(toggle => !toggle.label && toggle.title), report[6])
|
||||
check('860 / 760: icons only (tooltips kept), no More', report.slice(4, 6).every(item => !item.more && item.toggles.every(toggle => !toggle.label && toggle.title)), report.slice(4, 6))
|
||||
check('520: icons only (tooltips kept), no More', !report[6].more && report[6].toggles.every(toggle => !toggle.label && toggle.title), report[6])
|
||||
check('logo, file name and toolbar never overlap', report.every(item => item.logoRight <= item.docLeft + 2 + 0.5 && item.docRight <= item.objectLeft + 0.5), report.map(item => [item.width, item.logoRight, item.docLeft, item.docRight, item.objectLeft]))
|
||||
// Every item is either in the header or in More, never both and never lost.
|
||||
// Every item is either in the header or in More, never both and never lost: checked on the 520 row narrowed to 400px.
|
||||
const narrowed = await page.evaluate(async () => {
|
||||
const wrapper = document.querySelector('[data-width="520"]'); wrapper.style.maxWidth = '400px'
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
const header = wrapper.querySelector('.hm-sw-viewer-header')
|
||||
return { width: Math.round(header.getBoundingClientRect().width), overflow: header.scrollWidth - header.clientWidth, more: !!header.querySelector('.hm-sw-viewer-header__more') }
|
||||
})
|
||||
check('520 row narrowed to 400: More shown, no overflow', narrowed.more && narrowed.overflow <= 0, narrowed)
|
||||
const more = headers.nth(6).getByRole('button', { name: '추가 도구' })
|
||||
await more.click()
|
||||
const moreMenu = page.getByRole('menu', { name: '추가 도구' })
|
||||
@@ -264,8 +286,9 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const inHeader = await headers.nth(6).evaluate(element => [...element.querySelectorAll('.hm-sw-viewer-header__object :is(.hm-sw-viewer-header__button:not(.hm-sw-viewer-header__more), [role=radio], .hm-sw-viewer-header__switch-label)')]
|
||||
.map(item => item.getAttribute('aria-label') ?? item.textContent.trim().replace(/[\d,]+$/, '')))
|
||||
const all = ['Model', 'Layout', '전체 보기', '2.5D', '어두운 배경', '레이어', '설정', '속성']
|
||||
check('520: header + More hold every item exactly once (panel toggles never in More)', all.every(name => inHeader.includes(name) !== moreItems.includes(name)) && !moreItems.some(item => ['레이어', '설정', '속성'].includes(item)), { inHeader, moreItems })
|
||||
check('520 row at 400: header + More hold every item exactly once (panel toggles never in More)', all.every(name => inHeader.includes(name) !== moreItems.includes(name)) && !moreItems.some(item => ['레이어', '설정', '속성'].includes(item)), { inHeader, moreItems })
|
||||
await page.keyboard.press('Escape')
|
||||
await page.evaluate(async () => { document.querySelector('[data-width="520"]').style.maxWidth = ''; await new Promise(resolve => setTimeout(resolve, 400)) })
|
||||
// Icon toggle at 520 still reports pressed / commands.
|
||||
const icon = headers.nth(3).getByRole('button', { name: '레이어', exact: true })
|
||||
await icon.click()
|
||||
@@ -289,14 +312,15 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
for (const [name, dialog] of [['레이어', /레이어 \(/], ['설정', '옵션'], ['속성', '속성']]) {
|
||||
const toggle = header.getByRole('button', { name, exact: true })
|
||||
await toggle.hover()
|
||||
const hover = await toggle.evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
// 2026-11-13: Viewer Header hover (caf4c32, 2026-10-21 user request): no face (transparent), text / icon #FFC600.
|
||||
const hover = await toggle.evaluate(element => `${getComputedStyle(element).backgroundColor} / ${getComputedStyle(element).color}`)
|
||||
await toggle.click()
|
||||
const panel = page.getByRole('dialog', { name: dialog })
|
||||
panels.push({ name, iconOnly: (await toggle.locator('.hm-sw-viewer-header__label').count()) === 0, title: await toggle.getAttribute('title'), hover, open: await panel.isVisible(), pressed: await toggle.getAttribute('aria-pressed'), color: await toggle.evaluate(element => getComputedStyle(element).color) })
|
||||
await toggle.click()
|
||||
panels.at(-1).closed = (await panel.count()) === 0 && (await toggle.getAttribute('aria-pressed')) === 'false'
|
||||
}
|
||||
check(`narrow header (${width}px): icon toggles with tooltip, hover, pressed #FFC600, panel open / close`, width < 900 && panels.every(item => item.iconOnly && item.title && item.hover === 'rgb(38, 102, 81)' && item.open && item.pressed === 'true' && item.color === 'rgb(255, 198, 0)' && item.closed), panels)
|
||||
check(`narrow header (${width}px): icon toggles with tooltip, hover (no face, #FFC600), pressed #FFC600, panel open / close`, width < 900 && panels.every(item => item.iconOnly && item.title && item.hover === 'rgba(0, 0, 0, 0) / rgb(255, 198, 0)' && item.open && item.pressed === 'true' && item.color === 'rgb(255, 198, 0)' && item.closed), panels)
|
||||
await page.screenshot({ path: path.join(out, `viewer-narrow-${width}.png`), clip: { ...(await box(header)), height: 56 } })
|
||||
await page.close()
|
||||
}
|
||||
@@ -353,6 +377,11 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
// The header's own width is swept (10px steps) down and back up. Stage per width: 0 full, 1 icons only, 2+ More with
|
||||
// n items. Icons always come before More; narrower never has fewer steps; the way back gives the same state at
|
||||
// every width (no flicker, no stuck item); no overflow. A shorter file name moves every boundary to a narrower width.
|
||||
// 2026-11-13 (current approved rules): the panel toggles turn into icons before 전체 보기 (2026-10-23, user-approved) and a
|
||||
// button that became an icon stays one when narrower; below 560px the file name gives way to 56px (2026-10-26), so items
|
||||
// may come back from More into the header — instead of a falling item count, every width that shows More opens it and every
|
||||
// command must be in the header or in More exactly once. The steps use a header-width basis, not the file name width
|
||||
// (caf4c32, 2026-10-21), so a shorter file name keeps the same boundaries.
|
||||
const page = await newPage(1920, 900)
|
||||
await open(page, 'sw-viewer--viewer-header')
|
||||
const header = page.locator('.hm-sw-viewer-header').first()
|
||||
@@ -365,8 +394,32 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const iconOnly = icons.length > 0 && icons.every(button => !button.querySelector('.hm-sw-viewer-header__label'))
|
||||
const anyIcon = icons.some(button => !button.querySelector('.hm-sw-viewer-header__label'))
|
||||
const more = !!element.querySelector('.hm-sw-viewer-header__more')
|
||||
return { width, shown, iconOnly, anyIcon, more, stage: more ? 2 : iconOnly ? 1 : 0, overflow: Math.max(element.scrollWidth - element.clientWidth, object.scrollWidth - object.clientWidth - 1) }
|
||||
const nameOf = button => button.getAttribute('aria-label') ?? button.textContent.trim()
|
||||
const iconNames = icons.filter(button => !button.querySelector('.hm-sw-viewer-header__label')).map(nameOf)
|
||||
const labelNames = icons.filter(button => button.getClientRects().length && button.querySelector('.hm-sw-viewer-header__label')).map(nameOf)
|
||||
return { width, shown, iconOnly, anyIcon, iconNames, labelNames, more, stage: more ? 2 : iconOnly ? 1 : 0, overflow: Math.max(element.scrollWidth - element.clientWidth, object.scrollWidth - object.clientWidth - 1) }
|
||||
}, width)
|
||||
// Commands shown in the header (visible only; a switch is named by its full label) and in the opened More menu.
|
||||
const COMMANDS = ['Model', 'Layout', '전체 보기', '2.5D', '어두운 배경', '레이어', '설정', '속성']
|
||||
const PANEL_TOGGLES = ['레이어', '설정', '속성']
|
||||
const commandsAt = async width => {
|
||||
const inHeader = await header.evaluate(async (element, width) => {
|
||||
element.style.width = `${width}px`
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
return [...element.querySelectorAll('.hm-sw-viewer-header__object :is(button:not(.hm-sw-viewer-header__more), [role=radio], [role=switch])')]
|
||||
.filter(item => item.getClientRects().length)
|
||||
.map(item => item.getAttribute('role') === 'switch' ? element.querySelector(`label[for="${item.id}"] .hm-sw-viewer-header__switch-label-full`)?.textContent.trim()
|
||||
: (item.getAttribute('aria-label') ?? item.textContent.trim()).replace(/[\d,]+$/, ''))
|
||||
}, width)
|
||||
await header.getByRole('button', { name: '추가 도구' }).click()
|
||||
const menu = page.getByRole('menu', { name: '추가 도구' })
|
||||
await menu.waitFor({ timeout: 2000 })
|
||||
const inMore = (await menu.locator('[role^=menuitem]').allInnerTexts()).map(text => text.split(' · ')[0].trim())
|
||||
const menuBox = await menu.evaluate(element => { const r = element.getBoundingClientRect(); return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, vw: innerWidth, vh: innerHeight } })
|
||||
await page.keyboard.press('Escape')
|
||||
const closed = (await page.getByRole('menu', { name: '추가 도구' }).count()) === 0
|
||||
return { width, inHeader: inHeader.filter(name => COMMANDS.includes(name)), inMore, menuBox, closed, pageOverflow: await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth) }
|
||||
}
|
||||
const sweep = async () => {
|
||||
const down = [], up = []
|
||||
for (let width = 1440; width >= 400; width -= 10) down.push(await state(width))
|
||||
@@ -376,17 +429,33 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const boundaries = list => ({ icons: list.find(item => item.stage >= 1)?.width, more: list.find(item => item.stage === 2)?.width })
|
||||
const long = await sweep()
|
||||
check('sweep: no overflow at any width (400–1440)', [...long.down, ...long.up].every(item => item.overflow <= 0), [...long.down, ...long.up].filter(item => item.overflow > 0).map(item => item.width))
|
||||
check('sweep: icons switch together and always before More', [...long.down, ...long.up].every(item => item.anyIcon === item.iconOnly && (!item.more || item.iconOnly)))
|
||||
check('sweep: narrower never shows more (monotonic: full → icons → More, fewer items)', long.down.every((item, i) => i === 0 || (item.stage >= long.down[i - 1].stage && item.shown <= long.down[i - 1].shown)))
|
||||
check('sweep: icons always before More; panel toggles turn into icons before 전체 보기', [...long.down, ...long.up].every(item => (!item.more || item.iconOnly)
|
||||
&& (!item.iconNames.includes('전체 보기') || PANEL_TOGGLES.every(name => item.iconNames.includes(name)))),
|
||||
[...long.down, ...long.up].filter(item => (item.more && !item.iconOnly) || (item.iconNames.includes('전체 보기') && !PANEL_TOGGLES.every(name => item.iconNames.includes(name)))).map(item => [item.width, item.iconNames]))
|
||||
// A button that became an icon never shows its label again at a narrower width (it may move on into More).
|
||||
check('sweep: a button that became an icon never gets its label back when narrower', long.down.every((item, i) => i === 0 || !long.down[i - 1].iconNames.some(name => item.labelNames.includes(name))),
|
||||
long.down.filter((item, i) => i && long.down[i - 1].iconNames.some(name => item.labelNames.includes(name))).map(item => [item.width, item.labelNames]))
|
||||
// Every width (10px steps) that shows More: More opened for real; each command exactly once in the header or in More, the panel
|
||||
// toggles never in More, the menu inside the viewport and closed by Escape, no page overflow. Items moving back into the header
|
||||
// when the file name gives way (below 560px) are allowed.
|
||||
const moreWidths = long.down.filter(item => item.more).map(item => item.width)
|
||||
const commands = []
|
||||
for (const width of moreWidths) commands.push(await commandsAt(width))
|
||||
const commandIssues = commands.filter(item => !COMMANDS.every(name => item.inHeader.filter(other => other === name).length + item.inMore.filter(other => other === name).length === 1)
|
||||
|| item.inMore.some(name => PANEL_TOGGLES.includes(name)) || item.inHeader.length + item.inMore.length !== COMMANDS.length
|
||||
|| !(item.menuBox.left >= 0 && item.menuBox.top >= 0 && item.menuBox.right <= item.menuBox.vw + 0.5 && item.menuBox.bottom <= item.menuBox.vh + 0.5) || !item.closed || item.pageOverflow > 0)
|
||||
check(`sweep: every command exactly once in the header or More at each More width (${moreWidths.length} widths), menu inside the viewport, Escape closes`, moreWidths.length > 0 && commandIssues.length === 0,
|
||||
commandIssues.length ? commandIssues.slice(0, 3) : { widths: [moreWidths.at(0), moreWidths.at(-1)], sample: commands.at(-1) })
|
||||
check('sweep: growing back gives the same state at every width (reverse order, no stuck item)', long.down.every(item => { const back = long.up.find(other => other.width === item.width); return back.stage === item.stage && back.shown === item.shown }))
|
||||
check('sweep: 1440 full; each stage reached by measured room only', long.down[0].stage === 0 && long.down.at(-1).stage === 2, boundaries(long.down))
|
||||
// A short file name: the same content needs less room, so each step happens at a narrower width.
|
||||
// A short file name: the steps use the header-width basis of the file name area (caf4c32, 2026-10-21: short and long names
|
||||
// give the same result), so every boundary stays where it is with a long name.
|
||||
await header.evaluate(element => { element.style.width = '' })
|
||||
await header.locator('.hm-sw-viewer-header__document .hm-sw-header__document-button').click()
|
||||
await header.getByRole('button', { name: 'simple.dxf' }).click()
|
||||
const short = await sweep()
|
||||
const [a, b] = [boundaries(long.down), boundaries(short.down)]
|
||||
check('sweep: shorter file name moves the steps to narrower widths', b.icons < a.icons && b.more < a.more, { long: a, short: b })
|
||||
check('sweep: file name length does not move the steps (same boundaries for a short name)', a.icons !== undefined && a.more !== undefined && b.icons === a.icons && b.more === a.more, { long: a, short: b })
|
||||
await page.close()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { ComponentType } from "react"
|
||||
import type { ComponentType, ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { TunnelFacility } from "@hm-design/center-graphic"
|
||||
import {
|
||||
BridgeReferenceDemo,
|
||||
IntersectionReferenceDemo,
|
||||
InterchangeReferenceDemo,
|
||||
JunctionReferenceDemo,
|
||||
LinkedFacilityReferenceDemo,
|
||||
RoadsideReferenceDemo,
|
||||
TunnelReferenceDemo,
|
||||
} from "@hm-design/center-graphic/reference"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
CompactFacilityConnection,
|
||||
FACILITY_CONFIGS,
|
||||
LocalAssetFacility,
|
||||
RoadsideFacilityTag,
|
||||
TunnelFacility,
|
||||
type FacilityConfig,
|
||||
type RoadsideEntity,
|
||||
} from "@hm-design/center-graphic"
|
||||
import { LinkedFacilityReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
import { StoryPage, StoryPageHeader } from "@/components/story-page"
|
||||
import { withRepresentation } from "./center-shared"
|
||||
|
||||
// Owners: ui-viz docs/visualization/facility.md and facility-assets.md (128 approved SVG references, shipped unchanged).
|
||||
@@ -25,35 +25,225 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Common Story page. The CENTER representation wraps only the specimen, so the page title is never scaled with the graphic.
|
||||
// 2026-11-13: no visible "개요" section title (사업명 page rule); the specimens keep their indent via the common `sb-page-indent`.
|
||||
const page = (name: string, Demo: ComponentType): Story => ({
|
||||
name,
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title={name} />
|
||||
<StorySection title="개요">
|
||||
<div className="sb-page-indent">
|
||||
{withRepresentation(() => <Demo />)}
|
||||
</StorySection>
|
||||
</div>
|
||||
</StoryPage>
|
||||
),
|
||||
})
|
||||
|
||||
// 터널 page layout (2026-11-13): composed here from the public API (same props as the package TunnelReferenceDemo) so the
|
||||
// specimens can be grouped — one width-behaviour section with a Position Map column (SEPARATED → SHORT → MEDIUM → LONG) and an
|
||||
// Actual / Detail column (SEPARATED → ACTUAL SHORT → REF → LONG), same steps on the same row (1 column below a 1180px content
|
||||
// width), and the compact applied example full width below. The former standalone Position Map / Actual examples are the
|
||||
// MEDIUM / ACTUAL REF rows (same default name and length). Package classes are reused so the common title row, 12px title gap and the
|
||||
// 8px local scrollbar still apply; the layout lives in center-stories.css (`.sb-center-tunnel*`).
|
||||
const TUNNEL_WIDTH_NAMES: [string, string, string][] = [
|
||||
["SHORT", "터널", "L=80m"],
|
||||
["MEDIUM", "대산2터널", "L=750m"],
|
||||
["LONG", "서해안고속도로 대산연결 제2터널", "L=12,480m"],
|
||||
]
|
||||
const TUNNEL_ACTUAL_LABELS = ["ACTUAL SHORT", "ACTUAL REF", "ACTUAL LONG"]
|
||||
|
||||
function TunnelSample({ title, note, children }: { title: string; note?: string; children: ReactNode }) {
|
||||
return (
|
||||
<article className="tunnel-reference__sample">
|
||||
<header>
|
||||
<strong>{title}</strong>
|
||||
{note ? <span>{note}</span> : null}
|
||||
</header>
|
||||
<div className="tunnel-reference__stage">{children}</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function TunnelSpecimens() {
|
||||
return (
|
||||
<div className="sb-center-tunnel">
|
||||
<div className="tunnel-reference">
|
||||
{/* 2026-11-13: no "너비 동작" title row; each column carries the common specimen title (English + Korean note). */}
|
||||
<article className="tunnel-reference__sample">
|
||||
<div className="tunnel-reference__stage sb-center-tunnel__width-stage">
|
||||
<div className="sb-center-tunnel__width">
|
||||
{(["positionMap", "actualDetail"] as const).map((variant) => (
|
||||
<div className="tunnel-reference__width-tests sb-center-tunnel__width-group" key={variant}>
|
||||
<header>
|
||||
<strong>{variant === "positionMap" ? "Position Map" : "Actual / Detail"}</strong>
|
||||
<span>{variant === "positionMap" ? "위치도" : "실사화"}</span>
|
||||
</header>
|
||||
<div>
|
||||
<span>SEPARATED</span>
|
||||
<TunnelFacility variant={variant} separated />
|
||||
</div>
|
||||
{TUNNEL_WIDTH_NAMES.map(([size, name, length], index) => (
|
||||
<div key={size}>
|
||||
<span>{variant === "positionMap" ? size : TUNNEL_ACTUAL_LABELS[index]}</span>
|
||||
<TunnelFacility name={name} secondaryText={length} variant={variant} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<TunnelSample title="간략 적용 예제" note="노선 + 연결 + 터널 + 레이블">
|
||||
<CompactFacilityConnection className="tunnel-applied-example" routeMode="inline" segmentShape="tunnel" sourceStatus="SOURCE_RECEIVED" label="노선과 터널 시설물의 간략 적용 예제">
|
||||
<TunnelFacility name="대산2터널" secondaryText="L=750m" variant="positionMap" />
|
||||
</CompactFacilityConnection>
|
||||
</TunnelSample>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 교량 · 교차로 · 분기점 · 나들목 page layout (2026-11-13): the 터널 layout applied to the package FacilityReferenceDemo
|
||||
// specimens, composed from the public API with the same props. Position Map | Actual / Detail columns (SEPARATED first, then
|
||||
// SHORT / MEDIUM / LONG and ACTUAL SHORT / REF / LONG on the same rows), the compact applied example full width below. The former
|
||||
// standalone Position Map / Actual examples are the MEDIUM / ACTUAL REF rows (default name and secondary text) and the former
|
||||
// Separated example is each column's first row. `data-sb-compare-at` raises the 2-column breakpoint when the widest row needs it
|
||||
// (교량 LONG 511px artwork: 1210px); without it the 터널 1180px rule applies.
|
||||
function facilityWidthNames(config: FacilityConfig) {
|
||||
return [
|
||||
config.id === "bridge" ? "1교" : "신시",
|
||||
config.defaultName,
|
||||
config.id === "bridge" ? "서해안고속도로 대산연결 제2교량" : `서해안고속도로 ${config.defaultName}`,
|
||||
]
|
||||
}
|
||||
|
||||
function FacilitySpecimens({ config, compareAt }: { config: FacilityConfig; compareAt?: "1210" }) {
|
||||
const widthNames = facilityWidthNames(config)
|
||||
return (
|
||||
<div className="sb-center-tunnel" data-sb-compare-at={compareAt}>
|
||||
<div className="facility-reference" data-reference-facility={config.id}>
|
||||
<article className="facility-reference__sample">
|
||||
<div className="facility-reference__stage sb-center-tunnel__width-stage">
|
||||
<div className="sb-center-tunnel__width">
|
||||
{(["positionMap", "actualDetail"] as const).map((variant) => (
|
||||
<div className="facility-reference__width-tests sb-center-tunnel__width-group" key={variant}>
|
||||
<header>
|
||||
<strong>{variant === "positionMap" ? "Position Map" : "Actual / Detail"}</strong>
|
||||
<span>{variant === "positionMap" ? "위치도" : "실사화"}</span>
|
||||
</header>
|
||||
<div>
|
||||
<span>SEPARATED</span>
|
||||
<LocalAssetFacility config={config} variant={variant} separated />
|
||||
</div>
|
||||
{widthNames.map((name, index) => (
|
||||
<div key={name}>
|
||||
<span>{variant === "positionMap" ? ["SHORT", "MEDIUM", "LONG"][index] : `ACTUAL ${["SHORT", "REF", "LONG"][index]}`}</span>
|
||||
<LocalAssetFacility config={config} name={name} variant={variant} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<TunnelSample
|
||||
title="간략 적용 예제"
|
||||
note={config.id === "interchange" ? "SOURCE_PENDING · 기존 Preview 전용; 정확한 IC 구성 추론 안 함" : "SOURCE_RECEIVED · 관계 비교, 정확한 픽셀 정합 아님"}
|
||||
>
|
||||
<CompactFacilityConnection
|
||||
className={`facility-applied facility-applied--${config.routeMode}`}
|
||||
routeMode={config.routeMode}
|
||||
segmentShape={config.id === "bridge" ? "bridge" : undefined}
|
||||
sourceStatus={config.id === "interchange" ? "SOURCE_PENDING" : "SOURCE_RECEIVED"}
|
||||
label={`${config.label} 간략 적용 예제`}
|
||||
>
|
||||
<LocalAssetFacility config={config} />
|
||||
</CompactFacilityConnection>
|
||||
</TunnelSample>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 연계 노선 page layout (2026-11-13): the package RoadsideReferenceDemo specimens in the same 2-column structure and the same
|
||||
// single-line row labels as the other facility pages (SHORT / MEDIUM / LONG, ACTUAL SHORT / REF / LONG; the former two-line
|
||||
// "휴게소 · SHORT" meta is replaced — the entity stays in the tag text and its accessible name). Widest row 64px + 16px + 450px
|
||||
// LONG tag, so the 터널 1180px rule applies. Separated exists for Actual / Detail only, so it is that column's first row; at
|
||||
// 2 columns the Position Map column keeps an empty first row so the steps stay on the same rows, at 1 column the empty row is
|
||||
// not rendered. The 연계 / 노선 맥락 example is full width below.
|
||||
const ROADSIDE_TESTS: { size: string; actual: string; entity: RoadsideEntity; label: string }[] = [
|
||||
{ size: "SHORT", actual: "ACTUAL SHORT", entity: "rest-area", label: "휴게소" },
|
||||
{ size: "MEDIUM", actual: "ACTUAL REF", entity: "drowsy-shelter", label: "졸음쉼터" },
|
||||
{ size: "LONG", actual: "ACTUAL LONG", entity: "drowsy-shelter", label: "서해안고속도로 대산방향 졸음쉼터" },
|
||||
]
|
||||
|
||||
function RoadsideSpecimens() {
|
||||
return (
|
||||
<div className="sb-center-tunnel sb-center-roadside">
|
||||
<div className="facility-reference roadside-reference" data-reference-facility="roadside">
|
||||
<article className="facility-reference__sample">
|
||||
<div className="facility-reference__stage sb-center-tunnel__width-stage">
|
||||
<div className="sb-center-tunnel__width">
|
||||
<div className="facility-reference__width-tests sb-center-tunnel__width-group sb-center-roadside__position">
|
||||
<header><strong>Position Map</strong><span>위치도</span></header>
|
||||
<div className="sb-center-roadside__blank" aria-hidden="true" />
|
||||
{ROADSIDE_TESTS.map((test) => (
|
||||
<div key={test.size} data-roadside-test={test.size.toLowerCase()}>
|
||||
<span>{test.size}</span>
|
||||
{/* The tag is itself a span: the wrapper keeps the row-label rule (`> div > span`) off the tag text. */}
|
||||
<div><RoadsideFacilityTag entity={test.entity} label={test.label} /></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="facility-reference__width-tests sb-center-tunnel__width-group">
|
||||
<header><strong>Actual / Detail</strong><span>실사화</span></header>
|
||||
<div data-roadside-actual-test="separated">
|
||||
<span>SEPARATED</span>
|
||||
<RoadsideFacilityTag entity="drowsy-shelter" variant="actualDetail" separated />
|
||||
</div>
|
||||
{ROADSIDE_TESTS.map((test) => (
|
||||
<div key={test.size} data-roadside-actual-test={test.size.toLowerCase()}>
|
||||
<span>{test.actual}</span>
|
||||
<RoadsideFacilityTag entity={test.entity} label={test.label} variant="actualDetail" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<TunnelSample title="연계 / 노선 맥락" note="노선 + 앵커 + 연결선 + 시설물 태그">
|
||||
<CompactFacilityConnection className="facility-applied facility-applied--anchored roadside-applied" routeMode="anchored" sourceStatus="SOURCE_PENDING" label="졸음쉼터 연계 노선 간략 적용 예제">
|
||||
<RoadsideFacilityTag entity="drowsy-shelter" />
|
||||
</CompactFacilityConnection>
|
||||
</TunnelSample>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// `name` is repeated as a literal because Storybook's static indexer cannot read it through page(); it drives the sidebar label.
|
||||
export const Tunnel: Story = { ...page("터널", TunnelReferenceDemo), name: "터널" }
|
||||
export const Bridge: Story = { ...page("교량", BridgeReferenceDemo), name: "교량" }
|
||||
export const Intersection: Story = { ...page("교차로", IntersectionReferenceDemo), name: "교차로" }
|
||||
export const Junction: Story = { ...page("분기점", JunctionReferenceDemo), name: "분기점" }
|
||||
export const Interchange: Story = { ...page("나들목", InterchangeReferenceDemo), name: "나들목" }
|
||||
export const Roadside: Story = { ...page("휴게소 · 졸음쉼터", RoadsideReferenceDemo), name: "휴게소 · 졸음쉼터" }
|
||||
export const Tunnel: Story = { ...page("터널", TunnelSpecimens), name: "터널" }
|
||||
export const Bridge: Story = { ...page("교량", () => <FacilitySpecimens config={FACILITY_CONFIGS.bridge} compareAt="1210" />), name: "교량" }
|
||||
export const Intersection: Story = { ...page("교차로", () => <FacilitySpecimens config={FACILITY_CONFIGS.intersection} />), name: "교차로" }
|
||||
export const Junction: Story = { ...page("분기점", () => <FacilitySpecimens config={FACILITY_CONFIGS.junction} />), name: "분기점" }
|
||||
export const Interchange: Story = { ...page("나들목", () => <FacilitySpecimens config={FACILITY_CONFIGS.interchange} />), name: "나들목" }
|
||||
export const Roadside: Story = { ...page("휴게소 · 졸음쉼터", RoadsideSpecimens), name: "연계 노선" }
|
||||
// Linked facilities are a standalone section heading followed by item cards.
|
||||
// Keep the reference data/artwork and road-class grouping owned by the package.
|
||||
// 2026-11-13: same `sb-page-indent` start as the other facility pages; the page class scopes the Linked-only CSS
|
||||
// (hidden context notice / READY count / duplicate card name, card title and POSITION · ACTUAL label style).
|
||||
export const Linked: Story = {
|
||||
...page("연계 시설물", LinkedFacilityReferenceDemo),
|
||||
name: "연계 시설물",
|
||||
render: () => (
|
||||
<StoryPage className="sb-center-linked-page">
|
||||
<StoryPageHeader title="연계 시설물" />
|
||||
<div className="sb-page-indent">
|
||||
{withRepresentation(() => <LinkedFacilityReferenceDemo />)}
|
||||
</div>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { CenterInformationDisplayReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader } from "@/components/story-page"
|
||||
import { withRepresentation } from "./center-shared"
|
||||
|
||||
// Owner: ui-viz docs/patterns/center-information-presentation.md and center-information-{box,table,emphasis}.md.
|
||||
@@ -19,13 +19,14 @@ export const Information: Story = {
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="정보" />
|
||||
<StorySection title="개요">
|
||||
{/* 2026-11-13: no visible "개요" section title; indent kept via the common `sb-page-indent`. */}
|
||||
<div className="sb-page-indent">
|
||||
{withRepresentation(() => (
|
||||
<div className="sb-center-information">
|
||||
<CenterInformationDisplayReferenceDemo />
|
||||
</div>
|
||||
))}
|
||||
</StorySection>
|
||||
</div>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -10,8 +10,10 @@ export const CENTER_BOUNDARY =
|
||||
export const SCALE_NOTE =
|
||||
"제작 대상이 7680 × 2160이면 CENTER 그래픽을 100%, 3840 × 2160이면 75%로 표시합니다. Storybook 화면 크기는 배율을 정하지 않으며, 다른 제작 해상도의 배율은 정해지지 않았습니다. 제작 대상은 주소에 centerResolution=3840x2160 을 붙여 바꿉니다."
|
||||
|
||||
export const CenterFrame = ({ children }: { children: ReactNode }) => (
|
||||
<div className="sb-center" lang="ko">
|
||||
// `plain` drops the review card frames on 노선도 pages only. OrgChart previews reuse `.sb-center` and the card
|
||||
// classes directly, so the plain layout is a CENTER-page modifier rather than a change to the shared frame.
|
||||
export const CenterFrame = ({ children, plain = false }: { children: ReactNode; plain?: boolean }) => (
|
||||
<div className={plain ? "sb-center sb-center--plain" : "sb-center"} lang="ko">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -23,7 +25,7 @@ export const centerTargetResolution = (): CenterTargetResolution => {
|
||||
}
|
||||
|
||||
export const withRepresentation = (Story: () => ReactNode) => (
|
||||
<CenterFrame>
|
||||
<CenterFrame plain>
|
||||
<CenterRepresentation targetResolution={centerTargetResolution()}>
|
||||
<Story />
|
||||
</CenterRepresentation>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { RouteLine } from "@hm-design/center-graphic"
|
||||
import { CenterSpatialInformationReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { StoryPage, StoryPageHeader } from "@/components/story-page"
|
||||
import { withRepresentation } from "./center-shared"
|
||||
|
||||
// Owners: ui-viz docs/visualization/route.md, spatial-map.md, spatial-section.md, composition.md.
|
||||
@@ -21,9 +21,10 @@ export const Spatial: Story = {
|
||||
render: () => (
|
||||
<StoryPage className="sb-center-spatial-page">
|
||||
<StoryPageHeader title="노선·공간" />
|
||||
<StorySection title="개요">
|
||||
{/* 2026-11-13: no visible "개요" section title; indent kept via the common `sb-page-indent`. */}
|
||||
<div className="sb-page-indent">
|
||||
{withRepresentation(() => <CenterSpatialInformationReferenceDemo />)}
|
||||
</StorySection>
|
||||
</div>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
+370
-16
@@ -166,22 +166,8 @@
|
||||
gap: var(--sb-center-section-gap);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.sb-center .linked-facility__card {
|
||||
container: sb-linked-card / inline-size;
|
||||
}
|
||||
/* 56px label + 10px gap + measured 251px artwork + 48px common padding.
|
||||
Stack the label when that row cannot fit; preserve artwork size and local scroll. */
|
||||
@container sb-linked-card (max-width: 365px) {
|
||||
.sb-center .linked-facility__variant {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto;
|
||||
justify-items: safe center;
|
||||
gap: var(--sb-center-header-gap);
|
||||
}
|
||||
.sb-center .linked-facility__variant > small {
|
||||
grid-row: auto;
|
||||
}
|
||||
}
|
||||
/* 2026-11-13: the former narrow-card label stacking (`sb-linked-card` container) is replaced by the 연계 시설물
|
||||
card layout at the end of this file (POSITION | ACTUAL columns, `sb-linked-list` container). */
|
||||
.sb-center .sb-center-status { min-width: 720px; }
|
||||
|
||||
/* Spatial cards reuse the common frame above. Contrast belongs to the inner
|
||||
@@ -249,3 +235,371 @@
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* 노선도 pages only (CenterFrame `plain`, 2026-11-12): no review card frame.
|
||||
Each specimen is a plain heading + description followed by the artwork; wider
|
||||
gaps replace the card edges. The shared frame above stays for OrgChart previews,
|
||||
which reuse `.sb-center` and these card classes. Artwork, the spatial-section
|
||||
#efefef canvas and the review-caption fixes above are unchanged. */
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__sample, .facility-reference__sample,
|
||||
.center-title-reference__sample, .cid-demo__block, .linked-facility__card, .csi-panel) {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__sample, .facility-reference__sample,
|
||||
.center-title-reference__sample, .cid-demo__block, .linked-facility__card) > header,
|
||||
.sb-center.sb-center--plain .csi-panel > h4 {
|
||||
min-height: 0;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.sb-center.sb-center--plain .linked-facility__variant + .linked-facility__variant {
|
||||
border-top: 0;
|
||||
}
|
||||
.sb-center.sb-center--plain .csi-panel {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 노선도 layout rhythm (2026-11-12): the common StorySection / StateBoard rules of
|
||||
story-page.css and story-board.tsx (Button, Badge …), applied to the package demo DOM.
|
||||
- 2026-11-13: no visible "개요" section; the Story wraps the specimens in the common `sb-page-indent`
|
||||
(16px / 12px at 640px and below). 연계 시설물 keeps its own unindented page.
|
||||
- Specimen title row (사업명 page rule, 2026-11-13): title 16/24/700 primary, description or size
|
||||
12/18 secondary 8px to the right on the same row (wraps only when the row runs out),
|
||||
artwork 12px below, specimens 24px apart, columns 24px apart. Package text (Source / status) is kept.
|
||||
- Artwork starts on the label's left edge; wide artwork keeps its local scroll.
|
||||
- Linked road-class groups read as sections (16/24/700, 12px to content, 28px apart)
|
||||
with their card grid indented, columns from a 320px minimum (StateBoard default).
|
||||
Artwork size, colours and internal composition are untouched. */
|
||||
.sb-center.sb-center--plain,
|
||||
.sb-center-linked-page .sb-center.sb-center--plain {
|
||||
padding: 0;
|
||||
}
|
||||
/* Common specimen title row (2026-11-13, from the 사업명 page): package demo headers are
|
||||
`header > strong + span`, the 사업명 Story header is `header > h3 + span`. Overrides the package's
|
||||
space-between / narrow-screen column header on 노선도 pages only. */
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__sample, .facility-reference__sample,
|
||||
.center-title-reference__sample, .cid-demo__block, .sb-center-tunnel__width-group) > header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: flex-start;
|
||||
column-gap: 8px;
|
||||
row-gap: 0;
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__sample, .facility-reference__sample,
|
||||
.center-title-reference__sample, .cid-demo__block, .sb-center-tunnel__width-group) > header :is(strong, h3),
|
||||
.sb-center.sb-center--plain .csi-panel > h4 {
|
||||
margin: 0;
|
||||
color: var(--hm-sem-fg-primary);
|
||||
font-size: var(--sb-page-section-title-font-size, 16px);
|
||||
line-height: var(--sb-page-section-title-line-height, 24px);
|
||||
font-weight: var(--hm-font-weight-bold);
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__sample, .facility-reference__sample,
|
||||
.center-title-reference__sample, .cid-demo__block, .sb-center-tunnel__width-group) > header :is(span, small, p) {
|
||||
color: var(--hm-sem-fg-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
/* Exceptions kept as before: 연계 시설물 item cards (label 14/20/500) and the 노선·공간 long
|
||||
descriptions below their title (legend / note paragraphs, 14/20 secondary). */
|
||||
.sb-center.sb-center--plain .linked-facility__card > header :is(strong, h3) {
|
||||
color: var(--hm-sem-fg-secondary);
|
||||
font-size: var(--hm-label-font-size);
|
||||
line-height: var(--hm-label-line-height);
|
||||
font-weight: var(--hm-font-weight-medium);
|
||||
}
|
||||
.sb-center.sb-center--plain .linked-facility__card > header :is(span, small, p),
|
||||
.sb-center.sb-center--plain .csi-panel > :is(.csi-legend, .csi-note) {
|
||||
color: var(--hm-sem-fg-secondary);
|
||||
font-size: var(--hm-label-font-size);
|
||||
line-height: var(--hm-label-line-height);
|
||||
}
|
||||
/* Representation note ("표시 배율 · 제작 대상"): its own row on the content's right edge
|
||||
(flex end, wraps instead of overflowing). The 사업명 page shows it in its first title row instead. */
|
||||
.sb-center.sb-center--plain .center-representation__note {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* csi-panel is a 12px grid: pull the description up to 4px under its label. */
|
||||
.sb-center.sb-center--plain .csi-panel > h4 + .csi-legend {
|
||||
margin-top: -8px;
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__stage, .facility-reference__stage,
|
||||
.center-title-reference__stage, .cid-demo__stage, .linked-facility__variant) {
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
/* The demo stages' minimum heights centred the artwork in an empty frame (card era);
|
||||
without the card they only push the artwork away from its label, so the stage
|
||||
now follows the artwork's own height and starts at its top-left. */
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__stage, .facility-reference__stage) {
|
||||
min-height: 0;
|
||||
align-items: start;
|
||||
justify-items: start;
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.center-title-reference__stage, .cid-demo__stage) {
|
||||
min-height: 0;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__row, .facility-reference__row) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.sb-center.sb-center--plain .linked-facility__variant {
|
||||
min-height: 0;
|
||||
justify-items: start;
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference, .facility-reference, .center-title-reference,
|
||||
.cid-demo, .csi-demo) {
|
||||
gap: 24px;
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__variant-grid, .facility-reference__variant-grid) {
|
||||
gap: 24px;
|
||||
}
|
||||
/* 2026-11-13 (시설물 layout): road-class sections 24px apart, the 노선도 section gap. */
|
||||
.sb-center.sb-center--plain .linked-facility {
|
||||
gap: 24px;
|
||||
}
|
||||
/* 2026-11-13: header → card grid is the header's own 12px padding, like the common section title (story-page.css). */
|
||||
.sb-center.sb-center--plain .linked-facility__group {
|
||||
gap: 0;
|
||||
}
|
||||
/* 2026-11-13 (시설물 layout): common title row — description 12px secondary 8px right of the title (wraps when needed). */
|
||||
.sb-center.sb-center--plain .linked-facility__group > header {
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
column-gap: 8px;
|
||||
row-gap: 0;
|
||||
padding-bottom: 12px;
|
||||
/* 2026-11-13: no divider, matching the common section title (story-page.css). */
|
||||
}
|
||||
.sb-center.sb-center--plain .linked-facility__group > header h3 {
|
||||
color: var(--hm-sem-fg-primary);
|
||||
font-size: var(--sb-page-section-title-font-size, 16px);
|
||||
line-height: var(--sb-page-section-title-line-height, 24px);
|
||||
font-weight: var(--hm-font-weight-bold);
|
||||
}
|
||||
.sb-center.sb-center--plain .linked-facility__group > header span {
|
||||
color: var(--hm-sem-fg-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.sb-center.sb-center--plain .linked-facility__grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
|
||||
gap: 24px;
|
||||
padding-left: var(--sb-page-indent, 16px);
|
||||
}
|
||||
|
||||
/* 연계 시설물 page only (2026-11-13, StoryPage `sb-center-linked-page`). The package demo is unchanged; hidden here are only
|
||||
repeated texts: the top context notice (two phrases + grey bar, a layout note rather than a specimen), the road-class
|
||||
"N READY 시설물 집합" count (the cards themselves show it) and the card's first name (터널 / 교량 / 교차로 repeat the Korean
|
||||
name, JC / IC are printed on the artwork). The Korean name is the card title, shown once. The page uses `sb-page-indent`, so
|
||||
the card grid drops its own indent: titles and cards start on the same line as the other facility pages.
|
||||
Card layout (2026-11-13, replaces the 320px auto-fit grid): each card is title + two columns, POSITION | ACTUAL (the
|
||||
package's two `__variant` blocks, label 11px / 700 / .08em), each column label → full artwork 8px → split artwork 16px; the
|
||||
columns share their rows (subgrid), so full and split artwork line up. Widths measured on all 17 cards: widest POSITION
|
||||
240px, widest ACTUAL 252px → a 2-column card needs 240 + 24 + 252 = 516px, two cards per row 516 × 2 + 24 = 1056px. The
|
||||
switches follow the list width (`sb-linked-list` container): ≥ 1056px 2 cards per row, below 1 card; ≥ 516px POSITION |
|
||||
ACTUAL, below stacked (24px apart). Odd counts leave the last card on the left, no filler. No card frame (plain 노선도).
|
||||
Artwork, split pieces, the 12px title gap, the 24px section / card gaps and the 8px local scrollbar are unchanged. */
|
||||
.sb-center-linked-page :is(.linked-facility__context, .linked-facility__group > header > span, .linked-facility__card > header > strong) {
|
||||
display: none;
|
||||
}
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility {
|
||||
container: sb-linked-list / inline-size;
|
||||
}
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
padding-left: 0;
|
||||
}
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-content: start;
|
||||
gap: 12px;
|
||||
}
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__card > header > span {
|
||||
color: var(--hm-sem-fg-primary);
|
||||
font-size: var(--hm-label-font-size);
|
||||
line-height: var(--hm-label-line-height);
|
||||
font-weight: var(--hm-font-weight-bold);
|
||||
text-align: left;
|
||||
}
|
||||
/* One column: label, full artwork, split artwork. Overrides the package's 56px label column / 1fr 1fr rows. The stage keeps
|
||||
its overflow-x: auto, so artwork wider than the column scrolls locally with the 8px bar. */
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__variant {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto;
|
||||
row-gap: 16px;
|
||||
column-gap: 0;
|
||||
align-items: start;
|
||||
justify-items: start;
|
||||
padding: 0;
|
||||
}
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__variant + .linked-facility__variant {
|
||||
margin-top: 12px; /* card gap 12px + 12px = 24px between the stacked POSITION and ACTUAL blocks */
|
||||
}
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__variant > small {
|
||||
grid-row: auto;
|
||||
margin-bottom: -8px; /* 16px row gap - 8px = label → full artwork 8px */
|
||||
font-size: 11px;
|
||||
}
|
||||
@container sb-linked-list (min-width: 516px) {
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__card {
|
||||
grid-template-columns: repeat(2, minmax(max-content, 1fr));
|
||||
grid-template-rows: repeat(4, auto);
|
||||
column-gap: 24px;
|
||||
}
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__card > header {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__variant {
|
||||
grid-row: 2 / span 3;
|
||||
grid-template-rows: subgrid;
|
||||
}
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__variant + .linked-facility__variant {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
@container sb-linked-list (min-width: 1056px) {
|
||||
.sb-center-linked-page .sb-center.sb-center--plain .linked-facility__grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* 사업명 page only (2026-11-13, StoryPage className): the representation note ("표시 배율 · 제작 대상")
|
||||
is shown in the first specimen's title row (.sb-center-title-note, center-title.stories.tsx), so the
|
||||
package's separate note block above the specimens is hidden here. The note keeps the size text style
|
||||
(12px secondary, common title row rule) and sits on the row's right edge (margin-left: auto in the
|
||||
wrapping flex row, i.e. space-between for "name + size" | note); when the row runs out of room only
|
||||
the note wraps to the next line, still right-aligned. No absolute positioning. */
|
||||
.sb-center-title-page .sb-center.sb-center--plain .center-representation__note {
|
||||
display: none;
|
||||
}
|
||||
.sb-center.sb-center--plain .center-title-reference__sample > header.sb-center-title-row .sb-center-title-note {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 2026-11-13: graphic local scroll cue (노선도 pages only). CENTER artwork keeps its real size, so wide
|
||||
specimens scroll horizontally inside their stage; with overlay scrollbars that read as clipping.
|
||||
These stages get a visible 8px horizontal scrollbar, which only appears when the stage actually
|
||||
overflows (overflow-x: auto is unchanged). Thumb colours reuse the HM Scroll Area tokens.
|
||||
Chromium/WebKit use ::-webkit-scrollbar (forces a classic bar); others get the standard thin bar.
|
||||
Artwork size, the 12px title gap and the 24px specimen gap are untouched. */
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__stage, .facility-reference__stage,
|
||||
.center-title-reference__stage, .cid-demo__stage, .linked-facility__variant, .csi-panel > div)::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__stage, .facility-reference__stage,
|
||||
.center-title-reference__stage, .cid-demo__stage, .linked-facility__variant, .csi-panel > div)::-webkit-scrollbar-thumb {
|
||||
border-radius: 9999px;
|
||||
background: var(--hm-scroll-area-thumb-background, var(--hm-sem-stroke-default));
|
||||
}
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__stage, .facility-reference__stage,
|
||||
.center-title-reference__stage, .cid-demo__stage, .linked-facility__variant, .csi-panel > div)::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--hm-scroll-area-thumb-hover-background, var(--hm-sem-fg-secondary));
|
||||
}
|
||||
@supports not selector(::-webkit-scrollbar) {
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__stage, .facility-reference__stage,
|
||||
.center-title-reference__stage, .cid-demo__stage, .linked-facility__variant, .csi-panel > div) {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--hm-scroll-area-thumb-background, var(--hm-sem-stroke-default)) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* 터널 page layout (2026-11-13, center-facility.stories.tsx `TunnelSpecimens`). The breakpoint follows the
|
||||
preview content width (container query). One width-behaviour section: Position Map | Actual / Detail
|
||||
columns from 1180px (the widest row: 64px label + 16px + 497px artwork, twice, plus the 24px gap),
|
||||
1 column below. Each column is title + SEPARATED + SHORT / MEDIUM / LONG; the two groups share the
|
||||
parent rows (subgrid), so each step lines up with its Actual counterpart.
|
||||
Artwork size, the package row label style, the 12px title gap, the 24px specimen gap and the 8px
|
||||
local scrollbar are unchanged; narrow stages keep their horizontal scroll. */
|
||||
.sb-center-tunnel {
|
||||
container-type: inline-size;
|
||||
min-width: 0;
|
||||
}
|
||||
.sb-center-tunnel__width {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(max-content, 1fr);
|
||||
grid-auto-rows: auto;
|
||||
column-gap: 24px;
|
||||
row-gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
.sb-center.sb-center--plain .sb-center-tunnel__width > .sb-center-tunnel__width-group {
|
||||
display: grid;
|
||||
grid-row: span 5;
|
||||
grid-template-rows: subgrid;
|
||||
gap: inherit;
|
||||
width: auto;
|
||||
}
|
||||
.sb-center-tunnel__width-group + .sb-center-tunnel__width-group {
|
||||
margin-top: 8px; /* 16px row gap + 8px = the 24px specimen gap between the stacked groups */
|
||||
}
|
||||
/* 2026-11-13: no "너비 동작" title row. Each column starts with the common specimen title row
|
||||
(`header > strong + span`, rule above: 16/24/700 + 12px note), so both titles share row 1 and start on
|
||||
the same line. The stage loses its 12px title gap (no title above it); title → first row stays 12px. */
|
||||
.sb-center.sb-center--plain :is(.tunnel-reference__stage, .facility-reference__stage).sb-center-tunnel__width-stage {
|
||||
padding-top: 0;
|
||||
}
|
||||
.sb-center.sb-center--plain .sb-center-tunnel__width-group > header {
|
||||
margin-bottom: -4px; /* 16px row gap - 4px = the common 12px title → artwork gap */
|
||||
}
|
||||
/* 연계 노선: Position Map has no Separated row. Its empty first row is shown only at 2 columns (1180px block below: row
|
||||
alignment with the Actual / Detail SEPARATED row); at 1 column it is not rendered and the group spans 4 rows.
|
||||
Kept before the breakpoint blocks so their `span 5` wins at 2 columns (same specificity, source order). */
|
||||
.sb-center-roadside__position > .sb-center-roadside__blank {
|
||||
display: none; /* outranks the package row rule `.facility-reference__width-tests > div { display: grid }` */
|
||||
}
|
||||
.sb-center.sb-center--plain .sb-center-tunnel__width > .sb-center-tunnel__width-group.sb-center-roadside__position {
|
||||
grid-row: span 4;
|
||||
}
|
||||
/* 교량 · 교차로 · 분기점 · 나들목 · 연계 노선 (2026-11-13) reuse this layout. A page whose widest row does not fit at 1180px
|
||||
sets `data-sb-compare-at` (교량 1210px) and switches in its own block below; the rule set is the same. */
|
||||
@container (min-width: 1180px) {
|
||||
.sb-center-tunnel:not([data-sb-compare-at]) .sb-center-tunnel__width {
|
||||
grid-template-columns: repeat(2, minmax(max-content, 1fr));
|
||||
}
|
||||
.sb-center.sb-center--plain .sb-center-tunnel:not([data-sb-compare-at]) .sb-center-tunnel__width > .sb-center-tunnel__width-group {
|
||||
grid-row: 1 / span 5;
|
||||
}
|
||||
.sb-center-tunnel:not([data-sb-compare-at]) .sb-center-tunnel__width-group + .sb-center-tunnel__width-group {
|
||||
margin-top: 0;
|
||||
}
|
||||
.sb-center-tunnel:not([data-sb-compare-at]) .sb-center-roadside__blank {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@container (min-width: 1210px) {
|
||||
.sb-center-tunnel[data-sb-compare-at="1210"] .sb-center-tunnel__width {
|
||||
grid-template-columns: repeat(2, minmax(max-content, 1fr));
|
||||
}
|
||||
.sb-center.sb-center--plain .sb-center-tunnel[data-sb-compare-at="1210"] .sb-center-tunnel__width > .sb-center-tunnel__width-group {
|
||||
grid-row: 1 / span 5;
|
||||
}
|
||||
.sb-center-tunnel[data-sb-compare-at="1210"] .sb-center-tunnel__width-group + .sb-center-tunnel__width-group {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 노선·공간 page only (2026-11-13, StoryPage className `sb-center-spatial-page`): example-first layout.
|
||||
The package demo's descriptions (.csi-legend), KNOWN_DEVIATION notes (.csi-note) and the representation
|
||||
note ("표시 배율 · 제작 대상") are hidden here; the package source is unchanged. With them gone the
|
||||
existing rhythm applies as-is: title → label/artwork 12px (--sb-center-header-gap), specimens 24px apart.
|
||||
The section 2 sample controls (.csi-controls, user request 2026-11-13) are hidden too, so the examples
|
||||
show the demo's default 기준 (Source 표본) content. Section titles, specimen labels and artwork stay visible. */
|
||||
.sb-center-spatial-page .sb-center.sb-center--plain :is(.csi-panel > .csi-legend, .csi-panel > .csi-note,
|
||||
.csi-panel > .csi-controls, .center-representation__note) {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,8 +1,54 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { CenterTitle, CENTER_TITLE_COMPOSITIONS, type CenterTitleComposition } from "@hm-design/center-graphic"
|
||||
import { CenterTitleReferenceDemo } from "@hm-design/center-graphic/reference"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import { withRepresentation } from "./center-shared"
|
||||
import type { ComponentProps } from "react"
|
||||
import { StoryPage, StoryPageHeader } from "@/components/story-page"
|
||||
import { centerTargetResolution, withRepresentation } from "./center-shared"
|
||||
|
||||
// 사업명 page specimens (2026-11-13): composed here from the public API instead of the package reference demo, so the
|
||||
// page shows "composition name + size" on one row without the Type / Source viewport labels. The package demo
|
||||
// (`@hm-design/center-graphic/reference`) is unchanged; the stage class keeps the existing artwork backdrop.
|
||||
const TITLE_SPECIMENS: { key: string; title: string; meta: string; props: ComponentProps<typeof CenterTitle> }[] = [
|
||||
...(Object.entries(CENTER_TITLE_COMPOSITIONS) as [CenterTitleComposition, (typeof CENTER_TITLE_COMPOSITIONS)[CenterTitleComposition]][]).map(
|
||||
([composition, specimen]) => ({
|
||||
key: composition,
|
||||
title: specimen.label,
|
||||
meta: `${specimen.sourceViewport.width} × ${specimen.sourceViewport.height}`,
|
||||
props: { composition },
|
||||
}),
|
||||
),
|
||||
{
|
||||
key: "long",
|
||||
title: "노선구분 + 사업명 (긴 콘텐츠)",
|
||||
meta: "같은 연결과 고정 외곽 지오메트리; 콘텐츠 기반 너비",
|
||||
props: { composition: "routeProjectName", projectName: "서해안고속도로 대산연결 제2구간 장기 건설사업 기본 및 실시설계" },
|
||||
},
|
||||
]
|
||||
|
||||
// 2026-11-13: the representation note moves into the first specimen's title row (right side). The package note above
|
||||
// the specimens is hidden on this page only (center-stories.css); its text follows the same target resolution here.
|
||||
function representationNote() {
|
||||
const isCompact = centerTargetResolution() === "3840x2160"
|
||||
return `표시 배율 ${isCompact ? 75 : 100}% · 제작 대상 ${isCompact ? "3840" : "7680"} × 2160`
|
||||
}
|
||||
|
||||
function TitleSpecimens() {
|
||||
return (
|
||||
<div className="center-title-reference" data-reference-component="center-title">
|
||||
{TITLE_SPECIMENS.map(({ key, title, meta, props }, index) => (
|
||||
<article className="center-title-reference__sample" key={key} data-content-test={key === "long" ? "long" : undefined}>
|
||||
<header className="sb-center-title-row">
|
||||
<h3>{title}</h3>
|
||||
<span>{meta}</span>
|
||||
{index === 0 ? <span className="sb-center-title-note">{representationNote()}</span> : null}
|
||||
</header>
|
||||
<div className="center-title-reference__stage">
|
||||
<CenterTitle {...props} />
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Owner: ui-viz docs/components/center-title.md. Title Resolution B numeric output is TBD; the per-Type palette is SCOPED_DESIGN_APPROVAL.
|
||||
const meta = {
|
||||
@@ -20,11 +66,13 @@ export const Compositions: Story = {
|
||||
name: "사업명",
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
// 2026-11-13: no visible "개요" section title. The specimens keep their former indent (common `sb-page-indent`),
|
||||
// and the representation note sits on the first title row's right edge (TitleSpecimens, `sb-center-title-page`).
|
||||
<StoryPage className="sb-center-title-page">
|
||||
<StoryPageHeader title="사업명" />
|
||||
<StorySection title="개요">
|
||||
{withRepresentation(() => <CenterTitleReferenceDemo />)}
|
||||
</StorySection>
|
||||
<div className="sb-page-indent">
|
||||
{withRepresentation(() => <TitleSpecimens />)}
|
||||
</div>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
@@ -22,22 +22,22 @@ export const Area: Story = {
|
||||
<ChartSampleNotice />
|
||||
{/* Same grid and order as Line Chart, then a Stacked | Expanded row. */}
|
||||
<StorySectionGrid className={chartGrid.pairs}>
|
||||
<ChartSpecimen title="Curve" description="monotone">
|
||||
<ChartSpecimen title="Curve">
|
||||
<HMAreaChart {...sampleLineCompare} curve="monotone" label="월별 접수·완료 곡선형 영역 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Curve · Dot + Label" description="monotone · Dot과 값 Label">
|
||||
<ChartSpecimen title="Curve · Dot + Label">
|
||||
<HMAreaChart {...sampleLineCompare} curve="monotone" pointLabels label="월별 접수·완료 곡선형 Dot·값 Label 영역 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Straight" description="linear">
|
||||
<ChartSpecimen title="Straight">
|
||||
<HMAreaChart {...sampleLineCompare} curve="linear" label="월별 접수·완료 직선형 영역 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Straight · Color Dot + Label" description="linear · Dot 개별 색상 + 값 Label">
|
||||
<ChartSpecimen title="Straight · Color Dot + Label">
|
||||
<HMAreaChart {...sampleLinePointColors} curve="linear" pointLabels label="월별 접수·완료 직선형 포인트별 색상 영역 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Step" description="step">
|
||||
<ChartSpecimen title="Step">
|
||||
<HMAreaChart {...sampleLineCompare} curve="step" label="월별 접수·완료 계단형 영역 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Step · Dot + Label" description="step · Dot과 값 Label">
|
||||
<ChartSpecimen title="Step · Dot + Label">
|
||||
<HMAreaChart {...sampleLineCompare} curve="step" pointLabels label="월별 접수·완료 계단형 Dot·값 Label 영역 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
</StorySectionGrid>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const Bar: Story = {
|
||||
<ChartSpecimen title="Grouped" description="월별 접수·완료 현황 · 상반기 월별 건수">
|
||||
<HMBarChart {...sampleBar} label="월별 접수·완료 건수 막대 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Horizontal" description="항목별 처리 건수 · 가로 막대">
|
||||
<ChartSpecimen title="Horizontal" description="항목별 처리 건수">
|
||||
<HMHorizontalBarChart {...sampleHorizontalBar} label="항목별 처리 건수 가로 막대 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Stacked" description="월별 접수·완료 현황(누적)">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { StorySection } from "@/components/story-page"
|
||||
|
||||
// Storybook presentation only (not part of @hm-design/chart): the open Chart page layout (2026-11-12, user request).
|
||||
// Charts sit directly on the Storybook content background, without the example card (background, border, padding). Each chart
|
||||
// is one StorySection: H2 variant name → description (what the example shows) → thin section divider → optional toolbar → chart.
|
||||
// is one StorySection: H2 variant name → optional description (what the example shows) → 12px → optional toolbar → chart.
|
||||
// The charts themselves are the public @hm-design/chart components on the @hm-design/chart/examples sample data, with the same
|
||||
// props as the matching *Example card, so data, legend, tooltip and interaction are unchanged. No common layout change; the page
|
||||
// only sets the chart's own surface token (chartPageClass).
|
||||
|
||||
@@ -56,17 +56,17 @@ export const Donut: Story = {
|
||||
<ChartSampleNotice />
|
||||
{/* Two per row from 1024px, three from 1200px, one below; source order is the mobile order. */}
|
||||
<StorySectionGrid className={chartGrid.compact}>
|
||||
<ChartSpecimen title="Basic" description="기본" toolbar="중앙 표시 없음">
|
||||
<ChartSpecimen title="Basic" toolbar="중앙 표시 없음">
|
||||
<HMDonutChart data={sampleDonutStates.data} label="업무 상태 도넛 차트, 가운데 값 없음(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Basic · Center Text" description="전체 합계" toolbar="중앙 표시: 전체 합계">
|
||||
<ChartSpecimen title="Basic · Center Text" toolbar="중앙 표시: 전체 합계">
|
||||
<HMDonutChart data={sampleDonutStates.data} totalLabel={sampleDonutStates.totalLabel} label="업무 상태 도넛 차트, 가운데 전체 합계(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Active" description="선택된 항목" toolbar={`선택 항목: ${selected?.label ?? ""}`}>
|
||||
<ChartSpecimen title="Active" toolbar={`선택 항목: ${selected?.label ?? ""}`}>
|
||||
<HMDonutChart data={sampleDonutStates.data} activeId={sampleDonutStates.activeId} label="업무 상태 도넛 차트, 선택된 항목 강조(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<DonutInteractive />
|
||||
<ChartSpecimen title="Solid" description="입체형" toolbar={null}>
|
||||
<ChartSpecimen title="Solid" toolbar={null}>
|
||||
<HMSolidDonutChart data={sampleDonutStates.data} totalLabel={sampleDonutStates.totalLabel} label="업무 상태 입체형 도넛 차트, 가운데 전체 합계(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
</StorySectionGrid>
|
||||
|
||||
@@ -23,22 +23,22 @@ export const Line: Story = {
|
||||
{/* Source order is the mobile order. 1024px: Curve | Curve + labels per row. 1200px: three columns, plain styles in the
|
||||
first row and their Dot + Label versions below. */}
|
||||
<StorySectionGrid className={chartGrid.pairs}>
|
||||
<ChartSpecimen title="Curve" description="monotone · Dot 없음">
|
||||
<ChartSpecimen title="Curve">
|
||||
<HMLineChart {...sampleLineCompare} curve="monotone" dots={false} label="월별 접수·완료 곡선형 선 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Curve · Dot + Label" description="monotone · Dot과 값 Label">
|
||||
<ChartSpecimen title="Curve · Dot + Label">
|
||||
<HMLineChart {...sampleLineCompare} curve="monotone" pointLabels label="월별 접수·완료 곡선형 Dot·값 라벨 선 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Straight" description="linear · Dot 없음">
|
||||
<ChartSpecimen title="Straight">
|
||||
<HMLineChart {...sampleLineCompare} curve="linear" dots={false} label="월별 접수·완료 직선형 선 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Straight · Color Dot + Label" description="linear · Dot 개별 색상 + 값 Label">
|
||||
<ChartSpecimen title="Straight · Color Dot + Label">
|
||||
<HMLineChart {...sampleLinePointColors} curve="linear" pointLabels label="월별 접수·완료 직선형 포인트별 색상 선 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Step" description="step · Dot 없음">
|
||||
<ChartSpecimen title="Step">
|
||||
<HMLineChart {...sampleLineCompare} curve="step" dots={false} label="월별 접수·완료 계단형 선 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Step · Dot + Label" description="step · Dot과 값 Label">
|
||||
<ChartSpecimen title="Step · Dot + Label">
|
||||
<HMLineChart {...sampleLineCompare} curve="step" pointLabels label="월별 접수·완료 계단형 Dot·값 라벨 선 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
</StorySectionGrid>
|
||||
|
||||
@@ -21,16 +21,16 @@ export const Radar: Story = {
|
||||
<ChartSampleNotice />
|
||||
{/* Two per row from 1024px, all four in one row from 1600px, one below. */}
|
||||
<StorySectionGrid className={chartGrid.quad}>
|
||||
<ChartSpecimen title="Basic" description="영역">
|
||||
<ChartSpecimen title="Basic">
|
||||
<HMRadarChart {...sampleRadar} label="단계별 처리 수준 방사형 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Dot" description="축별 Dot">
|
||||
<ChartSpecimen title="Dot">
|
||||
<HMRadarChart {...sampleRadar} dots label="단계별 처리 수준 방사형 차트, Dot(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Lines Only" description="선만">
|
||||
<ChartSpecimen title="Lines Only">
|
||||
<HMRadarChart {...sampleRadar} filled={false} label="단계별 처리 수준 방사형 차트, 선만(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Custom Label" description="축 Label">
|
||||
<ChartSpecimen title="Custom Label">
|
||||
<HMRadarChart {...sampleRadar} customLabels label="단계별 처리 수준 방사형 차트, 축 Label(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
</StorySectionGrid>
|
||||
|
||||
@@ -21,22 +21,22 @@ export const Radial: Story = {
|
||||
<ChartSampleNotice />
|
||||
{/* Two per row from 1024px, three from 1200px, one below; source order is the mobile order. */}
|
||||
<StorySectionGrid className={chartGrid.compact}>
|
||||
<ChartSpecimen title="Basic" description="radial-simple · 항목별 막대">
|
||||
<ChartSpecimen title="Basic" description="항목별 막대">
|
||||
<HMRadialBarChart {...sampleRadial} label="업무 상태 방사형 막대 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Label" description="radial-label · 막대 안 항목명">
|
||||
<ChartSpecimen title="Label" description="막대 안 항목명">
|
||||
<HMRadialBarChart {...sampleRadial} labels startAngle={-90} endAngle={380} label="업무 상태 방사형 막대 차트, 항목명 Label(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Grid" description="radial-grid · 원형 Grid">
|
||||
<ChartSpecimen title="Grid" description="원형 Grid">
|
||||
<HMRadialBarChart {...sampleRadial} grid label="업무 상태 방사형 막대 차트, Grid(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Text" description="radial-text · 중앙 Text">
|
||||
<ChartSpecimen title="Text" description="중앙 Text">
|
||||
<HMRadialMeterChart {...sampleRadialText} startAngle={0} endAngle={250} innerRadius={75} outerRadius={90} trackRadii={[88.5, 76.5]} cornerRadius={10} label="진행 중 건수 방사형 차트, 중앙 Text(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Shape" description="radial-shape · 링 트랙">
|
||||
<ChartSpecimen title="Shape" description="링 트랙">
|
||||
<HMRadialMeterChart {...sampleRadialShape} startAngle={0} endAngle={100} innerRadius={50} outerRadius={95} trackRadii={[90.5, 54.5]} label="진행 중 건수 방사형 차트, Shape(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
<ChartSpecimen title="Stacked" description="radial-stacked · 반원 누적">
|
||||
<ChartSpecimen title="Stacked" description="반원 누적">
|
||||
<HMRadialStackedChart {...sampleRadialStacked} label="업무 상태 방사형 Stacked 차트(예시 데이터)" />
|
||||
</ChartSpecimen>
|
||||
</StorySectionGrid>
|
||||
|
||||
@@ -57,7 +57,16 @@ export function CoreComponentPreview({ kind, data = fixture, variant = 0, cardKi
|
||||
if (kind === 'Logo') {
|
||||
component = createLogoLegend(document, context, controller.signal);
|
||||
// Overview already supplies the section heading; keep the shared logo list intact.
|
||||
if (hideLogoHeading) component.querySelector(':scope > h2')?.remove();
|
||||
if (hideLogoHeading) {
|
||||
component.querySelector(':scope > h2')?.remove();
|
||||
// Without the heading, the list's UA top margin (1em = 13px) would sit under the Storybook section title
|
||||
// (2026-11-13: title → content stays the common 12px). Package CSS is unchanged.
|
||||
const list = component.querySelector<HTMLElement>(':scope > .hm-orgchart-logo-list');
|
||||
if (list) list.style.marginTop = '0';
|
||||
// 2026-11-13: 가족사 현황 page shows the 7 logo items without a card border. Padding, size and the
|
||||
// grid gap stay, so items remain separated by space only. Package CSS is unchanged.
|
||||
list?.querySelectorAll<HTMLElement>(':scope > li').forEach((item) => { item.style.border = '0'; });
|
||||
}
|
||||
}
|
||||
else if (kind === 'DepartmentColors') component = createDepartmentColors(document, context, controller.signal);
|
||||
else if (kind === 'PositionSummary') component = presentation === 'workdesk' ? createOrganizationTotals(document, context) : createPositionSummary(document, context);
|
||||
@@ -89,7 +98,9 @@ export function CoreComponentPreview({ kind, data = fixture, variant = 0, cardKi
|
||||
if (compactDepartmentPreview) {
|
||||
// Keep the representative and department cards built by the shared Core.
|
||||
// The Tunnel review frame styles wrappers only; approved artwork stays intact.
|
||||
shell.classList.add('sb-center', 'hm-orgchart-department-preview');
|
||||
// 2026-11-13: `sb-center--plain` drops the review card frame (grey panel, border, header strip),
|
||||
// like the 노선도 pages; the OrgChart artwork itself is untouched.
|
||||
shell.classList.add('sb-center', 'sb-center--plain', 'hm-orgchart-department-preview');
|
||||
const representative = component.querySelector<HTMLElement>(':scope > .hm-orgchart-representative-preview');
|
||||
if (representative) {
|
||||
const card = representative.querySelector<HTMLElement>('[data-card-kind="topLeader"]')!;
|
||||
@@ -139,7 +150,17 @@ export function CoreComponentPreview({ kind, data = fixture, variant = 0, cardKi
|
||||
preview.append(header, stage);
|
||||
return preview;
|
||||
});
|
||||
component.replaceChildren(...(representative ? [representative] : []), ...rows, ...statusPreviews);
|
||||
// 2026-11-13: the six department rows form their own titled section (like 대표자/파견자/휴직자),
|
||||
// keeping their 3-column grid inside it.
|
||||
const departments = document.createElement('section');
|
||||
departments.className = 'tunnel-reference__sample hm-orgchart-departments-section';
|
||||
const departmentsHeader = document.createElement('header');
|
||||
const departmentsTitle = document.createElement('h2'); departmentsTitle.textContent = '부서별 색상';
|
||||
departmentsHeader.append(departmentsTitle);
|
||||
const departmentRows = document.createElement('div'); departmentRows.className = 'hm-orgchart-department-rows';
|
||||
departmentRows.append(...rows);
|
||||
departments.append(departmentsHeader, departmentRows);
|
||||
component.replaceChildren(...(representative ? [representative] : []), departments, ...statusPreviews);
|
||||
component.classList.add('hm-orgchart-department-colors-design', 'tunnel-reference__variant-grid');
|
||||
for (const name of component.querySelectorAll<HTMLElement>('.hm-orgchart-person-name')) name.setAttribute('aria-label', sampleName);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
/* Department Colors Storybook adapter only. The actual Tunnel sample/stage
|
||||
frame comes from center-stories.css; this sheet never selects employee cards. */
|
||||
.hm-orgchart-department-preview .hm-orgchart-department-colors-design {
|
||||
/* Department Colors Storybook adapter only. The specimen wrappers reuse the
|
||||
center-stories.css classes in their `sb-center--plain` form (no grey panel,
|
||||
border or header strip); this sheet never selects employee cards. */
|
||||
|
||||
/* 2026-11-13: Button/Badge page rhythm. Four sections (대표자 / 부서별 색상 / 파견자 / 휴직자) sit
|
||||
the common section gap apart; 파견자·휴직자 share a row on desktop, columns 24px apart. */
|
||||
.sb-center.hm-orgchart-department-preview .hm-orgchart-department-colors-design {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--sb-center-column-gap);
|
||||
gap: var(--sb-page-section-gap, 28px) 24px;
|
||||
}
|
||||
|
||||
.hm-orgchart-department-preview .hm-orgchart-departments-section {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* The six department rows keep their 3-column grid, 24px apart, 12px under the section title. */
|
||||
.hm-orgchart-department-preview .hm-orgchart-department-rows {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 24px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.hm-orgchart-department-preview .hm-orgchart-color-preview {
|
||||
@@ -14,8 +30,17 @@
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Section titles: the common StorySection title (16/24/700, primary); department bars are not selected. */
|
||||
.sb-center.hm-orgchart-department-preview :is(.hm-orgchart-representative-preview, .hm-orgchart-departments-section, .hm-orgchart-status-preview) > header > h2 {
|
||||
margin: 0;
|
||||
color: var(--hm-sem-fg-primary);
|
||||
font-size: var(--sb-page-section-title-font-size, 16px);
|
||||
line-height: var(--sb-page-section-title-line-height, 24px);
|
||||
font-weight: var(--hm-font-weight-bold, 700);
|
||||
}
|
||||
|
||||
/* Group titles are OrgChart artwork. Preserve their established typography,
|
||||
white text and organization gradient within the common review header. */
|
||||
white text and organization gradient; the bar spans its column. */
|
||||
.hm-orgchart-department-preview .hm-orgchart-color-preview > header > .hm-orgchart-color-department-name {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
@@ -25,10 +50,11 @@
|
||||
font-weight: var(--hm-orgchart-resolved-font-weight-bold);
|
||||
}
|
||||
|
||||
/* Specimens start on the label's left edge; wide content keeps the stage's local scroll. */
|
||||
.hm-orgchart-department-preview .hm-orgchart-department-stage {
|
||||
display: grid;
|
||||
justify-items: safe center;
|
||||
align-items: center;
|
||||
justify-items: start;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.hm-orgchart-department-preview .hm-orgchart-preview-grid {
|
||||
@@ -36,21 +62,20 @@
|
||||
gap: var(--sb-center-column-gap);
|
||||
}
|
||||
|
||||
/* Keep Fixed's status presenter inside the existing review surface, without
|
||||
introducing the full canvas background or coordinates into the specimen. */
|
||||
/* Keep Fixed's status presenter without the full canvas background or coordinates. */
|
||||
.hm-orgchart-department-preview .hm-orgchart-status-preview-composition {
|
||||
width: max-content;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.hm-orgchart-department-preview .hm-orgchart-department-colors-design {
|
||||
.sb-center.hm-orgchart-department-preview :is(.hm-orgchart-department-colors-design, .hm-orgchart-department-rows) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.hm-orgchart-department-preview .hm-orgchart-department-colors-design {
|
||||
.sb-center.hm-orgchart-department-preview :is(.hm-orgchart-department-colors-design, .hm-orgchart-department-rows) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
/* Existing Expanded Story content layout; HM widget styles stay unchanged. */
|
||||
/* Reuse the existing Storybook review card surface through inherited inputs.
|
||||
Only the outer Auto preview consumes these; its controls/artwork stay intact. */
|
||||
/* 2026-11-13: no review card surface (grey panel, border, radius) around the Auto preview; these
|
||||
inputs only reach the outer Auto container, so the canvas, controls and artwork stay intact. */
|
||||
.hm-orgchart-story-expanded .hm-orgchart-story-preview.sb-center {
|
||||
--hm-orgchart-expanded-preview-background: var(--sb-center-card-background);
|
||||
--hm-orgchart-expanded-preview-border: var(--sb-center-card-border);
|
||||
--hm-orgchart-expanded-preview-radius: var(--sb-center-card-radius);
|
||||
--hm-orgchart-expanded-preview-background: transparent;
|
||||
--hm-orgchart-expanded-preview-border: 0;
|
||||
--hm-orgchart-expanded-preview-radius: 0;
|
||||
padding: 0;
|
||||
}
|
||||
/* Preview first: the chart follows the page header with the header's own 24px rhythm. */
|
||||
/* 확장 조직도 page only: no page gutters, so the chart uses the whole preview (like 전체 구성). */
|
||||
.sb-page.hm-orgchart-story-expanded { padding-block: 0; }
|
||||
.sb-page.hm-orgchart-story-expanded > .sb-page-inner { width: 100%; margin-inline: 0; }
|
||||
/* Preview first: the chart is the page's first visible block (the page title is the visually hidden H1), so it starts at the
|
||||
page's top gutter. 2026-11-13: the former margin-top: 24px was removed; the common first-block reset (story-page.css) already
|
||||
overrode it (computed 0px), so nothing moves. */
|
||||
.hm-orgchart-story-expanded .hm-orgchart-story-lead {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@@ -25,8 +25,13 @@ const interactions: OrgChartInteractionOptions = { collapse: true, zoom: true, p
|
||||
// The elements stay in the shadow DOM: the move feedback remains the cards' aria-describedby / live status
|
||||
// (visually hidden), and the OrgChart browser checks still read their text.
|
||||
const previewText = new CSSStyleSheet();
|
||||
// 2026-11-13: also hides the chart title and lets the canvas fill the preview height below the toolbar
|
||||
// (Core's fixed 65vh canvas height and outer padding are overridden here only); toolbar keeps an 8px safe inset.
|
||||
previewText.replaceSync(`
|
||||
.hm-orgchart-date, .hm-orgchart-expanded-selection { display: none; }
|
||||
.hm-orgchart-date, .hm-orgchart-expanded-selection, .hm-orgchart-expanded > .hm-orgchart-heading { display: none; }
|
||||
.hm-orgchart-expanded { height: 100vh; grid-template-rows: auto minmax(0, 1fr); gap: 0; padding: 0; }
|
||||
.hm-orgchart-expanded > .hm-orgchart-expanded-toolbar { padding: 8px; }
|
||||
.hm-orgchart-expanded > .hm-orgchart-viewport { height: auto; min-height: 0; }
|
||||
.hm-orgchart-move-feedback { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
||||
`);
|
||||
function hidePreviewText(container: HTMLElement): () => void {
|
||||
@@ -51,11 +56,11 @@ function ExpandedPreview() {
|
||||
|
||||
return <StoryPage className="hm-orgchart-story-page hm-orgchart-story-expanded">
|
||||
<StoryPageHeader title="확장 조직도" />
|
||||
{/* Preview first: the chart sits right under the page title; its own header already names it, so no section header. */}
|
||||
{/* Preview first: the chart is the first visible block (the page title is the hidden H1); its own header already names it, so no section header. */}
|
||||
<div className="hm-orgchart-story hm-orgchart-story-lead">
|
||||
{error ? <p role="alert" className="hm-orgchart-story-note">{error}</p> : null}
|
||||
<div ref={preview} className="hm-orgchart-story-preview sb-center">
|
||||
<OrgChart dataSource={source} stylesheetUrls={compositionStylesheets} displayMode="readOnly" layoutMode="auto" autoLayout={layout} interactions={interactions} dragDropEnabled={false} selectedCompanyIds={allCompanies} companyTopNodes={topNodes} employeePopup="hm-dialog" className="hm-orgchart-story-chart" onLifecycleError={cause => setError(cause instanceof Error ? cause.message : '조직도 설정을 적용할 수 없습니다.')} onStateChange={state => { if (state.status === 'error') setError(state.issues.map(issue => issue.message).join(' / ')); }} />
|
||||
<OrgChart dataSource={source} stylesheetUrls={compositionStylesheets} displayMode="readOnly" layoutMode="auto" initialView="fit" autoLayout={layout} interactions={interactions} dragDropEnabled={false} selectedCompanyIds={allCompanies} companyTopNodes={topNodes} employeePopup="hm-dialog" className="hm-orgchart-story-chart" onLifecycleError={cause => setError(cause instanceof Error ? cause.message : '조직도 설정을 적용할 수 없습니다.')} onStateChange={state => { if (state.status === 'error') setError(state.issues.map(issue => issue.message).join(' / ')); }} />
|
||||
</div>
|
||||
</div>
|
||||
</StoryPage>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { StrictMode } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { OverviewPreview } from './org-chart-page-previews';
|
||||
|
||||
const meta = { title: '조직도/개요', component: OverviewPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof OverviewPreview>;
|
||||
const meta = { title: '조직도/가족사 현황', component: OverviewPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => <StrictMode><Story /></StrictMode>] } satisfies Meta<typeof OverviewPreview>;
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
export const LogoPersonnelSummary: Story = { name: '개요', parameters: { __id: 'orgchart--logo-personnel-summary' } };
|
||||
export const LogoPersonnelSummary: Story = { name: '가족사 현황', parameters: { __id: 'orgchart--logo-personnel-summary' } };
|
||||
@@ -18,35 +18,33 @@ const compositionStylesheets = { engine: engineUrl, core: coreUrl };
|
||||
export function CompositionPreview() {
|
||||
const host = useRef<HTMLElement>(null);
|
||||
useLayoutEffect(() => centerCompositionPreview(host.current!), []);
|
||||
return <StoryPage className="hm-orgchart-story-page">
|
||||
// 2026-11-13: no section title and no page gutters; Core's own width/height fit fills the preview.
|
||||
return <StoryPage className="hm-orgchart-story-page hm-orgchart-composition-page">
|
||||
<StoryPageHeader title="전체 구성" />
|
||||
<StorySection title="고정형 조직도">
|
||||
<section ref={host} className="hm-orgchart-story hm-orgchart-story-full" aria-label="Full Composition">
|
||||
<OrgChart dataSource={compositionSource} stylesheetUrls={compositionStylesheets} displayMode="readOnly" className="hm-orgchart-story-chart" />
|
||||
</section>
|
||||
</StorySection>
|
||||
</StoryPage>;
|
||||
}
|
||||
|
||||
export function DepartmentTypographyPreview() {
|
||||
return <StoryPage className="hm-orgchart-story-page">
|
||||
<StoryPageHeader title="부서별 색상" />
|
||||
<StorySection title="조직별 컬러 · 카드 미리보기">
|
||||
{/* 2026-11-13: no shared section title; the preview renders its own four section titles. */}
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="DepartmentColors" presentation="design" /></div>
|
||||
</StorySection>
|
||||
</StoryPage>;
|
||||
}
|
||||
|
||||
export function OverviewPreview() {
|
||||
return <StoryPage className="hm-orgchart-story-page">
|
||||
<StoryPageHeader title="개요" />
|
||||
<StorySection title="관계사·회사 로고">
|
||||
<StoryPageHeader title="가족사 현황" />
|
||||
<StorySection title="가족사 로고">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="Logo" hideLogoHeading /></div>
|
||||
</StorySection>
|
||||
<StorySection title="조직·직위별 인원 현황">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="PositionSummary" presentation="workdesk" /></div>
|
||||
</StorySection>
|
||||
<StorySection title="관계사별 인원 현황">
|
||||
<StorySection title="가족사별 인원 현황">
|
||||
<div className="hm-orgchart-story-preview"><CoreComponentPreview kind="AffiliationSummary" presentation="workdesk" /></div>
|
||||
</StorySection>
|
||||
</StoryPage>;
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
.hm-orgchart-story-page .hm-orgchart-story { max-width: none; }
|
||||
.hm-orgchart-story-page .hm-orgchart-story-preview { min-width: 0; max-width: 100%; }
|
||||
|
||||
/* 전체 구성 page only (2026-11-13): no page gutters, so Core's proportional
|
||||
width/height fit uses the whole preview. Other StoryPage layouts are unchanged. */
|
||||
.sb-page.hm-orgchart-composition-page { padding-block: 0; }
|
||||
.sb-page.hm-orgchart-composition-page > .sb-page-inner { width: 100%; margin-inline: 0; }
|
||||
|
||||
.hm-orgchart-story-note {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
@@ -37,11 +37,10 @@
|
||||
.sw-footer-story-row--tight { gap: 0; }
|
||||
.sw-footer-story-inline { display: inline-flex; flex-direction: column; padding: 0; }
|
||||
/* Story page frame only (2026-11-11): the width rows sit in a container (.sw-story-responsive), where margins do not collapse,
|
||||
so the rows own the common section gap (story-page.css) and their sections drop their own top margin. */
|
||||
so their sections drop their own top margin. 2026-11-13: the container is also .sw-story-stack (sw-stories.css, as on the
|
||||
SW Header / Viewer / Step Header pages), whose flex gap (the common section gap) spaces the rows; hidden rows
|
||||
(sw-story-responsive-hidden, rows wider than the Story column) leave no gap, so the former sibling margins are gone. */
|
||||
.sb-page .sw-footer-story-width > .sb-page-section { margin-top: 0; }
|
||||
.sw-footer-story-width + .sw-footer-story-width { margin-top: var(--sb-page-section-gap, 24px); }
|
||||
/* The hidden 1920px row (Responsive More, narrower than 1920px) takes its gap with it. */
|
||||
@container sw-story-page (max-width: 2039.98px) { .sw-story-responsive-hidden + .sw-footer-story-width { margin-top: 0; } }
|
||||
.sw-footer-story-aux-compare { display: grid; grid-template-columns: repeat(auto-fill, minmax(560px, 1fr)); gap: 16px; }
|
||||
.sw-footer-story-aux-row { display: flex; align-items: center; gap: 12px; margin: 0; }
|
||||
.sw-footer-story-aux-row figcaption { flex: 0 0 120px; font-size: 12px; color: #3d4a45; }
|
||||
|
||||
@@ -35,6 +35,7 @@ type Story = StoryObj<typeof meta>
|
||||
function Screen({ width = 1920, canvas = 160, children }: { width?: number | string; canvas?: number; children: ReactNode }) {
|
||||
// 2026-10-22: always fluid (no inner scroller). `width` is a maximum, so the screen background and the footer share
|
||||
// the available width, commands that do not fit go to ▲ More, and an open menu is never clipped.
|
||||
// 2026-11-13: in the narrowest 화면 너비 row, sw-stories.css keeps the row width and scrolls it in a narrower Story column.
|
||||
return (
|
||||
<div className="sw-footer-story-screen" style={{ width: "100%", maxWidth: width }} data-width={width}>
|
||||
<div className="sw-footer-story-canvas" style={{ height: canvas }} aria-label="도면 영역 (예시)" />
|
||||
@@ -78,14 +79,18 @@ function Workspace({ width = "100%" }: { width?: number | string }) {
|
||||
}
|
||||
|
||||
export const FullFooter: Story = {
|
||||
name: "SW 하단 명령어바",
|
||||
name: "SW 명령어바",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="SW 하단 명령어바"description="사용자 제공 실제 화면(2026-10-20) 구성: 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." />
|
||||
{/* 2026-10-22 (user request): no section heading; the live composition follows the page title. Behaviour: command
|
||||
click = select + history, category tabs, empty slot guide, aux toggles, ▲ = command window, Layout tabs, Enter runs. */}
|
||||
<section className="sb-page-section" aria-label="실제 Footer 조합 (동작)">
|
||||
{/* 2026-11-13: below 760px (the narrowest 화면 너비 row) the composition keeps 760px and scrolls inside this section
|
||||
(sw-stories.css .sw-story-fit) instead of widening the page; the footer's own minimum widths are unchanged. */}
|
||||
<section className="sb-page-section sw-story-fit" data-sw-fit-min={760} aria-label="실제 Footer 조합 (동작)">
|
||||
<div className="sw-story-fit__scroll">
|
||||
<Workspace width={1920} />
|
||||
</div>
|
||||
</section>
|
||||
</StoryPage>
|
||||
),
|
||||
@@ -386,12 +391,16 @@ export const Responsive: Story = {
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="SW 하단 명령어바 — 화면 너비"description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
|
||||
{/* Same 1920px row rule as SW Header / SW Viewer Responsive More (sw-stories.css .sw-story-responsive). */}
|
||||
<div className="sw-story-responsive">
|
||||
<div className="sw-story-stack sw-story-responsive">
|
||||
{[1920, 1440, 1280, 950, 860, 760].map(width => (
|
||||
// 2026-10-23: the 1920 row stays inside the Story column (as SW Viewer › Responsive) and is hidden on narrow pages.
|
||||
<div key={width} className={`sw-footer-story-width${width === 1920 ? " sw-story-responsive-hidden" : ""}`}>
|
||||
// 2026-11-13: rows wider than the Story column are hidden and the narrowest row (760) keeps its width and scrolls
|
||||
// (sw-stories.css data-sw-width-row / data-sw-width-min), so every shown row is drawn at its title's width.
|
||||
<div key={width} className={`sw-footer-story-width${width === 1920 ? " sw-story-responsive-hidden" : ""}`} data-sw-width-row={width} data-sw-width-min={width === 760 ? width : undefined}>
|
||||
<StorySection title={`${width}px`}>
|
||||
<div className="sw-story-fit__scroll">
|
||||
<Screen width={width} canvas={90}><ReferenceFooter /></Screen>
|
||||
</div>
|
||||
</StorySection>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -790,18 +790,25 @@ export const ResponsiveMore: Story = {
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="SW Header — 화면 너비" />
|
||||
<div className="sw-story-stack sw-story-responsive">
|
||||
{MORE_WIDTHS.map(width => (
|
||||
{MORE_WIDTHS.map(width => {
|
||||
// 2026-11-13: rows wider than the Story column are hidden and the narrowest row keeps its width and scrolls
|
||||
// (sw-stories.css data-sw-width-row / data-sw-width-min), so every shown row is drawn at its title's width.
|
||||
const min = width === Math.min(...MORE_WIDTHS)
|
||||
return (
|
||||
// 2026-10-23: the 1920 row stays inside the Story column (as SW Viewer › Responsive) and is hidden on narrow pages.
|
||||
<div key={width} className={width === 1920 ? "sw-story-responsive-hidden" : undefined}>
|
||||
<div key={width} className={width === 1920 ? "sw-story-responsive-hidden" : undefined} data-sw-width-row={width} data-sw-width-min={min ? width : undefined}>
|
||||
<StorySection title={`${width}px`}>
|
||||
{/* Same structure as the Logo page (no black frame). This page only shows widths, so the five field dropdowns are
|
||||
held closed with the public controlled props; the open callbacks and header data are unchanged. */}
|
||||
<div className="sw-story-fit__scroll">
|
||||
<div style={{ width: "100%", maxWidth: width }} data-frame-width={width}>
|
||||
<StyleHeader layerMenuOpen={false} colorMenuOpen={false} lineStyleMenuOpen={false} textStyleMenuOpen={false} dimensionStyleMenuOpen={false} />
|
||||
</div>
|
||||
</div>
|
||||
</StorySection>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</StoryPage>
|
||||
),
|
||||
@@ -934,8 +941,8 @@ function PropertyCompare() {
|
||||
<p hidden data-property-log="">{log.join(" → ")}</p>
|
||||
<div className="sw-story-compare-scroll" data-compare-scroll="">
|
||||
<div 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></figcaption>
|
||||
{/* 2026-11-13: no visible caption — the panel title (선택된 객체 - …) already names the state; the figure keeps it as its accessible name. */}
|
||||
{PROPERTY_STATES.map(state => <figure key={state.example} className="sw-story-compare__cell" data-compare={state.example} aria-label={state.title}>
|
||||
<PropertyState example={state.example} onLog={line => setLog(lines => [...lines.slice(-7), `${state.example}: ${line}`])} />
|
||||
</figure>)}
|
||||
</div>
|
||||
|
||||
@@ -170,19 +170,26 @@ export const Responsive: Story = {
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="SW Step Header — 화면 너비" />
|
||||
<div className="sw-story-stack sw-story-responsive">
|
||||
{WIDTHS.map(width => (
|
||||
{WIDTHS.map(width => {
|
||||
// 2026-11-13: rows wider than the Story column are hidden and the narrowest row (1100) keeps its width and scrolls
|
||||
// (sw-stories.css data-sw-width-row / data-sw-width-min), so every shown row is drawn at its title's width.
|
||||
const min = width === Math.min(...WIDTHS)
|
||||
return (
|
||||
// The 1920 row shows only on a page with 1920px of room, like the SW Header / Viewer Responsive pages.
|
||||
<div key={width} className={width === 1920 ? "sw-story-responsive-hidden" : undefined}>
|
||||
<div key={width} className={width === 1920 ? "sw-story-responsive-hidden" : undefined} data-sw-width-row={width} data-sw-width-min={min ? width : undefined}>
|
||||
<StorySection title={`${width}px`}>
|
||||
<div className="sw-story-fit__scroll">
|
||||
<Row max={width}>
|
||||
<div className="sw-story-stack" data-width={width}>
|
||||
<StaticHeader />
|
||||
<MultiHeader step="body-plan" documentTitle="Example project" />
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
</StorySection>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</StoryPage>
|
||||
),
|
||||
|
||||
+50
-12
@@ -1,13 +1,47 @@
|
||||
/* Story canvas only: frames that simulate a header width. Never styles the header itself. */
|
||||
.sw-story-scroller { max-width: 100%; overflow-x: auto; }
|
||||
/* Responsive More pages: the 1920px row (sw-story-responsive-hidden) shows when the page has 1920px of usable width —
|
||||
the page (container sw-story-page, not the browser) minus the Inner's 60px side margins (story-page.css,
|
||||
60px from a 768px viewport), so 2040px (2026-11-05; was 2000px with the former 40px gutters); with less room the whole row —
|
||||
the page (container sw-story-page, not the browser) minus the Inner's 30px side margins (story-page.css,
|
||||
30px from a 768px viewport), so 1980px (2026-11-13; was 2040px with 60px margins, 2000px with the former 40px gutters); with less room the whole row —
|
||||
title, header, background, margin — is hidden. 2026-10-23: it stays inside the Story column like every other row (the
|
||||
former reach-out of the 80vw Inner, sw-story-responsive-wide, is gone); its preview is at most 1920px wide. */
|
||||
.sw-story-responsive { container-type: inline-size; }
|
||||
.sw-story-responsive { container: sw-story-rows / inline-size; }
|
||||
.sb-page:has(.sw-story-responsive) { container: sw-story-page / inline-size; }
|
||||
@container sw-story-page (max-width: 2039.98px) { .sw-story-responsive-hidden { display: none; } }
|
||||
@container sw-story-page (max-width: 1979.98px) { .sw-story-responsive-hidden { display: none; } }
|
||||
/* 2026-11-13 (user request): a width row is drawn at the width its title names. A row (data-sw-width-row) shows only where the
|
||||
Story column (container sw-story-rows) is at least that wide; the page's narrowest row (data-sw-width-min) always shows and,
|
||||
in a narrower column, keeps its own width (min-width on the frame inside .sw-story-fit__scroll) and scrolls there.
|
||||
Each page keeps its own width set (the components' own breakpoints). min-width and overflow apply only below that width, so
|
||||
open menus are not clipped where the row fits and a row can still be narrowed in place (e.g. the viewer More checks). */
|
||||
@container sw-story-rows (max-width: 1439.98px) { [data-sw-width-row="1440"]:not([data-sw-width-min]) { display: none; } }
|
||||
@container sw-story-rows (max-width: 1365.98px) { [data-sw-width-row="1366"]:not([data-sw-width-min]) { display: none; } }
|
||||
@container sw-story-rows (max-width: 1279.98px) { [data-sw-width-row="1280"]:not([data-sw-width-min]) { display: none; } }
|
||||
@container sw-story-rows (max-width: 949.98px) { [data-sw-width-row="950"]:not([data-sw-width-min]) { display: none; } }
|
||||
@container sw-story-rows (max-width: 859.98px) { [data-sw-width-row="860"]:not([data-sw-width-min]) { display: none; } }
|
||||
@container sw-story-rows (max-width: 759.98px) { [data-sw-width-row="760"]:not([data-sw-width-min]) { display: none; } }
|
||||
@container sw-story-rows (max-width: 1099.98px) {
|
||||
[data-sw-width-min="1100"] .sw-story-fit__scroll { overflow-x: auto; }
|
||||
[data-sw-width-min="1100"] .sw-story-fit__scroll > * { min-width: 1100px; }
|
||||
}
|
||||
@container sw-story-rows (max-width: 759.98px) {
|
||||
[data-sw-width-min="760"] .sw-story-fit__scroll { overflow-x: auto; }
|
||||
[data-sw-width-min="760"] .sw-story-fit__scroll > * { min-width: 760px; }
|
||||
}
|
||||
@container sw-story-rows (max-width: 519.98px) {
|
||||
[data-sw-width-min="520"] .sw-story-fit__scroll { overflow-x: auto; }
|
||||
[data-sw-width-min="520"] .sw-story-fit__scroll > * { min-width: 520px; }
|
||||
}
|
||||
/* The same rule for a single live example (SW 명령어바 760px, Viewer 로고 520px — the narrowest width each page set supports):
|
||||
the example keeps that width and scrolls inside the Story column only when the column is narrower. */
|
||||
.sw-story-fit { container: sw-story-fit / inline-size; }
|
||||
@container sw-story-fit (max-width: 759.98px) {
|
||||
[data-sw-fit-min="760"] .sw-story-fit__scroll { overflow-x: auto; }
|
||||
[data-sw-fit-min="760"] .sw-story-fit__scroll > * { min-width: 760px; }
|
||||
}
|
||||
@container sw-story-fit (max-width: 519.98px) {
|
||||
[data-sw-fit-min="520"] .sw-story-fit__scroll { overflow-x: auto; }
|
||||
[data-sw-fit-min="520"] .sw-story-fit__scroll > * { min-width: 520px; }
|
||||
}
|
||||
.sw-story-frame { background: #060606; }
|
||||
/* Shared drawing-area canvas of the SW Stories (2026-10-19): the SW/Viewer black grid — 40px cells, 1px lines of
|
||||
HM green #266651 at 18%, on #0A0A0B. Used behind the SW Header work area and dropdown room and the viewer frames. */
|
||||
@@ -32,6 +66,9 @@
|
||||
story-page.css, so the 8px stack gap and each section's own top margin do not add up. Headers inside are unchanged. */
|
||||
.sb-page .sw-story-stack:has(> .sb-page-section, > div > .sb-page-section) { gap: var(--sb-page-section-gap); margin-top: var(--sb-page-section-gap); }
|
||||
.sb-page .sw-story-stack > .sb-page-section, .sb-page .sw-story-stack > div > .sb-page-section { margin-top: 0; }
|
||||
/* 2026-11-13: SW Viewer 화면 너비 has a hidden callback log (<p hidden>) between the page H1 and the stack, so the common
|
||||
first-block reset (story-page.css) misses the stack. Only a stack right after the H1 and one hidden element starts at the top. */
|
||||
.sb-page > .sb-page-inner > .sb-page-a11y-title:first-child + [hidden] + .sw-story-stack { margin-top: 0; }
|
||||
/* Window Move Motion: window controls alone on the window-area surface, magnified for comparison. */
|
||||
.sw-story-window { display: flex; width: 152px; height: 48px; background: var(--hm-sw-header-document-background); }
|
||||
.sw-story-zoom { zoom: 3; width: max-content; }
|
||||
@@ -48,22 +85,23 @@
|
||||
/* Property panel 4-state comparison at the panels' own size (314px wide, never shrunk).
|
||||
2026-10-26 (user request): 3 columns — the short panels (none, all) stacked in column 1, the long ones (circle,
|
||||
expanded) side by side in columns 2 and 3. Rows are auto / 1fr so the long panels' height goes to row 2 only and
|
||||
"all" sits 24px under "none". Narrower screens: 2 columns × 2 rows, and one column below 787px; no Inner scroller
|
||||
and no sideways scroll (2026-10-22). 2026-11-05: each step needs its columns plus the Inner's 60px side margins
|
||||
(120, story-page.css) and a ~15px vertical scrollbar (the media queries read the viewport): 3 columns from 1125px
|
||||
(3 × 314 + 2 × 24 = 990), 2 columns from 787px (2 × 314 + 24 = 652). Before: 1080px / 720px with 16–40px gutters.
|
||||
Both steps sit above 768px, where the margins are 60px. The single column always fits: the margins are 16px up to
|
||||
480px and grow to 60px by 768px (story-page.css), so 375 / 390px screens have 343 / 358px for the 314px panel. */
|
||||
"all" sits 24px under "none". Narrower screens: 2 columns × 2 rows, and one column below 727px; no Inner scroller
|
||||
and no sideways scroll (2026-10-22). Each step needs its columns plus the Inner's side margins (story-page.css) and a
|
||||
~15px vertical scrollbar (the media queries read the viewport). 2026-11-13 (30px margins, 60 in all): 3 columns from
|
||||
1065px (3 × 314 + 2 × 24 = 990), 2 columns from 727px (2 × 314 + 24 = 652; the margins are still ~28px there, 652 + 56 + 15
|
||||
= 723). Before: 1125px / 787px with 60px margins (2026-11-05), 1080px / 720px with 16–40px gutters. The single column
|
||||
always fits: the margins are 16px up to 480px and grow to 30px by 768px, so 375 / 390px screens have 343 / 358px for
|
||||
the 314px panel. */
|
||||
.sw-story-compare-page .sw-story-log { margin-top: 12px; }
|
||||
.sw-story-compare.sw-story-compare--grid { display: grid; grid-template-columns: repeat(2, max-content); align-items: start; gap: 24px; }
|
||||
@media (min-width: 1125px) {
|
||||
@media (min-width: 1065px) {
|
||||
.sw-story-compare.sw-story-compare--grid { grid-template-columns: repeat(3, max-content); grid-template-rows: auto 1fr; }
|
||||
.sw-story-compare--grid > [data-compare="none"] { grid-area: 1 / 1; }
|
||||
.sw-story-compare--grid > [data-compare="all"] { grid-area: 2 / 1; }
|
||||
.sw-story-compare--grid > [data-compare="circle"] { grid-area: 1 / 2 / span 2; }
|
||||
.sw-story-compare--grid > [data-compare="expanded"] { grid-area: 1 / 3 / span 2; }
|
||||
}
|
||||
@media (max-width: 786.98px) { .sw-story-compare.sw-story-compare--grid { grid-template-columns: max-content; } }
|
||||
@media (max-width: 726.98px) { .sw-story-compare.sw-story-compare--grid { grid-template-columns: 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; }
|
||||
|
||||
@@ -211,8 +211,8 @@ export function PropertiesPanel({ demo, selected = true, ...placement }: { demo:
|
||||
headerActions={selected ? <SWViewerButton variant="ghost" aria-label="선택 확대" title="선택 확대" onClick={() => demo.record("zoomToSelection (미연결)")}><ZoomIn size={16} /></SWViewerButton> : undefined}>
|
||||
{selected ? <>
|
||||
<SWViewerBadge className="sw-viewer-story-props-id">{ENTITY_ID}</SWViewerBadge>
|
||||
<SWViewerSection title="일반 정보 (General)">{rows(ENTITY_ROWS)}</SWViewerSection>
|
||||
<SWViewerSection title="기하 속성 (Geometry)">{rows(GEOMETRY_ROWS)}</SWViewerSection>
|
||||
<SWViewerSection title="일반 정보">{rows(ENTITY_ROWS)}</SWViewerSection>
|
||||
<SWViewerSection title="기하 속성">{rows(GEOMETRY_ROWS)}</SWViewerSection>
|
||||
</> : (
|
||||
<div className="sw-viewer-story-empty"><MousePointerClick size={24} aria-hidden="true" />도면에서 객체(엔티티)를 클릭하면<br />상세 속성이 표시됩니다</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState, type ReactNode } from "react"
|
||||
import { useLayoutEffect, useRef, useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { withSWNotice } from "./sw-story-notice"
|
||||
import {
|
||||
@@ -32,11 +32,12 @@ function Log({ lines }: { lines: string[] }) {
|
||||
}
|
||||
|
||||
/** A viewer screen: header on top, the drawing area below it (panels are placed inside this frame). */
|
||||
function ViewerFrame({ width = 1440, height = 620, children, header }: { width?: number | null; height?: number; header: ReactNode; children?: ReactNode }) {
|
||||
function ViewerFrame({ width = 1440, minWidth, height = 620, children, header }: { width?: number | null; minWidth?: number; height?: number; header: ReactNode; children?: ReactNode }) {
|
||||
return (
|
||||
<div className="sw-story-scroller">
|
||||
{/* Fills the Story column up to `width` (null: the whole column), so panels placed at the right edge stay visible. */}
|
||||
<div className="sw-viewer-story-frame" style={{ width: "100%", maxWidth: width ?? undefined, height }} data-viewer-frame="">
|
||||
{/* Fills the Story column up to `width` (null: the whole column), so panels placed at the right edge stay visible.
|
||||
`minWidth` (설정창, 2026-11-13): the frame keeps that width and the scroller scrolls in a narrower column. */}
|
||||
<div className="sw-viewer-story-frame" style={{ width: "100%", maxWidth: width ?? undefined, minWidth, height }} data-viewer-frame="">
|
||||
{header}
|
||||
<div className="sw-viewer-story-canvas" aria-label="도면 영역 (예시)">{children}</div>
|
||||
</div>
|
||||
@@ -83,9 +84,8 @@ export const ViewerHeader: Story = {
|
||||
{/* The minimize / close example was removed from this Story; the component keeps supporting it
|
||||
(SWHeaderWindowControls at the right end of `trailing`, VIEWER.md §2.1). */}
|
||||
<StoryPageHeader title="SW Viewer Header" />
|
||||
<StorySection title="기본형">
|
||||
{/* 2026-11-13: no "기본형" section title; the viewer is the page's first block. */}
|
||||
<ViewerScreen />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -267,22 +267,57 @@ export const SettingsPanel: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
// 설정창 layout (2026-11-13): the three popups (레이어 314 / 속성 314 / 옵션 340px wide; 420 / 420 / 520px tall) sit in one
|
||||
// row from a 1076px frame (the former fixed 24 / 368 / 712 placement). In a narrower frame SWViewerPanel kept them inside the
|
||||
// frame on top of each other (68px overlap at a 1024px preview), so they move to 2 columns (레이어 · 속성, then 옵션; 속성 ends
|
||||
// at 682px) and, below a 700px frame, to 1 column, and the frame grows to fit. The frame keeps at least 520px (the narrowest
|
||||
// viewer width) and scrolls in a narrower column, so the 340px 옵션 popup always fits (SWViewerPanel width unchanged). Storybook presentation only: the panels keep their public API; each layout
|
||||
// remounts them (key) because defaultPosition is read on mount. The Story column width is measured (useLayoutEffect).
|
||||
type PanelLayout = 3 | 2 | 1
|
||||
type PanelPoint = { left: number; top: number }
|
||||
const PANEL_LAYOUTS: Record<PanelLayout, { height: number; layers: PanelPoint; properties: PanelPoint; options: PanelPoint }> = {
|
||||
3: { height: 620, layers: { left: 24, top: 16 }, properties: { left: 368, top: 16 }, options: { left: 712, top: 16 } },
|
||||
2: { height: 1052, layers: { left: 24, top: 16 }, properties: { left: 368, top: 16 }, options: { left: 24, top: 452 } },
|
||||
1: { height: 1488, layers: { left: 16, top: 16 }, properties: { left: 16, top: 452 }, options: { left: 16, top: 888 } },
|
||||
}
|
||||
function usePanelLayout() {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [layout, setLayout] = useState<PanelLayout>(3)
|
||||
useLayoutEffect(() => {
|
||||
const node = ref.current
|
||||
if (!node) return
|
||||
const update = () => {
|
||||
const width = Math.min(node.clientWidth, 1440)
|
||||
setLayout(width >= 1076 ? 3 : width >= 700 ? 2 : 1)
|
||||
}
|
||||
update()
|
||||
const observer = new ResizeObserver(update)
|
||||
observer.observe(node)
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
return { ref, layout }
|
||||
}
|
||||
|
||||
export const ContentVariants: Story = {
|
||||
// 2026-10-21: the one settings-popup Story (former 설정창 and 같은 프레임, 다른 내부 콘텐츠 pages); ID kept.
|
||||
// 2026-10-22 (user request): the 축척 지정 demo popup was removed.
|
||||
name: "설정창",
|
||||
render: () => {
|
||||
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["layers", "properties", "options"] })
|
||||
const { ref, layout } = usePanelLayout()
|
||||
const place = PANEL_LAYOUTS[layout]
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="SW Viewer — 설정창" />
|
||||
<Log lines={demo.log} />
|
||||
{/* 2026-10-22: 620px fits the tallest popup (옵션, 520px) now that 축척 지정 is gone. */}
|
||||
<ViewerFrame height={620} header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||||
<LayerPanel demo={demo} strategy="absolute" defaultPosition={{ left: 24, top: 16 }} />
|
||||
<PropertiesPanel demo={demo} strategy="absolute" defaultPosition={{ left: 368, top: 16 }} />
|
||||
<OptionsPanel demo={demo} strategy="absolute" defaultPosition={{ left: 712, top: 16 }} />
|
||||
{/* 2026-10-22: 620px fits the tallest popup (옵션, 520px) in the one-row layout now that 축척 지정 is gone. */}
|
||||
<div ref={ref} data-panel-layout={layout}>
|
||||
<ViewerFrame height={place.height} minWidth={520} header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||||
<LayerPanel key={`layers-${layout}`} demo={demo} strategy="absolute" defaultPosition={place.layers} />
|
||||
<PropertiesPanel key={`properties-${layout}`} demo={demo} strategy="absolute" defaultPosition={place.properties} />
|
||||
<OptionsPanel key={`options-${layout}`} demo={demo} strategy="absolute" defaultPosition={place.options} />
|
||||
</ViewerFrame>
|
||||
</div>
|
||||
</StoryPage>
|
||||
)
|
||||
},
|
||||
@@ -328,13 +363,17 @@ export const Logo: Story = {
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader title="SW Viewer — 로고" />
|
||||
<div className="sw-story-stack">
|
||||
{/* 2026-11-13: below the narrowest viewer width (520px, the 화면 너비 page's last row) the header keeps 520px and
|
||||
scrolls inside its section (sw-stories.css .sw-story-fit) instead of widening the page. */}
|
||||
<div className="sw-story-stack sw-story-fit" data-sw-fit-min={520}>
|
||||
{rows.map(row => (
|
||||
<StorySection key={row.title} title={row.title}>
|
||||
{/* 1440px is a maximum (2026-10-22): no scroller; the header fits the available width. */}
|
||||
{/* 1440px is a maximum (2026-10-22): the header fits the available width down to 520px. */}
|
||||
<div className="sw-story-fit__scroll">
|
||||
<div style={{ width: "100%", maxWidth: 1440 }} data-logo-case={row.title}>
|
||||
<SWViewerHeader {...row.props} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
|
||||
</div>
|
||||
</div>
|
||||
</StorySection>
|
||||
))}
|
||||
<Log lines={demo.log} />
|
||||
@@ -357,10 +396,13 @@ export const Responsive: Story = {
|
||||
// 2026-10-22 (user request): the 1920 row no longer reaches out of the content area (no sw-story-responsive-wide);
|
||||
// like the other rows it is a maximum inside the Story column. 2026-10-23: it is hidden on the same narrow pages
|
||||
// as the SW Header / Footer 1920 rows (sw-story-responsive-hidden, sw-stories.css), e.g. a 1920px screen.
|
||||
<div key={width} className={width === 1920 ? "sw-story-responsive-hidden" : undefined}><StorySection title={`${width}px`}>
|
||||
{/* The row width is a maximum (2026-10-22): on a narrower screen the header shrinks with it instead of scrolling. */}
|
||||
{/* Same structure as the Logo page (no scroller / bottom padding), so the spacing matches it. */}
|
||||
// 2026-11-13: rows wider than the Story column are hidden and the narrowest row (520) keeps its width and scrolls
|
||||
// (sw-stories.css data-sw-width-row / data-sw-width-min), so every shown row is drawn at its title's width.
|
||||
<div key={width} className={width === 1920 ? "sw-story-responsive-hidden" : undefined} data-sw-width-row={width} data-sw-width-min={width === 520 ? width : undefined}><StorySection title={`${width}px`}>
|
||||
{/* Same structure as the Logo page (no black frame / bottom padding), so the spacing matches it. */}
|
||||
<div className="sw-story-fit__scroll">
|
||||
<div style={{ width: "100%", maxWidth: width }} data-width={width}><SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} /></div>
|
||||
</div>
|
||||
</StorySection></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user