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:
shkim4 committed 2026-10-21 14:52:46 +09:00
1 parent 46119eacb5
commit 23d522c136
57 files changed
+2623 -333

No files matched your search

+71 -12
View File
@@ -60,32 +60,90 @@ const fonts = (page, selector, exclude = null) => page.evaluate(([selector, excl
const only = (map, expected) => Object.keys(map).length > 0 && Object.keys(map).every(family => family === expected)
const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${family}`).join(' | ') || 'no text'
// More (2026-10-21): the header shows More only when its measured room runs out (full → icons → More). The Story's
// 1000px header fits as icons, so the header is narrowed for this check (as text-weight does), then restored.
async function openMore(page, area) {
const header = page.locator(`${area} .hm-sw-viewer-header`)
await header.evaluate(element => { element.style.width = "700px" })
await page.locator(`${area} .hm-sw-viewer-header button[aria-label="추가 도구"]`).click()
}
const restoreWidth = (page, area) => page.locator(`${area} .hm-sw-viewer-header`).evaluate(element => { element.style.width = "" })
// Viewer header dropdown checked here (2026-10-21): the file-name list (.hm-sw-header__document-menu); the logo menu is gone.
const DOC_LIST = '.hm-sw-viewer-header .hm-sw-header__document-menu'
async function dropdownFonts(page, area, name, expected) {
const label = expected === DEFAULT ? 'Noto Sans KR' : 'Dotum'
await page.locator(`${area} .hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-button`).click()
await page.waitForSelector(`${area} ${DOC_LIST}`)
const list = `${area} :is(${DOC_LIST}, .hm-sw-viewer-header .hm-sw-header__document-button)` // file name + file open surfaces
check(`${name}: 파일명 버튼 + 샘플 목록 = ${label}`, only(await fonts(page, list), expected), fmt(await fonts(page, list)))
await page.keyboard.press('Escape')
}
// Token → painted property of each dropdown part. 2026-10-21: the logo menu keeps the SWHeader `...` menu colors (object
// border, text, row hover tokens; its face #0e3d2f is a fixed value, checked below); the document list keeps its tokens.
const COLOR_TOKENS = [
['--hm-sw-header-document-menu-background', DOC_LIST, 'backgroundColor'],
['--hm-sw-header-document-menu-border-color', DOC_LIST, 'borderTopColor'],
['--hm-sw-header-title-color', `${DOC_LIST} .hm-sw-header__document-row:not([data-active]) .hm-sw-header__document-item-name`, 'color'],
['--hm-sw-header-document-item-hover-background', `${DOC_LIST} .hm-sw-header__document-row:not([data-active])`, 'backgroundColor', 'hover'],
]
async function dropdownColors(page, def, themed) {
// Paint of every part with its menu open (hovered first when the token is a hover state), next to the token value
// resolved in the same scope (a probe painted with var(token), so both are in the computed rgb() form).
const paint = async (area, scopeSelector) => {
const result = {}
for (const [token, part, property, hover] of COLOR_TOKENS) {
await page.locator(`${area} .hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-button`).click()
const target = page.locator(`${area} ${part}`).first()
await target.waitFor()
if (hover) { await target.hover(); await page.waitForTimeout(120) }
result[token] = await target.evaluate((element, [token, property, scopeSelector]) => {
const probe = document.createElement('span'); probe.style.color = `var(${token})`
element.closest(scopeSelector).append(probe); const value = getComputedStyle(probe).color; probe.remove()
return { painted: getComputedStyle(element)[property], token: value }
}, [token, property, scopeSelector])
await page.mouse.move(2, 2)
await page.keyboard.press('Escape')
}
return result
}
const reference = await paint(def, '.hm-sw-viewer-header')
check('기본: 샘플 목록(SWHeader 파일명 목록 토큰) 배경·글자·Border·Hover = 기본 토큰 값', Object.values(reference).every(item => item.painted === item.token), JSON.stringify(reference))
// Themed: distinct colors set once on the outermost scope reach both menus; removed again afterwards.
const values = ['rgb(201, 11, 21)', 'rgb(202, 12, 22)', 'rgb(203, 13, 23)', 'rgb(204, 14, 24)']
const tokens = COLOR_TOKENS.map(item => item[0])
await page.locator(themed).evaluate((element, [tokens, values]) => tokens.forEach((token, i) => element.style.setProperty(token, values[i])), [tokens, values])
const themedPaint = await paint(themed, themed)
await page.locator(themed).evaluate((element, tokens) => tokens.forEach(token => element.style.removeProperty(token)), tokens)
check('뷰어 테마: 바깥 범위 토큰(배경·글자·Border·Hover)이 샘플 목록까지 상속', tokens.every((token, i) => themedPaint[token].painted === values[i]), JSON.stringify(themedPaint))
}
// ---------- 1. Theme inheritance Story ----------
{
const page = await newPage()
await open(page, 'sw-theme--theme-inheritance')
const scope = name => `[data-theme-case="${name}"]`
// Default (no theme): header, then its sample dropdown and More.
// Default (no theme): header, then its dropdowns and More. 2026-10-21: the header's dropdowns are the logo menu
// (파일 열기, SWHeader `...` menu) and the file-name sample list (SWHeader file-name list); the 샘플 menu button is gone.
const def = scope('default')
check('기본: 단독 SWViewerHeader = Noto Sans KR', only(await fonts(page, `${def} .hm-sw-viewer-header`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-header`)))
await page.locator(`${def} .hm-sw-viewer-header [aria-haspopup="menu"]`).first().click()
await page.waitForSelector(`${def} .hm-sw-viewer-menu`)
check('기본: Dropdown = Noto Sans KR', only(await fonts(page, `${def} .hm-sw-viewer-menu`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-menu`)))
await page.keyboard.press('Escape')
await page.locator(`${def} .hm-sw-viewer-header button[aria-label="추가 도구"]`).click()
await dropdownFonts(page, def, '기본', DEFAULT)
await openMore(page, def)
await page.waitForSelector(`${def} .hm-sw-viewer-menu[data-surface="beige"]`)
check('기본: More = Noto Sans KR', only(await fonts(page, `${def} .hm-sw-viewer-menu`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-menu`)))
await page.keyboard.press('Escape')
await restoreWidth(page, def)
// Viewer, themed through <SWTheme style>.
const viewer = scope('viewer-themed')
check('뷰어 테마: SWViewerHeader 전체 = Dotum', only(await fonts(page, `${viewer} .hm-sw-viewer-header`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-header`)))
await page.locator(`${viewer} .hm-sw-viewer-header [aria-haspopup="menu"]`).first().click()
await page.waitForSelector(`${viewer} .hm-sw-viewer-menu`)
check('뷰어 테마: Dropdown = Dotum', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
await page.keyboard.press('Escape')
await page.locator(`${viewer} .hm-sw-viewer-header button[aria-label="추가 도구"]`).click()
await dropdownFonts(page, viewer, '뷰어 테마', VERIFY)
// Colors / border / hover of the two dropdowns: the SWHeader default token values without a theme, and a token set
// through `style` on the outermost SW scope (here the themed <SWTheme>) reaches both menus. The colors are set only
// for this check and removed again (the Story itself themes the font only).
await dropdownColors(page, def, viewer)
await openMore(page, viewer)
await page.waitForSelector(`${viewer} .hm-sw-viewer-menu[data-surface="beige"]`)
check('뷰어 테마: More = Dotum', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
// A submenu of More, if any, is a nested menu inside a nested menu.
@@ -96,6 +154,7 @@ const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${fa
check('뷰어 테마: More 서브메뉴 = Dotum', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
}
await page.keyboard.press('Escape')
await restoreWidth(page, viewer)
const panels = await page.locator(`${viewer} .hm-sw-viewer-panel`).count()
check('뷰어 테마: SWViewerPanel(제목·입력·버튼·스위치·힌트) = Dotum', panels === 2 && only(await fonts(page, `${viewer} .hm-sw-viewer-panel`), VERIFY), `${panels} panels · ${fmt(await fonts(page, `${viewer} .hm-sw-viewer-panel`))}`)
await page.screenshot({ path: path.join(out, 'viewer-themed-more.png'), clip: await page.locator(viewer).boundingBox() })
@@ -210,7 +269,7 @@ for (const width of [1920, 1440, 1180, 900, 560]) for (const id of ids) {
await page.addStyleTag({ content: legacy })
await page.waitForTimeout(150)
const viewer = '[data-theme-case="viewer-themed"]'
await page.locator(`${viewer} .hm-sw-viewer-header button[aria-label="추가 도구"]`).click()
await openMore(page, viewer)
await page.waitForSelector(`${viewer} .hm-sw-viewer-menu[data-surface="beige"]`)
const more = await fonts(page, `${viewer} .hm-sw-viewer-menu`)
const command = await fonts(page, '[data-theme-case="general-themed"] .hm-sw-footer-command')