@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
  <!-- eslint-disable vue/block-tag-newline -->
2
2
  <script lang="ts">
3
3
  import { tv } from 'tailwind-variants'
4
- import type { AccordionRootProps, AccordionRootEmits, AccordionItemProps } from 'radix-vue'
4
+ import type { AccordionRootProps, AccordionRootEmits } 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/accordion'
@@ -11,7 +11,7 @@ const appConfig = _appConfig as AppConfig & { ui: { accordion: Partial<typeof th
11
11
 
12
12
  const accordion = tv({ extend: tv(theme), ...(appConfig.ui?.accordion || {}) })
13
13
 
14
- export interface AccordionItem extends Pick<AccordionItemProps, 'disabled'> {
14
+ export interface AccordionItem {
15
15
  label?: string
16
16
  icon?: string
17
17
  trailingIcon?: string
@@ -19,6 +19,7 @@ export interface AccordionItem extends Pick<AccordionItemProps, 'disabled'> {
19
19
  content?: string
20
20
  /** A unique value for the accordion item. Defaults to the index. */
21
21
  value?: string
22
+ disabled?: boolean
22
23
  }
23
24
 
24
25
  export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible' | 'defaultValue' | 'modelValue' | 'type' | 'disabled'> {
@@ -33,6 +34,11 @@ export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible
33
34
  * @defaultValue appConfig.ui.icons.chevronDown
34
35
  */
35
36
  trailingIcon?: string
37
+ /**
38
+ * The key used to get the label from the item.
39
+ * @defaultValue 'label'
40
+ */
41
+ labelKey?: string
36
42
  class?: any
37
43
  ui?: Partial<typeof accordion.slots>
38
44
  }
@@ -56,10 +62,12 @@ import { computed } from 'vue'
56
62
  import { AccordionRoot, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent, useForwardPropsEmits } from 'radix-vue'
57
63
  import { reactivePick } from '@vueuse/core'
58
64
  import { useAppConfig } from '#imports'
65
+ import { get } from '../utils'
59
66
 
60
67
  const props = withDefaults(defineProps<AccordionProps<T>>(), {
61
68
  type: 'single',
62
- collapsible: true
69
+ collapsible: true,
70
+ labelKey: 'label'
63
71
  })
64
72
  const emits = defineEmits<AccordionEmits>()
65
73
  const slots = defineSlots<AccordionSlots<T>>()
@@ -88,8 +96,8 @@ const ui = computed(() => accordion({
88
96
  <UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
89
97
  </slot>
90
98
 
91
- <span v-if="item.label || !!slots.default" :class="ui.label({ class: props.ui?.label })">
92
- <slot :item="item" :index="index" :open="open">{{ item.label }}</slot>
99
+ <span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: props.ui?.label })">
100
+ <slot :item="item" :index="index" :open="open">{{ get(item, props.labelKey as string) }}</slot>
93
101
  </span>
94
102
 
95
103
  <slot name="trailing" :item="item" :index="index" :open="open">
@@ -31,7 +31,7 @@ export interface AlertProps {
31
31
  actions?: ButtonProps[]
32
32
  /**
33
33
  * Display a close button to dismiss the alert.
34
- * `{ size: 'md', color: 'gray', variant: 'link' }`{lang="ts-type"}
34
+ * `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
35
35
  * @emits `close`
36
36
  * @defaultValue false
37
37
  */
@@ -117,7 +117,7 @@ const ui = computed(() => alert({
117
117
  v-if="close"
118
118
  :icon="closeIcon || appConfig.ui.icons.close"
119
119
  size="md"
120
- color="gray"
120
+ color="neutral"
121
121
  variant="link"
122
122
  aria-label="Close"
123
123
  v-bind="typeof close === 'object' ? close : undefined"
@@ -5,7 +5,7 @@ import type { AppConfig } from '@nuxt/schema'
5
5
  import _appConfig from '#build/app.config'
6
6
  import theme from '#build/ui/breadcrumb'
7
7
  import type { AvatarProps, LinkProps } from '../types'
8
- import type { DynamicSlots } from '../types/utils'
8
+ import type { DynamicSlots, PartialString } from '../types/utils'
9
9
 
10
10
  const appConfig = _appConfig as AppConfig & { ui: { breadcrumb: Partial<typeof theme> } }
11
11
 
@@ -30,8 +30,13 @@ export interface BreadcrumbProps<T> {
30
30
  * @defaultValue appConfig.ui.icons.chevronRight
31
31
  */
32
32
  separatorIcon?: string
33
+ /**
34
+ * The key used to get the label from the item.
35
+ * @defaultValue 'label'
36
+ */
37
+ labelKey?: string
33
38
  class?: any
34
- ui?: Partial<typeof breadcrumb.slots>
39
+ ui?: PartialString<typeof breadcrumb.slots>
35
40
  }
36
41
 
37
42
  type SlotProps<T> = (props: { item: T, index: number, active?: boolean }) => any
@@ -49,13 +54,16 @@ export type BreadcrumbSlots<T extends { slot?: string }> = {
49
54
  <script setup lang="ts" generic="T extends BreadcrumbItem">
50
55
  import { Primitive } from 'radix-vue'
51
56
  import { useAppConfig } from '#imports'
57
+ import { get } from '../utils'
58
+ import { pickLinkProps } from '../utils/link'
52
59
  import UIcon from './Icon.vue'
53
60
  import UAvatar from './Avatar.vue'
54
61
  import ULinkBase from './LinkBase.vue'
55
62
  import ULink from './Link.vue'
56
- import { pickLinkProps } from '../utils/link'
57
63
 
58
- const props = defineProps<BreadcrumbProps<T>>()
64
+ const props = withDefaults(defineProps<BreadcrumbProps<T>>(), {
65
+ labelKey: 'label'
66
+ })
59
67
  const slots = defineSlots<BreadcrumbSlots<T>>()
60
68
 
61
69
  const appConfig = useAppConfig()
@@ -73,13 +81,13 @@ const ui = breadcrumb()
73
81
  <ULinkBase v-bind="slotProps" as="span" :aria-current="active && (index === items!.length - 1) ? 'page' : undefined" :class="ui.link({ class: [props.ui?.link, item.class], active: index === items!.length - 1, disabled: !!item.disabled, to: !!item.to })">
74
82
  <slot :name="item.slot || 'item'" :item="item" :index="index">
75
83
  <slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="item" :active="index === items!.length - 1" :index="index">
76
- <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: index === items!.length - 1 })" />
77
- <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active: index === items!.length - 1 })" />
84
+ <UIcon v-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active: index === items!.length - 1 })" />
85
+ <UAvatar v-else-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active: index === items!.length - 1 })" />
78
86
  </slot>
79
87
 
80
- <span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
88
+ <span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
81
89
  <slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="index === items!.length - 1" :index="index">
82
- {{ item.label }}
90
+ {{ get(item, props.labelKey as string) }}
83
91
  </slot>
84
92
  </span>
85
93
 
@@ -5,6 +5,7 @@ import _appConfig from '#build/app.config'
5
5
  import theme from '#build/ui/button'
6
6
  import type { LinkProps } from './Link.vue'
7
7
  import type { UseComponentIconsProps } from '../composables/useComponentIcons'
8
+ import type { AvatarProps } from '../types'
8
9
  import type { PartialString } from '../types/utils'
9
10
  import { formLoadingInjectionKey } from '../composables/useFormField'
10
11
 
@@ -42,10 +43,10 @@ import { type Ref, computed, ref, inject } from 'vue'
42
43
  import { useForwardProps } from 'radix-vue'
43
44
  import { useComponentIcons } from '../composables/useComponentIcons'
44
45
  import { useButtonGroup } from '../composables/useButtonGroup'
45
- import UIcon from './Icon.vue'
46
- import ULink from './Link.vue'
47
46
  import { omit } from '../utils'
48
47
  import { pickLinkProps } from '../utils/link'
48
+ import UIcon from './Icon.vue'
49
+ import ULink from './Link.vue'
49
50
 
50
51
  const props = defineProps<ButtonProps>()
51
52
  const slots = defineSlots<ButtonSlots>()
@@ -99,6 +100,7 @@ const ui = computed(() => button({
99
100
  >
100
101
  <slot name="leading">
101
102
  <UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
103
+ <UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
102
104
  </slot>
103
105
 
104
106
  <slot>
@@ -0,0 +1,306 @@
1
+ <!-- eslint-disable vue/block-tag-newline -->
2
+ <script lang="ts">
3
+ import { tv, type VariantProps } from 'tailwind-variants'
4
+ import type { AppConfig } from '@nuxt/schema'
5
+ import type { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from 'embla-carousel'
6
+ import type { AutoplayOptionsType } from 'embla-carousel-autoplay'
7
+ import type { AutoScrollOptionsType } from 'embla-carousel-auto-scroll'
8
+ import type { AutoHeightOptionsType } from 'embla-carousel-auto-height'
9
+ import type { ClassNamesOptionsType } from 'embla-carousel-class-names'
10
+ import type { FadeOptionsType } from 'embla-carousel-fade'
11
+ import type { WheelGesturesPluginOptions } from 'embla-carousel-wheel-gestures'
12
+ import _appConfig from '#build/app.config'
13
+ import theme from '#build/ui/carousel'
14
+ import type { ButtonProps } from '../types'
15
+ import type { AcceptableValue, PartialString } from '../types/utils'
16
+
17
+ const appConfig = _appConfig as AppConfig & { ui: { carousel: Partial<typeof theme> } }
18
+
19
+ const carousel = tv({ extend: tv(theme), ...(appConfig.ui?.carousel || {}) })
20
+
21
+ type CarouselVariants = VariantProps<typeof carousel>
22
+
23
+ export interface CarouselProps<T> extends Omit<EmblaOptionsType, 'axis' | 'container' | 'slides' | 'direction'> {
24
+ /**
25
+ * Configure the prev button when arrows are enabled.
26
+ * @defaultValue { size: 'md', color: 'neutral', variant: 'link' }
27
+ */
28
+ prev?: ButtonProps
29
+ /**
30
+ * The icon displayed in the prev button.
31
+ * @defaultValue appConfig.ui.icons.arrowLeft
32
+ */
33
+ prevIcon?: string
34
+ /**
35
+ * Configure the next button when arrows are enabled.
36
+ * @defaultValue { size: 'md', color: 'neutral', variant: 'link' }
37
+ */
38
+ next?: ButtonProps
39
+ /**
40
+ * The icon displayed in the next button.
41
+ * @defaultValue appConfig.ui.icons.arrowRight
42
+ */
43
+ nextIcon?: string
44
+ /**
45
+ * Display prev and next buttons to scroll the carousel.
46
+ * @defaultValue false
47
+ */
48
+ arrows?: boolean
49
+ /**
50
+ * Display dots to scroll to a specific slide.
51
+ * @defaultValue false
52
+ */
53
+ dots?: boolean
54
+ orientation?: CarouselVariants['orientation']
55
+ items?: T[]
56
+ /**
57
+ * Enable Autoplay plugin
58
+ * @link https://www.embla-carousel.com/plugins/autoplay/
59
+ */
60
+ autoplay?: boolean | AutoplayOptionsType
61
+ /**
62
+ * Enable Auto Scroll plugin
63
+ * @link https://www.embla-carousel.com/plugins/auto-scroll/
64
+ */
65
+ autoScroll?: boolean | AutoScrollOptionsType
66
+ /**
67
+ * Enable Auto Height plugin
68
+ * @link https://www.embla-carousel.com/plugins/auto-height/
69
+ */
70
+ autoHeight?: boolean | AutoHeightOptionsType
71
+ /**
72
+ * Enable Class Names plugin
73
+ * @link https://www.embla-carousel.com/plugins/class-names/
74
+ */
75
+ classNames?: boolean | ClassNamesOptionsType
76
+ /**
77
+ * Enable Fade plugin
78
+ * @link https://www.embla-carousel.com/plugins/fade/
79
+ */
80
+ fade?: boolean | FadeOptionsType
81
+ /**
82
+ * Enable Wheel Gestures plugin
83
+ * @link https://www.embla-carousel.com/plugins/wheel-gestures/
84
+ */
85
+ wheelGestures?: boolean | WheelGesturesPluginOptions
86
+ class?: any
87
+ ui?: PartialString<typeof carousel.slots>
88
+ }
89
+
90
+ export type CarouselSlots<T> = {
91
+ default(props: { item: T, index: number }): any
92
+ }
93
+
94
+ </script>
95
+
96
+ <script setup lang="ts" generic="T extends AcceptableValue">
97
+ import { computed, ref, watch, onMounted } from 'vue'
98
+ import useEmblaCarousel from 'embla-carousel-vue'
99
+ import { useForwardProps } from 'radix-vue'
100
+ import { reactivePick, computedAsync } from '@vueuse/core'
101
+ import { useAppConfig } from '#imports'
102
+ import UButton from './Button.vue'
103
+
104
+ const props = withDefaults(defineProps<CarouselProps<T>>(), {
105
+ orientation: 'horizontal',
106
+ arrows: false,
107
+ dots: false,
108
+ // Embla Options
109
+ active: true,
110
+ align: 'center',
111
+ breakpoints: () => ({}),
112
+ containScroll: 'trimSnaps',
113
+ dragFree: false,
114
+ dragThreshold: 10,
115
+ duration: 25,
116
+ inViewThreshold: 0,
117
+ loop: false,
118
+ skipSnaps: false,
119
+ slidesToScroll: 1,
120
+ startIndex: 0,
121
+ watchDrag: true,
122
+ watchResize: true,
123
+ watchSlides: true,
124
+ watchFocus: true,
125
+ // Embla Plugins
126
+ autoplay: false,
127
+ autoScroll: false,
128
+ autoHeight: false,
129
+ classNames: false,
130
+ fade: false,
131
+ wheelGestures: false
132
+ })
133
+ defineSlots<CarouselSlots<T>>()
134
+
135
+ const appConfig = useAppConfig()
136
+ const rootProps = useForwardProps(reactivePick(props, 'active', 'align', 'breakpoints', 'containScroll', 'dragFree', 'dragThreshold', 'duration', 'inViewThreshold', 'loop', 'skipSnaps', 'slidesToScroll', 'startIndex', 'watchDrag', 'watchResize', 'watchSlides', 'watchFocus'))
137
+
138
+ const ui = computed(() => carousel({
139
+ orientation: props.orientation
140
+ }))
141
+
142
+ const options = computed<EmblaOptionsType>(() => ({
143
+ ...(props.fade ? { align: 'center', containScroll: false } : {}),
144
+ ...rootProps.value,
145
+ axis: props.orientation === 'horizontal' ? 'x' : 'y',
146
+ // TODO: Get from ConfigProvider
147
+ direction: 'ltr'
148
+ }))
149
+
150
+ const plugins = computedAsync<EmblaPluginType[]>(async () => {
151
+ const plugins = []
152
+
153
+ if (props.autoplay) {
154
+ const AutoplayPlugin = await import('embla-carousel-autoplay').then(r => r.default)
155
+ plugins.push(AutoplayPlugin(typeof props.autoplay === 'boolean' ? {} : props.autoplay))
156
+ }
157
+
158
+ if (props.autoScroll) {
159
+ const AutoScrollPlugin = await import('embla-carousel-auto-scroll').then(r => r.default)
160
+ plugins.push(AutoScrollPlugin(typeof props.autoScroll === 'boolean' ? {} : props.autoScroll))
161
+ }
162
+
163
+ if (props.autoHeight) {
164
+ const AutoHeightPlugin = await import('embla-carousel-auto-height').then(r => r.default)
165
+ plugins.push(AutoHeightPlugin(typeof props.autoHeight === 'boolean' ? {} : props.autoHeight))
166
+ }
167
+
168
+ if (props.classNames) {
169
+ const ClassNamesPlugin = await import('embla-carousel-class-names').then(r => r.default)
170
+ plugins.push(ClassNamesPlugin(typeof props.classNames === 'boolean' ? {} : props.classNames))
171
+ }
172
+
173
+ if (props.fade) {
174
+ const FadePlugin = await import('embla-carousel-fade').then(r => r.default)
175
+ plugins.push(FadePlugin(typeof props.fade === 'boolean' ? {} : props.fade))
176
+ }
177
+
178
+ if (props.wheelGestures) {
179
+ const { WheelGesturesPlugin } = await import('embla-carousel-wheel-gestures')
180
+ plugins.push(WheelGesturesPlugin(typeof props.wheelGestures === 'boolean' ? {} : props.wheelGestures))
181
+ }
182
+
183
+ return plugins
184
+ })
185
+
186
+ const [emblaRef, emblaApi] = useEmblaCarousel(options.value, plugins.value)
187
+
188
+ watch([options, plugins], () => {
189
+ emblaApi.value?.reInit(options.value, plugins.value)
190
+ })
191
+
192
+ function scrollPrev() {
193
+ emblaApi.value?.scrollPrev()
194
+ }
195
+ function scrollNext() {
196
+ emblaApi.value?.scrollNext()
197
+ }
198
+ function scrollTo(index: number) {
199
+ emblaApi.value?.scrollTo(index)
200
+ }
201
+
202
+ function onKeyDown(event: KeyboardEvent) {
203
+ const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'
204
+ const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'
205
+
206
+ if (event.key === prevKey) {
207
+ event.preventDefault()
208
+ scrollPrev()
209
+
210
+ return
211
+ }
212
+
213
+ if (event.key === nextKey) {
214
+ event.preventDefault()
215
+ scrollNext()
216
+ }
217
+ }
218
+
219
+ const canScrollNext = ref(false)
220
+ const canScrollPrev = ref(false)
221
+ const selectedIndex = ref<number>(0)
222
+ const scrollSnaps = ref<number[]>([])
223
+
224
+ function onInit(api: EmblaCarouselType) {
225
+ scrollSnaps.value = api?.scrollSnapList() || []
226
+ }
227
+ function onSelect(api: EmblaCarouselType) {
228
+ canScrollNext.value = api?.canScrollNext() || false
229
+ canScrollPrev.value = api?.canScrollPrev() || false
230
+ selectedIndex.value = api?.selectedScrollSnap() || 0
231
+ }
232
+
233
+ onMounted(() => {
234
+ if (!emblaApi.value) {
235
+ return
236
+ }
237
+
238
+ emblaApi.value?.on('init', onInit)
239
+ emblaApi.value?.on('init', onSelect)
240
+ emblaApi.value?.on('reInit', onInit)
241
+ emblaApi.value?.on('reInit', onSelect)
242
+ emblaApi.value?.on('select', onSelect)
243
+ })
244
+
245
+ defineExpose({
246
+ emblaRef,
247
+ emblaApi
248
+ })
249
+ </script>
250
+
251
+ <template>
252
+ <div
253
+ role="region"
254
+ aria-roledescription="carousel"
255
+ tabindex="0"
256
+ :class="ui.root({ class: [props.class, props.ui?.root] })"
257
+ @keydown="onKeyDown"
258
+ >
259
+ <div ref="emblaRef" :class="ui.viewport({ class: props.ui?.viewport })">
260
+ <div :class="ui.container({ class: props.ui?.container })">
261
+ <div
262
+ v-for="(item, index) in items"
263
+ :key="index"
264
+ role="group"
265
+ aria-roledescription="slide"
266
+ :class="ui.item({ class: props.ui?.item })"
267
+ >
268
+ <slot :item="item" :index="index" />
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <div v-if="arrows || dots" :class="ui.controls({ class: props.ui?.controls })">
274
+ <div v-if="arrows" :class="ui.arrows({ class: props.ui?.arrows })">
275
+ <UButton
276
+ :disabled="!canScrollPrev"
277
+ :icon="prevIcon || appConfig.ui.icons.arrowLeft"
278
+ size="md"
279
+ color="neutral"
280
+ variant="outline"
281
+ aria-label="Prev"
282
+ v-bind="typeof prev === 'object' ? prev : undefined"
283
+ :class="ui.prev({ class: props.ui?.prev })"
284
+ @click="scrollPrev"
285
+ />
286
+ <UButton
287
+ :disabled="!canScrollNext"
288
+ :icon="nextIcon || appConfig.ui.icons.arrowRight"
289
+ size="md"
290
+ color="neutral"
291
+ variant="outline"
292
+ aria-label="Next"
293
+ v-bind="typeof next === 'object' ? next : undefined"
294
+ :class="ui.next({ class: props.ui?.next })"
295
+ @click="scrollNext"
296
+ />
297
+ </div>
298
+
299
+ <div v-if="dots" :class="ui.dots({ class: props.ui?.dots })">
300
+ <template v-for="(_, index) in scrollSnaps" :key="index">
301
+ <button :class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })" @click="scrollTo(index)" />
302
+ </template>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </template>
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import type { InputHTMLAttributes } from 'vue'
3
2
  import { tv, type VariantProps } from 'tailwind-variants'
4
3
  import type { CheckboxRootProps } from 'radix-vue'
5
4
  import type { AppConfig } from '@nuxt/schema'
@@ -22,7 +21,7 @@ export interface CheckboxProps extends Pick<CheckboxRootProps, 'disabled' | 'req
22
21
  * @defaultValue appConfig.ui.icons.check
23
22
  */
24
23
  icon?: string
25
- indeterminate?: InputHTMLAttributes['indeterminate']
24
+ indeterminate?: boolean
26
25
  /**
27
26
  * The icon displayed when the checkbox is indeterminate.
28
27
  * @defaultValue appConfig.ui.icons.minus
@@ -64,11 +63,9 @@ const appConfig = useAppConfig()
64
63
  const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled } = useFormField<CheckboxProps>(props)
65
64
  const id = _id.value ?? useId()
66
65
 
67
- const indeterminate = computed(() => (modelValue.value === undefined && props.indeterminate))
68
-
69
66
  const checked = computed({
70
67
  get() {
71
- return indeterminate.value ? 'indeterminate' : modelValue.value
68
+ return props.indeterminate ? 'indeterminate' : modelValue.value
72
69
  },
73
70
  set(value) {
74
71
  modelValue.value = value === 'indeterminate' ? undefined : value
@@ -80,7 +77,7 @@ const ui = computed(() => checkbox({
80
77
  color: color.value,
81
78
  required: props.required,
82
79
  disabled: disabled.value,
83
- checked: (modelValue.value ?? props.defaultValue) || indeterminate.value
80
+ checked: (modelValue.value ?? props.defaultValue) || props.indeterminate
84
81
  }))
85
82
 
86
83
  function onUpdate(value: any) {
@@ -1,7 +1,7 @@
1
1
  <!-- eslint-disable vue/block-tag-newline -->
2
2
  <script lang="ts">
3
3
  import { tv } from 'tailwind-variants'
4
- import type { ComboboxRootProps, ComboboxRootEmits, ComboboxItemProps } from 'radix-vue'
4
+ import type { ComboboxRootProps, ComboboxRootEmits } from 'radix-vue'
5
5
  import type { FuseResult } from 'fuse.js'
6
6
  import type { AppConfig } from '@nuxt/schema'
7
7
  import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
@@ -15,7 +15,7 @@ const appConfig = _appConfig as AppConfig & { ui: { commandPalette: Partial<type
15
15
 
16
16
  const commandPalette = tv({ extend: tv(theme), ...(appConfig.ui?.commandPalette || {}) })
17
17
 
18
- export interface CommandPaletteItem extends Pick<ComboboxItemProps, 'disabled'> {
18
+ export interface CommandPaletteItem {
19
19
  prefix?: string
20
20
  label?: string
21
21
  suffix?: string
@@ -23,8 +23,10 @@ export interface CommandPaletteItem extends Pick<ComboboxItemProps, 'disabled'>
23
23
  avatar?: AvatarProps
24
24
  chip?: ChipProps
25
25
  kbds?: KbdProps['value'][] | KbdProps[]
26
+ loading?: boolean
27
+ disabled?: boolean
26
28
  slot?: string
27
- select?(e?: Event): void
29
+ onSelect?(e?: Event): void
28
30
  }
29
31
 
30
32
  export interface CommandPaletteGroup<T> {
@@ -67,7 +69,7 @@ export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'mult
67
69
  placeholder?: InputProps['placeholder']
68
70
  /**
69
71
  * Display a close button in the input (useful when inside a Modal for example).
70
- * `{ size: 'md', color: 'gray', variant: 'ghost' }`{lang="ts-type"}
72
+ * `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
71
73
  * @defaultValue false
72
74
  */
73
75
  close?: ButtonProps | boolean
@@ -90,6 +92,11 @@ export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'mult
90
92
  }
91
93
  */
92
94
  fuse?: UseFuseOptions<T>
95
+ /**
96
+ * The key used to get the label from the item.
97
+ * @defaultValue 'label'
98
+ */
99
+ labelKey?: string
93
100
  class?: any
94
101
  ui?: PartialString<typeof commandPalette.slots>
95
102
  }
@@ -116,17 +123,18 @@ import { defu } from 'defu'
116
123
  import { reactivePick } from '@vueuse/core'
117
124
  import { useFuse } from '@vueuse/integrations/useFuse'
118
125
  import { useAppConfig } from '#imports'
126
+ import { omit, get } from '../utils'
127
+ import { highlight } from '../utils/fuse'
119
128
  import UIcon from './Icon.vue'
120
129
  import UAvatar from './Avatar.vue'
121
130
  import UChip from './Chip.vue'
122
131
  import UKbd from './Kbd.vue'
123
132
  import UInput from './Input.vue'
124
- import { omit } from '../utils'
125
- import { highlight } from '../utils/fuse'
126
133
 
127
134
  const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
128
135
  modelValue: '',
129
- placeholder: 'Type a command or search...'
136
+ placeholder: 'Type a command or search...',
137
+ labelKey: 'label'
130
138
  })
131
139
  const emits = defineEmits<CommandPaletteEmits<T>>()
132
140
  const slots = defineSlots<CommandPaletteSlots<G, T>>()
@@ -144,7 +152,7 @@ const fuse = computed(() => defu({}, props.fuse, {
144
152
  fuseOptions: {
145
153
  ignoreLocation: true,
146
154
  threshold: 0.1,
147
- keys: ['label', 'suffix']
155
+ keys: [props.labelKey, 'suffix']
148
156
  },
149
157
  resultLimit: 12,
150
158
  matchAllWhenSearchEmpty: true
@@ -175,8 +183,8 @@ function getGroupWithItems(group: G, items: (T & { matches?: FuseResult<T>['matc
175
183
  items: items.slice(0, fuse.value.resultLimit).map((item) => {
176
184
  return {
177
185
  ...item,
178
- labelHtml: highlight<T>(item, searchTerm.value, 'label'),
179
- suffixHtml: highlight<T>(item, searchTerm.value, undefined, ['label'])
186
+ labelHtml: highlight<T>(item, searchTerm.value, props.labelKey),
187
+ suffixHtml: highlight<T>(item, searchTerm.value, undefined, [props.labelKey])
180
188
  }
181
189
  })
182
190
  }
@@ -233,7 +241,7 @@ const groups = computed(() => {
233
241
  v-if="close"
234
242
  :icon="closeIcon || appConfig.ui.icons.close"
235
243
  size="md"
236
- color="gray"
244
+ color="neutral"
237
245
  variant="ghost"
238
246
  aria-label="Close"
239
247
  v-bind="typeof close === 'object' ? close : undefined"
@@ -255,22 +263,23 @@ const groups = computed(() => {
255
263
 
256
264
  <ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
257
265
  <ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
258
- <ComboboxLabel v-if="group.label" :class="ui.label({ class: props.ui?.label })">
259
- {{ group.label }}
266
+ <ComboboxLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
267
+ {{ get(group, props.labelKey as string) }}
260
268
  </ComboboxLabel>
261
269
 
262
270
  <ComboboxItem
263
271
  v-for="(item, index) in group.items"
264
272
  :key="`group-${groupIndex}-${index}`"
265
- :value="omit(item, ['matches' as any, 'group' as any, 'select', 'labelHtml', 'suffixHtml'])"
273
+ :value="omit(item, ['matches' as any, 'group' as any, 'onSelect', 'labelHtml', 'suffixHtml'])"
266
274
  :disabled="item.disabled"
267
275
  :class="ui.item({ class: props.ui?.item })"
268
- @select="item.select"
276
+ @select="item.onSelect"
269
277
  >
270
278
  <slot :name="item.slot || group.slot || 'item'" :item="item" :index="index">
271
279
  <slot :name="item.slot ? `${item.slot}-leading` : group.slot ? `${group.slot}-leading` : `item-leading`" :item="item" :index="index">
272
- <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 })" />
280
+ <UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon, loading: true })" />
273
281
  <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
282
+ <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 })" />
274
283
  <UChip
275
284
  v-else-if="item.chip"
276
285
  :size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
@@ -281,11 +290,11 @@ const groups = computed(() => {
281
290
  />
282
291
  </slot>
283
292
 
284
- <span v-if="item.label || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel })">
293
+ <span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel })">
285
294
  <slot :name="item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`" :item="item" :index="index">
286
295
  <span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
287
296
 
288
- <span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase })" v-html="item.labelHtml || item.label" />
297
+ <span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
289
298
 
290
299
  <span :class="ui.itemLabelSuffix({ class: props.ui?.itemLabelSuffix })" v-html="item.suffixHtml || item.suffix" />
291
300
  </slot>