fix(sw): refine process header empty title states and UI guidance

- AI_GUIDE stage 5 "Header 전용 스타일의 적용 범위": header-only styles stay
  in the header (SW SWHeader / SWProcessHeader / SWViewerHeader, Web GNB /
  LNBMobileHeader / DashboardHeader); no header CSS or tokens on body UI,
  no global overrides, layout-only joins, package-shared tokens unchanged,
  whole-screen use only on explicit request. Synced to the 4 package
  copies; referenced from stage 6, SW and Web AI_USAGE §3
- SWProcessHeader: with documentTitle "" the file name keeps #AEDBCC on
  hover and a default cursor (root data-document-untitled). The button,
  focus and onDocumentClick are kept; a real file name keeps the yellow
  hover and pointer. Visual vs. click difference recorded as a UX item
- Step enablement documented as implemented: only an explicit disabled
  (step, group, process, header) turns steps off; project / file state
  never does; selection is activeStepId + onStepSelect (also for host
  previous / next); no activeStepId and empty groups described
- Regression check tests/process-header-states.check.mjs (15/15);
  typecheck:sw, build:sw, check:ai-docs pass; public-api keeps its 3
  existing failures, none new
This commit is contained in:
shkim4 committed 2026-11-05 16:45:54 +09:00
1 parent 6c61edadc1
commit 0086d66a94
14 files changed
+177 -9

No files matched your search

+7
View File
@@ -1,5 +1,12 @@
# Working Context
## 2026-11-05 — Header 스타일 적용 범위·빈 파일명 Hover·Step 활성화 규칙
- 단일 기준: `docs/ai/AI_GUIDE.md` 5단계 "Header 전용 스타일의 적용 범위"(4개 패키지 사본 sync, sw 사본 LF 유지). 참조만: AI_GUIDE 6단계 5번, `packages/sw/ai/AI_USAGE.md` §3, `packages/ui/ai/AI_USAGE.md` §3.
- `SWProcessHeader`: `documentTitle=""`이면 루트 `data-document-untitled`, 파일명 면 Hover 색 변화 없음·`cursor: default`(`sw-process-header.css` 빈 상태 블록). 버튼·`onDocumentClick`·크기·위치·색은 유지 — 테스트 앱 백업(`D:\신규 헤더 테스트_backup_*`)과 Storybook Live Multi Step이 빈 상태 클릭을 로그로 사용 중이라 클릭은 제거하지 않음.
- Step 활성화: 코드 변경 없음. `disabled: true`(단계·그룹·process·Header)만 비활성, 파일명·프로젝트 상태 무관을 `COMPONENT_CATALOG.md` §11 "단계 활성화"에 명시, `COMPOSITION_PATTERNS.md` §12-6 문구 수정. 회귀 검사 `packages/sw/tests/process-header-states.check.mjs`.
- 보류: 도구 아이콘 5개 Export(출처·권한 미확인, Figma에 Iconify `mage:` 흔적), `public-api.approved.json`, Step 선택 애니메이션, 테스트 프로젝트 생성. 미Commit.
## 2026-11-05 — 시안 기반 Header 도구 아이콘·창 제어 규칙 (B안, 문서만)
- 단일 기준: `packages/sw/ai/AI_USAGE.md` §5(기본 구성 vs 시안 재현, 아이콘 1~3순위, Storybook 전용 vs 공개, 상태 처리, 창 제어 `hiddenControls`, 예시, 보고).
+14 -1
View File
@@ -178,6 +178,19 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
3. 패키지가 실제로 공유하는 부분은 혼합이 아닙니다: `SWViewerPanel`은 `SWPopup` 프레임을 그대로 렌더링하고, 두 Header는 파일명·객체 수정·속성 면 토큰(`--hm-sw-header-document-*`·`object-*`·`property-*`)을 공유합니다. 소비 코드가 이 공유를 흉내 내 다른 모드 부품을 섞어도 된다는 뜻은 아닙니다.
4. 프로그램이 두 모드를 모두 가지면 모드 전환 시 Header 컴포넌트를 바꿔 끼웁니다(파일·패널 상태는 프로그램 상태로 유지). 세부는 `@hm-design/sw/ai/AI_USAGE.md` §0과 `VIEWER.md`.
### Header 전용 스타일의 적용 범위 (2026-11-05)
화면 상단 Header 컴포넌트 — SW `SWHeader`·`SWProcessHeader`·`SWViewerHeader`, Web `GNB`·`LNBMobileHeader`·`DashboardHeader` — 의 전용 스타일은 **그 Header 영역 안에서만** 씁니다. 전용 스타일은 Header의 class(`hm-sw-header__*`·`hm-sw-process-*`·`hm-sw-viewer-header__*`, Web Header의 `data-slot`)와 Header 모양을 정하는 값(배경·gradient·테두리·글자 외곽선·높이·간격)입니다. `DialogHeader`·`CardHeader`처럼 다른 컴포넌트 안의 머리 부품은 여기서 말하는 Header가 아닙니다. 이 규칙은 아래를 정할 뿐 기존 Header의 레이아웃·스타일·공개 API를 바꾸지 않습니다.
1. Header 밖의 본문·패널·카드·버튼·입력 필드와 다른 컴포넌트에 Header 전용 class·CSS·값을 붙이거나 복사하지 않습니다. "Header 스타일로" 같은 요청도 Header 영역에만 적용하고, 별도 요청이 없으면 Header 밖 기존 UI의 디자인과 동작은 그대로 둡니다.
2. 화면 전체 토큰·테마(Web Runtime Theme `--hm-sem-*`, SW 가장 바깥 범위의 `style` 토큰, 위 표의 범위)와 Header 전용 스타일을 구분합니다. Header를 맞추려고 화면 전체 테마 값을 바꾸지 않습니다.
3. Header를 구현하려고 전역 CSS(`html`·`body`·`*`, 공통 reset)를 바꾸거나 기존 본문 스타일을 덮어쓰지 않습니다.
4. Header와 본문의 경계에서는 배치에 필요한 연결만 소비 프로젝트 class로 처리합니다(Header 높이만큼 본문 시작 위치, sticky·z-index, 남는 높이를 본문에 주는 flex 등).
5. 공식 컴포넌트 내부 스타일을 소비 프로젝트에서 Override하지 않습니다(위 혼합 규칙 3). 바꿀 수 있는 것은 각 패키지가 공개한 Props·토큰뿐입니다.
6. 시안에 본문 디자인이 따로 있으면 그 영역의 패키지(위 혼합 규칙 1)의 공식 컴포넌트로 구현합니다. Header와 같은 패키지의 부품을 Header 밖에 둘 때도 그 부품의 공식 사용법(SW는 `<SWTheme>` 안)을 따르고 Header 모양을 덧입히지 않습니다.
7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`<SWTheme>` 부품이 `--hm-sw-header-*` 토큰을 읽음, 위 SW 분리 3). 소비 코드가 이를 흉내 내 Header 스타일을 다른 곳에 쓰면 안 됩니다.
8. 사용자가 화면 전체에 Header 디자인을 적용하라고 **명시한 경우에만** 범위를 넓힙니다. 이때도 공식 컴포넌트·공개 토큰으로만 맞추고 Header class를 복사하지 않으며, 결과 보고에 넓힌 범위를 적습니다.
## 6단계. 디자인 검증
소비 프로젝트에서 실제로 실행한 것만 PASS로 적습니다.
@@ -186,7 +199,7 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
2. 브라우저 렌더링: 재사용한 컴포넌트가 HM 외형으로 보이는지(CSS 진입점 누락 확인), 신규 UI가 같은 토큰을 쓰는지
3. 상태·동작: Hover / Focus(키보드) / Disabled, Popup 열기·닫기·Escape·Focus 복귀, 선택 패키지의 핵심 동작
4. 폭: Web 390 / 768 / 1280 / 1440px, SW 일반 모드는 대상 창 폭(Header는 반응형 More 포함), SW 뷰어 모드는 대상 창 폭을 줄였다 늘리며 실제 공간 부족에 따른 단계(`iconOnlyBelow` 항목 아이콘만 → `collapseBelow` 항목 More 이동, 숫자는 px 경계가 아니라 순서) 전후, OrgChart는 캔버스 크기·초기 Fit
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지(§5)
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지, Header 전용 스타일이 Header 밖에 쓰이지 않았는지(§5, "Header 전용 스타일의 적용 범위")
6. 실행하지 못한 항목은 `NOT VERIFIED`로 남깁니다.
## 7단계. 결과 보고 형식
+1
View File
@@ -340,6 +340,7 @@ Strana 비교 캡처는 `D:\Strana\_verification\header-rework-20261029\renders\
| 넘침 처리 | 그룹 접기·좌우 스크롤·More 없음(2026-11-03 확정) | 단계가 많으면 축소 외 수단이 없음 | 확정 정책 — 변경하려면 사용자 결정 필요 |
| 도구 버튼 툴팁 | `SWProcessSplitButton`은 `label`을 `title`로 전달. 호스트 `tools`의 `SWToolButton pill`은 2026-11-05부터 공백이 아닌 문자열 `children`이 기본 `title`(명시 `title` 우선, `title=""` 끔, 숫자·JSX 제외). 이 Header의 Story 도구는 이미 `title`을 전달해 DOM 변화 없음 | 아이콘만 보여 의미를 알기 어려울 수 있음(키보드 Focus·터치에서는 `title`이 보이지 않음 — 미해결) | 공통 원칙(DESIGN_RULES §15.2) — 자동 툴팁 구현됨 |
| 텍스트 로고 폭 | `main @ 06527ed` 기준: `logoText` 58px 고정 칸이라 더 긴 글자는 양쪽으로 넘쳐 홈 왼쪽에서 잘림. 2026-11-05 변경 기준: 최소 58px · 글자 폭만큼 확장(§3 홈 행, §8 로고 폭 행) | 글꼴에 따라 글자 폭·홈 폭이 달라짐(§3 실측은 Noto Sans KR 기준) | 공통 원칙(DESIGN_RULES §1 · §15.4). 로고 이름·글꼴별 고정 폭 예외를 두지 않음 |
| 파일 생성 전 파일명 면 | 2026-11-05부터 `documentTitle=""`이면 Hover 색 변화 없음(`#AEDBCC` 유지)·`cursor: default`. 버튼·키보드 Focus·`onDocumentClick`은 그대로(기존 사용처가 빈 상태 클릭을 받음) | 클릭할 수 없어 보이지만 실제로는 클릭·Focus가 됨(보이는 모양과 동작이 다름) | 별도 검토 — 클릭·Focus 제거는 사용처 확인과 사용자 결정 필요 |
## 13. Figma 도구 아이콘 공개 Export — 보류 및 향후 제안 (2026-11-05)
+14 -1
View File
@@ -178,6 +178,19 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
3. 패키지가 실제로 공유하는 부분은 혼합이 아닙니다: `SWViewerPanel`은 `SWPopup` 프레임을 그대로 렌더링하고, 두 Header는 파일명·객체 수정·속성 면 토큰(`--hm-sw-header-document-*`·`object-*`·`property-*`)을 공유합니다. 소비 코드가 이 공유를 흉내 내 다른 모드 부품을 섞어도 된다는 뜻은 아닙니다.
4. 프로그램이 두 모드를 모두 가지면 모드 전환 시 Header 컴포넌트를 바꿔 끼웁니다(파일·패널 상태는 프로그램 상태로 유지). 세부는 `@hm-design/sw/ai/AI_USAGE.md` §0과 `VIEWER.md`.
### Header 전용 스타일의 적용 범위 (2026-11-05)
화면 상단 Header 컴포넌트 — SW `SWHeader`·`SWProcessHeader`·`SWViewerHeader`, Web `GNB`·`LNBMobileHeader`·`DashboardHeader` — 의 전용 스타일은 **그 Header 영역 안에서만** 씁니다. 전용 스타일은 Header의 class(`hm-sw-header__*`·`hm-sw-process-*`·`hm-sw-viewer-header__*`, Web Header의 `data-slot`)와 Header 모양을 정하는 값(배경·gradient·테두리·글자 외곽선·높이·간격)입니다. `DialogHeader`·`CardHeader`처럼 다른 컴포넌트 안의 머리 부품은 여기서 말하는 Header가 아닙니다. 이 규칙은 아래를 정할 뿐 기존 Header의 레이아웃·스타일·공개 API를 바꾸지 않습니다.
1. Header 밖의 본문·패널·카드·버튼·입력 필드와 다른 컴포넌트에 Header 전용 class·CSS·값을 붙이거나 복사하지 않습니다. "Header 스타일로" 같은 요청도 Header 영역에만 적용하고, 별도 요청이 없으면 Header 밖 기존 UI의 디자인과 동작은 그대로 둡니다.
2. 화면 전체 토큰·테마(Web Runtime Theme `--hm-sem-*`, SW 가장 바깥 범위의 `style` 토큰, 위 표의 범위)와 Header 전용 스타일을 구분합니다. Header를 맞추려고 화면 전체 테마 값을 바꾸지 않습니다.
3. Header를 구현하려고 전역 CSS(`html`·`body`·`*`, 공통 reset)를 바꾸거나 기존 본문 스타일을 덮어쓰지 않습니다.
4. Header와 본문의 경계에서는 배치에 필요한 연결만 소비 프로젝트 class로 처리합니다(Header 높이만큼 본문 시작 위치, sticky·z-index, 남는 높이를 본문에 주는 flex 등).
5. 공식 컴포넌트 내부 스타일을 소비 프로젝트에서 Override하지 않습니다(위 혼합 규칙 3). 바꿀 수 있는 것은 각 패키지가 공개한 Props·토큰뿐입니다.
6. 시안에 본문 디자인이 따로 있으면 그 영역의 패키지(위 혼합 규칙 1)의 공식 컴포넌트로 구현합니다. Header와 같은 패키지의 부품을 Header 밖에 둘 때도 그 부품의 공식 사용법(SW는 `<SWTheme>` 안)을 따르고 Header 모양을 덧입히지 않습니다.
7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`<SWTheme>` 부품이 `--hm-sw-header-*` 토큰을 읽음, 위 SW 분리 3). 소비 코드가 이를 흉내 내 Header 스타일을 다른 곳에 쓰면 안 됩니다.
8. 사용자가 화면 전체에 Header 디자인을 적용하라고 **명시한 경우에만** 범위를 넓힙니다. 이때도 공식 컴포넌트·공개 토큰으로만 맞추고 Header class를 복사하지 않으며, 결과 보고에 넓힌 범위를 적습니다.
## 6단계. 디자인 검증
소비 프로젝트에서 실제로 실행한 것만 PASS로 적습니다.
@@ -186,7 +199,7 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
2. 브라우저 렌더링: 재사용한 컴포넌트가 HM 외형으로 보이는지(CSS 진입점 누락 확인), 신규 UI가 같은 토큰을 쓰는지
3. 상태·동작: Hover / Focus(키보드) / Disabled, Popup 열기·닫기·Escape·Focus 복귀, 선택 패키지의 핵심 동작
4. 폭: Web 390 / 768 / 1280 / 1440px, SW 일반 모드는 대상 창 폭(Header는 반응형 More 포함), SW 뷰어 모드는 대상 창 폭을 줄였다 늘리며 실제 공간 부족에 따른 단계(`iconOnlyBelow` 항목 아이콘만 → `collapseBelow` 항목 More 이동, 숫자는 px 경계가 아니라 순서) 전후, OrgChart는 캔버스 크기·초기 Fit
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지(§5)
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지, Header 전용 스타일이 Header 밖에 쓰이지 않았는지(§5, "Header 전용 스타일의 적용 범위")
6. 실행하지 못한 항목은 `NOT VERIFIED`로 남깁니다.
## 7단계. 결과 보고 형식
+14 -1
View File
@@ -178,6 +178,19 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
3. 패키지가 실제로 공유하는 부분은 혼합이 아닙니다: `SWViewerPanel`은 `SWPopup` 프레임을 그대로 렌더링하고, 두 Header는 파일명·객체 수정·속성 면 토큰(`--hm-sw-header-document-*`·`object-*`·`property-*`)을 공유합니다. 소비 코드가 이 공유를 흉내 내 다른 모드 부품을 섞어도 된다는 뜻은 아닙니다.
4. 프로그램이 두 모드를 모두 가지면 모드 전환 시 Header 컴포넌트를 바꿔 끼웁니다(파일·패널 상태는 프로그램 상태로 유지). 세부는 `@hm-design/sw/ai/AI_USAGE.md` §0과 `VIEWER.md`.
### Header 전용 스타일의 적용 범위 (2026-11-05)
화면 상단 Header 컴포넌트 — SW `SWHeader`·`SWProcessHeader`·`SWViewerHeader`, Web `GNB`·`LNBMobileHeader`·`DashboardHeader` — 의 전용 스타일은 **그 Header 영역 안에서만** 씁니다. 전용 스타일은 Header의 class(`hm-sw-header__*`·`hm-sw-process-*`·`hm-sw-viewer-header__*`, Web Header의 `data-slot`)와 Header 모양을 정하는 값(배경·gradient·테두리·글자 외곽선·높이·간격)입니다. `DialogHeader`·`CardHeader`처럼 다른 컴포넌트 안의 머리 부품은 여기서 말하는 Header가 아닙니다. 이 규칙은 아래를 정할 뿐 기존 Header의 레이아웃·스타일·공개 API를 바꾸지 않습니다.
1. Header 밖의 본문·패널·카드·버튼·입력 필드와 다른 컴포넌트에 Header 전용 class·CSS·값을 붙이거나 복사하지 않습니다. "Header 스타일로" 같은 요청도 Header 영역에만 적용하고, 별도 요청이 없으면 Header 밖 기존 UI의 디자인과 동작은 그대로 둡니다.
2. 화면 전체 토큰·테마(Web Runtime Theme `--hm-sem-*`, SW 가장 바깥 범위의 `style` 토큰, 위 표의 범위)와 Header 전용 스타일을 구분합니다. Header를 맞추려고 화면 전체 테마 값을 바꾸지 않습니다.
3. Header를 구현하려고 전역 CSS(`html`·`body`·`*`, 공통 reset)를 바꾸거나 기존 본문 스타일을 덮어쓰지 않습니다.
4. Header와 본문의 경계에서는 배치에 필요한 연결만 소비 프로젝트 class로 처리합니다(Header 높이만큼 본문 시작 위치, sticky·z-index, 남는 높이를 본문에 주는 flex 등).
5. 공식 컴포넌트 내부 스타일을 소비 프로젝트에서 Override하지 않습니다(위 혼합 규칙 3). 바꿀 수 있는 것은 각 패키지가 공개한 Props·토큰뿐입니다.
6. 시안에 본문 디자인이 따로 있으면 그 영역의 패키지(위 혼합 규칙 1)의 공식 컴포넌트로 구현합니다. Header와 같은 패키지의 부품을 Header 밖에 둘 때도 그 부품의 공식 사용법(SW는 `<SWTheme>` 안)을 따르고 Header 모양을 덧입히지 않습니다.
7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`<SWTheme>` 부품이 `--hm-sw-header-*` 토큰을 읽음, 위 SW 분리 3). 소비 코드가 이를 흉내 내 Header 스타일을 다른 곳에 쓰면 안 됩니다.
8. 사용자가 화면 전체에 Header 디자인을 적용하라고 **명시한 경우에만** 범위를 넓힙니다. 이때도 공식 컴포넌트·공개 토큰으로만 맞추고 Header class를 복사하지 않으며, 결과 보고에 넓힌 범위를 적습니다.
## 6단계. 디자인 검증
소비 프로젝트에서 실제로 실행한 것만 PASS로 적습니다.
@@ -186,7 +199,7 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
2. 브라우저 렌더링: 재사용한 컴포넌트가 HM 외형으로 보이는지(CSS 진입점 누락 확인), 신규 UI가 같은 토큰을 쓰는지
3. 상태·동작: Hover / Focus(키보드) / Disabled, Popup 열기·닫기·Escape·Focus 복귀, 선택 패키지의 핵심 동작
4. 폭: Web 390 / 768 / 1280 / 1440px, SW 일반 모드는 대상 창 폭(Header는 반응형 More 포함), SW 뷰어 모드는 대상 창 폭을 줄였다 늘리며 실제 공간 부족에 따른 단계(`iconOnlyBelow` 항목 아이콘만 → `collapseBelow` 항목 More 이동, 숫자는 px 경계가 아니라 순서) 전후, OrgChart는 캔버스 크기·초기 Fit
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지(§5)
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지, Header 전용 스타일이 Header 밖에 쓰이지 않았는지(§5, "Header 전용 스타일의 적용 범위")
6. 실행하지 못한 항목은 `NOT VERIFIED`로 남깁니다.
## 7단계. 결과 보고 형식
+14 -1
View File
@@ -178,6 +178,19 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
3. 패키지가 실제로 공유하는 부분은 혼합이 아닙니다: `SWViewerPanel`은 `SWPopup` 프레임을 그대로 렌더링하고, 두 Header는 파일명·객체 수정·속성 면 토큰(`--hm-sw-header-document-*`·`object-*`·`property-*`)을 공유합니다. 소비 코드가 이 공유를 흉내 내 다른 모드 부품을 섞어도 된다는 뜻은 아닙니다.
4. 프로그램이 두 모드를 모두 가지면 모드 전환 시 Header 컴포넌트를 바꿔 끼웁니다(파일·패널 상태는 프로그램 상태로 유지). 세부는 `@hm-design/sw/ai/AI_USAGE.md` §0과 `VIEWER.md`.
### Header 전용 스타일의 적용 범위 (2026-11-05)
화면 상단 Header 컴포넌트 — SW `SWHeader`·`SWProcessHeader`·`SWViewerHeader`, Web `GNB`·`LNBMobileHeader`·`DashboardHeader` — 의 전용 스타일은 **그 Header 영역 안에서만** 씁니다. 전용 스타일은 Header의 class(`hm-sw-header__*`·`hm-sw-process-*`·`hm-sw-viewer-header__*`, Web Header의 `data-slot`)와 Header 모양을 정하는 값(배경·gradient·테두리·글자 외곽선·높이·간격)입니다. `DialogHeader`·`CardHeader`처럼 다른 컴포넌트 안의 머리 부품은 여기서 말하는 Header가 아닙니다. 이 규칙은 아래를 정할 뿐 기존 Header의 레이아웃·스타일·공개 API를 바꾸지 않습니다.
1. Header 밖의 본문·패널·카드·버튼·입력 필드와 다른 컴포넌트에 Header 전용 class·CSS·값을 붙이거나 복사하지 않습니다. "Header 스타일로" 같은 요청도 Header 영역에만 적용하고, 별도 요청이 없으면 Header 밖 기존 UI의 디자인과 동작은 그대로 둡니다.
2. 화면 전체 토큰·테마(Web Runtime Theme `--hm-sem-*`, SW 가장 바깥 범위의 `style` 토큰, 위 표의 범위)와 Header 전용 스타일을 구분합니다. Header를 맞추려고 화면 전체 테마 값을 바꾸지 않습니다.
3. Header를 구현하려고 전역 CSS(`html`·`body`·`*`, 공통 reset)를 바꾸거나 기존 본문 스타일을 덮어쓰지 않습니다.
4. Header와 본문의 경계에서는 배치에 필요한 연결만 소비 프로젝트 class로 처리합니다(Header 높이만큼 본문 시작 위치, sticky·z-index, 남는 높이를 본문에 주는 flex 등).
5. 공식 컴포넌트 내부 스타일을 소비 프로젝트에서 Override하지 않습니다(위 혼합 규칙 3). 바꿀 수 있는 것은 각 패키지가 공개한 Props·토큰뿐입니다.
6. 시안에 본문 디자인이 따로 있으면 그 영역의 패키지(위 혼합 규칙 1)의 공식 컴포넌트로 구현합니다. Header와 같은 패키지의 부품을 Header 밖에 둘 때도 그 부품의 공식 사용법(SW는 `<SWTheme>` 안)을 따르고 Header 모양을 덧입히지 않습니다.
7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`<SWTheme>` 부품이 `--hm-sw-header-*` 토큰을 읽음, 위 SW 분리 3). 소비 코드가 이를 흉내 내 Header 스타일을 다른 곳에 쓰면 안 됩니다.
8. 사용자가 화면 전체에 Header 디자인을 적용하라고 **명시한 경우에만** 범위를 넓힙니다. 이때도 공식 컴포넌트·공개 토큰으로만 맞추고 Header class를 복사하지 않으며, 결과 보고에 넓힌 범위를 적습니다.
## 6단계. 디자인 검증
소비 프로젝트에서 실제로 실행한 것만 PASS로 적습니다.
@@ -186,7 +199,7 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
2. 브라우저 렌더링: 재사용한 컴포넌트가 HM 외형으로 보이는지(CSS 진입점 누락 확인), 신규 UI가 같은 토큰을 쓰는지
3. 상태·동작: Hover / Focus(키보드) / Disabled, Popup 열기·닫기·Escape·Focus 복귀, 선택 패키지의 핵심 동작
4. 폭: Web 390 / 768 / 1280 / 1440px, SW 일반 모드는 대상 창 폭(Header는 반응형 More 포함), SW 뷰어 모드는 대상 창 폭을 줄였다 늘리며 실제 공간 부족에 따른 단계(`iconOnlyBelow` 항목 아이콘만 → `collapseBelow` 항목 More 이동, 숫자는 px 경계가 아니라 순서) 전후, OrgChart는 캔버스 크기·초기 Fit
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지(§5)
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지, Header 전용 스타일이 Header 밖에 쓰이지 않았는지(§5, "Header 전용 스타일의 적용 범위")
6. 실행하지 못한 항목은 `NOT VERIFIED`로 남깁니다.
## 7단계. 결과 보고 형식
+1
View File
@@ -116,6 +116,7 @@ import "@hm-design/sw/styles.css" // 앱 진입점에서 한 번
- 소비 코드는 `hm-sw-*` class를 붙이거나 재정의하지 않는다. 배치는 자기 class(`className`)와 `style`로 한다.
- 뷰어 모드 컴포넌트(`SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`)는 자체 `hm-sw-theme` 범위를 가지므로 `<SWTheme>`로 감싸지 않아도 된다. 소비 코드는 `hm-sw-viewer-*` class도 붙이거나 재정의하지 않는다.
- 외형 변경은 §3.1의 방법만 쓴다. 사용자 확정 면(파일명·객체 수정·속성 입력)은 기본값 유지를 권장한다.
- Header(`SWHeader`·`SWProcessHeader`·`SWViewerHeader`) 전용 스타일은 Header 영역에만 쓴다. 본문·패널에 Header 모양을 적용하지 않는다([AI_GUIDE.md](./AI_GUIDE.md) 5단계 "Header 전용 스타일의 적용 범위").
- Light/Dark 분기는 없다. SW 디자인은 하나다.
### 3.1 테마(토큰) 설정 — 공식 방법 (2026-10-20)
+2 -1
View File
@@ -191,7 +191,7 @@ SW **뷰어 모드** 전용 Export입니다(2026-10-19 추가, 참고 DWG/DXF
| 컴포넌트 | 용도 | 주요 Props | 크기 | 검증 |
|---|---|---|---|---|
| `SWProcessHeader` | 49px Header: 로고 → 파일명 → 프로세스 영역 · Layer 면 → 창 영역(도구 pill · 창 이동 · 최소화 · 최대화/복원 · 종료) | `appearance`(`"default"` Strana · `"multi"` StrAna-SW 다중 Step, 기본 `default`, `process.appearance`가 없으면 프로세스 영역에도 적용), `documentTitle`(필수, 문서가 없으면 `""`), `documentEmptyLabel`(제목이 비었을 때 파일명 면 오른쪽 위 10px 표시, 예: `프로젝트 생성 전`), `onDocumentClick`, `logo`(커스텀 요소) / `logoText`(글자 로고) / `logoSrc`(SWHeader 로고 칸: 높이 17·로고 비율 폭, 32 × 17 → 홈 80, 두 타입 공통. 우선순위 `logo` > `logoText` > `logoSrc`, 셋 다 같은 로고 칸에 들어가 홈 = 12 + 로고 폭 + 36. 글자 로고는 최소 58px 칸 → 홈 최소 106, 더 긴 글자는 글자 폭만큼 넓어짐·잘림 없음), `logoAlt`(기본 `logoText`), `onHomeClick`, `homeMenuOpen`(로고 메뉴는 호스트 소유), `process: SWProcessBarProps`(필수), `layer?: SWProcessHeaderLayer`(`label` 기본 `레이어`, `value`, `visible`, `open`, `disabled`), `onLayerClick`, `onLayerVisibilityChange(next)`, `tools?: ReactNode`(`SWToolButton appearance="pill"`, Header가 pill로 감쌈), `toolsLabel`(기본 `도구`), `onWindowMoveStart`, `maximized`(기본 false = 최대화 글리프, true = 복원 글리프), `onMinimize`, `onMaximizeToggle`, `onClose`, `hiddenControls?: SWProcessHeaderHideableControlId[]`(`layer`·`tools`·`windowMove`·`minimize`·`maximize`·`close`), `disabled`(프로세스·Layer만, 로고·파일명·도구·창 버튼 제외), `<header>` HTML 속성·`ref` | 높이 49. `default`: 면 48, 파일명 286 : 가운데 1260(프로세스 1003 : Layer 257) 비율, 파일명 100~286, Layer 칸 152~257, 창 영역 약 312(도구 3개). `multi`: 면 49, 파일명 189 : 프로세스 1278, 파일명 100~189, SW pill(버튼 40 × 37), 창 영역 367(도구 5개, 최대화 숨김) | ◻ |
| `SWProcessHeader` | 49px Header: 로고 → 파일명 → 프로세스 영역 · Layer 면 → 창 영역(도구 pill · 창 이동 · 최소화 · 최대화/복원 · 종료) | `appearance`(`"default"` Strana · `"multi"` StrAna-SW 다중 Step, 기본 `default`, `process.appearance`가 없으면 프로세스 영역에도 적용), `documentTitle`(필수, 문서가 없으면 `""`), `documentEmptyLabel`(제목이 비었을 때 파일명 면 오른쪽 위 10px 표시, 예: `프로젝트 생성 전`. `documentTitle=""`이면 파일명 면에 Hover 색 변화·손가락 커서가 없음 — 크기·위치·색과 `onDocumentClick` 호출은 그대로, 2026-11-05), `onDocumentClick`, `logo`(커스텀 요소) / `logoText`(글자 로고) / `logoSrc`(SWHeader 로고 칸: 높이 17·로고 비율 폭, 32 × 17 → 홈 80, 두 타입 공통. 우선순위 `logo` > `logoText` > `logoSrc`, 셋 다 같은 로고 칸에 들어가 홈 = 12 + 로고 폭 + 36. 글자 로고는 최소 58px 칸 → 홈 최소 106, 더 긴 글자는 글자 폭만큼 넓어짐·잘림 없음), `logoAlt`(기본 `logoText`), `onHomeClick`, `homeMenuOpen`(로고 메뉴는 호스트 소유), `process: SWProcessBarProps`(필수), `layer?: SWProcessHeaderLayer`(`label` 기본 `레이어`, `value`, `visible`, `open`, `disabled`), `onLayerClick`, `onLayerVisibilityChange(next)`, `tools?: ReactNode`(`SWToolButton appearance="pill"`, Header가 pill로 감쌈), `toolsLabel`(기본 `도구`), `onWindowMoveStart`, `maximized`(기본 false = 최대화 글리프, true = 복원 글리프), `onMinimize`, `onMaximizeToggle`, `onClose`, `hiddenControls?: SWProcessHeaderHideableControlId[]`(`layer`·`tools`·`windowMove`·`minimize`·`maximize`·`close`), `disabled`(프로세스·Layer만, 로고·파일명·도구·창 버튼 제외), `<header>` HTML 속성·`ref` | 높이 49. `default`: 면 48, 파일명 286 : 가운데 1260(프로세스 1003 : Layer 257) 비율, 파일명 100~286, Layer 칸 152~257, 창 영역 약 312(도구 3개). `multi`: 면 49, 파일명 189 : 프로세스 1278, 파일명 100~189, SW pill(버튼 40 × 37), 창 영역 367(도구 5개, 최대화 숨김) | ◻ |
| `SWProcessSplitButton` | 화면 분할 도구(Strana 화면 분할, StrAna-SW 6758:12510): `tools` pill 안 버튼 + 233 × 36 선택 패널(칸 6개). 열리면 버튼이 어두운 면 + 금색 위·양옆으로 바뀌고 Header 끝까지 이어진 뒤 패널이 Header 바로 아래에 붙음 | `value: SWProcessSplitType`(필수, 버튼 아이콘·선택 칸), `onValueChange(value)`(다른 칸 선택, 패널은 열린 채), `open` / `defaultOpen` / `onOpenChange(open)`, `label`(기본 `화면 분할`, 버튼 접근성 이름·`title`·패널 이름), `typeLabels`, `disabled`. 키보드: Enter·Space 열기/닫기, ArrowDown 열고 선택 칸으로, 방향키·Home·End 선택, Escape 닫고 버튼으로, Tab 이탈·바깥 클릭 닫기. 실제 창 분할은 호스트 | 버튼 40 × 38(`multi` 37), 패널 233 × 36, 칸 38.5 × 34 | ◻ |
| `SWProcessBar` | 프로세스 영역: 그룹(번호·이름·단계 탭) + 그룹 사이 화살표, 활성 그룹 발광 면, 비활성 덮개. 단계 하나만 선택(radiogroup) | `appearance`(`"default"` · `"multi"`, 기본 `default`), `groups: SWProcessGroup[]`(필수, `id`·`number`·`label`·`labelHidden`(이름을 보조기술용으로만 둠, `multi`는 제목 줄 높이 유지)·`steps: SWProcessStep[]`(`id`·`label`·`disabled`)·`disabled`), `activeStepId`, `activeGroupId`(기본 = `activeStepId`가 속한 그룹), `onStepSelect(step, group)`(이미 선택된 단계를 다시 누르면 호출 안 함), `disabled`, `aria-label`(기본 `프로세스`), `div` 속성 | `default`: 높이 48, 탭 32(넓을 때 127). `multi`: 높이 49, 제목 12px 위 + 탭 25(글자 폭 + 25, 간격 0). 그룹은 자연 폭까지·좁으면 비례 축소 | ◻ |
@@ -199,6 +199,7 @@ SW **뷰어 모드** 전용 Export입니다(2026-10-19 추가, 참고 DWG/DXF
- **사다리꼴(그룹 면) 배치 확정(2026-11-03):** 시작(첫 그룹)과 끝(마지막 그룹)에만 둡니다(켜졌으면 발광 면, 아니면 비활성 면). 가운데 그룹은 켜져 있어도 도형 없이 밝은 바탕만 보입니다. 그룹 수와 관계없이, `default`·`multi` 공통입니다. 그룹 사이 화살표·비활성 덮개·선택 상태는 그대로입니다.
- **`multi`**(Figma StrAna-SW 873:16481 · 873:16254 · 873:17421): 그룹 수·단계 수는 데이터대로이며, 켜진 그룹보다 앞 그룹은 진행 완료(제목 700 흰색·밝은 탭), 뒤 그룹은 미진행(제목 500 `#C3EBDF`·70% 탭)으로 표시합니다. 바탕·덮개·발광·화살표·선택·키보드는 `default`와 같은 구현입니다.
- **키보드:** 탭 사이 Arrow(좌우·상하)·Home·End 이동 + 선택(비활성 단계 건너뜀), Tab은 선택된 단계 하나로만 들어갑니다.
- **단계 활성화(현재 구현, 2026-11-05 명시):** 단계는 기본으로 모두 선택할 수 있습니다. `step.disabled: true`, `group.disabled: true`, `process.disabled` 또는 Header `disabled`를 명시했을 때만 비활성입니다(생략·`false`는 활성). 프로젝트 생성 여부·파일명 유무(`documentTitle`, `documentEmptyLabel`)는 활성화에 영향을 주지 않으므로, 새 프로젝트라는 이유로 `disabled`를 넣지 않습니다. 단계 선택은 호스트가 `activeStepId`를 바꾸는 방식이며(`onStepSelect`에서 갱신, 이전·다음 버튼도 같은 값을 바꿈), 그룹의 켜짐·진행 완료 모양은 `activeStepId`로 정해집니다. `activeStepId`가 없으면 선택된 단계와 켜진 그룹 없이 모든 그룹이 켜지지 않은 모양이지만 단계는 그대로 선택할 수 있고, `groups`가 비면 단계 없이 프로세스 바탕만 그립니다. 컴포넌트는 단계를 만들거나 자동 선택하지 않습니다.
- **반응형:** 숨김·More·스크롤 없음. 보장 폭 1100px 이상이며 더 좁으면 Header 안에서 잘립니다. 탭 글자·파일명·Layer 이름은 `…` 말줄임. 공간이 부족하면 선택된 탭도 다른 탭과 같은 비율로 줄어듭니다(2026-11-03 개정): `default`는 모든 탭이 같은 폭, `multi`는 글자 폭에 비례하며 이름이 들어가지 않으면 `…`. 선택이 바뀌어도 탭 폭은 그대로입니다. 그룹은 이름 + 모든 탭 여백보다 좁아지지 않아 겹치지 않습니다. 그룹 사이 화살표·간격은 고정입니다. **넘침 정책 확정(2026-11-03): 그룹 접기·좌우 스크롤·More 메뉴를 추가하지 않습니다.**
- **원문 확인(현재 구현):** 파일명·Layer 값은 `title`로 전체 이름을 보여 줍니다. 단계 탭 이름과 `multi` 그룹 제목은 말줄임되어도 `title`이 없고 접근성 이름(탭 `aria-label`, 그룹 이름 `aria-describedby`)만 전체 이름입니다. 개선은 별도 과제입니다([DESIGN_RULES.md](./DESIGN_RULES.md) §15.1). 호스트 `tools`의 `SWToolButton pill`은 문자열 `children`이 기본 툴팁이 되며, 숫자·JSX `children`이면 호스트가 `title`을 전달합니다(§4).
- **범위 밖(호스트 소유):** 로고 메뉴 본체, Layer Popup, 실제 화면 분할 실행, 도구 아이콘(소비 측이 `tools`에 넣는 요소). 도움말·3D 보기·설정·메뉴·나가기 같은 Header 도구 아이콘은 공개 Export가 없고, Storybook `SW / Step Header`의 아이콘은 공개 API가 아닙니다. 시안의 아이콘 선택과 창 제어 버튼 생략(`hiddenControls`)은 [AI_USAGE.md](./AI_USAGE.md) §5를 따르며, 창 버튼 기본 구성(모두 표시)은 바뀌지 않습니다. 화면 분할 선택 UI는 `SWProcessSplitButton`(2026-11-03 추가). `SWHomeMenu`는 이 Header에 쓰지 않습니다(2단 타일 디자인이 다름).
+1 -1
View File
@@ -278,7 +278,7 @@ const groups: SWProcessGroup[] = [
3. 프로세스 영역만 다른 곳에 둘 때는 `<SWTheme>` 안에 `SWProcessBar`를 둡니다.
4. 다른 테마(Strana Dark 등)는 11처럼 가장 바깥 범위 `style`에 `--hm-sw-process-*`·`--hm-sw-header-*` 토큰을 지정합니다.
5. `SWHeader`의 메뉴(레이어·색상·선/문자/치수스타일 Dropdown)를 이 Header에 넣지 않습니다. 필요하면 `SWHeader`를 씁니다.
6. **그룹·단계가 많은 다중 Step 화면**(StrAna-SW Figma 873:16481 등)은 `appearance="multi"`입니다. 같은 `process` 데이터 구조를 쓰고, 제목이 없는 그룹은 `labelHidden`, 아직 열 수 없는 단계는 `disabled`, 문서가 없을 때는 `documentTitle=""` + `documentEmptyLabel`입니다. Figma 프레임에는 Layer 필드와 최대화 버튼이 없으므로 `layer`를 주지 않고 `hiddenControls={["maximize"]}`를 씁니다. 그룹·단계가 많아도 데이터를 줄이거나 접기·스크롤·More를 만들지 않습니다(넘침 정책: 선택 단계 포함 비례 축소 + `…`, 2026-11-03 개정).
6. **그룹·단계가 많은 다중 Step 화면**(StrAna-SW Figma 873:16481 등)은 `appearance="multi"`입니다. 같은 `process` 데이터 구조를 쓰고, 제목이 없는 그룹은 `labelHidden`, 호스트가 막아야 한다고 명시한 단계만 `disabled: true`(생략하면 선택 가능, 새 프로젝트·파일명 없음은 비활성 조건이 아님 — [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) 11절 "단계 활성화"), 문서가 없을 때는 `documentTitle=""` + `documentEmptyLabel`입니다. Figma 프레임에는 Layer 필드와 최대화 버튼이 없으므로 `layer`를 주지 않고 `hiddenControls={["maximize"]}`를 씁니다. 그룹·단계가 많아도 데이터를 줄이거나 접기·스크롤·More를 만들지 않습니다(넘침 정책: 선택 단계 포함 비례 축소 + `…`, 2026-11-03 개정).
7. **창 제어 버튼은 시안에 보이는 것만** 둡니다. 위 예시의 창 이동·최소화·종료는 그 Figma 프레임에 있기 때문이며 모든 화면의 기본이 아닙니다. 시안에 없는 버튼은 `hiddenControls`(`"windowMove"`·`"minimize"`·`"maximize"`·`"close"`)로 빼고, 잘린 시안·웹/데스크톱 구분은 [AI_USAGE.md](./AI_USAGE.md) §5.5, 예시는 §5.6을 따릅니다. 기존 화면의 창 버튼은 시안에 없다는 이유만으로 빼지 않습니다(AI_USAGE §5.1). 컴포넌트 기본값은 바꾸지 않습니다.
```tsx
@@ -52,10 +52,14 @@ export const SWProcessHeader = forwardRef<HTMLElement, SWProcessHeaderProps>(fun
const captions = (["minimize", "maximize", "close"] as const).filter(id => !hidden.has(id))
const windowShown = toolsShown || !hidden.has("windowMove") || captions.length > 0
const documentEmpty = documentTitle === "" && !!documentEmptyLabel
// No file yet (documentTitle ""): the file name does not look clickable (no hover color, default cursor). The button
// and `onDocumentClick` stay as they were (2026-11-05: hosts may still listen to it); only the look changes.
const documentUntitled = documentTitle === ""
return (
<header ref={ref} className={`hm-sw-theme hm-sw-process-header${className ? ` ${className}` : ""}`}
data-appearance={appearance === "multi" ? "multi" : undefined} data-document-empty={documentEmpty || undefined} {...rest}>
data-appearance={appearance === "multi" ? "multi" : undefined} data-document-empty={documentEmpty || undefined}
data-document-untitled={documentUntitled || undefined} {...rest}>
<div className="hm-sw-process-header__bar">
<SWHeaderHome logo={logoNode} logoSrc={logoSrc} logoAlt={logoAlt ?? logoText} onClick={onHomeClick} aria-expanded={homeMenuOpen} />
<SWHeaderDocumentTitle title={documentEmpty ? documentEmptyLabel : documentTitle} onClick={onDocumentClick} />
+5 -1
View File
@@ -470,7 +470,11 @@
text-align: right;
text-shadow: var(--hm-sw-header-label-outline);
}
.hm-sw-process-header[data-document-empty] .hm-sw-header__document-button:not(:hover) .hm-sw-header__document-name { color: var(--hm-sw-process-document-empty-color); }
/* 2026-11-05: before a file exists (documentTitle "") the file name does not look clickable: the label keeps its color
on hover (was #FFC600 like a file name) and the cursor stays default. Size, place, colors and the click are unchanged;
a real file name keeps the SWHeader hover. */
.hm-sw-process-header[data-document-empty] .hm-sw-header__document-button .hm-sw-header__document-name { color: var(--hm-sw-process-document-empty-color); }
.hm-sw-process-header[data-document-untitled] .hm-sw-header__document-button { cursor: default; }
/* ---------- appearance="multi" (StrAna-SW multi step, Figma 873:16481 / 873:16254 / 873:17421) ----------
Same markup and shared rules as the default; only sizes, the file name, the tool pill and the group content differ.
@@ -0,0 +1,84 @@
// SWProcessHeader state regression check (2026-11-05): step enablement, step selection by activeStepId, the "no step"
// cases and the file name before a file exists. Server render of the built package (no browser, no Storybook).
// Usage: npm run build -w @hm-design/sw && node packages/sw/tests/process-header-states.check.mjs
import { readFile } from "node:fs/promises"
import { createElement as h } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { SWProcessHeader } from "../dist/index.js"
const results = []
const check = (name, ok, detail) => { results.push(ok); console.log(`${ok ? "PASS" : "FAIL"} ${name}${ok || detail === undefined ? "" : ` ${JSON.stringify(detail)}`}`) }
const render = props => renderToStaticMarkup(h(SWProcessHeader, { appearance: "multi", documentTitle: "", documentEmptyLabel: "프로젝트 생성 전", ...props }))
const tabs = html => [...html.matchAll(/<button[^>]*role="radio"[^>]*>/g)].map(([tag]) => ({
id: tag.match(/data-step-id="([^"]*)"/)?.[1], disabled: /\sdisabled=""/.test(tag), checked: /aria-checked="true"/.test(tag), stop: /tabindex="0"/i.test(tag),
}))
const groupsOf = html => [...html.matchAll(/<div class="hm-sw-process-bar__group"([^>]*)>/g)].map(([, attrs]) => ({
active: /data-active="true"/.test(attrs), dim: /data-dim="true"/.test(attrs), done: /data-progress="done"/.test(attrs),
}))
const count = (list, key) => list.filter(item => item[key]).length
const GROUPS = [
{ id: "g1", number: "01", label: "데이터 구축", steps: [{ id: "input", label: "데이터입력" }, { id: "review", label: "데이터확인", disabled: false }] },
{ id: "g2", number: "02", label: "선형 입력", steps: [{ id: "line", label: "선형입력" }] },
{ id: "g3", number: "03", label: "해석", steps: [{ id: "model", label: "해석모델" }, { id: "result", label: "성과품", disabled: true }] },
]
// 1. Enablement: only an explicit disabled: true (step, group, process, header) turns steps off.
{
const t = tabs(render({ process: { groups: GROUPS, activeStepId: "input" } }))
check("steps without disabled (omitted or false) are enabled; only disabled: true is off", t.length === 5 && count(t, "disabled") === 1 && t[4].disabled)
const group = tabs(render({ process: { groups: GROUPS.map(g => (g.id === "g2" ? { ...g, disabled: true } : g)), activeStepId: "input" } }))
check("group.disabled turns only that group's steps off", count(group, "disabled") === 2 && group[2].disabled)
check("process.disabled turns every step off", count(tabs(render({ process: { groups: GROUPS, disabled: true } })), "disabled") === 5)
check("header disabled turns every step off", count(tabs(render({ process: { groups: GROUPS }, disabled: true })), "disabled") === 5)
}
// 2. Project / file state never decides enablement.
{
const variants = [
render({ process: { groups: GROUPS, activeStepId: "input" } }),
render({ documentEmptyLabel: undefined, process: { groups: GROUPS, activeStepId: "input" } }),
render({ documentTitle: "Bridge.sa", documentEmptyLabel: undefined, process: { groups: GROUPS, activeStepId: "input" } }),
].map(html => tabs(html).map(t => t.disabled).join())
check("empty title, no empty label and a real file name give the same enabled steps", new Set(variants).size === 1, variants)
}
// 3. Selection follows activeStepId (what step clicks, keyboard and host previous / next buttons change).
{
const order = ["input", "review", "line", "model"]
const states = order.map(step => { const html = render({ process: { groups: GROUPS, activeStepId: step } }); return { t: tabs(html), g: groupsOf(html) } })
check("each activeStepId checks exactly that step, with the single tab stop on it",
states.every(({ t }, i) => count(t, "checked") === 1 && t.findIndex(x => x.checked) === [0, 1, 2, 3][i] && count(t, "stop") === 1 && t[[0, 1, 2, 3][i]].stop))
const lit = states.map(({ g }) => g.findIndex(x => x.active))
check("crossing groups moves the lit group (input/review -> 01, line -> 02, model -> 03)", lit.join() === "0,0,1,2", lit)
check("multi: groups before the lit one are done, the others not", states[3].g.map(x => x.done).join() === "true,true,false" && states[0].g.every(x => !x.done))
}
// 4. No step set: nothing is created or selected, the steps stay selectable.
{
const html = render({ process: { groups: GROUPS } })
const t = tabs(html), g = groupsOf(html)
check("no activeStepId: no checked step, no lit or done group, every group dim", count(t, "checked") === 0 && g.every(x => !x.active && !x.done && x.dim))
check("no activeStepId: steps stay enabled and the first enabled step is the tab stop", count(t, "disabled") === 1 && count(t, "stop") === 1 && t[0].stop)
const empty = render({ process: { groups: [] } })
check("groups = []: no step tab and no group are rendered (nothing invented)", tabs(empty).length === 0 && groupsOf(empty).length === 0 && /hm-sw-process-bar/.test(empty))
}
// 5. File name before a file exists: marked untitled, still the same button (click kept), CSS removes the clickable look.
{
const untitled = render({ process: { groups: GROUPS } })
const named = render({ documentTitle: "Bridge.sa", documentEmptyLabel: undefined, process: { groups: GROUPS } })
check("documentTitle \"\" marks the header data-document-untitled; a real file name does not",
/<header[^>]*data-document-untitled="true"/.test(untitled) && !/data-document-untitled/.test(named))
check("the file name stays a button in both states (onDocumentClick is still wired)",
/<button[^>]*class="hm-sw-header__document-button"/.test(untitled) && /<button[^>]*class="hm-sw-header__document-button"/.test(named))
const css = (await readFile(new URL("../dist/styles.css", import.meta.url), "utf8")).replace(/\s+/g, " ")
check("styles.css: untitled file name has the default cursor", /\[data-document-untitled\] \.hm-sw-header__document-button ?\{ ?cursor: ?default/.test(css))
check("styles.css: the empty label keeps its color on hover (no :not(:hover) scope)",
/\[data-document-empty\] \.hm-sw-header__document-button \.hm-sw-header__document-name ?\{ ?color: ?var\(--hm-sw-process-document-empty-color\)/.test(css)
&& !/\[data-document-empty\] \.hm-sw-header__document-button:not\(:hover\)/.test(css))
}
const passed = results.filter(Boolean).length
console.log(`${passed}/${results.length} PASS`)
process.exitCode = passed === results.length ? 0 : 1
+14 -1
View File
@@ -178,6 +178,19 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
3. 패키지가 실제로 공유하는 부분은 혼합이 아닙니다: `SWViewerPanel`은 `SWPopup` 프레임을 그대로 렌더링하고, 두 Header는 파일명·객체 수정·속성 면 토큰(`--hm-sw-header-document-*`·`object-*`·`property-*`)을 공유합니다. 소비 코드가 이 공유를 흉내 내 다른 모드 부품을 섞어도 된다는 뜻은 아닙니다.
4. 프로그램이 두 모드를 모두 가지면 모드 전환 시 Header 컴포넌트를 바꿔 끼웁니다(파일·패널 상태는 프로그램 상태로 유지). 세부는 `@hm-design/sw/ai/AI_USAGE.md` §0과 `VIEWER.md`.
### Header 전용 스타일의 적용 범위 (2026-11-05)
화면 상단 Header 컴포넌트 — SW `SWHeader`·`SWProcessHeader`·`SWViewerHeader`, Web `GNB`·`LNBMobileHeader`·`DashboardHeader` — 의 전용 스타일은 **그 Header 영역 안에서만** 씁니다. 전용 스타일은 Header의 class(`hm-sw-header__*`·`hm-sw-process-*`·`hm-sw-viewer-header__*`, Web Header의 `data-slot`)와 Header 모양을 정하는 값(배경·gradient·테두리·글자 외곽선·높이·간격)입니다. `DialogHeader`·`CardHeader`처럼 다른 컴포넌트 안의 머리 부품은 여기서 말하는 Header가 아닙니다. 이 규칙은 아래를 정할 뿐 기존 Header의 레이아웃·스타일·공개 API를 바꾸지 않습니다.
1. Header 밖의 본문·패널·카드·버튼·입력 필드와 다른 컴포넌트에 Header 전용 class·CSS·값을 붙이거나 복사하지 않습니다. "Header 스타일로" 같은 요청도 Header 영역에만 적용하고, 별도 요청이 없으면 Header 밖 기존 UI의 디자인과 동작은 그대로 둡니다.
2. 화면 전체 토큰·테마(Web Runtime Theme `--hm-sem-*`, SW 가장 바깥 범위의 `style` 토큰, 위 표의 범위)와 Header 전용 스타일을 구분합니다. Header를 맞추려고 화면 전체 테마 값을 바꾸지 않습니다.
3. Header를 구현하려고 전역 CSS(`html`·`body`·`*`, 공통 reset)를 바꾸거나 기존 본문 스타일을 덮어쓰지 않습니다.
4. Header와 본문의 경계에서는 배치에 필요한 연결만 소비 프로젝트 class로 처리합니다(Header 높이만큼 본문 시작 위치, sticky·z-index, 남는 높이를 본문에 주는 flex 등).
5. 공식 컴포넌트 내부 스타일을 소비 프로젝트에서 Override하지 않습니다(위 혼합 규칙 3). 바꿀 수 있는 것은 각 패키지가 공개한 Props·토큰뿐입니다.
6. 시안에 본문 디자인이 따로 있으면 그 영역의 패키지(위 혼합 규칙 1)의 공식 컴포넌트로 구현합니다. Header와 같은 패키지의 부품을 Header 밖에 둘 때도 그 부품의 공식 사용법(SW는 `<SWTheme>` 안)을 따르고 Header 모양을 덧입히지 않습니다.
7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`<SWTheme>` 부품이 `--hm-sw-header-*` 토큰을 읽음, 위 SW 분리 3). 소비 코드가 이를 흉내 내 Header 스타일을 다른 곳에 쓰면 안 됩니다.
8. 사용자가 화면 전체에 Header 디자인을 적용하라고 **명시한 경우에만** 범위를 넓힙니다. 이때도 공식 컴포넌트·공개 토큰으로만 맞추고 Header class를 복사하지 않으며, 결과 보고에 넓힌 범위를 적습니다.
## 6단계. 디자인 검증
소비 프로젝트에서 실제로 실행한 것만 PASS로 적습니다.
@@ -186,7 +199,7 @@ SW 일반 모드와 뷰어 모드는 같은 패키지·같은 CSS 파일이지
2. 브라우저 렌더링: 재사용한 컴포넌트가 HM 외형으로 보이는지(CSS 진입점 누락 확인), 신규 UI가 같은 토큰을 쓰는지
3. 상태·동작: Hover / Focus(키보드) / Disabled, Popup 열기·닫기·Escape·Focus 복귀, 선택 패키지의 핵심 동작
4. 폭: Web 390 / 768 / 1280 / 1440px, SW 일반 모드는 대상 창 폭(Header는 반응형 More 포함), SW 뷰어 모드는 대상 창 폭을 줄였다 늘리며 실제 공간 부족에 따른 단계(`iconOnlyBelow` 항목 아이콘만 → `collapseBelow` 항목 More 이동, 숫자는 px 경계가 아니라 순서) 전후, OrgChart는 캔버스 크기·초기 Fit
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지(§5)
5. 혼합이면 영역 경계에서 스타일 침범이 없는지, SW면 선택하지 않은 모드의 컴포넌트·토큰이 섞이지 않았는지, Header 전용 스타일이 Header 밖에 쓰이지 않았는지(§5, "Header 전용 스타일의 적용 범위")
6. 실행하지 못한 항목은 `NOT VERIFIED`로 남깁니다.
## 7단계. 결과 보고 형식
+1
View File
@@ -54,6 +54,7 @@ import { applyRuntimeTheme, resetRuntimeTheme, loadRuntimeTheme } from "@hm-desi
- Light/Dark 분기 없음. `dark:` 분기·Dark Token을 추가하지 않는다.
- 테마 변경은 Runtime Theme(`applyRuntimeTheme(root, theme)`, `loadRuntimeTheme(root, url)`)만 쓴다. `theme`은 `{ schemaVersion: 1, tokens: { "color.bg.content": "#…" } }` 형식이며 키는 `runtimeThemeContract`에 있는 것만 유효하다.
- 한 페이지의 Navigation은 GNB / LNB / DASHBOARD 중 하나다([COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) §1).
- `GNB`·`LNBMobileHeader`·`DashboardHeader`의 전용 스타일은 Header 영역에만 쓴다. 본문 UI에 Header 모양을 적용하지 않는다([AI_GUIDE.md](./AI_GUIDE.md) 5단계 "Header 전용 스타일의 적용 범위").
## 4. 접근성 기본