@bitrix24/b24ui-nuxt 0.1.6 → 0.2.1
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/b24ui/alert.ts +18 -8
- package/.nuxt/b24ui/badge.ts +1 -1
- package/.nuxt/b24ui/content/description-list.ts +17 -7
- package/.nuxt/b24ui/index.ts +2 -0
- package/.nuxt/b24ui/input-menu.ts +591 -0
- package/.nuxt/b24ui/input.ts +5 -5
- package/.nuxt/b24ui/select-menu.ts +517 -0
- package/.nuxt/b24ui/select.ts +8 -8
- package/.nuxt/b24ui/toast.ts +18 -8
- package/README.md +8 -8
- package/dist/meta.cjs +13003 -3006
- package/dist/meta.d.cts +13003 -3006
- package/dist/meta.d.mts +13003 -3006
- package/dist/meta.d.ts +13003 -3006
- package/dist/meta.mjs +13003 -3006
- package/dist/module.cjs +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/components/Alert.vue +13 -10
- package/dist/runtime/components/App.vue +2 -3
- package/dist/runtime/components/Button.vue +1 -3
- package/dist/runtime/components/InputMenu.vue +507 -0
- package/dist/runtime/components/SelectMenu.vue +465 -0
- package/dist/runtime/components/Toast.vue +26 -14
- package/dist/runtime/components/Toaster.vue +2 -2
- package/dist/runtime/components/content/DescriptionList.vue +9 -7
- package/dist/runtime/composables/useToast.d.ts +5 -4
- package/dist/runtime/composables/useToast.js +2 -2
- package/dist/runtime/types/index.d.ts +3 -0
- package/dist/runtime/types/index.js +3 -0
- package/dist/runtime/types/utils.d.ts +5 -0
- package/dist/shared/{b24ui-nuxt.n3bAiAAD.mjs → b24ui-nuxt.CrjojW8t.mjs} +332 -36
- package/dist/shared/{b24ui-nuxt.BAQG__ma.cjs → b24ui-nuxt.D5cXbZSx.cjs} +331 -35
- package/dist/unplugin.cjs +1 -1
- package/dist/unplugin.mjs +1 -1
- package/dist/vite.cjs +1 -1
- package/dist/vite.mjs +1 -1
- package/package.json +23 -2
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { VariantProps } from 'tailwind-variants'
|
|
3
|
+
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxArrowProps, AcceptableValue } from 'reka-ui'
|
|
4
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
5
|
+
import _appConfig from '#build/app.config'
|
|
6
|
+
import theme from '#build/b24ui/select-menu'
|
|
7
|
+
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
|
|
8
|
+
import { tv } from '../utils/tv'
|
|
9
|
+
import type { AvatarProps, ChipProps, InputProps, IconComponent } from '../types'
|
|
10
|
+
import type { PartialString, MaybeArrayOfArray, MaybeArrayOfArrayItem, SelectModelValue, SelectModelValueEmits, SelectItemKey } from '../types/utils'
|
|
11
|
+
|
|
12
|
+
const appConfigSelectMenu = _appConfig as AppConfig & { b24ui: { selectMenu: Partial<typeof theme> } }
|
|
13
|
+
|
|
14
|
+
const selectMenu = tv({ extend: tv(theme), ...(appConfigSelectMenu.b24ui?.selectMenu || {}) })
|
|
15
|
+
|
|
16
|
+
export interface SelectMenuItem {
|
|
17
|
+
label?: string
|
|
18
|
+
icon?: IconComponent
|
|
19
|
+
avatar?: AvatarProps
|
|
20
|
+
chip?: ChipProps
|
|
21
|
+
/**
|
|
22
|
+
* The item type.
|
|
23
|
+
* @defaultValue 'item'
|
|
24
|
+
*/
|
|
25
|
+
type?: 'label' | 'separator' | 'item'
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
onSelect?(e?: Event): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type SelectMenuVariants = VariantProps<typeof selectMenu>
|
|
31
|
+
|
|
32
|
+
export interface SelectMenuProps<T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false> extends Pick<ComboboxRootProps<T>, 'open' | 'defaultOpen' | 'disabled' | 'name' | 'resetSearchTermOnBlur' | 'highlightOnHover'>, UseComponentIconsProps {
|
|
33
|
+
id?: string
|
|
34
|
+
/** The placeholder text when the select is empty. */
|
|
35
|
+
placeholder?: string
|
|
36
|
+
/**
|
|
37
|
+
* Whether to display the search input or not.
|
|
38
|
+
* Can be an object to pass additional props to the input.
|
|
39
|
+
* `{ placeholder: 'Search...', type: 'search' }`{lang="ts-type"}
|
|
40
|
+
* @defaultValue true
|
|
41
|
+
*/
|
|
42
|
+
searchInput?: boolean | InputProps
|
|
43
|
+
color?: SelectMenuVariants['color']
|
|
44
|
+
size?: SelectMenuVariants['size']
|
|
45
|
+
/** Removes padding from input. */
|
|
46
|
+
noPadding?: boolean
|
|
47
|
+
/** removes all borders (rings). */
|
|
48
|
+
noBorder?: boolean
|
|
49
|
+
/** removes all borders (rings) except the bottom one. */
|
|
50
|
+
underline?: boolean
|
|
51
|
+
/** Rounds the corners of the button. */
|
|
52
|
+
rounded?: boolean
|
|
53
|
+
tag?: string
|
|
54
|
+
tagColor?: SelectMenuVariants['tagColor']
|
|
55
|
+
required?: boolean
|
|
56
|
+
/**
|
|
57
|
+
* The icon displayed to open the menu.
|
|
58
|
+
* @defaultValue icons.chevronDown = `ChevronDownIcon`
|
|
59
|
+
*/
|
|
60
|
+
trailingIcon?: IconComponent
|
|
61
|
+
/**
|
|
62
|
+
* The icon displayed when an item is selected.
|
|
63
|
+
* @defaultValue icons.check = `CheckIcon`
|
|
64
|
+
*/
|
|
65
|
+
selectedIcon?: IconComponent
|
|
66
|
+
/**
|
|
67
|
+
* The content of the menu.
|
|
68
|
+
* @defaultValue { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }
|
|
69
|
+
*/
|
|
70
|
+
content?: Omit<ComboboxContentProps, 'as' | 'asChild' | 'forceMount'>
|
|
71
|
+
/**
|
|
72
|
+
* Display an arrow alongside the menu.
|
|
73
|
+
* @defaultValue false
|
|
74
|
+
*/
|
|
75
|
+
arrow?: boolean | Omit<ComboboxArrowProps, 'as' | 'asChild'>
|
|
76
|
+
/**
|
|
77
|
+
* Render the menu in a portal.
|
|
78
|
+
* @defaultValue true
|
|
79
|
+
*/
|
|
80
|
+
portal?: boolean
|
|
81
|
+
/**
|
|
82
|
+
* When `items` is an array of objects, select the field to use as the value instead of the object itself.
|
|
83
|
+
* @defaultValue undefined
|
|
84
|
+
*/
|
|
85
|
+
valueKey?: V
|
|
86
|
+
/**
|
|
87
|
+
* When `items` is an array of objects, select the field to use as the label.
|
|
88
|
+
* @defaultValue 'label'
|
|
89
|
+
*/
|
|
90
|
+
labelKey?: V
|
|
91
|
+
items?: I
|
|
92
|
+
/** The value of the SelectMenu when initially rendered. Use when you do not need to control the state of the SelectMenu. */
|
|
93
|
+
defaultValue?: SelectModelValue<T, V, M>
|
|
94
|
+
/** The controlled value of the SelectMenu. Can be binded-with with `v-model`. */
|
|
95
|
+
modelValue?: SelectModelValue<T, V, M>
|
|
96
|
+
/** Whether multiple options can be selected or not. */
|
|
97
|
+
multiple?: M & boolean
|
|
98
|
+
/** Highlight the ring color like a focus state. */
|
|
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' | { position?: 'top' | 'bottom', when?: 'empty' | 'always' }
|
|
105
|
+
/**
|
|
106
|
+
* Fields to filter items by.
|
|
107
|
+
* @defaultValue [labelKey]
|
|
108
|
+
*/
|
|
109
|
+
filterFields?: string[]
|
|
110
|
+
/**
|
|
111
|
+
* When `true`, disable the default filters, useful for custom filtering (useAsyncData, useFetch, etc.).
|
|
112
|
+
* @defaultValue false
|
|
113
|
+
*/
|
|
114
|
+
ignoreFilter?: boolean
|
|
115
|
+
class?: any
|
|
116
|
+
b24ui?: PartialString<typeof selectMenu.slots>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type SelectMenuEmits<T, V, M extends boolean> = Omit<ComboboxRootEmits<T>, 'update:modelValue'> & {
|
|
120
|
+
change: [payload: Event]
|
|
121
|
+
blur: [payload: FocusEvent]
|
|
122
|
+
focus: [payload: FocusEvent]
|
|
123
|
+
create: [item: string]
|
|
124
|
+
} & SelectModelValueEmits<T, V, M>
|
|
125
|
+
|
|
126
|
+
type SlotProps<T> = (props: { item: T, index: number }) => any
|
|
127
|
+
|
|
128
|
+
export interface SelectMenuSlots<T, M extends boolean> {
|
|
129
|
+
'leading'(props: { modelValue?: M extends true ? T[] : T, open: boolean, b24ui: any }): any
|
|
130
|
+
'default'(props: { modelValue?: M extends true ? T[] : T, open: boolean }): any
|
|
131
|
+
'trailing'(props: { modelValue?: M extends true ? T[] : T, open: boolean, b24ui: any }): any
|
|
132
|
+
'empty'(props: { searchTerm?: string }): any
|
|
133
|
+
'item': SlotProps<T>
|
|
134
|
+
'item-leading': SlotProps<T>
|
|
135
|
+
'item-label': SlotProps<T>
|
|
136
|
+
'item-trailing': SlotProps<T>
|
|
137
|
+
'create-item-label'(props: { item: string }): any
|
|
138
|
+
}
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean> = MaybeArrayOfArray<SelectMenuItem | AcceptableValue | boolean>, V extends SelectItemKey<T> | undefined = undefined, M extends boolean = false">
|
|
142
|
+
import { computed, toRef, toRaw } from 'vue'
|
|
143
|
+
import {
|
|
144
|
+
ComboboxRoot,
|
|
145
|
+
ComboboxArrow,
|
|
146
|
+
ComboboxAnchor,
|
|
147
|
+
ComboboxInput,
|
|
148
|
+
ComboboxTrigger,
|
|
149
|
+
ComboboxPortal,
|
|
150
|
+
ComboboxContent,
|
|
151
|
+
ComboboxViewport,
|
|
152
|
+
ComboboxEmpty,
|
|
153
|
+
ComboboxGroup,
|
|
154
|
+
ComboboxLabel,
|
|
155
|
+
ComboboxSeparator,
|
|
156
|
+
ComboboxItem,
|
|
157
|
+
ComboboxItemIndicator,
|
|
158
|
+
useForwardPropsEmits,
|
|
159
|
+
useFilter,
|
|
160
|
+
Primitive
|
|
161
|
+
} from 'reka-ui'
|
|
162
|
+
import { defu } from 'defu'
|
|
163
|
+
import { reactivePick, createReusableTemplate } from '@vueuse/core'
|
|
164
|
+
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
165
|
+
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
166
|
+
import { useFormField } from '../composables/useFormField'
|
|
167
|
+
import { useLocale } from '../composables/useLocale'
|
|
168
|
+
import { get, compare } from '../utils'
|
|
169
|
+
import icons from '../dictionary/icons'
|
|
170
|
+
import B24Avatar from './Avatar.vue'
|
|
171
|
+
import B24Chip from './Chip.vue'
|
|
172
|
+
import B24Input from './Input.vue'
|
|
173
|
+
|
|
174
|
+
const props = withDefaults(defineProps<SelectMenuProps<T, I, V, M>>(), {
|
|
175
|
+
portal: true,
|
|
176
|
+
searchInput: true,
|
|
177
|
+
labelKey: 'label' as never,
|
|
178
|
+
resetSearchTermOnBlur: true
|
|
179
|
+
})
|
|
180
|
+
const emits = defineEmits<SelectMenuEmits<T, V, M>>()
|
|
181
|
+
const slots = defineSlots<SelectMenuSlots<T, M>>()
|
|
182
|
+
|
|
183
|
+
const searchTerm = defineModel<string>('searchTerm', { default: '' })
|
|
184
|
+
|
|
185
|
+
const { t } = useLocale()
|
|
186
|
+
const { contains } = useFilter({ sensitivity: 'base' })
|
|
187
|
+
|
|
188
|
+
const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'multiple', 'resetSearchTermOnBlur', 'highlightOnHover'), emits)
|
|
189
|
+
const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps)
|
|
190
|
+
const arrowProps = toRef(() => props.arrow as ComboboxArrowProps)
|
|
191
|
+
const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: t('selectMenu.search'), type: 'search' }) as InputProps)
|
|
192
|
+
|
|
193
|
+
const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
|
|
194
|
+
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
|
195
|
+
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(
|
|
196
|
+
props,
|
|
197
|
+
{ trailingIcon: icons.chevronDown }
|
|
198
|
+
)))
|
|
199
|
+
|
|
200
|
+
const selectSize = computed(() => buttonGroupSize.value || formGroupSize.value)
|
|
201
|
+
|
|
202
|
+
const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate()
|
|
203
|
+
|
|
204
|
+
const isTag = computed(() => {
|
|
205
|
+
return props.tag
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const b24ui = computed(() => selectMenu({
|
|
209
|
+
color: color.value,
|
|
210
|
+
size: selectSize?.value,
|
|
211
|
+
loading: props.loading,
|
|
212
|
+
rounded: Boolean(props.rounded),
|
|
213
|
+
noPadding: Boolean(props.noPadding),
|
|
214
|
+
noBorder: Boolean(props.noBorder),
|
|
215
|
+
underline: Boolean(props.underline),
|
|
216
|
+
highlight: highlight.value,
|
|
217
|
+
tagColor: props.tagColor,
|
|
218
|
+
leading: Boolean(isLeading.value || !!props.avatar || !!slots.leading),
|
|
219
|
+
trailing: Boolean(isTrailing.value || !!slots.trailing),
|
|
220
|
+
buttonGroup: orientation.value
|
|
221
|
+
}))
|
|
222
|
+
|
|
223
|
+
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectMenuItem[][] : [])
|
|
224
|
+
// eslint-disable-next-line vue/no-dupe-keys
|
|
225
|
+
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
|
226
|
+
|
|
227
|
+
function displayValue(value: T | T[]): string {
|
|
228
|
+
if (props.multiple && Array.isArray(value)) {
|
|
229
|
+
return value.map(v => displayValue(v)).filter(Boolean).join(', ')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!props.valueKey) {
|
|
233
|
+
return value && (typeof value === 'object' ? get(value, props.labelKey as string) : value)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const item = items.value.find(item => compare(typeof item === 'object' ? get(item as Record<string, any>, props.valueKey as string) : item, value))
|
|
237
|
+
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const filteredGroups = computed(() => {
|
|
241
|
+
if (props.ignoreFilter || !searchTerm.value) {
|
|
242
|
+
return groups.value
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const fields = Array.isArray(props.filterFields) ? props.filterFields : [props.labelKey] as string[]
|
|
246
|
+
|
|
247
|
+
return groups.value.map(items => items.filter((item) => {
|
|
248
|
+
if (typeof item !== 'object') {
|
|
249
|
+
return contains(item, searchTerm.value)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (item.type && ['label', 'separator'].includes(item.type)) {
|
|
253
|
+
return true
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return fields.some(field => contains(get(item, field), searchTerm.value))
|
|
257
|
+
})).filter(group => group.filter(item => !item.type || !['label', 'separator'].includes(item.type)).length > 0)
|
|
258
|
+
})
|
|
259
|
+
const filteredItems = computed(() => filteredGroups.value.flatMap(group => group) as T[])
|
|
260
|
+
|
|
261
|
+
const createItem = computed(() => {
|
|
262
|
+
if (!props.createItem || !searchTerm.value) {
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const newItem = props.valueKey ? { [props.valueKey]: searchTerm.value } as T : searchTerm.value
|
|
267
|
+
|
|
268
|
+
if ((typeof props.createItem === 'object' && props.createItem.when === 'always') || props.createItem === 'always') {
|
|
269
|
+
return !filteredItems.value.find(item => compare(item, newItem, props.valueKey))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return !filteredItems.value.length
|
|
273
|
+
})
|
|
274
|
+
const createItemPosition = computed(() => typeof props.createItem === 'object' ? props.createItem.position : 'bottom')
|
|
275
|
+
|
|
276
|
+
function onUpdate(value: any) {
|
|
277
|
+
if (toRaw(props.modelValue) === value) {
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
// @ts-expect-error - 'target' does not exist in type 'EventInit'
|
|
281
|
+
const event = new Event('change', { target: { value } })
|
|
282
|
+
emits('change', event)
|
|
283
|
+
|
|
284
|
+
emitFormChange()
|
|
285
|
+
emitFormInput()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function onUpdateOpen(value: boolean) {
|
|
289
|
+
let timeoutId
|
|
290
|
+
|
|
291
|
+
if (!value) {
|
|
292
|
+
const event = new FocusEvent('blur')
|
|
293
|
+
|
|
294
|
+
emits('blur', event)
|
|
295
|
+
emitFormBlur()
|
|
296
|
+
|
|
297
|
+
// Since we use `displayValue` prop inside ComboboxInput we should reset searchTerm manually
|
|
298
|
+
// https://reka-ui.com/docs/components/combobox#api-reference
|
|
299
|
+
if (props.resetSearchTermOnBlur) {
|
|
300
|
+
const STATE_ANIMATION_DELAY_MS = 100
|
|
301
|
+
|
|
302
|
+
timeoutId = setTimeout(() => {
|
|
303
|
+
searchTerm.value = ''
|
|
304
|
+
}, STATE_ANIMATION_DELAY_MS)
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
const event = new FocusEvent('focus')
|
|
308
|
+
emits('focus', event)
|
|
309
|
+
emitFormFocus()
|
|
310
|
+
clearTimeout(timeoutId)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
</script>
|
|
314
|
+
|
|
315
|
+
<!-- eslint-disable vue/no-template-shadow -->
|
|
316
|
+
<template>
|
|
317
|
+
<Primitive as="div" :class="b24ui.root({ addNew: true, class: [props.b24ui?.root] })">
|
|
318
|
+
<DefineCreateItemTemplate>
|
|
319
|
+
<ComboboxGroup :class="b24ui.group({ addNew: true, class: props.b24ui?.group })">
|
|
320
|
+
<ComboboxItem
|
|
321
|
+
:class="b24ui.item({ addNew: true, class: props.b24ui?.item })"
|
|
322
|
+
:value="searchTerm"
|
|
323
|
+
@select.prevent="emits('create', searchTerm)"
|
|
324
|
+
>
|
|
325
|
+
<span :class="b24ui.itemLabel({ addNew: true, class: props.b24ui?.itemLabel })">
|
|
326
|
+
<slot name="create-item-label" :item="searchTerm">
|
|
327
|
+
<Component
|
|
328
|
+
:is="icons.plus"
|
|
329
|
+
:class="b24ui.itemLeadingIcon({ addNew: true, class: props.b24ui?.itemLeadingIcon })"
|
|
330
|
+
/>
|
|
331
|
+
{{ t('selectMenu.create', { label: searchTerm }) }}
|
|
332
|
+
</slot>
|
|
333
|
+
</span>
|
|
334
|
+
</ComboboxItem>
|
|
335
|
+
</ComboboxGroup>
|
|
336
|
+
</DefineCreateItemTemplate>
|
|
337
|
+
|
|
338
|
+
<ComboboxRoot
|
|
339
|
+
:id="id"
|
|
340
|
+
v-slot="{ modelValue, open }"
|
|
341
|
+
v-bind="{ ...rootProps, ...ariaAttrs }"
|
|
342
|
+
ignore-filter
|
|
343
|
+
as-child
|
|
344
|
+
:name="name"
|
|
345
|
+
:disabled="disabled"
|
|
346
|
+
@update:model-value="onUpdate"
|
|
347
|
+
@update:open="onUpdateOpen"
|
|
348
|
+
>
|
|
349
|
+
<ComboboxAnchor as-child>
|
|
350
|
+
<ComboboxTrigger :class="b24ui.base({ class: [props.class, props.b24ui?.base] })" tabindex="0">
|
|
351
|
+
<div v-if="isTag" :class="b24ui.tag({ class: props.b24ui?.tag })">
|
|
352
|
+
{{ props.tag }}
|
|
353
|
+
</div>
|
|
354
|
+
<span v-if="isLeading || !!avatar || !!slots.leading" :class="b24ui.leading({ class: props.b24ui?.leading })">
|
|
355
|
+
<slot name="leading" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :b24ui="b24ui">
|
|
356
|
+
<Component
|
|
357
|
+
:is="leadingIconName"
|
|
358
|
+
v-if="isLeading && leadingIconName"
|
|
359
|
+
:class="b24ui.leadingIcon({ class: props.b24ui?.leadingIcon })"
|
|
360
|
+
/>
|
|
361
|
+
<B24Avatar v-else-if="!!avatar" :size="((props.b24ui?.itemLeadingAvatarSize || b24ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="b24ui.itemLeadingAvatar({ class: props.b24ui?.itemLeadingAvatar })" />
|
|
362
|
+
</slot>
|
|
363
|
+
</span>
|
|
364
|
+
|
|
365
|
+
<slot :model-value="(modelValue as M extends true ? T[] : T)" :open="open">
|
|
366
|
+
<template v-for="displayedModelValue in [displayValue(modelValue as M extends true ? T[] : T)]" :key="displayedModelValue">
|
|
367
|
+
<span v-if="displayedModelValue" :class="b24ui.value({ class: props.b24ui?.value })">
|
|
368
|
+
{{ displayedModelValue }}
|
|
369
|
+
</span>
|
|
370
|
+
<span v-else :class="b24ui.placeholder({ class: props.b24ui?.placeholder })">
|
|
371
|
+
{{ placeholder ?? ' ' }}
|
|
372
|
+
</span>
|
|
373
|
+
</template>
|
|
374
|
+
</slot>
|
|
375
|
+
|
|
376
|
+
<span v-if="isTrailing || !!slots.trailing" :class="b24ui.trailing({ class: props.b24ui?.trailing })">
|
|
377
|
+
<slot name="trailing" :model-value="(modelValue as M extends true ? T[] : T)" :open="open" :b24ui="b24ui">
|
|
378
|
+
<Component
|
|
379
|
+
:is="trailingIconName"
|
|
380
|
+
v-if="trailingIconName"
|
|
381
|
+
:class="b24ui.trailingIcon({ class: props.b24ui?.trailingIcon })"
|
|
382
|
+
/>
|
|
383
|
+
</slot>
|
|
384
|
+
</span>
|
|
385
|
+
</ComboboxTrigger>
|
|
386
|
+
</ComboboxAnchor>
|
|
387
|
+
|
|
388
|
+
<ComboboxPortal :disabled="!portal">
|
|
389
|
+
<ComboboxContent :class="b24ui.content({ class: props.b24ui?.content })" v-bind="contentProps">
|
|
390
|
+
<ComboboxInput v-if="!!searchInput" v-model="searchTerm" :display-value="() => searchTerm" as-child>
|
|
391
|
+
<B24Input no-border autofocus autocomplete="off" v-bind="searchInputProps" :class="b24ui.input({ class: props.b24ui?.input })" />
|
|
392
|
+
</ComboboxInput>
|
|
393
|
+
|
|
394
|
+
<ComboboxEmpty :class="b24ui.empty({ class: props.b24ui?.empty })">
|
|
395
|
+
<slot name="empty" :search-term="searchTerm">
|
|
396
|
+
{{ searchTerm ? t('selectMenu.noMatch', { searchTerm }) : t('selectMenu.noData') }}
|
|
397
|
+
</slot>
|
|
398
|
+
</ComboboxEmpty>
|
|
399
|
+
|
|
400
|
+
<ComboboxViewport :class="b24ui.viewport({ class: props.b24ui?.viewport })">
|
|
401
|
+
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'top'" />
|
|
402
|
+
|
|
403
|
+
<ComboboxGroup v-for="(group, groupIndex) in filteredGroups" :key="`group-${groupIndex}`" :class="b24ui.group({ class: props.b24ui?.group })">
|
|
404
|
+
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
|
405
|
+
<ComboboxLabel v-if="item?.type === 'label'" :class="b24ui.label({ class: props.b24ui?.label })">
|
|
406
|
+
{{ get(item, props.labelKey as string) }}
|
|
407
|
+
</ComboboxLabel>
|
|
408
|
+
|
|
409
|
+
<ComboboxSeparator v-else-if="item?.type === 'separator'" :class="b24ui.separator({ class: props.b24ui?.separator })" />
|
|
410
|
+
|
|
411
|
+
<ComboboxItem
|
|
412
|
+
v-else
|
|
413
|
+
:class="b24ui.item({ class: props.b24ui?.item })"
|
|
414
|
+
:disabled="item.disabled"
|
|
415
|
+
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
|
416
|
+
@select="item.onSelect"
|
|
417
|
+
>
|
|
418
|
+
<slot name="item" :item="(item as T)" :index="index">
|
|
419
|
+
<slot name="item-leading" :item="(item as T)" :index="index">
|
|
420
|
+
<Component
|
|
421
|
+
:is="item.icon"
|
|
422
|
+
v-if="item.icon"
|
|
423
|
+
:class="b24ui.itemLeadingIcon({ class: props.b24ui?.itemLeadingIcon })"
|
|
424
|
+
/>
|
|
425
|
+
<B24Avatar v-else-if="item.avatar" :size="((props.b24ui?.itemLeadingAvatarSize || b24ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="b24ui.itemLeadingAvatar({ class: props.b24ui?.itemLeadingAvatar })" />
|
|
426
|
+
<B24Chip
|
|
427
|
+
v-else-if="item.chip"
|
|
428
|
+
:size="((props.b24ui?.itemLeadingChipSize || b24ui.itemLeadingChipSize()) as ChipProps['size'])"
|
|
429
|
+
inset
|
|
430
|
+
standalone
|
|
431
|
+
v-bind="item.chip"
|
|
432
|
+
:class="b24ui.itemLeadingChip({ class: props.b24ui?.itemLeadingChip })"
|
|
433
|
+
/>
|
|
434
|
+
</slot>
|
|
435
|
+
|
|
436
|
+
<span :class="b24ui.itemLabel({ class: props.b24ui?.itemLabel })">
|
|
437
|
+
<slot name="item-label" :item="(item as T)" :index="index">
|
|
438
|
+
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
|
439
|
+
</slot>
|
|
440
|
+
</span>
|
|
441
|
+
|
|
442
|
+
<span :class="b24ui.itemTrailing({ class: props.b24ui?.itemTrailing })">
|
|
443
|
+
<slot name="item-trailing" :item="(item as T)" :index="index" />
|
|
444
|
+
|
|
445
|
+
<ComboboxItemIndicator as-child>
|
|
446
|
+
<Component
|
|
447
|
+
:is="selectedIcon || icons.check"
|
|
448
|
+
:class="b24ui.itemTrailingIcon({ class: props.b24ui?.itemTrailingIcon })"
|
|
449
|
+
/>
|
|
450
|
+
</ComboboxItemIndicator>
|
|
451
|
+
</span>
|
|
452
|
+
</slot>
|
|
453
|
+
</ComboboxItem>
|
|
454
|
+
</template>
|
|
455
|
+
</ComboboxGroup>
|
|
456
|
+
|
|
457
|
+
<ReuseCreateItemTemplate v-if="createItem && createItemPosition === 'bottom'" />
|
|
458
|
+
</ComboboxViewport>
|
|
459
|
+
|
|
460
|
+
<ComboboxArrow v-if="!!arrow" v-bind="arrowProps" :class="b24ui.arrow({ class: props.b24ui?.arrow })" />
|
|
461
|
+
</ComboboxContent>
|
|
462
|
+
</ComboboxPortal>
|
|
463
|
+
</ComboboxRoot>
|
|
464
|
+
</Primitive>
|
|
465
|
+
</template>
|
|
@@ -6,6 +6,7 @@ import _appConfig from '#build/app.config'
|
|
|
6
6
|
import theme from '#build/b24ui/toast'
|
|
7
7
|
import { tv } from '../utils/tv'
|
|
8
8
|
import type { AvatarProps, ButtonProps, IconComponent } from '../types'
|
|
9
|
+
import type { StringOrVNode } from '../types/utils'
|
|
9
10
|
|
|
10
11
|
const appConfigToast = _appConfig as AppConfig & { b24ui: { toast: Partial<typeof theme> } }
|
|
11
12
|
|
|
@@ -19,15 +20,16 @@ export interface ToastProps extends Pick<ToastRootProps, 'defaultOpen' | 'open'
|
|
|
19
20
|
* @defaultValue 'li'
|
|
20
21
|
*/
|
|
21
22
|
as?: any
|
|
22
|
-
title?:
|
|
23
|
-
description?:
|
|
23
|
+
title?: StringOrVNode
|
|
24
|
+
description?: StringOrVNode
|
|
24
25
|
icon?: IconComponent
|
|
25
26
|
avatar?: AvatarProps
|
|
26
27
|
color?: ToastVariants['color']
|
|
28
|
+
orientation?: ToastVariants['orientation']
|
|
27
29
|
/**
|
|
28
30
|
* Display a list of actions:
|
|
29
|
-
* - under the title and description
|
|
30
|
-
* - next to the close button
|
|
31
|
+
* - under the title and description when orientation is `vertical`
|
|
32
|
+
* - next to the close button when orientation is `horizontal`
|
|
31
33
|
* `{ size: 'xs' }`{lang="ts-type"}
|
|
32
34
|
*/
|
|
33
35
|
actions?: ButtonProps[]
|
|
@@ -67,7 +69,8 @@ import B24Avatar from './Avatar.vue'
|
|
|
67
69
|
import B24Button from './Button.vue'
|
|
68
70
|
|
|
69
71
|
const props = withDefaults(defineProps<ToastProps>(), {
|
|
70
|
-
close: true
|
|
72
|
+
close: true,
|
|
73
|
+
orientation: 'vertical'
|
|
71
74
|
})
|
|
72
75
|
const emits = defineEmits<ToastEmits>()
|
|
73
76
|
const slots = defineSlots<ToastSlots>()
|
|
@@ -76,10 +79,10 @@ const { t } = useLocale()
|
|
|
76
79
|
|
|
77
80
|
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultOpen', 'open', 'duration', 'type'), emits)
|
|
78
81
|
|
|
79
|
-
const multiline = computed(() => !!props.title && !!props.description)
|
|
80
|
-
|
|
81
82
|
const b24ui = computed(() => toast({
|
|
82
|
-
color: props.color
|
|
83
|
+
color: props.color,
|
|
84
|
+
orientation: props.orientation,
|
|
85
|
+
title: !!props.title || !!slots.title
|
|
83
86
|
}))
|
|
84
87
|
|
|
85
88
|
const el = ref()
|
|
@@ -105,7 +108,8 @@ defineExpose({
|
|
|
105
108
|
ref="el"
|
|
106
109
|
v-slot="{ remaining, duration }"
|
|
107
110
|
v-bind="rootProps"
|
|
108
|
-
:
|
|
111
|
+
:data-orientation="orientation"
|
|
112
|
+
:class="b24ui.root({ class: [props.class, props.b24ui?.root] })"
|
|
109
113
|
:style="{ '--height': height }"
|
|
110
114
|
>
|
|
111
115
|
<slot name="leading">
|
|
@@ -120,16 +124,24 @@ defineExpose({
|
|
|
120
124
|
<div :class="b24ui.wrapper({ class: props.b24ui?.wrapper })">
|
|
121
125
|
<ToastTitle v-if="title || !!slots.title" :class="b24ui.title({ class: props.b24ui?.title })">
|
|
122
126
|
<slot name="title">
|
|
123
|
-
|
|
127
|
+
<component :is="title()" v-if="typeof title === 'function'" />
|
|
128
|
+
<component :is="title" v-else-if="typeof title === 'object'" />
|
|
129
|
+
<template v-else>
|
|
130
|
+
{{ title }}
|
|
131
|
+
</template>
|
|
124
132
|
</slot>
|
|
125
133
|
</ToastTitle>
|
|
126
134
|
<ToastDescription v-if="description || !!slots.description" :class="b24ui.description({ class: props.b24ui?.description })">
|
|
127
135
|
<slot name="description">
|
|
128
|
-
|
|
136
|
+
<component :is="description()" v-if="typeof description === 'function'" />
|
|
137
|
+
<component :is="description" v-else-if="typeof description === 'object'" />
|
|
138
|
+
<template v-else>
|
|
139
|
+
{{ description }}
|
|
140
|
+
</template>
|
|
129
141
|
</slot>
|
|
130
142
|
</ToastDescription>
|
|
131
143
|
|
|
132
|
-
<div v-if="
|
|
144
|
+
<div v-if="orientation === 'vertical' && actions?.length" :class="b24ui.actions({ class: props.b24ui?.actions })">
|
|
133
145
|
<slot name="actions">
|
|
134
146
|
<ToastAction v-for="(action, index) in actions" :key="index" :alt-text="action.label || 'Action'" as-child @click.stop>
|
|
135
147
|
<B24Button size="xs" :color="color" v-bind="action" />
|
|
@@ -138,8 +150,8 @@ defineExpose({
|
|
|
138
150
|
</div>
|
|
139
151
|
</div>
|
|
140
152
|
|
|
141
|
-
<div v-if="(
|
|
142
|
-
<template v-if="
|
|
153
|
+
<div v-if="(orientation === 'horizontal' && actions?.length) || close !== null" :class="b24ui.actions({ class: props.b24ui?.actions, orientation: 'horizontal' })">
|
|
154
|
+
<template v-if="orientation === 'horizontal' && actions?.length">
|
|
143
155
|
<slot name="actions">
|
|
144
156
|
<ToastAction v-for="(action, index) in actions" :key="index" :alt-text="action.label || 'Action'" as-child @click.stop>
|
|
145
157
|
<B24Button size="xs" :color="color" v-bind="action" />
|
|
@@ -118,10 +118,10 @@ function getOffset(index: number) {
|
|
|
118
118
|
'--transform': 'translateY(var(--translate)) scale(var(--scale))'
|
|
119
119
|
}"
|
|
120
120
|
:class="[b24ui.base(), {
|
|
121
|
-
'cursor-pointer': !!toast.
|
|
121
|
+
'cursor-pointer': !!toast.onClick
|
|
122
122
|
}]"
|
|
123
123
|
@update:open="onUpdateOpen($event, toast.id)"
|
|
124
|
-
@click="toast.
|
|
124
|
+
@click="toast.onClick && toast.onClick(toast)"
|
|
125
125
|
/>
|
|
126
126
|
|
|
127
127
|
<ToastPortal :disabled="!portal">
|
|
@@ -19,10 +19,11 @@ export interface DescriptionListItem {
|
|
|
19
19
|
avatar?: AvatarProps
|
|
20
20
|
slot?: string
|
|
21
21
|
description?: string
|
|
22
|
+
orientation?: DescriptionListVariants['orientation']
|
|
22
23
|
/**
|
|
23
24
|
* Display a list of actions:
|
|
24
|
-
* - under the description
|
|
25
|
-
* - next to the description
|
|
25
|
+
* - under the description when orientation is `vertical`
|
|
26
|
+
* - next to the description when orientation is `horizontal`
|
|
26
27
|
* `{ size: 'xs' }`{lang="ts-type"}
|
|
27
28
|
*/
|
|
28
29
|
actions?: ButtonProps[]
|
|
@@ -81,13 +82,13 @@ const b24ui = computed(() => descriptionList({
|
|
|
81
82
|
function normalizeItem(item: any) {
|
|
82
83
|
const label = get(item, props.labelKey as string)
|
|
83
84
|
const description = get(item, props.descriptionKey as string)
|
|
84
|
-
const
|
|
85
|
+
const orientation = item?.orientation || 'vertical'
|
|
85
86
|
|
|
86
87
|
return {
|
|
87
88
|
...item,
|
|
88
89
|
label,
|
|
89
90
|
description,
|
|
90
|
-
|
|
91
|
+
orientation
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -166,12 +167,13 @@ const normalizedItems = computed(() => {
|
|
|
166
167
|
</span>
|
|
167
168
|
</dt>
|
|
168
169
|
<dd
|
|
170
|
+
:data-orientation="item.orientation"
|
|
169
171
|
:class="b24ui.descriptionWrapper({
|
|
170
172
|
class: [
|
|
171
173
|
props.b24ui?.descriptionWrapper,
|
|
172
174
|
item?.b24ui?.descriptionWrapper
|
|
173
175
|
],
|
|
174
|
-
|
|
176
|
+
orientation: item.orientation
|
|
175
177
|
})"
|
|
176
178
|
>
|
|
177
179
|
<span
|
|
@@ -181,7 +183,7 @@ const normalizedItems = computed(() => {
|
|
|
181
183
|
props.b24ui?.description,
|
|
182
184
|
item?.b24ui?.description
|
|
183
185
|
],
|
|
184
|
-
|
|
186
|
+
orientation: item.orientation
|
|
185
187
|
})"
|
|
186
188
|
>
|
|
187
189
|
<slot name="description" :item="item" :index="index">
|
|
@@ -195,7 +197,7 @@ const normalizedItems = computed(() => {
|
|
|
195
197
|
props.b24ui?.actions,
|
|
196
198
|
item?.b24ui?.actions
|
|
197
199
|
],
|
|
198
|
-
|
|
200
|
+
orientation: item.orientation
|
|
199
201
|
})"
|
|
200
202
|
>
|
|
201
203
|
<slot name="actions" :item="item" :index="index">
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type { ToastProps } from '../types';
|
|
2
|
-
|
|
1
|
+
import type { ToastProps, ToastEmits } from '../types';
|
|
2
|
+
import type { EmitsToProps } from '../types/utils';
|
|
3
|
+
export interface Toast extends Omit<ToastProps, 'defaultOpen'>, EmitsToProps<ToastEmits> {
|
|
3
4
|
id: string | number;
|
|
4
|
-
|
|
5
|
+
onClick?: (toast: Toast) => void;
|
|
5
6
|
}
|
|
6
7
|
export declare function useToast(): {
|
|
7
8
|
toasts: import("vue").Ref<Toast[], Toast[]>;
|
|
8
|
-
add: (toast: Partial<Toast>) =>
|
|
9
|
+
add: (toast: Partial<Toast>) => Toast;
|
|
9
10
|
update: (id: string | number, toast: Omit<Partial<Toast>, "id">) => void;
|
|
10
11
|
remove: (id: string | number) => void;
|
|
11
12
|
clear: () => void;
|
|
@@ -18,14 +18,14 @@ export function useToast() {
|
|
|
18
18
|
}
|
|
19
19
|
running.value = false;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
function add(toast) {
|
|
22
22
|
const body = {
|
|
23
23
|
id: generateId(),
|
|
24
24
|
open: true,
|
|
25
25
|
...toast
|
|
26
26
|
};
|
|
27
27
|
queue.push(body);
|
|
28
|
-
|
|
28
|
+
processQueue();
|
|
29
29
|
return body;
|
|
30
30
|
}
|
|
31
31
|
function update(id, toast) {
|