@nuxt/ui 3.0.0-alpha.8 → 3.0.0-alpha.9
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/.nuxt/ui/button.ts +1 -1
- package/.nuxt/ui/card.ts +1 -1
- package/.nuxt/ui/carousel.ts +2 -2
- package/.nuxt/ui/container.ts +1 -1
- package/.nuxt/ui/index.ts +2 -0
- package/.nuxt/ui/input-menu.ts +3 -3
- package/.nuxt/ui/input-number.ts +204 -0
- package/.nuxt/ui/input.ts +2 -2
- package/.nuxt/ui/navigation-menu.ts +1 -1
- package/.nuxt/ui/pin-input.ts +145 -0
- package/.nuxt/ui/select-menu.ts +8 -13
- package/.nuxt/ui/select.ts +2 -2
- package/.nuxt/ui/table.ts +1 -0
- package/.nuxt/ui/tabs.ts +1 -1
- package/.nuxt/ui/textarea.ts +2 -2
- package/.nuxt/ui/toaster.ts +1 -1
- package/.nuxt/ui/tooltip.ts +1 -1
- package/README.md +52 -12
- package/cli/commands/{init.mjs → make/component.mjs} +4 -4
- package/cli/commands/make/index.mjs +14 -0
- package/cli/commands/make/locale.mjs +64 -0
- package/cli/index.mjs +2 -2
- package/cli/templates.mjs +46 -1
- package/cli/utils.mjs +14 -0
- package/dist/chunks/component-meta.cjs +7320 -3335
- package/dist/chunks/component-meta.mjs +7320 -3335
- package/dist/client/devtools/200.html +1 -1
- package/dist/client/devtools/404.html +1 -1
- package/dist/client/devtools/_nuxt/{hKiujHsR.js → BDC8ywlR.js} +1 -1
- package/dist/client/devtools/_nuxt/{DM3_wAgz.js → BT004Uxp.js} +22 -31
- package/dist/client/devtools/_nuxt/{Bj9hdafK.js → C1H2Soz3.js} +1 -1
- package/dist/client/devtools/_nuxt/builds/latest.json +1 -1
- package/dist/client/devtools/_nuxt/builds/meta/f98c13ea-d751-4b37-8ce3-94e99708745f.json +1 -0
- package/dist/client/devtools/_nuxt/entry.Bx6WJjrx.css +1 -0
- package/dist/client/devtools/_nuxt/error-404.Cq4qshcf.css +1 -0
- package/dist/client/devtools/_nuxt/error-500.ax1aM34m.css +1 -0
- package/dist/client/devtools/index.html +1 -1
- package/dist/module.cjs +13 -12
- package/dist/module.json +1 -1
- package/dist/module.mjs +13 -12
- package/dist/runtime/components/Alert.vue +4 -1
- package/dist/runtime/components/App.vue +11 -5
- package/dist/runtime/components/Breadcrumb.vue +8 -3
- package/dist/runtime/components/Carousel.vue +11 -7
- package/dist/runtime/components/CommandPalette.vue +5 -3
- package/dist/runtime/components/ContextMenuContent.vue +2 -0
- package/dist/runtime/components/DropdownMenuContent.vue +2 -0
- package/dist/runtime/components/Form.vue +18 -20
- package/dist/runtime/components/FormField.vue +7 -3
- package/dist/runtime/components/InputMenu.vue +97 -21
- package/dist/runtime/components/InputNumber.vue +192 -0
- package/dist/runtime/components/Link.vue +19 -5
- package/dist/runtime/components/Modal.vue +12 -3
- package/dist/runtime/components/NavigationMenu.vue +4 -2
- package/dist/runtime/components/Pagination.vue +12 -5
- package/dist/runtime/components/PinInput.vue +96 -0
- package/dist/runtime/components/Popover.vue +17 -1
- package/dist/runtime/components/Select.vue +1 -1
- package/dist/runtime/components/SelectMenu.vue +106 -30
- package/dist/runtime/components/Slideover.vue +12 -3
- package/dist/runtime/components/Table.vue +11 -1
- package/dist/runtime/components/Textarea.vue +1 -1
- package/dist/runtime/components/Toast.vue +4 -1
- package/dist/runtime/components/Toaster.vue +22 -14
- package/dist/runtime/composables/defineLocale.d.ts +9 -0
- package/dist/runtime/composables/defineLocale.js +4 -0
- package/dist/runtime/composables/useFormField.d.ts +4 -1
- package/dist/runtime/composables/useFormField.js +1 -1
- package/dist/runtime/composables/useLocale.d.ts +4 -0
- package/dist/runtime/composables/useLocale.js +10 -0
- package/dist/runtime/index.css +1 -1
- package/dist/runtime/locale/ar.d.ts +2 -0
- package/dist/runtime/locale/ar.js +47 -0
- package/dist/runtime/locale/cs.d.ts +2 -0
- package/dist/runtime/locale/cs.js +46 -0
- package/dist/runtime/locale/de.d.ts +2 -0
- package/dist/runtime/locale/de.js +46 -0
- package/dist/runtime/locale/en.d.ts +2 -0
- package/dist/runtime/locale/en.js +46 -0
- package/dist/runtime/locale/es.d.ts +2 -0
- package/dist/runtime/locale/es.js +46 -0
- package/dist/runtime/locale/fa_ir.d.ts +2 -0
- package/dist/runtime/locale/fa_ir.js +47 -0
- package/dist/runtime/locale/fr.d.ts +2 -0
- package/dist/runtime/locale/fr.js +46 -0
- package/dist/runtime/locale/index.d.ts +12 -0
- package/dist/runtime/locale/index.js +12 -0
- package/dist/runtime/locale/it.d.ts +2 -0
- package/dist/runtime/locale/it.js +46 -0
- package/dist/runtime/locale/pl.d.ts +2 -0
- package/dist/runtime/locale/pl.js +46 -0
- package/dist/runtime/locale/ru.d.ts +2 -0
- package/dist/runtime/locale/ru.js +46 -0
- package/dist/runtime/locale/zh_hans.d.ts +2 -0
- package/dist/runtime/locale/zh_hans.js +46 -0
- package/dist/runtime/locale/zh_hant.d.ts +2 -0
- package/dist/runtime/locale/zh_hant.js +46 -0
- package/dist/runtime/types/form.d.ts +7 -2
- package/dist/runtime/types/index.d.ts +3 -0
- package/dist/runtime/types/index.js +3 -0
- package/dist/runtime/types/locale.d.ts +48 -0
- package/dist/runtime/types/locale.js +0 -0
- package/dist/runtime/utils/form.d.ts +5 -9
- package/dist/runtime/utils/form.js +115 -50
- package/dist/runtime/utils/link.d.ts +1 -1
- package/dist/runtime/utils/locale.d.ts +15 -0
- package/dist/runtime/utils/locale.js +25 -0
- package/dist/shared/{ui.BU7wzH0D.cjs → ui.BPnAe4QS.cjs} +194 -36
- package/dist/shared/{ui.BufKhDiD.mjs → ui.d52SSCwA.mjs} +194 -36
- package/dist/unplugin.cjs +1 -1
- package/dist/unplugin.d.cts +5 -3
- package/dist/unplugin.d.mts +5 -3
- package/dist/unplugin.d.ts +5 -3
- package/dist/unplugin.mjs +1 -1
- package/dist/vite.cjs +1 -1
- package/dist/vite.mjs +1 -1
- package/package.json +33 -26
- package/dist/client/devtools/_nuxt/builds/meta/e6b425f7-b919-4014-915b-0ea07c316790.json +0 -1
- package/dist/client/devtools/_nuxt/entry.DaJP-ZMB.css +0 -24
- package/dist/client/devtools/_nuxt/error-404.B0Nmv3fV.css +0 -1
- package/dist/client/devtools/_nuxt/error-500.C8fgTxVz.css +0 -1
|
@@ -73,8 +73,8 @@ export interface LinkProps extends NuxtLinkProps {
|
|
|
73
73
|
active?: boolean
|
|
74
74
|
/** Will only be active if the current route is an exact match. */
|
|
75
75
|
exact?: boolean
|
|
76
|
-
/**
|
|
77
|
-
exactQuery?: boolean
|
|
76
|
+
/** Allows controlling how the current route query sets the link as active. */
|
|
77
|
+
exactQuery?: boolean | 'partial'
|
|
78
78
|
/** Will only be active if the current route hash is an exact match. */
|
|
79
79
|
exactHash?: boolean
|
|
80
80
|
/** The class to apply when the link is inactive. */
|
|
@@ -94,10 +94,11 @@ extendDevtoolsMeta({ example: 'LinkExample' })
|
|
|
94
94
|
|
|
95
95
|
<script setup lang="ts">
|
|
96
96
|
import { computed } from 'vue'
|
|
97
|
-
import { isEqual } from 'ohash'
|
|
97
|
+
import { isEqual, diff } from 'ohash'
|
|
98
98
|
import { useForwardProps } from 'radix-vue'
|
|
99
99
|
import { reactiveOmit } from '@vueuse/core'
|
|
100
100
|
import { useRoute } from '#imports'
|
|
101
|
+
import ULinkBase from './LinkBase.vue'
|
|
101
102
|
|
|
102
103
|
defineOptions({ inheritAttrs: false })
|
|
103
104
|
|
|
@@ -123,14 +124,27 @@ const ui = computed(() => tv({
|
|
|
123
124
|
}
|
|
124
125
|
}))
|
|
125
126
|
|
|
127
|
+
function isPartiallyEqual(item1: any, item2: any) {
|
|
128
|
+
const diffedKeys = diff(item1, item2).reduce((filtered, q) => {
|
|
129
|
+
if (q.type === 'added') {
|
|
130
|
+
filtered.push(q.key)
|
|
131
|
+
}
|
|
132
|
+
return filtered
|
|
133
|
+
}, [] as string[])
|
|
134
|
+
return isEqual(item1, item2, { excludeKeys: key => diffedKeys.includes(key) })
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
|
|
127
138
|
if (props.active !== undefined) {
|
|
128
139
|
return props.active
|
|
129
140
|
}
|
|
130
141
|
|
|
131
|
-
if (props.exactQuery
|
|
132
|
-
return false
|
|
142
|
+
if (props.exactQuery === 'partial') {
|
|
143
|
+
if (!isPartiallyEqual(linkRoute.query, route.query)) return false
|
|
144
|
+
} else if (props.exactQuery === true) {
|
|
145
|
+
if (!isEqual(linkRoute.query, route.query)) return false
|
|
133
146
|
}
|
|
147
|
+
|
|
134
148
|
if (props.exactHash && linkRoute.hash !== route.hash) {
|
|
135
149
|
return false
|
|
136
150
|
}
|
|
@@ -77,6 +77,7 @@ import { computed, toRef } from 'vue'
|
|
|
77
77
|
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, useForwardPropsEmits } from 'radix-vue'
|
|
78
78
|
import { reactivePick } from '@vueuse/core'
|
|
79
79
|
import { useAppConfig } from '#imports'
|
|
80
|
+
import { useLocale } from '../composables/useLocale'
|
|
80
81
|
import UButton from './Button.vue'
|
|
81
82
|
|
|
82
83
|
const props = withDefaults(defineProps<ModalProps>(), {
|
|
@@ -95,14 +96,22 @@ const contentEvents = computed(() => {
|
|
|
95
96
|
if (props.preventClose) {
|
|
96
97
|
return {
|
|
97
98
|
pointerDownOutside: (e: Event) => e.preventDefault(),
|
|
98
|
-
interactOutside: (e: Event) => e.preventDefault()
|
|
99
|
+
interactOutside: (e: Event) => e.preventDefault(),
|
|
100
|
+
escapeKeyDown: (e: Event) => e.preventDefault()
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
interactOutside: (e: Event) => {
|
|
106
|
+
if (e.target instanceof Element && e.target.closest('[data-sonner-toaster]')) {
|
|
107
|
+
return e.preventDefault()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
103
111
|
})
|
|
104
112
|
|
|
105
113
|
const appConfig = useAppConfig()
|
|
114
|
+
const { t } = useLocale()
|
|
106
115
|
|
|
107
116
|
const ui = computed(() => modal({
|
|
108
117
|
transition: props.transition,
|
|
@@ -143,7 +152,7 @@ const ui = computed(() => modal({
|
|
|
143
152
|
size="md"
|
|
144
153
|
color="neutral"
|
|
145
154
|
variant="ghost"
|
|
146
|
-
aria-label="
|
|
155
|
+
:aria-label="t('modal.close')"
|
|
147
156
|
v-bind="typeof close === 'object' ? close : undefined"
|
|
148
157
|
:class="ui.close({ class: props.ui?.close })"
|
|
149
158
|
/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
-
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps } from 'radix-vue'
|
|
3
|
+
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, CollapsibleRootProps } 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/navigation-menu'
|
|
@@ -17,7 +17,7 @@ export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'child
|
|
|
17
17
|
description?: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'> {
|
|
20
|
+
export interface NavigationMenuItem extends Omit<LinkProps, 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
|
|
21
21
|
label?: string
|
|
22
22
|
icon?: string
|
|
23
23
|
avatar?: AvatarProps
|
|
@@ -208,6 +208,8 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
|
|
208
208
|
:key="`list-${listIndex}-${index}`"
|
|
209
209
|
as="li"
|
|
210
210
|
:value="item.value || String(index)"
|
|
211
|
+
:default-open="item.defaultOpen"
|
|
212
|
+
:open="item.open"
|
|
211
213
|
:class="ui.item({ class: props.ui?.item })"
|
|
212
214
|
>
|
|
213
215
|
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item)" custom>
|
|
@@ -103,9 +103,11 @@ extendDevtoolsMeta({ defaultProps: { total: 50 } })
|
|
|
103
103
|
</script>
|
|
104
104
|
|
|
105
105
|
<script setup lang="ts">
|
|
106
|
+
import { computed } from 'vue'
|
|
106
107
|
import { PaginationRoot, PaginationList, PaginationListItem, PaginationFirst, PaginationPrev, PaginationEllipsis, PaginationNext, PaginationLast, useForwardPropsEmits } from 'radix-vue'
|
|
107
108
|
import { reactivePick } from '@vueuse/core'
|
|
108
109
|
import { useAppConfig } from '#imports'
|
|
110
|
+
import { useLocale } from '../composables/useLocale'
|
|
109
111
|
import UButton from './Button.vue'
|
|
110
112
|
|
|
111
113
|
const props = withDefaults(defineProps<PaginationProps>(), {
|
|
@@ -124,9 +126,14 @@ const emits = defineEmits<PaginationEmits>()
|
|
|
124
126
|
const slots = defineSlots<PaginationSlots>()
|
|
125
127
|
|
|
126
128
|
const appConfig = useAppConfig()
|
|
127
|
-
|
|
129
|
+
const { dir } = useLocale()
|
|
128
130
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultPage', 'disabled', 'itemsPerPage', 'page', 'showEdges', 'siblingCount', 'total'), emits)
|
|
129
131
|
|
|
132
|
+
const firstIcon = computed(() => props.firstIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronDoubleRight : appConfig.ui.icons.chevronDoubleLeft))
|
|
133
|
+
const prevIcon = computed(() => props.prevIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronRight : appConfig.ui.icons.chevronLeft))
|
|
134
|
+
const nextIcon = computed(() => props.nextIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronLeft : appConfig.ui.icons.chevronRight))
|
|
135
|
+
const lastIcon = computed(() => props.lastIcon || (dir.value === 'rtl' ? appConfig.ui.icons.chevronDoubleLeft : appConfig.ui.icons.chevronDoubleRight))
|
|
136
|
+
|
|
130
137
|
// eslint-disable-next-line vue/no-dupe-keys
|
|
131
138
|
const ui = pagination()
|
|
132
139
|
</script>
|
|
@@ -136,12 +143,12 @@ const ui = pagination()
|
|
|
136
143
|
<PaginationList v-slot="{ items }" :class="ui.list({ class: props.ui?.list })">
|
|
137
144
|
<PaginationFirst v-if="showControls || !!slots.first" as-child>
|
|
138
145
|
<slot name="first">
|
|
139
|
-
<UButton :color="color" :variant="variant" :size="size" :icon="firstIcon
|
|
146
|
+
<UButton :color="color" :variant="variant" :size="size" :icon="firstIcon" :to="to?.(1)" />
|
|
140
147
|
</slot>
|
|
141
148
|
</PaginationFirst>
|
|
142
149
|
<PaginationPrev v-if="showControls || !!slots.prev" as-child>
|
|
143
150
|
<slot name="prev">
|
|
144
|
-
<UButton :color="color" :variant="variant" :size="size" :icon="prevIcon
|
|
151
|
+
<UButton :color="color" :variant="variant" :size="size" :icon="prevIcon" :to="page > 1 ? to?.(page - 1) : undefined" />
|
|
145
152
|
</slot>
|
|
146
153
|
</PaginationPrev>
|
|
147
154
|
|
|
@@ -169,12 +176,12 @@ const ui = pagination()
|
|
|
169
176
|
|
|
170
177
|
<PaginationNext v-if="showControls || !!slots.next" as-child>
|
|
171
178
|
<slot name="next">
|
|
172
|
-
<UButton :color="color" :variant="variant" :size="size" :icon="nextIcon
|
|
179
|
+
<UButton :color="color" :variant="variant" :size="size" :icon="nextIcon" :to="page < pageCount ? to?.(pageCount) : undefined" />
|
|
173
180
|
</slot>
|
|
174
181
|
</PaginationNext>
|
|
175
182
|
<PaginationLast v-if="showControls || !!slots.last" as-child>
|
|
176
183
|
<slot name="last">
|
|
177
|
-
<UButton :color="color" :variant="variant" :size="size" :icon="lastIcon
|
|
184
|
+
<UButton :color="color" :variant="variant" :size="size" :icon="lastIcon" :to=" to?.(pageCount)" />
|
|
178
185
|
</slot>
|
|
179
186
|
</PaginationLast>
|
|
180
187
|
</PaginationList>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import _appConfig from '#build/app.config'
|
|
3
|
+
import theme from '#build/ui/pin-input'
|
|
4
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
5
|
+
import type { PinInputRootEmits, PinInputRootProps } from 'radix-vue'
|
|
6
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
7
|
+
import type { PartialString } from '../types/utils'
|
|
8
|
+
|
|
9
|
+
const appConfig = _appConfig as AppConfig & { ui: { pinInput: Partial<typeof theme> } }
|
|
10
|
+
|
|
11
|
+
const pinInput = tv({ extend: tv(theme), ...(appConfig.ui?.pinInput || {}) })
|
|
12
|
+
|
|
13
|
+
type PinInputVariants = VariantProps<typeof pinInput>
|
|
14
|
+
|
|
15
|
+
export interface PinInputProps extends Pick<PinInputRootProps, 'defaultValue' | 'disabled' | 'id' | 'mask' | 'modelValue' | 'name' | 'otp' | 'placeholder' | 'required' | 'type'> {
|
|
16
|
+
/**
|
|
17
|
+
* The element or component this component should render as.
|
|
18
|
+
* @defaultValue 'div'
|
|
19
|
+
*/
|
|
20
|
+
as?: any
|
|
21
|
+
color?: PinInputVariants['color']
|
|
22
|
+
variant?: PinInputVariants['variant']
|
|
23
|
+
size?: PinInputVariants['size']
|
|
24
|
+
length?: number | string
|
|
25
|
+
highlight?: boolean
|
|
26
|
+
class?: any
|
|
27
|
+
ui?: PartialString<typeof pinInput.slots>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type PinInputEmits = PinInputRootEmits & {
|
|
31
|
+
change: [payload: Event]
|
|
32
|
+
blur: [payload: Event]
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { ref, computed } from 'vue'
|
|
38
|
+
import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'radix-vue'
|
|
39
|
+
import { reactivePick } from '@vueuse/core'
|
|
40
|
+
import { useFormField } from '../composables/useFormField'
|
|
41
|
+
import { looseToNumber } from '../utils'
|
|
42
|
+
|
|
43
|
+
defineOptions({ inheritAttrs: false })
|
|
44
|
+
|
|
45
|
+
const props = withDefaults(defineProps<PinInputProps>(), {
|
|
46
|
+
type: 'text',
|
|
47
|
+
length: 5
|
|
48
|
+
})
|
|
49
|
+
const emits = defineEmits<PinInputEmits>()
|
|
50
|
+
|
|
51
|
+
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'placeholder', 'required', 'type'), emits)
|
|
52
|
+
const { emitFormInput, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled } = useFormField<PinInputProps>(props)
|
|
53
|
+
|
|
54
|
+
const ui = computed(() => pinInput({
|
|
55
|
+
color: color.value,
|
|
56
|
+
variant: props.variant,
|
|
57
|
+
size: size.value,
|
|
58
|
+
highlight: highlight.value
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
const completed = ref(false)
|
|
62
|
+
function onComplete(value: string[]) {
|
|
63
|
+
// @ts-expect-error - 'target' does not exist in type 'EventInit'
|
|
64
|
+
const event = new Event('change', { target: { value } })
|
|
65
|
+
emits('change', event)
|
|
66
|
+
emitFormChange()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onBlur(event: FocusEvent) {
|
|
70
|
+
if (!event.relatedTarget || completed.value) {
|
|
71
|
+
emits('blur', event)
|
|
72
|
+
emitFormBlur()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<PinInputRoot
|
|
79
|
+
v-bind="rootProps"
|
|
80
|
+
:id="id"
|
|
81
|
+
:name="name"
|
|
82
|
+
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
|
83
|
+
@update:model-value="emitFormInput()"
|
|
84
|
+
@complete="onComplete"
|
|
85
|
+
>
|
|
86
|
+
<PinInputInput
|
|
87
|
+
v-for="(ids, index) in looseToNumber(props.length)"
|
|
88
|
+
:key="ids"
|
|
89
|
+
:index="index"
|
|
90
|
+
:class="ui.base({ class: props.ui?.base })"
|
|
91
|
+
v-bind="$attrs"
|
|
92
|
+
:disabled="disabled"
|
|
93
|
+
@blur="onBlur"
|
|
94
|
+
/>
|
|
95
|
+
</PinInputRoot>
|
|
96
|
+
</template>
|
|
@@ -31,6 +31,11 @@ export interface PopoverProps extends PopoverRootProps, Pick<HoverCardRootProps,
|
|
|
31
31
|
* @defaultValue true
|
|
32
32
|
*/
|
|
33
33
|
portal?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* When `true`, the popover will not close when clicking outside.
|
|
36
|
+
* @defaultValue false
|
|
37
|
+
*/
|
|
38
|
+
preventClose?: boolean
|
|
34
39
|
class?: any
|
|
35
40
|
ui?: Partial<typeof popover.slots>
|
|
36
41
|
}
|
|
@@ -64,6 +69,17 @@ const slots = defineSlots<PopoverSlots>()
|
|
|
64
69
|
const pick = props.mode === 'hover' ? reactivePick(props, 'defaultOpen', 'open', 'openDelay', 'closeDelay') : reactivePick(props, 'defaultOpen', 'open', 'modal')
|
|
65
70
|
const rootProps = useForwardPropsEmits(pick, emits)
|
|
66
71
|
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8 }) as PopoverContentProps)
|
|
72
|
+
const contentEvents = computed(() => {
|
|
73
|
+
if (props.preventClose) {
|
|
74
|
+
return {
|
|
75
|
+
pointerDownOutside: (e: Event) => e.preventDefault(),
|
|
76
|
+
interactOutside: (e: Event) => e.preventDefault(),
|
|
77
|
+
escapeKeyDown: (e: Event) => e.preventDefault()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {}
|
|
82
|
+
})
|
|
67
83
|
const arrowProps = toRef(() => props.arrow as PopoverArrowProps)
|
|
68
84
|
|
|
69
85
|
// eslint-disable-next-line vue/no-dupe-keys
|
|
@@ -81,7 +97,7 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
|
|
|
81
97
|
</Component.Trigger>
|
|
82
98
|
|
|
83
99
|
<Component.Portal :disabled="!portal">
|
|
84
|
-
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })">
|
|
100
|
+
<Component.Content v-bind="contentProps" :class="ui.content({ class: [props.class, props.ui?.content] })" v-on="contentEvents">
|
|
85
101
|
<slot name="content" />
|
|
86
102
|
|
|
87
103
|
<Component.Arrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
|
|
@@ -70,7 +70,7 @@ export interface SelectProps<T extends MaybeArrayOfArrayItem<I>, I extends Maybe
|
|
|
70
70
|
* When `items` is an array of objects, select the field to use as the label.
|
|
71
71
|
* @defaultValue 'label'
|
|
72
72
|
*/
|
|
73
|
-
labelKey?:
|
|
73
|
+
labelKey?: V
|
|
74
74
|
items?: I
|
|
75
75
|
/** Highlight the ring color like a focus state. */
|
|
76
76
|
highlight?: boolean
|
|
@@ -34,11 +34,12 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
|
|
34
34
|
/** The placeholder text when the select is empty. */
|
|
35
35
|
placeholder?: string
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* Whether to display the search input or not.
|
|
38
38
|
* Can be an object to pass additional props to the input.
|
|
39
|
-
*
|
|
39
|
+
* `{ placeholder: 'Search...', variant: 'none' }`{lang="ts-type"}
|
|
40
|
+
* @defaultValue true
|
|
40
41
|
*/
|
|
41
|
-
searchInput?: boolean |
|
|
42
|
+
searchInput?: boolean | InputProps
|
|
42
43
|
color?: SelectMenuVariants['color']
|
|
43
44
|
variant?: SelectMenuVariants['variant']
|
|
44
45
|
size?: SelectMenuVariants['size']
|
|
@@ -69,9 +70,10 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
|
|
69
70
|
*/
|
|
70
71
|
portal?: boolean
|
|
71
72
|
/**
|
|
72
|
-
* Whether to filter items or not, can be an array of fields to filter.
|
|
73
|
+
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
|
|
73
74
|
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
|
|
74
|
-
*
|
|
75
|
+
* `['label']`{lang="ts-type"}
|
|
76
|
+
* @defaultValue true
|
|
75
77
|
*/
|
|
76
78
|
filter?: boolean | string[]
|
|
77
79
|
/**
|
|
@@ -83,10 +85,15 @@ export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends M
|
|
|
83
85
|
* When `items` is an array of objects, select the field to use as the label.
|
|
84
86
|
* @defaultValue 'label'
|
|
85
87
|
*/
|
|
86
|
-
labelKey?:
|
|
88
|
+
labelKey?: V
|
|
87
89
|
items?: I
|
|
88
90
|
/** Highlight the ring color like a focus state. */
|
|
89
91
|
highlight?: boolean
|
|
92
|
+
/**
|
|
93
|
+
* Determines if custom user input that does not exist in options can be added.
|
|
94
|
+
* @defaultValue false
|
|
95
|
+
*/
|
|
96
|
+
createItem?: boolean | 'always' | { placement?: 'top' | 'bottom', when?: 'empty' | 'always' }
|
|
90
97
|
class?: any
|
|
91
98
|
ui?: PartialString<typeof selectMenu.slots>
|
|
92
99
|
/** The controlled value of the Combobox. Can be binded-with with `v-model`. */
|
|
@@ -99,6 +106,7 @@ export type SelectMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>
|
|
|
99
106
|
change: [payload: Event]
|
|
100
107
|
blur: [payload: FocusEvent]
|
|
101
108
|
focus: [payload: FocusEvent]
|
|
109
|
+
create: [payload: Event, item: T]
|
|
102
110
|
} & SelectModelValueEmits<T, V, M>
|
|
103
111
|
|
|
104
112
|
type SlotProps<T> = (props: { item: T, index: number }) => any
|
|
@@ -112,32 +120,35 @@ export interface SelectMenuSlots<T> {
|
|
|
112
120
|
'item-leading': SlotProps<T>
|
|
113
121
|
'item-label': SlotProps<T>
|
|
114
122
|
'item-trailing': SlotProps<T>
|
|
123
|
+
'create-item-label'(props: { item: T }): any
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
extendDevtoolsMeta({ defaultProps: { items: ['Option 1', 'Option 2', 'Option 3'] } })
|
|
118
127
|
</script>
|
|
119
128
|
|
|
120
129
|
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
|
121
|
-
import { computed, toRef } from 'vue'
|
|
130
|
+
import { computed, toRef, toRaw } from 'vue'
|
|
122
131
|
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'radix-vue'
|
|
123
132
|
import { defu } from 'defu'
|
|
124
|
-
import
|
|
125
|
-
import { reactivePick } from '@vueuse/core'
|
|
133
|
+
import { isEqual } from 'ohash'
|
|
134
|
+
import { reactivePick, createReusableTemplate } from '@vueuse/core'
|
|
126
135
|
import { useAppConfig } from '#imports'
|
|
127
136
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
128
137
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
129
138
|
import { useFormField } from '../composables/useFormField'
|
|
139
|
+
import { useLocale } from '../composables/useLocale'
|
|
130
140
|
import { get, escapeRegExp } from '../utils'
|
|
131
141
|
import UIcon from './Icon.vue'
|
|
132
142
|
import UAvatar from './Avatar.vue'
|
|
133
143
|
import UChip from './Chip.vue'
|
|
144
|
+
import UInput from './Input.vue'
|
|
134
145
|
|
|
135
146
|
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
|
|
136
147
|
search: true,
|
|
137
148
|
portal: true,
|
|
138
149
|
autofocusDelay: 0,
|
|
139
|
-
searchInput:
|
|
140
|
-
filter:
|
|
150
|
+
searchInput: true,
|
|
151
|
+
filter: true,
|
|
141
152
|
labelKey: 'label' as never
|
|
142
153
|
})
|
|
143
154
|
|
|
@@ -147,9 +158,16 @@ const slots = defineSlots<SelectMenuSlots<T>>()
|
|
|
147
158
|
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
|
148
159
|
|
|
149
160
|
const appConfig = useAppConfig()
|
|
150
|
-
|
|
161
|
+
|
|
162
|
+
const { t } = useLocale()
|
|
163
|
+
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
|
|
151
164
|
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
|
|
152
165
|
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
|
|
166
|
+
const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: 'Search...', variant: 'none' }) as InputProps)
|
|
167
|
+
// This is a hack due to generic boolean casting (see https://github.com/nuxt/ui/issues/2541)
|
|
168
|
+
const multiple = toRef(() => typeof props.multiple === 'string' ? true : props.multiple)
|
|
169
|
+
|
|
170
|
+
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
|
|
153
171
|
|
|
154
172
|
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled } = useFormField<InputProps>(props)
|
|
155
173
|
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
|
@@ -169,32 +187,40 @@ const ui = computed(() => selectMenu({
|
|
|
169
187
|
}))
|
|
170
188
|
|
|
171
189
|
function displayValue(value: T | T[]): string {
|
|
172
|
-
if (
|
|
173
|
-
return value.map(v => displayValue(v)).join(', ')
|
|
190
|
+
if (multiple.value && Array.isArray(value)) {
|
|
191
|
+
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
|
|
174
192
|
}
|
|
175
193
|
|
|
176
|
-
|
|
194
|
+
if (!props.valueKey) {
|
|
195
|
+
return value && (typeof value === 'object' ? get(value, props.labelKey as string) : value)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const item = items.value.find(item => isEqual(get(item as Record<string, any>, props.valueKey as string), value)) ?? (props.createItem && value)
|
|
177
199
|
|
|
178
200
|
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
|
179
201
|
}
|
|
180
202
|
|
|
181
|
-
function filterFunction(
|
|
203
|
+
function filterFunction(
|
|
204
|
+
inputItems: ArrayOrWrapped<T> = items.value as ArrayOrWrapped<T>,
|
|
205
|
+
filterSearchTerm: string = searchTerm.value,
|
|
206
|
+
comparator = (item: any, term: string) => String(item).search(new RegExp(term, 'i')) !== -1
|
|
207
|
+
): ArrayOrWrapped<T> {
|
|
182
208
|
if (props.filter === false) {
|
|
183
|
-
return
|
|
209
|
+
return inputItems
|
|
184
210
|
}
|
|
185
211
|
|
|
186
212
|
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
|
|
187
|
-
const escapedSearchTerm = escapeRegExp(
|
|
213
|
+
const escapedSearchTerm = escapeRegExp(filterSearchTerm)
|
|
188
214
|
|
|
189
|
-
return
|
|
215
|
+
return inputItems.filter((item: T) => {
|
|
190
216
|
if (typeof item !== 'object') {
|
|
191
|
-
return
|
|
217
|
+
return comparator(item, escapedSearchTerm)
|
|
192
218
|
}
|
|
193
219
|
|
|
194
220
|
return fields.some((field) => {
|
|
195
221
|
const child = get(item, field as string)
|
|
196
222
|
|
|
197
|
-
return child !== null && child !== undefined &&
|
|
223
|
+
return child !== null && child !== undefined && comparator(child, escapedSearchTerm)
|
|
198
224
|
})
|
|
199
225
|
}) as ArrayOrWrapped<T>
|
|
200
226
|
}
|
|
@@ -203,7 +229,40 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|
|
203
229
|
// eslint-disable-next-line vue/no-dupe-keys
|
|
204
230
|
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
|
205
231
|
|
|
232
|
+
const creatable = computed(() => {
|
|
233
|
+
if (!props.createItem) {
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const isModelValueCustom = props.modelValue && filterFunction((props.multiple && Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue]) as ArrayOrWrapped<T>, searchTerm.value, (item, term) => String(item) === term).length === 1
|
|
238
|
+
|
|
239
|
+
if (isModelValueCustom) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const filteredItems = filterFunction()
|
|
244
|
+
const newItem = searchTerm.value && {
|
|
245
|
+
item: props.valueKey ? { [props.valueKey]: searchTerm.value, [props.labelKey ?? 'label']: searchTerm.value } : searchTerm.value,
|
|
246
|
+
position: ((typeof props.createItem === 'object' && props.createItem.placement) || 'bottom') as 'top' | 'bottom'
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
|
|
250
|
+
return (filteredItems.length === 1 && filterFunction(filteredItems, searchTerm.value, (item, term) => String(item) === term).length === 1) ? false : newItem
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return filteredItems.length > 0 ? false : newItem
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const rootItems = computed(() => [
|
|
257
|
+
...(creatable.value && creatable.value.position === 'top' ? [creatable.value.item] : []),
|
|
258
|
+
...filterFunction(),
|
|
259
|
+
...(creatable.value && creatable.value.position === 'bottom' ? [creatable.value.item] : [])
|
|
260
|
+
] as ArrayOrWrapped<T>)
|
|
261
|
+
|
|
206
262
|
function onUpdate(value: any) {
|
|
263
|
+
if (toRaw(props.modelValue) === value) {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
207
266
|
// @ts-expect-error - 'target' does not exist in type 'EventInit'
|
|
208
267
|
const event = new Event('change', { target: { value } })
|
|
209
268
|
emits('change', event)
|
|
@@ -224,6 +283,22 @@ function onUpdateOpen(value: boolean) {
|
|
|
224
283
|
</script>
|
|
225
284
|
|
|
226
285
|
<template>
|
|
286
|
+
<DefineCreateItemTemplate>
|
|
287
|
+
<ComboboxGroup v-if="creatable" :class="ui.group({ class: props.ui?.group })">
|
|
288
|
+
<ComboboxItem
|
|
289
|
+
:class="ui.item({ class: props.ui?.item })"
|
|
290
|
+
:value="valueKey && typeof creatable.item === 'object' ? get(creatable.item, props.valueKey as string) : creatable.item"
|
|
291
|
+
@select="e => emits('create', e, (creatable as any).item as T)"
|
|
292
|
+
>
|
|
293
|
+
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
|
294
|
+
<slot name="create-item-label" :item="(creatable.item as T)">
|
|
295
|
+
{{ t('selectMenu.create', { label: typeof creatable.item === 'object' ? get(creatable.item, props.labelKey as string) : creatable.item }) }}
|
|
296
|
+
</slot>
|
|
297
|
+
</span>
|
|
298
|
+
</ComboboxItem>
|
|
299
|
+
</ComboboxGroup>
|
|
300
|
+
</DefineCreateItemTemplate>
|
|
301
|
+
|
|
227
302
|
<ComboboxRoot
|
|
228
303
|
:id="id"
|
|
229
304
|
v-slot="{ modelValue, open }"
|
|
@@ -232,8 +307,9 @@ function onUpdateOpen(value: boolean) {
|
|
|
232
307
|
as-child
|
|
233
308
|
:name="name"
|
|
234
309
|
:disabled="disabled"
|
|
310
|
+
:multiple="multiple"
|
|
235
311
|
:display-value="() => searchTerm"
|
|
236
|
-
:filter-function="
|
|
312
|
+
:filter-function="() => rootItems"
|
|
237
313
|
@update:model-value="onUpdate"
|
|
238
314
|
@update:open="onUpdateOpen"
|
|
239
315
|
>
|
|
@@ -267,21 +343,19 @@ function onUpdateOpen(value: boolean) {
|
|
|
267
343
|
|
|
268
344
|
<ComboboxPortal :disabled="!portal">
|
|
269
345
|
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
|
|
270
|
-
<ComboboxInput
|
|
271
|
-
v-
|
|
272
|
-
|
|
273
|
-
autocomplete="off"
|
|
274
|
-
v-bind="typeof searchInput === 'object' ? searchInput : {}"
|
|
275
|
-
:class="ui.input({ class: props.ui?.input })"
|
|
276
|
-
/>
|
|
346
|
+
<ComboboxInput v-if="!!searchInput" as-child>
|
|
347
|
+
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
|
|
348
|
+
</ComboboxInput>
|
|
277
349
|
|
|
278
350
|
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
|
|
279
351
|
<slot name="empty" :search-term="searchTerm">
|
|
280
|
-
{{ searchTerm ?
|
|
352
|
+
{{ searchTerm ? t('selectMenu.noMatch', { searchTerm }) : t('selectMenu.noData') }}
|
|
281
353
|
</slot>
|
|
282
354
|
</ComboboxEmpty>
|
|
283
355
|
|
|
284
356
|
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
|
|
357
|
+
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'top'" />
|
|
358
|
+
|
|
285
359
|
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
|
286
360
|
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
|
287
361
|
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
|
@@ -328,6 +402,8 @@ function onUpdateOpen(value: boolean) {
|
|
|
328
402
|
</ComboboxItem>
|
|
329
403
|
</template>
|
|
330
404
|
</ComboboxGroup>
|
|
405
|
+
|
|
406
|
+
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'bottom'" />
|
|
331
407
|
</ComboboxViewport>
|
|
332
408
|
|
|
333
409
|
<ComboboxArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
|
|
@@ -75,6 +75,7 @@ import { computed, toRef } from 'vue'
|
|
|
75
75
|
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, useForwardPropsEmits } from 'radix-vue'
|
|
76
76
|
import { reactivePick } from '@vueuse/core'
|
|
77
77
|
import { useAppConfig } from '#imports'
|
|
78
|
+
import { useLocale } from '../composables/useLocale'
|
|
78
79
|
import UButton from './Button.vue'
|
|
79
80
|
|
|
80
81
|
const props = withDefaults(defineProps<SlideoverProps>(), {
|
|
@@ -94,14 +95,22 @@ const contentEvents = computed(() => {
|
|
|
94
95
|
if (props.preventClose) {
|
|
95
96
|
return {
|
|
96
97
|
pointerDownOutside: (e: Event) => e.preventDefault(),
|
|
97
|
-
interactOutside: (e: Event) => e.preventDefault()
|
|
98
|
+
interactOutside: (e: Event) => e.preventDefault(),
|
|
99
|
+
escapeKeyDown: (e: Event) => e.preventDefault()
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
interactOutside: (e: Event) => {
|
|
105
|
+
if (e.target instanceof Element && e.target.closest('[data-sonner-toaster]')) {
|
|
106
|
+
return e.preventDefault()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
102
110
|
})
|
|
103
111
|
|
|
104
112
|
const appConfig = useAppConfig()
|
|
113
|
+
const { t } = useLocale()
|
|
105
114
|
|
|
106
115
|
const ui = computed(() => slideover({
|
|
107
116
|
transition: props.transition,
|
|
@@ -142,7 +151,7 @@ const ui = computed(() => slideover({
|
|
|
142
151
|
size="md"
|
|
143
152
|
color="neutral"
|
|
144
153
|
variant="ghost"
|
|
145
|
-
aria-label="
|
|
154
|
+
:aria-label="t('slideover.close')"
|
|
146
155
|
v-bind="typeof close === 'object' ? close : undefined"
|
|
147
156
|
:class="ui.close({ class: props.ui?.close })"
|
|
148
157
|
/>
|