fix(sw): align header logo styles and refine viewer UI
Deploy Storybook / deploy (push) Successful in 1m13s

This commit is contained in:
shkim4 committed 2026-10-23 09:55:13 +09:00
1 parent 1d206c86cf
commit 205864b8a0
15 files changed
+236 -96

No files matched your search

+1 -1
View File
@@ -548,7 +548,7 @@ Composed Controls의 기존 레이어·색상·상세보기 예제는 보존하
| 토큰 | 기본값 | 사용처 |
|---|---|---|
| `--hm-sw-header-height` / `-bar-height` / `-field-height` | 49px / 48px / 38px | 헤더 / 면 / 필드 |
| `--hm-sw-header-logo-width` / `-logo-height` | 32px / 17px | 로고 영역(공통 배치, 로고 이미지는 프로그램별) |
| `--hm-sw-header-logo-width` / `-logo-height` | `auto`(2026-10-23, 이전 32px) / 17px | 로고 영역(공통 배치, 로고 이미지는 프로그램별). 폭은 로고 원본 비율, 홈·사선 배경이 로고 폭을 따름 |
| `--hm-sw-header-gap` | 4px | 필드 간격 |
| `--hm-sw-header-font-family` / `-font-size` / `-label-size` | Noto Sans KR… / 14px / 12px | 전체 / 값 / 라벨 |
| `--hm-sw-header-background` | `#060606` | 헤더 바탕 |
+72
View File
@@ -0,0 +1,72 @@
# SW Viewer 설정창(SWViewerPanel) 영역 구분 스타일
> 작성 2026-10-23. **현재 구현 조사 문서**이며 새 규칙을 정하지 않습니다. 값은 아래 소스와 Storybook `SW/Viewer › Settings Popup`(`sw-viewer--content-variants`)의 Computed Style 실측(Chromium, 2026-10-23)으로 확인했습니다. 구현으로 확인되지 않거나 기준이 불명확한 부분은 §6 확인 필요 항목에만 적습니다.
>
> - 공통(패키지) 스타일: `packages/sw/src/theme/sw.css`(SWPopup 프레임 `.hm-sw-property*`), `packages/sw/src/theme/sw-viewer.css`(뷰어 패널·콘텐츠 `.hm-sw-viewer-*`)
> - 컴포넌트: `packages/sw/src/components/sw-viewer/SWViewerPanel.tsx`(프레임 = `SWPopup`), `SWViewerControls.tsx`(`SWViewerSection`, `SWViewerField`, `SWViewerInput`, `SWViewerBadge`, `SWViewerHint` 등)
> - Storybook 전용 스타일: `src/sw/sw-viewer-stories.css`(`.sw-viewer-story-*`), 콘텐츠 구성 `src/sw/sw-viewer-fixture.tsx`(`LayerPanel`, `PropertiesPanel`, `OptionsPanel`)
>
> 실측의 `0.666667px` 테두리는 기기 배율(DPR 1.5)에서 1px CSS 값이 그려진 결과입니다. 아래 표에는 CSS 선언값을 적습니다.
## 1. 구조 요약
```
SWViewerPanel (= SWPopup, .hm-sw-property.hm-sw-viewer-panel) ← 테두리 박스(프레임)
├─ .hm-sw-property__titlebar ← 배경(그라데이션)만
├─ .hm-sw-property__body ← 배경·간격만(본문 스크롤)
│ └─ children: SWViewerSection(card | plain) / SWViewerField / 목록 …
└─ footer → .hm-sw-viewer-panel__footer ← 위쪽 선
.hm-sw-viewer-panel__resize ← 모서리 Grip(빗금)
```
## 2. 선(Border/Divider)으로 구분되는 영역
| 영역 | 목적 | 선택자 | 실제 값 | 조건·예외 | 구분 | Storybook 예시 |
|---|---|---|---|---|---|---|
| Footer 위 구분선 | 스크롤 본문과 하단 버튼 영역 분리 | `.hm-sw-viewer-panel__footer` | `border-top: 1px solid var(--hm-sw-viewer-panel-inner-border)`(#948871), `padding: 7px`, `gap: 8px`, 오른쪽 정렬 | `footer` Prop이 있고 접힘(`collapsed`)이 아닐 때만 렌더링 | 공통 | `SW/Viewer › Viewer Header`의 `모듈 구성 · 라이선스 고지` modal(`AboutPanel`, `확인` 버튼). Settings Popup 3개에는 footer 없음 |
| 전체 레이어 행 아래 선 | 고정 행(검색·전체 레이어)과 레이어 목록 분리 | `.sw-viewer-story-layers .sw-viewer-story-all` | `border-bottom: 1px solid var(--hm-sw-property-table)`(#d2c8b4), `padding-bottom: 8px`(실측 `padding: 0 6px 8px 0`) | 레이어 Popup에만 해당. 검색과 전체 레이어 사이에는 선 없음 | **Storybook 전용** | Settings Popup › `레이어 (9개)` |
| 속성 행 사이 선 | 속성 키/값 행 구분 | `.sw-viewer-story-props > div` | `border-bottom: 1px solid var(--hm-sw-viewer-panel-inner-border)`(#948871), `padding: 6px 0`, `gap: 12px` | 각 그룹의 **마지막 행에도** 선이 있음(실측 9행 모두) | **Storybook 전용** | Settings Popup › `속성` |
## 3. 선이 표시되지 않는 영역
| 영역 | 목적 | 선택자 | 실제 값 | 조건·예외 | 구분 | Storybook 예시 |
|---|---|---|---|---|---|---|
| Plain Section | 패널 본문에 카드 없이 콘텐츠를 바로 배치 | `.hm-sw-viewer-section[data-surface="plain"]` | 전용 규칙 없음 → 기본 `.hm-sw-viewer-section`만 적용: `display: flex; flex-direction: column; gap: 12px`, 테두리·배경·padding 0 | `SWViewerSection surface="plain"`을 명시했을 때 | 공통 | 레이어 Popup의 목록 Section, 속성 Popup의 `일반 정보`·`기하 속성` |
| 제목줄 ↔ 본문 경계 | — | `.hm-sw-property__titlebar` / `.hm-sw-property__body` | 경계선 없음. 제목줄은 배경 그라데이션, 본문은 위쪽 inset 하이라이트(§5)로만 구분 | 모든 패널 | 공통(SWPopup) | Settings Popup 전체 |
| 검색 ↔ 전체 레이어 | 고정 행 두 개 | `.sw-viewer-story-toolbar` | 선 없음, `gap: 12px 4px` | 레이어 Popup | Storybook 전용 | `레이어 (9개)` |
| 레이어 목록 행 사이 | 레이어 항목 나열 | `.sw-viewer-story-list`, `li` | 선 없음, 목록 `gap: 4px`, 행 `padding: 4px 6px` | — | Storybook 전용 | `레이어 (9개)` |
| 카드 Section 사이 | 옵션 그룹 나열 | 본문 `.hm-sw-property__body` | 선 없음, 본문 `gap: 8px`로만 분리(각 카드는 자기 테두리를 가짐) | `surface="card"` 연속 배치 | 공통 | `옵션` |
## 4. 테두리가 있는 박스 영역
| 영역 | 목적 | 선택자 | Border | Background | Radius | Padding·기타 | 조건·예외 | 구분 | 예시 |
|---|---|---|---|---|---|---|---|---|---|
| 패널 프레임 | 팝업 전체 외곽 | `.hm-sw-property`(SWPopup) + `.hm-sw-viewer-panel` | `1px solid var(--hm-sw-property-border)`(#a67d03) + `box-shadow: inset 0 0 0 1px #000`(안쪽 검정 1px) | `var(--hm-sw-property-surface)`(#e4dbc9) | 4px | padding 0, `overflow: hidden` | 모든 SWViewerPanel. modal이면 뒤에 `.hm-sw-viewer-panel-backdrop`(`rgba(0,0,0,.1)`) | 공통(SWPopup 그대로) | Settings Popup 3개 |
| Card Section | 설정 그룹 묶음 | `.hm-sw-viewer-section[data-surface="card"]` | `1px solid var(--hm-sw-viewer-panel-inner-border)`(#948871) | `var(--hm-sw-viewer-panel-field-background)`(#f1ece1) | `var(--hm-sw-viewer-radius)` 4px | `padding: var(--hm-sw-viewer-panel-spacing)` 12px, `gap: 12px`, `box-shadow: inset 0 1px 0 #fff` | `SWViewerSection`의 **기본값**(`surface` 생략 시 card) | 공통 | `옵션`의 `선가중치`·`표시`·`2.5D` |
| 검색 입력 | 레이어 검색 | `.hm-sw-theme .hm-sw-viewer-input[type="search"]` | `1px solid var(--hm-sw-viewer-panel-inner-border)` | field-background #f1ece1 | 4px | 80px 기본 폭, 높이 28px, `padding: 0 10px` | `type="search"`만. 값 입력(§5)과 다름 | 공통 | `레이어 (9개)` |
| 패널 버튼 outline | 보조 버튼 | `.hm-sw-theme .hm-sw-viewer-button` | `1px solid var(--hm-sw-viewer-panel-primary)`(#123328) | #fff (`primary`는 #123328) | 4px | 높이 24px, `padding: 0 8px` | 제목줄의 `variant="ghost"`는 테두리·배경 없음(26 × 26) | 공통 | `모듈 구성` modal의 `확인`(primary). Settings Popup에는 ghost만 |
## 5. 배경색·간격만으로 구분되는 영역
| 영역 | 목적 | 선택자 | 실제 값 | 조건·예외 | 구분 | 예시 |
|---|---|---|---|---|---|---|
| 제목줄 | 제목·닫기·동작 버튼 | `.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 덮어쓰기) | 전체 |
| 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` |
| 레이어 행 Hover | 행 강조 | `.sw-viewer-story-list li:hover` | `background: rgba(154,146,132,.25)`, radius 3px | Hover일 때만. 숨긴 레이어는 `opacity: .45` | Storybook 전용 | `레이어 (9개)` |
| 속성 그룹 간격 | 두 속성 그룹 분리 | `.sw-viewer-story-props-next` | `margin-top: var(--hm-sw-viewer-panel-spacing)` 12px(본문 gap 8px과 합쳐 20px) | 두 번째 그룹 | Storybook 전용 | `속성` |
| Hint | 보조 설명 | `.hm-sw-viewer-hint` | 선·배경 없음, 12px | — | 공통 | `옵션` |
| 빈 상태 | 선택 없음 안내 | `.sw-viewer-story-empty` | 선·배경 없음, `padding: 40px 0`, 가운데 정렬 | `PropertiesPanel selected={false}` | Storybook 전용 | `SW/Viewer › States` 등 |
| Resize Grip | 크기 조절 | `.hm-sw-viewer-panel__resize` | 14 × 14, inner-border 색 빗금 그라데이션(선 아님) | `resizable` 이고 접힘이 아닐 때 | 공통 | 전체 |
## 6. 확인 필요 항목
1. **Card가 기본값인 이유:** `SWViewerSection`의 기본 `surface`는 `card`입니다. 그러나 레이어·속성 Popup은 모두 `plain`을 명시하고, card는 `옵션` Popup만 씁니다. 어느 콘텐츠에 card/plain을 쓰는지에 대한 디자인 기준 문서는 확인되지 않았습니다.
2. **레이어 구분선 위치:** "전체 레이어 행 아래 `#d2c8b4` 1px 선, 검색과 사이에는 없음" 규칙은 `packages/sw/ai/VIEWER.md`에 소비 앱 규칙으로 적혀 있습니다. 실제 구현은 Storybook/예제의 소비 앱 CSS뿐이고 패키지 공통 스타일은 아닙니다.
3. **구분선 색 두 가지:** 레이어 구분선은 `--hm-sw-property-table`(#d2c8b4)이고, 속성 행 선·Footer 선·Card 테두리는 `--hm-sw-viewer-panel-inner-border`(#948871)입니다. 용도별로 색을 나누는 기준은 문서화되어 있지 않습니다.
4. **속성 행 마지막 선:** 각 그룹의 마지막 행에도 아래 선이 그려집니다(Storybook 전용 CSS). 의도인지 확인되지 않았습니다.
5. **Footer 예시 부족:** Footer 구분선은 대표 Settings Popup에 없습니다. `Viewer Header` Story의 modal에서만 보이며, 비-modal 패널 Footer의 디자인 기준 화면은 확인되지 않았습니다.
6. **입력 박스 두 형태:** 검색 입력은 항상 테두리 박스이고, 값 입력은 Hover/편집 때만 테두리가 생깁니다(2026-10-22 사용자 요청 주석). 다른 텍스트 입력(예: 검색이 아닌 일반 text)에 어느 쪽을 쓸지는 `type="search"` 여부로만 갈립니다.
7. **Card 안쪽 하이라이트:** `box-shadow: inset 0 1px 0 #fff`는 하드코드 값이고 대응 토큰이 없습니다.
+3 -3
View File
@@ -22,7 +22,7 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체
| 필수 | `documentTitle: string` |
| 구성 | `hiddenControls?: SWHeaderHideableControlId[]`(로고 외 부품 제거, `home` 제거 시 정적 로고. 하위 항목 `documentNew`·`documentOpen`·`layerFreeze`·`layerLock`도 제거 — 레이아웃 변화 없음), `menuOrder?: SWHeaderMenuId[]`(5개 메뉴끼리 순서), `controls?: SWHeaderControlConfig[]`(객체 영역 전체 allowlist·순서·label) |
| 로고 오른쪽 | `documentOpenPlacement?: "menu" \| "header"`(기본 `menu` = 기존 Dropdown 하단. `header` = 로고 바로 오른쪽에 파일명과 같은 면의 Open 버튼, Dropdown의 Open은 숨김. 둘 다 `onDocumentOpen`·`documentOpenLabel`), `leading?: ReactNode`(Open 뒤·파일명 앞 추가 부품, 예: `SWToolButton appearance="document"`) |
| 로고 | `logo?: ReactNode`, `logoSrc?: string`, `logoAlt?: string`, `onHomeClick`(`homeMenu`가 없을 때만 호출) |
| 로고 | `logo?: ReactNode`, `logoSrc?: string`, `logoAlt?: string`, `onHomeClick`(`homeMenu`가 없을 때만 호출). 로고는 높이 17px·원본 비율이고 홈과 사선 배경이 로고 폭을 따라 늘거나 줍니다. 로고가 없어도 홈과 사선 배경, `onHomeClick`·`homeMenu` 동작은 그대로입니다(빈 로고 자리 32px을 유지해 80 × 48, 2026-10-23). `SWViewerHeader`는 로고가 없으면 로고 면 전체를 그리지 않습니다(이때 `onHomeClick`·`logoMenu`는 누를 곳이 없음) |
| 로고 클릭 메뉴 | `homeMenu?: SWHeaderHomeMenu`(= `SWHomeMenu` Props에서 열림 상태 제외, 5절), `homeMenuOpen`, `defaultHomeMenuOpen`(기본 false), `onHomeMenuOpenChange(open, reason: SWHeaderHomeMenuChangeReason)` |
| 파일 | `documents`, `activeDocumentId`, `documentMenuOpen`, `onDocumentMenuOpenChange`, `onDocumentSelect`, `onDocumentClose`, `onDocumentNew`, `onDocumentOpen`, `onDocumentClick`, `documentIcon`, `documentNewLabel`, `documentOpenLabel` |
| 레이어 | `layer`, `layers`, `activeLayerId`, `layerMenuOpen`, `onLayerMenuOpenChange`, `onLayerSelect`, `onLayerVisibilityChange`, `onLayerFreezeChange`, `onLayerLockChange`, `onLayerDetail`, `onLayerOpen` |
@@ -47,7 +47,7 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체
| 컴포넌트 | 용도 | 주요 Props / Callback | 크기(Figma) | 상태 | 검증 |
|---|---|---|---|---|---|
| `SWTheme` | 단독 사용 범위(토큰·글꼴·초기화). 배치는 정하지 않음 | `div` HTML 속성, `className`, `style`(토큰 override — 가장 바깥 SW 범위에서 지정하면 안쪽 Header·Dropdown·More·Panel·명령창이 상속, [AI_USAGE.md](./AI_USAGE.md) §3.1) | — | — | ✅ |
| `SWHeaderHome` | 로고 슬롯 + 홈 버튼 | `logo`, `logoSrc`, `logoAlt`, `onClick`, `interactive`(false = 정적 로고) | 80 × 48, 로고 슬롯 32 × 17, 오른쪽 −17px로 다음 파일 면과 겹침 | Hover 배경 교체, Focus | ✅ |
| `SWHeaderHome` | 로고 슬롯 + 홈 버튼 | `logo`, `logoSrc`, `logoAlt`, `onClick`, `interactive`(false = 정적 로고) | 12 + 로고 폭 + 36 × 48(예시 로고 32 × 17에서 80 × 48, 로고 없음도 80 × 48), 오른쪽 −17px로 다음 파일 면과 겹침 | Hover 배경 교체, Focus | ✅ |
| `SWHeaderDocumentTitle` | 파일명 + 문서 Dropdown(+ New / Open) | `title`(필수), `documents`, `activeDocumentId`, `menuOpen`, `onMenuOpenChange`, `onDocumentSelect`, `onDocumentClose`, `onNew`, `onOpen`, `onClick`, `documentIcon`, `newLabel`, `openLabel`, `label`, `showNew` / `showOpen`(기본 true, 둘 다 false면 하단 없음) | 높이 48, 폭 190–310(반응형 clamp), 메뉴 310 | 텍스트만 Hover #FFC600, 열림 화살표 회전 | ✅ |
| `SWHeaderLayerMenu` | 레이어 필드 + Dropdown | `value`(필수), `layers`(필수), `activeLayerId`, `open`, `onOpenChange`, `onClick`, `selected`, `disabled`, `onLayerSelect`, `onVisibilityChange`, `onFreezeChange`, `onLockChange`, `onDetail`, `label`, `showFreeze` / `showLock`(기본 true. false = 그 상태를 그리지 않고 20px 자리만 유지) | 필드 234 × 38, 메뉴 234 | Hover 링, Selected, Expanded, 행 Hover | ✅ |
| `SWHeaderColorMenu` | 색상 필드 + Dropdown(ByLayer/ByBlock, 팔레트, 사용자 색, 추출, RGB 입력) | `value`, `colors`(필수), `userColors`, `mode`, `open`, `onOpenChange`, `onClick`, `selected`, `disabled`, `onColorSelect`, `onModeChange`, `onColorInput`, `onPick`, `onDetail`, `label` | 필드 192 × 38 | 칩 선택 시 열림 유지 | ✅ |
@@ -58,7 +58,7 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체
| `SWHeaderActions` | 인쇄·K 버튼 pill | `onPrint`, `onPopupGuide`, `disabled`, `showPrint`, `showPopupGuide`(하나만 남기면 pill이 축소) | pill 85 × 42(버튼 40 × 38) | 아이콘 Hover, Hover 링, K 가이드 미리보기(반복 Motion, Reduced Motion 정적) | ✅ |
| `SWHeaderWindowControls` | 창 이동·최소화·종료 | `onWindowMoveStart`, `onMinimize`, `onClose`, `showWindowMove`, `showMinimize`, `showClose` | 이동 88 × 48, 버튼 32 × 48 | 창 이동 Hover Motion 1.4s, 종료 Hover 자산 | ◻ |
| `SWHeaderProperties` | 상세보기 / 속성Bar · 속성창 토글 | `barOpen`, `panelOpen`, `panelId`, `disabled`, `onBarToggle`, `onPanelToggle`, `showCaption`, `showBar`, `showPanel` | 157 × 38 | 텍스트 Hover #FFC600 | ◻ |
| `SWHeaderPropertyBar` | 28px 속성 편집 띠 | `selection`, `properties`, `onPropertyChange`, `readOnly`, `disabled`, `div` 속성 | 높이 28, 콘텐츠 폭 | Input 상태 | ◻ |
| `SWHeaderPropertyBar` | 28px 속성 편집 띠 | `selection`, `properties`, `onPropertyChange`, `readOnly`, `disabled`, `div` 속성 | 높이 28(폭이 부족하면 항목이 다음 줄로 넘어가며 27px씩 늘어남, 가로 스크롤 없음, 2026-10-23), 콘텐츠 폭(`SWHeader` 안에서는 오른쪽 끝이 최소 속성Bar·속성창 구분선까지, 토글이 More에 있으면 콘텐츠 폭, 2026-10-23) | Input 상태 | ◻ |
| `SWHeaderPropertyPanel` | 데이터 기반 속성창(필드 타입 6종 `text`·`number`·`select`·`color`·`slider`·`computed` + picker, 그룹 레이아웃 `rows`·`circle`·`position`, 형상 Slot) | `selection`·`properties`·`groups`(0.2.0부터 선택), `title`, `children`(그룹 뒤 자유 콘텐츠), `autoFocus`, `onPropertyChange`, `menus`(`showLayerFreeze`/`showLayerLock` 포함), `expandedGroupIds`/`defaultExpandedGroupIds`/`onExpandedGroupsChange`, `toolActive`/`onToolActiveChange`, `toolLabel`, `onOverviewClick`, `shapeContent`, `onClose`, `id`, `anchorRef`, `style` | 314 폭, 높이는 콘텐츠(최대 화면, 본문 스크롤). `anchorRef`가 있으면 고정 위치 Overlay, 없으면 문서 흐름(위치는 `style`) | 4가지 선택 상태 | ✅ |
| `SWHeaderInput` | 수정 가능한 SW Input | `value`(필수), `onValueChange`, `onCommit`(Enter·Blur, 유효 + 변경 시), `onCancel`(Escape 복원), `valueType`(`text`/`number`), `parse`, `readOnly`, `disabled`, `commitOnUnmount`, `input` 속성 | 58 × 20 기준, Hover/편집 24px | Default/Hover/편집/ReadOnly/Disabled, IME 조합 중 Enter 무시 | ✅ |
| `parseSWHeaderColor` · `parseSWHeaderNumber` | Input 검증 도우미 | `r,g,b` / `#rrggbb`, 숫자(`min`, `max`) | — | — | ◻ |
+2 -2
View File
@@ -16,7 +16,7 @@
| 면 사이 간격 | 2px | (Header bar `gap`) |
| 필드 높이 | 38px(면 안 위아래 5px) | `--hm-sw-header-field-height` |
| 필드 사이 간격 | 4px | `--hm-sw-header-gap` |
| 로고 슬롯 | 32 × 17 (홈 80 × 48 안 12, 16) | `--hm-sw-header-logo-width` / `-height` |
| 로고 슬롯 | 높이 17, 폭은 로고 원본 비율(`-width` 기본 `auto`, Figma 예시 32 × 17). 홈 = 왼쪽 12 + 로고 + 36(사선), 로고는 그 안 콘텐츠 영역 가로·세로 가운데. 사선 배경(Figma 68 × 48)은 로고 폭만큼 늘거나 줄며 사선 모양·Hover는 그대로(9-slice). 로고가 없어도 홈과 사선 배경·동작은 그대로(빈 로고 자리 32px 유지, 80 × 48 · 사선 68 × 48, 파일명 위치 동일). `SWViewerHeader`는 로고가 없으면 로고 면 전체 없음(2026-10-23) | `--hm-sw-header-logo-width` / `-height` |
| 필드 폭 | 레이어 234, 색상 192, 선스타일 284, 문자·치수스타일 164 | — |
| 상세보기 / 창 영역 | 157 × 38 / 240 × 48(pill 85 × 42, 창 이동 88, 버튼 32 × 2) | — |
| 필드 내부 | padding 0 10px 0 11px, 텍스트 사이 12px | — |
@@ -142,7 +142,7 @@
| 영역 | 토큰 = 기본값 |
|---|---|
| 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 |
| 로고 면 | `--hm-sw-viewer-brand-width` 72px, `--hm-sw-viewer-brand-empty-width` 24px(로고 없음), `--hm-sw-viewer-brand-top` #123328, `--hm-sw-viewer-brand-middle` #205241, `--hm-sw-viewer-brand-bottom` #071e18 |
| 로고 면 | 2026-10-23부터 `SWHeader` 홈과 같은 사선 배경(Figma 에셋 9-slice)·같은 폭 계산(왼쪽 12 + 로고 + 36, 다음 부품과 17px 겹침, 예시 로고에서 79px). `--hm-sw-viewer-brand-width` 기본 `auto`. 로고 다음 첫 부품(파일 열기·파일명) 시작 여백 5px(`SWHeader`와 같음). 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-font-size` 14px, `--hm-sw-viewer-label-size` 11px, `--hm-sw-viewer-icon-size` 16px, `--hm-sw-viewer-disabled-opacity` 0.6 |
| 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 |
+3 -3
View File
@@ -37,9 +37,9 @@
| `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 24px`(패키지 CSS 고정값). 공간이 부족하면 아이콘만(라벨은 Tooltip·접근성 이름) 표시하고 More로 옮기지 않습니다. 실행 후 `onCommand({ id: "open", itemId: "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 5px`(로고 다음, `SWHeader`와 같음) · 로고가 없으면 `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” 문자·기본 아이콘 없음, 로고 면은 사선 끝 24px만). 주면 72px 면(로고가 더 넓으면 그만큼), 높이 최대 24px |
| `logo?`, `logoSrc?`, `logoAlt?`, `onHomeClick?` | `SWHeader`와 같은 로고 Props. **주지 않으면 아무것도 그리지 않습니다**(“HM” 문자·기본 아이콘 없음. 2026-10-23부터 로고 면도 그리지 않아 폭·여백이 0이고 파일 열기 또는 파일명이 Header 왼쪽 끝에서 12px 안쪽으로 시작합니다. 이때 `onHomeClick`·`logoMenu`는 누를 곳이 없습니다). 로고를 주면 `SWHeader`와 같은 내부 로고 부품과 같은 사선 배경(Figma 홈 에셋)·폭 계산을 씁니다: 높이 17px·원본 비율, 왼쪽 12 + 로고 + 36(다음 부품과 17px 겹침, 예시 로고 79px), 로고는 가로·세로 가운데, 다음 부품 시작 여백 5px. 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` | |
@@ -153,6 +153,6 @@ const groups: SWViewerHeaderGroup[] = [
## 7. 디자인 변경 시 주의
- 크기(52px Header, 34px 버튼, 40px 토글·탭·스위치 칸, 41~42px field 면, 72px 로고 면)와 색은 참고 뷰어 실측값입니다. 파일명·객체 수정·속성 면 배경은 SWHeader와 같은 사용자 확정 토큰(`--hm-sw-header-document-*`, `object-*`, `property-*`)을 공유하므로 바꾸면 두 Header가 함께 바뀝니다.
- 크기(52px Header, 34px 버튼, 40px 토글·탭·스위치 칸, 41~42px field 면)와 색은 참고 뷰어 실측값이고, 로고 면 64px는 2026-10-23 `SWHeader` 로고 기준입니다. 파일명·객체 수정·속성 면 배경은 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 닫기 추가, 로고 기본 표시 없음(사용자 결정).
@@ -254,6 +254,13 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
const [headerWidth, setHeaderWidth] = useState(1920)
const [, setAreaWidth] = useState(0)
const [barOffset, setBarOffset] = useState(0)
// 2026-10-23: the property bar strip reaches at least the 속성Bar | 속성창 divider (the 속성Bar toggle when the
// divider is not shown), measured like barOffset; null while the toggle is not in the header row (e.g. in More).
const [barEnd, setBarEnd] = useState<number | null>(null)
const measureBarEnd = (header: Element) => {
const edge = header.querySelector(".hm-sw-header__properties-divider") ?? header.querySelector(".hm-sw-header__properties .hm-sw-header__toggle:not([data-property-panel-trigger])")
setBarEnd(edge?.getClientRects().length ? edge.getBoundingClientRect().right - header.getBoundingClientRect().left : null)
}
// Open next to the logo (documentOpenPlacement="header") moves into More when the measured header has no room.
const openInHeader = documentOpenPlacement === "header" && shown("documentOpen")
const leadingRef = useRef<HTMLDivElement>(null)
@@ -270,6 +277,7 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
// The object area can change alone (leading content, file name, window area): it re-runs the measured More.
setAreaWidth(panelAnchorRef.current?.getBoundingClientRect().width ?? 0)
setBarOffset((panelAnchorRef.current?.getBoundingClientRect().left ?? bounds.left) - bounds.left)
measureBarEnd(header)
}
const observer = new ResizeObserver(update)
update()
@@ -277,6 +285,11 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
if (panelAnchorRef.current) observer.observe(panelAnchorRef.current)
return () => observer.disconnect()
}, [])
// The divider also moves when the measured More or the fields change inside the same object-area width.
useLayoutEffect(() => {
const header = fieldsRef.current?.closest(".hm-sw-header")
if (barOpen && header) measureBarEnd(header)
})
// Space for the parts right of the logo, measured: the file name gives way first (its CSS clamp subtracts
// --hm-sw-header-leading-width). Open and the other parts move into More by measurement (see "Measured More").
useLayoutEffect(() => {
@@ -729,7 +742,7 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
<SWHeaderWindowControls onWindowMoveStart={onWindowMoveStart} onMinimize={onMinimize} onClose={onClose} showWindowMove={shown("windowMove")} showMinimize={shown("minimize")} showClose={shown("close")} />
</div>}
</div>
{barOpen && barData && <div className="hm-sw-header__bar-anchor" style={{ left: barOffset, maxWidth: `calc(100% - ${barOffset}px)` }}><SWHeaderPropertyBar {...barData} disabled={disabled || propertyBar?.disabled} id={barId} /></div>}
{barOpen && barData && <div className="hm-sw-header__bar-anchor" style={{ left: barOffset, maxWidth: `calc(100% - ${barOffset}px)` }}><SWHeaderPropertyBar {...barData} disabled={disabled || propertyBar?.disabled} id={barId} style={barEnd !== null && barEnd > barOffset ? { minWidth: barEnd - barOffset } : undefined} /></div>}
{homeMenu && shown("home") && (
<div className="hm-sw-header__home-menu" style={{ left: homeBox.left, maxWidth: homeBox.width }}>
<SWHomeMenu {...homeMenu} id={homeMenu.id ?? homeMenuId} open={homeOpen} returnFocusRef={homeButtonRef}
@@ -1,10 +1,8 @@
import type { ReactNode } from "react"
import homeBackground from "../../../assets/home-bg.svg"
import homeBackgroundHover from "../../../assets/home-bg-hover.svg"
export interface SWHeaderHomeProps {
onClick?: () => void
/** Logo image URL, fitted (contain) into the 32 x 17 logo slot. */
/** Logo image URL, 17px high at its own ratio (the 32 x 17 example is not forced; 2026-10-23). */
logoSrc?: string
/** Logo element (inline SVG, <img>, ...). Takes precedence over `logoSrc`. */
logo?: ReactNode
@@ -21,22 +19,32 @@ export interface SWHeaderHomeProps {
}
/**
* Home button. The slanted background and the logo slot position are the common SW design;
* the logo itself belongs to each SW program, so no logo is rendered unless one is passed.
* The SW logo slot (sw.css `.hm-sw-header__logo`: 17px high, as wide as the logo at its own ratio): `logo` first,
* else an <img> of `logoSrc`, else nothing. Shared by SWHeaderHome and SWViewerHeader (2026-10-23) so both render the
* logo the same way; `className` adds the host's placement. Internal: not part of the package exports.
*/
export function SWHeaderLogo({ logo, logoSrc, className }: Pick<SWHeaderHomeProps, "logo" | "logoSrc"> & { className?: string }) {
const content = logo ?? (logoSrc ? <img src={logoSrc} alt="" /> : null)
return content ? <span className={`hm-sw-header__logo${className ? ` ${className}` : ""}`} aria-hidden="true">{content}</span> : null
}
/**
* Home button. The slanted background (Figma header/btn_home 26:10655, home-bg / home-bg-hover assets drawn as a
* 9-slice by sw.css, 2026-10-23) and the logo padding are the common SW design; the home is as wide as the logo
* needs. The logo itself belongs to each SW program, so no logo is rendered unless one is passed.
*/
export function SWHeaderHome({ onClick, logoSrc, logo, logoAlt, interactive = true, "aria-expanded": expanded, "aria-controls": controls }: SWHeaderHomeProps) {
const content = logo ?? (logoSrc ? <img src={logoSrc} alt="" /> : null)
if (!interactive) return (
<div className="hm-sw-header__home" data-static="" role={logoAlt ? "img" : undefined} aria-label={logoAlt}>
<img className="hm-sw-header__home-bg" src={homeBackground} alt="" aria-hidden="true" />
{content && <span className="hm-sw-header__logo" aria-hidden="true">{content}</span>}
<span className="hm-sw-header__home-bg" aria-hidden="true" />
<SWHeaderLogo logo={logo} logoSrc={logoSrc} />
</div>
)
return (
<button type="button" className="hm-sw-header__home" aria-label={logoAlt ? `${logoAlt} 홈` : "홈"} aria-expanded={expanded} aria-controls={controls} onClick={onClick}>
<img className="hm-sw-header__home-bg" src={homeBackground} alt="" aria-hidden="true" />
<img className="hm-sw-header__home-bg-hover" src={homeBackgroundHover} alt="" aria-hidden="true" />
{content && <span className="hm-sw-header__logo" aria-hidden="true">{content}</span>}
<span className="hm-sw-header__home-bg" aria-hidden="true" />
<span className="hm-sw-header__home-bg-hover" aria-hidden="true" />
<SWHeaderLogo logo={logo} logoSrc={logoSrc} />
</button>
)
}
@@ -1,6 +1,7 @@
import { forwardRef, useEffect, useId, useLayoutEffect, useRef, useState, type ButtonHTMLAttributes, type HTMLAttributes, type KeyboardEvent, type ReactNode } from "react"
import { moveRovingFocus } from "../shared/roving-focus"
import { SWHeaderDocumentTitle } from "../sw-header/components/SWHeaderDocumentTitle"
import { SWHeaderLogo } from "../sw-header/components/SWHeaderHome"
import { SWHeaderMore } from "../sw-header/components/SWHeaderMore"
import type { SWHeaderDocument } from "../sw-header/types"
import { SWToolButton } from "../sw-tool/SWTool"
@@ -12,9 +13,10 @@ import "../../theme/sw-viewer.css"
export interface SWViewerHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
/**
* Logo image URL of the program (same prop as SWHeader), max 24px high on the slanted green cap.
* The logo belongs to each program: nothing is rendered when omitted (no default text or icon) and the cap
* shrinks to its slanted edge.
* Logo image URL of the program (same prop as SWHeader), contained in the SWHeader 32 x 17 logo slot, 12px from
* the header's left edge and vertically centered on the slanted green cap.
* The logo belongs to each program: nothing is rendered when omitted (no default text or icon) and there is no cap
* either (2026-10-23, also with `onHomeClick` / `logoMenu`, which then have no trigger).
*/
logoSrc?: string
/** Logo element (inline SVG, <img>, ...). Takes precedence over `logoSrc` (same prop as SWHeader). */
@@ -296,7 +298,6 @@ export const SWViewerHeader = forwardRef<HTMLElement, SWViewerHeaderProps>(funct
const rootRef = useRef<HTMLElement | null>(null)
// Header width: a resize re-renders, and the layout pass below measures again.
const [, setWidth] = useState(1920)
const gradientId = `hm-sw-viewer-brand-${useId().replace(/:/g, "")}`
useLayoutEffect(() => {
const root = rootRef.current
if (!root) return
@@ -374,27 +375,21 @@ export const SWViewerHeader = forwardRef<HTMLElement, SWViewerHeaderProps>(funct
)
}
const name = documentTitle || documentPlaceholder
const mark = logo ?? (logoSrc ? <img src={logoSrc} alt="" /> : null)
// Same test as SWHeaderLogo (logo, else logoSrc). 2026-10-23: no logo => no cap at all (no slant, no empty box, no
// width), even with logoMenu / onHomeClick: both have no trigger then; the file part starts the row (sw-viewer.css).
const hasLogo = !!(logo ?? logoSrc)
// With a logo the cap is the SWHeader home shape (Figma header/btn_home: the shared 9-slice .hm-sw-header__home-bg)
// around the shared logo; no hover layer: the viewer keeps its own focus-only look.
const brandContent = (
<>
<svg className="hm-sw-viewer-header__brand-shape" viewBox="0 0 88 52" preserveAspectRatio="xMaxYMid slice" aria-hidden="true" focusable="false">
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--hm-sw-viewer-brand-top)" />
<stop offset="45%" stopColor="var(--hm-sw-viewer-brand-middle)" />
<stop offset="100%" stopColor="var(--hm-sw-viewer-brand-bottom)" />
</linearGradient>
</defs>
<path d="M 0 0 H 88 L 73 44 Q 70 52 62 52 H 0 Z" fill={`url(#${gradientId})`} />
<path d="M 88 0 L 73 44 Q 70 52 62 52 H 0" fill="none" stroke="var(--hm-sw-viewer-brand-middle)" vectorEffect="non-scaling-stroke" />
</svg>
{mark && <span className="hm-sw-viewer-header__brand-mark" aria-hidden="true">{mark}</span>}
<span className="hm-sw-header__home-bg" aria-hidden="true" />
<SWHeaderLogo logo={logo} logoSrc={logoSrc} className="hm-sw-viewer-header__brand-mark" />
</>
)
return (
<header ref={node => { rootRef.current = node; if (typeof forwardedRef === "function") forwardedRef(node); else if (forwardedRef) forwardedRef.current = node }}
className={`hm-sw-theme hm-sw-viewer-header${className ? ` ${className}` : ""}`} {...rest}>
{logoItems.length > 0
{hasLogo && (logoItems.length > 0
? <SWHeaderMore className="hm-sw-viewer-header__logo-menu" align="start" title={logoMenu?.title} label={logoMenu?.title ?? (logoAlt ? `${logoAlt} 메뉴` : "메뉴")}
open={openMenu === "logo"} onOpenChange={menuOpenChange("logo")}
items={logoItems.map(item => ({
@@ -402,12 +397,12 @@ export const SWViewerHeader = forwardRef<HTMLElement, SWViewerHeaderProps>(funct
onSelect: () => { item.onSelect?.(); command({ id: item.id, itemId: item.id }) },
}))}
renderTrigger={({ ref, ...trigger }) => (
<button ref={ref} type="button" className="hm-sw-viewer-header__brand" data-logo={mark ? "" : undefined} data-state={trigger["aria-expanded"] ? "open" : undefined}
<button ref={ref} type="button" className="hm-sw-viewer-header__brand" data-logo="" data-state={trigger["aria-expanded"] ? "open" : undefined}
aria-label={logoAlt ? `${logoAlt} 메뉴` : "메뉴"} title={logoAlt} {...trigger}>{brandContent}</button>
)} />
: onHomeClick
? <button type="button" className="hm-sw-viewer-header__brand" data-logo={mark ? "" : undefined} aria-label={logoAlt ? `${logoAlt} 홈` : "홈"} title={logoAlt} onClick={onHomeClick}>{brandContent}</button>
: <div className="hm-sw-viewer-header__brand" data-logo={mark ? "" : undefined} role={logoAlt ? "img" : undefined} aria-label={logoAlt}>{brandContent}</div>}
? <button type="button" className="hm-sw-viewer-header__brand" data-logo="" aria-label={logoAlt ? `${logoAlt} 홈` : "홈"} title={logoAlt} onClick={onHomeClick}>{brandContent}</button>
: <div className="hm-sw-viewer-header__brand" data-logo="" role={logoAlt ? "img" : undefined} aria-label={logoAlt}>{brandContent}</div>)}
{onDocumentOpen && (() => {
const label = documentOpenLabel ?? "Open", iconOnly = compact.has(OPEN_ID)
return (
+28 -8
View File
@@ -9,8 +9,11 @@
/* 2026-10-21 user request: the solid color of the Storybook viewer canvas (--sw-story-canvas base #0a0a0b), no gradient. */
--hm-sw-viewer-background: #0a0a0b;
--hm-sw-viewer-border-color: #071e18;
--hm-sw-viewer-brand-width: 72px;
/* No logo: no text or icon is drawn; the cap keeps only its slanted edge (no wide empty space). */
/* 2026-10-23: auto = the SWHeader home box (12px incl. the header border + logo + 36px): 79px with the 32 x 17
example logo, wider or narrower with the logo. */
--hm-sw-viewer-brand-width: auto;
/* These four are unused since 2026-10-23 (the cap is the SWHeader home shape; no cap without a logo); kept so
existing themes that set them stay valid. */
--hm-sw-viewer-brand-empty-width: 24px;
--hm-sw-viewer-brand-top: #123328;
--hm-sw-viewer-brand-middle: #205241;
@@ -100,12 +103,23 @@
margin-right: -2px;
font-weight: 700;
}
/* With a logo: the reference 72px cap (logo at 24px), wider only for a wider logo. Without: the slanted edge only. */
.hm-sw-viewer-header__brand[data-logo] { width: auto; min-width: var(--hm-sw-viewer-brand-width); padding: 0 25px 0 24px; }
/* With a logo (2026-10-23): the SWHeader home — the same shape (shared .hm-sw-header__home-bg 9-slice of the Figma
asset: gradient, stroke, shadow, slant), the same box (12px + logo + 36px, 11px here + the 1px header border; the
logo centered) and the same 17px overlap of the next part, so the logo and the file-name area sit where SWHeader
puts them. No hover layer: the viewer cap keeps its own (focus only) look. Without a logo there is no cap. */
.hm-sw-viewer-header__brand[data-logo] {
justify-content: center;
box-sizing: border-box;
width: var(--hm-sw-viewer-brand-width);
padding: 0 36px 0 11px;
margin-right: -17px;
overflow: hidden;
/* The 17px overlap must not take the clicks of the next part (Open icon / file name): only the surface behind the
shape (::before, up to where the next part starts) is the cap's hit area. Focus and keyboard are unchanged. */
pointer-events: none;
}
.hm-sw-viewer-header__brand[data-logo]::before { pointer-events: auto; }
button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-header__brand-shape { position: absolute; inset: 0 auto 0 0; width: calc(100% + 16px); height: 100%; pointer-events: none; }
.hm-sw-viewer-header__brand-mark { position: relative; display: inline-flex; align-items: center; }
.hm-sw-viewer-header__brand-mark > :is(img, svg) { display: block; max-height: 24px; width: auto; }
.hm-sw-viewer-header__document {
display: flex;
@@ -209,7 +223,13 @@ button.hm-sw-viewer-header__brand { text-align: left; }
/* No file open button before it: the file name starts 10px after the slanted logo edge (at mid height), as in
SWHeader (text 10px after its home slant; measured 10.5px here with 18px). */
.hm-sw-viewer-header__document[data-list] .hm-sw-header__document-button { padding: 0 16px 0 18px; }
/* After the file open button (2026-10-21 user decision): 8px in. */
/* Right after the logo cap (2026-10-23, the SWHeader home shape and 17px overlap): the first part keeps SWHeader's 5px
start (was 24px / 18px for the former viewer slant, which doubled the space after the logo). */
:is(.hm-sw-viewer-header__brand, .hm-sw-viewer-header__logo-menu) + .hm-sw-header__document[data-tool] > .hm-sw-viewer-header__open,
:is(.hm-sw-viewer-header__brand, .hm-sw-viewer-header__logo-menu) + .hm-sw-viewer-header__document[data-list] .hm-sw-header__document-button { padding-left: 5px; }
/* No logo (2026-10-23): no cap, so the first part (Open or the file name) starts the row 12px in, like SWHeader. */
.hm-sw-viewer-header > .hm-sw-header__document[data-tool]:first-child > .hm-sw-viewer-header__open,
.hm-sw-viewer-header > .hm-sw-viewer-header__document[data-list]:first-child .hm-sw-header__document-button { padding-left: 12px; }/* After the file open button (2026-10-21 user decision): 8px in. */
.hm-sw-viewer-header > .hm-sw-header__document[data-tool] + .hm-sw-viewer-header__document[data-list] .hm-sw-header__document-button { padding: 0 16px 0 8px; }
/* The list (SWHeader face, placement and rows unchanged) is never narrower than the file-name button it opens from:
its outer width is at least the button's (100% of the wrapper, plus the two borders the list starts left of it) and
+50 -30
View File
@@ -14,7 +14,8 @@
--hm-sw-header-bar-height: 48px;
--hm-sw-header-field-height: 38px;
--hm-sw-header-gap: 4px;
--hm-sw-header-logo-width: 32px;
/* auto: the logo's own width at the slot height (2026-10-23; the Figma example logo is 32 x 17). */
--hm-sw-header-logo-width: auto;
--hm-sw-header-logo-height: 17px;
/* Typography: SW default is Noto Sans KR (2026-10-20 user decision; the Dotum default of the same day is withdrawn,
@@ -234,27 +235,29 @@
/* Figma 26:10492. Header stays 49px; host content can reserve the extra 28px when open.
The strip ends after its last field (user correction 2026-10-19, overrides the fixed 1222px frame):
it shrinks to its content, so no empty brown area, and is capped by the space right of its left
edge (inline max-width); longer property lists scroll inside. */
edge (inline max-width); longer property lists wrap to more rows (2026-10-23, was a scroll). 2026-10-23: it reaches at least the 속성Bar | 속성창
divider of the header (inline min-width measured by SWHeader); the fields keep their place and the rest is the
same bar background. */
.hm-sw-header__bar-anchor { position: absolute; top: 49px; z-index: 2; }
.hm-sw-property-bar {
--hm-sw-property-bar-background: #271f13;
--hm-sw-property-bar-border: #08251c;
width: max-content;
max-width: 100%;
height: 28px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
/* 2026-10-23: no scroll. Without room the fields wrap to more 27px rows and the strip (background, border, radius)
grows with them; one row stays 28px. */
min-height: 28px;
border: 1px solid var(--hm-sw-property-bar-border);
border-top: 0;
border-radius: 0 0 6px 6px;
background: var(--hm-sw-property-bar-background);
box-shadow: inset 0 4px 4px rgba(0, 0, 0, .6);
}
.hm-sw-property-bar__content { display: flex; align-items: center; width: max-content; min-width: 100%; height: 27px; }
.hm-sw-property-bar__object { flex: 0 0 90px; width: 90px; text-align: center; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--hm-sw-header-selected-color); font-weight: 700; letter-spacing: .04em; }
.hm-sw-property-bar__fields { display: flex; align-items: center; gap: 16px; padding-right: 6px; }
.hm-sw-property-bar__field { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.hm-sw-property-bar__content { display: flex; align-items: flex-start; min-height: 27px; }
/* The object name stays on the first row; wrapped rows start under the first field with the same 16px gap. */
.hm-sw-property-bar__object { flex: 0 0 90px; width: 90px; line-height: 27px; text-align: center; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--hm-sw-header-selected-color); font-weight: 700; letter-spacing: .04em; }
.hm-sw-property-bar__fields { display: flex; flex: 1 1 auto; flex-wrap: wrap; align-items: center; min-width: 0; gap: 0 16px; padding-right: 6px; }
.hm-sw-property-bar__field { display: flex; align-items: center; gap: 6px; min-height: 27px; white-space: nowrap; }
.hm-sw-property-bar__field label { color: var(--hm-sw-header-text-color); letter-spacing: .04em; }
.hm-sw-property-bar__field .hm-sw-header__input { flex-shrink: 0; }
/* 2026-10-21 (user decision): the bar shows its values (반지름 · 지름 · 원주 · 면적 …) as plain text (an <output>, never
@@ -317,55 +320,73 @@
overflow: visible;
}
/* Home: 80 x 48, its curved background overlaps the document well by 15px. */
/* Home (Figma header/btn_home 26:10655): 12px + logo + 36px (the slant part) wide, 80 x 48 with the 32 x 17 example
logo; a wider or narrower logo widens or narrows it (2026-10-23, was a fixed 80px). The logo is centered in the box
between the 12px padding and the slant. Its curved background overlaps the document well by 15px. */
.hm-sw-header__home {
position: relative;
z-index: 2;
flex: 0 0 80px;
width: 80px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
box-sizing: border-box;
height: var(--hm-sw-header-bar-height);
padding: 0 36px 0 12px;
margin-right: -17px;
overflow: hidden;
}
/* Figma Rectangle 6671: document surface behind the slanted home shape (0-65px only,
so the file name at x=70 stays visible through the transparent part of the 80px box). */
.hm-sw-header__home::before {
/* Figma Rectangle 6671: document surface behind the slanted home shape (up to 15px before the home end, so the file
name stays visible through the transparent part of the box). Also behind the SWViewerHeader logo cap (2026-10-23,
same shape), so no header background shows between its slant and the next part. */
.hm-sw-header__home::before,
.hm-sw-viewer-header__brand[data-logo]::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 65px;
width: calc(100% - 15px);
border: 1px solid var(--hm-sw-header-document-border-color);
border-right: 0;
background: var(--hm-sw-header-document-background);
}
/* The Figma home shape (92 x 72 asset with its shadow, placed at -8, -12) as a 9-slice: the left 20px and the right
42px (slant + shadow) keep their width, only the flat middle stretches; the 12px shadow rows keep their height and
the 48px body follows the host height (SWHeader 48px: the asset 1:1; also used by the SWViewerHeader logo cap).
The asset gradients are vertical, so the stretch changes nothing else. */
.hm-sw-header__home-bg,
.hm-sw-header__home-bg-hover {
position: absolute;
left: -8px;
top: -12px;
width: 92px;
height: 72px;
max-width: none;
box-sizing: border-box;
width: calc(100% + 12px);
height: calc(100% + 24px);
border: solid transparent;
border-width: 12px 42px 12px 20px;
border-image: url("../assets/home-bg.svg") 12 42 12 20 fill / 12px 42px 12px 20px stretch;
pointer-events: none;
}
/* Logo slot: the Figma home logo frame (32 x 17 at 12, 16). The slot is common design;
the logo itself belongs to each SW program. Any logo (image or element, any ratio)
is contained inside the slot and never overflows it. */
.hm-sw-header__home-bg-hover { border-image-source: url("../assets/home-bg-hover.svg"); }
/* No logo (2026-10-23): the Figma home as before — the 32px logo place stays empty, so 80 x 48 with the 68 x 48 shape
and the file name where it always starts. */
.hm-sw-header__home:not(:has(> .hm-sw-header__logo)) { padding-left: 44px; }/* Logo slot: 17px high (--hm-sw-header-logo-height), as wide as the logo at its own ratio (--hm-sw-header-logo-width
auto; the Figma 32 x 17 is an example, 2026-10-23). The slot is common design; the logo itself belongs to each SW
program. A set slot width contains the logo (ratio kept, never cut). */
.hm-sw-header__logo {
position: absolute;
left: 12px;
top: 16px;
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: var(--hm-sw-header-logo-width);
height: var(--hm-sw-header-logo-height);
overflow: hidden;
pointer-events: none;
}
.hm-sw-header__logo > :is(img, svg) {
display: block;
width: 100%;
width: auto;
max-width: 100%;
height: 100%;
object-fit: contain;
}
@@ -2100,8 +2121,7 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
Its measured width is --hm-sw-header-leading-width, subtracted from the file-name clamp above. */
.hm-sw-header__leading { position: relative; z-index: 1; display: flex; flex: 0 0 auto; gap: 2px; height: var(--hm-sw-header-bar-height); }
/* The file name after an Open button no longer sits under the logo: 12px start padding (D-1), not 5px. */
:is(.hm-sw-header__leading, .hm-sw-header__document[data-tool]) + .hm-sw-header__document > .hm-sw-header__document-button { padding-left: 12px; }
/* While the header Open is shown, the default fields keep their width: a lack of room becomes a measured
:is(.hm-sw-header__leading, .hm-sw-header__document[data-tool]) + .hm-sw-header__document > .hm-sw-header__document-button { padding-left: 12px; }/* While the header Open is shown, the default fields keep their width: a lack of room becomes a measured
overflow that moves Open into More instead of narrowing the layer field. Custom leading content alone keeps
the existing behavior (file name first, then the layer field below 520px). Below 760px only (2026-10-21): from
760px up nothing moves into More, so the fields shrink beside Open like everywhere else. */
+5 -2
View File
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite"
import { useId, useState, type ComponentProps } from "react"
import { useId, useState, type ComponentProps, type CSSProperties } from "react"
import {
Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea,
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
@@ -85,7 +85,10 @@ function SurfaceUsage({ inCard = false }: { inCard?: boolean }) {
</div>
</div>
</TabsContent>
<TabsContent value="schedule" className="px-0 py-6">
{/* 2026-10-23: this panel can be narrower than the Table default 520px minimum (--hm-table-min-width), which made the
table container scroll. The documented token is reset for this panel only: the table follows the panel width
and long cell text wraps (Table overflow-wrap). */}
<TabsContent value="schedule" className="px-0 py-6" style={{ "--hm-table-min-width": "0px" } as CSSProperties}>
<Table aria-label="프로젝트 산출물 일정">
<TableHeader><TableRow><TableHead scope="col">산출물</TableHead><TableHead scope="col">마감일</TableHead></TableRow></TableHeader>
<TableBody><TableRow><TableCell>화면 설계서</TableCell><TableCell>10월 14일</TableCell></TableRow></TableBody>
+4 -1
View File
@@ -367,11 +367,14 @@ function ReferenceFooter() {
const [open, setOpen] = useState(false)
const [aux, setAux] = useState(REFERENCE_AUX)
const [active, setActive] = useState<string | null>(null)
// 2026-10-23: onRemove makes the layout tabs removable, so the × shows on hover as in SW Footer (no onRemove =
// a disabled, invisible ×). Removal is per screen row.
const [layouts, setLayouts] = useState(LAYOUTS_2)
return (
<SWFooter categories={REFERENCE_CATEGORIES} auxItems={aux} onAuxPressedChange={(id, pressed) => setAux(items => items.map(item => (item.id === id ? { ...item, pressed } : item)))}
activeToolId={active} onToolSelect={id => setActive(id)} onSlotAdd={noop} onCategoryAdd={noop} status="10.5939, 6.0728, 0.0000"
command={<SWFooterCommandLine history={REFERENCE_HISTORY} expanded={open} onExpandedChange={setOpen} />}
layoutTabs={<SWFooterLayoutTabs layouts={LAYOUTS_2} value={SW_FOOTER_MODEL} onAdd={noop} />} />
layoutTabs={<SWFooterLayoutTabs layouts={layouts} value={SW_FOOTER_MODEL} onAdd={noop} onRemove={id => setLayouts(items => items.filter(item => item.id !== id))} />} />
)
}
+2 -2
View File
@@ -7,7 +7,7 @@ import {
import "@hm-design/sw/styles.css"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { AUX_ITEMS, CATEGORIES, HISTORY } from "./sw-footer-fixture"
import { LayerPanel, OptionsPanel, SAMPLES, useViewerDemo } from "./sw-viewer-fixture"
import { LayerPanel, OptionsPanel, SAMPLES, VIEWER_STORY_LOGO, useViewerDemo } from "./sw-viewer-fixture"
import "./sw-stories.css"
import "./sw-viewer-stories.css"
@@ -42,7 +42,7 @@ function ViewerArea({ width, panels, ...rest }: { width: number; panels?: boolea
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: panels ? ["layers", "options"] : [] })
return (
<Frame width={width} height={panels ? 600 : 220} {...rest}
header={<SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
{panels && <>
<LayerPanel demo={demo} strategy="absolute" defaultPosition="top-start" />
<OptionsPanel demo={demo} strategy="absolute" />
+5
View File
@@ -6,6 +6,11 @@ import {
type SWHeaderDocument, type SWViewerCommand, type SWViewerHeaderGroup, type SWViewerHeaderProps, type SWViewerPanelProps,
} from "@hm-design/sw"
import { Info, Layers, List, MousePointerClick, RotateCcw, Scan, SlidersHorizontal, ZoomIn } from "lucide-react"
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
// 2026-10-23: the regular viewer Stories show the TEMPORARY placeholder logo of the SW Header Stories (not a product
// logo); only SW/Viewer › Logo › 로고 없음 renders without one.
export const VIEWER_STORY_LOGO = { logoSrc: placeholderLogo, logoAlt: "임시 로고" } satisfies Partial<SWViewerHeaderProps>
export const SAMPLES = [
{ id: "sample-basic", label: "기본 도면 · DWG", name: "BasicSample.dwg" },
+12 -11
View File
@@ -8,7 +8,7 @@ import "@hm-design/sw/styles.css"
import { Copy, Download, FileText, FolderOpen, Layers, Printer, Ruler, Scan, Search } from "lucide-react"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { showVerificationControls } from "./sw-story-flags"
import { LAYERS, LayerPanel, OptionsPanel, PropertiesPanel, SAMPLES, useViewerDemo, type ViewerDemo } from "./sw-viewer-fixture"
import { LAYERS, LayerPanel, OptionsPanel, PropertiesPanel, SAMPLES, VIEWER_STORY_LOGO, useViewerDemo, type ViewerDemo } from "./sw-viewer-fixture"
import "./sw-stories.css"
import "./sw-viewer-stories.css"
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
@@ -73,7 +73,7 @@ function ViewerScreen({ width = null as number | null, initialDocument = SAMPLES
return (
<>
<Log lines={demo.log} />
<ViewerFrame width={width} header={<SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={trailing} onCommand={demo.onCommand} />}>
<ViewerFrame width={width} header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={trailing} onCommand={demo.onCommand} />}>
<LayerPanel demo={demo} strategy="absolute" />
<OptionsPanel demo={demo} strategy="absolute" inset={{ side: 356 }} />
<PropertiesPanel demo={demo} strategy="absolute" inset={{ side: 24, top: 488 }} />
@@ -121,7 +121,7 @@ export const MenuGroups: Story = {
<div className="sw-story-stack">
{rows.map(row => (
<StorySection key={row.title} title={row.title}>
<div className="sw-story-scroller"><div style={{ width: 1440 }}><SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={pick(row.ids)} trailing={row.ids.includes("panels") ? demo.trailing : undefined} onCommand={demo.onCommand} /></div></div>
<div className="sw-story-scroller"><div style={{ width: 1440 }}><SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={pick(row.ids)} trailing={row.ids.includes("panels") ? demo.trailing : undefined} onCommand={demo.onCommand} /></div></div>
</StorySection>
))}
</div>
@@ -162,7 +162,7 @@ export const States: Story = {
<StoryPageHeader kicker={KICKER} title="SW Viewer — Active / Hover / Disabled" />
<Log lines={log} />
<div className="sw-story-scroller"><div style={{ width: 1440 }}>
<SWViewerHeader documentTitle="상태 확인용.dwg" groups={groups} onCommand={command => record(`${command.id}${command.value !== undefined ? `=${String(command.value)}` : ""}`)} />
<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle="상태 확인용.dwg" groups={groups} onCommand={command => record(`${command.id}${command.value !== undefined ? `=${String(command.value)}` : ""}`)} />
</div></div>
</StoryPage>
)
@@ -216,7 +216,7 @@ export const Dropdowns: Story = {
<StoryPageHeader kicker={KICKER} title="SW Viewer — 상단 메뉴 + Dropdown" />
<Log lines={log} />
<div className="sw-story-scroller sw-viewer-story-room"><div style={{ width: 1440 }}>
<SWViewerHeader documentTitle="C0060202-001-평면및종단면도.dwg" groups={groups}
<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle="C0060202-001-평면및종단면도.dwg" groups={groups}
onCommand={command => { const entry = findEntry(entries, command.id); if (entry) apply(entry); record(`${command.itemId}/${command.id}${command.value !== undefined ? `=${String(command.value)}` : ""}`) }} />
</div></div>
{showVerificationControls() && <StorySection title="SWViewerMenu 단독 (Context Menu)">
@@ -263,7 +263,7 @@ export const SettingsPanel: Story = {
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — 설정창 (SWViewerPanel)" />
<Log lines={demo.log} />
<ViewerFrame header={<SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
<ViewerFrame header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
<OptionsPanel demo={demo} strategy="absolute" />
</ViewerFrame>
</StoryPage>
@@ -282,7 +282,7 @@ export const ContentVariants: Story = {
<StoryPageHeader title="SW Viewer — 설정창 (SWViewerPanel)" />
<Log lines={demo.log} />
{/* 2026-10-22: 620px fits the tallest popup (옵션, 520px) now that 축척 지정 is gone. */}
<ViewerFrame height={620} header={<SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
<ViewerFrame height={620} header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
<LayerPanel demo={demo} strategy="absolute" defaultPosition={{ left: 24, top: 16 }} />
<PropertiesPanel demo={demo} strategy="absolute" defaultPosition={{ left: 368, top: 16 }} />
<OptionsPanel demo={demo} strategy="absolute" defaultPosition={{ left: 712, top: 16 }} />
@@ -307,7 +307,7 @@ export const VariableSize: Story = {
return (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — 가변 크기 팝업" />
<ViewerFrame height={520} header={<SWViewerHeader documentTitle="크기 비교.dwg" groups={[]} />}>
<ViewerFrame height={520} header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle="크기 비교.dwg" groups={[]} />}>
{sizes.map(size => (
<SWViewerPanel key={size.id} strategy="absolute" defaultPosition={{ left: size.left, top: 16 }} title={size.title} open={open[size.id]} onClose={() => setOpen({ ...open, [size.id]: false })} width={size.width} height={size.height}>
<SWViewerHint>{size.note}</SWViewerHint>
@@ -334,7 +334,7 @@ export const Responsive: Story = {
// like the other rows it is a maximum inside the Story column.
<div key={width}><StorySection title={`${width}px`}>
{/* The row width is a maximum (2026-10-22): on a narrower screen the header shrinks with it instead of scrolling. */}
<div className="sw-story-scroller sw-viewer-story-menu-row"><div style={{ width: "100%", maxWidth: width }} data-width={width}><SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} /></div></div>
<div className="sw-story-scroller sw-viewer-story-menu-row"><div style={{ width: "100%", maxWidth: width }} data-width={width}><SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} /></div></div>
</StorySection></div>
))}
</div>
@@ -373,7 +373,7 @@ export const CompareWithSWHeader: Story = {
<StoryPageHeader title="SW Viewer — 일반 SW Header와 비교" />
<StorySection title="나란히 비교 (1440px)">
<div className="sw-story-stack">
<div className="sw-story-scroller"><div style={{ width: 1440 }}><SWHeader documentTitle="C0060202-001-평면및종단면도" /></div></div>
<div className="sw-story-scroller"><div style={{ width: 1440 }}><SWHeader logoSrc={placeholderLogo} logoAlt="임시 로고" documentTitle="C0060202-001-평면및종단면도" /></div></div>
<div className="sw-story-scroller"><div style={{ width: 1440 }}><ViewerOnly /></div></div>
</div>
</StorySection>
@@ -414,5 +414,6 @@ export const Logo: Story = {
function ViewerOnly() {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
return <SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
// 2026-10-23: the same placeholder logo as the SWHeader row, so the two logo slots compare at the same width.
return <SWViewerHeader logoSrc={placeholderLogo} logoAlt="임시 로고" documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
}