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:
shkim4 committed 2026-10-22 14:18:45 +09:00
1 parent 1dd8fb3aad
commit 8cb98b373f
16 files changed
+79 -52

No files matched your search

+1 -1
View File
@@ -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` - 읽기: `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` - 재사용: `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-*` 토큰 - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `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 ```tsx
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
+1 -1
View File
@@ -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` - 읽기: `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` - 재사용: `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-*` 토큰 - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `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 ```tsx
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
+1 -1
View File
@@ -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` - 읽기: `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` - 재사용: `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-*` 토큰 - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `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 ```tsx
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
+1 -1
View File
@@ -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` - 읽기: `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` - 재사용: `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-*` 토큰 - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `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 ```tsx
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
+3 -2
View File
@@ -150,14 +150,15 @@ SW **뷰어 모드** 전용 Export입니다(2026-10-19 추가, 참고 DWG/DXF
| `SWViewerInput` | 28px 패널 Input(native input) | `input` 속성 전체 | ◻ | | `SWViewerInput` | 28px 패널 Input(native input) | `input` 속성 전체 | ◻ |
| `SWViewerButton` | 패널 버튼 | `variant`(`outline` / `primary` / `ghost`), `tone`(`"green"`), `icon`, `button` 속성 | ◻ | | `SWViewerButton` | 패널 버튼 | `variant`(`outline` / `primary` / `ghost`), `tone`(`"green"`), `icon`, `button` 속성 | ◻ |
| `SWViewerBadge` | 개수 Badge | `children`(필수), `span` 속성 | ◻ | | `SWViewerBadge` | 개수 Badge | `children`(필수), `span` 속성 | ◻ |
| `SWLayerVisibilityIcon` | 레이어 Popup 표시·숨김 전구 아이콘(SW Header 레이어 메뉴와 같은 20 × 20 그림, 장식용). 버튼·접근성 이름은 프로그램 담당 | `on: boolean`(필수, `true` 색 있는 전구 / `false` 회색) | ◻ |
| `SWViewerHint` | 12px 보조 설명 | `p` 속성 | ◻ | | `SWViewerHint` | 12px 보조 설명 | `p` 속성 | ◻ |
| `SWViewerStatus` | Header 불러오기 상태(`trailing`) | `state: SWViewerStatusState`(`idle`/`loading`/`ready`/`error`, 필수), `label`, `detail`, `div` 속성 | ◻ | | `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만 사용합니다. - **검증 표시:** 뷰어 Export는 이 카탈로그 기준의 독립 소비 프로젝트(Tarball) 브라우저 검증 기록이 없어 ◻입니다. 예제 H는 공개 Export만 사용합니다.
- 상세 크기·상태는 [VIEWER.md](./VIEWER.md) §2~4가 기준입니다. 이 표에 없는 Props(`items`, `content`, `renderBody` 등)는 없습니다. - 상세 크기·상태는 [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 일반 모드) ## 10. Footer (`SWFooter*`, SW 일반 모드)
+1 -1
View File
@@ -175,7 +175,7 @@ const groups: SWViewerHeaderGroup[] = [
| 오른쪽 끝 | `trailing`: `SWViewerStatus` + `SWViewerHeaderButton iconOnly` | | 오른쪽 끝 | `trailing`: `SWViewerStatus` + `SWViewerHeaderButton iconOnly` |
| 도구 창 | 비모달 `SWViewerPanel`(드래그·크기 조절·접기 기본). 여러 개면 `inset`·`defaultPosition`으로 겹치지 않게 | | 도구 창 | 비모달 `SWViewerPanel`(드래그·크기 조절·접기 기본). 여러 개면 `inset`·`defaultPosition`으로 겹치지 않게 |
| 확인·정보 | `SWViewerPanel modal` + `footer`의 `SWViewerButton variant="primary"` | | 확인·정보 | `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` 없이 아이콘까지만 | | 반응형 | 고정 너비가 아니라 Header의 실제 가용 폭으로 3단계: 기본(아이콘+텍스트) → `iconOnlyBelow` 항목 모두 아이콘만(툴팁 유지) → `collapseBelow` 항목을 하나씩 More로(값이 큰 것, 같으면 오른쪽 먼저). 넓어지면 역순 복귀. 패널 토글은 `collapseBelow` 없이 아이콘까지만 |
| 우클릭 메뉴 | `SWViewerMenu` + `anchorPoint` | | 우클릭 메뉴 | `SWViewerMenu` + `anchorPoint` |
+1 -1
View File
@@ -161,7 +161,7 @@
| 상태 표시 | `SWViewerStatus`: 글자 `--hm-sw-viewer-muted` 11px 400, `error`는 `--hm-sw-viewer-accent`, `loading` 아이콘 회전 | — | | 상태 표시 | `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만 씁니다. - **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를 쓰거나 덮어쓰지 않는다. - **뷰어 신규 컴포넌트 체크리스트:** ① VIEWER.md·카탈로그 §9 컴포넌트로 해결되는지 먼저 본다 ② 위 토큰을 `var(--…)`로만 읽고 일반 모드 전용 모양(§8 `SWPopupRow`, §9 `SWToolButton`)을 섞지 않는다 ③ 상태는 위 표대로 ④ 토큰에 없는 값은 결과 보고에 "AI 구성 값 — 디자인 확인 필요"로 적는다 ⑤ `hm-sw-viewer-*` class를 쓰거나 덮어쓰지 않는다.
## 12. 로고 클릭 메뉴 (`SWHeader homeMenu` / `SWHomeMenu`, Figma 26:10591) — SW 일반 모드 전용 ## 12. 로고 클릭 메뉴 (`SWHeader homeMenu` / `SWHomeMenu`, Figma 26:10591) — SW 일반 모드 전용
+9 -8
View File
@@ -83,7 +83,7 @@
|---|---| |---|---|
| `title`, `open`, `onClose`, `closeLabel?` | 필수는 `title`·`open`·`onClose`. 제목 아이콘은 없습니다(SW 팝업 디자인) | | `title`, `open`, `onClose`, `closeLabel?` | 필수는 `title`·`open`·`onClose`. 제목 아이콘은 없습니다(SW 팝업 디자인) |
| `children` | 스크롤 본문. `SWViewerSection`(카드/plain, 12px 제목), `SWViewerField`(라벨 + 컨트롤, `inline` / `stacked` + `below`), `SWViewerSwitch`, `SWViewerSlider`, `SWViewerInput`, `SWViewerButton`, `SWViewerBadge`, `SWViewerHint`, 프로그램 목록·표 등 무엇이든 | | `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 제목 막대 방식) | | `headerActions?` | 제목 막대 추가 버튼: `SWViewerButton variant="ghost"` (26px, 글자색만 Hover #FFC600 — SW 제목 막대 방식) |
| `width?`(314 = SWPopup), `height?`(420, `"auto"` 가능), `minWidth?`(250), `minHeight?`(140) | 메뉴별 크기. 최대 90vw × 90vh | | `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 아래) | | `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`·`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로 새 팝업을 만들지 않습니다. - **콘텐츠 교체:** 같은 프레임에 다른 `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)). - 패키지에는 레이어 목록 전용 컴포넌트가 없습니다. 목록은 `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, 16px 아이콘(`currentColor`, 색 `--hm-sw-viewer-panel-heading`), Hover 면 `rgba(154, 146, 132, .3)`, `cursor: pointer`. 표시 중이면 `Eye`, 숨김이면 `EyeOff`(사선이 있는 눈). 숨긴 행은 불투명도 0.45. - 전구 아이콘 버튼: 프로그램의 `<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`도 같은 동사). - 접근성 이름에 다음 동작을 넣습니다: 표시 중 `"<레이어> 숨기기"`, 숨김 `"<레이어> 표시하기"`(`title`도 같은 동사).
- 클릭하면 그 레이어 하나의 가시성만 바꾸고 프로그램의 가시성 Callback·상태를 그대로 호출합니다. 아이콘만 그리고 기능을 빼지 않습니다. 전체 켜기·끄기는 `SWViewerButton tone="green"`. - 클릭하면 그 레이어 하나의 가시성만 바꾸고 프로그램의 가시성 Callback·상태를 그대로 호출합니다. 아이콘만 그리고 기능을 빼지 않습니다.
- `SWViewerSwitch`는 옵션 패널의 ON/OFF 설정(그리드·2.5D 등)에만 씁니다. 레이어 가시성에는 `SWViewerSwitch`, Checkbox, ON/OFF 스위치, 임의 컨트롤을 쓰지 않습니다. - 전체 레이어 Switch: `<SWViewerField label="전체 레이어">{id => <SWViewerSwitch id={id} checked={allVisible} onCheckedChange={setAll} />}</SWViewerField>`. `checked`는 모든 레이어가 표시 중일 때만 `true`이고, 일부만 표시 중이면 `false`로 보이며 누르면 전체 표시가 됩니다. ON → 전체 표시, OFF → 전체 숨김. 별도 상태를 두지 않고 레이어 가시성 상태에서 계산하므로 개별 전환 시 자동으로 맞춰집니다. 이전의 `전체 켜기`·`전체 끄기` 버튼 두 개는 쓰지 않습니다.
- 그 밖의 `SWViewerSwitch`는 옵션 패널의 ON/OFF 설정(그리드·2.5D 등)에 씁니다.
## 5. 올바른 사용 ## 5. 올바른 사용
@@ -136,7 +137,7 @@ const groups: SWViewerHeaderGroup[] = [
| Header 버튼 글자에 `-webkit-text-stroke`·`border`로 외곽선 만들기 | 외곽선은 패키지 토큰이 이미 적용합니다 | | Header 버튼 글자에 `-webkit-text-stroke`·`border`로 외곽선 만들기 | 외곽선은 패키지 토큰이 이미 적용합니다 |
| 메뉴 색·간격을 `#123456`, `margin` 등 임의 값으로 덮기 | 토큰 `--hm-sw-viewer-*` / `--hm-sw-header-*`만 가장 바깥 SW 범위(`<SWTheme>` 또는 단독 컴포넌트 루트)의 `style`로 조정(기본값 유지 권장, [AI_USAGE.md](./AI_USAGE.md) §3.1) | | 메뉴 색·간격을 `#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 글꼴 토큰을 복사하지 않습니다 | | 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`가 전부 | | 존재하지 않는 Props 추측(`items`, `content`, `renderBody` …) | 위 표와 `dist/index.d.ts`가 전부 |
| 일반 모드 화면에서 `SWViewerHeader` 사용, 뷰어에서 `SWHeader` 재스타일 | 모드별 Header를 바꿔 끼웁니다(창 최소화·닫기 `SWHeaderWindowControls`를 `trailing` 끝에 두는 §2.1 구성은 허용) | | 일반 모드 화면에서 `SWViewerHeader` 사용, 뷰어에서 `SWHeader` 재스타일 | 모드별 Header를 바꿔 끼웁니다(창 최소화·닫기 `SWHeaderWindowControls`를 `trailing` 끝에 두는 §2.1 구성은 허용) |
| 기능 없는 메뉴를 가짜 동작으로 연결 | 항목 제외 또는 `disabled` + 미연결 보고 | | 기능 없는 메뉴를 가짜 동작으로 연결 | 항목 제외 또는 `disabled` + 미연결 보고 |
@@ -3,7 +3,7 @@
// open / close / focus / keyboard / drag behavior. Icons are the program's own (16px, currentColor). // open / close / focus / keyboard / drag behavior. Icons are the program's own (16px, currentColor).
import { useState } from "react" import { useState } from "react"
import { import {
SWViewerBadge, SWViewerButton, SWViewerField, SWViewerHeader, SWViewerHeaderButton, SWViewerHint, SWViewerInput, SWViewerPanel, SWLayerVisibilityIcon, SWViewerBadge, SWViewerButton, SWViewerField, SWViewerHeader, SWViewerHeaderButton, SWViewerHint, SWViewerInput, SWViewerPanel,
SWViewerSection, SWViewerSlider, SWViewerStatus, SWViewerSwitch, SWViewerSection, SWViewerSlider, SWViewerStatus, SWViewerSwitch,
type SWViewerHeaderGroup, type SWViewerHeaderGroup,
} from "@hm-design/sw" } 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" />, 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" />, 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" />, 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 }] 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 [grid, setGrid] = useState(true)
const [zoomSpeed, setZoomSpeed] = useState(1) const [zoomSpeed, setZoomSpeed] = useState(1)
const [about, setAbout] = useState(false) 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 [hiddenLayers, setHiddenLayers] = useState<ReadonlySet<string>>(new Set())
const toggleLayer = (name: string) => { const toggleLayer = (name: string) => {
const next = new Set(hiddenLayers), visible = !next.has(name) const next = new Set(hiddenLayers), visible = !next.has(name)
@@ -39,6 +36,12 @@ export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => vo
setHiddenLayers(next) setHiddenLayers(next)
onEvent?.(`layer:${name}=${!visible}`) 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: // 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). // 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 })}> <SWViewerPanel title="레이어" open={panels.layers} onClose={() => setPanels({ ...panels, layers: false })}>
<SWViewerSection surface="plain" aria-label="레이어 목록"> <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"> <ul className="app-viewer-layers">
{layers.map(layer => { {layers.map(layer => {
const visible = !hiddenLayers.has(layer.name) 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-swatch" style={{ background: layer.color }} aria-hidden="true" />
<span className="app-viewer-layer-name" title={layer.name}>{layer.name}</span> <span className="app-viewer-layer-name" title={layer.name}>{layer.name}</span>
<SWViewerBadge>{layer.count}</SWViewerBadge> <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 ? "숨기기" : "표시하기"} 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> </li>
) )
})} })}
+4 -3
View File
@@ -35,12 +35,13 @@
border-left: 1px solid var(--hm-sw-header-document-border-color); 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. /* 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 { 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 { 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:hover { background: rgba(154, 146, 132, 0.25); }
.app-viewer-layers > li[data-visible="false"] { opacity: 0.45; } .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-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-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-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-eye:hover { background: rgba(154, 146, 132, 0.3); } .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="" /> 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: "잠금" } const labels: Record<SWHeaderLayerIconKind, string> = { visibility: "표시", freeze: "동결", lock: "잠금" }
/** /**
@@ -7,4 +7,6 @@ export {
type SWViewerSectionProps, type SWViewerFieldProps, type SWViewerSwitchProps, type SWViewerSliderProps, type SWViewerInputProps, type SWViewerSectionProps, type SWViewerFieldProps, type SWViewerSwitchProps, type SWViewerSliderProps, type SWViewerInputProps,
type SWViewerButtonProps, type SWViewerBadgeProps, type SWViewerStatusProps, type SWViewerStatusState, type SWViewerButtonProps, type SWViewerBadgeProps, type SWViewerStatusProps, type SWViewerStatusState,
} from "./SWViewerControls" } 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" export type { SWViewerHeaderItem, SWViewerHeaderGroup, SWViewerMenuEntry, SWViewerCommand } from "./types"
+15 -14
View File
@@ -183,22 +183,23 @@ if (comparison.reference) {
check('collapse hides body', !(await layerPanel.locator('.hm-sw-property__body').isVisible())) check('collapse hides body', !(await layerPanel.locator('.hm-sw-property__body').isVisible()))
await title.dblclick({ position: { x: 30, y: 15 } }) await title.dblclick({ position: { x: 30, y: 15 } })
check('title double click expands', await layerPanel.locator('.hm-sw-property__body').isVisible()) check('title double click expands', await layerPanel.locator('.hm-sw-property__body').isVisible())
// Panel content works (layer eye). // Panel content works. 2026-10-22: per-layer bulb buttons (SWLayerVisibilityIcon) + one all-layers switch on top
// 전체 켜기 / 전체 끄기: green on hover and while pressed, default otherwise; other panel buttons unchanged. // (ON only when every layer is shown; a partial state reads OFF and a click shows all).
const allOn = layerPanel.getByRole('button', { name: '전체 켜기' })
const face = locator => locator.evaluate(element => [getComputedStyle(element).backgroundColor, getComputedStyle(element).color].join(' ')) const face = locator => locator.evaluate(element => [getComputedStyle(element).backgroundColor, getComputedStyle(element).color].join(' '))
const restFace = await face(allOn) const allSwitch = layerPanel.getByRole('switch', { name: '전체 레이어' })
await allOn.hover() const switchOn = async () => (await allSwitch.getAttribute('aria-checked')) === 'true'
const hoverFace = await face(allOn) const hiddenRows = () => layerPanel.locator('li[data-visible="false"]').count()
await page.mouse.down() 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)
const activeFace = await face(allOn) check('all-layers switch ON when all shown, no 전체 켜기 / 끄기 buttons', await switchOn() && (await layerPanel.getByRole('button', { name: /전체 (켜기|끄기)/ }).count()) === 0)
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(' | '))
await layerPanel.getByRole('button', { name: 'CR-TEXT 숨기기' }).click() await layerPanel.getByRole('button', { name: 'CR-TEXT 숨기기' }).click()
check('panel content callback', (await log(page)).includes('layer:CR-TEXT=false')) 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. // Close button hover face.
const close = layerPanel.getByRole('button', { name: '닫기' }) const close = layerPanel.getByRole('button', { name: '닫기' })
await close.hover() await close.hover()
@@ -219,7 +220,7 @@ if (comparison.reference) {
const confirm = about.getByRole('button', { name: '확인' }) const confirm = about.getByRole('button', { name: '확인' })
await confirm.hover() await confirm.hover()
const confirmHover = await face(confirm) 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() 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) 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') for (let index = 0; index < 4; index++) await page.keyboard.press('Tab')
+1 -1
View File
@@ -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` - 읽기: `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` - 재사용: `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-*` 토큰 - 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `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 ```tsx
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw" import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
+8 -6
View File
@@ -2,10 +2,10 @@
// UI state lives here (like a consuming program); no DWG/DXF engine — commands are logged, not executed. // UI state lives here (like a consuming program); no DWG/DXF engine — commands are logged, not executed.
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { 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, type SWHeaderDocument, type SWViewerCommand, type SWViewerHeaderGroup, type SWViewerHeaderProps, type SWViewerPanelProps,
} from "@hm-design/sw" } 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 = [ export const SAMPLES = [
{ id: "sample-basic", label: "기본 도면 · DWG", name: "BasicSample.dwg" }, { 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 [query, setQuery] = useState("")
const filtered = LAYERS.filter(layer => layer.name.toLowerCase().includes(query.trim().toLowerCase())) const filtered = LAYERS.filter(layer => layer.name.toLowerCase().includes(query.trim().toLowerCase()))
const set = (names: string[]) => demo.setHidden(new Set(names)) 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 ( return (
<SWViewerPanel title={`레이어 (${LAYERS.length}개)`} open={demo.panels.layers} onClose={() => demo.toggle("layers", false)} {...placement}> <SWViewerPanel title={`레이어 (${LAYERS.length}개)`} open={demo.panels.layers} onClose={() => demo.toggle("layers", false)} {...placement}>
<SWViewerSection surface="plain" aria-label="레이어 목록"> <SWViewerSection surface="plain" aria-label="레이어 목록">
<div className="sw-viewer-story-toolbar"> <div className="sw-viewer-story-toolbar">
<SWViewerInput type="search" placeholder="레이어 검색" aria-label="레이어 검색" value={query} onChange={event => setQuery(event.currentTarget.value)} /> <SWViewerInput type="search" placeholder="레이어 검색" aria-label="레이어 검색" value={query} onChange={event => setQuery(event.currentTarget.value)} />
<SWViewerButton tone="green" onClick={() => set([])}>전체 켜기</SWViewerButton> <SWViewerField label="전체 레이어">{id => <SWViewerSwitch id={id} checked={allVisible}
<SWViewerButton tone="green" onClick={() => set(LAYERS.map(layer => layer.name))}>전체 끄기</SWViewerButton> onCheckedChange={next => { set(next ? [] : LAYERS.map(layer => layer.name)); demo.record(`layer:all=${next}`) }} />}</SWViewerField>
</div> </div>
{filtered.length === 0 ? <SWViewerHint>검색 결과가 없습니다 ("{query}")</SWViewerHint> : ( {filtered.length === 0 ? <SWViewerHint>검색 결과가 없습니다 ("{query}")</SWViewerHint> : (
<ul className="sw-viewer-story-list"> <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-swatch" style={{ background: layer.color }} aria-hidden="true" />
<span className="sw-viewer-story-name" title={layer.name}>{layer.name}</span> <span className="sw-viewer-story-name" title={layer.name}>{layer.name}</span>
<SWViewerBadge>{layer.count}</SWViewerBadge> <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={() => { onClick={() => {
const next = new Set(demo.hidden) const next = new Set(demo.hidden)
if (visible) next.add(layer.name); else next.delete(layer.name) if (visible) next.add(layer.name); else next.delete(layer.name)
demo.setHidden(next) demo.setHidden(next)
demo.record(`layer:${layer.name}=${!visible}`) demo.record(`layer:${layer.name}=${!visible}`)
}}>{visible ? <Eye size={16} /> : <EyeOff size={16} />}</button> }}><SWLayerVisibilityIcon on={visible} /></button>
</li> </li>
) )
})} })}
+6 -4
View File
@@ -8,16 +8,18 @@
background: var(--sw-story-canvas); background: var(--sw-story-canvas);
} }
.sw-viewer-story-room { padding-bottom: 320px; } .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; } /* Search, then the all-layers switch row (SWViewerField + SWViewerSwitch, 2026-10-22). */
.sw-viewer-story-toolbar > input { flex: 1 0 100%; } .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 { 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 { 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:hover { background: rgba(154, 146, 132, 0.25); }
.sw-viewer-story-list li[data-visible="false"] { opacity: 0.45; } .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-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-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; } /* Visibility button around the public SWLayerVisibilityIcon (header bulb art, 20 x 20); 24 x 24 hit area kept. */
.sw-viewer-story-eye:hover { background: rgba(154, 146, 132, 0.3); } .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 { 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 > 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); } .sw-viewer-story-props dt { color: var(--hm-sw-viewer-panel-label); }