- SWProcessHeader logoText: at least 58px and as wide as the text, instead of a fixed 58px place that cut longer words (home >= 106px) - Align JSDoc (SWHeader, SWViewerHeader, SWProcessHeader) and docs with the content-width logo slot: DESIGN_RULES §1 common rule and §14, COMPONENT_CATALOG, VIEWER.md, SW Header / Step Header specs - No prop or public API change
165 lines
34 KiB
Markdown
165 lines
34 KiB
Markdown
# @hm-design/sw — 뷰어 모드 (Viewer Header · Popup) 사용 지침
|
||
|
||
기준: 2026-10-19, 참고 DWG/DXF 뷰어(`dwg-dxf-viewer-hm`) 상단 메뉴·팝업을 같은 해상도(1440 × 900)에서 실측해 패키지 컴포넌트로 옮긴 결과입니다. 아래 이름은 모두 `from "@hm-design/sw"` 공개 Export이고 CSS는 `@hm-design/sw/styles.css` 한 개입니다. 예제: [examples/ExampleH-ViewerMode.tsx](./examples/ExampleH-ViewerMode.tsx).
|
||
|
||
> **모드 선택 먼저:** 이 문서는 SW **뷰어 모드** 전용입니다. 화면이 뷰어 모드인지는 [AI_USAGE.md](./AI_USAGE.md) §0(설정 `HMDesign 사용 유형: SW 뷰어` → 요청 문구 → 불명확하면 질문)으로 정합니다. 전체 Export 목록은 [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §9, 토큰·상태 요약은 [DESIGN_RULES.md](./DESIGN_RULES.md) §11, 조합은 [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 7입니다.
|
||
>
|
||
> **일반 모드 금지 사항(상호 참조):** 뷰어 화면에 `SWHeader`·`SWPopup`·`SWPopupSection`·`SWPopupRow`·`SWToolButton`·`SWToolGroup`·`SWHeaderPropertyPanel`을 쓰거나 재스타일하지 않고, 일반 모드 화면에 `SWViewer*`와 `--hm-sw-viewer-*` 토큰을 쓰지 않습니다. `SWViewerPanel`이 내부에서 `SWPopup` 프레임을 렌더링하는 것과 두 Header가 `--hm-sw-header-document-*`·`object-*`·`property-*`·외곽선 토큰을 공유하는 것은 패키지 구현이며 모드 혼합이 아닙니다([AI_USAGE.md](./AI_USAGE.md) §0.3). 유일한 예외는 창 최소화·닫기 버전입니다: `SWHeaderWindowControls`(창 이동 없이 최소화·닫기)를 `trailing` 오른쪽 끝에 그대로 두는 구성은 2026-10-21 승인된 구성입니다(§2.1). 패키지에 없는 뷰어 UI는 [AI_USAGE.md](./AI_USAGE.md) §0.4 뷰어 열을 따릅니다.
|
||
|
||
## 1. 언제 무엇을 쓰나
|
||
|
||
| 화면 | 사용 | 쓰지 않음 |
|
||
|---|---|---|
|
||
| SW 일반 모드 Header | `SWHeader` (49px, Figma 26:8944) | `SWViewerHeader` |
|
||
| SW 뷰어 모드 Header | `SWViewerHeader` (52px, 참고 뷰어) | `SWHeader`에 뷰어 메뉴를 억지로 넣기, 직접 만든 툴바 |
|
||
| 뷰어 메뉴 Dropdown·서브메뉴·우클릭 메뉴 | `type: "menu"` 항목의 `entries`, 단독은 `SWViewerMenu` | `@hm-design/ui` DropdownMenu를 Header 안에 직접 배치 |
|
||
| 뷰어 팝업·설정창·도구 패널·입력 팝업 | `SWViewerPanel` (+ `modal`) | 새 프레임 작성, `SWPopup`(일반 SW 속성창 프레임)을 뷰어 디자인으로 재스타일 |
|
||
| 일반 SW 팝업·속성창 | `SWPopup`, `SWHeaderPropertyPanel` (변경 없음) | `SWViewerPanel` |
|
||
|
||
- 한 화면에는 Header 하나만 둡니다. 모드 전환은 프로그램이 Header 컴포넌트를 바꾸는 방식입니다(Storybook `SW/Viewer › SWHeader vs Viewer Header`). 파일·패널 상태는 프로그램 상태라 Header를 바꿔도 유지됩니다.
|
||
- 뷰어 모드 진입·종료, 파일 처리, DWG/DXF 렌더링, 메뉴 명령 실행은 **프로그램 담당**입니다. 패키지는 UI와 공통 인터랙션(열기/닫기, Focus, 키보드, 위치·크기)만 담당합니다.
|
||
- 기능이 없는 메뉴는 만들지 않습니다. 연결할 명령이 없으면 항목을 빼거나 `disabled`로 두고 “미연결”로 보고합니다.
|
||
|
||
## 2. `SWViewerHeader`
|
||
|
||
로고 면(사선) → 파일명 면 → 객체 수정 면(메뉴 그룹) → 오른쪽 끝 영역(`placement: "end"` 그룹 + `trailing`).
|
||
|
||
| Props | 설명 |
|
||
|---|---|
|
||
| `groups: SWViewerHeaderGroup[]` | 메뉴 데이터(순서대로 배치). 그룹: `id`, `label`(접근성 이름·More 구획 제목), `moreLabel?`, `surface?: "plain" \| "field"`(field = 사용자 확정 속성 입력 면), `placement?: "start" \| "end"`, `hidden?`, `items` |
|
||
| 항목 `SWViewerHeaderItem` | 공통: `id`, `label`, `icon?`(16px, 프로그램 제공), `title?`, `disabled?`, `hidden?`, `collapseBelow?`(공간이 부족할 때 More로 옮기는 대상·순서 — 값이 큰 것부터, px 기준 아님), `iconOnlyBelow?`(공간이 부족할 때 아이콘만으로 줄이는 대상, `icon` 필요 — 값의 크기는 무관), `iconOnly?`, `moreLabel?` |
|
||
| `type: "button"` | `onSelect` |
|
||
| `type: "toggle"` | `pressed`, `onPressedChange` (패널 열기/닫기 등) |
|
||
| `type: "menu"` | `entries: SWViewerMenuEntry[]`, `menuWidth?`, `menuAlign?: "start" \| "end"` |
|
||
| `type: "tabs"` | `value`, `options: { value, label, count?, title?, disabled? }[]`, `onValueChange` (Model / Layout). 폭(2026-10-23): 두 버튼은 항상 같은 폭(가장 긴 이름 + 개수 이상이라 겹치지 않음). tabs만 있는 그룹은 Header에 남는 공간만큼 넓어져 field 면 최대 378px(`--hm-sw-viewer-tabs-max-width`), 공간이 적으면 그만큼만 넓어집니다. 기본 폭보다 부족하면(2026-10-26) More로 옮기기 전에 최소 `--hm-sw-viewer-tabs-min-width`(48px)까지 좁아지며 이름부터, 다음으로 개수까지 `…`로 줄고(Header 560px 미만에서는 개수 숨김), 그 최소 폭으로도 들어가지 않을 때만 `collapseBelow` 단계대로 More로 이동합니다. 모양(2026-10-23, Figma 7652:40344): tabs만 있는 그룹은 field 면 없이 옆 field 그룹과 같은 42px(`--hm-sw-viewer-control-height` + 2px) 어두운 컨테이너(검정 여백 위·좌우 1px·아래 1.5px, 안쪽 그림자; 녹색 외곽선은 2026-10-23 제거)가 면이 되고, 버튼(컨테이너를 채워 37.5px)은 같은 Gradient 면과 1.428px Gradient 테두리(선택 금색 #a67d03→#fadd4f→#926e03 + 글자 #ffc600, 비선택 #189b6e→#287a5e + 흰 글자 + 상단 초록 광택), 이름 14px 700·개수 14px 400, 간격 14px, 긴 이름은 버튼 안에서 이름부터 `…`로 줄고 개수는 그대로(두 버튼 같은 폭·최대 378px 유지; 공간 계산에서 이름은 `--hm-sw-viewer-tab-name-basis`(3em)까지만 세므로 긴 이름이 탭 폭을 늘리거나 다른 메뉴의 아이콘화·More 이동을 바꾸지 않음; 전체 이름은 접근 가능한 이름으로 남고 툴팁이 필요하면 option `title`)(버튼 좌우 여백 5px), 객체 면 첫 그룹이 이 탭만 가진 경우에만 그룹 오른쪽 여백 1px(조건부, 다른 Header 그룹에는 없음 — 다음 그룹까지 1180px 이상 5px, 미만 3px; 최대 378px·높이 42px·More 이동 단계 그대로), 글자 외곽선은 기존 그대로입니다. 색은 `--hm-sw-viewer-tabs-*`·`--hm-sw-viewer-tab-*` 토큰으로만 바꿉니다(목록·기본값·Override: [DESIGN_RULES.md](./DESIGN_RULES.md) §11) |
|
||
| `type: "switch"` | `checked`, `onCheckedChange` (2.5D 등, 위 11px 캡션), `shortLabel?`(2026-10-26: Header 560px 미만에서 `label` 대신 보이는 짧은 캡션, 예 `어두운 배경` → `배경`. `label`은 접근 가능한 이름으로 유지, 없으면 `label` 그대로) |
|
||
| `type: "custom"` | `render()`, `menuEntries?`(More에 넣을 항목) |
|
||
| `onCommand(command)` | 모든 명령을 한 곳에서 받습니다: `{ id, itemId, value? }`. 항목 자체 Callback 다음에 호출 |
|
||
| `documentTitle`, `documentPlaceholder`, `documentTooltip`, `document?` | 파일명 면 |
|
||
| `documents?: SWHeaderDocument[]`, `activeDocumentId?`, `onDocumentSelect?(document)` | 파일명 클릭 시 아래 문서 목록(샘플 도면 등). `SWHeader` 파일명 목록(`SWHeaderDocumentTitle`)과 같은 모양·키보드(ArrowDown 열기, Escape, 바깥 클릭)이며 **Open·+ New는 표시하지 않음**. 목록 외곽 너비는 파일명 버튼 외곽 너비와 `SWHeader` 목록 너비(312px) 이상이며 긴 항목이면 넓어집니다. 열려도 로고가 위에 표시됨. 선택 시 프로그램이 파일을 불러오고 `documentTitle`·`activeDocumentId`를 갱신. `document`가 있으면 `document`가 우선 |
|
||
| `onDocumentOpen?()`, `documentOpenLabel?` | 로고 오른쪽 파일 열기 버튼(`SWHeader` `documentOpenPlacement="header"`와 같은 파일명 면 `SWToolButton appearance="document"` + 폴더 아이콘(다른 Header 아이콘과 같은 검정 외곽선 `SWViewerIcon`, 테두리·면 없음) + `documentOpenLabel`, 기본 `Open`). 아이콘 영역 16 × 16(`--hm-sw-viewer-icon-size`, 여백 0), 버튼 padding `0 12px 0 8px`(로고 다음) · 로고가 없으면 `0 12px`(패키지 CSS 고정값, 2026-10-23). 공간이 부족하면 아이콘만(라벨은 Tooltip·접근성 이름) 표시하고 More로 옮기지 않습니다. 실행 후 `onCommand({ id: "open", itemId: "document" })` |
|
||
| `logoMenu?: { title?, items }` | (2026-10-21 이후 뷰어 화면에서 사용하지 않음, 호환용) 로고 클릭 메뉴. 파일 열기는 `onDocumentOpen` 버튼을 씁니다. 항목 `{ id, label, icon?, value?, checked?, disabled?, hidden?, onSelect? }` |
|
||
| `logo?`, `logoSrc?`, `logoAlt?`, `onHomeClick?` | `SWHeader`와 같은 로고 Props. **주지 않으면 아무것도 그리지 않습니다**(“HM” 문자·기본 아이콘 없음. 2026-10-23부터 로고 면도 그리지 않아 폭·여백이 0이고 파일 열기 또는 파일명이 Header 왼쪽 끝에서 12px 안쪽으로 시작합니다. 이때 `onHomeClick`·`logoMenu`는 누를 곳이 없습니다). 로고를 주면 `SWHeader`와 같은 내부 로고 부품과 같은 사선 배경(Figma 홈 에셋)·폭 계산을 씁니다: 높이 17px·원본 비율, 왼쪽 12 + 로고 + 36(다음 부품과 17px 겹침, 예시 로고 79px), 로고는 가로·세로 가운데, 다음 부품 시작 여백 8px. Hover 효과는 Viewer 기존 그대로(없음)입니다(2026-10-23) |
|
||
| `trailing?: ReactNode` | 오른쪽 끝 자유 영역: `SWViewerStatus`, `SWViewerHeaderButton`, 창 최소화·닫기 버전이면 맨 끝에 `SWHeaderWindowControls`(§2.1). 이 세 부품은 `SWHeader` 창 영역처럼 전체 높이(48px)의 칸이 이어진 띠로 그려집니다: 같은 면, 칸 앞 1px 구분선(라이선스 버튼 앞뒤에는 없음), 위·아래 경계는 객체 면 테두리 하나라 모든 칸의 위·아래가 같음, 칸 사이 여백 없음, 객체 면 오른쪽 끝까지(2026-10-21). 메뉴 면은 띠 앞에서 `SWHeader` 객체 영역처럼 오른쪽 라운드(`--hm-sw-header-object-radius` 6px, 같은 면·테두리)로 끝나고 2px 간격 뒤에 띠가 옵니다. 상태 칸은 표시 전용, 아이콘 버튼(정보·라이선스) 칸은 테두리·선 없음 |
|
||
| `moreLabel?`, `toolbarLabel?`, `<header>` 속성, `ref` | |
|
||
|
||
- **반응형(2026-10-21, 실제 가용 폭 기준 3단계):** 고정 Breakpoint를 쓰지 않고 Header 행의 실제 남는 폭(로고·파일명·메뉴·Gap·Padding·More 버튼 포함)을 측정합니다. 파일명 면은 글자 수가 아니라 Header 폭으로 정한 기본 폭으로 계산하므로, 같은 폭에서는 짧은 파일명과 긴 파일명의 단계·파일 열기 버튼·파일명 면 너비가 같고 긴 이름은 `…`입니다(2026-10-21). ① 모두 들어가면 기본 형태(아이콘+텍스트), ② 부족하면 `icon`과 `iconOnlyBelow`가 있는 항목을 모두 아이콘만(라벨은 Tooltip·접근성 이름), ③ 그래도 부족하면 `collapseBelow` 항목을 하나씩 More(⋯, 베이지 260px Dropdown)로 옮깁니다(값이 큰 것부터, 같으면 오른쪽 먼저). ③의 판단(2026-10-26)은 tabs를 최소 폭(`--hm-sw-viewer-tabs-min-width`)으로 세므로, tabs가 `…`로 좁아져서 들어가면 More로 옮기지 않습니다. 파일명 면 기본 폭은 `clamp(80px, min(45cqw - 256px, 100cqw - 780px), 360px)`(1280px 이상은 이전과 같음), Header 560px 미만에서는 56px입니다. 넓어지면 More → 아이콘 → 기본 순서로 복귀하고, 상단과 More에 같은 항목이 동시에 나오지 않습니다. `collapseBelow`·`iconOnlyBelow`의 숫자는 px Breakpoint가 아니며 순서만 정합니다(`iconOnlyBelow`는 지정 여부만, `collapseBelow`는 값이 큰 것부터; Props 형식 동일). 로고 옆 파일 열기 버튼(`onDocumentOpen`)은 ② 단계에서 아이콘만으로 줄고 More로 옮기지 않으며, 샘플은 `documents`. 레이어·설정·속성은 `collapseBelow` 없이 아이콘까지만. 1180px 미만 간격 2px, 560px 미만 상태 글자 숨김(CSS container)은 그대로이고, 560px 미만에서는 tabs 개수 숨김·switch `shortLabel` 캡션·switch 좌우 여백 1px도 적용됩니다(2026-10-26). 가로 스크롤은 만들지 않습니다.
|
||
- More는 그룹/항목 단위 구획으로 자동 구성됩니다: menu·tabs는 자기 구획(`moreLabel`), button·toggle·switch는 그룹 구획(`group.moreLabel ?? group.label`). toggle·switch는 체크 항목, tabs는 라디오 항목이 됩니다.
|
||
- **Dropdown 색(2026-10-21):** Header 안의 Dropdown — 메뉴 버튼(`type: "menu"`), More(⋯), 로고 메뉴 — 은 크기·간격·글자는 `SWViewerMenu` 그대로, 색은 일반 `SWHeader`의 `...` 메뉴와 같습니다(면 #0e3d2f, Border `--hm-sw-header-object-border-color`, 그림자 0 4px 12px rgba(0,0,0,.5), 글자 `--hm-sw-header-text-color`, 보조 글자는 같은 색 80%, Hover `--hm-sw-header-hover`, 구분선 `--hm-sw-header-document-menu-divider-color`). 파일명 목록(`documents`)은 `SWHeader` 파일명 목록 모양을 유지하고, 단독 `SWViewerMenu`(우클릭 메뉴 등)는 기존 색 그대로입니다.
|
||
- **상태 표현:** Hover·Active·Pressed·열림·선택 탭 = 글자·아이콘 #FFC600(면·선택 박스 없음, Hover 전후 크기·위치 동일 — 2026-10-21 Hover 면 #266651 제거), Disabled = 60% 불투명, Focus = 2px #FFC600 외곽선.
|
||
- **글자·아이콘 외곽선(SWHeader와 동일):** 14px 글자 `--hm-sw-header-text-outline`(검정 1.3px), 11px 캡션·개수·상태 `--hm-sw-header-label-outline`(검정 1px), 아이콘 `SWViewerIcon`(SWHeader 아이콘 자산의 1px 검정 바깥 Stroke를 **SVG stroke**로 재현: 아이콘 뒤 복사본의 모든 도형을 검정, 원래 선 두께 + 화면 기준 양쪽 1px(`--hm-sw-viewer-icon-outline-width`)로 그림. 위 아이콘은 그대로라 색·모양·선 두께 유지. CSS `filter` 미사용, glow는 생략). Header 항목·`SWViewerHeaderButton`의 `icon`은 자동 적용, `custom` / `trailing`의 직접 그린 아이콘은 `<SWViewerIcon>`으로 감쌉니다. 색·크기·굵기·간격은 그대로이고 Hover/Active는 채움 색만 바뀝니다. Dropdown·패널 안에는 외곽선이 없습니다(SWHeader More 메뉴와 같음). 프로그램 아이콘은 `currentColor`로 그리면 상태 색과 외곽선이 자동 적용됩니다.
|
||
|
||
### 2.1 두 가지 구성 (2026-10-21)
|
||
|
||
| 버전 | 구성 | 사용 |
|
||
|---|---|---|
|
||
| 기본 | `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton` 등만 | 창 버튼이 없는 화면(웹 뷰어, 프로그램이 창 틀을 따로 가짐) |
|
||
| 최소화·닫기 | 기존 `trailing` 항목 **뒤 맨 끝**에 `<SWHeaderWindowControls showWindowMove={false} onMinimize={…} onClose={…} />` | 설치형 프로그램처럼 Header 오른쪽 끝에 창 최소화·닫기가 필요한 화면 |
|
||
|
||
```tsx
|
||
<SWViewerHeader groups={groups} onCommand={run} trailing={<>
|
||
<SWViewerStatus … />
|
||
<SWHeaderWindowControls showWindowMove={false} onMinimize={minimizeWindow} onClose={closeWindow} />
|
||
</>} />
|
||
```
|
||
|
||
- 새 API는 없습니다(`trailing` 슬롯 사용). 버튼 아이콘·크기·Hover는 `SWHeader`와 같고, 맨 끝에 두면 오른쪽 여백 없이 Header 끝에 붙습니다(패키지 CSS가 처리). 창 이동(`showWindowMove`)은 뷰어 버전에 넣지 않습니다.
|
||
- 실제 창 최소화·닫기는 프로그램 담당입니다(`onMinimize`·`onClose`). Storybook: `SW/Viewer › Viewer Header`의 두 번째 섹션.
|
||
- 이 버튼 외의 `SWHeader` 부품(필드·Dropdown·`SWHeaderActions` 등)은 뷰어 화면에 넣지 않습니다.
|
||
|
||
### 2.2 상태 Label(`SWViewerStatus`) 너비 (2026-10-23)
|
||
|
||
- **원칙:** 상태에 따라 텍스트가 변경되는 Label은 표시 가능한 텍스트의 최대 렌더링 너비를 기준으로 영역을 확보한다. 상태값 변경만으로 Label 너비와 주변 메뉴 위치가 불필요하게 변하지 않도록 한다. 반응형 환경에서 가용 공간이 부족하면 Label 영역을 축소하고 긴 텍스트는 말줄임표(`…`)로 처리한다. 화면 공간이 다시 확보되면 전체 텍스트를 정상 표시한다.
|
||
- **구현(패키지가 처리):** 후보는 상태별 기본 Label(`idle` 준비 · `loading` 불러오는 중 · `ready` 도면 열림 · `error` 불러오기 실패)과 전달한 `label`입니다. 모두 같은 Grid 칸에 두고 현재 것만 보이므로 칸 너비 = 실제 폰트로 그린 가장 긴 후보(기본 폰트 11px 400에서 "불러오기 실패" 약 64px)이며, 글자 수로 계산하지 않습니다. `idle`도 아이콘 자리를 비워 두어 상태가 바뀌어도 상태 칸 전체 너비가 같습니다. 상태별 기본 Label이 추가되면 자동으로 후보가 됩니다.
|
||
- **반응형:** 측정 단계(아이콘화 → More)와 파일명 축소(최소 56px)가 먼저 적용되고, 그래도 부족할 때만 Label이 줄며 `…`가 됩니다. 넓어지면 전체 텍스트로 돌아오고, 560px 미만에서는 기존대로 Label을 숨깁니다. 소비 프로젝트는 Label 폭을 따로 지정하거나 상태마다 다른 길이의 임의 문구를 넣지 않습니다(필요한 문구는 실제 상태 데이터로 `label`에 전달).
|
||
## 3. `SWViewerMenu` / `SWViewerMenuEntry`
|
||
|
||
`entries`: `item`(`icon?`, `shortcut?`, `onSelect`), `checkbox`(`checked`, `onCheckedChange`), `radio`(`checked`, `onSelect`), `submenu`(`entries` — 다단계), `label`, `separator`. 공통 `disabled?`, `hidden?`, `keepOpen?`.
|
||
|
||
- **테마:** `SWViewerMenu`에는 `style` Prop이 없습니다. 토큰은 메뉴를 감싼 바깥 SW 범위(`SWViewerHeader`·`SWViewerPanel`·`<SWTheme>`)에서 상속됩니다. 단독 우클릭 메뉴의 값을 바꾸려면 메뉴를 `<SWTheme style={theme}>` 안에 둡니다([AI_USAGE.md](./AI_USAGE.md) §3.1).
|
||
- Header의 `type: "menu"`가 이 Dropdown을 엽니다. 다른 위치(도면 우클릭 등)는 `SWViewerMenu`를 `open` / `onOpenChange` + `anchorRef`(버튼 아래 4px) 또는 `anchorPoint`(포인터 위치)로 씁니다. `surface="beige"`는 More 면, `initialFocus`는 `first`(키보드 열기) / `menu`(클릭 열기).
|
||
- 키보드: Enter·Space·ArrowDown 열기, ↑↓ Home End 이동, → 서브메뉴 열기, ← 한 단계 닫기, Escape 닫고 Trigger로 Focus 복귀, Tab·바깥 클릭 닫기. 화면 끝에서는 위/왼쪽으로 뒤집혀 화면 안에 남습니다.
|
||
|
||
## 4. `SWViewerPanel` — 팝업 프레임 + 자유 콘텐츠
|
||
|
||
프레임은 **공통 SW 팝업 `SWPopup`과 같은 컴포넌트·DOM**입니다(30px 제목 막대·Figma 제목 아트, #FFC600 700 제목 + 검정 1px 외곽선, SW 닫기 버튼, 1px #A67D03 테두리, 반경 4, #E4DBC9 면). 뷰어는 여기에 드래그·모서리 크기 조절·접기·경계 유지·앞으로 가져오기·모달을 더합니다. 314px가 아닌 폭에서도 제목 아트 곡선은 닫기 버튼에 붙어 있고 왼쪽 평면이 늘어납니다. 내용은 `children`·`footer`가 담당합니다. 일반 SW 팝업은 `SWPopup`(`titleActions?`, `footer?` 추가)을 그대로 씁니다.
|
||
|
||
| Props | 설명 |
|
||
|---|---|
|
||
| `title`, `open`, `onClose`, `closeLabel?` | 필수는 `title`·`open`·`onClose`. 제목 아이콘은 없습니다(SW 팝업 디자인) |
|
||
| `children` | 스크롤 본문. `SWViewerSection`(카드/plain, 12px 제목), `SWViewerField`(라벨 + 컨트롤, `inline` / `stacked` + `below`), `SWViewerSwitch`, 게이지 `SWSlider appearance="property"`(옵션 패널 기준, 아래 옵션 패널 항목), `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 버튼이 아니라 §4.1의 전체 레이어 전구) |
|
||
| `headerActions?` | 제목 막대 추가 버튼: `SWViewerButton variant="ghost"` (26px, 기본 아이콘색 `--hm-sw-viewer-panel-icon-color` #c7bda9 — 접기 버튼과 같음, 글자색만 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 아래) |
|
||
| `strategy?` | `fixed`(기본, 화면) / `absolute`(가장 가까운 positioned 조상 안 — 미리보기·임베드) |
|
||
| `draggable?`, `resizable?`, `collapsible?`, `defaultCollapsed?`, `collapsed?`, `onCollapsedChange?`, `onPositionChange?`, `onSizeChange?` | 제목 막대 드래그, 오른쪽 아래 모서리 크기 조절, 접기(버튼·제목 더블클릭). 이동·크기는 경계 10px 안 |
|
||
| `modal?` | 10% 검정 배경, Focus Trap, 가운데 배치(입력·확인 팝업) |
|
||
| `autoFocus?` | 열 때 패널로 Focus, 닫을 때 원래 위치로 복귀(기본 true) |
|
||
|
||
- Escape는 Focus가 패널 안에 있을 때 그 패널만 닫습니다. 비모달 패널은 바깥 클릭으로 닫히지 않습니다(참고 뷰어와 동일, 도구 창). 마지막으로 누른 패널이 맨 앞으로 옵니다.
|
||
- **콘텐츠 우선순위:** `children`·`footer`는 타입상 `ReactNode`이고 소스 주석도 자유 콘텐츠(“any component”)라고 적습니다. 그래도 기본은 `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·게이지 `SWSlider appearance="property"`·`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로 새 팝업을 만들지 않습니다.
|
||
- **본문 하단 경계(2026-10-22):** 뷰어 패널 본문은 아래 7px 여백 대신 스크롤 영역 밖의 8px 투명 경계를 둡니다(패키지 CSS, `padding-bottom: 0` + `border-bottom: 8px solid transparent`). 스크롤된 목록 행은 테두리 8px 위에서 잘리고, 맨 아래로 스크롤하면 마지막 행이 다 보입니다. 소비 코드에서 하단 여백·`padding`을 덧대지 않습니다. 일반 `SWPopup`은 7px 그대로입니다.
|
||
|
||
### 4.1 레이어 Popup — 개별·전체 표시·숨김 모두 전구 아이콘 버튼 (필수, 2026-10-22)
|
||
|
||
> **규칙:** SW Viewer 레이어 Popup에서 레이어별 표시·숨김은 SW Header 레이어 메뉴와 같은 전구 아이콘(`SWLayerVisibilityIcon`) 버튼을 사용한다(2026-10-21 눈 `Eye`/`EyeOff` 규정 대체). 전체 레이어 켜기·끄기도 Popup 상단의 같은 전구 버튼(`SWLayerVisibilityIcon`) 하나로 제어한다(2026-10-22 `SWViewerSwitch` 규정 대체). 개별·전체 표시·숨김을 Switch·Checkbox로 대체하지 않는다.
|
||
|
||
- 패키지에는 레이어 목록 전용 컴포넌트가 없습니다. 목록은 `SWViewerPanel` > `SWViewerSection surface="plain"` 안에 프로그램이 두고, 위에서부터 검색(선택) · **전체 레이어 전구** · 목록 순서입니다. 검색과 전체 레이어 행은 고정이고 **목록만 세로 스크롤**합니다: 바깥 Section(`app-…`)에 `flex: 1 1 auto; min-height: 0`, 목록에 `flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-gutter: stable`, 나머지 자식은 `flex-shrink: 0`(소비 앱 class, 예제 `example-layout.css`). 패널 본문 자체는 스크롤되지 않으며 팝업 크기(기본 314 × 420)는 그대로입니다. 검색과 전체 레이어 사이에는 구분선이 없고, 전체 레이어 행 아래에만 연한 1px 선 `#d2c8b4`(`--hm-sw-property-table`, `padding-bottom: 8px`)를 둡니다. 행은 색 점 · 레이어 이름 · 개수(`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·상태를 그대로 호출합니다. 아이콘만 그리고 기능을 빼지 않습니다.
|
||
- 전체 레이어 전구(2026-10-22): `<SWViewerSection surface="plain" title="전체 레이어" className="app-…"><button type="button" role="switch" aria-checked={allVisible} aria-label="전체 레이어" className="app-…-bulb" onClick={() => setAll(!allVisible)}><SWLayerVisibilityIcon on={allVisible} /></button></SWViewerSection>`. 버튼은 행의 전구 버튼과 같은 class·크기(24 × 24)이고 전구를 따로 그리지 않습니다. `role="switch"`·`aria-checked`·이름 `전체 레이어`로 켜짐 상태를 알리며 Enter·Space는 기본 button 동작입니다. 행 전구와 세로로 맞추려면 이 행에 목록과 같은 `padding-right: 6px` + `overflow-y: hidden; scrollbar-gutter: stable`을 주고 포커스 링은 `outline-offset: -2px`로 버튼 안에 둡니다. 제목은 기존 Section 제목(`.hm-sw-viewer-section__title`, 12px 700)을 그대로 쓰고 별도 제목 CSS·Prop을 만들지 않습니다. 제목과 전구를 한 줄에 두는 것은 소비 앱 자기 class(예: `section.app-viewer-all-layers { flex-direction: row; align-items: center; justify-content: space-between; min-height: 20px }`, 예제 `example-layout.css`)로 하고 `hm-sw-viewer-*` class는 덮지 않습니다. 켜짐(`aria-checked`, 색 있는 전구)은 모든 레이어가 표시 중일 때만 `true`이고, 일부만 표시 중이면 `false`(회색 전구)로 보이며 누르면 전체 표시가 됩니다. 켜진 전구를 누르면 전체 숨김. 별도 상태를 두지 않고 레이어 가시성 상태에서 계산하므로 개별 전환 시 자동으로 맞춰집니다. 이전의 `전체 켜기`·`전체 끄기` 버튼 두 개는 쓰지 않습니다.
|
||
- 그 밖의 `SWViewerSwitch`는 옵션 패널의 ON/OFF 설정(그리드·2.5D 등)에 씁니다.
|
||
- **옵션(설정) 패널(2026-10-22):** 하단 `취소`·`적용` 버튼이 없습니다(`footer` 미사용 → Footer 영역·구분선도 없음). 토글·Input·Slider는 바뀌는 즉시 프로그램 상태에 반영하고, 기본값 복원은 제목 막대의 초기화(`headerActions`) 버튼입니다. 확인이 필요한 입력 팝업(축척 지정 등)의 `footer`는 그대로 씁니다.
|
||
- **옵션 패널 게이지(필수, 2026-10-22 확정):** 선가중치·표시·2.5D 등 옵션 패널의 게이지는 `SWSlider appearance="property"`(스타일 B: 베이지 트랙 · 빨강 채움 · Thumb 14px, 높이 16px) 하나만 씁니다. Card(`SWViewerSection`)마다 필요한 값 하나에 게이지 하나를 `SWViewerField layout="stacked"`의 `below`에 두고, 숫자 입력(`SWViewerInput`)과 같은 값·`min`·`max`·`step`·`disabled`를 공유합니다. 녹색 panel 모양(스타일 A)인 `SWViewerSlider`·`SWSlider` 기본값(`appearance="panel"`)은 옵션 패널에 쓰지 않습니다(컴포넌트는 호환용으로 그대로 공개). Storybook `SW/Viewer › Settings Popup`, 예제 H.
|
||
|
||
### 4.2 패널 콘텐츠 글자 기준 (2026-10-22)
|
||
|
||
- 일반 콘텐츠(레이어 이름, 속성 행 Label)·`SWViewerField` Label: 14px 500. 읽기 전용 속성값은 14px, Weight는 패널 기본(400), 색 `--hm-sw-viewer-panel-text`(검정 계열). 속성 행 Label 색은 `--hm-sw-viewer-panel-label`.
|
||
- 설명·안내(`SWViewerHint`, 빈 상태 문구)·Input 값·`SWSelect` 선택값·Placeholder·Badge: 12px(기존 Weight 유지). Section 제목 12px 700, 패널 제목 14px 700은 그대로입니다.
|
||
- 글꼴은 패널이 상속하는 `--hm-sw-header-font-family`(Noto Sans KR)만 씁니다. 목록·값에 `ui-monospace` 등 다른 `font-family`를 따로 지정하지 않습니다.
|
||
- 속성 패널의 그룹(예: 일반 정보 / 기하 속성)을 더 떨어뜨릴 때는 두 번째 Section에 자기 class로 `margin-top: var(--hm-sw-viewer-panel-spacing)`(12px, 본문 간격 8px과 합쳐 20px)을 줍니다. 패널 본문 공통 간격은 바꾸지 않습니다.
|
||
|
||
## 5. 올바른 사용
|
||
|
||
```tsx
|
||
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
|
||
import "@hm-design/sw/styles.css"
|
||
|
||
const groups: SWViewerHeaderGroup[] = [
|
||
{ id: "view", label: "화면 보기", surface: "field", items: [{ type: "button", id: "fit", label: "전체 보기", icon: <FitIcon />, onSelect: fit }] },
|
||
{ id: "panels", label: "패널", placement: "end", items: [{ type: "toggle", id: "options", label: "설정", icon: <OptionsIcon />, pressed: open, onPressedChange: setOpen, iconOnlyBelow: 900 }] },
|
||
]
|
||
<SWViewerHeader documentTitle={fileName} groups={groups} onCommand={runCommand}
|
||
onDocumentOpen={openFile} documentOpenLabel="파일 열기"
|
||
documents={samples} activeDocumentId={sampleId} onDocumentSelect={loadSample} />
|
||
<SWViewerPanel title="옵션" open={open} onClose={() => setOpen(false)}>
|
||
<SWViewerSection title="표시">
|
||
<SWViewerField label="그리드">{id => <SWViewerSwitch id={id} checked={grid} onCheckedChange={setGrid} />}</SWViewerField>
|
||
</SWViewerSection>
|
||
</SWViewerPanel>
|
||
```
|
||
|
||
## 6. 잘못된 사용
|
||
|
||
| 하지 말 것 | 이유 / 대신 |
|
||
|---|---|
|
||
| `<SWViewerHeader logo="HM" />`처럼 로고가 없다고 임의 문자·아이콘 넣기 | 로고는 프로그램이 정합니다. 없으면 생략 |
|
||
| 뷰어 화면에 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`, `SWHeader homeMenu`) 넣기 또는 비슷한 2단 메뉴 직접 만들기 | 홈 메뉴는 SW 일반 모드 전용입니다. 뷰어의 파일 열기는 로고 옆 `onDocumentOpen` 버튼, 샘플 목록은 `documents`(직접 만든 Dropdown 금지) |
|
||
| 뷰어 Header를 `div` + `button`으로 다시 만들기, `hm-sw-viewer-*` class를 소비 코드에 붙이기 | 데이터(`groups`)와 `custom` / `trailing`으로 구성 |
|
||
| 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: 전구(`SWLayerVisibilityIcon`) 아이콘 버튼(전체 레이어 포함). Switch는 옵션 설정에만 |
|
||
| 존재하지 않는 Props 추측(`items`, `content`, `renderBody` …) | 위 표와 `dist/index.d.ts`가 전부 |
|
||
| 일반 모드 화면에서 `SWViewerHeader` 사용, 뷰어에서 `SWHeader` 재스타일 | 모드별 Header를 바꿔 끼웁니다(창 최소화·닫기 `SWHeaderWindowControls`를 `trailing` 끝에 두는 §2.1 구성은 허용) |
|
||
| 기능 없는 메뉴를 가짜 동작으로 연결 | 항목 제외 또는 `disabled` + 미연결 보고 |
|
||
|
||
## 7. 디자인 변경 시 주의
|
||
|
||
- 크기(52px Header, 34px 버튼, 40px 토글·스위치 칸, 41~42px field 면)와 색은 참고 뷰어 실측값이고(Model / Layout 탭은 2026-10-23 Figma 7652:40344 모양, 높이는 field 그룹과 같은 42px·버튼 37.5px), 로고 면은 2026-10-23 `SWHeader` 로고 기준이며 폭은 고정값이 아니라 로고 내용 폭을 따릅니다(Header 테두리 1px 안쪽에서 11 + 로고 + 36, 로고 위치는 SWHeader와 같은 x 12. 32 × 17 예시 로고에서 79px은 측정 예시, 공통 원칙은 [DESIGN_RULES.md](./DESIGN_RULES.md) §1). 파일명·객체 수정·속성 면 배경은 SWHeader와 같은 사용자 확정 토큰(`--hm-sw-header-document-*`, `object-*`, `property-*`)을 공유하므로 바꾸면 두 Header가 함께 바뀝니다.
|
||
- **높이:** 패키지는 뷰어(도면) 영역 높이를 정하지 않습니다. Header는 52px 한 줄, 팝업만 최대 90vh입니다. 화면 높이는 앱의 상위 레이아웃이 정하고(예: `html, body, #root { height: 100% }` → 세로 flex, Header 다음 도면 영역 `flex: 1; min-height: 0`), 도면 영역 크기 변화에 맞춰 렌더러 크기를 바꾸는 것은 프로그램 담당입니다(ResizeObserver 권장). Storybook 예시의 고정 높이는 미리보기 프레임 값일 뿐 컴포넌트 값이 아닙니다.
|
||
- Light/Dark 분기는 없습니다. 참고 뷰어와의 알려진 차이: 도면 공간(tabs) field 면 높이 42px(참고 41px — 참고 Tabs 루트가 39px인 구현 부산물), 크기 조절은 네이티브 `resize` 대신 같은 위치의 모서리 Grip(모든 브라우저 동작), 패널 Escape 닫기 추가, 로고 기본 표시 없음(사용자 결정).
|