@nuxt/ui 3.0.0-alpha.5 → 3.0.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +4 -2
  2. package/dist/module.d.mts +19 -5
  3. package/dist/module.d.ts +19 -5
  4. package/dist/module.json +2 -1
  5. package/dist/module.mjs +514 -338
  6. package/dist/runtime/components/Accordion.vue +13 -5
  7. package/dist/runtime/components/Alert.vue +2 -2
  8. package/dist/runtime/components/Breadcrumb.vue +16 -8
  9. package/dist/runtime/components/Button.vue +4 -2
  10. package/dist/runtime/components/Carousel.vue +306 -0
  11. package/dist/runtime/components/Checkbox.vue +3 -6
  12. package/dist/runtime/components/CommandPalette.vue +27 -18
  13. package/dist/runtime/components/ContextMenu.vue +33 -9
  14. package/dist/runtime/components/ContextMenuContent.vue +36 -8
  15. package/dist/runtime/components/DropdownMenu.vue +33 -9
  16. package/dist/runtime/components/DropdownMenuContent.vue +38 -8
  17. package/dist/runtime/components/Form.vue +6 -2
  18. package/dist/runtime/components/Input.vue +6 -4
  19. package/dist/runtime/components/InputMenu.vue +40 -24
  20. package/dist/runtime/components/LinkBase.vue +4 -2
  21. package/dist/runtime/components/Modal.vue +2 -2
  22. package/dist/runtime/components/NavigationMenu.vue +80 -43
  23. package/dist/runtime/components/Pagination.vue +2 -2
  24. package/dist/runtime/components/RadioGroup.vue +22 -3
  25. package/dist/runtime/components/Select.vue +22 -10
  26. package/dist/runtime/components/SelectMenu.vue +43 -31
  27. package/dist/runtime/components/Slideover.vue +2 -2
  28. package/dist/runtime/components/Table.vue +232 -0
  29. package/dist/runtime/components/Tabs.vue +15 -7
  30. package/dist/runtime/components/Toast.vue +2 -2
  31. package/dist/runtime/components/Toaster.vue +1 -1
  32. package/dist/runtime/composables/defineShortcuts.js +1 -1
  33. package/dist/runtime/composables/useComponentIcons.d.ts +3 -0
  34. package/dist/runtime/composables/useModal.d.ts +1 -1
  35. package/dist/runtime/composables/useSlideover.d.ts +1 -1
  36. package/dist/runtime/index.css +1 -1
  37. package/dist/runtime/keyframes.css +1 -0
  38. package/dist/runtime/plugins/colors.js +11 -7
  39. package/dist/runtime/types/form.d.ts +3 -1
  40. package/dist/runtime/types/index.d.ts +2 -0
  41. package/dist/runtime/types/index.js +2 -0
  42. package/dist/runtime/utils/form.d.ts +7 -1
  43. package/dist/runtime/utils/form.js +25 -1
  44. package/dist/runtime/utils/index.d.ts +1 -0
  45. package/dist/runtime/utils/index.js +3 -0
  46. package/dist/runtime/utils/link.d.ts +1 -1
  47. package/package.json +30 -14
  48. package/dist/runtime/types/component.d.ts +0 -17
  49. package/dist/runtime/types/component.js +0 -0
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { InputHTMLAttributes } from 'vue'
3
3
  import { tv, type VariantProps } from 'tailwind-variants'
4
- import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxItemProps, ComboboxArrowProps } from 'radix-vue'
4
+ import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxArrowProps } from 'radix-vue'
5
5
  import type { AppConfig } from '@nuxt/schema'
6
6
  import _appConfig from '#build/app.config'
7
7
  import theme from '#build/ui/input-menu'
@@ -13,7 +13,7 @@ const appConfig = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof th
13
13
 
14
14
  const inputMenu = tv({ extend: tv(theme), ...(appConfig.ui?.inputMenu || {}) })
15
15
 
16
- export interface InputMenuItem extends Pick<ComboboxItemProps, 'disabled'> {
16
+ export interface InputMenuItem {
17
17
  label?: string
18
18
  icon?: string
19
19
  avatar?: AvatarProps
@@ -23,7 +23,8 @@ export interface InputMenuItem extends Pick<ComboboxItemProps, 'disabled'> {
23
23
  * @defaultValue 'item'
24
24
  */
25
25
  type?: 'label' | 'separator' | 'item'
26
- select?(e?: Event): void
26
+ disabled?: boolean
27
+ onSelect?(e?: Event): void
27
28
  }
28
29
 
29
30
  type InputMenuVariants = VariantProps<typeof inputMenu>
@@ -86,6 +87,11 @@ export interface InputMenuProps<T> extends Pick<ComboboxRootProps<T>, 'modelValu
86
87
  * @defaultValue undefined
87
88
  */
88
89
  valueKey?: keyof T
90
+ /**
91
+ * When `items` is an array of objects, select the field to use as the label.
92
+ * @defaultValue 'label'
93
+ */
94
+ labelKey?: keyof T
89
95
  items?: T[] | T[][]
90
96
  /** Highlight the ring color like a focus state. */
91
97
  highlight?: boolean
@@ -118,15 +124,16 @@ export interface InputMenuSlots<T> {
118
124
  import { computed, ref, toRef, onMounted } from 'vue'
119
125
  import { ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits } from 'radix-vue'
120
126
  import { defu } from 'defu'
127
+ import isEqual from 'fast-deep-equal'
121
128
  import { reactivePick } from '@vueuse/core'
122
129
  import { useAppConfig } from '#imports'
123
130
  import { useButtonGroup } from '../composables/useButtonGroup'
124
131
  import { useComponentIcons } from '../composables/useComponentIcons'
125
132
  import { useFormField } from '../composables/useFormField'
133
+ import { get, escapeRegExp } from '../utils'
126
134
  import UIcon from './Icon.vue'
127
135
  import UAvatar from './Avatar.vue'
128
136
  import UChip from './Chip.vue'
129
- import { get } from '../utils'
130
137
 
131
138
  defineOptions({ inheritAttrs: false })
132
139
 
@@ -134,7 +141,8 @@ const props = withDefaults(defineProps<InputMenuProps<T>>(), {
134
141
  type: 'text',
135
142
  autofocusDelay: 0,
136
143
  portal: true,
137
- filter: () => ['label']
144
+ filter: () => ['label'],
145
+ labelKey: 'label' as keyof T
138
146
  })
139
147
  const emits = defineEmits<InputMenuEmits<T>>()
140
148
  const slots = defineSlots<InputMenuSlots<T>>()
@@ -156,18 +164,16 @@ const ui = computed(() => inputMenu({
156
164
  size: inputSize?.value,
157
165
  loading: props.loading,
158
166
  highlight: highlight.value,
159
- leading: isLeading.value || !!slots.leading,
167
+ leading: isLeading.value || !!props.avatar || !!slots.leading,
160
168
  trailing: isTrailing.value || !!slots.trailing,
161
169
  multiple: props.multiple,
162
170
  buttonGroup: orientation.value
163
171
  }))
164
172
 
165
- function displayValue(val: AcceptableValue) {
166
- if (typeof val === 'object') {
167
- return val.label
168
- }
173
+ function displayValue(value: AcceptableValue): string {
174
+ const item = items.value.find(item => props.valueKey ? isEqual(get(item as Record<string, any>, props.valueKey as string), value) : isEqual(item, value))
169
175
 
170
- return val && String(val)
176
+ return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
171
177
  }
172
178
 
173
179
  function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: string): ArrayOrWrapped<AcceptableValue> {
@@ -175,22 +181,25 @@ function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: stri
175
181
  return items
176
182
  }
177
183
 
178
- const fields = Array.isArray(props.filter) ? props.filter : ['label']
184
+ const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
185
+ const escapedSearchTerm = escapeRegExp(searchTerm)
179
186
 
180
187
  return items.filter((item) => {
181
188
  if (typeof item !== 'object') {
182
- return String(item).search(new RegExp(searchTerm, 'i')) !== -1
189
+ return String(item).search(new RegExp(escapedSearchTerm, 'i')) !== -1
183
190
  }
184
191
 
185
192
  return fields.some((field) => {
186
- const child = get(item, field)
193
+ const child = get(item, field as string)
187
194
 
188
- return child !== null && child !== undefined && String(child).search(new RegExp(searchTerm, 'i')) !== -1
195
+ return child !== null && child !== undefined && String(child).search(new RegExp(escapedSearchTerm, 'i')) !== -1
189
196
  })
190
197
  }) as ArrayOrWrapped<T>
191
198
  }
192
199
 
193
200
  const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as InputMenuItem[][] : [])
201
+ // eslint-disable-next-line vue/no-dupe-keys
202
+ const items = computed(() => groups.value.flatMap(group => group) as T[])
194
203
 
195
204
  const inputRef = ref<InstanceType<typeof ComboboxInput> | null>(null)
196
205
 
@@ -219,6 +228,10 @@ function onBlur(event: FocusEvent) {
219
228
  emitFormBlur()
220
229
  }
221
230
 
231
+ function onFocus(event: FocusEvent) {
232
+ emits('focus', event)
233
+ }
234
+
222
235
  function onUpdateOpen(value: boolean) {
223
236
  if (!value) {
224
237
  const event = new FocusEvent('blur')
@@ -260,11 +273,12 @@ defineExpose({
260
273
  delimiter=""
261
274
  as-child
262
275
  @blur="onBlur"
276
+ @focus="onFocus"
263
277
  >
264
278
  <TagsInputItem v-for="(item, index) in tags" :key="index" :value="(item as string)" :class="ui.tagsItem({ class: props.ui?.tagsItem })">
265
279
  <TagsInputItemText :class="ui.tagsItemText({ class: props.ui?.tagsItemText })">
266
280
  <slot name="tags-item-text" :item="(item as T)" :index="index">
267
- {{ typeof item === 'object' ? item.label : item }}
281
+ {{ displayValue(item as T) }}
268
282
  </slot>
269
283
  </TagsInputItemText>
270
284
 
@@ -296,11 +310,13 @@ defineExpose({
296
310
  :required="required"
297
311
  :class="ui.base({ class: props.ui?.base })"
298
312
  @blur="onBlur"
313
+ @focus="onFocus"
299
314
  />
300
315
 
301
- <span v-if="isLeading || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
316
+ <span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
302
317
  <slot name="leading" :model-value="(modelValue as T)" :open="open" :ui="ui">
303
- <UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
318
+ <UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
319
+ <UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
304
320
  </slot>
305
321
  </span>
306
322
 
@@ -323,7 +339,7 @@ defineExpose({
323
339
  <ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
324
340
  <template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
325
341
  <ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
326
- {{ item.label }}
342
+ {{ get(item, props.labelKey as string) }}
327
343
  </ComboboxLabel>
328
344
 
329
345
  <ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
@@ -332,13 +348,13 @@ defineExpose({
332
348
  v-else
333
349
  :class="ui.item({ class: props.ui?.item })"
334
350
  :disabled="item.disabled"
335
- :value="valueKey && typeof item === 'object' ? (item[valueKey as keyof InputMenuItem]) as AcceptableValue : item"
336
- @select="item.select"
351
+ :value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
352
+ @select="item.onSelect"
337
353
  >
338
354
  <slot name="item" :item="(item as T)" :index="index">
339
355
  <slot name="item-leading" :item="(item as T)" :index="index">
340
- <UAvatar v-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
341
- <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
356
+ <UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
357
+ <UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
342
358
  <UChip
343
359
  v-else-if="item.chip"
344
360
  :size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
@@ -351,7 +367,7 @@ defineExpose({
351
367
 
352
368
  <span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
353
369
  <slot name="item-label" :item="(item as T)" :index="index">
354
- {{ displayValue(item as T) }}
370
+ {{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
355
371
  </slot>
356
372
  </span>
357
373
 
@@ -3,7 +3,7 @@ export interface LinkBaseProps {
3
3
  as?: string
4
4
  type?: string
5
5
  disabled?: boolean
6
- onClick?: (e: MouseEvent) => void | Promise<void>
6
+ onClick?: ((e: MouseEvent) => void | Promise<void>) | Array<((e: MouseEvent) => void | Promise<void>)>
7
7
  href?: string
8
8
  navigate?: (e: MouseEvent) => void
9
9
  rel?: string
@@ -28,7 +28,9 @@ function onClickWrapper(e: MouseEvent) {
28
28
  }
29
29
 
30
30
  if (props.onClick) {
31
- props.onClick(e)
31
+ for (const onClick of Array.isArray(props.onClick) ? props.onClick : [props.onClick]) {
32
+ onClick(e)
33
+ }
32
34
  }
33
35
 
34
36
  if (props.href && props.navigate && !props.isExternal) {
@@ -37,7 +37,7 @@ export interface ModalProps extends DialogRootProps {
37
37
  portal?: boolean
38
38
  /**
39
39
  * Display a close button to dismiss the modal.
40
- * `{ size: 'md', color: 'gray', variant: 'ghost' }`{lang="ts-type"}
40
+ * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
41
41
  * @defaultValue true
42
42
  */
43
43
  close?: ButtonProps | boolean
@@ -138,7 +138,7 @@ const ui = computed(() => modal({
138
138
  v-if="close"
139
139
  :icon="closeIcon || appConfig.ui.icons.close"
140
140
  size="md"
141
- color="gray"
141
+ color="neutral"
142
142
  variant="ghost"
143
143
  aria-label="Close"
144
144
  v-bind="typeof close === 'object' ? close : undefined"
@@ -1,12 +1,12 @@
1
1
  <!-- eslint-disable vue/block-tag-newline -->
2
2
  <script lang="ts">
3
3
  import { tv, type VariantProps } from 'tailwind-variants'
4
- import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuItemProps, NavigationMenuContentProps } from 'radix-vue'
4
+ import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps } from 'radix-vue'
5
5
  import type { AppConfig } from '@nuxt/schema'
6
6
  import _appConfig from '#build/app.config'
7
7
  import theme from '#build/ui/navigation-menu'
8
8
  import type { AvatarProps, BadgeProps, LinkProps } from '../types'
9
- import type { DynamicSlots } from '../types/utils'
9
+ import type { DynamicSlots, PartialString } from '../types/utils'
10
10
 
11
11
  const appConfig = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
12
12
 
@@ -16,18 +16,20 @@ export interface NavigationMenuChildItem extends Omit<LinkProps, 'raw' | 'custom
16
16
  label: string
17
17
  description?: string
18
18
  icon?: string
19
- select?(e: Event): void
19
+ badge?: string | number | BadgeProps
20
+ onSelect?(e: Event): void
20
21
  }
21
22
 
22
- export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'>, Pick<NavigationMenuItemProps, 'value'> {
23
+ export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'> {
23
24
  label?: string
24
25
  icon?: string
25
26
  avatar?: AvatarProps
26
27
  badge?: string | number | BadgeProps
27
28
  trailingIcon?: string
28
29
  slot?: string
30
+ value?: string
29
31
  children?: NavigationMenuChildItem[]
30
- select?(e: Event): void
32
+ onSelect?(e: Event): void
31
33
  }
32
34
 
33
35
  type NavigationMenuVariants = VariantProps<typeof navigationMenu>
@@ -61,8 +63,13 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'd
61
63
  * @defaultValue false
62
64
  */
63
65
  arrow?: boolean
66
+ /**
67
+ * The key used to get the label from the item.
68
+ * @defaultValue 'label'
69
+ */
70
+ labelKey?: string
64
71
  class?: any
65
- ui?: Partial<typeof navigationMenu.slots>
72
+ ui?: PartialString<typeof navigationMenu.slots>
66
73
  }
67
74
 
68
75
  export interface NavigationMenuEmits extends NavigationMenuRootEmits {}
@@ -81,17 +88,20 @@ export type NavigationMenuSlots<T extends { slot?: string }> = {
81
88
  <script setup lang="ts" generic="T extends NavigationMenuItem">
82
89
  import { computed, toRef } from 'vue'
83
90
  import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'radix-vue'
84
- import { reactivePick } from '@vueuse/core'
91
+ import { reactivePick, createReusableTemplate } from '@vueuse/core'
92
+ import { get } from '../utils'
93
+ import { pickLinkProps } from '../utils/link'
85
94
  import ULinkBase from './LinkBase.vue'
86
95
  import ULink from './Link.vue'
87
96
  import UAvatar from './Avatar.vue'
88
97
  import UIcon from './Icon.vue'
89
98
  import UBadge from './Badge.vue'
90
- import { pickLinkProps } from '../utils/link'
99
+ import UCollapsible from './Collapsible.vue'
91
100
 
92
101
  const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
93
102
  orientation: 'horizontal',
94
- delayDuration: 0
103
+ delayDuration: 0,
104
+ labelKey: 'label'
95
105
  })
96
106
  const emits = defineEmits<NavigationMenuEmits>()
97
107
  const slots = defineSlots<NavigationMenuSlots<T>>()
@@ -99,6 +109,8 @@ const slots = defineSlots<NavigationMenuSlots<T>>()
99
109
  const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'delayDuration', 'skipDelayDuration', 'orientation'), emits)
100
110
  const contentProps = toRef(() => props.content)
101
111
 
112
+ const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, active?: boolean, index: number }>()
113
+
102
114
  const ui = computed(() => navigationMenu({
103
115
  orientation: props.orientation,
104
116
  color: props.color,
@@ -111,47 +123,58 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
111
123
  </script>
112
124
 
113
125
  <template>
126
+ <DefineItemTemplate v-slot="{ item, active, index }">
127
+ <slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
128
+ <slot :name="item.slot ? `${item.slot}-leading` : 'item-leading'" :item="(item as T)" :active="active" :index="index">
129
+ <UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
130
+ <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
131
+ </slot>
132
+
133
+ <span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
134
+ <slot :name="item.slot ? `${item.slot}-label` : 'item-label'" :item="(item as T)" :active="active" :index="index">
135
+ {{ get(item, props.labelKey as string) }}
136
+ </slot>
137
+
138
+ <UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
139
+ </span>
140
+
141
+ <span v-if="item.badge || item.children?.length || !!slots[item.slot ? `${item.slot}-trailing` : 'item-trailing']" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
142
+ <slot :name="item.slot ? `${item.slot}-trailing` : 'item-trailing'" :item="(item as T)" :active="active" :index="index">
143
+ <UBadge
144
+ v-if="item.badge"
145
+ color="neutral"
146
+ variant="outline"
147
+ :size="((props.ui?.linkTrailingBadgeSize || ui.linkTrailingBadgeSize()) as BadgeProps['size'])"
148
+ v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
149
+ :class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
150
+ />
151
+ <UIcon v-if="item.children?.length" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
152
+ </slot>
153
+ </span>
154
+ </slot>
155
+ </DefineItemTemplate>
156
+
114
157
  <NavigationMenuRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
115
158
  <template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">
116
159
  <NavigationMenuList :class="ui.list({ class: props.ui?.list })">
117
- <NavigationMenuItem v-for="(item, index) in list" :key="`list-${listIndex}-${index}`" :value="item.value || String(index)" :class="ui.item({ class: props.ui?.item })">
160
+ <component
161
+ :is="(item.children?.length && orientation === 'vertical') ? UCollapsible : NavigationMenuItem"
162
+ v-for="(item, index) in list"
163
+ :key="`list-${listIndex}-${index}`"
164
+ as="li"
165
+ :value="item.value || String(index)"
166
+ :class="ui.item({ class: props.ui?.item })"
167
+ >
118
168
  <ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
119
169
  <component
120
- :is="item.children?.length ? NavigationMenuTrigger : NavigationMenuLink"
170
+ :is="(item.children?.length && orientation === 'horizontal') ? NavigationMenuTrigger : NavigationMenuLink"
121
171
  v-bind="item.children?.length ? { disabled: item.disabled } : { active }"
122
172
  as-child
123
173
  :active="active"
124
- @select="item.select"
174
+ @select="item.onSelect"
125
175
  >
126
176
  <ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active, disabled: !!item.disabled })">
127
- <slot :name="item.slot || 'item'" :item="item" :index="index">
128
- <slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="item" :active="active" :index="index">
129
- <UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
130
- <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
131
- </slot>
132
-
133
- <span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
134
- <slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="active" :index="index">
135
- {{ item.label }}
136
- </slot>
137
-
138
- <UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
139
- </span>
140
-
141
- <span v-if="item.badge || (item.children?.length && orientation === 'horizontal') || !!slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
142
- <slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="item" :active="active" :index="index">
143
- <UBadge
144
- v-if="item.badge"
145
- color="gray"
146
- variant="outline"
147
- :size="((props.ui?.linkTrailingBadgeSize || ui.linkTrailingBadgeSize()) as BadgeProps['size'])"
148
- v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
149
- :class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
150
- />
151
- <UIcon v-if="item.children?.length && orientation === 'horizontal'" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
152
- </slot>
153
- </span>
154
- </slot>
177
+ <ReuseItemTemplate :item="item" :active="active" :index="index" />
155
178
  </ULinkBase>
156
179
  </component>
157
180
 
@@ -159,13 +182,13 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
159
182
  <ul :class="ui.childList({ class: props.ui?.childList })">
160
183
  <li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
161
184
  <ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
162
- <NavigationMenuLink as-child :active="childActive" @select="childItem.select">
185
+ <NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
163
186
  <ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, childItem.class], active: childActive })">
164
187
  <UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: props.ui?.childLinkIcon, active: childActive })" />
165
188
 
166
189
  <div :class="ui.childLinkWrapper({ class: props.ui?.childLinkWrapper })">
167
190
  <p :class="ui.childLinkLabel({ class: props.ui?.childLinkLabel, active: childActive })">
168
- {{ childItem.label }}
191
+ {{ get(childItem, props.labelKey as string) }}
169
192
 
170
193
  <UIcon v-if="childItem.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: props.ui?.childLinkLabelExternalIcon, active: childActive })" />
171
194
  </p>
@@ -180,7 +203,21 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
180
203
  </ul>
181
204
  </NavigationMenuContent>
182
205
  </ULink>
183
- </NavigationMenuItem>
206
+
207
+ <template v-if="item.children?.length && orientation === 'vertical'" #content>
208
+ <ul :class="ui.childList({ class: props.ui?.childList })">
209
+ <li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
210
+ <ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
211
+ <NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
212
+ <ULinkBase v-bind="childSlotProps" :class="ui.link({ class: [props.ui?.link, childItem.class], active: childActive, disabled: !!childItem.disabled })">
213
+ <ReuseItemTemplate :item="childItem" :active="childActive" :index="childIndex" />
214
+ </ULinkBase>
215
+ </NavigationMenuLink>
216
+ </ULink>
217
+ </li>
218
+ </ul>
219
+ </template>
220
+ </component>
184
221
  </NavigationMenuList>
185
222
 
186
223
  <div v-if="orientation === 'vertical' && listIndex < lists.length - 1" :class="ui.separator({ class: props.ui?.separator })" />
@@ -44,7 +44,7 @@ export interface PaginationProps extends Pick<PaginationRootProps, 'defaultPage'
44
44
  ellipsisIcon?: string
45
45
  /**
46
46
  * The color of the pagination controls.
47
- * @defaultValue 'gray'
47
+ * @defaultValue 'neutral'
48
48
  */
49
49
  color?: ButtonProps['color']
50
50
  /**
@@ -106,7 +106,7 @@ import { useAppConfig } from '#imports'
106
106
 
107
107
  const props = withDefaults(defineProps<PaginationProps>(), {
108
108
  size: 'md',
109
- color: 'gray',
109
+ color: 'neutral',
110
110
  variant: 'outline',
111
111
  activeColor: 'primary',
112
112
  activeVariant: 'solid',
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { tv, type VariantProps } from 'tailwind-variants'
3
- import type { RadioGroupRootProps, RadioGroupRootEmits, RadioGroupItemProps } from 'radix-vue'
3
+ import type { RadioGroupRootProps, RadioGroupRootEmits } from 'radix-vue'
4
4
  import type { AppConfig } from '@nuxt/schema'
5
5
  import _appConfig from '#build/app.config'
6
6
  import theme from '#build/ui/radio-group'
@@ -12,9 +12,11 @@ const radioGroup = tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup || {}) }
12
12
 
13
13
  type RadioGroupVariants = VariantProps<typeof radioGroup>
14
14
 
15
- export interface RadioGroupItem extends Pick<RadioGroupItemProps, 'disabled' | 'value'> {
15
+ export interface RadioGroupItem {
16
16
  label?: string
17
17
  description?: string
18
+ disabled?: boolean
19
+ value?: string
18
20
  }
19
21
 
20
22
  export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
@@ -29,6 +31,16 @@ export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultVa
29
31
  * @defaultValue 'value'
30
32
  */
31
33
  valueKey?: string
34
+ /**
35
+ * When `items` is an array of objects, select the field to use as the label.
36
+ * @defaultValue 'label'
37
+ */
38
+ labelKey?: string
39
+ /**
40
+ * When `items` is an array of objects, select the field to use as the description.
41
+ * @defaultValue 'description'
42
+ */
43
+ descriptionKey?: string
32
44
  items?: T[]
33
45
  size?: RadioGroupVariants['size']
34
46
  color?: RadioGroupVariants['color']
@@ -59,9 +71,12 @@ import { computed, useId } from 'vue'
59
71
  import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'radix-vue'
60
72
  import { reactivePick } from '@vueuse/core'
61
73
  import { useFormField } from '../composables/useFormField'
74
+ import { get } from '../utils'
62
75
 
63
76
  const props = withDefaults(defineProps<RadioGroupProps<T>>(), {
64
77
  valueKey: 'value',
78
+ labelKey: 'label',
79
+ descriptionKey: 'description',
65
80
  orientation: 'vertical'
66
81
  })
67
82
  const emits = defineEmits<RadioGroupEmits>()
@@ -89,11 +104,15 @@ function normalizeItem(item: any) {
89
104
  }
90
105
  }
91
106
 
92
- const value = item[props.valueKey]
107
+ const value = get(item, props.valueKey as string)
108
+ const label = get(item, props.labelKey as string)
109
+ const description = get(item, props.descriptionKey as string)
93
110
 
94
111
  return {
95
112
  ...item,
96
113
  value,
114
+ label,
115
+ description,
97
116
  id: `${id}:${value}`
98
117
  }
99
118
  }
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { tv, type VariantProps } from 'tailwind-variants'
3
- import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectArrowProps, SelectItemProps } from 'radix-vue'
3
+ import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectArrowProps } from 'radix-vue'
4
4
  import type { AppConfig } from '@nuxt/schema'
5
5
  import _appConfig from '#build/app.config'
6
6
  import theme from '#build/ui/select'
@@ -12,7 +12,7 @@ const appConfig = _appConfig as AppConfig & { ui: { select: Partial<typeof theme
12
12
 
13
13
  const select = tv({ extend: tv(theme), ...(appConfig.ui?.select || {}) })
14
14
 
15
- export interface SelectItem extends Pick<SelectItemProps, 'disabled' | 'value'> {
15
+ export interface SelectItem {
16
16
  label?: string
17
17
  icon?: string
18
18
  avatar?: AvatarProps
@@ -22,6 +22,8 @@ export interface SelectItem extends Pick<SelectItemProps, 'disabled' | 'value'>
22
22
  * @defaultValue 'item'
23
23
  */
24
24
  type?: 'label' | 'separator' | 'item'
25
+ value?: string
26
+ disabled?: boolean
25
27
  }
26
28
 
27
29
  type SelectVariants = VariantProps<typeof select>
@@ -63,6 +65,11 @@ export interface SelectProps<T> extends Omit<SelectRootProps, 'dir'>, UseCompone
63
65
  * @defaultValue 'value'
64
66
  */
65
67
  valueKey?: string
68
+ /**
69
+ * When `items` is an array of objects, select the field to use as the label.
70
+ * @defaultValue 'label'
71
+ */
72
+ labelKey?: string
66
73
  items?: T[] | T[][]
67
74
  /** Highlight the ring color like a focus state. */
68
75
  highlight?: boolean
@@ -97,12 +104,14 @@ import { useAppConfig } from '#imports'
97
104
  import { useButtonGroup } from '../composables/useButtonGroup'
98
105
  import { useComponentIcons } from '../composables/useComponentIcons'
99
106
  import { useFormField } from '../composables/useFormField'
107
+ import { get } from '../utils'
100
108
  import UIcon from './Icon.vue'
101
109
  import UAvatar from './Avatar.vue'
102
110
  import UChip from './Chip.vue'
103
111
 
104
112
  const props = withDefaults(defineProps<SelectProps<T>>(), {
105
113
  valueKey: 'value',
114
+ labelKey: 'label',
106
115
  portal: true
107
116
  })
108
117
  const emits = defineEmits<SelectEmits>()
@@ -124,7 +133,7 @@ const ui = computed(() => select({
124
133
  size: selectSize?.value,
125
134
  loading: props.loading,
126
135
  highlight: highlight.value,
127
- leading: isLeading.value || !!slots.leading,
136
+ leading: isLeading.value || !!props.avatar || !!slots.leading,
128
137
  trailing: isTrailing.value || !!slots.trailing,
129
138
  buttonGroup: orientation.value
130
139
  }))
@@ -162,9 +171,10 @@ function onUpdateOpen(value: boolean) {
162
171
  @update:open="onUpdateOpen"
163
172
  >
164
173
  <SelectTrigger :class="ui.base({ class: [props.class, props.ui?.base] })">
165
- <span v-if="isLeading || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
174
+ <span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
166
175
  <slot name="leading" :model-value="modelValue" :open="open" :ui="ui">
167
- <UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
176
+ <UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
177
+ <UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
168
178
  </slot>
169
179
  </span>
170
180
 
@@ -183,19 +193,21 @@ function onUpdateOpen(value: boolean) {
183
193
  <SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
184
194
  <template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
185
195
  <SelectLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
186
- {{ item.label }}
196
+ {{ get(item, props.labelKey as string) }}
187
197
  </SelectLabel>
198
+
188
199
  <SelectSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
200
+
189
201
  <SelectItem
190
202
  v-else
191
203
  :class="ui.item({ class: props.ui?.item })"
192
204
  :disabled="item.disabled"
193
- :value="typeof item === 'object' ? (item[valueKey as keyof SelectItem] as string) : item"
205
+ :value="typeof item === 'object' ? get(item, props.valueKey as string) : item"
194
206
  >
195
207
  <slot name="item" :item="(item as T)" :index="index">
196
208
  <slot name="item-leading" :item="(item as T)" :index="index">
197
- <UAvatar v-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
198
- <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
209
+ <UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
210
+ <UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
199
211
  <UChip
200
212
  v-else-if="item.chip"
201
213
  :size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
@@ -208,7 +220,7 @@ function onUpdateOpen(value: boolean) {
208
220
 
209
221
  <SelectItemText :class="ui.itemLabel({ class: props.ui?.itemLabel })">
210
222
  <slot name="item-label" :item="(item as T)" :index="index">
211
- {{ typeof item === 'object' ? item.label : item }}
223
+ {{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
212
224
  </slot>
213
225
  </SelectItemText>
214
226