feat(sw): refine viewer model layout design and theme tokens
Deploy Storybook / deploy (push) Successful in 1m13s

- Model / Layout tabs follow Figma 7652:40344: gradient faces, gold / green
  gradient borders, blurred outline, inner shadow and top gloss, with every
  color in its own --hm-sw-viewer-tab(s)-* token (consumer-overridable)
- Box height matches the adjacent field group (42px, from the control height
  token); padding 1px 1px 1.5px, button side padding 5px, 1px right margin
  for a tabs-only first group; 378px max width, equal buttons and the
  measured More steps are unchanged; existing text-shadow kept
- Storybook: SW/Viewer > Model / Layout story (states and token override);
  Logo story listed before Responsive More
- AI docs (VIEWER.md, DESIGN_RULES.md) and viewer / text-weight test
  expectations updated
This commit is contained in:
shkim4 committed 2026-10-23 16:27:41 +09:00
1 parent 7a5236f3ad
commit 325ec25e37
6 files changed
+197 -56

No files matched your search

+5 -3
View File
@@ -144,18 +144,20 @@
| 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 | | Header | `--hm-sw-viewer-height` 52px, `--hm-sw-viewer-background` #0a0a0b(단색, 2026-10-21 — Storybook 뷰어 캔버스 바탕색), `--hm-sw-viewer-border-color` #071e18, `--hm-sw-viewer-text` #fff, `--hm-sw-viewer-muted` #bddfd4, `--hm-sw-viewer-accent` #ffc600, `--hm-sw-viewer-hover` #266651 |
| 로고 면 | 2026-10-23부터 `SWHeader` 홈과 같은 사선 배경(Figma 에셋 9-slice)·같은 폭 계산(왼쪽 12 + 로고 + 36, 다음 부품과 17px 겹침, 예시 로고에서 79px). `--hm-sw-viewer-brand-width` 기본 `auto`. 로고 다음 첫 부품(파일 열기·파일명) 시작 여백 8px. Hover 효과 없음(기존 그대로, `SWHeader` Hover와 다름). 로고가 없으면 로고 면 없음(폭 0, 첫 부품 12px 안쪽). `--hm-sw-viewer-brand-empty-width` 24px·`--hm-sw-viewer-brand-top` #123328·`-middle` #205241·`-bottom` #071e18은 더 쓰지 않으며 호환용으로만 남음 | | 로고 면 | 2026-10-23부터 `SWHeader` 홈과 같은 사선 배경(Figma 에셋 9-slice)·같은 폭 계산(왼쪽 12 + 로고 + 36, 다음 부품과 17px 겹침, 예시 로고에서 79px). `--hm-sw-viewer-brand-width` 기본 `auto`. 로고 다음 첫 부품(파일 열기·파일명) 시작 여백 8px. Hover 효과 없음(기존 그대로, `SWHeader` Hover와 다름). 로고가 없으면 로고 면 없음(폭 0, 첫 부품 12px 안쪽). `--hm-sw-viewer-brand-empty-width` 24px·`--hm-sw-viewer-brand-top` #123328·`-middle` #205241·`-bottom` #071e18은 더 쓰지 않으며 호환용으로만 남음 |
| 크기·글자 | `--hm-sw-viewer-gap` 4px, `--hm-sw-viewer-radius` 4px, `--hm-sw-viewer-control-height` 40px, `--hm-sw-viewer-button-height` 34px, `--hm-sw-viewer-tabs-max-width` 378px(tabs만 있는 field 그룹의 최대 폭, 2026-10-23), `--hm-sw-viewer-font-size` 14px, `--hm-sw-viewer-label-size` 11px, `--hm-sw-viewer-icon-size` 16px, `--hm-sw-viewer-disabled-opacity` 0.6 | | 크기·글자 | `--hm-sw-viewer-gap` 4px, `--hm-sw-viewer-radius` 4px, `--hm-sw-viewer-control-height` 40px, `--hm-sw-viewer-button-height` 34px, `--hm-sw-viewer-tabs-max-width` 378px(tabs만 있는 field 그룹의 최대 폭, 2026-10-23), `--hm-sw-viewer-font-size` 14px, `--hm-sw-viewer-label-size` 11px, `--hm-sw-viewer-icon-size` 16px, `--hm-sw-viewer-disabled-opacity` 0.6 |
| Model / Layout 탭(Figma 7652:40344, 2026-10-23) | 컨테이너: `--hm-sw-viewer-tabs-height` = `calc(var(--hm-sw-viewer-control-height) + 2px)` = 42px(옆 field 그룹 바깥 높이와 같음; 바깥 1px은 blur 외곽선 자리, 그 안 검정 여백 위·좌우 1px·아래 1.5px, 버튼은 나머지 37.5px를 채움), `--hm-sw-viewer-tabs-radius` 6px, `--hm-sw-viewer-tabs-background` `rgba(0, 0, 0, 0.8)`(면), `--hm-sw-viewer-tabs-outline-color` #349f7f(42px 상자 바깥 가장자리 1px 선, `::before`가 상자와 같은 영역, blur는 `clip-path: inset(-1px …)`로 바깥 1px까지), `--hm-sw-viewer-tabs-outline-blur` 1px(그 선의 blur, Figma Layer blur 2), `--hm-sw-viewer-tabs-glow-color` `transparent`(추가 Glow 색, 기본 없음 — Figma는 blur 외곽선이 Glow), `--hm-sw-viewer-tabs-glow` `0 0 4px var(--hm-sw-viewer-tabs-glow-color)`, `--hm-sw-viewer-tabs-shadow` `inset 0 2.857px 2.857px rgba(0, 0, 0, 0.25)`(버튼 위에 겹치는 안쪽 그림자). 버튼 공통: 높이는 컨테이너를 채움(별도 높이 토큰 없음), `--hm-sw-viewer-tab-radius` 5px(바깥 모서리만), `--hm-sw-viewer-tab-border-width` 1.428px, `--hm-sw-viewer-tab-gap` 14px(이름–개수; 버튼 좌우 안쪽 여백 5px와 함께 기존 기본 폭·측정 단계 유지). 객체 면의 첫 그룹이 Model / Layout 탭만 가진 경우에만 그 그룹 오른쪽에 `margin-right: 1px`(조건부 규칙, 다른 Header 그룹 간격은 객체 면 gap 그대로): 다음 그룹까지 1180px 이상 5px, 미만 3px. 최대 너비 378px·높이 42px·반응형 More 이동 단계는 그대로(버튼 좌우 여백 5px), `--hm-sw-viewer-tab-background` #0e3d2f(Gradient 아래 바탕), `--hm-sw-viewer-tab-gradient` `linear-gradient(180deg, #081813 0%, #09271e 8.6884%, #0b3227 25.428%, #0e3d2f 51.252%, #0b3227 80.027%, #0a2b21 89.759%, #081813 100%)`. 테두리 토큰은 이미지(Figma Gradient Stroke, 위→아래)이며 단색은 `linear-gradient(<색>, <색>)`로 지정합니다. 선택: `--hm-sw-viewer-tab-active-border` `linear-gradient(180deg, #a67d03 0%, #f2cc0f 16.4%, #fadd4f 37.2%, #f2cc0f 62%, #926e03 87.9%)`, `--hm-sw-viewer-tab-active-text` = `var(--hm-sw-viewer-accent)`(#ffc600). 비선택: `--hm-sw-viewer-tab-inactive-border` `linear-gradient(180deg, #189b6e 0%, #287a5e 100%)`, `--hm-sw-viewer-tab-inactive-text` = `var(--hm-sw-viewer-text)`(#fff). 글자는 두 상태 모두 세로 가운데. 광택(비선택 상단): `--hm-sw-viewer-tab-highlight-color` #2d8b6b → `--hm-sw-viewer-tab-highlight-end` `rgba(11, 45, 34, 0)`, `--hm-sw-viewer-tab-highlight-opacity` 0.67, `--hm-sw-viewer-tab-highlight-height` 20px. 상호작용: `--hm-sw-viewer-tab-hover-border` = `var(--hm-sw-viewer-tab-inactive-border)`, `--hm-sw-viewer-tab-hover-text` = `var(--hm-sw-viewer-accent)`, `--hm-sw-viewer-tab-focus-color` = `var(--hm-sw-viewer-accent)`. `--hm-sw-viewer-tab-border-face`는 상태별 테두리를 고르는 내부 변수라 지정하지 않습니다 |
| 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 | | 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 | | Dropdown·More | `--hm-sw-viewer-menu-background` #fff, `--hm-sw-viewer-menu-border-color` #dedbd2, `--hm-sw-viewer-menu-text` #0a221d, `--hm-sw-viewer-menu-muted` #6e6759, `--hm-sw-viewer-menu-highlight` `rgba(189, 223, 212, 0.45)`, `--hm-sw-viewer-menu-highlight-text` #123328, `--hm-sw-viewer-menu-shadow` `0 0 12px rgba(0, 0, 0, 0.2)`, `--hm-sw-viewer-menu-item-height` 36px, `--hm-sw-viewer-more-background` #f5f0e5, `--hm-sw-viewer-more-border-color` #897445 |
| 패널 콘텐츠 | `--hm-sw-viewer-panel-background` = `var(--hm-sw-property-surface)`, `--hm-sw-viewer-panel-field-background` = `var(--hm-sw-property-content)`, `--hm-sw-viewer-panel-inner-border` = `var(--hm-sw-property-inner-border)`, `--hm-sw-viewer-panel-text` = `var(--hm-sw-property-label)`, `--hm-sw-viewer-panel-heading` #3e3523, `--hm-sw-viewer-panel-label` #6e6759, `--hm-sw-viewer-panel-primary` #123328, `--hm-sw-viewer-panel-switch-off` = `var(--hm-sw-property-inner-border)`, `--hm-sw-viewer-panel-switch-on` #266651, `--hm-sw-viewer-panel-badge-background` = `var(--hm-sw-property-table)`, `--hm-sw-viewer-panel-overlay` `rgba(0, 0, 0, 0.1)`, `--hm-sw-viewer-panel-spacing` 12px | | 패널 콘텐츠 | `--hm-sw-viewer-panel-background` = `var(--hm-sw-property-surface)`, `--hm-sw-viewer-panel-field-background` = `var(--hm-sw-property-content)`, `--hm-sw-viewer-panel-inner-border` = `var(--hm-sw-property-inner-border)`, `--hm-sw-viewer-panel-text` = `var(--hm-sw-property-label)`, `--hm-sw-viewer-panel-heading` #3e3523, `--hm-sw-viewer-panel-label` #6e6759, `--hm-sw-viewer-panel-primary` #123328, `--hm-sw-viewer-panel-switch-off` = `var(--hm-sw-property-inner-border)`, `--hm-sw-viewer-panel-switch-on` #266651, `--hm-sw-viewer-panel-badge-background` = `var(--hm-sw-property-table)`, `--hm-sw-viewer-panel-overlay` `rgba(0, 0, 0, 0.1)`, `--hm-sw-viewer-panel-spacing` 12px |
- **Model / Layout 탭 토큰:** 위 `--hm-sw-viewer-tabs-*`·`--hm-sw-viewer-tab-*`은 탭 컨테이너·버튼에만 연결되며 다른 Header 부품·SWHeader·Footer·패널에 쓰이지 않습니다. 탭 색만 바꾸려면 가장 바깥 SW 범위의 `style`로 덮습니다: 화면을 `<SWTheme style={...}>`로 감싸거나 단독 `SWViewerHeader`의 루트에 지정합니다(토큰이 그 범위에 선언되므로 일반 `div` 래퍼의 값은 가려짐). 예: `style={{ "--hm-sw-viewer-tab-active-border": "linear-gradient(#e07b00, #e07b00)", "--hm-sw-viewer-tabs-outline-color": "#4a7fd0" }}`. `var(...)` 기본값(`active-text`·`hover-*`·`focus-color`)은 SW 범위에서 계산되므로 공통 토큰(`--hm-sw-viewer-accent` 등)을 바꾸면 같은 범위에서 함께 따라가고, 탭 토큰을 직접 지정하면 공통 토큰과 독립합니다. 글자 외곽선(`text-shadow`)은 탭에서도 기존 그대로(이름 `--hm-sw-header-text-outline` 1.3px, 개수 `--hm-sw-header-label-outline` 1px)이며 탭 토큰이 아닙니다. 탭 class로 색을 덮지 않습니다.
- **공유 토큰(모드 혼합 아님):** 뷰어 Header의 파일명·객체 수정·속성 면은 `--hm-sw-header-document-*`, `--hm-sw-header-object-*`, `--hm-sw-header-property-*`를, 글자 외곽선은 `--hm-sw-header-text-outline`·`--hm-sw-header-label-outline`을 그대로 읽습니다(§2·§3). 패널 제목 막대 추가 버튼(`ghost`)의 Hover·Focus 색은 `--hm-sw-header-selected-color`입니다. - **공유 토큰(모드 혼합 아님):** 뷰어 Header의 파일명·객체 수정·속성 면은 `--hm-sw-header-document-*`, `--hm-sw-header-object-*`, `--hm-sw-header-property-*`를, 글자 외곽선은 `--hm-sw-header-text-outline`·`--hm-sw-header-label-outline`을 그대로 읽습니다(§2·§3). 패널 제목 막대 추가 버튼(`ghost`)의 Hover·Focus 색은 `--hm-sw-header-selected-color`입니다.
| 상태 | Header 항목(button·toggle·menu·tabs·switch) | 패널 콘텐츠 | | 상태 | Header 항목(button·toggle·menu·tabs·switch) | 패널 콘텐츠 |
|---|---|---| |---|---|---|
| Hover | 글자·아이콘 `--hm-sw-viewer-accent`(#ffc600), 면 없음, 크기·위치 변화 없음(2026-10-21; 상태 `SWViewerStatus`는 Hover 없음) | `outline` 버튼: 면 `--hm-sw-viewer-panel-field-background` / `primary`: 면·테두리 `--hm-sw-viewer-hover` / `tone="green"`: 면·테두리 `--hm-sw-viewer-hover`, 글자 #fff / `ghost`: 글자만 `--hm-sw-header-selected-color` | | Hover | 글자·아이콘 `--hm-sw-viewer-accent`(#ffc600), 면 없음, 크기·위치 변화 없음(2026-10-21; 상태 `SWViewerStatus`는 Hover 없음). 탭: 비선택 버튼만 글자 `--hm-sw-viewer-tab-hover-text`·테두리 `--hm-sw-viewer-tab-hover-border` | `outline` 버튼: 면 `--hm-sw-viewer-panel-field-background` / `primary`: 면·테두리 `--hm-sw-viewer-hover` / `tone="green"`: 면·테두리 `--hm-sw-viewer-hover`, 글자 #fff / `ghost`: 글자만 `--hm-sw-header-selected-color` |
| Active·Pressed·열림·선택 탭 | 글자·아이콘 `--hm-sw-viewer-accent`(#ffc600), 선택 박스 없음 | `tone="green"` 누르는 동안: 면·테두리 `--hm-sw-viewer-panel-primary`, 글자 #fff | | Active·Pressed·열림·선택 탭 | 글자·아이콘 `--hm-sw-viewer-accent`(#ffc600), 선택 박스 없음. 선택 탭: 테두리 `--hm-sw-viewer-tab-active-border`, 글자 `--hm-sw-viewer-tab-active-text`, 광택 없음 | `tone="green"` 누르는 동안: 면·테두리 `--hm-sw-viewer-panel-primary`, 글자 #fff |
| Checked(스위치) | 면 `--hm-sw-viewer-switch-on`(= `--hm-sw-viewer-hover`), thumb `--hm-sw-viewer-switch-thumb-on`(노란색 없음, 2026-10-22) | 면 `--hm-sw-viewer-panel-switch-on` | | Checked(스위치) | 면 `--hm-sw-viewer-switch-on`(= `--hm-sw-viewer-hover`), thumb `--hm-sw-viewer-switch-thumb-on`(노란색 없음, 2026-10-22) | 면 `--hm-sw-viewer-panel-switch-on` |
| Focus | 2px solid `--hm-sw-viewer-accent` 외곽선 | 버튼·Input·Slider: 2px solid `--hm-sw-viewer-panel-switch-on` 외곽선 | | Focus | 2px solid `--hm-sw-viewer-accent` 외곽선(탭: `--hm-sw-viewer-tab-focus-color`, 버튼 안쪽 -2px) | 버튼·Input·Slider: 2px solid `--hm-sw-viewer-panel-switch-on` 외곽선 |
| Disabled | `--hm-sw-viewer-disabled-opacity`(0.6) | 버튼·Input·Field 0.5 | | Disabled | `--hm-sw-viewer-disabled-opacity`(0.6) | 버튼·Input·Field 0.5 |
| Dropdown 항목 | 면 `--hm-sw-viewer-menu-highlight`, 글자 `--hm-sw-viewer-menu-highlight-text`(Focus·Hover·서브메뉴 열림), Disabled 0.5 | — | | Dropdown 항목 | 면 `--hm-sw-viewer-menu-highlight`, 글자 `--hm-sw-viewer-menu-highlight-text`(Focus·Hover·서브메뉴 열림), Disabled 0.5 | — |
| 상태 표시 | `SWViewerStatus`: 글자 `--hm-sw-viewer-muted` 11px 400, `error`는 `--hm-sw-viewer-accent`, `loading` 아이콘 회전 | — | | 상태 표시 | `SWViewerStatus`: 글자 `--hm-sw-viewer-muted` 11px 400, `error`는 `--hm-sw-viewer-accent`, `loading` 아이콘 회전 | — |
+2 -2
View File
@@ -31,7 +31,7 @@
| `type: "button"` | `onSelect` | | `type: "button"` | `onSelect` |
| `type: "toggle"` | `pressed`, `onPressedChange` (패널 열기/닫기 등) | | `type: "toggle"` | `pressed`, `onPressedChange` (패널 열기/닫기 등) |
| `type: "menu"` | `entries: SWViewerMenuEntry[]`, `menuWidth?`, `menuAlign?: "start" \| "end"` | | `type: "menu"` | `entries: SWViewerMenuEntry[]`, `menuWidth?`, `menuAlign?: "start" \| "end"` |
| `type: "tabs"` | `value`, `options: { value, label, count?, title?, disabled? }[]`, `onValueChange` (Model / Layout). 폭(2026-10-23): 두 버튼은 항상 같은 폭(가장 긴 이름 + 개수 이상이라 겹치지 않음). tabs만 있는 그룹은 Header에 남는 공간만큼 넓어져 field 면 최대 378px(`--hm-sw-viewer-tabs-max-width`), 공간이 적으면 그만큼만 넓어지고 기본 폭보다 부족하면 기존 측정 단계대로 More로 이동합니다. 모양(높이·글자·개수·색·field 면)은 그대로 | | `type: "tabs"` | `value`, `options: { value, label, count?, title?, disabled? }[]`, `onValueChange` (Model / Layout). 폭(2026-10-23): 두 버튼은 항상 같은 폭(가장 긴 이름 + 개수 이상이라 겹치지 않음). tabs만 있는 그룹은 Header에 남는 공간만큼 넓어져 field 면 최대 378px(`--hm-sw-viewer-tabs-max-width`), 공간이 적으면 그만큼만 넓어지고 기본 폭보다 부족하면 기존 측정 단계대로 More로 이동합니다. 모양(2026-10-23, Figma 7652:40344): tabs만 있는 그룹은 field 면 없이 옆 field 그룹과 같은 42px(`--hm-sw-viewer-control-height` + 2px) 어두운 컨테이너(검정 여백 위·좌우 1px·아래 1.5px, 가장자리 blur 처리한 1px #349f7f 선 — blur는 상자 밖 1px까지, 안쪽 그림자)가 면이 되고, 버튼(컨테이너를 채워 37.5px)은 같은 Gradient 면과 1.428px Gradient 테두리(선택 금색 #a67d03→#fadd4f→#926e03 + 글자 #ffc600, 비선택 #189b6e→#287a5e + 흰 글자 + 상단 초록 광택), 이름 14px 700·개수 14px 400, 간격 14px(버튼 좌우 여백 5px), 객체 면 첫 그룹이 이 탭만 가진 경우에만 그룹 오른쪽 여백 1px(조건부, 다른 Header 그룹에는 없음 — 다음 그룹까지 1180px 이상 5px, 미만 3px; 최대 378px·높이 42px·More 이동 단계 그대로), 글자 외곽선은 기존 그대로입니다. 색은 `--hm-sw-viewer-tabs-*`·`--hm-sw-viewer-tab-*` 토큰으로만 바꿉니다(목록·기본값·Override: [DESIGN_RULES.md](./DESIGN_RULES.md) §11, Story `SW/Viewer › Model / Layout`) |
| `type: "switch"` | `checked`, `onCheckedChange` (2.5D 등, 위 11px 캡션) | | `type: "switch"` | `checked`, `onCheckedChange` (2.5D 등, 위 11px 캡션) |
| `type: "custom"` | `render()`, `menuEntries?`(More에 넣을 항목) | | `type: "custom"` | `render()`, `menuEntries?`(More에 넣을 항목) |
| `onCommand(command)` | 모든 명령을 한 곳에서 받습니다: `{ id, itemId, value? }`. 항목 자체 Callback 다음에 호출 | | `onCommand(command)` | 모든 명령을 한 곳에서 받습니다: `{ id, itemId, value? }`. 항목 자체 Callback 다음에 호출 |
@@ -154,6 +154,6 @@ const groups: SWViewerHeaderGroup[] = [
## 7. 디자인 변경 시 주의 ## 7. 디자인 변경 시 주의
- 크기(52px Header, 34px 버튼, 40px 토글·탭·스위치 칸, 41~42px field 면)와 색은 참고 뷰어 실측값이고, 로고 면 64px는 2026-10-23 `SWHeader` 로고 기준입니다. 파일명·객체 수정·속성 면 배경은 SWHeader와 같은 사용자 확정 토큰(`--hm-sw-header-document-*`, `object-*`, `property-*`)을 공유하므로 바꾸면 두 Header가 함께 바뀝니다. - 크기(52px Header, 34px 버튼, 40px 토글·스위치 칸, 41~42px field 면)와 색은 참고 뷰어 실측값이고(Model / Layout 탭은 2026-10-23 Figma 7652:40344 모양, 높이는 field 그룹과 같은 42px·버튼 37.5px), 로고 면 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 예시의 고정 높이는 미리보기 프레임 값일 뿐 컴포넌트 값이 아닙니다. - **높이:** 패키지는 뷰어(도면) 영역 높이를 정하지 않습니다. 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 닫기 추가, 로고 기본 표시 없음(사용자 결정). - Light/Dark 분기는 없습니다. 참고 뷰어와의 알려진 차이: 도면 공간(tabs) field 면 높이 42px(참고 41px — 참고 Tabs 루트가 39px인 구현 부산물), 크기 조절은 네이티브 `resize` 대신 같은 위치의 모서리 Grip(모든 브라우저 동작), 패널 Escape 닫기 추가, 로고 기본 표시 없음(사용자 결정).
+119 -18
View File
@@ -28,6 +28,40 @@
--hm-sw-viewer-button-height: 34px; --hm-sw-viewer-button-height: 34px;
/* Model / Layout field face: widest size (2026-10-23); narrower when the header has less room. */ /* Model / Layout field face: widest size (2026-10-23); narrower when the header has less room. */
--hm-sw-viewer-tabs-max-width: 378px; --hm-sw-viewer-tabs-max-width: 378px;
/* Model / Layout colors and shape (Figma 7652:40344, 2026-10-23). Used only by .hm-sw-viewer-header__tabs / __tab:
set any of them on the SW scope or the header to recolor the tabs alone. Container: */
/* The outer height of the adjacent field group (40px control row + its 1px top and bottom border = 42px). */
--hm-sw-viewer-tabs-height: calc(var(--hm-sw-viewer-control-height) + 2px);
--hm-sw-viewer-tabs-radius: 6px;
--hm-sw-viewer-tabs-background: rgba(0, 0, 0, 0.8);
/* Outline: a 1px line just outside the frame, blurred (Figma Rectangle 9333: stroke outside + layer blur 2). */
--hm-sw-viewer-tabs-outline-color: #349f7f;
--hm-sw-viewer-tabs-outline-blur: 1px;
/* Extra glow around the frame: none in Figma (the blurred outline is the glow); set a color to add one. */
--hm-sw-viewer-tabs-glow-color: transparent;
--hm-sw-viewer-tabs-glow: 0 0 4px var(--hm-sw-viewer-tabs-glow-color);
--hm-sw-viewer-tabs-shadow: inset 0 2.857px 2.857px rgba(0, 0, 0, 0.25);
/* Buttons (both states share the face): */
--hm-sw-viewer-tab-radius: 5px;
--hm-sw-viewer-tab-border-width: 1.428px;
--hm-sw-viewer-tab-gap: 14px;
--hm-sw-viewer-tab-background: #0e3d2f;
--hm-sw-viewer-tab-gradient: linear-gradient(180deg, #081813 0%, #09271e 8.6884%, #0b3227 25.428%, #0e3d2f 51.252%, #0b3227 80.027%, #0a2b21 89.759%, #081813 100%);
/* Selected (Model in Figma) / not selected (Layout in Figma). Borders are images (Figma gradient strokes, top to
bottom); a solid border is linear-gradient(<color>, <color>). */
--hm-sw-viewer-tab-active-border: linear-gradient(180deg, #a67d03 0%, #f2cc0f 16.4%, #fadd4f 37.2%, #f2cc0f 62%, #926e03 87.9%);
--hm-sw-viewer-tab-active-text: var(--hm-sw-viewer-accent);
--hm-sw-viewer-tab-inactive-border: linear-gradient(180deg, #189b6e 0%, #287a5e 100%);
--hm-sw-viewer-tab-inactive-text: var(--hm-sw-viewer-text);
/* Top gloss of the not-selected button: */
--hm-sw-viewer-tab-highlight-color: #2d8b6b;
--hm-sw-viewer-tab-highlight-end: rgba(11, 45, 34, 0);
--hm-sw-viewer-tab-highlight-opacity: 0.67;
--hm-sw-viewer-tab-highlight-height: 20px;
/* Interaction (no Figma hover: the former text-only #FFC600 hover is kept): */
--hm-sw-viewer-tab-hover-border: var(--hm-sw-viewer-tab-inactive-border);
--hm-sw-viewer-tab-hover-text: var(--hm-sw-viewer-accent);
--hm-sw-viewer-tab-focus-color: var(--hm-sw-viewer-accent);
--hm-sw-viewer-font-size: 14px; --hm-sw-viewer-font-size: 14px;
--hm-sw-viewer-label-size: 11px; --hm-sw-viewer-label-size: 11px;
--hm-sw-viewer-icon-size: 16px; --hm-sw-viewer-icon-size: 16px;
@@ -353,31 +387,94 @@ button.hm-sw-viewer-header__brand { text-align: left; }
.hm-sw-viewer-icon__glyph { position: relative; } .hm-sw-viewer-icon__glyph { position: relative; }
.hm-sw-viewer-icon svg { width: 100%; height: 100%; overflow: visible; } .hm-sw-viewer-icon svg { width: 100%; height: 100%; overflow: visible; }
/* Tabs (Model / Layout): 40px triggers with an 11px count; the active one turns #FFC600. /* Tabs (Model / Layout), Figma 7652:40344 (2026-10-23): a dark container (overlay face with a 1px margin, a blurred
Width (2026-10-23): equal columns (1fr: each at least the widest name + count, so they never overlap). A group green line outside it, inner top shadow over the buttons) holding buttons 1px apart; each button has the gradient
face, a gold (selected) or green (not selected) gradient border and the name (Bold) + count (Regular) at 14px; the
not-selected one has a green top gloss. Every color is a --hm-sw-viewer-tab(s)-* token (see the token block).
Width (2026-10-23, kept): equal columns (1fr: each at least the widest name + count, so they never overlap). A group
holding only the tabs starts at that natural width and takes the object's free room up to holding only the tabs starts at that natural width and takes the object's free room up to
--hm-sw-viewer-tabs-max-width (outer field face); with less room it stays narrower, and below the natural width the --hm-sw-viewer-tabs-max-width (the container is the group face then); with less room it stays narrower, and below
measured steps move it into More as before (the measurement, `data-measuring`, uses the natural width). */ the natural width the measured steps move it into More as before (`data-measuring` uses the natural width). */
.hm-sw-viewer-header__tabs { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; align-items: center; gap: var(--hm-sw-viewer-gap); } .hm-sw-viewer-header__tabs {
.hm-sw-viewer-header__group:has(> .hm-sw-viewer-header__tabs:only-child) { flex: 1 0 auto; box-sizing: border-box; max-width: var(--hm-sw-viewer-tabs-max-width); } position: relative;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
align-items: stretch;
gap: 1px;
box-sizing: border-box;
height: var(--hm-sw-viewer-tabs-height);
/* The transparent 1px edge is the room of the blurred outline (it stays inside the box height); the padding is the
black margin around the buttons, which fill the rest. 1.5px at the bottom (2026-10-23 user value): the outline
blur covers part of a 1px bottom margin, while the inner shadow darkens the top, so 1px read as no room below. */
padding: 1px 1px 1.5px 1px;
border: 1px solid transparent;
border-radius: calc(var(--hm-sw-viewer-tabs-radius) + 1px);
background: var(--hm-sw-viewer-tabs-background) padding-box;
box-shadow: var(--hm-sw-viewer-tabs-glow);
}
/* The outline (blurred, on the frame edge) and the inner shadow lie over the buttons, as the last Figma layers do. */
.hm-sw-viewer-header__tabs::before {
content: "";
position: absolute;
z-index: 1;
/* The outer edge of the box (its border box, over the transparent 1px edge row): same size as the tabs box. */
inset: -1px;
border: 1px solid var(--hm-sw-viewer-tabs-outline-color);
border-radius: inherit;
filter: blur(var(--hm-sw-viewer-tabs-outline-blur));
/* The blur may soften 1px past that edge, no further (2026-10-23 user decision). */
clip-path: inset(-1px round calc(var(--hm-sw-viewer-tabs-radius) + 1px));
pointer-events: none;
}
.hm-sw-viewer-header__tabs::after { content: ""; position: absolute; z-index: 1; inset: 0; border-radius: var(--hm-sw-viewer-tabs-radius); box-shadow: var(--hm-sw-viewer-tabs-shadow); pointer-events: none; }
/* A group holding only the tabs: the container is its face (no field face around it, as in Figma: the tabs sit on
the header object surface). */
.hm-sw-viewer-header__group:has(> .hm-sw-viewer-header__tabs:only-child) { flex: 1 0 auto; box-sizing: border-box; max-width: var(--hm-sw-viewer-tabs-max-width); min-height: 0; padding: 0; border: 0; background: none; box-shadow: none; }
.hm-sw-viewer-header__group > .hm-sw-viewer-header__tabs:only-child { flex: 1 1 auto; } .hm-sw-viewer-header__group > .hm-sw-viewer-header__tabs:only-child { flex: 1 1 auto; }
.hm-sw-viewer-header .hm-sw-viewer-header__tab { /* 2026-10-23 user value: the tabs-only group that starts the object surface gets 1px more on its right (the buttons
give it back in their side padding). Other groups keep the object gap. */
.hm-sw-viewer-header__object > .hm-sw-viewer-header__group:first-child:has(> .hm-sw-viewer-header__tabs:only-child) {
margin-right: 1px;
}.hm-sw-viewer-header .hm-sw-viewer-header__tab {
position: relative;
isolation: isolate;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: var(--hm-sw-viewer-gap); gap: var(--hm-sw-viewer-tab-gap);
height: var(--hm-sw-viewer-control-height); box-sizing: border-box;
padding: 0 8px; padding: 0 5px; /* 6px less the 2px of the tabs edge row and the 1px group margin above, so the natural width (measured steps) is unchanged */
border: 1px solid transparent; /* The border is the state's border image painted under a transparent border (keeps the radius). */
border-radius: var(--hm-sw-viewer-radius); --hm-sw-viewer-tab-border-face: var(--hm-sw-viewer-tab-inactive-border);
color: var(--hm-sw-viewer-text); border: var(--hm-sw-viewer-tab-border-width) solid transparent;
border-radius: 0;
background: var(--hm-sw-viewer-tab-gradient) padding-box, linear-gradient(var(--hm-sw-viewer-tab-background), var(--hm-sw-viewer-tab-background)) padding-box, var(--hm-sw-viewer-tab-border-face) border-box;
color: var(--hm-sw-viewer-tab-inactive-text);
font-weight: 500; font-weight: 500;
line-height: 1.4; line-height: 20px;
white-space: nowrap; white-space: nowrap;
} }
.hm-sw-viewer-header .hm-sw-viewer-header__tab:hover:not(:disabled) { color: var(--hm-sw-viewer-accent); } .hm-sw-viewer-header .hm-sw-viewer-header__tab:first-child { border-start-start-radius: var(--hm-sw-viewer-tab-radius); border-end-start-radius: var(--hm-sw-viewer-tab-radius); }
.hm-sw-viewer-header .hm-sw-viewer-header__tab[data-state="active"] { color: var(--hm-sw-viewer-accent); } .hm-sw-viewer-header .hm-sw-viewer-header__tab:last-child { border-start-end-radius: var(--hm-sw-viewer-tab-radius); border-end-end-radius: var(--hm-sw-viewer-tab-radius); }
.hm-sw-viewer-header .hm-sw-viewer-header__tab:not([data-state="active"])::before {
content: "";
position: absolute;
z-index: -1;
inset: 0 0 auto;
height: var(--hm-sw-viewer-tab-highlight-height);
border-radius: inherit;
border-end-start-radius: 0;
border-end-end-radius: 0;
background: linear-gradient(180deg, var(--hm-sw-viewer-tab-highlight-color), var(--hm-sw-viewer-tab-highlight-end));
opacity: var(--hm-sw-viewer-tab-highlight-opacity);
pointer-events: none;
}
.hm-sw-viewer-header .hm-sw-viewer-header__tab:hover:not(:disabled, [data-state="active"]) { --hm-sw-viewer-tab-border-face: var(--hm-sw-viewer-tab-hover-border); color: var(--hm-sw-viewer-tab-hover-text); }
.hm-sw-viewer-header .hm-sw-viewer-header__tab[data-state="active"] { --hm-sw-viewer-tab-border-face: var(--hm-sw-viewer-tab-active-border); color: var(--hm-sw-viewer-tab-active-text); }
.hm-sw-viewer-header__count { font-size: var(--hm-sw-viewer-label-size); font-weight: 400; font-variant-numeric: tabular-nums; } .hm-sw-viewer-header__count { font-size: var(--hm-sw-viewer-label-size); font-weight: 400; font-variant-numeric: tabular-nums; }
/* In a tab the count has the name's size (Figma: 14px Regular) and color. */
.hm-sw-viewer-header__tab .hm-sw-viewer-header__count { font-size: inherit; }
/* Switch fields (2.5D, 어두운 배경): 11px muted caption above the switch. */ /* Switch fields (2.5D, 어두운 배경): 11px muted caption above the switch. */
.hm-sw-viewer-header__switch-field { .hm-sw-viewer-header__switch-field {
@@ -391,17 +488,21 @@ button.hm-sw-viewer-header__brand { text-align: left; }
} }
.hm-sw-viewer-header__switch-label { color: var(--hm-sw-viewer-muted); font-size: var(--hm-sw-viewer-label-size); font-weight: 400; line-height: 1; white-space: nowrap; } .hm-sw-viewer-header__switch-label { color: var(--hm-sw-viewer-muted); font-size: var(--hm-sw-viewer-label-size); font-weight: 400; line-height: 1; white-space: nowrap; }
.hm-sw-viewer-header :is(.hm-sw-viewer-header__button, .hm-sw-viewer-header__tab, .hm-sw-viewer-header__brand, .hm-sw-viewer-switch):focus-visible { .hm-sw-viewer-header :is(.hm-sw-viewer-header__button, .hm-sw-viewer-header__brand, .hm-sw-viewer-switch):focus-visible {
outline: 2px solid var(--hm-sw-viewer-accent); outline: 2px solid var(--hm-sw-viewer-accent);
outline-offset: 2px; outline-offset: 2px;
} }
/* Tabs: inside the button (the neighbor is 1px away and the container shadow lies on top). */
.hm-sw-viewer-header .hm-sw-viewer-header__tab:focus-visible { z-index: 2; outline: 2px solid var(--hm-sw-viewer-tab-focus-color); outline-offset: -2px; }
/* Text and icon stroke (2026-10-19 request): the SWHeader black outline on the viewer header, same tokens and /* Text and icon stroke (2026-10-19 request): the SWHeader black outline on the viewer header, same tokens and
method — an outside outline of the glyph / icon shape, never a box border. method — an outside outline of the glyph / icon shape, never a box border.
Text: 14px = --hm-sw-header-text-outline (1.3px black), 11px captions / counts / status = --hm-sw-header-label-outline Text: 14px = --hm-sw-header-text-outline (1.3px black), 11px captions / counts / status = --hm-sw-header-label-outline
(1px black), as SWHeader's 12px labels. Icons: SWViewerIcon (SVG stroke, see the token). Fill color, size, (1px black), as SWHeader's 12px labels. Icons: SWViewerIcon (SVG stroke, see the token). Fill color, size,
weight and spacing are unchanged; hover / active / open change only the fill, disabled fades the whole button weight and spacing are unchanged; hover / active / open change only the fill, disabled fades the whole button
(opacity) like SWHeader. Dropdown faces (light / beige) and panels get no outline, like SWHeader's More menu. */ (opacity) like SWHeader. Dropdown faces (light / beige) and panels get no outline, like SWHeader's More menu.
Model / Layout (2026-10-23 user decision): the Figma 7652:40344 look keeps these outlines unchanged (name 1.3px,
count 1px, also at the count's new 14px). */
.hm-sw-viewer-header :is( .hm-sw-viewer-header :is(
.hm-sw-viewer-header__brand-mark, .hm-sw-viewer-header__document-name, .hm-sw-viewer-header__label, .hm-sw-viewer-header__tab .hm-sw-viewer-header__brand-mark, .hm-sw-viewer-header__document-name, .hm-sw-viewer-header__label, .hm-sw-viewer-header__tab
) { ) {
+2 -1
View File
@@ -152,7 +152,8 @@ const labels = vNames.map(v => v.text)
check('Viewer: file name, 파일 열기, 전체 보기 · 레이어 · 설정 · 속성, Model · Layout are 14px 700 + 1.3px outline', want.every(w => labels.includes(w)) && labels.some(t => t.startsWith('Model')) && labels.some(t => t.startsWith('Layout')) && vNames.every(v => v.weight === '700' && v.size === '14px' && v.outline === OUTLINE_14), vNames) check('Viewer: file name, 파일 열기, 전체 보기 · 레이어 · 설정 · 속성, Model · Layout are 14px 700 + 1.3px outline', want.every(w => labels.includes(w)) && labels.some(t => t.startsWith('Model')) && labels.some(t => t.startsWith('Layout')) && vNames.every(v => v.weight === '700' && v.size === '14px' && v.outline === OUTLINE_14), vNames)
check('Viewer: 샘플 is no longer a header label', !labels.includes('샘플'), labels) check('Viewer: 샘플 is no longer a header label', !labels.includes('샘플'), labels)
const counts = await styles('.hm-sw-viewer-header >> nth=0 >> .hm-sw-viewer-header__count') const counts = await styles('.hm-sw-viewer-header >> nth=0 >> .hm-sw-viewer-header__count')
check('Viewer: tab counts keep 11px 400', counts.length > 0 && counts.every(c => c.weight === '400' && c.size === '11px'), counts) // 2026-10-23 Figma 7652:40344: counts are 14px (the name's size), still 400 with the 1px outline.
check('Viewer: tab counts 14px 400 (Figma 7652:40344)', counts.length > 0 && counts.every(c => c.weight === '400' && c.size === '14px'), counts)
// File open button (파일 열기, the logo menu is gone): hover keeps its label in place, size and weight. // File open button (파일 열기, the logo menu is gone): hover keeps its label in place, size and weight.
const openButton = vh.locator('[data-document-open]') const openButton = vh.locator('[data-document-open]')
await stableOnHover('Viewer 파일 열기', openButton, openButton.locator('.hm-sw-viewer-header__open-label')) await stableOnHover('Viewer 파일 열기', openButton, openButton.locator('.hm-sw-viewer-header__open-label'))
+2 -2
View File
@@ -82,8 +82,8 @@ if (reference && reference !== '-') {
await page.close() await page.close()
} }
// 2026-10-23: the Model / Layout buttons share one width (their field face widens up to 378px with room), so they and // 2026-10-23: the Model / Layout buttons share one width (their field face widens up to 378px with room), so they and
// their field are left out of the size comparison with the reference; height and look are unchanged. // their field are left out of the size comparison with the reference. Height: the box matches the field group (42px), so the buttons are 37.5px (margin 1px top and sides, 1.5px bottom; geometry rounds to 38).
check('geometry tabs: equal widths, 40px high', comparison.package.tab.length === 2 && comparison.package.tab.every(item => item.h === 40 && Math.abs(item.w - comparison.package.tab[0].w) <= 1), comparison.package.tab.map(item => `${item.w}x${item.h}`).join(' ')) check('geometry tabs: equal widths, 37.5px high (box = field group 42px)', comparison.package.tab.length === 2 && comparison.package.tab.every(item => item.h === 38 && Math.abs(item.w - comparison.package.tab[0].w) <= 1), comparison.package.tab.map(item => `${item.w}x${item.h}`).join(' '))
if (comparison.reference) { if (comparison.reference) {
// Same item = same size (positions shift with the file-name text width, which differs by sample data). // Same item = same size (positions shift with the file-name text width, which differs by sample data).
const noTabs = items => items.filter(item => !item.text.startsWith('Model')) const noTabs = items => items.filter(item => !item.text.startsWith('Model'))
+67 -30
View File
@@ -1,7 +1,7 @@
import { useRef, useState, type ReactNode } from "react" import { useRef, useState, type CSSProperties, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite" import type { Meta, StoryObj } from "@storybook/react-vite"
import { import {
SWHeader, SWHeaderWindowControls, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection, SWHeader, SWHeaderWindowControls, SWTheme, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
type SWViewerHeaderGroup, type SWViewerMenuEntry, type SWViewerHeaderGroup, type SWViewerMenuEntry,
} from "@hm-design/sw" } from "@hm-design/sw"
import "@hm-design/sw/styles.css" import "@hm-design/sw/styles.css"
@@ -175,6 +175,43 @@ export const States: Story = {
}, },
} }
// 2026-10-23 Figma 7652:40344: the Model / Layout tabs alone, each selected state, Disabled and a color token override
// (CSS variables on SWTheme, as a consuming program sets them). Hover / Focus: point at or Tab to a button.
function TabsRow({ initial, disabled = false, style }: { initial: string; disabled?: boolean; style?: CSSProperties }) {
const [value, setValue] = useState(initial)
const groups: SWViewerHeaderGroup[] = [{
id: "space", label: "도면 공간", surface: "field", items: [
{ type: "tabs", id: "space", label: "모델과 배치 선택", value, disabled, onValueChange: setValue, options: [{ value: "model", label: "Model", count: 17377 }, { value: "layout", label: "Layout", count: 7377 }] },
],
}]
const header = <SWViewerHeader documentTitle="Model Layout.dwg" groups={groups} />
// Overrides go on the outermost SW scope (SWTheme), where the package declares the tokens.
return <div className="sw-story-scroller" data-tabs-case={initial + (disabled ? "-disabled" : style ? "-override" : "")}><div style={{ width: 1440 }}>{style ? <SWTheme style={style}>{header}</SWTheme> : header}</div></div>
}
const TAB_OVERRIDE = {
"--hm-sw-viewer-tabs-outline-color": "#4a7fd0",
"--hm-sw-viewer-tabs-glow-color": "rgba(74, 127, 208, 0.6)",
"--hm-sw-viewer-tab-gradient": "linear-gradient(180deg, #0b1830, #16305c 50%, #0b1830)",
"--hm-sw-viewer-tab-active-border": "linear-gradient(180deg, #b35f00, #ffb347 40%, #b35f00)",
"--hm-sw-viewer-tab-active-text": "#ffb347",
"--hm-sw-viewer-tab-inactive-border": "linear-gradient(#3d6bb3, #3d6bb3)",
"--hm-sw-viewer-tab-inactive-text": "#dbe7ff",
"--hm-sw-viewer-tab-highlight-color": "#5b8fe0",
} as CSSProperties
export const ModelLayoutTabs: Story = {
name: "Model / Layout",
render: () => (
<StoryPage>
<StoryPageHeader title="SW Viewer — Model / Layout" />
<StorySection title="Model 선택"><TabsRow initial="model" /></StorySection>
<StorySection title="Layout 선택"><TabsRow initial="layout" /></StorySection>
<StorySection title="Disabled"><TabsRow initial="model" disabled /></StorySection>
<StorySection title="색상 토큰 Override (--hm-sw-viewer-tab(s)-*)"><TabsRow initial="model" style={TAB_OVERRIDE} /></StorySection>
</StoryPage>
),
}
const MENU_ENTRIES: SWViewerMenuEntry[] = [ const MENU_ENTRIES: SWViewerMenuEntry[] = [
{ type: "label", id: "l-file", label: "파일" }, { type: "label", id: "l-file", label: "파일" },
{ type: "item", id: "open", label: "파일 열기", icon: <FolderOpen size={16} />, shortcut: "Ctrl+O" }, { type: "item", id: "open", label: "파일 열기", icon: <FolderOpen size={16} />, shortcut: "Ctrl+O" },
@@ -326,6 +363,34 @@ export const VariableSize: Story = {
}, },
} }
export const Logo: Story = {
name: "Logo",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
const rows: { title: string; props: { logoSrc?: string; logo?: ReactNode; logoAlt?: string } }[] = [
{ title: "로고 없음 (기본)", props: {} },
{ title: "logoSrc (이미지 URL)", props: { logoSrc: placeholderLogo, logoAlt: "임시 로고" } },
{ title: "logo (요소)", props: { logo: <svg viewBox="0 0 32 17" width="32" height="17" aria-hidden="true"><rect x="0.5" y="0.5" width="31" height="16" rx="3" fill="none" stroke="#fff" /><text x="16" y="12.5" fill="#fff" fontSize="10" fontWeight="700" textAnchor="middle">LOGO</text></svg>, logoAlt: "예시 로고" } },
]
return (
<StoryPage>
<StoryPageHeader title="SW Viewer — 로고" />
<div className="sw-story-stack">
{rows.map(row => (
<StorySection key={row.title} title={row.title}>
{/* 1440px is a maximum (2026-10-22): no scroller; the header fits the available width. */}
<div style={{ width: "100%", maxWidth: 1440 }} data-logo-case={row.title}>
<SWViewerHeader {...row.props} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
</div>
</StorySection>
))}
<Log lines={demo.log} />
</div>
</StoryPage>
)
},
}
export const Responsive: Story = { export const Responsive: Story = {
name: "Responsive More", name: "Responsive More",
render: () => { render: () => {
@@ -391,34 +456,6 @@ export const CompareWithSWHeader: Story = {
), ),
} }
export const Logo: Story = {
name: "Logo",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
const rows: { title: string; props: { logoSrc?: string; logo?: ReactNode; logoAlt?: string } }[] = [
{ title: "로고 없음 (기본)", props: {} },
{ title: "logoSrc (이미지 URL)", props: { logoSrc: placeholderLogo, logoAlt: "임시 로고" } },
{ title: "logo (요소)", props: { logo: <svg viewBox="0 0 32 17" width="32" height="17" aria-hidden="true"><rect x="0.5" y="0.5" width="31" height="16" rx="3" fill="none" stroke="#fff" /><text x="16" y="12.5" fill="#fff" fontSize="10" fontWeight="700" textAnchor="middle">LOGO</text></svg>, logoAlt: "예시 로고" } },
]
return (
<StoryPage>
<StoryPageHeader title="SW Viewer — 로고" />
<div className="sw-story-stack">
{rows.map(row => (
<StorySection key={row.title} title={row.title}>
{/* 1440px is a maximum (2026-10-22): no scroller; the header fits the available width. */}
<div style={{ width: "100%", maxWidth: 1440 }} data-logo-case={row.title}>
<SWViewerHeader {...row.props} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
</div>
</StorySection>
))}
<Log lines={demo.log} />
</div>
</StoryPage>
)
},
}
function ViewerOnly() { function ViewerOnly() {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name }) const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
// 2026-10-23: the same placeholder logo as the SWHeader row, so the two logo slots compare at the same width. // 2026-10-23: the same placeholder logo as the SWHeader row, so the two logo slots compare at the same width.