feat(sw): viewer layer bulb, popup toggle switch, footer layout and 1440px story inner
Deploy Storybook / deploy (push) Successful in 1m11s
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:
1 parent
258e0a4b68
commit
c1549b2b4f
22 files changed
+121
-63
No files matched your search
@@ -18,10 +18,10 @@
|
||||
min-width: 0;
|
||||
margin-inline: auto;
|
||||
}
|
||||
/* Wide screens (2026-10-22): 80% of the viewport, i.e. 1536px at 1920px, growing beyond it (vw, so a scrollbar does not
|
||||
shrink it). Below 1920px the rule above applies. */
|
||||
/* Wide screens (2026-10-22): 1440px from 1920px up, fixed beyond it (was 80vw); never wider than the page minus the
|
||||
gutters, so no horizontal scroll. Below 1920px the rule above applies. */
|
||||
@media (min-width: 1920px) {
|
||||
.sb-page > .sb-page-inner { width: 80vw; }
|
||||
.sb-page > .sb-page-inner { width: min(1440px, calc(100% - 2 * var(--sb-page-gutter))); }
|
||||
}
|
||||
|
||||
.sb-page .sb-page-header {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
hm-sw-* classes. Surfaces behind standalone parts read the public --hm-sw-footer-* tokens. */
|
||||
.sw-footer-story-screen { position: relative; display: flex; flex-direction: column; background: #060606; }
|
||||
.sw-footer-story-canvas { position: relative; background: var(--sw-story-canvas); }
|
||||
.sw-footer-story-workspace { display: flex; flex-direction: column; height: 720px; background: #060606; }
|
||||
/* 2026-10-22: the workspace fits the viewport under the page title (720px at most), so the page itself does not
|
||||
scroll where there is room; a short window keeps 480px and the page scrolls instead of clipping. */
|
||||
.sw-footer-story-workspace { display: flex; flex-direction: column; height: clamp(480px, calc(100dvh - 210px), 720px); background: #060606; }
|
||||
.sw-footer-story-canvas--fill { flex: 1 1 auto; min-height: 0; }
|
||||
.sw-footer-story-nodes { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 16px; }
|
||||
.sw-footer-story-node { margin: 0; padding: 12px; border: 1px solid #dedbd2; border-radius: 6px; background: #fff; }
|
||||
|
||||
@@ -29,23 +29,15 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
/** A program screen bottom: drawing area above the footer (the layout tabs sit on its lower edge). */
|
||||
function Screen({ width = 1920, canvas = 160, fluid = false, children }: { width?: number | string; canvas?: number; fluid?: boolean; children: ReactNode }) {
|
||||
// `fluid` (2026-10-22, Responsive More): `width` is a maximum and there is no scroller, so the screen background and
|
||||
// the footer share the available width, commands that do not fit go to ▲ More, and an open menu is never clipped.
|
||||
if (fluid) return (
|
||||
function Screen({ width = 1920, canvas = 160, children }: { width?: number | string; canvas?: number; children: ReactNode }) {
|
||||
// 2026-10-22: always fluid (no inner scroller). `width` is a maximum, so the screen background and the footer share
|
||||
// the available width, commands that do not fit go to ▲ More, and an open menu is never clipped.
|
||||
return (
|
||||
<div className="sw-footer-story-screen" style={{ width: "100%", maxWidth: width }} data-width={width}>
|
||||
<div className="sw-footer-story-canvas" style={{ height: canvas }} aria-label="도면 영역 (예시)" />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="sw-story-scroller">
|
||||
<div className="sw-footer-story-screen" style={{ width }}>
|
||||
<div className="sw-footer-story-canvas" style={{ height: canvas }} aria-label="도면 영역 (예시)" />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FigmaFooter() {
|
||||
@@ -66,8 +58,8 @@ function Workspace({ width = "100%" }: { width?: number | string }) {
|
||||
return (
|
||||
<>
|
||||
<Log lines={demo.log} />
|
||||
<div className="sw-story-scroller">
|
||||
<div className="sw-footer-story-workspace" style={{ width }}>
|
||||
{/* 2026-10-22: no inner scroller; `width` is a maximum and the header / footer fit the available width. */}
|
||||
<div className="sw-footer-story-workspace" style={{ width: "100%", maxWidth: width }}>
|
||||
<SWHeader documentTitle="Drawing1.grm" logoSrc={placeholderLogo} logoAlt="예제 SW" />
|
||||
<div className="sw-footer-story-canvas sw-footer-story-canvas--fill" aria-label="도면 영역 (예시)" />
|
||||
<SWFooter categories={REFERENCE_CATEGORIES} categoryId={demo.categoryId} onCategoryChange={demo.onCategoryChange}
|
||||
@@ -78,7 +70,6 @@ function Workspace({ width = "100%" }: { width?: number | string }) {
|
||||
auxAll={{ onClick: demo.onSide("auxAll") }} commandAll={{ onClick: () => { setCommandOpen(open => !open); demo.onSide("commandAll")() }, pressed: commandOpen }}
|
||||
layoutTabs={<SWFooterLayoutTabs layouts={demo.layouts} value={demo.space} onValueChange={demo.onSpaceChange}
|
||||
onAdd={demo.onLayoutAdd} onRemove={demo.onLayoutRemove} onRename={demo.onLayoutRename} />} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@@ -394,7 +385,7 @@ export const Responsive: Story = {
|
||||
{[1920, 1440, 1280, 950, 860, 760].map(width => (
|
||||
<div key={width} className={`sw-footer-story-width${width === 1920 ? " sw-story-responsive-wide" : ""}`}>
|
||||
<StorySection title={`${width}px`}>
|
||||
<Screen width={width} canvas={90} fluid><ReferenceFooter /></Screen>
|
||||
<Screen width={width} canvas={90}><ReferenceFooter /></Screen>
|
||||
</StorySection>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -109,17 +109,20 @@ export function LayerPanel({ demo, ...placement }: { demo: ViewerDemo } & PanelP
|
||||
const [query, setQuery] = useState("")
|
||||
const filtered = LAYERS.filter(layer => layer.name.toLowerCase().includes(query.trim().toLowerCase()))
|
||||
const set = (names: string[]) => demo.setHidden(new Set(names))
|
||||
// 2026-10-22: one switch for all layers. ON only when every layer is shown; a partial state reads OFF, so a click turns all on.
|
||||
// 2026-10-22: one bulb for all layers (was a switch). ON only when every layer is shown; a partial state reads OFF, so a click turns all on.
|
||||
const allVisible = demo.hidden.size === 0
|
||||
const setAll = (next: boolean) => { set(next ? [] : LAYERS.map(layer => layer.name)); demo.record(`layer:all=${next}`) }
|
||||
return (
|
||||
<SWViewerPanel title={`레이어 (${LAYERS.length}개)`} open={demo.panels.layers} onClose={() => demo.toggle("layers", false)} {...placement}>
|
||||
<SWViewerSection surface="plain" aria-label="레이어 목록">
|
||||
{/* 2026-10-22: search + 전체 레이어 stay fixed; only the layer list scrolls (.sw-viewer-story-layers). */}
|
||||
<SWViewerSection surface="plain" aria-label="레이어 목록" className="sw-viewer-story-layers">
|
||||
<div className="sw-viewer-story-toolbar">
|
||||
<SWViewerInput type="search" placeholder="레이어 검색" aria-label="레이어 검색" value={query} onChange={event => setQuery(event.currentTarget.value)} />
|
||||
{/* 2026-10-22: the section title (.hm-sw-viewer-section__title) names the switch row; the switch stays on that line. */}
|
||||
{/* 2026-10-22: the section title (.hm-sw-viewer-section__title) names the row; the bulb (same button as the rows)
|
||||
stays on that line and keeps the former switch semantics (role="switch", name 전체 레이어). */}
|
||||
<SWViewerSection surface="plain" title="전체 레이어" className="sw-viewer-story-all">
|
||||
<SWViewerSwitch aria-label="전체 레이어" checked={allVisible}
|
||||
onCheckedChange={next => { set(next ? [] : LAYERS.map(layer => layer.name)); demo.record(`layer:all=${next}`) }} />
|
||||
<button type="button" role="switch" aria-checked={allVisible} aria-label="전체 레이어" className="sw-viewer-story-bulb"
|
||||
title={allVisible ? "전체 숨기기" : "전체 표시하기"} onClick={() => setAll(!allVisible)}><SWLayerVisibilityIcon on={allVisible} /></button>
|
||||
</SWViewerSection>
|
||||
</div>
|
||||
{filtered.length === 0 ? <SWViewerHint>검색 결과가 없습니다 ("{query}")</SWViewerHint> : (
|
||||
|
||||
@@ -8,11 +8,23 @@
|
||||
background: var(--sw-story-canvas);
|
||||
}
|
||||
.sw-viewer-story-room { padding-bottom: 320px; }
|
||||
/* Search, then the all-layers switch row (SWViewerField + SWViewerSwitch, 2026-10-22). */
|
||||
/* Search, then the all-layers bulb row (section title + SWLayerVisibilityIcon button, 2026-10-22). */
|
||||
.sw-viewer-story-toolbar { display: flex; flex-wrap: wrap; gap: 12px 4px; }
|
||||
.sw-viewer-story-toolbar > * { flex: 1 0 100%; }
|
||||
/* 전체 레이어: the plain SWViewerSection laid out as one row — its title on the left, the switch on the right. */
|
||||
/* 전체 레이어: the plain SWViewerSection laid out as one row — its title on the left, the bulb on the right. */
|
||||
.sw-viewer-story-toolbar > .sw-viewer-story-all { flex-direction: row; align-items: center; justify-content: space-between; min-height: 20px; }
|
||||
/* Its bulb lines up with the row bulbs: the row padding (6px) plus the same scrollbar gutter the list reserves. The
|
||||
gutter needs overflow, so the focus ring is drawn inside the 24px button. */
|
||||
.sw-viewer-story-layers .sw-viewer-story-all { padding-right: 6px; overflow-y: hidden; scrollbar-gutter: stable; }
|
||||
/* 2026-10-22 layer image: one light line under the 전체 레이어 row (none between search and that row), the SW
|
||||
property table tone (#d2c8b4) rather than the darker inner border. */
|
||||
.sw-viewer-story-layers .sw-viewer-story-all { padding-bottom: 8px; border-bottom: 1px solid var(--hm-sw-property-table); }
|
||||
.sw-viewer-story-all .sw-viewer-story-bulb:focus-visible { outline-offset: -2px; }
|
||||
/* Layer panel (2026-10-22): the section fills the panel body, search + 전체 레이어 stay put and only the list scrolls.
|
||||
No divider between the fixed rows and the list; the list keeps the panel's scrollbar style. */
|
||||
.sw-viewer-story-layers { flex: 1 1 auto; min-height: 0; }
|
||||
.sw-viewer-story-layers > .sw-viewer-story-toolbar { flex-shrink: 0; }
|
||||
.sw-viewer-story-layers > .sw-viewer-story-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
|
||||
.sw-viewer-story-list { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
||||
.sw-viewer-story-list li { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 3px; }
|
||||
.sw-viewer-story-list li:hover { background: rgba(154, 146, 132, 0.25); }
|
||||
|
||||
Reference in new issue
Block a user