Deploy Storybook / deploy (push) Successful in 1m11s
- SW Viewer layer popup: search + 전체 레이어 fixed, only the list scrolls; the all-layers control is the SWLayerVisibilityIcon bulb (role="switch"), one light #d2c8b4 line under it. - Example H and the layer popup AI docs follow the bulb layout (VIEWER, DESIGN_RULES, COMPONENT_CATALOG, COMPOSITION_PATTERNS, AI_GUIDE sync). - Popup toggle switch (Figma toggle_btn_new 633:75188 / 633:75902): one sw.css face for the HM UI Switch in SWPopup and SWViewerSwitch in SWViewerPanel; SW Header switches unchanged. - SW Footer stories: no inner scrollers, fluid screens; workspace fits the viewport. - SW Footer category tabs keep their positions when selected (divider slots kept, pocket edges drawn over the neighbours' padding). - Storybook .sb-page-inner: 1440px max from 1920px (was 80vw). - SW Header equal-split change was cancelled; header CSS unchanged. Verified: check:ai-docs PASS, viewer test 49 PASS (1 FAIL hover face + timeout pre-existing), footer-design-review 21 PASS, browser checks of switch/footer/inner. Not verified: footer-popup-viewer-refinement (needs full Storybook build), text-weight/hover tests, Switch 37x16 AI docs (deferred).
122 lines
8.3 KiB
TypeScript
122 lines
8.3 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 bulb: ON only when every layer is shown. A partial state reads OFF, so turning it on shows all.
|
|
const allVisible = hiddenLayers.size === 0
|
|
const [query, setQuery] = useState("")
|
|
const shownLayers = layers.filter(layer => layer.name.toLowerCase().includes(query.trim().toLowerCase()))
|
|
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={`레이어 (${layers.length}개)`} open={panels.layers} onClose={() => setPanels({ ...panels, layers: false })}>
|
|
<SWViewerSection surface="plain" aria-label="레이어 목록" className="app-viewer-layer-popup">
|
|
{/* Fixed: search, then the all-layers bulb (same button + SWLayerVisibilityIcon as the rows, role="switch").
|
|
Only the list below scrolls. Rows: swatch · name · count · bulb button, never a switch. */}
|
|
<SWViewerInput type="search" className="app-viewer-layer-search" placeholder="레이어 검색" aria-label="레이어 검색" value={query} onChange={event => setQuery(event.currentTarget.value)} />
|
|
<SWViewerSection surface="plain" title="전체 레이어" className="app-viewer-all-layers">
|
|
<button type="button" role="switch" aria-checked={allVisible} aria-label="전체 레이어" className="app-viewer-layer-bulb"
|
|
title={allVisible ? "전체 숨기기" : "전체 표시하기"} onClick={() => setAllLayers(!allVisible)}><SWLayerVisibilityIcon on={allVisible} /></button>
|
|
</SWViewerSection>
|
|
{shownLayers.length === 0 ? <SWViewerHint>검색 결과가 없습니다 ("{query}")</SWViewerHint> : <ul className="app-viewer-layers">
|
|
{shownLayers.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>
|
|
</>
|
|
)
|
|
}
|