Resizable | CoUI

Resizable

드래그로 패널 크기를 조절할 수 있는 분할 패널 컴포넌트

Resizable#

드래그 핸들을 사용하여 패널 간 크기를 조절할 수 있는 분할 레이아웃 컴포넌트입니다.

Live Preview#

Web
Panel A
Panel B
Flutter
Loading Flutter...
class ResizableDefaultExample extends StatelessComponent {
  const ResizableDefaultExample({super.key});

  @override
  Component build(BuildContext context) {
    final cs = context.colorScheme;
    return div(
      [
        CoResizable(
          panes: [
            CoreResizablePaneData<Component>(
              content: div(
                [Component.text('Panel A')],
                classes: 'flex items-center justify-center bg-${cs.surfaceContainer} text-${cs.onSurface} h-full',
              ),
              initialSize: 0.4,
            ),
            CoreResizablePaneData<Component>(
              content: div(
                [Component.text('Panel B')],
                classes: 'flex items-center justify-center bg-${cs.surfaceContainerHigh} text-${cs.onSurface} h-full',
              ),
              initialSize: 0.6,
            ),
          ],
        ),
      ],
      styles: Styles(raw: {'width': '100%', 'height': '200px'}),
    );
  }
}
class ResizableDefaultExample extends StatelessWidget {
  const ResizableDefaultExample({super.key});

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final onSurface = scheme.onSurface!;
    return SizedBox(
      height: 200,
      child: CoResizable(
        panes: [
          CoreResizablePaneData(
            content: Container(
              color: scheme.surfaceContainer!,
              alignment: Alignment.center,
              child: Text('Panel A', style: TextStyle(color: onSurface)),
            ),
            initialSize: 0.4,
          ),
          CoreResizablePaneData(
            content: Container(
              color: scheme.surfaceContainerHigh!,
              alignment: Alignment.center,
              child: Text('Panel B', style: TextStyle(color: onSurface)),
            ),
            initialSize: 0.6,
          ),
        ],
      ),
    );
  }
}

수직 분할#

Web
Top
Bottom
Flutter
Loading Flutter...
class ResizableVerticalExample extends StatelessComponent {
  const ResizableVerticalExample({super.key});

  @override
  Component build(BuildContext context) {
    final cs = context.colorScheme;
    return div(
      [
        CoResizable(
          direction: CoreResizableDirection.vertical,
          panes: [
            CoreResizablePaneData<Component>(
              content: div(
                [Component.text('Top')],
                classes: 'flex items-center justify-center bg-${cs.surfaceContainer} text-${cs.onSurface} h-full',
              ),
              initialSize: 0.5,
            ),
            CoreResizablePaneData<Component>(
              content: div(
                [Component.text('Bottom')],
                classes: 'flex items-center justify-center bg-${cs.surfaceContainerHigh} text-${cs.onSurface} h-full',
              ),
              initialSize: 0.5,
            ),
          ],
        ),
      ],
      styles: Styles(raw: {'width': '100%', 'height': '260px'}),
    );
  }
}
class ResizableVerticalExample extends StatelessWidget {
  const ResizableVerticalExample({super.key});

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final onSurface = scheme.onSurface!;
    return SizedBox(
      height: 260,
      child: CoResizable(
        direction: CoreResizableDirection.vertical,
        panes: [
          CoreResizablePaneData(
            content: Container(
              color: scheme.surfaceContainer!,
              alignment: Alignment.center,
              child: Text('Top', style: TextStyle(color: onSurface)),
            ),
            initialSize: 0.5,
          ),
          CoreResizablePaneData(
            content: Container(
              color: scheme.surfaceContainerHigh!,
              alignment: Alignment.center,
              child: Text('Bottom', style: TextStyle(color: onSurface)),
            ),
            initialSize: 0.5,
          ),
        ],
      ),
    );
  }
}

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • IDE, 코드 에디터, 대시보드처럼 사용자가 패널 크기를 직접 조정해야 할 때
  • 좌우 또는 상하로 분할된 레이아웃에서 각 패널의 비율을 사용자가 선택하게 할 때
  • 세 개 이상의 패널을 유연하게 분할 배치할 때

대신 다른 컴포넌트를 사용하세요:

  • Separator: 크기 조절 없이 시각적 구분선만 필요할 때
  • Row / Column: 고정된 비율의 레이아웃에는 일반 레이아웃 위젯 사용

기본 사용법 (Basic Usage)#

// 수평 분할 패널
CoResizable(
  panes: [
    CoreResizablePaneData(
      content: SidebarPanel(),
      initialSize: 0.3,
    ),
    CoreResizablePaneData(
      content: MainContentPanel(),
      initialSize: 0.7,
    ),
  ],
)

// 수직 분할 (min/max 제한 포함)
CoResizable(
  direction: CoreResizableDirection.vertical,
  onResize: (sizes) => saveLayout(sizes),
  panes: [
    CoreResizablePaneData(
      content: EditorPanel(),
      initialSize: 0.6,
      minSize: 0.2,
    ),
    CoreResizablePaneData(
      content: TerminalPanel(),
      initialSize: 0.4,
      minSize: 0.1,
      maxSize: 0.6,
    ),
  ],
)
// 수평 분할 패널
CoResizable(
  panes: [
    CoreResizablePaneData<Component>(
      content: div(
        [Component.text('사이드바')],
        classes: 'flex items-center justify-center h-full',
      ),
      initialSize: 0.3,
    ),
    CoreResizablePaneData<Component>(
      content: div(
        [Component.text('메인 콘텐츠')],
        classes: 'flex items-center justify-center h-full',
      ),
      initialSize: 0.7,
    ),
  ],
)

// 수직 분할
CoResizable(
  direction: CoreResizableDirection.vertical,
  panes: [
    CoreResizablePaneData<Component>(
      content: div([Component.text('에디터')]),
      initialSize: 0.6,
    ),
    CoreResizablePaneData<Component>(
      content: div([Component.text('터미널')]),
      initialSize: 0.4,
    ),
  ],
)

Props / Parameters#

속성타입기본값설명
panes List<CoreResizablePaneData<W>> 필수 분할할 패널 목록
direction CoreResizableDirection horizontal 분할 방향 (horizontal, vertical)
onResize void Function(List<double>)? null 크기 변경 콜백 (ratio 배열)
dividerThickness double? CoreBorderWidth.thick 디바이더 두께 (px)

CoreResizablePaneData<W>#

속성타입기본값설명
content W (Widget / Component) 필수 패널 내부 콘텐츠
initialSize double 0.5 초기 크기 비율 (0.0~1.0)
minSize double? null 최소 크기 비율
maxSize double? null 최대 크기 비율

동작 스펙 (Behavior)#

인터랙션#

  • 드래그: 디바이더를 드래그하여 인접 패널 크기 실시간 조정
  • 커서 변경: 수평은 col-resize, 수직은 row-resize
  • min/max 제한: 드래그가 제한 범위를 벗어나면 무시됨

상태 전환#

  • idledragging: 디바이더 드래그 시작
  • draggingidle: 드래그 종료 (마우스 업)
  • 드래그 중 문서 전역에서 마우스 이동을 추적 (요소 밖으로 벗어나도 유지)

애니메이션#

  • 드래그 중 즉시 반응 (애니메이션 없음)
  • 디바이더 색상은 hover 시 CoreDuration.short (150ms) 전환

사용 가이드라인 (Usage Guidelines)#

✅ Do#

minSize로 패널이 너무 좁아지지 않도록 제한

CoResizable(
  panes: [
    CoreResizablePaneData(
      content: SidebarPanel(),
      initialSize: 0.3,
      minSize: 0.2,
    ),
    CoreResizablePaneData(
      content: ContentPanel(),
      initialSize: 0.7,
    ),
  ],
)

최소 크기 제한이 없으면 사용자가 패널을 완전히 닫아 콘텐츠에 접근하지 못할 수 있습니다.

❌ Don't#

너무 많은 패널을 한 번에 분할하지 않기

패널이 4개를 초과하면 각 패널이 너무 좁아져 콘텐츠를 표시하기 어렵습니다.

✅ Do#

onResize 콜백으로 사용자 설정 저장

CoResizable(
  onResize: (sizes) => preferences.saveSizes(sizes),
  panes: panes,
)

사용자가 설정한 패널 크기를 저장하면 다음 방문 시 동일한 레이아웃이 유지됩니다.

접근성 (Accessibility)#

포커스 & 커서#

  • 디바이더 hover 시 리사이즈 커서 표시
  • 터치 타겟을 위해 디바이더 hit-test 영역은 CoreSpace.space8 (8px)

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명CoResizableCoResizable
패널 타입 CoreResizablePaneData<Widget> CoreResizablePaneData<Component>
드래그 감지 GestureDetector mousedown + document listeners
레이아웃 LayoutBuilder + Row/Column + Expanded CSS flex + ratio 기반 flex-grow
  • Separator: 크기 조절 없이 구분선만 표시할 때 사용
  • Divider: 콘텐츠 영역 내 단순 구분선에 사용