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

@@ -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)