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