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
420 lines
23 KiB
TypeScript
420 lines
23 KiB
TypeScript
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} />
|
||
}
|