@nuxt/ui 3.0.1 → 3.0.2

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 (71) hide show
  1. package/.nuxt/ui/button.ts +36 -36
  2. package/.nuxt/ui/container.ts +1 -1
  3. package/.nuxt/ui/navigation-menu.ts +2 -2
  4. package/.nuxt/ui/switch.ts +1 -1
  5. package/.nuxt/ui/tabs.ts +1 -1
  6. package/dist/module.cjs +1 -1
  7. package/dist/module.d.cts +2 -1
  8. package/dist/module.d.mts +2 -1
  9. package/dist/module.d.ts +2 -1
  10. package/dist/module.json +1 -1
  11. package/dist/module.mjs +1 -1
  12. package/dist/runtime/components/Accordion.vue +9 -8
  13. package/dist/runtime/components/Alert.vue +1 -1
  14. package/dist/runtime/components/App.vue +1 -1
  15. package/dist/runtime/components/Avatar.vue +7 -5
  16. package/dist/runtime/components/Breadcrumb.vue +11 -10
  17. package/dist/runtime/components/Button.vue +1 -1
  18. package/dist/runtime/components/Calendar.vue +40 -11
  19. package/dist/runtime/components/Carousel.vue +7 -3
  20. package/dist/runtime/components/CommandPalette.vue +12 -11
  21. package/dist/runtime/components/ContextMenu.vue +24 -12
  22. package/dist/runtime/components/ContextMenuContent.vue +22 -15
  23. package/dist/runtime/components/Drawer.vue +1 -5
  24. package/dist/runtime/components/DropdownMenu.vue +23 -12
  25. package/dist/runtime/components/DropdownMenuContent.vue +22 -15
  26. package/dist/runtime/components/Form.vue +1 -0
  27. package/dist/runtime/components/FormField.vue +1 -0
  28. package/dist/runtime/components/InputMenu.vue +92 -54
  29. package/dist/runtime/components/Link.vue +3 -0
  30. package/dist/runtime/components/LinkBase.vue +1 -0
  31. package/dist/runtime/components/Modal.vue +1 -1
  32. package/dist/runtime/components/NavigationMenu.vue +49 -25
  33. package/dist/runtime/components/RadioGroup.vue +23 -12
  34. package/dist/runtime/components/Select.vue +69 -42
  35. package/dist/runtime/components/SelectMenu.vue +83 -48
  36. package/dist/runtime/components/Slideover.vue +1 -1
  37. package/dist/runtime/components/Stepper.vue +11 -10
  38. package/dist/runtime/components/Table.vue +7 -2
  39. package/dist/runtime/components/Tabs.vue +6 -5
  40. package/dist/runtime/components/Toast.vue +1 -1
  41. package/dist/runtime/components/Tree.vue +46 -32
  42. package/dist/runtime/composables/defineShortcuts.js +0 -1
  43. package/dist/runtime/composables/useFormField.js +1 -1
  44. package/dist/runtime/composables/useOverlay.d.ts +3 -2
  45. package/dist/runtime/locale/hy.d.ts +3 -0
  46. package/dist/runtime/locale/hy.js +54 -0
  47. package/dist/runtime/locale/index.d.ts +1 -0
  48. package/dist/runtime/locale/index.js +1 -0
  49. package/dist/runtime/types/form.d.ts +1 -0
  50. package/dist/runtime/types/index.d.ts +1 -0
  51. package/dist/runtime/types/index.js +1 -0
  52. package/dist/runtime/types/utils.d.ts +28 -8
  53. package/dist/runtime/utils/index.d.ts +1 -0
  54. package/dist/runtime/utils/index.js +3 -0
  55. package/dist/runtime/utils/link.d.ts +2 -25
  56. package/dist/runtime/utils/link.js +31 -1
  57. package/dist/runtime/vue/components/Link.vue +3 -0
  58. package/dist/runtime/vue/stubs.d.ts +7 -2
  59. package/dist/runtime/vue/stubs.js +11 -2
  60. package/dist/shared/{ui.C6uTSFTI.cjs → ui.BL-XB3u1.cjs} +16 -16
  61. package/dist/shared/{ui.BCaWD5Ek.mjs → ui.BkNUP964.mjs} +16 -16
  62. package/dist/types.d.mts +7 -1
  63. package/dist/types.d.ts +7 -1
  64. package/dist/unplugin.cjs +1 -1
  65. package/dist/unplugin.d.cts +2 -1
  66. package/dist/unplugin.d.mts +2 -1
  67. package/dist/unplugin.d.ts +2 -1
  68. package/dist/unplugin.mjs +1 -1
  69. package/dist/vite.cjs +1 -1
  70. package/dist/vite.mjs +1 -1
  71. package/package.json +39 -22
@@ -7,15 +7,23 @@ import _appConfig from '#build/app.config'
7
7
  import theme from '#build/ui/navigation-menu'
8
8
  import { tv } from '../utils/tv'
9
9
  import type { AvatarProps, BadgeProps, LinkProps } from '../types'
10
- import type { DynamicSlots, MaybeArrayOfArray, MaybeArrayOfArrayItem, PartialString, EmitsToProps } from '../types/utils'
10
+ import type {
11
+ ArrayOrNested,
12
+ DynamicSlots,
13
+ MergeTypes,
14
+ NestedItem,
15
+ PartialString,
16
+ EmitsToProps
17
+ } from '../types/utils'
11
18
 
12
19
  const appConfigNavigationMenu = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
13
20
 
14
21
  const navigationMenu = tv({ extend: tv(theme), ...(appConfigNavigationMenu.ui?.navigationMenu || {}) })
15
22
 
16
- export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'children' | 'type'> {
23
+ export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'> {
17
24
  /** Description is only used when `orientation` is `horizontal`. */
18
25
  description?: string
26
+ [key: string]: any
19
27
  }
20
28
 
21
29
  export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
@@ -44,11 +52,12 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
44
52
  value?: string
45
53
  children?: NavigationMenuChildItem[]
46
54
  onSelect?(e: Event): void
55
+ [key: string]: any
47
56
  }
48
57
 
49
58
  type NavigationMenuVariants = VariantProps<typeof navigationMenu>
50
59
 
51
- export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
60
+ export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
52
61
  /**
53
62
  * The element or component this component should render as.
54
63
  * @defaultValue 'div'
@@ -110,31 +119,34 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'm
110
119
  * The key used to get the label from the item.
111
120
  * @defaultValue 'label'
112
121
  */
113
- labelKey?: string
122
+ labelKey?: keyof NestedItem<T>
114
123
  class?: any
115
124
  ui?: PartialString<typeof navigationMenu.slots>
116
125
  }
117
126
 
118
127
  export interface NavigationMenuEmits extends NavigationMenuRootEmits {}
119
128
 
120
- type SlotProps<T> = (props: { item: T, index: number, active?: boolean }) => any
129
+ type SlotProps<T extends NavigationMenuItem> = (props: { item: T, index: number, active?: boolean }) => any
121
130
 
122
- export type NavigationMenuSlots<T extends { slot?: string }> = {
131
+ export type NavigationMenuSlots<
132
+ A extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>,
133
+ T extends NestedItem<A> = NestedItem<A>
134
+ > = {
123
135
  'item': SlotProps<T>
124
136
  'item-leading': SlotProps<T>
125
137
  'item-label': SlotProps<T>
126
138
  'item-trailing': SlotProps<T>
127
139
  'item-content': SlotProps<T>
128
- } & DynamicSlots<T, SlotProps<T>>
140
+ } & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing' | 'content', { index: number, active?: boolean }>
129
141
 
130
142
  </script>
131
143
 
132
- <script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<NavigationMenuItem>">
144
+ <script setup lang="ts" generic="T extends ArrayOrNested<NavigationMenuItem>">
133
145
  import { computed, toRef } from 'vue'
134
146
  import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
135
147
  import { createReusableTemplate } from '@vueuse/core'
136
148
  import { useAppConfig } from '#imports'
137
- import { get } from '../utils'
149
+ import { get, isArrayOfArray } from '../utils'
138
150
  import { pickLinkProps } from '../utils/link'
139
151
  import ULinkBase from './LinkBase.vue'
140
152
  import ULink from './Link.vue'
@@ -143,7 +155,7 @@ import UIcon from './Icon.vue'
143
155
  import UBadge from './Badge.vue'
144
156
  import UCollapsible from './Collapsible.vue'
145
157
 
146
- const props = withDefaults(defineProps<NavigationMenuProps<I>>(), {
158
+ const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
147
159
  orientation: 'horizontal',
148
160
  contentOrientation: 'horizontal',
149
161
  externalIcon: true,
@@ -170,8 +182,14 @@ const rootProps = useForwardPropsEmits(computed(() => ({
170
182
  const contentProps = toRef(() => props.content)
171
183
 
172
184
  const appConfig = useAppConfig()
173
- const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
174
- const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, level?: number }>({
185
+ const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<
186
+ { item: NavigationMenuItem, index: number, active?: boolean },
187
+ NavigationMenuSlots<T>
188
+ >()
189
+ const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<
190
+ { item: NavigationMenuItem, index: number, level?: number },
191
+ NavigationMenuSlots<T>
192
+ >({
175
193
  props: {
176
194
  item: Object,
177
195
  index: Number,
@@ -189,30 +207,36 @@ const ui = computed(() => navigationMenu({
189
207
  highlightColor: props.highlightColor || props.color
190
208
  }))
191
209
 
192
- const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as T[][] : [])
210
+ const lists = computed<NavigationMenuItem[][]>(() =>
211
+ props.items?.length
212
+ ? isArrayOfArray(props.items)
213
+ ? props.items
214
+ : [props.items]
215
+ : []
216
+ )
193
217
  </script>
194
218
 
195
219
  <template>
196
220
  <DefineLinkTemplate v-slot="{ item, active, index }">
197
- <slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
198
- <slot :name="item.slot ? `${item.slot}-leading` : 'item-leading'" :item="(item as T)" :active="active" :index="index">
221
+ <slot :name="((item.slot || 'item') as keyof NavigationMenuSlots<T>)" :item="item" :index="index">
222
+ <slot :name="((item.slot ? `${item.slot}-leading` : 'item-leading') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
199
223
  <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 })" />
200
224
  <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
201
225
  </slot>
202
226
 
203
227
  <span
204
- v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : 'item-label'])"
228
+ v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>])"
205
229
  :class="ui.linkLabel({ class: props.ui?.linkLabel })"
206
230
  >
207
- <slot :name="item.slot ? `${item.slot}-label` : 'item-label'" :item="(item as T)" :active="active" :index="index">
231
+ <slot :name="((item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
208
232
  {{ get(item, props.labelKey as string) }}
209
233
  </slot>
210
234
 
211
235
  <UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
212
236
  </span>
213
237
 
214
- <span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[item.slot ? `${item.slot}-trailing` : 'item-trailing'])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
215
- <slot :name="item.slot ? `${item.slot}-trailing` : 'item-trailing'" :item="(item as T)" :active="active" :index="index">
238
+ <span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
239
+ <slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
216
240
  <UBadge
217
241
  v-if="item.badge"
218
242
  color="neutral"
@@ -222,7 +246,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
222
246
  :class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
223
247
  />
224
248
 
225
- <UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
249
+ <UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
226
250
  <UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
227
251
  </slot>
228
252
  </span>
@@ -239,23 +263,23 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
239
263
  :open="item.open"
240
264
  >
241
265
  <div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
242
- <ReuseLinkTemplate :item="(item as T)" :index="index" />
266
+ <ReuseLinkTemplate :item="item" :index="index" />
243
267
  </div>
244
268
  <ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && !collapsed) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
245
269
  <component
246
- :is="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) ? NavigationMenuTrigger : NavigationMenuLink"
270
+ :is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : NavigationMenuLink"
247
271
  as-child
248
272
  :active="active || item.active"
249
273
  :disabled="item.disabled"
250
274
  @select="item.onSelect"
251
275
  >
252
276
  <ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
253
- <ReuseLinkTemplate :item="(item as T)" :active="active || item.active" :index="index" />
277
+ <ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
254
278
  </ULinkBase>
255
279
  </component>
256
280
 
257
- <NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
258
- <slot :name="item.slot ? `${item.slot}-content` : 'item-content'" :item="(item as T)" :active="active" :index="index">
281
+ <NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
282
+ <slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
259
283
  <ul :class="ui.childList({ class: props.ui?.childList })">
260
284
  <li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
261
285
  <ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
@@ -1,10 +1,11 @@
1
1
  <script lang="ts">
2
2
  import type { VariantProps } from 'tailwind-variants'
3
- import type { RadioGroupRootProps, RadioGroupRootEmits, AcceptableValue } from 'reka-ui'
3
+ import type { RadioGroupRootProps, RadioGroupRootEmits } from 'reka-ui'
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'
7
7
  import { tv } from '../utils/tv'
8
+ import type { AcceptableValue } from '../types/utils'
8
9
 
9
10
  const appConfigRadioGroup = _appConfig as AppConfig & { ui: { radioGroup: Partial<typeof theme> } }
10
11
 
@@ -12,14 +13,16 @@ const radioGroup = tv({ extend: tv(theme), ...(appConfigRadioGroup.ui?.radioGrou
12
13
 
13
14
  type RadioGroupVariants = VariantProps<typeof radioGroup>
14
15
 
15
- export interface RadioGroupItem {
16
+ export type RadioGroupValue = AcceptableValue
17
+ export type RadioGroupItem = {
16
18
  label?: string
17
19
  description?: string
18
20
  disabled?: boolean
19
21
  value?: string
20
- }
22
+ [key: string]: any
23
+ } | RadioGroupValue
21
24
 
22
- export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
25
+ export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
23
26
  /**
24
27
  * The element or component this component should render as.
25
28
  * @defaultValue 'div'
@@ -63,16 +66,16 @@ export type RadioGroupEmits = RadioGroupRootEmits & {
63
66
  change: [payload: Event]
64
67
  }
65
68
 
66
- type SlotProps<T> = (props: { item: T, modelValue?: AcceptableValue }) => any
69
+ type SlotProps<T extends RadioGroupItem> = (props: { item: T & { id: string }, modelValue?: RadioGroupValue }) => any
67
70
 
68
- export interface RadioGroupSlots<T> {
71
+ export interface RadioGroupSlots<T extends RadioGroupItem = RadioGroupItem> {
69
72
  legend(props?: {}): any
70
73
  label: SlotProps<T>
71
74
  description: SlotProps<T>
72
75
  }
73
76
  </script>
74
77
 
75
- <script setup lang="ts" generic="T extends RadioGroupItem | AcceptableValue">
78
+ <script setup lang="ts" generic="T extends RadioGroupItem">
76
79
  import { computed, useId } from 'vue'
77
80
  import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'reka-ui'
78
81
  import { reactivePick } from '@vueuse/core'
@@ -102,11 +105,19 @@ const ui = computed(() => radioGroup({
102
105
  }))
103
106
 
104
107
  function normalizeItem(item: any) {
105
- if (['string', 'number', 'boolean'].includes(typeof item)) {
108
+ if (item === null) {
109
+ return {
110
+ id: `${id}:null`,
111
+ value: undefined,
112
+ label: undefined
113
+ }
114
+ }
115
+
116
+ if (typeof item === 'string' || typeof item === 'number') {
106
117
  return {
107
118
  id: `${id}:${item}`,
108
- value: item,
109
- label: item
119
+ value: String(item),
120
+ label: String(item)
110
121
  }
111
122
  }
112
123
 
@@ -170,10 +181,10 @@ function onUpdate(value: any) {
170
181
 
171
182
  <div :class="ui.wrapper({ class: props.ui?.wrapper })">
172
183
  <Label :class="ui.label({ class: props.ui?.label })" :for="item.id">
173
- <slot name="label" :item="item" :model-value="modelValue">{{ item.label }}</slot>
184
+ <slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">{{ item.label }}</slot>
174
185
  </Label>
175
186
  <p v-if="item.description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
176
- <slot name="description" :item="item" :model-value="modelValue">
187
+ <slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
177
188
  {{ item.description }}
178
189
  </slot>
179
190
  </p>
@@ -1,19 +1,29 @@
1
1
  <script lang="ts">
2
2
  import type { VariantProps } from 'tailwind-variants'
3
- import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectContentEmits, SelectArrowProps, AcceptableValue } from 'reka-ui'
3
+ import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectContentEmits, SelectArrowProps } from 'reka-ui'
4
4
  import type { AppConfig } from '@nuxt/schema'
5
5
  import _appConfig from '#build/app.config'
6
6
  import theme from '#build/ui/select'
7
7
  import type { UseComponentIconsProps } from '../composables/useComponentIcons'
8
8
  import { tv } from '../utils/tv'
9
9
  import type { AvatarProps, ChipProps, InputProps } from '../types'
10
- import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey, EmitsToProps } from '../types/utils'
10
+ import type {
11
+ AcceptableValue,
12
+ ArrayOrNested,
13
+ GetItemKeys,
14
+ GetItemValue,
15
+ GetModelValue,
16
+ GetModelValueEmits,
17
+ NestedItem,
18
+ PartialString,
19
+ EmitsToProps
20
+ } from '../types/utils'
11
21
 
12
22
  const appConfigSelect = _appConfig as AppConfig & { ui: { select: Partial<typeof theme> } }
13
23
 
14
24
  const select = tv({ extend: tv(theme), ...(appConfigSelect.ui?.select || {}) })
15
25
 
16
- export interface SelectItem {
26
+ interface SelectItemBase {
17
27
  label?: string
18
28
  /**
19
29
  * @IconifyIcon
@@ -26,13 +36,15 @@ export interface SelectItem {
26
36
  * @defaultValue 'item'
27
37
  */
28
38
  type?: 'label' | 'separator' | 'item'
29
- value?: string
39
+ value?: string | number
30
40
  disabled?: boolean
41
+ [key: string]: any
31
42
  }
43
+ export type SelectItem = SelectItemBase | AcceptableValue | boolean
32
44
 
33
45
  type SelectVariants = VariantProps<typeof select>
34
46
 
35
- export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Omit<SelectRootProps<T>, 'dir' | 'multiple' | 'modelValue' | 'defaultValue' | 'by'>, UseComponentIconsProps {
47
+ export interface SelectProps<T extends ArrayOrNested<SelectItem> = ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false> extends Omit<SelectRootProps<T>, 'dir' | 'multiple' | 'modelValue' | 'defaultValue' | 'by'>, UseComponentIconsProps {
36
48
  id?: string
37
49
  /** The placeholder text when the select is empty. */
38
50
  placeholder?: string
@@ -79,17 +91,17 @@ export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends Maybe
79
91
  * When `items` is an array of objects, select the field to use as the value.
80
92
  * @defaultValue 'value'
81
93
  */
82
- valueKey?: V
94
+ valueKey?: VK
83
95
  /**
84
96
  * When `items` is an array of objects, select the field to use as the label.
85
97
  * @defaultValue 'label'
86
98
  */
87
- labelKey?: V
88
- items?: I
99
+ labelKey?: keyof NestedItem<T>
100
+ items?: T
89
101
  /** The value of the Select when initially rendered. Use when you do not need to control the state of the Select. */
90
- defaultValue?: SelectModelValue<T, V, M, T extends { value: infer U } ? U : never>
102
+ defaultValue?: GetModelValue<T, VK, M>
91
103
  /** The controlled value of the Select. Can be bind as `v-model`. */
92
- modelValue?: SelectModelValue<T, V, M, T extends { value: infer U } ? U : never>
104
+ modelValue?: GetModelValue<T, VK, M>
93
105
  /** Whether multiple options can be selected or not. */
94
106
  multiple?: M & boolean
95
107
  /** Highlight the ring color like a focus state. */
@@ -98,18 +110,23 @@ export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends Maybe
98
110
  ui?: PartialString<typeof select.slots>
99
111
  }
100
112
 
101
- export type SelectEmits<T, V, M extends boolean> = Omit<SelectRootEmits<T>, 'update:modelValue'> & {
113
+ export type SelectEmits<A extends ArrayOrNested<SelectItem>, VK extends GetItemKeys<A> | undefined, M extends boolean> = Omit<SelectRootEmits, 'update:modelValue'> & {
102
114
  change: [payload: Event]
103
115
  blur: [payload: FocusEvent]
104
116
  focus: [payload: FocusEvent]
105
- } & SelectModelValueEmits<T, V, M, T extends { value: infer U } ? U : never>
117
+ } & GetModelValueEmits<A, VK, M>
106
118
 
107
- type SlotProps<T> = (props: { item: T, index: number }) => any
119
+ type SlotProps<T extends SelectItem> = (props: { item: T, index: number }) => any
108
120
 
109
- export interface SelectSlots<T, M extends boolean> {
110
- 'leading'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean, ui: any }): any
111
- 'default'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean }): any
112
- 'trailing'(props: { modelValue?: M extends true ? AcceptableValue[] : AcceptableValue, open: boolean, ui: any }): any
121
+ export interface SelectSlots<
122
+ A extends ArrayOrNested<SelectItem> = ArrayOrNested<SelectItem>,
123
+ VK extends GetItemKeys<A> | undefined = undefined,
124
+ M extends boolean = false,
125
+ T extends NestedItem<A> = NestedItem<A>
126
+ > {
127
+ 'leading'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof select> }): any
128
+ 'default'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean }): any
129
+ 'trailing'(props: { modelValue?: GetModelValue<A, VK, M>, open: boolean, ui: ReturnType<typeof select> }): any
113
130
  'item': SlotProps<T>
114
131
  'item-leading': SlotProps<T>
115
132
  'item-label': SlotProps<T>
@@ -117,7 +134,7 @@ export interface SelectSlots<T, M extends boolean> {
117
134
  }
118
135
  </script>
119
136
 
120
- <script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
137
+ <script setup lang="ts" generic="T extends ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false">
121
138
  import { computed, toRef } from 'vue'
122
139
  import { SelectRoot, SelectArrow, SelectTrigger, SelectPortal, SelectContent, SelectViewport, SelectLabel, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectSeparator, useForwardPropsEmits } from 'reka-ui'
123
140
  import { defu } from 'defu'
@@ -126,20 +143,20 @@ import { useAppConfig } from '#imports'
126
143
  import { useButtonGroup } from '../composables/useButtonGroup'
127
144
  import { useComponentIcons } from '../composables/useComponentIcons'
128
145
  import { useFormField } from '../composables/useFormField'
129
- import { get, compare } from '../utils'
146
+ import { compare, get, isArrayOfArray } from '../utils'
130
147
  import UIcon from './Icon.vue'
131
148
  import UAvatar from './Avatar.vue'
132
149
  import UChip from './Chip.vue'
133
150
 
134
151
  defineOptions({ inheritAttrs: false })
135
152
 
136
- const props = withDefaults(defineProps<SelectProps<T, I, V, M>>(), {
153
+ const props = withDefaults(defineProps<SelectProps<T, VK, M>>(), {
137
154
  valueKey: 'value' as never,
138
155
  labelKey: 'label' as never,
139
156
  portal: true
140
157
  })
141
- const emits = defineEmits<SelectEmits<T, V, M>>()
142
- const slots = defineSlots<SelectSlots<T, M>>()
158
+ const emits = defineEmits<SelectEmits<T, VK, M>>()
159
+ const slots = defineSlots<SelectSlots<T, VK, M>>()
143
160
 
144
161
  const appConfig = useAppConfig()
145
162
  const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required', 'multiple'), emits)
@@ -163,11 +180,17 @@ const ui = computed(() => select({
163
180
  buttonGroup: orientation.value
164
181
  }))
165
182
 
166
- const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectItem[][] : [])
183
+ const groups = computed<SelectItem[][]>(() =>
184
+ props.items?.length
185
+ ? isArrayOfArray(props.items)
186
+ ? props.items
187
+ : [props.items]
188
+ : []
189
+ )
167
190
  // eslint-disable-next-line vue/no-dupe-keys
168
191
  const items = computed(() => groups.value.flatMap(group => group) as T[])
169
192
 
170
- function displayValue(value?: AcceptableValue | AcceptableValue[]): string | undefined {
193
+ function displayValue(value?: GetItemValue<T, VK> | GetItemValue<T, VK>[]): string {
171
194
  if (props.multiple && Array.isArray(value)) {
172
195
  return value.map(v => displayValue(v)).filter(Boolean).join(', ')
173
196
  }
@@ -195,6 +218,10 @@ function onUpdateOpen(value: boolean) {
195
218
  emitFormFocus()
196
219
  }
197
220
  }
221
+
222
+ function isSelectItem(item: SelectItem): item is SelectItemBase {
223
+ return typeof item === 'object' && item !== null
224
+ }
198
225
  </script>
199
226
 
200
227
  <!-- eslint-disable vue/no-template-shadow -->
@@ -205,21 +232,21 @@ function onUpdateOpen(value: boolean) {
205
232
  v-bind="rootProps"
206
233
  :autocomplete="autocomplete"
207
234
  :disabled="disabled"
208
- :default-value="(defaultValue as (AcceptableValue | AcceptableValue[] | undefined))"
209
- :model-value="(modelValue as (AcceptableValue | AcceptableValue[] | undefined))"
235
+ :default-value="(defaultValue as (AcceptableValue | AcceptableValue[]))"
236
+ :model-value="(modelValue as (AcceptableValue | AcceptableValue[]))"
210
237
  @update:model-value="onUpdate"
211
238
  @update:open="onUpdateOpen"
212
239
  >
213
240
  <SelectTrigger :id="id" :class="ui.base({ class: [props.class, props.ui?.base] })" v-bind="{ ...$attrs, ...ariaAttrs }">
214
241
  <span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
215
- <slot name="leading" :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open" :ui="ui">
242
+ <slot name="leading" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
216
243
  <UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
217
244
  <UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
218
245
  </slot>
219
246
  </span>
220
247
 
221
- <slot :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open">
222
- <template v-for="displayedModelValue in [displayValue(modelValue)]" :key="displayedModelValue">
248
+ <slot :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open">
249
+ <template v-for="displayedModelValue in [displayValue(modelValue as GetModelValue<T, VK, M>)]" :key="displayedModelValue">
223
250
  <span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
224
251
  {{ displayedModelValue }}
225
252
  </span>
@@ -230,7 +257,7 @@ function onUpdateOpen(value: boolean) {
230
257
  </slot>
231
258
 
232
259
  <span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
233
- <slot name="trailing" :model-value="(modelValue as M extends true ? AcceptableValue[] : AcceptableValue)" :open="open" :ui="ui">
260
+ <slot name="trailing" :model-value="(modelValue as GetModelValue<T, VK, M>)" :open="open" :ui="ui">
234
261
  <UIcon v-if="trailingIconName" :name="trailingIconName" :class="ui.trailingIcon({ class: props.ui?.trailingIcon })" />
235
262
  </slot>
236
263
  </span>
@@ -241,24 +268,24 @@ function onUpdateOpen(value: boolean) {
241
268
  <SelectViewport :class="ui.viewport({ class: props.ui?.viewport })">
242
269
  <SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
243
270
  <template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
244
- <SelectLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
271
+ <SelectLabel v-if="isSelectItem(item) && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
245
272
  {{ get(item, props.labelKey as string) }}
246
273
  </SelectLabel>
247
274
 
248
- <SelectSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
275
+ <SelectSeparator v-else-if="isSelectItem(item) && item.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
249
276
 
250
277
  <SelectItem
251
278
  v-else
252
279
  :class="ui.item({ class: props.ui?.item })"
253
- :disabled="item.disabled"
254
- :value="typeof item === 'object' ? get(item, props.valueKey as string) : item"
280
+ :disabled="isSelectItem(item) && item.disabled"
281
+ :value="isSelectItem(item) ? get(item, props.valueKey as string) : item"
255
282
  >
256
- <slot name="item" :item="(item as T)" :index="index">
257
- <slot name="item-leading" :item="(item as T)" :index="index">
258
- <UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
259
- <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 })" />
283
+ <slot name="item" :item="(item as NestedItem<T>)" :index="index">
284
+ <slot name="item-leading" :item="(item as NestedItem<T>)" :index="index">
285
+ <UIcon v-if="isSelectItem(item) && item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
286
+ <UAvatar v-else-if="isSelectItem(item) && item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
260
287
  <UChip
261
- v-else-if="item.chip"
288
+ v-else-if="isSelectItem(item) && item.chip"
262
289
  :size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
263
290
  inset
264
291
  standalone
@@ -268,13 +295,13 @@ function onUpdateOpen(value: boolean) {
268
295
  </slot>
269
296
 
270
297
  <SelectItemText :class="ui.itemLabel({ class: props.ui?.itemLabel })">
271
- <slot name="item-label" :item="(item as T)" :index="index">
272
- {{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
298
+ <slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
299
+ {{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
273
300
  </slot>
274
301
  </SelectItemText>
275
302
 
276
303
  <span :class="ui.itemTrailing({ class: props.ui?.itemTrailing })">
277
- <slot name="item-trailing" :item="(item as T)" :index="index" />
304
+ <slot name="item-trailing" :item="(item as NestedItem<T>)" :index="index" />
278
305
 
279
306
  <SelectItemIndicator as-child>
280
307
  <UIcon :name="selectedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: props.ui?.itemTrailingIcon })" />