StageContainer#
부모의 가로 폭을 실시간으로 측정해 설정된 breakpoint rung(기본: Bootstrap
[576, 768, 992, 1200, 1400]) 으로 클램프한 뒤, 남는 여유 폭을 좌우 padding
으로 자동 분배해서 콘텐츠를 중앙 고정폭으로 유지하는 반응형 컨테이너입니다.
Flutter는 LayoutBuilder, Web은 ResizeObserver 를 사용하지만
builder에
전달되는 resolved padding 계산식은 완전히 동일합니다.
Live Preview#
class StageContainerDefaultExample extends StatelessComponent {
const StageContainerDefaultExample({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,
);
},
);
}
}
class StageContainerDefaultExample extends StatelessWidget {
const StageContainerDefaultExample({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(),
),
),
),
);
},
);
}
}
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),
),
);
}
}
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),
),
);
}
}
사용 시기 (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 이고, 반환 타입만 Widget ↔ Component
로 갈립니다:
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,
);
},
)
padding은 breakpoint(기본 Bootstrap rung)에 맞춰 이미 계산된 최종 값입니다. 여기에 또 다른 폭 판정을 얹으면 두 로직의 기준 rung이 어긋나 콘텐츠 폭이 이중으로 좁아지거나 넓어집니다.
접근성 (Accessibility)#
StageContainer 는 breakpoint 계산 유틸리티입니다. 자기 표면을 그리지 않고
계산된 padding 을 builder 에 넘기기만 하므로, 접근성 트리에 기여하는 시맨틱이
없습니다. 리더가 읽는 것은 전부 builder 가 반환한 내용입니다.
역할 / 시맨틱#
-
Flutter: 자체 엘리먼트를 만들지 않습니다.
build는builder(context, padding)의 결과만 내보내며 시맨틱 노드를 추가하지 않습니다. -
Web: 중첩된
<div>두 개를 그립니다(바깥은 측정용, 안쪽은 resolved padding 적용용). 둘 다 레이아웃 chrome 만 가지고role·aria-*·tabindex는 붙이지 않습니다. 호출자가 준attributes는 바깥<div>로 통과합니다.
즉 이 컴포넌트는 랜드마크가 아닙니다. role="main" · region · 제목 · 레이블을
내보내지 않으므로 리더의 랜드마크 목록에 나타나지 않습니다. 본문 영역을 랜드마크로
알리려면 builder 안에서 호출자가 직접 그 역할을 붙여야 합니다.
키보드#
처리하는 키가 없습니다. 양쪽 모두 자체 키 핸들러가 없고, Web 이 상속하는
onKeyDown / onKeyUp 은 호출자 통과용입니다.
포커스#
포커스를 받지 않습니다. FocusNode 도 tabindex 도 없고 트랩 · 복원도 하지
않습니다.
스크린 리더#
읽히는 것이 없습니다. Flutter 는 노드를 아예 만들지 않고, Web 이 추가하는 <div>
두 개는 이름이 없어 리더가 그냥 지나갑니다.
알려진 제약#
-
플랫폼 구조가 비대칭입니다. Flutter 는 엘리먼트를 0개, Web 은 래퍼
<div>를 2개 기여합니다. DOM 깊이에 의존하는 CSS 셀렉터나 테스트 selector 를 쓴다면 이 차이를 감안해야 합니다. -
Web 은 첫 페인트에 padding 이 아직 없습니다. 폭을
ResizeObserver로 측정하기 때문에 첫 측정이 도착하기 전 한 프레임 동안padding이 fallback 값으로 전달됩니다.builder가 이 값으로 분기한다면 첫 프레임의 레이아웃 점프를 고려하세요.
이 컴포넌트가 직접 다루지 않는 축은 전역 접근성 축 에 있습니다.
관련 컴포넌트#
- ResponsiveContainer — breakpoint별 max-width
- ResponsiveGrid — breakpoint별 column 수
- Resizable — 사용자 드래그 분할