Files
HMDesign/src/sw/sw-viewer.stories.tsx
T
shkim4 68fbcf6e15 feat(sw): viewer follow-up — SVG icon outline, shared SW popup frame, story grid, green layer buttons
- A: SWViewerIcon draws the SWHeader black outline as SVG stroke on a copy behind the icon (no CSS filter)
- B: SWViewerPanel renders the common SWPopup frame (same DOM / CSS) and adds drag, corner resize,
  collapse, bounds and modal; SWPopup gains optional titleActions / footer; title art spans any width
- C: shared Story canvas grid (--sw-story-canvas) behind the SW Header work area and dropdown room
- D: SWViewerButton tone="green" for 전체 켜기 / 전체 끄기: default kept, hover = primary (확인) hover
  token, pressed = primary face
- AI guide, verification scripts (76 checks, element regression) and evidence updated
2026-10-19 16:34:50 +09:00

401 lines
25 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, 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 { 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>
function Log({ lines }: { lines: string[] }) {
return <p className="sw-story-log" aria-live="polite" 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")[] }) {
const demo = useViewerDemo({ initialDocument, initialPanels: open })
return (
<>
<Log lines={demo.log} />
<ViewerFrame width={width} header={<SWViewerHeader documentTitle={demo.documentName} 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>
<StoryPageHeader kicker={KICKER} title="SW Viewer — SWViewerHeader"
description="참고 DWG/DXF 뷰어의 상단 메뉴를 패키지 컴포넌트로 옮긴 뷰어 모드 전용 Header입니다. 로고 면 → 파일명 면 → 객체 수정 면(메뉴 그룹) → 오른쪽 패널·상태·정보 영역이며, 메뉴는 groups 데이터로 구성합니다. 레이어·설정·속성 토글은 팝업을 열고, 정보 버튼은 모달 팝업을 엽니다." />
<StorySection title="기본형 (1440px, 도면 열림)" description="샘플▾ Dropdown, Model/Layout 탭, 2.5D·어두운 배경 스위치, 패널 토글을 직접 눌러 보세요. Command 줄에 onCommand 결과가 표시됩니다.">
<ViewerScreen />
</StorySection>
</StoryPage>
),
}
export const MenuGroups: Story = {
name: "Menu Groups",
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 — 메뉴 그룹" description="그룹은 groups 배열의 순서대로 놓입니다. surface=&quot;field&quot;는 사용자 확정 속성 입력 면(어두운 inset), placement=&quot;end&quot;는 오른쪽 끝 영역입니다. hidden으로 그룹·항목을 제거합니다." />
<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} 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",
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"
description="Hover: #266651 면, Active·열림·선택 탭: 글자와 아이콘 #FFC600(선택 박스 없음), Disabled: 60% 불투명, Focus: 2px #FFC600 외곽선(키보드). Hover와 Focus는 마우스·Tab으로 확인합니다." />
<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",
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"
description="type=&quot;menu&quot; 항목의 entries가 Dropdown이 됩니다. 일반 항목·체크·라디오·라벨·구분선·다단계 서브메뉴(submenu)를 지원합니다. 키보드: Enter/↓ 열기, ↑↓ 이동, → 서브메뉴, ← 닫기, Esc 닫고 버튼으로 복귀, 바깥 클릭 닫기." />
<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>
<StorySection title="SWViewerMenu 단독 (Context Menu)" description="anchorPoint로 지정 위치에 엽니다. 아래 영역을 마우스 오른쪽 버튼으로 클릭하세요.">
<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",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["options"] })
return (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — 설정창 (SWViewerPanel)"
description="설정 토글로 여는 옵션 팝업입니다. 제목 막대를 끌어 이동, 오른쪽 아래 모서리로 크기 조절, 제목 더블클릭·접기 버튼으로 접기, 닫기·Esc(팝업 안 Focus)로 닫습니다. 하단 버튼 영역(footer)을 포함한 예시입니다." />
<Log lines={demo.log} />
<ViewerFrame header={<SWViewerHeader documentTitle={demo.documentName} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
<OptionsPanel demo={demo} strategy="absolute" footer />
</ViewerFrame>
</StoryPage>
)
},
}
export const ContentVariants: Story = {
name: "Popup Content Variants",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: ["layers", "properties"] })
const [inputOpen, setInputOpen] = useState(true)
const [scale, setScale] = useState("1:500")
const [snap, setSnap] = useState(true)
return (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — 같은 프레임, 다른 내부 콘텐츠"
description="SWViewerPanel의 외형(제목 막대·닫기·테두리·스크롤 본문)은 같고 children / footer만 다릅니다: 검색 + 목록(레이어), 읽기 전용 정의 목록(속성), 입력 폼 + 하단 버튼(입력 팝업)." />
<Log lines={demo.log} />
<ViewerFrame height={640} header={<SWViewerHeader documentTitle={demo.documentName} 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: 712, top: 16 }} 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 => <select id={id} className="sw-viewer-story-select" defaultValue="mm"><option value="mm">mm</option><option value="m">m</option></select>}</SWViewerField>
</SWViewerSection>
</SWViewerPanel>
{!inputOpen && <button type="button" className="sw-viewer-story-reopen" onClick={() => setInputOpen(true)}>축척 지정 다시 열기</button>}
</ViewerFrame>
</StoryPage>
)
},
}
export const VariableSize: Story = {
name: "Variable Size Popups",
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 — 가변 크기 팝업"
description="width / height(&quot;auto&quot; 가능) / minWidth / minHeight로 메뉴별 크기를 정합니다. 최대 90vw × 90vh, 오른쪽 아래 모서리로 크기 조절, 화면(또는 strategy=&quot;absolute&quot;의 부모) 경계 안에서만 이동합니다." />
<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 Widths",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
return (
<StoryPage>
<StoryPageHeader kicker={KICKER} title="SW Viewer — 화면 너비"
description="참고 뷰어와 같은 기준입니다. Header 폭 1180px 미만: 샘플·도면 공간·2.5D·어두운 배경이 더보기(⋯)로 이동, 900px 미만: 패널 토글도 이동, 560px 미만: 파일 열기·전체 보기는 아이콘만, 상태 글자 숨김. 가로 스크롤은 만들지 않습니다." />
<Log lines={demo.log} />
<div className="sw-story-stack">
{[1440, 1100, 860, 520].map(width => (
<StorySection key={width} title={`${width}px`}>
<div className="sw-story-scroller"><div style={{ width }} data-width={width}><SWViewerHeader documentTitle={demo.documentName} 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} 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 kicker={KICKER} title="SW Viewer — 일반 SW Header와 비교"
description="일반 SW 모드는 기존 SWHeader(49px, Figma 26:8944)를 그대로 쓰고, 뷰어 모드만 SWViewerHeader(52px, 참고 뷰어)를 씁니다. 두 Header는 파일명·객체 수정·속성 면 토큰을 공유합니다. 아래 전환 예시는 화면 상태(파일명·패널 상태)를 유지한 채 Header만 바꿉니다. 예시에서는 SWHeader 로고(임시)를 누르면 뷰어 모드, 뷰어 로고(임시)를 누르면 일반 모드로 돌아갑니다." />
<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="일반 ↔ 뷰어 모드 전환" description="Header 교체만 보여 주는 UI 예시입니다. 실제 모드 진입·종료와 데이터 처리는 SW 프로그램 담당입니다.">
<ModeSwitchDemo />
</StorySection>
</StoryPage>
),
}
export const Logo: Story = {
name: "Logo",
render: () => {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
const rows: { title: string; description: string; props: { logoSrc?: string; logo?: ReactNode; logoAlt?: string } }[] = [
{ title: "로고 없음 (기본)", description: "logo / logoSrc를 넘기지 않으면 아무것도 그리지 않습니다(HM 문자·기본 아이콘 없음). 로고 면은 사선 끝(24px)만 남아 넓은 빈 공간이 생기지 않습니다.", props: {} },
{ title: "logoSrc (이미지 URL)", description: "SWHeader와 같은 Prop. 로고 면 72px(로고가 더 넓으면 그만큼), 높이 최대 24px.", props: { logoSrc: placeholderLogo, logoAlt: "임시 로고" } },
{ title: "logo (요소)", description: "logo가 logoSrc보다 우선합니다.", 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 kicker={KICKER} title="SW Viewer — 로고" description="로고는 각 프로그램이 정합니다. 제공된 경우에만 표시하고, Header 높이(52px)와 메뉴 정렬은 그대로입니다. SWHeader의 로고 동작은 바뀌지 않습니다." />
<div className="sw-story-stack">
{rows.map(row => (
<StorySection key={row.title} title={row.title} description={row.description}>
<div className="sw-story-scroller"><div style={{ width: 1440 }} data-logo-case={row.title}>
<SWViewerHeader {...row.props} documentTitle={demo.documentName} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
</div></div>
</StorySection>
))}
</div>
</StoryPage>
)
},
}
function ViewerOnly() {
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
return <SWViewerHeader documentTitle={demo.documentName} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
}