fix(sw): add default tooltip for pill tool buttons

- SWToolButton appearance="pill": a non-blank string children is also
  the native title; an explicit title wins and title="" turns it off;
  numbers, JSX and arrays are never converted. text / document keep
  passing only the given title. Accessible name, DOM and classes unchanged
- DESIGN_RULES §15: Header UX principles (ellipsis full text, icon-only
  tool buttons, print class shared by pill, font-dependent widths, scope)
- Reference them from DESIGN_RULES §3·§9, COMPONENT_CATALOG §4·§11,
  AI_USAGE §5.4, SW Header spec §17 and Step Header spec §12
This commit is contained in:
shkim4 committed 2026-11-05 15:55:23 +09:00
1 parent e70082550e
commit 22841affaf
6 files changed
+73 -8

No files matched your search

+2
View File
@@ -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)
+14
View File
@@ -327,6 +327,20 @@ 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). 로고 이름·글꼴별 고정 폭 예외를 두지 않음 |
## 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 파일도 그대로입니다.
+1 -1
View File
@@ -220,7 +220,7 @@ SW 토큰(`--hm-sw-header-*`, `--hm-sw-property-*`, `--hm-sw-viewer-*`, `--hm-sw
- 누르는 순간(`: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로 맞추지 않고 지원 범위 밖으로 보고합니다.
- 그림은 장식이므로 `<img alt="" />`로 두고 버튼 이름은 `children`으로 줍니다.
- 그림은 장식이므로 `<img alt="" />`로 두고 버튼 이름은 `children`(문자열이면 기본 툴팁, DESIGN_RULES §15.2)으로 줍니다.
- 상태를 맞추려고 `hm-sw-*` class나 패키지 CSS를 덮지 않습니다.
### 5.5 창 제어 버튼(창 이동·최소화·최대화·닫기)
+5 -2
View File
@@ -89,11 +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 일반 모드 전용)
@@ -190,7 +192,7 @@ SW **뷰어 모드** 전용 Export입니다(2026-10-19 추가, 참고 DWG/DXF
| 컴포넌트 | 용도 | 주요 Props | 크기 | 검증 |
|---|---|---|---|---|
| `SWProcessHeader` | 49px Header: 로고 → 파일명 → 프로세스 영역 · Layer 면 → 창 영역(도구 pill · 창 이동 · 최소화 · 최대화/복원 · 종료) | `appearance`(`"default"` Strana · `"multi"` StrAna-SW 다중 Step, 기본 `default`, `process.appearance`가 없으면 프로세스 영역에도 적용), `documentTitle`(필수, 문서가 없으면 `""`), `documentEmptyLabel`(제목이 비었을 때 파일명 면 오른쪽 위 10px 표시, 예: `프로젝트 생성 전`), `onDocumentClick`, `logo`(커스텀 요소) / `logoText`(글자 로고) / `logoSrc`(SWHeader 로고 칸: 높이 17·로고 비율 폭, 32 × 17 → 홈 80, 두 타입 공통. 우선순위 `logo` > `logoText` > `logoSrc`, 셋 다 같은 로고 칸에 들어가 홈 = 12 + 로고 폭 + 36. 글자 로고는 최소 58px 칸 → 홈 최소 106, 더 긴 글자는 글자 폭만큼 넓어짐·잘림 없음), `logoAlt`(기본 `logoText`), `onHomeClick`, `homeMenuOpen`(로고 메뉴는 호스트 소유), `process: SWProcessBarProps`(필수), `layer?: SWProcessHeaderLayer`(`label` 기본 `레이어`, `value`, `visible`, `open`, `disabled`), `onLayerClick`, `onLayerVisibilityChange(next)`, `tools?: ReactNode`(`SWToolButton appearance="pill"`, Header가 pill로 감쌈), `toolsLabel`(기본 `도구`), `onWindowMoveStart`, `maximized`(기본 false = 최대화 글리프, true = 복원 글리프), `onMinimize`, `onMaximizeToggle`, `onClose`, `hiddenControls?: SWProcessHeaderHideableControlId[]`(`layer`·`tools`·`windowMove`·`minimize`·`maximize`·`close`), `disabled`(프로세스·Layer만, 로고·파일명·도구·창 버튼 제외), `<header>` HTML 속성·`ref` | 높이 49. `default`: 면 48, 파일명 286 : 가운데 1260(프로세스 1003 : Layer 257) 비율, 파일명 100~286, Layer 칸 152~257, 창 영역 약 312(도구 3개). `multi`: 면 49, 파일명 189 : 프로세스 1278, 파일명 100~189, SW pill(버튼 40 × 37), 창 영역 367(도구 5개, 최대화 숨김) | ◻ |
| `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 | ◻ |
| `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그룹 이상은 렌더링되지만 디자인 미확정입니다.
@@ -198,6 +200,7 @@ SW **뷰어 모드** 전용 Export입니다(2026-10-19 추가, 참고 DWG/DXF
- **`multi`**(Figma StrAna-SW 873:16481 · 873:16254 · 873:17421): 그룹 수·단계 수는 데이터대로이며, 켜진 그룹보다 앞 그룹은 진행 완료(제목 700 흰색·밝은 탭), 뒤 그룹은 미진행(제목 500 `#C3EBDF`·70% 탭)으로 표시합니다. 바탕·덮개·발광·화살표·선택·키보드는 `default`와 같은 구현입니다.
- **키보드:** 탭 사이 Arrow(좌우·상하)·Home·End 이동 + 선택(비활성 단계 건너뜀), Tab은 선택된 단계 하나로만 들어갑니다.
- **반응형:** 숨김·More·스크롤 없음. 보장 폭 1100px 이상이며 더 좁으면 Header 안에서 잘립니다. 탭 글자·파일명·Layer 이름은 `…` 말줄임. 공간이 부족하면 선택된 탭도 다른 탭과 같은 비율로 줄어듭니다(2026-11-03 개정): `default`는 모든 탭이 같은 폭, `multi`는 글자 폭에 비례하며 이름이 들어가지 않으면 `…`. 선택이 바뀌어도 탭 폭은 그대로입니다. 그룹은 이름 + 모든 탭 여백보다 좁아지지 않아 겹치지 않습니다. 그룹 사이 화살표·간격은 고정입니다. **넘침 정책 확정(2026-11-03): 그룹 접기·좌우 스크롤·More 메뉴를 추가하지 않습니다.**
- **원문 확인(현재 구현):** 파일명·Layer 값은 `title`로 전체 이름을 보여 줍니다. 단계 탭 이름과 `multi` 그룹 제목은 말줄임되어도 `title`이 없고 접근성 이름(탭 `aria-label`, 그룹 이름 `aria-describedby`)만 전체 이름입니다. 개선은 별도 과제입니다([DESIGN_RULES.md](./DESIGN_RULES.md) §15.1). 호스트 `tools`의 `SWToolButton pill`은 문자열 `children`이 기본 툴팁이 되며, 숫자·JSX `children`이면 호스트가 `title`을 전달합니다(§4).
- **범위 밖(호스트 소유):** 로고 메뉴 본체, Layer Popup, 실제 화면 분할 실행, 도구 아이콘(소비 측이 `tools`에 넣는 요소). 도움말·3D 보기·설정·메뉴·나가기 같은 Header 도구 아이콘은 공개 Export가 없고, Storybook `SW / Step Header`의 아이콘은 공개 API가 아닙니다. 시안의 아이콘 선택과 창 제어 버튼 생략(`hiddenControls`)은 [AI_USAGE.md](./AI_USAGE.md) §5를 따르며, 창 버튼 기본 구성(모두 표시)은 바뀌지 않습니다. 화면 분할 선택 UI는 `SWProcessSplitButton`(2026-11-03 추가). `SWHomeMenu`는 이 Header에 쓰지 않습니다(2단 타일 디자인이 다름).
- **SWHeader와 같은 상호작용(2026-11-03):** 창 이동은 `SWHeaderWindowControls`의 창 이동(Hover 1.4s Motion·툴팁)을 그대로 씁니다. 글자 외곽선·Hover·Focus도 SWHeader 규칙(DESIGN_RULES §14). 도구 아이콘이 이미지면 `SWToolButton`의 `hoverIcon`으로 노란 그림(`#B6D0C9 → #FFC600`)을 함께 주고, 단색 SVG면 `currentColor`로 칠합니다.
- **단독 사용:** `SWProcessBar`만 쓸 때는 `<SWTheme>` 안에 둡니다. `SWProcessHeader`는 루트에 `hm-sw-theme`이 있어 `<SWTheme>`이 필요 없습니다.
+43 -1
View File
@@ -50,7 +50,7 @@
| Weight | 400, 500, 700만 사용(600 없음). SWHeader 안의 필드 값(레이어·색상·선스타일·가중치·스케일·문자/치수스타일 값)은 400(14px · 자간 0.56px), Composed Controls(SWHeader 밖 단독 메뉴)의 필드 값은 700(2026-10-20). 파일명·제목·토글·버튼·메뉴 글자 700 |
| 자간 | 값 0.04em, 라벨 0.02em, 파일명 0.06em |
| Text Stroke | Figma Stroke가 있는 텍스트만 검정 외곽선: 14·16px `--hm-sw-header-text-outline`(1.3px), 12px `--hm-sw-header-label-outline`(1px). 자기 색 Stroke 없음. More·Tooltip·`+ New`/`Open` 제외 |
| 말줄임 | 값은 한 줄 `ellipsis`, `title`로 전체 표시 |
| 말줄임 | 값은 한 줄 `ellipsis`, `title`로 전체 표시. 적용 범위·키보드·터치 기준과 Header별 현재 구현은 §15.1 |
## 4. 상태
@@ -127,6 +127,7 @@
| `pill` | 인쇄·K: pill(`rgba(0,0,0,.8)`, 1px `#092920`, radius 4px) 안 40 × 38 버튼, 아이콘 영역 24 × 23 | Hover 노란 gradient 링 + `hoverIcon`(생략 시 기본 아이콘 유지). 아이콘 색은 기존 인쇄 아이콘 `#B6D0C9` → Hover `#FFC600`(`--hm-sw-header-selected-color`) — 인쇄 이미지 두 장의 색입니다. `currentColor` 아이콘은 상속 글자색이며 Hover에서 바뀌지 않고 `pressed`만 `#FFC600`(링·`hoverIcon` 전환 없음). Process Header `tools`는 §14, 상태 비교는 [AI_USAGE.md](./AI_USAGE.md) §5.4 |
| `document` | 파일명 면: 48px, gradient 표면, 위·오른쪽·아래 1px gradient 테두리(`--hm-sw-header-document-border-gradient`), 글자 `#F0F7F5` 700 · 자간 0.06em | 글자만 Hover `#FFC600`. Home 바로 뒤면 로고 사선 접점 공유, 그 뒤 파일명은 왼쪽 여백 12px |
- 아이콘 전용 `pill`의 이름·툴팁 기준은 §15.2, `pill`이 인쇄 버튼 class를 공유하는 구조는 §15.3입니다.
- 이 절은 상태 값만 정합니다. 디자인 시안의 아이콘을 어디서 가져올지(제공 에셋 → 공개 Export → 미지원 보고)와 시안에 없는 창 제어 버튼 생략은 [AI_USAGE.md](./AI_USAGE.md) §5입니다(§14 Process Header 도구 pill에도 적용).
## 10. 새 컴포넌트 체크리스트
@@ -259,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` 아이콘 위치 규칙은 `<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에 같은 기준을 적용하지 않습니다.
@@ -27,7 +27,7 @@ interface GroupContext { selection: "none" | "single" }
const SWToolGroupContext = createContext<GroupContext | null>(null)
export const SWToolButton = forwardRef<HTMLButtonElement, SWToolButtonProps>(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<HTMLButtonElement, SWToolButtonProps>(fun
// The wrapper is the Header flex item; [data-tool] keeps it at its content width (not the file-name clamp).
return (
<div className="hm-sw-header__document" data-tool="">
<button ref={ref} type={type} className={classes("hm-sw-header__document-button")} data-selected={pressed ? "true" : undefined} {...state} {...rest}>
<button ref={ref} type={type} className={classes("hm-sw-header__document-button")} data-selected={pressed ? "true" : undefined} title={title} {...state} {...rest}>
<span className="hm-sw-header__document-name">{children}</span>
</button>
</div>
)
}
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 (
<button ref={ref} type={type} className={classes("hm-sw-header__action hm-sw-header__action--print hm-sw-tool__pill-button")} data-selected={pressed ? "true" : undefined} {...state} {...rest}>
<button ref={ref} type={type} className={classes("hm-sw-header__action hm-sw-header__action--print hm-sw-tool__pill-button")} data-selected={pressed ? "true" : undefined} title={tooltip} {...state} {...rest}>
{/* hm-sw-header__icon is the "hide on hover" half of the sw.css icon swap: only with a hover picture to show. */}
{icon !== undefined && <span className={hoverIcon !== undefined ? "hm-sw-header__icon hm-sw-tool__icon" : "hm-sw-tool__icon"} aria-hidden="true">{icon}</span>}
{hoverIcon !== undefined && <span className="hm-sw-header__icon-hover hm-sw-tool__icon" aria-hidden="true">{hoverIcon}</span>}
@@ -56,7 +60,7 @@ export const SWToolButton = forwardRef<HTMLButtonElement, SWToolButtonProps>(fun
)
}
return (
<button ref={ref} type={type} className={classes("hm-sw-header__toggle hm-sw-tool__text-button")} data-selected={pressed ? "true" : undefined} {...state} {...rest}>
<button ref={ref} type={type} className={classes("hm-sw-header__toggle hm-sw-tool__text-button")} data-selected={pressed ? "true" : undefined} title={title} {...state} {...rest}>
{children}
</button>
)