diff --git a/docs/patterns/lnb.md b/docs/patterns/lnb.md index 54cef51..68d54c7 100644 --- a/docs/patterns/lnb.md +++ b/docs/patterns/lnb.md @@ -90,7 +90,7 @@ Collapsible의 trigger/content는 `asChild`로 자체 `button`/`div`를 써서 C | 연결선 | 3뎁스만, 1px, 세로선 x 68, 가로 12, 글자까지 8, 행 세로 중앙, 마지막 행 중앙에서 끝 | 행마다 background 2겹(세로 `1px × 100%`, 마지막 행 `1px × 50%`, 가로 `12 × 1` at `calc(50% - 0.5px)`). DOM·tab stop·aria 없음 | MATCHED / IMPLEMENTATION_CHANGE(명세의 "배경 레이어" 방식) | | 연결선 색 | 기본·hover `--hm-green-tint` 40%, 선택 `--hm-green-light-2` 40% | 같음. 선택 색은 선택된 행의 선에만 적용(나머지 행은 기본색) | MATCHED | | 접힘 | 하위 메뉴·연결선 함께 숨김 | Radix가 내용을 unmount → 선도 같이 사라짐 | MATCHED | -| 중첩 그룹 | 배경·테두리·padding 중복, 폭 감소, 오른쪽 틈 금지 | 안쪽 그룹은 면·선·padding-bottom 없음, 폭 100%(바깥 그룹과 같은 252), 선택 항목 오른쪽 273(1px 겹침) | MATCHED | +| 중첩 그룹 | 배경·테두리·padding 중복, 폭 감소, 오른쪽 틈 금지 | 안쪽 그룹은 면·선·padding-bottom 없음, 폭 100%(바깥 그룹과 같은 220), 선택 항목 오른쪽 241(1px 겹침). 2026-11-10 240px 열 실측(Third Level, 1440); 이전 252/273은 272px 열 기준 | MATCHED | | 안쪽 그룹 이름 위치 | 명세 수치 없음, 이미지상 2뎁스 글자와 같은 시작선 | trigger padding-left = 68 − 화살표 18 − gap 12 = 38 → 글자 x 68 | INTENTIONAL_CHANGE(이미지 기준) | 모바일(<1024): LNB ONLY는 Desktop 열을 숨기고 같은 메뉴를 독립 `LNBMobileHeader / LNBMobileMenu`로 표시한다(§3b). GNB ONLY 내부 Sidebar는 [GNB §15](gnb.md#15-gnb-only-sidebar-composition-2026-10-08)의 GNB 단일 Sheet를 사용하며 독립 LNB Mobile Header/Menu를 추가하지 않는다. DASHBOARD ONLY 운영 화면에는 Sidebar가 없다([Dashboard 현재 계약](dashboard-layout.md#1-현재-계약)). diff --git a/docs/sw/sw-process-header-design-spec.md b/docs/sw/sw-process-header-design-spec.md index 435c024..1eceb76 100644 --- a/docs/sw/sw-process-header-design-spec.md +++ b/docs/sw/sw-process-header-design-spec.md @@ -90,9 +90,17 @@ Strana의 2그룹(01 Modeling: Structure · Loads / 02 Analysis Result: Results) 전체 목록과 Strana 원본 키는 §7 표의 왼쪽 두 열을 봅니다. +**Step Header 전용 alias (2026-11-10).** Step Header 규칙이 칠하는 SWHeader 토큰 18개는 같은 이름의 `--hm-sw-process-*`(가장 바깥 SW 범위, 기본값 `var(--hm-sw-header-…)`)를 거쳐 읽습니다: `background`, `height`, `text-color`, `selected-color`, `hover-border-gradient`, `hover-border-width`, `document-background`, `document-border-gradient`, `document-border-color`, `object-background`, `object-border-gradient`, `object-radius`, `property-background`, `property-radius`, `property-shadow`, `window-icon-color`, `window-icon-hover-color`, `window-button-hover-background`. `--hm-sw-process-base-border-gradient`는 `--hm-sw-process-document-border-gradient`를 거쳐 같은 기본값을 씁니다. + +- 기본(상속): `--hm-sw-process-*`가 없으면 가장 바깥 범위의 SWHeader 토큰 값이 그대로 적용됩니다. 계산값은 변경 전과 같습니다. +- Step Header만 변경: 가장 바깥 SW 범위에 `--hm-sw-process-*`를 지정합니다(SWHeader 토큰보다 우선, SWHeader·Viewer 영향 없음). +- 제약: alias는 가장 바깥 범위에서 계산되므로 안쪽 요소에서 바꾼 `--hm-sw-header-*`는 이 18개에 전달되지 않습니다. +- 공유 유지: 바 높이·필드·로고 높이, 글자 크기, 글자 외곽선, 재사용 부품(홈·로고, 파일명 버튼, 창 이동, 도구 pill)의 `sw.css` 규칙. +- `--hm-sw-process-text-color`는 Step Header 자체 글자 규칙(글자 로고, 레이어 필드)에만 적용됩니다. 루트의 기본 글자색은 SW 공통 규칙(`sw.css` `.hm-sw-header, .hm-sw-theme`)이 `--hm-sw-header-text-color`로 정하므로, 이를 상속하는 영역과 재사용 부품은 Header 토큰을 따릅니다. + ## 7. Strana 테마 → 토큰 Override 대응표 -Hanmac 열이 기본값입니다. 다른 테마는 Header의 `style`(가장 바깥 SW 범위)에 같은 토큰을 지정합니다. 예: `style={{ "--hm-sw-header-selected-color": "#FFC833" }}`. Strana ARGB `#AARRGGBB`는 CSS `rgba()`로 바꿉니다. 아래 표는 Strana `Styles/Theme/*/Colors.xaml`(HEAD `edda3cc`)에서 생성했습니다. +Hanmac 열이 기본값입니다. 다른 테마는 Header의 `style`(가장 바깥 SW 범위)에 같은 토큰을 지정합니다. 예: `style={{ "--hm-sw-header-selected-color": "#FFC833" }}`. Strana ARGB `#AARRGGBB`는 CSS `rgba()`로 바꿉니다. 아래 표는 Strana `Styles/Theme/*/Colors.xaml`(HEAD `edda3cc`)에서 생성했습니다. 표의 `--hm-sw-header-*` 중 §6의 18개는 Step Header만 바꿀 때 같은 이름의 `--hm-sw-process-*`(예: `--hm-sw-process-selected-color`)에 지정해도 됩니다(2026-11-10). | 토큰 | Strana `Color.Header.*` | Hanmac(기본값) | Dark | Light | HighContrast | diff --git a/packages/sw/ai/AI_USAGE.md b/packages/sw/ai/AI_USAGE.md index ce2d021..9f93bb9 100644 --- a/packages/sw/ai/AI_USAGE.md +++ b/packages/sw/ai/AI_USAGE.md @@ -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 범위**(``, 또는 단독으로 쓴 `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 범위**(``, 또는 단독으로 쓴 `SWHeader`·`SWProcessHeader`·`SWViewerHeader`·`SWViewerPanel`·`SWViewerMenu`·`SWFooter`의 루트)에서 한 번만 선언됩니다. 예외: 홈 메뉴 토큰(`--hm-sw-home-menu-*`, `--hm-sw-recent-files-*`)은 `SWHomeMenu` 루트(`.hm-sw-home-menu`)에 선언되어 ``로는 바뀌지 않습니다. `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가 같은 값)를 바꾸려면 화면을 `` 하나로 감싸고 거기서만 토큰을 정합니다. 프로그램 class(`` + `.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. 영역마다 다른 값이 필요하면 영역별 ``을 둡니다. 옆 영역끼리 섞이지 않고, 안쪽 ``은 그 안에서만 적용됩니다. 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. 판단 기록 남기기 diff --git a/packages/sw/ai/DESIGN_RULES.md b/packages/sw/ai/DESIGN_RULES.md index f97afe3..3a24b5e 100644 --- a/packages/sw/ai/DESIGN_RULES.md +++ b/packages/sw/ai/DESIGN_RULES.md @@ -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`)에 선언됩니다. 루트가 값을 다시 정하므로 ``이나 `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 범위(`` 또는 단독 `SWProcessHeader`의 `style`)에 `--hm-sw-process-*`를 지정합니다. SWHeader·Viewer에는 영향이 없습니다. +- 제약: alias는 가장 바깥 범위에서 계산되므로 안쪽 요소(예: `` 안 `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 토큰 | diff --git a/packages/sw/src/theme/sw-process-header.css b/packages/sw/src/theme/sw-process-header.css index 34a4902..dc77bf7 100644 --- a/packages/sw/src/theme/sw-process-header.css +++ b/packages/sw/src/theme/sw-process-header.css @@ -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); } diff --git a/packages/sw/tests/theme-inheritance.browser.mjs b/packages/sw/tests/theme-inheritance.browser.mjs index cce582d..e0045b7 100644 --- a/packages/sw/tests/theme-inheritance.browser.mjs +++ b/packages/sw/tests/theme-inheritance.browser.mjs @@ -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)}` diff --git a/packages/ui/ai/DESIGN_RULES.md b/packages/ui/ai/DESIGN_RULES.md index 0e5ece6..ef91433 100644 --- a/packages/ui/ai/DESIGN_RULES.md +++ b/packages/ui/ai/DESIGN_RULES.md @@ -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 | diff --git a/packages/ui/src/theme/lnb.css b/packages/ui/src/theme/lnb.css index a2ef265..77c92a7 100644 --- a/packages/ui/src/theme/lnb.css +++ b/packages/ui/src/theme/lnb.css @@ -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] {