diff --git a/WORKING_CONTEXT.md b/WORKING_CONTEXT.md index 87516eb..dde0857 100644 --- a/WORKING_CONTEXT.md +++ b/WORKING_CONTEXT.md @@ -1,5 +1,26 @@ # Working Context +## 2026-11-03 — SWProcessHeader 1차 구현 (Strana Main Header) + +- 작업 브랜치: `feature/sw-process-header`(main 6ca92c5에서 분기). 미Commit·미Push — Commit·Push는 사용자 별도 지시 대상. +- 범위: 방안 B(별도 컴포넌트) 승인안. `@hm-design/sw`에 `SWProcessHeader`·`SWProcessBar` 추가(`packages/sw/src/components/sw-process-header/`, `src/theme/sw-process-header.css`), Story `SW/Step Header`(후속 12에서 `SW/Process Header`에서 변경, `src/sw/sw-process-header*.tsx|css`), 이후 `SWProcessSplitButton` 추가(후속 5), 스펙 `docs/sw/sw-process-header-design-spec.md`, SW AI 문서 4종 갱신, `src/index.ts` Export 1줄. +- 변경 금지 유지: `SWHeader`·`SWViewerHeader`·`sw.css`·기존 Story·`docs/ai/AI_GUIDE.md`·`packages/ui`·`D:\Strana`. +- 1차 제외(호스트 소유·2차 후보): 로고 메뉴 본체, Layer Popup, Dark·Light·HighContrast 화면 비교. (화면 분할은 후속 5에서 추가) +- 검증: typecheck:sw · build:sw · check:ai-docs · tsc -b PASS, Storybook 상태·Hover·키보드 확인, Strana Hanmac 1100/1366/1920 캡처 비교, `SW/Header` 출력 HTML·기존 CSS 동일. Firefox·Safari·최신 분할 버튼 캡처는 미확인. +- 2026-11-03 후속: Story 설명·이벤트 로그 제거, 로고를 임시 로고(`logoSrc`, `src/sw/assets/sw-header-logo-placeholder.svg`)로 교체, 이 Header 로고 칸 기본 58 × 17(홈 106) 유지 — 후속 7에서 SWHeader 기준(홈 80)으로 대체. +- 2026-11-03 후속 2: `appearance="multi"`(StrAna-SW Figma 873:16481·873:16254·873:17421) 추가 — 같은 컴포넌트·DOM, CSS 분기, `documentEmptyLabel`·`labelHidden`·`SWProcessAppearance`, Story 7개, Figma 도구 아이콘 fixture. Default 치수·CSS 회귀 없음 확인. 넘침 정책 확정(말줄임, 접기·스크롤·More 없음; 선택 탭 유지는 후속 10에서 비례 축소로 대체). +- 2026-11-03 후속 3: 사다리꼴(그룹 면)은 시작·끝 그룹에만 — 켜진 가운데 그룹의 발광 면 제거(Figma 873:17421 일치), default·multi 공통. +- 2026-11-03 후속 4: SWHeader 정합 — multi에도 SW 글자 외곽선, 창 이동을 SWHeaderWindowControls(창 이동만, 1.4s Motion)로 교체(정적 그림·토큰 2개 삭제), Multi 도구 Hover 노란 그림(fixture hoverIcon), 눌림도 노란 아이콘. +- 2026-11-03 후속 5: 선택 Step 전체 이름을 default에도 적용(측정한 최소 폭, flex 통일 — 후속 10에서 비례 축소로 대체), Hover 그림 없는 도구 아이콘이 Hover 시 사라지던 문제 수정(범위 안 보완 — 후속 6에서 SWToolButton 공통 수정으로 대체), 화면 분할 `SWProcessSplitButton` 추가(Strana 6758:12510, 연장은 Header 끝까지). +- 2026-11-03 후속 6: SWToolButton 공통 아이콘 버그 수정 — pill 기본 아이콘의 `hm-sw-header__icon` class를 `hoverIcon`이 있을 때만 부여(`SWTool.tsx`), SWProcessHeader 범위 보완 CSS 삭제. SWHeader `tools`도 함께 해결, `sw.css`·색·Hover 정책 변경 없음. +- 2026-11-03 후속 7: 로고·파일명 영역 SWHeader 기준 통일(default·multi) — 로고 칸 58/55 고정 제거(SWHeader 자동 폭 → 홈 80), 홈과 파일명 간격 SWHeader와 같게(겹침 15 + 2), 파일명 padding 22·33/16·multi 자간 0 제거 → 글자 x 70. 파일명 최대 286/189 유지. +- 2026-11-03 후속 8: 화면 분할 Popup 하단 잘림 수정 — 아래 공간이 부족하면 위치 훅이 위로 뒤집어 Header에 가려지던 문제, `SWProcessSplitButton`은 항상 버튼 아래(스크롤 시 따라감). 설계 문서 §11.3. +- 2026-11-03 후속 9: Storybook `SW/Process Header` 간소화 — 사이드바 2개(Process Header = Default 섹션 + Multi Step 섹션(Figma 시안 3개, 각자 상태, 노드 ID 미표시) · Responsive), SW 페이지와 같은 제목 + StorySection. 검증용 숨김: Multi Step(시안 1920, data-figma-node) · 화면 분할 · Fixed Widths · Process Bar · Many Groups · Many Steps · States · Theme Override, Figma 시안 3개 Story를 한 페이지로 통합, 캡션·디버그 data 속성 제거. +- 2026-11-03 후속 10: 단계 탭 비례 축소 — 선택 탭 최소 폭(`--hm-sw-process-step-min`)·multi 선택 탭 `flex: 0 0 auto` 제거, 그룹 최소 폭은 모든 탭 여백만. 1920·2560 변화 없음. +- 2026-11-03 후속 11: 화면 분할 Popup 아이콘 하단 잘림 — 원인은 틀 상자 소수 크기(31.6 × 21.73)의 픽셀 맞춤(배율 100%에서 21줄), 틀 32 × 22로 수정(`sw-process-header.css`). 사용자 Chrome에서 재현·수정 확인. +- 2026-11-03 후속 12: Storybook 표시 명칭 `SW/Process Header` → `SW/Step Header`(페이지 제목 `SW Step Header`, 섹션·사이드바 `Step Header`, `Multi Step` 유지). Story ID는 `sw-step-header--*`로 바뀜. 컴포넌트·공개 API(`SWProcessHeader`, `SWProcessBar`, `SWProcessSplitButton`)·파일명은 유지. +- 기존 `package-lock.json` 수정과 미추적 `.claude/`는 범위 밖(보존). + ## 2026-10-28 — Command Search 예제 - 작업 브랜치: `codex/command-framework-options`. diff --git a/docs/sw/sw-process-header-design-spec.md b/docs/sw/sw-process-header-design-spec.md new file mode 100644 index 0000000..4e5394e --- /dev/null +++ b/docs/sw/sw-process-header-design-spec.md @@ -0,0 +1,327 @@ +# SW Step Header 디자인 명세 (`SWProcessHeader`) + +> 상태: `@hm-design/sw` 1차 구현 기준(2026-11-03). 코드: `packages/sw/src/components/sw-process-header/`, CSS: `packages/sw/src/theme/sw-process-header.css`, Story: `SW / Step Header`(2026-11-03 표시 명칭 변경, 컴포넌트·공개 API 이름 `SWProcessHeader`·`SWProcessBar` 등은 유지). +> 원본: Strana Main Header — `D:\Strana\Git_StranaDevDesign` HEAD `edda3cc`(Header 7173e82 + 분할 버튼 b7f412c)의 `HeaderStyles.xaml`, `HeaderBrushes.xaml`, `MainProcessBar.xaml`, `MainWorkspaceWindowView.xaml`, Hanmac `Colors.xaml`. Strana 저장소는 읽기 전용 참고이며 수정하지 않습니다. +> 승인: 2026-11-03 사용자 승인(방안 B · 설계 계획서 7절 9개 항목 권장안). + +## 1. 개요 + +SW 일반 모드의 **독립 Header**입니다. `SWHeader`(편집 메뉴 Header), `SWViewerHeader`(뷰어 모드)와 같은 층위이며 한 화면에는 Header를 하나만 둡니다. `SWHeader`·`SWViewerHeader`·`sw.css`는 바꾸지 않았습니다. + +| 공개 컴포넌트 | 역할 | +|---|---| +| `SWProcessHeader` | 49px Header 프레임: 로고 → 파일명 → 가운데(프로세스 · Layer 면) → 창 영역(도구 pill · 창 이동 · 최소화 · 최대화/복원 · 종료) | +| `SWProcessBar` | 프로세스 단계 영역(그룹 번호·이름, 단계 탭, 그룹 사이 화살표, 활성 그룹 발광). Header 밖에서는 `SWTheme` 안에서 씁니다 | +| `SWProcessSplitButton` | `tools` pill 안 화면 분할 도구(버튼 + 6칸 선택 패널, §11.3) | + +## 2. 구조와 재사용 + +``` +SWProcessHeader
49px (면 48 + 아래 1px 배경) +├─ SWHeaderHome 재사용 — 9-slice 홈 도형(Strana HomeFace/HomeBorder/HomeShadow와 같은 경로·gradient·그림자) +├─ SWHeaderDocumentTitle 재사용 — 파일명 면(DocFace / DocBorder = SW 문서 토큰과 같은 값) +├─ __center +│ ├─ SWProcessBar 신규 +│ └─ __layer 면 SW 객체 면 토큰(ObjFace / LayerFaceBorder와 같은 값) +│ └─ Layer 필드 신규 내부 — 전구는 SWHeaderLayerToggle(내부) 재사용, Strana 전구와 같은 그림 +└─ __window DocFace 면 + DocBorder 1px + ├─ SWToolGroup pill 재사용(`tools` 슬롯을 감쌈) — 테두리·면만 Strana 값으로 범위 안에서 조정 + ├─ 창 이동 재사용 — SWHeaderWindowControls(창 이동만): 줄무늬 · 아이콘 · 1.4s Hover Motion · 툴팁 + └─ 캡션 버튼 3개 신규 내부 — 최소화 · 최대화/복원 · 종료 +``` + +`SWHeaderLayerControl`·`SWHomeMenu`는 쓰지 않습니다(빈 20px 칸, 2단 타일 메뉴가 Strana와 달라서). `SWHeaderWindowControls`는 창 이동만 쓰고(`showMinimize`·`showClose` false), 최소화·최대화/복원·종료는 이 Header의 캡션 버튼입니다(2026-11-03 변경, §10). + +## 3. 레이아웃·반응형 + +Strana Grid의 별 비율을 CSS flex 비율로 옮겼습니다. 숨김 단계·More는 없습니다(Strana와 같음). 보장 폭은 1100px 이상이며 그보다 좁으면 Header 안에서 잘립니다. + +| 영역 | 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) | +| 파일명 | `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` | +| Layer 면 | `257*`, 최소 152, 최대 257, 양옆 1 | `flex: 257 1 0`, 칸 152 ~ 257, 면은 칸 안 양옆 1px(150 ~ 255, 가상 요소), 높이 48, radius 6 | +| Layer 필드 | 면 안 양옆 8, 최대 234 × 38, padding 12 / 10 | 같음(가운데 정렬) | +| 창 영역 | Auto: pill(위·왼쪽 3, 높이 42) + 창 이동 88 + 버튼 32 × 3 | 같음(약 313px, 도구 3개 기준) | +| 프로세스 그룹 | 자연 폭까지, 좁아지면 자연 폭 비율로 함께 줄어듦, 양옆 남는 폭은 같게 | 그룹 `flex: 0 1 auto`(flex-shrink가 기본 폭에 비례), 양옆 `flex: 1 1 0` | +| 단계 탭 | UniformGrid, 넓을 때 127 + 간격 1, 좁으면 0까지 | flex 한 줄 `flex: 1 1 0`(같은 폭), 크기 기준 요소 127px, 선택 탭도 함께 축소(§11.1) | + +## 4. 프로세스 영역 규칙 + +| 요소 | 규칙 | +|---|---| +| 바탕 | ProcessBase 세로 gradient, DocBorder 1px, 위 모서리 6 | +| 비활성 덮개 | 비활성 그룹 위 `--hm-sw-process-dim`. 왼쪽 남는 폭 = 첫 그룹, 오른쪽 남는 폭 = 마지막 그룹 밝기. 화살표 칸: 꼭지(앞 그룹 비활성) / 홈(뒤 그룹 비활성) | +| 활성 그룹 면 | 기운 왼쪽 변 + 둥근 아래 모서리 도형(ProcessGlowLeft), 화살표 쪽으로 사라짐, 발광 Halo + 가장자리 빛. 첫 그룹은 왼쪽 40px, 마지막 그룹은 좌우 반전·오른쪽 44px 밖으로 | +| 비활성 그룹 면 | ProcessIdleLeft 도형, Blur + 어두운 Halo + 테두리 빛, 바깥쪽에서 안쪽으로 사라짐(첫 그룹 79.8 → 198.4, 마지막 그룹 236.7 → 273.1) | +| 사다리꼴 배치(확정 2026-11-03) | 그룹 면(사다리꼴)은 프로세스 영역의 **시작(첫 그룹)과 끝(마지막 그룹)에만** 둡니다. 그 그룹이 켜져 있으면 발광 면, 아니면 비활성 면. 가운데 그룹에는 켜져 있어도 도형이 없고 밝은 바탕(덮개 없음)만 보입니다(Figma 873:17421). 그룹 수와 관계없이 같으며 `default`·`multi` 공통. 화살표·비활성 덮개·선택 상태는 그대로 | +| 화살표 | 16 × 48, 앞 그룹 활성 = On(채움 #27D9A5), 아니면 Dim(채움 25%, 선 50%) | +| 번호 / 이름 | 12px, 줄 높이 14. 번호 500 `#69C2A6`. 이름 최대 56px 줄바꿈, 활성 700 흰색 / 비활성 500 `#C3EBDF`, 폭은 늘 700 기준. 위 여백 10.67(한 줄) / 8.67(두 줄) | +| 탭 | 높이 32, padding 0 12 2.67, 16px 700 `#F0F7F5`. 모서리: 단독 16, 첫 탭 16 4 4 16, 마지막 탭 4 16 16 4, 가운데 4 | +| 탭 상태 | 기본 층 Opacity 0.7. Hover = 글자 `#FFC600`. Checked = 층 1, 바탕·그늘·광택 교체, 위 5 · 왼쪽 6 안쪽 그림자 0.75, 테두리 2px 0.25, 글자 `#FFC600`. Disabled = 층 0.5, 글자 `#64A491` | + +Strana의 2그룹(01 Modeling: Structure · Loads / 02 Analysis Result: Results)이 확정 디자인입니다. 1그룹·3그룹 이상은 렌더링되지만 디자인 미확정입니다. + +## 5. HM 규칙 적용으로 바뀐 점 (원본 대비 변경) + +| 항목 | Strana 원본 | 구현 | 근거 | +|---|---|---|---| +| 파일명 | TextBox(말줄임 없음, 편집 가능하나 반영 안 됨) | 버튼 + 말줄임, `title`로 전체 이름 | 승인 7 | +| Focus | 표시 없음(FocusVisual null) | SW 2px `#F0F7F5` outline | 승인 7 | +| 글자 외곽선 | DropShadow Blur 2 | SW `--hm-sw-header-text-outline`(14·16·18px) / `--hm-sw-header-label-outline`(10·12px). `multi`도 같음(Figma에는 Stroke 없음) | 승인 7, 2026-11-03 요청 | +| 창 이동 | 정적 장식(Motion 없음) | SWHeader 창 이동 Motion(1.4s 반복, Hover 해제 즉시 복원, 툴팁 `창 이동`, Drag 중 정지, Reduced Motion 정적) | 2026-11-03 요청(이전 승인 5의 "Motion 없음"을 대체) | +| 파일명 자간 | 0 | 0.06em(재사용한 SW 파일명 면) | 재사용 부품 | +| Layer 필드 | ToggleButton 안에 전구 ToggleButton 중첩 | 형제 button 2개(전구 · 필드), 보이는 모양 같음 | HTML은 버튼 중첩 불가 | +| 탭 3개 이상 가운데 탭 | 정의 없음(첫 탭 외 모두 오른쪽 모서리) | 모서리 4 | 일반화 | +| 테마 | 4개(Hanmac · Dark · Light · HighContrast) | Hanmac 값만 기본값, 나머지는 §7 Override | 승인 4, Light/Dark preset 금지 | +| 로고 메뉴 · Layer Popup | 있음 | 1차 범위 제외(버튼 상태·Callback만) | 승인 6 | +| 화면 분할 버튼 · 선택 Popup | 있음 | `SWProcessSplitButton`으로 추가(2026-11-03, §11). 버튼 아래 연장을 Header 끝까지 늘려 패널이 Header 아래에서 시작(Strana는 3px — 창 버튼 아래 끝을 가림) | 2026-11-03 요청 | +| 도구 아이콘 | DesignSystem Geometry | Storybook fixture(`src/sw/sw-process-header-fixture.tsx`)의 TSX 아이콘, 패키지는 `tools` 슬롯. 상태 색은 `currentColor` | 승인 8 | +| 좁은 폭의 탭 글자 · Layer 이름 | 잘림(Clip) | `…` 말줄임 | HM 말줄임 규칙(승인 7과 같은 기준) | +| 그림 자산 | XAML Geometry · DrawingBrush | 파일 대신 인라인 SVG(TSX)와 CSS — 색을 토큰으로 바꿀 수 있게 | 승인 4(토큰 Override) | +| 도구 버튼 눌림 | 노란 링 + 아이콘 `#FFC600` | 같음. `SWToolButton` pill에는 눌림 모양이 없어 이 Header 범위 안에서만 더함(`SWHeader`의 pill은 그대로) | 원본 재현 | +| 발광 · Blur | WPF DropShadow 30 / 15, Blur 10 / 8 / 2 | CSS `drop-shadow` 30 / 15px, `blur` 5 / 4 / 1px(반경 → 표준편차) | 근사값 — 1920 캡처 비교로 확인 | + +## 6. 토큰 + +새 토큰은 `--hm-sw-process-*`이며 `sw-process-header.css`의 가장 바깥 SW 범위(`sw-footer.css`와 같은 규칙)에 선언합니다. 값이 SW 토큰과 같은 곳은 SW 토큰을 그대로 읽습니다(파일명 면·테두리, 객체 면·테두리, 속성 필드 면·그림자, Hover gradient, 선택 `#FFC600`, 제목·글자, 창 이동 줄무늬, 창 버튼 글리프·Hover 배경). + +전체 목록과 Strana 원본 키는 §7 표의 왼쪽 두 열을 봅니다. + +## 7. Strana 테마 → 토큰 Override 대응표 + +Hanmac 열이 기본값입니다. 다른 테마는 Header의 `style`(가장 바깥 SW 범위)에 같은 토큰을 지정합니다. 예: `style={{ "--hm-sw-header-selected-color": "#FFC833" }}`. Strana ARGB `#AARRGGBB`는 CSS `rgba()`로 바꿉니다. 아래 표는 Strana `Styles/Theme/*/Colors.xaml`(HEAD `edda3cc`)에서 생성했습니다. + + +| 토큰 | Strana `Color.Header.*` | Hanmac(기본값) | Dark | Light | HighContrast | +|---|---|---|---|---|---| +| `--hm-sw-header-background` | Background | `#060606` | `#0b0d11` | `#b9c7d7` | `#000000` | +| `--hm-sw-header-text-color` | Text | `#f6f4f2` | `#f4f7fa` | `#1c2430` | `#ffffff` | +| `--hm-sw-header-title-color · --hm-sw-process-tab-text` | Title | `#f0f7f5` | `#f4f7fa` | `#1c2430` | `#ffffff` | +| `--hm-sw-header-selected-color` | Selected | `#ffc600` | `#ffc833` | `#0e54c6` | `#ffff00` | +| `--hm-sw-header-text-outline-color` | TextOutline | `#000000` | `#000000` | `#ffffff` | `#000000` | +| `--hm-sw-header-hover-border-gradient` | HoverBorderDark | `#a67d03` | `#a67d03` | `#2f5faf` | `#ffff00` | +| ″ | HoverBorder | `#f2cc0f` | `#f2cc0f` | `#5c88d6` | `#ffff00` | +| ″ | HoverBorderLight | `#fadd4f` | `#fadd4f` | `#8fb0e8` | `#ffff00` | +| ″ | HoverBorderDeep | `#926e03` | `#926e03` | `#2f5faf` | `#ffff00` | +| `--hm-sw-header-document-background` | DocFaceEdge | `#081813` | `#12151a` | `#c6d4e3` | `#000000` | +| ″ | DocFaceDeep | `#09271e` | `#181c23` | `#d0dce8` | `#000000` | +| ″ | DocFaceMid | `#0b3227` | `#1e232b` | `#d9e3ef` | `#000000` | +| ″ | DocFaceLight | `#0e3d2f` | `#262c36` | `#e3ebf4` | `#000000` | +| ″ | DocFaceLow | `#0a2b21` | `#1a1e25` | `#cfdae6` | `#000000` | +| `--hm-sw-header-document-border-gradient · --hm-sw-process-base-border-gradient` | DocBorderTop | `#0e4c3a` | `#3a4250` | `#ffffff` | `#ffffff` | +| ″ | DocBorderDark | `#041611` | `#0b0d11` | `#afbecd` | `#ffffff` | +| ″ | DocBorderBottom | `#18503f` | `#3f4856` | `#9daec0` | `#ffffff` | +| `--hm-sw-header-document-border-color` | DocBorderTop | `#0e4c3a` | `#3a4250` | `#ffffff` | `#ffffff` | +| `--hm-sw-header-object-background` | ObjFaceEdge | `#144838` | `#1c2027` | `#d3dce6` | `#000000` | +| ″ | ObjFaceLow | `#1a4536` | `#20252d` | `#dae2eb` | `#000000` | +| ″ | ObjFaceMid | `#1f4134` | `#22272f` | `#dde5ed` | `#000000` | +| ″ | ObjFaceDeep | `#10392d` | `#1a1e25` | `#d3dce6` | `#000000` | +| ″ | ObjFaceLight | `#296b55` | `#353d48` | `#f0f4f8` | `#000000` | +| ″ | ObjFaceUpper | `#1f5a47` | `#2a3039` | `#e7edf3` | `#000000` | +| ″ | ObjFaceTop | `#124133` | `#1e232b` | `#d8e0ea` | `#000000` | +| `--hm-sw-header-object-border-gradient` | ObjBorderTop | `#135643` | `#313843` | `#c3cfdb` | `#ffffff` | +| ″ | ObjBorderDeep | `#0e3d2f` | `#1e232b` | `#afbecd` | `#ffffff` | +| ″ | ObjBorderHighlight | `#34daaa` | `#74c4b4` | `#5c88d6` | `#ffffff` | +| ″ | ObjBorderDark | `#08241c` | `#12151a` | `#afbecd` | `#ffffff` | +| ″ | ObjBorderBottom | `#1f5e4b` | `#2e353f` | `#c3cfdb` | `#ffffff` | +| `--hm-sw-header-property-background` | PropertyFace | `rgba(0, 0, 0, 0.451)` | `rgba(0, 0, 0, 0.451)` | `rgba(255, 255, 255, 0.349)` | `rgba(0, 0, 0, 1)` | +| ″ | PropertyFaceLight | `rgba(0, 0, 0, 0.4)` | `rgba(0, 0, 0, 0.4)` | `rgba(255, 255, 255, 0.302)` | `rgba(0, 0, 0, 1)` | +| `--hm-sw-header-property-shadow` | PropertyShadow | `#09271e` | `#000000` | `#5a6878` | `#000000` | +| `--hm-sw-header-stripe-color` | Stripe | `rgba(28, 106, 81, 0.322)` | `rgba(63, 72, 86, 0.322)` | `rgba(47, 95, 175, 0.239)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-header-window-icon-color` | WindowIcon | `#adb0af` | `#adb0af` | `#4a5868` | `#ffffff` | +| `--hm-sw-header-window-icon-hover-color` | WindowIconHover | `#ffffff` | `#ffffff` | `#1c2430` | `#000000` | +| `--hm-sw-header-window-button-hover-background` | WindowButtonHover | `rgba(0, 0, 0, 0.2)` | `rgba(0, 0, 0, 0.2)` | `rgba(0, 0, 0, 0.122)` | `#ffff00` | +| `--hm-sw-header-layer-icon-hover-background` | LayerIconHover | `rgba(0, 0, 0, 0.251)` | `rgba(0, 0, 0, 0.251)` | `rgba(0, 0, 0, 0.149)` | `rgba(255, 255, 255, 0.251)` | +| `--hm-sw-process-base-background` | ProcessBaseEdge | `#0c5941` | `#1c2027` | `#c6d4e3` | `#000000` | +| ″ | ProcessBaseLight | `#19bf8d` | `#4a5260` | `#f0f4f8` | `#000000` | +| ″ | ProcessBaseMid | `#109069` | `#313843` | `#d9e3ef` | `#000000` | +| `--hm-sw-process-dim` | ProcessDim | `rgba(0, 68, 48, 0.902)` | `rgba(11, 13, 17, 0.902)` | `rgba(207, 218, 230, 0.902)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-number-color` | ProcessNumber | `#69c2a6` | `#74c4b4` | `#2c5a88` | `#ffffff` | +| `--hm-sw-process-group-color` | ProcessGroupText | `#c3ebdf` | `#c9d3de` | `#4a5868` | `#ffffff` | +| `--hm-sw-process-group-active-color` | ProcessGroupActive | `#ffffff` | `#ffffff` | `#1c2430` | `#ffff00` | +| `--hm-sw-process-glow-face-base` | ProcessGlowFaceTop | `#063b2b` | `#1a1e25` | `#e3ebf4` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessGlowFaceFade | `rgba(13, 101, 74, 0.051)` | `rgba(42, 48, 57, 0.051)` | `rgba(217, 227, 239, 0.051)` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessGlowInner | `rgba(24, 134, 101, 0.702)` | `rgba(63, 111, 104, 0.702)` | `rgba(92, 136, 214, 0.702)` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessGlowInnerFade | `rgba(24, 134, 101, 0)` | `rgba(63, 111, 104, 0)` | `rgba(92, 136, 214, 0)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-glow-face-rim` | ProcessGlowRim | `#247a60` | `#3f6f68` | `#5c88d6` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessGlowFaceFade | `rgba(13, 101, 74, 0.051)` | `rgba(42, 48, 57, 0.051)` | `rgba(217, 227, 239, 0.051)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-glow-face-sheen` | ProcessGlowSheenTop | `#20322c` | `#1e232b` | `#ffffff` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessGlowSheen | `#3dc79d` | `#74c4b4` | `#8fb0e6` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessGlowSheenFade | `rgba(61, 199, 157, 0.051)` | `rgba(116, 196, 180, 0.051)` | `rgba(143, 176, 230, 0.051)` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessGlowTint | `rgba(0, 95, 66, 0.251)` | `rgba(42, 48, 57, 0.251)` | `rgba(92, 136, 214, 0.251)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-glow-halo (× 0.85)` | ProcessGlowHalo | `#58b599` | `#74c4b4` | `#5c88d6` | `#000000` | +| `--hm-sw-process-glow-edge (× 0.65)` | ProcessGlowEdge | `#58b599` | `#74c4b4` | `#5c88d6` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-idle-face` | ProcessIdleInset | `rgba(0, 0, 0, 0.502)` | `rgba(0, 0, 0, 0.502)` | `rgba(0, 0, 0, 0.149)` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessIdleInsetFade | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessIdleShade | `rgba(23, 42, 26, 0.251)` | `rgba(0, 0, 0, 0.2)` | `rgba(47, 74, 110, 0.078)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-idle-halo (× 0.8)` | ProcessIdleHalo | `#11694f` | `#3a4656` | `#8fb0e6` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-idle-rim` | ProcessIdleRim | `rgba(4, 128, 90, 0.322)` | `rgba(116, 196, 180, 0.302)` | `rgba(255, 255, 255, 0.4)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-arrow-fill` | ProcessArrowFill | `#27d9a5` | `#74c4b4` | `#5c88d6` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-arrow-dim-fill` | ProcessArrowDimFill | `#abc7bf` | `#c9d3de` | `#8fa3b8` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-arrow-fill-fade` | ProcessArrowFillFade | `rgba(16, 109, 81, 0)` | `rgba(42, 48, 57, 0)` | `rgba(217, 227, 239, 0)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-arrow-edge` | ProcessArrowEdge | `#00432e` | `#1a1e25` | `#afbecd` | `#ffffff` | +| `--hm-sw-process-arrow-mid` | ProcessArrowMid | `#1c6a51` | `#4a5260` | `#5c88d6` | `#ffffff` | +| `--hm-sw-process-arrow-light` | ProcessArrowLight | `#3d9c7e` | `#5e8f87` | `#8fb0e6` | `#ffffff` | +| `--hm-sw-process-arrow-shadow` | ProcessArrowShadow | `rgba(0, 0, 0, 0.502)` | `rgba(0, 0, 0, 0.502)` | `rgba(255, 255, 255, 0.502)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-shade · -shade-mid · -shade-fade` | ProcessShade | `rgba(4, 34, 25, 0.878)` | `rgba(11, 13, 17, 0.878)` | `rgba(175, 190, 205, 0.878)` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessShadeMid | `rgba(4, 34, 25, 0.706)` | `rgba(11, 13, 17, 0.706)` | `rgba(175, 190, 205, 0.706)` | `rgba(0, 0, 0, 0)` | +| ″ | ProcessShadeFade | `rgba(4, 34, 25, 0)` | `rgba(11, 13, 17, 0)` | `rgba(175, 190, 205, 0)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-glow` | TabGlow | `rgba(31, 148, 113, 0.702)` | `rgba(74, 82, 96, 0.702)` | `rgba(255, 255, 255, 0.702)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-depth` | TabDepthFade | `rgba(0, 61, 41, 0)` | `rgba(26, 30, 37, 0)` | `rgba(198, 212, 227, 0)` | `rgba(0, 0, 0, 1)` | +| ″ | TabDepth | `rgba(0, 61, 41, 0.8)` | `rgba(26, 30, 37, 0.8)` | `rgba(198, 212, 227, 0.8)` | `rgba(0, 0, 0, 1)` | +| `--hm-sw-process-tab-shade` | TabShade | `rgba(0, 0, 0, 0.4)` | `rgba(0, 0, 0, 0.4)` | `rgba(0, 0, 0, 0.149)` | `rgba(0, 0, 0, 0)` | +| ″ | TabShadeFade | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-sheen` | TabSheen | `rgba(74, 255, 196, 0.502)` | `rgba(116, 196, 180, 0.502)` | `rgba(255, 255, 255, 0.502)` | `rgba(0, 0, 0, 0)` | +| ″ | TabSheenFade | `rgba(74, 255, 196, 0)` | `rgba(116, 196, 180, 0)` | `rgba(255, 255, 255, 0)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-border-gradient` | TabBorder | `#001e15` | `#0b0d11` | `#9daec0` | `#ffffff` | +| ″ | TabStrokeHighlight | `#17a77b` | `#4a5260` | `#ffffff` | `#ffffff` | +| ″ | TabStrokeLow | `#0b533d` | `#262c36` | `#9daec0` | `#ffffff` | +| `--hm-sw-process-tab-ring` | TabRing | `rgba(0, 0, 0, 0.251)` | `rgba(0, 0, 0, 0.251)` | `rgba(255, 255, 255, 0.251)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-active-base` | TabActiveBase | `rgba(0, 0, 0, 0.4)` | `rgba(0, 0, 0, 0.4)` | `rgba(255, 255, 255, 0.4)` | `rgba(0, 0, 0, 1)` | +| ″ | TabActiveBaseDeep | `rgba(0, 0, 0, 0.451)` | `rgba(0, 0, 0, 0.451)` | `rgba(255, 255, 255, 0.451)` | `rgba(0, 0, 0, 1)` | +| `--hm-sw-process-tab-active-shade` | TabActiveShade | `rgba(0, 0, 0, 0.6)` | `rgba(0, 0, 0, 0.6)` | `rgba(92, 136, 214, 0.2)` | `rgba(0, 0, 0, 0)` | +| ″ | TabShadeFade | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-active-sheen` | TabActiveSheenFade | `rgba(122, 206, 179, 0)` | `rgba(116, 196, 180, 0)` | `rgba(92, 136, 214, 0)` | `rgba(0, 0, 0, 0)` | +| ″ | TabActiveSheen | `rgba(122, 206, 179, 0.102)` | `rgba(116, 196, 180, 0.102)` | `rgba(92, 136, 214, 0.102)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-inset (끝은 투명)` | TabInset | `rgba(0, 0, 0, 0.659)` | `rgba(11, 13, 17, 0.659)` | `rgba(175, 190, 205, 0.659)` | `rgba(0, 0, 0, 0)` | +| ″ | TabInsetFade | `rgba(0, 0, 0, 0)` | `rgba(11, 13, 17, 0)` | `rgba(175, 190, 205, 0)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-disabled-shade` | TabShadeFade | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | `rgba(0, 0, 0, 0)` | +| ″ | TabDisabledShade | `rgba(0, 0, 0, 0.8)` | `rgba(0, 0, 0, 0.8)` | `rgba(0, 0, 0, 0.2)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-disabled-sheen` | TabDisabledSheen | `rgba(130, 181, 164, 0.502)` | `rgba(128, 142, 156, 0.502)` | `rgba(255, 255, 255, 0.502)` | `rgba(0, 0, 0, 0)` | +| ″ | TabActiveSheenFade | `rgba(122, 206, 179, 0)` | `rgba(116, 196, 180, 0)` | `rgba(92, 136, 214, 0)` | `rgba(0, 0, 0, 0)` | +| `--hm-sw-process-tab-disabled-text` | TabDisabledText | `#64a491` | `#6d7784` | `#8a97a6` | `#a0a0a0` | +| `--hm-sw-process-layer-border-gradient` | PropertyBorder | `#08251c` | `#12151a` | `#afbecd` | `#ffffff` | +| ″ | ObjBorderDeep | `#0e3d2f` | `#1e232b` | `#afbecd` | `#ffffff` | +| ″ | LayerFieldBorderHighlight | `#1e7c60` | `#4a5260` | `#ffffff` | `#ffffff` | +| ″ | ObjBorderDark | `#08241c` | `#12151a` | `#afbecd` | `#ffffff` | +| ″ | ToolBorderBottom | `#051612` | `#0b0d11` | `#9daec0` | `#ffffff` | +| `--hm-sw-process-pill-background` | PillFaceTop | `rgba(0, 0, 0, 0.851)` | `rgba(0, 0, 0, 0.851)` | `rgba(224, 228, 232, 0.851)` | `rgba(0, 0, 0, 1)` | +| ″ | PillFace | `rgba(0, 0, 0, 0.8)` | `rgba(0, 0, 0, 0.8)` | `rgba(246, 249, 252, 0.8)` | `rgba(0, 0, 0, 1)` | +| `--hm-sw-process-pill-border-gradient` | PillBorder | `#092920` | `#1a1e25` | `#afbecd` | `#ffffff` | +| ″ | PillBorderBottom | `#1c6f56` | `#313843` | `#c3cfdb` | `#ffffff` | +| `--hm-sw-process-tool-border-gradient` | PropertyBorder | `#08251c` | `#12151a` | `#afbecd` | `#ffffff` | +| ″ | ToolBorderHighlight | `#208769` | `#4a5260` | `#ffffff` | `#ffffff` | +| ″ | ObjBorderDark | `#08241c` | `#12151a` | `#afbecd` | `#ffffff` | +| ″ | ToolBorderBottom | `#051612` | `#0b0d11` | `#9daec0` | `#ffffff` | +| `--hm-sw-process-tool-icon-color` | Icon | `#b6d0c9` | `#c9d3de` | `#26303c` | `#ffffff` | +| `--hm-sw-process-close-hover-background` | CloseHover | `#e40000` | `#e40000` | `#e40000` | `#ff0000` | +| `--hm-sw-process-close-icon-hover` | CloseIconHover | `#ffffff` | `#ffffff` | `#ffffff` | `#ffffff` | +| `--hm-sw-process-tool-open-background` | ToolOpenEdge | `#081813` | `#0b0d11` | `#dce4ed` | `#000000` | +| ″ | ToolOpenMid | `#0b3227` | `#171b21` | `#e8eef5` | `#000000` | +| ″ | ToolOpenCenter | `#0e3d2f` | `#1e232b` | `#f3f6fa` | `#000000` | +| `--hm-sw-process-tool-open-edge` | ToolOpenEdge | `#081813` | `#0b0d11` | `#dce4ed` | `#000000` | +| `--hm-sw-process-split-panel-background` | SplitPanelFace | `#082018` | `#14171c` | `#ffffff` | `#000000` | +| `--hm-sw-process-split-cell-pane` | SplitCellPane | `#1b6b53` | `#3a4656` | `#e3ebf4` | `#000000` | +| `--hm-sw-process-split-icon-pane` | IconCell | `#003425` | `#12151a` | `#afbecd` | `#000000` | + +그라데이션 토큰은 여러 키의 조합입니다. 같은 위치(stop)에 테마 값을 넣어 토큰 전체를 지정합니다. 다음 키는 토큰이 아닙니다: `HomeFaceBottom`, `HomeFaceDeep`, `HomeFaceMid`, `HomeFaceTop`, `HomeBorderTop`, `HomeBorderLight`, `HomeBorderDark`, `HomeBorderBottom`, `HomeShadow`, `IconOutline`, `WindowMoveIcon`, `LayerBulbBase`, `LayerBulbBar`, `LayerBulbOnLight`, `LayerBulbOnMid`, `LayerBulbOnDark`, `LayerBulbOffLight`, `LayerBulbOffMid`, `LayerBulbOffDark`(SW 홈·전구·창 이동 그림 자산 — 테마 Override 대상 아님, Hanmac 그림), `IconScreen`, `IconScreenDeep`, `IconMonitorScreen`(Strana 도구 아이콘 전용 — 도구 아이콘은 소비 측이 그림). + + +## 8. 검증 + +2026-11-03 1차 구현(Chromium, Storybook 6024). + +| 항목 | 결과 | +|---|---| +| 폭 측정(파일명 / 프로세스 / 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 | +| 기존 `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는 확인하지 않았습니다. + +## 9. `appearance="multi"` — StrAna-SW 다중 Step (2026-11-03) + +원본: Figma StrAna-SW `4y8lTK1E1hAGYiMcYZwuNe`, 노드 `873:16481` · `873:16254` · `873:17421`(각 1920 × 49). Story: `SW / Step Header › Step Header`(Step Header 섹션 = `default` + Multi Step 섹션 = 세 시안, 노드 ID는 화면에 표시하지 않고 `data-figma-node`로만 유지), `Responsive`, 검증용(숨김) `Multi Step`(세 시안 1920) · `화면 분할` · `Fixed Widths` · `Process Bar` · `Many Groups` · `Many Steps` · `States` · `Theme Override`. + +### 9.1 시안 비교 + +세 시안은 **같은 구조의 상태 차이**입니다. 그래서 별도 컴포넌트 없이 `SWProcessHeader` / `SWProcessBar`의 `appearance="multi"` 하나로 구현했습니다. + +| 시안 | 파일명 면 | 켜진 그룹 · 선택 단계 | 그룹 상태 | +|---|---|---|---| +| 873:16481 | 파일 없음: `프로젝트 생성 전` 10px 700 `#AEDBCC` 오른쪽 위 | 01 데이터 구축 · 데이터입력 | 2·3 미진행(70%), 4·5 비활성(회색) | +| 873:16254 | 파일명 14px 700 `#F0F7F5`, 말줄임. 위치·여백은 SWHeader 우선(글자 x 70, padding 5 / 12, 자간 0.84px) | 01 데이터 구축 · 데이터입력 | 같음 | +| 873:17421 | 파일 없음 | 03 구조물 상세 및 계획 · 본체계획 | 1·2 진행 완료(덮개 + 밝은 탭), 4·5 비활성 | + +### 9.2 `default`와 공유한 것 / 다른 것 + +- **사다리꼴 배치:** 시작·끝 그룹에만(§4 확정 규칙). 873:17421처럼 가운데 그룹이 켜지면 도형 없이 밝은 바탕만 보입니다(2026-11-03 수정 — 이전 구현은 켜진 가운데 그룹에도 발광 면을 그렸음). +- **같은 구현(값 대조 일치):** 프로세스 바탕(ProcessBase gradient · DocBorder · 위 모서리 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 원본)에만 둡니다. + +### 9.3 넘침 처리 — 확정 정책(2026-11-03 사용자 승인) + +| 항목 | 결정 | 근거 | +|---|---|---| +| 그룹 폭 | 자연 폭(제목·탭 중 넓은 쪽 + 좌우 10)까지, 좁으면 자연 폭 비율로 함께 축소. 남는 폭은 양쪽에 같게 | `default`와 같은 규칙 | +| 탭 | 글자 폭 + 25, 좁으면 선택 탭 포함 글자 폭 비율로 축소 + `…`(2026-11-03 개정) | 옆 탭이 비정상적으로 좁아지지 않게 | +| 화살표 · 그룹 사이 36 | 고정 | Figma | +| 실제 넘침 | 그룹 접기·좌우 스크롤·More 메뉴를 두지 않음. 더 좁으면 영역 안에서 잘림 | 확정(Figma에 정의 없음) | + +측정(1920): 5그룹 11탭은 말줄임 없음. 1366에서는 11탭 중 10개, 8그룹 · 16탭(1920)에서는 15개가 말줄임됩니다. 겹침 · 영역 밖 탭 0. (개정 후 1366에서는 선택 탭도 다른 탭과 함께 줄어 `…`가 될 수 있습니다.) + +### 9.4 Figma와 남은 차이 + +- 873:17421의 켜진 그룹 미선택 탭은 70% · `#0C3125` · `rgba(76,167,138,.8)`, 873:16481은 100% · `#0A5940` · `rgba(68,178,143,.8)`로 시안끼리 다릅니다. 873:16481 값으로 통일했습니다. +- 873:17421의 선택 탭에만 있는 안쪽 2px 링(`rgba(0,0,0,.25)`)은 넣지 않았습니다(873:16481 기준). +- 비활성 글자는 `#87AD9D`(1개 탭) / `#7D9F91`(3개 탭) 두 값 중 `#7D9F91`로 통일했습니다. +- 873:17421의 `선형입력` 위치(599)는 873:16481(592.8)과 다릅니다. 구현은 그룹 간격 36 규칙(592)입니다. +- Figma 로고 자리는 글자 `LOGO`이며, Story는 임시 로고 이미지입니다(로고·파일명 글자는 SWHeader와 같은 크기·위치). +- ~~창 이동 아이콘 밝기 차이~~: SWHeader 창 이동 자산으로 바꾼 뒤 Figma와 같은 밝기(2026-11-03 해소). +- 글자 외곽선: Figma multi 시안의 글자에는 Stroke가 없으나 SW 규칙에 맞춰 외곽선을 적용했습니다(의도된 차이, §10). +- 장치 배율 1.5에서 1px 테두리 반올림으로 창 영역이 366.3px로 측정됩니다(Figma 367). + +## 10. SWHeader 스타일 정합 (2026-11-03) + +`SWHeader`의 실제 구현(sw.css)과 브라우저 렌더를 기준으로 맞췄습니다. `SWHeader`·`SWViewerHeader`·`sw.css`는 바꾸지 않았고, 모두 `default`·`multi` 공통입니다. + +| 항목 | SWHeader | Process Header(적용 결과) | 구현 | +|---|---|---|---| +| 글자 Stroke | 14·16px 검정 1.3px, 12px 1px(16방향 text-shadow), 말줄임 글자는 `overflow-clip-margin` 2px | 같음: 파일명·탭(선택·비선택·비활성)·Layer 값 1.3px, 그룹 제목·번호·Layer 라벨·파일 없음 표시 1px | SW 토큰 재사용. `multi`의 `text-shadow: none` 제거, 탭·제목 말줄임 글자는 clip margin 공통 | +| 창 이동 | 88 × 48, 줄무늬, 아이콘 → Hover 시 창·커서 1.4s 반복 + 툴팁, Drag 중 정지, 해제 즉시 복원, Reduced Motion 정적 | 같음(SWHeader 부품 그대로). `multi`만 Figma 위치로 줄무늬 82 × 39(3, 5)·아이콘 3px 위 | `SWHeaderWindowControls`(창 이동만) 재사용, 정적 그림·CSS·토큰 2개 삭제 | +| 도구 버튼 Hover | 노란 gradient 링 + 노란 아이콘(`hoverIcon` 또는 색) | 같음. Default fixture 아이콘은 `currentColor`, Multi Figma 아이콘은 `#B6D0C9 → #FFC600` Hover 그림(SW 인쇄 버튼 방식) | SWToolButton `hoverIcon`, 눌림(pressed)도 링 + 노란 아이콘 | +| 로고 · 파일명 · 탭 Hover | 로고 금색 테두리, 파일명 글자 `#FFC600` | 같음, 탭 글자 `#FFC600`, 비활성 탭은 변화 없음 | 재사용 | +| Layer 필드 Hover | 필드 노란 링 + 글자 `#FFC600`, 전구는 어두운 배경 | 같음 | 재사용 | +| 최소화 · 최대화 Hover | 배경 `rgba(0,0,0,.2)` + 흰 글리프(노랑 아님) | 같음 | SW 토큰 | +| 종료 Hover | `#E40000` + 흰 X | 같음(유지) | 확정 디자인 | +| Focus | 2px `#F0F7F5`, -2px | 같음(탭·도구·캡션 버튼) | SW 공통 규칙 | +| 선택 · Disabled | — | 선택 탭 `#FFC600`(Hover와 같은 색이지만 면이 다름), Disabled는 Hover·Focus 없음 | 기존 확정 디자인 | + +## 11. 선택 Step 표시 통일 · 아이콘 · 화면 분할 (2026-11-03) + +### 11.1 선택 Step 폭(두 appearance 공통, 2026-11-03 개정) + +- **개정:** 선택된 탭도 다른 탭과 같은 비율로 줄어듭니다(이전 "선택 탭 전체 이름 유지" 폐기 — 선택 탭에 폭을 먼저 주느라 옆 탭이 24 · 27px까지 좁아지던 문제). 공간이 충분하면 전체 이름, 부족하면 모든 탭이 기본 폭 비율로 줄고 `…`입니다(`default` 같은 폭, `multi` 글자 폭 비례). 선택이 바뀌어도 탭 폭은 그대로입니다. +- 그룹 최소 폭 = 좌우 여백 + (`default`만) 이름 + 모든 탭 여백 + 간격(측정). 그 위에서는 그룹이 비례로 줄고, 아래로는 줄지 않아 겹치지 않습니다. +- 탭 배치는 두 appearance 모두 flex 한 줄입니다(이전 `default`의 grid 대체). 넓을 때 `default` 탭은 127 균등 그대로입니다. +- 측정(개정 후, 고정 폭, `*` = 선택): `default` 1100 `*61 61 | 45`(이전 `*98 25 | 45`), 1366 `*124 124 | 124`, 1920 · 2560 `*127 127 | 127`(변화 없음). `multi` 1100 셋째 그룹 `*34 34 37 37`(이전 `*77 25 25 25`), 1366 `*54 54 61 61`(이전 `*77 47 53 53`), 1920 · 2560 `*77 77 89 89`(변화 없음). 겹침 0. 그룹 최소 폭의 합이 프로세스 영역보다 크면 영역 끝에서 잘립니다. + +### 11.2 도구 아이콘이 사라지던 문제 + +- 원인: `sw.css`의 `:is(.hm-sw-header__action, …):hover:not(:disabled) > .hm-sw-header__icon { visibility: hidden }`는 Hover 그림(`hoverIcon`)이 있다고 가정합니다. `hoverIcon` 없이 `currentColor` 아이콘만 준 `SWToolButton`은 Hover 순간 아이콘이 비었습니다(Default 도구 3개). +- 해결(2026-11-03 공통 수정): `SWToolButton`(pill)이 `hm-sw-header__icon` class를 `hoverIcon`이 있을 때만 기본 아이콘에 붙입니다. `hoverIcon`이 없으면 기본 아이콘이 Hover에도 그대로 보이고 색만 `#FFC600`입니다. `SWHeader`의 `tools`도 같은 컴포넌트라 함께 해결되었고, 이 Header 범위의 보완 CSS 규칙은 삭제했습니다(`sw.css` 변경 없음). 눌림은 Hover 그림이 있을 때만 그 그림으로 바뀝니다. Disabled·열림에서도 아이콘이 유지되며 크기 24 × 24로 같습니다. + +### 11.3 화면 분할(`SWProcessSplitButton`) + +원본: Strana `ViewportSplitSelector.xaml`, `Styles/Controls/ViewportSplitStyles.xaml`, `HeaderStyles.xaml`(ToolMenu · ToolOpenTail · ToolMenuStep · Icon.ToolLayout), MainWorkspaceWindowView 분할 Popup(HEAD `edda3cc`). + +| 항목 | Strana | 구현 | +|---|---|---| +| 버튼 | 도구 버튼 40 × 38, 아이콘 = 현재 분할 모양 | `SWToolButton` pill + 분할 아이콘(Strana Geometry, 틀은 `currentColor`) | +| 열림 | 면 ToolOpenFace, 금색 위·양옆, 아래 열림, Hover 링 없음, 아이콘 노랑 | 같음 | +| 연장 | 버튼 아래 3px(버튼 아래 1px 겹침) | 버튼 아래 1px 겹침, 길이는 Header 끝까지(최소 3px) — 패널이 창 버튼을 가리지 않게 | +| 패널 | 233 × 36, 면 `#082018`, 금색 양옆·아래, radius 0 2 2 2, 그림자 0 2 4 60% | 같음, Header 바로 아래·버튼 왼쪽, 화면 밖이면 좌우만 보정(SW 필드 Dropdown 위치 훅) | +| 위 선 | 버튼 오른쪽 끝 ~ 패널 끝 금색 | 같음 | +| 칸 | 38 × 34 × 6(Type01~06), Hover·선택 = 창 노랑, Disabled 40% | 38.5 × 34(233 / 6), 같은 그림·상태. 틀 상자는 32 × 22 정수(Strana 31.6 × 21.73, 그림 29.6 × 19.73 그대로) | +| 동작 | Popup StaysOpen=False(바깥 클릭 닫힘), 칸 선택 시 열린 채 | 같음 + Escape 닫고 버튼으로, Tab 이탈 닫힘, ArrowDown 열기, 방향키 선택(radiogroup) | + +값은 Props(`value`, `open`), 동작은 Callback(`onValueChange`, `onOpenChange`)이며 창을 실제로 나누는 일은 호스트가 합니다. `tools` 안에 두므로 버튼 순서는 호스트가 정하고 `default`·`multi`가 같은 컴포넌트를 씁니다. Figma 다중 Step 시안 3개에는 화면 분할 버튼이 없어 해당 Story에는 넣지 않았습니다(`Step Header` 페이지의 Step Header 섹션과 검증용 `화면 분할` Story에서 Default·Multi Step을 확인). + +- **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줄 · 아래 밝은 줄 표시 확인. diff --git a/packages/sw/ai/AI_USAGE.md b/packages/sw/ai/AI_USAGE.md index 48eb956..a6ebab7 100644 --- a/packages/sw/ai/AI_USAGE.md +++ b/packages/sw/ai/AI_USAGE.md @@ -48,15 +48,16 @@ SW 패키지에는 Header·Popup 계열이 두 벌 있습니다. 화면마다 ** | 아이콘 | 패키지 자산(필드 안 아이콘) | 프로그램 아이콘 16px, 직접 그린 아이콘은 `SWViewerIcon`으로 감쌈 | | 범위 | 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, [DESIGN_RULES.md](./DESIGN_RULES.md) §1~10·§12·§13, [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 1~6·8·10, [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) | +| 읽을 문서 | [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) | +- **프로세스 단계 Header**(로고 · 파일명 · 01 Modeling → 02 Analysis Result 같은 단계 탭 · Layer 필드 · 도구 pill · 최소화 · 최대화/복원 · 종료, Strana Main Header) 요청은 일반 모드의 독립 Header `SWProcessHeader`입니다(카탈로그 §11, DESIGN_RULES §14, PATTERNS 12). 그룹·단계가 많은 다중 Step 화면(StrAna-SW)은 같은 컴포넌트의 `appearance="multi"`입니다(별도 컴포넌트를 만들지 않음). `SWHeader` 대신 쓰며 같은 화면에 함께 두지 않습니다. - **하단 명령 막대(Footer)** 요청은 일반 모드입니다: [FOOTER.md](./FOOTER.md) §1 순서로 `SWFooter`가 있는지 확인하고, 명령·분류·보조기능·문구·아이콘만 Props로 바꿉니다. Footer 외형을 새로 그리지 않습니다. - **일반 모드 Header에 도구 막대를 더한 구성**(로고 옆 파일 열기 + Model/Layout·Fit·2.5D 버튼을 `SWHeader`에 추가)은 **일반 모드**입니다: `SWHeader`의 `documentOpenPlacement`·`leading`·`tools` + `SWToolButton` / `SWToolGroup`(PATTERNS 6, 예제 F). "뷰어"·"도면 보기" 화면을 요청받았다면 이 구성이 아니라 뷰어 모드(`SWViewer*`)를 씁니다. - 모드 진입·종료, 파일 처리, 도면 렌더링, 메뉴 명령 실행은 어느 모드든 **프로그램 담당**입니다. ### 0.3 모드 혼합 금지 -- **한 화면에 Header 하나.** `SWHeader`와 `SWViewerHeader`를 같은 화면에 함께 두지 않습니다. 두 모드를 오가는 프로그램은 상태를 유지한 채 Header 컴포넌트만 바꿉니다(VIEWER.md §1). +- **한 화면에 Header 하나.** `SWHeader`·`SWProcessHeader`·`SWViewerHeader` 중 둘 이상을 같은 화면에 함께 두지 않습니다. 두 모드를 오가는 프로그램은 상태를 유지한 채 Header 컴포넌트만 바꿉니다(VIEWER.md §1). - **뷰어 화면:** `SWHeader`에 뷰어 메뉴를 넣거나 `SWHeader`·`SWPopup`을 뷰어처럼 재스타일하지 않습니다. 뷰어 팝업은 `SWViewerPanel`입니다. 예외: 창 최소화·닫기 버전은 `SWViewerHeader`의 `trailing` 맨 끝에 `SWHeaderWindowControls`(`showWindowMove={false}`)를 그대로 둡니다(VIEWER.md §2.1, 2026-10-21). - **일반 화면:** `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`과 패널 컨트롤(`SWViewerSection` …)을 쓰지 않습니다. - **로고(홈) 메뉴는 SW 일반 모드 전용:** `SWHomeMenu`·`SWHomeMenuSection`·`SWHomeMenuItem`·`SWRecentFiles`·`useSWHomeMenuClose`와 `SWHeader`의 `homeMenu`·`homeMenuOpen`·`defaultHomeMenuOpen`·`onHomeMenuOpenChange`는 일반 모드 `SWHeader` 화면에서만 씁니다. 뷰어 화면에는 홈 메뉴를 만들지 않고(`SWViewerHeader` 옆·`custom` 항목·`SWViewerPanel` 안에 넣거나 비슷하게 다시 만들지 않음), 뷰어 Header와 기능은 그대로 둡니다. diff --git a/packages/sw/ai/COMPONENT_CATALOG.md b/packages/sw/ai/COMPONENT_CATALOG.md index 34971fd..5dc9f63 100644 --- a/packages/sw/ai/COMPONENT_CATALOG.md +++ b/packages/sw/ai/COMPONENT_CATALOG.md @@ -9,6 +9,7 @@ - **검증 표시:** ✅ = 2026-10-19 독립 소비 프로젝트(Tarball 설치)에서 브라우저 검증, ◻ = 소비 검증 범위 밖(Storybook 검증 기록만 있음). - **먼저 모드:** SW 일반 모드(편집) / SW 뷰어 모드(도면 보기)를 [AI_USAGE.md](./AI_USAGE.md) §0으로 정합니다. **뷰어 모드는 §9(`SWViewer*`)만** 쓰고(예외: 창 최소화·닫기 버전의 `trailing` 끝 `SWHeaderWindowControls`, [VIEWER.md](./VIEWER.md) §2.1), 아래 §1~8은 일반 모드용입니다. 두 모드를 한 화면에 섞지 않습니다. - **무엇을 쓸까(일반 모드):** Header 전체 → `SWHeader`(1). Header 부품 조합 → 개별 컨트롤(2). 팝업·설정창·정보창 → `SWPopup`(3). 도구 막대·탭·토글 → `SWToolButton` / `SWToolGroup`(4). 로고 클릭 2단 메뉴·최근파일 → `SWHeader homeMenu` / `SWHomeMenu`(5). 게이지바(투명도 등)는 `SWSlider`, 선택 목록(단위 등)은 `SWSelect`(3). Switch·Badge·Progress·Dialog(모달)·Label·FilterBar는 SW에 없습니다 → `@hm-design/ui`를 그대로 씁니다(필요하면 `SWPopup` children 안에). +- **무엇을 쓸까(일반 모드, 프로세스 단계 Header):** 로고 · 파일명 · 프로세스 단계 탭(01 Modeling → 02 Analysis Result 등) · Layer 필드 · 도구 pill · 창 이동 · 최소화 · 최대화/복원 · 종료 구성(Strana Main Header) → `SWProcessHeader`, 프로세스 영역만 → `SWProcessBar`(11). `SWHeader`와 같은 화면에 함께 두지 않습니다(Header는 화면에 하나). - **무엇을 쓸까(일반 모드 하단):** 하단 명령 막대(보조기능·명령 칸·분류 탭·명령어 전체보기) → `SWFooter`, 도면 공간 탭(Model/Layout) → `SWFooterLayoutTabs`, 명령창 → `SWFooterCommandLine`(10, [FOOTER.md](./FOOTER.md)). - **무엇을 쓸까(뷰어 모드):** Header → `SWViewerHeader`(`groups` + `trailing`), Dropdown·우클릭 → `SWViewerMenu`, 팝업·설정창·모달 → `SWViewerPanel` + `SWViewerSection`/`SWViewerField`/`SWViewerSwitch`/`SWViewerInput`/`SWViewerButton`, 옵션 패널 게이지 → `SWSlider appearance="property"`, 상태 → `SWViewerStatus`(9). @@ -88,7 +89,7 @@ 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 영역). `document` = 파일명 면(48px, 글자만 Hover #FFC600). Home 바로 뒤에 두면 로고 사선 접점을 그대로 공유 | ✅ | +| `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 바로 뒤에 두면 로고 사선 접점을 그대로 공유 | ✅ | | `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`입니다. @@ -180,3 +181,24 @@ SW **뷰어 모드** 전용 Export입니다(2026-10-19 추가, 참고 DWG/DXF - **상수·아이콘:** `SW_FOOTER_MODEL`(`"model"`, Model 탭의 `value`), `swFooterToolIcons`(`line`·`pline`·`arc`·`circle`·`ellipse`·`rect`·`poly`·`point`·`hatch`·`ncopy` → `{ icon, activeIcon }`), `swFooterAuxIcons`(`objectSnap`·`selectionEffect`·`orthogonal`·`gridSnap`·`gridBackground`·`lineWeight`, 인라인 SVG·채움 `currentColor`). - **공개 타입:** `SWFooterProps`, `SWFooterSideAction`, `SWFooterSideButtonProps`, `SWFooterAuxGroupProps`, `SWFooterAuxButtonProps`, `SWFooterToolButtonProps`, `SWFooterAddSlotProps`, `SWFooterGuideProps`, `SWFooterGuideContent`, `SWFooterMenuProps`, `SWFooterMenuItem`, `SWFooterLayoutTabsProps`, `SWFooterCommandLineProps`, `SWFooterTool`(`id`, `label`, `icon`, `activeIcon`, `title`, `ariaLabel`, `disabled`), `SWFooterCategory`(`id`, `label`, `tools`, `disabled`), `SWFooterAuxItem`(`id`, `label`, `icon`, `pressed`, `title`, `disabled`), `SWFooterLayout`(`id`, `label`, `locked`, `shape`), `SWFooterToolIconName`, `SWFooterAuxIconName`. - **검증 표시:** Footer Export는 Storybook(`SW/Footer`)과 저장소 안 Import·Build 검증만 있고 독립 소비 프로젝트(Tarball) 브라우저 검증 기록이 없어 ◻입니다. + +## 11. Process Header (`SWProcessHeader` · `SWProcessBar`, SW 일반 모드) + +프로세스 단계 Header(2026-11-03 추가). Strana Main Header 디자인을 SW 일반 모드의 **독립 Header**로 옮긴 것으로, `SWHeader`·`SWViewerHeader`와 같은 층위입니다. 한 화면에는 Header 하나만 둡니다. 토큰·상태는 [DESIGN_RULES.md](./DESIGN_RULES.md) §14, 조합은 [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 12. + +| 컴포넌트 | 용도 | 주요 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 | ◻ | +| `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은 선택된 단계 하나로만 들어갑니다. +- **반응형:** 숨김·More·스크롤 없음. 보장 폭 1100px 이상이며 더 좁으면 Header 안에서 잘립니다. 탭 글자·파일명·Layer 이름은 `…` 말줄임. 공간이 부족하면 선택된 탭도 다른 탭과 같은 비율로 줄어듭니다(2026-11-03 개정): `default`는 모든 탭이 같은 폭, `multi`는 글자 폭에 비례하며 이름이 들어가지 않으면 `…`. 선택이 바뀌어도 탭 폭은 그대로입니다. 그룹은 이름 + 모든 탭 여백보다 좁아지지 않아 겹치지 않습니다. 그룹 사이 화살표·간격은 고정입니다. **넘침 정책 확정(2026-11-03): 그룹 접기·좌우 스크롤·More 메뉴를 추가하지 않습니다.** +- **범위 밖(호스트 소유):** 로고 메뉴 본체, Layer Popup, 실제 화면 분할 실행, 도구 아이콘(소비 측이 `tools`에 넣는 요소). 화면 분할 선택 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`. +- **검증 표시:** Storybook(`SW/Step Header`)과 저장소 안 Typecheck·Build 검증만 있고 독립 소비 프로젝트(Tarball) 브라우저 검증 기록이 없어 ◻입니다. diff --git a/packages/sw/ai/COMPOSITION_PATTERNS.md b/packages/sw/ai/COMPOSITION_PATTERNS.md index ef04bfa..8ad81e1 100644 --- a/packages/sw/ai/COMPOSITION_PATTERNS.md +++ b/packages/sw/ai/COMPOSITION_PATTERNS.md @@ -4,7 +4,7 @@ ## 0. 어떤 패턴을 고를까 -먼저 SW 모드를 정합니다([AI_USAGE.md](./AI_USAGE.md) §0). 1~6과 8·10은 **SW 일반 모드**, 7은 **SW 뷰어 모드**이며 한 화면에서 섞지 않습니다. 11(테마 설정)은 두 모드 공통입니다. +먼저 SW 모드를 정합니다([AI_USAGE.md](./AI_USAGE.md) §0). 1~6과 8·10·12는 **SW 일반 모드**, 7은 **SW 뷰어 모드**이며 한 화면에서 섞지 않습니다. 11(테마 설정)은 두 모드 공통입니다. | 요청 | 모드 | 패턴 | |---|---|---| @@ -18,6 +18,7 @@ | 일반 모드 `SWHeader`에서 로고를 누르면 열리는 2단 메뉴(저장·내보내기·찾아열기·최근파일 등) | 일반 | 8. 로고 클릭 메뉴 | | 화면 하단 명령 막대(Line·Arc 등 명령, 분류 탭, 보조기능 토글, 명령창), Model/Layout 탭 | 일반 | 10. Footer | | 글꼴 등 SW 토큰을 화면 전체(Header·Dropdown·More·Panel·명령창)에 같은 값으로 바꾸기, Portal 요소 포함 | 일반·뷰어 공통 | 11. 테마(토큰) 설정 | +| 프로세스 단계(01 Modeling → 02 Analysis Result 등) 탭이 있는 Header, Strana Main Header | 일반 | 12. 프로세스 단계 Header | ## 1. 완성형 SW Header — [ExampleA-DefaultHeader.tsx](./examples/ExampleA-DefaultHeader.tsx) @@ -248,6 +249,49 @@ const appSWTheme = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-serif' - 기본 글꼴은 Noto Sans KR입니다(2026-10-20 확정). 기본값을 쓸 때는 글꼴 토큰을 지정하지 않습니다. Web UI 글꼴 토큰(`--hm-font-family-base`)을 SW 화면에 넣지 않습니다. - `hm-sw-*` class 선택자로 토큰을 덮거나 `!important`, 컴포넌트마다 같은 값 반복 지정을 하지 않습니다. +## 12. 프로세스 단계 Header (일반 모드) + +`SWHeader` 대신 쓰는 독립 Header입니다(한 화면에 Header 하나). 상태는 모두 프로그램이 갖고 Callback으로 갱신합니다. 로고 메뉴·Layer Popup·도구 Popup은 프로그램이 직접 엽니다([COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §11). + +```tsx +const groups: SWProcessGroup[] = [ + { id: "modeling", number: "01", label: "Modeling", steps: [{ id: "structure", label: "Structure" }, { id: "loads", label: "Loads" }] }, + { id: "analysis", number: "02", label: "Analysis Result", steps: [{ id: "results", label: "Results" }] }, +] + + setStep(s.id) }} + layer={{ label: "Layer", value: layerName, visible: layerVisible, open: layerPopupOpen }} + onLayerClick={toggleLayerPopup} onLayerVisibilityChange={setLayerVisible} + tools={<> + {/* ViewPropertyIcon: the program's own icon (the package ships no product icons). */} + } pressed={viewProperty} onClick={toggleViewProperty}>View Property + + } + onWindowMoveStart={startWindowDrag} maximized={maximized} + onMinimize={minimize} onMaximizeToggle={toggleMaximize} onClose={closeWindow} /> +``` + +1. 도구 아이콘은 프로그램이 그린 24 × 24 요소입니다. 버튼 상태 색(기본 `#B6D0C9`, Hover·눌림 `#FFC600`)을 따르려면 `currentColor`로 칠하고, 색이 고정된 이미지 아이콘이면 `hoverIcon`에 `#FFC600` 그림을 함께 줍니다(SW 인쇄 버튼과 같은 방식). +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 개정). + +```tsx + setStep(s.id) }} + tools={} hoverIcon={} onClick={openHelp}>도움말} + hiddenControls={["maximize"]} onWindowMoveStart={startWindowDrag} onMinimize={minimize} onClose={closeWindow} /> +``` + ## 9. 하지 말 것 - `hm-sw-*` class를 소비 코드에 쓰거나 CSS로 덮어쓰기(토큰 변경도 class 선택자가 아니라 가장 바깥 SW 범위의 `style` — 11) diff --git a/packages/sw/ai/DESIGN_RULES.md b/packages/sw/ai/DESIGN_RULES.md index 9b6abc4..8e5699c 100644 --- a/packages/sw/ai/DESIGN_RULES.md +++ b/packages/sw/ai/DESIGN_RULES.md @@ -6,7 +6,7 @@ - 기본값은 가장 바깥 SW 범위에서만 선언되고 안쪽 SW 범위(Dropdown·More·Panel·명령창)는 상속합니다(2026-10-20). 값을 바꿀 때는 가장 바깥 범위의 `style`만 씁니다 — [AI_USAGE.md](./AI_USAGE.md) §3.1. - SW는 `@hm-design/ui` 토큰을 쓰지 않습니다. UI 패키지 토큰을 복제하거나 섞지 않습니다. - Light/Dark 분기 없음. 전역 선택자(`html`, `body`, `*`) 없음. -- **모드:** §1~10과 §12·§13은 SW **일반 모드**(`SWHeader`·`SWPopup`·`SWToolButton`·`SWHomeMenu`·`SWFooter`) 기준입니다. SW **뷰어 모드**(`SWViewer*`)의 토큰·상태는 §11과 [VIEWER.md](./VIEWER.md)를 봅니다. 모드 선택·혼합 금지는 [AI_USAGE.md](./AI_USAGE.md) §0. +- **모드:** §1~10과 §12·§13·§14는 SW **일반 모드**(`SWHeader`·`SWPopup`·`SWToolButton`·`SWHomeMenu`·`SWFooter`·`SWProcessHeader`) 기준입니다. SW **뷰어 모드**(`SWViewer*`)의 토큰·상태는 §11과 [VIEWER.md](./VIEWER.md)를 봅니다. 모드 선택·혼합 금지는 [AI_USAGE.md](./AI_USAGE.md) §0. ## 1. 크기·간격 @@ -123,7 +123,7 @@ | appearance | 기존 기준 | 상태 | |---|---|---| | `text` | 속성Bar·속성창 토글: 19px 행, 14px 700, 자간 0.04em, 묶음 위 12px 캡션, 버튼 사이 1px 구분선 | Hover·`pressed` 글자 `#FFC600`, 테두리·배경 변화 없음 | -| `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`) | +| `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`) | | `document` | 파일명 면: 48px, gradient 표면, 위·오른쪽·아래 1px gradient 테두리(`--hm-sw-header-document-border-gradient`), 글자 `#F0F7F5` 700 · 자간 0.06em | 글자만 Hover `#FFC600`. Home 바로 뒤면 로고 사선 접점 공유, 그 뒤 파일명은 왼쪽 여백 12px | ## 10. 새 컴포넌트 체크리스트 @@ -214,3 +214,44 @@ - Figma에 없는 값: 실제 화면에서 가져온 값(바 안 명령창 배치, 좌표, ▲ 패널, 분류 ◀ ▶, 보조기능 아이콘만)과 AI 구성 값 — 디자인 확인 필요(측면 버튼 Hover 글자 `#FF5C00`·주황 테두리, 분류 행 `+` Hover, Disabled 40%, 명령창 최소 280px, 칸 영역 비율 0.57(두 실제 화면 재현 값), 보조기능 아이콘 전환 1279px, Layout 탭 이름 편집 입력의 동작). - 새 하단 UI는 위 토큰을 `var(--…)`로 읽고 `hm-sw-footer*` class를 쓰거나 덮어쓰지 않습니다. + +## 14. Process Header (`SWProcessHeader` · `SWProcessBar`) — SW 일반 모드 전용 + +원본 `src/theme/sw-process-header.css`(2026-11-03). Strana Main Header(Hanmac 테마)의 값입니다. 새 토큰은 `--hm-sw-process-*`(`.hm-sw-header`, `.hm-sw-theme` 범위)이고, 값이 같은 곳은 SW Header 토큰을 그대로 읽습니다(파일명 면·테두리, 객체 면·테두리, 속성 필드 면·그림자, Hover gradient, 선택 `#FFC600`, 제목·글자, 창 이동 줄무늬, 창 버튼 글리프 `#ADB0AF`·Hover 배경). Light/Dark preset은 없으며 다른 Strana 테마는 가장 바깥 범위 `style`에 같은 토큰을 지정합니다([AI_USAGE.md](./AI_USAGE.md) §3.1). + +| 항목 | 값 | 토큰 | +|---|---|---| +| 배치 | 홈·파일명 글자 = 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` | +| 비활성 덮개 | `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-*` | +| 비활성 바깥 그룹 면 | Blur 4 면 + `drop-shadow(0 0 15px rgba(17,105,79,.8))` + 테두리 빛 1.5px, 바깥에서 안쪽으로 사라짐(79.8→198.4 / 반전 236.7→273.1) | `--hm-sw-process-idle-*` | +| 사다리꼴 배치(확정 2026-11-03) | 그룹 면(사다리꼴)은 프로세스 영역의 **시작(첫 그룹)과 끝(마지막 그룹)에만** 둡니다. 그 그룹이 켜져 있으면 발광 면, 아니면 비활성 면. 가운데 그룹에는 켜져 있어도 도형이 없고 밝은 바탕(덮개 없음)만 보입니다(Figma 873:17421). 그룹 수와 관계없이 같으며 `default`·`multi` 공통. 화살표·비활성 덮개·선택 상태는 그대로 | +| 화살표 | 16 × 48. On 채움 `#27D9A5`, Dim 채움 `#ABC7BF` 25%·선 50%, 그늘이 다음 그룹 위로 20% | `--hm-sw-process-arrow-*`, `-shade*` | +| 탭 | 높이 32, padding 0 12 2.67, 16px 700 `#F0F7F5`, 간격 1, 넓을 때 127. 모서리: 단독 16 / 첫 16 4 4 16 / 마지막 4 16 16 4 / 가운데 4 | `--hm-sw-process-tab-*` | +| 선택 탭 폭(공통, 2026-11-03 개정) | 선택된 탭도 다른 탭과 같은 비율로 줄어듦(우선 배정 없음). 넓을 때 `default`는 127 균등 그대로. 좁으면 모든 탭이 여백 폭까지 줄고 `…`. 그룹은 이름 + 모든 탭 여백보다 좁아지지 않음(측정) | — | +| 화면 분할 도구 | 버튼 = SW pill 버튼(Hover 링·노란 아이콘). 열림 = 면 `--hm-sw-process-tool-open-background` + 금색 위·양옆(`--hm-sw-header-hover-border-gradient`), 링 없음, 아이콘 `#FFC600`. 버튼 아래 연장(면 `#081813` + 금색 양옆)이 Header 끝까지, 패널 233 × 36(면 `#082018`, 금색 양옆·아래, 위 오른쪽·아래 radius 2, 그림자 `0 2px 4px rgba(0,0,0,.6)`)이 Header 바로 아래 버튼 왼쪽에, 버튼 오른쪽부터 패널 끝까지 금색 위 선. 칸 38.5 × 34: 밝은 틀 32 × 22(`#B6D0C9`, 검정 1px, 정수 크기 — 배율 100%에서 아래 밝은 줄 유지) 안에 그림 29.6 × 19.73 + 초록 창(`#1B6B53`, 안쪽 검정 30% 1px) + 왼쪽 위 광택, Hover·선택 = 창만 `#FFC600`. 버튼 아이콘 창 `#003425` | `--hm-sw-process-split-*`, `--hm-sw-process-tool-open-*` | +| 탭 상태 | 기본 층 70%. Hover 글자 `#FFC600`. 선택 = 층 100%, 바탕 `--hm-sw-process-tab-active-base`, 위 5·왼쪽 6 안쪽 그림자 75%, 테두리 2px 25%, 글자 `#FFC600`. Disabled = 층 50%, 글자 `#64A491`(SW 기본 40% 대신). Focus 2px `#F0F7F5` | `--hm-sw-process-tab-*` | +| Layer 필드 | 최대 234 × 38, 면 안 양옆 8, padding 12 / 10, 속성 필드 면 + 테두리 `#08251C → #0E3D2F → #1E7C60 → #08241C → #051612`, 전구 20 + 8 + 라벨 12/14 · 값 14/16(400). Hover·열림 = 노란 gradient 링 + 글자 `#FFC600`, Disabled 40% | `--hm-sw-process-layer-border-gradient` | +| 도구 pill | 위·왼쪽 3, 높이 42, radius 4, 면 `rgba(0,0,0,.851)`(위 2px) → `.8`(4px), 테두리 `#092920 → #1C6F56`. 버튼 40 × 38 간격 1, 파일명 면 + 테두리 `#08251C → #208769 → #08241C → #051612`, 아이콘 24 `#B6D0C9`. Hover·눌림 = 노란 링 + 아이콘 `#FFC600` | `--hm-sw-process-pill-*`, `-tool-*` | +| 창 이동 | SWHeader 창 이동 그대로(`SWHeaderWindowControls` 창 이동만): 88 × 48, 줄무늬 82 × 37(3, 6), 아이콘 (28, 13), Hover 1.4s Motion + 툴팁, Drag 중 정지, 해제 즉시 복원 | §6, `--hm-sw-header-stripe-color` | +| 캡션 버튼 | 32 × 48 × 3. Hover 배경 `rgba(0,0,0,.2)` + 흰 글리프, 종료 Hover `#E40000` + 흰 글리프, Disabled 40% | `--hm-sw-header-window-*`, `--hm-sw-process-close-hover-background`, `-close-icon-hover` | + +- 글자 외곽선은 두 appearance 모두 SW 규칙(`--hm-sw-header-text-outline` 14·16·18px, `--hm-sw-header-label-outline` 10·12px)입니다. Strana 원본(DropShadow Blur 2), multi Figma(Stroke 없음)와 다른 의도된 차이입니다(2026-11-03). +- 도구 아이콘은 어떤 상태에서도 사라지지 않습니다: `hoverIcon`이 없는 `currentColor` 아이콘은 Hover에도 그대로 보이고 색만 `#FFC600`(`SWToolButton`은 `hoverIcon`이 있을 때만 Hover 아이콘 교체에 참여, SWHeader `tools`와 공통, 2026-11-03). +- Hover·Focus는 SWHeader와 같습니다: 도구 버튼 노란 링 + 노란 아이콘(`hoverIcon` 또는 `currentColor`, 눌림도 같은 모양), 로고 금색 테두리, 파일명·탭 글자 `#FFC600`, 최소화·최대화 어두운 배경 + 흰 글리프, 종료 `#E40000`, Focus 2px `#F0F7F5`. +- 파일 없음(`documentEmptyLabel`): 파일명 면 오른쪽 위 10px 700 `#AEDBCC`(`--hm-sw-process-document-empty-color`), 자간 −0.1px, 위 7px. 두 appearance 공통. + +**`appearance="multi"`**(StrAna-SW Figma 873:16481 · 873:16254 · 873:17421, 2026-11-03). 바탕·덮개·발광·비활성 면·화살표·선택·키보드는 위 `default` 규칙과 같은 구현입니다. 아래만 다릅니다. + +| 항목 | 값 | 토큰 | +|---|---|---| +| 프레임 | 면 49(아래 1px 없음), 파일명 최대 189(테두리 단색 `#0E4C3A`, 로고·글자 위치는 default·SWHeader와 같음), 프로세스 1278 비율, 창 영역 테두리 단색 | `--hm-sw-header-bar-height`, `--hm-sw-header-document-border-color` | +| 도구 pill | SW pill 그대로(`#092920`, `rgba(0,0,0,.8)`, 안쪽 그림자) 높이 41, 위 4 · 왼쪽 5 · 오른쪽 2, 버튼 40 × 37(단색 `#08251C`) | `--hm-sw-header-pill-*` | +| 창 이동 | SWHeader 창 이동(Motion 포함) 위치만: 줄무늬 82 × 39(3, 5), 아이콘·Motion 3px 위(28, 10) | `--hm-sw-header-stripe-color` | +| 그룹 | 좌우 10, 위 3.8: 제목 줄 14(12px, 번호와 간격 2, 한 줄 `…`) → 1.9 → 탭 25(위 19.7). 화살표 칸 16, 그룹 내용 사이 36. 발광 면 왼쪽 50 · 반전 오른쪽 48 밖 | — | +| 그룹 상태 | 켜진 그룹 = 제목 700 흰색, 진행 완료(켜진 그룹 앞) = 같은 제목 + 밝은 탭(바탕은 덮개), 미진행 = 제목 500 `#C3EBDF` | — | +| 탭 | 높이 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를 쓰거나 덮어쓰지 않습니다. diff --git a/packages/sw/src/components/sw-process-header/SWProcessArt.tsx b/packages/sw/src/components/sw-process-header/SWProcessArt.tsx new file mode 100644 index 0000000..77ec313 --- /dev/null +++ b/packages/sw/src/components/sw-process-header/SWProcessArt.tsx @@ -0,0 +1,167 @@ +import { useId } from "react" + +/* + * Internal art of SWProcessHeader / SWProcessBar (not exported from the package). Geometry is the Strana Main Header + * (HeaderStyles.xaml, HEAD edda3cc); colors are --hm-sw-process-* / --hm-sw-header-* tokens, so a theme override + * through the outermost SW scope recolors them. Inline SVG instead of asset files for that reason. + */ + +const stop = (token: string) => ({ stopColor: `var(${token})` }) + +/** Glow surface of the lit group (Header.ContentControl.ProcessShape). Face / halo / fade are CSS. */ +function GlowShape() { + return ( + <> + + + + {/* Edge light: 3px stroke, 10 blur, fading at the bottom (Header.Geometry.ProcessGlowLeftEdge at -1.5). */} + + + + + ) +} + +/** Idle surface of an unlit outer group (Header.ContentControl.ProcessShapeIdle). */ +function IdleShape() { + return ( + <> + + + + + + + + + ) +} + +/** Group surface behind the name and tabs. `mirrored`: the last group (Strana ScaleX −1, 44px outside). */ +export function SWProcessShape({ kind, mirrored }: { kind: "active" | "idle"; mirrored: boolean }) { + return ( + + ) +} + +/** + * Arrow between two groups (Header.Control.ProcessArrowOn / Dim, 16 x 48) and the dim cover of its cell: + * the tip is dim while the group before is unlit, the notch while the group after is unlit. + */ +export function SWProcessArrow({ on, dimTip, dimNotch }: { on: boolean; dimTip: boolean; dimNotch: boolean }) { + const id = useId() + return ( + + ) +} + +const CAPTION_GLYPHS = { + minimize: "M11,23 H21 V25 H11 Z", + maximize: "M11,19 H21 V29 H11 Z M12,21 V28 H20 V21 Z", + restore: "M13,18 H22 V27 H21 V20 H13 Z M11,21 H20 V30 H11 Z M12,23 V29 H19 V23 Z", + close: "M16 22.5854L20.5 18.0854L21.9142 19.4996L17.4142 23.9996L21.915 28.5004L20.5008 29.9146L16 25.4138L11.4992 29.9146L10.085 28.5004L14.5858 23.9996L10.0858 19.4996L11.5 18.0854L16 22.5854Z", +} as const + +/** Caption glyph (Header.Geometry.Caption*) in the 32 x 48 button, filled with the button color. */ +export function SWProcessCaptionGlyph({ kind }: { kind: keyof typeof CAPTION_GLYPHS }) { + return ( + + ) +} + +/** Viewport split layouts in the Strana order (ViewportSplitType Type01 … Type06). */ +export const SW_PROCESS_SPLIT_TYPES = ["single", "columns", "rows", "topWide", "leftTall", "grid"] as const + +type Rect = readonly [x: number, y: number, width: number, height: number] + +// Header button icon panes (Header.Geometry.ToolLayout.T1 … T5, default 2 x 2), in the 24 x 24 icon. +const W = 6.538, H = 3.448, FW = 15.11, FH = 8.87, X1 = 4.477, X2 = 13.049, Y1 = 7.597, Y2 = 13.019 +const ICON_PANES: Record<(typeof SW_PROCESS_SPLIT_TYPES)[number], readonly Rect[]> = { + single: [[X1, Y1, FW, FH]], + columns: [[X1, Y1, W, FH], [X2, Y1, W, FH]], + rows: [[X1, Y1, FW, H], [X1, Y2, FW, H]], + topWide: [[X1, Y1, FW, H], [X1, Y2, W, H], [X2, Y2, W, H]], + leftTall: [[X1, Y1, W, FH], [X2, Y1, W, H], [X2, Y2, W, H]], + grid: [[X1, Y1, W, H], [X2, Y1, W, H], [X1, Y2, W, H], [X2, Y2, W, H]], +} + +/** Split button icon (Header.Icon.ToolLayout): the frame takes the button color (currentColor), panes stay dark. */ +export function SWProcessSplitIcon({ type }: { type: (typeof SW_PROCESS_SPLIT_TYPES)[number] }) { + return ( + + ) +} + +// Selector cell panes (Geometry.SplitCell.T1 … T6) in the 29.6 x 19.73 frame, radius 1. +const CELL_PANES: Record<(typeof SW_PROCESS_SPLIT_TYPES)[number], readonly Rect[]> = { + single: [[0.82, 0.82, 27.954, 18.088]], + columns: [[0.82, 0.82, 13.566, 18.088], [15.2, 0.82, 13.566, 18.088]], + rows: [[0.82, 0.82, 27.99, 8.633], [0.82, 10.26, 27.99, 8.633]], + topWide: [[0.82, 0.82, 27.99, 8.633], [0.82, 10.26, 13.566, 8.633], [15.2, 10.26, 13.566, 8.633]], + leftTall: [[0.82, 0.82, 13.566, 18.088], [15.2, 0.81, 13.566, 8.633], [15.2, 10.28, 13.566, 8.633]], + grid: [[0.83, 0.81, 13.566, 8.633], [15.2, 0.81, 13.566, 8.633], [0.83, 10.26, 13.566, 8.633], [15.2, 10.28, 13.566, 8.633]], +} + +/** + * Selector cell picture (Button.SplitCell): light frame with a black outline, panes with a 1px inner edge (drawn on a + * line 0.5 inside, so the edge stays inside the pane), and the top-left sheen. + */ +export function SWProcessSplitCell({ type }: { type: (typeof SW_PROCESS_SPLIT_TYPES)[number] }) { + const id = useId() + return ( + + ) +} diff --git a/packages/sw/src/components/sw-process-header/SWProcessBar.tsx b/packages/sw/src/components/sw-process-header/SWProcessBar.tsx new file mode 100644 index 0000000..7c6d093 --- /dev/null +++ b/packages/sw/src/components/sw-process-header/SWProcessBar.tsx @@ -0,0 +1,145 @@ +import { forwardRef, Fragment, useId, useLayoutEffect, useRef, type KeyboardEvent } from "react" +import "../../theme/sw-process-header.css" +import { moveRovingFocus } from "../shared/roving-focus" +import { SWProcessArrow, SWProcessShape } from "./SWProcessArt" +import type { SWProcessBarProps } from "./types" + +type StepShape = "single" | "start" | "middle" | "end" +const stepShape = (index: number, count: number): StepShape => + count === 1 ? "single" : index === 0 ? "start" : index === count - 1 ? "end" : "middle" + +/** + * Process area of the Strana Main Header (MainProcessBar.xaml) and the StrAna-SW multi step header (`appearance="multi"`): + * groups with a number, a name and step tabs, an arrow between groups, the lit group on a glow surface. One checked step + * for the whole bar (radio group, arrow keys move and select). Groups keep their natural width and shrink together in + * proportion; the room left over is shared on both sides. Both appearances share this markup; only the group content + * (name beside or above the tabs, tab look) differs in CSS. Data in props, selection through `onStepSelect`. + */ +export const SWProcessBar = forwardRef(function SWProcessBar( + { appearance = "default", groups, activeStepId, activeGroupId, onStepSelect, disabled, className, onKeyDown, "aria-label": ariaLabel, ...rest }, forwardedRef, +) { + const id = useId() + const rootRef = useRef(null) + const multi = appearance === "multi" + const litGroupId = activeGroupId ?? groups.find(group => group.steps.some(step => step.id === activeStepId))?.id + // multi only: the groups before the lit one are done (Figma 873:17421: bright name and tabs on the dim surface). + const litIndex = groups.findIndex(group => group.id === litGroupId) + const enabledIds = groups.flatMap(group => disabled || group.disabled ? [] : group.steps.filter(step => !step.disabled).map(step => step.id)) + const tabStop = activeStepId !== undefined && enabledIds.includes(activeStepId) ? activeStepId : enabledIds[0] + + // Measured, so fonts and labels can change: + // - a two-line group name sits 2px higher (Strana: 8.67 instead of 10.67); + // - each group keeps room for its name (beside the tabs) and every tab down to its padding (the checked one too, so + // it shrinks with the others). Groups still shrink in proportion above that minimum and never overlap below it. + useLayoutEffect(() => { + const root = rootRef.current + if (!root) return + const px = (value: string) => parseFloat(value) || 0 + const update = () => { + root.querySelectorAll(".hm-sw-process-bar__name-text").forEach(name => { + const label = name.closest(".hm-sw-process-bar__label") + if (label) label.dataset.lines = name.getBoundingClientRect().height > 15 ? "2" : "1" + }) + root.querySelectorAll(".hm-sw-process-bar__group").forEach(group => { + const style = getComputedStyle(group) + const steps = group.querySelector(".hm-sw-process-bar__steps") + const label = group.querySelector(".hm-sw-process-bar__label") + if (!steps) return + const tabs = Array.from(steps.children) as HTMLElement[] + let min = px(style.paddingLeft) + px(style.paddingRight) + px(getComputedStyle(steps).columnGap) * Math.max(tabs.length - 1, 0) + tabs.forEach(tab => { + const tabStyle = getComputedStyle(tab) + min += px(tabStyle.paddingLeft) + px(tabStyle.paddingRight) + px(tabStyle.borderLeftWidth) + px(tabStyle.borderRightWidth) + }) + // Beside the tabs (default) the name takes its own width; above them (multi) it ellipsizes. + if (label && style.flexDirection !== "column") min += label.offsetWidth + px(getComputedStyle(label).marginRight) + group.style.minWidth = `${Math.ceil(min)}px` + }) + } + update() + const observer = new ResizeObserver(update) + root.querySelectorAll(".hm-sw-process-bar__label").forEach(node => observer.observe(node)) + return () => observer.disconnect() + }, [groups, activeStepId, appearance]) + + const keys = (event: KeyboardEvent) => { + onKeyDown?.(event) + if (event.defaultPrevented) return + moveRovingFocus(event, Array.from(event.currentTarget.querySelectorAll('button[role="radio"]:not(:disabled)')), { vertical: true, select: true }) + } + + const lit = (index: number) => groups[index]?.id === litGroupId + const last = groups.length - 1 + + return ( +
{ + rootRef.current = node + if (typeof forwardedRef === "function") forwardedRef(node) + else if (forwardedRef) forwardedRef.current = node + }} + role="radiogroup" + aria-label={ariaLabel ?? "프로세스"} + aria-disabled={disabled || undefined} + className={`hm-sw-process-bar${className ? ` ${className}` : ""}`} + data-appearance={multi ? "multi" : undefined} + onKeyDown={keys} + {...rest} + > +
+ ) +}) diff --git a/packages/sw/src/components/sw-process-header/SWProcessHeader.tsx b/packages/sw/src/components/sw-process-header/SWProcessHeader.tsx new file mode 100644 index 0000000..5e77e1b --- /dev/null +++ b/packages/sw/src/components/sw-process-header/SWProcessHeader.tsx @@ -0,0 +1,87 @@ +import { forwardRef } from "react" +import "../../theme/sw-process-header.css" +import { SWHeaderDocumentTitle } from "../sw-header/components/SWHeaderDocumentTitle" +import { SWHeaderHome } from "../sw-header/components/SWHeaderHome" +import { SWHeaderLayerToggle } from "../sw-header/components/SWHeaderLayerIcons" +import { SWHeaderWindowControls } from "../sw-header/components/SWHeaderWindowControls" +import { SWToolGroup } from "../sw-tool/SWTool" +import { SWProcessBar } from "./SWProcessBar" +import { SWProcessCaptionGlyph } from "./SWProcessArt" +import type { SWProcessHeaderLayer, SWProcessHeaderProps } from "./types" + +/** Layer field: bulb (the SWHeader layer toggle) and the field button as siblings, one look (Strana LayerField). */ +function SWProcessLayerField({ layer, disabled, onClick, onVisibilityChange }: { + layer: SWProcessHeaderLayer + disabled: boolean + onClick?: () => void + onVisibilityChange?: (nextVisible: boolean) => void +}) { + const label = layer.label ?? "레이어" + return ( +
+ {/* Bulb first: the tab order follows what is seen; the field button lies under it (z-index). */} + onVisibilityChange(next) : undefined} /> +
+ ) +} + +/** + * SW general-mode header of the Strana Main Header (49px): logo → file name → process area and layer surface → + * window area (tool pill, the SWHeader window move with its hover motion, minimize, maximize / restore, close). An independent header + * beside SWHeader and SWViewerHeader; one header per screen. Every state is a prop and every action a callback; the + * logo menu, the layer popup and the tool popups belong to the host. Reuses SWHeaderHome, SWHeaderDocumentTitle, + * the SWHeader layer bulb and the SWToolGroup pill; the rest is sw-process-header.css. `appearance="multi"` is the + * StrAna-SW multi step design on the same markup (CSS only: frame sizes, file name, tool pill, process group content). + */ +export const SWProcessHeader = forwardRef(function SWProcessHeader({ + appearance = "default", documentTitle, documentEmptyLabel, onDocumentClick, logo, logoText, logoSrc, logoAlt, onHomeClick, homeMenuOpen, + process, layer, onLayerClick, onLayerVisibilityChange, tools, toolsLabel = "도구", + onWindowMoveStart, maximized = false, onMinimize, onMaximizeToggle, onClose, hiddenControls, disabled = false, + className, ...rest +}, ref) { + const hidden = new Set(hiddenControls ?? []) + const logoNode = logo ?? (logoText ? {logoText} : undefined) + const toolsShown = tools !== undefined && tools !== null && tools !== false && !hidden.has("tools") + 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 + + return ( +
+
+ + +
+ + {layer && !hidden.has("layer") && ( +
+ +
+ )} +
+ {windowShown && ( +
+ {toolsShown && {tools}} + {/* The SWHeader window move as it is: stripes, icon, 1.4s hover motion and tooltip (minimize / close are ours). */} + {!hidden.has("windowMove") && } + {captions.map(id => ( + + ))} +
+ )} +
+
+ ) +}) diff --git a/packages/sw/src/components/sw-process-header/SWProcessSplitButton.tsx b/packages/sw/src/components/sw-process-header/SWProcessSplitButton.tsx new file mode 100644 index 0000000..6586ff3 --- /dev/null +++ b/packages/sw/src/components/sw-process-header/SWProcessSplitButton.tsx @@ -0,0 +1,126 @@ +import { forwardRef, useEffect, useId, useRef, useState, type FocusEvent, type KeyboardEvent } from "react" +import "../../theme/sw-process-header.css" +import { moveRovingFocus } from "../shared/roving-focus" +import { useOutsidePointerDown } from "../shared/use-outside-pointer" +import { useSWFixedDropdownPosition } from "../sw-header/components/SWHeaderDropdown" +import { SWToolButton } from "../sw-tool/SWTool" +import { SW_PROCESS_SPLIT_TYPES, SWProcessSplitCell, SWProcessSplitIcon } from "./SWProcessArt" +import type { SWProcessSplitButtonProps, SWProcessSplitType } from "./types" + +const TYPE_LABELS: Record = { + single: "1면", columns: "좌우 2분할", rows: "상하 2분할", topWide: "상단 1면 + 하단 2분할", leftTall: "좌측 1면 + 우측 2분할", grid: "4분할", +} + +/** + * Viewport split tool for the `tools` pill of SWProcessHeader (both appearances). The button is the SW pill button + * (hover ring, focus, disabled); open, it takes the Strana open look and the selector hangs from it: a 3px extension of + * the button, the 233 x 36 panel under its left edge and a gold line from its right edge to the panel end. The selector + * is a fixed overlay kept on screen (the SW field dropdown position), so the header clipping never cuts it. + * Keyboard: Enter / Space toggle, ArrowDown opens on the checked cell, arrows / Home / End move and choose (radio group), + * Escape closes back to the button, Tab out and a click outside close. + */ +export const SWProcessSplitButton = forwardRef(function SWProcessSplitButton( + { value, onValueChange, open: openProp, defaultOpen = false, onOpenChange, label = "화면 분할", typeLabels, disabled = false }, forwardedRef, +) { + const [internalOpen, setInternalOpen] = useState(defaultOpen) + const open = !disabled && (openProp ?? internalOpen) + const rootRef = useRef(null) + const triggerRef = useRef(null) + const popupRef = useRef(null) + const focusCellRef = useRef(false) + const popupId = useId() + + const setOpen = (next: boolean) => { + if (openProp === undefined) setInternalOpen(next) + onOpenChange?.(next) + } + const close = (restoreFocus: boolean) => { + setOpen(false) + if (restoreFocus) triggerRef.current?.focus() + } + + useOutsidePointerDown(open, rootRef, () => setOpen(false)) + const position = useSWFixedDropdownPosition(open, triggerRef, popupRef, "start") + + useEffect(() => { + if (!open || !focusCellRef.current || !position) return + focusCellRef.current = false + popupRef.current?.querySelector('[role="radio"][tabindex="0"]')?.focus() + }) + + // The button extension reaches the header bottom, so the panel starts under the header and never covers the + // window buttons next to the pill (Strana 3px; at least 3px when used outside a header). + const trigger = triggerRef.current?.getBoundingClientRect() + const headerBottom = triggerRef.current?.closest(".hm-sw-process-header")?.getBoundingClientRect().bottom + const tail = open && position && trigger && headerBottom !== undefined ? Math.max(3, Math.round(headerBottom - trigger.bottom + 1)) : 3 + // The selector hangs from the button (tail), so it never takes the hook's flip above the field: flipped, it was + // drawn over the header with its panel bottom cut. With no room below it stays under the button; the hook follows + // the page scroll, which brings it into view. + const top = position && trigger ? (position.flipped ? trigger.bottom : position.top) - 1 : 0 + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape" && open) { + event.stopPropagation() + close(true) + return + } + if (event.target === triggerRef.current) { + if (event.key === "ArrowDown" && !open) { + event.preventDefault() + focusCellRef.current = true + setOpen(true) + } + return + } + moveRovingFocus(event, Array.from(popupRef.current?.querySelectorAll('[role="radio"]') ?? []), { vertical: true, select: true }) + } + const onBlur = (event: FocusEvent) => { + if (open && !rootRef.current?.contains(event.relatedTarget as Node | null)) setOpen(false) + } + + return ( + + { + triggerRef.current = node + if (typeof forwardedRef === "function") forwardedRef(node) + else if (forwardedRef) forwardedRef.current = node + }} + appearance="pill" + icon={} + title={label} + aria-expanded={open} + aria-controls={open ? popupId : undefined} + disabled={disabled} + onClick={() => setOpen(!open)} + > + {label} + + {open && ( +
+
+ )} +
+ ) +}) diff --git a/packages/sw/src/components/sw-process-header/index.ts b/packages/sw/src/components/sw-process-header/index.ts new file mode 100644 index 0000000..1305c76 --- /dev/null +++ b/packages/sw/src/components/sw-process-header/index.ts @@ -0,0 +1,7 @@ +export { SWProcessHeader } from "./SWProcessHeader" +export { SWProcessBar } from "./SWProcessBar" +export { SWProcessSplitButton } from "./SWProcessSplitButton" +export type { + SWProcessHeaderProps, SWProcessHeaderLayer, SWProcessHeaderHideableControlId, + SWProcessBarProps, SWProcessGroup, SWProcessStep, SWProcessAppearance, SWProcessSplitType, SWProcessSplitButtonProps, +} from "./types" diff --git a/packages/sw/src/components/sw-process-header/types.ts b/packages/sw/src/components/sw-process-header/types.ts new file mode 100644 index 0000000..d28cdb1 --- /dev/null +++ b/packages/sw/src/components/sw-process-header/types.ts @@ -0,0 +1,151 @@ +import type { HTMLAttributes, PointerEventHandler, ReactNode } from "react" + +/** One tab of a process group (e.g. `Structure`). */ +export interface SWProcessStep { + /** Stable key, returned with the selection. */ + id: string + label: string + disabled?: boolean +} + +/** + * Process area design (2026-11-03). + * - `default`: the Strana Main Header — name beside 32px tabs of equal width (127 at most), two groups confirmed. + * - `multi`: StrAna-SW multi step (Figma 873:16481 / 873:16254 / 873:17421) — title above 25px tabs as wide as their + * label, any number of groups; groups before the lit one show as done. + * Both share the area surface, dim covers, group surfaces, arrows, selection and keyboard. + */ +export type SWProcessAppearance = "default" | "multi" + +/** + * One group of the process area (e.g. `01 Modeling`). `default` confirms two groups; `multi` takes any count. In both, + * the group surfaces (trapezoids) stand only at the start and the end of the area — never on a middle group, lit or not. + */ +export interface SWProcessGroup { + id: string + /** Number shown before the name, e.g. `01`. */ + number?: string + /** Group name: `default` wraps it at 56px (two lines move it 2px up), `multi` keeps one line above the tabs. */ + label: string + /** + * The name stays for assistive technology only (each step is described by it). `multi` keeps its row, so the tabs + * line up with the other groups (Figma groups without a title). Default false. + */ + labelHidden?: boolean + steps: readonly SWProcessStep[] + /** Disables every step of the group. */ + disabled?: boolean +} + +export interface SWProcessBarProps extends Omit, "children" | "onChange"> { + /** Design of the area. Default `default`. */ + appearance?: SWProcessAppearance + groups: readonly SWProcessGroup[] + /** The checked step (one for the whole bar). None checked when omitted. */ + activeStepId?: string + /** The lit group (glow surface, bright text). Defaults to the group of `activeStepId`. */ + activeGroupId?: string + /** A step other than the checked one was chosen (click, or arrow keys). The host updates `activeStepId`. */ + onStepSelect?: (step: SWProcessStep, group: SWProcessGroup) => void + /** Disables every step. */ + disabled?: boolean +} + +/** Layer field of the header (bulb + label + current layer). The host owns the layer data and its popup. */ +export interface SWProcessHeaderLayer { + /** Field label. Default `레이어`. */ + label?: string + /** Current layer name (ellipsis when narrow). */ + value: string + /** Bulb on (colored) when true. Default true. */ + visible?: boolean + /** The host popup is open: the field shows its open look (yellow ring and text) and `aria-expanded`. */ + open?: boolean + disabled?: boolean +} + +/** Parts that can be removed. Removed parts take no space; the logo, file name and process area always show. */ +export type SWProcessHeaderHideableControlId = "layer" | "tools" | "windowMove" | "minimize" | "maximize" | "close" + +export interface SWProcessHeaderProps extends Omit, "title" | "children"> { + /** + * Header design, also used by the process area unless `process.appearance` is set. `default`: Strana Main Header + * (48px surfaces, layer field, Strana tool pill). `multi`: StrAna-SW multi step (49px surfaces, 189px file name, + * SW tool pill 40 x 37). Logo and file-name text follow SWHeader in both. Default `default`. + */ + appearance?: SWProcessAppearance + /** File name in the document area (ellipsis when narrow, full name as tooltip). Empty while no document is open. */ + documentTitle: string + /** + * Shown instead of the file name while `documentTitle` is empty (e.g. `프로젝트 생성 전`): 10px 700 at the top right + * of the file-name surface (Figma 873:16481). Still the same button (`onDocumentClick`). + */ + documentEmptyLabel?: string + 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). */ + logoText?: string + /** Logo image URL, contained (ratio kept, centered) in the logo slot. */ + logoSrc?: string + /** Accessible product name; the logo button is announced as " 홈". Defaults to `logoText`. */ + logoAlt?: string + /** Logo click. The menu it opens belongs to the host. */ + onHomeClick?: () => void + /** The host logo menu is open: the logo keeps its hover look and `aria-expanded`. */ + homeMenuOpen?: boolean + /** Process area data and selection (`SWProcessBar` props). */ + process: SWProcessBarProps + /** Layer field. Omitted: no layer surface. */ + layer?: SWProcessHeaderLayer + /** Layer field click (open the host layer popup and set `layer.open`). */ + onLayerClick?: () => void + /** Bulb click. Without it the bulb is a status icon only. */ + onLayerVisibilityChange?: (nextVisible: boolean) => void + /** Tool buttons of the dark pill, e.g. `SWToolButton appearance="pill"` (40 x 38 each, `multi` 40 x 37, 1px apart). */ + tools?: ReactNode + /** Accessible name of the tool pill. Default `도구`. */ + toolsLabel?: string + /** Pointer down on the window-move area (the host moves the window). */ + onWindowMoveStart?: PointerEventHandler + /** Window maximized: the maximize button shows the restore glyph. Default false. */ + maximized?: boolean + onMinimize?: () => void + /** Maximize / restore button. The host toggles the window and updates `maximized`. */ + onMaximizeToggle?: () => void + onClose?: () => void + /** Parts to remove. Default: none. */ + hiddenControls?: readonly SWProcessHeaderHideableControlId[] + /** Disables the process area and the layer field (logo, file name, host tools and window buttons stay). */ + disabled?: boolean +} + +/** + * Viewport split layout (Strana ViewportSplitType, in this order): `single` Type01 one pane, `columns` Type02 left / + * right, `rows` Type03 top / bottom, `topWide` Type04 one top + two bottom, `leftTall` Type05 one left + two right, + * `grid` Type06 2 x 2. + */ +export type SWProcessSplitType = "single" | "columns" | "rows" | "topWide" | "leftTall" | "grid" + +/** + * Viewport split tool of the process header pill (Strana Main Header 화면 분할, StrAna-SW 6758:12510): a pill button + * showing the current layout and, under it, a 233 x 36 selector of the six layouts. Choosing a layout only reports it; + * splitting the program window is the host's job. + */ +export interface SWProcessSplitButtonProps { + /** Current layout: the button icon and the checked cell. */ + value: SWProcessSplitType + /** A cell other than the current one was chosen (click, or arrow keys). The selector stays open, like Strana. */ + onValueChange?: (value: SWProcessSplitType) => void + /** Controlled open state of the selector; uncontrolled when undefined. */ + open?: boolean + /** Uncontrolled initial state. Default false. */ + defaultOpen?: boolean + /** The selector opened or closed (button, Escape, a click outside, Tab out). */ + onOpenChange?: (open: boolean) => void + /** Accessible name of the button and the selector. Default `화면 분할`. */ + label?: string + /** Accessible names of the cells. Defaults: 1면, 좌우 2분할, 상하 2분할, 상단 1면 + 하단 2분할, 좌측 1면 + 우측 2분할, 4분할. */ + typeLabels?: Partial> + disabled?: boolean +} diff --git a/packages/sw/src/components/sw-tool/SWTool.tsx b/packages/sw/src/components/sw-tool/SWTool.tsx index 8dd3420..7eb0e6c 100644 --- a/packages/sw/src/components/sw-tool/SWTool.tsx +++ b/packages/sw/src/components/sw-tool/SWTool.tsx @@ -48,7 +48,8 @@ export const SWToolButton = forwardRef(fun if (appearance === "pill") { return ( diff --git a/packages/sw/src/index.ts b/packages/sw/src/index.ts index b5d339e..5592a90 100644 --- a/packages/sw/src/index.ts +++ b/packages/sw/src/index.ts @@ -21,3 +21,6 @@ export * from "./components/sw-viewer" // SW footer (2026-10-20): Figma 26:10097 bottom bar, command slots, category row, layout tabs and command line. export * from "./components/sw-footer" + +// SW process header (2026-11-03): the Strana Main Header as an independent SW general-mode header and its process area. +export * from "./components/sw-process-header" diff --git a/packages/sw/src/theme/sw-process-header.css b/packages/sw/src/theme/sw-process-header.css new file mode 100644 index 0000000..fad02f5 --- /dev/null +++ b/packages/sw/src/theme/sw-process-header.css @@ -0,0 +1,645 @@ +/* @hm-design/sw — SW Process Header (Strana Main Header, D:\Strana\Git_StranaDevDesign HEAD edda3cc: HeaderStyles.xaml, + HeaderBrushes.xaml, MainProcessBar.xaml, Hanmac Colors.xaml; 2026-11-03). SW general mode, an independent header + beside SWHeader / SWViewerHeader. Only --hm-sw-process-* tokens and hm-sw-process-* classes are new; values equal to + an SW token read that token. Never touches global selectors or SWHeader rules; the reused SW parts (home, file name, + tool pill) are adjusted only inside .hm-sw-process-header. Declared on the outermost SW scope only, like sw.css. */ + +:is(.hm-sw-header, .hm-sw-theme):not(:where(.hm-sw-header, .hm-sw-theme) *) { + /* Process area base (Header.Border.ProcessBase): ProcessBase gradient, DocBorder 1px, top corners 6. */ + --hm-sw-process-base-background: linear-gradient(180deg, #0c5941 0%, #19bf8d 10.1%, #109069 18.27%, #0c5941 100%); + --hm-sw-process-base-border-gradient: var(--hm-sw-header-document-border-gradient); + /* Cover over an unlit group, its arrow side and its outer rest (ProcessDim #E6004430). */ + --hm-sw-process-dim: rgba(0, 68, 48, 0.902); + + /* Group number and name (12px, line 14). */ + --hm-sw-process-number-color: #69c2a6; + --hm-sw-process-group-color: #c3ebdf; + --hm-sw-process-group-active-color: #fff; + + /* Lit group surface (ProcessGlowFace: three layer groups at 85 / 80 / 45%, over the shape bounds -4.95 .. 44.16), + halo (DropShadow 30, 85%) and edge light (3px, Blur 10, 65%). */ + --hm-sw-process-glow-face-base: linear-gradient(0deg, rgba(24, 134, 101, 0.702) 0%, rgba(24, 134, 101, 0) 40%), linear-gradient(180deg, #063b2b 0%, rgba(13, 101, 74, 0.051) 96.03%); + --hm-sw-process-glow-face-rim: linear-gradient(180deg, rgba(13, 101, 74, 0.051) 87.76%, #247a60 96.05%); + --hm-sw-process-glow-face-sheen: linear-gradient(rgba(0, 95, 66, 0.251), rgba(0, 95, 66, 0.251)), linear-gradient(180deg, #20322c 6.94%, #3dc79d 21.41%, rgba(61, 199, 157, 0.051) 96.05%); + --hm-sw-process-glow-halo: rgba(88, 181, 153, 0.85); + --hm-sw-process-glow-edge: rgba(88, 181, 153, 0.65); + /* Unlit outer group surface (ProcessShapeIdleFace, Blur 8, DropShadow 15 #11694F 80%, rim 1.5px Blur 2). */ + --hm-sw-process-idle-face: linear-gradient(180deg, rgba(0, 0, 0, 0.502) 10.08%, rgba(0, 0, 0, 0) 26.37%), linear-gradient(180deg, rgba(23, 42, 26, 0.251) 0%, rgba(23, 42, 26, 0.251) 62%, rgba(0, 0, 0, 0) 90%); + --hm-sw-process-idle-halo: rgba(17, 105, 79, 0.8); + --hm-sw-process-idle-rim: rgba(4, 128, 90, 0.322); + + /* Arrow between groups (16 x 48): fill, stroke stops, line shadow, shade over the next group. */ + --hm-sw-process-arrow-fill: #27d9a5; + --hm-sw-process-arrow-dim-fill: #abc7bf; + --hm-sw-process-arrow-fill-fade: rgba(16, 109, 81, 0); + --hm-sw-process-arrow-edge: #00432e; + --hm-sw-process-arrow-mid: #1c6a51; + --hm-sw-process-arrow-light: #3d9c7e; + --hm-sw-process-arrow-shadow: rgba(0, 0, 0, 0.502); + --hm-sw-process-shade: rgba(4, 34, 25, 0.878); + --hm-sw-process-shade-mid: rgba(4, 34, 25, 0.706); + --hm-sw-process-shade-fade: rgba(4, 34, 25, 0); + + /* Step tab (Header.RadioButton.ProcessTab, 32px): stacked layers, 70% unchecked / 100% checked / 50% disabled. */ + --hm-sw-process-tab-text: var(--hm-sw-header-title-color); + --hm-sw-process-tab-glow: rgba(31, 148, 113, 0.702); + --hm-sw-process-tab-depth: linear-gradient(180deg, rgba(0, 61, 41, 0) 0%, rgba(0, 61, 41, 0.8) 100%); + --hm-sw-process-tab-shade: linear-gradient(0deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0) 25%); + --hm-sw-process-tab-sheen: linear-gradient(180deg, rgba(74, 255, 196, 0.502) 0%, rgba(74, 255, 196, 0) 40%); + --hm-sw-process-tab-border-gradient: linear-gradient(180deg, #001e15 8.98%, #17a77b 22.9%, #0b533d 100%); + --hm-sw-process-tab-ring: rgba(0, 0, 0, 0.251); + --hm-sw-process-tab-active-base: linear-gradient(0deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 26.41%, rgba(0, 0, 0, 0.451) 70%, rgba(0, 0, 0, 0.4) 100%); + --hm-sw-process-tab-active-shade: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 80%); + --hm-sw-process-tab-active-sheen: linear-gradient(180deg, rgba(122, 206, 179, 0) 60%, rgba(122, 206, 179, 0.102) 100%); + --hm-sw-process-tab-inset: rgba(0, 0, 0, 0.659); + --hm-sw-process-tab-disabled-shade: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.8) 100%); + --hm-sw-process-tab-disabled-sheen: linear-gradient(180deg, rgba(130, 181, 164, 0.502) 0%, rgba(122, 206, 179, 0) 40%); + --hm-sw-process-tab-disabled-text: #64a491; + + /* Header frame parts that differ from SWHeader. */ + --hm-sw-process-layer-border-gradient: linear-gradient(180deg, #08251c 0%, #0e3d2f 14.57%, #1e7c60 37.06%, #08241c 80.5%, #051612 100%); + --hm-sw-process-pill-background: linear-gradient(180deg, rgba(0, 0, 0, 0.851) 0, rgba(0, 0, 0, 0.851) 2px, rgba(0, 0, 0, 0.8) 4px); + --hm-sw-process-pill-border-gradient: linear-gradient(180deg, #092920 0%, #1c6f56 115%); + --hm-sw-process-tool-border-gradient: linear-gradient(180deg, #08251c 0%, #208769 46.54%, #08241c 84.44%, #051612 100%); + --hm-sw-process-tool-icon-color: #b6d0c9; + --hm-sw-process-close-hover-background: #e40000; + --hm-sw-process-close-icon-hover: #fff; + + /* Viewport split tool (Strana 화면 분할, StrAna-SW 6758:12510): open button face, its 3px extension, the 233 x 36 + selector (face, shadow; the border is the SW hover gradient) and the cells (light frame, green panes, yellow when + hovered or checked), the button icon panes. */ + --hm-sw-process-tool-open-background: linear-gradient(180deg, #081813 0%, #0b3227 25.43%, #0e3d2f 51.25%, #0b3227 80.03%, #081813 100%); + --hm-sw-process-tool-open-edge: #081813; + --hm-sw-process-split-panel-background: #082018; + --hm-sw-process-split-panel-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); + --hm-sw-process-split-cell-frame: var(--hm-sw-process-tool-icon-color); + --hm-sw-process-split-cell-pane: #1b6b53; + --hm-sw-process-split-icon-pane: #003425; + + /* File name surface without a document (Figma 873:16481 "프로젝트 생성 전"): 10px 700 at the top right. */ + --hm-sw-process-document-empty-color: #aedbcc; + + /* appearance="multi" step tab (Figma 873:16481 / 873:17421, 25px, as wide as its label + 25). Base = a group not + reached yet (70%); lit = the lit group and the done groups; selected = the checked step; disabled = 50% on the + SW property surface. Text #F0F7F5 / selected #FFC600 are the SW title / selected tokens. */ + --hm-sw-process-multi-tab-background: linear-gradient(180deg, rgba(52, 143, 114, 0.8) 0%, rgba(2, 50, 35, 0.8) 100%); + --hm-sw-process-multi-tab-border: #0c3125; + --hm-sw-process-multi-tab-lit-background: linear-gradient(180deg, rgba(68, 178, 143, 0.8) 0%, rgba(0, 61, 41, 0.8) 100%); + --hm-sw-process-multi-tab-lit-border: #0a5940; + --hm-sw-process-multi-tab-shadow: 1px 1px 1px 0 rgba(31, 148, 113, 0.7), 1px 1px 1px 0 rgba(31, 148, 113, 0.7); + --hm-sw-process-multi-tab-selected-background: linear-gradient(0deg, #113228 0%, #28735c 100%); + --hm-sw-process-multi-tab-selected-border: #001e15; + --hm-sw-process-multi-tab-selected-shadow: 1px 1px 0.5px 0 rgba(31, 148, 113, 0.7), inset 3px 2px 3px 0 rgba(9, 39, 30, 0.42), inset 3px 2px 3px 0 rgba(9, 39, 30, 0.42); + --hm-sw-process-multi-tab-disabled-border: #021912; + --hm-sw-process-multi-tab-disabled-shadow: 1px 1px 1px 0 rgba(31, 148, 113, 0.7), inset 3px 2px 3px 0 rgba(9, 39, 30, 0.42), inset 3px 2px 3px 0 rgba(9, 39, 30, 0.42); + --hm-sw-process-multi-tab-disabled-text: #7d9f91; +} + +/* ---------- Header frame ---------- */ + +.hm-sw-process-header { + position: relative; + display: block; + width: 100%; + min-width: 0; + height: var(--hm-sw-header-height); + background: var(--hm-sw-header-background); + overflow: clip; +} +.hm-sw-process-header__bar { display: flex; align-items: stretch; height: var(--hm-sw-header-bar-height); min-width: 0; } + +/* 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. */ +.hm-sw-process-header__bar > .hm-sw-header__home { margin-right: -15px; } +.hm-sw-process-header__wordmark { + display: flex; + justify-content: center; + width: 58px; + color: var(--hm-sw-header-text-color); + font-size: 18px; + font-weight: 700; + line-height: var(--hm-sw-header-logo-height); + text-align: center; + white-space: nowrap; + text-shadow: var(--hm-sw-header-text-outline); +} +/* Logo menu open (host menu): the hover look stays, like Strana IsSubmenuOpen. */ +.hm-sw-process-header .hm-sw-header__home[aria-expanded="true"] .hm-sw-header__home-bg-hover { opacity: 1; } + +/* File name: SWHeaderDocumentTitle; only its width follows the Strana column (286*, 100 .. 286). */ +.hm-sw-process-header__bar > .hm-sw-header__document { flex: 286 1 0; min-width: 100px; max-width: 286px; } +/* No document list here, so no dropdown arrow (the Strana file name has none). */ +.hm-sw-process-header__bar > .hm-sw-header__document .hm-sw-header__arrow { display: none; } + +.hm-sw-process-header__center { display: flex; flex: 1260 1 0; min-width: 0; } +.hm-sw-process-header__center > .hm-sw-process-bar { flex: 1003 1 0; } + +/* Layer surface (Header.Border.LayerFace): SW object surface, 1px apart on both sides, 150 .. 255. */ +.hm-sw-process-header__layer { + display: flex; + align-items: center; + justify-content: center; + flex: 257 1 0; + min-width: 152px; + max-width: 257px; + position: relative; + height: var(--hm-sw-header-bar-height); +} +/* Surface on a pseudo element 1px in from both sides: no padding, border or margin on the flex item, so the + 1003 : 257 column ratio and its 152 .. 257 limits are the Strana ones exactly (surface 150 .. 255). */ +.hm-sw-process-header__layer::before { + content: ""; + position: absolute; + inset: 0 1px; + border: 1px solid transparent; + border-radius: var(--hm-sw-header-object-radius); + background: var(--hm-sw-header-object-background) padding-box, var(--hm-sw-header-object-border-gradient) border-box; +} +/* Layer field (Header.ToggleButton.LayerField): 38px, at most 234, property surface + layer border gradient. + The bulb and the field are sibling buttons; the field button covers the whole field under the bulb. */ +.hm-sw-process-header__layer-field { + position: relative; + display: flex; + align-items: center; + gap: 8px; + flex: 1 1 auto; + min-width: 0; + max-width: 234px; + height: var(--hm-sw-header-field-height); + margin: 0 9px; + padding: 0 10px 0 12px; + border: 1px solid transparent; + border-radius: var(--hm-sw-header-property-radius); + background: var(--hm-sw-header-property-background) padding-box, var(--hm-sw-process-layer-border-gradient) border-box; + box-shadow: var(--hm-sw-header-property-shadow); + color: var(--hm-sw-header-text-color); +} +.hm-sw-process-header__layer-field::after { + content: ""; + position: absolute; + inset: -1px; + padding: var(--hm-sw-header-hover-border-width); + border-radius: inherit; + background: var(--hm-sw-header-hover-border-gradient); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0); + opacity: 0; + pointer-events: none; +} +.hm-sw-process-header__layer-field:not([data-disabled]):hover::after, +.hm-sw-process-header__layer-field[data-open]::after { opacity: 1; } +.hm-sw-process-header__layer-field:not([data-disabled]):hover, +.hm-sw-process-header__layer-field[data-open] { color: var(--hm-sw-header-selected-color); } +.hm-sw-process-header__layer-field[data-disabled] { opacity: 0.4; } +.hm-sw-process-header .hm-sw-process-header__layer-field button:disabled { opacity: 1; } +.hm-sw-process-header__layer-button { position: absolute; inset: -1px; z-index: 0; border-radius: inherit; } +.hm-sw-process-header__layer-field > .hm-sw-header__layer-toggle { z-index: 1; } +.hm-sw-process-header__layer-text { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + min-width: 0; + padding: 0 var(--hm-sw-header-text-outline-room); + pointer-events: none; +} +.hm-sw-process-header__layer-label, +.hm-sw-process-header__layer-value { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +.hm-sw-process-header__layer-label { margin-left: 1px; font-size: var(--hm-sw-header-label-size); line-height: 14px; text-shadow: var(--hm-sw-header-label-outline); } +.hm-sw-process-header__layer-value { font-size: var(--hm-sw-header-font-size); line-height: 16px; text-shadow: var(--hm-sw-header-text-outline); } + +/* Window area (Header.Border.Window): DocFace + DocBorder 1px behind; the buttons cover the full 48px on top. */ +.hm-sw-process-header__window { position: relative; display: flex; flex: 0 0 auto; height: var(--hm-sw-header-bar-height); } +.hm-sw-process-header__window::before { + content: ""; + position: absolute; + inset: 0; + border: 1px solid transparent; + background: var(--hm-sw-header-document-background) padding-box, var(--hm-sw-header-document-border-gradient) border-box; + pointer-events: none; +} +.hm-sw-process-header__window > * { position: relative; } + +/* Tool pill: the SWToolGroup pill (40 x 38 buttons, 1px apart). `default` puts the Strana pill well and gradient + borders on it; `multi` keeps the SW pill as it is (Figma 873:16481 uses the SW pill: solid #092920, rgba(0,0,0,.8), + inner shadow, buttons with a solid #08251C border) at its own size. The selected (pressed) look — yellow ring and + icon — is added for both. */ +.hm-sw-process-header .hm-sw-process-header__window .hm-sw-process-header__tools { flex: 0 0 auto; height: 42px; margin: 3px 0 0 3px; } +.hm-sw-process-header:not([data-appearance="multi"]) .hm-sw-process-header__window .hm-sw-process-header__tools { + border-color: transparent; + background: var(--hm-sw-process-pill-background) padding-box, var(--hm-sw-process-pill-border-gradient) border-box; + box-shadow: none; +} +.hm-sw-process-header:not([data-appearance="multi"]) .hm-sw-process-header__tools .hm-sw-tool__pill-button { + border-color: transparent; + background: var(--hm-sw-header-document-background) padding-box, var(--hm-sw-process-tool-border-gradient) border-box; +} +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button { color: var(--hm-sw-process-tool-icon-color); } +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button:hover:not(:disabled), +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button[aria-pressed="true"] { color: var(--hm-sw-header-selected-color); } +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button[aria-pressed="true"]::after { opacity: 1; } +/* Pressed (not disabled) shows the hover picture too, like a hover that stays (SW pill hover icon = `hoverIcon`). */ +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button[aria-pressed="true"]:not(:disabled) > .hm-sw-header__icon { visibility: hidden; } +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__pill-button[aria-pressed="true"]:not(:disabled) > .hm-sw-header__icon-hover { visibility: visible; } +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__icon { inset: 0; } +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-tool__icon > :is(svg, img) { width: 24px; height: 24px; max-width: none; max-height: none; } + +/* Caption buttons (Header.WindowCaption, 32 x 48): glyph #ADB0AF; hover dark + white; close hover red + white. */ +.hm-sw-process-header__caption { display: block; flex: 0 0 32px; width: 32px; height: var(--hm-sw-header-bar-height); color: var(--hm-sw-header-window-icon-color); } +.hm-sw-process-header__caption > svg { display: block; } +.hm-sw-process-header__caption:hover:not(:disabled) { background: var(--hm-sw-header-window-button-hover-background); color: var(--hm-sw-header-window-icon-hover-color); } +.hm-sw-process-header__caption[data-kind="close"]:hover:not(:disabled) { background: var(--hm-sw-process-close-hover-background); color: var(--hm-sw-process-close-icon-hover); } + +/* ---------- Process area (SWProcessBar) ---------- */ + +.hm-sw-process-bar { + position: relative; + isolation: isolate; + display: flex; + align-items: stretch; + min-width: 0; + height: var(--hm-sw-header-bar-height); + /* Strana ClipToBounds: surfaces and the arrow shade never reach past the area (no scroll width when narrow). */ + overflow: clip; + color: var(--hm-sw-process-tab-text); +} +.hm-sw-process-bar::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + border: 1px solid transparent; + border-radius: 6px 6px 0 0; + background: var(--hm-sw-process-base-background) padding-box, var(--hm-sw-process-base-border-gradient) border-box; +} +/* Rest of the width on both sides: same brightness as the outer group next to it. */ +.hm-sw-process-bar__rest { position: relative; flex: 1 1 0; min-width: 0; } +.hm-sw-process-bar__rest[data-dim] { background: var(--hm-sw-process-dim); } +/* The cover keeps the base's 1px side borders and round corners visible (Strana margin 1, radius 5). */ +.hm-sw-process-bar__rest[data-side="start"] { margin-left: 1px; border-top-left-radius: 5px; } +.hm-sw-process-bar__rest[data-side="end"] { margin-right: 1px; border-top-right-radius: 5px; } + +/* Group: [20][number 4 name][12][tabs][20]. Natural width at most; shrinks with the others in proportion. */ +.hm-sw-process-bar__group { + position: relative; + display: flex; + align-items: center; + flex: 0 1 auto; + min-width: 0; + padding: 0 20px; +} +.hm-sw-process-bar__group[data-dim]::before { content: ""; position: absolute; inset: 0; z-index: 0; background: var(--hm-sw-process-dim); } +.hm-sw-process-bar__label, +.hm-sw-process-bar__steps { position: relative; z-index: 3; } + +.hm-sw-process-bar__label { + display: flex; + align-self: flex-start; + flex: 0 0 auto; + margin: 10.67px 12px 0 0; + font-size: var(--hm-sw-header-label-size); + line-height: 14px; + text-shadow: var(--hm-sw-header-label-outline); +} +.hm-sw-process-bar__label[data-lines="2"] { margin-top: 8.67px; } +.hm-sw-process-bar__number { flex: 0 0 auto; margin-right: 4px; color: var(--hm-sw-process-number-color); font-weight: 500; } +.hm-sw-process-bar__name { display: grid; max-width: 56px; } +.hm-sw-process-bar__name > * { grid-area: 1 / 1; } +.hm-sw-process-bar__name-reserve { font-weight: 700; visibility: hidden; } +.hm-sw-process-bar__name-text { justify-self: start; color: var(--hm-sw-process-group-color); font-weight: 500; } +.hm-sw-process-bar__group[data-active] .hm-sw-process-bar__name-text { color: var(--hm-sw-process-group-active-color); font-weight: 700; } + +/* Tabs (both appearances): a flex row where the checked tab shrinks with the others in the same proportion (2026-11-03, + it no longer keeps its whole name); a name without room ends with an ellipsis. default: all tabs share the row + equally (Strana UniformGrid, 127 each at most); multi overrides below. The group minimum (name + every tab at its + padding) is measured by SWProcessBar, so tabs never overlap. */ +.hm-sw-process-bar__steps { + display: flex; + column-gap: 1px; + align-items: center; + flex: 1 1 auto; + min-width: 0; +} +.hm-sw-process-bar__steps > .hm-sw-process-bar__step { flex: 1 1 0; } +.hm-sw-process-bar__step { + --hm-sw-process-step-radius: 4px; + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr); + align-items: center; + justify-items: center; + min-width: 0; + height: 32px; + padding: 0 12px 2.67px; + border-radius: var(--hm-sw-process-step-radius); + color: var(--hm-sw-process-tab-text); + font-size: 16px; + font-weight: 700; + line-height: 20px; + text-shadow: var(--hm-sw-header-text-outline); +} +.hm-sw-process-bar__step[data-shape="single"] { --hm-sw-process-step-radius: 16px; } +.hm-sw-process-bar__step[data-shape="start"] { --hm-sw-process-step-radius: 16px 4px 4px 16px; } +.hm-sw-process-bar__step[data-shape="end"] { --hm-sw-process-step-radius: 4px 16px 16px 4px; } +.hm-sw-process-bar__step-text, +.hm-sw-process-bar__step-size { grid-area: 1 / 1; } +/* The outline may paint into the room outside the clipped (ellipsis) text, like the SW file name. */ +.hm-sw-process-bar__step-text { position: relative; max-width: 100%; overflow: clip; overflow-clip-margin: var(--hm-sw-header-text-outline-room); white-space: nowrap; text-overflow: ellipsis; } +/* Natural width 127 (Strana MinWidth) = 103 + 24 padding; never wider than the column. */ +.hm-sw-process-bar__step-size { display: block; width: 103px; max-width: 100%; height: 0; } + +.hm-sw-process-bar__step-box { position: absolute; inset: 0; border-radius: inherit; opacity: 0.7; pointer-events: none; } +.hm-sw-process-bar__step-box > i { position: absolute; inset: 0; border-radius: inherit; } +.hm-sw-process-bar__step-box > [data-layer^="glow"] { inset: 1px -1px -1px 1px; border-right: 1px solid var(--hm-sw-process-tab-glow); border-bottom: 1px solid var(--hm-sw-process-tab-glow); } +.hm-sw-process-bar__step-box > [data-layer="depth"] { background: var(--hm-sw-process-tab-depth); } +.hm-sw-process-bar__step-box > [data-layer="shade"] { background: var(--hm-sw-process-tab-shade); } +.hm-sw-process-bar__step-box > [data-layer="sheen"] { background: var(--hm-sw-process-tab-sheen); } +.hm-sw-process-bar__step-box > [data-layer="frame"] { + --hm-sw-process-frame: 1px; + padding: var(--hm-sw-process-frame); + background: var(--hm-sw-process-tab-border-gradient); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0); + opacity: 0.85; +} +.hm-sw-process-bar__step-box > [data-layer^="inset"] { display: none; } +.hm-sw-process-bar__step-box > [data-layer="inset-top"] { background: linear-gradient(180deg, var(--hm-sw-process-tab-inset) 0, transparent 5px); } +.hm-sw-process-bar__step-box > [data-layer="inset-left"] { background: linear-gradient(90deg, var(--hm-sw-process-tab-inset) 0, transparent 6px); } +.hm-sw-process-bar__step-box > [data-layer="ring"] { border: 2px solid var(--hm-sw-process-tab-ring); } + +.hm-sw-process-bar__step:hover:not(:disabled) { color: var(--hm-sw-header-selected-color); } +.hm-sw-process-bar__step[aria-checked="true"] { color: var(--hm-sw-header-selected-color); } +.hm-sw-process-bar__step[aria-checked="true"] > .hm-sw-process-bar__step-box { opacity: 1; } +.hm-sw-process-bar__step[aria-checked="true"] [data-layer="depth"] { background: var(--hm-sw-process-tab-active-base); } +.hm-sw-process-bar__step[aria-checked="true"] [data-layer="shade"] { background: var(--hm-sw-process-tab-active-shade); } +.hm-sw-process-bar__step[aria-checked="true"] [data-layer="sheen"] { background: var(--hm-sw-process-tab-active-sheen); } +.hm-sw-process-bar__step[aria-checked="true"] [data-layer^="inset"] { display: block; opacity: 0.75; } +.hm-sw-process-bar__step[aria-checked="true"] [data-layer="frame"] { --hm-sw-process-frame: 2px; opacity: 0.25; } +.hm-sw-process-bar__step[aria-checked="true"] [data-layer="glow2"] { display: none; } +/* Disabled (after Checked, like the Strana trigger order). The SW button reset's 40% is replaced by the tab look. */ +.hm-sw-process-bar .hm-sw-process-bar__step:disabled { opacity: 1; color: var(--hm-sw-process-tab-disabled-text); cursor: default; } +.hm-sw-process-bar__step:disabled > .hm-sw-process-bar__step-box { opacity: 0.5; } +.hm-sw-process-bar__step:disabled [data-layer="depth"] { background: var(--hm-sw-process-tab-active-base); } +.hm-sw-process-bar__step:disabled [data-layer="shade"] { background: var(--hm-sw-process-tab-disabled-shade); } +.hm-sw-process-bar__step:disabled [data-layer="sheen"] { background: var(--hm-sw-process-tab-disabled-sheen); } +.hm-sw-process-bar__step:disabled [data-layer^="inset"] { display: block; } +.hm-sw-process-bar__step:disabled [data-layer="glow2"] { display: none; } + +/* Group surfaces: 40px out on the left of the first group, mirrored and 44px out on the right of the last one. + Clipped to their box like the Strana ClipToBounds. */ +.hm-sw-process-bar__shape { position: absolute; top: 0; left: calc(-1 * var(--hm-sw-process-shape-start, 40px)); right: 0; z-index: 1; height: var(--hm-sw-header-bar-height); overflow: hidden; pointer-events: none; } +.hm-sw-process-bar__shape[data-mirrored] { left: 0; right: calc(-1 * var(--hm-sw-process-shape-end, 44px)); transform: scaleX(-1); } +.hm-sw-process-bar__shape > * { position: absolute; } + +/* Lit: halo around the face (filter on the parent so the clipped face still glows), face fading at the arrow end. */ +.hm-sw-process-bar__glow-halo { inset: 0; filter: drop-shadow(0 0 30px var(--hm-sw-process-glow-halo)); } +.hm-sw-process-bar__glow-face { + position: absolute; + left: 0; + right: 0; + top: -4.9526px; + height: 49.1162px; + clip-path: path("M38.99,8.97 C38.41,4.2025 42.13,0 46.94,0 H99999 V49.1162 H50.98 C46.94,49.1162 43.53,46.0995 43.04,42.0862 Z"); + -webkit-mask: linear-gradient(90deg, #000 calc(100% - 83.4px), transparent calc(100% - 7.4px)); + mask: linear-gradient(90deg, #000 calc(100% - 83.4px), transparent calc(100% - 7.4px)); +} +.hm-sw-process-bar__glow-face > i { position: absolute; inset: 0; } +.hm-sw-process-bar__glow-face > i:nth-child(1) { background: var(--hm-sw-process-glow-face-base); opacity: 0.85; } +.hm-sw-process-bar__glow-face > i:nth-child(2) { background: var(--hm-sw-process-glow-face-rim); opacity: 0.8; } +.hm-sw-process-bar__glow-face > i:nth-child(3) { background: var(--hm-sw-process-glow-face-sheen); opacity: 0.45; } +.hm-sw-process-bar__glow-edge { + left: 0; + top: 0; + fill: none; + stroke: var(--hm-sw-process-glow-edge); + stroke-width: 3; + filter: blur(5px); + -webkit-mask: linear-gradient(180deg, #000 64%, rgba(0, 0, 0, 0.302) 91%); + mask: linear-gradient(180deg, #000 64%, rgba(0, 0, 0, 0.302) 91%); +} + +/* Unlit: blurred face with a dark halo and a rim light, fading from the outer end. */ +.hm-sw-process-bar__shape[data-kind="idle"] { + -webkit-mask: linear-gradient(90deg, #000 79.8px, transparent 198.4px); + mask: linear-gradient(90deg, #000 79.8px, transparent 198.4px); +} +.hm-sw-process-bar__shape[data-kind="idle"][data-mirrored] { + -webkit-mask: linear-gradient(90deg, #000 236.7px, transparent 273.1px); + mask: linear-gradient(90deg, #000 236.7px, transparent 273.1px); +} +.hm-sw-process-bar__idle-halo { inset: 0; filter: drop-shadow(0 0 15px var(--hm-sw-process-idle-halo)); } +.hm-sw-process-bar__idle-blur { position: absolute; inset: 0; filter: blur(4px); } +.hm-sw-process-bar__idle-face { + position: absolute; + left: 0; + right: 0; + top: -4.95264px; + height: 49.2497px; + background: var(--hm-sw-process-idle-face); + clip-path: path("M99999,0 H47.153 C42.095,0 38.306,4.63511 39.313,9.59189 L46.064,42.84164 C46.821,46.57024 50.099,49.24974 53.904,49.24974 H99999 Z"); +} +.hm-sw-process-bar__idle-rim { left: 0; top: 0; fill: none; stroke: var(--hm-sw-process-idle-rim); stroke-width: 1.5; filter: blur(1px); } +.hm-sw-process-bar__idle-rim > rect { fill: var(--hm-sw-process-idle-rim); stroke: none; } + +/* Arrow cell (16px): dim cover under the surfaces, the arrow above them, its shade over the next group. */ +.hm-sw-process-bar__arrow { position: relative; flex: 0 0 16px; width: 16px; } +.hm-sw-process-bar__arrow > svg { position: absolute; left: 0; top: 0; display: block; } +.hm-sw-process-bar__arrow-dim { z-index: 0; height: 100%; fill: var(--hm-sw-process-dim); } +.hm-sw-process-bar__arrow-art { z-index: 2; pointer-events: none; } +.hm-sw-process-bar__arrow-shadow { stroke: var(--hm-sw-process-arrow-shadow); } + +/* ---------- Shared additions (both appearances) ---------- */ + +/* A group name kept for assistive technology only (`labelHidden`). */ +.hm-sw-process-bar__label[data-label-hidden] { display: none; } + +/* No document (`documentEmptyLabel`, Figma 873:16481): a small label at the top right of the file-name surface. */ +.hm-sw-process-header[data-document-empty] .hm-sw-header__document-button { align-items: flex-start; } +.hm-sw-process-header[data-document-empty] .hm-sw-header__document-name { + margin-top: 7px; + font-size: 10px; + line-height: 14px; + letter-spacing: -0.1px; + 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); } + +/* ---------- 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. + Text outline, hover, focus and the window-move motion are the shared SW rules (Figma texts have no stroke; the SW + outline is applied on purpose, 2026-11-03 user request). */ + +/* Frame: 49px surfaces (no 1px gap). Logo and file-name text as the default (SWHeader). */ +.hm-sw-theme.hm-sw-process-header[data-appearance="multi"] { --hm-sw-header-bar-height: 49px; } +/* SWHeader window move at the Figma place of this frame: stripes 82 x 39 at (3, 5), the icon (and the motion with its + tooltip) 3px higher, at (28, 10). Only positions; the motion itself is the SWHeader one. */ +.hm-sw-process-header[data-appearance="multi"] .hm-sw-header__motion { background-position: 3px 5px; background-size: 82px 39px; } +.hm-sw-process-header[data-appearance="multi"] .hm-sw-header__motion > * { translate: 0 -3px; } +/* File name: 189 at most (Figma 873:16254), solid #0E4C3A border. */ +.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__bar > .hm-sw-header__document { flex: 189 1 0; max-width: 189px; } +.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__bar > .hm-sw-header__document .hm-sw-header__document-button { + background: var(--hm-sw-header-document-background) padding-box, linear-gradient(var(--hm-sw-header-document-border-color) 0 0) border-box; +} +.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__center { flex: 1278 1 0; } +.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__window::before { + background: var(--hm-sw-header-document-background) padding-box, linear-gradient(var(--hm-sw-header-document-border-color) 0 0) border-box; +} +/* SW pill at the Figma place: 41 high, 4 from the top, 5 from the window start, 2 before the window move. */ +.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__window .hm-sw-process-header__tools { height: 41px; margin: 4px 2px 0 5px; } +.hm-sw-process-header[data-appearance="multi"] .hm-sw-process-header__tools .hm-sw-tool__pill-button { height: 37px; } + +/* Process area: 49px, groups [10][title 14 / tabs 25][10] with the arrow between (content to content 36). */ +.hm-sw-process-bar[data-appearance="multi"] { + --hm-sw-header-bar-height: 49px; + --hm-sw-process-shape-start: 50px; + --hm-sw-process-shape-end: 48px; +} +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__group { + flex-direction: column; + align-items: flex-start; + padding: 3.8px 10px 0; +} +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__label { + align-self: stretch; + height: 14px; + min-width: 0; + margin: 0; +} +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__label[data-label-hidden] { display: flex; visibility: hidden; } +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__number { margin-right: 2px; } +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__name { grid-template-columns: minmax(0, 1fr); flex: 0 1 auto; min-width: 0; max-width: none; } +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__name > * { + overflow: clip; + overflow-clip-margin: var(--hm-sw-header-text-outline-room); + white-space: nowrap; + text-overflow: ellipsis; +} +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__group[data-progress="done"] .hm-sw-process-bar__name-text { + color: var(--hm-sw-process-group-active-color); + font-weight: 700; +} + +/* Tabs as wide as their label (padding 13 / 12), no gap; ends round (50). All of them, the checked one included, + shrink in proportion to that width with an ellipsis. */ +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__steps { + align-items: stretch; + flex: 0 1 auto; + max-width: 100%; + margin-top: 1.9px; +} +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__steps > .hm-sw-process-bar__step { flex: 0 1 auto; } +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__step { + --hm-sw-process-step-radius: 0; + height: 25px; + padding: 0 12px 0 13px; + font-size: 14px; + line-height: 23px; +} +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__step[data-shape="single"] { --hm-sw-process-step-radius: 50px; } +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__step[data-shape="start"] { --hm-sw-process-step-radius: 50px 0 0 50px; } +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__step[data-shape="end"] { --hm-sw-process-step-radius: 0 50px 50px 0; } +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__step-size { display: none; } + +/* One face instead of the default stacked layers. */ +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__step-box { + border: 1px solid var(--hm-sw-process-multi-tab-border); + background: var(--hm-sw-process-multi-tab-background); + box-shadow: var(--hm-sw-process-multi-tab-shadow); +} +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__step-box > i { display: none; } +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__group:is([data-active], [data-progress="done"]) .hm-sw-process-bar__step-box { + border-color: var(--hm-sw-process-multi-tab-lit-border); + background: var(--hm-sw-process-multi-tab-lit-background); + opacity: 1; +} +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__group .hm-sw-process-bar__step[aria-checked="true"] > .hm-sw-process-bar__step-box { + border-color: var(--hm-sw-process-multi-tab-selected-border); + background: var(--hm-sw-process-multi-tab-selected-background); + box-shadow: var(--hm-sw-process-multi-tab-selected-shadow); + opacity: 1; +} +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__step:disabled { color: var(--hm-sw-process-multi-tab-disabled-text); } +.hm-sw-process-bar[data-appearance="multi"] .hm-sw-process-bar__group .hm-sw-process-bar__step:disabled > .hm-sw-process-bar__step-box { + border-color: var(--hm-sw-process-multi-tab-disabled-border); + background: var(--hm-sw-header-property-background); + box-shadow: var(--hm-sw-process-multi-tab-disabled-shadow); + opacity: 0.5; +} + +/* ---------- Viewport split tool (SWProcessSplitButton, both appearances) ---------- */ + +.hm-sw-process-split { position: relative; display: flex; } +.hm-sw-process-split__icon-pane { fill: var(--hm-sw-process-split-icon-pane); stroke: #000; stroke-width: 0.962; } +.hm-sw-process-split__icon > rect:first-child { stroke: #000; stroke-width: 0.962; } + +/* Open (Header.ToggleButton.ToolMenu checked): darker face, gold top and sides, open at the bottom, no hover ring, + yellow icon. The bottom border is replaced by padding so the icon never moves. */ +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-process-split[data-open] > .hm-sw-tool__pill-button { + padding-bottom: 1px; + border-color: transparent; + border-bottom-width: 0; + border-radius: 2px 2px 0 0; + background: var(--hm-sw-process-tool-open-background) padding-box, var(--hm-sw-header-hover-border-gradient) border-box; + color: var(--hm-sw-header-selected-color); +} +.hm-sw-process-header .hm-sw-process-header__tools .hm-sw-process-split[data-open] > .hm-sw-tool__pill-button::after { opacity: 0; } + +/* Selector: fixed overlay 1px over the button bottom (no gap at any zoom), aligned to the button left (shifted only at + the screen edge — --hm-sw-process-split-offset keeps the extension under the button). The extension reaches the + header bottom (--hm-sw-process-split-tail, Strana 3px), so the panel never covers the window buttons. */ +.hm-sw-process-split__popup { position: fixed; z-index: 30; width: 233px; height: calc(var(--hm-sw-process-split-tail, 3px) + 36px); } +.hm-sw-process-split__tail { + position: absolute; + top: 0; + left: var(--hm-sw-process-split-offset, 0px); + width: 40px; + height: var(--hm-sw-process-split-tail, 3px); + border: solid transparent; + border-width: 0 1px; + background: linear-gradient(var(--hm-sw-process-tool-open-edge), var(--hm-sw-process-tool-open-edge)) padding-box, var(--hm-sw-header-hover-border-gradient) border-box; +} +.hm-sw-process-split__panel { + position: absolute; + top: var(--hm-sw-process-split-tail, 3px); + left: 0; + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + width: 233px; + height: 36px; + padding: 0.5px 0; + border: 1px solid transparent; + border-top-width: 0; + border-radius: 0 2px 2px 2px; + background: linear-gradient(var(--hm-sw-process-split-panel-background), var(--hm-sw-process-split-panel-background)) padding-box, var(--hm-sw-header-hover-border-gradient) border-box; + box-shadow: var(--hm-sw-process-split-panel-shadow); +} +/* Gold line on the panel top, from the button right edge to the panel end (Header.Border.ToolMenuStep). */ +.hm-sw-process-split__step { + position: absolute; + top: var(--hm-sw-process-split-tail, 3px); + left: calc(var(--hm-sw-process-split-offset, 0px) + 39px); + right: 0; + height: 1px; + border-top-right-radius: 2px; + background: var(--hm-sw-header-hover-border-gradient) 0 0 / 100% 36px no-repeat; + pointer-events: none; +} +.hm-sw-process-split__cell { display: grid; place-items: center; height: 34px; } +/* Whole-pixel frame around the 29.6 x 19.73 picture (Strana 31.6 x 21.73): a fractional box is snapped to 21 or 22 rows + depending on where the popup sits, and at 21 the light edge under the panes vanished (icons looked cut at 100%). */ +.hm-sw-process-split__frame { + display: block; + width: 32px; + height: 22px; + border: 1px solid #000; + border-radius: 2px; + background: var(--hm-sw-process-split-cell-frame); +} +.hm-sw-process-split__frame > svg { display: block; overflow: visible; } +.hm-sw-process-split__pane { fill: var(--hm-sw-process-split-cell-pane); stroke: rgba(0, 0, 0, 0.3); stroke-width: 1; } +.hm-sw-process-split__cell:is(:hover, [aria-checked="true"]) .hm-sw-process-split__pane { fill: var(--hm-sw-header-selected-color); } diff --git a/src/sw/assets/process-header/tool-controls-hover.svg b/src/sw/assets/process-header/tool-controls-hover.svg new file mode 100644 index 0000000..32409b1 --- /dev/null +++ b/src/sw/assets/process-header/tool-controls-hover.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/process-header/tool-controls.svg b/src/sw/assets/process-header/tool-controls.svg new file mode 100644 index 0000000..edf1ee9 --- /dev/null +++ b/src/sw/assets/process-header/tool-controls.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/process-header/tool-exit-hover.svg b/src/sw/assets/process-header/tool-exit-hover.svg new file mode 100644 index 0000000..5725bbc --- /dev/null +++ b/src/sw/assets/process-header/tool-exit-hover.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/sw/assets/process-header/tool-exit.svg b/src/sw/assets/process-header/tool-exit.svg new file mode 100644 index 0000000..5a99397 --- /dev/null +++ b/src/sw/assets/process-header/tool-exit.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/sw/assets/process-header/tool-help-hover.svg b/src/sw/assets/process-header/tool-help-hover.svg new file mode 100644 index 0000000..8a8925a --- /dev/null +++ b/src/sw/assets/process-header/tool-help-hover.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/process-header/tool-help.svg b/src/sw/assets/process-header/tool-help.svg new file mode 100644 index 0000000..056aa3a --- /dev/null +++ b/src/sw/assets/process-header/tool-help.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/process-header/tool-menu-list-hover.svg b/src/sw/assets/process-header/tool-menu-list-hover.svg new file mode 100644 index 0000000..11ad147 --- /dev/null +++ b/src/sw/assets/process-header/tool-menu-list-hover.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/sw/assets/process-header/tool-menu-list.svg b/src/sw/assets/process-header/tool-menu-list.svg new file mode 100644 index 0000000..44ef267 --- /dev/null +++ b/src/sw/assets/process-header/tool-menu-list.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/sw/assets/process-header/tool-view-3d-hover.svg b/src/sw/assets/process-header/tool-view-3d-hover.svg new file mode 100644 index 0000000..8986a57 --- /dev/null +++ b/src/sw/assets/process-header/tool-view-3d-hover.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/process-header/tool-view-3d.svg b/src/sw/assets/process-header/tool-view-3d.svg new file mode 100644 index 0000000..9e736a5 --- /dev/null +++ b/src/sw/assets/process-header/tool-view-3d.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/sw-process-header-fixture.tsx b/src/sw/sw-process-header-fixture.tsx new file mode 100644 index 0000000..b40c4c0 --- /dev/null +++ b/src/sw/sw-process-header-fixture.tsx @@ -0,0 +1,142 @@ +import { useState, type CSSProperties } from "react" +import type { SWProcessGroup, SWProcessSplitType, SWProcessStep } from "@hm-design/sw" + +// SW/Step Header demo data and tool icons (2026-11-03). Like a consuming program: the package ships no product +// icons, so the Strana tool pictures (HeaderStyles.xaml Header.Icon.*) live here. Fills that change with the button +// state use currentColor (the pill button color: #B6D0C9, hover / pressed #FFC600). + +/** Strana Main Header process data (DesignPreviewData.cs). */ +export const PROCESS_GROUPS: SWProcessGroup[] = [ + { id: "modeling", number: "01", label: "Modeling", steps: [{ id: "structure", label: "Structure" }, { id: "loads", label: "Loads" }] }, + { id: "analysis", number: "02", label: "Analysis Result", steps: [{ id: "results", label: "Results" }] }, +] + +/** + * StrAna-SW multi step data (Figma 873:16481 / 873:16254 / 873:17421). Groups without a Figma title keep their name for + * assistive technology only; the last two groups are not available yet (gray). + */ +export const MULTI_GROUPS: SWProcessGroup[] = [ + { id: "data", label: "데이터 구축", steps: [{ id: "data-input", label: "데이터입력" }, { id: "data-check", label: "데이터확인" }] }, + { id: "alignment", label: "선형 입력", labelHidden: true, steps: [{ id: "alignment-input", label: "선형입력" }] }, + { + id: "structure", label: "구조물 상세 및 계획", + steps: [{ id: "body-plan", label: "본체계획" }, { id: "body-detail", label: "본체상세" }, { id: "wing-plan", label: "날개벽계획" }, { id: "wing-detail", label: "날개벽상세" }], + }, + { id: "condition", label: "설계 및 해석조건", labelHidden: true, disabled: true, steps: [{ id: "condition", label: "설계 및 해석조건" }] }, + { + id: "analysis", label: "해석 및 성과품", labelHidden: true, disabled: true, + steps: [{ id: "analysis-model", label: "해석모델" }, { id: "design-review", label: "설계검토" }, { id: "deliverable", label: "성과품" }], + }, +] + +/** More groups than the Figma frame (8). */ +export const MULTI_MANY_GROUPS: SWProcessGroup[] = [ + ...MULTI_GROUPS.slice(0, 3), + { id: "drawing", label: "도면 작성", steps: [{ id: "drawing-create", label: "도면생성" }, { id: "drawing-edit", label: "도면편집" }] }, + { id: "quantity", label: "수량 산출", labelHidden: true, steps: [{ id: "quantity", label: "수량산출" }] }, + ...MULTI_GROUPS.slice(3), + { id: "report", label: "보고서", disabled: true, steps: [{ id: "report-write", label: "보고서작성" }, { id: "report-print", label: "출력" }] }, +] + +/** More steps in one group (8 in 구조물 상세 및 계획). */ +export const MULTI_MANY_STEPS: SWProcessGroup[] = MULTI_GROUPS.map(group => group.id !== "structure" ? group : { + ...group, + steps: [ + ...group.steps, + { id: "base-plan", label: "기초계획" }, { id: "base-detail", label: "기초상세" }, { id: "drain-plan", label: "배수계획" }, { id: "drain-detail", label: "배수상세" }, + ], +}) + +const OUTLINE = "#000" + +export function ViewPropertyIcon() { + return ( + + ) +} + +/** Sub window button picture: two monitors; the back one brightens on hover (story CSS, like Strana). */ +export function SubWindowIcon() { + return ( + + ) +} + +/** + * Demo program state: every header state is the program's; actions without state (tools, minimize, close, file name, + * window move) have nothing to show and are no-ops. + */ +export function useProcessHeaderDemo(initialStep = "structure") { + const [stepId, setStepId] = useState(initialStep) + const [layerVisible, setLayerVisible] = useState(true) + const [layerOpen, setLayerOpen] = useState(false) + const [homeOpen, setHomeOpen] = useState(false) + const [viewProperty, setViewProperty] = useState(false) + const [maximized, setMaximized] = useState(true) + const [splitType, setSplitType] = useState("grid") + const [splitOpen, setSplitOpen] = useState(false) + const none = () => {} + return { + stepId, layerVisible, layerOpen, homeOpen, viewProperty, maximized, splitType, splitOpen, + onSplitTypeChange: setSplitType, + onSplitOpenChange: setSplitOpen, + onStepSelect: (step: SWProcessStep) => setStepId(step.id), + onLayerVisibilityChange: setLayerVisible, + onLayerClick: () => setLayerOpen(open => !open), + onHomeClick: () => setHomeOpen(open => !open), + onViewProperty: () => setViewProperty(on => !on), + onTool: (_name: string) => none, + onMaximizeToggle: () => setMaximized(on => !on), + onMinimize: none, + onClose: none, + onDocumentClick: none, + onWindowMoveStart: none, + } +} + +/** + * Strana Dark theme through token overrides on the outermost SW scope (docs/sw/sw-process-header-design-spec.md §7). + * Partial on purpose: shows the mechanism with the solid colors and the main surfaces; the rest keeps Hanmac. + */ +export const DARK_OVERRIDE = { + "--hm-sw-header-background": "#0b0d11", + "--hm-sw-header-text-color": "#f4f7fa", + "--hm-sw-header-title-color": "#f4f7fa", + "--hm-sw-header-selected-color": "#ffc833", + "--hm-sw-header-stripe-color": "rgba(63, 72, 86, 0.322)", + "--hm-sw-header-document-background": "linear-gradient(180deg, #12151a 0%, #181c23 8.69%, #1e232b 25.43%, #262c36 51.25%, #1e232b 80.03%, #1a1e25 89.76%, #12151a 100%)", + "--hm-sw-header-document-border-gradient": "linear-gradient(180deg, #3a4250 0%, #0b0d11 13.73%, #0b0d11 89.51%, #3f4856 100%)", + "--hm-sw-header-object-background": "linear-gradient(0deg, #1c2027 0%, #20252d 6%, #22272f 10%, #1a1e25 18%, #353d48 72%, #2a3039 83%, #1c2027 92%, #1e232b 100%)", + "--hm-sw-header-object-border-gradient": "linear-gradient(180deg, #313843 0%, #1e232b 13.57%, #74c4b4 37.06%, #12151a 80.5%, #2e353f 100%)", + "--hm-sw-process-base-background": "linear-gradient(180deg, #1c2027 0%, #4a5260 10.1%, #313843 18.27%, #1c2027 100%)", + "--hm-sw-process-dim": "rgba(11, 13, 17, 0.902)", + "--hm-sw-process-number-color": "#74c4b4", + "--hm-sw-process-group-color": "#c9d3de", + "--hm-sw-process-glow-halo": "rgba(116, 196, 180, 0.85)", + "--hm-sw-process-arrow-fill": "#74c4b4", + "--hm-sw-process-arrow-edge": "#1a1e25", + "--hm-sw-process-arrow-mid": "#4a5260", + "--hm-sw-process-arrow-light": "#5e8f87", + "--hm-sw-process-tab-glow": "rgba(74, 82, 96, 0.702)", + "--hm-sw-process-tab-border-gradient": "linear-gradient(180deg, #0b0d11 8.98%, #4a5260 22.9%, #262c36 100%)", + "--hm-sw-process-tab-depth": "linear-gradient(180deg, rgba(26, 30, 37, 0) 0%, rgba(26, 30, 37, 0.8) 100%)", + "--hm-sw-process-tab-sheen": "linear-gradient(180deg, rgba(116, 196, 180, 0.502) 0%, rgba(116, 196, 180, 0) 40%)", + "--hm-sw-process-tab-disabled-text": "#6d7784", + "--hm-sw-process-tool-icon-color": "#c9d3de", +} as CSSProperties diff --git a/src/sw/sw-process-header-stories.css b/src/sw/sw-process-header-stories.css new file mode 100644 index 0000000..503b9ab --- /dev/null +++ b/src/sw/sw-process-header-stories.css @@ -0,0 +1,8 @@ +/* Story-owned layout for SW/Step Header. Like a consuming program it arranges public components and its own + fixture icons; it never styles hm-sw-* classes. Rows, stacks and scrollers are the shared SW story classes. */ +.sw-process-story-screen { width: 100%; max-width: 1920px; background: #060606; } +.sw-process-story-canvas { height: 120px; background: var(--sw-story-canvas, #1b1b1b); } +.sw-process-story-surface { padding: 12px; border-radius: 4px; background: #060606; } +/* Strana Header.Icon.ToolSubMonitor: the back monitor is 20% and 50% while its button is hovered or pressed. */ +.sw-process-story-sub-icon__back { opacity: 0.2; } +button:is(:hover, [aria-pressed="true"]) .sw-process-story-sub-icon__back { opacity: 0.5; } diff --git a/src/sw/sw-process-header.stories.tsx b/src/sw/sw-process-header.stories.tsx new file mode 100644 index 0000000..2aec98b --- /dev/null +++ b/src/sw/sw-process-header.stories.tsx @@ -0,0 +1,350 @@ +import { useState, type ReactNode } from "react" +import type { Meta, StoryObj } from "@storybook/react-vite" +import { + SWProcessBar, SWProcessHeader, SWProcessSplitButton, SWTheme, SWToolButton, type SWProcessGroup, type SWProcessHeaderProps, type SWProcessSplitType, +} from "@hm-design/sw" +import "@hm-design/sw/styles.css" +import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import { + DARK_OVERRIDE, MULTI_GROUPS, MULTI_MANY_GROUPS, MULTI_MANY_STEPS, PROCESS_GROUPS, SubWindowIcon, useProcessHeaderDemo, ViewPropertyIcon, +} from "./sw-process-header-fixture" +import "./sw-stories.css" +import "./sw-process-header-stories.css" +import placeholderLogo from "./assets/sw-header-logo-placeholder.svg" +import toolHelp from "./assets/process-header/tool-help.svg" +import toolView3d from "./assets/process-header/tool-view-3d.svg" +import toolControls from "./assets/process-header/tool-controls.svg" +import toolMenuList from "./assets/process-header/tool-menu-list.svg" +import toolExit from "./assets/process-header/tool-exit.svg" +import toolHelpHover from "./assets/process-header/tool-help-hover.svg" +import toolView3dHover from "./assets/process-header/tool-view-3d-hover.svg" +import toolControlsHover from "./assets/process-header/tool-controls-hover.svg" +import toolMenuListHover from "./assets/process-header/tool-menu-list-hover.svg" +import toolExitHover from "./assets/process-header/tool-exit-hover.svg" + +// SW/Step Header (2026-11-03, shown as "Step Header"; the components keep the SWProcess* names): the Strana Main Header as an independent SW general-mode header. Public API only, +// like a consuming program. The logo is the temporary SW placeholder through `logoSrc` (replace with the real logo). +// Page layout as SW/Header and SW/Viewer: title, then sections; checks stay as verification stories (hidden). +const KICKER = "HM DESIGN SYSTEM / SW" +const LOGO = { logoSrc: placeholderLogo, logoAlt: "임시 로고" } satisfies Partial + +const meta = { + title: "SW/Step Header", + parameters: { layout: "fullscreen", controls: { disable: true } }, +} satisfies Meta +export default meta +type Story = StoryObj + +interface SplitState { value: SWProcessSplitType; open?: boolean; defaultOpen?: boolean; onValueChange?: (value: SWProcessSplitType) => void; onOpenChange?: (open: boolean) => void } + +/** The three Strana tools as host content of the pill: fixture icons (SWToolButton) and the package split tool. */ +function Tools({ viewProperty = false, onViewProperty, onTool, split = { value: "grid" } }: { + viewProperty?: boolean; onViewProperty?: () => void; onTool?: (name: string) => () => void; split?: SplitState +}) { + return ( + <> + } pressed={viewProperty} onClick={onViewProperty} title="View Property">View Property + + } onClick={onTool?.("subWindow")} title="Sub 창">Sub 창 + + ) +} + +/** A header with fixed demo state (no program behind it). */ +function StaticHeader({ step = "structure", ...props }: Partial & { step?: string }) { + return ( + } maximized {...props} /> + ) +} + +function LiveHeader() { + const demo = useProcessHeaderDemo() + return ( +
+ } + onWindowMoveStart={demo.onWindowMoveStart} maximized={demo.maximized} + onMinimize={demo.onMinimize} onMaximizeToggle={demo.onMaximizeToggle} onClose={demo.onClose} /> +
+
+ ) +} + +/** + * The five Figma tools (fixture icons from the Figma file; the package ships no product icons). Like the SW print + * button, each has a hover picture with the #B6D0C9 fill in #FFC600 (`hoverIcon`). + */ +function MultiTools({ onTool, split }: { onTool?: (name: string) => () => void; split?: SplitState }) { + const tool = (name: string, src: string, hover: string, label: string) => ( + } hoverIcon={} onClick={onTool?.(name)} title={label}>{label} + ) + return ( + <> + {tool("help", toolHelp, toolHelpHover, "도움말")}{tool("view3d", toolView3d, toolView3dHover, "3D 보기")} + {split && } + {tool("controls", toolControls, toolControlsHover, "설정")}{tool("menu", toolMenuList, toolMenuListHover, "메뉴")} + {tool("exit", toolExit, toolExitHover, "나가기")} + + ) +} + +/** Figma multi step frame: no layer field, only minimize and close. */ +function MultiHeader({ step = "data-input", groups = MULTI_GROUPS, ...props }: Partial & { step?: string; groups?: SWProcessGroup[] }) { + return ( + } hiddenControls={["maximize"]} {...props} /> + ) +} + +/** A Figma multi step frame with its own program state (steps, logo, tools and window buttons respond). */ +function LiveMultiHeader({ step, documentTitle = "" }: { step: string; documentTitle?: string }) { + const demo = useProcessHeaderDemo(step) + return ( + } + onWindowMoveStart={demo.onWindowMoveStart} onMinimize={demo.onMinimize} onClose={demo.onClose} /> + ) +} + +/** Split tool with a live layout (the selector opens on load; choosing a layout updates the icon and the checked cell). */ +function useSplitDemo(initial: SWProcessSplitType): SplitState { + const [value, setValue] = useState(initial) + return { value, onValueChange: setValue, defaultOpen: true } +} + +/** Row of a page: as wide as the Story column, at most `max` (like the SW/Header Logo and Responsive rows). */ +function Row({ max = 1920, children }: { max?: number; children: ReactNode }) { + return
{children}
+} + +/** A fixed program width in a horizontal scroller (2560 / 1920 are wider than most pages). Verification only. */ +function Fixed({ width, children }: { width: number; children: ReactNode }) { + return
{children}
+} + +// ---------- Pages ---------- + +// Figma StrAna-SW multi step frames, in this order (the node ids stay as data only, never shown): +// 873:16481 no project yet, 873:16254 project open, 873:17421 a later step. +const FIGMA_FRAMES: { node: string; step: string; documentTitle?: string }[] = [ + { node: "873:16481", step: "data-input" }, + { node: "873:16254", step: "data-input", documentTitle: "Example project" }, + { node: "873:17421", step: "body-plan" }, +] + +// One page (2026-11-03): the Default header, then the three Multi Step frames; each header keeps its own state. +export const ProcessHeader: Story = { + name: "Step Header", + render: () => ( + + + + + + +
+ {FIGMA_FRAMES.map(frame => ( +
+ ))} +
+
+
+ ), +} + +const WIDTHS = [1920, 1366, 1100] + +export const Responsive: Story = { + name: "Responsive", + render: () => ( + + +
+ {WIDTHS.map(width => ( + // The 1920 row shows only on a page with 1920px of room, like the SW Header / Viewer Responsive pages. +
+ + +
+ + +
+
+
+
+ ))} +
+
+ ), +} + +/** Split selector open in both appearances (Default leftTall, Multi rows), over the drawing area. */ +function SplitOpenPage() { + const defaultSplit = useSplitDemo("leftTall") + const multiSplit = useSplitDemo("rows") + return ( + + +
+ +
} />
+ + +
} />
+ +
+ + ) +} + +// Hidden from the sidebar (2026-11-03): the split tool works in the Step Header page; this open-state check stays. +export const SplitOpen: Story = { + name: "화면 분할", + tags: ["verification"], + render: () => , +} + +// ---------- Verification (hidden from the sidebar; direct URLs kept for checks) ---------- + +/** The three Multi Step frames at the Figma 1920 width, marked with their node ids for comparisons. */ +export const MultiStep: Story = { + name: "Multi Step", + tags: ["verification"], + render: () => ( + + +
+ {FIGMA_FRAMES.map(frame => ( +
+ ))} +
+
+ ), +} + +export const FixedWidths: Story = { + name: "Fixed Widths", + tags: ["verification"], + render: () => ( + + +
+ {[1100, 1366, 1920, 2560].map(width => ( + +
+ + +
+
+ ))} +
+
+ ), +} + +const DISABLED_LOADS: SWProcessGroup[] = [ + { ...PROCESS_GROUPS[0], steps: [PROCESS_GROUPS[0].steps[0], { ...PROCESS_GROUPS[0].steps[1], disabled: true }] }, + PROCESS_GROUPS[1], +] + +export const ProcessBar: Story = { + name: "Process Bar", + tags: ["verification"], + render: () => ( + + + +
+
+
+
+
+
+
+
+
+
+
+ ), +} + +export const MultiStepManyGroups: Story = { + name: "Multi Step · Many Groups", + tags: ["verification"], + render: () => ( + + +
+ + +
+
+ ), +} + +export const MultiStepManySteps: Story = { + name: "Multi Step · Many Steps", + tags: ["verification"], + render: () => ( + + +
+ + +
+
+ ), +} + +// Three groups: renders, design not confirmed (trapezoids only on the first and last group). Verification only. +const THREE_GROUPS: SWProcessGroup[] = [ + PROCESS_GROUPS[0], + { id: "design", number: "02", label: "Design", steps: [{ id: "member", label: "Member" }, { id: "joint", label: "Joint", disabled: true }] }, + { ...PROCESS_GROUPS[1], number: "03" }, +] + +const STATES: [string, Partial][] = [ + ["default", {}], + ["results", { process: { groups: PROCESS_GROUPS, activeStepId: "results" } }], + ["layer-open", { layer: { label: "Layer", value: "Layer 0", open: true, visible: false }, tools: }], + ["home-open", { homeMenuOpen: true, maximized: false }], + ["disabled", { disabled: true }], + ["hidden-controls", { hiddenControls: ["tools", "windowMove", "maximize"] }], + ["long-names", { documentTitle: "A very long project file name for the ellipsis check.strn", layer: { label: "Layer", value: "Foundation_Pile_Cap_Layer_Long_Name" } }], + ["three-groups", { process: { groups: THREE_GROUPS, activeStepId: "member" } }], +] + +export const States: Story = { + name: "States", + tags: ["verification"], + render: () => ( + + +
+ {STATES.map(([state, props]) => ( +
+ ))} +
+
+ ), +} + +export const ThemeOverride: Story = { + name: "Theme Override", + tags: ["verification"], + render: () => ( + + + + + ), +}