feat(sw): add step header process tokens and align docs

This commit is contained in:
shkim4 committed 2026-11-10 17:25:00 +09:00
1 parent 2690a160e9
commit 8a09386b11
8 files changed
+112 -36

No files matched your search

+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-popup-*`, `--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-popup-*`, `--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. 판단 기록 남기기
+8
View File
@@ -193,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 일반 모드 전용
@@ -226,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 토큰 |
+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); }
@@ -196,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)
@@ -212,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] {