diff --git a/packages/sw/src/components/sw-header/SWHeader.tsx b/packages/sw/src/components/sw-header/SWHeader.tsx index 53b53f3..cb959e9 100644 --- a/packages/sw/src/components/sw-header/SWHeader.tsx +++ b/packages/sw/src/components/sw-header/SWHeader.tsx @@ -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(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(".hm-sw-header") + const area = panelAnchorRef.current + const fields = fieldsRef.current + const documentBox = header?.querySelector(":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(null) diff --git a/packages/sw/src/components/sw-header/components/SWHeaderPropertyPanel.tsx b/packages/sw/src/components/sw-header/components/SWHeaderPropertyPanel.tsx index 9bea231..349a57f 100644 --- a/packages/sw/src/components/sw-header/components/SWHeaderPropertyPanel.tsx +++ b/packages/sw/src/components/sw-header/components/SWHeaderPropertyPanel.tsx @@ -241,7 +241,9 @@ export function PropertyEditor({ property, onChange, menus }: { property: SWHead
{picker ? : ( -
{type === "select" ? ( diff --git a/packages/sw/src/theme/sw.css b/packages/sw/src/theme/sw.css index 13b8e07..eeecaa3 100644 --- a/packages/sw/src/theme/sw.css +++ b/packages/sw/src/theme/sw.css @@ -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 diff --git a/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs b/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs index 8265e0f..540f328 100644 --- a/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs +++ b/packages/sw/tests/footer-popup-viewer-refinement.browser.mjs @@ -131,7 +131,8 @@ await goto('sw-header--property-panel-compare') const prop = await page.locator('.hm-sw-property__value > input[type="range"]').first().evaluate(e => { const r = e.getBoundingClientRect(); return [Math.round(r.width), Math.round(r.height), getComputedStyle(e).getPropertyValue('--hm-sw-range-track-paint').trim().length > 0] }) check('Slider: property window gauge keeps 86 × 16 and its paint', prop[0] === 86 && prop[1] === 16 && prop[2], prop) -// 3b. Unit select: Popup 도면 설정 = Viewer 축척 지정 (same SWSelect, same computed look in every state) +// 3b. Unit select: Popup 도면 설정 (SWSelect). 2026-10-22 (user request): the Viewer 축척 지정 demo popup was removed, +// so the Viewer side of this comparison is gone; the Popup select keeps its look and behaviour checks. const selProps = ['appearance', 'height', 'paddingLeft', 'paddingRight', 'borderTopWidth', 'borderTopColor', 'borderTopStyle', 'borderRadius', 'backgroundColor', 'color', 'fontSize', 'fontWeight', 'lineHeight', 'outlineStyle', 'outlineColor', 'outlineWidth', 'opacity', 'cursor'] const selRead = loc => loc.evaluate((e, p) => { const s = getComputedStyle(e); return p.map(k => `${k}=${s[k]}`).join(';') + `;class=${e.className};tag=${e.tagName}` }, selProps) const selStates = async loc => { const d = await selRead(loc); await loc.hover(); await page.waitForTimeout(80); const h = await selRead(loc); await loc.focus(); await page.keyboard.press('Shift+Tab'); await page.keyboard.press('Tab'); await page.waitForTimeout(80); const f = await selRead(loc); await page.mouse.move(2, 2); return { d, h, f } } @@ -141,17 +142,9 @@ const ps = await selStates(popupSel) const popupOptions = await popupSel.evaluate(e => [...e.options].map(o => o.value)) await popupSel.selectOption('m'); await page.waitForTimeout(80) const popupPicked = [(await log()).includes('unit=m'), await popupSel.inputValue()] -await goto('sw-viewer--content-variants') -const viewerSel = page.locator('.hm-sw-viewer-panel [data-sw-select="unit"]') -const vs = await selStates(viewerSel) -const viewerOptions = await viewerSel.evaluate(e => [...e.options].map(o => o.value)) -await viewerSel.selectOption('m'); await page.waitForTimeout(80) -// 2026-10-21 (user decision): the chosen value of an editable Popup select is red (#E60403) like every editable value; -// the Viewer panel keeps its own text color. Everything else of the look stays the same. -const noColor = look => look.replace(/(^|;)color=[^;]*/, '$1') -check('Select: Popup 도면 설정 단위 = Viewer 축척 지정 단위 (default · hover · focus computed look, text color apart); Popup value red', noColor(ps.d) === noColor(vs.d) && noColor(ps.h) === noColor(vs.h) && noColor(ps.f) === noColor(vs.f) && ps.d.includes('class=hm-sw-select') && ps.d.includes('color=rgb(230, 4, 3)'), { ps, vs }) -check('Select: options and selection kept (Popup mm/cm/m, Viewer mm/m), change events', JSON.stringify(popupOptions) === '["mm","cm","m"]' && JSON.stringify(viewerOptions) === '["mm","m"]' && popupPicked[0] && popupPicked[1] === 'm' && await viewerSel.inputValue() === 'm', { popupOptions, viewerOptions, popupPicked }) -await page.locator('.hm-sw-viewer-panel', { hasText: '축척 지정' }).screenshot({ path: path.join(out, 'viewer-scale-select.png') }) +// 2026-10-21 (user decision): the chosen value of an editable Popup select is red (#E60403) like every editable value. +check('Select: Popup 도면 설정 단위 is SWSelect in default · hover · focus; value red', [ps.d, ps.h, ps.f].every(look => look.includes('class=hm-sw-select')) && ps.d.includes('color=rgb(230, 4, 3)'), { ps }) +check('Select: options and selection kept (Popup mm/cm/m), change events', JSON.stringify(popupOptions) === '["mm","cm","m"]' && popupPicked[0] && popupPicked[1] === 'm', { popupOptions, popupPicked }) // 3c. Header Open hysteresis (fix A): grow 520 → 1080, Open returns once and stays; shrink: Open moves first. await goto('sw-header--document-open') diff --git a/packages/sw/tests/storybook-inventory.mjs b/packages/sw/tests/storybook-inventory.mjs index 1477a51..2f24e12 100644 --- a/packages/sw/tests/storybook-inventory.mjs +++ b/packages/sw/tests/storybook-inventory.mjs @@ -103,7 +103,9 @@ export const menuCleanupHiddenIds = ['sw-header--interactive-states', 'sw-header // Second pass (same day): SW/Theme hidden as a whole; Viewer settings-panel merged into content-variants. 'sw-header--document-open', 'sw-header--composed-controls', ...themeStoryIds, ...['menu-groups', 'states', 'dropdowns', 'variable-size', 'settings-panel'].map(id => `sw-viewer--${id}`), // 2026-10-22 (user request): SWHeader vs Viewer Header left the sidebar the same way. - 'sw-viewer--compare-with-sw-header'] + 'sw-viewer--compare-with-sw-header', + // 2026-10-22 (user request): SW/Popup keeps only Property Panel in the sidebar. + 'sw-popup--custom-content', 'sw-popup--multiple-popups'] export const renamedEntries = { 'sw-header--overview': 'SW Header', 'sw-footer--full-footer': 'SW Footer', 'sw-header--logo-slot': 'Logo', // 2026-10-22 (user request): sidebar "Logo", page title "SW Header — 로고" 'sw-header--property-panel-compare': 'Property Panel', // 2026-10-22 (user request): was "Property Panel · 4개 비교" diff --git a/packages/sw/tests/storybook-simplification.browser.mjs b/packages/sw/tests/storybook-simplification.browser.mjs index 98c1eef..7ad1fc9 100644 --- a/packages/sw/tests/storybook-simplification.browser.mjs +++ b/packages/sw/tests/storybook-simplification.browser.mjs @@ -55,7 +55,8 @@ const hidden = ['sw-header--window-move-motion', 'sw-header--k-guide', 'sw-foote check('verification Stories not in sidebar', rows.every(r => !hidden.includes(r[0])), rows.filter(r => hidden.includes(r[0]))) const popupRows = await m.locator('#storybook-explorer-tree [data-item-id]').evaluateAll(list => list.map(e => e.dataset.itemId)) const pi = popupRows.indexOf('sw-popup') -check('Popup children order: Custom Content, Multiple Popups, Property Panel', JSON.stringify(popupRows.slice(pi + 1, pi + 4)) === JSON.stringify(['sw-popup--custom-content', 'sw-popup--multiple-popups', 'sw-header--property-panel-compare']), popupRows.slice(pi + 1, pi + 4)) +// 2026-10-22 (user request): SW/Popup shows only Property Panel; Custom Content / Multiple Popups are verification. +check('Popup children: Property Panel only', JSON.stringify(popupRows.slice(pi + 1, pi + 2)) === JSON.stringify(['sw-header--property-panel-compare']) && !popupRows.some(id => id.startsWith('sw-popup--')), popupRows.slice(pi + 1, pi + 4)) const headerIds = rows.filter(r => r[0].startsWith('sw-header--')).map(r => r[0]) check('Property Panel listed once (under Popup only)', headerIds.filter(id => id === 'sw-header--property-panel-compare').length === 1) await m.locator('[data-item-id="sw-header--property-panel-compare"]').click(); await m.waitForTimeout(800) diff --git a/src/components/story-page.css b/src/components/story-page.css index 9bd6d12..c5a60d1 100644 --- a/src/components/story-page.css +++ b/src/components/story-page.css @@ -18,10 +18,11 @@ min-width: 0; margin-inline: auto; } -/* Wide screens (2026-10-22): 1440px from 1920px up, fixed beyond it (was 80vw); never wider than the page minus the +/* Wide screens (2026-10-22, user request): 80% of the viewport — 1536px at 1920px, 2048px at 2560px, 3072px at + 3840px — growing with the screen (vw, so a scrollbar does not shrink it); never wider than the page minus the gutters, so no horizontal scroll. Below 1920px the rule above applies. */ @media (min-width: 1920px) { - .sb-page > .sb-page-inner { width: min(1440px, calc(100% - 2 * var(--sb-page-gutter))); } + .sb-page > .sb-page-inner { width: min(80vw, calc(100% - 2 * var(--sb-page-gutter))); } } .sb-page .sb-page-header { diff --git a/src/sw/sw-footer-stories.css b/src/sw/sw-footer-stories.css index 483178a..48b5071 100644 --- a/src/sw/sw-footer-stories.css +++ b/src/sw/sw-footer-stories.css @@ -32,7 +32,7 @@ .sw-footer-story-inline { display: inline-flex; flex-direction: column; padding: 0; } .sw-footer-story-width + .sw-footer-story-width { margin-top: 16px; } /* The hidden 1920px row (Responsive More, narrower than 1920px) takes its gap with it. */ -@container (max-width: 1919.98px) { .sw-story-responsive-wide + .sw-footer-story-width { margin-top: 0; } } +@container sw-story-page (max-width: 1999.98px) { .sw-story-responsive-wide + .sw-footer-story-width { margin-top: 0; } } .sw-footer-story-aux-compare { display: grid; grid-template-columns: repeat(auto-fill, minmax(560px, 1fr)); gap: 16px; } .sw-footer-story-aux-row { display: flex; align-items: center; gap: 12px; margin: 0; } .sw-footer-story-aux-row figcaption { flex: 0 0 120px; font-size: 12px; color: #3d4a45; } diff --git a/src/sw/sw-header.stories.tsx b/src/sw/sw-header.stories.tsx index 878aae7..44c94e3 100644 --- a/src/sw/sw-header.stories.tsx +++ b/src/sw/sw-header.stories.tsx @@ -440,40 +440,30 @@ export function ComposedDemo() { } -/** Neutral sample logos with different ratios (no product or brand). */ -const WIDE_SAMPLE_LOGO = "data:image/svg+xml," + encodeURIComponent( - '', -) -const TALL_SAMPLE_LOGO = ( - -) +// 2026-10-22 (user request): sidebar "Logo" (ID sw-header--logo-slot kept), page title "SW Header — 로고". Same cases, +// order and layout as SW/Viewer › Logo: 로고 없음 → logoSrc → logo (element), each header fluid up to 1440px (no +// scroller). The SW Header home click (callbacks.onHomeClick) is kept. +const LOGO_CASES: { title: string; props: Partial }[] = [ + { title: "로고 없음 (기본)", props: { logoSrc: undefined, logoAlt: undefined } }, + { title: "logoSrc (이미지 URL)", props: { logoSrc: placeholderLogo, logoAlt: "임시 로고" } }, + { title: "logo (요소)", props: { logo: , logoAlt: "예시 로고" } }, +] -// 2026-10-22 (user request): sidebar "Logo" (ID sw-header--logo-slot kept), page title "SW Header — 로고", -// frames fit the page (no scroller). export const LogoSlot: Story = { name: "Logo", parameters: { controls: { disable: true } }, render: () => ( - - - - - - - - - - - - - + +
+ {LOGO_CASES.map(row => ( + +
+ +
+
+ ))} +
), } diff --git a/src/sw/sw-popup.stories.tsx b/src/sw/sw-popup.stories.tsx index 27d130a..cbc23f9 100644 --- a/src/sw/sw-popup.stories.tsx +++ b/src/sw/sw-popup.stories.tsx @@ -164,7 +164,9 @@ function PopupPage() { ) } -export const CustomContent: Story = { name: "Custom Content", render: () => } +// 2026-10-22 (user request): SW/Popup keeps only Property Panel in the sidebar; these two stay for the browser tests +// (verification tag: ID and direct URL kept). +export const CustomContent: Story = { name: "Custom Content", tags: ["verification"], render: () => } function StackPage() { const [log, record] = useLog() @@ -190,4 +192,4 @@ function StackPage() { ) } -export const MultiplePopups: Story = { name: "Multiple Popups", render: () => } +export const MultiplePopups: Story = { name: "Multiple Popups", tags: ["verification"], render: () => } diff --git a/src/sw/sw-stories.css b/src/sw/sw-stories.css index 06b7917..e7e06b8 100644 --- a/src/sw/sw-stories.css +++ b/src/sw/sw-stories.css @@ -1,9 +1,12 @@ /* Story canvas only: frames that simulate a header width. Never styles the header itself. */ .sw-story-scroller { max-width: 100%; overflow-x: auto; } -/* Responsive More pages (2026-10-22): the 1920px row shows only when the content Inner itself is 1920px or wider - (container width, not the browser width); otherwise the whole row — title, header, background, margin — is hidden. */ +/* Responsive More pages (2026-10-22): the 1920px row shows when the page has 1920px of usable width — the page + (container sw-story-page, not the browser) minus its 40px gutters, so 2000px — and then reaches out of the 80vw Inner + evenly to its full 1920px; with less room the whole row — title, header, background, margin — is hidden. */ .sw-story-responsive { container-type: inline-size; } -@container (max-width: 1919.98px) { .sw-story-responsive-wide { display: none; } } +.sb-page:has(.sw-story-responsive) { container: sw-story-page / inline-size; } +.sw-story-responsive-wide { width: 1920px; max-width: none; margin-inline: calc((100% - 1920px) / 2); } +@container sw-story-page (max-width: 1999.98px) { .sw-story-responsive-wide { display: none; } } .sw-story-frame { background: #060606; } /* Shared drawing-area canvas of the SW Stories (2026-10-19): the SW/Viewer black grid — 40px cells, 1px lines of HM green #266651 at 18%, on #0A0A0B. Used behind the SW Header work area and dropdown room and the viewer frames. */ diff --git a/src/sw/sw-viewer-stories.css b/src/sw/sw-viewer-stories.css index ae7e7c7..93efb1f 100644 --- a/src/sw/sw-viewer-stories.css +++ b/src/sw/sw-viewer-stories.css @@ -42,7 +42,6 @@ .sw-viewer-story-props dd { margin: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--hm-sw-viewer-panel-text); user-select: all; } .sw-viewer-story-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 40px 0; text-align: center; font-size: 12px; color: var(--hm-sw-viewer-panel-label); } .sw-viewer-story-steps { margin: 0; padding-left: 20px; font-size: 14px; } -.sw-viewer-story-reopen { position: absolute; left: 712px; top: 16px; padding: 6px 10px; border-radius: 4px; background: #f5f0e5; font-size: 12px; } .sw-viewer-story-context { display: flex; align-items: center; justify-content: center; height: 160px; border: 1px dashed #9a9284; border-radius: 4px; background: #f5f0e5; color: #3e3523; font-size: 13px; } .sw-viewer-story-mode { display: flex; gap: 16px; margin-bottom: 8px; font-size: 13px; } /* Room below a single header row so its logo menu / document list are not cut by the scroller. */ diff --git a/src/sw/sw-viewer.stories.tsx b/src/sw/sw-viewer.stories.tsx index b2ba6a6..07f377a 100644 --- a/src/sw/sw-viewer.stories.tsx +++ b/src/sw/sw-viewer.stories.tsx @@ -1,7 +1,7 @@ import { useRef, useState, type ReactNode } from "react" import type { Meta, StoryObj } from "@storybook/react-vite" import { - SWHeader, SWHeaderWindowControls, SWSelect, SWViewerButton, SWViewerField, SWViewerHeader, SWViewerHint, SWViewerInput, SWViewerMenu, SWViewerPanel, SWViewerSection, SWViewerSwitch, + SWHeader, SWHeaderWindowControls, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection, type SWViewerHeaderGroup, type SWViewerMenuEntry, } from "@hm-design/sw" import "@hm-design/sw/styles.css" @@ -272,29 +272,19 @@ export const SettingsPanel: Story = { export const ContentVariants: Story = { // 2026-10-21: the one settings-popup Story (former 설정창 and 같은 프레임, 다른 내부 콘텐츠 pages); ID kept. + // 2026-10-22 (user request): the 축척 지정 demo popup was removed. name: "Settings Popup", render: () => { const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["layers", "properties", "options"] }) - const [inputOpen, setInputOpen] = useState(true) - const [scale, setScale] = useState("1:500") - const [snap, setSnap] = useState(true) return ( - }> + {/* 2026-10-22: 620px fits the tallest popup (옵션, 520px) now that 축척 지정 is gone. */} + }> - setInputOpen(false)} width={300} height="auto" resizable={false} - footer={<> setInputOpen(false)}>취소 { demo.record(`scale=${scale}, snap=${snap}`); setInputOpen(false) }}>적용}> - - {id => setScale(event.currentTarget.value)} style={{ width: 120 }} />} - {id => } - {id => demo.record(`unit=${value}`)} data-sw-select="unit" />} - - - {!inputOpen && } )