Merge branch 'v3' into feat/3880

This commit is contained in:
HugoRCD
2025-07-03 16:41:03 +02:00
85 changed files with 1206 additions and 716 deletions

View File

@@ -42,14 +42,15 @@ export interface ButtonSlots {
</script>
<script setup lang="ts">
import { type Ref, computed, ref, inject } from 'vue'
import { computed, ref, inject } from 'vue'
import type { Ref } from 'vue'
import { defu } from 'defu'
import { useForwardProps } from 'reka-ui'
import { useAppConfig } from '#imports'
import { useComponentIcons } from '../composables/useComponentIcons'
import { useButtonGroup } from '../composables/useButtonGroup'
import { formLoadingInjectionKey } from '../composables/useFormField'
import { omit } from '../utils'
import { omit, mergeClasses } from '../utils'
import { tv } from '../utils/tv'
import { pickLinkProps } from '../utils/link'
import UIcon from './Icon.vue'
@@ -57,11 +58,7 @@ import UAvatar from './Avatar.vue'
import ULink from './Link.vue'
import ULinkBase from './LinkBase.vue'
const props = withDefaults(defineProps<ButtonProps>(), {
active: undefined,
activeClass: '',
inactiveClass: ''
})
const props = defineProps<ButtonProps>()
const slots = defineSlots<ButtonSlots>()
const appConfig = useAppConfig() as Button['AppConfig']
@@ -96,10 +93,10 @@ const ui = computed(() => tv({
variants: {
active: {
true: {
base: props.activeClass
base: mergeClasses(appConfig.ui?.button?.variants?.active?.true?.base, props.activeClass)
},
false: {
base: props.inactiveClass
base: mergeClasses(appConfig.ui?.button?.variants?.active?.false?.base, props.inactiveClass)
}
}
}

View File

@@ -47,7 +47,6 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue'
import UKbd from './Kbd.vue'
// eslint-disable-next-line import/no-self-import
import UContextMenuContent from './ContextMenuContent.vue'
const props = defineProps<ContextMenuContentProps<T>>()

View File

@@ -53,7 +53,6 @@ import ULink from './Link.vue'
import UAvatar from './Avatar.vue'
import UIcon from './Icon.vue'
import UKbd from './Kbd.vue'
// eslint-disable-next-line import/no-self-import
import UDropdownMenuContent from './DropdownMenuContent.vue'
const props = defineProps<DropdownMenuContentProps<T>>()

View File

@@ -47,7 +47,8 @@ export interface FormFieldSlots {
</script>
<script setup lang="ts">
import { computed, ref, inject, provide, type Ref, useId } from 'vue'
import { computed, ref, inject, provide, useId } from 'vue'
import type { Ref } from 'vue'
import { Primitive, Label } from 'reka-ui'
import { useAppConfig } from '#imports'
import { formFieldInjectionKey, inputIdInjectionKey } from '../composables/useFormField'

View File

@@ -88,11 +88,12 @@ export interface LinkSlots {
<script setup lang="ts">
import { computed } from 'vue'
import { defu } from 'defu'
import { isEqual } from 'ohash/utils'
import { useForwardProps } from 'reka-ui'
import { defu } from 'defu'
import { reactiveOmit } from '@vueuse/core'
import { useRoute, useAppConfig } from '#imports'
import { mergeClasses } from '../utils'
import { tv } from '../utils/tv'
import { isPartiallyEqual } from '../utils/link'
import ULinkBase from './LinkBase.vue'
@@ -103,9 +104,7 @@ const props = withDefaults(defineProps<LinkProps>(), {
as: 'button',
type: 'button',
ariaCurrentValue: 'page',
active: undefined,
activeClass: '',
inactiveClass: ''
active: undefined
})
defineSlots<LinkSlots>()
@@ -119,8 +118,8 @@ const ui = computed(() => tv({
...defu({
variants: {
active: {
true: props.activeClass,
false: props.inactiveClass
true: mergeClasses(appConfig.ui?.link?.variants?.active?.true, props.activeClass),
false: mergeClasses(appConfig.ui?.link?.variants?.active?.false, props.inactiveClass)
}
}
}, appConfig.ui?.link || {})

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useOverlay, type Overlay } from '../composables/useOverlay'
import { useOverlay } from '../composables/useOverlay'
import type { Overlay } from '../composables/useOverlay'
const { overlays, unmount, close } = useOverlay()

View File

@@ -83,10 +83,10 @@ export interface TableProps<T extends TableData = TableData> extends TableOption
*/
empty?: string
/**
* Whether the table should have a sticky header.
* Whether the table should have a sticky header or footer. True for both, 'header' for header only, 'footer' for footer only.
* @defaultValue false
*/
sticky?: boolean
sticky?: boolean | 'header' | 'footer'
/** Whether the table should be in loading state. */
loading?: boolean
/**
@@ -172,6 +172,7 @@ export interface TableProps<T extends TableData = TableData> extends TableOption
}
type DynamicHeaderSlots<T, K = keyof T> = Record<string, (props: HeaderContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-header`, (props: HeaderContext<T, unknown>) => any>
type DynamicFooterSlots<T, K = keyof T> = Record<string, (props: HeaderContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-footer`, (props: HeaderContext<T, unknown>) => any>
type DynamicCellSlots<T, K = keyof T> = Record<string, (props: CellContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-cell`, (props: CellContext<T, unknown>) => any>
export type TableSlots<T extends TableData = TableData> = {
@@ -181,7 +182,7 @@ export type TableSlots<T extends TableData = TableData> = {
'caption': (props?: {}) => any
'body-top': (props?: {}) => any
'body-bottom': (props?: {}) => any
} & DynamicHeaderSlots<T> & DynamicCellSlots<T>
} & DynamicHeaderSlots<T> & DynamicFooterSlots<T> & DynamicCellSlots<T>
</script>
@@ -216,6 +217,22 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.table || {})
loadingAnimation: props.loadingAnimation
}))
const hasFooter = computed(() => {
function hasFooterRecursive(columns: TableColumn<T>[]): boolean {
for (const column of columns) {
if ('footer' in column) {
return true
}
if ('columns' in column && hasFooterRecursive(column.columns as TableColumn<T>[])) {
return true
}
}
return false
}
return hasFooterRecursive(columns.value)
})
const globalFilterState = defineModel<string>('globalFilter', { default: undefined })
const columnFiltersState = defineModel<ColumnFiltersState>('columnFilters', { default: [] })
const columnOrderState = defineModel<ColumnOrderState>('columnOrder', { default: [] })
@@ -461,6 +478,30 @@ defineExpose({
<slot name="body-bottom" />
</tbody>
<tfoot v-if="hasFooter" :class="ui.tfoot({ class: [props.ui?.tfoot] })">
<tr :class="ui.separator({ class: [props.ui?.separator] })" />
<tr v-for="footerGroup in tableApi.getFooterGroups()" :key="footerGroup.id" :class="ui.tr({ class: [props.ui?.tr] })">
<th
v-for="header in footerGroup.headers"
:key="header.id"
:data-pinned="header.column.getIsPinned()"
:colspan="header.colSpan > 1 ? header.colSpan : undefined"
:class="ui.th({
class: [
props.ui?.th,
typeof header.column.columnDef.meta?.class?.th === 'function' ? header.column.columnDef.meta.class.th(header) : header.column.columnDef.meta?.class?.th
],
pinned: !!header.column.getIsPinned()
})"
>
<slot :name="`${header.id}-footer`" v-bind="header.getContext()">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.footer" :props="header.getContext()" />
</slot>
</th>
</tr>
</tfoot>
</table>
</Primitive>
</template>

View File

@@ -36,6 +36,8 @@ interface Shortcut {
const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/
const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/
// keyboard keys which can be combined with Shift modifier (in addition to alphabet keys)
const shiftableKeys = ['arrowleft', 'arrowright', 'arrowup', 'arrowright', 'tab', 'escape', 'enter', 'backspace']
export function extractShortcuts(items: any[] | any[][]) {
const shortcuts: Record<string, Handler> = {}
@@ -76,7 +78,8 @@ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: Shor
return
}
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
const alphabetKey = /^[a-z]{1}$/i.test(e.key)
const shiftableKey = shiftableKeys.includes(e.key.toLowerCase())
let chainedKey
chainedInputs.value.push(e.key)
@@ -109,9 +112,9 @@ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: Shor
if (e.ctrlKey !== shortcut.ctrlKey) {
continue
}
// shift modifier is only checked in combination with alphabetical keys
// (shift with non-alphabetical keys would change the key)
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) {
// shift modifier is only checked in combination with alphabet keys and some extra keys
// (shift with special characters would change the key)
if ((alphabetKey || shiftableKey) && e.shiftKey !== shortcut.shiftKey) {
continue
}
// alt modifier changes the combined key anyways

View File

@@ -1,4 +1,5 @@
import { inject, provide, computed, type ComputedRef, type InjectionKey } from 'vue'
import { inject, provide, computed } from 'vue'
import type { ComputedRef, InjectionKey } from 'vue'
import type { AvatarGroupProps } from '../types'
export const avatarGroupInjectionKey: InjectionKey<ComputedRef<{ size: AvatarGroupProps['size'] }>> = Symbol('nuxt-ui.avatar-group')

View File

@@ -1,4 +1,5 @@
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useAppConfig } from '#imports'
import type { AvatarProps } from '../types'

View File

@@ -1,5 +1,7 @@
import { inject, computed, type InjectionKey, type Ref, type ComputedRef, provide } from 'vue'
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
import { inject, computed, provide } from 'vue'
import type { InjectionKey, Ref, ComputedRef } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import type { UseEventBusReturn } from '@vueuse/core'
import type { FormFieldProps } from '../types'
import type { FormEvent, FormInputEvents, FormFieldInjectedOptions, FormInjectedOptions } from '../types/form'
import type { GetObjectField } from '../types/utils'

View File

@@ -3,9 +3,34 @@ import { reactive, markRaw, shallowReactive } from 'vue'
import { createSharedComposable } from '@vueuse/core'
import type { ComponentProps, ComponentEmit } from 'vue-component-type-helpers'
// Extracts the first argument of the close event
type CloseEventArgType<T> = T extends (event: 'close', args_0: infer R) => void ? R : never
/**
* This is a workaround for a design limitation in TypeScript.
*
* Conditional types only match the last function overload, not a union of all possible
* parameter types. This workaround forces TypeScript to properly extract the 'close'
* event argument type from component emits with multiple event signatures.
*
* @see https://github.com/microsoft/TypeScript/issues/32164
*/
type CloseEventArgType<T> = T extends {
(event: 'close', arg_0: infer Arg, ...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
(...args: any[]): void
} ? Arg : never
export type OverlayOptions<OverlayAttrs = Record<string, any>> = {
defaultOpen?: boolean
props?: OverlayAttrs

View File

@@ -1,4 +1,5 @@
import { inject, provide, computed, type Ref, type InjectionKey } from 'vue'
import { inject, provide, computed } from 'vue'
import type { Ref, InjectionKey } from 'vue'
export const portalTargetInjectionKey: InjectionKey<Ref<string | HTMLElement>> = Symbol('nuxt-ui.portal-target')

View File

@@ -21,11 +21,11 @@ export interface Form<S extends FormSchema> {
blurredFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
}
export type FormSchema<I extends object = object, O extends object = I> =
| YupObjectSchema<I>
| JoiSchema<I>
| SuperstructSchema<any, any>
| StandardSchemaV1<I, O>
export type FormSchema<I extends object = object, O extends object = I>
= | YupObjectSchema<I>
| JoiSchema<I>
| SuperstructSchema<any, any>
| StandardSchemaV1<I, O>
// Define a utility type to infer the input type based on the schema type
export type InferInput<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<Schema>
@@ -83,10 +83,10 @@ export type FormInputEvent<T extends object> = {
eager?: boolean
}
export type FormEvent<T extends object> =
| FormInputEvent<T>
| FormChildAttachEvent
| FormChildDetachEvent
export type FormEvent<T extends object>
= | FormInputEvent<T>
| FormChildAttachEvent
| FormChildDetachEvent
export interface FormInjectedOptions {
disabled?: boolean

View File

@@ -30,8 +30,8 @@ type ComponentSlots<T extends { slots?: Record<string, any> }> = Id<{
[K in keyof T['slots']]?: ClassValue
}>
type GetComponentAppConfig<A, U extends string, K extends string> =
A extends Record<U, Record<K, any>> ? A[U][K] : {}
type GetComponentAppConfig<A, U extends string, K extends string>
= A extends Record<U, Record<K, any>> ? A[U][K] : {}
type ComponentAppConfig<
T,

View File

@@ -44,8 +44,8 @@ export type MergeTypes<T extends object> = {
export type GetItemKeys<I> = keyof Extract<NestedItem<I>, object>
export type GetItemValue<I, VK extends GetItemKeys<I> | undefined, T extends NestedItem<I> = NestedItem<I>> =
T extends object
export type GetItemValue<I, VK extends GetItemKeys<I> | undefined, T extends NestedItem<I> = NestedItem<I>>
= T extends object
? VK extends undefined
? T
: VK extends keyof T
@@ -70,10 +70,10 @@ export type GetModelValueEmits<
'update:modelValue': [payload: GetModelValue<T, VK, M>]
}
export type StringOrVNode =
| string
| VNode
| (() => VNode)
export type StringOrVNode
= | string
| VNode
| (() => VNode)
export type EmitsToProps<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: T[K] extends [...args: infer Args]

View File

@@ -85,3 +85,14 @@ export function compare<T>(value?: T, currentValue?: T, comparator?: string | ((
export function isArrayOfArray<A>(item: A[] | A[][]): item is A[][] {
return Array.isArray(item[0])
}
export function mergeClasses(appConfigClass?: string | string[], propClass?: string) {
if (!appConfigClass && !propClass) {
return ''
}
return [
...(Array.isArray(appConfigClass) ? appConfigClass : [appConfigClass]),
propClass
].filter(Boolean)
}

View File

@@ -1,4 +1,5 @@
import { createTV, type defaultConfig } from 'tailwind-variants'
import { createTV } from 'tailwind-variants'
import type { defaultConfig } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import appConfig from '#build/app.config'