feat(sw): refine headers, status label, popups and AI guide rules
Deploy Storybook / deploy (push) Successful in 1m10s

- SW Header: the five field previews hide and return together in one
  measured step; from 760 to 1920px the groups from 레이어 to 치수스타일
  share the room left before 속성보기, above 1920px they keep the 1920
  widths (controls bars and < 760px unchanged)
- SW Viewer Header Model / Layout: long names ellipsize in equal buttons
  (minmax(0, 1fr), name counted at --hm-sw-viewer-tab-name-basis) so they
  never change the box or the measured steps; green outer outline and its
  tokens removed; 378px / 42px, text-shadow and state unchanged
- SWViewerStatus: the label keeps the width of its widest real state text
  (idle keeps its icon slot), so a state change never moves the header;
  with too little room it ellipsizes after the file name gives way
- Popups: body inset shadow removed, title SVG drop shadow removed, viewer
  panel title icons use --hm-sw-viewer-panel-icon-color (#c7bda9)
- Storybook: SW/Viewer › Model / Layout story removed
- AI guide (docs/ai + package copies): design-only changes for every
  component, content limits for Submodule consumers (Header excluded),
  Submodule usage rules; VIEWER / DESIGN_RULES synced
- viewer.browser.mjs: explicit 1920 / 1440 labels and approved 1280
  icon-only expectations
This commit is contained in:
shkim4 committed 2026-10-23 18:18:17 +09:00
1 parent 325ec25e37
commit db03e155dd
19 files changed
+190 -119

No files matched your search

+12
View File
@@ -109,6 +109,18 @@
- 토큰·확인값이 있는 속성에 새 값을 만들지 않습니다. 색·크기·간격·radius·그림자는 각 패키지 `DESIGN_RULES.md`(SW 뷰어는 `VIEWER.md`도)의 토큰·확인값을 씁니다. 값이 없을 때만 위 ③으로 구성하고 "AI 구성 값"으로 보고합니다(추측한 값을 확정값처럼 쓰지 않음).
- 공통: Font Weight는 400 / 500 / 700만 씁니다(600 없음). Light/Dark 분기를 만들지 않습니다. 새 컴포넌트 CSS에 `html`·`body`·`*` 전역 규칙을 넣지 않습니다. 새 class는 소비 프로젝트 접두사(예: `app-`)를 씁니다.
- 요청에 없는 메뉴·통계·설명·다운로드·외부 API·AI 입력 UI를 추가하지 않습니다. 필요해 보이면 제안으로만 보고합니다.
- **디자인 적용 시 변경 범위 제한(2026-10-23, 모든 컴포넌트 — Header 포함).** Figma·디자인 시안 적용은 디자인 변경만 합니다. 일반 SW Header, SW Viewer Header, Popup·Panel, 그 밖의 HMDesign 컴포넌트와 HMDesign을 Git Submodule로 쓰는 소비 프로젝트에 모두 적용합니다.
- 바꾸지 않는 것: 실제 상태값(`selected`·`active`·`disabled`·`checked`·`expanded`·`hidden` 등), 메뉴의 기본 선택 상태(예: Model / Layout), Popup 열림·닫힘 상태, 스위치·체크박스 기본값, 입력값·숫자·개수 등 표시 데이터, Props와 그 기본값, 이벤트·클릭 동작, 조건부 렌더링, 기존 반응형 기능, 사용자 설정·상태 관리 로직, 요청받지 않은 다른 화면·컴포넌트.
- 바꿀 수 있는 것: 배경색·글자색·Border·Gradient·Shadow·Radius·Padding·Gap, 너비·높이·정렬·레이아웃, 기존 상태에 대응하는 시각 스타일. 선택 상태의 모양을 바꿔도 어떤 항목이 선택되어 있는지는 바꾸지 않습니다.
- Figma에 특정 상태가 그려져 있어도 컴포넌트의 기본 상태를 바꾸지 않습니다. 디자인 비교는 검증용 Story나 테스트 데이터에서 하고, Storybook 예시값을 소비 프로젝트의 기본값으로 복사하지 않습니다. 상태·기능 변경이 꼭 필요해 보이면 적용하지 말고 별도로 보고합니다.
- 작업 후 Git Diff로 요청 범위 밖 변경이 없는지 확인합니다.
- **Git Submodule 소비 프로젝트의 콘텐츠 생성 제한(2026-10-23, Header 제외).** HMDesign을 Git Submodule로 사용하는 소비 프로젝트의 UI 컴포넌트·화면에 적용합니다. **제외:** 일반 SW Header(`SWHeader`), SW Viewer Header(`SWViewerHeader`)와 그 밖의 모든 Header 관련 컴포넌트·영역 — 텍스트는 기존 Header 전용 AI 지침을 따르되, 위 "디자인 적용 시 변경 범위 제한"(상태·기능 변경 금지)은 Header에도 그대로 적용합니다.
1. 사용자가 요청하지 않은 설명·안내 문구·제목·라벨·툴팁을 임의로 추가하지 않습니다.
2. Storybook의 샘플 텍스트나 데이터를 실제 소비 프로젝트에 임의로 복사하지 않습니다.
3. Figma 또는 기존 컴포넌트에 없는 텍스트를 추측해서 생성하지 않습니다.
4. 기존 텍스트와 데이터를 임의로 변경하지 않습니다.
5. 필요한 콘텐츠가 불명확하면 임의로 확정하지 않고 결과 보고에 확인 대상으로 적습니다.
- **Git Submodule 사용 규칙(2026-10-23).** 소비 프로젝트에서 HMDesign을 사용할 때 요청 범위 밖은 바꾸지 않습니다. Submodule Commit 포인터는 사용자 승인 없이 바꾸지 않습니다. 소비 프로젝트의 기존 데이터와 기능을 우선 유지합니다.
### 없는 UI를 같은 스타일로 만들 때 확인할 항목
+1
View File
@@ -416,6 +416,7 @@ import type { CSSProperties } from "react"
- 뷰어의 파일 열기·샘플·Model/Layout 탭·2.5D·어두운 배경·로드 상태는 Figma SW Header에 없는 뷰어 전용 도구입니다. 필요하면 `tools` 슬롯에 호스트가 렌더링합니다(스타일은 호스트 책임).
- 헤더 높이는 49px입니다. 고정 배치(`position: fixed`)는 `className`·`style`로 호스트에서 지정합니다.
- 반응형은 모두 헤더 자신의 폭(container query)을 기준으로 하므로 사이드 패널 때문에 헤더가 좁아져도 동작합니다. 단계(1800 / 1180 / 1080 / 900 / 640 / 520)는 [디자인 명세 §12](./sw-header-design-spec.md#12-반응형)를 따르고, 그 위에서 실제 남은 공간을 측정해 부족한 메뉴만 더보기로 옮깁니다([§12.1](./sw-header-design-spec.md#121-실측-more--실제-사용-가능한-공간-기준-2026-10-19)).
- 가용 공간 분배(2026-10-23): 헤더 폭 760~1920px에서는 레이어~치수스타일 5개 그룹이 속성보기 앞의 남는 공간을 같은 양씩 나눠 속성보기 바로 옆까지 채우고, 1920px을 넘으면 1920px 폭(234 / 192 / 284 / 164 / 164px)에 고정되어 남는 공간은 속성보기 앞 여백이 됩니다. 숨김·More 단계와 창 영역 위치는 그대로이며 `controls` 구성 Header에는 적용하지 않습니다([AI 디자인 규칙](../../packages/sw/ai/DESIGN_RULES.md)).
- 뷰어는 현재 `@hm-design/ui`의 SWHeader 패턴을 사용합니다. 이 패키지로 바꾸는 작업은 이번 범위가 아닙니다.
## 10.1 상호작용 상태
+1 -1
View File
@@ -51,7 +51,7 @@ SWViewerPanel (= SWPopup, .hm-sw-property.hm-sw-viewer-panel) ← 테두리
| 영역 | 목적 | 선택자 | 실제 값 | 조건·예외 | 구분 | 예시 |
|---|---|---|---|---|---|---|
| 제목줄 | 제목·닫기·동작 버튼 | `.hm-sw-property__titlebar` | 높이 30px, `background: var(--hm-sw-header-document-background)`(녹색 세로 그라데이션), Figma 제목 아트(`property-title-single.svg`, 314px 초과 폭은 왼쪽 늘림 복제 `::before`) | 모든 패널. 드래그 가능하면 `cursor: grab` | 공통 | 전체 |
| 본문 | 스크롤 콘텐츠 영역 | `.hm-sw-property__body` + `.hm-sw-viewer-panel .hm-sw-property__body` | `padding: 7px 7px 0`, `border-bottom: 8px solid transparent`(스크롤 영역 밖 하단 여백), `gap: 8px`, `box-shadow: inset 0 2px 2px rgba(255,255,255,.6)`, 배경은 프레임 surface | 접힘이면 `display: none` | 공통(SWPopup 7px + 뷰어 하단 8px 덮어쓰기) | 전체 |
| 본문 | 스크롤 콘텐츠 영역 | `.hm-sw-property__body` + `.hm-sw-viewer-panel .hm-sw-property__body` | `padding: 7px 7px 0`, `border-bottom: 8px solid transparent`(스크롤 영역 밖 하단 여백), `gap: 8px`, 그림자 없음(2026-10-23 사용자 요청으로 `box-shadow: inset 0 2px 2px rgba(255,255,255,.6)` 제거), 배경은 프레임 surface | 접힘이면 `display: none` | 공통(SWPopup 7px + 뷰어 하단 8px 덮어쓰기) | 전체 |
| Field | 라벨 + 컨트롤 행 | `.hm-sw-viewer-field`, `__row` | 선·배경 없음, field `gap: 6px`, row `gap: 8px`, `min-height: 20px` | disabled면 `opacity: .5` | 공통 | `옵션` |
| 값 입력·단위 칸 | 숫자 입력 | `.hm-sw-viewer-input:not([type="search"])`, `.hm-sw-viewer-field__control:has(> unit)` | 기본 **투명 테두리**(1px transparent), radius 2px, 높이 28px. Hover/편집 시에만 `--hm-sw-property-field-hover-border-color` 테두리, 편집 중 배경 `--hm-sw-property-field-editing-background` | 상태가 바뀔 때만 박스로 보임 | 공통 | `옵션`의 `표시 배율`(px/mm) |
| Badge | 개수·유형 표시 | `.hm-sw-viewer-badge` | 테두리 없음, `background: var(--hm-sw-viewer-panel-badge-background)`(#d2c8b4), radius 4px, `padding: 2px 8px` | — | 공통 | 레이어 개수, `LINE · h=2F1A`(`속성` 맨 위, Card 밖, 자기 폭) |
+12
View File
@@ -109,6 +109,18 @@
- 토큰·확인값이 있는 속성에 새 값을 만들지 않습니다. 색·크기·간격·radius·그림자는 각 패키지 `DESIGN_RULES.md`(SW 뷰어는 `VIEWER.md`도)의 토큰·확인값을 씁니다. 값이 없을 때만 위 ③으로 구성하고 "AI 구성 값"으로 보고합니다(추측한 값을 확정값처럼 쓰지 않음).
- 공통: Font Weight는 400 / 500 / 700만 씁니다(600 없음). Light/Dark 분기를 만들지 않습니다. 새 컴포넌트 CSS에 `html`·`body`·`*` 전역 규칙을 넣지 않습니다. 새 class는 소비 프로젝트 접두사(예: `app-`)를 씁니다.
- 요청에 없는 메뉴·통계·설명·다운로드·외부 API·AI 입력 UI를 추가하지 않습니다. 필요해 보이면 제안으로만 보고합니다.
- **디자인 적용 시 변경 범위 제한(2026-10-23, 모든 컴포넌트 — Header 포함).** Figma·디자인 시안 적용은 디자인 변경만 합니다. 일반 SW Header, SW Viewer Header, Popup·Panel, 그 밖의 HMDesign 컴포넌트와 HMDesign을 Git Submodule로 쓰는 소비 프로젝트에 모두 적용합니다.
- 바꾸지 않는 것: 실제 상태값(`selected`·`active`·`disabled`·`checked`·`expanded`·`hidden` 등), 메뉴의 기본 선택 상태(예: Model / Layout), Popup 열림·닫힘 상태, 스위치·체크박스 기본값, 입력값·숫자·개수 등 표시 데이터, Props와 그 기본값, 이벤트·클릭 동작, 조건부 렌더링, 기존 반응형 기능, 사용자 설정·상태 관리 로직, 요청받지 않은 다른 화면·컴포넌트.
- 바꿀 수 있는 것: 배경색·글자색·Border·Gradient·Shadow·Radius·Padding·Gap, 너비·높이·정렬·레이아웃, 기존 상태에 대응하는 시각 스타일. 선택 상태의 모양을 바꿔도 어떤 항목이 선택되어 있는지는 바꾸지 않습니다.
- Figma에 특정 상태가 그려져 있어도 컴포넌트의 기본 상태를 바꾸지 않습니다. 디자인 비교는 검증용 Story나 테스트 데이터에서 하고, Storybook 예시값을 소비 프로젝트의 기본값으로 복사하지 않습니다. 상태·기능 변경이 꼭 필요해 보이면 적용하지 말고 별도로 보고합니다.
- 작업 후 Git Diff로 요청 범위 밖 변경이 없는지 확인합니다.
- **Git Submodule 소비 프로젝트의 콘텐츠 생성 제한(2026-10-23, Header 제외).** HMDesign을 Git Submodule로 사용하는 소비 프로젝트의 UI 컴포넌트·화면에 적용합니다. **제외:** 일반 SW Header(`SWHeader`), SW Viewer Header(`SWViewerHeader`)와 그 밖의 모든 Header 관련 컴포넌트·영역 — 텍스트는 기존 Header 전용 AI 지침을 따르되, 위 "디자인 적용 시 변경 범위 제한"(상태·기능 변경 금지)은 Header에도 그대로 적용합니다.
1. 사용자가 요청하지 않은 설명·안내 문구·제목·라벨·툴팁을 임의로 추가하지 않습니다.
2. Storybook의 샘플 텍스트나 데이터를 실제 소비 프로젝트에 임의로 복사하지 않습니다.
3. Figma 또는 기존 컴포넌트에 없는 텍스트를 추측해서 생성하지 않습니다.
4. 기존 텍스트와 데이터를 임의로 변경하지 않습니다.
5. 필요한 콘텐츠가 불명확하면 임의로 확정하지 않고 결과 보고에 확인 대상으로 적습니다.
- **Git Submodule 사용 규칙(2026-10-23).** 소비 프로젝트에서 HMDesign을 사용할 때 요청 범위 밖은 바꾸지 않습니다. Submodule Commit 포인터는 사용자 승인 없이 바꾸지 않습니다. 소비 프로젝트의 기존 데이터와 기능을 우선 유지합니다.
### 없는 UI를 같은 스타일로 만들 때 확인할 항목
+12
View File
@@ -109,6 +109,18 @@
- 토큰·확인값이 있는 속성에 새 값을 만들지 않습니다. 색·크기·간격·radius·그림자는 각 패키지 `DESIGN_RULES.md`(SW 뷰어는 `VIEWER.md`도)의 토큰·확인값을 씁니다. 값이 없을 때만 위 ③으로 구성하고 "AI 구성 값"으로 보고합니다(추측한 값을 확정값처럼 쓰지 않음).
- 공통: Font Weight는 400 / 500 / 700만 씁니다(600 없음). Light/Dark 분기를 만들지 않습니다. 새 컴포넌트 CSS에 `html`·`body`·`*` 전역 규칙을 넣지 않습니다. 새 class는 소비 프로젝트 접두사(예: `app-`)를 씁니다.
- 요청에 없는 메뉴·통계·설명·다운로드·외부 API·AI 입력 UI를 추가하지 않습니다. 필요해 보이면 제안으로만 보고합니다.
- **디자인 적용 시 변경 범위 제한(2026-10-23, 모든 컴포넌트 — Header 포함).** Figma·디자인 시안 적용은 디자인 변경만 합니다. 일반 SW Header, SW Viewer Header, Popup·Panel, 그 밖의 HMDesign 컴포넌트와 HMDesign을 Git Submodule로 쓰는 소비 프로젝트에 모두 적용합니다.
- 바꾸지 않는 것: 실제 상태값(`selected`·`active`·`disabled`·`checked`·`expanded`·`hidden` 등), 메뉴의 기본 선택 상태(예: Model / Layout), Popup 열림·닫힘 상태, 스위치·체크박스 기본값, 입력값·숫자·개수 등 표시 데이터, Props와 그 기본값, 이벤트·클릭 동작, 조건부 렌더링, 기존 반응형 기능, 사용자 설정·상태 관리 로직, 요청받지 않은 다른 화면·컴포넌트.
- 바꿀 수 있는 것: 배경색·글자색·Border·Gradient·Shadow·Radius·Padding·Gap, 너비·높이·정렬·레이아웃, 기존 상태에 대응하는 시각 스타일. 선택 상태의 모양을 바꿔도 어떤 항목이 선택되어 있는지는 바꾸지 않습니다.
- Figma에 특정 상태가 그려져 있어도 컴포넌트의 기본 상태를 바꾸지 않습니다. 디자인 비교는 검증용 Story나 테스트 데이터에서 하고, Storybook 예시값을 소비 프로젝트의 기본값으로 복사하지 않습니다. 상태·기능 변경이 꼭 필요해 보이면 적용하지 말고 별도로 보고합니다.
- 작업 후 Git Diff로 요청 범위 밖 변경이 없는지 확인합니다.
- **Git Submodule 소비 프로젝트의 콘텐츠 생성 제한(2026-10-23, Header 제외).** HMDesign을 Git Submodule로 사용하는 소비 프로젝트의 UI 컴포넌트·화면에 적용합니다. **제외:** 일반 SW Header(`SWHeader`), SW Viewer Header(`SWViewerHeader`)와 그 밖의 모든 Header 관련 컴포넌트·영역 — 텍스트는 기존 Header 전용 AI 지침을 따르되, 위 "디자인 적용 시 변경 범위 제한"(상태·기능 변경 금지)은 Header에도 그대로 적용합니다.
1. 사용자가 요청하지 않은 설명·안내 문구·제목·라벨·툴팁을 임의로 추가하지 않습니다.
2. Storybook의 샘플 텍스트나 데이터를 실제 소비 프로젝트에 임의로 복사하지 않습니다.
3. Figma 또는 기존 컴포넌트에 없는 텍스트를 추측해서 생성하지 않습니다.
4. 기존 텍스트와 데이터를 임의로 변경하지 않습니다.
5. 필요한 콘텐츠가 불명확하면 임의로 확정하지 않고 결과 보고에 확인 대상으로 적습니다.
- **Git Submodule 사용 규칙(2026-10-23).** 소비 프로젝트에서 HMDesign을 사용할 때 요청 범위 밖은 바꾸지 않습니다. Submodule Commit 포인터는 사용자 승인 없이 바꾸지 않습니다. 소비 프로젝트의 기존 데이터와 기능을 우선 유지합니다.
### 없는 UI를 같은 스타일로 만들 때 확인할 항목
+12
View File
@@ -109,6 +109,18 @@
- 토큰·확인값이 있는 속성에 새 값을 만들지 않습니다. 색·크기·간격·radius·그림자는 각 패키지 `DESIGN_RULES.md`(SW 뷰어는 `VIEWER.md`도)의 토큰·확인값을 씁니다. 값이 없을 때만 위 ③으로 구성하고 "AI 구성 값"으로 보고합니다(추측한 값을 확정값처럼 쓰지 않음).
- 공통: Font Weight는 400 / 500 / 700만 씁니다(600 없음). Light/Dark 분기를 만들지 않습니다. 새 컴포넌트 CSS에 `html`·`body`·`*` 전역 규칙을 넣지 않습니다. 새 class는 소비 프로젝트 접두사(예: `app-`)를 씁니다.
- 요청에 없는 메뉴·통계·설명·다운로드·외부 API·AI 입력 UI를 추가하지 않습니다. 필요해 보이면 제안으로만 보고합니다.
- **디자인 적용 시 변경 범위 제한(2026-10-23, 모든 컴포넌트 — Header 포함).** Figma·디자인 시안 적용은 디자인 변경만 합니다. 일반 SW Header, SW Viewer Header, Popup·Panel, 그 밖의 HMDesign 컴포넌트와 HMDesign을 Git Submodule로 쓰는 소비 프로젝트에 모두 적용합니다.
- 바꾸지 않는 것: 실제 상태값(`selected`·`active`·`disabled`·`checked`·`expanded`·`hidden` 등), 메뉴의 기본 선택 상태(예: Model / Layout), Popup 열림·닫힘 상태, 스위치·체크박스 기본값, 입력값·숫자·개수 등 표시 데이터, Props와 그 기본값, 이벤트·클릭 동작, 조건부 렌더링, 기존 반응형 기능, 사용자 설정·상태 관리 로직, 요청받지 않은 다른 화면·컴포넌트.
- 바꿀 수 있는 것: 배경색·글자색·Border·Gradient·Shadow·Radius·Padding·Gap, 너비·높이·정렬·레이아웃, 기존 상태에 대응하는 시각 스타일. 선택 상태의 모양을 바꿔도 어떤 항목이 선택되어 있는지는 바꾸지 않습니다.
- Figma에 특정 상태가 그려져 있어도 컴포넌트의 기본 상태를 바꾸지 않습니다. 디자인 비교는 검증용 Story나 테스트 데이터에서 하고, Storybook 예시값을 소비 프로젝트의 기본값으로 복사하지 않습니다. 상태·기능 변경이 꼭 필요해 보이면 적용하지 말고 별도로 보고합니다.
- 작업 후 Git Diff로 요청 범위 밖 변경이 없는지 확인합니다.
- **Git Submodule 소비 프로젝트의 콘텐츠 생성 제한(2026-10-23, Header 제외).** HMDesign을 Git Submodule로 사용하는 소비 프로젝트의 UI 컴포넌트·화면에 적용합니다. **제외:** 일반 SW Header(`SWHeader`), SW Viewer Header(`SWViewerHeader`)와 그 밖의 모든 Header 관련 컴포넌트·영역 — 텍스트는 기존 Header 전용 AI 지침을 따르되, 위 "디자인 적용 시 변경 범위 제한"(상태·기능 변경 금지)은 Header에도 그대로 적용합니다.
1. 사용자가 요청하지 않은 설명·안내 문구·제목·라벨·툴팁을 임의로 추가하지 않습니다.
2. Storybook의 샘플 텍스트나 데이터를 실제 소비 프로젝트에 임의로 복사하지 않습니다.
3. Figma 또는 기존 컴포넌트에 없는 텍스트를 추측해서 생성하지 않습니다.
4. 기존 텍스트와 데이터를 임의로 변경하지 않습니다.
5. 필요한 콘텐츠가 불명확하면 임의로 확정하지 않고 결과 보고에 확인 대상으로 적습니다.
- **Git Submodule 사용 규칙(2026-10-23).** 소비 프로젝트에서 HMDesign을 사용할 때 요청 범위 밖은 바꾸지 않습니다. Submodule Commit 포인터는 사용자 승인 없이 바꾸지 않습니다. 소비 프로젝트의 기존 데이터와 기능을 우선 유지합니다.
### 없는 UI를 같은 스타일로 만들 때 확인할 항목
+1 -1
View File
@@ -34,7 +34,7 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체
| 기타 | `activeMenu`, `disabled`(창 버튼 제외), `drawingModeLabel`, `tools?: ReactNode`(객체 영역 메뉴 뒤), `<header>` HTML 속성·`ref` |
- 데이터 배열(`layers`, `colors`, `lineTypes`/`lineWeights`, `textStyles`, `dimensionStyles`, `documents`)을 주지 않으면 해당 필드는 Dropdown 없는 버튼입니다.
- 반응형(2026-10-21): Header 폭 760px 이상에서는 More로 옮기지 않습니다. 레이어·색상·선스타일·문자스타일·치수스타일 5개 메뉴와 타이틀은 항상 보이고, 좁아지면 그리기모드 텍스트·선/치수 그림 숨김 → 파일명 축소(최소 96px, `…`) → 메뉴 값 `…` 순으로 줄입니다. 1080px 미만(1024 등)은 아이콘·색상 칩·폰트명을 숨긴 텍스트만, 900px 이하는 선스타일 필드에서 가중치·스케일을 숨깁니다. 모든 값과 타이틀이 잘리지 않는 폭은 약 1,200px 이상입니다. 760px 미만은 기존 More 동작을 유지합니다.
- 반응형(2026-10-21): Header 폭 760px 이상에서는 More로 옮기지 않습니다. 레이어·색상·선스타일·문자스타일·치수스타일 5개 메뉴와 타이틀은 항상 보이고, 좁아지면 그리기모드 텍스트 숨김 → 미리보기 5종(레이어 상태 아이콘·색상 칩·선 그림·글꼴명·치수 그림)을 한 번에 함께 숨김(2026-10-23, 모두 들어갈 공간이 생기면 함께 복원) → 파일명 축소(최소 96px, `…`) → 메뉴 값 `…` 순으로 줄입니다. 미리보기가 숨으면 이름·값 텍스트만 남고, 900px 이하는 선스타일 필드에서 가중치·스케일을 숨깁니다. 모든 값과 타이틀이 잘리지 않는 폭은 약 1,200px 이상입니다. 760px 미만은 기존 More 동작을 유지합니다.
- 반응형(More·대표 필드 전환·창 영역 축소)은 **`SWHeader` 안에서만** 동작합니다(CSS container `hm-sw-header`). 조합형 구성이 반응형이어야 하면 `controls` + `documentOpenPlacement` + `leading` + `tools`로 `SWHeader` 안에서 구성합니다(예제 F).
- (760px 미만) 로고 옆 Open은 실제 공간을 측정해 넘칠 때만 More로 이동하고, 그 시점보다 Open 폭만큼 넓어지면 돌아옵니다. Open이 보이는 동안 파일명이 먼저 줄고 레이어 필드는 Figma 폭을 유지합니다.
- (760px 미만) 메뉴도 고정 폭만이 아니라 실제 남은 공간을 기준으로 More에 옮기고 공간이 생기면 원래 자리·순서로 돌려놓습니다(Breakpoint는 그대로, 추가 Prop 없음). 메뉴를 열고 닫아도 Header 요소의 위치·폭은 바뀌지 않습니다.
+7 -5
View File
@@ -82,7 +82,9 @@
## 7. 반응형 (`SWHeader` 전용)
**760px 이상(2026-10-21 사용자 결정):** More 없이 5개 메뉴(레이어·색상·선스타일·문자스타일·치수스타일)를 항상 Header에 둡니다. 로고 옆 Open 유무와 관계없이 넘치지 않으며, 좁아질수록 **< 1800** 그리기모드 글자·선/치수 그림 숨김·간격 축소 → 파일명 축소(최소 96px, `…`) → 정보값 `…` 순으로 공간을 줄입니다. 메뉴 타이틀은 모든 폭에서 표시하며 공간이 부족하면 `…`로 줄어듭니다. **< 1080** 레이어 상태 아이콘·색상 칩·글꼴 이름 숨김, **≤ 900** 선스타일은 선스타일 값만. Dropdown 열기·닫기는 그대로입니다. 아래 Breakpoint·실측 More는 **760px 미만**에만 적용됩니다.
**760px 이상(2026-10-21 사용자 결정):** More 없이 5개 메뉴(레이어·색상·선스타일·문자스타일·치수스타일)를 항상 Header에 둡니다. 로고 옆 Open 유무와 관계없이 넘치지 않으며, 좁아질수록 **< 1800** 그리기모드 글자 숨김·간격 축소 → 미리보기 숨김 → 파일명 축소(최소 96px, `…`) → 정보값 `…` 순으로 공간을 줄입니다. **미리보기 일괄 숨김(2026-10-23 사용자 결정):** 레이어 상태 아이콘(전구·파란 아이콘·자물쇠)·색상 칩·선스타일 점선·문자스타일 글꼴명(Arial)·치수스타일 치수선 5종은 하나의 측정 단계로 처리해, 모두 들어갈 공간이 있을 때만 모두 표시하고 부족하면 같은 시점에 모두 숨기며(그 폭도 제거), 넓어져 다시 모두 들어가면 함께 복원합니다. 메뉴별로 차례로 숨기지 않으며 일부만 남는 상태는 없습니다(예시 데이터 기준 1920px 표시·1440px 숨김, 경계 약 1650px). 이름·값 텍스트는 그대로입니다. 메뉴 타이틀은 모든 폭에서 표시하며 공간이 부족하면 `…`로 줄어듭니다. **≤ 900** 선스타일은 선스타일 값만. Dropdown 열기·닫기는 그대로입니다.
**가용 공간 분배(2026-10-23 사용자 결정):** Header 폭 **760~1920px**(1440px 포함)에서는 레이어부터 치수스타일까지 5개 그룹이 속성보기 앞에 남는 공간을 나눠 가져(각 그룹이 자기 내용 폭에서 같은 양만큼 늘어나고 필드 면도 함께 늘어남) 속성보기 바로 옆까지 채웁니다. **1920px 초과**에서는 각 그룹이 1920px 폭(레이어 234 · 색상 192 · 선스타일 284 · 문자스타일 164 · 치수스타일 164px)으로 고정되고 남는 공간은 속성보기 앞 여백으로 남습니다(그룹을 더 늘리지 않음). 숨김·아이콘 전환·More 단계는 늘어나기 전 내용 폭으로 측정하므로 그대로이며, 오른쪽 창 영역·창 제어 위치와 Dropdown 폭도 그대로입니다. `controls`로 구성한 Header(`data-configured`)와 760px 미만에는 적용하지 않습니다. 소비 CSS로 그룹 폭을 따로 지정하지 않습니다. 아래 Breakpoint·실측 More는 **760px 미만**에만 적용됩니다.
760px 미만 Container `hm-sw-header` 기준 Breakpoint: **< 1800** 선/문자/치수스타일 → More, **< 1180** 그리기모드 숨김, **< 1080** 상세보기 → More·창 이동 24px(창 영역 176), **< 900** 색상 → More, **< 640** 인쇄·K → More·창 이동 숨김(창 영역 64), **< 520** 레이어 상태 아이콘 숨김. More에서 고른 필드는 첫 칸(레이어 자리)에 표시되고 Dropdown이 바로 열립니다(UI 상태만, 데이터 Callback 없음).
@@ -144,12 +146,12 @@
| Header | `--hm-sw-viewer-height` 52px, `--hm-sw-viewer-background` #0a0a0b(단색, 2026-10-21 — Storybook 뷰어 캔버스 바탕색), `--hm-sw-viewer-border-color` #071e18, `--hm-sw-viewer-text` #fff, `--hm-sw-viewer-muted` #bddfd4, `--hm-sw-viewer-accent` #ffc600, `--hm-sw-viewer-hover` #266651 |
| 로고 면 | 2026-10-23부터 `SWHeader` 홈과 같은 사선 배경(Figma 에셋 9-slice)·같은 폭 계산(왼쪽 12 + 로고 + 36, 다음 부품과 17px 겹침, 예시 로고에서 79px). `--hm-sw-viewer-brand-width` 기본 `auto`. 로고 다음 첫 부품(파일 열기·파일명) 시작 여백 8px. Hover 효과 없음(기존 그대로, `SWHeader` Hover와 다름). 로고가 없으면 로고 면 없음(폭 0, 첫 부품 12px 안쪽). `--hm-sw-viewer-brand-empty-width` 24px·`--hm-sw-viewer-brand-top` #123328·`-middle` #205241·`-bottom` #071e18은 더 쓰지 않으며 호환용으로만 남음 |
| 크기·글자 | `--hm-sw-viewer-gap` 4px, `--hm-sw-viewer-radius` 4px, `--hm-sw-viewer-control-height` 40px, `--hm-sw-viewer-button-height` 34px, `--hm-sw-viewer-tabs-max-width` 378px(tabs만 있는 field 그룹의 최대 폭, 2026-10-23), `--hm-sw-viewer-font-size` 14px, `--hm-sw-viewer-label-size` 11px, `--hm-sw-viewer-icon-size` 16px, `--hm-sw-viewer-disabled-opacity` 0.6 |
| Model / Layout 탭(Figma 7652:40344, 2026-10-23) | 컨테이너: `--hm-sw-viewer-tabs-height` = `calc(var(--hm-sw-viewer-control-height) + 2px)` = 42px(옆 field 그룹 바깥 높이와 같음; 바깥 1px은 blur 외곽선 자리, 그 안 검정 여백 위·좌우 1px·아래 1.5px, 버튼은 나머지 37.5px를 채움), `--hm-sw-viewer-tabs-radius` 6px, `--hm-sw-viewer-tabs-background` `rgba(0, 0, 0, 0.8)`(면), `--hm-sw-viewer-tabs-outline-color` #349f7f(42px 상자 바깥 가장자리 1px 선, `::before`가 상자와 같은 영역, blur는 `clip-path: inset(-1px …)`로 바깥 1px까지), `--hm-sw-viewer-tabs-outline-blur` 1px(그 선의 blur, Figma Layer blur 2), `--hm-sw-viewer-tabs-glow-color` `transparent`(추가 Glow 색, 기본 없음 — Figma는 blur 외곽선이 Glow), `--hm-sw-viewer-tabs-glow` `0 0 4px var(--hm-sw-viewer-tabs-glow-color)`, `--hm-sw-viewer-tabs-shadow` `inset 0 2.857px 2.857px rgba(0, 0, 0, 0.25)`(버튼 위에 겹치는 안쪽 그림자). 버튼 공통: 높이는 컨테이너를 채움(별도 높이 토큰 없음), `--hm-sw-viewer-tab-radius` 5px(바깥 모서리만), `--hm-sw-viewer-tab-border-width` 1.428px, `--hm-sw-viewer-tab-gap` 14px(이름–개수; 버튼 좌우 안쪽 여백 5px와 함께 기존 기본 폭·측정 단계 유지). 객체 면의 첫 그룹이 Model / Layout 탭만 가진 경우에만 그 그룹 오른쪽에 `margin-right: 1px`(조건부 규칙, 다른 Header 그룹 간격은 객체 면 gap 그대로): 다음 그룹까지 1180px 이상 5px, 미만 3px. 최대 너비 378px·높이 42px·반응형 More 이동 단계는 그대로(버튼 좌우 여백 5px), `--hm-sw-viewer-tab-background` #0e3d2f(Gradient 아래 바탕), `--hm-sw-viewer-tab-gradient` `linear-gradient(180deg, #081813 0%, #09271e 8.6884%, #0b3227 25.428%, #0e3d2f 51.252%, #0b3227 80.027%, #0a2b21 89.759%, #081813 100%)`. 테두리 토큰은 이미지(Figma Gradient Stroke, 위→아래)이며 단색은 `linear-gradient(<색>, <색>)`로 지정합니다. 선택: `--hm-sw-viewer-tab-active-border` `linear-gradient(180deg, #a67d03 0%, #f2cc0f 16.4%, #fadd4f 37.2%, #f2cc0f 62%, #926e03 87.9%)`, `--hm-sw-viewer-tab-active-text` = `var(--hm-sw-viewer-accent)`(#ffc600). 비선택: `--hm-sw-viewer-tab-inactive-border` `linear-gradient(180deg, #189b6e 0%, #287a5e 100%)`, `--hm-sw-viewer-tab-inactive-text` = `var(--hm-sw-viewer-text)`(#fff). 글자는 두 상태 모두 세로 가운데. 광택(비선택 상단): `--hm-sw-viewer-tab-highlight-color` #2d8b6b → `--hm-sw-viewer-tab-highlight-end` `rgba(11, 45, 34, 0)`, `--hm-sw-viewer-tab-highlight-opacity` 0.67, `--hm-sw-viewer-tab-highlight-height` 20px. 상호작용: `--hm-sw-viewer-tab-hover-border` = `var(--hm-sw-viewer-tab-inactive-border)`, `--hm-sw-viewer-tab-hover-text` = `var(--hm-sw-viewer-accent)`, `--hm-sw-viewer-tab-focus-color` = `var(--hm-sw-viewer-accent)`. `--hm-sw-viewer-tab-border-face`는 상태별 테두리를 고르는 내부 변수라 지정하지 않습니다 |
| Model / Layout 탭(Figma 7652:40344, 2026-10-23) | 컨테이너: `--hm-sw-viewer-tabs-height` = `calc(var(--hm-sw-viewer-control-height) + 2px)` = 42px(옆 field 그룹 바깥 높이와 같음; 바깥 1px은 투명 가장자리(외곽선 제거 후에도 크기 유지용), 그 안 검정 여백 위·좌우 1px·아래 1.5px, 버튼은 나머지 37.5px를 채움), `--hm-sw-viewer-tabs-radius` 6px, `--hm-sw-viewer-tabs-background` `rgba(0, 0, 0, 0.8)`(면), `--hm-sw-viewer-tabs-glow-color` `transparent`(추가 Glow 색, 기본 없음 — 녹색 외곽선은 2026-10-23 사용자 요청으로 제거되어 외곽선 토큰 없음), `--hm-sw-viewer-tabs-glow` `0 0 4px var(--hm-sw-viewer-tabs-glow-color)`, `--hm-sw-viewer-tabs-shadow` `inset 0 2.857px 2.857px rgba(0, 0, 0, 0.25)`(버튼 위에 겹치는 안쪽 그림자). 버튼 공통: 높이는 컨테이너를 채움(별도 높이 토큰 없음), `--hm-sw-viewer-tab-radius` 5px(바깥 모서리만), `--hm-sw-viewer-tab-border-width` 1.428px, `--hm-sw-viewer-tab-gap` 14px(이름–개수; 버튼 좌우 안쪽 여백 5px와 함께 기존 기본 폭·측정 단계 유지). 객체 면의 첫 그룹이 Model / Layout 탭만 가진 경우에만 그 그룹 오른쪽에 `margin-right: 1px`(조건부 규칙, 다른 Header 그룹 간격은 객체 면 gap 그대로): 다음 그룹까지 1180px 이상 5px, 미만 3px. 최대 너비 378px·높이 42px·반응형 More 이동 단계는 그대로(버튼 좌우 여백 5px), `--hm-sw-viewer-tab-background` #0e3d2f(Gradient 아래 바탕), `--hm-sw-viewer-tab-gradient` `linear-gradient(180deg, #081813 0%, #09271e 8.6884%, #0b3227 25.428%, #0e3d2f 51.252%, #0b3227 80.027%, #0a2b21 89.759%, #081813 100%)`. 테두리 토큰은 이미지(Figma Gradient Stroke, 위→아래)이며 단색은 `linear-gradient(<색>, <색>)`로 지정합니다. 선택: `--hm-sw-viewer-tab-active-border` `linear-gradient(180deg, #a67d03 0%, #f2cc0f 16.4%, #fadd4f 37.2%, #f2cc0f 62%, #926e03 87.9%)`, `--hm-sw-viewer-tab-active-text` = `var(--hm-sw-viewer-accent)`(#ffc600). 비선택: `--hm-sw-viewer-tab-inactive-border` `linear-gradient(180deg, #189b6e 0%, #287a5e 100%)`, `--hm-sw-viewer-tab-inactive-text` = `var(--hm-sw-viewer-text)`(#fff). 글자는 두 상태 모두 세로 가운데. 긴 이름(2026-10-23): 열은 `minmax(0, 1fr)`로 같은 폭을 유지하며 상자(최대 378px)를 넘지 않고, 이름(`.hm-sw-viewer-header__tab-name`)이 먼저 `text-overflow: ellipsis`로 줄며 개수는 줄이지 않음. `--hm-sw-viewer-tab-name-basis` 3em(14px에서 42px, 기본 "Model" 폭): 공간 계산에서 이름은 이 폭까지만 셈(더 짧은 이름은 자기 폭) — 긴 이름도 일반 이름과 같은 탭 폭·같은 반응형 단계(다른 메뉴 아이콘화·More 이동이 같음)이고, 상자에 남는 공간이 있을 때만 그만큼 이름이 더 보임. 광택(비선택 상단): `--hm-sw-viewer-tab-highlight-color` #2d8b6b → `--hm-sw-viewer-tab-highlight-end` `rgba(11, 45, 34, 0)`, `--hm-sw-viewer-tab-highlight-opacity` 0.67, `--hm-sw-viewer-tab-highlight-height` 20px. 상호작용: `--hm-sw-viewer-tab-hover-border` = `var(--hm-sw-viewer-tab-inactive-border)`, `--hm-sw-viewer-tab-hover-text` = `var(--hm-sw-viewer-accent)`, `--hm-sw-viewer-tab-focus-color` = `var(--hm-sw-viewer-accent)`. `--hm-sw-viewer-tab-border-face`는 상태별 테두리를 고르는 내부 변수라 지정하지 않습니다 |
| Header 스위치·아이콘 | `--hm-sw-viewer-switch-off` #020a07(OFF 트랙), `--hm-sw-viewer-switch-thumb-off` #5f7a71(OFF 원형, 흐린 회녹색), `--hm-sw-viewer-switch-on` = `var(--hm-sw-viewer-hover)`(ON 트랙), `--hm-sw-viewer-switch-thumb-on` = `var(--hm-sw-viewer-muted)`(ON 원형, 민트; 노란색·테두리 없음, 2026-10-22), `--hm-sw-viewer-switch-bevel` `inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 -1px 1px rgba(0, 0, 0, 0.25)`, `--hm-sw-viewer-icon-outline-color` = `var(--hm-sw-header-text-outline-color)`, `--hm-sw-viewer-icon-outline-width` 1px |
| Dropdown·More | `--hm-sw-viewer-menu-background` #fff, `--hm-sw-viewer-menu-border-color` #dedbd2, `--hm-sw-viewer-menu-text` #0a221d, `--hm-sw-viewer-menu-muted` #6e6759, `--hm-sw-viewer-menu-highlight` `rgba(189, 223, 212, 0.45)`, `--hm-sw-viewer-menu-highlight-text` #123328, `--hm-sw-viewer-menu-shadow` `0 0 12px rgba(0, 0, 0, 0.2)`, `--hm-sw-viewer-menu-item-height` 36px, `--hm-sw-viewer-more-background` #f5f0e5, `--hm-sw-viewer-more-border-color` #897445 |
| 패널 콘텐츠 | `--hm-sw-viewer-panel-background` = `var(--hm-sw-property-surface)`, `--hm-sw-viewer-panel-field-background` = `var(--hm-sw-property-content)`, `--hm-sw-viewer-panel-inner-border` = `var(--hm-sw-property-inner-border)`, `--hm-sw-viewer-panel-text` = `var(--hm-sw-property-label)`, `--hm-sw-viewer-panel-heading` #3e3523, `--hm-sw-viewer-panel-label` #6e6759, `--hm-sw-viewer-panel-primary` #123328, `--hm-sw-viewer-panel-switch-off` = `var(--hm-sw-property-inner-border)`, `--hm-sw-viewer-panel-switch-on` #266651, `--hm-sw-viewer-panel-badge-background` = `var(--hm-sw-property-table)`, `--hm-sw-viewer-panel-overlay` `rgba(0, 0, 0, 0.1)`, `--hm-sw-viewer-panel-spacing` 12px |
| 패널 콘텐츠 | `--hm-sw-viewer-panel-background` = `var(--hm-sw-property-surface)`, `--hm-sw-viewer-panel-field-background` = `var(--hm-sw-property-content)`, `--hm-sw-viewer-panel-inner-border` = `var(--hm-sw-property-inner-border)`, `--hm-sw-viewer-panel-text` = `var(--hm-sw-property-label)`, `--hm-sw-viewer-panel-heading` #3e3523, `--hm-sw-viewer-panel-label` #6e6759, `--hm-sw-viewer-panel-primary` #123328, `--hm-sw-viewer-panel-switch-off` = `var(--hm-sw-property-inner-border)`, `--hm-sw-viewer-panel-switch-on` #266651, `--hm-sw-viewer-panel-badge-background` = `var(--hm-sw-property-table)`, `--hm-sw-viewer-panel-overlay` `rgba(0, 0, 0, 0.1)`, `--hm-sw-viewer-panel-spacing` 12px, `--hm-sw-viewer-panel-icon-color` #c7bda9(패널 제목 막대 아이콘 버튼 — 접기·`ghost` 추가 버튼 — 전용, 2026-10-23. Header 상태 글자·스위치 캡션은 `--hm-sw-viewer-muted` 유지, SWPopup 자체 제목 아이콘은 이미지) |
- **Model / Layout 탭 토큰:** 위 `--hm-sw-viewer-tabs-*`·`--hm-sw-viewer-tab-*`은 탭 컨테이너·버튼에만 연결되며 다른 Header 부품·SWHeader·Footer·패널에 쓰이지 않습니다. 탭 색만 바꾸려면 가장 바깥 SW 범위의 `style`로 덮습니다: 화면을 `<SWTheme style={...}>`로 감싸거나 단독 `SWViewerHeader`의 루트에 지정합니다(토큰이 그 범위에 선언되므로 일반 `div` 래퍼의 값은 가려짐). 예: `style={{ "--hm-sw-viewer-tab-active-border": "linear-gradient(#e07b00, #e07b00)", "--hm-sw-viewer-tabs-outline-color": "#4a7fd0" }}`. `var(...)` 기본값(`active-text`·`hover-*`·`focus-color`)은 SW 범위에서 계산되므로 공통 토큰(`--hm-sw-viewer-accent` 등)을 바꾸면 같은 범위에서 함께 따라가고, 탭 토큰을 직접 지정하면 공통 토큰과 독립합니다. 글자 외곽선(`text-shadow`)은 탭에서도 기존 그대로(이름 `--hm-sw-header-text-outline` 1.3px, 개수 `--hm-sw-header-label-outline` 1px)이며 탭 토큰이 아닙니다. 탭 class로 색을 덮지 않습니다.
- **Model / Layout 탭 토큰:** 위 `--hm-sw-viewer-tabs-*`·`--hm-sw-viewer-tab-*`은 탭 컨테이너·버튼에만 연결되며 다른 Header 부품·SWHeader·Footer·패널에 쓰이지 않습니다. 탭 색만 바꾸려면 가장 바깥 SW 범위의 `style`로 덮습니다: 화면을 `<SWTheme style={...}>`로 감싸거나 단독 `SWViewerHeader`의 루트에 지정합니다(토큰이 그 범위에 선언되므로 일반 `div` 래퍼의 값은 가려짐). 예: `style={{ "--hm-sw-viewer-tab-active-border": "linear-gradient(#e07b00, #e07b00)", "--hm-sw-viewer-tab-inactive-border": "linear-gradient(#3d6bb3, #3d6bb3)" }}`. `var(...)` 기본값(`active-text`·`hover-*`·`focus-color`)은 SW 범위에서 계산되므로 공통 토큰(`--hm-sw-viewer-accent` 등)을 바꾸면 같은 범위에서 함께 따라가고, 탭 토큰을 직접 지정하면 공통 토큰과 독립합니다. 글자 외곽선(`text-shadow`)은 탭에서도 기존 그대로(이름 `--hm-sw-header-text-outline` 1.3px, 개수 `--hm-sw-header-label-outline` 1px)이며 탭 토큰이 아닙니다. 탭 class로 색을 덮지 않습니다.
- **공유 토큰(모드 혼합 아님):** 뷰어 Header의 파일명·객체 수정·속성 면은 `--hm-sw-header-document-*`, `--hm-sw-header-object-*`, `--hm-sw-header-property-*`를, 글자 외곽선은 `--hm-sw-header-text-outline`·`--hm-sw-header-label-outline`을 그대로 읽습니다(§2·§3). 패널 제목 막대 추가 버튼(`ghost`)의 Hover·Focus 색은 `--hm-sw-header-selected-color`입니다.
| 상태 | Header 항목(button·toggle·menu·tabs·switch) | 패널 콘텐츠 |
@@ -160,7 +162,7 @@
| Focus | 2px solid `--hm-sw-viewer-accent` 외곽선(탭: `--hm-sw-viewer-tab-focus-color`, 버튼 안쪽 -2px) | 버튼·Input·Slider: 2px solid `--hm-sw-viewer-panel-switch-on` 외곽선 |
| Disabled | `--hm-sw-viewer-disabled-opacity`(0.6) | 버튼·Input·Field 0.5 |
| Dropdown 항목 | 면 `--hm-sw-viewer-menu-highlight`, 글자 `--hm-sw-viewer-menu-highlight-text`(Focus·Hover·서브메뉴 열림), Disabled 0.5 | — |
| 상태 표시 | `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` 아이콘 회전. Label 칸은 표시 가능한 가장 긴 상태 텍스트의 실제 렌더링 너비로 고정되어 상태가 바뀌어도 너비·주변 메뉴 위치가 그대로이며(`idle`은 아이콘 자리 비움), 공간이 부족하면(측정 단계·파일명 축소 후) 줄어들며 `…`, 넓어지면 전체 표시([VIEWER.md](./VIEWER.md) §2.2) | — |
- **Typography(뷰어):** Header 항목 14px 500, 캡션·개수·상태 11px 400, 패널 필드 라벨 14px 500, Section 제목 12px 700, 패널 버튼·Badge 12px 500, Input 12px. 패널 콘텐츠(2026-10-22): 레이어 이름·속성 행 Label 14px 500, 읽기 전용 속성값 14px(패널 기본 Weight), 설명·Input·Select 값·Placeholder 12px, 별도 `font-family` 지정 없음([VIEWER.md](./VIEWER.md) §4.2). Font Weight는 400/500/700만 씁니다.
- **레이어 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 상단 `SWViewerSection title="전체 레이어"`(기존 Section 제목) + 같은 줄의 같은 전구 버튼 하나(`role="switch"`, 전체 표시일 때만 켜짐, 행 전구와 세로 정렬). 검색·전체 레이어 행은 고정, 목록만 세로 스크롤; 검색과 전체 레이어 사이 구분선 없음, 전체 레이어 행 아래 1px `#d2c8b4`(`--hm-sw-property-table`) + 아래 여백 8px. 개별·전체 표시·숨김은 Switch로 대체하지 않는다([VIEWER.md](./VIEWER.md) §4.1).
+7 -2
View File
@@ -31,7 +31,7 @@
| `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`), 공간이 적으면 그만큼만 넓어지고 기본 폭보다 부족하면 기존 측정 단계대로 More로 이동합니다. 모양(2026-10-23, Figma 7652:40344): tabs만 있는 그룹은 field 면 없이 옆 field 그룹과 같은 42px(`--hm-sw-viewer-control-height` + 2px) 어두운 컨테이너(검정 여백 위·좌우 1px·아래 1.5px, 가장자리 blur 처리한 1px #349f7f 선 — blur는 상자 밖 1px까지, 안쪽 그림자)가 면이 되고, 버튼(컨테이너를 채워 37.5px)은 같은 Gradient 면과 1.428px Gradient 테두리(선택 금색 #a67d03→#fadd4f→#926e03 + 글자 #ffc600, 비선택 #189b6e→#287a5e + 흰 글자 + 상단 초록 광택), 이름 14px 700·개수 14px 400, 간격 14px(버튼 좌우 여백 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, Story `SW/Viewer › Model / Layout`) |
| `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`), 공간이 적으면 그만큼만 넓어지고 기본 폭보다 부족하면 기존 측정 단계대로 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 캡션) |
| `type: "custom"` | `render()`, `menuEntries?`(More에 넣을 항목) |
| `onCommand(command)` | 모든 명령을 한 곳에서 받습니다: `{ id, itemId, value? }`. 항목 자체 Callback 다음에 호출 |
@@ -67,6 +67,11 @@
- 실제 창 최소화·닫기는 프로그램 담당입니다(`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?`.
@@ -84,7 +89,7 @@
| `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, 글자색만 Hover #FFC600 — SW 제목 막대 방식) |
| `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 조상 안 — 미리보기·임베드) |
@@ -1,5 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="308" height="38" viewBox="0 0 308 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Union" filter="url(#filter0_d_0_140)">
<g id="Union">
<mask id="path-1-inside-1_0_140" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M252 6H0V33C0 34.6569 1.34314 36 3 36H252H260H270.947C276.28 36 280.369 29.2601 284.685 22.1464C289.05 14.9502 293.648 7.37149 300 6H260H252Z"/>
</mask>
@@ -7,16 +7,6 @@
<path d="M0 6V5H-1V6H0ZM284.685 22.1464L285.54 22.665V22.665L284.685 22.1464ZM300 6L300.211 6.97748L300 5V6ZM0 6V7H252V6V5H0V6ZM0 33H1V6H0H-1V33H0ZM3 36V35C1.89543 35 1 34.1046 1 33H0H-1C-1 35.2091 0.790855 37 3 37V36ZM252 36V35H3V36V37H252V36ZM252 36V37H260V36V35H252V36ZM260 36V37H270.947V36V35H260V36ZM270.947 36V37C274.062 37 276.672 35.0311 278.953 32.3953C281.248 29.7442 283.395 26.2 285.54 22.665L284.685 22.1464L283.83 21.6277C281.658 25.2064 279.603 28.5891 277.441 31.0864C275.266 33.5989 273.165 35 270.947 35V36ZM284.685 22.1464L285.54 22.665C287.732 19.051 289.933 15.4276 292.363 12.5318C294.799 9.62962 297.357 7.59367 300.211 6.97748L300 6L299.789 5.02252C296.291 5.77782 293.374 8.21697 290.831 11.246C288.284 14.2816 286.003 18.0456 283.83 21.6277L284.685 22.1464ZM300 6V5H260V6V7H300V6ZM260 6V5H252V6V7H260V6Z" fill="url(#paint0_linear_0_140)" mask="url(#path-1-inside-1_0_140)"/>
</g>
<defs>
<filter id="filter0_d_0_140" x="0" y="0" width="308" height="38" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="4" dy="-2"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_140"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_140" result="shape"/>
</filter>
<linearGradient id="paint0_linear_0_140" x1="150" y1="6" x2="150" y2="36" gradientUnits="userSpaceOnUse">
<stop stop-color="#493927"/>
<stop offset="0.17" stop-color="#6F5739"/>

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+1 -11
View File
@@ -1,5 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="308" height="38" viewBox="0 0 308 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Union" filter="url(#filter0_d_0_167)">
<g id="Union">
<mask id="path-1-inside-1_0_167" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M252 6H0V33C0 34.6569 1.34314 36 3 36H252H260H270.947C276.28 36 280.369 29.2601 284.685 22.1464C289.05 14.9502 293.648 7.37149 300 6H260H252Z"/>
</mask>
@@ -8,16 +8,6 @@
<path d="M0 6V5H-1V6H0ZM284.685 22.1464L285.54 22.665V22.665L284.685 22.1464ZM300 6L300.211 6.97748L300 5V6ZM0 6V7H252V6V5H0V6ZM0 33H1V6H0H-1V33H0ZM3 36V35C1.89543 35 1 34.1046 1 33H0H-1C-1 35.2091 0.790855 37 3 37V36ZM252 36V35H3V36V37H252V36ZM252 36V37H260V36V35H252V36ZM260 36V37H270.947V36V35H260V36ZM270.947 36V37C274.062 37 276.672 35.0311 278.953 32.3953C281.248 29.7442 283.395 26.2 285.54 22.665L284.685 22.1464L283.83 21.6277C281.658 25.2064 279.603 28.5891 277.441 31.0864C275.266 33.5989 273.165 35 270.947 35V36ZM284.685 22.1464L285.54 22.665C287.732 19.051 289.933 15.4276 292.363 12.5318C294.799 9.62962 297.357 7.59367 300.211 6.97748L300 6L299.789 5.02252C296.291 5.77782 293.374 8.21697 290.831 11.246C288.284 14.2816 286.003 18.0456 283.83 21.6277L284.685 22.1464ZM300 6V5H260V6V7H300V6ZM260 6V5H252V6V7H260V6Z" fill="url(#paint1_linear_0_167)" mask="url(#path-1-inside-1_0_167)"/>
</g>
<defs>
<filter id="filter0_d_0_167" x="0" y="0" width="308" height="38" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="4" dy="-2"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_167"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_167" result="shape"/>
</filter>
<linearGradient id="paint0_linear_0_167" x1="161.151" y1="6" x2="161.151" y2="38.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F3025"/>
<stop offset="0.34535" stop-color="#194335"/>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -46,8 +46,8 @@ const MORE_BELOW = 760
/** File name width from 760px up: Figma 310px, smallest 96px (sw.css .hm-sw-header__document). */
const DOCUMENT_MAX = 310
const DOCUMENT_MIN = 96
/** Field pictures that hide one at a time while the header lacks room (sw.css [data-compact]). */
const COMPACT_LEVELS = 5
/** Field pictures hide together, in one step, while the header lacks room (sw.css [data-compact], 2026-10-23). */
const COMPACT_LEVELS = 1
/** From this header width the Figma field boxes apply (sw.css "Responsive"). */
const FIGMA_LAYOUT_FROM = 1800
@@ -304,9 +304,9 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
})
// 2026-10-22 (user request): from 760px to 1800px the space left decides, not fixed widths. While the object area lacks
// room, the field pictures hide one at a time (data-compact 1-5: 치수 아이콘, 선 그림, 글꼴명, 색상 칩, 레이어 상태
// 아이콘, sw.css), then the file name gives way (310 -> 96px), then the values ellipsize. With room to spare the
// pictures stay and the file name keeps 310px.
// room, the field pictures hide all at once (data-compact 1: 레이어 상태 아이콘, 색상 칩, 선 그림, 글꼴명, 치수 아이콘,
// sw.css; 2026-10-23 user request, formerly one at a time), then the file name gives way (310 -> 96px), then the
// values ellipsize. With room for all of them the pictures stay and the file name keeps 310px.
// Measured on every render and again once web fonts finish loading (text widths change, the header size does not).
const fitFieldsRef = useRef(() => {})
fitFieldsRef.current = () => {
@@ -45,12 +45,24 @@ const StatusIcon = ({ state }: { state: SWViewerStatusState }) => state === "idl
</svg>
)
/** Load state of the viewer header (reference status: muted 11px text, icon; text hides below 560px). */
/**
* Load state of the viewer header (reference status: muted 11px text, icon; text hides below 560px). The label area is
* as wide as the widest text it can show (2026-10-23): every state label (and a custom `label`) sits in the same grid
* cell, the others hidden, so a state change never resizes it or moves the menus; with too little room it ellipsizes.
* The icon slot is kept (empty) for `idle` for the same reason.
*/
export function SWViewerStatus({ state, label, detail, className, ...rest }: SWViewerStatusProps) {
const text = label ?? statusLabel[state]
const sizers = Array.from(new Set([text, ...Object.values(statusLabel)])).filter(candidate => candidate !== text)
return (
<div role="status" className={cx("hm-sw-viewer-status", className)} data-state={state} title={detail} {...rest}>
{state !== "idle" && <span className="hm-sw-viewer-status__icon"><SWViewerIcon><StatusIcon state={state} /></SWViewerIcon></span>}
<span className="hm-sw-viewer-status__label">{label ?? statusLabel[state]}</span>
<span className="hm-sw-viewer-status__icon" data-empty={state === "idle" ? "" : undefined} aria-hidden={state === "idle" ? true : undefined}>
{state !== "idle" && <SWViewerIcon><StatusIcon state={state} /></SWViewerIcon>}
</span>
<span className="hm-sw-viewer-status__label">
<span className="hm-sw-viewer-status__text">{text}</span>
{sizers.map(candidate => <span key={candidate} className="hm-sw-viewer-status__sizer" aria-hidden="true">{candidate}</span>)}
</span>
{detail && <span className="hm-sw-viewer-sr-only">: {detail}</span>}
</div>
)
@@ -212,7 +212,8 @@ function Tabs({ item, onCommand }: { item: Extract<SWViewerHeaderItem, { type: "
<button key={option.value} type="button" role="radio" aria-checked={selected} tabIndex={option.value === current ? 0 : -1}
className="hm-sw-viewer-header__tab" data-state={selected ? "active" : undefined} title={option.title} disabled={item.disabled || option.disabled}
onClick={() => { if (!selected) select(option.value) }}>
{option.label}
{/* The name shrinks first (ellipsis, sw-viewer.css); the full name stays the accessible name. */}
<span className="hm-sw-viewer-header__tab-name">{option.label}</span>
{option.count !== undefined && option.count > 0 && <span className="hm-sw-viewer-header__count">{option.count.toLocaleString()}</span>}
</button>
)
+57 -27
View File
@@ -34,10 +34,7 @@
--hm-sw-viewer-tabs-height: calc(var(--hm-sw-viewer-control-height) + 2px);
--hm-sw-viewer-tabs-radius: 6px;
--hm-sw-viewer-tabs-background: rgba(0, 0, 0, 0.8);
/* Outline: a 1px line just outside the frame, blurred (Figma Rectangle 9333: stroke outside + layer blur 2). */
--hm-sw-viewer-tabs-outline-color: #349f7f;
--hm-sw-viewer-tabs-outline-blur: 1px;
/* Extra glow around the frame: none in Figma (the blurred outline is the glow); set a color to add one. */
/* Extra glow around the frame: none by default (the green outline was removed on 2026-10-23); set a color to add one. */
--hm-sw-viewer-tabs-glow-color: transparent;
--hm-sw-viewer-tabs-glow: 0 0 4px var(--hm-sw-viewer-tabs-glow-color);
--hm-sw-viewer-tabs-shadow: inset 0 2.857px 2.857px rgba(0, 0, 0, 0.25);
@@ -45,6 +42,9 @@
--hm-sw-viewer-tab-radius: 5px;
--hm-sw-viewer-tab-border-width: 1.428px;
--hm-sw-viewer-tab-gap: 14px;
/* Width a tab name counts for in the space calculation (2026-10-23): a longer name measures as this (about the
default "Model"), so it never claims more room, takes free room when there is some and ellipsizes otherwise. */
--hm-sw-viewer-tab-name-basis: 3em;
--hm-sw-viewer-tab-background: #0e3d2f;
--hm-sw-viewer-tab-gradient: linear-gradient(180deg, #081813 0%, #09271e 8.6884%, #0b3227 25.428%, #0e3d2f 51.252%, #0b3227 80.027%, #0a2b21 89.759%, #081813 100%);
/* Selected (Model in Figma) / not selected (Layout in Figma). Borders are images (Figma gradient strokes, top to
@@ -105,6 +105,9 @@
--hm-sw-viewer-panel-badge-background: var(--hm-sw-property-table);
--hm-sw-viewer-panel-overlay: rgba(0, 0, 0, 0.1);
--hm-sw-viewer-panel-spacing: 12px;
/* Title-bar icon buttons of viewer panels (collapse, `ghost` header actions; 2026-10-23). Popup-only: the header
status text and switch captions keep --hm-sw-viewer-muted. SWPopup's own title icons are images. */
--hm-sw-viewer-panel-icon-color: #c7bda9;
}
.hm-sw-viewer-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@@ -160,7 +163,9 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-header__document {
display: flex;
align-items: center;
flex: 0 1 clamp(140px, 25cqw, 360px);
/* Very large shrink weight (2026-10-23): the file name gives way first, as before; the object (shrink 1) only once
the file name is at its minimum (see .hm-sw-viewer-header__object). */
flex: 0 1000000 clamp(140px, 25cqw, 360px);
width: clamp(220px, 25cqw, 360px);
min-width: 56px;
height: calc(var(--hm-sw-viewer-height) - 2px);
@@ -318,7 +323,10 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-header__object {
display: flex;
align-items: center;
flex: 1 0 auto;
/* Shrinkable (2026-10-23): the file name still gives way first (its huge shrink weight, down to its minimum); only
when nothing is left after the measured steps does the object give way, so the status label ellipsizes instead of
the header overflowing. The measurement (data-measuring) keeps flex: 0 0 auto. */
flex: 1 1 auto;
min-width: 0;
height: calc(var(--hm-sw-viewer-height) - 2px);
gap: var(--hm-sw-viewer-gap);
@@ -399,34 +407,25 @@ button.hm-sw-viewer-header__brand { text-align: left; }
position: relative;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
/* minmax(0, 1fr) (2026-10-23): equal columns that may shrink below their content, so a long name never pushes the
box past its group (max 378px) or makes the buttons unequal; the name ellipsizes instead (below). */
grid-auto-columns: minmax(0, 1fr);
min-width: 0;
align-items: stretch;
gap: 1px;
box-sizing: border-box;
height: var(--hm-sw-viewer-tabs-height);
/* The transparent 1px edge is the room of the blurred outline (it stays inside the box height); the padding is the
black margin around the buttons, which fill the rest. 1.5px at the bottom (2026-10-23 user value): the outline
blur covers part of a 1px bottom margin, while the inner shadow darkens the top, so 1px read as no room below. */
/* The transparent 1px edge was the room of the outline (removed 2026-10-23, user request); it stays so the box and
button sizes do not change. The padding is the black margin around the buttons, which fill the rest; 1.5px at the
bottom (2026-10-23 user value). */
padding: 1px 1px 1.5px 1px;
border: 1px solid transparent;
border-radius: calc(var(--hm-sw-viewer-tabs-radius) + 1px);
background: var(--hm-sw-viewer-tabs-background) padding-box;
box-shadow: var(--hm-sw-viewer-tabs-glow);
}
/* The outline (blurred, on the frame edge) and the inner shadow lie over the buttons, as the last Figma layers do. */
.hm-sw-viewer-header__tabs::before {
content: "";
position: absolute;
z-index: 1;
/* The outer edge of the box (its border box, over the transparent 1px edge row): same size as the tabs box. */
inset: -1px;
border: 1px solid var(--hm-sw-viewer-tabs-outline-color);
border-radius: inherit;
filter: blur(var(--hm-sw-viewer-tabs-outline-blur));
/* The blur may soften 1px past that edge, no further (2026-10-23 user decision). */
clip-path: inset(-1px round calc(var(--hm-sw-viewer-tabs-radius) + 1px));
pointer-events: none;
}
/* The inner shadow lies over the buttons, as the last Figma layer does. (The blurred green outline, ::before, was
removed on 2026-10-23 at the user's request, with its outline color / blur tokens.) */
.hm-sw-viewer-header__tabs::after { content: ""; position: absolute; z-index: 1; inset: 0; border-radius: var(--hm-sw-viewer-tabs-radius); box-shadow: var(--hm-sw-viewer-tabs-shadow); pointer-events: none; }
/* A group holding only the tabs: the container is its face (no field face around it, as in Figma: the tabs sit on
the header object surface). */
@@ -474,7 +473,23 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-header .hm-sw-viewer-header__tab[data-state="active"] { --hm-sw-viewer-tab-border-face: var(--hm-sw-viewer-tab-active-border); color: var(--hm-sw-viewer-tab-active-text); }
.hm-sw-viewer-header__count { font-size: var(--hm-sw-viewer-label-size); font-weight: 400; font-variant-numeric: tabular-nums; }
/* In a tab the count has the name's size (Figma: 14px Regular) and color. */
.hm-sw-viewer-header__tab .hm-sw-viewer-header__count { font-size: inherit; }
.hm-sw-viewer-header__tab .hm-sw-viewer-header__count { flex: 0 0 auto; font-size: inherit; }
/* Long names (2026-10-23 user decision): the name gives way first with an ellipsis, the count keeps its width; the
outline may paint 2px outside the clipped name (as the file name). The name counts at most
--hm-sw-viewer-tab-name-basis in the tabs' natural width (width + max-width: max-content: shorter names keep their own
width), so a long name changes neither the tabs' width nor the measured steps of the other items; it grows (flex-grow,
up to its text) only into room the box has anyway. */
.hm-sw-viewer-header .hm-sw-viewer-header__tab { min-width: 0; }
.hm-sw-viewer-header__tab-name {
flex: 1 1 auto;
width: var(--hm-sw-viewer-tab-name-basis);
max-width: max-content;
min-width: 0;
overflow: clip;
overflow-clip-margin: var(--hm-sw-header-text-outline-room);
text-overflow: ellipsis;
white-space: nowrap;
}
/* Switch fields (2.5D, 어두운 배경): 11px muted caption above the switch. */
.hm-sw-viewer-header__switch-field {
@@ -533,7 +548,22 @@ button.hm-sw-viewer-header__brand { text-align: left; }
font-weight: 400;
white-space: nowrap;
}
.hm-sw-viewer-status__icon { display: inline-flex; width: var(--hm-sw-viewer-icon-size); height: var(--hm-sw-viewer-icon-size); }
.hm-sw-viewer-status__icon { display: inline-flex; flex: 0 0 auto; width: var(--hm-sw-viewer-icon-size); height: var(--hm-sw-viewer-icon-size); }
/* Stable status width (2026-10-23): the empty idle icon slot keeps its place; the label is one grid cell holding the
shown text and, hidden, every other text it can show (SWViewerStatus), so its width is the widest real candidate in
the real font. With too little room (after the measured steps) it shrinks and the text ellipsizes; with room again
the full text returns. */
.hm-sw-viewer-status__icon[data-empty] { visibility: hidden; }
.hm-sw-viewer-status__label { display: inline-grid; grid-template-columns: minmax(0, auto); flex: 0 1 auto; min-width: 0; }
.hm-sw-viewer-status__label > * {
grid-area: 1 / 1;
min-width: 0;
overflow: clip;
overflow-clip-margin: var(--hm-sw-header-text-outline-room);
text-overflow: ellipsis;
white-space: nowrap;
}
.hm-sw-viewer-status__sizer { visibility: hidden; }
.hm-sw-viewer-status[data-state="error"] { color: var(--hm-sw-viewer-accent); }
.hm-sw-viewer-status__spin { animation: hm-sw-viewer-spin 1s linear infinite; }
@keyframes hm-sw-viewer-spin { to { transform: rotate(360deg); } }
@@ -753,7 +783,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
border: 0;
border-radius: 0;
background: transparent;
color: var(--hm-sw-viewer-muted);
color: var(--hm-sw-viewer-panel-icon-color);
}
.hm-sw-viewer-panel :is(.hm-sw-viewer-panel__icon-button, .hm-sw-viewer-button[data-variant="ghost"]):hover:not(:disabled) { background: transparent; color: var(--hm-sw-header-selected-color); }
.hm-sw-viewer-panel :is(.hm-sw-viewer-panel__icon-button, .hm-sw-viewer-button[data-variant="ghost"]):focus-visible { outline: 2px solid var(--hm-sw-header-selected-color); outline-offset: -2px; }
+17 -9
View File
@@ -1637,7 +1637,7 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
.hm-sw-property__close-hover { visibility: hidden; }
.hm-sw-property__close:hover > .hm-sw-property__close-default { visibility: hidden; }
.hm-sw-property__close:hover > .hm-sw-property__close-hover { visibility: visible; }
.hm-sw-property__body { display: flex; flex-direction: column; gap: 8px; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 7px 7px 7px; box-shadow: inset 0 2px 2px rgba(255,255,255,.6); }
.hm-sw-property__body { display: flex; flex-direction: column; gap: 8px; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 7px 7px 7px; }
/* Popup scrollbar: the body and any scrolling area inside it (zero-specificity so content can still override). */
.hm-sw-property__body, .hm-sw-property__body :where(*) { scrollbar-width: thin; scrollbar-color: var(--hm-sw-popup-scrollbar-thumb) var(--hm-sw-popup-scrollbar-track); }
/* Popup toggle switch (Figma toggle_btn_new 633:75188 / 633:75902): one face for every switch inside an SW popup —
@@ -1948,7 +1948,7 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
< 1800 the 그리기모드 text hides; fields take their content width and shrink with an ellipsis, each by its own width.
< 1080 window move area 88 -> 24 (window 240 -> 176), field insets 6px.
Measured (2026-10-22 user request, SWHeader data-compact): from 760px to 1800px, while the object area lacks
room the field pictures hide one at a time — 치수 아이콘, 선 그림, 글꼴명, 색상 칩, 레이어 상태 아이콘 — then the file
room the field pictures hide together — 레이어 상태 아이콘, 색상 칩, 선 그림, 글꼴명, 치수 아이콘 — then the file
name gives way (310 -> 96px), then the values ellipsize. With room to spare nothing hides.
<= 900 the 선스타일 field shows 선스타일 only (가중치 / 스케일 hide).
Below 760px (phones, e.g. 390) the former tiers are unchanged: parts move into More (선/문자/치수 스타일, 그리기모드,
@@ -1994,6 +1994,16 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
/* 1610 - 1301 + the tighter object area with titles, without 그리기모드 (891, measured) = 1200. */
.hm-sw-header__document { flex-basis: clamp(96px, calc(100cqw - 1200px - var(--hm-sw-header-leading-width, 0px)), 310px); }
}
/* Free room (2026-10-23 user request): from 760px (속성보기 shown) up to a 1920px header the groups from 레이어 to
치수스타일 share the room left before 속성보기 (each grows by the same amount from its own width, its field box with
it), so no gap stays there. Wider headers keep the 1920 widths (234 / 192 / 284 / 164 / 164) and the extra room stays
before 속성보기 (its auto margin). The standard bar only: a `controls` bar ([data-configured]) keeps its own layout.
The measured steps read the natural widths (data-measuring sets flex-grow 0), so hiding is unchanged. */
@container hm-sw-header (min-width: 760px) and (max-width: 1920px) {
.hm-sw-header__fields:not([data-configured]) { flex-grow: 1; }
.hm-sw-header__fields:not([data-configured]) > :is(.hm-sw-header__layer, .hm-sw-header__menu, .hm-sw-header__field),
.hm-sw-header__fields:not([data-configured]) :is(.hm-sw-header__layer, .hm-sw-header__menu) > .hm-sw-header__field { flex-grow: 1; }
}
@container hm-sw-header (min-width: 760px) and (max-width: 1079.98px) {
/* Values need the room (1024px, 2026-10-21 user request): insets 6px, every value part may shrink (ellipsis); the
pictures hide by measurement (data-compact). */
@@ -2151,13 +2161,11 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
/* ...and the file name gives way only down to the default design's smallest width (96px, see the clamps above). */
.hm-sw-header[data-leading="open"] .hm-sw-header__fields[data-configured] > .hm-sw-header__control[data-control="document"] { min-width: 96px; }
/* Measured pictures (SWHeader data-compact, 2026-10-22 user request): from 760px to 1800px the field pictures hide one at a
time while the object area lacks room — 1 치수 아이콘, 2 선 그림, 3 글꼴명, 4 색상 칩, 5 레이어 상태 아이콘. */
.hm-sw-header__fields:is([data-compact="1"], [data-compact="2"], [data-compact="3"], [data-compact="4"], [data-compact="5"]) .hm-sw-header__dimension-icon,
.hm-sw-header__fields:is([data-compact="2"], [data-compact="3"], [data-compact="4"], [data-compact="5"]) .hm-sw-header__linetype,
.hm-sw-header__fields:is([data-compact="3"], [data-compact="4"], [data-compact="5"]) .hm-sw-header__font-name,
.hm-sw-header__fields:is([data-compact="4"], [data-compact="5"]) .hm-sw-header__chip,
.hm-sw-header__fields[data-compact="5"] .hm-sw-header__field--layer .hm-sw-header__layer-icons { display: none; }
/* Measured pictures (SWHeader data-compact): from 760px to 1800px, while the object area lacks room, the field pictures
hide together in one step (2026-10-23 user request; formerly one at a time): 레이어 상태 아이콘, 색상 칩, 선 그림,
글꼴명, 치수 아이콘. They come back together once all of them fit. */
.hm-sw-header__fields[data-compact="1"] :is(.hm-sw-header__dimension-icon, .hm-sw-header__linetype, .hm-sw-header__font-name, .hm-sw-header__chip),
.hm-sw-header__fields[data-compact="1"] .hm-sw-header__field--layer .hm-sw-header__layer-icons { display: none; }
/* Measured More (SWHeader, 2026-10-19): on top of the breakpoints, parts move into More while the object area has no
room for them and return when it has. data-measuring is set for one synchronous layout read and removed before
+13 -4
View File
@@ -326,6 +326,9 @@ if (comparison.reference) {
tabsEqual: tabWidths.every(width => Math.abs(width - tabWidths[0]) <= 0.5),
tabsFit,
labels: [...element.querySelectorAll('.hm-sw-viewer-header__button, .hm-sw-viewer-header__tab, .hm-sw-viewer-header__switch-label')].map(item => item.textContent.trim() || item.getAttribute('aria-label')),
// Buttons shown as icons only by the measured steps (name = aria-label / title) and labels drawn as text.
iconOnly: [...element.querySelectorAll('.hm-sw-viewer-header__button[data-icon-only=""]')].map(item => item.getAttribute('aria-label') || item.getAttribute('title') || item.textContent.trim()).sort(),
shownLabels: [...element.querySelectorAll('.hm-sw-viewer-header__label')].filter(item => rect(item).width > 0).map(item => item.textContent.trim()),
}
})
report.push({ rowWidth: width, ...info })
@@ -335,10 +338,16 @@ if (comparison.reference) {
const at = width => report.find(item => item.rowWidth === width)
check('rows render at their own width', report.every(item => item.width === item.rowWidth), JSON.stringify(report.map(item => [item.rowWidth, item.width])))
check('no horizontal overflow, overlap or part outside the header at any row', report.every(item => item.overflow <= 0 && item.objectOverflow <= 1 && !item.overlaps && !item.outside), JSON.stringify(report.map(item => [item.rowWidth, item.overflow, item.objectOverflow, item.overlaps, item.outside])))
// 2026-10-23: the Model / Layout field widens with the free room up to 378px (1920: 378), narrower with less room;
// the buttons always share one width and keep name + count apart. With this Story's content 1920 / 1440 / 1280 show
// every item (no More); narrower rows move collapseBelow items (Model / Layout included) into More by the measured steps.
check('Model / Layout field: 378px at 1920, at most 378px and narrower with less room', at(1920).field === 378 && report.every(item => item.field === null || item.field <= 378) && at(1440).field >= at(1280).field, JSON.stringify(report.map(item => [item.rowWidth, item.field])))
// 2026-10-23: the Model / Layout field widens with the free room up to 378px (1920: 378), narrower with less room at
// the same step (1920 → 1440, both with every label); the buttons always share one width and keep name + count apart.
// With this Story's content 1920 / 1440 / 1280 show every item (no More); narrower rows move collapseBelow items
// (Model / Layout included) into More by the measured steps.
check('Model / Layout field: 378px at 1920, at most 378px and narrower with less room (1920 → 1440)', at(1920).field === 378 && report.every(item => item.field === null || item.field <= 378) && at(1440).field <= at(1920).field, JSON.stringify(report.map(item => [item.rowWidth, item.field])))
// 1920 / 1440 (unchanged): every button with its label, none icon only.
check('1920 / 1440: 전체 보기 · 레이어 · 설정 · 속성 labelled, no icon-only button', [1920, 1440].every(width => !at(width).iconOnly.length && ['전체 보기', '레이어', '설정', '속성'].every(name => at(width).shownLabels.includes(name))), JSON.stringify([1920, 1440].map(width => [width, at(width).shownLabels, at(width).iconOnly])))
// 1280 (2026-10-23, user-approved): the status label keeps the width of its widest text (SWViewerStatus), so at 1280
// the measured icon step turns exactly the three panel toggles into icons; 전체 보기 keeps its label, nothing is in More.
check('1280: exactly 레이어 · 설정 · 속성 icon only, 전체 보기 labelled, no More', JSON.stringify(at(1280).iconOnly) === JSON.stringify(['레이어', '설정', '속성'].sort()) && at(1280).shownLabels.includes('전체 보기') && !['레이어', '설정', '속성'].some(name => at(1280).shownLabels.includes(name)) && !at(1280).more, JSON.stringify([at(1280).shownLabels, at(1280).iconOnly, at(1280).more]))
check('Model / Layout buttons equal, name and count never overlap', report.every(item => item.tabsEqual && item.tabsFit))
check('1920 / 1440 / 1280: every item in the header, no More', [1920, 1440, 1280].every(width => !at(width).more && at(width).field && ['전체 보기', '2.5D', '어두운 배경', '레이어'].every(name => at(width).labels.includes(name))))
// 950 sits at the edge of the measured steps (icons only or More, depending on the font rendering); both fit.
+12
View File
@@ -109,6 +109,18 @@
- 토큰·확인값이 있는 속성에 새 값을 만들지 않습니다. 색·크기·간격·radius·그림자는 각 패키지 `DESIGN_RULES.md`(SW 뷰어는 `VIEWER.md`도)의 토큰·확인값을 씁니다. 값이 없을 때만 위 ③으로 구성하고 "AI 구성 값"으로 보고합니다(추측한 값을 확정값처럼 쓰지 않음).
- 공통: Font Weight는 400 / 500 / 700만 씁니다(600 없음). Light/Dark 분기를 만들지 않습니다. 새 컴포넌트 CSS에 `html`·`body`·`*` 전역 규칙을 넣지 않습니다. 새 class는 소비 프로젝트 접두사(예: `app-`)를 씁니다.
- 요청에 없는 메뉴·통계·설명·다운로드·외부 API·AI 입력 UI를 추가하지 않습니다. 필요해 보이면 제안으로만 보고합니다.
- **디자인 적용 시 변경 범위 제한(2026-10-23, 모든 컴포넌트 — Header 포함).** Figma·디자인 시안 적용은 디자인 변경만 합니다. 일반 SW Header, SW Viewer Header, Popup·Panel, 그 밖의 HMDesign 컴포넌트와 HMDesign을 Git Submodule로 쓰는 소비 프로젝트에 모두 적용합니다.
- 바꾸지 않는 것: 실제 상태값(`selected`·`active`·`disabled`·`checked`·`expanded`·`hidden` 등), 메뉴의 기본 선택 상태(예: Model / Layout), Popup 열림·닫힘 상태, 스위치·체크박스 기본값, 입력값·숫자·개수 등 표시 데이터, Props와 그 기본값, 이벤트·클릭 동작, 조건부 렌더링, 기존 반응형 기능, 사용자 설정·상태 관리 로직, 요청받지 않은 다른 화면·컴포넌트.
- 바꿀 수 있는 것: 배경색·글자색·Border·Gradient·Shadow·Radius·Padding·Gap, 너비·높이·정렬·레이아웃, 기존 상태에 대응하는 시각 스타일. 선택 상태의 모양을 바꿔도 어떤 항목이 선택되어 있는지는 바꾸지 않습니다.
- Figma에 특정 상태가 그려져 있어도 컴포넌트의 기본 상태를 바꾸지 않습니다. 디자인 비교는 검증용 Story나 테스트 데이터에서 하고, Storybook 예시값을 소비 프로젝트의 기본값으로 복사하지 않습니다. 상태·기능 변경이 꼭 필요해 보이면 적용하지 말고 별도로 보고합니다.
- 작업 후 Git Diff로 요청 범위 밖 변경이 없는지 확인합니다.
- **Git Submodule 소비 프로젝트의 콘텐츠 생성 제한(2026-10-23, Header 제외).** HMDesign을 Git Submodule로 사용하는 소비 프로젝트의 UI 컴포넌트·화면에 적용합니다. **제외:** 일반 SW Header(`SWHeader`), SW Viewer Header(`SWViewerHeader`)와 그 밖의 모든 Header 관련 컴포넌트·영역 — 텍스트는 기존 Header 전용 AI 지침을 따르되, 위 "디자인 적용 시 변경 범위 제한"(상태·기능 변경 금지)은 Header에도 그대로 적용합니다.
1. 사용자가 요청하지 않은 설명·안내 문구·제목·라벨·툴팁을 임의로 추가하지 않습니다.
2. Storybook의 샘플 텍스트나 데이터를 실제 소비 프로젝트에 임의로 복사하지 않습니다.
3. Figma 또는 기존 컴포넌트에 없는 텍스트를 추측해서 생성하지 않습니다.
4. 기존 텍스트와 데이터를 임의로 변경하지 않습니다.
5. 필요한 콘텐츠가 불명확하면 임의로 확정하지 않고 결과 보고에 확인 대상으로 적습니다.
- **Git Submodule 사용 규칙(2026-10-23).** 소비 프로젝트에서 HMDesign을 사용할 때 요청 범위 밖은 바꾸지 않습니다. Submodule Commit 포인터는 사용자 승인 없이 바꾸지 않습니다. 소비 프로젝트의 기존 데이터와 기능을 우선 유지합니다.
### 없는 UI를 같은 스타일로 만들 때 확인할 항목
+2 -39
View File
@@ -1,7 +1,7 @@
import { useRef, useState, type CSSProperties, type ReactNode } from "react"
import { useRef, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import {
SWHeader, SWHeaderWindowControls, SWTheme, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
SWHeader, SWHeaderWindowControls, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
type SWViewerHeaderGroup, type SWViewerMenuEntry,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
@@ -175,43 +175,6 @@ export const States: Story = {
},
}
// 2026-10-23 Figma 7652:40344: the Model / Layout tabs alone, each selected state, Disabled and a color token override
// (CSS variables on SWTheme, as a consuming program sets them). Hover / Focus: point at or Tab to a button.
function TabsRow({ initial, disabled = false, style }: { initial: string; disabled?: boolean; style?: CSSProperties }) {
const [value, setValue] = useState(initial)
const groups: SWViewerHeaderGroup[] = [{
id: "space", label: "도면 공간", surface: "field", items: [
{ type: "tabs", id: "space", label: "모델과 배치 선택", value, disabled, onValueChange: setValue, options: [{ value: "model", label: "Model", count: 17377 }, { value: "layout", label: "Layout", count: 7377 }] },
],
}]
const header = <SWViewerHeader documentTitle="Model Layout.dwg" groups={groups} />
// Overrides go on the outermost SW scope (SWTheme), where the package declares the tokens.
return <div className="sw-story-scroller" data-tabs-case={initial + (disabled ? "-disabled" : style ? "-override" : "")}><div style={{ width: 1440 }}>{style ? <SWTheme style={style}>{header}</SWTheme> : header}</div></div>
}
const TAB_OVERRIDE = {
"--hm-sw-viewer-tabs-outline-color": "#4a7fd0",
"--hm-sw-viewer-tabs-glow-color": "rgba(74, 127, 208, 0.6)",
"--hm-sw-viewer-tab-gradient": "linear-gradient(180deg, #0b1830, #16305c 50%, #0b1830)",
"--hm-sw-viewer-tab-active-border": "linear-gradient(180deg, #b35f00, #ffb347 40%, #b35f00)",
"--hm-sw-viewer-tab-active-text": "#ffb347",
"--hm-sw-viewer-tab-inactive-border": "linear-gradient(#3d6bb3, #3d6bb3)",
"--hm-sw-viewer-tab-inactive-text": "#dbe7ff",
"--hm-sw-viewer-tab-highlight-color": "#5b8fe0",
} as CSSProperties
export const ModelLayoutTabs: Story = {
name: "Model / Layout",
render: () => (
<StoryPage>
<StoryPageHeader title="SW Viewer — Model / Layout" />
<StorySection title="Model 선택"><TabsRow initial="model" /></StorySection>
<StorySection title="Layout 선택"><TabsRow initial="layout" /></StorySection>
<StorySection title="Disabled"><TabsRow initial="model" disabled /></StorySection>
<StorySection title="색상 토큰 Override (--hm-sw-viewer-tab(s)-*)"><TabsRow initial="model" style={TAB_OVERRIDE} /></StorySection>
</StoryPage>
),
}
const MENU_ENTRIES: SWViewerMenuEntry[] = [
{ type: "label", id: "l-file", label: "파일" },
{ type: "item", id: "open", label: "파일 열기", icon: <FolderOpen size={16} />, shortcut: "Ctrl+O" },