test(sw): align browser tests and theme story with Noto Sans KR and Bold
Font expectations move back to Noto Sans KR (Dotum becomes the theme inheritance verification value), viewer labels expect 700, and the new text-weight.browser.mjs checks the requested Bold scope and outlines.
This commit is contained in:
1 parent
033e24246a
commit
ef69f5143b
9 files changed
+205
-38
No files matched your search
@@ -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.startsWith("Dotum"), tokens)
|
||||
check("1 styles.css loaded; SWTheme provides tokens", tokens.height === "49px" && tokens.font.includes("Noto Sans KR"), tokens)
|
||||
|
||||
// 2. Example A: default header unchanged (Figma 1920 coordinates)
|
||||
const a = section("a")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SW Footer design review (2026-10-20, Dotum default): the four open design items and the Model / Layout text rule,
|
||||
// SW Footer design review (2026-10-20; SW default font Noto Sans KR since the Dotum default was withdrawn): 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.
|
||||
@@ -10,7 +10,7 @@ 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 [storybook = 'http://127.0.0.1:6024', outArg = 'docs/sw/sw-noto-bold-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)}` : ''}`) }
|
||||
@@ -51,7 +51,7 @@ for (const [i, label] of [[0, 'aux'], [1, 'commands']]) {
|
||||
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)
|
||||
check(`전체보기(${label}) text = Noto Sans KR 14px`, d.title.font === '"Noto Sans KR"' && d.title.size === '14px', d.title.font)
|
||||
}
|
||||
await away()
|
||||
|
||||
@@ -106,7 +106,7 @@ for (let i = 1; i < layoutCount; i++) {
|
||||
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}`)
|
||||
check(`Layout ${i} default/hover text: #161410 12px 400, same box (Noto Sans KR)`, [d, h].every(s => s.text.color === 'rgb(22, 20, 16)' && s.text.size === '12px' && s.text.weight === '400' && s.text.font === '"Noto Sans KR"') && ['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()
|
||||
@@ -132,7 +132,7 @@ for (let i = 0; i < catNames.length; i++) {
|
||||
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('Categories: tab text Noto Sans KR', (await catTabs.first().evaluate(e => getComputedStyle(e).fontFamily)).startsWith('"Noto Sans KR"'))
|
||||
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')
|
||||
|
||||
@@ -93,7 +93,7 @@ check('Footer: 화면 폭 변화 keeps the 6 widths, 참고 좁은 화면 + Figm
|
||||
// 2. Popup layer / color = the property window pickers (SWPopupPropertyRow); SWHeader field dropdowns stay header-only.
|
||||
await goto('sw-header--overview')
|
||||
const headerField = await page.locator('.hm-sw-header .hm-sw-header__field--layer').first().evaluate(e => { const c = getComputedStyle(e), v = getComputedStyle(e.querySelector('.hm-sw-header__field-value')); return [c.color, c.height, v.color, v.fontWeight].join('|') })
|
||||
// 2026-10-20: field values are Dotum Regular (400), Figma text property (hover-stability.browser.mjs).
|
||||
// 2026-10-20: SWHeader field values are Regular (400) (user decision, kept with Noto Sans KR; hover-stability.browser.mjs).
|
||||
check('Header: layer field keeps the header design (#F6F4F2, 38px, value 400)', headerField === 'rgb(246, 244, 242)|38px|rgb(246, 244, 242)|400', headerField)
|
||||
await goto('sw-popup--custom-content')
|
||||
const rowSig = loc => loc.evaluate(row => { const s = e => { const c = getComputedStyle(e); return [c.color, c.backgroundColor, c.backgroundImage.slice(0, 30), c.borderTopColor, c.height, c.fontSize, c.fontWeight, c.textShadow].join(',') }; return ['.hm-sw-property__value', '.hm-sw-property__trigger, .hm-sw-header__input', 'label'].map(q => row.querySelector(q) ? s(row.querySelector(q)) : '-').join(' / ') + ' / ' + row.querySelector('.hm-sw-property__value')?.dataset.picker })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 2026-10-20 Firefox counterpart of hover-stability.browser.mjs: the installed Firefox over WebDriver BiDi (no
|
||||
// Playwright build), real pointer moves. Home Menu tiles and the full-footer commands: icon / text / box are the same
|
||||
// at rest, on hover and while pressed; header field values are Dotum 400 and not cut; POLYGON / WIPEOUT ellipsis.
|
||||
// at rest, on hover and while pressed; SWHeader field values are 400 and not cut; POLYGON / WIPEOUT ellipsis.
|
||||
// Usage: node packages/sw/tests/hover-stability-firefox.browser.mjs <evidence-dir> [storybook-url]
|
||||
import { mkdir, writeFile, rm } from 'node:fs/promises'
|
||||
import { spawn } from 'node:child_process'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 2026-10-20 SW hover position stability + header field text + footer command ellipsis / tooltip.
|
||||
// Hover (and Active / Selected) may change only color, background and the hover ring: the icon, the text, their gap
|
||||
// and the item box stay where they are (Header Home Menu tiles, Footer command slots in every story and category,
|
||||
// including the ▲ panel). Also: Header field values are Dotum 400 and not cut; long footer commands end in an
|
||||
// including the ▲ panel). Also: SWHeader field values are Noto Sans KR 400 and not cut; long footer commands end in an
|
||||
// ellipsis inside the unchanged 68 × 47 slot and show their full name on hover.
|
||||
// Usage: build Storybook first, then node packages/sw/tests/hover-stability.browser.mjs [evidence-dir] [chromium|firefox]
|
||||
import { createRequire } from 'node:module'
|
||||
@@ -79,7 +79,7 @@ function judge(name, c) {
|
||||
return { name, ok: Object.keys(moved).length === 0, moved, colorChanged: c.d0.color !== c.h1.color, iconSwap: c.d0.iconSrc !== c.h1.iconSrc, default: c.d0, hover: c.h1, active: c.a }
|
||||
}
|
||||
|
||||
// ---------- 1. Header field values: Dotum 400, nothing cut ----------
|
||||
// ---------- 1. Header field values: Noto Sans KR 400, nothing cut ----------
|
||||
// Full-width headers (1800 / 1920px) show every field; narrower ones move the aux fields into More.
|
||||
const fields = []
|
||||
for (const id of ['sw-header--interactive-states', 'sw-header--document-open', 'sw-header--responsive-more']) {
|
||||
@@ -92,7 +92,7 @@ for (const id of ['sw-header--interactive-states', 'sw-header--document-open', '
|
||||
}
|
||||
}
|
||||
report.headerFields = fields
|
||||
check('Header: every field value is Dotum 400 / 14px / 0.56px', fields.length >= 6 && fields.every(f => f.weight === '400' && f.size === '14px' && f.spacing === '0.56px' && /dotum/i.test(f.family)), fields)
|
||||
check('Header: every field value is Noto Sans KR 400 / 14px / 0.56px', fields.length >= 6 && fields.every(f => f.weight === '400' && f.size === '14px' && f.spacing === '0.56px' && /noto sans kr/i.test(f.family)), fields)
|
||||
for (const want of ['Standard', '0_Layer_Cludmark', '1.00', '255,229,0']) {
|
||||
const f = fields.filter(x => x.text === want)
|
||||
check(`Header: "${want}" is shown whole (text width ≤ box)`, f.length > 0 && f.every(x => !x.cut), f)
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// 2026-10-20 SW text weight request: Bold only on the named texts, with the black text outline where asked.
|
||||
// Header file name (700) · Composed Controls field values (700) vs SWHeader field values (400) · Footer 보조기능 / 명령어
|
||||
// titles and category tabs (700, 전체보기 / Model · Layout / commands unchanged) · Viewer header file name, labels and
|
||||
// Model / Layout tabs (700, More kept) · recent files name / path / preview title (700 + outline). Hover keeps the
|
||||
// text in place. Usage: build Storybook first, then node packages/sw/tests/text-weight.browser.mjs [evidence-dir]
|
||||
import { createRequire } from 'node:module'
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import http from 'node:http'
|
||||
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 out = path.resolve(process.argv[2] ?? 'docs/sw/text-weight-qa'); await mkdir(out, { recursive: true })
|
||||
const root = path.resolve('storybook-static'), mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.ttf': 'font/ttf', '.woff2': 'font/woff2' }
|
||||
const server = http.createServer(async (req, res) => { try { const p = new URL(req.url, 'http://localhost').pathname, file = path.resolve(root, '.' + decodeURIComponent(p === '/' ? '/index.html' : p)); res.setHeader('Content-Type', mime[path.extname(file)] ?? 'application/octet-stream'); res.end(await readFile(file)) } catch { res.writeHead(404).end() } })
|
||||
await new Promise(r => server.listen(0, '127.0.0.1', r)); const base = `http://127.0.0.1:${server.address().port}`
|
||||
const browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
|
||||
const checks = []; const check = (n, ok, d) => { checks.push({ n, ok, d }); console.log(ok ? 'PASS' : 'FAIL', n, ok ? '' : JSON.stringify(d).slice(0, 500)) }
|
||||
const report = {}
|
||||
let errors = []
|
||||
const page = await browser.newPage({ viewport: { width: 1920, height: 1100 } })
|
||||
page.on('pageerror', e => errors.push(e.message)); page.on('console', m => { if (m.type() === 'error') errors.push(m.text()) })
|
||||
const goto = async id => { errors = []; await page.goto(`${base}/iframe.html?id=${id}&viewMode=story`); await page.locator('#storybook-root > *').first().waitFor(); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(300) }
|
||||
const away = async () => { await page.mouse.move(2, 2); await page.waitForTimeout(90) }
|
||||
const OUTLINE_14 = 'rgb(0, 0, 0) 1.3px 0px 0px', OUTLINE_12 = 'rgb(0, 0, 0) 1px 0px 0px'
|
||||
// Every shown element of a selector: weight, size, color, outline start, text rect, box rect, cut.
|
||||
const styles = sel => page.locator(sel).evaluateAll(list => list.filter(e => e.getClientRects().length && e.textContent.trim()).map(e => {
|
||||
const c = getComputedStyle(e), g = document.createRange(); g.selectNodeContents(e); const t = g.getBoundingClientRect(), b = e.getBoundingClientRect()
|
||||
return { text: e.textContent.trim(), weight: c.fontWeight, size: c.fontSize, family: c.fontFamily.split(',')[0], color: c.color, outline: c.textShadow.split(', rgb')[0], textW: Math.round(t.width * 100) / 100, boxW: Math.round(b.width * 100) / 100, cut: t.width > b.width + 0.05 }
|
||||
}))
|
||||
const rect = loc => loc.evaluate(e => { const g = document.createRange(); g.selectNodeContents(e); const t = g.getBoundingClientRect(), b = e.getBoundingClientRect(); return [t.x, t.y, t.width, t.height, b.x, b.y, b.width, b.height].map(v => Math.round(v * 100) / 100).join(',') })
|
||||
async function stableOnHover(name, target, text) {
|
||||
await target.scrollIntoViewIfNeeded(); await away()
|
||||
const d = await rect(text), w0 = await text.evaluate(e => getComputedStyle(e).fontWeight)
|
||||
await target.hover(); await page.waitForTimeout(110)
|
||||
const h = await rect(text), w1 = await text.evaluate(e => getComputedStyle(e).fontWeight)
|
||||
await away(); const d2 = await rect(text)
|
||||
check(`${name}: hover keeps the text place, size and weight (${w0})`, d === h && d === d2 && w0 === w1, { d, h, d2, w0, w1 })
|
||||
}
|
||||
|
||||
// ---------- 1. Header ----------
|
||||
for (const id of ['sw-header--overview', 'sw-header--document-open', 'sw-header--interactive-states']) {
|
||||
await goto(id)
|
||||
const names = await styles('.hm-sw-header .hm-sw-header__document-name')
|
||||
check(`Header ${id}: file name Noto Sans KR 14px 700 + 1.3px outline`, names.length > 0 && names.every(n => n.weight === '700' && n.size === '14px' && n.family === '"Noto Sans KR"' && n.outline === OUTLINE_14), names.slice(0, 3))
|
||||
const values = await styles('.hm-sw-header .hm-sw-header__field-value')
|
||||
report[`headerValues:${id}`] = values
|
||||
check(`Header ${id}: SWHeader field values stay Regular (400)`, values.length > 0 && values.every(v => v.weight === '400'), values.filter(v => v.weight !== '400'))
|
||||
}
|
||||
await goto('sw-header--overview')
|
||||
const docButton = page.locator('.hm-sw-header__document-button').first()
|
||||
await stableOnHover('Header file name', docButton, docButton.locator('.hm-sw-header__document-name'))
|
||||
// SWHeader More (narrow): fields moved into the More menu are still SWHeader fields (400).
|
||||
await goto('sw-header--responsive-more')
|
||||
const narrow = page.locator('.hm-sw-header').filter({ visible: true }).nth(2)
|
||||
const moreTrigger = narrow.locator('.hm-sw-header__more-trigger')
|
||||
if (await moreTrigger.count()) { await moreTrigger.click(); await page.waitForTimeout(200) }
|
||||
const moreValues = await styles('.hm-sw-header__field-value')
|
||||
check('Header More: values shown with the More menu open stay 400 (none turn Bold)', moreValues.length > 0 && moreValues.every(v => v.weight === '400'), moreValues.filter(v => v.weight !== '400'))
|
||||
await page.keyboard.press('Escape')
|
||||
// Composed Controls: standalone menu fields (outside SWHeader) are Bold; labels and the font sample unchanged.
|
||||
await goto('sw-header--composed-controls')
|
||||
const composed = await styles('[data-composed-controls] .hm-sw-header__field-value')
|
||||
report.composedValues = composed
|
||||
check('Composed Controls: every field value Noto Sans KR 14px 700 + 1.3px outline', composed.length >= 7 && composed.every(v => v.weight === '700' && v.size === '14px' && v.outline === OUTLINE_14), composed)
|
||||
const composedLabels = await styles('[data-composed-controls] .hm-sw-header__field-label')
|
||||
check('Composed Controls: field labels stay 12px 400', composedLabels.every(l => l.weight === '400' && l.size === '12px'), composedLabels)
|
||||
report.composedCut = composed.filter(v => v.cut).map(v => `${v.text} (${v.textW} / ${v.boxW}px)`)
|
||||
console.log('INFO Composed Controls values wider than their (unchanged) box at 700:', report.composedCut.join(', ') || 'none')
|
||||
const fields = page.locator('[data-composed-controls] .hm-sw-header__field')
|
||||
for (let i = 0, n = await fields.count(); i < n; i++) {
|
||||
const f = fields.nth(i), v = f.locator('.hm-sw-header__field-value').first()
|
||||
await stableOnHover(`Composed field ${await f.getAttribute('data-menu')}`, f, v)
|
||||
}
|
||||
|
||||
// ---------- 2. Footer ----------
|
||||
await goto('sw-footer--full-footer')
|
||||
const work = page.locator('.sw-footer-story-workspace .hm-sw-footer')
|
||||
for (const side of ['start', 'end']) {
|
||||
const btn = work.locator(side === 'end' ? '.hm-sw-footer-side[data-side="end"]' : '.hm-sw-footer-side:not([data-side="end"])')
|
||||
const read = () => btn.evaluate(e => [e.querySelector('.hm-sw-footer-side__title'), e.querySelector('.hm-sw-footer-side__subtitle')].map(t => { const c = getComputedStyle(t); return [t.textContent, c.fontWeight, c.fontSize, c.color, c.textShadow.split(', rgb')[0]].join('|') }))
|
||||
await away(); const d = await read()
|
||||
await btn.hover(); await page.waitForTimeout(110); const h = await read(); await away()
|
||||
check(`Footer ${side === 'end' ? '명령어' : '보조기능'}: title 700 (Default + Hover), 전체보기 keeps 400 + 1px outline`, d[0].split('|')[1] === '700' && h[0].split('|')[1] === '700' && d[1].split('|')[1] === '400' && h[1].split('|')[1] === '400' && d[1].endsWith(OUTLINE_12) && h[1].split('|')[3] === 'rgb(255, 92, 0)', { d, h })
|
||||
await stableOnHover(`Footer side ${side} title`, btn, btn.locator('.hm-sw-footer-side__title'))
|
||||
await stableOnHover(`Footer side ${side} 전체보기`, btn, btn.locator('.hm-sw-footer-side__subtitle'))
|
||||
}
|
||||
const tabs = await styles('.sw-footer-story-workspace .hm-sw-footer-menu__tab')
|
||||
report.footerTabs = tabs
|
||||
check(`Footer: every category tab (${tabs.length}) 14px 700 + 1.3px outline`, tabs.length >= 10 && tabs.every(t => t.weight === '700' && t.size === '14px' && t.outline === OUTLINE_14), tabs.filter(t => t.weight !== '700'))
|
||||
const tabLoc = work.locator('.hm-sw-footer-menu__tab')
|
||||
await stableOnHover('Footer tab 수정 (Default → Hover)', tabLoc.nth(1), tabLoc.nth(1))
|
||||
// Selected: the tab's text sits at the same place inside its own box, weight unchanged.
|
||||
const inBox = loc => loc.evaluate(e => { const g = document.createRange(); g.selectNodeContents(e); const t = g.getBoundingClientRect(), b = e.getBoundingClientRect(); return [t.x - b.x, t.y - b.y, t.width, b.width, b.height, getComputedStyle(e).fontWeight].map(v => typeof v === 'number' ? Math.round(v * 100) / 100 : v).join(',') })
|
||||
const before = await inBox(tabLoc.nth(1)); await tabLoc.nth(1).click(); await away(); const after = await inBox(tabLoc.nth(1))
|
||||
check('Footer tab 수정 selected: text place / size / weight inside the tab unchanged', before === after && await tabLoc.nth(1).getAttribute('aria-selected') === 'true', { before, after })
|
||||
const pocket = await work.locator('.hm-sw-footer-menu').evaluate(m => { const t = m.querySelector('[aria-selected="true"]').getBoundingClientRect(), y = t.y + 4
|
||||
const under = x => document.elementsFromPoint(x, y).find(e => e.matches('.hm-sw-footer__vacant, .hm-sw-footer__group'))?.className
|
||||
return [under(t.x + 3), under(t.x + t.width / 2), under(t.right - 3)] })
|
||||
check('Footer: 수정 pocket surface stays continuous under the Bold tab', pocket.every(p => p === 'hm-sw-footer__group'), pocket)
|
||||
await tabLoc.nth(0).click(); await away()
|
||||
const keep = await styles('.sw-footer-story-workspace :is(.hm-sw-footer-tab__select, .hm-sw-footer-tool__label, .hm-sw-footer-aux__label)')
|
||||
check('Footer: Model / Layout, command and auxiliary labels keep 400', keep.length > 0 && keep.every(k => k.weight === '400'), keep.filter(k => k.weight !== '400'))
|
||||
const area = await work.locator('.hm-sw-footer-menu__viewport').evaluate(e => ({ w: Math.round(e.getBoundingClientRect().width * 10) / 10, h: Math.round(e.getBoundingClientRect().height), content: e.scrollWidth, steps: [...e.parentElement.querySelectorAll('.hm-sw-footer-menu__step')].filter(s => s.getClientRects().length).length }))
|
||||
report.footerCategoryArea = area
|
||||
console.log('INFO Footer category area at 1920:', JSON.stringify(area))
|
||||
// The approved category row is 24px high (measured before and after the Bold change, 2026-10-20).
|
||||
check('Footer: category row height unchanged (24px)', area.h === 24, area)
|
||||
check('Footer: ◀ ▶ only when the categories overflow their area', (area.content > area.w + 0.5) === (area.steps === 2), area)
|
||||
await page.screenshot({ path: path.join(out, 'footer.png'), clip: { x: 0, y: 940, width: 1920, height: 160 } })
|
||||
|
||||
// ---------- 3. Viewer header ----------
|
||||
await goto('sw-viewer--viewer-header')
|
||||
const vh = page.locator('.hm-sw-viewer-header').first()
|
||||
const vNames = await styles('.hm-sw-viewer-header >> nth=0 >> :is(.hm-sw-viewer-header__document-name, .hm-sw-viewer-header__label, .hm-sw-viewer-header__tab)')
|
||||
report.viewer = vNames
|
||||
const want = ['파일 열기', '샘플', '전체 보기', '레이어', '설정', '속성']
|
||||
const labels = vNames.map(v => v.text)
|
||||
check('Viewer: file name, 파일열기 · 샘플 · 전체보기 · 레이어 · 설정 · 속성, Model · Layout are 14px 700 + 1.3px outline', want.every(w => labels.includes(w)) && labels.some(t => t.startsWith('Model')) && labels.some(t => t.startsWith('Layout')) && vNames.every(v => v.weight === '700' && v.size === '14px' && v.outline === OUTLINE_14), vNames)
|
||||
const counts = await styles('.hm-sw-viewer-header >> nth=0 >> .hm-sw-viewer-header__count')
|
||||
check('Viewer: tab counts keep 11px 400', counts.length > 0 && counts.every(c => c.weight === '400' && c.size === '11px'), counts)
|
||||
for (const t of ['파일 열기', '레이어']) {
|
||||
const lab = vh.locator('.hm-sw-viewer-header__label', { hasText: t }).first()
|
||||
await stableOnHover(`Viewer ${t}`, lab.locator('xpath=ancestor::button[1]'), lab)
|
||||
}
|
||||
const tab = vh.locator('.hm-sw-viewer-header__tab', { hasText: 'Layout' }).first()
|
||||
await stableOnHover('Viewer Layout tab', tab, tab)
|
||||
// More stays: narrow header shows the More button, it opens and lists the moved items.
|
||||
await vh.evaluate(e => { e.style.width = '700px' }); await page.waitForTimeout(200)
|
||||
const more = vh.getByRole('button', { name: /추가 도구/ })
|
||||
check('Viewer: More button shown at a narrow width', await more.count() === 1 && await more.isVisible())
|
||||
await more.click(); await page.waitForTimeout(200)
|
||||
const moreItems = await page.locator('.hm-sw-viewer-menu [role="menuitem"], .hm-sw-viewer-menu [role="menuitemradio"], .hm-sw-viewer-menu [role="menuitemcheckbox"]').count()
|
||||
check('Viewer: More opens with its entries', moreItems > 0, moreItems)
|
||||
await page.keyboard.press('Escape'); await page.waitForTimeout(100)
|
||||
check('Viewer: Escape closes More', await page.locator('.hm-sw-viewer-menu').filter({ visible: true }).count() === 0)
|
||||
await vh.evaluate(e => { e.style.width = '' })
|
||||
check('Viewer: no page errors', errors.length === 0, errors)
|
||||
|
||||
// ---------- 4. Recent files + preview ----------
|
||||
await goto('sw-header--home-menu')
|
||||
const recentTile = page.locator('.hm-sw-home-menu > .hm-sw-home-menu__sections .hm-sw-home-menu__item', { hasText: '최근파일' }).filter({ visible: true }).first()
|
||||
if (await recentTile.getAttribute('data-active') !== 'true') { await recentTile.click(); await page.waitForTimeout(300) }
|
||||
const rNames = await styles('.hm-sw-recent-files__name'), rPaths = await styles('.hm-sw-recent-files__path')
|
||||
report.recent = { names: rNames.slice(0, 3), paths: rPaths.slice(0, 3) }
|
||||
check(`Recent files: names (${rNames.length}) 14px 700 + 1.3px outline`, rNames.length >= 10 && rNames.every(n => n.weight === '700' && n.size === '14px' && n.outline === OUTLINE_14), rNames.slice(0, 2))
|
||||
check(`Recent files: paths (${rPaths.length}) 12px 700 + 1px outline, ellipsis kept`, rPaths.length >= 10 && rPaths.every(p => p.weight === '700' && p.size === '12px' && p.outline === OUTLINE_12), rPaths.slice(0, 2))
|
||||
const row = page.locator('.hm-sw-recent-files__row').first()
|
||||
const rowGeo = () => row.evaluate(r => [r.getBoundingClientRect().height, ...[...r.querySelectorAll('.hm-sw-recent-files__name, .hm-sw-recent-files__path')].map(e => Math.round(e.getBoundingClientRect().width))].join(','))
|
||||
const rowBefore = await rowGeo()
|
||||
await stableOnHover('Recent file name', row, row.locator('.hm-sw-recent-files__name'))
|
||||
await row.hover(); await page.waitForTimeout(250)
|
||||
const preview = await page.locator('.hm-sw-recent-files__preview').evaluate(p => { const t = p.querySelector('.hm-sw-recent-files__preview-title'), c = getComputedStyle(t), b = p.getBoundingClientRect(), body = p.querySelector('.hm-sw-recent-files__preview-body').getBoundingClientRect()
|
||||
return { title: t.textContent, weight: c.fontWeight, size: c.fontSize, color: c.color, outline: c.textShadow.split(', rgb')[0], titleW: t.getBoundingClientRect().width, overflow: c.textOverflow, w: b.width, h: b.height, bodyH: body.height } })
|
||||
report.preview = preview
|
||||
check('Recent preview: title 14px 700 + 1.3px outline, width 294 with ellipsis, body 243px', preview.weight === '700' && preview.size === '14px' && preview.outline === OUTLINE_14 && preview.titleW === 294 && preview.overflow === 'ellipsis' && preview.bodyH === 243, preview)
|
||||
await page.screenshot({ path: path.join(out, 'recent-files.png'), clip: { x: 0, y: 0, width: 1100, height: 900 } })
|
||||
check('Recent files: rows keep 50px and their text widths', rowBefore === await rowGeo() && rowBefore.startsWith('50,'), rowBefore)
|
||||
await row.press('Enter').catch(() => {})
|
||||
check('Recent files / preview: no page errors', errors.length === 0, errors)
|
||||
|
||||
await writeFile(path.join(out, 'text-weight.json'), JSON.stringify({ checks, report }, null, 2))
|
||||
const failed = checks.filter(c => !c.ok).length
|
||||
console.log(`${checks.length - failed}/${checks.length} PASS`)
|
||||
await browser.close(); server.close()
|
||||
process.exitCode = failed ? 1 : 0
|
||||
@@ -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 <SWTheme>; areas do
|
||||
// not mix; unthemed components keep the SW default Dotum (2026-10-20). Noto Sans KR is a verification value only.
|
||||
// not mix; unthemed components keep Noto Sans KR. Dotum is a verification value only (not the SW default).
|
||||
// 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 = '"Noto Sans KR", sans-serif'
|
||||
const VERIFY = 'Dotum, 돋움, sans-serif'
|
||||
const NESTED = '"Malgun Gothic", sans-serif'
|
||||
const DEFAULT = 'Dotum, 돋움, "Malgun Gothic", "Apple SD Gothic Neo", sans-serif'
|
||||
const DEFAULT = '"Noto Sans KR", "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 = 기본 Dotum', only(await fonts(page, `${def} .hm-sw-viewer-header`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-header`)))
|
||||
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 = 기본 Dotum', only(await fonts(page, `${def} .hm-sw-viewer-menu`), DEFAULT), fmt(await fonts(page, `${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 page.waitForSelector(`${def} .hm-sw-viewer-menu[data-surface="beige"]`)
|
||||
check('기본: More = 기본 Dotum', only(await fonts(page, `${def} .hm-sw-viewer-menu`), DEFAULT), fmt(await fonts(page, `${def} .hm-sw-viewer-menu`)))
|
||||
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')
|
||||
|
||||
// Viewer, themed through <SWTheme style>.
|
||||
const viewer = scope('viewer-themed')
|
||||
check('뷰어 테마: SWViewerHeader 전체 = 검증 글꼴', only(await fonts(page, `${viewer} .hm-sw-viewer-header`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-header`)))
|
||||
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 = 검증 글꼴', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${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 page.waitForSelector(`${viewer} .hm-sw-viewer-menu[data-surface="beige"]`)
|
||||
check('뷰어 테마: More = 검증 글꼴', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
|
||||
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.
|
||||
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 서브메뉴 = 검증 글꼴', only(await fonts(page, `${viewer} .hm-sw-viewer-menu`), VERIFY), fmt(await fonts(page, `${viewer} .hm-sw-viewer-menu`)))
|
||||
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')
|
||||
const panels = await page.locator(`${viewer} .hm-sw-viewer-panel`).count()
|
||||
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`))}`)
|
||||
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() })
|
||||
|
||||
// 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 = 검증 글꼴 (창 이동 Tooltip 별도)', only(headerFonts, VERIFY), fmt(headerFonts))
|
||||
check('일반 테마: SWHeader + 열린 레이어 Dropdown = Dotum (창 이동 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 = 검증 글꼴', 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`)))
|
||||
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`)))
|
||||
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) = 검증 글꼴', (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) = 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"]`)))
|
||||
|
||||
// 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 === DEFAULT, plain)
|
||||
check('Portal: SWTheme만 감싸면 기본 글꼴(테마 미전달)', plain.startsWith('"Noto Sans KR"'), plain)
|
||||
check('Portal: 같은 theme style의 SWTheme으로 감싸면 테마 적용', themed === VERIFY, themed)
|
||||
|
||||
// Areas.
|
||||
const area = name => fonts(page, `${scope(name)} .hm-sw-viewer-hint`)
|
||||
check('영역 A(검증 글꼴)', only(await area('area-a'), VERIFY), fmt(await area('area-a')))
|
||||
check('영역 A(Dotum)', 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 바깥(검증 글꼴)', outer === VERIFY, outer)
|
||||
check('영역 C 바깥(Dotum)', 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·명령창이 기본 글꼴로 되돌아감(결함 재현)', only(more, DEFAULT) && only(command, DEFAULT), `More ${fmt(more)} · 명령창 ${fmt(command)}`)
|
||||
check('대조: 기존 구조 주입 시 More·명령창이 Noto로 되돌아감(결함 재현)', only(more, DEFAULT) && only(command, DEFAULT), `More ${fmt(more)} · 명령창 ${fmt(command)}`)
|
||||
await page.close()
|
||||
}
|
||||
const failedRegression = regression.filter(item => item.result !== 'PASS')
|
||||
|
||||
@@ -376,7 +376,8 @@ if (comparison.reference) {
|
||||
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: 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)
|
||||
// 2026-10-20 user request: header labels (파일열기 · 샘플 · 전체보기 · 레이어 · 설정 · 속성), file name and Model / Layout are Bold (700).
|
||||
check('fill / size / weight / spacing kept (labels Bold 700 since 2026-10-20)', outline.labelColor === 'rgb(255, 255, 255)' && outline.labelFont === '14px 700 normal', outline.labelColor + ' ' + outline.labelFont)
|
||||
await page.close()
|
||||
}
|
||||
{
|
||||
|
||||
@@ -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 — <SWTheme> 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.
|
||||
// The SW default font is Dotum (2026-10-20). Noto Sans KR is used here ONLY to make the inheritance visible.
|
||||
// Dotum is used here ONLY to make the inheritance visible: the SW default font is Noto Sans KR (2026-10-20 decision).
|
||||
const KICKER = "HM DESIGN SYSTEM / SW"
|
||||
const VERIFY_FONT = '"Noto Sans KR", sans-serif'
|
||||
const VERIFY_FONT = 'Dotum, "돋움", 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: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW — 테마 토큰 상속"
|
||||
description={<>SW 토큰은 가장 바깥 SW 범위(<code><SWTheme></code> 또는 단독 컴포넌트 루트)에서 한 번 선언되고, 안쪽 Dropdown·More·패널·명령창은 그 값을 상속합니다. 토큰은 가장 바깥 범위의 <code>style</code>로만 바꿉니다. SW 기본 글꼴은 Dotum이며, 아래 Noto Sans KR은 <strong>상속 확인용 검증 값</strong>입니다.</>} />
|
||||
<StorySection title="1. 기본 (테마 지정 없음)" description="단독 SWViewerHeader. 기본 글꼴 Dotum. 1000px 폭이라 일부 항목이 More(⋯)로 이동합니다.">
|
||||
description={<>SW 토큰은 가장 바깥 SW 범위(<code><SWTheme></code> 또는 단독 컴포넌트 루트)에서 한 번 선언되고, 안쪽 Dropdown·More·패널·명령창은 그 값을 상속합니다. 토큰은 가장 바깥 범위의 <code>style</code>로만 바꿉니다. 아래 Dotum은 <strong>상속 확인용 검증 값</strong>이며 SW 기본 글꼴(Noto Sans KR)은 바뀌지 않습니다(Dotum 기본 적용은 디자인 승인 대기).</>} />
|
||||
<StorySection title="1. 기본 (테마 지정 없음)" description="단독 SWViewerHeader. 기본 글꼴 Noto Sans KR. 1000px 폭이라 일부 항목이 More(⋯)로 이동합니다.">
|
||||
<ViewerArea width={1000} data-theme-case="default" />
|
||||
</StorySection>
|
||||
<StorySection title="2. 뷰어 모드 — SWTheme style로 글꼴 지정" description="<SWTheme style={{ '--hm-sw-header-font-family': … }}> 안의 Header·Dropdown·More·패널(내부 컨트롤 포함)이 같은 글꼴을 씁니다.">
|
||||
@@ -105,10 +105,10 @@ export const ThemeInheritance: Story = {
|
||||
</StorySection>
|
||||
<StorySection title="5. 여러 테마 영역" description="옆 영역끼리 값이 섞이지 않고, 안쪽 SWTheme의 style은 그 안에서만 적용됩니다.">
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 12 }}>
|
||||
<SWTheme style={VERIFY_THEME} data-theme-case="area-a"><SWViewerHint>영역 A — 검증 글꼴(Noto Sans KR)</SWViewerHint></SWTheme>
|
||||
<SWTheme data-theme-case="area-b"><SWViewerHint>영역 B — 기본(Dotum)</SWViewerHint></SWTheme>
|
||||
<SWTheme style={VERIFY_THEME} data-theme-case="area-a"><SWViewerHint>영역 A — 검증 글꼴(Dotum)</SWViewerHint></SWTheme>
|
||||
<SWTheme data-theme-case="area-b"><SWViewerHint>영역 B — 기본(Noto Sans KR)</SWViewerHint></SWTheme>
|
||||
<SWTheme style={VERIFY_THEME} data-theme-case="area-c">
|
||||
<SWViewerHint>영역 C 바깥 — Noto Sans KR</SWViewerHint>
|
||||
<SWViewerHint>영역 C 바깥 — Dotum</SWViewerHint>
|
||||
<SWTheme style={NESTED_THEME} data-theme-case="area-c-nested"><SWViewerHint>영역 C 안쪽 — Malgun Gothic</SWViewerHint></SWTheme>
|
||||
</SWTheme>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user