111 lines
8.3 KiB
Markdown
111 lines
8.3 KiB
Markdown
# 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 밖은 잡히지 않음만 확인).
|