feat(sw): viewer header logo menu and file list; simplify SW Storybook
SW Viewer Header (approved optional props, public export names unchanged at 170): - SWViewerHeader: logoMenu, documents, activeDocumentId, onDocumentSelect, onDocumentOpen, documentOpenLabel; measured responsive toggles (full -> icons -> More) with the SWHeader More / file-list styling. - SW AI guides, ExampleH, viewer/text-weight/theme-inheritance tests and the logo-document-menu tests with their QA record. SW Storybook cleanup (Story IDs and test fixtures kept): - Sidebar: Overview -> "SW Header" (first), 전체 Footer -> "SW Footer", Footer/Viewer responsive pages -> "Responsive More", Viewer content-variants -> "Settings Popup" (merges the settings popup page). - Hidden by the verification tag (direct URL kept): Header Dropdown, Document Open, Interactive States, Document Dropdown, Hidden Sub Controls, Home Menu standalone; Footer tool groups / custom content / parts; SW/Theme; Viewer menu groups / item states / variable size / settings panel / Header + Dropdown. - Removed demo UI: Overview size and callback output (demo data and selection only with &verification), Home Menu Figma node table and width demo, Header Responsive More reduced to 1440 / 768 / 390, Popup reopen button, property-panel comparison captions, Viewer context-menu demo (&verification only). - Tests: storybook-inventory records the hidden and renamed Stories; overview / range-cross-browser / viewer open the demo controls with &verification; integrated-popup reloads the Story instead of the reopen button; dropdown-consolidation asserts Dropdown is hidden from the sidebar and still opens by its direct URL.
This commit is contained in:
1 parent
46119eacb5
commit
23d522c136
57 files changed
+2623
-333
No files matched your search
@@ -17,7 +17,7 @@ const browser = await chromium.launch({ headless: true, executablePath: process.
|
||||
const results = []
|
||||
const check = (name, pass, detail = '') => { results.push({ name, result: pass ? 'PASS' : 'FAIL', detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`) }
|
||||
const errors = []
|
||||
const story = id => `${storybook}/iframe.html?id=sw-viewer--${id}&viewMode=story`
|
||||
const story = id => `${storybook}/iframe.html?id=sw-viewer--${id}&viewMode=story${id === 'dropdowns' ? '&verification' : ''}`
|
||||
const newPage = async (width = 1440, height = 900) => {
|
||||
const page = await browser.newPage({ viewport: { width, height }, reducedMotion: 'reduce' })
|
||||
page.on('pageerror', error => errors.push(`${page.url()}: ${error.message}`))
|
||||
@@ -69,7 +69,8 @@ if (reference && reference !== '-') {
|
||||
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 } })
|
||||
await page.getByRole('button', { name: '샘플' }).click()
|
||||
// 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 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()
|
||||
@@ -96,32 +97,34 @@ 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')
|
||||
// Dropdown: click opens, entries listed, Escape closes and returns focus.
|
||||
const samples = page.getByRole('button', { name: '샘플' })
|
||||
// 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')
|
||||
await samples.click()
|
||||
const menu = page.getByRole('menu', { name: '샘플' })
|
||||
check('menu opens on click', await menu.isVisible())
|
||||
check('menu has 3 entries', (await menu.getByRole('menuitem').count()) === 3)
|
||||
const menu = page.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)
|
||||
check('menu placed under its button', Math.abs(menuBox.x - triggerBox.x) <= 1 && menuBox.y >= triggerBox.y + triggerBox.height, `menu ${Math.round(menuBox.x)},${Math.round(menuBox.y)} button ${Math.round(triggerBox.x)},${Math.round(triggerBox.y + triggerBox.height)}`)
|
||||
check('trigger shows open state', (await samples.getAttribute('aria-expanded')) === 'true' && (await samples.getAttribute('data-state')) === 'open')
|
||||
check('list placed under the file name', menuBox.y >= triggerBox.y + triggerBox.height - 1, `list ${Math.round(menuBox.x)},${Math.round(menuBox.y)} name ${Math.round(triggerBox.x)},${Math.round(triggerBox.y + triggerBox.height)}`)
|
||||
check('trigger shows open state', (await samples.getAttribute('aria-expanded')) === 'true' && (await samples.getAttribute('data-selected')) === 'true')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
check('ArrowDown moves focus in menu', (await page.evaluate(() => document.activeElement?.textContent)).includes('평면'))
|
||||
check('ArrowDown moves focus in the list', (await page.evaluate(() => document.activeElement?.textContent)).includes('평면'))
|
||||
await page.keyboard.press('Escape')
|
||||
check('Escape closes menu', !(await menu.isVisible().catch(() => false)))
|
||||
check('Escape returns focus to trigger', await samples.evaluate(element => element === document.activeElement))
|
||||
// Keyboard open + Enter select → file name changes, command logged.
|
||||
await samples.press('Enter')
|
||||
check('Enter opens with first entry focused', (await page.evaluate(() => document.activeElement?.getAttribute('role'))) === 'menuitem')
|
||||
await page.keyboard.press('End')
|
||||
check('Escape closes the list', !(await menu.isVisible().catch(() => false)))
|
||||
check('Escape returns focus to the file name', await samples.evaluate(element => element === document.activeElement))
|
||||
// Keyboard open + Enter select → file name changes, selection logged by the host.
|
||||
await samples.press('ArrowDown')
|
||||
check('ArrowDown opens with the first row focused', (await page.evaluate(() => document.activeElement?.classList.contains('hm-sw-header__document-item'))))
|
||||
// 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 entry (document changes)', (await page.locator('.hm-sw-viewer-header__document-name').innerText()) === 'simple.dxf')
|
||||
check('onCommand reports menu entry', (await log(page)).includes('samples/sample-dxf'), await log(page))
|
||||
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('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 menu', (await page.getByRole('menu').count()) === 0)
|
||||
check('outside click closes the list', (await page.getByRole('group', { name: '문서 목록' }).count()) === 0)
|
||||
// Tabs: arrow keys select.
|
||||
const model = page.getByRole('radio', { name: /Model/ })
|
||||
await model.focus()
|
||||
@@ -131,10 +134,10 @@ if (comparison.reference) {
|
||||
const depth = page.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'))
|
||||
// Disabled fit button is enabled once a document is open; hover face.
|
||||
const fit = page.getByRole('button', { name: '전체 보기' })
|
||||
await fit.hover()
|
||||
check('hover face #266651', (await fit.evaluate(element => getComputedStyle(element).backgroundColor)) === 'rgb(38, 102, 81)')
|
||||
// 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 })
|
||||
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 })
|
||||
await layers.click()
|
||||
@@ -272,7 +275,7 @@ if (comparison.reference) {
|
||||
await page.close()
|
||||
}
|
||||
{
|
||||
// Responsive: More collects the collapsed items, no horizontal overflow, icon-only below 560.
|
||||
// Responsive: measured steps (full → icons only → More), no horizontal overflow; More collects the collapsed items.
|
||||
const page = await newPage(1920, 1200)
|
||||
await open(page, 'responsive')
|
||||
const headers = page.locator('[data-width] .hm-sw-viewer-header')
|
||||
@@ -290,18 +293,24 @@ if (comparison.reference) {
|
||||
await page.screenshot({ path: path.join(out, `package-responsive-${width}.png`), clip: { x: box2.x, y: box2.y, width: box2.width, height: box2.height } })
|
||||
}
|
||||
check('no horizontal overflow at 1440 / 1100 / 860 / 520', report.every(item => item.overflow <= 0 && item.objectOverflow <= 1), JSON.stringify(report.map(item => [item.width, item.overflow, item.objectOverflow])))
|
||||
check('1440: no More', !report[0].more)
|
||||
check('1100: More, samples/tabs/switches moved', report[1].more && !report[1].labels.includes('샘플') && !report[1].labels.includes('2.5D') && report[1].labels.includes('레이어'))
|
||||
check('860: panel toggles moved too', report[2].more && !report[2].labels.includes('레이어'))
|
||||
check('520: 파일 열기 / 전체 보기 icon only', report[3].labels.includes('파일 열기') && !(await headers.nth(3).locator('.hm-sw-viewer-header__label', { hasText: '파일 열기' }).count()))
|
||||
const more = headers.nth(2).getByRole('button', { name: '추가 도구' })
|
||||
// 2026-10-21: 파일 열기 / 샘플 left the header (logo menu, file-name list). Measured steps instead of fixed widths:
|
||||
// full items while they fit, then icons only, then More; the panel toggles never move into More. With this Story's
|
||||
// content: 1440 / 1100 full, 860 icons only, 520 icons + More.
|
||||
// 2026-10-21: with the file open button after the logo, 1100 fits as icons (still no More) and 860 needs More.
|
||||
check('1440 / 1100: every item in the header (전체 보기 included), no More', [report[0], report[1]].every(item => !item.more && ['전체 보기', '2.5D', '어두운 배경', '레이어'].every(name => item.labels.includes(name))))
|
||||
check('860: icons only + More', report[2].more && !(await headers.nth(2).locator('.hm-sw-viewer-header__label', { hasText: '레이어' }).count()))
|
||||
check('520: icons only + More; panel toggles kept as icons (label → aria-label)', report[3].more && ['레이어', '설정', '속성'].every(name => report[3].labels.includes(name))
|
||||
&& !(await headers.nth(3).locator('.hm-sw-viewer-header__label', { hasText: '레이어' }).count()))
|
||||
check('no 파일 열기 / 샘플 header buttons at any width', report.every(item => !item.labels.includes('파일 열기') && !item.labels.includes('샘플')))
|
||||
const more = headers.nth(3).getByRole('button', { name: '추가 도구' })
|
||||
await more.click()
|
||||
const moreMenu = page.getByRole('menu', { name: '추가 도구' })
|
||||
const items = await moreMenu.locator('.hm-sw-viewer-menu__label, [role^=menuitem]').allInnerTexts()
|
||||
check('More sections like the reference', ['샘플 도면', '도면 공간', '화면 보기', '패널'].every(label => items.includes(label)), items.join(' | '))
|
||||
check('More face beige 260px', (await moreMenu.evaluate(element => `${getComputedStyle(element).backgroundColor} ${element.offsetWidth}`)) === 'rgb(245, 240, 229) 260')
|
||||
await moreMenu.getByRole('menuitemcheckbox', { name: '레이어' }).click()
|
||||
check('More checkbox reports command', (await log(page)).includes('layers=true'), await log(page))
|
||||
check('More sections: 도면 공간, 화면 보기 (with 전체 보기); no 패널', ['도면 공간', '화면 보기', '전체 보기'].every(label => items.includes(label)) && !items.includes('패널'), items.join(' | '))
|
||||
// 2026-10-21: the header dropdowns (More included) take the SWHeader `...` menu face (#0e3d2f); width unchanged.
|
||||
check('More face (SWHeader ... menu #0e3d2f) 260px', (await moreMenu.evaluate(element => `${getComputedStyle(element).backgroundColor} ${element.offsetWidth}`)) === 'rgb(14, 61, 47) 260')
|
||||
await moreMenu.getByRole('menuitemcheckbox', { name: '2.5D' }).click()
|
||||
check('More checkbox reports command', (await log(page)).includes('2.5d=true'), await log(page))
|
||||
await more.click()
|
||||
await page.getByRole('menuitemradio', { name: /Layout/ }).click()
|
||||
check('More radio selects tab value', (await log(page)).includes('space=layout1'))
|
||||
@@ -358,11 +367,13 @@ if (comparison.reference) {
|
||||
const header = document.querySelector('[data-logo-case] .hm-sw-viewer-header')
|
||||
const style = selector => getComputedStyle(header.querySelector(selector))
|
||||
return {
|
||||
label: style('.hm-sw-viewer-header__label').textShadow, document: style('.hm-sw-viewer-header__document-name').textShadow,
|
||||
// 2026-10-21: the file name is the SWHeader document button when `documents` is given (same outline token).
|
||||
label: style('.hm-sw-viewer-header__label').textShadow, document: style('.hm-sw-viewer-header__document-name, .hm-sw-header__document-name').textShadow,
|
||||
tab: style('.hm-sw-viewer-header__tab').textShadow, caption: style('.hm-sw-viewer-header__switch-label').textShadow,
|
||||
count: style('.hm-sw-viewer-header__count').textShadow,
|
||||
icon: (() => {
|
||||
const icon = header.querySelector('.hm-sw-viewer-header__icon')
|
||||
// A lucide (stroke) icon: since 2026-10-21 the first header icon is the More dots (fill), so take the 레이어 toggle.
|
||||
const icon = header.querySelector('.hm-sw-viewer-header__button[data-kind="toggle"] .hm-sw-viewer-header__icon')
|
||||
const copy = getComputedStyle(icon.querySelector('.hm-sw-viewer-icon__outline svg *')), glyph = getComputedStyle(icon.querySelector('.hm-sw-viewer-icon__glyph svg *'))
|
||||
// lucide 24-unit icon drawn at 16px: 1px each side = 1.5 units, so the copy is 2 + 3 units.
|
||||
return { filter: getComputedStyle(icon).filter, copyStroke: copy.stroke, copyWidth: parseFloat(copy.strokeWidth), glyphStroke: glyph.stroke, glyphWidth: parseFloat(glyph.strokeWidth) }
|
||||
|
||||
Reference in new issue
Block a user