- Add StoryNotice and a withSWNotice decorator so every visible SW Story shows the common notice above its title; non-SW Stories are unaffected. - Align the SW Header overview with the common SW Story page layout, let the preview Stage fill the available width and keep inner scroll on narrow screens. - Rename SW/Footer to SW/하단 명령어바 (sidebar title only, Story IDs fixed via id: "sw-footer"), drop the top SWHeader from its preview and lower the workspace height bounds by the removed 49px.
418 lines
23 KiB
TypeScript
418 lines
23 KiB
TypeScript
import { useRef, useState, type ReactNode } from "react"
|
||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||
import { withSWNotice } from "./sw-story-notice"
|
||
import {
|
||
SWHeader, SWViewerButton, SWViewerHeader, SWViewerHint, SWViewerMenu, SWViewerPanel, SWViewerSection,
|
||
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, VIEWER_STORY_LOGO, 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 } },
|
||
decorators: [withSWNotice],
|
||
} 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 | null; height?: number; header: ReactNode; children?: ReactNode }) {
|
||
return (
|
||
<div className="sw-story-scroller">
|
||
{/* Fills the Story column up to `width` (null: the whole column), so panels placed at the right edge stay visible. */}
|
||
<div className="sw-viewer-story-frame" style={{ width: "100%", maxWidth: width ?? undefined, 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>
|
||
)
|
||
}
|
||
|
||
// 2026-10-22 (user request): the Viewer Header screens have no 1440px maximum; they fill the Story column.
|
||
function ViewerScreen({ width = null as number | null, initialDocument = SAMPLES[1].name as string | null, open = [] as ("layers" | "options" | "properties")[] }) {
|
||
const demo = useViewerDemo({ initialDocument, initialPanels: open })
|
||
return (
|
||
<>
|
||
<Log lines={demo.log} />
|
||
<ViewerFrame width={width} header={<SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.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>
|
||
{/* The minimize / close example was removed from this Story; the component keeps supporting it
|
||
(SWHeaderWindowControls at the right end of `trailing`, VIEWER.md §2.1). */}
|
||
<StoryPageHeader title="SW Viewer Header" />
|
||
<StorySection title="기본형">
|
||
<ViewerScreen />
|
||
</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 {...VIEWER_STORY_LOGO} 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: [] },
|
||
],
|
||
},
|
||
// 2026-10-23: the Model / Layout tabs get a field group of their own, as in the viewer demo, so the Story shows
|
||
// their width rule (equal buttons, the field face up to 378px with room).
|
||
{
|
||
id: "tabs", label: "탭", 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 }] },
|
||
],
|
||
},
|
||
{
|
||
id: "field", label: "field 면", surface: "field", items: [
|
||
{ 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 {...VIEWER_STORY_LOGO} 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 {...VIEWER_STORY_LOGO} 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 {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||
<OptionsPanel demo={demo} strategy="absolute" />
|
||
</ViewerFrame>
|
||
</StoryPage>
|
||
)
|
||
},
|
||
}
|
||
|
||
export const ContentVariants: Story = {
|
||
// 2026-10-21: the one settings-popup Story (former 설정창 and 같은 프레임, 다른 내부 콘텐츠 pages); ID kept.
|
||
// 2026-10-22 (user request): the 축척 지정 demo popup was removed.
|
||
name: "설정창",
|
||
render: () => {
|
||
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["layers", "properties", "options"] })
|
||
return (
|
||
<StoryPage>
|
||
<StoryPageHeader title="SW Viewer — 설정창 (SWViewerPanel)" />
|
||
<Log lines={demo.log} />
|
||
{/* 2026-10-22: 620px fits the tallest popup (옵션, 520px) now that 축척 지정 is gone. */}
|
||
<ViewerFrame height={620} header={<SWViewerHeader {...VIEWER_STORY_LOGO} 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 }} />
|
||
<OptionsPanel demo={demo} strategy="absolute" defaultPosition={{ left: 712, top: 16 }} />
|
||
</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 {...VIEWER_STORY_LOGO} 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 Logo: Story = {
|
||
name: "로고",
|
||
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}>
|
||
{/* 1440px is a maximum (2026-10-22): no scroller; the header fits the available width. */}
|
||
<div style={{ width: "100%", maxWidth: 1440 }} data-logo-case={row.title}>
|
||
<SWViewerHeader {...row.props} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
|
||
</div>
|
||
</StorySection>
|
||
))}
|
||
<Log lines={demo.log} />
|
||
</div>
|
||
</StoryPage>
|
||
)
|
||
},
|
||
}
|
||
|
||
export const Responsive: Story = {
|
||
name: "화면 너비",
|
||
render: () => {
|
||
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
|
||
return (
|
||
<StoryPage>
|
||
<StoryPageHeader title="SW Viewer — 화면 너비" />
|
||
<Log lines={demo.log} />
|
||
<div className="sw-story-stack sw-story-responsive">
|
||
{[1920, 1440, 1280, 950, 860, 760, 520].map(width => (
|
||
// 2026-10-22 (user request): the 1920 row no longer reaches out of the content area (no sw-story-responsive-wide);
|
||
// like the other rows it is a maximum inside the Story column. 2026-10-23: it is hidden on the same narrow pages
|
||
// as the SW Header / Footer 1920 rows (sw-story-responsive-hidden, sw-stories.css), e.g. a 1920px screen.
|
||
<div key={width} className={width === 1920 ? "sw-story-responsive-hidden" : undefined}><StorySection title={`${width}px`}>
|
||
{/* The row width is a maximum (2026-10-22): on a narrower screen the header shrinks with it instead of scrolling. */}
|
||
{/* Same structure as the Logo page (no scroller / bottom padding), so the spacing matches it. */}
|
||
<div style={{ width: "100%", maxWidth: width }} data-width={width}><SWViewerHeader {...VIEWER_STORY_LOGO} documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} /></div>
|
||
</StorySection></div>
|
||
))}
|
||
</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>
|
||
</>
|
||
)
|
||
}
|
||
|
||
// 2026-10-22 (user request): removed from the sidebar; ID and direct URL kept for the viewer browser tests.
|
||
export const CompareWithSWHeader: Story = {
|
||
name: "SWHeader vs Viewer Header",
|
||
tags: ["verification"],
|
||
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 logoSrc={placeholderLogo} logoAlt="임시 로고" documentTitle="C0060202-001-평면및종단면도" /></div></div>
|
||
<div className="sw-story-scroller"><div style={{ width: 1440 }}><ViewerOnly /></div></div>
|
||
</div>
|
||
</StorySection>
|
||
<StorySection title="일반 ↔ 뷰어 모드 전환">
|
||
<ModeSwitchDemo />
|
||
</StorySection>
|
||
</StoryPage>
|
||
),
|
||
}
|
||
|
||
function ViewerOnly() {
|
||
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
|
||
// 2026-10-23: the same placeholder logo as the SWHeader row, so the two logo slots compare at the same width.
|
||
return <SWViewerHeader logoSrc={placeholderLogo} logoAlt="임시 로고" documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
|
||
}
|