@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.
Files changed (41) hide show
  1. package/dist/module.d.mts +6 -6
  2. package/dist/module.d.ts +6 -6
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +391 -257
  5. package/dist/runtime/components/Accordion.vue +13 -5
  6. package/dist/runtime/components/Breadcrumb.vue +14 -6
  7. package/dist/runtime/components/Button.vue +4 -2
  8. package/dist/runtime/components/Carousel.vue +6 -6
  9. package/dist/runtime/components/Checkbox.vue +3 -6
  10. package/dist/runtime/components/CommandPalette.vue +25 -16
  11. package/dist/runtime/components/ContextMenu.vue +31 -7
  12. package/dist/runtime/components/ContextMenuContent.vue +36 -8
  13. package/dist/runtime/components/DropdownMenu.vue +31 -7
  14. package/dist/runtime/components/DropdownMenuContent.vue +38 -8
  15. package/dist/runtime/components/Form.vue +3 -1
  16. package/dist/runtime/components/Input.vue +6 -4
  17. package/dist/runtime/components/InputMenu.vue +40 -24
  18. package/dist/runtime/components/LinkBase.vue +4 -2
  19. package/dist/runtime/components/NavigationMenu.vue +78 -41
  20. package/dist/runtime/components/RadioGroup.vue +22 -3
  21. package/dist/runtime/components/Select.vue +22 -10
  22. package/dist/runtime/components/SelectMenu.vue +43 -31
  23. package/dist/runtime/components/Table.vue +232 -0
  24. package/dist/runtime/components/Tabs.vue +15 -7
  25. package/dist/runtime/components/Toaster.vue +1 -1
  26. package/dist/runtime/composables/defineShortcuts.js +1 -1
  27. package/dist/runtime/composables/useComponentIcons.d.ts +3 -0
  28. package/dist/runtime/composables/useModal.d.ts +1 -1
  29. package/dist/runtime/composables/useSlideover.d.ts +1 -1
  30. package/dist/runtime/index.css +1 -1
  31. package/dist/runtime/keyframes.css +1 -0
  32. package/dist/runtime/types/form.d.ts +2 -1
  33. package/dist/runtime/types/index.d.ts +1 -0
  34. package/dist/runtime/types/index.js +1 -0
  35. package/dist/runtime/utils/form.d.ts +3 -0
  36. package/dist/runtime/utils/form.js +14 -0
  37. package/dist/runtime/utils/index.d.ts +1 -0
  38. package/dist/runtime/utils/index.js +3 -0
  39. package/package.json +15 -12
  40. package/dist/runtime/types/component.d.ts +0 -17
  41. 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, RadioGroupItemProps } from 'radix-vue'
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 extends Pick<RadioGroupItemProps, 'disabled' | 'value'> {
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[props.valueKey]
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, SelectItemProps } from 'radix-vue'
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 extends Pick<SelectItemProps, 'disabled' | 'value'> {
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.label }}
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[valueKey as keyof SelectItem] as string) : 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
- <UAvatar v-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
198
- <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
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.label : 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, ComboboxItemProps, ComboboxArrowProps } from 'radix-vue'
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 extends Pick<ComboboxItemProps, 'disabled'> {
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
- select?(e?: Event): void
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(val: T, multiple?: boolean): string {
154
- if (multiple && Array.isArray(val)) {
155
- return val.map(v => displayValue(v)).join(', ')
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
- if (typeof val === 'object') {
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 val && String(val)
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 : ['label']
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(searchTerm, 'i')) !== -1
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(searchTerm, 'i')) !== -1
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
- <span v-if="multiple ? modelValue?.length : modelValue !== undefined" :class="ui.value({ class: props.ui?.value })">
231
- {{ displayValue(modelValue as T, multiple) }}
232
- </span>
233
- <span v-else :class="ui.placeholder({ class: props.ui?.placeholder })">
234
- {{ placeholder ?? '&nbsp;' }}
235
- </span>
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 ?? '&nbsp;' }}
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.label }}
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[valueKey as keyof SelectMenuItem]) as AcceptableValue : item"
276
- @select="item.select"
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
- <UAvatar v-if="item.avatar" :size="((props.ui?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: props.ui?.itemLeadingAvatar })" />
281
- <UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: props.ui?.itemLeadingIcon })" />
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
- {{ displayValue(item as T) }}
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>