Files

8.3 KiB
Raw Permalink Blame History

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