fix(sw): 2026-10-21 SW design review and responsive header without More from 760px

User-approved design review (2026-10-21):
- SW Header: from 760px up nothing moves into More; the five menus stay with their
  titles (titles and values ellipsize), file name shrinks to 96px. Open next to the
  logo no longer blocks field shrinking above 760px (760/768 overflow fixed, also for
  configured `controls` headers). Dropdown yellow ring inside the button, black seam.
- Property Bar values as read-only red text; property popup "=" labels, read-only
  black values, per-entry hover with pointer.
- Home Menu recent files (3 rows, path + name caption, empty preview), Footer + slot
  guide off by default, Viewer file list without icons, Storybook widths 1920-760.
- Tests: old More expectations (integrated-improvement, keyboard-dismiss,
  footer-popup-viewer-refinement, storybook-cleanup-stage1) moved to the new rule;
  functional checks kept.
This commit is contained in:
shkim4 committed 2026-10-21 17:29:25 +09:00
1 parent a25d2b31c5
commit 2fc1b3e108
37 files changed
+447 -211

No files matched your search

@@ -80,6 +80,11 @@ let reference, viewerMenu, moreMenu, refTrigger
const page = await newPage()
await open(page, 'sw-viewer--compare-with-sw-header')
const header = page.locator('.hm-sw-header').first()
// 2026-10-21: SWHeader shows More only below 760px (wider headers compress instead): the reference `...` menu is
// measured with the first header's 1440px Story wrapper narrowed to 700px (measurement only; face colors and metrics
// do not depend on the width).
await header.evaluate(element => { element.parentElement.style.width = '700px' })
await page.waitForTimeout(300)
const more = header.getByRole('button', { name: '더보기' })
await more.click()
const menu = header.getByRole('menu', { name: '추가 도구' })
@@ -223,7 +228,8 @@ let reference, viewerMenu, moreMenu, refTrigger
await open(page, 'sw-viewer--responsive')
const headers = page.locator('[data-width] .hm-sw-viewer-header')
const report = []
for (const [index, width] of [1440, 1100, 860, 520].entries()) {
// 2026-10-21: the Story widths follow SW Footer / SW Header Responsive More (+ 520).
for (const [index, width] of [1920, 1440, 1280, 950, 860, 760, 520].entries()) {
const header = headers.nth(index)
const info = await header.evaluate(element => {
const rect = selector => element.querySelector(selector)?.getBoundingClientRect()
@@ -239,24 +245,24 @@ let reference, viewerMenu, moreMenu, refTrigger
}
})
report.push({ width, ...info })
const b = await box(header)
await page.screenshot({ path: path.join(out, `responsive-${width}.png`), clip: { x: b.x, y: b.y, width: b.width, height: b.height } })
// Element screenshot: scrolls the row in (7 rows since 2026-10-21 do not fit one viewport).
await header.screenshot({ path: path.join(out, `responsive-${width}.png`) })
}
check('no horizontal overflow at 1440 / 1100 / 860 / 520', report.every(item => item.overflow <= 0 && item.objectOverflow <= 1), report.map(item => [item.width, item.overflow, item.objectOverflow]))
check('no horizontal overflow at 1920 / 1440 / 1280 / 950 / 860 / 760 / 520', report.every(item => item.overflow <= 0 && item.objectOverflow <= 1), report.map(item => [item.width, item.overflow, item.objectOverflow]))
check('layer / settings / properties always in the header', report.every(item => JSON.stringify(item.toggles.map(toggle => toggle.name)) === '["레이어","설정","속성"]'), report.map(item => item.toggles.map(toggle => toggle.name)))
// 2026-10-21 measured steps: full → icons only → More, by the measured room (no fixed width). With the file open button
// after the logo, this Story's content fits fully at 1440, as icons at 1100, and needs More at 860 / 520.
check('1440: full items with labels (전체 보기 shown), no More; 1100: icons only, no More', report.slice(0, 1).every(item => item.fitShown && !item.more && item.toggles.every(toggle => toggle.label))
&& !report[1].more && report[1].fitShown && report[1].toggles.every(toggle => !toggle.label && toggle.title), report.map(item => [item.width, item.fitShown, item.more]))
check('860: icons only (tooltips kept) + More', report[2].more && report[2].toggles.every(toggle => !toggle.label && toggle.title), report[2])
check('520: icons only, then More for what still does not fit', report[3].more && report[3].toggles.every(toggle => !toggle.label && toggle.title), report[3])
// after the logo, this Story's content fits fully at 1920 / 1440, as icons at 1280 / 950, and needs More at 860 / 760 / 520.
check('1920 / 1440: full items with labels (전체 보기 shown), no More; 1280 / 950: icons only, no More', report.slice(0, 2).every(item => item.fitShown && !item.more && item.toggles.every(toggle => toggle.label))
&& report.slice(2, 4).every(item => !item.more && item.fitShown && item.toggles.every(toggle => !toggle.label && toggle.title)), report.map(item => [item.width, item.fitShown, item.more]))
check('860 / 760: icons only (tooltips kept) + More', report.slice(4, 6).every(item => item.more && item.toggles.every(toggle => !toggle.label && toggle.title)), report.slice(4, 6))
check('520: icons only, then More for what still does not fit', report[6].more && report[6].toggles.every(toggle => !toggle.label && toggle.title), report[6])
check('logo, file name and toolbar never overlap', report.every(item => item.logoRight <= item.docLeft + 2 + 0.5 && item.docRight <= item.objectLeft + 0.5), report.map(item => [item.width, item.logoRight, item.docLeft, item.docRight, item.objectLeft]))
// Every item is either in the header or in More, never both and never lost.
const more = headers.nth(3).getByRole('button', { name: '추가 도구' })
const more = headers.nth(6).getByRole('button', { name: '추가 도구' })
await more.click()
const moreMenu = page.getByRole('menu', { name: '추가 도구' })
const moreItems = (await moreMenu.locator('[role^=menuitem]').allInnerTexts()).map(text => text.split(' · ')[0])
const inHeader = await headers.nth(3).evaluate(element => [...element.querySelectorAll('.hm-sw-viewer-header__object :is(.hm-sw-viewer-header__button:not(.hm-sw-viewer-header__more), [role=radio], .hm-sw-viewer-header__switch-label)')]
const inHeader = await headers.nth(6).evaluate(element => [...element.querySelectorAll('.hm-sw-viewer-header__object :is(.hm-sw-viewer-header__button:not(.hm-sw-viewer-header__more), [role=radio], .hm-sw-viewer-header__switch-label)')]
.map(item => item.getAttribute('aria-label') ?? item.textContent.trim().replace(/[\d,]+$/, '')))
const all = ['Model', 'Layout', '전체 보기', '2.5D', '어두운 배경', '레이어', '설정', '속성']
check('520: header + More hold every item exactly once (panel toggles never in More)', all.every(name => inHeader.includes(name) !== moreItems.includes(name)) && !moreItems.some(item => ['레이어', '설정', '속성'].includes(item)), { inHeader, moreItems })