Files
HMDesign/docs/components/resizable.md

111 lines
8.3 KiB
Markdown
Raw Permalink 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.
# Resizable
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Resizable 고유의 API·Token·상태·결정만 기록한다.
## 1. 상태와 성격
| 항목 | 내용 |
|---|---|
| 성격 | **Interactive layout primitive** (panel 크기 조절). Dashboard/Sidebar/persistence를 소유하지 않는다 |
| 구현 상태 | 구현 완료 (현재 HM 규격) |
| 기준일 | 2026-10-03 |
| 기반 | shadcn/ui Resizable = **react-resizable-panels v4** (`^4.14.2`) |
| 대표 Story | `Components/Disclosure/Resizable` → `All` (이름 "Resizable") |
| 보류 | 아래 §12 |
## 2. 적용 근거
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Resizable이 없다. 참고할 수치가 없었다.
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Separator Token은 참조하지 않는다.
- **구현 방식**: 라이브러리가 drag, 최소/최대, collapsing, layout 계산, 키보드 resize, pointer hit-target 확장, separator ARIA를 소유한다. HM은 divider와 선택형 grip만 스타일한다.
- **v4 주의**: 라이브러리 이름과 API가 v3와 다르다. v3의 `PanelGroup`/`PanelResizeHandle`가 아니라 **`Group`/`Panel`/`Separator`**이고, 크기는 **숫자 = 픽셀, 단위 없는 문자열(`"30"`) 또는 `"30%"` = 퍼센트**다. shadcn 이름(`ResizablePanelGroup`/`ResizablePanel`/`ResizableHandle`)은 유지했다.
- **보류**: §12.
## 3. 기본 규격 (현재 HM 규격)
| 항목 | 값 |
|---|---|
| Group | `flex h-full w-full`. `orientation` `"horizontal"`(기본) / `"vertical"`. Group·Panel의 inline sizing은 라이브러리 것이라 덮을 수 없다 |
| Divider(Separator) | 1px, `--hm-sem-stroke-input-default`. 나란한 panel 사이는 세로선(`aria-orientation="vertical"`), 쌓인 panel 사이는 가로선 |
| Pointer target | 라이브러리가 1px divider 둘레로 확장한다(DOM 크기가 아니라 hit test). 8px 떨어진 곳은 잡히지 않았다 |
| Grip (`withHandle`) | 12 × 16px, 흰 면, 1px pale border, radius 2px, `GripVerticalIcon` 10px `--hm-sem-fg-secondary`, divider 가운데. 쌓인 panel 사이에서는 90° 회전 |
| 크기 | 소비자(Group의 `className`/부모)가 정한다. 컴포넌트가 높이/폭을 강제하지 않는다 |
## 4. 상태 규격
| 상태 | 값 |
|---|---|
| Hover (`data-separator="hover"`) | divider와 1px halo가 `--hm-sem-stroke-default` |
| Dragging (`data-separator="active"`) | divider와 1px halo가 `--hm-sem-action-primary` |
| Focus (`:focus-visible`) | divider와 1px halo가 `--hm-sem-stroke-input-focus` |
| Disabled | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함) |
| Collapsed panel | 라이브러리의 collapse(`collapsible`, `collapsedSize`). `aria-valuenow`가 0 |
## 5. API
라이브러리 props 그대로. export: `ResizablePanelGroup`(= `Group`), `ResizablePanel`(= `Panel`), `ResizableHandle`(= `Separator`).
- `ResizablePanelGroup`: `orientation`, `defaultLayout`, `onLayoutChange`/`onLayoutChanged`, `groupRef`, `disabled`, `resizeTargetMinimumSize`, `id`. 저장(`useDefaultLayout`) 같은 persistence는 라이브러리에 있지만 HM이 래핑하거나 쓰지 않았다.
- `ResizablePanel`: `defaultSize`, `minSize`, `maxSize`, `collapsible`, `collapsedSize`, `panelRef`, `onResize`, `id`.
- `ResizableHandle`: `withHandle`(shadcn, grip 표시), `disabled`, `disableDoubleClick`, 그리고 div props(`aria-label` 등). `data-slot="resizable-handle"`, grip은 `resizable-handle-grip`.
- 접근성(라이브러리): Separator는 `role="separator"`, `aria-orientation`, `aria-valuenow|min|max`(%), `aria-controls`(앞 panel id), `tabindex=0`. 키보드: 가로 배치는 ←/→(5% 단계), 세로 배치는 ↑/↓, Home/End는 최소/최대, collapsible이면 Enter가 접힘을 토글. 의미 있는 이름은 소비자가 `aria-label`로 준다.
- Group에는 `role`이 붙지 않는다(라이브러리 구조). 이름은 `aria-label`을 소비자가 준다.
## 6. Dependency
- `react-resizable-panels` ^4.14.2 (`packages/ui`에 추가, shadcn Resizable의 기반). 사용: Resizable. CENTER 패키지에는 추가하지 않았다.
## 7. Token
Component Mapping 위치: `packages/ui/src/theme/resizable.css` (`--hm-resizable-*`).
`divider-width|color`, `hover-color`, `active-color`, `focus-color`, `halo-width`, `grip-width|height|radius|background|border-color|icon-size|icon-color`, `disabled-opacity`. Semantic과 Primitive(`--hm-warm-neutral-pale`, `--hm-warm-neutral-mid`)만 참조하며 `--hm-separator-*`는 참조하지 않는다. 신규 Primitive와 Semantic 없음. 규칙은 `@layer components`에 있어 소비자 utility가 이긴다.
## 8. 라이브러리에서 유지한 항목
drag(pointer)와 최소/최대 clamp, collapsible, keyboard resize, double click 동작, layout callback, ARIA, 커서 처리, panel inline sizing.
## 9. 소비자 확장성
- Group 크기: `className`/`style`(Story는 frame의 `height`/`max-width`)로 정한다.
- Divider/Grip 색과 크기: `--hm-resizable-*` token을 덮거나 `ResizableHandle className`의 utility를 쓴다.
- Panel 내용과 최소 크기: 소비자가 `minSize`를 정한다(**`minSize` 없이 끌면 panel이 0까지 줄어들 수 있다.** Story는 모든 panel에 `minSize`를 둔다).
## 10. Storybook
- 대표 Story `All`("Resizable"): Horizontal(list min 15%), Vertical with grip, Horizontal with grip, Min/max(25%–60%), Collapsible panel을 한 페이지에 둔다. 프레임과 panel 내용은 Story 배치이며 Dashboard가 아니다.
- verification: `Horizontal`, `Vertical`, `Min Max`.
## 11. 사용자 확정 결정
- 2026-10-03: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). Dashboard state, 저장, preset은 이후 Layout Pattern에서 조합한다(작업 지시).
## 12. 미확정 / 보류
- 수치(1px divider, 12×16 grip, hover/active 색)는 현재 HM 규격이며 디자인 확정 시 재검토.
- **모바일**: 390px에서 가로 분할은 사용 가능하다(panel 107/249px). 하지만 좁은 화면의 가로 분할은 정보 밀도에 부적합할 수 있어, 자동으로 세로로 바꾸는 responsive는 만들지 않았다. 모바일 layout 대체는 Dashboard/Layout 단계에서 검토한다.
- 중첩 Group, 3개 이상 panel, Grid(`Grid`/`Cell`/`Gridline`, 2D)는 검증하지 않았다.
- 레이아웃 저장(`useDefaultLayout`, localStorage)과 사용자 preference는 만들지 않았다.
- 터치 기기에서의 pointer target(`resizeTargetMinimumSize.coarse`)은 라이브러리 기본이다.
## 13. 관련 파일
```
packages/ui/src/components/ui/resizable.tsx
packages/ui/src/theme/resizable.css
packages/ui/package.json (dependency: react-resizable-panels)
packages/ui/src/theme.css (import)
packages/ui/src/index.ts (export)
src/components/resizable.stories.tsx
```
## 14. 검증 상태
- `npm run typecheck`, `npm run build`: 통과(2026-10-03).
- 실제 DOM(Browser pane): Separator `role=separator`, `aria-orientation`(가로 배치는 vertical, 세로 배치는 horizontal), `aria-valuenow`가 30 → 50.6(drag 후), `aria-valuemin`/`max`가 panel의 min/max를 반영(예: list min 15, 다른 panel min 20이면 max 80), `aria-controls`, `tabindex=0`, divider 1px `rgb(222,219,210)`.
- pointer drag: divider를 오른쪽으로 끌어 30% → 50.6%(panel 282/275), 왼쪽으로 끝까지 끌어 min 15%(84px)에서 멈춤, 오른쪽 끝까지 끌면 다른 panel에 `minSize`가 없어 0까지 줄어듦(Story에 `minSize`를 추가하는 근거). 세로 분할은 아래로 drag해 40% → 67%.
- keyboard: 가로에서 ArrowLeft가 5%씩 줄고, Home이 최소(15), End가 최대, 세로에서는 ArrowUp이 줄고 ArrowRight는 무시, collapsible에서 Home이 0으로 접고 Enter가 이전 크기(30%)로 복원. focus 시 divider와 halo가 `rgb(18,51,40)`.
- 레이아웃: Group `flex`, panel 크기는 라이브러리 inline style, grip은 12×16px이고 쌓인 panel 사이에서 90° 회전.
- Responsive(390×844): 모든 Group이 356px 안에 들어가고 페이지 가로 overflow 없음, 가로 분할 panel 107/249px.
- 미확인: 터치 drag, 중첩/3개 이상 panel, hover 상태(`data-separator="hover"`)의 pointer 시각, 포인터 hit target의 정확한 범위(8px 밖은 잡히지 않음만 확인).