Merge branch 'integration/sw-home-menu' into feature/sw-home-menu
Integrate the SW home (logo) menu into SWHeader for SW general mode only. SW viewer mode code and stories are unchanged. - Resolve conflicts: keep both sw-home-menu and sw-viewer exports; renumber AI docs (catalog viewer 8->9, patterns home menu 7->8, design rules home menu 11->12) - SWHeader: start the home menu after the file name when it sits inside controls, so the file name stays visible - AI guides: mark home menu as general-mode only; forbid it in viewer mode (AI_USAGE, catalog, design rules, patterns, VIEWER.md, AI_GUIDE) - Tests: ignore wildcard identifiers in ai-docs check; update expected Storybook SW titles (SW/Tool removed, SW/Viewer added) - Add merge QA report docs/sw/sw-home-menu-merge-2026-10-19.md
No files matched your search
@@ -212,7 +212,7 @@ import { Button, Card, CardContent, FilterBar, FilterBarActions, FilterBarSearch
|
||||
### 예시 B1 — SW 일반 모드 (편집 Header + 도구 막대 + 설정 Popup)
|
||||
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(일반 모드) → COMPOSITION_PATTERNS "6. 파일 열기 + 도구 막대", 예제 `ai/examples/ExampleF-ViewerToolbar.tsx`(파일 이름의 Viewer는 과거 명칭이며 일반 모드 `SWHeader` 예제)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup), 로고(홈) 버튼 메뉴는 `SWHeader`의 `homeMenu` + `SWRecentFiles`(**일반 모드 전용**, 예제 `ExampleG-HomeMenu.tsx`)
|
||||
- 신규: SW에 없는 기능(예: 검색) → 소비 프로젝트 컴포넌트, `<SWTheme>` 안, `--hm-sw-header-*` 토큰, 안쪽 입력은 `SWHeaderInput`(예제 `ExampleD-SearchExtension.tsx`)
|
||||
|
||||
```tsx
|
||||
@@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css"
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx`
|
||||
- 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton`
|
||||
- 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
|
||||
```tsx
|
||||
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
|
||||
|
||||
@@ -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: <SaveIcon />, toggle: false, onSelect: () => host.save() },
|
||||
{ id: "export", label: "내보내기", icon: <ExportIcon /> },
|
||||
] },
|
||||
]
|
||||
|
||||
function App() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<SWHeader onHomeClick={() => setOpen(v => !v)} /* 기존 props */ />
|
||||
{/* 통합 전: host가 위치를 지정합니다(Header 객체 수정 영역 왼쪽 - 2px, 창 영역 왼쪽까지). */}
|
||||
<div style={{ position: "absolute", top: 0, left: objectAreaLeft - 2, maxWidth: windowLeft - objectAreaLeft + 2, zIndex: 3 }}>
|
||||
<SWTheme><SWHomeMenu sections={sections} open={open} onOpenChange={setOpen} /></SWTheme>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 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: <SWRecentFiles files={…} onFileOpen={…} />`. 패널은 한 번에 하나만 열리고 열린 동안 하단 강조가 전체 외곽선이 됩니다 |
|
||||
| 아이콘 | `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`가 담당합니다.
|
||||
@@ -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 구조
|
||||
|
||||
```
|
||||
<SWHomeMenu> overlay 패널(region/dialog 역할은 §5 R-6), open 상태·닫기 계약
|
||||
<SWHomeMenuSection title> 섹션 면 + 제목 badge(폭은 글자에 맞춤) + 그룹 box
|
||||
<SWHomeMenuItem icon label /> 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<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
||||
sections?: readonly SWHomeMenuSectionData[] // 또는 children(<SWHomeMenuSection>)
|
||||
open?: boolean // controlled
|
||||
defaultOpen?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onItemSelect?: (item: SWHomeMenuItemData, section: SWHomeMenuSectionData) => void
|
||||
/** 닫힐 때 Focus를 돌려줄 대상(기본: 연결된 홈 버튼). 통합 단계에서 SWHeader가 자동 연결 */
|
||||
returnFocusRef?: RefObject<HTMLElement>
|
||||
}
|
||||
export function SWHomeMenu(props: SWHomeMenuProps): JSX.Element
|
||||
export function SWHomeMenuSection(props: { title: string; children: ReactNode; className?: string }): JSX.Element
|
||||
export function SWHomeMenuItem(props: ButtonHTMLAttributes<HTMLButtonElement> & { label: string; icon?: ReactNode; iconHover?: ReactNode }): JSX.Element
|
||||
```
|
||||
|
||||
- **텍스트·아이콘·메뉴·이벤트·상태는 모두 외부에서 지정합니다.** 패키지에는 CAD 동작이 없습니다. Figma의 15개 메뉴는 Storybook fixture(또는 선택형 기본 아이콘 세트, §6 A-2)로만 제공합니다.
|
||||
- **독립 단계 연결 방식(SWHeader 무수정):** `<SWHeader onHomeClick={() => setOpen(v => !v)} />`와 `<SWHomeMenu open onOpenChange />`를 같은 상대 위치 컨테이너 안에 둡니다. 위치는 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 분기는 두지 않습니다. 단독 사용 시 `<SWTheme>` 안에서 렌더합니다.
|
||||
- **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 | 노드별 체크리스트 |
|
||||
| 단독 사용 | `<SWTheme>` 안 `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단계에서는 미검증 예정** |
|
||||
@@ -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/<node>.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%)" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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<SWHomeMenuProps, "open" \| "onOpenChange" \| "returnFocusRef">`, `homeMenuOpen?`, `defaultHomeMenuOpen?`(false), `onHomeMenuOpenChange?(open, reason)`. 미전달이면 기존 동작과 같습니다(하위 호환) |
|
||||
| I-3 | `components/sw-header/SWHeader.tsx` | 연결 | `homeMenu`가 있을 때만 홈 클릭으로 열림을 토글합니다. 기존 `onHomeClick`은 항상 먼저 호출합니다. 메뉴는 `<header>` 안의 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(포트 충돌 주의).
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -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
|
||||
<SWHomeMenu
|
||||
sections?: SWHomeMenuSectionData[] // 또는 children(<SWHomeMenuSection>/<SWHomeMenuItem>)
|
||||
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? }
|
||||
<SWRecentFiles files: SWFileEntry[] onFileOpen? onFileHover? renderPreview? closeOnOpen?=true emptyText? />
|
||||
SWFileEntry { id, name, path?, badge?, preview?, previewTitle?, disabled?, title? }
|
||||
```
|
||||
|
||||
- 텍스트·아이콘·항목·이벤트·Active·하위 패널은 모두 외부 데이터입니다. 패키지에는 메뉴 항목이나 CAD 동작이 들어 있지 않습니다.
|
||||
- 단독 사용: `<SWTheme>` 안에 둡니다. 메뉴 밖에서 쓰는 `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입니다. 정리 여부는 사용자가 결정합니다.
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 81 KiB |
@@ -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
|
||||
<SWHeader documentTitle={name} documents={documents} onDocumentNew={newFile} onDocumentOpen={openFile}
|
||||
homeMenu={{ sections, onItemSelect }} // SWHomeMenu Props (열림 상태 제외)
|
||||
onHomeMenuOpenChange={(open, reason) => {}} /> // reason: home | escape | background | outside | select | file | document
|
||||
```
|
||||
|
||||
- `sections`: `{ id, title, items: [{ id, label, icon, hoverIcon?, toggle?, panel?, disabled?, hidden?, onSelect? }] }[]`. 최근파일은 타일의 `panel: <SWRecentFiles files={…} onFileOpen={…} />`(선택).
|
||||
- 이벤트 우선순위: `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).
|
||||
@@ -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)은 이번 통합 범위 밖입니다.
|
||||
@@ -0,0 +1,49 @@
|
||||
# 홈 메뉴 Merge — `integration/sw-home-menu` → `feature/sw-package` (2026-10-19)
|
||||
|
||||
> 로컬 `git merge --no-commit --no-ff integration/sw-home-menu` (기준 `b434f27`, 대상 `1472e51`). **Merge Commit·Push는 하지 않았습니다(승인 대기).**
|
||||
|
||||
## 적용 범위 (사용자 정정 반영)
|
||||
|
||||
- 홈 메뉴(Figma 45개 노드, `SWHeader homeMenu` / `SWHomeMenu` / `SWRecentFiles`)는 **SW 일반 모드 전용**입니다.
|
||||
- SW 뷰어 모드(`SWViewer*`)에는 홈 메뉴를 적용하지 않습니다. 뷰어 코드·Story는 변경하지 않았습니다(`sw-viewer` 소스에 홈 메뉴 참조 없음).
|
||||
- AI 지침: AI_USAGE §0.2 표에 "로고(홈) 버튼 메뉴" 행(일반 = `homeMenu`, 뷰어 = 사용하지 않음), §0.3 혼합 금지 항목, 카탈로그 §5·DESIGN_RULES §12·PATTERNS 8의 "일반 모드 전용" 표기, VIEWER.md 잘못된 사용 행, 공통 AI_GUIDE 예시 B/B2.
|
||||
|
||||
## 충돌 해결
|
||||
|
||||
| 파일 | 해결 |
|
||||
|---|---|
|
||||
| `packages/sw/src/index.ts` | 두 Export 모두 유지(`sw-home-menu`, `sw-viewer`) |
|
||||
| `ai/AI_USAGE.md` | 뷰어 모드 문장 유지 + 홈 메뉴 문장 추가 |
|
||||
| `ai/COMPONENT_CATALOG.md` | 홈 메뉴 §5 추가, 이후 6 타입·7 비공개·8 제약, **뷰어 §8 → §9**(참조 갱신: AI_USAGE, VIEWER.md, DESIGN_RULES §11) |
|
||||
| `ai/COMPOSITION_PATTERNS.md` | 뷰어 7 유지, **홈 메뉴 7 → 8**, 하지 말 것 8 → 9 |
|
||||
| `ai/DESIGN_RULES.md` | 뷰어 §11 유지, **홈 메뉴 §11 → §12** |
|
||||
|
||||
`docs/sw/sw-home-menu-integration-2026-10-19.md`의 "패턴 7절, 디자인 규칙 11절"은 통합 당시 기록이라 그대로 둡니다(현재 번호는 위 표).
|
||||
|
||||
## Merge 중 추가 수정
|
||||
|
||||
| 파일 | 내용 | 이유 |
|
||||
|---|---|---|
|
||||
| `sw-header/SWHeader.tsx` | 홈 메뉴 시작 위치 = max(객체 수정 영역, `controls` 안 파일명 오른쪽) − 2px | `controls`로 파일명이 객체 수정 영역 안에 있는 Header(실제 SW 화면)에서 메뉴가 파일명을 덮음(x 63 → 378). 기본 Header 배치는 동일(캡처 바이트 동일) |
|
||||
| `tests/ai-docs.check.mjs` | `` `SWViewer*` `` 같은 와일드카드 표기를 Export 이름으로 보지 않음 | Merge 전 뷰어 문서에서도 실패하던 오탐 |
|
||||
| `tests/home-menu-integration.browser.mjs` | Story 목록 기대값에서 `SW/Tool` 제외, `SW/Viewer` 포함 | `SW/Tool`은 뷰어 작업(7f42923)에서 사용자 요청으로 삭제됨 |
|
||||
|
||||
## 검증
|
||||
|
||||
| 항목 | 결과 |
|
||||
|---|---|
|
||||
| SW typecheck / 앱 `tsc -b` / SW build / Storybook build | PASS / PASS / PASS / PASS |
|
||||
| AI 문서(`check:ai-docs`, `ai-docs.check.mjs`) | PASS / 32/32 |
|
||||
| 홈 메뉴 Storybook(`home-menu-integration.browser.mjs`, 배치 수정 후 재실행) | 22/22 |
|
||||
| 기존 Header·Popup 기능(`integrated-improvement` options·popup) | 89/0 |
|
||||
| 뷰어 Story 기능(`viewer.browser.mjs`, 참고 뷰어 없이) | 70/70 |
|
||||
| Merge 전(b434f27) 대비 픽셀: Header·Popup 요소(`viewer-regression`) / SW Header·Popup·Viewer 전체 Story 48장(1440·768, threshold 0) | 39/39 / 48/48 동일 (배치 수정 전 측정, 수정은 메뉴가 열린 동안만 실행) |
|
||||
| Figma 26:10591 | 현재 Figma 원본 = 저장 이미지(바이트 동일). 섹션 5·타일 15·크기 80×46·메뉴 1305×77·색·아이콘 일치. 글자는 Figma Dotum 14/12px vs HM Noto Sans KR(기존 "폰트 차이 · 디자인 승인 대기") |
|
||||
| 실제 SW 화면(`D:\디자인시스템\HMDesignSwTest`, 0.2.0 로컬 tarball) | typecheck·build PASS, 브라우저 14/14(열기·다시 클릭 닫기·Hover `#FF5C00`·최근파일·Escape 순서·문서 Dropdown·속성 패널 토글). 콘솔 404 1건은 앱의 `/favicon.ico` 없음(기존) |
|
||||
|
||||
## NOT VERIFIED
|
||||
|
||||
- `SW/Tool` 섹션 회귀(Story 삭제됨), 참고 DWG/DXF 뷰어 대비 비교, Firefox·Safari, SW + HM UI CSS 동시 사용, Registry Publish.
|
||||
- 배치 수정 후 Header·Popup·Viewer 픽셀 회귀 재실행(코드 경로상 메뉴가 닫혀 있으면 실행되지 않아 재사용).
|
||||
- 실제 SW 화면의 미연결 타일 12개(저장·복사저장·내보내기·도면묶기·복구하기·임시파일·Display·저장하기·불러오기·설명보기·문의하기·S/W정보): 뷰어 앱에 해당 기능이 없어 실행 동작 없음(`toggle: false`, 툴팁 "뷰어에서 지원하지 않는 기능").
|
||||
- 실제 SW 화면은 `logoSrc`를 주지 않아 로고 면이 비어 있음(기존 상태).
|
||||
@@ -212,7 +212,7 @@ import { Button, Card, CardContent, FilterBar, FilterBarActions, FilterBarSearch
|
||||
### 예시 B1 — SW 일반 모드 (편집 Header + 도구 막대 + 설정 Popup)
|
||||
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(일반 모드) → COMPOSITION_PATTERNS "6. 파일 열기 + 도구 막대", 예제 `ai/examples/ExampleF-ViewerToolbar.tsx`(파일 이름의 Viewer는 과거 명칭이며 일반 모드 `SWHeader` 예제)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup), 로고(홈) 버튼 메뉴는 `SWHeader`의 `homeMenu` + `SWRecentFiles`(**일반 모드 전용**, 예제 `ExampleG-HomeMenu.tsx`)
|
||||
- 신규: SW에 없는 기능(예: 검색) → 소비 프로젝트 컴포넌트, `<SWTheme>` 안, `--hm-sw-header-*` 토큰, 안쪽 입력은 `SWHeaderInput`(예제 `ExampleD-SearchExtension.tsx`)
|
||||
|
||||
```tsx
|
||||
@@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css"
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx`
|
||||
- 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton`
|
||||
- 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
|
||||
```tsx
|
||||
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
|
||||
|
||||
@@ -212,7 +212,7 @@ import { Button, Card, CardContent, FilterBar, FilterBarActions, FilterBarSearch
|
||||
### 예시 B1 — SW 일반 모드 (편집 Header + 도구 막대 + 설정 Popup)
|
||||
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(일반 모드) → COMPOSITION_PATTERNS "6. 파일 열기 + 도구 막대", 예제 `ai/examples/ExampleF-ViewerToolbar.tsx`(파일 이름의 Viewer는 과거 명칭이며 일반 모드 `SWHeader` 예제)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup), 로고(홈) 버튼 메뉴는 `SWHeader`의 `homeMenu` + `SWRecentFiles`(**일반 모드 전용**, 예제 `ExampleG-HomeMenu.tsx`)
|
||||
- 신규: SW에 없는 기능(예: 검색) → 소비 프로젝트 컴포넌트, `<SWTheme>` 안, `--hm-sw-header-*` 토큰, 안쪽 입력은 `SWHeaderInput`(예제 `ExampleD-SearchExtension.tsx`)
|
||||
|
||||
```tsx
|
||||
@@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css"
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx`
|
||||
- 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton`
|
||||
- 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
|
||||
```tsx
|
||||
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -212,7 +212,7 @@ import { Button, Card, CardContent, FilterBar, FilterBarActions, FilterBarSearch
|
||||
### 예시 B1 — SW 일반 모드 (편집 Header + 도구 막대 + 설정 Popup)
|
||||
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(일반 모드) → COMPOSITION_PATTERNS "6. 파일 열기 + 도구 막대", 예제 `ai/examples/ExampleF-ViewerToolbar.tsx`(파일 이름의 Viewer는 과거 명칭이며 일반 모드 `SWHeader` 예제)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup), 로고(홈) 버튼 메뉴는 `SWHeader`의 `homeMenu` + `SWRecentFiles`(**일반 모드 전용**, 예제 `ExampleG-HomeMenu.tsx`)
|
||||
- 신규: SW에 없는 기능(예: 검색) → 소비 프로젝트 컴포넌트, `<SWTheme>` 안, `--hm-sw-header-*` 토큰, 안쪽 입력은 `SWHeaderInput`(예제 `ExampleD-SearchExtension.tsx`)
|
||||
|
||||
```tsx
|
||||
@@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css"
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx`
|
||||
- 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton`
|
||||
- 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
|
||||
```tsx
|
||||
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
| 문서 | 내용 |
|
||||
|---|---|
|
||||
| [AI_GUIDE.md](./AI_GUIDE.md) | HMDesign 공통 진입점(모든 HMDesign 패키지에 같은 사본) |
|
||||
| [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) | 공식 공개 컴포넌트(일반 모드: Header·개별 컨트롤·Popup·도구 막대 / 뷰어 모드 §8: `SWViewer*` 전체 Export)·Props·Callback·크기·CSS 의존성 |
|
||||
| [DESIGN_RULES.md](./DESIGN_RULES.md) | SW 전용 토큰·Typography·간격·Hover/Focus/Selected·Overlay·Motion·반응형·Popup 외형·도구 버튼(일반 모드 §1~10), 뷰어 토큰·상태값 요약(§11) |
|
||||
| [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) | 공식 공개 컴포넌트(일반 모드: Header·개별 컨트롤·Popup·도구 막대·로고 클릭 메뉴 / 뷰어 모드 §9: `SWViewer*` 전체 Export)·Props·Callback·크기·CSS 의존성 |
|
||||
| [DESIGN_RULES.md](./DESIGN_RULES.md) | SW 전용 토큰·Typography·간격·Hover/Focus/Selected·Overlay·Motion·반응형·Popup 외형·도구 버튼·로고 클릭 메뉴(일반 모드 §1~10·§12), 뷰어 토큰·상태값 요약(§11) |
|
||||
| [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) | 완성형·간소화·기능 중심·확장 Header, Popup(자유 콘텐츠), 일반 모드 Header + 도구 막대(1~6), 뷰어 모드 기본 조합(7) |
|
||||
| [examples/](./examples) | 공개 API만 쓰는 예제. **일반 모드** A~F(E = 사용자 정의 Popup, F = 파일 열기 + 도구 막대. 독립 소비 프로젝트에서 Typecheck·Build·브라우저 검증. B·F 파일명의 "Viewer"는 과거 이름이며 일반 모드 `SWHeader` 구성), **뷰어 모드** H(`SWViewer*`) |
|
||||
| [VIEWER.md](./VIEWER.md) | **뷰어 모드 전용**: `SWViewerHeader`(데이터 메뉴·More·외곽선·로고 규칙), `SWViewerMenu`(Dropdown·서브메뉴·우클릭), `SWViewerPanel`(팝업 프레임 + 자유 콘텐츠). 일반 SW 모드는 `SWHeader`·`SWPopup`을 그대로 씁니다 |
|
||||
@@ -41,11 +41,12 @@ SW 패키지에는 Header·Popup 계열이 두 벌 있습니다. 화면마다 **
|
||||
| Header | `SWHeader`, 또는 `<SWTheme>` 안의 개별 컨트롤(`SWHeaderHome`, `SWHeaderDocumentTitle`, `SWHeaderLayerMenu`, `SWHeaderColorMenu`, `SWHeaderLineStyleMenu`, `SWHeaderTextStyleMenu`, `SWHeaderDimensionStyleMenu`, `SWHeaderActions`, `SWHeaderWindowControls`, `SWHeaderProperties` …) | `SWViewerHeader`(`groups` 데이터 + `trailing`), 오른쪽 끝·`custom` 버튼 `SWViewerHeaderButton`, 상태 `SWViewerStatus` |
|
||||
| Dropdown | 각 필드 Dropdown(`SWHeaderLayerMenu` …) | `type: "menu"` 항목, 단독·우클릭은 `SWViewerMenu` |
|
||||
| 팝업·설정창 | `SWPopup`(`titleActions`, `footer`) + `SWPopupSection` / `SWPopupRow` + `SWHeaderInput`, 속성창 `SWHeaderPropertyPanel`, 속성Bar `SWHeaderPropertyBar` | `SWViewerPanel`(이동·크기 조절·접기·`modal`, `headerActions`, `footer`) + `SWViewerSection` / `SWViewerField` / `SWViewerSwitch` / `SWViewerSlider` / `SWViewerInput` / `SWViewerButton` / `SWViewerBadge` / `SWViewerHint` |
|
||||
| 로고(홈) 버튼 메뉴 | `SWHeader`의 `homeMenu`(`SWHomeMenu` 데이터, 최근파일 `SWRecentFiles`, Figma 26:10591). 기본 숨김 → 로고 클릭 열기 → 다시 클릭 닫기 | **사용하지 않음.** `SWViewerHeader`에는 홈 메뉴 Props가 없고 로고 클릭은 `onHomeClick`만 호출합니다. 파일 열기·샘플은 Header `groups`의 `button`·`menu` 항목 |
|
||||
| 버튼·탭·토글 | `SWToolButton` / `SWToolGroup` | Header 항목 `type: "button" \| "toggle" \| "tabs" \| "switch"`, 패널 안 `SWViewerButton` |
|
||||
| 아이콘 | 패키지 자산(필드 안 아이콘) | 프로그램 아이콘 16px, 직접 그린 아이콘은 `SWViewerIcon`으로 감쌈 |
|
||||
| 범위 | Header 밖 부품은 `<SWTheme>` 안 | `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`은 루트에 `hm-sw-theme`이 있어 `<SWTheme>` 불필요. 패널 컨트롤은 `SWViewerPanel` 안 |
|
||||
| 토큰 | `--hm-sw-header-*`, Popup 콘텐츠 `--hm-sw-property-*` | `--hm-sw-viewer-*` + 공유 `--hm-sw-header-*`(아래 0.3) |
|
||||
| 읽을 문서 | [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §1~7, [DESIGN_RULES.md](./DESIGN_RULES.md) §1~10, [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 1~6, 예제 A~F | [VIEWER.md](./VIEWER.md), [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §8, [DESIGN_RULES.md](./DESIGN_RULES.md) §11, [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 7, [예제 H](./examples/ExampleH-ViewerMode.tsx) |
|
||||
| 읽을 문서 | [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §1~8, [DESIGN_RULES.md](./DESIGN_RULES.md) §1~10·§12, [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 1~6·8, 예제 A~G | [VIEWER.md](./VIEWER.md), [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §9, [DESIGN_RULES.md](./DESIGN_RULES.md) §11, [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 7, [예제 H](./examples/ExampleH-ViewerMode.tsx) |
|
||||
|
||||
- **일반 모드 Header에 도구 막대를 더한 구성**(로고 옆 파일 열기 + Model/Layout·Fit·2.5D 버튼을 `SWHeader`에 추가)은 **일반 모드**입니다: `SWHeader`의 `documentOpenPlacement`·`leading`·`tools` + `SWToolButton` / `SWToolGroup`(PATTERNS 6, 예제 F). "뷰어"·"도면 보기" 화면을 요청받았다면 이 구성이 아니라 뷰어 모드(`SWViewer*`)를 씁니다.
|
||||
- 모드 진입·종료, 파일 처리, 도면 렌더링, 메뉴 명령 실행은 어느 모드든 **프로그램 담당**입니다.
|
||||
@@ -55,6 +56,7 @@ SW 패키지에는 Header·Popup 계열이 두 벌 있습니다. 화면마다 **
|
||||
- **한 화면에 Header 하나.** `SWHeader`와 `SWViewerHeader`를 같은 화면에 함께 두지 않습니다. 두 모드를 오가는 프로그램은 상태를 유지한 채 Header 컴포넌트만 바꿉니다(VIEWER.md §1).
|
||||
- **뷰어 화면:** `SWHeader`에 뷰어 메뉴를 넣거나 `SWHeader`·`SWPopup`을 뷰어처럼 재스타일하지 않습니다. 뷰어 팝업은 `SWViewerPanel`입니다.
|
||||
- **일반 화면:** `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`과 패널 컨트롤(`SWViewerSection` …)을 쓰지 않습니다.
|
||||
- **로고(홈) 메뉴는 SW 일반 모드 전용:** `SWHomeMenu`·`SWHomeMenuSection`·`SWHomeMenuItem`·`SWRecentFiles`·`useSWHomeMenuClose`와 `SWHeader`의 `homeMenu`·`homeMenuOpen`·`defaultHomeMenuOpen`·`onHomeMenuOpenChange`는 일반 모드 `SWHeader` 화면에서만 씁니다. 뷰어 화면에는 홈 메뉴를 만들지 않고(`SWViewerHeader` 옆·`custom` 항목·`SWViewerPanel` 안에 넣거나 비슷하게 다시 만들지 않음), 뷰어 Header와 기능은 그대로 둡니다.
|
||||
- **토큰:** `--hm-sw-viewer-*` 토큰을 일반 모드 신규 UI에 쓰지 않고, 일반 모드 전용 모양(속성창 행 `SWPopupRow`, `SWToolButton`의 `pill`·`text` 모양, 일반 필드 Dropdown)을 뷰어 신규 UI에 가져오지 않습니다. 두 토큰은 같은 범위(`.hm-sw-header`, `.hm-sw-theme`)에 함께 선언되어 있어 기술적으로 읽히지만, 읽힌다는 것이 허용을 뜻하지 않습니다.
|
||||
- **공유 — 모드 혼합 아님(실제 코드가 공유하는 부분):**
|
||||
- `SWViewerPanel`은 내부에서 `SWPopup` 프레임(제목 막대·제목·닫기·테두리·면)을 렌더링합니다. 뷰어에서 `SWViewerPanel`을 쓰는 것은 일반 모드 혼합이 아닙니다. 단, 뷰어 화면에서 `SWPopup`을 직접 쓰지는 않습니다.
|
||||
@@ -76,8 +78,8 @@ SW 패키지에는 Header·Popup 계열이 두 벌 있습니다. 화면마다 **
|
||||
|
||||
## 1. 작업 순서 (필수)
|
||||
|
||||
1. **먼저 검색한다.** §0에서 모드를 정하고, 요청 기능을 [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md)의 공개 컴포넌트(뷰어 모드는 §8과 [VIEWER.md](./VIEWER.md))에서 찾는다.
|
||||
2. **있으면 재사용한다(1순위).** 뷰어 모드는 `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`이다(VIEWER.md). 일반 모드에서 완성형 Header가 필요하면 `SWHeader` 하나만 쓴다. 기존 Props(`hiddenControls`, `menuOrder`, `controls`, `documentOpenPlacement`, `leading`, `tools`, 각 데이터 배열·Callback)로 해결되는지 먼저 본다. 팝업·설정창은 `SWPopup`, 버튼·탭·토글은 `SWToolButton` / `SWToolGroup`이다.
|
||||
1. **먼저 검색한다.** §0에서 모드를 정하고, 요청 기능을 [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md)의 공개 컴포넌트(뷰어 모드는 §9와 [VIEWER.md](./VIEWER.md))에서 찾는다.
|
||||
2. **있으면 재사용한다(1순위).** 뷰어 모드는 `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`이다(VIEWER.md). 일반 모드에서 완성형 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` …)를 `<SWTheme>` 안에 배치한다. 배치용 CSS만 소비 프로젝트에 작성한다([COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md)).
|
||||
4. **조합으로도 안 될 때만 새로 만든다(3순위).** 새 컴포넌트는 **소비 프로젝트**에 작성하고, 공개 컴포넌트(예: `SWHeaderInput`)를 내부에 재사용하며, 외형은 공개 토큰 `--hm-sw-header-*`만 읽는다([DESIGN_RULES.md](./DESIGN_RULES.md), 예제 D). 뷰어 모드의 새 UI는 `--hm-sw-viewer-*`(+ 공유 `--hm-sw-header-*` 면·외곽선, [DESIGN_RULES.md](./DESIGN_RULES.md) §11)를 읽고 Header 버튼·패널 버튼·입력은 `SWViewerHeaderButton`·`SWViewerButton`·`SWViewerField` + `SWViewerInput`을 재사용한다. 모드별 상세는 §0.4. 뷰어 화면에 일반 모드 컨트롤 모양을 섞지 않는다.
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
- **열림 상태:** Dropdown은 `open` / `onOpenChange`(Controlled)를 주지 않으면 내부 상태로 동작합니다.
|
||||
- **공통 키보드:** Click·Enter·Space 열기/닫기(Hover로는 열리지 않음), ArrowDown 열고 첫(현재) 항목 Focus, Arrow 이동, Escape·바깥 클릭·Tab 이탈 닫기(Escape는 Trigger로 Focus 복귀).
|
||||
- **검증 표시:** ✅ = 2026-10-19 독립 소비 프로젝트(Tarball 설치)에서 브라우저 검증, ◻ = 소비 검증 범위 밖(Storybook 검증 기록만 있음).
|
||||
- **먼저 모드:** SW 일반 모드(편집) / SW 뷰어 모드(도면 보기)를 [AI_USAGE.md](./AI_USAGE.md) §0으로 정합니다. **뷰어 모드는 §8(`SWViewer*`)만** 쓰고, 아래 §1~4는 일반 모드용입니다. 두 모드를 한 화면에 섞지 않습니다.
|
||||
- **무엇을 쓸까(일반 모드):** Header 전체 → `SWHeader`(1). Header 부품 조합 → 개별 컨트롤(2). 팝업·설정창·정보창 → `SWPopup`(3). 도구 막대·탭·토글 → `SWToolButton` / `SWToolGroup`(4). Slider·Switch·Badge·Progress·Dialog(모달)·Label·FilterBar는 SW에 없습니다 → `@hm-design/ui`를 그대로 씁니다(필요하면 `SWPopup` children 안에).
|
||||
- **무엇을 쓸까(뷰어 모드):** Header → `SWViewerHeader`(`groups` + `trailing`), Dropdown·우클릭 → `SWViewerMenu`, 팝업·설정창·모달 → `SWViewerPanel` + `SWViewerSection`/`SWViewerField`/`SWViewerSwitch`/`SWViewerSlider`/`SWViewerInput`/`SWViewerButton`, 상태 → `SWViewerStatus`(8).
|
||||
- **먼저 모드:** SW 일반 모드(편집) / SW 뷰어 모드(도면 보기)를 [AI_USAGE.md](./AI_USAGE.md) §0으로 정합니다. **뷰어 모드는 §9(`SWViewer*`)만** 쓰고, 아래 §1~8은 일반 모드용입니다. 두 모드를 한 화면에 섞지 않습니다.
|
||||
- **무엇을 쓸까(일반 모드):** 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 안에).
|
||||
- **무엇을 쓸까(뷰어 모드):** Header → `SWViewerHeader`(`groups` + `trailing`), Dropdown·우클릭 → `SWViewerMenu`, 팝업·설정창·모달 → `SWViewerPanel` + `SWViewerSection`/`SWViewerField`/`SWViewerSwitch`/`SWViewerSlider`/`SWViewerInput`/`SWViewerButton`, 상태 → `SWViewerStatus`(9).
|
||||
|
||||
## 1. 완성형 템플릿
|
||||
|
||||
@@ -21,7 +21,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` |
|
||||
@@ -34,7 +35,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
|
||||
<SWHeader documentTitle={name} logoSrc={logo} logoAlt="제품명" layers={layers} activeLayerId={id} onLayerSelect={l => setId(l.id)} onClose={closeWindow} />
|
||||
```
|
||||
@@ -78,7 +78,7 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체
|
||||
|
||||
## 4. 도구 막대 — 기존 SW 버튼 3종
|
||||
|
||||
**일반 모드** 도구 막대입니다. 새 디자인 값 없이 기존 SW 버튼 모양을 공개 버튼으로 씁니다(일반 모드 `SWHeader`에 더하는 Model/Layout, Fit, 2.5D, 파일 열기 등). 뷰어 모드 화면은 §8 `SWViewerHeader` 항목을 씁니다. `SWHeader`의 `tools`·`leading` 안이나 `<SWTheme>` 안에 둡니다.
|
||||
**일반 모드** 도구 막대입니다. 새 디자인 값 없이 기존 SW 버튼 모양을 공개 버튼으로 씁니다(일반 모드 `SWHeader`에 더하는 Model/Layout, Fit, 2.5D, 파일 열기 등). 뷰어 모드 화면은 §9 `SWViewerHeader` 항목을 씁니다. `SWHeader`의 `tools`·`leading` 안이나 `<SWTheme>` 안에 둡니다.
|
||||
|
||||
| 컴포넌트 | 용도 | 주요 Props | 모양(기존 기준) | 검증 |
|
||||
|---|---|---|---|---|
|
||||
@@ -86,17 +86,36 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체
|
||||
| `SWToolGroup` | 버튼 묶음 | `appearance`(`text` 기본 = 상세보기 묶음, `pill` = 인쇄·K pill), `label`(`text`의 12px 캡션·접근성 이름), `selection`(`single` = radiogroup, 화살표·Home·End로 이동·선택), `div` 속성 | 버튼 사이 1px 구분선(`text`), 내용 폭 pill | ✅ |
|
||||
|
||||
- 탭(Model / Layout)은 별도 컴포넌트 없이 `SWToolGroup selection="single"` + `SWToolButton pressed`입니다.
|
||||
- 일반 모드의 상태 표시(로드 상태 등) 전용 SW 디자인은 아직 없습니다(디자인 확정 후 검토). 뷰어 모드는 `SWViewerStatus`(§8)가 있으며 일반 모드 Header에 가져오지 않습니다.
|
||||
- 일반 모드의 상태 표시(로드 상태 등) 전용 SW 디자인은 아직 없습니다(디자인 확정 후 검토). 뷰어 모드는 `SWViewerStatus`(§9)가 있으며 일반 모드 Header에 가져오지 않습니다.
|
||||
|
||||
## 5. 공개 타입
|
||||
## 5. 로고 클릭 메뉴 — Figma 2단 Header 메뉴 (SW 일반 모드 전용)
|
||||
|
||||
`SWHeaderProps`, 각 컴포넌트 Props(`SWHeaderHomeProps`, `SWHeaderDocumentTitleProps`, `SWHeaderLayerControlProps`, `SWHeaderLayerMenuProps`, `SWHeaderColorMenuProps`, `SWHeaderLineStyleMenuProps`, `SWHeaderTextStyleMenuProps`, `SWHeaderDimensionStyleMenuProps`, `SWHeaderInputProps`, `SWHeaderPropertiesProps`, `SWHeaderPropertyPanelProps`, `SWHeaderPropertyBarProps`, `SWHeaderActionsProps`, `SWHeaderWindowControlsProps`), `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`. 뷰어 모드 타입은 §8.
|
||||
> **SW 일반 모드(`SWHeader`)에서만 씁니다.** SW 뷰어 모드(§9 `SWViewer*`)에는 홈 메뉴가 없으며 이 절의 컴포넌트를 뷰어 화면에 넣지 않습니다([AI_USAGE.md](./AI_USAGE.md) §0.3).
|
||||
|
||||
## 6. 공개하지 않은 것 (사용 금지)
|
||||
Figma 26:10591(home on), 타일 15종 26:11355 … 26:11631, 섹션 제목 26:12604 … 26:12622, 최근파일 상태 662:67267 · 목록 662:67331 · 2뎁스 미리보기 662:67471을 **공통 컴포넌트 4개**로 표현합니다. 메뉴 항목·아이콘·제목·Active·이벤트·최근파일은 모두 데이터입니다(패키지에는 항목·아이콘이 없음). 운영체제 파일 선택창이 아니며, "찾아열기" 등의 실제 동작은 호스트가 `onSelect`에서 수행합니다.
|
||||
|
||||
`SWHeaderMore`, `SWHeaderField`/`SWHeaderFieldText`, `SWHeaderKGuide`, `useSWHeaderDropdown`, `SWHeaderMenuDetail`, `SWHeaderColorChip`, `SWHeaderLayerRows`, `SWHeaderColorPanelBody`, `SWHeaderLineStyleColumns`, `SWPopupFrame`, `useSWFixedDropdownPosition`, 속성창 내부 Picker, `useSWHeaderOverlay` 등은 내부 구현입니다. 필요하면 공개 컴포넌트로 대체하거나, 부족한 API로 보고합니다.
|
||||
| 컴포넌트 | 용도 | 주요 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 | ✅ |
|
||||
|
||||
## 7. 알려진 단독 사용 제약
|
||||
- **Header 연결:** `SWHeader`의 `homeMenu`에 `SWHomeMenu` Props를 주면 됩니다. 기본 숨김, 로고 클릭 열기, 다시 클릭 닫기. 위치는 SWHeader가 실제 DOM으로 정합니다(객체 수정 영역 −2px ~ 창 영역, 고정 x 없음. `controls`에 `document`가 있으면 파일명 오른쪽 −2px부터). 로고·파일명·창 영역은 가리지 않습니다.
|
||||
- **이벤트 우선순위:** `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`는 파일을 열면 자동).
|
||||
|
||||
## 6. 공개 타입
|
||||
|
||||
`SWHeaderProps`, 각 컴포넌트 Props(`SWHeaderHomeProps`, `SWHeaderDocumentTitleProps`, `SWHeaderLayerControlProps`, `SWHeaderLayerMenuProps`, `SWHeaderColorMenuProps`, `SWHeaderLineStyleMenuProps`, `SWHeaderTextStyleMenuProps`, `SWHeaderDimensionStyleMenuProps`, `SWHeaderInputProps`, `SWHeaderPropertiesProps`, `SWHeaderPropertyPanelProps`, `SWHeaderPropertyBarProps`, `SWHeaderActionsProps`, `SWHeaderWindowControlsProps`), `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`. 뷰어 모드 타입은 §9.
|
||||
|
||||
## 7. 공개하지 않은 것 (사용 금지)
|
||||
|
||||
`useSWPopupStack`, `SWHeaderMore`, `SWHeaderField`/`SWHeaderFieldText`, `SWHeaderKGuide`, `useSWHeaderDropdown`, `SWHeaderMenuDetail`, `SWHeaderColorChip`, `SWHeaderLayerRows`, `SWHeaderColorPanelBody`, `SWHeaderLineStyleColumns`, `SWPopupFrame`, `useSWFixedDropdownPosition`, 속성창 내부 Picker, `useSWHeaderOverlay` 등은 내부 구현입니다. 필요하면 공개 컴포넌트로 대체하거나, 부족한 API로 보고합니다.
|
||||
|
||||
## 8. 알려진 단독 사용 제약
|
||||
|
||||
| 항목 | 내용 | 대응 |
|
||||
|---|---|---|
|
||||
@@ -107,7 +126,7 @@ SW 공식 기준 Header(Figma 26:8944, 1920 × 49). 로고 · 파일 · 객체
|
||||
| Dropdown 위치 | `SWHeader` 안이면 헤더, 밖이면 뷰포트 안으로 좌우 보정. `SWPopup`·속성창 안이면 고정 위치 Overlay | Popup 밖에서는 부모에 `overflow: hidden`을 두지 않는다 |
|
||||
| 파일 열기 뒤 파일명 여백 | `SWToolButton appearance="document"` 바로 뒤의 파일명은 왼쪽 여백 12px(로고 아래가 아니므로) | 패키지 CSS가 처리. 소비 코드에서 조정하지 않음 |
|
||||
|
||||
## 8. 뷰어 모드 (`SWViewer*`)
|
||||
## 9. 뷰어 모드 (`SWViewer*`)
|
||||
|
||||
SW **뷰어 모드** 전용 Export입니다(2026-10-19 추가, 참고 DWG/DXF 뷰어 실측 기준). 모드 선택은 [AI_USAGE.md](./AI_USAGE.md) §0, Props 상세·상태·금지 사항은 [VIEWER.md](./VIEWER.md), 토큰은 [DESIGN_RULES.md](./DESIGN_RULES.md) §11, 조합은 [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 7과 [예제 H](./examples/ExampleH-ViewerMode.tsx)를 봅니다. 일반 모드 화면에서는 쓰지 않습니다. `SWViewerHeader`·`SWViewerMenu`·`SWViewerPanel`은 루트에 `hm-sw-theme`을 가지므로 `<SWTheme>`가 필요 없습니다.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## 0. 어떤 패턴을 고를까
|
||||
|
||||
먼저 SW 모드를 정합니다([AI_USAGE.md](./AI_USAGE.md) §0). 1~6은 **SW 일반 모드**, 7은 **SW 뷰어 모드**이며 한 화면에서 섞지 않습니다.
|
||||
먼저 SW 모드를 정합니다([AI_USAGE.md](./AI_USAGE.md) §0). 1~6과 8은 **SW 일반 모드**, 7은 **SW 뷰어 모드**이며 한 화면에서 섞지 않습니다.
|
||||
|
||||
| 요청 | 모드 | 패턴 |
|
||||
|---|---|---|
|
||||
@@ -15,6 +15,7 @@
|
||||
| SW Popup 디자인의 설정창·정보창·사용자 정의 폼 | 일반 | 5. Popup(외형 고정, 콘텐츠 자유) |
|
||||
| 일반 모드 `SWHeader`에 로고 옆 파일 열기·도구 막대(Model/Layout, Fit, 2.5D) 추가 | 일반 | 6. 파일 열기 + 도구 막대 |
|
||||
| 뷰어·DWG/DXF 보기·도면 뷰어·읽기 전용 화면(Header 메뉴, 패널, 모달, 불러오기 상태) | 뷰어 | 7. 뷰어 모드 기본 조합 |
|
||||
| 일반 모드 `SWHeader`에서 로고를 누르면 열리는 2단 메뉴(저장·내보내기·찾아열기·최근파일 등) | 일반 | 8. 로고 클릭 메뉴 |
|
||||
|
||||
## 1. 완성형 SW Header — [ExampleA-DefaultHeader.tsx](./examples/ExampleA-DefaultHeader.tsx)
|
||||
|
||||
@@ -174,7 +175,32 @@ const groups: SWViewerHeaderGroup[] = [
|
||||
- 상태(파일명·패널 열림·탭 값)는 프로그램이 가지고, 명령은 각 Callback 또는 `onCommand` 한 곳에서 받는다. 도면 영역 높이·렌더러 크기는 앱 레이아웃 담당(VIEWER.md §7).
|
||||
- 없는 기능은 메뉴를 만들지 않거나 `disabled` + 미연결 보고. 패키지에 없는 UI는 [AI_USAGE.md](./AI_USAGE.md) §0.4 뷰어 열을 따른다.
|
||||
|
||||
## 8. 하지 말 것
|
||||
## 8. 로고 클릭 메뉴 (일반 모드) — [ExampleG-HomeMenu.tsx](./examples/ExampleG-HomeMenu.tsx)
|
||||
|
||||
SW **일반 모드** `SWHeader` 전용입니다. 패턴 7(뷰어 모드)에는 홈 메뉴를 넣지 않습니다.
|
||||
|
||||
```tsx
|
||||
const sections: SWHomeMenuSectionData[] = [
|
||||
{ id: "working", title: "작업중인 파일", items: [
|
||||
{ id: "save", label: "저장", icon: <SaveIcon />, toggle: false, onSelect: save },
|
||||
] },
|
||||
{ id: "open", title: "다른 파일 열기", items: [
|
||||
{ id: "find-open", label: "찾아열기", icon: <OpenIcon />, toggle: false, onSelect: openFile },
|
||||
{ id: "recent", label: "최근파일", icon: <RecentIcon />, panel: <SWRecentFiles files={recent} onFileOpen={openRecent} /> },
|
||||
] },
|
||||
]
|
||||
<SWHeader documentTitle={name} documents={documents} onDocumentOpen={openFile} onDocumentNew={newFile}
|
||||
homeMenu={{ sections, onItemSelect }} onHomeMenuOpenChange={(open, reason) => log(open, reason)} />
|
||||
```
|
||||
|
||||
1. 카탈로그 §5의 데이터로 메뉴를 쓴다. 화면마다 타일·섹션·제목을 따로 만들지 않는다.
|
||||
2. 메뉴 데이터 변경: 항목 추가·삭제는 `items` 배열, 숨김은 `hidden`, 비활성은 `disabled`, 실행만 하는 버튼은 `toggle: false`, 켜고 끄는 버튼은 기본값. Active를 직접 제어하려면 `homeMenu.activeIds` + `onActiveChange`.
|
||||
3. 최근파일 패널은 선택 기능이다. 필요한 타일에만 `panel: <SWRecentFiles … />`를 준다. 파일을 열면 메뉴가 닫힌다(`closeOnOpen`).
|
||||
4. 로고 클릭은 메뉴만 연다. `onHomeClick`의 홈 이동은 `homeMenu`가 있으면 실행되지 않는다. 메뉴 항목에 홈 이동이 필요하면 항목 `onSelect`에서 한다.
|
||||
5. New / Open은 문서 Dropdown에 그대로 둔다(`documentOpenPlacement` 기본 `"menu"`). 로고 오른쪽에 별도 Open 버튼을 두지 않는다. 메뉴의 찾아열기·새파일은 같은 handler를 연결한다.
|
||||
6. 위치·폭은 `SWHeader`가 정한다. 소비 코드에서 메뉴 위치를 계산하거나 겹치는 wrapper를 만들지 않는다.
|
||||
|
||||
## 9. 하지 말 것
|
||||
|
||||
- `hm-sw-*` class를 소비 코드에 쓰거나 CSS로 덮어쓰기
|
||||
- `dist` 구현·`styles.css` 블록 복사로 같은 컴포넌트 재작성
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- 토큰은 `.hm-sw-header`와 `.hm-sw-theme`(`<SWTheme>`)에만 정의됩니다. 새 컴포넌트는 그 안에 렌더링합니다.
|
||||
- SW는 `@hm-design/ui` 토큰을 쓰지 않습니다. UI 패키지 토큰을 복제하거나 섞지 않습니다.
|
||||
- Light/Dark 분기 없음. 전역 선택자(`html`, `body`, `*`) 없음.
|
||||
- **모드:** §1~10은 SW **일반 모드**(`SWHeader`·`SWPopup`·`SWToolButton`) 기준입니다. SW **뷰어 모드**(`SWViewer*`)의 토큰·상태는 §11과 [VIEWER.md](./VIEWER.md)를 봅니다. 모드 선택·혼합 금지는 [AI_USAGE.md](./AI_USAGE.md) §0.
|
||||
- **모드:** §1~10과 §12는 SW **일반 모드**(`SWHeader`·`SWPopup`·`SWToolButton`·`SWHomeMenu`) 기준입니다. SW **뷰어 모드**(`SWViewer*`)의 토큰·상태는 §11과 [VIEWER.md](./VIEWER.md)를 봅니다. 모드 선택·혼합 금지는 [AI_USAGE.md](./AI_USAGE.md) §0.
|
||||
|
||||
## 1. 크기·간격
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
|
||||
Container `hm-sw-header` 기준 Breakpoint: **< 1800** 선/문자/치수스타일 → More, **< 1180** 그리기모드 숨김, **< 1080** 상세보기 → More·창 이동 24px(창 영역 176), **< 900** 색상 → More, **< 640** 인쇄·K → More·창 이동 숨김(창 영역 64), **< 520** 레이어 상태 아이콘 숨김. More에서 고른 필드는 첫 칸(레이어 자리)에 표시되고 Dropdown이 바로 열립니다(UI 상태만, 데이터 Callback 없음).
|
||||
|
||||
로고 클릭 메뉴는 좁은 폭에서 접히지 않고 메뉴 안에서 가로 스크롤합니다(좁은 폭 전용 디자인 없음, §12).
|
||||
|
||||
조합 Header(개별 컴포넌트)에는 이 규칙이 적용되지 않습니다. 필요한 컨트롤만 배치하고 폭 정책은 소비 앱이 정합니다.
|
||||
|
||||
## 8. Popup (속성창 프레임, `SWPopup` / `SWHeaderPropertyPanel`)
|
||||
@@ -148,4 +150,23 @@ Container `hm-sw-header` 기준 Breakpoint: **< 1800** 선/문자/치수스타
|
||||
| 상태 표시 | `SWViewerStatus`: 글자 `--hm-sw-viewer-muted` 11px 400, `error`는 `--hm-sw-viewer-accent`, `loading` 아이콘 회전 | — |
|
||||
|
||||
- **Typography(뷰어):** Header 항목 14px 500, 캡션·개수·상태 11px 400, 패널 필드 라벨 14px 500, Section 제목 12px 700, 패널 버튼·Badge 12px 500, Input 12px. Font Weight는 400/500/700만 씁니다.
|
||||
- **뷰어 신규 컴포넌트 체크리스트:** ① VIEWER.md·§8 컴포넌트로 해결되는지 먼저 본다 ② 위 토큰을 `var(--…)`로만 읽고 일반 모드 전용 모양(§8 `SWPopupRow`, §9 `SWToolButton`)을 섞지 않는다 ③ 상태는 위 표대로 ④ 토큰에 없는 값은 결과 보고에 "AI 구성 값 — 디자인 확인 필요"로 적는다 ⑤ `hm-sw-viewer-*` class를 쓰거나 덮어쓰지 않는다.
|
||||
- **뷰어 신규 컴포넌트 체크리스트:** ① VIEWER.md·카탈로그 §9 컴포넌트로 해결되는지 먼저 본다 ② 위 토큰을 `var(--…)`로만 읽고 일반 모드 전용 모양(§8 `SWPopupRow`, §9 `SWToolButton`)을 섞지 않는다 ③ 상태는 위 표대로 ④ 토큰에 없는 값은 결과 보고에 "AI 구성 값 — 디자인 확인 필요"로 적는다 ⑤ `hm-sw-viewer-*` class를 쓰거나 덮어쓰지 않는다.
|
||||
|
||||
## 12. 로고 클릭 메뉴 (`SWHeader homeMenu` / `SWHomeMenu`, Figma 26:10591) — SW 일반 모드 전용
|
||||
|
||||
이 절의 값은 일반 모드 `SWHeader`의 홈 메뉴에만 씁니다. 뷰어 모드(§11)에는 홈 메뉴가 없으며 `--hm-sw-home-menu-*`·`--hm-sw-recent-files-*` 토큰을 뷰어 신규 UI에 쓰지 않습니다.
|
||||
|
||||
| 항목 | 값 | 토큰 |
|
||||
|---|---|---|
|
||||
| 위치 | Header 맨 위(top 0), 객체 수정 영역 왼쪽 −2px ~ 창 영역 왼쪽(1920에서 x 375, 폭 1305). DOM 측정이며 고정 좌표 없음. `controls`로 파일명이 객체 수정 영역 안에 있으면 파일명 오른쪽 −2px부터 시작. 로고·파일명·창 영역은 가리지 않음. `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를 소비 코드에 쓰지 않습니다.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
기준: 2026-10-19, 참고 DWG/DXF 뷰어(`dwg-dxf-viewer-hm`) 상단 메뉴·팝업을 같은 해상도(1440 × 900)에서 실측해 패키지 컴포넌트로 옮긴 결과입니다. 아래 이름은 모두 `from "@hm-design/sw"` 공개 Export이고 CSS는 `@hm-design/sw/styles.css` 한 개입니다. 예제: [examples/ExampleH-ViewerMode.tsx](./examples/ExampleH-ViewerMode.tsx).
|
||||
|
||||
> **모드 선택 먼저:** 이 문서는 SW **뷰어 모드** 전용입니다. 화면이 뷰어 모드인지는 [AI_USAGE.md](./AI_USAGE.md) §0(설정 `HMDesign 사용 유형: SW 뷰어` → 요청 문구 → 불명확하면 질문)으로 정합니다. 전체 Export 목록은 [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §8, 토큰·상태 요약은 [DESIGN_RULES.md](./DESIGN_RULES.md) §11, 조합은 [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 7입니다.
|
||||
> **모드 선택 먼저:** 이 문서는 SW **뷰어 모드** 전용입니다. 화면이 뷰어 모드인지는 [AI_USAGE.md](./AI_USAGE.md) §0(설정 `HMDesign 사용 유형: SW 뷰어` → 요청 문구 → 불명확하면 질문)으로 정합니다. 전체 Export 목록은 [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §9, 토큰·상태 요약은 [DESIGN_RULES.md](./DESIGN_RULES.md) §11, 조합은 [COMPOSITION_PATTERNS.md](./COMPOSITION_PATTERNS.md) 7입니다.
|
||||
>
|
||||
> **일반 모드 금지 사항(상호 참조):** 뷰어 화면에 `SWHeader`·`SWPopup`·`SWPopupSection`·`SWPopupRow`·`SWToolButton`·`SWToolGroup`·`SWHeaderPropertyPanel`을 쓰거나 재스타일하지 않고, 일반 모드 화면에 `SWViewer*`와 `--hm-sw-viewer-*` 토큰을 쓰지 않습니다. `SWViewerPanel`이 내부에서 `SWPopup` 프레임을 렌더링하는 것과 두 Header가 `--hm-sw-header-document-*`·`object-*`·`property-*`·외곽선 토큰을 공유하는 것은 패키지 구현이며 모드 혼합이 아닙니다([AI_USAGE.md](./AI_USAGE.md) §0.3). 패키지에 없는 뷰어 UI는 [AI_USAGE.md](./AI_USAGE.md) §0.4 뷰어 열을 따릅니다.
|
||||
|
||||
@@ -96,6 +96,7 @@ const groups: SWViewerHeaderGroup[] = [
|
||||
| 하지 말 것 | 이유 / 대신 |
|
||||
|---|---|
|
||||
| `<SWViewerHeader logo="HM" />`처럼 로고가 없다고 임의 문자·아이콘 넣기 | 로고는 프로그램이 정합니다. 없으면 생략 |
|
||||
| 뷰어 화면에 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`, `SWHeader homeMenu`) 넣기 또는 비슷한 2단 메뉴 직접 만들기 | 홈 메뉴는 SW 일반 모드 전용입니다. 뷰어 로고 클릭은 `onHomeClick`, 파일 열기·샘플은 `groups`의 `button`·`menu` 항목 |
|
||||
| 뷰어 Header를 `div` + `button`으로 다시 만들기, `hm-sw-viewer-*` class를 소비 코드에 붙이기 | 데이터(`groups`)와 `custom` / `trailing`으로 구성 |
|
||||
| Header 버튼 글자에 `-webkit-text-stroke`·`border`로 외곽선 만들기 | 외곽선은 패키지 토큰이 이미 적용합니다 |
|
||||
| 메뉴 색·간격을 `#123456`, `margin` 등 임의 값으로 덮기 | 토큰 `--hm-sw-viewer-*` / `--hm-sw-header-*`만 `style`로 조정(기본값 유지 권장) |
|
||||
|
||||
@@ -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 }) => (
|
||||
<svg viewBox="0 0 18 18" width="18" height="18" aria-hidden="true"><path d={d} fill="none" stroke="currentColor" strokeWidth="1.6" /></svg>
|
||||
)
|
||||
|
||||
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<SWHomeMenuItemData[]>([])
|
||||
// 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: <Icon d="M3 3h10l2 2v10H3z M6 3v4h6V3" />, toggle: false, onSelect: () => onEvent?.("menu:save") },
|
||||
{ id: "export", label: "내보내기", icon: <Icon d="M9 12V3 M5 7l4-4 4 4 M3 15h12" />, toggle: false, onSelect: () => onEvent?.("menu:export") },
|
||||
] },
|
||||
{ id: "open", title: "다른 파일 열기", items: [
|
||||
{ id: "find-open", label: "찾아열기", icon: <Icon d="M3 5h5l2 2h5v8H3z" />, toggle: false, onSelect: openFile },
|
||||
// Optional sub panel: the tile toggles it (Active = open), the menu shows the outline accent meanwhile.
|
||||
{ id: "recent", label: "최근파일", icon: <Icon d="M9 4v5l3 2 M9 16A7 7 0 1 0 9 2a7 7 0 0 0 0 14" />,
|
||||
panel: <SWRecentFiles files={recentFiles} onFileOpen={file => onEvent?.(`recent:${file.id}`)} />, panelLabel: "최근파일" },
|
||||
{ id: "new-file", label: "새파일", icon: <Icon d="M4 2h7l3 3v11H4z M9 8v6 M6 11h6" />, toggle: false, onSelect: newFile },
|
||||
...extra,
|
||||
] },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<SWHeader
|
||||
documentTitle={documents.find(item => 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. */}
|
||||
<button type="button" style={{ marginTop: 40 }} onClick={() => setExtra(list => [...list, { id: `extra-${list.length + 1}`, label: `추가 ${list.length + 1}`, icon: <Icon d="M9 3v12 M3 9h12" /> }])}>
|
||||
메뉴 항목 추가
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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<HTMLElement, SWHeaderProps>(function SWHeader
|
||||
onPropertyPanelOpenChange,
|
||||
disabled = false,
|
||||
tools,
|
||||
homeMenu,
|
||||
homeMenuOpen,
|
||||
defaultHomeMenuOpen = false,
|
||||
onHomeMenuOpenChange,
|
||||
logo,
|
||||
logoSrc,
|
||||
logoAlt,
|
||||
@@ -260,6 +265,41 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(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<HTMLElement | null>(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<HTMLElement>(".hm-sw-header")
|
||||
homeButtonRef.current = header?.querySelector<HTMLElement>(":scope > .hm-sw-header__bar > .hm-sw-header__home") ?? null
|
||||
if (!homeOpen || !header || !panelAnchorRef.current) return
|
||||
const origin = header.getBoundingClientRect()
|
||||
// With `controls`, the file name sits inside the object area: start after it so it stays visible.
|
||||
const documentRight = fieldsRef.current?.querySelector<HTMLElement>(':scope > [data-control="document"]:not([data-hidden])')?.getBoundingClientRect().right
|
||||
const start = Math.max(panelAnchorRef.current.getBoundingClientRect().left, documentRight ?? -Infinity)
|
||||
const end = header.querySelector<HTMLElement>(":scope > .hm-sw-header__bar > .hm-sw-header__window")?.getBoundingClientRect().left ?? origin.right
|
||||
const next = { left: start - origin.left - 2, width: end - start + 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 +480,9 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(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 +520,8 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
|
||||
return (
|
||||
<header ref={ref} className={`hm-sw-header${className ? ` ${className}` : ""}`} data-leading={openShown ? "open" : hasLeading ? "" : undefined} {...rest}>
|
||||
<div className="hm-sw-header__bar">
|
||||
<SWHeaderHome onClick={shown("home") ? onHomeClick : undefined} interactive={shown("home")} logo={logo} logoSrc={logoSrc} logoAlt={logoAlt} />
|
||||
<SWHeaderHome onClick={!shown("home") ? undefined : homeMenu ? () => 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 && <div ref={leadingRef} className="hm-sw-header__leading">
|
||||
{openShown && <SWToolButton appearance="document" data-document-open="" disabled={disabled} onClick={onDocumentOpen}>{documentOpenLabel ?? "Open"}</SWToolButton>}
|
||||
{leading}
|
||||
@@ -505,6 +547,12 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
|
||||
</div>}
|
||||
</div>
|
||||
{barOpen && barData && <div className="hm-sw-header__bar-anchor" style={{ left: barOffset, maxWidth: `calc(100% - ${barOffset}px)` }}><SWHeaderPropertyBar {...barData} disabled={disabled || propertyBar?.disabled} id={barId} /></div>}
|
||||
{homeMenu && shown("home") && (
|
||||
<div className="hm-sw-header__home-menu" style={{ left: homeBox.left, maxWidth: homeBox.width }}>
|
||||
<SWHomeMenu {...homeMenu} id={homeMenu.id ?? homeMenuId} open={homeOpen} returnFocusRef={homeButtonRef}
|
||||
onOpenChange={(next, reason) => setHomeOpen(next, reason)} />
|
||||
</div>
|
||||
)}
|
||||
{panelOpen && propertyPanel && <SWHeaderPropertyPanel {...propertyPanel} menus={{ showLayerFreeze: layerStates.showFreeze, showLayerLock: layerStates.showLock, ...propertyPanel.menus }} id={panelId} anchorRef={panelAnchorRef} onClose={closePanel} />}
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -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 ? <img src={logoSrc} alt="" /> : null)
|
||||
if (!interactive) return (
|
||||
<div className="hm-sw-header__home" data-static="" role={logoAlt ? "img" : undefined} aria-label={logoAlt}>
|
||||
@@ -30,7 +33,7 @@ export function SWHeaderHome({ onClick, logoSrc, logo, logoAlt, interactive = tr
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<button type="button" className="hm-sw-header__home" aria-label={logoAlt ? `${logoAlt} 홈` : "홈"} onClick={onClick}>
|
||||
<button type="button" className="hm-sw-header__home" aria-label={logoAlt ? `${logoAlt} 홈` : "홈"} aria-expanded={expanded} aria-controls={controls} onClick={onClick}>
|
||||
<img className="hm-sw-header__home-bg" src={homeBackground} alt="" aria-hidden="true" />
|
||||
<img className="hm-sw-header__home-bg-hover" src={homeBackgroundHover} alt="" aria-hidden="true" />
|
||||
{content && <span className="hm-sw-header__logo" aria-hidden="true">{content}</span>}
|
||||
|
||||
@@ -272,7 +272,8 @@ function onDocumentEscape(event: KeyboardEvent) {
|
||||
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return
|
||||
popupStack.at(-1)?.close()
|
||||
}
|
||||
function useSWPopupStack(onClose?: () => void) {
|
||||
/** Internal (not exported by the package): also used by the open SWHomeMenu, which closes like a popup. */
|
||||
export function useSWPopupStack(onClose?: () => void) {
|
||||
const entry = useRef({ close: () => {} })
|
||||
entry.current.close = () => onClose?.()
|
||||
useEffect(() => {
|
||||
|
||||
@@ -31,3 +31,4 @@ export type {
|
||||
|
||||
export { SWHeaderPropertyBar, type SWHeaderPropertyBarProps } from "./components/SWHeaderPropertyBar"
|
||||
export type { SWHeaderControlId, SWHeaderControlConfig, SWHeaderFrameControlId, SWHeaderSubControlId, SWHeaderHideableControlId } from "./types"
|
||||
export type { SWHeaderHomeMenu, SWHeaderHomeMenuChangeReason } from "./types"
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { HTMLAttributes, PointerEventHandler, ReactNode } from "react"
|
||||
import type { SWHeaderPropertyBarProps } from "./components/SWHeaderPropertyBar"
|
||||
import type { SWHeaderPropertyPanelProps } from "./components/SWHeaderPropertyPanel"
|
||||
import type { SWHomeMenuCloseReason, SWHomeMenuProps } from "../sw-home-menu/types"
|
||||
|
||||
/** Header controls that can be shown as selected (text and arrow turn #FFC600). */
|
||||
export type SWHeaderMenuId = "layer" | "color" | "lineStyle" | "textStyle" | "dimensionStyle"
|
||||
@@ -146,6 +147,18 @@ export type SWHeaderFrameControlId = "home" | "windowMove" | "minimize" | "close
|
||||
export type SWHeaderSubControlId = "documentNew" | "documentOpen" | "layerFreeze" | "layerLock"
|
||||
/** Every Header part except the logo, which is always shown. */
|
||||
export type SWHeaderHideableControlId = SWHeaderControlId | SWHeaderFrameControlId | SWHeaderSubControlId
|
||||
/**
|
||||
* Logo (home) menu of SWHeader: the SWHomeMenu props without the open state, which SWHeader owns
|
||||
* (`homeMenuOpen` / `defaultHomeMenuOpen` / `onHomeMenuOpenChange`), and without the focus target (the logo).
|
||||
*/
|
||||
export type SWHeaderHomeMenu = Omit<SWHomeMenuProps, "open" | "defaultOpen" | "onOpenChange" | "returnFocusRef">
|
||||
|
||||
/**
|
||||
* Why the logo menu opened or closed: the SWHomeMenu reasons, `home` (logo click / re-click) or `document`
|
||||
* (the document dropdown opened, which closes the menu).
|
||||
*/
|
||||
export type SWHeaderHomeMenuChangeReason = SWHomeMenuCloseReason | "home" | "document"
|
||||
|
||||
export interface SWHeaderControlConfig {
|
||||
id: SWHeaderControlId
|
||||
visible?: boolean
|
||||
@@ -323,6 +336,17 @@ export interface SWHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"
|
||||
disabled?: boolean
|
||||
/** Extra tools rendered inside the object area, after the menus. */
|
||||
tools?: ReactNode
|
||||
/**
|
||||
* Logo (home) menu (Figma 26:10591): hidden by default, a logo click opens it and a second click closes it.
|
||||
* It is placed from the measured header (object area − 2px up to the window area). While it is set, a logo
|
||||
* click only toggles the menu and never calls `onHomeClick` (no unintended home navigation).
|
||||
*/
|
||||
homeMenu?: SWHeaderHomeMenu
|
||||
/** Controlled open state of `homeMenu`. */
|
||||
homeMenuOpen?: boolean
|
||||
/** Uncontrolled initial state of `homeMenu`. Default false. */
|
||||
defaultHomeMenuOpen?: boolean
|
||||
onHomeMenuOpenChange?: (open: boolean, reason: SWHeaderHomeMenuChangeReason) => void
|
||||
/**
|
||||
* Logo image URL of the SW program, contained in the 32 x 17 logo slot.
|
||||
* The logo is decided by each program: none is rendered when omitted.
|
||||
@@ -333,6 +357,7 @@ export interface SWHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title"
|
||||
/** Accessible product name; the home button is announced as "<logoAlt> 홈" (default "홈"). */
|
||||
logoAlt?: string
|
||||
|
||||
/** Logo click. Not called while `homeMenu` is set (the click toggles the menu instead). */
|
||||
onHomeClick?: () => void
|
||||
onDocumentClick?: () => void
|
||||
onLayerOpen?: () => void
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
import {
|
||||
createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState,
|
||||
type ButtonHTMLAttributes, type CSSProperties, type KeyboardEvent, type MouseEvent, type ReactNode,
|
||||
} from "react"
|
||||
import { useSWPopupStack } from "../sw-header/components/SWHeaderPropertyPanel"
|
||||
import titleBadge from "./assets/title-badge.svg"
|
||||
// Imported where the component is used (not only in index.ts) so bundlers keep it with sideEffects: ["**/*.css"].
|
||||
import "../../theme/sw-home-menu.css"
|
||||
import type {
|
||||
SWHomeMenuCloseReason, SWHomeMenuItemData, SWHomeMenuProps, SWHomeMenuSectionData, SWHomeMenuSelectEvent,
|
||||
} from "./types"
|
||||
|
||||
interface WindowContextValue {
|
||||
isActive: (id: string) => boolean
|
||||
select: (item: SWHomeMenuItemData, section?: SWHomeMenuSectionData) => boolean
|
||||
registerPanel: (id: string, panel: ReactNode | undefined, label: string) => void
|
||||
panelId: string
|
||||
close: (reason: SWHomeMenuCloseReason) => void
|
||||
}
|
||||
const WindowContext = createContext<WindowContextValue | null>(null)
|
||||
const SectionContext = createContext<SWHomeMenuSectionData | undefined>(undefined)
|
||||
|
||||
/** For content inside a panel (e.g. SWRecentFiles): close the whole menu after opening a file. */
|
||||
export function useSWHomeMenuClose() {
|
||||
return useContext(WindowContext)?.close
|
||||
}
|
||||
|
||||
const ITEM_SELECTOR = ".hm-sw-home-menu__item:not(:disabled)"
|
||||
|
||||
/**
|
||||
* While open, the menu is on the SW popup Escape stack (SWPopup / property window): Escape with the focus outside
|
||||
* every popup and the menu closes only the most recently used one (opened, focused or pressed last).
|
||||
*/
|
||||
function SWHomeMenuEscapeEntry({ onEscape, raiseRef }: { onEscape: () => void; raiseRef: { current: (() => void) | null } }) {
|
||||
raiseRef.current = useSWPopupStack(onEscape)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Figma 26:10591 "헤더/꾸러미 ver2/home on": the 2-tier header menu opened by the SW logo (home) button. Hidden by
|
||||
* default; the host toggles `open` from SWHeader `onHomeClick` (logo click / re-click). Sections of 80 x 46 tiles;
|
||||
* a tile may open an optional sub panel under the menu (Figma 최근파일 662:67331 → <SWRecentFiles>). Every text,
|
||||
* icon, item and action is host data. Only the menu is drawn: the home button and window area stay those of
|
||||
* <SWHeader>. Render it inside <SWTheme> (or next to SWHeader).
|
||||
*/
|
||||
export const SWHomeMenu = forwardRef<HTMLDivElement, SWHomeMenuProps>(function SWHomeMenu({
|
||||
sections, children, open: openProp, defaultOpen = false, onOpenChange, activeIds: activeProp, defaultActiveIds,
|
||||
onActiveChange, selection = "multiple", onItemSelect, closeOnBackgroundClick = true, closeOnSelect = false,
|
||||
closeOnEscape = true, closeOnOutsideClick = false, autoFocus = true, returnFocusRef, accent = "auto",
|
||||
className, style, onClick, onKeyDown, ...rest
|
||||
}, ref) {
|
||||
const [internalOpen, setInternalOpen] = useState(defaultOpen)
|
||||
const open = openProp ?? internalOpen
|
||||
const [internalActive, setInternalActive] = useState<readonly string[]>(defaultActiveIds ?? [])
|
||||
const activeIds = activeProp ?? internalActive
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
useImperativeHandle(ref, () => rootRef.current as HTMLDivElement)
|
||||
const previousFocus = useRef<HTMLElement | null>(null)
|
||||
const panelId = useId()
|
||||
// Panels of the rendered tiles (id → content); the open one is the Active tile that has a panel.
|
||||
const [panels, setPanels] = useState<Record<string, { panel: ReactNode; label: string }>>({})
|
||||
const registerPanel = useCallback((id: string, panel: ReactNode | undefined, label: string) => {
|
||||
setPanels(current => {
|
||||
if (panel == null) {
|
||||
if (!(id in current)) return current
|
||||
const { [id]: _removed, ...next } = current
|
||||
return next
|
||||
}
|
||||
if (current[id]?.panel === panel && current[id]?.label === label) return current
|
||||
return { ...current, [id]: { panel, label } }
|
||||
})
|
||||
}, [])
|
||||
const openPanelId = activeIds.find(id => id in panels)
|
||||
const latest = useRef({ activeIds, onActiveChange, onItemSelect, selection, closeOnSelect, panels })
|
||||
latest.current = { activeIds, onActiveChange, onItemSelect, selection, closeOnSelect, panels }
|
||||
|
||||
const setActive = useCallback((next: string[], event: SWHomeMenuSelectEvent) => {
|
||||
if (activeProp === undefined) setInternalActive(next)
|
||||
latest.current.onActiveChange?.(next, event)
|
||||
}, [activeProp])
|
||||
|
||||
const close = useCallback((reason: SWHomeMenuCloseReason) => {
|
||||
const root = rootRef.current
|
||||
// Focus inside the menu would be lost when it unmounts: return it to the trigger / previous element.
|
||||
if (root && root.contains(document.activeElement)) (returnFocusRef?.current ?? previousFocus.current)?.focus()
|
||||
// A sub panel closes with the menu: its tile returns to Default (the next open starts without it).
|
||||
const { activeIds: current, panels: known } = latest.current
|
||||
const openPanel = current.find(id => id in known)
|
||||
if (openPanel) setActive(current.filter(id => id !== openPanel), { item: { id: openPanel, label: known[openPanel].label }, active: false })
|
||||
if (openProp === undefined) setInternalOpen(false)
|
||||
onOpenChange?.(false, reason)
|
||||
}, [openProp, onOpenChange, returnFocusRef, setActive])
|
||||
|
||||
// Opening (not the first render): remember the focus to return to, then focus the first tile.
|
||||
const wasOpen = useRef(open)
|
||||
useLayoutEffect(() => {
|
||||
if (open && !wasOpen.current) {
|
||||
const active = document.activeElement
|
||||
previousFocus.current = active instanceof HTMLElement && !rootRef.current?.contains(active) ? active : null
|
||||
if (autoFocus) rootRef.current?.querySelector<HTMLElement>(ITEM_SELECTOR)?.focus()
|
||||
}
|
||||
wasOpen.current = open
|
||||
}, [open, autoFocus])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !closeOnOutsideClick) return
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
const target = event.target as Node
|
||||
if (rootRef.current?.contains(target) || returnFocusRef?.current?.contains(target)) return
|
||||
close("outside")
|
||||
}
|
||||
document.addEventListener("pointerdown", onPointerDown)
|
||||
return () => document.removeEventListener("pointerdown", onPointerDown)
|
||||
}, [open, closeOnOutsideClick, close, returnFocusRef])
|
||||
|
||||
// Panel position: under the menu, 1px right of its tile's left edge (Figma 최근파일 808 vs tile 807).
|
||||
const [panelLeft, setPanelLeft] = useState(0)
|
||||
useLayoutEffect(() => {
|
||||
if (!openPanelId || !rootRef.current) return
|
||||
const root = rootRef.current
|
||||
const measure = () => {
|
||||
const tile = root.querySelector<HTMLElement>(`[data-item-id="${CSS.escape(openPanelId)}"]`)
|
||||
if (tile) setPanelLeft(tile.getBoundingClientRect().left - root.getBoundingClientRect().left + 1)
|
||||
}
|
||||
measure()
|
||||
const scroller = root.querySelector(".hm-sw-home-menu__sections")
|
||||
scroller?.addEventListener("scroll", measure)
|
||||
const observer = new ResizeObserver(measure)
|
||||
observer.observe(root)
|
||||
return () => { scroller?.removeEventListener("scroll", measure); observer.disconnect() }
|
||||
}, [openPanelId])
|
||||
|
||||
const context = useMemo<WindowContextValue>(() => ({
|
||||
isActive: id => activeIds.includes(id),
|
||||
registerPanel,
|
||||
panelId,
|
||||
close,
|
||||
select: (item, section) => {
|
||||
const { activeIds: current, onItemSelect: notify, selection: mode, closeOnSelect: closeAfter, panels: known } = latest.current
|
||||
const toggle = item.toggle !== false
|
||||
const active = toggle && !current.includes(item.id)
|
||||
const event: SWHomeMenuSelectEvent = { item, section, active }
|
||||
if (toggle) {
|
||||
let next = active ? (mode === "single" ? [item.id] : [...current, item.id]) : current.filter(id => id !== item.id)
|
||||
// One sub panel at a time: opening a panel tile closes the other panel tile.
|
||||
if (active && item.panel != null) next = next.filter(id => id === item.id || !(id in known))
|
||||
setActive(next, event)
|
||||
}
|
||||
item.onSelect?.(event)
|
||||
notify?.(event)
|
||||
if (closeAfter && item.panel == null) close("select")
|
||||
return active
|
||||
},
|
||||
}), [activeIds, registerPanel, panelId, close, setActive])
|
||||
|
||||
const raiseRef = useRef<(() => void) | null>(null)
|
||||
if (!open) return null
|
||||
|
||||
/** Escape: an open sub panel closes first (focus back on its tile when it was inside), then the menu. */
|
||||
const escape = () => {
|
||||
if (openPanelId) {
|
||||
const item = { id: openPanelId, label: panels[openPanelId]?.label ?? openPanelId }
|
||||
const focusInside = rootRef.current?.contains(document.activeElement)
|
||||
setActive(activeIds.filter(id => id !== openPanelId), { item, active: false })
|
||||
if (focusInside) rootRef.current?.querySelector<HTMLElement>(`[data-item-id="${CSS.escape(openPanelId)}"]`)?.focus()
|
||||
return
|
||||
}
|
||||
close("escape")
|
||||
}
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLDivElement>) => {
|
||||
onClick?.(event)
|
||||
if (event.defaultPrevented || !closeOnBackgroundClick) return
|
||||
const target = event.target as Element
|
||||
if (!target.closest("button, a, input, [role='listbox'], .hm-sw-home-menu__panel")) close("background")
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
onKeyDown?.(event)
|
||||
if (event.defaultPrevented) return
|
||||
if (event.key === "Escape" && closeOnEscape) {
|
||||
// preventDefault: the document-level popup Escape handler ignores it (one Escape closes one layer).
|
||||
event.preventDefault()
|
||||
escape()
|
||||
return
|
||||
}
|
||||
// ArrowDown on the tile of the open sub panel moves into the panel (keyboard access, not in Figma).
|
||||
if (event.key === "ArrowDown" && openPanelId && (document.activeElement as HTMLElement | null)?.dataset.itemId === openPanelId) {
|
||||
const first = rootRef.current?.querySelector<HTMLElement>(".hm-sw-home-menu__panel :is(button, [href], input, [tabindex]):not(:disabled, [tabindex='-1'])")
|
||||
if (first) { event.preventDefault(); first.focus() }
|
||||
return
|
||||
}
|
||||
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return
|
||||
const items = Array.from(rootRef.current?.querySelectorAll<HTMLElement>(ITEM_SELECTOR) ?? [])
|
||||
const index = items.indexOf(document.activeElement as HTMLElement)
|
||||
if (index < 0) return
|
||||
event.preventDefault()
|
||||
const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1
|
||||
: (index + (event.key === "ArrowRight" ? 1 : -1) + items.length) % items.length
|
||||
items[next].focus()
|
||||
}
|
||||
|
||||
const resolvedAccent = accent === "auto" ? (openPanelId ? "outline" : "sections") : accent
|
||||
const openPanel = openPanelId ? panels[openPanelId] : undefined
|
||||
|
||||
return (
|
||||
<WindowContext.Provider value={context}>
|
||||
<div
|
||||
role="region"
|
||||
aria-label={rest["aria-label"] ?? "홈 메뉴"}
|
||||
{...rest}
|
||||
ref={rootRef}
|
||||
className={`hm-sw-home-menu${className ? ` ${className}` : ""}`}
|
||||
data-accent={resolvedAccent}
|
||||
data-panel-open={openPanel ? "true" : undefined}
|
||||
style={{ "--hm-sw-home-menu-title-art": `url("${titleBadge}")`, ...style } as CSSProperties}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocusCapture={event => { raiseRef.current?.(); rest.onFocusCapture?.(event) }}
|
||||
onPointerDownCapture={event => { raiseRef.current?.(); rest.onPointerDownCapture?.(event) }}
|
||||
>
|
||||
{closeOnEscape && <SWHomeMenuEscapeEntry onEscape={escape} raiseRef={raiseRef} />}
|
||||
<div className="hm-sw-home-menu__sections">
|
||||
{sections
|
||||
? sections.filter(section => !section.hidden).map(section => (
|
||||
<SWHomeMenuSection key={section.id} section={section} title={section.title}>
|
||||
{section.items.filter(item => !item.hidden).map(item => <SWHomeMenuItem key={item.id} item={item} />)}
|
||||
</SWHomeMenuSection>
|
||||
))
|
||||
: children}
|
||||
</div>
|
||||
{openPanel && (
|
||||
<div className="hm-sw-home-menu__panel" id={panelId} role="group" aria-label={openPanel.label} style={{ left: panelLeft }}>
|
||||
{openPanel.panel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</WindowContext.Provider>
|
||||
)
|
||||
})
|
||||
|
||||
export interface SWHomeMenuSectionProps {
|
||||
title: string
|
||||
children: ReactNode
|
||||
/** Data record passed back in select events (set automatically in data mode). */
|
||||
section?: SWHomeMenuSectionData
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** Figma section (e.g. Frame 2408): title badge + tile group. Its width follows the number of tiles. */
|
||||
export function SWHomeMenuSection({ title, children, section, className }: SWHomeMenuSectionProps) {
|
||||
const titleId = useId()
|
||||
return (
|
||||
<SectionContext.Provider value={section}>
|
||||
<div className={`hm-sw-home-menu__section${className ? ` ${className}` : ""}`} role="group" aria-labelledby={titleId}>
|
||||
<span className="hm-sw-home-menu__title" id={titleId}>{title}</span>
|
||||
<div className="hm-sw-home-menu__group">{children}</div>
|
||||
</div>
|
||||
</SectionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export interface SWHomeMenuItemProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onSelect" | "children"> {
|
||||
/** Data mode: the whole record. Otherwise give `itemId` + `label` (+ `icon`, `panel`, …). */
|
||||
item?: SWHomeMenuItemData
|
||||
itemId?: string
|
||||
label?: string
|
||||
icon?: ReactNode
|
||||
hoverIcon?: ReactNode
|
||||
toggle?: boolean
|
||||
panel?: ReactNode
|
||||
panelLabel?: string
|
||||
/** Outside <SWHomeMenu> only: controlled / initial Active and its change. Inside, the menu owns Active. */
|
||||
active?: boolean
|
||||
defaultActive?: boolean
|
||||
onActiveChange?: (active: boolean) => void
|
||||
onSelect?: (event: SWHomeMenuSelectEvent) => void
|
||||
}
|
||||
|
||||
/** Figma tile 80 x 46 with Default / Variant2 (Hover) / Variant3 (Active, same look as Variant2). */
|
||||
export function SWHomeMenuItem({
|
||||
item: itemProp, itemId, label, icon, hoverIcon, toggle, panel, panelLabel, active: activeProp, defaultActive = false,
|
||||
onActiveChange, onSelect, disabled, title, className, onClick, ...rest
|
||||
}: SWHomeMenuItemProps) {
|
||||
const host = useContext(WindowContext)
|
||||
const section = useContext(SectionContext)
|
||||
const generatedId = useId()
|
||||
const item: SWHomeMenuItemData = itemProp ?? {
|
||||
id: itemId ?? generatedId, label: label ?? "", icon, hoverIcon, toggle, panel, panelLabel, disabled, title, onSelect,
|
||||
"aria-label": rest["aria-label"],
|
||||
}
|
||||
const [ownActive, setOwnActive] = useState(defaultActive)
|
||||
const toggles = item.toggle !== false
|
||||
const active = toggles && (host ? host.isActive(item.id) : activeProp ?? ownActive)
|
||||
const isDisabled = item.disabled ?? disabled
|
||||
const hasPanel = host != null && item.panel != null
|
||||
|
||||
const register = host?.registerPanel
|
||||
useLayoutEffect(() => {
|
||||
if (!register) return
|
||||
register(item.id, item.panel ?? undefined, item.panelLabel ?? item.label)
|
||||
return () => register(item.id, undefined, "")
|
||||
}, [register, item.id, item.panel, item.panelLabel, item.label])
|
||||
|
||||
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
onClick?.(event)
|
||||
if (event.defaultPrevented) return
|
||||
if (host) {
|
||||
host.select(item, section)
|
||||
return
|
||||
}
|
||||
const next = toggles && !active
|
||||
if (toggles) {
|
||||
if (activeProp === undefined) setOwnActive(next)
|
||||
onActiveChange?.(next)
|
||||
}
|
||||
item.onSelect?.({ item, active: next })
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
{...rest}
|
||||
className={`hm-sw-home-menu__item${className ? ` ${className}` : ""}`}
|
||||
data-item-id={item.id}
|
||||
data-active={active ? "true" : undefined}
|
||||
data-has-hover-icon={item.hoverIcon ? "" : undefined}
|
||||
aria-pressed={toggles && !hasPanel ? active : undefined}
|
||||
aria-expanded={hasPanel ? active : undefined}
|
||||
aria-controls={hasPanel && active ? host.panelId : undefined}
|
||||
aria-label={item["aria-label"]}
|
||||
title={item.title ?? item.label}
|
||||
disabled={isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{item.icon != null && <span className="hm-sw-home-menu__icon" aria-hidden="true">{item.icon}</span>}
|
||||
{item.hoverIcon != null && <span className="hm-sw-home-menu__icon hm-sw-home-menu__icon--hover" aria-hidden="true">{item.hoverIcon}</span>}
|
||||
<span className="hm-sw-home-menu__label">{item.label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -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<HTMLAttributes<HTMLDivElement>, "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 <SWHomeMenu>: 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<HTMLDivElement>(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<HTMLDivElement>) => {
|
||||
onKeyDown?.(event)
|
||||
if (event.defaultPrevented || !["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return
|
||||
const rows = Array.from(listRef.current?.querySelectorAll<HTMLElement>(".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 (
|
||||
<div {...rest} className={`hm-sw-recent-files${className ? ` ${className}` : ""}`} onKeyDown={handleKeyDown}
|
||||
onMouseLeave={() => show(null)}>
|
||||
<div ref={listRef} className="hm-sw-recent-files__list" id={listId} role="list" aria-label={rest["aria-label"] ?? "최근 파일"}
|
||||
onScroll={() => hovered && setHovered(null)}>
|
||||
{files.length === 0 && <div className="hm-sw-recent-files__empty" role="listitem">{emptyText}</div>}
|
||||
{files.map(file => (
|
||||
<div key={file.id} role="listitem" className="hm-sw-recent-files__item">
|
||||
<button type="button" className="hm-sw-recent-files__row" data-file-id={file.id}
|
||||
data-hovered={hovered?.id === file.id ? "true" : undefined} disabled={file.disabled}
|
||||
title={file.title ?? (file.path ? `${file.name}\n${file.path}` : file.name)}
|
||||
onMouseEnter={event => show(file, event.currentTarget)} onFocus={event => show(file, event.currentTarget)}
|
||||
onClick={() => open(file)}>
|
||||
{file.badge != null && <span className="hm-sw-recent-files__badge" aria-hidden="true">{file.badge}</span>}
|
||||
<span className="hm-sw-recent-files__text">
|
||||
<span className="hm-sw-recent-files__name">{file.name}</span>
|
||||
{file.path && <span className="hm-sw-recent-files__path">{file.path}</span>}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{current && preview != null && (
|
||||
<div className="hm-sw-recent-files__preview" style={{ top: hovered?.top }} aria-hidden="true">
|
||||
<span className="hm-sw-recent-files__preview-title">{current.previewTitle ?? current.path ?? current.name}</span>
|
||||
<div className="hm-sw-recent-files__preview-body">{preview}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="132" height="27" viewBox="0 0 132 27" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Union" filter="url(#filter0_d_0_457)">
|
||||
<use xlink:href="#stroke0_0_457" fill="url(#paint0_linear_0_457)"/>
|
||||
<mask id="stroke1_0_457" fill="white">
|
||||
<use xlink:href="#stroke0_0_457"/>
|
||||
</mask>
|
||||
<path d="M27 6H4C9.02879 6.86861 12.6686 11.6685 16.1248 16.226C19.5413 20.7314 22.7783 25 27 25H28H104H105C109.222 25 112.459 20.7314 115.875 16.226C119.331 11.6685 122.971 6.86861 128 6H105H104H28H27Z" stroke-width="2" stroke="url(#paint1_linear_0_457)" mask="url(#stroke1_0_457)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_0_457" x="0" y="0" width="132" height="27" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_457"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_457" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_0_457" x1="66.3862" y1="6" x2="66.3862" y2="25" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#887757"/>
|
||||
<stop offset="0.755507" stop-color="#A8997C"/>
|
||||
<stop offset="1" stop-color="#C3B79E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_0_457" x1="66.3862" y1="6" x2="66.3862" y2="28" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#7B6332"/>
|
||||
<stop offset="1" stop-color="#9F8D68"/>
|
||||
</linearGradient>
|
||||
<path id="stroke0_0_457" d="M4 6H27H28H104H105H128C122.971 6.86861 119.331 11.6685 115.875 16.226C112.459 20.7314 109.222 25 105 25H104H28H27C22.7783 25 19.5413 20.7314 16.1248 16.226C12.6686 11.6685 9.02879 6.86861 4 6Z" fill-rule="evenodd" clip-rule="evenodd"/>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -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"
|
||||
@@ -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. <SWRecentFiles> (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<HTMLAttributes<HTMLDivElement>, "onSelect" | "title" | "children"> {
|
||||
/** Data mode. Use either `sections` or `children` (<SWHomeMenuSection>). */
|
||||
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<HTMLElement | null>
|
||||
accent?: SWHomeMenuAccent
|
||||
"aria-label"?: string
|
||||
}
|
||||
|
||||
/** One row of <SWRecentFiles> (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
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export {
|
||||
SWPopup, SWPopupSection, SWPopupRow,
|
||||
type SWPopupProps, type SWPopupSectionProps, type SWPopupRowProps,
|
||||
} from "./components/sw-popup/SWPopup"
|
||||
export * from "./components/sw-home-menu"
|
||||
|
||||
// SW viewer mode (2026-10-19): viewer header, dropdown, popup panel and panel controls.
|
||||
export * from "./components/sw-viewer"
|
||||
@@ -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; }
|
||||
@@ -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; }
|
||||
@@ -31,7 +31,7 @@ const docs = (await readdir(path.join(root, "ai"))).filter(name => name.endsWith
|
||||
for (const doc of docs) {
|
||||
const text = await readFile(path.join(root, "ai", doc), "utf8")
|
||||
const notPublic = new Set([...(text.split(/^## .*공개하지 않은 것.*$/m)[1]?.split(/^## /m)[0] ?? "").matchAll(/`(\w+)`/g)].map(match => match[1]))
|
||||
const mentioned = [...new Set([...text.matchAll(/`((?:SW|parseSW|swHeader|useSW)\w+)/g)].map(match => match[1]))]
|
||||
const mentioned = [...new Set([...text.matchAll(/`((?:SW|parseSW|swHeader|useSW)\w+)(?![\w*])/g)].map(match => match[1]))]
|
||||
const unknown = mentioned.filter(name => !publicNames.has(name) && !notPublic.has(name) && !/^SWSearchControl/.test(name))
|
||||
check(`${doc}: named SW identifiers are public exports`, unknown.length === 0, { mentioned: mentioned.length, unknown })
|
||||
const leaked = [...notPublic].filter(name => publicNames.has(name))
|
||||
|
||||
@@ -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 <evidence-dir> (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, Popup, Home Menu, Viewer (SW/Tool removed 2026-10-19); no SW/Header/Options', ['SW/Header', 'SW/Popup', 'SW/Home Menu', 'SW/Viewer'].every(t => titles.includes(t)) && !titles.includes('SW/Tool') && !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
|
||||
@@ -212,7 +212,7 @@ import { Button, Card, CardContent, FilterBar, FilterBarActions, FilterBarSearch
|
||||
### 예시 B1 — SW 일반 모드 (편집 Header + 도구 막대 + 설정 Popup)
|
||||
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(일반 모드) → COMPOSITION_PATTERNS "6. 파일 열기 + 도구 막대", 예제 `ai/examples/ExampleF-ViewerToolbar.tsx`(파일 이름의 Viewer는 과거 명칭이며 일반 모드 `SWHeader` 예제)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup)
|
||||
- 재사용: `SWHeader`(`documentOpenPlacement="header"`, `controls`, `tools`), `SWToolGroup` + `SWToolButton`(Model/Layout 탭, Fit 등), `SWPopup` + `SWPopupSection` + `SWPopupRow` + `SWHeaderInput`(설정 Popup), 로고(홈) 버튼 메뉴는 `SWHeader`의 `homeMenu` + `SWRecentFiles`(**일반 모드 전용**, 예제 `ExampleG-HomeMenu.tsx`)
|
||||
- 신규: SW에 없는 기능(예: 검색) → 소비 프로젝트 컴포넌트, `<SWTheme>` 안, `--hm-sw-header-*` 토큰, 안쪽 입력은 `SWHeaderInput`(예제 `ExampleD-SearchExtension.tsx`)
|
||||
|
||||
```tsx
|
||||
@@ -225,7 +225,7 @@ import "@hm-design/sw/styles.css"
|
||||
- 읽기: `node_modules/@hm-design/sw/ai/` → AI_USAGE §0(뷰어 모드) → `VIEWER.md`, 예제 `ai/examples/ExampleH-ViewerMode.tsx`
|
||||
- 재사용: `SWViewerHeader`(`groups` 데이터: `button`·`toggle`·`menu`·`tabs`·`switch`·`custom` 항목, `onCommand`, `trailing`에 `SWViewerStatus`·`SWViewerHeaderButton`), Dropdown은 `type: "menu"` 항목의 `entries`(단독·우클릭은 `SWViewerMenu`), 팝업은 `SWViewerPanel` + `SWViewerSection`·`SWViewerField`·`SWViewerSwitch`·`SWViewerSlider`·`SWViewerInput`·`SWViewerButton`
|
||||
- 신규: 뷰어에 없는 패널 콘텐츠 → 소비 프로젝트 컴포넌트를 `SWViewerPanel`의 `children`에, 안쪽은 `SWViewerField` + `SWViewerInput` 등, 외형은 `--hm-sw-viewer-*` 토큰
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
- 하지 않음: `SWHeader`·`SWPopup`을 뷰어 화면에 섞기, 로고(홈) 메뉴(`SWHomeMenu`·`SWRecentFiles`·`homeMenu`)를 뷰어 화면에 넣거나 비슷하게 만들기(홈 메뉴는 일반 모드 전용), 로고가 없다고 임의 문자·아이콘 넣기, 연결할 명령이 없는 메뉴를 가짜 동작으로 만들기(제외 또는 `disabled` + 미연결 보고)
|
||||
|
||||
```tsx
|
||||
import { SWViewerHeader, SWViewerPanel, SWViewerSection, SWViewerField, SWViewerSwitch, type SWViewerHeaderGroup } from "@hm-design/sw"
|
||||
|
||||
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,26 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="inventory_2">
|
||||
<g id="Vector" filter="url(#filter0_dd_0_10)">
|
||||
<path d="M17.5 4H5.5C4.75 4 4 4.675 4 5.5V7.7575C4 8.2975 4.3225 8.7625 4.75 9.025V16.5C4.75 17.325 5.575 18 6.25 18H16.75C17.425 18 18.25 17.325 18.25 16.5V9.025C18.6775 8.7625 19 8.2975 19 7.7575V5.5C19 4.675 18.25 4 17.5 4ZM16.75 16.5H6.25V9.25H16.75V16.5ZM17.5 7.75H5.5V5.5H17.5V7.75Z" fill="#FF5C00"/>
|
||||
<path d="M13.75 11.5H9.25V13H13.75V11.5Z" fill="#FF5C00"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_10" x="0" y="0" width="23" height="22" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_10"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_10" result="effect2_dropShadow_0_10"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_10" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,26 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="inventory_2">
|
||||
<g id="Vector" filter="url(#filter0_dd_0_451)">
|
||||
<path d="M17.5 4H5.5C4.75 4 4 4.675 4 5.5V7.7575C4 8.2975 4.3225 8.7625 4.75 9.025V16.5C4.75 17.325 5.575 18 6.25 18H16.75C17.425 18 18.25 17.325 18.25 16.5V9.025C18.6775 8.7625 19 8.2975 19 7.7575V5.5C19 4.675 18.25 4 17.5 4ZM16.75 16.5H6.25V9.25H16.75V16.5ZM17.5 7.75H5.5V5.5H17.5V7.75Z" fill="#B6B4AF"/>
|
||||
<path d="M13.75 11.5H9.25V13H13.75V11.5Z" fill="#B6B4AF"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_451" x="0" y="0" width="23" height="22" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_451"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_451" result="effect2_dropShadow_0_451"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_451" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,28 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="22" viewBox="0 0 24 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="monitor" clip-path="url(#clip0_0_10)">
|
||||
<g id="Vector" filter="url(#filter0_dd_0_10)">
|
||||
<path d="M18.4 3.55H5.6C4.72 3.55 4 4.27 4 5.15V13.95C4 14.83 4.72 15.55 5.6 15.55H9.6L8.8 16.35V17.95H15.2V16.35L14.4 15.55H18.4C19.28 15.55 20 14.83 20 13.95V5.15C20 4.27 19.28 3.55 18.4 3.55ZM18.4 13.95H5.6V5.15H18.4V13.95Z" fill="#FF5C00"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_10" x="-0.266667" y="-0.716667" width="24.5333" height="22.9333" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2.13333"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_10"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.533333"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_10" result="effect2_dropShadow_0_10"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_10" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_0_10">
|
||||
<rect width="24" height="22" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,28 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="22" viewBox="0 0 24 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="monitor" clip-path="url(#clip0_0_405)">
|
||||
<g id="Vector" filter="url(#filter0_dd_0_405)">
|
||||
<path d="M18.4 3.55H5.6C4.72 3.55 4 4.27 4 5.15V13.95C4 14.83 4.72 15.55 5.6 15.55H9.6L8.8 16.35V17.95H15.2V16.35L14.4 15.55H18.4C19.28 15.55 20 14.83 20 13.95V5.15C20 4.27 19.28 3.55 18.4 3.55ZM18.4 13.95H5.6V5.15H18.4V13.95Z" fill="#B6B4AF"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_405" x="-0.266667" y="-0.716667" width="24.5333" height="22.9333" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2.13333"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_405"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.533333"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_405" result="effect2_dropShadow_0_405"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_405" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_0_405">
|
||||
<rect width="24" height="22" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="22.52" height="23.2775" viewBox="0 0 22.52 23.2775" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="file_open" opacity="0.9">
|
||||
<g id="Vector" filter="url(#filter0_dd_0_10)">
|
||||
<path d="M12.25 19H5.5C4.675 19 4 18.325 4 17.5V5.5C4 4.675 4.675 4 5.5 4H11.5L16 8.5V13H14.5V9.25H10.75V5.5H5.5V17.5H12.25V19ZM17.02 18.745V17.065L14.8075 19.2775L13.75 18.22L15.9625 16H14.2825V14.5H18.52V18.745H17.02Z" fill="#FF5C00"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_10" x="0" y="0" width="22.52" height="23.2775" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_10"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_10" result="effect2_dropShadow_0_10"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_10" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="22.52" height="23.2775" viewBox="0 0 22.52 23.2775" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="file_open" opacity="0.9">
|
||||
<g id="Vector" filter="url(#filter0_dd_0_360)">
|
||||
<path d="M12.25 19H5.5C4.675 19 4 18.325 4 17.5V5.5C4 4.675 4.675 4 5.5 4H11.5L16 8.5V13H14.5V9.25H10.75V5.5H5.5V17.5H12.25V19ZM17.02 18.745V17.065L14.8075 19.2775L13.75 18.22L15.9625 16H14.2825V14.5H18.52V18.745H17.02Z" fill="#B6B4AF"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_360" x="0" y="0" width="22.52" height="23.2775" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_360"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_360" result="effect2_dropShadow_0_360"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_360" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,30 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="37" height="32" viewBox="0 0 37 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon_dualmonitor" filter="url(#filter0_d_0_259)">
|
||||
<g id="Union">
|
||||
<path d="M12 14V13H18V4H4V13H10V14H8V15H10H12H14V14H12Z" fill-rule="evenodd" clip-rule="evenodd" fill="#FFE500"/>
|
||||
<mask id="stroke0_0_259" fill="white">
|
||||
<path d="M12 14V13H18V4H4V13H10V14H8V15H10H12H14V14H12Z" fill-rule="evenodd" clip-rule="evenodd"/>
|
||||
</mask>
|
||||
<path d="M12 13V14H14V15H12H10H8V14H10V13H4V4H18V13H12Z" stroke-width="2" stroke="#264F42" mask="url(#stroke0_0_259)"/>
|
||||
</g>
|
||||
<g id="Union_2">
|
||||
<path d="M27 14V13H33V4H19V13H25V14H23V15H25H27H29V14H27Z" fill-rule="evenodd" clip-rule="evenodd" fill="white"/>
|
||||
<mask id="stroke1_0_259" fill="white">
|
||||
<path d="M27 14V13H33V4H19V13H25V14H23V15H25H27H29V14H27Z" fill-rule="evenodd" clip-rule="evenodd"/>
|
||||
</mask>
|
||||
<path d="M27 13V14H29V15H27H25H23V14H25V13H19V4H33V13H27Z" stroke-width="2" stroke="#4B7165" mask="url(#stroke1_0_259)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_0_259" x="0" y="0" width="37" height="36" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_259"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_259" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,30 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="37" height="32" viewBox="0 0 37 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon_dualmonitor" filter="url(#filter0_d_0_233)">
|
||||
<g id="Union">
|
||||
<path d="M12 14V13H18V4H4V13H10V14H8V15H10H12H14V14H12Z" fill-rule="evenodd" clip-rule="evenodd" fill="white"/>
|
||||
<mask id="stroke0_0_233" fill="white">
|
||||
<path d="M12 14V13H18V4H4V13H10V14H8V15H10H12H14V14H12Z" fill-rule="evenodd" clip-rule="evenodd"/>
|
||||
</mask>
|
||||
<path d="M12 13V14H14V15H12H10H8V14H10V13H4V4H18V13H12Z" stroke-width="2" stroke="#4B7165" mask="url(#stroke0_0_233)"/>
|
||||
</g>
|
||||
<g id="Union_2">
|
||||
<path d="M27 14V13H33V4H19V13H25V14H23V15H25H27H29V14H27Z" fill-rule="evenodd" clip-rule="evenodd" fill="#FFE500"/>
|
||||
<mask id="stroke1_0_233" fill="white">
|
||||
<path d="M27 14V13H33V4H19V13H25V14H23V15H25H27H29V14H27Z" fill-rule="evenodd" clip-rule="evenodd"/>
|
||||
</mask>
|
||||
<path d="M27 13V14H29V15H27H25H23V14H25V13H19V4H33V13H27Z" stroke-width="2" stroke="#264F42" mask="url(#stroke1_0_233)"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_0_233" x="0" y="0" width="37" height="36" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_233"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_233" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24.6989" height="20" viewBox="0 0 24.6989 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="folder_open">
|
||||
<g id="Union" filter="url(#filter0_dd_0_10)">
|
||||
<path d="M17.5 5.5H11.5L10 4H5.5C4.675 4 4.0075 4.675 4.0075 5.5L4 14.5C4 15.325 4.675 16 5.5 16H17.5C18.325 16 19 15.325 19 14.5L20.6646 8.25766C20.8339 7.62282 20.3554 7 19.6984 7H19.5H19C19 6.175 18.325 5.5 17.5 5.5ZM5.5 8.5V14.5H17.5L19.1 8.5H5.5Z" fill-rule="evenodd" clip-rule="evenodd" fill="#FF5C00"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_10" x="0" y="0" width="24.6989" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_10"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_10" result="effect2_dropShadow_0_10"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_10" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24.6989" height="20" viewBox="0 0 24.6989 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="folder_open">
|
||||
<g id="Union" filter="url(#filter0_dd_0_505)">
|
||||
<path d="M17.5 5.5H11.5L10 4H5.5C4.675 4 4.0075 4.675 4.0075 5.5L4 14.5C4 15.325 4.675 16 5.5 16H17.5C18.325 16 19 15.325 19 14.5L20.6646 8.25766C20.8339 7.62282 20.3554 7 19.6984 7H19.5H19C19 6.175 18.325 5.5 17.5 5.5ZM5.5 8.5V14.5H17.5L19.1 8.5H5.5Z" fill-rule="evenodd" clip-rule="evenodd" fill="#B6B4AF"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_505" x="0" y="0" width="24.6989" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_505"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_505" result="effect2_dropShadow_0_505"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_505" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,28 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="content_paste" clip-path="url(#clip0_0_10)">
|
||||
<g id="Vector" filter="url(#filter0_dd_0_10)">
|
||||
<path d="M14.25 2.5H11.115C10.8 1.63 9.975 1 9 1C8.025 1 7.2 1.63 6.885 2.5H3.75C2.925 2.5 2.25 3.175 2.25 4V15C2.25 15.825 2.925 16.5 3.75 16.5H14.25C15.075 16.5 15.75 15.825 15.75 15V4C15.75 3.175 15.075 2.5 14.25 2.5ZM9 2.5C9.4125 2.5 9.75 2.8375 9.75 3.25C9.75 3.6625 9.4125 4 9 4C8.5875 4 8.25 3.6625 8.25 3.25C8.25 2.8375 8.5875 2.5 9 2.5ZM14.25 15H3.75V4H5.25V6.25H12.75V4H14.25V15Z" fill="#FF5C00"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_10" x="-1.75" y="-3" width="21.5" height="23.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_10"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_10" result="effect2_dropShadow_0_10"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_10" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_0_10">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,28 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="content_paste" clip-path="url(#clip0_0_344)">
|
||||
<g id="Vector" filter="url(#filter0_dd_0_344)">
|
||||
<path d="M14.25 2.5H11.115C10.8 1.63 9.975 1 9 1C8.025 1 7.2 1.63 6.885 2.5H3.75C2.925 2.5 2.25 3.175 2.25 4V15C2.25 15.825 2.925 16.5 3.75 16.5H14.25C15.075 16.5 15.75 15.825 15.75 15V4C15.75 3.175 15.075 2.5 14.25 2.5ZM9 2.5C9.4125 2.5 9.75 2.8375 9.75 3.25C9.75 3.6625 9.4125 4 9 4C8.5875 4 8.25 3.6625 8.25 3.25C8.25 2.8375 8.5875 2.5 9 2.5ZM14.25 15H3.75V4H5.25V6.25H12.75V4H14.25V15Z" fill="#B6B4AF"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_344" x="-1.75" y="-3" width="21.5" height="23.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_344"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_344" result="effect2_dropShadow_0_344"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_344" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_0_344">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="question_answer">
|
||||
<g id="Vector" filter="url(#filter0_dd_0_10)">
|
||||
<path d="M13.75 5.5V10.75H6.3775L5.5 11.6275V5.5H13.75ZM14.5 4H4.75C4.3375 4 4 4.3375 4 4.75V15.25L7 12.25H14.5C14.9125 12.25 15.25 11.9125 15.25 11.5V4.75C15.25 4.3375 14.9125 4 14.5 4ZM18.25 7H16.75V13.75H7V15.25C7 15.6625 7.3375 16 7.75 16H16L19 19V7.75C19 7.3375 18.6625 7 18.25 7Z" fill="#FF5C00"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_0_10" x="0" y="0" width="23" height="23" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_10"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_0_10" result="effect2_dropShadow_0_10"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_0_10" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |