Files
HMDesign/src/sw/sw-footer.stories.tsx
T
shkim4 23d522c136 feat(sw): viewer header logo menu and file list; simplify SW Storybook
SW Viewer Header (approved optional props, public export names unchanged at 170):
- SWViewerHeader: logoMenu, documents, activeDocumentId, onDocumentSelect,
  onDocumentOpen, documentOpenLabel; measured responsive toggles (full ->
  icons -> More) with the SWHeader More / file-list styling.
- SW AI guides, ExampleH, viewer/text-weight/theme-inheritance tests and the
  logo-document-menu tests with their QA record.

SW Storybook cleanup (Story IDs and test fixtures kept):
- Sidebar: Overview -> "SW Header" (first), 전체 Footer -> "SW Footer",
  Footer/Viewer responsive pages -> "Responsive More", Viewer content-variants
  -> "Settings Popup" (merges the settings popup page).
- Hidden by the verification tag (direct URL kept): Header Dropdown,
  Document Open, Interactive States, Document Dropdown, Hidden Sub Controls,
  Home Menu standalone; Footer tool groups / custom content / parts;
  SW/Theme; Viewer menu groups / item states / variable size / settings
  panel / Header + Dropdown.
- Removed demo UI: Overview size and callback output (demo data and
  selection only with &verification), Home Menu Figma node table and width
  demo, Header Responsive More reduced to 1440 / 768 / 390, Popup reopen
  button, property-panel comparison captions, Viewer context-menu demo
  (&verification only).
- Tests: storybook-inventory records the hidden and renamed Stories;
  overview / range-cross-browser / viewer open the demo controls with
  &verification; integrated-popup reloads the Story instead of the reopen
  button; dropdown-consolidation asserts Dropdown is hidden from the sidebar
  and still opens by its direct URL.
2026-10-21 14:52:46 +09:00

425 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import {
SWFooter, SWFooterAddSlot, SWFooterAuxButton, SWFooterAuxGroup, SWFooterCommandLine, SWFooterGuide, SWFooterLayoutTabs, SWFooterMenu,
SWFooterToolButton, SWHeader, SWTheme, SW_FOOTER_MODEL, swFooterToolIcons,
type SWFooterAuxItem, type SWFooterCategory, type SWFooterLayout, type SWFooterTool,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
import { Crosshair, Grid3x3, Magnet, Move, RotateCw, Scissors, Spline, Square } from "lucide-react"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import {
AUX_ITEMS, CATEGORIES, DRAW_TOOLS, GuideFrame, GuideMovie, HISTORY, LAYOUTS, Log, Pin, REFERENCE_AUX, REFERENCE_CATEGORIES, REFERENCE_HISTORY,
useFooterDemo,
} from "./sw-footer-fixture"
import "./sw-stories.css"
import "./sw-footer-stories.css"
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
// SW/Footer (2026-10-20): Figma 이지빔-SW 26:10097 bottom bar and its 34 component sets as @hm-design/sw components.
// Stories use the public API only, like a consuming program. No CAD engine: commands are logged.
const KICKER = "HM DESIGN SYSTEM / SW"
const ref = (id: string) => new URL(`./assets/figma-reference/footer/${id}.png`, import.meta.url).href
const meta = {
title: "SW/Footer",
parameters: { layout: "fullscreen", controls: { disable: true } },
} satisfies Meta
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, children }: { width?: number | string; canvas?: number; children: ReactNode }) {
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() {
return (
<SWFooter categories={CATEGORIES} auxItems={AUX_ITEMS} onCategoryAdd={() => {}}
layoutTabs={<SWFooterLayoutTabs layouts={[{ id: "layout1", label: "Layout 01" }]} value={SW_FOOTER_MODEL} modelLabel={false} />} />
)
}
// ---------- 1. 전체 Footer · 실제 Footer 조합 ----------
const LAYOUTS_2: SWFooterLayout[] = [{ id: "layout2", label: "Layout 02" }, { id: "layout1", label: "Layout 01" }]
/** The reference screen (2026-10-20): SWHeader, drawing area, footer with the command line inside the bar. */
function Workspace({ width = "100%" }: { width?: number | string }) {
const demo = useFooterDemo({ aux: REFERENCE_AUX, layouts: LAYOUTS_2, history: REFERENCE_HISTORY })
const [commandOpen, setCommandOpen] = useState(false)
return (
<>
<Log lines={demo.log} />
<div className="sw-story-scroller">
<div className="sw-footer-story-workspace" style={{ 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}
status="7.4177, -0.2807, 0.0000"
command={<SWFooterCommandLine history={demo.history} onSubmit={demo.onCommand} expanded={commandOpen} onExpandedChange={setCommandOpen} />}
activeToolId={demo.activeToolId} onToolSelect={demo.onToolSelect} onSlotAdd={demo.onSlotAdd} onCategoryAdd={demo.onCategoryAdd}
slotGuide={{ media: <GuideMovie /> }}
auxItems={demo.aux} onAuxPressedChange={demo.onAuxPressedChange}
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>
</>
)
}
export const FullFooter: Story = {
name: "SW Footer",
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Footer"description="사용자 제공 실제 화면(2026-10-20) 구성: SWHeader + 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." />
<StorySection title="실제 Footer 조합 (동작)" description="명령 클릭 = 선택(다시 클릭 해제)·명령 기록 추가, 분류 탭(←/→, 바꿔도 칸 영역 고정), 빈 칸 + 클릭·Hover 가이드(영상 대신 Figma 6프레임 순환), 보조기능 토글(켜짐 = 주황 테두리·글자), 명령어 전체보기·▲ = 명령창 위로 펼치기, Layout 탭 선택·+·×·더블클릭/F2 이름 변경·Delete, 명령창 Enter 실행. 좁은 폭의 ▲ 확장 목록은 「화면 폭 변화」, Hover · Active · Selected · Disabled는 이 화면의 각 부품에서 동작합니다.">
<Workspace width={1920} />
</StorySection>
</StoryPage>
),
}
// ---------- 2. 도구 그룹별 표시 ----------
export const ToolGroups: Story = {
name: "도구 그룹별 표시",
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="도구 그룹별 표시" description="분류(categoryId)마다 다른 명령. 명령 뒤의 칸은 빈 칸(+)이고, 마지막 명령 뒤부터 bg_vacant(어두운 면)입니다. 1440px." />
{CATEGORIES.slice(0, 3).map(category => (
<StorySection key={category.id} title={`${category.label} — 명령 ${category.tools.length}개`}>
<Screen width={1440} canvas={40}><SWFooter categories={CATEGORIES} categoryId={category.id} auxItems={AUX_ITEMS} onSlotAdd={() => {}} /></Screen>
</StorySection>
))}
<StorySection title="보조기능 없음 · 측면 버튼 없음" description="auxItems 생략, auxAll={false}, commandAll={false}: 명령 영역만.">
<Screen width={1440} canvas={40}><SWFooter categories={CATEGORIES} auxAll={false} commandAll={false} /></Screen>
</StorySection>
</StoryPage>
),
}
// ---------- 3. Figma 디자인별 Variant (35 노드) ----------
function Node({ id, name, impl, states, children }: { id: string; name: string; impl: string; states: string; children: ReactNode }) {
return (
<figure className="sw-footer-story-node" data-node-id={id}>
<figcaption>
<strong>{id}</strong> <span>{name}</span>
<code>{impl}</code>
<small>{states}</small>
</figcaption>
<div className="sw-footer-story-node__pair">
<div><p className="sw-footer-story-node__label">Figma</p><img src={ref(id.replace(":", "-"))} alt={`Figma ${id} ${name}`} /></div>
<div><p className="sw-footer-story-node__label">구현</p><SWTheme className="sw-footer-story-node__live">{children}</SWTheme></div>
</div>
</figure>
)
}
const one = (layout: SWFooterLayout): SWFooterLayout[] => [layout]
const noop = () => {}
function TabSpecimens({ layout, editing = true }: { layout: SWFooterLayout; editing?: boolean }) {
const tabs = (value: string, edit?: boolean) => (
<SWFooterLayoutTabs layouts={one(layout)} value={value} modelLabel={false} onRemove={noop} onRename={noop} editingId={edit ? layout.id : null} />
)
return (
<div className="sw-footer-story-column">
{tabs("x")}
<Pin hover=".hm-sw-footer-tab">{tabs("x")}</Pin>
{tabs(layout.id)}
<Pin hover=".hm-sw-footer-tab">{tabs(layout.id)}</Pin>
{editing && tabs("x", true)}
{editing && <Pin hover=".hm-sw-footer-tab">{tabs("x", true)}</Pin>}
</div>
)
}
function ToolSpecimen({ tool }: { tool: SWFooterTool }) {
return (
<div className="sw-footer-story-column sw-footer-story-group-surface">
<SWFooterToolButton {...toolProps(tool)} />
<Pin hover=".hm-sw-footer-tool"><SWFooterToolButton {...toolProps(tool)} /></Pin>
<SWFooterToolButton {...toolProps(tool)} pressed />
</div>
)
}
function MenuSpecimen({ label }: { label: string }) {
const items = [{ id: "a", label }]
return (
<div className="sw-footer-story-column sw-footer-story-group-surface">
<SWFooterMenu items={items} value="" />
<Pin hover=".hm-sw-footer-menu__tab"><SWFooterMenu items={items} value="" /></Pin>
<SWFooterMenu items={items} value="a" />
</div>
)
}
const NCOPY = CATEGORIES[1].tools[0] as SWFooterTool
const TOOL_NODES: [string, SWFooterTool][] = [
["26:9529", DRAW_TOOLS[0]], ["26:9551", DRAW_TOOLS[1]], ["26:9576", DRAW_TOOLS[2]], ["26:9601", DRAW_TOOLS[3]], ["26:9629", DRAW_TOOLS[4]],
["26:9651", DRAW_TOOLS[5]], ["26:9673", DRAW_TOOLS[6]], ["26:9689", DRAW_TOOLS[7]], ["26:9702", DRAW_TOOLS[8]], ["26:9739", NCOPY],
]
const MENU_NODES: [string, string][] = [
["26:9459", "도형"], ["26:9466", "수정"], ["26:9473", "편집"], ["26:9480", "문자"], ["26:9487", "블록"],
["26:9494", "레이어"], ["26:9501", "뷰"], ["26:9508", "선택/조회"], ["26:9515", "도로/횡단"], ["26:9522", "구조/배근"],
]
const toolProps = (tool: SWFooterTool) => ({ label: tool.label, icon: tool.icon, activeIcon: tool.activeIcon, "aria-label": tool.ariaLabel })
/** Figma component sets of the six auxiliary toggles, in AUX_ITEMS order. */
const AUX_SETS = ["26:9248", "26:9267", "26:9298", "26:9320", "26:9381", "26:9412"]
const SMALL_COMMAND = { width: 120, ["--hm-sw-footer-command-expanded-height" as string]: "71px" }
export const FigmaVariants: Story = {
name: "Figma 디자인별 Variant (35 노드)",
// 2026-10-20 sidebar simplification: Figma node verification; ID and direct URL kept.
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="Figma 디자인별 Variant" description={<>요청된 35개 노드 전체. 왼쪽 = Figma 렌더(get_screenshot), 오른쪽 = 같은 순서의 구현 상태(Hover는 Story 전용 <code>data-sb-hover</code>로 고정). 노드 → 컴포넌트·Props 매핑은 <code>docs/sw/sw-footer-figma-mapping-2026-10-20.md</code>.</>} />
<StorySection title="Footer 전체" description="26:10097 레이아웃 분리 (1920 × 1080 중 하단 Footer)">
<Node id="26:10097" name="레이아웃 분리" impl="<SWFooter />" states="하단 72px + Layout 01 탭">
<div className="sw-footer-story-node__wide"><FigmaFooter /></div>
</Node>
</StorySection>
<StorySection title="명령 (도구 버튼)" description="Default / Variant2(Hover) / Variant3(Selected). Figma에서 Variant2와 Variant3는 같은 모양입니다.">
<div className="sw-footer-story-nodes">
{TOOL_NODES.map(([id, tool]) => (
<Node key={id} id={id} name={tool.id === "ncopy" ? "Ncopy" : tool.id} impl={`SWFooterToolButton (SWFooterTool "${tool.label}")`} states="Default · Hover · pressed"><ToolSpecimen tool={tool} /></Node>
))}
</div>
</StorySection>
<StorySection title="분류 탭" description="Default(#D4CBBD) / Variant2(Hover) / Variant3(현재 분류, 열린 Pocket).">
<div className="sw-footer-story-nodes">
{MENU_NODES.map(([id, label]) => (
<Node key={id} id={id} name={`text_${label.replace("/", "")}`} impl={`SWFooterMenu (item "${label}")`} states="Default · Hover · 선택"><MenuSpecimen label={label} /></Node>
))}
</div>
</StorySection>
<StorySection title="빈 칸 · 가이드">
<div className="sw-footer-story-nodes">
<Node id="26:9971" name="btn_add_menu" impl="SWFooterAddSlot" states="Default · Hover">
<div className="sw-footer-story-column sw-footer-story-group-surface">
<SWFooterAddSlot guide={false} />
<Pin hover=".hm-sw-footer-add"><SWFooterAddSlot guide={false} /></Pin>
</div>
</Node>
<Node id="26:9242" name="가이드 영상 보기" impl="SWFooterAddSlot guide (SWFooter slotGuide)" states="Default · Hover(가이드 표시)">
<div className="sw-footer-story-row sw-footer-story-group-surface sw-footer-story-guide-room">
<SWFooterAddSlot guide={{ media: <GuideFrame index={0} /> }} />
<SWFooterAddSlot guide={{ media: <GuideFrame index={0} /> }} guideOpen />
</div>
</Node>
<Node id="26:9096" name="guide_바로가기 등록" impl="SWFooterGuide media" states="Variant5 · 6 · 4 · 9 · 8 · 9 (가이드 영상 프레임)">
<div className="sw-footer-story-column">
{[0, 1, 2, 3, 4, 5].map(index => <SWFooterGuide key={index} data-standalone="" media={<GuideFrame index={index} />} />)}
</div>
</Node>
</div>
</StorySection>
<StorySection title="Layout 탭" description="Default · Hover · On · On hover · 이름 편집(Variant5) · 이름 편집 + Hover(Variant6).">
<div className="sw-footer-story-nodes">
<Node id="26:9794" name="layout01" impl='SWFooterLayoutTabs (마지막 탭, shape "slant")' states="6 상태"><TabSpecimens layout={{ id: "l1", label: "Layout 01" }} /></Node>
<Node id="26:9837" name="layout02" impl='SWFooterLayoutTabs (shape "rounded")' states="6 상태"><TabSpecimens layout={{ id: "l2", label: "Layout 02", shape: "rounded" }} /></Node>
<Node id="26:9885" name="layout03" impl='SWFooterLayoutTabs (shape "rounded")' states="Default · Hover · On · On hover"><TabSpecimens layout={{ id: "l3", label: "Layout 03", shape: "rounded" }} editing={false} /></Node>
<Node id="26:9914" name="model" impl="SWFooterLayoutTabs modelLabel" states="Default · Hover · On">
<div className="sw-footer-story-column">
<SWFooterLayoutTabs layouts={[]} value="x" />
<Pin hover=".hm-sw-footer-tab--model"><SWFooterLayoutTabs layouts={[]} value="x" /></Pin>
<SWFooterLayoutTabs layouts={[]} value={SW_FOOTER_MODEL} />
</div>
</Node>
<Node id="26:9776" name="btn_add_layout" impl="SWFooterLayoutTabs onAdd" states="Default · Hover">
<div className="sw-footer-story-column">
<SWFooterLayoutTabs layouts={[]} value="x" modelLabel={false} onAdd={noop} />
<Pin hover=".hm-sw-footer-tabs__add"><SWFooterLayoutTabs layouts={[]} value="x" modelLabel={false} onAdd={noop} /></Pin>
</div>
</Node>
<Node id="26:9785" name="btn_delete" impl='SWFooterLayoutTabs onRemove (shape "rounded")' states="Default · Hover">
<div className="sw-footer-story-column">
<Pin hover=".hm-sw-footer-tab"><SWFooterLayoutTabs layouts={[{ id: "a", label: "Layout 02", shape: "rounded" }]} value="x" modelLabel={false} onRemove={noop} /></Pin>
<Pin hover=".hm-sw-footer-tab, .hm-sw-footer-tab__remove"><SWFooterLayoutTabs layouts={[{ id: "a", label: "Layout 02", shape: "rounded" }]} value="x" modelLabel={false} onRemove={noop} /></Pin>
</div>
</Node>
<Node id="26:9931" name="btn_close2" impl='SWFooterLayoutTabs onRemove (shape "slant")' states="Default · Hover">
<div className="sw-footer-story-column">
<Pin hover=".hm-sw-footer-tab"><SWFooterLayoutTabs layouts={[{ id: "a", label: "Layout 01" }]} value="a" modelLabel={false} onRemove={noop} /></Pin>
<Pin hover=".hm-sw-footer-tab, .hm-sw-footer-tab__remove"><SWFooterLayoutTabs layouts={[{ id: "a", label: "Layout 01" }]} value="a" modelLabel={false} onRemove={noop} /></Pin>
</div>
</Node>
<Node id="26:9942" name="예시) 탭 상태변화" impl="SWFooterLayoutTabs value" states="Model · Layout 01 · 02 · 03 선택">
<div className="sw-footer-story-column">
{[SW_FOOTER_MODEL, "layout1", "layout2", "layout3"].map(value => <SWFooterLayoutTabs key={value} layouts={LAYOUTS} value={value} onAdd={noop} />)}
</div>
</Node>
</div>
</StorySection>
<StorySection title="보조기능 버튼 (btn_객체스냅 26:9248 … btn_선가중치 26:9412)" description="35개 목록 밖 보조 참고. 구현 열: Default · Hover · Selected · Selected + Hover (Hover는 data-sb-hover 고정).">
<div className="sw-footer-story-aux-compare">
{AUX_ITEMS.map((item, index) => (
<figure key={item.id} className="sw-footer-story-aux-row">
<figcaption>{AUX_SETS[index]} {item.label}</figcaption>
<img src={new URL(`./assets/figma-reference/footer/aux-buttons/${AUX_SETS[index].replace(":", "-")}.png`, import.meta.url).href} alt={`Figma ${AUX_SETS[index]} ${item.label}`} />
<SWTheme className="sw-footer-story-group-surface sw-footer-story-aux-states">
<SWFooterAuxButton label={item.label} icon={item.icon} pressed={false} />
<Pin hover=".hm-sw-footer-aux__button"><SWFooterAuxButton label={item.label} icon={item.icon} pressed={false} /></Pin>
<SWFooterAuxButton label={item.label} icon={item.icon} pressed />
<Pin hover=".hm-sw-footer-aux__button"><SWFooterAuxButton label={item.label} icon={item.icon} pressed /></Pin>
</SWTheme>
</figure>
))}
</div>
</StorySection>
<StorySection title="명령창">
<div className="sw-footer-story-nodes">
<Node id="26:10023" name="ico_arrow" impl="SWFooterCommandLine 펼치기 버튼" states="Default · Hover · On(펼침) · On + Hover">
<div className="sw-footer-story-column">
<SWFooterCommandLine history={[]} style={SMALL_COMMAND} />
<Pin hover=".hm-sw-footer-command__toggle"><SWFooterCommandLine history={[]} style={SMALL_COMMAND} /></Pin>
<SWFooterCommandLine history={[]} expanded style={SMALL_COMMAND} />
<Pin hover=".hm-sw-footer-command__toggle"><SWFooterCommandLine history={[]} expanded style={SMALL_COMMAND} /></Pin>
</div>
</Node>
<Node id="26:10048" name="command (접힘)" impl="SWFooterCommandLine" states="71px, 최근 3줄"><SWFooterCommandLine history={HISTORY} value="TEXT" /></Node>
<Node id="26:10056" name="command (펼침)" impl="SWFooterCommandLine expanded" states="270px"><SWFooterCommandLine history={HISTORY} value="TEXT" expanded /></Node>
</div>
</StorySection>
</StoryPage>
),
}
// ---------- 4. 도구 및 메뉴 변경 예제 ----------
const icon = (node: ReactNode) => <span className="sw-footer-story-lucide">{node}</span>
const CUSTOM: SWFooterCategory[] = [
{ id: "survey", label: "측량", tools: [
{ id: "move", label: "Move", icon: icon(<Move size={16} />) },
{ id: "rotate", label: "Rotate", icon: icon(<RotateCw size={16} />) },
{ id: "trim", label: "Trim", icon: icon(<Scissors size={16} />) },
{ id: "spline", label: "Spline", icon: icon(<Spline size={16} />), disabled: true },
] },
{ id: "draw", label: "그리기", tools: DRAW_TOOLS.slice(0, 4) },
{ id: "custom", label: "사용자", tools: [] },
]
const CUSTOM_AUX: SWFooterAuxItem[] = [
{ id: "snap", label: "스냅", icon: icon(<Magnet size={14} />), pressed: true },
{ id: "grid", label: "격자", icon: icon(<Grid3x3 size={14} />), pressed: false },
{ id: "ortho", label: "직교", icon: icon(<Square size={14} />), pressed: false },
{ id: "track", label: "추적", icon: icon(<Crosshair size={14} />) },
]
function CustomFooter() {
const [categories, setCategories] = useState(CUSTOM)
const [categoryId, setCategoryId] = useState("survey")
const [active, setActive] = useState<string | null>("move")
const [aux, setAux] = useState(CUSTOM_AUX)
const [log, setLog] = useState<string[]>([])
const note = (line: string) => setLog(lines => [...lines.slice(-5), line])
return (
<>
<Log lines={log} />
<Screen width={1440} canvas={60}>
<SWFooter categories={categories} categoryId={categoryId} onCategoryChange={setCategoryId} slotCount={10}
activeToolId={active} onToolSelect={id => { setActive(id); note(`tool:${id}`) }}
onSlotAdd={(index, category) => {
setCategories(list => list.map(item => (item.id === category ? { ...item, tools: [...item.tools, { id: `pt${index}`, label: `Pt ${index + 1}`, ...swFooterToolIcons.point }] } : item)))
note(`slotAdd:${category}#${index}`)
}}
slotGuide={{ title: "명령 추가", caption: "클릭", media: <span className="sw-footer-story-guide-text">빈 칸을 눌러 Point 명령 추가</span> }}
onCategoryAdd={() => { setCategories(list => [...list, { id: `c${list.length}`, label: `분류 ${list.length + 1}`, tools: [] }]); note("categoryAdd") }}
auxItems={aux} onAuxPressedChange={(id, pressed) => setAux(items => items.map(item => (item.id === id ? { ...item, pressed } : item)))}
onAuxItemClick={id => note(`aux:${id}`)}
auxAll={{ title: "설정", subtitle: "열기", onClick: () => note("auxAll") }} commandAll={{ title: "명령", subtitle: "목록", onClick: () => note("commandAll") }}
trailing={<span className="sw-footer-story-status">X 1024.50 · Y 512.25</span>}
layoutTabs={<SWFooterLayoutTabs layouts={[{ id: "plan", label: "평면도", locked: true }, { id: "section", label: "종단면" }]} value="plan" modelLabel="모델" />} />
</Screen>
</>
)
}
export const CustomContent: Story = {
name: "도구 및 메뉴 변경 예제",
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="도구 및 메뉴 변경" description="공통 디자인(크기·배경·상태)은 그대로, 내용만 데이터로 변경: 분류·명령 텍스트·아이콘(lucide), slotCount 10, 빈 칸 클릭으로 명령 추가, 분류 추가, 보조기능 4개(columns 3 → 2행), 측면 버튼 문구, trailing 좌표, Layout 이름·잠금·Model 문구." />
<StorySection title="사용자 구성"><CustomFooter /></StorySection>
</StoryPage>
),
}
// ---------- 5. 화면 폭 ----------
function ReferenceFooter() {
const [open, setOpen] = useState(false)
const [aux, setAux] = useState(REFERENCE_AUX)
const [active, setActive] = useState<string | null>(null)
return (
<SWFooter categories={REFERENCE_CATEGORIES} auxItems={aux} onAuxPressedChange={(id, pressed) => setAux(items => items.map(item => (item.id === id ? { ...item, pressed } : item)))}
activeToolId={active} onToolSelect={id => setActive(id)} onSlotAdd={noop} onCategoryAdd={noop} status="10.5939, 6.0728, 0.0000"
command={<SWFooterCommandLine history={REFERENCE_HISTORY} expanded={open} onExpandedChange={setOpen} />}
layoutTabs={<SWFooterLayoutTabs layouts={LAYOUTS_2} value={SW_FOOTER_MODEL} onAdd={noop} />} />
)
}
export const Responsive: Story = {
name: "Responsive More",
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="Responsive More" description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
{[1920, 1440, 1280, 950, 860, 760].map(width => (
<div key={width} className="sw-footer-story-width">
<StorySection title={`${width}px`}>
<Screen width={width} canvas={90}><ReferenceFooter /></Screen>
</StorySection>
</div>
))}
</StoryPage>
),
}
// ---------- 6. 개별 부품 ----------
export const Parts: Story = {
name: "개별 부품 (조합용)",
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
tags: ["verification"],
render: () => (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="개별 부품" description="SWFooter 없이 부품만 쓸 때는 <SWTheme> 안에 둡니다(SWFooter·SWFooterCommandLine은 루트에 hm-sw-theme이 있어 필요 없음)." />
<StorySection title="SWFooterAuxGroup (columns 3 / 2)">
<SWTheme className="sw-footer-story-row"><SWFooterAuxGroup items={AUX_ITEMS} /><SWFooterAuxGroup items={AUX_ITEMS.slice(0, 4)} columns={2} /></SWTheme>
</StorySection>
<StorySection title="SWFooterMenu + SWFooterToolButton">
<SWTheme className="sw-footer-story-group-surface sw-footer-story-inline">
<div className="sw-footer-story-row sw-footer-story-row--tight">{DRAW_TOOLS.slice(0, 3).map(tool => <SWFooterToolButton key={tool.id} {...toolProps(tool)} />)}<SWFooterAddSlot guide={false} /></div>
<SWFooterMenu items={CATEGORIES.slice(0, 4)} value="modify" onAdd={noop} style={{ width: 4 * 68 + 1 }} />
</SWTheme>
</StorySection>
<StorySection title="SWFooterCommandLine (Controlled 펼침)">
<CommandDemo />
</StorySection>
</StoryPage>
),
}
function CommandDemo() {
const [history, setHistory] = useState(HISTORY)
const [open, setOpen] = useState(false)
return <SWFooterCommandLine history={history} expanded={open} onExpandedChange={setOpen} onSubmit={command => setHistory(lines => [...lines, `Command: ${command}`])} />
}