feat(sw): integrated improvement 0.2.0 (popup content, header open, tool buttons)
SW package 0.2.0 integrated improvement (Task A, steps 1-8). All new props are
optional; existing SW Stories are pixel-identical to the previous build (28/28).
- Build reproducibility: dist text normalized to LF, dist/build-info.json
(commit, dirty, tool versions, no timestamp), packages/sw/.gitattributes
(eol=lf). LF checkouts give identical dist and tarball.
- SWHeader: documentOpenPlacement ("menu" default | "header": Open right of
the logo in the existing document-button look, reusing onDocumentOpen and
documentOpenLabel, no duplicate Open in the dropdown), leading slot,
hiddenControls sub-items documentNew / documentOpen / layerFreeze /
layerLock. Open moves into More on measured overflow (with hysteresis).
- showNew / showOpen (SWHeaderDocumentTitle), showFreeze / showLock (layer
menu, field, property window list).
- SWToolButton (text / pill / document) and SWToolGroup (radio selection),
reusing the existing toggle, pill and file-name looks.
- SWPopup / SWPopupSection / SWPopupRow on the shared property window frame;
SWHeaderPropertyPanel gains children, title, autoFocus and optional
selection / properties / groups. Escape stack for several popups.
- Field dropdowns inside a popup use the shared fixed overlay
(useSWFixedDropdownPosition), so the scrolling body no longer clips them.
- AI guide, examples E/F, README, Storybook (SW/Header/Options, SW/Tool,
SW/Popup) and verification record docs/sw/sw-integrated-improvement-2026-10-19.md.
- Regression scripts rescoped for the intended changes (unused-removal 81/81,
overview 52/54 with the two pre-existing historical assertions).
Known limitation (unresolved): tools / leading content (incl. SWToolGroup)
does not move into More. With a wide tool bar the configured fields shrink
instead; in Example F the document name and layer field reach 0px at 390px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5faa1f1ffe
commit
55179b4577
138 files changed
+17194
-162
No files matched your search
@@ -0,0 +1,168 @@
|
||||
import { useId, useState } from "react"
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite"
|
||||
import {
|
||||
SWHeaderColorMenu, SWHeaderInput, SWHeaderLayerMenu, SWHeaderPropertyPanel, SWPopup, SWPopupRow, SWPopupSection, SWTheme,
|
||||
type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderProperty,
|
||||
} from "@hm-design/sw"
|
||||
import "@hm-design/sw/styles.css"
|
||||
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider, Switch } from "@hm-design/ui"
|
||||
import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page"
|
||||
import "./sw-stories.css"
|
||||
import "./sw-integrated-stories.css"
|
||||
|
||||
// SWPopup (2026-10-19): the SW property window frame with free children. Public API only, like a consuming
|
||||
// project. HM UI components are placed inside to show that the content is not restricted.
|
||||
const KICKER = "HM DESIGN SYSTEM / SW"
|
||||
const LAYERS: SWHeaderLayer[] = [
|
||||
{ id: "0", name: "0", color: "#ffffff" },
|
||||
{ id: "wall", name: "WALL", color: "#ff4242" },
|
||||
{ id: "dim", name: "DIM", color: "#47af91" },
|
||||
{ id: "hatch", name: "HATCH", color: "#5aa0ff" },
|
||||
]
|
||||
const COLORS: SWHeaderColorOption[] = [
|
||||
{ id: "red", color: "#ff0000", label: "빨강" }, { id: "yellow", color: "#ffff00", label: "노랑" },
|
||||
{ id: "green", color: "#00ff00", label: "초록" }, { id: "blue", color: "#0000ff", label: "파랑" },
|
||||
]
|
||||
|
||||
const meta = {
|
||||
title: "SW/Popup",
|
||||
parameters: { layout: "fullscreen", controls: { disable: true } },
|
||||
} satisfies Meta
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
function useLog() {
|
||||
const [log, setLog] = useState<string[]>([])
|
||||
const view = <p className="sw-story-log" aria-live="polite" data-callback-log="">Callback: {log.length ? log.join(" → ") : "없음"}</p>
|
||||
return [view, (line: string) => setLog(lines => [...lines.slice(-9), line])] as const
|
||||
}
|
||||
|
||||
function SettingsPopup({ record }: { record: (line: string) => void }) {
|
||||
const ids = { name: useId(), scale: useId(), snap: useId() }
|
||||
const [name, setName] = useState("평면도-01")
|
||||
const [scale, setScale] = useState("100")
|
||||
const [layerId, setLayerId] = useState("wall")
|
||||
const [color, setColor] = useState({ value: "255,0,0", swatch: "#ff0000" })
|
||||
const [snap, setSnap] = useState(true)
|
||||
const [opacity, setOpacity] = useState([60])
|
||||
const [unit, setUnit] = useState("mm")
|
||||
return (
|
||||
<SWPopup title="도면 설정" onClose={() => record("settings.onClose")} data-popup="settings">
|
||||
<SWPopupSection title="도면">
|
||||
<SWPopupRow label="이름" htmlFor={ids.name}>
|
||||
<SWHeaderInput id={ids.name} value={name} onCommit={next => { setName(next); record(`name=${next}`) }} />
|
||||
</SWPopupRow>
|
||||
<SWPopupRow label="축척" htmlFor={ids.scale} unit="%" numeric>
|
||||
<SWHeaderInput id={ids.scale} valueType="number" value={scale} onCommit={next => { setScale(next); record(`scale=${next}`) }} />
|
||||
</SWPopupRow>
|
||||
<SWPopupRow label="단위" field="none">
|
||||
<Select value={unit} onValueChange={next => { setUnit(next); record(`unit=${next}`) }}>
|
||||
<SelectTrigger data-ui-select=""><SelectValue /></SelectTrigger>
|
||||
<SelectContent>{["mm", "cm", "m"].map(item => <SelectItem key={item} value={item}>{item}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
</SWPopupRow>
|
||||
</SWPopupSection>
|
||||
<SWPopupSection title="SW Dropdown">
|
||||
<div data-popup-layer="">
|
||||
<SWHeaderLayerMenu value={{ name: LAYERS.find(layer => layer.id === layerId)!.name }} layers={LAYERS} activeLayerId={layerId}
|
||||
onLayerSelect={layer => { setLayerId(layer.id); record(`layer=${layer.id}`) }} />
|
||||
</div>
|
||||
<div data-popup-color="">
|
||||
<SWHeaderColorMenu value={color} colors={COLORS} onColorSelect={option => { setColor({ value: option.value ?? option.color, swatch: option.color }); record(`color=${option.id}`) }} />
|
||||
</div>
|
||||
</SWPopupSection>
|
||||
<SWPopupSection title="HM UI">
|
||||
<SWPopupRow label="스냅" htmlFor={ids.snap} field="none">
|
||||
<Switch id={ids.snap} checked={snap} onCheckedChange={next => { setSnap(next); record(`snap=${next}`) }} data-ui-switch="" />
|
||||
</SWPopupRow>
|
||||
<SWPopupRow label="투명도" field="none">
|
||||
<Slider value={opacity} min={0} max={100} step={10} onValueChange={setOpacity} onValueCommit={next => record(`opacity=${next[0]}`)} aria-label="투명도" data-ui-slider="" />
|
||||
</SWPopupRow>
|
||||
</SWPopupSection>
|
||||
<div className="sw-story-popup-actions">
|
||||
<Button variant="secondary" size="sm" onClick={() => record("cancel")}>취소</Button>
|
||||
<Button size="sm" onClick={() => record("apply")} data-ui-apply="">적용</Button>
|
||||
</div>
|
||||
</SWPopup>
|
||||
)
|
||||
}
|
||||
|
||||
function ObjectInfoPopup({ record }: { record: (line: string) => void }) {
|
||||
const [x, setX] = useState("1200.5")
|
||||
return (
|
||||
<SWPopup title="객체 정보 · Beam-01" onClose={() => record("object.onClose")} data-popup="object">
|
||||
<SWPopupSection title="객체">
|
||||
<SWPopupRow label="이름" readOnly><SWHeaderInput value="Beam-01" readOnly aria-label="이름" /></SWPopupRow>
|
||||
<SWPopupRow label="타입" readOnly><SWHeaderInput value="Line" readOnly aria-label="타입" /></SWPopupRow>
|
||||
<SWPopupRow label="길이" unit="mm" readOnly><SWHeaderInput value="3000.0000" readOnly aria-label="길이" /></SWPopupRow>
|
||||
</SWPopupSection>
|
||||
<SWPopupSection title="좌표">
|
||||
<SWPopupRow label="X" unit="mm" numeric><SWHeaderInput valueType="number" value={x} aria-label="X" onCommit={next => { setX(next); record(`x=${next}`) }} /></SWPopupRow>
|
||||
<SWPopupRow label="Y" field="compact"><SWHeaderInput valueType="number" value="20" aria-label="Y" onCommit={next => record(`y=${next}`)} /></SWPopupRow>
|
||||
</SWPopupSection>
|
||||
</SWPopup>
|
||||
)
|
||||
}
|
||||
|
||||
function PanelWithChildren({ record }: { record: (line: string) => void }) {
|
||||
const [properties, setProperties] = useState<SWHeaderProperty[]>([
|
||||
{ id: "layer", name: "레이어", value: "wall", type: "select", groupId: "common", picker: "layer", options: LAYERS.map(({ id, name }) => ({ id, name })) },
|
||||
{ id: "color", name: "색상", value: "255,66,66", type: "color", groupId: "common" },
|
||||
{ id: "width", name: "선가중치", value: "0.25", type: "number", groupId: "common" },
|
||||
])
|
||||
return (
|
||||
<SWHeaderPropertyPanel title="레이어 속성 + 추가 콘텐츠" groups={[{ id: "common", name: "공통" }]} properties={properties}
|
||||
menus={{ layers: LAYERS }} onClose={() => record("panel.onClose")}
|
||||
onPropertyChange={(property, value) => { setProperties(list => list.map(item => item.id === property.id ? { ...item, value } : item)); record(`${property.id}=${value}`) }}>
|
||||
<SWPopupSection title="메모">
|
||||
<SWPopupRow label="설명"><SWHeaderInput value="외벽" aria-label="설명" onCommit={next => record(`memo=${next}`)} /></SWPopupRow>
|
||||
</SWPopupSection>
|
||||
</SWHeaderPropertyPanel>
|
||||
)
|
||||
}
|
||||
|
||||
function PopupPage() {
|
||||
const [log, record] = useLog()
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="SW Popup — 외형 고정, 콘텐츠 자유"
|
||||
description="SWPopup은 속성창 프레임(314px, 제목줄, 닫기, 내부 스크롤)을 그대로 쓰고 children으로 아무 콘텐츠나 받습니다. SWPopupSection / SWPopupRow는 속성창 그룹·행 모양을 그대로 재사용합니다. Header 없이 SWTheme 안에서 씁니다." />
|
||||
{log}
|
||||
<StorySection title="사용자 정의 설정 · 객체 정보 · 기존 속성창 + children" description="SW Input·Dropdown과 HM UI Switch·Slider·Select·Button을 함께 배치합니다. 기존 SWHeaderPropertyPanel도 children을 받습니다.">
|
||||
<SWTheme className="sw-story-popups" data-popups="">
|
||||
<SettingsPopup record={record} />
|
||||
<ObjectInfoPopup record={record} />
|
||||
<PanelWithChildren record={record} />
|
||||
</SWTheme>
|
||||
</StorySection>
|
||||
</StoryPage>
|
||||
)
|
||||
}
|
||||
|
||||
export const CustomContent: Story = { name: "Custom Content", render: () => <PopupPage /> }
|
||||
|
||||
function StackPage() {
|
||||
const [log, record] = useLog()
|
||||
const [open, setOpen] = useState(["a", "b", "c"])
|
||||
const close = (id: string) => { setOpen(list => list.filter(item => item !== id)); record(`close(${id})`) }
|
||||
const rows = Array.from({ length: 30 }, (_, index) => index + 1)
|
||||
return (
|
||||
<StoryPage>
|
||||
<StoryPageHeader kicker={KICKER} title="여러 Popup · Escape · 스크롤"
|
||||
description="Popup 안의 Escape는 그 Popup만 닫습니다. 바깥의 Escape는 가장 최근에 열리거나 사용한 Popup 하나만 닫습니다. Dropdown·편집 중인 Input이 처리한 Escape로는 닫히지 않습니다. 긴 콘텐츠는 본문이 스크롤됩니다." />
|
||||
{log}
|
||||
<button type="button" data-reopen="" onClick={() => setOpen(["a", "b", "c"])}>다시 열기</button>
|
||||
<SWTheme className="sw-story-popups" data-stack="">
|
||||
{open.includes("a") && <SWPopup title="Popup A" onClose={() => close("a")} data-popup="a"><SWPopupSection title="A"><SWPopupRow label="값"><SWHeaderInput value="A" aria-label="A 값" onCommit={next => record(`a=${next}`)} /></SWPopupRow></SWPopupSection></SWPopup>}
|
||||
{open.includes("b") && <SWPopup title="Popup B" onClose={() => close("b")} data-popup="b"><SWPopupSection title="B">
|
||||
<div data-popup-layer=""><SWHeaderLayerMenu value={{ name: "WALL" }} layers={LAYERS} activeLayerId="wall" /></div>
|
||||
</SWPopupSection></SWPopup>}
|
||||
{open.includes("c") && <SWPopup title="Popup C · 긴 콘텐츠" onClose={() => close("c")} data-popup="c" style={{ maxHeight: 360 }}>
|
||||
<SWPopupSection title="많은 필드">{rows.map(row => <SWPopupRow key={row} label={`필드 ${row}`} unit="mm" numeric><SWHeaderInput valueType="number" value={String(row)} aria-label={`필드 ${row}`} /></SWPopupRow>)}</SWPopupSection>
|
||||
</SWPopup>}
|
||||
</SWTheme>
|
||||
</StoryPage>
|
||||
)
|
||||
}
|
||||
|
||||
export const MultiplePopups: Story = { name: "Multiple Popups", render: () => <StackPage /> }
|
||||
Reference in new issue
Block a user