@nuxt/ui 3.0.0 → 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 (98) hide show
  1. package/.nuxt/ui/button.ts +36 -36
  2. package/.nuxt/ui/calendar.ts +1 -1
  3. package/.nuxt/ui/container.ts +1 -1
  4. package/.nuxt/ui/drawer.ts +8 -5
  5. package/.nuxt/ui/navigation-menu.ts +2 -2
  6. package/.nuxt/ui/switch.ts +1 -1
  7. package/.nuxt/ui/tabs.ts +1 -1
  8. package/.nuxt/ui.css +1 -1
  9. package/README.md +11 -11
  10. package/cli/package.json +1 -1
  11. package/dist/module.cjs +2 -2
  12. package/dist/module.d.cts +8 -7
  13. package/dist/module.d.mts +8 -7
  14. package/dist/module.d.ts +8 -7
  15. package/dist/module.json +2 -2
  16. package/dist/module.mjs +2 -2
  17. package/dist/runtime/components/Accordion.vue +11 -8
  18. package/dist/runtime/components/Alert.vue +1 -1
  19. package/dist/runtime/components/App.vue +1 -1
  20. package/dist/runtime/components/Avatar.vue +7 -5
  21. package/dist/runtime/components/Breadcrumb.vue +13 -10
  22. package/dist/runtime/components/Button.vue +1 -1
  23. package/dist/runtime/components/Calendar.vue +40 -11
  24. package/dist/runtime/components/Carousel.vue +7 -3
  25. package/dist/runtime/components/ColorPicker.vue +2 -0
  26. package/dist/runtime/components/CommandPalette.vue +14 -11
  27. package/dist/runtime/components/ContextMenu.vue +31 -17
  28. package/dist/runtime/components/ContextMenuContent.vue +22 -15
  29. package/dist/runtime/components/Drawer.vue +10 -15
  30. package/dist/runtime/components/DropdownMenu.vue +29 -16
  31. package/dist/runtime/components/DropdownMenuContent.vue +22 -15
  32. package/dist/runtime/components/Form.vue +22 -1
  33. package/dist/runtime/components/FormField.vue +6 -0
  34. package/dist/runtime/components/InputMenu.vue +93 -55
  35. package/dist/runtime/components/Link.vue +3 -0
  36. package/dist/runtime/components/LinkBase.vue +1 -0
  37. package/dist/runtime/components/Modal.vue +11 -8
  38. package/dist/runtime/components/NavigationMenu.vue +53 -27
  39. package/dist/runtime/components/PinInput.vue +2 -0
  40. package/dist/runtime/components/Popover.vue +3 -2
  41. package/dist/runtime/components/RadioGroup.vue +25 -14
  42. package/dist/runtime/components/Select.vue +70 -43
  43. package/dist/runtime/components/SelectMenu.vue +84 -49
  44. package/dist/runtime/components/Slideover.vue +11 -8
  45. package/dist/runtime/components/Stepper.vue +13 -10
  46. package/dist/runtime/components/Table.vue +8 -3
  47. package/dist/runtime/components/Tabs.vue +8 -5
  48. package/dist/runtime/components/Toast.vue +2 -2
  49. package/dist/runtime/components/Tooltip.vue +3 -2
  50. package/dist/runtime/components/Tree.vue +48 -32
  51. package/dist/runtime/composables/defineLocale.js +1 -0
  52. package/dist/runtime/composables/useFormField.js +1 -1
  53. package/dist/runtime/composables/useKbd.js +1 -1
  54. package/dist/runtime/composables/useLocale.js +2 -2
  55. package/dist/runtime/composables/useOverlay.d.ts +3 -2
  56. package/dist/runtime/composables/useOverlay.js +1 -1
  57. package/dist/runtime/locale/ca.d.ts +3 -0
  58. package/dist/runtime/locale/ca.js +54 -0
  59. package/dist/runtime/locale/ckb.d.ts +3 -0
  60. package/dist/runtime/locale/ckb.js +55 -0
  61. package/dist/runtime/locale/hy.d.ts +3 -0
  62. package/dist/runtime/locale/hy.js +54 -0
  63. package/dist/runtime/locale/index.d.ts +6 -0
  64. package/dist/runtime/locale/index.js +6 -0
  65. package/dist/runtime/locale/ro.d.ts +3 -0
  66. package/dist/runtime/locale/ro.js +54 -0
  67. package/dist/runtime/locale/ur.d.ts +3 -0
  68. package/dist/runtime/locale/ur.js +55 -0
  69. package/dist/runtime/locale/uz.d.ts +3 -0
  70. package/dist/runtime/locale/uz.js +54 -0
  71. package/dist/runtime/plugins/colors.js +19 -15
  72. package/dist/runtime/types/form.d.ts +2 -3
  73. package/dist/runtime/types/index.d.ts +2 -0
  74. package/dist/runtime/types/index.js +2 -0
  75. package/dist/runtime/types/utils.d.ts +28 -8
  76. package/dist/runtime/utils/form.d.ts +0 -4
  77. package/dist/runtime/utils/form.js +2 -47
  78. package/dist/runtime/utils/index.d.ts +1 -0
  79. package/dist/runtime/utils/index.js +3 -0
  80. package/dist/runtime/utils/link.d.ts +2 -25
  81. package/dist/runtime/utils/link.js +31 -1
  82. package/dist/runtime/utils/tv.js +1 -1
  83. package/dist/runtime/vue/components/Link.vue +3 -0
  84. package/dist/runtime/vue/plugins/head.js +3 -0
  85. package/dist/runtime/vue/stubs.d.ts +7 -3
  86. package/dist/runtime/vue/stubs.js +12 -4
  87. package/dist/shared/{ui.COBs1u5H.cjs → ui.BL-XB3u1.cjs} +26 -26
  88. package/dist/shared/{ui.yjbGE7Dx.mjs → ui.BkNUP964.mjs} +26 -26
  89. package/dist/types.d.mts +7 -1
  90. package/dist/types.d.ts +7 -1
  91. package/dist/unplugin.cjs +6 -2
  92. package/dist/unplugin.d.cts +2 -1
  93. package/dist/unplugin.d.mts +2 -1
  94. package/dist/unplugin.d.ts +2 -1
  95. package/dist/unplugin.mjs +6 -2
  96. package/dist/vite.cjs +1 -1
  97. package/dist/vite.mjs +1 -1
  98. package/package.json +46 -29
@@ -1,20 +1,29 @@
1
+ <!-- eslint-disable vue/block-tag-newline -->
1
2
  <script lang="ts">
2
3
  import type { VariantProps } from 'tailwind-variants'
3
- import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, CollapsibleRootProps } from 'reka-ui'
4
+ import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, NavigationMenuContentEmits, CollapsibleRootProps } from 'reka-ui'
4
5
  import type { AppConfig } from '@nuxt/schema'
5
6
  import _appConfig from '#build/app.config'
6
7
  import theme from '#build/ui/navigation-menu'
7
8
  import { tv } from '../utils/tv'
8
9
  import type { AvatarProps, BadgeProps, LinkProps } from '../types'
9
- import type { DynamicSlots, MaybeArrayOfArray, MaybeArrayOfArrayItem, PartialString } from '../types/utils'
10
+ import type {
11
+ ArrayOrNested,
12
+ DynamicSlots,
13
+ MergeTypes,
14
+ NestedItem,
15
+ PartialString,
16
+ EmitsToProps
17
+ } from '../types/utils'
10
18
 
11
19
  const appConfigNavigationMenu = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
12
20
 
13
21
  const navigationMenu = tv({ extend: tv(theme), ...(appConfigNavigationMenu.ui?.navigationMenu || {}) })
14
22
 
15
- export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'children' | 'type'> {
23
+ export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'> {
16
24
  /** Description is only used when `orientation` is `horizontal`. */
17
25
  description?: string
26
+ [key: string]: any
18
27
  }
19
28
 
20
29
  export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
@@ -43,11 +52,12 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
43
52
  value?: string
44
53
  children?: NavigationMenuChildItem[]
45
54
  onSelect?(e: Event): void
55
+ [key: string]: any
46
56
  }
47
57
 
48
58
  type NavigationMenuVariants = VariantProps<typeof navigationMenu>
49
59
 
50
- 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'> {
51
61
  /**
52
62
  * The element or component this component should render as.
53
63
  * @defaultValue 'div'
@@ -93,7 +103,7 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'm
93
103
  */
94
104
  highlightColor?: NavigationMenuVariants['highlightColor']
95
105
  /** The content of the menu. */
96
- content?: Omit<NavigationMenuContentProps, 'as' | 'asChild' | 'forceMount'>
106
+ content?: Omit<NavigationMenuContentProps, 'as' | 'asChild' | 'forceMount'> & Partial<EmitsToProps<NavigationMenuContentEmits>>
97
107
  /**
98
108
  * The orientation of the content.
99
109
  * Only works when `orientation` is `horizontal`.
@@ -109,30 +119,34 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'm
109
119
  * The key used to get the label from the item.
110
120
  * @defaultValue 'label'
111
121
  */
112
- labelKey?: string
122
+ labelKey?: keyof NestedItem<T>
113
123
  class?: any
114
124
  ui?: PartialString<typeof navigationMenu.slots>
115
125
  }
116
126
 
117
127
  export interface NavigationMenuEmits extends NavigationMenuRootEmits {}
118
128
 
119
- 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
120
130
 
121
- 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
+ > = {
122
135
  'item': SlotProps<T>
123
136
  'item-leading': SlotProps<T>
124
137
  'item-label': SlotProps<T>
125
138
  'item-trailing': SlotProps<T>
126
139
  'item-content': SlotProps<T>
127
- } & DynamicSlots<T, SlotProps<T>>
140
+ } & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing' | 'content', { index: number, active?: boolean }>
141
+
128
142
  </script>
129
143
 
130
- <script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<NavigationMenuItem>">
144
+ <script setup lang="ts" generic="T extends ArrayOrNested<NavigationMenuItem>">
131
145
  import { computed, toRef } from 'vue'
132
146
  import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
133
147
  import { createReusableTemplate } from '@vueuse/core'
134
148
  import { useAppConfig } from '#imports'
135
- import { get } from '../utils'
149
+ import { get, isArrayOfArray } from '../utils'
136
150
  import { pickLinkProps } from '../utils/link'
137
151
  import ULinkBase from './LinkBase.vue'
138
152
  import ULink from './Link.vue'
@@ -141,7 +155,7 @@ import UIcon from './Icon.vue'
141
155
  import UBadge from './Badge.vue'
142
156
  import UCollapsible from './Collapsible.vue'
143
157
 
144
- const props = withDefaults(defineProps<NavigationMenuProps<I>>(), {
158
+ const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
145
159
  orientation: 'horizontal',
146
160
  contentOrientation: 'horizontal',
147
161
  externalIcon: true,
@@ -168,8 +182,14 @@ const rootProps = useForwardPropsEmits(computed(() => ({
168
182
  const contentProps = toRef(() => props.content)
169
183
 
170
184
  const appConfig = useAppConfig()
171
- const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
172
- 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
+ >({
173
193
  props: {
174
194
  item: Object,
175
195
  index: Number,
@@ -187,30 +207,36 @@ const ui = computed(() => navigationMenu({
187
207
  highlightColor: props.highlightColor || props.color
188
208
  }))
189
209
 
190
- 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
+ )
191
217
  </script>
192
218
 
193
219
  <template>
194
220
  <DefineLinkTemplate v-slot="{ item, active, index }">
195
- <slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
196
- <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">
197
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 })" />
198
224
  <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
199
225
  </slot>
200
226
 
201
227
  <span
202
- 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>])"
203
229
  :class="ui.linkLabel({ class: props.ui?.linkLabel })"
204
230
  >
205
- <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">
206
232
  {{ get(item, props.labelKey as string) }}
207
233
  </slot>
208
234
 
209
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 })" />
210
236
  </span>
211
237
 
212
- <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 })">
213
- <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">
214
240
  <UBadge
215
241
  v-if="item.badge"
216
242
  color="neutral"
@@ -220,7 +246,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
220
246
  :class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
221
247
  />
222
248
 
223
- <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 })" />
224
250
  <UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
225
251
  </slot>
226
252
  </span>
@@ -237,23 +263,23 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
237
263
  :open="item.open"
238
264
  >
239
265
  <div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
240
- <ReuseLinkTemplate :item="(item as T)" :index="index" />
266
+ <ReuseLinkTemplate :item="item" :index="index" />
241
267
  </div>
242
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>
243
269
  <component
244
- :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"
245
271
  as-child
246
272
  :active="active || item.active"
247
273
  :disabled="item.disabled"
248
274
  @select="item.onSelect"
249
275
  >
250
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 })">
251
- <ReuseLinkTemplate :item="(item as T)" :active="active || item.active" :index="index" />
277
+ <ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
252
278
  </ULinkBase>
253
279
  </component>
254
280
 
255
- <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 })">
256
- <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">
257
283
  <ul :class="ui.childList({ class: props.ui?.childList })">
258
284
  <li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
259
285
  <ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
@@ -1,3 +1,4 @@
1
+ <!-- eslint-disable vue/block-tag-newline -->
1
2
  <script lang="ts">
2
3
  import type { VariantProps } from 'tailwind-variants'
3
4
  import type { PinInputRootEmits, PinInputRootProps } from 'reka-ui'
@@ -45,6 +46,7 @@ export type PinInputEmits = PinInputRootEmits & {
45
46
  change: [payload: Event]
46
47
  blur: [payload: Event]
47
48
  }
49
+
48
50
  </script>
49
51
 
50
52
  <script setup lang="ts">
@@ -1,9 +1,10 @@
1
1
  <script lang="ts">
2
- import type { PopoverRootProps, HoverCardRootProps, PopoverRootEmits, PopoverContentProps, PopoverArrowProps } from 'reka-ui'
2
+ import type { PopoverRootProps, HoverCardRootProps, PopoverRootEmits, PopoverContentProps, PopoverContentEmits, PopoverArrowProps } from 'reka-ui'
3
3
  import type { AppConfig } from '@nuxt/schema'
4
4
  import _appConfig from '#build/app.config'
5
5
  import theme from '#build/ui/popover'
6
6
  import { tv } from '../utils/tv'
7
+ import type { EmitsToProps } from '../types/utils'
7
8
 
8
9
  const appConfigPopover = _appConfig as AppConfig & { ui: { popover: Partial<typeof theme> } }
9
10
 
@@ -19,7 +20,7 @@ export interface PopoverProps extends PopoverRootProps, Pick<HoverCardRootProps,
19
20
  * The content of the popover.
20
21
  * @defaultValue { side: 'bottom', sideOffset: 8, collisionPadding: 8 }
21
22
  */
22
- content?: Omit<PopoverContentProps, 'as' | 'asChild' | 'forceMount'>
23
+ content?: Omit<PopoverContentProps, 'as' | 'asChild' | 'forceMount'> & Partial<EmitsToProps<PopoverContentEmits>>
23
24
  /**
24
25
  * Display an arrow alongside the popover.
25
26
  * @defaultValue false
@@ -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
 
@@ -161,8 +172,8 @@ function onUpdate(value: any) {
161
172
  <RadioGroupItem
162
173
  :id="item.id"
163
174
  :value="item.value"
164
- :disabled="disabled"
165
- :class="ui.base({ class: props.ui?.base })"
175
+ :disabled="item.disabled"
176
+ :class="ui.base({ class: props.ui?.base, disabled: item.disabled })"
166
177
  >
167
178
  <RadioGroupIndicator :class="ui.indicator({ class: props.ui?.indicator })" />
168
179
  </RadioGroupItem>
@@ -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, 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 } 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
@@ -64,7 +76,7 @@ export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends Maybe
64
76
  * The content of the menu.
65
77
  * @defaultValue { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }
66
78
  */
67
- content?: Omit<SelectContentProps, 'as' | 'asChild' | 'forceMount'>
79
+ content?: Omit<SelectContentProps, 'as' | 'asChild' | 'forceMount'> & Partial<EmitsToProps<SelectContentEmits>>
68
80
  /**
69
81
  * Display an arrow alongside the menu.
70
82
  * @defaultValue false
@@ -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 })" />