feat(sw): viewer header logo menu and file list; simplify SW Storybook
SW Viewer Header (approved optional props, public export names unchanged at 170): - SWViewerHeader: logoMenu, documents, activeDocumentId, onDocumentSelect, onDocumentOpen, documentOpenLabel; measured responsive toggles (full -> icons -> More) with the SWHeader More / file-list styling. - SW AI guides, ExampleH, viewer/text-weight/theme-inheritance tests and the logo-document-menu tests with their QA record. SW Storybook cleanup (Story IDs and test fixtures kept): - Sidebar: Overview -> "SW Header" (first), 전체 Footer -> "SW Footer", Footer/Viewer responsive pages -> "Responsive More", Viewer content-variants -> "Settings Popup" (merges the settings popup page). - Hidden by the verification tag (direct URL kept): Header Dropdown, Document Open, Interactive States, Document Dropdown, Hidden Sub Controls, Home Menu standalone; Footer tool groups / custom content / parts; SW/Theme; Viewer menu groups / item states / variable size / settings panel / Header + Dropdown. - Removed demo UI: Overview size and callback output (demo data and selection only with &verification), Home Menu Figma node table and width demo, Header Responsive More reduced to 1440 / 768 / 390, Popup reopen button, property-panel comparison captions, Viewer context-menu demo (&verification only). - Tests: storybook-inventory records the hidden and renamed Stories; overview / range-cross-browser / viewer open the demo controls with &verification; integrated-popup reloads the Story instead of the reopen button; dropdown-consolidation asserts Dropdown is hidden from the sidebar and still opens by its direct URL.
This commit is contained in:
1 parent
46119eacb5
commit
23d522c136
57 files changed
+2623
-333
No files matched your search
@@ -78,10 +78,10 @@ function Workspace({ width = "100%" }: { width?: number | string }) {
|
||||
}
|
||||
|
||||
export const FullFooter: Story = {
|
||||
name: "전체 Footer · 실제 조합",
|
||||
name: "SW Footer",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="전체 Footer · 실제 Footer 조합" description="사용자 제공 실제 화면(2026-10-20) 구성: SWHeader + 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." />
|
||||
<StoryPageHeader kicker={KICKER} title="SW Footer"description="사용자 제공 실제 화면(2026-10-20) 구성: SWHeader + 도면 영역 + SWFooter(좌표 status, 바 안의 명령창 command, 오른쪽 명령 칸, Layout 탭). 모든 상태는 호스트(Story)가 Props로 소유하고, 컴포넌트는 Callback만 보냅니다." />
|
||||
<StorySection title="실제 Footer 조합 (동작)" description="명령 클릭 = 선택(다시 클릭 해제)·명령 기록 추가, 분류 탭(←/→, 바꿔도 칸 영역 고정), 빈 칸 + 클릭·Hover 가이드(영상 대신 Figma 6프레임 순환), 보조기능 토글(켜짐 = 주황 테두리·글자), 명령어 전체보기·▲ = 명령창 위로 펼치기, Layout 탭 선택·+·×·더블클릭/F2 이름 변경·Delete, 명령창 Enter 실행. 좁은 폭의 ▲ 확장 목록은 「화면 폭 변화」, Hover · Active · Selected · Disabled는 이 화면의 각 부품에서 동작합니다.">
|
||||
<Workspace width={1920} />
|
||||
</StorySection>
|
||||
@@ -93,6 +93,8 @@ export const FullFooter: Story = {
|
||||
|
||||
export const ToolGroups: Story = {
|
||||
name: "도구 그룹별 표시",
|
||||
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
|
||||
tags: ["verification"],
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="도구 그룹별 표시" description="분류(categoryId)마다 다른 명령. 명령 뒤의 칸은 빈 칸(+)이고, 마지막 명령 뒤부터 bg_vacant(어두운 면)입니다. 1440px." />
|
||||
@@ -350,6 +352,8 @@ function CustomFooter() {
|
||||
|
||||
export const CustomContent: Story = {
|
||||
name: "도구 및 메뉴 변경 예제",
|
||||
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
|
||||
tags: ["verification"],
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="도구 및 메뉴 변경" description="공통 디자인(크기·배경·상태)은 그대로, 내용만 데이터로 변경: 분류·명령 텍스트·아이콘(lucide), slotCount 10, 빈 칸 클릭으로 명령 추가, 분류 추가, 보조기능 4개(columns 3 → 2행), 측면 버튼 문구, trailing 좌표, Layout 이름·잠금·Model 문구." />
|
||||
@@ -373,18 +377,17 @@ function ReferenceFooter() {
|
||||
}
|
||||
|
||||
export const Responsive: Story = {
|
||||
name: "화면 폭 변화",
|
||||
name: "Responsive More",
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="화면 폭 변화" description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
|
||||
<StorySection title="실제 조합 (바 안 명령창, command)" description="남는 폭 × 0.57 = 칸 영역, 나머지 = 명령창(최소 280). 1920 → 12칸(명령 10 + 빈 칸 2), 1440 → 명령 7 + ▲, 1280 → 5 + ▲, 950 → 4 + ▲·보조기능 아이콘만, 860 · 760 → ▲ 패널 여러 줄(왼쪽부터 채움). 각 폭에서 분류 탭을 눌러도 영역이 그대로입니다.">
|
||||
{[1920, 1440, 1280, 950, 860, 760].map(width => (
|
||||
<div key={width} className="sw-footer-story-width">
|
||||
<p className="sw-story-log">{width}px</p>
|
||||
<StoryPageHeader kicker={KICKER} title="Responsive More" description="칸 영역은 바 폭으로만 정해지고 분류의 명령 수와 무관합니다(분류를 바꿔도 위치 고정). Figma에는 좁은 폭 정의가 없어 사용자 제공 실제 화면(2026-10-20)을 따릅니다: 빈 칸(+)이 먼저 줄고, 그래도 넘치는 명령은 마지막 칸의 ▲(ico_arrow)로 칸 위 패널에 펼침(▼ = 열림, Escape·바깥 클릭 닫힘). 분류 탭이 넘치면 ◀ ▶로 스크롤. 바 폭 1279px 이하에서 보조기능은 아이콘만(45 × 33). 바 안 명령창은 최소 280px." />
|
||||
{[1920, 1440, 1280, 950, 860, 760].map(width => (
|
||||
<div key={width} className="sw-footer-story-width">
|
||||
<StorySection title={`${width}px`}>
|
||||
<Screen width={width} canvas={90}><ReferenceFooter /></Screen>
|
||||
</div>
|
||||
))}
|
||||
</StorySection>
|
||||
</StorySection>
|
||||
</div>
|
||||
))}
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
@@ -393,6 +396,8 @@ export const Responsive: Story = {
|
||||
|
||||
export const Parts: Story = {
|
||||
name: "개별 부품 (조합용)",
|
||||
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
|
||||
tags: ["verification"],
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="개별 부품" description="SWFooter 없이 부품만 쓸 때는 <SWTheme> 안에 둡니다(SWFooter·SWFooterCommandLine은 루트에 hm-sw-theme이 있어 필요 없음)." />
|
||||
|
||||
@@ -12,5 +12,7 @@ export default meta
|
||||
|
||||
export const ComposedControls: StoryObj<typeof meta> = {
|
||||
name: "Dropdown",
|
||||
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
|
||||
tags: ["verification"],
|
||||
render: () => <ComposedDemo />,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef, useState, type ReactNode } from "react"
|
||||
import { useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
// Public API only, like a consuming project.
|
||||
import {
|
||||
@@ -10,14 +10,14 @@ import "@hm-design/sw/styles.css"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import "./sw-home-menu.css"
|
||||
import {
|
||||
figmaHomeMenuItemIds, figmaHomeMenuSections, figmaRecentFiles, figmaTileNodes, figmaTitleNodes, withRecentFiles,
|
||||
figmaHomeMenuItemIds, figmaHomeMenuSections, figmaRecentFiles, withRecentFiles,
|
||||
} from "./sw-home-menu-fixture"
|
||||
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
|
||||
import figmaPropertyBar from "./assets/home-menu/figma-26-10492.png"
|
||||
|
||||
// Owner: docs/sw/figma-additional/analysis-report.md (Figma rj6wpsfk24lOWvWiVRAANP 26:10591 / 662:67267).
|
||||
// 2026-10-19: the former SW/Home Menu Stories live under SW/Header (SW normal mode only; SW/Viewer has no logo menu).
|
||||
// "Home Menu" and "Home Menu · 단독 사용 · 조합형" are the sidebar Stories; the other Stories keep their checks reachable
|
||||
// "Home Menu" is the sidebar Story (단독 사용 hidden 2026-10-21); the other Stories keep their checks reachable
|
||||
// by URL (verification tag). 2026-10-20: Figma screenshot comparisons that no test or document used were removed;
|
||||
// the Figma originals stay in docs/sw/figma-additional/screenshots/.
|
||||
const KICKER = "HM DESIGN SYSTEM / SW"
|
||||
@@ -85,42 +85,6 @@ function HeaderWithMenu({ width, defaultOpen = false, defaultActiveIds = [], acc
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Header width set by a range input (Storybook control only): the header measures its own width. The range ends at
|
||||
* the room the page gives (no page scroll), so the browser width and the slider both drive it.
|
||||
*/
|
||||
function ResizableHeaderWithMenu() {
|
||||
const [width, setWidth] = useState(1280)
|
||||
const [room, setRoom] = useState(1920)
|
||||
const observed = useRef<ResizeObserver | null>(null)
|
||||
const measureRef = useCallback((element: HTMLDivElement | null) => {
|
||||
observed.current?.disconnect()
|
||||
if (!element) return
|
||||
observed.current = new ResizeObserver(() => setRoom(Math.floor(element.clientWidth)))
|
||||
observed.current.observe(element)
|
||||
}, [])
|
||||
const shown = Math.min(width, room)
|
||||
return (
|
||||
<>
|
||||
<label className="sw-home-menu-story__width">
|
||||
헤더 폭 <input type="range" min={Math.min(390, room)} max={room} step={1} value={shown} onChange={event => setWidth(Number(event.target.value))} aria-label="헤더 폭" />
|
||||
<output>{shown}px</output>
|
||||
</label>
|
||||
<div ref={measureRef}><HeaderWithMenu width={shown}>{log => log}</HeaderWithMenu></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const tileTable = (
|
||||
<table className="sw-home-menu-story__table">
|
||||
<thead><tr><th>섹션</th><th>제목 노드</th><th>타일 (노드)</th></tr></thead>
|
||||
<tbody>{figmaHomeMenuSections.map(section => (
|
||||
<tr key={section.id}><td>{section.title}</td><td>{figmaTitleNodes[section.id]}</td>
|
||||
<td>{section.items.map(item => `${item.label} (${figmaTileNodes[item.id]})`).join(" · ")}</td></tr>
|
||||
))}</tbody>
|
||||
</table>
|
||||
)
|
||||
|
||||
/** The sidebar Story: the logo menu of SWHeader (SW normal mode) with its Figma states, sub panel and Escape stack. */
|
||||
export const HomeMenu: Story = {
|
||||
name: "Home Menu",
|
||||
@@ -131,16 +95,12 @@ export const HomeMenu: Story = {
|
||||
<StorySection title="로고 클릭 · 열기 / 닫기 (현재 폭)" description="5개 섹션(파일 · 손상된 파일 · Display · 설정 · 궁금하면) 15개 타일. 마우스를 올리면 Hover(Variant2), 누르는 동안 Pressed(Variant3)이며 클릭 후 ON 상태는 남지 않습니다. 최근파일 타일만 하위 패널을 열고, 목록이 열린 동안 Variant3를 유지합니다.">
|
||||
<HeaderWithMenu>{log => log}</HeaderWithMenu>
|
||||
</StorySection>
|
||||
<StorySection title="반응형 · 헤더 폭 조절 · 더보기(⋯)" description="메뉴 위치와 폭은 고정 x가 아니라 Header를 측정해 정합니다(객체 수정 영역 −2px ~ 창 영역). 폭이 부족한 Header에서는 기존 더보기(…) 규칙이 그대로 동작하고, 메뉴는 가로 스크롤 없이 들어가는 섹션만 앞에서부터 보여 주며 나머지는 끝의 더보기(⋯) 섹션으로 옮깁니다. 더보기를 누르면 옮긴 섹션이 텍스트 메뉴(섹션 제목 + 기능 목록, 그룹 사이 구분선)로 아래에 열리고(Hover·Active·클릭은 타일과 같음, 최근파일은 기존 목록을 엶), 폭이 넓어지면 원래 자리로 돌아갑니다. 브라우저 폭이나 슬라이더로 확인합니다(슬라이더 최대 = 현재 페이지 폭).">
|
||||
<ResizableHeaderWithMenu />
|
||||
</StorySection>
|
||||
<StorySection title="열림 (현재 폭)" description="열림 동안 로고는 Home On(노란 테두리), Header 상·하단에 #5F4B0F 2px 선이 표시되고 닫으면 기본으로 돌아갑니다(Figma 26:10591 / 662:67267).">
|
||||
<HeaderWithMenu defaultOpen autoFocus={false} />
|
||||
</StorySection>
|
||||
<StorySection title="Default / Variant2 · Hover / Variant3 · Active" description="Figma 버튼 세트 15개(26:11355 … 26:11631)는 모두 같은 SWHomeMenuItem입니다. Variant2(Hover)는 마우스를 올려 확인하며 Figma에서 Variant3와 시각 속성이 같습니다. Variant3는 activeIds로 지정합니다.">
|
||||
<SWTheme className="sw-home-menu-story__stage"><SWHomeMenu defaultOpen autoFocus={false} sections={figmaHomeMenuSections} closeOnBackgroundClick={false} aria-label="Default" /></SWTheme>
|
||||
<SWTheme className="sw-home-menu-story__stage"><SWHomeMenu defaultOpen autoFocus={false} sections={figmaHomeMenuSections} activeIds={figmaHomeMenuItemIds} closeOnBackgroundClick={false} aria-label="Active" /></SWTheme>
|
||||
{tileTable}
|
||||
</StorySection>
|
||||
<StorySection title="최근파일 하위 패널 (662:67267)" description="최근파일 타일을 누르면(Variant3) 메뉴 하단 강조가 전체 2px 외곽선으로 바뀌고 그 아래에 최근파일 목록이 열립니다. 행에 마우스를 올리면 2뎁스 미리보기가 옆에 나타납니다. 다시 누르거나 Escape로 닫습니다. 최근파일이 더보기 안에 있으면 목록은 더보기 타일 아래에 열리고 더보기 타일이 Variant3가 됩니다. 목록과 미리보기 이미지는 Story 데이터입니다.">
|
||||
<div className="sw-home-menu-story__tall"><HeaderWithMenu defaultOpen autoFocus={false} defaultActiveIds={["recent"]}>{log => log}</HeaderWithMenu></div>
|
||||
@@ -254,6 +214,8 @@ function StandaloneDemo() {
|
||||
|
||||
export const HomeMenuStandalone: Story = {
|
||||
name: "Home Menu · 단독 사용 · 조합형",
|
||||
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
|
||||
tags: ["verification"],
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SWTheme 안에서 단독 사용"
|
||||
|
||||
@@ -116,7 +116,7 @@ function DocumentOpenPage() {
|
||||
)
|
||||
}
|
||||
|
||||
export const DocumentOpen: Story = { name: "Document Open", render: () => <DocumentOpenPage /> }
|
||||
export const DocumentOpen: Story = { name: "Document Open", tags: ["verification"], render: () => <DocumentOpenPage /> }
|
||||
|
||||
function HiddenSubControlsPage() {
|
||||
const [log, record] = useLog()
|
||||
@@ -137,4 +137,4 @@ function HiddenSubControlsPage() {
|
||||
)
|
||||
}
|
||||
|
||||
export const HiddenSubControls: Story = { name: "Hidden Sub Controls", render: () => <HiddenSubControlsPage /> }
|
||||
export const HiddenSubControls: Story = { name: "Hidden Sub Controls", tags: ["verification"], render: () => <HiddenSubControlsPage /> }
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type SWHomeMenuSectionData,
|
||||
} from "@hm-design/sw"
|
||||
import { StoryPage, StoryPageHeader } from "@/components/story-page"
|
||||
import { showVerificationControls } from "./sw-story-flags"
|
||||
|
||||
type Selection = "none" | "all" | "circle" | "expanded"
|
||||
interface OverviewProps {
|
||||
@@ -128,7 +129,7 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi
|
||||
|
||||
return <StoryPage className="sw-story-overview">
|
||||
<StoryPageHeader kicker="HM DESIGN SYSTEM / SW" title="SW Header" />
|
||||
<div className="sw-overview-toolbar" aria-label="Storybook 전용 미리보기 설정">
|
||||
{showVerificationControls() && <div className="sw-overview-toolbar" aria-label="Storybook 전용 미리보기 설정">
|
||||
<label htmlFor={`${controlId}-selection`}>선택 상태</label>
|
||||
<select id={`${controlId}-selection`} value={example} onChange={event => changeExample(event.target.value as Selection)}>
|
||||
<option value="none">선택 없음</option><option value="all">전체 객체</option><option value="circle">원 선택</option><option value="expanded">전체 펼침</option>
|
||||
@@ -144,7 +145,7 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi
|
||||
<label><input type="checkbox" checked={disabled} onChange={event => setDisabled(event.target.checked)} />속성 Disabled</label>
|
||||
<label><input type="checkbox" checked={emptyShape} onChange={event => setEmptyShape(event.target.checked)} />형상 Slot 비우기</label>
|
||||
</div></details>
|
||||
</div>
|
||||
</div>}
|
||||
<div ref={slotRef} className="sw-overview-slot" data-preview-slot="">
|
||||
<div className="sw-overview-stage" data-preview-stage="" role="region" tabIndex={0} aria-label="SW Header 16:9 미리보기, 작은 화면에서는 내부 스크롤"
|
||||
style={{ ...size, "--sw-preview-panel-left": `${panelLeft}px`, "--sw-preview-panel-top": barOpen ? "77px" : "49px" } as CSSProperties}>
|
||||
@@ -196,9 +197,7 @@ export function SWHeaderOverview({ initial, fixtures, propertyGroups, propertyFi
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sw-overview-status">
|
||||
<span data-preview-size="">{Math.round(size.width)} × {Math.round(size.height)} · 16:9 · 내부 스크롤</span>
|
||||
<output data-preview-callback="" aria-live="polite" title={callbackLog.join(" → ")}>{callbackLog.join(" → ") || "Callback 없음"}</output>
|
||||
</div>
|
||||
{/* Callback record for the browser tests only; not shown on the page. */}
|
||||
<output hidden data-preview-callback="">{callbackLog.join(" → ")}</output>
|
||||
</StoryPage>
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type SWHeaderColorMode, type SWHeaderColorOption, type SWHeaderColorValue, type SWHeaderDimensionStyleOption,
|
||||
type SWHeaderDocument, type SWHeaderLayer, type SWHeaderLineStyleValue, type SWHeaderLineType, type SWHeaderLineWeight,
|
||||
type SWHeaderMenuId, type SWHeaderProps, type SWHeaderTextStyleOption, type SWHeaderProperty, type SWHeaderPropertyGroup, type SWHeaderPropertySelection, type SWHeaderPropertyMenus, type SWHeaderPropertyShapeContent,
|
||||
SWRecentFiles, SWToolButton,
|
||||
SWRecentFiles,
|
||||
} from "@hm-design/sw"
|
||||
import "@hm-design/sw/styles.css"
|
||||
import { SWHeaderMenuDetail } from "../../packages/sw/src/components/sw-header/components/SWHeaderDropdown"
|
||||
@@ -185,6 +185,7 @@ function InteractiveHeader(props: Partial<SWHeaderProps>) {
|
||||
const OVERVIEW_HOME_MENU = withRecentFiles(figmaHomeMenuSections, <SWRecentFiles files={figmaRecentFiles} />)
|
||||
|
||||
export const Overview: Story = {
|
||||
name: "SW Header",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <SWHeaderOverview initial={{ ...figmaArgs, ...callbacks }}
|
||||
fixtures={{ documents: SAMPLE_DOCUMENTS, layers: PROPERTY_LAYERS, colors: FIGMA_COLORS, userColors: FIGMA_USER_COLORS,
|
||||
@@ -195,6 +196,8 @@ export const Overview: Story = {
|
||||
|
||||
export const InteractiveStates: Story = {
|
||||
name: "Interactive States",
|
||||
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
|
||||
tags: ["verification"],
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
@@ -554,6 +557,8 @@ function DocumentDemo({ width, ...props }: DocumentHostProps & { width: number }
|
||||
|
||||
export const DocumentDropdown: Story = {
|
||||
name: "Document Dropdown",
|
||||
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
|
||||
tags: ["verification"],
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
@@ -771,79 +776,14 @@ function StyleHeader({ onLog, showState, ...props }: StyleHostProps) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Interactive host with a visible callback log (checks that every action calls one callback once). */
|
||||
function StyleDemo({ width, ...props }: StyleHostProps & { width: number }) {
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
return (
|
||||
<>
|
||||
<p className="sw-story-log" aria-live="polite" data-callback-log="">Callback: {log.length ? log.join(" → ") : "없음"}</p>
|
||||
<MenuFrame width={width}><StyleHeader showState {...props} onLog={line => setLog(lines => [...lines.slice(-11), line])} /></MenuFrame>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const MORE_WIDTHS = [1920, 1800, 1799, 1440, 1180, 1179, 1080, 1079, 900, 899, 768, 640, 639, 520, 519, 390]
|
||||
|
||||
/**
|
||||
* Measured More (2026-10-19): the header width comes from a range input and the host adds content the breakpoints
|
||||
* do not know about (buttons next to the logo, extra tools). Parts move into More only while the real room is
|
||||
* missing and return to their own place, in their own order, once it is back. Range / checkboxes are Storybook controls.
|
||||
*/
|
||||
function MeasuredMoreDemo() {
|
||||
const [width, setWidth] = useState(1920)
|
||||
const [leading, setLeading] = useState(true)
|
||||
const [tools, setTools] = useState(true)
|
||||
const [homeMenu, setHomeMenu] = useState(true)
|
||||
const [barOpen, setBarOpen] = useState(false)
|
||||
const [panelOpen, setPanelOpen] = useState(false)
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const record = (line: string) => setLog(lines => [...lines.slice(-7), line])
|
||||
return (
|
||||
<>
|
||||
<div className="sw-story-measured-controls" role="group" aria-label="실측 More 조건">
|
||||
<label>헤더 폭 <input type="range" min={390} max={1920} step={1} value={width} onChange={event => setWidth(Number(event.target.value))} aria-label="헤더 폭" /> <output data-measured-width="">{width}px</output></label>
|
||||
<label><input type="checkbox" checked={leading} onChange={event => setLeading(event.target.checked)} />로고 오른쪽 버튼(leading)</label>
|
||||
<label><input type="checkbox" checked={tools} onChange={event => setTools(event.target.checked)} />추가 도구(tools)</label>
|
||||
<label><input type="checkbox" checked={homeMenu} onChange={event => setHomeMenu(event.target.checked)} />홈 메뉴(homeMenu)</label>
|
||||
</div>
|
||||
<p className="sw-story-log" aria-live="polite" data-callback-log="">Callback: {log.length ? log.join(" → ") : "없음"}</p>
|
||||
<MenuFrame width={width}>
|
||||
<StyleHeader onLog={record}
|
||||
leading={leading ? <><SWToolButton appearance="document" onClick={() => record("leading: 저장")}>저장</SWToolButton><SWToolButton appearance="document" onClick={() => record("leading: 내보내기")}>내보내기</SWToolButton></> : undefined}
|
||||
tools={tools ? <SWToolButton onClick={() => record("tools: 측정")}>측정</SWToolButton> : undefined}
|
||||
homeMenu={homeMenu ? { sections: OVERVIEW_HOME_MENU, onItemSelect: event => record(`onItemSelect(${event.item.id})`) } : undefined}
|
||||
onHomeMenuOpenChange={(open, reason) => record(`homeMenuOpen(${open}, ${reason})`)}
|
||||
onPrint={() => record("onPrint")} onPopupGuide={() => record("onPopupGuide")}
|
||||
propertyBarOpen={barOpen} onPropertyBarOpenChange={next => { setBarOpen(next); record(`propertyBarOpen(${next})`) }}
|
||||
propertyPanelOpen={panelOpen} onPropertyPanelOpenChange={next => { setPanelOpen(next); record(`propertyPanelOpen(${next})`) }}
|
||||
propertyPanel={{ selection: { id: "none", title: "선택된 객체 없음", kind: "none" } }} />
|
||||
</MenuFrame>
|
||||
</>
|
||||
)
|
||||
}
|
||||
const MORE_WIDTHS = [1440, 768, 390]
|
||||
|
||||
export const ResponsiveMore: Story = {
|
||||
name: "Responsive More",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={KICKER}
|
||||
title="Responsive More — 대표 필드 전환"
|
||||
description="좁은 헤더에서 더보기(…)의 필드 항목을 고르면, 그 필드가 객체 수정 영역의 첫 칸(레이어 필드 자리, 대표 슬롯)에 표시되고 Dropdown이 바로 아래에 열립니다. 레이어는 더보기로 이동해 다시 고를 수 있습니다. 메뉴 전환은 UI 상태일 뿐이며 데이터 Callback은 호출하지 않습니다(필드 클릭과 같은 on*Open 1회만). 기존 Breakpoint(1800 / 1180 / 1080 / 900 / 640 / 520)는 그대로이고, 그 위에 실제 남은 공간을 측정해 부족한 메뉴만 더보기로 옮깁니다."
|
||||
/>
|
||||
<StorySection title="실제 사용 가능한 공간 기준 (폭 조절)" description="고정 폭만 보지 않고 객체 수정 영역에 실제로 남은 공간을 측정합니다. 로고 오른쪽 버튼·추가 도구처럼 Breakpoint가 모르는 요소가 공간을 쓰면, 표시할 수 없는 메뉴가 선·문자·치수스타일 → 그리기모드 → 속성Bar·속성창 → 색상 순(같은 단계는 오른쪽부터)으로 더보기에 들어갑니다. 공간이 다시 생기면 원래 자리·순서로 돌아옵니다. 로고 오른쪽 Open 버튼은 가장 먼저 옮겨지고 가장 늦게 돌아옵니다. Dropdown·홈 메뉴·더보기를 열고 닫아도 Header 요소의 위치와 너비는 바뀌지 않습니다.">
|
||||
<MeasuredMoreDemo />
|
||||
</StorySection>
|
||||
<StorySection title="전환 · Callback 기록 (1440)" description="… → 선스타일: 대표 칸이 선스타일(미리보기·이름·가중치·스케일)로 바뀌고 선스타일 Dropdown이 열립니다. … → 문자스타일: 선스타일 Dropdown이 닫히고 대표 칸이 문자스타일로. … → 레이어: 원래 배치로 복귀. 값은 모두 유지됩니다.">
|
||||
<StyleDemo width={1440} />
|
||||
</StorySection>
|
||||
<StorySection title="전환 · Callback 기록 (768)" description="색상도 더보기에 있는 폭입니다. … → 색상: 색상 필드와 목록.">
|
||||
<StyleDemo width={768} />
|
||||
</StorySection>
|
||||
<StorySection title="전환 · Callback 기록 (390)" description="대표 필드는 남는 폭에 맞춰 값이 말줄임되고, 목록은 헤더 안쪽으로 이동합니다.">
|
||||
<StyleDemo width={390} />
|
||||
</StorySection>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Header — 화면 너비" />
|
||||
{MORE_WIDTHS.map(width => (
|
||||
<StorySection key={width} title={`${width}px`}>
|
||||
<MenuFrame width={width}><StyleHeader /></MenuFrame>
|
||||
@@ -953,11 +893,11 @@ const BAR_FIXTURE: SWHeaderProperty[] = [
|
||||
{ id: "area", name: "면적", value: "51.5962", type: "number", groupId: "shape", min: 0 },
|
||||
]
|
||||
/* Property panel: the four Figma states with the same component and host data. */
|
||||
const PROPERTY_STATES: { example: PropertyExample; title: string; node: string; size: string }[] = [
|
||||
{ example: "none", title: "선택된 객체 없음", node: "26:11886", size: "314 × 256" },
|
||||
{ example: "all", title: "전체 객체 선택", node: "26:11823", size: "314 × 361" },
|
||||
{ example: "circle", title: "원 1개 선택", node: "26:11932", size: "314 × 658" },
|
||||
{ example: "expanded", title: "모든 항목 펼침", node: "26:12035", size: "314 × 754" },
|
||||
const PROPERTY_STATES: { example: PropertyExample; title: string }[] = [
|
||||
{ example: "none", title: "선택된 객체 없음" },
|
||||
{ example: "all", title: "전체 객체 선택" },
|
||||
{ example: "circle", title: "원 1개 선택" },
|
||||
{ example: "expanded", title: "모든 항목 펼침" },
|
||||
]
|
||||
function PropertyState({ example, shapeContent, onLog }: { example: PropertyExample; shapeContent?: SWHeaderPropertyShapeContent; onLog?: (line: string) => void }) {
|
||||
const [properties, setProperties] = useState(() => propertyFixture(example))
|
||||
@@ -971,25 +911,13 @@ function PropertyState({ example, shapeContent, onLog }: { example: PropertyExam
|
||||
/** Design check only: a real header shows one panel for the current selection.
|
||||
2 columns × 2 rows at the panels' own size; the Inner scrolls instead of shrinking or wrapping them. */
|
||||
function PropertyCompare() {
|
||||
const [sizes, setSizes] = useState<Record<string, string>>({})
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
const measure = () => setSizes(Object.fromEntries(Array.from(ref.current?.querySelectorAll<HTMLElement>("[data-compare]") ?? []).map(cell => {
|
||||
const panel = cell.querySelector<HTMLElement>(".hm-sw-property")!.getBoundingClientRect()
|
||||
return [cell.dataset.compare!, `${Math.round(panel.width)} × ${Math.round(panel.height)}`]
|
||||
})))
|
||||
measure()
|
||||
const observer = new ResizeObserver(measure)
|
||||
ref.current?.querySelectorAll(".hm-sw-property").forEach(panel => observer.observe(panel))
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
return <>
|
||||
<p className="sw-story-log" data-property-log="" aria-live="polite">{log.join(" → ") || "Callback 없음"}</p>
|
||||
<p hidden data-property-log="">{log.join(" → ")}</p>
|
||||
<div className="sw-story-compare-scroll" data-compare-scroll="">
|
||||
<div ref={ref} className="sw-story-compare sw-story-compare--grid">
|
||||
<div className="sw-story-compare sw-story-compare--grid">
|
||||
{PROPERTY_STATES.map(state => <figure key={state.example} className="sw-story-compare__cell" data-compare={state.example}>
|
||||
<figcaption><strong>{state.title}</strong><span>Figma {state.node} · {state.size}</span><span data-measured="">측정 {sizes[state.example] ?? "-"}</span></figcaption>
|
||||
<figcaption><strong>{state.title}</strong></figcaption>
|
||||
<PropertyState example={state.example} onLog={line => setLog(lines => [...lines.slice(-7), `${state.example}: ${line}`])} />
|
||||
</figure>)}
|
||||
</div>
|
||||
@@ -998,5 +926,5 @@ function PropertyCompare() {
|
||||
}
|
||||
/** Rendered by SW/Popup › Property Panel · 4개 비교 (sw-property-panel.stories.tsx, ID sw-header--property-panel-compare). */
|
||||
export function PropertyPanelComparePage() {
|
||||
return <StoryPage className="sw-story-compare-page"><StoryPageHeader kicker={KICKER} title="속성창 4개 상태 비교" description="네 패널은 같은 SWHeaderPropertyPanel입니다. 실제 Header에서는 현재 선택 객체의 패널 하나만 표시합니다." /><PropertyCompare /></StoryPage>
|
||||
return <StoryPage className="sw-story-compare-page"><StoryPageHeader kicker={KICKER} title="속성창 4개 상태 비교" /><PropertyCompare /></StoryPage>
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
.sw-home-menu-story__toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
|
||||
.sw-home-menu-story__toolbar > button { padding: 4px 8px; border: 1px solid #999; border-radius: 4px; background: #fff; font: inherit; font-size: 13px; cursor: pointer; }
|
||||
.sw-home-menu-story__log { margin: 12px 0 0; padding-left: 20px; font-family: monospace; font-size: 12px; }
|
||||
.sw-home-menu-story__table { border-collapse: collapse; font-size: 13px; }
|
||||
.sw-home-menu-story__table :is(th, td) { padding: 4px 8px; border: 1px solid #ccc; text-align: left; }
|
||||
.sw-home-menu-story__tall .sw-home-menu-story__frame { height: 860px; }
|
||||
.sw-home-menu-story__width { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 13px; }
|
||||
.sw-home-menu-story__width input { width: min(480px, 60vw); }
|
||||
|
||||
/* 2026-10-21 Storybook simplification (SW pages): no event log or long descriptions on screen; the log stays in the DOM
|
||||
for the browser tests. Same rule as sw-stories.css (this page does not load it). */
|
||||
.sb-page:has([class*="hm-sw-"]) :is(.sw-home-menu-story__log, [data-callback-log], .sb-page-description, .sb-page-section-description) { display: none; }
|
||||
@@ -191,7 +191,6 @@ function StackPage() {
|
||||
<StoryPageHeader kicker={KICKER} title="여러 Popup · Escape · 스크롤"
|
||||
description="Popup 안의 Escape는 그 Popup만 닫습니다. 바깥의 Escape는 가장 최근에 열리거나 사용한 Popup 하나만 닫습니다. Dropdown·편집 중인 Input이 처리한 Escape로는 닫히지 않습니다. 긴 콘텐츠는 본문이 스크롤됩니다." />
|
||||
{log}
|
||||
<button type="button" data-reopen="" onClick={() => setOpen(["a", "b", "c"])}>다시 열기</button>
|
||||
<SWTheme className="sw-story-popups" data-stack="">
|
||||
{open.includes("a") && <SWPopup title="Popup A" onClose={() => close("a")} data-popup="a"><SWPopupSection title="A"><SWPopupRow label="값"><SWHeaderInput value="A" aria-label="A 값" onCommit={next => record(`a=${next}`)} /></SWPopupRow></SWPopupSection></SWPopup>}
|
||||
{open.includes("b") && <SWPopup title="Popup B" onClose={() => close("b")} data-popup="b"><SWPopupSection title="B">
|
||||
|
||||
@@ -68,9 +68,12 @@
|
||||
.sw-overview-stage .hm-sw-property { position: absolute !important; left: var(--sw-preview-panel-left) !important; top: var(--sw-preview-panel-top) !important; max-height: none !important; }
|
||||
/* The document wrapper otherwise stacks below the independently positioned property Bar. */
|
||||
.sw-overview-stage .hm-sw-header__document:has(.hm-sw-header__document-menu) { z-index: 3; }
|
||||
.sw-overview-status { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 12px; min-width: 0; }
|
||||
.sw-overview-status output { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
/* Responsive More measured section: Storybook controls (width range, host content toggles) above the frame. */
|
||||
.sw-story-measured-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-bottom: 8px; font-size: 13px; }
|
||||
.sw-story-measured-controls label { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.sw-story-measured-controls input[type="range"] { width: min(480px, 60vw); }
|
||||
|
||||
/* 2026-10-21 Storybook simplification (SW pages): the components only. Callback logs, state dumps and long page /
|
||||
section descriptions are not shown; the log and state elements stay in the DOM because the browser tests read
|
||||
their text. Scoped to pages that render SW components, so other Story pages are unaffected. */
|
||||
.sb-page:has([class*="hm-sw-"]) :is([data-callback-log], [data-property-log], [data-style-state], .sb-page-description, .sb-page-section-description) { display: none; }
|
||||
@@ -0,0 +1,3 @@
|
||||
// Storybook demo controls the browser tests still drive (selection, list data, the viewer context menu) render only
|
||||
// when the preview URL carries `&verification`; the sidebar Stories show the real components without them.
|
||||
export const showVerificationControls = () => typeof window !== "undefined" && new URLSearchParams(window.location.search).has("verification")
|
||||
@@ -42,7 +42,7 @@ function ViewerArea({ width, panels, ...rest }: { width: number; panels?: boolea
|
||||
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name, initialPanels: panels ? ["layers", "options"] : [] })
|
||||
return (
|
||||
<Frame width={width} height={panels ? 600 : 220} {...rest}
|
||||
header={<SWViewerHeader documentTitle={demo.documentName} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||||
header={<SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||||
{panels && <>
|
||||
<LayerPanel demo={demo} strategy="absolute" defaultPosition="top-start" />
|
||||
<OptionsPanel demo={demo} strategy="absolute" />
|
||||
@@ -74,6 +74,8 @@ function PortalDemo() {
|
||||
|
||||
export const ThemeInheritance: Story = {
|
||||
name: "Theme Inheritance",
|
||||
// 2026-10-21 SW Storybook menu cleanup: hidden from the sidebar; ID and direct URL kept for the existing checks.
|
||||
tags: ["verification"],
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW — 테마 토큰 상속"
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
SWViewerBadge, SWViewerButton, SWViewerHeaderButton, SWViewerStatus, SWViewerField, SWViewerHint, SWViewerInput, SWViewerPanel, SWViewerSection, SWViewerSlider, SWViewerSwitch,
|
||||
type SWViewerCommand, type SWViewerHeaderGroup, type SWViewerPanelProps,
|
||||
type SWHeaderDocument, type SWViewerCommand, type SWViewerHeaderGroup, type SWViewerHeaderProps, type SWViewerPanelProps,
|
||||
} from "@hm-design/sw"
|
||||
import { Eye, EyeOff, FolderOpen, Info, Layers, List, MousePointerClick, RotateCcw, Scan, SlidersHorizontal, ZoomIn } from "lucide-react"
|
||||
import { Eye, EyeOff, Info, Layers, List, MousePointerClick, RotateCcw, Scan, SlidersHorizontal, ZoomIn } from "lucide-react"
|
||||
|
||||
export const SAMPLES = [
|
||||
{ id: "sample-basic", label: "기본 도면 · DWG", name: "BasicSample.dwg" },
|
||||
@@ -50,16 +50,19 @@ export function useViewerDemo({ initialDocument = null as string | null, initial
|
||||
const toggle = (id: ViewerPanelId, next = !panels[id]) => setPanels(current => ({ ...current, [id]: next }))
|
||||
const loaded = documentName !== null
|
||||
|
||||
// 2026-10-21: file open is the header button after the logo (SWHeader documentOpenPlacement="header"); the samples
|
||||
// are the file-name list (no Open / + New there).
|
||||
const openFile = () => record("파일 선택창 (미연결)")
|
||||
const documents = SAMPLES.map(sample => ({ id: sample.id, name: sample.name, title: sample.label }))
|
||||
const fileProps = {
|
||||
documents,
|
||||
activeDocumentId: SAMPLES.find(sample => sample.name === documentName)?.id,
|
||||
onDocumentSelect: (document: SWHeaderDocument) => { setDocumentName(document.name); record(`samples/${document.id}`) },
|
||||
onDocumentOpen: openFile,
|
||||
documentOpenLabel: "파일 열기",
|
||||
} satisfies Partial<SWViewerHeaderProps>
|
||||
|
||||
const groups = useMemo<SWViewerHeaderGroup[]>(() => [
|
||||
{
|
||||
id: "file", label: "파일", items: [
|
||||
{ type: "button", id: "open", label: "파일 열기", icon: <FolderOpen />, title: "파일 열기", iconOnlyBelow: 560, onSelect: () => record("파일 선택창 (미연결)") },
|
||||
{
|
||||
type: "menu", id: "samples", label: "샘플", moreLabel: "샘플 도면", collapseBelow: 1180,
|
||||
entries: SAMPLES.map(sample => ({ type: "item" as const, id: sample.id, label: sample.label, onSelect: () => setDocumentName(sample.name) })),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "space", label: "도면 공간", surface: "field", hidden: !loaded, items: [
|
||||
{ type: "tabs", id: "space", label: "모델과 배치 선택", moreLabel: "도면 공간", collapseBelow: 1180, value: space, options: SPACES, onValueChange: setSpace },
|
||||
@@ -67,16 +70,17 @@ export function useViewerDemo({ initialDocument = null as string | null, initial
|
||||
},
|
||||
{
|
||||
id: "view", label: "화면 보기", surface: "field", items: [
|
||||
{ type: "button", id: "fit", label: "전체 보기", icon: <Scan />, title: "전체 도면 맞춤 (F)", iconOnlyBelow: 560, disabled: !loaded },
|
||||
// 2026-10-21: shown while the header has room, icon only next, then into More (measured, see SWViewerHeader).
|
||||
{ type: "button", id: "fit", label: "전체 보기", icon: <Scan />, title: "전체 도면 맞춤 (F)", iconOnlyBelow: 560, collapseBelow: 1180, disabled: !loaded },
|
||||
{ type: "switch", id: "2.5d", label: "2.5D", checked: is25D, collapseBelow: 1180, onCheckedChange: set25D },
|
||||
{ type: "switch", id: "dark", label: "어두운 배경", checked: dark, collapseBelow: 1180, onCheckedChange: setDark },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "panels", label: "패널", placement: "end", items: [
|
||||
{ type: "toggle", id: "layers", label: "레이어", icon: <Layers />, title: "레이어 패널 열기 / 닫기", pressed: panels.layers, collapseBelow: 900, onPressedChange: next => toggle("layers", next) },
|
||||
{ type: "toggle", id: "options", label: "설정", icon: <SlidersHorizontal />, title: "옵션 패널 열기 / 닫기", pressed: panels.options, collapseBelow: 900, onPressedChange: next => toggle("options", next) },
|
||||
{ type: "toggle", id: "properties", label: "속성", icon: <List />, title: "속성 패널 열기 / 닫기", pressed: panels.properties, collapseBelow: 900, onPressedChange: next => toggle("properties", next) },
|
||||
{ type: "toggle", id: "layers", label: "레이어", icon: <Layers />, title: "레이어 패널 열기 / 닫기", pressed: panels.layers, iconOnlyBelow: 900, onPressedChange: next => toggle("layers", next) },
|
||||
{ type: "toggle", id: "options", label: "설정", icon: <SlidersHorizontal />, title: "옵션 패널 열기 / 닫기", pressed: panels.options, iconOnlyBelow: 900, onPressedChange: next => toggle("options", next) },
|
||||
{ type: "toggle", id: "properties", label: "속성", icon: <List />, title: "속성 패널 열기 / 닫기", pressed: panels.properties, iconOnlyBelow: 900, onPressedChange: next => toggle("properties", next) },
|
||||
],
|
||||
},
|
||||
], [loaded, space, is25D, dark, panels])
|
||||
@@ -93,7 +97,7 @@ export function useViewerDemo({ initialDocument = null as string | null, initial
|
||||
|
||||
return {
|
||||
documentName, setDocumentName, space, is25D, set25D, dark, setDark, panels, toggle, options, setOptions, hidden, setHidden,
|
||||
log, record, groups, trailing, onCommand, loaded, aboutOpen, setAboutOpen,
|
||||
log, record, groups, trailing, onCommand, loaded, aboutOpen, setAboutOpen, fileProps,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,3 +27,5 @@
|
||||
.sw-viewer-story-reopen { position: absolute; left: 712px; top: 16px; padding: 6px 10px; border-radius: 4px; background: #f5f0e5; font-size: 12px; }
|
||||
.sw-viewer-story-context { display: flex; align-items: center; justify-content: center; height: 160px; border: 1px dashed #9a9284; border-radius: 4px; background: #f5f0e5; color: #3e3523; font-size: 13px; }
|
||||
.sw-viewer-story-mode { display: flex; gap: 16px; margin-bottom: 8px; font-size: 13px; }
|
||||
/* Room below a single header row so its logo menu / document list are not cut by the scroller. */
|
||||
.sw-viewer-story-menu-row { padding-bottom: 120px; }
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
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"
|
||||
@@ -23,8 +24,9 @@ const 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 className="sw-story-log" aria-live="polite" data-callback-log="">Command: {lines.length ? lines.join(" → ") : "없음"}</p>
|
||||
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). */
|
||||
@@ -61,7 +63,7 @@ function ViewerScreen({ width = 1440, initialDocument = SAMPLES[1].name as strin
|
||||
return (
|
||||
<>
|
||||
<Log lines={demo.log} />
|
||||
<ViewerFrame width={width} header={<SWViewerHeader documentTitle={demo.documentName} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||||
<ViewerFrame width={width} header={<SWViewerHeader 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 }} />
|
||||
@@ -75,9 +77,8 @@ 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 결과가 표시됩니다.">
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — SWViewerHeader" />
|
||||
<StorySection title="기본형 (1440px, 도면 열림)">
|
||||
<ViewerScreen />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
@@ -86,6 +87,8 @@ export const ViewerHeader: Story = {
|
||||
|
||||
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) }))
|
||||
@@ -98,12 +101,12 @@ export const MenuGroups: Story = {
|
||||
]
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 메뉴 그룹" description="그룹은 groups 배열의 순서대로 놓입니다. surface="field"는 사용자 확정 속성 입력 면(어두운 inset), placement="end"는 오른쪽 끝 영역입니다. hidden으로 그룹·항목을 제거합니다." />
|
||||
<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} groups={pick(row.ids)} trailing={row.ids.includes("panels") ? demo.trailing : undefined} onCommand={demo.onCommand} /></div></div>
|
||||
<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>
|
||||
@@ -114,6 +117,8 @@ export const MenuGroups: Story = {
|
||||
|
||||
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])
|
||||
@@ -139,8 +144,7 @@ export const States: Story = {
|
||||
]
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — Active / Hover / Disabled"
|
||||
description="Hover: #266651 면, Active·열림·선택 탭: 글자와 아이콘 #FFC600(선택 박스 없음), Disabled: 60% 불투명, Focus: 2px #FFC600 외곽선(키보드). Hover와 Focus는 마우스·Tab으로 확인합니다." />
|
||||
<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)}` : ""}`)} />
|
||||
@@ -174,6 +178,8 @@ const MENU_ENTRIES: SWViewerMenuEntry[] = [
|
||||
|
||||
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[]>([])
|
||||
@@ -192,16 +198,15 @@ export const Dropdowns: Story = {
|
||||
]
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 상단 메뉴 + Dropdown"
|
||||
description="type="menu" 항목의 entries가 Dropdown이 됩니다. 일반 항목·체크·라디오·라벨·구분선·다단계 서브메뉴(submenu)를 지원합니다. 키보드: Enter/↓ 열기, ↑↓ 이동, → 서브메뉴, ← 닫기, Esc 닫고 버튼으로 복귀, 바깥 클릭 닫기." />
|
||||
<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>
|
||||
<StorySection title="SWViewerMenu 단독 (Context Menu)" description="anchorPoint로 지정 위치에 엽니다. 아래 영역을 마우스 오른쪽 버튼으로 클릭하세요.">
|
||||
{showVerificationControls() && <StorySection title="SWViewerMenu 단독 (Context Menu)">
|
||||
<ContextMenuDemo onSelect={line => record(line)} />
|
||||
</StorySection>
|
||||
</StorySection>}
|
||||
</StoryPage>
|
||||
)
|
||||
},
|
||||
@@ -235,14 +240,15 @@ function ContextMenuDemo({ onSelect }: { onSelect: (line: string) => void }) {
|
||||
|
||||
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)"
|
||||
description="설정 토글로 여는 옵션 팝업입니다. 제목 막대를 끌어 이동, 오른쪽 아래 모서리로 크기 조절, 제목 더블클릭·접기 버튼으로 접기, 닫기·Esc(팝업 안 Focus)로 닫습니다. 하단 버튼 영역(footer)을 포함한 예시입니다." />
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 설정창 (SWViewerPanel)" />
|
||||
<Log lines={demo.log} />
|
||||
<ViewerFrame header={<SWViewerHeader documentTitle={demo.documentName} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||||
<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>
|
||||
@@ -251,18 +257,18 @@ export const SettingsPanel: Story = {
|
||||
}
|
||||
|
||||
export const ContentVariants: Story = {
|
||||
name: "Popup Content Variants",
|
||||
// 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"] })
|
||||
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 kicker={KICKER} title="SW Viewer — 같은 프레임, 다른 내부 콘텐츠"
|
||||
description="SWViewerPanel의 외형(제목 막대·닫기·테두리·스크롤 본문)은 같고 children / footer만 다릅니다: 검색 + 목록(레이어), 읽기 전용 정의 목록(속성), 입력 폼 + 하단 버튼(입력 팝업)." />
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 설정창 (SWViewerPanel)" />
|
||||
<Log lines={demo.log} />
|
||||
<ViewerFrame height={640} header={<SWViewerHeader documentTitle={demo.documentName} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />}>
|
||||
<ViewerFrame height={870} 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: 712, top: 16 }} title="축척 지정" open={inputOpen} onClose={() => setInputOpen(false)} width={300} height="auto" resizable={false}
|
||||
@@ -273,6 +279,7 @@ export const ContentVariants: Story = {
|
||||
<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: 290 }} footer />
|
||||
{!inputOpen && <button type="button" className="sw-viewer-story-reopen" onClick={() => setInputOpen(true)}>축척 지정 다시 열기</button>}
|
||||
</ViewerFrame>
|
||||
</StoryPage>
|
||||
@@ -282,6 +289,8 @@ export const ContentVariants: Story = {
|
||||
|
||||
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: "내용 높이" },
|
||||
@@ -292,8 +301,7 @@ export const VariableSize: Story = {
|
||||
const lines = Array.from({ length: 24 }, (_, index) => `행 ${index + 1}`)
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 가변 크기 팝업"
|
||||
description="width / height("auto" 가능) / minWidth / minHeight로 메뉴별 크기를 정합니다. 최대 90vw × 90vh, 오른쪽 아래 모서리로 크기 조절, 화면(또는 strategy="absolute"의 부모) 경계 안에서만 이동합니다." />
|
||||
<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}>
|
||||
@@ -308,18 +316,17 @@ export const VariableSize: Story = {
|
||||
}
|
||||
|
||||
export const Responsive: Story = {
|
||||
name: "Responsive Widths",
|
||||
name: "Responsive More",
|
||||
render: () => {
|
||||
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 화면 너비"
|
||||
description="참고 뷰어와 같은 기준입니다. Header 폭 1180px 미만: 샘플·도면 공간·2.5D·어두운 배경이 더보기(⋯)로 이동, 900px 미만: 패널 토글도 이동, 560px 미만: 파일 열기·전체 보기는 아이콘만, 상태 글자 숨김. 가로 스크롤은 만들지 않습니다." />
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 화면 너비" />
|
||||
<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>
|
||||
<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>
|
||||
@@ -342,7 +349,7 @@ function ModeSwitchDemo() {
|
||||
<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")} />}>
|
||||
: <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>
|
||||
</>
|
||||
@@ -353,15 +360,14 @@ 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 로고(임시)를 누르면 뷰어 모드, 뷰어 로고(임시)를 누르면 일반 모드로 돌아갑니다." />
|
||||
<StoryPageHeader kicker={KICKER} 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="일반 ↔ 뷰어 모드 전환" description="Header 교체만 보여 주는 UI 예시입니다. 실제 모드 진입·종료와 데이터 처리는 SW 프로그램 담당입니다.">
|
||||
<StorySection title="일반 ↔ 뷰어 모드 전환">
|
||||
<ModeSwitchDemo />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
@@ -372,22 +378,23 @@ 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: "예시 로고" } },
|
||||
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 kicker={KICKER} title="SW Viewer — 로고" description="로고는 각 프로그램이 정합니다. 제공된 경우에만 표시하고, Header 높이(52px)와 메뉴 정렬은 그대로입니다. SWHeader의 로고 동작은 바뀌지 않습니다." />
|
||||
<StoryPageHeader kicker={KICKER} title="SW Viewer — 로고" />
|
||||
<div className="sw-story-stack">
|
||||
{rows.map(row => (
|
||||
<StorySection key={row.title} title={row.title} description={row.description}>
|
||||
<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} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
|
||||
<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>
|
||||
)
|
||||
@@ -396,5 +403,5 @@ export const Logo: Story = {
|
||||
|
||||
function ViewerOnly() {
|
||||
const demo = useViewerDemo({ initialDocument: SAMPLES[1].name })
|
||||
return <SWViewerHeader documentTitle={demo.documentName} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
|
||||
return <SWViewerHeader documentTitle={demo.documentName} {...demo.fileProps} groups={demo.groups} trailing={demo.trailing} onCommand={demo.onCommand} />
|
||||
}
|
||||
Reference in new issue
Block a user