diff --git a/docs/sw/figma-additional/ai-home-menu-guide.md b/docs/sw/figma-additional/ai-home-menu-guide.md new file mode 100644 index 0000000..35a8313 --- /dev/null +++ b/docs/sw/figma-additional/ai-home-menu-guide.md @@ -0,0 +1,73 @@ +# AI 가이드 초안 — SW 로고 클릭 메뉴 (`SWHomeMenu`) + +> 상태: **통합 전 초안**입니다(Task B 2단계, 2026-10-19). 통합 승인 후 `packages/sw/ai/COMPONENT_CATALOG.md`·`COMPOSITION_PATTERNS.md`·`DESIGN_RULES.md`로 옮깁니다. 지금은 `@hm-design/sw` 진입점에서 export되지 않으므로 소스 경로 `packages/sw/src/components/sw-home-menu`를 사용합니다. +> 기준: Figma `rj6wpsfk24lOWvWiVRAANP` 26:10591(메뉴), 26:11355…26:11631(타일 15개), 26:12604…(섹션 제목), 662:67267·662:67331·662:67471(최근파일 하위 패널). 구현: `SWHomeMenu.tsx`, `SWRecentFiles.tsx`, `theme/sw-home-menu.css`. + +## 1. 무엇인가 + +- SW Header의 **로고(홈) 버튼을 누르면 Header 위에 겹쳐 나타나는 2단 메뉴**입니다. 기본은 숨김이고, 로고를 다시 누르면 닫힙니다. +- 구성: 섹션(제목 badge + 타일 그룹) 여러 개 → 섹션마다 80 × 46 타일 여러 개. 타일 하나가 하위 패널(예: 최근파일 목록)을 열 수 있습니다. +- 운영체제 파일 선택창이 아닙니다. "찾아열기" 같은 항목의 실제 동작(파일 대화상자 등)은 host가 `onSelect`에서 수행합니다. + +## 2. 최소 사용법 + +```tsx +import { SWHeader, SWTheme } from "@hm-design/sw" +import { SWHomeMenu, type SWHomeMenuSectionData } from "@hm-design/sw" // 통합 후 경로 + +const sections: SWHomeMenuSectionData[] = [ + { id: "file", title: "작업중인 파일", items: [ + { id: "save", label: "저장", icon: , toggle: false, onSelect: () => host.save() }, + { id: "export", label: "내보내기", icon: }, + ] }, +] + +function App() { + const [open, setOpen] = useState(false) + return ( +
+ setOpen(v => !v)} /* 기존 props */ /> + {/* 통합 전: host가 위치를 지정합니다(Header 객체 수정 영역 왼쪽 - 2px, 창 영역 왼쪽까지). */} +
+ +
+
+ ) +} +``` + +## 3. 데이터 규칙 (새 메뉴를 만들 때) + +| 하고 싶은 것 | 방법 | +|---|---| +| 항목 추가 | 섹션 `items`에 `{ id, label, icon }`을 추가합니다. 폭·구분선·모서리·섹션 폭은 자동입니다 | +| 섹션 추가 | `{ id, title, items }`를 추가합니다. 제목 badge 폭은 글자 + 46px로 자동입니다 | +| 숨김 | `hidden: true`. 자리를 남기지 않습니다 | +| 비활성 | `disabled: true`. 기존 SW 규칙대로 opacity .4이며 Hover가 없습니다 | +| 실행만 하는 버튼 | `toggle: false`. Active(Variant3)가 되지 않습니다 | +| 켜고 끄는 버튼 | 기본값(`toggle` 생략)입니다. 클릭하면 Active, 다시 클릭하면 Default입니다 | +| Active 제어 | `activeIds` + `onActiveChange`(controlled) 또는 `defaultActiveIds`. 하나만 켜려면 `selection="single"` | +| 하위 패널 | `panel: `. 패널은 한 번에 하나만 열리고 열린 동안 하단 강조가 전체 외곽선이 됩니다 | +| 아이콘 | `currentColor` SVG 하나를 권장합니다(Default `#B6B4AF`, Hover·Active `#FF5C00` 자동). 색이 고정된 이미지 쌍이면 `hoverIcon`을 함께 줍니다 | +| 이벤트 | 항목 `onSelect(event)` → 메뉴 `onItemSelect(event)`. `event.active`는 클릭 후 상태입니다 | + +## 4. 상태와 닫기 (Figma 기준 + 접근성 보완) + +| 동작 | 결과 | 근거 | +|---|---|---| +| 로고 클릭 | 열기 / 다시 클릭하면 닫기 | Figma btn_home ON_CLICK → OVERLAY, 재클릭 닫기는 사용자 확정 | +| 타일 Hover | Variant2: 주황 gradient 1px 테두리, 아이콘·글자 `#FF5C00`, 1px 위·왼쪽 이동 | Figma Default ON_HOVER → V2 | +| 타일 클릭 | Variant3(Active, V2와 같은 모양) → 다시 클릭하면 Default | Figma V2 → V3 → Default | +| 메뉴 빈 곳 클릭 | 닫기 | Figma Group ON_CLICK → CLOSE (`closeOnBackgroundClick`) | +| Escape | 열린 패널을 먼저 닫고, 다시 누르면 메뉴를 닫은 뒤 로고로 Focus 복귀 | Figma 없음 · 접근성 (`closeOnEscape`) | +| 메뉴 밖 클릭 | 기본은 닫지 않음 | Figma overlay에 바깥 동작 없음 (`closeOnOutsideClick`) | +| ←/→/Home/End | 타일 사이 이동. 열린 패널 타일에서 ↓를 누르면 패널로 이동, ↑/↓로 행 이동, Enter로 파일 열기 | Figma 없음 · 접근성 | +| 최근파일 행 선택 | `onFileOpen(file)` 후 메뉴 닫기(`closeOnOpen`) | Figma 없음 · 검토 대상 | + +## 5. 하지 말 것 + +- 타일·섹션·제목을 Figma 화면별로 따로 만들지 않습니다. 모든 메뉴는 `sections` 데이터 하나로 표현합니다. +- `hm-sw-home-menu*` class를 소비 코드에서 붙여 외형을 만들지 않습니다(`className`은 확장용입니다). +- 새 색·크기를 임의로 정하지 않습니다. 필요한 값은 `--hm-sw-home-menu-*`·`--hm-sw-recent-files-*` 토큰을 `style`로 덮어씁니다. +- Font Weight는 400/500/700만 씁니다(타일 라벨·파일명 700, 섹션 제목 400). +- 홈 버튼이나 창 영역을 메뉴 안에 다시 그리지 않습니다. 그것은 `SWHeader`가 담당합니다. diff --git a/docs/sw/figma-additional/analysis-report.md b/docs/sw/figma-additional/analysis-report.md new file mode 100644 index 0000000..ef3d28f --- /dev/null +++ b/docs/sw/figma-additional/analysis-report.md @@ -0,0 +1,259 @@ +# 작업 B 1단계 — Figma 추가 디자인 분석·설계 보고 (2026-10-19) + +> **2단계 이후 정정(우선 적용):** [2단계 보고](./stage2-report.md)를 기준으로 봅니다. 아래 1단계 본문은 당시 기록으로 남겨 둡니다. +> - 로고 클릭 = 2단 Header 메뉴, 기본 숨김, 재클릭 닫힘(사용자 최종 정정). +> - 662:67267은 Figma 주석 "최근파일" 상태 화면입니다. 최근파일 목록 662:67331과 2뎁스 미리보기 662:67471이 함께 붙으며, 두 노드는 `SWRecentFiles`로 구현했습니다. +> - V3 = 클릭으로 켜지고 다시 클릭하면 꺼지는 Active 상태입니다(사용자 지시). +> - 아이콘 prop은 `iconHover`가 아니라 `hoverIcon`입니다(Task A `SWToolButton`과 이름을 맞춤). +> - `accent`의 기본값은 `"auto"`입니다(패널이 열린 동안 outline). + +> 상태: **분석·설계 보고만 작성했습니다(코드 수정 없음)**. 구현, Commit, Push는 하지 않았고 2단계는 사용자 승인 후 시작합니다. +> 대상: Figma `rj6wpsfk24lOWvWiVRAANP`, 속성Bar `26:10492`와 로고 클릭 노드 21개. 기준 브랜치는 `claude/sw-figma-logo-menu-36832b`(HEAD `5faa1f1`, `feature/sw-package`와 동일)입니다. +> 수집 자료: [figma-nodes.json](./figma-nodes.json)(노드별 메타데이터·디자인 컨텍스트 정규화·Prototype 연결), [screenshots/](./screenshots/)(노드 22개 PNG, 1x). + +## 0. 수집 방법과 한계 + +| 항목 | 내용 | 상태 | +|---|---|---| +| Figma 접근 | 공식 Figma MCP(`get_metadata`, `get_design_context`, `get_variable_defs`, `get_screenshot`)를 사용했습니다. `figma-design-to-code` 스킬을 먼저 불러왔습니다. 인증이 필요한 `plugin:figma:figma` 서버는 사용하지 않았습니다. | 완료 | +| 세부 속성 | 읽기 전용 `use_figma`(Plugin API)로 reactions, gradient stop, stroke, effect, font, 변형 간 diff를 확인했습니다. 캔버스는 수정하지 않았습니다. | 완료 | +| 디자인 컨텍스트 저장 | React/Tailwind 원문 대신 구현에 필요한 값만 `figma-nodes.json`에 정규화했습니다. 원문은 같은 node id로 다시 받을 수 있습니다. | 정규화본만 저장 | +| Asset | `get_design_context`의 asset URL은 2026-10-26에 만료됩니다. 이번 단계에서는 asset을 받지 않았고, 2단계에서 다시 받습니다. | 미수집 | +| Storybook 비교 | `localhost:6024`에서 이미 실행 중인 Storybook의 `SW/Header/Overview` 속성Bar를 측정했습니다. 그 서버가 어느 checkout에서 실행되는지는 확인하지 못했습니다. Pane 폭은 390px, DPR은 1.5였습니다. | 실측 1회, 출처 미확인 | +| 형제 노트 | 공유 노트 폴더에 다른 세션의 노트가 없었습니다. 작업 A의 계획은 사용자 메모리의 확정 기록 기준으로 대조했습니다. | 확인 | + +## 1. 노드별 표 (22개) + +"로고 클릭 화면 21개"는 실제로 **화면 상태 2개**와 **구성 컴포넌트 19개**(섹션 제목 4개, 버튼 세트 15개 × 상태 3개)입니다. 다섯 번째 섹션 제목 `tit_마이세팅`(26:12628, "My Setting")은 요청 목록에 없지만 화면에서 사용되므로 참고로 함께 정리했습니다. + +| # | Node | 이름 / 유형 | 크기 | 역할 | 상태·Prototype | +|---|---|---|---|---|---| +| 1 | 26:10492 | 속성Bar / FRAME (`header_green` → `선택한것` 26:10491 하위) | 1222×28 | 객체 속성 편집 strip | input ON_HOVER → hover(26:11659) | +| 2 | 26:10591 | 헤더/꾸러미 ver2/home on / COMPONENT | 1920×77 | **로고 클릭 overlay 본체** | btn_home ON_CLICK → OVERLAY(TOP_LEFT, 배경 없음), 메뉴 그룹 ON_CLICK → CLOSE | +| 3 | 662:67267 | 같은 이름 / FRAME | 1920×77(렌더 1935×193) | 2의 정적 사본. 팝업가이드 Variant5(K 미리보기)와 다른 하단 강조 | hover만 있음(open/close 없음) | +| 4 | 26:12604 | tit_작업중인파일 | 124×19 | 섹션 제목 "작업중인 파일" | 없음 | +| 5 | 26:12610 | tit_다른파일열기 | 123×19 | "다른파일 열기" | 없음 | +| 6 | 26:12622 | tit_파일손상시 | 111×19 | "파일 손상시" | 없음 | +| 7 | 26:12616 | tit_궁금하면 | 95×19 | "궁금하면" | 없음 | +| 8 | 26:11355 | btn_저장 / COMPONENT_SET | 3×(80×46) | 저장 · save · 그룹 첫 버튼 | D→hover V2, V2 click→V3, V3 click→D | +| 9 | 26:11371 | 복사저장 | 〃 | save_as · 중간 | 〃 | +| 10 | 26:11387 | 내보내기 | 〃 | file_open · 중간 | 〃 | +| 11 | 26:11403 | 도면묶기 | 〃 | inventory_2 · 마지막(구분선 없음) | 〃 | +| 12 | 26:11416 | 찾아열기 | 〃 | folder_open · 첫 버튼. 아이콘 top 5 | 〃. 구분선 gradient가 다름 | +| 13 | 26:11450 | 최근파일 | 〃 | 시계 붙은 파일 아이콘 · 중간 | **V2→V3 클릭 reaction 없음**. 구분선 gradient가 다름 | +| 14 | 26:11485 | 새파일 | 〃 | insert_drive_file · 마지막 | 〃 | +| 15 | 26:11507 | 복구하기 | 〃 | rotate_left · 첫 버튼 | 〃 | +| 16 | 26:11529 | 임시파일 | 〃 | file_copy · 마지막 | 〃 | +| 17 | 26:11542 | 디스플레이 | 〃 | monitor 24×22 · 첫 버튼. 라벨 "Display..."(원문에서 이미 말줄임) | 〃 | +| 18 | 26:11558 | 저장하기 | 〃 | 설정 저장 · 중간. gap 2 | 〃 | +| 19 | 26:11586 | 불러오기 | 〃 | save_alt · 마지막 | 〃 | +| 20 | 26:11599 | 설명보기 | 〃 | content_paste · 첫 버튼 | 〃 | +| 21 | 26:11615 | 문의하기 | 〃 | question_answer · 중간 | 〃 | +| 22 | 26:11631 | 소프트웨어 정보 | 〃 | 라벨 "S/W정보" · 마지막 | **V3가 button이 아니고 Default로 돌아가는 reaction 없음** | + +## 2. 속성Bar 차이표 (Figma 26:10492 ↔ 현재 구현·Storybook) + +비교 기준은 `packages/sw/src/theme/sw.css`(`.hm-sw-property-bar*`, `.hm-sw-header__input`), `SWHeaderPropertyBar.tsx`, Storybook `SW/Header/Overview` 실측입니다. **실제 차이가 있는 항목만** 적었습니다. 수정 위치는 모두 기존 공유 파일인 `sw.css`이므로 **통합 단계 항목**입니다. + +| # | 항목 | Figma | 현재 구현 / Storybook | 영향 | 수정 위치 | +|---|---|---|---|---|---| +| P1 | 테두리 색 | 좌·우·하 1px **세로 gradient**(`--gra-border-green`): `#08251C` 0% → `#208769` 37.1% → `#08241C` 80.5% → `#051612` 100%(위→아래) | 단색 `1px solid #08251C`. Storybook computed도 `rgb(8,37,28)` 단색 | 중간 높이의 초록 하이라이트(`#208769`)가 없음 | **통합 단계** `sw.css` `.hm-sw-property-bar`(gradient border는 기존 hover border 기법 재사용 검토) | +| P2 | 객체명 글자 외곽선 | 검정 1.3px Stroke + `#FFC600` 0.3px. 기존 정책상 검정 외곽선만 적용 | `text-shadow: none`. 외곽선 목록(`sw.css` 약 1472–1477행)에 `.hm-sw-property-bar__object`가 없음 | 노란 글자 가장자리가 Figma보다 흐림 | **통합 단계** `sw.css`, `--hm-sw-header-text-outline` 적용 | +| P3 | 라벨 글자 외곽선 | 14px 라벨에 검정 1px Stroke | `text-shadow: none` | 라벨 대비가 약함 | **통합 단계** `sw.css`, 1px 외곽선(`--hm-sw-header-label-outline`) 적용 검토 | +| P4 | 라벨 박스 폭 | 고정 텍스트 박스(반지름 46, 지름·원주·면적 30), 가운데 정렬. 박스 안 좌 1.4–2.1px, 우 1.8–2.8px 여백 | 자연 폭(예: "원주" 26.9px). 라벨→입력 간격이 Figma보다 약 2–3px 좁음. 콘텐츠 폭은 527.6px로 Figma 538px보다 짧음 | 글꼴 대체(Dotum → Noto Sans KR) 영향과 섞여 있음 | **검토 대상**(§5 R-P1). 고친다면 통합 단계 `sw.css` | +| P5 | 세로 위치 | 내용 Frame top 4px(20px 행) | 실측 top 3.5px(bar 875.2 → field 878.7, 27px 영역 가운데 정렬) | 0.5px | **검토 대상**(§5 R-P2) | + +**의도된 차이(수정하지 않음):** 바 폭은 Figma 1222px 고정 대신 콘텐츠 폭입니다(2026-10-19 사용자 확정). 글꼴은 Dotum Regular + Stroke 대신 Noto Sans KR이며 객체명·값은 700입니다(기존 Typography 정책). 입력 폭은 Figma 고정 58/66px 대신 값 길이를 따릅니다. 필드 순서와 객체명 문구는 Story fixture 데이터입니다. +**일치(차이 없음):** 높이 28, 배경 `#271F13`, 아래 radius 6, inset shadow `0 4 4 rgba(0,0,0,.6)`, 객체명 90px·`#FFC600`, 필드 간격 16·라벨-입력 6, 라벨 `#F6F4F2`·자간 4%, 입력 `#E60403`·자간 2%·padding 2/4/2/6·radius 2. 입력 상태 Default(면 없음) / hover(24px, 1px .25) / on(`#0F0D0A`, 1px .3)과 hover reaction도 같습니다. 아이콘은 Figma와 구현 모두 없습니다. +**미검증:** 1920px 폭 실측, Hover·편집 상태 Storybook 실측, P1–P3를 고친 뒤의 렌더 비교. + +## 3. 로고 클릭 화면 분석 + +### 3.1 화면 구성 (26:10591 기준, 1920 디자인) + +``` +y 0 ─ border-top 2px #5F4B0F ─────────────────────────────────────────────────── + [btn_home 80] (기존 Header가 비쳐 보임: 문서 제목 등) [섹션 5개, x377–1678] [창 영역 240: 인쇄·K pill | 창이동 | – | ×] +y 46 ─ border-bottom 2px #5F4B0F ─────────────── +y 47 ─ 하단 강조 strip 30px(섹션 뒤, 아래로 보이는 주황 gradient) +y 77 ─ 메뉴 끝(Header 48px보다 29px 아래로 내려옴) +``` + +- **Overlay**: 로고 클릭 시 `home on` 전체가 기존 Header 위에 TOP_LEFT로 겹칩니다(배경 없음). `bg black`과 `document_tit`가 숨겨져 있어 그 부분은 기존 Header가 그대로 보입니다. 즉 **새로 그려지는 것은 섹션 메뉴(x375–1680)이고**, 홈·창 영역은 기존 Header와 같은 컴포넌트 사본입니다. +- **섹션**(5개): 제목 badge(19px, Union 도형) + 버튼 그룹 box(46px) + 섹션 면(75px). 섹션 폭 = 버튼 수 × 80 + 좌우 padding(10, 첫·마지막 섹션은 한쪽 11)입니다. + - 작업중인 파일: 저장 · 복사저장 · 내보내기 · 도면묶기 + - 다른파일 열기: 찾아열기 · 최근파일 · 새파일 + - 파일 손상시: 복구하기 · 임시파일 + - My Setting: Display... · 저장하기 · 불러오기 + - 궁금하면: 설명보기 · 문의하기 · S/W정보 +- **버튼**: 80×46, 아이콘 18px(31,6), 라벨 14px(top 25). 마지막 버튼을 뺀 버튼 오른쪽에 1×44 gradient 구분선이 있습니다. + +### 3.2 공통 영역과 화면별 차이 + +| 구분 | 내용 | +|---|---| +| 공통(두 화면 동일) | 섹션 5개·버튼 15개·제목·구분선·면 gradient, 홈 버튼, 창 영역(인쇄·K·창이동·최소·종료), border-top/bottom `#5F4B0F` | +| 26:10591만 | 하단 강조 = 섹션별 사각형 5개(`#FEC501` + `#5F4B0F → #FF5100` gradient), btn_home 클릭 → open, 메뉴 클릭 → close | +| 662:67267만 | 하단 강조 = 메뉴 전체 1305×30에 2px gradient stroke(`#FF5100 → #5F4B0F`, fill 숨김), 팝업가이드 Variant5(K 미리보기 209×186, "다중인쇄/도면관리") 표시, open/close reaction 없음, btn_home 배경 asset이 다름 | +| 버튼 세트 공통 | Default / Variant2 / Variant3. **V2와 V3는 시각 속성이 같습니다**(Plugin API로 하위 노드 전체를 비교했고 위치만 다름) | +| 버튼별 차이 | 아이콘 asset과 위치(찾아열기 top 5, Display 24×22), 라벨 박스 폭(33–70), 그룹 내 위치에 따른 hover radius(첫 3 0 0 3 / 중간 0 / 마지막 0 3 3 0), 구분선 유무(마지막 없음), 찾아열기·최근파일만 구분선 gradient가 다름, 저장하기·S/W정보만 flex gap 2 | + +### 3.3 상태 + +| 상태 | Figma 근거 | 시각 | +|---|---|---| +| Default | 각 세트 Default | 면 없음, 아이콘 회색, 라벨 `#F6F4F2` + 검정 외곽선 | +| Hover | Default ON_HOVER → V2 | 1px INSIDE 주황 세로 gradient 테두리(`#DA6813` 8.5% / `#FFBB89` 37.9% / `#E5690D` 71.9% / `#AE3607` 92.8%), 아이콘·라벨 `#FF5C00`, 아이콘·라벨 1px 위·왼쪽 이동 | +| Pressed / 선택(?) | V2 ON_CLICK → V3, V3 ON_CLICK → Default | V2와 동일. 의미는 미정(§5 R-3) | +| Open / Close | btn_home ON_CLICK → OVERLAY, 메뉴 영역 ON_CLICK → CLOSE | — | +| Focus / Disabled / 키보드 | **Figma에 없음** | §5 검토 대상 | + +### 3.4 전환 관계 (Prototype) + +``` +[기존 Header] --btn_home click--> [home on overlay 26:10591] +[home on] --메뉴 영역(Group 2882) click--> CLOSE → [기존 Header] +버튼: Default --hover--> V2 --click--> V3 --click--> Default (최근파일: V2에서 끝, S/W정보: V3에서 끝) +팝업가이드: Default --hover--> Variant5(K 미리보기) ※ 기존 SWHeaderKGuide와 같은 대상 +``` + +Figma에는 메뉴 밖 클릭, Escape, 로고 재클릭으로 닫기, 각 버튼의 실제 동작(파일 대화상자, 최근 파일 목록 등)이 정의되어 있지 않습니다(§5). + +## 4. 공통 컴포넌트 구조·공개 API 초안 + +### 4.1 재사용 우선순위 판단 + +| 단계 | 후보 | 판단 | +|---|---|---| +| 기존 SW | `SWHeaderHome`(`onHomeClick`), `SWHeaderKGuide`, 창 제어, `useSWHeaderDropdown`의 닫기 계약(Escape·바깥 클릭·Tab 이탈·Focus 복귀), 텍스트 외곽선·focus 토큰, `SWTheme` | **재사용.** Header 쪽은 그대로 두고 메뉴만 새로 만듭니다. 홈·창 영역을 다시 그리지 않습니다 | +| 작업 A 계획 | `SWToolButton`(pill/text/document), `SWToolGroup`(selection), `SWPopup*` | §4.4 참고. 그대로 쓸 수 있는 appearance가 없습니다 | +| shadcn / Radix | Popover, NavigationMenu | `@hm-design/sw`는 `@hm-design/ui`·Radix에 의존하지 않습니다(독립 패키지). 새 의존성은 **별도 승인 대상**이므로 기존 SW hook 방식을 따릅니다 | +| 최소 Custom | 섹션 badge, 그룹 box, 80×46 타일 버튼 | 시각 요소만 새로 만듭니다(Figma 근거 있음) | + +### 4.2 구조 + +``` + overlay 패널(region/dialog 역할은 §5 R-6), open 상태·닫기 계약 + 섹션 면 + 제목 badge(폭은 글자에 맞춤) + 그룹 box + 80×46 타일 button. 마지막 항목은 구분선 자동 생략, 첫/마지막 hover radius 자동 +``` + +데이터형(`sections`)과 조합형(`children`) 중 하나만 써도 21개를 모두 표현할 수 있습니다. 위치·첫/마지막 여부·구분선·섹션 폭은 **항목 수에서 자동 계산**합니다. 따라서 AI가 항목만 추가해도 같은 스타일이 적용됩니다. + +### 4.3 공개 API 초안 + +```ts +export interface SWHomeMenuItemData { + id: string + label: string // 한 줄, 넘치면 말줄임 + title (기존 규칙) + icon?: ReactNode // currentColor SVG 권장: Default 회색 → Hover #FF5C00을 CSS가 처리 + iconHover?: ReactNode // 색 고정 이미지 쌍일 때만(기존 close/close-hover 방식) + disabled?: boolean // 시각은 검토 대상(R-4) + hidden?: boolean // 미렌더, 자리 없음(작업 A 숨김 정책과 맞춤) + title?: string // tooltip/전체 이름 + "aria-label"?: string + closeOnSelect?: boolean // 기본 true (Figma: 메뉴 클릭 → CLOSE) + onSelect?: (item: SWHomeMenuItemData) => void +} +export interface SWHomeMenuSectionData { + id: string + title: string + items: readonly SWHomeMenuItemData[] + hidden?: boolean +} +export interface SWHomeMenuProps extends Omit, "onSelect"> { + sections?: readonly SWHomeMenuSectionData[] // 또는 children() + open?: boolean // controlled + defaultOpen?: boolean + onOpenChange?: (open: boolean) => void + onItemSelect?: (item: SWHomeMenuItemData, section: SWHomeMenuSectionData) => void + /** 닫힐 때 Focus를 돌려줄 대상(기본: 연결된 홈 버튼). 통합 단계에서 SWHeader가 자동 연결 */ + returnFocusRef?: RefObject +} +export function SWHomeMenu(props: SWHomeMenuProps): JSX.Element +export function SWHomeMenuSection(props: { title: string; children: ReactNode; className?: string }): JSX.Element +export function SWHomeMenuItem(props: ButtonHTMLAttributes & { label: string; icon?: ReactNode; iconHover?: ReactNode }): JSX.Element +``` + +- **텍스트·아이콘·메뉴·이벤트·상태는 모두 외부에서 지정합니다.** 패키지에는 CAD 동작이 없습니다. Figma의 15개 메뉴는 Storybook fixture(또는 선택형 기본 아이콘 세트, §6 A-2)로만 제공합니다. +- **독립 단계 연결 방식(SWHeader 무수정):** ` setOpen(v => !v)} />`와 ``를 같은 상대 위치 컨테이너 안에 둡니다. 위치는 host가 `style`로 지정합니다. +- **통합 단계 연결 방식(안):** `SWHeader`에 `homeMenu?: SWHomeMenuProps | ReactNode`를 추가합니다. 홈 버튼에 `aria-expanded`/`aria-controls`를 붙이고, 객체 영역 시작 x에 앵커하며, 열림 중 홈 버튼 상태를 표시합니다. 작업 A의 `leading` 슬롯·파일 열기 배치와 같은 파일을 수정하므로 통합 단계에서만 합니다. +- **CSS:** 새 파일 `packages/sw/src/theme/sw-home-menu.css`에 `.hm-sw-home-menu*`만 둡니다. 새 토큰은 `--hm-sw-home-menu-*`이고, 기존 `--hm-sw-header-text-outline`·`--hm-sw-header-label-outline`·`--hm-sw-header-focus-color`·font 토큰은 그대로 읽습니다. 전역 선택자와 Dark 분기는 두지 않습니다. 단독 사용 시 `` 안에서 렌더합니다. +- **Font Weight:** 버튼 라벨 700(기존 "값·제목·토글 700"과 Figma 'Dotum/bold' 명칭), 제목 badge 12px 400(Stroke 없는 검정 글자)을 제안합니다. 600은 쓰지 않습니다. 최종값은 §5 R-5에서 확인합니다. + +### 4.4 작업 A와 겹치는 부분 + +| 작업 A 항목 | 겹침 | 제안 | +|---|---|---| +| `SWToolButton` appearance pill/text/document | 80×46 세로 아이콘+라벨 타일은 승인된 appearance에 없습니다 | B는 `SWHomeMenuItem`을 독립으로 만듭니다. 통합 단계에서 `SWToolButton appearance="tile"`로 합칠지 정합니다(**별도 승인 대상** A-1) | +| `SWToolGroup` selection="single" | 메뉴에는 선택 상태가 없습니다(V3 의미 미정) | 사용하지 않습니다. 키보드 이동 규칙만 맞춥니다 | +| `SWPopup`/`Section`/`Row` | 밝은 속성창 Popup과 시각·역할이 다릅니다 | 재사용하지 않습니다. 이름이 비슷한 `Section`은 namespace(`SWHomeMenuSection`)로 구분합니다 | +| `hiddenControls`·`show*` | 항목 숨김 개념이 같습니다 | 항목·섹션 `hidden`(미렌더, 자리 없음)으로 A와 같은 의미를 씁니다 | +| `documentOpenPlacement`, 문서 New/Open | "찾아열기·새파일"이 `onDocumentOpen`/`onDocumentNew`와 중복될 수 있습니다 | **검토 대상**(R-7). 컴포넌트는 콜백만 받습니다 | +| `leading` 슬롯·파일 열기 배치·반응형 | 메뉴 시작 x(Figma 375 = 문서 제목 끝)가 바뀝니다 | 통합 단계에서 실제 DOM 위치로 앵커합니다(x 하드코딩 금지) | +| 속성Bar(`.hm-sw-property-bar`) | 같은 49px 아래 overlay 영역을 씁니다(메뉴는 77px까지) | z-index·동시 표시는 R-8 | + +## 5. 검토 대상 (Figma에 없는 동작 — 확정하지 않음) + +| ID | 항목 | 제안(미확정) | +|---|---|---| +| R-1 | 메뉴 닫기: 바깥 클릭·Escape·로고 재클릭·Tab 이탈 | 기존 SW Dropdown 계약과 같게 처리하고 Focus는 홈 버튼으로 복귀 | +| R-2 | 항목 클릭 후 닫힘 | Figma CLOSE를 따라 기본 닫힘, `closeOnSelect`로 변경 | +| R-3 | V3(클릭 후)의 의미: 누름(pressed) 순간 표시인지, 유지되는 선택 상태인지. 최근파일·S/W정보의 reaction 누락 | 누름(`:active`) 표시로만 구현하고 유지 상태는 만들지 않음 | +| R-4 | Disabled·Focus-visible 시각 | 기존 SW 규칙(opacity .4, focus 2px `--hm-sw-header-focus-color`) | +| R-5 | 라벨·제목 Font Weight(700/400), Stroke 적용 범위 | §4.3 제안 | +| R-6 | 접근성 역할: `role="menu"` + 화살표 이동 vs region + button + Tab | button 목록 + 섹션 `aria-labelledby`, 화살표 이동은 작업 A 규칙과 맞춤 | +| R-7 | 찾아열기·새파일과 기존 `onDocumentOpen`/`onDocumentNew` 관계, 최근파일 하위 목록 여부 | 컴포넌트는 콜백만 받고, 연결은 host 또는 통합 단계에서 결정 | +| R-8 | 속성Bar·Dropdown·K 미리보기와의 z-index와 동시 표시 | 메뉴가 열리면 다른 Dropdown을 닫고 속성Bar보다 위에 표시 | +| R-9 | 1920 미만 반응형(메뉴 1305px) | 가용 폭 안 내부 가로 스크롤(속성Bar 정책과 동일). 섹션 접기·줄바꿈은 새 디자인이 필요해 A-4로 분리 | +| R-10 | 하단 강조 strip 두 가지(섹션별 vs 전체 2px) 중 기준 | 기본 디자인 컴포넌트인 26:10591(섹션별) | +| R-11 | home on의 border-top/bottom `#5F4B0F`와 홈 버튼 배경 asset이 기존 Header와 다른지 | 2단계에서 기존 `home-bg*.svg`·Header 선과 대조한 뒤 보고 | +| R-P1 | 속성Bar 라벨 고정 박스 여백(P4) | 글꼴 대체 영향이라 유지 권장 | +| R-P2 | 속성Bar 세로 0.5px(P5) | 유지 권장 | + +## 6. 별도 승인 대상 (새 디자인 판단 또는 승인 범위 초과) + +| ID | 항목 | 이유 | +|---|---|---| +| A-1 | `SWToolButton`에 `tile` appearance 추가 또는 `SWHomeMenuItem` 별도 유지 | 작업 A에서 확정된 공개 API 변경 | +| A-2 | Figma 메뉴 아이콘 15개(+hover 15개)를 패키지 기본 asset으로 제공할지, Story fixture로만 둘지 | 공개 asset·번들 크기 증가 | +| A-3 | `SWHeader`에 `homeMenu` prop·홈 버튼 열림 상태 추가 | 기존 Header API·스타일 변경(통합 단계) | +| A-4 | 좁은 폭에서 섹션 접기·More 이동 등 새 반응형 디자인 | Figma에 없는 디자인 | +| A-5 | Radix 등 새 dependency 사용 | 현재 `@hm-design/sw`에는 런타임 의존성이 없음 | +| A-6 | 속성Bar P1–P3 수정 | 기존 공유 CSS 변경이며, 사용자 확정 디자인 범위 안인지 확인 필요 | + +## 7. 파일 경계 + +| 구분 | 파일 | 내용 | +|---|---|---| +| **B 독립(신규만)** | `packages/sw/src/components/sw-home-menu/**` | `SWHomeMenu.tsx`, `SWHomeMenuSection.tsx`, `SWHomeMenuItem.tsx`, `types.ts`, `index.ts`, (A-2 승인 시) `assets/` | +| | `packages/sw/src/theme/sw-home-menu.css` | `.hm-sw-home-menu*` 스타일·토큰 | +| | `src/sw/sw-home-menu.stories.tsx` | Figma 21개 재현, 단독, 동적 데이터, Header 조합(host wiring) Story. Story CSS는 새 파일만 | +| | `docs/sw/figma-additional/**` | 이 보고서, 수집 자료, 2단계 검증 기록 | +| **통합 단계로 미룸** | `packages/sw/src/index.ts`, 테마 진입 CSS(`sw.css` import 체인) | export·CSS 연결(2단계 Story는 상대 경로 import로 확인) | +| | `packages/sw/src/components/sw-header/**`, `packages/sw/src/theme/sw.css` | `homeMenu` 연결(A-3), 속성Bar P1–P3(A-6) | +| | `build.mjs`, `package.json` | asset·CSS 번들 포함 | +| | `packages/sw/ai/**`, `packages/sw/README.md`, `docs/sw/sw-header-*.md` | AI 가이드·카탈로그·사용법. 2단계에서는 초안을 `docs/sw/figma-additional/`에 둠 | +| | 기존 stories(`src/sw/sw-header*.stories.tsx`, `sw-header-overview.tsx`, `sw-stories.css`) | 수정하지 않음 | + +## 8. 2단계 구현·검증 계획 (승인 후) + +**구현 순서** +1. Asset 재수집(만료 전) → A-2 결정에 따라 경로 확정 → 섹션 badge·그룹 box·구분선·타일 CSS +2. `SWHomeMenuItem` → `SWHomeMenuSection` → `SWHomeMenu`(open 계약, R-1·R-2 결정 반영) +3. Story `SW/Home Menu`: Figma 재현, 단독, 동적, Header 조합 +4. AI 가이드 초안(`docs/sw/figma-additional/ai-home-menu-draft.md`) → 통합 단계에서 `packages/sw/ai/`로 이동 + +**검증 범위**(실행하지 않은 항목은 미검증으로 기록) + +| 범위 | 방법 | 기준 | +|---|---|---| +| Figma 비교 | 1920 viewport, Figma 스크린샷과 같은 배율 비교 + 요소 실측 | 섹션 x·폭(340/260/180/260/261), 높이 75·46·19, 색·gradient·shadow | +| 21개 재현 | 제목 5개(참고 1개 포함), 버튼 15개 × Default/Hover/Pressed 상태 Story | 노드별 체크리스트 | +| 단독 사용 | `` 안 `SWHomeMenu`만 렌더 | Header 없이 동작·스타일 유지, 전역 CSS 누수 없음 | +| 동적 콘텐츠 | 항목 추가·삭제·숨김, 긴 라벨, 아이콘 없음, 섹션 1개·6개, 비동기 갱신 | 폭·구분선·radius 자동 계산, 말줄임 | +| Header/Popup 조합 | `onHomeClick` host wiring, 속성Bar·Dropdown·K 가이드 동시 상태 | 닫기 계약, Focus 복귀, z-index(R-8) | +| 반응형 | 1920/1440/1280/1080/900/768/640/390 | 가로 넘침 없이 내부 스크롤(R-9) | +| Storybook | 기존 SW Story 회귀 없음 | 기존 Story 무수정 확인 | +| AI 가이드 | 문서만으로 새 메뉴 항목·섹션 생성 시나리오 1건 | 임의 값 없이 같은 스타일 | +| 독립 React 19 소비 프로젝트 | 통합 단계 build 후 packed tarball 설치 | 타입·CSS·asset 로드. **dist·export 연결이 통합 단계라 2단계에서는 미검증 예정** | diff --git a/docs/sw/figma-additional/figma-nodes.json b/docs/sw/figma-additional/figma-nodes.json new file mode 100644 index 0000000..374037c --- /dev/null +++ b/docs/sw/figma-additional/figma-nodes.json @@ -0,0 +1,115 @@ +{ + "source": { + "fileKey": "rj6wpsfk24lOWvWiVRAANP", + "collectedAt": "2026-10-19", + "tools": "공식 Figma MCP get_metadata / get_design_context / get_variable_defs / get_screenshot + 읽기 전용 use_figma(Plugin API: reactions, fills, strokes, effects, fonts)", + "page": "2. 헤더 (SECTION) — 속성Bar만 header_green 26:10332의 'Property 1=선택한것' 26:10491 하위", + "note": "get_design_context의 React/Tailwind 원문은 화면 재현용 참고 코드라 그대로 저장하지 않고, 구현에 필요한 값만 아래에 정규화했습니다. 원문은 같은 node id로 다시 받을 수 있습니다. 응답에 들어 있던 asset URL은 2026-10-26에 만료되므로 2단계에서 다시 받아야 합니다. 스크린샷은 screenshots/.png (1x)입니다." + }, + "nodes": [ + { + "id": "26:10492", "name": "속성Bar", "type": "FRAME", "size": [1222, 28], + "screenshot": "screenshots/26-10492_property-bar.png", + "variables": { "--accent-yellow": "#FFC600", "--text-brown100": "#F6F4F2", "--text-red": "#E60403", "--gra-border-green": "(gradient, 값 없음)" }, + "metadata": "Rectangle 7541 1222x28 + Frame(top 4, h 20) = 객체명 Dotum_14_bold 90x16 + Frame(gap 16) x4 [라벨 Dotum_regular_14 (46|30)x16 + gap 6 + input 58|66x20]", + "designContext": { + "background": "#271F13", + "border": "좌·우·하 1px (상 0) GRADIENT_LINEAR 위→아래 #08251C 0% / #208769 37.1% / #08241C 80.5% / #051612 100%", + "radius": "0 0 6 6", + "effect": "INNER_SHADOW 0 4 4 rgba(0,0,0,.6)", + "object": "90px 가운데 정렬, Dotum Regular 14 / 4%, #FFC600, 아래층 Stroke 검정 1.3px + 위층 #FFC600 0.3px", + "label": "Dotum Regular 14 / 4%, #F6F4F2, Stroke 검정 1px, 고정 폭 텍스트 박스(46 또는 30) 가운데 정렬", + "fieldGap": 16, "labelInputGap": 6, + "input": "component set 'input'(26:11656) Default 26:11657 58x20 pad 2/4/2/6 r2 면·테두리 없음 / hover 26:11659 58x24 1px rgba(212,203,189,.25) / on 26:11661 58x24 #0F0D0A + 1px .3. 값 Dotum Regular 14 / 2%, 검정 1.3 + #E60403 0.3" + }, + "reactions": ["input 4개: ON_HOVER → CHANGE_TO input hover(26:11659)"] + }, + { + "id": "26:10591", "name": "헤더/꾸러미 ver2/home on", "type": "COMPONENT", "size": [1920, 77], + "screenshot": "screenshots/26-10591_home-on.png", + "role": "로고(btn_home) 클릭 시 열리는 overlay 자체(TOP_LEFT, 배경 없음, 바깥 클릭 상호작용 없음)", + "variables": { "--text-brown100": "#F6F4F2", "--gra-border-brown1": "(gradient)", "--gra-border-brown2": "(gradient)", "--accent-border-yellow": "(gradient)" }, + "hiddenLayers": ["bg balck 1920x48", "Rectangle 6671 310x48", "header/document_tit 310x48"], + "metadata": "창 영역 240x48(x1680) · 최소/종료 32x48 x2 · pill(인쇄 + 팝업가이드) 85x42 · 창이동 motion 88x48 · border-top/bottom 1920x2 #5F4B0F · btn_home 80x48 · Group 2882 x375 1305x77 = 하단 강조 strip(y47, 30px, 섹션별 사각형 5개) + 섹션 5개(x377 1301x75)", + "sections": [ + { "frame": "Frame 2408", "width": 340, "padding": "5 10 5 11", "title": "tit_작업중인파일 26:12604 124x19", "group": "g_작업중인파일 320x46", "items": ["btn_저장 26:11355", "복사저장 26:11371", "내보내기 26:11387", "도면묶기 26:11403"] }, + { "frame": "Frame 2409", "width": 260, "padding": "5 10", "title": "tit_다른파일열기 26:12610 123x19", "group": "g_다른파일열기 240x46", "items": ["찾아열기 26:11416", "최근파일 26:11450", "새파일 26:11485"] }, + { "frame": "Frame 2410", "width": 180, "padding": "5 10", "title": "tit_파일손상시 26:12622 111x19", "group": "g_파일손상시 160x46", "items": ["복구하기 26:11507", "임시파일 26:11529"] }, + { "frame": "Frame 2411", "width": 260, "padding": "5 10", "title": "tit_마이세팅 26:12628 107x19 (요청 목록 밖, 'My Setting')", "group": "Frame 2407 240x46", "items": ["디스플레이 26:11542", "저장하기 26:11558", "불러오기 26:11586"] }, + { "frame": "Frame 2412", "width": 261, "padding": "5 10 5 11", "title": "tit_궁금하면 26:12616 95x19", "group": "g_궁금하면 240x46", "items": ["설명보기 26:11599", "문의하기 26:11615", "소프트웨어 정보 26:11631"] } + ], + "designContext": { + "section": "h75, 1px #081813, radius 0 0 3 3, fill 2중 gradient(세로 #18150E 0 / #262015 21.3% / #332B1C 47% / #38301F 75% / #18150E 100% + 가장자리 어둡게 #16120A 0 / 투명 54.5% / #16120A 100%), 안쪽 1px #20190E 사각형", + "group": "h46, 1px GRADIENT stroke(#886D35 0 / #423625 8.8% / #F3DBA8 26.2% / #0E0B06 83.8% / #574B30 100%), radius 3, fill gradient(#19160E 2% / #38301F 43.7% / #463C29 67.7% / #2F281A 100%), DROP_SHADOW 0 2 radius 4 rgba(0,0,0,.6)", + "title": "Union 도형(위아래 반전) 19px, fill gradient #887757 → #A8997C 75.6% → #C3B79E, stroke 1px #7B6332 → #9F8D68, DROP_SHADOW 0 -2 radius 4 rgba(0,0,0,.4), 글자 Dotum Regular 12 / 2% #000 가운데, group 위에 -1px 겹침", + "bottomStrip": "y47 h30, 섹션별 사각형(343/263/181/262/264, -2px 겹침, radius 0 0 3 3) fill #FEC501 + GRADIENT #5F4B0F → #FF5100", + "headerLines": "border-top 0-2px, border-bottom 46-48px, #5F4B0F 1920px" + }, + "reactions": [ + "btn_home 26:10652 ON_CLICK → OVERLAY 26:10591", + "Group 2882(메뉴 전체) ON_CLICK → CLOSE", + "메뉴 버튼 15개 ON_HOVER → 각 Variant2", + "header/btn_최소최대화·btn_종료 ON_HOVER → hover", + "Component 743(인쇄) ON_HOVER → hover 26:224", + "꾸러미ver2/팝업가이드 ON_HOVER → Variant5 26:426 (209x186, SMART_ANIMATE)", + "창이동 motion ON_HOVER → 26:10258 SMART_ANIMATE", + "header/document_tit(숨김) MOUSE_ENTER → OVERLAY 26:10667 hover" + ] + }, + { + "id": "662:67267", "name": "헤더/꾸러미 ver2/home on", "type": "FRAME", "size": [1920, 77], "renderBounds": [1935, 193], + "screenshot": "screenshots/662-67267_home-on-popup-guide.png", + "role": "26:10591과 같은 구성의 독립 Frame. 팝업가이드가 Variant5(K 미리보기 209x186) 상태이고 하단 강조가 다름", + "diffFrom_26:10591": [ + "하단 strip: 섹션별 사각형 4개 숨김, Rectangle 6991만 1305x30, fill(#FF5100) 숨김 + 2px GRADIENT stroke #FF5100 → #5F4B0F", + "꾸러미ver2/팝업가이드 = Variant5 (검정 미리보기, '다중인쇄/도면관리')", + "btn_home 클릭·메뉴 CLOSE reaction 없음 (정적 상태 화면)", + "btn_home 배경 asset이 26:10591과 다른 파일(a80fd vs 9931a)" + ], + "reactions": ["창 버튼·인쇄·창이동·메뉴 버튼 15개 ON_HOVER만 (26:10591과 같은 대상의 다른 사본 id)"] + }, + { "id": "26:12604", "name": "tit_작업중인파일", "type": "COMPONENT", "size": [124, 19], "text": "작업중인 파일", "textBox": [78, 14], "screenshot": "screenshots/26-12604_tit-working-file.png" }, + { "id": "26:12610", "name": "tit_다른파일열기", "type": "COMPONENT", "size": [123, 19], "text": "다른파일 열기", "textBox": [78, 14], "screenshot": "screenshots/26-12610_tit-open-other.png" }, + { "id": "26:12622", "name": "tit_파일손상시", "type": "COMPONENT", "size": [111, 19], "text": "파일 손상시", "textBox": [66, 14], "screenshot": "screenshots/26-12622_tit-file-damaged.png" }, + { "id": "26:12616", "name": "tit_궁금하면", "type": "COMPONENT", "size": [95, 19], "text": "궁금하면", "textBox": [49, 14], "screenshot": "screenshots/26-12616_tit-curious.png" } + ], + "titleWidthRule": "제목 폭 = 글자 폭 + 약 46px(좌우 약 23px). 5개 모두 같은 Union 도형을 폭만 늘린 것", + "buttonSets": { + "common": { + "type": "COMPONENT_SET 120x218, variants 'Property 1' = Default / Variant2 / Variant3, 각 80x46", + "default": "면 없음, 아이콘 18x18 at (31,6) 회색 #B6B4AF 계열 SVG, 라벨 Dotum Regular 14 / 4% #F6F4F2 + 검정 1.3 / 자기 색 0.3 Stroke, top 25 가운데", + "variant2": "1px INSIDE GRADIENT stroke 위→아래 #DA6813 8.5% / #FFBB89 37.9% / #E5690D 71.9% / #AE3607 92.8%, 아이콘 hover SVG(#FF5C00), 라벨 #FF5C00, 아이콘·라벨 1px 위·왼쪽 이동(31,6→30,5 / top 25→24), radius = 그룹 첫 버튼 3 0 0 3, 마지막 0 3 3 0, 중간 0", + "variant3": "Variant2와 시각 속성 동일(Plugin API 전체 하위 노드 비교 결과 위치 y만 다름)", + "divider": "마지막 버튼을 뺀 버튼 오른쪽(-1px, hover -2px)에 1x44 세로선 GRADIENT(#886D35/#423625/#F3DBA8/#0E0B06/#574B30) + DROP_SHADOW -1 0 radius 2 #000", + "prototype": "Default ON_HOVER → Variant2, Variant2 ON_CLICK → Variant3, Variant3 ON_CLICK → Default" + }, + "items": [ + { "set": "26:11355", "name": "btn_저장", "label": "저장", "icon": "save", "position": "first", "variants": ["26:11356", "26:11361", "26:11366"], "screenshot": "screenshots/26-11355_btn-save.png", "note": "Default에 숨김(visible=false) 노란 gradient stroke 존재" }, + { "set": "26:11371", "name": "복사저장", "label": "복사저장", "icon": "save_as", "position": "middle", "variants": ["26:11372", "26:11377", "26:11382"], "screenshot": "screenshots/26-11371_btn-save-copy.png" }, + { "set": "26:11387", "name": "내보내기", "label": "내보내기", "icon": "file_open", "position": "middle", "variants": ["26:11388", "26:11393", "26:11398"], "screenshot": "screenshots/26-11387_btn-export.png" }, + { "set": "26:11403", "name": "도면묶기", "label": "도면묶기", "icon": "inventory_2", "position": "last", "variants": ["26:11404", "26:11408", "26:11412"], "screenshot": "screenshots/26-11403_btn-bundle.png" }, + { "set": "26:11416", "name": "찾아열기", "label": "찾아열기", "icon": "folder_open (top 5)", "position": "first", "variants": ["26:11417", "26:11428", "26:11439"], "screenshot": "screenshots/26-11416_btn-find-open.png", "note": "구분선 gradient가 다른 버튼과 다름(0deg 갈색 #493927 계열)" }, + { "set": "26:11450", "name": "최근파일", "label": "최근파일", "icon": "insert_drive_file(시계 포함)", "position": "middle", "variants": ["26:11451", "26:11462", "26:11473"], "screenshot": "screenshots/26-11450_btn-recent.png", "note": "구분선 gradient 2중(갈색 + 초록). Variant2 → Variant3 클릭 reaction 없음" }, + { "set": "26:11485", "name": "새파일", "label": "새파일", "icon": "insert_drive_file", "position": "last", "variants": ["26:11486", "26:11493", "26:11500"], "screenshot": "screenshots/26-11485_btn-new-file.png" }, + { "set": "26:11507", "name": "복구하기", "label": "복구하기", "icon": "rotate_left", "position": "first", "variants": ["26:11508", "26:11515", "26:11522"], "screenshot": "screenshots/26-11507_btn-recover.png" }, + { "set": "26:11529", "name": "임시파일", "label": "임시파일", "icon": "file_copy", "position": "last", "variants": ["26:11530", "26:11534", "26:11538"], "screenshot": "screenshots/26-11529_btn-temp-file.png" }, + { "set": "26:11542", "name": "디스플레이", "label": "Display...", "icon": "monitor 24x22 at (29,6)", "position": "first", "variants": ["26:11543", "26:11548", "26:11553"], "screenshot": "screenshots/26-11542_btn-display.png", "note": "라벨 박스 70px, Figma 원문 자체가 '...' 말줄임" }, + { "set": "26:11558", "name": "저장하기", "label": "저장하기", "icon": "설정 저장(그룹 아이콘 16.3x16.5)", "position": "middle", "variants": ["26:11559", "26:11568", "26:11577"], "screenshot": "screenshots/26-11558_btn-setting-save.png", "note": "flex gap 2px" }, + { "set": "26:11586", "name": "불러오기", "label": "불러오기", "icon": "save_alt", "position": "last", "variants": ["26:11587", "26:11591", "26:11595"], "screenshot": "screenshots/26-11586_btn-setting-load.png" }, + { "set": "26:11599", "name": "설명보기", "label": "설명보기", "icon": "content_paste", "position": "first", "variants": ["26:11600", "26:11605", "26:11610"], "screenshot": "screenshots/26-11599_btn-help.png" }, + { "set": "26:11615", "name": "문의하기", "label": "문의하기", "icon": "question_answer", "position": "middle", "variants": ["26:11616", "26:11621", "26:11626"], "screenshot": "screenshots/26-11615_btn-inquiry.png" }, + { "set": "26:11631", "name": "소프트웨어 정보", "label": "S/W정보", "icon": "정보(Frame 2405)", "position": "last", "variants": ["26:11632", "26:11639", "26:11646"], "screenshot": "screenshots/26-11631_btn-sw-info.png", "note": "Variant3가 button 형태가 아니며 Default로 돌아가는 클릭 reaction 없음" } + ] + }, + "supplementary": { + "note": "요청 21개 밖, 2단계 재확인(2026-10-19)에서 찾은 노드. 같은 SECTION '2. 헤더'의 주석 '최근파일'(26:11784) 아래 662:67267과 함께 배치. 서로 연결된 prototype reaction 없음(drop ON_CLICK의 action 비어 있음).", + "nodes": [ + { "id": "662:67331", "name": "최근파일", "type": "FRAME", "size": [354, 750], "screenshot": "screenshots/662-67331_recent-files.png", + "position": "662:67267 기준 x 808, y 77 (최근파일 타일 왼쪽 + 1, 메뉴 바로 아래)", + "designContext": "frame 1px #DA6813 좌·우·하, radius 0 0 3 3 / drop 354x750 #27241D, 1px #27241D, radius 0 0 4 4, shadow 0 2 4 rgba(0,0,0,.25) + inset 0 4 8 rgba(0,0,0,.4) / row 'header/최근파일/list' 50px, px 8, gap 8 (배지 없는 행 px 45) / icon_dualmonitor 29x28 (84a46 왼쪽 노랑, 9967e 오른쪽 노랑) / 이름 Dotum 14 4% #F6F4F2 (Stroke 2층) / 경로 12 4% #D4CBBD 60% / Hover 행 #62533D + 이름 #FF5C00" }, + { "id": "662:67471", "name": "파일출력 2뎁스", "type": "FRAME", "size": [310, 270], "screenshot": "screenshots/662-67471_file-preview-2depth.png", + "position": "최근파일 오른쪽 +1, Hover 행 top +1", + "designContext": "#62533D, 1px #62533D 상·우·하, radius 0 3 4 0, drop-shadow 0 2 1 rgba(0,0,0,.5), inset 2 0 2.5 rgba(0,0,0,.5), py 4 gap 4 / 경로 14px #FF5C00 (아래층 #D7D2B0) / image 918 310x244 (crop 158.67% x 191.87%, -25.97% / -52.46%)" } + ] + } +} diff --git a/docs/sw/figma-additional/integration-plan.md b/docs/sw/figma-additional/integration-plan.md new file mode 100644 index 0000000..f967c8e --- /dev/null +++ b/docs/sw/figma-additional/integration-plan.md @@ -0,0 +1,40 @@ +# Task A + B 통합 계획 — 로고 클릭 2단 Header 메뉴 (초안, 2026-10-19) + +> 상태: **미승인 계획**입니다. Task B는 신규 파일만 작성했고 아래 공유 파일은 아직 수정하지 않았습니다. 통합은 사용자 별도 승인 후 진행합니다. +> 근거: [2단계 보고](./stage2-report.md), Task A 현황(읽기 전용 확인, 미커밋): SWHeader `leading` 슬롯, `documentOpenPlacement`, CSS 변수 `--hm-sw-header-leading-width`, `components/sw-tool/`. + +## 1. 순서 + +1. Task A 변경을 먼저 확정한 뒤, 그 위에 Task B 신규 파일을 merge합니다. B 파일은 A 파일과 겹치지 않습니다. +2. export 연결 → 3. SWHeader 연결 → 4. 속성Bar 수정(선택) → 5. 문서·AI → 6. 검증. + +## 2. 작업 목록과 적용 방법 + +| # | 파일 | 변경 | 적용 방법 | +|---|---|---|---| +| I-1 | `packages/sw/src/index.ts` | 공개 export | `export * from "./components/sw-home-menu"` 한 줄. CSS는 `SWHomeMenu.tsx`·`SWRecentFiles.tsx`에서 import하므로 index에 CSS import를 추가하지 않습니다(`sideEffects` 때문에 index에서만 import하면 빌드에서 빠지는 것을 확인함) | +| I-2 | `components/sw-header/types.ts` | Header prop | `homeMenu?: Omit`, `homeMenuOpen?`, `defaultHomeMenuOpen?`(false), `onHomeMenuOpenChange?(open, reason)`. 미전달이면 기존 동작과 같습니다(하위 호환) | +| I-3 | `components/sw-header/SWHeader.tsx` | 연결 | `homeMenu`가 있을 때만 홈 클릭으로 열림을 토글합니다. 기존 `onHomeClick`은 항상 먼저 호출합니다. 메뉴는 `
` 안의 absolute 레이어(`top: 0`)로 렌더링하고, `returnFocusRef`는 홈 버튼입니다. 메뉴가 열리면 기존 필드 Dropdown(`activeMenu`)을 닫습니다 | +| I-4 | 같은 파일 | 앵커 | 시작 x = 객체 수정 영역 `getBoundingClientRect().left − header.left − 2px`, 폭 = 창 영역 left − 그 값입니다. 고정 375px을 쓰지 않습니다. Task A `leading` 슬롯이 있으면 `--hm-sw-header-leading-width`만큼 객체 영역이 이동하므로 **DOM 실측을 기준으로 하고 변수는 보조로 씁니다**. 기존 속성Bar `barOffset` 측정 방식과 같은 패턴입니다 | +| I-5 | `components/sw-header/components/SWHeaderHome.tsx` | 접근성 | `aria-expanded`·`aria-controls`(메뉴 id)를 추가합니다. Figma에 열림 시각 변화가 없으므로 스타일은 바꾸지 않습니다 | +| I-6 | `theme/sw.css` | z-index | 메뉴 레이어를 속성Bar 앵커(z 2)보다 위, 기존 Dropdown 패널과 같은 층에 둡니다. 동시에 열리지 않게 I-3에서 닫습니다 | +| I-7 | `theme/sw.css` (선택, A-6 승인 필요) | 속성Bar | P1: 좌·우·하 테두리를 세로 gradient `#08251C 0% / #208769 37.1% / #08241C 80.5% / #051612 100%`로 바꿉니다(기존 hover-ring mask 기법). P2: `.hm-sw-property-bar__object`에 `--hm-sw-header-text-outline`. P3: 라벨에 `--hm-sw-header-label-outline` | +| I-8 | `theme/sw.css` (검토) | home on 상·하단 선 | Figma 열림 상태의 1920 전폭 `#5F4B0F` 2px 선(y 0–2, 46–48)을 `.hm-sw-header[data-home-menu-open]`에서만 적용할지 사용자가 확인해야 합니다 | +| I-9 | `build.mjs`, `package.json` | 번들 | 변경이 필요 없을 것으로 예상합니다. `dist/styles.css`에 `.hm-sw-home-menu`가 포함되는지, `title-badge.svg`가 inline 또는 asset으로 나오는지 build 후 확인합니다 | +| I-10 | `packages/sw/ai/**`, `README.md`, `docs/sw/sw-header-usage.md` | 문서 | [AI 가이드 초안](./ai-home-menu-guide.md)을 COMPONENT_CATALOG·COMPOSITION_PATTERNS·DESIGN_RULES로 옮기고, `homeMenu` prop 사용 예를 추가합니다 | +| I-11 | 기존 stories | 예시 | `src/sw/sw-home-menu.stories.tsx`의 소스 경로 import를 `@hm-design/sw`로 바꿉니다. host wiring 예시는 I-3 이후 `SWHeader homeMenu` 예시로 대체합니다 | + +## 3. Task A와 맞출 결정 (사용자 확인) + +- `SWToolButton`에 `tile` appearance를 둘지, `SWHomeMenuItem`을 별도로 유지할지(A-1). +- 토글 이름: A는 `pressed`, B는 `active`/`activeIds`(별칭 추가 여부). +- `documentOpenPlacement` 기본값 `"menu"`는 메뉴와 독립적입니다. 찾아열기·새파일은 host가 `onDocumentOpen`·`onDocumentNew`와 같은 handler를 연결합니다(내부 결합 없음). +- Figma 아이콘 15쌍을 패키지 asset으로 둘지 여부(A-2). 현재는 Story fixture에만 있습니다. + +## 4. 통합 검증 + +1920·1440·1080·768·390 폭에서 확인합니다. +- 로고 열기·재클릭 닫기, 그리고 `leading`·문서 Dropdown(New/Open)과 겹치지 않는지. +- 기존 Header·속성Bar·Dropdown·K 가이드 회귀 없음. +- `tsc`, library build, packed tarball을 React 19 소비 프로젝트에 설치했을 때 동작하는지. +- Storybook 전체 Story(포트 충돌 주의). diff --git a/docs/sw/figma-additional/screenshots/26-10492_property-bar.png b/docs/sw/figma-additional/screenshots/26-10492_property-bar.png new file mode 100644 index 0000000..7632167 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-10492_property-bar.png differ diff --git a/docs/sw/figma-additional/screenshots/26-10591_home-on.png b/docs/sw/figma-additional/screenshots/26-10591_home-on.png new file mode 100644 index 0000000..2aee93c Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-10591_home-on.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11355_btn-save.png b/docs/sw/figma-additional/screenshots/26-11355_btn-save.png new file mode 100644 index 0000000..fc14c8b Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11355_btn-save.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11371_btn-save-copy.png b/docs/sw/figma-additional/screenshots/26-11371_btn-save-copy.png new file mode 100644 index 0000000..cba730f Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11371_btn-save-copy.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11387_btn-export.png b/docs/sw/figma-additional/screenshots/26-11387_btn-export.png new file mode 100644 index 0000000..f3197db Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11387_btn-export.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11403_btn-bundle.png b/docs/sw/figma-additional/screenshots/26-11403_btn-bundle.png new file mode 100644 index 0000000..58a6938 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11403_btn-bundle.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11416_btn-find-open.png b/docs/sw/figma-additional/screenshots/26-11416_btn-find-open.png new file mode 100644 index 0000000..4c4eab9 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11416_btn-find-open.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11450_btn-recent.png b/docs/sw/figma-additional/screenshots/26-11450_btn-recent.png new file mode 100644 index 0000000..2292127 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11450_btn-recent.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11485_btn-new-file.png b/docs/sw/figma-additional/screenshots/26-11485_btn-new-file.png new file mode 100644 index 0000000..7413ef6 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11485_btn-new-file.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11507_btn-recover.png b/docs/sw/figma-additional/screenshots/26-11507_btn-recover.png new file mode 100644 index 0000000..50a2f19 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11507_btn-recover.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11529_btn-temp-file.png b/docs/sw/figma-additional/screenshots/26-11529_btn-temp-file.png new file mode 100644 index 0000000..ad8b87f Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11529_btn-temp-file.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11542_btn-display.png b/docs/sw/figma-additional/screenshots/26-11542_btn-display.png new file mode 100644 index 0000000..74ce37f Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11542_btn-display.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11558_btn-setting-save.png b/docs/sw/figma-additional/screenshots/26-11558_btn-setting-save.png new file mode 100644 index 0000000..252929d Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11558_btn-setting-save.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11586_btn-setting-load.png b/docs/sw/figma-additional/screenshots/26-11586_btn-setting-load.png new file mode 100644 index 0000000..f315d4e Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11586_btn-setting-load.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11599_btn-help.png b/docs/sw/figma-additional/screenshots/26-11599_btn-help.png new file mode 100644 index 0000000..f797136 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11599_btn-help.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11615_btn-inquiry.png b/docs/sw/figma-additional/screenshots/26-11615_btn-inquiry.png new file mode 100644 index 0000000..8288ee8 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11615_btn-inquiry.png differ diff --git a/docs/sw/figma-additional/screenshots/26-11631_btn-sw-info.png b/docs/sw/figma-additional/screenshots/26-11631_btn-sw-info.png new file mode 100644 index 0000000..7855e9e Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-11631_btn-sw-info.png differ diff --git a/docs/sw/figma-additional/screenshots/26-12604_tit-working-file.png b/docs/sw/figma-additional/screenshots/26-12604_tit-working-file.png new file mode 100644 index 0000000..437d007 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-12604_tit-working-file.png differ diff --git a/docs/sw/figma-additional/screenshots/26-12610_tit-open-other.png b/docs/sw/figma-additional/screenshots/26-12610_tit-open-other.png new file mode 100644 index 0000000..1c16c83 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-12610_tit-open-other.png differ diff --git a/docs/sw/figma-additional/screenshots/26-12616_tit-curious.png b/docs/sw/figma-additional/screenshots/26-12616_tit-curious.png new file mode 100644 index 0000000..b239023 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-12616_tit-curious.png differ diff --git a/docs/sw/figma-additional/screenshots/26-12622_tit-file-damaged.png b/docs/sw/figma-additional/screenshots/26-12622_tit-file-damaged.png new file mode 100644 index 0000000..ed3e869 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/26-12622_tit-file-damaged.png differ diff --git a/docs/sw/figma-additional/screenshots/662-67267_home-on-popup-guide.png b/docs/sw/figma-additional/screenshots/662-67267_home-on-popup-guide.png new file mode 100644 index 0000000..efebf12 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/662-67267_home-on-popup-guide.png differ diff --git a/docs/sw/figma-additional/screenshots/662-67331_recent-files.png b/docs/sw/figma-additional/screenshots/662-67331_recent-files.png new file mode 100644 index 0000000..ab32fa6 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/662-67331_recent-files.png differ diff --git a/docs/sw/figma-additional/screenshots/662-67471_file-preview-2depth.png b/docs/sw/figma-additional/screenshots/662-67471_file-preview-2depth.png new file mode 100644 index 0000000..8e9f208 Binary files /dev/null and b/docs/sw/figma-additional/screenshots/662-67471_file-preview-2depth.png differ diff --git a/docs/sw/figma-additional/stage2-qa/compare-left-2x.png b/docs/sw/figma-additional/stage2-qa/compare-left-2x.png new file mode 100644 index 0000000..4dffb69 Binary files /dev/null and b/docs/sw/figma-additional/stage2-qa/compare-left-2x.png differ diff --git a/docs/sw/figma-additional/stage2-qa/compare-property-bar-2x.png b/docs/sw/figma-additional/stage2-qa/compare-property-bar-2x.png new file mode 100644 index 0000000..4b59c55 Binary files /dev/null and b/docs/sw/figma-additional/stage2-qa/compare-property-bar-2x.png differ diff --git a/docs/sw/figma-additional/stage2-qa/compare-recent-files.png b/docs/sw/figma-additional/stage2-qa/compare-recent-files.png new file mode 100644 index 0000000..17fa71c Binary files /dev/null and b/docs/sw/figma-additional/stage2-qa/compare-recent-files.png differ diff --git a/docs/sw/figma-additional/stage2-qa/compare-right-2x.png b/docs/sw/figma-additional/stage2-qa/compare-right-2x.png new file mode 100644 index 0000000..30bdd66 Binary files /dev/null and b/docs/sw/figma-additional/stage2-qa/compare-right-2x.png differ diff --git a/docs/sw/figma-additional/stage2-qa/react19-consumer.png b/docs/sw/figma-additional/stage2-qa/react19-consumer.png new file mode 100644 index 0000000..7438580 Binary files /dev/null and b/docs/sw/figma-additional/stage2-qa/react19-consumer.png differ diff --git a/docs/sw/figma-additional/stage2-qa/responsive-1080.png b/docs/sw/figma-additional/stage2-qa/responsive-1080.png new file mode 100644 index 0000000..10a3920 Binary files /dev/null and b/docs/sw/figma-additional/stage2-qa/responsive-1080.png differ diff --git a/docs/sw/figma-additional/stage2-qa/responsive-1920.png b/docs/sw/figma-additional/stage2-qa/responsive-1920.png new file mode 100644 index 0000000..3ffbe26 Binary files /dev/null and b/docs/sw/figma-additional/stage2-qa/responsive-1920.png differ diff --git a/docs/sw/figma-additional/stage2-qa/responsive-390.png b/docs/sw/figma-additional/stage2-qa/responsive-390.png new file mode 100644 index 0000000..7d4e65c Binary files /dev/null and b/docs/sw/figma-additional/stage2-qa/responsive-390.png differ diff --git a/docs/sw/figma-additional/stage2-report.md b/docs/sw/figma-additional/stage2-report.md new file mode 100644 index 0000000..8e3b3af --- /dev/null +++ b/docs/sw/figma-additional/stage2-report.md @@ -0,0 +1,129 @@ +# 작업 B 2단계 — 로고 클릭 2단 Header 메뉴 구현·검증 보고 (2026-10-19) + +> 기준: 사용자 최종 정정(로고 클릭 = 2단 Header 메뉴, 기본 숨김, 재클릭 닫힘), [1단계 분석](./analysis-report.md), Figma `rj6wpsfk24lOWvWiVRAANP`. +> 범위: Task B worktree `claude/sw-figma-logo-menu-36832b`(HEAD `5faa1f1`)의 **신규 파일만** 수정했습니다. Task A 공유 파일은 수정하지 않았고, Commit·Push·Merge·Publish도 하지 않았습니다. + +## 0. 해석 변경 이력 + +| 시점 | 해석 | 최종 | +|---|---|---| +| 1단계 | 로고 클릭 → `home on` 2단 메뉴 overlay | **유지(최종 정정과 일치)** | +| 2단계 중간 요청 | 로고 클릭 → "파일 열기 창" | Figma 재확인 결과 앱 내부 UI이며 OS 파일 선택창 호출 구조가 아닙니다. 이후 사용자 최종 정정으로 2단 메뉴로 돌아왔습니다 | +| 재확인으로 바뀐 점 | 662:67267을 단순 "하단 강조 outline 변형"으로 본 것 | 실제로는 Figma 주석 **"최근파일" 상태 화면**입니다. 최근파일이 열려 메뉴 전체에 주황 외곽선이 생기고, 그 아래 최근파일 목록(662:67331)과 2뎁스 미리보기(662:67471)가 붙습니다. 두 노드는 요청한 21개 목록 밖이라 **선택 기능**(`panel` + `SWRecentFiles`)으로 구현했습니다 | + +## 1. 구현 컴포넌트 + +| 파일(모두 신규) | 내용 | +|---|---| +| `packages/sw/src/components/sw-home-menu/SWHomeMenu.tsx` | `SWHomeMenu`(메뉴·열림·Active·닫기·키보드), `SWHomeMenuSection`(제목 badge + 그룹), `SWHomeMenuItem`(80×46 타일, Default / Variant2 / Variant3), `useSWHomeMenuClose` | +| `…/sw-home-menu/SWRecentFiles.tsx` | 선택 하위 패널: 최근파일 목록 354px, 행 50px, 2뎁스 미리보기 310×270 | +| `…/sw-home-menu/types.ts`, `index.ts` | 공개 타입·export | +| `…/sw-home-menu/assets/title-badge.svg` | Figma tit_* Union 도형(9-slice로 폭 가변) | +| `packages/sw/src/theme/sw-home-menu.css` | `.hm-sw-home-menu*`, `.hm-sw-recent-files*` 스타일과 `--hm-sw-home-menu-*`·`--hm-sw-recent-files-*` 토큰. 기존 SW 토큰(글꼴, 외곽선, focus, 행 Hover)을 그대로 읽습니다 | +| `src/sw/sw-home-menu.stories.tsx`, `sw-home-menu-fixture.tsx`, `sw-home-menu.css`, `src/sw/assets/home-menu/**` | Storybook `SW/Home Menu`(Figma 재현, Header 조합, 3 Variant, 데이터 변경, 단독, 최근파일, 속성Bar 비교, 비교 캔버스). Figma 아이콘 30개·미리보기 이미지는 **Story fixture 전용**입니다 | +| `docs/sw/figma-additional/ai-home-menu-guide.md`, `stage2-report.md`, `stage2-qa/**`, `screenshots/662-67331·662-67471` | AI 가이드 초안, 이 보고서, 검증 스크린샷 | + +## 2. Figma 노드 대응 (22 + 참고 3) + +| Node | 대응 | 상태 | +|---|---|---| +| 26:10492 속성Bar | 기존 `SWHeaderPropertyBar`(수정 없음). Story `속성Bar 비교`에서 Figma 값으로 비교 | 차이 3건은 통합 단계(§8) | +| 26:10591 home on | `SWHomeMenu` + `accent="auto"`(패널 없음 → 섹션별 강조) | 재현 | +| 662:67267 home on(최근파일 상태) | 최근파일 타일 Active + 패널 → `accent` 자동 외곽선. K 미리보기는 기존 `SWHeaderKGuide` Hover | 재현 | +| 26:12604 / 12610 / 12622 / 12616 (+26:12628 참고) | `SWHomeMenuSection.title` (badge 공통 1개) | 재현 | +| 26:11355 … 26:11631 버튼 세트 15개 | `SWHomeMenuItem` 1개 + Story 데이터 15건. Default / V2(Hover) / V3(Active) | 재현 | +| 662:67331 최근파일 / 662:67471 2뎁스 (참고) | `SWRecentFiles` (`panel`로 연결) | 재현(선택 기능) | + +## 3. 공개 API + +```ts +/) + open? defaultOpen?=false onOpenChange?(open, reason: "escape"|"background"|"outside"|"select"|"file") + activeIds? defaultActiveIds? onActiveChange?(ids, event) selection?: "multiple"|"single" + onItemSelect?(event) closeOnBackgroundClick?=true closeOnSelect?=false closeOnEscape?=true closeOnOutsideClick?=false + autoFocus?=true returnFocusRef? accent?: "auto"|"sections"|"outline" /> +SWHomeMenuItemData { id, label, icon?, hoverIcon?, toggle?=true, panel?, panelLabel?, disabled?, hidden?, title?, aria-label?, onSelect? } +SWHomeMenuSectionData { id, title, items, hidden? } + +SWFileEntry { id, name, path?, badge?, preview?, previewTitle?, disabled?, title? } +``` + +- 텍스트·아이콘·항목·이벤트·Active·하위 패널은 모두 외부 데이터입니다. 패키지에는 메뉴 항목이나 CAD 동작이 들어 있지 않습니다. +- 단독 사용: `` 안에 둡니다. 메뉴 밖에서 쓰는 `SWHomeMenuItem`도 `active`·`onActiveChange`로 동작합니다. +- Header 연동(통합 전): 기존 `SWHeader onHomeClick`으로 `open`을 토글합니다. 위치는 host가 Header DOM(객체 수정 영역 왼쪽 − 2px부터 창 영역 왼쪽까지)에서 측정합니다. + +## 4. Figma 디자인 일치 + +1x 캡처는 headless Chromium의 1920px 화면에서 Figma 스크린샷과 2배 확대로 비교했습니다(`stage2-qa/compare-*.png`). + +| 항목 | 결과 | +|---|---| +| 위치·크기 | 1920에서 메뉴 left 375, 폭 1305, 높이 77이며 섹션 폭은 340/260/180/260/261입니다. 그룹 46, 타일 80×46, 아이콘 18 슬롯(31,6), 라벨 top 25 **일치** | +| 색·gradient·그림자 | 섹션 면·안쪽 선, 그룹 테두리 gradient(위→아래)·면(아래→위)·그림자, 구분선, 주황 Hover 테두리, 하단 강조 2종 **일치**(Plugin API 값과 gradientTransform 방향 반영) | +| 아이콘 | Figma SVG 30개(Default·Hover)를 그대로 사용하고, 위치 보정 값은 fixture에 둠 **일치** | +| 최근파일·미리보기 | 354 폭, 행 50, 배경 `#27241D`, 주황 테두리, Hover 행 `#62533D` + `#FF5C00`, 미리보기 310×270 **일치**(DPR 1.5 화면에서는 테두리 반올림으로 269.3) | +| 의도된 차이 | 글꼴은 Dotum → Noto Sans KR이고, 타일 라벨·파일명은 700(기존 SW 정책)이라 Figma보다 굵습니다. 섹션 제목은 400이며 Figma보다 가늘고 badge가 7px 좁습니다(116.6 vs 124). 첫 섹션 면 gradient stop은 나머지 4개 기준으로 통일했습니다. 찾아열기·최근파일 구분선의 다른 gradient는 공통 구분선으로 통일했습니다 | +| Header 소유 차이 | home on의 1920 전폭 상·하단 `#5F4B0F` 2px 선은 Header 소유라 미구현(통합 검토) | + +## 5. 상태 전환·Overlay 테스트 (Storybook 6031, 실제 실행) + +| 테스트 | 결과 | +|---|---| +| 기본 숨김 | PASS (`hiddenByDefault: true`, consumer DOM에도 없음) | +| 로고 클릭 열기 / 재클릭 닫기 | PASS | +| Hover → V2 (실제 마우스) | PASS (테두리 opacity 1, `#FF5C00`, Hover 아이콘 표시, -1px 이동) | +| 클릭 → V3, 재클릭 → Default | PASS (`aria-pressed` true/false) | +| 메뉴 빈 곳 클릭 닫기 / Escape 닫기 + 로고 Focus 복귀 | PASS | +| ←/→/Home/End, Tab Focus 2px 윤곽선, Enter 실행 | PASS (실제 키 입력) | +| 최근파일: 열기 → 외곽선 강조, `aria-expanded`, 재클릭 닫기, ↓로 목록 진입, 행 Hover·Focus 미리보기, Enter/클릭 → `onFileOpen` + 메뉴 닫기, 다시 열면 Default | PASS | +| Escape 단계(패널 → 메뉴) | PASS | +| 데이터 변경(추가·삭제·숨김·disabled·긴 라벨·섹션 추가·single·accent) | PASS | +| 반응형 1920/1440/1080/768/390 | PASS: 로고·창 영역과 겹치지 않고 메뉴 안에서 가로 스크롤. 좁은 폭 접기 디자인은 없음(별도 승인) | +| 타입 | PASS: `tsc` SW 패키지, Story, 독립 consumer | +| 독립 React 19.3 consumer(Vite 빌드, 소스 import) | PASS: 렌더, 기본 숨김, CSS·SVG 번들 포함 | + +참고: 브라우저 pane은 background page라 ResizeObserver 측정을 할 수 없어, 반응형은 headless Chromium으로 확인했습니다. 메뉴 CSS를 `index.ts`에서만 import하면 `sideEffects` 설정 때문에 빌드에서 빠지는 것을 발견했고, 사용하는 컴포넌트 파일에서 import하도록 고쳤습니다. + +## 6. AI 재사용 + +[AI 가이드 초안](./ai-home-menu-guide.md) 기준입니다. 새 메뉴는 `sections` 데이터만으로 같은 스타일이 적용되고, 폭·구분선·모서리·badge 폭이 자동으로 계산됩니다. 소비 프로젝트에서 새 섹션 2개·항목 5개(currentColor 아이콘, `toggle:false`, 패널)를 만든 결과가 같은 스타일로 렌더링되는 것을 확인했습니다(`stage2-qa/react19-consumer.png`). AI 가이드를 `packages/sw/ai/`로 옮기는 작업은 통합 단계입니다. + +## 7. Task A와의 중복·충돌 + +| 항목 | 내용 | 조치 | +|---|---|---| +| `SWToolButton` (pill/text/document) | 80×46 타일 appearance가 없어 중복 구현은 아닙니다 | 통합 때 `tile` appearance로 합칠지 결정(별도 승인) | +| 아이콘 prop 이름 | A `hoverIcon` | B도 `hoverIcon`으로 맞춤 | +| 토글 상태 이름 | A `pressed` / B `active`·`activeIds` | 메뉴는 여러 항목 상태라 `activeIds` 유지. 통합 때 별칭 검토 | +| CSS | A `hm-sw-tool__*` / B `hm-sw-home-menu__*`, `hm-sw-recent-files__*` | 충돌 없음 | +| `documentOpenPlacement` | 사용자 최종 지시: 기본 `"menu"`(Open은 문서 Dropdown 안), 로고 옆 독립 Open 없음 | B 메뉴는 이 값과 무관합니다. B Story의 Header(5faa1f1)에는 독립 Open이 없습니다 | +| `leading` 슬롯 | 객체 수정 영역 시작 x가 바뀝니다 | B는 DOM 측정이라 하드코딩 없음 | +| 찾아열기·새파일 ↔ `onDocumentOpen`·`onDocumentNew` | 동작이 중복될 수 있습니다 | 컴포넌트는 콜백만 받습니다. host가 같은 handler를 연결하는 방식을 권장합니다(통합 문서에 기재) | + +## 8. 통합 시 수정할 공유 파일 + +| 파일 | 변경 | +|---|---| +| `packages/sw/src/index.ts` | `export * from "./components/sw-home-menu"` | +| `components/sw-header/SWHeader.tsx`, `types.ts` | (안) `homeMenu?: SWHomeMenuProps`, `homeMenuOpen`/`defaultHomeMenuOpen`/`onHomeMenuOpenChange`. 홈 버튼 `aria-expanded`·`aria-controls`, 객체 수정 영역 −2px ~ 창 영역 앵커, 열릴 때 다른 Dropdown 닫기, 기존 `onHomeClick` 유지(하위 호환) | +| `components/sw-header/components/SWHeaderHome.tsx` | 열림 상태 전달(시각 변화는 Figma에 없음) | +| `theme/sw.css` | 속성Bar P1 gradient 테두리, P2·P3 객체명·라벨 외곽선. home on 상·하단 `#5F4B0F` 선 적용 여부(검토) | +| `build.mjs`, `package.json` | 새 CSS가 `styles.css`에 포함되는지 확인(컴포넌트 import 방식이라 변경 불필요 예상, 미검증) | +| `packages/sw/ai/**`, `README.md`, `docs/sw/sw-header-usage.md` | AI 가이드 초안 반영 | +| 기존 stories | 필요하면 Header Overview에 메뉴 연결 | + +## 9. 판정 + +**PARTIAL** — 독립 구현과 검증은 PASS입니다. 다만 공유 파일 연동(공개 export, `SWHeader` 내장 연결, 속성Bar 수정)은 승인 범위상 통합 단계로 남았습니다. + +미검증 항목: +- 패키지 진입점(`@hm-design/sw`) export와 `build.mjs` dist의 `styles.css` 포함 여부. tarball 소비 검증도 하지 않았습니다(통합 단계). +- Task A 최신 Header(leading·documentOpenPlacement 반영본)와의 실제 조합. B는 5faa1f1 기준 Header로만 검증했습니다. +- Firefox·Safari 렌더링(Chromium만 확인). +- 속성Bar P1–P3를 수정한 뒤의 비교. +- 스크린리더 실제 낭독. + +검토 대상: V3의 의미(유지 토글 vs 순간 누름; 현재는 사용자 지시대로 유지 토글), 파일 열기 후 닫기 기본값, 최근파일 패널 열기 방식(Figma 연결 없음 → 클릭), 빈 목록 문구, 좁은 폭 디자인, Header 상·하단 선. + +로컬 검증 환경(커밋 대상 아님): worktree `node_modules` junction(→ HMDesign-SW), `.storybook-task-b/`(신규 Story만 로드하는 설정과 웹폰트 fs.allow), `.claude/launch.json`. 실행 중인 Storybook 포트는 6031입니다. 정리 여부는 사용자가 결정합니다. diff --git a/docs/sw/home-menu-integration-qa/baseline-improvement/integrated-improvement-result.json b/docs/sw/home-menu-integration-qa/baseline-improvement/integrated-improvement-result.json new file mode 100644 index 0000000..a4c6cad --- /dev/null +++ b/docs/sw/home-menu-integration-qa/baseline-improvement/integrated-improvement-result.json @@ -0,0 +1,1286 @@ +{ + "browser": "155.0.8059.80", + "checks": [ + { + "name": "menu (default): no Open next to the logo", + "status": "PASS" + }, + { + "name": "menu (default): dropdown footer keeps + New and Open", + "status": "PASS" + }, + { + "name": "header: Open takes the file name place right of the logo (same junction offset, 48px)", + "status": "PASS", + "data": { + "home": { + "x": 360, + "y": 851.5, + "w": 80, + "h": 48, + "right": 440 + }, + "openBox": { + "x": 425, + "y": 851.5, + "w": 58.265625, + "h": 48, + "right": 483.265625 + }, + "junction": -15 + } + }, + { + "name": "header: file name follows Open without overlap", + "status": "PASS", + "data": { + "openBox": { + "x": 425, + "y": 851.5, + "w": 58.265625, + "h": 48, + "right": 483.265625 + }, + "docBox": { + "x": 485.265625, + "y": 851.5, + "w": 249.734375, + "h": 48, + "right": 735 + } + } + }, + { + "name": "header: Open uses hm-sw-header__document-button (same surface, color, weight as the file name)", + "status": "PASS", + "data": { + "styles": { + "cls": "hm-sw-header__document-button", + "bg": "linear-gradient(rgb(8, 24, 19) 0%, rgb(9", + "color": "rgb(240, 247, 245)", + "weight": "700", + "padL": "5px", + "borderL": "0px", + "borderR": "1px" + }, + "fileStyles": { + "bg": "linear-gradient(rgb(8, 24, 19) 0%, rgb(9", + "color": "rgb(240, 247, 245)", + "weight": "700", + "padL": "12px" + } + } + }, + { + "name": "D-1: file name after Open starts with 12px padding", + "status": "PASS", + "data": { + "bg": "linear-gradient(rgb(8, 24, 19) 0%, rgb(9", + "color": "rgb(240, 247, 245)", + "weight": "700", + "padL": "12px" + } + }, + { + "name": "header: Open hover turns only the text #FFC600", + "status": "PASS" + }, + { + "name": "header: dropdown footer has + New only (no duplicate Open)", + "status": "PASS" + }, + { + "name": "header: Open calls onDocumentOpen", + "status": "PASS" + }, + { + "name": "documentOpenLabel is the Open text", + "status": "PASS" + }, + { + "name": "responsive 1920: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 2280, + "barRight": 2280, + "overlap": false, + "open": 58.265625, + "doc": 249.734375, + "layer": 234, + "more": false + } + }, + { + "name": "responsive 1920: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1920: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 1800: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 2160, + "barRight": 2160, + "overlap": false, + "open": 58.265625, + "doc": 190, + "layer": 234, + "more": false + } + }, + { + "name": "responsive 1800: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1800: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 1440: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1800, + "barRight": 1800, + "overlap": false, + "open": 58.265625, + "doc": 310, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 1440: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1440: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 1180: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1540, + "barRight": 1540, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 1180: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1180: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 1080: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1440, + "barRight": 1440, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 1080: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1080: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 900: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 1260, + "barRight": 1260, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 900: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 900: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 768: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 1128, + "barRight": 1128, + "overlap": false, + "open": 58.265625, + "doc": 178.734375, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 768: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 768: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 640: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1000, + "barRight": 1000, + "overlap": false, + "open": 58.265625, + "doc": 96, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 640: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 640: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 520: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 880, + "barRight": 880, + "overlap": false, + "open": 0, + "doc": 103, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 520: Open moved into More and works there", + "status": "PASS", + "data": { + "open": 0, + "inMore": true + } + }, + { + "name": "responsive 520: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 390: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 750, + "barRight": 750, + "overlap": false, + "open": 0, + "doc": 96, + "layer": 113, + "more": true + } + }, + { + "name": "responsive 390: Open moved into More and works there", + "status": "PASS", + "data": { + "open": 0, + "inMore": true + } + }, + { + "name": "resize: collapsed Open returns once when the header grows (measured, with hysteresis)", + "status": "PASS", + "data": [ + [ + 520, + 0 + ], + [ + 560, + 0 + ], + [ + 600, + 1 + ], + [ + 640, + 1 + ], + [ + 680, + 1 + ], + [ + 720, + 1 + ], + [ + 760, + 1 + ], + [ + 820, + 1 + ], + [ + 900, + 1 + ], + [ + 1080, + 1 + ] + ] + }, + { + "name": "hidden documentNew: footer keeps Open on its right side", + "status": "PASS", + "data": { + "footer": { + "x": 424, + "y": 442.5, + "w": 310, + "h": 40, + "right": 734 + }, + "action": { + "x": 675.96875, + "y": 451, + "w": 50.03125, + "h": 24, + "right": 726 + } + } + }, + { + "name": "hidden documentNew + documentOpen: no footer", + "status": "PASS" + }, + { + "name": "hidden layerFreeze / layerLock: field draws the bulb only, two empty 20px places", + "status": "PASS", + "data": [ + { + "empty": false, + "w": 20 + }, + { + "empty": true, + "w": 20 + }, + { + "empty": true, + "w": 20 + } + ] + }, + { + "name": "hidden layerFreeze / layerLock: rows have no snowflake / lock button, places kept", + "status": "PASS", + "data": [ + { + "buttons": [ + "hm-sw-header__layer-toggle--visibility", + "hm-sw-header__layer-item" + ], + "empty": 2, + "name": 931 + }, + { + "buttons": [ + "hm-sw-header__layer-toggle--visibility", + "hm-sw-header__layer-item" + ], + "empty": 2, + "name": 931 + }, + { + "buttons": [ + "hm-sw-header__layer-toggle--visibility", + "hm-sw-header__layer-item" + ], + "empty": 2, + "name": 931 + } + ] + }, + { + "name": "hidden layer states: the bulb still reports its callback", + "status": "PASS" + }, + { + "name": "composed: Open right of the logo at the same junction offset as SWHeader", + "status": "PASS", + "data": { + "cHome": { + "x": 360, + "y": 6118.5, + "w": 80, + "h": 48, + "right": 440 + }, + "cOpen": { + "x": 425, + "y": 6118.5, + "w": 58.265625, + "h": 48, + "right": 483.265625 + } + } + }, + { + "name": "composed: file name after Open has 12px padding", + "status": "PASS" + }, + { + "name": "composed: Open callback", + "status": "PASS" + }, + { + "name": "composed: showOpen={false} removes Open from the dropdown", + "status": "PASS" + }, + { + "name": "composed responsive 1920: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 1800: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 1440: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 1180: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 1080: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 900: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 768: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 768: 파일 열기 reachable in More", + "status": "PASS" + }, + { + "name": "composed responsive 640: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 640: 파일 열기 reachable in More", + "status": "PASS" + }, + { + "name": "composed responsive 520: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 520: 파일 열기 reachable in More", + "status": "PASS" + }, + { + "name": "composed responsive 390: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 390: 파일 열기 reachable in More", + "status": "PASS" + }, + { + "name": "tool: Model / Layout is a radio group", + "status": "PASS" + }, + { + "name": "tool: arrow key moves focus and selects Layout", + "status": "PASS" + }, + { + "name": "tool: 2.5D toggle reports and shows pressed (#FFC600)", + "status": "PASS" + }, + { + "name": "tool: text buttons are the 속성Bar toggle look (700, 19px row, 12px caption)", + "status": "PASS", + "data": { + "text": { + "weight": "700", + "h": 19 + }, + "ref": "12px" + } + }, + { + "name": "tool: pill button is 40 x 38 with the yellow ring and hover icon", + "status": "PASS", + "data": { + "pillBox": { + "x": 1467.234375, + "y": 336, + "w": 40, + "h": 38, + "right": 1507.234375 + }, + "hover": { + "ring": "1", + "icon": "hidden", + "hoverIcon": "visible" + } + } + }, + { + "name": "tool: pill click callback", + "status": "PASS" + }, + { + "name": "tool: pill accessible name from its text", + "status": "PASS" + }, + { + "name": "tool: document look keeps the logo junction in a composed bar", + "status": "PASS", + "data": { + "tHome": { + "x": 360, + "y": 491.5, + "w": 80, + "h": 48, + "right": 440 + }, + "tOpen": { + "x": 425, + "y": 491.5, + "w": 58.265625, + "h": 48, + "right": 483.265625 + } + } + }, + { + "name": "popup: SWPopup frames match the property window frame (314, surface, border, radius, title)", + "status": "PASS", + "data": [ + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + }, + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + }, + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + } + ] + }, + { + "name": "popup content: SWHeaderInput rows commit through the host callbacks", + "status": "PASS" + }, + { + "name": "popup content: SWPopupRow is the property row (72px label, 32px row, 28px box)", + "status": "PASS", + "data": { + "label": "72px", + "minH": "32px", + "box": "28px" + } + }, + { + "name": "popup content: HM UI Switch works inside", + "status": "PASS" + }, + { + "name": "popup content: HM UI Select (portal) works inside", + "status": "PASS" + }, + { + "name": "popup content: HM UI Slider works inside", + "status": "PASS", + "data": "Callback: name=평면도-02 → scale=50 → snap=false → unit=cm → opacity=70" + }, + { + "name": "popup content: HM UI Button works inside", + "status": "PASS" + }, + { + "name": "popup dropdown: SW layer dropdown opens right under its field (fixed overlay) and is not clipped", + "status": "PASS", + "data": { + "fieldBox": { + "x": 387, + "y": 548.5, + "w": 234, + "h": 38, + "right": 621 + }, + "menuBox": { + "x": 387, + "y": 586.5, + "w": 234, + "h": 160, + "right": 621 + }, + "hit": true + } + }, + { + "name": "popup dropdown: selection callback, list closes, popup stays", + "status": "PASS" + }, + { + "name": "popup dropdown: SW color dropdown becomes a fixed overlay", + "status": "PASS" + }, + { + "name": "popup dropdown: color chip callback", + "status": "PASS" + }, + { + "name": "popup dropdown: Escape closes the dropdown only", + "status": "PASS" + }, + { + "name": "object popup: read-only rows", + "status": "PASS" + }, + { + "name": "object popup: number row commits", + "status": "PASS" + }, + { + "name": "object popup: compact row uses the 41px hug field", + "status": "PASS", + "data": { + "cls": "hm-sw-header__input hm-sw-property__compact", + "w": 26.53125 + } + }, + { + "name": "property panel + children: children follow the property groups in the same body", + "status": "PASS", + "data": [ + "공통", + "메모" + ] + }, + { + "name": "property panel + children: child content callback", + "status": "PASS" + }, + { + "name": "popup: close button calls onClose", + "status": "PASS" + }, + { + "name": "short popup: dropdown extends past the popup and stays fully visible (no clipping)", + "status": "PASS", + "data": { + "bBox": { + "x": 706, + "y": 264.5, + "w": 314, + "h": 110, + "right": 1020 + }, + "menuB": { + "x": 717, + "y": 363.5, + "w": 234, + "h": 160, + "right": 951 + }, + "bHit": true + } + }, + { + "name": "several popups: Escape in a dropdown closes only the dropdown", + "status": "PASS" + }, + { + "name": "several popups: Escape on an edited input cancels the edit only", + "status": "PASS" + }, + { + "name": "several popups: Escape inside a popup closes that popup only", + "status": "PASS", + "data": "Callback: close(a)" + }, + { + "name": "several popups: Escape outside closes only the top popup (last used: B)", + "status": "PASS", + "data": "Callback: close(a) → close(b)" + }, + { + "name": "several popups: the popup used last becomes the top", + "status": "PASS", + "data": "Callback: close(a) → close(b) → close(a)" + }, + { + "name": "long content: the body scrolls inside the frame and the last row is reachable", + "status": "PASS", + "data": { + "scroll": { + "sh": 1000, + "ch": 328, + "overflow": "auto" + }, + "lastVisible": true + } + }, + { + "name": "no browser console or page errors", + "status": "PASS", + "data": [] + } + ], + "errors": [], + "data": { + "junction": -15, + "responsive": [ + { + "width": 1920, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 2280, + "barRight": 2280, + "overlap": false, + "open": 58.265625, + "doc": 249.734375, + "layer": 234, + "more": false, + "inMore": null + }, + { + "width": 1800, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 2160, + "barRight": 2160, + "overlap": false, + "open": 58.265625, + "doc": 190, + "layer": 234, + "more": false, + "inMore": null + }, + { + "width": 1440, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1800, + "barRight": 1800, + "overlap": false, + "open": 58.265625, + "doc": 310, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 1180, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1540, + "barRight": 1540, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 1080, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1440, + "barRight": 1440, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 900, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 1260, + "barRight": 1260, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 768, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 1128, + "barRight": 1128, + "overlap": false, + "open": 58.265625, + "doc": 178.734375, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 640, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1000, + "barRight": 1000, + "overlap": false, + "open": 58.265625, + "doc": 96, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 520, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 880, + "barRight": 880, + "overlap": false, + "open": 0, + "doc": 103, + "layer": 234, + "more": true, + "inMore": true + }, + { + "width": 390, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 750, + "barRight": 750, + "overlap": false, + "open": 0, + "doc": 96, + "layer": 113, + "more": true, + "inMore": true + } + ], + "hysteresis": [ + [ + 520, + 0 + ], + [ + 560, + 0 + ], + [ + 600, + 1 + ], + [ + 640, + 1 + ], + [ + 680, + 1 + ], + [ + 720, + 1 + ], + [ + 760, + 1 + ], + [ + 820, + 1 + ], + [ + 900, + 1 + ], + [ + 1080, + 1 + ] + ], + "composedResponsive": [ + { + "width": 1920, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 1800, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 1440, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 1180, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 1080, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 900, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 768, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + }, + { + "width": 640, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + }, + { + "width": 520, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + }, + { + "width": 390, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + ], + "popupFrames": [ + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + }, + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + }, + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + } + ] + }, + "status": "PASS" +} diff --git a/docs/sw/home-menu-integration-qa/baseline-visual/capture.json b/docs/sw/home-menu-integration-qa/baseline-visual/capture.json new file mode 100644 index 0000000..7454952 --- /dev/null +++ b/docs/sw/home-menu-integration-qa/baseline-visual/capture.json @@ -0,0 +1,34 @@ +{ + "browser": "155.0.8059.80", + "shots": [ + "overview-1920.png", + "detail-buttons-1920.png", + "interactive-states-1920.png", + "window-move-motion-1920.png", + "logo-slot-1920.png", + "document-dropdown-1920.png", + "responsive-more-1920.png", + "k-guide-1920.png", + "property-panel-compare-1920.png", + "composed-controls-1920.png", + "overview-1800.png", + "overview-1440.png", + "overview-1180.png", + "overview-1080.png", + "overview-900.png", + "overview-768.png", + "overview-640.png", + "overview-520.png", + "overview-390.png", + "responsive-more-1800.png", + "responsive-more-1440.png", + "responsive-more-1180.png", + "responsive-more-1080.png", + "responsive-more-900.png", + "responsive-more-768.png", + "responsive-more-640.png", + "responsive-more-520.png", + "responsive-more-390.png" + ], + "errors": [] +} \ No newline at end of file diff --git a/docs/sw/home-menu-integration-qa/final-improvement/integrated-improvement-result.json b/docs/sw/home-menu-integration-qa/final-improvement/integrated-improvement-result.json new file mode 100644 index 0000000..a4c6cad --- /dev/null +++ b/docs/sw/home-menu-integration-qa/final-improvement/integrated-improvement-result.json @@ -0,0 +1,1286 @@ +{ + "browser": "155.0.8059.80", + "checks": [ + { + "name": "menu (default): no Open next to the logo", + "status": "PASS" + }, + { + "name": "menu (default): dropdown footer keeps + New and Open", + "status": "PASS" + }, + { + "name": "header: Open takes the file name place right of the logo (same junction offset, 48px)", + "status": "PASS", + "data": { + "home": { + "x": 360, + "y": 851.5, + "w": 80, + "h": 48, + "right": 440 + }, + "openBox": { + "x": 425, + "y": 851.5, + "w": 58.265625, + "h": 48, + "right": 483.265625 + }, + "junction": -15 + } + }, + { + "name": "header: file name follows Open without overlap", + "status": "PASS", + "data": { + "openBox": { + "x": 425, + "y": 851.5, + "w": 58.265625, + "h": 48, + "right": 483.265625 + }, + "docBox": { + "x": 485.265625, + "y": 851.5, + "w": 249.734375, + "h": 48, + "right": 735 + } + } + }, + { + "name": "header: Open uses hm-sw-header__document-button (same surface, color, weight as the file name)", + "status": "PASS", + "data": { + "styles": { + "cls": "hm-sw-header__document-button", + "bg": "linear-gradient(rgb(8, 24, 19) 0%, rgb(9", + "color": "rgb(240, 247, 245)", + "weight": "700", + "padL": "5px", + "borderL": "0px", + "borderR": "1px" + }, + "fileStyles": { + "bg": "linear-gradient(rgb(8, 24, 19) 0%, rgb(9", + "color": "rgb(240, 247, 245)", + "weight": "700", + "padL": "12px" + } + } + }, + { + "name": "D-1: file name after Open starts with 12px padding", + "status": "PASS", + "data": { + "bg": "linear-gradient(rgb(8, 24, 19) 0%, rgb(9", + "color": "rgb(240, 247, 245)", + "weight": "700", + "padL": "12px" + } + }, + { + "name": "header: Open hover turns only the text #FFC600", + "status": "PASS" + }, + { + "name": "header: dropdown footer has + New only (no duplicate Open)", + "status": "PASS" + }, + { + "name": "header: Open calls onDocumentOpen", + "status": "PASS" + }, + { + "name": "documentOpenLabel is the Open text", + "status": "PASS" + }, + { + "name": "responsive 1920: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 2280, + "barRight": 2280, + "overlap": false, + "open": 58.265625, + "doc": 249.734375, + "layer": 234, + "more": false + } + }, + { + "name": "responsive 1920: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1920: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 1800: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 2160, + "barRight": 2160, + "overlap": false, + "open": 58.265625, + "doc": 190, + "layer": 234, + "more": false + } + }, + { + "name": "responsive 1800: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1800: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 1440: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1800, + "barRight": 1800, + "overlap": false, + "open": 58.265625, + "doc": 310, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 1440: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1440: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 1180: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1540, + "barRight": 1540, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 1180: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1180: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 1080: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1440, + "barRight": 1440, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 1080: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 1080: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 900: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 1260, + "barRight": 1260, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 900: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 900: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 768: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 1128, + "barRight": 1128, + "overlap": false, + "open": 58.265625, + "doc": 178.734375, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 768: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 768: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 640: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1000, + "barRight": 1000, + "overlap": false, + "open": 58.265625, + "doc": 96, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 640: Open shown next to the logo", + "status": "PASS", + "data": { + "open": 58.265625, + "inMore": null + } + }, + { + "name": "responsive 640: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 520: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 880, + "barRight": 880, + "overlap": false, + "open": 0, + "doc": 103, + "layer": 234, + "more": true + } + }, + { + "name": "responsive 520: Open moved into More and works there", + "status": "PASS", + "data": { + "open": 0, + "inMore": true + } + }, + { + "name": "responsive 520: layer field keeps its Figma width (234) while Open is shown", + "status": "PASS", + "data": 234 + }, + { + "name": "responsive 390: 49px, no overflow or overlap, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 750, + "barRight": 750, + "overlap": false, + "open": 0, + "doc": 96, + "layer": 113, + "more": true + } + }, + { + "name": "responsive 390: Open moved into More and works there", + "status": "PASS", + "data": { + "open": 0, + "inMore": true + } + }, + { + "name": "resize: collapsed Open returns once when the header grows (measured, with hysteresis)", + "status": "PASS", + "data": [ + [ + 520, + 0 + ], + [ + 560, + 0 + ], + [ + 600, + 1 + ], + [ + 640, + 1 + ], + [ + 680, + 1 + ], + [ + 720, + 1 + ], + [ + 760, + 1 + ], + [ + 820, + 1 + ], + [ + 900, + 1 + ], + [ + 1080, + 1 + ] + ] + }, + { + "name": "hidden documentNew: footer keeps Open on its right side", + "status": "PASS", + "data": { + "footer": { + "x": 424, + "y": 442.5, + "w": 310, + "h": 40, + "right": 734 + }, + "action": { + "x": 675.96875, + "y": 451, + "w": 50.03125, + "h": 24, + "right": 726 + } + } + }, + { + "name": "hidden documentNew + documentOpen: no footer", + "status": "PASS" + }, + { + "name": "hidden layerFreeze / layerLock: field draws the bulb only, two empty 20px places", + "status": "PASS", + "data": [ + { + "empty": false, + "w": 20 + }, + { + "empty": true, + "w": 20 + }, + { + "empty": true, + "w": 20 + } + ] + }, + { + "name": "hidden layerFreeze / layerLock: rows have no snowflake / lock button, places kept", + "status": "PASS", + "data": [ + { + "buttons": [ + "hm-sw-header__layer-toggle--visibility", + "hm-sw-header__layer-item" + ], + "empty": 2, + "name": 931 + }, + { + "buttons": [ + "hm-sw-header__layer-toggle--visibility", + "hm-sw-header__layer-item" + ], + "empty": 2, + "name": 931 + }, + { + "buttons": [ + "hm-sw-header__layer-toggle--visibility", + "hm-sw-header__layer-item" + ], + "empty": 2, + "name": 931 + } + ] + }, + { + "name": "hidden layer states: the bulb still reports its callback", + "status": "PASS" + }, + { + "name": "composed: Open right of the logo at the same junction offset as SWHeader", + "status": "PASS", + "data": { + "cHome": { + "x": 360, + "y": 6118.5, + "w": 80, + "h": 48, + "right": 440 + }, + "cOpen": { + "x": 425, + "y": 6118.5, + "w": 58.265625, + "h": 48, + "right": 483.265625 + } + } + }, + { + "name": "composed: file name after Open has 12px padding", + "status": "PASS" + }, + { + "name": "composed: Open callback", + "status": "PASS" + }, + { + "name": "composed: showOpen={false} removes Open from the dropdown", + "status": "PASS" + }, + { + "name": "composed responsive 1920: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 1800: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 1440: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 1180: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 1080: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 900: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 768: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 768: 파일 열기 reachable in More", + "status": "PASS" + }, + { + "name": "composed responsive 640: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 640: 파일 열기 reachable in More", + "status": "PASS" + }, + { + "name": "composed responsive 520: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 520: 파일 열기 reachable in More", + "status": "PASS" + }, + { + "name": "composed responsive 390: 49px, no overflow, nothing past the edge", + "status": "PASS", + "data": { + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + }, + { + "name": "composed responsive 390: 파일 열기 reachable in More", + "status": "PASS" + }, + { + "name": "tool: Model / Layout is a radio group", + "status": "PASS" + }, + { + "name": "tool: arrow key moves focus and selects Layout", + "status": "PASS" + }, + { + "name": "tool: 2.5D toggle reports and shows pressed (#FFC600)", + "status": "PASS" + }, + { + "name": "tool: text buttons are the 속성Bar toggle look (700, 19px row, 12px caption)", + "status": "PASS", + "data": { + "text": { + "weight": "700", + "h": 19 + }, + "ref": "12px" + } + }, + { + "name": "tool: pill button is 40 x 38 with the yellow ring and hover icon", + "status": "PASS", + "data": { + "pillBox": { + "x": 1467.234375, + "y": 336, + "w": 40, + "h": 38, + "right": 1507.234375 + }, + "hover": { + "ring": "1", + "icon": "hidden", + "hoverIcon": "visible" + } + } + }, + { + "name": "tool: pill click callback", + "status": "PASS" + }, + { + "name": "tool: pill accessible name from its text", + "status": "PASS" + }, + { + "name": "tool: document look keeps the logo junction in a composed bar", + "status": "PASS", + "data": { + "tHome": { + "x": 360, + "y": 491.5, + "w": 80, + "h": 48, + "right": 440 + }, + "tOpen": { + "x": 425, + "y": 491.5, + "w": 58.265625, + "h": 48, + "right": 483.265625 + } + } + }, + { + "name": "popup: SWPopup frames match the property window frame (314, surface, border, radius, title)", + "status": "PASS", + "data": [ + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + }, + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + }, + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + } + ] + }, + { + "name": "popup content: SWHeaderInput rows commit through the host callbacks", + "status": "PASS" + }, + { + "name": "popup content: SWPopupRow is the property row (72px label, 32px row, 28px box)", + "status": "PASS", + "data": { + "label": "72px", + "minH": "32px", + "box": "28px" + } + }, + { + "name": "popup content: HM UI Switch works inside", + "status": "PASS" + }, + { + "name": "popup content: HM UI Select (portal) works inside", + "status": "PASS" + }, + { + "name": "popup content: HM UI Slider works inside", + "status": "PASS", + "data": "Callback: name=평면도-02 → scale=50 → snap=false → unit=cm → opacity=70" + }, + { + "name": "popup content: HM UI Button works inside", + "status": "PASS" + }, + { + "name": "popup dropdown: SW layer dropdown opens right under its field (fixed overlay) and is not clipped", + "status": "PASS", + "data": { + "fieldBox": { + "x": 387, + "y": 548.5, + "w": 234, + "h": 38, + "right": 621 + }, + "menuBox": { + "x": 387, + "y": 586.5, + "w": 234, + "h": 160, + "right": 621 + }, + "hit": true + } + }, + { + "name": "popup dropdown: selection callback, list closes, popup stays", + "status": "PASS" + }, + { + "name": "popup dropdown: SW color dropdown becomes a fixed overlay", + "status": "PASS" + }, + { + "name": "popup dropdown: color chip callback", + "status": "PASS" + }, + { + "name": "popup dropdown: Escape closes the dropdown only", + "status": "PASS" + }, + { + "name": "object popup: read-only rows", + "status": "PASS" + }, + { + "name": "object popup: number row commits", + "status": "PASS" + }, + { + "name": "object popup: compact row uses the 41px hug field", + "status": "PASS", + "data": { + "cls": "hm-sw-header__input hm-sw-property__compact", + "w": 26.53125 + } + }, + { + "name": "property panel + children: children follow the property groups in the same body", + "status": "PASS", + "data": [ + "공통", + "메모" + ] + }, + { + "name": "property panel + children: child content callback", + "status": "PASS" + }, + { + "name": "popup: close button calls onClose", + "status": "PASS" + }, + { + "name": "short popup: dropdown extends past the popup and stays fully visible (no clipping)", + "status": "PASS", + "data": { + "bBox": { + "x": 706, + "y": 264.5, + "w": 314, + "h": 110, + "right": 1020 + }, + "menuB": { + "x": 717, + "y": 363.5, + "w": 234, + "h": 160, + "right": 951 + }, + "bHit": true + } + }, + { + "name": "several popups: Escape in a dropdown closes only the dropdown", + "status": "PASS" + }, + { + "name": "several popups: Escape on an edited input cancels the edit only", + "status": "PASS" + }, + { + "name": "several popups: Escape inside a popup closes that popup only", + "status": "PASS", + "data": "Callback: close(a)" + }, + { + "name": "several popups: Escape outside closes only the top popup (last used: B)", + "status": "PASS", + "data": "Callback: close(a) → close(b)" + }, + { + "name": "several popups: the popup used last becomes the top", + "status": "PASS", + "data": "Callback: close(a) → close(b) → close(a)" + }, + { + "name": "long content: the body scrolls inside the frame and the last row is reachable", + "status": "PASS", + "data": { + "scroll": { + "sh": 1000, + "ch": 328, + "overflow": "auto" + }, + "lastVisible": true + } + }, + { + "name": "no browser console or page errors", + "status": "PASS", + "data": [] + } + ], + "errors": [], + "data": { + "junction": -15, + "responsive": [ + { + "width": 1920, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 2280, + "barRight": 2280, + "overlap": false, + "open": 58.265625, + "doc": 249.734375, + "layer": 234, + "more": false, + "inMore": null + }, + { + "width": 1800, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 2160, + "barRight": 2160, + "overlap": false, + "open": 58.265625, + "doc": 190, + "layer": 234, + "more": false, + "inMore": null + }, + { + "width": 1440, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1800, + "barRight": 1800, + "overlap": false, + "open": 58.265625, + "doc": 310, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 1180, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1540, + "barRight": 1540, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 1080, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1440, + "barRight": 1440, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 900, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 1260, + "barRight": 1260, + "overlap": false, + "open": 58.265625, + "doc": 120, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 768, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 1128, + "barRight": 1128, + "overlap": false, + "open": 58.265625, + "doc": 178.734375, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 640, + "height": 49, + "barOverflow": 0, + "areaOverflow": 1, + "headerRight": 1000, + "barRight": 1000, + "overlap": false, + "open": 58.265625, + "doc": 96, + "layer": 234, + "more": true, + "inMore": null + }, + { + "width": 520, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 880, + "barRight": 880, + "overlap": false, + "open": 0, + "doc": 103, + "layer": 234, + "more": true, + "inMore": true + }, + { + "width": 390, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "headerRight": 750, + "barRight": 750, + "overlap": false, + "open": 0, + "doc": 96, + "layer": 113, + "more": true, + "inMore": true + } + ], + "hysteresis": [ + [ + 520, + 0 + ], + [ + 560, + 0 + ], + [ + 600, + 1 + ], + [ + 640, + 1 + ], + [ + 680, + 1 + ], + [ + 720, + 1 + ], + [ + 760, + 1 + ], + [ + 820, + 1 + ], + [ + 900, + 1 + ], + [ + 1080, + 1 + ] + ], + "composedResponsive": [ + { + "width": 1920, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 1800, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 1440, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 1180, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 1080, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 900, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 1, + "more": 0, + "clipped": false, + "right": 0 + }, + { + "width": 768, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + }, + { + "width": 640, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + }, + { + "width": 520, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + }, + { + "width": 390, + "height": 49, + "barOverflow": 0, + "areaOverflow": 0, + "open": 0, + "more": 1, + "clipped": false, + "right": 0 + } + ], + "popupFrames": [ + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + }, + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + }, + { + "w": 314, + "bg": "rgb(228, 219, 201)", + "border": "rgb(166, 125, 3)", + "radius": "4px", + "shadow": "rgb(0, 0, 0) 0px 0px 0px 1px inset", + "title": "rgb(255, 198, 0)", + "weight": "700", + "titlebar": 30, + "role": "dialog", + "modal": "false" + } + ] + }, + "status": "PASS" +} diff --git a/docs/sw/home-menu-integration-qa/final-visual/capture.json b/docs/sw/home-menu-integration-qa/final-visual/capture.json new file mode 100644 index 0000000..7454952 --- /dev/null +++ b/docs/sw/home-menu-integration-qa/final-visual/capture.json @@ -0,0 +1,34 @@ +{ + "browser": "155.0.8059.80", + "shots": [ + "overview-1920.png", + "detail-buttons-1920.png", + "interactive-states-1920.png", + "window-move-motion-1920.png", + "logo-slot-1920.png", + "document-dropdown-1920.png", + "responsive-more-1920.png", + "k-guide-1920.png", + "property-panel-compare-1920.png", + "composed-controls-1920.png", + "overview-1800.png", + "overview-1440.png", + "overview-1180.png", + "overview-1080.png", + "overview-900.png", + "overview-768.png", + "overview-640.png", + "overview-520.png", + "overview-390.png", + "responsive-more-1800.png", + "responsive-more-1440.png", + "responsive-more-1180.png", + "responsive-more-1080.png", + "responsive-more-900.png", + "responsive-more-768.png", + "responsive-more-640.png", + "responsive-more-520.png", + "responsive-more-390.png" + ], + "errors": [] +} \ No newline at end of file diff --git a/docs/sw/home-menu-integration-qa/home-menu/capture-default-1920.png b/docs/sw/home-menu-integration-qa/home-menu/capture-default-1920.png new file mode 100644 index 0000000..24b9845 Binary files /dev/null and b/docs/sw/home-menu-integration-qa/home-menu/capture-default-1920.png differ diff --git a/docs/sw/home-menu-integration-qa/home-menu/header-composition-open.png b/docs/sw/home-menu-integration-qa/home-menu/header-composition-open.png new file mode 100644 index 0000000..fb9c775 Binary files /dev/null and b/docs/sw/home-menu-integration-qa/home-menu/header-composition-open.png differ diff --git a/docs/sw/home-menu-integration-qa/home-menu/responsive-390.png b/docs/sw/home-menu-integration-qa/home-menu/responsive-390.png new file mode 100644 index 0000000..e9f6f87 Binary files /dev/null and b/docs/sw/home-menu-integration-qa/home-menu/responsive-390.png differ diff --git a/docs/sw/home-menu-integration-qa/home-menu/responsive-768.png b/docs/sw/home-menu-integration-qa/home-menu/responsive-768.png new file mode 100644 index 0000000..912d0a8 Binary files /dev/null and b/docs/sw/home-menu-integration-qa/home-menu/responsive-768.png differ diff --git a/docs/sw/home-menu-integration-qa/home-menu/result.json b/docs/sw/home-menu-integration-qa/home-menu/result.json new file mode 100644 index 0000000..89bc362 --- /dev/null +++ b/docs/sw/home-menu-integration-qa/home-menu/result.json @@ -0,0 +1,363 @@ +{ + "browser": "155.0.8059.80", + "checks": [ + { + "name": "Storybook SW titles: Header, Tool, Popup, Home Menu; no SW/Header/Options", + "status": "PASS", + "data": [ + "SW/Header", + "SW/Home Menu", + "SW/Popup", + "SW/Tool" + ] + }, + { + "name": "menu at Figma x 375, width 1305, height 77 (measured, no fixed x); header stays 49", + "status": "PASS", + "data": { + "left": 375, + "width": 1305, + "height": 77, + "headerHeight": 49, + "sections": [ + 340, + 260, + 180, + 260, + 261 + ], + "tiles": 15, + "tileSizes": [ + "80x46" + ], + "titles": 5, + "badgeInlined": true, + "icons": 30, + "iconsLoaded": 30 + } + }, + { + "name": "5 sections 340/260/180/260/261, 15 tiles 80x46, 5 titles", + "status": "PASS", + "data": { + "left": 375, + "width": 1305, + "height": 77, + "headerHeight": 49, + "sections": [ + 340, + 260, + 180, + 260, + 261 + ], + "tiles": 15, + "tileSizes": [ + "80x46" + ], + "titles": 5, + "badgeInlined": true, + "icons": 30, + "iconsLoaded": 30 + } + }, + { + "name": "title badge SVG and 30 tile icons render", + "status": "PASS", + "data": { + "left": 375, + "width": 1305, + "height": 77, + "headerHeight": 49, + "sections": [ + 340, + 260, + 180, + 260, + 261 + ], + "tiles": 15, + "tileSizes": [ + "80x46" + ], + "titles": 5, + "badgeInlined": true, + "icons": 30, + "iconsLoaded": 30 + } + }, + { + "name": "hidden by default (not in DOM), logo aria-expanded=false", + "status": "PASS" + }, + { + "name": "logo click opens, aria-expanded=true, aria-controls → menu id", + "status": "PASS" + }, + { + "name": "tile Hover: label #FF5C00", + "status": "PASS", + "data": "rgb(255, 92, 0)" + }, + { + "name": "tile click → Active (aria-pressed true), re-click → Default", + "status": "PASS", + "data": "true" + }, + { + "name": "logo re-click closes; onHomeClick never called while homeMenu is set", + "status": "PASS", + "data": "onHomeMenuOpenChange(false, home)\nonItemSelect(working / save, active=false)\nonActiveChange([])\nonItemSelect(working / save, active=true)\nonActiveChange([save])\nonHomeMenuOpenChange(true, home)" + }, + { + "name": "Escape inside closes the menu and returns focus to the logo", + "status": "PASS" + }, + { + "name": "click on the menu background closes it (Figma CLOSE)", + "status": "PASS" + }, + { + "name": "recent panel opens (outline accent); Escape closes the panel, then the menu", + "status": "PASS", + "data": { + "panelOpen": true, + "panelFirst": true + } + }, + { + "name": "opening the document dropdown closes the menu (reason document); New / Open stay in the dropdown", + "status": "PASS", + "data": "C0020101-006-평면및종단면도(06)\nC0020101-007-횡단면도\n+ New\nOpen" + }, + { + "name": "opening the menu closes the document dropdown", + "status": "PASS" + }, + { + "name": "no separate Open button next to the logo (documentOpenPlacement=\"menu\")", + "status": "PASS" + }, + { + "name": "focus outside: Escape closes the most recent (menu), then the popup", + "status": "PASS", + "data": { + "step1": { + "menu": 0, + "popup": 1 + }, + "step2": { + "menu": 0, + "popup": 0 + } + } + }, + { + "name": "popup opened after the menu: Escape outside closes the popup first, menu stays", + "status": "PASS", + "data": { + "menu": 1, + "popup": 0 + } + }, + { + "name": "Escape inside the popup closes only the popup", + "status": "PASS", + "data": { + "menu": 1, + "popup": 0 + } + }, + { + "name": "Escape inside the menu closes only the menu (popup stays), focus → logo", + "status": "PASS", + "data": { + "menu": 0, + "popup": 1, + "focusHome": true + } + }, + { + "name": "every width: menu starts at object area −2px, ends at the window area, covers neither logo nor file name, header 49, no page scroll", + "status": "PASS", + "data": [ + { + "width": 1920, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 1305, + "scrolls": false, + "pageScroll": false + }, + { + "width": 1440, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 825, + "scrolls": true, + "pageScroll": false + }, + { + "width": 1080, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 639, + "scrolls": true, + "pageScroll": false + }, + { + "width": 768, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 288, + "scrolls": true, + "pageScroll": false + }, + { + "width": 520, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 288, + "scrolls": true, + "pageScroll": false + }, + { + "width": 390, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 165, + "scrolls": true, + "pageScroll": false + } + ] + }, + { + "name": "without homeMenu: logo click calls onHomeClick, renders no menu, no aria-expanded", + "status": "PASS", + "data": [ + "[SWHeader Overview] onHomeClick" + ] + }, + { + "name": "no page / console errors", + "status": "PASS", + "data": [] + } + ], + "errors": [], + "data": { + "titles": [ + "SW/Header", + "SW/Home Menu", + "SW/Popup", + "SW/Tool" + ], + "geometry": { + "left": 375, + "width": 1305, + "height": 77, + "headerHeight": 49, + "sections": [ + 340, + 260, + 180, + 260, + 261 + ], + "tiles": 15, + "tileSizes": [ + "80x46" + ], + "titles": 5, + "badgeInlined": true, + "icons": 30, + "iconsLoaded": 30 + }, + "responsive": [ + { + "width": 1920, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 1305, + "scrolls": false, + "pageScroll": false + }, + { + "width": 1440, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 825, + "scrolls": true, + "pageScroll": false + }, + { + "width": 1080, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 639, + "scrolls": true, + "pageScroll": false + }, + { + "width": 768, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 288, + "scrolls": true, + "pageScroll": false + }, + { + "width": 520, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 288, + "scrolls": true, + "pageScroll": false + }, + { + "width": 390, + "headerHeight": 49, + "start": 0, + "endGap": 0, + "coversHome": false, + "coversDocument": false, + "menuWidth": 165, + "scrolls": true, + "pageScroll": false + } + ] + } +} \ No newline at end of file diff --git a/docs/sw/home-menu-integration-qa/visual-diff/compare-result.json b/docs/sw/home-menu-integration-qa/visual-diff/compare-result.json new file mode 100644 index 0000000..0bb1d0e --- /dev/null +++ b/docs/sw/home-menu-integration-qa/visual-diff/compare-result.json @@ -0,0 +1,146 @@ +{ + "total": 28, + "same": 28, + "results": [ + { + "file": "composed-controls-1920.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "detail-buttons-1920.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "document-dropdown-1920.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "interactive-states-1920.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "k-guide-1920.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "logo-slot-1920.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-1080.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-1180.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-1440.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-1800.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-1920.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-390.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-520.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-640.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-768.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "overview-900.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "property-panel-compare-1920.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-1080.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-1180.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-1440.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-1800.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-1920.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-390.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-520.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-640.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-768.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "responsive-more-900.png", + "status": "SAME", + "changed": 0 + }, + { + "file": "window-move-motion-1920.png", + "status": "SAME", + "changed": 0 + } + ] +} \ No newline at end of file diff --git a/docs/sw/sw-header-usage.md b/docs/sw/sw-header-usage.md index 915884b..774d421 100644 --- a/docs/sw/sw-header-usage.md +++ b/docs/sw/sw-header-usage.md @@ -633,3 +633,20 @@ Storybook: `Property Panel · 선택 없음 / 전체 선택 / 원 1개 / 모두 - `undefined`(미지정): Figma 원형 그림(기존 기본). `null` / `false`: 빈 영역 — 크기·치수 라벨 위치·패널 높이 유지. ReactNode: 외부 컴포넌트. 함수: `{ selection, group, properties }`(해당 그룹 항목)를 받아 렌더링 — 객체 유형별 교체에 사용합니다. - 내용은 영역 안에 맞춰지고(overflow hidden) 원주·지름·반지름·면적 입력은 그 위에 그대로 표시됩니다. 값은 `properties`로만 전달되며 형상 Slot과 독립입니다. 입력은 기존 SWHeaderInput 검증·Enter/Blur 확정·Escape 취소를 사용합니다. - 패키지는 CAD 계산·형상 렌더링 엔진을 포함하지 않습니다. Storybook의 사각형/점선 원 SVG는 Story 전용 샘플입니다. + +## 로고 클릭 메뉴 — `homeMenu` (2026-10-19) + +Figma 26:10591의 2단 Header 메뉴입니다. 기본 숨김, 로고 클릭으로 열고 다시 클릭하면 닫힙니다. + +```tsx + {}} /> // reason: home | escape | background | outside | select | file | document +``` + +- `sections`: `{ id, title, items: [{ id, label, icon, hoverIcon?, toggle?, panel?, disabled?, hidden?, onSelect? }] }[]`. 최근파일은 타일의 `panel: `(선택). +- 이벤트 우선순위: `homeMenu`가 있으면 로고 클릭은 메뉴 토글만 하고 `onHomeClick`은 호출되지 않습니다. `homeMenu`가 없으면 기존과 같이 `onHomeClick`만 호출됩니다. +- New / Open은 문서 Dropdown 안 그대로입니다(`documentOpenPlacement` 기본 `"menu"`). 메뉴가 열리면 필드·문서 Dropdown이 닫히고, 문서 Dropdown을 열면 메뉴가 닫힙니다. +- 위치: 객체 수정 영역 −2px ~ 창 영역(DOM 측정, 1920에서 x 375 · 폭 1305). 좁은 폭에서는 메뉴 안 가로 스크롤. +- Escape: 하위 패널 → 메뉴 순서, 닫히면 로고로 Focus. 속성창·`SWPopup`과 같은 Escape 순서(Focus가 모두 밖이면 최근 사용 하나). +- 상세: 패키지 [카탈로그 5절](../../packages/sw/ai/COMPONENT_CATALOG.md), [통합 검증 기록](./sw-home-menu-integration-2026-10-19.md). diff --git a/docs/sw/sw-home-menu-integration-2026-10-19.md b/docs/sw/sw-home-menu-integration-2026-10-19.md new file mode 100644 index 0000000..936a10d --- /dev/null +++ b/docs/sw/sw-home-menu-integration-2026-10-19.md @@ -0,0 +1,38 @@ +# SW Task A + B 통합 — 로고 클릭 메뉴 (2026-10-19) + +> 통합 Worktree `HMDesign-SW-Integration`, 브랜치 `integration/sw-home-menu`(기준 Task A `e8409ef`) + Task B `396539d`의 신규 파일 84개. 미커밋. + +## 결합 방식 + +- Task B 파일 84개는 모두 신규이며 Task A 파일과 겹치지 않아 Git 충돌이 없습니다(`5faa1f1` → `e8409ef` 조상 관계). +- 공유 파일 수정은 연결에 필요한 최소 범위입니다. + +| 파일 | 변경 | +|---|---| +| `packages/sw/src/index.ts` | `export * from "./components/sw-home-menu"` | +| `sw-header/types.ts`, `index.ts` | `homeMenu`, `homeMenuOpen`, `defaultHomeMenuOpen`, `onHomeMenuOpenChange`, 타입 `SWHeaderHomeMenu`, `SWHeaderHomeMenuChangeReason` | +| `sw-header/SWHeader.tsx` | `homeMenu`가 있을 때만: 로고 클릭 = 메뉴 토글(`onHomeClick` 미호출), DOM 측정 배치(객체 수정 영역 −2px ~ 창 영역), 메뉴와 문서·필드 Dropdown 상호 닫기. `homeMenu`가 없으면 기존 동작 그대로 | +| `components/SWHeaderHome.tsx` | `aria-expanded`·`aria-controls` 전달(시각 변화 없음) | +| `components/SWHeaderPropertyPanel.tsx` | 내부 `useSWPopupStack`에 `export`만 추가(패키지 공개 아님) | +| `theme/sw.css` | `.hm-sw-header__home-menu` 레이어 1줄(top 0, z-index 20) | +| `sw-home-menu/SWHomeMenu.tsx`(Task B) | 열린 동안 Popup Escape 스택에 합류(하위 패널 → 메뉴 순서는 그대로) | +| Story·Fixture(Task B) | `@hm-design/sw` 공식 Import, `SWHeader homeMenu` 사용, `통합 · Popup · Escape` Story 추가 | +| AI 문서·README·사용 가이드 | 카탈로그 5절, 조합 패턴 7절, 디자인 규칙 11절, 예제 G | + +## 검증 (실행 결과) + +| 항목 | 결과 | +|---|---| +| SW typecheck / 앱 `tsc -b` / SW build / Storybook build | PASS / PASS / PASS / PASS | +| `dist/styles.css` 홈 메뉴 스타일, `dist/index.d.ts` 공개 Export | 포함 / 포함 | +| AI 문서·공개 API 정합성(`ai-docs.check.mjs`) | 22/22 PASS | +| 기존 SW Story 28장 픽셀 비교(통합 전 기준선 대비, threshold 0) | 28/28 동일 | +| 기존 Header·Tool·Popup 기능 회귀(`integrated-improvement.browser.mjs`) | 96 PASS / 0 FAIL(통합 전과 동일) | +| 홈 메뉴 통합(`home-menu-integration.browser.mjs`) | 22/22 PASS | + +## 남은 항목 + +- 미실행: 독립 React 19 Tarball 소비 설치, SW + HM UI CSS 동시 사용, Figma 스크린샷 픽셀 비교(배치·크기는 DOM 실측으로만 확인), Firefox·Safari. +- 좁은 폭: 메뉴는 접히지 않고 메뉴 안에서 가로 스크롤(1440 825px, 1080 639px, 768·520 288px, 390 165px). 좁은 폭 디자인은 없습니다. +- 디자인 승인 대기(미적용): 속성Bar 차이 3건, 열림 상태 Header 상·하단 `#5F4B0F` 선, 폰트 차이. +- Task A의 기존 제한(넓은 `tools` + 390px에서 파일명·레이어 폭 0)은 이번 통합 범위 밖입니다. diff --git a/packages/sw/README.md b/packages/sw/README.md index bf32d93..5484277 100644 --- a/packages/sw/README.md +++ b/packages/sw/README.md @@ -2,6 +2,8 @@ > **0.2.0 통합 개선(2026-10-19):** 패키지에 포함된 AI 안내 [ai/AI_USAGE.md](./ai/AI_USAGE.md)·[카탈로그](./ai/COMPONENT_CATALOG.md)가 공개 API의 기준입니다. 추가: `SWPopup` / `SWPopupSection` / `SWPopupRow`(속성창 외형 + `children` 자유 콘텐츠, 여러 Popup Escape 스택, Popup 안 Dropdown 고정 Overlay), `SWHeaderPropertyPanel`의 `children`·`title`·`autoFocus`와 선택형 `selection`/`properties`/`groups`, `SWToolButton` / `SWToolGroup`(기존 글자 토글·pill·파일명 면 재사용), `SWHeader`의 `documentOpenPlacement`(로고 오른쪽 Open, 실측 기반 More 이동)·`leading`, `hiddenControls`의 `documentNew`·`documentOpen`·`layerFreeze`·`layerLock`, `showNew`/`showOpen`/`showFreeze`/`showLock`. 모두 선택 Props이며 기존 화면은 그대로입니다. 빌드는 줄바꿈 정규화로 재현 가능하고 `dist/build-info.json`에 출처 Commit·도구 버전을 남깁니다. 검증 기록: 저장소 `docs/sw/sw-integrated-improvement-2026-10-19.md`. +> **로고 클릭 메뉴(2026-10-19):** `SWHomeMenu` / `SWHomeMenuSection` / `SWHomeMenuItem` / `SWRecentFiles`(Figma 26:10591 2단 Header 메뉴, 최근파일 하위 패널)와 `SWHeader`의 `homeMenu`·`homeMenuOpen`·`defaultHomeMenuOpen`·`onHomeMenuOpenChange`. 기본 숨김 → 로고 클릭 열기 → 다시 클릭 닫기, 위치는 Header DOM 측정. `homeMenu`가 있으면 로고 클릭은 `onHomeClick`을 호출하지 않습니다. 모두 선택 Props이며 주지 않으면 기존 화면 그대로입니다([카탈로그 5절](./ai/COMPONENT_CATALOG.md), [예제 G](./ai/examples/ExampleG-HomeMenu.tsx)). 검증 기록: 저장소 `docs/sw/sw-home-menu-integration-2026-10-19.md`. + > **현재 기준 색인(2026-10-19 1차 정리):** 아래 날짜별 안내는 작성 당시 기록입니다. 현재 기준은 [사용 가이드](../../docs/sw/sw-header-usage.md), [디자인 명세](../../docs/sw/sw-header-design-spec.md) 상단 색인, 속성 필드 13개 [검증 기록](../../docs/sw/sw-property-fields-verification-2026-10-19.md), 게이지·속성Bar [후속 4건 기록](../../docs/sw/sw-header-followup-four-design-fixes-verification-2026-10-19.md)·[Firefox 게이지 기록](../../docs/sw/sw-header-firefox-range-close-verification-2026-10-19.md)입니다. Storybook은 `SW / Header` 33개 Story이며 전체 목록과 2차 검토 대상은 [1차 정리 보고](../../docs/sw/sw-first-pass-cleanup-review-2026-10-19.md)에 있습니다. > **3차 구현(2026-10-16):** `hiddenControls`(로고 외 모든 구성요소 개별 제거, 로고 필수), `menuOrder`(레이어·색상·선/문자/치수스타일 순서), 속성창 `shapeContent` Slot, 속성창 4상태 개별·비교 Story를 추가했습니다. 미지정 시 기존 디자인과 같습니다. [사용법](../../docs/sw/sw-header-usage.md#구성요소-표시메뉴-순서속성창-4상태형상-slot-2026-10-16-3차), [3차 검증 기록](../../docs/sw/sw-header-composition-shape-verification-2026-10-16.md). diff --git a/packages/sw/ai/AI_USAGE.md b/packages/sw/ai/AI_USAGE.md index ab53b6f..c561025 100644 --- a/packages/sw/ai/AI_USAGE.md +++ b/packages/sw/ai/AI_USAGE.md @@ -12,7 +12,7 @@ ## 1. 작업 순서 (필수) 1. **먼저 검색한다.** 요청 기능을 [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md)의 공개 컴포넌트에서 찾는다. -2. **있으면 재사용한다(1순위).** 완성형 Header가 필요하면 `SWHeader` 하나만 쓴다. 기존 Props(`hiddenControls`, `menuOrder`, `controls`, `documentOpenPlacement`, `leading`, `tools`, 각 데이터 배열·Callback)로 해결되는지 먼저 본다. 팝업·설정창은 `SWPopup`, 버튼·탭·토글은 `SWToolButton` / `SWToolGroup`이다. +2. **있으면 재사용한다(1순위).** 완성형 Header가 필요하면 `SWHeader` 하나만 쓴다. 기존 Props(`hiddenControls`, `menuOrder`, `controls`, `documentOpenPlacement`, `leading`, `tools`, 각 데이터 배열·Callback)로 해결되는지 먼저 본다. 팝업·설정창은 `SWPopup`, 버튼·탭·토글은 `SWToolButton` / `SWToolGroup`, 로고 클릭 2단 메뉴는 `SWHeader`의 `homeMenu`(`SWHomeMenu` 데이터, 최근파일은 `SWRecentFiles`)이다. `SWToolButton`과 `SWHomeMenuItem`은 역할이 다르다(도구 막대 버튼 / 메뉴 타일). 서로 대신 쓰지 않는다. - Popup 요청은 이렇게 판단한다: ① `SWPopup`이 프레임(외형·닫기·Escape·스크롤)을 제공한다 → ② 내용은 `children`으로 자유 구성(구조 제한 없음) → ③ 행 모양은 `SWPopupSection` / `SWPopupRow` + `SWHeaderInput` → ④ SW에 없는 컨트롤은 `@hm-design/ui`를 children에 → ⑤ 프레임을 다시 만들거나 없는 Props(`content`, `renderContent` 등)를 추측하지 않는다. 3. **없으면 조합한다(2순위).** 개별 공개 컴포넌트(`SWHeaderHome`, `SWHeaderDocumentTitle`, `SWHeaderLayerMenu` …)를 `` 안에 배치한다. 배치용 CSS만 소비 프로젝트에 작성한다([COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md)). 4. **조합으로도 안 될 때만 새로 만든다(3순위).** 새 컴포넌트는 **소비 프로젝트**에 작성하고, 공개 컴포넌트(예: `SWHeaderInput`)를 내부에 재사용하며, 외형은 공개 토큰 `--hm-sw-header-*`만 읽는다([DESIGN_RULES.md](./DESIGN_RULES.md), 예제 D). diff --git a/packages/sw/ai/COMPONENT_CATALOG.md b/packages/sw/ai/COMPONENT_CATALOG.md index d9000b2..ba28efe 100644 --- a/packages/sw/ai/COMPONENT_CATALOG.md +++ b/packages/sw/ai/COMPONENT_CATALOG.md @@ -7,7 +7,7 @@ - **열림 상태:** Dropdown은 `open` / `onOpenChange`(Controlled)를 주지 않으면 내부 상태로 동작합니다. - **공통 키보드:** Click·Enter·Space 열기/닫기(Hover로는 열리지 않음), ArrowDown 열고 첫(현재) 항목 Focus, Arrow 이동, Escape·바깥 클릭·Tab 이탈 닫기(Escape는 Trigger로 Focus 복귀). - **검증 표시:** ✅ = 2026-10-19 독립 소비 프로젝트(Tarball 설치)에서 브라우저 검증, ◻ = 소비 검증 범위 밖(Storybook 검증 기록만 있음). -- **무엇을 쓸까:** Header 전체 → `SWHeader`(1). Header 부품 조합 → 개별 컨트롤(2). 팝업·설정창·정보창 → `SWPopup`(3). 도구 막대·탭·토글 → `SWToolButton` / `SWToolGroup`(4). Slider·Switch·Badge·Progress·Dialog(모달)·Label·FilterBar는 SW에 없습니다 → `@hm-design/ui`를 그대로 씁니다(필요하면 `SWPopup` children 안에). +- **무엇을 쓸까:** Header 전체 → `SWHeader`(1). Header 부품 조합 → 개별 컨트롤(2). 팝업·설정창·정보창 → `SWPopup`(3). 도구 막대·탭·토글 → `SWToolButton` / `SWToolGroup`(4). 로고 클릭 2단 메뉴·최근파일 → `SWHeader homeMenu` / `SWHomeMenu`(5). Slider·Switch·Badge·Progress·Dialog(모달)·Label·FilterBar는 SW에 없습니다 → `@hm-design/ui`를 그대로 씁니다(필요하면 `SWPopup` children 안에). ## 1. 완성형 템플릿 @@ -19,7 +19,8 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체 | 필수 | `documentTitle: string` | | 구성 | `hiddenControls?: SWHeaderHideableControlId[]`(로고 외 부품 제거, `home` 제거 시 정적 로고. 하위 항목 `documentNew`·`documentOpen`·`layerFreeze`·`layerLock`도 제거 — 레이아웃 변화 없음), `menuOrder?: SWHeaderMenuId[]`(5개 메뉴끼리 순서), `controls?: SWHeaderControlConfig[]`(객체 영역 전체 allowlist·순서·label) | | 로고 오른쪽 | `documentOpenPlacement?: "menu" \| "header"`(기본 `menu` = 기존 Dropdown 하단. `header` = 로고 바로 오른쪽에 파일명과 같은 면의 Open 버튼, Dropdown의 Open은 숨김. 둘 다 `onDocumentOpen`·`documentOpenLabel`), `leading?: ReactNode`(Open 뒤·파일명 앞 추가 부품, 예: `SWToolButton appearance="document"`) | -| 로고 | `logo?: ReactNode`, `logoSrc?: string`, `logoAlt?: string`, `onHomeClick` | +| 로고 | `logo?: ReactNode`, `logoSrc?: string`, `logoAlt?: string`, `onHomeClick`(`homeMenu`가 없을 때만 호출) | +| 로고 클릭 메뉴 | `homeMenu?: SWHeaderHomeMenu`(= `SWHomeMenu` Props에서 열림 상태 제외, 5절), `homeMenuOpen`, `defaultHomeMenuOpen`(기본 false), `onHomeMenuOpenChange(open, reason: SWHeaderHomeMenuChangeReason)` | | 파일 | `documents`, `activeDocumentId`, `documentMenuOpen`, `onDocumentMenuOpenChange`, `onDocumentSelect`, `onDocumentClose`, `onDocumentNew`, `onDocumentOpen`, `onDocumentClick`, `documentIcon`, `documentNewLabel`, `documentOpenLabel` | | 레이어 | `layer`, `layers`, `activeLayerId`, `layerMenuOpen`, `onLayerMenuOpenChange`, `onLayerSelect`, `onLayerVisibilityChange`, `onLayerFreezeChange`, `onLayerLockChange`, `onLayerDetail`, `onLayerOpen` | | 색상 | `color`, `colors`, `userColors`, `colorMode`, `colorMenuOpen`, `onColorMenuOpenChange`, `onColorSelect`, `onColorModeChange`, `onColorInput`, `onColorPick`, `onColorDetail`, `onColorOpen` | @@ -32,7 +33,6 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체 - 데이터 배열(`layers`, `colors`, `lineTypes`/`lineWeights`, `textStyles`, `dimensionStyles`, `documents`)을 주지 않으면 해당 필드는 Dropdown 없는 버튼입니다. - 반응형(More·대표 필드 전환·창 영역 축소)은 **`SWHeader` 안에서만** 동작합니다(CSS container `hm-sw-header`). 조합형 구성이 반응형이어야 하면 `controls` + `documentOpenPlacement` + `leading` + `tools`로 `SWHeader` 안에서 구성합니다(예제 F). - 로고 옆 Open은 실제 공간을 측정해 넘칠 때만 More로 이동하고, 그 시점보다 Open 폭만큼 넓어지면 돌아옵니다. Open이 보이는 동안 파일명이 먼저 줄고 레이어 필드는 Figma 폭을 유지합니다. - ```tsx setId(l.id)} onClose={closeWindow} /> ``` @@ -86,15 +86,32 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체 - 탭(Model / Layout)은 별도 컴포넌트 없이 `SWToolGroup selection="single"` + `SWToolButton pressed`입니다. - 상태 표시(로드 상태 등) 전용 SW 디자인은 아직 없습니다(상태 표시 컴포넌트는 디자인 확정 후 검토). -## 5. 공개 타입 +## 5. 로고 클릭 메뉴 — Figma 2단 Header 메뉴 -`SWHeaderProps`, 각 컴포넌트 `*Props`, `SWThemeProps`, `SWPopupProps`, `SWPopupSectionProps`, `SWPopupRowProps`, `SWToolButtonProps`, `SWToolButtonAppearance`, `SWToolGroupProps`, `SWHeaderMenuId`, `SWHeaderControlId`, `SWHeaderControlConfig`, `SWHeaderFrameControlId`, `SWHeaderSubControlId`, `SWHeaderHideableControlId`, `SWHeaderDocument`, `SWHeaderLayer`, `SWHeaderLayerValue`, `SWHeaderColorValue`, `SWHeaderColorOption`, `SWHeaderColorMode`, `SWHeaderColorInputValue`, `SWHeaderLineStyleValue`, `SWHeaderLineType`, `SWHeaderLineWeight`, `SWHeaderTextStyleValue`, `SWHeaderTextStyleOption`, `SWHeaderDimensionStyleValue`, `SWHeaderDimensionStyleOption`, `SWHeaderInputValueType`, `SWHeaderProperty`, `SWHeaderPropertyType`, `SWHeaderPropertyPicker`, `SWHeaderPropertyMenus`, `SWHeaderPropertyGroup`, `SWHeaderPropertySelection`, `SWHeaderPropertyShapeContent`, `SWHeaderPropertyShapeContext`. +Figma 26:10591(home on), 타일 15종 26:11355 … 26:11631, 섹션 제목 26:12604 … 26:12622, 최근파일 상태 662:67267 · 목록 662:67331 · 2뎁스 미리보기 662:67471을 **공통 컴포넌트 4개**로 표현합니다. 메뉴 항목·아이콘·제목·Active·이벤트·최근파일은 모두 데이터입니다(패키지에는 항목·아이콘이 없음). 운영체제 파일 선택창이 아니며, "찾아열기" 등의 실제 동작은 호스트가 `onSelect`에서 수행합니다. -## 6. 공개하지 않은 것 (사용 금지) +| 컴포넌트 | 용도 | 주요 Props | 크기·동작 | 검증 | +|---|---|---|---|---| +| `SWHomeMenu` | 2단 메뉴 전체(섹션 여러 개). 보통 `SWHeader homeMenu`로 씀 | `sections`(`SWHomeMenuSectionData[]`) 또는 `children`, `open`/`defaultOpen`(false)/`onOpenChange(open, reason)`, `activeIds`/`defaultActiveIds`/`onActiveChange`, `selection`(`multiple` 기본 / `single`), `onItemSelect`, `closeOnBackgroundClick`(true), `closeOnSelect`(false), `closeOnEscape`(true), `closeOnOutsideClick`(false), `autoFocus`(true), `returnFocusRef`, `accent`(`auto`/`sections`/`outline`) | 높이 77(섹션 75 + 강조 2), 섹션 폭은 타일 수로 자동. 좁으면 메뉴 안 가로 스크롤 | ✅ | +| `SWHomeMenuSection` | 섹션(제목 badge + 타일 그룹) | `title`, `children` | badge 폭 = 글자 + 46px | ✅ | +| `SWHomeMenuItem` | 80 × 46 타일(Default / Hover / Active) | 데이터 `SWHomeMenuItemData`: `id`, `label`, `icon`, `hoverIcon`, `toggle`(true = 켜고 끄기, false = 실행만), `panel`, `panelLabel`, `disabled`, `hidden`, `title`, `onSelect`. 메뉴 밖 단독이면 `active`/`defaultActive`/`onActiveChange` | Hover·Active = 주황 gradient 1px 테두리 + `#FF5C00` 아이콘·글자 | ✅ | +| `SWRecentFiles` | 선택 하위 패널: 최근파일 목록 + 2뎁스 미리보기 | `files`(`SWFileEntry[]`: `id`, `name`, `path`, `badge`, `preview`, `previewTitle`, `disabled`), `onFileOpen`, `onFileHover`, `renderPreview`, `closeOnOpen`(true), `emptyText` | 폭 354, 행 50, 미리보기 310 × 270 | ✅ | -`SWHeaderMore`, `SWHeaderField`/`SWHeaderFieldText`, `SWHeaderKGuide`, `useSWHeaderDropdown`, `SWHeaderMenuDetail`, `SWHeaderColorChip`, `SWHeaderLayerRows`, `SWHeaderColorPanelBody`, `SWHeaderLineStyleColumns`, `SWPopupFrame`, `useSWFixedDropdownPosition`, 속성창 내부 Picker, `useSWHeaderOverlay` 등은 내부 구현입니다. 필요하면 공개 컴포넌트로 대체하거나, 부족한 API로 보고합니다. +- **Header 연결:** `SWHeader`의 `homeMenu`에 `SWHomeMenu` Props를 주면 됩니다. 기본 숨김, 로고 클릭 열기, 다시 클릭 닫기. 위치는 SWHeader가 실제 DOM으로 정합니다(객체 수정 영역 −2px ~ 창 영역, 고정 x 없음). 로고·파일명·창 영역은 가리지 않습니다. +- **이벤트 우선순위:** `homeMenu`가 있으면 로고 클릭은 메뉴 열기·닫기만 하고 `onHomeClick`을 호출하지 않습니다(의도하지 않은 홈 이동 방지). 상태 알림은 `onHomeMenuOpenChange`(`reason`: `home`, `escape`, `background`, `outside`, `select`, `file`, `document`). +- **New / Open:** 기존 문서 Dropdown 안(`documentOpenPlacement="menu"` 기본) 그대로입니다. 메뉴의 "찾아열기"·"새파일"에 같은 동작이 필요하면 호스트가 `onDocumentOpen`·`onDocumentNew`와 같은 함수를 `onSelect`에 연결합니다. 메뉴가 열리면 필드 Dropdown·문서 Dropdown이 닫히고, 문서 Dropdown을 열면 메뉴가 닫힙니다(`reason: "document"`). +- **Escape·Focus:** 메뉴 안의 Escape는 열린 하위 패널 → 메뉴 순서로 하나씩 닫고, 메뉴가 닫히면 로고로 Focus를 돌립니다. 메뉴는 Popup Escape 순서(3절)에 함께 들어가므로 Focus가 메뉴·Popup 밖이면 가장 최근에 열거나 사용한 하나만 닫힙니다. Popup 안의 Escape는 그 Popup만 닫습니다. +- **하위 패널 닫기:** 패널 안 콘텐츠는 `useSWHomeMenuClose()`로 메뉴 전체를 닫을 수 있습니다(`SWRecentFiles`는 파일을 열면 자동). -## 7. 알려진 단독 사용 제약 +## 6. 공개 타입 + +`SWHeaderProps`, 각 컴포넌트 `*Props`, `SWThemeProps`, `SWPopupProps`, `SWPopupSectionProps`, `SWPopupRowProps`, `SWToolButtonProps`, `SWToolButtonAppearance`, `SWToolGroupProps`, `SWHeaderMenuId`, `SWHeaderControlId`, `SWHeaderControlConfig`, `SWHeaderFrameControlId`, `SWHeaderSubControlId`, `SWHeaderHideableControlId`, `SWHeaderDocument`, `SWHeaderLayer`, `SWHeaderLayerValue`, `SWHeaderColorValue`, `SWHeaderColorOption`, `SWHeaderColorMode`, `SWHeaderColorInputValue`, `SWHeaderLineStyleValue`, `SWHeaderLineType`, `SWHeaderLineWeight`, `SWHeaderTextStyleValue`, `SWHeaderTextStyleOption`, `SWHeaderDimensionStyleValue`, `SWHeaderDimensionStyleOption`, `SWHeaderInputValueType`, `SWHeaderProperty`, `SWHeaderPropertyType`, `SWHeaderPropertyPicker`, `SWHeaderPropertyMenus`, `SWHeaderPropertyGroup`, `SWHeaderPropertySelection`, `SWHeaderPropertyShapeContent`, `SWHeaderPropertyShapeContext`, `SWHeaderHomeMenu`, `SWHeaderHomeMenuChangeReason`, `SWHomeMenuProps`, `SWHomeMenuSectionProps`, `SWHomeMenuItemProps`, `SWHomeMenuItemData`, `SWHomeMenuSectionData`, `SWHomeMenuSelectEvent`, `SWHomeMenuCloseReason`, `SWHomeMenuAccent`, `SWRecentFilesProps`, `SWFileEntry`. + +## 7. 공개하지 않은 것 (사용 금지) + +`useSWPopupStack`, `SWHeaderMore`, `SWHeaderField`/`SWHeaderFieldText`, `SWHeaderKGuide`, `useSWHeaderDropdown`, `SWHeaderMenuDetail`, `SWHeaderColorChip`, `SWHeaderLayerRows`, `SWHeaderColorPanelBody`, `SWHeaderLineStyleColumns`, `SWPopupFrame`, `useSWFixedDropdownPosition`, 속성창 내부 Picker, `useSWHeaderOverlay` 등은 내부 구현입니다. 필요하면 공개 컴포넌트로 대체하거나, 부족한 API로 보고합니다. + +## 8. 알려진 단독 사용 제약 | 항목 | 내용 | 대응 | |---|---|---| diff --git a/packages/sw/ai/COMPOSITION_PATTERNS.md b/packages/sw/ai/COMPOSITION_PATTERNS.md index 5d02109..130db8a 100644 --- a/packages/sw/ai/COMPOSITION_PATTERNS.md +++ b/packages/sw/ai/COMPOSITION_PATTERNS.md @@ -12,6 +12,7 @@ | 패키지에 없는 기능 | 4. 확장(소비 프로젝트 신규 컴포넌트) | | SW Popup 디자인의 설정창·정보창·사용자 정의 폼 | 5. Popup(외형 고정, 콘텐츠 자유) | | 로고 옆 파일 열기, 뷰어 도구 막대(Model/Layout, Fit, 2.5D) | 6. 파일 열기 + 도구 막대 | +| 로고를 누르면 열리는 2단 메뉴(저장·내보내기·찾아열기·최근파일 등) | 7. 로고 클릭 메뉴 | ## 1. 완성형 SW Header — [ExampleA-DefaultHeader.tsx](./examples/ExampleA-DefaultHeader.tsx) @@ -116,7 +117,30 @@ - 지원하지 않는 기능은 `hiddenControls`로 뺀다(`documentNew`, `documentOpen`, `layerFreeze`, `layerLock`). 가짜 버튼을 두지 않는다. - `SWHeader` 없이 같은 순서가 필요하면: `SWHeaderHome` → `SWToolButton appearance="document"` → `SWHeaderDocumentTitle showOpen={false}`(사선 접점·12px 여백은 패키지가 처리). 이 경우 More는 없다. -## 7. 하지 말 것 +## 7. 로고 클릭 메뉴 — [ExampleG-HomeMenu.tsx](./examples/ExampleG-HomeMenu.tsx) + +```tsx +const sections: SWHomeMenuSectionData[] = [ + { id: "working", title: "작업중인 파일", items: [ + { id: "save", label: "저장", icon: , toggle: false, onSelect: save }, + ] }, + { id: "open", title: "다른 파일 열기", items: [ + { id: "find-open", label: "찾아열기", icon: , toggle: false, onSelect: openFile }, + { id: "recent", label: "최근파일", icon: , panel: }, + ] }, +] + log(open, reason)} /> +``` + +1. 카탈로그 5절의 데이터로 메뉴를 쓴다. 화면마다 타일·섹션·제목을 따로 만들지 않는다. +2. 메뉴 데이터 변경: 항목 추가·삭제는 `items` 배열, 숨김은 `hidden`, 비활성은 `disabled`, 실행만 하는 버튼은 `toggle: false`, 켜고 끄는 버튼은 기본값. Active를 직접 제어하려면 `homeMenu.activeIds` + `onActiveChange`. +3. 최근파일 패널은 선택 기능이다. 필요한 타일에만 `panel: `를 준다. 파일을 열면 메뉴가 닫힌다(`closeOnOpen`). +4. 로고 클릭은 메뉴만 연다. `onHomeClick`의 홈 이동은 `homeMenu`가 있으면 실행되지 않는다. 메뉴 항목에 홈 이동이 필요하면 항목 `onSelect`에서 한다. +5. New / Open은 문서 Dropdown에 그대로 둔다(`documentOpenPlacement` 기본 `"menu"`). 로고 오른쪽에 별도 Open 버튼을 두지 않는다. 메뉴의 찾아열기·새파일은 같은 handler를 연결한다. +6. 위치·폭은 `SWHeader`가 정한다. 소비 코드에서 메뉴 위치를 계산하거나 겹치는 wrapper를 만들지 않는다. + +## 8. 하지 말 것 - `hm-sw-*` class를 소비 코드에 쓰거나 CSS로 덮어쓰기 - `dist` 구현·`styles.css` 블록 복사로 같은 컴포넌트 재작성 diff --git a/packages/sw/ai/DESIGN_RULES.md b/packages/sw/ai/DESIGN_RULES.md index b4a00df..083640e 100644 --- a/packages/sw/ai/DESIGN_RULES.md +++ b/packages/sw/ai/DESIGN_RULES.md @@ -82,6 +82,8 @@ Container `hm-sw-header` 기준 Breakpoint: **< 1800** 선/문자/치수스타일 → More, **< 1180** 그리기모드 숨김, **< 1080** 상세보기 → More·창 이동 24px(창 영역 176), **< 900** 색상 → More, **< 640** 인쇄·K → More·창 이동 숨김(창 영역 64), **< 520** 레이어 상태 아이콘 숨김. More에서 고른 필드는 첫 칸(레이어 자리)에 표시되고 Dropdown이 바로 열립니다(UI 상태만, 데이터 Callback 없음). +로고 클릭 메뉴는 좁은 폭에서 접히지 않고 메뉴 안에서 가로 스크롤합니다(좁은 폭 전용 디자인 없음, §11). + 조합 Header(개별 컴포넌트)에는 이 규칙이 적용되지 않습니다. 필요한 컨트롤만 배치하고 폭 정책은 소비 앱이 정합니다. ## 8. Popup (속성창 프레임, `SWPopup` / `SWHeaderPropertyPanel`) @@ -120,3 +122,20 @@ Container `hm-sw-header` 기준 Breakpoint: **< 1800** 선/문자/치수스타 6. `hm-sw-*` class를 새로 쓰거나 덮어쓰지 않는다(자기 접두사 사용, 예: `app-`). 7. 버튼·탭·토글이면 먼저 `SWToolButton`의 세 모양(§9)으로 해결되는지 본다. 팝업·설정창이면 `SWPopup` + `SWPopupSection` / `SWPopupRow`(§8). 8. 팝업 안의 새 콘텐츠는 §8의 Popup 토큰(`--hm-sw-property-*`)만 읽는다. Header용 어두운 토큰을 밝은 Popup 면에 섞지 않는다. + +## 11. 로고 클릭 메뉴 (`SWHeader homeMenu` / `SWHomeMenu`, Figma 26:10591) + +| 항목 | 값 | 토큰 | +|---|---|---| +| 위치 | Header 맨 위(top 0), 객체 수정 영역 왼쪽 −2px ~ 창 영역 왼쪽(1920에서 x 375, 폭 1305). DOM 측정이며 고정 좌표 없음. 로고·파일명·창 영역은 가리지 않음. `z-index` 20(속성Bar 위, Dropdown 30·속성창 40 아래) | — | +| 크기 | 메뉴 높이 77(섹션 75 + 하단 강조 2), 그룹 46, 타일 80 × 46, 아이콘 18 슬롯, 섹션 폭은 타일 수로 자동 | — | +| 섹션 면 | 세로 gradient 면 + 1px `#081813` + 안쪽 1px `#20190E`, 아래 모서리 3px | `--hm-sw-home-menu-section-*` | +| 섹션 제목 | Figma tit_* badge(9-slice), 글자 400 검정 | `--hm-sw-home-menu-title-color` | +| 타일 상태 | Default 아이콘 `#B6B4AF` · Hover·Active 주황 gradient 1px 테두리 + 아이콘·글자 `#FF5C00`, 1px 위·왼쪽 이동. 라벨 700 | `--hm-sw-home-menu-item-*`, `--hm-sw-home-menu-icon-color` | +| 하단 강조 | `sections` = 섹션마다 주황 띠, `outline` = 메뉴 전체 2px 외곽선(하위 패널이 열린 동안, `auto` 기본) | `--hm-sw-home-menu-accent-*` | +| 최근파일 패널 | 폭 354, 행 50, 배경 `#27241D`, 주황 테두리, 행 Hover `#62533D` + `#FF5C00`, 미리보기 310 × 270 | `--hm-sw-recent-files-*` | +| 동작 | 기본 숨김 → 로고 클릭 열기 → 다시 클릭 닫기. 메뉴 빈 곳 클릭 닫기(Figma CLOSE). Escape: 하위 패널 → 메뉴, 닫히면 로고로 Focus. 메뉴 밖 클릭은 닫지 않음(Figma overlay 기준) | — | +| 좁은 폭 | 접는 디자인이 없어 메뉴 안 가로 스크롤(의도된 동작, 겹침·잘림 없음) | — | + +- 열림 상태 Header 상·하단 `#5F4B0F` 2px 선(Figma home on)과 로고 열림 표시는 **적용하지 않습니다**(디자인 승인 대기). 임의로 추가하지 않습니다. +- 새 색·크기는 위 토큰만 `style`로 덮어씁니다. `hm-sw-home-menu*` class를 소비 코드에 쓰지 않습니다. diff --git a/packages/sw/ai/examples/ExampleG-HomeMenu.tsx b/packages/sw/ai/examples/ExampleG-HomeMenu.tsx new file mode 100644 index 0000000..71d3166 --- /dev/null +++ b/packages/sw/ai/examples/ExampleG-HomeMenu.tsx @@ -0,0 +1,62 @@ +// Example G — the logo (home) menu (Figma 26:10591): SWHeader `homeMenu` + sections data + the optional recent +// files panel. Hidden by default; a logo click opens it, a second click closes it. New / Open stay in the +// document dropdown. Every text, icon and action is the program's data; the package ships none. +import { useState } from "react" +import { + SWHeader, SWRecentFiles, + type SWFileEntry, type SWHomeMenuItemData, type SWHomeMenuSectionData, +} from "@hm-design/sw" +import { documents } from "./sample-data" + +// One currentColor icon per tile follows the state colors (Default #B6B4AF, Hover / Active #FF5C00). +const Icon = ({ d }: { d: string }) => ( + +) + +const recentFiles: SWFileEntry[] = [ + { id: "r1", name: "평면도_A.dwg", path: "D:\\Projects\\A" }, + { id: "r2", name: "종단면도_B.dwg", path: "D:\\Projects\\B" }, +] + +export function ExampleGHomeMenu({ onEvent }: { onEvent?: (name: string) => void }) { + const [activeDocumentId, setActiveDocumentId] = useState(documents[0].id) + const [extra, setExtra] = useState([]) + // The same handlers as the document dropdown: the menu adds entry points, never a second implementation. + const openFile = () => onEvent?.("document:open") + const newFile = () => onEvent?.("document:new") + + const sections: SWHomeMenuSectionData[] = [ + { id: "working", title: "작업중인 파일", items: [ + { id: "save", label: "저장", icon: , toggle: false, onSelect: () => onEvent?.("menu:save") }, + { id: "export", label: "내보내기", icon: , toggle: false, onSelect: () => onEvent?.("menu:export") }, + ] }, + { id: "open", title: "다른 파일 열기", items: [ + { id: "find-open", label: "찾아열기", icon: , toggle: false, onSelect: openFile }, + // Optional sub panel: the tile toggles it (Active = open), the menu shows the outline accent meanwhile. + { id: "recent", label: "최근파일", icon: , + panel: onEvent?.(`recent:${file.id}`)} />, panelLabel: "최근파일" }, + { id: "new-file", label: "새파일", icon: , toggle: false, onSelect: newFile }, + ...extra, + ] }, + ] + + return ( + <> + item.id === activeDocumentId)?.name ?? ""} + logoAlt="예제 SW" + documents={documents} + activeDocumentId={activeDocumentId} + onDocumentSelect={document => { setActiveDocumentId(document.id); onEvent?.(`document:${document.id}`) }} + onDocumentNew={newFile} + onDocumentOpen={openFile} + homeMenu={{ sections, onItemSelect: event => onEvent?.(`menu:select:${event.item.id}:${event.active}`) }} + onHomeMenuOpenChange={(open, reason) => onEvent?.(`menu:${open ? "open" : "close"}:${reason}`)} + /> + {/* Adding an item is a data change only: width, dividers and corners follow the new item count. */} + + + ) +} diff --git a/packages/sw/src/components/sw-header/SWHeader.tsx b/packages/sw/src/components/sw-header/SWHeader.tsx index b192deb..caccd37 100644 --- a/packages/sw/src/components/sw-header/SWHeader.tsx +++ b/packages/sw/src/components/sw-header/SWHeader.tsx @@ -20,7 +20,8 @@ import { SWHeaderPropertyPanel } from "./components/SWHeaderPropertyPanel" import { SWHeaderDimensionStyleMenu, SWHeaderTextStyleMenu } from "./components/SWHeaderStyleListMenus" import { SWHeaderWindowControls } from "./components/SWHeaderWindowControls" import { SWToolButton } from "../sw-tool/SWTool" -import type { SWHeaderControlId, SWHeaderControlConfig, SWHeaderFrameControlId, SWHeaderHideableControlId, SWHeaderMenuId, SWHeaderProps, SWHeaderSubControlId } from "./types" +import { SWHomeMenu } from "../sw-home-menu/SWHomeMenu" +import type { SWHeaderControlId, SWHeaderControlConfig, SWHeaderFrameControlId, SWHeaderHideableControlId, SWHeaderHomeMenuChangeReason, SWHeaderMenuId, SWHeaderProps, SWHeaderSubControlId } from "./types" const defaults = { layer: { name: "0" }, @@ -135,6 +136,10 @@ export const SWHeader = forwardRef(function SWHeader onPropertyPanelOpenChange, disabled = false, tools, + homeMenu, + homeMenuOpen, + defaultHomeMenuOpen = false, + onHomeMenuOpenChange, logo, logoSrc, logoAlt, @@ -260,6 +265,39 @@ export const SWHeader = forwardRef(function SWHeader setOpenCollapsed(false) } }) + + // Logo (home) menu: SWHeader owns its open state; a logo click toggles it instead of calling onHomeClick. + const homeMenuId = useId() + const homeButtonRef = useRef(null) + const [internalHomeOpen, setInternalHomeOpen] = useState(defaultHomeMenuOpen) + const homeOpen = !!homeMenu && shown("home") && (homeMenuOpen ?? internalHomeOpen) + const [internalDocumentOpen, setInternalDocumentOpen] = useState(false) + const documentOpenNow = documentMenuOpen ?? internalDocumentOpen + const setDocumentOpen = (next: boolean) => { + if (documentMenuOpen === undefined) setInternalDocumentOpen(next) + onDocumentMenuOpenChange?.(next) + } + const setHomeOpen = (next: boolean, reason: SWHeaderHomeMenuChangeReason) => { + if (homeMenuOpen === undefined) setInternalHomeOpen(next) + if (next !== homeOpen) onHomeMenuOpenChange?.(next, reason) + if (!next) return + // One header layer at a time: the menu covers the fields, so their dropdown and the document menu close. + if (openMenu) openers[openMenu].onOpenChange(false) + if (documentOpenNow) setDocumentOpen(false) + } + // Placement from the DOM (never a fixed x): 2px before the object area (Figma 375 vs 377 at 1920) up to the + // window area, so it follows the breakpoints, `leading`, the header Open and a narrowed file name. + const [homeBox, setHomeBox] = useState({ left: 0, width: 0 }) + useLayoutEffect(() => { + const header = fieldsRef.current?.closest(".hm-sw-header") + homeButtonRef.current = header?.querySelector(":scope > .hm-sw-header__bar > .hm-sw-header__home") ?? null + if (!homeOpen || !header || !panelAnchorRef.current) return + const origin = header.getBoundingClientRect() + const area = panelAnchorRef.current.getBoundingClientRect() + const end = header.querySelector(":scope > .hm-sw-header__bar > .hm-sw-header__window")?.getBoundingClientRect().left ?? origin.right + const next = { left: area.left - origin.left - 2, width: end - area.left + 2 } + if (Math.abs(next.left - homeBox.left) > 0.1 || Math.abs(next.width - homeBox.width) > 0.1) setHomeBox(next) + }) const menuIds = configuration.map(item => item.id).filter((id): id is SWHeaderMenuId => id in menuSlot) const baseMenu = menuIds.includes("layer") ? "layer" : menuIds[0] const validPromoted = promoted && enabled(promoted) ? promoted : null @@ -440,8 +478,9 @@ export const SWHeader = forwardRef(function SWHeader title={documentTitle} onClick={onDocumentClick} documents={documents} - menuOpen={documentMenuOpen} - onMenuOpenChange={onDocumentMenuOpenChange} + // With a logo menu SWHeader holds the document menu state, so the two never stay open together. + menuOpen={homeMenu ? documentOpenNow : documentMenuOpen} + onMenuOpenChange={homeMenu ? next => { setDocumentOpen(next); if (next && homeOpen) setHomeOpen(false, "document") } : onDocumentMenuOpenChange} onDocumentSelect={onDocumentSelect} activeDocumentId={activeDocumentId} onDocumentClose={onDocumentClose} @@ -479,7 +518,8 @@ export const SWHeader = forwardRef(function SWHeader return (
- + setHomeOpen(!homeOpen, "home") : onHomeClick} interactive={shown("home")} logo={logo} logoSrc={logoSrc} logoAlt={logoAlt} + aria-expanded={homeMenu && shown("home") ? homeOpen : undefined} aria-controls={homeOpen ? homeMenu?.id ?? homeMenuId : undefined} /> {hasLeading &&
{openShown && {documentOpenLabel ?? "Open"}} {leading} @@ -505,6 +545,12 @@ export const SWHeader = forwardRef(function SWHeader
}
{barOpen && barData &&
} + {homeMenu && shown("home") && ( +
+ setHomeOpen(next, reason)} /> +
+ )} {panelOpen && propertyPanel && }
) diff --git a/packages/sw/src/components/sw-header/components/SWHeaderHome.tsx b/packages/sw/src/components/sw-header/components/SWHeaderHome.tsx index d379f30..b8eaaf6 100644 --- a/packages/sw/src/components/sw-header/components/SWHeaderHome.tsx +++ b/packages/sw/src/components/sw-header/components/SWHeaderHome.tsx @@ -15,13 +15,16 @@ export interface SWHeaderHomeProps { * SWHeader always shows the logo; only the home action can be removed. Default true. */ interactive?: boolean + /** Logo menu state (SWHeader `homeMenu`). No visual change: Figma has no open look for the logo. */ + "aria-expanded"?: boolean + "aria-controls"?: string } /** * Home button. The slanted background and the logo slot position are the common SW design; * the logo itself belongs to each SW program, so no logo is rendered unless one is passed. */ -export function SWHeaderHome({ onClick, logoSrc, logo, logoAlt, interactive = true }: SWHeaderHomeProps) { +export function SWHeaderHome({ onClick, logoSrc, logo, logoAlt, interactive = true, "aria-expanded": expanded, "aria-controls": controls }: SWHeaderHomeProps) { const content = logo ?? (logoSrc ? : null) if (!interactive) return (
@@ -30,7 +33,7 @@ export function SWHeaderHome({ onClick, logoSrc, logo, logoAlt, interactive = tr
) return ( - + ) +} diff --git a/packages/sw/src/components/sw-home-menu/SWRecentFiles.tsx b/packages/sw/src/components/sw-home-menu/SWRecentFiles.tsx new file mode 100644 index 0000000..5ce14d4 --- /dev/null +++ b/packages/sw/src/components/sw-home-menu/SWRecentFiles.tsx @@ -0,0 +1,92 @@ +import { useId, useRef, useState, type HTMLAttributes, type KeyboardEvent, type ReactNode } from "react" +import "../../theme/sw-home-menu.css" +import { useSWHomeMenuClose } from "./SWHomeMenu" +import type { SWFileEntry } from "./types" + +export interface SWRecentFilesProps extends Omit, "children"> { + /** Host-owned list (most recent first). Nothing is read from disk here. */ + files: readonly SWFileEntry[] + /** A row was chosen (click / Enter). The host opens the file. */ + onFileOpen?: (file: SWFileEntry) => void + /** Hover / focus change of the previewed row (null when none). */ + onFileHover?: (file: SWFileEntry | null) => void + /** Preview content when a file has no `preview` (e.g. a thumbnail loaded by the host). */ + renderPreview?: (file: SWFileEntry) => ReactNode + /** Inside : close the menu after `onFileOpen`. Default true. */ + closeOnOpen?: boolean + /** Shown when `files` is empty (not in Figma). */ + emptyText?: ReactNode + "aria-label"?: string +} + +/** + * Figma 최근파일 662:67331: 354px recent-file list (rows 50px: optional 29 x 28 badge, name 14px, path 12px) + * with the 2-depth preview 662:67471 beside the hovered / focused row. The list height follows the rows and is + * capped (`--hm-sw-recent-files-max-height`, Figma 750 = 15 rows); longer lists scroll inside. + */ +export function SWRecentFiles({ + files, onFileOpen, onFileHover, renderPreview, closeOnOpen = true, emptyText = "최근 파일이 없습니다", + className, onKeyDown, ...rest +}: SWRecentFilesProps) { + const closeWindow = useSWHomeMenuClose() + const listId = useId() + const listRef = useRef(null) + const [hovered, setHovered] = useState<{ id: string; top: number } | null>(null) + const current = hovered ? files.find(file => file.id === hovered.id) : undefined + const preview = current ? current.preview ?? renderPreview?.(current) : undefined + + const show = (file: SWFileEntry | null, row?: HTMLElement) => { + const list = listRef.current + const top = row && list ? row.offsetTop - list.scrollTop + 1 : 0 + setHovered(file ? { id: file.id, top } : null) + if ((file?.id ?? null) !== (hovered?.id ?? null)) onFileHover?.(file) + } + const open = (file: SWFileEntry) => { + if (file.disabled) return + onFileOpen?.(file) + if (closeOnOpen) closeWindow?.("file") + } + + const handleKeyDown = (event: KeyboardEvent) => { + onKeyDown?.(event) + if (event.defaultPrevented || !["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return + const rows = Array.from(listRef.current?.querySelectorAll(".hm-sw-recent-files__row:not(:disabled)") ?? []) + const index = rows.indexOf(document.activeElement as HTMLElement) + event.preventDefault() + event.stopPropagation() + const next = event.key === "Home" ? 0 : event.key === "End" ? rows.length - 1 + : index < 0 ? 0 : Math.min(rows.length - 1, Math.max(0, index + (event.key === "ArrowDown" ? 1 : -1))) + rows[next]?.focus() + } + + return ( +
show(null)}> +
hovered && setHovered(null)}> + {files.length === 0 &&
{emptyText}
} + {files.map(file => ( +
+ +
+ ))} +
+ {current && preview != null && ( + + )} +
+ ) +} diff --git a/packages/sw/src/components/sw-home-menu/assets/title-badge.svg b/packages/sw/src/components/sw-home-menu/assets/title-badge.svg new file mode 100644 index 0000000..d8e9cfb --- /dev/null +++ b/packages/sw/src/components/sw-home-menu/assets/title-badge.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/sw/src/components/sw-home-menu/index.ts b/packages/sw/src/components/sw-home-menu/index.ts new file mode 100644 index 0000000..0502dab --- /dev/null +++ b/packages/sw/src/components/sw-home-menu/index.ts @@ -0,0 +1,9 @@ +export { + SWHomeMenu, SWHomeMenuSection, SWHomeMenuItem, useSWHomeMenuClose, + type SWHomeMenuSectionProps, type SWHomeMenuItemProps, +} from "./SWHomeMenu" +export { SWRecentFiles, type SWRecentFilesProps } from "./SWRecentFiles" +export type { + SWHomeMenuProps, SWHomeMenuItemData, SWHomeMenuSectionData, SWHomeMenuSelectEvent, SWHomeMenuCloseReason, SWHomeMenuAccent, + SWFileEntry, +} from "./types" diff --git a/packages/sw/src/components/sw-home-menu/types.ts b/packages/sw/src/components/sw-home-menu/types.ts new file mode 100644 index 0000000..17b6b08 --- /dev/null +++ b/packages/sw/src/components/sw-home-menu/types.ts @@ -0,0 +1,108 @@ +import type { HTMLAttributes, ReactNode, RefObject } from "react" + +/** One 80 x 46 tile of the logo (home) menu (Figma 26:11355 … 26:11631). Text, icons and actions belong to the host. */ +export interface SWHomeMenuItemData { + id: string + /** One line. Longer text ends with an ellipsis and the full text is shown as `title`. */ + label: string + /** + * Icon in an 18 x 18 slot (centered; larger art may overflow it like the Figma assets). A `currentColor` + * icon follows the state color (Default #B6B4AF, Hover / Active #FF5C00); an image needs `hoverIcon`. + */ + icon?: ReactNode + /** Replaces `icon` on Hover and Active, for fixed-color image pairs (Figma default / hover SVG). Same name as SWToolButton. */ + hoverIcon?: ReactNode + /** + * true (default): a click toggles Active (Figma Default → Variant2 on hover → Variant3 on click → Default). + * false: a plain action button without Active state. + */ + toggle?: boolean + /** + * Optional sub panel opened under the tile while the tile is Active, e.g. (Figma 최근파일 662:67331). + * Only one tile with a panel is Active at a time; a second click closes it. + */ + panel?: ReactNode + /** Accessible name of the panel (defaults to the label). */ + panelLabel?: string + disabled?: boolean + /** Not rendered; the remaining tiles close the gap (no empty slot). */ + hidden?: boolean + /** Tooltip; defaults to `label`. */ + title?: string + "aria-label"?: string + onSelect?: (event: SWHomeMenuSelectEvent) => void +} + +export interface SWHomeMenuSectionData { + id: string + /** Section badge text (Figma tit_* 26:12604 …). The badge width follows the text. */ + title: string + items: readonly SWHomeMenuItemData[] + hidden?: boolean +} + +export interface SWHomeMenuSelectEvent { + item: SWHomeMenuItemData + section?: SWHomeMenuSectionData + /** Active state after this click (always false for `toggle: false` items). */ + active: boolean +} + +export type SWHomeMenuCloseReason = "escape" | "background" | "outside" | "select" | "file" + +/** + * Bottom accent under the sections. `sections` = Figma 26:10591 (one orange strip per section); + * `outline` = Figma 662:67267 (2px orange outline around the whole menu, shown with an open sub panel). + * Default `auto`: `outline` while a panel is open, otherwise `sections`. + */ +export type SWHomeMenuAccent = "auto" | "sections" | "outline" + +export interface SWHomeMenuProps extends Omit, "onSelect" | "title" | "children"> { + /** Data mode. Use either `sections` or `children` (). */ + sections?: readonly SWHomeMenuSectionData[] + children?: ReactNode + /** Controlled open state (e.g. toggled by SWHeader `onHomeClick`, the logo). Closed renders nothing. */ + open?: boolean + /** Uncontrolled initial state. Default false: the menu is hidden until the logo is clicked. */ + defaultOpen?: boolean + onOpenChange?: (open: boolean, reason: SWHomeMenuCloseReason) => void + /** Controlled Active (Figma Variant3) tile ids. */ + activeIds?: readonly string[] + defaultActiveIds?: readonly string[] + onActiveChange?: (activeIds: string[], event: SWHomeMenuSelectEvent) => void + /** `multiple` (default, each tile toggles on its own as in Figma) or `single` (one Active tile at a time). */ + selection?: "multiple" | "single" + /** Every tile click, after the item's own `onSelect`. */ + onItemSelect?: (event: SWHomeMenuSelectEvent) => void + /** Figma: a click on the menu outside a tile closes it (Group 2882 ON_CLICK → CLOSE). Default true. */ + closeOnBackgroundClick?: boolean + /** Close after a tile click. Default false (Figma: a tile click changes the tile to Variant3). */ + closeOnSelect?: boolean + /** Not in Figma (keyboard access). Escape closes an open panel first, then the menu. Default true. */ + closeOnEscape?: boolean + /** Not in Figma (its overlay has no outside interaction). Default false. */ + closeOnOutsideClick?: boolean + /** Focus the first tile when the menu opens. Default true (keyboard access). */ + autoFocus?: boolean + /** Focus target after Escape. Default: the element that had focus when the menu opened. */ + returnFocusRef?: RefObject + accent?: SWHomeMenuAccent + "aria-label"?: string +} + +/** One row of (Figma header/최근파일/list, 354 x 50). */ +export interface SWFileEntry { + id: string + /** File name (14px, one line, ellipsis). */ + name: string + /** Location (12px, 60%, one line, ellipsis). */ + path?: string + /** 29 x 28 slot before the text (Figma icon_dualmonitor: the file is open on monitor 1 / 2). Rows without it keep the text column. */ + badge?: ReactNode + /** 2-depth preview shown beside the hovered / focused row (Figma 파일출력 2뎁스 662:67471). */ + preview?: ReactNode + /** Preview caption; defaults to `path`. */ + previewTitle?: string + disabled?: boolean + title?: string +} diff --git a/packages/sw/src/index.ts b/packages/sw/src/index.ts index 794042d..24288d3 100644 --- a/packages/sw/src/index.ts +++ b/packages/sw/src/index.ts @@ -10,3 +10,4 @@ export { SWPopup, SWPopupSection, SWPopupRow, type SWPopupProps, type SWPopupSectionProps, type SWPopupRowProps, } from "./components/sw-popup/SWPopup" +export * from "./components/sw-home-menu" diff --git a/packages/sw/src/theme/sw-home-menu.css b/packages/sw/src/theme/sw-home-menu.css new file mode 100644 index 0000000..ee7cd2f --- /dev/null +++ b/packages/sw/src/theme/sw-home-menu.css @@ -0,0 +1,335 @@ +/* @hm-design/sw — Home menu (Figma rj6wpsfk24lOWvWiVRAANP 26:10591 "헤더/꾸러미 ver2/home on", tiles 26:11355 …). + Scoped to .hm-sw-home-menu; reads the shared SW tokens of .hm-sw-header / .hm-sw-theme (font, text outline, + focus). New values are Figma values (Plugin API, 2026-10-19), never global selectors. */ + +.hm-sw-home-menu { + /* Section surface (Frame 2409–2412; Frame 2408 has slightly different stops): bottom → top + edge shade. */ + --hm-sw-home-menu-section-background: + linear-gradient(0deg, #16120a 0%, rgba(22, 18, 10, 0) 54.458%, #16120a 100%), + linear-gradient(0deg, #18150e 28.102%, #262015 41.084%, #332b1c 58.06%, #38301f 81.027%, #18150e 100%); + --hm-sw-home-menu-section-border-color: #081813; + --hm-sw-home-menu-section-inner-border-color: #20190e; + /* Tile group g_* : fill bottom → top, 1px stroke top → bottom, drop shadow 0 2 r4 60%. */ + --hm-sw-home-menu-group-background: linear-gradient(0deg, #19160e 2%, #38301f 43.678%, #463c29 67.668%, #2f281a 100%); + --hm-sw-home-menu-group-border: linear-gradient(180deg, #886d35 0%, #423625 8.846%, #f3dba8 26.169%, #0e0b06 83.792%, #574b30 100%); + --hm-sw-home-menu-group-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); + /* Tile divider 1 x 44 (same gradient as the group stroke) with -1 0 r2 black shadow. */ + --hm-sw-home-menu-divider: var(--hm-sw-home-menu-group-border); + --hm-sw-home-menu-divider-shadow: -1px 0 2px #000; + /* Tile states: Variant2 / Variant3 stroke (INSIDE 1px, top → bottom), icon and label color. */ + --hm-sw-home-menu-item-hover-border: linear-gradient(180deg, #da6813 8.5%, #ffbb89 37.9%, #e5690d 71.9%, #ae3607 92.8%); + --hm-sw-home-menu-item-color: var(--hm-sw-header-text-color); + --hm-sw-home-menu-item-accent-color: #ff5c00; + --hm-sw-home-menu-icon-color: #b6b4af; + /* Title badge (tit_* Union, 9-slice of the Figma SVG) */ + --hm-sw-home-menu-title-color: #000; + /* Bottom accent: 26:10591 per-section strip (top → bottom) / 662:67267 2px outline (bottom → top). */ + --hm-sw-home-menu-accent-strip: linear-gradient(180deg, #5f4b0f -6.4%, #ff5100 96.8%); + --hm-sw-home-menu-accent-outline: linear-gradient(0deg, #ff5100 0%, #5f4b0f 76.7%); + + /* Optional sub panel (Figma 최근파일 662:67331 / 파일출력 2뎁스 662:67471). */ + --hm-sw-recent-files-width: 354px; + --hm-sw-recent-files-max-height: 750px; + --hm-sw-recent-files-background: #27241d; + --hm-sw-recent-files-border-color: #da6813; + --hm-sw-recent-files-path-color: #d4cbbd; + --hm-sw-recent-files-row-hover-background: var(--hm-sw-header-layer-row-hover-background); + --hm-sw-recent-files-row-hover-color: var(--hm-sw-header-layer-row-hover-color); + --hm-sw-recent-files-preview-width: 310px; + --hm-sw-recent-files-preview-background: #62533d; + + position: relative; + isolation: isolate; + width: max-content; + max-width: 100%; +} +.hm-sw-home-menu__sections { + display: flex; + align-items: flex-start; + max-width: 100%; + /* 2px of the accent strip shows left, right and under the sections (menu 1305 x 77 in Figma). */ + padding: 0 2px 2px; + /* Not in Figma (1920px only): narrower hosts scroll the menu horizontally, like the property bar. */ + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; +} +/* Sub panel under the menu, at its tile (position set from the DOM). */ +.hm-sw-home-menu__panel { position: absolute; z-index: 1; top: 100%; } + +.hm-sw-home-menu__section { + position: relative; + display: flex; + flex: 0 0 auto; + flex-direction: column; + align-items: center; + height: 75px; + /* Figma: content 5px from the top and 10px from the sides, the 1px stroke included. */ + padding: 4px 9px 0; + border: 1px solid var(--hm-sw-home-menu-section-border-color); + border-radius: 0 0 3px 3px; + background: var(--hm-sw-home-menu-section-background); +} +/* Frame 2412 (last): 261px, 11px left. Frame 2408 keeps 340px with its group centered. */ +.hm-sw-home-menu__section:last-child { padding-left: 10px; } +/* Inner 1px #20190E (Rectangle 6977 / border, inset 1px). */ +.hm-sw-home-menu__section::before { + content: ""; + position: absolute; + inset: 0; + border: 1px solid var(--hm-sw-home-menu-section-inner-border-color); + border-radius: 0 0 2px 2px; + pointer-events: none; +} +/* 26:10591 Rectangle 6991–6995: 30px strip from y 47 behind the section, 2px visible under it. */ +.hm-sw-home-menu[data-accent="sections"] .hm-sw-home-menu__section::after { + content: ""; + position: absolute; + z-index: -1; + top: 46px; + right: -2px; + bottom: -3px; + left: -2px; + border-radius: 0 0 3px 3px; + background: var(--hm-sw-home-menu-accent-strip); + pointer-events: none; +} +.hm-sw-home-menu[data-accent="sections"] .hm-sw-home-menu__section:first-child::after { left: -3px; } +.hm-sw-home-menu[data-accent="sections"] .hm-sw-home-menu__section:last-child::after { right: -3px; } +/* 662:67267 Rectangle 6991: 1305 x 30 from y 47, 2px INSIDE gradient stroke. */ +.hm-sw-home-menu[data-accent="outline"]::after { + content: ""; + position: absolute; + z-index: -1; + top: 47px; + right: 0; + left: 0; + height: 30px; + padding: 2px; + border-radius: 0 0 3px 3px; + background: var(--hm-sw-home-menu-accent-outline); + -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); + pointer-events: none; +} + +/* tit_* : text + 23px each side; the Figma Union SVG (132 x 27 incl. its -2 r4 shadow) is 9-sliced. */ +.hm-sw-home-menu__title { + position: relative; + z-index: 1; + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: center; + height: 19px; + margin-bottom: -1px; + padding: 1px 23px 0; + color: var(--hm-sw-home-menu-title-color); + font-size: var(--hm-sw-header-label-size); + font-weight: 400; + line-height: 14px; + letter-spacing: 0.02em; + white-space: nowrap; +} +.hm-sw-home-menu__title::before { + content: ""; + position: absolute; + z-index: -1; + inset: -6px -4px -2px; + border-style: solid; + border-width: 0 30px; + border-image: var(--hm-sw-home-menu-title-art) 0 30 fill / 0 30px / 0 stretch; + pointer-events: none; +} + +.hm-sw-home-menu__group { + position: relative; + display: flex; + flex: 0 0 auto; + height: 46px; + border-radius: 3px; + background: var(--hm-sw-home-menu-group-background); + box-shadow: var(--hm-sw-home-menu-group-shadow); +} +/* INSIDE stroke drawn over the edge: tiles keep their full 80 x 46 like Figma. */ +.hm-sw-home-menu__group::before { + content: ""; + position: absolute; + inset: 0; + padding: 1px; + border-radius: inherit; + background: var(--hm-sw-home-menu-group-border); + -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); + pointer-events: none; +} + +.hm-sw-home-menu__item { + position: relative; + flex: 0 0 80px; + width: 80px; + height: 46px; + color: var(--hm-sw-home-menu-item-color); +} +.hm-sw-home-menu__item:first-child { border-radius: 3px 0 0 3px; } +.hm-sw-home-menu__item:last-child { border-radius: 0 3px 3px 0; } +.hm-sw-home-menu__item:only-child { border-radius: 3px; } +/* Divider at the right edge of every tile but the last. */ +.hm-sw-home-menu__item:not(:last-child)::before { + content: ""; + position: absolute; + z-index: 2; + top: 1px; + right: -1px; + width: 1px; + height: 44px; + background: var(--hm-sw-home-menu-divider); + box-shadow: var(--hm-sw-home-menu-divider-shadow); + pointer-events: none; +} +/* Variant2 / Variant3 stroke. */ +.hm-sw-home-menu__item::after { + content: ""; + position: absolute; + z-index: 3; + inset: 0; + padding: 1px; + border-radius: inherit; + background: var(--hm-sw-home-menu-item-hover-border); + -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; +} + +/* 18 x 18 icon slot at (31, 6); larger art is centered on it and may overflow (Figma assets carry shadows). */ +.hm-sw-home-menu__icon { + position: absolute; + top: 6px; + left: 31px; + display: grid; + place-items: center; + width: 18px; + height: 18px; + color: var(--hm-sw-home-menu-icon-color); +} +.hm-sw-home-menu__icon > * { flex: none; max-width: none; } +.hm-sw-home-menu__icon > :is(svg, img) { display: block; } +.hm-sw-home-menu__icon--hover { visibility: hidden; } + +.hm-sw-home-menu__label { + position: absolute; + top: 25px; + left: 50%; + max-width: 76px; + height: 16px; + padding: 0 var(--hm-sw-header-text-outline-room); + overflow: hidden; + transform: translateX(-50%); + font-size: var(--hm-sw-header-font-size); + font-weight: 700; + line-height: 16px; + letter-spacing: 0.04em; + white-space: nowrap; + text-overflow: ellipsis; + text-shadow: var(--hm-sw-header-text-outline); +} + +/* Variant2 (hover) = Variant3 (active): orange stroke, #FF5C00 icon and label, art 1px up-left. */ +.hm-sw-home-menu__item:is(:hover:not(:disabled), [data-active="true"])::after { opacity: 1; } +.hm-sw-home-menu__item:is(:hover:not(:disabled), [data-active="true"]) { + --hm-sw-home-menu-icon-color: var(--hm-sw-home-menu-item-accent-color); + color: var(--hm-sw-home-menu-item-accent-color); +} +.hm-sw-home-menu__item:is(:hover:not(:disabled), [data-active="true"]) .hm-sw-home-menu__icon { transform: translate(-1px, -1px); } +.hm-sw-home-menu__item:is(:hover:not(:disabled), [data-active="true"]) .hm-sw-home-menu__label { top: 24px; } +.hm-sw-home-menu__item[data-has-hover-icon]:is(:hover:not(:disabled), [data-active="true"]) .hm-sw-home-menu__icon:not(.hm-sw-home-menu__icon--hover) { visibility: hidden; } +.hm-sw-home-menu__item[data-has-hover-icon]:is(:hover:not(:disabled), [data-active="true"]) .hm-sw-home-menu__icon--hover { visibility: visible; } + +/* SWRecentFiles — Figma 최근파일 662:67331 (354 x 750: rows 50px) + 2-depth preview 662:67471 (310 x 270). */ +.hm-sw-recent-files { position: relative; width: var(--hm-sw-recent-files-width); } +/* Frame stroke: 1px #DA6813 left / right / bottom over the list edge (joins the menu's outline accent). */ +.hm-sw-recent-files::after { + content: ""; + position: absolute; + z-index: 2; + inset: 0; + border: 1px solid var(--hm-sw-recent-files-border-color); + border-top: 0; + border-radius: 0 0 3px 3px; + pointer-events: none; +} +.hm-sw-recent-files__list { + box-sizing: content-box; /* max-height = rows (15 x 50 = Figma 750) */ + max-height: var(--hm-sw-recent-files-max-height); + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + border: 1px solid var(--hm-sw-recent-files-background); + border-radius: 0 0 4px 4px; + background: var(--hm-sw-recent-files-background); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25), inset 0 4px 8px rgba(0, 0, 0, 0.4); +} +.hm-sw-recent-files__row { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + height: 50px; + padding: 1px 8px; + text-align: left; +} +/* Rows without a badge keep the text column at 45px (Figma px 45). */ +.hm-sw-recent-files__row:not(:has(> .hm-sw-recent-files__badge)) { padding-left: 45px; } +.hm-sw-recent-files__badge { display: grid; flex: 0 0 29px; place-items: center; width: 29px; height: 28px; } +.hm-sw-recent-files__badge > :is(svg, img) { display: block; max-width: none; } +.hm-sw-recent-files__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; max-width: 301px; } +.hm-sw-recent-files__name, +.hm-sw-recent-files__path { + display: block; + overflow: hidden; + overflow-clip-margin: var(--hm-sw-header-text-outline-room); + white-space: nowrap; + text-overflow: ellipsis; + letter-spacing: 0.04em; +} +.hm-sw-recent-files__name { color: var(--hm-sw-header-text-color); font-size: var(--hm-sw-header-font-size); font-weight: 700; line-height: 16px; text-shadow: var(--hm-sw-header-text-outline); } +.hm-sw-recent-files__path { color: var(--hm-sw-recent-files-path-color); font-size: var(--hm-sw-header-label-size); line-height: 14px; opacity: 0.6; text-shadow: var(--hm-sw-header-label-outline); } +.hm-sw-recent-files__row:is(:hover, [data-hovered="true"]):not(:disabled) { background: var(--hm-sw-recent-files-row-hover-background); } +.hm-sw-recent-files__row:is(:hover, [data-hovered="true"]):not(:disabled) .hm-sw-recent-files__name { color: var(--hm-sw-recent-files-row-hover-color); } +.hm-sw-recent-files__empty { padding: 16px 45px; color: var(--hm-sw-recent-files-path-color); font-size: var(--hm-sw-header-label-size); opacity: 0.6; } +/* 2-depth preview beside the row (1px gap), top aligned 1px under the row top. */ +.hm-sw-recent-files__preview { + position: absolute; + z-index: 3; + left: calc(100% + 1px); + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + width: var(--hm-sw-recent-files-preview-width); + padding: 3px 0 0; /* + 1px border = Figma py 4; 270px total with the 243px body */ + border: 1px solid var(--hm-sw-recent-files-preview-background); + border-left: 0; + border-radius: 0 3px 4px 0; + background: var(--hm-sw-recent-files-preview-background); + box-shadow: inset 2px 0 2.5px rgba(0, 0, 0, 0.5); + filter: drop-shadow(0 2px 1px rgba(0, 0, 0, 0.5)); + pointer-events: none; +} +.hm-sw-recent-files__preview-title { + width: 294px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: var(--hm-sw-recent-files-row-hover-color); + font-size: var(--hm-sw-header-font-size); + line-height: 18px; + letter-spacing: 0.02em; +} +.hm-sw-recent-files__preview-body { position: relative; width: 100%; height: 243px; overflow: hidden; border-radius: 0 0 4px 0; } +.hm-sw-recent-files__preview-body > :is(img, svg, canvas) { display: block; width: 100%; height: 100%; object-fit: cover; } diff --git a/packages/sw/src/theme/sw.css b/packages/sw/src/theme/sw.css index 772a591..dd022cf 100644 --- a/packages/sw/src/theme/sw.css +++ b/packages/sw/src/theme/sw.css @@ -1876,3 +1876,8 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l .hm-sw-header[data-leading="open"] .hm-sw-header__fields[data-configured] > .hm-sw-header__control:not([data-control="document"]) > * { flex-shrink: 0; } /* ...and the file name gives way only down to the default design's smallest width (96px, see the clamps above). */ .hm-sw-header[data-leading="open"] .hm-sw-header__fields[data-configured] > .hm-sw-header__control[data-control="document"] { min-width: 96px; } + +/* Logo (home) menu layer (SWHeader homeMenu, 2026-10-19). Top of the header, left / max-width measured from the + object area (−2px) to the window area. Above the property bar anchor (2), under the dropdowns (30) and the + property window (40). The logo, the file name and the window area stay visible beside it. */ +.hm-sw-header__home-menu { position: absolute; top: 0; z-index: 20; } diff --git a/packages/sw/tests/home-menu-integration.browser.mjs b/packages/sw/tests/home-menu-integration.browser.mjs new file mode 100644 index 0000000..f084cc9 --- /dev/null +++ b/packages/sw/tests/home-menu-integration.browser.mjs @@ -0,0 +1,171 @@ +// Core checks of the Task A + B integration (SWHeader homeMenu, 2026-10-19) on the built Storybook. +// Usage: node packages/sw/tests/home-menu-integration.browser.mjs (build Storybook first) +import { createRequire } from 'node:module' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import path from 'node:path' +import http from 'node:http' + +const runtime = process.env.SW_RUNTIME_MODULES ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules' +const require = createRequire(path.join(runtime, 'index.js')) +const out = path.resolve(process.argv[2] ?? 'docs/sw/home-menu-integration-qa/home-menu') +await mkdir(out, { recursive: true }) +const root = path.resolve('storybook-static'), mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.ttf': 'font/ttf', '.woff2': 'font/woff2' } +const server = http.createServer(async (req, res) => { try { const p = new URL(req.url, 'http://localhost').pathname, file = path.resolve(root, '.' + decodeURIComponent(p === '/' ? '/index.html' : p)); if (path.relative(root, file).startsWith('..')) return res.writeHead(403).end(); res.setHeader('Content-Type', mime[path.extname(file)] ?? 'application/octet-stream'); res.end(await readFile(file)) } catch { res.writeHead(404).end() } }) +await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) +const base = `http://127.0.0.1:${server.address().port}` +const { chromium } = require('playwright') +const browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' }) +const page = await browser.newPage({ viewport: { width: 1920, height: 1000 } }) +const report = { browser: browser.version(), checks: [], errors: [], data: {} } +page.on('pageerror', e => report.errors.push(e.message)) +page.on('console', m => { if (m.type() === 'error') report.errors.push(m.text()) }) +const check = (name, ok, data) => { report.checks.push({ name, status: ok ? 'PASS' : 'FAIL', data }); console.log(ok ? 'PASS' : 'FAIL', name, ok || data === undefined ? '' : JSON.stringify(data)) } +const goto = async id => { + await page.goto(`${base}/iframe.html?id=sw-home-menu--${id}&viewMode=story`) + await page.locator('.hm-sw-header, .hm-sw-theme').first().waitFor() + await page.evaluate(() => document.fonts.ready) + await page.mouse.move(1, 1) + await page.waitForTimeout(100) +} +const menu = () => page.locator('.hm-sw-header .hm-sw-home-menu') +const home = () => page.locator('.hm-sw-header__home').first() +const log = () => page.locator('.sw-home-menu-story__log').innerText() +const activeIsHome = () => page.evaluate(() => document.activeElement?.classList.contains('hm-sw-header__home')) + +// 1. Storybook menu structure +const index = JSON.parse(await readFile(path.join(root, 'index.json'), 'utf8')) +const titles = [...new Set(Object.values(index.entries).map(entry => entry.title).filter(title => title.startsWith('SW/')))].sort() +report.data.titles = titles +check('Storybook SW titles: Header, Tool, Popup, Home Menu; no SW/Header/Options', ['SW/Header', 'SW/Tool', 'SW/Popup', 'SW/Home Menu'].every(t => titles.includes(t)) && !titles.some(t => t.startsWith('SW/Header/')), titles) + +// 2. Figma 26:10591 geometry at 1920 through SWHeader's measured placement +await goto('capture-default') +const geometry = await page.evaluate(() => { + const header = document.querySelector('.hm-sw-header').getBoundingClientRect() + const m = document.querySelector('.hm-sw-home-menu').getBoundingClientRect() + const r = e => e.getBoundingClientRect() + const tiles = [...document.querySelectorAll('.hm-sw-home-menu__item')].map(r) + const badge = getComputedStyle(document.querySelector('.hm-sw-home-menu')).getPropertyValue('--hm-sw-home-menu-title-art') + const icons = [...document.querySelectorAll('.hm-sw-home-menu__icon img')] + return { + left: m.left - header.left, width: m.width, height: m.height, headerHeight: header.height, + sections: [...document.querySelectorAll('.hm-sw-home-menu__section')].map(e => Math.round(r(e).width)), + tiles: tiles.length, tileSizes: [...new Set(tiles.map(t => `${Math.round(t.width)}x${Math.round(t.height)}`))], + titles: document.querySelectorAll('.hm-sw-home-menu__title').length, + badgeInlined: badge.includes('data:image/svg+xml') || badge.includes('.svg'), + icons: icons.length, iconsLoaded: icons.filter(i => i.complete && i.naturalWidth > 0).length, + } +}) +report.data.geometry = geometry +check('menu at Figma x 375, width 1305, height 77 (measured, no fixed x); header stays 49', Math.abs(geometry.left - 375) < 0.6 && Math.abs(geometry.width - 1305) < 0.6 && Math.abs(geometry.height - 77) < 0.6 && geometry.headerHeight === 49, geometry) +check('5 sections 340/260/180/260/261, 15 tiles 80x46, 5 titles', JSON.stringify(geometry.sections) === '[340,260,180,260,261]' && geometry.tiles === 15 && geometry.tileSizes.join() === '80x46' && geometry.titles === 5, geometry) +check('title badge SVG and 30 tile icons render', geometry.badgeInlined && geometry.icons === 30 && geometry.iconsLoaded === 30, geometry) +await page.screenshot({ path: path.join(out, 'capture-default-1920.png'), clip: { x: 0, y: 0, width: 1920, height: 80 } }) + +// 3. Hidden by default, logo opens / re-click closes, onHomeClick not called, Hover / Active +await goto('header-composition') +check('hidden by default (not in DOM), logo aria-expanded=false', await menu().count() === 0 && await home().getAttribute('aria-expanded') === 'false') +await home().click() +check('logo click opens, aria-expanded=true, aria-controls → menu id', await menu().count() === 1 && await home().getAttribute('aria-expanded') === 'true' && await home().getAttribute('aria-controls') === await menu().getAttribute('id')) +const tile = page.locator('.hm-sw-home-menu__item[data-item-id="save"]') +await tile.hover() +await page.waitForTimeout(50) +const hover = await tile.evaluate(e => getComputedStyle(e.querySelector('.hm-sw-home-menu__label')).color) +check('tile Hover: label #FF5C00', hover === 'rgb(255, 92, 0)', hover) +await tile.click() +const pressedOn = await tile.getAttribute('aria-pressed') +await tile.click() +check('tile click → Active (aria-pressed true), re-click → Default', pressedOn === 'true' && await tile.getAttribute('aria-pressed') === 'false', pressedOn) +await home().click() +const text = await log() +check('logo re-click closes; onHomeClick never called while homeMenu is set', await menu().count() === 0 && !text.includes('onHomeClick') && text.includes('onHomeMenuOpenChange(false, home)'), text) + +// 4. Escape + focus, background click, sub panel first +await home().click() +await page.keyboard.press('Escape') +check('Escape inside closes the menu and returns focus to the logo', await menu().count() === 0 && await activeIsHome()) +await home().click() +await page.locator('.hm-sw-home-menu__title').first().click() +check('click on the menu background closes it (Figma CLOSE)', await menu().count() === 0) +await home().click() +await page.locator('.hm-sw-home-menu__item[data-item-id="recent"]').click() +const panelOpen = await page.locator('.hm-sw-home-menu__panel').count() === 1 && await menu().getAttribute('data-accent') === 'outline' +await page.keyboard.press('Escape') +const panelFirst = await page.locator('.hm-sw-home-menu__panel').count() === 0 && await menu().count() === 1 +await page.keyboard.press('Escape') +check('recent panel opens (outline accent); Escape closes the panel, then the menu', panelOpen && panelFirst && await menu().count() === 0, { panelOpen, panelFirst }) + +// 5. Document dropdown keeps New / Open and never stays open with the menu +await home().click() +await page.locator('.hm-sw-header__document-button').first().click() +const docMenu = page.locator('.hm-sw-header__document-menu') +const footer = await docMenu.innerText().catch(() => '') +check('opening the document dropdown closes the menu (reason document); New / Open stay in the dropdown', await menu().count() === 0 && footer.includes('New') && footer.includes('Open') && (await log()).includes('onHomeMenuOpenChange(false, document)'), footer) +await home().click() +check('opening the menu closes the document dropdown', await menu().count() === 1 && await docMenu.count() === 0) +check('no separate Open button next to the logo (documentOpenPlacement="menu")', await page.locator('[data-document-open]').count() === 0) +await page.screenshot({ path: path.join(out, 'header-composition-open.png'), clip: { x: 0, y: 0, width: 1920, height: 400 } }) + +// 6. Menu + SWPopup + property window: Escape order and focus outside +await goto('integration') +const popup = page.locator('.hm-sw-property:has-text("사용자 Popup")') +const blur = () => page.locator('.story-page-header, h1').first().click({ position: { x: 2, y: 2 } }).catch(() => page.mouse.click(1900, 990)) +await page.getByTestId('open-popup').click() +await home().click() +await blur() +await page.keyboard.press('Escape') +const step1 = { menu: await menu().count(), popup: await popup.count() } +await page.keyboard.press('Escape') +const step2 = { menu: await menu().count(), popup: await popup.count() } +check('focus outside: Escape closes the most recent (menu), then the popup', step1.menu === 0 && step1.popup === 1 && step2.popup === 0, { step1, step2 }) +await home().click() +await page.getByTestId('open-popup').click() +await blur() +await page.keyboard.press('Escape') +const step3 = { menu: await menu().count(), popup: await popup.count() } +check('popup opened after the menu: Escape outside closes the popup first, menu stays', step3.popup === 0 && step3.menu === 1, step3) +await page.getByTestId('open-popup').click() +await popup.locator('button').first().focus() +await page.keyboard.press('Escape') +const step4 = { menu: await menu().count(), popup: await popup.count() } +check('Escape inside the popup closes only the popup', step4.popup === 0 && step4.menu === 1, step4) +await page.locator('.hm-sw-home-menu__item').first().focus() +await page.getByTestId('open-popup').click() +await page.locator('.hm-sw-home-menu__item').first().focus() +await page.keyboard.press('Escape') +const step5 = { menu: await menu().count(), popup: await popup.count(), focusHome: await activeIsHome() } +check('Escape inside the menu closes only the menu (popup stays), focus → logo', step5.menu === 0 && step5.popup === 1 && step5.focusHome, step5) + +// 7. Responsive placement (measured), internal scroll reported +const responsive = [] +for (const width of [1920, 1440, 1080, 768, 520, 390]) { + await page.setViewportSize({ width, height: 900 }) + await goto('capture-responsive') + const box = await page.evaluate(() => { + const r = s => document.querySelector(s)?.getBoundingClientRect() + const header = r('.hm-sw-header'), area = r('.hm-sw-header__object-area'), win = r('.hm-sw-header__window'), m = r('.hm-sw-home-menu'), homeBox = r('.hm-sw-header__home'), doc = r('.hm-sw-header__document') + const scroller = document.querySelector('.hm-sw-home-menu__sections') + return { headerHeight: header.height, start: m.left - (area.left - 2), endGap: (win?.left ?? header.right) - m.right, coversHome: m.left < homeBox.right - 17, coversDocument: m.left < doc.right - 1, menuWidth: Math.round(m.width), scrolls: scroller.scrollWidth > scroller.clientWidth + 1, pageScroll: document.documentElement.scrollWidth > innerWidth } + }) + responsive.push({ width, ...box }) + if (width === 390 || width === 768) await page.screenshot({ path: path.join(out, `responsive-${width}.png`), clip: { x: 0, y: 0, width, height: 90 } }) +} +report.data.responsive = responsive +check('every width: menu starts at object area −2px, ends at the window area, covers neither logo nor file name, header 49, no page scroll', + responsive.every(r => Math.abs(r.start) < 0.6 && r.endGap > -0.6 && !r.coversHome && !r.coversDocument && r.headerHeight === 49 && !r.pageScroll), responsive) + +// 8. Without homeMenu (existing Header): the logo still calls onHomeClick only, no menu, no aria-expanded +await page.setViewportSize({ width: 1920, height: 1000 }) +const infos = [] +page.on('console', m => { if (m.type() === 'info') infos.push(m.text()) }) +await page.goto(`${base}/iframe.html?id=sw-header--overview&viewMode=story`) +await page.locator('.hm-sw-header').first().waitFor() +await home().click() +await page.waitForTimeout(50) +check('without homeMenu: logo click calls onHomeClick, renders no menu, no aria-expanded', infos.some(text => text.includes('onHomeClick')) && await page.locator('.hm-sw-home-menu').count() === 0 && await home().getAttribute('aria-expanded') === null, infos) + +check('no page / console errors', report.errors.length === 0, report.errors) +await writeFile(path.join(out, 'result.json'), JSON.stringify(report, null, 2)) +console.log(`${report.checks.filter(c => c.status === 'PASS').length}/${report.checks.length} PASS`) +await browser.close(); server.close() +process.exitCode = report.checks.every(c => c.status === 'PASS') ? 0 : 1 diff --git a/src/sw/assets/home-menu/662-67331_recent-files.png b/src/sw/assets/home-menu/662-67331_recent-files.png new file mode 100644 index 0000000..ab32fa6 Binary files /dev/null and b/src/sw/assets/home-menu/662-67331_recent-files.png differ diff --git a/src/sw/assets/home-menu/662-67471_file-preview-2depth.png b/src/sw/assets/home-menu/662-67471_file-preview-2depth.png new file mode 100644 index 0000000..8e9f208 Binary files /dev/null and b/src/sw/assets/home-menu/662-67471_file-preview-2depth.png differ diff --git a/src/sw/assets/home-menu/bundle-hover.svg b/src/sw/assets/home-menu/bundle-hover.svg new file mode 100644 index 0000000..86d6ffd --- /dev/null +++ b/src/sw/assets/home-menu/bundle-hover.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/bundle.svg b/src/sw/assets/home-menu/bundle.svg new file mode 100644 index 0000000..cd5466c --- /dev/null +++ b/src/sw/assets/home-menu/bundle.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/display-hover.svg b/src/sw/assets/home-menu/display-hover.svg new file mode 100644 index 0000000..c3a7320 --- /dev/null +++ b/src/sw/assets/home-menu/display-hover.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/display.svg b/src/sw/assets/home-menu/display.svg new file mode 100644 index 0000000..bda5416 --- /dev/null +++ b/src/sw/assets/home-menu/display.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/export-hover.svg b/src/sw/assets/home-menu/export-hover.svg new file mode 100644 index 0000000..ac614b1 --- /dev/null +++ b/src/sw/assets/home-menu/export-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/export.svg b/src/sw/assets/home-menu/export.svg new file mode 100644 index 0000000..0dec208 --- /dev/null +++ b/src/sw/assets/home-menu/export.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/figma-26-10492.png b/src/sw/assets/home-menu/figma-26-10492.png new file mode 100644 index 0000000..7632167 Binary files /dev/null and b/src/sw/assets/home-menu/figma-26-10492.png differ diff --git a/src/sw/assets/home-menu/figma-26-10591.png b/src/sw/assets/home-menu/figma-26-10591.png new file mode 100644 index 0000000..2aee93c Binary files /dev/null and b/src/sw/assets/home-menu/figma-26-10591.png differ diff --git a/src/sw/assets/home-menu/figma-662-67267.png b/src/sw/assets/home-menu/figma-662-67267.png new file mode 100644 index 0000000..efebf12 Binary files /dev/null and b/src/sw/assets/home-menu/figma-662-67267.png differ diff --git a/src/sw/assets/home-menu/figma-preview-h1100.png b/src/sw/assets/home-menu/figma-preview-h1100.png new file mode 100644 index 0000000..10873fe Binary files /dev/null and b/src/sw/assets/home-menu/figma-preview-h1100.png differ diff --git a/src/sw/assets/home-menu/file-open-monitor-a.svg b/src/sw/assets/home-menu/file-open-monitor-a.svg new file mode 100644 index 0000000..e379c03 --- /dev/null +++ b/src/sw/assets/home-menu/file-open-monitor-a.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/file-open-monitor-b.svg b/src/sw/assets/home-menu/file-open-monitor-b.svg new file mode 100644 index 0000000..55210c3 --- /dev/null +++ b/src/sw/assets/home-menu/file-open-monitor-b.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/find-open-hover.svg b/src/sw/assets/home-menu/find-open-hover.svg new file mode 100644 index 0000000..a1093bb --- /dev/null +++ b/src/sw/assets/home-menu/find-open-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/find-open.svg b/src/sw/assets/home-menu/find-open.svg new file mode 100644 index 0000000..85e375b --- /dev/null +++ b/src/sw/assets/home-menu/find-open.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/help-hover.svg b/src/sw/assets/home-menu/help-hover.svg new file mode 100644 index 0000000..fdcd595 --- /dev/null +++ b/src/sw/assets/home-menu/help-hover.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/help.svg b/src/sw/assets/home-menu/help.svg new file mode 100644 index 0000000..3938455 --- /dev/null +++ b/src/sw/assets/home-menu/help.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/inquiry-hover.svg b/src/sw/assets/home-menu/inquiry-hover.svg new file mode 100644 index 0000000..bf92418 --- /dev/null +++ b/src/sw/assets/home-menu/inquiry-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/inquiry.svg b/src/sw/assets/home-menu/inquiry.svg new file mode 100644 index 0000000..3964453 --- /dev/null +++ b/src/sw/assets/home-menu/inquiry.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/new-file-hover.svg b/src/sw/assets/home-menu/new-file-hover.svg new file mode 100644 index 0000000..528357c --- /dev/null +++ b/src/sw/assets/home-menu/new-file-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/new-file.svg b/src/sw/assets/home-menu/new-file.svg new file mode 100644 index 0000000..21ea076 --- /dev/null +++ b/src/sw/assets/home-menu/new-file.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/recent-hover.svg b/src/sw/assets/home-menu/recent-hover.svg new file mode 100644 index 0000000..bed8d9c --- /dev/null +++ b/src/sw/assets/home-menu/recent-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/recent.svg b/src/sw/assets/home-menu/recent.svg new file mode 100644 index 0000000..b00e4d3 --- /dev/null +++ b/src/sw/assets/home-menu/recent.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/recover-hover.svg b/src/sw/assets/home-menu/recover-hover.svg new file mode 100644 index 0000000..710313c --- /dev/null +++ b/src/sw/assets/home-menu/recover-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/recover.svg b/src/sw/assets/home-menu/recover.svg new file mode 100644 index 0000000..e002ca5 --- /dev/null +++ b/src/sw/assets/home-menu/recover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/save-as-hover.svg b/src/sw/assets/home-menu/save-as-hover.svg new file mode 100644 index 0000000..3cc911d --- /dev/null +++ b/src/sw/assets/home-menu/save-as-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/save-as.svg b/src/sw/assets/home-menu/save-as.svg new file mode 100644 index 0000000..0caa615 --- /dev/null +++ b/src/sw/assets/home-menu/save-as.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/save-hover.svg b/src/sw/assets/home-menu/save-hover.svg new file mode 100644 index 0000000..8b11ed5 --- /dev/null +++ b/src/sw/assets/home-menu/save-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/save.svg b/src/sw/assets/home-menu/save.svg new file mode 100644 index 0000000..743ea60 --- /dev/null +++ b/src/sw/assets/home-menu/save.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/setting-load-hover.svg b/src/sw/assets/home-menu/setting-load-hover.svg new file mode 100644 index 0000000..155684e --- /dev/null +++ b/src/sw/assets/home-menu/setting-load-hover.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/setting-load.svg b/src/sw/assets/home-menu/setting-load.svg new file mode 100644 index 0000000..ca872d9 --- /dev/null +++ b/src/sw/assets/home-menu/setting-load.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/setting-save-hover.svg b/src/sw/assets/home-menu/setting-save-hover.svg new file mode 100644 index 0000000..8519586 --- /dev/null +++ b/src/sw/assets/home-menu/setting-save-hover.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/setting-save.svg b/src/sw/assets/home-menu/setting-save.svg new file mode 100644 index 0000000..5a4a508 --- /dev/null +++ b/src/sw/assets/home-menu/setting-save.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/sw-info-hover.svg b/src/sw/assets/home-menu/sw-info-hover.svg new file mode 100644 index 0000000..7e11c84 --- /dev/null +++ b/src/sw/assets/home-menu/sw-info-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/sw-info.svg b/src/sw/assets/home-menu/sw-info.svg new file mode 100644 index 0000000..0caf335 --- /dev/null +++ b/src/sw/assets/home-menu/sw-info.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/temp-file-hover.svg b/src/sw/assets/home-menu/temp-file-hover.svg new file mode 100644 index 0000000..5aec987 --- /dev/null +++ b/src/sw/assets/home-menu/temp-file-hover.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/assets/home-menu/temp-file.svg b/src/sw/assets/home-menu/temp-file.svg new file mode 100644 index 0000000..4896d9f --- /dev/null +++ b/src/sw/assets/home-menu/temp-file.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/sw/sw-home-menu-fixture.tsx b/src/sw/sw-home-menu-fixture.tsx new file mode 100644 index 0000000..4a803d7 --- /dev/null +++ b/src/sw/sw-home-menu-fixture.tsx @@ -0,0 +1,113 @@ +// Storybook fixture for SWHomeMenu: the Figma 26:10591 sample menu (rj6wpsfk24lOWvWiVRAANP). +// Texts, icons and ids are sample data only; the package ships no menu items or icons. +import type { ReactNode } from "react" +import type { SWFileEntry, SWHomeMenuSectionData } from "@hm-design/sw" +import monitorA from "./assets/home-menu/file-open-monitor-a.svg" +import monitorB from "./assets/home-menu/file-open-monitor-b.svg" +import previewImage from "./assets/home-menu/figma-preview-h1100.png" +import save from "./assets/home-menu/save.svg" +import saveHover from "./assets/home-menu/save-hover.svg" +import saveAs from "./assets/home-menu/save-as.svg" +import saveAsHover from "./assets/home-menu/save-as-hover.svg" +import exportIcon from "./assets/home-menu/export.svg" +import exportHover from "./assets/home-menu/export-hover.svg" +import bundle from "./assets/home-menu/bundle.svg" +import bundleHover from "./assets/home-menu/bundle-hover.svg" +import findOpen from "./assets/home-menu/find-open.svg" +import findOpenHover from "./assets/home-menu/find-open-hover.svg" +import recent from "./assets/home-menu/recent.svg" +import recentHover from "./assets/home-menu/recent-hover.svg" +import newFile from "./assets/home-menu/new-file.svg" +import newFileHover from "./assets/home-menu/new-file-hover.svg" +import recover from "./assets/home-menu/recover.svg" +import recoverHover from "./assets/home-menu/recover-hover.svg" +import tempFile from "./assets/home-menu/temp-file.svg" +import tempFileHover from "./assets/home-menu/temp-file-hover.svg" +import display from "./assets/home-menu/display.svg" +import displayHover from "./assets/home-menu/display-hover.svg" +import settingSave from "./assets/home-menu/setting-save.svg" +import settingSaveHover from "./assets/home-menu/setting-save-hover.svg" +import settingLoad from "./assets/home-menu/setting-load.svg" +import settingLoadHover from "./assets/home-menu/setting-load-hover.svg" +import help from "./assets/home-menu/help.svg" +import helpHover from "./assets/home-menu/help-hover.svg" +import inquiry from "./assets/home-menu/inquiry.svg" +import inquiryHover from "./assets/home-menu/inquiry-hover.svg" +import swInfo from "./assets/home-menu/sw-info.svg" +import swInfoHover from "./assets/home-menu/sw-info-hover.svg" + +/** + * Figma icon asset (SVG with its own shadow margin). `dx` / `dy` move it from the centered position in the + * 18px slot to its Figma position (measured from each tile's icon frame and asset inset, 2026-10-19). + */ +function figmaIcon(src: string, width: number, height: number, dx = 0, dy = 0) { + return +} +function pair(defaultSrc: string, hoverSrc: string, width: number, height: number, dx = 0, dy = 0) { + return { icon: figmaIcon(defaultSrc, width, height, dx, dy), hoverIcon: figmaIcon(hoverSrc, width, height, dx, dy) } +} + +/** Figma node of each tile component set, for the 22-node checklist. */ +export const figmaTileNodes: Record = { + save: "26:11355", "save-as": "26:11371", export: "26:11387", bundle: "26:11403", + "find-open": "26:11416", recent: "26:11450", "new-file": "26:11485", + recover: "26:11507", "temp-file": "26:11529", + display: "26:11542", "setting-save": "26:11558", "setting-load": "26:11586", + help: "26:11599", inquiry: "26:11615", "sw-info": "26:11631", +} +export const figmaTitleNodes: Record = { + working: "26:12604", open: "26:12610", damaged: "26:12622", setting: "26:12628 (참고)", help: "26:12616", +} + +export const figmaHomeMenuSections: SWHomeMenuSectionData[] = [ + { id: "working", title: "작업중인 파일", items: [ + { id: "save", label: "저장", ...pair(save, saveHover, 21.5, 21.5, -0.25, -0.25) }, + { id: "save-as", label: "복사저장", ...pair(saveAs, saveAsHover, 23.3, 23, 0, 0.5) }, + { id: "export", label: "내보내기", ...pair(exportIcon, exportHover, 22.52, 23.2775, 0, 0.64) }, + { id: "bundle", label: "도면묶기", ...pair(bundle, bundleHover, 23, 22) }, + ] }, + { id: "open", title: "다른파일 열기", items: [ + { id: "find-open", label: "찾아열기", ...pair(findOpen, findOpenHover, 24.6989, 20, -0.15, -1) }, + { id: "recent", label: "최근파일", ...pair(recent, recentHover, 22, 24.5, 1, 1.25) }, + { id: "new-file", label: "새파일", ...pair(newFile, newFileHover, 20, 23) }, + ] }, + { id: "damaged", title: "파일 손상시", items: [ + { id: "recover", label: "복구하기", ...pair(recover, recoverHover, 21, 22.3601, 0, -0.5) }, + { id: "temp-file", label: "임시파일", ...pair(tempFile, tempFileHover, 22.25, 24.5, 0.125, 0.25) }, + ] }, + { id: "setting", title: "My Setting", items: [ + { id: "display", label: "Display...", ...pair(display, displayHover, 24, 22, 1, 2) }, + { id: "setting-save", label: "저장하기", ...pair(settingSave, settingSaveHover, 24.3087, 24.5, 1.15, 1.25) }, + { id: "setting-load", label: "불러오기", ...pair(settingLoad, settingLoadHover, 18, 18, 1, 0) }, + ] }, + { id: "help", title: "궁금하면", items: [ + { id: "help", label: "설명보기", ...pair(help, helpHover, 18, 18, 1, 0) }, + { id: "inquiry", label: "문의하기", ...pair(inquiry, inquiryHover, 23, 23, 1, 0.5) }, + { id: "sw-info", label: "S/W정보", ...pair(swInfo, swInfoHover, 21.5, 20, 0.75, 0) }, + ] }, +] + +export const figmaHomeMenuItemIds = figmaHomeMenuSections.flatMap(section => section.items.map(item => item.id)) + +// Figma 최근파일 662:67331 sample rows (15) and the 2-depth preview 662:67471 (sample data only). +const recentNames = [ + ["001-연결종합 계획평면도22222222.dwg", "a"], ["03.암발파공법적용도_231011.grm", "b"], ["1.배수공-일반사항_231004.dwg", "b"], + ["00.시공상세도 분석 및 목차_20240416.grm"], ["0_목차.dwg"], ["001-연결종합 계획평면도22222222.dwg"], ["03.암발파공법적용도_231011.grm"], + ["001-연결종합 계획평면도22222222.dwg"], ["03.암발파공법적용도_231011.grm"], ["0_목차.dwg"], ["00.시공상세도 분석 및 목차_20240416.grm"], + ["001-연결종합 계획평면도22222222.dwg"], ["03.암발파공법적용도_231011.grm"], ["0_목차.dwg"], ["00.시공상세도 분석 및 목차_20240416.grm"], +] as const +const monitor = { a: monitorA, b: monitorB } +export const figmaRecentFiles: SWFileEntry[] = recentNames.map(([name, badge], index) => ({ + id: `recent-${index + 1}`, + name, + path: index < 2 ? "Z:\\01.기술개발센터\\04_그리미시스템\\Design..." : "Z:\\01.기술개발센터\\04_그리미시스템\\D...", + badge: badge ? : undefined, + // Figma image 918 crop inside the 310 x 244 frame. + preview: , + previewTitle: "C:/Users/user/Desktop/H1100.DWG", +})) + +/** The Figma menu with the optional 최근파일 sub panel attached to the 최근파일 tile. */ +export function withRecentFiles(sections: readonly SWHomeMenuSectionData[], panel: ReactNode): SWHomeMenuSectionData[] { + return sections.map(section => ({ ...section, items: section.items.map(item => item.id === "recent" ? { ...item, panel, panelLabel: "최근파일" } : item) })) +} diff --git a/src/sw/sw-home-menu.css b/src/sw/sw-home-menu.css new file mode 100644 index 0000000..0fc2a1e --- /dev/null +++ b/src/sw/sw-home-menu.css @@ -0,0 +1,14 @@ +/* Storybook layout only for SW / Home Menu (frames, logs, fixtures). The menu itself uses no story class. */ +.sw-home-menu-story__scroll { overflow-x: auto; } +.sw-home-menu-story__frame { position: relative; min-height: 80px; } +.sw-home-menu-story__overlay { position: absolute; top: 0; z-index: 3; } +.sw-home-menu-story__figma { display: block; max-width: none; } +.sw-home-menu-story__stage { display: flex; flex-wrap: wrap; gap: 16px; padding: 16px; background: #060606; } +.sw-home-menu-story__single { display: flex; height: 46px; } +.sw-home-menu-story__toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; } +.sw-home-menu-story__toolbar > button { padding: 4px 8px; border: 1px solid #999; border-radius: 4px; background: #fff; font: inherit; font-size: 13px; cursor: pointer; } +.sw-home-menu-story__log { margin: 12px 0 0; padding-left: 20px; font-family: monospace; font-size: 12px; } +.sw-home-menu-story__table { border-collapse: collapse; font-size: 13px; } +.sw-home-menu-story__table :is(th, td) { padding: 4px 8px; border: 1px solid #ccc; text-align: left; } +.sw-home-menu-story__tall .sw-home-menu-story__frame { height: 860px; } +.sw-home-menu-story__figma-row { display: flex; align-items: flex-start; gap: 16px; margin-top: 12px; } diff --git a/src/sw/sw-home-menu.stories.tsx b/src/sw/sw-home-menu.stories.tsx new file mode 100644 index 0000000..5748237 --- /dev/null +++ b/src/sw/sw-home-menu.stories.tsx @@ -0,0 +1,342 @@ +import { useState, type ReactNode } from "react" +import type { Meta, StoryObj } from "@storybook/react-vite" +// Public API only, like a consuming project. +import { + SWHeader, SWHeaderPropertyBar, SWHomeMenu, SWHomeMenuItem, SWHomeMenuSection, SWPopup, SWPopupRow, SWPopupSection, + SWRecentFiles, SWTheme, + type SWHeaderProperty, type SWHomeMenuAccent, type SWHomeMenuItemData, type SWHomeMenuSectionData, type SWHomeMenuSelectEvent, +} from "@hm-design/sw" +import "@hm-design/sw/styles.css" +import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" +import "./sw-home-menu.css" +import { + figmaHomeMenuItemIds, figmaHomeMenuSections, figmaRecentFiles, figmaTileNodes, figmaTitleNodes, withRecentFiles, +} from "./sw-home-menu-fixture" +import placeholderLogo from "./assets/sw-header-logo-placeholder.svg" +import figmaHomeOn from "./assets/home-menu/figma-26-10591.png" +import figmaHomeOnOutline from "./assets/home-menu/figma-662-67267.png" +import figmaRecent from "./assets/home-menu/662-67331_recent-files.png" +import figmaPreview from "./assets/home-menu/662-67471_file-preview-2depth.png" +import figmaPropertyBar from "./assets/home-menu/figma-26-10492.png" + +// Owner: docs/sw/figma-additional/analysis-report.md (Figma rj6wpsfk24lOWvWiVRAANP 26:10591 / 662:67267). +const KICKER = "HM DESIGN SYSTEM / SW" + +const meta = { + title: "SW/Home Menu", + parameters: { layout: "fullscreen", controls: { disable: true } }, +} satisfies Meta +export default meta +type Story = StoryObj + +const headerArgs = { + logoSrc: placeholderLogo, + logoAlt: "임시 로고", + documentTitle: "C0020101-006-평면및종단면도(06)", + drawingModeLabel: "그리기모드", + layer: { name: "0_Layer_Cludmark" }, + color: { value: "255,229,0", swatch: "#ffe500" }, + lineStyle: { name: "Continuous", weight: "0.25", scale: "1.00" }, + textStyle: { font: "Arial", name: "Standard" }, + dimensionStyle: { name: "Standard" }, +} +const documents = [{ id: "doc-1", name: "C0020101-006-평면및종단면도(06)" }, { id: "doc-2", name: "C0020101-007-횡단면도" }] + +function useLog() { + const [entries, setEntries] = useState([]) + return { entries, record: (entry: string) => setEntries(list => [entry, ...list].slice(0, 8)) } +} +function Log({ entries }: { entries: string[] }) { + return
    {entries.length ? entries.map((entry, index) =>
  1. {entry}
  2. ) :
  3. 이벤트 없음
  4. }
+} +const describe = (event: SWHomeMenuSelectEvent) => `onItemSelect(${event.section?.id ?? "-"} / ${event.item.id}, active=${event.active})` + +/** + * SWHeader `homeMenu`: the logo toggles the menu (no onHomeClick), SWHeader places it from the measured header + * (object area − 2px to the window area; Figma x 375 → 1680 at 1920). New / Open stay in the document dropdown. + */ +function HeaderWithMenu({ width, defaultOpen = false, defaultActiveIds = [], accent, recent = true, children }: { + width?: number; defaultOpen?: boolean; defaultActiveIds?: string[]; accent?: SWHomeMenuAccent; recent?: boolean + children?: (log: ReactNode) => ReactNode +}) { + const [open, setOpen] = useState(defaultOpen) + const [activeIds, setActiveIds] = useState(defaultActiveIds) + const { entries, record } = useLog() + const [sections] = useState(() => recent + ? withRecentFiles(figmaHomeMenuSections, record(`onFileOpen(${file.id})`)} />) + : figmaHomeMenuSections) + + return ( + <> +
+ record("onDocumentNew")} onDocumentOpen={() => record("onDocumentOpen")} + onHomeClick={() => record("onHomeClick (homeMenu 사용 중에는 호출되지 않음)")} + homeMenuOpen={open} onHomeMenuOpenChange={(next, reason) => { setOpen(next); record(`onHomeMenuOpenChange(${next}, ${reason})`) }} + homeMenu={{ + sections, accent, activeIds, + onActiveChange: next => { setActiveIds(next); record(`onActiveChange([${next.join(", ")}])`) }, + onItemSelect: event => record(describe(event)), + }} /> +
+ {children?.()} + + ) +} + +export const FigmaHomeOn: Story = { + name: "Figma 재현 (26:10591)", + render: () => ( + + + +
+
+ +
Figma 26:10591 home on
+
+
+ ), +} + +/** 1:1 capture canvas for pixel comparison with the Figma 1920 x 77 frame (no story page chrome). */ +function FigmaCanvas({ active, accent, recent }: { active?: boolean; accent?: SWHomeMenuAccent; recent?: boolean }) { + const sections = recent ? withRecentFiles(figmaHomeMenuSections, ) : figmaHomeMenuSections + return ( +
+ {/* The integrated placement (measured by SWHeader), not a fixed x: it must land on Figma x 375. */} + +
+ ) +} +export const CaptureDefault: Story = { name: "비교 캔버스 · Default", tags: ["!autodocs"], render: () => } +export const CaptureActive: Story = { name: "비교 캔버스 · Variant3", tags: ["!autodocs"], render: () => } +export const CaptureOutline: Story = { name: "비교 캔버스 · outline", tags: ["!autodocs"], render: () => } +export const CaptureRecent: Story = { name: "비교 캔버스 · 최근파일", tags: ["!autodocs"], render: () => } + +export const HeaderComposition: Story = { + name: "Header 조합 · 로고 클릭", + render: () => ( + + + + {log => log} + + + ), +} + +export const Variants: Story = { + name: "3가지 Variant", + render: () => ( + + + + + + + + + + + + + + + {figmaHomeMenuSections.map(section => ( + + + ))} +
섹션제목 노드타일 (노드)
{section.title}{figmaTitleNodes[section.id]}{section.items.map(item => `${item.label} (${figmaTileNodes[item.id]})`).join(" · ")}
+
+
+ ), +} + +function DynamicDemo() { + const [sections, setSections] = useState(figmaHomeMenuSections) + const [selection, setSelection] = useState<"multiple" | "single">("multiple") + const [accent, setAccent] = useState("auto") + const [activeIds, setActiveIds] = useState(["save"]) + const [count, setCount] = useState(1) + const { entries, record } = useLog() + const update = (sectionId: string, change: (items: readonly SWHomeMenuItemData[]) => SWHomeMenuItemData[]) => + setSections(list => list.map(section => section.id === sectionId ? { ...section, items: change(section.items) } : section)) + const toggleItem = (id: string, key: "hidden" | "disabled") => setSections(list => list.map(section => ({ + ...section, items: section.items.map(item => item.id === id ? { ...item, [key]: !item[key] } : item), + }))) + const mono = + + return ( + <> +
+ + + + + + + + + +
+ + { setActiveIds(next); record(`onActiveChange([${next.join(", ")}])`) }} + onItemSelect={event => record(describe(event))} /> + + + + ) +} + +export const DynamicData: Story = { + name: "Props · 데이터 변경", + render: () => ( + + + + + ), +} + +function StandaloneDemo() { + const [active, setActive] = useState(false) + const { entries, record } = useLog() + const mono = + return ( + <> + + record(describe(event))}> + + + + + + + + +
+ { setActive(next); record(`onActiveChange(${next})`) }} /> +
+
+ + + ) +} + +export const Standalone: Story = { + name: "단독 사용 · 조합형", + render: () => ( + + + + + ), +} + +export const RecentFilesPanel: Story = { + name: "최근파일 하위 패널 (662:67267)", + render: () => ( + + + +
{log => log}
+
+ +
Figma 662:67267
+
+ Figma 662:67331 최근파일 + Figma 662:67471 파일출력 2뎁스 +
+
+
+ ), +} + +// Property bar check (Figma 26:10492) against the current SWHeaderPropertyBar. Values are the Figma sample; the +// bar itself is unchanged (3 differences, awaiting approval: home-menu-integration report §속성Bar). +const barProperties: SWHeaderProperty[] = [ + { id: "radius", name: "반지름", value: "4.0526", type: "number", groupId: "bar" }, + { id: "diameter", name: "지름", value: "8.1052", type: "number", groupId: "bar" }, + { id: "circumference", name: "원주", value: "8.1052", type: "number", groupId: "bar" }, + { id: "area", name: "면적", value: "51.5962", type: "number", groupId: "bar" }, +] +function PropertyBarCompare() { + const [values, setValues] = useState(barProperties) + return ( + + setValues(list => list.map(item => item.id === property.id ? { ...item, value } : item))} /> + + ) +} +export const PropertyBarCheck: Story = { + name: "속성Bar 비교 (26:10492)", + render: () => ( + + + + +
Figma 26:10492
+
+
+ ), +} +export const CapturePropertyBar: Story = { + name: "비교 캔버스 · 속성Bar", tags: ["!autodocs"], + render: () => {}} />, +} +/** Integration: logo menu + property window + SWPopup at the same time (Escape stack and focus). */ +function IntegrationDemo() { + const [open, setOpen] = useState(false) + const [panelOpen, setPanelOpen] = useState(false) + const [popupOpen, setPopupOpen] = useState(false) + const { entries, record } = useLog() + const [sections] = useState(() => withRecentFiles(figmaHomeMenuSections, record(`onFileOpen(${file.id})`)} />)) + return ( + <> +
+ +
+
+ record("onDocumentNew")} onDocumentOpen={() => record("onDocumentOpen")} + homeMenuOpen={open} onHomeMenuOpenChange={(next, reason) => { setOpen(next); record(`onHomeMenuOpenChange(${next}, ${reason})`) }} + homeMenu={{ sections, onItemSelect: event => record(describe(event)) }} + propertyPanelOpen={panelOpen} onPropertyPanelOpenChange={next => { setPanelOpen(next); record(`onPropertyPanelOpenChange(${next})`) }} + propertyPanel={{ selection: { id: "none", title: "선택된 객체 없음", kind: "none" } }} /> + {popupOpen && ( + + { setPopupOpen(false); record("SWPopup onClose") }}> + 값 + + + )} +
+ + + ) +} +export const Integration: Story = { + name: "통합 · Popup · Escape", + render: () => ( + + + + + ), +} + +/** Responsive check: the header at the viewport width with the menu open (no story page chrome). */ +export const CaptureResponsive: Story = { name: "비교 캔버스 · 반응형", tags: ["!autodocs"], render: () => }