feat(sw): measured header compaction, gauge recolor, popup menu and story cleanups
Deploy Storybook / deploy (push) Successful in 1m12s

- SWHeader (760-1799px): pictures hide one at a time by measured room (치수 아이콘, 선 그림,
  글꼴명, 색상 칩, 레이어 상태 아이콘), then the file name narrows 310 -> 96px, then values
  ellipsize; re-fit after web fonts load. 가중치/스케일 columns grow for longer values.
- Gauge style B / property window range: Figma controll_bar_new 633:77827 colors, ring and
  shadow on the previous size (6px track, 14px thumb).
- Property window: editable text values (e.g. 하이퍼링크) get the editing face like numbers.
- SW Header › Logo: same cases, order and layout as SW Viewer › Logo (ID kept).
- Storybook inner: 80vw from 1920px. Responsive More 1920px rows use the page's usable width
  (container sw-story-page) and span 1920px when there is room.
- SW/Popup sidebar: Property Panel only; Custom Content / Multiple Popups tagged verification.
- SW Viewer Settings Popup: 축척 지정 demo popup removed, frame 620px.
- Tests: inventory / simplification sidebar expectations, Popup-only unit select check.

Verified: check:ai-docs PASS, changed test syntax, headless Chrome checks of header compaction,
Responsive 1920 rows (1920 hidden / 2560, 3840 shown), Popup sidebar, Settings Popup.
Not verified: integrated-visual baselines (not updated), full regression, changed tests not run.
This commit is contained in:
shkim4 committed 2026-10-22 20:50:39 +09:00
1 parent c1549b2b4f
commit 6d46289fc5
13 files changed
+133 -94

No files matched your search

@@ -43,6 +43,13 @@ const subControls: readonly SWHeaderSubControlId[] = ["documentNew", "documentOp
const isMenu = (id: string): id is SWHeaderMenuId => id in menuSlot
/** Header width from which nothing moves into More (2026-10-21): the header compresses instead. Below it, as before. */
const MORE_BELOW = 760
/** File name width from 760px up: Figma 310px, smallest 96px (sw.css .hm-sw-header__document). */
const DOCUMENT_MAX = 310
const DOCUMENT_MIN = 96
/** Field pictures that hide one at a time while the header lacks room (sw.css [data-compact]). */
const COMPACT_LEVELS = 5
/** From this header width the Figma field boxes apply (sw.css "Responsive"). */
const FIGMA_LAYOUT_FROM = 1800
/**
* Order in which visible parts move into More when the measured object area has no room left (on top of the
@@ -283,6 +290,43 @@ export const SWHeader = forwardRef<HTMLElement, SWHeaderProps>(function SWHeader
}
})
// 2026-10-22 (user request): from 760px to 1800px the space left decides, not fixed widths. While the object area lacks
// room, the field pictures hide one at a time (data-compact 1-5: 치수 아이콘, 선 그림, 글꼴명, 색상 칩, 레이어 상태
// 아이콘, sw.css), then the file name gives way (310 -> 96px), then the values ellipsize. With room to spare the
// pictures stay and the file name keeps 310px.
// Measured on every render and again once web fonts finish loading (text widths change, the header size does not).
const fitFieldsRef = useRef(() => {})
fitFieldsRef.current = () => {
const header = fieldsRef.current?.closest<HTMLElement>(".hm-sw-header")
const area = panelAnchorRef.current
const fields = fieldsRef.current
const documentBox = header?.querySelector<HTMLElement>(":scope > .hm-sw-header__bar > .hm-sw-header__document")
if (!header || !area || !fields) return
const reset = () => { fields.removeAttribute("data-compact"); documentBox?.style.removeProperty("flex-basis") }
// From 1800px the fields are the fixed Figma boxes (pictures free no room) and the file name keeps its CSS clamp.
if (controls !== undefined || headerWidth < MORE_BELOW || headerWidth >= FIGMA_LAYOUT_FROM) { reset(); return }
documentBox?.style.setProperty("flex-basis", `${DOCUMENT_MAX}px`)
let level = 0
let room = 0
for (; level <= COMPACT_LEVELS; level++) {
fields.setAttribute("data-compact", String(level))
room = measureRoom(header, area)
if (room >= 0) break
}
if (level > COMPACT_LEVELS) fields.setAttribute("data-compact", String(COMPACT_LEVELS))
if (room < 0 && documentBox) documentBox.style.setProperty("flex-basis", `${Math.max(DOCUMENT_MIN, DOCUMENT_MAX + room)}px`)
}
useLayoutEffect(() => fitFieldsRef.current())
useEffect(() => {
const fonts = typeof document === "undefined" ? undefined : document.fonts
if (!fonts) return
let active = true
const refit = () => { if (active) fitFieldsRef.current() }
fonts.addEventListener("loadingdone", refit)
void fonts.ready.then(refit)
return () => { active = false; fonts.removeEventListener("loadingdone", refit) }
}, [])
// Logo (home) menu: SWHeader owns its open state; a logo click toggles it instead of calling onHomeClick.
const homeMenuId = useId()
const homeButtonRef = useRef<HTMLElement | null>(null)
@@ -241,7 +241,9 @@ export function PropertyEditor({ property, onChange, menus }: { property: SWHead
<div className="hm-sw-property__row" data-property-id={property.id} data-type={type} data-disabled={disabled || undefined}>
<label htmlFor={inputId} title={property.name}>{property.name}</label>
{picker ? <SWHeaderPropertyPickerField picker={picker} property={property} id={inputId} editable={editable} menus={menus} commit={commit} parse={parse} /> : (
<div className="hm-sw-property__value" data-field={field} data-on={field && editable && type === "number" ? "edit" : undefined}
<div className="hm-sw-property__value" data-field={field}
// 2026-10-22: editable text values (e.g. 하이퍼링크) get the same editing face as numbers.
data-on={field && editable && (type === "number" || type === "text") ? "edit" : undefined}
data-readonly={editable ? undefined : "true"}
onMouseDown={field ? focusInput : undefined}>
{type === "select" ? (
+40 -28
View File
@@ -105,14 +105,17 @@
--hm-sw-property-field-open-background: #211d13;
--hm-sw-property-field-editing-unit-color: #fff;
--hm-sw-property-field-open-unit-color: #d4cbbd;
/* Gauge (user-supplied Figma SVGs, 2026-10-19). Fill = Rectangle 6926: 6px pill, 1px black stroke,
vertical #8E0000 / #FF4242 37% / #E40000 76% / #1E0000 inside the stroke; its right end sits under
the thumb. Unfilled track = beige measured row by row from the user's reference image.
Thumb = Rectangle 6915: 14px conic + 2px 40% ring; its two drop shadows are CSS. */
--hm-sw-property-range-fill: linear-gradient(#000 0 1px, #8e0000 1px, #ff4242 2.46px, #e40000 3.99px, #1e0000 4.94px, #000 4.94px);
--hm-sw-property-range-fill-edge: #000;
--hm-sw-property-range-track: linear-gradient(#aea28b 0 1px, #c3b9a4 1px 2px, #c7bda9 2px 4px, #b7ac96 4px 5px, #ac9e84 5px);
--hm-sw-property-range-highlight: #eadec8;
/* Gauge (Figma 이지빔-SW controll_bar_new 633:77827, 2026-10-22): its colors on the previous size (user request:
6px track, 14px thumb kept). Track: #C7BDA9 face, 1px #8A7B5E border, 1px 1px 1px #FCF0D8 shadow. Fill over it:
1px #7B6D53 border, vertical #FF5656 / #E60403 45.667% / #B41010 93.167% inside the border; its right end is
2px into the thumb. Thumb: 2px #FF5656 ring, red conic face, 0 1px 2px rgba(0,0,0,.3). */
--hm-sw-property-range-fill: linear-gradient(#7b6d53 0 1px, #ff5656 1px, #e60403 2.827px, #b41010 4.727px 5px, #7b6d53 5px);
--hm-sw-property-range-fill-edge: #7b6d53;
--hm-sw-property-range-track: linear-gradient(#8a7b5e 0 1px, #c7bda9 1px 5px, #8a7b5e 5px);
--hm-sw-property-range-track-edge: #8a7b5e;
--hm-sw-property-range-highlight: #fcf0d8;
--hm-sw-property-range-thumb-ring: #ff5656;
--hm-sw-property-range-thumb: conic-gradient(from 180deg, #bf0000 -9.164%, #ab0000 3.558%, #d50000 8.453%, #f00 13.348%, #f00 25.617%, #ff0b0b 26.222%, #ff1616 26.826%, #ff2c2c 28.034%, #ff4343 29.243%, #ff5959 30.452%, #ff8585 32.869%, #ffb1b1 35.286%, #ff9090 43.686%, #ffa7a7 57.27%, #f27676 63.699%, #ec5e5e 66.913%, #e54545 70.128%, #d63434 72.887%, #c62323 75.646%, #b61111 78.405%, #ae0909 79.785%, #a60000 81.164%, #bf0000 90.836%, #ab0000 103.56%);
/* SWSlider appearance "panel" (2026-10-20): the gauge the SW popup used so far (HM UI Slider: 6px #E0DACE
track, #266651 range, 18px white thumb with a 2px #266651 ring), the same shape as SWViewerSlider. Existing
values only (#E0DACE = popup scrollbar track, #266651 = viewer panel accent). Appearance "property" uses the
@@ -666,7 +669,8 @@
flex row a max-content 6.4px under its content (cutting "Continuous" in the 284px field); grid tracks size the same in
Chromium and Firefox (2026-10-20). min-width 28px: a shrunk representative field (narrow More slot) shows an ellipsis,
never a cut glyph. */
.hm-sw-header__line-values { display: grid; grid-template-columns: minmax(28px, auto) 38px 38px; gap: 12px; min-width: 0; }
/* 가중치 / 스케일 keep the Figma 38px and grow for a longer value (e.g. ByLayer) when there is room (2026-10-22). */
.hm-sw-header__line-values { display: grid; grid-template-columns: minmax(28px, auto) minmax(38px, auto) minmax(38px, auto); gap: 12px; min-width: 0; }
.hm-sw-header__font-name {
flex: 0 0 auto;
@@ -1650,24 +1654,26 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
/* Property window gauge, also SWSlider appearance="property" (2026-10-20, same rules; the 86px width is the property row only). */
.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider) { flex: 0 0 86px; width: 86px; }
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"]) { appearance: none; height: 16px; margin: 0; background: transparent; cursor: pointer; }
/* --hm-sw-range-fill (0-1) is set from the value; the red ends under the thumb centre (14px thumb).
/* --hm-sw-range-fill (0-1) is set from the value; the red ends 2px into the 14px thumb.
The 3px radius rounds the fill's left cap; the first layer is its left stroke. Track and thumb are
shared values, painted once per engine: WebKit/Blink track + thumb, Gecko track + thumb (Gecko's
::-moz-range-progress is kept empty so the fill is never painted twice). */
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"]) {
--hm-sw-range-fill-width: calc(7px + (100% - 14px) * var(--hm-sw-range-fill, 0));
/* 633:77827: the fill ends 2px into the thumb (fill 105 / thumb left 103 in Figma); 14px thumb (previous size). */
--hm-sw-range-fill-width: calc(2px + (100% - 14px) * var(--hm-sw-range-fill, 0));
--hm-sw-range-track-paint:
linear-gradient(90deg, var(--hm-sw-property-range-fill-edge) 0 1px, transparent 1px) 0 0 / var(--hm-sw-range-fill-width) 100% no-repeat,
var(--hm-sw-property-range-fill) 0 0 / var(--hm-sw-range-fill-width) 100% no-repeat,
linear-gradient(90deg, var(--hm-sw-property-range-track-edge) 0 1px, transparent 1px calc(100% - 1px), var(--hm-sw-property-range-track-edge) calc(100% - 1px)),
var(--hm-sw-property-range-track);
--hm-sw-range-thumb-paint: url("../assets/property-range-thumb.svg") center / 14px 14px no-repeat;
--hm-sw-range-thumb-shadow: 0 0 3px #1c1a16, 0 2px 2px rgba(28, 26, 22, .4);
--hm-sw-range-thumb-paint: var(--hm-sw-property-range-thumb);
--hm-sw-range-thumb-shadow: 0 1px 2px rgba(0, 0, 0, .3);
}
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"])::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: var(--hm-sw-range-track-paint); box-shadow: 0 1px 0 var(--hm-sw-property-range-highlight); }
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"])::-webkit-slider-thumb { appearance: none; width: 14px; height: 14px; margin-top: -4px; border: 0; border-radius: 50%; background: var(--hm-sw-range-thumb-paint); box-shadow: var(--hm-sw-range-thumb-shadow); }
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"])::-moz-range-track { box-sizing: border-box; height: 6px; border: 0; border-radius: 3px; background: var(--hm-sw-range-track-paint); box-shadow: 0 1px 0 var(--hm-sw-property-range-highlight); }
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"])::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: var(--hm-sw-range-track-paint); box-shadow: 1px 1px 1px var(--hm-sw-property-range-highlight); }
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"])::-webkit-slider-thumb { appearance: none; box-sizing: border-box; width: 14px; height: 14px; margin-top: -4px; border: 2px solid var(--hm-sw-property-range-thumb-ring); border-radius: 50%; background: var(--hm-sw-range-thumb-paint); box-shadow: var(--hm-sw-range-thumb-shadow); }
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"])::-moz-range-track { box-sizing: border-box; height: 6px; border: 0; border-radius: 3px; background: var(--hm-sw-range-track-paint); box-shadow: 1px 1px 1px var(--hm-sw-property-range-highlight); }
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"])::-moz-range-progress { height: 0; background: transparent; }
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"])::-moz-range-thumb { box-sizing: border-box; width: 14px; height: 14px; border: 0; border-radius: 50%; background: var(--hm-sw-range-thumb-paint); box-shadow: var(--hm-sw-range-thumb-shadow); }
:is(.hm-sw-property__value > input[type="range"]:not(.hm-sw-slider), .hm-sw-slider[data-appearance="property"])::-moz-range-thumb { box-sizing: border-box; width: 14px; height: 14px; border: 2px solid var(--hm-sw-property-range-thumb-ring); border-radius: 50%; background: var(--hm-sw-range-thumb-paint); box-shadow: var(--hm-sw-range-thumb-shadow); }
/* SWSlider (2026-10-20): one native range, two looks. Fills the row by default (the host may set a width).
appearance="panel" is the gauge the SW popup used (also SWViewerSlider): the range is painted up to
--hm-sw-range-fill of the track. */
@@ -1900,11 +1906,11 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
From 760px up (2026-10-21 user decision; the SW Footer story widths) nothing moves into More: the five menus 레이어 · 색상 · 선스타일 ·
문자스타일 · 치수스타일 always stay in the header with their titles, and the More button is not shown. As it narrows:
>= 1800 Figma layout. 1920 -> 1800 only the file name shrinks (310 -> 190).
< 1800 the 그리기모드 text hides, the line-type / dimension pictures hide and the color chip is 24px (fields take
their content width); the file name gives way first (down to 96px, ellipsis), then the field values
shrink with an ellipsis, each by its own width.
< 1080 window move area 88 -> 24 (window 240 -> 176); layer-state icons, the color chip and the 문자스타일
font name hide, field insets 6px.
< 1800 the 그리기모드 text hides; fields take their content width and shrink with an ellipsis, each by its own width.
< 1080 window move area 88 -> 24 (window 240 -> 176), field insets 6px.
Measured (2026-10-22 user request, SWHeader data-compact): from 760px to 1800px, while the object area lacks
room the field pictures hide one at a time — 치수 아이콘, 선 그림, 글꼴명, 색상 칩, 레이어 상태 아이콘 — then the file
name gives way (310 -> 96px), then the values ellipsize. With room to spare nothing hides.
<= 900 the 선스타일 field shows 선스타일 only (가중치 / 스케일 hide).
Below 760px (phones, e.g. 390) the former tiers are unchanged: parts move into More (선/문자/치수 스타일, 그리기모드,
속성Bar/속성창, 색상, then 인쇄/팝업 가이드 below 640), and below 520 the layer-state icons hide.
@@ -1926,10 +1932,9 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
.hm-sw-header__fields > .hm-sw-header__control:is([data-control="print"], [data-control="popupGuide"]) { flex-shrink: 0; }
.hm-sw-header__layer > .hm-sw-header__field--layer { width: 100%; }
.hm-sw-header__field :is(.hm-sw-header__line-values, .hm-sw-header__font-name) { min-width: 0; }
/* Tighter fields: smaller insets, a 24px color chip, no line-type / dimension pictures (the value names stay). */
/* Tighter fields: smaller insets, a 24px color chip (the pictures hide by measurement, see data-compact below). */
.hm-sw-header__fields .hm-sw-header__field { padding: 0 8px; gap: 8px; }
.hm-sw-header__fields .hm-sw-header__chip { flex-basis: 24px; width: 24px; }
.hm-sw-header__fields :is(.hm-sw-header__linetype, .hm-sw-header__dimension-icon) { display: none; }
.hm-sw-header__fields .hm-sw-header__line-values { gap: 8px; }
/* Open dropdown (2026-10-21 user request): the fields are narrower than their lists here, so the yellow ring stays
inside each button and the list gets its own ring, as a shifted list (치수스타일) already does. */
@@ -1951,11 +1956,10 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
.hm-sw-header__document { flex-basis: clamp(96px, calc(100cqw - 1200px - var(--hm-sw-header-leading-width, 0px)), 310px); }
}
@container hm-sw-header (min-width: 760px) and (max-width: 1079.98px) {
/* Values need the room (1024px, 2026-10-21 user request): the layer-state icons, the color chip and the font name
beside 문자스타일 hide, insets 6px, every value part may shrink (ellipsis). */
/* Values need the room (1024px, 2026-10-21 user request): insets 6px, every value part may shrink (ellipsis); the
pictures hide by measurement (data-compact). */
.hm-sw-header__fields .hm-sw-header__field { padding: 0 6px; gap: 6px; }
.hm-sw-header__fields :is(.hm-sw-header__field--layer .hm-sw-header__layer-icons, .hm-sw-header__chip, .hm-sw-header__font-name) { display: none; }
.hm-sw-header__fields .hm-sw-header__line-values { grid-template-columns: minmax(0, auto) minmax(0, 38px) minmax(0, 38px); gap: 6px; }
.hm-sw-header__fields .hm-sw-header__line-values { grid-template-columns: minmax(0, auto) minmax(0, auto) minmax(0, auto); gap: 6px; }
}
@container hm-sw-header (min-width: 760px) and (max-width: 900px) {
/* 900px and narrower (2026-10-21 user request): the 선스타일 field keeps only 선스타일 (가중치 / 스케일 hide). */
@@ -2109,6 +2113,14 @@ div.hm-sw-header__menu-row { justify-content: space-between; gap: 8px; padding-l
/* ...and the file name gives way only down to the default design's smallest width (96px, see the clamps above). */
.hm-sw-header[data-leading="open"] .hm-sw-header__fields[data-configured] > .hm-sw-header__control[data-control="document"] { min-width: 96px; }
/* Measured pictures (SWHeader data-compact, 2026-10-22 user request): from 760px to 1800px the field pictures hide one at a
time while the object area lacks room — 1 치수 아이콘, 2 선 그림, 3 글꼴명, 4 색상 칩, 5 레이어 상태 아이콘. */
.hm-sw-header__fields:is([data-compact="1"], [data-compact="2"], [data-compact="3"], [data-compact="4"], [data-compact="5"]) .hm-sw-header__dimension-icon,
.hm-sw-header__fields:is([data-compact="2"], [data-compact="3"], [data-compact="4"], [data-compact="5"]) .hm-sw-header__linetype,
.hm-sw-header__fields:is([data-compact="3"], [data-compact="4"], [data-compact="5"]) .hm-sw-header__font-name,
.hm-sw-header__fields:is([data-compact="4"], [data-compact="5"]) .hm-sw-header__chip,
.hm-sw-header__fields[data-compact="5"] .hm-sw-header__field--layer .hm-sw-header__layer-icons { display: none; }
/* Measured More (SWHeader, 2026-10-19): on top of the breakpoints, parts move into More while the object area has no
room for them and return when it has. data-measuring is set for one synchronous layout read and removed before
paint: nothing in the object area shrinks, grows or takes free space through an auto margin, so the in-flow content