StageContainer | CoUI
LogoCoUI

StageContainer

뷰포트 너비를 breakpoint에 맞춰 클램프하고 남는 폭을 좌우 padding으로 재분배하는 반응형 컨테이너

StageContainer#

부모의 가로 폭을 실시간으로 측정해 설정된 breakpoint rung(기본: Bootstrap [576, 768, 992, 1200, 1400]) 으로 클램프한 뒤, 남는 여유 폭을 좌우 padding 으로 자동 분배해서 콘텐츠를 중앙 고정폭으로 유지하는 반응형 컨테이너입니다. Flutter는 LayoutBuilder, Web은 ResizeObserver 를 사용하지만 builder에 전달되는 resolved padding 계산식은 완전히 동일합니다.

Live Preview#

사용 시기 (When to Use)#

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

  • 큰 화면에서도 본문 영역이 고정 폭 + 중앙 정렬이어야 할 때 (랜딩 페이지, Docs, 블로그 아티클 등)
  • 단계적 breakpoint 리듬이 필요한 반응형 그리드 루트
  • Flutter/Web에 동일한 반응형 규칙을 적용하고 싶을 때

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

  • ResponsiveContainer: breakpoint별 max-width만 바꾸고 직접 padding 재분배는 하지 않을 때
  • ResponsiveGrid: breakpoint별 column 수 를 바꾸고 싶을 때

기본 사용법 (Basic Usage)#

StageContainer(
  builder: (context, padding) {
    return Padding(
      padding: padding.toValue(),
      child: Text('Content centered on breakpoint rungs'),
    );
  },
)

Web도 동일한 생성자로 사용합니다. builder 가 받는 padding 은 양 플랫폼 모두 CoreEdgeInsets 이고, 반환 타입만 WidgetComponent 로 갈립니다:

StageContainer(
  builder: (context, padding) {
    return div(
      [Text('Web content').bodyMedium.onSurface],
      styles: Styles(raw: {'padding': padding.toValue()}),
    );
  },
)

빠른 오버라이드 (Chain)#

이미 만든 StageContainer 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class StageContainerChainExample extends StatelessWidget {
  const StageContainerChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return StageContainer(
      builder: (context, padding) {
        return Padding(
          padding: padding.toValue(),
          child: Container(
            height: CoreSpace.space80,
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: theme.colorScheme.surfaceContainer.toValue(),
              borderRadius: BorderRadius.circular(CoreRadius.radius16),
              border: Border.all(color: theme.colorScheme.outline.toValue()),
            ),
            child: Text(
              'left: ${padding.left.toStringAsFixed(0)}, '
              'right: ${padding.right.toStringAsFixed(0)}',
              style: theme.typography.bodyMedium
                  .toValue(theme: theme)
                  .copyWith(
                    color: theme.colorScheme.onSurface.toValue(),
                  ),
            ),
          ),
        );
      },
    ).withStyle(
      const CoreStageContainerStyle(
        breakpoint: CoreConstantStageBreakpoint(CoreSpace.space256),
        padding: CoreEdgeInsets.symmetric(horizontal: CoreSpace.space32),
      ),
    );
  }
}
class StageContainerChainExample extends StatelessComponent {
  const StageContainerChainExample({super.key});

  @override
  Component build(BuildContext context) {
    final cs = context.theme.colorScheme;
    final boxClasses =
        'h-${CoreSpace.scale.space80} '
        'flex items-center justify-center '
        'bg-${cs.surfaceContainer} border border-${cs.outline} '
        'rounded-${CoreRadius.scale.radius16} '
        'text-${CoreTextStyles.bodyMedium.name} text-${cs.onSurface}';
    return StageContainer(
      builder: (context, padding) {
        return div(
          [
            Text(
              'left: ${padding.left.toStringAsFixed(0)}, '
              'right: ${padding.right.toStringAsFixed(0)}',
            ),
          ],
          classes: boxClasses,
        );
      },
    ).withStyle(
      const CoreStageContainerStyle(
        breakpoint: CoreConstantStageBreakpoint(CoreSpace.space256),
        padding: CoreEdgeInsets.symmetric(horizontal: CoreSpace.space32),
      ),
    );
  }
}

Props / Parameters#

이름타입기본값설명
builder CoreStageContainerBuilder<W>W Function(Object context, CoreEdgeInsets padding) required resolved padding이 전달되는 빌더 콜백. W 는 Flutter Widget / Web Component
breakpoint CoreStageBreakpoint? theme → CoreStageBreakpoint.defaultBreakpoints CoreConstantStageBreakpoint / CoreStagedStageBreakpoint 중 택1
padding CoreEdgeInsets? theme → CoreStageContainerStyle.defaultPadding ( symmetric(horizontal: CoreSpace.space72) ) 기본 패딩. 브레이크포인트 재분배는 이 값에 더해짐
stageContainerStyle CoreStageContainerStyle? null breakpoint / padding 을 담는 style 슬롯. 위젯 파라미터가 우선

Web 은 위 파라미터에 더해 id / classes / css / attributes 와 DOM 이벤트 슬롯을 받아 바깥 측정용 <div> 로 통과시킵니다.

CoreStageContainerStyle 필드#

필드타입설명
breakpoint CoreStageBreakpoint? Breakpoint strategy override.
padding CoreEdgeInsets? Base padding. Left / right are the minimum horizontal insets — the breakpoint redistribution adds to them.

Theming#

CoreComponentTheme.stageContainer 에서 기본값을 오버라이드할 수 있습니다. padding은 CoreEdgeInsets 로 저장되어 Flutter/Web 양쪽에서 동일 값 비교됩니다:

CoreComponentTheme(
  stageContainer: CoreStageContainerTheme(
    style: CoreStageContainerStyle(
      breakpoint: CoreConstantStageBreakpoint(CoreSpace.space200),
      padding: CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16),
    ),
  ),
)

동작 노트#

  • viewport < minSize: 좌우 padding을 0으로 만들어 컨텐츠가 전체 폭 사용
  • viewport > maxSize: 남는 폭을 좌우에 균등 분할해 콘텐츠를 중앙 고정폭으로
  • 중간 rung: 현재 rung의 minWidth에 클램프하고 남는 폭을 좌우 padding에 추가

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

✅ Do#

builder가 전달하는 padding을 그대로 사용

StageContainer(
  builder: (context, padding) {
    return Padding(
      padding: padding.toValue(),
      child: PageContent(),
    );
  },
)

padding은 이미 breakpoint 클램프와 여유 폭 재분배가 끝난 최종 값입니다 — Flutter와 Web 모두 같은 계산식을 거치므로 builder 안에서 반응형 로직을 다시 계산할 필요가 없습니다.


❌ Don't#

builder 안에서 별도의 breakpoint 판단을 추가하지 않기

// ❌ padding 위에 또 다른 breakpoint 로직을 덧붙임
StageContainer(
  builder: (context, padding) {
    final extra = MediaQuery.sizeOf(context).width > 1200 ? 32.0 : 0.0;
    return Padding(
      padding: padding.toValue() + EdgeInsets.symmetric(horizontal: extra),
      child: content,
    );
  },
)

paddingbreakpoint(기본 Bootstrap rung)에 맞춰 이미 계산된 최종 값입니다. 여기에 또 다른 폭 판정을 얹으면 두 로직의 기준 rung이 어긋나 콘텐츠 폭이 이중으로 좁아지거나 넓어집니다.

접근성 (Accessibility)#

StageContainer 는 breakpoint 계산 유틸리티입니다. 자기 표면을 그리지 않고 계산된 padding 을 builder 에 넘기기만 하므로, 접근성 트리에 기여하는 시맨틱이 없습니다. 리더가 읽는 것은 전부 builder 가 반환한 내용입니다.

역할 / 시맨틱#

  • Flutter: 자체 엘리먼트를 만들지 않습니다. buildbuilder(context, padding) 의 결과만 내보내며 시맨틱 노드를 추가하지 않습니다.
  • Web: 중첩된 <div> 두 개를 그립니다(바깥은 측정용, 안쪽은 resolved padding 적용용). 둘 다 레이아웃 chrome 만 가지고 role · aria-* · tabindex 는 붙이지 않습니다. 호출자가 준 attributes 는 바깥 <div> 로 통과합니다.

즉 이 컴포넌트는 랜드마크가 아닙니다. role="main" · region · 제목 · 레이블을 내보내지 않으므로 리더의 랜드마크 목록에 나타나지 않습니다. 본문 영역을 랜드마크로 알리려면 builder 안에서 호출자가 직접 그 역할을 붙여야 합니다.

키보드#

처리하는 키가 없습니다. 양쪽 모두 자체 키 핸들러가 없고, Web 이 상속하는 onKeyDown / onKeyUp 은 호출자 통과용입니다.

포커스#

포커스를 받지 않습니다. FocusNodetabindex 도 없고 트랩 · 복원도 하지 않습니다.

스크린 리더#

읽히는 것이 없습니다. Flutter 는 노드를 아예 만들지 않고, Web 이 추가하는 <div> 두 개는 이름이 없어 리더가 그냥 지나갑니다.

알려진 제약#

  • 플랫폼 구조가 비대칭입니다. Flutter 는 엘리먼트를 0개, Web 은 래퍼 <div> 를 2개 기여합니다. DOM 깊이에 의존하는 CSS 셀렉터나 테스트 selector 를 쓴다면 이 차이를 감안해야 합니다.
  • Web 은 첫 페인트에 padding 이 아직 없습니다. 폭을 ResizeObserver 로 측정하기 때문에 첫 측정이 도착하기 전 한 프레임 동안 padding 이 fallback 값으로 전달됩니다. builder 가 이 값으로 분기한다면 첫 프레임의 레이아웃 점프를 고려하세요.

이 컴포넌트가 직접 다루지 않는 축은 전역 접근성 축 에 있습니다.

관련 컴포넌트#