diff --git a/packages/sw/src/theme/sw-viewer.css b/packages/sw/src/theme/sw-viewer.css index d3dddf8..27d9e14 100644 --- a/packages/sw/src/theme/sw-viewer.css +++ b/packages/sw/src/theme/sw-viewer.css @@ -164,9 +164,11 @@ button.hm-sw-viewer-header__brand { text-align: left; } display: flex; 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). */ - flex: 0 1000000 clamp(140px, 25cqw, 360px); - width: clamp(220px, 25cqw, 360px); + 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); min-width: 56px; height: calc(var(--hm-sw-viewer-height) - 2px); padding: 0 16px 0 24px; @@ -312,7 +314,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, 25cqw, 360px); } +.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__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 diff --git a/packages/sw/tests/overview-firefox.browser.mjs b/packages/sw/tests/overview-firefox.browser.mjs index bdf21f1..d55623b 100644 --- a/packages/sw/tests/overview-firefox.browser.mjs +++ b/packages/sw/tests/overview-firefox.browser.mjs @@ -47,10 +47,11 @@ try { await evaluate('(()=>{const s=document.querySelector("select");s.value="expanded";s.dispatchEvent(new Event("change",{bubbles:true}));return true})()');await settle() await evaluate('(()=>{const b=document.querySelector("[data-property-panel-trigger]");if(b.getClientRects().length)b.click();else{document.querySelector(".hm-sw-header__more-trigger").click()}return true})()');await settle() await evaluate('(()=>{const m=[...document.querySelectorAll("[role=menuitemcheckbox]")].find(e=>e.textContent.trim()==="속성창");if(m)m.click();return true})()');await settle() - const popup=await data('(()=>{const e=document.querySelector(".hm-sw-property"),r=e.getBoundingClientRect();return {width:r.width,height:r.height,focus:document.activeElement===e}})()') - check(`Firefox ${width}: full original popup 314x754`,popup.width===314&&popup.height===754,popup) - const bottom=await data('(()=>{const z=document.querySelector(".hm-sw-property [data-property-id=z] input"),s=document.querySelector("[data-preview-stage]");z.scrollIntoView({block:"nearest",inline:"nearest"});z.focus();const r=z.getBoundingClientRect();return {top:s.scrollTop,visible:document.elementFromPoint(r.x+r.width/2,r.y+r.height/2)===z}})()') - check(`Firefox ${width}: popup bottom reachable and focusable`,bottom.top>0&&bottom.visible,bottom) + // 2026-10-26 (user request): the popup is clamped to the Stage room, so the Stage gets no scrollbar. + const popup=await data('(()=>{const e=document.querySelector(".hm-sw-property"),r=e.getBoundingClientRect(),c=getComputedStyle(document.querySelector("[data-preview-stage]"));return {width:r.width,height:r.height,limit:parseFloat(c.getPropertyValue("--sw-preview-stage-height"))-parseFloat(c.getPropertyValue("--sw-preview-panel-top"))-4,focus:document.activeElement===e}})()') + check(`Firefox ${width}: popup 314 x min(754, Stage room)`,popup.width===314&&Math.abs(popup.height-Math.min(754,popup.limit))<.5,popup) + const bottom=await data('(()=>{const z=document.querySelector(".hm-sw-property [data-property-id=z] input"),s=document.querySelector("[data-preview-stage]");z.scrollIntoView({block:"nearest",inline:"nearest"});z.focus();const r=z.getBoundingClientRect();return {top:s.scrollTop,stageScroll:s.scrollHeight>s.clientHeight,visible:document.elementFromPoint(r.x+r.width/2,r.y+r.height/2)===z}})()') + check(`Firefox ${width}: popup bottom reachable and focusable without Stage scroll`,!bottom.stageScroll&&bottom.visible,bottom) await key('\uE00C');await settle() check(`Firefox ${width}: Escape closes popup and restores focus`,await evaluate('!document.querySelector(".hm-sw-property")&&document.activeElement.matches("[data-property-panel-trigger],.hm-sw-header__more-trigger")')) await writeFile(path.join(out,`overview-firefox-${width}.png`),Buffer.from((await send('browsingContext.captureScreenshot',{context})).data,'base64')) diff --git a/packages/sw/tests/overview.browser.mjs b/packages/sw/tests/overview.browser.mjs index 966cbe6..cba946c 100644 --- a/packages/sw/tests/overview.browser.mjs +++ b/packages/sw/tests/overview.browser.mjs @@ -211,7 +211,9 @@ try { await page.getByLabel('선택 상태',{exact:true}).selectOption(selection) if(await page.locator('.hm-sw-property').count()===0)await panelButton() const box=await page.locator('.hm-sw-property').boundingBox() - check(`original panel size ${selection}:314x${height}`,box.width===314&&box.height===height,box) + // 2026-10-26 (user request): no Stage scrollbar when the panel opens; a panel taller than the Stage room is clamped. + const room=await stage.evaluate(e=>{const s=getComputedStyle(e);return {limit:parseFloat(s.getPropertyValue('--sw-preview-stage-height'))-parseFloat(s.getPropertyValue('--sw-preview-panel-top'))-4,scrollHeight:e.scrollHeight,height:e.clientHeight}}) + check(`panel size ${selection}:314 x min(${height}, Stage room), no Stage scroll`,box.width===314&&Math.abs(box.height-Math.min(height,room.limit))<.5&&room.scrollHeight<=room.height,{box,room}) } await page.screenshot({path:path.join(out,'overview-after-expanded.png')}) await stage.evaluate(e=>{e.scrollTop=e.scrollHeight}) @@ -219,11 +221,11 @@ try { check('expanded popup bottom accessible through internal scroll',await panelInput('z').inputValue()==='5') await page.screenshot({path:path.join(out,'overview-after-scrolled.png')}) await go();await page.screenshot({path:path.join(out,'overview-after.png')}) - // A small Inner keeps native component dimensions; the Stage, rather than the browser document, scrolls. + // A small Inner keeps the native Header width; the Stage, rather than the browser document, scrolls sideways only. await page.setViewportSize({width:390,height:700});await go();await page.getByLabel('선택 상태',{exact:true}).selectOption('expanded');await panelButton() await panelInput('z').scrollIntoViewIfNeeded();await panelInput('z').fill('3');await panelInput('z').press('Enter') const narrow=await stage.evaluate(e=>({top:e.scrollTop,left:e.scrollLeft,height:e.clientHeight,scrollHeight:e.scrollHeight,width:e.clientWidth,scrollWidth:e.scrollWidth})) - check('small Inner reaches full popup bottom with native Header and popup dimensions',await panelInput('z').inputValue()==='3'&&narrow.scrollHeight>narrow.height&&narrow.scrollWidth>=390,narrow) + check('small Inner reaches the popup bottom through the panel body, no Stage vertical scroll',await panelInput('z').inputValue()==='3'&&narrow.scrollHeight<=narrow.height&&narrow.scrollWidth>=390,narrow) await page.screenshot({path:path.join(out,'overview-narrow.png')}) await panelInput('z').press('Escape') check('narrow popup Escape restores More focus',await header.getByRole('button',{name:'더보기',exact:true}).evaluate(e=>document.activeElement===e)) diff --git a/packages/sw/tests/property-panel-comparison.browser.mjs b/packages/sw/tests/property-panel-comparison.browser.mjs index f180b90..e717a5c 100644 --- a/packages/sw/tests/property-panel-comparison.browser.mjs +++ b/packages/sw/tests/property-panel-comparison.browser.mjs @@ -90,8 +90,14 @@ try { const colGap = b.cell.x - a.cell.right, colGap2 = d.cell.x - c.cell.right, rowGap = c.cell.y - Math.max(a.cell.bottom, b.cell.bottom) report.data[`gaps${width}`] = { colGap, colGap2, rowGap } // 2026-10-22 (user request): no Inner scroller; the page scrolls. 2 columns × 2 rows, one column below 720px. - const oneColumn = width < 720 - if (oneColumn) check(`${width}: one column in order`, [a, b, c, d].every((cell, i, all) => i === 0 || (Math.abs(cell.cell.x - a.cell.x) < 0.5 && cell.cell.y >= all[i - 1].cell.bottom)), { a: a.cell, b: b.cell, c: c.cell, d: d.cell }) + // 2026-10-26 (user request): from 1080px, 3 columns — none/all stacked in column 1, circle and expanded beside them. + const oneColumn = width < 720, threeColumns = width >= 1080 + if (threeColumns) { + const gaps = { stack: b.cell.y - a.cell.bottom, col1: c.cell.x - Math.max(a.cell.right, b.cell.right), col2: d.cell.x - c.cell.right } + report.data[`gaps${width}`] = gaps + check(`${width}: 3 columns — none/all stacked, circle and expanded side by side`, Math.abs(a.cell.x - b.cell.x) < 0.5 && Math.abs(a.cell.y - c.cell.y) < 0.5 && Math.abs(a.cell.y - d.cell.y) < 0.5 && c.cell.x >= a.cell.right && d.cell.x >= c.cell.right, { a: a.cell, b: b.cell, c: c.cell, d: d.cell }) + check(`${width}: consistent 24px stack and column gaps`, Object.values(gaps).every(v => Math.abs(v - 24) < 0.5), gaps) + } else if (oneColumn) check(`${width}: one column in order`, [a, b, c, d].every((cell, i, all) => i === 0 || (Math.abs(cell.cell.x - a.cell.x) < 0.5 && cell.cell.y >= all[i - 1].cell.bottom)), { a: a.cell, b: b.cell, c: c.cell, d: d.cell }) else { check(`${width}: 2 columns × 2 rows by coordinates`, Math.abs(a.cell.y - b.cell.y) < 0.5 && Math.abs(c.cell.y - d.cell.y) < 0.5 && Math.abs(a.cell.x - c.cell.x) < 0.5 && Math.abs(b.cell.x - d.cell.x) < 0.5 && b.cell.x >= a.cell.right && c.cell.y >= Math.max(a.cell.bottom, b.cell.bottom), { a: a.cell, b: b.cell, c: c.cell, d: d.cell }) check(`${width}: consistent 24px column and row gaps`, [colGap, colGap2, rowGap].every(v => Math.abs(v - 24) < 0.5), { colGap, colGap2, rowGap }) diff --git a/src/sw/sw-header-overview.tsx b/src/sw/sw-header-overview.tsx index 5b47a8d..3961963 100644 --- a/src/sw/sw-header-overview.tsx +++ b/src/sw/sw-header-overview.tsx @@ -151,7 +151,7 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi }
+ style={{ ...size, "--sw-preview-stage-height": `${size.height}px`, "--sw-preview-panel-left": `${panelLeft}px`, "--sw-preview-panel-top": barOpen ? "77px" : "49px" } as CSSProperties}>
document.id === documentId)?.name ?? "열린 문서 없음"} diff --git a/src/sw/sw-header.stories.tsx b/src/sw/sw-header.stories.tsx index f6d4870..9046645 100644 --- a/src/sw/sw-header.stories.tsx +++ b/src/sw/sw-header.stories.tsx @@ -918,7 +918,8 @@ function PropertyState({ example, shapeContent, onLog }: { example: PropertyExam onClose={() => onLog?.("close")} /> } /** Design check only: a real header shows one panel for the current selection. - 2 columns × 2 rows at the panels' own size; the Inner scrolls instead of shrinking or wrapping them. */ + 3 columns at the panels' own size (none/all stacked, circle and expanded side by side); narrower screens fall back to + 2 × 2, then one column (sw-stories.css). */ function PropertyCompare() { const [log, setLog] = useState([]) return <> diff --git a/src/sw/sw-stories.css b/src/sw/sw-stories.css index 5d6734e..0ffd070 100644 --- a/src/sw/sw-stories.css +++ b/src/sw/sw-stories.css @@ -40,11 +40,20 @@ .sw-story-compare { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px; } .sw-story-compare__cell { margin: 0; } .sw-story-compare__cell figcaption { display: flex; flex-direction: column; gap: 2px; margin-bottom: 8px; font-size: 12px; color: #3d4a45; } -/* Property panel 4-state comparison: 2 columns × 2 rows at the panels' own size (314px wide, never shrunk). - 2026-10-22 (user request): no Inner scroller — the page scrolls normally, and a screen too narrow for two - panels (2 × 314 + 24px gap + gutters) shows one column instead of scrolling sideways. */ +/* Property panel 4-state comparison at the panels' own size (314px wide, never shrunk). + 2026-10-26 (user request): 3 columns — the short panels (none, all) stacked in column 1, the long ones (circle, + expanded) side by side in columns 2 and 3. Rows are auto / 1fr so the long panels' height goes to row 2 only and + "all" sits 24px under "none". Narrower screens: 2 columns × 2 rows (3 × 314 + 2 gaps + gutters do not fit), and one + column below 720px; no Inner scroller and no sideways scroll (2026-10-22). */ .sw-story-compare-page .sw-story-log { margin-top: 12px; } .sw-story-compare.sw-story-compare--grid { display: grid; grid-template-columns: repeat(2, max-content); align-items: start; gap: 24px; } +@media (min-width: 1080px) { + .sw-story-compare.sw-story-compare--grid { grid-template-columns: repeat(3, max-content); grid-template-rows: auto 1fr; } + .sw-story-compare--grid > [data-compare="none"] { grid-area: 1 / 1; } + .sw-story-compare--grid > [data-compare="all"] { grid-area: 2 / 1; } + .sw-story-compare--grid > [data-compare="circle"] { grid-area: 1 / 2 / span 2; } + .sw-story-compare--grid > [data-compare="expanded"] { grid-area: 1 / 3 / span 2; } +} @media (max-width: 719.98px) { .sw-story-compare.sw-story-compare--grid { grid-template-columns: max-content; } } /* Placement-only exception, same as the Overview Stage: the product clamps a panel to the viewport height; here the scroller provides the room so the 754px panel keeps its size. */ @@ -68,9 +77,11 @@ .sw-overview-slot { flex: 1 1 0; min-width: 0; min-height: 0; display: flex; align-items: center; justify-content: center; } .sw-overview-stage { flex: 0 0 auto; overflow: auto; overscroll-behavior: contain; background: var(--sw-story-canvas); outline: 1px solid var(--hm-sem-stroke-input-default); } .sw-overview-world { min-width: 390px; min-height: 100%; position: relative; } -/* Placement-only Storybook exception: keep the original panel size/content in the Stage scroll area. +/* Placement-only Storybook exception: the panel sits in the Stage. 2026-10-26 (user request): no Stage scrollbar when + it opens — the panel is clamped to the Stage height (as the product clamps it to the viewport) and its body scrolls. + The Stage height comes from the Story (--sw-preview-stage-height): the panel's containing block is the Header. Header's public API deliberately excludes panel style; package CSS and other Stories stay untouched. */ -.sw-overview-stage .hm-sw-property { position: absolute !important; left: var(--sw-preview-panel-left) !important; top: var(--sw-preview-panel-top) !important; max-height: none !important; } +.sw-overview-stage .hm-sw-property { position: absolute !important; left: var(--sw-preview-panel-left) !important; top: var(--sw-preview-panel-top) !important; max-height: calc(var(--sw-preview-stage-height) - var(--sw-preview-panel-top) - 4px) !important; } /* The document wrapper otherwise stacks below the independently positioned property Bar. */ .sw-overview-stage .hm-sw-header__document:has(.hm-sw-header__document-menu) { z-index: 3; } /* Responsive More measured section: Storybook controls (width range, host content toggles) above the frame. */