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

@@ -45,12 +45,24 @@ const StatusIcon = ({ state }: { state: SWViewerStatusState }) => state === "idl
</svg>
)
/** Load state of the viewer header (reference status: muted 11px text, icon; text hides below 560px). */
/**
* Load state of the viewer header (reference status: muted 11px text, icon; text hides below 560px). The label area is
* as wide as the widest text it can show (2026-10-23): every state label (and a custom `label`) sits in the same grid
* cell, the others hidden, so a state change never resizes it or moves the menus; with too little room it ellipsizes.
* The icon slot is kept (empty) for `idle` for the same reason.
*/
export function SWViewerStatus({ state, label, detail, className, ...rest }: SWViewerStatusProps) {
const text = label ?? statusLabel[state]
const sizers = Array.from(new Set([text, ...Object.values(statusLabel)])).filter(candidate => candidate !== text)
return (
<div role="status" className={cx("hm-sw-viewer-status", className)} data-state={state} title={detail} {...rest}>
{state !== "idle" && <span className="hm-sw-viewer-status__icon"><SWViewerIcon><StatusIcon state={state} /></SWViewerIcon></span>}
<span className="hm-sw-viewer-status__label">{label ?? statusLabel[state]}</span>
<span className="hm-sw-viewer-status__icon" data-empty={state === "idle" ? "" : undefined} aria-hidden={state === "idle" ? true : undefined}>
{state !== "idle" && <SWViewerIcon><StatusIcon state={state} /></SWViewerIcon>}
</span>
<span className="hm-sw-viewer-status__label">
<span className="hm-sw-viewer-status__text">{text}</span>
{sizers.map(candidate => <span key={candidate} className="hm-sw-viewer-status__sizer" aria-hidden="true">{candidate}</span>)}
</span>
{detail && <span className="hm-sw-viewer-sr-only">: {detail}</span>}
</div>
)
@@ -212,7 +212,8 @@ function Tabs({ item, onCommand }: { item: Extract<SWViewerHeaderItem, { type: "
<button key={option.value} type="button" role="radio" aria-checked={selected} tabIndex={option.value === current ? 0 : -1}
className="hm-sw-viewer-header__tab" data-state={selected ? "active" : undefined} title={option.title} disabled={item.disabled || option.disabled}
onClick={() => { if (!selected) select(option.value) }}>
{option.label}
{/* The name shrinks first (ellipsis, sw-viewer.css); the full name stays the accessible name. */}
<span className="hm-sw-viewer-header__tab-name">{option.label}</span>
{option.count !== undefined && option.count > 0 && <span className="hm-sw-viewer-header__count">{option.count.toLocaleString()}</span>}
</button>
)