LogoSkills

coui-flutter-rules

CoUI Flutter quick reference โ€” context.textStyles, Material 3 semantic styles. Use when styling Flutter UI with CoUI text styles and Material 3 semantic styles.

CoUI Flutter Quick Reference#

ํ•œ๋งˆ๋””๋กœ (๋น„๊ฐœ๋ฐœ์ž์šฉ)#

์ด ๋ฌธ์„œ๋Š” ์šฐ๋ฆฌ ์•ฑ ํ™”๋ฉด์„ ๋งŒ๋“ค ๋•Œ ์“ฐ๋Š” "๊ณต์‹ ๋ถ€ํ’ˆ ์นดํƒˆ๋กœ๊ทธ"์ด์ž "์กฐ๋ฆฝ ์„ค๋ช…์„œ"์ž…๋‹ˆ๋‹ค. ๊ฐ€๊ตฌ๋ฅผ ๋งŒ๋“ค ๋•Œ ์ •ํ•ด์ง„ ๊ทœ๊ฒฉ์˜ ๋‚˜์‚ฌ์™€ ํŒ์ž๋งŒ ์“ฐ๋ฉด ํ”๋“ค๋ฆผ ์—†์ด ํŠผํŠผํ•˜๋“ฏ์ด, ๊ธ€์ž ํฌ๊ธฐยท๋ฒ„ํŠผยท๋‚ ์งœ ์„ ํƒ๊ธฐ ๊ฐ™์€ ํ™”๋ฉด ๋ถ€ํ’ˆ(CoUI)์„ ์ •ํ•ด์ง„ ๋ฐฉ์‹๋Œ€๋กœ๋งŒ ์“ฐ๋„๋ก ์ •๋ฆฌํ•ด ๋‘์—ˆ์Šต๋‹ˆ๋‹ค. ๋•๋ถ„์— ๋ˆ„๊ฐ€ ๋งŒ๋“ค์–ด๋„ ์•ฑ์˜ ๋ชจ์–‘๊ณผ ๋А๋‚Œ์ด ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

๋ฌด์—‡์„ยท์–ธ์ œ (๋น„๊ฐœ๋ฐœ์ž์šฉ)#

  • ๋ฌด์—‡์„ ํ•ด ์ฃผ๋‚˜์š”: ๊ธ€์ž ์Šคํƒ€์ผ, ๋ฒ„ํŠผ ํฌ๊ธฐ, ์ž…๋ ฅ์นธ, ๋ฐฐ์ง€(์ƒํƒœ ํ‘œ์‹œ), ๋‚ ์งœ ์„ ํƒ๊ธฐ, ๊ฐ„๊ฒฉ, ์ƒ‰์ƒ ๋“ฑ ์ž์ฃผ ์“ฐ๋Š” ํ™”๋ฉด ๋ถ€ํ’ˆ์˜ "์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ๋ฒ•"๊ณผ "์“ฐ๋ฉด ์•ˆ ๋˜๋Š” ์˜›๋‚  ๋ฐฉ์‹"์„ ํ•œ๋ˆˆ์— ์ •๋ฆฌํ•ด ์ค๋‹ˆ๋‹ค.
  • ์–ธ์ œ ์“ฐ์ด๋‚˜์š”: ๊ฐœ๋ฐœ์ž๋‚˜ AI๊ฐ€ ์ƒˆ ํ™”๋ฉด์„ ๋งŒ๋“ค๊ฑฐ๋‚˜ ๊ธฐ์กด ํ™”๋ฉด์„ ์†๋ณผ ๋•Œ, ์ด ์นดํƒˆ๋กœ๊ทธ๋ฅผ ํŽผ์ณ ๋ณด๊ณ  ๊ทœ๊ฒฉ์— ๋งž๋Š” ๋ถ€ํ’ˆ์„ ๊ณจ๋ผ ์”๋‹ˆ๋‹ค.
  • ์™œ ์ข‹๋‚˜์š”: ํ™”๋ฉด๋งˆ๋‹ค ์ œ๊ฐ๊ฐ์ด ๋˜๊ฑฐ๋‚˜, ๋”๋Š” ์“ฐ์ง€ ์•Š๋Š” ์˜›๋‚  ๋ฐฉ์‹์„ ์ž˜๋ชป ๊ฐ€์ ธ๋‹ค ์“ฐ๋Š” ์‹ค์ˆ˜๋ฅผ ์ค„์—ฌ ์ค๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ์ ์œผ๋กœ ํ™”๋ฉด์ด ์ผ๊ด€๋˜๊ณ  ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ์šฉ์–ด (๋น„๊ฐœ๋ฐœ์ž์šฉ)#

์šฉ์–ด์‰ฌ์šด ์„ค๋ช…
CoUI์šฐ๋ฆฌ ํŒ€์ด ์“ฐ๋Š” ํ™”๋ฉด ๋ถ€ํ’ˆ ๋ชจ์Œ์ง‘(๋””์ž์ธ ์‹œ์Šคํ…œ). ์ •ํ•ด์ง„ ๊ทœ๊ฒฉ์˜ ๋ถ€ํ’ˆ๋งŒ ๊ณจ๋ผ ์“ฐ๋„๋ก ๋งŒ๋“  ๊ฒƒ
Deprecated"์ด์ œ๋Š” ์“ฐ์ง€ ๋งˆ์„ธ์š”"๋กœ ํ‘œ์‹œ๋œ ์˜›๋‚  ๋ฐฉ์‹. ๋‹น์žฅ์€ ๋™์ž‘ํ•ด๋„ ๊ณง ์‚ฌ๋ผ์ง€๋ฏ€๋กœ ์ƒˆ ๋ฐฉ์‹์œผ๋กœ ๋ฐ”๊ฟ”์•ผ ํ•จ
Typography(ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ)๊ธ€์ž์˜ ํฌ๊ธฐยท๊ตต๊ธฐ ๊ฐ™์€ ๊ธ€์”จ ์Šคํƒ€์ผ ๋ชจ์Œ
TextField์‚ฌ์šฉ์ž๊ฐ€ ๊ธ€์ž๋ฅผ ์ž…๋ ฅํ•˜๋Š” ์ž…๋ ฅ์นธ
Button(๋ฒ„ํŠผ)๋ˆŒ๋Ÿฌ์„œ ๋™์ž‘์„ ์‹คํ–‰ํ•˜๋Š” ๋ฒ„ํŠผ. ํฌ๊ธฐ์™€ ๋นฝ๋นฝํ•จ ์ •๋„๋ฅผ ์ •ํ•ด์ง„ ์˜ต์…˜์—์„œ ๊ณ ๋ฆ„
Badge(๋ฐฐ์ง€)"ํŒ๋งค์ค‘", "ํŒ๋งค ์ค‘์ง€"์ฒ˜๋Ÿผ ์ƒํƒœ๋ฅผ ์ž‘๊ฒŒ ํ‘œ์‹œํ•˜๋Š” ๋ผ๋ฒจ
DatePicker๋‹ฌ๋ ฅ์—์„œ ๋‚ ์งœ๋ฅผ ๊ณ ๋ฅด๋Š” ๋ถ€ํ’ˆ
FormField / Validator์ž…๋ ฅ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅธ์ง€(์˜ˆ: ํ•„์ˆ˜ ํ•ญ๋ชฉ ๋ˆ„๋ฝ) ๊ฒ€์‚ฌํ•ด ์ฃผ๋Š” ๊ธฐ๋Šฅ
Skeleton(์Šค์ผˆ๋ ˆํ†ค)๋‚ด์šฉ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋™์•ˆ ๋ณด์—ฌ ์ฃผ๋Š” ํšŒ์ƒ‰ ๋นˆ ํ™”๋ฉด(๋กœ๋”ฉ ์ž๋ฆฌํ‘œ์‹œ)
Tab(ํƒญ)ํ™”๋ฉด ์ƒ๋‹จ์—์„œ ํ•ญ๋ชฉ์„ ์ „ํ™˜ํ•˜๋Š” ๊ฐˆ๋ž˜(์˜ˆ: ์ถœํŒ์‚ฌ / ์ €์ž / ๊ณ„์•ฝ)
CoResizableํ™”๋ฉด์„ ์ขŒ์šฐ(๋˜๋Š” ์ƒํ•˜)๋กœ ๋‚˜๋ˆ  ๊ฒฝ๊ณ„์„ ์„ ๋Œ์–ด ๋น„์œจ์„ ์กฐ์ ˆํ•˜๋Š” ๋ถ„ํ•  ํŒจ๋„
ColorScheme / ์ƒ‰์ƒ ํ† ํฐ๋ฐฐ๊ฒฝยทํ…Œ๋‘๋ฆฌยท๊ธ€์ž ์ƒ‰์„ ์ด๋ฆ„์œผ๋กœ ์ •ํ•ด ๋‘” ํ‘œ์ค€ ์ƒ‰์ƒํ‘œ

๐Ÿ’ก ์ „์ฒด ๊ฐ€์ด๋“œ๋Š” CLAUDE.md๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”. ์ด ๋ฌธ์„œ๋Š” CoUI ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ ์‹œ ๋น ๋ฅธ ์ฐธ์กฐ๋ฅผ ์œ„ํ•œ ์š”์•ฝ๋ณธ์ž…๋‹ˆ๋‹ค.

Typography API ๊ฐ€์ด๋“œ#

ํ”„๋กœ์ ํŠธ ํ‘œ์ค€: context.textStyles + Material 3 Semantic#

์ด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” context.textStyles + Material 3 semantic styles(titleLarge, bodyMedium ๋“ฑ)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค (161๊ฑด/30ํŒŒ์ผ).

// โœ… ํ”„๋กœ์ ํŠธ ํ‘œ์ค€: M3 semantic styles ์‚ฌ์šฉ
style: context.textStyles.titleSmall                              // 16px + w600
style: context.textStyles.bodyMedium                              // 16px + w400
style: context.textStyles.labelLarge.copyWith(fontWeight: .w600)  // 14px + w600 (์ปค์Šคํ…€)

// โœ… copyWith ์กฐํ•ฉ
style: context.textStyles.titleSmall.copyWith(
  color: context.appColors.neutral5,
)

API ์„ ํƒ#

API์ƒํƒœ
context.textStyles.{titleX/bodyX/labelX/...} โœ… ํ”„๋กœ์ ํŠธ ํ‘œ์ค€ (M3 semantic)
context.textStyles.{lgSemibold/sm/xs/...} โš ๏ธ Deprecated (Tailwind ์กฐํ•ฉ โ€” CoUI ์‹ ๊ทœ API์—์„œ ์ œ๊ฑฐ๋จ)
context.typographyโŒ ์‚ฌ์šฉ ๊ธˆ์ง€

Material 3 Semantic Styles#

GetterSize + Weight์šฉ๋„
displayLarge52px + w300๋ฉ”์ธ ๋””์Šคํ”Œ๋ ˆ์ด
displayMedium48px + w300๋””์Šคํ”Œ๋ ˆ์ด
displaySmall44px + w300์ž‘์€ ๋””์Šคํ”Œ๋ ˆ์ด
headlineLarge32px + w600ํฐ ํ—ค๋“œ๋ผ์ธ
headlineMedium28px + w600ํ—ค๋“œ๋ผ์ธ
headlineSmall24px + w600์ž‘์€ ํ—ค๋“œ๋ผ์ธ
titleLarge20px + w600ํŽ˜์ด์ง€ ํƒ€์ดํ‹€
titleMedium18px + w600์„น์…˜ ์ œ๋ชฉ
titleSmall16px + w600์†Œ์ œ๋ชฉ, ๊ฐ•์กฐ
bodyLarge18px + w400ํฐ ๋ณธ๋ฌธ
bodyMedium16px + w400๊ธฐ๋ณธ ๋ณธ๋ฌธ
bodySmall14px + w400์ž‘์€ ๋ณธ๋ฌธ
labelLarge14px + w400๋ผ๋ฒจ
labelMedium12px + w400์บก์…˜
labelSmall10px + w400๋ฏธ์„ธ ๋ผ๋ฒจ

Deprecated ๋งคํ•‘ํ‘œ#

๊ตฌ๋ฒ„์ „ (Tailwind ์กฐํ•ฉ)์‹ ๋ฒ„์ „ (M3 semantic)๋น„๊ณ 
lgSemiboldtitleMedium18px + w600 ์ •ํ™• ์ผ์น˜
baseSemiboldtitleSmall16px + w600 ์ •ํ™• ์ผ์น˜
smSemibold labelLarge.copyWith(fontWeight: .w600) weight ์ถ”๊ฐ€
baseBold titleSmall.copyWith(fontWeight: .w700) weight ์ถ”๊ฐ€
lgbodyMedium ๋˜๋Š” titleSmall16px
smlabelMedium12px
xslabelSmall10px
typography.semiBold FontWeight.w600 CoUI์—์„œ deprecated
typography.normal FontWeight.w400 CoUI์—์„œ deprecated

TextField features ํŒŒ๋ผ๋ฏธํ„ฐ#

โš ๏ธ deprecated: leading, trailing ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” deprecated๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

// โœ… ์ƒˆ๋กœ์šด API: features ํŒŒ๋ผ๋ฏธํ„ฐ
TextField(
  controller: controller,
  placeholder: const Text('์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'),
  filled: true,  // ๋ฐฐ๊ฒฝ์ƒ‰ ์ ์šฉ (surfaceContainer)
  features: [
    InputFeature.leading(const Icon(Icons.search, size: 20)),
    InputFeature.trailing(const Icon(Icons.clear, size: 18)),
  ],
  onChanged: (value) => ...,
)

// โŒ deprecated: leading/trailing ํŒŒ๋ผ๋ฏธํ„ฐ
TextField(
  leading: Icon(Icons.search),   // deprecated
  trailing: Icon(Icons.clear),   // deprecated
)

TextField ์ฃผ์š” ์†์„ฑ#

์†์„ฑํƒ€์ž…์„ค๋ช…
filled bool true๋ฉด ๋ฐฐ๊ฒฝ์ƒ‰(surfaceContainer) ์ ์šฉ
border Border ํ…Œ๋‘๋ฆฌ ์„ค์ • (Border.all(color: context.colorScheme.surfaceContainerHigh))
borderRadiusBorderRadiusํ…Œ๋‘๋ฆฌ ๋ฐ˜๊ฒฝ
features List leading, trailing, clear ๋“ฑ

ButtonSize & ButtonDensity#

ButtonSize ์˜ต์…˜#

โš ๏ธ ์ฃผ์˜: ButtonSize.medium์€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค!

์ƒ์ˆ˜์Šค์ผ€์ผ์šฉ๋„
ButtonSize.xSmall0.5x๋งค์šฐ ์ž‘์€ ๋ฒ„ํŠผ
ButtonSize.small0.75x์ž‘์€ ๋ฒ„ํŠผ
ButtonSize.normal1.0x๊ธฐ๋ณธ๊ฐ’
ButtonSize.large2.0xํฐ ๋ฒ„ํŠผ
ButtonSize.xLarge3.0x๋งค์šฐ ํฐ ๋ฒ„ํŠผ
// โœ… CORRECT: ์กด์žฌํ•˜๋Š” ํฌ๊ธฐ ์‚ฌ์šฉ
ButtonStyle.primary(size: .normal)
ButtonStyle.primary(size: .small)

// โŒ WRONG: medium ์—†์Œ (์ปดํŒŒ์ผ ์—๋Ÿฌ)
ButtonStyle.primary(size: .medium)  // ์—๋Ÿฌ!

ButtonDensity ์˜ต์…˜#

์ƒ์ˆ˜์šฉ๋„
ButtonDensity.normal๊ธฐ๋ณธ ๋ฐ€๋„
ButtonDensity.comfortable์—ฌ์œ ๋กœ์šด ํŒจ๋”ฉ
ButtonDensity.dense์ปดํŒฉํŠธํ•œ ๋ฒ„ํŠผ (ํ…Œ์ด๋ธ” ๋‚ด ๋ฒ„ํŠผ)
ButtonDensity.compact์ตœ์†Œ ํŒจ๋”ฉ
ButtonDensity.icon์•„์ด์ฝ˜ ์ „์šฉ
ButtonDensity.iconComfortable์•„์ด์ฝ˜ + ์—ฌ์œ  ํŒจ๋”ฉ
ButtonDensity.iconDense์•„์ด์ฝ˜ + ์ปดํŒฉํŠธ
// ํ…Œ์ด๋ธ” ๋‚ด ์ปดํŒฉํŠธํ•œ ๋ฒ„ํŠผ
CoButton(
  variant: CoreButtonVariant.outline,
  size: CoreComponentSize.sm,
  onPressed: handleTap,
  child: const Text('๋ณต์‚ฌ'),
)

Badge ์ปดํฌ๋„ŒํŠธ#

์ƒํƒœ ํ‘œ์‹œ์šฉ ๋ฐฐ์ง€ ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. CoBadge(variant:)๋กœ ์ข…๋ฅ˜๋ฅผ, size:๋กœ ํฌ๊ธฐ๋ฅผ ์ •ํ•ฉ๋‹ˆ๋‹ค.

Badge variant#

variant์šฉ๋„์ƒ‰์ƒ
CoreBadgeVariant.primary๊ธ์ •์  ์ƒํƒœ (ํŒ๋งค์ค‘, ํ™œ์„ฑ ๋“ฑ)primary
CoreBadgeVariant.destructive ๋ถ€์ •์  ์ƒํƒœ (์ค‘์ง€, ์‚ญ์ œ ๋“ฑ) destructive (red)

์‚ฌ์šฉ ์˜ˆ์‹œ#

// ๊ธฐ๋ณธ ์‚ฌ์šฉ
const CoBadge(variant: CoreBadgeVariant.primary, child: Text('ํŒ๋งค์ค‘'))
const CoBadge(variant: CoreBadgeVariant.destructive, child: Text('ํŒ๋งค ์ค‘์ง€'))

// size๋กœ ์ปดํŒฉํŠธํ•˜๊ฒŒ
const CoBadge(
  variant: CoreBadgeVariant.primary,
  size: CoreComponentSize.sm,
  child: Text('ํŒ๋งค์ค‘'),
)

const CoBadge(
  variant: CoreBadgeVariant.destructive,
  size: CoreComponentSize.sm,
  child: Text('ํŒ๋งค ์ค‘์ง€'),
)

// ์กฐ๊ฑด๋ถ€ ๋ฐฐ์ง€
child: book.isActive
    ? const CoBadge(
        variant: CoreBadgeVariant.primary,
        size: CoreComponentSize.sm,
        child: Text('ํŒ๋งค์ค‘'),
      )
    : const CoBadge(
        variant: CoreBadgeVariant.destructive,
        size: CoreComponentSize.sm,
        child: Text('ํŒ๋งค ์ค‘์ง€'),
      ),

DatePicker ์ปดํฌ๋„ŒํŠธ#

CoUI DatePicker๋Š” Material DatePicker์™€ ๋‹ค๋ฅธ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•#

DatePicker(
  mode: .dialog,                          // dialog ๋˜๋Š” inline
  dialogTitle: Text('๋‚ ์งœ ์„ ํƒ'),         // ๋‹ค์ด์–ผ๋กœ๊ทธ ์ œ๋ชฉ (ํ•„์ˆ˜ ๊ถŒ์žฅ)
  value: currentValue.value,              // ํ˜„์žฌ ์„ ํƒ๋œ ๋‚ ์งœ
  placeholder: Text(field.hint),          // ๋ฏธ์„ ํƒ ์‹œ ํ‘œ์‹œ ํ…์ŠคํŠธ
  stateBuilder: (date) { ... },           // ๋‚ ์งœ ์ƒํƒœ ๊ฒฐ์ • ํ•จ์ˆ˜
  onChanged: (selectedDate) { ... },      // ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ
)

์ฃผ์š” ํŒŒ๋ผ๋ฏธํ„ฐ#

ํŒŒ๋ผ๋ฏธํ„ฐํƒ€์ž…์„ค๋ช…
mode DatePickerMode .dialog (ํŒ์—…), .inline (์ธ๋ผ์ธ)
dialogTitleWidget?๋‹ค์ด์–ผ๋กœ๊ทธ ์ƒ๋‹จ ์ œ๋ชฉ
valueDateTime?ํ˜„์žฌ ์„ ํƒ๋œ ๋‚ ์งœ
placeholderWidget?๋ฏธ์„ ํƒ ์‹œ ํ‘œ์‹œํ•  ํ…์ŠคํŠธ
stateBuilder DateState Function(DateTime) ๋‚ ์งœ๋ณ„ ์ƒํƒœ ๊ฒฐ์ •
onChangedValueChanged<DateTime>?๋‚ ์งœ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ

stateBuilder๋กœ ๋‚ ์งœ ๋ฒ”์œ„ ์ œํ•œ#

stateBuilder: (date) {
  // 1900๋…„ ์ด์ „ ๋˜๋Š” 2100๋…„ ์ดํ›„ ๋‚ ์งœ ๋น„ํ™œ์„ฑํ™”
  if (date.isBefore(DateTime(1900)) ||
      date.isAfter(DateTime(2100, 12, 31))) {
    return DateState.disabled;
  }
  return DateState.enabled;
}

DateState ์˜ต์…˜#

์ƒํƒœ์„ค๋ช…
DateState.enabled์„ ํƒ ๊ฐ€๋Šฅ
DateState.disabled์„ ํƒ ๋ถˆ๊ฐ€ (ํšŒ์ƒ‰ ์ฒ˜๋ฆฌ)
DateState.selectedํ˜„์žฌ ์„ ํƒ๋œ ๋‚ ์งœ

FormField + CoUI ์œ„์ ฏ ํ†ตํ•ฉ ํŒจํ„ด#

CoUI ์œ„์ ฏ์„ Material FormField์™€ ํ†ตํ•ฉํ•˜์—ฌ ๊ฒ€์ฆ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

DatePicker + FormField ์˜ˆ์‹œ#

material.FormField<DateTime>(
  builder: (formFieldState) {
    return Column(
      crossAxisAlignment: .start,
      children: [
        Expanded(
          child: DatePicker(
            mode: .dialog,
            dialogTitle: Text(context.i10n.common.select_date),
            value: currentValue.value,
            placeholder: Text(field.hint),
            stateBuilder: (date) {
              if (date.isBefore(DateTime(1900)) ||
                  date.isAfter(DateTime(2100, 12, 31))) {
                return DateState.disabled;
              }
              return DateState.enabled;
            },
            onChanged: (selectedDate) {
              dateNotifier.value = selectedDate;
              formFieldState.didChange(selectedDate);  // โœ… ํ•„์ˆ˜: ๊ฒ€์ฆ ๋™๊ธฐํ™”
            },
          ),
        ),
        if (formFieldState.hasError) ...[
          const CoGap(size: CoreSpace.space4),
          Text(
            formFieldState.errorText!,
            style: FormInputStyles.errorTextStyle(context.appColors),
          ),
        ],
      ],
    );
  },
  validator: (value) {
    if (field.isRequired && value == null) {
      return context.i10n.common.date_required(label: field.label);
    }
    return null;
  },
)

ํ•ต์‹ฌ ํฌ์ธํŠธ#

ํ•ญ๋ชฉ์„ค๋ช…
formFieldState.didChange(value) ํ•„์ˆ˜ - FormField ๊ฒ€์ฆ ์ƒํƒœ ๋™๊ธฐํ™”
formFieldState.hasError์—๋Ÿฌ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
formFieldState.errorTextvalidator ๋ฐ˜ํ™˜ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
FormInputStyles.errorTextStyle()ํ”„๋กœ์ ํŠธ ํ‘œ์ค€ ์—๋Ÿฌ ํ…์ŠคํŠธ ์Šคํƒ€์ผ

๋™์  ๋†’์ด ๊ณ„์‚ฐ (์—๋Ÿฌ ์ƒํƒœ)#

์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ๋กœ ์ธํ•œ ๋†’์ด ๋ณ€ํ™”๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ๊ฒฝ์šฐ:

// ์—๋Ÿฌ ์ƒํƒœ์— ๋”ฐ๋ฅธ ๋†’์ด ๊ณ„์‚ฐ
final baseHeight = 40.0;
final errorHeight = formFieldState.hasError ? 20.0 : 0.0;
final totalHeight = baseHeight + errorHeight;

ValueNotifier + useEffect ์™ธ๋ถ€ ๋ณ€๊ฒฝ ๊ฐ์ง€#

@override
Widget build(BuildContext context) {
  final dateNotifier = useState<DateTime?>(initialValue);

  // ์™ธ๋ถ€์—์„œ ๊ฐ’์ด ๋ณ€๊ฒฝ๋˜๋ฉด ๋™๊ธฐํ™”
  useEffect(() {
    if (externalValue != dateNotifier.value) {
      dateNotifier.value = externalValue;
    }
    return null;
  }, [externalValue]);

  return DatePicker(
    value: dateNotifier.value,
    onChanged: (date) {
      dateNotifier.value = date;
      onExternalChange?.call(date);
    },
  );
}

CoGap & Insets ์ƒ์ˆ˜#

CoGap ์‚ฌ์šฉ๋ฒ•#

โš ๏ธ deprecated: Gap.sX(), Gap(double), gap ํŒจํ‚ค์ง€๋Š” ๋ชจ๋‘ deprecated. ์‹ ๊ทœ ์ฝ”๋“œ๋Š” ๋ฐ˜๋“œ์‹œ CoGap์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค (Epic #5256, #5125).

const CoGap(size: CoreSpace.space4)   // 4px
const CoGap(size: CoreSpace.space8)   // 8px
const CoGap(size: CoreSpace.space16)  // 16px
const CoGap(size: CoreSpace.space24)  // 24px
CoGap(size: CoreSpace.space16)        // Insets.medium ๋Œ€์ฒด

Gap โ†’ CoGap ๋งคํ•‘ํ‘œ#

๊ตฌ๋ฒ„์ „ (Gap)์‹ ๋ฒ„์ „ (CoGap)๊ฐ’
Gap.s1()CoGap(size: CoreSpace.space4)4px
Gap.s2()CoGap(size: CoreSpace.space8)8px
Gap.s3()CoGap(size: CoreSpace.space12)12px
Gap.s4()CoGap(size: CoreSpace.space16)16px
Gap.s6()CoGap(size: CoreSpace.space24)24px
Gap.s8()CoGap(size: CoreSpace.space32)32px
Gap(Insets.small) CoGap(size: CoreSpace.space8) 8px
Gap(Insets.medium) CoGap(size: CoreSpace.space16) 16px
Gap(Insets.large) CoGap(size: CoreSpace.space24) 24px

Insets ์ƒ์ˆ˜ ํ‘œ#

์‹ ๊ทœ ์ฝ”๋“œ๋Š” Insets.* ๋Œ€์‹  CoreSpace.spaceN์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค (CoUI ๋””์ž์ธ ํ† ํฐ๊ณผ ์ง์ ‘ ์ •ํ•ฉ).

Insets๊ฐ’CoreSpace ๋งคํ•‘์šฉ๋„
Insets.xSmall4pxCoreSpace.space4์•„์ด์ฝ˜ ๊ฐ„๊ฒฉ
Insets.small8pxCoreSpace.space8์š”์†Œ ๊ฐ„๊ฒฉ
Insets.medium 16px CoreSpace.space16 ์„น์…˜ ํŒจ๋”ฉ
Insets.large24pxCoreSpace.space24์นด๋“œ ํŒจ๋”ฉ
Insets.xLarge 32px CoreSpace.space32 ํŽ˜์ด์ง€ ๋งˆ์ง„

๋กœ๋”ฉ/์Šค์ผˆ๋ ˆํ†ค ํŒจํ„ด#

// ์Šค์ผˆ๋ ˆํ†ค ๋กœ๋”ฉ (์ „์ฒด ์œ„์ ฏ)
widget.skeletonizer(enabled: state.isLoading)

// ์กฐ๊ฑด๋ถ€ ๋กœ๋”ฉ (์Šคํ”ผ๋„ˆ)
widget.loadingOr(
  isLoading: state.isLoading,
  loadingWidget: const CircularProgressIndicator(),
)

// ๋นˆ ์ƒํƒœ ์ฒ˜๋ฆฌ
listWidget.emptyOrWhen(
  condition: () => list.isEmpty,
  emptyWidget: const EmptyStateWidget(),
)

useEffect ํƒญ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ํŒจํ„ด (HookWidget)#

ํƒญ ๊ธฐ๋ฐ˜ UI์—์„œ ๊ฐ ํƒญ์˜ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•  ๋•Œ useEffect๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ํŒจํ„ด#

class MyTabbedPage extends HookWidget {
  const MyTabbedPage({super.key});

  @override
  Widget build(BuildContext context) {
    final selectedTab = useState(_Tab.first);

    // โœ… ํƒญ ๋ณ€๊ฒฝ ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ
    useEffect(() {
      final bloc = context.read<MyBloc>();
      switch (selectedTab.value) {
        case _Tab.first:
          bloc.add(const MyEvent.loadFirstTab());
        case _Tab.second:
          bloc.add(const MyEvent.loadSecondTab());
        case _Tab.third:
          bloc.add(const MyEvent.loadThirdTab());
      }
      return null;  // cleanup ๋ถˆํ•„์š”
    }, [selectedTab.value]);  // ์˜์กด์„ฑ: ํƒญ ๋ณ€๊ฒฝ ์‹œ๋งˆ๋‹ค ์‹คํ–‰

    return TabBarView(
      selectedTab: selectedTab,
      // ...
    );
  }
}

// โœ… private enum์œผ๋กœ ํƒญ ์ •์˜ (๋งค์ง ๋„˜๋ฒ„ ๋ฐฉ์ง€)
enum _Tab { first, second, third }

์ฃผ์š” ํฌ์ธํŠธ#

ํ•ญ๋ชฉ์„ค๋ช…
useEffect ์˜์กด์„ฑ[selectedTab.value] - ํƒญ ๋ณ€๊ฒฝ ์‹œ๋งˆ๋‹ค ์‹คํ–‰
cleanup ๋ฐ˜ํ™˜return null; - ๋ฐ์ดํ„ฐ ๋กœ๋“œ๋Š” cleanup ๋ถˆํ•„์š”
BLoC ์ด๋ฒคํŠธ๊ฐ ํƒญ๋ณ„๋กœ ๋ณ„๋„์˜ ๋กœ๋“œ ์ด๋ฒคํŠธ ๋ฐœ์ƒ
enum ์‚ฌ์šฉprivate enum์œผ๋กœ ํƒญ ์ธ๋ฑ์Šค ๊ด€๋ฆฌ (๋งค์ง ๋„˜๋ฒ„ ๋ฐฉ์ง€)

์ดˆ๊ธฐ ๋กœ๋”ฉ๊ณผ ํƒญ ๋ณ€๊ฒฝ ๋กœ๋”ฉ ๋ถ„๋ฆฌ#

@override
Widget build(BuildContext context) {
  final selectedTab = useState(_Tab.first);
  final isInitialized = useState(false);

  // ์ดˆ๊ธฐ ๋กœ๋”ฉ (ํ•œ ๋ฒˆ๋งŒ)
  useEffect(() {
    context.read<MyBloc>().add(const MyEvent.initialize());
    isInitialized.value = true;
    return null;
  }, const []);  // ๋นˆ ์˜์กด์„ฑ = ์ตœ์ดˆ 1ํšŒ

  // ํƒญ ๋ณ€๊ฒฝ ์‹œ ๋กœ๋”ฉ (์ดˆ๊ธฐํ™” ํ›„์—๋งŒ)
  useEffect(() {
    if (!isInitialized.value) return null;

    final bloc = context.read<MyBloc>();
    switch (selectedTab.value) {
      case _Tab.first:
        bloc.add(const MyEvent.loadFirstTab());
      // ...
    }
    return null;
  }, [selectedTab.value]);

  // ...
}

Private Enum ํƒญ ๊ด€๋ฆฌ#

// โœ… CORRECT: private enum ์‚ฌ์šฉ
enum _Tab {
  publishers,   // ์ถœํŒ์‚ฌ ํƒญ
  authors,      // ์ €์ž ํƒญ
  contracts,    // ๊ณ„์•ฝ ํƒญ
}

// Widget์—์„œ ์‚ฌ์šฉ
final selectedTab = useState(_Tab.publishers);

// Switch expression์œผ๋กœ ํƒญ๋ณ„ ๋กœ์ง
switch (selectedTab.value) {
  case _Tab.publishers => publishersList,
  case _Tab.authors => authorsList,
  case _Tab.contracts => contractsList,
}

// โŒ WRONG: ๋งค์ง ๋„˜๋ฒ„ ์‚ฌ์šฉ
final selectedTab = useState(0);  // 0์ด ๋ญ์ง€?
if (selectedTab.value == 0) { ... }  // ๊ฐ€๋…์„ฑ ๋‚˜์จ

์™„์ „ํ•œ ํŽ˜์ด์ง€ ์˜ˆ์‹œ#

class MyPage extends HookWidget {
  const MyPage({super.key});

  @override
  Widget build(BuildContext context) {
    final appColors = context.appColors;

    return BlocProvider(
      create: (_) => getIt<MyBloc>(),
      child: BlocBuilder<MyBloc, MyState>(
        builder: (context, state) {
          return Scaffold(
            headers: [
              AppBar(
                title: Text(
                  'ํŽ˜์ด์ง€ ์ œ๋ชฉ',
                  style: context.textStyles.titleMedium.copyWith(
                    color: appColors.neutral5,
                  ),
                ),
              ),
            ],
            child: Padding(
              padding: const .symmetric(horizontal: Insets.medium),
              child: Column(
                crossAxisAlignment: .start,
                children: [
                  const CoGap(size: CoreSpace.space16),
                  TextField(
                    placeholder: const Text('์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”'),
                    features: const [
                      InputFeature.leading(Icon(Icons.person, size: 20)),
                    ],
                    onChanged: (value) => context.read<MyBloc>().add(
                      MyEvent.nameChanged(value),
                    ),
                  ),
                  const CoGap(size: CoreSpace.space16),
                  CoButton(
                    variant: CoreButtonVariant.primary,
                    expanded: true,
                    enabled: state.isValid,
                    onPressed: () => context.read<MyBloc>().add(
                      const MyEvent.submitted(),
                    ),
                    child: const Text('์ €์žฅ'),
                  ),
                ],
              ),
            ).skeletonizer(enabled: state.isLoading),
          );
        },
      ),
    );
  }
}

CoResizable ์ขŒ์šฐ ๋ถ„ํ•  ํŒจ๋„#

CoResizable ์€ ์ขŒ์šฐ(๋˜๋Š” ์ƒํ•˜) ๋ถ„ํ•  ํŒจ๋„์„ ๋“œ๋ž˜๊ทธ๋กœ ๋น„์œจ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค. ๋ทฐ์–ด ์‚ฌ์ด๋“œ ํŒจ๋„ ๋“ฑ ๋ฐ˜์‘ํ˜• ๋ ˆ์ด์•„์›ƒ์— ํ™œ์šฉ (Epic #5883, .claude/rules/viewer-side-panels.md ์ฐธ์กฐ).

CoResizable(
  // direction: .horizontal (๊ธฐ๋ณธ๊ฐ’), ์„ธ๋กœ ๋ถ„ํ• ์ด๋ฉด .vertical
  panes: [
    CoreResizablePaneData(
      initialSize: 0.65,    // ์ดˆ๊ธฐ ๋น„์œจ (ํ•ฉ์ด 1.0)
      minSize: 0.4,         // ์ตœ์†Œ ๋น„์œจ (์„ ํƒ)
      // maxSize: 0.8,      // ์ตœ๋Œ€ ๋น„์œจ (์„ ํƒ)
      content: leftWidget,
    ),
    CoreResizablePaneData(
      initialSize: 0.35,
      minSize: 0.25,
      content: rightWidget,
    ),
  ],
)

๋ฐ˜์‘ํ˜• ํŒจํ„ด (๋ทฐ์–ด ์‚ฌ์ด๋“œ ํŒจ๋„)#

LayoutBuilder + breakpoint ๋ถ„๊ธฐ๋กœ ์ข์€ ํ™”๋ฉด์—์„œ๋Š” ๋‹จ์ผ ํŒจ๋„, ๋„“์€ ํ™”๋ฉด์—์„œ๋Š” split ์ ์šฉ:

LayoutBuilder(
  builder: (context, constraints) {
    final isWide = constraints.maxWidth >= 768;
    if (!isWide || !isPanelOpen) return viewer;
    return CoResizable(
      panes: [
        CoreResizablePaneData(
          initialSize: 0.65,
          minSize: 0.4,
          content: viewer,
        ),
        CoreResizablePaneData(
          initialSize: 0.35,
          minSize: 0.25,
          content: panelHost,
        ),
      ],
    );
  },
)

์ฃผ์˜#

  • panes ์˜ initialSize ํ•ฉ์€ 1.0 ์ด ๋˜์–ด์•ผ ๊ท ํ˜•. CoUI ๊ฐ€ flexPrecision ์œผ๋กœ ์ •์ˆ˜ flex ๋ณ€ํ™˜.
  • CoresizableDirection ์€ .horizontal (๊ธฐ๋ณธ) ๋˜๋Š” .vertical.
  • ํŒจ๋„ ์ฝ˜ํ…์ธ ๋Š” ์ž์ฒด layout ์ •์ฑ…์— ๋”ฐ๋ฆ„ โ€” Expanded / SafeArea ๋“ฑ์€ ์ฝ˜ํ…์ธ  ์œ„์ ฏ์ด ์ง์ ‘ ๊ด€๋ฆฌ.

์ƒ‰์ƒ ์ฒด๊ณ„ (Console UI)#

AppColors neutral ์‹œ๋ฆฌ์ฆˆ#

์ƒ‰์ƒHEX์šฉ๋„
neutral100#FFFFFFํฐ์ƒ‰ ๋ฐฐ๊ฒฝ
neutral95#F9FAFBํ—ค๋”/์„น์…˜ ๋ฐฐ๊ฒฝ (๋ณธ๋ฌธ๊ณผ ๊ตฌ๋ถ„)
neutral85#F4F5F6๋ฐ์€ ํšŒ์ƒ‰
neutral80#ECEEEF๊ธฐ๋ณธ ํ…Œ๋‘๋ฆฌ
neutral70#B4B8C4์ง„ํ•œ ํ…Œ๋‘๋ฆฌ (์„ ๋ช…ํ•œ ๊ตฌ๋ถ„์„ )
neutral60#8F929A๋น„ํ™œ์„ฑ ํ…์ŠคํŠธ
neutral30-๋ณธ๋ฌธ ํ…์ŠคํŠธ
// ํ—ค๋” ๋ฐฐ๊ฒฝ (๋ณธ๋ฌธ๊ณผ ๊ตฌ๋ถ„)
color: context.appColors.neutral95,

// ์ง„ํ•œ ํ…Œ๋‘๋ฆฌ (์„ ๋ช…ํ•˜๊ฒŒ)
border: Border(
  right: .new(color: context.appColors.neutral70),
),

// ๊ธฐ๋ณธ ํ…Œ๋‘๋ฆฌ
border: Border.all(color: context.appColors.neutral80),

ColorScheme M3 ํ† ํฐ (Material 3 ํ‘œ์ค€)#

CoUI ์‹ ๊ทœ API๋Š” Material 3 ColorScheme ํ† ํฐ์„ ํ‘œ์ค€์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ƒ‰์ƒ์šฉ๋„
surface๊ฐ€์žฅ ๋ฐ์€ ๋ฐฐ๊ฒฝ
surfaceContainer์ค‘๊ฐ„ ๋ฐฐ๊ฒฝ (filled TextField)
surfaceContainerHigh๊ฐ€์žฅ ์ง„ํ•œ ๋ฐฐ๊ฒฝ/ํ…Œ๋‘๋ฆฌ
onSurfaceํ‘œ๋ฉด ์œ„ ํ…์ŠคํŠธ
onSurfaceVariant๋ณด์กฐ ํ…์ŠคํŠธ (๋ฐ˜ํˆฌ๋ช…)
outlineVariant์•ฝํ•œ ํ…Œ๋‘๋ฆฌ/๊ตฌ๋ถ„์„ 

โš ๏ธ Deprecated: base100/200/300, baseContent๋Š” CoUI ์‹ ๊ทœ API์—์„œ deprecated. M3 ํ† ํฐ ์‚ฌ์šฉ ๊ถŒ์žฅ.

// TextField ๋ฐฐ๊ฒฝ
TextField(
  filled: true,  // surfaceContainer ๋ฐฐ๊ฒฝ
  border: Border.all(color: context.colorScheme.surfaceContainerHigh),
)

// ํ•„ํ„ฐ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ…Œ๋‘๋ฆฌ
OutlinedContainer(
  backgroundColor: theme.colorScheme.surface,
  borderColor: theme.colorScheme.surfaceContainerHigh,  // ์ง„ํ•œ ํ…Œ๋‘๋ฆฌ
)

์ฐธ์กฐ ๋ฌธ์„œ#

๋‹ค์Œ ๋‚ด์šฉ์€ **CLAUDE.md**๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”:

  • ์ƒ‰์ƒ ๊ทœ์น™ (AppColors, context.appColors)
  • Dot Shorthand ๊ทœ์น™ (Dart 3.10+)
  • Button/IconButton ์‚ฌ์šฉ๋ฒ•
  • BLoC Event/State ํŒจํ„ด (sealed class)
  • BLoC async ํ•ธ๋“ค๋Ÿฌ (isClosed ์ฒดํฌ)
  • Context Extensions ์ „์ฒด ๋ชฉ๋ก