feat(sw): integrate responsive home menu into header

- Merge SW / Home Menu Stories into SW / Header (sidebar: Home Menu; others verification-tagged)
- Home On: logo hover ring and header top/bottom #5F4B0F lines while the menu is open
- Measured More in SWHeader; home menu narrow widths use a More (...) section with a grouped
  text menu instead of horizontal scroll, placed right after the last visible section
- Action tiles no longer stay on after a click; 최근파일 stays Active only while its list is open
- More dropdown and recent files list never open together; list top joins the accent line
- Recent files preview falls back left / over the list at narrow widths (390px)
- Update Storybook, browser tests, docs, AI guides and QA evidence
This commit is contained in:
shkim4 committed 2026-10-19 22:46:28 +09:00
1 parent c542bdc282
commit 170a5c23ae
71 files changed
+12535 -150

No files matched your search

+417
View File
@@ -0,0 +1,417 @@
import { useCallback, useRef, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
// Public API only, like a consuming project.
import {
SWHeader, SWHeaderPropertyBar, SWHomeMenu, SWHomeMenuItem, SWHomeMenuSection, SWPopup, SWPopupRow, SWPopupSection,
SWRecentFiles, SWTheme,
type SWHeaderProperty, type SWHomeMenuAccent, type SWHomeMenuItemData, type SWHomeMenuSectionData, type SWHomeMenuSelectEvent,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import "./sw-home-menu.css"
import {
figmaHomeMenuItemIds, figmaHomeMenuSections, figmaRecentFiles, figmaTileNodes, figmaTitleNodes, withRecentFiles,
} from "./sw-home-menu-fixture"
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
import figmaHomeOn from "./assets/home-menu/figma-26-10591.png"
import figmaHomeOnOutline from "./assets/home-menu/figma-662-67267.png"
import figmaRecent from "./assets/home-menu/662-67331_recent-files.png"
import figmaPreview from "./assets/home-menu/662-67471_file-preview-2depth.png"
import figmaPropertyBar from "./assets/home-menu/figma-26-10492.png"
// Owner: docs/sw/figma-additional/analysis-report.md (Figma rj6wpsfk24lOWvWiVRAANP 26:10591 / 662:67267).
// 2026-10-19: the former SW/Home Menu Stories live under SW/Header (SW normal mode only; SW/Viewer has no logo menu).
// "Home Menu" is the sidebar Story; the other Stories keep their checks reachable by URL (verification tag).
const KICKER = "HM DESIGN SYSTEM / SW"
const meta = {
title: "SW/Header",
parameters: { layout: "fullscreen", controls: { disable: true } },
} satisfies Meta
export default meta
type Story = StoryObj<typeof meta>
const headerArgs = {
logoSrc: placeholderLogo,
logoAlt: "임시 로고",
documentTitle: "C0020101-006-평면및종단면도(06)",
drawingModeLabel: "그리기모드",
layer: { name: "0_Layer_Cludmark" },
color: { value: "255,229,0", swatch: "#ffe500" },
lineStyle: { name: "Continuous", weight: "0.25", scale: "1.00" },
textStyle: { font: "Arial", name: "Standard" },
dimensionStyle: { name: "Standard" },
}
const documents = [{ id: "doc-1", name: "C0020101-006-평면및종단면도(06)" }, { id: "doc-2", name: "C0020101-007-횡단면도" }]
function useLog() {
const [entries, setEntries] = useState<string[]>([])
return { entries, record: (entry: string) => setEntries(list => [entry, ...list].slice(0, 8)) }
}
function Log({ entries }: { entries: string[] }) {
return <ol className="sw-home-menu-story__log" aria-label="이벤트 기록">{entries.length ? entries.map((entry, index) => <li key={index}>{entry}</li>) : <li>이벤트 없음</li>}</ol>
}
const describe = (event: SWHomeMenuSelectEvent) => `onItemSelect(${event.section?.id ?? "-"} / ${event.item.id}, active=${event.active})`
/**
* SWHeader `homeMenu`: the logo toggles the menu (no onHomeClick), SWHeader places it from the measured header
* (object area − 2px to the window area; Figma x 375 → 1680 at 1920). New / Open stay in the document dropdown.
* `autoFocus={false}`: a menu open on load does not take focus (several menus share the Home Menu page).
*/
function HeaderWithMenu({ width, defaultOpen = false, defaultActiveIds = [], accent, recent = true, autoFocus, children }: {
width?: number; defaultOpen?: boolean; defaultActiveIds?: string[]; accent?: SWHomeMenuAccent; recent?: boolean; autoFocus?: boolean
children?: (log: ReactNode) => ReactNode
}) {
const [open, setOpen] = useState(defaultOpen)
const [activeIds, setActiveIds] = useState<string[]>(defaultActiveIds)
const { entries, record } = useLog()
const [sections] = useState(() => recent
? withRecentFiles(figmaHomeMenuSections, <SWRecentFiles files={figmaRecentFiles} onFileOpen={file => record(`onFileOpen(${file.id})`)} />)
: figmaHomeMenuSections)
return (
<>
<div className="sw-home-menu-story__frame" style={width ? { width } : undefined}>
<SWHeader {...headerArgs} documents={documents} activeDocumentId="doc-1"
onDocumentNew={() => record("onDocumentNew")} onDocumentOpen={() => record("onDocumentOpen")}
onHomeClick={() => record("onHomeClick (homeMenu 사용 중에는 호출되지 않음)")}
homeMenuOpen={open} onHomeMenuOpenChange={(next, reason) => { setOpen(next); record(`onHomeMenuOpenChange(${next}, ${reason})`) }}
homeMenu={{
sections, accent, activeIds, autoFocus,
onActiveChange: next => { setActiveIds(next); record(`onActiveChange([${next.join(", ")}])`) },
onItemSelect: event => record(describe(event)),
}} />
</div>
{children?.(<Log entries={entries} />)}
</>
)
}
/**
* Header width set by a range input (Storybook control only): the header measures its own width. The range ends at
* the room the page gives (no page scroll), so the browser width and the slider both drive it.
*/
function ResizableHeaderWithMenu() {
const [width, setWidth] = useState(1280)
const [room, setRoom] = useState(1920)
const observed = useRef<ResizeObserver | null>(null)
const measureRef = useCallback((element: HTMLDivElement | null) => {
observed.current?.disconnect()
if (!element) return
observed.current = new ResizeObserver(() => setRoom(Math.floor(element.clientWidth)))
observed.current.observe(element)
}, [])
const shown = Math.min(width, room)
return (
<>
<label className="sw-home-menu-story__width">
헤더 폭 <input type="range" min={Math.min(390, room)} max={room} step={1} value={shown} onChange={event => setWidth(Number(event.target.value))} aria-label="헤더 폭" />
<output>{shown}px</output>
</label>
<div ref={measureRef}><HeaderWithMenu width={shown}>{log => log}</HeaderWithMenu></div>
</>
)
}
const tileTable = (
<table className="sw-home-menu-story__table">
<thead><tr><th>섹션</th><th>제목 노드</th><th>타일 (노드)</th></tr></thead>
<tbody>{figmaHomeMenuSections.map(section => (
<tr key={section.id}><td>{section.title}</td><td>{figmaTitleNodes[section.id]}</td>
<td>{section.items.map(item => `${item.label} (${figmaTileNodes[item.id]})`).join(" · ")}</td></tr>
))}</tbody>
</table>
)
/** The sidebar Story: the logo menu of SWHeader (SW normal mode) with its Figma states, sub panel and Escape stack. */
export const HomeMenu: Story = {
name: "Home Menu",
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="Home Menu — 로고(홈) 버튼 메뉴"
description="SWHeader의 homeMenu 하나로 연결합니다(SW 일반 모드 전용, SW 뷰어 모드에는 적용하지 않음). 로고 클릭 = 열기, 다시 클릭 = 닫기(onHomeClick은 호출하지 않음), 메뉴 빈 곳 클릭 = 닫기(Figma CLOSE), 타일 클릭 = 실행(누르는 동안만 Variant3, 클릭 후 ON 없음), 최근파일만 목록이 열린 동안 Variant3. Escape는 더보기 → 하위 패널 → 메뉴 순으로 닫고 로고로 Focus를 돌립니다. ←/→/Home/End로 타일을 이동합니다. New / Open은 문서 Dropdown 안에 그대로 있고, Dropdown을 열면 메뉴가 닫힙니다. 메뉴는 Header 위에 겹치는 층이라 열고 닫아도 Header 요소의 위치·너비가 바뀌지 않습니다." />
<StorySection title="로고 클릭 · 열기 / 닫기 (현재 폭)" description="5개 섹션(파일 · 손상된 파일 · Display · 설정 · 궁금하면) 15개 타일. 마우스를 올리면 Hover(Variant2), 누르는 동안 Pressed(Variant3)이며 클릭 후 ON 상태는 남지 않습니다. 최근파일 타일만 하위 패널을 열고, 목록이 열린 동안 Variant3를 유지합니다.">
<HeaderWithMenu>{log => log}</HeaderWithMenu>
</StorySection>
<StorySection title="반응형 · 헤더 폭 조절 · 더보기(⋯)" description="메뉴 위치와 폭은 고정 x가 아니라 Header를 측정해 정합니다(객체 수정 영역 −2px ~ 창 영역). 폭이 부족한 Header에서는 기존 더보기(…) 규칙이 그대로 동작하고, 메뉴는 가로 스크롤 없이 들어가는 섹션만 앞에서부터 보여 주며 나머지는 끝의 더보기(⋯) 섹션으로 옮깁니다. 더보기를 누르면 옮긴 섹션이 텍스트 메뉴(섹션 제목 + 기능 목록, 그룹 사이 구분선)로 아래에 열리고(Hover·Active·클릭은 타일과 같음, 최근파일은 기존 목록을 엶), 폭이 넓어지면 원래 자리로 돌아갑니다. 브라우저 폭이나 슬라이더로 확인합니다(슬라이더 최대 = 현재 페이지 폭).">
<ResizableHeaderWithMenu />
</StorySection>
<StorySection title="Figma 26:10591 비교 (현재 폭)" description="위는 구현(열림, 현재 폭), 아래는 Figma 스크린샷(1920px을 현재 폭에 맞춰 축소)입니다. 1:1 비교는 'Home Menu · Figma 재현 (26:10591)' Story(1920px)에서 합니다. 열림 동안 로고는 Home On(노란 테두리), Header 상·하단에 #5F4B0F 2px 선이 표시되고 닫으면 기본으로 돌아갑니다(Figma 662:67267).">
<HeaderWithMenu defaultOpen autoFocus={false} />
<img className="sw-home-menu-story__figma-fit" src={figmaHomeOn} alt="Figma 26:10591 home on" width={1920} height={78} />
</StorySection>
<StorySection title="Default / Variant2 · Hover / Variant3 · Active" description="Figma 버튼 세트 15개(26:11355 … 26:11631)는 모두 같은 SWHomeMenuItem입니다. Variant2(Hover)는 마우스를 올려 확인하며 Figma에서 Variant3와 시각 속성이 같습니다. Variant3는 activeIds로 지정합니다.">
<SWTheme className="sw-home-menu-story__stage"><SWHomeMenu defaultOpen autoFocus={false} sections={figmaHomeMenuSections} closeOnBackgroundClick={false} aria-label="Default" /></SWTheme>
<SWTheme className="sw-home-menu-story__stage"><SWHomeMenu defaultOpen autoFocus={false} sections={figmaHomeMenuSections} activeIds={figmaHomeMenuItemIds} closeOnBackgroundClick={false} aria-label="Active" /></SWTheme>
{tileTable}
</StorySection>
<StorySection title="최근파일 하위 패널 (662:67267)" description="최근파일 타일을 누르면(Variant3) 메뉴 하단 강조가 전체 2px 외곽선으로 바뀌고 그 아래에 최근파일 목록이 열립니다. 행에 마우스를 올리면 2뎁스 미리보기가 옆에 나타납니다. 다시 누르거나 Escape로 닫습니다. 최근파일이 더보기 안에 있으면 목록은 더보기 타일 아래에 열리고 더보기 타일이 Variant3가 됩니다. 목록과 미리보기 이미지는 Story 데이터입니다. 1920px 1:1은 'Home Menu · 최근파일 하위 패널' Story입니다.">
<div className="sw-home-menu-story__tall"><HeaderWithMenu defaultOpen autoFocus={false} defaultActiveIds={["recent"]}>{log => log}</HeaderWithMenu></div>
<div className="sw-home-menu-story__figma-row">
<img src={figmaRecent} alt="Figma 662:67331 최근파일" width={362} height={758} />
<img src={figmaPreview} alt="Figma 662:67471 파일출력 2뎁스" width={315} height={276} />
</div>
</StorySection>
<StorySection title="통합 · 속성창 · SWPopup · Escape" description="Escape는 Focus가 있는 곳을 먼저 닫습니다(메뉴 안: 하위 패널 → 메뉴, Popup 안: 그 Popup). Focus가 모두의 밖이면 가장 최근에 열거나 사용한 하나만 닫습니다.">
<IntegrationDemo />
</StorySection>
</StoryPage>
),
}
export const HomeMenuFigma: Story = {
name: "Home Menu · Figma 재현 (26:10591)",
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="로고 클릭 메뉴 — Figma 26:10591 재현"
description="1920px Header 위에 SWHomeMenu를 겹친 상태입니다. 위는 구현, 아래는 Figma 스크린샷(1x)입니다. 로고를 다시 누르면 닫힙니다." />
<StorySection title="구현 (1920px)" description="SWHeader가 객체 수정 영역 시작(−2px)~창 영역 시작을 DOM에서 측정해 메뉴를 놓습니다. 열림 동안 로고는 Home On(노란 테두리), Header 상·하단에 #5F4B0F 2px 선이 표시되고 닫으면 기본으로 돌아갑니다(Figma 662:67267).">
<div className="sw-home-menu-story__scroll"><HeaderWithMenu width={1920} defaultOpen /></div>
</StorySection>
<StorySection title="Figma 26:10591">
<div className="sw-home-menu-story__scroll"><img className="sw-home-menu-story__figma" src={figmaHomeOn} alt="Figma 26:10591 home on" width={1920} height={78} /></div>
</StorySection>
</StoryPage>
),
}
/** 1:1 capture canvas for pixel comparison with the Figma 1920 x 77 frame (no story page chrome). */
function FigmaCanvas({ active, accent, recent }: { active?: boolean; accent?: SWHomeMenuAccent; recent?: boolean }) {
const sections = recent ? withRecentFiles(figmaHomeMenuSections, <SWRecentFiles files={figmaRecentFiles} />) : figmaHomeMenuSections
return (
<div className="sw-home-menu-story__frame" style={{ width: 1920, height: recent ? 840 : 80 }}>
{/* The integrated placement (measured by SWHeader), not a fixed x: it must land on Figma x 375. */}
<SWHeader {...headerArgs} defaultHomeMenuOpen homeMenu={{ sections, accent, autoFocus: false,
activeIds: active ? figmaHomeMenuItemIds : recent ? ["recent"] : undefined }} />
</div>
)
}
export const HomeMenuCaptureDefault: Story = { name: "Home Menu · 비교 캔버스 · Default", tags: ["!autodocs", "verification"], render: () => <FigmaCanvas /> }
export const HomeMenuCaptureActive: Story = { name: "Home Menu · 비교 캔버스 · Variant3", tags: ["!autodocs", "verification"], render: () => <FigmaCanvas active /> }
export const HomeMenuCaptureOutline: Story = { name: "Home Menu · 비교 캔버스 · outline", tags: ["!autodocs", "verification"], render: () => <FigmaCanvas accent="outline" /> }
export const HomeMenuCaptureRecent: Story = { name: "Home Menu · 비교 캔버스 · 최근파일", tags: ["!autodocs", "verification"], render: () => <FigmaCanvas recent /> }
export const HomeMenuHeaderComposition: Story = {
name: "Home Menu · Header 조합 · 로고 클릭",
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SWHeader homeMenu"
description="SWHeader의 homeMenu 하나로 연결합니다. 로고 클릭 = 열기/닫기(onHomeClick은 호출하지 않음), 메뉴 빈 곳 클릭 = 닫기(Figma CLOSE), 타일 클릭 = 실행(누르는 동안만 Variant3, 클릭 후 ON 없음), 최근파일만 목록이 열린 동안 Variant3. Escape는 하위 패널 → 메뉴 순으로 닫고 로고로 Focus를 돌립니다. ←/→/Home/End로 타일을 이동합니다. New / Open은 문서 Dropdown 안에 그대로 있고, Dropdown을 열면 메뉴가 닫힙니다." />
<StorySection title="현재 폭">
<HeaderWithMenu>{log => log}</HeaderWithMenu>
</StorySection>
</StoryPage>
),
}
export const HomeMenuVariants: Story = {
name: "Home Menu · 3가지 Variant",
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="Default / Variant2 / Variant3"
description="Figma 버튼 세트 15개(26:11355 … 26:11631)는 모두 같은 SWHomeMenuItem입니다. Variant2(Hover)는 마우스를 올려 확인하며, Figma에서 Variant3와 시각 속성이 같습니다. Variant3는 activeIds로 지정합니다." />
<StorySection title="Default">
<SWTheme className="sw-home-menu-story__stage"><SWHomeMenu defaultOpen sections={figmaHomeMenuSections} closeOnBackgroundClick={false} aria-label="Default" /></SWTheme>
</StorySection>
<StorySection title="Variant2 · Hover" description="마우스를 올린 타일만 Variant2입니다. 누르는 동안 Variant3이고 클릭 후에는 Default로 돌아갑니다(실행 버튼). 최근파일만 목록이 열린 동안 Variant3입니다.">
<SWTheme className="sw-home-menu-story__stage"><SWHomeMenu defaultOpen sections={figmaHomeMenuSections} closeOnBackgroundClick={false} aria-label="Hover" /></SWTheme>
</StorySection>
<StorySection title="Variant3 · Active (activeIds = 15개 전체)" description="Figma Variant3 시각 확인용으로 host가 activeIds를 직접 지정한 상태입니다(표시 전용). 클릭으로는 바뀌지 않습니다.">
<SWTheme className="sw-home-menu-story__stage"><SWHomeMenu defaultOpen sections={figmaHomeMenuSections} activeIds={figmaHomeMenuItemIds} closeOnBackgroundClick={false} aria-label="Active" /></SWTheme>
</StorySection>
<StorySection title="Figma 노드 대응" description="섹션 제목 tit_* 와 타일 세트">{tileTable}</StorySection>
</StoryPage>
),
}
function DynamicDemo() {
const [sections, setSections] = useState<SWHomeMenuSectionData[]>(figmaHomeMenuSections)
const [selection, setSelection] = useState<"multiple" | "single">("multiple")
const [accent, setAccent] = useState<SWHomeMenuAccent>("auto")
const [activeIds, setActiveIds] = useState<string[]>([])
const [count, setCount] = useState(1)
const { entries, record } = useLog()
const update = (sectionId: string, change: (items: readonly SWHomeMenuItemData[]) => SWHomeMenuItemData[]) =>
setSections(list => list.map(section => section.id === sectionId ? { ...section, items: change(section.items) } : section))
const toggleItem = (id: string, key: "hidden" | "disabled") => setSections(list => list.map(section => ({
...section, items: section.items.map(item => item.id === id ? { ...item, [key]: !item[key] } : item),
})))
const mono = <svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path fill="currentColor" d="M3 3h12v12H3z M5 5v8h8V5z" /></svg>
return (
<>
<div className="sw-home-menu-story__toolbar" role="toolbar" aria-label="데이터 변경">
<button type="button" onClick={() => { update("help", items => [...items, { id: `custom-${count}`, label: `새 메뉴 ${count}`, icon: mono }]); setCount(count + 1) }}>궁금하면에 항목 추가(currentColor 아이콘)</button>
<button type="button" onClick={() => update("help", items => items.slice(0, -1))}>궁금하면 마지막 항목 삭제</button>
<button type="button" onClick={() => toggleItem("export", "hidden")}>내보내기 숨김 전환</button>
<button type="button" onClick={() => toggleItem("recover", "disabled")}>복구하기 disabled 전환</button>
<button type="button" onClick={() => update("damaged", items => items.map(item => item.id === "temp-file" ? { ...item, label: item.label === "임시파일" ? "임시파일 자동 저장 목록" : "임시파일" } : item))}>긴 라벨 전환</button>
<button type="button" onClick={() => setSections(list => list.some(section => section.id === "extra") ? list.filter(section => section.id !== "extra")
: [...list, { id: "extra", title: "새 섹션", items: [{ id: "extra-a", label: "도구 A", icon: mono }] }])}>섹션 추가/삭제</button>
<button type="button" onClick={() => setSelection(selection === "multiple" ? "single" : "multiple")}>selection: {selection}</button>
<button type="button" onClick={() => setAccent(accent === "auto" ? "sections" : accent === "sections" ? "outline" : "auto")}>accent: {accent}</button>
<button type="button" onClick={() => setActiveIds([])}>Active 모두 해제</button>
</div>
<SWTheme className="sw-home-menu-story__stage">
<SWHomeMenu defaultOpen sections={sections} selection={selection} accent={accent} closeOnBackgroundClick={false}
activeIds={activeIds} onActiveChange={next => { setActiveIds(next); record(`onActiveChange([${next.join(", ")}])`) }}
onItemSelect={event => record(describe(event))} />
</SWTheme>
<Log entries={entries} />
</>
)
}
export const HomeMenuDynamicData: Story = {
name: "Home Menu · Props · 데이터 변경",
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="데이터 기반 항목 · 선택 상태 제어"
description="항목·섹션 추가/삭제/숨김, disabled, 긴 라벨, selection, accent를 외부 데이터로 바꿉니다. 폭·구분선·모서리는 항목 수에서 자동으로 계산됩니다." />
<StorySection title="sections + activeIds (Controlled)"><DynamicDemo /></StorySection>
</StoryPage>
),
}
function StandaloneDemo() {
const [active, setActive] = useState(false)
const { entries, record } = useLog()
const mono = <svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><circle cx="9" cy="9" r="6" fill="none" stroke="currentColor" strokeWidth="2" /></svg>
return (
<>
<SWTheme className="sw-home-menu-story__stage">
<SWHomeMenu defaultOpen aria-label="조합형 메뉴" closeOnBackgroundClick={false} onItemSelect={event => record(describe(event))}>
<SWHomeMenuSection title="조합형 섹션">
<SWHomeMenuItem itemId="a" label="도구 A" icon={mono} />
<SWHomeMenuItem itemId="b" label="도구 B" icon={mono} />
<SWHomeMenuItem itemId="c" label="실행" icon={mono} toggle={false} />
</SWHomeMenuSection>
</SWHomeMenu>
</SWTheme>
<SWTheme className="sw-home-menu-story__stage">
<div className="sw-home-menu-story__single">
<SWHomeMenuItem label="단독 타일" icon={mono} active={active} onActiveChange={next => { setActive(next); record(`onActiveChange(${next})`) }} />
</div>
</SWTheme>
<Log entries={entries} />
</>
)
}
export const HomeMenuStandalone: Story = {
name: "Home Menu · 단독 사용 · 조합형",
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SWTheme 안에서 단독 사용"
description="Header 없이 <SWTheme>만으로 렌더링합니다. children(SWHomeMenuSection / SWHomeMenuItem) 조합형과 메뉴 밖 단독 SWHomeMenuItem(active / onActiveChange)을 보여 줍니다." />
<StorySection title="children 조합 · 단독 타일"><StandaloneDemo /></StorySection>
</StoryPage>
),
}
export const HomeMenuRecentFiles: Story = {
name: "Home Menu · 최근파일 하위 패널 (662:67267)",
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="최근파일 — Figma 662:67267 + 662:67331 + 662:67471"
description="Figma 주석 '최근파일' 상태입니다. 최근파일 타일을 누르면(Variant3) 메뉴 하단 강조가 전체 2px 외곽선으로 바뀌고 그 아래에 최근파일 목록이 열립니다. 행에 마우스를 올리면 2뎁스 미리보기가 옆에 나타납니다. 다시 누르거나 Escape로 닫습니다. 목록과 미리보기 이미지는 Story 데이터입니다. K 미리보기(Variant5)는 기존 SWHeader K 가이드 Hover가 담당합니다." />
<StorySection title="구현 (1920px)">
<div className="sw-home-menu-story__scroll sw-home-menu-story__tall"><HeaderWithMenu width={1920} defaultOpen defaultActiveIds={["recent"]}>{log => log}</HeaderWithMenu></div>
</StorySection>
<StorySection title="Figma 662:67267 · 662:67331 · 662:67471">
<div className="sw-home-menu-story__scroll"><img className="sw-home-menu-story__figma" src={figmaHomeOnOutline} alt="Figma 662:67267" width={1935} height={193} /></div>
<div className="sw-home-menu-story__figma-row">
<img src={figmaRecent} alt="Figma 662:67331 최근파일" width={362} height={758} />
<img src={figmaPreview} alt="Figma 662:67471 파일출력 2뎁스" width={315} height={276} />
</div>
</StorySection>
</StoryPage>
),
}
// Property bar check (Figma 26:10492) against the current SWHeaderPropertyBar. Values are the Figma sample; the
// bar itself is unchanged (3 differences, awaiting approval: home-menu-integration report §속성Bar).
const barProperties: SWHeaderProperty[] = [
{ id: "radius", name: "반지름", value: "4.0526", type: "number", groupId: "bar" },
{ id: "diameter", name: "지름", value: "8.1052", type: "number", groupId: "bar" },
{ id: "circumference", name: "원주", value: "8.1052", type: "number", groupId: "bar" },
{ id: "area", name: "면적", value: "51.5962", type: "number", groupId: "bar" },
]
function PropertyBarCompare() {
const [values, setValues] = useState(barProperties)
return (
<SWTheme className="sw-home-menu-story__stage" style={{ display: "block" }}>
<SWHeaderPropertyBar selection={{ id: "circle", title: "원", kind: "single" }} properties={values}
onPropertyChange={(property, value) => setValues(list => list.map(item => item.id === property.id ? { ...item, value } : item))} />
</SWTheme>
)
}
export const HomeMenuPropertyBarCheck: Story = {
name: "Home Menu · 속성Bar 비교 (26:10492)",
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="속성Bar — Figma 26:10492 비교"
description="현재 SWHeaderPropertyBar(수정 없음)에 Figma 예시 값을 넣었습니다. 입력에 마우스를 올리면 hover, 클릭하면 on(편집) 상태입니다." />
<StorySection title="현재 구현"><PropertyBarCompare /></StorySection>
<StorySection title="Figma 26:10492 (1222 x 28)">
<div className="sw-home-menu-story__scroll"><img className="sw-home-menu-story__figma" src={figmaPropertyBar} alt="Figma 26:10492" width={1222} height={28} /></div>
</StorySection>
</StoryPage>
),
}
export const HomeMenuCapturePropertyBar: Story = {
name: "Home Menu · 비교 캔버스 · 속성Bar", tags: ["!autodocs", "verification"],
render: () => <SWTheme style={{ background: "#060606", width: 1222, padding: 0 }}><SWHeaderPropertyBar selection={{ id: "circle", title: "원", kind: "single" }} properties={barProperties} onPropertyChange={() => {}} /></SWTheme>,
}
/** Integration: logo menu + property window + SWPopup at the same time (Escape stack and focus). */
function IntegrationDemo() {
const [open, setOpen] = useState(false)
const [panelOpen, setPanelOpen] = useState(false)
const [popupOpen, setPopupOpen] = useState(false)
const { entries, record } = useLog()
const [sections] = useState(() => withRecentFiles(figmaHomeMenuSections, <SWRecentFiles files={figmaRecentFiles} onFileOpen={file => record(`onFileOpen(${file.id})`)} />))
return (
<>
<div className="sw-home-menu-story__toolbar" role="toolbar" aria-label="열기">
<button type="button" data-testid="open-popup" onClick={() => setPopupOpen(true)}>SWPopup 열기</button>
</div>
<div className="sw-home-menu-story__frame sw-home-menu-story__tall">
<SWHeader {...headerArgs} documents={documents} activeDocumentId="doc-1"
onDocumentNew={() => record("onDocumentNew")} onDocumentOpen={() => record("onDocumentOpen")}
homeMenuOpen={open} onHomeMenuOpenChange={(next, reason) => { setOpen(next); record(`onHomeMenuOpenChange(${next}, ${reason})`) }}
homeMenu={{ sections, onItemSelect: event => record(describe(event)) }}
propertyPanelOpen={panelOpen} onPropertyPanelOpenChange={next => { setPanelOpen(next); record(`onPropertyPanelOpenChange(${next})`) }}
propertyPanel={{ selection: { id: "none", title: "선택된 객체 없음", kind: "none" } }} />
{popupOpen && (
<SWTheme style={{ position: "absolute", top: 120, right: 24 }}>
<SWPopup title="사용자 Popup" onClose={() => { setPopupOpen(false); record("SWPopup onClose") }}>
<SWPopupSection title="내용"><SWPopupRow label="항목">값</SWPopupRow></SWPopupSection>
</SWPopup>
</SWTheme>
)}
</div>
<Log entries={entries} />
</>
)
}
export const HomeMenuIntegration: Story = {
name: "Home Menu · 통합 · Popup · Escape",
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="로고 메뉴 + 속성창 + SWPopup"
description="Escape는 Focus가 있는 곳을 먼저 닫습니다(메뉴 안: 하위 패널 → 메뉴, Popup 안: 그 Popup). Focus가 모두의 밖이면 가장 최근에 열거나 사용한 하나만 닫습니다." />
<StorySection title="현재 폭"><IntegrationDemo /></StorySection>
</StoryPage>
),
}
/** Responsive check: the header at the viewport width with the menu open (no story page chrome). */
export const HomeMenuCaptureResponsive: Story = { name: "Home Menu · 비교 캔버스 · 반응형", tags: ["!autodocs", "verification"], render: () => <HeaderWithMenu defaultOpen /> }