feat(sw): refine headers, status label, popups and AI guide rules
Deploy Storybook / deploy (push) Successful in 1m10s

- SW Header: the five field previews hide and return together in one
  measured step; from 760 to 1920px the groups from 레이어 to 치수스타일
  share the room left before 속성보기, above 1920px they keep the 1920
  widths (controls bars and < 760px unchanged)
- SW Viewer Header Model / Layout: long names ellipsize in equal buttons
  (minmax(0, 1fr), name counted at --hm-sw-viewer-tab-name-basis) so they
  never change the box or the measured steps; green outer outline and its
  tokens removed; 378px / 42px, text-shadow and state unchanged
- SWViewerStatus: the label keeps the width of its widest real state text
  (idle keeps its icon slot), so a state change never moves the header;
  with too little room it ellipsizes after the file name gives way
- Popups: body inset shadow removed, title SVG drop shadow removed, viewer
  panel title icons use --hm-sw-viewer-panel-icon-color (#c7bda9)
- Storybook: SW/Viewer › Model / Layout story removed
- AI guide (docs/ai + package copies): design-only changes for every
  component, content limits for Submodule consumers (Header excluded),
  Submodule usage rules; VIEWER / DESIGN_RULES synced
- viewer.browser.mjs: explicit 1920 / 1440 labels and approved 1280
  icon-only expectations
This commit is contained in:
shkim4 committed 2026-10-23 18:18:17 +09:00
1 parent 325ec25e37
commit db03e155dd
19 files changed
+190 -119

No files matched your search

+13 -4
View File
@@ -326,6 +326,9 @@ if (comparison.reference) {
tabsEqual: tabWidths.every(width => Math.abs(width - tabWidths[0]) <= 0.5),
tabsFit,
labels: [...element.querySelectorAll('.hm-sw-viewer-header__button, .hm-sw-viewer-header__tab, .hm-sw-viewer-header__switch-label')].map(item => item.textContent.trim() || item.getAttribute('aria-label')),
// Buttons shown as icons only by the measured steps (name = aria-label / title) and labels drawn as text.
iconOnly: [...element.querySelectorAll('.hm-sw-viewer-header__button[data-icon-only=""]')].map(item => item.getAttribute('aria-label') || item.getAttribute('title') || item.textContent.trim()).sort(),
shownLabels: [...element.querySelectorAll('.hm-sw-viewer-header__label')].filter(item => rect(item).width > 0).map(item => item.textContent.trim()),
}
})
report.push({ rowWidth: width, ...info })
@@ -335,10 +338,16 @@ if (comparison.reference) {
const at = width => report.find(item => item.rowWidth === width)
check('rows render at their own width', report.every(item => item.width === item.rowWidth), JSON.stringify(report.map(item => [item.rowWidth, item.width])))
check('no horizontal overflow, overlap or part outside the header at any row', report.every(item => item.overflow <= 0 && item.objectOverflow <= 1 && !item.overlaps && !item.outside), JSON.stringify(report.map(item => [item.rowWidth, item.overflow, item.objectOverflow, item.overlaps, item.outside])))
// 2026-10-23: the Model / Layout field widens with the free room up to 378px (1920: 378), narrower with less room;
// the buttons always share one width and keep name + count apart. With this Story's content 1920 / 1440 / 1280 show
// every item (no More); narrower rows move collapseBelow items (Model / Layout included) into More by the measured steps.
check('Model / Layout field: 378px at 1920, at most 378px and narrower with less room', at(1920).field === 378 && report.every(item => item.field === null || item.field <= 378) && at(1440).field >= at(1280).field, JSON.stringify(report.map(item => [item.rowWidth, item.field])))
// 2026-10-23: the Model / Layout field widens with the free room up to 378px (1920: 378), narrower with less room at
// the same step (1920 → 1440, both with every label); the buttons always share one width and keep name + count apart.
// With this Story's content 1920 / 1440 / 1280 show every item (no More); narrower rows move collapseBelow items
// (Model / Layout included) into More by the measured steps.
check('Model / Layout field: 378px at 1920, at most 378px and narrower with less room (1920 → 1440)', at(1920).field === 378 && report.every(item => item.field === null || item.field <= 378) && at(1440).field <= at(1920).field, JSON.stringify(report.map(item => [item.rowWidth, item.field])))
// 1920 / 1440 (unchanged): every button with its label, none icon only.
check('1920 / 1440: 전체 보기 · 레이어 · 설정 · 속성 labelled, no icon-only button', [1920, 1440].every(width => !at(width).iconOnly.length && ['전체 보기', '레이어', '설정', '속성'].every(name => at(width).shownLabels.includes(name))), JSON.stringify([1920, 1440].map(width => [width, at(width).shownLabels, at(width).iconOnly])))
// 1280 (2026-10-23, user-approved): the status label keeps the width of its widest text (SWViewerStatus), so at 1280
// the measured icon step turns exactly the three panel toggles into icons; 전체 보기 keeps its label, nothing is in More.
check('1280: exactly 레이어 · 설정 · 속성 icon only, 전체 보기 labelled, no More', JSON.stringify(at(1280).iconOnly) === JSON.stringify(['레이어', '설정', '속성'].sort()) && at(1280).shownLabels.includes('전체 보기') && !['레이어', '설정', '속성'].some(name => at(1280).shownLabels.includes(name)) && !at(1280).more, JSON.stringify([at(1280).shownLabels, at(1280).iconOnly, at(1280).more]))
check('Model / Layout buttons equal, name and count never overlap', report.every(item => item.tabsEqual && item.tabsFit))
check('1920 / 1440 / 1280: every item in the header, no More', [1920, 1440, 1280].every(width => !at(width).more && at(width).field && ['전체 보기', '2.5D', '어두운 배경', '레이어'].every(name => at(width).labels.includes(name))))
// 950 sits at the edge of the measured steps (icons only or More, depending on the font rendering); both fit.