8.3 KiB
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 1pxrgb(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 밖은 잡히지 않음만 확인).