diff --git a/playgrounds/nuxt/app/pages/components/command-palette.vue b/playgrounds/nuxt/app/pages/components/command-palette.vue index abf3b2916d..bf4bd4efe0 100644 --- a/playgrounds/nuxt/app/pages/components/command-palette.vue +++ b/playgrounds/nuxt/app/pages/components/command-palette.vue @@ -75,6 +75,14 @@ const groups = computed(() => [{ toast.add({ title: 'Label added!' }) }, kbds: ['meta', 'L'] + }, { + label: 'Add label', + description: 'Add a label to the current item.', + icon: 'i-lucide-tag', + onSelect(e: Event) { + e.preventDefault() + }, + kbds: ['meta', 'L'] }, { label: 'More actions', placeholder: 'Search actions...', diff --git a/playgrounds/nuxt/app/pages/components/context-menu.vue b/playgrounds/nuxt/app/pages/components/context-menu.vue index 5b6de6f4b9..6296ce32d3 100644 --- a/playgrounds/nuxt/app/pages/components/context-menu.vue +++ b/playgrounds/nuxt/app/pages/components/context-menu.vue @@ -13,6 +13,7 @@ const items = computed(() => [ }], [{ label: 'Appearance', + description: 'Change the appearance of the app', children: [{ label: 'System', icon: 'i-lucide-monitor' @@ -62,6 +63,7 @@ const items = computed(() => [ label: 'Developer', children: [[{ label: 'View Source', + description: 'View the source code of the app', kbds: ['option', 'meta', 'U'], onSelect() { console.log('View Source clicked') diff --git a/playgrounds/nuxt/app/pages/components/dropdown-menu.vue b/playgrounds/nuxt/app/pages/components/dropdown-menu.vue index 4a8deb9377..553b2bf996 100644 --- a/playgrounds/nuxt/app/pages/components/dropdown-menu.vue +++ b/playgrounds/nuxt/app/pages/components/dropdown-menu.vue @@ -13,6 +13,7 @@ const items = computed(() => [ }], [{ label: 'Profile', + description: 'View your profile', icon: 'i-lucide-user', slot: 'custom' as const, onSelect(e: Event) { @@ -21,6 +22,7 @@ const items = computed(() => [ } }, { label: 'Billing', + description: 'Manage billing', icon: 'i-lucide-credit-card', kbds: ['meta', 'b'], onSelect() { @@ -52,6 +54,7 @@ const items = computed(() => [ } }], [{ label: 'More', + description: 'Import from more sources', icon: 'i-lucide-circle-plus', children: [{ label: 'Import from Slack', diff --git a/playgrounds/nuxt/app/pages/components/input-menu.vue b/playgrounds/nuxt/app/pages/components/input-menu.vue index 653905206b..8e1c56b916 100644 --- a/playgrounds/nuxt/app/pages/components/input-menu.vue +++ b/playgrounds/nuxt/app/pages/components/input-menu.vue @@ -22,22 +22,27 @@ const items = [[{ label: 'Fruits', type: 'label' as const }, ...fruits], [{ labe 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 successfully', icon: 'i-lucide-circle-check' }, { label: 'Canceled', value: 'canceled', + description: 'Issues that have been cancelled or rejected', icon: 'i-lucide-circle-x' }] satisfies InputMenuItem[] diff --git a/playgrounds/nuxt/app/pages/components/select-menu.vue b/playgrounds/nuxt/app/pages/components/select-menu.vue index 61e3ce51b5..566130a4f6 100644 --- a/playgrounds/nuxt/app/pages/components/select-menu.vue +++ b/playgrounds/nuxt/app/pages/components/select-menu.vue @@ -22,22 +22,27 @@ const items = [[{ label: 'Fruits', type: 'label' }, ...fruits], [{ label: 'Veget 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 successfully', icon: 'i-lucide-circle-check' }, { label: 'Canceled', value: 'canceled', + description: 'Issues that have been cancelled or rejected', icon: 'i-lucide-circle-x' }] satisfies SelectMenuItem[] diff --git a/playgrounds/nuxt/app/pages/components/select.vue b/playgrounds/nuxt/app/pages/components/select.vue index cacaeaf1e7..2afcff889a 100644 --- a/playgrounds/nuxt/app/pages/components/select.vue +++ b/playgrounds/nuxt/app/pages/components/select.vue @@ -21,22 +21,27 @@ const items = [[{ label: 'Fruits', type: 'label' as const }, ...fruits], [{ labe 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 successfully', icon: 'i-lucide-circle-check' }, { label: 'Canceled', value: 'canceled', + description: 'Issues that have been cancelled or rejected', icon: 'i-lucide-circle-x' }] satisfies SelectItem[] diff --git a/src/runtime/components/CommandPalette.vue b/src/runtime/components/CommandPalette.vue index 990245a04e..f9c8db9cb7 100644 --- a/src/runtime/components/CommandPalette.vue +++ b/src/runtime/components/CommandPalette.vue @@ -16,6 +16,7 @@ export interface CommandPaletteItem extends Omit void class?: any - ui?: Pick + ui?: Pick [key: string]: any } @@ -153,6 +154,11 @@ export interface CommandPaletteProps = CommandP * @defaultValue 'label' */ labelKey?: GetItemKeys + /** + * The key used to get the description from the item. + * @defaultValue 'description' + */ + descriptionKey?: GetItemKeys class?: any ui?: CommandPalette['slots'] } @@ -171,6 +177,7 @@ export type CommandPaletteSlots = CommandPalett 'item': SlotProps 'item-leading': SlotProps 'item-label': SlotProps + 'item-description': SlotProps 'item-trailing': SlotProps } & Record> & Record> @@ -200,6 +207,7 @@ import UKbd from './Kbd.vue' const props = withDefaults(defineProps>(), { modelValue: '', labelKey: 'label', + descriptionKey: 'description', autofocus: true, back: true, virtualize: false @@ -393,14 +401,22 @@ function onSelect(e: Event, item: T) { /> - - - {{ item.prefix }} + + + + {{ item.prefix }} - + - - + + + + + + + {{ get(item, props.descriptionKey as string) }} + + diff --git a/src/runtime/components/ContextMenu.vue b/src/runtime/components/ContextMenu.vue index 97bea7ad3a..018026997e 100644 --- a/src/runtime/components/ContextMenu.vue +++ b/src/runtime/components/ContextMenu.vue @@ -11,6 +11,7 @@ type ContextMenu = ComponentConfig export interface ContextMenuItem extends Omit { label?: string + description?: string /** * @IconifyIcon */ @@ -34,7 +35,7 @@ export interface ContextMenuItem extends Omit void onUpdateChecked?: (checked: boolean) => void class?: any - ui?: Pick + ui?: Pick [key: string]: any } @@ -75,6 +76,11 @@ export interface ContextMenuProps = Arr * @defaultValue 'label' */ labelKey?: GetItemKeys + /** + * The key used to get the description from the item. + * @defaultValue 'description' + */ + descriptionKey?: GetItemKeys disabled?: boolean class?: any ui?: ContextMenu['slots'] @@ -92,11 +98,12 @@ export type ContextMenuSlots< 'item': SlotProps 'item-leading': SlotProps 'item-label': (props: { item: T, active?: boolean, index: number }) => any + 'item-description': (props: { item: T, active?: boolean, index: number }) => any 'item-trailing': SlotProps 'content-top': (props?: {}) => any 'content-bottom': (props?: {}) => any } -& DynamicSlots, 'label', { active?: boolean, index: number }> +& DynamicSlots, 'label' | 'description', { active?: boolean, index: number }> & DynamicSlots, 'leading' | 'trailing', { active?: boolean, index: number, ui: ContextMenu['ui'] }> @@ -114,7 +121,8 @@ const props = withDefaults(defineProps>(), { portal: true, modal: true, externalIcon: true, - labelKey: 'label' + labelKey: 'label', + descriptionKey: 'description' }) const emits = defineEmits() const slots = defineSlots>() @@ -144,6 +152,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.contextMenu :items="items" :portal="portal" :label-key="(labelKey as keyof NestedItem)" + :description-key="(descriptionKey as keyof NestedItem)" :checked-icon="checkedIcon" :loading-icon="loadingIcon" :external-icon="externalIcon" diff --git a/src/runtime/components/ContextMenuContent.vue b/src/runtime/components/ContextMenuContent.vue index a346d7a6f2..16c437bd18 100644 --- a/src/runtime/components/ContextMenuContent.vue +++ b/src/runtime/components/ContextMenuContent.vue @@ -13,6 +13,7 @@ interface ContextMenuContentProps> exte portal?: boolean | string | HTMLElement sub?: boolean labelKey: GetItemKeys + descriptionKey: GetItemKeys /** * @IconifyIcon */ @@ -58,7 +59,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 getProxySlots = () => omit(slots, ['default']) const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>() @@ -82,12 +83,20 @@ const groups = computed(() => - - - {{ get(item, props.labelKey as string) }} - + + + + {{ get(item, props.labelKey as string) }} + + + + - + + + {{ get(item, props.descriptionKey as string) }} + + @@ -136,6 +145,7 @@ const groups = computed(() => :items="(item.children as T)" :align-offset="-4" :label-key="labelKey" + :description-key="descriptionKey" :checked-icon="checkedIcon" :loading-icon="loadingIcon" :external-icon="externalIcon" diff --git a/src/runtime/components/DropdownMenu.vue b/src/runtime/components/DropdownMenu.vue index 5b8ec56da1..dfab17eea0 100644 --- a/src/runtime/components/DropdownMenu.vue +++ b/src/runtime/components/DropdownMenu.vue @@ -11,6 +11,7 @@ type DropdownMenu = ComponentConfig export interface DropdownMenuItem extends Omit { label?: string + description?: string /** * @IconifyIcon */ @@ -34,7 +35,7 @@ export interface DropdownMenuItem extends Omit void onUpdateChecked?: (checked: boolean) => void class?: any - ui?: Pick + ui?: Pick [key: string]: any } @@ -83,6 +84,11 @@ export interface DropdownMenuProps = A * @defaultValue 'label' */ labelKey?: GetItemKeys + /** + * The key used to get the description from the item. + * @defaultValue 'description' + */ + descriptionKey?: GetItemKeys disabled?: boolean class?: any ui?: DropdownMenu['slots'] @@ -100,11 +106,12 @@ export type DropdownMenuSlots< 'item': SlotProps 'item-leading': SlotProps 'item-label': (props: { item: T, active?: boolean, index: number }) => any + 'item-description': (props: { item: T, active?: boolean, index: number }) => any 'item-trailing': SlotProps 'content-top': (props?: {}) => any 'content-bottom': (props?: {}) => any } -& DynamicSlots, 'label', { active?: boolean, index: number }> +& DynamicSlots, 'label' | 'description', { active?: boolean, index: number }> & DynamicSlots, 'leading' | 'trailing', { active?: boolean, index: number, ui: DropdownMenu['ui'] }> @@ -123,7 +130,8 @@ const props = withDefaults(defineProps>(), { portal: true, modal: true, externalIcon: true, - labelKey: 'label' + labelKey: 'label', + descriptionKey: 'description' }) const emits = defineEmits() const slots = defineSlots>() @@ -154,6 +162,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.dropdownMenu :items="items" :portal="portal" :label-key="(labelKey as keyof NestedItem)" + :description-key="(descriptionKey as keyof NestedItem)" :checked-icon="checkedIcon" :loading-icon="loadingIcon" :external-icon="externalIcon" diff --git a/src/runtime/components/DropdownMenuContent.vue b/src/runtime/components/DropdownMenuContent.vue index f3a301918d..30a9678415 100644 --- a/src/runtime/components/DropdownMenuContent.vue +++ b/src/runtime/components/DropdownMenuContent.vue @@ -14,6 +14,7 @@ interface DropdownMenuContentProps> ex portal?: boolean | string | HTMLElement sub?: boolean labelKey: GetItemKeys + descriptionKey: GetItemKeys /** * @IconifyIcon */ @@ -36,10 +37,10 @@ interface DropdownMenuContentEmits extends RekaDropdownMenuContentEmits {} type DropdownMenuContentSlots< A extends ArrayOrNested = ArrayOrNested, T extends NestedItem = NestedItem -> = Pick, 'item' | 'item-leading' | 'item-label' | 'item-trailing' | 'content-top' | 'content-bottom'> & { +> = Pick, 'item' | 'item-leading' | 'item-label' | 'item-description' | 'item-trailing' | 'content-top' | 'content-bottom'> & { default(props?: {}): any } -& DynamicSlots, 'label', { active?: boolean, index: number }> +& DynamicSlots, 'label' | 'description', { active?: boolean, index: number }> & DynamicSlots, 'leading' | 'trailing', { active?: boolean, index: number, ui: DropdownMenu['ui'] }> @@ -69,7 +70,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 getProxySlots = () => omit(slots, ['default']) const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: DropdownMenuItem, active?: boolean, index: number }>() @@ -93,12 +94,20 @@ const groups = computed(() => - - - {{ get(item, props.labelKey as string) }} - + + + + {{ get(item, props.labelKey as string) }} + + + + - + + + {{ get(item, props.descriptionKey as string) }} + + @@ -149,6 +158,7 @@ const groups = computed(() => :align-offset="-4" :side-offset="3" :label-key="labelKey" + :description-key="descriptionKey" :checked-icon="checkedIcon" :loading-icon="loadingIcon" :external-icon="externalIcon" diff --git a/src/runtime/components/InputMenu.vue b/src/runtime/components/InputMenu.vue index 22b0ddb512..73dbe7db3a 100644 --- a/src/runtime/components/InputMenu.vue +++ b/src/runtime/components/InputMenu.vue @@ -13,6 +13,7 @@ type InputMenu = ComponentConfig export type InputMenuValue = AcceptableValue export type InputMenuItem = InputMenuValue | { label?: string + description?: string /** * @IconifyIcon */ @@ -27,7 +28,7 @@ export type InputMenuItem = InputMenuValue | { disabled?: boolean onSelect?: (e: Event) => void class?: any - ui?: Pick + ui?: Pick [key: string]: any } @@ -117,6 +118,11 @@ export interface InputMenuProps = ArrayOr * @defaultValue 'label' */ labelKey?: GetItemKeys + /** + * When `items` is an array of objects, select the field to use as the description. + * @defaultValue 'description' + */ + descriptionKey?: GetItemKeys items?: T /** The value of the InputMenu when initially rendered. Use when you do not need to control the state of the InputMenu. */ defaultValue?: GetModelValue @@ -172,6 +178,7 @@ export interface InputMenuSlots< 'item': SlotProps 'item-leading': SlotProps 'item-label'(props: { item: T, index: number }): any + 'item-description'(props: { item: T, index: number }): any 'item-trailing': SlotProps 'tags-item-text'(props: { item: T, index: number }): any 'tags-item-delete': SlotProps @@ -206,6 +213,7 @@ const props = withDefaults(defineProps>(), { autofocusDelay: 0, portal: true, labelKey: 'label', + descriptionKey: 'description', resetSearchTermOnBlur: true, resetSearchTermOnSelect: true, virtualize: false @@ -473,10 +481,18 @@ defineExpose({ /> - - - {{ isInputItem(item) ? get(item, props.labelKey as string) : item }} - + + + + {{ isInputItem(item) ? get(item, props.labelKey as string) : item }} + + + + + + {{ get(item, props.descriptionKey as string) }} + + diff --git a/src/runtime/components/Select.vue b/src/runtime/components/Select.vue index 6c25194f96..901dbb4c53 100644 --- a/src/runtime/components/Select.vue +++ b/src/runtime/components/Select.vue @@ -12,6 +12,7 @@ type Select = ComponentConfig export type SelectValue = AcceptableValue export type SelectItem = SelectValue | { label?: string + description?: string /** * @IconifyIcon */ @@ -27,7 +28,7 @@ export type SelectItem = SelectValue | { disabled?: boolean onSelect?: (e: Event) => void class?: any - ui?: Pick + ui?: Pick [key: string]: any } @@ -84,6 +85,11 @@ export interface SelectProps = ArrayOrNested * @defaultValue 'label' */ labelKey?: GetItemKeys + /** + * When `items` is an array of objects, select the field to use as the description. + * @defaultValue 'description' + */ + descriptionKey?: GetItemKeys items?: T /** The value of the Select when initially rendered. Use when you do not need to control the state of the Select. */ defaultValue?: GetModelValue @@ -119,6 +125,7 @@ export interface SelectSlots< 'item': SlotProps 'item-leading': SlotProps 'item-label'(props: { item: T, index: number }): any + 'item-description'(props: { item: T, index: number }): any 'item-trailing': SlotProps 'content-top': (props?: {}) => any 'content-bottom': (props?: {}) => any @@ -146,6 +153,7 @@ defineOptions({ inheritAttrs: false }) const props = withDefaults(defineProps>(), { valueKey: 'value' as never, labelKey: 'label', + descriptionKey: 'description', portal: true, autofocusDelay: 0 }) @@ -327,11 +335,19 @@ defineExpose({ /> - - - {{ isSelectItem(item) ? get(item, props.labelKey as string) : item }} - - + + + + {{ isSelectItem(item) ? get(item, props.labelKey as string) : item }} + + + + + + {{ get(item, props.descriptionKey as string) }} + + + diff --git a/src/runtime/components/SelectMenu.vue b/src/runtime/components/SelectMenu.vue index 5111c46496..4f3738552c 100644 --- a/src/runtime/components/SelectMenu.vue +++ b/src/runtime/components/SelectMenu.vue @@ -12,6 +12,7 @@ type SelectMenu = ComponentConfig export type SelectMenuValue = AcceptableValue export type SelectMenuItem = SelectMenuValue | { label?: string + description?: string /** * @IconifyIcon */ @@ -26,7 +27,7 @@ export type SelectMenuItem = SelectMenuValue | { disabled?: boolean onSelect?: (e: Event) => void class?: any - ui?: Pick + ui?: Pick [key: string]: any } @@ -109,6 +110,11 @@ export interface SelectMenuProps = Array * @defaultValue 'label' */ labelKey?: GetItemKeys + /** + * When `items` is an array of objects, select the field to use as the description. + * @defaultValue 'description' + */ + descriptionKey?: GetItemKeys items?: T /** The value of the SelectMenu when initially rendered. Use when you do not need to control the state of the SelectMenu. */ defaultValue?: GetModelValue @@ -166,6 +172,7 @@ export interface SelectMenuSlots< 'item': SlotProps 'item-leading': SlotProps 'item-label'(props: { item: T, index: number }): any + 'item-description'(props: { item: T, index: number }): any 'item-trailing': SlotProps 'content-top': (props?: {}) => any 'content-bottom': (props?: {}) => any @@ -197,6 +204,7 @@ const props = withDefaults(defineProps>(), { portal: true, searchInput: true, labelKey: 'label', + descriptionKey: 'description', resetSearchTermOnBlur: true, resetSearchTermOnSelect: true, autofocusDelay: 0, @@ -454,10 +462,18 @@ defineExpose({ /> - - - {{ isSelectItem(item) ? get(item, props.labelKey as string) : item }} - + + + + {{ isSelectItem(item) ? get(item, props.labelKey as string) : item }} + + + + + + {{ get(item, props.descriptionKey as string) }} + + diff --git a/src/theme/command-palette.ts b/src/theme/command-palette.ts index a7e24583d9..d689c10504 100644 --- a/src/theme/command-palette.ts +++ b/src/theme/command-palette.ts @@ -12,7 +12,7 @@ export default (options: Required) => ({ group: 'p-1 isolate', empty: 'py-6 text-center text-sm text-muted', label: 'p-1.5 text-xs font-semibold text-highlighted', - item: 'group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75', + item: 'group relative w-full flex items-start gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75', itemLeadingIcon: 'shrink-0 size-5', itemLeadingAvatar: 'shrink-0', itemLeadingAvatarSize: '2xs', @@ -23,7 +23,9 @@ export default (options: Required) => ({ 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', itemTrailingKbdsSize: 'md', + itemWrapper: 'flex-1 flex flex-col items-start min-w-0', itemLabel: 'truncate space-x-1 text-dimmed', + itemDescription: 'truncate text-muted', itemLabelBase: 'text-highlighted [&>mark]:text-inverted [&>mark]:bg-primary', itemLabelPrefix: 'text-default', itemLabelSuffix: 'text-dimmed [&>mark]:text-inverted [&>mark]:bg-primary' diff --git a/src/theme/context-menu.ts b/src/theme/context-menu.ts index 9a49e6b7e2..48623185d9 100644 --- a/src/theme/context-menu.ts +++ b/src/theme/context-menu.ts @@ -7,7 +7,7 @@ export default (options: Required) => ({ group: 'p-1 isolate', label: 'w-full flex items-center font-semibold text-highlighted', separator: '-mx-1 my-1 h-px bg-border', - item: 'group relative w-full flex items-center select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75', + item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75', itemLeadingIcon: 'shrink-0', itemLeadingAvatar: 'shrink-0', itemLeadingAvatarSize: '', @@ -15,7 +15,9 @@ export default (options: Required) => ({ itemTrailingIcon: 'shrink-0', itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0', itemTrailingKbdsSize: '', + itemWrapper: 'flex-1 flex flex-col items-start min-w-0', itemLabel: 'truncate', + itemDescription: 'truncate text-muted', itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed' }, variants: { diff --git a/src/theme/dropdown-menu.ts b/src/theme/dropdown-menu.ts index c47b132fac..8ed0fbd9b0 100644 --- a/src/theme/dropdown-menu.ts +++ b/src/theme/dropdown-menu.ts @@ -8,7 +8,7 @@ export default (options: Required) => ({ group: 'p-1 isolate', label: 'w-full flex items-center font-semibold text-highlighted', separator: '-mx-1 my-1 h-px bg-border', - item: 'group relative w-full flex items-center select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75', + item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75', itemLeadingIcon: 'shrink-0', itemLeadingAvatar: 'shrink-0', itemLeadingAvatarSize: '', @@ -16,7 +16,9 @@ export default (options: Required) => ({ itemTrailingIcon: 'shrink-0', itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0', itemTrailingKbdsSize: '', + itemWrapper: 'flex-1 flex flex-col items-start min-w-0', itemLabel: 'truncate', + itemDescription: 'truncate text-muted', itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed' }, variants: { diff --git a/src/theme/input-menu.ts b/src/theme/input-menu.ts index 5ce2d22a31..a9b3b60baa 100644 --- a/src/theme/input-menu.ts +++ b/src/theme/input-menu.ts @@ -14,7 +14,7 @@ export default (options: Required) => { empty: 'text-center text-muted', label: 'font-semibold text-highlighted', separator: '-mx-1 my-1 h-px bg-border', - item: ['group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50', options.theme.transitions && 'transition-colors before:transition-colors'], + item: ['group relative w-full flex items-start gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50', options.theme.transitions && 'transition-colors before:transition-colors'], itemLeadingIcon: ['shrink-0 text-dimmed group-data-highlighted:not-group-data-disabled:text-default', options.theme.transitions && 'transition-colors'], itemLeadingAvatar: 'shrink-0', itemLeadingAvatarSize: '', @@ -22,7 +22,9 @@ export default (options: Required) => { itemLeadingChipSize: '', itemTrailing: 'ms-auto inline-flex gap-1.5 items-center', itemTrailingIcon: 'shrink-0', + itemWrapper: 'flex-1 flex flex-col min-w-0', itemLabel: 'truncate', + itemDescription: 'truncate text-muted', tagsItem: 'px-1.5 py-0.5 rounded-sm font-medium inline-flex items-center gap-0.5 ring ring-inset ring-accented bg-elevated text-default data-disabled:cursor-not-allowed data-disabled:opacity-75', tagsItemText: 'truncate', tagsItemDelete: ['inline-flex items-center rounded-xs text-dimmed hover:text-default hover:bg-accented/75 disabled:pointer-events-none', options.theme.transitions && 'transition-colors'], diff --git a/src/theme/select.ts b/src/theme/select.ts index d46a41ad6d..3b746a3515 100644 --- a/src/theme/select.ts +++ b/src/theme/select.ts @@ -17,7 +17,7 @@ export default (options: Required) => { empty: 'text-center text-muted', label: 'font-semibold text-highlighted', separator: '-mx-1 my-1 h-px bg-border', - item: ['group relative w-full flex items-center select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50', options.theme.transitions && 'transition-colors before:transition-colors'], + item: ['group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50', options.theme.transitions && 'transition-colors before:transition-colors'], itemLeadingIcon: ['shrink-0 text-dimmed group-data-highlighted:not-group-data-disabled:text-default', options.theme.transitions && 'transition-colors'], itemLeadingAvatar: 'shrink-0', itemLeadingAvatarSize: '', @@ -25,7 +25,9 @@ export default (options: Required) => { itemLeadingChipSize: '', itemTrailing: 'ms-auto inline-flex gap-1.5 items-center', itemTrailingIcon: 'shrink-0', - itemLabel: 'truncate' + itemWrapper: 'flex-1 flex flex-col min-w-0', + itemLabel: 'truncate', + itemDescription: 'truncate text-muted' }, variants: { ...fieldGroupVariant, diff --git a/test/components/CommandPalette.spec.ts b/test/components/CommandPalette.spec.ts index 8881b28fc2..4faeeb9014 100644 --- a/test/components/CommandPalette.spec.ts +++ b/test/components/CommandPalette.spec.ts @@ -65,15 +65,49 @@ describe('CommandPalette', () => { }] }] + const groupsWithDescription = [{ + id: 'actions', + items: [{ + label: 'Create Project', + description: 'Start a new project from scratch', + icon: 'i-lucide-folder-plus', + kbds: ['meta', 'N'] + }, { + label: 'Open File', + description: 'Browse and open an existing file', + icon: 'i-lucide-file', + kbds: ['meta', 'O'] + }, { + label: 'Settings', + description: 'Configure your preferences', + icon: 'i-lucide-settings', + kbds: ['meta', ','] + }] + }, { + id: 'recent', + label: 'Recent Files', + items: [{ + label: 'index.vue', + description: '/src/pages/index.vue', + icon: 'i-lucide-file-code' + }, { + label: 'app.vue', + description: '/app.vue', + icon: 'i-lucide-file-code' + }] + }] + const props = { groups } it.each([ // Props ['with groups', { props }], + ['with groups with description', { props: { groups: groupsWithDescription } }], ['without data', {}], ['with modelValue', { props: { ...props, modelValue: groups[2]?.items[0] } }], ['with defaultValue', { props: { ...props, defaultValue: groups[2]?.items[0] } }], ['with labelKey', { props: { ...props, labelKey: 'icon' } }], + ['with descriptionKey', { props: { groups: groupsWithDescription, descriptionKey: 'label' } }], ['with placeholder', { props: { ...props, placeholder: 'Search...' } }], ['with disabled', { props: { ...props, disabled: true } }], ['with icon', { props: { ...props, icon: 'i-lucide-terminal' } }], @@ -91,6 +125,7 @@ describe('CommandPalette', () => { ['with item slot', { props, slots: { item: () => 'Item slot' } }], ['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }], ['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }], + ['with item-description slot', { props: { groups: groupsWithDescription }, slots: { 'item-description': () => 'Item description slot' } }], ['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }], ['with custom slot', { props, slots: { custom: () => 'Custom slot' } }], ['with close slot', { props: { ...props, close: true }, slots: { close: () => 'Close slot' } }], diff --git a/test/components/ContextMenu.spec.ts b/test/components/ContextMenu.spec.ts index e66906c4f0..7aac8d8dca 100644 --- a/test/components/ContextMenu.spec.ts +++ b/test/components/ContextMenu.spec.ts @@ -83,12 +83,40 @@ describe('ContextMenu', () => { }] ] + const itemsWithDescription = [ + [{ + label: 'Profile', + description: 'View and edit your profile', + icon: 'i-lucide-user', + children: [{ + label: 'Settings', + description: 'Configure your preferences', + icon: 'i-lucide-settings' + }, { + label: 'Logout', + description: 'Sign out of your account', + icon: 'i-lucide-log-out' + }] + }], [{ + label: 'Dashboard', + description: 'Main overview page', + color: 'primary', + kbds: ['meta', 'D'] + }, { + label: 'Analytics', + description: 'View detailed statistics', + kbds: ['shift', 'meta', 'A'] + }] + ] + const props = { portal: false, items } it.each([ // Props ['with items', { props }], + ['with items with description', { props: { ...props, items: itemsWithDescription } }], ['with labelKey', { props: { ...props, labelKey: 'icon' } }], + ['with descriptionKey', { props: { ...props, items: itemsWithDescription, descriptionKey: 'label' } }], ['with disabled', { props: { ...props, disabled: true } }], ...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]), ['with externalIcon', { props: { ...props, externalIcon: 'i-lucide-external-link' } }], @@ -100,6 +128,7 @@ describe('ContextMenu', () => { ['with item slot', { props, slots: { item: () => 'Item slot' } }], ['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }], ['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }], + ['with item-description slot', { props: { ...props, items: itemsWithDescription }, slots: { 'item-description': () => 'Item description slot' } }], ['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }], ['with custom slot', { props, slots: { custom: () => 'Custom slot' } }] ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ContextMenuProps, slots?: Partial }) => { diff --git a/test/components/DropdownMenu.spec.ts b/test/components/DropdownMenu.spec.ts index f31e938aca..e2bdd52525 100644 --- a/test/components/DropdownMenu.spec.ts +++ b/test/components/DropdownMenu.spec.ts @@ -94,12 +94,36 @@ describe('DropdownMenu', () => { }] ] + const itemsWithDescription = [ + [{ + label: 'My account', + description: 'Account settings', + avatar: { + src: 'https://github.com/benjamincanac.png' + }, + type: 'label' + }], + [{ + label: 'Profile', + description: 'View your profile', + icon: 'i-lucide-user', + slot: 'custom' + }, { + label: 'Billing', + description: 'Manage billing', + icon: 'i-lucide-credit-card', + kbds: ['meta', 'b'] + }] + ] + const props = { open: true, portal: false, items } it.each([ // Props ['with items', { props }], + ['with items with description', { props: { ...props, items: itemsWithDescription } }], ['with labelKey', { props: { ...props, labelKey: 'icon' } }], + ['with descriptionKey', { props: { ...props, descriptionKey: 'description' } }], ['with disabled', { props: { ...props, disabled: true } }], ['with arrow', { props: { ...props, arrow: true } }], ...sizes.map((size: string) => [`with size ${size}`, { props: { ...props, size } }]), @@ -112,6 +136,7 @@ describe('DropdownMenu', () => { ['with item slot', { props, slots: { item: () => 'Item slot' } }], ['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }], ['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }], + ['with item-description slot', { props: { ...props, items: itemsWithDescription }, slots: { 'item-description': () => 'Item description slot' } }], ['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }], ['with custom slot', { props, slots: { custom: () => 'Custom slot' } }] ])('renders %s correctly', async (nameOrHtml: string, options: { props?: DropdownMenuProps, slots?: Partial }) => { diff --git a/test/components/InputMenu.spec.ts b/test/components/InputMenu.spec.ts index 44067bd90d..2f32a3d640 100644 --- a/test/components/InputMenu.spec.ts +++ b/test/components/InputMenu.spec.ts @@ -36,15 +36,19 @@ describe('InputMenu', () => { icon: 'i-lucide-circle-x' }] + const itemsWithDescription = [...items.map(item => ({ ...item, description: 'Description' }))] + const props = { open: true, portal: false, items } it.each([ // Props ['with items', { props }], + ['with items with description', { props: { ...props, items: itemsWithDescription } }], ['with modelValue', { props: { ...props, modelValue: items[0] } }], ['with defaultValue', { props: { ...props, defaultValue: items[0] } }], ['with valueKey', { props: { ...props, valueKey: 'value' } }], ['with labelKey', { props: { ...props, labelKey: 'value' } }], + ['with descriptionKey', { props: { ...props, descriptionKey: 'description' } }], ['with multiple', { props: { ...props, multiple: true } }], ['with multiple and modelValue', { props: { ...props, multiple: true, modelValue: [items[0], items[1]] } }], ['with id', { props: { ...props, id: 'id' } }], @@ -83,6 +87,7 @@ describe('InputMenu', () => { ['with item slot', { props, slots: { item: () => 'Item slot' } }], ['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }], ['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }], + ['with item-description slot', { props: { ...props, items: itemsWithDescription }, slots: { 'item-description': () => 'Item description slot' } }], ['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }], ['with create-item-label slot', { props: { ...props, searchTerm: 'New value', createItem: true }, slots: { 'create-item-label': () => 'Create item slot' } }] ])('renders %s correctly', async (nameOrHtml: string, options: { props?: InputMenuProps, slots?: Partial }) => { diff --git a/test/components/Select.spec.ts b/test/components/Select.spec.ts index 941c92a6e0..cb3886780f 100644 --- a/test/components/Select.spec.ts +++ b/test/components/Select.spec.ts @@ -36,15 +36,19 @@ describe('Select', () => { icon: 'i-lucide-circle-x' }] + const itemsWithDescription = [...items.map(item => ({ ...item, description: 'Description' }))] + const props = { open: true, portal: false, items } it.each([ // Props ['with items', { props }], + ['with items with description', { props: { ...props, items: itemsWithDescription } }], ['with modelValue', { props: { ...props, modelValue: items[0]?.value } }], ['with defaultValue', { props: { ...props, defaultValue: items[0]?.value } }], ['with valueKey', { props: { ...props, valueKey: 'label' } }], ['with labelKey', { props: { ...props, labelKey: 'value' } }], + ['with descriptionKey', { props: { ...props, descriptionKey: 'description' } }], ['with multiple', { props: { ...props, multiple: true } }], ['with multiple and modelValue', { props: { ...props, multiple: true, modelValue: [items[0], items[1]] } }], ['with id', { props: { ...props, id: 'id' } }], @@ -80,6 +84,7 @@ describe('Select', () => { ['with item slot', { props, slots: { item: () => 'Item slot' } }], ['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }], ['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }], + ['with item-description slot', { props: { ...props, items: itemsWithDescription }, slots: { 'item-description': () => 'Item description slot' } }], ['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }] ])('renders %s correctly', async (nameOrHtml: string, options: { props?: SelectProps, slots?: Partial }) => { const html = await ComponentRender(nameOrHtml, options, Select) diff --git a/test/components/SelectMenu.spec.ts b/test/components/SelectMenu.spec.ts index 43b0f52865..99551ed76d 100644 --- a/test/components/SelectMenu.spec.ts +++ b/test/components/SelectMenu.spec.ts @@ -36,15 +36,19 @@ describe('SelectMenu', () => { icon: 'i-lucide-circle-x' }] + const itemsWithDescription = [...items.map(item => ({ ...item, description: 'Description' }))] + const props = { open: true, portal: false, items } it.each([ // Props ['with items', { props }], + ['with items with description', { props: { ...props, items: itemsWithDescription } }], ['with modelValue', { props: { ...props, modelValue: items[0] } }], ['with defaultValue', { props: { ...props, defaultValue: items[0] } }], ['with valueKey', { props: { ...props, valueKey: 'value' } }], ['with labelKey', { props: { ...props, labelKey: 'value' } }], + ['with descriptionKey', { props: { ...props, descriptionKey: 'description' } }], ['with multiple', { props: { ...props, multiple: true } }], ['with multiple and modelValue', { props: { ...props, multiple: true, modelValue: [items[0], items[1]] } }], ['with id', { props: { ...props, id: 'id' } }], @@ -85,6 +89,7 @@ describe('SelectMenu', () => { ['with item slot', { props, slots: { item: () => 'Item slot' } }], ['with item-leading slot', { props, slots: { 'item-leading': () => 'Item leading slot' } }], ['with item-label slot', { props, slots: { 'item-label': () => 'Item label slot' } }], + ['with item-description slot', { props: { ...props, items: itemsWithDescription }, slots: { 'item-description': () => 'Item description slot' } }], ['with item-trailing slot', { props, slots: { 'item-trailing': () => 'Item trailing slot' } }], ['with create-item-label slot', { props: { ...props, searchTerm: 'New value', createItem: true }, slots: { 'create-item-label': () => 'Create item slot' } }] ])('renders %s correctly', async (nameOrHtml: string, options: { props?: SelectMenuProps, slots?: Partial }) => { diff --git a/test/components/__snapshots__/CommandPalette-vue.spec.ts.snap b/test/components/__snapshots__/CommandPalette-vue.spec.ts.snap index 9d24c56f04..dd58c29122 100644 --- a/test/components/__snapshots__/CommandPalette-vue.spec.ts.snap +++ b/test/components/__snapshots__/CommandPalette-vue.spec.ts.snap @@ -8,27 +8,36 @@ exports[`CommandPalette > renders with as correctly 1`] = ` @@ -45,27 +54,36 @@ exports[`CommandPalette > renders with class correctly 1`] = `
@@ -80,27 +98,36 @@ exports[`CommandPalette > renders with close correctly 1`] = `
@@ -115,27 +142,36 @@ exports[`CommandPalette > renders with close slot correctly 1`] = `
@@ -150,27 +186,36 @@ exports[`CommandPalette > renders with closeIcon correctly 1`] = `
@@ -187,25 +232,33 @@ exports[`CommandPalette > renders with custom slot correctly 1`] = `
@@ -222,27 +275,62 @@ exports[`CommandPalette > renders with defaultValue correctly 1`] = `
+
+ + +" +`; + +exports[`CommandPalette > renders with descriptionKey correctly 1`] = ` +"
+
+ +
+
+
@@ -259,27 +347,36 @@ exports[`CommandPalette > renders with disabled correctly 1`] = `
@@ -296,27 +393,36 @@ exports[`CommandPalette > renders with empty slot correctly 1`] = `
@@ -333,27 +439,36 @@ exports[`CommandPalette > renders with footer slot correctly 1`] = `
@@ -370,27 +485,62 @@ exports[`CommandPalette > renders with groups correctly 1`] = `
+
+ + +
" +`; + +exports[`CommandPalette > renders with groups with description correctly 1`] = ` +"
+
+ +
+
+
@@ -407,27 +557,36 @@ exports[`CommandPalette > renders with icon correctly 1`] = `
@@ -444,15 +603,42 @@ exports[`CommandPalette > renders with item slot correctly 1`] = `
+
+ + +
" +`; + +exports[`CommandPalette > renders with item-description slot correctly 1`] = ` +"
+
+ +
+
+
@@ -469,27 +655,36 @@ exports[`CommandPalette > renders with item-label slot correctly 1`] = `
@@ -506,21 +701,33 @@ exports[`CommandPalette > renders with item-leading slot correctly 1`] = `
@@ -537,21 +744,33 @@ exports[`CommandPalette > renders with item-trailing slot correctly 1`] = `
@@ -568,30 +787,34 @@ exports[`CommandPalette > renders with labelKey correctly 1`] = `
" +`; + +exports[`CommandPalette > renders with descriptionKey correctly 1`] = ` +"
+
+ +
+
+
@@ -265,27 +353,36 @@ exports[`CommandPalette > renders with disabled correctly 1`] = `
@@ -302,27 +399,36 @@ exports[`CommandPalette > renders with empty slot correctly 1`] = `
@@ -339,27 +445,36 @@ exports[`CommandPalette > renders with footer slot correctly 1`] = `
@@ -376,27 +491,62 @@ exports[`CommandPalette > renders with groups correctly 1`] = `
+
+ + +
" +`; + +exports[`CommandPalette > renders with groups with description correctly 1`] = ` +"
+
+ +
+
+
@@ -413,27 +563,36 @@ exports[`CommandPalette > renders with icon correctly 1`] = `
@@ -450,15 +609,42 @@ exports[`CommandPalette > renders with item slot correctly 1`] = `
+
+ + +
" +`; + +exports[`CommandPalette > renders with item-description slot correctly 1`] = ` +"
+
+ +
+
+
@@ -475,27 +661,36 @@ exports[`CommandPalette > renders with item-label slot correctly 1`] = `
@@ -512,21 +707,33 @@ exports[`CommandPalette > renders with item-leading slot correctly 1`] = `
@@ -543,21 +750,33 @@ exports[`CommandPalette > renders with item-trailing slot correctly 1`] = `
@@ -574,30 +793,34 @@ exports[`CommandPalette > renders with labelKey correctly 1`] = `