diff --git a/docs/sw/sw-header-design-spec.md b/docs/sw/sw-header-design-spec.md index 6ecc06d..a73b7b7 100644 --- a/docs/sw/sw-header-design-spec.md +++ b/docs/sw/sw-header-design-spec.md @@ -557,7 +557,8 @@ Composed Controls의 기존 레이어·색상·상세보기 예제는 보존하 | `--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-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-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-viewer-header-menu-*`를 읽음 | +| `--hm-sw-header-k-guide-border-color` / `-k-guide-background` | `var(--hm-sw-property-border)`(`#A67D03`) / `#0E3C2E` | K 가이드 패널 테두리·면(2026-11-10 개별 토큰, 값 변경 없음). 속성창 테두리와 독립 | | `--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) | @@ -589,7 +590,7 @@ Composed Controls의 기존 레이어·색상·상세보기 예제는 보존하 | `--hm-sw-header-menu-list-max-height` / `-color-palette-max-height` | 240px / 118px | 긴 목록·사용자 색상 내부 스크롤 시작(구현값) | | `--hm-sw-header-color-mode-color` / `-color-mode-background` / `-color-mode-on-background` | `#D4CBBD` / `rgba(98,83,61,.3)` / `#62533D` | ByLayer / ByBlock | | `--hm-sw-header-color-pick-background` / `-color-chip-border-color` / `-color-input-border-color` | `#C5B19E → #5F554C` / `#C9C9C9` / `rgba(197,177,158,.2)` | 색상 추출, 22px 칩, 색상 입력 Hover·구분선 | -| `--hm-sw-header-input-color` / `-input-hover-border-color` / `-input-editing-background` / `-input-editing-border-color` | `#E60403` / `rgba(212,203,189,.25)` / `#0F0D0A` / `rgba(212,203,189,.3)` | `SWHeaderInput`(§5.5), caret 색 포함 | +| `--hm-sw-header-input-color` / `-input-hover-border-color` / `-input-editing-background` / `-input-editing-border-color` | `#E60403` / `rgba(212,203,189,.25)` / `#0F0D0A` / `rgba(212,203,189,.3)` | `SWHeaderInput`(§5.5), caret 색 포함. Popup(속성창·`SWPopupRow`·뷰어 패널) 값 글자색은 이 값을 기본값으로 하는 `--hm-sw-popup-value-color`(2026-11-10)를 읽음 | ## 15. Figma 비교 결과와 의도적 차이 diff --git a/packages/sw/ai/AI_USAGE.md b/packages/sw/ai/AI_USAGE.md index e48f607..b4f5b58 100644 --- a/packages/sw/ai/AI_USAGE.md +++ b/packages/sw/ai/AI_USAGE.md @@ -107,7 +107,7 @@ import "@hm-design/sw/styles.css" // 앱 진입점에서 한 번 - SW 화면은 `SWTheme`(또는 SW 컴포넌트 루트)와 SW 토큰의 글꼴을 그대로 쓴다. Web UI도 Noto Sans KR이지만 Web 토큰(`--hm-font-family-base`)이나 Web `theme.css`의 글꼴 설정을 SW 화면에 복사하지 않고, SW 컴포넌트에 `font-family`를 개별 지정하지 않는다. 프로그램(사용자)이 다른 글꼴을 정한 경우에만 §3.1 방법으로 `--hm-sw-header-font-family`를 바꾸고 결과 보고에 "글꼴 변경"으로 적는다. - Weight 범위(2026-10-20 사용자 확정): SWHeader 안의 필드 값은 400, Composed Controls(SWHeader 밖에 단독 배치한 `SWHeaderLayerMenu` 등)의 필드 값은 700. 파일명·Footer 보조기능/명령어 제목·Footer 분류 탭·Viewer Header 파일명/버튼 라벨/Model·Layout·최근파일 이름/경로/미리보기 제목, Footer Model/Layout 탭 라벨(모든 상태, 2026-10-22 사용자 확정, 이름 편집 입력은 400)은 700, Footer `전체보기`·명령·보조기능 라벨은 400. 임의로 일괄 변경하지 않는다. Figma 고정 폭 칸(예: 가중치·스케일 38px)에 그보다 긴 값(예: `ByLayer`)이 들어가면 말줄임 + `title`이 된다. Figma 레이어 이름의 `Dotum/bold/14`를 보고 700으로 바꾸거나 겹침·외곽선을 따로 만들지 않는다. - CSS는 전역(`html`, `body`, `*`)을 건드리지 않는다. `body` margin 등 페이지 기본값은 소비 앱이 정한다. -- 스크롤바: SW 전용 스크롤바 디자인은 없다. 기존 SW 컴포넌트는 브라우저 기본 스크롤바를 쓰며(속성Bar만 `scrollbar-width: thin`), Popup 본문 스크롤은 `SWPopup`이 처리한다. 새 스크롤 영역에 임의 스크롤바 색·두께를 만들지 않는다. +- 스크롤바: 일반 스크롤바 디자인은 없다. 패키지가 정한 곳만 `scrollbar-width: thin`이다 — 속성Bar, Popup 본문(`SWPopup`·`SWViewerPanel`·속성창, `--hm-sw-popup-scrollbar-thumb`·`-track`), Footer 명령창 기록(`--hm-sw-footer-command-scrollbar-thumb`·`-track`, 2026-11-10). 그 밖의 SW 컴포넌트는 브라우저 기본 스크롤바를 쓴다. 새 스크롤 영역에 임의 스크롤바 색·두께를 만들지 않는다. - TypeScript에서 CSS side-effect import 오류가 나면 Vite의 `vite/client` 타입을 포함한다. ## 3. 스타일 규칙 @@ -138,6 +138,12 @@ SW 토큰(`--hm-sw-header-*`, `--hm-sw-property-*`, `--hm-sw-popup-*`, `--hm-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. - Viewer Header(`SWViewerHeader`, 2026-11-10): 파일명·객체 수정·속성 면의 SWHeader 토큰 10개를 `--hm-sw-viewer-document-*`·`--hm-sw-viewer-object-*`·`--hm-sw-viewer-property-*` alias(기본값 `var(--hm-sw-header-…)`)로 읽습니다. 기본은 SWHeader 테마 공유 그대로이고, 가장 바깥 범위에 `--hm-sw-viewer-*`를 정하면 Viewer Header만 바뀝니다. 안쪽 요소에서 바꾼 `--hm-sw-header-*`는 이 alias에 전달되지 않습니다. 목록·적용 범위는 [DESIGN_RULES.md](./DESIGN_RULES.md) §11. +- 컴포넌트 전용 색 토큰(2026-11-10, 값 변경 없음): 아래 9개는 같은 방식(가장 바깥 범위에서 정함, 기본값은 기존 토큰 또는 기존 값)이며, 정하지 않으면 지금 모양 그대로이고 정하면 해당 부분만 바뀝니다. + - Viewer Header 메뉴(로고 메뉴·Header 안 Dropdown·More): `--hm-sw-viewer-header-menu-background`·`-border-color`·`-shadow`·`-item-hover-background`(기본 SWHeader `--hm-sw-header-more-*`). SWHeader `...` 메뉴는 그대로입니다. [DESIGN_RULES.md](./DESIGN_RULES.md) §11. + - SWHeader K 가이드 패널: `--hm-sw-header-k-guide-border-color`(기본 `var(--hm-sw-property-border)`)·`--hm-sw-header-k-guide-background`(`#0e3c2e`). 속성창 테두리는 그대로입니다. §5. + - Popup 값 글자색: `--hm-sw-popup-value-color`(기본 `var(--hm-sw-header-input-color)`). 속성Bar 값은 그대로입니다. §8. + - Footer 명령창 기록 스크롤바: `--hm-sw-footer-command-scrollbar-thumb`(`#574b30`)·`-track`(`transparent`). §13. + - 공유 유지(전용 토큰 없음): Focus 링(`--hm-sw-header-focus-color`, Footer 포함), 글자색·글자 외곽선, Viewer 메뉴 구분선. ## 4. 판단 기록 남기기 diff --git a/packages/sw/ai/DESIGN_RULES.md b/packages/sw/ai/DESIGN_RULES.md index 0307e85..5157c3f 100644 --- a/packages/sw/ai/DESIGN_RULES.md +++ b/packages/sw/ai/DESIGN_RULES.md @@ -71,7 +71,8 @@ - 목록 최대 높이 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)). +- 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·로고 메뉴는 기본값이 이 4개인 `--hm-sw-viewer-header-menu-*` alias를 읽습니다(§11, [VIEWER.md](./VIEWER.md)). 정하지 않으면 같은 색이고, alias를 정하면 뷰어 메뉴만 바뀝니다. +- K 가이드 패널 색(2026-11-10 개별 토큰, 값 변경 없음): 테두리 `--hm-sw-header-k-guide-border-color`(기본 `var(--hm-sw-property-border)`, `#A67D03`), 면 `--hm-sw-header-k-guide-background`(`#0E3C2E`). 정하면 K 가이드만 바뀌고 속성창 테두리(`--hm-sw-property-border`)는 그대로입니다. 문구 글자·외곽선은 `--hm-sw-header-text-color`·`--hm-sw-header-label-outline` 그대로입니다. ## 6. Motion @@ -111,7 +112,8 @@ | 구분선 | 개수 목록 아래선 검정 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` | +| 행(`SWPopupRow`) | 최소 32px, 라벨 72px 700 검정, 값 글자 `#E60403` 500, 단위 12px | `--hm-sw-property-label`, `--hm-sw-popup-value-color` | +| 값 글자색(2026-11-10 개별 토큰, 값 변경 없음) | 편집 가능한 값 `#E60403`: 속성창·`SWPopupRow`의 값 Input(값·게이지 값·색상 값 칸), 값 `select`, 속성창 안 `SWSelect`(사용 가능), Dropdown Trigger, `SWViewerPanel`의 `SWViewerInput`(검색 제외). 읽기 전용 값은 라벨색 그대로. 기본 `var(--hm-sw-header-input-color)`라 Header 테마를 따르고, 정하면 Popup 값만 바뀜(SWHeader 속성Bar 값은 `--hm-sw-header-input-color`) | `--hm-sw-popup-value-color` | | 값 상자 | 28px, Hover 1px `rgba(33,29,19,.4)`, 숫자 편집 On `#0F0D0A`(단위 흰색), Dropdown On `#211D13` | `--hm-sw-property-field-*` | | 동작 | Non-modal(Dim·Portal·Focus trap 없음). Escape는 Popup 안이면 그 Popup, 밖이면 최근 사용 Popup 하나. `autoFocus`면 열릴 때 Focus, 안에서 닫히면 원래 위치로 | — | @@ -160,7 +162,8 @@ - **Model / Layout 탭 토큰:** 위 `--hm-sw-viewer-tabs-*`·`--hm-sw-viewer-tab-*`은 탭 컨테이너·버튼에만 연결되며 다른 Header 부품·SWHeader·Footer·패널에 쓰이지 않습니다. 탭 색만 바꾸려면 가장 바깥 SW 범위의 `style`로 덮습니다: 화면을 ``로 감싸거나 단독 `SWViewerHeader`의 루트에 지정합니다(토큰이 그 범위에 선언되므로 일반 `div` 래퍼의 값은 가려짐). 예: `style={{ "--hm-sw-viewer-tab-active-border": "linear-gradient(#e07b00, #e07b00)", "--hm-sw-viewer-tab-inactive-border": "linear-gradient(#3d6bb3, #3d6bb3)" }}`. `var(...)` 기본값(`active-text`·`hover-*`·`focus-color`)은 SW 범위에서 계산되므로 공통 토큰(`--hm-sw-viewer-accent` 등)을 바꾸면 같은 범위에서 함께 따라가고, 탭 토큰을 직접 지정하면 공통 토큰과 독립합니다. 글자 외곽선(`text-shadow`)은 탭에서도 기존 그대로(이름 `--hm-sw-header-text-outline` 1.3px, 개수 `--hm-sw-header-label-outline` 1px)이며 탭 토큰이 아닙니다. 탭 class로 색을 덮지 않습니다. - **공유 토큰(모드 혼합 아님):** 뷰어 Header의 파일명·객체 수정·속성 면은 `--hm-sw-header-document-*`, `--hm-sw-header-object-*`, `--hm-sw-header-property-*`를, 글자 외곽선은 `--hm-sw-header-text-outline`·`--hm-sw-header-label-outline`을 그대로 읽습니다(§2·§3). 패널 제목 막대 추가 버튼(`ghost`)의 Hover·Focus 색은 `--hm-sw-header-selected-color`입니다. -- **Viewer Header 전용 덮어쓰기(2026-11-10, 공유 유지):** 위 면 토큰 10개는 같은 이름의 `--hm-sw-viewer-*` alias(가장 바깥 SW 범위, 기본값 `var(--hm-sw-header-…)`)를 거쳐 읽습니다 — `--hm-sw-viewer-document-background`·`-document-border-gradient`·`-document-border-color`, `--hm-sw-viewer-object-background`·`-object-border-gradient`·`-object-radius`, `--hm-sw-viewer-property-background`·`-property-border-color`·`-property-radius`·`-property-shadow`. 적용 범위: 파일명 면, 객체 수정 면, 속성 입력 그룹(`data-surface="field"`), 오른쪽 상태·버튼 띠와 그 시작 조각. 기본 동작은 그대로 공유입니다(정하지 않으면 SWHeader 테마를 따름). Viewer Header만 바꾸려면 가장 바깥 SW 범위(`` 또는 단독 `SWViewerHeader`의 `style`)에 `--hm-sw-viewer-*`를 지정합니다(SWHeader·Step Header 영향 없음). 제약: alias는 가장 바깥 범위에서 계산되므로 안쪽 요소에서만 바꾼 `--hm-sw-header-*`는 이 10개에 전달되지 않습니다. 파일명 버튼 등 재사용 SWHeader 부품의 `sw.css` 규칙(로고가 있을 때 로고 칸 바탕 — SWHeader 홈과 같은 공용 규칙 `.hm-sw-header__home::before, .hm-sw-viewer-header__brand[data-logo]::before` — 포함), Dropdown·More·파일 목록, 패널(`selected-color`·`input-color`), 글자 외곽선은 계속 `--hm-sw-header-*`를 읽습니다. +- **Viewer Header 전용 덮어쓰기(2026-11-10, 공유 유지):** 위 면 토큰 10개는 같은 이름의 `--hm-sw-viewer-*` alias(가장 바깥 SW 범위, 기본값 `var(--hm-sw-header-…)`)를 거쳐 읽습니다 — `--hm-sw-viewer-document-background`·`-document-border-gradient`·`-document-border-color`, `--hm-sw-viewer-object-background`·`-object-border-gradient`·`-object-radius`, `--hm-sw-viewer-property-background`·`-property-border-color`·`-property-radius`·`-property-shadow`. 적용 범위: 파일명 면, 객체 수정 면, 속성 입력 그룹(`data-surface="field"`), 오른쪽 상태·버튼 띠와 그 시작 조각. 기본 동작은 그대로 공유입니다(정하지 않으면 SWHeader 테마를 따름). Viewer Header만 바꾸려면 가장 바깥 SW 범위(`` 또는 단독 `SWViewerHeader`의 `style`)에 `--hm-sw-viewer-*`를 지정합니다(SWHeader·Step Header 영향 없음). 제약: alias는 가장 바깥 범위에서 계산되므로 안쪽 요소에서만 바꾼 `--hm-sw-header-*`는 이 10개에 전달되지 않습니다. 파일명 버튼 등 재사용 SWHeader 부품의 `sw.css` 규칙(로고가 있을 때 로고 칸 바탕 — SWHeader 홈과 같은 공용 규칙 `.hm-sw-header__home::before, .hm-sw-viewer-header__brand[data-logo]::before` — 포함), 파일 목록, 패널 `selected-color`, 글자 외곽선은 계속 `--hm-sw-header-*`를 읽습니다. Header 안 Dropdown·More·로고 메뉴는 아래 메뉴 alias, 패널 값 글자색은 `--hm-sw-popup-value-color`(§8)를 읽습니다. +- **Viewer Header 메뉴 전용 덮어쓰기(2026-11-10, 공유 유지):** 로고 메뉴와 Header 안 Dropdown(메뉴 버튼·More·하위 메뉴)은 `--hm-sw-viewer-header-menu-background`·`-border-color`·`-shadow`·`-item-hover-background`(가장 바깥 SW 범위, 기본값 각각 `var(--hm-sw-header-more-menu-background)`·`var(--hm-sw-header-more-menu-border-color)`·`var(--hm-sw-header-more-menu-shadow)`·`var(--hm-sw-header-more-item-hover-background)`)를 읽습니다. Hover 토큰은 Focus·서브메뉴 열림에도 같습니다. 정하지 않으면 SWHeader `...` 메뉴와 같은 색이고, 정하면 Viewer Header 메뉴만 바뀝니다(SWHeader More 영향 없음). 글자 `--hm-sw-header-text-color`(보조 글자 80%)·구분선 `--hm-sw-header-document-menu-divider-color`·Disabled 0.5는 공유 그대로이고, 단독 `SWViewerMenu`는 위 `--hm-sw-viewer-menu-*`·`--hm-sw-viewer-more-*`를 씁니다. 제약은 면 alias와 같습니다(안쪽 요소에서만 바꾼 `--hm-sw-header-more-*`는 전달되지 않음). | 상태 | Header 항목(button·toggle·menu·tabs·switch) | 패널 콘텐츠 | |---|---|---| @@ -216,7 +219,7 @@ | Layout 탭 | 높이 22, 12px 자간 -5%(Model 포함, 2026-10-22; 이전 2%), 라벨 700(모든 상태, 2026-10-22; 이름 편집 입력은 400). Default 면 `#C3B79E` 글자 `#161410`. On: `linear-gradient(0deg, #7C6C4C, #7C6C4C 85%, #A6946F)` 흰 글자. 이름 편집: `#0F0D0A` 빨강 `#E60403` 주황 테두리. Hover·On·편집 글자 아래 70% 검정 1.3px 외곽선. 둥근 탭: 1px `rgba(0,0,0,.05)`(Hover·편집 `#DA6813`), 위 radius 4, 안쪽 그림자 `inset 0 -3px 4px rgba(56,48,31,.25)`, ×(16) Hover 노랑. 사선 탭(마지막): 97 × 22 자산(9-slice, 2026-10-22: 97px·라벨 57px가 최소, 라벨이 길면 사선 21px·모서리 4px는 고정하고 가운데만 늘어남, 라벨 최대 12em), On hover = 베이지 면 + 주황 글자, ×(28, 빨강 사선). Hover(선택 아님, 두 모양 모두, 2026-10-20) = Model과 같은 `gra-border-orange` 면(사선 탭은 자산 모양 안)만 바뀌고 글자는 Default 그대로(`#161410` 12px 700, 외곽선 없음, 위치·크기 불변) | `--hm-sw-footer-tab-*`, `--hm-sw-footer-gra-border-orange` | | `+` 버튼 | Layout 옆 `+`(24 × 22) Hover = `gra-border-orange` 면 + `+`(분류 행 `+` 자산). 분류 행 `+`(28 × 12) Hover = `+` 채움 `#FF5C00`(검정 외곽선 그대로) | `--hm-sw-footer-gra-border-orange`, `--hm-sw-footer-accent` | | Model 탭 | 73 × 22 자산(왼쪽 사선, 9-slice: 73px 최소, 긴 라벨이면 사선 21px 고정·가운데만 늘어남, 라벨 최대 12em, 2026-10-22), 글자 왼쪽 22. Hover·On = 자산 모양 안 `gra-border-orange` 면 + 검정 `#161410` 글자(12px, 외곽선 없음)(2026-10-20 사용자 확정, 이전 Figma On 갈색·흰 글자 대체). 라벨은 모든 상태 700(2026-10-22). 앞 사선 탭과 8px 겹침 | `--hm-sw-footer-gra-border-orange` | -| 명령창 | 566 × 71 / 270, 위 radius 4, 위 71px `linear-gradient(180deg, #635537, #262218 5%, #281F16 10%, #282216)` + 아래 `#282216`. 12px 자간 10% 줄 14, 1px 외곽선. 입력 칸 23(좌우 4) `#16130D` + `0 -1px 2px rgba(255,255,255,.25)`, `inset 0 1px 4px #000`. 화살표 18 × 18 검정 radius 2(4, 4 오른쪽 위) | `--hm-sw-footer-command-*` | +| 명령창 | 566 × 71 / 270, 위 radius 4, 위 71px `linear-gradient(180deg, #635537, #262218 5%, #281F16 10%, #282216)` + 아래 `#282216`. 12px 자간 10% 줄 14, 1px 외곽선. 입력 칸 23(좌우 4) `#16130D` + `0 -1px 2px rgba(255,255,255,.25)`, `inset 0 1px 4px #000`. 화살표 18 × 18 검정 radius 2(4, 4 오른쪽 위). 기록 스크롤바 `thin`, Thumb `#574B30` / Track `transparent`(2026-11-10 개별 토큰, 값 변경 없음) | `--hm-sw-footer-command-*`(스크롤바 `--hm-sw-footer-command-scrollbar-thumb`·`-track`) | | 바 안 명령창(실제 화면) | 보조기능 뒤 4px, 바 아래에 붙어 펼치면 위로 270. 남는 폭(바 − 보조기능 영역 − 명령어 전체보기)을 칸 영역 0.57 : 명령창 나머지(최소 280)로 나눔 — 칸 수는 바 폭으로만 계산, 분류의 명령 수와 무관(1920 → 12칸) | `--hm-sw-footer-command-min-width`, `--hm-sw-footer-slot-share` | | 좌표(실제 화면) | 바 위 왼쪽, 12px `#F6F4F2` 1px 외곽선, 아래 3px | — | | 좁은 폭(실제 화면) | 빈 칸이 먼저 줄고, 넘치는 명령은 마지막 칸 ▲(ico_arrow 18 × 18, 오른쪽 위 4) → 명령 칸 위 패널(같은 칸, 오른쪽 맞춤, 테두리 1px `#886D35`·아래 없음·위 radius 4, 명령 칸 면). 분류 탭 넘침 = 탭 영역 잘림 + ◀ ▶(13 × 16, 6 × 8 삼각형 `#F6F4F2`, Hover `#FF5C00`) + +. 바 1279px 이하 보조기능 아이콘만 45 × 33 | `--hm-sw-footer-aux-compact-width` | diff --git a/packages/sw/ai/VIEWER.md b/packages/sw/ai/VIEWER.md index c71361b..1665722 100644 --- a/packages/sw/ai/VIEWER.md +++ b/packages/sw/ai/VIEWER.md @@ -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`의 `...` 메뉴와 같은 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`(우클릭 메뉴 등)는 기존 색 그대로입니다. +- **Dropdown 색(2026-10-21):** Header 안의 Dropdown — 메뉴 버튼(`type: "menu"`), More(⋯), 로고 메뉴 — 은 크기·간격·글자는 `SWViewerMenu` 그대로, 색은 기본적으로 일반 `SWHeader`의 `...` 메뉴와 같습니다. 2026-11-10부터 면·Border·그림자·Hover는 Viewer 전용 alias를 거쳐 읽습니다 — 면 `--hm-sw-viewer-header-menu-background`(기본 `var(--hm-sw-header-more-menu-background)`, #0e3d2f), Border `--hm-sw-viewer-header-menu-border-color`(기본 `var(--hm-sw-header-more-menu-border-color)` = `var(--hm-sw-header-object-border-color)`), 그림자 `--hm-sw-viewer-header-menu-shadow`(기본 `var(--hm-sw-header-more-menu-shadow)`, 0 4px 12px rgba(0,0,0,.5)), Hover·Focus·서브메뉴 열림 `--hm-sw-viewer-header-menu-item-hover-background`(기본 `var(--hm-sw-header-more-item-hover-background)` = `var(--hm-sw-header-hover)`). 이 4개를 가장 바깥 SW 범위에 정하면 Viewer Header 메뉴만 바뀌고 SWHeader `...` 메뉴는 그대로입니다. 글자 `--hm-sw-header-text-color`, 보조 글자는 같은 색 80%, 구분선 `--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`의 직접 그린 아이콘은 ``으로 감쌉니다. 색·크기·굵기·간격은 그대로이고 Hover/Active는 채움 색만 바뀝니다. Dropdown·패널 안에는 외곽선이 없습니다(SWHeader More 메뉴와 같음). 프로그램 아이콘은 `currentColor`로 그리면 상태 색과 외곽선이 자동 적용됩니다. - **면 색(SWHeader와 공유, 2026-11-10 Viewer 전용 덮어쓰기 추가):** 파일명·객체 수정·속성 입력 면과 오른쪽 상태·버튼 띠는 기본으로 SWHeader 토큰(`--hm-sw-header-document-*`·`-object-*`·`-property-*`)을 따릅니다. Viewer Header만 바꾸려면 같은 이름의 `--hm-sw-viewer-document-*`·`--hm-sw-viewer-object-*`·`--hm-sw-viewer-property-*`(10개)를 가장 바깥 SW 범위(`` 또는 단독 `SWViewerHeader`의 `style`)에 지정합니다. 안쪽 요소에서 바꾼 `--hm-sw-header-*`는 이 값에 전달되지 않습니다. 목록은 [DESIGN_RULES.md](./DESIGN_RULES.md) §11. diff --git a/packages/sw/src/theme/sw-footer.css b/packages/sw/src/theme/sw-footer.css index ce6286f..6a68068 100644 --- a/packages/sw/src/theme/sw-footer.css +++ b/packages/sw/src/theme/sw-footer.css @@ -94,6 +94,9 @@ --hm-sw-footer-command-input-shadow: 0 -1px 2px rgba(255, 255, 255, 0.25), inset 0 1px 4px #000; --hm-sw-footer-command-font-size: 12px; --hm-sw-footer-command-line-height: 14px; + /* History scrollbar (thin; 2026-11-10 individual tokens, values as before: the former scrollbar-color literals). */ + --hm-sw-footer-command-scrollbar-thumb: #574b30; + --hm-sw-footer-command-scrollbar-track: transparent; /* Not in Figma (reference screens 2026-10-20): the command line inside the bar keeps at least this width. */ --hm-sw-footer-command-min-width: 280px; /* Not in Figma: with the command line inside the bar, the slot area takes this share of the free width (bar minus @@ -613,7 +616,7 @@ overflow-y: auto; list-style: none; scrollbar-width: thin; - scrollbar-color: #574b30 transparent; + scrollbar-color: var(--hm-sw-footer-command-scrollbar-thumb) var(--hm-sw-footer-command-scrollbar-track); text-shadow: var(--hm-sw-header-label-outline); } .hm-sw-footer-command__history > li { white-space: pre; } diff --git a/packages/sw/src/theme/sw-viewer.css b/packages/sw/src/theme/sw-viewer.css index e2bca6d..9a69ce3 100644 --- a/packages/sw/src/theme/sw-viewer.css +++ b/packages/sw/src/theme/sw-viewer.css @@ -127,6 +127,17 @@ --hm-sw-viewer-property-border-color: var(--hm-sw-header-property-border-color); --hm-sw-viewer-property-radius: var(--hm-sw-header-property-radius); --hm-sw-viewer-property-shadow: var(--hm-sw-header-property-shadow); + + /* Viewer Header menus (2026-11-10): the logo menu and the header dropdowns (menu buttons, More, their submenus) read + these copies of the SWHeader `...` More tokens. Each defaults to the More token, so the shared look stays; set + --hm-sw-viewer-header-menu-* on the outermost SW scope to change the Viewer Header menus alone (SWHeader More + unchanged). Resolved on the outermost scope like the face aliases above. Text color and the divider stay shared + (--hm-sw-header-text-color, --hm-sw-header-document-menu-divider-color); a standalone SWViewerMenu keeps + --hm-sw-viewer-menu-* / -more-*. */ + --hm-sw-viewer-header-menu-background: var(--hm-sw-header-more-menu-background); + --hm-sw-viewer-header-menu-border-color: var(--hm-sw-header-more-menu-border-color); + --hm-sw-viewer-header-menu-shadow: var(--hm-sw-header-more-menu-shadow); + --hm-sw-viewer-header-menu-item-hover-background: var(--hm-sw-header-more-item-hover-background); } .hm-sw-viewer-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } @@ -225,7 +236,10 @@ button.hm-sw-viewer-header__brand { text-align: left; } min-width: 160px; padding: 4px; border-width: 1px; + border-color: var(--hm-sw-viewer-header-menu-border-color); border-radius: var(--hm-sw-viewer-radius); + background: var(--hm-sw-viewer-header-menu-background); + box-shadow: var(--hm-sw-viewer-header-menu-shadow); color: var(--hm-sw-header-text-color); font-size: 14px; font-weight: 400; @@ -237,7 +251,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-more-item-hover-background); outline: none; } +.hm-sw-viewer-header__logo-menu .hm-sw-header__more-item:is(:hover, :focus):not(:disabled) { background: var(--hm-sw-viewer-header-menu-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; } @@ -318,13 +332,13 @@ button.hm-sw-viewer-header__brand { text-align: left; } 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-more-menu-border-color); - background: var(--hm-sw-header-more-menu-background); - box-shadow: var(--hm-sw-header-more-menu-shadow); + border-color: var(--hm-sw-viewer-header-menu-border-color); + background: var(--hm-sw-viewer-header-menu-background); + box-shadow: var(--hm-sw-viewer-header-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-more-item-hover-background); + background: var(--hm-sw-viewer-header-menu-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; } @@ -880,8 +894,8 @@ button.hm-sw-viewer-header__brand { text-align: left; } border-color: transparent; border-radius: 2px; background: transparent; - color: var(--hm-sw-header-input-color); - caret-color: var(--hm-sw-header-input-color); + color: var(--hm-sw-popup-value-color); + caret-color: var(--hm-sw-popup-value-color); font-size: 14px; font-weight: 500; line-height: 16px; diff --git a/packages/sw/src/theme/sw.css b/packages/sw/src/theme/sw.css index 53758e7..a998e74 100644 --- a/packages/sw/src/theme/sw.css +++ b/packages/sw/src/theme/sw.css @@ -112,12 +112,21 @@ --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); + /* Popup value (2026-11-10): the red editable value of the property window / SWPopupRow (input, select, SWSelect, + dropdown trigger) and of SWViewerPanel inputs. Defaults to the header input color it used, so a header theme still + reaches it; set it to change popup values alone (the SWHeader 속성Bar keeps --hm-sw-header-input-color). */ + --hm-sw-popup-value-color: var(--hm-sw-header-input-color); /* 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. */ + viewer logo menu default to the same face and shadow (--hm-sw-viewer-header-menu-*, sw-viewer.css). 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); + /* SWHeader K guide preview panel (2026-11-10 individual tokens, values as before): its border defaulted to the + property window border, its face was a literal. Text and outline stay the header tokens. */ + --hm-sw-header-k-guide-border-color: var(--hm-sw-property-border); + --hm-sw-header-k-guide-background: #0e3c2e; /* 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; @@ -1696,16 +1705,19 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l .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; } .hm-sw-property__value > .hm-sw-header__input { width: auto; max-width: 100%; min-width: 0; flex: 1; text-shadow: none; letter-spacing: .02em; font-weight: 500; } -.hm-sw-property__value > .hm-sw-header__input:focus { color: var(--hm-sw-header-input-color); } -.hm-sw-property__value > select:not(.hm-sw-select) { flex: 1; min-width: 0; width: 0; height: 28px; padding: 0; appearance: none; border: 1px solid transparent; border-radius: 2px; background: transparent; color: var(--hm-sw-header-input-color); font: inherit; } -.hm-sw-property__value > select:not(.hm-sw-select):disabled { opacity: 1; appearance: none; color: var(--hm-sw-header-input-color); } +/* Popup value color (2026-11-10, --hm-sw-popup-value-color): the inputs of the value, gauge value and color value cells + (before: the shared .hm-sw-header__input color); read-only values keep the label color (rule below, higher specificity). */ +:is(.hm-sw-property__value, .hm-sw-property__slider-value, .hm-sw-property__color-value) > .hm-sw-header__input, +.hm-sw-property__value > .hm-sw-header__input:focus { color: var(--hm-sw-popup-value-color); caret-color: var(--hm-sw-popup-value-color); } +.hm-sw-property__value > select:not(.hm-sw-select) { flex: 1; min-width: 0; width: 0; height: 28px; padding: 0; appearance: none; border: 1px solid transparent; border-radius: 2px; background: transparent; color: var(--hm-sw-popup-value-color); font: inherit; } +.hm-sw-property__value > select:not(.hm-sw-select):disabled { opacity: 1; appearance: none; color: var(--hm-sw-popup-value-color); } .hm-sw-property__value > select:not(.hm-sw-select):focus { background: var(--hm-sw-property-content); border-color: var(--hm-sw-property-inner-border); } /* SWSelect: native select, panel field look; focus = the SW popup focus ring. */ .hm-sw-select { flex: 0 0 auto; height: 28px; padding: 0 8px; border: 1px solid var(--hm-sw-select-border-color); border-radius: 4px; background: var(--hm-sw-select-background); color: var(--hm-sw-select-text); font: inherit; font-size: 12px; } .hm-sw-select:disabled { opacity: 0.4; cursor: default; } /* 2026-10-21: in an SW popup the chosen value of an enabled select is editable, so it is red like every editable value; a disabled select and the option list keep --hm-sw-select-text. The viewer panels keep their own text color (sw-viewer.css). */ -.hm-sw-property .hm-sw-select:not(:disabled) { color: var(--hm-sw-header-input-color); } +.hm-sw-property .hm-sw-select:not(:disabled) { color: var(--hm-sw-popup-value-color); } .hm-sw-property .hm-sw-select option { color: var(--hm-sw-select-text); } .hm-sw-property__unit { flex: 0 0 auto; font-size: 12px; white-space: nowrap; } /* Color preview (26:12175): 70 x 16 black tile (1px padding, radius 2), 68 x 14 swatch with a white 1px border. */ @@ -1915,7 +1927,7 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l align-self: stretch; min-width: 0; padding: 0 7px; - color: var(--hm-sw-header-input-color); + color: var(--hm-sw-popup-value-color); text-align: left; letter-spacing: 0.02em; } @@ -1948,7 +1960,7 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l /* K guide 26:11698 / 26:11669: original image, fixed mask viewport and continuous pan/zoom. */ .hm-sw-k-guide { position: relative; flex: 0 0 40px; height: 38px; } -.hm-sw-k-guide__panel { z-index: 45; height: 149px; min-height: 149px; overflow: hidden; border: 1px solid var(--hm-sw-property-border); border-radius: 2px; background: #0e3c2e; animation: hm-sw-k-guide-enter 511ms ease-out; } +.hm-sw-k-guide__panel { z-index: 45; height: 149px; min-height: 149px; overflow: hidden; border: 1px solid var(--hm-sw-header-k-guide-border-color); border-radius: 2px; background: var(--hm-sw-header-k-guide-background); animation: hm-sw-k-guide-enter 511ms ease-out; } .hm-sw-k-guide__viewport { position: absolute; top: 6px; left: 6px; width: 192px; height: 109px; mask-size: 192px 109px; mask-repeat: no-repeat; outline: 1px solid #000; } .hm-sw-k-guide__viewport > img { display: block; max-width: none; width: 456px; height: 256.5px; transform-origin: 0 0; transform: translate(0, 0) scale(1); transition-property: transform; transition-timing-function: ease-in-out; } .hm-sw-k-guide__panel[data-frame="2"] .hm-sw-k-guide__viewport > img { transform: translate(0, -40.738px) scale(1); } diff --git a/packages/sw/tests/theme-inheritance.browser.mjs b/packages/sw/tests/theme-inheritance.browser.mjs index 8ac16e0..01fdbe1 100644 --- a/packages/sw/tests/theme-inheritance.browser.mjs +++ b/packages/sw/tests/theme-inheritance.browser.mjs @@ -274,6 +274,118 @@ async function dropdownColors(page, def, themed) { check('Viewer Header 토큰은 SWHeader·Step Header 화면에 영향 없음', untouched.every(item => item.includes(' same ')), untouched.join(', ')) } +// ---------- 1d. Component tokens (2026-11-10): viewer header menus, K guide, popup value, footer command scrollbar ---------- +// Each defaults to the token / literal it replaced (same value), follows a shared token set on the outermost SW scope, +// and when set itself changes its component alone. +{ + const setOn = (page, root, values) => page.locator(root).first().evaluate((element, values) => { + let outer = element + for (let e = element; e; e = e.parentElement) if (e.matches?.('.hm-sw-header, .hm-sw-theme')) outer = e + Object.entries(values).forEach(([token, value]) => value === null ? outer.style.removeProperty(token) : outer.style.setProperty(token, value)) + }, values) + const C1 = 'rgb(44, 55, 66)', C2 = 'rgb(1, 2, 3)' + const flow = async (label, page, root, read, shared, own, expect) => { + const base = await read() + await setOn(page, root, Object.fromEntries(shared.map(token => [token, C1]))) + const inherited = await read() + await setOn(page, root, Object.fromEntries(own.map(token => [token, C2]))) + const mine = await read() + await setOn(page, root, Object.fromEntries([...shared, ...own].map(token => [token, null]))) + const back = await read() + check(`${label}: 기본값 = 기존 값`, JSON.stringify(base) === JSON.stringify(expect), JSON.stringify(base)) + check(`${label}: 바깥 범위의 공유 토큰을 따름`, Object.values(inherited).every(value => value.includes(C1)), JSON.stringify(inherited)) + check(`${label}: 전용 토큰 지정이 우선, 지우면 복귀`, Object.values(mine).every(value => value.includes(C2)) && JSON.stringify(back) === JSON.stringify(base), JSON.stringify(mine)) + } + // Viewer Header menus: a header dropdown (도면) and the logo menu (no Story renders `logoMenu`: the same class + // structure is appended to the header, so the real rules apply). + const viewer = await newPage() + await open(viewer, 'sw-viewer--dropdowns') + await viewer.locator('.hm-sw-viewer-header').first().evaluate(header => { + const wrap = document.createElement('div'); wrap.className = 'hm-sw-viewer-header__logo-menu hm-sw-header__more' + wrap.innerHTML = '
' + header.append(wrap) + }) + await viewer.getByRole('button', { name: '도면' }).first().click() + const menu = viewer.locator('.hm-sw-viewer-header .hm-sw-viewer-menu[data-surface]').first() + await menu.locator('.hm-sw-viewer-menu__item:not([aria-disabled])').first().hover() + const readMenu = () => viewer.evaluate(() => { + const m = getComputedStyle(document.querySelector('.hm-sw-viewer-header .hm-sw-viewer-menu[data-surface]')) + const hovered = [...document.querySelectorAll('.hm-sw-viewer-header .hm-sw-viewer-menu .hm-sw-viewer-menu__item')].find(item => item.matches(':hover, :focus')) + const logo = getComputedStyle(document.querySelector('[data-probe]')) + return { face: `${m.backgroundColor} | ${m.borderTopColor} | ${m.boxShadow}`, hover: hovered ? getComputedStyle(hovered).backgroundColor : 'none', logo: `${logo.backgroundColor} | ${logo.borderTopColor} | ${logo.boxShadow}` } + }) + const header = await newPage() + await open(header, 'sw-header--overview') + // Expected defaults: the SWHeader More menu face (same tokens) — computed on a probe More menu inside SWHeader. + const moreFace = await header.locator('.hm-sw-header').first().evaluate(root => { + const wrap = document.createElement('div'); wrap.className = 'hm-sw-header__more' + wrap.innerHTML = '
'; root.append(wrap) + const s = getComputedStyle(wrap.firstChild); const value = `${s.backgroundColor} | ${s.borderTopColor} | ${s.boxShadow}`; wrap.remove(); return value + }) + const hoverDefault = 'rgba(255, 255, 255, 0.08)' + await flow('Viewer Header 메뉴', viewer, '.hm-sw-viewer-header', readMenu, + ['--hm-sw-header-more-menu-background', '--hm-sw-header-more-menu-border-color', '--hm-sw-header-more-menu-shadow', '--hm-sw-header-more-item-hover-background'], + ['--hm-sw-viewer-header-menu-background', '--hm-sw-viewer-header-menu-border-color', '--hm-sw-viewer-header-menu-shadow', '--hm-sw-viewer-header-menu-item-hover-background'], + { face: moreFace, hover: hoverDefault, logo: moreFace }) + // The shadow token is a full shadow: the C1 / C2 color strings above are not valid shadows, so the shadow keeps its + // value there; check it once with a real shadow. + await setOn(viewer, '.hm-sw-viewer-header', { '--hm-sw-viewer-header-menu-shadow': `0 0 0 3px ${C2}` }) + const shadow = await readMenu() + await setOn(viewer, '.hm-sw-viewer-header', { '--hm-sw-viewer-header-menu-shadow': null }) + check('Viewer Header 메뉴: 그림자 전용 토큰', shadow.face.endsWith(`${C2} 0px 0px 0px 3px`) && shadow.logo.endsWith(`${C2} 0px 0px 0px 3px`), JSON.stringify(shadow)) + await viewer.close() + // K guide panel (hover opens it). + const guide = await newPage() + await open(guide, 'sw-header--k-guide') + await guide.locator('.hm-sw-k-guide button').first().hover() + await guide.locator('.hm-sw-k-guide__panel').first().waitFor() + const readGuide = () => guide.evaluate(() => { const s = getComputedStyle(document.querySelector('.hm-sw-k-guide__panel')); return { border: s.borderTopColor, face: s.backgroundColor } }) + // Shared token: the border follows --hm-sw-property-border; the face was a literal (own token only). + const guideBase = await readGuide() + await setOn(guide, '.hm-sw-header', { '--hm-sw-property-border': C1 }) + const guideShared = await readGuide() + await setOn(guide, '.hm-sw-header', { '--hm-sw-header-k-guide-border-color': C2, '--hm-sw-header-k-guide-background': C2 }) + const guideOwn = await readGuide() + await setOn(guide, '.hm-sw-header', { '--hm-sw-property-border': null, '--hm-sw-header-k-guide-border-color': null, '--hm-sw-header-k-guide-background': null }) + check('K 가이드: 기본값 = 기존 값(#A67D03 / #0E3C2E)', guideBase.border === 'rgb(166, 125, 3)' && guideBase.face === 'rgb(14, 60, 46)', JSON.stringify(guideBase)) + check('K 가이드: 바깥 범위의 --hm-sw-property-border를 따름', guideShared.border === C1 && guideShared.face === guideBase.face, JSON.stringify(guideShared)) + check('K 가이드: 전용 토큰 지정이 우선, 지우면 복귀', guideOwn.border === C2 && guideOwn.face === C2 && JSON.stringify(await readGuide()) === JSON.stringify(guideBase), JSON.stringify(guideOwn)) + await guide.close() + // Popup value color: property window inputs (value, gauge value), dropdown triggers, and the SWHeader 속성Bar untouched. + const popup = await newPage() + await open(popup, 'sw-header--property-panel-compare') + const readValue = () => popup.evaluate(() => { + const color = selector => [...new Set([...document.querySelectorAll(selector)].map(e => getComputedStyle(e).color))].join(',') + return { + input: color('.hm-sw-property__value:not([data-readonly]) > .hm-sw-header__input:not(:read-only)'), + gauge: color('.hm-sw-property__slider-value:not([data-readonly]) > .hm-sw-header__input:not(:read-only)'), + trigger: color('.hm-sw-property__value:not([data-readonly]) .hm-sw-property__trigger'), + } + }) + await flow('Popup 값 글자색', popup, '.hm-sw-property', readValue, ['--hm-sw-header-input-color'], ['--hm-sw-popup-value-color'], + { input: 'rgb(230, 4, 3)', gauge: 'rgb(230, 4, 3)', trigger: 'rgb(230, 4, 3)' }) + await popup.close() + const bar = await newPage() + await open(bar, 'sw-header--overview') + const barColor = () => bar.evaluate(() => [...document.querySelectorAll('.hm-sw-property-bar .hm-sw-header__input, .hm-sw-property-bar__value')].map(e => getComputedStyle(e).color).join(',')) + const barBefore = await barColor() + await setOn(bar, '.hm-sw-header', { '--hm-sw-popup-value-color': C2 }) + check('Popup 값 토큰은 SWHeader 속성Bar 값에 영향 없음', barBefore.length > 0 && barBefore === await barColor(), barBefore) + await bar.close() + // Footer command history scrollbar. + const footer = await newPage() + await open(footer, 'sw-footer--full-footer') + const readBar = () => footer.evaluate(() => ({ scrollbar: getComputedStyle(document.querySelector('.hm-sw-footer-command__history')).scrollbarColor })) + const footerBase = await readBar() + await setOn(footer, '.hm-sw-footer-command', { '--hm-sw-footer-command-scrollbar-thumb': C2, '--hm-sw-footer-command-scrollbar-track': C1 }) + const footerOwn = await readBar() + await setOn(footer, '.hm-sw-footer-command', { '--hm-sw-footer-command-scrollbar-thumb': null, '--hm-sw-footer-command-scrollbar-track': null }) + check('Footer 명령창 스크롤바: 기본값 = 기존 값(#574B30 / transparent)', footerBase.scrollbar === 'rgb(87, 75, 48) rgba(0, 0, 0, 0)', JSON.stringify(footerBase)) + check('Footer 명령창 스크롤바: 전용 토큰 지정이 우선, 지우면 복귀', footerOwn.scrollbar === `${C2} ${C1}` && JSON.stringify(await readBar()) === JSON.stringify(footerBase), JSON.stringify(footerOwn)) + await footer.close() + 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) *)'