feat(sw): implement SW header design system

This commit is contained in:
shkim4 committed 2026-10-16 18:08:53 +09:00
1 parent 9407c8567f
commit fc26afd440
107 files changed
+40063 -1

No files matched your search

@@ -0,0 +1,47 @@
{
"mode": "baseline",
"browser": "155.0.8059.79",
"checks": [
{
"name": "no browser errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"styles": {},
"storyIds": [
"sw-header--overview",
"sw-header--default",
"sw-header--controls",
"sw-header--interactive-states",
"sw-header--responsive",
"sw-header--window-move-motion",
"sw-header--composed-controls",
"sw-header--logo-slot",
"sw-header--document-dropdown",
"sw-header--layer-dropdown",
"sw-header--color-dropdown",
"sw-header--line-style-dropdown",
"sw-header--text-dimension-style-dropdown",
"sw-header--responsive-more",
"sw-header--text-stroke",
"sw-header--layer-detail-input",
"sw-header--dynamic-data",
"sw-header--property-panel",
"sw-header--property-panel-variants",
"sw-header--k-guide",
"sw-header--additional-hover",
"sw-header--property-bar",
"sw-header--dynamic-controls",
"sw-header--colorize",
"sw-header--composition",
"sw-header--property-panel-none",
"sw-header--property-panel-all",
"sw-header--property-panel-circle",
"sw-header--property-panel-expanded",
"sw-header--property-panel-compare",
"sw-header--property-shape-slot"
],
"status": "PASS"
}
@@ -0,0 +1 @@
["sw-header--overview","sw-header--default","sw-header--controls","sw-header--interactive-states","sw-header--responsive","sw-header--window-move-motion","sw-header--composed-controls","sw-header--logo-slot","sw-header--document-dropdown","sw-header--layer-dropdown","sw-header--color-dropdown","sw-header--line-style-dropdown","sw-header--text-dimension-style-dropdown","sw-header--responsive-more","sw-header--text-stroke","sw-header--layer-detail-input","sw-header--dynamic-data","sw-header--property-panel","sw-header--property-panel-variants","sw-header--k-guide","sw-header--additional-hover","sw-header--property-bar","sw-header--dynamic-controls","sw-header--colorize","sw-header--composition","sw-header--property-panel-none","sw-header--property-panel-all","sw-header--property-panel-circle","sw-header--property-panel-expanded","sw-header--property-panel-compare","sw-header--property-shape-slot"]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,14 @@
{
"mode": "capture",
"browser": "155.0.8059.79",
"checks": [
{
"name": "no browser errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"styles": {},
"status": "PASS"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,114 @@
{
"mode": "preservation",
"browser": "155.0.8059.79",
"checks": [
{
"name": "required responsive width 390",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "required responsive width 768",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "required responsive width 1280",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "required responsive width 1440",
"status": "PASS",
"data": {
"height": 49,
"overflow": 0,
"documentOverflow": 0
}
},
{
"name": "preserve property panel none",
"status": "PASS",
"data": {
"x": 360,
"y": 204.5,
"width": 314,
"height": 256
}
},
{
"name": "preserve property panel all",
"status": "PASS",
"data": {
"x": 360,
"y": 204.5,
"width": 314,
"height": 361
}
},
{
"name": "preserve property panel circle",
"status": "PASS",
"data": {
"x": 360,
"y": 204.5,
"width": 314,
"height": 658
}
},
{
"name": "preserve property panel expanded",
"status": "PASS",
"data": {
"x": 360,
"y": 204.5,
"width": 314,
"height": 754
}
},
{
"name": "preserve window motion 1400ms",
"status": "PASS"
},
{
"name": "window motion leave clears animations",
"status": "PASS"
},
{
"name": "preserve K guide animation",
"status": "PASS"
},
{
"name": "K guide leave closes",
"status": "PASS"
},
{
"name": "preserve Colorize size",
"status": "PASS"
},
{
"name": "preserve editable Input",
"status": "PASS"
},
{
"name": "no browser errors",
"status": "PASS",
"data": []
}
],
"errors": [],
"styles": {},
"status": "PASS"
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,109 @@
# SW Header 추가 수정 검증 — 2026-10-16
현재 요청 A–H의 결과입니다. 기존 통합 작업/과거 PASS 기록을 덮어쓰지 않습니다. CAD 프로그램 통합이나 전체 HMDesign 프로젝트 승인 판정이 아닙니다.
1. **최종 판정: PASS — 요청한 추가 기능과 실행 검증 범위.** PASS-A~I의 기능·빌드·브라우저 조건을 확인했습니다. Guard 원본 NOT VERIFIED와 남은 디자인 차이는 21~22번에 별도로 기록합니다. 이 보고가 미실행 영역을 PASS로 바꾸지 않습니다.
2. **시작 Git 상태.** 절대 경로 `D:\디자인시스템\HMDesign-SW`, Branch `feature/sw-package`, HEAD `9407c8567f57301659e591c03451cca1b828733c`. 기존 상태는 `M package.json`, `M package-lock.json`, `?? packages/sw/`, `?? src/sw/`, `?? docs/sw/`였습니다. 사용자 지정 Worktree/Branch를 사용했고 기존 변경을 보존했습니다. 시작 시 패키지·Story·문서 복사본과 SHA-256을 QA 경로에 남겼습니다.
3. **재사용.** 기존 Dropdown 8종·id 기반 데이터·Color/Number parser·SWHeaderInput·선 패턴·문자/치수 SVG·레이어 상태 아이콘·문서 목록·More 대표 필드·로고 슬롯·창 제어를 사용했습니다. 다른 패키지 소스는 수정하지 않았습니다.
4. **상세 5개 Hover: PASS.** 배경 .15→.25, radius 24→25, Mouse Leave 복원, Hover Callback 없음, 기존 클릭 1회/닫기, focus-visible/disabled 보존. 치수 상세는 실제 Hover 26:10799(기본 아이콘·글자 100%)이며 Variant3를 혼합하지 않았습니다. 다른 4개는 원본 Hover 아이콘·글자 35%입니다.
5. **5개 버튼 크기: PASS.** 실제 브라우저 측정은 레이어 113×24 / 색상 99×24 / 선스타일 128×24 / 문자스타일 142×24 / 치수스타일 142×24px로 각 Figma 기준과 일치합니다. Noto Sans KR에서도 텍스트가 버튼 안에 잘리지 않는 것을 확인했습니다.
6. **ByLayer/ByBlock Hover: PASS.** 82.5×28px, 글자 #FF5C00, 검정 text-shadow 유지. 선택된 버튼에 Hover해도 #62533D 선택 배경과 On Inset Shadow가 남습니다. Mouse Leave에서 전경색 #D4CBBD로 돌아옵니다.
7. **필수 선택: PASS.** 초기 ByLayer, 재클릭 유지·중복 Callback 없음, 다른 모드로 전환, Controlled ByLayer/ByBlock/null 변경, 팔레트/사용자 색상 선택 후 정확히 하나 표시를 확인했습니다. null의 직접 색상 API 의미와 색상값은 유지하고 모드 표시만 마지막 유효 모드를 기억합니다.
8. **레이어 목록 Hover/Selected: PASS.** 갈색 배경·주황 이름을 동일 적용합니다. 다른 행 Hover와 Mouse Leave에서 선택을 유지하고, 실제 선택 Callback을 받은 호스트 데이터 변경으로 선택 강조가 이동합니다. 기존 전구/눈꽃/자물쇠 매핑은 그대로입니다. 제공되지 않은 별도 레이어 상세 편집 창은 만들지 않았습니다.
9. **선/문자/치수 Hover/Selected: PASS.** #62533D / #FF5C00, 선과 치수 미리보기도 동일 강조입니다. CENTER 22·7·22·7 및 HIDDEN 33·33의 원래 패턴, 문자 미리보기와 치수 원본 SVG를 유지했습니다. 상단 필드/문서 선택 정책은 변경하지 않았습니다.
10. **속성창 4개 상태: PASS.** SWHeaderPropertyPanel 하나가 외부 selection/properties/groups/expandedGroupIds에 따라 바뀝니다. Story fixture 측정: 선택 없음 314×256 / 전체 선택 314×361 / 원 1개 314×658 / 모두 펼침 314×754px. 객체 개수·수치·예시 텍스트는 패키지에 고정하지 않았습니다. Dim 없는 non-modal 도구창입니다.
11. **동적 입력/Callback: PASS.** Text/Number/Select/Color/Slider/Computed, id/group/order, validation, unit/annotation, ReadOnly/Disabled, 외부 값 변경, 항목 추가/삭제, 선택 객체 교체, 옵션 삭제를 지원합니다. Enter/Blur 확정·Escape 취소·변경 없는 중복 보고 방지를 확인했습니다. 삭제된 항목이나 이전 선택의 draft는 폐기합니다. 펼침이 속성값을 바꾸지 않으며, 원형 그래픽의 추가 일반 항목도 위치 영역과 겹치지 않습니다. CAD 계산/객체 변경은 호스트 책임입니다.
12. **내부 아이콘: PASS.** Default→Hover→Active→다시 Default 및 호스트 Callback을 확인했습니다. 29×27.13px wrapper, 원본 SVG/Gradient/Active 금색 테두리 사용. Figma SVG export root가 29×28로 반올림되어 root 크기는 변경하지 않았습니다. 기존 CAD 액션 계약은 없어 toolActive/onToolActiveChange/toolLabel을 통해 호스트에 상태만 통지합니다.
13. **닫기 버튼: PASS.** 원본 44×30 SVG의 Default/빨강 Hover·흰 X·클릭 닫기를 확인했습니다. Escape와 속성창 버튼 재클릭도 닫으며, Desktop은 원래 속성창 버튼, Mobile은 More로 Focus를 돌립니다. Header 종료 Callback과 분리했습니다.
14. **K 가이드: PASS.** 기존 40×38 K 버튼 하나를 재사용합니다. 아래 208×149 가이드, 원본 이미지/마스크, 녹색 배경·금색 경계·문구, 511ms 표시 전환, 버튼→팝업 이동 시 Hover 유지, Hover 종료 닫기를 확인했습니다. 기존 640px Breakpoint 아래에서는 기존 More 항목에 같은 가이드를 연결합니다.
15. **K 이미지 반복: PASS.** 단일 원본 이미지의 CSS transform을 연속 보간해 Pan/Zoom합니다. 400ms 지연/1022ms 전환 → 100ms 지연/1022ms 전환 → 150ms 지연/511ms 전환, 마지막 화면 400ms 유지 후 511ms 복귀로 반복합니다. 브라우저 시간 샘플에서 4개 상태·다음 반복과 15개 이상의 중간 transform을 확인했습니다. 재진입 초기화·타이머 정리·Reduced Motion 첫 프레임 정지·More Escape 후 재열기 시 잔상 없음도 확인했습니다.
16. **Text Stroke Width: PASS.** 요청한 일반 선언과 `--hm-sw-header-text-stroke-width` 변수는 제거했습니다. 일반 자기 색 Stroke 중복 선언도 없습니다. 검정 text-shadow·상세 pill의 별도 solid-stroke-width·SVG/Border/선 Stroke는 유지했습니다.
17. **반응형/접근성: PASS(실행 범위).** 1920/1440/1080/900/768/640/390px에서 Header 49px, Header 가로 넘침 0, 속성창/K 가이드 viewport 경계, 내부 스크롤을 확인했습니다. 기존 Breakpoint는 그대로입니다. 실제 label/control id·접근성 이름·dialog 연결·키보드 Enter/Escape·Focus 복귀·ReadOnly/Disabled를 확인했습니다. 직접 Windows 입력기/보조기기 검증은 22번을 따릅니다.
18. **기존 Story/회귀: PASS.** 기존 17개 export를 유지하고 Property Panel / Property Panel Variants / K Guide / Additional Hover 4개를 추가하여 빌드 index 21개입니다. 모든 Story 렌더와 적용되는 Header 높이를 확인했습니다. 추가로 문서 1개·New/Open·색상 입력 검증·사용자 색상 2개 및 Callback·선 스케일/패턴·레이어 3개 Toggle·대표 More 이동을 확인했습니다. 기존 모든 상호작용의 가능한 조합을 전수 검사했다는 뜻은 아닙니다.
19. **빌드/검증: PASS.** 실제 package.json의 `npm.cmd run typecheck:sw`, `build:sw`, Root `typecheck`, Root `build`, `build-storybook` 최종 실행이 모두 exit 0입니다. 통합 브라우저 189/189, 보완 브라우저/소스 보존 45/45, 사용자 색상·More 최종 경계 별도 PASS, pageerror 0. SW 출력은 styles.css 약 82.48kB / index.js 약 674.64kB이며 원본 PNG가 data URI로 포함됩니다. Root의 기존 유형 Tailwind `bg-[var(...)]` 최적화 경고와 500kB chunk 경고, Storybook 번들 크기/시간 안내는 남습니다. 별도 lint 명령은 package.json에 없고 실행하지 않았습니다.
20. **변경 파일.** 시작 baseline 대비 기존 17개 수정 + 새 구현/자산 17개 추가, 이 검증 보고서 1개 추가 = 소스/문서 35개입니다. 기존 root package.json/lock 변경은 이번 작업에서 수정하지 않았습니다. 아래 목록은 생성 dist를 제외합니다.
```text
MOD packages/sw/README.md
MOD packages/sw/src/assets.d.ts
ADD packages/sw/src/assets/k-guide-mask-default.svg
ADD packages/sw/src/assets/k-guide-mask-fit.svg
ADD packages/sw/src/assets/k-guide-mask-pan.svg
ADD packages/sw/src/assets/k-guide.png
ADD packages/sw/src/assets/layer-detail-hover.svg
ADD packages/sw/src/assets/property-circle.svg
ADD packages/sw/src/assets/property-close-hover.svg
ADD packages/sw/src/assets/property-close.svg
ADD packages/sw/src/assets/property-expand.svg
ADD packages/sw/src/assets/property-footer.svg
ADD packages/sw/src/assets/property-title-single.svg
ADD packages/sw/src/assets/property-title.svg
ADD packages/sw/src/assets/property-tool-active.svg
ADD packages/sw/src/assets/property-tool-hover.svg
ADD packages/sw/src/assets/property-tool.svg
MOD packages/sw/src/components/sw-header/index.ts
MOD packages/sw/src/components/sw-header/SWHeader.tsx
MOD packages/sw/src/components/sw-header/types.ts
MOD packages/sw/src/components/sw-header/components/SWHeaderActions.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderColorMenu.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderDropdown.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderInput.tsx
ADD packages/sw/src/components/sw-header/components/SWHeaderKGuide.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderLineStyleMenu.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderMore.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderProperties.tsx
ADD packages/sw/src/components/sw-header/components/SWHeaderPropertyPanel.tsx
MOD packages/sw/src/theme/sw.css
MOD src/sw/sw-header.stories.tsx
MOD src/sw/sw-stories.css
MOD docs/sw/sw-header-design-spec.md
MOD docs/sw/sw-header-usage.md
ADD docs/sw/sw-header-additional-verification-2026-10-16.md
```
21. **Figma와 남은 차이.** 기존 SW의 Noto Sans KR(400/500/700)을 유지해 Dotum 원본과 글자 폭/획/베이스라인은 다릅니다. 폰트 준비 완료 후 선스타일 전체 폭 473.1875px, 첫 열 187.1875px로 Figma 468/182보다 +5.1875px입니다(기존 content 폭 정책 보존). 5개 상세 버튼과 4개 속성창의 외곽 크기는 기준과 일치합니다. 투명도 thumb는 호스트 min/max/value에 비례하므로 Figma 예시의 장식 위치와 다를 수 있으며 임의 수치 변환을 넣지 않았습니다. 내부 도구 SVG root의 28px export 반올림도 12번과 같습니다. 반복 복귀 전환은 Figma 원본에 없어 요청에 따라 추가한 구현값입니다.
22. **NOT VERIFIED.** Guard 원본 status/staticStatus는 NOT VERIFIED, FAIL 0 / 미검증 issue 28입니다. 독립 SW CSS import 해석(ENTRY-002), Story 호스트 native button(REUSE-004), SW token 연결/동적 class/style 해석(TOKEN-001/002)이 남습니다. styleEvidence는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED이며 원본 JSON은 변경하지 않았습니다. 실제 브라우저 결과는 별도 증거입니다. HM UI와 독립된 SW 패키지여서 native label/select/range를 사용한 근거는 명세에 기록했습니다. 모든 픽셀/모든 애니메이션 프레임의 자동 이미지 diff, Figma 프로토타입 직접 재생 시각 동기 비교, 실제 Windows 한글 IME, screen reader·색 대비 전수 감사, 실제 CAD·파일/OS 창 동작·운영 배포는 미검증입니다.
23. **종료 Git 상태.** Branch/HEAD는 시작과 같은 `feature/sw-package` / `9407c8567f57301659e591c03451cca1b828733c`. 상태 범주는 기존과 동일한 root 2개 M 및 SW 3개 폴더 Untracked입니다. git diff --check 통과. Stage/Commit/Push/PR/Merge/Publish/배포를 수행하지 않았습니다. 기존 문서/홈/창 제어/레이어 아이콘/필드 소스, 기존 자산 30개, 과거 검증 로그 5개, 창 이동 1400ms CSS 블록은 baseline과 바이트 단위로 동일합니다. 사용자 검토/승인을 기다립니다.
## 증거 위치
QA root: `C:\Users\user\.codex\visualizations\2026\10\16\01a14374-df22-7311-831d-2d3a4c522f95\sw-header`
- `baseline/`, `baseline-sha256.json`, `git-start.txt`: 시작 보존본.
- `figma/`: 직접 확인한 Figma 디자인 context.
- `verify.mjs` / `browser-results.json`: 통합 189개.
- `followup.mjs` / `followup-results.json`: 보완 45개 및 원본 보존.
- `palette-results.json`, `final-edge-results.json`, `line-font-ready.json`: 사용자 색상/More/폰트 준비 후 폭.
- `final-panel-none/all/circle/expanded.png`, `verified-k-guide.png`: 실제 React 렌더.
- `guard.json`: 원본 Guard 판정(덮어쓰지 않음).
- `sw-typecheck-final.log`, `sw-build-final.log`, `root-typecheck-final.log`, `root-build-final.log`, `storybook-build-final.log`: 최종 명령 로그.
- `changed-files.json`: baseline 대비 파일 목록(보고서 추가 전 수집).
## 시간 기록 (Asia/Seoul)
- Started At: 2026-10-16 15:45:05 KST
- Completed At: 2026-10-16 16:17:40 KST
- Actual Duration: 32분 35초
- Estimated Duration: 약 90~130분
- Variance: 예상보다 57분 25초~97분 25초 짧음
@@ -0,0 +1,55 @@
# SW Header 3차 — 구성요소 표시·메뉴 순서·속성창 4상태·형상 Slot 검증
이번 요청의 구현·실행 범위 기록입니다. 이전 검증 문서는 수정하지 않았습니다. Commit/Push/PR/배포는 하지 않았습니다.
## 1. 판정
**PASS(요청 범위의 실행 검증)** — PASS-A~J. 미검증 항목은 7절에 분리했습니다.
## 2. Git
- 시작·종료 동일: `D:\디자인시스템\HMDesign-SW`, `feature/sw-package`, HEAD `9407c8567f57301659e591c03451cca1b828733c`.
- 상태: `M package.json`, `M package-lock.json`(작업 전부터 존재, 이번에 수정하지 않음), `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`(SW 작업 전체가 아직 미커밋).
## 3. 구현 요약
| 요구 | 구현 |
|---|---|
| 로고 외 개별 제거 | `SWHeader.hiddenControls` (`SWHeaderHideableControlId` = 기존 `SWHeaderControlId` + `home`/`windowMove`/`minimize`/`close`) |
| 로고 필수 | 로고는 대상 id가 아님. `home` 제거 시 `SWHeaderHome interactive={false}` 정적 로고 |
| 유사 메뉴 순서 | `SWHeader.menuOrder` (5개 메뉴끼리만 자리 교환, 지정 우선·누락은 기본 순서·중복/미지 id 무시) |
| More 연동 | 기존 Breakpoint·대표 슬롯 재사용. 객체 영역 요소 제거 시 스크립트가 More/대표 결정, 빈 More 미생성 |
| 속성창 4상태 | 기존 `SWHeaderPropertyPanel` + 데이터. 개별 Story 4개 + 비교 Story 1개 |
| 형상 Slot | `shapeContent` (undefined=Figma 원형, null/false=빈 영역, ReactNode, Render Prop) |
재사용: 기존 `controls` 필터·More·대표 슬롯 로직, `SWHeaderCommitPresence`(제거 시 draft Callback 방지), SWHeaderInput, Property 타입·Fixture, 하위 공개 컴포넌트(선택적 show Props만 추가, 기본 true).
## 4. 실행 검증 (Playwright Chromium + Storybook dev 6024, dist 재빌드 후)
- **기본 디자인 보존:** 변경 전/후 `Default`(1920 Frame)·`Controls`(viewport) Story의 Header 하위 98개 요소 위치·크기·display·visibility·color·background를 1920/1800/1440/1180/1080/900/768/640/520/390px에서 비교 — **차이 0**.
- **구성 11종 × 7폭(1920~390) = 77건 PASS:** 기본 전체, 구성 A/B/C, 파일 목록·레이어·색상·스타일 그룹 제거, 여러 메뉴 제거, 순서 변경, 숨김+순서 변경. 각 건에서 Header 49px, 로고 32×17 표시, 제거 요소 DOM 부재, DOM 메뉴 순서 = 기대 순서, Header 밖 노출/가로 스크롤 없음, More 열었을 때 빈 목록 없음, 제거 요소가 More에 없음.
- 최초 실행 3건 FAIL(여러 메뉴 제거 @900/768/640): 최소화 제거로 짧아진 창 영역 밖으로 투명한 창 이동 Tooltip이 넘침 → `data-reduced` 창 영역 clip + 24px 이동 영역 Tooltip 좌측 표시로 수정 후 재실행 0 FAIL.
- **동작 25건 PASS:** 열린 Dropdown 숨김 시 제거, 재표시 값 유지·닫힘, 순서 반전 렌더링·Callback 없음·값 유지, 재정렬 후 Dropdown 열기·Escape Focus 복귀·Tab 순서, 중복/알 수 없는 id 처리, More→문자스타일 대표 슬롯, 대표 메뉴 제거 시 레이어 복귀, 레이어 제거 시 색상이 대표, More 제외/재표시 접근, 속성창 숨김 시 Popup 닫힘·Callback 없음, 정적 로고(클릭 Callback 없음)·홈 재표시 Callback, 로고만 구성(More·창 영역 없음), 콘솔 오류 0.
- 1건 기록: "색상 숨김 Callback 없음" 검사에서 `onColorMenuOpenChange(false)`가 기록됨. 체크박스 클릭이 Dropdown 바깥 클릭이 되어 **숨기기 전에** 기존 바깥 클릭 닫힘 계약이 실행된 것이며, 숨김 자체가 만든 Callback이 아님(코드상 제거는 내부 상태만 정리).
- **속성창 24건 PASS:** 개별 Story 실측 314×256 / 314×361 / 314×658 / 314×754(Figma와 일치), 내용 잘림 없음. Enter 확정·Escape 취소·잘못된 숫자 거부. 기본 형상 영역 208×208 @(39,26), 그룹 260px. 비교 Story 4개 동시 렌더·실측 표시, 1440/768/390px 줄바꿈·페이지 가로 스크롤 없음. 형상 Slot 기본/비움/외부 컴포넌트/교체/Render Prop context, 비움·교체 시 패널 높이·라벨 위치 동일, 형상 그룹 항목 추가·삭제 반영. Header 내 속성창 열기·닫기 및 `onPropertyPanelOpenChange`·`onPropertyPanelToggle` 보존.
- 자동 검사 1건(외부 지름·반지름 변경)은 측정이 재렌더 전에 실행된 타이밍 문제였고, Browser pane에서 재확인해 입력값 10.0000 / 5.0000 반영을 확인.
- **Header 내 속성창 경계:** 1920/1440/1080/900/768/640/390px에서 패널이 Viewport 안(좁은 폭은 기존 More 경유).
- **Story 회귀:** 기존 24개 + 신규 7개 = 31개 전부 렌더, page/console 오류 0.
## 5. 빌드 (순차, exit 0)
`npm run typecheck:sw` → `build:sw` → `typecheck`(root, workspaces) → `build`(root) → `build-storybook`(31개 SW Story 포함). 경고는 기존 Tailwind `bg-[var(...)]` CSS 최적화 경고와 chunk 크기 경고뿐이며 신규 경고 없음.
## 6. Figma와 남은 차이
- 형상 라벨 x 위치 1~2px(원주 113 vs 111.5, 지름 100 vs 102, 반지름 154 vs 152.5, 면적 100 vs 98.5)와 지름 y 110 vs 108은 기존 구현값을 유지(이번 범위에서 변경 안 함).
- 로고만 구성에서 녹색 객체 영역 배경은 Header 표면으로 유지(구현 판단).
- 부분 제거 시 상세보기 블록·창 영역 폭은 Figma에 없는 구현값(auto).
- `26:11338` 색상 돋보기 Overlay는 요청대로 미구현(호스트 연동 대상).
## 7. NOT VERIFIED
- 실제 CAD 호스트 앱 연동, 실제 형상 렌더러 연동.
- 스크린리더 등 보조기기, Firefox/Safari, 터치 기기.
- 창 이동 Tooltip의 1080px 이상 Hover 애니메이션 중 축소된 창 영역에서의 잘림 정도(시각 확인 안 함).
- Figma 픽셀 오버레이 비교(크기·위치 수치 비교만 수행).
@@ -0,0 +1,213 @@
# SW Header 추가 디자인 5건 — 최종 검증 기록 (2026-10-16)
## 1. 판정
**PASS — 이번 요청의 수정 및 아래 명시된 Chrome 실행 검증 범위.** 전체 HMDesign, CAD 호스트 연동, 모든 브라우저 또는 Figma 전체 픽셀 일치에 대한 승인은 아니다. Guard의 `NOT VERIFIED`는 별도로 유지한다.
작업 범위는 기존 SW 패키지와 기존 Storybook 예제(BOTH)다. 신규 업무 화면·Navigation·Story는 추가하지 않았다. Commit / Push / PR / Merge / Publish / 배포는 하지 않았다.
## 2. 시작 Branch / HEAD / Git Status
- 경로: `D:\디자인시스템\HMDesign-SW`
- Branch: `feature/sw-package`
- HEAD: `9407c8567f57301659e591c03451cca1b828733c`
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`
- 기존 상태: `M package.json`, `M package-lock.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`.
- 이번 요청에 명시된 작업 경로·브랜치·Commit/Push 금지가 일반 main/공식 작업 폴더 지침보다 우선한다. 원격 동기화나 Git 이력 변경은 하지 않았다.
## 3. 이전 작업 이어서 완료한 내용과 보존 근거
기존 구현을 유지하고 5건의 원인에 필요한 부분만 수정했다. 시작 상태의 `packages/sw`, `src/sw`, `docs/sw`, root manifest/lock을 별도 QA baseline에 복사하고 소스·문서 SHA-256을 기록했다.
- 기존 `hiddenControls`, `menuOrder`, More 및 대표 슬롯, 속성Bar, 속성창 데이터·입력·Callback·형상 Slot 구현을 재사용했다.
- 기존 `docs/sw` 문서 10개(명세·사용법·검증 기록 포함), 기존 SVG/이미지, `package.json`, `package-lock.json`은 시작 baseline과 바이트가 같다.
- `SWHeader.tsx`, PropertyPanel/PropertyBar, ColorMenu, KGuide, WindowControls, 공개 export 등 나머지 소스는 수정하지 않았다. 공개 Props의 이름·타입·Callback 시그니처도 그대로다(`types.ts`는 설명 주석만 수정).
- 이전 Motion·Colorize·ByLayer/ByBlock·Text Stroke 정책은 소스 보존 근거다. 이번에 Motion 전체 프레임이나 CAD 동작을 새로 검증한 것으로 표현하지 않는다.
- 다른 디자인 시스템 패키지 소스, DWG/DXF Viewer, 기존 `_local` 파일은 수정하지 않았다. 빌드 산출물은 각 기존 빌드 명령으로 재생성했다.
## 4. 추가 수정 5건 결과
- **A PASS:** 선스타일 메뉴와 Dropdown의 실제 접촉 구간 1px만 검정색으로 수정.
- **B PASS:** 치수스타일 고정 Story 두 개만 오른쪽 끝을 필드와 일치시킴. 동적 위치 계산은 수정하지 않음.
- **C PASS:** 속성창 헤더의 원본 대각선·그림자를 가리던 레이어 순서 복원, 도구 버튼 위치와 제목 자간 보정. 네 상태의 디자인 차이 유지.
- **D PASS:** Figma Hover `26:10799`를 재확인하고 치수 전용 Hover 규칙을 명시. 호스트 상세 Callback 없는 경우에도 Hover 가능하게 수정.
- **E PASS:** Callback 부재와 Disabled를 분리. 일반 레이어 상세 Hover 및 명시적 Disabled 예제를 구분하고 기존 Callback 유지.
## 5. 선스타일 연결 영역 검정색
원인은 `.hm-sw-header__line-menu::before`의 노란 Gradient ring이 필드와 접하는 상단에도 그려지는 것이었다. 기존 ring은 유지하고 기존 `::after`를 접촉 구간에만 배치했다. 색은 실제 선언된 `--hm-sw-header-layer-menu-border-color`(`#000`)를 재사용한다.
공통 Dropdown의 `panelStyle.left`에서 접촉 시작점을 구하고 필드의 실제 폭을 ResizeObserver로 추적한다. More에서 필드가 축소되어도 284px을 강제로 칠하지 않는다. 공통 Dropdown Position/Align은 수정하지 않았다.
브라우저 결과:
- Expanded 접촉 구간의 PNG RGB 실측: `0,0,0`.
- 상단의 접촉하지 않는 좌우 부분·좌우/하단 테두리: 기존 노란색 유지.
- 접촉 구간 아래 전체 Dropdown PNG 비교: 변경 픽셀 **0**. 선 Preview, 항목 정렬, Hover/Selected 행도 그대로다.
- Default → Hover → Mouse Leave의 필드 색상·ring 복귀와 Selected 행 강조를 확인했다. 제목/값의 기존 노란색을 유지한다.
- 1920/1440/1280/768/390px에서 검정 구간이 실제 필드 폭 밖으로 나가지 않음을 확인했다.
## 6. 치수스타일 고정 Story 위치
대상은 `치수스타일 Expanded (Figma 26:11242)`와 `치수스타일 행·상세 버튼 Hover (고정)`뿐이다.
원인은 1920px 고정 Header를 좁은 Story scroller에서 렌더링할 때 공통 경계 보정이 실제 화면 폭을 기준으로 목록을 왼쪽으로 옮기는 것이었다. 두 `MenuFrame`에만 `fixedDimension`을 지정하고 Story CSS로 `left:auto; right:0`을 적용했다. 고정 상태의 ring도 동일한 필드 기준으로 맞췄다.
- 1440px 화면의 수정 전 두 예제: 필드 오른쪽 `1641px`, Dropdown 오른쪽 `1439px` — **202px 어긋남**.
- 수정 후 두 예제: 필드/Dropdown 오른쪽 모두 `1641px` — **차이 0px**.
- Figma 필드 폭 164px, 목록 폭 164px, 필드 하단에서 목록 시작이라는 상대 위치를 유지한다.
- 1920px specimen을 가로 스크롤한 뒤에도 필드와 목록 오른쪽 끝이 일치함을 확인했다. 화면 폭에 맞춰 고정 Header 자체를 축소하지 않았다.
## 7. 동적 Dropdown 위치 불변
`useSWHeaderDropdown` 및 Text/Dimension의 공통 Position/Align을 수정하지 않았다. `선택 Callback`의 동적 치수 Dropdown 수정 전후 실측:
- 1440px 화면: 필드 `x=1156, width=164`, Dropdown `x=1156, width=164`, 오른쪽 모두 `1320` — 전후 동일.
- 2000px 화면: 필드/Dropdown `x=1037, width=164`, 오른쪽 `1201` — 전후 동일.
- 고정 Story 전용 CSS는 동적 예제에 적용되지 않는다.
## 8. 속성창 네 상태 헤더 Figma 비교
[Figma 속성창 원본](https://www.figma.com/design/rj6wpsfk24lOWvWiVRAANP/이지빔-SW?node-id=26-11822&m=dev)을 get_design_context(스크린샷 포함)와 읽기 전용 Plugin API로 직접 확인했다. 원본 디자인은 수정하지 않았다.
- `26:11886` 선택 없음: **314×256px**, 녹색 Gradient Union, 추가 도구 아이콘 있음.
- `26:11823` 전체 11: **314×361px**, 녹색 Gradient Union, 추가 도구 아이콘 있음.
- `26:11932` 원 1개: **314×658px**, 갈색 Union(`#211D13`), 추가 도구 아이콘 없음.
- `26:12035` 모두 펼침: **314×754px**, 원 1개와 같은 갈색 헤더, 추가 도구 아이콘 없음.
- **닫기 버튼은 Figma 네 상태 모두 있다.** 단일 원 상태의 닫기 기능을 제거하지 않았다.
원본 Union SVG를 새로 내려받아 기존 파일과 SHA-256을 비교했으며 동일했다. 디자인 차이는 잘못된 자산이 아니라 닫기 버튼 배경이 Union 오른쪽 대각선을 덮는 paint order였다. Union을 해당 배경 위에 배치하고 제목·도구 버튼은 그 위에 유지했다. Union은 `pointer-events:none`이어서 닫기 버튼을 차단하지 않는다.
공통 실측(패널 좌상단 기준):
- 헤더 내부 영역 `(1,1)`, **312×30px**. 외곽 금색 1px·radius 4px 및 본문 경계/그림자 보존.
- 제목 `x=8, y=8`, 14px, 기존 Noto Sans KR·700 유지, Figma 자간 **0.56px(0.04em)** 적용.
- 추가 도구 버튼 `(241,2)`, **29×27.125px**(Figma 27.129px의 브라우저 반올림). SVG root 29×28은 보존.
- 닫기 `(269,1)`, **44×30px**. 원본 Default/Hover 자산과 클릭 기능 유지.
- 본문/전체 높이 네 상태 모두 시작 baseline과 동일.
네 패널 PNG를 직접 검토했다. 제목과 도구 클릭, 상단 X/키보드 Escape 닫기, Focus 복귀, Enter 확정·Escape 취소 및 Property Callback을 Chrome에서 확인했다. 형상 Slot·4개 상태 비교 Story도 유지했다.
## 9. 치수스타일 상세 Hover
Figma `26:10796` Default와 `26:10799` Hover를 직접 확인했다. `26:10802` Variant3는 혼합하지 않는다.
- Default: **142×24px**, 배경 `rgba(197,177,158,.15)`, radius 24px.
- Hover: 배경 `rgba(197,177,158,.25)`, radius 25px, **기본 아이콘·문자 opacity 1**.
- Mouse Leave: Default 배경·radius 복귀.
- Callback이 있는 동적 예제는 시작 시점에도 위 Hover 값과 일치했다. 이를 다른 상세 버튼/Variant3와 같게 바꾸지 않았다. 치수 전용 규칙을 명시하고, Callback 없는 Dynamic Data에서 자동 disabled가 되던 경로를 제거했다.
- Callback 없는 경우 Hover는 동작하지만 CAD 상세 기능을 만들어 실행하지 않는다. Callback이 있는 클릭은 기존 `onDimensionStyleDetail` **1회** 후 닫힘을 확인했다.
- 색상/선/문자 상세 버튼의 기존 규칙은 수정하지 않았다.
## 10. 레이어 상세 Hover 및 Disabled 원인
실제 원인은 `disabled={!onDetail}`였다. Hover CSS는 존재했지만 `:hover:not(:disabled)`에서 제외됐다. pointer-events나 상위 컨테이너가 원인은 아니었다.
- 일반 상태는 Callback 없어도 활성 Hover. 기본 크기 **113×24px** 유지(3배 Story 실측 339×72).
- Default .15/radius 24 → Hover .25/radius 25 → Mouse Leave Default 복귀.
- Hover 아이콘·글자 35%는 Figma `26:10771`대로 유지.
- 기존 메뉴의 명시적 `disabled` 계약은 유지한다. Story의 Disabled specimen은 이제 호스트가 실제 `fieldset disabled`를 명시한다. Callback 부재를 Disabled 예시로 사용하지 않는다.
- Callback 연결 예제에서 `onLayerDetail` 1회 후 목록 닫힘 확인. 연결되지 않은 CAD 기능·상세창은 추가하지 않았다.
## 11. 기본 디자인 10개 폭 비교
수정 전/후 Default(1920px Frame)와 Controls(viewport)의 Header 및 하위 DOM 총 **99개(루트 1 + 하위 98)**를 비교했다.
- 폭: **1920,1800,1440,1180,1080,900,768,640,520,390px**.
- 2개 Story × 10폭 = **20건**, 위치·크기·display·visibility·color·background 차이 **0**.
- 후속 좁은 화면 보정은 열린 선스타일 Dropdown의 접촉선 폭만 변경한다. 닫힌 기본 Header에는 적용되지 않으며 해당 5개 폭을 별도 재검증했다.
## 12. 메뉴 구성 및 기능 검증
**11개 구성 × 7폭(1920/1440/1080/900/768/640/390) = 77건 PASS.** 기본 전체, A/B/C, 파일·레이어·색상·스타일 그룹·여러 메뉴 제거, 순서 변경, 숨김+순서 변경.
각 구성의 로고 32×17 유지, Header 49px, DOM 메뉴 순서, 가로 overflow 없음, More 목록이 비지 않음, 숨긴 메뉴가 More에 남지 않음을 확인했다.
추가 실행: More→문자스타일 대표 슬롯, 대표 제거→레이어 복귀, 메뉴 숨김/재표시, Dropdown 바깥 클릭, 속성창 열기·닫기/Focus 복귀, 상세 Callback, 형상 Slot 기본/비움/외부 컴포넌트/교체/Render Prop, 형상 속성 추가·삭제/외부 값 반영. 4개 패널 비교는 1440/1280/768/390px에서 가로 페이지 overflow 없이 표시됐다.
Outside Click과 React 타이밍 구분:
- Checkbox를 실제 포인터로 클릭하면 기존 Dropdown의 바깥 pointerdown이 먼저 닫힘 Callback을 발생시킨다. 별도로 checkbox의 DOM click만 호출해 숨길 때는 합성 닫힘 Callback이 없음을 확인했다. 실제 기능 오류로 분류하지 않는다.
- 형상 지름·반지름 외부 변경은 React 렌더링 완료 조건을 기다린 뒤 `10.0000 / 5.0000`을 확인했다. 재렌더 이전 즉시 읽기를 성공/실패 근거로 쓰지 않았다.
- 테스트의 잘못된 Story ID와 다른 예제 로그 선택을 수정했다. 해당 실패는 제품 코드 오류가 아니며 수정된 검사와 오류를 분리한 재실행 결과를 보존한다.
## 13. Storybook 전체 렌더링
기존 **31개 Story** 유지. 실제 Chrome에서 31/31 렌더링 및 보이는 이미지 로딩 확인. 잘못된 Story ID에서 발생한 오류를 분리하기 위한 독립 재실행에서 **page error 0 / console error 0 / HTTP error 0**.
Story를 새로 추가하거나 기존 대표 Story를 교체하지 않았다.
## 14. SW / Root Typecheck 및 Build
최종 소스 기준 다음 명령 모두 **exit 0**:
- `npm.cmd run typecheck:sw`
- `npm.cmd run build:sw`
- `npm.cmd run typecheck` (Root·workspaces)
- `npm.cmd run build`
- `npm.cmd run build-storybook`
실제 소스가 다시 변경된 연결선 보정 이후 영향을 받는 검증을 재실행했다. 조건이 바뀌지 않은 77개 구성·기본 Geometry 검사는 반복하지 않았다. CSS 최적화의 기존 Tailwind arbitrary value 경고와 chunk 크기 경고는 남아 있다. 빌드 실패는 없다.
## 15. 이번 변경 파일
시작 baseline 대비 수정 **7개**, 새 보고서 **1개**:
1. `packages/sw/src/components/sw-header/components/SWHeaderLayerMenu.tsx` — Callback 없는 레이어 상세 자동 Disabled 제거.
2. `packages/sw/src/components/sw-header/components/SWHeaderLineStyleMenu.tsx` — 접촉선 시작점·실제 필드 폭 전달/추적.
3. `packages/sw/src/components/sw-header/components/SWHeaderStyleListMenus.tsx` — 치수 상세 Callback 없는 Hover 허용.
4. `packages/sw/src/components/sw-header/types.ts` — 위 Callback의 설명 주석만 정정.
5. `packages/sw/src/theme/sw.css` — 접촉선, 치수 전용 Hover, 속성창 paint order/도구 위치/제목 자간.
6. `src/sw/sw-header.stories.tsx` — 고정 치수 두 예제 범위 지정, 레이어의 명시적 Disabled 예제.
7. `src/sw/sw-stories.css` — 위 고정 Story와 Disabled specimen 컨테이너 스타일.
8. 이 검증 문서.
미커밋 SW 폴더 전체를 이번에 새로 작성한 것으로 집계하지 않는다. root manifest/lock의 기존 변경도 이번 변경 파일에 포함하지 않는다. 시작 baseline 대비 실제 diff를 읽고 수정 범위를 검토했다.
## 16. 남은 디자인 차이
- 기존 정책의 **Noto Sans KR**(Figma Dotum 대신)·400/500/700을 유지했다. 제목을 포함한 글리프 모양·문자 폭은 Figma와 완전 동일하지 않다.
- 기존 선스타일 열의 Noto 내용 폭, 형상 라벨 위치 등의 과거 문서상 차이는 이번 범위 밖으로 유지한다. 본문·형상 위치를 임의 변경하지 않았다.
- Figma 픽셀 전체 일치로 주장하지 않는다. 이번에 PNG의 변경 픽셀 0을 확인한 범위는 선스타일 접촉선 이외 Dropdown 보존 비교다.
## 17. NOT VERIFIED 및 Guard
Guard는 Storybook PARTIAL로 변경 파일을 검사하고, 마지막 연결선 소스 변경 후 해당 두 파일만 PARTIAL 재검사했다. **FAIL 0**, 원본 `status/staticStatus/runtimeStatus`는 **NOT VERIFIED**다. 이를 브라우저 PASS로 덮어쓰지 않았다.
원인: Guard가 독립 `@hm-design/sw/styles.css` 및 SW 패키지 구현 graph를 HM UI 소비 화면과 같이 해석하지 못하는 ENTRY/SCOPE 항목, Storybook 예제의 native controls·동적 class/style·token 근거 항목. SW build와 실제 컴포넌트 렌더링은 별도 실행 증거다.
CSS 원본 근거도 그대로 유지: **STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED**. 내부 SW CSS를 모두 검사해 승인한 결과가 아니며 `checks=[]`인 정적 scope 한계가 있다. 이번 수정의 색·크기·상태는 위 개별 Figma/브라우저 검증으로만 판단한다.
이번 미검증: 실제 CAD 호스트/형상 렌더러, Firefox/Safari, 터치, 스크린리더, 전체 화면 픽셀 overlay, 기존 K/창 이동 Motion 전체 프레임 재검사. 과거 검증 기록은 보존한다.
## 18. 종료 Branch / HEAD / Git Status
- Branch: `feature/sw-package`
- HEAD: `9407c8567f57301659e591c03451cca1b828733c` (시작과 동일).
- 상태: `M package.json`, `M package-lock.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`.
- Staged Diff 없음. 기존 파일 삭제/초기화 없음. Commit / Push / PR / Merge / Publish / 배포 없음.
## 실행 근거 및 재현
QA Root: `C:\Users\user\.codex\visualizations\2026\10\16\01a143c9-30e2-7c42-aa06-c658d456a984\sw-header`.
- `baseline/`, `baseline-sha256.json`, `git-start.txt`: 복구·변경 전 근거.
- `before/capture.json`, `after/capture.json`: 10폭 Geometry 및 네 패널 실측.
- `before/probe.json`, `after/probe.json`: 고정/동적 치수 위치와 Hover.
- `verification.json`: 최초 통합 검사(테스트 수정 전 실패 기록 포함).
- `verification-followup.json`, `verification-A.json`, `verification-E.json`: 연결선·레이어의 영향 범위 재검사.
- `verification-all SW.json`: 실제 31개 Story의 독립 오류 검사.
- `edges-results.json`: 닫힌 선스타일 상태, 5개 폭 접촉선, 고정 Story 스크롤 검증(8 PASS / 0 FAIL).
- `build-results-final.json`, Typecheck/Build `.log`: 최종 5개 명령 exit 0.
- `guard.json`, `guard-final-affected.json`: 덮어쓰지 않은 원본 NOT VERIFIED.
- `panel-{none,all,circle,expanded}-final.png`, `line-seam-final.png`, `dimension-detail-hover.png`, `layer-detail-hover.png`, `fixed-dimension-scrolled.png`: Chrome 캡처.
재현 스크립트 `capture.mjs`, `probe.mjs`, `verify.mjs`, `edges.mjs`, `builds.mjs`는 QA Root에 있다. 이번 작업에서는 로컬 Storybook `127.0.0.1:6043`과 기존 Playwright/Chrome을 사용했다. 재현 시 같은 소스·dist 및 서버 포트를 확인한다.
## 시간 기록 (Asia/Seoul)
- Started At: 2026-10-16 17:17:10 KST
- Completed At: 2026-10-16 17:40:21 KST
- Actual Duration: 23분 11초
- Estimated Duration: 약 30~50분 (분석 5~10 / 수정 15~25 / 통합 검증 10~15분)
- Variance: 최초 예상 범위보다 6분 49초~26분 49초 짧음
+681
View File
@@ -0,0 +1,681 @@
# SW Header 디자인 명세
> **상세 버튼 공통화 기준(2026-10-16, 이번 요청):** 상세 5개는 기존 `SWHeaderMenuDetail`을 공유하며 **현재 문자스타일 상세**를 우선합니다. Default는 배경 `rgba(197,177,158,.15)`·radius 24·글자 35%·아이콘 SVG 20%, Hover는 배경 .25·radius 25·글자 35%·아이콘 SVG 35%입니다. Disabled는 Default 외형과 클릭 차단을 사용합니다. Callback이 없어도 Hover되며 클릭은 무동작입니다. 치수 상세의 글자 100% Hover와 Callback 부재에 따른 비활성 설명은 이 기준으로 대체합니다. 기존 폭 113/99/128/142/142px·높이 24px, 제거된 Text Stroke 선언을 유지합니다. 과거 검증 문서는 당시 기록으로 보존합니다. [이번 검증 기록](./sw-header-detail-buttons-verification-2026-10-16.md).
> **3차 기준(2026-10-16):** 문서 끝 [3차 기준](#3차-기준--구성요소-표시메뉴-순서속성창-상태형상-slot-2026-10-16)이 구성요소 표시·메뉴 순서·속성창 상태·형상 Slot에 우선합니다. [3차 검증 기록](./sw-header-composition-shape-verification-2026-10-16.md).
> **후속 5개 요구사항(2026-10-16):** 이 문서 끝의 후속 기준이 속성Bar·메뉴 구성·Colorize·Text Stroke 비교 정리에 우선합니다. 이전 `solid-stroke-width` 보존 설명은 역사적 기록이며 최신 구현에는 적용하지 않습니다. [새 검증 기록](./sw-header-followup-five-verification-2026-10-16.md).
> **현재 추가 기준:** 이 문서의 과거 Hover 없음·선택 글자만 노랑·일반 자기 색 Stroke 내용보다 아래 [추가 수정 기준](#추가-수정-기준--속성창k-가이드-2026-10-16)이 우선합니다. 과거 검증 기록은 당시 결과로 보존합니다.
> 상태: `@hm-design/sw` 0.1.0 구현 기준(2026-10-16). 코드: `packages/sw/src/components/sw-header/`, CSS: `packages/sw/src/theme/sw.css`, Story: `SW / Header`.
> 값 표기: **확인** = Figma MCP(`get_metadata`·`get_design_context`) 또는 사용자 확정 CSS에서 직접 확인, **구현값** = Figma에 없어 구현에서 정한 값, **추정** = 확인 근거 없음.
## 1. 개요
SW Header는 여러 설치형 SW에서 공통으로 쓰는 49px 헤더입니다(Figma 기준 화면은 이지빔 SW). 로고 / 파일명 / 객체 수정 영역 / 창 영역의 4개 면으로 구성됩니다. `@hm-design/ui`와 독립된 `@hm-design/sw` 패키지에서 관리하며, SW 전용 토큰(`--hm-sw-header-*`)만 사용합니다.
우선순위: ① 사용자 확정 CSS·선택·반응형 정책 → ② Figma 원본 → ③ 기존 DWG/DXF 뷰어 구현(`D:\디자인시스템\테스트\dwg-dxf-viewer-hm`) → ④ HMDesign 공통 규칙(패키지·타입·Storybook·접근성).
## 2. Figma 원본
| 항목 | 값 |
|---|---|
| File | `rj6wpsfk24lOWvWiVRAANP` (이지빔 SW) |
| Node | `26:8944` `header_green` (instance) |
| 크기 | 1920 × 49 (내부 면 높이 48) — 확인 |
| 접근 | Figma MCP로 metadata·design context·screenshot 확인(2026-10-16) |
## 3. 레이어 구성
표시 레이어(확인). 같은 내용의 `header` frame이 두 번 겹쳐 있으며(`26:10337`, `26:10372`), 두 번째 frame에만 `Rectangle 6671`(x0, 310 × 48, 파일명 면과 같은 gradient)이 홈 뒤에 깔립니다. 구현은 한 번만 렌더링하고 Rectangle 6671은 홈 영역 0–65px의 배경으로 재현합니다.
| Figma 레이어 | x / y | W × H | 구현 컴포넌트 |
|---|---|---|---|
| `header/btn_home` | 0 / 0 | 80 × 48 | `SWHeaderHome` |
| `header/document_tit` | 65 / 0 | 310 × 48 | `SWHeaderDocumentTitle` (문서 Dropdown 포함, §5.1) |
| `Rectangle 7428` (객체 수정 면) | 377 / 0 | 1301 × 48 | `.hm-sw-header__object-area` |
| `Frame 427319899` 그리기모드 | 377 / 5 | 90 × 38 | `.hm-sw-header__mode` |
| `header/menu/레이어` | 467 / 5 | 234 × 38 | `SWHeaderLayerControl` (`layers` 전달 시 `SWHeaderLayerMenu`, 레이어 Dropdown §5.2) |
| `header/menu/색상` | 705 / 5 | 192 × 38 | `SWHeaderColorControl` |
| `header/menu/선스타일` | 901 / 5 | 284 × 38 | `SWHeaderLineStyle` |
| `header/menu/문자스타일` | 1189 / 5 | 164 × 38 | `SWHeaderTextStyle` |
| `header/menu/치수스타일` | 1357 / 5 | 164 × 38 | `SWHeaderDimensionStyle` |
| `Frame 427319953` 상세보기 | 1521 / 6 | 157 × 38 | `SWHeaderProperties` |
| `Rectangle 6669` 창 영역 | 1680 / 0 | 240 × 48 | `.hm-sw-header__window` |
| `Group 2913` 인쇄·가이드 pill | 1683 / 3 | 85 × 42 | `SWHeaderActions` |
| `창이동 motion` | 1768 / 0 | 88 × 48 | `SWHeaderWindowControls` |
| `header/btn_최소최대화`, `btn_종료` | 1856 / 0 | 32 × 48 각 | `SWHeaderWindowControls` |
숨김 레이어(확인, 구현 제외): `Ellipse`, `Component 761`, `Component 762`, `창이동`(65 × 48), `Component 724/mouse`.
Figma `get_design_context`에는 드롭다운 행(`HeaderDropdownBrownRow`), `ByLayer/ByBlock`, `Colorize`, 상세 pill(`레이어 상세` 등), GIF 가이드 카드도 포함되어 있으나 헤더에 표시되지 않는 하위 메뉴·팝업 구성요소입니다. 최초 범위(헤더 1개)에서는 구현하지 않았습니다. 이 중 레이어 Dropdown(`HeaderDropdownBrownRow`, `레이어 상세` pill)은 2026-10-16 추가 요청으로 구현했습니다(§5.2). 색상·선스타일·문자스타일·치수스타일 Dropdown과 ByLayer/ByBlock·Colorize·상세 pill·Input은 같은 날 후속 요청으로 구현했습니다(§5.3–§5.5). 각 `… 상세` 팝업의 내부 화면은 Figma 전달 전이라 범위 밖입니다.
## 4. 전체 레이아웃
`flex` 한 줄, 면 사이 2px. 1920px에서 Figma 좌표와 동일합니다(브라우저 측정, §15).
```
[홈 80(-17 겹침)] [파일명 310] 2 [객체 수정 1301: 그리기모드 90 | 레이어 234 · 색상 192 · 선 284 · 문자 164 · 치수 164 (gap 4) | 상세보기 157] 2 [창 240: pill 85 · 창 이동 88 · 최소화 32 · 종료 32]
```
- 홈은 80px 박스(overflow clip)이고 오른쪽 15px이 파일명 면 위에 겹칩니다. 파일명 글자는 x=70에서 시작하며 홈 박스의 투명 부분으로 보입니다.
- 객체 수정 면 안의 그리기모드 셀과 상세보기 블록은 Figma처럼 면의 바깥 테두리 위치(377 / 1678)에 맞춥니다(1px 음수 margin).
- 내부 필드 높이 38, 위 5px(테두리 1 + padding 4).
## 5. 컴포넌트별 내부
| 영역 | 구성(확인) |
|---|---|
| 홈 | 배경 SVG 92 × 72(위치 −8, −12, 그림자 포함 영역) + 로고 영역 32 × 17(위치 12, 16, Figma `bi_egbim` frame). 로고 이미지는 §9.1 |
| 파일명 | 글자 왼쪽 5px, 화살표 7 × 4(`#47AF91`, 오른쪽 12px), 글자–화살표 15px. 클릭 시 문서 Dropdown(§5.1) |
| 레이어 | 아이콘 3개(20 × 20, gap 0) · gap 8 · 라벨/값 |
| 색상 | 칩 60 × 20(검정 바탕, 안쪽 58 × 18 흰 1px 테두리, 기본 `#FFE500`) · gap 12 · 라벨/값 |
| 선스타일 | 선 미리보기 10 × 4 × 5(gap 2, 흰 바탕·검정 1px) · gap 12 · 선스타일 / 가중치(38) / 스케일(38), gap 12, 가운데 정렬 |
| 문자스타일 | 글꼴명 16px 700 · gap 12 · 라벨/값 |
| 치수스타일 | 치수 아이콘 60 × 15 · gap 12 · 라벨/값 |
| 상세보기 | 위 19px: `상세보기` 12px / 아래 19px: `속성Bar` · 구분선 1 × 12(검정, `1px 0 2px rgba(255,255,255,.25)`) · `속성창` |
| 인쇄·가이드 | pill 85 × 42(`rgba(0,0,0,.8)`, 1px `#092920`, radius 4, inset `0 2px 2px rgba(0,0,0,.25)`), 버튼 40 × 38 두 개 gap 1. 인쇄 버튼은 파일명 gradient·1px `#08251C`·radius 2 |
| 창 이동 | 2px 줄무늬 4px 간격(`#1C6A51` 32%, x3–85, y6, 높이 37) + 아이콘 32 × 30(위 13) |
| 최소화 | 10 × 2 막대 `#ADB0AF` 가운데 |
| 종료 | SVG 32 × 48 |
### 5.1 문서 Dropdown (Figma `header/document_tit` 26:10662)
Figma의 `hover` Variant는 목록이 펼쳐진 모습을 정의합니다. 구현은 **클릭으로 열고 닫으며** 마우스 Hover만으로는 열지 않습니다. 파일명 Hover는 글자만 `#FFC600`으로 바꾸며 노란 테두리는 표시하지 않습니다(§11.1).
| 항목 | Figma(확인) | 구현 |
|---|---|---|
| 열린 파일명 | 글자 `#FFC600`, 화살표 노란색 위쪽(`Polygon 353`) | 같음. 화살표는 CSS 삼각형을 180° 회전 |
| 위치 | 파일명(310 × 48) 바로 아래, `right: 0` | 헤더 아래로 겹치는 Overlay(헤더 49px 유지). 1920에서 바깥 박스 x 63–375(Figma와 같음). 좁은 폭에서도 화면 안에 있도록 왼쪽 기준 정렬 + `max-width: 100cqw − 71px` |
| 크기 | 항목 310 + 테두리 1px × 2, 예시 4개 = 목록 128 + 하단 40 | 바깥 312 × 170(4개). 목록 최대 320px 후 내부 세로 스크롤 |
| 배경 · 테두리 | `#103127`, 1px `#041611`, 하단 radius 4, `0 2px 4px rgba(0,0,0,.25)` | 같음 |
| 안쪽 그림자 | 목록·하단 영역 각각 `inset 0 4px 8px rgba(0,0,0,.4)` | 같음 |
| 문서 항목 | 310 × 32, padding 8, gap 8, 아이콘 29 × 12(`icon_dualmonitor`), 14px `#F0F7F5` | 같음(글자 700, 말줄임). Hover: Figma `header/document_tit/list`(26:10680) 그대로 검정 40% 배경 오버레이(`#103127` 위 합성), 글자·아이콘 변화 없음 |
| 하단 | 높이 40, padding 8, 위 구분선 `rgba(194,217,210,.2)` | 같음 |
| New / Open | `+ New` 54 × 24, `Open` 47 × 24, 배경 `rgba(182,208,201,.2)`, radius 2, padding 5 / 6, 14px 검정 70%, 양 끝 정렬 | 같은 값. Noto Sans KR로 55.4 / 50px(글자를 자르지 않음). Hover: Figma 26:10762 / 26:10805 그대로 배경 `rgba(182,208,201,.3)`, 글자 검정 100%. 크기·padding·radius 변화 없음 |
- 데이터 없음: `documents=[]`이면 목록 없이 하단만 표시합니다. `documents`를 전달하지 않으면 Dropdown 없이 기존 파일명 버튼입니다(하위 호환).
- 패키지에는 문서명·파일 형식을 넣지 않습니다(Figma 예시 `Drawing1.grm`은 Storybook 테스트 데이터로도 쓰지 않음). 아이콘은 기본값으로 Figma `icon_dualmonitor`를 쓰고 항목별(`icon`)·전체(`documentIcon`)로 바꿀 수 있습니다.
- 동작: 클릭/Enter/Space로 열고 닫기, ↓로 열면서 첫 항목 Focus, ↓↑ 이동, Escape·바깥 클릭으로 닫기(Focus는 파일명으로 복귀), Tab으로 목록 밖으로 나가면 닫기(Focus 갇힘 없음). 파일명 버튼은 `aria-expanded` / `aria-controls`로 목록(`role="group"`, `aria-label="문서 목록"`)과 연결됩니다.
#### 5.1.1 현재 문서 · 문서 닫기 (사용자 추가 요구, Figma에 없음)
2026-10-16 사용자 최종 지시. 위 Figma 확인 항목과 구분해 기록합니다.
| 항목 | 정책 |
|---|---|
| 목록 순서 | `documents` 배열 순서를 그대로 표시합니다. 현재 문서를 앞으로 옮기지 않으며, 다른 문서를 선택해도 순서·위치가 바뀌지 않습니다. |
| 현재 문서(`activeDocumentId`) | 문서명 글자만 `#FFC600`. 나머지 문서는 `#F0F7F5`. 테두리·배경·아이콘 변화 없음(다색 아이콘은 레이어 상태 아이콘과 같은 이유로 유지). 버튼에 `aria-current="true"` |
| Hover와 선택 | Hover는 배경(검정 40%, Figma 26:10680), 선택은 글자색이라 동시에 적용돼도 구분됩니다. |
| X(문서 닫기) | `onDocumentClose`를 주면 **모든 행** 오른쪽 끝(24 × 24, 오른쪽 4px)에 X가 있습니다. 평상시 숨김(opacity 0, 자리는 항상 확보)이며, **마우스를 올린 행** 또는 **키보드 Focus가 있는 행**에만 표시됩니다(선택 여부와 무관). Dropdown이 아니라 그 문서를 닫는 버튼이며 이름은 `문서 닫기`, 툴팁은 `<문서명> 닫기` |
| X 모양 | 8 × 8 선 아이콘, `rgba(240,247,245,.7)`. X 자체 Hover: 배경 `rgba(182,208,201,.2)`, 아이콘 `#F0F7F5`(구현값). 노란 테두리 없음 |
| 배치 | 행 높이 32px 유지. 문서명은 X 자리 앞에서 말줄임(1920에서 문서명 오른쪽 끝 338 < X 시작 346). X 표시·숨김으로 아이콘·문서명이 움직이지 않음 |
| 동작 | X는 선택 버튼과 형제 버튼이라 `onDocumentSelect`가 함께 호출되지 않습니다. X 후 Dropdown은 열린 채로 두고 Focus는 목록 첫 항목으로 옮깁니다(행이 사라져도 Focus 유실 없음). 실제 문서 닫기·목록 갱신·다음 선택 문서 결정은 호스트가 합니다. |
- 접근성 확인 필요: Figma New / Open 글자색(검정 70%)은 어두운 배경에서 대비가 낮습니다. Figma대로 구현했으며 변경 여부는 디자인 결정 사항입니다.
### 5.2 레이어 Dropdown (Figma `header/menu/레이어` 26:10940, 2026-10-16)
Figma 기준 5개 Node: ① `header/menu/레이어` 26:10940(default 26:10941 / hover 26:10952 / Variant3 = 펼침 26:10963) ② `icon_전구` 26:10810 ③ `icon_눈꽃` 26:10855 ④ `icon_자물쇠` 26:10912(각 20 × 20, Variant 4개) ⑤ `header/dropdown/brown/row` 26:10687(Default 26:10688 / Hover 26:10695). 모두 Figma MCP `get_design_context`·screenshot으로 확인했습니다.
`layers`를 전달하면 레이어 필드가 Dropdown 버튼이 됩니다(`SWHeaderLayerMenu`). 전달하지 않으면 기존 필드 버튼(`onLayerOpen`)과 DOM·외형이 같습니다(하위 호환). 필드 Hover는 기존 노란 Gradient Border + 글자 `#FFC600`이며 **클릭으로만 열립니다**.
| 항목 | Figma(확인) | 구현 |
|---|---|---|
| 필드 | 234 × 38, radius 3, 기존 gradient·그림자, 아이콘 3개 + `레이어` 라벨/값 | 기존 필드 그대로. 1920에서 x 467(브라우저 466.7) |
| 펼친 필드(Variant3) | 테두리 `#081813`, 위쪽만 radius 3, 라벨·값 `#FFC600` | 같음(`data-expanded`, `aria-expanded`) |
| 펼침 외곽선 | `#A67D03`(=노란 테두리 토큰) 1px, 236 × 320, 위치 −1 / −1, radius 4 | 기존 `--hm-sw-header-hover-border-gradient`로 필드+목록 바깥 1px 링. 크기 변화 없음 |
| 목록 위치 | 필드 바로 아래, 같은 왼쪽 | 필드 아래 Overlay(`top: 100%`), 헤더 49px·다른 필드 위치 불변 |
| 목록 | 234 × 280(행 30 × 8 + 하단 40), `#27241D`, 검정 1px(inside), 하단 radius 3, `0 4px 8px rgba(16,48,38,.25)` | 같음. 테두리는 overlay라 행이 234 × 30 그대로 |
| 행 | 234 × 30, padding 12, 아이콘 20 × 3(gap 0) · 8 · 견본 16 · 8 · 이름(14, `#F6F4F2`) | 같음(이름 Noto Sans KR 14 / 700 / 0.04em, 말줄임). 1920 측정: 아이콘 x+12, 견본 x+80, 이름 x+104(+1 padding) |
| 색상 견본 | 16 × 16 검정 radius 2, 안쪽 1px 위치 흰색 + 1px `#C9C9C9` radius 1 | 같음. 안쪽 색은 `layer.color`(기본 `#FFFFFF`) |
| 행 Hover | 배경 `#62533D`, 글자 `#FF5C00` | 같음. 이 목록 전용(다른 컨트롤의 노란 Hover와 별개) |
| 아이콘 Hover | 전구: 검정 25% 배경 radius 2 / 눈꽃·자물쇠: Hover SVG(같은 아이콘 + 검정 25% 타일) | 같음. 눈꽃·자물쇠는 Figma Hover SVG를 그대로 겹쳐 표시 |
| Off → Off Hover | Smart Animate, ease-out 300ms | `transition: 300ms ease-out`(Off만). On Hover는 즉시. Reduced Motion 시 전환 없음 |
| 하단 | 높이 40, padding 8 / 12, 1px `rgba(197,177,158,.15)`(inside), 오른쪽 정렬 | 같음 |
| 레이어 상세 | pill radius 24, padding 5 / 10, gap 4, 배경 `rgba(197,177,158,.15)`, 아이콘 14(`ico_layer`), 글자 14 흰색 opacity .35 | 같음. 글자 폭 Noto 69.3px(Figma Dotum 75px), 버튼 107.3 × 24 |
| 목록 높이 상한 | 예시 8행 | **구현값** 240px(8행). 넘치면 목록만 세로 스크롤, 하단 고정 |
#### 5.2.1 아이콘 On/Off와 데이터 의미
| 아이콘 | 컬러(Default/Variant2) | 회색(Variant3/Variant4) | 데이터 |
|---|---|---|---|
| 전구 | `visible !== false` | `visible === false` | `onLayerVisibilityChange(layer, nextVisible)` |
| 눈꽃 | `frozen !== true` | `frozen === true` | `onLayerFreezeChange(layer, nextFrozen)` |
| 자물쇠 | `locked !== true` | `locked === true` | `onLayerLockChange(layer, nextLocked)` |
- **확정 매핑(2026-10-16 사용자 확정)**: 전구 컬러 `visible=true` / 회색 `visible=false`, 눈꽃 컬러 `frozen=false`(동결 해제) / 회색 `frozen=true`(동결), 자물쇠 컬러 `locked=false`(잠금 해제) / 회색 `locked=true`(잠금). 클릭하면 해당 boolean의 반전값을 Callback으로 전달합니다(전구 `!visible`, 눈꽃 `!frozen`, 자물쇠 `!locked`). Figma 원본 SVG와 Default/Hover 디자인은 그대로입니다. 구현 위치는 `layerIconOn()`(`SWHeaderLayerIcons.tsx`) 한 곳입니다.
- `aria-pressed`는 아이콘 색이 아닌 데이터 의미(표시됨 / 동결됨 / 잠김)를 따릅니다. 이름은 `<레이어명> 표시|동결|잠금`.
- 아이콘은 데이터만 반영합니다. 클릭 시 Callback으로 다음 값을 알리고, 실제 변경·도면 반영은 호스트가 `layers`를 갱신해 수행합니다. Hover는 CSS이며 데이터를 바꾸지 않습니다.
- Callback이 없는 아이콘은 버튼이 아닌 상태 표시(`role="img"`, `<이름> 표시 켜짐/꺼짐`)이며 Hover·클릭 반응이 없습니다.
- 필드의 아이콘 3개는 현재 레이어(`activeLayerId`) 상태를 표시합니다(필드 안 아이콘은 장식, 버튼 아님). 현재 레이어가 없으면 모두 컬러(기존과 같음).
#### 5.2.2 선택 · 이벤트 · 키보드
| 동작 | 결과 |
|---|---|
| 필드 클릭 / Enter / Space | 열기·닫기(`onLayerOpen`도 매 클릭 호출, `onLayerMenuOpenChange`) |
| 행 선택(견본·이름 버튼) | `onLayerSelect` 1회 → 목록 닫힘, Focus는 필드로 복귀(문서 Dropdown과 같음) |
| 전구 / 눈꽃 / 자물쇠 | 해당 Callback 1회, `onLayerSelect` 호출 없음, 목록 유지, Focus는 아이콘에 유지 |
| 레이어 상세 | `onLayerDetail` 1회 후 목록 닫힘. Callback이 없으면 Figma 외형 그대로 `disabled`(추가 흐림 없음), 임의 팝업 없음 |
| 바깥 pointerdown · Escape · Tab으로 벗어남 | 닫힘(Escape는 필드로 Focus 복귀). 다른 메뉴(문서·더보기)를 누르면 바깥 클릭으로 닫혀 Overlay가 겹치지 않음 |
| ↓ (닫힘) | 열고 현재 레이어(없으면 첫 행) 이름 버튼에 Focus |
| ↓ ↑ (열림) | 행 이동, 같은 열(전구·눈꽃·자물쇠·이름) 유지, 하단 버튼 포함 순환 |
| ← → | 행 안에서 아이콘·이름 사이 이동 |
- 행은 `<li>` 안에 아이콘 버튼 3개와 선택 버튼 1개를 형제로 둡니다(중첩 버튼 없음). 따라서 아이콘 클릭이 행 선택으로 전파되지 않습니다.
- 현재 레이어(**사용자 규칙 확장, Figma 없음**): 문서 Dropdown과 같이 목록 순서를 유지하고 이름만 `#FFC600`(Hover 중에도 유지, 배경만 갈색). 버튼에 `aria-current="true"`.
- `disabled` 레이어는 선택·아이콘 버튼 모두 비활성(기존 `opacity .4`), 행 Hover 없음.
- 데이터 없음: `layers=[]`이면 목록 없이 하단(레이어 상세)만 표시하고 필드는 `layer.name`을 표시합니다.
#### 5.2.3 반응형
목록은 234px를 유지하고 필드 왼쪽에 맞춰 엽니다. 헤더(또는 화면) 오른쪽을 넘으면 넘는 만큼 왼쪽으로 이동하고(1px 여백), 이때 노란 외곽선은 목록만 감쌉니다(`data-shifted`). 520 미만의 필드 아이콘 숨김·레이어명 말줄임 등 기존 단계는 그대로입니다. 측정 결과는 [레이어 Dropdown 검증 기록](./sw-header-layer-dropdown-verification-2026-10-16.md)에 있습니다.
### 5.3 색상 · 선스타일 · 문자스타일 · 치수스타일 Dropdown (2026-10-16)
Figma 8개 노드(모두 MCP `get_design_context` + Plugin API로 확인): ① `header/menu/색상` 26:10986(Default 26:10987 / hover 26:10996 / 펼침 26:11005) ② `header/menu/색상_addUserColor` 26:11036(26:11037 / 26:11046 / 26:11055) ③ `ByLayer` 26:12666(Default 26:12667 / hover 26:12669 / on 26:12671) ④ `ByBlock` 26:12659(26:12660 / 26:12662 / 26:12664) ⑤ `header/menu/선스타일` 26:11104(26:11105 / 26:11124 / 26:11143) ⑥ `header/menu/문자스타일` 26:11181(26:11182 / 26:11191 / 26:11200) ⑦ `header/menu/치수스타일` 26:11215(26:11216 / 26:11229 / 26:11242) ⑧ `header/dropdown/brown/row` 26:10702(Default 26:10703 / Hover 26:10706). 보조: 행 Variant(26:10713 / 26:10748 / 26:10755 / 26:10718 / 26:10729), `input` 26:11656, `header/menu/색상/input` 26:11308, `btn_상세/*` 26:10774 / 26:10781 / 26:10788 / 26:10795, `btn_colorize` 26:11302.
데이터 Props(`colors`, `lineTypes`/`lineWeights`, `textStyles`, `dimensionStyles`)를 주면 각 필드가 Dropdown 버튼이 됩니다(`SWHeaderColorMenu`, `SWHeaderLineStyleMenu`, `SWHeaderTextStyleMenu`, `SWHeaderDimensionStyleMenu`). 주지 않으면 기존 필드 버튼과 외형·동작이 같습니다(하위 호환). 동작 계약은 레이어 Dropdown(§5.2.2)과 같습니다: 클릭/Enter/Space로 열고 닫기(Hover로 열지 않음), ↓로 열며 현재 항목(없으면 첫 항목) Focus, ↓↑ 이동, Escape·바깥 pointerdown·Tab 이탈로 닫기, 한 번에 하나만 열림, 헤더 49px·다른 필드 위치 불변, 목록은 헤더 밖으로 넘치면 안쪽으로 이동.
| 항목 | Figma(확인) | 구현 |
|---|---|---|
| 펼친 필드(Variant3) | 테두리 `#081813`, 위쪽만 radius 3, 라벨·값 `#FFC600`, 선/글꼴/치수 미리보기 `#FFC600` | 같음(`data-expanded`). 노란 링: 색상·문자·치수는 필드+목록 1px(194 × 198 / 166 × 140), 선스타일은 필드 링 286 × 39 + drop_wrap 링 |
| 공통 면 | `#27241D`, 검정 1px(inside), 하단 radius 3 | 같음(레이어 Dropdown 토큰 재사용, 테두리는 overlay) |
| 색상 목록 | 192 × 158(사용자 색상 2개 192 × 182), top 38, padding-top 10, gap 10, 가운데 정렬 | 같음. 브라우저 측정 ByLayer 그룹 (12.5, 10) 167 × 30, 팔레트 (12, 50) 168 × 22, 도구 (15, 82) 162 × 26, 하단 y 118 × 40 |
| ByLayer / ByBlock | 각 82.5 × 28, 검정 167 × 30 타일(p 1, radius 4). Default `rgba(98,83,61,.3)` + inner shadow(ByLayer `2px -2px 4px` · `-5px 2px 4px`, ByBlock `-2px -2px 4px` · `6px 2px 4px`, `rgba(0,0,0,.6)`), on `#62533D` + `1px 2px 2px rgba(217,207,187,.25)` · `-1px -2px 2px rgba(0,0,0,.4)`, 글자 `#D4CBBD` | 같음. inner shadow는 Figma처럼 글자 위 overlay. `aria-pressed`, 상호 배타 표시는 `colorMode` |
| 색상 칩 | 22 × 22: 검정 20 radius 2 + 18 견본, 1px `#C9C9C9`, radius 1. 기본 7색 `space-between`, 사용자 색상 gap 2 | 같음. 사용자 색상은 줄바꿈, 팔레트+4행(118px) 초과 시 팔레트만 세로 스크롤(구현값). 현재 색과 같은 칩 `aria-pressed`(시각 표시 없음, Figma 없음) |
| 색상 추출 | 24 × 24, `#C5B19E → #5F554C`, 1px 검정 40%, radius 3, drop-shadow `0 2px 2px rgba(0,0,0,.4)`, 아이콘 18(`colorize`) | 같음(`color-pick.svg` = Figma 원본). `onColorPick` 후 닫힘. 실제 추출은 호스트 |
| 색상 입력 | 130 × 26, 칩(p 2) + 값(px 8, 빨강 `#E60403`). hover 1px `rgba(197,177,158,.2)`, 편집 시 값 영역 검정 + 왼쪽 구분선 | 같음(`SWHeaderInput` 사용). `r,g,b`(0–255) 또는 `#rrggbb` 검증, 잘못된 값 복원. "컬러칩 클릭"(색 선택 팝업)은 Figma에 "추후 디자인 진행예정" → 미구현 |
| 선스타일 목록 | drop_wrap 468 × 192(x −92, y 37, 1px `#A67D03` inside), drop 466 × 190, 열 182 / 142 / 142(구분선 `rgba(197,177,158,.15)`) | 같은 구조. **차이**: 선 종류 열은 Noto Sans KR에서 "Continuous"(약 88px)가 잘리지 않도록 최소 182 · 최대 240px로 늘어나 1920에서 473.2 × 192(x −94) |
| 선 종류 행(26:10702) | 182 × 30, padding 12, gap 8, 미리보기 70 × 2(`#F6F4F2` + 검정 1px outside), Hover `#62533D` · 선·글자 `#FF5C00` | 같음(1920 측정 미리보기 x+12, y 14). CENTER 22·7·22·7, HIDDEN 33·33(`swHeaderLinePatterns`), 패턴은 데이터 `pattern` |
| 가중치 · 스케일 | 142 × 30 행, ByDefault 값 오른쪽, User·스케일은 빨간 수정 가능 글자(input) | 같음. User(`editable`)와 스케일은 `SWHeaderInput`(가중치 0 이상, 스케일 0 초과) |
| 문자스타일 목록 | 164, 행 30(글꼴 16 + 이름 14, gap 12), 하단 40(px 10, 테두리 .25, 왼쪽 정렬) | 같음. **차이**: 글꼴 칸 38px(Dotum) → Noto "Arial" 42px라 최대 56px까지 내용 폭. Figma Variant3의 상세 버튼은 hover Variant(.25)로 배치되어 있으나 Hover 정책상 Default(.15)로 표시 |
| 치수스타일 목록 | 164, 행 30(미리보기 60 × 15 + 이름, gap 12), 하단 40(px 10, 테두리 .25, 오른쪽 정렬) | 같음. 행 미리보기는 Figma 행 자산 `menu-dimension-row.svg`(`#F6F4F2`)와 Hover `menu-dimension-row-hover.svg`(`#FF5C00`, foreground fill 1곳만 다름) |
| 하단 상세 버튼 | `btn_상세/색상·선스타일·문자스타일·치수스타일`: 아이콘 14, gap 4, padding 5 / 10, `rgba(197,177,158,.15)`, radius 24, 글자 35% | 레이어 상세와 같은 스타일. Callback 없으면 Figma 외형 그대로 비활성. 상세 팝업은 미구현 |
- 선택: 색상·선스타일은 여러 속성을 연속 변경하도록 선택 후에도 열린 상태를 유지하고(구현 결정), 문자·치수스타일은 행 선택 후 닫힙니다. 각 열은 자기 속성의 Callback만 호출합니다(선 종류 → `onLineTypeSelect`만, 가중치·스케일 유지).
- 현재 항목(`active*Id`, 미지정 시 이름 일치)은 레이어 목록과 같이 이름만 `#FFC600`이며 `aria-current="true"`. Hover(배경)와 구분됩니다.
- **Hover 정책(2026-10-16 사용자 확정)**: Dropdown 안 독립 버튼(ByLayer·ByBlock·색상 칩·사용자 색상·색상 추출·`… 상세`)은 배경·글자·아이콘·테두리·그림자·밝기·크기·위치 변화가 없습니다. Figma Hover Variant(ByLayer 주황 글자, 상세 .25 등)는 비교 자료입니다. 클릭·On·`:focus-visible`은 유지합니다. **예외**: 목록 행(레이어·선 종류·가중치·스케일·문자·치수)의 Figma 행 Hover와 Input의 Figma Hover(수정 가능 표시)는 유지합니다. 문서 Dropdown Hover는 변경하지 않았습니다.
- 동적 데이터: 모든 목록은 `id` 기반 배열 Props이며 추가·삭제·이름/값 변경·재정렬에 소스 수정이 필요 없습니다. 현재 항목이 삭제되면 선택 표시가 없어지고 필드는 값 Props를 표시합니다. 긴 목록은 Dropdown 안에서만 스크롤합니다(행 240px).
- 좁은 헤더: 목록은 헤더 안쪽으로 이동하고, 헤더 폭 480 미만에서는 선스타일 3열을 세로로 쌓습니다(가로 스크롤·화면 밖 이탈 없음, 필요 시 세로 스크롤).
### 5.4 반응형 More — 대표 필드 전환 (2026-10-16)
대표 슬롯 = 객체 수정 영역의 첫 필드 칸(레이어 필드 자리). 선택 근거: 1920 배열의 첫 칸이자 모든 Breakpoint에서 끝까지 남는 칸이며, 로고·파일명·창 제어 영역과 분리되어 있습니다.
- 더보기에서 필드 항목(색상·선/문자/치수스타일)을 고르면 그 필드가 대표 칸(`order: -1`)에 표시되고 Dropdown이 열리며 Focus가 필드로 이동합니다. 레이어는 그동안 더보기(tier `slot`)로 옮겨지고, 더보기 → 레이어로 복귀합니다. 화면에 보이는 필드는 더보기에서 빠집니다.
- 전환 상태는 `SWHeader` 내부 UI 상태(`data-promoted`)이며 데이터와 분리됩니다. 필드 클릭과 같은 `on*Open` 1회 + `on*MenuOpenChange(true)`만 호출하고 데이터 Callback은 호출하지 않습니다. 값·다른 필드 상태는 그대로입니다.
- CSS는 해당 필드가 더보기에 있는 폭(선/문자/치수 < 1800, 색상 < 900)에서만 전환을 적용하므로 1920 배열과 기존 Breakpoint는 바뀌지 않습니다. 대표 필드는 남는 폭에 맞춰 줄어들 수 있고(값 말줄임), 객체 수정 영역 밖으로 넘치지 않습니다.
- 검증(16개 폭 1920–390): [검증 기록](./sw-header-dropdowns-stroke-input-verification-2026-10-16.md).
### 5.5 레이어 상세 버튼 · 수정 가능한 Input (2026-10-16)
- `btn_상세/레이어` 26:10767(Default 26:10768 / Hover 26:10771): 기존 구현(§5.2)이 Default와 같습니다 — 아이콘 14(`layer-detail.svg` = Figma `ico_layer` Default, Hover 아이콘은 opacity .35로 다름), gap 4, padding 5 / 10, `rgba(197,177,158,.15)`, radius 24, 글자 35% + 흰색 0.4px Stroke. Figma 113 × 24는 Dotum 기준이고 Noto Sans KR에서 107.3 × 24(기존 APPROVED DIFFERENCE). **Hover(.25 · radius 25)는 사용자 정책상 적용하지 않으며** Storybook에 Figma 참조 이미지로만 둡니다. Callback은 기존 `onLayerDetail`을 재사용하고 상세 팝업은 만들지 않습니다.
- `input` 26:11656(Default 26:11657 58 × 20 / hover 26:11659 58 × 24 / on 26:11661 58 × 24) → 공개 `SWHeaderInput`: padding 2 / 4 / 2 / 6, radius 2, 14px `#E60403` + 검정 1.3px Text Stroke. Hover 1px `rgba(212,203,189,.25)`, 편집(Focus) `#0F0D0A` + 1px `.3`; 테두리가 padding 1px을 대신해 글자 위치는 그대로입니다. 폭은 값 길이(`4.0526` = 58px, 브라우저 측정 일치).
- `cursor` 26:11664(1 × 14 `#E60403`, Variant2 투명): 네이티브 caret을 `caret-color: #E60403`으로 표시하고 깜박임은 브라우저 기본입니다. 가짜 커서를 만들지 않아 커서 위치·선택·IME가 기본 동작합니다. caret 높이는 글꼴 metrics를 따르므로 14px과 정확히 같다고 보증하지 않습니다(NOT VERIFIED).
- 동작: 편집 중 임시값(`onValueChange`)과 확정값(`value`)을 분리합니다. Enter·Focus 이동·감싼 Dropdown 닫힘에서 `parse`(기본: `valueType` text = 비어 있지 않은 텍스트, number = 소수) 통과 + 변경 시에만 `onCommit`, 잘못된 값은 확정값으로 복원, Escape는 편집 취소(`onCancel`, 첫 Escape는 Dropdown을 닫지 않음), IME 조합 중 Enter는 확정하지 않음. `readOnly`(또는 `onCommit` 없음)·`disabled` 지원.
## 6. Typography
| 용도 | Figma | 구현 |
|---|---|---|
| 파일명 | Dotum 14, tracking 0.84px | Noto Sans KR 14 / 700 / 0.06em, `#F0F7F5` |
| 그리기모드·필드 값·토글 | Dotum 14(bold 스타일명), tracking 0.56px | Noto Sans KR 14 / 700 / 0.04em, `#F6F4F2` |
| 필드 라벨·상세보기 | Dotum 12, tracking 0.24px | Noto Sans KR 12 / 400 / 0.02em |
| 문자스타일 글꼴명 | Dotum 16 | Noto Sans KR 16 / 700 |
- Font Weight는 400 / 700만 사용합니다(브라우저 측정 확인). 600·750·800 없음.
- ~~Figma 글자는 검정·밝은색 두 레이어를 겹친 구조입니다. 사용자 요청에 따라 글자 외곽선·겹침 효과를 넣지 않고 밝은색 한 겹만 사용합니다.~~ (과거 기준 보존. 2026-10-16 후속 요청으로 §6.1 Text Stroke를 복원했습니다.)
- 폰트 파일은 패키지에 포함하지 않습니다. 소비 프로젝트가 Noto Sans KR을 로드하며, 없으면 `Malgun Gothic` → `Apple SD Gothic Neo` → `sans-serif`로 대체됩니다.
### 6.1 Text Stroke (2026-10-16)
Figma Plugin API로 26:8944와 메뉴 8개·문서·레이어 Component Set의 모든 TEXT 노드(Variant 포함, 숨김 제외)를 조사했습니다. 모든 Stroke는 **OUTSIDE**입니다.
| 영역 | Figma 구조 | 구현 |
|---|---|---|
| 14px 값(파일명, 그리기모드, 레이어·색상·선/문자/치수 값, 가중치·스케일 값, 속성Bar·속성창, 문서·레이어 목록 이름, 새 Dropdown 행·ByLayer/ByBlock) | 같은 글자 2층: 아래층 검정 Stroke 1.3px, 위층 채움 + 같은 색 Stroke 0.3px | `text-shadow` 16방향 1.3px 검정 원(`--hm-sw-header-text-outline`) + `-webkit-text-stroke-width: 0.6px`(가운데 정렬 → 바깥 0.3px, `currentColor`) |
| 16px 글꼴 미리보기(필드·문자스타일 행) | 14px과 같음(1.3 + 0.3) | 같음 |
| 12px 라벨(레이어·색상·선스타일·가중치·스케일·문자스타일·치수스타일·상세보기) | 채움 + 검정 Stroke 1px | `--hm-sw-header-label-outline`(1px 검정 16방향), 자기 색 Stroke 없음 |
| 수정 가능 글자(색상·가중치·스케일·상세 Input) | 아래층 검정 1.3px + 위층 `#E60403` 0.3px | 14px과 같음(`SWHeaderInput`) |
| `… 상세` pill(레이어·색상·선/문자/치수) | 흰색 + 흰색 Stroke 0.4px, 검정 없음, 35% | `-webkit-text-stroke-width: 0.8px`(바깥 0.4px) |
| `+ New` / `Open` | 검정 70% + 검정 0.4px | **미적용**(2026-10-16 추가 요청: 두 버튼은 기존 상태 유지) |
| 더보기 메뉴, 창 이동 Tooltip | Figma에 없음 / Stroke 없음 | 미적용 |
- `-webkit-text-stroke` 단독은 획 중심 기준이라 글자 안쪽을 깎으므로 검정 외곽선은 `text-shadow`로 그립니다(글자 뒤, 글자 모양을 덮지 않음). Hover·Selected·행 Hover는 채움색만 바뀌고 외곽선은 검정 그대로입니다. Input의 caret·선택 영역은 shadow 위에 그려져 가려지지 않습니다.
- 말줄임 텍스트(`overflow`)는 `overflow: clip` + `overflow-clip-margin: 2px`로 외곽선이 상자 경계에서 잘리지 않게 했습니다(`overflow-clip-margin` 미지원 Safari 등에서는 가장자리 1px 내외가 잘릴 수 있음 — NOT VERIFIED).
- 글꼴·크기·자간·기준선·Font Weight(400/700)는 바꾸지 않았습니다. Figma는 Dotum Regular + Stroke로 굵기를 표현하고 구현은 Noto Sans KR 700 + 같은 Stroke이므로, 구현 글자가 Figma보다 약간 굵게 보입니다(남은 차이, `--hm-sw-header-text-stroke-width`로 조정 가능).
- 레이어 Dropdown 이름·레이어 상세에도 같은 기준이 적용됩니다(CSS만, 레이어 컴포넌트·매핑 코드는 변경 없음).
## 7. Color · Gradient
| 토큰 | 값 | 근거 |
|---|---|---|
| `--hm-sw-header-background` | `#060606` | Figma header frame |
| `--hm-sw-header-text-color` | `#F6F4F2` | Figma `--text-brown100` |
| `--hm-sw-header-title-color` | `#F0F7F5` | Figma `--text-green100` |
| `--hm-sw-header-arrow-color` | `#47AF91` | Figma `--solid-arrow` |
| `--hm-sw-header-selected-color` | `#FFC600` | 사용자 확정 선택 상태 |
| `--hm-sw-header-focus-color` | `#F0F7F5` | 구현값(Figma 없음) |
| `--hm-sw-header-hover` | `rgba(255,255,255,0.08)` | 구현값. 현재 최소화·종료 버튼과 더보기 메뉴 항목에만 사용 |
| `--hm-sw-header-hover-border-gradient` | 180deg `#A67D03` 0% → `#F2CC0F` 16.41% → `#FADD4F` 37.19% → `#F2CC0F` 62.03% → `#926E03` 87.88% | Figma `header/btn_home` hover 테두리(26:10655) |
| `--hm-sw-header-document-background` | 사용자 확정 180deg gradient(§10 B) | 사용자 확정 = Figma 동일 |
| `--hm-sw-header-object-background` | 사용자 확정 0deg gradient(§10 A) | 사용자 확정 = Figma 동일 |
| `--hm-sw-header-property-background` | 사용자 확정 반투명 검정 gradient(§10 C) | 사용자 확정 = Figma 동일 |
| `--hm-sw-header-pill-background` | `rgba(0,0,0,0.8)` | Figma `Rectangle 6959` |
| `--hm-sw-header-stripe-color` | `rgba(28,106,81,0.32)` | Figma 창 이동 줄무늬 |
| `--hm-sw-header-window-icon-color` | `#ADB0AF` | Figma 최소화 막대 |
창 영역(`Rectangle 6669`)과 인쇄 버튼은 파일명 gradient·테두리를 Figma 그대로 재사용합니다.
## 8. Border · Radius · Shadow
| 면 | Border | Radius | Shadow |
|---|---|---|---|
| 파일명 | 위·오른쪽·아래 1px `#0E4C3A`, 왼쪽 0 | 0 | none |
| 객체 수정 | 1px `#135643` | 6px | none |
| 속성 필드(5개 메뉴, 더보기 버튼) | 1px `#08251C` | 3px | `0 1px 4px 0 rgba(9,39,30,.7)` |
| 창 영역 | 1px `#0E4C3A`(4면, pseudo) | 0 | none |
| 인쇄·가이드 pill | 1px `#092920` | 4px | inset `0 2px 2px rgba(0,0,0,.25)` |
## 9. 아이콘 · Assets
Figma MCP에서 내려받은 원본 SVG 13개(종료 hover 포함)와 Hover 색 Variant 2개, 총 15개를 `packages/sw/src/assets/`에 두고 build 시 data URI로 `dist/index.js`에 포함합니다. 임시 Figma URL 참조는 없습니다(`dist` 검색 0건). 다색 SVG 원본을 수정·재작성하지 않았습니다. Hover 색 Variant(`print-hover.svg`, `popup-guide-hover.svg`)는 원본을 복사해 `fill="#B6D0C9"` 1곳만 `#FFC600`으로 바꾼 파일이며 path·mask·filter·gradient는 원본과 같습니다(§11.2).
| 파일 | Figma 요소 | 렌더링 크기 |
|---|---|---|
| `home-bg.svg` | `Rectangle 7508` 홈 배경(Default) | 92 × 72 |
| `home-bg-hover.svg` | `Rectangle 7509` 홈 배경(hover, 노란 gradient 테두리) | 92 × 72 |
| `document-icon.svg` | `icon_dualmonitor` 문서 Dropdown 기본 아이콘(29 × 12 영역, 그림자 포함) | 37 × 19(위치 −4, −3) |
| `layer-union.svg`, `layer-ellipse.svg` | 레이어 아이콘 1(합성) | 14 × 18, 14 × 14 |
| `layer-state-b.svg`, `layer-state-a.svg` | 레이어 아이콘 2, 3 = `icon_눈꽃` / `icon_자물쇠` Default(컬러) | 20 × 20 |
| `layer-ellipse-off.svg` | `icon_전구` Variant3/4 회색 전구알(26:10833, union·막대는 Default와 같음) | 14 × 14 |
| `layer-freeze-hover.svg`, `layer-freeze-off.svg`, `layer-freeze-off-hover.svg` | `icon_눈꽃` Variant2 / 3 / 4(26:10870 / 26:10884 / 26:10898) | 20 × 20 |
| `layer-lock-hover.svg`, `layer-lock-off.svg`, `layer-lock-off-hover.svg` | `icon_자물쇠` Variant2 / 3 / 4(26:10919 / 26:10925 / 26:10931) | 20 × 20 |
| `layer-detail.svg` | `ico_layer` 레이어 상세 아이콘(26:1474) | 14 × 14 |
레이어 Dropdown(§5.2) 자산 8개는 2026-10-16 Figma MCP에서 내려받은 원본입니다. 기존 `layer-union/ellipse/state-a/state-b`는 Figma 원본과 mask id 외 동일함을 비교해 그대로 재사용했습니다. Hover SVG는 Default 아이콘 + 검정 25% 20 × 20 타일(radius 2)이며 그 외 차이가 없음을 확인했습니다.
| `menu-dimension-ico.svg` | 치수 아이콘 | 60 × 15 |
| `menu-dimension-row.svg`, `menu-dimension-row-hover.svg` | 치수스타일 Dropdown 행 미리보기 Default(`#F6F4F2`) / Hover(`#FF5C00`)(26:10729, 2026-10-16 Figma 원본, foreground fill 1곳만 다름) | 60 × 15 |
| `color-pick.svg` | `btn_colorize` 아이콘 `colorize`(26:11303, 2026-10-16 Figma 원본) | 18 × 18 |
| `print.svg` | 인쇄 | 24 × 22.9(버튼 inset 20.1/20/19.62%) |
| `print-hover.svg` | 인쇄 Hover(원본의 `#B6D0C9` → `#FFC600`만 변경, Figma Variant 없음) | 24 × 22.9(같은 위치) |
| `popup-guide.svg` | `꾸러미ver2/팝업가이드` | 40 × 38 |
| `popup-guide-hover.svg` | 팝업 가이드 Hover(원본의 `#B6D0C9` → `#FFC600`만 변경, Figma Variant 없음) | 40 × 38(같은 위치) |
| `window-motion.svg` | 창 이동 아이콘 | 32 × 30 |
| `close.svg` | `header/btn_종료` Default(26:12162) | 32 × 48 |
| `close-hover.svg` | `header/btn_종료` Variant2 = hover(26:12166): `#E40000` 32 × 48 + 흰 X, X path는 Default와 같음 | 32 × 48 |
파일명 화살표(Figma `Polygon 352`, 단색)는 선택 시 색을 바꾸기 위해 SVG 대신 CSS 삼각형(7 × 4)으로 그립니다. 기존 뷰어의 임시 `HM` 텍스트 로고는 사용하지 않았습니다.
### 9.1 로고 (확정 디자인 아님)
SW Header는 여러 SW 프로그램이 공통으로 사용합니다. 따라서 **로고 영역의 배치는 공통 디자인이고, 로고 이미지 자체는 사용하는 SW 프로그램이 결정하는 항목**입니다. 패키지는 특정 제품 로고·브랜드명·파일 경로를 포함하지 않습니다.
| 항목 | 기준 | 구분 |
|---|---|---|
| 로고 영역 | 홈 버튼(80 × 48) 안 x 12 / y 16, 32 × 17 (Figma `bi_egbim` frame). 토큰 `--hm-sw-header-logo-width` / `-logo-height` | 공통 디자인(확정) |
| 홈 배경 | `home-bg.svg` 사선 녹색 면 + 0–65px 파일명 gradient 바탕 | 공통 디자인(확정) |
| 로고 이미지 | `logoSrc`(이미지 URL) 또는 `logo`(React 요소)로 각 프로그램이 전달. 비율이 달라도 영역 안에 `contain`으로 맞추고 넘치지 않음 | 프로그램별 결정 |
| 로고 미전달 | 로고를 그리지 않음. 홈 배경·파일명·객체 수정 영역 위치는 그대로 | 공통 동작 |
| 접근성 | `logoAlt` 전달 시 홈 버튼 이름 "<logoAlt> 홈", 없으면 "홈" | 공통 동작 |
- Figma 26:8944의 이지빔 로고(`bi_egbim`)는 기준 화면의 예시이며 공통 컴포넌트에 넣지 않습니다(2026-10-16 사용자 지시로 패키지에서 제거).
- Storybook 기본 화면의 로고는 임시 자산 `src/sw/assets/sw-header-logo-placeholder.svg`(브랜드명 없는 점선 사각형 도형)입니다. Storybook 전용 파일이며 패키지에 포함되지 않고, 어떤 제품의 공식 로고도 아닙니다.
## 10. 사용자 확정 CSS
신규 패키지에서는 기존 뷰어 class 대신 SW 전용 class와 토큰을 씁니다. 값은 그대로입니다.
| 기존 뷰어 class | 신규 class | 토큰 |
|---|---|---|
| A `.cad-toolbar-object-area` | `.hm-sw-header__object-area` | `--hm-sw-header-object-background`, `-object-border-color`, `-object-radius` |
| B `.cad-toolbar-document` | `.hm-sw-header__document` | `--hm-sw-header-document-background`, `-document-border-color` |
| C `.cad-toolbar-property-field` | `.hm-sw-header__field` | `--hm-sw-header-property-background`, `-property-border-color`, `-property-radius`, `-property-shadow` |
```css
/* A. 객체 수정 */
border-radius: 6px; border: 1px solid #135643; box-shadow: none;
background: linear-gradient(0deg, #144838 0%, #1A4536 6%, #1F4134 10%, #10392D 18%, #296B55 72%, #1F5A47 83%, #144838 92%, #124133 100%);
/* B. 파일명 */
border-radius: 0; border-top/right/bottom: 1px solid #0E4C3A; border-left: 0; box-shadow: none;
background: linear-gradient(180deg, #081813 0%, #09271E 8.69%, #0B3227 25.43%, #0E3D2F 51.25%, #0B3227 80.03%, #0A2B21 89.76%, #081813 100%);
/* C. 속성 입력 */
border-radius: 3px; border: 1px solid #08251C; box-shadow: 0 1px 4px 0 rgba(9, 39, 30, 0.70);
background: linear-gradient(0deg, rgba(0,0,0,.45) 0%, rgba(0,0,0,.45) 26.41%, rgba(0,0,0,.40) 70%, rgba(0,0,0,.45) 100%);
```
세 값은 Figma `get_design_context`의 해당 레이어 값과도 일치합니다(Figma 26.407% → 확정 26.41%). C의 반투명 검정은 A 위에서 합성되는 것이 의도입니다.
## 11. 선택 · Focus · Hover · Disabled
| 상태 | 표현 | 근거 |
|---|---|---|
| 선택(`activeMenu`, `propertyBarOpen`, `propertyPanelOpen`, 더보기 열림) | 텍스트·단색 아이콘(파일명 화살표, 더보기 점)만 `#FFC600`. 배경·테두리·그림자 변화 없음 | 사용자 확정 |
| Focus(키보드) | 2px `#F0F7F5` outline, offset −2px. 선택과 별도 | 정책(선택과 구분) + 구현값 |
| Hover | 노란 gradient 테두리(§11.1) 유지. 5개 필드는 텍스트·지정 스타일 미리보기도 `#FFC600`(§11.5). 배경·크기·padding·radius·그림자 변화 없음. 인쇄·팝업 가이드와 최소화·종료는 기존 §11.2 유지 | Figma 홈 버튼 hover(26:10655) + 사용자 추가 §11.5, Figma 26:12156 / 26:12161 |
| Selected + Hover | 선택 글자색 `#FFC600` 유지 + Hover 테두리. 두 표현이 다른 속성이라 함께 보임 | 정의 |
| Focus + Hover | Focus 윤곽선(2px `#F0F7F5`)이 Hover 테두리 위에 그려짐 | 정의 |
| Disabled(`disabled`) | 메뉴·속성·인쇄/가이드 opacity .4, 커서 기본, Hover 테두리·아이콘 색 변화 없음. 창 제어는 유지 | 구현값 |
레이어 상태 아이콘 3개는 원본 다색 SVG(노랑 전구·하늘색·자물쇠)로, 선택 시 `#FFC600`으로 바꾸지 않습니다. 레이어 상태 의미를 보존하기 위한 의도적 차이입니다(§15).
### 11.1 Hover 테두리 (공통 상호작용 규칙)
**Figma에서 확인한 것**(`header/btn_home`, node 26:10655, Variant `Default` / `hover`, 80 × 48): Hover는 배경 SVG의 1px 테두리만 녹색 gradient에서 노란 gradient로 바꿉니다. 채움·그림자·크기·로고 위치는 Default와 같습니다(두 SVG를 비교해 확인). 방향은 위 → 아래(`y1 12 → y2 60`)이며 stop은 0% `#A67D03`, 16.41% `#F2CC0F`, 37.19% `#FADD4F`, 62.03% `#F2CC0F`, 87.88% `#926E03`(이후 100%까지 같은 색)입니다.
**구현 적용 범위**(Figma 홈 버튼 규칙을 다른 버튼에 확장한 구현 결정):
| 대상 | 방식 |
|---|---|
| 홈 | Figma hover SVG(`home-bg-hover.svg`)를 Default 위에 겹쳐 사선 외곽선에 정확히 표시 |
| 레이어·색상·선스타일·문자스타일·치수스타일, 더보기 버튼 | 1px 테두리 위치, radius 3px |
| 인쇄 | 1px 테두리 위치, radius 2px. 아이콘 색 변화는 §11.2 |
| 팝업 가이드 | SVG 안의 1px 외곽선 위치, radius 2px. 아이콘 색 변화는 §11.2 |
파일명(문서 Dropdown 버튼)은 예외입니다(2026-10-16 사용자 지시): Hover 시 노란 테두리 없이 **파일명 글자만 `#FFC600`**으로 바꾸고, 녹색 테두리·배경 gradient·초록 화살표는 그대로 둡니다. Dropdown이 열린 동안에는 글자 `#FFC600`과 노란 위쪽 화살표를 유지하고, 닫힌 뒤 Hover가 풀리면 Default로 돌아갑니다. 키보드 Focus 윤곽선은 별도로 유지합니다.
문서 Dropdown 안의 목록 항목과 `+ New` / `Open`도 노란 테두리를 쓰지 않고 각자의 Figma hover를 따릅니다(§5.1): 목록 항목은 검정 40% 배경, `+ New` / `Open`은 배경 20% → 30%·글자 70% → 100%. 속성Bar · 속성창도 예외입니다(2026-10-16 사용자 지시): Hover 시 **글자만 `#FFC600`**이고 노란 테두리·배경·그림자가 없습니다. 선택(`propertyBarOpen` / `propertyPanelOpen`)도 글자 `#FFC600`이며 선택된 토글에 Hover해도 테두리가 생기지 않습니다. 위의 '상세보기' 안내 글자는 바뀌지 않고, 키보드 Focus 윤곽선은 그대로입니다.
즉 **파일명·속성Bar·속성창 Hover(글자 노랑) · 목록 항목 Hover(검정 40% 배경) · 하단 버튼 Hover(배경·글자 진하게)는 서로 다른 정책**이며, 노란 gradient 테두리는 위 표의 버튼(홈, 5개 속성 필드, 더보기, 인쇄, 팝업 가이드)에만 적용합니다.
제외: 그리기모드(클릭 대상이 아닌 텍스트), 창 이동(드래그 영역), 최소화·종료(외곽선이 없는 창 제어 버튼, Figma 전용 hover를 따름 — §11.2), 더보기 메뉴 안 항목(팝업 목록, 기존 배경 Hover 유지), 비활성 버튼.
구현: 토큰 `--hm-sw-header-hover-border-gradient`, `--hm-sw-header-hover-border-width`(1px). 대상 버튼의 `::after`가 기존 테두리 위치에 gradient 링(mask)을 그리며, 레이아웃·클릭 영역은 바뀌지 않습니다. gradient는 각 버튼 높이 기준 위 → 아래로 적용됩니다. 이전 구현값(필드·토글 배경 겹침, 인쇄·가이드 `brightness`)은 Figma 기준에 맞춰 제거했습니다.
### 11.2 창 제어 · 바로가기 아이콘 Hover (2026-10-16)
**Figma에서 확인한 것**(`get_design_context` / `get_metadata`):
| 버튼 | Figma | Default | Hover(Variant2) |
|---|---|---|---|
| 최소화 | `header/btn_최소최대화` 26:12156(Default 26:12157, Variant2 26:12159) | 32 × 48, 배경 없음, 막대 10 × 2 `#ADB0AF` | 배경 `rgba(0,0,0,.2)`, 막대 `#FFFFFF`(크기·위치 같음) |
| 종료 | `header/btn_종료` 26:12161(Default 26:12162, Variant2 26:12166) | 32 × 48 SVG, X `#ADB0AF` | 같은 SVG 구조에 `#E40000` 32 × 48 사각형 + X `white`(X path 동일) |
Figma 헤더(26:8944)에서 최소화·종료 frame(`Frame 427319960`)은 창 영역 배경 `Rectangle 6669`(1px 테두리)보다 위에 있습니다. 그래서 Hover 배경은 창 영역 테두리 위까지 32 × 48 전체를 덮습니다. 구현은 `.hm-sw-header__window-buttons`를 `position: relative`로 두어 창 영역 `::before` 테두리 위에 그립니다. Default는 버튼이 투명하므로 이전과 같습니다.
**구현**:
| 버튼 | 방식 |
|---|---|
| 최소화 | `:hover` 배경 `--hm-sw-header-window-button-hover-background`(`rgba(0,0,0,.2)`), 막대 `--hm-sw-header-window-icon-hover-color`(`#FFF`) |
| 종료 | Figma hover SVG(`close-hover.svg`)를 Default SVG와 같은 칸(`grid-area: 1 / 1`)에 두고 Hover 때만 보이게 전환. Figma X 형태 그대로 |
| 인쇄 · 팝업 가이드 | 기존 노란 gradient 테두리(§11.1) 유지 + Hover 때 `*-hover.svg`로 전환. 아이콘의 `#B6D0C9` 채움만 `#FFC600`이고 검정 외곽선(mask)·그림자(filter)·팝업 가이드 녹색 gradient 타일·테두리는 그대로 |
- 전환은 `.hm-sw-header__icon` / `.hm-sw-header__icon-hover` 두 `<img>`의 `visibility`로 합니다. 같은 위치·크기라 레이아웃·클릭 영역이 바뀌지 않고, 두 이미지가 겹쳐 그려지지 않아 그림자가 두 번 칠해지지 않습니다.
- 외부 `<img>` SVG는 CSS로 특정 fill만 바꿀 수 없어 로컬 SVG Variant를 둡니다. 전체 SVG에 CSS `filter`를 쓰지 않습니다.
- 최소화·종료에는 노란 테두리·그림자·크기 변화가 없습니다. 비활성(`disabled`) 인쇄·팝업 가이드는 아이콘 색이 바뀌지 않습니다. 키보드 Focus 윤곽선은 그대로이며 Focus만으로는 Hover 표현을 하지 않습니다(§11.1과 같음).
- 종료는 `onClose`만 호출합니다. 실제 창 닫기는 호스트가 구현하며 Storybook은 창을 닫지 않습니다.
- 인쇄 · 팝업 가이드 Hover 아이콘 색은 Figma Variant가 없는 사용자 지시(2026-10-16) 값입니다.
### 11.3 창 이동 Hover Motion — Figma 26:10220 (2026-10-16)
> **GIF 전달 전 구현의 보존 기록입니다. 현재 움직임·반복·좌표는 아래 §11.4를 우선합니다.** Figma 원본의 자산·Tooltip 크기와 색상 근거는 계속 유효합니다.
기준: [창이동 motion](https://www.figma.com/design/rj6wpsfk24lOWvWiVRAANP/이지빔-SW?node-id=26-10220&m=dev). 기존 Default SVG와 `div` / `onPointerDown` 드래그 계약은 유지합니다. 아래 좌표는 88 × 48px 창 이동 영역의 왼쪽 위 기준이며 SVG stroke의 바깥 여백과 구분합니다.
- Default `26:10221`: 아이콘 32 × 30px, (28, 13).
- Hover `26:10258`: 창 24 × 18.857px, (32, 15); 커서의 회전 외곽 박스 15.106 × 15.568px, (52, 13); Tooltip (68, 13).
- Hover2 `26:10295`: 창 (59, 12); 커서 (72, 10); Tooltip (89, 11). 창·커서 크기는 Hover와 같습니다.
- Tooltip `26:215`: `창 이동`, 45 × 17px, 검정 80% 배경, 흰색 12px/400 글자, radius 2px, padding 2px 6px. 색상은 `--hm-sw-header-tooltip-background` / `--hm-sw-header-tooltip-color`입니다. 글꼴은 기존 SW의 Noto Sans KR 정책을 따릅니다(Figma는 Inter).
- 기존 `window-motion.svg`는 유지하고, Hover용 `window-motion-window.svg`(28 × 22.8574), `window-motion-cursor.svg`(13.0291 × 16.7588)를 사용합니다. 2026-10-16 원본 비교에서 Hover 자산 두 개는 일치했고 Default는 SVG 마스크 식별자만 달랐습니다.
Figma Prototype reactions를 직접 확인한 전환:
1. Default → Hover: `ON_HOVER`, `SMART_ANIMATE`, `GENTLE`, 1.0220937728881836초.
2. Hover → Hover2: `AFTER_TIMEOUT` 0.001초, `SMART_ANIMATE`, `QUICK`, 0.744255542755127초.
3. Hover2 → Default: `AFTER_TIMEOUT` 0.001초, `DISSOLVE`, `BOUNCY`, 0.9582165479660034초.
구현은 약 2.7265658초의 CSS Keyframes 4개를 사용합니다. 포인터 진입당 1회 재생하고, Hover를 유지하면 Default에서 멈춥니다. 포인터가 나가면 즉시 Default로 복귀하며 재진입 시 처음부터 시작합니다. Figma에 전환 고리는 있으나 Default로 돌아온 뒤 포인터 유지 중 자동 재시작하는지는 Prototype player에서 실재생하지 않았으므로 **NOT VERIFIED**입니다. 현재 1회 정책은 변경하지 않았습니다.
GENTLE / QUICK / BOUNCY는 기존 코드의 spring 표본을 CSS `linear()`로 근사한 값입니다. 키프레임에 timing function을 직접 선언하며 QUICK의 overshoot와 DISSOLVE의 opacity 혼합을 유지합니다. BOUNCY는 opacity 범위에 맞춘 단조 곡선입니다. Figma API는 preset 이름과 시간을 반환했지만 수치 곡선은 반환하지 않았으므로 Figma player와 중간 프레임이 완전히 같다고 보증하지 않습니다. `linear()` 미지원 브라우저의 선형 fallback은 실기기 **NOT VERIFIED**입니다.
드래그 영역은 기존 `role="presentation"` div와 `onWindowMoveStart(event)`를 유지합니다. 모든 장식 자식과 Tooltip은 `pointer-events: none`으로 포인터를 가로채지 않습니다. Tooltip은 Figma 좌표대로 영역 밖으로 나와 최소화와 종료 왼쪽 일부에 시각적으로 겹칩니다. 이를 겹침 없음으로 판정하지 않습니다. 브라우저 hit test와 버튼 Callback은 통과했으며 실제 네이티브 창 이동·최소화·종료는 호스트 검증이 필요합니다.
반응형은 기존 container query를 유지합니다. 헤더 폭 1080px 이상은 88px 영역과 전체 모션, 640–1079px는 24px 영역에서 그림을 숨기고 정적 Tooltip만 (영역 폭 + 1px, 11px)에 표시, 640px 미만은 영역 전체를 숨깁니다. 헤더 높이는 49px입니다.
`prefers-reduced-motion: reduce`에서는 4개 애니메이션을 끄고 기본 아이콘을 유지하며 Hover 동안 정적 Tooltip을 표시합니다. 24px 단계의 아이콘 숨김 규칙은 그대로입니다. 2026-10-16 검증에서 기존 `> *` 해제 규칙의 낮은 specificity 때문에 애니메이션이 유지되는 오류를 발견해, 레이어별 Hover 선택자와 동일 specificity인 `:is(...)` 선택자로 수정했습니다. 재생 도중 reduce 전환도 즉시 정지합니다.
창 이동 div 자체는 기존대로 Tab 대상이 아니며 Focus Tooltip이나 키보드 창 이동을 제공하지 않습니다. 최소화·종료의 접근성 이름, Tab 이동과 Focus 윤곽선은 유지합니다. 키보드 창 이동 대안과 실제 보조기술 검증은 호스트 과제이며 접근성 전체 PASS를 뜻하지 않습니다.
브라우저 검증·빌드·Guard 한계·기존 작업 보존 근거는 [최종 검증 기록](./sw-header-window-motion-verification-2026-10-16.md)에 별도로 남겼습니다. 아래 기존 반응형 검증 기록은 당시 결과로 보존합니다.
### 11.4 현재 창 이동 Motion — ani_hover_move.gif 우선 (2026-10-16)
> 아래는 원본 GIF의 경로·1640ms 분석 기록입니다. 후속 사용자 요청으로 현재 시간만 1400ms로 단축했으며 현재 시간과 필드 Hover 정책은 §11.5를 우선합니다. 경로·자산 근거는 계속 유효합니다.
사용자 제공 `D:\디자인시스템\ani_hover_move.gif`를 실제 움직임의 최우선 기준으로 적용했습니다. 원본은 640 × 192px, 41프레임 × 40ms = 1640ms, `loop=0`(무한 반복)입니다. SHA-256: `c7d22819cfb8f8cd8593684ce1e5c676a545fa09fa9fc7573d59f0f237d28026`. GIF의 높이 192 / Header bar 높이 48 = 4배이며, 창의 녹색 부분 폭 96 / 4 = 기존 SVG의 녹색 폭 24px도 일치합니다. 이 비율로 상대 이동량을 환산하고 GIF 자체는 제품 자산으로 넣지 않았습니다.
현재 우선순위는 **움직임·등장 순서·시간·유지·반복 = GIF**, **88 × 48 영역·녹색 줄무늬·SVG·Tooltip 규격 = SW/Figma**입니다. 과거 §11.3의 2.727초/1회/Smart Animate spring은 현재 동작이 아닙니다.
1. Hover 전: 기존 `window-motion.svg` 32 × 30px, (28, 13). 변경하지 않았습니다.
2. 0–520ms: 같은 창·커서를 분리 SVG로 표시합니다. 창은 제자리, 커서는 오른쪽 아래에서 위로 12.5px 상승하며 왼쪽 0.75px의 작은 이동이 있습니다. 창 크기를 줄이거나 커서를 위에서 fade-in하지 않습니다.
3. 520ms: `창 이동` 글자가 등장합니다. GIF 13번 프레임과 같은 시점이며 이전에는 숨깁니다.
4. 약 600–1040ms: 창·커서·문구가 오른쪽 위로 함께 이동합니다. 창은 시작점 대비 +28 / −4.5px, 커서는 최초 위치 대비 +27.25 / −17px입니다. 40ms별 표본 사이를 CSS로 보간해 GIF의 가속·감속을 유지하고 추가 spring overshoot는 넣지 않습니다.
5. 1040–1640ms: 끝 자세를 600ms 유지합니다. 1640ms에서 즉시 첫 프레임으로 돌아가 Hover 동안 반복합니다. GIF에 없는 복귀 dissolve는 추가하지 않았습니다.
6. Hover 해제: 애니메이션 제거, 기존 Default SVG로 즉시 복귀. 재진입 시 새 사이클을 시작합니다.
분리된 창은 원본 `window-motion-window.svg`의 28 × 22.8574px 크기로 (28, 13)에 표시합니다. 커서의 회전 wrapper는 15.106 × 15.568px, 시작 (44.25, 26.25)이며 SVG 원본 13.0291 × 16.7588px와 −40° 회전은 유지합니다. Tooltip 시작 box는 (60.75, 14.5), 끝 box는 (88.75, 10)입니다. wrapper/box 좌표와 색이 있는 실제 glyph 좌표는 구분합니다.
Tooltip의 45 × 17px, 검정 80% 배경, 흰색 12px/400 글자, radius 2px, padding 2px 6px는 유지합니다. Figma 선언에 맞춰 Tooltip만 `Inter`를 우선하고 기존 SW 글꼴을 fallback으로 사용합니다. 새 폰트 파일이나 의존성은 추가하지 않았으며, 한글 또는 Inter 미설치 환경에서는 기존 Noto Sans KR 등이 표시됩니다. 문구와 커서 사이의 보이는 간격은 GIF의 약 9px를 기준으로 맞췄습니다.
기존 div / `role="presentation"` / `onPointerDown={onWindowMoveStart}`와 공개 Props는 변경하지 않았습니다. 장식의 `pointer-events: none`도 유지합니다. 포인터를 누르는 `:active` 동안 3개 CSS 애니메이션을 일시정지하고 Tooltip을 숨기며, 새로운 포인터 캡처·전역 이벤트·preventDefault는 추가하지 않습니다. 포인터 캡처 및 실제 창 이동은 기존대로 호스트 책임입니다.
1080px 미만 24px 단계는 그림을 숨기고 정적 Tooltip만, 640px 미만은 영역 전체를 숨기는 기존 정책을 유지합니다. Reduced Motion에서는 3개 애니메이션을 끄고 기존 Default SVG와 정적 Tooltip을 표시합니다. 재생 중 reduce 전환도 적용됩니다. 창 이동의 기존 키보드 미지원과 실제 호스트 통합 검증 한계는 §11.3과 같습니다.
프레임별 비교·최종 빌드·반응형·Drag·보존 근거는 [GIF 기준 검증 기록](./sw-header-window-motion-gif-verification-2026-10-16.md)에 기록합니다. 이전 검증 기록은 당시 상태로 보존합니다.
### 11.5 현재 추가 기준 — 1400ms 및 그룹 필드 Hover (2026-10-16)
후속 사용자 요청으로 창 이동의 전체 주기를 **1640 → 1400ms**로 단축했습니다. 세 애니메이션의 duration만 1.4초로 바꿨고 Keyframes의 모든 좌표·백분율·보간은 동일합니다. 시간 비율은 1400/1640(35/41), 주기 단축은 약 14.63%이며 재생 속도 기준으로는 약 17.14% 빠릅니다. 원본 GIF와 다른 사용자 승인 시간 변경입니다.
- 문구 등장: 520 → 약 443.90ms.
- 이동 완료: 1040 → 약 887.80ms.
- 끝 자세 유지: 600 → 약 512.20ms.
- 반복: 1640 → 1400ms. 오른쪽 28px / 위쪽 4.5px, 커서 상승과 상대 경로는 그대로입니다.
- 기존 SVG·줄무늬·Tooltip 디자인·Hover 해제·Drag 중 일시정지·Reduced Motion·88px/24px/숨김·49px 높이를 유지합니다. 패키지는 호스트의 Pointer Capture를 방해하거나 별도 캡처를 추가하지 않습니다.
그룹 필드(`SWHeaderLayerControl`, `SWHeaderColorControl`, `SWHeaderLineStyle`, `SWHeaderTextStyle`, `SWHeaderDimensionStyle`)는 **각 버튼 전체를 Hover**하면 내부 라벨·값을 `--hm-sw-header-selected-color`(기본 `#FFC600`)로 표시합니다. 선스타일의 가중치·스케일과 문자스타일의 폰트 이름 미리보기도 포함합니다. 공용 컴포넌트의 실제 동작이므로 Full Header와 Composed Controls에 같은 규칙을 적용하며 Story 전용 색 흉내는 사용하지 않습니다.
기존 노란 Gradient Hover Border는 유지합니다. 선스타일의 5개 미리보기 조각은 흰색 채움만 노란색으로 바꾸며 검정 1px border·형태·간격은 그대로입니다. 문자 미리보기는 SVG가 아닌 기존 폰트 이름 텍스트이며 색만 바꿉니다. 원래 별도 검정 Stroke/그림자가 없으므로 새 외곽선을 추가하지 않습니다.
치수 미리보기는 기존 `menu-dimension-ico.svg`를 보존하고 `menu-dimension-ico-hover.svg`를 추가합니다. 실제 foreground path의 `fill="white"` 1곳만 `#FFC600`으로 바꿉니다. mask용 흰색 rect, 검정 외곽선 path, mask 참조·좌표·60 × 15px 크기는 유지합니다. 두 이미지는 같은 60 × 15px wrapper 안에서 visibility만 전환하며 CSS filter는 사용하지 않습니다.
색상 견본의 실제 `swatch`, 흰색/검정 chip border, 레이어 상태 SVG·막대 색은 바꾸지 않습니다. Hover 이탈 시 원래 색으로 돌아가고 Selected의 기존 텍스트 색은 유지합니다. Disabled는 새 Hover 텍스트·미리보기·Border 효과를 적용하지 않으며 Disabled+Selected도 기존 색을 유지합니다. 미리보기의 선택 단독 상태를 새로 확장하지 않습니다.
**상세보기는 제외**합니다. `속성Bar` / `속성창`은 기존처럼 해당 버튼의 텍스트만 노란색이고, 상세보기 caption·검정 divider·배경·그림자는 유지하며 노란 Hover Border를 추가하지 않습니다. 홈·문서 Dropdown·인쇄/가이드·최소화/종료는 이번 추가 기준으로 변경하지 않습니다.
Composed Controls의 기존 레이어·색상·상세보기 예제는 보존하고, 기존 공개 선/문자/치수 컴포넌트 예제와 5개 필드의 고정 Hover 비교를 추가했습니다. 검증과 파일별 근거는 [속도·Composed Hover 검증 기록](./sw-header-speed-composed-hover-verification-2026-10-16.md)에 남깁니다.
## 12. 반응형
2026-10-16 사용자 지시로 기존 1180 / 900 / 560 기준 대신 측정값으로 단계를 다시 정했습니다. 모든 단계를 헤더 폭 기준 container query(`container: hm-sw-header / inline-size`)로 처리하며 헤더 가로 스크롤은 만들지 않습니다.
원칙: 5개 필드는 어느 폭에서도 Figma 폭(234 / 192 / 284 / 164 / 164)을 유지합니다. Noto Sans KR로 측정한 필드 내용 폭이 231 / 168 / 278 / 142 / 163px로 Figma 폭과 거의 같아, 필드를 줄이면 값만 잘리기 때문입니다. 공간이 부족하면 기능을 더보기로 옮기고, 문서명만 말줄임합니다.
필요 폭 = 고정부(홈 63 + 간격 6 + 창 영역) + 객체 수정 면 + 문서명.
| 헤더 폭 | 동작 | 객체 수정 면 / 창 영역 | 문서명 폭 |
|---|---|---|---|
| ≥ 1800 | Figma 기본 레이아웃(1920과 동일 구성) | 1301 / 240 | 1920: 310 → 1800: 190 |
| < 1800 | 선·문자·치수 스타일 → 더보기 | 719 / 240 | 120–310 |
| < 1180 | 그리기모드 라벨 숨김 | 633 / 240 | 120–310 |
| < 1080 | 속성Bar·속성창 → 더보기, 창 이동 88 → 24px | 480 / 176 | 120–310 |
| < 900 | 색상 → 더보기 | 284 / 176 | 96–310 |
| < 640 | 인쇄·팝업 가이드 → 더보기, 창 이동 숨김 | 284 / 64 | 96–310 |
| < 520 | 레이어 상태 아이콘 숨김, 레이어 필드만 줄어들며 레이어명 말줄임 | — | 96 |
- 1800 기준: 5개 필드를 모두 표시할 때 문서명이 190px 이상 남는 최소 폭입니다(1610 + 190). 1770은 문서명 160px로 표시는 가능하지만 사용자 제안(약 1800)과 문서명 가독성을 고려해 1800을 채택했습니다.
- 1180 / 1080 / 900 / 640은 각 단계에서 문서명이 최소 폭(120 또는 96px)에 닿기 직전의 폭입니다. 기존 1180 기준은 "그리기모드 라벨 숨김"으로, 900 기준은 "색상 이동"으로 역할을 바꿨습니다. 기존 560 화면 폭 규칙은 헤더 폭 640 규칙으로 대체했습니다(헤더 폭 ≤ 화면 폭이므로 화면 560 미만에서도 항상 적용됨). 원 규칙의 "파일 열기·전체 보기 글자 숨김"은 Figma 헤더에 해당 도구가 없어 적용 대상이 없습니다.
- 문서명 폭은 `clamp(최소, 100cqw − 나머지 고정폭, 310px)`입니다. 문서명은 말줄임, 글자 크기는 줄이지 않습니다.
- 더보기 메뉴는 옮겨진 기능만 표시하며, 선택(`#FFC600`)·체크 상태와 Props·Callback 동작은 헤더에 있을 때와 같습니다.
검증(2026-10-16, Storybook `SW / Header / Responsive`, 18개 폭): 1920 / 1800 / 1799 / 1770 / 1600 / 1440 / 1280 / 1180 / 1179 / 1080 / 1079 / 900 / 899 / 768 / 640 / 639 / 560 / 390에서 헤더 가로 스크롤 0, 요소 겹침 0, 객체 수정 면 밖으로 넘침 0. 필드 값·라벨 잘림은 390px의 레이어명 1건뿐입니다. 각 폭에서 더보기 항목 선택 시 Callback이 호출되고, 선택 상태가 반영되며, 메뉴가 닫히는 것을 확인했습니다(속성Bar·속성창 체크 전환, 인쇄·팝업 가이드 Callback 포함). 실제 Viewport 1799 / 1079 / 390에서도 같은 결과입니다.
## 13. 컴포넌트 분리 기준
- 공개: `SWHeader`(전체), `SWHeaderHome`, `SWHeaderDocumentTitle`, `SWHeaderLayerControl`, `SWHeaderLayerMenu`(레이어 필드 + Dropdown, §5.2), `SWHeaderColorMenu` · `SWHeaderLineStyleMenu` · `SWHeaderTextStyleMenu` · `SWHeaderDimensionStyleMenu`(§5.3), `SWHeaderLinePreview` · `swHeaderLinePatterns`, `SWHeaderInput` · `parseSWHeaderColor` · `parseSWHeaderNumber`(§5.5), `SWHeaderColorControl`, `SWHeaderLineStyle`, `SWHeaderTextStyle`, `SWHeaderDimensionStyle`, `SWHeaderProperties`, `SWHeaderActions`, `SWHeaderWindowControls`, `SWTheme`(단독 사용 시 토큰 범위).
- 내부: `SWHeaderField`/`SWHeaderFieldText`(5개 메뉴 공통 면·라벨/값), `SWHeaderMore`(더보기), `SWHeaderDocumentItem`(문서 Dropdown 행: 선택 버튼 + 문서 닫기 버튼). 문서 Dropdown은 `SWHeaderDocumentTitle`이 소유하며 `documents`·Callback으로 다른 SW에서도 그대로 재사용합니다. 같은 필드 스타일을 메뉴별로 중복 구현하지 않습니다.
- 데이터는 Props, 사용자 동작은 Callback입니다. 파일 열기·창 이동·최소화·종료·도면 처리는 패키지 밖(호스트)에서 구현합니다.
## 14. SW 전용 토큰 목록
`.hm-sw-header`와 `.hm-sw-theme`(= `<SWTheme>`)에 정의합니다. `@hm-design/ui` 토큰을 참조·변경하지 않습니다.
| 토큰 | 기본값 | 사용처 |
|---|---|---|
| `--hm-sw-header-height` / `-bar-height` / `-field-height` | 49px / 48px / 38px | 헤더 / 면 / 필드 |
| `--hm-sw-header-logo-width` / `-logo-height` | 32px / 17px | 로고 영역(공통 배치, 로고 이미지는 프로그램별) |
| `--hm-sw-header-gap` | 4px | 필드 간격 |
| `--hm-sw-header-font-family` / `-font-size` / `-label-size` | Noto Sans KR… / 14px / 12px | 전체 / 값 / 라벨 |
| `--hm-sw-header-background` | `#060606` | 헤더 바탕 |
| `--hm-sw-header-text-color` / `-title-color` | `#F6F4F2` / `#F0F7F5` | 일반 / 파일명 |
| `--hm-sw-header-selected-color` | `#FFC600` | 선택 |
| `--hm-sw-header-focus-color` | `#F0F7F5` | Focus |
| `--hm-sw-header-arrow-color` | `#47AF91` | 파일명 화살표 |
| `--hm-sw-header-hover` | `rgba(255,255,255,.08)` | 더보기 메뉴 항목 Hover 배경 |
| `--hm-sw-header-hover-border-gradient` / `-hover-border-width` | Figma 홈 hover gradient / 1px | Hover 테두리(§11.1) |
| `--hm-sw-header-document-menu-width` / `-document-menu-background` / `-document-menu-border-color` / `-document-menu-border-width` | 310px / `#103127` / `#041611` / 1px | 문서 Dropdown(§5.1) |
| `--hm-sw-header-document-menu-shadow` / `-document-menu-inset-shadow` / `-document-menu-divider-color` | `0 2px 4px rgba(0,0,0,.25)` / `inset 0 4px 8px rgba(0,0,0,.4)` / `rgba(194,217,210,.2)` | 문서 Dropdown(§5.1) |
| `--hm-sw-header-document-list-max-height` | 320px | 목록 내부 스크롤 시작 높이(구현값) |
| `--hm-sw-header-document-action-background` / `-document-action-color` | `rgba(182,208,201,.2)` / `rgba(0,0,0,.7)` | New / Open Default(§5.1) |
| `--hm-sw-header-document-action-hover-background` / `-document-action-hover-color` | `rgba(182,208,201,.3)` / `#000` | New / Open Hover(Figma 26:10762 / 26:10805) |
| `--hm-sw-header-document-item-hover-background` | `rgba(0,0,0,.4)` | 문서 목록 항목 Hover(Figma 26:10680) |
| `--hm-sw-header-document-close-color` / `-document-close-hover-background` / `-document-close-hover-color` | `rgba(240,247,245,.7)` / `rgba(182,208,201,.2)` / `#F0F7F5` | 문서 닫기 X(사용자 추가, 구현값, §5.1.1) |
| `--hm-sw-header-layer-menu-width` / `-layer-menu-background` / `-layer-menu-border-color` / `-layer-menu-radius` / `-layer-menu-shadow` | 234px / `#27241D` / `#000` / 3px / `0 4px 8px rgba(16,48,38,.25)` | 레이어 Dropdown(§5.2) |
| `--hm-sw-header-layer-expanded-border-color` | `#081813` | 펼친 레이어 필드 테두리(Variant3) |
| `--hm-sw-header-layer-list-max-height` | 240px | 레이어 목록 내부 스크롤 시작 높이(구현값, 8행) |
| `--hm-sw-header-layer-row-hover-background` / `-layer-row-hover-color` | `#62533D` / `#FF5C00` | 레이어 행 Hover(26:10695) |
| `--hm-sw-header-layer-icon-hover-background` | `rgba(0,0,0,.25)` | 전구 Hover(눈꽃·자물쇠는 Hover SVG) |
| `--hm-sw-header-layer-chip-border-color` | `#C9C9C9` | 레이어 색상 견본 테두리 |
| `--hm-sw-header-layer-footer-border-color` / `-layer-detail-background` / `-layer-detail-color` / `-layer-detail-text-opacity` | `rgba(197,177,158,.15)` / `rgba(197,177,158,.15)` / `#FFF` / .35 | 레이어 상세 영역·버튼 |
| `--hm-sw-header-document-background` / `-document-border-color` | §10 B / `#0E4C3A` | 파일명, 홈 뒤, 창 영역, 인쇄 버튼 |
| `--hm-sw-header-object-background` / `-object-border-color` / `-object-radius` | §10 A / `#135643` / 6px | 객체 수정 면, 더보기 메뉴 테두리 |
| `--hm-sw-header-property-background` / `-property-border-color` / `-property-radius` / `-property-shadow` | §10 C / `#08251C` / 3px / §10 C | 5개 필드, 더보기 버튼 |
| `--hm-sw-header-pill-background` / `-pill-border-color` | `rgba(0,0,0,.8)` / `#092920` | 인쇄·가이드 pill |
| `--hm-sw-header-stripe-color` | `rgba(28,106,81,.32)` | 창 이동 줄무늬 |
| `--hm-sw-header-window-icon-color` | `#ADB0AF` | 최소화 막대 |
| `--hm-sw-header-window-button-hover-background` / `-window-icon-hover-color` | `rgba(0,0,0,.2)` / `#FFF` | 최소화 Hover(Figma 26:12159). 종료 Hover는 Figma SVG(`close-hover.svg`) |
| `--hm-sw-header-text-outline-color` / `-text-outline` / `-label-outline` | `#000` / 1.3px 16방향 / 1px 16방향 | Text Stroke 검정 외곽선(§6.1) |
| `--hm-sw-header-text-stroke-width` / `-solid-stroke-width` / `-text-outline-room` | 0.6px / 0.8px / 2px | 자기 색 Stroke(바깥 0.3 / 0.4px), 말줄임 상자 외곽선 여유 |
| `--hm-sw-header-menu-divider-color` / `-menu-divider-strong-color` | `rgba(197,177,158,.15)` / `.25` | 새 Dropdown 열 구분선·하단 테두리(§5.3) |
| `--hm-sw-header-menu-list-max-height` / `-color-palette-max-height` | 240px / 118px | 긴 목록·사용자 색상 내부 스크롤 시작(구현값) |
| `--hm-sw-header-color-mode-color` / `-color-mode-background` / `-color-mode-on-background` | `#D4CBBD` / `rgba(98,83,61,.3)` / `#62533D` | ByLayer / ByBlock |
| `--hm-sw-header-color-pick-background` / `-color-chip-border-color` / `-color-input-border-color` | `#C5B19E → #5F554C` / `#C9C9C9` / `rgba(197,177,158,.2)` | 색상 추출, 22px 칩, 색상 입력 Hover·구분선 |
| `--hm-sw-header-input-color` / `-input-hover-border-color` / `-input-editing-background` / `-input-editing-border-color` | `#E60403` / `rgba(212,203,189,.25)` / `#0F0D0A` / `rgba(212,203,189,.3)` | `SWHeaderInput`(§5.5), caret 색 포함 |
## 15. Figma 비교 결과와 의도적 차이
1920 × 49, Storybook `SW / Header / Default`에서 `getBoundingClientRect`로 측정했습니다.
| 항목 | 판정 | 내용 |
|---|---|---|
| 전체 크기 49 / 면 48 | MATCH | |
| 면 좌표·폭(홈 0/80, 파일명 65/310, 객체 377/1301, 창 1680/240) | MATCH | |
| 필드 좌표·폭(467/234, 705/192, 901/284, 1189/164, 1357/164, 상세보기 1521/157 y6) | MATCH | |
| 인쇄·가이드 pill(1683/85, y3), 창 이동(1768/88), 최소화·종료(1856, 1888 / 32) | MATCH | |
| 사용자 확정 3개 면(gradient·border·radius·shadow) | MATCH | computed style 확인 |
| Assets 12개 위치·크기, 임시 URL 없음 | MATCH | |
| 문서 Dropdown(위치 x 63–375 / y 48, 312 × 170, 항목 32, 하단 40, 색·테두리·radius·그림자, 열린 상태 노란 글자·위 화살표) | MATCH | 2026-10-16 Storybook `Document Dropdown`에서 측정. §5.1 |
| 문서 Dropdown 열기 방식(Figma `hover` Variant → 클릭) | APPROVED DIFFERENCE | 사용자 지시 |
| 레이어 Dropdown(필드 234 × 38, 목록 234 × 280, 행 30, 아이콘 20, 견본 16, `#27241D`·`#62533D`·`#FF5C00`, 아이콘 Hover, 레이어 상세, 펼침 외곽선) | MATCH | 2026-10-16 Storybook `Layer Dropdown`에서 측정. §5.2 |
| 레이어 Dropdown 열기 방식(Figma hover → Variant3 → 클릭) | MATCH | 사용자 지시와 Figma 흐름 동일: Hover는 테두리만, 클릭 시 펼침 |
| 레이어 상세 버튼 폭(Figma 글자 75 → 69.3, 버튼 107.3) | APPROVED DIFFERENCE | 글꼴 차이(Dotum → Noto Sans KR) |
| 눈꽃·자물쇠 컬러 = 동결 해제·잠금 해제 | APPROVED(사용자 확정 2026-10-16) | Figma에는 의미 정의 없음, 사용자 지시로 확정(§5.2.1). 실제 호스트 반영은 NOT VERIFIED |
| 현재 레이어 이름 `#FFC600`, 목록 높이 상한 240px, 좁은 폭 왼쪽 이동 | 구현값(Figma 없음) | 문서 Dropdown 규칙 확장 / 8행 기준 / 화면 밖 넘침 방지 |
| New / Open 버튼 폭 54 / 47 → 55.4 / 50 | APPROVED DIFFERENCE | 글꼴 차이(Dotum → Noto Sans KR) |
| 로고 이미지 | APPROVED DIFFERENCE | Figma 이지빔 로고 대신 프로그램별 로고(`logoSrc`/`logo`). 로고 영역 위치·크기는 MATCH. §9.1 |
| 글꼴 Dotum → Noto Sans KR, Bold 표기 → 700 | APPROVED DIFFERENCE | 글자 폭이 넓어 값 텍스트 위치가 수 px 다름 |
| 글자 겹침 레이어(검정+밝은색) → 단일 밝은색 | ~~APPROVED DIFFERENCE~~ → 2026-10-16 복원 | §6.1 Text Stroke. 남은 차이: Noto 700 + Stroke가 Dotum + Stroke보다 약간 굵음 |
| 색상·선스타일·문자·치수 Dropdown 구조·크기·색 | MATCH (2026-10-16 측정) | §5.3. 차이: 선 종류 열 182 → 187.2(Noto 글자 폭), 문자스타일 글꼴 칸 38 → 내용 폭, 상세 pill 글자 폭 |
| Dropdown 독립 버튼 Hover | APPROVED DIFFERENCE | Figma Hover Variant 있음, 사용자 정책으로 Hover 효과 없음(§5.3) |
| 레이어 상세 버튼 Hover(.25 / radius 25) | APPROVED DIFFERENCE | 같은 정책(§5.5) |
| Input Default / Hover / 편집 / 커서 | MATCH (58 × 20 / 58 × 24 / 58 × 24, caret `#E60403`) | caret 높이 14px은 NOT VERIFIED(네이티브) |
| 선택 시 레이어 상태 아이콘 색 유지 | APPROVED DIFFERENCE 후보 | 다색 상태 아이콘 의미 보존. 확정 필요 |
| 파일명 화살표 SVG → CSS 삼각형 | APPROVED DIFFERENCE 후보 | 선택 색 전환용, 크기·색 동일 |
| 홈 버튼 Hover | MATCH | Figma hover SVG 그대로 사용(26:10655) |
| 최소화 Hover(배경 검정 20%, 막대 흰색, 32 × 48) | MATCH | Figma 26:12159 값과 computed style 비교(§11.2) |
| 종료 Hover(`#E40000` 32 × 48, 흰 X) | MATCH | Figma 26:12166 SVG 그대로 사용(§11.2) |
| 인쇄·팝업 가이드 Hover 아이콘 `#B6D0C9` → `#FFC600` | APPROVED DIFFERENCE(확장) | Figma Variant 없음. 2026-10-16 사용자 지시, 해당 fill만 바꾼 로컬 SVG(§11.2) |
| 다른 버튼 Hover 테두리 | APPROVED DIFFERENCE(확장) | Figma에는 홈 버튼 hover만 있음. 같은 gradient를 각 버튼 형태·radius에 맞춰 적용(2026-10-16 사용자 지시). §11.1 |
| Focus·Disabled 표현 | NOT VERIFIED | Figma에 Variant 없음, 구현값 |
| 반응형 단계(1800 / 1180 / 1080 / 900 / 640 / 520) | MATCH(정책) | 2026-10-16 사용자 지시에 따라 측정값으로 재설정, 필드 값 잘림 없음(390 레이어명 제외). §12 |
| 픽셀 단위 이미지 diff | NOT VERIFIED | Figma 스크린샷은 1024px 축소본만 확보, 육안·좌표 비교만 수행 |
## 16. AI 재현 규칙
SW Header는 Figma Node 26:8944를 기준으로 구현한다. 사용자가 확정한 CSS와 반응형 정책을 우선하며, SW 전용 디자인 토큰을 사용한다. HMDesign 공통 UI의 전역 스타일을 변경하지 않는다. 동일한 디자인을 재현할 때 전체 헤더를 이미지로 대체하지 않고 재사용 가능한 컴포넌트로 구성한다.
추가 규칙:
1. 새 화면에서는 `@hm-design/sw`의 `SWHeader`를 그대로 쓰고 값·Callback만 연결한다. 헤더 DOM이나 class를 복사하지 않는다.
2. 하위 컨트롤만 쓸 때는 `<SWTheme>` 안에 공개 하위 컴포넌트를 둔다. 소비 코드에서 `hm-sw-*` class를 직접 붙이지 않는다.
3. 색·gradient는 `--hm-sw-header-*` 토큰으로만 바꾼다. `@hm-design/ui` 토큰과 섞거나 같은 HEX라는 이유로 합치지 않는다.
4. Font Weight는 400 / 700만 쓴다(500은 허용되나 현재 미사용). 글자 외곽선은 Figma에 Stroke가 있는 텍스트에만 §6.1 토큰으로 적용한다(2026-10-16, 이전 "외곽선 없음" 규칙 대체). `font-weight`로 외곽선을 흉내 내지 않는다.
10. Dropdown 안 독립 버튼은 Hover 효과를 넣지 않는다. 목록 행은 Figma 행 Hover, Input은 Figma Hover를 따른다(§5.3, §5.5). Dropdown 항목은 패키지에 고정하지 않고 `id` 기반 배열 Props로 받는다.
5. 선택은 글자·단색 아이콘 `#FFC600`만 바꾸고 배경·테두리·그림자를 추가하지 않는다. Focus 표시를 지우지 않는다.
6. 기능이 화면에서 빠질 때는 반드시 더보기로 옮긴다. 필드를 줄여 값을 자르지 말고 기능을 옮긴다. 헤더 가로 스크롤을 만들지 않는다.
7. Figma Assets는 `packages/sw/src/assets/`의 원본 SVG를 쓴다. 임시 Figma URL·스크린샷 이미지를 쓰지 않는다.
8. 로고는 확정 디자인이 아니다. 공통 컴포넌트에 특정 SW 로고·브랜드명·파일 경로를 넣지 않고, 각 프로그램이 `logoSrc` 또는 `logo`와 `logoAlt`로 전달한다. 로고 영역의 위치·크기는 바꾸지 않는다.
9. Hover 테두리는 §11.1의 노란 gradient 1px을 사용한다. 배경·크기·그림자를 바꾸지 않고, 새 버튼도 같은 토큰과 대상 목록 규칙을 따른다. 텍스트·아이콘 예외는 §11.1·§11.2·§11.5를 따른다(5개 필드의 텍스트와 지정 미리보기는 §11.5, 최소화·종료는 Figma 전용 hover, 인쇄·팝업 가이드는 기존 테두리 + 지정 fill 변경). SVG는 원본의 해당 foreground fill만 바꾼 로컬 Variant를 사용하며 CSS filter를 쓰지 않는다.
## 17. 향후 공통화 검토 참고
이번 단계에서는 통합하지 않습니다. 비교 시 참고합니다.
| 구분 | 항목 |
|---|---|
| SW 고유(분리 유지) | 3개 확정 gradient 면, 홈·창 이동·pill 구성, 선택 `#FFC600`(배경 없음), 49px 헤더 구조 |
| 유사하지만 의미 다름 | 선택 노랑 `#FFC600`(HM 팝업 제목 노랑과 같은 값이나 역할 다름), 녹색 계열(HM `green-*` palette와 값 비교 필요) |
| 공통화 후보 | Noto Sans KR·400/500/700 정책, 더보기 메뉴(HM `DropdownMenu`와 기능 동일 — 스타일은 SW 전용), Focus 정책, 말줄임·no-scroll 반응형 원칙 |
| 비교 필요 | 필드 38px vs HM control height, radius 3/6 vs HM radius scale, Hover 색 |
## 추가 수정 기준 — 속성창·K 가이드 (2026-10-16)
사용자의 SW Header 추가 요청 A–H가 같은 날짜의 이전 규칙보다 우선합니다. 이 절은 과거 PASS 기록을 재판정하거나 덮어쓰지 않습니다.
- 상세 5개는 높이 24px, 고정 폭 레이어 113 / 색상 99 / 선 128 / 문자·치수 142px입니다. Default 배경 `rgba(197,177,158,.15)`·radius 24 → Hover .25·radius 25, 아이콘 14·gap 4·가로 padding 10을 유지합니다. 레이어·색상·선·문자는 원본 Hover 아이콘으로 바뀌고 글자는 35%입니다.
- 치수 상세의 실제 Hover `26:10799`는 기본 아이콘·글자 100%입니다. `26:10802` Variant3(Hover 아이콘·글자 35%)와 혼합하지 않습니다. Hover는 Callback을 호출하지 않고 기존 클릭·disabled·focus-visible을 유지합니다.
- ByLayer/ByBlock은 Hover 글자 `#FF5C00`, 선택 배경 `#62533D`·기존 Inset Shadow를 유지합니다. 항상 하나만 표시되고 같은 모드 재클릭은 Callback을 호출하지 않습니다. 처음 모드를 지정하지 않으면 ByLayer입니다.
- `colorMode=null`의 기존 직접 색상 API 의미는 유지합니다. 필수 모드 **표시**는 마지막 유효한 모드를 기억하며 팔레트 선택·직접 입력으로 지워지지 않습니다. 모드 표시를 색상 데이터 변경으로 해석하지 않습니다.
- 레이어·선·문자·치수 목록의 Hover/Selected는 배경 `#62533D`·글자/선 `#FF5C00`로 동일합니다. 치수는 기존 Hover SVG를 씁니다. 마우스가 다른 행으로 이동해도 선택 행 강조는 남고 Props의 id 변경으로 이동합니다. 문서 목록과 상단 필드의 기존 노랑 정책은 유지합니다.
- 지정된 일반 `-webkit-text-stroke-width: var(--hm-sw-header-text-stroke-width)` 선언과 미사용 변수는 제거했습니다. 검정 `text-shadow`, 상세 pill의 별도 `--hm-sw-header-solid-stroke-width`, SVG/Border/선 Stroke는 보존했습니다.
- 속성창 `26:11822`는 `SWHeaderPropertyPanel` 하나가 외부 선택·항목·그룹·펼침 데이터로 렌더링합니다. 선택 없음/전체/단일 원/공통 항목 확장의 Story fixture는 314×256 / 314×361 / 314×658 / 314×754px입니다. CAD 값 계산과 객체 변경은 하지 않습니다.
- 너비 314px, 내용에 따른 높이, viewport 경계 보정, 내부 세로 스크롤, non-modal dialog(`aria-modal=false`)입니다. Dim은 없습니다. 기존 속성창 버튼 또는 More → 속성창으로 열고 재클릭·X·Escape로 닫습니다. 닫으면 보이는 원래 버튼/More로 Focus를 돌립니다.
- 항목은 id·이름·값·type·groupId·order·readOnly·disabled·validate·unit·Callback을 갖습니다. Text/Number/Select/Color/Slider/Computed를 지원합니다. `SWHeaderInput`을 재사용하고 Enter/Blur 확정·변경 없는 중복 보고 방지·Escape 취소·외부 값 반영을 적용합니다. 선택 id 교체/항목 삭제는 편집 임시값을 폐기합니다. 그룹 펼침은 값 변경과 분리됩니다.
- 공통 입력 외 Circle 레이아웃은 원본 208px SVG와 호스트 수치/graphicSlot을 결합하고, Position 레이아웃은 동적 열을 사용합니다. 원형 항목의 추가 일반 속성도 도형 아래에 배치됩니다. 모든 fixture 숫자·객체 종류/개수는 Storybook 데이터입니다.
- 내부 아이콘 29×27.13px은 원본 Default/Hover/Active SVG를 사용합니다(원본 SVG export root는 29×28로 반올림되며 root를 재조정하지 않습니다). 금색 Active 테두리와 다시 클릭하면 Default로 돌아가는 상태를 보존합니다. 현재 저장소에 CAD 액션 계약이 없어 `toolActive/onToolActiveChange/toolLabel`을 통해 호스트에만 알립니다.
- 닫기 버튼 44×30px은 속성창 전용 원본 SVG입니다. Hover는 빨강/흰 X이며 Header 창 종료와 별개입니다.
- K 버튼은 기존 40×38px 버튼 하나를 유지합니다. 하단 가이드는 원본 이미지/마스크·208×149px·금색 경계·짙은 녹색·문구를 유지합니다. 표시 Fade는 511ms입니다.
- K 원본 이미지 Pan/Zoom은 400ms 지연 → 1022ms 전환 → 100ms 지연 → 1022ms 전환 → 150ms 지연 → 511ms 전환입니다. 마지막 화면은 400ms 유지 후 511ms로 처음에 복귀합니다(반복을 위한 구현값). 재진입은 처음부터 재생하고 영역을 떠나면 타이머를 정리합니다. Reduced Motion은 첫 프레임 정지 이미지입니다.
- 버튼과 가이드는 한 Hover/Focus 영역입니다. 기존 640px Breakpoint 아래에서는 K 버튼을 추가 생성하지 않고 기존 More의 팝업 가이드 항목 Hover/Focus에 같은 가이드를 연결합니다. 기존 창 이동 1400ms CSS와 자산은 변경하지 않습니다.
- 이 패키지는 HM UI와 독립된 SW control library이므로 panel의 native label/select/range는 SW 전용 스타일·실제 control id로 연결합니다. HM UI의 Label이나 Select를 새 의존성으로 추가하지 않습니다. Story의 외부 데이터 조작 버튼은 테스트 호스트입니다.
검증·차이·NOT VERIFIED는 [추가 검증 기록](./sw-header-additional-verification-2026-10-16.md)을 확인합니다.
## 후속 5개 요구사항 기준 (2026-10-16)
- **속성Bar 26:10492:** 최대 1222×28px, #271F13, 좌우·하단 1px #08251C, 하단 radius 6px, inset 0 4px 4px black 60%. 객체명 90px·#FFC600, 필드간 16px·라벨/값 간 6px, 라벨 #F6F4F2·입력 #E60403. 기존 Noto Sans KR 400/700 정책과 SWHeaderInput을 재사용하므로 Dotum 원본과 획·폭은 다릅니다. 기존 Header 49px 아래의 별도 strip이며 객체 수정 영역에 정렬하고 좁으면 내부 가로 스크롤합니다. 실제 CAD 계산은 없습니다.
- **동적 데이터:** 속성창과 동일 SWHeaderProperty/Selection 레코드, onPropertyChange를 공유할 수 있습니다. id/order/name/value/validation/readOnly/disabled는 외부 데이터이며 Enter/Blur·Escape·외부 변경·삭제 draft 폐기를 적용합니다. Bar와 Panel 열림은 독립적입니다.
- **컨트롤 구성:** 선택적 controls allowlist로 파일·레이어·색상·선/문자/치수스타일·속성Bar·속성창·상세보기 caption·그리기모드·인쇄·K를 개별 제거/숨김/재추가/재정렬/허용 라벨 변경합니다. 미전달 시 기존 기본 디자인을 유지하며, 구성 모드에서만 개별 순서대로 한 영역에 배치합니다. 홈·창 제어는 공통 프레임으로 유지합니다. 기존 Breakpoint와 More 대표 정책은 남은 항목에 적용합니다.
- **Text Stroke:** Text Stroke Story의 지정 header 원본 스크린샷 블록과 전용 import만 제거했습니다. 원본 PNG·다른 비교 Story·타이포그래피·색상·검정 text-shadow는 보존합니다.
- **반복 선언 원인:** src/theme/sw.css 원본 1394행(작업 전)에 별도 solid-stroke-width 참조가 실제 존재했습니다. build.mjs는 src 진입점의 CSS를 dist/styles.css로 재빌드하고 Storybook은 공개 dist CSS를 불러옵니다. 별도 생성기가 해당 선언을 추가하는 증거는 없으며 build가 src를 역생성하지도 않습니다. 원본 선언과 유일한 변수 정의를 제거했습니다. 과거 `text-stroke-width` 제거와 다른 수정입니다. 과거 사용자가 편집한 파일 위치·캐시는 직접 재현할 수 없으므로 특정 사용자 행위를 원인으로 단정하지 않습니다.
- **btn_colorize 26:11301:** 색상 메뉴의 기존 색상 추출 버튼에 Default 26:11302 / Hover 26:11305 SVG를 로컬 자산으로 저장해 연결했습니다. 24×24 버튼, 18×18 아이콘, radius 3, gradient #C5B19E→#5F554C, 기본 border black40%, drop shadow 0 2px 2px black40%. Hover border #DA6813·black25% overlay·Hover 전용 SVG, Mouse Leave 복원. 키보드 focus-visible와 접근성 이름을 제공합니다. 클릭은 기존 onColorPick이며 색상 값/모드를 바꾸지 않습니다.
- **Overlay:** 26:11338은 현재 구현에 없고 호스트 API 연동 대상입니다. 임의 색상창을 생성하지 않았습니다.
- **재사용 근거:** 신규 업무 페이지가 아닌 독립 SW component package 수정입니다. SWHeaderInput/Property 타입/기존 메뉴·More를 재사용했습니다. HM UI 의존성·FilterBar·일반 Navigation은 이 구성요소의 역할에 맞지 않아 추가하지 않았습니다. native label은 실제 input id를 연결하며 기존 패키지 독립성을 유지합니다.
새 검증과 과거 기록은 [후속 검증 기록](./sw-header-followup-five-verification-2026-10-16.md)에서 구분합니다.
## 3차 기준 — 구성요소 표시·메뉴 순서·속성창 상태·형상 Slot (2026-10-16)
- **로고 필수:** 로고 슬롯(32×17, 홈 배경 위 12,16)은 모든 구성에서 표시합니다. `home` 제거 시 같은 배경·위치의 정적 로고(Hover 없음)입니다.
- **개별 제거:** `hiddenControls`로 홈·파일·그리기모드·5개 메뉴·상세보기 caption·속성Bar·속성창·인쇄·K·창 이동·최소화·종료를 각각 제거합니다. 남은 요소는 기본 크기를 유지하고 제거한 공간은 남기지 않습니다(구현값: 상세보기 블록 일부 제거 시 폭 auto·최소 64px, 인쇄/K pill과 창 버튼은 남은 버튼 폭, 창 영역 폭 auto). 아무것도 제거하지 않으면 기존 1920px 디자인과 동일합니다.
- **창 이동 Tooltip(구현값):** 최소화·종료 중 하나 이상을 제거하면 창 영역 오른쪽 밖은 잘리고(Header 밖 노출 방지), 1080px 미만 24px 이동 영역의 Tooltip은 이동 영역 왼쪽에 표시합니다. 기본 구성의 1400ms Motion·위치는 변경하지 않았습니다.
- **메뉴 순서:** `menuOrder`는 5개 유사 메뉴끼리만 자리를 바꿉니다. 기본은 레이어 → 색상 → 선스타일 → 문자스타일 → 치수스타일(Figma 26:8944)입니다.
- **More:** Breakpoint는 그대로입니다. 제거된 메뉴는 More에도 없고, More 목록은 현재 순서를 따릅니다. 대표 슬롯은 레이어(없으면 순서상 첫 메뉴)이며 More 선택 메뉴가 대신합니다.
- **속성창 4상태:** 26:11886 314×256, 26:11823 314×361, 26:11932 314×658, 26:12035 314×754 — 같은 SWHeaderPropertyPanel과 데이터로 구성하며 Storybook 실측이 일치합니다.
- **형상 Slot:** Figma 26:11932 `Group 2937`(219×260) 안 `Group 2920` 208×208 그림 영역을 `.hm-sw-property__shape`(그룹 콘텐츠 기준 39, 26)로 분리했습니다. 기본 내용은 기존 Figma 원형 SVG(`property-circle.svg`)입니다. 비움·외부 컴포넌트에도 라벨 위치와 패널 높이는 같습니다.
- **Figma와 남은 차이(기존 구현 유지):** Figma 라벨 x 위치는 원주 111.5/지름 102/반지름 152.5/면적 98.5이고 구현은 113/100/154/100(1~2px, 기존 값), 지름 y는 Figma 108·구현 110입니다. 이번 범위에서 값 변경은 하지 않았습니다.
@@ -0,0 +1,45 @@
# SW Header 상세 버튼 5개 공통화 검증 — 2026-10-16
1. **최종 판정: PASS(요청 범위 / Chromium).** 실제 Pointer Hover를 포함한 주 검증 256건, 보존 확인 15건 PASS. 범용 Guard의 미검증은 아래 15번에 별도로 남깁니다. 과거 보고서를 덮어쓰지 않았습니다.
2. **시작 Git:** `D:\디자인시스템\HMDesign-SW`, `feature/sw-package`, HEAD `9407c8567f57301659e591c03451cca1b828733c`. `M package.json`, `M package-lock.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`가 작업 전부터 있었습니다. 해당 미커밋·미추적 작업을 보존했습니다. 작업별 경로·브랜치 및 Commit/Push 보류 지시를 적용했습니다.
3. **기존 구조:** 색상·선·문자·치수는 `SWHeaderDropdown.tsx`의 내부 공통 `SWHeaderMenuDetail`을 사용했으나 `disabled={!onClick}`으로 Callback 부재 시 Hover가 차단됐습니다. 레이어는 별도 JSX, 치수는 글자 100%·Default 아이콘을 사용하는 별도 Hover 규칙이 있었습니다.
4. **공통화:** 기존 `SWHeaderMenuDetail`을 보완하고 레이어까지 연결했습니다. 새로운 중복 컴포넌트나 공개 export는 추가하지 않았습니다. `SWHeaderMenuDetailProps`는 native `ButtonHTMLAttributes`를 확장하며 `label`, `icon`, `onClick`, `disabled`, `className`, `aria-label` 등을 지원합니다. 장식 아이콘과 접근성 이름을 구분합니다. 레이어의 기존 class와 토큰 이름은 유지했습니다. 상태 CSS는 `.hm-sw-header__menu-detail` 한 곳에서 관리하고 버튼별 CSS에는 폭만 남겼습니다.
5. **문자스타일 기준:** Figma 파일 `rj6wpsfk24lOWvWiVRAANP`, Default `26:10789` / Hover `26:10792`를 Plugin API 읽기와 스크린샷으로 확인했습니다. 변경 전 실제 문자스타일 버튼을 브라우저로 측정한 `detail-button-qa/baseline.json`의 `textDefault` / `textHover`가 구현 기준입니다. Figma의 배경 .15/.25, radius 24/25, 14px 아이콘, gap 4, padding 5/10, 글자 35%, 아이콘 내부 SVG 20%/35%와 일치합니다. Disabled 전용 Figma Variant는 이 2개 노드에 없으며 현재 Default 외형을 보존하고 클릭만 차단했습니다.
6. **Default 5개:** 레이어 113×24, 색상 99×24, 선스타일 128×24, 문자스타일 142×24, 치수스타일 142×24px. 전부 배경 `rgba(197,177,158,0.15)`, 글자 `rgb(255,255,255)`와 opacity .35, radius 24px, border 0, box-shadow none, Text Stroke 0px. 아이콘 요소 opacity는 1이며 SVG path 내부 opacity .2입니다. 기존 레이블·아이콘을 유지했습니다.
7. **Hover 5개:** 실제 `locator.hover()` 후 전부 배경 `rgba(197,177,158,0.25)`, radius 25px, 글자 opacity .35, SVG path .35로 전환. 밝은 치수 예외는 제거했습니다. Mouse Leave 시 5개 모두 Default로 복귀했습니다. CSS 강제 pseudo 상태만으로 PASS 처리하지 않았습니다.
8. **Disabled:** 명시적 `disabled`만 적용하며 Callback 유무와 분리했습니다. 5개 모두 Default 외형 유지, 실제 포인터 클릭 시 Callback 없음. Hover 차단, 키보드 포커스 제외는 native disabled 동작입니다. 기존 fieldset disabled도 native `:disabled` 규칙을 따릅니다. 일반 버튼은 키보드 Focus-visible 윤곽선을 유지합니다.
9. **Callback:** 비교 Story에서 각 일반 버튼 클릭 1회·Enter 1회와 Disabled 클릭 차단을 확인했습니다. Callback 없는 5개도 실제 Hover·클릭을 수행했고 오류가 없습니다. 실제 Dropdown 5종에서 기존 `onLayerDetail` / `onColorDetail` / `onLineStyleDetail` / `onTextStyleDetail` / `onDimensionStyleDetail` 호출·닫힘·필드 Focus 복귀를 확인했습니다. 호스트 Callback이 없으면 실행·팝업·CAD 처리를 흉내 내지 않고 무동작입니다.
10. **Computed Style 차이:** 동일 상태의 5개 스타일은 변경 전 문자스타일 기준과 비교하여 차이 0(폭·텍스트의 의도된 차이 제외). 배경·color·opacity·border·radius·box-shadow·글자색/opacity/Stroke·아이콘 opacity/content를 비교했습니다. 행 Hover는 선·문자·치수 목록에서 기존 `rgb(98,83,61)` 유지. 상세 버튼 외 목록/Selected/상단 Header Hover 규칙은 수정하지 않았습니다. 전체 실측값은 [주 검증 JSON](./detail-button-qa/verify-result.json)에 있습니다.
11. **Story 및 회귀:** 기존 31개 유지 + verification Story 1개 = **32개 렌더 PASS**, page/console 오류 0. Default·Controls 2개 Story 각각 10폭(1920/1800/1440/1180/1080/900/768/640/520/390)에서 Header 포함 99개 요소의 위치·크기·display·visibility·color·background를 전후 비교해 차이 0. 11개 구성 프리셋×7폭 = **77/77 PASS**(메뉴 DOM 순서·제거·Header 높이·overflow 및 More 비어 있지 않음). 390/768/1280/1440에서 Header·문서 가로 overflow 0. 속성창 4상태는 314×256/361/658/754px, 창 이동은 3개 1400ms 애니메이션과 Leave 정리, K는 511ms 진입과 Leave 닫힘, Colorize 24px, 기존 편집 Input과 caret 색을 확인했습니다. [보존 확인 JSON](./detail-button-qa/preservation-result.json). 속성Bar·형상 Slot을 포함한 기존 Story가 렌더링됐으며 그 기능 소스는 수정하지 않았습니다. 기존 각 Story의 모든 상호작용을 전수 재검사했다는 뜻은 아닙니다.
12. **Typecheck / Build:** `npm.cmd run typecheck:sw`, `npm.cmd run build:sw`, `npm.cmd run typecheck` 모두 exit 0. Root typecheck의 pretypecheck가 package build를 수행한 후 동일 산출물을 사용해 `npm.cmd --ignore-scripts run build`, `npm.cmd --ignore-scripts run build-storybook`을 실행해 exit 0을 확인했습니다. 후속 Story 설명 수정은 Storybook만 다시 빌드했습니다. 기존 `bg-[var(...)]` CSS 최적화 경고와 chunk 크기 경고가 있으며 빌드 실패는 없습니다. 로그는 `detail-button-qa/root-typecheck.log`, `root-build.log`, `storybook-build.log`에 있습니다. 최종 브라우저 검증은 정적 Storybook 빌드를 로컬 HTTP로 제공해 실행했습니다. 패키지 재빌드 중 dev HMR 404가 발생한 시험은 PASS로 사용하지 않았습니다.
13. **변경 파일:** `packages/sw/src/components/sw-header/components/` 아래 `SWHeaderDropdown.tsx`, `SWHeaderLayerMenu.tsx`, `SWHeaderStyleListMenus.tsx`, `SWHeaderColorMenu.tsx`, `SWHeaderLineStyleMenu.tsx`; `packages/sw/src/theme/sw.css`; `src/sw/sw-header.stories.tsx`, `src/sw/sw-stories.css`; `docs/sw/sw-header-usage.md`, `docs/sw/sw-header-design-spec.md`; 신규 `packages/sw/tests/detail-buttons.browser.mjs`, 이 보고서, `docs/sw/detail-button-qa/` 증거. Color/Line 파일은 Callback 설명 주석만 갱신했습니다. Build가 gitignored `dist`·`storybook-static`도 갱신했습니다. `package.json`/lock·UI/OrgChart/Center Graphic/기존 공개 export·기존 `_local/` 파일은 편집하지 않았습니다.
14. **남은 디자인 차이:** Figma Dotum Regular와 현재 Noto Sans KR 700의 글리프 형태·굵기 차이는 기존대로입니다. Figma TEXT의 .4px Stroke와 달리 현재 구현의 상세 글자 Stroke는 0px이며, 사용자가 제거 유지 지시한 선언을 복구하지 않았습니다. 버튼 간 Default/Hover/Disabled 불일치는 발견하지 못했습니다. Figma와 현재 버튼 차이는 이번 요청의 우선순위에 따라 현재 문자스타일을 유지했습니다.
15. **NOT VERIFIED:** 실제 CAD/설치형 SW 호스트의 상세 팝업과 명령 적용, Firefox/Safari, 스크린리더 실제 낭독, 전체 Story의 모든 기존 상호작용 및 전 화면 픽셀 diff. 범용 Guard는 `status/staticStatus=NOT VERIFIED`, FAIL 0입니다. 원인: `@hm-design/sw/styles.css` resolver 및 native 버튼 재사용 판정은 범용 UI 검사로 자동 확정되지 않음. SW는 기존부터 UI와 독립된 컴포넌트 패키지이며 내부 버튼을 보완한 작업입니다. Guard 원본의 `styleEvidence`는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED이며 checks가 빈 배열이므로 SW CSS가 모두 검증됐다는 의미로 사용하지 않습니다. 원본 [guard.json](./detail-button-qa/guard.json)을 덮어 승격하지 않고 위 브라우저·Figma 근거를 별도로 제시합니다.
16. **종료 Git:** `feature/sw-package`, HEAD `9407c8567f57301659e591c03451cca1b828733c`; 상태 표시는 시작과 같은 `M package.json`, `M package-lock.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`이며 SW 디렉터리 안에 이번 변경이 추가됐습니다. 공식 원격 `main`도 같은 SHA를 읽기 확인했습니다. Stage/Commit/Push/PR/Merge/Publish/배포 없음. `git diff --check` 통과.
재현: `node packages/sw/tests/detail-buttons.browser.mjs verify docs/sw/detail-button-qa static`, 추가 보존 확인은 `preservation` 모드. Chrome/Playwright 경로는 `SW_CHROME_PATH` / `SW_PLAYWRIGHT_PATH`로 바꿀 수 있습니다. 변경 전 baseline은 이 작업 시작 상태이며 새로 덮어쓰면 기존 회귀 비교 기준이 바뀝니다.
비교 이미지: [Default](./detail-button-qa/comparison-default.png), [실제 Pointer Hover](./detail-button-qa/comparison-pointer-hover.png).
시간 기록(Asia/Seoul, KST):
- Started At: 2026-10-16 17:47:45
- Completed At: 2026-10-16 18:01:40
- Actual Duration: 13분 55초
- Estimated Duration: 최초 30~50분
- Variance: 예상보다 16분 05초~36분 05초 짧음
@@ -0,0 +1,114 @@
# SW Header 검증 기록 — Figma Dropdown 8종 · More 대표 필드 · Text Stroke · Input (2026-10-16)
> 이 문서는 이번 작업의 새 결과만 기록합니다. 이전 검증 기록([레이어 Dropdown](./sw-header-layer-dropdown-verification-2026-10-16.md), [속도·Composed Hover](./sw-header-speed-composed-hover-verification-2026-10-16.md), [창 이동 GIF](./sw-header-window-motion-gif-verification-2026-10-16.md), [창 이동 과거](./sw-header-window-motion-verification-2026-10-16.md))은 당시 결과로 보존합니다. 기준 명세: [디자인 명세 §5.3–§5.5, §6.1](./sw-header-design-spec.md).
## 1. 범위와 환경
| 항목 | 값 |
|---|---|
| Worktree / Branch / HEAD | `D:\디자인시스템\HMDesign-SW` / `feature/sw-package` / `9407c85`(시작·종료 동일, Commit 없음) |
| 요청 | ① Figma 8개 Dropdown ② 반응형 More 대표 필드 ③ 헤더 전체 Text Stroke ④ Dropdown 내부 Hover 정책 + 추가 요청 ⑤ 레이어 상세 버튼 · 수정 가능한 Input · 커서 · 문서 1개 기본 목록 ⑥ 동적 데이터 구조 |
| Figma | `rj6wpsfk24lOWvWiVRAANP` — 26:10986, 26:11036, 26:12666, 26:12659, 26:11104, 26:10702, 26:11181, 26:11215, 26:8944, 26:10767, 26:11656, 26:11664 (+ 행·input·상세 버튼 Variant). MCP `get_design_context` / `get_screenshot` + Plugin API 읽기 전용 스크립트(TEXT Stroke 전수 조사, Variant별 geometry) |
| Storybook | 이 Worktree의 dev 서버 `127.0.0.1:6024`(이미 실행 중이던 프로세스 사용) |
| 브라우저 검증 | Headless Chromium(Playwright, 다른 Worktree `HMDesign-OrgChart-Spike/node_modules`의 패키지를 읽기 전용으로 로드, 그 Worktree는 수정하지 않음). 스크립트·스크린샷은 세션 scratchpad(저장소 밖). 앱 내 Browser pane은 숨김 상태라 스크린샷이 갱신되지 않아 사용하지 않음 |
## 2. Figma 8개 디자인 (PASS-A)
브라우저 `getBoundingClientRect`(1920 프레임, 필드 기준 상대 좌표).
| 노드 | Figma | 구현 측정 | 판정 |
|---|---|---|---|
| 색상 26:10986 | 필드 192 × 38, 목록 192 × 158 @ y38, ByLayer 그룹 (12.5, 10) 167 × 30, 팔레트 (12, 50) 168 × 22, 도구 (15, 82), 하단 y118 h40, 링 194 × 198 | 192 × 38 / 192 × 158 @ 38 / (12.5, 48−38=10) 167 × 30 / (12, 50) / (15, 82) / 118 × 40 / 링 | MATCH |
| 사용자 색상 26:11036 | 본문 182, 사용자 행 24 | 182 | MATCH(사용자 칩 왼쪽 정렬 수정 후) |
| ByLayer / ByBlock | 82.5 × 28, Default / on 배경·inner shadow, 글자 `#D4CBBD` | 82.5 × 28, `aria-pressed` 상호 배타, on `#62533D` | MATCH(Hover 정책 §5) |
| 선스타일 26:11104 | 필드 284, drop_wrap 468 × 192 @ (−92, 37), drop 466 × 190, 열 182/142/142 | 473.2 × 192 @ (−94, 37), 열 187.2/142/142 | **차이**: 선 종류 열이 "Continuous"(Noto) 폭에 맞춰 +5.2px |
| 선 종류 행 26:10702 | 182 × 30, p12, gap 8, 70 × 2 미리보기 @ (12, 14), Hover `#62533D` / `#FF5C00` | 187.2 × 30, 미리보기 (12, 14) 70 × 2, CENTER 22·7·22·7, HIDDEN 33·33, Hover 색 일치 | MATCH(폭 차이 위와 같음) |
| 문자스타일 26:11181 | 164, 행 30 × 2, 하단 40(왼쪽 정렬), 본문 100 | 164 × 100, 행 30, 하단 (0, 98) 40 | MATCH(글꼴 칸 38 → 내용 폭 최대 56) |
| 치수스타일 26:11215 | 164, 행 30, 미리보기 60 × 15 @ (12, 7.5), 하단 오른쪽 | 164 × 100, 미리보기 (12, 7.5) 60 × 15, Hover 자산 전환 | MATCH |
| 상세 pill(색상·선·문자·치수) | 14 아이콘, gap 4, p 5/10, `.15`, radius 24, 글자 35% | 같음, 폭은 Noto 글자 폭(색상 상세 94.1 vs Figma 99) | MATCH(글꼴 폭 차이) |
자산: `color-pick.svg`(Figma `colorize`), `menu-dimension-row.svg` / `-hover.svg`(Figma 26:10729 행, mask id 외 동일 · Hover는 foreground fill 1곳만 다름). 상세 아이콘은 기존 `layer-detail.svg`가 Figma `ico_layer` Default와 동일해 재사용. `dist/index.js`·`dist/styles.css`에서 `figma.com/api/mcp/asset` 0건.
## 3. 상호작용 · Callback · Keyboard (PASS-E)
`Color Dropdown` Story의 호스트 예제(전 속성 연결)에서 실행, 26개 항목 모두 PASS:
- Hover로 열리지 않음 / 클릭으로 열림(`aria-expanded`, `aria-controls` 연결).
- 칩 → `onColorSelect(red)` 1회, 필드 `241,4,4`로 갱신, 목록 유지. ByLayer → ByBlock: `aria-pressed` `false,true`.
- RGB `256,0,0` → `aria-invalid`, Enter 시 보고 없이 복원. `#00ff00` → `onColorInput(0,255,0)` 1회. caret `rgb(230, 4, 3)`.
- Escape 닫기 + 필드 Focus 복귀. ↓ 열기 + 눌린 항목 Focus, ↓ 다음 항목.
- 선스타일을 열면 색상 닫힘. 선 종류 HIDDEN → 가중치·스케일 유지, 가중치 ByBlock → 선 종류·스케일 유지, User 0.35 / 스케일 2.5 확정, 스케일 0 거부, Tab으로 마지막 항목을 벗어나면 닫힘, 편집 중 Escape는 편집만 취소(목록 유지).
- 바깥 클릭 닫힘. 문자스타일 / 치수스타일 선택 → 닫힘 + 필드 갱신. 다른 속성(레이어·색상) 유지. 문자스타일 상세 → `onTextStyleDetail` 1회 후 닫힘.
- 선택·입력 Callback 중복 0, 페이지 오류 0.
## 4. 반응형 More 대표 필드 (PASS-B)
`Responsive More` Story, 16개 폭(1920 · 1800 · 1799 · 1440 · 1180 · 1179 · 1080 · 1079 · 900 · 899 · 768 · 640 · 639 · 520 · 519 · 390) × 더보기에 있는 모든 필드 항목. **0 FAIL**.
- 모든 폭: 헤더 49px, 헤더 가로 넘침 0, 객체 수정 영역 자식 겹침 0 · 영역 밖 0. ≥1800은 More 숨김(1920 배열 유지).
- 더보기 → 필드: 그 필드가 첫 칸, Dropdown 열림, Dropdown 좌·우가 헤더 안(390에서 선스타일은 세로 3단 191.2px), Focus = 그 필드, 겹침·넘침 0. → 레이어: 원래 배치, 레이어 Dropdown 열림.
- 1440 순서 테스트(선스타일 → 문자스타일 → 레이어): Callback = 각 `on*MenuOpenChange` + `on*Open` 1회씩, 데이터 Callback 0, 상태표(값) 변화 없음.
- 대표 슬롯 선택 근거: 객체 수정 영역 첫 칸(레이어 자리) — 1920 배열 첫 칸이며 모든 Breakpoint에서 남는 칸, 로고·파일명·창 제어와 분리. 최소 변경(CSS `order`·표시 규칙 + `SWHeader` UI 상태 1개).
## 5. Dropdown 내부 Hover 정책 (PASS-D)
- 독립 버튼(ByLayer · ByBlock · 칩 · 사용자 색상 · 색상 추출 · 상세 4종 · 레이어 상세): Storybook 전용 `data-sb-hover`로 Hover를 고정한 스크린샷이 Default와 동일. 레이어 상세는 computed style(배경 `rgba(197,177,158,.15)`, radius 24px, padding 5/10, 글자 .35) Default = Hover 고정.
- 목록 행 Hover 유지: 선 종류(1·4행), 가중치, 스케일, 문자·치수 행 = `#62533D` + `#FF5C00`(선 미리보기·치수 자산 포함). 현재 행 Hover는 이름 `#FFC600` 유지.
- Input Hover 유지(추가 요청): 1px `rgba(212,203,189,.25)`, 색상 입력은 `rgba(197,177,158,.2)`.
- 문서 Dropdown Hover(행 검정 40%, `+ New`/`Open` .3 / `#000`)는 변경 없음(computed 확인).
## 6. Text Stroke (PASS-C)
Figma 조사 결과와 구현 기준은 [명세 §6.1](./sw-header-design-spec.md#61-text-stroke-2026-10-16). Default Story computed style:
| 요소 | text-shadow | 자기 색 Stroke | 판정 |
|---|---|---|---|
| 파일명, 그리기모드, 필드 값, 글꼴 미리보기, 속성Bar/속성창, 문서 목록 이름 | 검정 1.3px 16방향 | 0.6px(바깥 0.3) | PASS |
| 12px 라벨, 상세보기 caption | 검정 1px 16방향 | 0 | PASS |
| 레이어 상세 / `… 상세` 글자 | 없음 | 0.8px(바깥 0.4) | PASS |
| `+ New` / `Open`, 창 이동 Tooltip, 더보기 항목 | 없음 | 0 | PASS(Figma 없음 / 요청으로 유지) |
| Input 값(`#E60403`) | 검정 1.3px | 0.6px | PASS |
- 8배 확대 비교: 검정 외곽선이 글자 바깥에 그려지고 Hover(`#FFC600`)·행 Hover(`#FF5C00`)에서도 유지. 말줄임 텍스트 외곽선 잘림 없음(`overflow-clip-margin` 2px).
- 남은 차이: Noto Sans KR 700 + Stroke는 Figma Dotum Regular + Stroke보다 약간 굵게 보입니다(Font Weight 정책 유지). 토큰으로 조정 가능.
## 7. 레이어 상세 · Input · 커서 · 문서 1개 (추가 요청)
| 항목 | 결과 |
|---|---|
| 레이어 상세 Default | 107.3 × 24(Figma 113 × 24는 Dotum 글자 폭), 배경·radius·padding·gap·글자 35% 일치. Hover 고정 = Default(무변화). Callback 없을 때 disabled + Figma 외형(opacity 1) |
| Input Default / Hover / 편집 | 58 × 20 / 58 × 24 / 58 × 24 — Figma와 동일. 글자 x 위치 Default = Hover(6px). 편집 `#0F0D0A` + 1px `.3`. ReadOnly는 20px 유지, Disabled 40% |
| 커서 | caret `#E60403`(computed). 깜박임·위치·선택은 네이티브. caret 높이 1 × 14 정밀 일치는 NOT VERIFIED |
| 편집 | 숫자 `12.75` Enter → `onCommit(12.75)`, `1.2.3` 복원, Escape → `onCancel` + 복원, 외부 값 `12.5` 반영, Tab(blur) 확정 |
| 한글 IME | CDP `Input.imeSetComposition`으로 조합 중 Enter 이벤트 → 확정 안 됨, 조합 완료 후 Enter → `한글` 확정. 실제 OS IME(Windows 한글 입력기) 수동 입력은 NOT VERIFIED |
| 문서 1개 기본 | Default Story에서 파일명 클릭 → 문서 1행(현재 문서 `#FFC600`, X 있음). `+ New` 55 × 24 / `Open` 50 × 24, 배경 .2, 글자 검정 70%, Hover .3 / `#000` — 기존 그대로 |
## 8. 동적 데이터 (추가 요청)
`Dynamic Data` Story: 선 종류 추가 · 순서 뒤집기(현재 항목 id로 유지) · 첫 항목 이름 변경(그 항목만), 현재 레이어 삭제(선택 표시 없음, 필드는 `layer` Props, 오류 없음), 레이어 추가, User 가중치 값 변경(그 행만), 문자스타일 14개(목록 240px 내부 스크롤, 헤더 49px), 문서 추가(2행, 현재 표시), 팔레트 삭제 · 사용자 색상 추가(6 + 3칩), 페이지 오류 0 — 13개 항목 PASS.
## 9. 보호 대상 (PASS-F)
- 기준 스냅샷(작업 시작 시 scratchpad 복사본)과 비교해 변경 없음: `SWHeaderLayerMenu.tsx`, `SWHeaderLayerIcons.tsx`, `SWHeaderWindowControls.tsx`, `SWHeaderDocumentTitle.tsx`, `SWHeaderHome.tsx`, `SWHeaderActions.tsx`, `SWHeaderProperties.tsx`.
- 창 이동 Motion CSS 블록과 `motion`이 들어간 모든 CSS 줄이 기준과 동일(1400ms · Keyframes · Drag 정지 · Reduced Motion).
- 레이어: 전구·눈꽃·자물쇠 클릭 → `onLayerVisibilityChange(layer-1, false)` · `onLayerFreezeChange(layer-1, true)` · `onLayerLockChange(layer-1, true)`, `onLayerSelect` 없음, 목록 유지, 아이콘 회색 — 확정 매핑 유지. `SWHeader`는 레이어 Dropdown 열림 상태를 제어형으로 전달(더보기 → 레이어 열기용)하며 레이어 컴포넌트 동작은 그대로.
- 1920 좌표: 레이어 467/234, 색상 705/192, 선 901/284, 문자 1189/164, 치수 1357/164, 상세보기 1521/157, 창 1680/240, 파일명 65/310, 높이 49 — 기준과 동일.
- 전체 17개 SW Header Story 렌더링 오류 0, 헤더 높이 모두 49px.
## 10. 빌드 (PASS-G)
| 명령 | 결과 |
|---|---|
| `npm run typecheck:sw` | PASS |
| `npm run build:sw` | PASS (`dist/styles.css` 70.71 kB, `dist/index.js` 239.32 kB) |
| `npm run typecheck`(루트, `build:packages` 포함) | PASS |
| `npm run build`(루트) | PASS. 기존 Tailwind `bg-[var(...)]` CSS 최적화 경고 1건·chunk 크기 경고는 이번 변경과 무관 |
| `npm run build-storybook` | PASS. SW Header Story 17개 index 확인(기존 10 + 신규 7) |
## 11. 남은 차이 · NOT VERIFIED
- 선 종류 열 182 → 187.2px(전체 468 → 473.2px), 문자스타일 글꼴 칸 38 → 내용 폭, 상세 pill 글자 폭: Dotum → Noto Sans KR 글자 폭 차이(글자 잘림 방지).
- Text Stroke 굵기 체감 차이(Noto 700 + Stroke vs Dotum Regular + Stroke).
- Figma 문자스타일 Variant3의 상세 버튼은 hover Variant(.25)로 배치되어 있으나 Hover 정책상 Default(.15)로 표시.
- 색상 선택 팝업("컬러칩 클릭", Figma "추후 디자인 진행예정")과 모든 `… 상세` 팝업 내부 화면: 미구현(Figma 전달 후).
- NOT VERIFIED: 실제 SW 호스트(설치형 SW / Electron)에서의 색·선·문자·치수 적용과 색상 추출, 실제 OS 한글 IME 수동 입력, caret 높이 14px 정밀 일치, Safari·Firefox의 `overflow-clip-margin`·`field-sizing`·`:has()` 렌더링(Chromium만 검증), 스크린리더 실제 낭독, 픽셀 단위 이미지 diff(좌표·computed style·확대 육안 비교만 수행).
@@ -0,0 +1,98 @@
# SW Header 후속 5개 요구사항 — 최종 통합 검증
이번 요청의 구현·실행 범위에 대한 기록입니다. 과거 PASS 문서를 덮어쓰지 않으며 전체 HMDesign·CAD 호스트·배포 승인을 뜻하지 않습니다.
1. **최종 판정: PASS — 요청한 구현 및 실행 검사 범위.** PASS-A~H를 확인했습니다. 별도 Figma 돋보기 Overlay 구현은 요청의 우선 구현 범위 밖인 호스트 연결 대상으로 남기며, Guard와 보조기기 등 미검증은 21번에서 분리합니다.
2. **시작 Git:** `D:\디자인시스템\HMDesign-SW`, `feature/sw-package`, `9407c8567f57301659e591c03451cca1b828733c`. 기존 `M package.json`, `M package-lock.json`, `?? packages/sw/`, `?? src/sw/`, `?? docs/sw/`를 보존했습니다. 이번 요청의 명시 경로·브랜치·Commit/Push 금지가 일반 Gitea main 정책보다 우선합니다. 시작 파일을 QA baseline에 복사하고 SHA-256을 기록했습니다.
3. **재사용:** 기존 Dropdown 8종·레이어 아이콘·Color/Number parser·SWHeaderInput·속성창 Property/Selection 타입·More·K 가이드·1400ms 창 이동·문서 목록·로고 슬롯·창 제어를 재사용했습니다. 다른 패키지 소스·기존 root manifest/lock은 수정하지 않았습니다. 원본 자산 45개와 과거 검증 문서 6개는 baseline과 바이트 단위로 같습니다.
4. **속성Bar 디자인/크기:** 새 공개 `SWHeaderPropertyBar`. Figma 26:10492를 직접 확인해 최대 1222×28, #271F13, 좌우/하단 #08251C, 하단 radius 6, inset shadow를 구현했습니다. 독립 기준 Story 실측 1222×28px입니다. 연결된 바는 객체 수정 영역의 실제 왼쪽부터 부모의 남은 폭을 사용합니다. 1920px 실제 Header에서 바 1222×28, Header 49px을 확인했으며 x=377을 하드코딩하지 않았습니다. 기존 Noto Sans KR 정책의 차이는 20번을 따릅니다.
5. **열기/닫기:** 기존 속성Bar 버튼 및 좁은 화면의 More를 연결했습니다. 미전달 시 내부 상태, propertyBarOpen/onPropertyBarOpenChange의 Controlled 상태, defaultPropertyBarOpen, 기존 onPropertyBarToggle을 지원합니다. 반복 클릭 닫기·외부 상태 변경을 확인했습니다. 바와 속성창은 동시 표시 및 독립 닫기가 가능하며, 바 토글이 다른 Dropdown 상태를 초기화하지 않습니다. 호스트 콘텐츠는 열린 바의 별도 28px 공간을 예약할 수 있습니다.
6. **동적 데이터/검증:** 속성창과 같은 SWHeaderProperty/Selection 레코드 및 onPropertyChange를 공유합니다. 객체 교체·필드 추가/삭제·라벨·순서·값·외부 갱신·전체/개별 ReadOnly/Disabled, min/max/validate를 확인했습니다. Enter/Blur 확정·Escape 취소·유효하지 않은 값 복원·편집 중 필드 삭제 시 draft 폐기를 확인했습니다. 값은 호스트 데이터이며 원/수치/CAD 계산을 패키지 기본값으로 넣지 않았습니다. Bar의 select는 option id 입력 검사, slider는 숫자 편집, computed는 읽기 전용으로 처리합니다.
7. **메뉴 단위 구성:** `controls?: readonly SWHeaderControlConfig[]`와 SWHeaderControlId를 공개했습니다. document/drawingMode/5개 메뉴/details/propertyBar/propertyPanel/print/popupGuide 각각을 allowlist로 구성합니다. 개별 제거·여러 개 제거·일부만 사용·모두 제거·재추가·visible:false/복원·배열 순서·허용 라벨 변경을 실제 UI로 확인했습니다. 파일명 데이터와 문서 접근성 접두사, K 아이콘과 접근성 이름을 구분했습니다. 컨트롤 내부 옵션 API는 기존과 같습니다. 설정 미전달 시 기존 1920px 디자인·계층을 유지하며, 설정 모드는 개별 컨트롤을 배열 순서로 같은 객체 영역에 배치합니다.
8. **More/값/Callback:** 기존 Breakpoint를 유지합니다. 제거 항목은 More에서도 제외하고, 남은 레이어 또는 첫 필드를 대표로 사용하며 대표 삭제 시 대체합니다. More에서 필드를 선택하면 표시·열림·Focus가 연결됩니다. 재정렬은 keyed 노드를 유지합니다. 편집 중 메뉴의 구조적 제거는 draft를 폐기하고 불필요한 데이터 Callback을 보내지 않습니다. 일반 Dropdown 닫힘의 기존 commit 동작은 유지합니다. 재추가 시 외부 선택값은 보존하고 내부 Dropdown은 닫힙니다. 열린 메뉴 재정렬 시 경계 위치도 다시 계산합니다. 개별 컨트롤 하나만 남긴 7개 폭을 포함한 경계 검사 84/84가 통과했습니다.
9. **Text Stroke Story:** 지정 제목·원본 스크린샷 영역·전용 import를 제거했습니다. 나머지 콘텐츠·실제 Header Typography/색상·다른 Figma 비교 Story는 유지했습니다. 원본 PNG 파일은 보존했습니다.
10. **반복 선언의 원인:** 작업 전 원본 `packages/sw/src/theme/sw.css` 1394행에 정확한 solid-stroke-width 참조가 실제로 있었고 변수는 131행에 정의되어 있었습니다. `packages/sw/src/index.ts` → 원본 CSS, `build.mjs` → dist/styles.css, Story → 공개 `@hm-design/sw/styles.css` → dist의 경로를 확인했습니다. 원본과 산출물이 달랐으므로 dist만 삭제하는 방식은 다음 build 때 원본에서 다시 포함되는 구조입니다. 소스·빌드/복사 스크립트·Storybook import·중복 정의를 조사했고 별도 생성기가 이 선언을 추가하거나 dist가 src를 역생성하는 증거는 없습니다. 과거 사용자가 실제 편집했던 파일 위치나 당시 캐시까지 관찰한 것은 아니므로 그 행위를 단정하지 않습니다.
11. **원본 수정:** `packages/sw/src/theme/sw.css`의 `:is(.hm-sw-header__layer-detail, .hm-sw-header__menu-detail) > span` Stroke 선언과 유일한 미사용 `--hm-sw-header-solid-stroke-width`를 제거했습니다. 덮어쓰기 `0`은 추가하지 않았습니다. 기존 text-shadow·다른 메뉴 텍스트·SVG·상세 버튼 크기 및 Hover는 보존했습니다. 이전 text-stroke-width 변수 제거와 구분한 새 수정입니다.
12. **Build/HMR 재생성 방지:** 원본/패키지 dist와 최종 Storybook CSS 14개에서 해당 선언·변수 일치 0입니다. 실제 브라우저에서 5개 상세 버튼의 Default/Hover 및 열린 선택 필드 상태를 확인했습니다. 일시적 CSS 검사 표식을 원본에 넣고 패키지 빌드 → 브라우저 computed 반영 → 원본 복원·재빌드 → 표식 제거 반영 → 새로고침까지 실행했습니다. Vite WebSocket의 `/packages/sw/dist/styles.css` update와 computed 변경을 확인했습니다. CSS import는 Vite의 js-update 모듈 형태로 전달됩니다. 검사 표식은 최종 소스/산출물에 없습니다. 재생성 방지 HMR 검사 8/8입니다. 현재 브라우저에서 오래된 Stroke를 적용하는 캐시 증거는 없었습니다.
13. **btn_colorize 비교:** Figma 26:11301 및 Default 26:11302 / Hover 26:11305 context·스크린샷을 직접 확인했습니다. 각각의 원본 SVG를 colorize-default.svg/colorize-hover.svg로 저장했습니다. 버튼 실측 24×24, 두 SVG의 원본·렌더 크기 18×18, radius 3, 지정 gradient·기본 border·drop shadow, Hover #DA6813 border·검정25% overlay·전용 SVG를 확인했습니다. 임시 Figma URL은 런타임에 없습니다. 기존 color-pick.svg는 기존 자산 보존 원칙으로 남겼습니다.
14. **아이콘/Callback:** 기존 색상 메뉴의 색상 추출 위치를 교체했습니다. Default/Hover/Mouse Leave/키보드 focus-visible/Enter 클릭을 확인했습니다. Hover 시 Callback 없음, 클릭 시 기존 onColorPick 1회·메뉴 닫힘, 기존 색상값·모드 유지가 통과했습니다. 색상 입력 검증·사용자 색상·ByLayer/ByBlock 필수 선택과 선택 강조도 기존 회귀 검사를 통과했습니다.
15. **26:11338 Overlay:** 패키지 내 실제 색상 돋보기 Overlay 구현은 없습니다. 기존 onColorPick으로 호스트가 연결할 수 있으며 별도의 임의 색상 선택창은 만들지 않았습니다. 현재 구현 상태는 사용 가이드·Story에 명시했습니다.
16. **반응형:** 실제 Header 실측 폭 1920/1440/1080/900/768/640/390px에서 49px·가로 넘침 없음·바 28px·viewport 경계·스크롤로 마지막 필드 접근·More 대표 전환·삭제 후 대체·Dropdown 경계를 확인했습니다. 각 컨트롤 단독 구성도 같은 7개 폭에서 검사했습니다. 기존 속성창/K 가이드 경계와 창 이동 Motion은 기존 회귀와 보존 검사로 확인했습니다. 서로 다른 팝업 상태와 속성Bar는 독립적이며 overlay z-index는 기존 Dropdown보다 낮습니다.
17. **Story 회귀:** 기존 21개 export를 보존하고 Property Bar / Dynamic Controls / Colorize 3개를 추가해 총 24개입니다. 모든 Story 렌더·적용 Header 높이와 기존 주요 상호작용을 검사했습니다. 기존 통합 195/195, 기존 보완 43/43, 신규 122/122, 추가 경계 84/84, HMR 8/8이 통과했습니다. 보완 원본의 45개 중 이번 라벨 API로 수정한 DocumentTitle/Fields의 바이트 동일성 2개는 제외하고 나머지 보존/상호작용을 검사했습니다. 모든 가능한 호스트 데이터 조합의 전수 검증이라는 뜻은 아닙니다.
18. **명령/경고:** package.json의 실제 명령을 확인해 최종 SW Typecheck → SW Build → Root Typecheck → Root Build → Storybook Build 순차 실행, 모두 exit 0입니다. pre 훅에 포함된 build:packages 반복은 기존 명령 정의 그대로입니다. SW 출력 styles.css 약85.83kB/index.js 약686.35kB. Root/Storybook의 기존 유형 Tailwind bg-[var(...)] CSS 최적화 경고·500kB chunk 경고·Storybook 플러그인 시간 안내는 남습니다. SW 신규 타입/빌드 오류나 신규 형태의 소스 경고는 없습니다. 로컬 초기 개발 서버의 전역 설정/registry 쓰기 제한은 브라우저 검증용 서버에서 해결했고 제품 소스를 수정하지 않았습니다. lint 명령은 정의되어 있지 않습니다.
19. **변경 파일:** baseline 대비 소스/문서 23개(기존 19개 수정, 신규 자산/컴포넌트/본 보고 4개 추가), 생성 dist 제외입니다. 기존 root package.json/lock은 이번 작업 전후 바이트가 같습니다. 상세 목록은 아래에 있습니다.
20. **Figma와 남은 차이:** 기존 패키지의 Noto Sans KR 400/700 정책을 유지해 Figma Dotum Regular와 글자 폭/획/베이스라인이 다릅니다. 바의 수치는 호스트 예시 데이터이고 산술 관계를 계산/보정하지 않습니다. 구성 모드는 사용자가 지정한 순서대로 펼친 구성이므로 승인된 기본 그룹 배치와 다를 수 있으며 미설정 기본 디자인은 보존합니다. 과거 보고에 있는 선스타일 content 폭 차이와 운영 호스트별 font/CSS 커스터마이징은 이번 범위 밖입니다.
21. **NOT VERIFIED:** Guard 원본 status/staticStatus는 NOT VERIFIED, FAIL 0 / 미검증65건(ENTRY-002 1, REUSE-004 43, SCOPE-001 15, TOKEN-001 3, TOKEN-002 3)입니다. styleEvidence는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED이며 checks 배열은 비어 있습니다. 이를 전체 디자인 또는 동적 입력의 PASS로 확대하지 않습니다. 독립 SW CSS 공개 entry, Story native 호스트 조작, 동적 class/style 및 guard scope 해석 한계는 브라우저 증거와 별개로 남깁니다. 픽셀 단위 자동 Figma 이미지 diff, 실제 Figma 프로토타입 직접 재생, Windows IME/스크린리더 실기기, 모든 대비 조합, CAD 연산·파일/OS 창 동작·실제 26:11338·외부 소비앱 캐시·배포는 미검증입니다.
22. **종료 Git:** 시작과 같은 feature/sw-package / 9407c8567f57301659e591c03451cca1b828733c, 기존 root 2개 M와 SW 3개 폴더 Untracked 범주를 유지합니다. git diff --check 통과. Stage/Commit/Push/PR/Merge/Publish/배포, Reset/Clean/강제 Checkout을 수행하지 않았습니다. 사용자 검토/승인을 기다립니다.
## 변경 파일 목록
```text
MOD packages/sw/README.md
MOD packages/sw/src/components/sw-header/index.ts
MOD packages/sw/src/components/sw-header/SWHeader.tsx
MOD packages/sw/src/components/sw-header/types.ts
MOD packages/sw/src/components/sw-header/components/SWHeaderColorMenu.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderDocumentTitle.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderDropdown.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderFields.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderInput.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderKGuide.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderLayerMenu.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderLineStyleMenu.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderMore.tsx
MOD packages/sw/src/components/sw-header/components/SWHeaderStyleListMenus.tsx
MOD packages/sw/src/theme/sw.css
MOD src/sw/sw-header.stories.tsx
MOD src/sw/sw-stories.css
MOD docs/sw/sw-header-design-spec.md
MOD docs/sw/sw-header-usage.md
ADD packages/sw/src/assets/colorize-default.svg
ADD packages/sw/src/assets/colorize-hover.svg
ADD packages/sw/src/components/sw-header/components/SWHeaderPropertyBar.tsx
ADD docs/sw/sw-header-followup-five-verification-2026-10-16.md
```
## 검증 증거
QA root: `C:\Users\user\.codex\visualizations\2026\10\16\01a1439a-bd89-7030-8978-48f687e44b0d\sw-header`
- baseline/, baseline-sha256.json, git-start.txt: 기존 미커밋 파일 보존본.
- legacy-verify.mjs / browser-results.json: 기존 통합195개.
- legacy-followup.mjs / followup-results.json: 기존 보완43개·원본 보존.
- new-verify.mjs / new-results.json: 신규122개, 실제7개 Header 폭.
- edges.mjs / edges-results.json: 단독 구성·열린 메뉴 재정렬84개.
- hmr.mjs / hmr-results.json / hmr-build.log: 재빌드·HMR·복원·새로고침8개.
- build-results.json / final-*.log: 최종 순차5개 명령.
- guard-final.json: 원본 미검증65개, 임의로 PASS로 변경하지 않음.
- changed-files.json: baseline 대비 파일 목록.
- header-bar-1920.png, header-bar-390.png, colorize-hover.png: 실제 React 렌더.
## 시간 기록 (Asia/Seoul)
- Started At: 2026-10-16 16:26:26 KST
- Completed At: 2026-10-16 16:50:41 KST
- Actual Duration: 24분 15초
- Estimated Duration: 약 90분
- Variance: 예상보다 65분 45초 짧음
@@ -0,0 +1,147 @@
# SW Header 레이어 Dropdown 검증 기록 (2026-10-16)
> 범위: `@hm-design/sw` 레이어 필드 Dropdown과 전구·눈꽃·자물쇠 On/Off. 기준: Figma `rj6wpsfk24lOWvWiVRAANP` 5개 Node. 규격은 [디자인 명세 §5.2](./sw-header-design-spec.md#52-레이어-dropdown-figma-headermenu레이어-2610940-2026-10-16), 사용법은 [사용 가이드](./sw-header-usage.md#레이어-dropdown-예)를 따릅니다.
## 1. 작업 환경
| 항목 | 값 |
|---|---|
| Worktree / Branch | `D:\디자인시스템\HMDesign-SW` / `feature/sw-package` |
| HEAD | `9407c85` (시작·종료 동일, Commit 없음) |
| 시작 상태 | `M package.json`, `M package-lock.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/` — 기존 미커밋·Untracked 파일 보존(시작 시 스크래치 백업, Reset/Clean/Checkout 없음) |
| 검증 Storybook | `npx storybook dev -p 6047`(6024·6031·6032는 다른 세션 사용 중) |
## 2. Figma 5개 Node
| # | Node | 내용 | 확인 방법 |
|---|---|---|---|
| ① | `26:10940` `header/menu/레이어` (default 26:10941, hover 26:10952, Variant3 26:10963) | 필드·펼침 목록·레이어 상세 | `get_design_context` + screenshot |
| ② | `26:10810` `icon_전구` | 4 Variant: 컬러 / 컬러 Hover(검정 25%) / 회색(ellipse `#E6E6E6→#555`) / 회색 Hover | `get_design_context` + screenshot |
| ③ | `26:10855` `icon_눈꽃` | 4 Variant SVG(`#00CCFF` / `#949494`, Hover = 검정 25% 타일 추가) | 동일 |
| ④ | `26:10912` `icon_자물쇠` | 4 Variant SVG(`#E4BF4D·#D8A300` / `#BEBEBE·#A1A1A1`, Hover = 타일 추가) | 동일 |
| ⑤ | `26:10687` `header/dropdown/brown/row` (Default 26:10688, Hover 26:10695) | 행 234 × 30, Hover `#62533D` / `#FF5C00` | 동일 |
Prototype의 Smart Animate(Off → Off Hover, ease-out 300ms)는 사용자 요청서 기재값을 따랐습니다. Figma Prototype player 재생으로는 확인하지 않았습니다(NOT VERIFIED).
## 3. 자산
- 신규 8개(`layer-ellipse-off`, `layer-freeze-hover/off/off-hover`, `layer-lock-hover/off/off-hover`, `layer-detail`)를 Figma MCP에서 내려받아 `packages/sw/src/assets/`에 저장. 수정·재작성 없음.
- 기존 `layer-union/ellipse/state-a/state-b`는 Figma 원본과 mask id 외 동일 → 재사용.
- Hover SVG = Default SVG + `<rect 20×20 rx2 fill=black 25%>` 1줄 외 동일(정규화 diff).
- `packages/sw/src`, `src/sw`, `packages/sw/dist`, `storybook-static`에서 `figma.com/api` 검색 0건.
## 4. 디자인 측정 (Storybook `SW / Header / Layer Dropdown`, 1920 프레임, `getBoundingClientRect`·computed style)
| 항목 | 기대(Figma) | 측정 | 판정 |
|---|---|---|---|
| 필드 | 234 × 38, x 467 | 234 × 38, x 466.7(헤더 기준) | MATCH |
| 펼친 필드 | 테두리 `#081813`, radius 3 3 0 0, 라벨·값 `#FFC600` | `rgb(8,24,19)`, `3px 3px 0 0`, `rgb(255,198,0)` | MATCH |
| 펼침 외곽선 | 236 × 320 at −1/−1, radius 4 | `::before` inset −39 / −1 / −1 / −1, radius 4, 노란 gradient | MATCH |
| 목록 | 234 × 280, 필드 바로 아래 | 234 × 280, y = 필드 하단 | MATCH |
| 배경·그림자·radius | `#27241D`, `0 4px 8px rgba(16,48,38,.25)`, 0 0 3 3 | 동일 | MATCH |
| 행 | 234 × 30 × 8 | 234 × 30, 8개 | MATCH |
| 아이콘 | 20 × 20, x+12 / 32 / 52 | 동일 | MATCH |
| 견본 | 16 × 16 at x+80, 안쪽 14 × 14 | 동일 | MATCH |
| 이름 | x+104(+1), `#F6F4F2`, 700 | 동일 | MATCH |
| 하단 | 40, 버튼 오른쪽 12px | 40, 버튼 오른쪽 끝 = 목록 −12 | MATCH |
| 레이어 상세 | radius 24, 배경 `rgba(197,177,158,.15)`, 아이콘 14, 글자 opacity .35 | 동일, 버튼 107.3 × 24(글자 69.3) | MATCH(글자 폭은 글꼴 차이) |
| 행 Hover | `#62533D` / `#FF5C00` | `rgb(98,83,61)` / `rgb(255,92,0)`(고정·실제 Hover 모두) | MATCH |
| 아이콘 Hover | 전구 검정 25% 배경, 눈꽃·자물쇠 Hover SVG | 전구 `rgba(0,0,0,.25)`, Hover img opacity 1 | MATCH |
| Off Hover 전환 | 300ms ease-out | Off: `0.3s ease-out`, On: 0s | MATCH |
| 헤더 높이 | 49 | 49 | MATCH |
3배 확대 아이콘 표(12개 Variant)를 Figma screenshot과 육안 비교: 형태·색 일치. 픽셀 diff는 수행하지 않았습니다.
## 5. 동작 검증
JS 이벤트(`click`, `PointerEvent`, `KeyboardEvent`)와 실제 마우스·키보드 입력(브라우저 pane `left_click`, `hover`, `key`)으로 확인했습니다.
| 시나리오 | 결과 |
|---|---|
| 필드 클릭 → 열림, 다시 클릭 → 닫힘 | PASS (`aria-expanded` 동기화, `onLayerMenuOpenChange` true/false) |
| 전구 / 눈꽃 / 자물쇠 클릭(JS 4회, 실제 마우스 1회, 키보드 Space·Enter 2회) | PASS — 각 Callback 1회, `onLayerSelect` 0회, 목록 유지, 해당 레이어만 변경(상태표 확인) |
| 행 선택(JS, 실제 Enter) | PASS — `onLayerSelect` 1회, 목록 닫힘, Focus 필드 복귀, 필드에 레이어명·상태 아이콘 반영(회색 전구 확인) |
| 바깥 pointerdown, 다른 필드 pointerdown | PASS — 닫힘 |
| Escape(JS, 실제 키) | PASS — 닫힘, Focus 필드 |
| ↓(닫힘) | PASS — 열림, 현재 레이어 이름 버튼 Focus(2px `#F0F7F5` outline) |
| ↓↑ / ←→ | PASS — 같은 열 유지 행 이동, 행 안 이동 |
| Tab 진입 / Shift+Tab 두 번으로 이탈 | PASS — 목록 안에서는 유지, 밖으로 나가면 닫힘 |
| 레이어 상세(Callback 있음) | PASS — `onLayerDetail` 1회 후 닫힘 |
| 레이어 상세(Callback 없음) | PASS — `disabled`, opacity 1(Figma 외형), cursor default |
| Callback 없는 아이콘 | PASS — `role="img"` span 24개, 버튼 0개 |
| disabled 레이어 | PASS — 아이콘 3개·선택 버튼 모두 disabled |
| 20개 레이어 | PASS — 목록 240px 내부 스크롤(scrollHeight 600), 전체 280, 헤더 가로 스크롤 0 |
| 레이어 없음 | PASS — 하단만(40px), 필드는 `layer.name` |
| 긴 레이어명 | PASS — 행·필드 ellipsis, 오른쪽 padding 12 유지 |
| 콘솔 오류 | 0건 |
## 6. 반응형
`Layer Dropdown`의 Callback 예제 프레임 폭을 바꿔 측정하고, 같은 폭의 기존 `Responsive` Story(`layers` 없음) 필드 위치와 비교했습니다.
| 폭 | 필드 x / 폭 (기존과 동일) | 열린 뒤 필드 이동 | 헤더 가로 스크롤 | 목록 헤더 안 | 목록 이동 | 아이콘·텍스트 겹침 | 견본 |
|---|---|---|---|---|---|---|---|
| 1920 | 466.7 / 234 ✔ | 없음 | 0 | ✔ | 0 | 없음 | 16 × 16 |
| 1800 | 346.7 / 234 ✔ | 없음 | 0 | ✔ | 0 | 없음 | 16 × 16 |
| 1799 | 466.7 / 234 ✔ | 없음 | 0 | ✔ | 0 | 없음 | 16 × 16 |
| 1440 | 466.7 / 234 ✔ | 없음 | 0 | ✔ | 0 | 없음 | 16 × 16 |
| 1180 | 306.7 / 234 ✔ | 없음 | 0 | ✔ | 0 | 없음 | 16 × 16 |
| 1080 | 207.7 / 234 ✔ | 없음 | 0 | ✔ | 0 | 없음 | 16 × 16 |
| 900 | 246.7 / 234 ✔ | 없음 | 0 | ✔ | 0 | 없음 | 16 × 16 |
| 768 | 310.7 / 234 ✔ | 없음 | 0 | ✔ | 0 | 없음 | 16 × 16 |
| 640 | 182.7 / 234 ✔ | 없음 | 0 | ✔ | 0 | 없음 | 16 × 16 |
| 390 | 167.7 / 113.7 ✔ | 없음 | 0 | ✔ | 왼쪽 이동, 오른쪽 여백 1.3 | 없음 | 16 × 16 |
- 1차 측정에서 390px 열림 상태에 헤더 `scrollWidth`가 1px 늘어나는 문제(외곽선이 목록 밖 1px)를 발견해, 이동 계산에 1px 여백을 두도록 수정 후 320 / 360 / 390 / 500 / 519 / 640에서 0을 재확인했습니다.
- 390에서 실제 조작(열기 + 자물쇠 Toggle → Callback 1회, 목록 유지) 확인. 브라우저 Viewport 390 에뮬레이션은 Storybook 페이지 내부 폭이 766px로 잡혀 헤더 프레임 390 기준으로만 확인했습니다(실기기 NOT VERIFIED).
- 더보기 메뉴 정책·기존 Breakpoint 변경 없음(필드 위치가 기존 Story와 동일).
## 7. 회귀 확인
| 항목 | 결과 |
|---|---|
| `layers` 미전달 필드 | 기존 DOM(아이콘 4개 img 14×18 / 14×14 / 20×20 / 20×20, `aria-expanded=false`)과 동일 |
| Composed Controls 고정 Hover | 5개 필드 값 `#FFC600`, Gradient ring opacity 1, 선 미리보기 `#FFC600` 유지 |
| 문서 Dropdown·창 이동·인쇄·최소화·종료·속성 토글 CSS | 변경 없음(diff는 레이어 관련 규칙 추가뿐) |
## 8. Build
| 명령 | 결과 |
|---|---|
| `npm run typecheck -w @hm-design/sw` | PASS |
| `npm run build:sw` | PASS (`dist/styles.css` 45.35 kB, `dist/index.js` 202.28 kB) |
| `npm run typecheck`(root, packages build 포함) | PASS |
| `npm run build`(root) | PASS (기존 500 kB chunk 경고만) |
| `npm run build-storybook` | PASS, index에 `sw-header--layer-dropdown` |
## 9. 아이콘 색상 ↔ 상태 매핑 확정 검증 (사용자 확정, 2026-10-16)
확정 기준: 전구 컬러 `visible=true` / 회색 `false`, 눈꽃 컬러 `frozen=false`(동결 해제) / 회색 `true`(동결), 자물쇠 컬러 `locked=false`(잠금 해제) / 회색 `true`(잠금). 클릭 시 해당 boolean 반전값을 Callback으로 전달합니다. 기존 구현(`layerIconOn()`, `onChange(layer, !pressed)`)이 이 기준과 같아 동작 코드는 바꾸지 않았고, 타입 주석·Story 설명·문서의 잠정 표기만 확정으로 바꿨습니다. SVG·Default/Hover CSS 변경 없음.
검증: Storybook `Layer Dropdown`의 Callback 예제에서 각 아이콘을 두 번씩 클릭했습니다. 색은 렌더링된 SVG의 실제 fill(전구 `#FAB846`/`#9C9C9C`, 눈꽃 `#00CCFF`/`#949494`, 자물쇠 `#E4BF4D`/`#BEBEBE`)로 판정했습니다.
| 레이어 · 아이콘 | 클릭 전 | Callback | 클릭 후 |
|---|---|---|---|
| layer-0 전구 | `visible=true` 컬러 | `onLayerVisibilityChange(layer-0, false)` | `false` 회색 |
| layer-0 전구 | `false` 회색 | `(layer-0, true)` | `true` 컬러 |
| layer-0 눈꽃 | `frozen=false` 컬러 | `onLayerFreezeChange(layer-0, true)` | `true` 회색 |
| layer-0 눈꽃 | `true` 회색 | `(layer-0, false)` | `false` 컬러 |
| layer-0 자물쇠 | `locked=false` 컬러 | `onLayerLockChange(layer-0, true)` | `true` 회색 |
| layer-0 자물쇠 | `true` 회색 | `(layer-0, false)` | `false` 컬러 |
| layer-5 전구 | `visible=false` 회색 | `onLayerVisibilityChange(layer-5, true)` | `true` 컬러 |
| layer-5 전구 | `true` 컬러 | `(layer-5, false)` | `false` 회색 |
| layer-5 눈꽃 | `frozen=true` 회색 | `onLayerFreezeChange(layer-5, false)` | `false` 컬러 |
| layer-5 눈꽃 | `false` 컬러 | `(layer-5, true)` | `true` 회색 |
| layer-5 자물쇠 | `locked=true` 회색 | `onLayerLockChange(layer-5, false)` | `false` 컬러 |
| layer-5 자물쇠 | `false` 컬러 | `(layer-5, true)` | `true` 회색 |
- 모든 클릭에서 Callback 1회, `onLayerSelect` 0회, 목록 유지. `aria-pressed`는 표시됨 / 동결됨 / 잠김과 일치.
- 클릭하지 않은 다른 레이어 7개의 상태는 변하지 않음.
## 10. 남은 차이 · NOT VERIFIED
- 아이콘 색상 ↔ 상태 매핑은 위 §9로 확정·검증했습니다. 실제 SW 호스트에서 이 값으로 도면이 표시·동결·잠금되는지는 외부 연동이 필요합니다(NOT VERIFIED).
- 실제 SW 호스트에서의 레이어 표시·동결·잠금 반영과 레이어 상세 화면: 패키지 단독 검증 불가(NOT VERIFIED).
- 현재 레이어 이름 `#FFC600`, 목록 최대 높이 240px, 좁은 폭 왼쪽 이동은 Figma에 없는 구현값입니다.
- 레이어 상세 버튼 Hover는 Figma Variant가 없어 추가하지 않았습니다.
- Figma Smart Animate 곡선의 Prototype player 실재생 비교, 픽셀 diff, 실기기 390px, 스크린리더 실사용 검증은 수행하지 않았습니다.
@@ -0,0 +1,94 @@
# SW Header — 1400ms 및 Composed Controls Hover 통합 검증
## 1. 최종 판정: PARTIAL
요청 범위의 Chrome 브라우저 통합 검사 170개와 이웃 필드 영향 검사 5개, 픽셀 비교, Root Typecheck / SW Build / Root Build / Storybook Build는 PASS입니다. 실제 네이티브 호스트 창 제어·다른 브라우저·보조기술·독립 SW Guard coverage는 NOT VERIFIED이므로 전체 PASS로 확대하지 않습니다.
## 2. 작업 시작 시 Git 상태
- 경로 `D:\디자인시스템\HMDesign-SW`, branch `feature/sw-package`.
- HEAD `9407c8567f57301659e591c03451cca1b828733c`.
- 기존 변경 ` M package-lock.json`, ` M package.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`.
- 기존 파일 SHA-256과 수정 전 CSS·필드 TSX·Story 사본을 `_local/sw-speed-hover-qa/`에 보존했습니다. 미추적 파일을 삭제·초기화하지 않았습니다.
## 3. 창 이동 Motion 변경 전후
전체 1640 → **1400ms**, Tooltip 520 → **약 443.90ms**, 이동 완료 1040 → **약 887.80ms**, 완료 후 유지 600 → **약 512.20ms**입니다. 모든 구간은 1400/1640(35/41)로 동일하게 단축했습니다. 주기는 약 14.63% 짧아지고 재생 속도는 약 17.14% 빨라집니다. 원본 GIF와 다른 사용자 요청 사항입니다.
세 CSS animation duration만 변경했습니다. 실제 Hover에서 1.4초와 2.8초 iteration을 확인했고, 이탈 시 원래 SVG로 복귀합니다. Drag 중 paused/Tooltip 숨김, 호스트 Pointer Capture 이벤트 전달, Reduced Motion의 원래 아이콘·정적 Tooltip도 통과했습니다.
## 4. 이동 경로 보존
세 `@keyframes`의 좌표·백분율·보간 선언은 수정 전과 바이트 단위로 동일합니다. 이전 41개 브라우저 프레임을 시간 × 35/41에 맞춰 다시 측정했으며 창·커서·Tooltip의 상대 위치 차이는 **최대 0px**였습니다. 따라서 오른쪽 28px / 위쪽 4.5px, 커서 상승·등장 순서·가속/감속 비율은 보존합니다. GIF와의 공간 오차 기록을 현재 시간까지 같다는 뜻으로 해석하지 않습니다.
## 5. Composed Controls Hover 대상
공용 레이어·색상·선스타일·문자스타일·치수스타일 필드의 버튼 전체 Hover에 적용했습니다. 동일 컴포넌트를 사용하는 Full Header에도 같은 규칙을 적용합니다. Composed Controls의 기존 레이어·색상·상세보기 예제는 보존하고 기존 공개 선/문자/치수 컴포넌트와 5개 필드의 고정 Hover 비교를 추가했습니다. 새 Story나 새 공개 API는 없습니다.
상세보기는 이번 필드 강조에서 제외합니다. `속성Bar`/`속성창`의 텍스트 전용 Hover, caption·검정 divider·기존 선택 토글·배경을 유지하고 Hover Border를 추가하지 않았습니다.
## 6. 텍스트 · 선 · 아이콘 색상
- PASS: 각 필드 버튼의 빈 모서리 부분에 Hover해도 모든 라벨·값이 `rgb(255,198,0)`입니다. 선스타일의 가중치·스케일, 폰트 이름 텍스트도 포함합니다.
- PASS: 선 미리보기 5개 조각의 채움만 노란색, 1px 검정 border와 형태·위치 유지.
- PASS: 문자 미리보기는 기존 텍스트이며 색만 변경. 원래 별도 검정 Stroke/그림자가 없으므로 새로운 외곽선을 추가하지 않았습니다.
- PASS: 치수 Hover SVG는 foreground path의 흰 채움 1곳만 `#FFC600`으로 변경. mask용 흰 rect와 검정 외곽선 path·참조·좌표는 원본과 동일합니다. 60 × 15px wrapper에서 원본/Variant visibility만 교대하며 CSS filter는 없습니다.
- PASS: 색상 견본의 실제 값 `#ffe500`, chip border, 레이어 상태 SVG·막대 색 유지. 표시 문자열·실제 값은 변경되지 않습니다.
- PASS: Default / Selected / Disabled / Disabled+Selected × 5개 필드에서 Hover·이탈·콜백을 확인. Disabled는 새 Hover 효과가 없고 Selected의 기존 색은 유지됩니다. 선택 단독 상태의 미리보기 색을 확장하지 않았습니다.
## 7. 검정 외곽선 · 기존 Hover Border
기존 노란 Gradient `::after`의 선언은 그대로이며 enabled Hover에서는 opacity 1, Disabled에서는 0입니다. 검정 선 border·치수 mask는 유지합니다. 4배 브라우저 캡처에서 5개 필드 모두 Default와 Hover의 순수 검정 픽셀 위치 집합이 동일했습니다. 선 미리보기 필드는 1920개, 치수 필드는 2903개의 검정 픽셀이 유지됐습니다. 문자 필드는 원래 별도 검정 픽셀이 0개였습니다.
5개 Default 화면은 수정 전과 후의 PNG 픽셀이 모두 동일했습니다. Hover 전후 버튼·텍스트·미리보기의 크기/위치·배경·그림자가 같았고, 5개 이웃 영향 검사에서도 Hover한 필드 외 텍스트 색은 변하지 않았습니다.
## 8. Storybook 시각 검증
실제 빌드된 `SW / Header / Window Move Motion`과 `Composed Controls`를 Chrome `155.0.8059.79`에서 열었습니다. 속도 보정된 9개 고정 프레임이 실제 1400ms CSS animation을 pause하고, 반복 경계에서 문구가 숨겨지는 것을 확인했습니다. Composed의 실제 세 스타일 필드와 5개 고정 Hover, 기존 속성창 클릭·선택 상태를 확인했습니다. 비교 PNG와 Story 전체 캡처도 시각 검토했습니다.
이는 실제 공용 React 컴포넌트 결과이며 별도 HTML/CSS 모방 화면이 아닙니다. 개발 서버 프로세스나 포트를 변경하지 않고 임시 localhost 서버에서 검사 후 종료했습니다.
## 9. 반응형 · Callback 회귀
요청 폭 1920 / 1800 / 1440 / 1180 / 1080 / 900 / 768 / 640 / 390px와 경계 1079 / 639px를 추가 확인했습니다. 모두 Header 높이 49px, Header/문서 가로 overflow 0px이며 기존 auxiliary 필드·속성·색상·인쇄/가이드의 More 이동 정책이 유지됐습니다. 이동 영역도 88 / 24 / 숨김 그대로입니다.
일반/선택 필드 클릭과 Enter 콜백, Disabled 클릭 차단, Full Header 5개 필드 콜백, More로 이동한 선스타일 콜백·메뉴 닫힘, 상세보기 선택 토글, 최소화·종료 콜백을 확인했습니다. Drag의 원래 div 이벤트와 호스트 모의 Pointer Capture·move/up도 통과했습니다. 실제 OS 창 제어는 NOT VERIFIED입니다.
## 10. Typecheck · Build · Storybook · Guard
- PASS: `npm.cmd run typecheck`. 기존 pretypecheck의 theme 확인·workspace package build와 SW Build도 포함합니다.
- PASS: SW package build(`build:sw`, 위 prerequisite 포함), `npm.cmd --ignore-scripts run build`, `npm.cmd --ignore-scripts run build-storybook`. 이미 통과한 prerequisite은 반복하지 않았습니다.
- PASS: 브라우저 통합 조건 170개, 이웃 영향 5개, 5개 Default 픽셀 보존과 검정 픽셀 비교. pageerror 0건.
- 기존 빌드 경고: 문서 예시 `bg-[var(...)]`, Storybook `:host-context` minifier, 500kB 초과 bundle와 plugin timing 안내. exit 0이며 범위 밖 코드를 수정하지 않았습니다.
- Guard 1회: `node scripts/ai-generation-guard.mjs --target . --profile STORYBOOK --scope PARTIAL --files src/sw/sw-header.stories.tsx,packages/sw/src/theme/sw.css,packages/sw/src/components/sw-header/components/SWHeaderFields.tsx --json`. 원본 status/staticStatus는 NOT VERIFIED, FAIL 없음. SW styles 연결 `ENTRY-002`, 패키지 구현 graph 밖 `SCOPE-001`, 연결에 의존한 Token/동적 값 미검증이 남았습니다. Guard를 수정하거나 결과를 PASS로 덮어쓰지 않았습니다.
- Guard styleEvidence: STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED, checks=[]. 독립 SW 실검사 완료라는 의미가 아닙니다. 별도 Runtime/사용자 디자인 의도는 위 검사 범위 PASS, 전체 검증은 PARTIAL입니다.
## 11. 변경 파일 목록
1. `packages/sw/src/theme/sw.css` — 1400ms 및 필드 Hover.
2. `packages/sw/src/components/sw-header/components/SWHeaderFields.tsx` — 치수 원본/Variant의 동일 크기 wrapper.
3. `packages/sw/src/assets/menu-dimension-ico-hover.svg` — 신규 foreground 색 Variant.
4. `src/sw/sw-header.stories.tsx` — 기존 모션·Composed Story 보완.
5. `docs/sw/sw-header-design-spec.md` — 현재 §11.5 및 기존 정책 정합.
6. `docs/sw/sw-header-usage.md` — 현재 시간·필드 Hover·Story 사용법.
7. `docs/sw/sw-header-window-motion-gif-verification-2026-10-16.md` — 과거 내용 뒤 후속 기록만 추가.
8. `packages/sw/README.md` — 현재 동작과 링크.
9. 이 검증 기록 — 신규 문서.
Git 무시 경로 `_local/sw-speed-hover-qa/`에 시작 해시·사본, 전/후 Default/Hover PNG, `pixel-comparison.json`, `composed-hover-comparison.png`, Story·폭별 PNG, `verification.json`, `neighbors.json`, 원본 `guard.json`, 빌드 로그와 `final-summary.json`을 보존합니다.
## 12. 기존 디자인 보존 근거
수정한 필드 블록·duration·주석을 원래 값으로 복원한 임시 문자열이 시작 CSS 전체와 동일합니다. 따라서 상세보기·홈·문서·인쇄/가이드·최소화/종료·반응형 CSS는 바이트 단위로 보존됐습니다. 필드 TSX도 치수 wrapper/import 외 문자열이 동일하며 기존 Callback·Props를 변경하지 않았습니다. 다른 Story, 기존 SVG, `SWHeaderWindowControls.tsx`, root package.json/Lockfile은 시작 상태를 유지합니다.
과거 GIF 검증 문서는 시작 사본이 현재 문서의 정확한 prefix임을 확인했습니다(append-only). 다른 패키지 소스·Worktree·DWG/DXF Viewer는 수정하지 않았습니다. Root의 기존 package build에 따른 Git 무시 dist 재생성만 있습니다. 의존성 설치·공개 API 변경은 없습니다.
## 13. 남은 차이 · NOT VERIFIED
원본 GIF와 시간만 다릅니다(사용자 요청). 기존 Tooltip의 최소화/종료 일부 시각적 겹침은 유지하며 포인터를 차단하지 않습니다. 실제 네이티브 창 제어, 다른 브라우저·터치/펜·보조기술, 외부 CSS 커스터마이징, 독립 SW Guard coverage는 미검증입니다. 현재 Chrome 기본 테마 범위의 PASS를 전체 플랫폼 승인으로 확대하지 않습니다.
## 14. 작업 종료 시 Git 상태
`feature/sw-package`, HEAD `9407c8567f57301659e591c03451cca1b828733c` 유지. status 그룹도 ` M package-lock.json`, ` M package.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`를 유지하며 실제 이번 변경은 해당 미추적 그룹 내부에 있습니다. Index 변경·Commit·Push·PR·Merge·Publish·배포는 수행하지 않았습니다.
작업 시간(KST): Started At 2026-10-16 13:54:54 / Completed At 2026-10-16 14:08:24 / Actual Duration 13분 30초 / Estimated Duration 15~25분 / Variance 15분 대비 −1분 30초, 25분 대비 −11분 30초.
+626
View File
@@ -0,0 +1,626 @@
# SW Header 사용 가이드
> **상세 버튼 최신 기준(2026-10-16):** 레이어·색상·선·문자·치수 상세는 문자스타일 디자인의 `SWHeaderMenuDetail`을 공통 사용합니다. `on*Detail` 미전달 시 Hover 유지·클릭 무동작이며, Callback 전달 시 기존 1회 호출·닫힘·Focus 복귀를 유지합니다. 내부 공통 컴포넌트는 `label`, `icon`, `onClick`, `disabled`, `className`, `aria-label` 등 native 버튼 Props를 지원합니다. 새 공개 export는 추가하지 않았습니다. 비교 Story는 `SW/Header → Detail Buttons (Shared States)`(verification 태그, 직접 ID `sw-header--detail-buttons`)입니다. 과거의 Callback 미전달=Disabled 및 치수만 밝은 Hover 설명보다 이 기준이 우선합니다. [이번 검증 기록](./sw-header-detail-buttons-verification-2026-10-16.md).
> **3차 기준(2026-10-16):** 로고 외 개별 제거 `hiddenControls`, 유사 메뉴 순서 `menuOrder`, 속성창 4상태 Story·비교, 형상 Slot `shapeContent`를 추가했습니다. [3차 절](#구성요소-표시메뉴-순서속성창-4상태형상-slot-2026-10-16-3차)과 [3차 검증 기록](./sw-header-composition-shape-verification-2026-10-16.md)이 같은 주제의 이전 설명보다 우선합니다.
> **후속 5개 요구사항 기준(2026-10-16):** `SWHeaderPropertyBar`, 메뉴 단위 `controls`, `btn_colorize` Default/Hover를 추가했습니다. 상세 pill의 `solid-stroke-width`도 제거했습니다. 아래 후속 연결 절과 [새 검증 기록](./sw-header-followup-five-verification-2026-10-16.md)이 현재 기준입니다. 과거 실행 기록은 보존합니다.
> **최신 API/동작:** 아래 [속성창·K 가이드 추가 연결](#속성창k-가이드-추가-연결-2026-10-16)을 먼저 확인하세요. 색상 기준은 정확히 하나 표시되며, 상세 버튼 Hover와 목록 Selected 정책이 변경됐습니다. 과거 예시의 `colorMode=null`은 직접 색상 의미를 유지하지만 모드 표시까지 해제하지 않습니다.
`@hm-design/sw`의 SW Header를 화면에 연결하는 방법입니다. 디자인 값과 판정은 [디자인 명세](./sw-header-design-spec.md)를 따릅니다.
> 패키지는 아직 npm Registry에 게시하지 않았습니다. 아래 설치 방법은 HMDesign Workspace 또는 로컬 tarball 기준입니다.
## 1. 패키지 구조
```text
packages/sw/
├── src/
│ ├── assets/ Figma 원본 SVG와 Hover Variant (build 시 data URI로 포함, 로고 제외)
│ ├── components/
│ │ ├── sw-header/
│ │ │ ├── SWHeader.tsx 전체 헤더
│ │ │ ├── components/ 하위 컨트롤 (공개 + 내부)
│ │ │ ├── types.ts Props / 값 타입
│ │ │ └── index.ts
│ │ └── sw-theme/SWTheme.tsx 하위 컨트롤 단독 사용 시 토큰 범위
│ ├── theme/sw.css SW 전용 토큰·스타일
│ └── index.ts
├── build.mjs 독립 library build (Vite lib + tsc 선언)
├── package.json exports: "." , "./styles.css"
└── README.md
```
`@hm-design/ui`·`@hm-design/center-graphic`·OrgChart 패키지를 import하지 않습니다. Peer는 `react`·`react-dom` `^19.0.0`뿐입니다.
## 2. 설치
### HMDesign Workspace 안
루트 `package.json`의 Workspace(`packages/*`)에 포함되어 있습니다.
```bash
npm install
npm run build:sw
```
`build:packages`(typecheck·build·Storybook 전처리)도 마지막에 `build:sw`를 실행합니다.
### 다른 React 프로젝트(로컬 tarball)
```bash
npm pack -w @hm-design/sw --pack-destination ../packs
```
```bash
npm install ../packs/hm-design-sw-0.1.0.tgz
```
소비 프로젝트는 `src` 내부 파일을 참조하지 않고 `dist` 공개 API만 사용합니다.
## 3. Import
```tsx
import { SWHeader } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
```
- CSS는 JS에서 자동 import되지 않으므로 앱 진입점 등에서 한 번 import합니다.
- TypeScript 7에서 CSS side-effect import 타입 오류가 나면 Vite 프로젝트의 `/// <reference types="vite/client" />`(또는 `declare module "*.css"`)를 둡니다.
- 글꼴: 패키지는 폰트 파일을 포함하지 않습니다. 앱에서 Noto Sans KR(400/700)을 로드합니다.
## 4. 공개 API
| Export | 용도 |
|---|---|
| `SWHeader` | 전체 헤더. `ref`는 `<header>` |
| `SWHeaderHome` | 홈 버튼(로고 영역). `logoSrc` / `logo` / `logoAlt` / `onClick` |
| `SWHeaderDocumentTitle` | 파일명 버튼. `documents`를 주면 문서 Dropdown(`title`, `onClick`, `documents`, `menuOpen`, `onMenuOpenChange`, `onDocumentSelect`, `onNew`, `onOpen`, `documentIcon`, `newLabel`, `openLabel`) |
| `SWHeaderLayerControl`, `SWHeaderColorControl`, `SWHeaderLineStyle`, `SWHeaderTextStyle`, `SWHeaderDimensionStyle` | 속성 필드 5종. `SWHeaderLayerControl`은 선택 `state`(현재 레이어의 표시·동결·잠금)로 필드 아이콘을 회색으로 표시 |
| `SWHeaderLayerMenu` | 레이어 필드 + 레이어 Dropdown(`value`, `layers`, `activeLayerId`, `open`, `onOpenChange`, `onClick`, `onLayerSelect`, `onVisibilityChange`, `onFreezeChange`, `onLockChange`, `onDetail`). `SWHeader`는 `layers`가 있을 때 이것을 사용 |
| `SWHeaderColorMenu` | 색상 필드 + 색상 Dropdown(2026-10-16, `value`, `colors`, `userColors`, `mode`, `open`, `onOpenChange`, `onClick`, `onColorSelect`, `onModeChange`, `onColorInput`, `onPick`, `onDetail`). `SWHeader`는 `colors`가 있을 때 사용 |
| `SWHeaderLineStyleMenu` | 선스타일 필드 + 3열 Dropdown(`value`, `lineTypes`, `lineWeights`, `activeLineTypeId`, `activeLineWeightId`, `onLineTypeSelect`, `onLineWeightSelect`, `onLineWeightValueChange`, `onScaleChange`, `onDetail`, 열림 Props). `SWHeader`는 `lineTypes` 또는 `lineWeights`가 있을 때 사용 |
| `SWHeaderTextStyleMenu`, `SWHeaderDimensionStyleMenu` | 문자·치수스타일 필드 + 목록 Dropdown(`value`, `textStyles` / `dimensionStyles`, `active*Id`, `on*Select`, `onDetail`, 열림 Props) |
| `SWHeaderLinePreview`, `swHeaderLinePatterns` | 70 × 2 선 미리보기와 Figma CENTER(22·7·22·7) / HIDDEN(33·33) 패턴 |
| `SWHeaderInput` | 수정 가능한 SW Input(Figma input 26:11656 · cursor 26:11664). 레이어 상세 등 상세 영역의 값 편집용 |
| `parseSWHeaderColor`, `parseSWHeaderNumber` | Input 유효성 검사 도우미(`r,g,b` / `#rrggbb`, 소수) |
| `SWHeaderProperties` | 상세보기(속성Bar / 속성창) |
| `SWHeaderActions` | 인쇄 · 팝업 가이드 |
| `SWHeaderWindowControls` | 창 이동 · 최소화 · 종료 |
| `SWTheme` | 하위 컨트롤을 헤더 밖에서 쓸 때 토큰·글꼴·버튼 초기화 범위 |
| 타입 | `SWHeaderProps`, `SWHeaderMenuId`, `SWHeaderLayerValue`, `SWHeaderLayer`, `SWHeaderColorValue`, `SWHeaderLineStyleValue`, `SWHeaderTextStyleValue`, `SWHeaderDimensionStyleValue`, `SWHeaderDocument`, `SWHeaderColorMode`, `SWHeaderColorOption`, `SWHeaderColorInputValue`, `SWHeaderLineType`, `SWHeaderLineWeight`, `SWHeaderTextStyleOption`, `SWHeaderDimensionStyleOption`, `SWHeaderInputValueType`, 각 하위 컴포넌트 `*Props`, `SWThemeProps` |
내부 구현(`SWHeaderField`, `SWHeaderMore`, `useSWHeaderDropdown`, `SWHeaderMenuDetail`)은 공개하지 않습니다.
## 5. SWHeader Props
`HTMLAttributes<HTMLElement>`(단 `title`, `children`, `color` 제외)를 함께 받습니다. `className`·`style`·`aria-*`는 `<header>`에 전달됩니다.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `documentTitle` | `string` | 필수 | 파일명. 좁으면 말줄임, 전체 이름은 `title` 툴팁 |
| `drawingModeLabel` | `string` | `"그리기모드"` | 그리기 모드 라벨 |
| `layer` | `{ name }` | `{ name: "0" }` | 현재 레이어(`layers`에 `activeLayerId`와 같은 항목이 없을 때 필드에 표시) |
| `layers` | `SWHeaderLayer[]` | — | 레이어 필드 아래 Dropdown 목록(배열 순서 그대로). 없으면 Dropdown 없이 기존 필드 버튼, `[]`이면 하단(레이어 상세)만 |
| `activeLayerId` | `string` | — | 현재 레이어. 필드에 이름·상태 아이콘 표시, 목록에서 이름만 `#FFC600` |
| `layerMenuOpen` | `boolean` | — | 레이어 Dropdown 열림 상태(제어형). 없으면 내부에서 관리 |
| `color` | `{ value, swatch? }` | `{ value: "ByLayer", swatch: "#ffffff" }` | 색상 값 표시와 칩 색 |
| `lineStyle` | `{ name, weight, scale }` | `ByLayer / ByLayer / 1.00` | 선스타일 |
| `textStyle` | `{ font, name }` | `Arial / Standard` | 문자스타일 |
| `dimensionStyle` | `{ name }` | `Standard` | 치수스타일 |
| `activeMenu` | `"layer" \| "color" \| "lineStyle" \| "textStyle" \| "dimensionStyle" \| null` | `null` | 선택(열림) 표시 |
| `propertyBarOpen` | `boolean` | `false` | 속성Bar 눌림 |
| `propertyPanelOpen` | `boolean` | `false` | 속성창 눌림 |
| `disabled` | `boolean` | `false` | 메뉴·속성·인쇄/가이드 비활성(창 제어 제외) |
| `tools` | `ReactNode` | — | 객체 수정 영역의 필드 뒤 추가 도구 |
| `logoSrc` | `string` | — | 로고 이미지 URL. 32 × 17 로고 영역에 `contain`으로 맞춤 |
| `logo` | `ReactNode` | — | 로고 요소(인라인 SVG 등). `logoSrc`보다 우선 |
| `logoAlt` | `string` | — | 제품명. 홈 버튼 이름이 "<logoAlt> 홈"(없으면 "홈") |
| `documents` | `SWHeaderDocument[]` | — | 파일명 아래 문서 Dropdown 목록. 없으면 Dropdown 없이 기존 버튼, `[]`이면 New / Open만 |
| `documentMenuOpen` | `boolean` | — | 문서 Dropdown 열림 상태(제어형). 없으면 내부에서 관리 |
| `activeDocumentId` | `string` | — | 현재 문서. 목록 순서는 그대로 두고 이 문서의 이름만 `#FFC600` |
| `documentIcon` | `ReactNode` | Figma 문서 아이콘 | `icon`이 없는 문서의 아이콘(29 × 12 영역) |
| `documentNewLabel` / `documentOpenLabel` | `string` | `+ New` / `Open` | 하단 버튼 글자 |
| `colors` | `SWHeaderColorOption[]` | — | 색상 Dropdown 기본 팔레트(2026-10-16). 없으면 Dropdown 없이 기존 필드 버튼 |
| `userColors` | `SWHeaderColorOption[]` | — | 사용자 색상(개수 가변, 2px 간격 줄바꿈, 4행 초과 시 팔레트 영역 스크롤) |
| `colorMode` | `"byLayer" \| "byBlock" \| null` | — | 눌린 ByLayer / ByBlock. `null`·미지정 = 명시 색 |
| `colorMenuOpen` | `boolean` | — | 색상 Dropdown 열림(제어형). 없으면 내부 관리 |
| `lineTypes` / `lineWeights` | `SWHeaderLineType[]` / `SWHeaderLineWeight[]` | — | 선스타일 Dropdown의 선 종류 / 가중치 열. 둘 다 없으면 기존 필드 버튼. 스케일 열은 `lineStyle.scale` |
| `activeLineTypeId` / `activeLineWeightId` | `string` | `lineStyle.name` / `.weight`와 같은 이름의 행 | 현재 행(이름 `#FFC600`) |
| `lineStyleMenuOpen` | `boolean` | — | 선스타일 Dropdown 열림(제어형) |
| `textStyles` / `dimensionStyles` | `SWHeaderTextStyleOption[]` / `SWHeaderDimensionStyleOption[]` | — | 문자 / 치수스타일 Dropdown 행. 없으면 기존 필드 버튼 |
| `activeTextStyleId` / `activeDimensionStyleId` | `string` | `textStyle.name` / `dimensionStyle.name`과 같은 이름의 행 | 현재 행 |
| `textStyleMenuOpen` / `dimensionStyleMenuOpen` | `boolean` | — | 각 Dropdown 열림(제어형) |
Dropdown 데이터 타입(모두 `id`로 식별, 배열 순서 = 표시 순서, 패키지에 고정 데이터 없음):
| 타입 | 필드 |
|---|---|
| `SWHeaderColorOption` | `{ id; color; value?; label?; disabled? }` — `color` CSS 색, `value` 보고·표시 값(기본 `color`의 `r,g,b`), `label` 접근성 이름 |
| `SWHeaderColorInputValue` | `{ value: "r,g,b"; rgb: [r, g, b]; hex: "#rrggbb" }` |
| `SWHeaderLineType` | `{ id; name; pattern?: number[]; disabled? }` — `pattern` 70px 미리보기의 대시 길이(4px 간격, 비율 맞춤), 없으면 실선 |
| `SWHeaderLineWeight` | `{ id; name; value?; editable?; disabled? }` — `value` 오른쪽 값(ByDefault 0.25), `editable` 이면 값 Input(User) |
| `SWHeaderTextStyleOption` | `{ id; name; font; disabled? }` |
| `SWHeaderDimensionStyleOption` | `{ id; name; disabled? }` |
`SWHeaderDocument`: `{ id: string; name: string; title?: string; icon?: ReactNode; disabled?: boolean }` — `title`은 툴팁(기본 `name`).
`SWHeaderLayer`: `{ id: string; name: string; color?: string; visible?: boolean; frozen?: boolean; locked?: boolean; disabled?: boolean }` — `color`는 16px 견본 색(기본 `#ffffff`), 기본값은 `visible: true`, `frozen: false`, `locked: false`. 확정 매핑([명세 §5.2.1](./sw-header-design-spec.md#521-아이콘-onoff와-데이터-의미)):
| 아이콘 | 컬러 | 회색 | 클릭 시 Callback 값 |
|---|---|---|---|
| 전구 | `visible=true` | `visible=false` | `onLayerVisibilityChange(layer, !visible)` |
| 눈꽃 | `frozen=false`(동결 해제) | `frozen=true`(동결) | `onLayerFreezeChange(layer, !frozen)` |
| 자물쇠 | `locked=false`(잠금 해제) | `locked=true`(잠금) | `onLayerLockChange(layer, !locked)` |
헤더는 값을 직접 바꾸지 않으므로 호스트가 받은 값으로 `layers`를 갱신해야 아이콘 색이 바뀝니다.
> **로고는 확정 디자인이 아닙니다.** 로고 영역의 위치·크기는 공통 디자인이고, 로고 이미지는 사용하는 SW 프로그램이 정합니다. 패키지에는 로고가 들어 있지 않으며 `logo`·`logoSrc`를 전달하지 않으면 로고 없이 표시됩니다(배치는 그대로). Storybook의 기본 로고는 임시 자산(`sw-header-logo-placeholder.svg`)입니다.
기본값은 Figma 표시값이 아니라 중립값입니다. Figma 샘플 값은 Storybook `figmaArgs`를 참고합니다.
## 6. Callback
| Callback | 호출 시점 |
|---|---|
| `onHomeClick` | 홈(로고) 클릭 |
| `onDocumentClick` | 파일명 클릭(Dropdown 열고 닫을 때도 호출) |
| `onDocumentMenuOpenChange(open)` | 문서 Dropdown 열림·닫힘 |
| `onDocumentSelect(document)` | 문서 항목 선택. 문서를 여는 동작은 호스트가 구현 |
| `onDocumentClose(document)` | 행의 X(문서 닫기). 주면 모든 행에 X가 생기고 Hover·키보드 Focus한 행에만 보입니다. 선택 Callback과 중복되지 않습니다. 실제 닫기·목록 갱신·다음 선택은 호스트가 처리 |
| `onDocumentNew`, `onDocumentOpen` | Dropdown 하단 `+ New` / `Open` |
| `onLayerOpen`, `onColorOpen`, `onLineStyleOpen`, `onTextStyleOpen`, `onDimensionStyleOpen` | 각 필드 클릭(Dropdown이 있으면 열고 닫을 때도 호출) 또는 더보기 항목 선택 1회(대표 필드 전환, §7 "반응형 More") |
| `onLayerMenuOpenChange(open)` | 레이어 Dropdown 열림·닫힘 |
| `onLayerSelect(layer)` | 레이어 행(견본·이름) 선택. 목록이 닫힙니다. 현재 레이어 변경은 호스트가 `activeLayerId`로 반영 |
| `onLayerVisibilityChange(layer, nextVisible)` | 전구 클릭. 목록 유지, 선택 Callback 없음 |
| `onLayerFreezeChange(layer, nextFrozen)` | 눈꽃 클릭. 목록 유지, 선택 Callback 없음 |
| `onLayerLockChange(layer, nextLocked)` | 자물쇠 클릭. 목록 유지, 선택 Callback 없음 |
| `onLayerDetail()` | 하단 `레이어 상세`. 호출 후 목록이 닫힙니다. 없으면 버튼은 Figma 외형 그대로 비활성 |
| `onColorMenuOpenChange(open)` 등 `on*MenuOpenChange` | 각 Dropdown 열림·닫힘(색상·선/문자/치수스타일) |
| `onColorSelect(option)` | 팔레트·사용자 색상 칩. 목록 유지 |
| `onColorModeChange(mode)` | ByLayer / ByBlock. 목록 유지. 상호 배타 표시는 호스트가 `colorMode`로 반영 |
| `onColorInput(value)` | RGB 입력 확정(Enter·Focus 이동·목록 닫힘). 유효한 `r,g,b`(0–255) 또는 `#rrggbb`만, 값이 바뀐 경우만 |
| `onColorPick()` | 색상 추출 버튼. 목록이 닫히고 실제 추출(도면 픽셀 등)은 호스트가 구현 |
| `onLineTypeSelect(type)` / `onLineWeightSelect(weight)` | 선 종류 / 가중치 행. 다른 열 값은 건드리지 않고 목록 유지 |
| `onLineWeightValueChange(weight, value)` | `editable` 가중치 Input 확정(0 이상 소수) |
| `onLineScaleChange(value)` | 스케일 Input 확정(0 초과 소수) |
| `onTextStyleSelect(style)` / `onDimensionStyleSelect(style)` | 행 선택 후 목록 닫힘 |
| `onColorDetail`, `onLineStyleDetail`, `onTextStyleDetail`, `onDimensionStyleDetail` | 하단 `… 상세`. 전달 시 호출 후 목록 닫힘. 없으면 공통 Default/Hover를 유지하고 클릭은 무동작. 상세 팝업은 패키지 범위 밖(추후 Figma 전달 후) |
| `onPropertyBarToggle`, `onPropertyPanelToggle` | 속성 토글 클릭 또는 더보기 항목 선택 |
| `onPrint`, `onPopupGuide` | 인쇄·팝업 가이드 |
| `onWindowMoveStart(event)` | 창 이동 영역 pointer down |
| `onMinimize`, `onClose` | 최소화·종료 |
헤더는 선택·열림 상태를 스스로 저장하지 않습니다. 호스트가 Callback에서 상태를 바꾸고 Props로 다시 내려줍니다. 실제 드롭다운 목록, 파일 열기, 창 이동·최소화·종료(브라우저 / Electron / 설치형 SW마다 다름)는 호스트가 구현합니다.
## 7. 전체 헤더 예제
```tsx
import { useState } from "react"
import { SWHeader, type SWHeaderMenuId } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
export function DrawingHeader({ fileName }: { fileName: string }) {
const [activeMenu, setActiveMenu] = useState<SWHeaderMenuId | null>(null)
const [panelOpen, setPanelOpen] = useState(false)
const toggle = (id: SWHeaderMenuId) => () => setActiveMenu(current => (current === id ? null : id))
return (
<SWHeader
documentTitle={fileName}
layer={{ name: "0_Layer_Cludmark" }}
color={{ value: "255,229,0", swatch: "#ffe500" }}
lineStyle={{ name: "Continuous", weight: "0.25", scale: "1.00" }}
textStyle={{ font: "Arial", name: "Standard" }}
dimensionStyle={{ name: "Standard" }}
activeMenu={activeMenu}
propertyPanelOpen={panelOpen}
onLayerOpen={toggle("layer")}
onColorOpen={toggle("color")}
onLineStyleOpen={toggle("lineStyle")}
onTextStyleOpen={toggle("textStyle")}
onDimensionStyleOpen={toggle("dimensionStyle")}
onPropertyPanelToggle={() => setPanelOpen(open => !open)}
onClose={() => window.close()}
/>
)
}
```
### 로고 지정 예
```tsx
import productLogo from "./assets/product-logo.svg" // 각 프로그램의 로고 파일
<SWHeader documentTitle={fileName} logoSrc={productLogo} logoAlt="제품명" />
// React 요소로 전달(인라인 SVG 등). logoSrc보다 우선합니다.
<SWHeader documentTitle={fileName} logo={<ProductMark />} logoAlt="제품명" />
```
- 로고 영역은 32 × 17px입니다. 가로·세로 비율이 달라도 영역 안에 맞춰지고 잘리지 않습니다. 선명하게 보이도록 SVG 또는 64 × 34px 이상 이미지를 권장합니다.
- 로고 영역 크기는 디자인 기준이므로 바꾸지 않는 것을 권장합니다(토큰 `--hm-sw-header-logo-width` / `-logo-height`).
### 문서 Dropdown 예
```tsx
const [openDocuments, setOpenDocuments] = useState<SWHeaderDocument[]>([])
<SWHeader
documentTitle={current.name}
documents={openDocuments} // 각 프로그램의 문서 목록(표시 순서 그대로)
activeDocumentId={current.id} // 이름만 #FFC600, 순서는 그대로
onDocumentSelect={document => activate(document.id)}
onDocumentClose={document => closeDocument(document.id)} // 목록·현재 문서는 호스트가 갱신
onDocumentNew={createDocument}
onDocumentOpen={showOpenDialog} // 파일 열기는 호스트가 구현
/>
```
- 클릭으로 열고 닫습니다(Hover로 열리지 않음). 바깥 클릭·Escape·항목 선택·New/Open 후 닫힙니다.
- Dropdown은 헤더 아래로 겹쳐 열립니다. SWHeader의 조상 요소에 `overflow: hidden`이 있으면 잘리므로, 헤더는 페이지 최상단처럼 잘리지 않는 위치에 둡니다.
### 레이어 Dropdown 예
```tsx
const [layers, setLayers] = useState<SWHeaderLayer[]>(() => drawing.layers.map(toHeaderLayer))
const patch = (id: string, change: Partial<SWHeaderLayer>) =>
setLayers(list => list.map(layer => (layer.id === id ? { ...layer, ...change } : layer)))
<SWHeader
documentTitle={fileName}
layers={layers} // 각 프로그램의 레이어 목록(표시 순서 그대로)
activeLayerId={currentLayerId} // 필드 표시 + 목록에서 이름만 #FFC600
onLayerSelect={layer => setCurrentLayer(layer.id)}
onLayerVisibilityChange={(layer, visible) => { viewer.setLayerVisible(layer.id, visible); patch(layer.id, { visible }) }}
onLayerFreezeChange={(layer, frozen) => { viewer.setLayerFrozen(layer.id, frozen); patch(layer.id, { frozen }) }}
onLayerLockChange={(layer, locked) => { viewer.setLayerLocked(layer.id, locked); patch(layer.id, { locked }) }}
onLayerDetail={openLayerManager} // 없으면 버튼 비활성(임의 팝업 없음)
/>
```
- 레이어 필드를 클릭해야 열립니다(Hover는 테두리·글자색만). 행 선택·Escape·바깥 클릭·Tab 이탈·레이어 상세 후 닫힙니다. 전구·눈꽃·자물쇠 클릭은 목록을 유지합니다.
- 헤더는 레이어 상태를 저장하지 않습니다. Callback에서 도면(`viewer.*`은 예시)과 `layers`를 함께 갱신해야 아이콘이 바뀝니다. 실제 표시·동결·잠금 처리와 편집 가능 여부 판단은 호스트 책임입니다.
- 키보드: 필드에서 ↓로 열면 현재 레이어에 Focus, ↓↑ 행 이동(같은 열), ←→ 행 안 이동, Enter/Space 실행. 상세 규격은 [디자인 명세 §5.2](./sw-header-design-spec.md#52-레이어-dropdown-figma-headermenu레이어-2610940-2026-10-16).
### 색상 · 선/문자/치수스타일 Dropdown 예 (2026-10-16)
```tsx
const [color, setColor] = useState<SWHeaderColorValue>({ value: "ByLayer", swatch: "#ffffff" })
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>("byLayer")
const [lineStyle, setLineStyle] = useState({ name: "Continuous", weight: "0.25", scale: "1.00" })
<SWHeader
documentTitle={fileName}
color={color}
colors={program.palette} // [{ id: "red", color: "#ff0000", label: "빨강" }, ...]
userColors={program.userColors} // 개수 가변
colorMode={colorMode}
onColorSelect={option => { viewer.setColor(option.color); setColor({ value: option.value ?? option.color, swatch: option.color }); setColorMode(null) }}
onColorModeChange={mode => { viewer.setColorMode(mode); setColorMode(mode) }}
onColorInput={({ value, hex }) => { viewer.setColor(hex); setColor({ value, swatch: hex }); setColorMode(null) }}
onColorPick={startPickFromDrawing} // 실제 픽셀 추출은 호스트
onColorDetail={openColorDetail} // 없으면 Hover 유지·클릭 무동작(상세 팝업 미제공)
lineStyle={lineStyle}
lineTypes={program.lineTypes} // [{ id: "center", name: "CENTER", pattern: swHeaderLinePatterns.center }, ...]
lineWeights={program.lineWeights} // [{ id: "user", name: "User", value: "0.25", editable: true }, ...]
onLineTypeSelect={type => setLineStyle(s => ({ ...s, name: type.name }))} // 가중치·스케일 유지
onLineWeightSelect={weight => setLineStyle(s => ({ ...s, weight: weight.value ?? weight.name }))}
onLineWeightValueChange={(weight, value) => updateWeight(weight.id, value)}
onLineScaleChange={scale => setLineStyle(s => ({ ...s, scale }))}
textStyle={textStyle}
textStyles={program.textStyles} // [{ id, font: "Arial", name: "Standard" }, ...]
onTextStyleSelect={style => setTextStyle({ font: style.font, name: style.name })}
dimensionStyle={dimensionStyle}
dimensionStyles={program.dimensionStyles} // [{ id, name: "ISO - 25" }, ...]
onDimensionStyleSelect={style => setDimensionStyle({ name: style.name })}
/>
```
- 클릭으로 열고 닫습니다(Hover는 노란 테두리·글자만). 다른 Dropdown을 열면 앞의 것이 닫히고, 바깥 클릭·Escape·Tab 이탈로 닫힙니다. 헤더 높이(49px)와 다른 필드 위치는 변하지 않습니다.
- 색상·선스타일은 여러 속성을 이어서 바꿀 수 있도록 선택 후에도 열려 있고, 문자·치수스타일은 행 선택 후 닫힙니다. 색상 추출과 `… 상세`는 호출 후 닫힙니다.
- 헤더는 값을 저장하지 않습니다. Callback 값으로 `color` / `colorMode` / `lineStyle` 등을 갱신해야 필드가 바뀝니다. 명시 색을 고를 때 ByLayer/ByBlock을 해제할지는 호스트 정책입니다(Storybook 예제는 해제).
- 키보드: 필드에서 ↓로 열면 현재(눌린) 항목 또는 첫 항목에 Focus, ↓↑로 Dropdown 안 버튼·Input 순서대로 이동, Enter/Space 실행, Escape 닫기(필드로 Focus 복귀).
- Hover: 상세 버튼 5개는 문자스타일 상세 기준 공통 Hover(배경 .25·radius 25·아이콘 35%·글자 35%)를 적용합니다. ByLayer/ByBlock은 기존 후속 Hover 기준을 따릅니다. 색상 칩·색상 추출·목록 행(선 종류·가중치·스케일·문자·치수)·Input의 현재 동작은 유지합니다.
- 좁은 헤더: 목록은 헤더 안으로 옮겨지며, 468px 선스타일 목록은 헤더 폭 480 미만에서 세로로 쌓입니다.
### 동적 데이터 구조
모든 Dropdown(문서·레이어·색상·선 종류·가중치·문자·치수스타일)은 Props 배열로만 그립니다.
- 항목 추가·삭제·이름/값 변경·순서 변경은 배열을 바꾸면 되고 컴포넌트 소스 수정이 필요 없습니다. 각 항목은 `id`로 식별되어 바뀐 항목만 다시 그리고 다른 행의 상태(입력 중 값 등)를 보존합니다.
- 현재 항목(`active*Id`)이 배열에서 사라지면 선택 표시가 없어지고 필드는 값 Props(`layer`, `lineStyle` 등)를 그대로 표시합니다. 다음 현재 항목은 호스트가 정합니다.
- `disabled`(선택 불가), `editable`(가중치 값 Input), Input의 `readOnly` / `disabled`를 항목별로 줄 수 있습니다.
- 목록이 길면 Dropdown 안에서만 스크롤합니다(행 목록 최대 240px = 8행, 사용자 색상은 팔레트 118px). 헤더 레이아웃은 유지됩니다.
- Storybook `SW / Header / Dynamic Data`에서 추가·삭제·이름·값·순서 변경을 직접 확인할 수 있습니다.
### 수정 가능한 Input — `SWHeaderInput`
레이어 상세 등 상세 영역에서 속성값을 직접 고칠 때 씁니다(Figma input 26:11656, cursor 26:11664). 상세 팝업 레이아웃은 아직 없으므로 Input과 계약만 제공합니다.
```tsx
import { SWHeaderInput, parseSWHeaderNumber, SWTheme } from "@hm-design/sw"
<SWTheme>
<SWHeaderInput
aria-label="X 좌표"
valueType="number" // "text"(기본) | "number"
value={entity.x} // 확정값(호스트). 바뀌면 최신 값 표시
onValueChange={draft => preview(draft)} // 편집 중 임시값(선택)
onCommit={next => updateEntity({ x: next })} // Enter · Focus 이동 · 감싼 Dropdown 닫힘, 유효 + 변경 시만
onCancel={() => clearPreview()} // Escape: 확정값으로 복원
parse={draft => parseSWHeaderNumber(draft, { min: 0 })} // 선택: 형식·범위 직접 지정
/>
</SWTheme>
```
- 상태: Default(배경·테두리 없음, 58 × 20 기준) / Hover(1px `rgba(212,203,189,.25)`, 24px) / 편집 중(`#0F0D0A`, 1px `.3`, 24px). 글자 `#E60403` 14px + 검정 Text Stroke, 글자 위치는 상태와 무관하게 고정. 폭은 값 길이에 맞춥니다.
- 커서: 네이티브 caret을 `#E60403`(`caret-color`)으로 표시하고 깜박임도 네이티브입니다(가짜 커서 없음). 선택·IME(한글 조합 중 Enter는 확정하지 않음)는 브라우저 기본 동작입니다.
- `onCommit`이 없거나 `readOnly`이면 읽기 전용(Hover·편집 표시 없음), `disabled`는 40%.
- 선택형(목록) 입력은 Figma 디자인 전달 후 같은 데이터 정의(`id`·이름·값·타입)로 확장합니다. 현재는 텍스트·숫자와 사용자 `parse`(임의 형식)를 지원합니다.
### 반응형 More — 대표 필드 전환
헤더가 좁아 필드가 더보기(…)로 옮겨졌을 때, 더보기에서 필드 항목을 고르면 그 필드가 객체 수정 영역 첫 칸(레이어 필드 자리)에 표시되고 Dropdown이 바로 아래에 열립니다. Focus는 그 필드로 옮겨갑니다.
- 레이어는 그동안 더보기로 이동하며, 더보기 → 레이어로 원래 배치로 돌아갑니다. 이미 화면에 있는 필드는 더보기에 나오지 않습니다.
- 전환은 UI 상태입니다. 데이터 Callback은 호출되지 않고, 필드 클릭과 같은 `on*Open` 1회와 `on*MenuOpenChange(true)`만 호출됩니다. 값은 그대로 유지됩니다.
- 폭이 넓어져 그 필드가 다시 보이는 단계에서는 1920 기준 배치로 돌아갑니다(Breakpoint 1800 / 1180 / 1080 / 900 / 640 / 520은 그대로).
- Dropdown 데이터(`colors` 등)가 없는 필드도 같은 방식으로 대표 칸에 표시됩니다(목록 없이 `on*Open`만).
## 8. 하위 컴포넌트 재사용
헤더 밖에서는 `SWTheme`로 감쌉니다. 소비자가 `hm-sw-*` class를 붙이지 않습니다.
```tsx
import { SWTheme, SWHeaderLayerControl, SWHeaderColorControl, SWHeaderProperties } from "@hm-design/sw"
<SWTheme>
<SWHeaderLayerControl value={{ name: "0" }} onClick={openLayers} />
<SWHeaderColorControl value={{ value: "255,229,0", swatch: "#ffe500" }} selected />
<SWHeaderProperties panelOpen={panelOpen} onPanelToggle={togglePanel} />
</SWTheme>
```
`SWTheme`는 배치(flex 등)를 정하지 않습니다. 배치는 `className`·`style`로 소비 화면에서 정합니다.
## 9. 테마 커스터마이징
`--hm-sw-header-*` 토큰만 바꿉니다. 사용자 확정 3개 면(파일명·객체 수정·속성 입력)은 기본값을 유지하는 것을 권장합니다.
```tsx
import type { CSSProperties } from "react"
<SWHeader documentTitle="a.dwg" style={{ "--hm-sw-header-selected-color": "#ffd84d" } as CSSProperties} />
```
토큰 목록과 사용처는 [디자인 명세 §14](./sw-header-design-spec.md#14-sw-전용-토큰-목록)를 따릅니다. `@hm-design/ui` Theme Runtime(JSON)과는 연결되어 있지 않습니다.
## 10. DWG/DXF 뷰어 연동 시 고려사항
- 기존 뷰어 `Toolbar.tsx`의 파일명(`fileName`)은 `documentTitle`로, 패널 토글(`propsOpen` 등)은 `propertyPanelOpen` + `onPropertyPanelToggle`로 연결할 수 있습니다.
- 뷰어의 파일 열기·샘플·Model/Layout 탭·2.5D·어두운 배경·로드 상태는 Figma SW Header에 없는 뷰어 전용 도구입니다. 필요하면 `tools` 슬롯에 호스트가 렌더링합니다(스타일은 호스트 책임).
- 헤더 높이는 49px입니다. 고정 배치(`position: fixed`)는 `className`·`style`로 호스트에서 지정합니다.
- 반응형은 모두 헤더 자신의 폭(container query)을 기준으로 하므로 사이드 패널 때문에 헤더가 좁아져도 동작합니다. 단계(1800 / 1180 / 1080 / 900 / 640 / 520)는 [디자인 명세 §12](./sw-header-design-spec.md#12-반응형)를 따릅니다.
- 뷰어는 현재 `@hm-design/ui`의 SWHeader 패턴을 사용합니다. 이 패키지로 바꾸는 작업은 이번 범위가 아닙니다.
## 10.1 상호작용 상태
- 그룹 필드 Hover(현재 추가 기준): 레이어·색상·선/문자/치수스타일은 버튼 전체 Hover 시 라벨·값과 지정된 선/폰트/치수 미리보기가 `#FFC600`이 됩니다. 기존 Gradient Hover Border와 검정 외곽선은 유지하며 색상 견본·레이어 상태 아이콘은 바꾸지 않습니다. Disabled는 새 Hover 효과를 적용하지 않고 Selected 색은 기존대로입니다. 상세보기의 텍스트 전용 Hover는 변경하지 않습니다. [디자인 명세 §11.5](./sw-header-design-spec.md#115-현재-추가-기준--1400ms-및-그룹-필드-hover-2026-10-16).
- Hover Border: 테두리가 있는 버튼(홈, 5개 속성 필드, 더보기, 인쇄, 팝업 가이드)은 마우스를 올리면 테두리가 Figma 홈 버튼의 노란 gradient로 바뀝니다. 5개 필드 내부 텍스트·미리보기는 위 추가 기준을 함께 적용합니다. 파일명과 속성Bar·속성창은 예외로, 테두리 없이 글자만 `#FFC600`으로 바뀝니다(파일명 Dropdown은 클릭해야 열림). 문서 Dropdown 안은 Figma hover를 따릅니다: 목록 항목은 검정 40% 배경, `+ New` / `Open`은 배경이 진해지고(20% → 30%) 글자가 검정 100%가 됩니다(노란 테두리 없음). 배경·크기·위치는 변하지 않습니다. 비활성 버튼은 바뀌지 않습니다.
- Hover(창 제어 · 바로가기 아이콘): 인쇄·팝업 가이드는 노란 테두리와 함께 아이콘의 `#B6D0C9` 부분만 `#FFC600`으로 바뀝니다(외곽선·그림자·타일 색은 그대로). 최소화는 검정 20% 배경 + 흰 막대, 종료는 `#E40000` 배경 + 흰 X로 바뀝니다(Figma 26:12156 / 26:12161, 32 × 48 유지, 노란 테두리 없음). 소비자는 class나 Prop을 추가하지 않습니다. 상세는 [디자인 명세 §11.2](./sw-header-design-spec.md#112-창-제어--바로가기-아이콘-hover-2026-10-16).
- Selected(`activeMenu`, `propertyBarOpen`, `propertyPanelOpen`): 글자만 `#FFC600`. Hover와 함께 표시됩니다.
- Focus: 키보드 Focus 윤곽선은 Hover 테두리 위에 표시됩니다.
- Hover 테두리 색은 `--hm-sw-header-hover-border-gradient` 토큰, 최소화 Hover 배경·막대 색은 `--hm-sw-header-window-button-hover-background` / `--hm-sw-header-window-icon-hover-color` 토큰으로 바꿀 수 있습니다. 종료·인쇄·팝업 가이드 Hover 아이콘은 패키지 SVG Asset입니다. 대상·제외 목록은 [디자인 명세 §11.1](./sw-header-design-spec.md#111-hover-테두리-공통-상호작용-규칙)을 따릅니다.
- Text Stroke(2026-10-16): Figma에 Stroke가 있는 헤더·Dropdown 텍스트는 검정 외곽선(14·16px 1.3px, 12px 라벨 1px)과 자기 색 0.3px Stroke를 표시합니다. Hover·Selected는 채움색만 바뀌고 외곽선은 유지됩니다. 더보기 메뉴·창 이동 Tooltip은 Figma에 Stroke가 없어 적용하지 않고, `+ New` / `Open`은 요청에 따라 기존 그대로입니다. 외곽선 색·굵기는 `--hm-sw-header-text-outline-color`, `--hm-sw-header-text-stroke-width` 등으로 바꿀 수 있습니다([명세 §6.1](./sw-header-design-spec.md#61-text-stroke-2026-10-16)).
## 10.2 창 이동 Hover Motion
별도 Prop이나 의존성 없이 Hover하면 GIF의 경로·구간 비율을 유지하면서 후속 사용자 요청에 따라 **1.4초마다 반복**합니다(원본 GIF 1.64초와 다름). 커서가 아래에서 위로 먼저 올라가고 약 444ms에 문구가 나타난 뒤 창·커서·문구가 오른쪽 위로 이동합니다. 약 888ms에 이동을 마치고 약 512ms 유지한 뒤 반복합니다. 28px / −4.5px 이동과 가속·감속 비율은 그대로이며 이탈 시 즉시 기본 SVG로 돌아갑니다. 크기·줄무늬·SVG·Tooltip 규격은 SW/Figma를 유지합니다.
`onWindowMoveStart`는 기존 `PointerEventHandler<HTMLDivElement>`입니다. 예를 들어 호스트는 `event.currentTarget.setPointerCapture(event.pointerId)`로 이후 포인터를 추적할 수 있습니다. 패키지는 포인터를 캡처하거나 실제 창을 이동시키지 않습니다. 아이콘과 `창 이동` Tooltip은 `pointer-events: none`이며 Callback 이벤트와 최소화·종료 클릭을 가로채지 않습니다. 누르는 동안 CSS `:active`로 모션을 멈추고 Tooltip을 숨깁니다. Tooltip은 창 이동 영역 밖의 최소화 및 종료 왼쪽 일부에 겹칠 수 있습니다.
- 헤더 폭 ≥1080px: 88px 영역, GIF 기준 반복 모션과 Tooltip.
- 640–1079px: 24px 드래그 영역, 그림은 숨기고 정적 Tooltip만 표시.
- <640px: 기존대로 창 이동 영역 숨김.
- Reduced Motion: 기본 아이콘과 정적 Hover Tooltip을 유지하고 애니메이션을 끕니다. 24px 단계에서는 기존대로 그림이 숨겨집니다. OS 설정을 재생 중 바꾸어도 적용됩니다.
- Focus: 창 이동 div는 기존대로 Tab 대상이 아니며 키보드 창 이동·Focus Tooltip을 제공하지 않습니다. 실제 호스트의 키보드 창 이동 대안과 보조기술 검증은 별도로 필요합니다. 최소화·종료는 이름이 있는 기본 버튼이며 Focus 표시가 유지됩니다.
Tooltip은 45 × 17px, 검정 80% 배경과 흰색 12px/400 글자입니다. Tooltip만 Figma의 Inter를 우선하고 한글·미설치 환경은 기존 SW 글꼴로 fallback합니다. 별도 폰트 설치는 하지 않습니다. 경로·자산은 [명세 §11.4](./sw-header-design-spec.md#114-현재-창-이동-motion--ani_hover_movegif-우선-2026-10-16), 현재 시간과 필드 Hover는 [명세 §11.5](./sw-header-design-spec.md#115-현재-추가-기준--1400ms-및-그룹-필드-hover-2026-10-16), 결과는 [현재 통합 검증](./sw-header-speed-composed-hover-verification-2026-10-16.md)을 참고합니다. 원본 GIF 속도 검증과 GIF 전달 전 결과는 각각 [GIF 검증 기록](./sw-header-window-motion-gif-verification-2026-10-16.md), [과거 검증 기록](./sw-header-window-motion-verification-2026-10-16.md)에 보존합니다.
## 11. 지원 · 미지원
| 지원 | 미지원(호스트 또는 후속 작업) |
|---|---|
| Figma 헤더 1개(26:8944) 표시·선택·Focus·Disabled | 색상·선스타일·문자스타일·치수·레이어 **상세 팝업**(Figma 전달 후), 색 선택 팝업(Figma "추후 디자인 진행예정") |
| 색상(26:10986 / 26:11036)·선스타일(26:11104)·문자스타일(26:11181)·치수스타일(26:11215) Dropdown, ByLayer/ByBlock, RGB·가중치·스케일 Input(2026-10-16) | 실제 색 적용·픽셀 색상 추출·선/문자/치수 스타일 적용(호스트 구현) |
| 반응형 More 대표 필드 전환, `SWHeaderInput`(Default / Hover / 편집 / 커서), Text Stroke(2026-10-16) | 선택형 상세 Input(Figma 미전달) |
| 문서 Dropdown(26:10662) 목록·선택·New/Open Callback·키보드 | 실제 파일 열기·새 문서 만들기(호스트 구현) |
| 레이어 Dropdown(26:10940) 목록·선택·전구/눈꽃/자물쇠 Callback·레이어 상세 Callback·키보드 | 실제 레이어 표시·동결·잠금과 레이어 상세 화면(호스트 구현) |
| 1800 / 1180 / 1080 / 900 / 640 / 520 반응형 + 더보기(옮긴 기능도 같은 Props·Callback) | SW 팝업(DraggablePanel) |
| SSR 문자열 렌더링(window 접근 없음) | 창 이동·최소화·종료의 실제 동작 |
| React 19, ESM, 타입 선언 | CommonJS 빌드, Light/Dark 테마, Runtime Theme JSON 연결 |
## 12. Storybook
```bash
npm run storybook
```
`SW / Header` 아래 Story: Overview, Default, Controls, Interactive States(인쇄·팝업 가이드·최소화·종료 Default/Hover 비교와 Callback 기록 포함), Responsive, Composed Controls, Logo Slot(기본 임시 로고 / 사용자 지정 로고 / 로고 미전달), Document Dropdown(Default / Hover / Open / 선택 / 여러 문서 / 문서 없음 / 긴 파일명 / 좁은 화면), Layer Dropdown(Layer Default / Hover / Open(Figma 8행) / Row Hover / 전구·눈꽃·자물쇠 4상태 3배 확대 / 선택·Toggle Callback 기록과 상태표 / 여러 상태·현재 레이어 / 긴 레이어명 / 20개 스크롤 / 레이어 없음 / 레이어 상세 / 상태 표시 전용 / 좁은 화면 1440–390). Responsive Story는 헤더 폭 18개(1920–390, 각 단계 경계 전후 포함) 프레임을 나열합니다. 모든 단계가 헤더 폭 기준이므로 Viewport를 바꾸지 않아도 확인할 수 있습니다.
`Window Move Motion`은 GIF 주요 3상태와 속도 변경 후 약 0 / 205 / 410 / 444 / 683 / 888 / 1366 / 1400ms 프레임, 실제 Hover·Drag·최소화·종료 Callback 및 반복(`motion cycle`) 기록, 축소 폭 예제를 제공합니다. 내부 고정 시간은 원본 GIF 시간 × 1400/1640의 정확한 값입니다. 고정 프레임은 Storybook 전용 Hover attribute와 Web Animations API로 시간을 멈춥니다. Reduced Motion에서는 기본 아이콘·정적 Tooltip 정책을 따르므로 정상 타임라인 비교는 해당 설정을 끄고 Story를 다시 로드합니다.
2026-10-16 추가 Story: `Color Dropdown`, `Line Style Dropdown`, `Text · Dimension Style Dropdown`(Default / Hover / Expanded / 행 Hover / 독립 버튼 Hover 무변화 / Callback 기록 / 좁은 화면), `Responsive More`(대표 필드 전환 Callback 기록 1440·768·390 + 16개 폭), `Text Stroke`(Figma 원본 이미지 비교 · 3배 확대 · 말줄임), `Layer Detail · Input`(Figma 참조 이미지 · 레이어 상세 Default/Hover 무변화/Disabled/Callback · Input Default/Hover/ReadOnly/Disabled/편집/확정·취소·외부 값·IME), `Dynamic Data`(항목 추가·삭제·이름·값·순서 변경). `Document Dropdown`의 첫 예제는 현재 열린 문서 1개이며, 기본 `figmaArgs`도 그 문서 1개를 전달합니다(`documents=[]`는 예외 상태 예제로만 남김). 검증 결과는 [Dropdown·More·Text Stroke·Input 검증 기록](./sw-header-dropdowns-stroke-input-verification-2026-10-16.md)에 있습니다.
`Composed Controls`에는 기존 레이어·색상·상세보기와 공개 선/문자/치수 컨트롤, 5개 필드의 고정 Hover 비교가 있습니다. 실제 공용 컴포넌트를 렌더링하며 60 × 15px 치수 Hover SVG는 흰 foreground만 노란색으로 바꾸고 검정 mask 외곽선을 유지합니다. 기존 Story만 보완했으며 새 Story는 추가하지 않았습니다. [현재 통합 검증](./sw-header-speed-composed-hover-verification-2026-10-16.md)을 참고합니다.
## 속성창·K 가이드 추가 연결 (2026-10-16)
기존 공개 Props를 유지한 추가 API입니다. `propertyPanel`을 전달하면 기존 속성창 버튼에 패널이 연결됩니다. 미전달 시 기존 토글/Callback 계약만 유지하며 임의의 CAD 데이터/편집 창을 만들지 않습니다.
```tsx
import { useState } from "react"
import { SWHeader, type SWHeaderProperty } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
function DrawingHeader() {
const [properties, setProperties] = useState<SWHeaderProperty[]>([
{ id: "layer", groupId: "common", name: "레이어", type: "text", value: "층_01" },
{ id: "weight", groupId: "common", name: "선가중치", type: "number", value: "0.25", min: 0, unit: "mm" },
])
return <SWHeader
documentTitle="도면"
propertyPanel={{
selection: { id: "selection-42", kind: "single", title: "선택된 객체 - 호스트 객체" },
groups: [{ id: "common", collapsedCount: 6 }],
properties,
onPropertyChange: (property, value) => {
// 실제 CAD 반영 및 계산은 호스트에서 수행한 뒤 결과를 Props로 돌려줍니다.
setProperties(items => items.map(item => item.id === property.id ? { ...item, value } : item))
},
onToolActiveChange: active => console.info("호스트 선택 도구", active),
}}
onPropertyPanelOpenChange={open => console.info("속성창 열림", open)}
/>
}
```
- Uncontrolled: `propertyPanelOpen`을 생략합니다. Controlled: `propertyPanelOpen={open}`, `onPropertyPanelOpenChange={setOpen}`을 함께 전달합니다. 기존 `onPropertyPanelToggle`은 버튼/More 클릭 시 그대로 호출됩니다. 이전 Controlled 호스트가 새 Open Callback 없이 기존 Toggle Callback만 사용한다면 X/Escape 닫기에서도 기존 Toggle로 알립니다.
- 선택 대상이 바뀔 때 `selection.id`도 반드시 변경합니다. 같은 property id라도 이전 객체의 임시 편집값은 재사용하지 않습니다.
- `selection.kind`: none/all/single. `selection.title`·`counts`·`overviewTitle`은 호스트 데이터입니다. counts의 선택적 row/column은 Figma 집계 배치를 표현할 수 있습니다. `onOverviewClick`도 선택 변경을 요청할 뿐 내부 CAD 동작은 없습니다.
- 항목 타입: text/number/select/color/slider/computed. `id/name/value/type/groupId`가 필수이고, `order`로 표시 순서를 지정합니다. 중복 id를 사용하지 마세요.
- `options`는 select의 id/name/disabled 목록입니다. 선택은 Enter 또는 Focus 이동으로 확정하고 Escape로 취소합니다. 삭제되거나 disabled가 된 option은 새 값으로 확정하지 않습니다. 옵션 데이터가 바뀌면 임시 선택을 현재 호스트 값으로 돌립니다.
- `color`는 기존 RGB/HEX parser를 재사용합니다. `swatch`로 별도 견본을 제공할 수 있습니다. `linePreview/linePattern`은 기존 선 미리보기를 재사용합니다. `annotation`은 (ByL) 같은 보조 정보, `unit`은 mm/% 같은 단위입니다.
- number/slider는 `min/max`, slider는 `step`을 지원합니다. slider의 기본 범위는 0~100이며 Story의 0~1 등은 호스트가 지정한 범위입니다. 수치를 계산하거나 Figma의 시각적 thumb 위치에 맞춰 임의 변환하지 않습니다.
- `validate(draft)`가 false면 거부합니다. 항목의 `onCommit(value, property)`와 패널의 `onPropertyChange(property, value)`는 서로 다른 통지 지점입니다. 둘 다 연결하면 각각 통지됩니다.
- 그룹은 `layout=rows/circle/position`, `order/name/collapsedCount/positionLabel`을 받습니다. circle은 원본 SVG 위 `graphicSlot=circumference/diameter/radius/area`의 항목을 배치합니다. 다른 항목은 그래픽 아래에 배치합니다. 계산값은 computed/readOnly로 전달합니다.
- `expandedGroupIds/onExpandedGroupsChange`로 펼침을 제어하거나, Uncontrolled일 때 `defaultExpandedGroupIds`로 시작합니다. 펼침은 property Callback을 실행하지 않습니다.
- `toolActive/onToolActiveChange/toolLabel`로 내부 아이콘을 연결합니다. 현재 의미는 호스트가 정하는 선택 도구 상태 통지이며 CAD 기능을 내장하지 않습니다.
- 단독 사용: `<SWTheme><SWHeaderPropertyPanel ... onClose={close} /></SWTheme>`. 단독 배치는 호스트의 `style`로 지정할 수 있습니다.
- 동적 패널 입력은 `SWHeaderInput commitOnUnmount={false}`를 사용합니다. 기존 Input의 기본 `true` 동작은 유지합니다.
- K 가이드는 별도 데이터를 요구하지 않습니다. 기존 K 버튼 Hover/Focus로 원본 이미지가 Pan/Zoom 반복되고, 작은 화면의 More에서도 같은 미리보기가 보입니다. Reduced Motion·Escape·재진입 초기화가 적용됩니다. 클릭은 기존 `onPopupGuide`만 호출합니다.
- 색상 모드의 null은 직접 색상 의미를 보존하고, 필수 모드 표시는 마지막 유효 모드(처음 ByLayer)를 유지합니다. 선택된 모드를 재클릭해도 onColorModeChange가 중복 호출되지 않습니다.
- 신규 Story: Property Panel / Property Panel Variants / K Guide / Additional Hover. 기존 17개 Story를 유지합니다.
실행 결과와 제한은 [추가 검증 기록](./sw-header-additional-verification-2026-10-16.md)을 따릅니다.
## 후속 5개 요구사항 연결 (2026-10-16)
### 속성Bar와 공유 데이터
```tsx
import { SWHeader, SWHeaderPropertyBar, type SWHeaderProperty } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
const fields: SWHeaderProperty[] = [
{ id: "length", groupId: "shape", name: "길이", type: "number", value: length, min: 0 },
]
const data = {
selection: { id: objectId, title: objectName, kind: "single" as const },
properties: fields,
onPropertyChange: (property: SWHeaderProperty, next: string) => updateObject(property.id, next),
}
<SWHeader documentTitle={fileName}
propertyBar={data} propertyBarOpen={barOpen} onPropertyBarOpenChange={setBarOpen}
propertyPanel={{ ...data, groups: [{ id: "shape", name: "형상" }] }}
propertyPanelOpen={panelOpen} onPropertyPanelOpenChange={setPanelOpen} />
```
- `propertyBar`는 `SWHeaderPropertyBarProps`: selection/properties/onPropertyChange, 전체 readOnly/disabled 및 div HTML 속성입니다. 개별 property의 readOnly/disabled/validate/min/max도 적용합니다. 컴포넌트 밖에서 단독 사용하면 `SWTheme`로 감쌉니다.
- `propertyBar` 생략 시 propertyPanel의 selection/properties/onPropertyChange만 공유합니다. 두 데이터를 모두 생략하면 토글 상태만 제공하며 임의 속성이나 CAD 수치를 만들지 않습니다.
- `propertyBarOpen` 생략 시 내부 상태, `defaultPropertyBarOpen`으로 초기값을 정합니다. Controlled는 `propertyBarOpen/onPropertyBarOpenChange`를 함께 사용합니다. 기존 `onPropertyBarToggle`도 클릭마다 보존됩니다. 속성창 열림과 독립적입니다.
- properties는 id로 식별하고 `order` 오름차순(같은 값은 배열 순서)으로 표시합니다. 객체 교체 시 selection.id를 바꿉니다. name/value/항목 추가·제거·외부 변경을 반영합니다.
- 값 편집은 `SWHeaderInput`을 사용합니다. Enter/Blur 확정, Escape 취소, 유효하지 않은 입력 복원, 외부 value 반영, 편집 중 삭제 draft 폐기를 지원합니다. 바의 select 타입은 option id를 입력하고 해당 options로 검사합니다. slider 타입은 숫자로 편집합니다. computed는 읽기 전용입니다. 속성창의 select/range UI를 바에 복제하지 않습니다.
- 헤더는 49px, 바는 별도 28px입니다. 폭은 최대 1222px이며 객체 수정 영역의 실제 위치부터 부모의 남은 폭을 사용합니다. x=377 고정값은 없습니다. 좁으면 focus 가능한 region 안에서 가로 스크롤하며 input focus로 해당 값에 접근할 수 있습니다. 호스트 콘텐츠는 열림 상태에 따라 필요한 28px 공간을 예약하세요. 기존 Dropdown은 바 위에 표시됩니다.
### 메뉴 컴포넌트 자체 구성
```tsx
<SWHeader documentTitle={fileName}
controls={[{ id: "lineStyle", label: "선 종류" }, { id: "layer" }]}
layers={layers} activeLayerId={layerId} onLayerSelect={selectLayer}
lineTypes={lineTypes} lineStyle={lineStyle} onLineTypeSelect={selectLineType} />
```
- `controls?: readonly SWHeaderControlConfig[]`는 전체 allowlist입니다. 미전달하면 승인된 기존 구성·계층·1920px 디자인을 유지합니다. `[]`는 선택적 컨트롤 전부 제거입니다. 홈/로고·창 제어는 유지합니다.
- id: document / drawingMode / layer / color / lineStyle / textStyle / dimensionStyle / details / propertyBar / propertyPanel / print / popupGuide. `details`는 상세보기 안내 caption이며 필수 액션이 아닙니다.
- 배열에서 빼거나 `visible:false`로 숨기며, 배열 순서가 실제 DOM·키보드·배치 순서입니다. 중복 id는 첫 번째 표시 항목만 사용합니다. 숨김 해제/재추가할 수 있습니다. `label`은 필드 라벨·버튼 이름·More 이름을 바꿉니다. 문서는 실제 documentTitle을 유지하고 접근성 이름 접두사를 바꿉니다. K는 아이콘을 유지하고 접근성 이름을 바꿉니다.
- 구성 모드는 각 컨트롤을 하나의 객체 수정 영역 안에서 재배치합니다. 기본 모드의 그룹 외곽 배치를 바꾸지 않습니다. 파일·색상 등을 제거한 공간은 고정 슬롯으로 남지 않습니다.
- 기존 1800/1180/1080/900/640/520px 기준은 유지합니다. 남은 필드 중 레이어(없으면 배열의 첫 필드)를 대표 슬롯으로 사용하고 More 선택으로 대체합니다. 대표 요소 삭제 시 남은 필드로 대체합니다. 표시 중인 대표는 More에서 제외되고, 구성에서 빠진 요소는 More에서도 사라집니다.
- 값은 기존 Props/Callback으로 계속 호스트가 소유합니다. 재정렬은 keyed 컴포넌트를 유지하고, 제거된 Dropdown과 draft는 정리하며 데이터 Callback을 생성하지 않습니다. 내부 열림 상태는 제거 후 재추가 시 닫힘입니다. Controlled 열림은 호스트가 전달한 값이 우선합니다. 일반적인 Dropdown 닫힘 시 기존 입력 확정 계약은 유지합니다.
- 구성요소 배열과 Dropdown 내부 옵션 배열은 별개입니다. layers/colors/userColors/lineTypes/lineWeights/textStyles/dimensionStyles의 동적 기능은 그대로 사용합니다.
### Colorize와 CSS 경로
- 색상 메뉴의 기존 `색상 추출` 위치에 Figma `26:11302`/`26:11305` 원본 SVG를 연결했습니다. 버튼 24×24, 아이콘 18×18, Hover는 주황 경계와 검정 25% overlay입니다. Hover는 Callback을 실행하지 않으며 클릭/Enter는 기존 `onColorPick`을 호출하고 메뉴를 닫습니다.
- `26:11338 color_돋보기` Overlay는 패키지에 구현되어 있지 않습니다. 호스트가 onColorPick에서 연결할 대상이며 임의 대체 색상창은 만들지 않았습니다.
- CSS 편집 원본은 `packages/sw/src/theme/sw.css`입니다. SW build가 dist/styles.css를 만들고 Story가 공개 `@hm-design/sw/styles.css`를 읽습니다. dist만 편집하면 다음 빌드에서 원본이 다시 포함됩니다. 원본 상세 span의 Stroke 선언과 미사용 solid-stroke-width 변수를 제거했으므로 산출물 override는 필요하지 않습니다. 외부 소비 앱에는 SW를 다시 빌드한 뒤 산출물을 갱신하세요.
- Text Stroke Story의 지정 원본 스크린샷·제목·전용 import를 제거했습니다. 이미지 파일과 나머지 비교 Story·text-shadow는 보존했습니다.
- 기존 21개 Story에 Property Bar / Dynamic Controls / Colorize를 추가했습니다. 현재 실행 결과는 [후속 검증 기록](./sw-header-followup-five-verification-2026-10-16.md)을 따릅니다.
## 구성요소 표시·메뉴 순서·속성창 4상태·형상 Slot (2026-10-16 3차)
이 절이 같은 주제의 이전 설명보다 우선합니다. 검증 결과는 [3차 검증 기록](./sw-header-composition-shape-verification-2026-10-16.md)에 별도로 기록했습니다.
### 로고 필수, 나머지 개별 제거 — `hiddenControls`
```tsx
<SWHeader documentTitle={fileName} logoSrc={logoUrl} logoAlt="EasyBeam"
hiddenControls={["document", "color", "textStyle", "print", "minimize"]}
/* 값·목록·Callback Props는 기존 그대로 */ />
```
- `hiddenControls?: readonly SWHeaderHideableControlId[]`. 대상 id: `home`, `document`, `drawingMode`, `layer`, `color`, `lineStyle`, `textStyle`, `dimensionStyle`, `details`, `propertyBar`, `propertyPanel`, `print`, `popupGuide`, `windowMove`, `minimize`, `close`. 로고는 대상이 아니며 항상 표시합니다(`logo` / `logoSrc` / `logoAlt` Props 유지).
- `home`을 제거하면 홈 동작만 사라지고 로고는 같은 32×17 슬롯에 정적 이미지로 남습니다(Hover·`onHomeClick` 없음).
- 전체 `controls` 목록 없이 쓸 수 있으며, 남은 요소는 기본 디자인의 크기·계층·Breakpoint를 유지합니다. `controls`와 함께 쓰면 두 조건을 모두 적용합니다(`controls`의 `visible:false`와 같은 의미).
- 제거한 요소는 렌더링하지 않습니다: 공간 없음, Dropdown·속성창·속성Bar·K 가이드 표시 안 함, Callback 없음. 열린 상태에서 제거하면 내부 열림 상태만 조용히 닫힙니다(데이터 Callback 없음).
- 배열에서 다시 빼면 재표시됩니다. 선택값은 호스트 Props이므로 그대로이고, 내부 Dropdown은 닫힌 상태로 돌아옵니다.
- 미전달 또는 `[]`이면 승인된 기본 Header와 동일합니다(1920~390px DOM 위치·크기·색 비교 차이 0).
- 객체 영역 요소를 제거하면 More 이동은 CSS 대신 스크립트가 같은 Breakpoint(1800/1180/1080/900/640)로 결정합니다. 표시할 항목이 없는 폭에서는 More 버튼을 만들지 않습니다. 창 영역은 남은 요소 폭만 차지합니다.
- 로고만 남겨도 Header 높이 49px와 녹색 객체 영역 배경은 유지합니다.
### 유사 메뉴 순서 — `menuOrder`
```tsx
<SWHeader documentTitle={fileName}
menuOrder={["color", "layer", "dimensionStyle", "lineStyle", "textStyle"]} />
```
- 대상: `layer`, `color`, `lineStyle`, `textStyle`, `dimensionStyle`(`SWHeaderMenuId`). 메뉴는 메뉴가 이미 차지한 자리 안에서만 서로 바뀌며 홈·파일·로고·그리기모드·상세보기·창 제어는 움직이지 않습니다.
- 미전달: 승인된 기본 순서 `레이어 → 색상 → 선스타일 → 문자스타일 → 치수스타일`(`controls`를 쓰면 그 배열 순서).
- 누락 규칙: 지정한 id가 먼저 지정 순서로 오고, 빠진 id는 그 뒤에 기본 순서로 붙습니다. 중복 id는 첫 번째만, 알 수 없는 id는 무시합니다. 숨긴 메뉴가 들어 있어도 오류 없이 건너뜁니다.
- 순서는 DOM·Tab 순서·More 목록 순서에 함께 반영됩니다. React key는 메뉴 id라 재정렬해도 값·Callback·Dropdown·Focus가 유지되며, 순서 변경만으로는 어떤 Callback도 호출하지 않습니다.
- 대표 슬롯: 레이어가 표시 중이면 레이어, 아니면 순서상 첫 메뉴가 기본 대표입니다. More에서 고른 메뉴가 대표 자리를 대신하며, 그 메뉴가 제거되면 기본 대표로 돌아갑니다.
### 속성창 4가지 상태
새 팝업 없이 기존 `SWHeaderPropertyPanel` 하나를 호스트 데이터로 바꿉니다.
| 상태 | Figma | selection.kind | 데이터 |
|---|---|---|---|
| 선택된 객체 없음 | 26:11886 · 314×256 | `none` | common 그룹만 |
| 전체 객체 선택 | 26:11823 · 314×361 | `all` + `counts` | common 그룹, 혼합값 `mixed` |
| 원 1개 선택 | 26:11932 · 314×658 | `single` + `overviewTitle` | common(collapsedCount) + shape(circle) + position |
| 모든 항목 펼침 | 26:12035 · 314×754 | `single` | 위와 같고 `expandedGroupIds={["common"]}` |
Storybook: `Property Panel · 선택 없음 / 전체 선택 / 원 1개 / 모두 펼침` 개별 Story와 `Property Panel · 4개 비교`(디자인 검증용, 실측 크기 표시, 좁으면 줄바꿈). 실제 Header는 현재 선택에 맞는 패널 하나만 표시합니다.
### 형상 영역 Slot — `shapeContent`
```tsx
<SWHeader documentTitle={fileName} propertyPanel={{
selection, properties, groups,
shapeContent: ({ selection, group, properties }) => <MyShapeViewer objectId={selection.id} />,
}} />
```
- `layout: "circle"` 그룹의 208×208 영역(그룹 260px 안 39, 26)에 렌더링합니다. `SWHeaderPropertyPanel`과 `SWHeader.propertyPanel` 모두 같은 Prop입니다.
- `undefined`(미지정): Figma 원형 그림(기존 기본). `null` / `false`: 빈 영역 — 크기·치수 라벨 위치·패널 높이 유지. ReactNode: 외부 컴포넌트. 함수: `{ selection, group, properties }`(해당 그룹 항목)를 받아 렌더링 — 객체 유형별 교체에 사용합니다.
- 내용은 영역 안에 맞춰지고(overflow hidden) 원주·지름·반지름·면적 입력은 그 위에 그대로 표시됩니다. 값은 `properties`로만 전달되며 형상 Slot과 독립입니다. 입력은 기존 SWHeaderInput 검증·Enter/Blur 확정·Escape 취소를 사용합니다.
- 패키지는 CAD 계산·형상 렌더링 엔진을 포함하지 않습니다. Storybook의 사각형/점선 원 SVG는 Story 전용 샘플입니다.
@@ -0,0 +1,101 @@
# SW Header 창 이동 Motion — GIF 기준 최종 검증 (2026-10-16)
## 1. 최종 판정: PARTIAL
GIF 기준 모션 수정과 Chrome 브라우저 검증 **PASS**, Root Typecheck / SW Build / Root Build / Storybook Build **PASS**입니다. 실제 호스트의 OS 창 이동·최소화·종료, 다른 브라우저·보조기술과 독립 SW 패키지의 Guard coverage는 **NOT VERIFIED**이므로 전체 PASS로 확대하지 않습니다. GIF 이전 검증은 [과거 기록](./sw-header-window-motion-verification-2026-10-16.md)으로 보존합니다.
## 2. 시작 Git 상태
- Worktree: `D:\디자인시스템\HMDesign-SW`, branch `feature/sw-package`.
- HEAD: `9407c8567f57301659e591c03451cca1b828733c`.
- 기존 변경: ` M package-lock.json`, ` M package.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`.
- `_local/sw-motion-gif-qa/baseline.json`에 기존 SW·Story·문서와 root manifest/Lockfile의 SHA-256을 보존하고, 변경 전 CSS·창 제어 TSX·Story 사본도 저장했습니다. 기존 미추적 파일을 삭제·초기화하지 않았습니다.
## 3. 기존 구현과 GIF의 차이
기존 구현은 2.7265658초 1회 재생이며, 커서가 이미 위쪽에 있는 별도 SVG로 fade-in하고 창을 축소한 뒤 오른쪽으로 이동했습니다. 문구가 이른 시점부터 나타났고 마지막에 dissolve로 Default로 복귀했습니다. 이 방식은 이번 GIF의 ‘아래쪽 커서 상승 → 문구 → 전체 이동 → 유지 → 즉시 반복’과 달랐습니다. 수정 전에 같은 40ms 간격의 41개 실제 브라우저 프레임을 캡처했습니다.
## 4. 추가 수정 범위
기존 모션 CSS만 GIF 표본으로 바꾸고 기존 `Window Move Motion` Story의 설명·고정 시간·반복 로그를 갱신했습니다. 기존 Default SVG, Hover용 SVG 두 개, 줄무늬, Tooltip box·색상·크기, 공개 API와 TSX 이벤트 연결은 유지했습니다. Tooltip 글꼴 선언만 Figma의 Inter를 우선하고 기존 SW 글꼴을 fallback하도록 명시했습니다. 폰트 파일이나 의존성은 추가하지 않았습니다.
누르는 동안 CSS `:active`로 3개 애니메이션을 일시정지하고 Tooltip을 숨깁니다. 전역 이벤트·포인터 캡처·preventDefault·React 상태는 추가하지 않았습니다.
## 5. 프레임별 비교 결과
기준 파일: `D:\디자인시스템\ani_hover_move.gif`, SHA-256 `c7d22819cfb8f8cd8593684ce1e5c676a545fa09fa9fc7573d59f0f237d28026`. 640 × 192px, 41프레임, 각 40ms, 1640ms, 무한 반복입니다.
GIF 높이 192px를 48px Header bar의 4배로 환산했습니다. 창의 녹색 폭도 96 / 4 = 24px로 기존 SVG와 일치합니다. GIF의 여분 가로 캔버스는 UI에 넣지 않았습니다. 비교 이미지에서만 가로 여백 32px를 잘라 기존 아이콘의 시작 위치를 정렬했습니다. 비교 결과는 Header 크기의 CSS px로 표시합니다.
- 0–520ms: 창은 제자리, 커서는 오른쪽 아래에서 위로 약 12.5px 이동하고 왼쪽으로 0.75px 이동합니다.
- 520ms: 문구가 처음 나타납니다. 41개 프레임 전체에서 문구 표시 여부가 일치했습니다.
- 약 600–1040ms: 창은 오른쪽 28px / 위 4.5px 이동합니다. 커서는 최초 위치 대비 오른쪽 27.25px / 위 17px입니다. 40ms 표본 사이를 선형 보간하여 원본의 가속·감속을 따라가며 GIF에 없는 spring overshoot를 제거했습니다.
- 1040–1640ms: 600ms 동안 창·커서·문구의 끝 자세를 유지합니다. 1640ms에 첫 프레임으로 즉시 반복합니다.
- 최종 41개 캡처의 녹색 도형 좌상단 비교: 창 위치 최대 오차 0px, 커서 최대 오차 0.5px. 문구 glyph 좌상단 최대 오차 0.5px입니다.
- 커서와 문구의 보이는 간격: GIF 9px, 구현 9.25–9.5px이며 프레임별 차이는 0.5px 이내입니다.
위 수치는 색이 있는 도형/glyph의 경계 좌표 비교이며 전체 픽셀 동일성 판정은 아닙니다. 기존 Figma SVG의 검정 외곽선·녹색 줄무늬·Tooltip 배경을 유지했으므로 GIF의 검정 캔버스와 외관 전체를 복제하지 않았습니다. 현재 동작은 Figma Prototype의 1.022/0.744/0.958초 spring·1회 정책보다 GIF의 시간과 반복을 우선합니다.
## 6. Drag 검증
실제 공개 `@hm-design/sw`를 React로 렌더링한 QA fixture에서 확인했습니다. `onWindowMoveStart`가 원래 div의 PointerEvent를 한 번 받았고 `target/currentTarget`, `pointerId`, `pointerType=mouse`, `buttons=1`, `defaultPrevented=false`를 유지했습니다. 호스트 모의 코드가 설정한 Pointer Capture를 통해 Tooltip·창 제어 버튼 위로 이동해도 pointermove와 pointerup이 전달됐습니다.
포인터를 누르는 동안 애니메이션은 paused이며 200ms가 지나도 currentTime이 변하지 않았고 Tooltip은 숨겨졌습니다. Drag 중 최소화·종료 콜백은 발생하지 않았고, 이후 각 버튼 클릭은 자신의 콜백만 호출했습니다. Reduced Motion에서도 드래그 콜백·해제가 유지됐습니다. 실제 OS 창 이동과 네이티브 호스트 통합은 NOT VERIFIED입니다.
## 7. Tooltip · 반복 · Reduced Motion · Focus
- PASS: 45 × 17px, 검정 80% 배경, 흰색 12px/400, Inter 우선 + SW fallback 선언. 한글 또는 Inter가 없는 환경은 기존 Noto Sans KR 등으로 표시되며 Inter 파일 로드를 보증하지 않습니다.
- PASS: 장식 자식 전체의 `pointer-events: none`, Tooltip 겹침 위치에서 최소화 버튼 hit test, 최소화·종료 Callback 전달.
- PASS: 실제 Hover를 유지해 1.64초 간격의 iteration 2회, 중복 animationstart 없음. 12회 빠른 진입·이탈에서 start/cancel 각 12회, 잔여 애니메이션 0개.
- PASS: Hover 해제 즉시 Default SVG·위치·문구 숨김, 재진입 시 처음부터 시작.
- PASS: 재생 중 reduce 전환 시 애니메이션 0개, 원래 Default SVG와 정적 Tooltip 유지. 누르는 동안 Tooltip 숨김, 이탈하면 숨김. 24px 단계의 기존 그림 숨김 정책도 유지.
- PASS: 최소화 → 종료 Tab 이동, 2px Focus 윤곽선, Enter로 종료 콜백.
- 기존 제한: 이동 div는 role=presentation/Tab 비대상이며 키보드 창 이동·Focus Tooltip을 제공하지 않습니다. 호스트의 키보드 대안과 보조기술은 미검증입니다.
## 8. 반응형
1920 / 1440 / 1280(추가) / 1080 / 1079 / 768 / 640 / 639 / 390px에서 실제 viewport와 Header 폭을 검사했습니다. 모두 높이 49px, Header 및 문서의 가로 overflow 0px, Tooltip은 Header 경계 안이었습니다. 1080px 이상 이동 영역 88px, 640–1079px는 24px와 정적 Tooltip만, 639px 이하는 영역 숨김을 유지합니다. 최소화·종료의 배치와 클릭은 유지했습니다.
Tooltip은 기존처럼 이동 영역 밖으로 나와 최소화·종료 일부에 시각적으로 겹칩니다. 시각적 겹침이 없다고 판정하지 않았으며, 포인터 차단이나 가로 스크롤이 없음을 확인했습니다. 기존 문서명·레이어명의 말줄임 구조는 변경하지 않았습니다.
## 9. Typecheck · Build · Storybook · Guard
- PASS: `npm.cmd run typecheck` — root·SW를 포함한 workspace 타입 검사와 기존 pretypecheck의 theme 확인·package build. 최종 CSS와 Story 변경 뒤 실행했습니다.
- PASS: `npm.cmd run build:sw` 및 최종 pretypecheck에 포함된 SW Build.
- PASS: `npm.cmd --ignore-scripts run build`, `npm.cmd --ignore-scripts run build-storybook`. 이미 통과한 공통 prerequisite을 중복 실행하지 않았습니다.
- PASS: Chrome `155.0.8059.79`, 실제 CSS 애니메이션·9개 Story 단계·520ms 등장/1640ms 반복 경계·motion cycle/Drag/최소화/종료 로그·Reduced Motion. 브라우저 통합 검사 41개 PASS, pageerror 0건, HTTP 오류 0건.
- 기존 경고: 문서 예시 `bg-[var(...)]` CSS 최적화, Storybook `:host-context` minifier, 500kB 초과 chunk와 plugin timing 안내. exit 0이며 범위 밖의 기존 코드를 수정하지 않았습니다.
- 개발 서버의 기존 포트·프로세스를 건드리지 않고 빌드된 Storybook과 QA fixture를 임시 localhost 서버에서 검사하고 종료했습니다.
- Guard 1회: `node scripts/ai-generation-guard.mjs --target . --profile STORYBOOK --scope PARTIAL --files src/sw/sw-header.stories.tsx,packages/sw/src/theme/sw.css --json`. 원본 status/staticStatus **NOT VERIFIED**, FAIL 없음. SW styles export 미해석 `ENTRY-002`, 패키지 graph 밖 `SCOPE-001`, 연결에 의존한 Token/동적 값 미검증이 남았습니다. Guard를 수정하거나 PASS로 덮어쓰지 않았습니다.
- Guard 원본 styleEvidence는 STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED이며 checks=[]입니다. SW 실검사 완료라는 뜻은 아닙니다. 별도 Runtime은 위 Chrome 범위 PASS, 디자인 의도는 GIF 프레임 좌표·순서 및 Figma 자산·Tooltip 규격 범위에서 확인했고, 전체 검증은 호스트 등의 미검증 때문에 PARTIAL입니다.
## 10. 변경 파일 · 근거
1. `packages/sw/src/theme/sw.css` — 창 이동 모션 블록만 수정.
2. `src/sw/sw-header.stories.tsx` — 기존 모션 Story 블록만 갱신.
3. `docs/sw/sw-header-design-spec.md` — 과거 §11.3을 보존하고 현재 GIF 기준 §11.4 추가.
4. `docs/sw/sw-header-usage.md` — 현재 모션·Drag·반복·Story 사용법 반영.
5. `packages/sw/README.md` — 현재 GIF 기준과 검증 링크 반영.
6. 이 검증 기록 — 신규 문서.
Git 무시 경로 `_local/sw-motion-gif-qa/`에 원본 분석 JSON, 전/후 41개 PNG 및 좌표 JSON, `frame-comparison.json`, 비교 이미지 `gif-before-after.png`, 실제 React 캡처 미리보기 `window-move-preview.gif`, 폭별 PNG, `storybook-final.png`, `verification.json`, build 로그, 원본 `guard.json`, 시작 해시와 사본을 보존합니다. GIF는 QA 미리보기일 뿐 제품 Header에 사용하지 않습니다.
## 11. 기존 디자인 · 작업 보호
모션 블록 밖 CSS와 기존 TSX/이벤트 연결이 시작 사본과 동일함을 확인했습니다. SVG·나머지 Story·기존 반응형·다른 Header 요소·root package.json/Lockfile·과거 검증 문서도 시작 해시와 일치합니다. 다른 패키지 소스·다른 Worktree·DWG/DXF 뷰어를 변경하지 않았습니다. Root의 기존 package build는 Git 무시 dist만 재생성했습니다. 의존성 설치는 없습니다.
## 12. 남은 항목
실제 Electron/설치형 호스트의 OS 창 이동·최소화·종료, 터치/펜, 다른 브라우저, 키보드 창 이동 대안과 스크린리더, 전용 SW Guard coverage는 NOT VERIFIED입니다. Tooltip의 기존 시각적 버튼 겹침은 명시한 채 유지했습니다.
## 13. 종료 Git 상태
브랜치 `feature/sw-package`, HEAD `9407c8567f57301659e591c03451cca1b828733c`를 유지합니다. 기존 status 그룹 ` M package-lock.json`, ` M package.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`도 유지하며 이번 변경은 그 미추적 그룹 내부에 포함됩니다. Index 변경·Commit·Push·PR·Merge·Publish·배포는 수행하지 않았습니다. `git diff --check`와 문서 링크/공백 검사를 확인했습니다.
작업 시간(KST, GIF 수신 후 실제 작업): Started At 2026-10-16 13:38:28 / Completed At 2026-10-16 13:52:16 / Actual Duration 13분 48초 / Estimated Duration 20~30분 / Variance 20분 대비 −6분 12초, 30분 대비 −16분 12초.
## 14. 후속 변경 — 1400ms 및 Composed Controls Hover
위 1.64초·41프레임 검증은 당시 결과로 보존합니다. 이후 사용자 요청으로 경로·Keyframes 비율을 유지한 채 주기만 1400ms로 변경했습니다. 현재 문구 등장 약 444ms, 이동 완료 약 888ms, 유지 약 512ms입니다. 원본 GIF의 시간과 다른 의도적인 변경입니다.
같은 후속 작업에서 그룹 필드의 Hover 텍스트 및 지정 선/폰트/치수 미리보기를 `#FFC600`으로 추가 강조했습니다. 검정 외곽선과 기존 Gradient Hover Border·색상 견본·레이어 상태를 유지하고 상세보기는 제외했습니다. 이 변경의 새로운 검증 결과와 미검증 범위는 [속도·Composed Hover 통합 검증](./sw-header-speed-composed-hover-verification-2026-10-16.md)에 별도로 기록합니다. 위 과거 수치와 판정을 소급 변경하지 않습니다.
@@ -0,0 +1,85 @@
# SW Header 창 이동 Motion 최종 검증 — 2026-10-16
## 1. 최종 판정: PARTIAL
요청 범위의 Chrome 브라우저 검증과 빌드는 통과했습니다. Reduced Motion의 실제 CSS 오류 한 건을 수정하고 영향 범위를 재검증했습니다. 실제 호스트 창 제어, Figma player의 반복 정책·수치 곡선 동등성, 전용 SW Guard 검증은 미완료이므로 전체 PASS로 처리하지 않습니다.
## 2. 작업 시작 시 Git 상태
- 경로: `D:\디자인시스템\HMDesign-SW`, 브랜치 `feature/sw-package`.
- HEAD: `9407c8567f57301659e591c03451cca1b828733c`.
- origin: `https://gitea.hmac.kr/b25027/HMDesign.git`.
- 기존 변경: ` M package-lock.json`, ` M package.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`. SW는 미추적 상태이므로 일반 `git diff`만으로 기존 구현과 이번 작업을 구분할 수 없습니다.
- 이 작업은 사용자 지정 SW Worktree에서만 수행했고 Git 이력·Index·원격을 변경하지 않았습니다. 시작 파일의 SHA-256은 `_local/sw-motion-qa/baseline.json`에 보존했습니다.
## 3. 기존 Claude 구현에서 확인한 사항
`SWHeaderWindowControls.tsx`의 기존 div / `onPointerDown`, 3개 SVG, 4개 CSS Keyframes, 직접 선언된 `linear()` easing, 1회 재생·즉시 이탈 복귀, Tooltip, 88px / 24px / 숨김 단계, `Window Move Motion` Story와 7개 고정 프레임이 실제로 존재했습니다. 300ms의 Hover 창·커서·Tooltip opacity는 `0.930667`로 측정했습니다. 모션과 Drag 코드를 다시 작성하지 않았습니다.
## 4. 이번 추가 수정
기존 Reduced Motion 규칙 `.hm-sw-header__motion:hover > *`보다 레이어별 애니메이션 선택자의 specificity가 높아 모션 해제가 적용되지 않았습니다. 이 규칙만 동일 specificity의 `:is(.레이어...)` 선택자로 바꿨습니다. 일반 Keyframes·시간·SVG·좌표·공개 Props·Callback·Breakpoint는 그대로입니다. 나머지 수정은 문서 추가입니다.
초기 검증 JSON의 두 FAIL은 보존했습니다. 수정 후 해당 Reduced Motion 검사와 영향 범위만 별도로 재검사했으며, 성공했던 일반 모션·반응형 검사를 형식적으로 반복하지 않았습니다.
## 5. Figma 3단계 비교
Figma `rj6wpsfk24lOWvWiVRAANP` / `26:10220`의 디자인 컨텍스트·스크린샷·Prototype reactions를 읽었습니다. Default `26:10221`, Hover `26:10258`, Hover2 `26:10295`의 크기와 좌표는 [명세 §11.3](./sw-header-design-spec.md#113-창-이동-hover-motion--figma-2610220-2026-10-16)에 기록했습니다.
- PASS: 1022ms의 창 (32, 15), 24 × 18.857px; 1767ms의 창 (59, 12), 커서 (72, 10), Tooltip (89, 11). CSS subpixel 오차 0.05px 이내로 비교했습니다.
- PASS: GENTLE / QUICK / BOUNCY preset 이름, SMART_ANIMATE / DISSOLVE, 1.0220938 / 0.7442555 / 0.9582165초와 두 1ms 대기는 원본 reactions와 일치합니다. QUICK overshoot, DISSOLVE 혼합, 실제 1회 재생 완료·Default 복귀도 확인했습니다.
- PASS: Hover SVG 두 개는 원본과 일치하며 Default SVG는 마스크 ID 차이만 있습니다. 원본 파일을 교체하지 않았습니다.
- PARTIAL: Figma motion API의 animated-node 결과는 비어 있었고 preset의 수치 곡선은 반환되지 않았습니다. 기존 spring 표본 근사와 opacity용 단조 BOUNCY를 유지했으며, 중간 프레임의 완전 동일성은 검증하지 않았습니다. Tooltip 글꼴은 기존 SW 정책인 Noto Sans KR이며 Figma의 Inter와 다릅니다.
- NOT VERIFIED: Figma Prototype player에서 Default 복귀 뒤 포인터 유지 중 자동 재시작하는지. 구현은 1회 재생 정책을 유지했습니다.
## 6. Drag 검증
공개 `@hm-design/sw` 패키지를 실제 React로 렌더링한 로컬 QA fixture에서 PointerEvent를 받았습니다. `onWindowMoveStart` 1회, `currentTarget`과 `target`이 기존 div임, `pointerId`·`pointerType=mouse`·`buttons=1` 전달, `defaultPrevented=false`를 확인했습니다. 호스트 모의 코드가 Pointer Capture를 설정한 뒤 Tooltip·최소화 영역 위로 이동해도 pointermove와 pointerup이 전달됐습니다. Drag 중 최소화·종료 콜백이 발생하지 않았고, 이후 각 버튼 클릭은 자신의 콜백만 호출했습니다. Reduced Motion에서도 같은 전달을 확인했습니다.
실제 Electron/설치형 호스트의 OS 창 이동·최소화·종료, 터치·펜 입력과 네이티브 Drag 중 애니메이션은 **NOT VERIFIED**입니다. 브라우저 검증은 호스트 통합 완료 판정이 아닙니다.
## 7. Tooltip · Reduced Motion · Focus
- PASS: Tooltip과 내부 그래픽의 `pointer-events: none`; Tooltip이 겹친 위치의 hit test는 최소화 버튼에 도달합니다. 실제 버튼 콜백도 전달됩니다.
- PASS: Hover 종료 즉시 기본 상태; 빠른 진입·이탈 12회에서 start/cancel 각각 12회, 잔여 애니메이션 0개; 포인터를 4초 이상 유지해도 비정상 반복 없음.
- PASS(수정 후): reduce에서 애니메이션 0개, 기본 아이콘과 정적 Tooltip 유지, 이탈 시 숨김, 재생 중 reduce 전환 즉시 정지. 24px 영역은 기존대로 그림을 숨기고 Tooltip만 표시합니다.
- PASS: 최소화 → 종료 Tab 이동, 접근성 이름과 2px Focus 윤곽선, Enter로 종료 콜백.
- 기존 제한: 창 이동 div는 `role=presentation`, `tabIndex=-1`이며 키보드 이동·Focus Tooltip을 제공하지 않습니다. 키보드 이동 대안과 보조기술 검증은 호스트 과제입니다. 접근성 전체 PASS를 선언하지 않습니다.
## 8. 반응형
실제 viewport와 독립 Header 폭으로 1920 / 1440 / 1280(추가) / 1080 / 1079 / 768 / 640 / 639 / 390px를 검사했습니다. 모든 폭에서 높이 49px, 문서와 Header의 가로 overflow 0px, Tooltip은 Header의 세로·가로 경계 안이었습니다. 1080px 이상 move 88px, 640–1079px move 24px와 정적 Tooltip, 639px 이하 숨김을 확인했습니다.
Tooltip은 원본 좌표 관계상 최소화와 종료 왼쪽 일부에 **시각적으로 겹칩니다**. 클릭을 차단하거나 가로 스크롤을 만들지는 않으나, 이를 ‘시각적 겹침 없음’으로 판정하지 않았고 승인된 배치를 임의로 바꾸지 않았습니다. 390px의 기존 문서명·레이어명 말줄임도 유지됩니다.
## 9. Typecheck · Build · Storybook · Guard
- PASS: `npm.cmd run typecheck` — root와 SW를 포함한 workspace typecheck. 기존 pretypecheck의 theme 확인·package build도 통과했습니다.
- PASS: `npm.cmd run build:sw` — CSS 수정 후 최종 산출물 생성.
- PASS: `npm.cmd --ignore-scripts run build`, `npm.cmd --ignore-scripts run build-storybook` — 이미 통과한 공통 prerequisite의 중복 실행을 생략했습니다. CSS 변경 후 관련 build 결과를 다시 생성했습니다. 의존성 설치·Lockfile 수정은 없습니다.
- PASS: 빌드된 Storybook의 실제 CSS animation, 단계별 7개 프레임 고정 시간, 기존 pointer/animation/callback log, 정상·reduce 모드. Chrome `155.0.8059.79`, headless mouse/keyboard에서 확인했습니다. 6024 개발 서버의 포트 불가 안내 뒤 기존 프로세스를 건드리지 않고 임시 localhost 서버를 사용했으며 종료했습니다.
- 기존 경고: 문서 예시 `bg-[var(...)]`의 CSS 최적화, Storybook의 `:host-context` minifier, 500kB 초과 chunk 및 plugin timing 안내. 관련 과거 문서에도 기록된 경고이며 이번 오류로 분류하지 않았습니다. runtime pageerror는 0건입니다. 초기 fixture에서 404 console 경고 1건이 있었고 최종 동일 fixture의 HTTP 오류 재확인에서는 0건이었습니다(초기 경고 URL은 기록하지 않아 원인 단정 안 함).
- Guard 1회: `node scripts/ai-generation-guard.mjs --target . --profile STORYBOOK --scope PARTIAL --files src/sw/sw-header.stories.tsx,packages/sw/src/theme/sw.css --json`. 원본 `status` / `staticStatus`는 **NOT VERIFIED**입니다. `@hm-design/sw/styles.css` 연결 미해석(`ENTRY-002`), 패키지 구현이 Storybook graph 밖(`SCOPE-001`), 해당 연결에 의존한 Token/동적 값 미확인이 남았습니다. Guard를 변경하거나 원본 결과를 PASS로 덮어쓰지 않았습니다.
- Guard `styleEvidence`: STATIC PASS / RUNTIME NOT VERIFIED / DESIGN_INTENT PASS / FINAL NOT VERIFIED, `checks=[]`. SW CSS 실검사 완료를 뜻하지 않습니다. 별도 브라우저 Runtime은 위 범위 PASS, 디자인 의도는 정적 좌표 확인과 spring/반복 미검증을 합쳐 PARTIAL, 최종은 PARTIAL입니다.
## 10. 변경 파일
1. `packages/sw/src/theme/sw.css` — Reduced Motion 해제 선택자 수정.
2. `docs/sw/sw-header-design-spec.md` — §11.3 추가.
3. `docs/sw/sw-header-usage.md` — §10.2와 기존 Story 설명 보완.
4. `packages/sw/README.md` — 모션 사용·검증 링크 추가.
5. 이 검증 기록 — 신규 문서.
로컬 QA fixture·JSON·캡처·로그는 Git 무시 경로 `_local/sw-motion-qa/`에 있습니다. 원본 FAIL을 포함한 `verification.json`, 수정 후 `reduced-final.json`, 종합 `final-summary.json`, 원본 `guard.json`, `baseline.json`, build 로그와 폭별 PNG를 보존합니다. 이 파일들은 패키지 배포·공개 API에 포함하지 않습니다.
## 11. 기존 디자인 · 작업 보호
시작 해시와 비교해 기존 파일 중 변경된 것은 위 CSS·문서 4개뿐입니다. 새 선택자를 원래 한 줄로 되돌린 임시 사본의 SHA-256이 시작 CSS와 일치하여, CSS의 다른 바이트는 보존됨을 확인했습니다. 기존 TSX·SVG·Story·package.json·Lockfile은 시작 상태와 일치합니다. 다른 패키지의 소스·DWG/DXF 뷰어·다른 Worktree는 수정하지 않았습니다. Root의 기존 prerequisite build에 의해 Git 무시 dist는 재생성됐습니다.
## 12. 남은 문제 · NOT VERIFIED
실제 네이티브 창 제어, Figma player 반복 및 수치 easing의 완전 동등성, `linear()` 미지원 브라우저, 다른 브라우저·터치·펜·스크린리더, 키보드 창 이동 대안, 전용 SW Guard coverage는 미검증/후속 범위입니다. Tooltip의 시각적 버튼 겹침은 위와 같이 명시하며 변경하지 않았습니다.
## 13. 작업 종료 시 Git 상태
브랜치와 HEAD는 시작 상태를 유지합니다. 변경 그룹도 ` M package-lock.json`, ` M package.json`, `?? docs/sw/`, `?? packages/sw/`, `?? src/sw/`이며, 기존 미추적 그룹 내부에 이번 CSS·문서 변경이 포함됩니다. Stage·Commit·Push·PR·Merge·Publish·배포는 수행하지 않았습니다. Git 상태만으로 이번 변경이 0개라는 뜻은 아니며 파일별 시작 해시 비교를 함께 사용합니다.
+13
View File
@@ -14,6 +14,7 @@
"@hm-design/center-graphic": "*", "@hm-design/center-graphic": "*",
"@hm-design/org-chart": "*", "@hm-design/org-chart": "*",
"@hm-design/org-chart-react": "*", "@hm-design/org-chart-react": "*",
"@hm-design/sw": "*",
"@hm-design/ui": "*", "@hm-design/ui": "*",
"lucide-react": "^1.49.0", "lucide-react": "^1.49.0",
"react": "19.3.0", "react": "19.3.0",
@@ -1218,6 +1219,10 @@
"resolved": "packages/org-chart-react", "resolved": "packages/org-chart-react",
"link": true "link": true
}, },
"node_modules/@hm-design/sw": {
"resolved": "packages/sw",
"link": true
},
"node_modules/@hm-design/ui": { "node_modules/@hm-design/ui": {
"resolved": "packages/ui", "resolved": "packages/ui",
"link": true "link": true
@@ -10718,6 +10723,14 @@
} }
} }
}, },
"packages/sw": {
"name": "@hm-design/sw",
"version": "0.1.0",
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"packages/ui": { "packages/ui": {
"name": "@hm-design/ui", "name": "@hm-design/ui",
"version": "0.1.0", "version": "0.1.0",
+4 -1
View File
@@ -31,16 +31,19 @@
"dev:org-chart-example": "node examples/org-chart/serve.mjs", "dev:org-chart-example": "node examples/org-chart/serve.mjs",
"test:org-chart-browser": "node examples/org-chart/verify.mjs", "test:org-chart-browser": "node examples/org-chart/verify.mjs",
"build:org-chart-react": "npm run build -w @hm-design/org-chart-react", "build:org-chart-react": "npm run build -w @hm-design/org-chart-react",
"build:sw": "npm run build -w @hm-design/sw",
"typecheck:sw": "npm run typecheck -w @hm-design/sw",
"typecheck:org-chart-react": "npm run typecheck -w @hm-design/org-chart-react", "typecheck:org-chart-react": "npm run typecheck -w @hm-design/org-chart-react",
"build:org-chart-react-example": "vite build --config examples/org-chart-react/vite.config.ts", "build:org-chart-react-example": "vite build --config examples/org-chart-react/vite.config.ts",
"dev:org-chart-react-example": "vite --config examples/org-chart-react/vite.config.ts", "dev:org-chart-react-example": "vite --config examples/org-chart-react/vite.config.ts",
"test:org-chart-react-browser": "node examples/org-chart-react/verify.mjs", "test:org-chart-react-browser": "node examples/org-chart-react/verify.mjs",
"build:packages": "npm run build:org-chart && npm run build:ui && npm run build:center-graphic && npm run build:org-chart-react" "build:packages": "npm run build:org-chart && npm run build:ui && npm run build:center-graphic && npm run build:org-chart-react && npm run build:sw"
}, },
"dependencies": { "dependencies": {
"@hm-design/org-chart": "*", "@hm-design/org-chart": "*",
"@hm-design/org-chart-react": "*", "@hm-design/org-chart-react": "*",
"@hm-design/center-graphic": "*", "@hm-design/center-graphic": "*",
"@hm-design/sw": "*",
"@hm-design/ui": "*", "@hm-design/ui": "*",
"lucide-react": "^1.49.0", "lucide-react": "^1.49.0",
"react": "19.3.0", "react": "19.3.0",
+81
View File
@@ -0,0 +1,81 @@
# @hm-design/sw
> **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).
> **후속 구현(2026-10-16):** 속성Bar(`SWHeaderPropertyBar`), 메뉴 단위 `controls` allowlist, Colorize 원본 Default/Hover를 추가했습니다. 상세 pill의 미사용 Stroke 선언·변수는 원본에서 제거했습니다. [사용법](../../docs/sw/sw-header-usage.md#후속-5개-요구사항-연결-2026-10-16), [새 검증 기록](../../docs/sw/sw-header-followup-five-verification-2026-10-16.md)을 따릅니다. 이전 설명과 PASS 로그는 보존합니다.
> **최신 추가 구현(2026-10-16):** 상세 5개 Hover·색상 기준 필수 선택·목록 Hover/Selected 통일·데이터 기반 속성창·K 가이드 반복을 제공합니다. 아래 추가 연결과 [현재 검증 보고](../../docs/sw/sw-header-additional-verification-2026-10-16.md)를 따릅니다. 아래의 이전 Hover 없음·일반 자기 색 Stroke 설명은 추가 기준으로 대체됩니다.
HM Design System의 SW(설치형·도면 SW) 전용 React 컴포넌트 패키지입니다. `@hm-design/ui`와 독립적으로 빌드·배포·사용하며 서로 의존하지 않습니다.
현재 포함: **SW Header** (Figma `rj6wpsfk24lOWvWiVRAANP` node `26:8944` `header_green`), `SWTheme`.
```tsx
import { SWHeader } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
<SWHeader documentTitle="C0020101-006-평면및종단면도(06)" logoSrc={productLogo} logoAlt="제품명" onClose={closeWindow} />
```
- 로고는 확정 디자인이 아닙니다. 로고 영역 배치는 공통이고 로고 이미지는 각 SW 프로그램이 `logoSrc` 또는 `logo`로 전달합니다. 패키지에는 로고가 없으며, 전달하지 않으면 로고 없이 표시됩니다.
- 파일명을 클릭하면 문서 Dropdown(Figma 26:10662)이 열립니다. 목록은 `documents`, 선택·New·Open은 Callback으로 각 프로그램이 연결합니다(`documents` 미전달 시 Dropdown 없음).
- 레이어 필드를 클릭하면 레이어 Dropdown(Figma 26:10940, 갈색 목록·주황 행 Hover)이 열립니다. 목록은 `layers`(`id`, `name`, `color`, `visible`, `frozen`, `locked`, `disabled`), 현재 레이어는 `activeLayerId`이며 선택은 `onLayerSelect`, 전구·눈꽃·자물쇠는 `onLayerVisibilityChange` / `onLayerFreezeChange` / `onLayerLockChange`, 하단 버튼은 `onLayerDetail`로 각 프로그램이 연결합니다. 아이콘은 데이터만 반영하며 실제 표시·동결·잠금은 호스트가 처리합니다(`layers` 미전달 시 기존 필드 버튼). 확정 매핑: 전구 컬러 `visible=true`·회색 `false`, 눈꽃 컬러 `frozen=false`·회색 `true`, 자물쇠 컬러 `locked=false`·회색 `true`이며 클릭 시 각 boolean의 반전값을 전달합니다([명세 §5.2](../../docs/sw/sw-header-design-spec.md#52-레이어-dropdown-figma-headermenu레이어-2610940-2026-10-16), [검증 기록](../../docs/sw/sw-header-layer-dropdown-verification-2026-10-16.md)).
- Hover는 Figma 홈 버튼의 노란 gradient 1px 테두리를 SW Header의 테두리 있는 버튼에 공통 적용합니다(디자인 명세 §11.1).
- 창 이동 Hover Motion은 GIF의 경로를 유지하고 후속 사용자 요청으로 1.64 → **1.4초**로 단축했습니다. 커서 상승 → 약 444ms 문구 등장 → 오른쪽 위로 이동 → 약 888ms부터 512ms 유지 → 반복입니다. 기존 SVG·줄무늬·Tooltip·`onWindowMoveStart`·Drag 중 정지·Reduced Motion·88px/24px/숨김 정책은 유지합니다. 실제 창 이동은 호스트 책임이며 [사용 가이드 §10.2](../../docs/sw/sw-header-usage.md#102-창-이동-hover-motion), [현재 검증 기록](../../docs/sw/sw-header-speed-composed-hover-verification-2026-10-16.md)을 따릅니다.
- 그룹 필드 Hover는 텍스트와 선/폰트/치수 미리보기를 `#FFC600`으로 표시합니다. 기존 Gradient Border·검정 외곽선·색상 견본·레이어 상태·Disabled/Selected 정책을 유지하며 상세보기는 기존 텍스트 전용 Hover 그대로입니다([명세 §11.5](../../docs/sw/sw-header-design-spec.md#115-현재-추가-기준--1400ms-및-그룹-필드-hover-2026-10-16)).
- 색상·선스타일·문자스타일·치수스타일 Dropdown(2026-10-16, Figma 26:10986 / 26:11036 / 26:12666 / 26:12659 / 26:11104 / 26:10702 / 26:11181 / 26:11215): `colors`·`userColors`·`colorMode`, `lineTypes`·`lineWeights`, `textStyles`, `dimensionStyles`를 주면 각 필드가 Dropdown이 됩니다(주지 않으면 기존 버튼). 모든 항목은 `id` 기반 배열 Props이며 추가·삭제·이름/값 변경·재정렬에 소스 수정이 필요 없습니다. 선택·입력은 `onColorSelect` / `onColorModeChange` / `onColorInput` / `onColorPick`, `onLineTypeSelect` / `onLineWeightSelect` / `onLineWeightValueChange` / `onLineScaleChange`, `onTextStyleSelect`, `onDimensionStyleSelect`, 하단 `… 상세`는 `on*Detail`로 각 프로그램이 연결합니다(상세 팝업은 미제공). Dropdown 안 독립 버튼은 Hover 효과가 없고 목록 행·Input은 Figma Hover를 유지합니다([명세 §5.3](../../docs/sw/sw-header-design-spec.md#53-색상--선스타일--문자스타일--치수스타일-dropdown-2026-10-16), [사용 가이드 §7](../../docs/sw/sw-header-usage.md#색상--선문자치수스타일-dropdown-예-2026-10-16)).
```tsx
<SWHeader
documentTitle={fileName}
color={color} colors={palette} userColors={userColors} colorMode={colorMode}
onColorSelect={option => applyColor(option)} onColorModeChange={setColorMode} onColorInput={({ hex }) => applyHex(hex)}
lineStyle={lineStyle} lineTypes={lineTypes} lineWeights={lineWeights}
onLineTypeSelect={type => setLineStyle(s => ({ ...s, name: type.name }))}
textStyle={textStyle} textStyles={textStyles} onTextStyleSelect={style => setTextStyle(style)}
dimensionStyle={dimensionStyle} dimensionStyles={dimensionStyles} onDimensionStyleSelect={style => setDimensionStyle(style)}
/>
```
- 반응형 More: 좁은 헤더에서 더보기의 필드를 고르면 그 필드가 첫 칸(레이어 자리)에 표시되고 Dropdown이 바로 아래 열립니다. 레이어는 더보기에서 다시 고를 수 있으며, 전환은 UI 상태라 데이터 Callback을 호출하지 않습니다([명세 §5.4](../../docs/sw/sw-header-design-spec.md#54-반응형-more--대표-필드-전환-2026-10-16)).
- Text Stroke: Figma에 Stroke가 있는 텍스트만 검정 외곽선(14·16px 1.3px, 12px 1px) + 자기 색 0.3px를 표시합니다. 더보기·Tooltip·`+ New`/`Open`은 제외([명세 §6.1](../../docs/sw/sw-header-design-spec.md#61-text-stroke-2026-10-16)).
- `SWHeaderInput`: 레이어 상세 등 상세 영역에서 쓰는 수정 가능 Input(Figma 26:11656, Default / Hover / 편집, caret `#E60403`). `value`(확정값) · `onValueChange`(임시값) · `onCommit`(Enter·Focus 이동, 유효 + 변경 시) · `onCancel`(Escape) · `valueType`(`text` / `number`) · `parse` · `readOnly` · `disabled`([명세 §5.5](../../docs/sw/sw-header-design-spec.md#55-레이어-상세-버튼--수정-가능한-input-2026-10-16)).
- Peer: `react`, `react-dom` `^19.0.0`. 런타임 dependency는 없습니다.
- 출력: `dist/index.js`(ESM, Figma SVG asset은 data URI로 포함, 임시 Figma URL 없음), `dist/*.d.ts`, `dist/styles.css`.
- CSS는 자동 import되지 않습니다. `@hm-design/sw/styles.css`를 한 번 import합니다.
- Registry에는 아직 게시하지 않았습니다. 설치·사용·Props는 [사용 가이드](../../docs/sw/sw-header-usage.md), 디자인 기준은 [디자인 명세](../../docs/sw/sw-header-design-spec.md)를 따릅니다.
```bash
npm run build -w @hm-design/sw
npm run typecheck -w @hm-design/sw
```
## 추가 연결: 속성창·K 가이드 (2026-10-16)
- `SWHeaderPropertyPanel` 및 property/group/selection 타입을 공개합니다. `SWHeader.propertyPanel`에 외부 데이터를 전달하면 기존 속성창 버튼·More와 연결됩니다.
- `propertyPanelOpen/onPropertyPanelOpenChange`로 Controlled 열림을 지원하며, 생략하면 내부 열림 상태를 사용합니다. 기존 `onPropertyPanelToggle`을 보존합니다.
- Text/Number/Select/Color/Slider/Computed, id 기반 동적 항목, 검증·확정·취소·외부 변경·ReadOnly/Disabled·그룹 펼침을 지원합니다. CAD 계산·선택·객체 변경은 호스트가 담당합니다.
- 상세 5개는 Figma 고정 크기와 Hover를 적용합니다. ByLayer/ByBlock은 정확히 하나가 표시되며, 기존 null 직접 색상 의미는 보존합니다. 레이어/선/문자/치수 목록은 Hover와 Selected 디자인이 같습니다.
- K는 기존 버튼을 재사용하고 원본 이미지 Pan/Zoom을 반복합니다. 640px 미만에서는 기존 More 항목에서도 같은 가이드를 표시합니다. Reduced Motion은 정적 이미지이며 기존 창 이동 1400ms는 유지합니다.
- 지정된 일반 text-stroke-width 선언·변수는 제거했고 검정 text-shadow와 상세 pill의 별도 Stroke는 보존했습니다.
- 사용 예시와 API는 [가이드 추가 절](../../docs/sw/sw-header-usage.md#속성창k-가이드-추가-연결-2026-10-16), 디자인 값은 [명세 추가 절](../../docs/sw/sw-header-design-spec.md#추가-수정-기준--속성창k-가이드-2026-10-16)을 확인하세요.
- 검증: 기존 17 + 신규 4 Story, [현재 실행 결과 및 NOT VERIFIED](../../docs/sw/sw-header-additional-verification-2026-10-16.md). 과거 로그는 보존합니다.
## 후속 5개 요구사항 API (2026-10-16)
- `SWHeaderPropertyBar` / `SWHeaderPropertyBarProps`를 공개합니다. `propertyBar`로 selection/properties/onPropertyChange와 readOnly/disabled를 전달합니다. 속성창과 SWHeaderProperty/Selection 데이터를 공유하며, `propertyBarOpen/defaultPropertyBarOpen/onPropertyBarOpenChange`는 별도의 표시 상태입니다. 최대 폭 1222·높이 28px이고 Header 49px 아래에 표시됩니다.
- `SWHeaderControlId` / `SWHeaderControlConfig` / `SWHeader.controls`는 메뉴 자체의 전체 allowlist입니다. undefined는 기존 기본 구성, []는 선택적 컨트롤 없음입니다. id/visible/label과 배열 순서로 제거·복원·변경·재배치하며 More도 동일 구성을 따릅니다. 내부 옵션 배열·선택값 Props는 그대로입니다.
- Text Stroke Story의 지정 원본 이미지 블록·전용 import를 제거했습니다. 원본 PNG와 다른 비교 Story는 보존합니다.
- 상세 span의 `-webkit-text-stroke-width`와 미사용 `--hm-sw-header-solid-stroke-width`를 src/theme/sw.css에서 제거했습니다. build는 이 원본에서 dist/styles.css를 만드므로 dist만 편집하지 않습니다. 검정 text-shadow·SVG Stroke·상세 Hover는 보존합니다.
- 색상 추출 버튼은 로컬 Figma Default/Hover SVG를 사용합니다(24px/18px). 기존 `onColorPick`은 그대로이며 Hover는 Callback을 실행하지 않습니다. `26:11338` 돋보기 Overlay는 호스트 연동 대상입니다.
- Storybook은 기존 21개와 신규 Property Bar / Dynamic Controls / Colorize, 총 24개입니다. [최신 검증 기록](../../docs/sw/sw-header-followup-five-verification-2026-10-16.md)을 참조하세요.
## 3차 API (2026-10-16)
- `SWHeader.hiddenControls?: readonly SWHeaderHideableControlId[]` — `SWHeaderControlId` + `SWHeaderFrameControlId`(`home` / `windowMove` / `minimize` / `close`). 로고는 항상 표시하며 `home` 제거 시 정적 로고입니다. 제거한 요소는 공간·Popup·Callback이 없습니다.
- `SWHeader.menuOrder?: readonly SWHeaderMenuId[]` — 5개 유사 메뉴끼리만 순서 변경. 지정 id 우선, 누락 id는 기본 순서로 뒤에, 중복·알 수 없는 id 무시. More 목록도 같은 순서입니다.
- `SWHeaderPropertyPanelProps.shapeContent?: SWHeaderPropertyShapeContent` — `undefined` Figma 원형, `null`/`false` 빈 208×208 영역, ReactNode 또는 `(context: SWHeaderPropertyShapeContext) => ReactNode`. `SWHeader.propertyPanel`로도 전달됩니다.
- 하위 공개 컴포넌트에 선택적 표시 Props 추가(기본 true): `SWHeaderHome.interactive`, `SWHeaderProperties.showCaption/showBar/showPanel`, `SWHeaderActions.showPrint/showPopupGuide`, `SWHeaderWindowControls.showWindowMove/showMinimize/showClose`.
- Storybook: 기존 24개 + Composition, Property Panel 개별 4개, 4개 비교, 형상 Slot = 31개.
+57
View File
@@ -0,0 +1,57 @@
// Self-contained library build for @hm-design/sw (no shared script, no @hm-design/ui).
import { readFile, readdir, rm, writeFile } from "node:fs/promises"
import { existsSync } from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { execFileSync } from "node:child_process"
import { build } from "vite"
const packageRoot = fileURLToPath(new URL("./", import.meta.url))
const output = path.join(packageRoot, "dist")
const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"))
const externals = Object.keys({ ...manifest.dependencies, ...manifest.peerDependencies })
await rm(output, { recursive: true, force: true })
await build({
configFile: false,
root: packageRoot,
publicDir: false,
build: {
target: "es2022",
outDir: output,
minify: false,
// Figma SVG assets are inlined as data URIs: the package needs no asset path resolution.
assetsInlineLimit: Number.MAX_SAFE_INTEGER,
lib: {
entry: { index: path.join(packageRoot, "src/index.ts") },
formats: ["es"],
fileName: (_format, entry) => `${entry}.js`,
cssFileName: "styles",
},
rolldownOptions: {
external: id => externals.some(dep => id === dep || id.startsWith(`${dep}/`)),
},
},
})
execFileSync(process.execPath, [path.join(packageRoot, "../../node_modules/typescript/bin/tsc"), "-p", path.join(packageRoot, "tsconfig.build.json")], { stdio: "inherit" })
// Declarations describe APIs, not stylesheet loading; explicit .js specifiers also support NodeNext.
async function finishDeclarations(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const file = path.join(directory, entry.name)
if (entry.isDirectory()) await finishDeclarations(file)
else if (entry.name.endsWith(".d.ts")) {
const text = (await readFile(file, "utf8"))
.replace(/^import\s+["'][^"']+\.css["'];?\s*$/gm, "")
.replace(/(from\s+["']|import\(["'])(\.\.?\/[^"']+)(["'])/g, (_match, before, specifier, after) => {
const target = path.resolve(directory, specifier)
const resolved = existsSync(`${target}.d.ts`) ? specifier
: existsSync(path.join(target, "index.d.ts")) ? `${specifier}/index` : specifier
return `${before}${resolved.endsWith(".js") ? resolved : `${resolved}.js`}${after}`
})
await writeFile(file, text)
}
}
}
await finishDeclarations(output)
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@hm-design/sw",
"version": "0.1.0",
"private": false,
"description": "HM Design System SW (EasyBeam-style desktop software) components. Independent of @hm-design/ui.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./styles.css": "./dist/styles.css"
},
"files": [
"dist"
],
"scripts": {
"typecheck": "tsc -p tsconfig.json",
"build": "node build.mjs",
"prepack": "npm run build"
},
"sideEffects": [
"**/*.css"
],
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
+9
View File
@@ -0,0 +1,9 @@
declare module "*.svg" {
const url: string
export default url
}
declare module "*.png" {
const url: string
export default url
}
declare module "*.css"
+6
View File
@@ -0,0 +1,6 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="32" height="48" viewBox="0 0 32 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="header/btn_&#236;&#162;&#133;&#235;&#163;&#140;">
<rect width="32" height="48" fill="#E40000"/>
<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M16 22.5854L20.5 18.0854L21.9142 19.4996L17.4142 23.9996L21.915 28.5004L20.5008 29.9146L16 25.4138L11.4992 29.9146L10.085 28.5004L14.5858 23.9996L10.0858 19.4996L11.5 18.0854L16 22.5854Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 544 B

+5
View File
@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="32" height="48" viewBox="0 0 32 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="header/btn_&#236;&#162;&#133;&#235;&#163;&#140;">
<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M16 22.5854L20.5 18.0854L21.9142 19.4996L17.4142 23.9996L21.915 28.5004L20.5008 29.9146L16 25.4138L11.4992 29.9146L10.085 28.5004L14.5858 23.9996L10.0858 19.4996L11.5 18.0854L16 22.5854Z" fill="#ADB0AF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 500 B

+5
View File
@@ -0,0 +1,5 @@
<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="colorize" opacity="0.7">
<path id="Vector" d="M13.245 4.0575L13.935 4.7475L11.9175 6.765L11.2275 6.075L13.245 4.0575ZM13.2525 2.25C13.0575 2.25 12.87 2.325 12.72 2.4675L10.38 4.8075L8.9325 3.375L7.875 4.4325L8.94 5.4975L2.25 12.1875V15.75H5.8125L12.5025 9.06L13.5675 10.125L14.625 9.0675L13.185 7.6275L15.525 5.2875C15.825 4.9875 15.825 4.515 15.5325 4.2225L13.7775 2.4675C13.6275 2.325 13.44 2.25 13.2525 2.25ZM8.19 11.25L6.75 9.81L9.795 6.765L11.235 8.205L8.19 11.25Z" fill="black"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 671 B

@@ -0,0 +1,5 @@
<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="colorize" opacity="0.7">
<path id="Vector" d="M13.245 4.0575L13.935 4.7475L11.9175 6.765L11.2275 6.075L13.245 4.0575ZM13.2525 2.25C13.0575 2.25 12.87 2.325 12.72 2.4675L10.38 4.8075L8.9325 3.375L7.875 4.4325L8.94 5.4975L2.25 12.1875V15.75H5.8125L12.5025 9.06L13.5675 10.125L14.625 9.0675L13.185 7.6275L15.525 5.2875C15.825 4.9875 15.825 4.515 15.5325 4.2225L13.7775 2.4675C13.6275 2.325 13.44 2.25 13.2525 2.25ZM8.19 11.25L6.75 9.81L9.795 6.765L11.235 8.205L8.19 11.25Z" fill="black"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 671 B

@@ -0,0 +1,8 @@
<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="colorize">
<g id="Vector">
<path d="M13.245 4.0575L13.935 4.7475L11.9175 6.765L11.2275 6.075L13.245 4.0575ZM13.2525 2.25C13.0575 2.25 12.87 2.325 12.72 2.4675L10.38 4.8075L8.9325 3.375L7.875 4.4325L8.94 5.4975L2.25 12.1875V15.75H5.8125L12.5025 9.06L13.5675 10.125L14.625 9.0675L13.185 7.6275L15.525 5.2875C15.825 4.9875 15.825 4.515 15.5325 4.2225L13.7775 2.4675C13.6275 2.325 13.44 2.25 13.2525 2.25ZM8.19 11.25L6.75 9.81L9.795 6.765L11.235 8.205L8.19 11.25Z" fill="#FF5C00"/>
<path d="M13.2529 1.75C13.5348 1.7501 13.8066 1.8486 14.0293 2.02441L14.1221 2.10547L15.8857 3.86914C16.3766 4.36002 16.3705 5.14859 15.8789 5.64062L13.8916 7.62695L15.332 9.06738L13.5674 10.832L12.502 9.7666L6.16602 16.1035L6.01953 16.25H1.75V11.9805L1.89648 11.834L8.23242 5.49707L7.16797 4.43262L8.93066 2.66992L9.28418 3.01953L10.3779 4.10156L12.3662 2.11426L12.376 2.10449V2.10547C12.6158 1.87768 12.9255 1.75 13.2529 1.75ZM7.45703 9.80957L8.19043 10.543L10.5283 8.20508L9.79492 7.47168L7.45703 9.80957Z" stroke="black" stroke-opacity="0.4"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+30
View File
@@ -0,0 +1,30 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="37" height="19" viewBox="0 0 37 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_dualmonitor" filter="url(#filter0_d_0_121)">
<g id="Union">
<mask id="path-1-inside-1_0_121" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 14V13H18V4H4V13H10V14H8V15H10H12H14V14H12Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 14V13H18V4H4V13H10V14H8V15H10H12H14V14H12Z" fill="white"/>
<path d="M12 13V12H11V13H12ZM12 14H11V15H12V14ZM18 13V14H19V13H18ZM18 4H19V3H18V4ZM4 4V3H3V4H4ZM4 13H3V14H4V13ZM10 13H11V12H10V13ZM10 14V15H11V14H10ZM8 14V13H7V14H8ZM8 15H7V16H8V15ZM14 15V16H15V15H14ZM14 14H15V13H14V14ZM12 13H11V14H12H13V13H12ZM18 13V12H12V13V14H18V13ZM18 4H17V13H18H19V4H18ZM4 4V5H18V4V3H4V4ZM4 13H5V4H4H3V13H4ZM10 13V12H4V13V14H10V13ZM10 14H11V13H10H9V14H10ZM8 14V15H10V14V13H8V14ZM8 15H9V14H8H7V15H8ZM10 15V14H8V15V16H10V15ZM12 15V14H10V15V16H12V15ZM14 15V14H12V15V16H14V15ZM14 14H13V15H14H15V14H14ZM12 14V15H14V14V13H12V14Z" fill="#4B7165" mask="url(#path-1-inside-1_0_121)"/>
</g>
<g id="Union_2">
<mask id="path-3-inside-2_0_121" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27 14V13H33V4H19V13H25V14H23V15H25H27H29V14H27Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M27 14V13H33V4H19V13H25V14H23V15H25H27H29V14H27Z" fill="#FFE500"/>
<path d="M27 13V12H26V13H27ZM27 14H26V15H27V14ZM33 13V14H34V13H33ZM33 4H34V3H33V4ZM19 4V3H18V4H19ZM19 13H18V14H19V13ZM25 13H26V12H25V13ZM25 14V15H26V14H25ZM23 14V13H22V14H23ZM23 15H22V16H23V15ZM29 15V16H30V15H29ZM29 14H30V13H29V14ZM27 13H26V14H27H28V13H27ZM33 13V12H27V13V14H33V13ZM33 4H32V13H33H34V4H33ZM19 4V5H33V4V3H19V4ZM19 13H20V4H19H18V13H19ZM25 13V12H19V13V14H25V13ZM25 14H26V13H25H24V14H25ZM23 14V15H25V14V13H23V14ZM23 15H24V14H23H22V15H23ZM25 15V14H23V15V16H25V15ZM27 15V14H25V15V16H27V15ZM29 15V14H27V15V16H29V15ZM29 14H28V15H29H30V14H29ZM27 14V15H29V14V13H27V14Z" fill="#264F42" mask="url(#path-3-inside-2_0_121)"/>
</g>
</g>
<defs>
<filter id="filter0_d_0_121" x="0" y="-1" width="37" 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 0.6 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_121"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_121" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="6.06218" height="3" viewBox="0 0 6.06218 3" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Polygon 352" d="M3.03109 0L6.06218 3H0L3.03109 0Z" fill="#47AF91"/>
</svg>

After

Width:  |  Height:  |  Size: 259 B

+32
View File
@@ -0,0 +1,32 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="92" height="72" viewBox="0 0 92 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Rectangle 7508" filter="url(#filter0_d_0_10)">
<path d="M8 12H76L61.8234 54.5298C60.7345 57.7966 57.6774 60 54.2339 60H8V12Z" fill="url(#paint0_linear_0_10)"/>
<path d="M75.3057 12.5L61.3486 54.3721C60.3277 57.4342 57.4623 59.4998 54.2344 59.5H8.5V12.5H75.3057Z" stroke="url(#paint1_linear_0_10)"/>
</g>
<defs>
<filter id="filter0_d_0_10" x="0" y="0" width="92" height="72" 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 dx="4"/>
<feGaussianBlur stdDeviation="6"/>
<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.9 0"/>
<feBlend mode="multiply" in2="BackgroundImageFix" result="effect1_dropShadow_0_10"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_10" result="shape"/>
</filter>
<linearGradient id="paint0_linear_0_10" x1="8" y1="60.0004" x2="8" y2="12" gradientUnits="userSpaceOnUse">
<stop stop-color="#1F3F36"/>
<stop offset="0.09" stop-color="#0A2C22"/>
<stop offset="0.15" stop-color="#0D392C"/>
<stop offset="0.88" stop-color="#0E3F31"/>
<stop offset="0.97" stop-color="#0A2C22"/>
</linearGradient>
<linearGradient id="paint1_linear_0_10" x1="42" y1="12" x2="42" y2="60" gradientUnits="userSpaceOnUse">
<stop stop-color="#A67D03"/>
<stop offset="0.164109" stop-color="#F2CC0F"/>
<stop offset="0.371922" stop-color="#FADD4F"/>
<stop offset="0.620285" stop-color="#F2CC0F"/>
<stop offset="0.878785" stop-color="#926E03"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+31
View File
@@ -0,0 +1,31 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="92" height="72" viewBox="0 0 92 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Rectangle 7508" filter="url(#filter0_d_0_2957)">
<path d="M8 12H76L61.8234 54.5298C60.7345 57.7966 57.6774 60 54.2339 60H8V12Z" fill="url(#paint0_linear_0_2957)"/>
<path d="M75.3057 12.5L61.3486 54.3721C60.3277 57.4342 57.4623 59.4998 54.2344 59.5H8.5V12.5H75.3057Z" stroke="url(#paint1_linear_0_2957)"/>
</g>
<defs>
<filter id="filter0_d_0_2957" x="0" y="0" width="92" height="72" 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 dx="4"/>
<feGaussianBlur stdDeviation="6"/>
<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.9 0"/>
<feBlend mode="multiply" in2="BackgroundImageFix" result="effect1_dropShadow_0_2957"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_2957" result="shape"/>
</filter>
<linearGradient id="paint0_linear_0_2957" x1="8" y1="60.0004" x2="8" y2="12" gradientUnits="userSpaceOnUse">
<stop stop-color="#1F3F36"/>
<stop offset="0.09" stop-color="#0A2C22"/>
<stop offset="0.15" stop-color="#0D392C"/>
<stop offset="0.88" stop-color="#0E3F31"/>
<stop offset="0.97" stop-color="#0A2C22"/>
</linearGradient>
<linearGradient id="paint1_linear_0_2957" x1="42" y1="12" x2="42" y2="60" gradientUnits="userSpaceOnUse">
<stop stop-color="#08251C"/>
<stop offset="0.370561" stop-color="#208769"/>
<stop offset="0.805" stop-color="#08241C"/>
<stop offset="1" stop-color="#051612"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="192" height="109" viewBox="0 0 192 109" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Rectangle 7349" width="192" height="109" fill="#D9D9D9"/>
</svg>

After

Width:  |  Height:  |  Size: 245 B

@@ -0,0 +1,12 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="192" height="109" viewBox="0 0 192 109" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Rectangle 7349" x="0.5" y="0.5" width="191" height="108" fill="black" stroke="url(#paint0_linear_0_14)"/>
<defs>
<linearGradient id="paint0_linear_0_14" x1="96" y1="0" x2="96" y2="109" gradientUnits="userSpaceOnUse">
<stop stop-color="#A67D03"/>
<stop offset="0.164109" stop-color="#F2CC0F"/>
<stop offset="0.371922" stop-color="#FADD4F"/>
<stop offset="0.620285" stop-color="#F2CC0F"/>
<stop offset="0.878785" stop-color="#926E03"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 647 B

@@ -0,0 +1,12 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="192" height="109" viewBox="0 0 192 109" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Rectangle 7349" x="0.5" y="0.5" width="191" height="108" fill="#D9D9D9" stroke="url(#paint0_linear_0_12)"/>
<defs>
<linearGradient id="paint0_linear_0_12" x1="96" y1="0" x2="96" y2="109" gradientUnits="userSpaceOnUse">
<stop stop-color="#A67D03"/>
<stop offset="0.164109" stop-color="#F2CC0F"/>
<stop offset="0.371922" stop-color="#FADD4F"/>
<stop offset="0.620285" stop-color="#F2CC0F"/>
<stop offset="0.878785" stop-color="#926E03"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ico_layer">
<path id="Union" opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M1 1H11V3H13V10V13H10H3V11H1V1ZM3 10H10V3V2H2V10H3Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 340 B

+5
View File
@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ico_layer">
<path id="Union" opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M1 1H11V3H13V10V13H10H3V11H1V1ZM3 10H10V3V2H2V10H3Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 339 B

@@ -0,0 +1,13 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Ellipse 419">
<circle cx="7" cy="7" r="6" fill="url(#paint0_radial_0_5)"/>
<circle cx="7" cy="7" r="6.5" stroke="black" stroke-opacity="0.5"/>
</g>
<defs>
<radialGradient id="paint0_radial_0_5" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7 5.28571) rotate(90) scale(8.14286)">
<stop stop-color="#E6E6E6"/>
<stop offset="0.440437" stop-color="#9C9C9C"/>
<stop offset="1" stop-color="#555555"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 635 B

+13
View File
@@ -0,0 +1,13 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Ellipse 419">
<circle cx="7" cy="7" r="6" fill="url(#paint0_radial_0_2958)"/>
<circle cx="7" cy="7" r="6.5" stroke="black" stroke-opacity="0.5"/>
</g>
<defs>
<radialGradient id="paint0_radial_0_2958" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7 5.28571) rotate(90) scale(8.14286)">
<stop stop-color="#FFE6BC"/>
<stop offset="0.440437" stop-color="#FAB846"/>
<stop offset="1" stop-color="#D38600"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 641 B

@@ -0,0 +1,13 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_&#235;&#136;&#136;&#234;&#189;&#131;">
<rect width="20" height="20" rx="2" fill="black" fill-opacity="0.25"/>
<g id="icon_&#235;&#136;&#136;&#234;&#189;&#131;_2">
<mask id="path-2-outside-1_0_74" maskUnits="userSpaceOnUse" x="1.5" y="1.5" width="17" height="17" fill="black">
<rect fill="white" x="1.5" y="1.5" width="17" height="17"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C9.72386 2.5 9.5 2.72386 9.5 3V4.79282L8.56066 3.85348C8.3654 3.65822 8.04882 3.65822 7.85355 3.85348C7.65829 4.04874 7.65829 4.36532 7.85355 4.56058L9.26777 5.9748C9.33429 6.04133 9.4149 6.08519 9.5 6.10638V8.78906L6.40824 5.6973C6.21298 5.50204 5.8964 5.50204 5.70113 5.6973C5.50587 5.89257 5.50587 6.20915 5.70113 6.40441L8.79672 9.5H6.10247C6.08128 9.4149 6.03742 9.33429 5.97089 9.26777L4.55668 7.85355C4.36142 7.65829 4.04483 7.65829 3.84957 7.85355C3.65431 8.04882 3.65431 8.3654 3.84957 8.56066L4.78891 9.5H3C2.72386 9.5 2.5 9.72386 2.5 10C2.5 10.2761 2.72386 10.5 3 10.5H4.61704L3.84957 11.2675C3.65431 11.4627 3.65431 11.7793 3.84957 11.9746C4.04483 12.1698 4.36142 12.1698 4.55668 11.9746L5.97089 10.5604C5.98993 10.5413 6.00712 10.5211 6.02244 10.5H8.79688L5.11429 14.1826C4.91903 14.3778 4.91903 14.6944 5.11429 14.8897C5.30955 15.085 5.62614 15.085 5.8214 14.8897L9.5 11.2111V13.7217C9.4149 13.7429 9.33429 13.7868 9.26777 13.8533L7.85355 15.2675C7.65829 15.4628 7.65829 15.7794 7.85355 15.9746C8.04882 16.1699 8.3654 16.1699 8.56066 15.9746L9.5 15.0353V17C9.5 17.2761 9.72386 17.5 10 17.5C10.2761 17.5 10.5 17.2761 10.5 17V15.2072L11.2675 15.9746C11.4627 16.1699 11.7793 16.1699 11.9746 15.9746C12.1698 15.7794 12.1698 15.4628 11.9746 15.2675L10.5604 13.8533C10.5413 13.8343 10.5211 13.8171 10.5 13.8018V11.2033L14.1864 14.8897C14.3817 15.085 14.6983 15.085 14.8935 14.8897C15.0888 14.6944 15.0888 14.3778 14.8935 14.1826L11.2109 10.5H13.8057C13.821 10.5211 13.8382 10.5413 13.8572 10.5604L15.2714 11.9746C15.4667 12.1698 15.7833 12.1698 15.9786 11.9746C16.1738 11.7793 16.1738 11.4627 15.9786 11.2675L15.2111 10.5H17C17.2761 10.5 17.5 10.2761 17.5 10C17.5 9.72386 17.2761 9.5 17 9.5H15.0392L15.9786 8.56066C16.1738 8.3654 16.1738 8.04882 15.9786 7.85355C15.7833 7.65829 15.4667 7.65829 15.2714 7.85355L13.8572 9.26777C13.7907 9.33429 13.7468 9.4149 13.7257 9.5H11.2111L14.3067 6.40441C14.5019 6.20915 14.5019 5.89257 14.3067 5.6973C14.1114 5.50204 13.7948 5.50204 13.5996 5.6973L10.5 8.79688V6.02635C10.5211 6.01102 10.5413 5.99384 10.5604 5.9748L11.9746 4.56058C12.1698 4.36532 12.1698 4.04874 11.9746 3.85348C11.7793 3.65822 11.4627 3.65822 11.2675 3.85348L10.5 4.62094V3C10.5 2.72386 10.2761 2.5 10 2.5Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C9.72386 2.5 9.5 2.72386 9.5 3V4.79282L8.56066 3.85348C8.3654 3.65822 8.04882 3.65822 7.85355 3.85348C7.65829 4.04874 7.65829 4.36532 7.85355 4.56058L9.26777 5.9748C9.33429 6.04133 9.4149 6.08519 9.5 6.10638V8.78906L6.40824 5.6973C6.21298 5.50204 5.8964 5.50204 5.70113 5.6973C5.50587 5.89257 5.50587 6.20915 5.70113 6.40441L8.79672 9.5H6.10247C6.08128 9.4149 6.03742 9.33429 5.97089 9.26777L4.55668 7.85355C4.36142 7.65829 4.04483 7.65829 3.84957 7.85355C3.65431 8.04882 3.65431 8.3654 3.84957 8.56066L4.78891 9.5H3C2.72386 9.5 2.5 9.72386 2.5 10C2.5 10.2761 2.72386 10.5 3 10.5H4.61704L3.84957 11.2675C3.65431 11.4627 3.65431 11.7793 3.84957 11.9746C4.04483 12.1698 4.36142 12.1698 4.55668 11.9746L5.97089 10.5604C5.98993 10.5413 6.00712 10.5211 6.02244 10.5H8.79688L5.11429 14.1826C4.91903 14.3778 4.91903 14.6944 5.11429 14.8897C5.30955 15.085 5.62614 15.085 5.8214 14.8897L9.5 11.2111V13.7217C9.4149 13.7429 9.33429 13.7868 9.26777 13.8533L7.85355 15.2675C7.65829 15.4628 7.65829 15.7794 7.85355 15.9746C8.04882 16.1699 8.3654 16.1699 8.56066 15.9746L9.5 15.0353V17C9.5 17.2761 9.72386 17.5 10 17.5C10.2761 17.5 10.5 17.2761 10.5 17V15.2072L11.2675 15.9746C11.4627 16.1699 11.7793 16.1699 11.9746 15.9746C12.1698 15.7794 12.1698 15.4628 11.9746 15.2675L10.5604 13.8533C10.5413 13.8343 10.5211 13.8171 10.5 13.8018V11.2033L14.1864 14.8897C14.3817 15.085 14.6983 15.085 14.8935 14.8897C15.0888 14.6944 15.0888 14.3778 14.8935 14.1826L11.2109 10.5H13.8057C13.821 10.5211 13.8382 10.5413 13.8572 10.5604L15.2714 11.9746C15.4667 12.1698 15.7833 12.1698 15.9786 11.9746C16.1738 11.7793 16.1738 11.4627 15.9786 11.2675L15.2111 10.5H17C17.2761 10.5 17.5 10.2761 17.5 10C17.5 9.72386 17.2761 9.5 17 9.5H15.0392L15.9786 8.56066C16.1738 8.3654 16.1738 8.04882 15.9786 7.85355C15.7833 7.65829 15.4667 7.65829 15.2714 7.85355L13.8572 9.26777C13.7907 9.33429 13.7468 9.4149 13.7257 9.5H11.2111L14.3067 6.40441C14.5019 6.20915 14.5019 5.89257 14.3067 5.6973C14.1114 5.50204 13.7948 5.50204 13.5996 5.6973L10.5 8.79688V6.02635C10.5211 6.01102 10.5413 5.99384 10.5604 5.9748L11.9746 4.56058C12.1698 4.36532 12.1698 4.04874 11.9746 3.85348C11.7793 3.65822 11.4627 3.65822 11.2675 3.85348L10.5 4.62094V3C10.5 2.72386 10.2761 2.5 10 2.5Z" fill="#00CCFF"/>
<path d="M9.5 4.79282L8.79289 5.49992C9.07889 5.78592 9.50901 5.87148 9.88268 5.7167C10.2564 5.56192 10.5 5.19728 10.5 4.79282H9.5ZM8.56066 3.85348L9.26777 3.14637L9.26777 3.14637L8.56066 3.85348ZM7.85355 3.85348L7.14645 3.14637L7.14645 3.14637L7.85355 3.85348ZM7.85355 4.56058L7.14645 5.26769L7.14645 5.26769L7.85355 4.56058ZM9.26777 5.9748L9.97487 5.26769V5.26769L9.26777 5.9748ZM9.5 6.10638H10.5C10.5 5.64719 10.1873 5.24701 9.74168 5.13603L9.5 6.10638ZM9.5 8.78906L8.79289 9.49617C9.07889 9.78217 9.50901 9.86772 9.88268 9.71294C10.2564 9.55816 10.5 9.19352 10.5 8.78906H9.5ZM5.70113 6.40441L6.40824 5.6973V5.6973L5.70113 6.40441ZM8.79672 9.5V10.5C9.20119 10.5 9.56582 10.2564 9.7206 9.88268C9.87538 9.50901 9.78983 9.07889 9.50383 8.79289L8.79672 9.5ZM6.10247 9.5L5.13212 9.74168C5.2431 10.1873 5.64328 10.5 6.10247 10.5V9.5ZM5.97089 9.26777L5.26378 9.97487V9.97487L5.97089 9.26777ZM4.55668 7.85355L5.26378 7.14645L5.26378 7.14645L4.55668 7.85355ZM3.84957 7.85355L3.14246 7.14645L3.14246 7.14645L3.84957 7.85355ZM3.84957 8.56066L3.14246 9.26777L3.14246 9.26777L3.84957 8.56066ZM4.78891 9.5V10.5C5.19337 10.5 5.55801 10.2564 5.71279 9.88268C5.86757 9.50901 5.78202 9.07889 5.49602 8.79289L4.78891 9.5ZM4.61704 10.5L5.32414 11.2071C5.61014 10.9211 5.6957 10.491 5.54092 10.1173C5.38614 9.74364 5.0215 9.5 4.61704 9.5V10.5ZM3.84957 11.2675L4.55668 11.9746L4.55668 11.9746L3.84957 11.2675ZM3.84957 11.9746L4.55668 11.2675L4.55668 11.2675L3.84957 11.9746ZM4.55668 11.9746L3.84957 11.2675L3.84957 11.2675L4.55668 11.9746ZM5.97089 10.5604L6.678 11.2675L6.678 11.2675L5.97089 10.5604ZM6.02244 10.5V9.5C5.70212 9.5 5.40119 9.65345 5.21307 9.91271L6.02244 10.5ZM8.79688 10.5L9.50398 11.2071C9.78998 10.9211 9.87554 10.491 9.72076 10.1173C9.56597 9.74364 9.20134 9.5 8.79688 9.5V10.5ZM5.11429 14.1826L5.8214 14.8897L5.8214 14.8897L5.11429 14.1826ZM5.11429 14.8897L5.8214 14.1826L5.8214 14.1826L5.11429 14.8897ZM9.5 11.2111H10.5C10.5 10.8066 10.2564 10.442 9.88268 10.2872C9.50901 10.1324 9.07889 10.218 8.79289 10.504L9.5 11.2111ZM9.5 13.7217L9.74168 14.6921C10.1873 14.5811 10.5 14.1809 10.5 13.7217H9.5ZM9.26777 13.8533L8.56066 13.1462L8.56066 13.1462L9.26777 13.8533ZM7.85355 15.2675L7.14645 14.5604L7.14644 14.5604L7.85355 15.2675ZM7.85355 15.9746L7.14644 16.6818L7.14645 16.6818L7.85355 15.9746ZM8.56066 15.9746L7.85355 15.2675L7.85355 15.2675L8.56066 15.9746ZM9.5 15.0353H10.5C10.5 14.6308 10.2564 14.2662 9.88268 14.1114C9.50901 13.9566 9.07889 14.0422 8.79289 14.3282L9.5 15.0353ZM10.5 15.2072L11.2071 14.5001C10.9211 14.2141 10.491 14.1285 10.1173 14.2833C9.74364 14.4381 9.5 14.8027 9.5 15.2072H10.5ZM11.2675 15.9746L11.9746 15.2675L11.9746 15.2675L11.2675 15.9746ZM10.5 13.8018H9.5C9.5 14.1221 9.65345 14.423 9.91271 14.6111L10.5 13.8018ZM10.5 11.2033L11.2071 10.4962C10.9211 10.2102 10.491 10.1246 10.1173 10.2794C9.74364 10.4342 9.5 10.7988 9.5 11.2033H10.5ZM14.1864 14.8897L13.4793 15.5968L13.4793 15.5968L14.1864 14.8897ZM11.2109 10.5V9.5C10.8065 9.5 10.4418 9.74364 10.2871 10.1173C10.1323 10.491 10.2178 10.9211 10.5038 11.2071L11.2109 10.5ZM13.8057 10.5L14.615 9.9127C14.4269 9.65344 14.126 9.5 13.8057 9.5V10.5ZM15.9786 11.2675L16.6857 10.5604V10.5604L15.9786 11.2675ZM15.2111 10.5V9.5C14.8066 9.5 14.442 9.74364 14.2872 10.1173C14.1324 10.491 14.218 10.9211 14.504 11.2071L15.2111 10.5ZM15.0392 9.5L14.3321 8.79289C14.0461 9.07889 13.9606 9.50901 14.1153 9.88268C14.2701 10.2564 14.6348 10.5 15.0392 10.5V9.5ZM15.9786 8.56066L15.2714 7.85355L15.2714 7.85355L15.9786 8.56066ZM15.9786 7.85355L16.6857 7.14645L16.6857 7.14644L15.9786 7.85355ZM15.2714 7.85355L14.5643 7.14644L14.5643 7.14645L15.2714 7.85355ZM13.8572 9.26777L14.5643 9.97488L14.5643 9.97487L13.8572 9.26777ZM13.7257 9.5V10.5C14.1848 10.5 14.585 10.1873 14.696 9.74168L13.7257 9.5ZM11.2111 9.5L10.504 8.79289C10.218 9.07889 10.1324 9.50901 10.2872 9.88268C10.442 10.2564 10.8066 10.5 11.2111 10.5V9.5ZM14.3067 6.40441L15.0138 7.11152L15.0138 7.11152L14.3067 6.40441ZM14.3067 5.6973L15.0138 4.9902L15.0138 4.99019L14.3067 5.6973ZM13.5996 5.6973L12.8925 4.9902V4.9902L13.5996 5.6973ZM10.5 8.79688H9.5C9.5 9.20134 9.74364 9.56597 10.1173 9.72076C10.491 9.87554 10.9211 9.78998 11.2071 9.50398L10.5 8.79688ZM10.5 6.02635L9.91271 5.21698C9.65345 5.4051 9.5 5.70603 9.5 6.02635H10.5ZM10.5604 5.9748L11.2675 6.68191L11.2675 6.6819L10.5604 5.9748ZM11.9746 4.56058L11.2675 3.85348L11.2675 3.85348L11.9746 4.56058ZM11.9746 3.85348L11.2675 4.56058L11.2675 4.56059L11.9746 3.85348ZM11.2675 3.85348L11.9746 4.56058L11.9746 4.56058L11.2675 3.85348ZM10.5 4.62094H9.5C9.5 5.02541 9.74364 5.39004 10.1173 5.54482C10.491 5.6996 10.9211 5.61405 11.2071 5.32805L10.5 4.62094ZM9.5 3H10.5C10.5 3.27614 10.2761 3.5 10 3.5V2.5V1.5C9.17157 1.5 8.5 2.17157 8.5 3H9.5ZM9.5 4.79282H10.5V3H9.5H8.5V4.79282H9.5ZM8.56066 3.85348L7.85355 4.56058L8.79289 5.49992L9.5 4.79282L10.2071 4.08571L9.26777 3.14637L8.56066 3.85348ZM7.85355 3.85348L8.56066 4.56059C8.3654 4.75585 8.04882 4.75585 7.85355 4.56059L8.56066 3.85348L9.26777 3.14637C8.68198 2.56058 7.7322Line truncated
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,13 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_&#235;&#136;&#136;&#234;&#189;&#131;">
<rect width="20" height="20" rx="2" fill="black" fill-opacity="0.25"/>
<g id="icon_&#235;&#136;&#136;&#234;&#189;&#131;_2">
<mask id="path-2-outside-1_0_18" maskUnits="userSpaceOnUse" x="1.5" y="1.5" width="17" height="17" fill="black">
<rect fill="white" x="1.5" y="1.5" width="17" height="17"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C9.72386 2.5 9.5 2.72386 9.5 3V4.79282L8.56066 3.85348C8.3654 3.65822 8.04882 3.65822 7.85355 3.85348C7.65829 4.04874 7.65829 4.36532 7.85355 4.56058L9.26777 5.9748C9.33429 6.04133 9.4149 6.08519 9.5 6.10638V8.78906L6.40824 5.6973C6.21298 5.50204 5.8964 5.50204 5.70113 5.6973C5.50587 5.89257 5.50587 6.20915 5.70113 6.40441L8.79672 9.5H6.10247C6.08128 9.4149 6.03742 9.33429 5.97089 9.26777L4.55668 7.85355C4.36142 7.65829 4.04483 7.65829 3.84957 7.85355C3.65431 8.04882 3.65431 8.3654 3.84957 8.56066L4.78891 9.5H3C2.72386 9.5 2.5 9.72386 2.5 10C2.5 10.2761 2.72386 10.5 3 10.5H4.61704L3.84957 11.2675C3.65431 11.4627 3.65431 11.7793 3.84957 11.9746C4.04483 12.1698 4.36142 12.1698 4.55668 11.9746L5.97089 10.5604C5.98993 10.5413 6.00712 10.5211 6.02244 10.5H8.79688L5.11429 14.1826C4.91903 14.3778 4.91903 14.6944 5.11429 14.8897C5.30955 15.085 5.62614 15.085 5.8214 14.8897L9.5 11.2111V13.7217C9.4149 13.7429 9.33429 13.7868 9.26777 13.8533L7.85355 15.2675C7.65829 15.4628 7.65829 15.7794 7.85355 15.9746C8.04882 16.1699 8.3654 16.1699 8.56066 15.9746L9.5 15.0353V17C9.5 17.2761 9.72386 17.5 10 17.5C10.2761 17.5 10.5 17.2761 10.5 17V15.2072L11.2675 15.9746C11.4627 16.1699 11.7793 16.1699 11.9746 15.9746C12.1698 15.7794 12.1698 15.4628 11.9746 15.2675L10.5604 13.8533C10.5413 13.8343 10.5211 13.8171 10.5 13.8018V11.2033L14.1864 14.8897C14.3817 15.085 14.6983 15.085 14.8935 14.8897C15.0888 14.6944 15.0888 14.3778 14.8935 14.1826L11.2109 10.5H13.8057C13.821 10.5211 13.8382 10.5413 13.8572 10.5604L15.2714 11.9746C15.4667 12.1698 15.7833 12.1698 15.9786 11.9746C16.1738 11.7793 16.1738 11.4627 15.9786 11.2675L15.2111 10.5H17C17.2761 10.5 17.5 10.2761 17.5 10C17.5 9.72386 17.2761 9.5 17 9.5H15.0392L15.9786 8.56066C16.1738 8.3654 16.1738 8.04882 15.9786 7.85355C15.7833 7.65829 15.4667 7.65829 15.2714 7.85355L13.8572 9.26777C13.7907 9.33429 13.7468 9.4149 13.7257 9.5H11.2111L14.3067 6.40441C14.5019 6.20915 14.5019 5.89257 14.3067 5.6973C14.1114 5.50204 13.7948 5.50204 13.5996 5.6973L10.5 8.79688V6.02635C10.5211 6.01102 10.5413 5.99384 10.5604 5.9748L11.9746 4.56058C12.1698 4.36532 12.1698 4.04874 11.9746 3.85348C11.7793 3.65822 11.4627 3.65822 11.2675 3.85348L10.5 4.62094V3C10.5 2.72386 10.2761 2.5 10 2.5Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C9.72386 2.5 9.5 2.72386 9.5 3V4.79282L8.56066 3.85348C8.3654 3.65822 8.04882 3.65822 7.85355 3.85348C7.65829 4.04874 7.65829 4.36532 7.85355 4.56058L9.26777 5.9748C9.33429 6.04133 9.4149 6.08519 9.5 6.10638V8.78906L6.40824 5.6973C6.21298 5.50204 5.8964 5.50204 5.70113 5.6973C5.50587 5.89257 5.50587 6.20915 5.70113 6.40441L8.79672 9.5H6.10247C6.08128 9.4149 6.03742 9.33429 5.97089 9.26777L4.55668 7.85355C4.36142 7.65829 4.04483 7.65829 3.84957 7.85355C3.65431 8.04882 3.65431 8.3654 3.84957 8.56066L4.78891 9.5H3C2.72386 9.5 2.5 9.72386 2.5 10C2.5 10.2761 2.72386 10.5 3 10.5H4.61704L3.84957 11.2675C3.65431 11.4627 3.65431 11.7793 3.84957 11.9746C4.04483 12.1698 4.36142 12.1698 4.55668 11.9746L5.97089 10.5604C5.98993 10.5413 6.00712 10.5211 6.02244 10.5H8.79688L5.11429 14.1826C4.91903 14.3778 4.91903 14.6944 5.11429 14.8897C5.30955 15.085 5.62614 15.085 5.8214 14.8897L9.5 11.2111V13.7217C9.4149 13.7429 9.33429 13.7868 9.26777 13.8533L7.85355 15.2675C7.65829 15.4628 7.65829 15.7794 7.85355 15.9746C8.04882 16.1699 8.3654 16.1699 8.56066 15.9746L9.5 15.0353V17C9.5 17.2761 9.72386 17.5 10 17.5C10.2761 17.5 10.5 17.2761 10.5 17V15.2072L11.2675 15.9746C11.4627 16.1699 11.7793 16.1699 11.9746 15.9746C12.1698 15.7794 12.1698 15.4628 11.9746 15.2675L10.5604 13.8533C10.5413 13.8343 10.5211 13.8171 10.5 13.8018V11.2033L14.1864 14.8897C14.3817 15.085 14.6983 15.085 14.8935 14.8897C15.0888 14.6944 15.0888 14.3778 14.8935 14.1826L11.2109 10.5H13.8057C13.821 10.5211 13.8382 10.5413 13.8572 10.5604L15.2714 11.9746C15.4667 12.1698 15.7833 12.1698 15.9786 11.9746C16.1738 11.7793 16.1738 11.4627 15.9786 11.2675L15.2111 10.5H17C17.2761 10.5 17.5 10.2761 17.5 10C17.5 9.72386 17.2761 9.5 17 9.5H15.0392L15.9786 8.56066C16.1738 8.3654 16.1738 8.04882 15.9786 7.85355C15.7833 7.65829 15.4667 7.65829 15.2714 7.85355L13.8572 9.26777C13.7907 9.33429 13.7468 9.4149 13.7257 9.5H11.2111L14.3067 6.40441C14.5019 6.20915 14.5019 5.89257 14.3067 5.6973C14.1114 5.50204 13.7948 5.50204 13.5996 5.6973L10.5 8.79688V6.02635C10.5211 6.01102 10.5413 5.99384 10.5604 5.9748L11.9746 4.56058C12.1698 4.36532 12.1698 4.04874 11.9746 3.85348C11.7793 3.65822 11.4627 3.65822 11.2675 3.85348L10.5 4.62094V3C10.5 2.72386 10.2761 2.5 10 2.5Z" fill="#949494"/>
<path d="M9.5 4.79282L8.79289 5.49992C9.07889 5.78592 9.50901 5.87148 9.88268 5.7167C10.2564 5.56192 10.5 5.19728 10.5 4.79282H9.5ZM8.56066 3.85348L9.26777 3.14637L9.26777 3.14637L8.56066 3.85348ZM7.85355 3.85348L7.14645 3.14637L7.14645 3.14637L7.85355 3.85348ZM7.85355 4.56058L7.14645 5.26769L7.14645 5.26769L7.85355 4.56058ZM9.26777 5.9748L9.97487 5.26769V5.26769L9.26777 5.9748ZM9.5 6.10638H10.5C10.5 5.64719 10.1873 5.24701 9.74168 5.13603L9.5 6.10638ZM9.5 8.78906L8.79289 9.49617C9.07889 9.78217 9.50901 9.86772 9.88268 9.71294C10.2564 9.55816 10.5 9.19352 10.5 8.78906H9.5ZM5.70113 6.40441L6.40824 5.6973V5.6973L5.70113 6.40441ZM8.79672 9.5V10.5C9.20119 10.5 9.56582 10.2564 9.7206 9.88268C9.87538 9.50901 9.78983 9.07889 9.50383 8.79289L8.79672 9.5ZM6.10247 9.5L5.13212 9.74168C5.2431 10.1873 5.64328 10.5 6.10247 10.5V9.5ZM5.97089 9.26777L5.26378 9.97487V9.97487L5.97089 9.26777ZM4.55668 7.85355L5.26378 7.14645L5.26378 7.14645L4.55668 7.85355ZM3.84957 7.85355L3.14246 7.14645L3.14246 7.14645L3.84957 7.85355ZM3.84957 8.56066L3.14246 9.26777L3.14246 9.26777L3.84957 8.56066ZM4.78891 9.5V10.5C5.19337 10.5 5.55801 10.2564 5.71279 9.88268C5.86757 9.50901 5.78202 9.07889 5.49602 8.79289L4.78891 9.5ZM4.61704 10.5L5.32414 11.2071C5.61014 10.9211 5.6957 10.491 5.54092 10.1173C5.38614 9.74364 5.0215 9.5 4.61704 9.5V10.5ZM3.84957 11.2675L4.55668 11.9746L4.55668 11.9746L3.84957 11.2675ZM3.84957 11.9746L4.55668 11.2675L4.55668 11.2675L3.84957 11.9746ZM4.55668 11.9746L3.84957 11.2675L3.84957 11.2675L4.55668 11.9746ZM5.97089 10.5604L6.678 11.2675L6.678 11.2675L5.97089 10.5604ZM6.02244 10.5V9.5C5.70212 9.5 5.40119 9.65345 5.21307 9.91271L6.02244 10.5ZM8.79688 10.5L9.50398 11.2071C9.78998 10.9211 9.87554 10.491 9.72076 10.1173C9.56597 9.74364 9.20134 9.5 8.79688 9.5V10.5ZM5.11429 14.1826L5.8214 14.8897L5.8214 14.8897L5.11429 14.1826ZM5.11429 14.8897L5.8214 14.1826L5.8214 14.1826L5.11429 14.8897ZM9.5 11.2111H10.5C10.5 10.8066 10.2564 10.442 9.88268 10.2872C9.50901 10.1324 9.07889 10.218 8.79289 10.504L9.5 11.2111ZM9.5 13.7217L9.74168 14.6921C10.1873 14.5811 10.5 14.1809 10.5 13.7217H9.5ZM9.26777 13.8533L8.56066 13.1462L8.56066 13.1462L9.26777 13.8533ZM7.85355 15.2675L7.14645 14.5604L7.14644 14.5604L7.85355 15.2675ZM7.85355 15.9746L7.14644 16.6818L7.14645 16.6818L7.85355 15.9746ZM8.56066 15.9746L7.85355 15.2675L7.85355 15.2675L8.56066 15.9746ZM9.5 15.0353H10.5C10.5 14.6308 10.2564 14.2662 9.88268 14.1114C9.50901 13.9566 9.07889 14.0422 8.79289 14.3282L9.5 15.0353ZM10.5 15.2072L11.2071 14.5001C10.9211 14.2141 10.491 14.1285 10.1173 14.2833C9.74364 14.4381 9.5 14.8027 9.5 15.2072H10.5ZM11.2675 15.9746L11.9746 15.2675L11.9746 15.2675L11.2675 15.9746ZM10.5 13.8018H9.5C9.5 14.1221 9.65345 14.423 9.91271 14.6111L10.5 13.8018ZM10.5 11.2033L11.2071 10.4962C10.9211 10.2102 10.491 10.1246 10.1173 10.2794C9.74364 10.4342 9.5 10.7988 9.5 11.2033H10.5ZM14.1864 14.8897L13.4793 15.5968L13.4793 15.5968L14.1864 14.8897ZM11.2109 10.5V9.5C10.8065 9.5 10.4418 9.74364 10.2871 10.1173C10.1323 10.491 10.2178 10.9211 10.5038 11.2071L11.2109 10.5ZM13.8057 10.5L14.615 9.9127C14.4269 9.65344 14.126 9.5 13.8057 9.5V10.5ZM15.9786 11.2675L16.6857 10.5604V10.5604L15.9786 11.2675ZM15.2111 10.5V9.5C14.8066 9.5 14.442 9.74364 14.2872 10.1173C14.1324 10.491 14.218 10.9211 14.504 11.2071L15.2111 10.5ZM15.0392 9.5L14.3321 8.79289C14.0461 9.07889 13.9606 9.50901 14.1153 9.88268C14.2701 10.2564 14.6348 10.5 15.0392 10.5V9.5ZM15.9786 8.56066L15.2714 7.85355L15.2714 7.85355L15.9786 8.56066ZM15.9786 7.85355L16.6857 7.14645L16.6857 7.14644L15.9786 7.85355ZM15.2714 7.85355L14.5643 7.14644L14.5643 7.14645L15.2714 7.85355ZM13.8572 9.26777L14.5643 9.97488L14.5643 9.97487L13.8572 9.26777ZM13.7257 9.5V10.5C14.1848 10.5 14.585 10.1873 14.696 9.74168L13.7257 9.5ZM11.2111 9.5L10.504 8.79289C10.218 9.07889 10.1324 9.50901 10.2872 9.88268C10.442 10.2564 10.8066 10.5 11.2111 10.5V9.5ZM14.3067 6.40441L15.0138 7.11152L15.0138 7.11152L14.3067 6.40441ZM14.3067 5.6973L15.0138 4.9902L15.0138 4.99019L14.3067 5.6973ZM13.5996 5.6973L12.8925 4.9902V4.9902L13.5996 5.6973ZM10.5 8.79688H9.5C9.5 9.20134 9.74364 9.56597 10.1173 9.72076C10.491 9.87554 10.9211 9.78998 11.2071 9.50398L10.5 8.79688ZM10.5 6.02635L9.91271 5.21698C9.65345 5.4051 9.5 5.70603 9.5 6.02635H10.5ZM10.5604 5.9748L11.2675 6.68191L11.2675 6.6819L10.5604 5.9748ZM11.9746 4.56058L11.2675 3.85348L11.2675 3.85348L11.9746 4.56058ZM11.9746 3.85348L11.2675 4.56058L11.2675 4.56059L11.9746 3.85348ZM11.2675 3.85348L11.9746 4.56058L11.9746 4.56058L11.2675 3.85348ZM10.5 4.62094H9.5C9.5 5.02541 9.74364 5.39004 10.1173 5.54482C10.491 5.6996 10.9211 5.61405 11.2071 5.32805L10.5 4.62094ZM9.5 3H10.5C10.5 3.27614 10.2761 3.5 10 3.5V2.5V1.5C9.17157 1.5 8.5 2.17157 8.5 3H9.5ZM9.5 4.79282H10.5V3H9.5H8.5V4.79282H9.5ZM8.56066 3.85348L7.85355 4.56058L8.79289 5.49992L9.5 4.79282L10.2071 4.08571L9.26777 3.14637L8.56066 3.85348ZM7.85355 3.85348L8.56066 4.56059C8.3654 4.75585 8.04882 4.75585 7.85355 4.56059L8.56066 3.85348L9.26777 3.14637C8.68198 2.56058 7.7322Line truncated
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,12 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_&#235;&#136;&#136;&#234;&#189;&#131;">
<g id="icon_&#235;&#136;&#136;&#234;&#189;&#131;_2">
<mask id="path-2-outside-1_0_46" maskUnits="userSpaceOnUse" x="1.5" y="1.5" width="17" height="17" fill="black">
<rect fill="white" x="1.5" y="1.5" width="17" height="17"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C9.72386 2.5 9.5 2.72386 9.5 3V4.79282L8.56066 3.85348C8.3654 3.65822 8.04882 3.65822 7.85355 3.85348C7.65829 4.04874 7.65829 4.36532 7.85355 4.56058L9.26777 5.9748C9.33429 6.04133 9.4149 6.08519 9.5 6.10638V8.78906L6.40824 5.6973C6.21298 5.50204 5.8964 5.50204 5.70113 5.6973C5.50587 5.89257 5.50587 6.20915 5.70113 6.40441L8.79672 9.5H6.10247C6.08128 9.4149 6.03742 9.33429 5.97089 9.26777L4.55668 7.85355C4.36142 7.65829 4.04483 7.65829 3.84957 7.85355C3.65431 8.04882 3.65431 8.3654 3.84957 8.56066L4.78891 9.5H3C2.72386 9.5 2.5 9.72386 2.5 10C2.5 10.2761 2.72386 10.5 3 10.5H4.61704L3.84957 11.2675C3.65431 11.4627 3.65431 11.7793 3.84957 11.9746C4.04483 12.1698 4.36142 12.1698 4.55668 11.9746L5.97089 10.5604C5.98993 10.5413 6.00712 10.5211 6.02244 10.5H8.79688L5.11429 14.1826C4.91903 14.3778 4.91903 14.6944 5.11429 14.8897C5.30955 15.085 5.62614 15.085 5.8214 14.8897L9.5 11.2111V13.7217C9.4149 13.7429 9.33429 13.7868 9.26777 13.8533L7.85355 15.2675C7.65829 15.4628 7.65829 15.7794 7.85355 15.9746C8.04882 16.1699 8.3654 16.1699 8.56066 15.9746L9.5 15.0353V17C9.5 17.2761 9.72386 17.5 10 17.5C10.2761 17.5 10.5 17.2761 10.5 17V15.2072L11.2675 15.9746C11.4627 16.1699 11.7793 16.1699 11.9746 15.9746C12.1698 15.7794 12.1698 15.4628 11.9746 15.2675L10.5604 13.8533C10.5413 13.8343 10.5211 13.8171 10.5 13.8018V11.2033L14.1864 14.8897C14.3817 15.085 14.6983 15.085 14.8935 14.8897C15.0888 14.6944 15.0888 14.3778 14.8935 14.1826L11.2109 10.5H13.8057C13.821 10.5211 13.8382 10.5413 13.8572 10.5604L15.2714 11.9746C15.4667 12.1698 15.7833 12.1698 15.9786 11.9746C16.1738 11.7793 16.1738 11.4627 15.9786 11.2675L15.2111 10.5H17C17.2761 10.5 17.5 10.2761 17.5 10C17.5 9.72386 17.2761 9.5 17 9.5H15.0392L15.9786 8.56066C16.1738 8.3654 16.1738 8.04882 15.9786 7.85355C15.7833 7.65829 15.4667 7.65829 15.2714 7.85355L13.8572 9.26777C13.7907 9.33429 13.7468 9.4149 13.7257 9.5H11.2111L14.3067 6.40441C14.5019 6.20915 14.5019 5.89257 14.3067 5.6973C14.1114 5.50204 13.7948 5.50204 13.5996 5.6973L10.5 8.79688V6.02635C10.5211 6.01102 10.5413 5.99384 10.5604 5.9748L11.9746 4.56058C12.1698 4.36532 12.1698 4.04874 11.9746 3.85348C11.7793 3.65822 11.4627 3.65822 11.2675 3.85348L10.5 4.62094V3C10.5 2.72386 10.2761 2.5 10 2.5Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C9.72386 2.5 9.5 2.72386 9.5 3V4.79282L8.56066 3.85348C8.3654 3.65822 8.04882 3.65822 7.85355 3.85348C7.65829 4.04874 7.65829 4.36532 7.85355 4.56058L9.26777 5.9748C9.33429 6.04133 9.4149 6.08519 9.5 6.10638V8.78906L6.40824 5.6973C6.21298 5.50204 5.8964 5.50204 5.70113 5.6973C5.50587 5.89257 5.50587 6.20915 5.70113 6.40441L8.79672 9.5H6.10247C6.08128 9.4149 6.03742 9.33429 5.97089 9.26777L4.55668 7.85355C4.36142 7.65829 4.04483 7.65829 3.84957 7.85355C3.65431 8.04882 3.65431 8.3654 3.84957 8.56066L4.78891 9.5H3C2.72386 9.5 2.5 9.72386 2.5 10C2.5 10.2761 2.72386 10.5 3 10.5H4.61704L3.84957 11.2675C3.65431 11.4627 3.65431 11.7793 3.84957 11.9746C4.04483 12.1698 4.36142 12.1698 4.55668 11.9746L5.97089 10.5604C5.98993 10.5413 6.00712 10.5211 6.02244 10.5H8.79688L5.11429 14.1826C4.91903 14.3778 4.91903 14.6944 5.11429 14.8897C5.30955 15.085 5.62614 15.085 5.8214 14.8897L9.5 11.2111V13.7217C9.4149 13.7429 9.33429 13.7868 9.26777 13.8533L7.85355 15.2675C7.65829 15.4628 7.65829 15.7794 7.85355 15.9746C8.04882 16.1699 8.3654 16.1699 8.56066 15.9746L9.5 15.0353V17C9.5 17.2761 9.72386 17.5 10 17.5C10.2761 17.5 10.5 17.2761 10.5 17V15.2072L11.2675 15.9746C11.4627 16.1699 11.7793 16.1699 11.9746 15.9746C12.1698 15.7794 12.1698 15.4628 11.9746 15.2675L10.5604 13.8533C10.5413 13.8343 10.5211 13.8171 10.5 13.8018V11.2033L14.1864 14.8897C14.3817 15.085 14.6983 15.085 14.8935 14.8897C15.0888 14.6944 15.0888 14.3778 14.8935 14.1826L11.2109 10.5H13.8057C13.821 10.5211 13.8382 10.5413 13.8572 10.5604L15.2714 11.9746C15.4667 12.1698 15.7833 12.1698 15.9786 11.9746C16.1738 11.7793 16.1738 11.4627 15.9786 11.2675L15.2111 10.5H17C17.2761 10.5 17.5 10.2761 17.5 10C17.5 9.72386 17.2761 9.5 17 9.5H15.0392L15.9786 8.56066C16.1738 8.3654 16.1738 8.04882 15.9786 7.85355C15.7833 7.65829 15.4667 7.65829 15.2714 7.85355L13.8572 9.26777C13.7907 9.33429 13.7468 9.4149 13.7257 9.5H11.2111L14.3067 6.40441C14.5019 6.20915 14.5019 5.89257 14.3067 5.6973C14.1114 5.50204 13.7948 5.50204 13.5996 5.6973L10.5 8.79688V6.02635C10.5211 6.01102 10.5413 5.99384 10.5604 5.9748L11.9746 4.56058C12.1698 4.36532 12.1698 4.04874 11.9746 3.85348C11.7793 3.65822 11.4627 3.65822 11.2675 3.85348L10.5 4.62094V3C10.5 2.72386 10.2761 2.5 10 2.5Z" fill="#949494"/>
<path d="M9.5 4.79282L8.79289 5.49992C9.07889 5.78592 9.50901 5.87148 9.88268 5.7167C10.2564 5.56192 10.5 5.19728 10.5 4.79282H9.5ZM8.56066 3.85348L9.26777 3.14637L9.26777 3.14637L8.56066 3.85348ZM7.85355 3.85348L7.14645 3.14637L7.14645 3.14637L7.85355 3.85348ZM7.85355 4.56058L7.14645 5.26769L7.14645 5.26769L7.85355 4.56058ZM9.26777 5.9748L9.97487 5.26769V5.26769L9.26777 5.9748ZM9.5 6.10638H10.5C10.5 5.64719 10.1873 5.24701 9.74168 5.13603L9.5 6.10638ZM9.5 8.78906L8.79289 9.49617C9.07889 9.78217 9.50901 9.86772 9.88268 9.71294C10.2564 9.55816 10.5 9.19352 10.5 8.78906H9.5ZM5.70113 6.40441L6.40824 5.6973V5.6973L5.70113 6.40441ZM8.79672 9.5V10.5C9.20119 10.5 9.56582 10.2564 9.7206 9.88268C9.87538 9.50901 9.78983 9.07889 9.50383 8.79289L8.79672 9.5ZM6.10247 9.5L5.13212 9.74168C5.2431 10.1873 5.64328 10.5 6.10247 10.5V9.5ZM5.97089 9.26777L5.26378 9.97487V9.97487L5.97089 9.26777ZM4.55668 7.85355L5.26378 7.14645L5.26378 7.14645L4.55668 7.85355ZM3.84957 7.85355L3.14246 7.14645L3.14246 7.14645L3.84957 7.85355ZM3.84957 8.56066L3.14246 9.26777L3.14246 9.26777L3.84957 8.56066ZM4.78891 9.5V10.5C5.19337 10.5 5.55801 10.2564 5.71279 9.88268C5.86757 9.50901 5.78202 9.07889 5.49602 8.79289L4.78891 9.5ZM4.61704 10.5L5.32414 11.2071C5.61014 10.9211 5.6957 10.491 5.54092 10.1173C5.38614 9.74364 5.0215 9.5 4.61704 9.5V10.5ZM3.84957 11.2675L4.55668 11.9746L4.55668 11.9746L3.84957 11.2675ZM3.84957 11.9746L4.55668 11.2675L4.55668 11.2675L3.84957 11.9746ZM4.55668 11.9746L3.84957 11.2675L3.84957 11.2675L4.55668 11.9746ZM5.97089 10.5604L6.678 11.2675L6.678 11.2675L5.97089 10.5604ZM6.02244 10.5V9.5C5.70212 9.5 5.40119 9.65345 5.21307 9.91271L6.02244 10.5ZM8.79688 10.5L9.50398 11.2071C9.78998 10.9211 9.87554 10.491 9.72076 10.1173C9.56597 9.74364 9.20134 9.5 8.79688 9.5V10.5ZM5.11429 14.1826L5.8214 14.8897L5.8214 14.8897L5.11429 14.1826ZM5.11429 14.8897L5.8214 14.1826L5.8214 14.1826L5.11429 14.8897ZM9.5 11.2111H10.5C10.5 10.8066 10.2564 10.442 9.88268 10.2872C9.50901 10.1324 9.07889 10.218 8.79289 10.504L9.5 11.2111ZM9.5 13.7217L9.74168 14.6921C10.1873 14.5811 10.5 14.1809 10.5 13.7217H9.5ZM9.26777 13.8533L8.56066 13.1462L8.56066 13.1462L9.26777 13.8533ZM7.85355 15.2675L7.14645 14.5604L7.14644 14.5604L7.85355 15.2675ZM7.85355 15.9746L7.14644 16.6818L7.14645 16.6818L7.85355 15.9746ZM8.56066 15.9746L7.85355 15.2675L7.85355 15.2675L8.56066 15.9746ZM9.5 15.0353H10.5C10.5 14.6308 10.2564 14.2662 9.88268 14.1114C9.50901 13.9566 9.07889 14.0422 8.79289 14.3282L9.5 15.0353ZM10.5 15.2072L11.2071 14.5001C10.9211 14.2141 10.491 14.1285 10.1173 14.2833C9.74364 14.4381 9.5 14.8027 9.5 15.2072H10.5ZM11.2675 15.9746L11.9746 15.2675L11.9746 15.2675L11.2675 15.9746ZM10.5 13.8018H9.5C9.5 14.1221 9.65345 14.423 9.91271 14.6111L10.5 13.8018ZM10.5 11.2033L11.2071 10.4962C10.9211 10.2102 10.491 10.1246 10.1173 10.2794C9.74364 10.4342 9.5 10.7988 9.5 11.2033H10.5ZM14.1864 14.8897L13.4793 15.5968L13.4793 15.5968L14.1864 14.8897ZM11.2109 10.5V9.5C10.8065 9.5 10.4418 9.74364 10.2871 10.1173C10.1323 10.491 10.2178 10.9211 10.5038 11.2071L11.2109 10.5ZM13.8057 10.5L14.615 9.9127C14.4269 9.65344 14.126 9.5 13.8057 9.5V10.5ZM15.9786 11.2675L16.6857 10.5604V10.5604L15.9786 11.2675ZM15.2111 10.5V9.5C14.8066 9.5 14.442 9.74364 14.2872 10.1173C14.1324 10.491 14.218 10.9211 14.504 11.2071L15.2111 10.5ZM15.0392 9.5L14.3321 8.79289C14.0461 9.07889 13.9606 9.50901 14.1153 9.88268C14.2701 10.2564 14.6348 10.5 15.0392 10.5V9.5ZM15.9786 8.56066L15.2714 7.85355L15.2714 7.85355L15.9786 8.56066ZM15.9786 7.85355L16.6857 7.14645L16.6857 7.14644L15.9786 7.85355ZM15.2714 7.85355L14.5643 7.14644L14.5643 7.14645L15.2714 7.85355ZM13.8572 9.26777L14.5643 9.97488L14.5643 9.97487L13.8572 9.26777ZM13.7257 9.5V10.5C14.1848 10.5 14.585 10.1873 14.696 9.74168L13.7257 9.5ZM11.2111 9.5L10.504 8.79289C10.218 9.07889 10.1324 9.50901 10.2872 9.88268C10.442 10.2564 10.8066 10.5 11.2111 10.5V9.5ZM14.3067 6.40441L15.0138 7.11152L15.0138 7.11152L14.3067 6.40441ZM14.3067 5.6973L15.0138 4.9902L15.0138 4.99019L14.3067 5.6973ZM13.5996 5.6973L12.8925 4.9902V4.9902L13.5996 5.6973ZM10.5 8.79688H9.5C9.5 9.20134 9.74364 9.56597 10.1173 9.72076C10.491 9.87554 10.9211 9.78998 11.2071 9.50398L10.5 8.79688ZM10.5 6.02635L9.91271 5.21698C9.65345 5.4051 9.5 5.70603 9.5 6.02635H10.5ZM10.5604 5.9748L11.2675 6.68191L11.2675 6.6819L10.5604 5.9748ZM11.9746 4.56058L11.2675 3.85348L11.2675 3.85348L11.9746 4.56058ZM11.9746 3.85348L11.2675 4.56058L11.2675 4.56059L11.9746 3.85348ZM11.2675 3.85348L11.9746 4.56058L11.9746 4.56058L11.2675 3.85348ZM10.5 4.62094H9.5C9.5 5.02541 9.74364 5.39004 10.1173 5.54482C10.491 5.6996 10.9211 5.61405 11.2071 5.32805L10.5 4.62094ZM9.5 3H10.5C10.5 3.27614 10.2761 3.5 10 3.5V2.5V1.5C9.17157 1.5 8.5 2.17157 8.5 3H9.5ZM9.5 4.79282H10.5V3H9.5H8.5V4.79282H9.5ZM8.56066 3.85348L7.85355 4.56058L8.79289 5.49992L9.5 4.79282L10.2071 4.08571L9.26777 3.14637L8.56066 3.85348ZM7.85355 3.85348L8.56066 4.56059C8.3654 4.75585 8.04882 4.75585 7.85355 4.56059L8.56066 3.85348L9.26777 3.14637C8.68198 2.56058 7.7322Line truncated
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,15 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_&#236;&#158;&#144;&#235;&#172;&#188;&#236;&#135;&#160;">
<rect width="20" height="20" rx="2" fill="black" fill-opacity="0.25"/>
<g id="Union">
<mask id="path-2-outside-1_0_34" maskUnits="userSpaceOnUse" x="3" y="2" width="14" height="16" fill="black">
<rect fill="white" x="3" y="2" width="14" height="16"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8V5C13 4.44772 12.5523 4 12 4H8C7.44772 4 7 4.44772 7 5V8H13ZM6 8H5C4.44772 8 4 8.44772 4 9V16C4 16.5523 4.44772 17 5 17H15C15.5523 17 16 16.5523 16 16V9C16 8.44772 15.5523 8 15 8H14V5C14 3.96435 13.2128 3.113 12.2041 3.01074L12 3H8C6.89543 3 6 3.89543 6 5V8Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8V5C13 4.44772 12.5523 4 12 4H8C7.44772 4 7 4.44772 7 5V8H13ZM6 8H5C4.44772 8 4 8.44772 4 9V16C4 16.5523 4.44772 17 5 17H15C15.5523 17 16 16.5523 16 16V9C16 8.44772 15.5523 8 15 8H14V5C14 3.96435 13.2128 3.113 12.2041 3.01074L12 3H8C6.89543 3 6 3.89543 6 5V8Z" fill="#777777"/>
<path d="M13 8V9H14V8H13ZM7 8H6V9H7V8ZM6 8V9H7V8H6ZM14 8H13V9H14V8ZM12.2041 3.01074L12.305 2.01584L12.2809 2.0134L12.2567 2.01212L12.2041 3.01074ZM12 3L12.0526 2.00138L12.0263 2H12V3ZM13 8H14V5H13H12V8H13ZM13 5H14C14 3.89543 13.1046 3 12 3V4V5H13ZM12 4V3H8V4V5H12V4ZM8 4V3C6.89543 3 6 3.89543 6 5H7H8V4ZM7 5H6V8H7H8V5H7ZM7 8V9H13V8V7H7V8ZM5 8V9H6V8V7H5V8ZM4 9H5V8V7C3.89543 7 3 7.89543 3 9H4ZM4 16H5V9H4H3V16H4ZM5 17V16H5H4H3C3 17.1046 3.89543 18 5 18V17ZM15 17V16H5V17V18H15V17ZM16 16H15V17V18C16.1046 18 17 17.1046 17 16H16ZM16 9H15V16H16H17V9H16ZM15 8V9H16H17C17 7.89543 16.1046 7 15 7V8ZM14 8V9H15V8V7H14V8ZM14 5H13V8H14H15V5H14ZM12.2041 3.01074L12.1032 4.00564C12.6068 4.0567 13 4.48311 13 5H14H15C15 3.44559 13.8187 2.16931 12.305 2.01584L12.2041 3.01074ZM12 3L11.9474 3.99862L12.1515 4.00936L12.2041 3.01074L12.2567 2.01212L12.0526 2.00138L12 3ZM8 3V4H12V3V2H8V3ZM6 5H7C7 4.44772 7.44772 4 8 4V3V2C6.34315 2 5 3.34315 5 5H6ZM6 8H7V5H6H5V8H6Z" fill="black" mask="url(#path-2-outside-1_0_34)"/>
</g>
<path id="Rectangle 7448" d="M4 9C4 8.44772 4.44772 8 5 8H10V17H5C4.44772 17 4 16.5523 4 16V9Z" fill="#E4BF4D"/>
<path id="Rectangle 7449" d="M10 8H15C15.5523 8 16 8.44772 16 9V16C16 16.5523 15.5523 17 15 17H10V8Z" fill="#D8A300"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,15 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_&#236;&#158;&#144;&#235;&#172;&#188;&#236;&#135;&#160;">
<rect width="20" height="20" rx="2" fill="black" fill-opacity="0.25"/>
<g id="Union">
<mask id="path-2-outside-1_0_10" maskUnits="userSpaceOnUse" x="3" y="2" width="14" height="16" fill="black">
<rect fill="white" x="3" y="2" width="14" height="16"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8V5C13 4.44772 12.5523 4 12 4H8C7.44772 4 7 4.44772 7 5V8H13ZM6 8H5C4.44772 8 4 8.44772 4 9V16C4 16.5523 4.44772 17 5 17H15C15.5523 17 16 16.5523 16 16V9C16 8.44772 15.5523 8 15 8H14V5C14 3.96435 13.2128 3.113 12.2041 3.01074L12 3H8C6.89543 3 6 3.89543 6 5V8Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8V5C13 4.44772 12.5523 4 12 4H8C7.44772 4 7 4.44772 7 5V8H13ZM6 8H5C4.44772 8 4 8.44772 4 9V16C4 16.5523 4.44772 17 5 17H15C15.5523 17 16 16.5523 16 16V9C16 8.44772 15.5523 8 15 8H14V5C14 3.96435 13.2128 3.113 12.2041 3.01074L12 3H8C6.89543 3 6 3.89543 6 5V8Z" fill="#777777"/>
<path d="M13 8V9H14V8H13ZM7 8H6V9H7V8ZM6 8V9H7V8H6ZM14 8H13V9H14V8ZM12.2041 3.01074L12.305 2.01584L12.2809 2.0134L12.2567 2.01212L12.2041 3.01074ZM12 3L12.0526 2.00138L12.0263 2H12V3ZM13 8H14V5H13H12V8H13ZM13 5H14C14 3.89543 13.1046 3 12 3V4V5H13ZM12 4V3H8V4V5H12V4ZM8 4V3C6.89543 3 6 3.89543 6 5H7H8V4ZM7 5H6V8H7H8V5H7ZM7 8V9H13V8V7H7V8ZM5 8V9H6V8V7H5V8ZM4 9H5V8V7C3.89543 7 3 7.89543 3 9H4ZM4 16H5V9H4H3V16H4ZM5 17V16H5H4H3C3 17.1046 3.89543 18 5 18V17ZM15 17V16H5V17V18H15V17ZM16 16H15V17V18C16.1046 18 17 17.1046 17 16H16ZM16 9H15V16H16H17V9H16ZM15 8V9H16H17C17 7.89543 16.1046 7 15 7V8ZM14 8V9H15V8V7H14V8ZM14 5H13V8H14H15V5H14ZM12.2041 3.01074L12.1032 4.00564C12.6068 4.0567 13 4.48311 13 5H14H15C15 3.44559 13.8187 2.16931 12.305 2.01584L12.2041 3.01074ZM12 3L11.9474 3.99862L12.1515 4.00936L12.2041 3.01074L12.2567 2.01212L12.0526 2.00138L12 3ZM8 3V4H12V3V2H8V3ZM6 5H7C7 4.44772 7.44772 4 8 4V3V2C6.34315 2 5 3.34315 5 5H6ZM6 8H7V5H6H5V8H6Z" fill="black" mask="url(#path-2-outside-1_0_10)"/>
</g>
<path id="Rectangle 7448" d="M4 9C4 8.44772 4.44772 8 5 8H10V17H5C4.44772 17 4 16.5523 4 16V9Z" fill="#BEBEBE"/>
<path id="Rectangle 7449" d="M10 8H15C15.5523 8 16 8.44772 16 9V16C16 16.5523 15.5523 17 15 17H10V8Z" fill="#A1A1A1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_&#236;&#158;&#144;&#235;&#172;&#188;&#236;&#135;&#160;">
<g id="Union">
<mask id="path-2-outside-1_0_22" maskUnits="userSpaceOnUse" x="3" y="2" width="14" height="16" fill="black">
<rect fill="white" x="3" y="2" width="14" height="16"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8V5C13 4.44772 12.5523 4 12 4H8C7.44772 4 7 4.44772 7 5V8H13ZM6 8H5C4.44772 8 4 8.44772 4 9V16C4 16.5523 4.44772 17 5 17H15C15.5523 17 16 16.5523 16 16V9C16 8.44772 15.5523 8 15 8H14V5C14 3.96435 13.2128 3.113 12.2041 3.01074L12 3H8C6.89543 3 6 3.89543 6 5V8Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8V5C13 4.44772 12.5523 4 12 4H8C7.44772 4 7 4.44772 7 5V8H13ZM6 8H5C4.44772 8 4 8.44772 4 9V16C4 16.5523 4.44772 17 5 17H15C15.5523 17 16 16.5523 16 16V9C16 8.44772 15.5523 8 15 8H14V5C14 3.96435 13.2128 3.113 12.2041 3.01074L12 3H8C6.89543 3 6 3.89543 6 5V8Z" fill="#777777"/>
<path d="M13 8V9H14V8H13ZM7 8H6V9H7V8ZM6 8V9H7V8H6ZM14 8H13V9H14V8ZM12.2041 3.01074L12.305 2.01584L12.2809 2.0134L12.2567 2.01212L12.2041 3.01074ZM12 3L12.0526 2.00138L12.0263 2H12V3ZM13 8H14V5H13H12V8H13ZM13 5H14C14 3.89543 13.1046 3 12 3V4V5H13ZM12 4V3H8V4V5H12V4ZM8 4V3C6.89543 3 6 3.89543 6 5H7H8V4ZM7 5H6V8H7H8V5H7ZM7 8V9H13V8V7H7V8ZM5 8V9H6V8V7H5V8ZM4 9H5V8V7C3.89543 7 3 7.89543 3 9H4ZM4 16H5V9H4H3V16H4ZM5 17V16H5H4H3C3 17.1046 3.89543 18 5 18V17ZM15 17V16H5V17V18H15V17ZM16 16H15V17V18C16.1046 18 17 17.1046 17 16H16ZM16 9H15V16H16H17V9H16ZM15 8V9H16H17C17 7.89543 16.1046 7 15 7V8ZM14 8V9H15V8V7H14V8ZM14 5H13V8H14H15V5H14ZM12.2041 3.01074L12.1032 4.00564C12.6068 4.0567 13 4.48311 13 5H14H15C15 3.44559 13.8187 2.16931 12.305 2.01584L12.2041 3.01074ZM12 3L11.9474 3.99862L12.1515 4.00936L12.2041 3.01074L12.2567 2.01212L12.0526 2.00138L12 3ZM8 3V4H12V3V2H8V3ZM6 5H7C7 4.44772 7.44772 4 8 4V3V2C6.34315 2 5 3.34315 5 5H6ZM6 8H7V5H6H5V8H6Z" fill="black" mask="url(#path-2-outside-1_0_22)"/>
</g>
<path id="Rectangle 7448" d="M4 9C4 8.44772 4.44772 8 5 8H10V17H5C4.44772 17 4 16.5523 4 16V9Z" fill="#BEBEBE"/>
<path id="Rectangle 7449" d="M10 8H15C15.5523 8 16 8.44772 16 9V16C16 16.5523 15.5523 17 15 17H10V8Z" fill="#A1A1A1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_&#236;&#158;&#144;&#235;&#172;&#188;&#236;&#135;&#160;">
<g id="Union">
<mask id="path-2-outside-1_0_3068" maskUnits="userSpaceOnUse" x="3" y="2" width="14" height="16" fill="black">
<rect fill="white" x="3" y="2" width="14" height="16"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8V5C13 4.44772 12.5523 4 12 4H8C7.44772 4 7 4.44772 7 5V8H13ZM6 8H5C4.44772 8 4 8.44772 4 9V16C4 16.5523 4.44772 17 5 17H15C15.5523 17 16 16.5523 16 16V9C16 8.44772 15.5523 8 15 8H14V5C14 3.96435 13.2128 3.113 12.2041 3.01074L12 3H8C6.89543 3 6 3.89543 6 5V8Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8V5C13 4.44772 12.5523 4 12 4H8C7.44772 4 7 4.44772 7 5V8H13ZM6 8H5C4.44772 8 4 8.44772 4 9V16C4 16.5523 4.44772 17 5 17H15C15.5523 17 16 16.5523 16 16V9C16 8.44772 15.5523 8 15 8H14V5C14 3.96435 13.2128 3.113 12.2041 3.01074L12 3H8C6.89543 3 6 3.89543 6 5V8Z" fill="#777777"/>
<path d="M13 8V9H14V8H13ZM7 8H6V9H7V8ZM6 8V9H7V8H6ZM14 8H13V9H14V8ZM12.2041 3.01074L12.305 2.01584L12.2809 2.0134L12.2567 2.01212L12.2041 3.01074ZM12 3L12.0526 2.00138L12.0263 2H12V3ZM13 8H14V5H13H12V8H13ZM13 5H14C14 3.89543 13.1046 3 12 3V4V5H13ZM12 4V3H8V4V5H12V4ZM8 4V3C6.89543 3 6 3.89543 6 5H7H8V4ZM7 5H6V8H7H8V5H7ZM7 8V9H13V8V7H7V8ZM5 8V9H6V8V7H5V8ZM4 9H5V8V7C3.89543 7 3 7.89543 3 9H4ZM4 16H5V9H4H3V16H4ZM5 17V16H5H4H3C3 17.1046 3.89543 18 5 18V17ZM15 17V16H5V17V18H15V17ZM16 16H15V17V18C16.1046 18 17 17.1046 17 16H16ZM16 9H15V16H16H17V9H16ZM15 8V9H16H17C17 7.89543 16.1046 7 15 7V8ZM14 8V9H15V8V7H14V8ZM14 5H13V8H14H15V5H14ZM12.2041 3.01074L12.1032 4.00564C12.6068 4.0567 13 4.48311 13 5H14H15C15 3.44559 13.8187 2.16931 12.305 2.01584L12.2041 3.01074ZM12 3L11.9474 3.99862L12.1515 4.00936L12.2041 3.01074L12.2567 2.01212L12.0526 2.00138L12 3ZM8 3V4H12V3V2H8V3ZM6 5H7C7 4.44772 7.44772 4 8 4V3V2C6.34315 2 5 3.34315 5 5H6ZM6 8H7V5H6H5V8H6Z" fill="black" mask="url(#path-2-outside-1_0_3068)"/>
</g>
<path id="Rectangle 7448" d="M4 9C4 8.44772 4.44772 8 5 8H10V17H5C4.44772 17 4 16.5523 4 16V9Z" fill="#E4BF4D"/>
<path id="Rectangle 7449" d="M10 8H15C15.5523 8 16 8.44772 16 9V16C16 16.5523 15.5523 17 15 17H10V8Z" fill="#D8A300"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_&#235;&#136;&#136;&#234;&#189;&#131;">
<g id="icon_&#235;&#136;&#136;&#234;&#189;&#131;_2">
<mask id="path-2-outside-1_0_3098" maskUnits="userSpaceOnUse" x="1.5" y="1.5" width="17" height="17" fill="black">
<rect fill="white" x="1.5" y="1.5" width="17" height="17"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C9.72386 2.5 9.5 2.72386 9.5 3V4.79282L8.56066 3.85348C8.3654 3.65822 8.04882 3.65822 7.85355 3.85348C7.65829 4.04874 7.65829 4.36532 7.85355 4.56058L9.26777 5.9748C9.33429 6.04133 9.4149 6.08519 9.5 6.10638V8.78906L6.40824 5.6973C6.21298 5.50204 5.8964 5.50204 5.70113 5.6973C5.50587 5.89257 5.50587 6.20915 5.70113 6.40441L8.79672 9.5H6.10247C6.08128 9.4149 6.03742 9.33429 5.97089 9.26777L4.55668 7.85355C4.36142 7.65829 4.04483 7.65829 3.84957 7.85355C3.65431 8.04882 3.65431 8.3654 3.84957 8.56066L4.78891 9.5H3C2.72386 9.5 2.5 9.72386 2.5 10C2.5 10.2761 2.72386 10.5 3 10.5H4.61704L3.84957 11.2675C3.65431 11.4627 3.65431 11.7793 3.84957 11.9746C4.04483 12.1698 4.36142 12.1698 4.55668 11.9746L5.97089 10.5604C5.98993 10.5413 6.00712 10.5211 6.02244 10.5H8.79688L5.11429 14.1826C4.91903 14.3778 4.91903 14.6944 5.11429 14.8897C5.30955 15.085 5.62614 15.085 5.8214 14.8897L9.5 11.2111V13.7217C9.4149 13.7429 9.33429 13.7868 9.26777 13.8533L7.85355 15.2675C7.65829 15.4628 7.65829 15.7794 7.85355 15.9746C8.04882 16.1699 8.3654 16.1699 8.56066 15.9746L9.5 15.0353V17C9.5 17.2761 9.72386 17.5 10 17.5C10.2761 17.5 10.5 17.2761 10.5 17V15.2072L11.2675 15.9746C11.4627 16.1699 11.7793 16.1699 11.9746 15.9746C12.1698 15.7794 12.1698 15.4628 11.9746 15.2675L10.5604 13.8533C10.5413 13.8343 10.5211 13.8171 10.5 13.8018V11.2033L14.1864 14.8897C14.3817 15.085 14.6983 15.085 14.8935 14.8897C15.0888 14.6944 15.0888 14.3778 14.8935 14.1826L11.2109 10.5H13.8057C13.821 10.5211 13.8382 10.5413 13.8572 10.5604L15.2714 11.9746C15.4667 12.1698 15.7833 12.1698 15.9786 11.9746C16.1738 11.7793 16.1738 11.4627 15.9786 11.2675L15.2111 10.5H17C17.2761 10.5 17.5 10.2761 17.5 10C17.5 9.72386 17.2761 9.5 17 9.5H15.0392L15.9786 8.56066C16.1738 8.3654 16.1738 8.04882 15.9786 7.85355C15.7833 7.65829 15.4667 7.65829 15.2714 7.85355L13.8572 9.26777C13.7907 9.33429 13.7468 9.4149 13.7257 9.5H11.2111L14.3067 6.40441C14.5019 6.20915 14.5019 5.89257 14.3067 5.6973C14.1114 5.50204 13.7948 5.50204 13.5996 5.6973L10.5 8.79688V6.02635C10.5211 6.01102 10.5413 5.99384 10.5604 5.9748L11.9746 4.56058C12.1698 4.36532 12.1698 4.04874 11.9746 3.85348C11.7793 3.65822 11.4627 3.65822 11.2675 3.85348L10.5 4.62094V3C10.5 2.72386 10.2761 2.5 10 2.5Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C9.72386 2.5 9.5 2.72386 9.5 3V4.79282L8.56066 3.85348C8.3654 3.65822 8.04882 3.65822 7.85355 3.85348C7.65829 4.04874 7.65829 4.36532 7.85355 4.56058L9.26777 5.9748C9.33429 6.04133 9.4149 6.08519 9.5 6.10638V8.78906L6.40824 5.6973C6.21298 5.50204 5.8964 5.50204 5.70113 5.6973C5.50587 5.89257 5.50587 6.20915 5.70113 6.40441L8.79672 9.5H6.10247C6.08128 9.4149 6.03742 9.33429 5.97089 9.26777L4.55668 7.85355C4.36142 7.65829 4.04483 7.65829 3.84957 7.85355C3.65431 8.04882 3.65431 8.3654 3.84957 8.56066L4.78891 9.5H3C2.72386 9.5 2.5 9.72386 2.5 10C2.5 10.2761 2.72386 10.5 3 10.5H4.61704L3.84957 11.2675C3.65431 11.4627 3.65431 11.7793 3.84957 11.9746C4.04483 12.1698 4.36142 12.1698 4.55668 11.9746L5.97089 10.5604C5.98993 10.5413 6.00712 10.5211 6.02244 10.5H8.79688L5.11429 14.1826C4.91903 14.3778 4.91903 14.6944 5.11429 14.8897C5.30955 15.085 5.62614 15.085 5.8214 14.8897L9.5 11.2111V13.7217C9.4149 13.7429 9.33429 13.7868 9.26777 13.8533L7.85355 15.2675C7.65829 15.4628 7.65829 15.7794 7.85355 15.9746C8.04882 16.1699 8.3654 16.1699 8.56066 15.9746L9.5 15.0353V17C9.5 17.2761 9.72386 17.5 10 17.5C10.2761 17.5 10.5 17.2761 10.5 17V15.2072L11.2675 15.9746C11.4627 16.1699 11.7793 16.1699 11.9746 15.9746C12.1698 15.7794 12.1698 15.4628 11.9746 15.2675L10.5604 13.8533C10.5413 13.8343 10.5211 13.8171 10.5 13.8018V11.2033L14.1864 14.8897C14.3817 15.085 14.6983 15.085 14.8935 14.8897C15.0888 14.6944 15.0888 14.3778 14.8935 14.1826L11.2109 10.5H13.8057C13.821 10.5211 13.8382 10.5413 13.8572 10.5604L15.2714 11.9746C15.4667 12.1698 15.7833 12.1698 15.9786 11.9746C16.1738 11.7793 16.1738 11.4627 15.9786 11.2675L15.2111 10.5H17C17.2761 10.5 17.5 10.2761 17.5 10C17.5 9.72386 17.2761 9.5 17 9.5H15.0392L15.9786 8.56066C16.1738 8.3654 16.1738 8.04882 15.9786 7.85355C15.7833 7.65829 15.4667 7.65829 15.2714 7.85355L13.8572 9.26777C13.7907 9.33429 13.7468 9.4149 13.7257 9.5H11.2111L14.3067 6.40441C14.5019 6.20915 14.5019 5.89257 14.3067 5.6973C14.1114 5.50204 13.7948 5.50204 13.5996 5.6973L10.5 8.79688V6.02635C10.5211 6.01102 10.5413 5.99384 10.5604 5.9748L11.9746 4.56058C12.1698 4.36532 12.1698 4.04874 11.9746 3.85348C11.7793 3.65822 11.4627 3.65822 11.2675 3.85348L10.5 4.62094V3C10.5 2.72386 10.2761 2.5 10 2.5Z" fill="#00CCFF"/>
<path d="M9.5 4.79282L8.79289 5.49992C9.07889 5.78592 9.50901 5.87148 9.88268 5.7167C10.2564 5.56192 10.5 5.19728 10.5 4.79282H9.5ZM8.56066 3.85348L9.26777 3.14637L9.26777 3.14637L8.56066 3.85348ZM7.85355 3.85348L7.14645 3.14637L7.14645 3.14637L7.85355 3.85348ZM7.85355 4.56058L7.14645 5.26769L7.14645 5.26769L7.85355 4.56058ZM9.26777 5.9748L9.97487 5.26769V5.26769L9.26777 5.9748ZM9.5 6.10638H10.5C10.5 5.64719 10.1873 5.24701 9.74168 5.13603L9.5 6.10638ZM9.5 8.78906L8.79289 9.49617C9.07889 9.78217 9.50901 9.86772 9.88268 9.71294C10.2564 9.55816 10.5 9.19352 10.5 8.78906H9.5ZM5.70113 6.40441L6.40824 5.6973V5.6973L5.70113 6.40441ZM8.79672 9.5V10.5C9.20119 10.5 9.56582 10.2564 9.7206 9.88268C9.87538 9.50901 9.78983 9.07889 9.50383 8.79289L8.79672 9.5ZM6.10247 9.5L5.13212 9.74168C5.2431 10.1873 5.64328 10.5 6.10247 10.5V9.5ZM5.97089 9.26777L5.26378 9.97487V9.97487L5.97089 9.26777ZM4.55668 7.85355L5.26378 7.14645L5.26378 7.14645L4.55668 7.85355ZM3.84957 7.85355L3.14246 7.14645L3.14246 7.14645L3.84957 7.85355ZM3.84957 8.56066L3.14246 9.26777L3.14246 9.26777L3.84957 8.56066ZM4.78891 9.5V10.5C5.19337 10.5 5.55801 10.2564 5.71279 9.88268C5.86757 9.50901 5.78202 9.07889 5.49602 8.79289L4.78891 9.5ZM4.61704 10.5L5.32414 11.2071C5.61014 10.9211 5.6957 10.491 5.54092 10.1173C5.38614 9.74364 5.0215 9.5 4.61704 9.5V10.5ZM3.84957 11.2675L4.55668 11.9746L4.55668 11.9746L3.84957 11.2675ZM3.84957 11.9746L4.55668 11.2675L4.55668 11.2675L3.84957 11.9746ZM4.55668 11.9746L3.84957 11.2675L3.84957 11.2675L4.55668 11.9746ZM5.97089 10.5604L6.678 11.2675L6.678 11.2675L5.97089 10.5604ZM6.02244 10.5V9.5C5.70212 9.5 5.40119 9.65345 5.21307 9.91271L6.02244 10.5ZM8.79688 10.5L9.50398 11.2071C9.78998 10.9211 9.87554 10.491 9.72076 10.1173C9.56597 9.74364 9.20134 9.5 8.79688 9.5V10.5ZM5.11429 14.1826L5.8214 14.8897L5.8214 14.8897L5.11429 14.1826ZM5.11429 14.8897L5.8214 14.1826L5.8214 14.1826L5.11429 14.8897ZM9.5 11.2111H10.5C10.5 10.8066 10.2564 10.442 9.88268 10.2872C9.50901 10.1324 9.07889 10.218 8.79289 10.504L9.5 11.2111ZM9.5 13.7217L9.74168 14.6921C10.1873 14.5811 10.5 14.1809 10.5 13.7217H9.5ZM9.26777 13.8533L8.56066 13.1462L8.56066 13.1462L9.26777 13.8533ZM7.85355 15.2675L7.14645 14.5604L7.14644 14.5604L7.85355 15.2675ZM7.85355 15.9746L7.14644 16.6818L7.14645 16.6818L7.85355 15.9746ZM8.56066 15.9746L7.85355 15.2675L7.85355 15.2675L8.56066 15.9746ZM9.5 15.0353H10.5C10.5 14.6308 10.2564 14.2662 9.88268 14.1114C9.50901 13.9566 9.07889 14.0422 8.79289 14.3282L9.5 15.0353ZM10.5 15.2072L11.2071 14.5001C10.9211 14.2141 10.491 14.1285 10.1173 14.2833C9.74364 14.4381 9.5 14.8027 9.5 15.2072H10.5ZM11.2675 15.9746L11.9746 15.2675L11.9746 15.2675L11.2675 15.9746ZM10.5 13.8018H9.5C9.5 14.1221 9.65345 14.423 9.91271 14.6111L10.5 13.8018ZM10.5 11.2033L11.2071 10.4962C10.9211 10.2102 10.491 10.1246 10.1173 10.2794C9.74364 10.4342 9.5 10.7988 9.5 11.2033H10.5ZM14.1864 14.8897L13.4793 15.5968L13.4793 15.5968L14.1864 14.8897ZM11.2109 10.5V9.5C10.8065 9.5 10.4418 9.74364 10.2871 10.1173C10.1323 10.491 10.2178 10.9211 10.5038 11.2071L11.2109 10.5ZM13.8057 10.5L14.615 9.9127C14.4269 9.65344 14.126 9.5 13.8057 9.5V10.5ZM15.9786 11.2675L16.6857 10.5604V10.5604L15.9786 11.2675ZM15.2111 10.5V9.5C14.8066 9.5 14.442 9.74364 14.2872 10.1173C14.1324 10.491 14.218 10.9211 14.504 11.2071L15.2111 10.5ZM15.0392 9.5L14.3321 8.79289C14.0461 9.07889 13.9606 9.50901 14.1153 9.88268C14.2701 10.2564 14.6348 10.5 15.0392 10.5V9.5ZM15.9786 8.56066L15.2714 7.85355L15.2714 7.85355L15.9786 8.56066ZM15.9786 7.85355L16.6857 7.14645L16.6857 7.14644L15.9786 7.85355ZM15.2714 7.85355L14.5643 7.14644L14.5643 7.14645L15.2714 7.85355ZM13.8572 9.26777L14.5643 9.97488L14.5643 9.97487L13.8572 9.26777ZM13.7257 9.5V10.5C14.1848 10.5 14.585 10.1873 14.696 9.74168L13.7257 9.5ZM11.2111 9.5L10.504 8.79289C10.218 9.07889 10.1324 9.50901 10.2872 9.88268C10.442 10.2564 10.8066 10.5 11.2111 10.5V9.5ZM14.3067 6.40441L15.0138 7.11152L15.0138 7.11152L14.3067 6.40441ZM14.3067 5.6973L15.0138 4.9902L15.0138 4.99019L14.3067 5.6973ZM13.5996 5.6973L12.8925 4.9902V4.9902L13.5996 5.6973ZM10.5 8.79688H9.5C9.5 9.20134 9.74364 9.56597 10.1173 9.72076C10.491 9.87554 10.9211 9.78998 11.2071 9.50398L10.5 8.79688ZM10.5 6.02635L9.91271 5.21698C9.65345 5.4051 9.5 5.70603 9.5 6.02635H10.5ZM10.5604 5.9748L11.2675 6.68191L11.2675 6.6819L10.5604 5.9748ZM11.9746 4.56058L11.2675 3.85348L11.2675 3.85348L11.9746 4.56058ZM11.9746 3.85348L11.2675 4.56058L11.2675 4.56059L11.9746 3.85348ZM11.2675 3.85348L11.9746 4.56058L11.9746 4.56058L11.2675 3.85348ZM10.5 4.62094H9.5C9.5 5.02541 9.74364 5.39004 10.1173 5.54482C10.491 5.6996 10.9211 5.61405 11.2071 5.32805L10.5 4.62094ZM9.5 3H10.5C10.5 3.27614 10.2761 3.5 10 3.5V2.5V1.5C9.17157 1.5 8.5 2.17157 8.5 3H9.5ZM9.5 4.79282H10.5V3H9.5H8.5V4.79282H9.5ZM8.56066 3.85348L7.85355 4.56058L8.79289 5.49992L9.5 4.79282L10.2071 4.08571L9.26777 3.14637L8.56066 3.85348ZM7.85355 3.85348L8.56066 4.56059C8.3654 4.75585 8.04882 4.75585 7.85355 4.56059L8.56066 3.85348L9.26777 3.14637C8.68198 2.56058 7.7322Line truncated
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="14" height="18" viewBox="0 0 14 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Union">
<mask id="path-1-outside-1_0_2922" maskUnits="userSpaceOnUse" x="0" y="0" width="14" height="18" fill="black">
<rect fill="white" width="14" height="18"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 12.1973C11.7934 11.1599 13 9.22085 13 7C13 3.68629 10.3137 1 7 1C3.68629 1 1 3.68629 1 7C1 9.22085 2.2066 11.1599 4 12.1973V16C4 16.5523 4.44772 17 5 17H9C9.55228 17 10 16.5523 10 16V12.1973Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 12.1973C11.7934 11.1599 13 9.22085 13 7C13 3.68629 10.3137 1 7 1C3.68629 1 1 3.68629 1 7C1 9.22085 2.2066 11.1599 4 12.1973V16C4 16.5523 4.44772 17 5 17H9C9.55228 17 10 16.5523 10 16V12.1973Z" fill="#D9D9D9"/>
<path d="M10 12.1973L9.49927 11.3317L9 11.6205V12.1973H10ZM4 12.1973H5V11.6205L4.50073 11.3317L4 12.1973ZM13 7H12C12 8.84945 10.9964 10.4657 9.49927 11.3317L10 12.1973L10.5007 13.0629C12.5904 11.8541 14 9.59225 14 7H13ZM7 1V2C9.76142 2 12 4.23858 12 7H13H14C14 3.13401 10.866 0 7 0V1ZM1 7H2C2 4.23858 4.23858 2 7 2V1V0C3.13401 0 0 3.13401 0 7H1ZM4 12.1973L4.50073 11.3317C3.00361 10.4657 2 8.84945 2 7H1H0C0 9.59225 1.40958 11.8541 3.49927 13.0629L4 12.1973ZM4 16H5V12.1973H4H3V16H4ZM5 17V16H4H3C3 17.1046 3.89543 18 5 18V17ZM9 17V16H5V17V18H9V17ZM10 16H9V17V18C10.1046 18 11 17.1046 11 16H10ZM10 12.1973H9V16H10H11V12.1973H10Z" fill="black" mask="url(#path-1-outside-1_0_2922)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,12 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="60" height="15" viewBox="0 0 60 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame 427319903">
<g id="Union">
<mask id="path-1-outside-1_0_2950" maskUnits="userSpaceOnUse" x="0" y="0" width="60" height="15" fill="black">
<rect fill="white" width="60" height="15"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H2V14H1V1ZM53.5 7L7.5 7V4.46891L3 7.5L7.5 10.5311V8L53.5 8V10.5311L58 7.5V14H59V1H58V7.5L53.5 4.46891V7Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H2V14H1V1ZM53.5 7L7.5 7V4.46891L3 7.5L7.5 10.5311V8L53.5 8V10.5311L58 7.5V14H59V1H58V7.5L53.5 4.46891V7Z" fill="#FFC600"/>
<path d="M2 1H3V0H2V1ZM1 1V0H0V1H1ZM2 14V15H3V14H2ZM1 14H0V15H1V14ZM53.5 7V8H54.5V7H53.5ZM7.5 7H6.5V8H7.5V7ZM7.5 4.46891H8.5V2.58964L6.94134 3.63952L7.5 4.46891ZM3 7.5L2.44134 6.6706L1.21 7.5L2.44134 8.3294L3 7.5ZM7.5 10.5311L6.94134 11.3605L8.5 12.4104V10.5311H7.5ZM7.5 8V7H6.5V8H7.5ZM53.5 8H54.5V7H53.5V8ZM53.5 10.5311H52.5V12.4104L54.0587 11.3605L53.5 10.5311ZM58 14H57V15H58V14ZM59 14V15H60V14H59ZM59 1H60V0H59V1ZM58 1V0H57V1H58ZM53.5 4.46891L54.0587 3.63952L52.5 2.58964V4.46891H53.5ZM2 1V0H1V1V2H2V1ZM2 14H3V1H2H1V14H2ZM1 14V15H2V14V13H1V14ZM1 1H0V14H1H2V1H1ZM53.5 7V6L7.5 6V7V8L53.5 8V7ZM7.5 7H8.5V4.46891H7.5H6.5V7H7.5ZM7.5 4.46891L6.94134 3.63952L2.44134 6.6706L3 7.5L3.55866 8.3294L8.05866 5.29831L7.5 4.46891ZM3 7.5L2.44134 8.3294L6.94134 11.3605L7.5 10.5311L8.05866 9.70169L3.55866 6.6706L3 7.5ZM7.5 10.5311H8.5V8H7.5H6.5V10.5311H7.5ZM7.5 8V9L53.5 9V8V7L7.5 7V8ZM53.5 8H52.5V10.5311H53.5H54.5V8H53.5ZM53.5 10.5311L54.0587 11.3605L58.5587 8.3294L58 7.5L57.4413 6.6706L52.9413 9.70169L53.5 10.5311ZM58 14H59V7.5H58H57V14H58ZM59 14V13H58V14V15H59V14ZM59 1H58V14H59H60V1H59ZM58 1V2H59V1V0H58V1ZM58 7.5H59V1H58H57V7.5H58ZM58 7.5L58.5587 6.6706L54.0587 3.63952L53.5 4.46891L52.9413 5.29831L57.4413 8.3294L58 7.5ZM53.5 4.46891H52.5V7H53.5H54.5V4.46891H53.5Z" fill="black" mask="url(#path-1-outside-1_0_2950)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,12 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="60" height="15" viewBox="0 0 60 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame 427319903">
<g id="Union">
<mask id="path-1-outside-1_0_2950" maskUnits="userSpaceOnUse" x="0" y="0" width="60" height="15" fill="black">
<rect fill="white" width="60" height="15"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H2V14H1V1ZM53.5 7L7.5 7V4.46891L3 7.5L7.5 10.5311V8L53.5 8V10.5311L58 7.5V14H59V1H58V7.5L53.5 4.46891V7Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H2V14H1V1ZM53.5 7L7.5 7V4.46891L3 7.5L7.5 10.5311V8L53.5 8V10.5311L58 7.5V14H59V1H58V7.5L53.5 4.46891V7Z" fill="white"/>
<path d="M2 1H3V0H2V1ZM1 1V0H0V1H1ZM2 14V15H3V14H2ZM1 14H0V15H1V14ZM53.5 7V8H54.5V7H53.5ZM7.5 7H6.5V8H7.5V7ZM7.5 4.46891H8.5V2.58964L6.94134 3.63952L7.5 4.46891ZM3 7.5L2.44134 6.6706L1.21 7.5L2.44134 8.3294L3 7.5ZM7.5 10.5311L6.94134 11.3605L8.5 12.4104V10.5311H7.5ZM7.5 8V7H6.5V8H7.5ZM53.5 8H54.5V7H53.5V8ZM53.5 10.5311H52.5V12.4104L54.0587 11.3605L53.5 10.5311ZM58 14H57V15H58V14ZM59 14V15H60V14H59ZM59 1H60V0H59V1ZM58 1V0H57V1H58ZM53.5 4.46891L54.0587 3.63952L52.5 2.58964V4.46891H53.5ZM2 1V0H1V1V2H2V1ZM2 14H3V1H2H1V14H2ZM1 14V15H2V14V13H1V14ZM1 1H0V14H1H2V1H1ZM53.5 7V6L7.5 6V7V8L53.5 8V7ZM7.5 7H8.5V4.46891H7.5H6.5V7H7.5ZM7.5 4.46891L6.94134 3.63952L2.44134 6.6706L3 7.5L3.55866 8.3294L8.05866 5.29831L7.5 4.46891ZM3 7.5L2.44134 8.3294L6.94134 11.3605L7.5 10.5311L8.05866 9.70169L3.55866 6.6706L3 7.5ZM7.5 10.5311H8.5V8H7.5H6.5V10.5311H7.5ZM7.5 8V9L53.5 9V8V7L7.5 7V8ZM53.5 8H52.5V10.5311H53.5H54.5V8H53.5ZM53.5 10.5311L54.0587 11.3605L58.5587 8.3294L58 7.5L57.4413 6.6706L52.9413 9.70169L53.5 10.5311ZM58 14H59V7.5H58H57V14H58ZM59 14V13H58V14V15H59V14ZM59 1H58V14H59H60V1H59ZM58 1V2H59V1V0H58V1ZM58 7.5H59V1H58H57V7.5H58ZM58 7.5L58.5587 6.6706L54.0587 3.63952L53.5 4.46891L52.9413 5.29831L57.4413 8.3294L58 7.5ZM53.5 4.46891H52.5V7H53.5H54.5V4.46891H53.5Z" fill="black" mask="url(#path-1-outside-1_0_2950)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,12 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="60" height="15" viewBox="0 0 60 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame 427319903">
<g id="Union">
<mask id="path-1-outside-1_0_8" maskUnits="userSpaceOnUse" x="0" y="0" width="60" height="15" fill="black">
<rect fill="white" width="60" height="15"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H2V14H1V1ZM53.5 7L7.5 7V4.46891L3 7.5L7.5 10.5311V8L53.5 8V10.5311L58 7.5V14H59V1H58V7.5L53.5 4.46891V7Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H2V14H1V1ZM53.5 7L7.5 7V4.46891L3 7.5L7.5 10.5311V8L53.5 8V10.5311L58 7.5V14H59V1H58V7.5L53.5 4.46891V7Z" fill="#FF5C00"/>
<path d="M2 1H3V0H2V1ZM1 1V0H0V1H1ZM2 14V15H3V14H2ZM1 14H0V15H1V14ZM53.5 7V8H54.5V7H53.5ZM7.5 7H6.5V8H7.5V7ZM7.5 4.46891H8.5V2.58964L6.94134 3.63952L7.5 4.46891ZM3 7.5L2.44134 6.6706L1.21 7.5L2.44134 8.3294L3 7.5ZM7.5 10.5311L6.94134 11.3605L8.5 12.4104V10.5311H7.5ZM7.5 8V7H6.5V8H7.5ZM53.5 8H54.5V7H53.5V8ZM53.5 10.5311H52.5V12.4104L54.0587 11.3605L53.5 10.5311ZM58 14H57V15H58V14ZM59 14V15H60V14H59ZM59 1H60V0H59V1ZM58 1V0H57V1H58ZM53.5 4.46891L54.0587 3.63952L52.5 2.58964V4.46891H53.5ZM2 1V0H1V1V2H2V1ZM2 14H3V1H2H1V14H2ZM1 14V15H2V14V13H1V14ZM1 1H0V14H1H2V1H1ZM53.5 7V6L7.5 6V7V8L53.5 8V7ZM7.5 7H8.5V4.46891H7.5H6.5V7H7.5ZM7.5 4.46891L6.94134 3.63952L2.44134 6.6706L3 7.5L3.55866 8.3294L8.05866 5.29831L7.5 4.46891ZM3 7.5L2.44134 8.3294L6.94134 11.3605L7.5 10.5311L8.05866 9.70169L3.55866 6.6706L3 7.5ZM7.5 10.5311H8.5V8H7.5H6.5V10.5311H7.5ZM7.5 8V9L53.5 9V8V7L7.5 7V8ZM53.5 8H52.5V10.5311H53.5H54.5V8H53.5ZM53.5 10.5311L54.0587 11.3605L58.5587 8.3294L58 7.5L57.4413 6.6706L52.9413 9.70169L53.5 10.5311ZM58 14H59V7.5H58H57V14H58ZM59 14V13H58V14V15H59V14ZM59 1H58V14H59H60V1H59ZM58 1V2H59V1V0H58V1ZM58 7.5H59V1H58H57V7.5H58ZM58 7.5L58.5587 6.6706L54.0587 3.63952L53.5 4.46891L52.9413 5.29831L57.4413 8.3294L58 7.5ZM53.5 4.46891H52.5V7H53.5H54.5V4.46891H53.5Z" fill="black" mask="url(#path-1-outside-1_0_8)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,12 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="60" height="15" viewBox="0 0 60 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame 427319903">
<g id="Union">
<mask id="path-1-outside-1_0_15" maskUnits="userSpaceOnUse" x="0" y="0" width="60" height="15" fill="black">
<rect fill="white" width="60" height="15"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H2V14H1V1ZM53.5 7L7.5 7V4.46891L3 7.5L7.5 10.5311V8L53.5 8V10.5311L58 7.5V14H59V1H58V7.5L53.5 4.46891V7Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H2V14H1V1ZM53.5 7L7.5 7V4.46891L3 7.5L7.5 10.5311V8L53.5 8V10.5311L58 7.5V14H59V1H58V7.5L53.5 4.46891V7Z" fill="#F6F4F2"/>
<path d="M2 1H3V0H2V1ZM1 1V0H0V1H1ZM2 14V15H3V14H2ZM1 14H0V15H1V14ZM53.5 7V8H54.5V7H53.5ZM7.5 7H6.5V8H7.5V7ZM7.5 4.46891H8.5V2.58964L6.94134 3.63952L7.5 4.46891ZM3 7.5L2.44134 6.6706L1.21 7.5L2.44134 8.3294L3 7.5ZM7.5 10.5311L6.94134 11.3605L8.5 12.4104V10.5311H7.5ZM7.5 8V7H6.5V8H7.5ZM53.5 8H54.5V7H53.5V8ZM53.5 10.5311H52.5V12.4104L54.0587 11.3605L53.5 10.5311ZM58 14H57V15H58V14ZM59 14V15H60V14H59ZM59 1H60V0H59V1ZM58 1V0H57V1H58ZM53.5 4.46891L54.0587 3.63952L52.5 2.58964V4.46891H53.5ZM2 1V0H1V1V2H2V1ZM2 14H3V1H2H1V14H2ZM1 14V15H2V14V13H1V14ZM1 1H0V14H1H2V1H1ZM53.5 7V6L7.5 6V7V8L53.5 8V7ZM7.5 7H8.5V4.46891H7.5H6.5V7H7.5ZM7.5 4.46891L6.94134 3.63952L2.44134 6.6706L3 7.5L3.55866 8.3294L8.05866 5.29831L7.5 4.46891ZM3 7.5L2.44134 8.3294L6.94134 11.3605L7.5 10.5311L8.05866 9.70169L3.55866 6.6706L3 7.5ZM7.5 10.5311H8.5V8H7.5H6.5V10.5311H7.5ZM7.5 8V9L53.5 9V8V7L7.5 7V8ZM53.5 8H52.5V10.5311H53.5H54.5V8H53.5ZM53.5 10.5311L54.0587 11.3605L58.5587 8.3294L58 7.5L57.4413 6.6706L52.9413 9.70169L53.5 10.5311ZM58 14H59V7.5H58H57V14H58ZM59 14V13H58V14V15H59V14ZM59 1H58V14H59H60V1H59ZM58 1V2H59V1V0H58V1ZM58 7.5H59V1H58H57V7.5H58ZM58 7.5L58.5587 6.6706L54.0587 3.63952L53.5 4.46891L52.9413 5.29831L57.4413 8.3294L58 7.5ZM53.5 4.46891H52.5V7H53.5H54.5V4.46891H53.5Z" fill="black" mask="url(#path-1-outside-1_0_15)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,40 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="40" height="38" viewBox="0 0 40 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="&#234;&#190;&#184;&#235;&#159;&#172;&#235;&#175;&#184;ver2/&#237;&#140;&#157;&#236;&#151;&#133;&#234;&#176;&#128;&#236;&#157;&#180;&#235;&#147;&#156; ">
<rect id="Rectangle 6961" x="0.5" y="0.5" width="39" height="37" rx="1.5" fill="url(#paint0_linear_0_3057)" stroke="url(#paint1_linear_0_3057)"/>
<g id="Union" filter="url(#filter0_d_0_3057)">
<mask id="path-2-outside-1_0_3057" maskUnits="userSpaceOnUse" x="10" y="9.5" width="20" height="20" fill="black">
<rect fill="white" x="10" y="9.5" width="20" height="20"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 12H27C27.2761 12 27.5 12.2239 27.5 12.5V26.5C27.5 26.7761 27.2761 27 27 27H13C12.7239 27 12.5 26.7761 12.5 26.5V12.5C12.5 12.2239 12.7239 12 13 12ZM11 12.5C11 11.3954 11.8954 10.5 13 10.5H27C28.1046 10.5 29 11.3954 29 12.5V26.5C29 27.6046 28.1046 28.5 27 28.5H13C11.8954 28.5 11 27.6046 11 26.5V12.5ZM17.3152 14.424V24H19.0312V22.32L20.0032 21.216L21.6352 24H23.5432L20.9992 20.028L23.3272 17.28H21.4072L19.0792 20.184H19.0312V14.424H17.3152Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 12H27C27.2761 12 27.5 12.2239 27.5 12.5V26.5C27.5 26.7761 27.2761 27 27 27H13C12.7239 27 12.5 26.7761 12.5 26.5V12.5C12.5 12.2239 12.7239 12 13 12ZM11 12.5C11 11.3954 11.8954 10.5 13 10.5H27C28.1046 10.5 29 11.3954 29 12.5V26.5C29 27.6046 28.1046 28.5 27 28.5H13C11.8954 28.5 11 27.6046 11 26.5V12.5ZM17.3152 14.424V24H19.0312V22.32L20.0032 21.216L21.6352 24H23.5432L20.9992 20.028L23.3272 17.28H21.4072L19.0792 20.184H19.0312V14.424H17.3152Z" fill="#FFC600"/>
<path d="M17.3152 24H16.3152V25H17.3152V24ZM17.3152 14.424V13.424H16.3152V14.424H17.3152ZM19.0312 24V25H20.0312V24H19.0312ZM19.0312 22.32L18.2806 21.6592L18.0312 21.9425V22.32H19.0312ZM20.0032 21.216L20.8659 20.7103L20.1665 19.5172L19.2526 20.5552L20.0032 21.216ZM21.6352 24L20.7725 24.5057L21.0622 25H21.6352V24ZM23.5432 24V25H25.3712L24.3853 23.4607L23.5432 24ZM20.9992 20.028L20.2362 19.3816L19.7587 19.9453L20.1571 20.5673L20.9992 20.028ZM23.3272 17.28L24.0902 17.9264L25.485 16.28H23.3272V17.28ZM21.4072 17.28V16.28H20.9272L20.6269 16.6545L21.4072 17.28ZM19.0792 20.184V21.184H19.5592L19.8594 20.8095L19.0792 20.184ZM19.0312 20.184H18.0312V21.184H19.0312V20.184ZM19.0312 14.424H20.0312V13.424H19.0312V14.424ZM27 12V11H13V12V13H27V12ZM27.5 12.5H28.5C28.5 11.6716 27.8284 11 27 11V12V13C26.7239 13 26.5 12.7761 26.5 12.5H27.5ZM27.5 26.5H28.5V12.5H27.5H26.5V26.5H27.5ZM27 27V28C27.8284 28 28.5 27.3284 28.5 26.5H27.5H26.5C26.5 26.2239 26.7239 26 27 26V27ZM13 27V28H27V27V26H13V27ZM12.5 26.5H11.5C11.5 27.3284 12.1716 28 13 28V27V26C13.2761 26 13.5 26.2239 13.5 26.5H12.5ZM12.5 12.5H11.5V26.5H12.5H13.5V12.5H12.5ZM13 12V11C12.1716 11 11.5 11.6716 11.5 12.5H12.5H13.5C13.5 12.7761 13.2761 13 13 13V12ZM13 10.5V9.5C11.3431 9.5 10 10.8431 10 12.5H11H12C12 11.9477 12.4477 11.5 13 11.5V10.5ZM27 10.5V9.5H13V10.5V11.5H27V10.5ZM29 12.5H30C30 10.8431 28.6569 9.5 27 9.5V10.5V11.5C27.5523 11.5 28 11.9477 28 12.5H29ZM29 26.5H30V12.5H29H28V26.5H29ZM27 28.5V29.5C28.6569 29.5 30 28.1569 30 26.5H29H28C28 27.0523 27.5523 27.5 27 27.5V28.5ZM13 28.5V29.5H27V28.5V27.5H13V28.5ZM11 26.5H10C10 28.1569 11.3431 29.5 13 29.5V28.5V27.5C12.4477 27.5 12 27.0523 12 26.5H11ZM11 12.5H10V26.5H11H12V12.5H11ZM17.3152 24H18.3152V14.424H17.3152H16.3152V24H17.3152ZM19.0312 24V23H17.3152V24V25H19.0312V24ZM19.0312 22.32H18.0312V24H19.0312H20.0312V22.32H19.0312ZM20.0032 21.216L19.2526 20.5552L18.2806 21.6592L19.0312 22.32L19.7817 22.9808L20.7537 21.8768L20.0032 21.216ZM21.6352 24L22.4979 23.4943L20.8659 20.7103L20.0032 21.216L19.1405 21.7217L20.7725 24.5057L21.6352 24ZM23.5432 24V23H21.6352V24V25H23.5432V24ZM20.9992 20.028L20.1571 20.5673L22.7011 24.5393L23.5432 24L24.3853 23.4607L21.8413 19.4887L20.9992 20.028ZM23.3272 17.28L22.5642 16.6336L20.2362 19.3816L20.9992 20.028L21.7622 20.6744L24.0902 17.9264L23.3272 17.28ZM21.4072 17.28V18.28H23.3272V17.28V16.28H21.4072V17.28ZM19.0792 20.184L19.8594 20.8095L22.1874 17.9055L21.4072 17.28L20.6269 16.6545L18.2989 19.5585L19.0792 20.184ZM19.0312 20.184V21.184H19.0792V20.184V19.184H19.0312V20.184ZM19.0312 14.424H18.0312V20.184H19.0312H20.0312V14.424H19.0312ZM17.3152 14.424V15.424H19.0312V14.424V13.424H17.3152V14.424Z" fill="black" mask="url(#path-2-outside-1_0_3057)"/>
</g>
</g>
<defs>
<filter id="filter0_d_0_3057" x="6.77366" y="6.27366" width="26.4527" height="26.4527" 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="1.61317"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0235294 0 0 0 0 0.101961 0 0 0 0 0.0784314 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_3057"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_3057" result="shape"/>
</filter>
<linearGradient id="paint0_linear_0_3057" x1="20" y1="0" x2="20" y2="38" gradientUnits="userSpaceOnUse">
<stop stop-color="#081813"/>
<stop offset="0.086884" stop-color="#09271E"/>
<stop offset="0.254279" stop-color="#0B3227"/>
<stop offset="0.512521" stop-color="#0E3D2F"/>
<stop offset="0.800267" stop-color="#0B3227"/>
<stop offset="0.897594" stop-color="#0A2B21"/>
<stop offset="1" stop-color="#081813"/>
</linearGradient>
<linearGradient id="paint1_linear_0_3057" x1="20" y1="0" x2="20" y2="38" gradientUnits="userSpaceOnUse">
<stop stop-color="#08251C"/>
<stop offset="0.465449" stop-color="#208769"/>
<stop offset="0.844436" stop-color="#08241C"/>
<stop offset="1" stop-color="#051612"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

+40
View File
@@ -0,0 +1,40 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="40" height="38" viewBox="0 0 40 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="&#234;&#190;&#184;&#235;&#159;&#172;&#235;&#175;&#184;ver2/&#237;&#140;&#157;&#236;&#151;&#133;&#234;&#176;&#128;&#236;&#157;&#180;&#235;&#147;&#156; ">
<rect id="Rectangle 6961" x="0.5" y="0.5" width="39" height="37" rx="1.5" fill="url(#paint0_linear_0_3057)" stroke="url(#paint1_linear_0_3057)"/>
<g id="Union" filter="url(#filter0_d_0_3057)">
<mask id="path-2-outside-1_0_3057" maskUnits="userSpaceOnUse" x="10" y="9.5" width="20" height="20" fill="black">
<rect fill="white" x="10" y="9.5" width="20" height="20"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 12H27C27.2761 12 27.5 12.2239 27.5 12.5V26.5C27.5 26.7761 27.2761 27 27 27H13C12.7239 27 12.5 26.7761 12.5 26.5V12.5C12.5 12.2239 12.7239 12 13 12ZM11 12.5C11 11.3954 11.8954 10.5 13 10.5H27C28.1046 10.5 29 11.3954 29 12.5V26.5C29 27.6046 28.1046 28.5 27 28.5H13C11.8954 28.5 11 27.6046 11 26.5V12.5ZM17.3152 14.424V24H19.0312V22.32L20.0032 21.216L21.6352 24H23.5432L20.9992 20.028L23.3272 17.28H21.4072L19.0792 20.184H19.0312V14.424H17.3152Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 12H27C27.2761 12 27.5 12.2239 27.5 12.5V26.5C27.5 26.7761 27.2761 27 27 27H13C12.7239 27 12.5 26.7761 12.5 26.5V12.5C12.5 12.2239 12.7239 12 13 12ZM11 12.5C11 11.3954 11.8954 10.5 13 10.5H27C28.1046 10.5 29 11.3954 29 12.5V26.5C29 27.6046 28.1046 28.5 27 28.5H13C11.8954 28.5 11 27.6046 11 26.5V12.5ZM17.3152 14.424V24H19.0312V22.32L20.0032 21.216L21.6352 24H23.5432L20.9992 20.028L23.3272 17.28H21.4072L19.0792 20.184H19.0312V14.424H17.3152Z" fill="#B6D0C9"/>
<path d="M17.3152 24H16.3152V25H17.3152V24ZM17.3152 14.424V13.424H16.3152V14.424H17.3152ZM19.0312 24V25H20.0312V24H19.0312ZM19.0312 22.32L18.2806 21.6592L18.0312 21.9425V22.32H19.0312ZM20.0032 21.216L20.8659 20.7103L20.1665 19.5172L19.2526 20.5552L20.0032 21.216ZM21.6352 24L20.7725 24.5057L21.0622 25H21.6352V24ZM23.5432 24V25H25.3712L24.3853 23.4607L23.5432 24ZM20.9992 20.028L20.2362 19.3816L19.7587 19.9453L20.1571 20.5673L20.9992 20.028ZM23.3272 17.28L24.0902 17.9264L25.485 16.28H23.3272V17.28ZM21.4072 17.28V16.28H20.9272L20.6269 16.6545L21.4072 17.28ZM19.0792 20.184V21.184H19.5592L19.8594 20.8095L19.0792 20.184ZM19.0312 20.184H18.0312V21.184H19.0312V20.184ZM19.0312 14.424H20.0312V13.424H19.0312V14.424ZM27 12V11H13V12V13H27V12ZM27.5 12.5H28.5C28.5 11.6716 27.8284 11 27 11V12V13C26.7239 13 26.5 12.7761 26.5 12.5H27.5ZM27.5 26.5H28.5V12.5H27.5H26.5V26.5H27.5ZM27 27V28C27.8284 28 28.5 27.3284 28.5 26.5H27.5H26.5C26.5 26.2239 26.7239 26 27 26V27ZM13 27V28H27V27V26H13V27ZM12.5 26.5H11.5C11.5 27.3284 12.1716 28 13 28V27V26C13.2761 26 13.5 26.2239 13.5 26.5H12.5ZM12.5 12.5H11.5V26.5H12.5H13.5V12.5H12.5ZM13 12V11C12.1716 11 11.5 11.6716 11.5 12.5H12.5H13.5C13.5 12.7761 13.2761 13 13 13V12ZM13 10.5V9.5C11.3431 9.5 10 10.8431 10 12.5H11H12C12 11.9477 12.4477 11.5 13 11.5V10.5ZM27 10.5V9.5H13V10.5V11.5H27V10.5ZM29 12.5H30C30 10.8431 28.6569 9.5 27 9.5V10.5V11.5C27.5523 11.5 28 11.9477 28 12.5H29ZM29 26.5H30V12.5H29H28V26.5H29ZM27 28.5V29.5C28.6569 29.5 30 28.1569 30 26.5H29H28C28 27.0523 27.5523 27.5 27 27.5V28.5ZM13 28.5V29.5H27V28.5V27.5H13V28.5ZM11 26.5H10C10 28.1569 11.3431 29.5 13 29.5V28.5V27.5C12.4477 27.5 12 27.0523 12 26.5H11ZM11 12.5H10V26.5H11H12V12.5H11ZM17.3152 24H18.3152V14.424H17.3152H16.3152V24H17.3152ZM19.0312 24V23H17.3152V24V25H19.0312V24ZM19.0312 22.32H18.0312V24H19.0312H20.0312V22.32H19.0312ZM20.0032 21.216L19.2526 20.5552L18.2806 21.6592L19.0312 22.32L19.7817 22.9808L20.7537 21.8768L20.0032 21.216ZM21.6352 24L22.4979 23.4943L20.8659 20.7103L20.0032 21.216L19.1405 21.7217L20.7725 24.5057L21.6352 24ZM23.5432 24V23H21.6352V24V25H23.5432V24ZM20.9992 20.028L20.1571 20.5673L22.7011 24.5393L23.5432 24L24.3853 23.4607L21.8413 19.4887L20.9992 20.028ZM23.3272 17.28L22.5642 16.6336L20.2362 19.3816L20.9992 20.028L21.7622 20.6744L24.0902 17.9264L23.3272 17.28ZM21.4072 17.28V18.28H23.3272V17.28V16.28H21.4072V17.28ZM19.0792 20.184L19.8594 20.8095L22.1874 17.9055L21.4072 17.28L20.6269 16.6545L18.2989 19.5585L19.0792 20.184ZM19.0312 20.184V21.184H19.0792V20.184V19.184H19.0312V20.184ZM19.0312 14.424H18.0312V20.184H19.0312H20.0312V14.424H19.0312ZM17.3152 14.424V15.424H19.0312V14.424V13.424H17.3152V14.424Z" fill="black" mask="url(#path-2-outside-1_0_3057)"/>
</g>
</g>
<defs>
<filter id="filter0_d_0_3057" x="6.77366" y="6.27366" width="26.4527" height="26.4527" 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="1.61317"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0235294 0 0 0 0 0.101961 0 0 0 0 0.0784314 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_3057"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_3057" result="shape"/>
</filter>
<linearGradient id="paint0_linear_0_3057" x1="20" y1="0" x2="20" y2="38" gradientUnits="userSpaceOnUse">
<stop stop-color="#081813"/>
<stop offset="0.086884" stop-color="#09271E"/>
<stop offset="0.254279" stop-color="#0B3227"/>
<stop offset="0.512521" stop-color="#0E3D2F"/>
<stop offset="0.800267" stop-color="#0B3227"/>
<stop offset="0.897594" stop-color="#0A2B21"/>
<stop offset="1" stop-color="#081813"/>
</linearGradient>
<linearGradient id="paint1_linear_0_3057" x1="20" y1="0" x2="20" y2="38" gradientUnits="userSpaceOnUse">
<stop stop-color="#08251C"/>
<stop offset="0.465449" stop-color="#208769"/>
<stop offset="0.844436" stop-color="#08241C"/>
<stop offset="1" stop-color="#051612"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="22.9091" viewBox="0 0 24 22.9091" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="print" clip-path="url(#clip0_0_2981)">
<g id="Union" filter="url(#filter0_d_0_2981)">
<mask id="path-1-outside-1_0_2981" maskUnits="userSpaceOnUse" x="2" y="2.36328" width="20" height="18" fill="black">
<rect fill="white" x="2" y="2.36328" width="20" height="18"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.4 7.66328H18.3C19.79 7.66328 21 8.81328 21 10.2433V15.3933H17.4V18.8333H6.6V15.3933H3V10.2433C3 8.81328 4.21 7.66328 5.7 7.66328H6.6V3.36328H17.4V7.66328ZM15.6 5.08328H8.4V7.66328H15.6V5.08328ZM8.4 17.1133H15.6V13.6733H8.4V17.1133ZM17.4 11.9533V13.6733H19.2V10.2333C19.2 9.76328 18.8 9.37328 18.3 9.37328H5.7C5.2 9.37328 4.8 9.76328 4.8 10.2333V13.6733H6.6V11.9533H17.4ZM18.3 10.6633C18.3 11.1382 17.8971 11.5233 17.4 11.5233C16.9029 11.5233 16.5 11.1382 16.5 10.6633C16.5 10.1883 16.9029 9.80328 17.4 9.80328C17.8971 9.80328 18.3 10.1883 18.3 10.6633Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.4 7.66328H18.3C19.79 7.66328 21 8.81328 21 10.2433V15.3933H17.4V18.8333H6.6V15.3933H3V10.2433C3 8.81328 4.21 7.66328 5.7 7.66328H6.6V3.36328H17.4V7.66328ZM15.6 5.08328H8.4V7.66328H15.6V5.08328ZM8.4 17.1133H15.6V13.6733H8.4V17.1133ZM17.4 11.9533V13.6733H19.2V10.2333C19.2 9.76328 18.8 9.37328 18.3 9.37328H5.7C5.2 9.37328 4.8 9.76328 4.8 10.2333V13.6733H6.6V11.9533H17.4ZM18.3 10.6633C18.3 11.1382 17.8971 11.5233 17.4 11.5233C16.9029 11.5233 16.5 11.1382 16.5 10.6633C16.5 10.1883 16.9029 9.80328 17.4 9.80328C17.8971 9.80328 18.3 10.1883 18.3 10.6633Z" fill="#FFC600"/>
<path d="M17.4 7.66328H16.5V8.56328H17.4V7.66328ZM21 15.3933V16.2933H21.9V15.3933H21ZM17.4 15.3933V14.4933H16.5V15.3933H17.4ZM17.4 18.8333V19.7333H18.3V18.8333H17.4ZM6.6 18.8333H5.7V19.7333H6.6V18.8333ZM6.6 15.3933H7.5V14.4933H6.6V15.3933ZM3 15.3933H2.1V16.2933H3V15.3933ZM6.6 7.66328V8.56328H7.5V7.66328H6.6ZM6.6 3.36328V2.46328H5.7V3.36328H6.6ZM17.4 3.36328H18.3V2.46328H17.4V3.36328ZM8.4 5.08328V4.18328H7.5V5.08328H8.4ZM15.6 5.08328H16.5V4.18328H15.6V5.08328ZM8.4 7.66328H7.5V8.56328H8.4V7.66328ZM15.6 7.66328V8.56328H16.5V7.66328H15.6ZM15.6 17.1133V18.0133H16.5V17.1133H15.6ZM8.4 17.1133H7.5V18.0133H8.4V17.1133ZM15.6 13.6733H16.5V12.7733H15.6V13.6733ZM8.4 13.6733V12.7733H7.5V13.6733H8.4ZM17.4 13.6733H16.5V14.5733H17.4V13.6733ZM17.4 11.9533H18.3V11.0533H17.4V11.9533ZM19.2 13.6733V14.5733H20.1V13.6733H19.2ZM4.8 13.6733H3.9V14.5733H4.8V13.6733ZM6.6 13.6733V14.5733H7.5V13.6733H6.6ZM6.6 11.9533V11.0533H5.7V11.9533H6.6ZM18.3 7.66328V6.76328H17.4V7.66328V8.56328H18.3V7.66328ZM21 10.2433H21.9C21.9 8.27672 20.2466 6.76328 18.3 6.76328V7.66328V8.56328C19.3334 8.56328 20.1 9.34984 20.1 10.2433H21ZM21 15.3933H21.9V10.2433H21H20.1V15.3933H21ZM17.4 15.3933V16.2933H21V15.3933V14.4933H17.4V15.3933ZM17.4 18.8333H18.3V15.3933H17.4H16.5V18.8333H17.4ZM6.6 18.8333V19.7333H17.4V18.8333V17.9333H6.6V18.8333ZM6.6 15.3933H5.7V18.8333H6.6H7.5V15.3933H6.6ZM3 15.3933V16.2933H6.6V15.3933V14.4933H3V15.3933ZM3 10.2433H2.1V15.3933H3H3.9V10.2433H3ZM5.7 7.66328V6.76328C3.75339 6.76328 2.1 8.27672 2.1 10.2433H3H3.9C3.9 9.34984 4.66661 8.56328 5.7 8.56328V7.66328ZM6.6 7.66328V6.76328H5.7V7.66328V8.56328H6.6V7.66328ZM6.6 3.36328H5.7V7.66328H6.6H7.5V3.36328H6.6ZM17.4 3.36328V2.46328H6.6V3.36328V4.26328H17.4V3.36328ZM17.4 7.66328H18.3V3.36328H17.4H16.5V7.66328H17.4ZM8.4 5.08328V5.98328H15.6V5.08328V4.18328H8.4V5.08328ZM8.4 7.66328H9.3V5.08328H8.4H7.5V7.66328H8.4ZM15.6 7.66328V6.76328H8.4V7.66328V8.56328H15.6V7.66328ZM15.6 5.08328H14.7V7.66328H15.6H16.5V5.08328H15.6ZM15.6 17.1133V16.2133H8.4V17.1133V18.0133H15.6V17.1133ZM15.6 13.6733H14.7V17.1133H15.6H16.5V13.6733H15.6ZM8.4 13.6733V14.5733H15.6V13.6733V12.7733H8.4V13.6733ZM8.4 17.1133H9.3V13.6733H8.4H7.5V17.1133H8.4ZM17.4 13.6733H18.3V11.9533H17.4H16.5V13.6733H17.4ZM19.2 13.6733V12.7733H17.4V13.6733V14.5733H19.2V13.6733ZM19.2 10.2333H18.3V13.6733H19.2H20.1V10.2333H19.2ZM18.3 9.37328V10.2733C18.3072 10.2733 18.3108 10.2746 18.3121 10.2751C18.314 10.2759 18.3144 10.2765 18.3137 10.2758C18.313 10.2751 18.3097 10.2717 18.3063 10.2641C18.3026 10.2557 18.3 10.2447 18.3 10.2333H19.2H20.1C20.1 9.23328 19.2635 8.47328 18.3 8.47328V9.37328ZM5.7 9.37328V10.2733H18.3V9.37328V8.47328H5.7V9.37328ZM4.8 10.2333H5.7C5.7 10.2447 5.69736 10.2557 5.69367 10.2641C5.69029 10.2717 5.68701 10.2751 5.6863 10.2758C5.68555 10.2765 5.686 10.2759 5.68789 10.2751C5.68922 10.2746 5.69279 10.2733 5.7 10.2733V9.37328V8.47328C4.73654 8.47328 3.9 9.23328 3.9 10.2333H4.8ZM4.8 13.6733H5.7V10.2333H4.8H3.9V13.6733H4.8ZM6.6 13.6733V12.7733H4.8V13.6733V14.5733H6.6V13.6733ZM6.6 11.9533H5.7V13.6733H6.6H7.5V11.9533H6.6ZM17.4 11.9533V11.0533H6.6V11.9533V12.8533H17.4V11.9533ZM17.4 11.5233V12.4233C18.3551 12.4233 19.2 11.6734 19.2 10.6633H18.3H17.4C17.4 10.6534 17.4024 10.6427 17.4063 10.6337C17.4099 10.6256 17.4136 10.6217 17.4146 10.6207C17.4166 10.6188 17.4134 10.6233 17.4 10.6233V11.5233ZM16.5 10.6633H15.6C15.6 11.6734 16.4449 12.4233 17.4 12.4233V11.5233V10.6233C17.3866 10.6233 17.3834 10.6188 17.3854 10.6207C17.3864 10.6217 17.3901 10.6256 17.3937 10.6337C17.3976 10.6427 17.4 10.6534 17.4 10.6633H16.5ZM17.4 9.80328V8.90328C16.4449 8.90328 15.6 9.65314 15.6 10.6633H16.5H17.4C17.4 10.6732 17.3976 10.6839 17.3937 10.6928C17.3901 10.701 17.3864 10.7048 17.3854 10.7059C17.3834 10.7078 17.3866 10.7033 17.4 10.7033V9.80328ZM18.3 10.6633H19.2C19.2 9.65314 18.3551 8.90328 17.4 8.90328V9.80328V10.7033C17.4134 10.7033 17.4166 10.7078 17.4146 10.7059C17.4136 10.7048 17.4099 10.701 17.4063 10.6928C17.4024 10.6839 17.4 10.6732 17.4 10.6633H18.3Z" fill="black" mask="url(#path-1-outside-1_0_2981)"/>
</g>
</g>
<defs>
<filter id="filter0_d_0_2981" x="-1.13" y="-0.766719" width="26.26" height="23.73" 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="1.615"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0235294 0 0 0 0 0.101961 0 0 0 0 0.0784314 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_2981"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_2981" result="shape"/>
</filter>
<clipPath id="clip0_0_2981">
<rect width="24" height="22.9091" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="22.9091" viewBox="0 0 24 22.9091" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="print" clip-path="url(#clip0_0_2981)">
<g id="Union" filter="url(#filter0_d_0_2981)">
<mask id="path-1-outside-1_0_2981" maskUnits="userSpaceOnUse" x="2" y="2.36328" width="20" height="18" fill="black">
<rect fill="white" x="2" y="2.36328" width="20" height="18"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.4 7.66328H18.3C19.79 7.66328 21 8.81328 21 10.2433V15.3933H17.4V18.8333H6.6V15.3933H3V10.2433C3 8.81328 4.21 7.66328 5.7 7.66328H6.6V3.36328H17.4V7.66328ZM15.6 5.08328H8.4V7.66328H15.6V5.08328ZM8.4 17.1133H15.6V13.6733H8.4V17.1133ZM17.4 11.9533V13.6733H19.2V10.2333C19.2 9.76328 18.8 9.37328 18.3 9.37328H5.7C5.2 9.37328 4.8 9.76328 4.8 10.2333V13.6733H6.6V11.9533H17.4ZM18.3 10.6633C18.3 11.1382 17.8971 11.5233 17.4 11.5233C16.9029 11.5233 16.5 11.1382 16.5 10.6633C16.5 10.1883 16.9029 9.80328 17.4 9.80328C17.8971 9.80328 18.3 10.1883 18.3 10.6633Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.4 7.66328H18.3C19.79 7.66328 21 8.81328 21 10.2433V15.3933H17.4V18.8333H6.6V15.3933H3V10.2433C3 8.81328 4.21 7.66328 5.7 7.66328H6.6V3.36328H17.4V7.66328ZM15.6 5.08328H8.4V7.66328H15.6V5.08328ZM8.4 17.1133H15.6V13.6733H8.4V17.1133ZM17.4 11.9533V13.6733H19.2V10.2333C19.2 9.76328 18.8 9.37328 18.3 9.37328H5.7C5.2 9.37328 4.8 9.76328 4.8 10.2333V13.6733H6.6V11.9533H17.4ZM18.3 10.6633C18.3 11.1382 17.8971 11.5233 17.4 11.5233C16.9029 11.5233 16.5 11.1382 16.5 10.6633C16.5 10.1883 16.9029 9.80328 17.4 9.80328C17.8971 9.80328 18.3 10.1883 18.3 10.6633Z" fill="#B6D0C9"/>
<path d="M17.4 7.66328H16.5V8.56328H17.4V7.66328ZM21 15.3933V16.2933H21.9V15.3933H21ZM17.4 15.3933V14.4933H16.5V15.3933H17.4ZM17.4 18.8333V19.7333H18.3V18.8333H17.4ZM6.6 18.8333H5.7V19.7333H6.6V18.8333ZM6.6 15.3933H7.5V14.4933H6.6V15.3933ZM3 15.3933H2.1V16.2933H3V15.3933ZM6.6 7.66328V8.56328H7.5V7.66328H6.6ZM6.6 3.36328V2.46328H5.7V3.36328H6.6ZM17.4 3.36328H18.3V2.46328H17.4V3.36328ZM8.4 5.08328V4.18328H7.5V5.08328H8.4ZM15.6 5.08328H16.5V4.18328H15.6V5.08328ZM8.4 7.66328H7.5V8.56328H8.4V7.66328ZM15.6 7.66328V8.56328H16.5V7.66328H15.6ZM15.6 17.1133V18.0133H16.5V17.1133H15.6ZM8.4 17.1133H7.5V18.0133H8.4V17.1133ZM15.6 13.6733H16.5V12.7733H15.6V13.6733ZM8.4 13.6733V12.7733H7.5V13.6733H8.4ZM17.4 13.6733H16.5V14.5733H17.4V13.6733ZM17.4 11.9533H18.3V11.0533H17.4V11.9533ZM19.2 13.6733V14.5733H20.1V13.6733H19.2ZM4.8 13.6733H3.9V14.5733H4.8V13.6733ZM6.6 13.6733V14.5733H7.5V13.6733H6.6ZM6.6 11.9533V11.0533H5.7V11.9533H6.6ZM18.3 7.66328V6.76328H17.4V7.66328V8.56328H18.3V7.66328ZM21 10.2433H21.9C21.9 8.27672 20.2466 6.76328 18.3 6.76328V7.66328V8.56328C19.3334 8.56328 20.1 9.34984 20.1 10.2433H21ZM21 15.3933H21.9V10.2433H21H20.1V15.3933H21ZM17.4 15.3933V16.2933H21V15.3933V14.4933H17.4V15.3933ZM17.4 18.8333H18.3V15.3933H17.4H16.5V18.8333H17.4ZM6.6 18.8333V19.7333H17.4V18.8333V17.9333H6.6V18.8333ZM6.6 15.3933H5.7V18.8333H6.6H7.5V15.3933H6.6ZM3 15.3933V16.2933H6.6V15.3933V14.4933H3V15.3933ZM3 10.2433H2.1V15.3933H3H3.9V10.2433H3ZM5.7 7.66328V6.76328C3.75339 6.76328 2.1 8.27672 2.1 10.2433H3H3.9C3.9 9.34984 4.66661 8.56328 5.7 8.56328V7.66328ZM6.6 7.66328V6.76328H5.7V7.66328V8.56328H6.6V7.66328ZM6.6 3.36328H5.7V7.66328H6.6H7.5V3.36328H6.6ZM17.4 3.36328V2.46328H6.6V3.36328V4.26328H17.4V3.36328ZM17.4 7.66328H18.3V3.36328H17.4H16.5V7.66328H17.4ZM8.4 5.08328V5.98328H15.6V5.08328V4.18328H8.4V5.08328ZM8.4 7.66328H9.3V5.08328H8.4H7.5V7.66328H8.4ZM15.6 7.66328V6.76328H8.4V7.66328V8.56328H15.6V7.66328ZM15.6 5.08328H14.7V7.66328H15.6H16.5V5.08328H15.6ZM15.6 17.1133V16.2133H8.4V17.1133V18.0133H15.6V17.1133ZM15.6 13.6733H14.7V17.1133H15.6H16.5V13.6733H15.6ZM8.4 13.6733V14.5733H15.6V13.6733V12.7733H8.4V13.6733ZM8.4 17.1133H9.3V13.6733H8.4H7.5V17.1133H8.4ZM17.4 13.6733H18.3V11.9533H17.4H16.5V13.6733H17.4ZM19.2 13.6733V12.7733H17.4V13.6733V14.5733H19.2V13.6733ZM19.2 10.2333H18.3V13.6733H19.2H20.1V10.2333H19.2ZM18.3 9.37328V10.2733C18.3072 10.2733 18.3108 10.2746 18.3121 10.2751C18.314 10.2759 18.3144 10.2765 18.3137 10.2758C18.313 10.2751 18.3097 10.2717 18.3063 10.2641C18.3026 10.2557 18.3 10.2447 18.3 10.2333H19.2H20.1C20.1 9.23328 19.2635 8.47328 18.3 8.47328V9.37328ZM5.7 9.37328V10.2733H18.3V9.37328V8.47328H5.7V9.37328ZM4.8 10.2333H5.7C5.7 10.2447 5.69736 10.2557 5.69367 10.2641C5.69029 10.2717 5.68701 10.2751 5.6863 10.2758C5.68555 10.2765 5.686 10.2759 5.68789 10.2751C5.68922 10.2746 5.69279 10.2733 5.7 10.2733V9.37328V8.47328C4.73654 8.47328 3.9 9.23328 3.9 10.2333H4.8ZM4.8 13.6733H5.7V10.2333H4.8H3.9V13.6733H4.8ZM6.6 13.6733V12.7733H4.8V13.6733V14.5733H6.6V13.6733ZM6.6 11.9533H5.7V13.6733H6.6H7.5V11.9533H6.6ZM17.4 11.9533V11.0533H6.6V11.9533V12.8533H17.4V11.9533ZM17.4 11.5233V12.4233C18.3551 12.4233 19.2 11.6734 19.2 10.6633H18.3H17.4C17.4 10.6534 17.4024 10.6427 17.4063 10.6337C17.4099 10.6256 17.4136 10.6217 17.4146 10.6207C17.4166 10.6188 17.4134 10.6233 17.4 10.6233V11.5233ZM16.5 10.6633H15.6C15.6 11.6734 16.4449 12.4233 17.4 12.4233V11.5233V10.6233C17.3866 10.6233 17.3834 10.6188 17.3854 10.6207C17.3864 10.6217 17.3901 10.6256 17.3937 10.6337C17.3976 10.6427 17.4 10.6534 17.4 10.6633H16.5ZM17.4 9.80328V8.90328C16.4449 8.90328 15.6 9.65314 15.6 10.6633H16.5H17.4C17.4 10.6732 17.3976 10.6839 17.3937 10.6928C17.3901 10.701 17.3864 10.7048 17.3854 10.7059C17.3834 10.7078 17.3866 10.7033 17.4 10.7033V9.80328ZM18.3 10.6633H19.2C19.2 9.65314 18.3551 8.90328 17.4 8.90328V9.80328V10.7033C17.4134 10.7033 17.4166 10.7078 17.4146 10.7059C17.4136 10.7048 17.4099 10.701 17.4063 10.6928C17.4024 10.6839 17.4 10.6732 17.4 10.6633H18.3Z" fill="black" mask="url(#path-1-outside-1_0_2981)"/>
</g>
</g>
<defs>
<filter id="filter0_d_0_2981" x="-1.13" y="-0.766719" width="26.26" height="23.73" 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="1.615"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0235294 0 0 0 0 0.101961 0 0 0 0 0.0784314 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_2981"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_2981" result="shape"/>
</filter>
<clipPath id="clip0_0_2981">
<rect width="24" height="22.9091" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,11 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="208" height="208" viewBox="0 0 208 208" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 2745">
<circle id="Ellipse 409" cx="104" cy="104" r="103.5" stroke="#726E65"/>
<circle id="Ellipse 410" cx="104" cy="104" r="84" stroke="#161410" stroke-width="2"/>
<path id="Vector 923" d="M19.8594 34.2539L29.1703 41.972" stroke="#726E65"/>
<path id="Polygon 335" d="M23.6286 38.2515L22.4597 47.059L15.1599 40.9383L23.6286 38.2515Z" fill="#726E65"/>
<path id="Polygon 336" d="M23.6677 38.1969L32.1364 35.5101L24.8365 29.3894L23.6677 38.1969Z" fill="#726E65"/>
<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M19.8047 103.013L27.3047 107.776V103.75L180.062 103.75V108.25L187.562 103.487L180.062 98.7242V102.75L27.3047 102.75V98.2496L19.8047 103.013Z" fill="#726E65"/>
<path id="Union_2" fill-rule="evenodd" clip-rule="evenodd" d="M104 104L108.763 111.5H104.5L104.5 180.5H108.763L104 188L99.2369 180.5H103.5L103.5 111.5H99.2369L104 104Z" fill="#726E65"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,28 @@
<svg width="44" height="30" viewBox="0 0 44 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="44" height="30" fill="#F5F5F5"/>
<g id="2. &#237;&#151;&#164;&#235;&#141;&#148;">
<path d="M-2243 -4131C-2243 -4132.1 -2242.1 -4133 -2241 -4133H3016C3017.1 -4133 3018 -4132.1 3018 -4131V8497C3018 8498.1 3017.1 8499 3016 8499H-2241C-2242.1 8499 -2243 8498.1 -2243 8497V-4131Z" fill="white"/>
<path d="M-2241 -4132H3016V-4134H-2241V-4132ZM3017 -4131V8497H3019V-4131H3017ZM3016 8498H-2241V8500H3016V8498ZM-2242 8497V-4131H-2244V8497H-2242ZM-2241 8498C-2241.55 8498 -2242 8497.55 -2242 8497H-2244C-2244 8498.66 -2242.66 8500 -2241 8500V8498ZM3017 8497C3017 8497.55 3016.55 8498 3016 8498V8500C3017.66 8500 3019 8498.66 3019 8497H3017ZM3016 -4132C3016.55 -4132 3017 -4131.55 3017 -4131H3019C3019 -4132.66 3017.66 -4134 3016 -4134V-4132ZM-2241 -4134C-2242.66 -4134 -2244 -4132.66 -2244 -4131H-2242C-2242 -4131.55 -2241.55 -4132 -2241 -4132V-4134Z" fill="black" fill-opacity="0.1"/>
<g id="btn_close popup">
<rect x="-19.5" y="-69.5" width="83" height="119" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
<g id="Property 1=Variant2">
<g id="Rectangle 7347">
<path d="M0 0H41C42.6569 0 44 1.34315 44 3V30H0V0Z" fill="url(#paint0_linear_25_48)"/>
<path d="M0 0H41C42.6569 0 44 1.34315 44 3V30H0V0Z" fill="#E40000"/>
</g>
<g id="Group 2894">
<path id="Line 940" d="M26 9L35.0909 18.0909" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
<path id="Line 941" d="M35.2891 9L26.1982 18.0909" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</g>
</g>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_25_48" x1="23.6354" y1="0" x2="23.6354" y2="32.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F3025"/>
<stop offset="0.209337" stop-color="#194335"/>
<stop offset="0.913092" stop-color="#0B221B"/>
<stop offset="1" stop-color="#0D231C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+43
View File
@@ -0,0 +1,43 @@
<svg width="44" height="30" viewBox="0 0 44 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="44" height="30" fill="#F5F5F5"/>
<g id="2. &#237;&#151;&#164;&#235;&#141;&#148;">
<path d="M-2243 -4081C-2243 -4082.1 -2242.1 -4083 -2241 -4083H3016C3017.1 -4083 3018 -4082.1 3018 -4081V8547C3018 8548.1 3017.1 8549 3016 8549H-2241C-2242.1 8549 -2243 8548.1 -2243 8547V-4081Z" fill="white"/>
<path d="M-2241 -4082H3016V-4084H-2241V-4082ZM3017 -4081V8547H3019V-4081H3017ZM3016 8548H-2241V8550H3016V8548ZM-2242 8547V-4081H-2244V8547H-2242ZM-2241 8548C-2241.55 8548 -2242 8547.55 -2242 8547H-2244C-2244 8548.66 -2242.66 8550 -2241 8550V8548ZM3017 8547C3017 8547.55 3016.55 8548 3016 8548V8550C3017.66 8550 3019 8548.66 3019 8547H3017ZM3016 -4082C3016.55 -4082 3017 -4081.55 3017 -4081H3019C3019 -4082.66 3017.66 -4084 3016 -4084V-4082ZM-2241 -4084C-2242.66 -4084 -2244 -4082.66 -2244 -4081H-2242C-2242 -4081.55 -2241.55 -4082 -2241 -4082V-4084Z" fill="black" fill-opacity="0.1"/>
<g id="btn_close popup">
<rect x="-19.5" y="-19.5" width="83" height="119" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
<g id="Property 1=Default">
<g id="Rectangle 7347">
<path d="M41 0.5C42.3807 0.5 43.5 1.61929 43.5 3V29.5H0.5V0.5H41Z" fill="url(#paint0_linear_25_48)"/>
<path d="M41 0.5C42.3807 0.5 43.5 1.61929 43.5 3V29.5H0.5V0.5H41Z" fill="black" fill-opacity="0.4"/>
<path d="M41 0.5C42.3807 0.5 43.5 1.61929 43.5 3V29.5H0.5V0.5H41Z" stroke="url(#paint1_linear_25_48)"/>
</g>
<g id="Group 2894" filter="url(#filter0_d_25_48)">
<path id="Line 940" d="M26 9L35.0909 18.0909" stroke="#969696" stroke-width="1.5" stroke-linecap="round"/>
<path id="Line 941" d="M35.2891 9L26.1982 18.0909" stroke="#969696" stroke-width="1.5" stroke-linecap="round"/>
</g>
</g>
</g>
</g>
<defs>
<filter id="filter0_d_25_48" x="21.25" y="4.25" width="18.7891" height="18.5918" 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.4 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_25_48"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_25_48" result="shape"/>
</filter>
<linearGradient id="paint0_linear_25_48" x1="23.6354" y1="0" x2="23.6354" y2="32.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F3025"/>
<stop offset="0.209337" stop-color="#194335"/>
<stop offset="0.913092" stop-color="#0B221B"/>
<stop offset="1" stop-color="#0D231C"/>
</linearGradient>
<linearGradient id="paint1_linear_25_48" x1="33.5" y1="25.3484" x2="33.5" y2="1.61974e-07" gradientUnits="userSpaceOnUse">
<stop stop-color="#154838"/>
<stop offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="296" height="12" viewBox="0 0 296 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="btn_view more">
<path id="Polygon 357" opacity="0.5" d="M148 8.5L151.031 4.75H144.969L148 8.5Z" fill="#26221A"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 300 B

@@ -0,0 +1,31 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="317" height="36.0847" viewBox="0 0 317 36.0847" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Union" filter="url(#filter0_d_0_137)">
<mask id="path-1-inside-1_0_137" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M265 6H313C306.648 7.28392 302.05 14.3788 297.685 21.1155C293.369 27.775 289.28 34.0846 283.947 34.0846L266 34.0847H8C5.79087 34.0847 4 32.2939 4 30.0847V9.00012C4 7.34327 5.34315 6.00012 7 6.00012H265V6Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M265 6H313C306.648 7.28392 302.05 14.3788 297.685 21.1155C293.369 27.775 289.28 34.0846 283.947 34.0846L266 34.0847H8C5.79087 34.0847 4 32.2939 4 30.0847V9.00012C4 7.34327 5.34315 6.00012 7 6.00012H265V6Z" fill="url(#paint0_linear_0_137)"/>
<path d="M313 6L313.198 6.98018L313 5V6ZM265 6V5H264V6H265ZM297.685 21.1155L298.524 21.6593V21.6593L297.685 21.1155ZM283.947 34.0846V33.0846V33.0846V34.0846ZM266 34.0847V35.0847V35.0847V34.0847ZM265 6.00012V7.00012H266V6.00012H265ZM313 6V5H265V6V7H313V6ZM297.685 21.1155L298.524 21.6593C300.717 18.2754 302.915 14.8877 305.342 12.1805C307.771 9.4715 310.329 7.56003 313.198 6.98018L313 6L312.802 5.01982C309.319 5.72389 306.402 8.0018 303.853 10.8454C301.302 13.6908 299.018 17.2188 296.845 20.5716L297.685 21.1155ZM283.947 34.0846V35.0846C287.035 35.0846 289.64 33.255 291.932 30.7763C294.23 28.2904 296.38 24.9677 298.524 21.6593L297.685 21.1155L296.845 20.5716C294.674 23.9227 292.621 27.0846 290.463 29.4185C288.299 31.7594 286.192 33.0846 283.947 33.0846V34.0846ZM266 34.0847V35.0847L283.947 35.0846V34.0846V33.0846L266 33.0847V34.0847ZM266 34.0847V33.0847H8V34.0847V35.0847H266V34.0847ZM8 34.0847V33.0847C6.34315 33.0847 5 31.7416 5 30.0847H4H3C3 32.8461 5.23858 35.0847 8 35.0847V34.0847ZM4 30.0847H5V9.00012H4H3V30.0847H4ZM4 9.00012H5C5 7.89555 5.89543 7.00012 7 7.00012V6.00012V5.00012C4.79086 5.00012 3 6.79098 3 9.00012H4ZM7 6.00012V7.00012H265V6.00012V5.00012H7V6.00012ZM265 6H264V6.00012H265H266V6H265Z" fill="url(#paint1_linear_0_137)" mask="url(#path-1-inside-1_0_137)"/>
</g>
<defs>
<filter id="filter0_d_0_137" x="0" y="0" width="317" height="36.0847" 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_137"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_137" result="shape"/>
</filter>
<linearGradient id="paint0_linear_0_137" x1="169.245" y1="26.4913" x2="169.245" y2="52.571" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F3025"/>
<stop offset="0.209337" stop-color="#194335"/>
<stop offset="0.913092" stop-color="#0B221B"/>
<stop offset="1" stop-color="#0D231C"/>
</linearGradient>
<linearGradient id="paint1_linear_0_137" x1="186.108" y1="26.4913" x2="186.108" y2="52.571" gradientUnits="userSpaceOnUse">
<stop stop-color="#154838"/>
<stop offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,28 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="308" height="38" viewBox="0 0 308 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Union" filter="url(#filter0_d_0_140)">
<mask id="path-1-inside-1_0_140" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M252 6H0V33C0 34.6569 1.34314 36 3 36H252H260H270.947C276.28 36 280.369 29.2601 284.685 22.1464C289.05 14.9502 293.648 7.37149 300 6H260H252Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M252 6H0V33C0 34.6569 1.34314 36 3 36H252H260H270.947C276.28 36 280.369 29.2601 284.685 22.1464C289.05 14.9502 293.648 7.37149 300 6H260H252Z" fill="#211D13"/>
<path d="M0 6V5H-1V6H0ZM284.685 22.1464L285.54 22.665V22.665L284.685 22.1464ZM300 6L300.211 6.97748L300 5V6ZM0 6V7H252V6V5H0V6ZM0 33H1V6H0H-1V33H0ZM3 36V35C1.89543 35 1 34.1046 1 33H0H-1C-1 35.2091 0.790855 37 3 37V36ZM252 36V35H3V36V37H252V36ZM252 36V37H260V36V35H252V36ZM260 36V37H270.947V36V35H260V36ZM270.947 36V37C274.062 37 276.672 35.0311 278.953 32.3953C281.248 29.7442 283.395 26.2 285.54 22.665L284.685 22.1464L283.83 21.6277C281.658 25.2064 279.603 28.5891 277.441 31.0864C275.266 33.5989 273.165 35 270.947 35V36ZM284.685 22.1464L285.54 22.665C287.732 19.051 289.933 15.4276 292.363 12.5318C294.799 9.62962 297.357 7.59367 300.211 6.97748L300 6L299.789 5.02252C296.291 5.77782 293.374 8.21697 290.831 11.246C288.284 14.2816 286.003 18.0456 283.83 21.6277L284.685 22.1464ZM300 6V5H260V6V7H300V6ZM260 6V5H252V6V7H260V6Z" fill="url(#paint0_linear_0_140)" mask="url(#path-1-inside-1_0_140)"/>
</g>
<defs>
<filter id="filter0_d_0_140" x="0" y="0" width="308" height="38" 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 dx="4" 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_140"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_140" result="shape"/>
</filter>
<linearGradient id="paint0_linear_0_140" x1="150" y1="6" x2="150" y2="36" gradientUnits="userSpaceOnUse">
<stop stop-color="#493927"/>
<stop offset="0.17" stop-color="#6F5739"/>
<stop offset="0.505" stop-color="#A38663"/>
<stop offset="0.849271" stop-color="#2C2213"/>
<stop offset="1" stop-color="#413A2B"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+32
View File
@@ -0,0 +1,32 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="308" height="38" viewBox="0 0 308 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Union" filter="url(#filter0_d_0_167)">
<mask id="path-1-inside-1_0_167" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M252 6H0V33C0 34.6569 1.34314 36 3 36H252H260H270.947C276.28 36 280.369 29.2601 284.685 22.1464C289.05 14.9502 293.648 7.37149 300 6H260H252Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M252 6H0V33C0 34.6569 1.34314 36 3 36H252H260H270.947C276.28 36 280.369 29.2601 284.685 22.1464C289.05 14.9502 293.648 7.37149 300 6H260H252Z" fill="url(#paint0_linear_0_167)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M252 6H0V33C0 34.6569 1.34314 36 3 36H252H260H270.947C276.28 36 280.369 29.2601 284.685 22.1464C289.05 14.9502 293.648 7.37149 300 6H260H252Z" fill="black" fill-opacity="0.2"/>
<path d="M0 6V5H-1V6H0ZM284.685 22.1464L285.54 22.665V22.665L284.685 22.1464ZM300 6L300.211 6.97748L300 5V6ZM0 6V7H252V6V5H0V6ZM0 33H1V6H0H-1V33H0ZM3 36V35C1.89543 35 1 34.1046 1 33H0H-1C-1 35.2091 0.790855 37 3 37V36ZM252 36V35H3V36V37H252V36ZM252 36V37H260V36V35H252V36ZM260 36V37H270.947V36V35H260V36ZM270.947 36V37C274.062 37 276.672 35.0311 278.953 32.3953C281.248 29.7442 283.395 26.2 285.54 22.665L284.685 22.1464L283.83 21.6277C281.658 25.2064 279.603 28.5891 277.441 31.0864C275.266 33.5989 273.165 35 270.947 35V36ZM284.685 22.1464L285.54 22.665C287.732 19.051 289.933 15.4276 292.363 12.5318C294.799 9.62962 297.357 7.59367 300.211 6.97748L300 6L299.789 5.02252C296.291 5.77782 293.374 8.21697 290.831 11.246C288.284 14.2816 286.003 18.0456 283.83 21.6277L284.685 22.1464ZM300 6V5H260V6V7H300V6ZM260 6V5H252V6V7H260V6Z" fill="url(#paint1_linear_0_167)" mask="url(#path-1-inside-1_0_167)"/>
</g>
<defs>
<filter id="filter0_d_0_167" x="0" y="0" width="308" height="38" 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 dx="4" 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_167"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_167" result="shape"/>
</filter>
<linearGradient id="paint0_linear_0_167" x1="161.151" y1="6" x2="161.151" y2="38.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F3025"/>
<stop offset="0.34535" stop-color="#194335"/>
<stop offset="0.913092" stop-color="#0B221B"/>
<stop offset="1" stop-color="#0D231C"/>
</linearGradient>
<linearGradient id="paint1_linear_0_167" x1="178.023" y1="6" x2="178.023" y2="38.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#154838"/>
<stop offset="1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,74 @@
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="29" height="28" fill="#F5F5F5"/>
<g id="2. &#237;&#151;&#164;&#235;&#141;&#148;">
<path d="M-2585 -4174.26C-2585 -4175.36 -2584.1 -4176.26 -2583 -4176.26H2674C2675.1 -4176.26 2676 -4175.36 2676 -4174.26V8453.74C2676 8454.85 2675.1 8455.74 2674 8455.74H-2583C-2584.1 8455.74 -2585 8454.85 -2585 8453.74V-4174.26Z" fill="white"/>
<path d="M-2583 -4175.26H2674V-4177.26H-2583V-4175.26ZM2675 -4174.26V8453.74H2677V-4174.26H2675ZM2674 8454.74H-2583V8456.74H2674V8454.74ZM-2584 8453.74V-4174.26H-2586V8453.74H-2584ZM-2583 8454.74C-2583.55 8454.74 -2584 8454.29 -2584 8453.74H-2586C-2586 8455.4 -2584.66 8456.74 -2583 8456.74V8454.74ZM2675 8453.74C2675 8454.29 2674.55 8454.74 2674 8454.74V8456.74C2675.66 8456.74 2677 8455.4 2677 8453.74H2675ZM2674 -4175.26C2674.55 -4175.26 2675 -4174.81 2675 -4174.26H2677C2677 -4175.91 2675.66 -4177.26 2674 -4177.26V-4175.26ZM-2583 -4177.26C-2584.66 -4177.26 -2586 -4175.92 -2586 -4174.26H-2584C-2584 -4174.81 -2583.55 -4175.26 -2583 -4175.26V-4177.26Z" fill="black" fill-opacity="0.1"/>
<g id="Group 427322294">
<rect x="-19.5" y="-113.758" width="68" height="160.387" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
<g id="Property 1=Variant3">
<g id="Group 3815">
<g id="Group 3789">
<g id="Group 3788">
<g id="Rectangle 6959">
<g filter="url(#filter0_i_25_48)">
<rect width="29" height="27.129" rx="3" fill="black" fill-opacity="0.8"/>
</g>
<rect x="0.5" y="0.5" width="28" height="26.129" rx="2.5" stroke="url(#paint0_linear_25_48)"/>
</g>
<g id="Group 3787">
<g id="Group 3781">
<g id="Component 743">
<rect id="Rectangle 6960" x="2.37109" y="2.36719" width="24.2581" height="22.3871" rx="1.5" fill="url(#paint1_linear_25_48)" stroke="url(#paint2_linear_25_48)"/>
</g>
</g>
</g>
</g>
</g>
</g>
<path id="Vector" d="M11.6957 5L5 8.86435V16.44L11.6957 20.3043L18.3913 16.44V8.86435L11.6957 5ZM10.7391 17.54L6.91304 15.34V10.8443L10.7391 13.0539V17.54ZM8.07043 9.30435L11.6957 7.20957L15.3304 9.30435L11.6957 11.3991L8.07043 9.30435ZM16.4783 15.34L12.6522 17.54V13.0539L16.4783 10.8443V15.34Z" fill="#FFC600" stroke="black" stroke-miterlimit="10"/>
<g id="Group 3780">
<g id="Group 3782">
<g id="Group 3779">
<g id="Group">
<path id="Vector_2" d="M24.4202 15.8162L15.1842 9.18308L17.0539 20.7914L19.1947 18.0907L21.7913 22.3464L23.7959 20.9877L21.1992 16.7321L24.4202 15.8162Z" fill="#FFC600"/>
<path id="Vector_3" d="M21.8936 22.9544C21.6725 22.9962 21.4276 22.8983 21.2995 22.6823L19.1337 19.1331L17.5037 21.1897C17.361 21.3703 17.1286 21.4527 16.899 21.3904C16.6781 21.3264 16.509 21.1374 16.4782 20.903L14.6085 9.29468C14.5673 9.05267 14.6707 8.80256 14.8686 8.6787C15.065 8.54553 15.3205 8.54529 15.5098 8.69207L24.7457 15.3251C24.9334 15.4626 25.0278 15.6946 24.9951 15.9314C24.9625 16.1681 24.7949 16.3631 24.577 16.4235L22.1231 17.1272L24.289 20.6764C24.3763 20.8137 24.4048 20.9812 24.3683 21.1418C24.3318 21.3024 24.2464 21.4435 24.1144 21.5261L22.1099 22.8847C22.0439 22.926 21.9675 22.9597 21.8967 22.973L21.8936 22.9544ZM19.0906 17.4888C19.1348 17.4804 19.179 17.472 19.2264 17.4823C19.4112 17.4954 19.5837 17.5974 19.6846 17.7608L21.965 21.4998L22.9878 20.8069L20.7074 17.068C20.6065 16.9045 20.5893 16.6964 20.6492 16.5122C20.7179 16.3263 20.8596 16.1938 21.0421 16.1401L23.0353 15.5714L15.9743 10.5066L17.4039 19.3734L18.7253 17.7019C18.8156 17.5887 18.9403 17.5171 19.0729 17.4921L19.0906 17.4888Z" fill="black"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs>
<filter id="filter0_i_25_48" x="0" y="0" width="29" height="29.1289" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<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="1"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_25_48"/>
</filter>
<linearGradient id="paint0_linear_25_48" x1="14.5" y1="0" x2="14.5" y2="31.1984" gradientUnits="userSpaceOnUse">
<stop stop-color="#092920"/>
<stop offset="1" stop-color="#1C6F56"/>
</linearGradient>
<linearGradient id="paint1_linear_25_48" x1="14.5001" y1="1.86719" x2="14.5001" y2="25.2543" gradientUnits="userSpaceOnUse">
<stop stop-color="#081813"/>
<stop offset="0.086884" stop-color="#09271E"/>
<stop offset="0.254279" stop-color="#0B3227"/>
<stop offset="0.512521" stop-color="#0E3D2F"/>
<stop offset="0.800267" stop-color="#0B3227"/>
<stop offset="0.897594" stop-color="#0A2B21"/>
<stop offset="1" stop-color="#081813"/>
</linearGradient>
<linearGradient id="paint2_linear_25_48" x1="14.5001" y1="1.86719" x2="14.5001" y2="25.2543" gradientUnits="userSpaceOnUse">
<stop stop-color="#A67D03"/>
<stop offset="0.164109" stop-color="#F2CC0F"/>
<stop offset="0.371922" stop-color="#FADD4F"/>
<stop offset="0.620285" stop-color="#F2CC0F"/>
<stop offset="0.878785" stop-color="#926E03"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,73 @@
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="29" height="28" fill="#F5F5F5"/>
<g id="2. &#237;&#151;&#164;&#235;&#141;&#148;">
<path d="M-2585 -4127.13C-2585 -4128.24 -2584.1 -4129.13 -2583 -4129.13H2674C2675.1 -4129.13 2676 -4128.24 2676 -4127.13V8500.87C2676 8501.97 2675.1 8502.87 2674 8502.87H-2583C-2584.1 8502.87 -2585 8501.97 -2585 8500.87V-4127.13Z" fill="white"/>
<path d="M-2583 -4128.13H2674V-4130.13H-2583V-4128.13ZM2675 -4127.13V8500.87H2677V-4127.13H2675ZM2674 8501.87H-2583V8503.87H2674V8501.87ZM-2584 8500.87V-4127.13H-2586V8500.87H-2584ZM-2583 8501.87C-2583.55 8501.87 -2584 8501.42 -2584 8500.87H-2586C-2586 8502.52 -2584.66 8503.87 -2583 8503.87V8501.87ZM2675 8500.87C2675 8501.42 2674.55 8501.87 2674 8501.87V8503.87C2675.66 8503.87 2677 8502.52 2677 8500.87H2675ZM2674 -4128.13C2674.55 -4128.13 2675 -4127.69 2675 -4127.13H2677C2677 -4128.79 2675.66 -4130.13 2674 -4130.13V-4128.13ZM-2583 -4130.13C-2584.66 -4130.13 -2586 -4128.79 -2586 -4127.13H-2584C-2584 -4127.69 -2583.55 -4128.13 -2583 -4128.13V-4130.13Z" fill="black" fill-opacity="0.1"/>
<g id="Group 427322294">
<rect x="-19.5" y="-66.6328" width="68" height="160.387" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
<g id="Property 1=Variant2">
<g id="Group 3815">
<g id="Group 3789">
<g id="Group 3788">
<g id="Rectangle 6959">
<g filter="url(#filter0_i_25_48)">
<rect width="29" height="27.129" rx="3" fill="black" fill-opacity="0.8"/>
</g>
<rect x="0.5" y="0.5" width="28" height="26.129" rx="2.5" stroke="url(#paint0_linear_25_48)"/>
</g>
<g id="Group 3787">
<g id="Group 3781">
<g id="Component 743">
<rect id="Rectangle 6960" x="2.37109" y="2.36719" width="24.2581" height="22.3871" rx="1.5" fill="url(#paint1_linear_25_48)" stroke="url(#paint2_linear_25_48)"/>
</g>
</g>
</g>
</g>
</g>
</g>
<path id="Vector" d="M11.6957 5L5 8.86435V16.44L11.6957 20.3043L18.3913 16.44V8.86435L11.6957 5ZM10.7391 17.54L6.91304 15.34V10.8443L10.7391 13.0539V17.54ZM8.07043 9.30435L11.6957 7.20957L15.3304 9.30435L11.6957 11.3991L8.07043 9.30435ZM16.4783 15.34L12.6522 17.54V13.0539L16.4783 10.8443V15.34Z" fill="#FFC600" stroke="black" stroke-miterlimit="10"/>
<g id="Group 3780">
<g id="Group 3782">
<g id="Group 3779">
<g id="Group">
<path id="Vector_2" d="M24.4202 15.8162L15.1842 9.18308L17.0539 20.7914L19.1947 18.0907L21.7913 22.3464L23.7959 20.9877L21.1992 16.7321L24.4202 15.8162Z" fill="#FFC600"/>
<path id="Vector_3" d="M21.8936 22.9544C21.6725 22.9962 21.4276 22.8983 21.2995 22.6823L19.1337 19.1331L17.5037 21.1897C17.361 21.3703 17.1286 21.4527 16.899 21.3904C16.6781 21.3264 16.509 21.1374 16.4782 20.903L14.6085 9.29468C14.5673 9.05267 14.6707 8.80256 14.8686 8.6787C15.065 8.54553 15.3205 8.54529 15.5098 8.69207L24.7457 15.3251C24.9334 15.4626 25.0278 15.6946 24.9951 15.9314C24.9625 16.1681 24.7949 16.3631 24.577 16.4235L22.1231 17.1272L24.289 20.6764C24.3763 20.8137 24.4048 20.9812 24.3683 21.1418C24.3318 21.3024 24.2464 21.4435 24.1144 21.5261L22.1099 22.8847C22.0439 22.926 21.9675 22.9597 21.8967 22.973L21.8936 22.9544ZM19.0906 17.4888C19.1348 17.4804 19.179 17.472 19.2264 17.4823C19.4112 17.4954 19.5837 17.5974 19.6846 17.7608L21.965 21.4998L22.9878 20.8069L20.7074 17.068C20.6065 16.9045 20.5893 16.6964 20.6492 16.5122C20.7179 16.3263 20.8596 16.1938 21.0421 16.1401L23.0353 15.5714L15.9743 10.5066L17.4039 19.3734L18.7253 17.7019C18.8156 17.5887 18.9403 17.5171 19.0729 17.4921L19.0906 17.4888Z" fill="black"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs>
<filter id="filter0_i_25_48" x="0" y="0" width="29" height="29.1289" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<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="1"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_25_48"/>
</filter>
<linearGradient id="paint0_linear_25_48" x1="14.5" y1="0" x2="14.5" y2="31.1984" gradientUnits="userSpaceOnUse">
<stop stop-color="#092920"/>
<stop offset="1" stop-color="#1C6F56"/>
</linearGradient>
<linearGradient id="paint1_linear_25_48" x1="14.5001" y1="1.86719" x2="14.5001" y2="25.2543" gradientUnits="userSpaceOnUse">
<stop stop-color="#081813"/>
<stop offset="0.086884" stop-color="#09271E"/>
<stop offset="0.254279" stop-color="#0B3227"/>
<stop offset="0.512521" stop-color="#0E3D2F"/>
<stop offset="0.800267" stop-color="#0B3227"/>
<stop offset="0.897594" stop-color="#0A2B21"/>
<stop offset="1" stop-color="#081813"/>
</linearGradient>
<linearGradient id="paint2_linear_25_48" x1="14.5001" y1="1.86719" x2="14.5001" y2="25.2543" gradientUnits="userSpaceOnUse">
<stop stop-color="#08251C"/>
<stop offset="0.465449" stop-color="#208769"/>
<stop offset="0.844436" stop-color="#08241C"/>
<stop offset="1" stop-color="#051612"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+73
View File
@@ -0,0 +1,73 @@
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="29" height="28" fill="#F5F5F5"/>
<g id="2. &#237;&#151;&#164;&#235;&#141;&#148;">
<path d="M-2585 -4080C-2585 -4081.1 -2584.1 -4082 -2583 -4082H2674C2675.1 -4082 2676 -4081.1 2676 -4080V8548C2676 8549.1 2675.1 8550 2674 8550H-2583C-2584.1 8550 -2585 8549.1 -2585 8548V-4080Z" fill="white"/>
<path d="M-2583 -4081H2674V-4083H-2583V-4081ZM2675 -4080V8548H2677V-4080H2675ZM2674 8549H-2583V8551H2674V8549ZM-2584 8548V-4080H-2586V8548H-2584ZM-2583 8549C-2583.55 8549 -2584 8548.55 -2584 8548H-2586C-2586 8549.66 -2584.66 8551 -2583 8551V8549ZM2675 8548C2675 8548.55 2674.55 8549 2674 8549V8551C2675.66 8551 2677 8549.66 2677 8548H2675ZM2674 -4081C2674.55 -4081 2675 -4080.55 2675 -4080H2677C2677 -4081.66 2675.66 -4083 2674 -4083V-4081ZM-2583 -4083C-2584.66 -4083 -2586 -4081.66 -2586 -4080H-2584C-2584 -4080.55 -2583.55 -4081 -2583 -4081V-4083Z" fill="black" fill-opacity="0.1"/>
<g id="Group 427322294">
<rect x="-19.5" y="-19.5" width="68" height="160.387" rx="4.5" stroke="#9747FF" stroke-dasharray="10 5"/>
<g id="Property 1=Default">
<g id="Group 3815">
<g id="Group 3789">
<g id="Group 3788">
<g id="Rectangle 6959">
<g filter="url(#filter0_i_25_48)">
<rect width="29" height="27.129" rx="3" fill="black" fill-opacity="0.8"/>
</g>
<rect x="0.5" y="0.5" width="28" height="26.129" rx="2.5" stroke="url(#paint0_linear_25_48)"/>
</g>
<g id="Group 3787">
<g id="Group 3781">
<g id="Component 743">
<rect id="Rectangle 6960" x="2.37109" y="2.36719" width="24.2581" height="22.3871" rx="1.5" fill="url(#paint1_linear_25_48)" stroke="url(#paint2_linear_25_48)"/>
</g>
</g>
</g>
</g>
</g>
</g>
<path id="Vector" d="M11.6957 5L5 8.86435V16.44L11.6957 20.3043L18.3913 16.44V8.86435L11.6957 5ZM10.7391 17.54L6.91304 15.34V10.8443L10.7391 13.0539V17.54ZM8.07043 9.30435L11.6957 7.20957L15.3304 9.30435L11.6957 11.3991L8.07043 9.30435ZM16.4783 15.34L12.6522 17.54V13.0539L16.4783 10.8443V15.34Z" fill="#B6D0C9" stroke="black" stroke-miterlimit="10"/>
<g id="Group 3780">
<g id="Group 3782">
<g id="Group 3779">
<g id="Group">
<path id="Vector_2" d="M24.4202 15.8162L15.1842 9.18308L17.0539 20.7914L19.1947 18.0907L21.7913 22.3464L23.7959 20.9877L21.1992 16.7321L24.4202 15.8162Z" fill="#B6D0C9"/>
<path id="Vector_3" d="M21.8936 22.9544C21.6725 22.9962 21.4276 22.8983 21.2995 22.6823L19.1337 19.1331L17.5037 21.1897C17.361 21.3703 17.1286 21.4527 16.899 21.3904C16.6781 21.3264 16.509 21.1374 16.4782 20.903L14.6085 9.29468C14.5673 9.05267 14.6707 8.80256 14.8686 8.6787C15.065 8.54553 15.3205 8.54529 15.5098 8.69207L24.7457 15.3251C24.9334 15.4626 25.0278 15.6946 24.9951 15.9314C24.9625 16.1681 24.7949 16.3631 24.577 16.4235L22.1231 17.1272L24.289 20.6764C24.3763 20.8137 24.4048 20.9812 24.3683 21.1418C24.3318 21.3024 24.2464 21.4435 24.1144 21.5261L22.1099 22.8847C22.0439 22.926 21.9675 22.9597 21.8967 22.973L21.8936 22.9544ZM19.0906 17.4888C19.1348 17.4804 19.179 17.472 19.2264 17.4823C19.4112 17.4954 19.5837 17.5974 19.6846 17.7608L21.965 21.4998L22.9878 20.8069L20.7074 17.068C20.6065 16.9045 20.5893 16.6964 20.6492 16.5122C20.7179 16.3263 20.8596 16.1938 21.0421 16.1401L23.0353 15.5714L15.9743 10.5066L17.4039 19.3734L18.7253 17.7019C18.8156 17.5887 18.9403 17.5171 19.0729 17.4921L19.0906 17.4888Z" fill="black"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<defs>
<filter id="filter0_i_25_48" x="0" y="0" width="29" height="29.1289" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<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="1"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_25_48"/>
</filter>
<linearGradient id="paint0_linear_25_48" x1="14.5" y1="0" x2="14.5" y2="31.1984" gradientUnits="userSpaceOnUse">
<stop stop-color="#092920"/>
<stop offset="1" stop-color="#1C6F56"/>
</linearGradient>
<linearGradient id="paint1_linear_25_48" x1="14.5001" y1="1.86719" x2="14.5001" y2="25.2543" gradientUnits="userSpaceOnUse">
<stop stop-color="#081813"/>
<stop offset="0.086884" stop-color="#09271E"/>
<stop offset="0.254279" stop-color="#0B3227"/>
<stop offset="0.512521" stop-color="#0E3D2F"/>
<stop offset="0.800267" stop-color="#0B3227"/>
<stop offset="0.897594" stop-color="#0A2B21"/>
<stop offset="1" stop-color="#081813"/>
</linearGradient>
<linearGradient id="paint2_linear_25_48" x1="14.5001" y1="1.86719" x2="14.5001" y2="25.2543" gradientUnits="userSpaceOnUse">
<stop stop-color="#08251C"/>
<stop offset="0.465449" stop-color="#208769"/>
<stop offset="0.844436" stop-color="#08241C"/>
<stop offset="1" stop-color="#051612"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="13.0291" height="16.7588" viewBox="0 0 13.0291 16.7588" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Union">
<mask id="path-1-outside-1_0_6" maskUnits="userSpaceOnUse" x="-0.992468" y="0" width="15" height="17" fill="black">
<rect fill="white" x="-0.992468" width="15" height="17"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69338 2.11056C6.61968 1.96315 6.40932 1.96315 6.33561 2.11056L2.0294 10.723C1.94597 10.8898 2.11623 11.07 2.28752 10.9961L6.03315 9.37989V14.5588C6.03315 14.6693 6.12269 14.7588 6.23315 14.7588H6.92474C7.03519 14.7588 7.12474 14.6693 7.12474 14.5588V9.4364L10.7416 10.9962C10.9129 11.0701 11.0831 10.89 10.9997 10.7231L6.69338 2.11056Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69338 2.11056C6.61968 1.96315 6.40932 1.96315 6.33561 2.11056L2.0294 10.723C1.94597 10.8898 2.11623 11.07 2.28752 10.9961L6.03315 9.37989V14.5588C6.03315 14.6693 6.12269 14.7588 6.23315 14.7588H6.92474C7.03519 14.7588 7.12474 14.6693 7.12474 14.5588V9.4364L10.7416 10.9962C10.9129 11.0701 11.0831 10.89 10.9997 10.7231L6.69338 2.11056Z" fill="#1C6A51"/>
<path d="M6.33561 2.11056L4.54676 1.21613L4.54676 1.21613L6.33561 2.11056ZM6.69338 2.11056L4.90453 3.00498L4.90453 3.00498L6.69338 2.11056ZM2.0294 10.723L3.81825 11.6174L3.81825 11.6174L2.0294 10.723ZM2.28752 10.9961L1.49517 9.15971H1.49517L2.28752 10.9961ZM6.03315 9.37989H8.03315V6.3387L5.2408 7.54354L6.03315 9.37989ZM7.12474 9.4364L7.91675 7.5999L5.12474 6.39581V9.4364H7.12474ZM10.7416 10.9962L9.94956 12.8327H9.94956L10.7416 10.9962ZM10.9997 10.7231L9.21081 11.6175L9.21081 11.6176L10.9997 10.7231ZM6.33561 2.11056L8.12447 3.00499C7.46112 4.33167 5.56787 4.33167 4.90453 3.00498L6.69338 2.11056L8.48224 1.21613C7.67149 -0.405373 5.35752 -0.40538 4.54676 1.21613L6.33561 2.11056ZM2.0294 10.723L3.81825 11.6174L8.12447 3.00498L6.33561 2.11056L4.54676 1.21613L0.240545 9.82856L2.0294 10.723ZM2.28752 10.9961L1.49517 9.15971C3.03678 8.49454 4.56914 10.1156 3.81825 11.6174L2.0294 10.723L0.240545 9.82856C-0.677198 11.664 1.19568 13.6454 3.07987 12.8324L2.28752 10.9961ZM6.03315 9.37989L5.2408 7.54354L1.49517 9.15971L2.28752 10.9961L3.07987 12.8324L6.8255 11.2162L6.03315 9.37989ZM6.03315 14.5588H8.03315V9.37989H6.03315H4.03315V14.5588H6.03315ZM6.23315 14.7588V12.7588C7.22726 12.7588 8.03315 13.5647 8.03315 14.5588H6.03315H4.03315C4.03315 15.7738 5.01813 16.7588 6.23315 16.7588V14.7588ZM6.92474 14.7588V12.7588H6.23315V14.7588V16.7588H6.92474V14.7588ZM7.12474 14.5588H5.12474C5.12474 13.5647 5.93064 12.7588 6.92474 12.7588V14.7588V16.7588C8.13975 16.7588 9.12474 15.7738 9.12474 14.5588H7.12474ZM7.12474 9.4364H5.12474V14.5588H7.12474H9.12474V9.4364H7.12474ZM10.7416 10.9962L11.5336 9.15972L7.91675 7.5999L7.12474 9.4364L6.33272 11.2729L9.94956 12.8327L10.7416 10.9962ZM10.9997 10.7231L9.21081 11.6176C8.46001 10.1159 9.992 8.49489 11.5336 9.15972L10.7416 10.9962L9.94956 12.8327C11.8337 13.6453 13.7062 11.664 12.7885 9.82869L10.9997 10.7231ZM6.69338 2.11056L4.90453 3.00498L9.21081 11.6175L10.9997 10.7231L12.7885 9.8287L8.48224 1.21613L6.69338 2.11056Z" fill="black" mask="url(#path-1-outside-1_0_6)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1,16 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="28" height="22.8574" viewBox="0 0 28 22.8574" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 2901">
<g id="Union">
<mask id="path-1-outside-1_0_9" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="23" fill="black">
<rect fill="white" width="28" height="23"/>
<path d="M4 18.8574H24V4H4V18.8574ZM26 19.8574L25.9951 19.959C25.9475 20.4297 25.5732 20.8036 25.1025 20.8516L25 20.8574H3L2.89746 20.8516C2.42684 20.8036 2.05248 20.4297 2.00488 19.959L2 19.8574V3C2 2.48232 2.39333 2.05621 2.89746 2.00488L3 2H25L25.1025 2.00488C25.6067 2.05621 26 2.48232 26 3V19.8574Z"/>
</mask>
<path d="M4 18.8574H24V4H4V18.8574ZM26 19.8574L25.9951 19.959C25.9475 20.4297 25.5732 20.8036 25.1025 20.8516L25 20.8574H3L2.89746 20.8516C2.42684 20.8036 2.05248 20.4297 2.00488 19.959L2 19.8574V3C2 2.48232 2.39333 2.05621 2.89746 2.00488L3 2H25L25.1025 2.00488C25.6067 2.05621 26 2.48232 26 3V19.8574Z" fill="#1C6A51"/>
<path d="M4 18.8574H2V20.8574H4V18.8574ZM24 18.8574V20.8574H26V18.8574H24ZM24 4H26V2H24V4ZM4 4V2H2V4H4ZM26 19.8574L27.9977 19.9535L28 19.9055V19.8574H26ZM25.9951 19.959L27.985 20.1602L27.9903 20.1077L27.9928 20.055L25.9951 19.959ZM25.1025 20.8516L25.2166 22.8483L25.261 22.8458L25.3053 22.8413L25.1025 20.8516ZM25 20.8574V22.8574H25.0571L25.1141 22.8542L25 20.8574ZM3 20.8574L2.8859 22.8542L2.9429 22.8574H3V20.8574ZM2.89746 20.8516L2.69472 22.8413L2.73896 22.8458L2.78336 22.8483L2.89746 20.8516ZM2.00488 19.959L0.00719023 20.055L0.00972354 20.1077L0.015031 20.1602L2.00488 19.959ZM2 19.8574H0V19.9055L0.00230742 19.9535L2 19.8574ZM2 3L0 3V3H2ZM2.89746 2.00488L2.80233 0.00714648L2.74849 0.00971043L2.69486 0.0151707L2.89746 2.00488ZM3 2V0H2.95241L2.90487 0.00226367L3 2ZM25 2L25.0951 0.00226367L25.0476 0H25V2ZM25.1025 2.00488L25.3051 0.0151708L25.2515 0.00971043L25.1977 0.00714648L25.1025 2.00488ZM4 18.8574V20.8574H24V18.8574V16.8574H4V18.8574ZM24 18.8574H26V4H24H22V18.8574H24ZM24 4V2H4V4V6H24V4ZM4 4H2V18.8574H4H6V4H4ZM26 19.8574L24.0023 19.7614L23.9974 19.8629L25.9951 19.959L27.9928 20.055L27.9977 19.9535L26 19.8574ZM25.9951 19.959L24.0053 19.7578C24.0534 19.2817 24.4276 18.91 24.8998 18.8619L25.1025 20.8516L25.3053 22.8413C26.7187 22.6972 27.8416 21.5777 27.985 20.1602L25.9951 19.959ZM25.1025 20.8516L24.9884 18.8548L24.8859 18.8607L25 20.8574L25.1141 22.8542L25.2166 22.8483L25.1025 20.8516ZM25 20.8574V18.8574H3V20.8574V22.8574H25V20.8574ZM3 20.8574L3.1141 18.8607L3.01156 18.8548L2.89746 20.8516L2.78336 22.8483L2.8859 22.8542L3 20.8574ZM2.89746 20.8516L3.1002 18.8619C3.57242 18.91 3.9466 19.2817 3.99473 19.7578L2.00488 19.959L0.015031 20.1602C0.158372 21.5777 1.28126 22.6972 2.69472 22.8413L2.89746 20.8516ZM2.00488 19.959L4.00258 19.8629L3.99769 19.7614L2 19.8574L0.00230742 19.9535L0.00719023 20.055L2.00488 19.959ZM2 19.8574H4V3H2H0V19.8574H2ZM2 3L4 3C4 3.51806 3.60718 3.94296 3.10006 3.99459L2.89746 2.00488L2.69486 0.0151707C1.17948 0.169471 2.02656e-06 1.44659 0 3L2 3ZM2.89746 2.00488L2.99259 4.00262L3.09513 3.99774L3 2L2.90487 0.00226367L2.80233 0.00714648L2.89746 2.00488ZM3 2V4H25V2V0H3V2ZM25 2L24.9049 3.99774L25.0074 4.00262L25.1025 2.00488L25.1977 0.00714648L25.0951 0.00226367L25 2ZM25.1025 2.00488L24.8999 3.99459C24.3928 3.94296 24 3.51806 24 3H26H28C28 1.44659 26.8205 0.169471 25.3051 0.0151708L25.1025 2.00488ZM26 3H24V19.8574H26H28V3H26Z" fill="black" mask="url(#path-1-outside-1_0_9)"/>
</g>
<path id="Polygon 341" d="M14.2946 7.14286L15.7793 9.71429H12.81L14.2946 7.14286Z" fill="#1C6A51"/>
<path id="Polygon 342" d="M14.2946 15.7143L15.7793 13.1429H12.81L14.2946 15.7143Z" fill="#1C6A51"/>
<path id="Polygon 343" d="M18.5804 11.4286L16.0089 12.9132L16.0089 9.94396L18.5804 11.4286Z" fill="#1C6A51"/>
<path id="Polygon 344" d="M10.0089 11.4286L12.5804 12.9132L12.5804 9.94396L10.0089 11.4286Z" fill="#1C6A51"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+26
View File
@@ -0,0 +1,26 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="32" height="30" viewBox="0 0 32 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame 427319834">
<g id="Group 2901">
<g id="Union">
<mask id="path-1-outside-1_0_2939" maskUnits="userSpaceOnUse" x="0" y="0" width="28" height="23" fill="black">
<rect fill="white" width="28" height="23"/>
<path d="M4 19H24V4H4V19ZM26 20C26 20.5523 25.5523 21 25 21H3C2.44772 21 2 20.5523 2 20V3C2 2.48232 2.39333 2.05621 2.89746 2.00488L3 2H25L25.1025 2.00488C25.6067 2.05621 26 2.48232 26 3V20Z"/>
</mask>
<path d="M4 19H24V4H4V19ZM26 20C26 20.5523 25.5523 21 25 21H3C2.44772 21 2 20.5523 2 20V3C2 2.48232 2.39333 2.05621 2.89746 2.00488L3 2H25L25.1025 2.00488C25.6067 2.05621 26 2.48232 26 3V20Z" fill="#1C6A51"/>
<path d="M4 19H2V21H4V19ZM24 19V21H26V19H24ZM24 4H26V2H24V4ZM4 4V2H2V4H4ZM3 21L3 23H3V21ZM2.89746 2.00488L2.80233 0.00714648L2.74849 0.00971043L2.69486 0.0151707L2.89746 2.00488ZM3 2V0H2.95241L2.90487 0.00226367L3 2ZM25 2L25.0951 0.00226367L25.0476 0H25V2ZM25.1025 2.00488L25.3051 0.0151708L25.2515 0.00971043L25.1977 0.00714648L25.1025 2.00488ZM4 19V21H24V19V17H4V19ZM24 19H26V4H24H22V19H24ZM24 4V2H4V4V6H24V4ZM4 4H2V19H4H6V4H4ZM26 20H24C24 19.4477 24.4477 19 25 19V21V23C26.6569 23 28 21.6569 28 20H26ZM25 21V19H3V21V23H25V21ZM3 21L3 19C3.55228 19 4 19.4477 4 20H2H0C0 21.6569 1.34315 23 3 23L3 21ZM2 20H4V3H2H0V20H2ZM2 3H4C4 3.51806 3.60718 3.94296 3.10006 3.99459L2.89746 2.00488L2.69486 0.0151707C1.17948 0.16947 0 1.44659 0 3H2ZM2.89746 2.00488L2.99259 4.00262L3.09513 3.99774L3 2L2.90487 0.00226367L2.80233 0.00714648L2.89746 2.00488ZM3 2V4H25V2V0H3V2ZM25 2L24.9049 3.99774L25.0074 4.00262L25.1025 2.00488L25.1977 0.00714648L25.0951 0.00226367L25 2ZM25.1025 2.00488L24.8999 3.99459C24.3928 3.94296 24 3.51806 24 3H26H28C28 1.44659 26.8205 0.169471 25.3051 0.0151708L25.1025 2.00488ZM26 3H24V20H26H28V3H26Z" fill="black" mask="url(#path-1-outside-1_0_2939)"/>
</g>
<path id="Polygon 341" d="M14.2946 7.14286L15.7793 9.71429H12.81L14.2946 7.14286Z" fill="#1C6A51"/>
<path id="Polygon 342" d="M14.2946 15.7143L15.7793 13.1429H12.81L14.2946 15.7143Z" fill="#1C6A51"/>
<path id="Polygon 343" d="M18.5804 11.4286L16.0089 12.9132L16.0089 9.94396L18.5804 11.4286Z" fill="#1C6A51"/>
<path id="Polygon 344" d="M10.0089 11.4286L12.5804 12.9132L12.5804 9.94396L10.0089 11.4286Z" fill="#1C6A51"/>
</g>
<g id="Union_2">
<mask id="path-7-outside-2_0_2939" maskUnits="userSpaceOnUse" x="13.4163" y="10.5484" width="22.4181" height="22.6646" fill="black">
<rect fill="white" x="13.4163" y="10.5484" width="22.4181" height="22.6646"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.6606 16.8666C20.5094 16.8011 20.3483 16.9363 20.3866 17.0966L22.6238 26.4621C22.6671 26.6435 22.9133 26.6721 22.997 26.5054L24.8275 22.8597L28.1564 26.8269C28.2274 26.9115 28.3536 26.9226 28.4382 26.8516L28.968 26.407C29.0526 26.336 29.0637 26.2099 28.9926 26.1253L25.7 22.2013L29.4733 21.0713C29.652 21.0178 29.6666 20.7704 29.4955 20.6962L20.6606 16.8666Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.6606 16.8666C20.5094 16.8011 20.3483 16.9363 20.3866 17.0966L22.6238 26.4621C22.6671 26.6435 22.9133 26.6721 22.997 26.5054L24.8275 22.8597L28.1564 26.8269C28.2274 26.9115 28.3536 26.9226 28.4382 26.8516L28.968 26.407C29.0526 26.336 29.0637 26.2099 28.9926 26.1253L25.7 22.2013L29.4733 21.0713C29.652 21.0178 29.6666 20.7704 29.4955 20.6962L20.6606 16.8666Z" fill="#1C6A51"/>
<path d="M20.3866 17.0966L18.4413 17.5613L18.4413 17.5613L20.3866 17.0966ZM20.6606 16.8666L19.8652 18.7017L19.8652 18.7017L20.6606 16.8666ZM22.6238 26.4621L24.569 25.9974L24.569 25.9974L22.6238 26.4621ZM22.997 26.5054L21.2097 25.6079L21.2097 25.6079L22.997 26.5054ZM24.8275 22.8597L26.3596 21.5741L24.4048 19.2444L23.0402 21.9622L24.8275 22.8597ZM28.1564 26.8269L26.6244 28.1125L28.1564 26.8269ZM28.4382 26.8516L27.1526 25.3195L28.4382 26.8516ZM28.968 26.407L30.2536 27.9391L28.968 26.407ZM28.9926 26.1253L30.5247 24.8397L28.9926 26.1253ZM25.7 22.2013L25.1263 20.2853L22.2135 21.1576L24.1679 23.4869L25.7 22.2013ZM29.4733 21.0713L30.0471 22.9872L30.0471 22.9872L29.4733 21.0713ZM29.4955 20.6962L28.7001 22.5312L28.7001 22.5312L29.4955 20.6962ZM20.3866 17.0966L22.3318 16.6319C22.6765 18.0746 21.2261 19.2916 19.8652 18.7017L20.6606 16.8666L21.4561 15.0316C19.7927 14.3106 18.0201 15.798 18.4413 17.5613L20.3866 17.0966ZM22.6238 26.4621L24.569 25.9974L22.3318 16.6319L20.3866 17.0966L18.4413 17.5613L20.6785 26.9268L22.6238 26.4621ZM22.997 26.5054L21.2097 25.6079C21.9631 24.1075 24.1789 24.3643 24.569 25.9974L22.6238 26.4621L20.6785 26.9268C21.1553 28.9227 23.8636 29.2367 24.7844 27.4028L22.997 26.5054ZM24.8275 22.8597L23.0402 21.9622L21.2097 25.6079L22.997 26.5054L24.7844 27.4028L26.6149 23.7571L24.8275 22.8597ZM28.1564 26.8269L29.6885 25.5414L26.3596 21.5741L24.8275 22.8597L23.2954 24.1452L26.6244 28.1125L28.1564 26.8269ZM28.4382 26.8516L27.1526 25.3195C27.9142 24.6805 29.0495 24.7798 29.6885 25.5414L28.1564 26.8269L26.6244 28.1125C27.4054 29.0433 28.793 29.1647 29.7238 28.3837L28.4382 26.8516ZM28.968 26.407L27.6824 24.8749L27.1526 25.3195L28.4382 26.8516L29.7238 28.3837L30.2536 27.9391L28.968 26.407ZM28.9926 26.1253L27.4606 27.4108C26.8216 26.6493 26.9209 25.5139 27.6824 24.8749L28.968 26.407L30.2536 27.9391C31.1843 27.1581 31.3057 25.7705 30.5247 24.8397L28.9926 26.1253ZM25.7 22.2013L24.1679 23.4869L27.4606 27.4108L28.9926 26.1253L30.5247 24.8397L27.2321 20.9157L25.7 22.2013ZM29.4733 21.0713L28.8996 19.1554L25.1263 20.2853L25.7 22.2013L26.2738 24.1172L30.0471 22.9872L29.4733 21.0713ZM29.4955 20.6962L28.7001 22.5312C27.1597 21.8636 27.2913 19.637 28.8996 19.1554L29.4733 21.0713L30.0471 22.9872C32.0127 22.3986 32.1736 19.6772 30.2909 18.8612L29.4955 20.6962ZM20.6606 16.8666L19.8652 18.7017L28.7001 22.5312L29.4955 20.6962L30.2909 18.8612L21.4561 15.0316L20.6606 16.8666Z" fill="black" mask="url(#path-7-outside-2_0_2939)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

@@ -0,0 +1,461 @@
import { forwardRef, useEffect, useLayoutEffect, Fragment, type ReactNode, useId, useRef, useState } from "react"
import { SWHeaderActions } from "./components/SWHeaderActions"
import { SWHeaderColorMenu } from "./components/SWHeaderColorMenu"
import { SWHeaderDocumentTitle } from "./components/SWHeaderDocumentTitle"
import {
SWHeaderColorControl, SWHeaderDimensionStyle, SWHeaderLayerControl,
SWHeaderLineStyle, SWHeaderTextStyle,
} from "./components/SWHeaderFields"
import { SWHeaderHome } from "./components/SWHeaderHome"
import { SWHeaderLayerMenu } from "./components/SWHeaderLayerMenu"
import { SWHeaderLineStyleMenu } from "./components/SWHeaderLineStyleMenu"
import { SWHeaderMore, type SWHeaderMoreItem } from "./components/SWHeaderMore"
import { SWHeaderProperties } from "./components/SWHeaderProperties"
import { SWHeaderPropertyBar } from "./components/SWHeaderPropertyBar"
import { SWHeaderCommitPresence } from "./components/SWHeaderInput"
import { SWHeaderKGuide } from "./components/SWHeaderKGuide"
import printIcon from "../../assets/print.svg"
import printHover from "../../assets/print-hover.svg"
import { SWHeaderPropertyPanel } from "./components/SWHeaderPropertyPanel"
import { SWHeaderDimensionStyleMenu, SWHeaderTextStyleMenu } from "./components/SWHeaderStyleListMenus"
import { SWHeaderWindowControls } from "./components/SWHeaderWindowControls"
import type { SWHeaderControlId, SWHeaderControlConfig, SWHeaderFrameControlId, SWHeaderHideableControlId, SWHeaderMenuId, SWHeaderProps } from "./types"
const defaults = {
layer: { name: "0" },
color: { value: "ByLayer", swatch: "#ffffff" },
lineStyle: { name: "ByLayer", weight: "ByLayer", scale: "1.00" },
textStyle: { font: "Arial", name: "Standard" },
dimensionStyle: { name: "Standard" },
}
/** `data-menu` of each field (its Figma variant name). */
const menuSlot: Record<SWHeaderMenuId, string> = {
layer: "layer", color: "color", lineStyle: "line-style", textStyle: "text-style", dimensionStyle: "dimension-style",
}
const defaultMenuOrder = Object.keys(menuSlot) as SWHeaderMenuId[]
const defaultControls: SWHeaderControlConfig[] = (["document", "drawingMode", ...defaultMenuOrder, "details", "propertyBar", "propertyPanel", "print", "popupGuide"] as const).map(id => ({ id }))
const frameControls: readonly SWHeaderFrameControlId[] = ["home", "windowMove", "minimize", "close"]
const isMenu = (id: string): id is SWHeaderMenuId => id in menuSlot
/**
* Menu order: listed known ids first (duplicates and unknown ids ignored), missing ids after them in
* the default order. Menus only swap places among the slots menus already take; other parts never move.
*/
function orderMenus(items: SWHeaderControlConfig[], menuOrder?: readonly SWHeaderMenuId[]) {
if (!menuOrder) return items
const listed = menuOrder.filter((id, index) => isMenu(id) && menuOrder.indexOf(id) === index)
const rank = [...listed, ...defaultMenuOrder.filter(id => !listed.includes(id))]
const menus = items.filter(item => isMenu(item.id)).sort((a, b) => rank.indexOf(a.id as SWHeaderMenuId) - rank.indexOf(b.id as SWHeaderMenuId))
let next = 0
return items.map(item => (isMenu(item.id) ? menus[next++] : item))
}
/**
* SW Header (Figma node 26:8944, 1920 x 49). Pure presentation: values are props,
* user actions are callbacks, window behavior belongs to the host program.
*/
export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader(
{
documentTitle,
controls,
hiddenControls,
menuOrder,
documents,
documentMenuOpen,
onDocumentMenuOpenChange,
onDocumentSelect,
activeDocumentId,
onDocumentClose,
onDocumentNew,
onDocumentOpen,
documentIcon,
documentNewLabel,
documentOpenLabel,
drawingModeLabel = "그리기모드",
layer = defaults.layer,
layers,
activeLayerId,
layerMenuOpen,
onLayerMenuOpenChange,
onLayerSelect,
onLayerVisibilityChange,
onLayerFreezeChange,
onLayerLockChange,
onLayerDetail,
color = defaults.color,
lineStyle = defaults.lineStyle,
textStyle = defaults.textStyle,
dimensionStyle = defaults.dimensionStyle,
colors,
userColors,
colorMode,
colorMenuOpen,
onColorMenuOpenChange,
onColorSelect,
onColorModeChange,
onColorInput,
onColorPick,
onColorDetail,
lineTypes,
lineWeights,
activeLineTypeId,
activeLineWeightId,
lineStyleMenuOpen,
onLineStyleMenuOpenChange,
onLineTypeSelect,
onLineWeightSelect,
onLineWeightValueChange,
onLineScaleChange,
onLineStyleDetail,
textStyles,
activeTextStyleId,
textStyleMenuOpen,
onTextStyleMenuOpenChange,
onTextStyleSelect,
onTextStyleDetail,
dimensionStyles,
activeDimensionStyleId,
dimensionStyleMenuOpen,
onDimensionStyleMenuOpenChange,
onDimensionStyleSelect,
onDimensionStyleDetail,
activeMenu = null,
propertyBarOpen,
defaultPropertyBarOpen = false,
propertyBar,
onPropertyBarOpenChange,
propertyPanelOpen,
propertyPanel,
onPropertyPanelOpenChange,
disabled = false,
tools,
logo,
logoSrc,
logoAlt,
onHomeClick,
onDocumentClick,
onLayerOpen,
onColorOpen,
onLineStyleOpen,
onTextStyleOpen,
onDimensionStyleOpen,
onPropertyBarToggle,
onPropertyPanelToggle,
onPrint,
onPopupGuide,
onWindowMoveStart,
onMinimize,
onClose,
className,
...rest
},
ref,
) {
// The logo is never in this list: it is always shown. Hidden parts are unmounted (no space, no popup, no callback).
const removed = new Set<string>(hiddenControls ?? [])
const shown = (id: SWHeaderHideableControlId) => !removed.has(id)
const seen = new Set<SWHeaderControlId>()
const configuration = orderMenus((controls ?? defaultControls).filter(item => item.visible !== false && shown(item.id) && !seen.has(item.id) && !!seen.add(item.id)), menuOrder)
const enabled = (id: SWHeaderControlId) => configuration.some(item => item.id === id)
// Default design with removed object parts: same structure and breakpoints, but the script decides what
// moves into More (the CSS alone assumes every field is present). Untouched defaults stay CSS-only.
const scripted = controls !== undefined || [...removed].some(id => !frameControls.includes(id as SWHeaderFrameControlId))
const presentRef = useRef(new Set(configuration.map(item => item.id)))
presentRef.current = new Set(configuration.map(item => item.id))
const label = (id: SWHeaderControlId, fallback: string) => configuration.find(item => item.id === id)?.label ?? fallback
const [internalBarOpen, setInternalBarOpen] = useState(defaultPropertyBarOpen)
const barOpen = !disabled && enabled("propertyBar") && (propertyBarOpen ?? internalBarOpen)
const toggleBar = () => {
if (!enabled("propertyBar") || disabled) return
const next = !barOpen
if (propertyBarOpen === undefined) setInternalBarOpen(next)
onPropertyBarToggle?.()
onPropertyBarOpenChange?.(next)
}
// Uncontrolled open dropdown (one at a time). A controlled `*MenuOpen` prop takes precedence.
const [openMenu, setOpenMenu] = useState<SWHeaderMenuId | null>(null)
// Representative slot (UI state only, never program data): a field chosen in More takes the layer
// field's place while it is in More at the current width (CSS decides, see sw.css "Representative slot").
const [promoted, setPromoted] = useState<SWHeaderMenuId | null>(null)
const focusRef = useRef<SWHeaderMenuId | null>(null)
const fieldsRef = useRef<HTMLDivElement>(null)
const panelAnchorRef = useRef<HTMLDivElement>(null)
const panelId = useId()
const barId = useId()
const [internalPanelOpen, setInternalPanelOpen] = useState(false)
const panelOpen = !disabled && enabled("propertyPanel") && (propertyPanelOpen ?? internalPanelOpen)
const setPanelOpen = (next: boolean) => {
if (propertyPanelOpen === undefined) setInternalPanelOpen(next)
if (next !== panelOpen) onPropertyPanelOpenChange?.(next)
if (next) setOpenMenu(null)
}
const togglePanel = () => { onPropertyPanelToggle?.(); setPanelOpen(!panelOpen) }
const closePanel = () => {
if (propertyPanelOpen !== undefined && !onPropertyPanelOpenChange) onPropertyPanelToggle?.()
setPanelOpen(false)
const header = panelAnchorRef.current?.closest(".hm-sw-header")
const trigger = header?.querySelector<HTMLButtonElement>('[data-property-panel-trigger]')
const more = header?.querySelector<HTMLButtonElement>('.hm-sw-header__more-trigger')
;(trigger?.getClientRects().length ? trigger : more)?.focus()
}
const [headerWidth, setHeaderWidth] = useState(1920)
const [barOffset, setBarOffset] = useState(0)
useLayoutEffect(() => {
const header = fieldsRef.current?.closest(".hm-sw-header")
if (!header) return
const update = () => {
const bounds = header.getBoundingClientRect()
setHeaderWidth(bounds.width)
setBarOffset((panelAnchorRef.current?.getBoundingClientRect().left ?? bounds.left) - bounds.left)
}
const observer = new ResizeObserver(update)
update()
observer.observe(header)
if (panelAnchorRef.current) observer.observe(panelAnchorRef.current)
return () => observer.disconnect()
}, [])
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
const isOverflow = (id: SWHeaderControlId) => id === "color" ? headerWidth < 900 : ["lineStyle", "textStyle", "dimensionStyle"].includes(id) ? headerWidth < 1800 : ["propertyBar", "propertyPanel", "details"].includes(id) ? headerWidth < 1080 : ["print", "popupGuide"].includes(id) ? headerWidth < 640 : false
const representative = validPromoted && isOverflow(validPromoted) ? validPromoted : baseMenu
const fieldHidden = (id: SWHeaderMenuId) => id !== representative && (isOverflow(id) || (id === baseMenu && representative !== baseMenu))
// Removal is structural: close stale internal dropdown state silently, never send data callbacks.
const controlKey = configuration.map(item => item.id).join(",")
useEffect(() => {
if (openMenu && !enabled(openMenu)) setOpenMenu(null)
if (promoted && !enabled(promoted)) setPromoted(null)
if (!enabled("propertyPanel")) setInternalPanelOpen(false)
if (!enabled("propertyBar")) setInternalBarOpen(false)
}, [controlKey, openMenu, promoted])
const hasMenu: Record<SWHeaderMenuId, boolean> = {
layer: layers !== undefined,
color: colors !== undefined,
lineStyle: lineTypes !== undefined || lineWeights !== undefined,
textStyle: textStyles !== undefined,
dimensionStyle: dimensionStyles !== undefined,
}
const openChange = (id: SWHeaderMenuId, callback?: (open: boolean) => void) => (next: boolean) => {
if (next && panelOpen) setPanelOpen(false)
setOpenMenu(current => (next ? id : current === id ? null : current))
callback?.(next)
}
const menuOpenProps = (id: SWHeaderMenuId, controlled: boolean | undefined, callback?: (open: boolean) => void) => ({
open: enabled(id) && (controlled ?? openMenu === id),
onOpenChange: openChange(id, callback),
})
const layerOpen = menuOpenProps("layer", layerMenuOpen, onLayerMenuOpenChange)
const colorOpen = menuOpenProps("color", colorMenuOpen, onColorMenuOpenChange)
const lineStyleOpen = menuOpenProps("lineStyle", lineStyleMenuOpen, onLineStyleMenuOpenChange)
const textStyleOpen = menuOpenProps("textStyle", textStyleMenuOpen, onTextStyleMenuOpenChange)
const dimensionStyleOpen = menuOpenProps("dimensionStyle", dimensionStyleMenuOpen, onDimensionStyleMenuOpenChange)
const openers: Record<SWHeaderMenuId, { onOpenChange: (open: boolean) => void; onOpen?: () => void }> = {
layer: { onOpenChange: layerOpen.onOpenChange, onOpen: onLayerOpen },
color: { onOpenChange: colorOpen.onOpenChange, onOpen: onColorOpen },
lineStyle: { onOpenChange: lineStyleOpen.onOpenChange, onOpen: onLineStyleOpen },
textStyle: { onOpenChange: textStyleOpen.onOpenChange, onOpen: onTextStyleOpen },
dimensionStyle: { onOpenChange: dimensionStyleOpen.onOpenChange, onOpen: onDimensionStyleOpen },
}
/**
* More → field: show the field in the representative slot (choosing the layer gives the slot back),
* open its dropdown right below and move focus to the field. Calls the field's open callback once,
* exactly like a click on the field; no data callback is called.
*/
const promote = (id: SWHeaderMenuId) => () => {
setPromoted(id === baseMenu ? null : id)
focusRef.current = id
if (hasMenu[id]) openers[id].onOpenChange(true)
openers[id].onOpen?.()
}
useEffect(() => {
const id = focusRef.current
if (!id) return
focusRef.current = null
// The slot is the field itself, or the wrapper of a field with a dropdown (layer: .hm-sw-header__layer).
const slot = fieldsRef.current?.querySelector<HTMLElement>(scripted ? `[data-control="${id}"]` : id === "layer" ? ":scope > :is(.hm-sw-header__layer, [data-menu='layer'])" : `:scope > [data-menu="${menuSlot[id]}"]`)
const field = slot?.matches(".hm-sw-header__field") ? slot : slot?.querySelector<HTMLElement>(".hm-sw-header__field")
field?.focus()
})
const currentLayerName = layers?.find(item => item.id === activeLayerId)?.name ?? layer.name
const allMoreItems: SWHeaderMoreItem[] = [
{ id: "layer", menu: "layer", label: "레이어", value: currentLayerName, tier: "slot", selected: activeMenu === "layer", disabled, onSelect: promote("layer") },
{ id: "color", menu: "color", label: "색상", value: color.value, tier: "narrow", selected: activeMenu === "color", disabled, onSelect: promote("color") },
{ id: "lineStyle", menu: "line-style", label: "선스타일", value: lineStyle.name, tier: "aux", selected: activeMenu === "lineStyle", disabled, onSelect: promote("lineStyle") },
{ id: "textStyle", menu: "text-style", label: "문자스타일", value: textStyle.name, tier: "aux", selected: activeMenu === "textStyle", disabled, onSelect: promote("textStyle") },
{ id: "dimensionStyle", menu: "dimension-style", label: "치수스타일", value: dimensionStyle.name, tier: "aux", selected: activeMenu === "dimensionStyle", disabled, onSelect: promote("dimensionStyle") },
{ id: "propertyBar", label: "속성Bar", tier: "panel", checked: barOpen, disabled, onSelect: toggleBar },
{ id: "propertyPanel", label: "속성창", tier: "panel", checked: panelOpen, disabled, onSelect: togglePanel },
{ id: "print", label: "인쇄", tier: "action", disabled, onSelect: onPrint },
{ id: "popupGuide", label: "팝업 가이드", tier: "action", disabled, guide: true, onSelect: onPopupGuide },
]
const moreItems = configuration.flatMap(config => {
const item = allMoreItems.find(item => item.id === config.id)
if (!item) return []
const hidden = config.id in menuSlot ? fieldHidden(config.id as SWHeaderMenuId) : isOverflow(config.id)
return [{ ...item, label: config.label ?? item.label, visible: scripted ? hidden : undefined }]
})
const fieldNodes: Record<SWHeaderMenuId, ReactNode> = {
layer: (layers ? (
<SWHeaderLayerMenu label={label("layer", "레이어")}
value={layer}
layers={layers}
activeLayerId={activeLayerId}
{...layerOpen}
onClick={onLayerOpen}
selected={activeMenu === "layer"}
disabled={disabled}
onLayerSelect={onLayerSelect}
onVisibilityChange={onLayerVisibilityChange}
onFreezeChange={onLayerFreezeChange}
onLockChange={onLayerLockChange}
onDetail={onLayerDetail}
/>
) : (
<SWHeaderLayerControl label={label("layer", "레이어")} value={layer} selected={activeMenu === "layer"} disabled={disabled} onClick={onLayerOpen} />
)),
color: (colors ? (
<SWHeaderColorMenu label={label("color", "색상")}
value={color}
colors={colors}
userColors={userColors}
mode={colorMode}
{...colorOpen}
onClick={onColorOpen}
selected={activeMenu === "color"}
disabled={disabled}
onColorSelect={onColorSelect}
onModeChange={onColorModeChange}
onColorInput={onColorInput}
onPick={onColorPick}
onDetail={onColorDetail}
/>
) : (
<SWHeaderColorControl label={label("color", "색상")} value={color} selected={activeMenu === "color"} disabled={disabled} onClick={onColorOpen} />
)),
lineStyle: (hasMenu.lineStyle ? (
<SWHeaderLineStyleMenu label={label("lineStyle", "선스타일")}
value={lineStyle}
lineTypes={lineTypes}
lineWeights={lineWeights}
activeLineTypeId={activeLineTypeId}
activeLineWeightId={activeLineWeightId}
{...lineStyleOpen}
onClick={onLineStyleOpen}
selected={activeMenu === "lineStyle"}
disabled={disabled}
onLineTypeSelect={onLineTypeSelect}
onLineWeightSelect={onLineWeightSelect}
onLineWeightValueChange={onLineWeightValueChange}
onScaleChange={onLineScaleChange}
onDetail={onLineStyleDetail}
/>
) : (
<SWHeaderLineStyle label={label("lineStyle", "선스타일")} value={lineStyle} selected={activeMenu === "lineStyle"} disabled={disabled} onClick={onLineStyleOpen} />
)),
textStyle: (textStyles ? (
<SWHeaderTextStyleMenu label={label("textStyle", "문자스타일")}
value={textStyle}
textStyles={textStyles}
activeTextStyleId={activeTextStyleId}
{...textStyleOpen}
onClick={onTextStyleOpen}
selected={activeMenu === "textStyle"}
disabled={disabled}
onTextStyleSelect={onTextStyleSelect}
onDetail={onTextStyleDetail}
/>
) : (
<SWHeaderTextStyle label={label("textStyle", "문자스타일")} value={textStyle} selected={activeMenu === "textStyle"} disabled={disabled} onClick={onTextStyleOpen} />
)),
dimensionStyle: (dimensionStyles ? (
<SWHeaderDimensionStyleMenu label={label("dimensionStyle", "치수스타일")}
value={dimensionStyle}
dimensionStyles={dimensionStyles}
activeDimensionStyleId={activeDimensionStyleId}
{...dimensionStyleOpen}
onClick={onDimensionStyleOpen}
selected={activeMenu === "dimensionStyle"}
disabled={disabled}
onDimensionStyleSelect={onDimensionStyleSelect}
onDetail={onDimensionStyleDetail}
/>
) : (
<SWHeaderDimensionStyle label={label("dimensionStyle", "치수스타일")} value={dimensionStyle} selected={activeMenu === "dimensionStyle"} disabled={disabled} onClick={onDimensionStyleOpen} />
)),
}
const documentNode = (<SWHeaderDocumentTitle
label={label("document", "문서")}
title={documentTitle}
onClick={onDocumentClick}
documents={documents}
menuOpen={documentMenuOpen}
onMenuOpenChange={onDocumentMenuOpenChange}
onDocumentSelect={onDocumentSelect}
activeDocumentId={activeDocumentId}
onDocumentClose={onDocumentClose}
onNew={onDocumentNew}
onOpen={onDocumentOpen}
documentIcon={documentIcon}
newLabel={documentNewLabel}
openLabel={documentOpenLabel}
/>)
const configuredNodes: Partial<Record<SWHeaderControlId, ReactNode>> = {
...fieldNodes,
document: documentNode,
drawingMode: <span className="hm-sw-header__mode">{label("drawingMode", drawingModeLabel)}</span>,
details: <span className="hm-sw-header__properties-caption">{label("details", "상세보기")}</span>,
propertyBar: <button type="button" className="hm-sw-header__toggle" aria-pressed={barOpen} aria-controls={barOpen ? barId : undefined} disabled={disabled} onClick={toggleBar}>{label("propertyBar", "속성Bar")}</button>,
propertyPanel: <button type="button" className="hm-sw-header__toggle" data-property-panel-trigger="" aria-pressed={panelOpen} aria-haspopup={propertyPanel ? "dialog" : undefined} aria-expanded={panelOpen} aria-controls={panelOpen ? panelId : undefined} disabled={disabled} onClick={togglePanel}>{label("propertyPanel", "속성창")}</button>,
print: <button type="button" className="hm-sw-header__action hm-sw-header__action--print" aria-label={label("print", "인쇄")} disabled={disabled} onClick={onPrint}><img className="hm-sw-header__icon" src={printIcon} alt="" /><img className="hm-sw-header__icon-hover" src={printHover} alt="" /></button>,
popupGuide: <SWHeaderKGuide label={label("popupGuide", "팝업 가이드")} disabled={disabled} onClick={onPopupGuide} />,
}
const presence = (id: SWHeaderControlId, node: ReactNode) => <SWHeaderCommitPresence.Provider value={() => presentRef.current.has(id)}>{node}</SWHeaderCommitPresence.Provider>
const actionsShown = controls === undefined && (enabled("print") || enabled("popupGuide"))
const windowShown = actionsShown || frameControls.slice(1).some(shown)
const windowReduced = removed.size > 0 && (frameControls.some(id => removed.has(id)) || (controls === undefined && !(enabled("print") && enabled("popupGuide"))))
// CSS-only default layout: a field holding the first slot instead of the removed layer is styled as promoted.
const promotedSlot = validPromoted ?? (!scripted && baseMenu && baseMenu !== "layer" ? baseMenu : null)
const barData = propertyBar ?? (propertyPanel && {
selection: propertyPanel.selection,
properties: propertyPanel.properties,
onPropertyChange: propertyPanel.onPropertyChange,
})
return (
<header ref={ref} className={`hm-sw-header${className ? ` ${className}` : ""}`} {...rest}>
<div className="hm-sw-header__bar">
<SWHeaderHome onClick={shown("home") ? onHomeClick : undefined} interactive={shown("home")} logo={logo} logoSrc={logoSrc} logoAlt={logoAlt} />
{controls === undefined && enabled("document") && documentNode}
<div ref={panelAnchorRef} className="hm-sw-header__object-area" role="toolbar" aria-label="객체 수정" data-configured={controls !== undefined || undefined} data-promoted={promotedSlot ? menuSlot[promotedSlot] : undefined}>
{controls === undefined && enabled("drawingMode") && <span className="hm-sw-header__mode">{drawingModeLabel}</span>}
<div ref={fieldsRef} className="hm-sw-header__fields" data-configured={controls !== undefined || undefined}>
{controls ? configuration.map(config => {
const hidden = config.id in menuSlot ? fieldHidden(config.id as SWHeaderMenuId) : isOverflow(config.id) || (config.id === "drawingMode" && headerWidth < 1180)
return <div key={config.id} className="hm-sw-header__control" data-control={config.id} data-hidden={hidden || undefined}>{presence(config.id, configuredNodes[config.id])}</div>
}) : scripted ? menuIds.map(id => (
<div key={id} className="hm-sw-header__control" data-control={id} data-hidden={fieldHidden(id) || undefined} data-representative={(id === representative && id !== baseMenu) || undefined}>{presence(id, fieldNodes[id])}</div>
)) : menuIds.map(id => <Fragment key={id}>{fieldNodes[id]}</Fragment>)}
{tools}
</div>
{moreItems.some(item => item.visible !== false) && <SWHeaderMore items={moreItems} />}
{controls === undefined && <SWHeaderProperties barOpen={barOpen} panelOpen={panelOpen} panelId={propertyPanel ? panelId : undefined} disabled={disabled} onBarToggle={toggleBar} onPanelToggle={togglePanel} showCaption={enabled("details")} showBar={enabled("propertyBar")} showPanel={enabled("propertyPanel")} />}
</div>
{windowShown && <div className="hm-sw-header__window" data-configured={controls !== undefined || undefined} data-reduced={windowReduced || undefined}>
{actionsShown && <SWHeaderActions disabled={disabled} onPrint={onPrint} onPopupGuide={onPopupGuide} showPrint={enabled("print")} showPopupGuide={enabled("popupGuide")} />}
<SWHeaderWindowControls onWindowMoveStart={onWindowMoveStart} onMinimize={onMinimize} onClose={onClose} showWindowMove={shown("windowMove")} showMinimize={shown("minimize")} showClose={shown("close")} />
</div>}
</div>
{barOpen && barData && <div className="hm-sw-header__bar-anchor" style={{ left: barOffset, width: `calc(100% - ${barOffset}px)` }}><SWHeaderPropertyBar {...barData} disabled={disabled || propertyBar?.disabled} id={barId} /></div>}
{panelOpen && propertyPanel && <SWHeaderPropertyPanel {...propertyPanel} id={panelId} anchorRef={panelAnchorRef} onClose={closePanel} />}
</header>
)
})
@@ -0,0 +1,26 @@
import printIcon from "../../../assets/print.svg"
import printIconHover from "../../../assets/print-hover.svg"
import { SWHeaderKGuide } from "./SWHeaderKGuide"
export interface SWHeaderActionsProps {
disabled?: boolean
onPrint?: () => void
onPopupGuide?: () => void
/** Individual buttons (default true). The pill shrinks to the remaining button; none renders nothing. */
showPrint?: boolean
showPopupGuide?: boolean
}
/** Print and popup-guide buttons inside the dark rounded pill. */
export function SWHeaderActions({ disabled, onPrint, onPopupGuide, showPrint = true, showPopupGuide = true }: SWHeaderActionsProps) {
if (!showPrint && !showPopupGuide) return null
return (
<div className="hm-sw-header__actions" role="group" aria-label="바로가기" data-partial={showPrint && showPopupGuide ? undefined : ""}>
{showPrint && <button type="button" className="hm-sw-header__action hm-sw-header__action--print" aria-label="인쇄" disabled={disabled} onClick={onPrint}>
<img className="hm-sw-header__icon" src={printIcon} alt="" aria-hidden="true" />
<img className="hm-sw-header__icon-hover" src={printIconHover} alt="" aria-hidden="true" />
</button>}
{showPopupGuide && <SWHeaderKGuide disabled={disabled} onClick={onPopupGuide} />}
</div>
)
}
@@ -0,0 +1,166 @@
import { useEffect, useState } from "react"
import colorPickIcon from "../../../assets/colorize-default.svg"
import colorPickHover from "../../../assets/colorize-hover.svg"
import type { SWHeaderColorInputValue, SWHeaderColorMode, SWHeaderColorOption, SWHeaderColorValue } from "../types"
import { SWHeaderColorChip, SWHeaderMenuDetail, useSWHeaderDropdown } from "./SWHeaderDropdown"
import { SWHeaderColorControl } from "./SWHeaderFields"
import { SWHeaderInput, parseSWHeaderColor } from "./SWHeaderInput"
export interface SWHeaderColorMenuProps {
/** Current color shown in the field, the input and its chip. */
label?: string
value: SWHeaderColorValue
/** Basic palette (Figma: white, red, green, yellow, blue, magenta, cyan). Host data. */
colors: SWHeaderColorOption[]
/** User colors: extra rows below the palette, any count. */
userColors?: SWHeaderColorOption[]
/** Color mode; null still means explicit color. The required mode indicator remembers its last mode. */
mode?: SWHeaderColorMode | null
/** Controlled open state. Leave undefined to let the component manage it. */
open?: boolean
onOpenChange?: (open: boolean) => void
/** Called on every click of the field (also when the dropdown toggles). */
onClick?: () => void
/** Keeps the field in its selected (#FFC600) look while closed. */
selected?: boolean
disabled?: boolean
onColorSelect?: (option: SWHeaderColorOption) => void
onModeChange?: (mode: SWHeaderColorMode) => void
onColorInput?: (value: SWHeaderColorInputValue) => void
/** Color pick (색상 추출). The dropdown closes so the host can pick from the drawing. */
onPick?: () => void
/** "색상 상세". Optional host action; omitting it keeps hover and performs no CAD action. */
onDetail?: () => void
}
const modes: { mode: SWHeaderColorMode; label: string }[] = [
{ mode: "byLayer", label: "ByLayer" },
{ mode: "byBlock", label: "ByBlock" },
]
/** Value text of an option: its `value`, else the RGB text of its color. */
const optionValue = (option: SWHeaderColorOption) => option.value ?? parseSWHeaderColor(option.color)?.value ?? option.color
/**
* Color field with its dropdown (Figma header/menu/색상 26:10986, user colors 26:11036): ByLayer /
* ByBlock (26:12666 / 26:12659), 22px palette, user color rows, color pick + RGB input, "색상 상세".
* Every control only reports through its callback; the host updates `value` / `mode`. Choosing a
* color or a mode keeps the dropdown open (several properties can be set in a row).
* Latest request restores Figma hover for the mode pair and detail pill; chips/pick remain unchanged.
*/
export function SWHeaderColorMenu({
value, label, colors, userColors, mode, open: openProp, onOpenChange, onClick, selected = false, disabled = false,
onColorSelect, onModeChange, onColorInput, onPick, onDetail,
}: SWHeaderColorMenuProps) {
const { open, setOpen, close, panelRef, triggerRef, menuId, panelStyle, shifted, rootProps } = useSWHeaderDropdown({ open: openProp, onOpenChange, disabled })
const swatch = value.swatch ?? "#ffe500"
const current = parseSWHeaderColor(value.value) ?? parseSWHeaderColor(swatch)
const inputValue = current?.value ?? value.value
const [lastMode, setLastMode] = useState<SWHeaderColorMode>(mode ?? "byLayer")
useEffect(() => { if (mode) setLastMode(mode) }, [mode])
const selectedMode = mode ?? lastMode
const chip = (option: SWHeaderColorOption) => {
const pressed = !mode && current !== null && parseSWHeaderColor(option.color)?.hex === current.hex
const label = option.label ?? optionValue(option)
return (
<button
key={option.id}
type="button"
className="hm-sw-header__color-option"
title={label}
aria-label={label}
aria-pressed={pressed}
disabled={option.disabled}
onClick={() => onColorSelect?.(option)}
>
<SWHeaderColorChip color={option.color} />
</button>
)
}
return (
<div {...rootProps} className="hm-sw-header__menu hm-sw-header__menu--color" data-menu="color">
<SWHeaderColorControl
buttonRef={triggerRef}
label={label}
value={value}
selected={selected || open}
expanded={open}
controls={open ? menuId : undefined}
disabled={disabled}
onClick={() => {
onClick?.()
setOpen(!open)
}}
/>
{open && (
<div
ref={panelRef}
id={menuId}
className="hm-sw-header__menu-panel hm-sw-header__color-menu"
role="group"
aria-label="색상"
data-shifted={shifted ? "true" : undefined}
style={panelStyle}
>
<div className="hm-sw-header__color-modes" role="group" aria-label="색상 기준">
{modes.map(item => (
<button
key={item.mode}
type="button"
className={`hm-sw-header__color-mode hm-sw-header__color-mode--${item.mode === "byLayer" ? "layer" : "block"}`}
aria-pressed={selectedMode === item.mode}
onClick={() => {
if (selectedMode === item.mode) return
setLastMode(item.mode)
onModeChange?.(item.mode)
}}
>
<span className="hm-sw-header__menu-text">{item.label}</span>
</button>
))}
</div>
<div className="hm-sw-header__color-palette">
{colors.length > 0 && <div className="hm-sw-header__color-row" role="group" aria-label="기본 색상">{colors.map(chip)}</div>}
{userColors && userColors.length > 0 && (
<div className="hm-sw-header__color-row hm-sw-header__color-row--user" role="group" aria-label="사용자 색상">{userColors.map(chip)}</div>
)}
</div>
<div className="hm-sw-header__color-tools">
<button
type="button"
className="hm-sw-header__color-pick"
aria-label="색상 추출"
title="색상 추출"
disabled={!onPick}
onClick={() => {
onPick?.()
close(true)
}}
>
<img className="hm-sw-header__colorize-default" src={colorPickIcon} alt="" />
<img className="hm-sw-header__colorize-hover" src={colorPickHover} alt="" />
</button>
<span className="hm-sw-header__color-input">
<SWHeaderColorChip color={swatch} />
<SWHeaderInput
aria-label="색상값 (R,G,B)"
inputMode="text"
value={inputValue}
parse={draft => parseSWHeaderColor(draft)?.value ?? null}
onCommit={onColorInput ? next => {
const parsed = parseSWHeaderColor(next)
if (parsed) onColorInput(parsed)
} : undefined}
/>
</span>
</div>
<div className="hm-sw-header__menu-footer">
<SWHeaderMenuDetail label="색상 상세" onClick={onDetail && (() => { onDetail(); close(true) })} />
</div>
</div>
)}
</div>
)
}
@@ -0,0 +1,219 @@
import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode } from "react"
import documentIcon from "../../../assets/document-icon.svg"
import type { SWHeaderDocument } from "../types"
export interface SWHeaderDocumentTitleProps {
title: string
label?: string
/** Called on every click of the file name (also when the document menu toggles). */
onClick?: () => void
/**
* Documents shown in the dropdown below the file name. When omitted, the file name is a plain
* button without a dropdown (the 0.1.0 behavior). An empty array opens a menu with only New / Open.
*/
documents?: SWHeaderDocument[]
/** Controlled open state of the document menu. Leave undefined to let the component manage it. */
menuOpen?: boolean
onMenuOpenChange?: (open: boolean) => void
onDocumentSelect?: (document: SWHeaderDocument) => void
/**
* Id of the current document. Its name is shown in #FFC600; the list keeps the order of
* `documents` (selecting another document never moves rows).
*/
activeDocumentId?: string
/**
* Close Document. When given, every document row gets an X button ("문서 닫기"), hidden until
* the row is hovered or focused (space is always reserved). The host closes the document and
* updates `documents` / `activeDocumentId`; the header never picks the next document itself.
*/
onDocumentClose?: (document: SWHeaderDocument) => void
onNew?: () => void
onOpen?: () => void
/** Icon used for documents without their own `icon`. Defaults to the Figma document icon. */
documentIcon?: ReactNode
newLabel?: string
openLabel?: string
}
/**
* File name button. With `documents`, it is a disclosure button that opens the document menu
* (Figma header/document_tit 26:10662): click / Enter / Space toggle it, ArrowDown opens it and
* focuses the first entry, Escape or a click outside closes it.
*/
export function SWHeaderDocumentTitle({
title,
label = "문서",
onClick,
documents,
menuOpen,
onMenuOpenChange,
onDocumentSelect,
activeDocumentId,
onDocumentClose,
onNew,
onOpen,
documentIcon: iconOverride,
newLabel = "+ New",
openLabel = "Open",
}: SWHeaderDocumentTitleProps) {
const hasMenu = documents !== undefined
const [internalOpen, setInternalOpen] = useState(false)
const open = hasMenu && (menuOpen ?? internalOpen)
const rootRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
const menuId = useId()
// Set when the keyboard opens the menu or a document is closed, so the first entry gets focus
// once the menu (re)renders and focus is never lost with a removed row.
const focusFirstRef = useRef(false)
const [, rerender] = useState(0)
const setOpen = (next: boolean) => {
if (menuOpen === undefined) setInternalOpen(next)
onMenuOpenChange?.(next)
}
const close = (restoreFocus: boolean) => {
setOpen(false)
if (restoreFocus) triggerRef.current?.focus()
}
useEffect(() => {
if (!open) return
const onPointerDown = (event: PointerEvent) => {
if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
}
document.addEventListener("pointerdown", onPointerDown)
return () => document.removeEventListener("pointerdown", onPointerDown)
})
useEffect(() => {
if (open && focusFirstRef.current) {
focusFirstRef.current = false
entries()[0]?.focus()
}
})
const entries = () => Array.from(rootRef.current?.querySelectorAll<HTMLButtonElement>(".hm-sw-header__document-menu button:not(:disabled)") ?? [])
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (!hasMenu) return
if (event.key === "Escape" && open) {
event.stopPropagation()
close(true)
return
}
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return
event.preventDefault()
if (!open) {
focusFirstRef.current = true
setOpen(true)
return
}
const list = entries()
const index = list.indexOf(document.activeElement as HTMLButtonElement)
const next = event.key === "ArrowDown" ? (index + 1) % list.length : (index - 1 + list.length) % list.length
list[next]?.focus()
}
const items = documents ?? []
const active = items.find(item => item.id === activeDocumentId)
const iconFor = (item: SWHeaderDocument) =>
item.icon ?? iconOverride ?? <img className="hm-sw-header__document-icon-default" src={documentIcon} alt="" />
return (
<div
ref={rootRef}
className="hm-sw-header__document"
onKeyDown={onKeyDown}
onBlur={event => {
// Tabbing out of the file name and its menu closes the menu; focus is never trapped.
if (open && !rootRef.current?.contains(event.relatedTarget as Node | null)) setOpen(false)
}}
>
<button
ref={triggerRef}
type="button"
className="hm-sw-header__document-button"
title={title}
aria-label={`${label}: ${title}`}
aria-expanded={hasMenu ? open : undefined}
aria-controls={open ? menuId : undefined}
data-selected={open ? "true" : undefined}
onClick={() => {
onClick?.()
if (hasMenu) setOpen(!open)
}}
>
<span className="hm-sw-header__document-name">{title}</span>
<span className="hm-sw-header__arrow" aria-hidden="true" />
</button>
{open && (
<div id={menuId} className="hm-sw-header__document-menu" role="group" aria-label="문서 목록">
{items.length > 0 && (
<ul className="hm-sw-header__document-list">
{items.map(item => (
<SWHeaderDocumentItem
key={item.id}
document={item}
icon={iconFor(item)}
active={item === active}
onSelect={() => {
onDocumentSelect?.(item)
close(true)
}}
onClose={onDocumentClose ? () => {
// Park focus inside the header before the row may disappear, then refocus the
// first entry after the host updates the list. The menu stays open.
triggerRef.current?.focus()
focusFirstRef.current = true
onDocumentClose(item)
rerender(count => count + 1)
} : undefined}
/>
))}
</ul>
)}
<div className="hm-sw-header__document-footer">
<button type="button" className="hm-sw-header__document-action" onClick={() => { onNew?.(); close(true) }}>{newLabel}</button>
<button type="button" className="hm-sw-header__document-action" onClick={() => { onOpen?.(); close(true) }}>{openLabel}</button>
</div>
</div>
)}
</div>
)
}
/**
* One document row (32px): select button (icon 29 x 12 + name) and an optional Close Document
* button for this document (shown on row hover / focus). Sibling buttons, so closing never selects.
*/
function SWHeaderDocumentItem({ document, icon, active, onSelect, onClose }: {
document: SWHeaderDocument
icon: ReactNode
active: boolean
onSelect: () => void
onClose?: () => void
}) {
return (
<li className="hm-sw-header__document-row" data-active={active ? "true" : undefined} data-disabled={document.disabled ? "true" : undefined}>
<button
type="button"
className="hm-sw-header__document-item"
title={document.title ?? document.name}
aria-current={active ? "true" : undefined}
disabled={document.disabled}
onClick={onSelect}
>
<span className="hm-sw-header__document-icon" aria-hidden="true">{icon}</span>
<span className="hm-sw-header__document-item-name">{document.name}</span>
</button>
{onClose && (
<button type="button" className="hm-sw-header__document-close" aria-label="문서 닫기" title={`${document.name} 닫기`} onClick={onClose}>
<svg width="8" height="8" viewBox="0 0 8 8" aria-hidden="true" focusable="false">
<path d="M1 1l6 6M7 1L1 7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
</button>
)}
</li>
)
}
@@ -0,0 +1,156 @@
import { useEffect, useId, useLayoutEffect, useRef, useState, type FocusEvent, type KeyboardEvent } from "react"
import detailIcon from "../../../assets/layer-detail.svg"
import type { ButtonHTMLAttributes, ReactNode } from "react"
export interface SWHeaderDropdownOptions {
/** Controlled open state. Leave undefined to let the component manage it. */
open?: boolean
onOpenChange?: (open: boolean) => void
disabled?: boolean
/** `start`: left edge under the field (color, text, dimension). `center`: centered under it (line style). */
align?: "start" | "center"
}
/**
* Shared behavior of the field dropdowns added after the layer dropdown (color, line style, text style,
* dimension style), the same contract as SWHeaderLayerMenu: click / Enter / Space toggle (hover never
* opens), ArrowDown opens and focuses the current (or first) entry, ArrowUp / ArrowDown move, Escape /
* a click outside / Tab out close. The list overlays below the field and is shifted horizontally only
* when it would leave the header (or the screen).
*/
export function useSWHeaderDropdown({ open: openProp, onOpenChange, disabled = false, align = "start" }: SWHeaderDropdownOptions) {
const [internalOpen, setInternalOpen] = useState(false)
const open = !disabled && (openProp ?? internalOpen)
const rootRef = useRef<HTMLDivElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
const menuId = useId()
const focusEntryRef = useRef(false)
// Left offset of the panel from the field; `base` is the unshifted position for `align`.
const [position, setPosition] = useState({ left: 0, base: 0 })
const setOpen = (next: boolean) => {
if (openProp === undefined) setInternalOpen(next)
onOpenChange?.(next)
}
const close = (restoreFocus: boolean) => {
setOpen(false)
if (restoreFocus) triggerRef.current?.focus()
}
useEffect(() => {
if (!open) return
const onPointerDown = (event: PointerEvent) => {
if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
}
document.addEventListener("pointerdown", onPointerDown)
return () => document.removeEventListener("pointerdown", onPointerDown)
})
useLayoutEffect(() => {
if (!open) return
const update = () => {
const root = rootRef.current
const panel = panelRef.current
if (!root || !panel) return
const bounds = (root.closest(".hm-sw-header") ?? document.documentElement).getBoundingClientRect()
// 1px inside the bounds so the outline ring (drawn 1px outside the panel) never overflows.
const min = Math.max(bounds.left, 0) + 1
const max = Math.min(bounds.right, document.documentElement.clientWidth) - 1
const field = root.getBoundingClientRect()
const base = align === "center" ? Math.round((field.width - panel.offsetWidth) / 2) : 0
let left = field.left + base
if (left + panel.offsetWidth > max) left = max - panel.offsetWidth
if (left < min) left = min
const next = Math.round(left - field.left)
setPosition(current => (current.left === next && current.base === base ? current : { left: next, base }))
}
update()
window.addEventListener("resize", update)
return () => window.removeEventListener("resize", update)
}) // Reposition after a host-driven control reorder as well as open/resize.
/** Focusable entries of the panel in DOM order (rows, inputs, footer). */
const entries = () => Array.from(panelRef.current?.querySelectorAll<HTMLElement>("button:not(:disabled), input:not(:disabled)") ?? [])
useEffect(() => {
if (open && focusEntryRef.current) {
focusEntryRef.current = false
const current = panelRef.current?.querySelector<HTMLElement>('[aria-current="true"]:not(:disabled), [aria-pressed="true"]:not(:disabled)')
;(current ?? entries()[0])?.focus()
}
})
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Escape" && open) {
event.stopPropagation()
close(true)
return
}
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return
if (!open) {
if (event.target !== triggerRef.current) return
event.preventDefault()
focusEntryRef.current = true
setOpen(true)
return
}
const list = entries()
if (list.length === 0) return
event.preventDefault()
const index = list.indexOf(document.activeElement as HTMLElement)
if (index < 0) {
list[event.key === "ArrowDown" ? 0 : list.length - 1]?.focus()
return
}
list[(index + (event.key === "ArrowDown" ? 1 : -1) + list.length) % list.length]?.focus()
}
const onBlur = (event: FocusEvent<HTMLDivElement>) => {
// Tabbing out of the field and its dropdown closes it; focus is never trapped.
if (open && !rootRef.current?.contains(event.relatedTarget as Node | null)) setOpen(false)
}
return {
open,
setOpen,
close,
rootRef,
panelRef,
triggerRef,
menuId,
/** Panel `left` relative to the field (centered and / or shifted to stay on screen). */
panelStyle: position.left !== 0 ? { left: position.left } : undefined,
/** The panel no longer sits under the field (the outline then rings the panel only). */
shifted: position.left !== position.base,
rootProps: { ref: rootRef, onKeyDown, onBlur, "data-open": open ? "true" : undefined },
}
}
export interface SWHeaderMenuDetailProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
label: string
/** Optional replacement icon; decorative, with the shared 14px size and state opacity. */
icon?: ReactNode
}
/** All five footer buttons use the text-style detail design (26:10789 / 26:10792).
* Callback absence is a safe no-op; only an explicit disabled prop disables the button.
* The actual CAD detail popup remains the host's responsibility.
*/
export function SWHeaderMenuDetail({ label, icon, className, disabled = false, ...props }: SWHeaderMenuDetailProps) {
return (
<button {...props} type="button" className={["hm-sw-header__menu-detail", className].filter(Boolean).join(" ")} data-detail={label} disabled={disabled}>
{icon === undefined ? <img src={detailIcon} alt="" /> : <span className="hm-sw-header__detail-icon" aria-hidden="true">{icon}</span>}
<span data-detail-label="">{label}</span>
</button>
)
}
/** 22px color chip (Figma color chip 22px 26:11091): black 20px tile, 18px swatch with a #C9C9C9 border. */
export function SWHeaderColorChip({ color }: { color: string }) {
return (
<span className="hm-sw-header__color-chip" aria-hidden="true">
<span style={{ background: color }} />
</span>
)
}
@@ -0,0 +1,52 @@
import type { ReactNode, Ref } from "react"
export interface SWHeaderFieldProps {
/** Accessible name of the field button, e.g. `레이어`. */
label: string
selected?: boolean
disabled?: boolean
onClick?: () => void
/** Modifier for the Figma width. */
variant: "layer" | "color" | "line-style" | "text-style" | "dimension-style"
/** `aux` fields move into the More menu below the compact breakpoint. */
tier?: "main" | "aux"
/** Open state of a dropdown owned by the field itself (layer, color, line / text / dimension style). Defaults to `selected`. */
expanded?: boolean
/** Id of that dropdown while it is open. */
controls?: string
buttonRef?: Ref<HTMLButtonElement>
children: ReactNode
}
/** Shared SW property field surface (user-confirmed gradient, border and shadow). */
export function SWHeaderField({ label, selected = false, disabled, onClick, variant, tier = "main", expanded, controls, buttonRef, children }: SWHeaderFieldProps) {
return (
<button
ref={buttonRef}
type="button"
className={`hm-sw-header__field hm-sw-header__field--${variant}`}
data-menu={variant}
data-tier={tier}
data-selected={selected ? "true" : undefined}
data-expanded={expanded ? "true" : undefined}
aria-haspopup="true"
aria-expanded={expanded ?? selected}
aria-controls={controls}
aria-label={label}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
)
}
/** Small label (12px) above a bold value (14px) used by every field. */
export function SWHeaderFieldText({ label, value }: { label: string; value: string }) {
return (
<span className="hm-sw-header__field-text">
<span className="hm-sw-header__field-label">{label}</span>
<span className="hm-sw-header__field-value" title={value}>{value}</span>
</span>
)
}
@@ -0,0 +1,79 @@
import dimensionIcon from "../../../assets/menu-dimension-ico.svg"
import dimensionIconHover from "../../../assets/menu-dimension-ico-hover.svg"
import type {
SWHeaderColorValue, SWHeaderDimensionStyleValue, SWHeaderLayer, SWHeaderLayerValue,
SWHeaderLineStyleValue, SWHeaderTextStyleValue,
} from "../types"
import { SWHeaderField, SWHeaderFieldText, type SWHeaderFieldProps } from "./SWHeaderField"
import { SWHeaderLayerIconArt, layerIconOn } from "./SWHeaderLayerIcons"
interface FieldBase extends Pick<SWHeaderFieldProps, "expanded" | "controls" | "buttonRef"> {
label?: string
selected?: boolean
disabled?: boolean
onClick?: () => void
}
export interface SWHeaderLayerControlProps extends FieldBase {
value: SWHeaderLayerValue
/** State of the current layer for the three field icons (gray when off). Defaults to all colored. */
state?: Pick<SWHeaderLayer, "visible" | "frozen" | "locked">
}
export function SWHeaderLayerControl({ value, state, label = "레이어", ...rest }: SWHeaderLayerControlProps) {
return (
<SWHeaderField label={label} variant="layer" {...rest}>
<span className="hm-sw-header__layer-icons" aria-hidden="true">
<SWHeaderLayerIconArt kind="visibility" on={layerIconOn("visibility", state)} />
<SWHeaderLayerIconArt kind="freeze" on={layerIconOn("freeze", state)} />
<SWHeaderLayerIconArt kind="lock" on={layerIconOn("lock", state)} />
</span>
<SWHeaderFieldText label={label} value={value.name} />
</SWHeaderField>
)
}
export function SWHeaderColorControl({ value, label, ...rest }: FieldBase & { value: SWHeaderColorValue }) {
return (
<SWHeaderField label={label ?? "색상"} variant="color" {...rest}>
<span className="hm-sw-header__chip" aria-hidden="true">
<span style={{ background: value.swatch ?? "#ffe500" }} />
</span>
<SWHeaderFieldText label={label ?? "색상"} value={value.value} />
</SWHeaderField>
)
}
export function SWHeaderLineStyle({ value, label, ...rest }: FieldBase & { value: SWHeaderLineStyleValue }) {
return (
<SWHeaderField label={label ?? "선스타일"} variant="line-style" tier="aux" {...rest}>
<span className="hm-sw-header__linetype" aria-hidden="true"><i /><i /><i /><i /><i /></span>
<span className="hm-sw-header__line-values">
<SWHeaderFieldText label={label ?? "선스타일"} value={value.name} />
<SWHeaderFieldText label="가중치" value={value.weight} />
<SWHeaderFieldText label="스케일" value={value.scale} />
</span>
</SWHeaderField>
)
}
export function SWHeaderTextStyle({ value, label, ...rest }: FieldBase & { value: SWHeaderTextStyleValue }) {
return (
<SWHeaderField label={label ?? "문자스타일"} variant="text-style" tier="aux" {...rest}>
<span className="hm-sw-header__font-name" title={value.font}>{value.font}</span>
<SWHeaderFieldText label={label ?? "문자스타일"} value={value.name} />
</SWHeaderField>
)
}
export function SWHeaderDimensionStyle({ value, label, ...rest }: FieldBase & { value: SWHeaderDimensionStyleValue }) {
return (
<SWHeaderField label={label ?? "치수스타일"} variant="dimension-style" tier="aux" {...rest}>
<span className="hm-sw-header__dimension-icon" aria-hidden="true">
<img className="hm-sw-header__dimension-icon-default" src={dimensionIcon} alt="" />
<img className="hm-sw-header__dimension-icon-hover" src={dimensionIconHover} alt="" />
</span>
<SWHeaderFieldText label={label ?? "치수스타일"} value={value.name} />
</SWHeaderField>
)
}
@@ -0,0 +1,39 @@
import type { ReactNode } from "react"
import homeBackground from "../../../assets/home-bg.svg"
import homeBackgroundHover from "../../../assets/home-bg-hover.svg"
export interface SWHeaderHomeProps {
onClick?: () => void
/** Logo image URL, fitted (contain) into the 32 x 17 logo slot. */
logoSrc?: string
/** Logo element (inline SVG, <img>, ...). Takes precedence over `logoSrc`. */
logo?: ReactNode
/** Accessible product name. The home button is announced as "<logoAlt> 홈". */
logoAlt?: string
/**
* false: the logo stays in the same slot as a static image (no home button, hover or callback).
* SWHeader always shows the logo; only the home action can be removed. Default true.
*/
interactive?: boolean
}
/**
* 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) {
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}>
<img className="hm-sw-header__home-bg" src={homeBackground} alt="" aria-hidden="true" />
{content && <span className="hm-sw-header__logo" aria-hidden="true">{content}</span>}
</div>
)
return (
<button type="button" className="hm-sw-header__home" aria-label={logoAlt ? `${logoAlt} 홈` : "홈"} 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>}
</button>
)
}
@@ -0,0 +1,163 @@
import { createContext, useContext, forwardRef, useEffect, useRef, useState, type InputHTMLAttributes } from "react"
/** Structural removal discards drafts; ordinary dropdown close retains the legacy commit contract. */
export const SWHeaderCommitPresence = createContext<() => boolean>(() => true)
/** Built-in validation of the draft. `parse` overrides it (any custom format). */
export type SWHeaderInputValueType = "text" | "number"
export interface SWHeaderInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "type" | "size"> {
/** Current (committed) value from the host. A new value replaces the draft unless it is being edited. */
value: string
/** Every edit of the draft (temporary value, not committed). */
onValueChange?: (draft: string) => void
/**
* Committed value: Enter, leaving the input, or the dropdown around it closing. Called only when `parse`
* accepts the draft and the result differs from `value`; an invalid draft is discarded and `value` restored.
*/
onCommit?: (value: string) => void
/** Escape with an edited draft: the draft is restored to `value` (the event does not close the dropdown). */
onCancel?: () => void
/** `text` (default): trimmed, non-empty text. `number`: a decimal number (see `parseSWHeaderNumber`). */
valueType?: SWHeaderInputValueType
/** Validation + normalization, overriding `valueType`. Return the value to commit, or `null` for an invalid draft. */
parse?: (draft: string) => string | null
/** Default true (legacy dropdown behavior). False discards a draft when a dynamic property is removed. */
commitOnUnmount?: boolean
}
const parsers: Record<SWHeaderInputValueType, (draft: string) => string | null> = {
text: draft => draft.trim() || null,
number: draft => parseSWHeaderNumber(draft),
}
/**
* Editable SW input (Figma input 26:11656): red 14px value with the black text outline. Default has no
* surface; Hover shows a 1px rgba(212,203,189,.25) border (24px); editing (focus) shows #0F0D0A with a
* .3 border and the native caret in #E60403 (Figma cursor 26:11664, 1px; it blinks natively). The width
* follows the text. A real <input>: selection, IME (Enter while composing never commits) and the host
* value stay intact; values are never hard-coded here.
*/
export const SWHeaderInput = forwardRef<HTMLInputElement, SWHeaderInputProps>(function SWHeaderInput(
{ value, onValueChange, onCommit, onCancel, valueType = "text", parse = parsers[valueType], commitOnUnmount = true, readOnly, disabled, className, style, onBlur, onKeyDown, ...rest },
ref,
) {
const [draft, setDraft] = useState(value)
const [invalid, setInvalid] = useState(false)
const draftRef = useRef(value)
const isPresent = useContext(SWHeaderCommitPresence)
const pendingRef = useRef<string | null>(null)
const latest = useRef({ value, parse, onCommit, commitOnUnmount, readOnly, disabled })
latest.current = { value, parse, onCommit, commitOnUnmount, readOnly, disabled }
useEffect(() => {
draftRef.current = value
setDraft(value)
setInvalid(false)
pendingRef.current = null
}, [value, readOnly, disabled])
const update = (next: string) => {
draftRef.current = next
setDraft(next)
}
const commit = () => {
const { value: current, parse: parseDraft, onCommit: report } = latest.current
if (latest.current.readOnly || latest.current.disabled) return
if (draftRef.current === current) return
const next = parseDraft(draftRef.current)
setInvalid(false)
if (next === null) {
update(current)
return
}
update(next)
if (next !== current && next !== pendingRef.current) { pendingRef.current = next; report?.(next) }
}
// A click outside closes a dropdown before its input blurs: still commit what was typed.
useEffect(() => () => {
if (!isPresent() || !latest.current.commitOnUnmount || latest.current.readOnly || latest.current.disabled) return
const { value: current, parse: parseDraft, onCommit: report } = latest.current
if (draftRef.current === current) return
const next = parseDraft(draftRef.current)
if (next !== null && next !== current && next !== pendingRef.current) report?.(next)
}, [])
return (
<input
ref={ref}
type="text"
autoComplete="off"
spellCheck={false}
inputMode={valueType === "number" ? "decimal" : undefined}
{...rest}
className={`hm-sw-header__input${className ? ` ${className}` : ""}`}
// Width follows the draft (digits are 1ch); the paddings are added in CSS.
style={{ ["--hm-sw-input-chars" as string]: Math.max(draft.length, 1), ...style }}
aria-invalid={invalid || undefined}
data-invalid={invalid ? "true" : undefined}
value={draft}
// Without onCommit the value is shown read-only (same look, nothing is reported).
readOnly={readOnly || !onCommit}
disabled={disabled}
onChange={event => {
update(event.target.value)
setInvalid(latest.current.parse(event.target.value) === null)
onValueChange?.(event.target.value)
}}
onBlur={event => {
commit()
onBlur?.(event)
}}
onKeyDown={event => {
onKeyDown?.(event)
// IME composition (e.g. Hangul) owns Enter / Escape until it ends.
if (event.defaultPrevented || event.nativeEvent.isComposing || event.keyCode === 229) return
if (event.key === "Enter") {
event.preventDefault()
commit()
} else if (event.key === "Escape" && draftRef.current !== latest.current.value) {
// The first Escape only cancels the edit; the next one closes the dropdown around it.
event.stopPropagation()
update(latest.current.value)
setInvalid(false)
onCancel?.()
}
}}
/>
)
})
/** `r,g,b` (0-255, spaces allowed) or `#rgb` / `#rrggbb` → normalized color, otherwise null. */
export function parseSWHeaderColor(text: string): { value: string; rgb: [number, number, number]; hex: string } | null {
const trimmed = text.trim()
let rgb: number[] | null = null
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(trimmed)
if (hex) {
const digits = hex[1].length === 3 ? hex[1].split("").map(digit => digit + digit).join("") : hex[1]
rgb = [0, 2, 4].map(index => parseInt(digits.slice(index, index + 2), 16))
} else {
const parts = trimmed.split(/\s*,\s*/)
if (parts.length === 3 && parts.every(part => /^\d{1,3}$/.test(part))) rgb = parts.map(Number)
}
if (!rgb || rgb.some(channel => channel > 255)) return null
const [r, g, b] = rgb
return { value: `${r},${g},${b}`, rgb: [r, g, b], hex: `#${rgb.map(channel => channel.toString(16).padStart(2, "0")).join("")}` }
}
/**
* Decimal number parser (`-1.5`, `0.25`, `.5`) → its text, otherwise null. `min` / `max` are inclusive;
* `minExclusive` makes `min` exclusive (e.g. a scale greater than zero).
*/
export function parseSWHeaderNumber(
text: string,
{ min = -Infinity, max = Infinity, minExclusive = false }: { min?: number; max?: number; minExclusive?: boolean } = {},
): string | null {
const trimmed = text.trim()
if (!/^-?(\d+\.?\d*|\.\d+)$/.test(trimmed)) return null
const number = Number(trimmed)
if (!Number.isFinite(number) || number > max || (minExclusive ? number <= min : number < min)) return null
return trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed
}
@@ -0,0 +1,77 @@
import { useEffect, useId, useRef, useState, type RefObject } from "react"
import popupGuide from "../../../assets/popup-guide.svg"
import popupGuideHover from "../../../assets/popup-guide-hover.svg"
import guideImage from "../../../assets/k-guide.png"
import defaultMask from "../../../assets/k-guide-mask-default.svg"
import panMask from "../../../assets/k-guide-mask-pan.svg"
import fitMask from "../../../assets/k-guide-mask-fit.svg"
import { useSWHeaderOverlay } from "./SWHeaderPropertyPanel"
/** Original K button. Its button and floating guide form one hover/focus region. */
export function SWHeaderKGuide({ disabled, onClick, label = "팝업 가이드" }: { disabled?: boolean; onClick?: () => void; label?: string }) {
const triggerRef = useRef<HTMLButtonElement>(null)
const regionRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)
const [dismissed, setDismissed] = useState(false)
const guideId = useId()
const visible = open && !disabled && !dismissed
return (
<div ref={regionRef} className="hm-sw-k-guide" data-open={visible || undefined} onPointerEnter={() => { setDismissed(false); setOpen(true) }} onPointerLeave={() => { setOpen(false); setDismissed(false) }} onFocus={() => setOpen(true)} onBlur={event => {
if (!regionRef.current?.contains(event.relatedTarget as Node | null)) { setOpen(false); setDismissed(false) }
}} onKeyDown={event => { if (event.key === "Escape" && visible) { event.stopPropagation(); setDismissed(true) } }}>
<button ref={triggerRef} type="button" className="hm-sw-header__action hm-sw-header__action--guide" aria-label={label} aria-describedby={visible ? guideId : undefined} disabled={disabled} onClick={onClick}>
<img className="hm-sw-header__icon" src={popupGuide} alt="" aria-hidden="true" />
<img className="hm-sw-header__icon-hover" src={popupGuideHover} alt="" aria-hidden="true" />
</button>
{visible && <SWHeaderKGuidePreview anchorRef={triggerRef} id={guideId} />}
</div>
)
}
/** Same preview for the existing More entry on narrow headers; no duplicate K button. */
export function SWHeaderKGuidePreview({ anchorRef, id }: { anchorRef: RefObject<HTMLElement | null>; id?: string }) {
const [reduced, setReduced] = useState(false)
const [frame, setFrame] = useState(1)
const [duration, setDuration] = useState(0)
const { panelRef, style } = useSWHeaderOverlay(anchorRef, 208, -84)
useEffect(() => {
const query = window.matchMedia("(prefers-reduced-motion: reduce)")
const update = () => setReduced(query.matches)
update(); query.addEventListener("change", update)
return () => query.removeEventListener("change", update)
}, [])
useEffect(() => {
setFrame(1); setDuration(0)
if (reduced) return
let timer: ReturnType<typeof setTimeout>
let phase = 0
// Delay + Figma transition, then a short natural return for the user-requested loop.
const phases = [
{ frame: 2, delay: 400, duration: 1022 },
{ frame: 3, delay: 1122, duration: 1022 },
{ frame: 4, delay: 1172, duration: 511 },
{ frame: 1, delay: 911, duration: 511 },
]
const schedule = () => {
const next = phases[phase]
timer = setTimeout(() => {
setDuration(next.duration); setFrame(next.frame)
phase = (phase + 1) % phases.length
if (phase === 0) {
// Complete the return transition before starting the next 400ms initial hold.
timer = setTimeout(schedule, 511)
} else schedule()
}, next.delay)
}
schedule()
return () => clearTimeout(timer)
}, [reduced])
return (
<div ref={panelRef} id={id} role="tooltip" className="hm-sw-k-guide__panel" data-frame={frame} data-reduced-motion={reduced || undefined} style={style}>
<div className="hm-sw-k-guide__viewport" style={{ maskImage: `url("${frame === 4 ? fitMask : frame === 1 ? defaultMask : panMask}")` }}>
<img src={guideImage} alt="" style={{ transitionDuration: `${duration}ms` }} />
</div>
<span>다중인쇄/도면관리</span>
</div>
)
}
@@ -0,0 +1,87 @@
import layerEllipse from "../../../assets/layer-ellipse.svg"
import layerEllipseOff from "../../../assets/layer-ellipse-off.svg"
import layerFreezeHover from "../../../assets/layer-freeze-hover.svg"
import layerFreezeOff from "../../../assets/layer-freeze-off.svg"
import layerFreezeOffHover from "../../../assets/layer-freeze-off-hover.svg"
import layerLockHover from "../../../assets/layer-lock-hover.svg"
import layerLockOff from "../../../assets/layer-lock-off.svg"
import layerLockOffHover from "../../../assets/layer-lock-off-hover.svg"
import layerStateA from "../../../assets/layer-state-a.svg"
import layerStateB from "../../../assets/layer-state-b.svg"
import layerUnion from "../../../assets/layer-union.svg"
import type { SWHeaderLayer } from "../types"
/** 전구 icon_전구 26:10810 · 눈꽃 icon_눈꽃 26:10855 · 자물쇠 icon_자물쇠 26:10912 (20 x 20, 4 variants each). */
export type SWHeaderLayerIconKind = "visibility" | "freeze" | "lock"
/** Figma Default / Variant2 (hover) = colored "on", Variant3 / Variant4 (hover) = gray "off". */
const art = {
freeze: { on: layerStateB, onHover: layerFreezeHover, off: layerFreezeOff, offHover: layerFreezeOffHover },
lock: { on: layerStateA, onHover: layerLockHover, off: layerLockOff, offHover: layerLockOffHover },
}
/**
* Colored ("on") variant per layer state (user-confirmed 2026-10-16, design spec §5.2.1):
* bulb visible, snowflake not frozen, lock not locked. Gray means hidden / frozen / locked.
*/
export function layerIconOn(kind: SWHeaderLayerIconKind, layer?: Pick<SWHeaderLayer, "visible" | "frozen" | "locked">) {
if (kind === "visibility") return layer?.visible !== false
if (kind === "freeze") return layer?.frozen !== true
return layer?.locked !== true
}
/** Static icon art. The bulb is the Figma composite (union + bars + ellipse); off swaps only the ellipse. */
export function SWHeaderLayerIconArt({ kind, on }: { kind: SWHeaderLayerIconKind; on: boolean }) {
if (kind === "visibility") {
return (
<span className="hm-sw-header__layer-icon hm-sw-header__layer-icon--composite">
<img className="hm-sw-header__layer-union" src={layerUnion} alt="" />
<span className="hm-sw-header__layer-bars"><i /><i /><i /><i /><i /></span>
<img className="hm-sw-header__layer-ellipse" src={on ? layerEllipse : layerEllipseOff} alt="" />
</span>
)
}
return <img className="hm-sw-header__layer-icon" src={on ? art[kind].on : art[kind].off} alt="" />
}
const labels: Record<SWHeaderLayerIconKind, string> = { visibility: "표시", freeze: "동결", lock: "잠금" }
/**
* One 20 x 20 state toggle of a layer row. The visual comes from the layer data only; hover is CSS
* (bulb: black 25% background, snowflake / lock: their Figma hover asset). Without `onChange` it is a
* status icon, so no click does nothing.
*/
export function SWHeaderLayerToggle({ kind, layer, onChange }: {
kind: SWHeaderLayerIconKind
layer: SWHeaderLayer
onChange?: (layer: SWHeaderLayer, next: boolean) => void
}) {
const on = layerIconOn(kind, layer)
// aria-pressed follows the data meaning (shown / frozen / locked), not the icon color.
const pressed = kind === "visibility" ? on : !on
const className = `hm-sw-header__layer-toggle hm-sw-header__layer-toggle--${kind}`
const label = `${layer.name} ${labels[kind]}`
if (!onChange) {
return (
<span className={className} data-on={on ? "true" : "false"} role="img" aria-label={`${label} ${pressed ? "켜짐" : "꺼짐"}`}>
<SWHeaderLayerIconArt kind={kind} on={on} />
</span>
)
}
return (
<button
type="button"
className={className}
data-on={on ? "true" : "false"}
aria-pressed={pressed}
aria-label={label}
disabled={layer.disabled}
onClick={() => onChange(layer, !pressed)}
>
<SWHeaderLayerIconArt kind={kind} on={on} />
{kind !== "visibility" && (
<img className="hm-sw-header__layer-toggle-hover" src={on ? art[kind].onHover : art[kind].offHover} alt="" />
)}
</button>
)
}
@@ -0,0 +1,232 @@
import { useEffect, useId, useLayoutEffect, useRef, useState, type KeyboardEvent } from "react"
import { SWHeaderMenuDetail } from "./SWHeaderDropdown"
import type { SWHeaderLayer, SWHeaderLayerValue } from "../types"
import { SWHeaderLayerControl } from "./SWHeaderFields"
import { SWHeaderLayerToggle } from "./SWHeaderLayerIcons"
export interface SWHeaderLayerMenuProps {
/** Name shown in the field when no `layers` entry matches `activeLayerId`. */
label?: string
value: SWHeaderLayerValue
/** Rows of the dropdown, in this order. An empty array shows only the footer. */
layers: SWHeaderLayer[]
/** Current layer: shown in the field (name and state icons) and as a #FFC600 name in the list. */
activeLayerId?: string
/** Controlled open state. Leave undefined to let the component manage it. */
open?: boolean
onOpenChange?: (open: boolean) => void
/** Called on every click of the field (also when the dropdown toggles). */
onClick?: () => void
/** Keeps the field in its selected (#FFC600) look while closed. */
selected?: boolean
disabled?: boolean
onLayerSelect?: (layer: SWHeaderLayer) => void
onVisibilityChange?: (layer: SWHeaderLayer, nextVisible: boolean) => void
onFreezeChange?: (layer: SWHeaderLayer, nextFrozen: boolean) => void
onLockChange?: (layer: SWHeaderLayer, nextLocked: boolean) => void
/** "레이어 상세". Optional host action; omitting it does not disable hover or simulate CAD work. */
onDetail?: () => void
}
/**
* Layer field with its dropdown (Figma header/menu/레이어 26:10940, rows header/dropdown/brown/row
* 26:10687). Click / Enter / Space toggle it (hover never opens it), ArrowDown opens it and focuses
* the current row, Escape / a click outside / Tab out close it. Selecting a row closes it; the bulb,
* snowflake and lock are sibling buttons of the row selection, so they never select and keep it open.
*/
export function SWHeaderLayerMenu({
value,
label,
layers,
activeLayerId,
open: openProp,
onOpenChange,
onClick,
selected = false,
disabled = false,
onLayerSelect,
onVisibilityChange,
onFreezeChange,
onLockChange,
onDetail,
}: SWHeaderLayerMenuProps) {
const [internalOpen, setInternalOpen] = useState(false)
const open = !disabled && (openProp ?? internalOpen)
const rootRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
const menuId = useId()
// Set when the keyboard opens the menu, so the current (or first) row gets focus once it renders.
const focusRowRef = useRef(false)
// Horizontal offset that keeps the 234px list inside the header (narrow screens only).
const [offset, setOffset] = useState(0)
const setOpen = (next: boolean) => {
if (openProp === undefined) setInternalOpen(next)
onOpenChange?.(next)
}
const close = (restoreFocus: boolean) => {
setOpen(false)
if (restoreFocus) triggerRef.current?.focus()
}
useEffect(() => {
if (!open) return
const onPointerDown = (event: PointerEvent) => {
if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
}
document.addEventListener("pointerdown", onPointerDown)
return () => document.removeEventListener("pointerdown", onPointerDown)
})
useLayoutEffect(() => {
if (!open) return
const update = () => {
const root = rootRef.current
const menu = menuRef.current
if (!root || !menu) return
const bounds = (root.closest(".hm-sw-header") ?? document.documentElement).getBoundingClientRect()
// 1px inside the bounds so the outline ring (drawn 1px outside the list) never overflows.
const left = Math.max(bounds.left, 0) + 1
const right = Math.min(bounds.right, document.documentElement.clientWidth) - 1
const start = root.getBoundingClientRect().left
let next = Math.min(0, right - menu.offsetWidth - start)
if (start + next < left) next = left - start
setOffset(next > 0 ? Math.ceil(next) : Math.floor(next))
}
update()
window.addEventListener("resize", update)
return () => window.removeEventListener("resize", update)
}) // Host-driven reordering can move an open dropdown without a window resize.
useEffect(() => {
if (open && focusRowRef.current) {
focusRowRef.current = false
const current = rootRef.current?.querySelector<HTMLButtonElement>(".hm-sw-header__layer-row[data-active] .hm-sw-header__layer-item:not(:disabled)")
;(current ?? grid()[0]?.[0])?.focus()
}
})
/** Rows of focusable controls: each layer row (toggles + selection), then the footer. */
const grid = () => Array.from(rootRef.current?.querySelectorAll<HTMLElement>(".hm-sw-header__layer-row, .hm-sw-header__layer-footer") ?? [])
.map(row => Array.from(row.querySelectorAll<HTMLButtonElement>("button:not(:disabled)")))
.filter(row => row.length > 0)
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Escape" && open) {
event.stopPropagation()
close(true)
return
}
const vertical = event.key === "ArrowDown" || event.key === "ArrowUp"
const horizontal = event.key === "ArrowLeft" || event.key === "ArrowRight"
if (!vertical && !horizontal) return
if (!open) {
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return
event.preventDefault()
focusRowRef.current = true
setOpen(true)
return
}
const rows = grid()
if (rows.length === 0) return
const active = document.activeElement as HTMLButtonElement
const rowIndex = rows.findIndex(row => row.includes(active))
event.preventDefault()
if (rowIndex < 0) {
// Focus on the field: enter the list.
if (vertical) rows[event.key === "ArrowDown" ? 0 : rows.length - 1][0]?.focus()
return
}
const row = rows[rowIndex]
const column = row.indexOf(active)
if (horizontal) {
row[(column + (event.key === "ArrowRight" ? 1 : -1) + row.length) % row.length]?.focus()
return
}
const target = rows[(rowIndex + (event.key === "ArrowDown" ? 1 : -1) + rows.length) % rows.length]
// Same column (bulb / snowflake / lock / name) in the next row; the footer has one button.
target[Math.min(column, target.length - 1)]?.focus()
}
const current = layers.find(item => item.id === activeLayerId)
return (
<div
ref={rootRef}
className="hm-sw-header__layer"
data-open={open ? "true" : undefined}
onKeyDown={onKeyDown}
onBlur={event => {
// Tabbing out of the field and its dropdown closes it; focus is never trapped.
if (open && !rootRef.current?.contains(event.relatedTarget as Node | null)) setOpen(false)
}}
>
<SWHeaderLayerControl
buttonRef={triggerRef}
label={label}
value={current ? { name: current.name } : value}
state={current}
selected={selected || open}
expanded={open}
controls={open ? menuId : undefined}
disabled={disabled}
onClick={() => {
onClick?.()
setOpen(!open)
}}
/>
{open && (
<div
ref={menuRef}
id={menuId}
className="hm-sw-header__layer-menu"
role="group"
aria-label="레이어 목록"
data-shifted={offset !== 0 ? "true" : undefined}
style={offset !== 0 ? { left: offset } : undefined}
>
<div className="hm-sw-header__layer-panel">
{layers.length > 0 && (
<ul className="hm-sw-header__layer-list">
{layers.map(layer => (
<li
key={layer.id}
className="hm-sw-header__layer-row"
data-active={layer === current ? "true" : undefined}
data-disabled={layer.disabled ? "true" : undefined}
>
<span className="hm-sw-header__layer-toggles">
<SWHeaderLayerToggle kind="visibility" layer={layer} onChange={onVisibilityChange} />
<SWHeaderLayerToggle kind="freeze" layer={layer} onChange={onFreezeChange} />
<SWHeaderLayerToggle kind="lock" layer={layer} onChange={onLockChange} />
</span>
<button
type="button"
className="hm-sw-header__layer-item"
title={layer.name}
aria-current={layer === current ? "true" : undefined}
disabled={layer.disabled}
onClick={() => {
onLayerSelect?.(layer)
close(true)
}}
>
<span className="hm-sw-header__layer-swatch" aria-hidden="true">
<span style={{ background: layer.color ?? "#ffffff" }} />
</span>
<span className="hm-sw-header__layer-name">{layer.name}</span>
</button>
</li>
))}
</ul>
)}
<div className="hm-sw-header__layer-footer">
<SWHeaderMenuDetail label="레이어 상세" className="hm-sw-header__layer-detail" disabled={disabled} onClick={onDetail && (() => { onDetail(); close(true) })} />
</div>
</div>
</div>
)}
</div>
)
}
@@ -0,0 +1,154 @@
import { useLayoutEffect, type CSSProperties } from "react"
import type { SWHeaderLineStyleValue, SWHeaderLineType, SWHeaderLineWeight } from "../types"
import { SWHeaderMenuDetail, useSWHeaderDropdown } from "./SWHeaderDropdown"
import { SWHeaderLineStyle } from "./SWHeaderFields"
import { SWHeaderInput, parseSWHeaderNumber } from "./SWHeaderInput"
/** Figma line type previews (header/menu/선스타일 26:11143): dash lengths of the 70px line, 4px gaps. */
export const swHeaderLinePatterns = {
/** CENTER: long 22 / short 7. */
center: [22, 7, 22, 7],
/** HIDDEN: two 33px dashes. */
hidden: [33, 33],
} as const satisfies Record<string, readonly number[]>
/** 70 x 2 line preview with a 1px black outline (Figma Rectangle 7532, stroke outside). */
export function SWHeaderLinePreview({ pattern }: { pattern?: readonly number[] }) {
const segments = pattern && pattern.length > 0 ? pattern : [1]
return (
<span className="hm-sw-header__line-preview" aria-hidden="true">
{segments.map((length, index) => <i key={index} style={{ flexGrow: length }} />)}
</span>
)
}
export interface SWHeaderLineStyleMenuProps {
/** Values shown in the field (line style / weight / scale) and in the scale input. */
label?: string
value: SWHeaderLineStyleValue
/** Line type column. Host data. */
lineTypes?: SWHeaderLineType[]
/** Line weight column. Host data. */
lineWeights?: SWHeaderLineWeight[]
/** Current line type; defaults to the row named `value.name`. */
activeLineTypeId?: string
/** Current line weight; defaults to the row named `value.weight`. */
activeLineWeightId?: string
/** Controlled open state. Leave undefined to let the component manage it. */
open?: boolean
onOpenChange?: (open: boolean) => void
/** Called on every click of the field (also when the dropdown toggles). */
onClick?: () => void
/** Keeps the field in its selected (#FFC600) look while closed. */
selected?: boolean
disabled?: boolean
onLineTypeSelect?: (lineType: SWHeaderLineType) => void
onLineWeightSelect?: (weight: SWHeaderLineWeight) => void
onLineWeightValueChange?: (weight: SWHeaderLineWeight, value: string) => void
onScaleChange?: (value: string) => void
/** "선스타일 상세". Optional host action; omitting it keeps hover and performs no CAD action. */
onDetail?: () => void
}
/**
* Line style field with its 3-column dropdown (Figma header/menu/선스타일 26:11104; line type rows
* header/dropdown/brown/row 26:10702): line type 182 · weight 142 · scale 142, centered under the field.
* Each column changes only its own property and the dropdown stays open. Rows keep their Figma hover
* (#62533D, #FF5C00 text and line); selected rows retain the same emphasis after mouse leave.
*/
export function SWHeaderLineStyleMenu({
value, label, lineTypes = [], lineWeights = [], activeLineTypeId, activeLineWeightId, open: openProp, onOpenChange, onClick,
selected = false, disabled = false, onLineTypeSelect, onLineWeightSelect, onLineWeightValueChange, onScaleChange, onDetail,
}: SWHeaderLineStyleMenuProps) {
const { open, setOpen, close, panelRef, triggerRef, menuId, panelStyle, rootProps } = useSWHeaderDropdown({ open: openProp, onOpenChange, disabled, align: "center" })
const currentType = lineTypes.find(item => (activeLineTypeId === undefined ? item.name === value.name : item.id === activeLineTypeId))
const currentWeight = lineWeights.find(item => (activeLineWeightId === undefined ? item.name === value.weight : item.id === activeLineWeightId))
// A promoted field can shrink below 284px. Paint only its real contact width, without changing
// the shared dropdown alignment or the rest of the yellow outline.
useLayoutEffect(() => {
const field = triggerRef.current
const panel = panelRef.current
if (!open || !field || !panel) return
const update = () => panel.style.setProperty("--hm-sw-line-seam-width", `${field.getBoundingClientRect().width}px`)
update()
const observer = new ResizeObserver(update)
observer.observe(field)
return () => observer.disconnect()
}, [open, triggerRef, panelRef])
return (
<div {...rootProps} className="hm-sw-header__menu hm-sw-header__menu--line-style" data-menu="line-style" data-tier="aux">
<SWHeaderLineStyle
buttonRef={triggerRef}
label={label}
value={value}
selected={selected || open}
expanded={open}
controls={open ? menuId : undefined}
disabled={disabled}
onClick={() => {
onClick?.()
setOpen(!open)
}}
/>
{open && (
<div ref={panelRef} id={menuId} className="hm-sw-header__menu-panel hm-sw-header__line-menu" role="group" aria-label="선스타일"
style={{ ...panelStyle, "--hm-sw-line-seam-left": `${-(panelStyle?.left ?? 0)}px` } as CSSProperties}>
<div className="hm-sw-header__line-drop">
<div className="hm-sw-header__line-column hm-sw-header__line-column--type">
<ul className="hm-sw-header__menu-list" aria-label="선 종류">
{lineTypes.map(item => (
<li key={item.id} className="hm-sw-header__menu-row hm-sw-header__menu-row--line" data-active={item === currentType ? "true" : undefined} data-disabled={item.disabled ? "true" : undefined}>
<button type="button" className="hm-sw-header__menu-item" title={item.name} aria-current={item === currentType ? "true" : undefined} disabled={item.disabled} onClick={() => onLineTypeSelect?.(item)}>
<SWHeaderLinePreview pattern={item.pattern} />
<span className="hm-sw-header__menu-text hm-sw-header__menu-name">{item.name}</span>
</button>
</li>
))}
</ul>
<div className="hm-sw-header__menu-footer">
<SWHeaderMenuDetail label="선스타일 상세" onClick={onDetail && (() => { onDetail(); close(true) })} />
</div>
</div>
<div className="hm-sw-header__line-column">
<ul className="hm-sw-header__menu-list" aria-label="가중치">
{lineWeights.map(item => (
<li key={item.id} className="hm-sw-header__menu-row hm-sw-header__menu-row--weight" data-active={item === currentWeight ? "true" : undefined} data-disabled={item.disabled ? "true" : undefined}>
<button type="button" className="hm-sw-header__menu-item" title={item.name} aria-current={item === currentWeight ? "true" : undefined} disabled={item.disabled} onClick={() => onLineWeightSelect?.(item)}>
<span className="hm-sw-header__menu-text hm-sw-header__menu-name">{item.name}</span>
{item.value !== undefined && !item.editable && <span className="hm-sw-header__menu-text hm-sw-header__menu-value">{item.value}</span>}
</button>
{item.editable && (
// Sibling of the row button (no nested controls): editing never selects the row.
<SWHeaderInput
aria-label={`${item.name} 가중치`}
valueType="number"
value={item.value ?? ""}
disabled={item.disabled}
parse={draft => parseSWHeaderNumber(draft, { min: 0 })}
onCommit={onLineWeightValueChange && (next => onLineWeightValueChange(item, next))}
/>
)}
</li>
))}
</ul>
</div>
<div className="hm-sw-header__line-column">
<div className="hm-sw-header__menu-row hm-sw-header__menu-row--weight">
<span className="hm-sw-header__menu-label hm-sw-header__menu-text hm-sw-header__menu-name">스케일</span>
<SWHeaderInput
aria-label="스케일"
valueType="number"
value={value.scale}
parse={draft => parseSWHeaderNumber(draft, { min: 0, minExclusive: true })}
onCommit={onScaleChange}
/>
</div>
</div>
</div>
</div>
)}
</div>
)
}
@@ -0,0 +1,118 @@
import { useEffect, useId, useRef, useState, type KeyboardEvent } from "react"
import { SWHeaderKGuidePreview } from "./SWHeaderKGuide"
export interface SWHeaderMoreItem {
visible?: boolean
id: string
label: string
/** Current value shown at the right (e.g. the line style name). */
value?: string
/**
* Header width where the item appears: `aux` < 1800, `panel` < 1080, `narrow` < 900, `action` < 640.
* `slot`: only while another field has taken this field's place (the layer field, see SWHeader).
*/
tier: "aux" | "panel" | "narrow" | "action" | "slot"
/** Field this item stands for (`data-menu` of the field), used by the representative-slot CSS. */
menu?: string
/** Renders a checkable item. */
checked?: boolean
selected?: boolean
disabled?: boolean
onSelect?: () => void
/** Reuse the K guide when its original button has moved into More. */
guide?: boolean
}
export interface SWHeaderMoreProps {
items: SWHeaderMoreItem[]
}
/** Overflow menu that keeps every moved control reachable. */
export function SWHeaderMore({ items }: SWHeaderMoreProps) {
const [open, setOpen] = useState(false)
const rootRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
const menuId = useId()
const guideId = useId()
const guideRef = useRef<HTMLButtonElement>(null)
const [guideOpen, setGuideOpen] = useState(false)
useEffect(() => { if (!open) setGuideOpen(false) }, [open])
useEffect(() => {
if (!open) return
const onPointerDown = (event: PointerEvent) => {
if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
}
document.addEventListener("pointerdown", onPointerDown)
return () => document.removeEventListener("pointerdown", onPointerDown)
}, [open])
const visibleItems = () => Array.from(rootRef.current?.querySelectorAll<HTMLButtonElement>('[role^="menuitem"]:not(:disabled)') ?? []).filter(item => item.offsetParent !== null)
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && open) {
event.stopPropagation()
setOpen(false)
triggerRef.current?.focus()
} else if ((event.key === "ArrowDown" || event.key === "ArrowUp") && open) {
event.preventDefault()
const entries = visibleItems()
const index = entries.indexOf(document.activeElement as HTMLButtonElement)
const next = event.key === "ArrowDown" ? (index + 1) % entries.length : (index - 1 + entries.length) % entries.length
entries[next]?.focus()
}
}
return (
<div ref={rootRef} className="hm-sw-header__more" onKeyDown={onKeyDown} onPointerLeave={() => setGuideOpen(false)}>
<button
ref={triggerRef}
type="button"
className="hm-sw-header__more-trigger"
aria-label="더보기"
aria-haspopup="menu"
aria-expanded={open}
aria-controls={open ? menuId : undefined}
onClick={() => setOpen(value => !value)}
>
<svg width="16" height="4" viewBox="0 0 16 4" aria-hidden="true" focusable="false">
<circle cx="2" cy="2" r="1.6" fill="currentColor" />
<circle cx="8" cy="2" r="1.6" fill="currentColor" />
<circle cx="14" cy="2" r="1.6" fill="currentColor" />
</svg>
</button>
{open && (
<div id={menuId} role="menu" aria-label="추가 도구" className="hm-sw-header__more-menu">
{items.map(item => (
<button
key={item.id}
ref={item.guide ? guideRef : undefined}
type="button"
role={item.checked === undefined ? "menuitem" : "menuitemcheckbox"}
aria-checked={item.checked}
className="hm-sw-header__more-item"
data-tier={item.tier}
data-visible={item.visible}
hidden={item.visible === false}
data-menu={item.menu}
data-selected={item.selected || item.checked ? "true" : undefined}
disabled={item.disabled}
aria-describedby={item.guide && guideOpen ? guideId : undefined}
onPointerEnter={() => setGuideOpen(!!item.guide && !item.disabled)}
onFocus={() => setGuideOpen(!!item.guide && !item.disabled)}
onBlur={event => { if (!rootRef.current?.contains(event.relatedTarget as Node | null)) setGuideOpen(false) }}
onClick={() => {
item.onSelect?.()
setOpen(false)
}}
>
<span>{item.label}</span>
{item.value && <span className="hm-sw-header__more-value">{item.value}</span>}
</button>
))}
{guideOpen && <SWHeaderKGuidePreview anchorRef={guideRef} id={guideId} />}
</div>
)}
</div>
)
}
@@ -0,0 +1,30 @@
import { useId } from "react"
export interface SWHeaderPropertiesProps {
barOpen?: boolean
panelOpen?: boolean
panelId?: string
disabled?: boolean
onBarToggle?: () => void
onPanelToggle?: () => void
/** Individual parts (default true). Removed parts take no space; with one toggle the divider is removed. */
showCaption?: boolean
showBar?: boolean
showPanel?: boolean
}
/** `상세보기` caption above the 속성Bar / 속성창 toggle pair. */
export function SWHeaderProperties({ barOpen = false, panelOpen = false, panelId, disabled, onBarToggle, onPanelToggle, showCaption = true, showBar = true, showPanel = true }: SWHeaderPropertiesProps) {
const captionId = useId()
if (!showCaption && !showBar && !showPanel) return null
return (
<div className="hm-sw-header__properties" role="group" aria-labelledby={showCaption ? captionId : undefined} aria-label={showCaption ? undefined : "상세보기"} data-partial={showCaption && showBar && showPanel ? undefined : ""}>
{showCaption && <span id={captionId} className="hm-sw-header__properties-caption">상세보기</span>}
{(showBar || showPanel) && <div className="hm-sw-header__properties-row">
{showBar && <button type="button" className="hm-sw-header__toggle" aria-pressed={barOpen} data-selected={barOpen ? "true" : undefined} disabled={disabled} onClick={onBarToggle}>속성Bar</button>}
{showBar && showPanel && <span className="hm-sw-header__properties-divider" aria-hidden="true" />}
{showPanel && <button type="button" className="hm-sw-header__toggle" data-property-panel-trigger="" aria-pressed={panelOpen} aria-haspopup={panelId ? "dialog" : undefined} aria-expanded={panelId ? panelOpen : undefined} aria-controls={panelOpen ? panelId : undefined} data-selected={panelOpen ? "true" : undefined} disabled={disabled} onClick={onPanelToggle}>속성창</button>}
</div>}
</div>
)
}
@@ -0,0 +1,53 @@
import { useId, type HTMLAttributes } from "react"
import { SWHeaderInput, parseSWHeaderColor, parseSWHeaderNumber } from "./SWHeaderInput"
import type { SWHeaderProperty, SWHeaderPropertySelection } from "./SWHeaderPropertyPanel"
export interface SWHeaderPropertyBarProps extends HTMLAttributes<HTMLDivElement> {
selection: SWHeaderPropertySelection
/** Same host-owned records as the property panel; array order is retained unless order is set. */
properties: readonly SWHeaderProperty[]
onPropertyChange?: (property: SWHeaderProperty, value: string) => void
readOnly?: boolean
disabled?: boolean
}
function BarField({ property, onPropertyChange, readOnly, disabled }: Pick<SWHeaderPropertyBarProps, "onPropertyChange" | "readOnly" | "disabled"> & { property: SWHeaderProperty }) {
const id = useId()
const editable = !readOnly && !disabled && !property.readOnly && !property.disabled && property.type !== "computed"
const parse = (draft: string) => {
if (property.validate && !property.validate(draft)) return null
if (property.type === "number" || property.type === "slider") return parseSWHeaderNumber(draft, { min: property.min, max: property.max })
if (property.type === "color") return parseSWHeaderColor(draft)?.value ?? null
if (property.type === "select") return property.options?.some(option => option.id === draft && !option.disabled) ? draft : null
return draft.trim() || null
}
return (
<div className="hm-sw-property-bar__field" data-property-id={property.id}>
<label htmlFor={id}>{property.name}</label>
<SWHeaderInput id={id} value={property.mixed ? "다양함" : property.value}
valueType={property.type === "number" || property.type === "slider" ? "number" : "text"}
disabled={disabled || property.disabled} readOnly={!editable} parse={parse} commitOnUnmount={false}
onCommit={editable && (onPropertyChange || property.onCommit) ? next => {
property.onCommit?.(next, property)
onPropertyChange?.(property, next)
} : undefined} />
{property.unit && <span>{property.unit}</span>}
</div>
)
}
/** Figma 26:10492: a separate 28px editing strip; no CAD values or calculations are built in. */
export function SWHeaderPropertyBar({ selection, properties, onPropertyChange, readOnly, disabled, className, ...rest }: SWHeaderPropertyBarProps) {
return (
<div {...rest} className={`hm-sw-property-bar${className ? ` ${className}` : ""}`} role="region" aria-label={rest["aria-label"] ?? "속성Bar"} tabIndex={0}>
<div className="hm-sw-property-bar__content" key={selection.id}>
<span className="hm-sw-property-bar__object" title={selection.title}>{selection.title}</span>
<div className="hm-sw-property-bar__fields">
{[...properties].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).map(property => (
<BarField key={property.id} property={property} onPropertyChange={onPropertyChange} readOnly={readOnly} disabled={disabled} />
))}
</div>
</div>
</div>
)
}
@@ -0,0 +1,257 @@
import { useEffect, useId, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode, type RefObject } from "react"
import circleGraphic from "../../../assets/property-circle.svg"
import titleGraphic from "../../../assets/property-title.svg"
import singleTitleGraphic from "../../../assets/property-title-single.svg"
import footerGraphic from "../../../assets/property-footer.svg"
import expandGraphic from "../../../assets/property-expand.svg"
import toolDefault from "../../../assets/property-tool.svg"
import toolHover from "../../../assets/property-tool-hover.svg"
import toolActiveGraphic from "../../../assets/property-tool-active.svg"
import closeDefault from "../../../assets/property-close.svg"
import closeHover from "../../../assets/property-close-hover.svg"
import { SWHeaderInput, parseSWHeaderColor, parseSWHeaderNumber } from "./SWHeaderInput"
import { SWHeaderLinePreview } from "./SWHeaderLineStyleMenu"
export type SWHeaderPropertyType = "text" | "number" | "select" | "color" | "slider" | "computed"
export interface SWHeaderProperty {
id: string
name: string
value: string
type: SWHeaderPropertyType
groupId: string
order?: number
readOnly?: boolean
disabled?: boolean
mixed?: boolean
unit?: string
annotation?: string
min?: number
max?: number
step?: number
options?: readonly { id: string; name: string; disabled?: boolean }[]
/** Optional color swatch / original line pattern, supplied by the host. */
swatch?: string
linePattern?: readonly number[]
linePreview?: boolean
graphicSlot?: "circumference" | "diameter" | "radius" | "area"
validate?: (draft: string) => boolean
onCommit?: (value: string, property: SWHeaderProperty) => void
}
export interface SWHeaderPropertyGroup {
id: string
name?: string
order?: number
layout?: "rows" | "circle" | "position"
/** Rows beyond this count are revealed by the expansion button. */
collapsedCount?: number
positionLabel?: string
}
export interface SWHeaderPropertySelection {
/** Change whenever the selected objects change, even if their property ids match. */
id: string
title: string
kind: "none" | "all" | "single"
counts?: readonly { id: string; name: string; count: number; row?: number; column?: number }[]
/** Host-owned return to the overall selection; no CAD selection is performed internally. */
overviewTitle?: string
}
/** What a shape slot receives: the selection, its group and that group's properties (values stay in the rows). */
export interface SWHeaderPropertyShapeContext {
selection: SWHeaderPropertySelection
group: SWHeaderPropertyGroup
properties: readonly SWHeaderProperty[]
}
/**
* Shape area of a `layout: "circle"` group (Figma 26:11932, 208 x 208 inside the 260px shape group).
* undefined: the Figma circle drawing. null / false: the empty area (size, value labels and panel height
* are kept). A node or render function: host content (e.g. a shape viewer), fitted into the area.
*/
export type SWHeaderPropertyShapeContent = ReactNode | ((context: SWHeaderPropertyShapeContext) => ReactNode)
export interface SWHeaderPropertyPanelProps {
selection: SWHeaderPropertySelection
properties: readonly SWHeaderProperty[]
groups: readonly SWHeaderPropertyGroup[]
onPropertyChange?: (property: SWHeaderProperty, value: string) => void
expandedGroupIds?: readonly string[]
defaultExpandedGroupIds?: readonly string[]
onExpandedGroupsChange?: (ids: string[]) => void
toolActive?: boolean
onToolActiveChange?: (active: boolean) => void
toolLabel?: string
onOverviewClick?: () => void
/** Replaceable shape area; see SWHeaderPropertyShapeContent. Independent of the property values. */
shapeContent?: SWHeaderPropertyShapeContent
onClose?: () => void
id?: string
/** Supplied by SWHeader; standalone use can position the panel with style. */
anchorRef?: RefObject<HTMLElement | null>
style?: CSSProperties
}
const ordered = <T extends { order?: number }>(items: readonly T[]) => [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
/** Viewport-bound overlay position shared by the property tool window and K guide. No portal or dim. */
export function useSWHeaderOverlay(anchorRef: RefObject<HTMLElement | null> | undefined, width: number, offset = 0) {
const panelRef = useRef<HTMLDivElement>(null)
const [style, setStyle] = useState<CSSProperties>({ visibility: anchorRef ? "hidden" : undefined })
useLayoutEffect(() => {
if (!anchorRef) return
const update = () => {
const anchor = anchorRef.current
if (!anchor) return
const header = anchor.closest(".hm-sw-header")
const bounds = (header ?? document.documentElement).getBoundingClientRect()
const preferred = width === 314 ? anchor.querySelector<HTMLElement>('[data-property-panel-trigger]') : null
const more = width === 314 ? anchor.querySelector<HTMLElement>('.hm-sw-header__more-trigger') : null
const target = (preferred?.getClientRects().length ? preferred : more ?? anchor).getBoundingClientRect()
const viewportWidth = document.documentElement.clientWidth
const actualWidth = Math.min(width, viewportWidth - 8, bounds.width - 8)
const left = Math.max(Math.max(bounds.left, 0) + 4, Math.min(target.left + offset, Math.min(bounds.right, viewportWidth) - actualWidth - 4))
const top = Math.max(4, Math.min(target.bottom, window.innerHeight - (width === 208 ? 153 : 34)))
setStyle({ position: "fixed", left, top, width: actualWidth, maxHeight: Math.max(30, window.innerHeight - top - 4) })
}
update()
const observer = new ResizeObserver(update)
if (anchorRef.current) observer.observe(anchorRef.current.closest(".hm-sw-header") ?? anchorRef.current)
window.addEventListener("resize", update)
window.addEventListener("scroll", update, true)
return () => { observer.disconnect(); window.removeEventListener("resize", update); window.removeEventListener("scroll", update, true) }
}, [anchorRef, width, offset])
return { panelRef, style }
}
function PropertyEditor({ property, onChange }: { property: SWHeaderProperty; onChange?: SWHeaderPropertyPanelProps["onPropertyChange"] }) {
const inputId = useId()
const { type, value, disabled, mixed, readOnly, unit } = property
const [draft, setDraft] = useState(value)
useEffect(() => setDraft(value), [value, property.options, disabled, readOnly])
const pendingRef = useRef<string | null>(null)
useEffect(() => { pendingRef.current = null }, [value])
const editable = !disabled && !readOnly && type !== "computed" && (!!onChange || !!property.onCommit)
const parse = (draft: string) => {
if (property.validate && !property.validate(draft)) return null
if (type === "number" || type === "slider") return parseSWHeaderNumber(draft, { min: property.min, max: property.max })
if (type === "color") return parseSWHeaderColor(draft)?.value ?? null
if (type === "select") return property.options?.some(option => option.id === draft && !option.disabled) ? draft : null
return draft.trim()
}
const commit = (next: string) => {
if (!editable || next === value || next === pendingRef.current) return
if (parse(next) === null) { setDraft(value); return }
pendingRef.current = next
property.onCommit?.(next, property)
onChange?.(property, next)
}
const cancelDraft = () => setDraft(value)
const nativeKeys = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.nativeEvent.isComposing) return
if (event.key === "Enter") { event.preventDefault(); commit(draft) }
if (event.key === "Escape" && draft !== value) { event.stopPropagation(); cancelDraft() }
}
return (
<div className="hm-sw-property__row" data-property-id={property.id} data-type={type} data-disabled={disabled || undefined}>
<label htmlFor={inputId} title={property.name}>{property.name}</label>
<div className="hm-sw-property__value">
{type === "select" ? (
<select id={inputId} aria-label={property.name} value={mixed && draft === value ? "__mixed" : draft} disabled={!editable} onChange={event => setDraft(event.target.value)} onBlur={() => commit(draft)} onKeyDown={nativeKeys}>
{mixed && <option value="__mixed" disabled>다양함</option>}
{!mixed && !property.options?.some(option => option.id === value) && <option value={value}>{value}</option>}
{property.options?.map(option => <option key={option.id} value={option.id} disabled={option.disabled}>{option.name}</option>)}
</select>
) : (
<SWHeaderInput
id={inputId} aria-label={property.name} value={mixed ? "다양함" : value}
valueType={type === "number" || type === "slider" ? "number" : "text"}
disabled={disabled} readOnly={!editable} parse={parse} commitOnUnmount={false}
onCommit={editable ? commit : undefined}
/>
)}
{property.annotation && <span className="hm-sw-property__unit">{property.annotation}</span>}
{unit && type !== "slider" && <span className="hm-sw-property__unit">{unit}</span>}
{type === "color" && <span className="hm-sw-property__swatch" aria-hidden="true"><i style={{ backgroundColor: property.swatch ?? parseSWHeaderColor(value)?.hex ?? "transparent" }} /></span>}
{property.linePreview && <SWHeaderLinePreview pattern={property.linePattern} />}
{type === "slider" && <input type="range" aria-label={`${property.name} 슬라이더`} min={property.min ?? 0} max={property.max ?? 100} step={property.step ?? 1} value={mixed ? property.min ?? 0 : draft} disabled={!editable} onChange={event => setDraft(event.target.value)} onBlur={() => commit(draft)} onKeyDown={nativeKeys} />}
{unit && type === "slider" && <span className="hm-sw-property__unit">{unit}</span>}
</div>
</div>
)
}
/** A single data-driven, non-modal property tool window (Figma 26:11822). */
export function SWHeaderPropertyPanel({
selection, properties, groups, onPropertyChange, expandedGroupIds, defaultExpandedGroupIds = [],
onExpandedGroupsChange, toolActive: controlledTool, onToolActiveChange, toolLabel = "객체 선택 도구",
onOverviewClick, shapeContent, onClose, id, anchorRef, style: customStyle,
}: SWHeaderPropertyPanelProps) {
const titleId = useId()
const [expanded, setExpanded] = useState<string[]>([...defaultExpandedGroupIds])
const [internalTool, setInternalTool] = useState(false)
const active = controlledTool ?? internalTool
const expandedIds = expandedGroupIds ?? expanded
const { panelRef, style } = useSWHeaderOverlay(anchorRef, 314)
useLayoutEffect(() => { if (anchorRef) panelRef.current?.focus() }, [anchorRef, panelRef])
useEffect(() => {
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape" && !event.defaultPrevented && !event.isComposing) onClose?.()
}
document.addEventListener("keydown", onKey)
return () => document.removeEventListener("keydown", onKey)
}, [onClose])
const toggleGroup = (groupId: string) => {
const next = expandedIds.includes(groupId) ? expandedIds.filter(id => id !== groupId) : [...expandedIds, groupId]
if (expandedGroupIds === undefined) setExpanded(next)
onExpandedGroupsChange?.(next)
}
return (
<div ref={panelRef} id={id} tabIndex={-1} className="hm-sw-property" data-selection-kind={selection.kind} role="dialog" aria-modal="false" aria-labelledby={titleId} style={{ ...style, ...customStyle }} onKeyDown={event => {
if (event.key === "Escape" && !event.nativeEvent.isComposing) { event.stopPropagation(); onClose?.() }
}}>
<div className="hm-sw-property__titlebar">
<img className="hm-sw-property__title-art" src={selection.kind === "single" ? singleTitleGraphic : titleGraphic} alt="" />
<span id={titleId} className="hm-sw-property__title" title={selection.title}>{selection.title}</span>
{selection.kind !== "single" && <button type="button" className="hm-sw-property__tool" aria-label={toolLabel} title={toolLabel} aria-pressed={active} onClick={() => {
if (controlledTool === undefined) setInternalTool(!active)
onToolActiveChange?.(!active)
}}>
<img className={active ? "hm-sw-property__tool-active" : "hm-sw-property__tool-default"} src={active ? toolActiveGraphic : toolDefault} alt="" />
{!active && <img className="hm-sw-property__tool-hover" src={toolHover} alt="" />}
</button>}
<button type="button" className="hm-sw-property__close" aria-label="속성창 닫기" onClick={onClose}>
<img className="hm-sw-property__close-default" src={closeDefault} alt="" />
<img className="hm-sw-property__close-hover" src={closeHover} alt="" />
</button>
</div>
<div className="hm-sw-property__body" key={selection.id}>
{ordered(groups).map(group => {
const items = ordered(properties.filter(property => property.groupId === group.id))
if (!items.length || (selection.kind === "none" && group.layout && group.layout !== "rows")) return null
const isExpanded = expandedIds.includes(group.id)
const expandable = group.collapsedCount !== undefined && items.length > group.collapsedCount
const visible = expandable && !isExpanded ? items.slice(0, group.collapsedCount) : items
return (
<section key={group.id} className={`hm-sw-property__group hm-sw-property__group--${group.layout ?? "rows"}`} aria-label={group.name ?? "공통 속성"}>
{group.name && <h3>{group.name}</h3>}
<div className="hm-sw-property__group-content">
{group.layout === "circle" && (() => {
const content = typeof shapeContent === "function" ? shapeContent({ selection, group, properties: items }) : shapeContent
const fallback = shapeContent === undefined
return <div className="hm-sw-property__shape" data-shape-slot={fallback ? "default" : content === null || content === false ? "empty" : "custom"}>
{fallback ? <img className="hm-sw-property__circle" src={circleGraphic} alt="원형 도형의 지름과 반지름 안내" /> : content}
</div>
})()}
{group.layout === "position" && <div className="hm-sw-property__position-head"><span>구분</span>{visible.map(item => <span key={item.id}>{item.name}</span>)}</div>}
<div className="hm-sw-property__editors">
{group.layout === "position" && <span className="hm-sw-property__position-label">{group.positionLabel}</span>}
{visible.map(property => <div key={property.id} className="hm-sw-property__editor" data-slot={property.graphicSlot}><PropertyEditor property={property} onChange={onPropertyChange} /></div>)}
</div>
{expandable && <button type="button" className="hm-sw-property__expand" aria-label={`${group.name ?? "공통 속성"} ${isExpanded ? "접기" : "펼치기"}`} aria-expanded={isExpanded} onClick={() => toggleGroup(group.id)}><img src={expandGraphic} alt="" /></button>}
</div>
</section>
)
})}
{selection.kind === "all" && !!selection.counts?.length && <div className="hm-sw-property__counts" aria-label="선택된 객체 종류와 개수">{selection.counts.map(item => <span key={item.id} style={{ gridColumn: item.column, gridRow: item.row }}>{item.name} {item.count}</span>)}</div>}
</div>
{selection.overviewTitle && <button type="button" className="hm-sw-property__overview" onClick={onOverviewClick} disabled={!onOverviewClick}><img src={footerGraphic} alt="" /><span>{selection.overviewTitle}</span></button>}
</div>
)
}
@@ -0,0 +1,150 @@
import type { ReactNode, Ref } from "react"
import dimensionRowIcon from "../../../assets/menu-dimension-row.svg"
import dimensionRowIconHover from "../../../assets/menu-dimension-row-hover.svg"
import type {
SWHeaderDimensionStyleOption, SWHeaderDimensionStyleValue, SWHeaderTextStyleOption, SWHeaderTextStyleValue,
} from "../types"
import { SWHeaderMenuDetail, useSWHeaderDropdown, type SWHeaderDropdownOptions } from "./SWHeaderDropdown"
import { SWHeaderDimensionStyle, SWHeaderTextStyle } from "./SWHeaderFields"
interface ListMenuBase extends Pick<SWHeaderDropdownOptions, "open" | "onOpenChange" | "disabled"> {
/** Called on every click of the field (also when the dropdown toggles). */
onClick?: () => void
/** Keeps the field in its selected (#FFC600) look while closed. */
label?: string
selected?: boolean
/** Optional footer host action. All detail buttons remain hoverable without a host callback. */
onDetail?: () => void
}
interface StyleListMenuProps<T extends { id: string; name: string; disabled?: boolean }> extends ListMenuBase {
variant: "text-style" | "dimension-style"
label: string
items: T[]
current?: T
onSelect?: (item: T) => void
/** Field button (receives the dropdown wiring). */
field: (wiring: { buttonRef: Ref<HTMLButtonElement>; expanded: boolean; controls?: string; selected: boolean; disabled: boolean; onClick: () => void }) => ReactNode
/** Row content before the name. */
preview: (item: T) => ReactNode
}
/**
* Single-list style dropdown (text / dimension style): 164px rows of 30px with their Figma hover
* (#62533D, #FF5C00), a 40px footer with the "상세" pill. Choosing a row reports it and closes the list.
*/
function StyleListMenu<T extends { id: string; name: string; disabled?: boolean }>({
variant, label, items, current, onSelect, field, preview, open: openProp, onOpenChange, onClick, selected = false, disabled = false, onDetail,
}: StyleListMenuProps<T>) {
const { open, setOpen, close, panelRef, triggerRef, menuId, panelStyle, shifted, rootProps } = useSWHeaderDropdown({ open: openProp, onOpenChange, disabled })
return (
<div {...rootProps} className={`hm-sw-header__menu hm-sw-header__menu--${variant}`} data-menu={variant} data-tier="aux">
{field({
buttonRef: triggerRef,
expanded: open,
controls: open ? menuId : undefined,
selected: selected || open,
disabled,
onClick: () => {
onClick?.()
setOpen(!open)
},
})}
{open && (
<div
ref={panelRef}
id={menuId}
className={`hm-sw-header__menu-panel hm-sw-header__list-menu hm-sw-header__list-menu--${variant}`}
role="group"
aria-label={label}
data-shifted={shifted ? "true" : undefined}
style={panelStyle}
>
{items.length > 0 && (
<ul className="hm-sw-header__menu-list">
{items.map(item => (
<li key={item.id} className="hm-sw-header__menu-row hm-sw-header__menu-row--style" data-active={item === current ? "true" : undefined} data-disabled={item.disabled ? "true" : undefined}>
<button
type="button"
className="hm-sw-header__menu-item"
title={item.name}
aria-current={item === current ? "true" : undefined}
disabled={item.disabled}
onClick={() => {
onSelect?.(item)
close(true)
}}
>
{preview(item)}
<span className="hm-sw-header__menu-text hm-sw-header__menu-name">{item.name}</span>
</button>
</li>
))}
</ul>
)}
<div className="hm-sw-header__menu-footer">
<SWHeaderMenuDetail label={`${label} 상세`} onClick={onDetail && (() => { onDetail(); close(true) })} />
</div>
</div>
)}
</div>
)
}
export interface SWHeaderTextStyleMenuProps extends ListMenuBase {
value: SWHeaderTextStyleValue
/** Rows (Figma: Arial / Standard, 돋움 / Dotum). Host data. */
textStyles: SWHeaderTextStyleOption[]
/** Current style; defaults to the row named `value.name`. */
activeTextStyleId?: string
onTextStyleSelect?: (style: SWHeaderTextStyleOption) => void
}
/** Text style field with its dropdown (Figma header/menu/문자스타일 26:11181): 16px font sample + style name. */
export function SWHeaderTextStyleMenu({ value, label = "문자스타일", textStyles, activeTextStyleId, onTextStyleSelect, ...rest }: SWHeaderTextStyleMenuProps) {
const current = textStyles.find(item => (activeTextStyleId === undefined ? item.name === value.name : item.id === activeTextStyleId))
return (
<StyleListMenu
{...rest}
variant="text-style"
label={label}
items={textStyles}
current={current}
onSelect={onTextStyleSelect}
field={wiring => <SWHeaderTextStyle label={label} value={value} {...wiring} />}
preview={item => <span className="hm-sw-header__menu-text hm-sw-header__menu-font" title={item.font}>{item.font}</span>}
/>
)
}
export interface SWHeaderDimensionStyleMenuProps extends ListMenuBase {
value: SWHeaderDimensionStyleValue
/** Rows (Figma: Standard, ISO - 25). Host data. */
dimensionStyles: SWHeaderDimensionStyleOption[]
/** Current style; defaults to the row named `value.name`. */
activeDimensionStyleId?: string
onDimensionStyleSelect?: (style: SWHeaderDimensionStyleOption) => void
}
/** Dimension style field with its dropdown (Figma header/menu/치수스타일 26:11215): dimension sample + style name. */
export function SWHeaderDimensionStyleMenu({ value, label = "치수스타일", dimensionStyles, activeDimensionStyleId, onDimensionStyleSelect, ...rest }: SWHeaderDimensionStyleMenuProps) {
const current = dimensionStyles.find(item => (activeDimensionStyleId === undefined ? item.name === value.name : item.id === activeDimensionStyleId))
return (
<StyleListMenu
{...rest}
variant="dimension-style"
label={label}
items={dimensionStyles}
current={current}
onSelect={onDimensionStyleSelect}
field={wiring => <SWHeaderDimensionStyle label={label} value={value} {...wiring} />}
preview={() => (
// Figma row asset (#F6F4F2) and its hover variant (#FF5C00): only the foreground fill differs.
<span className="hm-sw-header__menu-dimension" aria-hidden="true">
<img className="hm-sw-header__menu-dimension-default" src={dimensionRowIcon} alt="" />
<img className="hm-sw-header__menu-dimension-hover" src={dimensionRowIconHover} alt="" />
</span>
)}
/>
)
}
@@ -0,0 +1,43 @@
import type { PointerEventHandler } from "react"
import windowMotion from "../../../assets/window-motion.svg"
import windowMotionWindow from "../../../assets/window-motion-window.svg"
import windowMotionCursor from "../../../assets/window-motion-cursor.svg"
import closeIcon from "../../../assets/close.svg"
import closeIconHover from "../../../assets/close-hover.svg"
export interface SWHeaderWindowControlsProps {
onWindowMoveStart?: PointerEventHandler<HTMLDivElement>
onMinimize?: () => void
onClose?: () => void
/** Individual parts (default true). Removed parts take no space. */
showWindowMove?: boolean
showMinimize?: boolean
showClose?: boolean
}
/**
* Window-move area plus minimize and close. Window behavior is owned by the host.
* The move area plays the Figma "창이동 motion" hover (26:10220) in CSS only; its graphics and
* tooltip ignore the pointer, so pointer down always reaches the area.
*/
export function SWHeaderWindowControls({ onWindowMoveStart, onMinimize, onClose, showWindowMove = true, showMinimize = true, showClose = true }: SWHeaderWindowControlsProps) {
return (
<>
{showWindowMove && <div className="hm-sw-header__motion" data-sw-window-move="" role="presentation" onPointerDown={onWindowMoveStart}>
<img className="hm-sw-header__motion-icon" src={windowMotion} alt="" aria-hidden="true" />
<span className="hm-sw-header__motion-window" aria-hidden="true"><img src={windowMotionWindow} alt="" /></span>
<span className="hm-sw-header__motion-cursor" aria-hidden="true"><img src={windowMotionCursor} alt="" /></span>
<span className="hm-sw-header__motion-tooltip" role="tooltip">창 이동</span>
</div>}
{(showMinimize || showClose) && <div className="hm-sw-header__window-buttons" data-partial={showMinimize && showClose ? undefined : ""}>
{showMinimize && <button type="button" className="hm-sw-header__window-button hm-sw-header__window-button--minimize" aria-label="최소화" onClick={onMinimize}>
<span aria-hidden="true" />
</button>}
{showClose && <button type="button" className="hm-sw-header__window-button hm-sw-header__window-button--close" aria-label="종료" onClick={onClose}>
<img className="hm-sw-header__icon" src={closeIcon} alt="" aria-hidden="true" />
<img className="hm-sw-header__icon-hover" src={closeIconHover} alt="" aria-hidden="true" />
</button>}
</div>}
</>
)
}
@@ -0,0 +1,33 @@
export { SWHeader } from "./SWHeader"
export { SWHeaderHome, type SWHeaderHomeProps } from "./components/SWHeaderHome"
export { SWHeaderDocumentTitle, type SWHeaderDocumentTitleProps } from "./components/SWHeaderDocumentTitle"
export {
SWHeaderLayerControl, SWHeaderColorControl, SWHeaderLineStyle,
SWHeaderTextStyle, SWHeaderDimensionStyle, type SWHeaderLayerControlProps,
} from "./components/SWHeaderFields"
export { SWHeaderLayerMenu, type SWHeaderLayerMenuProps } from "./components/SWHeaderLayerMenu"
export { SWHeaderColorMenu, type SWHeaderColorMenuProps } from "./components/SWHeaderColorMenu"
export {
SWHeaderLineStyleMenu, SWHeaderLinePreview, swHeaderLinePatterns, type SWHeaderLineStyleMenuProps,
} from "./components/SWHeaderLineStyleMenu"
export {
SWHeaderTextStyleMenu, SWHeaderDimensionStyleMenu,
type SWHeaderTextStyleMenuProps, type SWHeaderDimensionStyleMenuProps,
} from "./components/SWHeaderStyleListMenus"
export {
SWHeaderInput, parseSWHeaderColor, parseSWHeaderNumber,
type SWHeaderInputProps, type SWHeaderInputValueType,
} from "./components/SWHeaderInput"
export { SWHeaderProperties, type SWHeaderPropertiesProps } from "./components/SWHeaderProperties"
export { SWHeaderPropertyPanel, type SWHeaderPropertyPanelProps, type SWHeaderProperty, type SWHeaderPropertyType, type SWHeaderPropertyGroup, type SWHeaderPropertySelection, type SWHeaderPropertyShapeContent, type SWHeaderPropertyShapeContext } from "./components/SWHeaderPropertyPanel"
export { SWHeaderActions, type SWHeaderActionsProps } from "./components/SWHeaderActions"
export { SWHeaderWindowControls, type SWHeaderWindowControlsProps } from "./components/SWHeaderWindowControls"
export type {
SWHeaderProps, SWHeaderMenuId, SWHeaderLayerValue, SWHeaderLayer, SWHeaderColorValue,
SWHeaderLineStyleValue, SWHeaderTextStyleValue, SWHeaderDimensionStyleValue, SWHeaderDocument,
SWHeaderColorMode, SWHeaderColorOption, SWHeaderColorInputValue, SWHeaderLineType, SWHeaderLineWeight,
SWHeaderTextStyleOption, SWHeaderDimensionStyleOption,
} from "./types"
export { SWHeaderPropertyBar, type SWHeaderPropertyBarProps } from "./components/SWHeaderPropertyBar"
export type { SWHeaderControlId, SWHeaderControlConfig, SWHeaderFrameControlId, SWHeaderHideableControlId } from "./types"
@@ -0,0 +1,329 @@
import type { HTMLAttributes, PointerEventHandler, ReactNode } from "react"
import type { SWHeaderPropertyBarProps } from "./components/SWHeaderPropertyBar"
import type { SWHeaderPropertyPanelProps } from "./components/SWHeaderPropertyPanel"
/** Header controls that can be shown as selected (text and arrow turn #FFC600). */
export type SWHeaderMenuId = "layer" | "color" | "lineStyle" | "textStyle" | "dimensionStyle"
export interface SWHeaderLayerValue {
/** Current layer name. */
name: string
}
/**
* One row of the layer dropdown (Figma header/menu/레이어 26:10940). The host program owns the data:
* the toggles only report the next value through callbacks; the header never changes a layer itself.
*/
export interface SWHeaderLayer {
/** Stable key, returned with every callback. */
id: string
/** Layer name (ellipsis when long). */
name: string
/** CSS color of the 16 x 16 swatch. Defaults to `#ffffff`. */
color?: string
/** Shown: colored bulb when true, gray when false. A click reports `!visible`. Default `true`. */
visible?: boolean
/** Frozen: gray snowflake when true, colored (thawed) when false. A click reports `!frozen`. Default `false`. */
frozen?: boolean
/** Locked: gray lock when true, colored (unlocked) when false. A click reports `!locked`. Default `false`. */
locked?: boolean
/** Disables selecting the row and its three toggles. */
disabled?: boolean
}
export interface SWHeaderColorValue {
/** Display text, e.g. `255,229,0`. */
value: string
/** CSS color of the swatch. Defaults to `#ffe500`. */
swatch?: string
}
export interface SWHeaderLineStyleValue {
name: string
/** Line weight text, e.g. `0.25`. */
weight: string
/** Line scale text, e.g. `1.00`. */
scale: string
}
export interface SWHeaderTextStyleValue {
/** Font name shown at the left of the field, e.g. `Arial`. */
font: string
/** Text style name, e.g. `Standard`. */
name: string
}
/** One entry of the document dropdown (no file type or product assumed). */
export interface SWHeaderDocument {
/** Stable key, returned with the selection. */
id: string
/** Display name (ellipsis when long). */
name: string
/** Tooltip; defaults to `name` (e.g. a full path). */
title?: string
/** Icon in the 29 x 12 slot; defaults to `documentIcon` or the Figma document icon. */
icon?: ReactNode
disabled?: boolean
}
export interface SWHeaderDimensionStyleValue {
name: string
}
/** ByLayer / ByBlock pair of the color dropdown (Figma ByLayer 26:12666, ByBlock 26:12659). */
export type SWHeaderColorMode = "byLayer" | "byBlock"
/** One 22px chip of the color dropdown (Figma color chip 22px 26:11091). The host owns the palette. */
export interface SWHeaderColorOption {
/** Stable key, returned with the selection. */
id: string
/** CSS color of the chip. */
color: string
/** Value text reported with the selection, e.g. `255,0,0`. Defaults to the RGB text of `color`. */
value?: string
/** Accessible name, e.g. `빨강`. Defaults to `value`. */
label?: string
disabled?: boolean
}
/** A color typed into the color dropdown input, validated before it is reported. */
export interface SWHeaderColorInputValue {
/** Normalized `r,g,b` text, e.g. `255,229,0`. */
value: string
rgb: [number, number, number]
/** `#rrggbb`. */
hex: string
}
/**
* One row of the line type column (Figma header/dropdown/brown/row 26:10702). `pattern` lists the dash
* lengths of the 70px preview (4px gaps, scaled to fill 70px); omit it for a solid line.
* `swHeaderLinePatterns` holds the Figma CENTER / HIDDEN patterns.
*/
export interface SWHeaderLineType {
id: string
name: string
pattern?: readonly number[]
disabled?: boolean
}
/** One row of the line weight column. `value` is shown at the right (ByDefault); `editable` shows it in an input (User). */
export interface SWHeaderLineWeight {
id: string
name: string
value?: string
editable?: boolean
disabled?: boolean
}
/** One row of the text style dropdown (font sample 16px + style name). */
export interface SWHeaderTextStyleOption {
id: string
/** Style name, e.g. `Standard`. */
name: string
/** Font name shown at the left, e.g. `Arial`. */
font: string
disabled?: boolean
}
/** One row of the dimension style dropdown (dimension sample + style name). */
export interface SWHeaderDimensionStyleOption {
id: string
name: string
disabled?: boolean
}
/** Optional complete allowlist. Omitted => approved default layout; [] => no optional controls.
* Array order controls rendered order. Stable ids preserve data and component state on reordering. */
export type SWHeaderControlId = "document" | SWHeaderMenuId | "drawingMode" | "details" | "propertyBar" | "propertyPanel" | "print" | "popupGuide"
/** Common frame parts outside the object area. Their position is fixed; each can be removed. */
export type SWHeaderFrameControlId = "home" | "windowMove" | "minimize" | "close"
/** Every Header part except the logo, which is always shown. */
export type SWHeaderHideableControlId = SWHeaderControlId | SWHeaderFrameControlId
export interface SWHeaderControlConfig {
id: SWHeaderControlId
visible?: boolean
label?: string
}
export interface SWHeaderProps extends Omit<HTMLAttributes<HTMLElement>, "title" | "children" | "color"> {
/** File name shown in the document area (ellipsis when narrow). */
documentTitle: string
controls?: readonly SWHeaderControlConfig[]
/**
* Parts to remove, in either layout (default design or `controls`). Works without passing a full
* `controls` list, so the default design and order of the remaining parts are kept. Removing
* "home" keeps the logo as a static image. Unknown ids are ignored. Default: none.
*/
hiddenControls?: readonly SWHeaderHideableControlId[]
/**
* Order of the similar menus (layer / color / lineStyle / textStyle / dimensionStyle). They swap
* places only among their own slots; other parts never move. Listed ids come first (unknown and
* duplicate ids are ignored), missing ids follow in the default order. Omitted: approved default
* order (or the `controls` order). Order never changes values or calls a callback.
*/
menuOrder?: readonly SWHeaderMenuId[]
/**
* Documents of the dropdown under the file name (Figma 26:10662). When omitted the file name
* stays a plain button (no dropdown); an empty array shows only New / Open.
*/
documents?: SWHeaderDocument[]
/** Controlled open state of the document dropdown; uncontrolled when undefined. */
documentMenuOpen?: boolean
onDocumentMenuOpenChange?: (open: boolean) => void
/** A document row was chosen. Opening it is up to the host. */
onDocumentSelect?: (document: SWHeaderDocument) => void
/** Current document id: its name is #FFC600 in the dropdown. The list keeps the `documents` order. */
activeDocumentId?: string
/**
* Close Document: an X ("문서 닫기") on every document row, shown on row hover or keyboard focus.
* The host closes that document and updates `documents` / `activeDocumentId`; the header does
* not choose the next document.
*/
onDocumentClose?: (document: SWHeaderDocument) => void
/** "+ New" in the dropdown footer. */
onDocumentNew?: () => void
/** "Open" in the dropdown footer. */
onDocumentOpen?: () => void
/** Default icon for documents without `icon`. Defaults to the Figma document icon. */
documentIcon?: ReactNode
/** Footer labels. Default `+ New` / `Open` (Figma). */
documentNewLabel?: string
documentOpenLabel?: string
/** Label of the drawing-mode area. Defaults to `그리기모드`. */
drawingModeLabel?: string
/** Current layer shown in the field (used when no `layers` entry matches `activeLayerId`). */
layer?: SWHeaderLayerValue
/**
* Layers of the dropdown under the layer field (Figma 26:10940). When omitted the field stays a
* plain button without a dropdown (the previous behavior); an empty array shows only the footer.
*/
layers?: SWHeaderLayer[]
/** Current layer id: shown in the field (name/state icons); its row retains the hover emphasis. */
activeLayerId?: string
/** Controlled open state of the layer dropdown; uncontrolled when undefined. */
layerMenuOpen?: boolean
onLayerMenuOpenChange?: (open: boolean) => void
/** A layer row (swatch / name) was chosen. The dropdown closes. */
onLayerSelect?: (layer: SWHeaderLayer) => void
/** Bulb toggle. Without it the bulb is a status icon only. The dropdown stays open. */
onLayerVisibilityChange?: (layer: SWHeaderLayer, nextVisible: boolean) => void
/** Snowflake toggle. Without it the snowflake is a status icon only. The dropdown stays open. */
onLayerFreezeChange?: (layer: SWHeaderLayer, nextFrozen: boolean) => void
/** Lock toggle. Without it the lock is a status icon only. The dropdown stays open. */
onLayerLockChange?: (layer: SWHeaderLayer, nextLocked: boolean) => void
/** "레이어 상세" in the dropdown footer. Optional host action; hover remains enabled without it. */
onLayerDetail?: () => void
color?: SWHeaderColorValue
lineStyle?: SWHeaderLineStyleValue
textStyle?: SWHeaderTextStyleValue
dimensionStyle?: SWHeaderDimensionStyleValue
/**
* Basic palette of the color dropdown (Figma header/menu/색상 26:10986). When omitted the color
* field stays a plain button (the previous behavior). The header never changes the color itself:
* every control only reports through its callback and the host updates `color` / `colorMode`.
*/
colors?: SWHeaderColorOption[]
/** User colors, shown in extra 24px rows below the palette (Figma 26:11036). Any count. */
userColors?: SWHeaderColorOption[]
/** Color mode. null still means explicit color; the required indicator retains the last mode (default ByLayer). */
colorMode?: SWHeaderColorMode | null
/** Controlled open state of the color dropdown; uncontrolled when undefined. */
colorMenuOpen?: boolean
onColorMenuOpenChange?: (open: boolean) => void
/** A palette or user color chip was clicked. The dropdown stays open. */
onColorSelect?: (option: SWHeaderColorOption) => void
/** ByLayer / ByBlock was clicked. The dropdown stays open. */
onColorModeChange?: (mode: SWHeaderColorMode) => void
/** A valid `r,g,b` (0-255) or `#rrggbb` was committed in the color input (Enter or leaving the input). */
onColorInput?: (value: SWHeaderColorInputValue) => void
/** Color pick button (Figma btn_colorize). Picking from the drawing is the host's job; the dropdown closes. */
onColorPick?: () => void
/** "색상 상세". Without it the button keeps its Figma look, disabled. The detail popup is not part of the header. */
onColorDetail?: () => void
/** Line type column of the line style dropdown (Figma header/menu/선스타일 26:11104). When omitted (and no `lineWeights`) the field stays a plain button. */
lineTypes?: SWHeaderLineType[]
/** Line weight column (ByLayer / ByBlock / ByDefault / User ...). */
lineWeights?: SWHeaderLineWeight[]
/** Current line type; defaults to the row named `lineStyle.name`. */
activeLineTypeId?: string
/** Current line weight; defaults to the row named `lineStyle.weight`. */
activeLineWeightId?: string
lineStyleMenuOpen?: boolean
onLineStyleMenuOpenChange?: (open: boolean) => void
/** A line type row was clicked. Weight and scale are not touched. The dropdown stays open. */
onLineTypeSelect?: (lineType: SWHeaderLineType) => void
/** A line weight row was clicked. Line type and scale are not touched. The dropdown stays open. */
onLineWeightSelect?: (weight: SWHeaderLineWeight) => void
/** A valid non-negative number was committed in an editable weight row (User). */
onLineWeightValueChange?: (weight: SWHeaderLineWeight, value: string) => void
/** A valid positive number was committed in the scale input. */
onLineScaleChange?: (value: string) => void
/** "선스타일 상세". Without it the button keeps its Figma look, disabled. */
onLineStyleDetail?: () => void
/** Rows of the text style dropdown (Figma header/menu/문자스타일 26:11181). When omitted the field stays a plain button. */
textStyles?: SWHeaderTextStyleOption[]
/** Current text style; defaults to the row named `textStyle.name`. */
activeTextStyleId?: string
textStyleMenuOpen?: boolean
onTextStyleMenuOpenChange?: (open: boolean) => void
/** A text style row was chosen. The dropdown closes. */
onTextStyleSelect?: (style: SWHeaderTextStyleOption) => void
/** "문자스타일 상세". Without it the button keeps its Figma look, disabled. */
onTextStyleDetail?: () => void
/** Rows of the dimension style dropdown (Figma header/menu/치수스타일 26:11215). When omitted the field stays a plain button. */
dimensionStyles?: SWHeaderDimensionStyleOption[]
/** Current dimension style; defaults to the row named `dimensionStyle.name`. */
activeDimensionStyleId?: string
dimensionStyleMenuOpen?: boolean
onDimensionStyleMenuOpenChange?: (open: boolean) => void
/** A dimension style row was chosen. The dropdown closes. */
onDimensionStyleSelect?: (style: SWHeaderDimensionStyleOption) => void
/** "치수스타일 상세". Optional host action; hover remains enabled without it. */
onDimensionStyleDetail?: () => void
/** Which menu is selected (open) in the host program. */
activeMenu?: SWHeaderMenuId | null
propertyBarOpen?: boolean
defaultPropertyBarOpen?: boolean
propertyBar?: SWHeaderPropertyBarProps
onPropertyBarOpenChange?: (open: boolean) => void
propertyPanelOpen?: boolean
/** Additive property tool window configuration. Values and object changes belong to the host. */
propertyPanel?: Omit<SWHeaderPropertyPanelProps, "onClose" | "anchorRef" | "id" | "style">
/** Open/close notification for the tool window. Existing onPropertyPanelToggle is preserved. */
onPropertyPanelOpenChange?: (open: boolean) => void
/** Disables the menu / property / action groups (window controls stay enabled). */
disabled?: boolean
/** Extra tools rendered inside the object area, after the menus. */
tools?: ReactNode
/**
* 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.
*/
logoSrc?: string
/** Logo element (inline SVG, <img>, ...). Takes precedence over `logoSrc`. */
logo?: ReactNode
/** Accessible product name; the home button is announced as "<logoAlt> 홈" (default "홈"). */
logoAlt?: string
onHomeClick?: () => void
onDocumentClick?: () => void
onLayerOpen?: () => void
onColorOpen?: () => void
onLineStyleOpen?: () => void
onTextStyleOpen?: () => void
onDimensionStyleOpen?: () => void
onPropertyBarToggle?: () => void
onPropertyPanelToggle?: () => void
onPrint?: () => void
onPopupGuide?: () => void
/** Pointer down on the window-move area (the host decides how to move the window). */
onWindowMoveStart?: PointerEventHandler<HTMLDivElement>
onMinimize?: () => void
onClose?: () => void
}
@@ -0,0 +1,11 @@
import { forwardRef, type HTMLAttributes } from "react"
export type SWThemeProps = HTMLAttributes<HTMLDivElement>
/**
* Scope for SW sub-components used outside <SWHeader>: provides the SW tokens,
* typography and control reset. Override any `--hm-sw-header-*` token through `style`.
*/
export const SWTheme = forwardRef<HTMLDivElement, SWThemeProps>(function SWTheme({ className, ...rest }, ref) {
return <div ref={ref} className={`hm-sw-theme${className ? ` ${className}` : ""}`} {...rest} />
})
+4
View File
@@ -0,0 +1,4 @@
import "./theme/sw.css"
export * from "./components/sw-header"
export { SWTheme, type SWThemeProps } from "./components/sw-theme/SWTheme"
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,164 @@
import { createRequire } from 'node:module'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import http from 'node:http'
// Usage: node packages/sw/tests/detail-buttons.browser.mjs baseline|verify <evidence-dir> [base-url]
const mode = process.argv[2] ?? 'verify'
const output = path.resolve(process.argv[3] ?? 'docs/sw/detail-button-qa')
let base = process.argv[4] ?? 'static'
let server
if (base === 'static') {
const root=path.resolve('storybook-static'),mime={'.html':'text/html','.js':'text/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.ttf':'font/ttf'}
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));base=`http://127.0.0.1:${server.address().port}`
}
const { chromium } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
await mkdir(output, { recursive: true })
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 = { mode, browser: browser.version(), checks: [], errors: [], styles: {} }
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 }); if (!ok) console.error('FAIL', name, data) }
const goto = async id => {
await page.goto(`${base}/iframe.html?id=sw-header--${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)
}
const style = e => {
const s = getComputedStyle(e), t = e.querySelector('[data-detail-label],span:last-child'), i = e.querySelector('img')
return { background:s.backgroundColor,color:s.color,opacity:s.opacity,border:s.border, radius:s.borderRadius,shadow:s.boxShadow,
textColor:getComputedStyle(t).color,textOpacity:getComputedStyle(t).opacity,textStroke:getComputedStyle(t).webkitTextStrokeWidth,
iconOpacity:i && getComputedStyle(i).opacity,iconContent:i && getComputedStyle(i).content,
height:e.getBoundingClientRect().height,width:e.getBoundingClientRect().width,outline:s.outline,disabled:e.disabled }
}
const snapshots = {}
try {
if (mode === 'capture') {
await goto('detail-buttons')
await page.screenshot({path:path.join(output,'comparison-default.png'),fullPage:true})
await page.locator('[data-detail-row="문자스타일 상세"] [data-detail-case="hover"] button').hover()
await page.screenshot({path:path.join(output,'comparison-pointer-hover.png'),fullPage:true})
} else if (mode === 'preservation') {
await goto('controls')
for(const width of [390,768,1280,1440]){
await page.setViewportSize({width,height:1000})
const layout=await page.locator('header.hm-sw-header').first().evaluate(h=>({height:h.getBoundingClientRect().height,overflow:h.scrollWidth-h.clientWidth,documentOverflow:document.documentElement.scrollWidth-document.documentElement.clientWidth}))
check(`required responsive width ${width}`,layout.height===49&&layout.overflow===0&&layout.documentOverflow===0,layout)
}
await page.setViewportSize({width:1920,height:1000})
for(const [id,height] of [['none',256],['all',361],['circle',658],['expanded',754]]){
await goto(`property-panel-${id}`)
const box=await page.locator('.hm-sw-property').first().boundingBox()
check(`preserve property panel ${id}`,box.width===314&&box.height===height,box)
}
await goto('controls')
const motion=page.locator('[data-sw-window-move]').first();await motion.hover()
check('preserve window motion 1400ms',await motion.evaluate(e=>e.getAnimations({subtree:true}).length===3&&e.getAnimations({subtree:true}).every(a=>a.effect.getTiming().duration===1400)))
await page.mouse.move(1,900);check('window motion leave clears animations',await motion.evaluate(e=>e.getAnimations({subtree:true}).length===0))
const guide=page.locator('.hm-sw-k-guide').first();await guide.locator('button').hover()
check('preserve K guide animation',await guide.locator('.hm-sw-k-guide__panel').isVisible()&&await guide.locator('.hm-sw-k-guide__panel').evaluate(e=>getComputedStyle(e).animationDuration==='0.511s'))
await page.mouse.move(1,900);check('K guide leave closes',await guide.locator('.hm-sw-k-guide__panel').count()===0)
await goto('colorize');const colorHeader=page.locator('header.hm-sw-header').first();await colorHeader.getByRole('button',{name:'색상',exact:true}).click()
check('preserve Colorize size',(await colorHeader.getByRole('button',{name:'색상 추출',exact:true}).boundingBox()).width===24)
await goto('layer-detail-input');const editing=page.getByRole('textbox',{name:'Editing',exact:true})
check('preserve editable Input',await editing.isEditable()&&await editing.evaluate(e=>getComputedStyle(e).caretColor==='rgb(230, 4, 3)'))
} else {
const index = await (await fetch(`${base}/index.json`)).json()
const ids = Object.values(index.entries).filter(e => e.title === 'SW/Header' && e.type === 'story').map(e => e.id)
report.storyIds = ids
if (mode === 'baseline') await writeFile(path.join(output, 'baseline-story-ids.json'), JSON.stringify(ids))
for (const story of ['default', 'controls']) {
await goto(story)
for (const width of [1920,1800,1440,1180,1080,900,768,640,520,390]) {
await page.setViewportSize({ width, height:1000 })
snapshots[`${story}-${width}`] = await page.locator('header.hm-sw-header').first().evaluate(h => [h,...h.querySelectorAll('*')].map(e => {
const b=e.getBoundingClientRect(),s=getComputedStyle(e)
return {tag:e.tagName,class:e.getAttribute('class'),x:b.x,y:b.y,width:b.width,height:b.height,display:s.display,visibility:s.visibility,color:s.color,background:s.backgroundColor}
}))
}
}
await page.setViewportSize({ width:1920,height:1000 })
await goto('text-dimension-style-dropdown')
const original = page.getByRole('button',{name:'문자스타일 상세',exact:true}).first()
snapshots.textDefault = await original.evaluate(style)
if (!(await original.isDisabled())) { await original.hover(); snapshots.textHover = await original.evaluate(style) }
else {
// Existing callback-enabled example supplies the effective text-style Hover reference.
const enabled = page.locator('button[data-detail="문자스타일 상세"]:not(:disabled)').first()
await enabled.hover(); snapshots.textHover = await enabled.evaluate(style)
}
if (mode === 'baseline') {
await writeFile(path.join(output,'baseline.json'),JSON.stringify(snapshots,null,2))
} else {
const before=JSON.parse(await readFile(path.join(output,'baseline.json'),'utf8'))
for(const key of Object.keys(before).filter(k=>!k.startsWith('text'))) check(`unchanged header ${key}`,JSON.stringify(before[key])===JSON.stringify(snapshots[key]))
const oldIds=JSON.parse(await readFile(path.join(output,'baseline-story-ids.json'),'utf8'))
check('all previous 31 stories retained',oldIds.length===31&&oldIds.every(id=>ids.includes(id)),{before:oldIds.length,after:ids.length})
for(const id of ids){await goto(id.replace('sw-header--',''));check(`render ${id}`,!(await page.locator('.sb-errordisplay').isVisible()))}
await goto('composition')
const presets=['기본 전체','구성 A','구성 B (레이어·선스타일)','구성 C (로고만)','파일 목록 제거','레이어 제거','색상 제거','스타일 그룹 제거','여러 메뉴 제거','순서 변경','숨김 + 순서 변경']
const all=['layer','color','lineStyle','textStyle','dimensionStyle']
const removed=[[],['textStyle','dimensionStyle'],['color','textStyle','dimensionStyle'],all,[],['layer'],['color'],['lineStyle','textStyle','dimensionStyle'],['color','textStyle'],[],['layer','textStyle']]
const orders=[...Array(9).fill(all),['color','layer','dimensionStyle','lineStyle','textStyle'],['dimensionStyle','textStyle','color','layer','lineStyle']]
for(let p=0;p<presets.length;p++)for(const width of [1920,1440,1080,900,768,640,390]){
await page.setViewportSize({width,height:1000});await page.getByRole('button',{name:presets[p],exact:true}).click()
const h=page.locator('[data-composition-demo] header.hm-sw-header')
const actual=await h.evaluate(h=>({height:h.getBoundingClientRect().height,overflow:h.scrollWidth-h.clientWidth,menus:[...h.querySelectorAll('.hm-sw-header__fields > *')].filter(e=>e.querySelector('.hm-sw-header__field')).map(e=>e.dataset.control??({layer:'layer',color:'color','line-style':'lineStyle','text-style':'textStyle','dimension-style':'dimensionStyle'}[e.dataset.menu]??(e.classList.contains('hm-sw-header__layer')?'layer':null)))}))
const expected=orders[p].filter(id=>!removed[p].includes(id))
check(`composition ${presets[p]} @${width}`,actual.height===49&&actual.overflow===0&&JSON.stringify(actual.menus)===JSON.stringify(expected),{actual,expected})
const more=h.locator('.hm-sw-header__more-trigger')
if(await more.isVisible()) {await more.click();const items=await h.locator('.hm-sw-header__more-item').allTextContents();check(`nonempty More ${presets[p]} @${width}`,items.length>0);await page.keyboard.press('Escape')}
}
await page.setViewportSize({width:1920,height:1000})
// Real dropdown integration, separate from the shared-button specimen.
await goto('color-dropdown')
const host=page.locator('section').filter({has:page.getByRole('heading',{name:'선택 · 입력 · Callback 기록',exact:true})})
for(const [name,callback] of [['색상','onColorDetail'],['선스타일','onLineStyleDetail'],['문자스타일','onTextStyleDetail'],['치수스타일','onDimensionStyleDetail']]){
const trigger=host.getByRole('button',{name,exact:true});await trigger.click()
const detail=host.getByRole('button',{name:`${name} 상세`,exact:true});await detail.hover()
check(`actual ${name} shared hover`,(await detail.evaluate(style)).textOpacity==='0.35')
if(name!=='색상'){
const row=host.locator('.hm-sw-header__menu-row').first();await row.locator('button').first().hover()
check(`unchanged ${name} list hover`,await row.evaluate(e=>getComputedStyle(e).backgroundColor)==='rgb(98, 83, 61)')
}
await detail.click();check(`actual ${name} callback / close / focus`,(await host.locator('[data-callback-log]').textContent()).includes(callback)&&await trigger.getAttribute('aria-expanded')==='false'&&await trigger.evaluate(e=>e===document.activeElement))
}
await goto('layer-detail-input')
const layerHost=page.locator('section').filter({has:page.getByRole('heading',{name:'레이어 상세 Click Callback',exact:true})})
const layerTrigger=layerHost.getByRole('button',{name:'레이어',exact:true});await layerTrigger.click()
await layerHost.getByRole('button',{name:'레이어 상세',exact:true}).click()
check('actual layer callback / close / focus',(await layerHost.locator('[data-callback-log]').textContent()).includes('onLayerDetail')&&await layerTrigger.getAttribute('aria-expanded')==='false'&&await layerTrigger.evaluate(e=>e===document.activeElement))
await goto('detail-buttons')
const rows=page.locator('[data-detail-comparison] [data-detail-row]')
const names=['레이어 상세','색상 상세','선스타일 상세','문자스타일 상세','치수스타일 상세'], widths=[113,99,128,142,142]
for(let j=0;j<5;j++){
const row=rows.nth(j),button=row.locator('[data-detail-case="default"] button'),off=row.locator('[data-detail-case="disabled"] button'),empty=row.locator('[data-detail-case="no-callback"] button')
await page.mouse.move(1,1);const rest=await button.evaluate(style)
await button.hover();const hover=await button.evaluate(style)
await page.mouse.move(1,1);const leave=await button.evaluate(style)
await off.hover({force:true});const disabled=await off.evaluate(style)
report.styles[names[j]]={default:rest,hover,disabled}
for(const [state,s,ref] of [['default',rest,before.textDefault],['hover',hover,before.textHover]]){
check(`${names[j]} ${state} equals original text design`,['background','color','opacity','border','radius','shadow','textColor','textOpacity','textStroke','iconOpacity','iconContent'].every(k=>s[k]===ref[k]),{actual:s,reference:ref})
check(`${names[j]} ${state} dimensions`,s.width===widths[j]&&s.height===24,s)
}
check(`${names[j]} leave restores`,JSON.stringify(rest)===JSON.stringify(leave))
check(`${names[j]} disabled stays default`,disabled.disabled&&['background','textOpacity','iconContent','radius'].every(k=>disabled[k]===rest[k]))
const count=await row.locator('output').textContent();const box=await off.boundingBox();await page.mouse.click(box.x+box.width/2,box.y+box.height/2)
check(`${names[j]} disabled blocks callback`,await row.locator('output').textContent()===count)
await button.click();check(`${names[j]} callback once`,await row.locator('output').textContent()==='1')
await empty.hover();check(`${names[j]} callback-free hover`,!(await empty.isDisabled())&&(await empty.evaluate(style)).background===hover.background);await empty.click()
await page.mouse.move(1,1);await button.focus();await page.keyboard.press('Tab');await page.keyboard.press('Shift+Tab')
check(`${names[j]} keyboard focus visible`,await button.evaluate(e=>e.matches(':focus-visible')&&getComputedStyle(e).outlineStyle!=='none'))
await page.keyboard.press('Enter');check(`${names[j]} keyboard callback`,await row.locator('output').textContent()==='2')
}
await page.mouse.move(1,1);await page.screenshot({path:path.join(output,'comparison-default.png'),fullPage:true})
await rows.nth(3).locator('[data-detail-case="hover"] button').hover();await page.screenshot({path:path.join(output,'comparison-pointer-hover.png'),fullPage:true})
}
}
check('no browser errors',report.errors.length===0,report.errors)
} catch(e) { check('suite completed',false,e.stack) }
finally { report.status=report.checks.every(c=>c.status==='PASS')?'PASS':'FAIL';await writeFile(path.join(output,`${mode}-result.json`),JSON.stringify(report,null,2));await browser.close();if(server)await new Promise(resolve=>server.close(resolve));console.log(JSON.stringify({status:report.status,checks:report.checks.length,stories:report.storyIds?.length,errors:report.errors}));process.exitCode=report.status==='PASS'?0:1 }
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": false,
"declaration": true,
"emitDeclarationOnly": true,
"rootDir": "src",
"outDir": "dist",
"types": ["react", "react-dom"]
},
"include": ["src"]
}
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more