feat(sw): viewer follow-up — SVG icon outline, shared SW popup frame, story grid, green layer buttons
- A: SWViewerIcon draws the SWHeader black outline as SVG stroke on a copy behind the icon (no CSS filter) - B: SWViewerPanel renders the common SWPopup frame (same DOM / CSS) and adds drag, corner resize, collapse, bounds and modal; SWPopup gains optional titleActions / footer; title art spans any width - C: shared Story canvas grid (--sw-story-canvas) behind the SW Header work area and dropdown room - D: SWViewerButton tone="green" for 전체 켜기 / 전체 끄기: default kept, hover = primary (확인) hover token, pressed = primary face - AI guide, verification scripts (76 checks, element regression) and evidence updated
This commit is contained in:
1 parent
7f42923ccf
commit
68fbcf6e15
45 files changed
+1463
-377
No files matched your search
@@ -157,7 +157,7 @@ if (comparison.reference) {
|
||||
check("corner resize changes size", resized.width < sized.width - 40 && resized.height < sized.height - 80, `${Math.round(sized.width)}x${Math.round(sized.height)} → ${Math.round(resized.width)}x${Math.round(resized.height)}`)
|
||||
// Drag: title bar moves the panel and it stays inside the frame.
|
||||
const before = await box(layerPanel)
|
||||
const title = layerPanel.locator('.hm-sw-viewer-panel__titlebar')
|
||||
const title = layerPanel.locator('.hm-sw-property__titlebar')
|
||||
const titleBox = await box(title)
|
||||
await page.mouse.move(titleBox.x + 40, titleBox.y + 15)
|
||||
await page.mouse.down()
|
||||
@@ -175,16 +175,29 @@ if (comparison.reference) {
|
||||
`right gap ${Math.round(frame.x + frame.width - (clamped.x + clamped.width))}, bottom gap ${Math.round(frame.y + frame.height - (clamped.y + clamped.height))}`)
|
||||
// Collapse by button and double click.
|
||||
await layerPanel.getByRole('button', { name: '접기' }).click()
|
||||
check('collapse hides body', (await layerPanel.locator('.hm-sw-viewer-panel__body').count()) === 0)
|
||||
check('collapse hides body', !(await layerPanel.locator('.hm-sw-property__body').isVisible()))
|
||||
await title.dblclick({ position: { x: 30, y: 15 } })
|
||||
check('title double click expands', (await layerPanel.locator('.hm-sw-viewer-panel__body').count()) === 1)
|
||||
check('title double click expands', await layerPanel.locator('.hm-sw-property__body').isVisible())
|
||||
// Panel content works (layer eye).
|
||||
// 전체 켜기 / 전체 끄기: green on hover and while pressed, default otherwise; other panel buttons unchanged.
|
||||
const allOn = layerPanel.getByRole('button', { name: '전체 켜기' })
|
||||
const face = locator => locator.evaluate(element => [getComputedStyle(element).backgroundColor, getComputedStyle(element).color].join(' '))
|
||||
const restFace = await face(allOn)
|
||||
await allOn.hover()
|
||||
const hoverFace = await face(allOn)
|
||||
await page.mouse.down()
|
||||
const activeFace = await face(allOn)
|
||||
await page.mouse.up()
|
||||
const afterFace = await face(allOn)
|
||||
await page.mouse.move(5, 5)
|
||||
const leftFace = await face(allOn)
|
||||
check('전체 켜기: default kept, green hover / active, back after release', restFace === 'rgb(255, 255, 255) rgb(18, 51, 40)' && hoverFace === 'rgb(38, 102, 81) rgb(255, 255, 255)' && activeFace === 'rgb(18, 51, 40) rgb(255, 255, 255)' && afterFace === hoverFace && leftFace === restFace, [restFace, hoverFace, activeFace, afterFace, leftFace].join(' | '))
|
||||
await layerPanel.getByRole('button', { name: 'CR-TEXT 숨기기' }).click()
|
||||
check('panel content callback', (await log(page)).includes('layer:CR-TEXT=false'))
|
||||
// Close button hover face.
|
||||
const close = layerPanel.getByRole('button', { name: '닫기' })
|
||||
await close.hover()
|
||||
check('close hover #E40000 / white X', (await close.evaluate(element => [getComputedStyle(element).getPropertyValue('--hm-sw-viewer-close-glow').trim(), getComputedStyle(element).color].join(' '))) === '#e40000 rgb(255, 255, 255)')
|
||||
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)
|
||||
// Two panels: the last touched comes to the front.
|
||||
@@ -192,12 +205,17 @@ if (comparison.reference) {
|
||||
await page.getByRole('button', { name: '설정', exact: true }).click()
|
||||
const options = page.getByRole('dialog', { name: '옵션' })
|
||||
const z = async () => Promise.all([layerPanel, options].map(item => item.evaluate(element => Number(getComputedStyle(element).zIndex))))
|
||||
await layerPanel.locator('.hm-sw-viewer-panel__titlebar').click({ position: { x: 30, y: 15 } })
|
||||
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: /라이선스/ })
|
||||
const confirm = about.getByRole('button', { name: '확인' })
|
||||
await confirm.hover()
|
||||
const confirmHover = await face(confirm)
|
||||
check('전체 켜기 hover = 라이선스 확인 hover (same token)', confirmHover === hoverFace, `${confirmHover} / ${hoverFace}`)
|
||||
await about.locator('.hm-sw-property__title').hover()
|
||||
check('modal popup opens with backdrop', (await about.getAttribute('aria-modal')) === 'true' && (await page.locator('.hm-sw-viewer-panel-backdrop').count()) === 1)
|
||||
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)))
|
||||
@@ -343,7 +361,13 @@ if (comparison.reference) {
|
||||
return {
|
||||
label: style('.hm-sw-viewer-header__label').textShadow, document: style('.hm-sw-viewer-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: style('.hm-sw-viewer-header__icon').filter, chevron: style('.hm-sw-viewer-header__chevron').filter,
|
||||
count: style('.hm-sw-viewer-header__count').textShadow,
|
||||
icon: (() => {
|
||||
const icon = header.querySelector('.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) }
|
||||
})(),
|
||||
labelColor: style('.hm-sw-viewer-header__label').color,
|
||||
labelFont: [style('.hm-sw-viewer-header__label').fontSize, style('.hm-sw-viewer-header__label').fontWeight, style('.hm-sw-viewer-header__label').letterSpacing].join(' '),
|
||||
}
|
||||
@@ -351,7 +375,7 @@ if (comparison.reference) {
|
||||
const blacks = value => value.split('rgb(0, 0, 0)').length - 1
|
||||
check('text outline 1.3px black on 14px text (label / file name / tab)', [outline.label, outline.document, outline.tab].every(value => value.includes('rgb(0, 0, 0) 1.3px 0px 0px') && blacks(value) === 16), outline.label.slice(0, 60))
|
||||
check('label outline 1px black on 11px captions / counts', [outline.caption, outline.count].every(value => value.includes('rgb(0, 0, 0) 1px 0px 0px') && blacks(value) === 16), outline.caption.slice(0, 60))
|
||||
check('icon outline: 4 x 1px black drop-shadows + #061A14 glow', [outline.icon, outline.chevron].every(value => (value.match(/drop-shadow/g) ?? []).length === 5 && value.includes('rgb(0, 0, 0) 1px 0px 0px') && value.includes('rgb(6, 26, 20)')), outline.icon.slice(0, 90))
|
||||
check('icon outline: SVG stroke copy, no CSS filter', outline.icon.filter === 'none' && outline.icon.copyStroke === 'rgb(0, 0, 0)' && outline.icon.copyWidth === outline.icon.glyphWidth + 3 && outline.icon.glyphStroke !== 'rgb(0, 0, 0)', JSON.stringify(outline.icon))
|
||||
check('fill / size / weight / spacing kept', outline.labelColor === 'rgb(255, 255, 255)' && outline.labelFont === '14px 500 normal', outline.labelColor + ' ' + outline.labelFont)
|
||||
await page.close()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user