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
@@ -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>
|
||||
}
|
||||
Reference in new issue
Block a user