From ed7e8790ec1965124e00888db2bc857533f8fdcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Fri, 13 Nov 2026 17:58:01 +0900 Subject: [PATCH] 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 --- .storybook/qa/STORY-INVENTORY.md | 42 +- .storybook/qa/story-inventory.json | 160 +++++--- WORKING_CONTEXT.md | 274 +++++++++++++ docs/center-graphic/README.md | 7 + .../components/CenterInformationDisplay.tsx | 9 +- .../components/CenterSpatialInformation.tsx | 13 +- .../src/components/FacilityBatch.tsx | 16 +- .../src/components/TunnelFacility.tsx | 6 +- packages/chart/README.md | 2 +- .../property-panel-comparison.browser.mjs | 48 ++- packages/sw/tests/storybook-inventory.mjs | 30 +- .../storybook-unused-removal.browser.mjs | 2 +- .../viewer-logo-document-menu.browser.mjs | 115 ++++-- src/center/center-facility.stories.tsx | 230 ++++++++++- src/center/center-information.stories.tsx | 7 +- src/center/center-shared.tsx | 8 +- src/center/center-spatial.stories.tsx | 7 +- src/center/center-stories.css | 386 +++++++++++++++++- src/center/center-title.stories.tsx | 62 ++- src/chart/area-chart.stories.tsx | 12 +- src/chart/bar-chart.stories.tsx | 2 +- src/chart/chart-story.tsx | 2 +- src/chart/donut-chart.stories.tsx | 8 +- src/chart/line-chart.stories.tsx | 12 +- src/chart/radar-chart.stories.tsx | 8 +- src/chart/radial-chart.stories.tsx | 12 +- src/org-chart/core-component-preview.tsx | 27 +- src/org-chart/department-colors-preview.css | 47 ++- src/org-chart/org-chart-expanded-story.css | 18 +- src/org-chart/org-chart-expanded.stories.tsx | 11 +- src/org-chart/org-chart-p1.stories.tsx | 4 +- src/org-chart/org-chart-page-previews.tsx | 22 +- src/org-chart/org-chart-stories.css | 5 + src/sw/sw-footer-stories.css | 7 +- src/sw/sw-footer.stories.tsx | 21 +- src/sw/sw-header.stories.tsx | 35 +- src/sw/sw-process-header.stories.tsx | 31 +- src/sw/sw-stories.css | 62 ++- src/sw/sw-viewer-fixture.tsx | 4 +- src/sw/sw-viewer.stories.tsx | 84 +++- 40 files changed, 1525 insertions(+), 333 deletions(-) diff --git a/.storybook/qa/STORY-INVENTORY.md b/.storybook/qa/STORY-INVENTORY.md index 381695b..39c399f 100644 --- a/.storybook/qa/STORY-INVENTORY.md +++ b/.storybook/qa/STORY-INVENTORY.md @@ -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. diff --git a/.storybook/qa/story-inventory.json b/.storybook/qa/story-inventory.json index 3601e38..9b11321 100644 --- a/.storybook/qa/story-inventory.json +++ b/.storybook/qa/story-inventory.json @@ -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", diff --git a/WORKING_CONTEXT.md b/WORKING_CONTEXT.md index cc5ebe3..b27c0e6 100644 --- a/WORKING_CONTEXT.md +++ b/WORKING_CONTEXT.md @@ -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/-examples.tsx`(Storybook 전용)로 옮기고 `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 › 뷰어 › 화면 너비: 숨김 콜백 기록 `
- + {['navy', 'teal', 'orange', 'violet'].map((palette) => ( - + - + - + {CONTENT_STATES.map((state) => (
{state.label} diff --git a/packages/center-graphic/src/components/CenterSpatialInformation.tsx b/packages/center-graphic/src/components/CenterSpatialInformation.tsx index f83271f..b165468 100644 --- a/packages/center-graphic/src/components/CenterSpatialInformation.tsx +++ b/packages/center-graphic/src/components/CenterSpatialInformation.tsx @@ -452,11 +452,6 @@ export function CenterSpatialInformationReferenceDemo() {
@@ -489,9 +483,7 @@ export function CenterSpatialInformationReferenceDemo() {
diff --git a/packages/center-graphic/src/components/FacilityBatch.tsx b/packages/center-graphic/src/components/FacilityBatch.tsx index 1d5238e..035befe 100644 --- a/packages/center-graphic/src/components/FacilityBatch.tsx +++ b/packages/center-graphic/src/components/FacilityBatch.tsx @@ -422,8 +422,8 @@ function FacilityReferenceDemo({ config, alternateName }: { config: FacilityConf return (
- - + +
{alternateName ? ( @@ -435,14 +435,14 @@ function FacilityReferenceDemo({ config, alternateName }: { config: FacilityConf ) : null} - +
- +
{widthNames.map((name, index) => (
{['SHORT', 'MEDIUM', 'LONG'][index]}
@@ -473,7 +473,7 @@ export function RoadsideReferenceDemo() { return (
- +
{tests.map((test) => (
@@ -484,7 +484,7 @@ export function RoadsideReferenceDemo() {
- +
{tests.map((test) => (
@@ -495,11 +495,11 @@ export function RoadsideReferenceDemo() {
- + - +
diff --git a/packages/center-graphic/src/components/TunnelFacility.tsx b/packages/center-graphic/src/components/TunnelFacility.tsx index 1bbd7f3..b5586fb 100644 --- a/packages/center-graphic/src/components/TunnelFacility.tsx +++ b/packages/center-graphic/src/components/TunnelFacility.tsx @@ -178,15 +178,15 @@ export function TunnelReferenceDemo() { return (
- + - +
- +
diff --git a/packages/chart/README.md b/packages/chart/README.md index 7135000..5dbf781 100644 --- a/packages/chart/README.md +++ b/packages/chart/README.md @@ -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. 렌더링 diff --git a/packages/sw/tests/property-panel-comparison.browser.mjs b/packages/sw/tests/property-panel-comparison.browser.mjs index ca4006f..a51b920 100644 --- a/packages/sw/tests/property-panel-comparison.browser.mjs +++ b/packages/sw/tests/property-panel-comparison.browser.mjs @@ -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,21 +145,31 @@ 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') { - // 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 }) - await page.waitForTimeout(150) - const after = await page.evaluate(() => { - const d = document.querySelector('.hm-sw-property__drop')?.getBoundingClientRect() - const f = document.querySelector('[data-compare="expanded"] [data-property-id="layer"] [aria-expanded="true"]')?.getBoundingClientRect() - return d && f ? { dTop: d.top, dBottom: d.bottom, fTop: f.top, fBottom: f.bottom } : null - }) - report.data.dropScroll = { moved, before: info, after } - check('Dropdown follows the 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))) } + // Scroll the page while open (2026-10-22: no Inner scroller): the fixed overlay must stay attached to its field. + // 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 = { 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') + await trigger.evaluate(() => scrollTo({ top: 0, behavior: 'instant' })) + await page.setViewportSize(size) + } // Select a layer value: callback updates only that panel. { const { drop } = await openPicker('all') diff --git a/packages/sw/tests/storybook-inventory.mjs b/packages/sw/tests/storybook-inventory.mjs index 2f24e12..b42cb19 100644 --- a/packages/sw/tests/storybook-inventory.mjs +++ b/packages/sw/tests/storybook-inventory.mjs @@ -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). diff --git a/packages/sw/tests/storybook-unused-removal.browser.mjs b/packages/sw/tests/storybook-unused-removal.browser.mjs index 7159354..37b1d6d 100644 --- a/packages/sw/tests/storybook-unused-removal.browser.mjs +++ b/packages/sw/tests/storybook-unused-removal.browser.mjs @@ -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' diff --git a/packages/sw/tests/viewer-logo-document-menu.browser.mjs b/packages/sw/tests/viewer-logo-document-menu.browser.mjs index 43fe652..a62064b 100644 --- a/packages/sw/tests/viewer-logo-document-menu.browser.mjs +++ b/packages/sw/tests/viewer-logo-document-menu.browser.mjs @@ -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() } diff --git a/src/center/center-facility.stories.tsx b/src/center/center-facility.stories.tsx index 32d5ccf..3c32447 100644 --- a/src/center/center-facility.stories.tsx +++ b/src/center/center-facility.stories.tsx @@ -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 // 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: () => ( - +
{withRepresentation(() => )} - +
), }) +// 터널 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 ( +
+
+ {title} + {note ? {note} : null} +
+
{children}
+
+ ) +} + +function TunnelSpecimens() { + return ( +
+
+ {/* 2026-11-13: no "너비 동작" title row; each column carries the common specimen title (English + Korean note). */} +
+
+
+ {(["positionMap", "actualDetail"] as const).map((variant) => ( +
+
+ {variant === "positionMap" ? "Position Map" : "Actual / Detail"} + {variant === "positionMap" ? "위치도" : "실사화"} +
+
+ SEPARATED + +
+ {TUNNEL_WIDTH_NAMES.map(([size, name, length], index) => ( +
+ {variant === "positionMap" ? size : TUNNEL_ACTUAL_LABELS[index]} + +
+ ))} +
+ ))} +
+
+
+ + + + + + +
+
+ ) +} + +// 교량 · 교차로 · 분기점 · 나들목 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 ( +
+
+
+
+
+ {(["positionMap", "actualDetail"] as const).map((variant) => ( +
+
+ {variant === "positionMap" ? "Position Map" : "Actual / Detail"} + {variant === "positionMap" ? "위치도" : "실사화"} +
+
+ SEPARATED + +
+ {widthNames.map((name, index) => ( +
+ {variant === "positionMap" ? ["SHORT", "MEDIUM", "LONG"][index] : `ACTUAL ${["SHORT", "REF", "LONG"][index]}`} + +
+ ))} +
+ ))} +
+
+
+ + + + + + +
+
+ ) +} + +// 연계 노선 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 ( +
+
+
+
+
+
+
Position Map위치도
+ +
+
Actual / Detail실사화
+
+ SEPARATED + +
+ {ROADSIDE_TESTS.map((test) => ( +
+ {test.actual} + +
+ ))} +
+
+
+
+ + + + + + +
+
+ ) +} + // `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("교량", () => ), name: "교량" } +export const Intersection: Story = { ...page("교차로", () => ), name: "교차로" } +export const Junction: Story = { ...page("분기점", () => ), name: "분기점" } +export const Interchange: Story = { ...page("나들목", () => ), 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: () => ( - {withRepresentation(() => )} +
+ {withRepresentation(() => )} +
), } diff --git a/src/center/center-information.stories.tsx b/src/center/center-information.stories.tsx index e544dd9..5757a00 100644 --- a/src/center/center-information.stories.tsx +++ b/src/center/center-information.stories.tsx @@ -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: () => ( - + {/* 2026-11-13: no visible "개요" section title; indent kept via the common `sb-page-indent`. */} +
{withRepresentation(() => (
))} - +
), } diff --git a/src/center/center-shared.tsx b/src/center/center-shared.tsx index 44dacb5..468a79d 100644 --- a/src/center/center-shared.tsx +++ b/src/center/center-shared.tsx @@ -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 }) => ( -
+// `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 }) => ( +
{children}
) @@ -23,7 +25,7 @@ export const centerTargetResolution = (): CenterTargetResolution => { } export const withRepresentation = (Story: () => ReactNode) => ( - + diff --git a/src/center/center-spatial.stories.tsx b/src/center/center-spatial.stories.tsx index 91cd86d..c505736 100644 --- a/src/center/center-spatial.stories.tsx +++ b/src/center/center-spatial.stories.tsx @@ -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: () => ( - + {/* 2026-11-13: no visible "개요" section title; indent kept via the common `sb-page-indent`. */} +
{withRepresentation(() => )} - +
), } diff --git a/src/center/center-stories.css b/src/center/center-stories.css index e5dcbf6..5890208 100644 --- a/src/center/center-stories.css +++ b/src/center/center-stories.css @@ -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; +} diff --git a/src/center/center-title.stories.tsx b/src/center/center-title.stories.tsx index 7c73c87..4642f01 100644 --- a/src/center/center-title.stories.tsx +++ b/src/center/center-title.stories.tsx @@ -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 }[] = [ + ...(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 ( +
+ {TITLE_SPECIMENS.map(({ key, title, meta, props }, index) => ( +
+
+

{title}

+ {meta} + {index === 0 ? {representationNote()} : null} +
+
+ +
+
+ ))} +
+ ) +} // 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: () => ( - + // 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`). + - - {withRepresentation(() => )} - +
+ {withRepresentation(() => )} +
), } diff --git a/src/chart/area-chart.stories.tsx b/src/chart/area-chart.stories.tsx index eae9803..0ed8195 100644 --- a/src/chart/area-chart.stories.tsx +++ b/src/chart/area-chart.stories.tsx @@ -22,22 +22,22 @@ export const Area: Story = { {/* Same grid and order as Line Chart, then a Stacked | Expanded row. */} - + - + - + - + - + - + diff --git a/src/chart/bar-chart.stories.tsx b/src/chart/bar-chart.stories.tsx index 4178514..247e6ae 100644 --- a/src/chart/bar-chart.stories.tsx +++ b/src/chart/bar-chart.stories.tsx @@ -26,7 +26,7 @@ export const Bar: Story = { - + diff --git a/src/chart/chart-story.tsx b/src/chart/chart-story.tsx index 92b90f8..301d518 100644 --- a/src/chart/chart-story.tsx +++ b/src/chart/chart-story.tsx @@ -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). diff --git a/src/chart/donut-chart.stories.tsx b/src/chart/donut-chart.stories.tsx index 7c45c63..877f458 100644 --- a/src/chart/donut-chart.stories.tsx +++ b/src/chart/donut-chart.stories.tsx @@ -56,17 +56,17 @@ export const Donut: Story = { {/* Two per row from 1024px, three from 1200px, one below; source order is the mobile order. */} - + - + - + - + diff --git a/src/chart/line-chart.stories.tsx b/src/chart/line-chart.stories.tsx index 50a96fa..52862ba 100644 --- a/src/chart/line-chart.stories.tsx +++ b/src/chart/line-chart.stories.tsx @@ -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. */} - + - + - + - + - + - + diff --git a/src/chart/radar-chart.stories.tsx b/src/chart/radar-chart.stories.tsx index 0f5b425..8c83abf 100644 --- a/src/chart/radar-chart.stories.tsx +++ b/src/chart/radar-chart.stories.tsx @@ -21,16 +21,16 @@ export const Radar: Story = { {/* Two per row from 1024px, all four in one row from 1600px, one below. */} - + - + - + - + diff --git a/src/chart/radial-chart.stories.tsx b/src/chart/radial-chart.stories.tsx index fe91301..6677edc 100644 --- a/src/chart/radial-chart.stories.tsx +++ b/src/chart/radial-chart.stories.tsx @@ -21,22 +21,22 @@ export const Radial: Story = { {/* Two per row from 1024px, three from 1200px, one below; source order is the mobile order. */} - + - + - + - + - + - + diff --git a/src/org-chart/core-component-preview.tsx b/src/org-chart/core-component-preview.tsx index d7ec988..f8ea528 100644 --- a/src/org-chart/core-component-preview.tsx +++ b/src/org-chart/core-component-preview.tsx @@ -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(':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(':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(':scope > .hm-orgchart-representative-preview'); if (representative) { const card = representative.querySelector('[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('.hm-orgchart-person-name')) name.setAttribute('aria-label', sampleName); } diff --git a/src/org-chart/department-colors-preview.css b/src/org-chart/department-colors-preview.css index 4bfe34a..775ecf7 100644 --- a/src/org-chart/department-colors-preview.css +++ b/src/org-chart/department-colors-preview.css @@ -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); } } diff --git a/src/org-chart/org-chart-expanded-story.css b/src/org-chart/org-chart-expanded-story.css index 1eb3195..70dcd81 100644 --- a/src/org-chart/org-chart-expanded-story.css +++ b/src/org-chart/org-chart-expanded-story.css @@ -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; } diff --git a/src/org-chart/org-chart-expanded.stories.tsx b/src/org-chart/org-chart-expanded.stories.tsx index ebd294b..1ad4d10 100644 --- a/src/org-chart/org-chart-expanded.stories.tsx +++ b/src/org-chart/org-chart-expanded.stories.tsx @@ -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 - {/* 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. */}
{error ?

{error}

: null}
- setError(cause instanceof Error ? cause.message : '조직도 설정을 적용할 수 없습니다.')} onStateChange={state => { if (state.status === 'error') setError(state.issues.map(issue => issue.message).join(' / ')); }} /> + setError(cause instanceof Error ? cause.message : '조직도 설정을 적용할 수 없습니다.')} onStateChange={state => { if (state.status === 'error') setError(state.issues.map(issue => issue.message).join(' / ')); }} />
; diff --git a/src/org-chart/org-chart-p1.stories.tsx b/src/org-chart/org-chart-p1.stories.tsx index 55090b0..4a02663 100644 --- a/src/org-chart/org-chart-p1.stories.tsx +++ b/src/org-chart/org-chart-p1.stories.tsx @@ -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) => ] } satisfies Meta; +const meta = { title: '조직도/가족사 현황', component: OverviewPreview, parameters: { layout: 'fullscreen', controls: { disable: true } }, decorators: [(Story) => ] } satisfies Meta; export default meta; type Story = StoryObj; -export const LogoPersonnelSummary: Story = { name: '개요', parameters: { __id: 'orgchart--logo-personnel-summary' } }; +export const LogoPersonnelSummary: Story = { name: '가족사 현황', parameters: { __id: 'orgchart--logo-personnel-summary' } }; diff --git a/src/org-chart/org-chart-page-previews.tsx b/src/org-chart/org-chart-page-previews.tsx index 5066d03..2389d96 100644 --- a/src/org-chart/org-chart-page-previews.tsx +++ b/src/org-chart/org-chart-page-previews.tsx @@ -18,35 +18,33 @@ const compositionStylesheets = { engine: engineUrl, core: coreUrl }; export function CompositionPreview() { const host = useRef(null); useLayoutEffect(() => centerCompositionPreview(host.current!), []); - return + // 2026-11-13: no section title and no page gutters; Core's own width/height fit fills the preview. + return - -
- -
-
+
+ +
; } export function DepartmentTypographyPreview() { return - -
-
+ {/* 2026-11-13: no shared section title; the preview renders its own four section titles. */} +
; } export function OverviewPreview() { return - - + +
- +
; diff --git a/src/org-chart/org-chart-stories.css b/src/org-chart/org-chart-stories.css index 939a6e2..32eb625 100644 --- a/src/org-chart/org-chart-stories.css +++ b/src/org-chart/org-chart-stories.css @@ -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; diff --git a/src/sw/sw-footer-stories.css b/src/sw/sw-footer-stories.css index 84f1d9b..660577e 100644 --- a/src/sw/sw-footer-stories.css +++ b/src/sw/sw-footer-stories.css @@ -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; } diff --git a/src/sw/sw-footer.stories.tsx b/src/sw/sw-footer.stories.tsx index 50901cb..bd747bc 100644 --- a/src/sw/sw-footer.stories.tsx +++ b/src/sw/sw-footer.stories.tsx @@ -35,6 +35,7 @@ type Story = StoryObj 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 (
@@ -78,14 +79,18 @@ function Workspace({ width = "100%" }: { width?: number | string }) { } export const FullFooter: Story = { - name: "SW 하단 명령어바", + name: "SW 명령어바", render: () => ( {/* 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. */} -
- + {/* 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. */} +
+
+ +
), @@ -386,12 +391,16 @@ export const Responsive: Story = { {/* Same 1920px row rule as SW Header / SW Viewer Responsive More (sw-stories.css .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. -
+ // 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. +
- +
+ +
))} diff --git a/src/sw/sw-header.stories.tsx b/src/sw/sw-header.stories.tsx index cb346ce..19eddd0 100644 --- a/src/sw/sw-header.stories.tsx +++ b/src/sw/sw-header.stories.tsx @@ -790,18 +790,25 @@ export const ResponsiveMore: Story = {
- {MORE_WIDTHS.map(width => ( - // 2026-10-23: the 1920 row stays inside the Story column (as SW Viewer › Responsive) and is hidden on narrow pages. -
- - {/* 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. */} -
- -
-
-
- ))} + {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. +
+ + {/* 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. */} +
+
+ +
+
+
+
+ ) + })}
), @@ -934,8 +941,8 @@ function PropertyCompare() {
- {PROPERTY_STATES.map(state =>
-
{state.title}
+ {/* 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 =>
setLog(lines => [...lines.slice(-7), `${state.example}: ${line}`])} />
)}
diff --git a/src/sw/sw-process-header.stories.tsx b/src/sw/sw-process-header.stories.tsx index 90f883b..e69d08f 100644 --- a/src/sw/sw-process-header.stories.tsx +++ b/src/sw/sw-process-header.stories.tsx @@ -170,19 +170,26 @@ export const Responsive: Story = {
- {WIDTHS.map(width => ( - // The 1920 row shows only on a page with 1920px of room, like the SW Header / Viewer Responsive pages. -
- - -
- - + {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. +
+ +
+ +
+ + +
+
- -
-
- ))} + +
+ ) + })}
), diff --git a/src/sw/sw-stories.css b/src/sw/sw-stories.css index 9ee384d..f1bd562 100644 --- a/src/sw/sw-stories.css +++ b/src/sw/sw-stories.css @@ -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 (
)} diff --git a/src/sw/sw-viewer.stories.tsx b/src/sw/sw-viewer.stories.tsx index d48f5bd..96566cb 100644 --- a/src/sw/sw-viewer.stories.tsx +++ b/src/sw/sw-viewer.stories.tsx @@ -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 (
- {/* Fills the Story column up to `width` (null: the whole column), so panels placed at the right edge stay visible. */} -
+ {/* 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. */} +
{header}
{children}
@@ -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). */} - - - + {/* 2026-11-13: no "기본형" section title; the viewer is the page's first block. */} + ), } @@ -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 = { + 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(null) + const [layout, setLayout] = useState(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 ( - {/* 2026-10-22: 620px fits the tallest popup (옵션, 520px) now that 축척 지정 is gone. */} - }> - - - - + {/* 2026-10-22: 620px fits the tallest popup (옵션, 520px) in the one-row layout now that 축척 지정 is gone. */} +
+ }> + + + + +
) }, @@ -328,12 +363,16 @@ export const Logo: Story = { return ( -
+ {/* 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. */} +
{rows.map(row => ( - {/* 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. */} +
+
+ +
))} @@ -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. -
- {/* 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. +
+ {/* Same structure as the Logo page (no black frame / bottom padding), so the spacing matches it. */} +
+
+
))}