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
23 changes: 23 additions & 0 deletions docs/content/1.getting-started/2.installation/2.vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,29 @@ export default defineConfig({
When using this option, `vue-router` is not required as Inertia.js provides its own routing system. The components that would normally use `RouterLink` will automatically use Inertia's `InertiaLink` component instead.
::

### `router`

Use the `router` option to disable router integration entirely.

```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
})
]
})
```

::note
When this option is disabled, components that would normally integrate with routing (like `ULink`) will behave as regular anchor tags for external links or plain elements for internal navigation. This is useful for single-page applications without routing or when using custom routing solutions.
::

## Continuous Releases

Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.
Expand Down
1 change: 1 addition & 0 deletions src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const defaultOptions = {
prefix: 'U',
fonts: true,
colorMode: true,
router: true,
theme: {
colors: undefined,
transitions: true
Expand Down
6 changes: 6 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export interface ModuleOptions {
*/
colorMode?: boolean

/**
* Enable or disable router integration
* @defaultValue `true`
*/
router?: boolean

/**
* Customize how the theme is generated
* @link https://ui.nuxt.com/getting-started/theme
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
const inertiaOverrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'inertia/components') })
const inertiaOverrideNames = new Set(inertiaOverrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))

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

const pluginOptions = defu(options.components, <ComponentsOptions>{
dts: options.dts ?? true,
exclude: [
Expand All @@ -30,6 +33,9 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
],
resolvers: [
(componentName) => {
if (options.router === false && minimalOverrideNames.has(componentName)) {
return { name: 'default', from: join(runtimeDir, 'minimal/components', `${componentName.slice(options.prefix.length)}.vue`) }
}
if (options.inertia && inertiaOverrideNames.has(componentName)) {
return { name: 'default', from: join(runtimeDir, 'inertia/components', `${componentName.slice(options.prefix.length)}.vue`) }
}
Expand Down Expand Up @@ -63,6 +69,9 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
}

const filename = id.match(/([^/]+)\.vue$/)?.[1]
if (filename && options.router === false && minimalOverrideNames.has(`${options.prefix}${filename}`)) {
return join(runtimeDir, 'minimal/components', `${filename}.vue`)
}
if (filename && options.inertia && inertiaOverrideNames.has(`${options.prefix}${filename}`)) {
return join(runtimeDir, 'inertia/components', `${filename}.vue`)
}
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 @@ -10,7 +10,15 @@ import type { NuxtUIOptions } from '../unplugin'
* 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 })
let stubPath: string

if (options.router === false) {
stubPath = resolvePathSync('../runtime/minimal/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
} else if (options.inertia) {
stubPath = resolvePathSync('../runtime/inertia/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
} else {
stubPath = resolvePathSync('../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
}

return {
name: 'nuxt:ui',
Expand Down
212 changes: 212 additions & 0 deletions src/runtime/minimal/components/Link.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<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/utils'

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

interface NuxtLinkProps {
/**
* 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
/**
* 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
/**
* Allows passing additional attributes to the actual rendered link.
*/
linkAttrs?: Record<string, any>
ariaCurrentValue?: string
}

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 active. */
activeClass?: string
/** 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 { defu } from 'defu'
import { hasProtocol } from 'ufo'
import { useAppConfig } from '#imports'
import { tv } from '../../utils/tv'
import ULinkBase from '../../components/LinkBase.vue'

defineOptions({ inheritAttrs: false })

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

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

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

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

const isExternal = computed(() => {
if (props.external) {
return true
}

if (!to.value) {
return false
}

return typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true })
})

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

// Without router, we can't determine if link is active
return false
})

const rel = computed(() => {
if (props.noRel) {
return undefined
}

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

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

return undefined
})

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

return ui.value({
active: active.value,
disabled: !!props.disabled,
class: [props.class]
})
}
</script>

<template>
<template v-if="custom">
<slot
v-bind="{
...$attrs,
as,
type,
disabled,
href: to,
rel,
target: isExternal ? '_blank' : props.target,
isExternal,
active
}"
/>
</template>
<ULinkBase
v-else
v-bind="{
...$attrs,
as: to && !isExternal && !disabled ? 'a' : as,
type,
disabled,
href: to,
rel,
target: isExternal ? '_blank' : props.target,
isExternal
}"
:class="resolveLinkClass()"
>
<slot :active="active" />
</ULinkBase>
</template>
Loading