feat(sw): add independent viewer header face tokens

This commit is contained in:
shkim4 committed 2026-11-10 17:51:47 +09:00
1 parent 65fa928694
commit f61d181bb1
5 files changed
+79 -16

No files matched your search

+1
View File
@@ -137,6 +137,7 @@ SW 토큰(`--hm-sw-header-*`, `--hm-sw-property-*`, `--hm-sw-popup-*`, `--hm-sw-
5. **하지 않음:** `hm-sw-*` class를 선택자로 쓴 CSS(`.hm-sw-header, .hm-sw-theme { --… }` 등), `!important`, 안쪽 컴포넌트마다 같은 값을 반복 지정, SW 범위가 아닌 조상(`body`, 앱 루트 `div`)에만 토큰 지정(SW 범위가 기본값을 다시 선언하므로 적용되지 않음). 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)`)은 가장 바깥 범위에서 계산됩니다. 참조되는 토큰을 바꿀 때도 가장 바깥 범위에서 바꿉니다. - 다른 토큰을 참조하는 토큰(예: `--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. - 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.
## 4. 판단 기록 남기기 ## 4. 판단 기록 남기기
+1
View File
@@ -160,6 +160,7 @@
- **Model / Layout 탭 토큰:** 위 `--hm-sw-viewer-tabs-*`·`--hm-sw-viewer-tab-*`은 탭 컨테이너·버튼에만 연결되며 다른 Header 부품·SWHeader·Footer·패널에 쓰이지 않습니다. 탭 색만 바꾸려면 가장 바깥 SW 범위의 `style`로 덮습니다: 화면을 `<SWTheme style={...}>`로 감싸거나 단독 `SWViewerHeader`의 루트에 지정합니다(토큰이 그 범위에 선언되므로 일반 `div` 래퍼의 값은 가려짐). 예: `style={{ "--hm-sw-viewer-tab-active-border": "linear-gradient(#e07b00, #e07b00)", "--hm-sw-viewer-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로 색을 덮지 않습니다. - **Model / Layout 탭 토큰:** 위 `--hm-sw-viewer-tabs-*`·`--hm-sw-viewer-tab-*`은 탭 컨테이너·버튼에만 연결되며 다른 Header 부품·SWHeader·Footer·패널에 쓰이지 않습니다. 탭 색만 바꾸려면 가장 바깥 SW 범위의 `style`로 덮습니다: 화면을 `<SWTheme style={...}>`로 감싸거나 단독 `SWViewerHeader`의 루트에 지정합니다(토큰이 그 범위에 선언되므로 일반 `div` 래퍼의 값은 가려짐). 예: `style={{ "--hm-sw-viewer-tab-active-border": "linear-gradient(#e07b00, #e07b00)", "--hm-sw-viewer-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`입니다. - **공유 토큰(모드 혼합 아님):** 뷰어 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 범위(`<SWTheme style>` 또는 단독 `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-*`를 읽습니다.
| 상태 | Header 항목(button·toggle·menu·tabs·switch) | 패널 콘텐츠 | | 상태 | Header 항목(button·toggle·menu·tabs·switch) | 패널 콘텐츠 |
|---|---|---| |---|---|---|
+1
View File
@@ -48,6 +48,7 @@
- **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`의 `...` 메뉴와 같은 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 외곽선. - **상태 표현:** 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`로 그리면 상태 색과 외곽선이 자동 적용됩니다. - **글자·아이콘 외곽선(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`로 그리면 상태 색과 외곽선이 자동 적용됩니다.
- **면 색(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 범위(`<SWTheme style>` 또는 단독 `SWViewerHeader`의 `style`)에 지정합니다. 안쪽 요소에서 바꾼 `--hm-sw-header-*`는 이 값에 전달되지 않습니다. 목록은 [DESIGN_RULES.md](./DESIGN_RULES.md) §11.
### 2.1 두 가지 구성 (2026-10-21) ### 2.1 두 가지 구성 (2026-10-21)
+32 -16
View File
@@ -111,6 +111,22 @@
/* Title-bar icon buttons of viewer panels (collapse, `ghost` header actions; 2026-10-23). Popup-only: the header /* Title-bar icon buttons of viewer panels (collapse, `ghost` header actions; 2026-10-23). Popup-only: the header
status text and switch captions keep --hm-sw-viewer-muted. SWPopup's own title icons are images. */ status text and switch captions keep --hm-sw-viewer-muted. SWPopup's own title icons are images. */
--hm-sw-viewer-panel-icon-color: #c7bda9; --hm-sw-viewer-panel-icon-color: #c7bda9;
/* Viewer Header faces (2026-11-10): the file name, object and property surfaces, the trailing status / button strip and
its start piece read these copies of the SWHeader tokens. Each defaults to the SWHeader token, so the shared look
stays (a --hm-sw-header-* theme on the outermost SW scope still reaches the viewer); set --hm-sw-viewer-* there to
change the Viewer 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. Reused SWHeader parts keep reading --hm-sw-header-* in sw.css. */
--hm-sw-viewer-document-background: var(--hm-sw-header-document-background);
--hm-sw-viewer-document-border-gradient: var(--hm-sw-header-document-border-gradient);
--hm-sw-viewer-document-border-color: var(--hm-sw-header-document-border-color);
--hm-sw-viewer-object-background: var(--hm-sw-header-object-background);
--hm-sw-viewer-object-border-gradient: var(--hm-sw-header-object-border-gradient);
--hm-sw-viewer-object-radius: var(--hm-sw-header-object-radius);
--hm-sw-viewer-property-background: var(--hm-sw-header-property-background);
--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);
} }
.hm-sw-viewer-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } .hm-sw-viewer-sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@@ -179,7 +195,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
/* SWHeader file-name surface: the Figma gradient stroke through the transparent top / right / bottom 1px. */ /* SWHeader file-name surface: the Figma gradient stroke through the transparent top / right / bottom 1px. */
border: 1px solid transparent; border: 1px solid transparent;
border-left: 0; border-left: 0;
background: var(--hm-sw-header-document-background) padding-box, var(--hm-sw-header-document-border-gradient) border-box; background: var(--hm-sw-viewer-document-background) padding-box, var(--hm-sw-viewer-document-border-gradient) border-box;
} }
.hm-sw-viewer-header__document-name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .hm-sw-viewer-header__document-name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
@@ -339,8 +355,8 @@ button.hm-sw-viewer-header__brand { text-align: left; }
padding-inline: var(--hm-sw-viewer-gap); padding-inline: var(--hm-sw-viewer-gap);
/* SWHeader object surface: the Figma gradient stroke through the transparent 1px border. */ /* SWHeader object surface: the Figma gradient stroke through the transparent 1px border. */
border: 1px solid transparent; border: 1px solid transparent;
border-radius: var(--hm-sw-header-object-radius); border-radius: var(--hm-sw-viewer-object-radius);
background: var(--hm-sw-header-object-background) padding-box, var(--hm-sw-header-object-border-gradient) border-box; background: var(--hm-sw-viewer-object-background) padding-box, var(--hm-sw-viewer-object-border-gradient) border-box;
} }
.hm-sw-viewer-header__group { .hm-sw-viewer-header__group {
@@ -351,10 +367,10 @@ button.hm-sw-viewer-header__brand { text-align: left; }
padding-inline: var(--hm-sw-viewer-gap); padding-inline: var(--hm-sw-viewer-gap);
} }
.hm-sw-viewer-header__group[data-surface="field"] { .hm-sw-viewer-header__group[data-surface="field"] {
border: 1px solid var(--hm-sw-header-property-border-color); border: 1px solid var(--hm-sw-viewer-property-border-color);
border-radius: var(--hm-sw-header-property-radius); border-radius: var(--hm-sw-viewer-property-radius);
background: var(--hm-sw-header-property-background); background: var(--hm-sw-viewer-property-background);
box-shadow: var(--hm-sw-header-property-shadow); box-shadow: var(--hm-sw-viewer-property-shadow);
} }
.hm-sw-viewer-header__actions { .hm-sw-viewer-header__actions {
display: flex; display: flex;
@@ -589,10 +605,10 @@ button.hm-sw-viewer-header__brand { text-align: left; }
align-self: stretch; align-self: stretch;
height: auto; height: auto;
border-radius: 0; border-radius: 0;
background: var(--hm-sw-header-document-background); background: var(--hm-sw-viewer-document-background);
/* Only the 1px line before a segment: top, bottom and right are the object surface border itself, so every segment /* Only the 1px line before a segment: top, bottom and right are the object surface border itself, so every segment
(and the license button between them) has the same top and bottom edge (2026-10-21; no doubled lines). */ (and the license button between them) has the same top and bottom edge (2026-10-21; no doubled lines). */
box-shadow: inset 1px 0 0 var(--hm-sw-header-document-border-color); box-shadow: inset 1px 0 0 var(--hm-sw-viewer-document-border-color);
position: relative; position: relative;
} }
/* Strip outline (2026-10-23): the file-name gradient stroke (--hm-sw-header-document-border-gradient) over the object /* Strip outline (2026-10-23): the file-name gradient stroke (--hm-sw-header-document-border-gradient) over the object
@@ -606,7 +622,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
inset: 0; inset: 0;
border: 0 solid transparent; border: 0 solid transparent;
border-block-width: 1px; border-block-width: 1px;
border-image: var(--hm-sw-header-document-border-gradient) 1 / 1 / 1 0; border-image: var(--hm-sw-viewer-document-border-gradient) 1 / 1 / 1 0;
pointer-events: none; pointer-events: none;
} }
.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):last-child::after { .hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):last-child::after {
@@ -632,7 +648,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
/* 2026-10-23: the first segment's left side is the strip's outer gradient line, painted inside it (a 1px layer in place /* 2026-10-23: the first segment's left side is the strip's outer gradient line, painted inside it (a 1px layer in place
of the flat divider, so its width is unchanged). */ of the flat divider, so its width is unchanged). */
.hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *) { .hm-sw-viewer-header .hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons):not(:is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons) + *) {
background: var(--hm-sw-header-document-border-gradient) left top / 1px 100% no-repeat border-box, var(--hm-sw-header-document-background); background: var(--hm-sw-viewer-document-border-gradient) left top / 1px 100% no-repeat border-box, var(--hm-sw-viewer-document-background);
box-shadow: none; box-shadow: none;
} }
/* The empty strip-start mark (SWViewerHeader, right before `trailing`) carries the surface end, so the first segment /* The empty strip-start mark (SWViewerHeader, right before `trailing`) carries the surface end, so the first segment
@@ -646,21 +662,21 @@ button.hm-sw-viewer-header__brand { text-align: left; }
position: absolute; position: absolute;
top: calc(-1 * var(--hm-sw-header-object-border-width, 1px)); top: calc(-1 * var(--hm-sw-header-object-border-width, 1px));
bottom: calc(-1 * var(--hm-sw-header-object-border-width, 1px)); bottom: calc(-1 * var(--hm-sw-header-object-border-width, 1px));
left: calc(-1 * (var(--hm-sw-header-object-radius) + 2px)); left: calc(-1 * (var(--hm-sw-viewer-object-radius) + 2px));
pointer-events: none; pointer-events: none;
} }
.hm-sw-viewer-header__strip-start:has(+ :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons))::before { .hm-sw-viewer-header__strip-start:has(+ :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons))::before {
width: calc(var(--hm-sw-header-object-radius) + 2px); width: calc(var(--hm-sw-viewer-object-radius) + 2px);
background: var(--hm-sw-viewer-background); background: var(--hm-sw-viewer-background);
} }
.hm-sw-viewer-header__strip-start:has(+ :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons))::after { .hm-sw-viewer-header__strip-start:has(+ :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons))::after {
box-sizing: border-box; box-sizing: border-box;
width: var(--hm-sw-header-object-radius); width: var(--hm-sw-viewer-object-radius);
/* The object surface end: its face and gradient stroke, spanning the same 1px-outset height so both line up. */ /* The object surface end: its face and gradient stroke, spanning the same 1px-outset height so both line up. */
border: 1px solid transparent; border: 1px solid transparent;
border-left: 0; border-left: 0;
border-radius: 0 var(--hm-sw-header-object-radius) var(--hm-sw-header-object-radius) 0; border-radius: 0 var(--hm-sw-viewer-object-radius) var(--hm-sw-viewer-object-radius) 0;
background: var(--hm-sw-header-object-background) padding-box, var(--hm-sw-header-object-border-gradient) border-box; background: var(--hm-sw-viewer-object-background) padding-box, var(--hm-sw-viewer-object-border-gradient) border-box;
} }
/* No line around the license / info button: the window buttons right after it drop their line before it. */ /* No line around the license / info button: the window buttons right after it drop their line before it. */
.hm-sw-viewer-header .hm-sw-viewer-header__actions > .hm-sw-viewer-header__button + .hm-sw-header__window-buttons { box-shadow: none; } .hm-sw-viewer-header .hm-sw-viewer-header__actions > .hm-sw-viewer-header__button + .hm-sw-header__window-buttons { box-shadow: none; }
@@ -230,6 +230,50 @@ async function dropdownColors(page, def, themed) {
await header.close() await header.close()
} }
// ---------- 1c. Viewer Header (2026-11-10): --hm-sw-viewer-document-* / -object-* / -property-* face aliases ----------
// Shared by default (an SWHeader theme on the outermost SW scope reaches the viewer faces); a --hm-sw-viewer-* value there
// changes the Viewer Header faces alone and leaves SWHeader and the Step Header as they are.
{
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 page = await newPage()
await open(page, 'sw-viewer--viewer-header')
const read = () => page.evaluate(() => {
// Every match (the Story has more than one header; some file-name areas carry no face of their own).
const paint = selector => { const all = [...document.querySelectorAll(selector)]; if (!all.length) return null; return all.map(e => { const s = getComputedStyle(e); return `${s.backgroundColor} ${s.backgroundImage}` }).join(' || ') }
// The document face token paints the trailing status / button strip segments in this Story.
return { document: paint('.hm-sw-viewer-header__actions > :is(.hm-sw-viewer-status, .hm-sw-viewer-header__button, .hm-sw-header__window-buttons)'), property: paint('.hm-sw-viewer-header__group[data-surface="field"]') }
})
const A = 'linear-gradient(rgb(44, 55, 66), rgb(44, 55, 66))', B = 'linear-gradient(rgb(1, 2, 3), rgb(1, 2, 3))'
const base = await read()
await setOn(page, '.hm-sw-viewer-header', { '--hm-sw-header-document-background': A, '--hm-sw-header-property-background': A })
const inherited = await read()
check('Viewer Header: 바깥 범위의 SWHeader 토큰(파일명·속성 면)이 그대로 전달', base.document !== null && base.property !== null && inherited.document.includes('rgb(44, 55, 66)') && inherited.property.includes('rgb(44, 55, 66)'), JSON.stringify({ base, inherited }))
await setOn(page, '.hm-sw-viewer-header', { '--hm-sw-viewer-document-background': B, '--hm-sw-viewer-property-background': B })
const own = await read()
check('Viewer Header: --hm-sw-viewer-* 지정이 SWHeader 토큰보다 우선', own.document.includes('rgb(1, 2, 3)') && own.property.includes('rgb(1, 2, 3)'), JSON.stringify(own))
await setOn(page, '.hm-sw-viewer-header', { '--hm-sw-header-document-background': null, '--hm-sw-header-property-background': null, '--hm-sw-viewer-document-background': null, '--hm-sw-viewer-property-background': null })
check('Viewer Header: 지정을 지우면 기본값으로 복귀', JSON.stringify(await read()) === JSON.stringify(base), JSON.stringify(base))
await page.close()
// The Viewer Header face tokens on SWHeader and Step Header pages change nothing there.
const names = ['document-background', 'document-border-gradient', 'document-border-color', 'object-background', 'object-border-gradient', 'object-radius', 'property-background', 'property-border-color', 'property-radius', 'property-shadow']
const values = Object.fromEntries(names.map(name => [`--hm-sw-viewer-${name}`, name.endsWith('radius') ? '9px' : name.endsWith('shadow') ? '0 0 0 3px rgb(1, 2, 3)' : name.endsWith('color') ? 'rgb(1, 2, 3)' : B]))
const untouched = []
for (const [id, root] of [['sw-header--overview', '.hm-sw-header'], ['sw-step-header--process-header', '.hm-sw-process-header']]) {
const other = await newPage()
await open(other, id)
const paint = () => other.evaluate(root => [...document.querySelectorAll(`${root}, ${root} *`)].map(element => { const s = getComputedStyle(element); return `${s.color}|${s.backgroundColor}|${s.backgroundImage}|${s.boxShadow}|${s.borderTopColor}|${s.borderTopLeftRadius}` }).join('\n'), root)
const before = await paint()
await setOn(other, root, values)
untouched.push(`${id} ${before === await paint() ? 'same' : 'CHANGED'} ${before.split('\n').length}`)
await other.close()
}
check('Viewer Header 토큰은 SWHeader·Step Header 화면에 영향 없음', untouched.every(item => item.includes(' same ')), untouched.join(', '))
}
// ---------- 2. Regression: new cascade vs legacy cascade on every existing SW Story ---------- // ---------- 2. Regression: new cascade vs legacy cascade on every existing SW Story ----------
const here = path.dirname(fileURLToPath(import.meta.url)) 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 outermost = ':is(.hm-sw-header, .hm-sw-theme):not(:where(.hm-sw-header, .hm-sw-theme) *)'