feat(sw): viewer layer popup bulb icon and all-layers switch
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.
This commit is contained in:
1 parent
1dd8fb3aad
commit
8cb98b373f
16 files changed
+79
-52
No files matched your search
+1
-1
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 일반 모드)
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -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 일반 모드 전용
|
||||
|
||||
@@ -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)).
|
||||
- 눈 아이콘 버튼: `<button type="button">` 24 × 24, radius 4, 16px 아이콘(`currentColor`, 색 `--hm-sw-viewer-panel-heading`), Hover 면 `rgba(154, 146, 132, .3)`, `cursor: pointer`. 표시 중이면 `Eye`, 숨김이면 `EyeOff`(사선이 있는 눈). 숨긴 행은 불투명도 0.45.
|
||||
- 패키지에는 레이어 목록 전용 컴포넌트가 없습니다. 목록은 `SWViewerPanel` > `SWViewerSection surface="plain"` 안에 프로그램이 두고, 위에서부터 검색(선택) · **전체 레이어 Switch** · 목록 순서입니다. 행은 색 점 · 레이어 이름 · 개수(`SWViewerBadge`) · **전구 아이콘 버튼** 순서입니다(Storybook `SW/Viewer › Viewer Header`의 레이어 Popup, 예제 [ExampleH-ViewerMode.tsx](./examples/ExampleH-ViewerMode.tsx)).
|
||||
- 전구 아이콘 버튼: 프로그램의 `<button type="button">` 24 × 24, radius 4, Hover 면 `rgba(154, 146, 132, .3)`, `cursor: pointer` 안에 `<SWLayerVisibilityIcon on={visible} />`(20 × 20, 장식용). 표시 중이면 색 있는 전구, 숨김이면 회색 전구(Header 레이어 메뉴와 같은 그림). 숨긴 행은 불투명도 0.45. 아이콘을 직접 그리거나 `hm-sw-header__*` class를 붙이지 않습니다.
|
||||
- 접근성 이름에 다음 동작을 넣습니다: 표시 중 `"<레이어> 숨기기"`, 숨김 `"<레이어> 표시하기"`(`title`도 같은 동사).
|
||||
- 클릭하면 그 레이어 하나의 가시성만 바꾸고 프로그램의 가시성 Callback·상태를 그대로 호출합니다. 아이콘만 그리고 기능을 빼지 않습니다. 전체 켜기·끄기는 `SWViewerButton tone="green"`.
|
||||
- `SWViewerSwitch`는 옵션 패널의 ON/OFF 설정(그리드·2.5D 등)에만 씁니다. 레이어 가시성에는 `SWViewerSwitch`, Checkbox, ON/OFF 스위치, 임의 컨트롤을 쓰지 않습니다.
|
||||
- 클릭하면 그 레이어 하나의 가시성만 바꾸고 프로그램의 가시성 Callback·상태를 그대로 호출합니다. 아이콘만 그리고 기능을 빼지 않습니다.
|
||||
- 전체 레이어 Switch: `<SWViewerField label="전체 레이어">{id => <SWViewerSwitch id={id} checked={allVisible} onCheckedChange={setAll} />}</SWViewerField>`. `checked`는 모든 레이어가 표시 중일 때만 `true`이고, 일부만 표시 중이면 `false`로 보이며 누르면 전체 표시가 됩니다. ON → 전체 표시, OFF → 전체 숨김. 별도 상태를 두지 않고 레이어 가시성 상태에서 계산하므로 개별 전환 시 자동으로 맞춰집니다. 이전의 `전체 켜기`·`전체 끄기` 버튼 두 개는 쓰지 않습니다.
|
||||
- 그 밖의 `SWViewerSwitch`는 옵션 패널의 ON/OFF 설정(그리드·2.5D 등)에 씁니다.
|
||||
|
||||
## 5. 올바른 사용
|
||||
|
||||
@@ -136,7 +137,7 @@ const groups: SWViewerHeaderGroup[] = [
|
||||
| Header 버튼 글자에 `-webkit-text-stroke`·`border`로 외곽선 만들기 | 외곽선은 패키지 토큰이 이미 적용합니다 |
|
||||
| 메뉴 색·간격을 `#123456`, `margin` 등 임의 값으로 덮기 | 토큰 `--hm-sw-viewer-*` / `--hm-sw-header-*`만 가장 바깥 SW 범위(`<SWTheme>` 또는 단독 컴포넌트 루트)의 `style`로 조정(기본값 유지 권장, [AI_USAGE.md](./AI_USAGE.md) §3.1) |
|
||||
| Header·Dropdown·More·Panel의 글꼴을 각각 따로 지정, `.hm-sw-viewer-menu` 등 class로 덮기 | 화면을 `<SWTheme style={theme}>` 하나로 감싸면 Header·Dropdown·More·Panel이 같은 값을 상속합니다. 글꼴 기본값은 Noto Sans KR이며 Web UI 글꼴 토큰을 복사하지 않습니다 |
|
||||
| 레이어 Popup의 표시·숨김을 `SWViewerSwitch`·Checkbox·ON/OFF 스위치로 만들기 | §4.1: 눈(`Eye`/`EyeOff`) 아이콘 버튼. Switch는 옵션 설정 전용 |
|
||||
| 레이어 Popup의 레이어별 표시·숨김을 `SWViewerSwitch`·Checkbox·ON/OFF 스위치·눈 아이콘으로 만들기 | §4.1: 전구(`SWLayerVisibilityIcon`) 아이콘 버튼. Switch는 상단 전체 레이어 제어와 옵션 설정에만 |
|
||||
| 존재하지 않는 Props 추측(`items`, `content`, `renderBody` …) | 위 표와 `dist/index.d.ts`가 전부 |
|
||||
| 일반 모드 화면에서 `SWViewerHeader` 사용, 뷰어에서 `SWHeader` 재스타일 | 모드별 Header를 바꿔 끼웁니다(창 최소화·닫기 `SWHeaderWindowControls`를 `trailing` 끝에 두는 §2.1 구성은 허용) |
|
||||
| 기능 없는 메뉴를 가짜 동작으로 연결 | 항목 제외 또는 `disabled` + 미연결 보고 |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// open / close / focus / keyboard / drag behavior. Icons are the program's own (16px, currentColor).
|
||||
import { useState } from "react"
|
||||
import {
|
||||
SWViewerBadge, SWViewerButton, SWViewerField, SWViewerHeader, SWViewerHeaderButton, SWViewerHint, SWViewerInput, SWViewerPanel,
|
||||
SWLayerVisibilityIcon, SWViewerBadge, SWViewerButton, SWViewerField, SWViewerHeader, SWViewerHeaderButton, SWViewerHint, SWViewerInput, SWViewerPanel,
|
||||
SWViewerSection, SWViewerSlider, SWViewerStatus, SWViewerSwitch,
|
||||
type SWViewerHeaderGroup,
|
||||
} from "@hm-design/sw"
|
||||
@@ -17,9 +17,6 @@ const icons = {
|
||||
layers: <Icon d="M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5" />,
|
||||
options: <Icon d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12M20 18h0" />,
|
||||
info: <Icon d="M12 8h.01M11 12h1v5h1M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z" />,
|
||||
// Layer visibility: eye (shown) / eye with a slash (hidden). Never a switch or checkbox (VIEWER.md §4.1).
|
||||
eye: <Icon d="M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z" />,
|
||||
eyeOff: <Icon d="M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM3 3l18 18" />,
|
||||
}
|
||||
const layers = [{ name: "0", color: "#ffffff", count: 120 }, { name: "WALL", color: "#ff3b30", count: 42 }, { name: "DIM", color: "#34c759", count: 18 }]
|
||||
|
||||
@@ -31,7 +28,7 @@ export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => vo
|
||||
const [grid, setGrid] = useState(true)
|
||||
const [zoomSpeed, setZoomSpeed] = useState(1)
|
||||
const [about, setAbout] = useState(false)
|
||||
// Program-owned visibility per layer; each eye button toggles only its own layer.
|
||||
// Program-owned visibility per layer; each bulb button toggles only its own layer.
|
||||
const [hiddenLayers, setHiddenLayers] = useState<ReadonlySet<string>>(new Set())
|
||||
const toggleLayer = (name: string) => {
|
||||
const next = new Set(hiddenLayers), visible = !next.has(name)
|
||||
@@ -39,6 +36,12 @@ export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => vo
|
||||
setHiddenLayers(next)
|
||||
onEvent?.(`layer:${name}=${!visible}`)
|
||||
}
|
||||
// All-layers switch: ON only when every layer is shown. A partial state reads OFF, so turning it on shows all.
|
||||
const allVisible = hiddenLayers.size === 0
|
||||
const setAllLayers = (visible: boolean) => {
|
||||
setHiddenLayers(new Set(visible ? [] : layers.map(layer => layer.name)))
|
||||
onEvent?.(`layer:all=${visible}`)
|
||||
}
|
||||
|
||||
// File open: the header button after the logo (icon + label, icon only when the header is narrow). File name click:
|
||||
// the sample list only (no Open / + New).
|
||||
@@ -73,7 +76,8 @@ export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => vo
|
||||
|
||||
<SWViewerPanel title="레이어" open={panels.layers} onClose={() => setPanels({ ...panels, layers: false })}>
|
||||
<SWViewerSection surface="plain" aria-label="레이어 목록">
|
||||
{/* Layer list: swatch · name · count · eye button. Visibility is the eye icon button, not SWViewerSwitch. */}
|
||||
{/* Top: all-layers SWViewerSwitch. Rows: swatch · name · count · bulb button (SWLayerVisibilityIcon), not a switch. */}
|
||||
<SWViewerField label="전체 레이어">{id => <SWViewerSwitch id={id} checked={allVisible} onCheckedChange={setAllLayers} />}</SWViewerField>
|
||||
<ul className="app-viewer-layers">
|
||||
{layers.map(layer => {
|
||||
const visible = !hiddenLayers.has(layer.name)
|
||||
@@ -82,9 +86,9 @@ export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => vo
|
||||
<span className="app-viewer-layer-swatch" style={{ background: layer.color }} aria-hidden="true" />
|
||||
<span className="app-viewer-layer-name" title={layer.name}>{layer.name}</span>
|
||||
<SWViewerBadge>{layer.count}</SWViewerBadge>
|
||||
<button type="button" className="app-viewer-layer-eye"
|
||||
<button type="button" className="app-viewer-layer-bulb"
|
||||
aria-label={`${layer.name} ${visible ? "숨기기" : "표시하기"}`} title={visible ? "숨기기" : "표시하기"}
|
||||
onClick={() => toggleLayer(layer.name)}>{visible ? icons.eye : icons.eyeOff}</button>
|
||||
onClick={() => toggleLayer(layer.name)}><SWLayerVisibilityIcon on={visible} /></button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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); }
|
||||
@@ -44,6 +44,19 @@ export function SWHeaderLayerIconArt({ kind, on }: { kind: SWHeaderLayerIconKind
|
||||
return <img className="hm-sw-header__layer-icon" src={on ? art[kind].on : art[kind].off} alt="" />
|
||||
}
|
||||
|
||||
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 <SWHeaderLayerIconArt kind="visibility" on={on} />
|
||||
}
|
||||
|
||||
const labels: Record<SWHeaderLayerIconKind, string> = { visibility: "표시", freeze: "동결", lock: "잠금" }
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 (
|
||||
<SWViewerPanel title={`레이어 (${LAYERS.length}개)`} open={demo.panels.layers} onClose={() => demo.toggle("layers", false)} {...placement}>
|
||||
<SWViewerSection surface="plain" aria-label="레이어 목록">
|
||||
<div className="sw-viewer-story-toolbar">
|
||||
<SWViewerInput type="search" placeholder="레이어 검색" aria-label="레이어 검색" value={query} onChange={event => setQuery(event.currentTarget.value)} />
|
||||
<SWViewerButton tone="green" onClick={() => set([])}>전체 켜기</SWViewerButton>
|
||||
<SWViewerButton tone="green" onClick={() => set(LAYERS.map(layer => layer.name))}>전체 끄기</SWViewerButton>
|
||||
<SWViewerField label="전체 레이어">{id => <SWViewerSwitch id={id} checked={allVisible}
|
||||
onCheckedChange={next => { set(next ? [] : LAYERS.map(layer => layer.name)); demo.record(`layer:all=${next}`) }} />}</SWViewerField>
|
||||
</div>
|
||||
{filtered.length === 0 ? <SWViewerHint>검색 결과가 없습니다 ("{query}")</SWViewerHint> : (
|
||||
<ul className="sw-viewer-story-list">
|
||||
@@ -126,13 +128,13 @@ export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelP
|
||||
<span className="sw-viewer-story-swatch" style={{ background: layer.color }} aria-hidden="true" />
|
||||
<span className="sw-viewer-story-name" title={layer.name}>{layer.name}</span>
|
||||
<SWViewerBadge>{layer.count}</SWViewerBadge>
|
||||
<button type="button" className="sw-viewer-story-eye" aria-label={`${layer.name} ${visible ? "숨기기" : "표시하기"}`} title={visible ? "숨기기" : "표시하기"}
|
||||
<button type="button" className="sw-viewer-story-bulb" aria-label={`${layer.name} ${visible ? "숨기기" : "표시하기"}`} title={visible ? "숨기기" : "표시하기"}
|
||||
onClick={() => {
|
||||
const next = new Set(demo.hidden)
|
||||
if (visible) next.add(layer.name); else next.delete(layer.name)
|
||||
demo.setHidden(next)
|
||||
demo.record(`layer:${layer.name}=${!visible}`)
|
||||
}}>{visible ? <Eye size={16} /> : <EyeOff size={16} />}</button>
|
||||
}}><SWLayerVisibilityIcon on={visible} /></button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -8,16 +8,18 @@
|
||||
background: var(--sw-story-canvas);
|
||||
}
|
||||
.sw-viewer-story-room { padding-bottom: 320px; }
|
||||
.sw-viewer-story-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 12px 4px; }
|
||||
.sw-viewer-story-toolbar > input { flex: 1 0 100%; }
|
||||
/* Search, then the all-layers switch row (SWViewerField + SWViewerSwitch, 2026-10-22). */
|
||||
.sw-viewer-story-toolbar { display: flex; flex-wrap: wrap; gap: 12px 4px; }
|
||||
.sw-viewer-story-toolbar > * { flex: 1 0 100%; }
|
||||
.sw-viewer-story-list { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
||||
.sw-viewer-story-list li { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 3px; }
|
||||
.sw-viewer-story-list li:hover { background: rgba(154, 146, 132, 0.25); }
|
||||
.sw-viewer-story-list li[data-visible="false"] { opacity: 0.45; }
|
||||
.sw-viewer-story-swatch { flex: 0 0 10px; width: 10px; height: 10px; border: 1px solid var(--hm-sw-viewer-panel-inner-border); border-radius: 9999px; }
|
||||
.sw-viewer-story-name { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: ui-monospace, monospace; font-size: 12px; }
|
||||
.sw-viewer-story-eye { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; color: var(--hm-sw-viewer-panel-heading); cursor: pointer; }
|
||||
.sw-viewer-story-eye:hover { background: rgba(154, 146, 132, 0.3); }
|
||||
/* Visibility button around the public SWLayerVisibilityIcon (header bulb art, 20 x 20); 24 x 24 hit area kept. */
|
||||
.sw-viewer-story-bulb { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; }
|
||||
.sw-viewer-story-bulb:hover { background: rgba(154, 146, 132, 0.3); }
|
||||
.sw-viewer-story-props { margin: 0; }
|
||||
.sw-viewer-story-props > div { display: flex; justify-content: space-between; gap: 12px; padding: 6px 0; border-bottom: 1px solid var(--hm-sw-viewer-panel-inner-border); font-size: 12px; }
|
||||
.sw-viewer-story-props dt { color: var(--hm-sw-viewer-panel-label); }
|
||||
|
||||
Reference in new issue
Block a user