User-approved 2026-10-22 SW changes (CSS only, public API unchanged). - Footer Model / Layout tab labels 700 in every state, letter-spacing -5%; the name-edit input keeps 400. - Slanted Layout tab and Model tab draw their SVG assets as 9-slice border-images: 97px / 73px stay the minimum, the slant and corners keep their size and only the flat middle grows with a longer label (max 12em, then ellipsis, like the rounded tabs). Hover surfaces use a union mask of the same asset (no mask-border), so Chromium and Firefox match. The default width renders pixel-identical to the previous drawing; the 8px slant / Model joint is unchanged. - Viewer panel body: the 7px bottom padding (inside the scrollport) becomes a 2px transparent bottom border, so scrolled rows stop 2px above the frame border instead of running into it. SWPopup keeps its 7px. - AI docs: DESIGN_RULES, AI_USAGE, FOOTER, VIEWER updated. - Tests: text-weight and footer-design-review expect the 700 labels and read the Model surface from the 9-slice ::before.
117 lines
15 KiB
Markdown
117 lines
15 KiB
Markdown
# SW Footer — `SWFooter` 계열 (SW 일반 모드)
|
||
|
||
SW 프로그램 하단 명령 막대입니다. 기준은 Figma 이지빔-SW `레이아웃 분리`(26:10097) 하단 72px과 부품 노드 34개(Plugin API로 읽은 값, 2026-10-20)이고, 바 안 명령창·좌표·좁은 폭 동작은 사용자 제공 실제 화면(2026-10-20)을 따릅니다. 두 기준이 다르면 Figma가 우선입니다. 단, 2026-10-20 사용자 확정으로 선택된 Model 탭과 Layout 탭 Hover는 주황 그라데이션(Figma `gra-border-orange`)입니다. **SW 일반 모드(편집 화면) 전용**이며 뷰어 모드(`SWViewer*`) 화면에는 쓰지 않습니다([AI_USAGE.md](./AI_USAGE.md) §0).
|
||
|
||
컴포넌트는 **표시와 사용자 입력 전달**만 합니다. CAD 명령 실행, 명령 등록, 레이아웃 생성·삭제, 명령 해석은 소비 프로그램이 Callback을 받아 처리하고 Props를 갱신합니다.
|
||
|
||
## 1. AI 작업 순서 (Footer 요청을 받았을 때)
|
||
|
||
1. **존재 확인:** `node_modules/@hm-design/sw/ai/FOOTER.md`(이 문서)와 [COMPONENT_CATALOG.md](./COMPONENT_CATALOG.md) §10이 있는지 확인합니다. 있으면 Footer를 새로 만들지 않습니다.
|
||
2. **Export 확인:** `node_modules/@hm-design/sw/dist/index.d.ts`에서 `SWFooter`·`SWFooterLayoutTabs`·`SWFooterCommandLine`·`swFooterToolIcons`가 Export되는지 확인합니다. CSS는 `@hm-design/sw/styles.css` 한 개(이미 Import했다면 추가 없음).
|
||
3. **기존 Footer 우선:** 하단 막대 전체 → `SWFooter`. 일부만(보조기능만, 분류 탭만 …) → 아래 §3 부품을 `<SWTheme>` 안에서 조합.
|
||
4. **Props로 구성:** 분류·명령·보조기능·문구·아이콘은 데이터(`categories`, `auxItems`, `auxAll`, `commandAll`, `slotGuide`)로 넣습니다. 선택 상태(`categoryId`, `activeToolId`, `pressed`, `value`)는 호스트 상태로 제어합니다.
|
||
5. **Variant:** Figma 상태는 Props로만 나옵니다(§4 표). Hover·Focus는 CSS가 처리합니다.
|
||
6. **재구현 금지:** Footer의 배경·크기·Pocket 탭·구분선·글자 외곽선을 `div`와 CSS로 다시 그리지 않습니다. `hm-sw-footer*` class를 소비 CSS에서 재정의하거나 복사하지 않습니다. 내용(명령·메뉴·아이콘·문구)만 바꿉니다.
|
||
|
||
판단을 결과 보고에 남깁니다: "SWFooter 재사용 — categories 3개, 보조기능 6개, Layout 탭 연결, 명령 실행은 호스트 onToolSelect".
|
||
|
||
## 2. `SWFooter` (완성형)
|
||
|
||
```tsx
|
||
import { SWFooter, SWFooterLayoutTabs, SW_FOOTER_MODEL, swFooterToolIcons, swFooterAuxIcons } from "@hm-design/sw"
|
||
import "@hm-design/sw/styles.css"
|
||
|
||
<SWFooter
|
||
categories={[{ id: "shape", label: "도형", tools: [{ id: "line", label: "Line", ...swFooterToolIcons.line }] }]}
|
||
categoryId={categoryId} onCategoryChange={setCategoryId}
|
||
activeToolId={running} onToolSelect={(toolId, categoryId, tool) => runCommand(toolId)}
|
||
onSlotAdd={(slotIndex, categoryId) => openRegisterDialog(slotIndex)}
|
||
auxItems={[{ id: "snap", label: "객체스냅", icon: swFooterAuxIcons.objectSnap, pressed: snap }]}
|
||
onAuxPressedChange={(id, pressed) => setSnap(pressed)}
|
||
commandAll={{ onClick: toggleCommandList, pressed: listOpen }}
|
||
status={`${x}, ${y}, ${z}`}
|
||
command={<SWFooterCommandLine history={history} onSubmit={runCommandText} expanded={listOpen} onExpandedChange={setListOpen} />}
|
||
layoutTabs={<SWFooterLayoutTabs layouts={layouts} value={space} onValueChange={setSpace} onAdd={addLayout} onRemove={removeLayout} onRename={renameLayout} />}
|
||
/>
|
||
```
|
||
|
||
| Prop | 기본값 | 설명 |
|
||
|---|---|---|
|
||
| `categories` | (필수) | `SWFooterCategory[]` — `{ id, label, tools: SWFooterTool[], disabled? }`. 아래 분류 행과 위 명령 칸 |
|
||
| `categoryId` / `defaultCategoryId` / `onCategoryChange` | 첫 분류 | 현재 분류(Controlled / Uncontrolled) |
|
||
| `activeToolId` / `onToolSelect(toolId, categoryId, tool)` | `null` | 실행 중 명령 = 주황(Figma Variant3, `aria-pressed`) |
|
||
| `slotCount` | `17` / `command`가 있으면 제한 없음 | 명령 + 빈 칸(+)의 최대 칸 수. **칸 영역의 폭은 바 폭으로만 정해지고 분류의 명령 수와 무관**합니다(분류를 바꿔도 칸 영역·명령창·명령어 전체보기·Layout 탭 위치가 그대로). 명령이 적으면 빈 칸, 많으면 마지막 칸의 ▲ 패널(§4.1) |
|
||
| `onSlotAdd(slotIndex, categoryId)` | 없음 | 빈 칸 클릭(명령 등록). **없으면 빈 칸은 모양만 있고 반응하지 않음** |
|
||
| `slotGuide` | `false` | 빈 칸 Hover·키보드 Focus 시 위에 뜨는 208 × 149 안내(선택 사용). 2026-10-21부터 기본은 없음(+ 위 미리보기 제거). 필요할 때만 `{}`(Figma 가이드 `명령어 등록` / `빠른사용`) 또는 `{ title, caption, media }`를 전달. `media`는 194 × 111 영상·이미지(프로그램 제공) |
|
||
| `onCategoryAdd` | 없음 | 분류 행 끝 + 버튼 표시 |
|
||
| `auxItems` / `onAuxPressedChange(id, pressed)` / `onAuxItemClick(id)` | 없음 | 3 × 2 보조기능 토글(`SWFooterAuxItem`: `id, label, icon?, pressed?, title?, disabled?`). `pressed`를 주면 토글 |
|
||
| `auxAll` / `commandAll` | 표시 | 양끝 74 × 71 버튼 `{ title, subtitle, tooltip, onClick, pressed, disabled }`. 기본 문구 `보조기능 / 전체보기`, `명령어 / 전체보기`. `false` = 숨김 |
|
||
| `command` | 없음 | 바 안 명령창(보통 `<SWFooterCommandLine>`, 실제 화면 구성). 보조기능 격자와 명령 칸 사이의 남는 폭(최소 280px)을 쓰고, 명령 칸은 오른쪽 `명령어 전체보기` 바로 앞으로 붙습니다. 펼치면 위(도면 영역)로 커집니다 |
|
||
| `status` | 없음 | 바 위 왼쪽 글자(커서 좌표 등, 12px) |
|
||
| `layoutTabs` | 없음 | 바 위 오른쪽 끝에 붙는 탭(보통 `SWFooterLayoutTabs`) |
|
||
| `trailing` | 없음 | 바 오른쪽 빈 영역 콘텐츠(좌표·상태) |
|
||
| `label` | `명령 도구` | `<footer>` 접근성 이름 |
|
||
|
||
`SWFooterTool`: `{ id, label, icon?, activeIcon?, title?, ariaLabel?, disabled? }`. `icon` = 기본(18 × 18), `activeIcon` = Hover·선택 아이콘(없으면 `icon` 유지). 줄인 라벨(`Rect...`)은 `ariaLabel`로 전체 이름을 줍니다. 칸보다 긴 라벨(`POLYGON`, `WIPEOUT` …)은 68 × 47 칸과 14px 글자 그대로 `…`로 줄고, `title`이 없으면 잘린 동안만 전체 라벨이 Hover·Focus Tooltip(native `title`)으로 나옵니다(접근성 이름은 항상 전체 라벨 또는 `ariaLabel`). 라벨을 임의로 줄이거나(`Poly`) 칸·글자 크기를 바꾸지 않습니다. Figma 명령 아이콘은 `swFooterToolIcons.line / pline / arc / circle / ellipse / rect / poly / point / hatch / ncopy`(각 `{ icon, activeIcon }`), 보조기능 아이콘은 `swFooterAuxIcons.objectSnap / selectionEffect / orthogonal / gridSnap / gridBackground / lineWeight`(인라인 SVG, 채움 `currentColor`: 버튼 상태 색을 그대로 받음). 프로그램이 직접 넣는 보조기능 아이콘도 `currentColor`로 그리면 Selected 주황이 적용됩니다(고정 색 `<img>`는 색이 바뀌지 않음).
|
||
|
||
## 3. 부품 (조합용, `<SWTheme>` 안)
|
||
|
||
| 컴포넌트 | 크기 | 주요 Props |
|
||
|---|---|---|
|
||
| `SWFooterSideButton` | 74 × 71 | `title`(필수), `subtitle`, `side`(`start`/`end`, 그림자 방향), `tooltip`, `button` 속성 |
|
||
| `SWFooterAuxGroup` | 104 × 33 칸, gap 2 | `items`(필수), `onPressedChange`, `onItemClick`, `columns`(3), `label` |
|
||
| `SWFooterAuxButton` | 104 × 33 | `label`(필수), `icon`, `pressed`, `button` 속성 |
|
||
| `SWFooterToolButton` | 68 × 47 칸 | `label`(필수), `icon`, `activeIcon`, `pressed`, `button` 속성(`title` 생략 시 잘린 라벨만 자동 Tooltip). 연속 배치하면 칸 사이 1px 금색 구분선 |
|
||
| `SWFooterAddSlot` | 68 × 47 칸(+ 68 × 46) | `guide`(기본 `false` = 없음 / `{}` Figma 가이드 / 내용 객체), `guideOpen`(Controlled), `onGuideOpenChange`, `button` 속성 |
|
||
| `SWFooterGuide` | 208 × 149 | `title`, `caption`, `media`, `div` 속성. 단독 카드로 둘 때 `data-standalone=""` |
|
||
| `SWFooterMenu` | 높이 24 | `items`(`{ id, label, disabled? }[]`, 필수), `value`(필수), `onValueChange`, `onAdd`, `addLabel`, `label`, `panelId`, `tabIdPrefix`. 현재 탭은 위 칸과 이어지는 Pocket 모양(SVG, 위치 자동 계산) |
|
||
| `SWFooterLayoutTabs` | 높이 22 | `layouts`(`SWFooterLayout[]` = `{ id, label, locked?, shape? }`, 필수), `value`(layout id 또는 `SW_FOOTER_MODEL`), `onValueChange`, `onAdd`, `onRemove`, `onRename`, `editingId`/`onEditingChange`, `modelLabel`(`"Model"` / `false`), `label`, `addLabel`, `removeLabel` |
|
||
| `SWFooterCommandLine` | 566 × 71 / 270 (`SWFooter command` 안에서는 남는 폭) | `history`(`ReactNode[]`, 필수), `value`/`onValueChange`, `onSubmit`, `prompt`(`Command:`), `expanded`/`defaultExpanded`/`onExpandedChange`, `inputProps`, `historyLabel`, `expandLabel`, `collapseLabel` |
|
||
|
||
- `SWFooter`·`SWFooterCommandLine`은 루트에 `hm-sw-theme`이 있어 `<SWTheme>`가 필요 없습니다. 나머지 부품은 `<SWTheme>` 안에 둡니다.
|
||
- `SWFooterMenu`의 Pocket은 부모 배경(명령 칸 면)을 비춰 보입니다. 단독으로 둘 때 부모에 `background: var(--hm-sw-footer-group-background)`를 줍니다.
|
||
- `SWFooterLayoutTabs`의 마지막 탭은 사선 모양(Figma layout01, × = btn_close2), 나머지는 둥근 모양(layout02/03, × = btn_delete)입니다. `shape`로 바꿀 수 있습니다. 탭 순서는 배열 순서 그대로(Figma 예시: `+`, Layout 03, 02, 01, Model).
|
||
- 탭 글자(2026-10-22): Model·Layout 라벨은 모든 상태 12px **700**, 자간 **-5%**. 이름 편집 입력은 400. 탭 폭은 라벨에 맞춰 늘어납니다(패키지 CSS가 처리): 사선 탭 97px·Model 73px·둥근 탭은 기본 폭이 최소이고, 사선·모서리는 그대로 가운데만 늘어나며(9-slice), 라벨은 최대 12em 이후 말줄임. 폭·자간을 소비 CSS로 덮지 않습니다.
|
||
- `SWFooterCommandLine`은 `SWFooter`의 `command`에 넣는 것이 기본 구성입니다(실제 화면). 바 밖에 두면 위치(보통 도면 영역 왼쪽 아래)는 소비 레이아웃이 정합니다. 접힘 = 마지막 3줄, 펼침 = 전체 기록 스크롤.
|
||
|
||
## 4. 상태 (Figma Variant ↔ Props)
|
||
|
||
| 요소 | Figma | 구현 |
|
||
|---|---|---|
|
||
| 명령 | Default `#F6F4F2` / Variant2 Hover = Variant3 선택: `#FF5C00` + 주황 아이콘 | Hover는 CSS, 선택은 `activeToolId`·`pressed`. 아이콘·라벨 위치·크기와 칸 크기는 모든 상태에서 같음 |
|
||
| 분류 탭 | Default `#D4CBBD` / Hover·현재 `#FF5C00`, 현재 탭은 Pocket | `categoryId`·`value` |
|
||
| 빈 칸 | Default + / Hover 주황 점선 링 + 주황 + | `slotGuide`를 전달한 경우에만 Hover·Focus 시 안내 표시(기본 없음) |
|
||
| Layout 탭 | Default 베이지 `#C3B79E` / Hover(선택 아님, ×) = Model과 같은 주황 그라데이션 면(글자는 Default와 동일: 검정 `#161410`, 크기·굵기·위치 불변) / On 갈색 그라데이션·흰 글자 / On hover / Variant5 이름 편집(`#0F0D0A`, 빨강 `#E60403`) / Variant6 편집 + Hover | `value`, 편집 = 더블클릭·F2 또는 `editingId` |
|
||
| Model 탭 | Default 베이지 / Hover·On = 주황 그라데이션 면(`gra-border-orange`) + 검정 `#161410` 글자 | `value === SW_FOOTER_MODEL` |
|
||
| `+` 버튼 | Layout 옆 `+` Hover = 같은 주황 그라데이션 면 + `+`. 분류 행 `+` Hover = `+` `#FF5C00` | `onAdd` / `onCategoryAdd` |
|
||
| 명령창 화살표 | Default ↑ / Hover 주황 ↑ / On ↓ / On hover 주황 ↓ | `expanded` |
|
||
| Disabled | Figma 없음 | SW 공통 40%(명령·빈 칸·탭·보조기능) |
|
||
| 보조기능 | Figma btn_객체스냅 26:9248 … btn_선가중치 26:9412: Default 갈색 그라데이션 테두리·아이콘 `#F1EDE5`·글자 `#F6F4F2` / Variant2 = Variant3 주황 그라데이션 테두리·아이콘·글자 `#FF5C00` | Default 그대로. **Hover = 노랑 테두리만**(SW Header Figma Hover 토큰 `--hm-sw-header-hover-border-gradient`, 1px, 아이콘·글자 기본색). **Selected(`pressed`) = 주황 테두리 + 주황 아이콘 + 주황 글자 동시**, Hover 중에도·마우스가 벗어나도 유지. 크기·위치 변화 없음 |
|
||
| 측면 버튼 Hover·Pressed | Figma 없음 | Footer 규칙 재사용(2026-10-20): 제목·부제 모두 `#FF5C00` + 테두리 주황 그라데이션(보조기능 Selected 테두리). 크기·면·그림자 그대로, Disabled는 기본 모양 (디자인 확인 필요) |
|
||
|
||
### 4.1 좁은 폭 (Figma 없음, 실제 화면 2026-10-20)
|
||
|
||
0. 칸 영역은 바 폭에서 정해집니다. 명령창이 없으면 그룹이 바를 채우고(최대 `slotCount` 17), 명령창(`command`)이 있으면 남는 폭(바 − 보조기능 영역 − 명령어 전체보기)의 `--hm-sw-footer-slot-share`(0.57)를 칸 영역, 나머지를 명령창(최소 280px)이 씁니다. 1920 → 12칸·명령창 629px, 1440 → 8칸, 1280 → 6칸, 950 → 5칸.
|
||
1. 빈 칸(+)이 먼저 줄어듭니다.
|
||
2. 명령이 남는 칸보다 많으면 들어가는 명령만 두고 **마지막 칸이 ▲**(Figma ico_arrow)가 됩니다. ▲를 누르면 나머지 명령이 명령 칸 바로 위(오른쪽 맞춤, 같은 칸 모양)에 펼쳐지고 ▼가 됩니다. 명령을 고르거나 Escape·바깥 클릭이면 닫힙니다. 숨은 명령이 선택 중이면 ▲가 주황입니다.
|
||
3. 분류 탭이 넘치면 탭 영역 안에서 잘리고 ◀ ▶(스크롤)와 +가 오른쪽에 붙습니다. 현재 탭과 Focus 탭은 자동으로 보이게 스크롤됩니다.
|
||
4. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33, 이름은 접근성 이름·tooltip)입니다.
|
||
5. 바 안 명령창은 최소 280px을 유지합니다. 측면 버튼·Layout 탭은 크기가 바뀌지 않습니다(▲ 패널은 Layout 탭 위에 겹침).
|
||
|
||
## 5. 키보드·접근성
|
||
|
||
- 명령 칸: `role="toolbar"`, 칸 하나만 Tab 정지, ←/→/Home/End 이동. 명령 버튼 `aria-pressed`. ▲ = `aria-expanded`, 열면 첫 명령으로 Focus, Escape = 닫고 ▲로 Focus.
|
||
- 분류 행: `role="tablist"`, ←/→/Home/End = 이동 + 선택, 명령 칸이 `tabpanel`. 분류 + 버튼은 tablist 밖의 일반 버튼. ◀ ▶는 마우스용(`tabIndex -1`, 키보드는 탭 이동이 자동 스크롤).
|
||
- 빈 칸: 이름 `명령어 등록 (n번 칸)`, 가이드가 보이면 `aria-describedby`.
|
||
- Layout 탭: `role="tablist"`, ←/→ 선택, **Delete** = 삭제(`onRemove`), **F2**·더블클릭 = 이름 편집(Enter 저장, Escape 취소, 바깥 클릭 저장). ×는 마우스 전용(`tabIndex -1`).
|
||
- 명령창: 기록 `role="log"`(`aria-live="polite"`), 입력 `<label>` 연결, Enter = `onSubmit`(한글 조합 중 Enter 무시), 화살표 `aria-expanded`.
|
||
- Focus: SW 공통 2px `#F0F7F5`.
|
||
|
||
## 6. 하지 말 것
|
||
|
||
- 뷰어 모드 화면에 `SWFooter`를 넣지 않습니다(뷰어는 `SWViewerHeader` 탭·버튼으로 Model/Layout을 다룸).
|
||
- 명령 실행·명령 등록 로직을 컴포넌트 안에 넣으려고 래핑하지 않습니다. Callback에서 처리합니다.
|
||
- 칸 크기(68 × 47)·바 높이(72)·탭 높이(22)를 바꾸려고 class를 덮어쓰지 않습니다. 필요한 경우 공개 토큰(`--hm-sw-footer-*`)을 `style`로 바꾸고 결과 보고에 "디자인 확인 필요"로 남깁니다.
|
||
- 아이콘을 Figma 자산 대신 임의 SVG로 바꿔 "Figma 아이콘"이라고 보고하지 않습니다. 새 아이콘은 프로그램 제공 18 × 18(보조 14~18) 요소입니다.
|
||
- 이 문서에 없는 Props(`items`, `tools` 최상위, `onCommand` 등)는 없습니다.
|