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
5 changes: 5 additions & 0 deletions playground/app/pages/components/input-menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,23 @@ const selectedItems = ref([fruits[0]!, vegetables[0]!])

const statuses = [{
label: 'Backlog',
description: 'Issues that have been identified but not yet prioritized',
icon: 'i-lucide-circle-help'
}, {
label: 'Todo',
description: 'Issues that are ready to be worked on',
icon: 'i-lucide-circle-plus'
}, {
label: 'In Progress',
description: 'Issues that are currently being worked on',
icon: 'i-lucide-circle-arrow-up'
}, {
label: 'Done',
description: 'Issues that have been completed successfully',
icon: 'i-lucide-circle-check'
}, {
label: 'Canceled',
description: 'Issues that have been cancelled or rejected',
icon: 'i-lucide-circle-x'
}] satisfies InputMenuItem[]

Expand Down
5 changes: 5 additions & 0 deletions playground/app/pages/components/select-menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,27 @@ const selectedItems = ref([fruits[0]!, vegetables[0]!])
const statuses = [{
label: 'Backlog',
value: 'backlog',
description: 'Issues that have been identified but not yet prioritized',
icon: 'i-lucide-circle-help'
}, {
label: 'Todo',
value: 'todo',
description: 'Issues that are ready to be worked on',
icon: 'i-lucide-circle-plus'
}, {
label: 'In Progress',
value: 'in_progress',
description: 'Issues that are currently being worked on',
icon: 'i-lucide-circle-arrow-up'
}, {
label: 'Done',
value: 'done',
description: 'Issues that have been completed',
icon: 'i-lucide-circle-check'
}, {
label: 'Canceled',
value: 'canceled',
description: 'Issues that have been canceled or rejected',
icon: 'i-lucide-circle-x'
}] satisfies SelectMenuItem[]

Expand Down
5 changes: 5 additions & 0 deletions playground/app/pages/components/select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,27 @@ const selectedItems = ref([fruits[0]!, vegetables[0]!])
const statuses = [{
label: 'Backlog',
value: 'backlog',
description: 'Issues that have been identified but not yet prioritized',
icon: 'i-lucide-circle-help'
}, {
label: 'Todo',
value: 'todo',
description: 'Issues that are ready to be worked on',
icon: 'i-lucide-circle-plus'
}, {
label: 'In Progress',
value: 'in_progress',
description: 'Issues that are currently being worked on',
icon: 'i-lucide-circle-arrow-up'
}, {
label: 'Done',
value: 'done',
description: 'Issues that have been completed',
icon: 'i-lucide-circle-check'
}, {
label: 'Canceled',
value: 'canceled',
description: 'Issues that have been canceled or rejected',
icon: 'i-lucide-circle-x'
}] satisfies SelectItem[]

Expand Down
35 changes: 25 additions & 10 deletions src/runtime/components/CommandPalette.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface CommandPaletteItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
prefix?: string
label?: string
suffix?: string
description?: string
/**
* @IconifyIcon
*/
Expand All @@ -33,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' | 'itemDescription' | 'itemContent' | 'itemTrailing' | 'itemTrailingKbds' | 'itemTrailingKbdsSize' | 'itemTrailingHighlightedIcon' | 'itemTrailingIcon'>
[key: string]: any
}

Expand Down Expand Up @@ -135,6 +136,11 @@ export interface CommandPaletteProps<G extends CommandPaletteGroup<T> = CommandP
* @defaultValue 'label'
*/
labelKey?: string
/**
* The key used to get the description from the item.
* @defaultValue 'description'
*/
descriptionKey?: string
class?: any
ui?: CommandPalette['slots']
}
Expand All @@ -153,6 +159,7 @@ export type CommandPaletteSlots<G extends CommandPaletteGroup<T> = CommandPalett
'item': SlotProps<T>
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
'item-description': SlotProps<T>
'item-trailing': SlotProps<T>
} & Record<string, SlotProps<G>> & Record<string, SlotProps<T>>

Expand Down Expand Up @@ -182,6 +189,7 @@ import UKbd from './Kbd.vue'
const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
modelValue: '',
labelKey: 'label',
descriptionKey: 'description',
autofocus: true,
back: true
})
Expand Down Expand Up @@ -403,15 +411,22 @@ function onSelect(e: Event, item: T) {
/>
</slot>

<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>]" :class="ui.itemLabel({ class: [props.ui?.itemLabel, item.ui?.itemLabel], active: active || item.active })">
<slot :name="((item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: [props.ui?.itemLabelPrefix, item.ui?.itemLabelPrefix] })">{{ item.prefix }}</span>

<span :class="ui.itemLabelBase({ class: [props.ui?.itemLabelBase, item.ui?.itemLabelBase], active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />

<span :class="ui.itemLabelSuffix({ class: [props.ui?.itemLabelSuffix, item.ui?.itemLabelSuffix], active: active || item.active })" v-html="item.suffixHtml || item.suffix" />
</slot>
</span>
<div :class="ui.itemContent({ class: [props.ui?.itemContent, item.ui?.itemContent] })">
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>]" :class="ui.itemLabel({ class: [props.ui?.itemLabel, item.ui?.itemLabel], active: active || item.active })">
<slot :name="((item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: [props.ui?.itemLabelPrefix, item.ui?.itemLabelPrefix] })">{{ item.prefix }}</span>

<span :class="ui.itemLabelBase({ class: [props.ui?.itemLabelBase, item.ui?.itemLabelBase], active: active || item.active })" v-html="item.labelHtml || get(item, props.labelKey as string)" />

<span :class="ui.itemLabelSuffix({ class: [props.ui?.itemLabelSuffix, item.ui?.itemLabelSuffix], active: active || item.active })" v-html="item.suffixHtml || item.suffix" />
</slot>
</span>
<div v-if="get(item, props.descriptionKey as string)" :class="ui.itemDescription({ class: [props.ui?.itemDescription, item.ui?.itemDescription] })">
<slot :name="((item.slot ? `${item.slot}-description` : group.slot ? `${group.slot}-description` : `item-description`) as keyof CommandPaletteSlots<G, T>)" :item="(item as any)" :index="index">
{{ get(item, props.descriptionKey as string) }}
</slot>
</div>
</div>

<span :class="ui.itemTrailing({ class: [props.ui?.itemTrailing, item.ui?.itemTrailing] })">
<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">
Expand Down
13 changes: 11 additions & 2 deletions src/runtime/components/ContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type ContextMenu = ComponentConfig<typeof theme, AppConfig, 'contextMenu'>

export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
label?: string
description?: string
/**
* @IconifyIcon
*/
Expand All @@ -33,7 +34,7 @@ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custo
onSelect?(e: Event): void
onUpdateChecked?(checked: boolean): void
class?: any
ui?: Pick<ContextMenu['slots'], 'item' | 'label' | 'separator' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLabel' | 'itemLabelExternalIcon' | 'itemTrailing' | 'itemTrailingIcon' | 'itemTrailingKbds' | 'itemTrailingKbdsSize'>
ui?: Pick<ContextMenu['slots'], 'item' | 'label' | 'separator' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLabel' | 'itemLabelExternalIcon' | 'itemDescription' | 'itemContent' | 'itemTrailing' | 'itemTrailingIcon' | 'itemTrailingKbds' | 'itemTrailingKbdsSize'>
[key: string]: any
}

Expand Down Expand Up @@ -74,6 +75,11 @@ export interface ContextMenuProps<T extends ArrayOrNested<ContextMenuItem> = Arr
* @defaultValue 'label'
*/
labelKey?: keyof NestedItem<T>
/**
* The key used to get the description from the item.
* @defaultValue 'description'
*/
descriptionKey?: keyof NestedItem<T>
disabled?: boolean
class?: any
ui?: ContextMenu['slots']
Expand All @@ -91,6 +97,7 @@ export type ContextMenuSlots<
'item': SlotProps<T>
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
'item-description': SlotProps<T>
'item-trailing': SlotProps<T>
'content-top': (props?: {}) => any
'content-bottom': (props?: {}) => any
Expand All @@ -111,7 +118,8 @@ const props = withDefaults(defineProps<ContextMenuProps<T>>(), {
portal: true,
modal: true,
externalIcon: true,
labelKey: 'label'
labelKey: 'label',
descriptionKey: 'description'
})
const emits = defineEmits<ContextMenuEmits>()
const slots = defineSlots<ContextMenuSlots<T>>()
Expand Down Expand Up @@ -141,6 +149,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.contextMenu
:items="items"
:portal="portal"
:label-key="(labelKey as keyof NestedItem<T>)"
:description-key="(descriptionKey as keyof NestedItem<T>)"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
Expand Down
25 changes: 17 additions & 8 deletions src/runtime/components/ContextMenuContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface ContextMenuContentProps<T extends ArrayOrNested<ContextMenuItem>> exte
portal?: boolean | string | HTMLElement
sub?: boolean
labelKey: keyof NestedItem<T>
descriptionKey: keyof NestedItem<T>
/**
* @IconifyIcon
*/
Expand Down Expand Up @@ -57,7 +58,7 @@ const { dir } = useLocale()
const appConfig = useAppConfig()

const portalProps = usePortal(toRef(() => props.portal))
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'descriptionKey', 'checkedIcon', 'loadingIcon', 'externalIcon', 'class', 'ui', 'uiOverride'), emits)
const proxySlots = omit(slots, ['default'])

const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>()
Expand All @@ -81,13 +82,20 @@ const groups = computed<ContextMenuItem[][]>(() =>
<UAvatar v-else-if="item.avatar" :size="((item.ui?.itemLeadingAvatarSize || uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: [uiOverride?.itemLeadingAvatar, item.ui?.itemLeadingAvatar], active })" />
</slot>

<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: [uiOverride?.itemLabel, item.ui?.itemLabel], active })">
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>

<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: [uiOverride?.itemLabelExternalIcon, item.ui?.itemLabelExternalIcon], color: item?.color, active })" />
</span>
<div :class="ui.itemContent({ class: [uiOverride?.itemContent, item.ui?.itemContent] })">
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: [uiOverride?.itemLabel, item.ui?.itemLabel], active })">
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>

<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: [uiOverride?.itemLabelExternalIcon, item.ui?.itemLabelExternalIcon], color: item?.color, active })" />
</span>
<div v-if="get(item, props.descriptionKey as string)" :class="ui.itemDescription({ class: [uiOverride?.itemDescription, item.ui?.itemDescription] })">
<slot :name="((item.slot ? `${item.slot}-description`: 'item-description') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
{{ get(item, props.descriptionKey as string) }}
</slot>
</div>
</div>

<span :class="ui.itemTrailing({ class: [uiOverride?.itemTrailing, item.ui?.itemTrailing] })">
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
Expand Down Expand Up @@ -135,6 +143,7 @@ const groups = computed<ContextMenuItem[][]>(() =>
:items="(item.children as T)"
:align-offset="-4"
:label-key="labelKey"
:description-key="descriptionKey"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
Expand Down
13 changes: 11 additions & 2 deletions src/runtime/components/DropdownMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type DropdownMenu = ComponentConfig<typeof theme, AppConfig, 'dropdownMenu'>

export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
label?: string
description?: string
/**
* @IconifyIcon
*/
Expand All @@ -33,7 +34,7 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cust
onSelect?(e: Event): void
onUpdateChecked?(checked: boolean): void
class?: any
ui?: Pick<DropdownMenu['slots'], 'item' | 'label' | 'separator' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLabel' | 'itemLabelExternalIcon' | 'itemTrailing' | 'itemTrailingIcon' | 'itemTrailingKbds' | 'itemTrailingKbdsSize'>
ui?: Pick<DropdownMenu['slots'], 'item' | 'label' | 'separator' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLabel' | 'itemLabelExternalIcon' | 'itemDescription' | 'itemContent' | 'itemTrailing' | 'itemTrailingIcon' | 'itemTrailingKbds' | 'itemTrailingKbdsSize'>
[key: string]: any
}

Expand Down Expand Up @@ -82,6 +83,11 @@ export interface DropdownMenuProps<T extends ArrayOrNested<DropdownMenuItem> = A
* @defaultValue 'label'
*/
labelKey?: keyof NestedItem<T>
/**
* The key used to get the description from the item.
* @defaultValue 'description'
*/
descriptionKey?: keyof NestedItem<T>
disabled?: boolean
class?: any
ui?: DropdownMenu['slots']
Expand All @@ -99,6 +105,7 @@ export type DropdownMenuSlots<
'item': SlotProps<T>
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
'item-description': SlotProps<T>
'item-trailing': SlotProps<T>
'content-top': (props?: {}) => any
'content-bottom': (props?: {}) => any
Expand All @@ -120,7 +127,8 @@ const props = withDefaults(defineProps<DropdownMenuProps<T>>(), {
portal: true,
modal: true,
externalIcon: true,
labelKey: 'label'
labelKey: 'label',
descriptionKey: 'description'
})
const emits = defineEmits<DropdownMenuEmits>()
const slots = defineSlots<DropdownMenuSlots<T>>()
Expand Down Expand Up @@ -151,6 +159,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.dropdownMenu
:items="items"
:portal="portal"
:label-key="(labelKey as keyof NestedItem<T>)"
:description-key="(descriptionKey as keyof NestedItem<T>)"
:checked-icon="checkedIcon"
:loading-icon="loadingIcon"
:external-icon="externalIcon"
Expand Down
Loading