@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.
- package/README.md +4 -2
- package/dist/module.d.mts +19 -5
- package/dist/module.d.ts +19 -5
- package/dist/module.json +2 -1
- package/dist/module.mjs +514 -338
- package/dist/runtime/components/Accordion.vue +13 -5
- package/dist/runtime/components/Alert.vue +2 -2
- package/dist/runtime/components/Breadcrumb.vue +16 -8
- package/dist/runtime/components/Button.vue +4 -2
- package/dist/runtime/components/Carousel.vue +306 -0
- package/dist/runtime/components/Checkbox.vue +3 -6
- package/dist/runtime/components/CommandPalette.vue +27 -18
- package/dist/runtime/components/ContextMenu.vue +33 -9
- package/dist/runtime/components/ContextMenuContent.vue +36 -8
- package/dist/runtime/components/DropdownMenu.vue +33 -9
- package/dist/runtime/components/DropdownMenuContent.vue +38 -8
- package/dist/runtime/components/Form.vue +6 -2
- 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/Modal.vue +2 -2
- package/dist/runtime/components/NavigationMenu.vue +80 -43
- package/dist/runtime/components/Pagination.vue +2 -2
- 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/Slideover.vue +2 -2
- package/dist/runtime/components/Table.vue +232 -0
- package/dist/runtime/components/Tabs.vue +15 -7
- package/dist/runtime/components/Toast.vue +2 -2
- 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/plugins/colors.js +11 -7
- package/dist/runtime/types/form.d.ts +3 -1
- package/dist/runtime/types/index.d.ts +2 -0
- package/dist/runtime/types/index.js +2 -0
- package/dist/runtime/utils/form.d.ts +7 -1
- package/dist/runtime/utils/form.js +25 -1
- package/dist/runtime/utils/index.d.ts +1 -0
- package/dist/runtime/utils/index.js +3 -0
- package/dist/runtime/utils/link.d.ts +1 -1
- package/package.json +30 -14
- package/dist/runtime/types/component.d.ts +0 -17
- package/dist/runtime/types/component.js +0 -0
|
@@ -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) {
|
|
@@ -37,7 +37,7 @@ export interface ModalProps extends DialogRootProps {
|
|
|
37
37
|
portal?: boolean
|
|
38
38
|
/**
|
|
39
39
|
* Display a close button to dismiss the modal.
|
|
40
|
-
* `{ size: 'md', color: '
|
|
40
|
+
* `{ size: 'md', color: 'neutral', variant: 'ghost' }`{lang="ts-type"}
|
|
41
41
|
* @defaultValue true
|
|
42
42
|
*/
|
|
43
43
|
close?: ButtonProps | boolean
|
|
@@ -138,7 +138,7 @@ const ui = computed(() => modal({
|
|
|
138
138
|
v-if="close"
|
|
139
139
|
:icon="closeIcon || appConfig.ui.icons.close"
|
|
140
140
|
size="md"
|
|
141
|
-
color="
|
|
141
|
+
color="neutral"
|
|
142
142
|
variant="ghost"
|
|
143
143
|
aria-label="Close"
|
|
144
144
|
v-bind="typeof close === 'object' ? close : undefined"
|
|
@@ -1,12 +1,12 @@
|
|
|
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'
|
|
8
8
|
import type { AvatarProps, BadgeProps, LinkProps } from '../types'
|
|
9
|
-
import type { DynamicSlots } from '../types/utils'
|
|
9
|
+
import type { DynamicSlots, PartialString } from '../types/utils'
|
|
10
10
|
|
|
11
11
|
const appConfig = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
|
|
12
12
|
|
|
@@ -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,8 +63,13 @@ 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
|
-
ui?:
|
|
72
|
+
ui?: PartialString<typeof navigationMenu.slots>
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
export interface NavigationMenuEmits extends NavigationMenuRootEmits {}
|
|
@@ -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="gray"
|
|
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 })" />
|
|
@@ -44,7 +44,7 @@ export interface PaginationProps extends Pick<PaginationRootProps, 'defaultPage'
|
|
|
44
44
|
ellipsisIcon?: string
|
|
45
45
|
/**
|
|
46
46
|
* The color of the pagination controls.
|
|
47
|
-
* @defaultValue '
|
|
47
|
+
* @defaultValue 'neutral'
|
|
48
48
|
*/
|
|
49
49
|
color?: ButtonProps['color']
|
|
50
50
|
/**
|
|
@@ -106,7 +106,7 @@ import { useAppConfig } from '#imports'
|
|
|
106
106
|
|
|
107
107
|
const props = withDefaults(defineProps<PaginationProps>(), {
|
|
108
108
|
size: 'md',
|
|
109
|
-
color: '
|
|
109
|
+
color: 'neutral',
|
|
110
110
|
variant: 'outline',
|
|
111
111
|
activeColor: 'primary',
|
|
112
112
|
activeVariant: 'solid',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
-
import type { RadioGroupRootProps, RadioGroupRootEmits
|
|
3
|
+
import type { RadioGroupRootProps, RadioGroupRootEmits } from 'radix-vue'
|
|
4
4
|
import type { AppConfig } from '@nuxt/schema'
|
|
5
5
|
import _appConfig from '#build/app.config'
|
|
6
6
|
import theme from '#build/ui/radio-group'
|
|
@@ -12,9 +12,11 @@ const radioGroup = tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup || {}) }
|
|
|
12
12
|
|
|
13
13
|
type RadioGroupVariants = VariantProps<typeof radioGroup>
|
|
14
14
|
|
|
15
|
-
export interface RadioGroupItem
|
|
15
|
+
export interface RadioGroupItem {
|
|
16
16
|
label?: string
|
|
17
17
|
description?: string
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
value?: string
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
|
|
@@ -29,6 +31,16 @@ export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultVa
|
|
|
29
31
|
* @defaultValue 'value'
|
|
30
32
|
*/
|
|
31
33
|
valueKey?: string
|
|
34
|
+
/**
|
|
35
|
+
* When `items` is an array of objects, select the field to use as the label.
|
|
36
|
+
* @defaultValue 'label'
|
|
37
|
+
*/
|
|
38
|
+
labelKey?: string
|
|
39
|
+
/**
|
|
40
|
+
* When `items` is an array of objects, select the field to use as the description.
|
|
41
|
+
* @defaultValue 'description'
|
|
42
|
+
*/
|
|
43
|
+
descriptionKey?: string
|
|
32
44
|
items?: T[]
|
|
33
45
|
size?: RadioGroupVariants['size']
|
|
34
46
|
color?: RadioGroupVariants['color']
|
|
@@ -59,9 +71,12 @@ import { computed, useId } from 'vue'
|
|
|
59
71
|
import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'radix-vue'
|
|
60
72
|
import { reactivePick } from '@vueuse/core'
|
|
61
73
|
import { useFormField } from '../composables/useFormField'
|
|
74
|
+
import { get } from '../utils'
|
|
62
75
|
|
|
63
76
|
const props = withDefaults(defineProps<RadioGroupProps<T>>(), {
|
|
64
77
|
valueKey: 'value',
|
|
78
|
+
labelKey: 'label',
|
|
79
|
+
descriptionKey: 'description',
|
|
65
80
|
orientation: 'vertical'
|
|
66
81
|
})
|
|
67
82
|
const emits = defineEmits<RadioGroupEmits>()
|
|
@@ -89,11 +104,15 @@ function normalizeItem(item: any) {
|
|
|
89
104
|
}
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
const value = item
|
|
107
|
+
const value = get(item, props.valueKey as string)
|
|
108
|
+
const label = get(item, props.labelKey as string)
|
|
109
|
+
const description = get(item, props.descriptionKey as string)
|
|
93
110
|
|
|
94
111
|
return {
|
|
95
112
|
...item,
|
|
96
113
|
value,
|
|
114
|
+
label,
|
|
115
|
+
description,
|
|
97
116
|
id: `${id}:${value}`
|
|
98
117
|
}
|
|
99
118
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
-
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectArrowProps
|
|
3
|
+
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectArrowProps } from 'radix-vue'
|
|
4
4
|
import type { AppConfig } from '@nuxt/schema'
|
|
5
5
|
import _appConfig from '#build/app.config'
|
|
6
6
|
import theme from '#build/ui/select'
|
|
@@ -12,7 +12,7 @@ const appConfig = _appConfig as AppConfig & { ui: { select: Partial<typeof theme
|
|
|
12
12
|
|
|
13
13
|
const select = tv({ extend: tv(theme), ...(appConfig.ui?.select || {}) })
|
|
14
14
|
|
|
15
|
-
export interface SelectItem
|
|
15
|
+
export interface SelectItem {
|
|
16
16
|
label?: string
|
|
17
17
|
icon?: string
|
|
18
18
|
avatar?: AvatarProps
|
|
@@ -22,6 +22,8 @@ export interface SelectItem extends Pick<SelectItemProps, 'disabled' | 'value'>
|
|
|
22
22
|
* @defaultValue 'item'
|
|
23
23
|
*/
|
|
24
24
|
type?: 'label' | 'separator' | 'item'
|
|
25
|
+
value?: string
|
|
26
|
+
disabled?: boolean
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
type SelectVariants = VariantProps<typeof select>
|
|
@@ -63,6 +65,11 @@ export interface SelectProps<T> extends Omit<SelectRootProps, 'dir'>, UseCompone
|
|
|
63
65
|
* @defaultValue 'value'
|
|
64
66
|
*/
|
|
65
67
|
valueKey?: string
|
|
68
|
+
/**
|
|
69
|
+
* When `items` is an array of objects, select the field to use as the label.
|
|
70
|
+
* @defaultValue 'label'
|
|
71
|
+
*/
|
|
72
|
+
labelKey?: string
|
|
66
73
|
items?: T[] | T[][]
|
|
67
74
|
/** Highlight the ring color like a focus state. */
|
|
68
75
|
highlight?: boolean
|
|
@@ -97,12 +104,14 @@ import { useAppConfig } from '#imports'
|
|
|
97
104
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
98
105
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
99
106
|
import { useFormField } from '../composables/useFormField'
|
|
107
|
+
import { get } from '../utils'
|
|
100
108
|
import UIcon from './Icon.vue'
|
|
101
109
|
import UAvatar from './Avatar.vue'
|
|
102
110
|
import UChip from './Chip.vue'
|
|
103
111
|
|
|
104
112
|
const props = withDefaults(defineProps<SelectProps<T>>(), {
|
|
105
113
|
valueKey: 'value',
|
|
114
|
+
labelKey: 'label',
|
|
106
115
|
portal: true
|
|
107
116
|
})
|
|
108
117
|
const emits = defineEmits<SelectEmits>()
|
|
@@ -124,7 +133,7 @@ const ui = computed(() => select({
|
|
|
124
133
|
size: selectSize?.value,
|
|
125
134
|
loading: props.loading,
|
|
126
135
|
highlight: highlight.value,
|
|
127
|
-
leading: isLeading.value || !!slots.leading,
|
|
136
|
+
leading: isLeading.value || !!props.avatar || !!slots.leading,
|
|
128
137
|
trailing: isTrailing.value || !!slots.trailing,
|
|
129
138
|
buttonGroup: orientation.value
|
|
130
139
|
}))
|
|
@@ -162,9 +171,10 @@ function onUpdateOpen(value: boolean) {
|
|
|
162
171
|
@update:open="onUpdateOpen"
|
|
163
172
|
>
|
|
164
173
|
<SelectTrigger :class="ui.base({ class: [props.class, props.ui?.base] })">
|
|
165
|
-
<span v-if="isLeading || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
174
|
+
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
166
175
|
<slot name="leading" :model-value="modelValue" :open="open" :ui="ui">
|
|
167
|
-
<UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
176
|
+
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
177
|
+
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
|
168
178
|
</slot>
|
|
169
179
|
</span>
|
|
170
180
|
|
|
@@ -183,19 +193,21 @@ function onUpdateOpen(value: boolean) {
|
|
|
183
193
|
<SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
|
184
194
|
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
|
185
195
|
<SelectLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
|
186
|
-
{{ item.
|
|
196
|
+
{{ get(item, props.labelKey as string) }}
|
|
187
197
|
</SelectLabel>
|
|
198
|
+
|
|
188
199
|
<SelectSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
|
200
|
+
|
|
189
201
|
<SelectItem
|
|
190
202
|
v-else
|
|
191
203
|
:class="ui.item({ class: props.ui?.item })"
|
|
192
204
|
:disabled="item.disabled"
|
|
193
|
-
:value="typeof item === 'object' ? (item
|
|
205
|
+
:value="typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
|
194
206
|
>
|
|
195
207
|
<slot name="item" :item="(item as T)" :index="index">
|
|
196
208
|
<slot name="item-leading" :item="(item as T)" :index="index">
|
|
197
|
-
<
|
|
198
|
-
<
|
|
209
|
+
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
|
210
|
+
<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 })" />
|
|
199
211
|
<UChip
|
|
200
212
|
v-else-if="item.chip"
|
|
201
213
|
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
|
@@ -208,7 +220,7 @@ function onUpdateOpen(value: boolean) {
|
|
|
208
220
|
|
|
209
221
|
<SelectItemText :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
|
210
222
|
<slot name="item-label" :item="(item as T)" :index="index">
|
|
211
|
-
{{ typeof item === 'object' ? item.
|
|
223
|
+
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
|
212
224
|
</slot>
|
|
213
225
|
</SelectItemText>
|
|
214
226
|
|