diff --git a/packages/sw/tests/ai-template-consumer.browser.mjs b/packages/sw/tests/ai-template-consumer.browser.mjs index b4b1abf..ad917cc 100644 --- a/packages/sw/tests/ai-template-consumer.browser.mjs +++ b/packages/sw/tests/ai-template-consumer.browser.mjs @@ -41,7 +41,7 @@ await page.evaluate(() => document.fonts.ready) const assets = await page.evaluate(() => [...document.images].map(image => ({ src: image.currentSrc.slice(0, 30), ok: image.complete && image.naturalWidth > 0 }))) check("1 all SVG/PNG assets decode (data URI, no external path)", assets.length > 0 && assets.every(item => item.ok && item.src.startsWith("data:")), { count: assets.length, broken: assets.filter(item => !item.ok) }) const tokens = await page.evaluate(() => { const s = getComputedStyle(document.querySelector('[data-example="b"] .hm-sw-theme')); return { height: s.getPropertyValue("--hm-sw-header-height").trim(), font: s.fontFamily } }) -check("1 styles.css loaded; SWTheme provides tokens", tokens.height === "49px" && tokens.font.includes("Noto Sans KR"), tokens) +check("1 styles.css loaded; SWTheme provides tokens", tokens.height === "49px" && tokens.font.startsWith("Dotum"), tokens) // 2. Example A: default header unchanged (Figma 1920 coordinates) const a = section("a") diff --git a/packages/sw/tests/footer-design-review.browser.mjs b/packages/sw/tests/footer-design-review.browser.mjs new file mode 100644 index 0000000..ef5105c --- /dev/null +++ b/packages/sw/tests/footer-design-review.browser.mjs @@ -0,0 +1,139 @@ +// SW Footer design review (2026-10-20, Dotum default): the four open design items and the Model / Layout text rule, +// measured in the real Storybook (`SW/Footer › 전체 Footer · 실제 조합`). Computed values + element screenshots for +// each state; design judgement (PASS / REVIEW) is written in the report, this script only records facts and checks +// the approved, unambiguous rules. +// Usage: node packages/sw/tests/footer-design-review.browser.mjs [storybook-url] [evidence-dir] +import { createRequire } from 'node:module' +import { mkdir, writeFile } from 'node:fs/promises' +import path from 'node:path' + +const runtime = process.env.SW_RUNTIME_MODULES ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules' +const require = createRequire(path.join(runtime, 'index.js')) +const { chromium } = require('playwright') +const [storybook = 'http://127.0.0.1:6024', outArg = 'docs/sw/sw-dotum-font-qa/footer-design'] = process.argv.slice(2) +const out = path.resolve(outArg); await mkdir(out, { recursive: true }) +const results = [], facts = {}, errors = [] +const check = (name, pass, detail = '') => { results.push({ name, result: pass ? 'PASS' : 'FAIL', detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail !== '' ? ` — ${JSON.stringify(detail)}` : ''}`) } +const GRADIENT = 'linear-gradient(rgb(218, 104, 19) 8.52%, rgb(255, 187, 137) 37.93%, rgb(229, 105, 13) 71.85%, rgb(174, 54, 7) 92.77%)' +const browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' }) +const page = await browser.newPage({ viewport: { width: 2400, height: 1400 }, reducedMotion: 'reduce' }) +page.on('pageerror', e => errors.push(e.message)); page.on('console', m => { if (m.type() === 'error' && !/favicon|404/.test(m.text())) errors.push(m.text()) }) +await page.goto(`${storybook}/iframe.html?id=sw-footer--full-footer&viewMode=story`) +await page.waitForSelector('.hm-sw-footer'); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(400) +const footer = page.locator('.hm-sw-footer').first() +await footer.scrollIntoViewIfNeeded() + +const style = (loc, pseudo = null) => loc.evaluate((e, pseudo) => { + const c = getComputedStyle(e, pseudo), r = e.getBoundingClientRect() + return { x: +r.x.toFixed(1), y: +r.y.toFixed(1), w: +r.width.toFixed(1), h: +r.height.toFixed(1), color: c.color, bg: c.backgroundImage === 'none' ? c.backgroundColor : c.backgroundImage.replace(/url("data:[^"]{0,4000}")/g, 'url(data)'), opacity: c.opacity, font: c.fontFamily.split(',')[0], size: c.fontSize, weight: c.fontWeight, shadow: c.textShadow === 'none' ? 'none' : 'outline', mask: (c.maskImage || c.webkitMaskImage || '').slice(0, 60) } +}, pseudo) +// Element shot (scrolls into view; the hover state is kept because the pointer stays over the element). +const shot = (loc, name) => loc.screenshot({ path: path.join(out, `${name}.png`) }) +const away = () => page.mouse.move(5, 5) + +// 0. Token value +const token = await footer.evaluate(e => getComputedStyle(e).getPropertyValue('--hm-sw-footer-gra-border-orange').trim()) +facts.token = token +check('Token --hm-sw-footer-gra-border-orange = user gradient (#DA6813 8.52% / #FFBB89 37.93% / #E5690D 71.85% / #AE3607 92.77%)', token.replace(/\s+/g, ' ').toLowerCase() === 'linear-gradient(180deg, #da6813 8.52%, #ffbb89 37.93%, #e5690d 71.85%, #ae3607 92.77%)', token) + +// 1. 전체보기 side buttons hover (보조기능 / 명령어) +facts.side = [] +for (const [i, label] of [[0, 'aux'], [1, 'commands']]) { + const side = footer.locator('.hm-sw-footer-side').nth(i) + await away(); await page.waitForTimeout(100) + const d = { box: await style(side), ring: await style(side, '::after'), title: await style(side.locator('.hm-sw-footer-side__title')), sub: await style(side.locator('.hm-sw-footer-side__subtitle')) } + await shot(side, `side-${label}-default`) + await side.hover(); await page.waitForTimeout(150) + const h = { box: await style(side), ring: await style(side, '::after'), title: await style(side.locator('.hm-sw-footer-side__title')), sub: await style(side.locator('.hm-sw-footer-side__subtitle')) } + await shot(side, `side-${label}-hover`) + facts.side.push({ label, default: d, hover: h }) + check(`전체보기(${label}) hover: ring over the whole button (opacity 0 → 1, gra-border-orange, same box)`, d.ring.opacity === '0' && h.ring.opacity === '1' && h.ring.bg === GRADIENT && h.ring.w === h.box.w && h.ring.h === h.box.h, { ring: h.ring.w + '×' + h.ring.h, box: h.box.w + '×' + h.box.h }) + check(`전체보기(${label}) hover: both text lines #FF5C00`, h.title.color === 'rgb(255, 92, 0)' && h.sub.color === 'rgb(255, 92, 0)', [h.title.color, h.sub.color]) + const rel = s => ({ w: s.box.w, h: s.box.h, tx: +(s.title.x - s.box.x).toFixed(1), ty: +(s.title.y - s.box.y).toFixed(1), tw: s.title.w, sx: +(s.sub.x - s.box.x).toFixed(1), sy: +(s.sub.y - s.box.y).toFixed(1), sw: s.sub.w }) + check(`전체보기(${label}) hover: size and text boxes (relative to the button) unchanged`, JSON.stringify(rel(d)) === JSON.stringify(rel(h)), [rel(d), rel(h)]) + check(`전체보기(${label}) text = Dotum 14px`, d.title.font === 'Dotum' && d.title.size === '14px', d.title.font) +} +await away() + +// 2. + buttons: category row add (below 명령어 전체보기 side) and Layout add +facts.add = {} +for (const [sel, name] of [['.hm-sw-footer-menu__add', 'category-add'], ['.hm-sw-footer-tabs__add', 'layout-add']]) { + const btn = footer.locator(sel).first() + if (!(await btn.count())) { check(`${name}: present`, false); continue } + const d = { box: await style(btn), before: await style(btn, '::before') } + await shot(btn, `${name}-default`, 4) + await btn.hover(); await page.waitForTimeout(150) + const h = { box: await style(btn), before: await style(btn, '::before') } + await shot(btn, `${name}-hover`, 4) + facts.add[name] = { default: d, hover: h } + check(`${name} hover: same size`, d.box.w === h.box.w && d.box.h === h.box.h, `${h.box.w}×${h.box.h}`) + await away() +} +const cat = facts.add['category-add'].hover +check('category + hover: icon swaps to the orange + (menu-add-hover), no extra surface', facts.add['category-add'].default.box.bg !== cat.box.bg && cat.box.bg.startsWith('url(') && !cat.box.bg.includes('linear-gradient'), 'background image changes on hover') +const lay = facts.add['layout-add'].hover +check('Layout + hover: gra-border-orange surface masked to the button shape, + icon on top', lay.before.bg.startsWith('url(') && lay.before.bg.includes('rgb(218, 104, 19)') && lay.before.mask.startsWith('url('), 'url(+) , gra-border-orange ; mask=button shape') +// click keeps working +const addCalls = await page.evaluate(() => document.body.innerText.match(/onSlotAdd|onCategoryAdd|onLayoutAdd|onAdd/g)?.length ?? 0) +facts.addCallsBefore = addCalls + +// 3. Model tab: selected vs not selected, Layout text rule +const tabs = footer.locator('.hm-sw-footer-tab') +const tabInfo = async loc => ({ tab: await style(loc), before: await style(loc, '::before'), text: await style(loc.locator('.hm-sw-footer-tab__label, .hm-sw-footer-tab__select').first()) }) +const model = footer.locator('.hm-sw-footer-tab--model').first() +const modelState = await model.getAttribute('data-state') +facts.model = { initialState: modelState, selected: null, unselected: null } +// Make a Layout selected so the Model is unselected, then reselect the Model. +const layoutTabs = footer.locator('.hm-sw-footer-tab:not(.hm-sw-footer-tab--model)') +const layoutCount = await layoutTabs.count() +if (modelState === 'on') { facts.model.selected = await tabInfo(model); await shot(model, 'model-selected', 4) } +await layoutTabs.first().locator('.hm-sw-footer-tab__select').click(); await away(); await page.waitForTimeout(150) +facts.model.unselected = await tabInfo(model); await shot(model, 'model-unselected', 4) +facts.layoutSelected = await tabInfo(layoutTabs.first()); await shot(layoutTabs.first(), 'layout-selected', 4) +await model.hover(); await page.waitForTimeout(150); facts.model.unselectedHover = await tabInfo(model); await shot(model, 'model-unselected-hover', 4); await away() +await model.locator('.hm-sw-footer-tab__select').click(); await away(); await page.waitForTimeout(150) +facts.model.selected = await tabInfo(model); await shot(model, 'model-selected', 4) +const mu = facts.model.unselected, ms = facts.model.selected +check('Model selected: gra-border-orange surface (::before) masked to the model shape', ms.before.bg === GRADIENT && ms.before.mask.startsWith('url('), ms.before.bg.slice(0, 60)) +check('Model not selected: its own tab image surface (tab-model.svg, beige), no orange ::before', mu.tab.bg.startsWith('url(') && !mu.before.bg.includes('rgb(218, 104, 19)'), { tab: mu.tab.bg, before: mu.before.bg }) +check('Model text #161410 12px 400 no outline — selected, unselected and hover identical', [ms, mu, facts.model.unselectedHover].every(s => s.text.color === 'rgb(22, 20, 16)' && s.text.size === '12px' && s.text.weight === '400' && s.text.shadow === 'none') && ['x', 'y', 'w', 'h'].every(k => ms.text[k] === mu.text[k] && mu.text[k] === facts.model.unselectedHover.text[k]), [ms.text, mu.text].map(t => `${t.color} ${t.size} ${t.weight} ${t.font}`)) +// Layout default vs hover text (non-selected layouts) +facts.layoutHover = [] +for (let i = 1; i < layoutCount; i++) { + const t = layoutTabs.nth(i) + if ((await t.getAttribute('data-state')) === 'on') continue + const d = await tabInfo(t); await shot(t, `layout-${i}-default`, 4) + await t.hover(); await page.waitForTimeout(150) + const h = await tabInfo(t); await shot(t, `layout-${i}-hover`, 4); await away() + facts.layoutHover.push({ i, d, h }) + check(`Layout ${i} default/hover text: #161410 12px 400, same box (Dotum)`, [d, h].every(s => s.text.color === 'rgb(22, 20, 16)' && s.text.size === '12px' && s.text.weight === '400' && s.text.font === 'Dotum') && ['x', 'y', 'w', 'h'].every(k => d.text[k] === h.text[k]), `${d.text.color} → ${h.text.color}, ${d.text.w}×${d.text.h}`) +} +// restore the initial selection +if (modelState !== 'on') await layoutTabs.first().locator('.hm-sw-footer-tab__select').click() + +// 4. Category pocket surface for 도형 / 수정 / 편집 / 문자 (and every other category) +const catTabs = footer.locator('.hm-sw-footer-menu__tab') +const catNames = await catTabs.allTextContents() +facts.categories = [] +for (let i = 0; i < catNames.length; i++) { + await catTabs.nth(i).click(); await away(); await page.waitForTimeout(150) + const info = await footer.evaluate(f => { + const tab = f.querySelector('.hm-sw-footer-menu__tab[aria-selected="true"]'), tr = tab.getBoundingClientRect() + const vacant = f.querySelector('.hm-sw-footer__vacant'), vr = vacant?.getBoundingClientRect() + const slots = f.querySelectorAll('.hm-sw-footer__slots > *, .hm-sw-footer-slot').length + // What is painted right behind the open pocket (just above the tab's top edge, across its width)? + const xs = [0.1, 0.5, 0.9].map(p => tr.x + tr.width * p) + const behind = xs.map(x => { const els = document.elementsFromPoint(x, tr.y - 2); const e = els.find(el => el !== tab && !tab.contains(el)); return e ? (e.className.baseVal ?? e.className).toString().split(' ')[0] : '' }) + return { name: tab.textContent.trim(), tab: [Math.round(tr.x), Math.round(tr.y), Math.round(tr.width), Math.round(tr.height)], vacantBottom: vr ? Math.round(vr.bottom) : null, vacantLeft: vr ? Math.round(vr.left) : null, tabTop: Math.round(tr.top), slots, behind } + }) + facts.categories.push(info) + await shot(footer.locator('.hm-sw-footer__group').first(), `category-${i}-${info.name.replace(/[\\/]/g, '_')}`, 2) +} +const pocketOk = facts.categories.every(c => c.vacantBottom === null || c.vacantBottom <= c.tabTop + 1) +check('Categories: vacant surface never reaches the selected tab pocket (no split surface) — every category', pocketOk, facts.categories.map(c => `${c.name}: vacant↓${c.vacantBottom} tab↑${c.tabTop} behind=${[...new Set(c.behind)].join('/')}`)) +check('Categories: one surface behind the whole pocket (left / middle / right) — every category', facts.categories.every(c => new Set(c.behind).size === 1), facts.categories.map(c => `${c.name}:${[...new Set(c.behind)].join('/')}`)) +check('Categories: tab text Dotum', (await catTabs.first().evaluate(e => getComputedStyle(e).fontFamily)).startsWith('Dotum')) +check('Console / page error 없음', errors.length === 0, errors.slice(0, 3)) +await browser.close() +await writeFile(path.join(out, 'footer-design-review.json'), JSON.stringify({ storybook, results, facts, errors }, null, 2) + '\n') +console.log(`${results.filter(r => r.result === 'PASS').length}/${results.length} PASS`) diff --git a/packages/sw/tests/theme-inheritance.browser.mjs b/packages/sw/tests/theme-inheritance.browser.mjs index 007855e..1e6ebab 100644 --- a/packages/sw/tests/theme-inheritance.browser.mjs +++ b/packages/sw/tests/theme-inheritance.browser.mjs @@ -1,7 +1,7 @@ // SW theme token inheritance (2026-10-20, A-1). // 1) SW/Theme › Theme Inheritance: a token set through `style` on the outermost SW scope reaches the header, dropdowns, // More, panels, footer command line and tooltips; portals get it only when wrapped in a themed ; areas do -// not mix; unthemed components keep Noto Sans KR. Dotum is a verification value only (not the SW default). +// not mix; unthemed components keep the SW default Dotum (2026-10-20). Noto Sans KR is a verification value only. // 2) Regression: every existing SW Story renders identically with the new cascade and with the legacy cascade // (token blocks re-declared on every `.hm-sw-header, .hm-sw-theme`, i.e. the code before this change): computed // font / color / geometry of every SW element and an element screenshot comparison. No Story sets an override, @@ -22,9 +22,9 @@ const [storybook = 'http://127.0.0.1:6024', outArg] = process.argv.slice(2) const out = path.resolve(outArg ?? 'docs/sw/theme-inheritance-qa') await mkdir(out, { recursive: true }) -const VERIFY = 'Dotum, 돋움, sans-serif' +const VERIFY = '"Noto Sans KR", sans-serif' const NESTED = '"Malgun Gothic", sans-serif' -const DEFAULT = '"Noto Sans KR", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif' +const DEFAULT = 'Dotum, 돋움, "Malgun Gothic", "Apple SD Gothic Neo", sans-serif' 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 = [] @@ -67,52 +67,52 @@ const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${fa // Default (no theme): header, then its sample dropdown and More. 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`))) + check('기본: 단독 SWViewerHeader = 기본 Dotum', 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`))) + check('기본: Dropdown = 기본 Dotum', 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 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`))) + check('기본: More = 기본 Dotum', only(await fonts(page, `${def} .hm-sw-viewer-menu`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-menu`))) await page.keyboard.press('Escape') // Viewer, themed through . 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`))) + check('뷰어 테마: SWViewerHeader 전체 = 검증 글꼴', 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`))) + check('뷰어 테마: Dropdown = 검증 글꼴', 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 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`))) + check('뷰어 테마: More = 검증 글꼴', 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. const sub = page.locator(`${viewer} .hm-sw-viewer-menu[data-surface="beige"] [aria-haspopup="menu"]`).first() if (await sub.count()) { await sub.hover() await page.waitForTimeout(150) - check('뷰어 테마: More 서브메뉴 = Dotum', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`))) + check('뷰어 테마: More 서브메뉴 = 검증 글꼴', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`))) } await page.keyboard.press('Escape') 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`))}`) + check('뷰어 테마: SWViewerPanel(제목·입력·버튼·스위치·힌트) = 검증 글꼴', 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() }) // General mode: SWHeader (+ open layer dropdown, window-move tooltip), SWFooter (+ command line, guide tooltip). const general = scope('general-themed') const headerFonts = await fonts(page, `${general} .hm-sw-header`, '.hm-sw-header__motion-tooltip') - check('일반 테마: SWHeader + 열린 레이어 Dropdown = Dotum (창 이동 Tooltip 별도)', only(headerFonts, VERIFY), fmt(headerFonts)) + check('일반 테마: SWHeader + 열린 레이어 Dropdown = 검증 글꼴 (창 이동 Tooltip 별도)', only(headerFonts, VERIFY), fmt(headerFonts)) const tooltip = await page.locator(`${general} .hm-sw-header__motion-tooltip`).evaluate(element => getComputedStyle(element).fontFamily) check('일반 테마: 창 이동 Tooltip = Inter → 테마 글꼴', tooltip === `Inter, ${VERIFY}`, tooltip) const layerMenu = await page.locator(`${general} .hm-sw-header`).getByText('A-WALL').count() check('일반 테마: 레이어 Dropdown이 열린 상태(목록 행 A-WALL 표시)', layerMenu > 0, `${layerMenu} row`) - check('일반 테마: SWFooter = Dotum', only(await fonts(page, `${general} .hm-sw-footer`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer`))) - check('일반 테마: 명령창(SWFooterCommandLine, 중첩 hm-sw-theme) = Dotum', only(await fonts(page, `${general} .hm-sw-footer-command`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer-command`))) + check('일반 테마: SWFooter = 검증 글꼴', only(await fonts(page, `${general} .hm-sw-footer`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer`))) + check('일반 테마: 명령창(SWFooterCommandLine, 중첩 hm-sw-theme) = 검증 글꼴', only(await fonts(page, `${general} .hm-sw-footer-command`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer-command`))) await page.locator(`${general} .hm-sw-footer-add`).first().focus() await page.waitForTimeout(150) const guide = page.locator(`${general} .hm-sw-footer [role="tooltip"]`) - check('일반 테마: 명령 등록 안내(Tooltip) = Dotum', (await guide.count()) > 0 && only(await fonts(page, `${general} .hm-sw-footer [role="tooltip"]`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer [role="tooltip"]`))) + check('일반 테마: 명령 등록 안내(Tooltip) = 검증 글꼴', (await guide.count()) > 0 && only(await fonts(page, `${general} .hm-sw-footer [role="tooltip"]`), VERIFY), fmt(await fonts(page, `${general} .hm-sw-footer [role="tooltip"]`))) // Portals. const [plain, themed, insideSource] = await page.evaluate(() => { @@ -122,15 +122,15 @@ const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${fa !!document.querySelector('[data-theme-case="portal-source"] [data-portal]')] }) check('Portal: DOM이 테마 영역 밖에 렌더링됨', !insideSource) - check('Portal: SWTheme만 감싸면 기본 글꼴(테마 미전달)', plain.startsWith('"Noto Sans KR"'), plain) + check('Portal: SWTheme만 감싸면 기본 글꼴(테마 미전달)', plain === DEFAULT, plain) check('Portal: 같은 theme style의 SWTheme으로 감싸면 테마 적용', themed === VERIFY, themed) // Areas. const area = name => fonts(page, `${scope(name)} .hm-sw-viewer-hint`) - check('영역 A(Dotum)', only(await area('area-a'), VERIFY), fmt(await area('area-a'))) + check('영역 A(검증 글꼴)', only(await area('area-a'), VERIFY), fmt(await area('area-a'))) check('영역 B(기본) — 옆 영역 값이 섞이지 않음', only(await area('area-b'), DEFAULT), fmt(await area('area-b'))) const outer = await page.locator(`${scope('area-c')} > .hm-sw-viewer-hint`).evaluate(element => getComputedStyle(element).fontFamily) - check('영역 C 바깥(Dotum)', outer === VERIFY, outer) + check('영역 C 바깥(검증 글꼴)', outer === VERIFY, outer) check('영역 C 안쪽 SWTheme style(Malgun Gothic)만 안쪽에 적용', only(await area('area-c-nested'), NESTED), fmt(await area('area-c-nested'))) await page.screenshot({ path: path.join(out, 'theme-inheritance-full.png'), fullPage: true }) await page.close() @@ -213,7 +213,7 @@ for (const width of [1920, 1440, 1180, 900, 560]) for (const id of ids) { 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') - check('대조: 기존 구조 주입 시 More·명령창이 Noto로 되돌아감(결함 재현)', only(more, DEFAULT) && only(command, DEFAULT), `More ${fmt(more)} · 명령창 ${fmt(command)}`) + check('대조: 기존 구조 주입 시 More·명령창이 기본 글꼴로 되돌아감(결함 재현)', only(more, DEFAULT) && only(command, DEFAULT), `More ${fmt(more)} · 명령창 ${fmt(command)}`) await page.close() } const failedRegression = regression.filter(item => item.result !== 'PASS') diff --git a/src/sw/sw-theme.stories.tsx b/src/sw/sw-theme.stories.tsx index a389af1..bc4c2d8 100644 --- a/src/sw/sw-theme.stories.tsx +++ b/src/sw/sw-theme.stories.tsx @@ -14,9 +14,9 @@ import "./sw-viewer-stories.css" // SW theme token inheritance (2026-10-20). The official way to change SW tokens: set them through `style` (or the // consumer's own class) on the OUTERMOST SW scope — around the screen, or the component root when it is // used alone. Nested SW roots (viewer menus, More, panels, footer command line) inherit the value. -// Dotum is used here ONLY to make the inheritance visible: it is NOT the SW default font (design approval pending). +// The SW default font is Dotum (2026-10-20). Noto Sans KR is used here ONLY to make the inheritance visible. const KICKER = "HM DESIGN SYSTEM / SW" -const VERIFY_FONT = 'Dotum, "돋움", sans-serif' +const VERIFY_FONT = '"Noto Sans KR", sans-serif' const VERIFY_THEME = { "--hm-sw-header-font-family": VERIFY_FONT } as CSSProperties const NESTED_THEME = { "--hm-sw-header-font-family": '"Malgun Gothic", sans-serif' } as CSSProperties @@ -77,8 +77,8 @@ export const ThemeInheritance: Story = { render: () => ( SW 토큰은 가장 바깥 SW 범위(<SWTheme> 또는 단독 컴포넌트 루트)에서 한 번 선언되고, 안쪽 Dropdown·More·패널·명령창은 그 값을 상속합니다. 토큰은 가장 바깥 범위의 style로만 바꿉니다. 아래 Dotum은 상속 확인용 검증 값이며 SW 기본 글꼴(Noto Sans KR)은 바뀌지 않습니다(Dotum 기본 적용은 디자인 승인 대기).} /> - + description={<>SW 토큰은 가장 바깥 SW 범위(<SWTheme> 또는 단독 컴포넌트 루트)에서 한 번 선언되고, 안쪽 Dropdown·More·패널·명령창은 그 값을 상속합니다. 토큰은 가장 바깥 범위의 style로만 바꿉니다. SW 기본 글꼴은 Dotum이며, 아래 Noto Sans KR은 상속 확인용 검증 값입니다.} /> + @@ -105,10 +105,10 @@ export const ThemeInheritance: Story = {
- 영역 A — 검증 글꼴(Dotum) - 영역 B — 기본(Noto Sans KR) + 영역 A — 검증 글꼴(Noto Sans KR) + 영역 B — 기본(Dotum) - 영역 C 바깥 — Dotum + 영역 C 바깥 — Noto Sans KR 영역 C 안쪽 — Malgun Gothic