Files
HMDesign/packages/sw/ai/COMPOSITION_PATTERNS.md
T
shkim4 1472e51f47 feat(sw): integrate home menu into SWHeader (Task A + B)
Combine Task A (e8409ef, SW 0.2.0 integrated improvement) with Task B
(396539d, Figma-based SWHomeMenu / SWHomeMenuSection / SWHomeMenuItem /
SWRecentFiles). Task B's 84 new files are taken unchanged except for the
public-import Storybook update and joining the popup Escape stack.

- Export the home menu from @hm-design/sw
- SWHeader homeMenu / homeMenuOpen / defaultHomeMenuOpen /
  onHomeMenuOpenChange: hidden by default, the logo toggles it (onHomeClick
  is not called while homeMenu is set), placed from the measured header,
  closes with / closes the document and field dropdowns
- Logo aria-expanded / aria-controls; menu layer CSS (z-index 20)
- AI catalog / patterns / design rules, example G, README, usage guide
- Storybook SW/Home Menu uses @hm-design/sw; integration story

Verified: SW typecheck, tsc -b, SW build, Storybook build, AI docs 22/22,
28/28 existing SW shots pixel-identical, regression 96 PASS / 0 FAIL,
home menu checks 22/22. Not run: tarball consumer install.
2026-10-19 15:39:47 +09:00

10 KiB

SW 조합 패턴

모든 패턴은 공개 Export만 사용합니다. 전체 코드는 examples/에 있으며, 설치된 패키지에서 꺼낸 그대로 독립 React 19 + TypeScript + Vite 프로젝트의 tsc·Production Build·브라우저 검증을 통과했습니다(2026-10-19).

0. 어떤 패턴을 고를까

요청 패턴
기존 SW Header 전체(일부 부품 제거·메뉴 순서 변경 포함) 1. 완성형
기존 컨트롤 일부만, 다른 배치·순서 2. 조합 Header
기존 Dropdown을 툴바·패널 등 다른 곳에 3. 개별 컨트롤
패키지에 없는 기능 4. 확장(소비 프로젝트 신규 컴포넌트)
SW Popup 디자인의 설정창·정보창·사용자 정의 폼 5. Popup(외형 고정, 콘텐츠 자유)
로고 옆 파일 열기, 뷰어 도구 막대(Model/Layout, Fit, 2.5D) 6. 파일 열기 + 도구 막대
로고를 누르면 열리는 2단 메뉴(저장·내보내기·찾아열기·최근파일 등) 7. 로고 클릭 메뉴

1. 완성형 SW Header — ExampleA-DefaultHeader.tsx

<SWHeader documentTitle={name} documents={documents} layers={layers} colors={colors} onClose={close} />
  • 부품 제거: hiddenControls={["print", "popupGuide"]}(로고는 항상 표시, "home"은 정적 로고).
  • 메뉴 순서: menuOrder={["color", "layer"]}(5개 메뉴끼리, 누락분은 기본 순서로 뒤에).
  • 객체 영역 전체를 직접 나열: controls={[{ id: "document" }, { id: "layer" }, { id: "print" }]}.
  • 추가 도구: tools={<MyTool />}(객체 영역 메뉴 뒤). 새 도구는 아래 4의 규칙을 따른다.
  • 이 경우에만 반응형 More가 동작합니다.

2. 조합 Header (간소화·기능 중심)

<SWTheme>을 Header 막대로 쓰고, 막대·면 배치 CSS는 소비 프로젝트가 작성합니다(example-layout.css). 이 CSS는 --hm-sw-header-* 토큰만 읽고 hm-sw-* class를 건드리지 않습니다.

<SWTheme className="app-sw-bar">                 {/* 49px, #060606, flex, gap 2px */}
  <SWHeaderHome logoAlt="뷰어" onClick={goHome} />  {/* 80px, 다음 파일 면과 겹침 */}
  <SWHeaderDocumentTitle title={name} documents={documents} onDocumentSelect={open} />
  <div className="app-sw-surface">                 {/* 객체 수정 면 토큰 */}
    <SWHeaderLayerMenu value={layer} layers={layers} onLayerSelect={selectLayer} />
    <SWHeaderColorMenu value={color} colors={colors} onColorSelect={selectColor} />
  </div>
  <div className="app-sw-window">                  {/* 창 영역 면 토큰 */}
    <SWHeaderActions showPrint={false} onPopupGuide={showGuide} />
  </div>
</SWTheme>
  • 간소화 Viewer: ExampleB-ViewerHeader.tsx — 로고 → 파일 → 레이어 → 색상 → K.
  • 기능 중심: ExampleC-FeatureHeader.tsx — 파일 → 선 → 문자 → 치수스타일 → 인쇄.
  • 순서는 JSX 순서입니다(DOM·Tab 순서 동일). CSS order로 바꾸지 않습니다.
  • 로고 없이 파일 면이 처음이면 왼쪽 테두리가 없으므로 wrapper에 1px 테두리를 둡니다(예제 C app-sw-file-edge).
  • K만/인쇄만: SWHeaderActions의 showPrint={false} / showPopupGuide={false}.
  • 창 제어: <SWHeaderWindowControls onWindowMoveStart={startDrag} onMinimize={min} onClose={close} />(창 영역 면 안).
  • 조합 Header에는 반응형 More가 없습니다. 좁은 화면이 필요하면 완성형을 쓰거나 표시할 컨트롤을 줄입니다.

3. 기존 Dropdown을 다른 레이아웃에서

<SWTheme className="app-toolbar">
  <SWHeaderLineStyleMenu value={lineStyle} lineTypes={lineTypes} lineWeights={lineWeights} onLineTypeSelect={setType} />
</SWTheme>
  • Dropdown은 필드 바로 아래에 열리고 뷰포트 안으로 좌우 보정됩니다. 부모에 overflow: hidden을 두지 않습니다.
  • Controlled가 필요하면 open / onOpenChange를 함께 줍니다.
  • Dropdown이 필요 없고 호스트가 자체 팝업을 열면 SWHeaderLayerControl 등 필드 버튼만 씁니다.

4. 확장 — 없는 컨트롤 새로 만들기 — SWSearchControl.tsx, ExampleD-SearchExtension.tsx

요청 예: "레이어와 색상은 기존 SW 컴포넌트, 검색 버튼은 SW 디자인으로 새로."

  1. 카탈로그 검색: 검색 컴포넌트 없음(MISSING).
  2. 조합 검토: 텍스트 입력·확정·취소·IME는 SWHeaderInput으로 충족 → 재사용. 필드 면과 검색 아이콘 버튼은 공개 컴포넌트가 없음 → 신규.
  3. 신규 부분은 소비 프로젝트에 작성: 38px 속성 필드 면, 12px 라벨, gradient Hover 링, 아이콘 버튼은 글자색 Hover #FFC600. 모든 값은 토큰(sw-search-control.css).
  4. 기존 컨트롤과 같은 면(app-sw-surface)에 나란히 배치.
  5. 검증: 레이어 필드와 높이·위치·테두리·radius·배경·그림자·라벨 크기 일치, Hover 링, Enter 확정 → onSearch, Escape 복원.

새 컨트롤은 패키지 공식 컴포넌트로 자동 등록하지 않습니다. 여러 프로젝트에서 반복되면 SW 패키지 승격을 별도로 검토합니다.

5. Popup — 외형 고정, 콘텐츠 자유 — ExampleE-CustomPopup.tsx

요청 예: "HMDesign SW Popup 디자인을 사용해서 내부에 새로운 설정 폼을 만들어 줘."

  1. 카탈로그 3절에서 SWPopup을 찾는다(프레임 = 속성창 외형·닫기·Escape·스크롤).
  2. 내용은 children으로 구성한다. 행 모양은 SWPopupSection + SWPopupRow, 값 입력은 SWHeaderInput.
  3. SW에 없는 컨트롤(Switch, Slider, Select, Button 등)은 @hm-design/ui를 children 안에 그대로 둔다. 구조 제한은 없다.
  4. 프레임(폭, 제목줄, 닫기, 표면색)을 다시 만들지 않고, hm-sw-property* class를 쓰지 않는다.
<SWTheme>
  <SWPopup title="도면 설정" onClose={close}>
    <SWPopupSection title="도면">
      <SWPopupRow label="이름" htmlFor={id}><SWHeaderInput id={id} value={name} onCommit={setName} /></SWPopupRow>
      <SWPopupRow label="축척" unit="%" numeric><SWHeaderInput aria-label="축척" valueType="number" value={scale} onCommit={setScale} /></SWPopupRow>
    </SWPopupSection>
    <MyProjectFields />
  </SWPopup>
</SWTheme>
  • 데이터 기반 속성창이 필요하면 SWHeaderPropertyPanel(properties / groups)을 쓰고, 추가 내용은 같은 컴포넌트의 children으로 붙인다.
  • 위치는 호스트가 정한다: 문서 흐름(style) 또는 anchorRef(기준 요소 아래 고정 Overlay). 열고 닫는 상태도 호스트가 가진다(조건부 렌더링).
  • 여러 Popup을 동시에 띄워도 Escape는 하나씩 닫힌다(카탈로그 3절).

6. 파일 열기 + 도구 막대 — ExampleF-ViewerToolbar.tsx

<SWHeader documentTitle={name} documents={documents} documentOpenPlacement="header" documentOpenLabel="파일 열기" onDocumentOpen={open}
  controls={[{ id: "document" }, { id: "layer" }, { id: "color" }, { id: "popupGuide" }]}
  hiddenControls={["documentNew", "layerFreeze", "layerLock"]}
  tools={<SWToolGroup label="보기" selection="single">
    <SWToolButton pressed={view === "model"} onClick={showModel}>Model</SWToolButton>
    <SWToolButton pressed={view === "layout"} onClick={showLayout}>Layout</SWToolButton>
  </SWToolGroup>} />
  • 파일 열기는 tools에 직접 만들지 않는다. documentOpenPlacement="header"가 로고 바로 오른쪽에 파일명과 같은 면으로 그리고 Dropdown의 Open은 숨긴다(중복 없음). 공간이 부족하면 More로 이동한다.
  • 지원하지 않는 기능은 hiddenControls로 뺀다(documentNew, documentOpen, layerFreeze, layerLock). 가짜 버튼을 두지 않는다.
  • SWHeader 없이 같은 순서가 필요하면: SWHeaderHome → SWToolButton appearance="document" → SWHeaderDocumentTitle showOpen={false}(사선 접점·12px 여백은 패키지가 처리). 이 경우 More는 없다.

7. 로고 클릭 메뉴 — ExampleG-HomeMenu.tsx

const sections: SWHomeMenuSectionData[] = [
  { id: "working", title: "작업중인 파일", items: [
    { id: "save", label: "저장", icon: <SaveIcon />, toggle: false, onSelect: save },
  ] },
  { id: "open", title: "다른 파일 열기", items: [
    { id: "find-open", label: "찾아열기", icon: <OpenIcon />, toggle: false, onSelect: openFile },
    { id: "recent", label: "최근파일", icon: <RecentIcon />, panel: <SWRecentFiles files={recent} onFileOpen={openRecent} /> },
  ] },
]
<SWHeader documentTitle={name} documents={documents} onDocumentOpen={openFile} onDocumentNew={newFile}
  homeMenu={{ sections, onItemSelect }} onHomeMenuOpenChange={(open, reason) => log(open, reason)} />
  1. 카탈로그 5절의 데이터로 메뉴를 쓴다. 화면마다 타일·섹션·제목을 따로 만들지 않는다.
  2. 메뉴 데이터 변경: 항목 추가·삭제는 items 배열, 숨김은 hidden, 비활성은 disabled, 실행만 하는 버튼은 toggle: false, 켜고 끄는 버튼은 기본값. Active를 직접 제어하려면 homeMenu.activeIds + onActiveChange.
  3. 최근파일 패널은 선택 기능이다. 필요한 타일에만 panel: <SWRecentFiles … />를 준다. 파일을 열면 메뉴가 닫힌다(closeOnOpen).
  4. 로고 클릭은 메뉴만 연다. onHomeClick의 홈 이동은 homeMenu가 있으면 실행되지 않는다. 메뉴 항목에 홈 이동이 필요하면 항목 onSelect에서 한다.
  5. New / Open은 문서 Dropdown에 그대로 둔다(documentOpenPlacement 기본 "menu"). 로고 오른쪽에 별도 Open 버튼을 두지 않는다. 메뉴의 찾아열기·새파일은 같은 handler를 연결한다.
  6. 위치·폭은 SWHeader가 정한다. 소비 코드에서 메뉴 위치를 계산하거나 겹치는 wrapper를 만들지 않는다.

8. 하지 말 것

  • hm-sw-* class를 소비 코드에 쓰거나 CSS로 덮어쓰기
  • dist 구현·styles.css 블록 복사로 같은 컴포넌트 재작성
  • @hm-design/sw/dist/...·src/... 내부 경로 Import
  • 문서에 없는 Props 추측, 새 색·크기 값 도입
  • 같은 화면에 SWHeader와 조합 Header를 겹쳐 두 개의 Header 만들기