fix(sw): stabilize footer command layout and overflow alignment
This commit is contained in:
1 parent
41e963a12a
commit
73e30242f2
7 files changed
+36
-21
No files matched your search
@@ -62,7 +62,7 @@
|
||||
| 보조기능 Hover | 버튼 세트에 Hover 없음 | 노랑 테두리만(SW Header Figma Hover 토큰), 아이콘·글자 기본색 | 사용자 확정 2026-10-20 |
|
||||
| 보조기능 Default 테두리 | Figma 갈색 그라데이션 | 그라데이션(2026-10-20 수정 전에는 단색 `#886D35`) | Figma 일치 |
|
||||
| Hover(측면 버튼), Disabled | Figma 없음 | 글자 `#FF5C00`, 40% | AI 구성 값 — 디자인 확인 필요 |
|
||||
| 좁은 폭 | Figma 없음 / 실제 화면 | 빈 칸 축소 → ▲ 패널, 분류 ◀ ▶, 보조기능 아이콘만(1279px 이하), 명령창 최소 280 | 전환 폭·최소 폭은 AI 구성 값 |
|
||||
| 좁은 폭 | Figma 없음 / 실제 화면 | 빈 칸 축소 → ▲ 패널, 분류 ◀ ▶, 보조기능 아이콘만(1279px 이하), 명령창 최소 280, 바 안 명령창 구성의 칸 영역 = 남는 폭 × 0.57 | 전환 폭·최소 폭·비율은 AI 구성 값(비율은 두 실제 화면 재현) |
|
||||
| 명령창 줄 수 | Figma 3줄(줄 14px) / 실제 화면 접힘 5줄 | Figma 3줄 | Figma 우선 |
|
||||
| 가이드 영상 | Figma는 6프레임 스토리보드(래스터) | `media`로 프로그램이 제공, Story만 프레임 순환 | 영상 자산은 프로그램 소유 |
|
||||
| WIPEOUT · 객체스냅추적 아이콘 | 실제 화면에만 있음 | Story에서 lucide 아이콘(프로그램 제공 방식) | Figma 자산 없음 |
|
||||
@@ -79,7 +79,9 @@
|
||||
| Storybook 렌더링(8개 Story, 실행 중인 6024) | PASS — Story별 Footer 수·콘솔 오류 확인 |
|
||||
| Figma ↔ 구현 1:1 비교(기본 Footer, 35 노드 Story) | PASS — 위치 측정: 보조기능 (76, 2), 명령 칸 396·y 1, 명령어 전체보기 1552, 분류 탭 Figma 대비 ≤ 12px |
|
||||
| 동작(실제 조합 Story) | PASS — 명령 선택, 분류 ←/→, 빈 칸 가이드 위치(가운데·4px 위)·클릭, 보조기능 토글, Layout 선택·F2 이름 변경·Delete·추가, 명령창 Enter·펼치기, ▲ 패널 열기·선택 후 닫힘·Escape 복귀 |
|
||||
| 화면 폭 | PASS — 실제 조합 1920 → 12칸, 1440 → 10칸, 1280 → 명령 6 + ▲, 950 → 명령 4 + ▲·보조기능 아이콘만·분류 ◀ ▶ |
|
||||
| 화면 폭 | PASS — 실제 조합 1920 → 12칸(명령창 629), 1440 → 명령 7 + ▲, 1280 → 5 + ▲, 950 → 4 + ▲·보조기능 아이콘만·분류 ◀ ▶. Figma 구성 1920 → 17칸, 1280 → 11, 1024 → 10, 820 → 6 + ▲ |
|
||||
| ▲ 패널 줄바꿈(2026-10-20 수정) | PASS — 패널 `justify-content: flex-end` → `flex-start`. 데이터 순서대로 각 줄 왼쪽 첫 칸부터 채움: 1440(3개 1줄), 1280(5개 1줄 가득), 950(5 + WIPEOUT 1), 860(4 + 3), 760(2 × 4 + 1, 5줄), Figma 820(3개). 칸 68 × 47, 패널은 명령 칸 오른쪽 끝 정렬 유지 |
|
||||
| 분류 전환 시 영역 유지(2026-10-20 수정) | PASS — 같은 폭에서 명령 10 / 1 / 0개 분류를 전환해도 칸 영역·명령창·명령어 전체보기·Layout 탭·보조기능 좌표와 폭 동일(8개 폭 모두). 수정 전에는 바 안 명령창 구성의 칸 수가 "명령 수 + 2"라 817 → 205 → 137px로 변함 |
|
||||
| Header·Popup 영향 | 수정 없음(기존 파일 변경 없음, Footer CSS는 `hm-sw-footer*`와 새 토큰만) |
|
||||
| 독립 소비 프로젝트(Tarball) 설치·빌드 | 미검증 |
|
||||
| `npm run build-storybook` (정적 빌드) | 미검증 |
|
||||
|
||||
@@ -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` 속성 | ◻ |
|
||||
|
||||
@@ -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를 쓰거나 덮어쓰지 않습니다.
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -462,9 +462,9 @@ export const Responsive: Story = {
|
||||
name: "화면 폭 변화",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="화면 폭 변화" description="Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
|
||||
<StorySection title="실제 조합 (바 안 명령창, command)" description="1920 → 12칸(명령 10 + 빈 칸 2), 1440 · 1280 → 빈 칸 축소, 950 → 명령 4개 + ▲, 보조기능 아이콘만.">
|
||||
{[1920, 1440, 1280, 950].map(width => (
|
||||
<StoryPageHeader kicker={KICKER} title="화면 폭 변화" description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
|
||||
<StorySection title="실제 조합 (바 안 명령창, command)" description="남는 폭 × 0.57 = 칸 영역, 나머지 = 명령창(최소 280). 1920 → 12칸(명령 10 + 빈 칸 2), 1440 → 명령 7 + ▲, 1280 → 5 + ▲, 950 → 4 + ▲·보조기능 아이콘만, 860 · 760 → ▲ 패널 여러 줄(왼쪽부터 채움). 각 폭에서 분류 탭을 눌러도 영역이 그대로입니다.">
|
||||
{[1920, 1440, 1280, 950, 860, 760].map(width => (
|
||||
<div key={width} className="sw-footer-story-width">
|
||||
<p className="sw-story-log">{width}px</p>
|
||||
<Screen width={width} canvas={90}><ReferenceFooter /></Screen>
|
||||
|
||||
Reference in new issue
Block a user