@nuxt/ui 3.0.0-alpha.6 → 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 (41) hide show
  1. package/dist/module.d.mts +6 -6
  2. package/dist/module.d.ts +6 -6
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +391 -257
  5. package/dist/runtime/components/Accordion.vue +13 -5
  6. package/dist/runtime/components/Breadcrumb.vue +14 -6
  7. package/dist/runtime/components/Button.vue +4 -2
  8. package/dist/runtime/components/Carousel.vue +6 -6
  9. package/dist/runtime/components/Checkbox.vue +3 -6
  10. package/dist/runtime/components/CommandPalette.vue +25 -16
  11. package/dist/runtime/components/ContextMenu.vue +31 -7
  12. package/dist/runtime/components/ContextMenuContent.vue +36 -8
  13. package/dist/runtime/components/DropdownMenu.vue +31 -7
  14. package/dist/runtime/components/DropdownMenuContent.vue +38 -8
  15. package/dist/runtime/components/Form.vue +3 -1
  16. package/dist/runtime/components/Input.vue +6 -4
  17. package/dist/runtime/components/InputMenu.vue +40 -24
  18. package/dist/runtime/components/LinkBase.vue +4 -2
  19. package/dist/runtime/components/NavigationMenu.vue +78 -41
  20. package/dist/runtime/components/RadioGroup.vue +22 -3
  21. package/dist/runtime/components/Select.vue +22 -10
  22. package/dist/runtime/components/SelectMenu.vue +43 -31
  23. package/dist/runtime/components/Table.vue +232 -0
  24. package/dist/runtime/components/Tabs.vue +15 -7
  25. package/dist/runtime/components/Toaster.vue +1 -1
  26. package/dist/runtime/composables/defineShortcuts.js +1 -1
  27. package/dist/runtime/composables/useComponentIcons.d.ts +3 -0
  28. package/dist/runtime/composables/useModal.d.ts +1 -1
  29. package/dist/runtime/composables/useSlideover.d.ts +1 -1
  30. package/dist/runtime/index.css +1 -1
  31. package/dist/runtime/keyframes.css +1 -0
  32. package/dist/runtime/types/form.d.ts +2 -1
  33. package/dist/runtime/types/index.d.ts +1 -0
  34. package/dist/runtime/types/index.js +1 -0
  35. package/dist/runtime/utils/form.d.ts +3 -0
  36. package/dist/runtime/utils/form.js +14 -0
  37. package/dist/runtime/utils/index.d.ts +1 -0
  38. package/dist/runtime/utils/index.js +3 -0
  39. package/package.json +15 -12
  40. package/dist/runtime/types/component.d.ts +0 -17
  41. 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">
@@ -30,6 +30,11 @@ 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
39
  ui?: PartialString<typeof breadcrumb.slots>
35
40
  }
@@ -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>
@@ -55,32 +55,32 @@ export interface CarouselProps<T> extends Omit<EmblaOptionsType, 'axis' | 'conta
55
55
  items?: T[]
56
56
  /**
57
57
  * Enable Autoplay plugin
58
- * @see https://www.embla-carousel.com/plugins/autoplay/
58
+ * @link https://www.embla-carousel.com/plugins/autoplay/
59
59
  */
60
60
  autoplay?: boolean | AutoplayOptionsType
61
61
  /**
62
62
  * Enable Auto Scroll plugin
63
- * @see https://www.embla-carousel.com/plugins/auto-scroll/
63
+ * @link https://www.embla-carousel.com/plugins/auto-scroll/
64
64
  */
65
65
  autoScroll?: boolean | AutoScrollOptionsType
66
66
  /**
67
67
  * Enable Auto Height plugin
68
- * @see https://www.embla-carousel.com/plugins/auto-height/
68
+ * @link https://www.embla-carousel.com/plugins/auto-height/
69
69
  */
70
70
  autoHeight?: boolean | AutoHeightOptionsType
71
71
  /**
72
72
  * Enable Class Names plugin
73
- * @see https://www.embla-carousel.com/plugins/class-names/
73
+ * @link https://www.embla-carousel.com/plugins/class-names/
74
74
  */
75
75
  classNames?: boolean | ClassNamesOptionsType
76
76
  /**
77
77
  * Enable Fade plugin
78
- * @see https://www.embla-carousel.com/plugins/fade/
78
+ * @link https://www.embla-carousel.com/plugins/fade/
79
79
  */
80
80
  fade?: boolean | FadeOptionsType
81
81
  /**
82
82
  * Enable Wheel Gestures plugin
83
- * @see https://www.embla-carousel.com/plugins/wheel-gestures/
83
+ * @link https://www.embla-carousel.com/plugins/wheel-gestures/
84
84
  */
85
85
  wheelGestures?: boolean | WheelGesturesPluginOptions
86
86
  class?: any
@@ -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> {
@@ -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
  }
@@ -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>
@@ -1,7 +1,7 @@
1
1
  <!-- eslint-disable vue/block-tag-newline -->
2
2
  <script lang="ts">
3
3
  import { tv, type VariantProps } from 'tailwind-variants'
4
- import type { ContextMenuRootProps, ContextMenuRootEmits, ContextMenuContentProps, ContextMenuTriggerProps, ContextMenuItemProps } from 'radix-vue'
4
+ import type { ContextMenuRootProps, ContextMenuRootEmits, ContextMenuContentProps } 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/context-menu'
@@ -12,7 +12,7 @@ const appConfig = _appConfig as AppConfig & { ui: { contextMenu: Partial<typeof
12
12
 
13
13
  const contextMenu = tv({ extend: tv(theme), ...(appConfig.ui?.contextMenu || {}) })
14
14
 
15
- export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<ContextMenuItemProps, 'disabled'> {
15
+ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
16
16
  label?: string
17
17
  icon?: string
18
18
  avatar?: AvatarProps
@@ -22,19 +22,33 @@ export interface ContextMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custo
22
22
  * The item type.
23
23
  * @defaultValue 'link'
24
24
  */
25
- type?: 'label' | 'separator' | 'link'
25
+ type?: 'label' | 'separator' | 'link' | 'checkbox'
26
26
  slot?: string
27
+ loading?: boolean
28
+ disabled?: boolean
29
+ checked?: boolean
27
30
  open?: boolean
28
31
  defaultOpen?: boolean
29
32
  children?: ContextMenuItem[] | ContextMenuItem[][]
30
- select?(e: Event): void
33
+ onSelect?(e: Event): void
34
+ onUpdateChecked?(checked: boolean): void
31
35
  }
32
36
 
33
37
  type ContextMenuVariants = VariantProps<typeof contextMenu>
34
38
 
35
- export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'>, Pick<ContextMenuTriggerProps, 'disabled'> {
39
+ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'> {
36
40
  size?: ContextMenuVariants['size']
37
41
  items?: T[] | T[][]
42
+ /**
43
+ * The icon displayed when an item is checked.
44
+ * @defaultValue appConfig.ui.icons.check
45
+ */
46
+ checkedIcon?: string
47
+ /**
48
+ * The icon displayed when an item is loading.
49
+ * @defaultValue appConfig.ui.icons.loading
50
+ */
51
+ loadingIcon?: string
38
52
  /** The content of the menu. */
39
53
  content?: Omit<ContextMenuContentProps, 'as' | 'asChild' | 'forceMount'>
40
54
  /**
@@ -42,6 +56,12 @@ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'>,
42
56
  * @defaultValue true
43
57
  */
44
58
  portal?: boolean
59
+ /**
60
+ * The key used to get the label from the item.
61
+ * @defaultValue 'label'
62
+ */
63
+ labelKey?: string
64
+ disabled?: boolean
45
65
  class?: any
46
66
  ui?: PartialString<typeof contextMenu.slots>
47
67
  }
@@ -64,12 +84,13 @@ export type ContextMenuSlots<T extends { slot?: string }> = {
64
84
  import { computed, toRef } from 'vue'
65
85
  import { ContextMenuRoot, ContextMenuTrigger, useForwardPropsEmits } from 'radix-vue'
66
86
  import { reactivePick } from '@vueuse/core'
67
- import UContextMenuContent from './ContextMenuContent.vue'
68
87
  import { omit } from '../utils'
88
+ import UContextMenuContent from './ContextMenuContent.vue'
69
89
 
70
90
  const props = withDefaults(defineProps<ContextMenuProps<T>>(), {
71
91
  portal: true,
72
- modal: true
92
+ modal: true,
93
+ labelKey: 'label'
73
94
  })
74
95
  const emits = defineEmits<ContextMenuEmits>()
75
96
  const slots = defineSlots<ContextMenuSlots<T>>()
@@ -96,6 +117,9 @@ const ui = computed(() => contextMenu({
96
117
  v-bind="contentProps"
97
118
  :items="items"
98
119
  :portal="portal"
120
+ :label-key="labelKey"
121
+ :checked-icon="checkedIcon"
122
+ :loading-icon="loadingIcon"
99
123
  >
100
124
  <template v-for="(_, name) in proxySlots" #[name]="slotData: any">
101
125
  <slot :name="name" v-bind="slotData" />
@@ -10,6 +10,9 @@ interface ContextMenuContentProps<T> extends Omit<RadixContextMenuContentProps,
10
10
  items?: T[] | T[][]
11
11
  portal?: boolean
12
12
  sub?: boolean
13
+ labelKey: string
14
+ checkedIcon?: string
15
+ loadingIcon?: string
13
16
  class?: any
14
17
  ui: typeof _contextMenu
15
18
  uiOverride?: any
@@ -24,13 +27,13 @@ import { ContextMenu } from 'radix-vue/namespaced'
24
27
  import { useForwardPropsEmits } from 'radix-vue'
25
28
  import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
26
29
  import { useAppConfig } from '#imports'
30
+ import { omit, get } from '../utils'
31
+ import { pickLinkProps } from '../utils/link'
27
32
  import ULinkBase from './LinkBase.vue'
28
33
  import ULink from './Link.vue'
29
34
  import UAvatar from './Avatar.vue'
30
35
  import UIcon from './Icon.vue'
31
36
  import UKbd from './Kbd.vue'
32
- import { omit } from '../utils'
33
- import { pickLinkProps } from '../utils/link'
34
37
 
35
38
  const props = defineProps<ContextMenuContentProps<T>>()
36
39
  const emits = defineEmits<ContextMenuContentEmits>()
@@ -49,25 +52,30 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
49
52
  <DefineItemTemplate v-slot="{ item, active, index }">
50
53
  <slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
51
54
  <slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index">
52
- <UAvatar v-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
55
+ <UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, loading: true })" />
53
56
  <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, active })" />
57
+ <UAvatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
54
58
  </slot>
55
59
 
56
- <span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
60
+ <span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
57
61
  <slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
58
- {{ item.label }}
62
+ {{ get(item, props.labelKey as string) }}
59
63
  </slot>
60
64
 
61
65
  <UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, active })" />
62
66
  </span>
63
67
 
64
- <span v-if="item.children?.length || item.kbds?.length || !!slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
68
+ <span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
65
69
  <slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="(item as T)" :active="active" :index="index">
66
70
  <UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, active })" />
67
71
  <span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
68
72
  <UKbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
69
73
  </span>
70
74
  </slot>
75
+
76
+ <ContextMenu.ItemIndicator as-child>
77
+ <UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon })" />
78
+ </ContextMenu.ItemIndicator>
71
79
  </span>
72
80
  </slot>
73
81
  </DefineItemTemplate>
@@ -85,7 +93,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
85
93
  as="button"
86
94
  type="button"
87
95
  :disabled="item.disabled"
88
- :text-value="item.label"
96
+ :text-value="get(item, props.labelKey as string)"
89
97
  :class="ui.item({ class: uiOverride?.item })"
90
98
  >
91
99
  <ReuseItemTemplate :item="item" :index="index" />
@@ -99,6 +107,9 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
99
107
  :portal="portal"
100
108
  :items="item.children"
101
109
  :align-offset="-4"
110
+ :label-key="labelKey"
111
+ :checked-icon="checkedIcon"
112
+ :loading-icon="loadingIcon"
102
113
  v-bind="item.content"
103
114
  >
104
115
  <template v-for="(_, name) in proxySlots" #[name]="slotData: any">
@@ -106,7 +117,24 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
106
117
  </template>
107
118
  </UContextMenuContent>
108
119
  </ContextMenu.Sub>
109
- <ContextMenu.Item v-else as-child :disabled="item.disabled" :text-value="item.label" @select="item.select">
120
+ <ContextMenu.CheckboxItem
121
+ v-else-if="item.type === 'checkbox'"
122
+ :checked="item.checked"
123
+ :disabled="item.disabled"
124
+ :text-value="get(item, props.labelKey as string)"
125
+ :class="ui.item({ class: [uiOverride?.item, item.class] })"
126
+ @update:checked="item.onUpdateChecked"
127
+ @select="item.onSelect"
128
+ >
129
+ <ReuseItemTemplate :item="item" :index="index" />
130
+ </ContextMenu.CheckboxItem>
131
+ <ContextMenu.Item
132
+ v-else
133
+ as-child
134
+ :disabled="item.disabled"
135
+ :text-value="get(item, props.labelKey as string)"
136
+ @select="item.onSelect"
137
+ >
110
138
  <ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" custom>
111
139
  <ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.class], active })">
112
140
  <ReuseItemTemplate :item="item" :active="active" :index="index" />
@@ -1,7 +1,7 @@
1
1
  <!-- eslint-disable vue/block-tag-newline -->
2
2
  <script lang="ts">
3
3
  import { tv, type VariantProps } from 'tailwind-variants'
4
- import type { DropdownMenuRootProps, DropdownMenuRootEmits, DropdownMenuContentProps, DropdownMenuArrowProps, DropdownMenuTriggerProps, DropdownMenuItemProps } from 'radix-vue'
4
+ import type { DropdownMenuRootProps, DropdownMenuRootEmits, DropdownMenuContentProps, DropdownMenuArrowProps } 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/dropdown-menu'
@@ -12,7 +12,7 @@ const appConfig = _appConfig as AppConfig & { ui: { dropdownMenu: Partial<typeof
12
12
 
13
13
  const dropdownMenu = tv({ extend: tv(theme), ...(appConfig.ui?.dropdownMenu || {}) })
14
14
 
15
- export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<DropdownMenuItemProps, 'disabled'> {
15
+ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'> {
16
16
  label?: string
17
17
  icon?: string
18
18
  avatar?: AvatarProps
@@ -22,19 +22,33 @@ export interface DropdownMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cust
22
22
  * The item type.
23
23
  * @defaultValue 'link'
24
24
  */
25
- type?: 'label' | 'separator' | 'link'
25
+ type?: 'label' | 'separator' | 'link' | 'checkbox'
26
26
  slot?: string
27
+ loading?: boolean
28
+ disabled?: boolean
29
+ checked?: boolean
27
30
  open?: boolean
28
31
  defaultOpen?: boolean
29
32
  children?: DropdownMenuItem[] | DropdownMenuItem[][]
30
- select?(e: Event): void
33
+ onSelect?(e: Event): void
34
+ onUpdateChecked?(checked: boolean): void
31
35
  }
32
36
 
33
37
  type DropdownMenuVariants = VariantProps<typeof dropdownMenu>
34
38
 
35
- export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>, Pick<DropdownMenuTriggerProps, 'disabled'> {
39
+ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'> {
36
40
  size?: DropdownMenuVariants['size']
37
41
  items?: T[] | T[][]
42
+ /**
43
+ * The icon displayed when an item is checked.
44
+ * @defaultValue appConfig.ui.icons.check
45
+ */
46
+ checkedIcon?: string
47
+ /**
48
+ * The icon displayed when an item is loading.
49
+ * @defaultValue appConfig.ui.icons.loading
50
+ */
51
+ loadingIcon?: string
38
52
  /**
39
53
  * The content of the menu.
40
54
  * @defaultValue { side: 'bottom', sideOffset: 8 }
@@ -50,6 +64,12 @@ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>
50
64
  * @defaultValue true
51
65
  */
52
66
  portal?: boolean
67
+ /**
68
+ * The key used to get the label from the item.
69
+ * @defaultValue 'label'
70
+ */
71
+ labelKey?: string
72
+ disabled?: boolean
53
73
  class?: any
54
74
  ui?: PartialString<typeof dropdownMenu.slots>
55
75
  }
@@ -73,12 +93,13 @@ import { computed, toRef } from 'vue'
73
93
  import { defu } from 'defu'
74
94
  import { DropdownMenuRoot, DropdownMenuTrigger, DropdownMenuArrow, useForwardPropsEmits } from 'radix-vue'
75
95
  import { reactivePick } from '@vueuse/core'
76
- import UDropdownMenuContent from './DropdownMenuContent.vue'
77
96
  import { omit } from '../utils'
97
+ import UDropdownMenuContent from './DropdownMenuContent.vue'
78
98
 
79
99
  const props = withDefaults(defineProps<DropdownMenuProps<T>>(), {
80
100
  portal: true,
81
- modal: true
101
+ modal: true,
102
+ labelKey: 'label'
82
103
  })
83
104
  const emits = defineEmits<DropdownMenuEmits>()
84
105
  const slots = defineSlots<DropdownMenuSlots<T>>()
@@ -106,6 +127,9 @@ const ui = computed(() => dropdownMenu({
106
127
  v-bind="contentProps"
107
128
  :items="items"
108
129
  :portal="portal"
130
+ :label-key="labelKey"
131
+ :checked-icon="checkedIcon"
132
+ :loading-icon="loadingIcon"
109
133
  >
110
134
  <template v-for="(_, name) in proxySlots" #[name]="slotData: any">
111
135
  <slot :name="name" v-bind="slotData" />