refactor(storybook): consolidate dropdown stories into composed controls
This commit is contained in:
1 parent
c165753bbe
commit
9482d45f34
74 files changed
+38833
-283
No files matched your search
@@ -0,0 +1,8 @@
|
||||
/* Story-owned composition only. Public controls retain package sizes and styling. */
|
||||
.sw-composed-controls .sw-story-composed { flex-wrap: wrap; }
|
||||
.sw-composed-controls__settings { margin-bottom: 12px; font-size: 13px; }
|
||||
.sw-composed-controls__settings summary { width: max-content; cursor: pointer; }
|
||||
.sw-composed-controls__settings-content { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; padding-block: 8px; }
|
||||
.sw-composed-controls__settings-content label { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.sw-composed-controls__status { margin-top: 12px; font-size: 12px; overflow-wrap: anywhere; }
|
||||
.sw-composed-controls__status output { display: block; }
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import { ComposedDemo } from "./sw-header.stories"
|
||||
import "./sw-composed-controls.css"
|
||||
|
||||
const meta = {
|
||||
title: "SW/Header/Dropdown",
|
||||
// Keep existing bookmarks while placing the single example under Dropdown.
|
||||
id: "sw-header",
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
} satisfies Meta
|
||||
export default meta
|
||||
|
||||
export const ComposedControls: StoryObj<typeof meta> = {
|
||||
name: "Composed Controls",
|
||||
render: () => <ComposedDemo />,
|
||||
}
|
||||
+122
-263
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react"
|
||||
import { useEffect, useId, useRef, useState, type ReactNode } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import {
|
||||
SWHeader, SWHeaderColorControl, SWHeaderLayerControl, SWHeaderLineStyle, SWHeaderTextStyle, SWHeaderDimensionStyle,
|
||||
SWHeader, SWHeaderColorMenu, SWHeaderLineStyleMenu, SWHeaderTextStyleMenu, SWHeaderDimensionStyleMenu,
|
||||
SWHeaderProperties, SWHeaderWindowControls, SWTheme, SWHeaderLayerMenu, SWHeaderInput, parseSWHeaderColor, swHeaderLinePatterns, SWHeaderPropertyPanel, SWHeaderPropertyBar,
|
||||
type SWHeaderColorMode, type SWHeaderColorOption, type SWHeaderColorValue, type SWHeaderDimensionStyleOption,
|
||||
type SWHeaderDocument, type SWHeaderLayer, type SWHeaderLineStyleValue, type SWHeaderLineType, type SWHeaderLineWeight,
|
||||
@@ -65,6 +65,7 @@ const callbacks = {
|
||||
|
||||
const meta = {
|
||||
title: "SW/Header",
|
||||
excludeStories: ["ComposedDemo"],
|
||||
component: SWHeader,
|
||||
parameters: { layout: "fullscreen" },
|
||||
args: { ...figmaArgs, ...callbacks },
|
||||
@@ -302,48 +303,126 @@ export const WindowMoveMotion: Story = {
|
||||
),
|
||||
}
|
||||
|
||||
export const ComposedControls: Story = {
|
||||
name: "Composed Controls",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => <ComposedDemo />,
|
||||
}
|
||||
|
||||
function ComposedDemo() {
|
||||
// ComposedDemo is exported for the Dropdown CSF file; exclude it from this file's Story index.
|
||||
export function ComposedDemo() {
|
||||
const panelId = useId()
|
||||
const detailRef = useRef<HTMLDivElement>(null)
|
||||
const [activeMenu, setActiveMenu] = useState<SWHeaderMenuId | null>(null)
|
||||
const [scenario, setScenario] = useState("normal")
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const [layerReadOnly, setLayerReadOnly] = useState(false)
|
||||
const [detailCallbacks, setDetailCallbacks] = useState(true)
|
||||
const [layers, setLayers] = useState(SAMPLE_LAYERS)
|
||||
const [weights, setWeights] = useState(FIGMA_LINE_WEIGHTS)
|
||||
const [weightId, setWeightId] = useState("w-default")
|
||||
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>(null)
|
||||
const [textStyle, setTextStyle] = useState(figmaArgs.textStyle!)
|
||||
const [dimensionStyle, setDimensionStyle] = useState(figmaArgs.dimensionStyle!)
|
||||
const [properties, setProperties] = useState(() => propertyFixture("circle").map(item => item.id === "layer" ? { ...item, value: "0" } : item))
|
||||
const [bar, setBar] = useState(false)
|
||||
const [panel, setPanel] = useState(false)
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="Composed Controls" description="공개 하위 컴포넌트만 골라 다른 SW 화면에 재사용하는 예입니다. SWTheme이 SW 토큰·글꼴·버튼 초기화 범위를 제공하므로 소비자가 hm-* class를 붙이지 않습니다. 녹색 면은 예제 배치용 Story 배경입니다." />
|
||||
<StorySection title="레이어 · 색상 · 상세보기">
|
||||
<SWTheme className="sw-story-composed">
|
||||
<SWHeaderLayerControl value={{ name: "0_Layer_Cludmark" }} />
|
||||
<SWHeaderColorControl value={{ value: "255,229,0", swatch: "#ffe500" }} selected />
|
||||
<SWHeaderProperties panelOpen={panel} onPanelToggle={() => setPanel(value => !value)} />
|
||||
</SWTheme>
|
||||
</StorySection>
|
||||
<StorySection title="선스타일 · 문자스타일 · 치수스타일" description="필드 전체 Hover 시 라벨·값·선/폰트/치수 미리보기가 #FFC600입니다. 선의 검정 border와 치수 SVG의 검정 외곽선은 유지합니다.">
|
||||
<div className="sw-story-scroller">
|
||||
<SWTheme className="sw-story-composed">
|
||||
<SWHeaderLineStyle value={figmaArgs.lineStyle!} onClick={callbacks.onLineStyleOpen} />
|
||||
<SWHeaderTextStyle value={figmaArgs.textStyle!} onClick={callbacks.onTextStyleOpen} />
|
||||
<SWHeaderDimensionStyle value={figmaArgs.dimensionStyle!} onClick={callbacks.onDimensionStyleOpen} />
|
||||
</SWTheme>
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
// The overlay positions itself on mount; focus after that initial hidden layout is resolved.
|
||||
useEffect(() => {
|
||||
if (!panel) return
|
||||
const frame = requestAnimationFrame(() => document.getElementById(panelId)?.focus())
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [panel, panelId])
|
||||
const record = (line: string) => { setLog(lines => [...lines.slice(-11), line]); console.info(`[SWHeader Composed] ${line}`) }
|
||||
const value = (id: string) => properties.find(item => item.id === id)?.value ?? ""
|
||||
const change = (id: string, next: string, event: string) => {
|
||||
setProperties(items => items.map(item => item.id === id ? { ...item, value: next, mixed: false } : item))
|
||||
if (id === "color") setColorMode(null)
|
||||
record(`${event}(${id}, ${next})`)
|
||||
}
|
||||
const availableLayers = scenario === "empty" ? [] : scenario === "long" ? LONG_LAYERS : scenario === "many" ? MANY_LAYERS : layers
|
||||
const currentLayer = availableLayers.find(item => item.name === value("layer"))
|
||||
const currentWeight = weights.find(item => item.id === weightId)
|
||||
const color = parseSWHeaderColor(value("color"))
|
||||
const menuProps = (id: SWHeaderMenuId) => ({
|
||||
open: activeMenu === id, disabled,
|
||||
onClick: () => record(`on${id[0].toUpperCase() + id.slice(1)}Open`),
|
||||
onOpenChange: (open: boolean) => {
|
||||
setActiveMenu(current => open ? id : current === id ? null : current)
|
||||
record(`${id}Open(${open})`)
|
||||
},
|
||||
})
|
||||
const detail = (name: string) => detailCallbacks ? () => record(`on${name}Detail`) : undefined
|
||||
const toggleLayer = (key: "visible" | "frozen" | "locked", event: string) => layerReadOnly ? undefined : (layer: SWHeaderLayer, next: boolean) => {
|
||||
setLayers(items => items.map(item => item.id === layer.id || item.name === layer.id ? { ...item, [key]: next } : item))
|
||||
record(`${event}(${layer.id}, ${next})`)
|
||||
}
|
||||
const chooseWeight = (weight: SWHeaderLineWeight) => {
|
||||
setWeightId(weight.id)
|
||||
if (weight.value !== undefined) change("weight", weight.value, "onLineWeightSelect")
|
||||
else record(`onLineWeightSelect(${weight.id})`)
|
||||
}
|
||||
const changeWeight = (weight: SWHeaderLineWeight, next: string) => {
|
||||
setWeights(items => items.map(item => item.id === weight.id ? { ...item, value: next } : item))
|
||||
if (weightId === weight.id) change("weight", next, "onLineWeightValueChange")
|
||||
else record(`onLineWeightValueChange(${weight.id}, ${next})`)
|
||||
}
|
||||
const closePanel = () => {
|
||||
setPanel(false); record("onPropertyPanelOpenChange(false)")
|
||||
detailRef.current?.querySelector<HTMLButtonElement>("[data-property-panel-trigger]")?.focus()
|
||||
}
|
||||
const menus: SWHeaderPropertyMenus = {
|
||||
layers: availableLayers, lineTypes: PROPERTY_LINE_TYPES, lineWeights: weights, activeLineWeightId: weightId, lineScale: value("scale"),
|
||||
colors: FIGMA_COLORS, userColors: scenario === "many" ? MANY_USER_COLORS : FIGMA_USER_COLORS, colorMode,
|
||||
onLayerVisibilityChange: toggleLayer("visible", "onLayerVisibilityChange"), onLayerFreezeChange: toggleLayer("frozen", "onLayerFreezeChange"), onLayerLockChange: toggleLayer("locked", "onLayerLockChange"),
|
||||
onLayerDetail: detail("Layer"), onLineStyleDetail: detail("LineStyle"), onColorDetail: detail("Color"),
|
||||
onLineWeightSelect: chooseWeight, onLineWeightValueChange: changeWeight, onLineScaleChange: next => change("scale", next, "onLineScaleChange"),
|
||||
onColorModeChange: mode => { setColorMode(mode); record(`onColorModeChange(${mode})`) }, onColorPick: () => record("onColorPick"),
|
||||
}
|
||||
const panelProperties = properties.map(item => ({ ...item, disabled, ...(item.id === "layer" ? { options: availableLayers.map(layer => ({ ...layer, id: layer.name })) } : {}) }))
|
||||
return <StoryPage className="sw-composed-controls">
|
||||
<StoryPageHeader kicker={KICKER} title="Composed Controls" description="SW Header의 레이어·색상·선스타일·문자스타일·치수스타일과 상세보기를 직접 조작합니다. 메뉴의 상세 버튼은 기존 호스트 Callback을 호출하며 CAD 상세 화면은 생성하지 않습니다." />
|
||||
<details className="sw-composed-controls__settings">
|
||||
<summary>시연 설정</summary>
|
||||
<div className="sw-composed-controls__settings-content">
|
||||
<label>목록 데이터 <select value={scenario} onChange={event => { setActiveMenu(null); setScenario(event.target.value) }}>
|
||||
<option value="normal">기본</option><option value="long">긴 레이어명</option><option value="many">레이어 20개 · 사용자 색상 40개</option><option value="empty">빈 레이어 목록</option>
|
||||
</select></label>
|
||||
<label><input type="checkbox" checked={disabled} onChange={event => { setActiveMenu(null); setDisabled(event.target.checked) }} />컨트롤 Disabled</label>
|
||||
<label><input type="checkbox" checked={layerReadOnly} onChange={event => setLayerReadOnly(event.target.checked)} />레이어 상태 표시 전용</label>
|
||||
<label><input type="checkbox" checked={detailCallbacks} onChange={event => setDetailCallbacks(event.target.checked)} />메뉴 상세 Callback 연결</label>
|
||||
</div>
|
||||
</details>
|
||||
<SWTheme>
|
||||
<div className="sw-story-composed" role="group" aria-label="조합 컨트롤" data-composed-controls="">
|
||||
<SWHeaderLayerMenu {...menuProps("layer")} value={{ name: value("layer") }} layers={availableLayers} activeLayerId={currentLayer?.id}
|
||||
onLayerSelect={layer => change("layer", layer.name, "onLayerSelect")} onDetail={detail("Layer")}
|
||||
onVisibilityChange={scenario === "normal" ? menus.onLayerVisibilityChange : undefined} onFreezeChange={scenario === "normal" ? menus.onLayerFreezeChange : undefined} onLockChange={scenario === "normal" ? menus.onLayerLockChange : undefined} />
|
||||
<SWHeaderColorMenu {...menuProps("color")} value={{ value: colorMode === "byLayer" ? "ByLayer" : colorMode === "byBlock" ? "ByBlock" : value("color"), swatch: color?.hex }}
|
||||
colors={FIGMA_COLORS} userColors={menus.userColors} mode={colorMode}
|
||||
onColorSelect={item => change("color", item.value ?? parseSWHeaderColor(item.color)?.value ?? item.color, "onColorSelect")}
|
||||
onColorInput={next => change("color", next.value, "onColorInput")} onModeChange={mode => { setColorMode(mode); record(`onColorModeChange(${mode})`) }}
|
||||
onPick={() => record("onColorPick")} onDetail={detail("Color")} />
|
||||
<SWHeaderLineStyleMenu {...menuProps("lineStyle")} value={{ name: value("line"), weight: currentWeight?.value === undefined ? currentWeight?.name ?? value("weight") : value("weight"), scale: value("scale") }}
|
||||
lineTypes={FIGMA_LINE_TYPES} lineWeights={weights} activeLineWeightId={weightId}
|
||||
onLineTypeSelect={item => change("line", item.name, "onLineTypeSelect")} onLineWeightSelect={chooseWeight} onLineWeightValueChange={changeWeight}
|
||||
onScaleChange={next => change("scale", next, "onLineScaleChange")} onDetail={detail("LineStyle")} />
|
||||
<SWHeaderTextStyleMenu {...menuProps("textStyle")} value={textStyle} textStyles={FIGMA_TEXT_STYLES}
|
||||
onTextStyleSelect={item => { setTextStyle({ font: item.font, name: item.name }); record(`onTextStyleSelect(${item.id})`) }} onDetail={detail("TextStyle")} />
|
||||
<SWHeaderDimensionStyleMenu {...menuProps("dimensionStyle")} value={dimensionStyle} dimensionStyles={FIGMA_DIMENSION_STYLES}
|
||||
onDimensionStyleSelect={item => { setDimensionStyle({ name: item.name }); record(`onDimensionStyleSelect(${item.id})`) }} onDetail={detail("DimensionStyle")} />
|
||||
<div ref={detailRef}>
|
||||
<SWHeaderProperties disabled={disabled} barOpen={bar} panelOpen={panel} panelId={panelId}
|
||||
onBarToggle={() => { setActiveMenu(null); setBar(next => !next); record(`onPropertyBarOpenChange(${!bar})`) }}
|
||||
onPanelToggle={() => { setActiveMenu(null); setPanel(next => !next); record(`onPropertyPanelOpenChange(${!panel})`) }} />
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="그룹 필드 Hover (고정)" description="기존 노란 Gradient Border에 텍스트·지정 미리보기 강조만 더합니다. 색상 견본과 레이어 상태는 원래 색을 유지하며, 상세보기는 기존 텍스트 전용 Hover 그대로입니다.">
|
||||
<div className="sw-story-scroller">
|
||||
<PinHover selectors={[".hm-sw-header__field"]}>
|
||||
<SWTheme className="sw-story-composed">
|
||||
<SWHeaderLayerControl value={figmaArgs.layer!} />
|
||||
<SWHeaderColorControl value={figmaArgs.color!} />
|
||||
<SWHeaderLineStyle value={figmaArgs.lineStyle!} />
|
||||
<SWHeaderTextStyle value={figmaArgs.textStyle!} />
|
||||
<SWHeaderDimensionStyle value={figmaArgs.dimensionStyle!} />
|
||||
</SWTheme>
|
||||
</PinHover>
|
||||
</div>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
)
|
||||
</div>
|
||||
{bar && <SWHeaderPropertyBar selection={propertySelection("circle")} properties={panelProperties.filter(item => BAR_FIXTURE.some(barItem => barItem.id === item.id))}
|
||||
onPropertyChange={(property, next) => change(property.id, next, "onPropertyChange")} />}
|
||||
{panel && <SWHeaderPropertyPanel id={panelId} anchorRef={detailRef} selection={propertySelection("circle")} properties={panelProperties} groups={PROPERTY_GROUPS}
|
||||
menus={{ ...menus, layers: availableLayers.map(layer => ({ ...layer, id: layer.name })) }}
|
||||
onPropertyChange={(property, next) => change(property.id, next, "onPropertyChange")} onClose={closePanel} />}
|
||||
</SWTheme>
|
||||
<div className="sw-composed-controls__status">
|
||||
<output data-style-state="">레이어: {value("layer")} · 색상: {colorMode ?? value("color")} · 선: {value("line")} / {value("weight")} / {value("scale")} · 문자: {textStyle.name} · 치수: {dimensionStyle.name}</output>
|
||||
<output aria-live="polite" data-callback-log="">Callback: {log.length ? log.join(" → ") : "없음"}</output>
|
||||
</div>
|
||||
</StoryPage>
|
||||
}
|
||||
|
||||
/** Neutral sample logos with different ratios (no product or brand). */
|
||||
@@ -558,16 +637,7 @@ const MANY_LAYERS: SWHeaderLayer[] = Array.from({ length: 20 }, (_, index) => ({
|
||||
frozen: index % 5 === 2,
|
||||
locked: index % 6 === 3,
|
||||
}))
|
||||
/**
|
||||
* Icon variant matrix: rows 1-2 colored (visible, thawed, unlocked), rows 3-4 gray (hidden, frozen,
|
||||
* locked); rows 2 and 4 get pinned icon hover.
|
||||
*/
|
||||
const ICON_LAYERS: SWHeaderLayer[] = [
|
||||
{ id: "icon-on", name: "컬러 (Default)", visible: true, frozen: false, locked: false },
|
||||
{ id: "icon-on-hover", name: "컬러 Hover (Variant2)", visible: true, frozen: false, locked: false },
|
||||
{ id: "icon-off", name: "회색 (Variant3)", visible: false, frozen: true, locked: true },
|
||||
{ id: "icon-off-hover", name: "회색 Hover (Variant4)", visible: false, frozen: true, locked: true },
|
||||
]
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
type LayerHostProps = Partial<SWHeaderProps> & { onLog?: (line: string) => void; showState?: boolean }
|
||||
@@ -618,94 +688,6 @@ function LayerDemo({ width, ...props }: LayerHostProps & { width: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Standalone open layer menu: the 12 icon variants (rows 2 and 4 pin the icon hover). */
|
||||
function IconMatrix() {
|
||||
return (
|
||||
<PinHover selectors={[".hm-sw-header__layer-row:nth-child(2) .hm-sw-header__layer-toggle", ".hm-sw-header__layer-row:nth-child(4) .hm-sw-header__layer-toggle"]}>
|
||||
<div className="sw-story-zoom sw-story-layer-room">
|
||||
<SWTheme className="sw-story-composed">
|
||||
<SWHeaderLayerMenu
|
||||
value={{ name: "아이콘 Variant" }}
|
||||
layers={ICON_LAYERS}
|
||||
open
|
||||
onVisibilityChange={noop}
|
||||
onFreezeChange={noop}
|
||||
onLockChange={noop}
|
||||
/>
|
||||
</SWTheme>
|
||||
</div>
|
||||
</PinHover>
|
||||
)
|
||||
}
|
||||
|
||||
const NARROW_LAYER_WIDTHS = [1440, 1080, 900, 768, 640, 390]
|
||||
|
||||
export const LayerDropdown: Story = {
|
||||
name: "Layer Dropdown",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={KICKER}
|
||||
title="Layer Dropdown"
|
||||
description="레이어 필드를 클릭하면 Figma header/menu/레이어(26:10940)의 레이어 목록이 필드 아래로 겹쳐 열립니다. Hover만으로는 열리지 않습니다. 행은 header/dropdown/brown/row(26:10687), 아이콘은 icon_전구(26:10810)·icon_눈꽃(26:10855)·icon_자물쇠(26:10912)입니다. 레이어 목록과 각 상태는 호스트 데이터이며 아래 레이어명은 테스트용 예시입니다."
|
||||
/>
|
||||
<StorySection title="Layer Default" description="layers를 전달한 기본 상태. 필드 234 × 38, 기존 테두리·그림자, 아이콘 3개 + 라벨/레이어명. layers를 전달하지 않으면 기존 필드 버튼(onLayerOpen) 그대로입니다.">
|
||||
<Frame width={1920}><LayerHeader layers={FIGMA_LAYERS} /></Frame>
|
||||
</StorySection>
|
||||
<StorySection title="Layer Hover (고정)" description="기존 노란 Gradient Border와 라벨·레이어명 #FFC600. 목록은 열리지 않습니다.">
|
||||
<Frame width={1920}><PinHover selectors={[".hm-sw-header__field--layer"]}><LayerHeader layers={FIGMA_LAYERS} /></PinHover></Frame>
|
||||
</StorySection>
|
||||
<StorySection title="Layer Dropdown Open (Figma 8개 행)" description="Variant3: 라벨·레이어명 #FFC600, 필드 테두리 #081813·하단 각진 모서리, 필드+목록을 감싸는 노란 Gradient 1px 외곽선(236 × 320). 목록 234 × 280(행 30 × 8 + 하단 40), 배경 #27241D, 검정 1px, 하단 radius 3px, Shadow 0 4px 8px rgba(16,48,38,.25). 이 예제는 onLayerDetail이 없어 클릭은 무동작이며 상세 버튼의 공통 Hover는 유지합니다.">
|
||||
<MenuFrame width={1920}><LayerHeader layers={FIGMA_LAYERS} layerMenuOpen onLayerDetail={undefined} /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="Layer Row Default · Row Hover (고정)" description="두 번째 행 Hover 고정: 배경 #62533D, 레이어명 #FF5C00(26:10695). 행 크기·아이콘 위치는 그대로입니다. 다른 필드·문서 목록의 Hover 색과 무관한 이 목록 전용 색입니다.">
|
||||
<MenuFrame width={1920}><PinHover selectors={[".hm-sw-header__layer-row:nth-child(2)"]}><LayerHeader layers={FIGMA_LAYERS} layerMenuOpen /></PinHover></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="전구 · 눈꽃 · 자물쇠 On / Hover / Off / Off Hover (3배 확대)" description="1행 컬러(Default: visible=true · frozen=false · locked=false) · 2행 컬러 Hover(Variant2, 고정) · 3행 회색(Variant3: visible=false · frozen=true · locked=true) · 4행 회색 Hover(Variant4, 고정). 전구 Hover는 검정 25% 배경, 눈꽃·자물쇠 Hover는 Figma Hover SVG(같은 아이콘 + 검정 25% 타일)입니다. Off → Off Hover는 Figma Smart Animate에 맞춰 300ms ease-out으로 전환됩니다.">
|
||||
<IconMatrix />
|
||||
</StorySection>
|
||||
<StorySection title="레이어 선택 · 개별 아이콘 Toggle · Callback 기록" description="필드 클릭으로 열고, 레이어명(색상 견본 포함)을 누르면 onLayerSelect만 호출되고 필드에 그 레이어가 표시되며 목록이 닫힙니다. 전구·눈꽃·자물쇠는 각각 onLayerVisibilityChange(layer, !visible) / onLayerFreezeChange(layer, !frozen) / onLayerLockChange(layer, !locked)만 한 번 호출하고 목록은 열린 채 유지됩니다(전구 컬러 = visible=true, 눈꽃 컬러 = frozen=false 동결 해제, 자물쇠 컬러 = locked=false 잠금 해제, 회색은 반대). 위 상태표로 해당 레이어만 바뀌는 것을 확인합니다. 키보드: Enter/Space 열기, ↓↑ 행 이동(같은 열 유지), ←→ 행 안 이동, Escape 닫기, Tab으로 벗어나면 닫힘.">
|
||||
<LayerDemo width={1920} activeLayerId="layer-0" />
|
||||
</StorySection>
|
||||
<StorySection title="여러 레이어의 서로 다른 상태 · 현재 레이어" description="현재 레이어(activeLayerId)는 Hover와 같은 갈색 배경·주황 글자이고 필드 아이콘도 그 레이어 상태를 따릅니다. Sample-Defpoints는 disabled 예시입니다.">
|
||||
<MenuFrame width={1920}><LayerHeader activeLayerId="layer-5" layerMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="현재 레이어 Hover (고정)" description="현재 레이어의 갈색 배경·주황 글자는 Hover 및 Mouse Leave 뒤에도 유지됩니다.">
|
||||
<MenuFrame width={1920}><PinHover selectors={[".hm-sw-header__layer-row[data-active]"]}><LayerHeader activeLayerId="layer-1" layerMenuOpen /></PinHover></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="긴 레이어명" description="필드와 행 모두 말줄임으로 처리합니다. 전체 이름은 title로 확인합니다.">
|
||||
<MenuFrame width={1920}><LayerHeader layers={LONG_LAYERS} activeLayerId="long-1" layerMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="많은 레이어 (20개)" description="목록 부분만 세로 스크롤(최대 8행, 240px)하고 레이어 상세 영역은 고정입니다. 헤더에는 스크롤이 생기지 않습니다.">
|
||||
<MenuFrame width={1920}><LayerHeader layers={MANY_LAYERS} layerMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="레이어가 없는 상태" description="layers=[]이면 목록 없이 레이어 상세 영역만 표시하고 필드는 layer.name을 표시합니다.">
|
||||
<MenuFrame width={1920}><LayerHeader layers={[]} layerMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="레이어 상세 버튼" description="onLayerDetail을 전달하면 Callback 호출 후 목록을 닫습니다(아래 기록). 전달하지 않아도 공통 Default/Hover(텍스트 35%)를 유지하며 클릭은 안전한 무동작입니다(위 Figma 8개 행 예제). Disabled는 명시적으로 지정합니다.">
|
||||
<LayerDemo width={1920} layers={FIGMA_LAYERS} />
|
||||
</StorySection>
|
||||
<StorySection title="상태 표시 전용(Toggle Callback 없음)" description="onLayerVisibilityChange 등을 전달하지 않으면 해당 아이콘은 버튼이 아닌 상태 표시(role=img)이며 Hover·클릭 반응이 없습니다.">
|
||||
<MenuFrame width={1920}>
|
||||
<SWHeader {...figmaArgs} {...callbacks} layers={SAMPLE_LAYERS} activeLayerId="layer-0" layerMenuOpen />
|
||||
</MenuFrame>
|
||||
</StorySection>
|
||||
{NARROW_LAYER_WIDTHS.map(width => (
|
||||
<StorySection
|
||||
key={width}
|
||||
title={`좁은 화면 (헤더 ${width}px)`}
|
||||
description={width < 520
|
||||
? "520 미만에서는 필드의 상태 아이콘이 숨고 레이어명이 말줄임됩니다. 목록은 234px 그대로이며 헤더 밖으로 넘치지 않도록 왼쪽으로 이동합니다(이때 노란 외곽선은 목록만 감쌉니다). 필드를 눌러 열고 조작합니다."
|
||||
: "필드 위치와 헤더 49px를 유지하고 목록은 필드 왼쪽에 맞춰 열립니다."}
|
||||
>
|
||||
<LayerDemo width={width} activeLayerId="layer-1" layerMenuOpen={width < 520 ? undefined : true} />
|
||||
</StorySection>
|
||||
))}
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------
|
||||
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.
|
||||
@@ -843,127 +825,6 @@ function StyleDemo({ width, ...props }: StyleHostProps & { width: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Latest explicit hover exceptions: mode pair and five detail buttons. */
|
||||
const MENU_BUTTON_HOVER = [".hm-sw-header__color-mode", ".hm-sw-header__color-option", ".hm-sw-header__color-pick", ".hm-sw-header__menu-detail"]
|
||||
|
||||
export const ColorDropdown: Story = {
|
||||
name: "Color Dropdown",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={KICKER}
|
||||
title="Color Dropdown"
|
||||
description="색상 필드를 클릭하면 Figma header/menu/색상(26:10986)의 목록이 필드 아래로 겹쳐 열립니다(Hover만으로는 열리지 않음). ByLayer/ByBlock(26:12666 / 26:12659), 22px 기본 팔레트, 사용자 색상(26:11036), 색상 추출, RGB 입력, 색상 상세 버튼으로 구성됩니다. 팔레트·사용자 색상·현재 색상은 호스트 데이터이며 아래 값은 Figma 예시입니다."
|
||||
/>
|
||||
<StorySection title="Default" description="colors를 전달하면 색상 필드가 Dropdown 버튼이 됩니다. 192 × 38, 견본은 현재 색상(color.swatch).">
|
||||
<Frame width={1920}><StyleHeader /></Frame>
|
||||
</StorySection>
|
||||
<StorySection title="Hover (고정)" description="기존 노란 Gradient Border + 라벨·값 #FFC600. 색상 견본은 실제 색 그대로입니다.">
|
||||
<Frame width={1920}><PinHover selectors={[".hm-sw-header__field--color"]}><StyleHeader /></PinHover></Frame>
|
||||
</StorySection>
|
||||
<StorySection title="Expanded (Figma 26:11005)" description="192 × 158: ByLayer/ByBlock 167 × 30 · 팔레트 168 × 22(칩 22px 7개) · 색상 추출 24 + 입력 130 · 하단 40(색상 상세). 필드 테두리 #081813·아래 모서리 각짐, 필드+목록 노란 1px 외곽선(194 × 198).">
|
||||
<MenuFrame width={1920}><StyleHeader userColors={[]} colorMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="사용자 추가 색상 (Figma 26:11036)" description="userColors 2개: 24px 행이 추가되어 본문 182px. 개수는 가변이며 2px 간격으로 줄바꿈합니다.">
|
||||
<MenuFrame width={1920}><StyleHeader colorMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="사용자 색상 40개" description="팔레트 + 사용자 색상 4행(118px)을 넘으면 팔레트 영역만 세로 스크롤합니다. 헤더 높이·다른 필드 위치는 그대로입니다.">
|
||||
<MenuFrame width={1920}><StyleHeader userColors={MANY_USER_COLORS} colorMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="ByLayer On (Figma on 26:12671)" description="colorMode='byLayer': #62533D 배경 + 밝은 inner shadow. 텍스트 #D4CBBD + 검정 Stroke. ByBlock은 Default(rgba(98,83,61,.3) + 어두운 inner shadow).">
|
||||
<MenuFrame width={1920}><StyleHeader initialColorMode="byLayer" colorMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="내부 버튼 Hover (고정) — 2026-10-16 추가 수정 정책" description="ByLayer / ByBlock 글자 #FF5C00. 선택 배경과 Inset Shadow는 유지합니다. 색상 상세는 배경 .25·radius 25·Hover 아이콘으로 변합니다. 색상 칩은 Hover가 없고, 색상 추출은 btn_colorize Hover(26:11305)입니다.">
|
||||
<MenuFrame width={1920}><PinHover selectors={MENU_BUTTON_HOVER}><StyleHeader colorMenuOpen /></PinHover></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="RGB 입력 Hover (고정)" description="입력은 수정 가능함을 알리는 Figma Hover를 유지합니다(header/menu/색상/input hover: 1px rgba(197,177,158,.2)). 편집 중(Focus)에는 값 영역이 검정이 되고 커서는 #E60403입니다 — 아래 실제 예제에서 클릭해 확인합니다.">
|
||||
<MenuFrame width={1920}><PinHover selectors={[".hm-sw-header__color-input", ".hm-sw-header__color-input .hm-sw-header__input"]}><StyleHeader colorMenuOpen /></PinHover></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="선택 · 입력 · Callback 기록" description="칩 → onColorSelect, ByLayer/ByBlock → onColorModeChange(서로 배타: 이 호스트는 선택한 쪽만 On), 입력(255,0,0 · #00ff00 등, Enter 또는 Focus 이동) → 유효하면 onColorInput, 잘못된 값(256,0,0 · abc)은 복원. 색상 추출 → onColorPick 후 닫힘, 색상 상세 → onColorDetail 후 닫힘. 선택·입력 후에도 목록은 열려 있고 다른 속성(레이어·선스타일 등)은 그대로입니다. Escape·바깥 클릭·Tab으로 닫힘.">
|
||||
<StyleDemo width={1920} />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
export const LineStyleDropdown: Story = {
|
||||
name: "Line Style Dropdown",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={KICKER}
|
||||
title="Line Style Dropdown"
|
||||
description="선스타일 필드를 클릭하면 Figma header/menu/선스타일(26:11104)의 3열 목록(선 종류 182 · 가중치 142 · 스케일 142, 468 × 192)이 필드 가운데 아래로 열립니다. 선 종류 행은 header/dropdown/brown/row(26:10702). 각 열은 자기 속성만 바꾸며 목록은 열린 채 유지됩니다."
|
||||
/>
|
||||
<StorySection title="Default" description="284 × 38. 선 미리보기 10 × 4 × 5, 선스타일 / 가중치 / 스케일.">
|
||||
<Frame width={1920}><StyleHeader /></Frame>
|
||||
</StorySection>
|
||||
<StorySection title="Hover (고정)" description="라벨·값 #FFC600, 선 미리보기 채움 #FFC600(검정 1px 유지), 노란 Gradient Border.">
|
||||
<Frame width={1920}><PinHover selectors={[".hm-sw-header__field--line-style"]}><StyleHeader /></PinHover></Frame>
|
||||
</StorySection>
|
||||
<StorySection title="Expanded (Figma 26:11143)" description="필드 링(286 × 39) + 468 × 192 drop_wrap(1px 노란 링, x −92, y 37). 선 종류 5행 + 하단 40(선스타일 상세), 가중치 4행(ByDefault 0.25, User 입력), 스케일 입력. 현재 선 종류·가중치는 Hover와 같은 갈색 배경·주황 텍스트·선입니다.">
|
||||
<MenuFrame width={1920}><StyleHeader lineStyleMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="선 종류 행 Default · Hover (Figma 26:10703 / 26:10706, 고정)" description="1·4번째 행 Hover 고정: 배경 #62533D, 선 미리보기·텍스트 #FF5C00, 검정 Stroke 유지, 크기 182 × 30·위치 그대로. CENTER·HIDDEN은 각자의 패턴(22·7 / 33·33)을 유지합니다. 가중치 2번째 행·스케일 행도 Figma 행 Hover를 고정했습니다.">
|
||||
<MenuFrame width={1920}>
|
||||
<PinHover selectors={[".hm-sw-header__line-column--type .hm-sw-header__menu-row:nth-child(1)", ".hm-sw-header__line-column--type .hm-sw-header__menu-row:nth-child(4)", ".hm-sw-header__line-column:nth-child(2) .hm-sw-header__menu-row:nth-child(2)", ".hm-sw-header__line-column:nth-child(3) .hm-sw-header__menu-row"]}>
|
||||
<StyleHeader lineStyleMenuOpen />
|
||||
</PinHover>
|
||||
</MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="현재 행 Hover (고정)" description="현재 선 종류(Continuous)는 Hover 및 Mouse Leave 뒤에도 갈색 배경·주황 선·이름을 유지합니다.">
|
||||
<MenuFrame width={1920}><PinHover selectors={[".hm-sw-header__menu-row[data-active]"]}><StyleHeader lineStyleMenuOpen /></PinHover></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="상세 버튼 Hover (고정)" description="2026-10-16 추가 수정 기준으로 선스타일 상세의 Figma 배경 .25·radius 25·Hover 아이콘을 적용합니다.">
|
||||
<MenuFrame width={1920}><PinHover selectors={MENU_BUTTON_HOVER}><StyleHeader lineStyleMenuOpen /></PinHover></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="선 종류 · 가중치 · 스케일 선택 Callback" description="선 종류 → onLineTypeSelect(가중치·스케일 유지), 가중치 → onLineWeightSelect(선 종류·스케일 유지), User 입력 → onLineWeightValueChange(0 이상), 스케일 입력 → onLineScaleChange(0 초과). 입력은 Enter/Focus 이동으로 확정, Escape는 취소. ↓↑로 항목 이동.">
|
||||
<StyleDemo width={1920} />
|
||||
</StorySection>
|
||||
<StorySection title="좁은 헤더 390px" description="468px 목록이 헤더보다 넓으면 3열을 세로로 쌓아(필요 시 세로 스크롤) 화면 밖 이탈·가로 스크롤 없이 모든 항목에 접근합니다. 더보기 → 선스타일로 엽니다.">
|
||||
<StyleDemo width={390} />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
export const TextDimensionStyleDropdown: Story = {
|
||||
name: "Text · Dimension Style Dropdown",
|
||||
parameters: { controls: { disable: true } },
|
||||
render: () => (
|
||||
<StoryPage>
|
||||
<StoryPageHeader
|
||||
kicker={KICKER}
|
||||
title="Text · Dimension Style Dropdown"
|
||||
description="문자스타일(26:11181)과 치수스타일(26:11215) 필드의 목록입니다. 164px, 행 30px, 하단 40px(상세 버튼). 행을 고르면 Callback 후 닫힙니다. 목록은 호스트 데이터이며 아래 값은 Figma 예시입니다."
|
||||
/>
|
||||
<StorySection title="Default · Hover (고정)" description="위: Default. 아래: Hover — 라벨·값·글꼴 미리보기 #FFC600, 치수 미리보기는 Figma Hover SVG, 검정 외곽선 유지.">
|
||||
<div className="sw-story-stack">
|
||||
<Frame width={1920}><StyleHeader /></Frame>
|
||||
<Frame width={1920}><PinHover selectors={[".hm-sw-header__field--text-style", ".hm-sw-header__field--dimension-style"]}><StyleHeader /></PinHover></Frame>
|
||||
</div>
|
||||
</StorySection>
|
||||
<StorySection title="문자스타일 Expanded (Figma 26:11200)" description="Arial / Standard, 돋움 / Dotum. 글꼴 16px + 이름 14px(모두 검정 Stroke). 하단 '문자스타일 상세'는 Figma처럼 왼쪽 정렬(테두리 .25).">
|
||||
<MenuFrame width={1920}><StyleHeader textStyleMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="문자스타일 행·상세 버튼 Hover (고정)" description="두 번째 행과 현재 스타일 모두 #62533D / 글꼴·이름 #FF5C00입니다. 상세 버튼도 Figma Hover입니다.">
|
||||
<MenuFrame width={1920}><PinHover selectors={[".hm-sw-header__list-menu--text-style .hm-sw-header__menu-row:nth-child(2)", ".hm-sw-header__menu-detail"]}><StyleHeader textStyleMenuOpen /></PinHover></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="치수스타일 Expanded (Figma 26:11242)" description="Standard, ISO - 25. 치수 미리보기 60 × 15(Figma 행 자산 #F6F4F2). 하단 '치수스타일 상세' 오른쪽 정렬.">
|
||||
<MenuFrame width={1920} fixedDimension><StyleHeader dimensionStyleMenuOpen /></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="치수스타일 행·상세 버튼 Hover (고정)" description="두 번째 행: #62533D, 이름·치수 미리보기 #FF5C00(Figma Hover 자산). 상세 버튼 Hover는 26:10799(기본 아이콘·텍스트 100%)이며 Variant3와 혼합하지 않습니다.">
|
||||
<MenuFrame width={1920} fixedDimension><PinHover selectors={[".hm-sw-header__list-menu--dimension-style .hm-sw-header__menu-row:nth-child(2)", ".hm-sw-header__menu-detail"]}><StyleHeader dimensionStyleMenuOpen /></PinHover></MenuFrame>
|
||||
</StorySection>
|
||||
<StorySection title="선택 Callback" description="행 → onTextStyleSelect / onDimensionStyleSelect 1회 후 닫힘, 상세 → on*Detail 1회 후 닫힘. 필드 값이 호스트 상태로 갱신됩니다.">
|
||||
<StyleDemo width={1920} />
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
),
|
||||
}
|
||||
|
||||
const MORE_WIDTHS = [1920, 1800, 1799, 1440, 1180, 1179, 1080, 1079, 900, 899, 768, 640, 639, 520, 519, 390]
|
||||
|
||||
export const ResponsiveMore: Story = {
|
||||
@@ -1348,8 +1209,6 @@ export const Colorize: Story = {
|
||||
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 }[] = [
|
||||
|
||||
Reference in new issue
Block a user