feat(sw): viewer header keeps every menu down to 520px
Deploy Storybook / deploy (push) Successful in 1m16s

- File name: clamp(80px, min(45cqw - 256px, 100cqw - 780px), 360px),
  56px below 560px; 1280px and wider unchanged
- Model / Layout tabs narrow (name, then count ellipsized) down to the new
  --hm-sw-viewer-tabs-min-width (48px) before anything moves into More;
  the measurement counts them at that minimum once every icon step applies
- Switch items get an optional shortLabel shown below 560px (어두운 배경 ->
  배경); counts hidden there
- More appears only below about 520px; viewer test and AI guides updated
This commit is contained in:
shkim4 committed 2026-10-26 20:00:50 +09:00
1 parent 7976801034
commit cabd2f67de
8 files changed
+83 -34

No files matched your search

@@ -121,9 +121,11 @@ function responsiveSteps(groups: SWViewerHeaderGroup[], extraIcons: string[] = [
* width up to its usual width (so a short name leaves room to the menus), the object surface without stretching and
* the trailing area without its auto margin (`data-measuring`, sw-viewer.css). Read and reset in one layout effect,
* never painted; positioned layers (menus, logo menu) never count. Same method as SWHeader's measured More.
* `tabsAtMin` (2026-10-26): once every icon step is applied, the Model / Layout tabs count at their minimum width
* (`data-measuring="min"`), so they narrow with an ellipsis before anything moves into More.
*/
function measureRoom(root: HTMLElement) {
root.setAttribute("data-measuring", "")
function measureRoom(root: HTMLElement, tabsAtMin = false) {
root.setAttribute("data-measuring", tabsAtMin ? "min" : "")
const style = getComputedStyle(root), box = root.getBoundingClientRect()
const end = box.right - parseFloat(style.borderRightWidth) - parseFloat(style.paddingRight)
let used = box.left + parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft)
@@ -226,7 +228,11 @@ function SwitchField({ item, onCommand }: { item: Extract<SWViewerHeaderItem, {
const id = useId()
return (
<div className="hm-sw-viewer-header__switch-field" title={item.title}>
<label htmlFor={id} className="hm-sw-viewer-header__switch-label">{item.label}</label>
{/* shortLabel (2026-10-26): the narrow-header caption (sw-viewer.css); the full label stays the accessible name. */}
<label htmlFor={id} className="hm-sw-viewer-header__switch-label" data-short={item.shortLabel ? "" : undefined}>
<span className="hm-sw-viewer-header__switch-label-full">{item.label}</span>
{item.shortLabel && <span className="hm-sw-viewer-header__switch-label-short" aria-hidden="true">{item.shortLabel}</span>}
</label>
<SWViewerSwitch id={id} checked={item.checked} disabled={item.disabled}
onCheckedChange={next => { item.onCheckedChange?.(next); onCommand({ id: item.id, itemId: item.id, value: next }) }} />
</div>
@@ -342,7 +348,7 @@ export const SWViewerHeader = forwardRef<HTMLElement, SWViewerHeaderProps>(funct
if (level !== 0) return setLevel(0)
}
if (level > steps.length) return setLevel(steps.length)
const room = measureRoom(root)
const room = measureRoom(root, level >= steps.filter(step => step.kind === "icons").length)
const freed = freedRef.current
const last = level > 0 ? freed[level - 1] : undefined
if (last && last.room === undefined) last.room = room - last.before
@@ -108,6 +108,11 @@ export type SWViewerHeaderItem =
| (SWViewerHeaderItemBase & {
/** Caption above a switch, e.g. 2.5D. */
type: "switch"
/**
* Shorter caption shown instead of `label` in a narrow header (below 560px header width), e.g. 배경 for 어두운 배경.
* `label` stays the accessible name. Default: `label`.
*/
shortLabel?: string
checked: boolean
onCheckedChange?: (checked: boolean) => void
})