@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.
- package/dist/module.d.mts +6 -6
- package/dist/module.d.ts +6 -6
- package/dist/module.json +1 -1
- package/dist/module.mjs +391 -257
- package/dist/runtime/components/Accordion.vue +13 -5
- package/dist/runtime/components/Breadcrumb.vue +14 -6
- package/dist/runtime/components/Button.vue +4 -2
- package/dist/runtime/components/Carousel.vue +6 -6
- package/dist/runtime/components/Checkbox.vue +3 -6
- package/dist/runtime/components/CommandPalette.vue +25 -16
- package/dist/runtime/components/ContextMenu.vue +31 -7
- package/dist/runtime/components/ContextMenuContent.vue +36 -8
- package/dist/runtime/components/DropdownMenu.vue +31 -7
- package/dist/runtime/components/DropdownMenuContent.vue +38 -8
- package/dist/runtime/components/Form.vue +3 -1
- package/dist/runtime/components/Input.vue +6 -4
- package/dist/runtime/components/InputMenu.vue +40 -24
- package/dist/runtime/components/LinkBase.vue +4 -2
- package/dist/runtime/components/NavigationMenu.vue +78 -41
- package/dist/runtime/components/RadioGroup.vue +22 -3
- package/dist/runtime/components/Select.vue +22 -10
- package/dist/runtime/components/SelectMenu.vue +43 -31
- package/dist/runtime/components/Table.vue +232 -0
- package/dist/runtime/components/Tabs.vue +15 -7
- package/dist/runtime/components/Toaster.vue +1 -1
- package/dist/runtime/composables/defineShortcuts.js +1 -1
- package/dist/runtime/composables/useComponentIcons.d.ts +3 -0
- package/dist/runtime/composables/useModal.d.ts +1 -1
- package/dist/runtime/composables/useSlideover.d.ts +1 -1
- package/dist/runtime/index.css +1 -1
- package/dist/runtime/keyframes.css +1 -0
- package/dist/runtime/types/form.d.ts +2 -1
- package/dist/runtime/types/index.d.ts +1 -0
- package/dist/runtime/types/index.js +1 -0
- package/dist/runtime/utils/form.d.ts +3 -0
- package/dist/runtime/utils/form.js +14 -0
- package/dist/runtime/utils/index.d.ts +1 -0
- package/dist/runtime/utils/index.js +3 -0
- package/package.json +15 -12
- package/dist/runtime/types/component.d.ts +0 -17
- package/dist/runtime/types/component.js +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<!-- eslint-disable vue/block-tag-newline -->
|
|
1
2
|
<script lang="ts">
|
|
2
3
|
import { tv } from 'tailwind-variants'
|
|
3
4
|
import type { DropdownMenuContentProps as RadixDropdownMenuContentProps, DropdownMenuContentEmits as RadixDropdownMenuContentEmits } from 'radix-vue'
|
|
@@ -10,6 +11,9 @@ interface DropdownMenuContentProps<T> extends Omit<RadixDropdownMenuContentProps
|
|
|
10
11
|
items?: T[] | T[][]
|
|
11
12
|
portal?: boolean
|
|
12
13
|
sub?: boolean
|
|
14
|
+
labelKey: string
|
|
15
|
+
checkedIcon?: string
|
|
16
|
+
loadingIcon?: string
|
|
13
17
|
class?: any
|
|
14
18
|
ui: typeof _dropdownMenu
|
|
15
19
|
uiOverride?: any
|
|
@@ -20,6 +24,7 @@ interface DropdownMenuContentEmits extends RadixDropdownMenuContentEmits {}
|
|
|
20
24
|
type DropdownMenuContentSlots<T extends { slot?: string }> = Omit<DropdownMenuSlots<T>, 'default'> & {
|
|
21
25
|
default(props?: {}): any
|
|
22
26
|
}
|
|
27
|
+
|
|
23
28
|
</script>
|
|
24
29
|
|
|
25
30
|
<script setup lang="ts" generic="T extends DropdownMenuItem">
|
|
@@ -28,13 +33,13 @@ import { DropdownMenu } from 'radix-vue/namespaced'
|
|
|
28
33
|
import { useForwardPropsEmits } from 'radix-vue'
|
|
29
34
|
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
|
|
30
35
|
import { useAppConfig } from '#imports'
|
|
36
|
+
import { omit, get } from '../utils'
|
|
37
|
+
import { pickLinkProps } from '../utils/link'
|
|
31
38
|
import ULinkBase from './LinkBase.vue'
|
|
32
39
|
import ULink from './Link.vue'
|
|
33
40
|
import UAvatar from './Avatar.vue'
|
|
34
41
|
import UIcon from './Icon.vue'
|
|
35
42
|
import UKbd from './Kbd.vue'
|
|
36
|
-
import { omit } from '../utils'
|
|
37
|
-
import { pickLinkProps } from '../utils/link'
|
|
38
43
|
|
|
39
44
|
const props = defineProps<DropdownMenuContentProps<T>>()
|
|
40
45
|
const emits = defineEmits<DropdownMenuContentEmits>()
|
|
@@ -53,25 +58,30 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|
|
53
58
|
<DefineItemTemplate v-slot="{ item, active, index }">
|
|
54
59
|
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
|
55
60
|
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
|
56
|
-
<
|
|
61
|
+
<UIcon v-if="item.loading" :name="loadingIcon || appConfig.ui.icons.loading" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, loading: true })" />
|
|
57
62
|
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, active })" />
|
|
63
|
+
<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 })" />
|
|
58
64
|
</slot>
|
|
59
65
|
|
|
60
|
-
<span v-if="item.
|
|
66
|
+
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
|
|
61
67
|
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
|
|
62
|
-
{{ item.
|
|
68
|
+
{{ get(item, props.labelKey as string) }}
|
|
63
69
|
</slot>
|
|
64
70
|
|
|
65
71
|
<UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, active })" />
|
|
66
72
|
</span>
|
|
67
73
|
|
|
68
|
-
<span
|
|
74
|
+
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
|
|
69
75
|
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
|
70
76
|
<UIcon v-if="item.children?.length" :name="appConfig.ui.icons.chevronRight" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, active })" />
|
|
71
77
|
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
|
|
72
78
|
<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" />
|
|
73
79
|
</span>
|
|
74
80
|
</slot>
|
|
81
|
+
|
|
82
|
+
<DropdownMenu.ItemIndicator as-child>
|
|
83
|
+
<UIcon :name="checkedIcon || appConfig.ui.icons.check" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon })" />
|
|
84
|
+
</DropdownMenu.ItemIndicator>
|
|
75
85
|
</span>
|
|
76
86
|
</slot>
|
|
77
87
|
</DefineItemTemplate>
|
|
@@ -89,7 +99,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|
|
89
99
|
as="button"
|
|
90
100
|
type="button"
|
|
91
101
|
:disabled="item.disabled"
|
|
92
|
-
:text-value="item.
|
|
102
|
+
:text-value="get(item, props.labelKey as string)"
|
|
93
103
|
:class="ui.item({ class: uiOverride?.item })"
|
|
94
104
|
>
|
|
95
105
|
<ReuseItemTemplate :item="item" :index="index" />
|
|
@@ -106,6 +116,9 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|
|
106
116
|
align="start"
|
|
107
117
|
:align-offset="-4"
|
|
108
118
|
:side-offset="3"
|
|
119
|
+
:label-key="labelKey"
|
|
120
|
+
:checked-icon="checkedIcon"
|
|
121
|
+
:loading-icon="loadingIcon"
|
|
109
122
|
v-bind="item.content"
|
|
110
123
|
>
|
|
111
124
|
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
|
|
@@ -113,7 +126,24 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|
|
113
126
|
</template>
|
|
114
127
|
</UDropdownMenuContent>
|
|
115
128
|
</DropdownMenu.Sub>
|
|
116
|
-
<DropdownMenu.
|
|
129
|
+
<DropdownMenu.CheckboxItem
|
|
130
|
+
v-else-if="item.type === 'checkbox'"
|
|
131
|
+
:checked="item.checked"
|
|
132
|
+
:disabled="item.disabled"
|
|
133
|
+
:text-value="get(item, props.labelKey as string)"
|
|
134
|
+
:class="ui.item({ class: [uiOverride?.item, item.class] })"
|
|
135
|
+
@update:checked="item.onUpdateChecked"
|
|
136
|
+
@select="item.onSelect"
|
|
137
|
+
>
|
|
138
|
+
<ReuseItemTemplate :item="item" :index="index" />
|
|
139
|
+
</DropdownMenu.CheckboxItem>
|
|
140
|
+
<DropdownMenu.Item
|
|
141
|
+
v-else
|
|
142
|
+
as-child
|
|
143
|
+
:disabled="item.disabled"
|
|
144
|
+
:text-value="get(item, props.labelKey as string)"
|
|
145
|
+
@select="item.onSelect"
|
|
146
|
+
>
|
|
117
147
|
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
|
|
118
148
|
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.class], active })">
|
|
119
149
|
<ReuseItemTemplate :item="item" :active="active" :index="index" />
|
|
@@ -35,7 +35,7 @@ export interface FormSlots {
|
|
|
35
35
|
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
|
|
36
36
|
import { useEventBus } from '@vueuse/core'
|
|
37
37
|
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
|
|
38
|
-
import { getYupErrors, isYupSchema, getValibotErrors, isValibotSchema, getZodErrors, isZodSchema, getJoiErrors, isJoiSchema, getStandardErrors, isStandardSchema } from '../utils/form'
|
|
38
|
+
import { getYupErrors, isYupSchema, getValibotErrors, isValibotSchema, getZodErrors, isZodSchema, getJoiErrors, isJoiSchema, getStandardErrors, isStandardSchema, getSuperStructErrors, isSuperStructSchema } from '../utils/form'
|
|
39
39
|
import { FormValidationException } from '../types/form'
|
|
40
40
|
|
|
41
41
|
const props = withDefaults(defineProps<FormProps<T>>(), {
|
|
@@ -113,6 +113,8 @@ async function getErrors(): Promise<FormErrorWithId[]> {
|
|
|
113
113
|
errs = errs.concat(await getJoiErrors(props.state, props.schema))
|
|
114
114
|
} else if (isValibotSchema(props.schema)) {
|
|
115
115
|
errs = errs.concat(await getValibotErrors(props.state, props.schema))
|
|
116
|
+
} else if (isSuperStructSchema(props.schema)) {
|
|
117
|
+
errs = errs.concat(await getSuperStructErrors(props.state, props.schema))
|
|
116
118
|
} else if (isStandardSchema(props.schema)) {
|
|
117
119
|
errs = errs.concat(await getStandardErrors(props.state, props.schema))
|
|
118
120
|
} else {
|
|
@@ -5,6 +5,7 @@ import type { AppConfig } from '@nuxt/schema'
|
|
|
5
5
|
import _appConfig from '#build/app.config'
|
|
6
6
|
import theme from '#build/ui/input'
|
|
7
7
|
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
|
8
|
+
import type { AvatarProps } from '../types'
|
|
8
9
|
import type { PartialString } from '../types/utils'
|
|
9
10
|
|
|
10
11
|
const appConfig = _appConfig as AppConfig & { ui: { input: Partial<typeof theme> } }
|
|
@@ -51,8 +52,8 @@ import { ref, computed, onMounted } from 'vue'
|
|
|
51
52
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
52
53
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
53
54
|
import { useFormField } from '../composables/useFormField'
|
|
54
|
-
import UIcon from './Icon.vue'
|
|
55
55
|
import { looseToNumber } from '../utils'
|
|
56
|
+
import UIcon from './Icon.vue'
|
|
56
57
|
|
|
57
58
|
defineOptions({ inheritAttrs: false })
|
|
58
59
|
|
|
@@ -79,7 +80,7 @@ const ui = computed(() => input({
|
|
|
79
80
|
size: inputSize?.value,
|
|
80
81
|
loading: props.loading,
|
|
81
82
|
highlight: highlight.value,
|
|
82
|
-
leading: isLeading.value || !!slots.leading,
|
|
83
|
+
leading: isLeading.value || !!props.avatar || !!slots.leading,
|
|
83
84
|
trailing: isTrailing.value || !!slots.trailing,
|
|
84
85
|
buttonGroup: orientation.value
|
|
85
86
|
}))
|
|
@@ -165,9 +166,10 @@ onMounted(() => {
|
|
|
165
166
|
|
|
166
167
|
<slot />
|
|
167
168
|
|
|
168
|
-
<span v-if="isLeading || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
169
|
+
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
169
170
|
<slot name="leading">
|
|
170
|
-
<UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
171
|
+
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
172
|
+
<UAvatar v-else-if="!!avatar" :size="((props.ui?.leadingAvatarSize || ui.leadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.leadingAvatar({ class: props.ui?.leadingAvatar })" />
|
|
171
173
|
</slot>
|
|
172
174
|
</span>
|
|
173
175
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { InputHTMLAttributes } from 'vue'
|
|
3
3
|
import { tv, type VariantProps } from 'tailwind-variants'
|
|
4
|
-
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps,
|
|
4
|
+
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxArrowProps } from 'radix-vue'
|
|
5
5
|
import type { AppConfig } from '@nuxt/schema'
|
|
6
6
|
import _appConfig from '#build/app.config'
|
|
7
7
|
import theme from '#build/ui/input-menu'
|
|
@@ -13,7 +13,7 @@ const appConfig = _appConfig as AppConfig & { ui: { inputMenu: Partial<typeof th
|
|
|
13
13
|
|
|
14
14
|
const inputMenu = tv({ extend: tv(theme), ...(appConfig.ui?.inputMenu || {}) })
|
|
15
15
|
|
|
16
|
-
export interface InputMenuItem
|
|
16
|
+
export interface InputMenuItem {
|
|
17
17
|
label?: string
|
|
18
18
|
icon?: string
|
|
19
19
|
avatar?: AvatarProps
|
|
@@ -23,7 +23,8 @@ export interface InputMenuItem extends Pick<ComboboxItemProps, 'disabled'> {
|
|
|
23
23
|
* @defaultValue 'item'
|
|
24
24
|
*/
|
|
25
25
|
type?: 'label' | 'separator' | 'item'
|
|
26
|
-
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
onSelect?(e?: Event): void
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
type InputMenuVariants = VariantProps<typeof inputMenu>
|
|
@@ -86,6 +87,11 @@ export interface InputMenuProps<T> extends Pick<ComboboxRootProps<T>, 'modelValu
|
|
|
86
87
|
* @defaultValue undefined
|
|
87
88
|
*/
|
|
88
89
|
valueKey?: keyof T
|
|
90
|
+
/**
|
|
91
|
+
* When `items` is an array of objects, select the field to use as the label.
|
|
92
|
+
* @defaultValue 'label'
|
|
93
|
+
*/
|
|
94
|
+
labelKey?: keyof T
|
|
89
95
|
items?: T[] | T[][]
|
|
90
96
|
/** Highlight the ring color like a focus state. */
|
|
91
97
|
highlight?: boolean
|
|
@@ -118,15 +124,16 @@ export interface InputMenuSlots<T> {
|
|
|
118
124
|
import { computed, ref, toRef, onMounted } from 'vue'
|
|
119
125
|
import { ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits } from 'radix-vue'
|
|
120
126
|
import { defu } from 'defu'
|
|
127
|
+
import isEqual from 'fast-deep-equal'
|
|
121
128
|
import { reactivePick } from '@vueuse/core'
|
|
122
129
|
import { useAppConfig } from '#imports'
|
|
123
130
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
124
131
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
125
132
|
import { useFormField } from '../composables/useFormField'
|
|
133
|
+
import { get, escapeRegExp } from '../utils'
|
|
126
134
|
import UIcon from './Icon.vue'
|
|
127
135
|
import UAvatar from './Avatar.vue'
|
|
128
136
|
import UChip from './Chip.vue'
|
|
129
|
-
import { get } from '../utils'
|
|
130
137
|
|
|
131
138
|
defineOptions({ inheritAttrs: false })
|
|
132
139
|
|
|
@@ -134,7 +141,8 @@ const props = withDefaults(defineProps<InputMenuProps<T>>(), {
|
|
|
134
141
|
type: 'text',
|
|
135
142
|
autofocusDelay: 0,
|
|
136
143
|
portal: true,
|
|
137
|
-
filter: () => ['label']
|
|
144
|
+
filter: () => ['label'],
|
|
145
|
+
labelKey: 'label' as keyof T
|
|
138
146
|
})
|
|
139
147
|
const emits = defineEmits<InputMenuEmits<T>>()
|
|
140
148
|
const slots = defineSlots<InputMenuSlots<T>>()
|
|
@@ -156,18 +164,16 @@ const ui = computed(() => inputMenu({
|
|
|
156
164
|
size: inputSize?.value,
|
|
157
165
|
loading: props.loading,
|
|
158
166
|
highlight: highlight.value,
|
|
159
|
-
leading: isLeading.value || !!slots.leading,
|
|
167
|
+
leading: isLeading.value || !!props.avatar || !!slots.leading,
|
|
160
168
|
trailing: isTrailing.value || !!slots.trailing,
|
|
161
169
|
multiple: props.multiple,
|
|
162
170
|
buttonGroup: orientation.value
|
|
163
171
|
}))
|
|
164
172
|
|
|
165
|
-
function displayValue(
|
|
166
|
-
|
|
167
|
-
return val.label
|
|
168
|
-
}
|
|
173
|
+
function displayValue(value: AcceptableValue): string {
|
|
174
|
+
const item = items.value.find(item => props.valueKey ? isEqual(get(item as Record<string, any>, props.valueKey as string), value) : isEqual(item, value))
|
|
169
175
|
|
|
170
|
-
return
|
|
176
|
+
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: string): ArrayOrWrapped<AcceptableValue> {
|
|
@@ -175,22 +181,25 @@ function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: stri
|
|
|
175
181
|
return items
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
const fields = Array.isArray(props.filter) ? props.filter : [
|
|
184
|
+
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
|
|
185
|
+
const escapedSearchTerm = escapeRegExp(searchTerm)
|
|
179
186
|
|
|
180
187
|
return items.filter((item) => {
|
|
181
188
|
if (typeof item !== 'object') {
|
|
182
|
-
return String(item).search(new RegExp(
|
|
189
|
+
return String(item).search(new RegExp(escapedSearchTerm, 'i')) !== -1
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
return fields.some((field) => {
|
|
186
|
-
const child = get(item, field)
|
|
193
|
+
const child = get(item, field as string)
|
|
187
194
|
|
|
188
|
-
return child !== null && child !== undefined && String(child).search(new RegExp(
|
|
195
|
+
return child !== null && child !== undefined && String(child).search(new RegExp(escapedSearchTerm, 'i')) !== -1
|
|
189
196
|
})
|
|
190
197
|
}) as ArrayOrWrapped<T>
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as InputMenuItem[][] : [])
|
|
201
|
+
// eslint-disable-next-line vue/no-dupe-keys
|
|
202
|
+
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
|
194
203
|
|
|
195
204
|
const inputRef = ref<InstanceType<typeof ComboboxInput> | null>(null)
|
|
196
205
|
|
|
@@ -219,6 +228,10 @@ function onBlur(event: FocusEvent) {
|
|
|
219
228
|
emitFormBlur()
|
|
220
229
|
}
|
|
221
230
|
|
|
231
|
+
function onFocus(event: FocusEvent) {
|
|
232
|
+
emits('focus', event)
|
|
233
|
+
}
|
|
234
|
+
|
|
222
235
|
function onUpdateOpen(value: boolean) {
|
|
223
236
|
if (!value) {
|
|
224
237
|
const event = new FocusEvent('blur')
|
|
@@ -260,11 +273,12 @@ defineExpose({
|
|
|
260
273
|
delimiter=""
|
|
261
274
|
as-child
|
|
262
275
|
@blur="onBlur"
|
|
276
|
+
@focus="onFocus"
|
|
263
277
|
>
|
|
264
278
|
<TagsInputItem v-for="(item, index) in tags" :key="index" :value="(item as string)" :class="ui.tagsItem({ class: props.ui?.tagsItem })">
|
|
265
279
|
<TagsInputItemText :class="ui.tagsItemText({ class: props.ui?.tagsItemText })">
|
|
266
280
|
<slot name="tags-item-text" :item="(item as T)" :index="index">
|
|
267
|
-
{{
|
|
281
|
+
{{ displayValue(item as T) }}
|
|
268
282
|
</slot>
|
|
269
283
|
</TagsInputItemText>
|
|
270
284
|
|
|
@@ -296,11 +310,13 @@ defineExpose({
|
|
|
296
310
|
:required="required"
|
|
297
311
|
:class="ui.base({ class: props.ui?.base })"
|
|
298
312
|
@blur="onBlur"
|
|
313
|
+
@focus="onFocus"
|
|
299
314
|
/>
|
|
300
315
|
|
|
301
|
-
<span v-if="isLeading || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
316
|
+
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
302
317
|
<slot name="leading" :model-value="(modelValue as T)" :open="open" :ui="ui">
|
|
303
|
-
<UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
318
|
+
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
319
|
+
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
|
304
320
|
</slot>
|
|
305
321
|
</span>
|
|
306
322
|
|
|
@@ -323,7 +339,7 @@ defineExpose({
|
|
|
323
339
|
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
|
324
340
|
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
|
325
341
|
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
|
326
|
-
{{ item.
|
|
342
|
+
{{ get(item, props.labelKey as string) }}
|
|
327
343
|
</ComboboxLabel>
|
|
328
344
|
|
|
329
345
|
<ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
|
@@ -332,13 +348,13 @@ defineExpose({
|
|
|
332
348
|
v-else
|
|
333
349
|
:class="ui.item({ class: props.ui?.item })"
|
|
334
350
|
:disabled="item.disabled"
|
|
335
|
-
:value="valueKey && typeof item === 'object' ? (item
|
|
336
|
-
@select="item.
|
|
351
|
+
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
|
352
|
+
@select="item.onSelect"
|
|
337
353
|
>
|
|
338
354
|
<slot name="item" :item="(item as T)" :index="index">
|
|
339
355
|
<slot name="item-leading" :item="(item as T)" :index="index">
|
|
340
|
-
<
|
|
341
|
-
<
|
|
356
|
+
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
|
357
|
+
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
|
342
358
|
<UChip
|
|
343
359
|
v-else-if="item.chip"
|
|
344
360
|
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
|
@@ -351,7 +367,7 @@ defineExpose({
|
|
|
351
367
|
|
|
352
368
|
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
|
353
369
|
<slot name="item-label" :item="(item as T)" :index="index">
|
|
354
|
-
{{
|
|
370
|
+
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
|
355
371
|
</slot>
|
|
356
372
|
</span>
|
|
357
373
|
|
|
@@ -3,7 +3,7 @@ export interface LinkBaseProps {
|
|
|
3
3
|
as?: string
|
|
4
4
|
type?: string
|
|
5
5
|
disabled?: boolean
|
|
6
|
-
onClick?: (e: MouseEvent) => void | Promise<void>
|
|
6
|
+
onClick?: ((e: MouseEvent) => void | Promise<void>) | Array<((e: MouseEvent) => void | Promise<void>)>
|
|
7
7
|
href?: string
|
|
8
8
|
navigate?: (e: MouseEvent) => void
|
|
9
9
|
rel?: string
|
|
@@ -28,7 +28,9 @@ function onClickWrapper(e: MouseEvent) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if (props.onClick) {
|
|
31
|
-
props.onClick
|
|
31
|
+
for (const onClick of Array.isArray(props.onClick) ? props.onClick : [props.onClick]) {
|
|
32
|
+
onClick(e)
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
if (props.href && props.navigate && !props.isExternal) {
|
|
@@ -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 { NavigationMenuRootProps, NavigationMenuRootEmits,
|
|
4
|
+
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps } from 'radix-vue'
|
|
5
5
|
import type { AppConfig } from '@nuxt/schema'
|
|
6
6
|
import _appConfig from '#build/app.config'
|
|
7
7
|
import theme from '#build/ui/navigation-menu'
|
|
@@ -16,18 +16,20 @@ export interface NavigationMenuChildItem extends Omit<LinkProps, 'raw' | 'custom
|
|
|
16
16
|
label: string
|
|
17
17
|
description?: string
|
|
18
18
|
icon?: string
|
|
19
|
-
|
|
19
|
+
badge?: string | number | BadgeProps
|
|
20
|
+
onSelect?(e: Event): void
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'
|
|
23
|
+
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'> {
|
|
23
24
|
label?: string
|
|
24
25
|
icon?: string
|
|
25
26
|
avatar?: AvatarProps
|
|
26
27
|
badge?: string | number | BadgeProps
|
|
27
28
|
trailingIcon?: string
|
|
28
29
|
slot?: string
|
|
30
|
+
value?: string
|
|
29
31
|
children?: NavigationMenuChildItem[]
|
|
30
|
-
|
|
32
|
+
onSelect?(e: Event): void
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
type NavigationMenuVariants = VariantProps<typeof navigationMenu>
|
|
@@ -61,6 +63,11 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'd
|
|
|
61
63
|
* @defaultValue false
|
|
62
64
|
*/
|
|
63
65
|
arrow?: boolean
|
|
66
|
+
/**
|
|
67
|
+
* The key used to get the label from the item.
|
|
68
|
+
* @defaultValue 'label'
|
|
69
|
+
*/
|
|
70
|
+
labelKey?: string
|
|
64
71
|
class?: any
|
|
65
72
|
ui?: PartialString<typeof navigationMenu.slots>
|
|
66
73
|
}
|
|
@@ -81,17 +88,20 @@ export type NavigationMenuSlots<T extends { slot?: string }> = {
|
|
|
81
88
|
<script setup lang="ts" generic="T extends NavigationMenuItem">
|
|
82
89
|
import { computed, toRef } from 'vue'
|
|
83
90
|
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'radix-vue'
|
|
84
|
-
import { reactivePick } from '@vueuse/core'
|
|
91
|
+
import { reactivePick, createReusableTemplate } from '@vueuse/core'
|
|
92
|
+
import { get } from '../utils'
|
|
93
|
+
import { pickLinkProps } from '../utils/link'
|
|
85
94
|
import ULinkBase from './LinkBase.vue'
|
|
86
95
|
import ULink from './Link.vue'
|
|
87
96
|
import UAvatar from './Avatar.vue'
|
|
88
97
|
import UIcon from './Icon.vue'
|
|
89
98
|
import UBadge from './Badge.vue'
|
|
90
|
-
import
|
|
99
|
+
import UCollapsible from './Collapsible.vue'
|
|
91
100
|
|
|
92
101
|
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
|
|
93
102
|
orientation: 'horizontal',
|
|
94
|
-
delayDuration: 0
|
|
103
|
+
delayDuration: 0,
|
|
104
|
+
labelKey: 'label'
|
|
95
105
|
})
|
|
96
106
|
const emits = defineEmits<NavigationMenuEmits>()
|
|
97
107
|
const slots = defineSlots<NavigationMenuSlots<T>>()
|
|
@@ -99,6 +109,8 @@ const slots = defineSlots<NavigationMenuSlots<T>>()
|
|
|
99
109
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'delayDuration', 'skipDelayDuration', 'orientation'), emits)
|
|
100
110
|
const contentProps = toRef(() => props.content)
|
|
101
111
|
|
|
112
|
+
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, active?: boolean, index: number }>()
|
|
113
|
+
|
|
102
114
|
const ui = computed(() => navigationMenu({
|
|
103
115
|
orientation: props.orientation,
|
|
104
116
|
color: props.color,
|
|
@@ -111,47 +123,58 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
|
|
111
123
|
</script>
|
|
112
124
|
|
|
113
125
|
<template>
|
|
126
|
+
<DefineItemTemplate v-slot="{ item, active, index }">
|
|
127
|
+
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
|
128
|
+
<slot :name="item.slot ? `${item.slot}-leading` : 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
|
129
|
+
<UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
|
|
130
|
+
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
|
|
131
|
+
</slot>
|
|
132
|
+
|
|
133
|
+
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
|
134
|
+
<slot :name="item.slot ? `${item.slot}-label` : 'item-label'" :item="(item as T)" :active="active" :index="index">
|
|
135
|
+
{{ get(item, props.labelKey as string) }}
|
|
136
|
+
</slot>
|
|
137
|
+
|
|
138
|
+
<UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
|
|
139
|
+
</span>
|
|
140
|
+
|
|
141
|
+
<span v-if="item.badge || item.children?.length || !!slots[item.slot ? `${item.slot}-trailing` : 'item-trailing']" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
|
142
|
+
<slot :name="item.slot ? `${item.slot}-trailing` : 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
|
143
|
+
<UBadge
|
|
144
|
+
v-if="item.badge"
|
|
145
|
+
color="neutral"
|
|
146
|
+
variant="outline"
|
|
147
|
+
:size="((props.ui?.linkTrailingBadgeSize || ui.linkTrailingBadgeSize()) as BadgeProps['size'])"
|
|
148
|
+
v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
|
|
149
|
+
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
|
|
150
|
+
/>
|
|
151
|
+
<UIcon v-if="item.children?.length" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
|
152
|
+
</slot>
|
|
153
|
+
</span>
|
|
154
|
+
</slot>
|
|
155
|
+
</DefineItemTemplate>
|
|
156
|
+
|
|
114
157
|
<NavigationMenuRoot v-bind="rootProps" :class="ui.root({ class: [props.class, props.ui?.root] })">
|
|
115
158
|
<template v-for="(list, listIndex) in lists" :key="`list-${listIndex}`">
|
|
116
159
|
<NavigationMenuList :class="ui.list({ class: props.ui?.list })">
|
|
117
|
-
<
|
|
160
|
+
<component
|
|
161
|
+
:is="(item.children?.length && orientation === 'vertical') ? UCollapsible : NavigationMenuItem"
|
|
162
|
+
v-for="(item, index) in list"
|
|
163
|
+
:key="`list-${listIndex}-${index}`"
|
|
164
|
+
as="li"
|
|
165
|
+
:value="item.value || String(index)"
|
|
166
|
+
:class="ui.item({ class: props.ui?.item })"
|
|
167
|
+
>
|
|
118
168
|
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
|
119
169
|
<component
|
|
120
|
-
:is="item.children?.length ? NavigationMenuTrigger : NavigationMenuLink"
|
|
170
|
+
:is="(item.children?.length && orientation === 'horizontal') ? NavigationMenuTrigger : NavigationMenuLink"
|
|
121
171
|
v-bind="item.children?.length ? { disabled: item.disabled } : { active }"
|
|
122
172
|
as-child
|
|
123
173
|
:active="active"
|
|
124
|
-
@select="item.
|
|
174
|
+
@select="item.onSelect"
|
|
125
175
|
>
|
|
126
176
|
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active, disabled: !!item.disabled })">
|
|
127
|
-
<
|
|
128
|
-
<slot :name="item.slot ? `${item.slot}-leading`: 'item-leading'" :item="item" :active="active" :index="index">
|
|
129
|
-
<UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
|
|
130
|
-
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
|
|
131
|
-
</slot>
|
|
132
|
-
|
|
133
|
-
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
|
|
134
|
-
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="active" :index="index">
|
|
135
|
-
{{ item.label }}
|
|
136
|
-
</slot>
|
|
137
|
-
|
|
138
|
-
<UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
|
|
139
|
-
</span>
|
|
140
|
-
|
|
141
|
-
<span v-if="item.badge || (item.children?.length && orientation === 'horizontal') || !!slots[item.slot ? `${item.slot}-trailing`: 'item-trailing']" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
|
142
|
-
<slot :name="item.slot ? `${item.slot}-trailing`: 'item-trailing'" :item="item" :active="active" :index="index">
|
|
143
|
-
<UBadge
|
|
144
|
-
v-if="item.badge"
|
|
145
|
-
color="neutral"
|
|
146
|
-
variant="outline"
|
|
147
|
-
:size="((props.ui?.linkTrailingBadgeSize || ui.linkTrailingBadgeSize()) as BadgeProps['size'])"
|
|
148
|
-
v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
|
|
149
|
-
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
|
|
150
|
-
/>
|
|
151
|
-
<UIcon v-if="item.children?.length && orientation === 'horizontal'" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
|
152
|
-
</slot>
|
|
153
|
-
</span>
|
|
154
|
-
</slot>
|
|
177
|
+
<ReuseItemTemplate :item="item" :active="active" :index="index" />
|
|
155
178
|
</ULinkBase>
|
|
156
179
|
</component>
|
|
157
180
|
|
|
@@ -159,13 +182,13 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
|
|
159
182
|
<ul :class="ui.childList({ class: props.ui?.childList })">
|
|
160
183
|
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
|
|
161
184
|
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
|
|
162
|
-
<NavigationMenuLink as-child :active="childActive" @select="childItem.
|
|
185
|
+
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
|
|
163
186
|
<ULinkBase v-bind="childSlotProps" :class="ui.childLink({ class: [props.ui?.childLink, childItem.class], active: childActive })">
|
|
164
187
|
<UIcon v-if="childItem.icon" :name="childItem.icon" :class="ui.childLinkIcon({ class: props.ui?.childLinkIcon, active: childActive })" />
|
|
165
188
|
|
|
166
189
|
<div :class="ui.childLinkWrapper({ class: props.ui?.childLinkWrapper })">
|
|
167
190
|
<p :class="ui.childLinkLabel({ class: props.ui?.childLinkLabel, active: childActive })">
|
|
168
|
-
{{ childItem.
|
|
191
|
+
{{ get(childItem, props.labelKey as string) }}
|
|
169
192
|
|
|
170
193
|
<UIcon v-if="childItem.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: props.ui?.childLinkLabelExternalIcon, active: childActive })" />
|
|
171
194
|
</p>
|
|
@@ -180,7 +203,21 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
|
|
180
203
|
</ul>
|
|
181
204
|
</NavigationMenuContent>
|
|
182
205
|
</ULink>
|
|
183
|
-
|
|
206
|
+
|
|
207
|
+
<template v-if="item.children?.length && orientation === 'vertical'" #content>
|
|
208
|
+
<ul :class="ui.childList({ class: props.ui?.childList })">
|
|
209
|
+
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
|
|
210
|
+
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
|
|
211
|
+
<NavigationMenuLink as-child :active="childActive" @select="childItem.onSelect">
|
|
212
|
+
<ULinkBase v-bind="childSlotProps" :class="ui.link({ class: [props.ui?.link, childItem.class], active: childActive, disabled: !!childItem.disabled })">
|
|
213
|
+
<ReuseItemTemplate :item="childItem" :active="childActive" :index="childIndex" />
|
|
214
|
+
</ULinkBase>
|
|
215
|
+
</NavigationMenuLink>
|
|
216
|
+
</ULink>
|
|
217
|
+
</li>
|
|
218
|
+
</ul>
|
|
219
|
+
</template>
|
|
220
|
+
</component>
|
|
184
221
|
</NavigationMenuList>
|
|
185
222
|
|
|
186
223
|
<div v-if="orientation === 'vertical' && listIndex < lists.length - 1" :class="ui.separator({ class: props.ui?.separator })" />
|