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:
1 parent
a25d2b31c5
commit
2fc1b3e108
37 files changed
+447
-211
No files matched your search
@@ -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()
|
||||
}
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user