@nuxt/ui 3.0.0-alpha.6 → 3.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.d.mts +6 -6
- package/dist/module.d.ts +6 -6
- package/dist/module.json +1 -1
- package/dist/module.mjs +391 -257
- package/dist/runtime/components/Accordion.vue +13 -5
- package/dist/runtime/components/Breadcrumb.vue +14 -6
- package/dist/runtime/components/Button.vue +4 -2
- package/dist/runtime/components/Carousel.vue +6 -6
- package/dist/runtime/components/Checkbox.vue +3 -6
- package/dist/runtime/components/CommandPalette.vue +25 -16
- package/dist/runtime/components/ContextMenu.vue +31 -7
- package/dist/runtime/components/ContextMenuContent.vue +36 -8
- package/dist/runtime/components/DropdownMenu.vue +31 -7
- package/dist/runtime/components/DropdownMenuContent.vue +38 -8
- package/dist/runtime/components/Form.vue +3 -1
- package/dist/runtime/components/Input.vue +6 -4
- package/dist/runtime/components/InputMenu.vue +40 -24
- package/dist/runtime/components/LinkBase.vue +4 -2
- package/dist/runtime/components/NavigationMenu.vue +78 -41
- package/dist/runtime/components/RadioGroup.vue +22 -3
- package/dist/runtime/components/Select.vue +22 -10
- package/dist/runtime/components/SelectMenu.vue +43 -31
- package/dist/runtime/components/Table.vue +232 -0
- package/dist/runtime/components/Tabs.vue +15 -7
- package/dist/runtime/components/Toaster.vue +1 -1
- package/dist/runtime/composables/defineShortcuts.js +1 -1
- package/dist/runtime/composables/useComponentIcons.d.ts +3 -0
- package/dist/runtime/composables/useModal.d.ts +1 -1
- package/dist/runtime/composables/useSlideover.d.ts +1 -1
- package/dist/runtime/index.css +1 -1
- package/dist/runtime/keyframes.css +1 -0
- package/dist/runtime/types/form.d.ts +2 -1
- package/dist/runtime/types/index.d.ts +1 -0
- package/dist/runtime/types/index.js +1 -0
- package/dist/runtime/utils/form.d.ts +3 -0
- package/dist/runtime/utils/form.js +14 -0
- package/dist/runtime/utils/index.d.ts +1 -0
- package/dist/runtime/utils/index.js +3 -0
- package/package.json +15 -12
- package/dist/runtime/types/component.d.ts +0 -17
- package/dist/runtime/types/component.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
-
import type { RadioGroupRootProps, RadioGroupRootEmits
|
|
3
|
+
import type { RadioGroupRootProps, RadioGroupRootEmits } from 'radix-vue'
|
|
4
4
|
import type { AppConfig } from '@nuxt/schema'
|
|
5
5
|
import _appConfig from '#build/app.config'
|
|
6
6
|
import theme from '#build/ui/radio-group'
|
|
@@ -12,9 +12,11 @@ const radioGroup = tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup || {}) }
|
|
|
12
12
|
|
|
13
13
|
type RadioGroupVariants = VariantProps<typeof radioGroup>
|
|
14
14
|
|
|
15
|
-
export interface RadioGroupItem
|
|
15
|
+
export interface RadioGroupItem {
|
|
16
16
|
label?: string
|
|
17
17
|
description?: string
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
value?: string
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
|
|
@@ -29,6 +31,16 @@ export interface RadioGroupProps<T> extends Pick<RadioGroupRootProps, 'defaultVa
|
|
|
29
31
|
* @defaultValue 'value'
|
|
30
32
|
*/
|
|
31
33
|
valueKey?: string
|
|
34
|
+
/**
|
|
35
|
+
* When `items` is an array of objects, select the field to use as the label.
|
|
36
|
+
* @defaultValue 'label'
|
|
37
|
+
*/
|
|
38
|
+
labelKey?: string
|
|
39
|
+
/**
|
|
40
|
+
* When `items` is an array of objects, select the field to use as the description.
|
|
41
|
+
* @defaultValue 'description'
|
|
42
|
+
*/
|
|
43
|
+
descriptionKey?: string
|
|
32
44
|
items?: T[]
|
|
33
45
|
size?: RadioGroupVariants['size']
|
|
34
46
|
color?: RadioGroupVariants['color']
|
|
@@ -59,9 +71,12 @@ import { computed, useId } from 'vue'
|
|
|
59
71
|
import { RadioGroupRoot, RadioGroupItem, RadioGroupIndicator, Label, useForwardPropsEmits } from 'radix-vue'
|
|
60
72
|
import { reactivePick } from '@vueuse/core'
|
|
61
73
|
import { useFormField } from '../composables/useFormField'
|
|
74
|
+
import { get } from '../utils'
|
|
62
75
|
|
|
63
76
|
const props = withDefaults(defineProps<RadioGroupProps<T>>(), {
|
|
64
77
|
valueKey: 'value',
|
|
78
|
+
labelKey: 'label',
|
|
79
|
+
descriptionKey: 'description',
|
|
65
80
|
orientation: 'vertical'
|
|
66
81
|
})
|
|
67
82
|
const emits = defineEmits<RadioGroupEmits>()
|
|
@@ -89,11 +104,15 @@ function normalizeItem(item: any) {
|
|
|
89
104
|
}
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
const value = item
|
|
107
|
+
const value = get(item, props.valueKey as string)
|
|
108
|
+
const label = get(item, props.labelKey as string)
|
|
109
|
+
const description = get(item, props.descriptionKey as string)
|
|
93
110
|
|
|
94
111
|
return {
|
|
95
112
|
...item,
|
|
96
113
|
value,
|
|
114
|
+
label,
|
|
115
|
+
description,
|
|
97
116
|
id: `${id}:${value}`
|
|
98
117
|
}
|
|
99
118
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
-
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectArrowProps
|
|
3
|
+
import type { SelectRootProps, SelectRootEmits, SelectContentProps, SelectArrowProps } from 'radix-vue'
|
|
4
4
|
import type { AppConfig } from '@nuxt/schema'
|
|
5
5
|
import _appConfig from '#build/app.config'
|
|
6
6
|
import theme from '#build/ui/select'
|
|
@@ -12,7 +12,7 @@ const appConfig = _appConfig as AppConfig & { ui: { select: Partial<typeof theme
|
|
|
12
12
|
|
|
13
13
|
const select = tv({ extend: tv(theme), ...(appConfig.ui?.select || {}) })
|
|
14
14
|
|
|
15
|
-
export interface SelectItem
|
|
15
|
+
export interface SelectItem {
|
|
16
16
|
label?: string
|
|
17
17
|
icon?: string
|
|
18
18
|
avatar?: AvatarProps
|
|
@@ -22,6 +22,8 @@ export interface SelectItem extends Pick<SelectItemProps, 'disabled' | 'value'>
|
|
|
22
22
|
* @defaultValue 'item'
|
|
23
23
|
*/
|
|
24
24
|
type?: 'label' | 'separator' | 'item'
|
|
25
|
+
value?: string
|
|
26
|
+
disabled?: boolean
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
type SelectVariants = VariantProps<typeof select>
|
|
@@ -63,6 +65,11 @@ export interface SelectProps<T> extends Omit<SelectRootProps, 'dir'>, UseCompone
|
|
|
63
65
|
* @defaultValue 'value'
|
|
64
66
|
*/
|
|
65
67
|
valueKey?: string
|
|
68
|
+
/**
|
|
69
|
+
* When `items` is an array of objects, select the field to use as the label.
|
|
70
|
+
* @defaultValue 'label'
|
|
71
|
+
*/
|
|
72
|
+
labelKey?: string
|
|
66
73
|
items?: T[] | T[][]
|
|
67
74
|
/** Highlight the ring color like a focus state. */
|
|
68
75
|
highlight?: boolean
|
|
@@ -97,12 +104,14 @@ import { useAppConfig } from '#imports'
|
|
|
97
104
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
98
105
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
99
106
|
import { useFormField } from '../composables/useFormField'
|
|
107
|
+
import { get } from '../utils'
|
|
100
108
|
import UIcon from './Icon.vue'
|
|
101
109
|
import UAvatar from './Avatar.vue'
|
|
102
110
|
import UChip from './Chip.vue'
|
|
103
111
|
|
|
104
112
|
const props = withDefaults(defineProps<SelectProps<T>>(), {
|
|
105
113
|
valueKey: 'value',
|
|
114
|
+
labelKey: 'label',
|
|
106
115
|
portal: true
|
|
107
116
|
})
|
|
108
117
|
const emits = defineEmits<SelectEmits>()
|
|
@@ -124,7 +133,7 @@ const ui = computed(() => select({
|
|
|
124
133
|
size: selectSize?.value,
|
|
125
134
|
loading: props.loading,
|
|
126
135
|
highlight: highlight.value,
|
|
127
|
-
leading: isLeading.value || !!slots.leading,
|
|
136
|
+
leading: isLeading.value || !!props.avatar || !!slots.leading,
|
|
128
137
|
trailing: isTrailing.value || !!slots.trailing,
|
|
129
138
|
buttonGroup: orientation.value
|
|
130
139
|
}))
|
|
@@ -162,9 +171,10 @@ function onUpdateOpen(value: boolean) {
|
|
|
162
171
|
@update:open="onUpdateOpen"
|
|
163
172
|
>
|
|
164
173
|
<SelectTrigger :class="ui.base({ class: [props.class, props.ui?.base] })">
|
|
165
|
-
<span v-if="isLeading || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
174
|
+
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
166
175
|
<slot name="leading" :model-value="modelValue" :open="open" :ui="ui">
|
|
167
|
-
<UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
176
|
+
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
177
|
+
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
|
168
178
|
</slot>
|
|
169
179
|
</span>
|
|
170
180
|
|
|
@@ -183,19 +193,21 @@ function onUpdateOpen(value: boolean) {
|
|
|
183
193
|
<SelectGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
|
184
194
|
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
|
185
195
|
<SelectLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
|
186
|
-
{{ item.
|
|
196
|
+
{{ get(item, props.labelKey as string) }}
|
|
187
197
|
</SelectLabel>
|
|
198
|
+
|
|
188
199
|
<SelectSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
|
200
|
+
|
|
189
201
|
<SelectItem
|
|
190
202
|
v-else
|
|
191
203
|
:class="ui.item({ class: props.ui?.item })"
|
|
192
204
|
:disabled="item.disabled"
|
|
193
|
-
:value="typeof item === 'object' ? (item
|
|
205
|
+
:value="typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
|
194
206
|
>
|
|
195
207
|
<slot name="item" :item="(item as T)" :index="index">
|
|
196
208
|
<slot name="item-leading" :item="(item as T)" :index="index">
|
|
197
|
-
<
|
|
198
|
-
<
|
|
209
|
+
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
|
210
|
+
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
|
199
211
|
<UChip
|
|
200
212
|
v-else-if="item.chip"
|
|
201
213
|
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
|
@@ -208,7 +220,7 @@ function onUpdateOpen(value: boolean) {
|
|
|
208
220
|
|
|
209
221
|
<SelectItemText :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
|
210
222
|
<slot name="item-label" :item="(item as T)" :index="index">
|
|
211
|
-
{{ typeof item === 'object' ? item.
|
|
223
|
+
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
|
212
224
|
</slot>
|
|
213
225
|
</SelectItemText>
|
|
214
226
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
-
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps,
|
|
3
|
+
import type { ComboboxRootProps, ComboboxRootEmits, ComboboxContentProps, ComboboxArrowProps } from 'radix-vue'
|
|
4
4
|
import type { AppConfig } from '@nuxt/schema'
|
|
5
5
|
import _appConfig from '#build/app.config'
|
|
6
6
|
import theme from '#build/ui/select-menu'
|
|
@@ -12,7 +12,7 @@ const appConfig = _appConfig as AppConfig & { ui: { selectMenu: Partial<typeof t
|
|
|
12
12
|
|
|
13
13
|
const selectMenu = tv({ extend: tv(theme), ...(appConfig.ui?.selectMenu || {}) })
|
|
14
14
|
|
|
15
|
-
export interface SelectMenuItem
|
|
15
|
+
export interface SelectMenuItem {
|
|
16
16
|
label?: string
|
|
17
17
|
icon?: string
|
|
18
18
|
avatar?: AvatarProps
|
|
@@ -22,7 +22,8 @@ export interface SelectMenuItem extends Pick<ComboboxItemProps, 'disabled'> {
|
|
|
22
22
|
* @defaultValue 'item'
|
|
23
23
|
*/
|
|
24
24
|
type?: 'label' | 'separator' | 'item'
|
|
25
|
-
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
onSelect?(e?: Event): void
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
type SelectMenuVariants = VariantProps<typeof selectMenu>
|
|
@@ -77,6 +78,11 @@ export interface SelectMenuProps<T> extends Pick<ComboboxRootProps<T>, 'modelVal
|
|
|
77
78
|
* @defaultValue undefined
|
|
78
79
|
*/
|
|
79
80
|
valueKey?: keyof T
|
|
81
|
+
/**
|
|
82
|
+
* When `items` is an array of objects, select the field to use as the label.
|
|
83
|
+
* @defaultValue 'label'
|
|
84
|
+
*/
|
|
85
|
+
labelKey?: keyof T
|
|
80
86
|
items?: T[] | T[][]
|
|
81
87
|
/** Highlight the ring color like a focus state. */
|
|
82
88
|
highlight?: boolean
|
|
@@ -108,22 +114,24 @@ export interface SelectMenuSlots<T> {
|
|
|
108
114
|
import { computed, toRef } from 'vue'
|
|
109
115
|
import { ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxPortal, ComboboxContent, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'radix-vue'
|
|
110
116
|
import { defu } from 'defu'
|
|
117
|
+
import isEqual from 'fast-deep-equal'
|
|
111
118
|
import { reactivePick } from '@vueuse/core'
|
|
112
119
|
import { useAppConfig } from '#imports'
|
|
113
120
|
import { useButtonGroup } from '../composables/useButtonGroup'
|
|
114
121
|
import { useComponentIcons } from '../composables/useComponentIcons'
|
|
115
122
|
import { useFormField } from '../composables/useFormField'
|
|
123
|
+
import { get, escapeRegExp } from '../utils'
|
|
116
124
|
import UIcon from './Icon.vue'
|
|
117
125
|
import UAvatar from './Avatar.vue'
|
|
118
126
|
import UChip from './Chip.vue'
|
|
119
|
-
import { get } from '../utils'
|
|
120
127
|
|
|
121
128
|
const props = withDefaults(defineProps<SelectMenuProps<T>>(), {
|
|
122
129
|
search: true,
|
|
123
130
|
portal: true,
|
|
124
131
|
autofocusDelay: 0,
|
|
125
132
|
searchInput: () => ({ placeholder: 'Search...' }),
|
|
126
|
-
filter: () => ['label']
|
|
133
|
+
filter: () => ['label'],
|
|
134
|
+
labelKey: 'label' as keyof T
|
|
127
135
|
})
|
|
128
136
|
const emits = defineEmits<SelectMenuEmits<T>>()
|
|
129
137
|
const slots = defineSlots<SelectMenuSlots<T>>()
|
|
@@ -145,21 +153,19 @@ const ui = computed(() => selectMenu({
|
|
|
145
153
|
size: selectSize?.value,
|
|
146
154
|
loading: props.loading,
|
|
147
155
|
highlight: highlight.value,
|
|
148
|
-
leading: isLeading.value || !!slots.leading,
|
|
156
|
+
leading: isLeading.value || !!props.avatar || !!slots.leading,
|
|
149
157
|
trailing: isTrailing.value || !!slots.trailing,
|
|
150
158
|
buttonGroup: orientation.value
|
|
151
159
|
}))
|
|
152
160
|
|
|
153
|
-
function displayValue(
|
|
154
|
-
if (multiple && Array.isArray(
|
|
155
|
-
return
|
|
161
|
+
function displayValue(value: T): string {
|
|
162
|
+
if (props.multiple && Array.isArray(value)) {
|
|
163
|
+
return value.map(v => displayValue(v)).join(', ')
|
|
156
164
|
}
|
|
157
165
|
|
|
158
|
-
|
|
159
|
-
return val.label
|
|
160
|
-
}
|
|
166
|
+
const item = items.value.find(item => props.valueKey ? isEqual(get(item as Record<string, any>, props.valueKey as string), value) : isEqual(item, value))
|
|
161
167
|
|
|
162
|
-
return
|
|
168
|
+
return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item)
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: string): ArrayOrWrapped<AcceptableValue> {
|
|
@@ -167,22 +173,25 @@ function filterFunction(items: ArrayOrWrapped<AcceptableValue>, searchTerm: stri
|
|
|
167
173
|
return items
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
const fields = Array.isArray(props.filter) ? props.filter : [
|
|
176
|
+
const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey]
|
|
177
|
+
const escapedSearchTerm = escapeRegExp(searchTerm)
|
|
171
178
|
|
|
172
179
|
return items.filter((item) => {
|
|
173
180
|
if (typeof item !== 'object') {
|
|
174
|
-
return String(item).search(new RegExp(
|
|
181
|
+
return String(item).search(new RegExp(escapedSearchTerm, 'i')) !== -1
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
return fields.some((field) => {
|
|
178
|
-
const child = get(item, field)
|
|
185
|
+
const child = get(item, field as string)
|
|
179
186
|
|
|
180
|
-
return child !== null && child !== undefined && String(child).search(new RegExp(
|
|
187
|
+
return child !== null && child !== undefined && String(child).search(new RegExp(escapedSearchTerm, 'i')) !== -1
|
|
181
188
|
})
|
|
182
189
|
}) as ArrayOrWrapped<T>
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as SelectMenuItem[][] : [])
|
|
193
|
+
// eslint-disable-next-line vue/no-dupe-keys
|
|
194
|
+
const items = computed(() => groups.value.flatMap(group => group) as T[])
|
|
186
195
|
|
|
187
196
|
function onUpdate(value: any) {
|
|
188
197
|
// @ts-expect-error - 'target' does not exist in type 'EventInit'
|
|
@@ -220,19 +229,22 @@ function onUpdateOpen(value: boolean) {
|
|
|
220
229
|
>
|
|
221
230
|
<ComboboxAnchor as-child>
|
|
222
231
|
<ComboboxTrigger :class="ui.base({ class: [props.class, props.ui?.base] })" tabindex="0">
|
|
223
|
-
<span v-if="isLeading || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
232
|
+
<span v-if="isLeading || !!avatar || !!slots.leading" :class="ui.leading({ class: props.ui?.leading })">
|
|
224
233
|
<slot name="leading" :model-value="(modelValue as T)" :open="open" :ui="ui">
|
|
225
|
-
<UIcon v-if="leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
234
|
+
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
|
|
235
|
+
<UAvatar v-else-if="!!avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
|
226
236
|
</slot>
|
|
227
237
|
</span>
|
|
228
238
|
|
|
229
239
|
<slot :model-value="(modelValue as T)" :open="open">
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
240
|
+
<template v-for="displayedModelValue in [displayValue(modelValue)]" :key="displayedModelValue">
|
|
241
|
+
<span v-if="displayedModelValue" :class="ui.value({ class: props.ui?.value })">
|
|
242
|
+
{{ displayedModelValue }}
|
|
243
|
+
</span>
|
|
244
|
+
<span v-else :class="ui.placeholder({ class: props.ui?.placeholder })">
|
|
245
|
+
{{ placeholder ?? ' ' }}
|
|
246
|
+
</span>
|
|
247
|
+
</template>
|
|
236
248
|
</slot>
|
|
237
249
|
|
|
238
250
|
<span v-if="isTrailing || !!slots.trailing" :class="ui.trailing({ class: props.ui?.trailing })">
|
|
@@ -263,7 +275,7 @@ function onUpdateOpen(value: boolean) {
|
|
|
263
275
|
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
|
|
264
276
|
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
|
|
265
277
|
<ComboboxLabel v-if="item?.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
|
266
|
-
{{ item.
|
|
278
|
+
{{ get(item, props.labelKey as string) }}
|
|
267
279
|
</ComboboxLabel>
|
|
268
280
|
|
|
269
281
|
<ComboboxSeparator v-else-if="item?.type === 'separator'" :class="ui.separator({ class: props.ui?.separator })" />
|
|
@@ -272,13 +284,13 @@ function onUpdateOpen(value: boolean) {
|
|
|
272
284
|
v-else
|
|
273
285
|
:class="ui.item({ class: props.ui?.item })"
|
|
274
286
|
:disabled="item.disabled"
|
|
275
|
-
:value="valueKey && typeof item === 'object' ? (item
|
|
276
|
-
@select="item.
|
|
287
|
+
:value="valueKey && typeof item === 'object' ? get(item, props.valueKey as string) : item"
|
|
288
|
+
@select="item.onSelect"
|
|
277
289
|
>
|
|
278
290
|
<slot name="item" :item="(item as T)" :index="index">
|
|
279
291
|
<slot name="item-leading" :item="(item as T)" :index="index">
|
|
280
|
-
<
|
|
281
|
-
<
|
|
292
|
+
<UIcon v-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
|
|
293
|
+
<UAvatar v-else-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
|
|
282
294
|
<UChip
|
|
283
295
|
v-else-if="item.chip"
|
|
284
296
|
:size="((props.ui?.itemLeadingChipSize || ui.itemLeadingChipSize()) as ChipProps['size'])"
|
|
@@ -291,7 +303,7 @@ function onUpdateOpen(value: boolean) {
|
|
|
291
303
|
|
|
292
304
|
<span :class="ui.itemLabel({ class: props.ui?.itemLabel })">
|
|
293
305
|
<slot name="item-label" :item="(item as T)" :index="index">
|
|
294
|
-
{{
|
|
306
|
+
{{ typeof item === 'object' ? get(item, props.labelKey as string) : item }}
|
|
295
307
|
</slot>
|
|
296
308
|
</span>
|
|
297
309
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<!-- eslint-disable vue/block-tag-newline -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import type { Ref } from 'vue'
|
|
4
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
5
|
+
import type { AppConfig } from '@nuxt/schema'
|
|
6
|
+
import type {
|
|
7
|
+
Row,
|
|
8
|
+
ColumnDef,
|
|
9
|
+
ColumnFiltersState,
|
|
10
|
+
ColumnPinningState,
|
|
11
|
+
RowSelectionState,
|
|
12
|
+
SortingState,
|
|
13
|
+
ExpandedState,
|
|
14
|
+
VisibilityState,
|
|
15
|
+
GlobalFilterOptions,
|
|
16
|
+
ColumnFiltersOptions,
|
|
17
|
+
ColumnPinningOptions,
|
|
18
|
+
VisibilityOptions,
|
|
19
|
+
ExpandedOptions,
|
|
20
|
+
SortingOptions,
|
|
21
|
+
RowSelectionOptions,
|
|
22
|
+
Updater
|
|
23
|
+
} from '@tanstack/vue-table'
|
|
24
|
+
import _appConfig from '#build/app.config'
|
|
25
|
+
import theme from '#build/ui/table'
|
|
26
|
+
|
|
27
|
+
const appConfig = _appConfig as AppConfig & { ui: { table: Partial<typeof theme> } }
|
|
28
|
+
|
|
29
|
+
const table = tv({ extend: tv(theme), ...(appConfig.ui?.table || {}) })
|
|
30
|
+
|
|
31
|
+
type TableVariants = VariantProps<typeof table>
|
|
32
|
+
|
|
33
|
+
export type TableColumn<T> = ColumnDef<T>
|
|
34
|
+
|
|
35
|
+
export interface TableData {
|
|
36
|
+
[key: string]: any
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TableProps<T> {
|
|
40
|
+
data?: T[]
|
|
41
|
+
columns?: TableColumn<T>[]
|
|
42
|
+
/**
|
|
43
|
+
* Whether the table should have a sticky header.
|
|
44
|
+
* @defaultValue false
|
|
45
|
+
*/
|
|
46
|
+
sticky?: boolean
|
|
47
|
+
/** Whether the table should be in loading state. */
|
|
48
|
+
loading?: boolean
|
|
49
|
+
loadingColor?: TableVariants['loadingColor']
|
|
50
|
+
loadingAnimation?: TableVariants['loadingAnimation']
|
|
51
|
+
/**
|
|
52
|
+
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/global-filtering#table-options)
|
|
53
|
+
* @link [Guide](https://tanstack.com/table/v8/docs/guide/global-filtering)
|
|
54
|
+
*/
|
|
55
|
+
globalFilterOptions?: Omit<GlobalFilterOptions<T>, 'onGlobalFilterChange'>
|
|
56
|
+
/**
|
|
57
|
+
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-filtering#table-options)
|
|
58
|
+
* @link [Guide](https://tanstack.com/table/v8/docs/guide/column-filtering)
|
|
59
|
+
*/
|
|
60
|
+
columnFiltersOptions?: Omit<ColumnFiltersOptions<T>, 'getFilteredRowModel' | 'onColumnFiltersChange'>
|
|
61
|
+
/**
|
|
62
|
+
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-pinning#table-options)
|
|
63
|
+
* @link [Guide](https://tanstack.com/table/v8/docs/guide/column-pinning)
|
|
64
|
+
*/
|
|
65
|
+
columnPinningOptions?: Omit<ColumnPinningOptions, 'onColumnPinningChange'>
|
|
66
|
+
/**
|
|
67
|
+
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/column-visibility#table-options)
|
|
68
|
+
* @link [Guide](https://tanstack.com/table/v8/docs/guide/column-visibility)
|
|
69
|
+
*/
|
|
70
|
+
visibilityOptions?: Omit<VisibilityOptions, 'onColumnVisibilityChange'>
|
|
71
|
+
/**
|
|
72
|
+
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/sorting#table-options)
|
|
73
|
+
* @link [Guide](https://tanstack.com/table/v8/docs/guide/sorting)
|
|
74
|
+
*/
|
|
75
|
+
sortingOptions?: Omit<SortingOptions<T>, 'getSortedRowModel' | 'onSortingChange'>
|
|
76
|
+
/**
|
|
77
|
+
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/expanding#table-options)
|
|
78
|
+
* @link [Guide](https://tanstack.com/table/v8/docs/guide/expanding)
|
|
79
|
+
*/
|
|
80
|
+
expandedOptions?: Omit<ExpandedOptions<T>, 'getExpandedRowModel' | 'onExpandedChange'>
|
|
81
|
+
/**
|
|
82
|
+
* @link [API Docs](https://tanstack.com/table/v8/docs/api/features/row-selection#table-options)
|
|
83
|
+
* @link [Guide](https://tanstack.com/table/v8/docs/guide/row-selection)
|
|
84
|
+
*/
|
|
85
|
+
rowSelectionOptions?: Omit<RowSelectionOptions<T>, 'onRowSelectionChange'>
|
|
86
|
+
class?: any
|
|
87
|
+
ui?: Partial<typeof table.slots>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface TableSlots<T> {
|
|
91
|
+
expanded(props: { row: Row<T> }): any
|
|
92
|
+
empty(props?: {}): any
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<script setup lang="ts" generic="T extends TableData">
|
|
98
|
+
import { computed } from 'vue'
|
|
99
|
+
import {
|
|
100
|
+
FlexRender,
|
|
101
|
+
getCoreRowModel,
|
|
102
|
+
getFilteredRowModel,
|
|
103
|
+
getSortedRowModel,
|
|
104
|
+
getExpandedRowModel,
|
|
105
|
+
useVueTable
|
|
106
|
+
} from '@tanstack/vue-table'
|
|
107
|
+
import { upperFirst } from 'scule'
|
|
108
|
+
|
|
109
|
+
const props = defineProps<TableProps<T>>()
|
|
110
|
+
defineSlots<TableSlots<T>>()
|
|
111
|
+
|
|
112
|
+
const data = computed(() => props.data ?? [])
|
|
113
|
+
const columns = computed<TableColumn<T>[]>(() => props.columns ?? Object.keys(data.value[0] ?? {}).map((accessorKey: string) => ({ accessorKey, header: upperFirst(accessorKey) })))
|
|
114
|
+
|
|
115
|
+
const ui = computed(() => table({
|
|
116
|
+
sticky: props.sticky,
|
|
117
|
+
loading: props.loading,
|
|
118
|
+
loadingColor: props.loadingColor,
|
|
119
|
+
loadingAnimation: props.loadingAnimation
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
const globalFilterState = defineModel<string>('globalFilter', { default: undefined })
|
|
123
|
+
const columnFiltersState = defineModel<ColumnFiltersState>('columnFilters', { default: [] })
|
|
124
|
+
const columnVisibilityState = defineModel<VisibilityState>('columnVisibility', { default: {} })
|
|
125
|
+
const columnPinningState = defineModel<ColumnPinningState>('columnPinning', { default: {} })
|
|
126
|
+
const rowSelectionState = defineModel<RowSelectionState>('rowSelection', { default: {} })
|
|
127
|
+
const sortingState = defineModel<SortingState>('sorting', { default: [] })
|
|
128
|
+
const expandedState = defineModel<ExpandedState>('expanded', { default: {} })
|
|
129
|
+
|
|
130
|
+
const tableApi = useVueTable({
|
|
131
|
+
data,
|
|
132
|
+
columns: columns.value,
|
|
133
|
+
getCoreRowModel: getCoreRowModel(),
|
|
134
|
+
...(props.globalFilterOptions || {}),
|
|
135
|
+
onGlobalFilterChange: updaterOrValue => valueUpdater(updaterOrValue, globalFilterState),
|
|
136
|
+
...(props.columnFiltersOptions || {}),
|
|
137
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
138
|
+
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFiltersState),
|
|
139
|
+
...(props.visibilityOptions || {}),
|
|
140
|
+
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibilityState),
|
|
141
|
+
...(props.columnPinningOptions || {}),
|
|
142
|
+
onColumnPinningChange: updaterOrValue => valueUpdater(updaterOrValue, columnPinningState),
|
|
143
|
+
...(props.rowSelectionOptions || {}),
|
|
144
|
+
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelectionState),
|
|
145
|
+
...(props.sortingOptions || {}),
|
|
146
|
+
getSortedRowModel: getSortedRowModel(),
|
|
147
|
+
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sortingState),
|
|
148
|
+
...(props.expandedOptions || {}),
|
|
149
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
150
|
+
onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expandedState),
|
|
151
|
+
state: {
|
|
152
|
+
get globalFilter() {
|
|
153
|
+
return globalFilterState.value
|
|
154
|
+
},
|
|
155
|
+
get columnFilters() {
|
|
156
|
+
return columnFiltersState.value
|
|
157
|
+
},
|
|
158
|
+
get columnVisibility() {
|
|
159
|
+
return columnVisibilityState.value
|
|
160
|
+
},
|
|
161
|
+
get columnPinning() {
|
|
162
|
+
return columnPinningState.value
|
|
163
|
+
},
|
|
164
|
+
get expanded() {
|
|
165
|
+
return expandedState.value
|
|
166
|
+
},
|
|
167
|
+
get rowSelection() {
|
|
168
|
+
return rowSelectionState.value
|
|
169
|
+
},
|
|
170
|
+
get sorting() {
|
|
171
|
+
return sortingState.value
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
|
|
177
|
+
ref.value = typeof updaterOrValue === 'function' ? updaterOrValue(ref.value) : updaterOrValue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
defineExpose({
|
|
181
|
+
tableApi
|
|
182
|
+
})
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<template>
|
|
186
|
+
<div :class="ui.root({ class: [props.class, props.ui?.root] })">
|
|
187
|
+
<table :class="ui.base({ class: [props.ui?.base] })">
|
|
188
|
+
<thead :class="ui.thead({ class: [props.ui?.thead] })">
|
|
189
|
+
<tr v-for="headerGroup in tableApi.getHeaderGroups()" :key="headerGroup.id" :class="ui.tr({ class: [props.ui?.tr] })">
|
|
190
|
+
<th
|
|
191
|
+
v-for="header in headerGroup.headers"
|
|
192
|
+
:key="header.id"
|
|
193
|
+
:data-pinned="header.column.getIsPinned()"
|
|
194
|
+
:class="ui.th({ class: [props.ui?.th], pinned: !!header.column.getIsPinned() })"
|
|
195
|
+
>
|
|
196
|
+
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
|
|
197
|
+
</th>
|
|
198
|
+
</tr>
|
|
199
|
+
</thead>
|
|
200
|
+
|
|
201
|
+
<tbody :class="ui.tbody({ class: [props.ui?.tbody] })">
|
|
202
|
+
<template v-if="tableApi.getRowModel().rows?.length">
|
|
203
|
+
<template v-for="row in tableApi.getRowModel().rows" :key="row.id">
|
|
204
|
+
<tr :data-selected="row.getIsSelected()" :data-expanded="row.getIsExpanded()" :class="ui.tr({ class: [props.ui?.tr] })">
|
|
205
|
+
<td
|
|
206
|
+
v-for="cell in row.getVisibleCells()"
|
|
207
|
+
:key="cell.id"
|
|
208
|
+
:data-pinned="cell.column.getIsPinned()"
|
|
209
|
+
:class="ui.td({ class: [props.ui?.td], pinned: !!cell.column.getIsPinned() })"
|
|
210
|
+
>
|
|
211
|
+
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
|
212
|
+
</td>
|
|
213
|
+
</tr>
|
|
214
|
+
<tr v-if="row.getIsExpanded()" :class="ui.tr({ class: [props.ui?.tr] })">
|
|
215
|
+
<td :colspan="row.getAllCells().length" :class="ui.td({ class: [props.ui?.td] })">
|
|
216
|
+
<slot name="expanded" :row="row" />
|
|
217
|
+
</td>
|
|
218
|
+
</tr>
|
|
219
|
+
</template>
|
|
220
|
+
</template>
|
|
221
|
+
|
|
222
|
+
<tr v-else :class="ui.tr({ class: [props.ui?.tr] })">
|
|
223
|
+
<td :colspan="columns?.length" :class="ui.empty({ class: props.ui?.empty })">
|
|
224
|
+
<slot name="empty">
|
|
225
|
+
No results
|
|
226
|
+
</slot>
|
|
227
|
+
</td>
|
|
228
|
+
</tr>
|
|
229
|
+
</tbody>
|
|
230
|
+
</table>
|
|
231
|
+
</div>
|
|
232
|
+
</template>
|