From 8cb98b373f9e5321c8d6c032a6d2031e942dc508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Thu, 22 Oct 2026 14:18:45 +0900 Subject: [PATCH] feat(sw): viewer layer popup bulb icon and all-layers switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-approved 2026-10-22 change to the SW Viewer layer popup. - New public export SWLayerVisibilityIcon({ on }) + SWLayerVisibilityIconProps: the SW Header layer bulb art reused as is (header design and behavior unchanged). - Layer rows: eye (Eye / EyeOff) icon replaced by the bulb, 24 x 24 button kept. - All layers: the 전체 켜기 / 전체 끄기 buttons replaced by one SWViewerField + SWViewerSwitch on top. ON only when every layer is shown; a partial state reads OFF and a click shows all. Derived from the hidden-layer state, so it stays synced. - AI docs (VIEWER.md 4.1, catalog, design rules, composition, AI_GUIDE sync) and example H follow the new rule, replacing the 2026-10-21 eye-icon rule. - viewer.browser.mjs: layer popup checks moved to the bulb / switch behavior. --- docs/ai/AI_GUIDE.md | 2 +- packages/org-chart-react/ai/AI_GUIDE.md | 2 +- packages/org-chart/ai/AI_GUIDE.md | 2 +- packages/sw/ai/AI_GUIDE.md | 2 +- packages/sw/ai/COMPONENT_CATALOG.md | 5 ++-- packages/sw/ai/COMPOSITION_PATTERNS.md | 2 +- packages/sw/ai/DESIGN_RULES.md | 2 +- packages/sw/ai/VIEWER.md | 17 ++++++----- .../sw/ai/examples/ExampleH-ViewerMode.tsx | 20 ++++++++----- packages/sw/ai/examples/example-layout.css | 7 +++-- .../components/SWHeaderLayerIcons.tsx | 13 +++++++++ packages/sw/src/components/sw-viewer/index.ts | 2 ++ packages/sw/tests/viewer.browser.mjs | 29 ++++++++++--------- packages/ui/ai/AI_GUIDE.md | 2 +- src/sw/sw-viewer-fixture.tsx | 14 +++++---- src/sw/sw-viewer-stories.css | 10 ++++--- 16 files changed, 79 insertions(+), 52 deletions(-) diff --git a/docs/ai/AI_GUIDE.md b/docs/ai/AI_GUIDE.md index b11f343..839262a 100644 --- a/docs/ai/AI_GUIDE.md +++ b/docs/ai/AI_GUIDE.md @@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css" - 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx` - 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`. 창 최소화·닫기가 필요한 버전은 `trailing` 맨 끝에 `SWHeaderWindowControls showWindowMove={false}` — VIEWER.md §2.1), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton` - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰 -- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 표시·숨김을 `SWViewerSwitch`·Checkbox로 만들기(눈 `Eye`/`EyeOff` 아이콘 버튼 필수, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) +- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 레이어별 표시·숨김을 `SWViewerSwitch`·Checkbox·눈 아이콘으로 만들기(전구 `SWLayerVisibilityIcon` 아이콘 버튼 필수, 전체 레이어는 상단 `SWViewerSwitch` 하나, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) ```tsx import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" diff --git a/packages/org-chart-react/ai/AI_GUIDE.md b/packages/org-chart-react/ai/AI_GUIDE.md index b11f343..839262a 100644 --- a/packages/org-chart-react/ai/AI_GUIDE.md +++ b/packages/org-chart-react/ai/AI_GUIDE.md @@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css" - 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx` - 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`. 창 최소화·닫기가 필요한 버전은 `trailing` 맨 끝에 `SWHeaderWindowControls showWindowMove={false}` — VIEWER.md §2.1), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton` - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰 -- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 표시·숨김을 `SWViewerSwitch`·Checkbox로 만들기(눈 `Eye`/`EyeOff` 아이콘 버튼 필수, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) +- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 레이어별 표시·숨김을 `SWViewerSwitch`·Checkbox·눈 아이콘으로 만들기(전구 `SWLayerVisibilityIcon` 아이콘 버튼 필수, 전체 레이어는 상단 `SWViewerSwitch` 하나, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) ```tsx import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" diff --git a/packages/org-chart/ai/AI_GUIDE.md b/packages/org-chart/ai/AI_GUIDE.md index b11f343..839262a 100644 --- a/packages/org-chart/ai/AI_GUIDE.md +++ b/packages/org-chart/ai/AI_GUIDE.md @@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css" - 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx` - 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`. 창 최소화·닫기가 필요한 버전은 `trailing` 맨 끝에 `SWHeaderWindowControls showWindowMove={false}` — VIEWER.md §2.1), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton` - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰 -- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 표시·숨김을 `SWViewerSwitch`·Checkbox로 만들기(눈 `Eye`/`EyeOff` 아이콘 버튼 필수, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) +- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 레이어별 표시·숨김을 `SWViewerSwitch`·Checkbox·눈 아이콘으로 만들기(전구 `SWLayerVisibilityIcon` 아이콘 버튼 필수, 전체 레이어는 상단 `SWViewerSwitch` 하나, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) ```tsx import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" diff --git a/packages/sw/ai/AI_GUIDE.md b/packages/sw/ai/AI_GUIDE.md index b11f343..839262a 100644 --- a/packages/sw/ai/AI_GUIDE.md +++ b/packages/sw/ai/AI_GUIDE.md @@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css" - 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx` - 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`. 창 최소화·닫기가 필요한 버전은 `trailing` 맨 끝에 `SWHeaderWindowControls showWindowMove={false}` — VIEWER.md §2.1), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton` - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰 -- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 표시·숨김을 `SWViewerSwitch`·Checkbox로 만들기(눈 `Eye`/`EyeOff` 아이콘 버튼 필수, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) +- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 레이어별 표시·숨김을 `SWViewerSwitch`·Checkbox·눈 아이콘으로 만들기(전구 `SWLayerVisibilityIcon` 아이콘 버튼 필수, 전체 레이어는 상단 `SWViewerSwitch` 하나, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) ```tsx import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" diff --git a/packages/sw/ai/COMPONENT_CATALOG.md b/packages/sw/ai/COMPONENT_CATALOG.md index ea6eeb7..12adc11 100644 --- a/packages/sw/ai/COMPONENT_CATALOG.md +++ b/packages/sw/ai/COMPONENT_CATALOG.md @@ -150,14 +150,15 @@ SW **뷰어 모드** 전용 Export입니다(2026-10-19 추가, 참고 DWG/DXF | `SWViewerInput` | 28px 패널 Input(native input) | `input` 속성 전체 | ◻ | | `SWViewerButton` | 패널 버튼 | `variant`(`outline` / `primary` / `ghost`), `tone`(`"green"`), `icon`, `button` 속성 | ◻ | | `SWViewerBadge` | 개수 Badge | `children`(필수), `span` 속성 | ◻ | +| `SWLayerVisibilityIcon` | 레이어 Popup 표시·숨김 전구 아이콘(SW Header 레이어 메뉴와 같은 20 × 20 그림, 장식용). 버튼·접근성 이름은 프로그램 담당 | `on: boolean`(필수, `true` 색 있는 전구 / `false` 회색) | ◻ | | `SWViewerHint` | 12px 보조 설명 | `p` 속성 | ◻ | | `SWViewerStatus` | Header 불러오기 상태(`trailing`) | `state: SWViewerStatusState`(`idle`/`loading`/`ready`/`error`, 필수), `label`, `detail`, `div` 속성 | ◻ | -- **공개 타입:** `SWViewerHeaderProps`, `SWViewerHeaderButtonProps`, `SWViewerMenuProps`, `SWViewerIconProps`, `SWViewerPanelProps`, `SWViewerPanelPlacement`(`"top-end"` / `"top-start"` / `"center"`), `SWViewerSectionProps`, `SWViewerFieldProps`, `SWViewerSwitchProps`, `SWViewerSliderProps`, `SWViewerInputProps`, `SWViewerButtonProps`, `SWViewerBadgeProps`, `SWViewerStatusProps`, `SWViewerStatusState`, `SWViewerHeaderItem`(`button`·`toggle`·`menu`·`tabs`·`switch`·`custom`), `SWViewerHeaderGroup`, `SWViewerMenuEntry`(`item`·`checkbox`·`radio`·`submenu`·`label`·`separator`), `SWViewerCommand`(`id`, `itemId`, `value`). +- **공개 타입:** `SWViewerHeaderProps`, `SWViewerHeaderButtonProps`, `SWViewerMenuProps`, `SWViewerIconProps`, `SWViewerPanelProps`, `SWViewerPanelPlacement`(`"top-end"` / `"top-start"` / `"center"`), `SWViewerSectionProps`, `SWViewerFieldProps`, `SWViewerSwitchProps`, `SWViewerSliderProps`, `SWViewerInputProps`, `SWViewerButtonProps`, `SWViewerBadgeProps`, `SWLayerVisibilityIconProps`, `SWViewerStatusProps`, `SWViewerStatusState`, `SWViewerHeaderItem`(`button`·`toggle`·`menu`·`tabs`·`switch`·`custom`), `SWViewerHeaderGroup`, `SWViewerMenuEntry`(`item`·`checkbox`·`radio`·`submenu`·`label`·`separator`), `SWViewerCommand`(`id`, `itemId`, `value`). - **검증 표시:** 뷰어 Export는 이 카탈로그 기준의 독립 소비 프로젝트(Tarball) 브라우저 검증 기록이 없어 ◻입니다. 예제 H는 공개 Export만 사용합니다. - 상세 크기·상태는 [VIEWER.md](./VIEWER.md) §2~4가 기준입니다. 이 표에 없는 Props(`items`, `content`, `renderBody` 등)는 없습니다. -- **레이어 Popup 가시성(필수):** SW Viewer 레이어 Popup에서 레이어 표시·숨김 기능은 반드시 HMDesign의 눈(Eye/EyeOff) 아이콘 버튼을 사용한다. Toggle Switch로 대체하지 않는다. 행 구성·크기·접근성 이름은 [VIEWER.md](./VIEWER.md) §4.1, 예제 H. `SWViewerSwitch`는 옵션 ON/OFF 전용입니다. +- **레이어 Popup 가시성(필수, 2026-10-22):** SW Viewer 레이어 Popup에서 레이어별 표시·숨김은 `SWLayerVisibilityIcon`(SW Header 전구)을 담은 아이콘 버튼을 사용한다. 전체 레이어 켜기·끄기는 Popup 상단의 `SWViewerSwitch` 하나(전체 표시일 때만 ON, 일부 표시는 OFF → 누르면 전체 표시)다. 레이어별 표시·숨김을 Switch·Checkbox로 대체하지 않는다. 행 구성·크기·접근성 이름은 [VIEWER.md](./VIEWER.md) §4.1, 예제 H. 그 밖의 `SWViewerSwitch`는 옵션 ON/OFF용입니다. ## 10. Footer (`SWFooter*`, SW 일반 모드) diff --git a/packages/sw/ai/COMPOSITION_PATTERNS.md b/packages/sw/ai/COMPOSITION_PATTERNS.md index ff4af66..8ecc588 100644 --- a/packages/sw/ai/COMPOSITION_PATTERNS.md +++ b/packages/sw/ai/COMPOSITION_PATTERNS.md @@ -175,7 +175,7 @@ const groups: SWViewerHeaderGroup[] = [ | 오른쪽 끝 | `trailing`: `SWViewerStatus` + `SWViewerHeaderButton iconOnly` | | 도구 창 | 비모달 `SWViewerPanel`(드래그·크기 조절·접기 기본). 여러 개면 `inset`·`defaultPosition`으로 겹치지 않게 | | 확인·정보 | `SWViewerPanel modal` + `footer`의 `SWViewerButton variant="primary"` | -| 패널 내용 | `SWViewerSection` > `SWViewerField` > `SWViewerSwitch` / `SWViewerSlider` / `SWViewerInput`, 목록은 `surface="plain"` + 프로그램 목록, 설명 `SWViewerHint`. **레이어 표시·숨김은 눈(`Eye`/`EyeOff`) 아이콘 버튼, Switch 금지**(VIEWER.md §4.1, 예제 H) | +| 패널 내용 | `SWViewerSection` > `SWViewerField` > `SWViewerSwitch` / `SWViewerSlider` / `SWViewerInput`, 목록은 `surface="plain"` + 프로그램 목록, 설명 `SWViewerHint`. **레이어별 표시·숨김은 전구(`SWLayerVisibilityIcon`) 아이콘 버튼(Switch 금지), 전체 레이어는 상단 `SWViewerSwitch` 하나**(VIEWER.md §4.1, 예제 H) | | 반응형 | 고정 너비가 아니라 Header의 실제 가용 폭으로 3단계: 기본(아이콘+텍스트) → `iconOnlyBelow` 항목 모두 아이콘만(툴팁 유지) → `collapseBelow` 항목을 하나씩 More로(값이 큰 것, 같으면 오른쪽 먼저). 넓어지면 역순 복귀. 패널 토글은 `collapseBelow` 없이 아이콘까지만 | | 우클릭 메뉴 | `SWViewerMenu` + `anchorPoint` | diff --git a/packages/sw/ai/DESIGN_RULES.md b/packages/sw/ai/DESIGN_RULES.md index c67c5c3..40077e0 100644 --- a/packages/sw/ai/DESIGN_RULES.md +++ b/packages/sw/ai/DESIGN_RULES.md @@ -161,7 +161,7 @@ | 상태 표시 | `SWViewerStatus`: 글자 `--hm-sw-viewer-muted` 11px 400, `error`는 `--hm-sw-viewer-accent`, `loading` 아이콘 회전 | — | - **Typography(뷰어):** Header 항목 14px 500, 캡션·개수·상태 11px 400, 패널 필드 라벨 14px 500, Section 제목 12px 700, 패널 버튼·Badge 12px 500, Input 12px. Font Weight는 400/500/700만 씁니다. -- **레이어 Popup 가시성(2026-10-21 필수):** SW Viewer 레이어 Popup에서 레이어 표시·숨김 기능은 반드시 HMDesign의 눈(Eye/EyeOff) 아이콘 버튼을 사용한다. Toggle Switch로 대체하지 않는다. 버튼 24 × 24 · radius 4 · 16px 아이콘 · 색 `--hm-sw-viewer-panel-heading` · Hover 면 `rgba(154, 146, 132, .3)` · `cursor: pointer`, 숨긴 행 불투명도 0.45([VIEWER.md](./VIEWER.md) §4.1). +- **레이어 Popup 가시성(2026-10-22 필수, 10-21 눈 아이콘 규정 대체):** SW Viewer 레이어 Popup에서 레이어별 표시·숨김은 SW Header 레이어 메뉴와 같은 전구 아이콘(`SWLayerVisibilityIcon`, 20 × 20, 표시 = 색 있는 전구 / 숨김 = 회색) 버튼을 사용한다. 버튼 24 × 24 · radius 4 · Hover 면 `rgba(154, 146, 132, .3)` · `cursor: pointer`, 숨긴 행 불투명도 0.45. 전체 레이어 켜기·끄기는 Popup 상단 `SWViewerField` + `SWViewerSwitch` 하나(전체 표시일 때만 ON). 레이어별 표시·숨김은 Switch로 대체하지 않는다([VIEWER.md](./VIEWER.md) §4.1). - **뷰어 신규 컴포넌트 체크리스트:** ① VIEWER.md·카탈로그 §9 컴포넌트로 해결되는지 먼저 본다 ② 위 토큰을 `var(--…)`로만 읽고 일반 모드 전용 모양(§8 `SWPopupRow`, §9 `SWToolButton`)을 섞지 않는다 ③ 상태는 위 표대로 ④ 토큰에 없는 값은 결과 보고에 "AI 구성 값 — 디자인 확인 필요"로 적는다 ⑤ `hm-sw-viewer-*` class를 쓰거나 덮어쓰지 않는다. ## 12. 로고 클릭 메뉴 (`SWHeader homeMenu` / `SWHomeMenu`, Figma 26:10591) — SW 일반 모드 전용 diff --git a/packages/sw/ai/VIEWER.md b/packages/sw/ai/VIEWER.md index 84c0b62..b133641 100644 --- a/packages/sw/ai/VIEWER.md +++ b/packages/sw/ai/VIEWER.md @@ -83,7 +83,7 @@ |---|---| | `title`, `open`, `onClose`, `closeLabel?` | 필수는 `title`·`open`·`onClose`. 제목 아이콘은 없습니다(SW 팝업 디자인) | | `children` | 스크롤 본문. `SWViewerSection`(카드/plain, 12px 제목), `SWViewerField`(라벨 + 컨트롤, `inline` / `stacked` + `below`), `SWViewerSwitch`, `SWViewerSlider`, `SWViewerInput`, `SWViewerButton`, `SWViewerBadge`, `SWViewerHint`, 프로그램 목록·표 등 무엇이든 | -| `footer?` | 하단 버튼 영역(본문 스크롤과 무관하게 고정). `SWViewerButton variant="outline" \| "primary"`. `tone="green"` = 기본은 그대로, Hover는 primary(확인) 버튼 Hover와 같은 토큰 `--hm-sw-viewer-hover`, 누르는 동안 primary 면 `--hm-sw-viewer-panel-primary` (예: 레이어 전체 켜기 / 전체 끄기) | +| `footer?` | 하단 버튼 영역(본문 스크롤과 무관하게 고정). `SWViewerButton variant="outline" \| "primary"`. `tone="green"` = 기본은 그대로, Hover는 primary(확인) 버튼 Hover와 같은 토큰 `--hm-sw-viewer-hover`, 누르는 동안 primary 면 `--hm-sw-viewer-panel-primary` (레이어 전체 표시는 버튼이 아니라 §4.1의 전체 레이어 Switch) | | `headerActions?` | 제목 막대 추가 버튼: `SWViewerButton variant="ghost"` (26px, 글자색만 Hover #FFC600 — SW 제목 막대 방식) | | `width?`(314 = SWPopup), `height?`(420, `"auto"` 가능), `minWidth?`(250), `minHeight?`(140) | 메뉴별 크기. 최대 90vw × 90vh | | `defaultPosition?` | `{ left, top }` 또는 `"top-end"`(기본) / `"top-start"` / `"center"`(modal 기본), `inset?: { top?, side? }`(기본 56 / 24 = 52px Header 아래) | @@ -96,15 +96,16 @@ - **콘텐츠 우선순위:** `children`·`footer`는 타입상 `ReactNode`이고 소스 주석도 자유 콘텐츠(“any component”)라고 적습니다. 그래도 기본은 `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton`·`SWViewerBadge`·`SWViewerHint`와 선택 목록 `SWSelect`(축척 지정 단위, 뷰어 패널 토큰으로 표시) 우선입니다. 이것으로 안 되는 컨트롤(예: Select·Checkbox·표)만 `@hm-design/ui` 컴포넌트나 소비 프로젝트 요소를 넣을 수 있습니다. `@hm-design/ui`는 SW 패키지 dependency가 아니어서 별도 설치(사용 유형 혼합)가 필요하고, `SWPopup`과 달리 뷰어 패널 안 `@hm-design/ui` 외형 정합성은 문서화·검증 기록이 없으므로 결과 보고에 “디자인 확인 필요”로 적습니다. Header 안에는 `@hm-design/ui` DropdownMenu 등을 직접 두지 않습니다(§1 표). - **콘텐츠 교체:** 같은 프레임에 다른 `children`을 넣습니다(레이어 목록 / 옵션 / 속성 / 입력 폼 — Storybook `SW/Viewer › Settings Popup`). 프레임을 복사하거나 `hm-sw-viewer-*` class로 새 팝업을 만들지 않습니다. -### 4.1 레이어 Popup — 표시·숨김은 눈 아이콘 버튼 (필수, 2026-10-21) +### 4.1 레이어 Popup — 개별 표시·숨김은 전구 아이콘 버튼, 전체는 Switch (필수, 2026-10-22) -> **규칙:** SW Viewer 레이어 Popup에서 레이어 표시·숨김 기능은 반드시 HMDesign의 눈(Eye/EyeOff) 아이콘 버튼을 사용한다. Toggle Switch로 대체하지 않는다. +> **규칙:** SW Viewer 레이어 Popup에서 레이어별 표시·숨김은 SW Header 레이어 메뉴와 같은 전구 아이콘(`SWLayerVisibilityIcon`) 버튼을 사용한다(2026-10-21 눈 `Eye`/`EyeOff` 규정 대체). 전체 레이어 켜기·끄기는 Popup 상단의 `SWViewerSwitch` 하나로 제어한다. 레이어별 표시·숨김을 Switch·Checkbox로 대체하지 않는다. -- 패키지에는 레이어 목록 전용 컴포넌트가 없습니다. 목록은 `SWViewerPanel` > `SWViewerSection surface="plain"` 안에 프로그램이 두고, 행은 색 점 · 레이어 이름 · 개수(`SWViewerBadge`) · **눈 아이콘 버튼** 순서입니다(Storybook `SW/Viewer › Viewer Header`의 레이어 Popup, 예제 [ExampleH-ViewerMode.tsx](./examples/ExampleH-ViewerMode.tsx)). -- 눈 아이콘 버튼: ` + onClick={() => toggleLayer(layer.name)}> ) })} diff --git a/packages/sw/ai/examples/example-layout.css b/packages/sw/ai/examples/example-layout.css index 9722e72..9fac03d 100644 --- a/packages/sw/ai/examples/example-layout.css +++ b/packages/sw/ai/examples/example-layout.css @@ -35,12 +35,13 @@ border-left: 1px solid var(--hm-sw-header-document-border-color); } /* Viewer layer list (example H), the same look as Storybook SW/Viewer's layer popup. Panel tokens only. - Layer visibility is a 24 x 24 eye icon button (Eye / EyeOff), never a switch or checkbox. */ + Layer visibility is a 24 x 24 button around SWLayerVisibilityIcon (header bulb), never a switch or checkbox; + the all-layers control above the list is SWViewerField + SWViewerSwitch. */ .app-viewer-layers { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; } .app-viewer-layers > li { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 3px; } .app-viewer-layers > li:hover { background: rgba(154, 146, 132, 0.25); } .app-viewer-layers > li[data-visible="false"] { opacity: 0.45; } .app-viewer-layer-swatch { flex: 0 0 10px; width: 10px; height: 10px; border: 1px solid var(--hm-sw-viewer-panel-inner-border); border-radius: 9999px; } .app-viewer-layer-name { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: ui-monospace, monospace; font-size: 12px; } -.app-viewer-layer-eye { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 4px; background: none; color: var(--hm-sw-viewer-panel-heading); cursor: pointer; } -.app-viewer-layer-eye:hover { background: rgba(154, 146, 132, 0.3); } +.app-viewer-layer-bulb { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 4px; background: none; cursor: pointer; } +.app-viewer-layer-bulb:hover { background: rgba(154, 146, 132, 0.3); } diff --git a/packages/sw/src/components/sw-header/components/SWHeaderLayerIcons.tsx b/packages/sw/src/components/sw-header/components/SWHeaderLayerIcons.tsx index 2283f8e..0b957df 100644 --- a/packages/sw/src/components/sw-header/components/SWHeaderLayerIcons.tsx +++ b/packages/sw/src/components/sw-header/components/SWHeaderLayerIcons.tsx @@ -44,6 +44,19 @@ export function SWHeaderLayerIconArt({ kind, on }: { kind: SWHeaderLayerIconKind return } +export interface SWLayerVisibilityIconProps { + /** Colored bulb when `true` (layer shown), gray when `false` (hidden). */ + on: boolean +} + +/** + * Public (2026-10-22): the header layer bulb art (20 x 20, decorative) for a program's own layer list, e.g. the + * viewer layer popup. Art only; the program owns the button, its accessible name and the visibility state. + */ +export function SWLayerVisibilityIcon({ on }: SWLayerVisibilityIconProps) { + return +} + const labels: Record = { visibility: "표시", freeze: "동결", lock: "잠금" } /** diff --git a/packages/sw/src/components/sw-viewer/index.ts b/packages/sw/src/components/sw-viewer/index.ts index 2cac01d..d217dfd 100644 --- a/packages/sw/src/components/sw-viewer/index.ts +++ b/packages/sw/src/components/sw-viewer/index.ts @@ -7,4 +7,6 @@ export { type SWViewerSectionProps, type SWViewerFieldProps, type SWViewerSwitchProps, type SWViewerSliderProps, type SWViewerInputProps, type SWViewerButtonProps, type SWViewerBadgeProps, type SWViewerStatusProps, type SWViewerStatusState, } from "./SWViewerControls" +// Layer popup visibility (2026-10-22): the SW header layer bulb art, reused as is. +export { SWLayerVisibilityIcon, type SWLayerVisibilityIconProps } from "../sw-header/components/SWHeaderLayerIcons" export type { SWViewerHeaderItem, SWViewerHeaderGroup, SWViewerMenuEntry, SWViewerCommand } from "./types" diff --git a/packages/sw/tests/viewer.browser.mjs b/packages/sw/tests/viewer.browser.mjs index d397349..ffcaab1 100644 --- a/packages/sw/tests/viewer.browser.mjs +++ b/packages/sw/tests/viewer.browser.mjs @@ -183,22 +183,23 @@ if (comparison.reference) { check('collapse hides body', !(await layerPanel.locator('.hm-sw-property__body').isVisible())) await title.dblclick({ position: { x: 30, y: 15 } }) check('title double click expands', await layerPanel.locator('.hm-sw-property__body').isVisible()) - // Panel content works (layer eye). - // 전체 켜기 / 전체 끄기: green on hover and while pressed, default otherwise; other panel buttons unchanged. - const allOn = layerPanel.getByRole('button', { name: '전체 켜기' }) + // Panel content works. 2026-10-22: per-layer bulb buttons (SWLayerVisibilityIcon) + one all-layers switch on top + // (ON only when every layer is shown; a partial state reads OFF and a click shows all). const face = locator => locator.evaluate(element => [getComputedStyle(element).backgroundColor, getComputedStyle(element).color].join(' ')) - const restFace = await face(allOn) - await allOn.hover() - const hoverFace = await face(allOn) - await page.mouse.down() - const activeFace = await face(allOn) - await page.mouse.up() - const afterFace = await face(allOn) - await page.mouse.move(5, 5) - const leftFace = await face(allOn) - check('전체 켜기: default kept, green hover / active, back after release', restFace === 'rgb(255, 255, 255) rgb(18, 51, 40)' && hoverFace === 'rgb(38, 102, 81) rgb(255, 255, 255)' && activeFace === 'rgb(18, 51, 40) rgb(255, 255, 255)' && afterFace === hoverFace && leftFace === restFace, [restFace, hoverFace, activeFace, afterFace, leftFace].join(' | ')) + const allSwitch = layerPanel.getByRole('switch', { name: '전체 레이어' }) + const switchOn = async () => (await allSwitch.getAttribute('aria-checked')) === 'true' + const hiddenRows = () => layerPanel.locator('li[data-visible="false"]').count() + check('layer rows use the header bulb icon', (await layerPanel.locator('li button .hm-sw-header__layer-icon--composite').count()) === (await layerPanel.locator('li').count()) && (await layerPanel.locator('li button svg').count()) === 0) + check('all-layers switch ON when all shown, no 전체 켜기 / 끄기 buttons', await switchOn() && (await layerPanel.getByRole('button', { name: /전체 (켜기|끄기)/ }).count()) === 0) await layerPanel.getByRole('button', { name: 'CR-TEXT 숨기기' }).click() check('panel content callback', (await log(page)).includes('layer:CR-TEXT=false')) + check('one layer hidden -> switch OFF (partial)', !(await switchOn()) && (await hiddenRows()) === 1) + await allSwitch.click() + check('partial + switch -> all shown, switch ON', await switchOn() && (await hiddenRows()) === 0) + await allSwitch.click() + check('switch OFF -> all hidden', !(await switchOn()) && (await hiddenRows()) === (await layerPanel.locator('li').count())) + for (const button of await layerPanel.locator('li button').all()) await button.click() + check('each layer shown again -> switch ON (synced)', await switchOn() && (await hiddenRows()) === 0) // Close button hover face. const close = layerPanel.getByRole('button', { name: '닫기' }) await close.hover() @@ -219,7 +220,7 @@ if (comparison.reference) { const confirm = about.getByRole('button', { name: '확인' }) await confirm.hover() const confirmHover = await face(confirm) - check('전체 켜기 hover = 라이선스 확인 hover (same token)', confirmHover === hoverFace, `${confirmHover} / ${hoverFace}`) + check('라이선스 확인 hover = --hm-sw-viewer-hover face', confirmHover === 'rgb(38, 102, 81) rgb(255, 255, 255)', confirmHover) await about.locator('.hm-sw-property__title').hover() check('modal popup opens with backdrop', (await about.getAttribute('aria-modal')) === 'true' && (await page.locator('.hm-sw-viewer-panel-backdrop').count()) === 1) for (let index = 0; index < 4; index++) await page.keyboard.press('Tab') diff --git a/packages/ui/ai/AI_GUIDE.md b/packages/ui/ai/AI_GUIDE.md index b11f343..839262a 100644 --- a/packages/ui/ai/AI_GUIDE.md +++ b/packages/ui/ai/AI_GUIDE.md @@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css" - 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx` - 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`. 창 최소화·닫기가 필요한 버전은 `trailing` 맨 끝에 `SWHeaderWindowControls showWindowMove={false}` — VIEWER.md §2.1), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton` - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰 -- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 표시·숨김을 `SWViewerSwitch`·Checkbox로 만들기(눈 `Eye`/`EyeOff` 아이콘 버튼 필수, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) +- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기(예외: 위 `trailing` 끝의 `SWHeaderWindowControls` 최소화·닫기), 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 레이어 Popup의 레이어별 표시·숨김을 `SWViewerSwitch`·Checkbox·눈 아이콘으로 만들기(전구 `SWLayerVisibilityIcon` 아이콘 버튼 필수, 전체 레이어는 상단 `SWViewerSwitch` 하나, VIEWER.md §4.1), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고) ```tsx import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" diff --git a/src/sw/sw-viewer-fixture.tsx b/src/sw/sw-viewer-fixture.tsx index a826e01..def0763 100644 --- a/src/sw/sw-viewer-fixture.tsx +++ b/src/sw/sw-viewer-fixture.tsx @@ -2,10 +2,10 @@ // UI state lives here (like a consuming program); no DWG/DXF engine — commands are logged, not executed. import { useMemo, useState } from "react" import { - SWViewerBadge, SWViewerButton, SWViewerHeaderButton, SWViewerStatus, SWViewerField, SWViewerHint, SWViewerInput, SWViewerPanel, SWViewerSection, SWViewerSlider, SWViewerSwitch, + SWLayerVisibilityIcon, SWViewerBadge, SWViewerButton, SWViewerHeaderButton, SWViewerStatus, SWViewerField, SWViewerHint, SWViewerInput, SWViewerPanel, SWViewerSection, SWViewerSlider, SWViewerSwitch, type SWHeaderDocument, type SWViewerCommand, type SWViewerHeaderGroup, type SWViewerHeaderProps, type SWViewerPanelProps, } from "@hm-design/sw" -import { Eye, EyeOff, Info, Layers, List, MousePointerClick, RotateCcw, Scan, SlidersHorizontal, ZoomIn } from "lucide-react" +import { Info, Layers, List, MousePointerClick, RotateCcw, Scan, SlidersHorizontal, ZoomIn } from "lucide-react" export const SAMPLES = [ { id: "sample-basic", label: "기본 도면 · DWG", name: "BasicSample.dwg" }, @@ -109,13 +109,15 @@ export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelP const [query, setQuery] = useState("") const filtered = LAYERS.filter(layer => layer.name.toLowerCase().includes(query.trim().toLowerCase())) const set = (names: string[]) => demo.setHidden(new Set(names)) + // 2026-10-22: one switch for all layers. ON only when every layer is shown; a partial state reads OFF, so a click turns all on. + const allVisible = demo.hidden.size === 0 return ( demo.toggle("layers", false)} {...placement}>
setQuery(event.currentTarget.value)} /> - set([])}>전체 켜기 - set(LAYERS.map(layer => layer.name))}>전체 끄기 + {id => { set(next ? [] : LAYERS.map(layer => layer.name)); demo.record(`layer:all=${next}`) }} />}
{filtered.length === 0 ? 검색 결과가 없습니다 ("{query}") : (
    @@ -126,13 +128,13 @@ export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelP