mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-21 23:40:39 +01:00
feat(module): add support for vue using unplugin (#2416)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -3,8 +3,54 @@ import type { ButtonHTMLAttributes } from 'vue'
|
||||
import { tv } from 'tailwind-variants'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import type { RouterLinkProps, RouteLocationRaw } from 'vue-router'
|
||||
import theme from '#build/ui/link'
|
||||
import type { NuxtLinkProps } from '#app'
|
||||
|
||||
interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
|
||||
/**
|
||||
* Route Location the link should navigate to when clicked on.
|
||||
*/
|
||||
to?: RouteLocationRaw // need to manually type to avoid breaking typedPages
|
||||
/**
|
||||
* An alias for `to`. If used with `to`, `href` will be ignored
|
||||
*/
|
||||
href?: NuxtLinkProps['to']
|
||||
/**
|
||||
* Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases
|
||||
*/
|
||||
external?: boolean
|
||||
/**
|
||||
* Where to display the linked URL, as the name for a browsing context.
|
||||
*/
|
||||
target?: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null
|
||||
/**
|
||||
* A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links.
|
||||
*/
|
||||
rel?: 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | (string & {}) | null
|
||||
/**
|
||||
* If set to true, no rel attribute will be added to the link
|
||||
*/
|
||||
noRel?: boolean
|
||||
/**
|
||||
* A class to apply to links that have been prefetched.
|
||||
*/
|
||||
prefetchedClass?: string
|
||||
/**
|
||||
* When enabled will prefetch middleware, layouts and payloads of links in the viewport.
|
||||
*/
|
||||
prefetch?: boolean
|
||||
/**
|
||||
* Allows controlling when to prefetch links. By default, prefetch is triggered only on visibility.
|
||||
*/
|
||||
prefetchOn?: 'visibility' | 'interaction' | Partial<{
|
||||
visibility: boolean
|
||||
interaction: boolean
|
||||
}>
|
||||
/**
|
||||
* Escape hatch to disable `prefetch` attribute.
|
||||
*/
|
||||
noPrefetch?: boolean
|
||||
}
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { link: Partial<typeof theme> } }
|
||||
|
||||
|
||||
15
src/runtime/vue/components/Icon.vue
Normal file
15
src/runtime/vue/components/Icon.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
export interface IconProps {
|
||||
name: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
defineProps<IconProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Icon :icon="name.replace(/^i-/, '')" />
|
||||
</template>
|
||||
195
src/runtime/vue/components/Link.vue
Normal file
195
src/runtime/vue/components/Link.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script lang="ts">
|
||||
import type { ButtonHTMLAttributes } from 'vue'
|
||||
import { tv } from 'tailwind-variants'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import _appConfig from '#build/app.config'
|
||||
import type { RouterLinkProps, RouteLocationRaw } from 'vue-router'
|
||||
import theme from '#build/ui/link'
|
||||
|
||||
interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
|
||||
/**
|
||||
* Route Location the link should navigate to when clicked on.
|
||||
*/
|
||||
to?: RouteLocationRaw // need to manually type to avoid breaking typedPages
|
||||
/**
|
||||
* An alias for `to`. If used with `to`, `href` will be ignored
|
||||
*/
|
||||
href?: NuxtLinkProps['to']
|
||||
/**
|
||||
* Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases
|
||||
*/
|
||||
external?: boolean
|
||||
/**
|
||||
* Where to display the linked URL, as the name for a browsing context.
|
||||
*/
|
||||
target?: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null
|
||||
/**
|
||||
* A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links.
|
||||
*/
|
||||
rel?: 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | (string & {}) | null
|
||||
/**
|
||||
* If set to true, no rel attribute will be added to the link
|
||||
*/
|
||||
noRel?: boolean
|
||||
/**
|
||||
* A class to apply to links that have been prefetched.
|
||||
*/
|
||||
prefetchedClass?: string
|
||||
/**
|
||||
* When enabled will prefetch middleware, layouts and payloads of links in the viewport.
|
||||
*/
|
||||
prefetch?: boolean
|
||||
/**
|
||||
* Allows controlling when to prefetch links. By default, prefetch is triggered only on visibility.
|
||||
*/
|
||||
prefetchOn?: 'visibility' | 'interaction' | Partial<{
|
||||
visibility: boolean
|
||||
interaction: boolean
|
||||
}>
|
||||
/**
|
||||
* Escape hatch to disable `prefetch` attribute.
|
||||
*/
|
||||
noPrefetch?: boolean
|
||||
}
|
||||
|
||||
const appConfig = _appConfig as AppConfig & { ui: { link: Partial<typeof theme> } }
|
||||
|
||||
const link = tv({ extend: tv(theme), ...(appConfig.ui?.link || {}) })
|
||||
|
||||
export interface LinkProps extends NuxtLinkProps {
|
||||
/**
|
||||
* The element or component this component should render as when not a link.
|
||||
* @defaultValue 'button'
|
||||
*/
|
||||
as?: any
|
||||
/**
|
||||
* The type of the button when not a link.
|
||||
* @defaultValue 'button'
|
||||
*/
|
||||
type?: ButtonHTMLAttributes['type']
|
||||
disabled?: boolean
|
||||
/** Force the link to be active independent of the current route. */
|
||||
active?: boolean
|
||||
/** Will only be active if the current route is an exact match. */
|
||||
exact?: boolean
|
||||
/** Will only be active if the current route query is an exact match. */
|
||||
exactQuery?: boolean
|
||||
/** Will only be active if the current route hash is an exact match. */
|
||||
exactHash?: boolean
|
||||
/** The class to apply when the link is inactive. */
|
||||
inactiveClass?: string
|
||||
custom?: boolean
|
||||
/** When `true`, only styles from `class`, `activeClass`, and `inactiveClass` will be applied. */
|
||||
raw?: boolean
|
||||
class?: any
|
||||
}
|
||||
|
||||
export interface LinkSlots {
|
||||
default(props: { active: boolean }): any
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { isEqual } from 'ohash'
|
||||
import { useForwardProps } from 'radix-vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { hasProtocol } from 'ufo'
|
||||
import { useRoute } from '#imports'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<LinkProps>(), {
|
||||
as: 'button',
|
||||
type: 'button',
|
||||
active: undefined,
|
||||
activeClass: '',
|
||||
inactiveClass: ''
|
||||
})
|
||||
defineSlots<LinkSlots>()
|
||||
|
||||
const route = useRoute()
|
||||
const routerLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'to'))
|
||||
|
||||
const ui = computed(() => tv({
|
||||
extend: link,
|
||||
variants: {
|
||||
active: {
|
||||
true: props.activeClass,
|
||||
false: props.inactiveClass
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const isExternal = computed(() => typeof props.to === 'string' && hasProtocol(props.to, { acceptRelative: true }))
|
||||
|
||||
function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
|
||||
if (props.active !== undefined) {
|
||||
return props.active
|
||||
}
|
||||
|
||||
if (!props.to) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (props.exactQuery && !isEqual(linkRoute.query, route.query)) {
|
||||
return false
|
||||
}
|
||||
if (props.exactHash && linkRoute.hash !== route.hash) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (props.exact && isExactActive) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!props.exact && isActive) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function resolveLinkClass({ route, isActive, isExactActive }: any) {
|
||||
const active = isLinkActive({ route, isActive, isExactActive })
|
||||
|
||||
if (props.raw) {
|
||||
return [props.class, active ? props.activeClass : props.inactiveClass]
|
||||
}
|
||||
|
||||
return ui.value({ class: props.class, active, disabled: props.disabled })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink v-slot="{ href, navigate, route: linkRoute, isActive, isExactActive }" v-bind="routerLinkProps" :to="to || '#'" custom>
|
||||
<template v-if="custom">
|
||||
<slot
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
as,
|
||||
type,
|
||||
disabled,
|
||||
href: to ? (isExternal ? to as string : href) : undefined,
|
||||
navigate,
|
||||
active: isLinkActive({ route: linkRoute, isActive, isExactActive })
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<ULinkBase
|
||||
v-else
|
||||
v-bind="{
|
||||
...$attrs,
|
||||
as,
|
||||
type,
|
||||
disabled,
|
||||
href: to ? (isExternal ? to as string : href) : undefined,
|
||||
navigate
|
||||
}"
|
||||
:class="resolveLinkClass({ route: linkRoute, isActive: isActive, isExactActive: isExactActive })"
|
||||
>
|
||||
<slot :active="isLinkActive({ route: linkRoute, isActive, isExactActive })" />
|
||||
</ULinkBase>
|
||||
</RouterLink>
|
||||
</template>
|
||||
8
src/runtime/vue/plugins/color-mode.ts
Normal file
8
src/runtime/vue/plugins/color-mode.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { useDark } from '@vueuse/core'
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
export default {
|
||||
install() {
|
||||
useDark()
|
||||
}
|
||||
} satisfies Plugin
|
||||
8
src/runtime/vue/plugins/head.ts
Normal file
8
src/runtime/vue/plugins/head.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createHead } from '@unhead/vue'
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
app.use(createHead())
|
||||
}
|
||||
} satisfies Plugin
|
||||
36
src/runtime/vue/stubs.ts
Normal file
36
src/runtime/vue/stubs.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ref } from 'vue'
|
||||
import type { Ref, Plugin as VuePlugin } from 'vue'
|
||||
|
||||
import appConfig from '#build/app.config'
|
||||
import type { NuxtApp } from '#app'
|
||||
|
||||
export { useHead } from '@unhead/vue'
|
||||
export { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
export const useAppConfig = () => appConfig
|
||||
|
||||
const state: Record<string, any> = {}
|
||||
|
||||
export const useState = <T>(key: string, init: () => T): Ref<T> => {
|
||||
if (state[key]) {
|
||||
return state[key] as Ref<T>
|
||||
}
|
||||
const value = ref(init())
|
||||
state[key] = value
|
||||
return value as Ref<T>
|
||||
}
|
||||
|
||||
export function useNuxtApp() {
|
||||
return {
|
||||
isHydrating: true,
|
||||
payload: { serverRendered: false }
|
||||
}
|
||||
}
|
||||
|
||||
export function defineNuxtPlugin(plugin: (nuxtApp: NuxtApp) => void) {
|
||||
return {
|
||||
install(app) {
|
||||
plugin({ vueApp: app } as NuxtApp)
|
||||
}
|
||||
} satisfies VuePlugin
|
||||
}
|
||||
Reference in New Issue
Block a user