fix(sw): align header logo styles and refine viewer UI
Deploy Storybook / deploy (push) Successful in 1m13s
Deploy Storybook / deploy (push) Successful in 1m13s
This commit is contained in:
1 parent
1d206c86cf
commit
205864b8a0
15 files changed
+236
-96
No files matched your search
@@ -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` | 헤더 바탕 |
|
||||
|
||||
@@ -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`는 하드코드 값이고 대응 토큰이 없습니다.
|
||||
@@ -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`) | — | — | ◻ |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))} />} />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
Reference in new issue
Block a user