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.
425 lines
26 KiB
TypeScript
425 lines
26 KiB
TypeScript
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}`])} />
|
||
}
|