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 /** 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 (
{children}
) } function FigmaFooter() { return ( {}} layoutTabs={} /> ) } // ---------- 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 ( <>
} activeToolId={demo.activeToolId} onToolSelect={demo.onToolSelect} onSlotAdd={demo.onSlotAdd} onCategoryAdd={demo.onCategoryAdd} slotGuide={{ media: }} auxItems={demo.aux} onAuxPressedChange={demo.onAuxPressedChange} auxAll={{ onClick: demo.onSide("auxAll") }} commandAll={{ onClick: () => { setCommandOpen(open => !open); demo.onSide("commandAll")() }, pressed: commandOpen }} layoutTabs={} />
) } export const FullFooter: Story = { name: "SW Footer", render: () => ( ), } // ---------- 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: () => ( {CATEGORIES.slice(0, 3).map(category => ( {}} /> ))} ), } // ---------- 3. Figma 디자인별 Variant (35 노드) ---------- function Node({ id, name, impl, states, children }: { id: string; name: string; impl: string; states: string; children: ReactNode }) { return (
{id} {name} {impl} {states}

Figma

{`Figma

구현

{children}
) } const one = (layout: SWFooterLayout): SWFooterLayout[] => [layout] const noop = () => {} function TabSpecimens({ layout, editing = true }: { layout: SWFooterLayout; editing?: boolean }) { const tabs = (value: string, edit?: boolean) => ( ) return (
{tabs("x")} {tabs("x")} {tabs(layout.id)} {tabs(layout.id)} {editing && tabs("x", true)} {editing && {tabs("x", true)}}
) } function ToolSpecimen({ tool }: { tool: SWFooterTool }) { return (
) } function MenuSpecimen({ label }: { label: string }) { const items = [{ id: "a", label }] return (
) } 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: () => ( 요청된 35개 노드 전체. 왼쪽 = Figma 렌더(get_screenshot), 오른쪽 = 같은 순서의 구현 상태(Hover는 Story 전용 data-sb-hover로 고정). 노드 → 컴포넌트·Props 매핑은 docs/sw/sw-footer-figma-mapping-2026-10-20.md.} />
{TOOL_NODES.map(([id, tool]) => ( ))}
{MENU_NODES.map(([id, label]) => ( ))}
}} /> }} guideOpen />
{[0, 1, 2, 3, 4, 5].map(index => } />)}
{[SW_FOOTER_MODEL, "layout1", "layout2", "layout3"].map(value => )}
{AUX_ITEMS.map((item, index) => (
{AUX_SETS[index]} {item.label}
{`Figma
))}
), } // ---------- 4. 도구 및 메뉴 변경 예제 ---------- const icon = (node: ReactNode) => {node} const CUSTOM: SWFooterCategory[] = [ { id: "survey", label: "측량", tools: [ { id: "move", label: "Move", icon: icon() }, { id: "rotate", label: "Rotate", icon: icon() }, { id: "trim", label: "Trim", icon: icon() }, { id: "spline", label: "Spline", icon: icon(), disabled: true }, ] }, { id: "draw", label: "그리기", tools: DRAW_TOOLS.slice(0, 4) }, { id: "custom", label: "사용자", tools: [] }, ] const CUSTOM_AUX: SWFooterAuxItem[] = [ { id: "snap", label: "스냅", icon: icon(), pressed: true }, { id: "grid", label: "격자", icon: icon(), pressed: false }, { id: "ortho", label: "직교", icon: icon(), pressed: false }, { id: "track", label: "추적", icon: icon() }, ] function CustomFooter() { const [categories, setCategories] = useState(CUSTOM) const [categoryId, setCategoryId] = useState("survey") const [active, setActive] = useState("move") const [aux, setAux] = useState(CUSTOM_AUX) const [log, setLog] = useState([]) const note = (line: string) => setLog(lines => [...lines.slice(-5), line]) return ( <> { 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: 빈 칸을 눌러 Point 명령 추가 }} 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={X 1024.50 · Y 512.25} layoutTabs={} /> ) } 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: () => ( ), } // ---------- 5. 화면 폭 ---------- function ReferenceFooter() { const [open, setOpen] = useState(false) const [aux, setAux] = useState(REFERENCE_AUX) const [active, setActive] = useState(null) return ( 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={} layoutTabs={} /> ) } export const Responsive: Story = { name: "Responsive More", render: () => ( {[1920, 1440, 1280, 950, 860, 760].map(width => (
))}
), } // ---------- 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: () => (
{DRAW_TOOLS.slice(0, 3).map(tool => )}
), } function CommandDemo() { const [history, setHistory] = useState(HISTORY) const [open, setOpen] = useState(false) return setHistory(lines => [...lines, `Command: ${command}`])} /> }