Files
HMDesign/packages/sw/ai/examples/ExampleH-ViewerMode.tsx
T
shkim4 6f031cab18
Deploy Storybook / deploy (push) Successful in 1m11s
style(sw): drop the 취소 / 적용 footer from the viewer options popup
Options already apply on every toggle, input and slider change, so the footer
only recorded an event and closed the popup. The options panel no longer
passes footer (no empty footer area or divider); reset stays in the title bar.
The 축척 지정 input popup keeps its footer. VIEWER.md §4.1, the composition
pattern and Example H follow.
2026-10-22 17:03:48 +09:00

117 lines
7.5 KiB
TypeScript

// Example H — viewer mode: SWViewerHeader (data-driven menu) + SWViewerPanel popups with free content.
// Only public components. The program owns the state and the commands; the package owns the look and the
// open / close / focus / keyboard / drag behavior. Icons are the program's own (16px, currentColor).
import { useState } from "react"
import {
SWLayerVisibilityIcon, SWViewerBadge, SWViewerButton, SWViewerField, SWViewerHeader, SWViewerHeaderButton, SWViewerHint, SWViewerInput, SWViewerPanel,
SWViewerSection, SWViewerSlider, SWViewerStatus, SWViewerSwitch,
type SWViewerHeaderGroup,
} from "@hm-design/sw"
import "./example-layout.css"
const Icon = ({ d }: { d: string }) => (
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={d} /></svg>
)
const icons = {
fit: <Icon d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5" />,
layers: <Icon d="M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5" />,
options: <Icon d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12M20 18h0" />,
info: <Icon d="M12 8h.01M11 12h1v5h1M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z" />,
}
const layers = [{ name: "0", color: "#ffffff", count: 120 }, { name: "WALL", color: "#ff3b30", count: 42 }, { name: "DIM", color: "#34c759", count: 18 }]
export function ExampleHViewerMode({ onEvent }: { onEvent?: (name: string) => void }) {
const [fileName, setFileName] = useState<string | null>(null)
const [space, setSpace] = useState("model")
const [depth, setDepth] = useState(false)
const [panels, setPanels] = useState({ layers: false, options: false })
const [grid, setGrid] = useState(true)
const [zoomSpeed, setZoomSpeed] = useState(1)
const [about, setAbout] = useState(false)
// Program-owned visibility per layer; each bulb button toggles only its own layer.
const [hiddenLayers, setHiddenLayers] = useState<ReadonlySet<string>>(new Set())
const toggleLayer = (name: string) => {
const next = new Set(hiddenLayers), visible = !next.has(name)
if (visible) next.add(name); else next.delete(name)
setHiddenLayers(next)
onEvent?.(`layer:${name}=${!visible}`)
}
// All-layers switch: ON only when every layer is shown. A partial state reads OFF, so turning it on shows all.
const allVisible = hiddenLayers.size === 0
const setAllLayers = (visible: boolean) => {
setHiddenLayers(new Set(visible ? [] : layers.map(layer => layer.name)))
onEvent?.(`layer:all=${visible}`)
}
// File open: the header button after the logo (icon + label, icon only when the header is narrow). File name click:
// the sample list only (no Open / + New).
const openFile = () => onEvent?.("file:open")
const samples = [{ id: "sample-a", name: "BasicSample.dwg", title: "기본 도면 · DWG" }, { id: "sample-b", name: "simple.dxf", title: "기본 도형 · DXF" }]
const groups: SWViewerHeaderGroup[] = [
{ id: "space", label: "도면 공간", surface: "field", hidden: !fileName, items: [
{ type: "tabs", id: "space", label: "모델과 배치 선택", collapseBelow: 1180, value: space, onValueChange: setSpace,
options: [{ value: "model", label: "Model", count: 1200 }, { value: "layout", label: "Layout", count: 40 }] },
] },
{ id: "view", label: "화면 보기", surface: "field", items: [
// Measured steps: full while it fits, icon only next, then into More (the numbers only give the order).
{ type: "button", id: "fit", label: "전체 보기", icon: icons.fit, iconOnlyBelow: 560, collapseBelow: 1180, disabled: !fileName },
{ type: "switch", id: "depth", label: "2.5D", checked: depth, collapseBelow: 1180, onCheckedChange: setDepth },
] },
{ id: "panels", label: "패널", placement: "end", items: [
{ type: "toggle", id: "layers", label: "레이어", icon: icons.layers, pressed: panels.layers, iconOnlyBelow: 900, onPressedChange: layers => setPanels({ ...panels, layers }) },
{ type: "toggle", id: "options", label: "설정", icon: icons.options, pressed: panels.options, iconOnlyBelow: 900, onPressedChange: options => setPanels({ ...panels, options }) },
] },
]
return (
<>
{/* No logo prop: nothing is drawn on the cap (no "HM" text). Pass logoSrc / logo to show the program's logo. */}
<SWViewerHeader documentTitle={fileName} groups={groups}
documents={samples} activeDocumentId={samples.find(sample => sample.name === fileName)?.id} onDocumentSelect={sample => setFileName(sample.name)} onDocumentOpen={openFile} documentOpenLabel="파일 열기"
onCommand={command => onEvent?.(`${command.itemId}/${command.id}${command.value === undefined ? "" : `=${String(command.value)}`}`)}
trailing={<>
<SWViewerStatus state={fileName ? "ready" : "idle"} />
<SWViewerHeaderButton iconOnly icon={icons.info} label="정보" onClick={() => setAbout(true)} />
</>} />
<SWViewerPanel title="레이어" open={panels.layers} onClose={() => setPanels({ ...panels, layers: false })}>
<SWViewerSection surface="plain" aria-label="레이어 목록">
{/* Top: all-layers SWViewerSwitch. Rows: swatch · name · count · bulb button (SWLayerVisibilityIcon), not a switch. */}
<SWViewerSection surface="plain" title="전체 레이어" className="app-viewer-all-layers">
<SWViewerSwitch aria-label="전체 레이어" checked={allVisible} onCheckedChange={setAllLayers} />
</SWViewerSection>
<ul className="app-viewer-layers">
{layers.map(layer => {
const visible = !hiddenLayers.has(layer.name)
return (
<li key={layer.name} data-visible={visible}>
<span className="app-viewer-layer-swatch" style={{ background: layer.color }} aria-hidden="true" />
<span className="app-viewer-layer-name" title={layer.name}>{layer.name}</span>
<SWViewerBadge>{layer.count}</SWViewerBadge>
<button type="button" className="app-viewer-layer-bulb"
aria-label={`${layer.name} ${visible ? "숨기기" : "표시하기"}`} title={visible ? "숨기기" : "표시하기"}
onClick={() => toggleLayer(layer.name)}><SWLayerVisibilityIcon on={visible} /></button>
</li>
)
})}
</ul>
</SWViewerSection>
</SWViewerPanel>
<SWViewerPanel title="옵션" open={panels.options} onClose={() => setPanels({ ...panels, options: false })} width={340} height="auto" inset={{ side: 356 }}>
<SWViewerSection title="표시">
<SWViewerField label="그리드">{id => <SWViewerSwitch id={id} checked={grid} onCheckedChange={setGrid} />}</SWViewerField>
<SWViewerField label="줌 속도" layout="stacked" below={<SWViewerSlider aria-label="줌 속도" value={zoomSpeed} min={0.2} max={3} step={0.1} onValueChange={setZoomSpeed} />}>
{id => <SWViewerInput id={id} type="number" min={0.2} max={3} step={0.1} value={zoomSpeed} onChange={event => setZoomSpeed(Number(event.currentTarget.value))} />}
</SWViewerField>
</SWViewerSection>
</SWViewerPanel>
<SWViewerPanel modal title="정보" open={about} onClose={() => setAbout(false)} width={360} height="auto" resizable={false} collapsible={false}
footer={<SWViewerButton variant="primary" onClick={() => setAbout(false)}>확인</SWViewerButton>}>
<SWViewerHint>모듈 구성과 라이선스 고지는 프로그램이 제공합니다.</SWViewerHint>
</SWViewerPanel>
</>
)
}