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

+27 -24
View File
@@ -33,14 +33,14 @@ const box = locator => locator.boundingBox()
const geometry = async (page, selectors) => page.evaluate(selectors => {
const header = document.querySelector(selectors.header)
const base = header.getBoundingClientRect()
return Object.fromEntries(Object.entries(selectors.parts).map(([name, selector]) => [name, [...document.querySelectorAll(selector)].map(element => {
return Object.fromEntries(Object.entries(selectors.parts).map(([name, selector]) => [name, [...(selectors.scoped ? header : document).querySelectorAll(selector)].map(element => {
const rect = element.getBoundingClientRect()
return { x: Math.round(rect.x - base.x), y: Math.round(rect.y - base.y), w: Math.round(rect.width), h: Math.round(rect.height), text: element.textContent.trim().slice(0, 12) }
})]))
}, selectors)
const parts = {
reference: { header: 'header', parts: { document: '.cad-toolbar-document', object: '.cad-toolbar-object-area', field: '.cad-toolbar-property-field', button: '.cad-toolbar [data-slot=button]', toggle: '.cad-toolbar [data-slot=toggle]', tab: '.cad-toolbar [data-slot=tabs-trigger]', switchField: '.cad-toolbar-switch-field' } },
package: { header: '.hm-sw-viewer-header', parts: { document: '.hm-sw-viewer-header__document', object: '.hm-sw-viewer-header__object', field: '.hm-sw-viewer-header__group[data-surface=field]', button: '.hm-sw-viewer-header__button:not([data-kind=toggle])', toggle: '.hm-sw-viewer-header__button[data-kind=toggle]', tab: '.hm-sw-viewer-header__tab', switchField: '.hm-sw-viewer-header__switch-field' } },
package: { header: '.hm-sw-viewer-header', scoped: true, parts: { document: '.hm-sw-viewer-header__document', object: '.hm-sw-viewer-header__object', field: '.hm-sw-viewer-header__group[data-surface=field]', button: '.hm-sw-viewer-header__button:not([data-kind=toggle])', toggle: '.hm-sw-viewer-header__button[data-kind=toggle]', tab: '.hm-sw-viewer-header__tab', switchField: '.hm-sw-viewer-header__switch-field' } },
}
const comparison = {}
if (reference && reference !== '-') {
@@ -65,16 +65,17 @@ if (reference && reference !== '-') {
{
const page = await newPage(1920, 1000)
await open(page, 'viewer-header')
const frame = page.locator('[data-viewer-frame]')
// 2026-10-21: the Story also shows the minimize / close version below; the checks use the first (basic) frame.
const frame = page.locator('[data-viewer-frame]').first()
const origin = await box(frame)
comparison.package = await geometry(page, parts.package)
await page.screenshot({ path: path.join(out, 'package-1440-loaded.png'), clip: { x: origin.x, y: origin.y, width: 1440, height: 56 } })
// 2026-10-21: the samples are the file-name list (SWHeaderDocumentTitle), see viewer-logo-document-menu.browser.mjs.
await page.locator('.hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-button').click()
await frame.locator('.hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-button').click()
await page.screenshot({ path: path.join(out, 'package-1440-sample-menu.png'), clip: { x: origin.x, y: origin.y, width: 1440, height: 200 } })
await page.keyboard.press('Escape')
await page.getByRole('button', { name: '레이어' }).click()
await page.getByRole('button', { name: '설정' }).click()
await frame.getByRole('button', { name: '레이어' }).click()
await frame.getByRole('button', { name: '설정' }).click()
await page.waitForTimeout(200)
await page.screenshot({ path: path.join(out, 'package-1440-panels.png'), clip: { x: origin.x, y: origin.y, width: 1440, height: 620 } })
await page.close()
@@ -97,11 +98,13 @@ if (comparison.reference) {
// Wide enough for the 1440px story frame plus the story page padding (panels at the frame right edge stay on screen).
const page = await newPage(1920, 1000)
await open(page, 'viewer-header')
// 2026-10-21: the Story also shows the minimize / close version below; these checks use the first (basic) frame.
const root = page.locator('[data-viewer-frame]').first()
// Samples (2026-10-21): the file-name list (SWHeaderDocumentTitle) replaced the 샘플 menu button. Click opens, rows listed,
// Escape closes and returns focus; the logo menu (Open) is checked in viewer-logo-document-menu.browser.mjs.
const samples = page.locator('.hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-button')
const samples = page.locator('.hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-button').first()
await samples.click()
const menu = page.getByRole('group', { name: '문서 목록' })
const menu = root.getByRole('group', { name: '문서 목록' })
check('document list opens on click', await menu.isVisible())
check('document list has the 3 samples', (await menu.locator('.hm-sw-header__document-item').count()) === 3)
const menuBox = await box(menu), triggerBox = await box(samples)
@@ -119,34 +122,34 @@ if (comparison.reference) {
// 2026-10-21: no Open below the rows (file open is the header button); ArrowUp wraps to the last sample.
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Enter')
check('Enter selects a sample (document changes)', (await page.locator('.hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-name').innerText()) === 'simple.dxf')
check('Enter selects a sample (document changes)', (await page.locator('.hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-name').first().innerText()) === 'simple.dxf')
check('onDocumentSelect reported', (await log(page)).includes('samples/sample-dxf'), await log(page))
// Outside click closes.
await samples.click()
await page.mouse.click(700, 600)
check('outside click closes the list', (await page.getByRole('group', { name: '문서 목록' }).count()) === 0)
check('outside click closes the list', (await root.getByRole('group', { name: '문서 목록' }).count()) === 0)
// Tabs: arrow keys select.
const model = page.getByRole('radio', { name: /Model/ })
const model = root.getByRole('radio', { name: /Model/ })
await model.focus()
await page.keyboard.press('ArrowRight')
check('tabs ArrowRight selects Layout', (await page.getByRole('radio', { name: /Layout/ }).getAttribute('aria-checked')) === 'true')
check('tabs ArrowRight selects Layout', (await root.getByRole('radio', { name: /Layout/ }).getAttribute('aria-checked')) === 'true')
// Switch
const depth = page.getByRole('switch', { name: '2.5D' })
const depth = root.getByRole('switch', { name: '2.5D' })
await depth.click()
check('header switch toggles', (await depth.getAttribute('aria-checked')) === 'true' && (await log(page)).includes('2.5d=true'))
// Hover face (2026-10-21: 전체 보기 left the header row for More, so the panel toggle is hovered).
const hovered = page.getByRole('button', { name: '레이어', exact: true })
const hovered = root.getByRole('button', { name: '레이어', exact: true })
await hovered.hover()
check('hover face #266651', (await hovered.evaluate(element => getComputedStyle(element).backgroundColor)) === 'rgb(38, 102, 81)')
// Toggle opens the panel; Escape inside closes it and focus goes back.
const layers = page.getByRole('button', { name: '레이어', exact: true })
const layers = root.getByRole('button', { name: '레이어', exact: true })
await layers.click()
const layerPanel = page.getByRole('dialog', { name: /레이어 \(/ })
const layerPanel = root.getByRole('dialog', { name: /레이어 \(/ })
check('toggle opens panel', await layerPanel.isVisible())
check('toggle pressed state + #FFC600', (await layers.getAttribute('aria-pressed')) === 'true' && (await layers.evaluate(element => getComputedStyle(element).color)) === 'rgb(255, 198, 0)')
check('panel focused on open', await layerPanel.evaluate(element => element.contains(document.activeElement)))
await page.keyboard.press('Escape')
check('Escape closes panel', (await page.getByRole('dialog', { name: /레이어 \(/ }).count()) === 0)
check('Escape closes panel', (await root.getByRole('dialog', { name: /레이어 \(/ }).count()) === 0)
check('focus returns to toggle', await layers.evaluate(element => element === document.activeElement))
// Resize by the native corner (right after opening, top-end placement).
await layers.click()
@@ -171,7 +174,7 @@ if (comparison.reference) {
await page.mouse.down()
await page.mouse.move(after.x + 3000, after.y + 3000, { steps: 6 })
await page.mouse.up()
const frame = await box(page.locator('.sw-viewer-story-canvas'))
const frame = await box(root.locator('.sw-viewer-story-canvas'))
const clamped = await box(layerPanel)
check('drag stays inside bounds (10px edge)', Math.round(frame.x + frame.width - (clamped.x + clamped.width)) === 10 && Math.round(frame.y + frame.height - (clamped.y + clamped.height)) === 10,
`right gap ${Math.round(frame.x + frame.width - (clamped.x + clamped.width))}, bottom gap ${Math.round(frame.y + frame.height - (clamped.y + clamped.height))}`)
@@ -201,18 +204,18 @@ if (comparison.reference) {
await close.hover()
check('close hover = SWPopup hover asset', await close.evaluate(element => getComputedStyle(element.querySelector('.hm-sw-property__close-hover')).visibility === 'visible' && getComputedStyle(element.querySelector('.hm-sw-property__close-default')).visibility === 'hidden'))
await close.click()
check('close button closes', (await page.getByRole('dialog', { name: /레이어 \(/ }).count()) === 0)
check('close button closes', (await root.getByRole('dialog', { name: /레이어 \(/ }).count()) === 0)
// Two panels: the last touched comes to the front.
await layers.click()
await page.getByRole('button', { name: '설정', exact: true }).click()
const options = page.getByRole('dialog', { name: '옵션' })
await root.getByRole('button', { name: '설정', exact: true }).click()
const options = root.getByRole('dialog', { name: '옵션' })
const z = async () => Promise.all([layerPanel, options].map(item => item.evaluate(element => Number(getComputedStyle(element).zIndex))))
await layerPanel.locator('.hm-sw-property__titlebar').click({ position: { x: 30, y: 15 } })
const [zl, zo] = await z()
check('last touched panel on top', zl > zo, `layer z ${zl}, options z ${zo}`)
// Modal popup: backdrop, focus trap, Escape.
await page.getByRole('button', { name: '정보 및 라이선스' }).click()
const about = page.getByRole('dialog', { name: /라이선스/ })
await root.getByRole('button', { name: '정보 및 라이선스' }).click()
const about = root.getByRole('dialog', { name: /라이선스/ })
const confirm = about.getByRole('button', { name: '확인' })
await confirm.hover()
const confirmHover = await face(confirm)
@@ -222,7 +225,7 @@ if (comparison.reference) {
for (let index = 0; index < 4; index++) await page.keyboard.press('Tab')
check('modal focus stays inside', await about.evaluate(element => element.contains(document.activeElement)))
await page.keyboard.press('Escape')
check('modal Escape closes', (await page.getByRole('dialog', { name: /라이선스/ }).count()) === 0)
check('modal Escape closes', (await root.getByRole('dialog', { name: /라이선스/ }).count()) === 0)
await page.close()
}
{