LogoSkills

CoUI Flutter Quick Reference (v0.127 bare-widget standard)

All signatures below are verified against the kobic-vendored CoUI:

๊ธฐ์ค€: 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; the cc-coui plugin'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_core contracts
  • Import everything through the core barrel:
// โœ… 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 on package:coui_flutter directly.


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, ButtonDensity do 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 removed ButtonStyle.primary().copyWith(...). There is no separate ButtonSize/ButtonDensity scale โ€” size is CoreComponentSize (xs/sm/md/lg/xl) and dimensional tweaks (padding, height, etc.) flow through buttonStyle.


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 / CoBadge do not exist โ€” use Badge(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, CoRadixIcons no longer compile (v0.127 de-prefix). HeroIcon (the standalone heroicons package widget) is not used โ€” CoUI's own HeroIcons icon set (a StyledIconName, via the CoUI Icon widget) 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) => ...,
)

Input chrome/dimensional overrides go through inputStyle: CoreInputStyle(...) (no size:/shape: parameter). CoreInputType: text, password, email, number, tel, url, search. TextField overrides go through textFieldStyle: CoreTextFieldStyle(...); size via size: .md (CoreComponentSize). See coui-input / coui-text-field for 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,
)
APIStatus
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)#

TokenSizePurpose
xs12pxCaption, annotation
sm14pxSecondary text, labels
base16pxDefault body
lg18pxEmphasis body, subheading
xl20pxHeading
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.mediumCoreSpace.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#

ColorHEXPurpose
neutral100#FFFFFFWhite background
neutral95 #F9FAFB Header/section background (distinct from body)
neutral85#F4F5F6Light gray
neutral80#ECEEEFDefault border
neutral70 #B4B8C4 Dark border (clear separator)
neutral60#8F929AInactive 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#

ColorPurpose
base100Lightest background
base200Medium background
base300Darkest 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