Files
HMDesign/src/sw/sw-viewer.stories.tsx
T
shkim4 da0e647b49 docs(storybook): simplify sidebar Story pages around the UI
Drop the category kicker from the 83 sidebar Stories (kicker is now optional;
hidden verification Stories keep it) and shorten or remove repeated, obvious
or internal page/section descriptions. Required specs, limits, virtual-data
and undecided-value notices stay. Story IDs, exports, fixtures, shared state
notes and package code are unchanged. Record: docs/reports/storybook-ui-simplification-2026-10-22.md
2026-10-22 16:13:58 +09:00

420 lines
23 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 { useRef, useState, type ReactNode } from "react"
import type { Meta, StoryObj } from "@storybook/react-vite"
import {
SWHeader, SWHeaderWindowControls, SWSelect, SWViewerButton, SWViewerField, SWViewerHeader, SWViewerHint, SWViewerInput, SWViewerMenu, SWViewerPanel, SWViewerSection, SWViewerSwitch,
type SWViewerHeaderGroup, type SWViewerMenuEntry,
} from "@hm-design/sw"
import "@hm-design/sw/styles.css"
import { Copy, Download, FileText, FolderOpen, Layers, Printer, Ruler, Scan, Search } from "lucide-react"
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
import { showVerificationControls } from "./sw-story-flags"
import { LAYERS, LayerPanel, OptionsPanel, PropertiesPanel, SAMPLES, useViewerDemo, type ViewerDemo } from "./sw-viewer-fixture"
import "./sw-stories.css"
import "./sw-viewer-stories.css"
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
// SW viewer mode (2026-10-19): the reference DWG/DXF viewer top menu and popups as package components.
// Stories use the public @hm-design/sw API only, like a consuming program. No DWG/DXF engine: commands are logged.
const KICKER = "HM DESIGN SYSTEM / SW"
const meta = {
title: "SW/Viewer",
parameters: { layout: "fullscreen", controls: { disable: true } },
} satisfies Meta
export default meta
type Story = StoryObj<typeof meta>
// Command record for the browser tests only (2026-10-21 Storybook simplification: not shown on the page).
function Log({ lines }: { lines: string[] }) {
return <p hidden data-callback-log="">Command: {lines.length ? lines.join(" → ") : "없음"}</p>
}
/** A viewer screen: header on top, the drawing area below it (panels are placed inside this frame). */
function ViewerFrame({ width = 1440, height = 620, children, header }: { width?: number; height?: number; header: ReactNode; children?: ReactNode }) {
return (
<div className="sw-story-scroller">
{/* Fills the Story column up to `width`, so panels placed at the right edge stay visible. */}
<div className="sw-viewer-story-frame" style={{ width: "100%", maxWidth: width, height }} data-viewer-frame="">
{header}
<div className="sw-viewer-story-canvas" aria-label="도면 영역 (예시)">{children}</div>
</div>
</div>
)
}
function AboutPanel({ demo }: { demo: ViewerDemo }) {
return (
<SWViewerPanel modal strategy="absolute" title="모듈 구성 · 라이선스 고지" open={demo.aboutOpen} onClose={() => demo.setAboutOpen(false)} width={420} height="auto" resizable={false} collapsible={false}
footer={<SWViewerButton variant="primary" onClick={() => demo.setAboutOpen(false)}>확인</SWViewerButton>}>
<SWViewerSection title="처리 파이프라인">
<ol className="sw-viewer-story-steps">
<li>파일 로드 (.dwg → acadrust WASM, .dxf → dxf-parser)</li>
<li>CAD 파싱 결과 정규화 (레이어 / 스페이스 / 엔티티)</li>
<li>Three.js 씬 구성 및 렌더링</li>
</ol>
</SWViewerSection>
<SWViewerHint>Storybook 예시: 링크와 실제 고지문은 프로그램이 제공합니다.</SWViewerHint>
</SWViewerPanel>
)
}
function ViewerScreen({ width = 1440, initialDocument = SAMPLES[1].name as string | null, open = [] as ("layers" | "options" | "properties")[], windowControls = false }) {
const demo = useViewerDemo({ initialDocument, initialPanels: open })
// 2026-10-21 new viewer version: SWHeader's own minimize / close (SWHeaderWindowControls, same icons, size, hover)
// at the right end after the existing trailing items (the `trailing` slot, no new API). The program owns the real
// window actions (onMinimize / onClose, as in SWHeader); the Story only logs them.
const trailing = windowControls ? (
<>
{demo.trailing}
<SWHeaderWindowControls showWindowMove={false} onMinimize={() => demo.record("minimize")} onClose={() => demo.record("close")} />
</>
) : demo.trailing
return (
<>
<Log lines={demo.log} />
<ViewerFrame width={width} header={<SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={trailing} onCommand={demo.onCommand} />}>
<LayerPanel demo={demo} strategy="absolute" />
<OptionsPanel demo={demo} strategy="absolute" inset={{ side: 356 }} />
<PropertiesPanel demo={demo} strategy="absolute" inset={{ side: 24, top: 488 }} />
<AboutPanel demo={demo} />
</ViewerFrame>
</>
)
}
export const ViewerHeader: Story = {
name: "Viewer Header",
render: () => (
<StoryPage>
<StoryPageHeader title="SW Viewer — SWViewerHeader" />
<StorySection title="기본형 (1440px, 도면 열림)">
<ViewerScreen />
</StorySection>
<StorySection title="최소화 · 닫기 버튼 (1440px, 도면 열림)" description="SWHeader의 최소화·닫기 버튼(SWHeaderWindowControls)을 trailing 오른쪽 끝에 그대로 둔 버전입니다. 실제 창 최소화·닫기는 프로그램이 처리합니다.">
<div data-viewer-window-controls=""><ViewerScreen windowControls /></div>
</StorySection>
</StoryPage>
),
}
export const MenuGroups: Story = {
name: "Menu Groups",
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
tags: ["verification"],
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
const pick = (ids: string[]): SWViewerHeaderGroup[] => demo.groups.map(group => ({ ...group, hidden: group.hidden || !ids.includes(group.id) }))
const rows: { title: string; ids: string[] }[] = [
{ title: "파일 그룹만 (파일 열기 · 샘플)", ids: ["file"] },
{ title: "도면 공간 (field 면 · tabs)", ids: ["space"] },
{ title: "화면 보기 (field 면 · button + switch)", ids: ["view"] },
{ title: "오른쪽 영역 (placement=end 패널 토글 + trailing 상태 · 정보)", ids: ["panels"] },
{ title: "전체", ids: demo.groups.map(group => group.id) },
]
return (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — 메뉴 그룹" />
<Log lines={demo.log} />
<div className="sw-story-stack">
{rows.map(row => (
<StorySection key={row.title} title={row.title}>
<div className="sw-story-scroller"><div style={{ width: 1440 }}><SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={pick(row.ids)} trailing={row.ids.includes("panels") ? demo.trailing : undefined} onCommand={demo.onCommand} /></div></div>
</StorySection>
))}
</div>
</StoryPage>
)
},
}
export const States: Story = {
name: "Item States",
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
tags: ["verification"],
render: () => {
const [log, setLog] = useState<string[]>([])
const record = (line: string) => setLog(lines => [...lines.slice(-7), line])
const groups: SWViewerHeaderGroup[] = [
{
id: "states", label: "상태 예시", items: [
{ type: "button", id: "default", label: "기본", icon: <FolderOpen /> },
{ type: "toggle", id: "active", label: "Active (on)", icon: <Layers />, pressed: true },
{ type: "toggle", id: "inactive", label: "Toggle (off)", icon: <Layers />, pressed: false },
{ type: "button", id: "disabled", label: "Disabled", icon: <Scan />, disabled: true },
{ type: "menu", id: "menu", label: "Dropdown", entries: [{ type: "item", id: "a", label: "항목 A" }, { type: "item", id: "b", label: "항목 B (disabled)", disabled: true }] },
{ type: "menu", id: "menu-disabled", label: "Dropdown (disabled)", disabled: true, entries: [] },
],
},
{
id: "field", label: "field 면", surface: "field", items: [
{ type: "tabs", id: "tabs", label: "탭", value: "model", options: [{ value: "model", label: "Model", count: 17377 }, { value: "layout", label: "Layout", count: 237 }, { value: "off", label: "Disabled", disabled: true }] },
{ type: "switch", id: "on", label: "Switch on", checked: true },
{ type: "switch", id: "off", label: "Switch off", checked: false },
{ type: "switch", id: "sw-disabled", label: "Disabled", checked: false, disabled: true },
],
},
]
return (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — Active / Hover / Disabled" />
<Log lines={log} />
<div className="sw-story-scroller"><div style={{ width: 1440 }}>
<SWViewerHeader documentTitle="상태 확인용.dwg" groups={groups} onCommand={command => record(`${command.id}${command.value !== undefined ? `=${String(command.value)}` : ""}`)} />
</div></div>
</StoryPage>
)
},
}
const MENU_ENTRIES: SWViewerMenuEntry[] = [
{ type: "label", id: "l-file", label: "파일" },
{ type: "item", id: "open", label: "파일 열기", icon: <FolderOpen size={16} />, shortcut: "Ctrl+O" },
{
type: "submenu", id: "recent", label: "최근 도면", icon: <FileText size={16} />, entries: [
...SAMPLES.map(sample => ({ type: "item" as const, id: `recent:${sample.id}`, label: sample.name })),
{ type: "separator", id: "s-recent" },
{ type: "submenu", id: "recent-more", label: "보관 도면", entries: [{ type: "item", id: "archive:2025", label: "2025-평면도.dwg" }, { type: "item", id: "archive:2024", label: "2024-종단면도.dwg" }] },
],
},
{ type: "item", id: "export", label: "PNG로 내보내기", icon: <Download size={16} /> },
{ type: "item", id: "print", label: "인쇄 (미연결)", icon: <Printer size={16} />, disabled: true },
{ type: "separator", id: "s1" },
{ type: "label", id: "l-view", label: "화면 보기" },
{ type: "checkbox", id: "grid", label: "그리드", checked: true },
{ type: "checkbox", id: "lineweight", label: "선가중치 표시", checked: false },
{ type: "separator", id: "s2" },
{ type: "label", id: "l-unit", label: "단위" },
{ type: "radio", id: "unit:mm", label: "mm", checked: true },
{ type: "radio", id: "unit:m", label: "m", checked: false },
]
export const Dropdowns: Story = {
name: "Header + Dropdown",
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
tags: ["verification"],
render: () => {
const [entries, setEntries] = useState(MENU_ENTRIES)
const [log, setLog] = useState<string[]>([])
const record = (line: string) => setLog(lines => [...lines.slice(-7), line])
const apply = (entry: SWViewerMenuEntry) => setEntries(list => list.map(item => {
if (entry.type === "checkbox" && item.id === entry.id && item.type === "checkbox") return { ...item, checked: !item.checked }
if (entry.type === "radio" && item.type === "radio" && item.id.startsWith("unit:")) return { ...item, checked: item.id === entry.id }
return item
}))
const groups: SWViewerHeaderGroup[] = [
{ id: "file", label: "파일", items: [{ type: "menu", id: "file", label: "도면", icon: <FileText />, entries: entries.map(entry => ({ ...entry, ...(entry.type === "checkbox" || entry.type === "radio" ? { keepOpen: false } : {}) })) }] },
{ id: "tools", label: "도구", items: [
{ type: "menu", id: "measure", label: "측정", icon: <Ruler />, entries: [{ type: "item", id: "distance", label: "거리" }, { type: "item", id: "area", label: "면적" }, { type: "submenu", id: "angle", label: "각도", entries: [{ type: "item", id: "angle:2", label: "두 선 사이" }, { type: "item", id: "angle:3", label: "세 점" }] }] },
{ type: "menu", id: "align-end", label: "오른쪽 정렬 메뉴", menuAlign: "end", menuWidth: 220, entries: [{ type: "item", id: "copy", label: "좌표 복사", icon: <Copy size={16} /> }, { type: "item", id: "find", label: "객체 찾기", icon: <Search size={16} /> }] },
] },
]
return (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — 상단 메뉴 + Dropdown" />
<Log lines={log} />
<div className="sw-story-scroller sw-viewer-story-room"><div style={{ width: 1440 }}>
<SWViewerHeader documentTitle="C0060202-001-평면및종단면도.dwg" groups={groups}
onCommand={command => { const entry = findEntry(entries, command.id); if (entry) apply(entry); record(`${command.itemId}/${command.id}${command.value !== undefined ? `=${String(command.value)}` : ""}`) }} />
</div></div>
{showVerificationControls() && <StorySection title="SWViewerMenu 단독 (Context Menu)">
<ContextMenuDemo onSelect={line => record(line)} />
</StorySection>}
</StoryPage>
)
},
}
function findEntry(entries: SWViewerMenuEntry[], id: string): SWViewerMenuEntry | undefined {
for (const entry of entries) {
if (entry.id === id) return entry
if (entry.type === "submenu") { const found = findEntry(entry.entries, id); if (found) return found }
}
return undefined
}
function ContextMenuDemo({ onSelect }: { onSelect: (line: string) => void }) {
const [point, setPoint] = useState<{ x: number; y: number } | null>(null)
return (
<div className="sw-viewer-story-context" onContextMenu={event => { event.preventDefault(); setPoint({ x: event.clientX, y: event.clientY }) }} data-context-area="">
도면 영역 (오른쪽 클릭)
<SWViewerMenu open={point !== null} onOpenChange={open => { if (!open) setPoint(null) }} anchorPoint={point ?? undefined} label="도면 메뉴" initialFocus="menu"
entries={[
{ type: "item", id: "fit", label: "전체 보기", shortcut: "F" },
{ type: "item", id: "zoom-selection", label: "선택 확대" },
{ type: "separator", id: "s" },
{ type: "submenu", id: "layer", label: "레이어", entries: LAYERS.slice(0, 4).map(layer => ({ type: "item" as const, id: `hide:${layer.name}`, label: `${layer.name} 숨기기` })) },
{ type: "item", id: "props", label: "속성 보기" },
]}
onEntrySelect={entry => onSelect(`context/${entry.id}`)} />
</div>
)
}
export const SettingsPanel: Story = {
name: "Settings Popup",
// 2026-10-21: merged into the Settings Popup Story (content-variants); hidden, ID kept for the existing checks.
tags: ["verification"],
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["options"] })
return (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — 설정창 (SWViewerPanel)" />
<Log lines={demo.log} />
<ViewerFrame header={<SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
<OptionsPanel demo={demo} strategy="absolute" footer />
</ViewerFrame>
</StoryPage>
)
},
}
export const ContentVariants: Story = {
// 2026-10-21: the one settings-popup Story (former 설정창 and 같은 프레임, 다른 내부 콘텐츠 pages); ID kept.
name: "Settings Popup",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["layers", "properties", "options"] })
const [inputOpen, setInputOpen] = useState(true)
const [scale, setScale] = useState("1:500")
const [snap, setSnap] = useState(true)
return (
<StoryPage>
<StoryPageHeader title="SW Viewer — 설정창 (SWViewerPanel)" />
<Log lines={demo.log} />
<ViewerFrame height={760} header={<SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
<LayerPanel demo={demo} strategy="absolute" defaultPosition={{ left: 24, top: 16 }} />
<PropertiesPanel demo={demo} strategy="absolute" defaultPosition={{ left: 368, top: 16 }} />
<SWViewerPanel strategy="absolute" defaultPosition={{ left: 368, top: 452 }} title="축척 지정" open={inputOpen} onClose={() => setInputOpen(false)} width={300} height="auto" resizable={false}
footer={<><SWViewerButton onClick={() => setInputOpen(false)}>취소</SWViewerButton><SWViewerButton variant="primary" onClick={() => { demo.record(`scale=${scale}, snap=${snap}`); setInputOpen(false) }}>적용</SWViewerButton></>}>
<SWViewerSection title="입력">
<SWViewerField label="축척">{id => <SWViewerInput id={id} value={scale} onChange={event => setScale(event.currentTarget.value)} style={{ width: 120 }} />}</SWViewerField>
<SWViewerField label="스냅 사용">{id => <SWViewerSwitch id={id} checked={snap} onCheckedChange={setSnap} />}</SWViewerField>
<SWViewerField label="단위">{id => <SWSelect id={id} defaultValue="mm" options={[{ value: "mm" }, { value: "m" }]} onValueChange={value => demo.record(`unit=${value}`)} data-sw-select="unit" />}</SWViewerField>
</SWViewerSection>
</SWViewerPanel>
<OptionsPanel demo={demo} strategy="absolute" defaultPosition={{ left: 712, top: 16 }} footer />
{!inputOpen && <button type="button" className="sw-viewer-story-reopen" onClick={() => setInputOpen(true)}>축척 지정 다시 열기</button>}
</ViewerFrame>
</StoryPage>
)
},
}
export const VariableSize: Story = {
name: "Variable Size Popups",
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
tags: ["verification"],
render: () => {
const sizes: { id: string; title: string; width: number; height: number | "auto"; left: number; note: string }[] = [
{ id: "s", title: "250 × auto (최소 폭)", width: 250, height: "auto", left: 16, note: "내용 높이" },
{ id: "m", title: "320 × 420 (기본)", width: 320, height: 420, left: 282, note: "본문 스크롤" },
{ id: "l", title: "480 × 300 (가로형)", width: 480, height: 300, left: 618, note: "resize: both" },
]
const [open, setOpen] = useState<Record<string, boolean>>({ s: true, m: true, l: true })
const lines = Array.from({ length: 24 }, (_, index) => `행 ${index + 1}`)
return (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — 가변 크기 팝업" />
<ViewerFrame height={520} header={<SWViewerHeader documentTitle="크기 비교.dwg" groups={[]} />}>
{sizes.map(size => (
<SWViewerPanel key={size.id} strategy="absolute" defaultPosition={{ left: size.left, top: 16 }} title={size.title} open={open[size.id]} onClose={() => setOpen({ ...open, [size.id]: false })} width={size.width} height={size.height}>
<SWViewerHint>{size.note}</SWViewerHint>
<SWViewerSection surface="plain">{(size.height === "auto" ? lines.slice(0, 3) : lines).map(line => <div key={line}>{line}</div>)}</SWViewerSection>
</SWViewerPanel>
))}
</ViewerFrame>
</StoryPage>
)
},
}
export const Responsive: Story = {
name: "Responsive More",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
return (
<StoryPage>
<StoryPageHeader title="SW Viewer — 화면 너비" />
<Log lines={demo.log} />
<div className="sw-story-stack">
{[1920, 1440, 1280, 950, 860, 760, 520].map(width => (
<StorySection key={width} title={`${width}px`}>
<div className="sw-story-scroller sw-viewer-story-menu-row"><div style={{ width }} data-width={width}><SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} /></div></div>
</StorySection>
))}
</div>
</StoryPage>
)
},
}
function ModeSwitchDemo() {
const [mode, setMode] = useState<"normal" | "viewer">("normal")
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
const headerRef = useRef<HTMLElement>(null)
return (
<>
<div className="sw-viewer-story-mode" role="radiogroup" aria-label="모드">
{(["normal", "viewer"] as const).map(value => (
<label key={value}><input type="radio" name="sw-mode" checked={mode === value} onChange={() => setMode(value)} /> {value === "normal" ? "일반 SW 모드 (SWHeader)" : "뷰어 모드 (SWViewerHeader)"}</label>
))}
</div>
<Log lines={demo.log} />
<ViewerFrame height={480} header={mode === "normal"
? <SWHeader ref={headerRef} documentTitle={demo.documentName ?? ""} logoSrc={placeholderLogo} logoAlt="임시 로고 (뷰어 모드로)" onHomeClick={() => setMode("viewer")} />
: <SWViewerHeader ref={headerRef} documentTitle={demo.documentName} documents={demo.fileProps.documents} activeDocumentId={demo.fileProps.activeDocumentId} onDocumentSelect={demo.fileProps.onDocumentSelect} onDocumentOpen={demo.fileProps.onDocumentOpen} documentOpenLabel={demo.fileProps.documentOpenLabel} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} logoSrc={placeholderLogo} logoAlt="임시 로고 (일반 모드로)" onHomeClick={() => setMode("normal")} />}>
{mode === "viewer" && <><LayerPanel demo={demo} strategy="absolute" /><OptionsPanel demo={demo} strategy="absolute" inset={{ side: 356 }} /></>}
</ViewerFrame>
</>
)
}
export const CompareWithSWHeader: Story = {
name: "SWHeader vs Viewer Header",
render: () => (
<StoryPage>
<StoryPageHeader title="SW Viewer — 일반 SW Header와 비교" />
<StorySection title="나란히 비교 (1440px)">
<div className="sw-story-stack">
<div className="sw-story-scroller"><div style={{ width: 1440 }}><SWHeader documentTitle="C0060202-001-평면및종단면도" /></div></div>
<div className="sw-story-scroller"><div style={{ width: 1440 }}><ViewerOnly /></div></div>
</div>
</StorySection>
<StorySection title="일반 ↔ 뷰어 모드 전환">
<ModeSwitchDemo />
</StorySection>
</StoryPage>
),
}
export const Logo: Story = {
name: "Logo",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
const rows: { title: string; props: { logoSrc?: string; logo?: ReactNode; logoAlt?: string } }[] = [
{ title: "로고 없음 (기본)", props: {} },
{ title: "logoSrc (이미지 URL)", props: { logoSrc: placeholderLogo, logoAlt: "임시 로고" } },
{ title: "logo (요소)", props: { logo: <svg viewBox="0 0 32 17" width="32" height="17" aria-hidden="true"><rect x="0.5" y="0.5" width="31" height="16" rx="3" fill="none" stroke="#fff" /><text x="16" y="12.5" fill="#fff" fontSize="10" fontWeight="700" textAnchor="middle">LOGO</text></svg>, logoAlt: "예시 로고" } },
]
return (
<StoryPage>
<StoryPageHeader title="SW Viewer — 로고" />
<div className="sw-story-stack">
{rows.map(row => (
<StorySection key={row.title} title={row.title}>
<div className="sw-story-scroller"><div style={{ width: 1440 }} data-logo-case={row.title}>
<SWViewerHeader {...row.props} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
</div></div>
</StorySection>
))}
<Log lines={demo.log} />
</div>
</StoryPage>
)
},
}
function ViewerOnly() {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
return <SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
}