Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<script setup lang="ts">
const toast = useToast()

const selectedStatus = ref('In Progress')

const statusItems = [
{
label: 'Backlog',
icon: 'i-lucide-circle',
onClick: () => {
selectedStatus.value = 'Backlog'
toast.add({ title: 'Status changed to Backlog' })
}
},
{
label: 'Todo',
icon: 'i-lucide-circle-dot',
onClick: () => {
selectedStatus.value = 'Todo'
toast.add({ title: 'Status changed to Todo' })
}
},
{
label: 'In Progress',
icon: 'i-lucide-loader-circle',
onClick: () => {
selectedStatus.value = 'In Progress'
toast.add({ title: 'Status changed to In Progress' })
}
},
{
label: 'Done',
icon: 'i-lucide-circle-check',
onClick: () => {
selectedStatus.value = 'Done'
toast.add({ title: 'Status changed to Done' })
}
}
]

const groups = [
{
id: 'tasks',
label: 'Tasks',
items: [
{
label: 'Design new landing page',
suffix: 'High priority',
icon: 'i-lucide-layout',
chip: {
label: 'In Progress',
color: 'info' as const
}
},
{
label: 'Fix authentication bug',
suffix: 'Critical',
icon: 'i-lucide-bug',
chip: {
label: 'Todo',
color: 'warning' as const
}
},
{
label: 'Update documentation',
suffix: 'Medium priority',
icon: 'i-lucide-file-text',
chip: {
label: 'Backlog',
color: 'neutral' as const
}
},
{
label: 'Implement dark mode',
suffix: 'Low priority',
icon: 'i-lucide-moon',
chip: {
label: 'Done',
color: 'success' as const
}
},
{
label: 'Optimize performance',
suffix: 'High priority',
icon: 'i-lucide-zap',
chip: {
label: 'In Progress',
color: 'info' as const
}
},
{
label: 'Add unit tests',
suffix: 'Medium priority',
icon: 'i-lucide-test-tube',
chip: {
label: 'Todo',
color: 'warning' as const
}
}
]
}
]
</script>

<template>
<UCommandPalette :groups="groups" placeholder="Search tasks..." class="flex-1 h-80">
<template #actions>
<UDropdownMenu :items="statusItems">
<UButton :label="selectedStatus" color="neutral" variant="outline" trailing-icon="i-lucide-chevron-down" />
</UDropdownMenu>
</template>
</UCommandPalette>
</template>
14 changes: 14 additions & 0 deletions docs/content/docs/2.components/command-palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,20 @@ props:
You can also use the `#item`, `#item-leading`, `#item-label` and `#item-trailing` slots to customize all items.
::

### With actions slot

Use the `#actions` slot to add custom components in the input's trailing area, such as a [DropdownMenu](/docs/components/dropdown-menu) for filtering or status selection.

::component-example
---
collapse: true
name: 'command-palette-actions-slot-example'
class: '!p-0'
props:
autofocus: false
---
::

## API

### Props
Expand Down
7 changes: 7 additions & 0 deletions playgrounds/nuxt/app/pages/components/command-palette.vue
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ defineShortcuts({
<UCommandPalette
v-model="selected"
v-model:search-term="searchTerm"
children-icon="i-lucide-arrow-right"
:loading="status === 'pending'"
:groups="groups"
:fuse="{
Expand All @@ -196,6 +197,12 @@ defineShortcuts({
class="sm:max-h-96"
@update:model-value="onSelect"
>
<template #actions>
<UDropdownMenu :items="[{ label: 'Backlog' }, { label: 'Todo' }, { label: 'In Progress' }, { label: 'Done' }]">
<UButton label="In Progress" color="neutral" variant="outline" trailing-icon="i-lucide-chevron-down" />
</UDropdownMenu>
</template>

<template #footer>
<div class="flex items-center justify-between gap-2">
<UIcon name="i-simple-icons-nuxtdotjs" class="size-5 text-dimmed ml-1" />
Expand Down
24 changes: 18 additions & 6 deletions src/runtime/components/CommandPalette.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
children?: CommandPaletteItem[]
onSelect?(e?: Event): void
class?: any
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
ui?: Pick<CommandPalette['slots'], 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemLabel' | 'itemLabelPrefix' | 'itemLabelBase' | 'itemLabelSuffix' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon' | 'childrenIcon'>
[key: string]: any
}

Expand Down Expand Up @@ -70,6 +70,12 @@ export interface CommandPaletteProps<G extends CommandPaletteGroup<T> = CommandP
* @IconifyIcon
*/
icon?: IconProps['name']
/**
* The icon displayed on the right side of the input.
* @defaultValue appConfig.ui.icons.search
* @IconifyIcon
*/
trailingIcon?: IconProps['name']
/**
* The icon displayed when an item is selected.
* @defaultValue appConfig.ui.icons.check
Expand All @@ -81,7 +87,7 @@ export interface CommandPaletteProps<G extends CommandPaletteGroup<T> = CommandP
* @defaultValue appConfig.ui.icons.chevronRight
* @IconifyIcon
*/
trailingIcon?: IconProps['name']
childrenIcon?: IconProps['name']
/**
* The placeholder text for the input.
* @defaultValue t('commandPalette.placeholder')
Expand Down Expand Up @@ -167,6 +173,7 @@ export type CommandPaletteSlots<G extends CommandPaletteGroup<T> = CommandPalett
'empty'(props: { searchTerm?: string }): any
'footer'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'back'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'actions'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'close'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
'item': SlotProps<T>
'item-leading': SlotProps<T>
Expand Down Expand Up @@ -234,7 +241,8 @@ const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: C
})

const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.commandPalette || {}) })({
virtualize: !!props.virtualize
virtualize: !!props.virtualize,
close: !!(props.close || slots.close)
}))

const fuse = computed(() => defu({}, props.fuse, {
Expand Down Expand Up @@ -407,8 +415,8 @@ function onSelect(e: Event, item: T) {
<slot :name="((item.slot ? `${item.slot}-trailing` : group?.slot ? `${group.slot}-trailing` : `item-trailing`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<UIcon
v-if="item.children && item.children.length > 0"
:name="trailingIcon || appConfig.ui.icons.chevronRight"
:class="ui.itemTrailingIcon({ class: [props.ui?.itemTrailingIcon, item.ui?.itemTrailingIcon] })"
:name="childrenIcon || appConfig.ui.icons.chevronRight"
:class="ui.childrenIcon({ class: [props.ui?.childrenIcon, item.ui?.childrenIcon] })"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not keep using ui.itemTrailingIcon here? πŸ€”

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because in this case the icon only appears when the item has children, with itemTrailingIcon you'd think there'd be an icon for each item I think πŸ€”

/>

<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: [props.ui?.itemTrailingKbds, item.ui?.itemTrailingKbds] })">
Expand Down Expand Up @@ -454,7 +462,11 @@ function onSelect(e: Event, item: T) {
</slot>
</template>

<template v-if="close || !!slots.close" #trailing>
<template v-if="trailingIcon || close || !!slots.close || !!slots.actions" #trailing>
<UIcon v-if="trailingIcon" :name="trailingIcon" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the ui.trailingIcon from the UInput component here not from the CommandPalette. I'll make a PR to expose it (related to #4823)

<div v-if="!!slots.actions" :class="ui.actions({ class: props.ui?.actions })">
<slot name="actions" :ui="ui" />
</div>
<slot name="close" :ui="ui">
<UButton
v-if="close"
Expand Down
8 changes: 8 additions & 0 deletions src/theme/command-palette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ export default (options: Required<ModuleOptions>) => ({
slots: {
root: 'flex flex-col min-h-0 min-w-0 divide-y divide-default',
input: '[&>input]:h-12',
actions: '',
close: '',
back: 'p-0',
trailingIcon: 'shrink-0 size-5 text-dimmed',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be needed.

content: 'relative overflow-hidden flex flex-col',
footer: 'p-1',
viewport: 'relative scroll-py-1 overflow-y-auto flex-1 focus:outline-none',
Expand All @@ -19,6 +21,7 @@ export default (options: Required<ModuleOptions>) => ({
itemLeadingChip: 'shrink-0 size-5',
itemLeadingChipSize: 'md',
itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
childrenIcon: 'shrink-0 size-5',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be needed.

itemTrailingIcon: 'shrink-0 size-5',
itemTrailingHighlightedIcon: 'shrink-0 size-5 text-dimmed hidden group-data-highlighted:inline-flex',
itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0 gap-0.5',
Expand Down Expand Up @@ -51,6 +54,11 @@ export default (options: Required<ModuleOptions>) => ({
true: {
itemLeadingIcon: 'animate-spin'
}
},
close: {
true: {
actions: 'me-2'
}
}
}
})
3 changes: 3 additions & 0 deletions test/components/CommandPalette.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ describe('CommandPalette', () => {
['with placeholder', { props: { ...props, placeholder: 'Search...' } }],
['with disabled', { props: { ...props, disabled: true } }],
['with icon', { props: { ...props, icon: 'i-lucide-terminal' } }],
['with trailingIcon', { props: { ...props, inputTrailingIcon: 'i-lucide-settings' } }],
['with childrenIcon', { props: { ...props, childrenIcon: 'i-lucide-arrow-right' } }],
['with loading', { props: { ...props, loading: true } }],
['with loadingIcon', { props: { ...props, loading: true, loadingIcon: 'i-lucide-loader' } }],
['with selectedIcon', { props: { ...props, selectedIcon: 'i-lucide-badge-check', modelValue: groups[2]?.items[0] } }],
Expand All @@ -90,6 +92,7 @@ describe('CommandPalette', () => {
['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }],
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }],
['with custom slot', { props, slots: { custom: () => 'Custom slot' } }],
['with actions slot', { props, slots: { actions: () => 'Actions slot' } }],
['with close slot', { props: { ...props, close: true }, slots: { close: () => 'Close slot' } }],
['with footer slot', { props, slots: { footer: () => 'Footer slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: CommandPaletteProps, slots?: Partial<CommandPaletteSlots> }) => {
Expand Down
Loading
Loading