feat(sw): add remaining component theme tokens
Add nine component tokens declared on the outermost SW scope. Each defaults to the token or literal it replaces, so the default look and header theme inheritance are unchanged; setting one changes its component alone. - Viewer Header menus (logo menu, header dropdowns, More): --hm-sw-viewer-header-menu-background / -border-color / -shadow / -item-hover-background (default: --hm-sw-header-more-*) - SWHeader K guide: --hm-sw-header-k-guide-border-color (default var(--hm-sw-property-border)) / -background (#0e3c2e) - Popup value color: --hm-sw-popup-value-color (default var(--hm-sw-header-input-color)); the property bar keeps the header token - Footer command history scrollbar: --hm-sw-footer-command-scrollbar-thumb (#574b30) / -track (transparent) Footer focus ring, side shadow, shared text color and dividers stay shared. Docs (DESIGN_RULES, AI_USAGE, VIEWER, header design spec) and theme-inheritance section 1d updated.
This commit is contained in:
1 parent
28e00f2870
commit
228d8872fc
8 files changed
+174
-23
No files matched your search
@@ -274,6 +274,118 @@ async function dropdownColors(page, def, themed) {
|
||||
check('Viewer Header 토큰은 SWHeader·Step Header 화면에 영향 없음', untouched.every(item => item.includes(' same ')), untouched.join(', '))
|
||||
}
|
||||
|
||||
// ---------- 1d. Component tokens (2026-11-10): viewer header menus, K guide, popup value, footer command scrollbar ----------
|
||||
// Each defaults to the token / literal it replaced (same value), follows a shared token set on the outermost SW scope,
|
||||
// and when set itself changes its component alone.
|
||||
{
|
||||
const setOn = (page, root, values) => page.locator(root).first().evaluate((element, values) => {
|
||||
let outer = element
|
||||
for (let e = element; e; e = e.parentElement) if (e.matches?.('.hm-sw-header, .hm-sw-theme')) outer = e
|
||||
Object.entries(values).forEach(([token, value]) => value === null ? outer.style.removeProperty(token) : outer.style.setProperty(token, value))
|
||||
}, values)
|
||||
const C1 = 'rgb(44, 55, 66)', C2 = 'rgb(1, 2, 3)'
|
||||
const flow = async (label, page, root, read, shared, own, expect) => {
|
||||
const base = await read()
|
||||
await setOn(page, root, Object.fromEntries(shared.map(token => [token, C1])))
|
||||
const inherited = await read()
|
||||
await setOn(page, root, Object.fromEntries(own.map(token => [token, C2])))
|
||||
const mine = await read()
|
||||
await setOn(page, root, Object.fromEntries([...shared, ...own].map(token => [token, null])))
|
||||
const back = await read()
|
||||
check(`${label}: 기본값 = 기존 값`, JSON.stringify(base) === JSON.stringify(expect), JSON.stringify(base))
|
||||
check(`${label}: 바깥 범위의 공유 토큰을 따름`, Object.values(inherited).every(value => value.includes(C1)), JSON.stringify(inherited))
|
||||
check(`${label}: 전용 토큰 지정이 우선, 지우면 복귀`, Object.values(mine).every(value => value.includes(C2)) && JSON.stringify(back) === JSON.stringify(base), JSON.stringify(mine))
|
||||
}
|
||||
// Viewer Header menus: a header dropdown (도면) and the logo menu (no Story renders `logoMenu`: the same class
|
||||
// structure is appended to the header, so the real rules apply).
|
||||
const viewer = await newPage()
|
||||
await open(viewer, 'sw-viewer--dropdowns')
|
||||
await viewer.locator('.hm-sw-viewer-header').first().evaluate(header => {
|
||||
const wrap = document.createElement('div'); wrap.className = 'hm-sw-viewer-header__logo-menu hm-sw-header__more'
|
||||
wrap.innerHTML = '<div class="hm-sw-header__more-menu" data-probe=""><button type="button" class="hm-sw-header__more-item" data-visible="true">probe</button></div>'
|
||||
header.append(wrap)
|
||||
})
|
||||
await viewer.getByRole('button', { name: '도면' }).first().click()
|
||||
const menu = viewer.locator('.hm-sw-viewer-header .hm-sw-viewer-menu[data-surface]').first()
|
||||
await menu.locator('.hm-sw-viewer-menu__item:not([aria-disabled])').first().hover()
|
||||
const readMenu = () => viewer.evaluate(() => {
|
||||
const m = getComputedStyle(document.querySelector('.hm-sw-viewer-header .hm-sw-viewer-menu[data-surface]'))
|
||||
const hovered = [...document.querySelectorAll('.hm-sw-viewer-header .hm-sw-viewer-menu .hm-sw-viewer-menu__item')].find(item => item.matches(':hover, :focus'))
|
||||
const logo = getComputedStyle(document.querySelector('[data-probe]'))
|
||||
return { face: `${m.backgroundColor} | ${m.borderTopColor} | ${m.boxShadow}`, hover: hovered ? getComputedStyle(hovered).backgroundColor : 'none', logo: `${logo.backgroundColor} | ${logo.borderTopColor} | ${logo.boxShadow}` }
|
||||
})
|
||||
const header = await newPage()
|
||||
await open(header, 'sw-header--overview')
|
||||
// Expected defaults: the SWHeader More menu face (same tokens) — computed on a probe More menu inside SWHeader.
|
||||
const moreFace = await header.locator('.hm-sw-header').first().evaluate(root => {
|
||||
const wrap = document.createElement('div'); wrap.className = 'hm-sw-header__more'
|
||||
wrap.innerHTML = '<div class="hm-sw-header__more-menu"></div>'; root.append(wrap)
|
||||
const s = getComputedStyle(wrap.firstChild); const value = `${s.backgroundColor} | ${s.borderTopColor} | ${s.boxShadow}`; wrap.remove(); return value
|
||||
})
|
||||
const hoverDefault = 'rgba(255, 255, 255, 0.08)'
|
||||
await flow('Viewer Header 메뉴', viewer, '.hm-sw-viewer-header', readMenu,
|
||||
['--hm-sw-header-more-menu-background', '--hm-sw-header-more-menu-border-color', '--hm-sw-header-more-menu-shadow', '--hm-sw-header-more-item-hover-background'],
|
||||
['--hm-sw-viewer-header-menu-background', '--hm-sw-viewer-header-menu-border-color', '--hm-sw-viewer-header-menu-shadow', '--hm-sw-viewer-header-menu-item-hover-background'],
|
||||
{ face: moreFace, hover: hoverDefault, logo: moreFace })
|
||||
// The shadow token is a full shadow: the C1 / C2 color strings above are not valid shadows, so the shadow keeps its
|
||||
// value there; check it once with a real shadow.
|
||||
await setOn(viewer, '.hm-sw-viewer-header', { '--hm-sw-viewer-header-menu-shadow': `0 0 0 3px ${C2}` })
|
||||
const shadow = await readMenu()
|
||||
await setOn(viewer, '.hm-sw-viewer-header', { '--hm-sw-viewer-header-menu-shadow': null })
|
||||
check('Viewer Header 메뉴: 그림자 전용 토큰', shadow.face.endsWith(`${C2} 0px 0px 0px 3px`) && shadow.logo.endsWith(`${C2} 0px 0px 0px 3px`), JSON.stringify(shadow))
|
||||
await viewer.close()
|
||||
// K guide panel (hover opens it).
|
||||
const guide = await newPage()
|
||||
await open(guide, 'sw-header--k-guide')
|
||||
await guide.locator('.hm-sw-k-guide button').first().hover()
|
||||
await guide.locator('.hm-sw-k-guide__panel').first().waitFor()
|
||||
const readGuide = () => guide.evaluate(() => { const s = getComputedStyle(document.querySelector('.hm-sw-k-guide__panel')); return { border: s.borderTopColor, face: s.backgroundColor } })
|
||||
// Shared token: the border follows --hm-sw-property-border; the face was a literal (own token only).
|
||||
const guideBase = await readGuide()
|
||||
await setOn(guide, '.hm-sw-header', { '--hm-sw-property-border': C1 })
|
||||
const guideShared = await readGuide()
|
||||
await setOn(guide, '.hm-sw-header', { '--hm-sw-header-k-guide-border-color': C2, '--hm-sw-header-k-guide-background': C2 })
|
||||
const guideOwn = await readGuide()
|
||||
await setOn(guide, '.hm-sw-header', { '--hm-sw-property-border': null, '--hm-sw-header-k-guide-border-color': null, '--hm-sw-header-k-guide-background': null })
|
||||
check('K 가이드: 기본값 = 기존 값(#A67D03 / #0E3C2E)', guideBase.border === 'rgb(166, 125, 3)' && guideBase.face === 'rgb(14, 60, 46)', JSON.stringify(guideBase))
|
||||
check('K 가이드: 바깥 범위의 --hm-sw-property-border를 따름', guideShared.border === C1 && guideShared.face === guideBase.face, JSON.stringify(guideShared))
|
||||
check('K 가이드: 전용 토큰 지정이 우선, 지우면 복귀', guideOwn.border === C2 && guideOwn.face === C2 && JSON.stringify(await readGuide()) === JSON.stringify(guideBase), JSON.stringify(guideOwn))
|
||||
await guide.close()
|
||||
// Popup value color: property window inputs (value, gauge value), dropdown triggers, and the SWHeader 속성Bar untouched.
|
||||
const popup = await newPage()
|
||||
await open(popup, 'sw-header--property-panel-compare')
|
||||
const readValue = () => popup.evaluate(() => {
|
||||
const color = selector => [...new Set([...document.querySelectorAll(selector)].map(e => getComputedStyle(e).color))].join(',')
|
||||
return {
|
||||
input: color('.hm-sw-property__value:not([data-readonly]) > .hm-sw-header__input:not(:read-only)'),
|
||||
gauge: color('.hm-sw-property__slider-value:not([data-readonly]) > .hm-sw-header__input:not(:read-only)'),
|
||||
trigger: color('.hm-sw-property__value:not([data-readonly]) .hm-sw-property__trigger'),
|
||||
}
|
||||
})
|
||||
await flow('Popup 값 글자색', popup, '.hm-sw-property', readValue, ['--hm-sw-header-input-color'], ['--hm-sw-popup-value-color'],
|
||||
{ input: 'rgb(230, 4, 3)', gauge: 'rgb(230, 4, 3)', trigger: 'rgb(230, 4, 3)' })
|
||||
await popup.close()
|
||||
const bar = await newPage()
|
||||
await open(bar, 'sw-header--overview')
|
||||
const barColor = () => bar.evaluate(() => [...document.querySelectorAll('.hm-sw-property-bar .hm-sw-header__input, .hm-sw-property-bar__value')].map(e => getComputedStyle(e).color).join(','))
|
||||
const barBefore = await barColor()
|
||||
await setOn(bar, '.hm-sw-header', { '--hm-sw-popup-value-color': C2 })
|
||||
check('Popup 값 토큰은 SWHeader 속성Bar 값에 영향 없음', barBefore.length > 0 && barBefore === await barColor(), barBefore)
|
||||
await bar.close()
|
||||
// Footer command history scrollbar.
|
||||
const footer = await newPage()
|
||||
await open(footer, 'sw-footer--full-footer')
|
||||
const readBar = () => footer.evaluate(() => ({ scrollbar: getComputedStyle(document.querySelector('.hm-sw-footer-command__history')).scrollbarColor }))
|
||||
const footerBase = await readBar()
|
||||
await setOn(footer, '.hm-sw-footer-command', { '--hm-sw-footer-command-scrollbar-thumb': C2, '--hm-sw-footer-command-scrollbar-track': C1 })
|
||||
const footerOwn = await readBar()
|
||||
await setOn(footer, '.hm-sw-footer-command', { '--hm-sw-footer-command-scrollbar-thumb': null, '--hm-sw-footer-command-scrollbar-track': null })
|
||||
check('Footer 명령창 스크롤바: 기본값 = 기존 값(#574B30 / transparent)', footerBase.scrollbar === 'rgb(87, 75, 48) rgba(0, 0, 0, 0)', JSON.stringify(footerBase))
|
||||
check('Footer 명령창 스크롤바: 전용 토큰 지정이 우선, 지우면 복귀', footerOwn.scrollbar === `${C2} ${C1}` && JSON.stringify(await readBar()) === JSON.stringify(footerBase), JSON.stringify(footerOwn))
|
||||
await footer.close()
|
||||
await header.close()
|
||||
}
|
||||
|
||||
// ---------- 2. Regression: new cascade vs legacy cascade on every existing SW Story ----------
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const outermost = ':is(.hm-sw-header, .hm-sw-theme):not(:where(.hm-sw-header, .hm-sw-theme) *)'
|
||||
|
||||
Reference in new issue
Block a user