diff --git a/WORKING_CONTEXT.md b/WORKING_CONTEXT.md index fa2e937..94b3d79 100644 --- a/WORKING_CONTEXT.md +++ b/WORKING_CONTEXT.md @@ -1,5 +1,23 @@ # Working Context +## 2026-11-05 — Step Header 프로세스 영역 양 끝 직각 + +- 사용자 결정: `SWProcessHeader appearance="multi"`만 프로세스 영역(파일명 옆·창 영역 옆) 위 모서리를 직각으로(`default`는 Strana 6px·덮개 5px 유지 — 처음 두 타입 모두로 들었다가 사용자 정정). `sw-process-header.css` multi 블록에 바탕 `::before`·양 끝 덮개 `border-radius: 0` 1줄. 단계 탭 캡슐·화살표·사다리꼴 면·색·높이 변경 없음. Header 최외곽은 원래 직각. +- 문서: DESIGN_RULES §14 프로세스 바탕 행, Process 명세 §9.2. 회귀 검사 `process-header-states.check.mjs`에 default 6/5 유지·multi 0 검사 추가. 미Commit. + +## 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`, 예시, 보고). +- 참조만 추가: `docs/ai/AI_GUIDE.md` 아이콘 칸(+ 4개 패키지 사본 sync, sw 사본은 LF 유지), `COMPOSITION_PATTERNS.md` §12·§9, `COMPONENT_CATALOG.md` §4·§11, `DESIGN_RULES.md` §9. +- 보류: Figma 도구 아이콘 5개(help·view-3d·controls·menu-list·exit) Export — 출처·권한 미확인. 제안 구조 `swProcessToolIcons`와 전제 조건은 `docs/sw/sw-process-header-design-spec.md` §13. 소스·Props·CSS·공개 API·Fixture 변경 없음. + ## 2026-11-03 — SWProcessHeader 1차 구현 (Strana Main Header) - 작업 브랜치: `feature/sw-process-header`(main 6ca92c5에서 분기). 미Commit·미Push — Commit·Push는 사용자 별도 지시 대상. diff --git a/docs/ai/AI_GUIDE.md b/docs/ai/AI_GUIDE.md index 927a051..68b0f36 100644 --- a/docs/ai/AI_GUIDE.md +++ b/docs/ai/AI_GUIDE.md @@ -141,7 +141,7 @@ | 색·Typography | DESIGN_RULES §2·§3 | DESIGN_RULES §2·§3 | VIEWER.md `SWViewerHeader`·디자인 변경 절 | DESIGN_RULES §2·§3 | | 크기·여백 | DESIGN_RULES §4 | DESIGN_RULES §1 | VIEWER.md 디자인 변경 절(52px Header 등 실측값) | 카드·간격 변경 금지(승인 디자인) | | Border·Radius·배경·그림자 | DESIGN_RULES §2·§4 | DESIGN_RULES §2·§8 | VIEWER.md `SWViewerPanel` 절(프레임은 SWPopup과 같음) | 공개 입력만 | -| 아이콘 | `lucide-react`(패키지 의존성) | 패키지 내장 SVG만, 새 아이콘은 사용자 제공 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon` | Core 내장 | +| 아이콘 | `lucide-react`(패키지 의존성) | 사용자·프로젝트 제공 에셋 → 패키지 공개 Export 중 같은 기능 → 없으면 미지원 보고(외부 라이브러리·AI 생성 SVG·Storybook 파일로 대체 금지). 시안 기반 Header 도구 아이콘·창 제어 버튼은 `@hm-design/sw` `ai/AI_USAGE.md` §5 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon`. 선택 순서는 SW 일반 모드와 같음(`AI_USAGE.md` §5) | Core 내장 | | 스크롤바 | `ScrollArea`(Radix), DESIGN_RULES §6 | 전용 디자인 없음 — 브라우저 기본(AI_USAGE §2) | 전용 디자인 없음 — 패널 본문 스크롤은 `SWViewerPanel` | Core 내부 Zoom/Pan | | Hover / Active / Focus / Disabled | DESIGN_RULES §5 | DESIGN_RULES §4 | VIEWER.md `SWViewerHeader` 절 상태 표현 | Core 내장 | | Popup·Panel | `Dialog`·`AlertDialog`·`Sheet`·`Drawer`·`Popover` | `SWPopup` + `SWPopupSection`/`SWPopupRow` | `SWViewerPanel`(+ `modal`) + `SWViewerSection`/`SWViewerField` | Core 팝업 / HM Dialog / 어댑터 | @@ -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-header-design-spec.md b/docs/sw/sw-header-design-spec.md index b828d2d..bbdcb10 100644 --- a/docs/sw/sw-header-design-spec.md +++ b/docs/sw/sw-header-design-spec.md @@ -321,10 +321,10 @@ SW Header는 여러 SW 프로그램이 공통으로 사용합니다. 따라서 * | 항목 | 기준 | 구분 | |---|---|---| -| 로고 영역 | 홈 버튼(80 × 48) 안 x 12 / y 16, 32 × 17 (Figma `bi_egbim` frame). 토큰 `--hm-sw-header-logo-width` / `-logo-height` | 공통 디자인(확정) | +| 로고 영역 | 홈 버튼 안 x 12 / 세로 가운데, 높이 17 · 폭은 로고 내용 폭(2026-10-23부터, Figma `bi_egbim` frame 32 × 17은 예시이며 이때 홈 80 × 48). 홈 = 12 + 로고 + 36, 사선 배경이 로고 폭을 따라 늘거나 줄며 고정 폭·로고별 예외 없음. 토큰 `--hm-sw-header-logo-width`(기본 `auto`) / `-logo-height` | 공통 디자인(확정) | | 홈 배경 | `home-bg.svg` 사선 녹색 면 + 0–65px 파일명 gradient 바탕 | 공통 디자인(확정) | -| 로고 이미지 | `logoSrc`(이미지 URL) 또는 `logo`(React 요소)로 각 프로그램이 전달. 비율이 달라도 영역 안에 `contain`으로 맞추고 넘치지 않음 | 프로그램별 결정 | -| 로고 미전달 | 로고를 그리지 않음. 홈 배경·파일명·객체 수정 영역 위치는 그대로 | 공통 동작 | +| 로고 이미지 | `logoSrc`(이미지 URL) 또는 `logo`(React 요소)로 각 프로그램이 전달. 이미지·SVG는 높이 17·원본 비율로 표시하고 그 폭만큼 홈이 늘어남(잘리지 않음). 커스텀 글자 요소는 한 줄·`line-height: 17px`로 전달(로고 칸은 상자만 17로 제한, CSS가 강제하지 않음) | 프로그램별 결정 | +| 로고 미전달 | 로고를 그리지 않음. 빈 로고 자리 32px 유지(홈 80 × 48), 홈 배경·파일명·객체 수정 영역 위치는 그대로 | 공통 동작 | | 접근성 | `logoAlt` 전달 시 홈 버튼 이름 " 홈", 없으면 "홈" | 공통 동작 | - Figma 26:8944의 이지빔 로고(`bi_egbim`)는 기준 화면의 예시이며 공통 컴포넌트에 넣지 않습니다(2026-10-16 사용자 지시로 패키지에서 제거). @@ -642,7 +642,7 @@ SW Header는 Figma Node 26:8944를 기준으로 구현한다. 사용자가 확 5. 선택은 글자·단색 아이콘 `#FFC600`만 바꾸고 배경·테두리·그림자를 추가하지 않는다. Focus 표시를 지우지 않는다. 6. 기능이 화면에서 빠질 때는 반드시 더보기로 옮긴다. 필드를 줄여 값을 자르지 말고 기능을 옮긴다. 헤더 가로 스크롤을 만들지 않는다. 7. Figma Assets는 `packages/sw/src/assets/`의 원본 SVG를 쓴다. 임시 Figma URL·스크린샷 이미지를 쓰지 않는다. -8. 로고는 확정 디자인이 아니다. 공통 컴포넌트에 특정 SW 로고·브랜드명·파일 경로를 넣지 않고, 각 프로그램이 `logoSrc` 또는 `logo`와 `logoAlt`로 전달한다. 로고 영역의 위치·크기는 바꾸지 않는다. +8. 로고는 확정 디자인이 아니다. 공통 컴포넌트에 특정 SW 로고·브랜드명·파일 경로를 넣지 않고, 각 프로그램이 `logoSrc` 또는 `logo`와 `logoAlt`로 전달한다. 로고 영역의 위치(x 12·세로 가운데)와 높이 17은 바꾸지 않고, 폭은 로고 내용을 따른다(고정 폭·로고별 예외를 추가하지 않는다). 9. Hover 테두리는 §11.1의 노란 gradient 1px을 사용한다. 배경·크기·그림자를 바꾸지 않고, 새 버튼도 같은 토큰과 대상 목록 규칙을 따른다. 텍스트·아이콘 예외는 §11.1·§11.2·§11.5를 따른다(5개 필드의 텍스트와 지정 미리보기는 §11.5, 최소화·종료는 Figma 전용 hover, 인쇄·팝업 가이드는 기존 테두리 + 지정 fill 변경). SVG는 원본의 해당 foreground fill만 바꾼 로컬 Variant를 사용하며 CSS filter를 쓰지 않는다. ## 17. 향후 공통화 검토 참고 @@ -655,6 +655,8 @@ SW Header는 Figma Node 26:8944를 기준으로 구현한다. 사용자가 확 | 유사하지만 의미 다름 | 선택 노랑 `#FFC600`(HM 팝업 제목 노랑과 같은 값이나 역할 다름), 녹색 계열(HM `green-*` palette와 값 비교 필요) | | 공통화 후보 | Noto Sans KR·400/500/700 정책, 더보기 메뉴(HM `DropdownMenu`와 기능 동일 — 스타일은 SW 전용), Focus 정책, 말줄임·no-scroll 반응형 원칙 | | 비교 필요 | 필드 38px vs HM control height, radius 3/6 vs HM radius scale, Hover 색 | +| class 역할 분리 검토(2026-11-05) | 인쇄 버튼 class `hm-sw-header__action--print`가 `SWToolButton appearance="pill"`(`tools`, `SWProcessHeader`, `SWProcessSplitButton`)에도 붙어 pill 외형을 겸합니다. 분리 여부는 별도 구현 과제이며 영향 범위·원칙은 `packages/sw/ai/DESIGN_RULES.md` §15.3 | +| 원문 확인·툴팁 | 메뉴 타이틀(필드 12px 라벨)은 말줄임되지만 `title`이 없고 버튼 `aria-label`만 원문입니다. 인쇄·창 버튼은 `aria-label`만 있습니다. 공통 원칙·개선 필요 항목은 DESIGN_RULES §15.1 · §15.2 | ## 추가 수정 기준 — 속성창·K 가이드 (2026-10-16) @@ -696,7 +698,7 @@ SW Header는 Figma Node 26:8944를 기준으로 구현한다. 사용자가 확 ## 3차 기준 — 구성요소 표시·메뉴 순서·속성창 상태·형상 Slot (2026-10-16) -- **로고 필수:** 로고 슬롯(32×17, 홈 배경 위 12,16)은 모든 구성에서 표시합니다. `home` 제거 시 같은 배경·위치의 정적 로고(Hover 없음)입니다. +- **로고 필수:** 로고 슬롯(높이 17·로고 폭, 홈 배경 위 x 12·세로 가운데)은 모든 구성에서 표시합니다. `home` 제거 시 같은 배경·위치의 정적 로고(Hover 없음)입니다. - **개별 제거:** `hiddenControls`로 홈·파일·그리기모드·5개 메뉴·상세보기 caption·속성Bar·속성창·인쇄·K·창 이동·최소화·종료를 각각 제거합니다. 남은 요소는 기본 크기를 유지하고 제거한 공간은 남기지 않습니다(구현값: 상세보기 블록 일부 제거 시 폭 auto·최소 64px, 인쇄/K pill과 창 버튼은 남은 버튼 폭, 창 영역 폭 auto). 아무것도 제거하지 않으면 기존 1920px 디자인과 동일합니다. - **창 이동 Tooltip(구현값):** 최소화·종료 중 하나 이상을 제거하면 창 영역 오른쪽 밖은 잘리고(Header 밖 노출 방지), 1080px 미만 24px 이동 영역의 Tooltip은 이동 영역 왼쪽에 표시합니다. 기본 구성의 1400ms Motion·위치는 변경하지 않았습니다. - **메뉴 순서:** `menuOrder`는 5개 유사 메뉴끼리만 자리를 바꿉니다. 기본은 레이어 → 색상 → 선스타일 → 문자스타일 → 치수스타일(Figma 26:8944)입니다. diff --git a/docs/sw/sw-process-header-design-spec.md b/docs/sw/sw-process-header-design-spec.md index aba07c9..139ffa9 100644 --- a/docs/sw/sw-process-header-design-spec.md +++ b/docs/sw/sw-process-header-design-spec.md @@ -38,7 +38,7 @@ Strana Grid의 별 비율을 CSS flex 비율로 옮겼습니다. 숨김 단계· | 영역 | Strana | 구현 | |---|---|---| -| 홈 | 106 × 48, 오른쪽 17px이 파일명 위로 겹침 | **SWHeader 우선(2026-11-03):** `SWHeaderHome` 그대로 = 12 + 로고 칸(높이 17, 로고 비율 폭) + 36, 32 × 17 로고 → 80. 오른쪽 15px이 파일명과 겹친 뒤 2px 간격(SWHeader bar gap, `margin-right: -15px`). `logoText`는 18px 700, 58px 칸(→ 106) | +| 홈 | 106 × 48, 오른쪽 17px이 파일명 위로 겹침 | **SWHeader 우선(2026-11-03):** `SWHeaderHome` 그대로 = 12 + 로고 칸(높이 17, 로고 비율 폭) + 36, 32 × 17 로고 → 80. 오른쪽 15px이 파일명과 겹친 뒤 2px 간격(SWHeader bar gap, `margin-right: -15px`). `logoText`는 18px 700·`--hm-sw-header-text-color`·SW 외곽선·한 줄(줄바꿈 없음), 최소 58px 칸 가운데(→ 홈 최소 106). 글자가 더 넓으면 칸이 글자 폭만큼 넓어지고 홈(클릭 영역)·사선 배경·파일명 시작 위치가 따라감(2026-11-05, 잘림 없음). 홈은 줄지 않으므로(`flex: 0 0 auto`) 늘어난 폭만큼 파일명(최소 100)과 가운데 영역이 flex 비율대로 줄어듦. 참고 실측(Noto Sans KR, 글꼴에 따라 다름): StrAna 59.29 → 홈 107.29, Accessway 93.72 → 홈 141.72 | | 파일명 | `286*`, 최소 100, 최대 286, 글자 시작 22 | 폭만 Strana: `flex: 286 1 0`, `min-width: 100px`, `max-width: 286px`. 글자는 SWHeader 그대로(x 65 시작, padding 5 / 12 → 글자 x 70, 14px 700·자간 0.84px·SW 외곽선·세로 가운데), SW 화살표 숨김 | | 가운데 | `1260*` | `flex: 1260 1 0` | | 프로세스 | `1003*` | `flex: 1003 1 0` | @@ -229,6 +229,7 @@ Hanmac 열이 기본값입니다. 다른 테마는 Header의 `style`(가장 바 | 폭 측정(파일명 / 프로세스 / Layer 칸 / 창 영역) | 1920: 280.8 / 984.8 / 252.3 / 313 = Strana 별 비율 계산값. 1100: 129.1 / 416.9 / 152(최소) / 313. 1366: 178.3 / 625.4 / 160.3 / 313. 2560: 286(최대) / 1615 / 257(최대) / 313. 넘침 없음. 홈 80(SWHeader 기준, 2026-11-03) 이후 파일명 / 프로세스: 1100 133.7 / 437, 1366 182.9 / 641.4, 1920 285.4 / 1000.8, 2560 286 / 1639.7, 창 영역 312, 넘침 없음 | | Strana 캡처 비교(1.5배, 나란히) | Hanmac 1920 Modeling · Results, 1366, 1100 — 배치·면·발광·화살표·탭·Layer·도구·창 버튼 일치. 차이: 글자 외곽선 두께, 파일명 자간, 좁은 폭의 말줄임(§5) | | 동작 | 탭 클릭·Arrow·Home·End(비활성 건너뜀), 같은 탭 재클릭 시 Callback 없음, 전구·필드 분리, 최대화/복원 전환, 로고 열림 모양, 도구 눌림 모양, Hover(종료 `#E40000`, 최소화, Layer 링, 탭 글자), Focus outline | +| 로고 폭(2026-11-05, Chromium, `multi`, 1440·1100) | 기준: 글자 잘림 0, 홈 = 12 + 로고 칸 + 36, 사선 배경 = 홈 + 12(왼쪽 −8 · 오른쪽 +4, 홈 끝을 따라감), 홈·파일명 겹침 15, Header 49, 단계 클릭·Enter 동작 유지. 참고 실측: Accessway 홈 106 → 141.72에서 가운데 영역 1440 1024.2 → 992.5(−31.7), 1100 728 → 696.89(−31.11), 파일명 151.47 → 146.78 / 107.67 → 103.06. 더 긴 글자(187.44)는 파일명이 최소 100에서 멈춤. 이미지 로고(32 × 17) Story는 `default`·`multi` 모두 홈 80 그대로. 커스텀 `logo`는 코드 경로만 확인(같은 로고 칸) | | 기존 `SWHeader` | 변경 전후 빌드에서 `SW/Header` Overview · Responsive More 출력 HTML 동일, `dist/styles.css`의 기존 내용 동일(새 규칙만 추가) | Strana 비교 캡처는 `D:\Strana\_verification\header-rework-20261029\renders\Hanmac-{1100,1366,1920}-*.png`(최종 Commit 7173e82 · b7f412c 이전 렌더)입니다. 분할 버튼 변경(b7f412c) 이후 최신 캡처와 다른 테마(Dark · Light · HighContrast)의 화면 비교는 미확인입니다. Firefox · Safari는 확인하지 않았습니다. @@ -250,7 +251,7 @@ Strana 비교 캡처는 `D:\Strana\_verification\header-rework-20261029\renders\ ### 9.2 `default`와 공유한 것 / 다른 것 - **사다리꼴 배치:** 시작·끝 그룹에만(§4 확정 규칙). 873:17421처럼 가운데 그룹이 켜지면 도형 없이 밝은 바탕만 보입니다(2026-11-03 수정 — 이전 구현은 켜진 가운데 그룹에도 발광 면을 그렸음). -- **같은 구현(값 대조 일치):** 프로세스 바탕(ProcessBase gradient · DocBorder · 위 모서리 6), 비활성 덮개(`#004430` 90%, 꼭지·홈 모양 포함), 활성 발광 면·비활성 바깥 면(같은 경로), 화살표(선·채움·그늘 같은 값, Dim 25% / 50%), 홈 도형, 파일명 면 gradient, 창 이동 아이콘·줄무늬 색, 캡션 버튼 글리프, 선택·키보드·Callback. +- **같은 구현(값 대조 일치):** 프로세스 바탕(ProcessBase gradient · DocBorder. 위 모서리는 2026-11-05부터 `multi`만 직각 — 사용자 결정, `default`는 6 유지), 비활성 덮개(`#004430` 90%, 꼭지·홈 모양 포함), 활성 발광 면·비활성 바깥 면(같은 경로), 화살표(선·채움·그늘 같은 값, Dim 25% / 50%), 홈 도형, 파일명 면 gradient, 창 이동 아이콘·줄무늬 색, 캡션 버튼 글리프, 선택·키보드·Callback. - **multi 전용 CSS:** 49px 면, 파일명 최대 189·단색 테두리(로고·파일명 글자는 SWHeader 기준으로 default와 공통), SW 기본 pill(41 · 40 × 37), 창 이동 위치, 그룹 내용(제목 위 · 탭 25), 탭 4상태, 발광 면 위치(왼쪽 50 / 반전 48). - **새로 만든 것:** 진행 완료 상태(`data-progress="done"`, multi만), `documentEmptyLabel`, `labelHidden`, `appearance`. 컴포넌트 DOM은 하나이며 multi는 탭의 층 9개 대신 면 하나만 그립니다. - **Figma 도구 아이콘 5개**(help · view-3d · controls · menu-list · exit-arrow)는 Story fixture(`src/sw/assets/process-header/tool-*.svg`, Figma 원본)에만 둡니다. @@ -325,3 +326,36 @@ Strana 비교 캡처는 `D:\Strana\_verification\header-rework-20261029\renders\ - **Popup 하단 잘림(2026-11-03, 수정):** 재현 조건 = Popup 아래 공간이 부족한 화면(예: Storybook Canvas 높이 400px의 Multi Step). 공용 위치 훅 `useSWFixedDropdownPosition`이 아래 공간이 없으면 필드 위로 뒤집는데(`flipped`), 이 선택기는 꼬리·패널을 항상 아래쪽 모양으로 그려 Header 위에 겹치고 패널 하단이 Header에 가려졌습니다(아이콘 SVG·`viewBox`·`overflow`·세로 정렬은 정상). 수정: `SWProcessSplitButton`이 뒤집힘을 쓰지 않고 항상 버튼 아래에 붙습니다(좌우 보정은 훅 그대로). 아래 공간이 부족하면 화면 끝에서 잘리지만 페이지 스크롤에 따라 위치가 갱신되어 스크롤하면 전체가 보입니다. Default·Multi 공통(같은 컴포넌트), CSS 변경 없음. - **아이콘 하단 잘림(2026-11-03, 수정):** 사용자 Chrome(배율 100%, 화면 1920 × 1080, Storybook 미리보기 1620 × 871)에서 재현. Responsive 1366 · 1100 행의 Popup 첫 열림에서 6개 틀이 21줄로 그려져 판 아래 밝은 테두리가 사라짐(아래가 잘린 것처럼 보임). 원인: 틀 상자 31.6 × 21.73(소수)이 화면 픽셀로 맞춰질 때 위치에 따라 21줄 또는 22줄이 되고, SVG 판은 크기가 고정이라 21줄일 때 아래 밝은 줄이 덮임. 패널 하단 테두리(`#926E03`)는 수정 전에도 그려져 있었음. 수정: 틀 상자를 32 × 22 정수로(그림 SVG·패널·위치 변경 없음). 수정 후 같은 Chrome에서 1366 · 1100 행, Default · Multi Step, 1920 고정 폭 Header 모두 틀 22줄 · 아래 밝은 줄 표시 확인. + +## 12. UX 검토 항목 — 외부 소비 프로젝트 요청 (2026-11-05, 정책 변경 아님) + +외부 소비 프로젝트의 개선 요청을 이 Header 고유 항목과 SW Header 공통 원칙으로 나눈 기록입니다. 공통 원칙(말줄임 원문 확인, 아이콘 버튼 이름·툴팁, class 역할, 글꼴별 폭)은 `packages/sw/ai/DESIGN_RULES.md` §15를 따릅니다. 아래 "현재 동작"은 그대로 유지하며, 정책 변경이 필요한 항목은 사용자 결정 전까지 바꾸지 않습니다. 이 절은 §8·§9의 과거 측정·검증 기록을 재판정하지 않습니다. + +| 항목 | 현재 동작(유지) | 사용자 경험상 문제 | 구분 | +|---|---|---|---| +| 단계가 많을 때 말줄임 | `multi` 1920 5그룹 11탭은 말줄임 없음, 1366에서는 11탭 중 10개 `…`(§9 측정, §11.1 개정 후 선택 탭 포함) | 짧게 잘린 단계명끼리 구분하기 어려움 | 별도 검토 | +| 단계·그룹 원문 확인 | 단계 탭 `aria-label` = 전체 이름, 그룹 이름은 `aria-describedby`로 연결. 탭·`multi` 그룹 제목에 `title` 없음(파일명·Layer 값은 `title` 있음) | 마우스·터치 사용자는 잘린 이름의 원문을 볼 수 없음 | 개선 필요(구현 별도) | +| 선택 단계 최소 폭 | 선택 탭도 다른 탭과 같은 비율로 줄어듦(§11.1, 선택 탭 우선 배정 폐기) | 선택 단계가 `…`가 되면 현재 단계를 알아보기 어려움 | 정책 결정 필요(폐기 사유인 옆 탭 24 · 27px 문제와 함께 검토) | +| 권장 화면 폭 | 보장 폭 1100px 이상. 단계 수·이름 길이별 권장 폭 기준은 없음 | 소비 프로젝트가 단계 구성에 필요한 폭을 판단하기 어려움 | 별도 검토(측정 기준 정의 필요) | +| 넘침 처리 | 그룹 접기·좌우 스크롤·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) + +2026-11-05 사용자 결정(B안): 시안 기반 아이콘 선택·창 제어 버튼 생략 규칙은 `packages/sw/ai/AI_USAGE.md` §5에 단일 기준으로 정의하고, 아래 아이콘의 공개 Export는 **보류**합니다. 소스·Props·CSS·공개 API·`public-api.approved.json`은 바꾸지 않았고 Fixture 파일도 그대로입니다. + +| 기능 | Fixture(Storybook 전용) | Figma 프레임 이름 | 상태 | +|---|---|---|---| +| 도움말 | `src/sw/assets/process-header/tool-help(-hover).svg` | help | 공개 API 아님 | +| 3D 보기 | `tool-view-3d(-hover).svg` | view-3d | 〃 | +| 설정 | `tool-controls(-hover).svg` | controls(라벨 "설정"은 Story 기준, Figma 원본 라벨 미확인) | 〃 | +| 메뉴 | `tool-menu-list(-hover).svg` | menu-list | 〃 | +| 나가기 | `tool-exit(-hover).svg` | exit-arrow | 〃 | + +- **확인된 사실:** Figma StrAna-SW(`4y8lTK1E1hAGYiMcYZwuNe`) 원본 내보내기(adbb034, §9.2). 기본 `#B6D0C9` / Hover `#FFC600` 두 장, 24 × 24, ``로 쓰면 SVG 내부 id 충돌 없음. `SWToolButton`의 `icon`·`hoverIcon`과 그대로 호환(Process Header에서는 `pressed`도 Hover 그림). +- **미확인:** 원작자·사용 권한·라이선스. 프레임 이름의 " 1" 접미사는 외부 SVG를 Figma로 가져왔을 가능성을 시사(추정). +- **Export 전제 조건:** ① 5개 아이콘의 출처·사용 권한(공개 패키지 배포 포함) 사용자 확인, ② 아래 구조와 이름의 별도 승인, ③ `public-api.approved.json` 갱신 승인. +- **제안 구조(미구현, 승인 전 문서화·사용 금지):** `swFooterToolIcons` 선례와 같은 방식. `packages/sw/src/components/sw-process-header/icons.tsx`에 `swProcessToolIcons: Record`(기능 키 `help` · `view3d` · `settings` · `menu` · `exit`, ``), SVG 10개는 같은 폴더 `assets/`로 이동(data URI 번들), `sw-process-header/index.ts`에서 Export. 사용: `도움말`. 지원 범위는 `SWProcessHeader`의 `tools`(두 appearance), `SWHeader` `tools`는 별도 검증. +- **Export 시 함께 할 일:** Story를 공개 Export로 전환하고 Fixture 중복 제거, `COMPONENT_CATALOG.md` §11 Export 추가, `AI_USAGE.md` §5.2 공개 목록 갱신·§5.3 Storybook 전용 목록에서 제외, `check:ai-docs` · `typecheck:sw` · `build:sw` · `public-api` 검사 · Tarball 소비 프로젝트 Import·Hover/Pressed 확인. diff --git a/packages/org-chart-react/ai/AI_GUIDE.md b/packages/org-chart-react/ai/AI_GUIDE.md index 927a051..68b0f36 100644 --- a/packages/org-chart-react/ai/AI_GUIDE.md +++ b/packages/org-chart-react/ai/AI_GUIDE.md @@ -141,7 +141,7 @@ | 색·Typography | DESIGN_RULES §2·§3 | DESIGN_RULES §2·§3 | VIEWER.md `SWViewerHeader`·디자인 변경 절 | DESIGN_RULES §2·§3 | | 크기·여백 | DESIGN_RULES §4 | DESIGN_RULES §1 | VIEWER.md 디자인 변경 절(52px Header 등 실측값) | 카드·간격 변경 금지(승인 디자인) | | Border·Radius·배경·그림자 | DESIGN_RULES §2·§4 | DESIGN_RULES §2·§8 | VIEWER.md `SWViewerPanel` 절(프레임은 SWPopup과 같음) | 공개 입력만 | -| 아이콘 | `lucide-react`(패키지 의존성) | 패키지 내장 SVG만, 새 아이콘은 사용자 제공 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon` | Core 내장 | +| 아이콘 | `lucide-react`(패키지 의존성) | 사용자·프로젝트 제공 에셋 → 패키지 공개 Export 중 같은 기능 → 없으면 미지원 보고(외부 라이브러리·AI 생성 SVG·Storybook 파일로 대체 금지). 시안 기반 Header 도구 아이콘·창 제어 버튼은 `@hm-design/sw` `ai/AI_USAGE.md` §5 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon`. 선택 순서는 SW 일반 모드와 같음(`AI_USAGE.md` §5) | Core 내장 | | 스크롤바 | `ScrollArea`(Radix), DESIGN_RULES §6 | 전용 디자인 없음 — 브라우저 기본(AI_USAGE §2) | 전용 디자인 없음 — 패널 본문 스크롤은 `SWViewerPanel` | Core 내부 Zoom/Pan | | Hover / Active / Focus / Disabled | DESIGN_RULES §5 | DESIGN_RULES §4 | VIEWER.md `SWViewerHeader` 절 상태 표현 | Core 내장 | | Popup·Panel | `Dialog`·`AlertDialog`·`Sheet`·`Drawer`·`Popover` | `SWPopup` + `SWPopupSection`/`SWPopupRow` | `SWViewerPanel`(+ `modal`) + `SWViewerSection`/`SWViewerField` | Core 팝업 / HM Dialog / 어댑터 | @@ -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 927a051..68b0f36 100644 --- a/packages/org-chart/ai/AI_GUIDE.md +++ b/packages/org-chart/ai/AI_GUIDE.md @@ -141,7 +141,7 @@ | 색·Typography | DESIGN_RULES §2·§3 | DESIGN_RULES §2·§3 | VIEWER.md `SWViewerHeader`·디자인 변경 절 | DESIGN_RULES §2·§3 | | 크기·여백 | DESIGN_RULES §4 | DESIGN_RULES §1 | VIEWER.md 디자인 변경 절(52px Header 등 실측값) | 카드·간격 변경 금지(승인 디자인) | | Border·Radius·배경·그림자 | DESIGN_RULES §2·§4 | DESIGN_RULES §2·§8 | VIEWER.md `SWViewerPanel` 절(프레임은 SWPopup과 같음) | 공개 입력만 | -| 아이콘 | `lucide-react`(패키지 의존성) | 패키지 내장 SVG만, 새 아이콘은 사용자 제공 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon` | Core 내장 | +| 아이콘 | `lucide-react`(패키지 의존성) | 사용자·프로젝트 제공 에셋 → 패키지 공개 Export 중 같은 기능 → 없으면 미지원 보고(외부 라이브러리·AI 생성 SVG·Storybook 파일로 대체 금지). 시안 기반 Header 도구 아이콘·창 제어 버튼은 `@hm-design/sw` `ai/AI_USAGE.md` §5 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon`. 선택 순서는 SW 일반 모드와 같음(`AI_USAGE.md` §5) | Core 내장 | | 스크롤바 | `ScrollArea`(Radix), DESIGN_RULES §6 | 전용 디자인 없음 — 브라우저 기본(AI_USAGE §2) | 전용 디자인 없음 — 패널 본문 스크롤은 `SWViewerPanel` | Core 내부 Zoom/Pan | | Hover / Active / Focus / Disabled | DESIGN_RULES §5 | DESIGN_RULES §4 | VIEWER.md `SWViewerHeader` 절 상태 표현 | Core 내장 | | Popup·Panel | `Dialog`·`AlertDialog`·`Sheet`·`Drawer`·`Popover` | `SWPopup` + `SWPopupSection`/`SWPopupRow` | `SWViewerPanel`(+ `modal`) + `SWViewerSection`/`SWViewerField` | Core 팝업 / HM Dialog / 어댑터 | @@ -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 927a051..68b0f36 100644 --- a/packages/sw/ai/AI_GUIDE.md +++ b/packages/sw/ai/AI_GUIDE.md @@ -141,7 +141,7 @@ | 색·Typography | DESIGN_RULES §2·§3 | DESIGN_RULES §2·§3 | VIEWER.md `SWViewerHeader`·디자인 변경 절 | DESIGN_RULES §2·§3 | | 크기·여백 | DESIGN_RULES §4 | DESIGN_RULES §1 | VIEWER.md 디자인 변경 절(52px Header 등 실측값) | 카드·간격 변경 금지(승인 디자인) | | Border·Radius·배경·그림자 | DESIGN_RULES §2·§4 | DESIGN_RULES §2·§8 | VIEWER.md `SWViewerPanel` 절(프레임은 SWPopup과 같음) | 공개 입력만 | -| 아이콘 | `lucide-react`(패키지 의존성) | 패키지 내장 SVG만, 새 아이콘은 사용자 제공 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon` | Core 내장 | +| 아이콘 | `lucide-react`(패키지 의존성) | 사용자·프로젝트 제공 에셋 → 패키지 공개 Export 중 같은 기능 → 없으면 미지원 보고(외부 라이브러리·AI 생성 SVG·Storybook 파일로 대체 금지). 시안 기반 Header 도구 아이콘·창 제어 버튼은 `@hm-design/sw` `ai/AI_USAGE.md` §5 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon`. 선택 순서는 SW 일반 모드와 같음(`AI_USAGE.md` §5) | Core 내장 | | 스크롤바 | `ScrollArea`(Radix), DESIGN_RULES §6 | 전용 디자인 없음 — 브라우저 기본(AI_USAGE §2) | 전용 디자인 없음 — 패널 본문 스크롤은 `SWViewerPanel` | Core 내부 Zoom/Pan | | Hover / Active / Focus / Disabled | DESIGN_RULES §5 | DESIGN_RULES §4 | VIEWER.md `SWViewerHeader` 절 상태 표현 | Core 내장 | | Popup·Panel | `Dialog`·`AlertDialog`·`Sheet`·`Drawer`·`Popover` | `SWPopup` + `SWPopupSection`/`SWPopupRow` | `SWViewerPanel`(+ `modal`) + `SWViewerSection`/`SWViewerField` | Core 팝업 / HM Dialog / 어댑터 | @@ -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 a6ebab7..8a58cca 100644 --- a/packages/sw/ai/AI_USAGE.md +++ b/packages/sw/ai/AI_USAGE.md @@ -45,7 +45,7 @@ SW 패키지에는 Header·Popup 계열이 두 벌 있습니다. 화면마다 ** | 로고(홈) 버튼 메뉴 | `SWHeader`의 `homeMenu`(`SWHomeMenu` 데이터, 최근파일 `SWRecentFiles`, Figma 26:10591). 기본 숨김 → 로고 클릭 열기 → 다시 클릭 닫기 | **사용하지 않음.** `SWViewerHeader`에는 홈 메뉴 Props가 없고 로고 클릭은 `onHomeClick`만 호출합니다. 파일 열기는 로고 오른쪽 버튼(`onDocumentOpen`·`documentOpenLabel`), 샘플 목록은 파일명 클릭 목록(`documents`·`activeDocumentId`·`onDocumentSelect`)이며 `groups` 항목으로 만들지 않습니다([VIEWER.md](./VIEWER.md) §2) | | 하단 Footer | `SWFooter`(+ 바 안 명령창 `command={}`, 좌표 `status`, `SWFooterLayoutTabs`, 부품 `SWFooter*`), [FOOTER.md](./FOOTER.md) | **사용하지 않음.** 뷰어 화면에는 하단 명령 막대가 없습니다. 도면 공간 탭은 `SWViewerHeader`의 `tabs` 항목 | | 버튼·탭·토글 | `SWToolButton` / `SWToolGroup` | Header 항목 `type: "button" \| "toggle" \| "tabs" \| "switch"`, 패널 안 `SWViewerButton` | -| 아이콘 | 패키지 자산(필드 안 아이콘) | 프로그램 아이콘 16px, 직접 그린 아이콘은 `SWViewerIcon`으로 감쌈 | +| 아이콘 | 패키지 자산(필드 안 아이콘). 시안의 도구 아이콘 선택은 §5.2 | 프로그램 아이콘 16px, 직접 그린 아이콘은 `SWViewerIcon`으로 감쌈. 시안의 도구 아이콘 선택은 §5.2 | | 범위 | Header 밖 부품은 `` 안 | `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`은 루트에 `hm-sw-theme`이 있어 `` 불필요. 패널 컨트롤은 `SWViewerPanel` 안 | | 토큰 | `--hm-sw-header-*`, Popup 콘텐츠 `--hm-sw-property-*` | `--hm-sw-viewer-*` + 공유 `--hm-sw-header-*`(아래 0.3) | | 읽을 문서 | [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §1~8·§10·§11, [DESIGN_RULES.md](./DESIGN_RULES.md) §1~10·§12·§13·§14, [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 1~6·8·10·12, [FOOTER.md](./FOOTER.md), 예제 A~G·I | [VIEWER.md](./VIEWER.md), [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §9, [DESIGN_RULES.md](./DESIGN_RULES.md) §11, [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 7, [예제 H](./examples/ExampleH-ViewerMode.tsx) | @@ -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) @@ -148,3 +149,124 @@ SW 토큰(`--hm-sw-header-*`, `--hm-sw-property-*`, `--hm-sw-viewer-*`, `--hm-sw | 예: 뷰어 측정 패널 | 신규 콘텐츠(소비 프로젝트) | `SWViewerPanel` children에 `SWViewerField` + `SWViewerInput`. 토큰에 없는 값은 "AI 구성 값 — 디자인 확인 필요" | 새 컴포넌트를 SW 패키지 공식 컴포넌트로 승격할지는 별도 검토 대상이다. 임의로 패키지에 추가하지 않는다. + +## 5. 디자인 시안 기반 Header 구현 — 도구 아이콘·창 제어 버튼 판단 (2026-11-05) + +화면 이미지·Figma·캡처 같은 **디자인 시안**을 보고 SW Header(`SWHeader`·`SWProcessHeader`·`SWViewerHeader`)의 도구 버튼과 창 제어 버튼을 구현할 때 따르는 **단일 기준**입니다. 다른 문서는 이 절을 요약·참조만 합니다. 크기·색·상태 값은 [DESIGN_RULES.md](./DESIGN_RULES.md)(§9·§14, 뷰어는 [VIEWER.md](./VIEWER.md))를 따르며 이 절에서 다시 정하지 않습니다. + +### 5.1 컴포넌트 기본 구성과 시안 재현은 다릅니다 + +| 구분 | 뜻 | 바꾸는 방법 | +|---|---|---| +| 컴포넌트 기본 구성 | Props를 생략했을 때 컴포넌트가 그리는 구성(예: `SWProcessHeader`는 `hiddenControls`가 없으면 창 이동·최소화·최대화/복원·종료를 모두 그림). 기존 소비 프로젝트가 의존하는 패키지 기본값 | 바꾸지 않습니다. 패키지 기본값·CSS를 고치거나 `hm-sw-*` class로 숨기지 않습니다 | +| 시안 재현 | 시안에 **보이는** 구성만 화면에 두는 것 | 공개 Props(`hiddenControls`·`tools`·`trailing` 등)로만 맞춥니다 | + +- 카탈로그 예시·예제 파일·Storybook에 어떤 버튼이나 아이콘이 있다는 사실은 시안에 그것을 만들 근거가 아닙니다. +- **기존 화면 작업**(기존 화면을 HMDesign으로 옮기기, 이미 HMDesign을 쓰는 화면에 새 시안 적용)에는 [AI_GUIDE.md](./AI_GUIDE.md) 4단계 "기존 화면 전환 시 원본 보존"·"디자인 적용 시 변경 범위 제한"이 우선합니다. 원본에 있는 버튼·아이콘·창 제어 기능을 "시안에 없다"는 이유만으로 지우거나 `hiddenControls`를 더해 숨기지 않고, 원본에 없는 창 제어 버튼을 시안만 근거로 더하지 않습니다. 시안과 원본이 다르면 결과 보고의 "원본 대비 변경"에 확인 대상으로 적습니다. §5.5의 생략·추가 규칙은 신규 화면 기준입니다. +- 사용자가 창 제어·도구 버튼 구성 변경을 **명시적으로 요청**했으면 요청한 버튼에 한해 공개 Props로 바꾸고, "원본 대비 변경"의 근거를 "사용자 승인"으로 적습니다. 요청 범위 밖의 버튼은 그대로 둡니다. + +### 5.2 도구 아이콘 선택 우선순위 + +| 순위 | 조건 | 할 일 | +|---|---|---| +| 1 | 사용자가 이번 작업에 제공했거나 사용을 승인한 이미지·SVG, 또는 소비 프로젝트에 이미 등록된 공식 아이콘이 있음 | 그 에셋을 씁니다. 상태별 그림이 있으면 기본은 `icon`, Hover 그림은 `hoverIcon`에 넣습니다(§5.4) | +| 2 | 제공된 에셋이 없고, `@hm-design/sw`가 **공개 Export**하는 아이콘·부품 중 **기능과 의미가 같은 것**이 있음 | 그것을 재사용합니다. 시안과 모양이 조금 달라도 기능이 같고 그 아이콘의 사용 위치·크기·상태 규칙 안이면 HMDesign 것을 우선합니다 | +| 3 | 1·2 모두 없음 | 대체하지 않고 **미지원으로 보고**합니다(아래) | + +- **시안 속 아이콘의 사용 권한:** Figma 등 시안에 그려져 있다는 사실만으로 사용 권한이 있다고 보지 않습니다. 시안에서 아이콘을 추출해 쓰려면 사용자가 그 에셋을 제공했거나 사용을 명시적으로 승인했는지 먼저 확인하고, 확인 전에는 1순위로 쓰지 않습니다(2·3순위로 판단). 이 승인은 해당 소비 프로젝트에서의 사용에만 해당하며, HMDesign 패키지 공식 등록·공개 배포 권한은 별도로 확인합니다(§5.3). + +**2순위 — 현재 공개된 아이콘·아이콘 포함 부품**(목록 밖 이름은 [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md)와 `dist/index.d.ts`에서 확인): + +| 공개 Export | 기능 | 사용 위치 | +|---|---|---| +| `SWHeader` 기본 부품(인쇄 `print`, K 가이드 `popupGuide`, 창 이동·최소화·종료) | 인쇄, 단축키 안내, 창 제어 | `SWHeader` 안(`hiddenControls`로 개별 제거). 아이콘만 꺼내 쓰는 API는 없습니다 | +| `SWHeaderActions` | 인쇄·K 가이드 버튼 pill(`onPrint`·`onPopupGuide`·`showPrint`·`showPopupGuide`·`disabled`) | `` 안 SW 일반 모드 조합 Header([COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 2). 컴포넌트 단위로만 재사용합니다 — 인쇄·K 아이콘 단독 Export는 없어 `SWToolButton`의 `icon`에 넣을 수 없습니다. `SWHeader`(이미 내장)·`SWProcessHeader` `tools`·뷰어 화면([VIEWER.md](./VIEWER.md) §2.1)에는 넣지 않습니다(검증된 조합 아님) | +| `SWHeaderWindowControls` | 창 이동·최소화·종료 | SW 일반 모드 조합 Header, 뷰어 `trailing` 끝(최소화·닫기만, `showWindowMove={false}`, VIEWER.md §2.1) | +| `SWProcessSplitButton` | 화면 분할 | `SWProcessHeader`의 `tools` | +| `SWLayerVisibilityIcon` | 레이어 표시·숨김 전구 | 뷰어 레이어 Popup(VIEWER.md §4.1) | +| `swFooterToolIcons` · `swFooterAuxIcons` | Footer 명령(Line·Circle …)·보조기능(객체스냅 …) | `SWFooter` 칸([FOOTER.md](./FOOTER.md)). 크기·상태 색이 Header 도구 버튼과 달라 Header pill에는 쓰지 않습니다 | + +- 모양이 비슷하다는 이유만으로 기능이 다른 아이콘을 쓰지 않습니다(예: `swFooterToolIcons.rect`를 최대화 아이콘으로 쓰기 금지). +- `SWViewerIcon`은 외곽선만 더하는 감싸기 부품이며 그림을 제공하지 않습니다. +- **Header 도구 버튼용 범용 아이콘(도움말·3D 보기·설정·메뉴·나가기 등)은 현재 공개 Export가 없습니다.** 이런 아이콘이 시안에 있고 1순위 에셋도 없으면 3순위입니다. + +**3순위 — 공식 아이콘이 없을 때:** + +1. 외부 아이콘 라이브러리(`lucide-react` 등 — Web 패키지 의존성이지만 SW 화면에 쓰지 않음), AI가 그린 SVG, Storybook 파일 복사로 대신하지 않습니다. +2. 코드를 쓰기 전에 필요한 아이콘·기능·필요한 상태(기본·Hover 등)를 사용자에게 알리고 에셋 제공 또는 HMDesign 공식 아이콘 등록이 필요하다고 요청합니다. +3. 답을 기다리지 않고 진행해야 하면 그 도구 버튼을 넣지 않거나, 사용자가 승인한 임시 표시만 쓰고 결과 보고에 "미지원 아이콘 — 에셋 필요"로 적습니다. 승인 없이 임시 아이콘을 최종 디자인으로 확정하지 않습니다. + +### 5.3 Storybook 전용 에셋과 공개 에셋의 차이 + +| 구분 | 공개 에셋·API | Storybook 전용 에셋(Fixture) | +|---|---|---| +| 위치 | `@hm-design/sw` 진입점 Export(카탈로그에 이름이 있음). SVG는 JS에 data URI로 포함 | HMDesign 저장소의 Story 데모 파일. 설치 패키지(`dist`·`ai`)에 포함되지 않음 | +| 보장 | 버전 호환·사용 규칙 문서화 대상 | 데모용. 출처·사용 권한·유지를 보장하지 않음 | +| 소비 프로젝트 | Import해서 사용 | Import·복사·경로 참조 금지(Git Submodule로 저장소 파일이 보여도 같음) | + +- Storybook 화면에 보인다는 것은 공개 API라는 뜻이 아닙니다. Storybook은 조합·외형 참고용입니다([AI_GUIDE.md](./AI_GUIDE.md) 4단계 "Git Submodule 사용 규칙"). +- **2026-11-05 현재 Storybook 전용(공개 API 아님):** `SW / Step Header` Story의 도구 아이콘 — Multi Step의 도움말·3D 보기·설정·메뉴·나가기, Default의 View Property·Sub 창. 출처·사용 권한 확인 전이라 패키지에 등록하지 않았습니다. 시안에 이 아이콘이 있으면 1순위(사용자·프로젝트 제공)로 받거나 3순위로 보고합니다. + +### 5.4 아이콘 상태 처리 — 기존 공개 API 방식 그대로 + +- `SWToolButton appearance="pill"`의 `icon`(기본 그림)과 `hoverIcon`(상태 그림)만 씁니다. Disabled 전용 아이콘 Prop은 없습니다. 상태별 현재 동작(패키지 CSS 기준)은 버튼이 놓인 곳에 따라 다릅니다. + +| 상태 | `SWProcessHeader`의 `tools` 안 | 그 밖의 `pill`(`SWHeader`의 `tools`, `` 조합) | +|---|---|---| +| Default | `icon`. `currentColor` = `#B6D0C9`(`--hm-sw-process-tool-icon-color`) | `icon`. `currentColor` = 상속 글자색 `--hm-sw-header-text-color`(`#F6F4F2`), `#B6D0C9` 아님 | +| Hover | 노란 링 + `currentColor` `#FFC600` + `hoverIcon`이 있으면 그 그림 | 노란 링 + `hoverIcon`이 있으면 그 그림. `currentColor` 색은 바뀌지 않음 | +| Pressed(`pressed` Prop, `aria-pressed`) | Hover와 같은 모양(노란 링 + `#FFC600` + `hoverIcon`) | `currentColor`만 `#FFC600`. 노란 링·`hoverIcon` 전환 없음 | +| Disabled | Hover 반응 없음, 기본 그림 유지, 투명도 0.4 | 같음 | + +- 누르는 순간(`:active`)의 별도 모양은 없습니다. 위 Pressed는 `pressed` Prop 상태입니다. +- 색이 고정된 이미지는 기본·Hover 두 장을 줍니다. Hover 그림이 제공되지 않으면 `hoverIcon`을 생략하며(Hover에도 기본 그림 유지), 제공된 그림의 색을 바꿔 Hover 그림을 만들지 않습니다. `currentColor` 한 장으로 상태 색을 따르는 방식은 `SWProcessHeader`의 `tools`에서만 `#B6D0C9` → `#FFC600`이 됩니다. 그 밖의 `pill`에서 시안 색이 인쇄 버튼처럼 `#B6D0C9` → `#FFC600`이면 기본·Hover 두 장 이미지를 씁니다(값은 [DESIGN_RULES.md](./DESIGN_RULES.md) §9·§14). +- 위 표와 다른 상태 모양이 시안에 있으면 CSS로 맞추지 않고 지원 범위 밖으로 보고합니다. +- 그림은 장식이므로 ``로 두고 버튼 이름은 `children`(문자열이면 기본 툴팁, DESIGN_RULES §15.2)으로 줍니다. +- 상태를 맞추려고 `hm-sw-*` class나 패키지 CSS를 덮지 않습니다. + +### 5.5 창 제어 버튼(창 이동·최소화·최대화·닫기) + +1. 시안에 **명확히 보이는** 창 제어 버튼만 구현합니다(웹 화면은 4번, 기존 화면은 §5.1이 우선). +2. 시안에 없고 별도 요구도 없으면 만들지 않는 것이 기본입니다. 컴포넌트 기본 구성(§5.1)에 있어도 공개 숨김 API로 뺍니다(신규 화면 기준, 기존 화면은 §5.1). +3. 시안의 오른쪽이 잘렸거나 일부가 보이지 않으면 보이지 않는 버튼의 존재를 확정하지 않습니다. 코드를 쓰기 전에 묻고, 답 없이 진행해야 하면 보이는 범위만 구현한 뒤 결과 보고에 "확인 필요(시안 잘림)"로 적습니다. +4. 브라우저에서 쓰는 웹 화면에서는 운영체제 창 제어 버튼을 새로 만들지 않는 것이 기본입니다. 시안에 창 제어 버튼이 보여도 실제로 연결할 창 동작(최소화·최대화·닫기를 처리하는 셸·Callback 요구)이 확인되지 않으면 임의로 구현하지 않습니다. 코드를 쓰기 전에 묻고, 답 없이 진행해야 하면 신규 창 제어 버튼 생성을 보류하고(아래 공개 숨김 API로 생략) 결과 보고에 "보류 — 창 동작 요구 확인 필요"로 적습니다. 빈 Callback·임의 `window.close()` 연결 같은 가짜 동작을 만들지 않습니다. 기존 화면에 이미 있는 창 제어 기능은 이 규칙을 이유로 별도 승인 없이 제거하지 않습니다(§5.1). +5. 데스크톱 프로그램(설치형·WebView 셸 등)처럼 창 제어가 필수인 환경은 플랫폼 요구와 사용자 명세를 우선합니다. 시안에 없는데 플랫폼상 필요해 보이면 임의로 더하지 않고 확인합니다. 실제 창 이동·최소화·최대화·종료는 프로그램 담당입니다(Callback 연결). +6. 숨김·생략은 아래 공개 API로만 합니다. 적절한 공개 API가 없으면 CSS로 감추는 우회 대신 지원 여부와 제약을 보고합니다. + +| Header | 기본 구성 | 시안에 없을 때(공개 API) | +|---|---|---| +| `SWProcessHeader` | 창 이동·최소화·최대화/복원·종료 표시 | `hiddenControls`에 `"windowMove"`·`"minimize"`·`"maximize"`·`"close"` 중 없는 것. 넷 다 숨기고 `tools`도 없으면 창 영역 자체가 렌더링되지 않음 | +| `SWHeader` | 창 이동·최소화·종료 표시 | `hiddenControls`에 `"windowMove"`·`"minimize"`·`"close"` 중 없는 것 | +| `SWViewerHeader` | 창 제어 없음 | `trailing`에 `SWHeaderWindowControls`를 넣지 않음. 시안에 최소화·닫기가 있으면 `trailing` 맨 끝에 `showWindowMove={false}`로만 둠(VIEWER.md §2.1 승인 구성). 창 이동은 뷰어에서 지원하지 않으므로 시안에 있어도 넣지 않고 미지원으로 보고 | +| `` 안 조합 Header | — | `SWHeaderWindowControls`를 두지 않거나 `showWindowMove`·`showMinimize`·`showClose`로 일부만 표시 | + +### 5.6 공식 구현 예시 (공개 API만) + +```tsx +import { SWHeader, SWProcessHeader, SWToolButton } from "@hm-design/sw" +// 1순위 에셋: 사용자·소비 프로젝트가 제공한 파일(HMDesign 패키지·Storybook 파일이 아님) +import helpIcon from "./assets/help.svg" +import helpIconHover from "./assets/help-hover.svg" + +// 시안: 창 제어 버튼 없음, 도구는 도움말 하나 + setStep(s.id) }} + tools={} hoverIcon={} onClick={openHelp}>도움말} + hiddenControls={["windowMove", "minimize", "maximize", "close"]} /> + +// 시안: 최소화·닫기만 있음(창 이동·최대화 없음) + + +// 시안: 완성형 SWHeader에 창 제어 버튼 없음 + +``` + +### 5.7 결과 보고에 남길 것 + +§4 표에 다음 판단을 함께 적습니다. + +| 요청 항목 | 판단 | 근거 | +|---|---|---| +| 예: 도움말 아이콘 | 1순위 사용 / 2순위 재사용 / 3순위 미지원 | 사용자 제공 파일명 / 재사용한 공개 Export 이름 / "공개 아이콘 없음 — 에셋 제공 또는 공식 등록 필요(기능·필요 상태)" | +| 예: 창 제어 버튼 | 구현 / 생략 / 보류 / 원본 유지 / 확인 필요 | 시안에 보인 버튼 목록, 사용한 `hiddenControls` 값, 시안 잘림·웹/데스크톱·플랫폼 요구 여부, 기존 화면이면 원본 구성과 사용자 승인 여부 | diff --git a/packages/sw/ai/COMPONENT_CATALOG.md b/packages/sw/ai/COMPONENT_CATALOG.md index 5dc9f63..41e9140 100644 --- a/packages/sw/ai/COMPONENT_CATALOG.md +++ b/packages/sw/ai/COMPONENT_CATALOG.md @@ -23,7 +23,7 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체 | 필수 | `documentTitle: string` | | 구성 | `hiddenControls?: SWHeaderHideableControlId[]`(로고 외 부품 제거, `home` 제거 시 정적 로고. 하위 항목 `documentNew`·`documentOpen`·`layerFreeze`·`layerLock`도 제거 — 레이아웃 변화 없음), `menuOrder?: SWHeaderMenuId[]`(5개 메뉴끼리 순서), `controls?: SWHeaderControlConfig[]`(객체 영역 전체 allowlist·순서·label) | | 로고 오른쪽 | `documentOpenPlacement?: "menu" \| "header"`(기본 `menu` = 기존 Dropdown 하단. `header` = 로고 바로 오른쪽에 파일명과 같은 면의 Open 버튼, Dropdown의 Open은 숨김. 둘 다 `onDocumentOpen`·`documentOpenLabel`), `leading?: ReactNode`(Open 뒤·파일명 앞 추가 부품, 예: `SWToolButton appearance="document"`) | -| 로고 | `logo?: ReactNode`, `logoSrc?: string`, `logoAlt?: string`, `onHomeClick`(`homeMenu`가 없을 때만 호출). 로고는 높이 17px·원본 비율이고 홈과 사선 배경이 로고 폭을 따라 늘거나 줍니다. 로고가 없어도 홈과 사선 배경, `onHomeClick`·`homeMenu` 동작은 그대로입니다(빈 로고 자리 32px을 유지해 80 × 48, 2026-10-23). `SWViewerHeader`는 로고가 없으면 로고 면 전체를 그리지 않습니다(이때 `onHomeClick`·`logoMenu`는 누를 곳이 없음) | +| 로고 | `logo?: ReactNode`, `logoSrc?: string`, `logoAlt?: string`, `onHomeClick`(`homeMenu`가 없을 때만 호출). 로고는 높이 17px·원본 비율이고 홈과 사선 배경이 로고 폭을 따라 늘거나 줍니다(고정 폭 없음, 긴 로고도 잘리지 않음). 글자 로고는 `logo`에 요소로 전달하고 한 줄·`line-height: 17px`로 맞춥니다(공통 원칙 [DESIGN_RULES.md](./DESIGN_RULES.md) §1). 로고가 없어도 홈과 사선 배경, `onHomeClick`·`homeMenu` 동작은 그대로입니다(빈 로고 자리 32px을 유지해 80 × 48, 2026-10-23). `SWViewerHeader`는 로고가 없으면 로고 면 전체를 그리지 않습니다(이때 `onHomeClick`·`logoMenu`는 누를 곳이 없음) | | 로고 클릭 메뉴 | `homeMenu?: SWHeaderHomeMenu`(= `SWHomeMenu` Props에서 열림 상태 제외, 5절), `homeMenuOpen`, `defaultHomeMenuOpen`(기본 false), `onHomeMenuOpenChange(open, reason: SWHeaderHomeMenuChangeReason)` | | 파일 | `documents`, `activeDocumentId`, `documentMenuOpen`, `onDocumentMenuOpenChange`, `onDocumentSelect`, `onDocumentClose`, `onDocumentNew`, `onDocumentOpen`, `onDocumentClick`, `documentIcon`, `documentNewLabel`, `documentOpenLabel` | | 레이어 | `layer`, `layers`, `activeLayerId`, `layerMenuOpen`, `onLayerMenuOpenChange`, `onLayerSelect`, `onLayerVisibilityChange`, `onLayerFreezeChange`, `onLayerLockChange`, `onLayerDetail`, `onLayerOpen` | @@ -89,10 +89,13 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체 | 컴포넌트 | 용도 | 주요 Props | 모양(기존 기준) | 검증 | |---|---|---|---|---| -| `SWToolButton` | 버튼·토글 | `appearance`(`text` 기본 / `pill` / `document`), `pressed`(토글·선택), `children`(글자. `pill`은 접근성 이름으로만 사용), `icon`·`hoverIcon`(`pill`), `button` 속성 | `text` = 속성Bar·속성창 글자 토글(700, Hover·선택 #FFC600, 행 19px). `pill` = 인쇄·K 40 × 38(노란 Hover 링, 아이콘 24 × 23 영역, `hoverIcon` 생략 시 Hover에도 기본 아이콘 유지). `document` = 파일명 면(48px, 글자만 Hover #FFC600). Home 바로 뒤에 두면 로고 사선 접점을 그대로 공유 | ✅ | +| `SWToolButton` | 버튼·토글 | `appearance`(`text` 기본 / `pill` / `document`), `pressed`(토글·선택), `children`(글자. `pill`은 화면에 보이지 않는 접근성 이름이며 문자열이면 기본 `title`), `icon`·`hoverIcon`(`pill`), `title`(명시 시 우선, `""` = 툴팁 없음), `button` 속성 | `text` = 속성Bar·속성창 글자 토글(700, Hover·선택 #FFC600, 행 19px). `pill` = 인쇄·K 40 × 38(노란 Hover 링, 아이콘 24 × 23 영역, `hoverIcon` 생략 시 Hover에도 기본 아이콘 유지). `document` = 파일명 면(48px, 글자만 Hover #FFC600). Home 바로 뒤에 두면 로고 사선 접점을 그대로 공유 | ✅ | | `SWToolGroup` | 버튼 묶음 | `appearance`(`text` 기본 = 상세보기 묶음, `pill` = 인쇄·K pill), `label`(`text`의 12px 캡션·접근성 이름), `selection`(`single` = radiogroup, 화살표·Home·End로 이동·선택), `div` 속성 | 버튼 사이 1px 구분선(`text`), 내용 폭 pill | ✅ | - 탭(Model / Layout)은 별도 컴포넌트 없이 `SWToolGroup selection="single"` + `SWToolButton pressed`입니다. +- `pill`은 공백이 아닌 문자열 `children`을 기본 툴팁(`title`)으로 씁니다. 명시한 `title`이 우선하고 `title=""`이면 툴팁이 없습니다. 숫자·JSX `children`은 변환하지 않으므로 필요하면 `title`을 전달합니다. `text`·`document`는 전달한 `title`만 사용합니다([DESIGN_RULES.md](./DESIGN_RULES.md) §15.2). +- `pill`의 `icon`·`hoverIcon`에 넣을 아이콘은 [AI_USAGE.md](./AI_USAGE.md) §5.2 순서로 고릅니다(패키지는 Header 도구용 범용 아이콘을 Export하지 않음). +- `pill` 버튼은 SWHeader 인쇄 버튼과 같은 `hm-sw-header__action--print` class를 가집니다. 이 class로 인쇄 버튼을 찾거나 덮지 않습니다([DESIGN_RULES.md](./DESIGN_RULES.md) §15.3). - 일반 모드의 상태 표시(로드 상태 등) 전용 SW 디자인은 아직 없습니다(디자인 확정 후 검토). 뷰어 모드는 `SWViewerStatus`(§9)가 있으며 일반 모드 Header에 가져오지 않습니다. ## 5. 로고 클릭 메뉴 — Figma 2단 Header 메뉴 (SW 일반 모드 전용) @@ -188,16 +191,18 @@ 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, 두 타입 공통. 글자 로고는 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`(기본 `화면 분할`), `typeLabels`, `disabled`. 키보드: Enter·Space 열기/닫기, ArrowDown 열고 선택 칸으로, 방향키·Home·End 선택, Escape 닫고 버튼으로, Tab 이탈·바깥 클릭 닫기. 실제 창 분할은 호스트 | 버튼 40 × 38(`multi` 37), 패널 233 × 36, 칸 38.5 × 34 | ◻ | +| `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). 그룹은 자연 폭까지·좁으면 비례 축소 | ◻ | - **`default` 확정 디자인은 2그룹**(01 Modeling: Structure · Loads / 02 Analysis Result: Results)입니다. 1그룹·3그룹 이상은 렌더링되지만 디자인 미확정입니다. - **사다리꼴(그룹 면) 배치 확정(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 Popup, 실제 화면 분할 실행, 도구 아이콘(소비 측이 `tools`에 넣는 요소). 화면 분할 선택 UI는 `SWProcessSplitButton`(2026-11-03 추가). `SWHomeMenu`는 이 Header에 쓰지 않습니다(2단 타일 디자인이 다름). +- **원문 확인(현재 구현):** 파일명·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단 타일 디자인이 다름). - **SWHeader와 같은 상호작용(2026-11-03):** 창 이동은 `SWHeaderWindowControls`의 창 이동(Hover 1.4s Motion·툴팁)을 그대로 씁니다. 글자 외곽선·Hover·Focus도 SWHeader 규칙(DESIGN_RULES §14). 도구 아이콘이 이미지면 `SWToolButton`의 `hoverIcon`으로 노란 그림(`#B6D0C9 → #FFC600`)을 함께 주고, 단색 SVG면 `currentColor`로 칠합니다. - **단독 사용:** `SWProcessBar`만 쓸 때는 `` 안에 둡니다. `SWProcessHeader`는 루트에 `hm-sw-theme`이 있어 ``이 필요 없습니다. - **공개 타입:** `SWProcessHeaderProps`, `SWProcessHeaderLayer`, `SWProcessHeaderHideableControlId`, `SWProcessBarProps`, `SWProcessGroup`, `SWProcessStep`, `SWProcessAppearance`(`"default" | "multi"`), `SWProcessSplitType`(`single` · `columns` · `rows` · `topWide` · `leftTall` · `grid` = Strana Type01~06), `SWProcessSplitButtonProps`. diff --git a/packages/sw/ai/COMPOSITION_PATTERNS.md b/packages/sw/ai/COMPOSITION_PATTERNS.md index 8ad81e1..c5cd2ca 100644 --- a/packages/sw/ai/COMPOSITION_PATTERNS.md +++ b/packages/sw/ai/COMPOSITION_PATTERNS.md @@ -273,14 +273,16 @@ const groups: SWProcessGroup[] = [ onMinimize={minimize} onMaximizeToggle={toggleMaximize} onClose={closeWindow} /> ``` -1. 도구 아이콘은 프로그램이 그린 24 × 24 요소입니다. 버튼 상태 색(기본 `#B6D0C9`, Hover·눌림 `#FFC600`)을 따르려면 `currentColor`로 칠하고, 색이 고정된 이미지 아이콘이면 `hoverIcon`에 `#FFC600` 그림을 함께 줍니다(SW 인쇄 버튼과 같은 방식). +1. 도구 아이콘은 프로그램이 그린 24 × 24 요소입니다. 버튼 상태 색(기본 `#B6D0C9`, Hover·눌림 `#FFC600`)을 따르려면 `currentColor`로 칠하고, 색이 고정된 이미지 아이콘이면 `hoverIcon`에 `#FFC600` 그림을 함께 줍니다(SW 인쇄 버튼과 같은 방식). 시안의 아이콘을 고를 때는 [AI_USAGE.md](./AI_USAGE.md) §5.2 순서(제공 에셋 → 공개 Export → 미지원 보고)를 따릅니다. Storybook `SW / Step Header`의 도구 아이콘은 공개 API가 아닙니다(§5.3). 2. 화면 분할은 `SWProcessSplitButton`을 `tools`의 원하는 자리에 둡니다(두 appearance 공통). 고른 값으로 실제 창을 나누는 일은 프로그램이 합니다. 분할 아이콘·선택 패널을 직접 그리지 않습니다. 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 +// helpIcon / helpIconHover: 사용자·소비 프로젝트가 제공한 에셋(패키지에 도움말 아이콘 Export 없음, AI_USAGE §5.2) `(또는 `SWHeader`의 `tools`) 안에서 렌더링한다. @@ -221,8 +225,9 @@ | 항목 | 값 | 토큰 | |---|---|---| -| 배치 | 홈·파일명 글자 = SWHeader 그대로(두 타입 공통, 2026-11-03): 홈 12 + 로고(32 × 17 → 80) + 36, 파일명은 홈과 15 겹친 뒤 2 간격(x 65), padding 5 / 12, 14px 700·자간 0.06em·SW 외곽선, 화살표 없음(글자 로고 18px 700 → 홈 106) → 프로세스 → Layer 면(객체 면, 양옆 1) → 창 영역(파일명 면 + 테두리 1px) | SW 토큰 | -| 프로세스 바탕 | 세로 `#0C5941 → #19BF8D 10.1% → #109069 18.27% → #0C5941`, 파일명 테두리 1px, 위 모서리 6 | `--hm-sw-process-base-background`, `-base-border-gradient` | +| 배치 | 홈·파일명 글자 = 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 토큰 | +| 로고 폭(Process 고유, 2026-11-05) | 공통 원칙은 §1 "로고 확장 공통 원칙". Process 고유: `logoText`만 최소 58px 칸(→ 홈 최소 106), 넓어진 홈만큼 파일명(최소 100)·가운데 영역이 flex 비율로 줄어듦. 로고 변경 시 `default`·`multi`를 각각 확인하고 단계 말줄임 정책은 그대로 둡니다 | SW 토큰 | +| 프로세스 바탕 | 세로 `#0C5941 → #19BF8D 10.1% → #109069 18.27% → #0C5941`, 파일명 테두리 1px, 위 모서리 6. `multi`는 양 끝 직각(2026-11-05 — 단계 탭 모양과 별개) | `--hm-sw-process-base-background`, `-base-border-gradient` | | 비활성 덮개 | `rgba(0, 68, 48, .902)`: 비활성 그룹, 화살표 꼭지(앞 그룹 비활성)·홈(뒤 그룹 비활성), 비활성 바깥 그룹 쪽 남는 폭 | `--hm-sw-process-dim` | | 그룹 글자 | 번호 12px 500 `#69C2A6`, 이름 12px(최대 56, 줄바꿈) 활성 700 `#FFF` / 비활성 500 `#C3EBDF`, 위 10.67(한 줄)·8.67(두 줄), 그룹 좌우 20, 이름 뒤 12 | `--hm-sw-process-number-color`, `-group-color`, `-group-active-color` | | 활성 그룹 면 | 기운 변 도형, 3층 면(85·80·45%), 발광 `drop-shadow(0 0 30px rgba(88,181,153,.85))`, 가장자리 빛 3px Blur 5, 화살표 쪽 76px에 걸쳐 사라짐. 첫 그룹 왼쪽 40px 밖, 마지막 그룹 좌우 반전·44px 밖 | `--hm-sw-process-glow-*` | @@ -255,3 +260,44 @@ | 탭 | 높이 25, 글자 폭 + 25(padding 13 / 12), 14px 700, 간격 0. 모서리: 단독 50, 첫 50 0 0 50, 마지막 0 50 50 0, 가운데 0. 좁으면 선택 탭 포함 비례 축소 + `…`. 화살표 칸·그룹 사이 36 고정. 그룹 접기·좌우 스크롤·More 없음(넘침 정책 확정 2026-11-03) | — | | 탭 상태 | 기본(미진행) 70% `rgba(52,143,114,.8) → rgba(2,50,35,.8)` · `#0C3125`. 밝음(켜진·진행 완료) 100% `rgba(68,178,143,.8) → rgba(0,61,41,.8)` · `#0A5940`. 선택 `#113228 → #28735C`(아래 → 위) · `#001E15` · 안쪽 그림자, 글자 `#FFC600`. Disabled 50% 속성 필드 면 · `#021912`, 글자 `#7D9F91`. 공통 그림자 `1px 1px 1px rgba(31,148,113,.7)`. Hover 글자 `#FFC600`, Focus 2px `#F0F7F5` | `--hm-sw-process-multi-tab-*`, `--hm-sw-header-property-background` | - 새 프로세스 UI는 위 토큰을 `var(--…)`로 읽고 `hm-sw-process-*` class를 쓰거나 덮어쓰지 않습니다. + +## 15. Header 공통 UX·레이아웃 원칙 (`SWHeader` · `SWViewerHeader` · `SWProcessHeader`, 2026-11-05) + +세 Header와 그 안의 SW 도구 버튼에 공통으로 적용합니다. 크기·반응형·말줄임 시점 같은 Header별 값은 각 절(§7 SWHeader, §11·[VIEWER.md](./VIEWER.md) 뷰어, §14 Process)과 디자인 명세를 따르며 이 절은 그 정책을 바꾸지 않습니다. "현재 구현"은 소스 기준 사실이고, "개선 필요"는 아직 구현되지 않은 권고입니다(구현 완료가 아님). + +### 15.1 말줄임 텍스트의 원문 확인 + +- 말줄임 때문에 전체 내용을 알 수 없는 텍스트(파일명·단계명·그룹명·값 등)에는 원문을 확인할 수단을 둡니다. 기본 수단은 그 요소나 버튼의 `title`이고, SW 툴팁(창 이동·K 가이드 등)은 해당 컴포넌트에 이미 있는 경우에만 씁니다. 모든 텍스트에 툴팁을 일괄로 붙이지 않습니다. +- 기존 접근성 이름(`aria-label`, 연결된 label)은 원문 그대로 유지합니다. 툴팁은 접근성 이름을 대신하지 않습니다. +- `title`은 마우스 Hover에서만 보이고 키보드 Focus·터치에서는 표시되지 않습니다. 키보드·터치 사용자에게도 원문이 필요한 곳은 다른 확인 경로(목록·More·Popup 등)나 Focus 표시를 컴포넌트별로 검토합니다. 현재 공통 구현은 없습니다. +- 잘렸을 때만 `title`을 붙이는 방식은 `SWFooterToolButton`에 선례가 있습니다(Hover·Focus 때 실제 폭을 측정하고, 전달된 `title`이 없을 때만 적용). 적용 여부는 컴포넌트별로 판단합니다. + +| Header | 현재 구현: 원문 `title` 있음 | 개선 필요: 말줄임되지만 `title` 없음(접근성 이름은 원문) | +|---|---|---| +| `SWHeader` | 파일명, 문서 목록 항목(`title ?? name`), 필드 값, 문자스타일 글꼴명, 레이어 목록 이름, 속성Bar 객체명 | 메뉴 타이틀(필드 12px 라벨, 버튼 `aria-label`) | +| `SWViewerHeader` | 파일명(`documentTooltip`, 기본 이름), 상태 표시(`detail`), 아이콘만 남은 버튼·토글(`title ?? label`) | Model / Layout 탭 이름(`option.title`을 줄 때만), 아이콘만 남은 `menu` 항목(`item.title`을 줄 때만), 말줄임된 일반 항목 라벨 | +| `SWProcessHeader` | 파일명, Layer 값(`layer.value`) | 단계 탭 이름, `multi` 그룹 제목(그룹 이름은 `aria-describedby`로 연결) | + +### 15.2 아이콘 전용 도구 버튼 + +- 아이콘만 보이는 버튼은 의미를 식별할 수 있는 접근성 이름이 있어야 합니다. `SWToolButton appearance="pill"`은 `children`을 시각적으로 숨긴 글자(`hm-sw-tool__label`)로 그려 접근성 이름으로 씁니다(또는 `aria-label`). +- **현재 구현(`SWToolButton` 자동 툴팁, 2026-11-05):** `appearance="pill"`만 적용합니다. 우선순위는 ① 명시한 `title`(`title=""`이면 툴팁 없음 — 공식 비활성화 방법) → ② 공백이 아닌 문자열 `children`을 원문 그대로 `title`로 사용 → ③ 툴팁 없음입니다. 빈 문자열·공백만 있는 문자열·숫자·JSX·배열(`{label} 보기`처럼 값을 이어 쓴 JSX 포함) `children`은 문자열로 변환하지 않아 툴팁이 생기지 않으므로, 이때는 템플릿 문자열(`` {`${label} 보기`} ``)이나 `title`을 씁니다. `text`·`document`는 전달한 `title`만 그대로 붙입니다(자동 툴팁 없음). 접근성 이름(`children`·`aria-label`·`aria-labelledby`)과 클릭·키보드 동작, DOM·class는 그대로입니다. `aria-label`과 `children`이 다르면 툴팁은 `children`, 접근성 이름은 `aria-label`입니다. +- `SWProcessSplitButton`은 `label`(기본 `화면 분할`)을 `children`과 `title`에 함께 전달합니다. SWHeader의 인쇄·창 버튼과 Process 창 버튼은 `SWToolButton`이 아니며 `aria-label`만 있고 `title`이 없습니다(창 이동은 SW 툴팁). +- Hover 툴팁은 보조 수단이며 접근성 이름·Focus 표시·키보드 동작을 대신하지 않습니다. 자동 툴팁도 네이티브 `title`이라 키보드 Focus·터치에서는 보이지 않습니다(§15.1, 미해결). 툴팁을 더할 때 기존 `aria-*`와 클릭·Enter·Space 동작을 바꾸지 않습니다. + +### 15.3 CSS 클래스 역할 구분 + +- 여러 버튼이 공유하는 외형(공통)과 특정 기능 전용 스타일은 class 이름으로 구분합니다. 기능 이름이 붙은 class를 다른 기능의 버튼이 재사용하면 그 class의 변경·선택자가 모든 사용처에 영향을 주므로, 변경 전에 사용처(TSX·CSS·테스트·QA baseline)를 검색합니다. +- **현재 구조:** `hm-sw-header__action--print`는 SWHeader 인쇄 버튼(`SWHeader.tsx`, `SWHeaderActions.tsx`)과 `SWToolButton appearance="pill"`(→ `SWHeader`·`SWProcessHeader`의 `tools`, `SWProcessSplitButton`)에 함께 붙습니다. pill 버튼의 1px 테두리·radius·파일명 면 배경은 이 class에서 오고, `> img` 아이콘 위치 규칙은 ``를 직접 자식으로 둔 인쇄 버튼에만 맞습니다(pill은 `.hm-sw-tool__icon`). Process는 `.hm-sw-tool__pill-button`으로 자기 값을 덮습니다. `tests/header-composition.browser.mjs`(인쇄 버튼 수)와 `docs/sw/detail-button-qa/baseline.json`도 이 class를 기준으로 삼습니다. +- 소비 코드는 이 class로 인쇄 버튼을 찾거나 덮지 않습니다(§10-6). 공통 pill 외형 class와 인쇄 전용 class의 분리는 class·선택자·테스트 baseline이 함께 바뀌는 별도 구현 과제입니다. + +### 15.4 글꼴에 따른 실제 텍스트 폭 + +- 로고 폭 원칙은 §1 "로고 확장 공통 원칙"을 따르며 여기서 반복하지 않습니다. 이 절은 글꼴 차이만 다룹니다. +- 텍스트 로고·파일명·탭·필드 값처럼 폭이 내용에 따라 정해지는 영역은 실제 렌더링 글꼴에 따라 폭이 다릅니다. §3의 SW 기본 글꼴은 앱이 글꼴 파일을 로드할 때 기준이며, 소비 프로젝트가 Noto Sans KR을 로드하지 않았거나 다른 글꼴을 지정하면 대체 글꼴로 그려져 글자 폭·말줄임 시점·홈 폭이 문서 실측값과 달라질 수 있습니다. +- 문서의 px 실측(예: §14·[COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md)의 Process 로고 칸 값, HMDesign 저장소 Process 명세의 StrAna·Accessway 값)은 해당 글꼴·브라우저·당시 Commit 기준의 검증 사례이며 고정 규격이나 디자인 토큰이 아닙니다. 새 로고 이름이나 글꼴마다 고정 폭·예외를 추가하지 않고, 내용 폭을 따르는 기존 구조와 Header별 최소 폭으로 처리합니다. +- 폭을 검증할 때는 글꼴이 실제로 로드됐는지 확인하고, 다른 글꼴 환경의 결과는 글꼴 이름을 함께 기록합니다. + +### 15.5 공통 규칙으로 강제하지 않는 사항 + +- 단계 수·이름 길이에 따른 말줄임, 선택 단계 최소 폭, 권장 화면 폭, 단계 축소·More·스크롤은 `SWProcessHeader` 고유 검토 항목입니다(현재 동작은 [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) 11절 반응형, 검토 기록은 HMDesign 저장소 `docs/sw/sw-process-header-design-spec.md` §12). 다른 Header에 같은 기준을 적용하지 않습니다. diff --git a/packages/sw/ai/VIEWER.md b/packages/sw/ai/VIEWER.md index 75dce0c..479174f 100644 --- a/packages/sw/ai/VIEWER.md +++ b/packages/sw/ai/VIEWER.md @@ -159,6 +159,6 @@ const groups: SWViewerHeaderGroup[] = [ ## 7. 디자인 변경 시 주의 -- 크기(52px Header, 34px 버튼, 40px 토글·스위치 칸, 41~42px field 면)와 색은 참고 뷰어 실측값이고(Model / Layout 탭은 2026-10-23 Figma 7652:40344 모양, 높이는 field 그룹과 같은 42px·버튼 37.5px), 로고 면 64px는 2026-10-23 `SWHeader` 로고 기준입니다. 파일명·객체 수정·속성 면 배경은 SWHeader와 같은 사용자 확정 토큰(`--hm-sw-header-document-*`, `object-*`, `property-*`)을 공유하므로 바꾸면 두 Header가 함께 바뀝니다. +- 크기(52px Header, 34px 버튼, 40px 토글·스위치 칸, 41~42px field 면)와 색은 참고 뷰어 실측값이고(Model / Layout 탭은 2026-10-23 Figma 7652:40344 모양, 높이는 field 그룹과 같은 42px·버튼 37.5px), 로고 면은 2026-10-23 `SWHeader` 로고 기준이며 폭은 고정값이 아니라 로고 내용 폭을 따릅니다(Header 테두리 1px 안쪽에서 11 + 로고 + 36, 로고 위치는 SWHeader와 같은 x 12. 32 × 17 예시 로고에서 79px은 측정 예시, 공통 원칙은 [DESIGN_RULES.md](./DESIGN_RULES.md) §1). 파일명·객체 수정·속성 면 배경은 SWHeader와 같은 사용자 확정 토큰(`--hm-sw-header-document-*`, `object-*`, `property-*`)을 공유하므로 바꾸면 두 Header가 함께 바뀝니다. - **높이:** 패키지는 뷰어(도면) 영역 높이를 정하지 않습니다. Header는 52px 한 줄, 팝업만 최대 90vh입니다. 화면 높이는 앱의 상위 레이아웃이 정하고(예: `html, body, #root { height: 100% }` → 세로 flex, Header 다음 도면 영역 `flex: 1; min-height: 0`), 도면 영역 크기 변화에 맞춰 렌더러 크기를 바꾸는 것은 프로그램 담당입니다(ResizeObserver 권장). Storybook 예시의 고정 높이는 미리보기 프레임 값일 뿐 컴포넌트 값이 아닙니다. - Light/Dark 분기는 없습니다. 참고 뷰어와의 알려진 차이: 도면 공간(tabs) field 면 높이 42px(참고 41px — 참고 Tabs 루트가 39px인 구현 부산물), 크기 조절은 네이티브 `resize` 대신 같은 위치의 모서리 Grip(모든 브라우저 동작), 패널 Escape 닫기 추가, 로고 기본 표시 없음(사용자 결정). diff --git a/packages/sw/src/components/sw-header/types.ts b/packages/sw/src/components/sw-header/types.ts index 3245ba8..1416af7 100644 --- a/packages/sw/src/components/sw-header/types.ts +++ b/packages/sw/src/components/sw-header/types.ts @@ -348,7 +348,8 @@ export interface SWHeaderProps extends Omit, "title" defaultHomeMenuOpen?: boolean onHomeMenuOpenChange?: (open: boolean, reason: SWHeaderHomeMenuChangeReason) => void /** - * Logo image URL of the SW program, contained in the 32 x 17 logo slot. + * Logo image URL of the SW program, 17px high at its own ratio in the logo slot (32 x 17 is the Figma example, not a + * fixed size); the home and its slant follow the logo width (12 + logo + 36). * The logo is decided by each program: none is rendered when omitted. */ logoSrc?: string 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/components/sw-process-header/types.ts b/packages/sw/src/components/sw-process-header/types.ts index d28cdb1..5dbad02 100644 --- a/packages/sw/src/components/sw-process-header/types.ts +++ b/packages/sw/src/components/sw-process-header/types.ts @@ -84,7 +84,7 @@ export interface SWProcessHeaderProps extends Omit, onDocumentClick?: () => void /** Logo element in the SWHeader logo slot (17px high, as wide as the logo: 32 x 17 -> home 80px). Takes precedence over `logoText` and `logoSrc`. */ logo?: ReactNode - /** Text logo (e.g. `StrAna`): 18px 700 with the SW text outline in a 58px place (home 106px). */ + /** Text logo (e.g. `StrAna`): 18px 700 with the SW text outline in a place of at least 58px (home at least 106px); a longer text widens the place and the home to its own width, never cut. */ logoText?: string /** Logo image URL, contained (ratio kept, centered) in the logo slot. */ logoSrc?: string diff --git a/packages/sw/src/components/sw-tool/SWTool.tsx b/packages/sw/src/components/sw-tool/SWTool.tsx index 7eb0e6c..a2c76d4 100644 --- a/packages/sw/src/components/sw-tool/SWTool.tsx +++ b/packages/sw/src/components/sw-tool/SWTool.tsx @@ -27,7 +27,7 @@ interface GroupContext { selection: "none" | "single" } const SWToolGroupContext = createContext(null) export const SWToolButton = forwardRef(function SWToolButton( - { appearance = "text", pressed, children, icon, hoverIcon, className, type = "button", ...rest }, ref, + { appearance = "text", pressed, children, icon, hoverIcon, className, type = "button", title, ...rest }, ref, ) { const group = useContext(SWToolGroupContext) const radio = group?.selection === "single" @@ -39,15 +39,19 @@ export const SWToolButton = forwardRef(fun // The wrapper is the Header flex item; [data-tool] keeps it at its content width (not the file-name clamp). return (
-
) } if (appearance === "pill") { + // Default tooltip (2026-11-05): the pill shows only its icon, so a non-blank string `children` (its accessible name) + // is also the native `title`, as written. An explicit `title` wins and `title=""` turns it off; numbers and JSX + // children are never converted. The accessible name is unchanged (children / aria-label / aria-labelledby). + const tooltip = title ?? (typeof children === "string" && children.trim() !== "" ? children : undefined) return ( - ) diff --git a/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx b/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx index caa112f..d92da9a 100644 --- a/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx +++ b/packages/sw/src/components/sw-viewer/SWViewerHeader.tsx @@ -13,8 +13,9 @@ import "../../theme/sw-viewer.css" export interface SWViewerHeaderProps extends Omit, "title"> { /** - * Logo image URL of the program (same prop as SWHeader), contained in the SWHeader 32 x 17 logo slot, 12px from - * the header's left edge and vertically centered on the slanted green cap. + * Logo image URL of the program (same prop as SWHeader), 17px high at its own ratio in the SWHeader logo slot (32 x 17 + * is an example, not a fixed size), 12px from the header's left edge and vertically centered on the slanted green + * cap, which follows the logo width. * The logo belongs to each program: nothing is rendered when omitted (no default text or icon) and there is no cap * either (2026-10-23, also with `onHomeClick` / `logoMenu`, which then have no trigger). */ diff --git a/packages/sw/src/theme/sw-process-header.css b/packages/sw/src/theme/sw-process-header.css index f31c8f9..9f0b69c 100644 --- a/packages/sw/src/theme/sw-process-header.css +++ b/packages/sw/src/theme/sw-process-header.css @@ -111,12 +111,15 @@ /* Logo and file name follow SWHeader (2026-11-03, both appearances): SWHeaderHome as it is (logo slot as wide as the logo, 32 x 17 example -> home 80, 12px padding, slant), the file name 2px after the home overlap (SWHeader bar gap) with the SW padding 5 / 12 and font. Only the file-name width stays per appearance. The text logo (`logoText`) is - 18px 700 in a 58px place; a wider word overflows that place evenly on both sides, like the Strana centered content. */ + 18px 700 in a place of at least 58px (-> home 106); a wider word widens the place to its own width (2026-11-05, was + a fixed 58px that a longer word overflowed on both sides and the home cut on the left), so the home, its slant and + the file name follow it like any wider logo. */ .hm-sw-process-header__bar > .hm-sw-header__home { margin-right: -15px; } .hm-sw-process-header__wordmark { display: flex; justify-content: center; - width: 58px; + width: max-content; + min-width: 58px; color: var(--hm-sw-header-text-color); font-size: 18px; font-weight: 700; @@ -467,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. @@ -522,6 +529,10 @@ --hm-sw-process-shape-start: 50px; --hm-sw-process-shape-end: 48px; } +/* Square ends (2026-11-05 user decision, `multi` only): the base and the end covers meet the file name and the window + area with right angles. `default` keeps the Strana top corners 6 (cover 5); the step tabs keep their own shapes. */ +.hm-sw-process-bar[data-appearance="multi"]::before, +.hm-sw-process-bar[data-appearance="multi"] > .hm-sw-process-bar__rest { border-radius: 0; } .hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__group { flex-direction: column; align-items: flex-start; 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..79f625b --- /dev/null +++ b/packages/sw/tests/process-header-states.check.mjs @@ -0,0 +1,92 @@ +// 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)) + // 6. Process-area ends (2026-11-05): `multi` square (base and end covers radius 0), `default` keeps the Strana 6 / 5. + const block = selector => css.match(new RegExp(`(?:^|\\}|\\*/) ?${selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} ?\\{([^}]*)\\}`))?.[1] ?? null + const base = block(".hm-sw-process-bar::before") ?? "" + check("styles.css: default keeps the process base top corners 6 and the end covers 5", + /border-radius: ?6px 6px 0 0/.test(base) && /border-top-left-radius: ?5px/.test(block('.hm-sw-process-bar__rest[data-side="start"]') ?? "") + && /border-top-right-radius: ?5px/.test(block('.hm-sw-process-bar__rest[data-side="end"]') ?? ""), { base }) + check("styles.css: multi sets radius 0 on the process base and both end covers", + /\.hm-sw-process-bar\[data-appearance="?multi"?\]::before ?, ?\.hm-sw-process-bar\[data-appearance="?multi"?\] ?> ?\.hm-sw-process-bar__rest ?\{ ?border-radius: ?0 ?;? ?\}/.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/sw/tests/property-panel-comparison.browser.mjs b/packages/sw/tests/property-panel-comparison.browser.mjs index e717a5c..ca4006f 100644 --- a/packages/sw/tests/property-panel-comparison.browser.mjs +++ b/packages/sw/tests/property-panel-comparison.browser.mjs @@ -91,7 +91,9 @@ try { report.data[`gaps${width}`] = { colGap, colGap2, rowGap } // 2026-10-22 (user request): no Inner scroller; the page scrolls. 2 columns × 2 rows, one column below 720px. // 2026-10-26 (user request): from 1080px, 3 columns — none/all stacked in column 1, circle and expanded beside them. - const oneColumn = width < 720, threeColumns = width >= 1080 + // 2026-11-05: the Story page Inner keeps 60px on each side (story-page.css), so the steps moved to 787px / 1125px + // (sw-stories.css .sw-story-compare--grid). It keeps 16px up to 480px (growing to 60px at 768px), so 390px has 358px. + const oneColumn = width < 787, threeColumns = width >= 1125 if (threeColumns) { const gaps = { stack: b.cell.y - a.cell.bottom, col1: c.cell.x - Math.max(a.cell.right, b.cell.right), col2: d.cell.x - c.cell.right } report.data[`gaps${width}`] = gaps diff --git a/packages/sw/tests/text-weight.browser.mjs b/packages/sw/tests/text-weight.browser.mjs index c3f197f..b429bfd 100644 --- a/packages/sw/tests/text-weight.browser.mjs +++ b/packages/sw/tests/text-weight.browser.mjs @@ -140,8 +140,8 @@ await page.keyboard.press('Escape') report.reference = { more: refMore[0], documents: refDocs[0], viewerItem: refViewerItem[0], viewerLabel: refViewerLabel[0] } await goto('sw-viewer--viewer-header') -// The 1440px Story frame at full width (as the viewer suites): the header has room for every label. -await page.addStyleTag({ content: '.sb-page { --sb-page-max-width: 100vw; }' }); await page.waitForTimeout(200) +// The 1440px Story frame at full width (the 1920px page's Inner is 1800px): the header has room for every label. +await page.waitForTimeout(200) const vh = page.locator('.hm-sw-viewer-header').first() const vNames = await styles('.hm-sw-viewer-header >> nth=0 >> :is(.hm-sw-header__document-name, .hm-sw-viewer-header__document-name, .hm-sw-viewer-header__label, .hm-sw-viewer-header__tab)') report.viewer = vNames diff --git a/packages/sw/tests/theme-inheritance.browser.mjs b/packages/sw/tests/theme-inheritance.browser.mjs index d329eee..47c6f32 100644 --- a/packages/sw/tests/theme-inheritance.browser.mjs +++ b/packages/sw/tests/theme-inheritance.browser.mjs @@ -39,7 +39,6 @@ const newPage = async (width = 1440, height = 1000) => { const open = async (page, id) => { await page.goto(`${storybook}/iframe.html?id=${id}&viewMode=story`) await page.waitForSelector('#storybook-root > *') - await page.addStyleTag({ content: '.sb-page { --sb-page-max-width: 100vw; }' }) await page.evaluate(() => document.fonts.ready) await page.waitForTimeout(300) } diff --git a/packages/sw/tests/viewer-logo-document-menu-firefox.browser.mjs b/packages/sw/tests/viewer-logo-document-menu-firefox.browser.mjs index 3949eb6..77de065 100644 --- a/packages/sw/tests/viewer-logo-document-menu-firefox.browser.mjs +++ b/packages/sw/tests/viewer-logo-document-menu-firefox.browser.mjs @@ -46,8 +46,8 @@ const ev = async expr => { const r = await send('script.evaluate', { expression: const goto = async story => { await send('browsingContext.navigate', { context, url: `${base}/iframe.html?id=${story}&viewMode=story`, wait: 'complete' }) for (let i = 0; i < 40; i++) { if (await ev(`!!document.querySelector('.hm-sw-viewer-header')`)) break; await sleep(250) } - // Lift the Story page envelope like the Chromium suites; scroll instantly (the preview sets smooth scrolling). - await ev(`(() => { const s = document.createElement('style'); s.textContent = '.sb-page { --sb-page-max-width: 100vw; } html { scroll-behavior: auto !important; }'; document.head.append(s); return true })()`) + // Scroll instantly (the preview sets smooth scrolling). + await ev(`(() => { const s = document.createElement('style'); s.textContent = 'html { scroll-behavior: auto !important; }'; document.head.append(s); return true })()`) await ev('document.fonts.ready.then(() => true)'); await sleep(400) } const frames = () => ev('new Promise(r => requestAnimationFrame(() => requestAnimationFrame(() => r(true))))') diff --git a/packages/sw/tests/viewer-logo-document-menu.browser.mjs b/packages/sw/tests/viewer-logo-document-menu.browser.mjs index 5a83ad0..43fe652 100644 --- a/packages/sw/tests/viewer-logo-document-menu.browser.mjs +++ b/packages/sw/tests/viewer-logo-document-menu.browser.mjs @@ -29,7 +29,6 @@ const newPage = async (width = 1920, height = 1000) => { const open = async (page, id) => { await page.goto(story(id)) await page.waitForSelector('.hm-sw-viewer-header') - await page.addStyleTag({ content: '.sb-page { --sb-page-max-width: 100vw; }' }) await page.evaluate(() => document.fonts.ready) await page.waitForTimeout(200) } diff --git a/packages/sw/tests/viewer.browser.mjs b/packages/sw/tests/viewer.browser.mjs index 84939c8..8b3c4bc 100644 --- a/packages/sw/tests/viewer.browser.mjs +++ b/packages/sw/tests/viewer.browser.mjs @@ -24,9 +24,10 @@ const newPage = async (width = 1440, height = 900) => { page.on('console', message => { if (message.type() === 'error' && !/favicon|404/.test(message.text())) errors.push(`${page.url()}: ${message.text()}`) }) return page } -// 2026-10-23: waits for a visible header (the Responsive Story's first, 1920px row is hidden below a 2000px page). -// Test-only: lift the 1280px Story page envelope so the 1440px frames are measured at full width. -const open = async (page, id) => { await page.goto(story(id)); await page.waitForSelector('.hm-sw-viewer-header:visible'); await page.addStyleTag({ content: '.sb-page { --sb-page-max-width: 100vw; }' }); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(200) } +// 2026-10-23: waits for a visible header (the Responsive Story's first, 1920px row is hidden below a 2040px page). +// 2026-11-05: the Story page Inner is the page width minus 60px on each side from a 768px viewport (16px up to 480px, +// growing in between; story-page.css), so no envelope override is needed. +const open = async (page, id) => { await page.goto(story(id)); await page.waitForSelector('.hm-sw-viewer-header:visible'); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(200) } const log = page => page.locator('[data-callback-log]').first().innerText() const box = locator => locator.boundingBox() @@ -293,7 +294,7 @@ if (comparison.reference) { } { // Responsive: measured steps (full → icons only → More), no horizontal overflow; More collects the collapsed items. - // 2026-10-23: rows are picked by their data-width (not by position). The Story's 1920px row is hidden below a 2000px + // 2026-10-23: rows are picked by their data-width (not by position). The Story's 1920px row is hidden below a 2040px // page (sw-story-responsive-hidden), so the page is 2500px wide and every row is shown at its own maximum width. const page = await newPage(2500, 1200) await open(page, 'responsive') diff --git a/packages/ui/ai/AI_GUIDE.md b/packages/ui/ai/AI_GUIDE.md index 927a051..68b0f36 100644 --- a/packages/ui/ai/AI_GUIDE.md +++ b/packages/ui/ai/AI_GUIDE.md @@ -141,7 +141,7 @@ | 색·Typography | DESIGN_RULES §2·§3 | DESIGN_RULES §2·§3 | VIEWER.md `SWViewerHeader`·디자인 변경 절 | DESIGN_RULES §2·§3 | | 크기·여백 | DESIGN_RULES §4 | DESIGN_RULES §1 | VIEWER.md 디자인 변경 절(52px Header 등 실측값) | 카드·간격 변경 금지(승인 디자인) | | Border·Radius·배경·그림자 | DESIGN_RULES §2·§4 | DESIGN_RULES §2·§8 | VIEWER.md `SWViewerPanel` 절(프레임은 SWPopup과 같음) | 공개 입력만 | -| 아이콘 | `lucide-react`(패키지 의존성) | 패키지 내장 SVG만, 새 아이콘은 사용자 제공 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon` | Core 내장 | +| 아이콘 | `lucide-react`(패키지 의존성) | 사용자·프로젝트 제공 에셋 → 패키지 공개 Export 중 같은 기능 → 없으면 미지원 보고(외부 라이브러리·AI 생성 SVG·Storybook 파일로 대체 금지). 시안 기반 Header 도구 아이콘·창 제어 버튼은 `@hm-design/sw` `ai/AI_USAGE.md` §5 | 프로그램 제공 16px `currentColor` 아이콘, 외곽선은 `SWViewerIcon`. 선택 순서는 SW 일반 모드와 같음(`AI_USAGE.md` §5) | Core 내장 | | 스크롤바 | `ScrollArea`(Radix), DESIGN_RULES §6 | 전용 디자인 없음 — 브라우저 기본(AI_USAGE §2) | 전용 디자인 없음 — 패널 본문 스크롤은 `SWViewerPanel` | Core 내부 Zoom/Pan | | Hover / Active / Focus / Disabled | DESIGN_RULES §5 | DESIGN_RULES §4 | VIEWER.md `SWViewerHeader` 절 상태 표현 | Core 내장 | | Popup·Panel | `Dialog`·`AlertDialog`·`Sheet`·`Drawer`·`Popover` | `SWPopup` + `SWPopupSection`/`SWPopupRow` | `SWViewerPanel`(+ `modal`) + `SWViewerSection`/`SWViewerField` | Core 팝업 / HM Dialog / 어댑터 | @@ -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. 접근성 기본 diff --git a/src/components/story-page.css b/src/components/story-page.css index 934db33..4793608 100644 --- a/src/components/story-page.css +++ b/src/components/story-page.css @@ -1,5 +1,5 @@ /* Storybook presentation only. Common page frame for representative Stories; all selectors stay inside .sb-page. - Inner (fluid, 60px each side; vertical gutter 16–40) and title (kicker 12 / title 28·36·700 / description, underlined) reuse Foundation / Color. + Inner (fluid; 16px each side up to 480px, growing to 60px at 768px; vertical gutter 16–40) and title (kicker 12 / title 28·36·700 / description, underlined) reuse Foundation / Color. Section reuses the Center Graphic / 교량 split: heading + description, a divider, then the specimen. */ .sb-page { --sb-page-gutter: clamp(16px, 3vw, 40px); @@ -12,8 +12,10 @@ } .sb-page > .sb-page-inner { box-sizing: border-box; - /* 2026-11-05 (user request): no max width; the Inner follows the page width and keeps 60px on each side. */ - width: calc(100% - 120px); + /* 2026-11-05 (user request): no max width; the Inner follows the page width. Side margin (each side) = 16px up to a + 480px viewport (358px Inner at 390px, so a 314px specimen fits), then linear to 60px at 768px (44px over 288px, + 11 / 72 of the viewport growth), 60px from 768px. The Inner keeps growing (about 0.69px per 1px), with no jump. */ + width: calc(100% - 2 * clamp(16px, calc(16px + (100vw - 480px) * 11 / 72), 60px)); min-width: 0; margin-inline: auto; } diff --git a/src/sw/sw-footer-stories.css b/src/sw/sw-footer-stories.css index 5c218b5..c8e3d13 100644 --- a/src/sw/sw-footer-stories.css +++ b/src/sw/sw-footer-stories.css @@ -32,7 +32,7 @@ .sw-footer-story-inline { display: inline-flex; flex-direction: column; padding: 0; } .sw-footer-story-width + .sw-footer-story-width { margin-top: 16px; } /* The hidden 1920px row (Responsive More, narrower than 1920px) takes its gap with it. */ -@container sw-story-page (max-width: 1999.98px) { .sw-story-responsive-hidden + .sw-footer-story-width { margin-top: 0; } } +@container sw-story-page (max-width: 2039.98px) { .sw-story-responsive-hidden + .sw-footer-story-width { margin-top: 0; } } .sw-footer-story-aux-compare { display: grid; grid-template-columns: repeat(auto-fill, minmax(560px, 1fr)); gap: 16px; } .sw-footer-story-aux-row { display: flex; align-items: center; gap: 12px; margin: 0; } .sw-footer-story-aux-row figcaption { flex: 0 0 120px; font-size: 12px; color: #3d4a45; } diff --git a/src/sw/sw-stories.css b/src/sw/sw-stories.css index 0ffd070..ecf37ee 100644 --- a/src/sw/sw-stories.css +++ b/src/sw/sw-stories.css @@ -1,12 +1,13 @@ /* Story canvas only: frames that simulate a header width. Never styles the header itself. */ .sw-story-scroller { max-width: 100%; overflow-x: auto; } /* Responsive More pages: the 1920px row (sw-story-responsive-hidden) shows when the page has 1920px of usable width — - the page (container sw-story-page, not the browser) minus its 40px gutters, so 2000px; with less room the whole row — + the page (container sw-story-page, not the browser) minus the Inner's 60px side margins (story-page.css, + 60px from a 768px viewport), so 2040px (2026-11-05; was 2000px with the former 40px gutters); with less room the whole row — title, header, background, margin — is hidden. 2026-10-23: it stays inside the Story column like every other row (the former reach-out of the 80vw Inner, sw-story-responsive-wide, is gone); its preview is at most 1920px wide. */ .sw-story-responsive { container-type: inline-size; } .sb-page:has(.sw-story-responsive) { container: sw-story-page / inline-size; } -@container sw-story-page (max-width: 1999.98px) { .sw-story-responsive-hidden { display: none; } } +@container sw-story-page (max-width: 2039.98px) { .sw-story-responsive-hidden { display: none; } } .sw-story-frame { background: #060606; } /* Shared drawing-area canvas of the SW Stories (2026-10-19): the SW/Viewer black grid — 40px cells, 1px lines of HM green #266651 at 18%, on #0A0A0B. Used behind the SW Header work area and dropdown room and the viewer frames. */ @@ -43,18 +44,22 @@ /* Property panel 4-state comparison at the panels' own size (314px wide, never shrunk). 2026-10-26 (user request): 3 columns — the short panels (none, all) stacked in column 1, the long ones (circle, expanded) side by side in columns 2 and 3. Rows are auto / 1fr so the long panels' height goes to row 2 only and - "all" sits 24px under "none". Narrower screens: 2 columns × 2 rows (3 × 314 + 2 gaps + gutters do not fit), and one - column below 720px; no Inner scroller and no sideways scroll (2026-10-22). */ + "all" sits 24px under "none". Narrower screens: 2 columns × 2 rows, and one column below 787px; no Inner scroller + and no sideways scroll (2026-10-22). 2026-11-05: each step needs its columns plus the Inner's 60px side margins + (120, story-page.css) and a ~15px vertical scrollbar (the media queries read the viewport): 3 columns from 1125px + (3 × 314 + 2 × 24 = 990), 2 columns from 787px (2 × 314 + 24 = 652). Before: 1080px / 720px with 16–40px gutters. + Both steps sit above 768px, where the margins are 60px. The single column always fits: the margins are 16px up to + 480px and grow to 60px by 768px (story-page.css), so 375 / 390px screens have 343 / 358px for the 314px panel. */ .sw-story-compare-page .sw-story-log { margin-top: 12px; } .sw-story-compare.sw-story-compare--grid { display: grid; grid-template-columns: repeat(2, max-content); align-items: start; gap: 24px; } -@media (min-width: 1080px) { +@media (min-width: 1125px) { .sw-story-compare.sw-story-compare--grid { grid-template-columns: repeat(3, max-content); grid-template-rows: auto 1fr; } .sw-story-compare--grid > [data-compare="none"] { grid-area: 1 / 1; } .sw-story-compare--grid > [data-compare="all"] { grid-area: 2 / 1; } .sw-story-compare--grid > [data-compare="circle"] { grid-area: 1 / 2 / span 2; } .sw-story-compare--grid > [data-compare="expanded"] { grid-area: 1 / 3 / span 2; } } -@media (max-width: 719.98px) { .sw-story-compare.sw-story-compare--grid { grid-template-columns: max-content; } } +@media (max-width: 786.98px) { .sw-story-compare.sw-story-compare--grid { grid-template-columns: max-content; } } /* Placement-only exception, same as the Overview Stage: the product clamps a panel to the viewport height; here the scroller provides the room so the 754px panel keeps its size. */ .sw-story-compare--grid .hm-sw-property { max-height: none; }