mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-18 05:58:07 +01:00
feat: rewrite to use app config and rework docs (#143)
Co-authored-by: Daniel Roe <daniel@roe.dev> Co-authored-by: Sébastien Chopin <seb@nuxt.com>
This commit is contained in:
@@ -1,123 +1,135 @@
|
||||
<template>
|
||||
<span :class="wrapperClass">
|
||||
<img v-if="url && !error" :class="avatarClass" :src="url" :alt="alt" :onerror="() => onError()">
|
||||
<span v-else-if="text || placeholder" :class="placeholderClass">{{ text || placeholder }}</span>
|
||||
<span v-else-if="text || placeholder" :class="ui.placeholder">{{ text || placeholder }}</span>
|
||||
|
||||
<span v-if="chip" :class="chipClass" />
|
||||
<span v-if="chipColor" :class="chipClass" />
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
chip: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.chip.variant).includes(value)
|
||||
}
|
||||
},
|
||||
chipPosition: {
|
||||
type: String,
|
||||
default: 'top-right',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.chip.position).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.wrapper
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.background
|
||||
},
|
||||
placeholderClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.placeholder
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatar.rounded
|
||||
}
|
||||
})
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
return classNames(
|
||||
props.wrapperClass,
|
||||
props.backgroundClass,
|
||||
$ui.avatar.size[props.size],
|
||||
props.rounded ? 'rounded-full' : props.roundedClass
|
||||
)
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
$ui.avatar.size[props.size],
|
||||
props.rounded ? 'rounded-full' : props.roundedClass
|
||||
)
|
||||
})
|
||||
|
||||
const chipClass = computed(() => {
|
||||
return classNames(
|
||||
$ui.avatar.chip.base,
|
||||
$ui.avatar.chip.variant[props.chip],
|
||||
$ui.avatar.chip.position[props.chipPosition],
|
||||
$ui.avatar.chip.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const url = computed(() => {
|
||||
if (typeof props.src === 'boolean') {
|
||||
return null
|
||||
}
|
||||
return props.src
|
||||
})
|
||||
|
||||
const placeholder = computed(() => {
|
||||
return (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2)
|
||||
})
|
||||
|
||||
const error = ref(false)
|
||||
|
||||
watch(() => props.src, () => {
|
||||
if (error.value) {
|
||||
error.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function onError () {
|
||||
error.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAvatar' }
|
||||
import { defineComponent, ref, computed, watch } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
src: {
|
||||
type: [String, Boolean],
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.avatar.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
chipColor: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return appConfig.ui.colors.includes(value)
|
||||
}
|
||||
},
|
||||
chipVariant: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.avatar.default.chipVariant,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.chip.variant).includes(value)
|
||||
}
|
||||
},
|
||||
chipPosition: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.avatar.default.chipPosition,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.chip.position).includes(value)
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.avatar>>,
|
||||
default: () => appConfig.ui.avatar
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.avatar>>(() => defu({}, props.ui, appConfig.ui.avatar))
|
||||
|
||||
const wrapperClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.wrapper,
|
||||
ui.value.background,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const chipClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.chip.base,
|
||||
ui.value.chip.size[props.size],
|
||||
ui.value.chip.position[props.chipPosition],
|
||||
ui.value.chip.variant[props.chipVariant]?.replaceAll('{color}', props.chipColor)
|
||||
)
|
||||
})
|
||||
|
||||
const url = computed(() => {
|
||||
if (typeof props.src === 'boolean') {
|
||||
return null
|
||||
}
|
||||
return props.src
|
||||
})
|
||||
|
||||
const placeholder = computed(() => {
|
||||
return (props.alt || '').split(' ').map(word => word.charAt(0)).join('').substring(0, 2)
|
||||
})
|
||||
|
||||
const error = ref(false)
|
||||
|
||||
watch(() => props.src, () => {
|
||||
if (error.value) {
|
||||
error.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function onError () {
|
||||
error.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
wrapperClass,
|
||||
avatarClass,
|
||||
chipClass,
|
||||
url,
|
||||
placeholder,
|
||||
error,
|
||||
onError
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
80
src/runtime/components/elements/AvatarGroup.ts
Normal file
80
src/runtime/components/elements/AvatarGroup.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { h, computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import Avatar from './Avatar.vue'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.avatarGroup>>,
|
||||
default: () => appConfig.ui.avatarGroup
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.avatarGroup>>(() => defu({}, props.ui, appConfig.ui.avatarGroup))
|
||||
|
||||
const children = computed(() => {
|
||||
let children = slots.default?.()
|
||||
// @ts-ignore-next
|
||||
if (children.length && children[0].type.name === 'ContentSlot') {
|
||||
// @ts-ignore-next
|
||||
children = children[0].ctx.slots.default?.()
|
||||
}
|
||||
return children
|
||||
})
|
||||
|
||||
const max = computed(() => typeof props.max === 'string' ? parseInt(props.max, 10) : props.max)
|
||||
|
||||
const clones = computed(() => children.value.map((node, index) => {
|
||||
if (!props.max || (max.value && index < max.value)) {
|
||||
if (props.size) {
|
||||
node.props.size = props.size
|
||||
}
|
||||
|
||||
node.props.class = node.props.class || ''
|
||||
node.props.class += ` ${classNames(
|
||||
ui.value.ring,
|
||||
ui.value.margin
|
||||
)}`
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
if (max.value !== undefined && index === max.value) {
|
||||
return h(Avatar, {
|
||||
size: props.size,
|
||||
text: `+${children.value.length - max.value}`,
|
||||
class: classNames(
|
||||
ui.value.ring,
|
||||
ui.value.margin
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return null
|
||||
}).filter(Boolean).reverse())
|
||||
|
||||
return () => h('div', { class: ui.value.wrapper }, clones.value)
|
||||
}
|
||||
})
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-row-reverse">
|
||||
<Avatar
|
||||
v-if="remainingGroupSize > 0"
|
||||
:size="size"
|
||||
:text="`+${remainingGroupSize}`"
|
||||
:class="avatarClass"
|
||||
/>
|
||||
<Avatar
|
||||
v-for="(avatar, index) of displayedGroup"
|
||||
:key="index"
|
||||
v-bind="avatar"
|
||||
:size="size"
|
||||
:class="avatarClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import Avatar from './Avatar.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
group: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatarGroup.ring
|
||||
},
|
||||
marginClass: {
|
||||
type: String,
|
||||
default: () => $ui.avatarGroup.margin
|
||||
}
|
||||
})
|
||||
|
||||
const avatars = computed(() => {
|
||||
return props.group.map((avatar) => {
|
||||
return typeof avatar === 'string' ? { src: avatar } : avatar
|
||||
})
|
||||
})
|
||||
|
||||
const displayedGroup = computed(() => {
|
||||
if (!props.max) { return [...avatars.value].reverse() }
|
||||
|
||||
return avatars.value.slice(0, props.max).reverse()
|
||||
})
|
||||
|
||||
const remainingGroupSize = computed(() => {
|
||||
if (!props.max) { return 0 }
|
||||
|
||||
return avatars.value.length - props.max
|
||||
})
|
||||
|
||||
const avatarClass = computed(() => {
|
||||
return classNames(
|
||||
props.ringClass,
|
||||
props.marginClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UAvatarGroup' }
|
||||
</script>
|
||||
@@ -4,50 +4,69 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.badge.size).includes(value)
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.badge.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.badge.size).includes(value)
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.badge.default.color,
|
||||
validator (value: string) {
|
||||
return appConfig.ui.colors.includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.badge.default.variant,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.badge.variant).includes(value)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.badge>>,
|
||||
default: () => appConfig.ui.badge
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.badge.variant).includes(value)
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.badge>>(() => defu({}, props.ui, appConfig.ui.badge))
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.font,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size],
|
||||
ui.value.variant[props.variant]?.replaceAll('{color}', props.color)
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
badgeClass
|
||||
}
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.badge.base
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.badge.size[props.size],
|
||||
$ui.badge.variant[props.variant],
|
||||
props.rounded ? 'rounded-full' : 'rounded-md'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UBadge' }
|
||||
</script>
|
||||
|
||||
@@ -8,220 +8,224 @@
|
||||
>
|
||||
<Icon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
|
||||
<slot>
|
||||
<span :class="[truncate ? 'text-left break-all line-clamp-1' : '', compact ? 'hidden sm:block' : '']">
|
||||
<span :class="[labelCompact && 'hidden sm:block']">{{ label }}</span>
|
||||
<span v-if="labelCompact" class="sm:hidden">{{ labelCompact }}</span>
|
||||
<span v-if="label" :class="[truncate ? 'text-left break-all line-clamp-1' : '']">
|
||||
{{ label }}
|
||||
</span>
|
||||
</slot>
|
||||
<Icon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, useSlots } from 'vue'
|
||||
<script lang="ts">
|
||||
import { ref, computed, defineComponent, useSlots } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router'
|
||||
import NuxtLink from '#app/components/nuxt-link'
|
||||
import { defu } from 'defu'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
import { NuxtLink } from '#components'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Icon
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
labelCompact: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.button.size).includes(value)
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
padded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.button.size).includes(value)
|
||||
}
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.color,
|
||||
validator (value: string) {
|
||||
return [...appConfig.ui.colors, ...Object.keys(appConfig.ui.button.color)].includes(value)
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.variant,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.button.variant).includes(value)
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.button.default.loadingIcon
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationNormalized | RouteLocationRaw>,
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
square: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.button>>,
|
||||
default: () => appConfig.ui.button
|
||||
}
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.button.variant).includes(value)
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.button>>(() => defu({}, props.ui, appConfig.ui.button))
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const button = ref(null)
|
||||
|
||||
const buttonIs = computed(() => {
|
||||
if (props.to) {
|
||||
return NuxtLink
|
||||
}
|
||||
|
||||
return 'button'
|
||||
})
|
||||
|
||||
const buttonProps = computed(() => {
|
||||
if (props.to) {
|
||||
return { to: props.to, target: props.target }
|
||||
} else {
|
||||
return { disabled: props.disabled || props.loading, type: props.type }
|
||||
}
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||
})
|
||||
|
||||
const isSquare = computed(() => props.square || (!slots.default && !props.label))
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.font,
|
||||
ui.value.rounded,
|
||||
ui.value.size[props.size],
|
||||
ui.value.gap[props.size],
|
||||
props.padded && ui.value[isSquare.value ? 'square' : 'spacing'][props.size],
|
||||
variant?.replaceAll('{color}', props.color),
|
||||
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center'
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.leadingIcon || props.icon
|
||||
})
|
||||
|
||||
const trailingIconName = computed(() => {
|
||||
if (props.loading && !isLeading.value) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.trailingIcon || props.icon
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
button,
|
||||
buttonIs,
|
||||
buttonProps,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
isSquare,
|
||||
buttonClass,
|
||||
leadingIconName,
|
||||
trailingIconName,
|
||||
leadingIconClass,
|
||||
trailingIconClass
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.loading
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationNormalized | RouteLocationRaw>,
|
||||
default: null
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.rounded
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.button.icon.base
|
||||
},
|
||||
leadingIconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
trailingIconClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
square: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const button = ref(null)
|
||||
|
||||
const buttonIs = computed(() => {
|
||||
if (props.to) {
|
||||
return NuxtLink
|
||||
}
|
||||
|
||||
return 'button'
|
||||
})
|
||||
|
||||
const buttonProps = computed(() => {
|
||||
if (props.to) {
|
||||
return { to: props.to, target: props.target }
|
||||
} else {
|
||||
return { disabled: props.disabled || props.loading, type: props.type }
|
||||
}
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||
})
|
||||
|
||||
const isSquare = computed(() => props.square || (!slots.default && !props.label))
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.button.size[props.size],
|
||||
$ui.button[isSquare.value ? 'square' : (props.compact ? 'compact' : 'spacing')][props.size],
|
||||
$ui.button.variant[props.variant],
|
||||
props.block ? 'w-full flex justify-center items-center' : 'inline-flex items-center',
|
||||
props.rounded ? 'rounded-full' : props.roundedClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.leadingIcon || props.icon
|
||||
})
|
||||
|
||||
const trailingIconName = computed(() => {
|
||||
if (props.loading && !isLeading.value) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.trailingIcon || props.icon
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.button.icon.size[props.size],
|
||||
(!!slots.default || !!props.label?.length) && $ui.button.icon.leading[props.compact ? 'compactSpacing' : 'spacing'][props.size],
|
||||
props.leadingIconClass,
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.button.icon.size[props.size],
|
||||
(!!slots.default || !!props.label?.length) && $ui.button.icon.trailing[props.compact ? 'compactSpacing' : 'spacing'][props.size],
|
||||
props.trailingIconClass,
|
||||
props.loading && !isLeading.value && 'animate-spin'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UButton' }
|
||||
</script>
|
||||
|
||||
80
src/runtime/components/elements/ButtonGroup.ts
Normal file
80
src/runtime/components/elements/ButtonGroup.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { h, computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.avatar.size).includes(value)
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.buttonGroup>>,
|
||||
default: () => appConfig.ui.buttonGroup
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.buttonGroup>>(() => defu({}, props.ui, appConfig.ui.buttonGroup))
|
||||
|
||||
const children = computed(() => {
|
||||
let children = slots.default?.()
|
||||
// @ts-ignore-next
|
||||
if (children.length && children[0].type.name === 'ContentSlot') {
|
||||
// @ts-ignore-next
|
||||
children = children[0].ctx.slots.default?.()
|
||||
}
|
||||
return children
|
||||
})
|
||||
|
||||
const rounded = computed(() => ({
|
||||
'rounded-none': { left: 'rounded-l-none', right: 'rounded-r-none' },
|
||||
'rounded-sm': { left: 'rounded-l-sm', right: 'rounded-r-sm' },
|
||||
rounded: { left: 'rounded-l', right: 'rounded-r' },
|
||||
'rounded-md': { left: 'rounded-l-md', right: 'rounded-r-md' },
|
||||
'rounded-lg': { left: 'rounded-l-lg', right: 'rounded-r-lg' },
|
||||
'rounded-xl': { left: 'rounded-l-xl', right: 'rounded-r-xl' },
|
||||
'rounded-2xl': { left: 'rounded-l-2xl', right: 'rounded-r-2xl' },
|
||||
'rounded-3xl': { left: 'rounded-l-3xl', right: 'rounded-r-3xl' },
|
||||
'rounded-full': { left: 'rounded-l-full', right: 'rounded-r-full' }
|
||||
}[ui.value.rounded]))
|
||||
|
||||
const clones = computed(() => children.value.map((node, index) => {
|
||||
if (props.size) {
|
||||
node.props.size = props.size
|
||||
}
|
||||
|
||||
node.props.class = node.props.class || ''
|
||||
node.props.class += ' !shadow-none'
|
||||
node.props.ui = node.props.ui || {}
|
||||
node.props.ui.rounded = ''
|
||||
|
||||
if (index === 0) {
|
||||
node.props.ui.rounded = rounded.value.left
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
node.props.class += ' -ml-px'
|
||||
}
|
||||
|
||||
if (index === children.value.length - 1) {
|
||||
node.props.ui.rounded = rounded.value.right
|
||||
}
|
||||
|
||||
return node
|
||||
}))
|
||||
|
||||
return () => h('div', { class: [ui.value.wrapper, ui.value.rounded, ui.value.shadow] }, clones.value)
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Menu v-slot="{ open }" as="div" :class="wrapperClass" @mouseleave="onMouseLeave">
|
||||
<Menu v-slot="{ open }" as="div" :class="ui.wrapper" @mouseleave="onMouseLeave">
|
||||
<MenuButton
|
||||
ref="trigger"
|
||||
as="div"
|
||||
@@ -8,17 +8,17 @@
|
||||
role="button"
|
||||
@mouseover="onMouseOver"
|
||||
>
|
||||
<slot :open="open">
|
||||
<slot :open="open" :disabled="disabled">
|
||||
<button :disabled="disabled">
|
||||
Open
|
||||
</button>
|
||||
</slot>
|
||||
</MenuButton>
|
||||
|
||||
<div v-if="open && items.length" ref="container" :class="[containerClass, widthClass]" @mouseover="onMouseOver">
|
||||
<transition appear v-bind="transitionClass">
|
||||
<MenuItems :class="[baseClass, divideClass, ringClass, roundedClass, shadowClass, backgroundClass]" static>
|
||||
<div v-for="(subItems, index) of items" :key="index" :class="groupClass">
|
||||
<div v-if="open && items.length" ref="container" :class="[ui.container, ui.width]" @mouseover="onMouseOver">
|
||||
<transition appear v-bind="ui.transition">
|
||||
<MenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background]" static>
|
||||
<div v-for="(subItems, index) of items" :key="index" :class="ui.spacing">
|
||||
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
|
||||
<Component
|
||||
v-bind="omit(item, ['click'])"
|
||||
@@ -26,13 +26,13 @@
|
||||
:class="resolveItemClass({ active, disabled: itemDisabled })"
|
||||
@click="item.click"
|
||||
>
|
||||
<slot :name="item.slot" :item="item">
|
||||
<Icon v-if="item.icon" :name="item.icon" :class="[itemIconClass, item.iconClass]" />
|
||||
<Avatar v-if="item.avatar" v-bind="{ size: 'xxs', ...item.avatar }" :class="itemAvatarClass" />
|
||||
<slot :name="item.slot || 'item'" :item="item">
|
||||
<Icon v-if="item.icon" :name="item.icon" :class="[ui.item.icon.base, active ? ui.item.icon.active : ui.item.icon.inactive, item.iconClass]" />
|
||||
<Avatar v-else-if="item.avatar" v-bind="{ size: ui.item.avatar.size, ...item.avatar }" :class="ui.item.avatar.base" />
|
||||
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
|
||||
<span v-if="item.shortcuts?.length" :class="itemShortcutsClass">
|
||||
<span v-if="item.shortcuts?.length" :class="ui.item.shortcuts">
|
||||
<kbd v-for="shortcut of item.shortcuts" :key="shortcut" class="font-sans">{{ shortcut }}</kbd>
|
||||
</span>
|
||||
</slot>
|
||||
@@ -45,29 +45,39 @@
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem
|
||||
} from '@headlessui/vue'
|
||||
<script lang="ts">
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { defineComponent, ref, computed, onMounted } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import NuxtLink from '#app/components/nuxt-link'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import { classNames, omit } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { Avatar as AvatarType } from '../../types/avatar'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
import { NuxtLink } from '#components'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array as PropType<{
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
MenuItem,
|
||||
Icon,
|
||||
Avatar
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array as PropType<{
|
||||
to?: RouteLocationNormalized
|
||||
exact?: boolean
|
||||
label: string
|
||||
@@ -79,176 +89,123 @@ const props = defineProps({
|
||||
click?: Function
|
||||
shortcuts?: string[]
|
||||
}[][]>,
|
||||
default: () => []
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'click',
|
||||
validator: (value: string) => {
|
||||
return ['click', 'hover'].includes(value)
|
||||
default: () => []
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'click',
|
||||
validator: (value: string) => {
|
||||
return ['click', 'hover'].includes(value)
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
popper: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.dropdown>>,
|
||||
default: () => appConfig.ui.dropdown
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.container
|
||||
},
|
||||
widthClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.width
|
||||
},
|
||||
backgroundClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.background
|
||||
},
|
||||
shadowClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.shadow
|
||||
},
|
||||
roundedClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.rounded
|
||||
},
|
||||
ringClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.ring
|
||||
},
|
||||
divideClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.divide
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.base
|
||||
},
|
||||
transitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.dropdown.transition
|
||||
},
|
||||
groupClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.group
|
||||
},
|
||||
itemBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.base
|
||||
},
|
||||
itemActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.active
|
||||
},
|
||||
itemInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.inactive
|
||||
},
|
||||
itemDisabledClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.disabled
|
||||
},
|
||||
itemIconClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.icon
|
||||
},
|
||||
itemAvatarClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.avatar
|
||||
},
|
||||
itemShortcutsClass: {
|
||||
type: String,
|
||||
default: () => $ui.dropdown.item.shortcuts
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
openDelay: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
closeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.dropdown.popperOptions))
|
||||
const ui = computed<Partial<typeof appConfig.ui.dropdown>>(() => defu({}, props.ui, appConfig.ui.dropdown))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
|
||||
|
||||
function resolveItemClass ({ active, disabled }: { active: boolean, disabled: boolean }) {
|
||||
return classNames(
|
||||
props.itemBaseClass,
|
||||
active ? props.itemActiveClass : props.itemInactiveClass,
|
||||
disabled && props.itemDisabledClass
|
||||
)
|
||||
}
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/menu/menu.ts#L131
|
||||
const menuApi = ref<any>(null)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const menuProvides = trigger.value?.$.provides
|
||||
if (!menuProvides) {
|
||||
return
|
||||
function resolveItemClass ({ active, disabled }: { active: boolean, disabled: boolean }) {
|
||||
return classNames(
|
||||
ui.value.item.base,
|
||||
active ? ui.value.item.active : ui.value.item.inactive,
|
||||
disabled && ui.value.item.disabled
|
||||
)
|
||||
}
|
||||
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
|
||||
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
|
||||
}, 200)
|
||||
|
||||
// https://github.com/tailwindlabs/headlessui/blob/f66f4926c489fc15289d528294c23a3dc2aee7b1/packages/%40headlessui-vue/src/components/menu/menu.ts#L131
|
||||
const menuApi = ref<any>(null)
|
||||
|
||||
let openTimeout: NodeJS.Timeout | null = null
|
||||
let closeTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
// @ts-expect-error internals
|
||||
const menuProvides = trigger.value?.$.provides
|
||||
if (!menuProvides) {
|
||||
return
|
||||
}
|
||||
const menuProvidesSymbols = Object.getOwnPropertySymbols(menuProvides)
|
||||
menuApi.value = menuProvidesSymbols.length && menuProvides[menuProvidesSymbols[0]]
|
||||
}, 200)
|
||||
})
|
||||
|
||||
function onMouseOver () {
|
||||
if (props.mode !== 'hover' || !menuApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// cancel programmed closing
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout)
|
||||
closeTimeout = null
|
||||
}
|
||||
// dropdown already open
|
||||
if (menuApi.value.menuState === 0) {
|
||||
return
|
||||
}
|
||||
openTimeout = openTimeout || setTimeout(() => {
|
||||
menuApi.value.openMenu && menuApi.value.openMenu()
|
||||
openTimeout = null
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
function onMouseLeave () {
|
||||
if (props.mode !== 'hover' || !menuApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// cancel programmed opening
|
||||
if (openTimeout) {
|
||||
clearTimeout(openTimeout)
|
||||
openTimeout = null
|
||||
}
|
||||
// dropdown already closed
|
||||
if (menuApi.value.menuState === 1) {
|
||||
return
|
||||
}
|
||||
closeTimeout = closeTimeout || setTimeout(() => {
|
||||
menuApi.value.closeMenu && menuApi.value.closeMenu()
|
||||
closeTimeout = null
|
||||
}, props.closeDelay)
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
trigger,
|
||||
container,
|
||||
resolveItemClass,
|
||||
onMouseOver,
|
||||
onMouseLeave,
|
||||
omit,
|
||||
NuxtLink
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function onMouseOver () {
|
||||
if (props.mode !== 'hover' || !menuApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// cancel programmed closing
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout)
|
||||
closeTimeout = null
|
||||
}
|
||||
// dropdown already open
|
||||
if (menuApi.value.menuState === 0) {
|
||||
return
|
||||
}
|
||||
openTimeout = openTimeout || setTimeout(() => {
|
||||
menuApi.value.openMenu && menuApi.value.openMenu()
|
||||
openTimeout = null
|
||||
}, props.openDelay)
|
||||
}
|
||||
|
||||
function onMouseLeave () {
|
||||
if (props.mode !== 'hover' || !menuApi.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// cancel programmed opening
|
||||
if (openTimeout) {
|
||||
clearTimeout(openTimeout)
|
||||
openTimeout = null
|
||||
}
|
||||
// dropdown already closed
|
||||
if (menuApi.value.menuState === 1) {
|
||||
return
|
||||
}
|
||||
closeTimeout = closeTimeout || setTimeout(() => {
|
||||
menuApi.value.closeMenu && menuApi.value.closeMenu()
|
||||
closeTimeout = null
|
||||
}, props.closeDelay)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UDropdown' }
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,3 @@ defineProps({
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UIcon' }
|
||||
</script>
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<button v-if="isButton" v-bind="$attrs" :class="inactiveClass">
|
||||
<slot />
|
||||
</button>
|
||||
<a v-else-if="isExternalLink" v-bind="$attrs" :href="to" :target="target" :class="inactiveClass">
|
||||
<slot />
|
||||
</a>
|
||||
<router-link
|
||||
v-else
|
||||
v-slot="{ href, navigate, isActive, isExactActive }"
|
||||
v-bind="$props as RouterLinkProps"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="resolveLinkClass({ isActive, isExactActive })"
|
||||
@click="navigate"
|
||||
>
|
||||
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
|
||||
</a>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteLocationNormalized, RouterLinkProps } from 'vue-router'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
// @ts-expect-error internal props
|
||||
...RouterLink.props,
|
||||
to: {
|
||||
type: [String, Object] as PropType<string | RouteLocationNormalized>,
|
||||
default: null
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
exact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const isExternalLink = computed(() => {
|
||||
return typeof props.to === 'string' && props.to.startsWith('http')
|
||||
})
|
||||
const isButton = computed(() => {
|
||||
return !props.to
|
||||
})
|
||||
|
||||
function resolveLinkClass ({ isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
|
||||
if (props.exact) {
|
||||
return isExactActive ? props.activeClass : props.inactiveClass
|
||||
} else {
|
||||
return isActive ? props.activeClass : props.inactiveClass
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ULink',
|
||||
inheritAttrs: false
|
||||
}
|
||||
</script>
|
||||
36
src/runtime/components/elements/LinkCustom.vue
Normal file
36
src/runtime/components/elements/LinkCustom.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<NuxtLink
|
||||
v-slot="{ href, navigate, exact, isActive, isExactActive }"
|
||||
custom
|
||||
>
|
||||
<a
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:class="resolveLinkClass({ isActive, isExactActive })"
|
||||
@click="navigate"
|
||||
>
|
||||
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
function resolveLinkClass ({ isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
|
||||
if (isActive || isExactActive) {
|
||||
return props.activeClass
|
||||
}
|
||||
|
||||
return props.inactiveClass
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user