fix(sw): stabilize footer command layout and overflow alignment

This commit is contained in:
shkim4 committed 2026-10-20 10:38:42 +09:00
1 parent 41e963a12a
commit 73e30242f2
7 files changed
+36 -21

No files matched your search

+1 -1
View File
@@ -159,7 +159,7 @@ SW **뷰어 모드** 전용 Export입니다(2026-10-19 추가, 참고 DWG/DXF
| 컴포넌트 | 용도 | 주요 Props | 검증 |
|---|---|---|---|
| `SWFooter` | 72px 하단 막대: 보조기능 버튼 → 3 × 2 보조기능 → (바 안 명령창) → 명령 칸(68 × 47) + 분류 행(24) → 명령어 전체보기, 위 오른쪽 끝 Layout 탭 | `categories: SWFooterCategory[]`(필수), `categoryId`/`defaultCategoryId`/`onCategoryChange`, `activeToolId`, `onToolSelect(toolId, categoryId, tool)`, `slotCount`(17), `onSlotAdd(slotIndex, categoryId)`, `slotGuide`(`SWFooterGuideContent \| false`), `onCategoryAdd`, `auxItems`, `onAuxPressedChange`, `onAuxItemClick`, `auxAll`/`commandAll`(`SWFooterSideAction \| false`), `command`(바 안 명령창), `status`(바 위 왼쪽 좌표), `layoutTabs`, `trailing`, `label`, `<footer>` 속성·`ref`. 좁은 폭: 빈 칸 축소 → ▲ 패널, 분류 ◀ ▶, 보조기능 아이콘만 | ◻ |
| `SWFooter` | 72px 하단 막대: 보조기능 버튼 → 3 × 2 보조기능 → (바 안 명령창) → 명령 칸(68 × 47) + 분류 행(24) → 명령어 전체보기, 위 오른쪽 끝 Layout 탭 | `categories: SWFooterCategory[]`(필수), `categoryId`/`defaultCategoryId`/`onCategoryChange`, `activeToolId`, `onToolSelect(toolId, categoryId, tool)`, `slotCount`(최대 칸 수, 17 / `command`가 있으면 제한 없음 — 칸 영역 폭은 바 폭으로만 결정), `onSlotAdd(slotIndex, categoryId)`, `slotGuide`(`SWFooterGuideContent \| false`), `onCategoryAdd`, `auxItems`, `onAuxPressedChange`, `onAuxItemClick`, `auxAll`/`commandAll`(`SWFooterSideAction \| false`), `command`(바 안 명령창), `status`(바 위 왼쪽 좌표), `layoutTabs`, `trailing`, `label`, `<footer>` 속성·`ref`. 좁은 폭: 빈 칸 축소 → ▲ 패널, 분류 ◀ ▶, 보조기능 아이콘만 | ◻ |
| `SWFooterSideButton` | 양끝 74 × 71 버튼(보조기능 / 명령어 전체보기) | `title`(필수), `subtitle`, `side`(`start`/`end`), `tooltip`, `button` 속성 | ◻ |
| `SWFooterAuxGroup` | 보조기능 토글 격자(Figma 3 × 2) | `items: SWFooterAuxItem[]`(필수), `onPressedChange`, `onItemClick`, `columns`(3), `label` | ◻ |
| `SWFooterAuxButton` | 보조기능 104 × 33 버튼(Hover 노랑 테두리, `pressed` = 주황 테두리·아이콘·글자) | `label`(필수), `icon`(`currentColor` 아이콘이면 상태 색 적용), `pressed`, `button` 속성 | ◻ |
+2 -2
View File
@@ -195,9 +195,9 @@ Container `hm-sw-header` 기준 Breakpoint: **< 1800** 선/문자/치수스타
| Layout 탭 | 높이 22, 12px 자간 2%. Default 면 `#C3B79E` 글자 `#161410`. On: `linear-gradient(0deg, #7C6C4C, #7C6C4C 85%, #A6946F)` 흰 글자. 이름 편집: `#0F0D0A` 빨강 `#E60403` 주황 테두리. Hover·On·편집 글자 아래 70% 검정 1.3px 외곽선. 둥근 탭: 1px `rgba(0,0,0,.05)`(Hover·편집 `#DA6813`), 위 radius 4, 안쪽 그림자 `inset 0 -3px 4px rgba(56,48,31,.25)`, ×(16) Hover 노랑. 사선 탭(마지막): 97 × 22 자산, Hover = 주황 면 + 검정 글자, On hover = 베이지 면 + 주황 글자, ×(28, 빨강 사선) | `--hm-sw-footer-tab-*` |
| Model 탭 | 73 × 22 자산(왼쪽 사선), 글자 왼쪽 22, Hover 주황, On 갈색. 앞 사선 탭과 8px 겹침 | — |
| 명령창 | 566 × 71 / 270, 위 radius 4, 위 71px `linear-gradient(180deg, #635537, #262218 5%, #281F16 10%, #282216)` + 아래 `#282216`. 12px 자간 10% 줄 14, 1px 외곽선. 입력 칸 23(좌우 4) `#16130D` + `0 -1px 2px rgba(255,255,255,.25)`, `inset 0 1px 4px #000`. 화살표 18 × 18 검정 radius 2(4, 4 오른쪽 위) | `--hm-sw-footer-command-*` |
| 바 안 명령창(실제 화면) | 보조기능 뒤 4px, 남는 폭(최소 280), 바 아래에 붙어 펼치면 위로 270 | `--hm-sw-footer-command-min-width` |
| 바 안 명령창(실제 화면) | 보조기능 뒤 4px, 바 아래에 붙어 펼치면 위로 270. 남는 폭(바 − 보조기능 영역 − 명령어 전체보기)을 칸 영역 0.57 : 명령창 나머지(최소 280)로 나눔 — 칸 수는 바 폭으로만 계산, 분류의 명령 수와 무관(1920 → 12칸) | `--hm-sw-footer-command-min-width`, `--hm-sw-footer-slot-share` |
| 좌표(실제 화면) | 바 위 왼쪽, 12px `#F6F4F2` 1px 외곽선, 아래 3px | — |
| 좁은 폭(실제 화면) | 빈 칸이 먼저 줄고, 넘치는 명령은 마지막 칸 ▲(ico_arrow 18 × 18, 오른쪽 위 4) → 명령 칸 위 패널(같은 칸, 오른쪽 맞춤, 테두리 1px `#886D35`·아래 없음·위 radius 4, 명령 칸 면). 분류 탭 넘침 = 탭 영역 잘림 + ◀ ▶(13 × 16, 6 × 8 삼각형 `#F6F4F2`, Hover `#FF5C00`) + +. 바 1279px 이하 보조기능 아이콘만 45 × 33 | `--hm-sw-footer-aux-compact-width` |
- Figma에 없는 값: 실제 화면에서 가져온 값(바 안 명령창 배치, 좌표, ▲ 패널, 분류 ◀ ▶, 보조기능 아이콘만)과 AI 구성 값 — 디자인 확인 필요(측면 버튼 Hover 글자 `#FF5C00`, Disabled 40%, 명령창 최소 280px, 보조기능 아이콘 전환 1279px, Layout 탭 이름 편집 입력의 동작).
- Figma에 없는 값: 실제 화면에서 가져온 값(바 안 명령창 배치, 좌표, ▲ 패널, 분류 ◀ ▶, 보조기능 아이콘만)과 AI 구성 값 — 디자인 확인 필요(측면 버튼 Hover 글자 `#FF5C00`, Disabled 40%, 명령창 최소 280px, 칸 영역 비율 0.57(두 실제 화면 재현 값), 보조기능 아이콘 전환 1279px, Layout 탭 이름 편집 입력의 동작).
- 새 하단 UI는 위 토큰을 `var(--…)`로 읽고 `hm-sw-footer*` class를 쓰거나 덮어쓰지 않습니다.
+2 -1
View File
@@ -40,7 +40,7 @@ import "@hm-design/sw/styles.css"
| `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`가 있으면 명령 수 + 2 | 명령 + 빈 칸(+) 수. 바 폭이 좁으면 빈 칸부터 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲ 패널로 이동(§4.1) |
| `slotCount` | `17` / `command`가 있으면 제한 없음 | 명령 + 빈 칸(+)의 최대 칸 수. **칸 영역의 폭은 바 폭으로만 정해지고 분류의 명령 수와 무관**합니다(분류를 바꿔도 칸 영역·명령창·명령어 전체보기·Layout 탭 위치가 그대로). 명령이 적으면 빈 칸, 많으면 마지막 칸의 ▲ 패널(§4.1) |
| `onSlotAdd(slotIndex, categoryId)` | 없음 | 빈 칸 클릭(명령 등록). **없으면 빈 칸은 모양만 있고 반응하지 않음** |
| `slotGuide` | Figma 가이드 | 빈 칸 Hover·키보드 Focus 시 위에 뜨는 208 × 149 안내 `{ title = "명령어 등록", caption = "빠른사용", media? }`. `media`는 194 × 111 영상·이미지(프로그램 제공). `false` = 없음 |
| `onCategoryAdd` | 없음 | 분류 행 끝 + 버튼 표시 |
@@ -89,6 +89,7 @@ import "@hm-design/sw/styles.css"
### 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 탭은 자동으로 보이게 스크롤됩니다.
@@ -32,8 +32,9 @@ export interface SWFooterProps extends Omit<HTMLAttributes<HTMLElement>, "childr
/** A command was clicked. The host runs it and updates `activeToolId`. */
onToolSelect?: (toolId: string, categoryId: string, tool: SWFooterTool) => void
/**
* Slots of the command row (commands + empty + slots). Default 17 (Figma 1920px frame), shrinking to the bar width.
* With `command` the row keeps its own width: default commands + 2 empty slots.
* Most slots of the command row (commands + empty + slots). The row width follows the bar width only (never the
* command count of the category). Default 17 (Figma 1920px frame); with `command` no limit (the share of the
* free width decides).
*/
slotCount?: number
/** An empty + slot was clicked (register a command). `slotIndex` is 0-based in the row. Omitted: the empty slots are inert. */
@@ -87,13 +88,17 @@ export const SWFooter = forwardRef<HTMLElement, SWFooterProps>(function SWFooter
const panelId = `${uid}panel`
const tabPrefix = `${uid}tab`
// Room for slots, measured (Figma 26:10097 has no narrow layout; the reference screens of 2026-10-20 do):
// empty slots go first, then the commands that do not fit move to the ▲ panel above the last slot.
// Room for slots comes from the bar width only, never from the current category's command count: switching
// categories keeps the slot area, 명령어 전체보기 and the command line in place (2026-10-20). Without `command`
// the group fills the bar (Figma 26:10097: up to 17 slots); with `command` the free width is shared by
// --hm-sw-footer-slot-share (command line ≥ --hm-sw-footer-command-min-width). Commands beyond the room move
// to the ▲ panel above the last slot; fewer commands leave empty slots.
const rootRef = useRef<HTMLElement | null>(null)
const groupRef = useRef<HTMLDivElement>(null)
const auxRef = useRef<HTMLDivElement>(null)
const embedded = command !== undefined && command !== null && command !== false
const [fit, setFit] = useState(Number.POSITIVE_INFINITY)
// Before the first measure: the Figma 1920 row (17 slots). Never infinite (the row renders this many slots).
const [fit, setFit] = useState(slotCount ?? 17)
useLayoutEffect(() => {
const root = rootRef.current
const group = groupRef.current
@@ -104,8 +109,11 @@ export const SWFooter = forwardRef<HTMLElement, SWFooterProps>(function SWFooter
if (embedded) {
const aux = auxRef.current
const auxWidth = aux ? aux.offsetWidth + parseFloat(getComputedStyle(aux).marginRight || "0") : 0
const commandMin = parseFloat(getComputedStyle(root).getPropertyValue("--hm-sw-footer-command-min-width")) || 0
room = root.clientWidth - auxWidth - commandMin - 4 - end - 2
const style = getComputedStyle(root)
const commandMin = parseFloat(style.getPropertyValue("--hm-sw-footer-command-min-width")) || 0
const share = parseFloat(style.getPropertyValue("--hm-sw-footer-slot-share")) || 0.5
const free = root.clientWidth - auxWidth - 4 - end - 2
room = Math.min(free * share, free - commandMin)
}
setFit(Math.max(1, Math.floor((room - 1) / SLOT)))
}
@@ -116,11 +124,10 @@ export const SWFooter = forwardRef<HTMLElement, SWFooterProps>(function SWFooter
if (auxRef.current) observer.observe(auxRef.current)
return () => observer.disconnect()
}, [embedded])
const overflow = tools.length > fit
const visible = overflow ? tools.slice(0, Math.max(0, fit - 1)) : tools
const slots = Math.max(1, Math.min(slotCount ?? (embedded ? Number.POSITIVE_INFINITY : 17), fit))
const overflow = tools.length > slots
const visible = overflow ? tools.slice(0, Math.max(0, slots - 1)) : tools
const hidden = overflow ? tools.slice(visible.length) : []
const wanted = embedded ? slotCount ?? tools.length + 2 : slotCount ?? 17
const slots = overflow ? fit : Math.max(tools.length, Math.min(wanted, fit))
// ▲ panel of the commands that do not fit (ico_arrow 26:10023: ▲ closed, ▼ open).
const [moreOpen, setMoreOpen] = useState(false)
+6 -1
View File
@@ -89,6 +89,10 @@
--hm-sw-footer-command-line-height: 14px;
/* Not in Figma (reference screens 2026-10-20): the command line inside the bar keeps at least this width. */
--hm-sw-footer-command-min-width: 280px;
/* Not in Figma: with the command line inside the bar, the slot area takes this share of the free width (bar minus
the auxiliary area and 명령어 전체보기), the command line the rest. 0.57 reproduces both reference screens
(1920 → 12 slots / command line 629px, 952 → 4 commands + ▲). Bar width only, never the command count. */
--hm-sw-footer-slot-share: 0.57;
/* Not in Figma: below this bar width the auxiliary toggles show their icon only (reference screen 2026-10-20). */
--hm-sw-footer-aux-compact-width: 45px;
}
@@ -282,7 +286,8 @@
bottom: 100%;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
/* Rows fill from the left in data order; a short last row stays on the left (the panel itself is right aligned). */
justify-content: flex-start;
border: 1px solid var(--hm-sw-footer-group-border-color);
border-bottom: 0;
border-radius: var(--hm-sw-footer-group-radius) var(--hm-sw-footer-group-radius) 0 0;