diff --git a/WORKING_CONTEXT.md b/WORKING_CONTEXT.md index dea3533..719b46b 100644 --- a/WORKING_CONTEXT.md +++ b/WORKING_CONTEXT.md @@ -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`, 예시, 보고). diff --git a/docs/ai/AI_GUIDE.md b/docs/ai/AI_GUIDE.md index 92c6ee5..68b0f36 100644 --- a/docs/ai/AI_GUIDE.md +++ b/docs/ai/AI_GUIDE.md @@ -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는 `` 안)을 따르고 Header 모양을 덧입히지 않습니다. +7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`` 부품이 `--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단계. 결과 보고 형식 diff --git a/docs/sw/sw-process-header-design-spec.md b/docs/sw/sw-process-header-design-spec.md index 7b55c01..8d86b45 100644 --- a/docs/sw/sw-process-header-design-spec.md +++ b/docs/sw/sw-process-header-design-spec.md @@ -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) diff --git a/packages/org-chart-react/ai/AI_GUIDE.md b/packages/org-chart-react/ai/AI_GUIDE.md index 92c6ee5..68b0f36 100644 --- a/packages/org-chart-react/ai/AI_GUIDE.md +++ b/packages/org-chart-react/ai/AI_GUIDE.md @@ -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는 `` 안)을 따르고 Header 모양을 덧입히지 않습니다. +7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`` 부품이 `--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단계. 결과 보고 형식 diff --git a/packages/org-chart/ai/AI_GUIDE.md b/packages/org-chart/ai/AI_GUIDE.md index 92c6ee5..68b0f36 100644 --- a/packages/org-chart/ai/AI_GUIDE.md +++ b/packages/org-chart/ai/AI_GUIDE.md @@ -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는 `` 안)을 따르고 Header 모양을 덧입히지 않습니다. +7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`` 부품이 `--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단계. 결과 보고 형식 diff --git a/packages/sw/ai/AI_GUIDE.md b/packages/sw/ai/AI_GUIDE.md index 92c6ee5..68b0f36 100644 --- a/packages/sw/ai/AI_GUIDE.md +++ b/packages/sw/ai/AI_GUIDE.md @@ -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는 `` 안)을 따르고 Header 모양을 덧입히지 않습니다. +7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`` 부품이 `--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단계. 결과 보고 형식 diff --git a/packages/sw/ai/AI_USAGE.md b/packages/sw/ai/AI_USAGE.md index 5f75fe6..8a58cca 100644 --- a/packages/sw/ai/AI_USAGE.md +++ b/packages/sw/ai/AI_USAGE.md @@ -116,6 +116,7 @@ import "@hm-design/sw/styles.css" // 앱 진입점에서 한 번 - 소비 코드는 `hm-sw-*` class를 붙이거나 재정의하지 않는다. 배치는 자기 class(`className`)와 `style`로 한다. - 뷰어 모드 컴포넌트(`SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`)는 자체 `hm-sw-theme` 범위를 가지므로 ``로 감싸지 않아도 된다. 소비 코드는 `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) diff --git a/packages/sw/ai/COMPONENT_CATALOG.md b/packages/sw/ai/COMPONENT_CATALOG.md index 4d5eb9c..41e9140 100644 --- a/packages/sw/ai/COMPONENT_CATALOG.md +++ b/packages/sw/ai/COMPONENT_CATALOG.md @@ -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만, 로고·파일명·도구·창 버튼 제외), `
` 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만, 로고·파일명·도구·창 버튼 제외), `
` 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단 타일 디자인이 다름). diff --git a/packages/sw/ai/COMPOSITION_PATTERNS.md b/packages/sw/ai/COMPOSITION_PATTERNS.md index 9a4862c..c5cd2ca 100644 --- a/packages/sw/ai/COMPOSITION_PATTERNS.md +++ b/packages/sw/ai/COMPOSITION_PATTERNS.md @@ -278,7 +278,7 @@ const groups: SWProcessGroup[] = [ 3. 프로세스 영역만 다른 곳에 둘 때는 `` 안에 `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 diff --git a/packages/sw/src/components/sw-process-header/SWProcessHeader.tsx b/packages/sw/src/components/sw-process-header/SWProcessHeader.tsx index 5e77e1b..6f35240 100644 --- a/packages/sw/src/components/sw-process-header/SWProcessHeader.tsx +++ b/packages/sw/src/components/sw-process-header/SWProcessHeader.tsx @@ -52,10 +52,14 @@ export const SWProcessHeader = forwardRef(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 (
+ data-appearance={appearance === "multi" ? "multi" : undefined} data-document-empty={documentEmpty || undefined} + data-document-untitled={documentUntitled || undefined} {...rest}>
diff --git a/packages/sw/src/theme/sw-process-header.css b/packages/sw/src/theme/sw-process-header.css index 302dcda..c0a9a70 100644 --- a/packages/sw/src/theme/sw-process-header.css +++ b/packages/sw/src/theme/sw-process-header.css @@ -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. diff --git a/packages/sw/tests/process-header-states.check.mjs b/packages/sw/tests/process-header-states.check.mjs new file mode 100644 index 0000000..525f4bb --- /dev/null +++ b/packages/sw/tests/process-header-states.check.mjs @@ -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(/]*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(/
]*)>/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", + /]*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)", + /]*class="hm-sw-header__document-button"/.test(untitled) && /]*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 diff --git a/packages/ui/ai/AI_GUIDE.md b/packages/ui/ai/AI_GUIDE.md index 92c6ee5..68b0f36 100644 --- a/packages/ui/ai/AI_GUIDE.md +++ b/packages/ui/ai/AI_GUIDE.md @@ -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는 `` 안)을 따르고 Header 모양을 덧입히지 않습니다. +7. 패키지가 공식으로 공유하는 부분은 위반이 아닙니다(예: `SWPopup`·`` 부품이 `--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단계. 결과 보고 형식 diff --git a/packages/ui/ai/AI_USAGE.md b/packages/ui/ai/AI_USAGE.md index 685c6d5..2e46ba7 100644 --- a/packages/ui/ai/AI_USAGE.md +++ b/packages/ui/ai/AI_USAGE.md @@ -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. 접근성 기본