๊ธฐ์ค: coui v0.127.15 (
Co์ ๋์ด ์ ๊ฑฐ๋ de-prefix ์ธ๋. kobic development๋ v0.127.15+ ์๋ธ๋ชจ๋๋ก ๋ง์ด๊ทธ๋ ์ด์ ์๋ฃ). ๐ก See CLAUDE.md for the full guide. This document is a quick reference; thecc-couiplugin's per-component skills (coui-button,coui-input,coui-text-field,coui-badge,coui-gap, โฆ) are the source of truth โ check there when in doubt.
Authoritative Source#
All signatures below are verified against the kobic-vendored CoUI:
- Components:
package/coui/packages/coui_flutter/lib/src/components/**/co_*.dart - Contracts/enums:
package/coui/packages/coui_flutter+coui_corecontracts - Import everything through the
corebarrel:
// โ
CORRECT: single barrel import
import 'package:core/core.dart';
// โ WRONG: never import coui_flutter directly in feature/app code
import 'package:coui_flutter/coui_flutter.dart';
CoUI widgets are re-exported through
package:core/core.dart. Feature and app code must never depend onpackage:coui_flutterdirectly.
Components are bare <X> with variant: / size: enums#
CoUI follows a single unified widget per component (Button, Badge,
TextField, Icon, Gap) โ as of v0.127 there is
no Co prefix on any
widget class; Co<X> names were removed in a hard break and no longer compile
(no aliases). There are no named constructors like Button.primary()
/
PrimaryBadge / ButtonStyle.ghost(). Visual style is selected via the
variant: (and size:) enum parameters; chrome overrides flow through the
single style object (buttonStyle: / badgeStyle: / inputStyle:
/
textFieldStyle: / iconStyle:).
| Concept | โ v0.127 standard | โ Removed / never existed |
|---|---|---|
| Button | Button(variant: ..., size: ...) |
CoButton
,
Button.primary()
,
Button.ghost()
,
ButtonStyle.ghost()
|
| Badge | Badge(variant: ...) |
CoBadge, PrimaryBadge, DestructiveBadge |
| Gap | const Gap.space16() / Gap(gapStyle: CoreGapStyle(size: 16)) |
CoGap
,
Gap.s4()
,
Gap(Insets.medium)
,
Spacing.s4
,
Insets.*
,
Gap(size: CoreSpace.spaceN)
(v0.92 bare-
size
form)
|
| Icon | Icon(LucideIcons.x) |
CoIcon, HeroIcon(HeroIcons.x) |
| Input (bare, no label/prefix/suffix) | Input(type: CoreInputType.x, ...) |
CoInput |
| TextField (label/description/errorText/prefix/suffix) | TextField(label: ..., prefix: ..., features: [CoreInputFeature.clear()]) |
CoTextField
,
FormTextField
, bare
InputFeature.leading(...)
/
.trailing(...)
(leading/trailing icons go through
prefix
/
suffix
directly;
CoreInputFeature
only has
.clear()
/
.passwordToggle()
)
|
| Text style | context.textStyles.lgSemibold |
context.theme.typography, Text().lg.bold chaining |
Buttons โ Button#
class Button extends StatefulWidget {
const Button({
required this.child,
super.key,
this.variant = CoreButtonVariant.primary,
this.size = CoreComponentSize.md,
this.shape = CoreButtonShape.rectangle,
this.leading,
this.trailing,
this.onPressed,
this.enabled,
this.buttonStyle,
this.expanded = false,
// ... interaction callbacks
});
}
CoreButtonVariant#
primary ยท secondary ยท outline ยท ghost ยท link
ยท text ยท plain ยท
destructive ยท card ยท menu ยท menubar
ยท fixed
CoreComponentSize (shared by all components)#
xs ยท sm ยท md (default) ยท lg ยท xl
ButtonSize.medium,ButtonSize.normal,ButtonDensitydo not exist.
Usage#
// Primary action โ uses defaults (variant: primary, size: md)
Button(
expanded: true,
enabled: state.isValid,
onPressed: () => context.read<MyBloc>().add(const MyEvent.submitted()),
child: const Text('์ ์ฅ'),
)
// Secondary / cancel โ dot-shorthand on the enum
Button(
variant: .ghost,
onPressed: () => context.pop(),
child: const Text('์ทจ์'),
)
// Destructive
Button(
variant: .destructive,
onPressed: onDelete,
child: const Text('์ญ์ '),
)
// Sized + leading icon
Button(
variant: .outline,
size: .sm,
leading: const Icon(LucideIcons.pencil),
onPressed: onEdit,
child: const Text('ํธ์ง'),
)
// Icon-only via shape
Button(
variant: .ghost,
shape: .square,
onPressed: onMore,
child: const Icon(LucideIcons.ellipsisVertical),
)
Chrome / dimensional overrides go through
buttonStyle: CoreButtonStyle(...), not through removedButtonStyle.primary().copyWith(...). There is no separateButtonSize/ButtonDensityscale โ size isCoreComponentSize(xs/sm/md/lg/xl) and dimensional tweaks (padding, height, etc.) flow throughbuttonStyle.
Badge โ Badge#
class Badge extends StatefulWidget {
const Badge({
this.variant = CoreBadgeVariant.defaultVariant, // primary
required this.child,
super.key,
this.size = CoreComponentSize.sm,
this.leading,
this.trailing,
this.onPressed,
this.badgeStyle,
});
}
CoreBadgeVariant#
primary (default) ยท secondary ยท outline ยท destructive
Usage#
// Positive / active status
const Badge(child: Text('ํ๋งค์ค')) // variant defaults to primary
// Negative status
const Badge(variant: .destructive, child: Text('ํ๋งค์ค์ง'))
// With a leading icon
Badge(
variant: .secondary,
leading: const Icon(LucideIcons.eye),
child: const Text('๋ฏธ๋ฆฌ๋ณด๊ธฐ'),
)
// Conditional
book.isActive
? const Badge(child: Text('ํ๋งค์ค'))
: const Badge(variant: .destructive, child: Text('ํ๋งค์ค์ง'))
PrimaryBadge/DestructiveBadge/CoBadgedo not exist โ useBadge(variant: ...).
Icons โ Icon#
class Icon extends StatefulWidget {
const Icon(this.icon, {super.key, this.iconStyle});
}
Icons come from Iconify icon sets re-exported by CoUI โ kobic primarily uses
LucideIcons and RadixIcons (bare names, no Co
prefix in v0.127).
const Icon(LucideIcons.trash)
const Icon(RadixIcons.chevronDown)
// Size / color override
Icon(
LucideIcons.circleAlert,
iconStyle: CoreIconStyle(size: 16, color: context.appColors.error.toValue()),
)
CoIcon,CoLucideIcons,CoRadixIconsno longer compile (v0.127 de-prefix).HeroIcon(the standaloneheroiconspackage widget) is not used โ CoUI's ownHeroIconsicon set (aStyledIconName, via the CoUIIconwidget) is the canonical way to render HeroIcons glyphs if needed.
Text Input โ Input (bare) vs TextField (labelled)#
Input is the bare single-line text-entry primitive (type, placeholder,
value, onChanged, validation/length/range attributes) โ it has
no
label / prefix / suffix slots. For a labelled field with prefix / suffix /
errorText / built-in clear-or-password-toggle affordances, use TextField.
There is no FormTextField, and there is no bare InputFeature
leading/trailing widget-building API โ the platform-neutral data model is
CoreInputFeature, and it only has two factories: .clear()
and
.passwordToggle() (arbitrary leading/trailing icons go through TextField's
own prefix/suffix parameters, not through a feature).
// Input โ bare text-entry primitive, no label
Input(
type: CoreInputType.email,
placeholder: '์ด๋ฉ์ผ์ ์
๋ ฅํ์ธ์',
value: state.email,
onChanged: (value) =>
context.read<MyBloc>().add(MyEvent.emailChanged(value)),
)
// Error variant
Input(
type: CoreInputType.email,
placeholder: '์ด๋ฉ์ผ',
variant: CoreInputVariant.error,
onChanged: (value) => ...,
)
// TextField โ labelled field with label / prefix / errorText / built-in features
TextField(
label: '์ด๋ฉ์ผ',
placeholder: const Text('์ด๋ฉ์ผ์ ์
๋ ฅํ์ธ์'),
controller: controller,
keyboardType: TextInputType.emailAddress,
prefix: const Icon(LucideIcons.mail),
errorText: state.emailError,
onChanged: (value) => ...,
)
// TextField โ password with the built-in visibility-toggle feature
TextField(
label: '๋น๋ฐ๋ฒํธ',
obscureText: true,
features: const [CoreInputFeature.passwordToggle()],
onChanged: (value) => ...,
)
Inputchrome/dimensional overrides go throughinputStyle: CoreInputStyle(...)(nosize:/shape:parameter).CoreInputType:text,password,number,tel,url,search.TextFieldoverrides go throughtextFieldStyle: CoreTextFieldStyle(...); size viasize: .md(CoreComponentSize). Seecoui-input/coui-text-fieldfor the complete parameter set.
Typography โ context.textStyles#
context.textStyles returns the CoUI Typography from the current theme.
Use its style getters directly; never use context.theme.typography or the
removed Text().lg.bold chaining.
// โ
Project standard
style: context.textStyles.lgSemibold
style: context.textStyles.smSemibold
style: context.textStyles.baseMedium
// โ
copyWith composition
style: context.textStyles.lgSemibold.copyWith(
color: context.appColors.neutral5,
)
| API | Status |
|---|---|
context.textStyles.<getter> | โ Project standard |
context.theme.typography | โ Prohibited |
Text('...').lg.bold chaining | โ Removed |
Common getters (<size><weight>)#
xsSemibold ยท xsBold ยท smNormal ยท smSemibold ยท smBold
ยท
baseNormal ยท baseSemibold ยท baseBold ยท
lgNormal ยท lgSemibold ยท lgBold ยท
xlNormal ยท xlSemibold ยท xlBold ยท
x2lBold ยท x3lSemibold โฆ (plus Material-3 semantic getters such as
titleMedium, bodyMedium, labelLargeSemiBold).
Size scale (approx px)#
| Token | Size | Purpose |
|---|---|---|
xs | 12px | Caption, annotation |
sm | 14px | Secondary text, labels |
base | 16px | Default body |
lg | 18px | Emphasis body, subheading |
xl | 20px | Heading |
x2l+ | 24px+ | Large headings |
Text('์ ๋ชฉ', style: context.textStyles.lgSemibold)
Text('๋ณธ๋ฌธ', style: context.textStyles.baseNormal)
Text('์บก์
', style: context.textStyles.smNormal)
Spacing โ Gap (token constructors / gapStyle)#
Prefer the Gap.spaceN() token constructors. For non-token sizes, a cross-axis
extent, or a fill color use the single gapStyle: CoreGapStyle(...) slot.
Insets, Spacing, and older CoGap forms are removed/legacy. As of v0.127
there is no Co prefix โ the widget is bare Gap; the bare
Gap(size: CoreSpace.spaceN) constructor-level size: form (pre-v0.127) is
also gone โ use the token constructor or gapStyle: instead.
class Gap extends StatelessWidget {
const Gap({this.gapStyle, super.key});
// plus const Gap.spaceN() per CoreSpace.spaceN, and Gap.expand(...)
}
// โ
v0.127 standard โ token constructors
const Gap.space4() // 4px
const Gap.space8() // 8px
const Gap.space16() // 16px
const Gap.space24() // 24px
// โ
Non-token size / cross extent / fill color via gapStyle
const Gap(gapStyle: CoreGapStyle(size: 18))
// EdgeInsets / padding also use CoreSpace tokens
Padding(
padding: const EdgeInsets.symmetric(horizontal: CoreSpace.space16),
child: child,
)
CoreSpace tokens (double, logical px)#
space0 ยท space2 ยท space4 ยท space6 ยท space8
ยท space10 ยท space12 ยท
space14 ยท space16 ยท space20 ยท space24
ยท space28 ยท space32 โฆ (2px
steps to 16, then 4px steps). The token name is the pixel value. Each value
has a matching Gap.spaceN() constructor.
| โ Removed / legacy | โ Replacement (v0.127.15) |
|---|---|
โ CoGap (any form) | Gap |
const Gap.s1() (4px) | const Gap.space4() |
const Gap.s2() (8px) | const Gap.space8() |
const Gap.s4() (16px) | const Gap.space16() |
Gap(Insets.medium) | const Gap.space16() |
const Gap(size: CoreSpace.space16) (pre-v0.127 bare-size form) |
const Gap.space16()
or
const Gap(gapStyle: CoreGapStyle(size: CoreSpace.space16))
|
Spacing.s4, Insets.medium | CoreSpace.space16 |
Loading / Skeleton Patterns#
// Skeleton loading (entire widget)
widget.skeletonizer(enabled: state.isLoading)
// Conditional loading (spinner)
widget.loadingOr(
isLoading: state.isLoading,
loadingWidget: const CircularProgressIndicator(),
)
// Empty state handling
listWidget.emptyOrWhen(
condition: () => list.isEmpty,
emptyWidget: const EmptyStateWidget(),
)
Complete Page Example#
import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';
class MyPage extends HookWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
final appColors = context.appColors;
return BlocSignalProvider(
create: (_) => MyBloc(),
child: BlocSignalBuilder<MyBloc, MyState>(
builder: (context, state) {
return Scaffold(
headers: [
AppBar(
title: Text(
'ํ์ด์ง ์ ๋ชฉ',
style: context.textStyles.lgSemibold.copyWith(
color: appColors.neutral5,
),
),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: CoreSpace.space16,
),
child: Column(
crossAxisAlignment: .start,
children: [
const Gap.space16(),
Input(
type: CoreInputType.text,
placeholder: '์ด๋ฆ์ ์
๋ ฅํ์ธ์',
onChanged: (value) => context.read<MyBloc>().add(
MyEvent.nameChanged(value),
),
),
const Gap.space16(),
Button(
expanded: true,
enabled: state.isValid,
onPressed: () => context.read<MyBloc>().add(
const MyEvent.submitted(),
),
child: const Text('์ ์ฅ'),
),
],
),
).skeletonizer(enabled: state.isLoading),
);
},
),
);
}
}
Color Scheme (Console UI)#
AppColors Neutral Series#
| Color | HEX | Purpose |
|---|---|---|
neutral100 | #FFFFFF | White background |
neutral95 |
#F9FAFB | Header/section background (distinct from body) |
neutral85 | #F4F5F6 | Light gray |
neutral80 | #ECEEEF | Default border |
neutral70 |
#B4B8C4 | Dark border (clear separator) |
neutral60 | #8F929A | Inactive text |
neutral30 | - | Body text |
// Header background (distinct from body)
color: context.appColors.neutral95,
// Dark border (clear separation)
border: Border(right: BorderSide(color: context.appColors.neutral70)),
// Default border
border: Border.all(color: context.appColors.neutral80),
ColorScheme Base Series#
| Color | Purpose |
|---|---|
base100 | Lightest background |
base200 | Medium background |
base300 | Darkest background/border |
OutlinedContainer(
backgroundColor: theme.colorScheme.base100,
borderColor: theme.colorScheme.base300, // ์งํ ํ
๋๋ฆฌ
)
Reference Documents#
Refer to CLAUDE.md for the following:
- Color rules (AppColors, context.appColors)
- Dot Shorthand rules (Dart 3.10+) โ note
variant: .ghost,size: .sm - BLoC Event/State patterns (sealed class)
- BLoC async handlers (isClosed check)
- Complete list of Context Extensions