크로스 플랫폼#
CoUI는 Flutter와 Jaspr Web에서 동일한 컴포넌트 세트를 제공합니다.
아키텍처#
coui_core (공유 계약)
├── coui_flutter (Flutter 구현)
└── coui_web (Jaspr Web 구현)
공유 계약 (coui_core)#
플랫폼 간 공통 인터페이스를 정의합니다:
// 예: CoreButtonContract (widget 타입만 <W> generic)
abstract interface class CoreButtonContract<W> {
bool? get enabled;
CoreVoidCallback? get onPressed;
CoreButtonVariant get variant;
CoreButtonStyle? get buttonStyle;
}
API 비교#
생성자 패턴#
// Named parameters, Widget tree — 이름·순서·기본값이 Web과 동일
Button(
variant: CoreButtonVariant.primary,
size: CoreComponentSize.md,
onPressed: () {},
child: Text('Click'),
)
// Named parameters, Component tree — Flutter와 글자 그대로 동일
Button(
variant: CoreButtonVariant.primary,
size: CoreComponentSize.md,
onPressed: () {},
child: Text('Click'),
)
테마 적용#
// CoUIApp(Flutter 의 앱 루트) + CoreComponentTheme 슬롯
CoUIApp(
theme: ThemeData.fromCore(
CoreThemePresets.light,
coreComponentTheme: const CoreComponentTheme(
button: CoreButtonTheme(
style: CoreButtonStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
),
),
),
),
home: MyHomePage(),
)
// CoUIWeb(coui_web 의 Theme provider, Flutter CoUIApp 대응) + 동일한
// CoreComponentTheme 슬롯
CoUIWeb(
theme: ThemeData.fromCore(
CoreThemePresets.light,
coreComponentTheme: const CoreComponentTheme(
button: CoreButtonTheme(
style: CoreButtonStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
),
),
),
),
child: MyApp(),
)
컴포넌트 매핑#
전체 140+ 컴포넌트가 양쪽 플랫폼에서 동일하게 제공됩니다:
| 카테고리 | Flutter | Web |
|---|---|---|
| Form (Input, Select, etc.) | 20+ | 20+ |
| Display (Avatar, Badge, etc.) | 25+ | 25+ |
| Navigation (Menu, Tabs, etc.) | 10+ | 10+ |
| Overlay (Dialog, Toast, etc.) | 10+ | 10+ |
| Layout (Accordion, Grid, etc.) | 15+ | 15+ |
| Control (Button, FAB, etc.) | 10+ | 10+ |
| Chart (Tracker) | 1 | 1 |
| Locale (Localizations) | 1 | 1 |
디자인 토큰#
CoUI는 coui_core에 정의된 디자인 토큰을 Web/Flutter 양쪽에서 공유합니다. 소비자용 맵은 디자인 토큰
을 보세요.
Border Radius 토큰#
field/selector/box 같은 글로벌 시맨틱 radius 토큰은 의도적으로 없다
—
컴포넌트가 자기 CoreXxxStyle.defaultBorderRadius에서 CoreRadius의 숫자
primitive(radius0 ~ radius64, radius9999)를 직접 고른다. 각 컴포넌트가
실제로 고른 값:
| 컴포넌트 | CoreXxxStyle.defaultBorderRadius |
Web (Tailwind) | Flutter |
|---|---|---|---|
TextField |
CoreRadius.radius4 |
rounded-${CoreRadius.scale.radius4} |
BorderRadius.circular(CoreRadius.radius4) |
Button |
CoreRadius.radius8 |
rounded-${CoreRadius.scale.radius8} |
BorderRadius.circular(CoreRadius.radius8) |
Card |
CoreRadius.radius16 |
rounded-${CoreRadius.scale.radius16} |
BorderRadius.circular(CoreRadius.radius16) |
Badge |
CoreRadius.radius9999 |
rounded-${CoreRadius.scale.radius9999} |
BorderRadius.circular(CoreRadius.radius9999) |
Size 토큰#
시맨틱 크기는 CoreComponentSize(xs/sm/md/lg/xl) 하나로 통일되고,
실제 픽셀 값은 각 컴포넌트의 CoreXxxStyle.defaultsBySize map이 정한다 —
전역 "field size" 상수는 없다.
| 컴포넌트 | size: md일 때 높이 | Web (Tailwind) | Flutter |
|---|---|---|---|
TextField |
CoreSize.size40 = 40px |
h-10 |
CoreTextFieldStyle.defaultsBySize[.md]!.height |
애니메이션 토큰#
인터랙션 애니메이션은 CoreDuration을 단일 소스로 사용합니다.
컴포넌트는 시맨틱 alias tier 를 참조합니다. raw 스케일(CoreDuration.ms150 등)은 그대로 남아 있지만, alias 를 거쳐야 reduced-motion 같은 전역 모션 오버라이드가 그 값에 닿습니다.
| alias | 값 | 용도 |
|---|---|---|
CoreDuration.instant | 100ms | 즉각 반응 — 체크 표시, 토글 |
CoreDuration.fast | 150ms | 작은 요소 — 툴팁 등장, 버튼 hover, 입력 포커스 |
CoreDuration.normal | 200ms | 대부분의 전환 기본값 — 드롭다운, 카드 hover, 아코디언 |
CoreDuration.moderate | 300ms | 중간 요소 — 모달 진입, 패널 슬라이드 |
CoreDuration.slow | 500ms | 페이지 레벨 전환, 복합 choreography |
표시 시간(CoreDuration.ms1000 이상)은 "얼마나 오래 머무르나"이지 "얼마나 빨리 움직이나"가 아니라 alias 를 갖지 않습니다.
여러 요소가 순차로 들어올 때는 항목 간격이 아니라 시퀀스 총합을 정합니다 — CoreDuration.slow(500ms)를 넘지 않고, 그 총합이 Core 의 duration 필드에서 와야 "동작 줄이기" 설정이 닿습니다. 형태는 둘 다 유효합니다: 고정 간격(30–50ms)으로 차례로 들어오거나,
총합 / 개수로 간격을 파생해 다 같이 도착하거나. 항목 수가 런타임에 정해지므로 절대 간격 하나로 못 박지 않습니다.
순환하는 표시(스피너·로딩 점·shimmer)는 들어오는 게 아니라 도는 것이라 이 상한의 대상이 아니지만, 양 플랫폼이 같은 Core 값을 읽어야 하는 것은 같습니다.
// Duration(milliseconds:)가 CoreDuration 토큰을 경유
const kDefaultDuration = Duration(milliseconds: CoreDuration.fast); // 150ms
const kToggleDuration = Duration(milliseconds: CoreDuration.instant); // 100ms
// 컴포넌트에서 사용
AnimatedContainer(
duration: Duration(milliseconds: CoreDuration.normal), // 200ms
curve: Curves.easeInOut,
child: card,
)
// Tailwind duration 클래스로 매핑 (컴포넌트 resolver 가 CoreDuration 토큰에서 파생)
Button(
variant: CoreButtonVariant.primary,
// base: 'transition-colors duration-150'
onPressed: () {},
child: Text('Primary'),
)
// onTap 이 있는 카드는 interactive 로 간주되어 hover shadow 가 자동 적용
Card(
onTap: () {},
// base: 'transition-shadow duration-200'
child: Text('Card'),
)
컴포넌트별 애니메이션 매핑#
| 컴포넌트 | 인터랙션 | Duration | Web | Flutter |
|---|---|---|---|---|
| Button | hover 색상 | fast (150ms) | transition-colors duration-150 |
WidgetState 기반 |
| Card | hover shadow (onTap 있을 때) | normal (200ms) | transition-shadow duration-200 |
hoverElevation + AnimatedContainer |
| Toggle | thumb slide | instant (100ms) | transition-transform duration-100 |
AnimatedPositioned |
| Accordion | expand/collapse | normal (200ms) | transition-all duration-200 |
SizeTransition |
| TextField | focus ring | fast (150ms) | transition-colors duration-150 |
AnimatedContainer |
Spring 프리셋#
Flutter에서 물리 기반 애니메이션이 필요할 때 CoreSpringConfig를 사용합니다:
| 프리셋 | damping | stiffness | 용도 |
|---|---|---|---|
| gentle | 0.8 | 200 | 부드러운 전환 |
| standard | 0.7 | 300 | 기본 |
| bouncy | 0.5 | 400 | 탄성 효과 |
| snappy | 0.9 | 500 | 빠른 반응 |
| stiff | 1.0 | 600 | 즉각 반응 |
통일된 컴포넌트 API#
Web과 Flutter는 동일한 named properties API를 사용합니다.
Layout#
슬롯 이름이 같고 Text 도 양 플랫폼에서 같은 이름이라, 이 예제는 Flutter 와 Web
에서 글자 그대로 동일합니다 — 탭을 나눌 것이 없습니다.
Card(
header: Text('Card Title').titleMedium.semibold,
body: Text('Description').bodyMedium.onSurfaceVariant,
footer: Button(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: Text('Action'),
),
)
Form#
Select<String>(
items: [
CoreSelectItem(value: 'apple', label: 'Apple'),
CoreSelectItem(value: 'banana', label: 'Banana'),
],
placeholder: 'Select a fruit',
onChanged: (v) => print(v),
)
Select<String>(
items: [
CoreSelectItem(value: 'apple', label: 'Apple'),
CoreSelectItem(value: 'banana', label: 'Banana'),
],
placeholder: 'Select a fruit',
onChanged: (v) => print(v),
)
Display#
데이터 모델(CoreTimelineItem)은 coui_core 에 살기 때문에 양 플랫폼이 같은
타입을 그대로 씁니다 — 여기서도 코드가 동일합니다.
Timeline(
items: [
CoreTimelineItem(title: 'Order placed', timestamp: '10:00 AM'),
CoreTimelineItem(title: 'Shipped', timestamp: '2:00 PM'),
],
)
개발 팁#
- API 먼저 설계: coui_core에 계약을 정의하고 양쪽 구현
-
Named properties 우선:
child대신title,content,footer등 명시적 파라미터 - String 입력 지원: 가능하면 String을 받아 내부에서 Text/Component.text로 변환
-
CoreDuration 사용: 하드코딩 duration 대신
Duration(milliseconds: CoreDuration.*)참조 - 테스트 분리: Flutter widget test + Web unit test 별도 작성
- DCM 통일: 양쪽 패키지에 동일한 DCM 규칙 적용
-
Barrel export: 양쪽
coui_flutter.dart/coui_web.dart동기화 유지