Files
HMDesign/src/sw/sw-footer-fixture.tsx
T

166 lines
9.3 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 { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from "react"
import {
swFooterAuxIcons, swFooterToolIcons,
type SWFooterAuxItem, type SWFooterCategory, type SWFooterLayout, type SWFooterTool,
} from "@hm-design/sw"
import { PenLine, SquareDashed } from "lucide-react"
import guideSheet from "./assets/figma-reference/footer/26-9096.png"
// SW/Footer fixture: the Figma 26:10097 footer content as public-API data, like a consuming program builds it.
/** Figma 도형 commands (26:10111): Line … Hatch, 9 slots. */
export const DRAW_TOOLS: SWFooterTool[] = [
{ id: "line", label: "Line", ...swFooterToolIcons.line },
{ id: "pline", label: "PLine", ariaLabel: "Polyline", ...swFooterToolIcons.pline },
{ id: "arc", label: "Arc", ...swFooterToolIcons.arc },
{ id: "circle", label: "Circle", ...swFooterToolIcons.circle },
{ id: "ellipse", label: "Ellipse", ...swFooterToolIcons.ellipse },
{ id: "rect", label: "Rect...", ariaLabel: "Rectangle", ...swFooterToolIcons.rect },
{ id: "poly", label: "Poly", ariaLabel: "Polygon", ...swFooterToolIcons.poly },
{ id: "point", label: "Point", ...swFooterToolIcons.point },
{ id: "hatch", label: "Hatch", ...swFooterToolIcons.hatch },
]
/** Figma category row (text_도형 … text_구조배근). Only 도형 has Figma commands; 수정 shows the NCopy component (26:9739). */
export const CATEGORIES: SWFooterCategory[] = [
{ id: "shape", label: "도형", tools: DRAW_TOOLS },
{ id: "modify", label: "수정", tools: [{ id: "ncopy", label: "NCopy", ariaLabel: "Multiple copy", ...swFooterToolIcons.ncopy }] },
{ id: "edit", label: "편집", tools: [] },
{ id: "text", label: "문자", tools: [] },
{ id: "layer", label: "레이어", tools: [] },
{ id: "block", label: "블록", tools: [] },
{ id: "view", label: "뷰", tools: [] },
{ id: "select", label: "선택/조회", tools: [] },
{ id: "road", label: "도로/횡단", tools: [] },
{ id: "structure", label: "구조/배근", tools: [] },
]
/** Figma 26:10104 (3 × 2). */
export const AUX_ITEMS: SWFooterAuxItem[] = [
{ id: "objectSnap", label: "객체스냅", icon: swFooterAuxIcons.objectSnap, pressed: false },
{ id: "selectionEffect", label: "선택효과", icon: swFooterAuxIcons.selectionEffect, pressed: false },
{ id: "orthogonal", label: "직각교차", icon: swFooterAuxIcons.orthogonal, pressed: false },
{ id: "gridSnap", label: "격자스냅", icon: swFooterAuxIcons.gridSnap, pressed: false },
{ id: "gridBackground", label: "격자배경", icon: swFooterAuxIcons.gridBackground, pressed: false },
{ id: "lineWeight", label: "선가중치", icon: swFooterAuxIcons.lineWeight, pressed: false },
]
/**
* Reference screen (2026-10-20, src/sw/assets/footer-reference): the program's own content in the same footer.
* Labels, categories and the extra command are program data; WIPEOUT and 객체스냅추적 have no Figma icon, so the
* Story passes its own (lucide) icons the way a program would.
*/
const lucide = (node: ReactNode) => <span className="sw-footer-story-lucide">{node}</span>
export const REFERENCE_TOOLS: SWFooterTool[] = [
{ id: "line", label: "LINE", ...swFooterToolIcons.line },
{ id: "pline", label: "PLINE", ariaLabel: "Polyline", ...swFooterToolIcons.pline },
{ id: "arc", label: "ARC", ...swFooterToolIcons.arc },
{ id: "circle", label: "CIRCLE", ...swFooterToolIcons.circle },
{ id: "ellipse", label: "ELLIPSE", ...swFooterToolIcons.ellipse },
{ id: "rect", label: "RECTA...", ariaLabel: "Rectangle", ...swFooterToolIcons.rect },
{ id: "poly", label: "POLYGON", ...swFooterToolIcons.poly },
{ id: "point", label: "POINT", ...swFooterToolIcons.point },
{ id: "hatch", label: "HATCH", ...swFooterToolIcons.hatch },
{ id: "wipeout", label: "WIPEOUT", icon: lucide(<SquareDashed size={16} />) },
]
export const REFERENCE_CATEGORIES: SWFooterCategory[] = [
{ id: "shape", label: "도형", tools: REFERENCE_TOOLS },
...["수정", "편집", "문자", "치수/연산", "블록", "레이어", "뷰", "선택/조회", "모델링", "선형/횡단", "구조/배근"]
.map((label, index) => ({ id: `c${index}`, label, tools: index === 0 ? CATEGORIES[1].tools : [] })),
]
export const REFERENCE_AUX: SWFooterAuxItem[] = [
{ id: "objectSnap", label: "객체스냅", icon: swFooterAuxIcons.objectSnap, pressed: false },
{ id: "orthogonal", label: "직각교차", icon: swFooterAuxIcons.orthogonal, pressed: false },
{ id: "lineWeight", label: "선가중치", icon: swFooterAuxIcons.lineWeight, pressed: false },
{ id: "gridBackground", label: "격자배경", icon: swFooterAuxIcons.gridBackground, pressed: true },
{ id: "gridSnap", label: "격자스냅", icon: swFooterAuxIcons.gridSnap, pressed: false },
{ id: "objectTrack", label: "객체스냅추적", icon: lucide(<PenLine size={14} />), pressed: true },
]
export const REFERENCE_HISTORY = [
"Raster image support loaded.",
"Command: PSLTSCALE",
"Enter new value for PSLTSCALE <ON>: _ON",
"Command: VSBACKGROUNDS",
"Enter new value for VSBACKGROUNDS <1>: 1",
]
export const LAYOUTS: SWFooterLayout[] = [
{ id: "layout3", label: "Layout 03" },
{ id: "layout2", label: "Layout 02" },
{ id: "layout1", label: "Layout 01" },
]
export const HISTORY = [
"Fillet: [Polyline/Radius/Trim/Settings/Multiple]/<Select first entity>: R",
"Fillet radius <0.0000>: 6",
"Current settings: mode = NOTRIM, radius = 6.0000",
]
/**
* The six frames of guide_바로가기 등록 (26:9096 Variant5 → 6 → 4 → 9 → 8 → 9): the guide "video" of the empty slot,
* cut from the Figma export of the set (each frame's 194 × 111 preview at +6, +6).
*/
const GUIDE_FRAMES = [[20, 52], [20, 237], [23, 422], [23, 599], [19, 807], [22, 1001]] as const
export function GuideFrame({ index }: { index: number }) {
const [x, y] = GUIDE_FRAMES[index % GUIDE_FRAMES.length]
return <span className="sw-footer-story-guide-frame" style={{ backgroundImage: `url(${guideSheet})`, backgroundPosition: `-${x + 6}px -${y + 6}px` }} aria-hidden="true" />
}
/** Cycles the frames like the guide video (Storybook only; a program passes its own <video>). */
export function GuideMovie({ interval = 900 }: { interval?: number }) {
const [frame, setFrame] = useState(0)
useEffect(() => { const timer = window.setInterval(() => setFrame(value => value + 1), interval); return () => window.clearInterval(timer) }, [interval])
return <GuideFrame index={frame} />
}
/** Pins :hover visuals on descendants matching `hover` (data-sb-hover, mirrored by .storybook/pseudo-states.ts). */
export function Pin({ hover, children }: { hover?: string; children: ReactNode }) {
const ref = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (!hover) return
const nodes = Array.from(ref.current?.querySelectorAll(hover) ?? [])
nodes.forEach(node => node.setAttribute("data-sb-hover", ""))
return () => nodes.forEach(node => node.removeAttribute("data-sb-hover"))
})
return <div ref={ref} className="sw-footer-story-pin">{children}</div>
}
/** Footer demo state shared by the interactive Stories. */
export function useFooterDemo({ aux: initialAux = AUX_ITEMS, layouts: initialLayouts = LAYOUTS, history: initialHistory = HISTORY } = {}) {
const [categoryId, setCategoryId] = useState("shape")
const [activeToolId, setActiveToolId] = useState<string | null>(null)
const [aux, setAux] = useState(initialAux)
const [layouts, setLayouts] = useState(initialLayouts)
const [space, setSpace] = useState("model")
const [history, setHistory] = useState(initialHistory)
const [log, setLog] = useState<string[]>([])
const note = (line: string) => setLog(lines => [...lines.slice(-5), line])
return {
categoryId, activeToolId, aux, layouts, space, history, log,
onCategoryChange: (id: string) => { setCategoryId(id); setActiveToolId(null); note(`category:${id}`) },
onToolSelect: (id: string) => {
setActiveToolId(current => (current === id ? null : id))
setHistory(lines => [...lines, `Command: ${id.toUpperCase()}`])
note(`tool:${id}`)
},
onSlotAdd: (index: number, category: string) => note(`slotAdd:${category}#${index}`),
onCategoryAdd: () => note("categoryAdd"),
onAuxPressedChange: (id: string, pressed: boolean) => { setAux(items => items.map(item => (item.id === id ? { ...item, pressed } : item))); note(`aux:${id}=${pressed}`) },
onSpaceChange: (value: string) => { setSpace(value); note(`space:${value}`) },
onLayoutAdd: () => {
setLayouts(items => {
const next = Math.max(0, ...items.map(item => Number(item.id.replace("layout", "")) || 0)) + 1
return [{ id: `layout${next}`, label: `Layout ${String(next).padStart(2, "0")}` }, ...items]
})
note("layoutAdd")
},
onLayoutRemove: (id: string) => { setLayouts(items => items.filter(item => item.id !== id)); setSpace(value => (value === id ? "model" : value)); note(`layoutRemove:${id}`) },
onLayoutRename: (id: string, label: string) => { setLayouts(items => items.map(item => (item.id === id ? { ...item, label } : item))); note(`layoutRename:${id}=${label}`) },
onCommand: (command: string) => { if (command.trim()) setHistory(lines => [...lines, `Command: ${command}`]); note(`command:${command}`) },
onSide: (name: string) => () => note(name),
}
}
export function Log({ lines }: { lines: string[] }) {
return <p className="sw-story-log" aria-live="polite" data-callback-log="">Callback: {lines.length ? lines.join(" → ") : "없음"}</p>
}