Files
HMDesign/docs/sw/sw-header-usage.md
T
shkim4andClaude Opus 5.5 92830fe11d chore(sw): organize Storybook components and documentation
First-pass cleanup only; no design, behavior, public API or Story ID change.
- Stories: correct stale Text Stroke / Colorize / "latest" texts, reuse
  parseSWHeaderColor, derive the field fixture from PROPERTY_FIXTURE
- Comments: date the "Latest request" notes in sw.css and the color menu
- Docs: fix removed text-stroke tokens in spec/usage/README, add spec §18
  (property fields), current-state index banners, Storybook 33 pointer
- Add docs/sw/sw-first-pass-cleanup-review-2026-10-19.md (Story table,
  structure, SAFE / REVIEW list for the second pass)

Checks: SW + app typecheck, SW build, Storybook build, 33 Story IDs
unchanged, property fields 77/77, detail-buttons verify 257 checks, 111 doc
links 0 broken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:52:46 +09:00

634 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SW Header 사용 가이드
> **현재 기준 색인(2026-10-19 1차 정리):** 아래 날짜별 안내 중 최신 항목이 같은 주제의 이전 설명보다 우선합니다. 속성 필드 13개·필드 Dropdown(`picker`·`menus`·`decimals`)은 [속성창·K 가이드 추가 연결](#속성창k-가이드-추가-연결-2026-10-16)의 2026-10-19 항목, 표시·숨김(`hiddenControls`)·메뉴 순서(`menuOrder`)·속성창 4상태·형상 Slot은 [3차 절](#구성요소-표시메뉴-순서속성창-4상태형상-slot-2026-10-16-3차), 속성Bar·`controls`·Colorize는 [후속 절](#후속-5개-요구사항-연결-2026-10-16)을 봅니다. 현재 Storybook 33개 목록은 [§12](#12-storybook)와 [1차 정리 보고](./sw-first-pass-cleanup-review-2026-10-19.md)에 있습니다.
> **상세 버튼 최신 기준(2026-10-16):** 레이어·색상·선·문자·치수 상세는 문자스타일 디자인의 `SWHeaderMenuDetail`을 공통 사용합니다. `on*Detail` 미전달 시 Hover 유지·클릭 무동작이며, Callback 전달 시 기존 1회 호출·닫힘·Focus 복귀를 유지합니다. 내부 공통 컴포넌트는 `label`, `icon`, `onClick`, `disabled`, `className`, `aria-label` 등 native 버튼 Props를 지원합니다. 새 공개 export는 추가하지 않았습니다. 비교 Story는 `SW/Header → Detail Buttons (Shared States)`(verification 태그, 직접 ID `sw-header--detail-buttons`)입니다. 과거의 Callback 미전달=Disabled 및 치수만 밝은 Hover 설명보다 이 기준이 우선합니다. [이번 검증 기록](./sw-header-detail-buttons-verification-2026-10-16.md).
> **3차 기준(2026-10-16):** 로고 외 개별 제거 `hiddenControls`, 유사 메뉴 순서 `menuOrder`, 속성창 4상태 Story·비교, 형상 Slot `shapeContent`를 추가했습니다. [3차 절](#구성요소-표시메뉴-순서속성창-4상태형상-slot-2026-10-16-3차)과 [3차 검증 기록](./sw-header-composition-shape-verification-2026-10-16.md)이 같은 주제의 이전 설명보다 우선합니다.
> **후속 5개 요구사항 기준(2026-10-16):** `SWHeaderPropertyBar`, 메뉴 단위 `controls`, `btn_colorize` Default/Hover를 추가했습니다. 상세 pill의 `solid-stroke-width`도 제거했습니다. 아래 후속 연결 절과 [새 검증 기록](./sw-header-followup-five-verification-2026-10-16.md)이 현재 기준입니다. 과거 실행 기록은 보존합니다.
> **최신 API/동작:** 아래 [속성창·K 가이드 추가 연결](#속성창k-가이드-추가-연결-2026-10-16)을 먼저 확인하세요. 색상 기준은 정확히 하나 표시되며, 상세 버튼 Hover와 목록 Selected 정책이 변경됐습니다. 과거 예시의 `colorMode=null`은 직접 색상 의미를 유지하지만 모드 표시까지 해제하지 않습니다.
`@hm-design/sw`의 SW Header를 화면에 연결하는 방법입니다. 디자인 값과 판정은 [디자인 명세](./sw-header-design-spec.md)를 따릅니다.
> 패키지는 아직 npm Registry에 게시하지 않았습니다. 아래 설치 방법은 HMDesign Workspace 또는 로컬 tarball 기준입니다.
## 1. 패키지 구조
```text
packages/sw/
├── src/
│ ├── assets/ Figma 원본 SVG와 Hover Variant (build 시 data URI로 포함, 로고 제외)
│ ├── components/
│ │ ├── sw-header/
│ │ │ ├── SWHeader.tsx 전체 헤더
│ │ │ ├── components/ 하위 컨트롤 (공개 + 내부)
│ │ │ ├── types.ts Props / 값 타입
│ │ │ └── index.ts
│ │ └── sw-theme/SWTheme.tsx 하위 컨트롤 단독 사용 시 토큰 범위
│ ├── theme/sw.css SW 전용 토큰·스타일
│ └── index.ts
├── build.mjs 독립 library build (Vite lib + tsc 선언)
├── package.json exports: "." , "./styles.css"
└── README.md
```
`@hm-design/ui`·`@hm-design/center-graphic`·OrgChart 패키지를 import하지 않습니다. Peer는 `react`·`react-dom` `^19.0.0`뿐입니다.
## 2. 설치
### HMDesign Workspace 안
루트 `package.json`의 Workspace(`packages/*`)에 포함되어 있습니다.
```bash
npm install
npm run build:sw
```
`build:packages`(typecheck·build·Storybook 전처리)도 마지막에 `build:sw`를 실행합니다.
### 다른 React 프로젝트(로컬 tarball)
```bash
npm pack -w @hm-design/sw --pack-destination ../packs
```
```bash
npm install ../packs/hm-design-sw-0.1.0.tgz
```
소비 프로젝트는 `src` 내부 파일을 참조하지 않고 `dist` 공개 API만 사용합니다.
## 3. Import
```tsx
import { SWHeader } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
```
- CSS는 JS에서 자동 import되지 않으므로 앱 진입점 등에서 한 번 import합니다.
- TypeScript 7에서 CSS side-effect import 타입 오류가 나면 Vite 프로젝트의 `/// <reference types="vite/client" />`(또는 `declare module "*.css"`)를 둡니다.
- 글꼴: 패키지는 폰트 파일을 포함하지 않습니다. 앱에서 Noto Sans KR(400/700)을 로드합니다.
## 4. 공개 API
| Export | 용도 |
|---|---|
| `SWHeader` | 전체 헤더. `ref`는 `<header>` |
| `SWHeaderHome` | 홈 버튼(로고 영역). `logoSrc` / `logo` / `logoAlt` / `onClick` |
| `SWHeaderDocumentTitle` | 파일명 버튼. `documents`를 주면 문서 Dropdown(`title`, `onClick`, `documents`, `menuOpen`, `onMenuOpenChange`, `onDocumentSelect`, `onNew`, `onOpen`, `documentIcon`, `newLabel`, `openLabel`) |
| `SWHeaderLayerControl`, `SWHeaderColorControl`, `SWHeaderLineStyle`, `SWHeaderTextStyle`, `SWHeaderDimensionStyle` | 속성 필드 5종. `SWHeaderLayerControl`은 선택 `state`(현재 레이어의 표시·동결·잠금)로 필드 아이콘을 회색으로 표시 |
| `SWHeaderLayerMenu` | 레이어 필드 + 레이어 Dropdown(`value`, `layers`, `activeLayerId`, `open`, `onOpenChange`, `onClick`, `onLayerSelect`, `onVisibilityChange`, `onFreezeChange`, `onLockChange`, `onDetail`). `SWHeader`는 `layers`가 있을 때 이것을 사용 |
| `SWHeaderColorMenu` | 색상 필드 + 색상 Dropdown(2026-10-16, `value`, `colors`, `userColors`, `mode`, `open`, `onOpenChange`, `onClick`, `onColorSelect`, `onModeChange`, `onColorInput`, `onPick`, `onDetail`). `SWHeader`는 `colors`가 있을 때 사용 |
| `SWHeaderLineStyleMenu` | 선스타일 필드 + 3열 Dropdown(`value`, `lineTypes`, `lineWeights`, `activeLineTypeId`, `activeLineWeightId`, `onLineTypeSelect`, `onLineWeightSelect`, `onLineWeightValueChange`, `onScaleChange`, `onDetail`, 열림 Props). `SWHeader`는 `lineTypes` 또는 `lineWeights`가 있을 때 사용 |
| `SWHeaderTextStyleMenu`, `SWHeaderDimensionStyleMenu` | 문자·치수스타일 필드 + 목록 Dropdown(`value`, `textStyles` / `dimensionStyles`, `active*Id`, `on*Select`, `onDetail`, 열림 Props) |
| `SWHeaderLinePreview`, `swHeaderLinePatterns` | 70 × 2 선 미리보기와 Figma CENTER(22·7·22·7) / HIDDEN(33·33) 패턴 |
| `SWHeaderInput` | 수정 가능한 SW Input(Figma input 26:11656 · cursor 26:11664). 레이어 상세 등 상세 영역의 값 편집용 |
| `parseSWHeaderColor`, `parseSWHeaderNumber` | Input 유효성 검사 도우미(`r,g,b` / `#rrggbb`, 소수) |
| `SWHeaderProperties` | 상세보기(속성Bar / 속성창) |
| `SWHeaderActions` | 인쇄 · 팝업 가이드 |
| `SWHeaderWindowControls` | 창 이동 · 최소화 · 종료 |
| `SWTheme` | 하위 컨트롤을 헤더 밖에서 쓸 때 토큰·글꼴·버튼 초기화 범위 |
| 타입 | `SWHeaderProps`, `SWHeaderMenuId`, `SWHeaderLayerValue`, `SWHeaderLayer`, `SWHeaderColorValue`, `SWHeaderLineStyleValue`, `SWHeaderTextStyleValue`, `SWHeaderDimensionStyleValue`, `SWHeaderDocument`, `SWHeaderColorMode`, `SWHeaderColorOption`, `SWHeaderColorInputValue`, `SWHeaderLineType`, `SWHeaderLineWeight`, `SWHeaderTextStyleOption`, `SWHeaderDimensionStyleOption`, `SWHeaderInputValueType`, 각 하위 컴포넌트 `*Props`, `SWThemeProps` |
내부 구현(`SWHeaderField`, `SWHeaderMore`, `useSWHeaderDropdown`, `SWHeaderMenuDetail`)은 공개하지 않습니다.
## 5. SWHeader Props
`HTMLAttributes<HTMLElement>`(단 `title`, `children`, `color` 제외)를 함께 받습니다. `className`·`style`·`aria-*`는 `<header>`에 전달됩니다.
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| `documentTitle` | `string` | 필수 | 파일명. 좁으면 말줄임, 전체 이름은 `title` 툴팁 |
| `drawingModeLabel` | `string` | `"그리기모드"` | 그리기 모드 라벨 |
| `layer` | `{ name }` | `{ name: "0" }` | 현재 레이어(`layers`에 `activeLayerId`와 같은 항목이 없을 때 필드에 표시) |
| `layers` | `SWHeaderLayer[]` | — | 레이어 필드 아래 Dropdown 목록(배열 순서 그대로). 없으면 Dropdown 없이 기존 필드 버튼, `[]`이면 하단(레이어 상세)만 |
| `activeLayerId` | `string` | — | 현재 레이어. 필드에 이름·상태 아이콘 표시, 목록에서 이름만 `#FFC600` |
| `layerMenuOpen` | `boolean` | — | 레이어 Dropdown 열림 상태(제어형). 없으면 내부에서 관리 |
| `color` | `{ value, swatch? }` | `{ value: "ByLayer", swatch: "#ffffff" }` | 색상 값 표시와 칩 색 |
| `lineStyle` | `{ name, weight, scale }` | `ByLayer / ByLayer / 1.00` | 선스타일 |
| `textStyle` | `{ font, name }` | `Arial / Standard` | 문자스타일 |
| `dimensionStyle` | `{ name }` | `Standard` | 치수스타일 |
| `activeMenu` | `"layer" \| "color" \| "lineStyle" \| "textStyle" \| "dimensionStyle" \| null` | `null` | 선택(열림) 표시 |
| `propertyBarOpen` | `boolean` | `false` | 속성Bar 눌림 |
| `propertyPanelOpen` | `boolean` | `false` | 속성창 눌림 |
| `disabled` | `boolean` | `false` | 메뉴·속성·인쇄/가이드 비활성(창 제어 제외) |
| `tools` | `ReactNode` | — | 객체 수정 영역의 필드 뒤 추가 도구 |
| `logoSrc` | `string` | — | 로고 이미지 URL. 32 × 17 로고 영역에 `contain`으로 맞춤 |
| `logo` | `ReactNode` | — | 로고 요소(인라인 SVG 등). `logoSrc`보다 우선 |
| `logoAlt` | `string` | — | 제품명. 홈 버튼 이름이 "<logoAlt> 홈"(없으면 "홈") |
| `documents` | `SWHeaderDocument[]` | — | 파일명 아래 문서 Dropdown 목록. 없으면 Dropdown 없이 기존 버튼, `[]`이면 New / Open만 |
| `documentMenuOpen` | `boolean` | — | 문서 Dropdown 열림 상태(제어형). 없으면 내부에서 관리 |
| `activeDocumentId` | `string` | — | 현재 문서. 목록 순서는 그대로 두고 이 문서의 이름만 `#FFC600` |
| `documentIcon` | `ReactNode` | Figma 문서 아이콘 | `icon`이 없는 문서의 아이콘(29 × 12 영역) |
| `documentNewLabel` / `documentOpenLabel` | `string` | `+ New` / `Open` | 하단 버튼 글자 |
| `colors` | `SWHeaderColorOption[]` | — | 색상 Dropdown 기본 팔레트(2026-10-16). 없으면 Dropdown 없이 기존 필드 버튼 |
| `userColors` | `SWHeaderColorOption[]` | — | 사용자 색상(개수 가변, 2px 간격 줄바꿈, 4행 초과 시 팔레트 영역 스크롤) |
| `colorMode` | `"byLayer" \| "byBlock" \| null` | — | 눌린 ByLayer / ByBlock. `null`·미지정 = 명시 색 |
| `colorMenuOpen` | `boolean` | — | 색상 Dropdown 열림(제어형). 없으면 내부 관리 |
| `lineTypes` / `lineWeights` | `SWHeaderLineType[]` / `SWHeaderLineWeight[]` | — | 선스타일 Dropdown의 선 종류 / 가중치 열. 둘 다 없으면 기존 필드 버튼. 스케일 열은 `lineStyle.scale` |
| `activeLineTypeId` / `activeLineWeightId` | `string` | `lineStyle.name` / `.weight`와 같은 이름의 행 | 현재 행(이름 `#FFC600`) |
| `lineStyleMenuOpen` | `boolean` | — | 선스타일 Dropdown 열림(제어형) |
| `textStyles` / `dimensionStyles` | `SWHeaderTextStyleOption[]` / `SWHeaderDimensionStyleOption[]` | — | 문자 / 치수스타일 Dropdown 행. 없으면 기존 필드 버튼 |
| `activeTextStyleId` / `activeDimensionStyleId` | `string` | `textStyle.name` / `dimensionStyle.name`과 같은 이름의 행 | 현재 행 |
| `textStyleMenuOpen` / `dimensionStyleMenuOpen` | `boolean` | — | 각 Dropdown 열림(제어형) |
Dropdown 데이터 타입(모두 `id`로 식별, 배열 순서 = 표시 순서, 패키지에 고정 데이터 없음):
| 타입 | 필드 |
|---|---|
| `SWHeaderColorOption` | `{ id; color; value?; label?; disabled? }` — `color` CSS 색, `value` 보고·표시 값(기본 `color`의 `r,g,b`), `label` 접근성 이름 |
| `SWHeaderColorInputValue` | `{ value: "r,g,b"; rgb: [r, g, b]; hex: "#rrggbb" }` |
| `SWHeaderLineType` | `{ id; name; pattern?: number[]; disabled? }` — `pattern` 70px 미리보기의 대시 길이(4px 간격, 비율 맞춤), 없으면 실선 |
| `SWHeaderLineWeight` | `{ id; name; value?; editable?; disabled? }` — `value` 오른쪽 값(ByDefault 0.25), `editable` 이면 값 Input(User) |
| `SWHeaderTextStyleOption` | `{ id; name; font; disabled? }` |
| `SWHeaderDimensionStyleOption` | `{ id; name; disabled? }` |
`SWHeaderDocument`: `{ id: string; name: string; title?: string; icon?: ReactNode; disabled?: boolean }` — `title`은 툴팁(기본 `name`).
`SWHeaderLayer`: `{ id: string; name: string; color?: string; visible?: boolean; frozen?: boolean; locked?: boolean; disabled?: boolean }` — `color`는 16px 견본 색(기본 `#ffffff`), 기본값은 `visible: true`, `frozen: false`, `locked: false`. 확정 매핑([명세 §5.2.1](./sw-header-design-spec.md#521-아이콘-onoff와-데이터-의미)):
| 아이콘 | 컬러 | 회색 | 클릭 시 Callback 값 |
|---|---|---|---|
| 전구 | `visible=true` | `visible=false` | `onLayerVisibilityChange(layer, !visible)` |
| 눈꽃 | `frozen=false`(동결 해제) | `frozen=true`(동결) | `onLayerFreezeChange(layer, !frozen)` |
| 자물쇠 | `locked=false`(잠금 해제) | `locked=true`(잠금) | `onLayerLockChange(layer, !locked)` |
헤더는 값을 직접 바꾸지 않으므로 호스트가 받은 값으로 `layers`를 갱신해야 아이콘 색이 바뀝니다.
> **로고는 확정 디자인이 아닙니다.** 로고 영역의 위치·크기는 공통 디자인이고, 로고 이미지는 사용하는 SW 프로그램이 정합니다. 패키지에는 로고가 들어 있지 않으며 `logo`·`logoSrc`를 전달하지 않으면 로고 없이 표시됩니다(배치는 그대로). Storybook의 기본 로고는 임시 자산(`sw-header-logo-placeholder.svg`)입니다.
기본값은 Figma 표시값이 아니라 중립값입니다. Figma 샘플 값은 Storybook `figmaArgs`를 참고합니다.
## 6. Callback
| Callback | 호출 시점 |
|---|---|
| `onHomeClick` | 홈(로고) 클릭 |
| `onDocumentClick` | 파일명 클릭(Dropdown 열고 닫을 때도 호출) |
| `onDocumentMenuOpenChange(open)` | 문서 Dropdown 열림·닫힘 |
| `onDocumentSelect(document)` | 문서 항목 선택. 문서를 여는 동작은 호스트가 구현 |
| `onDocumentClose(document)` | 행의 X(문서 닫기). 주면 모든 행에 X가 생기고 Hover·키보드 Focus한 행에만 보입니다. 선택 Callback과 중복되지 않습니다. 실제 닫기·목록 갱신·다음 선택은 호스트가 처리 |
| `onDocumentNew`, `onDocumentOpen` | Dropdown 하단 `+ New` / `Open` |
| `onLayerOpen`, `onColorOpen`, `onLineStyleOpen`, `onTextStyleOpen`, `onDimensionStyleOpen` | 각 필드 클릭(Dropdown이 있으면 열고 닫을 때도 호출) 또는 더보기 항목 선택 1회(대표 필드 전환, §7 "반응형 More") |
| `onLayerMenuOpenChange(open)` | 레이어 Dropdown 열림·닫힘 |
| `onLayerSelect(layer)` | 레이어 행(견본·이름) 선택. 목록이 닫힙니다. 현재 레이어 변경은 호스트가 `activeLayerId`로 반영 |
| `onLayerVisibilityChange(layer, nextVisible)` | 전구 클릭. 목록 유지, 선택 Callback 없음 |
| `onLayerFreezeChange(layer, nextFrozen)` | 눈꽃 클릭. 목록 유지, 선택 Callback 없음 |
| `onLayerLockChange(layer, nextLocked)` | 자물쇠 클릭. 목록 유지, 선택 Callback 없음 |
| `onLayerDetail()` | 하단 `레이어 상세`. 호출 후 목록이 닫힙니다. 없으면 버튼은 Figma 외형 그대로 비활성 |
| `onColorMenuOpenChange(open)` 등 `on*MenuOpenChange` | 각 Dropdown 열림·닫힘(색상·선/문자/치수스타일) |
| `onColorSelect(option)` | 팔레트·사용자 색상 칩. 목록 유지 |
| `onColorModeChange(mode)` | ByLayer / ByBlock. 목록 유지. 상호 배타 표시는 호스트가 `colorMode`로 반영 |
| `onColorInput(value)` | RGB 입력 확정(Enter·Focus 이동·목록 닫힘). 유효한 `r,g,b`(0–255) 또는 `#rrggbb`만, 값이 바뀐 경우만 |
| `onColorPick()` | 색상 추출 버튼. 목록이 닫히고 실제 추출(도면 픽셀 등)은 호스트가 구현 |
| `onLineTypeSelect(type)` / `onLineWeightSelect(weight)` | 선 종류 / 가중치 행. 다른 열 값은 건드리지 않고 목록 유지 |
| `onLineWeightValueChange(weight, value)` | `editable` 가중치 Input 확정(0 이상 소수) |
| `onLineScaleChange(value)` | 스케일 Input 확정(0 초과 소수) |
| `onTextStyleSelect(style)` / `onDimensionStyleSelect(style)` | 행 선택 후 목록 닫힘 |
| `onColorDetail`, `onLineStyleDetail`, `onTextStyleDetail`, `onDimensionStyleDetail` | 하단 `… 상세`. 전달 시 호출 후 목록 닫힘. 없으면 공통 Default/Hover를 유지하고 클릭은 무동작. 상세 팝업은 패키지 범위 밖(추후 Figma 전달 후) |
| `onPropertyBarToggle`, `onPropertyPanelToggle` | 속성 토글 클릭 또는 더보기 항목 선택 |
| `onPrint`, `onPopupGuide` | 인쇄·팝업 가이드 |
| `onWindowMoveStart(event)` | 창 이동 영역 pointer down |
| `onMinimize`, `onClose` | 최소화·종료 |
헤더는 선택·열림 상태를 스스로 저장하지 않습니다. 호스트가 Callback에서 상태를 바꾸고 Props로 다시 내려줍니다. 실제 드롭다운 목록, 파일 열기, 창 이동·최소화·종료(브라우저 / Electron / 설치형 SW마다 다름)는 호스트가 구현합니다.
## 7. 전체 헤더 예제
```tsx
import { useState } from "react"
import { SWHeader, type SWHeaderMenuId } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
export function DrawingHeader({ fileName }: { fileName: string }) {
const [activeMenu, setActiveMenu] = useState<SWHeaderMenuId | null>(null)
const [panelOpen, setPanelOpen] = useState(false)
const toggle = (id: SWHeaderMenuId) => () => setActiveMenu(current => (current === id ? null : id))
return (
<SWHeader
documentTitle={fileName}
layer={{ name: "0_Layer_Cludmark" }}
color={{ value: "255,229,0", swatch: "#ffe500" }}
lineStyle={{ name: "Continuous", weight: "0.25", scale: "1.00" }}
textStyle={{ font: "Arial", name: "Standard" }}
dimensionStyle={{ name: "Standard" }}
activeMenu={activeMenu}
propertyPanelOpen={panelOpen}
onLayerOpen={toggle("layer")}
onColorOpen={toggle("color")}
onLineStyleOpen={toggle("lineStyle")}
onTextStyleOpen={toggle("textStyle")}
onDimensionStyleOpen={toggle("dimensionStyle")}
onPropertyPanelToggle={() => setPanelOpen(open => !open)}
onClose={() => window.close()}
/>
)
}
```
### 로고 지정 예
```tsx
import productLogo from "./assets/product-logo.svg" // 각 프로그램의 로고 파일
<SWHeader documentTitle={fileName} logoSrc={productLogo} logoAlt="제품명" />
// React 요소로 전달(인라인 SVG 등). logoSrc보다 우선합니다.
<SWHeader documentTitle={fileName} logo={<ProductMark />} logoAlt="제품명" />
```
- 로고 영역은 32 × 17px입니다. 가로·세로 비율이 달라도 영역 안에 맞춰지고 잘리지 않습니다. 선명하게 보이도록 SVG 또는 64 × 34px 이상 이미지를 권장합니다.
- 로고 영역 크기는 디자인 기준이므로 바꾸지 않는 것을 권장합니다(토큰 `--hm-sw-header-logo-width` / `-logo-height`).
### 문서 Dropdown 예
```tsx
const [openDocuments, setOpenDocuments] = useState<SWHeaderDocument[]>([])
<SWHeader
documentTitle={current.name}
documents={openDocuments} // 각 프로그램의 문서 목록(표시 순서 그대로)
activeDocumentId={current.id} // 이름만 #FFC600, 순서는 그대로
onDocumentSelect={document => activate(document.id)}
onDocumentClose={document => closeDocument(document.id)} // 목록·현재 문서는 호스트가 갱신
onDocumentNew={createDocument}
onDocumentOpen={showOpenDialog} // 파일 열기는 호스트가 구현
/>
```
- 클릭으로 열고 닫습니다(Hover로 열리지 않음). 바깥 클릭·Escape·항목 선택·New/Open 후 닫힙니다.
- Dropdown은 헤더 아래로 겹쳐 열립니다. SWHeader의 조상 요소에 `overflow: hidden`이 있으면 잘리므로, 헤더는 페이지 최상단처럼 잘리지 않는 위치에 둡니다.
### 레이어 Dropdown 예
```tsx
const [layers, setLayers] = useState<SWHeaderLayer[]>(() => drawing.layers.map(toHeaderLayer))
const patch = (id: string, change: Partial<SWHeaderLayer>) =>
setLayers(list => list.map(layer => (layer.id === id ? { ...layer, ...change } : layer)))
<SWHeader
documentTitle={fileName}
layers={layers} // 각 프로그램의 레이어 목록(표시 순서 그대로)
activeLayerId={currentLayerId} // 필드 표시 + 목록에서 이름만 #FFC600
onLayerSelect={layer => setCurrentLayer(layer.id)}
onLayerVisibilityChange={(layer, visible) => { viewer.setLayerVisible(layer.id, visible); patch(layer.id, { visible }) }}
onLayerFreezeChange={(layer, frozen) => { viewer.setLayerFrozen(layer.id, frozen); patch(layer.id, { frozen }) }}
onLayerLockChange={(layer, locked) => { viewer.setLayerLocked(layer.id, locked); patch(layer.id, { locked }) }}
onLayerDetail={openLayerManager} // 없으면 버튼 비활성(임의 팝업 없음)
/>
```
- 레이어 필드를 클릭해야 열립니다(Hover는 테두리·글자색만). 행 선택·Escape·바깥 클릭·Tab 이탈·레이어 상세 후 닫힙니다. 전구·눈꽃·자물쇠 클릭은 목록을 유지합니다.
- 헤더는 레이어 상태를 저장하지 않습니다. Callback에서 도면(`viewer.*`은 예시)과 `layers`를 함께 갱신해야 아이콘이 바뀝니다. 실제 표시·동결·잠금 처리와 편집 가능 여부 판단은 호스트 책임입니다.
- 키보드: 필드에서 ↓로 열면 현재 레이어에 Focus, ↓↑ 행 이동(같은 열), ←→ 행 안 이동, Enter/Space 실행. 상세 규격은 [디자인 명세 §5.2](./sw-header-design-spec.md#52-레이어-dropdown-figma-headermenu레이어-2610940-2026-10-16).
### 색상 · 선/문자/치수스타일 Dropdown 예 (2026-10-16)
```tsx
const [color, setColor] = useState<SWHeaderColorValue>({ value: "ByLayer", swatch: "#ffffff" })
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>("byLayer")
const [lineStyle, setLineStyle] = useState({ name: "Continuous", weight: "0.25", scale: "1.00" })
<SWHeader
documentTitle={fileName}
color={color}
colors={program.palette} // [{ id: "red", color: "#ff0000", label: "빨강" }, ...]
userColors={program.userColors} // 개수 가변
colorMode={colorMode}
onColorSelect={option => { viewer.setColor(option.color); setColor({ value: option.value ?? option.color, swatch: option.color }); setColorMode(null) }}
onColorModeChange={mode => { viewer.setColorMode(mode); setColorMode(mode) }}
onColorInput={({ value, hex }) => { viewer.setColor(hex); setColor({ value, swatch: hex }); setColorMode(null) }}
onColorPick={startPickFromDrawing} // 실제 픽셀 추출은 호스트
onColorDetail={openColorDetail} // 없으면 Hover 유지·클릭 무동작(상세 팝업 미제공)
lineStyle={lineStyle}
lineTypes={program.lineTypes} // [{ id: "center", name: "CENTER", pattern: swHeaderLinePatterns.center }, ...]
lineWeights={program.lineWeights} // [{ id: "user", name: "User", value: "0.25", editable: true }, ...]
onLineTypeSelect={type => setLineStyle(s => ({ ...s, name: type.name }))} // 가중치·스케일 유지
onLineWeightSelect={weight => setLineStyle(s => ({ ...s, weight: weight.value ?? weight.name }))}
onLineWeightValueChange={(weight, value) => updateWeight(weight.id, value)}
onLineScaleChange={scale => setLineStyle(s => ({ ...s, scale }))}
textStyle={textStyle}
textStyles={program.textStyles} // [{ id, font: "Arial", name: "Standard" }, ...]
onTextStyleSelect={style => setTextStyle({ font: style.font, name: style.name })}
dimensionStyle={dimensionStyle}
dimensionStyles={program.dimensionStyles} // [{ id, name: "ISO - 25" }, ...]
onDimensionStyleSelect={style => setDimensionStyle({ name: style.name })}
/>
```
- 클릭으로 열고 닫습니다(Hover는 노란 테두리·글자만). 다른 Dropdown을 열면 앞의 것이 닫히고, 바깥 클릭·Escape·Tab 이탈로 닫힙니다. 헤더 높이(49px)와 다른 필드 위치는 변하지 않습니다.
- 색상·선스타일은 여러 속성을 이어서 바꿀 수 있도록 선택 후에도 열려 있고, 문자·치수스타일은 행 선택 후 닫힙니다. 색상 추출과 `… 상세`는 호출 후 닫힙니다.
- 헤더는 값을 저장하지 않습니다. Callback 값으로 `color` / `colorMode` / `lineStyle` 등을 갱신해야 필드가 바뀝니다. 명시 색을 고를 때 ByLayer/ByBlock을 해제할지는 호스트 정책입니다(Storybook 예제는 해제).
- 키보드: 필드에서 ↓로 열면 현재(눌린) 항목 또는 첫 항목에 Focus, ↓↑로 Dropdown 안 버튼·Input 순서대로 이동, Enter/Space 실행, Escape 닫기(필드로 Focus 복귀).
- Hover: 상세 버튼 5개는 문자스타일 상세 기준 공통 Hover(배경 .25·radius 25·아이콘 35%·글자 35%)를 적용합니다. ByLayer/ByBlock은 기존 후속 Hover 기준을 따릅니다. 색상 칩·색상 추출·목록 행(선 종류·가중치·스케일·문자·치수)·Input의 현재 동작은 유지합니다.
- 좁은 헤더: 목록은 헤더 안으로 옮겨지며, 468px 선스타일 목록은 헤더 폭 480 미만에서 세로로 쌓입니다.
### 동적 데이터 구조
모든 Dropdown(문서·레이어·색상·선 종류·가중치·문자·치수스타일)은 Props 배열로만 그립니다.
- 항목 추가·삭제·이름/값 변경·순서 변경은 배열을 바꾸면 되고 컴포넌트 소스 수정이 필요 없습니다. 각 항목은 `id`로 식별되어 바뀐 항목만 다시 그리고 다른 행의 상태(입력 중 값 등)를 보존합니다.
- 현재 항목(`active*Id`)이 배열에서 사라지면 선택 표시가 없어지고 필드는 값 Props(`layer`, `lineStyle` 등)를 그대로 표시합니다. 다음 현재 항목은 호스트가 정합니다.
- `disabled`(선택 불가), `editable`(가중치 값 Input), Input의 `readOnly` / `disabled`를 항목별로 줄 수 있습니다.
- 목록이 길면 Dropdown 안에서만 스크롤합니다(행 목록 최대 240px = 8행, 사용자 색상은 팔레트 118px). 헤더 레이아웃은 유지됩니다.
- Storybook `SW / Header / Dynamic Data`에서 추가·삭제·이름·값·순서 변경을 직접 확인할 수 있습니다.
### 수정 가능한 Input — `SWHeaderInput`
레이어 상세 등 상세 영역에서 속성값을 직접 고칠 때 씁니다(Figma input 26:11656, cursor 26:11664). 상세 팝업 레이아웃은 아직 없으므로 Input과 계약만 제공합니다.
```tsx
import { SWHeaderInput, parseSWHeaderNumber, SWTheme } from "@hm-design/sw"
<SWTheme>
<SWHeaderInput
aria-label="X 좌표"
valueType="number" // "text"(기본) | "number"
value={entity.x} // 확정값(호스트). 바뀌면 최신 값 표시
onValueChange={draft => preview(draft)} // 편집 중 임시값(선택)
onCommit={next => updateEntity({ x: next })} // Enter · Focus 이동 · 감싼 Dropdown 닫힘, 유효 + 변경 시만
onCancel={() => clearPreview()} // Escape: 확정값으로 복원
parse={draft => parseSWHeaderNumber(draft, { min: 0 })} // 선택: 형식·범위 직접 지정
/>
</SWTheme>
```
- 상태: Default(배경·테두리 없음, 58 × 20 기준) / Hover(1px `rgba(212,203,189,.25)`, 24px) / 편집 중(`#0F0D0A`, 1px `.3`, 24px). 글자 `#E60403` 14px + 검정 Text Stroke, 글자 위치는 상태와 무관하게 고정. 폭은 값 길이에 맞춥니다.
- 커서: 네이티브 caret을 `#E60403`(`caret-color`)으로 표시하고 깜박임도 네이티브입니다(가짜 커서 없음). 선택·IME(한글 조합 중 Enter는 확정하지 않음)는 브라우저 기본 동작입니다.
- `onCommit`이 없거나 `readOnly`이면 읽기 전용(Hover·편집 표시 없음), `disabled`는 40%.
- 선택형(목록) 입력은 Figma 디자인 전달 후 같은 데이터 정의(`id`·이름·값·타입)로 확장합니다. 현재는 텍스트·숫자와 사용자 `parse`(임의 형식)를 지원합니다.
### 반응형 More — 대표 필드 전환
헤더가 좁아 필드가 더보기(…)로 옮겨졌을 때, 더보기에서 필드 항목을 고르면 그 필드가 객체 수정 영역 첫 칸(레이어 필드 자리)에 표시되고 Dropdown이 바로 아래에 열립니다. Focus는 그 필드로 옮겨갑니다.
- 레이어는 그동안 더보기로 이동하며, 더보기 → 레이어로 원래 배치로 돌아갑니다. 이미 화면에 있는 필드는 더보기에 나오지 않습니다.
- 전환은 UI 상태입니다. 데이터 Callback은 호출되지 않고, 필드 클릭과 같은 `on*Open` 1회와 `on*MenuOpenChange(true)`만 호출됩니다. 값은 그대로 유지됩니다.
- 폭이 넓어져 그 필드가 다시 보이는 단계에서는 1920 기준 배치로 돌아갑니다(Breakpoint 1800 / 1180 / 1080 / 900 / 640 / 520은 그대로).
- Dropdown 데이터(`colors` 등)가 없는 필드도 같은 방식으로 대표 칸에 표시됩니다(목록 없이 `on*Open`만).
## 8. 하위 컴포넌트 재사용
헤더 밖에서는 `SWTheme`로 감쌉니다. 소비자가 `hm-sw-*` class를 붙이지 않습니다.
```tsx
import { SWTheme, SWHeaderLayerControl, SWHeaderColorControl, SWHeaderProperties } from "@hm-design/sw"
<SWTheme>
<SWHeaderLayerControl value={{ name: "0" }} onClick={openLayers} />
<SWHeaderColorControl value={{ value: "255,229,0", swatch: "#ffe500" }} selected />
<SWHeaderProperties panelOpen={panelOpen} onPanelToggle={togglePanel} />
</SWTheme>
```
`SWTheme`는 배치(flex 등)를 정하지 않습니다. 배치는 `className`·`style`로 소비 화면에서 정합니다.
## 9. 테마 커스터마이징
`--hm-sw-header-*` 토큰만 바꿉니다. 사용자 확정 3개 면(파일명·객체 수정·속성 입력)은 기본값을 유지하는 것을 권장합니다.
```tsx
import type { CSSProperties } from "react"
<SWHeader documentTitle="a.dwg" style={{ "--hm-sw-header-selected-color": "#ffd84d" } as CSSProperties} />
```
토큰 목록과 사용처는 [디자인 명세 §14](./sw-header-design-spec.md#14-sw-전용-토큰-목록)를 따릅니다. `@hm-design/ui` Theme Runtime(JSON)과는 연결되어 있지 않습니다.
## 10. DWG/DXF 뷰어 연동 시 고려사항
- 기존 뷰어 `Toolbar.tsx`의 파일명(`fileName`)은 `documentTitle`로, 패널 토글(`propsOpen` 등)은 `propertyPanelOpen` + `onPropertyPanelToggle`로 연결할 수 있습니다.
- 뷰어의 파일 열기·샘플·Model/Layout 탭·2.5D·어두운 배경·로드 상태는 Figma SW Header에 없는 뷰어 전용 도구입니다. 필요하면 `tools` 슬롯에 호스트가 렌더링합니다(스타일은 호스트 책임).
- 헤더 높이는 49px입니다. 고정 배치(`position: fixed`)는 `className`·`style`로 호스트에서 지정합니다.
- 반응형은 모두 헤더 자신의 폭(container query)을 기준으로 하므로 사이드 패널 때문에 헤더가 좁아져도 동작합니다. 단계(1800 / 1180 / 1080 / 900 / 640 / 520)는 [디자인 명세 §12](./sw-header-design-spec.md#12-반응형)를 따릅니다.
- 뷰어는 현재 `@hm-design/ui`의 SWHeader 패턴을 사용합니다. 이 패키지로 바꾸는 작업은 이번 범위가 아닙니다.
## 10.1 상호작용 상태
- 그룹 필드 Hover(현재 추가 기준): 레이어·색상·선/문자/치수스타일은 버튼 전체 Hover 시 라벨·값과 지정된 선/폰트/치수 미리보기가 `#FFC600`이 됩니다. 기존 Gradient Hover Border와 검정 외곽선은 유지하며 색상 견본·레이어 상태 아이콘은 바꾸지 않습니다. Disabled는 새 Hover 효과를 적용하지 않고 Selected 색은 기존대로입니다. 상세보기의 텍스트 전용 Hover는 변경하지 않습니다. [디자인 명세 §11.5](./sw-header-design-spec.md#115-현재-추가-기준--1400ms-및-그룹-필드-hover-2026-10-16).
- Hover Border: 테두리가 있는 버튼(홈, 5개 속성 필드, 더보기, 인쇄, 팝업 가이드)은 마우스를 올리면 테두리가 Figma 홈 버튼의 노란 gradient로 바뀝니다. 5개 필드 내부 텍스트·미리보기는 위 추가 기준을 함께 적용합니다. 파일명과 속성Bar·속성창은 예외로, 테두리 없이 글자만 `#FFC600`으로 바뀝니다(파일명 Dropdown은 클릭해야 열림). 문서 Dropdown 안은 Figma hover를 따릅니다: 목록 항목은 검정 40% 배경, `+ New` / `Open`은 배경이 진해지고(20% → 30%) 글자가 검정 100%가 됩니다(노란 테두리 없음). 배경·크기·위치는 변하지 않습니다. 비활성 버튼은 바뀌지 않습니다.
- Hover(창 제어 · 바로가기 아이콘): 인쇄·팝업 가이드는 노란 테두리와 함께 아이콘의 `#B6D0C9` 부분만 `#FFC600`으로 바뀝니다(외곽선·그림자·타일 색은 그대로). 최소화는 검정 20% 배경 + 흰 막대, 종료는 `#E40000` 배경 + 흰 X로 바뀝니다(Figma 26:12156 / 26:12161, 32 × 48 유지, 노란 테두리 없음). 소비자는 class나 Prop을 추가하지 않습니다. 상세는 [디자인 명세 §11.2](./sw-header-design-spec.md#112-창-제어--바로가기-아이콘-hover-2026-10-16).
- Selected(`activeMenu`, `propertyBarOpen`, `propertyPanelOpen`): 글자만 `#FFC600`. Hover와 함께 표시됩니다.
- Focus: 키보드 Focus 윤곽선은 Hover 테두리 위에 표시됩니다.
- Hover 테두리 색은 `--hm-sw-header-hover-border-gradient` 토큰, 최소화 Hover 배경·막대 색은 `--hm-sw-header-window-button-hover-background` / `--hm-sw-header-window-icon-hover-color` 토큰으로 바꿀 수 있습니다. 종료·인쇄·팝업 가이드 Hover 아이콘은 패키지 SVG Asset입니다. 대상·제외 목록은 [디자인 명세 §11.1](./sw-header-design-spec.md#111-hover-테두리-공통-상호작용-규칙)을 따릅니다.
- Text Stroke(2026-10-16): Figma에 Stroke가 있는 헤더·Dropdown 텍스트는 검정 외곽선(14·16px 1.3px, 12px 라벨 1px)을 표시합니다(자기 색 0.3px Stroke는 2026-10-16 추가 수정으로 제거). Hover·Selected는 채움색만 바뀌고 외곽선은 유지됩니다. 더보기 메뉴·창 이동 Tooltip은 Figma에 Stroke가 없어 적용하지 않고, `+ New` / `Open`은 요청에 따라 기존 그대로입니다. 외곽선 색은 `--hm-sw-header-text-outline-color`, 외곽선 전체는 `--hm-sw-header-text-outline` / `--hm-sw-header-label-outline`으로 바꿀 수 있습니다(`--hm-sw-header-text-stroke-width`는 제거됨)([명세 §6.1](./sw-header-design-spec.md#61-text-stroke-2026-10-16)).
## 10.2 창 이동 Hover Motion
별도 Prop이나 의존성 없이 Hover하면 GIF의 경로·구간 비율을 유지하면서 후속 사용자 요청에 따라 **1.4초마다 반복**합니다(원본 GIF 1.64초와 다름). 커서가 아래에서 위로 먼저 올라가고 약 444ms에 문구가 나타난 뒤 창·커서·문구가 오른쪽 위로 이동합니다. 약 888ms에 이동을 마치고 약 512ms 유지한 뒤 반복합니다. 28px / −4.5px 이동과 가속·감속 비율은 그대로이며 이탈 시 즉시 기본 SVG로 돌아갑니다. 크기·줄무늬·SVG·Tooltip 규격은 SW/Figma를 유지합니다.
`onWindowMoveStart`는 기존 `PointerEventHandler<HTMLDivElement>`입니다. 예를 들어 호스트는 `event.currentTarget.setPointerCapture(event.pointerId)`로 이후 포인터를 추적할 수 있습니다. 패키지는 포인터를 캡처하거나 실제 창을 이동시키지 않습니다. 아이콘과 `창 이동` Tooltip은 `pointer-events: none`이며 Callback 이벤트와 최소화·종료 클릭을 가로채지 않습니다. 누르는 동안 CSS `:active`로 모션을 멈추고 Tooltip을 숨깁니다. Tooltip은 창 이동 영역 밖의 최소화 및 종료 왼쪽 일부에 겹칠 수 있습니다.
- 헤더 폭 ≥1080px: 88px 영역, GIF 기준 반복 모션과 Tooltip.
- 640–1079px: 24px 드래그 영역, 그림은 숨기고 정적 Tooltip만 표시.
- <640px: 기존대로 창 이동 영역 숨김.
- Reduced Motion: 기본 아이콘과 정적 Hover Tooltip을 유지하고 애니메이션을 끕니다. 24px 단계에서는 기존대로 그림이 숨겨집니다. OS 설정을 재생 중 바꾸어도 적용됩니다.
- Focus: 창 이동 div는 기존대로 Tab 대상이 아니며 키보드 창 이동·Focus Tooltip을 제공하지 않습니다. 실제 호스트의 키보드 창 이동 대안과 보조기술 검증은 별도로 필요합니다. 최소화·종료는 이름이 있는 기본 버튼이며 Focus 표시가 유지됩니다.
Tooltip은 45 × 17px, 검정 80% 배경과 흰색 12px/400 글자입니다. Tooltip만 Figma의 Inter를 우선하고 한글·미설치 환경은 기존 SW 글꼴로 fallback합니다. 별도 폰트 설치는 하지 않습니다. 경로·자산은 [명세 §11.4](./sw-header-design-spec.md#114-현재-창-이동-motion--ani_hover_movegif-우선-2026-10-16), 현재 시간과 필드 Hover는 [명세 §11.5](./sw-header-design-spec.md#115-현재-추가-기준--1400ms-및-그룹-필드-hover-2026-10-16), 결과는 [현재 통합 검증](./sw-header-speed-composed-hover-verification-2026-10-16.md)을 참고합니다. 원본 GIF 속도 검증과 GIF 전달 전 결과는 각각 [GIF 검증 기록](./sw-header-window-motion-gif-verification-2026-10-16.md), [과거 검증 기록](./sw-header-window-motion-verification-2026-10-16.md)에 보존합니다.
## 11. 지원 · 미지원
| 지원 | 미지원(호스트 또는 후속 작업) |
|---|---|
| Figma 헤더 1개(26:8944) 표시·선택·Focus·Disabled | 색상·선스타일·문자스타일·치수·레이어 **상세 팝업**(Figma 전달 후), 색 선택 팝업(Figma "추후 디자인 진행예정") |
| 색상(26:10986 / 26:11036)·선스타일(26:11104)·문자스타일(26:11181)·치수스타일(26:11215) Dropdown, ByLayer/ByBlock, RGB·가중치·스케일 Input(2026-10-16) | 실제 색 적용·픽셀 색상 추출·선/문자/치수 스타일 적용(호스트 구현) |
| 반응형 More 대표 필드 전환, `SWHeaderInput`(Default / Hover / 편집 / 커서), Text Stroke(2026-10-16) | 선택형 상세 Input(Figma 미전달) |
| 문서 Dropdown(26:10662) 목록·선택·New/Open Callback·키보드 | 실제 파일 열기·새 문서 만들기(호스트 구현) |
| 레이어 Dropdown(26:10940) 목록·선택·전구/눈꽃/자물쇠 Callback·레이어 상세 Callback·키보드 | 실제 레이어 표시·동결·잠금과 레이어 상세 화면(호스트 구현) |
| 1800 / 1180 / 1080 / 900 / 640 / 520 반응형 + 더보기(옮긴 기능도 같은 Props·Callback) | SW 팝업(DraggablePanel) |
| SSR 문자열 렌더링(window 접근 없음) | 창 이동·최소화·종료의 실제 동작 |
| React 19, ESM, 타입 선언 | CommonJS 빌드, Light/Dark 테마, Runtime Theme JSON 연결 |
## 12. Storybook
```bash
npm run storybook
```
현재(2026-10-19) `SW / Header`에는 33개 Story가 있습니다. Story ID·분류·용도 전체 표는 [1차 정리 보고 §2](./sw-first-pass-cleanup-review-2026-10-19.md#2-storybook-전체-목록-및-분류)를 봅니다. 아래 문단은 Story를 추가한 시점별 설명입니다.
`SW / Header` 아래 Story: Overview, Default, Controls, Interactive States(인쇄·팝업 가이드·최소화·종료 Default/Hover 비교와 Callback 기록 포함), Responsive, Composed Controls, Logo Slot(기본 임시 로고 / 사용자 지정 로고 / 로고 미전달), Document Dropdown(Default / Hover / Open / 선택 / 여러 문서 / 문서 없음 / 긴 파일명 / 좁은 화면), Layer Dropdown(Layer Default / Hover / Open(Figma 8행) / Row Hover / 전구·눈꽃·자물쇠 4상태 3배 확대 / 선택·Toggle Callback 기록과 상태표 / 여러 상태·현재 레이어 / 긴 레이어명 / 20개 스크롤 / 레이어 없음 / 레이어 상세 / 상태 표시 전용 / 좁은 화면 1440–390). Responsive Story는 헤더 폭 18개(1920–390, 각 단계 경계 전후 포함) 프레임을 나열합니다. 모든 단계가 헤더 폭 기준이므로 Viewport를 바꾸지 않아도 확인할 수 있습니다.
`Window Move Motion`은 GIF 주요 3상태와 속도 변경 후 약 0 / 205 / 410 / 444 / 683 / 888 / 1366 / 1400ms 프레임, 실제 Hover·Drag·최소화·종료 Callback 및 반복(`motion cycle`) 기록, 축소 폭 예제를 제공합니다. 내부 고정 시간은 원본 GIF 시간 × 1400/1640의 정확한 값입니다. 고정 프레임은 Storybook 전용 Hover attribute와 Web Animations API로 시간을 멈춥니다. Reduced Motion에서는 기본 아이콘·정적 Tooltip 정책을 따르므로 정상 타임라인 비교는 해당 설정을 끄고 Story를 다시 로드합니다.
2026-10-16 추가 Story: `Color Dropdown`, `Line Style Dropdown`, `Text · Dimension Style Dropdown`(Default / Hover / Expanded / 행 Hover / 독립 버튼 Hover 무변화 / Callback 기록 / 좁은 화면), `Responsive More`(대표 필드 전환 Callback 기록 1440·768·390 + 16개 폭), `Text Stroke`(Figma 원본 이미지 비교 · 3배 확대 · 말줄임), `Layer Detail · Input`(Figma 참조 이미지 · 레이어 상세 Default/Hover 무변화/Disabled/Callback · Input Default/Hover/ReadOnly/Disabled/편집/확정·취소·외부 값·IME), `Dynamic Data`(항목 추가·삭제·이름·값·순서 변경). `Document Dropdown`의 첫 예제는 현재 열린 문서 1개이며, 기본 `figmaArgs`도 그 문서 1개를 전달합니다(`documents=[]`는 예외 상태 예제로만 남김). 검증 결과는 [Dropdown·More·Text Stroke·Input 검증 기록](./sw-header-dropdowns-stroke-input-verification-2026-10-16.md)에 있습니다.
`Composed Controls`에는 기존 레이어·색상·상세보기와 공개 선/문자/치수 컨트롤, 5개 필드의 고정 Hover 비교가 있습니다. 실제 공용 컴포넌트를 렌더링하며 60 × 15px 치수 Hover SVG는 흰 foreground만 노란색으로 바꾸고 검정 mask 외곽선을 유지합니다. 기존 Story만 보완했으며 새 Story는 추가하지 않았습니다. [현재 통합 검증](./sw-header-speed-composed-hover-verification-2026-10-16.md)을 참고합니다.
## 속성창·K 가이드 추가 연결 (2026-10-16)
기존 공개 Props를 유지한 추가 API입니다. `propertyPanel`을 전달하면 기존 속성창 버튼에 패널이 연결됩니다. 미전달 시 기존 토글/Callback 계약만 유지하며 임의의 CAD 데이터/편집 창을 만들지 않습니다.
```tsx
import { useState } from "react"
import { SWHeader, type SWHeaderProperty } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
function DrawingHeader() {
const [properties, setProperties] = useState<SWHeaderProperty[]>([
{ id: "layer", groupId: "common", name: "레이어", type: "text", value: "층_01" },
{ id: "weight", groupId: "common", name: "선가중치", type: "number", value: "0.25", min: 0, unit: "mm" },
])
return <SWHeader
documentTitle="도면"
propertyPanel={{
selection: { id: "selection-42", kind: "single", title: "선택된 객체 - 호스트 객체" },
groups: [{ id: "common", collapsedCount: 6 }],
properties,
onPropertyChange: (property, value) => {
// 실제 CAD 반영 및 계산은 호스트에서 수행한 뒤 결과를 Props로 돌려줍니다.
setProperties(items => items.map(item => item.id === property.id ? { ...item, value } : item))
},
onToolActiveChange: active => console.info("호스트 선택 도구", active),
}}
onPropertyPanelOpenChange={open => console.info("속성창 열림", open)}
/>
}
```
- Uncontrolled: `propertyPanelOpen`을 생략합니다. Controlled: `propertyPanelOpen={open}`, `onPropertyPanelOpenChange={setOpen}`을 함께 전달합니다. 기존 `onPropertyPanelToggle`은 버튼/More 클릭 시 그대로 호출됩니다. 이전 Controlled 호스트가 새 Open Callback 없이 기존 Toggle Callback만 사용한다면 X/Escape 닫기에서도 기존 Toggle로 알립니다.
- 선택 대상이 바뀔 때 `selection.id`도 반드시 변경합니다. 같은 property id라도 이전 객체의 임시 편집값은 재사용하지 않습니다.
- `selection.kind`: none/all/single. `selection.title`·`counts`·`overviewTitle`은 호스트 데이터입니다. counts의 선택적 row/column은 Figma 집계 배치를 표현할 수 있습니다. `onOverviewClick`도 선택 변경을 요청할 뿐 내부 CAD 동작은 없습니다.
- 항목 타입: text/number/select/color/slider/computed. `id/name/value/type/groupId`가 필수이고, `order`로 표시 순서를 지정합니다. 중복 id를 사용하지 마세요.
- `options`는 select의 id/name/disabled 목록입니다. 선택은 Enter 또는 Focus 이동으로 확정하고 Escape로 취소합니다. 삭제되거나 disabled가 된 option은 새 값으로 확정하지 않습니다. 옵션 데이터가 바뀌면 임시 선택을 현재 호스트 값으로 돌립니다.
- `color`는 기존 RGB/HEX parser를 재사용합니다. `swatch`로 별도 견본을 제공할 수 있습니다. `linePreview/linePattern`은 기존 선 미리보기를 재사용합니다. `annotation`은 (ByL) 같은 보조 정보, `unit`은 mm/% 같은 단위입니다.
- number/slider는 `min/max`, slider는 `step`을 지원합니다. slider의 기본 범위는 0~100이며 Story의 0~1 등은 호스트가 지정한 범위입니다. 수치를 계산하거나 Figma의 시각적 thumb 위치에 맞춰 임의 변환하지 않습니다.
- `validate(draft)`가 false면 거부합니다. 항목의 `onCommit(value, property)`와 패널의 `onPropertyChange(property, value)`는 서로 다른 통지 지점입니다. 둘 다 연결하면 각각 통지됩니다.
- 그룹은 `layout=rows/circle/position`, `order/name/collapsedCount/positionLabel`을 받습니다. circle은 원본 SVG 위 `graphicSlot=circumference/diameter/radius/area`의 항목을 배치합니다. 다른 항목은 그래픽 아래에 배치합니다. 계산값은 computed/readOnly로 전달합니다.
- `expandedGroupIds/onExpandedGroupsChange`로 펼침을 제어하거나, Uncontrolled일 때 `defaultExpandedGroupIds`로 시작합니다. 펼침은 property Callback을 실행하지 않습니다.
- `toolActive/onToolActiveChange/toolLabel`로 내부 아이콘을 연결합니다. 현재 의미는 호스트가 정하는 선택 도구 상태 통지이며 CAD 기능을 내장하지 않습니다.
- 단독 사용: `<SWTheme><SWHeaderPropertyPanel ... onClose={close} /></SWTheme>`. 단독 배치는 호스트의 `style`로 지정할 수 있습니다.
- 동적 패널 입력은 `SWHeaderInput commitOnUnmount={false}`를 사용합니다. 기존 Input의 기본 `true` 동작은 유지합니다.
- 속성 필드 상태(2026-10-19, Figma 26:12388 ~ 26:12649): 공통 행은 200 × 28 필드(선가중치처럼 단위 없는 숫자와 투명도 값은 41px 내외 Hug)입니다. Default는 값만, Hover는 `rgba(33,29,19,.4)` 테두리, 숫자 편집 On은 `#0F0D0A` + 흰 단위, Dropdown On은 `#211D13`입니다. 하이퍼링크(text)·재료(computed)는 Figma에 On이 없어 Default/Hover만 적용합니다. [검증 기록](./sw-property-fields-verification-2026-10-19.md).
- 필드 Dropdown: `property.picker`(`"layer" | "lineStyle" | "color" | false`)와 패널 `menus`(`SWHeaderPropertyMenus`: layers·toggle Callback, lineTypes·lineWeights·lineScale, colors·colorMode·onColorPick 등)를 전달합니다. `type: "color"`는 색상, `type: "select"` + `linePreview`는 선스타일 Dropdown이 기본이며 레이어는 `picker: "layer"`를 지정합니다. 목록 선택은 해당 속성의 `onPropertyChange`(값 = 행 id)로 확정되고, 레이어 Toggle·가중치·스케일·ByLayer/ByBlock·색상 추출·상세는 `menus` Callback만 호출합니다. `menus`가 없으면 `options`로 목록을 만듭니다. `mixed`는 `다양함`으로 표시하며 현재 행을 표시하지 않습니다.
- `decimals`를 지정한 숫자는 확정값을 고정 소수 자릿수로 정규화합니다(예: 선축적 `2` → `2.0000`). 색상 입력은 기존 0~255 검증을 유지합니다.
- K 가이드는 별도 데이터를 요구하지 않습니다. 기존 K 버튼 Hover/Focus로 원본 이미지가 Pan/Zoom 반복되고, 작은 화면의 More에서도 같은 미리보기가 보입니다. Reduced Motion·Escape·재진입 초기화가 적용됩니다. 클릭은 기존 `onPopupGuide`만 호출합니다.
- 색상 모드의 null은 직접 색상 의미를 보존하고, 필수 모드 표시는 마지막 유효 모드(처음 ByLayer)를 유지합니다. 선택된 모드를 재클릭해도 onColorModeChange가 중복 호출되지 않습니다.
- 신규 Story: Property Panel / Property Panel Variants / K Guide / Additional Hover. 기존 17개 Story를 유지합니다.
실행 결과와 제한은 [추가 검증 기록](./sw-header-additional-verification-2026-10-16.md)을 따릅니다.
## 후속 5개 요구사항 연결 (2026-10-16)
### 속성Bar와 공유 데이터
```tsx
import { SWHeader, SWHeaderPropertyBar, type SWHeaderProperty } from "@hm-design/sw"
import "@hm-design/sw/styles.css"
const fields: SWHeaderProperty[] = [
{ id: "length", groupId: "shape", name: "길이", type: "number", value: length, min: 0 },
]
const data = {
selection: { id: objectId, title: objectName, kind: "single" as const },
properties: fields,
onPropertyChange: (property: SWHeaderProperty, next: string) => updateObject(property.id, next),
}
<SWHeader documentTitle={fileName}
propertyBar={data} propertyBarOpen={barOpen} onPropertyBarOpenChange={setBarOpen}
propertyPanel={{ ...data, groups: [{ id: "shape", name: "형상" }] }}
propertyPanelOpen={panelOpen} onPropertyPanelOpenChange={setPanelOpen} />
```
- `propertyBar`는 `SWHeaderPropertyBarProps`: selection/properties/onPropertyChange, 전체 readOnly/disabled 및 div HTML 속성입니다. 개별 property의 readOnly/disabled/validate/min/max도 적용합니다. 컴포넌트 밖에서 단독 사용하면 `SWTheme`로 감쌉니다.
- `propertyBar` 생략 시 propertyPanel의 selection/properties/onPropertyChange만 공유합니다. 두 데이터를 모두 생략하면 토글 상태만 제공하며 임의 속성이나 CAD 수치를 만들지 않습니다.
- `propertyBarOpen` 생략 시 내부 상태, `defaultPropertyBarOpen`으로 초기값을 정합니다. Controlled는 `propertyBarOpen/onPropertyBarOpenChange`를 함께 사용합니다. 기존 `onPropertyBarToggle`도 클릭마다 보존됩니다. 속성창 열림과 독립적입니다.
- properties는 id로 식별하고 `order` 오름차순(같은 값은 배열 순서)으로 표시합니다. 객체 교체 시 selection.id를 바꿉니다. name/value/항목 추가·제거·외부 변경을 반영합니다.
- 값 편집은 `SWHeaderInput`을 사용합니다. Enter/Blur 확정, Escape 취소, 유효하지 않은 입력 복원, 외부 value 반영, 편집 중 삭제 draft 폐기를 지원합니다. 바의 select 타입은 option id를 입력하고 해당 options로 검사합니다. slider 타입은 숫자로 편집합니다. computed는 읽기 전용입니다. 속성창의 select/range UI를 바에 복제하지 않습니다.
- 헤더는 49px, 바는 별도 28px입니다. 폭은 최대 1222px이며 객체 수정 영역의 실제 위치부터 부모의 남은 폭을 사용합니다. x=377 고정값은 없습니다. 좁으면 focus 가능한 region 안에서 가로 스크롤하며 input focus로 해당 값에 접근할 수 있습니다. 호스트 콘텐츠는 열림 상태에 따라 필요한 28px 공간을 예약하세요. 기존 Dropdown은 바 위에 표시됩니다.
### 메뉴 컴포넌트 자체 구성
```tsx
<SWHeader documentTitle={fileName}
controls={[{ id: "lineStyle", label: "선 종류" }, { id: "layer" }]}
layers={layers} activeLayerId={layerId} onLayerSelect={selectLayer}
lineTypes={lineTypes} lineStyle={lineStyle} onLineTypeSelect={selectLineType} />
```
- `controls?: readonly SWHeaderControlConfig[]`는 전체 allowlist입니다. 미전달하면 승인된 기존 구성·계층·1920px 디자인을 유지합니다. `[]`는 선택적 컨트롤 전부 제거입니다. 홈/로고·창 제어는 유지합니다.
- id: document / drawingMode / layer / color / lineStyle / textStyle / dimensionStyle / details / propertyBar / propertyPanel / print / popupGuide. `details`는 상세보기 안내 caption이며 필수 액션이 아닙니다.
- 배열에서 빼거나 `visible:false`로 숨기며, 배열 순서가 실제 DOM·키보드·배치 순서입니다. 중복 id는 첫 번째 표시 항목만 사용합니다. 숨김 해제/재추가할 수 있습니다. `label`은 필드 라벨·버튼 이름·More 이름을 바꿉니다. 문서는 실제 documentTitle을 유지하고 접근성 이름 접두사를 바꿉니다. K는 아이콘을 유지하고 접근성 이름을 바꿉니다.
- 구성 모드는 각 컨트롤을 하나의 객체 수정 영역 안에서 재배치합니다. 기본 모드의 그룹 외곽 배치를 바꾸지 않습니다. 파일·색상 등을 제거한 공간은 고정 슬롯으로 남지 않습니다.
- 기존 1800/1180/1080/900/640/520px 기준은 유지합니다. 남은 필드 중 레이어(없으면 배열의 첫 필드)를 대표 슬롯으로 사용하고 More 선택으로 대체합니다. 대표 요소 삭제 시 남은 필드로 대체합니다. 표시 중인 대표는 More에서 제외되고, 구성에서 빠진 요소는 More에서도 사라집니다.
- 값은 기존 Props/Callback으로 계속 호스트가 소유합니다. 재정렬은 keyed 컴포넌트를 유지하고, 제거된 Dropdown과 draft는 정리하며 데이터 Callback을 생성하지 않습니다. 내부 열림 상태는 제거 후 재추가 시 닫힘입니다. Controlled 열림은 호스트가 전달한 값이 우선합니다. 일반적인 Dropdown 닫힘 시 기존 입력 확정 계약은 유지합니다.
- 구성요소 배열과 Dropdown 내부 옵션 배열은 별개입니다. layers/colors/userColors/lineTypes/lineWeights/textStyles/dimensionStyles의 동적 기능은 그대로 사용합니다.
### Colorize와 CSS 경로
- 색상 메뉴의 기존 `색상 추출` 위치에 Figma `26:11302`/`26:11305` 원본 SVG를 연결했습니다. 버튼 24×24, 아이콘 18×18, Hover는 주황 경계와 검정 25% overlay입니다. Hover는 Callback을 실행하지 않으며 클릭/Enter는 기존 `onColorPick`을 호출하고 메뉴를 닫습니다.
- `26:11338 color_돋보기` Overlay는 패키지에 구현되어 있지 않습니다. 호스트가 onColorPick에서 연결할 대상이며 임의 대체 색상창은 만들지 않았습니다.
- CSS 편집 원본은 `packages/sw/src/theme/sw.css`입니다. SW build가 dist/styles.css를 만들고 Story가 공개 `@hm-design/sw/styles.css`를 읽습니다. dist만 편집하면 다음 빌드에서 원본이 다시 포함됩니다. 원본 상세 span의 Stroke 선언과 미사용 solid-stroke-width 변수를 제거했으므로 산출물 override는 필요하지 않습니다. 외부 소비 앱에는 SW를 다시 빌드한 뒤 산출물을 갱신하세요.
- Text Stroke Story의 지정 원본 스크린샷·제목·전용 import를 제거했습니다. 이미지 파일과 나머지 비교 Story·text-shadow는 보존했습니다.
- 기존 21개 Story에 Property Bar / Dynamic Controls / Colorize를 추가했습니다. 현재 실행 결과는 [후속 검증 기록](./sw-header-followup-five-verification-2026-10-16.md)을 따릅니다.
## 구성요소 표시·메뉴 순서·속성창 4상태·형상 Slot (2026-10-16 3차)
이 절이 같은 주제의 이전 설명보다 우선합니다. 검증 결과는 [3차 검증 기록](./sw-header-composition-shape-verification-2026-10-16.md)에 별도로 기록했습니다.
### 로고 필수, 나머지 개별 제거 — `hiddenControls`
```tsx
<SWHeader documentTitle={fileName} logoSrc={logoUrl} logoAlt="EasyBeam"
hiddenControls={["document", "color", "textStyle", "print", "minimize"]}
/* 값·목록·Callback Props는 기존 그대로 */ />
```
- `hiddenControls?: readonly SWHeaderHideableControlId[]`. 대상 id: `home`, `document`, `drawingMode`, `layer`, `color`, `lineStyle`, `textStyle`, `dimensionStyle`, `details`, `propertyBar`, `propertyPanel`, `print`, `popupGuide`, `windowMove`, `minimize`, `close`. 로고는 대상이 아니며 항상 표시합니다(`logo` / `logoSrc` / `logoAlt` Props 유지).
- `home`을 제거하면 홈 동작만 사라지고 로고는 같은 32×17 슬롯에 정적 이미지로 남습니다(Hover·`onHomeClick` 없음).
- 전체 `controls` 목록 없이 쓸 수 있으며, 남은 요소는 기본 디자인의 크기·계층·Breakpoint를 유지합니다. `controls`와 함께 쓰면 두 조건을 모두 적용합니다(`controls`의 `visible:false`와 같은 의미).
- 제거한 요소는 렌더링하지 않습니다: 공간 없음, Dropdown·속성창·속성Bar·K 가이드 표시 안 함, Callback 없음. 열린 상태에서 제거하면 내부 열림 상태만 조용히 닫힙니다(데이터 Callback 없음).
- 배열에서 다시 빼면 재표시됩니다. 선택값은 호스트 Props이므로 그대로이고, 내부 Dropdown은 닫힌 상태로 돌아옵니다.
- 미전달 또는 `[]`이면 승인된 기본 Header와 동일합니다(1920~390px DOM 위치·크기·색 비교 차이 0).
- 객체 영역 요소를 제거하면 More 이동은 CSS 대신 스크립트가 같은 Breakpoint(1800/1180/1080/900/640)로 결정합니다. 표시할 항목이 없는 폭에서는 More 버튼을 만들지 않습니다. 창 영역은 남은 요소 폭만 차지합니다.
- 로고만 남겨도 Header 높이 49px와 녹색 객체 영역 배경은 유지합니다.
### 유사 메뉴 순서 — `menuOrder`
```tsx
<SWHeader documentTitle={fileName}
menuOrder={["color", "layer", "dimensionStyle", "lineStyle", "textStyle"]} />
```
- 대상: `layer`, `color`, `lineStyle`, `textStyle`, `dimensionStyle`(`SWHeaderMenuId`). 메뉴는 메뉴가 이미 차지한 자리 안에서만 서로 바뀌며 홈·파일·로고·그리기모드·상세보기·창 제어는 움직이지 않습니다.
- 미전달: 승인된 기본 순서 `레이어 → 색상 → 선스타일 → 문자스타일 → 치수스타일`(`controls`를 쓰면 그 배열 순서).
- 누락 규칙: 지정한 id가 먼저 지정 순서로 오고, 빠진 id는 그 뒤에 기본 순서로 붙습니다. 중복 id는 첫 번째만, 알 수 없는 id는 무시합니다. 숨긴 메뉴가 들어 있어도 오류 없이 건너뜁니다.
- 순서는 DOM·Tab 순서·More 목록 순서에 함께 반영됩니다. React key는 메뉴 id라 재정렬해도 값·Callback·Dropdown·Focus가 유지되며, 순서 변경만으로는 어떤 Callback도 호출하지 않습니다.
- 대표 슬롯: 레이어가 표시 중이면 레이어, 아니면 순서상 첫 메뉴가 기본 대표입니다. More에서 고른 메뉴가 대표 자리를 대신하며, 그 메뉴가 제거되면 기본 대표로 돌아갑니다.
### 속성창 4가지 상태
새 팝업 없이 기존 `SWHeaderPropertyPanel` 하나를 호스트 데이터로 바꿉니다.
| 상태 | Figma | selection.kind | 데이터 |
|---|---|---|---|
| 선택된 객체 없음 | 26:11886 · 314×256 | `none` | common 그룹만 |
| 전체 객체 선택 | 26:11823 · 314×361 | `all` + `counts` | common 그룹, 혼합값 `mixed` |
| 원 1개 선택 | 26:11932 · 314×658 | `single` + `overviewTitle` | common(collapsedCount) + shape(circle) + position |
| 모든 항목 펼침 | 26:12035 · 314×754 | `single` | 위와 같고 `expandedGroupIds={["common"]}` |
Storybook: `Property Panel · 선택 없음 / 전체 선택 / 원 1개 / 모두 펼침` 개별 Story와 `Property Panel · 4개 비교`(디자인 검증용, 실측 크기 표시, 좁으면 줄바꿈). 실제 Header는 현재 선택에 맞는 패널 하나만 표시합니다.
### 형상 영역 Slot — `shapeContent`
```tsx
<SWHeader documentTitle={fileName} propertyPanel={{
selection, properties, groups,
shapeContent: ({ selection, group, properties }) => <MyShapeViewer objectId={selection.id} />,
}} />
```
- `layout: "circle"` 그룹의 208×208 영역(그룹 260px 안 39, 26)에 렌더링합니다. `SWHeaderPropertyPanel`과 `SWHeader.propertyPanel` 모두 같은 Prop입니다.
- `undefined`(미지정): Figma 원형 그림(기존 기본). `null` / `false`: 빈 영역 — 크기·치수 라벨 위치·패널 높이 유지. ReactNode: 외부 컴포넌트. 함수: `{ selection, group, properties }`(해당 그룹 항목)를 받아 렌더링 — 객체 유형별 교체에 사용합니다.
- 내용은 영역 안에 맞춰지고(overflow hidden) 원주·지름·반지름·면적 입력은 그 위에 그대로 표시됩니다. 값은 `properties`로만 전달되며 형상 Slot과 독립입니다. 입력은 기존 SWHeaderInput 검증·Enter/Blur 확정·Escape 취소를 사용합니다.
- 패키지는 CAD 계산·형상 렌더링 엔진을 포함하지 않습니다. Storybook의 사각형/점선 원 SVG는 Story 전용 샘플입니다.