refactor(storybook): remove unused SW header demo stories
Remove nine SW/Header demo pages (property-fields, property-shape-slot, composition, colorize, dynamic-controls, property-bar, additional-hover, property-panel, layer-detail-input) and the demo code and Storybook-only CSS used only by them. SW Stories 19 -> 10, total 338 -> 329. The package runtime, public API, AI template and the retained Stories (Overview, Dropdown, Property Panel 4-state comparison, Window Move Motion and others) are unchanged. Add a current regression script and mark the older scripts that open the retired IDs as historical evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8e5fbeebc
commit
5faa1f1ffe
58 files changed
+10577
-499
No files matched your search
@@ -2,10 +2,10 @@ import { useEffect, useId, useRef, useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import {
|
||||
SWHeader, SWHeaderColorMenu, SWHeaderLineStyleMenu, SWHeaderTextStyleMenu, SWHeaderDimensionStyleMenu,
|
||||
SWHeaderProperties, SWHeaderWindowControls, SWTheme, SWHeaderLayerMenu, SWHeaderInput, parseSWHeaderColor, swHeaderLinePatterns, SWHeaderPropertyPanel, SWHeaderPropertyBar,
|
||||
SWHeaderProperties, SWHeaderWindowControls, SWTheme, SWHeaderLayerMenu, parseSWHeaderColor, swHeaderLinePatterns, SWHeaderPropertyPanel, SWHeaderPropertyBar,
|
||||
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 SWHeaderControlConfig, type SWHeaderHideableControlId, type SWHeaderPropertyShapeContent,
|
||||
type SWHeaderMenuId, type SWHeaderProps, type SWHeaderTextStyleOption, type SWHeaderProperty, type SWHeaderPropertyGroup, type SWHeaderPropertySelection, type SWHeaderPropertyMenus, type SWHeaderPropertyShapeContent,
|
||||
} from "@hm-design/sw"
|
||||
import "@hm-design/sw/styles.css"
|
||||
import { SWHeaderMenuDetail } from "../../packages/sw/src/components/sw-header/components/SWHeaderDropdown"
|
||||
@@ -13,8 +13,6 @@ import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-pag
|
||||
import "./sw-stories.css"
|
||||
import { SWHeaderOverview } from "./sw-header-overview"
|
||||
import placeholderLogo from "./assets/sw-header-logo-placeholder.svg"
|
||||
import figmaLayerDetailReference from "./assets/figma-reference/btn-detail-layer-26-10767.png"
|
||||
import figmaInputReference from "./assets/figma-reference/input-26-11656.png"
|
||||
|
||||
// Owner: docs/sw/sw-header-design-spec.md (Figma rj6wpsfk24lOWvWiVRAANP, node 26:8944 "header_green").
|
||||
// The package is independent of @hm-design/ui: this file imports only @hm-design/sw for the specimen.
|
||||
@@ -615,7 +613,6 @@ export const DocumentDropdown: Story = {
|
||||
}
|
||||
|
||||
/** Test data only. The package never ships layer names; each SW program passes its own list. */
|
||||
const FIGMA_LAYERS: SWHeaderLayer[] = Array.from({ length: 8 }, (_, index) => ({ id: `figma-${index + 1}`, name: "CC-D GNL" }))
|
||||
const SAMPLE_LAYERS: SWHeaderLayer[] = [
|
||||
{ id: "layer-0", name: "0" },
|
||||
{ id: "layer-1", name: "Sample-Center", color: "#ff0000" },
|
||||
@@ -640,56 +637,6 @@ const MANY_LAYERS: SWHeaderLayer[] = Array.from({ length: 20 }, (_, index) => ({
|
||||
locked: index % 6 === 3,
|
||||
}))
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
type LayerHostProps = Partial<SWHeaderProps> & { onLog?: (line: string) => void; showState?: boolean }
|
||||
|
||||
const describeLayers = (layers: SWHeaderLayer[]) => layers.map(layer =>
|
||||
`${layer.id}: visible=${layer.visible !== false} frozen=${layer.frozen === true} locked=${layer.locked === true}`).join("\n")
|
||||
|
||||
/**
|
||||
* Host-owned layers: the SW program keeps the list, the current layer and each layer's state. The
|
||||
* header only reports the next value; this example writes it back so the icons follow.
|
||||
*/
|
||||
function LayerHeader({ layers: initialLayers = SAMPLE_LAYERS, activeLayerId: initialActiveId, onLog, showState, ...props }: LayerHostProps) {
|
||||
const [layers, setLayers] = useState(initialLayers)
|
||||
const [activeId, setActiveId] = useState(initialActiveId)
|
||||
const update = (id: string, patch: Partial<SWHeaderLayer>) => setLayers(list => list.map(item => (item.id === id ? { ...item, ...patch } : item)))
|
||||
const header = (
|
||||
<SWHeader
|
||||
{...figmaArgs}
|
||||
{...callbacks}
|
||||
layers={layers}
|
||||
activeLayerId={activeId}
|
||||
onLayerSelect={layer => { console.info("[SWHeader] layerSelect", layer.id); onLog?.(`onLayerSelect(${layer.id})`); setActiveId(layer.id) }}
|
||||
onLayerVisibilityChange={(layer, next) => { onLog?.(`onLayerVisibilityChange(${layer.id}, ${next})`); update(layer.id, { visible: next }) }}
|
||||
onLayerFreezeChange={(layer, next) => { onLog?.(`onLayerFreezeChange(${layer.id}, ${next})`); update(layer.id, { frozen: next }) }}
|
||||
onLayerLockChange={(layer, next) => { onLog?.(`onLayerLockChange(${layer.id}, ${next})`); update(layer.id, { locked: next }) }}
|
||||
onLayerDetail={() => { console.info("[SWHeader] layerDetail"); onLog?.("onLayerDetail") }}
|
||||
onLayerMenuOpenChange={open => onLog?.(`onLayerMenuOpenChange(${open})`)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
if (!showState) return header
|
||||
return (
|
||||
<>
|
||||
<pre className="sw-story-layer-state" data-layer-state="">{`${describeLayers(layers)}\nactiveLayerId: ${activeId ?? "없음"}`}</pre>
|
||||
{header}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** Interactive demo with a visible callback log, to check that selection and toggles never double-fire. */
|
||||
function LayerDemo({ width, ...props }: LayerHostProps & { 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}><LayerHeader showState {...props} onLog={line => setLog(lines => [...lines.slice(-9), line])} /></MenuFrame>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
Field dropdowns (2026-10-16): color 26:10986 / 26:11036, ByLayer 26:12666, ByBlock 26:12659,
|
||||
line style 26:11104 (rows 26:10702), text style 26:11181, dimension style 26:11215.
|
||||
@@ -735,16 +682,16 @@ const FIGMA_DIMENSION_STYLES: SWHeaderDimensionStyleOption[] = [
|
||||
{ id: "iso-25", name: "ISO - 25" },
|
||||
]
|
||||
|
||||
type StyleHostProps = Partial<SWHeaderProps> & { onLog?: (line: string) => void; showState?: boolean; initialColorMode?: SWHeaderColorMode | null }
|
||||
type StyleHostProps = Partial<SWHeaderProps> & { onLog?: (line: string) => void; showState?: boolean }
|
||||
|
||||
/**
|
||||
* Host-owned field data: the SW program keeps color, line / text / dimension style and the lists. The
|
||||
* header only reports; this example writes each report back (an explicit color clears ByLayer / ByBlock,
|
||||
* which is this host's choice) so the fields follow. Each property changes alone.
|
||||
*/
|
||||
function StyleHeader({ onLog, showState, initialColorMode = null, ...props }: StyleHostProps) {
|
||||
function StyleHeader({ onLog, showState, ...props }: StyleHostProps) {
|
||||
const [color, setColor] = useState<SWHeaderColorValue>(figmaArgs.color!)
|
||||
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>(initialColorMode)
|
||||
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>(null)
|
||||
const [lineStyle, setLineStyle] = useState<SWHeaderLineStyleValue>(figmaArgs.lineStyle!)
|
||||
const [lineWeights, setLineWeights] = useState(FIGMA_LINE_WEIGHTS)
|
||||
const [weightId, setWeightId] = useState<string | undefined>("w-default")
|
||||
@@ -857,101 +804,6 @@ export const ResponsiveMore: Story = {
|
||||
),
|
||||
}
|
||||
|
||||
/** Host-owned detail values for the editable input (the detail popup itself is not designed yet). */
|
||||
function InputDemo() {
|
||||
const [value, setValue] = useState("4.0526")
|
||||
const [name, setName] = useState("벽체_외벽")
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const add = (line: string) => setLog(lines => [...lines.slice(-9), line])
|
||||
return (
|
||||
<>
|
||||
<p className="sw-story-log" aria-live="polite" data-callback-log="">Callback: {log.length ? log.join(" → ") : "없음"}</p>
|
||||
<pre className="sw-story-layer-state" data-input-state="">{`확정값: number=${value} · text=${name}`}</pre>
|
||||
<div className="sw-story-controls">
|
||||
<button type="button" onClick={() => { setValue("12.5"); add("host: value=12.5") }}>외부 값 변경 (12.5)</button>
|
||||
<button type="button" onClick={() => { setName("레이어 이름 변경"); add("host: text 변경") }}>외부 텍스트 변경</button>
|
||||
</div>
|
||||
<div className="sw-story-zoom">
|
||||
<SWTheme className="sw-story-composed sw-story-detail">
|
||||
<SWHeaderInput
|
||||
aria-label="숫자 속성"
|
||||
valueType="number"
|
||||
value={value}
|
||||
onValueChange={draft => add(`onValueChange(${draft})`)}
|
||||
onCommit={next => { add(`onCommit(${next})`); setValue(next) }}
|
||||
onCancel={() => add("onCancel")}
|
||||
/>
|
||||
<SWHeaderInput
|
||||
aria-label="텍스트 속성"
|
||||
value={name}
|
||||
onCommit={next => { add(`onCommit(${next})`); setName(next) }}
|
||||
onCancel={() => add("onCancel")}
|
||||
/>
|
||||
</SWTheme>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const LayerDetailInput: Story = {
|
||||
name: "Layer Detail · Input",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={KICKER}
|
||||
title="Layer Detail Button · Editable Input"
|
||||
description="레이어 Dropdown 하단 '레이어 상세' 버튼(btn_상세/레이어 26:10767)과, 상세 영역에서 쓸 수정 가능한 Input(input 26:11656) · 커서(cursor 26:11664)입니다. 상세 팝업 전체는 Figma 전달 전이라 만들지 않으며, 버튼 Callback(onLayerDetail)과 SWHeaderInput 계약만 제공합니다."
|
||||
/>
|
||||
<StorySection title="Figma 원본 (비교용 이미지)" description="왼쪽: 레이어 상세 Default / Hover(배경 .25 · radius 25 · 아이콘 35%). 오른쪽: Input Default 58 × 20 / Hover 58 × 24 / On 58 × 24. 최신 요청에 따라 아래 실제 버튼에도 Hover를 적용합니다. 이미지는 Storybook 비교용이며 패키지에 포함하지 않습니다.">
|
||||
<div className="sw-story-row sw-story-figma">
|
||||
<img className="sw-story-reference" src={figmaLayerDetailReference} alt="Figma btn_상세/레이어 26:10767" width={180} height={108} />
|
||||
<img className="sw-story-reference" src={figmaInputReference} alt="Figma input 26:11656" width={98} height={140} />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="레이어 상세 — Default · Hover (고정) · Disabled (3배 확대)" description="왼쪽: Callback 없이 사용 가능(Default). 가운데: Hover 배경 .25·radius 25·Hover 아이콘. 오른쪽: 호스트가 fieldset disabled를 명시한 상태. Figma와 같은 113 × 24, 아이콘 14 · 간격 4 · padding 5 / 10 · 배경 rgba(197,177,158,.15) · radius 24 · 글자 35%. CAD 상세 실행은 호스트 Callback이 있을 때만 가능합니다.">
|
||||
<div className="sw-story-row">
|
||||
{[{ detail: undefined, hover: false, disabled: false }, { detail: noop, hover: true, disabled: false }, { detail: undefined, hover: false, disabled: true }].map((item, index) => {
|
||||
const specimen = (
|
||||
<div className="sw-story-zoom sw-story-layer-room" data-detail-case={index}>
|
||||
<fieldset className="sw-story-explicit-disabled" disabled={item.disabled}><SWTheme className="sw-story-composed">
|
||||
<SWHeaderLayerMenu value={{ name: "0" }} layers={FIGMA_LAYERS.slice(0, 2)} open onDetail={item.detail} />
|
||||
</SWTheme></fieldset>
|
||||
</div>
|
||||
)
|
||||
return item.hover ? <PinHover key={index} selectors={[".hm-sw-header__layer-detail"]}>{specimen}</PinHover> : <div key={index}>{specimen}</div>
|
||||
})}
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="레이어 상세 Click Callback" description="필드를 열고 '레이어 상세'를 누르면 onLayerDetail 1회 후 목록이 닫힙니다(기존 동작). 레이어 선택·전구·눈꽃·자물쇠 매핑은 변경하지 않았습니다.">
|
||||
<LayerDemo width={1920} layers={FIGMA_LAYERS} />
|
||||
</StorySection>
|
||||
<StorySection title="Input — Default · Hover (고정) · ReadOnly · Disabled (3배 확대)" description="Default: 배경·테두리 없음, #E60403 14px + 검정 1.3px Stroke. Hover: 1px rgba(212,203,189,.25), 24px(글자 위치 그대로). ReadOnly(onCommit 없음 또는 readOnly): Hover·편집 상태 없음. Disabled: 40%.">
|
||||
<PinHover selectors={[".sw-story-pin-hover"]}>
|
||||
<div className="sw-story-zoom">
|
||||
<SWTheme className="sw-story-composed sw-story-detail">
|
||||
<SWHeaderInput aria-label="Default" value="4.0526" onCommit={noop} />
|
||||
<SWHeaderInput aria-label="Hover" className="sw-story-pin-hover" value="4.0526" onCommit={noop} />
|
||||
<SWHeaderInput aria-label="ReadOnly" value="4.0526" onCommit={noop} readOnly />
|
||||
<SWHeaderInput aria-label="Disabled" value="4.0526" onCommit={noop} disabled />
|
||||
</SWTheme>
|
||||
</div>
|
||||
</PinHover>
|
||||
</StorySection>
|
||||
<StorySection title="Input — Editing (Focus)" description="편집 중(26:11661): 배경 #0F0D0A, 1px rgba(212,203,189,.3), 글자 #E60403 유지, 네이티브 커서 #E60403(caret-color, 깜박임 = Figma cursor Default / Variant2). 이 예제는 입력에 자동 Focus합니다.">
|
||||
<div className="sw-story-zoom">
|
||||
<SWTheme className="sw-story-composed sw-story-detail">
|
||||
<SWHeaderInput aria-label="Editing" value="4.0526" onCommit={noop} autoFocus />
|
||||
</SWTheme>
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="Input — 입력 · Enter 확정 · Escape 취소 · 외부 값 갱신" description="숫자 입력(valueType='number'): 유효한 소수만 onCommit, 잘못된 값은 복원. 텍스트 입력(한글 IME 포함): 조합 중 Enter는 확정하지 않습니다. Escape는 편집 취소(onCancel) — 확정값으로 복원. Focus 이동(Tab / 바깥 클릭)도 확정합니다. 위 버튼으로 호스트 값을 바꾸면 최신 값이 표시됩니다.">
|
||||
<InputDemo />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
/* Additional review 2026-10-16: fixture values are host data, never package defaults. */
|
||||
const PROPERTY_GROUPS: SWHeaderPropertyGroup[] = [
|
||||
{ id: "common", collapsedCount: 6 },
|
||||
@@ -1038,239 +890,17 @@ function usePropertyMenus(record: (line: string) => void, setProperties: (update
|
||||
}
|
||||
}
|
||||
|
||||
function PropertyPanelDemo() {
|
||||
const [example, setExample] = useState<PropertyExample>("none")
|
||||
const [selection, setSelection] = useState(propertySelection("none"))
|
||||
const [properties, setProperties] = useState(propertyFixture("none"))
|
||||
const [expanded, setExpanded] = useState<string[]>([])
|
||||
const [controlled, setControlled] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [width, setWidth] = useState(1920)
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const record = (text: string) => setLog(lines => [...lines.slice(-19), text])
|
||||
const menus = usePropertyMenus(record, setProperties)
|
||||
const changeExample = (next: PropertyExample) => {
|
||||
setExample(next); setSelection(propertySelection(next)); setProperties(propertyFixture(next))
|
||||
setExpanded(next === "expanded" ? ["common"] : [])
|
||||
}
|
||||
const panel = {
|
||||
selection, properties, groups: PROPERTY_GROUPS, expandedGroupIds: expanded, menus,
|
||||
onExpandedGroupsChange: setExpanded,
|
||||
onPropertyChange: (property: SWHeaderProperty, value: string) => {
|
||||
record("property(" + property.id + ", " + value + ")")
|
||||
setProperties(items => items.map(item => item.id === property.id ? { ...item, value, mixed: false, ...(item.id === "line" ? { linePattern: linePatternOf(value) } : {}) } : item))
|
||||
},
|
||||
onToolActiveChange: (next: boolean) => record("tool(" + next + ")"),
|
||||
onOverviewClick: () => changeExample("all"),
|
||||
}
|
||||
return (
|
||||
<div data-property-demo="" data-example={example}>
|
||||
<div className="sw-story-controls">
|
||||
{(["none", "all", "circle", "expanded"] as const).map((name, index) => <button key={name} data-example-button={name} onClick={() => changeExample(name)}>{["선택 없음", "전체 선택", "원 1개", "모두 펼침"][index]}</button>)}
|
||||
{[1920, 1440, 1080, 900, 768, 640, 390].map(value => <button key={value} onClick={() => setWidth(value)}>{value}px</button>)}
|
||||
<button onClick={() => { setControlled(value => !value); setOpen(false) }}>Controlled: {String(controlled)}</button>
|
||||
<button onClick={() => setOpen(value => !value)}>외부 Open 변경</button>
|
||||
</div>
|
||||
<div className="sw-story-controls">
|
||||
<button onClick={() => setProperties(items => [...items, { id: "extra-" + items.length, groupId: "common", name: "추가 속성", type: "text", value: "호스트 값" }])}>속성 추가</button>
|
||||
<button onClick={() => setProperties(items => [...items, { id: "shape-extra-" + items.length, groupId: "shape", name: "추가 형상 속성", type: "text", value: "호스트 값" }])}>형상 속성 추가</button>
|
||||
<button onClick={() => setProperties(items => items.map(item => item.id === "line" ? { ...item, options: item.options?.filter(option => option.id !== "CENTER") } : item))}>CENTER 옵션 삭제</button>
|
||||
<button onClick={() => setProperties(items => items.filter(item => item.id !== "layer"))}>레이어 속성 삭제</button>
|
||||
<button onClick={() => setProperties(items => items.map(item => item.id === "layer" ? { ...item, value: "외부 변경값" } : item))}>외부 값 변경</button>
|
||||
<button onClick={() => setSelection(current => ({ ...current, id: current.id + "-changed" }))}>선택 객체 교체</button>
|
||||
<button onClick={() => setProperties(items => [...items].reverse())}>속성 순서 뒤집기</button>
|
||||
<button onClick={() => setProperties(items => items.map(item => item.id === "weight" ? { ...item, disabled: !item.disabled } : item))}>가중치 Disabled</button>
|
||||
<button onClick={() => setProperties(items => items.map(item => item.id === "layer" ? { ...item, readOnly: !item.readOnly } : item))}>레이어 ReadOnly</button>
|
||||
<button onClick={() => setProperties(items => items.map(item => item.id === "opacity" ? { ...item, value: "0.8" } : item))}>투명도 외부 값 변경</button>
|
||||
<button onClick={() => setProperties(items => items.map(item => item.id === "opacity" ? { ...item, disabled: !item.disabled } : item))}>투명도 Disabled</button>
|
||||
<button onClick={() => setProperties(items => items.map(item => item.id === "opacity" ? { ...item, readOnly: !item.readOnly } : item))}>투명도 ReadOnly</button>
|
||||
</div>
|
||||
<p className="sw-story-log" data-property-log="" aria-live="polite">{log.join(" → ") || "Callback 없음"}</p>
|
||||
<pre className="sw-story-layer-state" data-property-state="">{JSON.stringify({ selection: selection.id, values: properties.map(item => [item.id, item.value]), expanded, open })}</pre>
|
||||
<div className="sw-story-property-room"><Frame width={width}><SWHeader {...figmaArgs} propertyPanel={panel} propertyPanelOpen={controlled ? open : undefined}
|
||||
onPropertyPanelOpenChange={next => { record("open(" + next + ")"); if (controlled) setOpen(next) }}
|
||||
onPropertyPanelToggle={() => record("legacyToggle")}
|
||||
layers={SAMPLE_LAYERS} activeLayerId="layer-0" colors={FIGMA_COLORS} lineTypes={FIGMA_LINE_TYPES} textStyles={FIGMA_TEXT_STYLES} dimensionStyles={FIGMA_DIMENSION_STYLES}
|
||||
/></Frame></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PropertyPanel: Story = {
|
||||
name: "Property Panel",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="속성창" description="Figma 4개 상태를 외부 데이터·그룹 펼침으로 구성합니다. 속성창 버튼(좁은 화면: 더보기)으로 열고 닫으세요. Enter/Blur 확정, Escape 취소, 다음 Escape 닫기." /><PropertyPanelDemo /></StoryPage>,
|
||||
}
|
||||
export const KGuide: Story = {
|
||||
name: "K Guide",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="K 가이드 반복 애니메이션" description="기존 K 버튼 Hover/Focus → 원본 이미지 Pan·Zoom 반복. 영역을 벗어나면 정지, 재진입은 처음부터. Reduced Motion은 정지 이미지." /><div className="sw-story-property-room"><SWHeader {...figmaArgs} {...callbacks} /></div></StoryPage>,
|
||||
}
|
||||
function RequiredColorDemo() {
|
||||
const [mode, setMode] = useState<SWHeaderColorMode | null>(null)
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
return <div data-mode-demo=""><div className="sw-story-controls">
|
||||
<button onClick={() => setMode("byLayer")}>외부 ByLayer</button><button onClick={() => setMode("byBlock")}>외부 ByBlock</button><button onClick={() => setMode(null)}>외부 직접 색상</button>
|
||||
</div><p data-mode-log="">{log.join(",")}</p><MenuFrame width={1920}><SWHeader {...figmaArgs} colors={FIGMA_COLORS} colorMode={mode} onColorModeChange={next => { setMode(next); setLog(items => [...items, next]) }} onColorSelect={() => setMode(null)} /></MenuFrame></div>
|
||||
}
|
||||
export const AdditionalHover: Story = {
|
||||
name: "Additional Hover",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="추가 Hover 및 Selected" description="5개 상세 버튼 Hover와 목록 Selected 강조, 색상 기준 필수 선택. 실제 Hover·클릭으로 검증합니다." /><StyleDemo width={1920} onLayerDetail={() => console.info("[SWHeader] onLayerDetail")} initialColorMode="byLayer" /><RequiredColorDemo /></StoryPage>,
|
||||
}
|
||||
|
||||
const BAR_FIXTURE: SWHeaderProperty[] = [
|
||||
{ id: "radius", name: "반지름", value: "4.0526", type: "number", groupId: "shape", min: 0 },
|
||||
{ id: "diameter", name: "지름", value: "8.1052", type: "number", groupId: "shape", min: 0 },
|
||||
{ id: "circumference", name: "원주", value: "8.1052", type: "number", groupId: "shape", min: 0 },
|
||||
{ id: "area", name: "면적", value: "51.5962", type: "number", groupId: "shape", min: 0 },
|
||||
]
|
||||
function PropertyBarDemo() {
|
||||
const [selection, setSelection] = useState<SWHeaderPropertySelection>({ id: "circle", title: "원", kind: "single" })
|
||||
const [properties, setProperties] = useState(BAR_FIXTURE)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [controlled, setControlled] = useState(false)
|
||||
const [readOnly, setReadOnly] = useState(false)
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const change = (property: SWHeaderProperty, value: string) => {
|
||||
setProperties(items => items.map(item => item.id === property.id ? { ...item, value } : item))
|
||||
setLog(items => [...items, `${property.id}=${value}`])
|
||||
}
|
||||
const data = { selection, properties, onPropertyChange: change }
|
||||
return <div data-bar-demo="">
|
||||
<div className="sw-story-controls">
|
||||
<button onClick={() => setControlled(value => !value)}>Controlled 전환</button>
|
||||
<button onClick={() => setOpen(value => !value)}>외부 열림 변경</button>
|
||||
<button onClick={() => setProperties(items => [...items, { id: `extra-${items.length}`, name: "추가 속성", value: "10", type: "number", groupId: "shape", min: 0 }])}>Bar 필드 추가</button>
|
||||
<button onClick={() => setProperties(items => items.filter(item => item.id !== "radius"))}>Bar 반지름 삭제</button>
|
||||
<button onClick={() => setProperties(items => [...items].reverse().map(item => ({ ...item, order: undefined })))}>Bar 순서 반전</button>
|
||||
<button onClick={() => setProperties(items => items.map(item => ({ ...item, name: `${item.name} 변경`, value: "12.5" })))}>Bar 외부 값 변경</button>
|
||||
<button onClick={() => { setSelection({ id: "line", title: "직선", kind: "single" }); setProperties([{ id: "length", name: "길이", value: "25", type: "number", groupId: "shape", min: 0 }]) }}>Bar 객체 교체</button>
|
||||
<button onClick={() => setReadOnly(value => !value)}>Bar ReadOnly</button>
|
||||
<button onClick={() => setDisabled(value => !value)}>Bar Disabled</button>
|
||||
<button onClick={() => setProperties(items => items.map(item => ({ ...item, readOnly: item.id === "diameter", disabled: item.id === "area" })))}>Bar 개별 제한</button>
|
||||
</div>
|
||||
<p data-bar-log="">{log.join(" → ")}</p>
|
||||
<div className="sw-story-property-room">
|
||||
<StyleHeader propertyBar={{ ...data, readOnly, disabled }} propertyPanel={{ ...data, groups: [{ id: "shape", name: "형상" }] }}
|
||||
propertyPanelOpen={undefined} propertyBarOpen={controlled ? open : undefined} onPropertyBarOpenChange={next => { setOpen(next); setLog(items => [...items, `open=${next}`]) }} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
export const PropertyBar: Story = {
|
||||
name: "Property Bar",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage className="sw-story-followup"><StoryPageHeader kicker={KICKER} title="속성Bar · 동적 편집" description="Figma 26:10492. 속성창과 독립 열림, 동일 호스트 데이터. 수치는 Figma 예시이며 CAD 계산은 수행하지 않습니다." /><PropertyBarDemo />
|
||||
<StorySection title="콘텐츠 폭 기준 (부모 1222px, 높이 28)"><div className="sw-story-scroller"><SWTheme><div style={{ width: 1222 }}><SWHeaderPropertyBar selection={{ id: "reference", title: "원", kind: "single" }} properties={BAR_FIXTURE} /></div></SWTheme></div></StorySection>
|
||||
</StoryPage>,
|
||||
}
|
||||
|
||||
const CONTROL_FIXTURE: SWHeaderControlConfig[] = ["document", "drawingMode", "layer", "color", "lineStyle", "textStyle", "dimensionStyle", "details", "propertyBar", "propertyPanel", "print", "popupGuide"].map(id => ({ id: id as SWHeaderControlConfig["id"] }))
|
||||
function DynamicControlsDemo() {
|
||||
const [controls, setControls] = useState(CONTROL_FIXTURE)
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const [title, setTitle] = useState("현재 문서.dwg")
|
||||
const [documents, setDocuments] = useState<SWHeaderDocument[]>([{ id: "current", name: "현재 문서.dwg" }])
|
||||
return <div data-controls-demo="">
|
||||
<div className="sw-story-controls">
|
||||
{CONTROL_FIXTURE.map(item => <label key={item.id}><input type="checkbox" aria-label={`구성 ${item.id}`} checked={controls.some(control => control.id === item.id && control.visible !== false)} onChange={event => setControls(items => event.target.checked ? [...items.filter(control => control.id !== item.id), item] : items.filter(control => control.id !== item.id))} />{item.id}</label>)}
|
||||
<button onClick={() => setControls(CONTROL_FIXTURE)}>구성 초기화</button>
|
||||
<button onClick={() => setControls([])}>모두 제거</button>
|
||||
<button onClick={() => setControls([{ id: "layer" }, { id: "lineStyle" }])}>레이어·선스타일만</button>
|
||||
<button onClick={() => setControls(items => items.filter(item => item.id !== "document" && item.id !== "color"))}>파일·색상 제거</button>
|
||||
<button onClick={() => setControls(items => [...items].reverse())}>구성 순서 반전</button>
|
||||
<button onClick={() => setControls(items => items.map(item => ({ ...item, label: `${item.id} 변경` })))}>구성 라벨 변경</button>
|
||||
<button onClick={() => setControls(items => items.map(item => item.id === "color" ? { ...item, visible: false } : item))}>색상 숨김</button>
|
||||
<button onClick={() => setControls(items => items.map(item => ({ ...item, visible: true })))}>숨김 복원</button>
|
||||
</div>
|
||||
<p data-config-log="">{log.join(" → ")}</p>
|
||||
<div className="sw-story-property-room"><StyleHeader controls={controls} onLog={line => setLog(items => [...items, line])}
|
||||
propertyBarOpen={undefined} propertyPanelOpen={undefined}
|
||||
documentTitle={title} documents={documents} activeDocumentId="current" onDocumentSelect={document => { setTitle(document.name); setLog(items => [...items, `document=${document.id}`]) }}
|
||||
onDocumentNew={() => setDocuments(items => [...items, { id: `new-${items.length}`, name: `새 문서 ${items.length}` }])}
|
||||
propertyBar={{ selection: { id: "circle", title: "원", kind: "single" }, properties: BAR_FIXTURE }}
|
||||
propertyPanel={{ selection: { id: "circle", title: "원", kind: "single" }, properties: BAR_FIXTURE, groups: [{ id: "shape" }] }} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
export const DynamicControls: Story = {
|
||||
name: "Dynamic Controls",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage className="sw-story-followup"><StoryPageHeader kicker={KICKER} title="메뉴 컴포넌트 구성" description="Dropdown 내부 옵션과 별개인 controls allowlist. 항목 제거·재추가·숨김·순서·라벨, More 대표 슬롯과 값/Callback 보존." /><DynamicControlsDemo /></StoryPage>,
|
||||
}
|
||||
export const Colorize: Story = {
|
||||
name: "Colorize",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="btn_colorize" description="Figma 26:11302 / 26:11305의 원본 SVG, 24px 버튼과 18px 아이콘. 클릭은 기존 onColorPick 계약입니다. 색상 돋보기 Overlay 26:11338은 호스트 연결 대상입니다." /><StyleDemo width={1920} /></StoryPage>,
|
||||
}
|
||||
|
||||
/* 2026-10-16 composition review: every part except the logo can be removed (hiddenControls), the five
|
||||
similar menus can be reordered (menuOrder). Untouched props keep the approved default header. */
|
||||
const HIDEABLE_PARTS: { id: SWHeaderHideableControlId; name: string }[] = [
|
||||
{ id: "home", name: "홈" }, { id: "document", name: "파일 목록" }, { id: "drawingMode", name: "그리기모드" },
|
||||
{ id: "layer", name: "레이어" }, { id: "color", name: "색상" }, { id: "lineStyle", name: "선스타일" },
|
||||
{ id: "textStyle", name: "문자스타일" }, { id: "dimensionStyle", name: "치수스타일" }, { id: "details", name: "상세보기" },
|
||||
{ id: "propertyBar", name: "속성Bar" }, { id: "propertyPanel", name: "속성창" }, { id: "print", name: "인쇄" },
|
||||
{ id: "popupGuide", name: "K 가이드" }, { id: "windowMove", name: "창 이동" }, { id: "minimize", name: "최소화" }, { id: "close", name: "종료" },
|
||||
]
|
||||
const MENU_IDS: SWHeaderMenuId[] = ["layer", "color", "lineStyle", "textStyle", "dimensionStyle"]
|
||||
const COMPOSITION_PRESETS: { name: string; hidden: SWHeaderHideableControlId[]; order?: SWHeaderMenuId[] }[] = [
|
||||
{ name: "기본 전체", hidden: [] },
|
||||
{ name: "구성 A", hidden: ["drawingMode", "textStyle", "dimensionStyle", "details", "propertyBar", "propertyPanel", "print", "popupGuide", "windowMove", "minimize", "close"] },
|
||||
{ name: "구성 B (레이어·선스타일)", hidden: HIDEABLE_PARTS.map(part => part.id).filter(id => id !== "layer" && id !== "lineStyle") },
|
||||
{ name: "구성 C (로고만)", hidden: HIDEABLE_PARTS.map(part => part.id) },
|
||||
{ name: "파일 목록 제거", hidden: ["document"] },
|
||||
{ name: "레이어 제거", hidden: ["layer"] },
|
||||
{ name: "색상 제거", hidden: ["color"] },
|
||||
{ name: "스타일 그룹 제거", hidden: ["lineStyle", "textStyle", "dimensionStyle"] },
|
||||
{ name: "여러 메뉴 제거", hidden: ["document", "color", "textStyle", "print", "minimize"] },
|
||||
{ name: "순서 변경", hidden: [], order: ["color", "layer", "dimensionStyle", "lineStyle", "textStyle"] },
|
||||
{ name: "숨김 + 순서 변경", hidden: ["layer", "textStyle"], order: ["dimensionStyle", "textStyle", "color", "layer"] },
|
||||
]
|
||||
|
||||
function CompositionDemo() {
|
||||
const [hidden, setHidden] = useState<SWHeaderHideableControlId[]>([])
|
||||
const [order, setOrder] = useState<SWHeaderMenuId[] | undefined>(undefined)
|
||||
const [width, setWidth] = useState<number | null>(null)
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const record = (line: string) => setLog(lines => [...lines.slice(-11), line])
|
||||
const toggle = (id: SWHeaderHideableControlId) => setHidden(list => list.includes(id) ? list.filter(item => item !== id) : [...list, id])
|
||||
const header = <StyleHeader showState onLog={record} hiddenControls={hidden} menuOrder={order}
|
||||
onHomeClick={() => record("onHomeClick")} onMinimize={() => record("onMinimize")} onClose={() => record("onClose")}
|
||||
onPrint={() => record("onPrint")} onPopupGuide={() => record("onPopupGuide")}
|
||||
onPropertyBarOpenChange={next => record(`onPropertyBarOpenChange(${next})`)} onPropertyPanelOpenChange={next => record(`onPropertyPanelOpenChange(${next})`)}
|
||||
propertyBarOpen={undefined} propertyPanelOpen={undefined}
|
||||
propertyBar={{ selection: { id: "circle", title: "원", kind: "single" }, properties: BAR_FIXTURE }}
|
||||
propertyPanel={{ selection: { id: "circle", title: "원", kind: "single" }, properties: BAR_FIXTURE, groups: [{ id: "shape" }] }} />
|
||||
return <div data-composition-demo="" data-hidden={hidden.join(",")} data-order={order?.join(",") ?? "default"}>
|
||||
<div className="sw-story-controls"><strong>프리셋</strong>
|
||||
{COMPOSITION_PRESETS.map(preset => <button key={preset.name} onClick={() => { setHidden(preset.hidden); setOrder(preset.order) }}>{preset.name}</button>)}
|
||||
</div>
|
||||
<div className="sw-story-controls"><strong>표시 (로고 고정)</strong>
|
||||
{HIDEABLE_PARTS.map(part => <label key={part.id}><input type="checkbox" aria-label={`표시 ${part.id}`} checked={!hidden.includes(part.id)} onChange={() => toggle(part.id)} />{part.name}</label>)}
|
||||
</div>
|
||||
<div className="sw-story-controls"><strong>메뉴 순서</strong>
|
||||
<button onClick={() => setOrder(undefined)}>기본 순서</button>
|
||||
<button onClick={() => setOrder([...(order ?? MENU_IDS)].reverse())}>순서 반전</button>
|
||||
<button onClick={() => setOrder(list => { const items = [...(list ?? MENU_IDS)]; items.push(items.shift()!); return items })}>첫 메뉴를 끝으로</button>
|
||||
<button onClick={() => setOrder(["color", "color", "unknown" as SWHeaderMenuId, "layer"])}>중복·알 수 없는 ID</button>
|
||||
<code data-order-state="">{(order ?? ["(default)"]).join(" → ")}</code>
|
||||
</div>
|
||||
<div className="sw-story-controls"><strong>너비</strong>
|
||||
<button onClick={() => setWidth(null)}>실제 화면</button>
|
||||
{[1920, 1440, 1080, 900, 768, 640, 390].map(value => <button key={value} onClick={() => setWidth(value)}>{value}px</button>)}
|
||||
</div>
|
||||
<p className="sw-story-log" aria-live="polite" data-callback-log="">Callback: {log.length ? log.join(" → ") : "없음"}</p>
|
||||
<div className="sw-story-property-room">{width ? <Frame width={width}>{header}</Frame> : header}</div>
|
||||
</div>
|
||||
}
|
||||
export const Composition: Story = {
|
||||
name: "Composition (Visibility · Order)",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage className="sw-story-followup"><StoryPageHeader kicker={KICKER} title="Header 구성 · 메뉴 순서" description="hiddenControls로 로고 외 모든 구성요소를 개별 제거·재표시하고, menuOrder로 레이어·색상·선/문자/치수스타일 순서를 바꿉니다. 아무것도 지정하지 않으면 승인된 기본 디자인입니다. 값은 호스트 상태라 재표시·순서 변경 후에도 유지되며, 순서 변경은 Callback을 호출하지 않습니다." /><CompositionDemo /></StoryPage>,
|
||||
}
|
||||
|
||||
/* 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" },
|
||||
@@ -1320,109 +950,3 @@ export const PropertyPanelCompare: Story = {
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage className="sw-story-compare-page"><StoryPageHeader kicker={KICKER} title="속성창 4개 상태 비교" description="네 패널은 같은 SWHeaderPropertyPanel입니다. 실제 Header에서는 현재 선택 객체의 패널 하나만 표시합니다." /><PropertyCompare /></StoryPage>,
|
||||
}
|
||||
|
||||
/* Shape slot: replaceable area; the values stay in the property rows. */
|
||||
function SampleShapeViewer({ label, kind }: { label: string; kind: "circle" | "rect" }) {
|
||||
return <svg width="208" height="208" viewBox="0 0 208 208" role="img" aria-label={label} data-sample-shape={kind}>
|
||||
{kind === "circle" ? <circle cx="104" cy="104" r="84" fill="none" stroke="#1d5a8c" strokeWidth="2" strokeDasharray="6 4" />
|
||||
: <rect x="34" y="54" width="140" height="100" fill="none" stroke="#1d5a8c" strokeWidth="2" />}
|
||||
<text x="104" y="200" textAnchor="middle" fontSize="11" fill="#1d5a8c">{label}</text>
|
||||
</svg>
|
||||
}
|
||||
const SHAPE_MODES = ["default", "empty", "custom", "render"] as const
|
||||
const SHAPE_MODE_LABELS = { default: "기본 Figma 형상", empty: "비움 (null)", custom: "외부 컴포넌트", render: "Render Prop" }
|
||||
function ShapeSlotDemo() {
|
||||
const [mode, setMode] = useState<(typeof SHAPE_MODES)[number]>("default")
|
||||
const [kind, setKind] = useState<"circle" | "rect">("circle")
|
||||
const [properties, setProperties] = useState(() => propertyFixture("circle"))
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const shapeContent: SWHeaderPropertyShapeContent = mode === "default" ? undefined : mode === "empty" ? null
|
||||
: mode === "custom" ? <SampleShapeViewer label="외부 형상 컴포넌트" kind={kind} />
|
||||
: context => <SampleShapeViewer label={`${context.selection.title} · ${context.properties.length}개 값`} kind={kind} />
|
||||
return <div data-shape-demo="" data-mode={mode} data-kind={kind}>
|
||||
<div className="sw-story-controls"><strong>형상 Slot</strong>
|
||||
{SHAPE_MODES.map(name => <button key={name} data-shape-mode={name} onClick={() => setMode(name)}>{SHAPE_MODE_LABELS[name]}</button>)}
|
||||
<button onClick={() => setKind(value => value === "circle" ? "rect" : "circle")}>형상 교체 (원 ↔ 사각형)</button>
|
||||
</div>
|
||||
<div className="sw-story-controls"><strong>속성값 (형상과 독립)</strong>
|
||||
<button onClick={() => setProperties(items => items.map(item => item.id === "diameter" ? { ...item, value: "10.0000" } : item.id === "radius" ? { ...item, value: "5.0000" } : item))}>지름·반지름 외부 변경</button>
|
||||
<button onClick={() => setProperties(items => [...items, { id: `shape-extra-${items.length}`, groupId: "shape", name: "추가 형상값", type: "text", value: "호스트 값" }])}>형상 그룹 항목 추가</button>
|
||||
<button onClick={() => setProperties(items => items.filter(item => item.id !== "area"))}>면적 삭제</button>
|
||||
</div>
|
||||
<p className="sw-story-log" data-property-log="">{log.join(" → ") || "Callback 없음"}</p>
|
||||
<div className="sw-story-property-state"><SWTheme><SWHeaderPropertyPanel selection={propertySelection("circle")} properties={properties} groups={PROPERTY_GROUPS} shapeContent={shapeContent}
|
||||
onPropertyChange={(property, value) => { setLog(lines => [...lines.slice(-9), `property(${property.id}, ${value})`]); setProperties(items => items.map(item => item.id === property.id ? { ...item, value } : item)) }} /></SWTheme></div>
|
||||
</div>
|
||||
}
|
||||
export const PropertyShapeSlot: Story = {
|
||||
name: "Property Panel · 형상 Slot",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage><StoryPageHeader kicker={KICKER} title="형상 영역 Slot" description="shapeContent: 미지정은 Figma 원형 그림, null은 208 × 208 빈 영역(치수 라벨·패널 높이 유지), ReactNode·Render Prop은 외부 형상 컴포넌트입니다. 예시 SVG는 Storybook 전용 샘플이며 패키지에 포함되지 않습니다. 원주·지름·반지름·면적·위치 값은 properties로만 표시됩니다." /><ShapeSlotDemo /></StoryPage>,
|
||||
}
|
||||
|
||||
/* Property fields (2026-10-19): the 13 Figma field nodes in the real SWHeaderPropertyPanel rows.
|
||||
Default / pinned Hover columns are specimens; the live panel is the one to click (On / dropdown). */
|
||||
const FIELD_NODES = [
|
||||
{ node: "26:12388 / 26:12404", name: "선스타일", states: "Default · Hover / On(Dropdown)" },
|
||||
{ node: "26:12250 / 26:12265", name: "레이어 — 다양함", states: "Default · Hover / On(Dropdown)" },
|
||||
{ node: "26:12260 / 26:12291", name: "레이어 — 단일 값", states: "Default · Hover / On(Dropdown)" },
|
||||
{ node: "26:12314", name: "선축적", states: "Default · Hover · On(편집)" },
|
||||
{ node: "26:12649", name: "선가중치 · 투명도", states: "Default · Hover · On(편집)" },
|
||||
{ node: "26:12170 / 26:12184", name: "색상", states: "Default · Hover / On(Dropdown)" },
|
||||
{ node: "26:12583", name: "하이퍼링크", states: "Default · Hover (Figma On 없음)" },
|
||||
{ node: "26:12590", name: "두께", states: "Default · Hover · On(편집)" },
|
||||
{ node: "26:12574", name: "재료", states: "Default · Hover (Figma On 없음)" },
|
||||
]
|
||||
// The common rows of PROPERTY_FIXTURE plus a mixed layer row. Figma sample text 255,259,0 is out of range:
|
||||
// shown as host text only, never accepted as input.
|
||||
const FIELD_FIXTURE: SWHeaderProperty[] = PROPERTY_FIXTURE.filter(item => item.groupId === "common").flatMap(item =>
|
||||
item.id === "layer" ? [{ ...item, id: "layer-mixed", mixed: true }, item]
|
||||
: item.id === "color" ? [{ ...item, value: "255,259,0", swatch: "#ffe500" }]
|
||||
: [item])
|
||||
const FIELD_HOVER = [".hm-sw-property__value[data-field]", ".hm-sw-property__compact"]
|
||||
function FieldPanel({ title, properties, onChange, menus }: { title: string; properties: SWHeaderProperty[]; onChange?: (property: SWHeaderProperty, value: string) => void; menus?: SWHeaderPropertyMenus }) {
|
||||
return <SWTheme><SWHeaderPropertyPanel selection={{ id: title, title, kind: "none" }} properties={properties} groups={[{ id: "common" }]} onPropertyChange={onChange} menus={menus} /></SWTheme>
|
||||
}
|
||||
function PropertyFieldsDemo() {
|
||||
const [properties, setProperties] = useState(FIELD_FIXTURE)
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const record = (line: string) => setLog(lines => [...lines.slice(-11), line])
|
||||
const menus = usePropertyMenus(record, setProperties as (update: (items: SWHeaderProperty[]) => SWHeaderProperty[]) => void)
|
||||
const [noMenus, setNoMenus] = useState(false)
|
||||
const patch = (id: string, change: Partial<SWHeaderProperty>) => setProperties(items => items.map(item => item.id === id ? { ...item, ...change } : item))
|
||||
const change = (property: SWHeaderProperty, value: string) => {
|
||||
record(`property(${property.id}, ${value})`)
|
||||
patch(property.id, { value, mixed: false, ...(property.id === "line" ? { linePattern: linePatternOf(value) } : {}), ...(property.id === "color" ? { swatch: undefined } : {}) })
|
||||
}
|
||||
return <div data-fields-demo="">
|
||||
<div className="sw-story-controls"><strong>외부 데이터</strong>
|
||||
<button data-action="layer-mixed" onClick={() => patch("layer-mixed", { mixed: !properties.find(item => item.id === "layer-mixed")?.mixed })}>레이어 다양함 ↔ 단일</button>
|
||||
<button data-action="layer-external" onClick={() => patch("layer", { value: "CC-D GNL" })}>레이어 외부 값 변경</button>
|
||||
<button data-action="scale-external" onClick={() => patch("scale", { value: "2.5000" })}>선축적 외부 값 변경</button>
|
||||
<button data-action="thickness-unit" onClick={() => patch("thickness", { unit: properties.find(item => item.id === "thickness")?.unit === "mm" ? "cm" : "mm" })}>두께 단위 변경</button>
|
||||
<button data-action="line-center" onClick={() => patch("line", { value: "CENTER", linePattern: linePatternOf("CENTER") })}>선스타일 외부 CENTER</button>
|
||||
<button data-action="readonly" onClick={() => setProperties(items => items.map(item => item.id === "thickness" || item.id === "layer" ? { ...item, readOnly: !item.readOnly } : item))}>두께·레이어 ReadOnly</button>
|
||||
<button data-action="disabled" onClick={() => setProperties(items => items.map(item => item.id === "scale" || item.id === "color" ? { ...item, disabled: !item.disabled } : item))}>선축적·색상 Disabled</button>
|
||||
<button data-action="add-row" onClick={() => setProperties(items => [...items, { id: `extra-${items.length}`, name: "추가 속성", value: "호스트 값", type: "text", groupId: "common" }])}>속성 추가</button>
|
||||
<button data-action="remove-row" onClick={() => setProperties(items => items.filter(item => item.id !== "link"))}>하이퍼링크 삭제</button>
|
||||
<button data-action="no-menus" onClick={() => setNoMenus(value => !value)}>menus 없음(options 사용): {String(noMenus)}</button>
|
||||
</div>
|
||||
<p className="sw-story-log" data-property-log="" aria-live="polite">{log.join(" → ") || "Callback 없음"}</p>
|
||||
<pre className="sw-story-layer-state" data-property-state="">{JSON.stringify(properties.map(item => [item.id, item.mixed ? "다양함" : item.value]))}</pre>
|
||||
<div className="sw-story-compare">
|
||||
<figure className="sw-story-compare__cell" data-fields="live"><figcaption><strong>실제 조작</strong><span>클릭 → On · Enter 확정 · Escape 취소/닫기</span></figcaption>
|
||||
<FieldPanel title="실제 조작" properties={properties} onChange={change} menus={noMenus ? undefined : menus} /></figure>
|
||||
<figure className="sw-story-compare__cell" data-fields="default"><figcaption><strong>Default</strong><span>Figma Default</span></figcaption>
|
||||
<FieldPanel title="Default" properties={FIELD_FIXTURE} /></figure>
|
||||
<figure className="sw-story-compare__cell" data-fields="hover"><figcaption><strong>Hover (고정)</strong><span>Figma Hover · 값 변경 없음</span></figcaption>
|
||||
<PinHover selectors={FIELD_HOVER}><FieldPanel title="Hover" properties={FIELD_FIXTURE} /></PinHover></figure>
|
||||
</div>
|
||||
<table className="sw-story-node-table"><thead><tr><th>Figma</th><th>항목</th><th>상태</th></tr></thead>
|
||||
<tbody>{FIELD_NODES.map(item => <tr key={item.node}><td>{item.node}</td><td>{item.name}</td><td>{item.states}</td></tr>)}</tbody></table>
|
||||
</div>
|
||||
}
|
||||
export const PropertyFields: Story = {
|
||||
name: "Property Panel · 필드 13개",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <StoryPage className="sw-story-followup"><StoryPageHeader kicker={KICKER} title="속성창 입력·선택 필드" description="Figma 13개 필드(26:12388 ~ 26:12649)를 기존 SWHeaderPropertyPanel 행으로 표시합니다. Default · Hover 고정 표본과 실제 조작 패널입니다. 숫자는 편집 On(#0F0D0A), 레이어·선스타일·색상은 Dropdown On(#211D13)이며 하이퍼링크·재료는 Figma에 On이 없어 Default · Hover만 있습니다. 색상 255,259,0은 Figma 표기 예시로, 입력 검증은 0~255를 유지합니다." /><PropertyFieldsDemo /></StoryPage>,
|
||||
}
|
||||
@@ -15,33 +15,18 @@
|
||||
runtime dropdown still clamps to the real viewport. Scroll the specimen to see its right edge. */
|
||||
.sw-story-fixed-dimension .hm-sw-header__list-menu--dimension-style { left: auto !important; right: 0; }
|
||||
.sw-story-fixed-dimension .hm-sw-header__list-menu--dimension-style[data-shifted="true"]::before { top: calc(-1 * var(--hm-sw-header-field-height) - 1px); }
|
||||
.sw-story-explicit-disabled { min-width: 0; margin: 0; padding: 0; border: 0; }
|
||||
.sw-story-log { margin: 0 0 8px; font-size: 13px; color: #3d4a45; }
|
||||
.sw-story-stack { display: flex; flex-direction: column; gap: 8px; }
|
||||
/* Window Move Motion: window controls alone on the window-area surface, magnified for comparison. */
|
||||
.sw-story-window { display: flex; width: 152px; height: 48px; background: var(--hm-sw-header-document-background); }
|
||||
.sw-story-zoom { zoom: 3; width: max-content; }
|
||||
.sw-story-row { display: flex; flex-wrap: wrap; gap: 24px; }
|
||||
.sw-story-stages { display: grid; grid-template-columns: repeat(auto-fill, minmax(480px, 1fr)); gap: 16px; }
|
||||
.sw-story-stage { margin: 0; }
|
||||
.sw-story-stage figcaption { margin-top: 6px; font-size: 13px; color: #3d4a45; }
|
||||
/* Layer Dropdown: room below a standalone layer field for its overlay list. */
|
||||
.sw-story-layer-room { padding-bottom: 300px; }
|
||||
.sw-story-layer-state { margin: 0 0 8px; font-size: 12px; color: #3d4a45; font-family: ui-monospace, monospace; white-space: pre-wrap; }
|
||||
/* Field dropdowns / Dynamic Data: story-only controls and Figma reference images. */
|
||||
.sw-story-reference { display: block; max-width: none; image-rendering: pixelated; }
|
||||
.sw-story-figma { padding: 12px; background: #27241d; }
|
||||
.sw-story-figma .sw-story-reference { zoom: 2; }
|
||||
.sw-story-detail { background: var(--hm-sw-header-layer-menu-background); padding: 8px 12px; gap: 12px; }
|
||||
.sw-story-dynamic { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; }
|
||||
.sw-story-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 12px; color: #3d4a45; }
|
||||
.sw-story-controls > strong { min-width: 110px; }
|
||||
.sw-story-controls > button { padding: 2px 8px; border: 1px solid #b6c4bf; border-radius: 4px; background: #fff; color: #1f2a26; font-size: 12px; cursor: pointer; }
|
||||
.sw-story-property-room { min-height: 820px; }
|
||||
/* New responsive scenarios measure the Header at the actual viewport width. */
|
||||
.sw-story-followup.sb-page > .sb-page-inner { width: 100%; }
|
||||
/* Property panel states (2026-10-16): standalone panels at their natural size; the comparison wraps. */
|
||||
.sw-story-property-state { display: inline-block; }
|
||||
.sw-story-compare { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px; }
|
||||
.sw-story-compare__cell { margin: 0; }
|
||||
.sw-story-compare__cell figcaption { display: flex; flex-direction: column; gap: 2px; margin-bottom: 8px; font-size: 12px; color: #3d4a45; }
|
||||
@@ -59,9 +44,6 @@
|
||||
.sw-story-detail-comparison { overflow-x: auto; padding: 16px; background: var(--hm-sw-header-layer-menu-background); }
|
||||
.sw-story-detail-row { display: grid; grid-template-columns: 140px repeat(4, 180px); align-items: center; gap: 16px; min-width: 924px; padding: 12px 0; }
|
||||
.sw-story-detail-row > div { display: flex; align-items: center; gap: 8px; }
|
||||
/* Property fields (2026-10-19): Figma node table under the specimens. */
|
||||
.sw-story-node-table { margin-top: 16px; border-collapse: collapse; font-size: 12px; color: #3d4a45; }
|
||||
.sw-story-node-table :is(th, td) { padding: 4px 10px; border-bottom: 1px solid #d5ddd9; text-align: left; }
|
||||
|
||||
/* Overview host layout only. Native controls below are Storybook data controls, outside the product. */
|
||||
.sw-story-overview.sb-page { height: 100dvh; min-height: 0; padding-block: 16px; }
|
||||
|
||||
Reference in new issue
Block a user