Gap#
Row/Column (또는 Web의 flex) 레이아웃에서 자식 간격을 추가하는 유틸리티 컴포넌트입니다. 부모의 주축(main axis)을 자동 감지해 해당 축으로만 공간을 차지합니다.
CoUI는 세 가지 변형을 제공합니다:
- Gap — 고정 크기의 기본 간격
- MaxGap — 최대 크기까지만 차지하고 공간이 모자라면 양보하는 유연한 간격
-
SliverGap —
CustomScrollView안의 sliver 간격 (Flutter 전용 프로토콜; Web은 flex 스페이서로 대체)
Live Preview#
class GapDefaultExample extends StatelessComponent {
const GapDefaultExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('A'),
),
Gap.space16(),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('B'),
),
],
classes: 'flex flex-row items-center',
);
}
}
class GapDefaultExample extends StatelessWidget {
const GapDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('A'),
),
const Gap.space16(),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('B'),
),
],
);
}
}
class GapMaxExample extends StatelessComponent {
const GapMaxExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('A'),
),
MaxGap.space64(),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('B'),
),
],
classes: 'flex flex-row items-center',
);
}
}
class GapMaxExample extends StatelessWidget {
const GapMaxExample({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('A'),
),
const MaxGap.space64(),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('B'),
),
],
);
}
}
class GapSliverExample extends StatelessComponent {
const GapSliverExample({super.key});
@override
Component build(BuildContext context) {
final cs = context.colorScheme;
final textClass = 'text-\${CoreTextStyles.bodyMedium.name} text-\${cs.onSurface}';
return div(
[
div([Text('Top item')], classes: textClass),
SliverGap.space24(),
div([Text('Middle item')], classes: textClass),
SliverGap.space24(),
div([Text('Bottom item')], classes: textClass),
],
// `w-full` mirrors Flutter's `CustomScrollView`, which always
// stretches to the cross-axis extent — without it the column
// shrinks to fit and the preview centres it while Flutter's list
// sits at the left edge.
classes: 'flex flex-col items-start w-full',
styles: Styles(raw: {'height': '\${160 / 16}rem'}),
);
}
}
class GapSliverExample extends StatelessWidget {
const GapSliverExample({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textStyle = theme.typography.bodyMedium.toValue(theme: theme).copyWith(
color: theme.colorScheme.onSurface.toValue(),
);
return SizedBox(
height: 160,
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(child: Text('Top item', style: textStyle)),
const SliverGap.space24(),
SliverToBoxAdapter(child: Text('Middle item', style: textStyle)),
const SliverGap.space24(),
SliverToBoxAdapter(child: Text('Bottom item', style: textStyle)),
],
),
);
}
}
class GapChainExample extends StatelessComponent {
const GapChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('A'),
),
const Gap.space16().withStyle(
const CoreGapStyle(
size: CoreSpace.space64,
crossAxisExtent: CoreSpace.space24,
color: CoreColor.token(CoreColors.primary),
),
),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('B'),
),
],
classes: 'flex flex-row items-center',
);
}
}
class GapChainExample extends StatelessWidget {
const GapChainExample({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('A'),
),
const Gap.space16().withStyle(
const CoreGapStyle(
size: CoreSpace.space64,
crossAxisExtent: CoreSpace.space24,
color: CoreColor.token(CoreColors.primary),
),
),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('B'),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- Row나 Column 내에서 자식 간 일관된 간격이 필요할 때
CoreSpace디자인 토큰 기반 간격을 사용하고 싶을 때SizedBox대안으로 의도가 분명한 간격 표현이 필요할 때
대신 다른 컴포넌트를 사용하세요:
Padding: 위젯 주변에 사방 여백을 추가할 때Spacer: Row/Column에서 남은 공간을 모두 채울 때Divider: 시각적 구분선이 필요할 때
기본 사용법 (Basic Usage)#
간격 값은 CoreSpace 토큰마다 하나씩 있는 Gap.spaceN() 이름 있는 생성자로 넣습니다 — 전부 const
이고, 내부에서 const CoreGapStyle(size: CoreSpace.spaceN) 을 접어줍니다.
// 수직 간격 (부모가 Column/flex-col일 때)
Column(
children: [
Text('제목'),
const Gap.space16(),
Text('내용'),
],
)
// 수평 간격 (부모가 Row/flex-row일 때)
Row(
children: [
Icon(LucideIcons.user),
const Gap.space8(),
Text('사용자 이름'),
],
)
// cross 축 / 색이 필요하면 gapStyle 슬롯으로
// (예: 4px 세로 spacer를 96px 가로로 강조)
const Gap(
gapStyle: CoreGapStyle(
size: CoreSpace.space4,
crossAxisExtent: CoreSize.size96,
),
)
// 색을 채워 디버그/구분선 목적
const Gap(
gapStyle: CoreGapStyle(
size: CoreSpace.space16,
color: CoreColor.token(CoreColors.error),
),
)
// cross 축을 무한대로 확장 (parent 전체 채우기)
Gap.expand(CoreSpace.space16)
MaxGap 사용법#
// 주축을 최대 space64까지만 차지, 공간 부족 시 양보
Row(
children: [
Button(variant: .primary, onPressed: () {}, child: Text('A')),
const MaxGap.space64(),
Button(variant: .primary, onPressed: () {}, child: Text('B')),
],
)
SliverGap 사용법#
CustomScrollView(
slivers: [
SliverToBoxAdapter(child: Text('Top')),
const SliverGap.space24(),
SliverToBoxAdapter(child: Text('Middle')),
const SliverGap(
gapStyle: CoreGapStyle(
size: CoreSpace.space24,
color: CoreColor.token(CoreColors.error),
),
),
SliverToBoxAdapter(child: Text('Bottom')),
],
)
Web은 sliver 프로토콜이 없으므로
SliverGap이Gap과 동일한 flex 스페이서로 렌더링됩니다. API는 동일합니다.
빠른 오버라이드 (Chain)#
이미 만든 Gap 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class GapChainExample extends StatelessWidget {
const GapChainExample({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('A'),
),
const Gap.space16().withStyle(
const CoreGapStyle(
size: CoreSpace.space64,
crossAxisExtent: CoreSpace.space24,
color: CoreColor.token(CoreColors.primary),
),
),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('B'),
),
],
);
}
}
class GapChainExample extends StatelessComponent {
const GapChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('A'),
),
const Gap.space16().withStyle(
const CoreGapStyle(
size: CoreSpace.space64,
crossAxisExtent: CoreSpace.space24,
color: CoreColor.token(CoreColors.primary),
),
),
Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('B'),
),
],
classes: 'flex flex-row items-center',
);
}
}
Props / Parameters#
Gap · MaxGap · SliverGap 셋 다 생성자 파라미터가 gapStyle 하나뿐입니다 — 모든 chrome (size
/ crossAxisExtent / color) 이 그 단일 슬롯으로 흐릅니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
gapStyle |
CoreGapStyle? |
null |
간격·교차축·색 단일 진입점 |
세 컴포넌트 모두 CoreGapStyle 을 공유합니다 (MaxGapStyle / SliverGapStyle 은 없습니다).
CoreGapStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
size |
double? |
Gap size along the parent's main axis (logical px).
null
inherits from the theme or the design-system default ([defaultSize]).
|
crossAxisExtent |
double? |
Gap size along the parent's cross axis (logical px).
null
leaves the cross axis to the parent — Web stretches (
align-self: stretch
), Flutter passes
RenderGap
a
0
extent the parent may stretch. No paired
default*
: both platforms branch on it being unset, so a default flips both — the stretch class goes away and every gap claims cross-axis space, painting a band wherever [color] is set. Cross-axis fill is opt-in through
Gap.expand
(
double.infinity
).
Deliberately has no default* — absence is the design.
Both platforms branch on absence, in opposite directions: Web emits
align-self: stretch
ONLY when null; Flutter passes RenderGap a 0 cross extent so the gap claims no cross-axis space. A default removes the stretch class and makes every gap in the kit claim space (painting a band wherever
color
is set). Cross-axis fill is opt-in via Gap.expand(double.infinity).
|
color |
CoreColor? |
Fill colour painted across the gap's rectangle.
null
keeps the gap transparent. No paired
default*
: Flutter returns early from
paint
and Web emits no
background-color
. A default would paint a rectangle in every gap in the kit —
Gap
is the spacer every other component composes.
Deliberately has no default* — absence is the design.
RenderGap.paint returns early on null (
if (fill == null) return
) and Web omits background-color. A default paints a rectangle in every gap in the kit — Gap is the spacer every other component composes.
|
이름 있는 생성자#
-
Gap.spaceN()/MaxGap.spaceN()/SliverGap.spaceN()—CoreSpace토큰마다 하나씩 있는const단축형 (space0~space256).const CoreGapStyle(size: CoreSpace.spaceN)을 접어줍니다. -
Gap.expand(mainAxisExtent, {color})/MaxGap.expand(...)—crossAxisExtent를double.infinity로 설정한 단축형.SliverGap에는 없습니다.
테마 (프로젝트 공통)#
세 컴포넌트가 각자 자기 테마 슬롯을 갖고, 모두 { style: CoreGapStyle? } 하나만 노출합니다 — CoreGapTheme
/ CoreMaxGapTheme / CoreSliverGapTheme.
CoreGapStyle.defaultSize
→ CoreGapTheme.style // 프로젝트 공통
→ widget.gapStyle // 인스턴스별
동작 스펙 (Behavior)#
방향 자동 감지#
별도 axis/direction 파라미터 없음. 부모의 flex/axis 방향을 자동으로 따라갑니다.
-
Flutter:
RenderGap이 부모RenderFlex.direction을 감지. 부모가 Flex가 아니면 enclosingScrollable의 axis를 fallback으로 사용. -
Web: CSS
flex-basis: ${size/16}rem+flex-shrink: 0+align-self: stretch→ 부모flex-direction에 따라row면width로,column이면height로 해석됨
렌더링#
-
Flutter:
LeafRenderObjectWidget→RenderGap(custom RenderBox). - Web:
<div>+ inline CSS (no Tailwind 클래스).
사용 가이드라인 (Usage Guidelines)#
✅ Do — 디자인 토큰 사용#
Column(
children: [
TitleWidget(),
const Gap.space16(),
BodyWidget(),
const Gap.space24(),
ActionWidget(),
],
)
❌ Don't — 매직 넘버 사용#
const Gap(gapStyle: CoreGapStyle(size: 13)) // 왜 13px?
const Gap(gapStyle: CoreGapStyle(size: 27)) // 일관성 없음
토큰 밖 값을 넣으려면 gapStyle 슬롯을 직접 열어야 하고, Gap.spaceN() 단축형은 애초에 토큰만 노출합니다.
❌ Don't — Padding 용도로 사용#
컨테이너 내부 여백은 Padding 또는 CSS p-* class 사용. Gap은 자식 간 간격 전용.
접근성 (Accessibility)#
- 정적 간격 컴포넌트로 인터랙션 없음
- 스크린 리더에 시각적 요소로만 노출
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Gap / MaxGap / SliverGap | 동일 |
| 렌더링 | 자체 RenderGap / RenderSliverGap |
<div> + CSS flex-basis |
| 방향 감지 | 부모 Flex.direction → Scrollable axis | 부모 flex-direction (CSS) |
| 색 해석 | scheme.resolve(color).toValue() → Color |
color.toValue() → inline background-color |
| Sliver 지원 | RenderSliver 기반 | flex 스페이서로 대체 (API 동일) |