feat(sw): viewer header keeps every menu down to 520px
Deploy Storybook / deploy (push) Successful in 1m16s
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:
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
|
||||
})
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
--hm-sw-viewer-button-height: 34px;
|
||||
/* Model / Layout field face: widest size (2026-10-23); narrower when the header has less room. */
|
||||
--hm-sw-viewer-tabs-max-width: 378px;
|
||||
/* Narrowest Model / Layout field (2026-10-26): below its natural width the tabs narrow down to this (names and counts
|
||||
ellipsized) before the measured steps move anything into More. */
|
||||
--hm-sw-viewer-tabs-min-width: 48px;
|
||||
/* Model / Layout colors and shape (Figma 7652:40344, 2026-10-23). Used only by .hm-sw-viewer-header__tabs / __tab:
|
||||
set any of them on the SW scope or the header to recolor the tabs alone. Container: */
|
||||
/* The outer height of the adjacent field group (40px control row + its 1px top and bottom border = 42px). */
|
||||
@@ -165,10 +168,11 @@ button.hm-sw-viewer-header__brand { text-align: left; }
|
||||
align-items: center;
|
||||
/* Very large shrink weight (2026-10-23): the file name gives way first, as before; the object (shrink 1) only once
|
||||
the file name is at its minimum (see .hm-sw-viewer-header__object).
|
||||
2026-10-26 (user request): narrower below 1280px (171px at 950px, was 25cqw = 237px) so a 950px header has no More;
|
||||
from 1280px the same as before (320px, 360px from 1370px). The measurement (data-measuring) uses the same value. */
|
||||
flex: 0 1000000 clamp(140px, calc(45cqw - 256px), 360px);
|
||||
width: clamp(220px, calc(45cqw - 256px), 360px);
|
||||
2026-10-26 (user request): narrower below 1280px so 950px and 860px headers have no More (about 170px at 950px and
|
||||
80px at 860px, was 25cqw = 237px / 215px; minimum 80px, was 140px); from 1280px the same as before (320px, 360px
|
||||
from 1370px). The measurement (data-measuring) uses the same value. */
|
||||
flex: 0 1000000 clamp(80px, min(45cqw - 256px, 100cqw - 780px), 360px);
|
||||
width: clamp(220px, min(45cqw - 256px, 100cqw - 780px), 360px);
|
||||
min-width: 56px;
|
||||
height: calc(var(--hm-sw-viewer-height) - 2px);
|
||||
padding: 0 16px 0 24px;
|
||||
@@ -314,7 +318,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
|
||||
Every part at its natural width: the file name at its usual flex basis (2026-10-21 user request: the header width
|
||||
decides it, never the length of the name, so a short and a long name give the same steps, Open label and widths),
|
||||
the object surface neither stretched nor shrunk, the trailing area without its auto margin. */
|
||||
.hm-sw-viewer-header[data-measuring] .hm-sw-viewer-header__document { flex: 0 0 clamp(140px, calc(45cqw - 256px), 360px); }
|
||||
.hm-sw-viewer-header[data-measuring] .hm-sw-viewer-header__document { flex: 0 0 clamp(80px, min(45cqw - 256px, 100cqw - 780px), 360px); }
|
||||
.hm-sw-viewer-header[data-measuring] .hm-sw-viewer-header__object { flex: 0 0 auto; }
|
||||
.hm-sw-viewer-header[data-measuring] .hm-sw-viewer-header__actions { margin-left: 0; }
|
||||
/* While the logo menu or the document list is open, the header takes the viewer dropdown layer (SWViewerMenu, 60) so
|
||||
@@ -403,8 +407,11 @@ button.hm-sw-viewer-header__brand { text-align: left; }
|
||||
not-selected one has a green top gloss. Every color is a --hm-sw-viewer-tab(s)-* token (see the token block).
|
||||
Width (2026-10-23, kept): equal columns (1fr: each at least the widest name + count, so they never overlap). A group
|
||||
holding only the tabs starts at that natural width and takes the object's free room up to
|
||||
--hm-sw-viewer-tabs-max-width (the container is the group face then); with less room it stays narrower, and below
|
||||
the natural width the measured steps move it into More as before (`data-measuring` uses the natural width). */
|
||||
--hm-sw-viewer-tabs-max-width (the container is the group face then); with less room it stays narrower.
|
||||
2026-10-26 (user request): below the natural width the tabs narrow (names, then counts ellipsized) down to
|
||||
--hm-sw-viewer-tabs-min-width instead of moving into More. The group's own width is that minimum and it grows into
|
||||
the free room, so the file name keeps its width; the measurement uses the natural width until every icon step is
|
||||
applied, then the minimum (`data-measuring="min"`, SWViewerHeader). */
|
||||
.hm-sw-viewer-header__tabs {
|
||||
position: relative;
|
||||
display: grid;
|
||||
@@ -431,7 +438,9 @@ button.hm-sw-viewer-header__brand { text-align: left; }
|
||||
.hm-sw-viewer-header__tabs::after { content: ""; position: absolute; z-index: 1; inset: 0; border-radius: var(--hm-sw-viewer-tabs-radius); box-shadow: var(--hm-sw-viewer-tabs-shadow); pointer-events: none; }
|
||||
/* A group holding only the tabs: the container is its face (no field face around it, as in Figma: the tabs sit on
|
||||
the header object surface). */
|
||||
.hm-sw-viewer-header__group:has(> .hm-sw-viewer-header__tabs:only-child) { flex: 1 0 auto; box-sizing: border-box; max-width: var(--hm-sw-viewer-tabs-max-width); min-height: 0; padding: 0; border: 0; background: none; box-shadow: none; }
|
||||
.hm-sw-viewer-header__group:has(> .hm-sw-viewer-header__tabs:only-child) { flex: 1 0 auto; box-sizing: border-box; width: var(--hm-sw-viewer-tabs-min-width); max-width: var(--hm-sw-viewer-tabs-max-width); min-height: 0; padding: 0; border: 0; background: none; box-shadow: none; }
|
||||
.hm-sw-viewer-header[data-measuring] .hm-sw-viewer-header__group:has(> .hm-sw-viewer-header__tabs:only-child) { flex: 0 0 auto; width: max-content; }
|
||||
.hm-sw-viewer-header[data-measuring="min"] .hm-sw-viewer-header__group:has(> .hm-sw-viewer-header__tabs:only-child) { width: var(--hm-sw-viewer-tabs-min-width); }
|
||||
.hm-sw-viewer-header__group > .hm-sw-viewer-header__tabs:only-child { flex: 1 1 auto; }
|
||||
/* 2026-10-23 user value: the tabs-only group that starts the object surface gets 1px more on its right (the buttons
|
||||
give it back in their side padding). Other groups keep the object gap. */
|
||||
@@ -475,18 +484,20 @@ button.hm-sw-viewer-header__brand { text-align: left; }
|
||||
.hm-sw-viewer-header .hm-sw-viewer-header__tab[data-state="active"] { --hm-sw-viewer-tab-border-face: var(--hm-sw-viewer-tab-active-border); color: var(--hm-sw-viewer-tab-active-text); }
|
||||
.hm-sw-viewer-header__count { font-size: var(--hm-sw-viewer-label-size); font-weight: 400; font-variant-numeric: tabular-nums; }
|
||||
/* In a tab the count has the name's size (Figma: 14px Regular) and color. */
|
||||
.hm-sw-viewer-header__tab .hm-sw-viewer-header__count { flex: 0 0 auto; font-size: inherit; }
|
||||
.hm-sw-viewer-header__tab .hm-sw-viewer-header__count { flex: 0 1 auto; min-width: 0; overflow: clip; overflow-clip-margin: var(--hm-sw-header-text-outline-room); text-overflow: ellipsis; white-space: nowrap; font-size: inherit; }
|
||||
/* Long names (2026-10-23 user decision): the name gives way first with an ellipsis, the count keeps its width; the
|
||||
outline may paint 2px outside the clipped name (as the file name). The name counts at most
|
||||
--hm-sw-viewer-tab-name-basis in the tabs' natural width (width + max-width: max-content: shorter names keep their own
|
||||
width), so a long name changes neither the tabs' width nor the measured steps of the other items; it grows (flex-grow,
|
||||
up to its text) only into room the box has anyway. */
|
||||
.hm-sw-viewer-header .hm-sw-viewer-header__tab { min-width: 0; }
|
||||
up to its text) only into room the box has anyway.
|
||||
2026-10-26 (narrow tabs): the name gives way first (large shrink weight) down to its first letter + ellipsis, then the
|
||||
count ellipsizes too; whatever is still too wide is clipped at the button edge. */
|
||||
.hm-sw-viewer-header .hm-sw-viewer-header__tab { min-width: 0; overflow: clip; }
|
||||
.hm-sw-viewer-header__tab-name {
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1000 auto;
|
||||
width: var(--hm-sw-viewer-tab-name-basis);
|
||||
max-width: max-content;
|
||||
min-width: 0;
|
||||
min-width: 1.6em;
|
||||
overflow: clip;
|
||||
overflow-clip-margin: var(--hm-sw-header-text-outline-room);
|
||||
text-overflow: ellipsis;
|
||||
@@ -503,6 +514,7 @@ button.hm-sw-viewer-header__brand { text-align: left; }
|
||||
min-height: var(--hm-sw-viewer-control-height);
|
||||
padding-inline: 8px;
|
||||
}
|
||||
.hm-sw-viewer-header__switch-label-short { display: none; }
|
||||
.hm-sw-viewer-header__switch-label { color: var(--hm-sw-viewer-muted); font-size: var(--hm-sw-viewer-label-size); font-weight: 400; line-height: 1; white-space: nowrap; }
|
||||
|
||||
.hm-sw-viewer-header :is(.hm-sw-viewer-header__button, .hm-sw-viewer-header__brand, .hm-sw-viewer-switch):focus-visible {
|
||||
@@ -668,6 +680,14 @@ button.hm-sw-viewer-header__brand { text-align: left; }
|
||||
@container hm-sw-viewer (max-width: 559.98px) {
|
||||
.hm-sw-viewer-header__document { padding-right: 8px; }
|
||||
.hm-sw-viewer-status__label { display: none; }
|
||||
/* 2026-10-26 (user request: every menu stays in the header): the file name goes down to 56px and a switch with a
|
||||
`shortLabel` shows it as its caption (어두운 배경 → 배경); the full label stays its accessible name. */
|
||||
.hm-sw-viewer-header__document, .hm-sw-viewer-header[data-measuring] .hm-sw-viewer-header__document { flex-basis: 56px; }
|
||||
.hm-sw-viewer-header__switch-label[data-short] > .hm-sw-viewer-header__switch-label-full { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||
.hm-sw-viewer-header__switch-label[data-short] > .hm-sw-viewer-header__switch-label-short { display: inline; }
|
||||
.hm-sw-viewer-header__switch-field { padding-inline: 1px; }
|
||||
/* The Model / Layout counts are hidden (2026-10-26 user request); the names stay, ellipsized. */
|
||||
.hm-sw-viewer-header__tab .hm-sw-viewer-header__count { display: none; }
|
||||
}
|
||||
|
||||
/* ---------- Switch (header / panel faces) ---------- */
|
||||
|
||||
Reference in new issue
Block a user