@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
|
@@ -35,7 +35,7 @@ export interface CommandPaletteGroup<T> {
|
|
|
35
35
|
slot?: string
|
|
36
36
|
items?: T[]
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* Whether to filter group items with [useFuse](https://vueuse.org/integrations/useFuse).
|
|
39
39
|
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
|
|
40
40
|
* @defaultValue true
|
|
41
41
|
*/
|
|
@@ -124,6 +124,7 @@ import { defu } from 'defu'
|
|
|
124
124
|
import { reactivePick } from '@vueuse/core'
|
|
125
125
|
import { useFuse } from '@vueuse/integrations/useFuse'
|
|
126
126
|
import { useAppConfig } from '#imports'
|
|
127
|
+
import { useLocale } from '../composables/useLocale'
|
|
127
128
|
import { omit, get } from '../utils'
|
|
128
129
|
import { highlight } from '../utils/fuse'
|
|
129
130
|
import UIcon from './Icon.vue'
|
|
@@ -144,6 +145,7 @@ const slots = defineSlots<CommandPaletteSlots<G, T>>()
|
|
|
144
145
|
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
|
145
146
|
|
|
146
147
|
const appConfig = useAppConfig()
|
|
148
|
+
const { t } = useLocale()
|
|
147
149
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'disabled', 'multiple', 'modelValue', 'defaultValue', 'selectedValue', 'resetSearchTermOnBlur'), emits)
|
|
148
150
|
const inputProps = useForwardProps(reactivePick(props, 'loading', 'loadingIcon', 'placeholder'))
|
|
149
151
|
|
|
@@ -245,7 +247,7 @@ const groups = computed(() => {
|
|
|
245
247
|
size="md"
|
|
246
248
|
color="neutral"
|
|
247
249
|
variant="ghost"
|
|
248
|
-
aria-label="
|
|
250
|
+
:aria-label="t('commandPalette.close')"
|
|
249
251
|
v-bind="typeof close === 'object' ? close : undefined"
|
|
250
252
|
:class="ui.close({ class: props.ui?.close })"
|
|
251
253
|
@click="emits('update:open', false)"
|
|
@@ -259,7 +261,7 @@ const groups = computed(() => {
|
|
|
259
261
|
<ComboboxContent :class="ui.content({ class: props.ui?.content })" :dismissable="false">
|
|
260
262
|
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
|
|
261
263
|
<slot name="empty" :search-term="searchTerm">
|
|
262
|
-
{{ searchTerm ?
|
|
264
|
+
{{ searchTerm ? t('commandPalette.noMatch', { searchTerm }) : t('commandPalette.noData') }}
|
|
263
265
|
</slot>
|
|
264
266
|
</ComboboxEmpty>
|
|
265
267
|
|
|
@@ -34,6 +34,8 @@ import ULink from './Link.vue'
|
|
|
34
34
|
import UAvatar from './Avatar.vue'
|
|
35
35
|
import UIcon from './Icon.vue'
|
|
36
36
|
import UKbd from './Kbd.vue'
|
|
37
|
+
// eslint-disable-next-line import/no-self-import
|
|
38
|
+
import UContextMenuContent from './ContextMenuContent.vue'
|
|
37
39
|
|
|
38
40
|
const props = defineProps<ContextMenuContentProps<T>>()
|
|
39
41
|
const emits = defineEmits<ContextMenuContentEmits>()
|
|
@@ -40,6 +40,8 @@ import ULink from './Link.vue'
|
|
|
40
40
|
import UAvatar from './Avatar.vue'
|
|
41
41
|
import UIcon from './Icon.vue'
|
|
42
42
|
import UKbd from './Kbd.vue'
|
|
43
|
+
// eslint-disable-next-line import/no-self-import
|
|
44
|
+
import UDropdownMenuContent from './DropdownMenuContent.vue'
|
|
43
45
|
|
|
44
46
|
const props = defineProps<DropdownMenuContentProps<T>>()
|
|
45
47
|
const emits = defineEmits<DropdownMenuContentEmits>()
|
|
@@ -38,7 +38,7 @@ extendDevtoolsMeta({ example: 'FormExample' })
|
|
|
38
38
|
import { provide, inject, nextTick, ref, onUnmounted, onMounted, computed, useId, readonly } from 'vue'
|
|
39
39
|
import { useEventBus } from '@vueuse/core'
|
|
40
40
|
import { formOptionsInjectionKey, formInputsInjectionKey, formBusInjectionKey, formLoadingInjectionKey } from '../composables/useFormField'
|
|
41
|
-
import {
|
|
41
|
+
import { parseSchema } from '../utils/form'
|
|
42
42
|
import { FormValidationException } from '../types/form'
|
|
43
43
|
|
|
44
44
|
const props = withDefaults(defineProps<FormProps<T>>(), {
|
|
@@ -94,13 +94,13 @@ onUnmounted(() => {
|
|
|
94
94
|
const errors = ref<FormErrorWithId[]>([])
|
|
95
95
|
provide('form-errors', errors)
|
|
96
96
|
|
|
97
|
-
const inputs = ref<Record<string, string>>({})
|
|
97
|
+
const inputs = ref<Record<string, { id?: string, pattern?: RegExp }>>({})
|
|
98
98
|
provide(formInputsInjectionKey, inputs)
|
|
99
99
|
|
|
100
100
|
function resolveErrorIds(errs: FormError[]): FormErrorWithId[] {
|
|
101
101
|
return errs.map(err => ({
|
|
102
102
|
...err,
|
|
103
|
-
id: inputs.value[err.name]
|
|
103
|
+
id: inputs.value[err.name]?.id
|
|
104
104
|
}))
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -108,20 +108,11 @@ async function getErrors(): Promise<FormErrorWithId[]> {
|
|
|
108
108
|
let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
|
|
109
109
|
|
|
110
110
|
if (props.schema) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
errs = errs.concat(await getYupErrors(props.state, props.schema))
|
|
115
|
-
} else if (isJoiSchema(props.schema)) {
|
|
116
|
-
errs = errs.concat(await getJoiErrors(props.state, props.schema))
|
|
117
|
-
} else if (isValibotSchema(props.schema)) {
|
|
118
|
-
errs = errs.concat(await getValibotErrors(props.state, props.schema))
|
|
119
|
-
} else if (isSuperStructSchema(props.schema)) {
|
|
120
|
-
errs = errs.concat(await getSuperStructErrors(props.state, props.schema))
|
|
121
|
-
} else if (isStandardSchema(props.schema)) {
|
|
122
|
-
errs = errs.concat(await getStandardErrors(props.state, props.schema))
|
|
111
|
+
const { errors, result } = await parseSchema(props.state, props.schema as FormSchema<typeof props.state>)
|
|
112
|
+
if (errors) {
|
|
113
|
+
errs = errs.concat(errors)
|
|
123
114
|
} else {
|
|
124
|
-
|
|
115
|
+
Object.assign(props.state, result)
|
|
125
116
|
}
|
|
126
117
|
}
|
|
127
118
|
|
|
@@ -129,7 +120,7 @@ async function getErrors(): Promise<FormErrorWithId[]> {
|
|
|
129
120
|
}
|
|
130
121
|
|
|
131
122
|
async function _validate(opts: { name?: string | string[], silent?: boolean, nested?: boolean } = { silent: false, nested: true }): Promise<T | false> {
|
|
132
|
-
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name
|
|
123
|
+
const names = opts.name && !Array.isArray(opts.name) ? [opts.name] : opts.name as string[]
|
|
133
124
|
|
|
134
125
|
const nestedValidatePromises = !names && opts.nested
|
|
135
126
|
? Array.from(nestedForms.value.values()).map(
|
|
@@ -143,9 +134,16 @@ async function _validate(opts: { name?: string | string[], silent?: boolean, nes
|
|
|
143
134
|
: []
|
|
144
135
|
|
|
145
136
|
if (names) {
|
|
146
|
-
const otherErrors = errors.value.filter(error => !names
|
|
147
|
-
|
|
148
|
-
|
|
137
|
+
const otherErrors = errors.value.filter(error => !names.some((name) => {
|
|
138
|
+
const pattern = inputs.value?.[name]?.pattern
|
|
139
|
+
return name === error.name || (pattern && error.name.match(pattern))
|
|
140
|
+
}))
|
|
141
|
+
|
|
142
|
+
const pathErrors = (await getErrors()).filter(error => names.some((name) => {
|
|
143
|
+
const pattern = inputs.value?.[name]?.pattern
|
|
144
|
+
return name === error.name || (pattern && error.name.match(pattern))
|
|
145
|
+
}))
|
|
146
|
+
|
|
149
147
|
errors.value = otherErrors.concat(pathErrors)
|
|
150
148
|
} else {
|
|
151
149
|
errors.value = await getErrors()
|
|
@@ -12,7 +12,10 @@ const formField = tv({ extend: tv(theme), ...(appConfig.ui?.formField || {}) })
|
|
|
12
12
|
type FormFieldVariants = VariantProps<typeof formField>
|
|
13
13
|
|
|
14
14
|
export interface FormFieldProps {
|
|
15
|
+
/** The name of the FormField. Also used to match form errors. */
|
|
15
16
|
name?: string
|
|
17
|
+
/** A regular expression to match form error names. */
|
|
18
|
+
errorPattern?: RegExp
|
|
16
19
|
label?: string
|
|
17
20
|
description?: string
|
|
18
21
|
help?: string
|
|
@@ -54,7 +57,7 @@ const ui = computed(() => formField({
|
|
|
54
57
|
|
|
55
58
|
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
|
|
56
59
|
|
|
57
|
-
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name)?.message)
|
|
60
|
+
const error = computed(() => props.error || formErrors?.value?.find(error => error.name === props.name || (props.errorPattern && error.name.match(props.errorPattern)))?.message)
|
|
58
61
|
|
|
59
62
|
const id = ref(useId())
|
|
60
63
|
|
|
@@ -65,7 +68,8 @@ provide(formFieldInjectionKey, computed(() => ({
|
|
|
65
68
|
name: props.name,
|
|
66
69
|
size: props.size,
|
|
67
70
|
eagerValidation: props.eagerValidation,
|
|
68
|
-
validateOnInputDelay: props.validateOnInputDelay
|
|
71
|
+
validateOnInputDelay: props.validateOnInputDelay,
|
|
72
|
+
errorPattern: props.errorPattern
|
|
69
73
|
}) as FormFieldInjectedOptions<FormFieldProps>))
|
|
70
74
|
</script>
|
|
71
75
|
|
|
@@ -92,7 +96,7 @@ provide(formFieldInjectionKey, computed(() => ({
|
|
|
92
96
|
</p>
|
|
93
97
|
</div>
|
|
94
98
|
|
|
95
|
-
<div :class="[label && ui.container({ class: props.ui?.container })]">
|
|
99
|
+
<div :class="[(label || !!slots.label || description || !!slots.description) && ui.container({ class: props.ui?.container })]">
|
|
96
100
|
<slot :error="error" />
|
|
97
101
|
|
|
98
102
|
<p v-if="(typeof error === 'string' && error) || !!slots.error" :class="ui.error({ class: props.ui?.error })">
|
|
@@ -78,9 +78,10 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
|
|
|
78
78
|
*/
|
|
79
79
|
portal?: boolean
|
|
80
80
|
/**
|
|
81
|
-
* Whether to filter items or not, can be an array of fields to filter.
|
|
82
|
-
* When `false`, items will not be filtered which is useful for custom filtering.
|
|
83
|
-
*
|
|
81
|
+
* Whether to filter items or not, can be an array of fields to filter. Defaults to `[labelKey]`.
|
|
82
|
+
* When `false`, items will not be filtered which is useful for custom filtering (useAsyncData, useFetch, etc.).
|
|
83
|
+
* `['label']`{lang="ts-type"}
|
|
84
|
+
* @defaultValue true
|
|
84
85
|
*/
|
|
85
86
|
filter?: boolean | string[]
|
|
86
87
|
/**
|
|
@@ -92,10 +93,15 @@ export interface InputMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends Ma
|
|
|
92
93
|
* When `items` is an array of objects, select the field to use as the label.
|
|
93
94
|
* @defaultValue 'label'
|
|
94
95
|
*/
|
|
95
|
-
labelKey?:
|
|
96
|
+
labelKey?: V
|
|
96
97
|
items?: I
|
|
97
98
|
/** Highlight the ring color like a focus state. */
|
|
98
99
|
highlight?: boolean
|
|
100
|
+
/**
|
|
101
|
+
* Determines if custom user input that does not exist in options can be added.
|
|
102
|
+
* @defaultValue false
|
|
103
|
+
*/
|
|
104
|
+
createItem?: boolean | 'always' | { placement?: 'top' | 'bottom', when?: 'empty' | 'always' }
|
|
99
105
|
class?: any
|
|
100
106
|
ui?: PartialString<typeof inputMenu.slots>
|
|
101
107
|
/** The controlled value of the Combobox. Can be binded-with with `v-model`. */
|
|
@@ -108,6 +114,7 @@ export type InputMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>,
|
|
|
108
114
|
change: [payload: Event]
|
|
109
115
|
blur: [payload: FocusEvent]
|
|
110
116
|
focus: [payload: FocusEvent]
|
|
117
|
+
create: [payload: Event, item: T]
|
|
111
118
|
} & SelectModelValueEmits<T, V, M>
|
|
112
119
|
|
|
113
120
|
type SlotProps<T> = (props: { item: T, index: number }) => any
|
|
@@ -122,21 +129,23 @@ export interface InputMenuSlots<T> {
|
|
|
122
129
|
'item-trailing': SlotProps<T>
|
|
123
130
|
'tags-item-text': SlotProps<T>
|
|
124
131
|
'tags-item-delete': SlotProps<T>
|
|
132
|
+
'create-item-label'(props: { item: T }): any
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
extendDevtoolsMeta({ defaultProps: { items: ['Option 1', 'Option 2', 'Option 3'] } })
|
|
128
136
|
</script>
|
|
129
137
|
|
|
130
138
|
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<InputMenuItem | AcceptableValue> = MaybeArrayOfArray<InputMenuItem | AcceptableValue>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
|
131
|
-
import { computed, ref, toRef, onMounted } from 'vue'
|
|
139
|
+
import { computed, ref, toRef, onMounted, toRaw } from 'vue'
|
|
132
140
|
import { ComboboxRoot, ComboboxArrow, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, TagsInputRoot, TagsInputItem, TagsInputItemText, TagsInputItemDelete, TagsInputInput, useForwardPropsEmits } from 'radix-vue'
|
|
133
141
|
import { defu } from 'defu'
|
|
134
|
-
import
|
|
135
|
-
import { reactivePick } from '@vueuse/core'
|
|
142
|
+
import { isEqual } from 'ohash'
|
|
143
|
+
import { reactivePick, createReusableTemplate } from '@vueuse/core'
|
|
136
144
|
import { useAppConfig } from '#imports'
|
|
137
145
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
138
146
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
139
147
|
import { useFormField } from '../composables/useFormField'
|
|
148
|
+
import { useLocale } from '../composables/useLocale'
|
|
140
149
|
import { get, escapeRegExp } from '../utils'
|
|
141
150
|
import UIcon from './Icon.vue'
|
|
142
151
|
import UAvatar from './Avatar.vue'
|
|
@@ -148,8 +157,8 @@ const props = withDefaults(defineProps<InputMenuProps<T, I, V, M>>(), {
|
|
|
148
157
|
type: 'text',
|
|
149
158
|
autofocusDelay: 0,
|
|
150
159
|
portal: true,
|
|
151
|
-
filter:
|
|
152
|
-
labelKey: 'label' as
|
|
160
|
+
filter: true,
|
|
161
|
+
labelKey: 'label' as never
|
|
153
162
|
})
|
|
154
163
|
const emits = defineEmits<InputMenuEmits<T, V, M>>()
|
|
155
164
|
const slots = defineSlots<InputMenuSlots<T>>()
|
|
@@ -157,14 +166,19 @@ const slots = defineSlots<InputMenuSlots<T>>()
|
|
|
157
166
|
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
|
158
167
|
|
|
159
168
|
const appConfig = useAppConfig()
|
|
160
|
-
const
|
|
169
|
+
const { t } = useLocale()
|
|
170
|
+
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'selectedValue', 'open', 'defaultOpen', 'resetSearchTermOnBlur'), emits)
|
|
161
171
|
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, position: 'popper' }) as ComboboxContentProps)
|
|
162
172
|
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
|
|
173
|
+
// This is a hack due to generic boolean casting (see https://github.com/nuxt/ui/issues/2541)
|
|
174
|
+
const multiple = toRef(() => typeof props.multiple === 'string' ? true : props.multiple)
|
|
163
175
|
|
|
164
176
|
const { emitFormBlur, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled } = useFormField<InputProps>(props)
|
|
165
177
|
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
|
166
178
|
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))
|
|
167
179
|
|
|
180
|
+
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
|
|
181
|
+
|
|
168
182
|
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
|
|
169
183
|
|
|
170
184
|
const ui = computed(() => inputMenu({
|
|
@@ -175,33 +189,41 @@ const ui = computed(() => inputMenu({
|
|
|
175
189
|
highlight: highlight.value,
|
|
176
190
|
leading: isLeading.value || !!props.avatar || !!slots.leading,
|
|
177
191
|
trailing: isTrailing.value || !!slots.trailing,
|
|
178
|
-
multiple:
|
|
192
|
+
multiple: multiple.value,
|
|
179
193
|
buttonGroup: orientation.value
|
|
180
194
|
}))
|
|
181
195
|
|
|
182
|
-
function displayValue(value:
|
|
183
|
-
|
|
196
|
+
function displayValue(value: T): string {
|
|
197
|
+
if (!props.valueKey) {
|
|
198
|
+
return value && (typeof value === 'object' ? get(value, props.labelKey as string) : value)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const item = items.value.find(item => isEqual(get(item as Record<string, any>, props.valueKey as string), value))
|
|
184
202
|
|
|
185
203
|
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
function filterFunction(
|
|
206
|
+
function filterFunction(
|
|
207
|
+
inputItems: ArrayOrWrapped<T> = items.value as ArrayOrWrapped<T>,
|
|
208
|
+
filterSearchTerm: string = searchTerm.value,
|
|
209
|
+
comparator = (item: any, term: string) => String(item).search(new RegExp(term, 'i')) !== -1
|
|
210
|
+
): ArrayOrWrapped<T> {
|
|
189
211
|
if (props.filter === false) {
|
|
190
|
-
return
|
|
212
|
+
return inputItems
|
|
191
213
|
}
|
|
192
214
|
|
|
193
215
|
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
|
|
194
|
-
const escapedSearchTerm = escapeRegExp(
|
|
216
|
+
const escapedSearchTerm = escapeRegExp(filterSearchTerm ?? '')
|
|
195
217
|
|
|
196
|
-
return
|
|
218
|
+
return inputItems.filter((item) => {
|
|
197
219
|
if (typeof item !== 'object') {
|
|
198
|
-
return
|
|
220
|
+
return comparator(item, escapedSearchTerm)
|
|
199
221
|
}
|
|
200
222
|
|
|
201
223
|
return fields.some((field) => {
|
|
202
224
|
const child = get(item, field as string)
|
|
203
225
|
|
|
204
|
-
return child !== null && child !== undefined &&
|
|
226
|
+
return child !== null && child !== undefined && comparator(child, escapedSearchTerm)
|
|
205
227
|
})
|
|
206
228
|
}) as ArrayOrWrapped<T>
|
|
207
229
|
}
|
|
@@ -210,6 +232,36 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
|
|
|
210
232
|
// eslint-disable-next-line vue/no-dupe-keys
|
|
211
233
|
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
|
212
234
|
|
|
235
|
+
const creatable = computed(() => {
|
|
236
|
+
if (!props.createItem) {
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
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
|
|
241
|
+
|
|
242
|
+
if (isModelValueCustom) {
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const filteredItems = filterFunction()
|
|
247
|
+
const newItem = searchTerm.value && {
|
|
248
|
+
item: props.valueKey ? { [props.valueKey]: searchTerm.value, [props.labelKey ?? 'label']: searchTerm.value } : searchTerm.value,
|
|
249
|
+
position: ((typeof props.createItem === 'object' && props.createItem.placement) || 'bottom') as 'top' | 'bottom'
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
|
|
253
|
+
return (filteredItems.length === 1 && filterFunction(filteredItems, searchTerm.value, (item, term) => String(item) === term).length === 1) ? false : newItem
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return filteredItems.length > 0 ? false : newItem
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const rootItems = computed(() => [
|
|
260
|
+
...(creatable.value && creatable.value.position === 'top' ? [creatable.value.item] : []),
|
|
261
|
+
...filterFunction(),
|
|
262
|
+
...(creatable.value && creatable.value.position === 'bottom' ? [creatable.value.item] : [])
|
|
263
|
+
] as ArrayOrWrapped<T>)
|
|
264
|
+
|
|
213
265
|
const inputRef = ref<InstanceType<typeof ComboboxInput> | null>(null)
|
|
214
266
|
|
|
215
267
|
function autoFocus() {
|
|
@@ -225,6 +277,9 @@ onMounted(() => {
|
|
|
225
277
|
})
|
|
226
278
|
|
|
227
279
|
function onUpdate(value: any) {
|
|
280
|
+
if (toRaw(props.modelValue) === value) {
|
|
281
|
+
return
|
|
282
|
+
}
|
|
228
283
|
// @ts-expect-error - 'target' does not exist in type 'EventInit'
|
|
229
284
|
const event = new Event('change', { target: { value } })
|
|
230
285
|
emits('change', event)
|
|
@@ -258,6 +313,22 @@ defineExpose({
|
|
|
258
313
|
</script>
|
|
259
314
|
|
|
260
315
|
<template>
|
|
316
|
+
<DefineCreateItemTemplate>
|
|
317
|
+
<ComboboxGroup v-if="creatable" :class="ui.group({ class: props.ui?.group })">
|
|
318
|
+
<ComboboxItem
|
|
319
|
+
:class="ui.item({ class: props.ui?.item })"
|
|
320
|
+
:value="valueKey && typeof creatable.item === 'object' ? get(creatable.item, props.valueKey as string) : creatable.item"
|
|
321
|
+
@select="e => emits('create', e, (creatable as any).item as T)"
|
|
322
|
+
>
|
|
323
|
+
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
|
324
|
+
<slot name="create-item-label" :item="(creatable.item as T)">
|
|
325
|
+
{{ t('inputMenu.create', { label: typeof creatable.item === 'object' ? get(creatable.item, props.labelKey as string) : creatable.item }) }}
|
|
326
|
+
</slot>
|
|
327
|
+
</span>
|
|
328
|
+
</ComboboxItem>
|
|
329
|
+
</ComboboxGroup>
|
|
330
|
+
</DefineCreateItemTemplate>
|
|
331
|
+
|
|
261
332
|
<ComboboxRoot
|
|
262
333
|
:id="id"
|
|
263
334
|
v-slot="{ modelValue, open }"
|
|
@@ -265,8 +336,9 @@ defineExpose({
|
|
|
265
336
|
v-model:search-term="searchTerm"
|
|
266
337
|
:name="name"
|
|
267
338
|
:disabled="disabled"
|
|
339
|
+
:multiple="multiple"
|
|
268
340
|
:display-value="displayValue"
|
|
269
|
-
:filter-function="
|
|
341
|
+
:filter-function="() => rootItems"
|
|
270
342
|
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
|
271
343
|
:as-child="!!multiple"
|
|
272
344
|
@update:model-value="onUpdate"
|
|
@@ -340,11 +412,13 @@ defineExpose({
|
|
|
340
412
|
<ComboboxContent :class="ui.content({ class: props.ui?.content })" v-bind="contentProps">
|
|
341
413
|
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
|
|
342
414
|
<slot name="empty" :search-term="searchTerm">
|
|
343
|
-
{{ searchTerm ?
|
|
415
|
+
{{ searchTerm ? t('inputMenu.noMatch', { searchTerm }) : t('inputMenu.noData') }}
|
|
344
416
|
</slot>
|
|
345
417
|
</ComboboxEmpty>
|
|
346
418
|
|
|
347
419
|
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
|
|
420
|
+
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'top'" />
|
|
421
|
+
|
|
348
422
|
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
|
349
423
|
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
|
350
424
|
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
|
@@ -391,6 +465,8 @@ defineExpose({
|
|
|
391
465
|
</ComboboxItem>
|
|
392
466
|
</template>
|
|
393
467
|
</ComboboxGroup>
|
|
468
|
+
|
|
469
|
+
<ReuseCreateItemTemplate v-if="creatable && creatable.position === 'bottom'" />
|
|
394
470
|
</ComboboxViewport>
|
|
395
471
|
|
|
396
472
|
<ComboboxArrow v-if="!!arrow" v-bind="arrowProps" :class="ui.arrow({ class: props.ui?.arrow })" />
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
+
import type { NumberFieldRootProps } from 'radix-vue'
|
|
4
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
5
|
+
import _appConfig from '#build/app.config'
|
|
6
|
+
import theme from '#build/ui/input-number'
|
|
7
|
+
import type { ButtonProps } from '../types'
|
|
8
|
+
|
|
9
|
+
const appConfig = _appConfig as AppConfig & { ui: { inputNumber: Partial<typeof theme> } }
|
|
10
|
+
|
|
11
|
+
const inputNumber = tv({ extend: tv(theme), ...(appConfig.ui?.inputNumber || {}) })
|
|
12
|
+
|
|
13
|
+
type InputNumberVariants = VariantProps<typeof inputNumber>
|
|
14
|
+
|
|
15
|
+
export interface InputNumberProps extends Pick<NumberFieldRootProps, 'modelValue' | 'defaultValue' | 'min' | 'max' | 'step' | 'disabled' | 'required' | 'id' | 'name' | 'formatOptions'> {
|
|
16
|
+
/**
|
|
17
|
+
* The element or component this component should render as.
|
|
18
|
+
* @defaultValue 'div'
|
|
19
|
+
*/
|
|
20
|
+
as?: any
|
|
21
|
+
class?: any
|
|
22
|
+
/** The placeholder text when the input is empty. */
|
|
23
|
+
placeholder?: string
|
|
24
|
+
ui?: Partial<typeof inputNumber.slots>
|
|
25
|
+
color?: InputNumberVariants['color']
|
|
26
|
+
variant?: InputNumberVariants['variant']
|
|
27
|
+
size?: InputNumberVariants['size']
|
|
28
|
+
/** Highlight the ring color like a focus state. */
|
|
29
|
+
highlight?: boolean
|
|
30
|
+
/**
|
|
31
|
+
* The orientation of the input menu.
|
|
32
|
+
* @defaultValue 'horizontal'
|
|
33
|
+
*/
|
|
34
|
+
orientation?: 'vertical' | 'horizontal'
|
|
35
|
+
/**
|
|
36
|
+
* Configure the increment button. The `color` and `size` are inherited.
|
|
37
|
+
* @defaultValue { variant: 'link' }
|
|
38
|
+
*/
|
|
39
|
+
increment?: ButtonProps
|
|
40
|
+
/**
|
|
41
|
+
* The icon displayed to increment the value.
|
|
42
|
+
* @defaultValue appConfig.ui.icons.plus
|
|
43
|
+
*/
|
|
44
|
+
incrementIcon?: string
|
|
45
|
+
/**
|
|
46
|
+
* Configure the decrement button. The `color` and `size` are inherited.
|
|
47
|
+
* @defaultValue { variant: 'link' }
|
|
48
|
+
*/
|
|
49
|
+
decrement?: ButtonProps
|
|
50
|
+
/**
|
|
51
|
+
* The icon displayed to decrement the value.
|
|
52
|
+
* @defaultValue appConfig.ui.icons.minus
|
|
53
|
+
*/
|
|
54
|
+
decrementIcon?: string
|
|
55
|
+
autofocus?: boolean
|
|
56
|
+
autofocusDelay?: number
|
|
57
|
+
/**
|
|
58
|
+
* The locale to use for formatting and parsing numbers.
|
|
59
|
+
* @defaultValue UApp.locale.code
|
|
60
|
+
*/
|
|
61
|
+
locale?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface InputNumberEmits {
|
|
65
|
+
(e: 'update:modelValue', payload: number): void
|
|
66
|
+
(e: 'blur', event: FocusEvent): void
|
|
67
|
+
(e: 'change', payload: Event): void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface InputNumberSlots {
|
|
71
|
+
increment(props?: {}): any
|
|
72
|
+
decrement(props?: {}): any
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<script setup lang="ts">
|
|
77
|
+
import { onMounted, ref, computed } from 'vue'
|
|
78
|
+
import { NumberFieldRoot, NumberFieldInput, NumberFieldDecrement, NumberFieldIncrement, useForwardPropsEmits } from 'radix-vue'
|
|
79
|
+
import { reactivePick } from '@vueuse/core'
|
|
80
|
+
import { useFormField } from '../composables/useFormField'
|
|
81
|
+
import { useLocale } from '../composables/useLocale'
|
|
82
|
+
import UButton from './Button.vue'
|
|
83
|
+
|
|
84
|
+
defineOptions({ inheritAttrs: false })
|
|
85
|
+
|
|
86
|
+
const props = withDefaults(defineProps<InputNumberProps>(), {
|
|
87
|
+
orientation: 'horizontal'
|
|
88
|
+
})
|
|
89
|
+
const emits = defineEmits<InputNumberEmits>()
|
|
90
|
+
defineSlots<InputNumberSlots>()
|
|
91
|
+
|
|
92
|
+
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'formatOptions'), emits)
|
|
93
|
+
|
|
94
|
+
const { emitFormBlur, emitFormChange, emitFormInput, id, color, size, name, highlight, disabled } = useFormField<InputNumberProps>(props)
|
|
95
|
+
|
|
96
|
+
const { t, code: codeLocale } = useLocale()
|
|
97
|
+
const locale = computed(() => props.locale || codeLocale.value)
|
|
98
|
+
|
|
99
|
+
const ui = computed(() => inputNumber({
|
|
100
|
+
color: color.value,
|
|
101
|
+
variant: props.variant,
|
|
102
|
+
size: size.value,
|
|
103
|
+
highlight: highlight.value,
|
|
104
|
+
orientation: props.orientation
|
|
105
|
+
}))
|
|
106
|
+
|
|
107
|
+
const incrementIcon = computed(() => props.incrementIcon || (props.orientation === 'horizontal' ? appConfig.ui.icons.plus : appConfig.ui.icons.chevronUp))
|
|
108
|
+
const decrementIcon = computed(() => props.decrementIcon || (props.orientation === 'horizontal' ? appConfig.ui.icons.minus : appConfig.ui.icons.chevronDown))
|
|
109
|
+
|
|
110
|
+
const inputRef = ref<InstanceType<typeof NumberFieldInput> | null>(null)
|
|
111
|
+
|
|
112
|
+
function autoFocus() {
|
|
113
|
+
if (props.autofocus) {
|
|
114
|
+
inputRef.value?.$el?.focus()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onMounted(() => {
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
autoFocus()
|
|
121
|
+
}, props.autofocusDelay)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
function onUpdate(value: number) {
|
|
125
|
+
// @ts-expect-error - 'target' does not exist in type 'EventInit'
|
|
126
|
+
const event = new Event('change', { target: { value } })
|
|
127
|
+
emits('change', event)
|
|
128
|
+
|
|
129
|
+
emitFormChange()
|
|
130
|
+
emitFormInput()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function onBlur(event: FocusEvent) {
|
|
134
|
+
emitFormBlur()
|
|
135
|
+
emits('blur', event)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
defineExpose({
|
|
139
|
+
inputRef
|
|
140
|
+
})
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<template>
|
|
144
|
+
<NumberFieldRoot
|
|
145
|
+
v-bind="rootProps"
|
|
146
|
+
:id="id"
|
|
147
|
+
:class="ui.root({ class: [props.class, props.ui?.root] })"
|
|
148
|
+
:name="name"
|
|
149
|
+
:disabled="disabled"
|
|
150
|
+
:locale="locale"
|
|
151
|
+
@update:model-value="onUpdate"
|
|
152
|
+
>
|
|
153
|
+
<NumberFieldInput
|
|
154
|
+
v-bind="$attrs"
|
|
155
|
+
ref="inputRef"
|
|
156
|
+
:placeholder="placeholder"
|
|
157
|
+
:required="required"
|
|
158
|
+
:class="ui.base({ class: props.ui?.base })"
|
|
159
|
+
@blur="onBlur"
|
|
160
|
+
/>
|
|
161
|
+
|
|
162
|
+
<div :class="ui.increment({ class: props.ui?.increment })">
|
|
163
|
+
<NumberFieldIncrement as-child :disabled="disabled">
|
|
164
|
+
<slot name="increment">
|
|
165
|
+
<UButton
|
|
166
|
+
:icon="incrementIcon"
|
|
167
|
+
:color="color"
|
|
168
|
+
:size="size"
|
|
169
|
+
variant="link"
|
|
170
|
+
:aria-label="t('inputNumber.increment')"
|
|
171
|
+
v-bind="typeof increment === 'object' ? increment : undefined"
|
|
172
|
+
/>
|
|
173
|
+
</slot>
|
|
174
|
+
</NumberFieldIncrement>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div :class="ui.decrement({ class: props.ui?.decrement })">
|
|
178
|
+
<NumberFieldDecrement as-child :disabled="disabled">
|
|
179
|
+
<slot name="decrement">
|
|
180
|
+
<UButton
|
|
181
|
+
:icon="decrementIcon"
|
|
182
|
+
:color="color"
|
|
183
|
+
:size="size"
|
|
184
|
+
variant="link"
|
|
185
|
+
:aria-label="t('inputNumber.decrement')"
|
|
186
|
+
v-bind="typeof decrement === 'object' ? decrement : undefined"
|
|
187
|
+
/>
|
|
188
|
+
</slot>
|
|
189
|
+
</NumberFieldDecrement>
|
|
190
|
+
</div>
|
|
191
|
+
</NumberFieldRoot>
|
|
192
|
+
</template>
|