feat(sw): bold 9-slice footer tabs and viewer panel bottom edge

User-approved 2026-10-22 SW changes (CSS only, public API unchanged).
- Footer Model / Layout tab labels 700 in every state, letter-spacing -5%;
  the name-edit input keeps 400.
- Slanted Layout tab and Model tab draw their SVG assets as 9-slice
  border-images: 97px / 73px stay the minimum, the slant and corners keep
  their size and only the flat middle grows with a longer label (max 12em,
  then ellipsis, like the rounded tabs). Hover surfaces use a union mask of
  the same asset (no mask-border), so Chromium and Firefox match. The
  default width renders pixel-identical to the previous drawing; the 8px
  slant / Model joint is unchanged.
- Viewer panel body: the 7px bottom padding (inside the scrollport) becomes
  a 2px transparent bottom border, so scrolled rows stop 2px above the
  frame border instead of running into it. SWPopup keeps its 7px.
- AI docs: DESIGN_RULES, AI_USAGE, FOOTER, VIEWER updated.
- Tests: text-weight and footer-design-review expect the 700 labels and read
  the Model surface from the 9-slice ::before.
This commit is contained in:
shkim4 committed 2026-10-22 15:32:07 +09:00
1 parent 449398e3de
commit fc65b2e836
8 files changed
+88 -36

No files matched your search

@@ -78,7 +78,8 @@ 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()) })
// 2026-10-22: the label (Bold 700) is the text; the 9-slice tab surface is the ::before border-image.
const tabInfo = async loc => ({ tab: await style(loc), before: await style(loc, '::before'), surface: await loc.evaluate(e => getComputedStyle(e, '::before').borderImageSource), text: await style(loc.locator('.hm-sw-footer-tab__label').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 }
@@ -94,8 +95,8 @@ await model.locator('.hm-sw-footer-tab__select').click(); await away(); await pa
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}`))
check('Model not selected: its own tab image surface (tab-model.svg 9-slice on ::before, beige), no orange ::before', mu.surface.startsWith('url(') && !mu.before.bg.includes('rgb(218, 104, 19)'), { surface: mu.surface.slice(0, 40), before: mu.before.bg })
check('Model text #161410 12px 700 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 === '700' && 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++) {
@@ -105,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 (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}`)
check(`Layout ${i} default/hover text: #161410 12px 700, same box (Noto Sans KR)`, [d, h].every(s => s.text.color === 'rgb(22, 20, 16)' && s.text.size === '12px' && s.text.weight === '700' && 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()
+6 -3
View File
@@ -1,6 +1,6 @@
// 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
// titles and category tabs (700, 전체보기 / commands unchanged; Model · Layout tabs 700 since 2026-10-22) · Viewer header file name, labels and
// Model / Layout tabs (700, More kept; 2026-10-21 logo menu and sample list follow the SWHeader `...` menu / file-name
// list rules) · 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]
@@ -99,8 +99,11 @@ const pocket = await work.locator('.hm-sw-footer-menu').evaluate(m => { const t
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'))
// 2026-10-22 user request: the Model / Layout tab labels are 700; command and auxiliary labels keep 400.
const layoutTabs = await styles('.sw-footer-story-workspace .hm-sw-footer-tab__label')
check(`Footer: Model / Layout tab labels (${layoutTabs.length}) 12px 700`, layoutTabs.length >= 3 && layoutTabs.every(k => k.weight === '700' && k.size === '12px'), layoutTabs.filter(k => k.weight !== '700' || k.size !== '12px'))
const keep = await styles('.sw-footer-story-workspace :is(.hm-sw-footer-tool__label, .hm-sw-footer-aux__label)')
check('Footer: 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))