feat(sw): viewer layer bulb, popup toggle switch, footer layout and 1440px story inner
Deploy Storybook / deploy (push) Successful in 1m11s

- SW Viewer layer popup: search + 전체 레이어 fixed, only the list scrolls; the all-layers
  control is the SWLayerVisibilityIcon bulb (role="switch"), one light #d2c8b4 line under it.
- Example H and the layer popup AI docs follow the bulb layout (VIEWER, DESIGN_RULES,
  COMPONENT_CATALOG, COMPOSITION_PATTERNS, AI_GUIDE sync).
- Popup toggle switch (Figma toggle_btn_new 633:75188 / 633:75902): one sw.css face for the
  HM UI Switch in SWPopup and SWViewerSwitch in SWViewerPanel; SW Header switches unchanged.
- SW Footer stories: no inner scrollers, fluid screens; workspace fits the viewport.
- SW Footer category tabs keep their positions when selected (divider slots kept, pocket
  edges drawn over the neighbours' padding).
- Storybook .sb-page-inner: 1440px max from 1920px (was 80vw).
- SW Header equal-split change was cancelled; header CSS unchanged.

Verified: check:ai-docs PASS, viewer test 49 PASS (1 FAIL hover face + timeout pre-existing),
footer-design-review 21 PASS, browser checks of switch/footer/inner.
Not verified: footer-popup-viewer-refinement (needs full Storybook build), text-weight/hover
tests, Switch 37x16 AI docs (deferred).
This commit is contained in:
shkim4 committed 2026-10-22 20:20:00 +09:00
1 parent 258e0a4b68
commit c1549b2b4f
22 files changed
+121 -63

No files matched your search

@@ -36,8 +36,10 @@ export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => vo
setHiddenLayers(next)
onEvent?.(`layer:${name}=${!visible}`)
}
// All-layers switch: ON only when every layer is shown. A partial state reads OFF, so turning it on shows all.
// All-layers bulb: ON only when every layer is shown. A partial state reads OFF, so turning it on shows all.
const allVisible = hiddenLayers.size === 0
const [query, setQuery] = useState("")
const shownLayers = layers.filter(layer => layer.name.toLowerCase().includes(query.trim().toLowerCase()))
const setAllLayers = (visible: boolean) => {
setHiddenLayers(new Set(visible ? [] : layers.map(layer => layer.name)))
onEvent?.(`layer:all=${visible}`)
@@ -74,14 +76,17 @@ export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => vo
<SWViewerHeaderButton iconOnly icon={icons.info} label="정보" onClick={() => setAbout(true)} />
</>} />
<SWViewerPanel title="레이어" open={panels.layers} onClose={() => setPanels({ ...panels, layers: false })}>
<SWViewerSection surface="plain" aria-label="레이어 목록">
{/* Top: all-layers SWViewerSwitch. Rows: swatch · name · count · bulb button (SWLayerVisibilityIcon), not a switch. */}
<SWViewerPanel title={`레이어 (${layers.length}개)`} open={panels.layers} onClose={() => setPanels({ ...panels, layers: false })}>
<SWViewerSection surface="plain" aria-label="레이어 목록" className="app-viewer-layer-popup">
{/* Fixed: search, then the all-layers bulb (same button + SWLayerVisibilityIcon as the rows, role="switch").
Only the list below scrolls. Rows: swatch · name · count · bulb button, never a switch. */}
<SWViewerInput type="search" className="app-viewer-layer-search" placeholder="레이어 검색" aria-label="레이어 검색" value={query} onChange={event => setQuery(event.currentTarget.value)} />
<SWViewerSection surface="plain" title="전체 레이어" className="app-viewer-all-layers">
<SWViewerSwitch aria-label="전체 레이어" checked={allVisible} onCheckedChange={setAllLayers} />
<button type="button" role="switch" aria-checked={allVisible} aria-label="전체 레이어" className="app-viewer-layer-bulb"
title={allVisible ? "전체 숨기기" : "전체 표시하기"} onClick={() => setAllLayers(!allVisible)}><SWLayerVisibilityIcon on={allVisible} /></button>
</SWViewerSection>
<ul className="app-viewer-layers">
{layers.map(layer => {
{shownLayers.length === 0 ? <SWViewerHint>검색 결과가 없습니다 ("{query}")</SWViewerHint> : <ul className="app-viewer-layers">
{shownLayers.map(layer => {
const visible = !hiddenLayers.has(layer.name)
return (
<li key={layer.name} data-visible={visible}>
@@ -94,7 +99,7 @@ export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => vo
</li>
)
})}
</ul>
</ul>}
</SWViewerSection>
</SWViewerPanel>
+13 -3
View File
@@ -36,13 +36,23 @@
}
/* Viewer layer list (example H), the same look as Storybook SW/Viewer's layer popup. Panel tokens only.
Layer visibility is a 24 x 24 button around SWLayerVisibilityIcon (header bulb), never a switch or checkbox;
the all-layers control above the list is SWViewerSection title="전체 레이어" + SWViewerSwitch on one row. */
the all-layers control above the list is SWViewerSection title="전체 레이어" + the same bulb button on one row.
Search and that row stay fixed; only the list scrolls (the section fills the panel body). */
.app-viewer-layer-popup { flex: 1 1 auto; min-height: 0; }
.app-viewer-layer-popup > :not(.app-viewer-layers) { flex-shrink: 0; }
/* Search spans the popup (the panel input is 80px by default). */
.app-viewer-layer-popup > input.app-viewer-layer-search { width: 100%; }
.app-viewer-layer-popup > .app-viewer-layers { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
.app-viewer-layers { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
.app-viewer-layers > li { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 3px; }
.app-viewer-layers > li:hover { background: rgba(154, 146, 132, 0.25); }
.app-viewer-layers > li[data-visible="false"] { opacity: 0.45; }
/* 전체 레이어: the plain SWViewerSection laid out as one row — title (.hm-sw-viewer-section__title) left, switch right. */
section.app-viewer-all-layers { flex-direction: row; align-items: center; justify-content: space-between; min-height: 20px; }
/* 전체 레이어: the plain SWViewerSection laid out as one row — title (.hm-sw-viewer-section__title) left, bulb right.
No line between search and this row; one light line (#d2c8b4, --hm-sw-property-table) under it. The bulb lines up
with the row bulbs: row padding (6px) + the list's scrollbar gutter (needs overflow, so the focus ring sits inside). */
section.app-viewer-all-layers { flex-direction: row; align-items: center; justify-content: space-between; min-height: 20px;
padding: 0 6px 8px 0; border-bottom: 1px solid var(--hm-sw-property-table); overflow-y: hidden; scrollbar-gutter: stable; }
.app-viewer-all-layers .app-viewer-layer-bulb:focus-visible { outline-offset: -2px; }
.app-viewer-layer-swatch { flex: 0 0 10px; width: 10px; height: 10px; border: 1px solid var(--hm-sw-viewer-panel-inner-border); border-radius: 9999px; }
.app-viewer-layer-name { flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 14px; font-weight: 500; }
.app-viewer-layer-bulb { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 4px; background: none; cursor: pointer; }