Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions docs/content/docs/1.getting-started/2.installation/2.vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default defineConfig({
}
}),
ui({
inertia: true
router: 'inertia'
})
]
})
Expand All @@ -105,7 +105,7 @@ export default defineConfig({
inertia(),
vue(),
ui({
inertia: true
router: 'inertia'
})
]
})
Expand Down Expand Up @@ -658,10 +658,36 @@ export default defineConfig({
})
```

### `inertia`
### `router`

Use the `router` option to configure the routing integration mode.

- Default: `true`{lang="ts-type"}
- Values: `true`{lang="ts-type"} (vue-router), `false`{lang="ts-type"} (no routing), `'inertia'`{lang="ts-type"} (Inertia.js)

```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
plugins: [
vue(),
ui({
router: false,
})
]
})
```

### `inertia` ::badge{label="Deprecated" color="warning"}

Use the `inertia` option to enable compatibility with [Inertia.js](https://inertiajs.com/).

::warning
This option is deprecated. Use `router: 'inertia'` instead.
::

```ts [vite.config.ts]
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
Expand All @@ -671,7 +697,7 @@ export default defineConfig({
plugins: [
vue(),
ui({
inertia: true
inertia: true,
})
]
})
Expand Down
20 changes: 15 additions & 5 deletions src/plugins/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AutoImportComponents from 'unplugin-vue-components'
import type { Options as ComponentsOptions } from 'unplugin-vue-components/types'
import type { NuxtUIOptions } from '../unplugin'
import { runtimeDir } from '../unplugin'
import { resolveRouterMode } from '../utils/router'

/**
* This plugin adds all the Nuxt UI components as auto-imports.
Expand Down Expand Up @@ -39,11 +40,14 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
return [componentName, c]
}))

const inertiaOverrides = globSync('**/*.vue', {
cwd: join(runtimeDir, 'inertia/components')
})
const inertiaOverrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'inertia/components') })
const inertiaOverrideNames = new Set(inertiaOverrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))

const noRouterOverrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'no-router/components') })
const noRouterOverrideNames = new Set(noRouterOverrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))

const routerMode = resolveRouterMode(options)

const pluginOptions = defu(options.components, <ComponentsOptions>{
dts: options.dts ?? true,
exclude: [
Expand All @@ -53,7 +57,10 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
],
resolvers: [
(componentName) => {
if (options.inertia && inertiaOverrideNames.has(componentName)) {
if (routerMode === 'none' && noRouterOverrideNames.has(componentName)) {
return { name: 'default', from: join(runtimeDir, 'no-router/components', `${componentName.slice(options.prefix.length)}.vue`) }
}
if (routerMode === 'inertia' && inertiaOverrideNames.has(componentName)) {
return { name: 'default', from: join(runtimeDir, 'inertia/components', `${componentName.slice(options.prefix.length)}.vue`) }
}
if (overrideNames.has(componentName)) {
Expand Down Expand Up @@ -88,7 +95,10 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
}

const filename = id.match(/([^/]+)\.vue$/)?.[1]
if (filename && options.inertia && inertiaOverrideNames.has(`${options.prefix}${filename}`)) {
if (filename && routerMode === 'none' && noRouterOverrideNames.has(`${options.prefix}${filename}`)) {
return join(runtimeDir, 'no-router/components', `${filename}.vue`)
}
if (filename && routerMode === 'inertia' && inertiaOverrideNames.has(`${options.prefix}${filename}`)) {
return join(runtimeDir, 'inertia/components', `${filename}.vue`)
}
if (filename && overrideNames.has(`${options.prefix}${filename}`)) {
Expand Down
10 changes: 9 additions & 1 deletion src/plugins/nuxt-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ import { resolvePathSync } from 'mlly'
import type { UnpluginOptions } from 'unplugin'
import type { NuxtUIOptions } from '../unplugin'
import { runtimeDir } from '../unplugin'
import { resolveRouterMode } from '../utils/router'

/**
* This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components.
*/
export default function NuxtEnvironmentPlugin(options: NuxtUIOptions) {
const stubPath = resolvePathSync(options.inertia ? '../runtime/inertia/stubs' : '../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
const routerMode = resolveRouterMode(options)
const stubsPath = routerMode === 'inertia'
? '../runtime/inertia/stubs'
: routerMode === 'none'
? '../runtime/no-router/stubs'
: '../runtime/vue/stubs'

const stubPath = resolvePathSync(stubsPath, { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })

return {
name: 'nuxt:ui',
Expand Down
193 changes: 193 additions & 0 deletions src/runtime/no-router/components/Link.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<script lang="ts">
import type { ButtonHTMLAttributes } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/link'
import type { ComponentConfig } from '../../types/tv'

type Link = ComponentConfig<typeof theme, AppConfig, 'link'>

interface BaseLinkProps {
/**
* Route Location the link should navigate to when clicked on.
*/
to?: string
/**
* An alias for `to`. If used with `to`, `href` will be ignored
*/
href?: string
/**
* 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
}

export interface LinkProps extends BaseLinkProps {
/**
* 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
/** Allows controlling how the current route query sets the link as active. */
exactQuery?: boolean | 'partial'
/** 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
/** The class to apply when the link is active. */
activeClass?: string
/** The value of the `aria-current` attribute when the link is active. */
ariaCurrentValue?: 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 { defu } from 'defu'
import { hasProtocol } from 'ufo'
import { useAppConfig } from '#imports'
import { mergeClasses } from '../../utils'
import { tv } from '../../utils/tv'
import ULinkBase from '../../components/LinkBase.vue'

defineOptions({ inheritAttrs: false })

const props = withDefaults(defineProps<LinkProps>(), {
as: 'button',
type: 'button',
ariaCurrentValue: 'page',
active: undefined
})
defineSlots<LinkSlots>()

const appConfig = useAppConfig() as Link['AppConfig']

const ui = computed(() => tv({
extend: tv(theme),
...defu({
variants: {
active: {
true: mergeClasses(appConfig.ui?.link?.variants?.active?.true, props.activeClass),
false: mergeClasses(appConfig.ui?.link?.variants?.active?.false, props.inactiveClass)
}
}
}, appConfig.ui?.link || {})
}))

const href = computed(() => props.to ?? props.href)

const isExternal = computed(() => {
if (props.target === '_blank') {
return true
}

if (props.external) {
return true
}

if (!href.value) {
return false
}

return hasProtocol(href.value, { acceptRelative: true })
})

const isLinkActive = computed(() => {
if (props.active !== undefined) {
return props.active
}

return false
})

const linkClass = computed(() => {
const active = isLinkActive.value

if (props.raw) {
return [props.class, active ? props.activeClass : props.inactiveClass]
}

return ui.value({ class: props.class, active, disabled: props.disabled })
})

const linkRel = computed(() => {
if (props.noRel) {
return null
}

if (props.rel) {
return props.rel
}

if (isExternal.value) {
return 'noopener noreferrer'
}

return null
})
</script>

<template>
<template v-if="custom">
<slot
v-bind="{
...$attrs,
as,
type,
disabled,
href: href,
navigate: undefined,
rel: linkRel,
target: target || (isExternal ? '_blank' : undefined),
isExternal,
active: isLinkActive
}"
/>
</template>
<ULinkBase
v-else
v-bind="{
...$attrs,
as,
type,
disabled,
href: href,
navigate: undefined,
rel: linkRel,
target: target || (isExternal ? '_blank' : undefined),
isExternal
}"
:class="linkClass"
>
<slot :active="isLinkActive" />
</ULinkBase>
</template>
61 changes: 61 additions & 0 deletions src/runtime/no-router/components/LinkBase.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script lang="ts">
import type { LinkProps } from '../../types'

export interface LinkBaseProps {
as?: string
type?: string
disabled?: boolean
onClick?: ((e: MouseEvent) => void | Promise<void>) | Array<((e: MouseEvent) => void | Promise<void>)>
href?: string
target?: LinkProps['target']
rel?: LinkProps['rel']
active?: boolean
isExternal?: boolean
}
</script>

<script setup lang="ts">
import { Primitive } from 'reka-ui'

const props = withDefaults(defineProps<LinkBaseProps>(), {
as: 'button',
type: 'button'
})

function onClickWrapper(e: MouseEvent) {
if (props.disabled) {
e.stopPropagation()
e.preventDefault()
return
}

if (props.onClick) {
for (const onClick of Array.isArray(props.onClick) ? props.onClick : [props.onClick]) {
onClick(e)
}
}
}
</script>

<template>
<Primitive
v-bind="href ? {
'as': 'a',
'href': disabled ? undefined : href,
'aria-disabled': disabled ? 'true' : undefined,
'role': disabled ? 'link' : undefined,
'tabindex': disabled ? -1 : undefined
} : as === 'button' ? {
as,
type,
disabled
} : {
as
}"
:rel="rel"
:target="target"
@click="onClickWrapper"
>
<slot />
</Primitive>
</template>
Loading
Loading