Author SHA1 Message Date
shkim4 8a09386b11 feat(sw): add step header process tokens and align docs 2026-11-10 17:25:00 +09:00
shkim4 2690a160e9 Merge pull request 'fix(sw): restore dropdown focus after position calculation' (#33) from fix/sw-property-layer-keyboard-focus into main
Deploy Storybook / deploy (push) Successful in 1m20s
Reviewed-on: #33
2026-11-10 15:35:24 +09:00
shkim4 d159ad58af fix(sw): restore dropdown focus after position calculation 2026-11-10 15:33:30 +09:00
shkim4 b6b7c656e3 Merge pull request 'fix(sw): restore object snap tracking outline and selected colors' (#32) from fix/sw-object-track-icon-outline into main
Deploy Storybook / deploy (push) Successful in 1m20s
Reviewed-on: #32
2026-11-10 15:24:57 +09:00
shkim4 6eae67628c fix(sw): restore object snap tracking outline and selected colors 2026-11-10 15:24:02 +09:00
shkim4 4fc37c7dd3 Merge pull request 'test(sw): align regression tests with current components' (#31) from fix/sw-tests-alignment into main
Deploy Storybook / deploy (push) Successful in 1m20s
Reviewed-on: #31
2026-11-10 14:54:29 +09:00
shkim4 b01ccaca01 test(sw): align regression tests with current components 2026-11-10 14:53:06 +09:00
shkim4 3cbe879910 Merge pull request 'feat(sw): add independent popup and more design tokens' (#30) from feat/sw-popup-more-tokens into main
Deploy Storybook / deploy (push) Successful in 1m21s
Reviewed-on: #30
2026-11-10 13:36:42 +09:00
shkim4 4c6fff5dea feat(sw): add independent popup and more design tokens 2026-11-10 13:33:27 +09:00
shkim4 39a089ad0e Merge pull request 'Feat/chart package' (#29) from feat/chart-package into main
Deploy Storybook / deploy (push) Successful in 1m21s
Reviewed-on: #29
2026-11-10 11:53:04 +09:00
20 changed files with 475 additions and 108 deletions

No files matched your search

+1 -1
View File
@@ -90,7 +90,7 @@ Collapsible의 trigger/content는 `asChild`로 자체 `button`/`div`를 써서 C
| 연결선 | 3뎁스만, 1px, 세로선 x 68, 가로 12, 글자까지 8, 행 세로 중앙, 마지막 행 중앙에서 끝 | 행마다 background 2겹(세로 `1px × 100%`, 마지막 행 `1px × 50%`, 가로 `12 × 1` at `calc(50% - 0.5px)`). DOM·tab stop·aria 없음 | MATCHED / IMPLEMENTATION_CHANGE(명세의 "배경 레이어" 방식) |
| 연결선 색 | 기본·hover `--hm-green-tint` 40%, 선택 `--hm-green-light-2` 40% | 같음. 선택 색은 선택된 행의 선에만 적용(나머지 행은 기본색) | MATCHED |
| 접힘 | 하위 메뉴·연결선 함께 숨김 | Radix가 내용을 unmount → 선도 같이 사라짐 | MATCHED |
| 중첩 그룹 | 배경·테두리·padding 중복, 폭 감소, 오른쪽 틈 금지 | 안쪽 그룹은 면·선·padding-bottom 없음, 폭 100%(바깥 그룹과 같은 252), 선택 항목 오른쪽 273(1px 겹침) | MATCHED |
| 중첩 그룹 | 배경·테두리·padding 중복, 폭 감소, 오른쪽 틈 금지 | 안쪽 그룹은 면·선·padding-bottom 없음, 폭 100%(바깥 그룹과 같은 220), 선택 항목 오른쪽 241(1px 겹침). 2026-11-10 240px 열 실측(Third Level, 1440); 이전 252/273은 272px 열 기준 | MATCHED |
| 안쪽 그룹 이름 위치 | 명세 수치 없음, 이미지상 2뎁스 글자와 같은 시작선 | trigger padding-left = 68 − 화살표 18 − gap 12 = 38 → 글자 x 68 | INTENTIONAL_CHANGE(이미지 기준) |
모바일(<1024): LNB ONLY는 Desktop 열을 숨기고 같은 메뉴를 독립 `LNBMobileHeader / LNBMobileMenu`로 표시한다(§3b). GNB ONLY 내부 Sidebar는 [GNB §15](gnb.md#15-gnb-only-sidebar-composition-2026-10-08)의 GNB 단일 Sheet를 사용하며 독립 LNB Mobile Header/Menu를 추가하지 않는다. DASHBOARD ONLY 운영 화면에는 Sidebar가 없다([Dashboard 현재 계약](dashboard-layout.md#1-현재-계약)).
+3 -2
View File
@@ -556,7 +556,8 @@ Composed Controls의 기존 레이어·색상·상세보기 예제는 보존하
| `--hm-sw-header-selected-color` | `#FFC600` | 선택 |
| `--hm-sw-header-focus-color` | `#F0F7F5` | Focus |
| `--hm-sw-header-arrow-color` | `#47AF91` | 파일명 화살표 |
| `--hm-sw-header-hover` | `rgba(255,255,255,.08)` | 더보기 메뉴 항목 Hover 배경 |
| `--hm-sw-header-hover` | `rgba(255,255,255,.08)` | 더보기 메뉴 항목 Hover 배경(`--hm-sw-header-more-item-hover-background` 기본값) |
| `--hm-sw-header-more-menu-background` / `-more-menu-shadow` / `-more-menu-border-color` / `-more-item-hover-background` | `#0E3D2F` / `0 4px 12px rgba(0,0,0,.5)` / `var(--hm-sw-header-object-border-color)` / `var(--hm-sw-header-hover)` | 더보기(`...`) 메뉴 면·그림자·테두리·항목 Hover(2026-11-10 개별 토큰, 값 변경 없음). 뷰어 Header Dropdown·로고 메뉴도 사용 |
| `--hm-sw-header-hover-border-gradient` / `-hover-border-width` | Figma 홈 hover gradient / 1px | Hover 테두리(§11.1) |
| `--hm-sw-header-home-on-line-color` / `-home-on-line-width` | `#5F4B0F` / 2px | Home On(로고 메뉴 열림, Figma 662:67267): Header 전폭 상·하단 선. 로고는 Hover 테두리 유지 |
| `--hm-sw-header-document-menu-width` / `-document-menu-background` / `-document-menu-border-color` / `-document-menu-border-width` | 310px / `#103127` / `#041611` / 1px | 문서 Dropdown(§5.1) |
@@ -575,7 +576,7 @@ Composed Controls의 기존 레이어·색상·상세보기 예제는 보존하
| `--hm-sw-header-layer-footer-border-color` / `-layer-detail-background` / `-layer-detail-color` / `-layer-detail-text-opacity` | `rgba(197,177,158,.15)` / `rgba(197,177,158,.15)` / `#FFF` / .35 | 레이어 상세 영역·버튼 |
| `--hm-sw-header-document-background` / `-document-border-color` | §10 B / `#0E4C3A` | 파일명 면, 홈 뒤, 창 영역, 인쇄 버튼(단색 선: 홈 뒤·창 영역) |
| `--hm-sw-header-document-border-gradient` | §7 | 파일명 면 테두리(SW Header·Viewer, Open 면 포함) |
| `--hm-sw-header-object-background` / `-object-border-color` / `-object-radius` | §10 A / `#135643` / 6px | 객체 수정 면, 더보기 메뉴 테두리(단색) |
| `--hm-sw-header-object-background` / `-object-border-color` / `-object-radius` | §10 A / `#135643` / 6px | 객체 수정 면, 더보기 메뉴 테두리(단색, `--hm-sw-header-more-menu-border-color` 기본값) |
| `--hm-sw-header-object-border-gradient` | §7 | 객체 수정 면 테두리(SW Header·Viewer, Viewer 끝 둥근 면 포함) |
| `--hm-sw-header-property-background` / `-property-border-color` / `-property-radius` / `-property-shadow` | §10 C / `#08251C` / 3px / §10 C | 5개 필드, 더보기 버튼 |
| `--hm-sw-header-pill-background` / `-pill-border-color` | `rgba(0,0,0,.8)` / `#092920` | 인쇄·가이드 pill |
+9 -1
View File
@@ -90,9 +90,17 @@ Strana의 2그룹(01 Modeling: Structure · Loads / 02 Analysis Result: Results)
전체 목록과 Strana 원본 키는 §7 표의 왼쪽 두 열을 봅니다.
**Step Header 전용 alias (2026-11-10).** Step Header 규칙이 칠하는 SWHeader 토큰 18개는 같은 이름의 `--hm-sw-process-*`(가장 바깥 SW 범위, 기본값 `var(--hm-sw-header-…)`)를 거쳐 읽습니다: `background`, `height`, `text-color`, `selected-color`, `hover-border-gradient`, `hover-border-width`, `document-background`, `document-border-gradient`, `document-border-color`, `object-background`, `object-border-gradient`, `object-radius`, `property-background`, `property-radius`, `property-shadow`, `window-icon-color`, `window-icon-hover-color`, `window-button-hover-background`. `--hm-sw-process-base-border-gradient`는 `--hm-sw-process-document-border-gradient`를 거쳐 같은 기본값을 씁니다.
- 기본(상속): `--hm-sw-process-*`가 없으면 가장 바깥 범위의 SWHeader 토큰 값이 그대로 적용됩니다. 계산값은 변경 전과 같습니다.
- Step Header만 변경: 가장 바깥 SW 범위에 `--hm-sw-process-*`를 지정합니다(SWHeader 토큰보다 우선, SWHeader·Viewer 영향 없음).
- 제약: alias는 가장 바깥 범위에서 계산되므로 안쪽 요소에서 바꾼 `--hm-sw-header-*`는 이 18개에 전달되지 않습니다.
- 공유 유지: 바 높이·필드·로고 높이, 글자 크기, 글자 외곽선, 재사용 부품(홈·로고, 파일명 버튼, 창 이동, 도구 pill)의 `sw.css` 규칙.
- `--hm-sw-process-text-color`는 Step Header 자체 글자 규칙(글자 로고, 레이어 필드)에만 적용됩니다. 루트의 기본 글자색은 SW 공통 규칙(`sw.css` `.hm-sw-header, .hm-sw-theme`)이 `--hm-sw-header-text-color`로 정하므로, 이를 상속하는 영역과 재사용 부품은 Header 토큰을 따릅니다.
## 7. Strana 테마 → 토큰 Override 대응표
Hanmac 열이 기본값입니다. 다른 테마는 Header의 `style`(가장 바깥 SW 범위)에 같은 토큰을 지정합니다. 예: `style={{ "--hm-sw-header-selected-color": "#FFC833" }}`. Strana ARGB `#AARRGGBB`는 CSS `rgba()`로 바꿉니다. 아래 표는 Strana `Styles/Theme/*/Colors.xaml`(HEAD `edda3cc`)에서 생성했습니다.
Hanmac 열이 기본값입니다. 다른 테마는 Header의 `style`(가장 바깥 SW 범위)에 같은 토큰을 지정합니다. 예: `style={{ "--hm-sw-header-selected-color": "#FFC833" }}`. Strana ARGB `#AARRGGBB`는 CSS `rgba()`로 바꿉니다. 아래 표는 Strana `Styles/Theme/*/Colors.xaml`(HEAD `edda3cc`)에서 생성했습니다. 표의 `--hm-sw-header-*` 중 §6의 18개는 Step Header만 바꿀 때 같은 이름의 `--hm-sw-process-*`(예: `--hm-sw-process-selected-color`)에 지정해도 됩니다(2026-11-10).
<!-- theme-table:start -->
| 토큰 | Strana `Color.Header.*` | Hanmac(기본값) | Dark | Light | HighContrast |
+2 -1
View File
@@ -121,7 +121,7 @@ import "@hm-design/sw/styles.css" // 앱 진입점에서 한 번
### 3.1 테마(토큰) 설정 — 공식 방법 (2026-10-20)
SW 토큰(`--hm-sw-header-*`, `--hm-sw-property-*`, `--hm-sw-viewer-*`, `--hm-sw-footer-*`)의 기본값은 **가장 바깥 SW 범위**(`<SWTheme>`, 또는 단독으로 쓴 `SWHeader`·`SWViewerHeader`·`SWViewerPanel`·`SWViewerMenu`·`SWFooter`의 루트)에서 한 번만 선언됩니다. 그 안의 SW 요소 — Header의 Dropdown·More, `SWViewerMenu`, `SWViewerPanel`과 안쪽 컨트롤, `SWFooterCommandLine`, Tooltip·안내 — 는 자기 루트에 `hm-sw-theme`이 있어도 값을 다시 정하지 않고 **상속**합니다.
SW 토큰(`--hm-sw-header-*`, `--hm-sw-property-*`, `--hm-sw-popup-*`, `--hm-sw-viewer-*`, `--hm-sw-footer-*`, `--hm-sw-process-*`, `--hm-sw-switch-*`, `--hm-sw-slider-*`, `--hm-sw-select-*`)의 기본값은 **가장 바깥 SW 범위**(`<SWTheme>`, 또는 단독으로 쓴 `SWHeader`·`SWProcessHeader`·`SWViewerHeader`·`SWViewerPanel`·`SWViewerMenu`·`SWFooter`의 루트)에서 한 번만 선언됩니다. 예외: 홈 메뉴 토큰(`--hm-sw-home-menu-*`, `--hm-sw-recent-files-*`)은 `SWHomeMenu` 루트(`.hm-sw-home-menu`)에 선언되어 `<SWTheme style>`로는 바뀌지 않습니다. `SWHomeMenu`의 `style`(SWHeader에서는 `homeMenu.style`)로 정합니다([DESIGN_RULES.md](./DESIGN_RULES.md) §12). 그 안의 SW 요소 — Header의 Dropdown·More, `SWViewerMenu`, `SWViewerPanel`과 안쪽 컨트롤, `SWFooterCommandLine`, Tooltip·안내 — 는 자기 루트에 `hm-sw-theme`이 있어도 값을 다시 정하지 않고 **상속**합니다.
1. 화면 전체(Header·Panel·Menu가 같은 값)를 바꾸려면 화면을 `<SWTheme style={theme}>` 하나로 감싸고 거기서만 토큰을 정합니다. 프로그램 class(`<SWTheme className="app-sw-theme">` + `.app-sw-theme { --hm-sw-header-font-family: … }`)도 됩니다.
```tsx
@@ -136,6 +136,7 @@ SW 토큰(`--hm-sw-header-*`, `--hm-sw-property-*`, `--hm-sw-viewer-*`, `--hm-sw
4. 영역마다 다른 값이 필요하면 영역별 `<SWTheme>`을 둡니다. 옆 영역끼리 섞이지 않고, 안쪽 `<SWTheme style>`은 그 안에서만 적용됩니다.
5. **하지 않음:** `hm-sw-*` class를 선택자로 쓴 CSS(`.hm-sw-header, .hm-sw-theme { --… }` 등), `!important`, 안쪽 컴포넌트마다 같은 값을 반복 지정, SW 범위가 아닌 조상(`body`, 앱 루트 `div`)에만 토큰 지정(SW 범위가 기본값을 다시 선언하므로 적용되지 않음).
- 다른 토큰을 참조하는 토큰(예: `--hm-sw-viewer-panel-background` = `var(--hm-sw-property-surface)`)은 가장 바깥 범위에서 계산됩니다. 참조되는 토큰을 바꿀 때도 가장 바깥 범위에서 바꿉니다.
- Step Header(`SWProcessHeader`, 2026-11-10): 칠하는 SWHeader 토큰 18개를 `--hm-sw-process-*` alias(기본값 `var(--hm-sw-header-…)`)로 읽습니다. 아무것도 정하지 않으면 SWHeader 테마를 따르고, `--hm-sw-process-selected-color` 등을 가장 바깥 범위에 정하면 Step Header만 바뀝니다. 위 규칙대로 안쪽 요소에서 바꾼 `--hm-sw-header-*`는 이 alias에 전달되지 않습니다. 목록·공유 유지 토큰은 [DESIGN_RULES.md](./DESIGN_RULES.md) §14.
## 4. 판단 기록 남기기
+16 -5
View File
@@ -71,6 +71,7 @@
- 목록 최대 높이 240px(8행), 넘치면 내부 스크롤. 색상 팔레트 118px.
- 속성창·K 가이드는 `position: fixed` Overlay(뷰포트 안 보정, Portal·Dim 없음).
- 한 번에 하나의 Dropdown(SWHeader). 단독 사용 시 각 컴포넌트가 바깥 클릭으로 스스로 닫힙니다.
- More(`...`) 메뉴 색(2026-11-10 개별 토큰, 값 변경 없음): 면 `--hm-sw-header-more-menu-background`(`#0E3D2F`), 그림자 `--hm-sw-header-more-menu-shadow`(`0 4px 12px rgba(0,0,0,.5)`), 테두리 `--hm-sw-header-more-menu-border-color`(기본 `var(--hm-sw-header-object-border-color)`), 항목 Hover `--hm-sw-header-more-item-hover-background`(기본 `var(--hm-sw-header-hover)`). 뷰어 Header의 Dropdown·로고 메뉴도 같은 토큰을 읽습니다([VIEWER.md](./VIEWER.md)).
## 6. Motion
@@ -102,11 +103,13 @@
| 항목 | 값 | 토큰 |
|---|---|---|
| 폭 / 높이 | 314px / 콘텐츠 높이(최대 화면 − 8px), 넘치면 본문 세로 스크롤 | — |
| 표면 / 테두리 / radius | `#E4DBC9` / 1px `#A67D03` / 4px, 안쪽 1px 검정 그림자 | `--hm-sw-property-surface`, `--hm-sw-property-border` |
| 제목줄 | 30px, Figma 제목 아트, 제목 14px 700 `#FFC600` + 검정 외곽선, 닫기 44 × 30(Hover 자산) | `--hm-sw-header-selected-color` |
| 표면 / 테두리 / radius | `#E4DBC9` / 1px `#A67D03` / 4px, 안쪽 1px 검정 그림자 | `--hm-sw-property-surface`, `--hm-sw-property-border`, `--hm-sw-popup-frame-shadow` |
| 제목줄 | 30px, Figma 제목 아트, 면 = 파일명 면 gradient, 제목 14px 700 `#FFC600` + 검정 외곽선, 닫기 44 × 30(Hover 자산) | `--hm-sw-popup-titlebar-background`(기본 `var(--hm-sw-header-document-background)`), `--hm-sw-popup-title-color`(기본 `var(--hm-sw-header-selected-color)`), `--hm-sw-popup-title-outline`(기본 `var(--hm-sw-header-label-outline)`) |
| 본문 | padding 7px, 그룹 사이 8px | — |
| 본문 스크롤바 | `thin`, Thumb `#9A9284` / Track `#E0DACE`(DWG/DXF Viewer 패널과 동일). 본문과 그 안의 스크롤 영역에 적용, 페이지 스크롤바는 그대로 | `--hm-sw-popup-scrollbar-thumb`, `--hm-sw-popup-scrollbar-track` |
| 그룹 면(`SWPopupSection`) | 제목 700 · 검정 70%, 내용 면 `#F1ECE1`, 1px `#585349`, radius 4px | `--hm-sw-property-content` |
| 그룹 면(`SWPopupSection`) | 제목 700 · 검정 70%, 내용 면 `#F1ECE1`, 1px `#585349`, radius 4px, 안쪽 입체 그림자 | `--hm-sw-property-content`, `--hm-sw-popup-group-heading-color`, `--hm-sw-popup-group-border-color`, `--hm-sw-popup-group-shadow` |
| 구분선 | 개수 목록 아래선 검정 50%, 위치 표 선 검정 40% | `--hm-sw-popup-divider-color`, `--hm-sw-popup-table-line-color` |
| Switch 그림자 | Off·On 안쪽 그림자 + 테두리 링 + 1px `#FCF0D8` 하이라이트, Thumb `0 .5px 1px rgba(0,0,0,.6)` | `--hm-sw-switch-off-shadow`, `--hm-sw-switch-on-shadow`, `--hm-sw-switch-thumb-shadow` |
| 속성 행(`SWPopupPropertyRow`) | 속성창 행 그대로(라벨 72px 700, 값 상자 28px, `picker` 필드). 데이터 = `SWHeaderProperty` + `menus` | `--hm-sw-property-*`, `--hm-sw-property-field-*` |
| 행(`SWPopupRow`) | 최소 32px, 라벨 72px 700 검정, 값 글자 `#E60403` 500, 단위 12px | `--hm-sw-property-label`, `--hm-sw-header-input-color` |
| 값 상자 | 28px, Hover 1px `rgba(33,29,19,.4)`, 숫자 편집 On `#0F0D0A`(단위 흰색), Dropdown On `#211D13` | `--hm-sw-property-field-*` |
@@ -139,7 +142,7 @@
5. 값은 Props, 동작은 Callback. CAD 데이터를 내부에서 바꾸지 않는다.
6. `hm-sw-*` class를 새로 쓰거나 덮어쓰지 않는다(자기 접두사 사용, 예: `app-`).
7. 버튼·탭·토글이면 먼저 `SWToolButton`의 세 모양(§9)으로 해결되는지 본다. 팝업·설정창이면 `SWPopup` + `SWPopupSection` / `SWPopupRow`(§8).
8. 팝업 안의 새 콘텐츠는 §8의 Popup 토큰(`--hm-sw-property-*`)만 읽는다. Header용 어두운 토큰을 밝은 Popup 면에 섞지 않는다.
8. 팝업 안의 새 콘텐츠는 §8의 Popup 토큰(`--hm-sw-property-*`, `--hm-sw-popup-*`)만 읽는다. Header용 어두운 토큰을 밝은 Popup 면에 섞지 않는다. Popup 제목줄 토큰의 기본값은 Header 토큰을 따르므로 Header 토큰을 바꾼 테마는 그대로 적용되고, Popup만 바꿀 때는 `--hm-sw-popup-*`를 지정한다(2026-11-10).
## 11. 뷰어 모드 토큰·상태 요약 (`SWViewer*` 전용)
@@ -190,6 +193,7 @@
- Home On(Figma 662:67267, 2026-10-19 사용자 확정): 메뉴가 열려 있는 동안 SWHeader가 자동으로 로고에 노란 Hover 테두리를 유지하고, Header 전폭 상·하단에 `#5F4B0F` 2px 선(`--hm-sw-header-home-on-line-color` / `-width`)을 그립니다. 닫으면 기본 상태로 돌아갑니다. 크기·위치 변화가 없고 SW 일반 모드 전용입니다(뷰어 모드에는 없음). 소비 코드에서 별도 class나 상태를 추가하지 않습니다.
- 새 색·크기는 위 토큰만 `style`로 덮어씁니다. `hm-sw-home-menu*` class를 소비 코드에 쓰지 않습니다.
- 덮어쓰는 위치(2026-11-10 구현 확인): 홈 메뉴 토큰 39개(`--hm-sw-home-menu-*` 28, `--hm-sw-recent-files-*` 11)는 가장 바깥 SW 범위가 아니라 `SWHomeMenu` 루트(`.hm-sw-home-menu`, `sw-home-menu.css`)에 선언됩니다. 루트가 값을 다시 정하므로 `<SWTheme style>`이나 `SWHeader`의 `style`로 지정한 `--hm-sw-home-menu-*`·`--hm-sw-recent-files-*`는 적용되지 않습니다. `SWHomeMenu`의 `style`, 또는 `SWHeader`에서는 `homeMenu.style`(그대로 `SWHomeMenu`에 전달)로 지정합니다. 단, 홈 메뉴 토큰 중 `--hm-sw-header-layer-*`·`--hm-sw-header-text-color`를 참조하는 값(더보기 Dropdown·최근파일 면·행 Hover 등)은 홈 메뉴 요소에서 계산되므로 바깥 SW 범위에서 그 Header 토큰을 바꾸면 따라 바뀝니다. 선언 위치는 현재 구조를 유지합니다.
## 13. Footer (`SWFooter*`, Figma 26:10097) — SW 일반 모드 전용
@@ -201,7 +205,7 @@
| 배치(1920, Figma) | 보조기능 버튼 0~74 → 2 → 보조기능 격자 76~392(위 2) → 4 → 명령 그룹 396~(위 1) → 명령어 전체보기(명령 칸 바로 뒤, 1552~1626) → 나머지 bg_vacant | — |
| 측면 버튼 | 74 × 71, 자산 면(그라데이션 + 그라데이션 테두리), 그림자 8px 60% 검정(왼쪽 버튼 → 오른쪽 5px, 오른쪽 버튼 → 왼쪽 5px). 제목 14px 1.3px 외곽선, 부제 14px 1px 외곽선, 위 18 · 줄 간격 2. Hover·Pressed·Expanded = 제목·부제 `#FF5C00` + 1px 주황 그라데이션 테두리(`--hm-sw-footer-gra-border-orange`), Disabled 기본 모양 | `--hm-sw-footer-side-*` |
| 보조기능 | 104 × 33, gap 2, radius 3, 면 `linear-gradient(0deg, #262218 8%, #4A3C2B 34%, #594C31 55%, #312A1B 90%)`, 아이콘 중심 (19.5, 16.5), 라벨 13px 왼쪽 36 폭 57 가운데. 테두리 1px 그라데이션(위 → 아래): Default `#886D35 0%, #423625 8.85%, #F3DBA8 26.17%, #0E0B06 83.79%, #574B30 100%`(Figma btn_* 26:9248 …) | `--hm-sw-footer-aux-*`, `--hm-sw-footer-aux-border` |
| 보조기능 상태 | Hover(선택 아님) = 테두리만 노랑 `--hm-sw-header-hover-border-gradient`(`#A67D03 → #F2CC0F → #FADD4F → #F2CC0F → #926E03`, SW Header Figma Hover 재사용 — 버튼 세트에는 Hover 없음, 사용자 확정 2026-10-20). Selected(`aria-pressed="true"`, Figma Variant2 = Variant3) = 테두리 `#DA6813 8.52%, #FFBB89 37.93%, #E5690D 71.85%, #AE3607 92.77%`(Figma `gra-border-orange` = `--hm-sw-footer-gra-border-orange`) + 아이콘 채움·글자 `#FF5C00`, Hover보다 우선. 아이콘 외곽선 검정 90% 1px는 모든 상태 동일 | `--hm-sw-footer-aux-hover-border`, `--hm-sw-footer-aux-pressed-border`, `--hm-sw-footer-aux-icon-color`(`#F1EDE5`) |
| 보조기능 상태 | Hover(선택 아님) = 테두리만 노랑 `--hm-sw-header-hover-border-gradient`(`#A67D03 → #F2CC0F → #FADD4F → #F2CC0F → #926E03`, SW Header Figma Hover 재사용 — 버튼 세트에는 Hover 없음, 사용자 확정 2026-10-20). Selected(`aria-pressed="true"`, Figma Variant2 = Variant3) = 테두리 `#DA6813 8.52%, #FFBB89 37.93%, #E5690D 71.85%, #AE3607 92.77%`(Figma `gra-border-orange` = `--hm-sw-footer-gra-border-orange`) + 아이콘 채움·글자 `#FF5C00`, Hover보다 우선. 선택 면 = `--hm-sw-footer-aux-pressed-background`(`linear-gradient(0deg, #594931 0%, #3B3021 23%, #1B170F 58%, #0D0B07 100%)`, Figma 없음 — 실제 프로그램 화면 샘플링 근사값, 사용자 확정 2026-11-09). 아이콘 외곽선 검정 90% 1px는 모든 상태 동일 | `--hm-sw-footer-aux-hover-border`, `--hm-sw-footer-aux-pressed-border`, `--hm-sw-footer-aux-pressed-background`, `--hm-sw-footer-aux-icon-color`(`#F1EDE5`) |
| 명령 그룹 면 | 테두리 위·왼쪽·아래 1px `#886D35`, 왼쪽 위 radius 4, 면 `linear-gradient(0deg, #2F281A 0%, #312A1B 5%, #332B1C 36%, #463C29 58%, #38301F 79%, #19160E 98%)`. 마지막 명령 뒤(빈 칸·나머지)는 bg_vacant `linear-gradient(0deg, #18150E 28%, #262015 41%, #332B1C 58%, #38301F 81%, #18150E 100%)`, 명령 칸 높이까지만(2026-10-20: 분류 행 아래로 내려가지 않아 선택 탭 Pocket 면이 어느 탭에서나 같음) | `--hm-sw-footer-group-*`, `--hm-sw-footer-vacant-background` |
| 명령 칸 | 68 × 47, 라벨 14px `#F6F4F2` 위 6(좌우 2, 넘치면 `…` 말줄임 — 칸·글자 크기 그대로, 2026-10-20), 아이콘 18 × 18 위 26 가운데. 칸 사이 구분선 1 × 37(위 5) `linear-gradient(0deg, #20190E, #8A7A5B 45%, #F3DBA8 70%, #423625)` + `-1px 0 1px #000` | `--hm-sw-footer-slot-*`, `--hm-sw-footer-divider*` |
| 명령 상태 | Hover = 선택(Figma Variant2 = Variant3): 글자 `#FF5C00` + 주황 아이콘. 색·아이콘만 바뀌고 아이콘·라벨 위치·크기·간격과 칸 크기는 그대로. Disabled 40%(Figma 없음) | `--hm-sw-footer-accent` |
@@ -223,6 +227,13 @@
원본 `src/theme/sw-process-header.css`(2026-11-03). Strana Main Header(Hanmac 테마)의 값입니다. 새 토큰은 `--hm-sw-process-*`(`.hm-sw-header`, `.hm-sw-theme` 범위)이고, 값이 같은 곳은 SW Header 토큰을 그대로 읽습니다(파일명 면·테두리, 객체 면·테두리, 속성 필드 면·그림자, Hover gradient, 선택 `#FFC600`, 제목·글자, 창 이동 줄무늬, 창 버튼 글리프 `#ADB0AF`·Hover 배경). Light/Dark preset은 없으며 다른 Strana 테마는 가장 바깥 범위 `style`에 같은 토큰을 지정합니다([AI_USAGE.md](./AI_USAGE.md) §3.1).
Step Header 전용 토큰(2026-11-10): Step Header 규칙이 칠하는 SWHeader 토큰 18개는 같은 이름의 `--hm-sw-process-*`를 거쳐 읽습니다 — `background`·`height`·`text-color`·`selected-color`·`hover-border-gradient`·`hover-border-width`·`document-background`·`document-border-gradient`·`document-border-color`·`object-background`·`object-border-gradient`·`object-radius`·`property-background`·`property-radius`·`property-shadow`·`window-icon-color`·`window-icon-hover-color`·`window-button-hover-background`(예: `--hm-sw-process-selected-color`). 기본값은 각각 `var(--hm-sw-header-…)`라 계산값은 그대로입니다. `--hm-sw-process-base-border-gradient`도 `--hm-sw-process-document-border-gradient`를 거쳐 같은 기본값을 씁니다.
- 기본 동작(상속): `--hm-sw-process-*`를 정하지 않으면 가장 바깥 SW 범위의 `--hm-sw-header-*` 값이 Step Header에도 적용됩니다.
- Step Header만 바꾸기: 가장 바깥 SW 범위(`<SWTheme style>` 또는 단독 `SWProcessHeader`의 `style`)에 `--hm-sw-process-*`를 지정합니다. SWHeader·Viewer에는 영향이 없습니다.
- 제약: alias는 가장 바깥 범위에서 계산되므로 안쪽 요소(예: `<SWTheme>` 안 `SWProcessHeader`의 `style`)에서 `--hm-sw-header-*`만 바꾸면 이 18개에는 전달되지 않습니다. 이 경우 같은 위치에 `--hm-sw-process-*`를 지정하거나 바깥 범위에서 바꿉니다.
- 공유 유지: 바 높이(`--hm-sw-header-bar-height`)·필드·로고 높이, 글자 크기, 글자 외곽선(`text-outline`·`label-outline`·`text-outline-room`)과 재사용 부품(홈·로고, 파일명 버튼, 창 이동, 도구 pill)의 `sw.css` 규칙은 SWHeader 토큰을 그대로 읽습니다.
- `--hm-sw-process-text-color` 범위: Step Header 자체 글자 규칙(글자 로고, 레이어 필드)에만 적용됩니다. Step Header 루트의 기본 글자색은 SW 공통 규칙(`sw.css`의 `.hm-sw-header, .hm-sw-theme { color: var(--hm-sw-header-text-color) }`)이 정하므로, 그 색을 상속하는 영역과 재사용 부품의 글자는 `--hm-sw-header-text-color`를 따릅니다.
| 항목 | 값 | 토큰 |
|---|---|---|
| 배치 | 홈·파일명 글자 = SWHeader 그대로(두 타입 공통, 2026-11-03): 홈 12 + 로고(32 × 17 → 80) + 36, 파일명은 홈과 15 겹친 뒤 2 간격(x 65), padding 5 / 12, 14px 700·자간 0.06em·SW 외곽선, 화살표 없음(글자 로고 18px 700, 최소 58px 칸 → 홈 최소 106, 더 긴 글자는 글자 폭만큼 넓어짐) → 프로세스 → Layer 면(객체 면, 양옆 1) → 창 영역(파일명 면 + 테두리 1px) | SW 토큰 |
+1 -1
View File
@@ -45,7 +45,7 @@
- **반응형(2026-10-21, 실제 가용 폭 기준 3단계):** 고정 Breakpoint를 쓰지 않고 Header 행의 실제 남는 폭(로고·파일명·메뉴·Gap·Padding·More 버튼 포함)을 측정합니다. 파일명 면은 글자 수가 아니라 Header 폭으로 정한 기본 폭으로 계산하므로, 같은 폭에서는 짧은 파일명과 긴 파일명의 단계·파일 열기 버튼·파일명 면 너비가 같고 긴 이름은 `…`입니다(2026-10-21). ① 모두 들어가면 기본 형태(아이콘+텍스트), ② 부족하면 `icon`과 `iconOnlyBelow`가 있는 항목을 모두 아이콘만(라벨은 Tooltip·접근성 이름), ③ 그래도 부족하면 `collapseBelow` 항목을 하나씩 More(⋯, 베이지 260px Dropdown)로 옮깁니다(값이 큰 것부터, 같으면 오른쪽 먼저). ③의 판단(2026-10-26)은 tabs를 최소 폭(`--hm-sw-viewer-tabs-min-width`)으로 세므로, tabs가 `…`로 좁아져서 들어가면 More로 옮기지 않습니다. 파일명 면 기본 폭은 `clamp(80px, min(45cqw - 256px, 100cqw - 780px), 360px)`(1280px 이상은 이전과 같음), Header 560px 미만에서는 56px입니다. 넓어지면 More → 아이콘 → 기본 순서로 복귀하고, 상단과 More에 같은 항목이 동시에 나오지 않습니다. `collapseBelow`·`iconOnlyBelow`의 숫자는 px Breakpoint가 아니며 순서만 정합니다(`iconOnlyBelow`는 지정 여부만, `collapseBelow`는 값이 큰 것부터; Props 형식 동일). 로고 옆 파일 열기 버튼(`onDocumentOpen`)은 ② 단계에서 아이콘만으로 줄고 More로 옮기지 않으며, 샘플은 `documents`. 레이어·설정·속성은 `collapseBelow` 없이 아이콘까지만. 1180px 미만 간격 2px, 560px 미만 상태 글자 숨김(CSS container)은 그대로이고, 560px 미만에서는 tabs 개수 숨김·switch `shortLabel` 캡션·switch 좌우 여백 1px도 적용됩니다(2026-10-26). 가로 스크롤은 만들지 않습니다.
- More는 그룹/항목 단위 구획으로 자동 구성됩니다: menu·tabs는 자기 구획(`moreLabel`), button·toggle·switch는 그룹 구획(`group.moreLabel ?? group.label`). toggle·switch는 체크 항목, tabs는 라디오 항목이 됩니다.
- **Dropdown 색(2026-10-21):** Header 안의 Dropdown — 메뉴 버튼(`type: "menu"`), More(⋯), 로고 메뉴 — 은 크기·간격·글자는 `SWViewerMenu` 그대로, 색은 일반 `SWHeader`의 `...` 메뉴와 같습니다(면 #0e3d2f, Border `--hm-sw-header-object-border-color`, 그림자 0 4px 12px rgba(0,0,0,.5), 글자 `--hm-sw-header-text-color`, 보조 글자는 같은 색 80%, Hover `--hm-sw-header-hover`, 구분선 `--hm-sw-header-document-menu-divider-color`). 파일명 목록(`documents`)은 `SWHeader` 파일명 목록 모양을 유지하고, 단독 `SWViewerMenu`(우클릭 메뉴 등)는 기존 색 그대로입니다.
- **Dropdown 색(2026-10-21):** Header 안의 Dropdown — 메뉴 버튼(`type: "menu"`), More(⋯), 로고 메뉴 — 은 크기·간격·글자는 `SWViewerMenu` 그대로, 색은 일반 `SWHeader`의 `...` 메뉴와 같은 More 토큰을 읽습니다(2026-11-10: 면 `--hm-sw-header-more-menu-background` #0e3d2f, Border `--hm-sw-header-more-menu-border-color` = `var(--hm-sw-header-object-border-color)`, 그림자 `--hm-sw-header-more-menu-shadow` 0 4px 12px rgba(0,0,0,.5), 글자 `--hm-sw-header-text-color`, 보조 글자는 같은 색 80%, Hover `--hm-sw-header-more-item-hover-background` = `var(--hm-sw-header-hover)`, 구분선 `--hm-sw-header-document-menu-divider-color`). 파일명 목록(`documents`)은 `SWHeader` 파일명 목록 모양을 유지하고, 단독 `SWViewerMenu`(우클릭 메뉴 등)는 기존 색 그대로입니다.
- **상태 표현:** Hover·Active·Pressed·열림·선택 탭 = 글자·아이콘 #FFC600(면·선택 박스 없음, Hover 전후 크기·위치 동일 — 2026-10-21 Hover 면 #266651 제거), Disabled = 60% 불투명, Focus = 2px #FFC600 외곽선.
- **글자·아이콘 외곽선(SWHeader와 동일):** 14px 글자 `--hm-sw-header-text-outline`(검정 1.3px), 11px 캡션·개수·상태 `--hm-sw-header-label-outline`(검정 1px), 아이콘 `SWViewerIcon`(SWHeader 아이콘 자산의 1px 검정 바깥 Stroke를 **SVG stroke**로 재현: 아이콘 뒤 복사본의 모든 도형을 검정, 원래 선 두께 + 화면 기준 양쪽 1px(`--hm-sw-viewer-icon-outline-width`)로 그림. 위 아이콘은 그대로라 색·모양·선 두께 유지. CSS `filter` 미사용, glow는 생략). Header 항목·`SWViewerHeaderButton`의 `icon`은 자동 적용, `custom` / `trailing`의 직접 그린 아이콘은 `<SWViewerIcon>`으로 감쌉니다. 색·크기·굵기·간격은 그대로이고 Hover/Active는 채움 색만 바뀝니다. Dropdown·패널 안에는 외곽선이 없습니다(SWHeader More 메뉴와 같음). 프로그램 아이콘은 `currentColor`로 그리면 상태 색과 외곽선이 자동 적용됩니다.
@@ -109,11 +109,16 @@ export function useSWHeaderDropdown({ open: openProp, onOpenChange, disabled = f
/** Focusable entries of the panel in DOM order (rows, inputs, footer). */
const entries = () => Array.from(panelRef.current?.querySelectorAll<HTMLElement>("button:not(:disabled), input:not(:disabled)") ?? [])
// ArrowDown on the closed trigger asks for the entry focus. A fixed overlay (property window, SW popup) stays
// visibility: hidden until its position is measured, and focus() on it is a no-op: the request stays pending and is
// retried on the render that shows the list (the position update), then cleared once focus moved; closing drops it.
useEffect(() => {
if (open && focusEntryRef.current) {
focusEntryRef.current = false
if (!open) { focusEntryRef.current = false; return }
if (focusEntryRef.current) {
const current = panelRef.current?.querySelector<HTMLElement>('[aria-current="true"]:not(:disabled), [aria-pressed="true"]:not(:disabled)')
;(current ?? entries()[0])?.focus()
const target = current ?? entries()[0]
target?.focus()
if (!target || document.activeElement === target) focusEntryRef.current = false
}
})
+3 -1
View File
@@ -25,6 +25,8 @@
--hm-sw-footer-aux-height: 33px;
--hm-sw-footer-aux-gap: 2px;
--hm-sw-footer-aux-background: linear-gradient(0deg, #262218 8%, #4a3c2b 34%, #594c31 55%, #312a1b 90%);
/* Selected face (not in Figma: sampled from the reference program screen, user decision 2026-11-09; stops approximate). */
--hm-sw-footer-aux-pressed-background: linear-gradient(0deg, #594931 0%, #3b3021 23%, #1b170f 58%, #0d0b07 100%);
--hm-sw-footer-aux-radius: 3px;
--hm-sw-footer-aux-font-size: 13px;
/* 1px borders (Plugin API, btn_객체스냅 26:9248 … btn_선가중치 26:9412): Default = brown gradient, Variant2 / Variant3
@@ -242,7 +244,7 @@
/* Selected (Figma Variant2 / Variant3): orange border, icon and text together; it stays while hovered and after
the pointer leaves. Icons follow through currentColor (swFooterAuxIcons, or any currentColor icon). */
.hm-sw-footer-aux__button[aria-pressed="true"] {
background: var(--hm-sw-footer-aux-background) padding-box, var(--hm-sw-footer-aux-pressed-border) border-box;
background: var(--hm-sw-footer-aux-pressed-background) padding-box, var(--hm-sw-footer-aux-pressed-border) border-box;
}
.hm-sw-footer-aux__button[aria-pressed="true"] :is(.hm-sw-footer-aux__icon, .hm-sw-footer-aux__label) { color: var(--hm-sw-footer-accent); }
+53 -29
View File
@@ -7,7 +7,7 @@
:is(.hm-sw-header, .hm-sw-theme):not(:where(.hm-sw-header, .hm-sw-theme) *) {
/* Process area base (Header.Border.ProcessBase): ProcessBase gradient, DocBorder 1px, top corners 6. */
--hm-sw-process-base-background: linear-gradient(180deg, #0c5941 0%, #19bf8d 10.1%, #109069 18.27%, #0c5941 100%);
--hm-sw-process-base-border-gradient: var(--hm-sw-header-document-border-gradient);
--hm-sw-process-base-border-gradient: var(--hm-sw-process-document-border-gradient);
/* Cover over an unlit group, its arrow side and its outer rest (ProcessDim #E6004430). */
--hm-sw-process-dim: rgba(0, 68, 48, 0.902);
@@ -93,6 +93,30 @@
--hm-sw-process-multi-tab-disabled-border: #021912;
--hm-sw-process-multi-tab-disabled-shadow: 1px 1px 1px 0 rgba(31, 148, 113, 0.7), inset 3px 2px 3px 0 rgba(9, 39, 30, 0.42), inset 3px 2px 3px 0 rgba(9, 39, 30, 0.42);
--hm-sw-process-multi-tab-disabled-text: #7d9f91;
/* Step Header's own copies of the SWHeader tokens it paints with (2026-11-10). Each defaults to the SWHeader token, so a
theme that sets --hm-sw-header-* on the outermost SW scope still reaches the Step Header; set --hm-sw-process-* there to
change the Step Header alone. Resolved on the outermost scope like every alias here: a --hm-sw-header-* value set on an
inner element no longer reaches these. The reused SW parts (home / logo, file-name button, window move) keep reading
--hm-sw-header-* in sw.css. */
--hm-sw-process-background: var(--hm-sw-header-background);
--hm-sw-process-height: var(--hm-sw-header-height);
--hm-sw-process-text-color: var(--hm-sw-header-text-color);
--hm-sw-process-selected-color: var(--hm-sw-header-selected-color);
--hm-sw-process-hover-border-gradient: var(--hm-sw-header-hover-border-gradient);
--hm-sw-process-hover-border-width: var(--hm-sw-header-hover-border-width);
--hm-sw-process-document-background: var(--hm-sw-header-document-background);
--hm-sw-process-document-border-gradient: var(--hm-sw-header-document-border-gradient);
--hm-sw-process-document-border-color: var(--hm-sw-header-document-border-color);
--hm-sw-process-object-background: var(--hm-sw-header-object-background);
--hm-sw-process-object-border-gradient: var(--hm-sw-header-object-border-gradient);
--hm-sw-process-object-radius: var(--hm-sw-header-object-radius);
--hm-sw-process-property-background: var(--hm-sw-header-property-background);
--hm-sw-process-property-radius: var(--hm-sw-header-property-radius);
--hm-sw-process-property-shadow: var(--hm-sw-header-property-shadow);
--hm-sw-process-window-icon-color: var(--hm-sw-header-window-icon-color);
--hm-sw-process-window-icon-hover-color: var(--hm-sw-header-window-icon-hover-color);
--hm-sw-process-window-button-hover-background: var(--hm-sw-header-window-button-hover-background);
}
/* ---------- Header frame ---------- */
@@ -102,8 +126,8 @@
display: block;
width: 100%;
min-width: 0;
height: var(--hm-sw-header-height);
background: var(--hm-sw-header-background);
height: var(--hm-sw-process-height);
background: var(--hm-sw-process-background);
overflow: clip;
}
.hm-sw-process-header__bar { display: flex; align-items: stretch; height: var(--hm-sw-header-bar-height); min-width: 0; }
@@ -120,7 +144,7 @@
justify-content: center;
width: max-content;
min-width: 58px;
color: var(--hm-sw-header-text-color);
color: var(--hm-sw-process-text-color);
font-size: 18px;
font-weight: 700;
line-height: var(--hm-sw-header-logo-height);
@@ -157,8 +181,8 @@
position: absolute;
inset: 0 1px;
border: 1px solid transparent;
border-radius: var(--hm-sw-header-object-radius);
background: var(--hm-sw-header-object-background) padding-box, var(--hm-sw-header-object-border-gradient) border-box;
border-radius: var(--hm-sw-process-object-radius);
background: var(--hm-sw-process-object-background) padding-box, var(--hm-sw-process-object-border-gradient) border-box;
}
/* Layer field (Header.ToggleButton.LayerField): 38px, at most 234, property surface + layer border gradient.
The bulb and the field are sibling buttons; the field button covers the whole field under the bulb. */
@@ -174,18 +198,18 @@
margin: 0 9px;
padding: 0 10px 0 12px;
border: 1px solid transparent;
border-radius: var(--hm-sw-header-property-radius);
background: var(--hm-sw-header-property-background) padding-box, var(--hm-sw-process-layer-border-gradient) border-box;
box-shadow: var(--hm-sw-header-property-shadow);
color: var(--hm-sw-header-text-color);
border-radius: var(--hm-sw-process-property-radius);
background: var(--hm-sw-process-property-background) padding-box, var(--hm-sw-process-layer-border-gradient) border-box;
box-shadow: var(--hm-sw-process-property-shadow);
color: var(--hm-sw-process-text-color);
}
.hm-sw-process-header__layer-field::after {
content: "";
position: absolute;
inset: -1px;
padding: var(--hm-sw-header-hover-border-width);
padding: var(--hm-sw-process-hover-border-width);
border-radius: inherit;
background: var(--hm-sw-header-hover-border-gradient);
background: var(--hm-sw-process-hover-border-gradient);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
@@ -195,7 +219,7 @@
.hm-sw-process-header__layer-field:not([data-disabled]):hover::after,
.hm-sw-process-header__layer-field[data-open]::after { opacity: 1; }
.hm-sw-process-header__layer-field:not([data-disabled]):hover,
.hm-sw-process-header__layer-field[data-open] { color: var(--hm-sw-header-selected-color); }
.hm-sw-process-header__layer-field[data-open] { color: var(--hm-sw-process-selected-color); }
.hm-sw-process-header__layer-field[data-disabled] { opacity: 0.4; }
.hm-sw-process-header .hm-sw-process-header__layer-field button:disabled { opacity: 1; }
.hm-sw-process-header__layer-button { position: absolute; inset: -1px; z-index: 0; border-radius: inherit; }
@@ -221,7 +245,7 @@
position: absolute;
inset: 0;
border: 1px solid transparent;
background: var(--hm-sw-header-document-background) padding-box, var(--hm-sw-header-document-border-gradient) border-box;
background: var(--hm-sw-process-document-background) padding-box, var(--hm-sw-process-document-border-gradient) border-box;
pointer-events: none;
}
.hm-sw-process-header__window > * { position: relative; }
@@ -238,11 +262,11 @@
}
.hm-sw-process-header:not([data-appearance="multi"]) .hm-sw-process-header__tools .hm-sw-tool__pill-button {
border-color: transparent;
background: var(--hm-sw-header-document-background) padding-box, var(--hm-sw-process-tool-border-gradient) border-box;
background: var(--hm-sw-process-document-background) padding-box, var(--hm-sw-process-tool-border-gradient) border-box;
}
.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button { color: var(--hm-sw-process-tool-icon-color); }
.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button:hover:not(:disabled),
.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button[aria-pressed="true"] { color: var(--hm-sw-header-selected-color); }
.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button[aria-pressed="true"] { color: var(--hm-sw-process-selected-color); }
.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button[aria-pressed="true"]::after { opacity: 1; }
/* Pressed (not disabled) shows the hover picture too, like a hover that stays (SW pill hover icon = `hoverIcon`). */
.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button[aria-pressed="true"]:not(:disabled) > .hm-sw-header__icon { visibility: hidden; }
@@ -251,9 +275,9 @@
.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__icon > :is(svg, img) { width: 24px; height: 24px; max-width: none; max-height: none; }
/* Caption buttons (Header.WindowCaption, 32 x 48): glyph #ADB0AF; hover dark + white; close hover red + white. */
.hm-sw-process-header__caption { display: block; flex: 0 0 32px; width: 32px; height: var(--hm-sw-header-bar-height); color: var(--hm-sw-header-window-icon-color); }
.hm-sw-process-header__caption { display: block; flex: 0 0 32px; width: 32px; height: var(--hm-sw-header-bar-height); color: var(--hm-sw-process-window-icon-color); }
.hm-sw-process-header__caption > svg { display: block; }
.hm-sw-process-header__caption:hover:not(:disabled) { background: var(--hm-sw-header-window-button-hover-background); color: var(--hm-sw-header-window-icon-hover-color); }
.hm-sw-process-header__caption:hover:not(:disabled) { background: var(--hm-sw-process-window-button-hover-background); color: var(--hm-sw-process-window-icon-hover-color); }
.hm-sw-process-header__caption[data-kind="close"]:hover:not(:disabled) { background: var(--hm-sw-process-close-hover-background); color: var(--hm-sw-process-close-icon-hover); }
/* ---------- Process area (SWProcessBar) ---------- */
@@ -374,8 +398,8 @@
.hm-sw-process-bar__step-box > [data-layer="inset-left"] { background: linear-gradient(90deg, var(--hm-sw-process-tab-inset) 0, transparent 6px); }
.hm-sw-process-bar__step-box > [data-layer="ring"] { border: 2px solid var(--hm-sw-process-tab-ring); }
.hm-sw-process-bar__step:hover:not(:disabled) { color: var(--hm-sw-header-selected-color); }
.hm-sw-process-bar__step[aria-checked="true"] { color: var(--hm-sw-header-selected-color); }
.hm-sw-process-bar__step:hover:not(:disabled) { color: var(--hm-sw-process-selected-color); }
.hm-sw-process-bar__step[aria-checked="true"] { color: var(--hm-sw-process-selected-color); }
.hm-sw-process-bar__step[aria-checked="true"] > .hm-sw-process-bar__step-box { opacity: 1; }
.hm-sw-process-bar__step[aria-checked="true"] [data-layer="depth"] { background: var(--hm-sw-process-tab-active-base); }
.hm-sw-process-bar__step[aria-checked="true"] [data-layer="shade"] { background: var(--hm-sw-process-tab-active-shade); }
@@ -490,11 +514,11 @@
/* File name: 189 at most (Figma 873:16254), solid #0E4C3A border. */
.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__bar > .hm-sw-header__document { flex: 189 1 0; max-width: 189px; }
.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__bar > .hm-sw-header__document .hm-sw-header__document-button {
background: var(--hm-sw-header-document-background) padding-box, linear-gradient(var(--hm-sw-header-document-border-color) 0 0) border-box;
background: var(--hm-sw-process-document-background) padding-box, linear-gradient(var(--hm-sw-process-document-border-color) 0 0) border-box;
}
.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__center { flex: 1278 1 0; }
.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__window::before {
background: var(--hm-sw-header-document-background) padding-box, linear-gradient(var(--hm-sw-header-document-border-color) 0 0) border-box;
background: var(--hm-sw-process-document-background) padding-box, linear-gradient(var(--hm-sw-process-document-border-color) 0 0) border-box;
}
/* SW pill at the Figma place: 41 high, 4 from the top, 5 from the window start, 2 before the window move. */
.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__window .hm-sw-process-header__tools { height: 41px; margin: 4px 2px 0 5px; }
@@ -609,7 +633,7 @@
.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__step:disabled { color: var(--hm-sw-process-multi-tab-disabled-text); }
.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__group .hm-sw-process-bar__step:disabled > .hm-sw-process-bar__step-box {
border-color: var(--hm-sw-process-multi-tab-disabled-border);
background: var(--hm-sw-header-property-background);
background: var(--hm-sw-process-property-background);
box-shadow: var(--hm-sw-process-multi-tab-disabled-shadow);
opacity: 0.5;
}
@@ -627,8 +651,8 @@
border-color: transparent;
border-bottom-width: 0;
border-radius: 2px 2px 0 0;
background: var(--hm-sw-process-tool-open-background) padding-box, var(--hm-sw-header-hover-border-gradient) border-box;
color: var(--hm-sw-header-selected-color);
background: var(--hm-sw-process-tool-open-background) padding-box, var(--hm-sw-process-hover-border-gradient) border-box;
color: var(--hm-sw-process-selected-color);
}
.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-process-split[data-open] > .hm-sw-tool__pill-button::after { opacity: 0; }
@@ -644,7 +668,7 @@
height: var(--hm-sw-process-split-tail, 3px);
border: solid transparent;
border-width: 0 1px;
background: linear-gradient(var(--hm-sw-process-tool-open-edge), var(--hm-sw-process-tool-open-edge)) padding-box, var(--hm-sw-header-hover-border-gradient) border-box;
background: linear-gradient(var(--hm-sw-process-tool-open-edge), var(--hm-sw-process-tool-open-edge)) padding-box, var(--hm-sw-process-hover-border-gradient) border-box;
}
.hm-sw-process-split__panel {
position: absolute;
@@ -658,7 +682,7 @@
border: 1px solid transparent;
border-top-width: 0;
border-radius: 0 2px 2px 2px;
background: linear-gradient(var(--hm-sw-process-split-panel-background), var(--hm-sw-process-split-panel-background)) padding-box, var(--hm-sw-header-hover-border-gradient) border-box;
background: linear-gradient(var(--hm-sw-process-split-panel-background), var(--hm-sw-process-split-panel-background)) padding-box, var(--hm-sw-process-hover-border-gradient) border-box;
box-shadow: var(--hm-sw-process-split-panel-shadow);
}
/* Gold line on the panel top, from the button right edge to the panel end (Header.Border.ToolMenuStep). */
@@ -669,7 +693,7 @@
right: 0;
height: 1px;
border-top-right-radius: 2px;
background: var(--hm-sw-header-hover-border-gradient) 0 0 / 100% 36px no-repeat;
background: var(--hm-sw-process-hover-border-gradient) 0 0 / 100% 36px no-repeat;
pointer-events: none;
}
.hm-sw-process-split__cell { display: grid; place-items: center; height: 34px; }
@@ -685,4 +709,4 @@
}
.hm-sw-process-split__frame > svg { display: block; overflow: visible; }
.hm-sw-process-split__pane { fill: var(--hm-sw-process-split-cell-pane); stroke: rgba(0, 0, 0, 0.3); stroke-width: 1; }
.hm-sw-process-split__cell:is(:hover, [aria-checked="true"]) .hm-sw-process-split__pane { fill: var(--hm-sw-header-selected-color); }
.hm-sw-process-split__cell:is(:hover, [aria-checked="true"]) .hm-sw-process-split__pane { fill: var(--hm-sw-process-selected-color); }
+9 -9
View File
@@ -221,7 +221,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
padding: 0 12px;
color: inherit;
}
.hm-sw-viewer-header__logo-menu .hm-sw-header__more-item:is(:hover, :focus):not(:disabled) { background: var(--hm-sw-header-hover); outline: none; }
.hm-sw-viewer-header__logo-menu .hm-sw-header__more-item:is(:hover, :focus):not(:disabled) { background: var(--hm-sw-header-more-item-hover-background); outline: none; }
.hm-sw-viewer-header__logo-menu .hm-sw-header__more-item:disabled { opacity: 0.5; }
/* Title = SWViewerMenu label, directly above its items (one group, no divider); icons = SWViewerMenu icon slot. */
.hm-sw-viewer-header__logo-menu .hm-sw-header__more-title { min-height: 0; padding: 6px 12px; font-size: 12px; font-weight: 500; line-height: 18px; }
@@ -297,18 +297,18 @@ button.hm-sw-viewer-header__brand { text-align: left; }
a segment (none around the license button), no gap, top and bottom on the object surface border. The strip ends at the object surface edge, as in SWHeader. Status stays display only. */
.hm-sw-viewer-header__object:has(> .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):last-child) { padding-right: 0; }
/* Viewer header dropdown colors (2026-10-21): the menus of the header — menu buttons, More and their submenus — use the
colors of the SWHeader `...` menu (sw.css .hm-sw-header__more-menu): its face and shadow (fixed values there), the
object border token, the header text, secondary text at 80% (as its values) and the white 8% row hover with the text
unchanged. Size, spacing and type stay SWViewerMenu. A standalone SWViewerMenu (context menu) and the document list
keep their own colors. */
colors of the SWHeader `...` menu (sw.css .hm-sw-header__more-menu): its More tokens for face, shadow, border and
row hover (2026-11-10, same values as before), the header text and secondary text at 80% (as its values), the text
unchanged on hover. Size, spacing and type stay SWViewerMenu. A standalone SWViewerMenu (context menu) and the
document list keep their own colors. */
.hm-sw-viewer-header .hm-sw-viewer-menu[data-surface] {
border-color: var(--hm-sw-header-object-border-color);
background: #0e3d2f;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
border-color: var(--hm-sw-header-more-menu-border-color);
background: var(--hm-sw-header-more-menu-background);
box-shadow: var(--hm-sw-header-more-menu-shadow);
color: var(--hm-sw-header-text-color);
}
.hm-sw-viewer-header .hm-sw-viewer-menu .hm-sw-viewer-menu__item:is(:focus, :hover, [data-state="open"]):not([aria-disabled]) {
background: var(--hm-sw-header-hover);
background: var(--hm-sw-header-more-item-hover-background);
color: inherit;
}
.hm-sw-viewer-header .hm-sw-viewer-menu :is(.hm-sw-viewer-menu__label, .hm-sw-viewer-menu__shortcut, .hm-sw-viewer-menu__chevron) { color: inherit; opacity: 0.8; }
+40 -18
View File
@@ -96,6 +96,28 @@
--hm-sw-switch-on-border: #d2570b;
--hm-sw-switch-thumb-off: linear-gradient(180deg, #fbedd4 0%, #eee3da 100%);
--hm-sw-switch-thumb-on: linear-gradient(180deg, #e8741e 0%, #c0520d 100%);
/* Switch depth (2026-11-10 individual tokens, values as before): inner shadows + ring + 1px highlight, thumb drop. */
--hm-sw-switch-off-shadow: inset 1px 0 2px rgba(0, 0, 0, .3), inset 0 2px 2px rgba(0, 0, 0, .3), inset 0 0 0 1px var(--hm-sw-switch-off-border), 1px 1px 1px #fcf0d8;
--hm-sw-switch-on-shadow: inset 1px 0 1px 1px rgba(0, 0, 0, .2), inset 0 1px 1px 1px rgba(0, 0, 0, .2), inset 0 0 0 .8px var(--hm-sw-switch-on-border), 1px 1px 1px #fcf0d8;
--hm-sw-switch-thumb-shadow: 0 .5px 1px rgba(0, 0, 0, .6);
/* Popup frame (SWPopupFrame: SWPopup, property window, SWViewerPanel; 2026-11-10 individual tokens). The title bar
defaults follow the header tokens it used so far, so a theme that sets those header tokens is unchanged; set the
popup token to change the popup alone. */
--hm-sw-popup-titlebar-background: var(--hm-sw-header-document-background);
--hm-sw-popup-title-color: var(--hm-sw-header-selected-color);
--hm-sw-popup-title-outline: var(--hm-sw-header-label-outline);
--hm-sw-popup-frame-shadow: inset 0 0 0 1px #000;
--hm-sw-popup-group-heading-color: rgba(0, 0, 0, .7);
--hm-sw-popup-group-border-color: #585349;
--hm-sw-popup-group-shadow: 0 2px 2px rgba(255, 255, 255, .6), inset 0 2px 4px #e1cfab, inset 0 -2px 2px rgba(104, 95, 76, .8);
--hm-sw-popup-divider-color: rgba(0, 0, 0, .5);
--hm-sw-popup-table-line-color: rgba(0, 0, 0, .4);
/* SWHeader `...` More menu (2026-11-10 individual tokens, values as before). The viewer header dropdowns and the
viewer logo menu take the same face and shadow. Border and row hover default to the header tokens they used. */
--hm-sw-header-more-menu-background: #0e3d2f;
--hm-sw-header-more-menu-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
--hm-sw-header-more-menu-border-color: var(--hm-sw-header-object-border-color);
--hm-sw-header-more-item-hover-background: var(--hm-sw-header-hover);
/* Popup scrollbar (every SW popup body: SWPopup, SWViewerPanel, property window). Same as the DWG/DXF viewer
panels (thin, HM warm-neutral-mid thumb on warm-neutral-light track). */
--hm-sw-popup-scrollbar-thumb: #9a9284;
@@ -1353,10 +1375,10 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
min-width: 200px;
max-width: calc(100vw - 16px);
padding: 4px;
border: 1px solid var(--hm-sw-header-object-border-color);
border: 1px solid var(--hm-sw-header-more-menu-border-color);
border-radius: var(--hm-sw-header-property-radius);
background: #0e3d2f;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
background: var(--hm-sw-header-more-menu-background);
box-shadow: var(--hm-sw-header-more-menu-shadow);
}
.hm-sw-header__more-item {
display: none;
@@ -1368,7 +1390,7 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
border-radius: 2px;
white-space: nowrap;
}
.hm-sw-header__more-item:hover:not(:disabled) { background: var(--hm-sw-header-hover); }
.hm-sw-header__more-item:hover:not(:disabled) { background: var(--hm-sw-header-more-item-hover-background); }
.hm-sw-header__more-item[role="menuitemcheckbox"]::after { content: ""; width: 14px; text-align: center; }
.hm-sw-header__more-item[aria-checked="true"]::after { content: "✓"; }
.hm-sw-header__more-value { font-size: var(--hm-sw-header-label-size); opacity: 0.8; }
@@ -1619,13 +1641,13 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
overflow: hidden; border: 1px solid var(--hm-sw-property-border); border-radius: 4px;
background: var(--hm-sw-property-surface); color: var(--hm-sw-property-label);
font-size: 14px; font-weight: 400; line-height: 16px; letter-spacing: .02em;
box-shadow: inset 0 0 0 1px #000;
box-shadow: var(--hm-sw-popup-frame-shadow);
}
.hm-sw-property__titlebar { position: relative; display: flex; align-items: center; flex: 0 0 30px; height: 30px; background: var(--hm-sw-header-document-background); }
.hm-sw-property__titlebar { position: relative; display: flex; align-items: center; flex: 0 0 30px; height: 30px; background: var(--hm-sw-popup-titlebar-background); }
/* Figma paints btn_close first, then Union over its left side. Keep Union's diagonal and shadow
above the close background; the title and tool remain interactive on top of it. */
.hm-sw-property__title-art { position: absolute; z-index: 1; width: 308px; height: 38px; left: 0; top: -2px; transform: scaleY(-1); pointer-events: none; }
.hm-sw-property__title { position: relative; z-index: 2; flex: 1; min-width: 0; margin-left: 7px; padding-right: 4px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-weight: 700; letter-spacing: .04em; color: var(--hm-sw-header-selected-color); text-shadow: var(--hm-sw-header-label-outline); }
.hm-sw-property__title { position: relative; z-index: 2; flex: 1; min-width: 0; margin-left: 7px; padding-right: 4px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-weight: 700; letter-spacing: .04em; color: var(--hm-sw-popup-title-color); text-shadow: var(--hm-sw-popup-title-outline); }
.hm-sw-property__tool { position: absolute; z-index: 2; right: 43px; top: 1px; width: 29px; height: 27.13px; }
.hm-sw-property:not([data-selection-kind="single"]) .hm-sw-property__title { margin-right: 29px; }
.hm-sw-property__tool > img { position: absolute; top: 0; left: 0; display: block; width: 29px; height: 27.13px; }
@@ -1649,17 +1671,17 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
box-sizing: border-box; display: inline-flex; align-items: center; flex-shrink: 0;
width: 37px; height: 16px; padding: 0; border: 0; border-radius: 100px;
background: var(--hm-sw-switch-off-background);
box-shadow: inset 1px 0 2px rgba(0, 0, 0, .3), inset 0 2px 2px rgba(0, 0, 0, .3), inset 0 0 0 1px var(--hm-sw-switch-off-border), 1px 1px 1px #fcf0d8;
box-shadow: var(--hm-sw-switch-off-shadow);
}
.hm-sw-property [role="switch"]:is([data-slot="switch"], .hm-sw-viewer-switch)[data-state="checked"] {
background: var(--hm-sw-switch-on-background);
box-shadow: inset 1px 0 1px 1px rgba(0, 0, 0, .2), inset 0 1px 1px 1px rgba(0, 0, 0, .2), inset 0 0 0 .8px var(--hm-sw-switch-on-border), 1px 1px 1px #fcf0d8;
box-shadow: var(--hm-sw-switch-on-shadow);
}
.hm-sw-property [role="switch"]:is([data-slot="switch"], .hm-sw-viewer-switch):hover:not(:disabled) { background-image: none; }
.hm-sw-property [role="switch"]:is([data-slot="switch"], .hm-sw-viewer-switch):focus-visible { outline: 2px solid var(--hm-sw-switch-on-border); outline-offset: 1px; }
.hm-sw-property [role="switch"] > :is([data-slot="switch-thumb"], .hm-sw-viewer-switch__thumb) {
width: 12px; height: 12px; border-radius: 6.5px;
background: var(--hm-sw-switch-thumb-off); box-shadow: 0 .5px 1px rgba(0, 0, 0, .6);
background: var(--hm-sw-switch-thumb-off); box-shadow: var(--hm-sw-switch-thumb-shadow);
transform: translateX(2px);
}
.hm-sw-property [role="switch"][data-state="checked"] > :is([data-slot="switch-thumb"], .hm-sw-viewer-switch__thumb) {
@@ -1668,8 +1690,8 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
.hm-sw-property[data-selection-kind="all"] .hm-sw-property__body { padding-bottom: 0; }
.hm-sw-property[data-selection-kind="all"] .hm-sw-property__counts { margin-top: -8px; }
.hm-sw-property__group { flex: 0 0 auto; }
.hm-sw-property__group > h3 { margin: 0 0 4px; font: inherit; font-weight: 700; color: rgba(0,0,0,.7); }
.hm-sw-property__group--rows > .hm-sw-property__group-content { border: 1px solid #585349; border-radius: 4px; padding: 2px; background: var(--hm-sw-property-content); box-shadow: 0 2px 2px rgba(255,255,255,.6), inset 0 2px 4px #e1cfab, inset 0 -2px 2px rgba(104,95,76,.8); }
.hm-sw-property__group > h3 { margin: 0 0 4px; font: inherit; font-weight: 700; color: var(--hm-sw-popup-group-heading-color); }
.hm-sw-property__group--rows > .hm-sw-property__group-content { border: 1px solid var(--hm-sw-popup-group-border-color); border-radius: 4px; padding: 2px; background: var(--hm-sw-property-content); box-shadow: var(--hm-sw-popup-group-shadow); }
.hm-sw-property__row { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 6px; }
.hm-sw-property__row > label { flex: 0 0 72px; min-width: 0; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hm-sw-property__value { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; }
@@ -1738,7 +1760,7 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
.hm-sw-property__expand > img { display: block; width: 296px; height: 12px; margin-left: calc((100% - 296px) / 2); }
.hm-sw-property__expand[aria-expanded="true"] > img { transform: rotate(180deg); }
.hm-sw-property__counts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.hm-sw-property__counts > span { min-height: 28px; padding: 5px 0; border-bottom: 1px solid rgba(0,0,0,.5); font-weight: 700; }
.hm-sw-property__counts > span { min-height: 28px; padding: 5px 0; border-bottom: 1px solid var(--hm-sw-popup-divider-color); font-weight: 700; }
/* 2026-10-21 design review: each object type entry (원 1, 폴리선 10 …) changes its own text color on Hover, with the
SW row accent #FF5C00 (the header list row hover color). Display only: no click, selection or filter. */
/* Only the object list (columns 3-4); the summary entries of columns 1-2 (data-count-summary) stay plain text. */
@@ -1768,17 +1790,17 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
.hm-sw-property__group--position { margin-top: 8px; }
.hm-sw-property__group--position .hm-sw-property__group-content { margin: 0 4px; }
.hm-sw-property__position-head, .hm-sw-property__group--position .hm-sw-property__editors { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); }
.hm-sw-property__position-head { background: var(--hm-sw-property-table); border-top: 1px solid rgba(0,0,0,.4); }
.hm-sw-property__position-head > span, .hm-sw-property__position-label { min-height: 24px; padding: 3px 0; text-align: center; font-weight: 700; border-bottom: 1px solid rgba(0,0,0,.4); }
.hm-sw-property__position-head > span + span, .hm-sw-property__group--position .hm-sw-property__editor { border-left: 1px solid rgba(0,0,0,.4); }
.hm-sw-property__position-head { background: var(--hm-sw-property-table); border-top: 1px solid var(--hm-sw-popup-table-line-color); }
.hm-sw-property__position-head > span, .hm-sw-property__position-label { min-height: 24px; padding: 3px 0; text-align: center; font-weight: 700; border-bottom: 1px solid var(--hm-sw-popup-table-line-color); }
.hm-sw-property__position-head > span + span, .hm-sw-property__group--position .hm-sw-property__editor { border-left: 1px solid var(--hm-sw-popup-table-line-color); }
.hm-sw-property__position-label { background: var(--hm-sw-property-table); }
.hm-sw-property__group--position .hm-sw-property__row { padding: 0; gap: 0; min-height: 24px; }
.hm-sw-property__group--position label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.hm-sw-property__group--position .hm-sw-header__input { padding: 0; width: 100%; height: 24px; text-align: center; }
.hm-sw-property__group--position .hm-sw-property__editor { border-bottom: 1px solid rgba(0,0,0,.4); }
.hm-sw-property__group--position .hm-sw-property__editor { border-bottom: 1px solid var(--hm-sw-popup-table-line-color); }
.hm-sw-property__overview { position: relative; flex: 0 0 28px; height: 28px; text-align: left; overflow: hidden; }
.hm-sw-property__overview > img { position: absolute; left: -3px; top: -2px; width: 317px; height: 36.0847px; transform: scaleY(-1); }
.hm-sw-property__overview > span { position: relative; display: block; padding: 6px 7px; color: var(--hm-sw-header-selected-color); text-shadow: var(--hm-sw-header-label-outline); font-weight: 700; }
.hm-sw-property__overview > span { position: relative; display: block; padding: 6px 7px; color: var(--hm-sw-popup-title-color); text-shadow: var(--hm-sw-popup-title-outline); font-weight: 700; }
.hm-sw-property__overview:disabled { opacity: 1; }
.hm-sw-property__row[data-disabled="true"] { opacity: .4; }
.hm-sw-property button:focus-visible, .hm-sw-property input:focus-visible, .hm-sw-property select:focus-visible { outline: 2px solid var(--hm-sw-property-border); outline-offset: -2px; }
+198
View File
@@ -0,0 +1,198 @@
// Test-only host for property-fields.browser.mjs (2026-11-10). Not a Storybook Story and not shipped (package "files" is
// dist + ai). It restores the host data and host controls of the two demos removed in 5faa1f1 that the field checks
// need: SW/Header › Property Fields (data-fields live / default / pinned hover) and the Header-hosted Property Panel
// (원 1개 selection). Records and controls are copied from src/sw/sw-header.stories.tsx at 5faa1f1^ without changes.
// Uses only the public @hm-design/sw API, like a consuming project. The pinned Hover column uses the Storybook pseudo-state
// mirror (.storybook/pseudo-states.ts) exactly as the retired Story did.
import { useEffect, useRef, useState, type ReactNode } from "react"
import { createRoot } from "react-dom/client"
import {
SWHeader, SWHeaderPropertyPanel, SWTheme, swHeaderLinePatterns,
type SWHeaderColorMode, type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderLineType, type SWHeaderLineWeight,
type SWHeaderProperty, type SWHeaderPropertyGroup, type SWHeaderPropertyMenus, type SWHeaderPropertySelection,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
import { ensurePseudoStates } from "../../../../.storybook/pseudo-states"
const FIGMA_COLORS: SWHeaderColorOption[] = [
{ id: "white", color: "#ffffff", label: "흰색" },
{ id: "red", color: "#f10404", label: "빨강" },
{ id: "green", color: "#48ff08", label: "초록" },
{ id: "yellow", color: "#ffff00", label: "노랑" },
{ id: "blue", color: "#0012ff", label: "파랑" },
{ id: "magenta", color: "#ff00f6", label: "자홍" },
{ id: "cyan", color: "#00f6ff", label: "청록" },
]
const FIGMA_LINE_TYPES: SWHeaderLineType[] = [
{ id: "byblock", name: "ByBlock" },
{ id: "bylayer", name: "ByLayer" },
{ id: "continuous", name: "Continuous" },
{ id: "center", name: "CENTER", pattern: swHeaderLinePatterns.center },
{ id: "hidden", name: "HIDDEN", pattern: swHeaderLinePatterns.hidden },
]
const FIGMA_LINE_WEIGHTS: SWHeaderLineWeight[] = [
{ id: "w-bylayer", name: "ByLayer" },
{ id: "w-byblock", name: "ByBlock" },
{ id: "w-default", name: "ByDefault", value: "0.25" },
{ id: "w-user", name: "User", value: "0.25", editable: true },
]
const PROPERTY_GROUPS: SWHeaderPropertyGroup[] = [
{ id: "common", collapsedCount: 6 },
{ id: "shape", name: "형상", layout: "circle" },
{ id: "position", name: "위치", layout: "position", positionLabel: "중간점" },
]
const PROPERTY_FIXTURE: SWHeaderProperty[] = [
{ id: "layer", name: "레이어", value: "0_Layer_Cludmark", type: "text", groupId: "common", picker: "layer", validate: draft => draft.length <= 40 },
{ id: "color", name: "색상", value: "255,229,0", type: "color", groupId: "common" },
{ id: "line", name: "선스타일", value: "Continuous", type: "select", groupId: "common", unit: "(ByL)", linePreview: true, options: [{ id: "Continuous", name: "Continuous" }, { id: "CENTER", name: "CENTER" }, { id: "HIDDEN", name: "HIDDEN" }] },
{ id: "weight", name: "선가중치", value: "0.25", type: "number", groupId: "common", min: 0 },
{ id: "scale", name: "선축적", value: "1.0000", type: "number", groupId: "common", min: .0001, decimals: 4, unit: "(LTscale:1)" },
{ id: "opacity", name: "투명도", value: "0.25", type: "slider", groupId: "common", min: 0, max: 1, step: .01, annotation: "(ByL)", unit: "%" },
{ id: "link", name: "하이퍼링크", value: "--", type: "text", groupId: "common" },
{ id: "thickness", name: "두께", value: "0.25", type: "number", groupId: "common", unit: "mm", min: 0 },
{ id: "material", name: "재료", value: "-", type: "computed", groupId: "common", unit: "(ByL)" },
{ id: "circumference", name: "원주", value: "0.25", type: "computed", groupId: "shape", graphicSlot: "circumference" },
{ id: "diameter", name: "지름", value: "8.1502", type: "number", groupId: "shape", graphicSlot: "diameter", min: 0 },
{ id: "radius", name: "반지름", value: "4.0526", type: "number", groupId: "shape", graphicSlot: "radius", min: 0 },
{ id: "area", name: "면적", value: "51.5962", type: "computed", groupId: "shape", graphicSlot: "area" },
{ id: "x", name: "X", value: "48.0808", type: "number", groupId: "position" },
{ id: "y", name: "Y", value: "14.9160", type: "number", groupId: "position" },
{ id: "z", name: "Z", value: "0.0000", type: "number", groupId: "position" },
]
const PROPERTY_COUNTS = [
{ id: "shape", name: "도형", count: 10, row: 1, column: 1 }, { id: "line", name: "직선", count: 10, row: 1, column: 2 },
{ id: "circle", name: "원", count: 1, row: 1, column: 3 }, { id: "ray", name: "광선", count: 1, row: 1, column: 4 },
{ id: "curve", name: "곡선", count: 10, row: 2, column: 3 }, { id: "arc", name: "호", count: 1, row: 2, column: 4 },
{ id: "polyline", name: "폴리선", count: 10, row: 3, column: 3 }, { id: "polyline2", name: "폴리선", count: 2, row: 3, column: 4 },
{ id: "text1", name: "문자", count: 1, row: 4, column: 1 }, { id: "text2", name: "문자", count: 1, row: 4, column: 2 },
{ id: "text3", name: "문자", count: 1, row: 4, column: 3 },
]
type PropertyExample = "none" | "all" | "circle" | "expanded"
function propertySelection(example: PropertyExample): SWHeaderPropertySelection {
return example === "none" ? { id: "none", title: "선택된 객체 - 없음", kind: "none" }
: example === "all" ? { id: "all", title: "선택된 객체 - 전체 11", kind: "all", counts: PROPERTY_COUNTS }
: { id: "circle", title: "선택된 객체 - 원1", kind: "single", overviewTitle: "선택된 객체 - 전체 11" }
}
function propertyFixture(example: PropertyExample) {
return PROPERTY_FIXTURE.filter(item => (example === "none" || example === "all") ? item.groupId === "common" : true).map(item => ({
...item, ...(example === "none" && item.id === "layer" ? { value: "0" } : {}),
...(example === "none" && item.id === "color" ? { value: "138,138,138" } : {}),
...(example === "all" && item.id === "layer" ? { mixed: true } : {}),
...((example === "none" || example === "all") && item.id === "opacity" ? { value: "0" } : {}),
}))
}
const PROPERTY_LAYERS: SWHeaderLayer[] = [
{ id: "0", name: "0" },
{ id: "0_Layer_Cludmark", name: "0_Layer_Cludmark", color: "#ff0000" },
{ id: "CC-D GNL", name: "CC-D GNL" },
{ id: "Sample-Wall", name: "Sample-Wall", color: "#ffe500", visible: false },
{ id: "Sample-Door", name: "Sample-Door", color: "#00ff00", frozen: true },
{ id: "Sample-Text", name: "Sample-Text", color: "#00ffff", locked: true },
{ id: "Sample-Defpoints", name: "Sample-Defpoints", color: "#808080", disabled: true },
]
const PROPERTY_LINE_TYPES: SWHeaderLineType[] = FIGMA_LINE_TYPES.map(item => ({ ...item, id: item.name }))
const linePatternOf = (value: string) => PROPERTY_LINE_TYPES.find(item => item.id === value)?.pattern
function usePropertyMenus(record: (line: string) => void, setProperties: (update: (items: SWHeaderProperty[]) => SWHeaderProperty[]) => void): SWHeaderPropertyMenus {
const [layers, setLayers] = useState(PROPERTY_LAYERS)
const [weights, setWeights] = useState(FIGMA_LINE_WEIGHTS)
const [weightId, setWeightId] = useState("w-default")
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>(null)
const setValue = (id: string, value: string) => setProperties(items => items.map(item => item.id === id ? { ...item, value, mixed: false } : item))
const scale = "1.0000"
const toggle = (key: "visible" | "frozen" | "locked") => (layer: SWHeaderLayer, next: boolean) => {
record(`layer.${key}(${layer.id}, ${next})`)
setLayers(items => items.map(item => item.id === layer.id ? { ...item, [key]: next } : item))
}
return {
layers, onLayerVisibilityChange: toggle("visible"), onLayerFreezeChange: toggle("frozen"), onLayerLockChange: toggle("locked"),
onLayerDetail: () => record("layerDetail"),
lineTypes: PROPERTY_LINE_TYPES, lineWeights: weights, activeLineWeightId: weightId, lineScale: scale,
onLineWeightSelect: weight => { record(`lineWeight(${weight.id})`); setWeightId(weight.id); if (weight.value) setValue("weight", weight.value) },
onLineWeightValueChange: (weight, value) => { record(`lineWeightValue(${weight.id}, ${value})`); setWeights(items => items.map(item => item.id === weight.id ? { ...item, value } : item)) },
onLineScaleChange: value => { record(`lineScale(${value})`); setValue("scale", Number(value).toFixed(4)) },
onLineStyleDetail: () => record("lineStyleDetail"),
colors: FIGMA_COLORS, colorMode,
onColorModeChange: mode => { record(`colorMode(${mode})`); setColorMode(mode) },
onColorPick: property => record(`colorPick(${property.id})`),
onColorDetail: () => record("colorDetail"),
}
}
function PinHover({ selectors, children }: { selectors: string[]; children: ReactNode }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
ensurePseudoStates()
ref.current?.querySelectorAll(selectors.join(",")).forEach(element => element.setAttribute("data-sb-hover", ""))
}, [selectors])
return <div ref={ref}>{children}</div>
}
/* SW/Header › Property Fields (retired in 5faa1f1): the common rows of PROPERTY_FIXTURE plus a mixed layer row. Figma
sample text 255,259,0 is out of range: shown as host text only, never accepted as input. */
const FIELD_FIXTURE: SWHeaderProperty[] = PROPERTY_FIXTURE.filter(item => item.groupId === "common").flatMap(item =>
item.id === "layer" ? [{ ...item, id: "layer-mixed", mixed: true }, item]
: item.id === "color" ? [{ ...item, value: "255,259,0", swatch: "#ffe500" }]
: [item])
const FIELD_HOVER = [".hm-sw-property__value[data-field]", ".hm-sw-property__compact"]
function FieldPanel({ title, properties, onChange, menus }: { title: string; properties: SWHeaderProperty[]; onChange?: (property: SWHeaderProperty, value: string) => void; menus?: SWHeaderPropertyMenus }) {
return <SWTheme><SWHeaderPropertyPanel selection={{ id: title, title, kind: "none" }} properties={properties} groups={[{ id: "common" }]} onPropertyChange={onChange} menus={menus} /></SWTheme>
}
function PropertyFieldsHost() {
const [properties, setProperties] = useState(FIELD_FIXTURE)
const [log, setLog] = useState<string[]>([])
const record = (line: string) => setLog(lines => [...lines.slice(-11), line])
const menus = usePropertyMenus(record, setProperties as (update: (items: SWHeaderProperty[]) => SWHeaderProperty[]) => void)
const [noMenus, setNoMenus] = useState(false)
const patch = (id: string, change: Partial<SWHeaderProperty>) => setProperties(items => items.map(item => item.id === id ? { ...item, ...change } : item))
const change = (property: SWHeaderProperty, value: string) => {
record(`property(${property.id}, ${value})`)
patch(property.id, { value, mixed: false, ...(property.id === "line" ? { linePattern: linePatternOf(value) } : {}), ...(property.id === "color" ? { swatch: undefined } : {}) })
}
return <div data-fields-demo="">
<div><strong>외부 데이터</strong>
<button data-action="layer-mixed" onClick={() => patch("layer-mixed", { mixed: !properties.find(item => item.id === "layer-mixed")?.mixed })}>레이어 다양함 ↔ 단일</button>
<button data-action="layer-external" onClick={() => patch("layer", { value: "CC-D GNL" })}>레이어 외부 값 변경</button>
<button data-action="scale-external" onClick={() => patch("scale", { value: "2.5000" })}>선축적 외부 값 변경</button>
<button data-action="thickness-unit" onClick={() => patch("thickness", { unit: properties.find(item => item.id === "thickness")?.unit === "mm" ? "cm" : "mm" })}>두께 단위 변경</button>
<button data-action="line-center" onClick={() => patch("line", { value: "CENTER", linePattern: linePatternOf("CENTER") })}>선스타일 외부 CENTER</button>
<button data-action="readonly" onClick={() => setProperties(items => items.map(item => item.id === "thickness" || item.id === "layer" ? { ...item, readOnly: !item.readOnly } : item))}>두께·레이어 ReadOnly</button>
<button data-action="disabled" onClick={() => setProperties(items => items.map(item => item.id === "scale" || item.id === "color" ? { ...item, disabled: !item.disabled } : item))}>선축적·색상 Disabled</button>
<button data-action="add-row" onClick={() => setProperties(items => [...items, { id: `extra-${items.length}`, name: "추가 속성", value: "호스트 값", type: "text", groupId: "common" }])}>속성 추가</button>
<button data-action="remove-row" onClick={() => setProperties(items => items.filter(item => item.id !== "link"))}>하이퍼링크 삭제</button>
<button data-action="no-menus" onClick={() => setNoMenus(value => !value)}>menus 없음(options 사용): {String(noMenus)}</button>
</div>
<p data-property-log="" aria-live="polite">{log.join(" → ") || "Callback 없음"}</p>
<div style={{ display: "flex", gap: 24, alignItems: "flex-start" }}>
<figure style={{ margin: 0 }} data-fields="live"><FieldPanel title="실제 조작" properties={properties} onChange={change} menus={noMenus ? undefined : menus} /></figure>
<figure style={{ margin: 0 }} data-fields="default"><FieldPanel title="Default" properties={FIELD_FIXTURE} /></figure>
<figure style={{ margin: 0 }} data-fields="hover"><PinHover selectors={FIELD_HOVER}><FieldPanel title="Hover" properties={FIELD_FIXTURE} /></PinHover></figure>
</div>
</div>
}
/* SW/Header › Property Panel (retired in 5faa1f1): SWHeader with the propertyPanel prop and the selection buttons. */
function HeaderPanelHost() {
const [selection, setSelection] = useState(propertySelection("none"))
const [properties, setProperties] = useState(propertyFixture("none"))
const [expanded, setExpanded] = useState<string[]>([])
const menus = usePropertyMenus(() => {}, setProperties)
const changeExample = (next: PropertyExample) => {
setSelection(propertySelection(next)); setProperties(propertyFixture(next))
setExpanded(next === "expanded" ? ["common"] : [])
}
const panel = {
selection, properties, groups: PROPERTY_GROUPS, expandedGroupIds: expanded, menus,
onExpandedGroupsChange: setExpanded,
onPropertyChange: (property: SWHeaderProperty, value: string) => setProperties(items => items.map(item => item.id === property.id ? { ...item, value, mixed: false } : item)),
onOverviewClick: () => changeExample("all"),
}
return <div data-property-demo="">
<div>{(["none", "all", "circle", "expanded"] as const).map((name, index) => <button key={name} data-example-button={name} onClick={() => changeExample(name)}>{["선택 없음", "전체 선택", "원 1개", "모두 펼침"][index]}</button>)}</div>
<div style={{ width: 1920, minHeight: 820 }}>
<SWHeader documentTitle="C0020101-006-평면및종단면도(06)" drawingModeLabel="그리기모드" layer={{ name: "0_Layer_Cludmark" }} color={{ value: "255,229,0", swatch: "#ffe500" }}
lineStyle={{ name: "Continuous", weight: "0.25", scale: "1.00" }} textStyle={{ font: "Arial", name: "Standard" }} dimensionStyle={{ name: "Standard" }} propertyPanel={panel} />
</div>
</div>
}
const view = new URLSearchParams(location.search).get("view")
createRoot(document.getElementById("root")!).render(view === "header" ? <HeaderPanelHost /> : <PropertyFieldsHost />)
+47 -20
View File
@@ -1,17 +1,34 @@
// Historical since 2026-10-19: opens SW Story IDs retired by the unused demo removal (property-fields). Kept as evidence
// for its recorded run; current regression: storybook-unused-removal.browser.mjs.
// Property fields (Figma 26:12388 ~ 26:12649): Default / real pointer Hover / Mouse Leave / On (editing, dropdown),
// selection callbacks, keyboard, Escape, external data, Disabled / ReadOnly, and the four panel states.
// 2026-11-10 (test alignment): the SW/Header › Property Fields and Property Panel demos this suite opened were removed in
// 5faa1f1 (approved). Sections 1–9 and the Header-hosted panel run on the test-only fixture fixtures/property-fields.tsx,
// which restores those demos' host data and host controls with the public API (as header-composition does); the four
// panel states run on the current Story sw-header--property-panel-compare (SW/Popup › 속성창). Every check is unchanged.
// Usage: node packages/sw/tests/property-fields.browser.mjs <evidence-dir> <label> [storybook-url]
// Storybook serves packages/sw/dist: build @hm-design/sw first.
import { mkdir, writeFile } from 'node:fs/promises'
// The fixture and Storybook use packages/sw/dist: build @hm-design/sw first.
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import http from 'node:http'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { loadPlaywright } from './playwright-runtime.mjs'
const out = path.resolve(process.argv[2] ?? 'docs/sw/property-fields-qa')
const label = process.argv[3] ?? 'run'
const base = process.argv[4] ?? 'http://127.0.0.1:6024'
const { chromium } = loadPlaywright()
await mkdir(out, { recursive: true })
// Test-only fixture page (built into the evidence directory, served only for this run).
const site = path.join(out, 'fixture')
await mkdir(site, { recursive: true })
await createRequire(import.meta.url)('esbuild').build({
entryPoints: [fileURLToPath(new URL('./fixtures/property-fields.tsx', import.meta.url))], bundle: true, outdir: site, format: 'esm', jsx: 'automatic', minify: true,
loader: { '.ttf': 'file', '.woff': 'file', '.woff2': 'file', '.svg': 'file', '.png': 'file' }, define: { 'process.env.NODE_ENV': '"production"' }, logLevel: 'warning',
})
await writeFile(path.join(site, 'index.html'), '<!doctype html><html lang="ko"><head><meta charset="utf-8"><link rel="icon" href="data:,"><link rel="stylesheet" href="property-fields.css"></head><body style="margin:0"><div id="root"></div><script type="module" src="property-fields.js"></script></body></html>')
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.ttf': 'font/ttf', '.woff2': 'font/woff2' }
const server = http.createServer(async (req, res) => { try { const p = new URL(req.url, 'http://localhost').pathname, file = path.resolve(site, '.' + decodeURIComponent(p === '/' ? '/index.html' : p)); if (path.relative(site, file).startsWith('..')) return res.writeHead(403).end(); res.setHeader('Content-Type', mime[path.extname(file)] ?? 'application/octet-stream'); res.end(await readFile(file)) } catch { res.writeHead(404).end() } })
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
const fixture = `http://127.0.0.1:${server.address().port}/`
const browser = await chromium.launch({ executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 }, deviceScaleFactor: 2 })
const report = { label, checks: [], errors: [] }
@@ -19,7 +36,7 @@ page.on('pageerror', e => report.errors.push(e.message)); page.on('console', m =
const check = (name, ok, data) => { report.checks.push({ name, status: ok ? 'PASS' : 'FAIL', data }); console.log(ok ? 'PASS' : 'FAIL', name, JSON.stringify(data)) }
const shot = async (name, locator) => writeFile(path.join(out, `${label}-${name}.png`), await (locator ?? page).screenshot())
await page.goto(`${base}/iframe.html?id=sw-header--property-fields&viewMode=story`)
await page.goto(fixture)
await page.locator('[data-fields="live"] .hm-sw-property').waitFor(); await page.evaluate(() => document.fonts.ready)
const live = page.locator('[data-fields="live"]')
const row = id => live.locator(`.hm-sw-property__row[data-property-id="${id}"]`)
@@ -35,9 +52,13 @@ for (const id of ['layer-mixed', 'layer', 'color', 'line', 'scale', 'link', 'thi
const s = await style(field(id), 'borderTopColor', 'backgroundColor', 'borderTopLeftRadius')
check(`Default ${id}: 200 x 28, radius 2, no border / surface`, s.w === 200 && s.h === 28 && s.borderTopColor === CLEAR && s.backgroundColor === CLEAR && s.borderTopLeftRadius === '2px', s)
}
// 2026-11-10: since the user-approved 2026-10-21 design review (2fc1b3e) 선가중치 is no longer the 41px hug field but the
// 200 x 28 box like 선축적, and the 투명도 value is its own 28px box before the gauge (SWHeaderPropertyPanel.tsx). The
// same properties are checked on the current fields: size, padding (7px incl. no border), radius, no border / surface.
const compactField = id => id === 'weight' ? field('weight') : row(id).locator('.hm-sw-property__slider-value')
for (const id of ['weight', 'opacity']) {
const s = await style(row(id).locator('.hm-sw-property__compact'), 'borderTopColor', 'backgroundColor', 'paddingLeft', 'paddingRight')
check(`Default ${id}: compact 28px hug field (pl 6 / pr 4 incl. border), no border`, s.h === 28 && s.w < 60 && s.borderTopColor === CLEAR && s.paddingLeft === '5px' && s.paddingRight === '3px', s)
const s = await style(compactField(id), 'borderTopColor', 'backgroundColor', 'paddingLeft', 'paddingRight', 'borderTopLeftRadius')
check(`Default ${id}: ${id === 'weight' ? '200 x 28 box' : '28px slider value box'} (pl / pr 7), radius 2, no border / surface`, s.h === 28 && (id === 'weight' ? s.w === 200 : s.w < 200) && s.borderTopColor === CLEAR && s.backgroundColor === CLEAR && s.paddingLeft === '7px' && s.paddingRight === '7px' && s.borderTopLeftRadius === '2px', s)
}
const typo = await row('scale').evaluate(r => { const v = getComputedStyle(r.querySelector('.hm-sw-header__input')), u = getComputedStyle(r.querySelector('.hm-sw-property__unit')); return { value: [v.color, v.fontSize], unit: [u.color, u.fontSize] } })
check('Typography: value #E60403 14px, unit #000 12px', typo.value.join() === 'rgb(230, 4, 3),14px' && typo.unit.join() === 'rgb(0, 0, 0),12px', typo)
@@ -56,9 +77,9 @@ for (const id of ['layer-mixed', 'layer', 'color', 'line', 'scale', 'link', 'thi
check(`Mouse Leave ${id}: back to Default`, left.borderTopColor === CLEAR, left)
}
for (const id of ['weight', 'opacity']) {
const c = row(id).locator('.hm-sw-property__compact')
const c = compactField(id)
await c.hover(); const s = await style(c, 'borderTopColor'); await page.mouse.move(1, 1); const l = await style(c, 'borderTopColor')
check(`Hover / Leave ${id} compact`, s.borderTopColor === HOVER && l.borderTopColor === CLEAR, { hover: s.borderTopColor, leave: l.borderTopColor })
check(`Hover / Leave ${id} ${id === 'weight' ? 'box' : 'slider value box'}`, s.borderTopColor === HOVER && l.borderTopColor === CLEAR, { hover: s.borderTopColor, leave: l.borderTopColor })
}
await field('scale').hover(); await shot('hover-scale', field('scale')); await page.mouse.move(1, 1)
@@ -78,8 +99,9 @@ check('Enter: 선축적 2 → validated, 4 decimals committed', (await log()).in
await scale.fill('7'); await page.keyboard.press('Escape')
check('Escape: draft cancelled (2.0000 restored, no callback)', await scale.inputValue() === '2.0000' && !(await log()).includes('7'), await scale.inputValue())
const weight = row('weight').locator('input')
await weight.click(); const w = await style(weight, 'backgroundColor')
check('On(편집) 선가중치 compact: #0F0D0A', w.backgroundColor === 'rgb(15, 13, 10)', w)
// 2fc1b3e (2026-10-21): the editing face is on the 200px box (as 선축적), no longer on the hug field's input.
await weight.click(); const w = await style(field('weight'), 'backgroundColor')
check('On(편집) 선가중치 box: #0F0D0A', w.backgroundColor === 'rgb(15, 13, 10)', w)
await shot('on-weight', row('weight'))
await weight.fill('-1'); await page.keyboard.press('Enter')
check('Invalid 선가중치 -1 (min 0): restored, no callback', await weight.inputValue() === '0.25' && !(await log()).includes('weight, -1'), await weight.inputValue())
@@ -87,10 +109,11 @@ await weight.fill('0.5'); await page.keyboard.press('Enter')
check('선가중치 0.5 committed', (await log()).includes('property(weight, 0.5)'), await log())
await page.mouse.click(1, 1)
// 4. No invented On: 하이퍼링크 (text) focus keeps the Hover look only; 재료 (computed) is read-only.
// 4. 하이퍼링크 (editable text): since 6d46289 (2026-10-22, approved) its editing face is the numbers' On (#0F0D0A,
// Hover border); 재료 (computed) stays read-only without On.
await row('link').locator('input').click()
const link = await style(field('link'), 'backgroundColor', 'borderTopColor')
check('하이퍼링크 focus: no On surface (Figma has no On), editable', link.backgroundColor === CLEAR && link.borderTopColor === HOVER, link)
check('하이퍼링크 focus: editing face #0F0D0A like numbers (6d46289), Hover border, editable', link.backgroundColor === 'rgb(15, 13, 10)' && link.borderTopColor === HOVER, link)
await row('link').locator('input').fill('http://a'); await page.keyboard.press('Enter')
check('하이퍼링크 commit via host callback', (await log()).includes('property(link, http://a)'), await log())
await row('material').locator('input').click()
@@ -132,16 +155,20 @@ check('Keyboard: ArrowDown opens + focuses list, Escape closes to trigger', focu
await layerTrigger.click(); await page.mouse.click(1300, 900)
check('Outside click closes', await drop.count() === 0, null)
// 6. Line style (26:12388 / 26:12404): 466 = 182 + 142 + 142, right-aligned, preview follows the value.
// 6. Line style (26:12388 / 26:12404): right-aligned, preview follows the value. Since 2fc1b3e (2026-10-21 design
// review, DESIGN_RULES 속성창 입력) the property window list is the line type column only (preview + name; type column
// 182px, growing to 240px only for wider font names); weights / scale stay in the SWHeader line style field.
await field('line').locator('.hm-sw-property__trigger').click()
f = await style(field('line'), 'backgroundColor'); d = await style(drop.locator('.hm-sw-header__line-drop'))
const cols = await drop.locator('.hm-sw-header__line-column').evaluateAll(es => es.map(e => +e.getBoundingClientRect().width.toFixed(2)))
const unitOn = await style(field('line').locator('.hm-sw-property__unit'), 'color')
check('Line style On: #211D13, (ByL) #D4CBBD, 3 columns, right edges meet (or shifted to the 4px screen edge when no room)', f.backgroundColor === 'rgb(33, 29, 19)' && unitOn.color === 'rgb(212, 203, 189)' && cols.length === 3 && (Math.abs(d.x + d.w - (f.x + f.w)) < 0.5 || d.x === 4) && Math.abs(d.y - (f.y + f.h)) < 0.5, { f, d, cols })
check('Line style drop 466 x 190 (type column grows only for wider font names)', d.w >= 466 && d.h === 190, { w: d.w, h: d.h, cols })
const typeOnly = await drop.locator('.hm-sw-header__line-column').evaluateAll(es => es.length === 1 && es[0].classList.contains('hm-sw-header__line-column--type'))
check('Line style On: #211D13, (ByL) #D4CBBD, 1 column (line types), right edges meet (or shifted to the 4px screen edge when no room)', f.backgroundColor === 'rgb(33, 29, 19)' && unitOn.color === 'rgb(212, 203, 189)' && cols.length === 1 && typeOnly && (Math.abs(d.x + d.w - (f.x + f.w)) < 0.5 || d.x === 4) && Math.abs(d.y - (f.y + f.h)) < 0.5, { f, d, cols, typeOnly })
check('Line style drop 182–240 x 190 (type column grows only for wider font names)', d.w >= 182 && d.w <= 240 && Math.abs(d.w - cols[0]) < 0.5 && d.h === 190, { w: d.w, h: d.h, cols })
await shot('on-line', page)
await drop.getByRole('button', { name: 'ByBlock', exact: true }).nth(1).click()
check('Weight column: callback only, stays open', (await log()).includes('lineWeight(w-byblock)') && await drop.count() === 1, await log())
// No weight column: the only ByBlock entry is the line type, and no weight / scale callback was called.
const byBlock = await drop.getByRole('button', { name: 'ByBlock', exact: true }).count()
check('Weight column: absent (ByBlock only as a line type), no lineWeight / lineScale callback, list stays open', byBlock === 1 && !/lineWeight|lineScale/.test(await log()) && await drop.count() === 1, { byBlock, log: await log() })
await drop.getByRole('button', { name: 'CENTER' }).click()
const segs = await field('line').locator('.hm-sw-header__line-preview > i').count()
check('Line type select: property(line, CENTER), closes, preview = CENTER pattern (4 segments)', (await log()).includes('property(line, CENTER)') && await drop.count() === 0 && segs === 4, { segs })
@@ -212,7 +239,7 @@ const want = { none: [314, 256], all: [314, 361], circle: [314, 658], expanded:
check('4 panel states: 314 x 256 / 361 / 658 / 754', sizes.every(([k, w, h]) => w === want[k][0] && h === want[k][1]), sizes)
await shot('compare', page.locator('.sw-story-compare'))
// The header-hosted panel: layer field opens its list over the header page as well.
await page.goto(`${base}/iframe.html?id=sw-header--property-panel&viewMode=story`)
await page.goto(`${fixture}?view=header`)
await page.locator('.hm-sw-header').waitFor()
await page.getByRole('button', { name: '원 1개', exact: true }).click()
await page.locator('.hm-sw-header [data-property-panel-trigger]').click()
@@ -223,7 +250,7 @@ const stillOpen = await page.locator('.hm-sw-property').count()
check('Header-hosted panel: layer list attached; Escape closes list only', hosted && Math.abs(hosted.right) < 0.5 && Math.abs(hosted.top) < 0.5 && stillOpen === 1, { hosted, stillOpen })
check('no console / page errors', report.errors.length === 0, report.errors)
await writeFile(path.join(out, `${label}-result.json`), JSON.stringify(report, null, 2))
await browser.close()
await browser.close(); server.close()
const failed = report.checks.filter(c => c.status === 'FAIL').length
console.log(`${report.checks.length - failed}/${report.checks.length} PASS`)
process.exit(failed ? 1 : 0)
+24 -7
View File
@@ -1,6 +1,6 @@
{
"package": "@hm-design/sw",
"approvedCommit": "8cb98b373f9e5321c8d6c032a6d2031e942dc508",
"approvedCommit": "3cbe87991010f08b131e8b80c9f478c3fa721004",
"manifest": {
"name": "@hm-design/sw",
"exports": {
@@ -137,6 +137,18 @@
"SWPopupRowProps": "type",
"SWPopupSection": "value",
"SWPopupSectionProps": "type",
"SWProcessAppearance": "type",
"SWProcessBar": "value",
"SWProcessBarProps": "type",
"SWProcessGroup": "type",
"SWProcessHeader": "value",
"SWProcessHeaderHideableControlId": "type",
"SWProcessHeaderLayer": "type",
"SWProcessHeaderProps": "type",
"SWProcessSplitButton": "value",
"SWProcessSplitButtonProps": "type",
"SWProcessSplitType": "type",
"SWProcessStep": "type",
"SWRecentFiles": "value",
"SWRecentFilesProps": "type",
"SWSelect": "value",
@@ -207,7 +219,7 @@
"components/sw-header/components/SWHeaderDropdown.d.ts": "06ce5aa914826b6dc85df923a24baded80d16f43aebce39845f5b18d9d996504",
"components/sw-header/components/SWHeaderField.d.ts": "741fdd1b0e7da4cf8f7adeaec021eb4eb103d7312f34e80135d00c3510159901",
"components/sw-header/components/SWHeaderFields.d.ts": "43950332583b4c74231a793e18651b45494165626cc45e3dfcf50169ec189edc",
"components/sw-header/components/SWHeaderHome.d.ts": "aa1e56611e92f309d588fcb12c089bfb0cd336de050d9aca2f043118ba01ae54",
"components/sw-header/components/SWHeaderHome.d.ts": "656f93b57e670cb422946abdf711ebe779ddff41786159cdb6f4091bf366f7ee",
"components/sw-header/components/SWHeaderInput.d.ts": "6b379ee70b9bcbf6e9a0808ca891b86838a9bfa355bfa12f036088b26241dd9c",
"components/sw-header/components/SWHeaderLayerIcons.d.ts": "bb4b955a7850c445a3ff14c0e25034124dca49208aed0fafd922878ff7099ac5",
"components/sw-header/components/SWHeaderLayerMenu.d.ts": "0fb65b14f52cd2abf54e74ea09ed4b783fe1665416dfd910bd6cd7d3e1995711",
@@ -219,23 +231,28 @@
"components/sw-header/components/SWHeaderWindowControls.d.ts": "db8bfbd14340e03c28e407f58fc08cef55b9eef49f406ba53fc50e994f5f0834",
"components/sw-header/index.d.ts": "958a64efd05f7b8ab683ecb342fdcdef488db0be97cf9a2bf5ad3e3ac4632ab7",
"components/sw-header/SWHeader.d.ts": "3adac1db372451d9f86a6f6df1df0e9696cd40c3ed7945531ee378da122136d8",
"components/sw-header/types.d.ts": "fa7602374753bf7b6b48c0fa9f1c30a932ed12ccb4d16488c836e964b76f9122",
"components/sw-header/types.d.ts": "fdc3fafb111331f2edd754029bdc78231b18e563df662b849d8a6a8eaf30ebcd",
"components/sw-home-menu/index.d.ts": "b1f67a4e16c7b3a64b93f348bfb17fcd2ada999527cb93ccb77ca6308564091c",
"components/sw-home-menu/SWHomeMenu.d.ts": "2ce530d253374a8b20a20d8b6ddd5c733abfc8d518162d5a7490d432986a662c",
"components/sw-home-menu/SWRecentFiles.d.ts": "0c3c8ececc537ab6a03ce14a91dd1c878e9539b4022ce486ebe161262fb2c710",
"components/sw-home-menu/types.d.ts": "81499bc8edf2f2acfb2f556d7f397019ba99046a808e0607cb96a29a67b6f315",
"components/sw-popup/SWPopup.d.ts": "9af7b5f34bed9c863e343018ed64dc6960d09f5664fdfc1c26fcc7204acdd041",
"components/sw-process-header/index.d.ts": "1fe66c0854bac5f2fa0eeb2bd0bb16056ab1af08750b9911841d34881ec7abb5",
"components/sw-process-header/SWProcessBar.d.ts": "51e69c5dfeeaecee0db96b3b1511ee32d69667ea85aa67e01210bc9fe70854fb",
"components/sw-process-header/SWProcessHeader.d.ts": "e7740f8a1131963a3e43b271ff1664fdd9f06a5fca60217501d7598f557e6b65",
"components/sw-process-header/SWProcessSplitButton.d.ts": "b865f46609301677f9686a6d6d00e8caacfc94e5fdc776eb87fb8f93184f96ff",
"components/sw-process-header/types.d.ts": "02778af5e73c4b3be04bc8fe939a105fe3588526ce10614cf43aebe1fce68849",
"components/sw-select/SWSelect.d.ts": "9cf523e8a8329a2be7bdfba56c1a2c647b2676480efa3f6dfa26a971ea0a9c3b",
"components/sw-slider/SWSlider.d.ts": "7d9c48299cb5644d859753e1bc56ae43dc7ab282e64432d57d3e29b2960aaad5",
"components/sw-theme/SWTheme.d.ts": "cdcc2e7c4b80241afa4d0b7c3da671b7086c16f9b9c231c1927c931757e4244c",
"components/sw-tool/SWTool.d.ts": "23b1eb156578cd3f1dee325afbd81988d88c4a3f421f7350a0e8c47c652e804c",
"components/sw-viewer/index.d.ts": "e57a9ec0b91ae576b4b09b4851cd8e19b6fb4ea4bbdf62a578b4b94d7da6ffe2",
"components/sw-viewer/SWViewerControls.d.ts": "ca32e8a2a7de2cfefec36273ffb473271ad438cfb663f44a25871ee6aed9222d",
"components/sw-viewer/SWViewerHeader.d.ts": "586d07e74f5c2673fbe1d4849ed72cdcf80e7f8a391393168f101dc613490d24",
"components/sw-viewer/SWViewerControls.d.ts": "a75bc7d54db126d1bd9999cf12dc3b7edfb24cb16f75ba44b2e9dda422623aa2",
"components/sw-viewer/SWViewerHeader.d.ts": "0a46e07d7f6b33dc389cf4739e563be045cad12d84585384b1cc4d861c4acdbe",
"components/sw-viewer/SWViewerIcon.d.ts": "3c81582860e481b037b395b3584ff93af73500e4040d8423c9da15c2b4dd108e",
"components/sw-viewer/SWViewerMenu.d.ts": "15fa615c3472d84037780f3c93387acbc6b4d05f5fb56731b0a99262b3f7f109",
"components/sw-viewer/SWViewerPanel.d.ts": "49b040d3f3d00de30e3fc0493fd2ce78e309616ec97a7ce226dfef300b640f92",
"components/sw-viewer/types.d.ts": "e46b4605a3fcab78325bff526a374a891fdcf0256dc7824a9414d2336b2f85d7",
"index.d.ts": "34f0607f63df53d5a1186a4ad763a8bf7b433e20645d868f6f7368ed0853828c"
"components/sw-viewer/types.d.ts": "13b928b6871b9865e30929b37a17e6c820353f18477d96c1cc09a7d69e40cd18",
"index.d.ts": "1e156f688534c117f880c7bd32f06110240caff3d02157db0265edb8e3eb41f5"
}
}
+10 -2
View File
@@ -16,7 +16,15 @@ import { existsSync } from "node:fs"
import { readFile, writeFile } from "node:fs/promises"
import path from "node:path"
import { fileURLToPath, pathToFileURL } from "node:url"
import { approvedMainCommit } from "./storybook-inventory.mjs"
// Reviewed main merge the approved surface must belong to (2026-11-10: own baseline, split from approvedMainCommit in
// storybook-inventory.mjs, which also anchors the Story inventory / source regressions of overview, storybook-unused-
// removal, dropdown-consolidation and storybook-cleanup-stage1; moving that one would accept their unrecorded Story and
// source changes). Update it only in a reviewed test-only change that re-snapshots public-api.approved.json from a clean
// build of that merge after its .d.ts changes were compared with the previous approved build.
// History: approvedMainCommit fc65b2e (snapshot 8cb98b3) -> 3cbe879 (2026-11-10, Merge PR #30; snapshot from the clean
// 3cbe879 build: SWProcess* exports of the user-approved Step Header adbb034 / e700825, optional shortLabel? of the SWViewerHeaderItem switch item in
// cabd2f6, the internal SWHeaderLogo declaration of 205864b (not exported from the entry) and JSDoc-only changes).
const approvedPublicApiMainCommit = "3cbe87991010f08b131e8b80c9f478c3fa721004"
const packageRoot = fileURLToPath(new URL("../", import.meta.url))
const snapshotFile = fileURLToPath(new URL("./public-api.approved.json", import.meta.url))
@@ -82,7 +90,7 @@ if (process.argv[2] === "--snapshot") {
const results = []
const check = (name, ok, data) => { results.push(ok); console.log(ok ? "PASS" : "FAIL", name, data === undefined ? "" : JSON.stringify(data)) }
const ancestor = (a, b) => { try { execFileSync("git", ["merge-base", "--is-ancestor", a, b], { cwd: packageRoot, stdio: "ignore" }); return true } catch { return false } }
check("approved surface comes from a reviewed commit (ancestor of approvedMainCommit and HEAD)", ancestor(approved.approvedCommit, approvedMainCommit) && ancestor(approved.approvedCommit, "HEAD"), { approvedCommit: approved.approvedCommit, approvedMainCommit })
check("approved surface comes from a reviewed commit (ancestor of approvedPublicApiMainCommit and HEAD)", ancestor(approved.approvedCommit, approvedPublicApiMainCommit) && ancestor(approved.approvedCommit, "HEAD"), { approvedCommit: approved.approvedCommit, approvedPublicApiMainCommit })
const manifest = await manifestOf(manifestFile)
const changedFields = ["name", ...manifestFields].filter(field => JSON.stringify(manifest[field]) !== JSON.stringify(approved.manifest[field]))
@@ -60,10 +60,11 @@ const only = (map, expected) => Object.keys(map).length > 0 && Object.keys(map).
const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${family}`).join(' | ') || 'no text'
// More (2026-10-21): the header shows More only when its measured room runs out (full → icons → More). The Story's
// 1000px header fits as icons, so the header is narrowed for this check (as text-weight does), then restored.
// 1000px header fits as icons, so the header is narrowed for this check, then restored. 2026-11-10: 480px, since
// cabd2f6 (2026-10-26) the viewer header keeps every menu down to about 520px and shows More only below that.
async function openMore(page, area) {
const header = page.locator(`${area} .hm-sw-viewer-header`)
await header.evaluate(element => { element.style.width = "700px" })
await header.evaluate(element => { element.style.width = "480px" })
await page.locator(`${area} .hm-sw-viewer-header button[aria-label="추가 도구"]`).click()
}
const restoreWidth = (page, area) => page.locator(`${area} .hm-sw-viewer-header`).evaluate(element => { element.style.width = "" })
@@ -195,10 +196,45 @@ async function dropdownColors(page, def, themed) {
await page.close()
}
// ---------- 1b. Step Header (2026-11-10): --hm-sw-process-* copies of the SWHeader tokens it paints with ----------
// Without a Step Header value an SWHeader theme on the outermost SW scope still reaches the Step Header; a
// --hm-sw-process-* value there changes the Step Header alone (it wins over the SWHeader token) and leaves SWHeader as is.
{
const page = await newPage()
await open(page, 'sw-step-header--process-header')
const outerOf = selector => page.locator(selector).first().evaluateHandle(element => { let outer = element; for (let e = element; e; e = e.parentElement) if (e.matches?.('.hm-sw-header, .hm-sw-theme')) outer = e; return outer })
const scope = await outerOf('.hm-sw-process-header')
const set = async (values) => scope.evaluate((outer, values) => Object.entries(values).forEach(([token, value]) => value === null ? outer.style.removeProperty(token) : outer.style.setProperty(token, value)), values)
const read = () => page.evaluate(() => {
const header = getComputedStyle(document.querySelector('.hm-sw-process-header'))
const step = document.querySelector('.hm-sw-process-bar__step[aria-checked="true"]')
return { background: `${header.backgroundColor} ${header.backgroundImage}`, selected: step ? getComputedStyle(step).color : null }
})
const base = await read()
await set({ '--hm-sw-header-background': 'rgb(44, 55, 66)', '--hm-sw-header-selected-color': 'rgb(11, 22, 33)' })
const inherited = await read()
check('Step Header: 바깥 범위의 SWHeader 토큰(배경·선택 색)이 그대로 전달', base.selected !== null && inherited.background.includes('rgb(44, 55, 66)') && inherited.selected === 'rgb(11, 22, 33)', JSON.stringify({ base, inherited }))
await set({ '--hm-sw-process-background': 'rgb(1, 2, 3)', '--hm-sw-process-selected-color': 'rgb(77, 88, 99)' })
const own = await read()
check('Step Header: --hm-sw-process-* 지정이 SWHeader 토큰보다 우선', own.background.includes('rgb(1, 2, 3)') && own.selected === 'rgb(77, 88, 99)', JSON.stringify(own))
await set({ '--hm-sw-header-background': null, '--hm-sw-header-selected-color': null, '--hm-sw-process-background': null, '--hm-sw-process-selected-color': null })
check('Step Header: 지정을 지우면 기본값으로 복귀', JSON.stringify(await read()) === JSON.stringify(base), JSON.stringify(base))
await page.close()
// The same Step Header tokens on an SWHeader page change nothing there.
const header = await newPage()
await open(header, 'sw-header--overview')
const paint = () => header.evaluate(() => [...document.querySelectorAll('.hm-sw-header, .hm-sw-header *')].map(element => { const s = getComputedStyle(element); return `${s.color}|${s.backgroundColor}|${s.backgroundImage}|${s.boxShadow}|${s.borderTopColor}` }).join('\n'))
const before = await paint()
await header.locator('.hm-sw-header').first().evaluate(element => { let outer = element; for (let e = element; e; e = e.parentElement) if (e.matches?.('.hm-sw-header, .hm-sw-theme')) outer = e; ['background', 'height', 'text-color', 'selected-color', 'hover-border-gradient', 'document-background', 'object-background', 'property-background', 'window-icon-color'].forEach(name => outer.style.setProperty(`--hm-sw-process-${name}`, 'rgb(1, 2, 3)')) })
check('Step Header 토큰은 SWHeader 화면에 영향 없음', before === await paint(), `${before.split('\n').length} elements`)
await header.close()
}
// ---------- 2. Regression: new cascade vs legacy cascade on every existing SW Story ----------
const here = path.dirname(fileURLToPath(import.meta.url))
const outermost = ':is(.hm-sw-header, .hm-sw-theme):not(:where(.hm-sw-header, .hm-sw-theme) *)'
const tokenBlocks = (await Promise.all(['sw.css', 'sw-viewer.css', 'sw-footer.css'].map(file => readFile(path.join(here, '../src/theme', file), 'utf8'))))
// 2026-11-10: sw-process-header.css (Step Header aliases) and the Step Header Stories joined the comparison.
const tokenBlocks = (await Promise.all(['sw.css', 'sw-viewer.css', 'sw-footer.css', 'sw-process-header.css'].map(file => readFile(path.join(here, '../src/theme', file), 'utf8'))))
.map(text => {
const start = text.indexOf(`${outermost} {`)
const end = text.indexOf('\n}', start)
@@ -211,7 +247,7 @@ const legacy = tokenBlocks.map(block => `.hm-sw-header,\n.hm-sw-theme ${block}`)
// command row) even though no value changes, so the legacy shot is also compared with this control shot.
const control = tokenBlocks.map(block => `${outermost} ${block}`).join('\n')
const index = await (await fetch(`${storybook}/index.json`)).json()
const ids = Object.values(index.entries).filter(entry => entry.type === 'story' && /^sw-(header|popup|viewer|footer|home-menu)/.test(entry.id)).map(entry => entry.id)
const ids = Object.values(index.entries).filter(entry => entry.type === 'story' && /^sw-(header|popup|viewer|footer|home-menu|step-header)/.test(entry.id)).map(entry => entry.id)
const snapshot = page => page.evaluate(() => [...document.querySelectorAll('.hm-sw-header, .hm-sw-theme')].flatMap(root => [root, ...root.querySelectorAll('*')]).map(element => {
const style = getComputedStyle(element), rect = element.getBoundingClientRect()
return `${style.fontFamily}|${style.fontSize}|${style.color}|${style.backgroundImage.slice(0, 80)}|${Math.round(rect.x * 10)},${Math.round(rect.y * 10)},${Math.round(rect.width * 10)},${Math.round(rect.height * 10)}`
+1 -1
View File
@@ -30,7 +30,7 @@ Brown·Orange 계열 팔레트(`--hm-sem-palette-brown-*`, `--hm-sem-palette-ora
| 페이지·콘텐츠 배경 | `--hm-sem-bg-content` | Beige |
| 카드·팝업 표면 | `--hm-sem-bg-surface` | White |
| 보조 면 | `--hm-sem-bg-subtle` | warm-neutral-muted |
| 브랜드 배경(gradient) | `--hm-sem-bg-brand`, `--hm-sem-bg-brown`, `--hm-sem-bg-orange` | 세로 gradient(GNB·Brand 버튼 면) |
| 브랜드 배경(gradient) | `--hm-sem-bg-brand`, `--hm-sem-bg-brown`, `--hm-sem-bg-orange` | 공통 세로 gradient 배경(소비 화면 배경용). 패키지 컴포넌트(GNB·Button·Dashboard)에는 자동 연결되지 않음 — GNB 면은 `--hm-gnb-background`(저장소 `docs/guidelines/runtime-theme-contract.md` "Brown·Orange 공통 Background") |
| 기본 글자 | `--hm-sem-fg-primary` | green-dark-1 |
| 보조 글자 | `--hm-sem-fg-secondary` | warm-neutral-dark |
| placeholder | `--hm-sem-fg-placeholder` | gray-500 |
+1 -1
View File
@@ -1,5 +1,5 @@
/* LNB pattern (layout visual reference: D:\컴포넌트\ui-viz Patterns/LNB, implementation: HM Collapsible + plain elements).
A 272px green column: horizontal 5-stop surface, brand on top, menu in the middle, text-only profile at the bottom. The current
A 240px green column (--hm-lnb-width; 272px before 2026-11-09): horizontal 5-stop surface, brand on top, menu in the middle, text-only profile at the bottom. The current
item is a Beige face (left corners rounded) that runs to the content edge and joins it through two concave corners; hover is
Black 10% to the same edge; an open group is a Black 8% face with a 1px green line. Spacing values are pattern-local. */
:root, [data-hm-theme] {
+4 -2
View File
@@ -50,7 +50,9 @@ export const AUX_ITEMS: SWFooterAuxItem[] = [
* Labels, categories and the extra command are program data; WIPEOUT and 객체스냅추적 have no Figma icon, so the
* Story passes its own (lucide) icons the way a program would.
*/
const lucide = (node: ReactNode) => <span className="sw-footer-story-lucide">{node}</span>
const lucide = (node: ReactNode, outlined = false) => (
<span className={outlined ? "sw-footer-story-lucide sw-footer-story-lucide--outlined" : "sw-footer-story-lucide"}>{node}</span>
)
export const REFERENCE_TOOLS: SWFooterTool[] = [
{ id: "line", label: "LINE", ...swFooterToolIcons.line },
{ id: "pline", label: "PLINE", ariaLabel: "Polyline", ...swFooterToolIcons.pline },
@@ -74,7 +76,7 @@ export const REFERENCE_AUX: SWFooterAuxItem[] = [
{ id: "lineWeight", label: "선가중치", icon: swFooterAuxIcons.lineWeight, pressed: false },
{ id: "gridBackground", label: "격자배경", icon: swFooterAuxIcons.gridBackground, pressed: true },
{ id: "gridSnap", label: "격자스냅", icon: swFooterAuxIcons.gridSnap, pressed: false },
{ id: "objectTrack", label: "객체스냅추적", icon: lucide(<PenLine size={14} />), pressed: true },
{ id: "objectTrack", label: "객체스냅추적", icon: lucide(<PenLine size={14} />, true), pressed: true },
]
export const REFERENCE_HISTORY = [
"Raster image support loaded.",
+5
View File
@@ -27,6 +27,11 @@
.sw-footer-story-guide-frame { display: block; width: 194px; height: 111px; background-repeat: no-repeat; background-size: 250px 1179px; }
.sw-footer-story-guide-text { display: grid; place-items: center; height: 100%; padding: 8px; font-size: 12px; color: #d4cbbd; text-align: center; }
.sw-footer-story-lucide { display: grid; place-items: center; }
/* Aux icons carry a 1px OUTSIDE black 90% outline (AuxIcon); a stroke icon has none, so the same outline is drawn around its alpha. */
.sw-footer-story-lucide--outlined svg {
filter: drop-shadow(1px 0 0 rgba(0, 0, 0, .9)) drop-shadow(-1px 0 0 rgba(0, 0, 0, .9))
drop-shadow(0 1px 0 rgba(0, 0, 0, .9)) drop-shadow(0 -1px 0 rgba(0, 0, 0, .9));
}
.sw-footer-story-status { padding: 0 12px; font-size: 12px; color: #d4cbbd; }
.sw-footer-story-row { display: flex; align-items: flex-start; gap: 16px; }
.sw-footer-story-row--tight { gap: 0; }