Command#
키보드 중심의 커맨드 팔레트 컴포넌트입니다. 빠른 탐색, 검색, 액션 실행에 사용됩니다. 검색 입력, 그룹화된 항목 목록, 키보드 힌트 푸터로 구성되며, query에 대해 클라이언트 측 필터링을 수행합니다.
Live Preview#
class CommandDefaultExample extends StatefulComponent {
const CommandDefaultExample({super.key});
@override
State<CommandDefaultExample> createState() => _CommandDefaultExampleState();
}
class _CommandDefaultExampleState extends State<CommandDefaultExample> {
String _query = '';
@override
Component build(BuildContext context) {
return Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: const [
CoreCommandGroup(
heading: 'Suggestions',
items: [
CoreCommandItem(
value: 'calendar',
label: 'Calendar',
icon: Icon(LucideIcons.calendar),
),
CoreCommandItem(
value: 'search',
label: 'Search Emoji',
icon: Icon(LucideIcons.smile),
),
CoreCommandItem(
value: 'calculator',
label: 'Calculator',
icon: Icon(LucideIcons.calculator),
),
],
),
CoreCommandGroup(
heading: 'Settings',
items: [
CoreCommandItem(
value: 'profile',
label: 'Profile',
icon: Icon(LucideIcons.user),
shortcut: '⌘P',
),
CoreCommandItem(
value: 'billing',
label: 'Billing',
icon: Icon(LucideIcons.creditCard),
shortcut: '⌘B',
),
],
),
],
);
}
}
class CommandDefaultExample extends StatefulWidget {
const CommandDefaultExample({super.key});
@override
State<CommandDefaultExample> createState() => _CommandDefaultExampleState();
}
class _CommandDefaultExampleState extends State<CommandDefaultExample> {
String _query = '';
@override
Widget build(BuildContext context) {
return Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: const [
CoreCommandGroup(
heading: 'Suggestions',
items: [
CoreCommandItem(
value: 'calendar',
label: 'Calendar',
icon: Icon(LucideIcons.calendar),
),
CoreCommandItem(
value: 'search',
label: 'Search Emoji',
icon: Icon(LucideIcons.smile),
),
CoreCommandItem(
value: 'calculator',
label: 'Calculator',
icon: Icon(LucideIcons.calculator),
),
],
),
CoreCommandGroup(
heading: 'Settings',
items: [
CoreCommandItem(
value: 'profile',
label: 'Profile',
icon: Icon(LucideIcons.user),
shortcut: '⌘P',
),
CoreCommandItem(
value: 'billing',
label: 'Billing',
icon: Icon(LucideIcons.creditCard),
shortcut: '⌘B',
),
],
),
],
);
}
}
class CommandChainExample extends StatefulComponent {
const CommandChainExample({super.key});
@override
State<CommandChainExample> createState() => _CommandChainExampleState();
}
class _CommandChainExampleState extends State<CommandChainExample> {
String _query = '';
static const List<CoreCommandGroup<Component>> _groups = [
CoreCommandGroup(
heading: 'Suggestions',
items: [
CoreCommandItem(
value: 'calendar',
label: 'Calendar',
icon: Icon(LucideIcons.calendar),
),
CoreCommandItem(
value: 'search',
label: 'Search Emoji',
icon: Icon(LucideIcons.smile),
),
CoreCommandItem(
value: 'calculator',
label: 'Calculator',
icon: Icon(LucideIcons.calculator),
),
],
),
CoreCommandGroup(
heading: 'Settings',
items: [
CoreCommandItem(
value: 'profile',
label: 'Profile',
icon: Icon(LucideIcons.user),
shortcut: '⌘P',
),
CoreCommandItem(
value: 'billing',
label: 'Billing',
icon: Icon(LucideIcons.creditCard),
shortcut: '⌘B',
),
],
),
];
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: _groups,
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(searchPadding)까지 한 번에.
Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: _groups,
).withStyle(
const CoreCommandStyle(
backgroundColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
itemBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
searchPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
vertical: CoreSpace.space8,
),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
class CommandChainExample extends StatefulWidget {
const CommandChainExample({super.key});
@override
State<CommandChainExample> createState() => _CommandChainExampleState();
}
class _CommandChainExampleState extends State<CommandChainExample> {
String _query = '';
static const List<CoreCommandGroup<Widget>> _groups = [
CoreCommandGroup(
heading: 'Suggestions',
items: [
CoreCommandItem(
value: 'calendar',
label: 'Calendar',
icon: Icon(LucideIcons.calendar),
),
CoreCommandItem(
value: 'search',
label: 'Search Emoji',
icon: Icon(LucideIcons.smile),
),
CoreCommandItem(
value: 'calculator',
label: 'Calculator',
icon: Icon(LucideIcons.calculator),
),
],
),
CoreCommandGroup(
heading: 'Settings',
items: [
CoreCommandItem(
value: 'profile',
label: 'Profile',
icon: Icon(LucideIcons.user),
shortcut: '⌘P',
),
CoreCommandItem(
value: 'billing',
label: 'Billing',
icon: Icon(LucideIcons.creditCard),
shortcut: '⌘B',
),
],
),
];
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: _groups,
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(searchPadding)까지 한 번에.
Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: _groups,
).withStyle(
const CoreCommandStyle(
backgroundColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
itemBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
searchPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
vertical: CoreSpace.space8,
),
),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 앱 전체의 기능이나 페이지를 빠르게 탐색하는 커맨드 팔레트가 필요할 때
- 많은 수의 항목 중 키보드 입력으로 필터링하여 선택해야 할 때
- 개발자 도구나 고급 사용자를 위한 단축키 중심 인터페이스를 구현할 때
- 검색어 기반으로 그룹화된 액션 목록을 제공할 때
대신 다른 컴포넌트를 사용하세요:
Select: 정해진 목록에서 단순히 하나를 선택할 때DropdownMenu: 컨텍스트 메뉴나 단순한 옵션 선택이 필요할 때TextField: 자유 입력 텍스트 필드만 필요할 때
기본 사용법 (Basic Usage)#
Flutter와 Web 모두 동일한 Command API를 사용합니다. groups로 그룹화된 항목을 선언하고, controlled query/onQueryChanged로 검색어를 관리합니다.
Command(
query: query,
onQueryChanged: (q) => setState(() => query = q),
onSelect: (value) => runCommand(value),
groups: const [
CoreCommandGroup(
heading: 'Suggestions',
items: [
CoreCommandItem(value: 'calendar', label: 'Calendar'),
CoreCommandItem(value: 'search', label: 'Search Emoji'),
CoreCommandItem(value: 'calculator', label: 'Calculator'),
],
),
CoreCommandGroup(
heading: 'Settings',
items: [
CoreCommandItem(value: 'profile', label: 'Profile', shortcut: '⌘P'),
CoreCommandItem(value: 'billing', label: 'Billing', shortcut: '⌘B'),
],
),
],
)
빠른 오버라이드 (Chain)#
이미 만든 Command 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius24처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius24 ==
CoreRadius.radius24) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class CommandChainExample extends StatefulWidget {
const CommandChainExample({super.key});
@override
State<CommandChainExample> createState() => _CommandChainExampleState();
}
class _CommandChainExampleState extends State<CommandChainExample> {
String _query = '';
static const List<CoreCommandGroup<Widget>> _groups = [
CoreCommandGroup(
heading: 'Suggestions',
items: [
CoreCommandItem(
value: 'calendar',
label: 'Calendar',
icon: Icon(LucideIcons.calendar),
),
CoreCommandItem(
value: 'search',
label: 'Search Emoji',
icon: Icon(LucideIcons.smile),
),
CoreCommandItem(
value: 'calculator',
label: 'Calculator',
icon: Icon(LucideIcons.calculator),
),
],
),
CoreCommandGroup(
heading: 'Settings',
items: [
CoreCommandItem(
value: 'profile',
label: 'Profile',
icon: Icon(LucideIcons.user),
shortcut: '⌘P',
),
CoreCommandItem(
value: 'billing',
label: 'Billing',
icon: Icon(LucideIcons.creditCard),
shortcut: '⌘B',
),
],
),
];
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: _groups,
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(searchPadding)까지 한 번에.
Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: _groups,
).withStyle(
const CoreCommandStyle(
backgroundColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
itemBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
searchPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
vertical: CoreSpace.space8,
),
),
),
],
);
}
}
class CommandChainExample extends StatefulComponent {
const CommandChainExample({super.key});
@override
State<CommandChainExample> createState() => _CommandChainExampleState();
}
class _CommandChainExampleState extends State<CommandChainExample> {
String _query = '';
static const List<CoreCommandGroup<Component>> _groups = [
CoreCommandGroup(
heading: 'Suggestions',
items: [
CoreCommandItem(
value: 'calendar',
label: 'Calendar',
icon: Icon(LucideIcons.calendar),
),
CoreCommandItem(
value: 'search',
label: 'Search Emoji',
icon: Icon(LucideIcons.smile),
),
CoreCommandItem(
value: 'calculator',
label: 'Calculator',
icon: Icon(LucideIcons.calculator),
),
],
),
CoreCommandGroup(
heading: 'Settings',
items: [
CoreCommandItem(
value: 'profile',
label: 'Profile',
icon: Icon(LucideIcons.user),
shortcut: '⌘P',
),
CoreCommandItem(
value: 'billing',
label: 'Billing',
icon: Icon(LucideIcons.creditCard),
shortcut: '⌘B',
),
],
),
];
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: _groups,
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(searchPadding)까지 한 번에.
Command(
query: _query,
onQueryChanged: (q) => setState(() => _query = q),
onSelect: (_) {},
groups: _groups,
).withStyle(
const CoreCommandStyle(
backgroundColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
itemBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
searchPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
vertical: CoreSpace.space8,
),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
Props / Parameters#
Command (Flutter / Web 동일)#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
groups |
List<CoreCommandGroup> |
필수 | 표시할 커맨드 그룹 목록 |
variant |
CoreCommandVariant |
standard |
시각 variant |
placeholder |
String |
'Type a command or search...' |
검색 입력 플레이스홀더 |
query |
String |
'' |
현재 검색어 (controlled) |
emptyLabel |
String |
'No results found.' |
검색 결과 없을 때 메시지 |
autofocus |
bool |
true |
검색 입력 자동 포커스 여부 |
onQueryChanged |
void Function(String)? |
null |
검색어 변경 핸들러 |
onSelect |
void Function(String)? |
null |
항목 선택 시 value 콜백 |
commandStyle |
CoreCommandStyle? |
null |
팔레트 / 항목 chrome 스타일 슬롯 |
CoreCommandGroup#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
heading |
String? |
null |
그룹 헤딩 텍스트 |
items |
List<CoreCommandItem> |
필수 | 그룹 내 항목 목록 |
CoreCommandItem#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | String | 필수 | 항목 고유 식별자 |
label | String | 필수 | 표시 텍스트 (검색 필터 대상) |
icon |
Widget? / Component? |
null |
선택적 leading 아이콘 |
shortcut |
String? |
null |
키보드 단축키 표시 |
enabled |
bool |
true |
활성화 여부 (비활성 시 키보드 탐색 skip) |
스타일 시스템 (Style System)#
CoreCommandStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor |
CoreColor? |
Palette background fill colour override. |
borderColor |
CoreColor? |
Palette border stroke colour override. |
borderRadius |
CoreBorderRadius? |
Palette corner radius override. |
borderWidth |
double? |
Palette / divider border stroke width override (logical px). |
searchPadding |
CoreEdgeInsets? |
Search row padding override. |
searchGapStyle |
CoreGapStyle? |
Nested gap style between search-row leading icon and the
<input>
— forwarded to
Gap(gapStyle: …)
.
|
searchIconStyle |
CoreIconStyle? |
Search leading icon style override — forwarded straight to
Icon(iconStyle: …)
by the resolver.
|
searchInputHeight |
double? |
Search input row height override (logical px). |
listPadding |
CoreEdgeInsets? |
Item-list outer padding override. |
headingPadding |
CoreEdgeInsets? |
Group-heading padding override. |
itemPadding |
CoreEdgeInsets? |
Item inner padding override. |
itemBorderRadius |
CoreBorderRadius? |
Item corner radius override. |
itemGapStyle |
CoreGapStyle? |
Nested gap style between an item's icon ↔ label and label ↔ shortcut — forwarded to
Gap(gapStyle: …)
.
|
itemIconStyle |
CoreIconStyle? |
Per-item icon style override (nested slot — ambient-forwarded to caller-supplied
item.icon
widgets).
|
itemHighlightColor |
CoreColor? |
Item highlight (hover / keyboard-focus) background override. |
itemTextStyle |
CoreTextStyle? |
Item label text style override. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
itemForegroundColor
field removed). Defaults to [defaultItemTextStyle].
|
itemForegroundColor |
CoreColor? |
Foreground colour for an item in its resting (not-highlighted) state — label text and the ambient icon tint. The highlighted cell's foreground comes from [itemTextStyle]'s colour (painted together with [itemHighlightColor]); this field owns the idle state so a light theme doesn't paint on-accent (near-white) text on the plain panel background. |
itemClickableStyle |
CoreClickableStyle? |
Nested style for the composed per-item
Clickable
— carries any press / focus-ring / cursor chrome overrides for the item affordance. Merged on top of [defaultItemClickableStyle] and raw-forwarded to
Clickable(clickableStyle:)
.
|
highlightDuration |
Duration? |
Item highlight transition duration override. |
footerPadding |
CoreEdgeInsets? |
Footer padding override. |
footerSpacing |
double? |
Spacing between footer keyboard-hint groups (logical px) — flows into native
Row.spacing
(Flutter) / flex
gap
(Web), so the spacer renders via the native flexbox API.
|
emptyPadding |
CoreEdgeInsets? |
Empty-state padding override. |
disabledAlpha |
double? |
Disabled item content alpha override (0..1). |
listMaxHeight |
double? |
Results-list scroll-container max height override (logical px). |
kbdStyle |
CoreKbdStyle? |
Footer keyboard-hint
Kbd
style override (nested slot, merged on top of [defaultKbdStyle] and raw-forwarded to the composed unified
Kbd
— its own resolver fills the remaining defaults).
|
footerKbdIconStyle |
CoreIconStyle? |
Footer keyboard-hint icon style override (nested slot — forwarded to the
Icon
glyphs inside the footer
Kbd
chips).
|
labelStyle |
CoreTextStyle? |
Item label text style override. |
headingStyle |
CoreTextStyle? |
Group heading text style override. |
shortcutStyle |
CoreTextStyle? |
Item-shortcut text style override. null → [defaultShortcutStyle]. |
emptyStyle |
CoreTextStyle? |
Empty-state text style override. null → [defaultEmptyStyle]. |
footerStyle |
CoreTextStyle? |
Footer keyboard-hint label text style override. null → [defaultFooterStyle]. |
footerBackgroundColor |
CoreColor? |
Footer bar background colour override. null → [defaultFooterBackgroundColor]. |
placeholderColor |
CoreColor? |
Search
<input>
placeholder text colour override.
null
→ [defaultPlaceholderColor].
|
동작 스펙 (Behavior)#
인터랙션#
- 타이핑: 입력 즉시 실시간 퍼지(fuzzy) 검색으로 항목 필터링
- 키보드 탐색:
↑/↓화살표로 항목 이동,Enter로 선택 - 마우스 호버: 항목 하이라이트, 클릭으로 선택
상태 전환#
- 입력창 포커스 → 목록 표시
- 검색어 입력 → 실시간 필터링
- 항목 선택 →
onSelect콜백 실행 → 닫기 - 빈 결과 →
emptyMessage표시
애니메이션#
- 목록 열기/닫기: 150ms fade + slide
- 항목 하이라이트 전환: 즉시 (인스턴트)
- 필터링 결과 변경: 100ms ease
사용 가이드라인 (Usage Guidelines)#
✅ Do#
Dialog와 함께 사용하여 전역 커맨드 팔레트 구현
// Ctrl+K 단축키로 커맨드 팔레트 열기
Dialog(
isOpen: isCommandOpen,
onClose: handleCloseCommand,
child: Command(
placeholder: '명령어 또는 페이지 검색...',
query: query,
onQueryChanged: handleQueryChanged,
groups: appCommandGroups,
),
)
커맨드 팔레트는 일반적으로 전체 화면 오버레이로 표시되어 빠른 접근성을 제공합니다.
❌ Don't#
너무 많은 그룹과 항목을 평탄하게 나열 금지
// ❌ 그룹 없이 50개 항목을 모두 나열
Command(
groups: [
CoreCommandGroup(
items: allFiftyItems,
),
],
)
그룹 없이 많은 항목을 나열하면 탐색이 어렵습니다. 관련 항목을 의미 있는 그룹으로 분류하세요.
✅ Do#
단축키를 shortcut prop으로 표시
CoreCommandItem(
value: 'save',
label: '저장',
shortcut: 'Ctrl+S',
)
커맨드 팔레트에서 단축키를 보여주면 사용자가 점차 단축키를 익혀 생산성을 높일 수 있습니다.
❌ Don't#
파괴적인 액션을 확인 없이 즉시 실행 금지
// ❌ 삭제 액션을 바로 실행
Command(
onSelect: handleDeleteAll, // 'delete-all' 선택 시 확인 없이 즉시 삭제
groups: const [
CoreCommandGroup(
items: [CoreCommandItem(value: 'delete-all', label: '모두 삭제')],
),
],
)
커맨드 팔레트에서 파괴적인 액션을 선택하면 실수로 실행될 수 있습니다. 확인 다이얼로그를 거치도록 구현하세요.
✅ Do#
자주 사용하는 명령어를 상단에 배치하세요.
Command(
groups: [
CoreCommandGroup(
heading: '최근 사용',
items: recentCommands, // 자주 쓰는 항목 우선
),
CoreCommandGroup(
heading: '전체 명령어',
items: allCommands,
),
],
)
사용 빈도 높은 명령어를 상단에 배치하면 사용자가 빠르게 접근하여 생산성을 높일 수 있습니다.
❌ Don't#
Command 팔레트를 일반 폼 입력으로 사용하지 마세요.
// ❌ Command를 단순 검색 입력으로 대체
Command(
onSelect: handleSearchResult, // 검색 전용으로만 사용
groups: [CoreCommandGroup(items: searchResults)],
)
Command 팔레트는 단축키(⌘K)로 접근하는 글로벌 액션 도구입니다. 단순 검색에는 Input 또는 AutoComplete를 사용하세요.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
↑ / ↓ | 이전/다음 항목으로 이동 |
Enter | 선택된 항목 실행 |
Escape | 커맨드 팔레트 닫기 |
Tab | 다음 포커스 가능 요소로 이동 |
스크린 리더#
- Flutter:
Semantics위젯으로combobox역할 전달, 활성 항목 상태 알림 -
Web:
role="combobox",aria-expanded,aria-activedescendant자동 적용
터치 타겟#
- 각 CommandItem의 최소 높이: 48px
크로스 플랫폼 차이점 (Platform Differences)#
CommandAPI는 Flutter / Web에서 완전히 동일합니다. 아래는 내부 구현 차이점만 나열합니다.
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Command | Command |
| 검색 방식 | Dart 문자열 매칭 (대소문자 무시 substring) | JS 문자열 매칭 (동일 로직) |
| 키보드 탐색 | Focus + KeyEvent | keydown 이벤트 |
관련 컴포넌트 (Related Components)#
- Kbd: CommandItem의
shortcut표시에 함께 사용 - Dialog: 전역 커맨드 팔레트를 오버레이로 표시할 때 사용
- TextField: 단순 텍스트 검색만 필요할 때 대안
조합 예제#
// Command + Dialog 조합으로 전역 커맨드 팔레트 구현
Dialog(
isOpen: isOpen,
onClose: handleClose,
child: Command(
placeholder: '명령어를 입력하세요...',
query: query,
onQueryChanged: handleQueryChanged,
onSelect: handleCommandSelect,
groups: [
CoreCommandGroup(
heading: '최근 항목',
items: recentItems
.map((item) => CoreCommandItem(
value: item.id,
label: item.title,
))
.toList(),
),
CoreCommandGroup(
heading: '설정',
items: const [
CoreCommandItem(
value: 'theme',
label: '테마 변경',
shortcut: 'Ctrl+T',
),
],
),
],
),
)